@adia-ai/web-modules 0.8.36 → 0.8.37

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (89) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/README.md +3 -3
  3. package/agent-admin/admin-roster/admin-roster.js +1 -1
  4. package/agent-admin/admin-settings/admin-settings.js +1 -1
  5. package/agent-admin/agent-admin/agent-admin.js +1 -1
  6. package/billing/billing-overview/billing-overview.a2ui.json +281 -0
  7. package/billing/billing-overview/billing-overview.class.js +838 -0
  8. package/billing/billing-overview/billing-overview.css +223 -0
  9. package/billing/billing-overview/billing-overview.js +16 -0
  10. package/billing/billing-overview/billing-overview.yaml +441 -0
  11. package/billing/index.js +21 -0
  12. package/billing/invoice-detail/invoice-detail.a2ui.json +276 -0
  13. package/billing/invoice-detail/invoice-detail.class.js +711 -0
  14. package/billing/invoice-detail/invoice-detail.css +305 -0
  15. package/billing/invoice-detail/invoice-detail.js +16 -0
  16. package/billing/invoice-detail/invoice-detail.yaml +411 -0
  17. package/billing/invoice-history/invoice-history.a2ui.json +315 -0
  18. package/billing/invoice-history/invoice-history.class.js +672 -0
  19. package/billing/invoice-history/invoice-history.css +130 -0
  20. package/billing/invoice-history/invoice-history.js +16 -0
  21. package/billing/invoice-history/invoice-history.yaml +398 -0
  22. package/billing/payment-method-form/payment-method-form.a2ui.json +310 -0
  23. package/billing/payment-method-form/payment-method-form.class.js +886 -0
  24. package/billing/payment-method-form/payment-method-form.css +149 -0
  25. package/billing/payment-method-form/payment-method-form.js +16 -0
  26. package/billing/payment-method-form/payment-method-form.yaml +430 -0
  27. package/billing/payment-method-list/payment-method-list.a2ui.json +283 -0
  28. package/billing/payment-method-list/payment-method-list.class.js +705 -0
  29. package/billing/payment-method-list/payment-method-list.css +128 -0
  30. package/billing/payment-method-list/payment-method-list.js +16 -0
  31. package/billing/payment-method-list/payment-method-list.yaml +355 -0
  32. package/billing/plan-picker/plan-picker.a2ui.json +349 -0
  33. package/billing/plan-picker/plan-picker.class.js +635 -0
  34. package/billing/plan-picker/plan-picker.css +227 -0
  35. package/billing/plan-picker/plan-picker.js +16 -0
  36. package/billing/plan-picker/plan-picker.yaml +453 -0
  37. package/chat/README.md +1 -1
  38. package/chat/chat-composer/chat-composer.js +2 -2
  39. package/chat/chat-empty/chat-empty.js +2 -2
  40. package/chat/chat-shell/chat-shell.js +4 -4
  41. package/chat/chat-sidebar/chat-sidebar.js +2 -2
  42. package/chat/chat-surfaces/chat-surfaces.js +2 -2
  43. package/chat/chat-thread/chat-thread.js +2 -2
  44. package/dashboard/dashboard-layout/dashboard-layout.a2ui.json +254 -0
  45. package/dashboard/dashboard-layout/dashboard-layout.css +190 -0
  46. package/dashboard/dashboard-layout/dashboard-layout.js +289 -0
  47. package/dashboard/dashboard-layout/dashboard-layout.yaml +374 -0
  48. package/dashboard/date-range-selector/date-range-selector.a2ui.json +236 -0
  49. package/dashboard/date-range-selector/date-range-selector.css +84 -0
  50. package/dashboard/date-range-selector/date-range-selector.js +686 -0
  51. package/dashboard/date-range-selector/date-range-selector.yaml +358 -0
  52. package/dashboard/index.js +14 -0
  53. package/dist/everything.min.js +1 -1
  54. package/editor/editor-canvas/editor-canvas.js +2 -2
  55. package/editor/editor-shell/editor-shell.js +2 -2
  56. package/editor/editor-sidebar/editor-sidebar.js +2 -2
  57. package/editor/editor-toolbar/editor-toolbar.js +2 -2
  58. package/feedback/confirm-dialog/confirm-dialog.a2ui.json +206 -0
  59. package/feedback/confirm-dialog/confirm-dialog.css +157 -0
  60. package/feedback/confirm-dialog/confirm-dialog.js +383 -0
  61. package/feedback/confirm-dialog/confirm-dialog.yaml +297 -0
  62. package/feedback/index.js +10 -0
  63. package/form/form-popover/form-popover.js +1 -1
  64. package/generative/index.d.ts +1 -1
  65. package/generative/index.js +2 -2
  66. package/onboarding/index.js +1 -0
  67. package/onboarding/onboarding-checklist/onboarding-checklist.a2ui.json +263 -0
  68. package/onboarding/onboarding-checklist/onboarding-checklist.css +157 -0
  69. package/onboarding/onboarding-checklist/onboarding-checklist.js +428 -0
  70. package/onboarding/onboarding-checklist/onboarding-checklist.yaml +236 -0
  71. package/package.json +99 -6
  72. package/runtime/a2ui-root/a2ui-root.js +3 -3
  73. package/runtime/gen-root/gen-root.js +5 -5
  74. package/settings/index.js +12 -0
  75. package/settings/integrations-page/integrations-page.a2ui.json +274 -0
  76. package/settings/integrations-page/integrations-page.css +177 -0
  77. package/settings/integrations-page/integrations-page.d.ts +77 -0
  78. package/settings/integrations-page/integrations-page.js +413 -0
  79. package/settings/integrations-page/integrations-page.yaml +317 -0
  80. package/settings/notification-preferences/notification-preferences.a2ui.json +245 -0
  81. package/settings/notification-preferences/notification-preferences.css +204 -0
  82. package/settings/notification-preferences/notification-preferences.js +622 -0
  83. package/settings/notification-preferences/notification-preferences.yaml +358 -0
  84. package/shell/admin-command/admin-command.js +2 -2
  85. package/shell/admin-shell/admin-shell.js +2 -2
  86. package/shell/admin-sidebar/admin-sidebar.js +2 -2
  87. package/shell/embed-shell/embed-shell.js +3 -3
  88. package/simple/simple-shell/simple-shell.js +1 -1
  89. package/theme/theme-panel/theme-panel.js +2 -2
