Standardize global empty-state UI component

This commit is contained in:
2026-03-11 21:38:15 +01:00
parent 51e9a5c19a
commit a1c7c9cf7d
7 changed files with 147 additions and 18 deletions

View File

@@ -28,6 +28,7 @@ return [
'css/components/app-details-titlebar.css',
'css/components/app-details-validation-summary.css',
'css/components/app-dashboard-titlebar.css',
'css/components/app-empty-state.css',
'css/components/app-details.css',
'css/components/app-details-card.css',
'css/components/app-list-table.css',

View File

@@ -257,7 +257,7 @@ require templatePath('partials/app-breadcrumb.phtml');
</tbody>
</table>
<?php else: ?>
<small class="muted"><?php e(t('No entries found')); ?></small>
<?php $emptyState = ['message' => t('No entries found'), 'size' => 'compact']; require templatePath('partials/app-empty-state.phtml'); ?>
<?php endif; ?>
</div>
<div class="app-stats-table">
@@ -291,7 +291,7 @@ require templatePath('partials/app-breadcrumb.phtml');
</tbody>
</table>
<?php else: ?>
<small class="muted"><?php e(t('No entries found')); ?></small>
<?php $emptyState = ['message' => t('No entries found'), 'size' => 'compact']; require templatePath('partials/app-empty-state.phtml'); ?>
<?php endif; ?>
</div>
</div>
@@ -546,7 +546,7 @@ require templatePath('partials/app-breadcrumb.phtml');
</table>
<?php else: ?>
<div class="app-stats-table-empty">
<small class="muted"><?php e(t('No entries found')); ?></small>
<?php $emptyState = ['message' => t('No entries found'), 'size' => 'compact']; require templatePath('partials/app-empty-state.phtml'); ?>
</div>
<?php endif; ?>
<?php elseif ($canViewUserLifecycleAudit && $userLifecycleAuditStatsAvailable): ?>
@@ -569,12 +569,12 @@ require templatePath('partials/app-breadcrumb.phtml');
</table>
<?php else: ?>
<div class="app-stats-table-empty">
<small class="muted"><?php e(t('No entries found')); ?></small>
<?php $emptyState = ['message' => t('No entries found'), 'size' => 'compact']; require templatePath('partials/app-empty-state.phtml'); ?>
</div>
<?php endif; ?>
<?php else: ?>
<div class="app-stats-table-empty">
<small class="muted"><?php e(t('No entries found')); ?></small>
<?php $emptyState = ['message' => t('No entries found'), 'size' => 'compact']; require templatePath('partials/app-empty-state.phtml'); ?>
</div>
<?php endif; ?>
</div>
@@ -625,7 +625,7 @@ require templatePath('partials/app-breadcrumb.phtml');
</table>
<?php else: ?>
<div class="app-stats-table-empty">
<small class="muted"><?php e(t('No entries found')); ?></small>
<?php $emptyState = ['message' => t('No entries found'), 'size' => 'compact']; require templatePath('partials/app-empty-state.phtml'); ?>
</div>
<?php endif; ?>
<?php elseif ($canViewSystemAudit && $systemAuditStatsAvailable): ?>
@@ -650,12 +650,12 @@ require templatePath('partials/app-breadcrumb.phtml');
</table>
<?php else: ?>
<div class="app-stats-table-empty">
<small class="muted"><?php e(t('No entries found')); ?></small>
<?php $emptyState = ['message' => t('No entries found'), 'size' => 'compact']; require templatePath('partials/app-empty-state.phtml'); ?>
</div>
<?php endif; ?>
<?php else: ?>
<div class="app-stats-table-empty">
<small class="muted"><?php e(t('No entries found')); ?></small>
<?php $emptyState = ['message' => t('No entries found'), 'size' => 'compact']; require templatePath('partials/app-empty-state.phtml'); ?>
</div>
<?php endif; ?>
</div>
@@ -806,7 +806,7 @@ require templatePath('partials/app-breadcrumb.phtml');
</table>
<?php else: ?>
<div class="app-stats-table-empty">
<small class="muted"><?php e(t('No entries found')); ?></small>
<?php $emptyState = ['message' => t('No entries found'), 'size' => 'compact']; require templatePath('partials/app-empty-state.phtml'); ?>
</div>
<?php endif; ?>
<div class="app-stats-table-explanation">
@@ -916,7 +916,7 @@ require templatePath('partials/app-breadcrumb.phtml');
</table>
<?php else: ?>
<div class="app-stats-table-empty">
<small class="muted"><?php e(t('No entries found')); ?></small>
<?php $emptyState = ['message' => t('No entries found'), 'size' => 'compact']; require templatePath('partials/app-empty-state.phtml'); ?>
</div>
<?php endif; ?>
</div>
@@ -948,7 +948,7 @@ require templatePath('partials/app-breadcrumb.phtml');
</table>
<?php else: ?>
<div class="app-stats-table-empty">
<small class="muted"><?php e(t('No entries found')); ?></small>
<?php $emptyState = ['message' => t('No entries found'), 'size' => 'compact']; require templatePath('partials/app-empty-state.phtml'); ?>
</div>
<?php endif; ?>
</div>
@@ -1068,7 +1068,7 @@ require templatePath('partials/app-breadcrumb.phtml');
</table>
<?php else: ?>
<div class="app-stats-table-empty">
<small class="muted"><?php e(t('No entries found')); ?></small>
<?php $emptyState = ['message' => t('No entries found'), 'size' => 'compact']; require templatePath('partials/app-empty-state.phtml'); ?>
</div>
<?php endif; ?>
</div>
@@ -1109,7 +1109,7 @@ require templatePath('partials/app-breadcrumb.phtml');
</table>
<?php else: ?>
<div class="app-stats-table-empty">
<small class="muted"><?php e(t('No entries found')); ?></small>
<?php $emptyState = ['message' => t('No entries found'), 'size' => 'compact']; require templatePath('partials/app-empty-state.phtml'); ?>
</div>
<?php endif; ?>
</div>
@@ -1137,7 +1137,7 @@ require templatePath('partials/app-breadcrumb.phtml');
</table>
<?php else: ?>
<div class="app-stats-table-empty">
<small class="muted"><?php e(t('No entries found')); ?></small>
<?php $emptyState = ['message' => t('No entries found'), 'size' => 'compact']; require templatePath('partials/app-empty-state.phtml'); ?>
</div>
<?php endif; ?>
</div>

View File

@@ -282,7 +282,13 @@ $ssoSyncNeedsGraph = !empty($ssoUiState['sync_needs_graph']);
</details>
<hr>
<?php if (!$customFieldDefinitions): ?>
<p class="muted"><?php e(t('No custom fields configured yet')); ?></p>
<?php
$emptyState = [
'message' => t('No custom fields configured yet'),
'size' => 'compact',
];
require templatePath('partials/app-empty-state.phtml');
?>
<?php endif; ?>
<?php foreach ($customFieldDefinitions as $definition): ?>
<?php

View File

@@ -678,15 +678,25 @@ foreach ($tenants as $tenant) {
<p class="muted"><?php e(t('Assign tenants first to edit custom fields')); ?></p>
<?php else: ?>
<?php $hasRenderedCustomFields = false; ?>
<?php $hasConfiguredTenantDefinitions = false; ?>
<?php foreach ($customFieldTenantMap as $tenantId => $tenant): ?>
<?php $tenantDefinitions = $customFieldDefinitionsByTenant[$tenantId] ?? []; ?>
<?php if ($tenantDefinitions): ?>
<?php $hasConfiguredTenantDefinitions = true; ?>
<?php endif; ?>
<details name="custom-fields-tenant" data-details-key="custom-fields-tenant-<?php e((string) $tenantId); ?>">
<summary>
<?php e((string) ($tenant['description'] ?? t('Custom fields for tenant'))); ?>
</summary>
<hr>
<?php if (!$tenantDefinitions): ?>
<p class="muted"><?php e(t('No custom fields configured for this tenant')); ?></p>
<?php
$emptyState = [
'message' => t('No custom fields configured for this tenant'),
'size' => 'compact',
];
require templatePath('partials/app-empty-state.phtml');
?>
<?php else: ?>
<div>
<?php foreach ($tenantDefinitions as $definition): ?>
@@ -829,8 +839,14 @@ foreach ($tenants as $tenant) {
</details>
<hr>
<?php endforeach; ?>
<?php if (!$hasRenderedCustomFields): ?>
<p class="muted"><?php e(t('No custom fields configured for selected tenants')); ?></p>
<?php if (!$hasRenderedCustomFields && $hasConfiguredTenantDefinitions): ?>
<?php
$emptyState = [
'message' => t('No custom fields configured for selected tenants'),
'size' => 'compact',
];
require templatePath('partials/app-empty-state.phtml');
?>
<?php endif; ?>
<?php endif; ?>
</div>

View File

@@ -0,0 +1,58 @@
<?php
/**
* Shared empty-state component.
*
* @var array<string, mixed> $emptyState
*/
$emptyState = is_array($emptyState ?? null) ? $emptyState : [];
$emptyMessage = trim((string) ($emptyState['message'] ?? ''));
if ($emptyMessage === '') {
return;
}
$emptyHint = trim((string) ($emptyState['hint'] ?? ''));
$emptySize = strtolower(trim((string) ($emptyState['size'] ?? 'default')));
if (!in_array($emptySize, ['default', 'compact'], true)) {
$emptySize = 'default';
}
$emptyAlign = strtolower(trim((string) ($emptyState['align'] ?? 'center')));
if (!in_array($emptyAlign, ['center', 'left'], true)) {
$emptyAlign = 'center';
}
$emptyAction = is_array($emptyState['action'] ?? null) ? $emptyState['action'] : [];
$emptyActionLabel = trim((string) ($emptyAction['label'] ?? ''));
$emptyActionHref = trim((string) ($emptyAction['href'] ?? ''));
$emptyActionForm = trim((string) ($emptyAction['form'] ?? ''));
$emptyActionClass = trim((string) ($emptyAction['class'] ?? 'secondary outline small'));
$emptyActionTarget = trim((string) ($emptyAction['target'] ?? ''));
$emptyActionRel = trim((string) ($emptyAction['rel'] ?? ''));
$showAction = $emptyActionLabel !== '' && ($emptyActionHref !== '' || $emptyActionForm !== '');
?>
<div class="app-empty-state" data-size="<?php e($emptySize); ?>" data-align="<?php e($emptyAlign); ?>">
<p class="app-empty-state-message"><?php e($emptyMessage); ?></p>
<?php if ($emptyHint !== ''): ?>
<small class="app-empty-state-hint"><?php e($emptyHint); ?></small>
<?php endif; ?>
<?php if ($showAction): ?>
<?php if ($emptyActionHref !== ''): ?>
<a
role="button"
class="<?php e($emptyActionClass); ?> app-empty-state-action"
href="<?php e($emptyActionHref); ?>"
<?php if ($emptyActionTarget !== ''): ?>target="<?php e($emptyActionTarget); ?>"<?php endif; ?>
<?php if ($emptyActionRel !== ''): ?>rel="<?php e($emptyActionRel); ?>"<?php endif; ?>
>
<?php e($emptyActionLabel); ?>
</a>
<?php else: ?>
<button type="submit" class="<?php e($emptyActionClass); ?> app-empty-state-action" form="<?php e($emptyActionForm); ?>">
<?php e($emptyActionLabel); ?>
</button>
<?php endif; ?>
<?php endif; ?>
</div>

View File

@@ -311,6 +311,10 @@
--app-card-box-shadow: var(--app-box-shadow);
--app-card-sectioning-background-color: rgb(251, 251.5, 252.25);
--app-details-card-open-summary-bg: var(--app-card-sectioning-background-color);
--app-empty-border-color: var(--app-muted-border-color);
--app-empty-bg: var(--app-card-background-color);
--app-empty-text: var(--app-color);
--app-empty-hint: var(--app-muted-color);
--app-dropdown-background-color: #fff;
--app-dropdown-border-color: #eff1f4;
--app-dropdown-box-shadow: var(--app-box-shadow);

View File

@@ -0,0 +1,44 @@
@layer components {
.app-empty-state {
border: 1px dashed var(--app-empty-border-color);
border-radius: var(--app-border-radius);
background: var(--app-empty-bg);
color: var(--app-empty-text);
padding: calc(var(--app-spacing) * 0.875) var(--app-spacing);
display: grid;
gap: calc(var(--app-spacing) * 0.35);
text-align: center;
}
.app-empty-state[data-size="compact"] {
padding: calc(var(--app-spacing) * 0.625) calc(var(--app-spacing) * 0.75);
gap: calc(var(--app-spacing) * 0.25);
}
.app-empty-state[data-align="left"] {
text-align: left;
}
.app-empty-state-message {
margin: 0;
color: var(--app-empty-text);
font-size: 0.95rem;
line-height: 1.4;
}
.app-empty-state-hint {
margin: 0;
color: var(--app-empty-hint);
font-size: 0.85rem;
line-height: 1.3;
}
.app-empty-state-action {
justify-self: center;
margin-top: calc(var(--app-spacing) * 0.15);
}
.app-empty-state[data-align="left"] .app-empty-state-action {
justify-self: start;
}
}