@adia-ai/web-modules 0.8.35 → 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.
- package/CHANGELOG.md +22 -0
- package/README.md +3 -3
- package/agent-admin/admin-roster/admin-roster.js +1 -1
- package/agent-admin/admin-settings/admin-settings.js +1 -1
- package/agent-admin/agent-admin/agent-admin.js +1 -1
- package/billing/billing-overview/billing-overview.a2ui.json +281 -0
- package/billing/billing-overview/billing-overview.class.js +838 -0
- package/billing/billing-overview/billing-overview.css +223 -0
- package/billing/billing-overview/billing-overview.js +16 -0
- package/billing/billing-overview/billing-overview.yaml +441 -0
- package/billing/index.js +21 -0
- package/billing/invoice-detail/invoice-detail.a2ui.json +276 -0
- package/billing/invoice-detail/invoice-detail.class.js +711 -0
- package/billing/invoice-detail/invoice-detail.css +305 -0
- package/billing/invoice-detail/invoice-detail.js +16 -0
- package/billing/invoice-detail/invoice-detail.yaml +411 -0
- package/billing/invoice-history/invoice-history.a2ui.json +315 -0
- package/billing/invoice-history/invoice-history.class.js +672 -0
- package/billing/invoice-history/invoice-history.css +130 -0
- package/billing/invoice-history/invoice-history.js +16 -0
- package/billing/invoice-history/invoice-history.yaml +398 -0
- package/billing/payment-method-form/payment-method-form.a2ui.json +310 -0
- package/billing/payment-method-form/payment-method-form.class.js +886 -0
- package/billing/payment-method-form/payment-method-form.css +149 -0
- package/billing/payment-method-form/payment-method-form.js +16 -0
- package/billing/payment-method-form/payment-method-form.yaml +430 -0
- package/billing/payment-method-list/payment-method-list.a2ui.json +283 -0
- package/billing/payment-method-list/payment-method-list.class.js +705 -0
- package/billing/payment-method-list/payment-method-list.css +128 -0
- package/billing/payment-method-list/payment-method-list.js +16 -0
- package/billing/payment-method-list/payment-method-list.yaml +355 -0
- package/billing/plan-picker/plan-picker.a2ui.json +349 -0
- package/billing/plan-picker/plan-picker.class.js +635 -0
- package/billing/plan-picker/plan-picker.css +227 -0
- package/billing/plan-picker/plan-picker.js +16 -0
- package/billing/plan-picker/plan-picker.yaml +453 -0
- package/chat/README.md +1 -1
- package/chat/chat-composer/chat-composer.js +2 -2
- package/chat/chat-empty/chat-empty.js +2 -2
- package/chat/chat-shell/chat-shell.js +4 -4
- package/chat/chat-sidebar/chat-sidebar.js +2 -2
- package/chat/chat-surfaces/chat-surfaces.js +2 -2
- package/chat/chat-thread/chat-thread.js +2 -2
- package/dashboard/dashboard-layout/dashboard-layout.a2ui.json +254 -0
- package/dashboard/dashboard-layout/dashboard-layout.css +190 -0
- package/dashboard/dashboard-layout/dashboard-layout.js +289 -0
- package/dashboard/dashboard-layout/dashboard-layout.yaml +374 -0
- package/dashboard/date-range-selector/date-range-selector.a2ui.json +236 -0
- package/dashboard/date-range-selector/date-range-selector.css +84 -0
- package/dashboard/date-range-selector/date-range-selector.js +686 -0
- package/dashboard/date-range-selector/date-range-selector.yaml +358 -0
- package/dashboard/index.js +14 -0
- package/dist/everything.min.js +1 -1
- package/editor/editor-canvas/editor-canvas.js +2 -2
- package/editor/editor-shell/editor-shell.js +2 -2
- package/editor/editor-sidebar/editor-sidebar.js +2 -2
- package/editor/editor-toolbar/editor-toolbar.js +2 -2
- package/feedback/confirm-dialog/confirm-dialog.a2ui.json +206 -0
- package/feedback/confirm-dialog/confirm-dialog.css +157 -0
- package/feedback/confirm-dialog/confirm-dialog.js +383 -0
- package/feedback/confirm-dialog/confirm-dialog.yaml +297 -0
- package/feedback/index.js +10 -0
- package/form/form-popover/form-popover.js +1 -1
- package/generative/index.d.ts +1 -1
- package/generative/index.js +2 -2
- package/onboarding/index.js +1 -0
- package/onboarding/onboarding-checklist/onboarding-checklist.a2ui.json +263 -0
- package/onboarding/onboarding-checklist/onboarding-checklist.css +157 -0
- package/onboarding/onboarding-checklist/onboarding-checklist.js +428 -0
- package/onboarding/onboarding-checklist/onboarding-checklist.yaml +236 -0
- package/package.json +99 -6
- package/runtime/a2ui-root/a2ui-root.js +3 -3
- package/runtime/gen-root/gen-root.js +5 -5
- package/settings/index.js +12 -0
- package/settings/integrations-page/integrations-page.a2ui.json +274 -0
- package/settings/integrations-page/integrations-page.css +177 -0
- package/settings/integrations-page/integrations-page.d.ts +77 -0
- package/settings/integrations-page/integrations-page.js +413 -0
- package/settings/integrations-page/integrations-page.yaml +317 -0
- package/settings/notification-preferences/notification-preferences.a2ui.json +245 -0
- package/settings/notification-preferences/notification-preferences.css +204 -0
- package/settings/notification-preferences/notification-preferences.js +622 -0
- package/settings/notification-preferences/notification-preferences.yaml +358 -0
- package/shell/admin-command/admin-command.js +2 -2
- package/shell/admin-shell/admin-shell.js +2 -2
- package/shell/admin-sidebar/admin-sidebar.js +2 -2
- package/shell/embed-shell/embed-shell.js +3 -3
- package/simple/simple-shell/simple-shell.js +1 -1
- 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
|