@@ -0,0 +1,289 @@
1
+ /**
2
+ * <dashboard-layout-ui> — Module-tier dashboard-body composite.
3
+ *
4
+ * Crystallizes the recurring "toolbar + KPI grid + chart area + table"
5
+ * dashboard layout into four named regions:
6
+ *
7
+ * <dashboard-layout-ui density="" kpi-columns="4" chart-split="2:1">
8
+ * <row-ui slot="toolbar">…</row-ui>
9
+ * <div slot="kpis">…<card-ui><stat-ui/></card-ui>…</div>
10
+ * <div slot="charts">…<card-ui><chart-ui/></card-ui>…</div>
11
+ * <card-ui slot="table">…<table-ui/>…</card-ui>
12
+ * <card-ui slot="aside">…</card-ui> <!-- optional -->
13
+ * </dashboard-layout-ui>
14
+ *
15
+ * Per ADR-0033 (Light-DOM substrate) — slot= attributes are decorative
16
+ * metadata; CSS rules in dashboard-layout.css position bands via
17
+ * `:scope > [slot="X"]` selectors. The composite stamps no internal
18
+ * DOM (zero-parts strategy, same shape as <visually-hidden-ui> /
19
+ * <admin-page-body>); every rendered band is consumer-supplied.
20
+ *
21
+ * Per ADR-0027 — composites that programmatically reference primitives
22
+ * do NOT auto-import them. Consumer (or demo shell) must explicitly
23
+ * import <card-ui>, <grid-ui>, <stat-ui>, <chart-ui>, <table-ui>.
24
+ *
25
+ * SPEC: .claude/docs/specs/implementation-ready/SPEC-018-dashboard-layout.md.
26
+ */
27
+
28
+ import { UIElement } from '@adia-ai/web-components/core/element';
29
+ import { untracked } from '@adia-ai/web-components/core/signals';
30
+
31
+ // ── Validation tables ────────────────────────────────────────────
32
+
33
+ const VALID_DENSITIES = new Set(['', 'comfortable', 'compact', 'spacious']);
34
+ const VALID_KPI_COLUMNS = new Set(['2', '3', '4', '5', '6']);
35
+ const VALID_CHART_SPLIT = new Set(['', '2', '2:1', '3:2', '3:1']);
36
+ const REGION_SLOTS = ['toolbar', 'kpis', 'charts', 'table', 'aside'];
37
+
38
+ // ── UIDashboardLayout ────────────────────────────────────────────
39
+
40
+ export class UIDashboardLayout extends UIElement {
41
+ static get properties() {
42
+ return {
43
+ density: { type: String, default: '', reflect: true },
44
+ 'kpi-columns': { type: String, default: '4', reflect: true, attribute: 'kpi-columns' },
45
+ 'chart-split': { type: String, default: '', reflect: true, attribute: 'chart-split' },
46
+ loading: { type: Boolean, default: false, reflect: true },
47
+ gap: { type: String, default: '', reflect: true },
48
+ };
49
+ }
50
+
51
+ // Zero-parts stamping strategy. The composite renders no internal
52
+ // DOM — all bands are consumer-supplied via named slots. Returning
53
+ // null prevents the base UIElement stamp() pipeline from running
54
+ // replaceChildren() and wiping the authored slotted children.
55
+ // Same pattern as <date-range-selector-ui> + <admin-page-body>.
56
+ static template = () => null;
57
+
58
+ // ── Internal state ──────────────────────────────────────────────
59
+
60
+ /** Tracks which regions had children at last observation. Used so
61
+ * region-empty fires only on transition (had children → empty),
62
+ * not on every mutation. */
63
+ #regionPopulated = { toolbar: false, kpis: false, charts: false, table: false, aside: false };
64
+
65
+ /** MutationObserver for descendant slot-change tracking. */
66
+ #observer = null;
67
+
68
+ /** Suppress nested attribute callbacks when the composite is doing
69
+ * an internal property write (e.g. clamping an out-of-range
70
+ * kpi-columns back to "4"). */
71
+ #internalUpdate = false;
72
+
73
+ // ── Public imperative API (SPEC §4 Methods) ─────────────────────
74
+
75
+ /**
76
+ * Programmatic equivalent of setting the [density] attribute.
77
+ * Fires `density-change` once with the new value.
78
+ *
79
+ * @param {string} value — '' | 'comfortable' | 'compact' | 'spacious'
80
+ */
81
+ setDensity(value) {
82
+ const next = VALID_DENSITIES.has(value) ? value : '';
83
+ if (this.density === next) return;
84
+ this.density = next;
85
+ // attributeChangedCallback below dispatches density-change.
86
+ }
87
+
88
+ /**
89
+ * Returns the immediate child element marked with the given slot, or
90
+ * null if no such child exists. The dashboard places each named slot
91
+ * via CSS grid-area, so consumers typically wrap multiple children
92
+ * in a single slot host (e.g. `<div slot="kpis">…N cards…</div>`).
93
+ *
94
+ * @param {string} name — toolbar | kpis | charts | table | aside
95
+ * @returns {HTMLElement | null}
96
+ */
97
+ getRegion(name) {
98
+ if (!REGION_SLOTS.includes(name)) return null;
99
+ return this.querySelector(`:scope > [slot="${name}"]`);
100
+ }
101
+
102
+ // ── Lifecycle ───────────────────────────────────────────────────
103
+
104
+ connected() {
105
+ // role="region" — the dashboard is a major landmark within the
106
+ // page main. Consumers supply aria-label (or aria-labelledby on
107
+ // an associated <admin-page-header heading>).
108
+ if (!this.hasAttribute('role')) {
109
+ this.setAttribute('role', 'region');
110
+ }
111
+
112
+ // Clamp out-of-range attributes to documented defaults so
113
+ // consumers using a2ui-generated markup don't end up with a
114
+ // misfiring container-query collapse on kpi-columns="8".
115
+ untracked(() => this.#clampProps());
116
+
117
+ // aria-busy mirrors [loading] for the first paint; subsequent
118
+ // changes flow through attributeChangedCallback below.
119
+ this.#syncAriaBusy();
120
+
121
+ // Apply the [gap] override → public token if set.
122
+ this.#applyGapAttr();
123
+
124
+ // Watch for region-empty transitions. Children are arbitrary
125
+ // (consumers might dynamically remove the last kpi card), so a
126
+ // MutationObserver on the host's childList captures structural
127
+ // changes regardless of which descendant fires.
128
+ this.#snapshotRegions();
129
+ // gh#285 — SSR DOM shims (linkedom) have no MutationObserver global.
130
+ if (typeof MutationObserver !== 'undefined') {
131
+ this.#observer = new MutationObserver((records) => this.#onMutate(records));
132
+ this.#observer.observe(this, { childList: true });
133
+ }
134
+ }
135
+
136
+ disconnected() {
137
+ if (this.#observer) {
138
+ this.#observer.disconnect();
139
+ this.#observer = null;
140
+ }
141
+ }
142
+
143
+ attributeChangedCallback(name, oldVal, newVal) {
144
+ super.attributeChangedCallback?.(name, oldVal, newVal);
145
+ if (this.#internalUpdate) return;
146
+
147
+ if (name === 'density' && oldVal !== newVal) {
148
+ // Clamp + emit density-change. attributeChangedCallback runs
149
+ // before connected() on declarative attributes, so guard the
150
+ // dispatch behind isConnected to keep first-paint quiet — the
151
+ // initial declarative value is just a starting state, not an
152
+ // event-worthy transition.
153
+ if (!VALID_DENSITIES.has(newVal)) {
154
+ this.#internalUpdate = true;
155
+ try { this.density = ''; } finally { this.#internalUpdate = false; }
156
+ return;
157
+ }
158
+ if (this.isConnected) {
159
+ this.dispatchEvent(new CustomEvent('density-change', {
160
+ bubbles: true,
161
+ detail: { density: newVal },
162
+ }));
163
+ }
164
+ return;
165
+ }
166
+
167
+ if (name === 'kpi-columns' && oldVal !== newVal) {
168
+ if (newVal != null && !VALID_KPI_COLUMNS.has(newVal)) {
169
+ // eslint-disable-next-line no-console
170
+ console.warn(
171
+ `[dashboard-layout-ui] kpi-columns="${newVal}" out of range — falling back to "4". ` +
172
+ `Use one of: ${[...VALID_KPI_COLUMNS].join(', ')}.`,
173
+ );
174
+ this.#internalUpdate = true;
175
+ try { this['kpi-columns'] = '4'; } finally { this.#internalUpdate = false; }
176
+ }
177
+ return;
178
+ }
179
+
180
+ if (name === 'chart-split' && oldVal !== newVal) {
181
+ if (newVal != null && !VALID_CHART_SPLIT.has(newVal)) {
182
+ // eslint-disable-next-line no-console
183
+ console.warn(
184
+ `[dashboard-layout-ui] chart-split="${newVal}" not in enum — falling back to "" (single column). ` +
185
+ `Use one of: "" (full), 2, 2:1, 3:2, 3:1.`,
186
+ );
187
+ this.#internalUpdate = true;
188
+ try { this['chart-split'] = ''; } finally { this.#internalUpdate = false; }
189
+ }
190
+ return;
191
+ }
192
+
193
+ if (name === 'loading' && oldVal !== newVal) {
194
+ this.#syncAriaBusy();
195
+ return;
196
+ }
197
+
198
+ if (name === 'gap' && oldVal !== newVal) {
199
+ this.#applyGapAttr();
200
+ return;
201
+ }
202
+ }
203
+
204
+ // ── Internal: prop clamping ─────────────────────────────────────
205
+
206
+ #clampProps() {
207
+ if (!VALID_DENSITIES.has(this.density)) {
208
+ this.#internalUpdate = true;
209
+ try { this.density = ''; } finally { this.#internalUpdate = false; }
210
+ }
211
+ if (!VALID_KPI_COLUMNS.has(this['kpi-columns'])) {
212
+ // eslint-disable-next-line no-console
213
+ console.warn(
214
+ `[dashboard-layout-ui] kpi-columns="${this['kpi-columns']}" out of range — clamping to "4".`,
215
+ );
216
+ this.#internalUpdate = true;
217
+ try { this['kpi-columns'] = '4'; } finally { this.#internalUpdate = false; }
218
+ }
219
+ if (!VALID_CHART_SPLIT.has(this['chart-split'])) {
220
+ // eslint-disable-next-line no-console
221
+ console.warn(
222
+ `[dashboard-layout-ui] chart-split="${this['chart-split']}" not in enum — clamping to "".`,
223
+ );
224
+ this.#internalUpdate = true;
225
+ try { this['chart-split'] = ''; } finally { this.#internalUpdate = false; }
226
+ }
227
+ }
228
+
229
+ // ── Internal: aria-busy + gap wiring ────────────────────────────
230
+
231
+ #syncAriaBusy() {
232
+ if (this.loading) {
233
+ this.setAttribute('aria-busy', 'true');
234
+ } else {
235
+ this.removeAttribute('aria-busy');
236
+ }
237
+ }
238
+
239
+ /**
240
+ * Map the [gap="N"] attribute to the public --dashboard-layout-gap
241
+ * custom property. The CSS reads `var(--dashboard-layout-gap, …)`
242
+ * so empty / unset falls back to the density-modulated default.
243
+ */
244
+ #applyGapAttr() {
245
+ const g = (this.gap || '').trim();
246
+ if (g === '') {
247
+ this.style.removeProperty('--dashboard-layout-gap');
248
+ return;
249
+ }
250
+ // Accept numeric 1..6 (mapped to --a-space-N) or pass through any
251
+ // explicit token-style value. Out-of-range numeric falls through
252
+ // to no-op so authors don't get a silent broken layout.
253
+ if (/^[1-6]$/.test(g)) {
254
+ this.style.setProperty('--dashboard-layout-gap', `var(--a-space-${g})`);
255
+ } else {
256
+ // Allow consumer to pass a literal var() / length; trust the input.
257
+ this.style.setProperty('--dashboard-layout-gap', g);
258
+ }
259
+ }
260
+
261
+ // ── Internal: region-empty transition tracking ──────────────────
262
+
263
+ #snapshotRegions() {
264
+ for (const slot of REGION_SLOTS) {
265
+ this.#regionPopulated[slot] = !!this.querySelector(`:scope > [slot="${slot}"]`);
266
+ }
267
+ }
268
+
269
+ #onMutate(_records) {
270
+ // Coarse-grained: re-check every region after any childList
271
+ // mutation. Cardinality is small (5 slots) and the operation is
272
+ // a single CSS selector lookup; cheap enough to avoid record diffing.
273
+ for (const slot of REGION_SLOTS) {
274
+ const hadContent = this.#regionPopulated[slot];
275
+ const hasContent = !!this.querySelector(`:scope > [slot="${slot}"]`);
276
+ if (hadContent && !hasContent) {
277
+ this.dispatchEvent(new CustomEvent('region-empty', {
278
+ bubbles: true,
279
+ detail: { region: slot },
280
+ }));
281
+ }
282
+ this.#regionPopulated[slot] = hasContent;
283
+ }
284
+ }
285
+ }
286
+
287
+ if (!customElements.get('dashboard-layout-ui')) {
288
+ customElements.define('dashboard-layout-ui', UIDashboardLayout);
289
+ }
@@ -0,0 +1,374 @@
1
+ # Edit this file; run `npm run build:components` to regenerate a2ui.json.
2
+ $schema: ../../../../scripts/schemas/component.yaml.schema.json
3
+ name: UIDashboardLayout
4
+ tag: dashboard-layout-ui
5
+ status: experimental
6
+ component: DashboardLayout
7
+ category: layout
8
+ version: 1
9
+ description: |
10
+ Module-tier dashboard-body composite. Crystallizes the recurring
11
+ "toolbar + KPI grid + chart area + table" dashboard layout into a
12
+ single primitive with four named regions (toolbar / kpis / charts /
13
+ table) plus an optional aside column. Each region is filled by
14
+ consumer-supplied <card-ui>s; the composite owns band sizing,
15
+ responsive breakpoints, and a [density] knob that propagates to
16
+ every descendant primitive via the parametric --a-density cascade.
17
+
18
+ Sits inside an <admin-page-body>; replaces the hand-rolled
19
+ <col-ui gap="4"> + nested <grid-ui> + <card-ui> chains every
20
+ analytics consumer reinvents today. Per ADR-0033 (Light-DOM
21
+ substrate) — slot= attributes are decorative metadata; CSS rules
22
+ match `:scope > [slot="X"]` to position bands in DOM order.
23
+
24
+ Spec: .claude/docs/specs/implementation-ready/SPEC-018-dashboard-layout.md.
25
+
26
+ # Per ADR-0027 — composites that programmatically create primitives
27
+ # do NOT auto-import them. The consumer (app shell or demo page) must
28
+ # explicitly import each composed primitive.
29
+ composes:
30
+ - card-ui
31
+ - grid-ui
32
+ - row-ui
33
+ - stat-ui
34
+ - chart-ui
35
+ - table-ui
36
+
37
+ props:
38
+ density:
39
+ description: |
40
+ Spacing scale — propagates --a-density to descendant primitives
41
+ via CSS custom-property cascade. Empty string inherits whatever
42
+ density is in effect on an ancestor (the default — respects an
43
+ app-level density theme). `compact` / `comfortable` / `spacious`
44
+ explicitly override.
45
+ type: string
46
+ default: ""
47
+ enum:
48
+ - ""
49
+ - comfortable
50
+ - compact
51
+ - spacious
52
+ reflect: true
53
+
54
+ kpi-columns:
55
+ description: |
56
+ Target column count for the KPI band at desktop width. Range 2-6;
57
+ out-of-range values fall back to 4. Container-query collapse
58
+ reduces this at narrower widths (≤48em → 2; ≤32em → 1).
59
+ type: string
60
+ default: "4"
61
+ enum:
62
+ - "2"
63
+ - "3"
64
+ - "4"
65
+ - "5"
66
+ - "6"
67
+ reflect: true
68
+
69
+ chart-split:
70
+ description: |
71
+ Chart-band column split. Empty string = single full-width chart;
72
+ `2` = two equal columns; `2:1` / `3:2` / `3:1` = asymmetric
73
+ ratios. Collapses to single column at narrow widths (≤48em).
74
+ type: string
75
+ default: ""
76
+ enum:
77
+ - ""
78
+ - "2"
79
+ - "2:1"
80
+ - "3:2"
81
+ - "3:1"
82
+ reflect: true
83
+
84
+ loading:
85
+ description: |
86
+ Marks the composite as data-loading. Sets aria-busy=true on the
87
+ host and dims slotted children to 60% opacity with pointer-events
88
+ suppressed. Skeletons inside slotted cards continue to render
89
+ normally; the dim is on top of them.
90
+ type: boolean
91
+ default: false
92
+ reflect: true
93
+
94
+ gap:
95
+ description: |
96
+ Override band-to-band vertical gap. Empty string = the
97
+ density-modulated default (--a-space-4 at comfortable). Numeric
98
+ `1`..`6` selects the corresponding --a-space-N step.
99
+ type: string
100
+ default: ""
101
+ reflect: true
102
+
103
+ events:
104
+ density-change:
105
+ description: |
106
+ Fired when [density] changes (programmatic or via setDensity()).
107
+ Bubbles; detail.density carries the new value.
108
+ detail:
109
+ density:
110
+ type: string
111
+ description: The new density value (comfortable / compact / spacious).
112
+
113
+ region-empty:
114
+ description: |
115
+ Fired when a slot transitions from having content to being empty
116
+ (consumer-driven removal of the last child for that slot).
117
+ Useful for parent-level adjust-layout logic.
118
+ detail:
119
+ region:
120
+ type: string
121
+ description: The slot name that became empty (toolbar / kpis / charts / table / aside).
122
+
123
+ slots:
124
+ toolbar:
125
+ description: |
126
+ Top band — filter chips, segmented date range, global controls.
127
+ Typically a single <row-ui> or <date-range-selector-ui> (SPEC-019).
128
+ Collapses when empty (no row reserved).
129
+
130
+ kpis:
131
+ description: |
132
+ KPI band — typically a wrapper element containing N <card-ui>
133
+ instances each with one <stat-ui>. The composite drives the
134
+ grid template via [kpi-columns]. Collapses when empty.
135
+
136
+ charts:
137
+ description: |
138
+ Chart band — wrapper containing 1-3 <card-ui> with <chart-ui>.
139
+ [chart-split] drives the grid columns. Collapses when empty.
140
+
141
+ table:
142
+ description: |
143
+ Table band — typically a single <card-ui slot="table"> containing
144
+ <table-ui> and optional <pagination-ui> in the footer. Full-bleed
145
+ across the main column. Collapses when empty.
146
+
147
+ aside:
148
+ description: |
149
+ Optional right-side panel (e.g., live activity feed). When set,
150
+ promotes the host to a 2-column grid with the aside as a sticky
151
+ right column on desktop. Collapses when empty.
152
+
153
+ states:
154
+ - name: idle
155
+ description: Default; all regions interactive.
156
+ - name: loading
157
+ description: Data fetch in progress; aria-busy=true; content dimmed.
158
+ attribute: loading
159
+ - name: compact
160
+ description: Compact-density variant; tighter band gaps + density cascade.
161
+ attribute: density
162
+ - name: spacious
163
+ description: Spacious-density variant; looser band gaps + density cascade.
164
+ attribute: density
165
+
166
+ traits: []
167
+
168
+ tokens:
169
+ --dashboard-layout-gap:
170
+ description: Band-to-band vertical gap.
171
+ default: var(--a-space-4)
172
+ --dashboard-layout-kpi-gap:
173
+ description: Inter-card gap inside the KPI band.
174
+ default: var(--a-space-2)
175
+ --dashboard-layout-chart-gap:
176
+ description: Inter-card gap inside the chart band.
177
+ default: var(--a-space-4)
178
+ --dashboard-layout-aside-width:
179
+ description: Aside column width when [slot="aside"] is present.
180
+ default: 20em
181
+ --dashboard-layout-padding:
182
+ description: Outer padding around the composite.
183
+ default: "0"
184
+
185
+ requiredIcons: []
186
+
187
+ a2ui:
188
+ rules:
189
+ - rule: |
190
+ DashboardLayout MUST receive children via the four named slots
191
+ (toolbar / kpis / charts / table) plus the optional aside.
192
+ Children without a [slot] attribute will not render in any band.
193
+ reason: Named-region slot contract.
194
+ - rule: |
195
+ DashboardLayout[kpi-columns] MUST be 2-6. Out-of-range values
196
+ fall back to the default 4. Eight cards in a row is too narrow
197
+ to be legible at typical container widths; container queries
198
+ collapse the band to 2 columns ≤48em and 1 column ≤32em.
199
+ reason: KPI-column range contract.
200
+ - rule: |
201
+ DashboardLayout[chart-split] MUST be one of the documented
202
+ enum strings — "" (full-width) / 2 / 2:1 / 3:2 / 3:1.
203
+ reason: Chart-split enum contract.
204
+ - rule: |
205
+ A DashboardLayout SHOULD sit inside AdminPageBody for the
206
+ canonical chrome stack. Top-level placement is allowed for
207
+ surface-only demos but not the production shape.
208
+ reason: Canonical composition.
209
+ - rule: |
210
+ A DashboardLayout MUST NOT contain another DashboardLayout as
211
+ a descendant. Two parametric-density containers fight over the
212
+ --a-density cascade. For nested dashboards use sibling
213
+ DashboardLayouts switched by Tabs.
214
+ reason: No-nesting invariant.
215
+
216
+ anti_patterns:
217
+ - description: |
218
+ Child without a [slot] attribute. Lands in the implicit default
219
+ region and breaks the band layout. Every direct child MUST
220
+ declare its slot — toolbar / kpis / charts / table / aside.
221
+ wrong: |
222
+ <dashboard-layout-ui>
223
+ <card-ui><!-- no slot -->...</card-ui>
224
+ </dashboard-layout-ui>
225
+ right: |
226
+ <dashboard-layout-ui>
227
+ <card-ui slot="kpis">...</card-ui>
228
+ </dashboard-layout-ui>
229
+ rule: |
230
+ Every direct child of dashboard-layout-ui declares its slot.
231
+
232
+ - description: |
233
+ Nested DashboardLayout. Two density containers fight over the
234
+ --a-density cascade. The inner one re-creates a 2-column layout
235
+ inside a charts band, which is incoherent.
236
+ wrong: |
237
+ <dashboard-layout-ui>
238
+ <dashboard-layout-ui slot="charts">...</dashboard-layout-ui>
239
+ </dashboard-layout-ui>
240
+ right: |
241
+ <tabs-ui>
242
+ <tab-panel-ui><dashboard-layout-ui>...</dashboard-layout-ui></tab-panel-ui>
243
+ <tab-panel-ui><dashboard-layout-ui>...</dashboard-layout-ui></tab-panel-ui>
244
+ </tabs-ui>
245
+ rule: |
246
+ For nested dashboards, use sibling DashboardLayouts switched by
247
+ Tabs — not DashboardLayout inside DashboardLayout.
248
+
249
+ - description: |
250
+ kpi-columns out of range. Eight cards in a row is too narrow
251
+ to be legible at typical container widths; the container-query
252
+ collapse (≤48em → 2 cols, ≤32em → 1 col) won't help when N is
253
+ already that large at desktop.
254
+ wrong: |
255
+ <dashboard-layout-ui kpi-columns="8">...</dashboard-layout-ui>
256
+ right: |
257
+ <dashboard-layout-ui kpi-columns="4">...</dashboard-layout-ui>
258
+ rule: |
259
+ kpi-columns ∈ {2,3,4,5,6}. Split into 2 rows of 4 by composing
260
+ 2 DashboardLayouts vertically if 8 cards are truly necessary.
261
+
262
+ examples:
263
+ - name: canonical-analytics-dashboard
264
+ description: |
265
+ KPI-strip + 2:1 chart split + table band inside an admin page.
266
+ a2ui: |
267
+ [
268
+ {
269
+ "id": "dash-1",
270
+ "component": "DashboardLayout",
271
+ "density": "",
272
+ "kpiColumns": "4",
273
+ "chartSplit": "2:1",
274
+ "children": ["dash-toolbar", "dash-kpis", "dash-charts", "dash-table"]
275
+ },
276
+ {
277
+ "id": "dash-toolbar",
278
+ "component": "Row",
279
+ "slot": "toolbar"
280
+ },
281
+ {
282
+ "id": "dash-kpis",
283
+ "component": "Group",
284
+ "slot": "kpis"
285
+ },
286
+ {
287
+ "id": "dash-charts",
288
+ "component": "Group",
289
+ "slot": "charts"
290
+ },
291
+ {
292
+ "id": "dash-table",
293
+ "component": "Card",
294
+ "slot": "table"
295
+ }
296
+ ]
297
+
298
+ - name: compact-six-kpi
299
+ description: |
300
+ Six-KPI dashboard at compact density (power-user view).
301
+ a2ui: |
302
+ [
303
+ {
304
+ "id": "dash-2",
305
+ "component": "DashboardLayout",
306
+ "density": "compact",
307
+ "kpiColumns": "6"
308
+ }
309
+ ]
310
+
311
+ - name: with-aside-feed
312
+ description: |
313
+ Dashboard with a sticky aside column carrying a live activity feed.
314
+ a2ui: |
315
+ [
316
+ {
317
+ "id": "dash-3",
318
+ "component": "DashboardLayout",
319
+ "chartSplit": "2",
320
+ "children": ["d3-aside"]
321
+ },
322
+ {
323
+ "id": "d3-aside",
324
+ "component": "Card",
325
+ "slot": "aside"
326
+ }
327
+ ]
328
+
329
+ keywords:
330
+ - dashboard
331
+ - dashboard-layout
332
+ - analytics
333
+ - kpi-grid
334
+ - chart-area
335
+ - dashboard-body
336
+ - admin-dashboard
337
+ - reporting
338
+ - metrics-page
339
+ - layout-container
340
+
341
+ synonyms:
342
+ dashboard:
343
+ - analytics
344
+ - report
345
+ - overview
346
+ - metrics
347
+ layout:
348
+ - container
349
+ - grid
350
+ - frame
351
+ - shell
352
+ kpi:
353
+ - metric
354
+ - stat
355
+ - measure
356
+ charts:
357
+ - chart-area
358
+ - viz
359
+ - graphs
360
+ tags:
361
+ - DashboardLayout
362
+ - Dashboard
363
+ - AnalyticsDashboard
364
+ - MetricsDashboard
365
+
366
+ related:
367
+ - AdminPageBody
368
+ - AdminPage
369
+ - Card
370
+ - Grid
371
+ - Stat
372
+ - Chart
373
+ - Table
374
+ - DateRangeSelector