From a1c7c9cf7d728c13ecdb8f9f325418e7e544e5e6 Mon Sep 17 00:00:00 2001
From: fs
Date: Wed, 11 Mar 2026 21:38:15 +0100
Subject: [PATCH] Standardize global empty-state UI component
---
config/assets.php | 1 +
pages/admin/stats/index(default).phtml | 28 ++++++------
pages/admin/tenants/_form.phtml | 8 +++-
pages/admin/users/_form.phtml | 22 +++++++--
templates/partials/app-empty-state.phtml | 58 ++++++++++++++++++++++++
web/css/base/variables.base.css | 4 ++
web/css/components/app-empty-state.css | 44 ++++++++++++++++++
7 files changed, 147 insertions(+), 18 deletions(-)
create mode 100644 templates/partials/app-empty-state.phtml
create mode 100644 web/css/components/app-empty-state.css
diff --git a/config/assets.php b/config/assets.php
index 8711955..b5269d5 100644
--- a/config/assets.php
+++ b/config/assets.php
@@ -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',
diff --git a/pages/admin/stats/index(default).phtml b/pages/admin/stats/index(default).phtml
index 6e489cc..9471a6f 100644
--- a/pages/admin/stats/index(default).phtml
+++ b/pages/admin/stats/index(default).phtml
@@ -257,7 +257,7 @@ require templatePath('partials/app-breadcrumb.phtml');
-
+ t('No entries found'), 'size' => 'compact']; require templatePath('partials/app-empty-state.phtml'); ?>
@@ -291,7 +291,7 @@ require templatePath('partials/app-breadcrumb.phtml');
-
+ t('No entries found'), 'size' => 'compact']; require templatePath('partials/app-empty-state.phtml'); ?>
@@ -546,7 +546,7 @@ require templatePath('partials/app-breadcrumb.phtml');
-
+ t('No entries found'), 'size' => 'compact']; require templatePath('partials/app-empty-state.phtml'); ?>
@@ -569,12 +569,12 @@ require templatePath('partials/app-breadcrumb.phtml');
-
+ t('No entries found'), 'size' => 'compact']; require templatePath('partials/app-empty-state.phtml'); ?>
-
+ t('No entries found'), 'size' => 'compact']; require templatePath('partials/app-empty-state.phtml'); ?>
@@ -625,7 +625,7 @@ require templatePath('partials/app-breadcrumb.phtml');
-
+ t('No entries found'), 'size' => 'compact']; require templatePath('partials/app-empty-state.phtml'); ?>
@@ -650,12 +650,12 @@ require templatePath('partials/app-breadcrumb.phtml');
-
+ t('No entries found'), 'size' => 'compact']; require templatePath('partials/app-empty-state.phtml'); ?>
-
+ t('No entries found'), 'size' => 'compact']; require templatePath('partials/app-empty-state.phtml'); ?>
@@ -806,7 +806,7 @@ require templatePath('partials/app-breadcrumb.phtml');
-
+ t('No entries found'), 'size' => 'compact']; require templatePath('partials/app-empty-state.phtml'); ?>
@@ -916,7 +916,7 @@ require templatePath('partials/app-breadcrumb.phtml');
-
+ t('No entries found'), 'size' => 'compact']; require templatePath('partials/app-empty-state.phtml'); ?>
@@ -948,7 +948,7 @@ require templatePath('partials/app-breadcrumb.phtml');
-
+ t('No entries found'), 'size' => 'compact']; require templatePath('partials/app-empty-state.phtml'); ?>
@@ -1068,7 +1068,7 @@ require templatePath('partials/app-breadcrumb.phtml');
-
+ t('No entries found'), 'size' => 'compact']; require templatePath('partials/app-empty-state.phtml'); ?>
@@ -1109,7 +1109,7 @@ require templatePath('partials/app-breadcrumb.phtml');
-
+ t('No entries found'), 'size' => 'compact']; require templatePath('partials/app-empty-state.phtml'); ?>
@@ -1137,7 +1137,7 @@ require templatePath('partials/app-breadcrumb.phtml');
-
+ t('No entries found'), 'size' => 'compact']; require templatePath('partials/app-empty-state.phtml'); ?>
diff --git a/pages/admin/tenants/_form.phtml b/pages/admin/tenants/_form.phtml
index 3cb9f33..aa77a8d 100644
--- a/pages/admin/tenants/_form.phtml
+++ b/pages/admin/tenants/_form.phtml
@@ -282,7 +282,13 @@ $ssoSyncNeedsGraph = !empty($ssoUiState['sync_needs_graph']);
-
+ t('No custom fields configured yet'),
+ 'size' => 'compact',
+ ];
+ require templatePath('partials/app-empty-state.phtml');
+ ?>
+
$tenant): ?>
+
+
+
-
+ t('No custom fields configured for this tenant'),
+ 'size' => 'compact',
+ ];
+ require templatePath('partials/app-empty-state.phtml');
+ ?>
@@ -829,8 +839,14 @@ foreach ($tenants as $tenant) {
-
-
+
+ t('No custom fields configured for selected tenants'),
+ 'size' => 'compact',
+ ];
+ require templatePath('partials/app-empty-state.phtml');
+ ?>
diff --git a/templates/partials/app-empty-state.phtml b/templates/partials/app-empty-state.phtml
new file mode 100644
index 0000000..dc32266
--- /dev/null
+++ b/templates/partials/app-empty-state.phtml
@@ -0,0 +1,58 @@
+ $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 !== '');
+
+?>
+
diff --git a/web/css/base/variables.base.css b/web/css/base/variables.base.css
index d95132e..8d10d5b 100644
--- a/web/css/base/variables.base.css
+++ b/web/css/base/variables.base.css
@@ -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);
diff --git a/web/css/components/app-empty-state.css b/web/css/components/app-empty-state.css
new file mode 100644
index 0000000..ee2b5f4
--- /dev/null
+++ b/web/css/components/app-empty-state.css
@@ -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;
+ }
+}