@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.
Files changed (89) hide show
  1. package/CHANGELOG.md +22 -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,383 @@
1
+ /**
2
+ * <confirm-dialog-ui> — Non-destructive yes/no confirmation dialog.
3
+ *
4
+ * Thin composite over <modal-ui> that pre-stamps the canonical confirm
5
+ * shape (header title + optional icon · body description + default slot ·
6
+ * footer cancel + confirm buttons), wires Cancel-default focus, and
7
+ * re-emits the underlying modal close as both `cancel` and `close`
8
+ * events so consumers don't have to disambiguate dismissal paths.
9
+ *
10
+ * SPEC: .claude/docs/specs/implementation-ready/SPEC-049-confirm-dialog-yes-no.md
11
+ * WAI-APG: https://www.w3.org/WAI/ARIA/apg/patterns/dialog-modal/
12
+ *
13
+ * Why a composite (not a primitive subclassing <modal-ui>):
14
+ * <modal-ui> owns native <dialog> + focus trap + backdrop + Escape.
15
+ * The composite decorates by projecting markup into modal-ui's
16
+ * light-DOM slots (header/body/footer); display:contents on the host
17
+ * keeps it out of the layout box-tree per ADR-0033 light-DOM stance.
18
+ *
19
+ * Public API:
20
+ * .show() — sets open = true
21
+ * .hide() — sets open = false
22
+ *
23
+ * Events:
24
+ * confirm — user activated the confirm button (bubbles)
25
+ * cancel — Cancel button, Escape, or backdrop click (bubbles)
26
+ * close — re-emitted from inner <modal-ui> close (bubbles)
27
+ *
28
+ * Focus model (WAI Dialog):
29
+ * On open, focus lands on the Cancel button (not the affirmative).
30
+ * On dismiss, modal-ui restores focus to the previously focused
31
+ * element (its #previousFocus field).
32
+ */
33
+
34
+ import { UIElement } from '@adia-ai/web-components/core/element';
35
+
36
+ export class ConfirmDialog extends UIElement {
37
+ static properties = {
38
+ open: { type: Boolean, default: false, reflect: true },
39
+ title: { type: String, default: '', reflect: true },
40
+ description: { type: String, default: '', reflect: true },
41
+ confirmText: { type: String, default: 'Confirm', reflect: true, attribute: 'confirm-text' },
42
+ cancelText: { type: String, default: 'Cancel', reflect: true, attribute: 'cancel-text' },
43
+ confirmVariant: { type: String, default: 'primary', reflect: true, attribute: 'confirm-variant' },
44
+ size: { type: String, default: 'sm', reflect: true },
45
+ loading: { type: Boolean, default: false, reflect: true },
46
+ icon: { type: String, default: '', reflect: true },
47
+ };
48
+
49
+ // No template — the composite imperatively stamps a single <modal-ui>
50
+ // child once in connected(), then mutates its inner header/body/footer
51
+ // shape across renders. Returning html`` from template() would trigger
52
+ // stamp() → replaceChildren() on every render and wipe authored slot
53
+ // children (same rationale as modal-ui itself; see modal/modal.class.js).
54
+ static template = () => null;
55
+
56
+ #modal = null;
57
+ #headerHost = null;
58
+ #iconHost = null;
59
+ #titleEl = null;
60
+ #bodyHost = null;
61
+ #descEl = null;
62
+ #defaultSlotHost = null;
63
+ #footerHost = null;
64
+ #cancelBtn = null;
65
+ #confirmBtn = null;
66
+ #cleanups = [];
67
+ #stamped = false;
68
+ #titleId = '';
69
+ #descId = '';
70
+ // Set true by the cancel/confirm button handlers immediately before
71
+ // they flip [open]=false. The modal-ui's close event then re-emits
72
+ // close on the host but skips the cancel re-emit (the explicit
73
+ // button-handler already dispatched).
74
+ #suppressNextCloseCancel = false;
75
+
76
+ connected() {
77
+ if (!this.#stamped) {
78
+ this.#titleId = `${this.#uid}-title`;
79
+ this.#descId = `${this.#uid}-desc`;
80
+ this.#stamp();
81
+ this.#wireListeners();
82
+ this.#stamped = true;
83
+ }
84
+ }
85
+
86
+ disconnected() {
87
+ for (const fn of this.#cleanups) {
88
+ try { fn(); } catch { /* tolerate handler missing */ }
89
+ }
90
+ this.#cleanups.length = 0;
91
+ }
92
+
93
+ /** Imperative open. Setting `.open = true` is equivalent. */
94
+ show() { this.open = true; }
95
+
96
+ /** Imperative close. Setting `.open = false` is equivalent. */
97
+ hide() { this.open = false; }
98
+
99
+ // ── Render — kept narrow; only state-dependent attribute pushes ────
100
+
101
+ render() {
102
+ if (!this.#stamped) return;
103
+
104
+ // Forward [open] / [size] / [text] to the inner modal-ui.
105
+ if (this.#modal) {
106
+ // §FB-Wave1-QA — DON'T set modal-ui's [text]. Modal-ui renders [text]
107
+ // via ::before on its header, which would duplicate our own
108
+ // <text-ui slot="heading"> rendered inside the header slot. The
109
+ // explicit <text-ui slot="heading"> is the authoritative label
110
+ // (it carries the aria-labelledby id wiring below).
111
+ this.#modal.setAttribute('size', this.size || 'sm');
112
+ this.#modal.open = !!this.open;
113
+ }
114
+
115
+ // Title text + id wiring (aria-labelledby).
116
+ if (this.#titleEl) {
117
+ this.#titleEl.id = this.#titleId;
118
+ this.#titleEl.textContent = this.title || '';
119
+ }
120
+
121
+ // Description text + id wiring (aria-describedby).
122
+ if (this.#descEl) {
123
+ this.#descEl.id = this.#descId;
124
+ this.#descEl.textContent = this.description || '';
125
+ // Collapse when empty so the body region doesn't reserve a paragraph slot.
126
+ this.#descEl.hidden = !this.description;
127
+ }
128
+
129
+ // Header icon — show when [icon] is set AND no slotted icon-override.
130
+ if (this.#iconHost) {
131
+ const hasOverride = this.querySelector(':scope > [slot="icon"]');
132
+ if (hasOverride) {
133
+ // Author-supplied override sits inside the header; hide stamped icon.
134
+ this.#iconHost.hidden = true;
135
+ } else if (this.icon) {
136
+ this.#iconHost.hidden = false;
137
+ this.#iconHost.setAttribute('name', this.icon);
138
+ } else {
139
+ this.#iconHost.hidden = true;
140
+ }
141
+ }
142
+
143
+ // Wire role + aria-labelledby + aria-describedby on the inner <dialog>
144
+ // once it exists. modal-ui ensures the dialog part on its own connect;
145
+ // we just decorate.
146
+ const dialog = this.#modal?.querySelector(':scope > [slot="dialog"]');
147
+ if (dialog) {
148
+ // WAI-APG Dialog (NOT alertdialog — that's destructive; SPEC-048).
149
+ dialog.setAttribute('role', 'dialog');
150
+ dialog.setAttribute('aria-modal', 'true');
151
+ if (this.title) dialog.setAttribute('aria-labelledby', this.#titleId);
152
+ else dialog.removeAttribute('aria-labelledby');
153
+ if (this.description) dialog.setAttribute('aria-describedby', this.#descId);
154
+ else dialog.removeAttribute('aria-describedby');
155
+ }
156
+
157
+ // Footer buttons — labels, variants, disabled state.
158
+ if (this.#cancelBtn) {
159
+ this.#cancelBtn.setAttribute('text', this.cancelText || 'Cancel');
160
+ if (this.loading) this.#cancelBtn.setAttribute('disabled', '');
161
+ else this.#cancelBtn.removeAttribute('disabled');
162
+ }
163
+ if (this.#confirmBtn) {
164
+ this.#confirmBtn.setAttribute('text', this.confirmText || 'Confirm');
165
+ this.#confirmBtn.setAttribute(
166
+ 'variant',
167
+ this.confirmVariant === 'ghost' ? 'ghost' : 'primary',
168
+ );
169
+ if (this.loading) {
170
+ this.#confirmBtn.setAttribute('disabled', '');
171
+ this.#confirmBtn.setAttribute('icon', 'spinner');
172
+ } else {
173
+ this.#confirmBtn.removeAttribute('disabled');
174
+ this.#confirmBtn.removeAttribute('icon');
175
+ }
176
+ }
177
+
178
+ // Cancel-default focus model — schedule on next frame after
179
+ // modal-ui's dialog.showModal() has lifted the panel into the top
180
+ // layer. modal-ui re-stamps the <dialog> synchronously inside its
181
+ // own render but we still need a microtask gap so the focus call
182
+ // lands after the native focus-restore that <dialog>.showModal()
183
+ // triggers (which puts focus on the first focusable).
184
+ if (this.open && this.#cancelBtn && !this.loading) {
185
+ this.#scheduleCancelFocus();
186
+ }
187
+ }
188
+
189
+ // ── Internal — DOM stamping ────────────────────────────────────────
190
+
191
+ #stamp() {
192
+ // Build the modal-ui subtree imperatively (avoids innerHTML +
193
+ // template stamping, both of which would race modal-ui's own
194
+ // render() in happy-dom + Safari).
195
+ //
196
+ // Slot vocabulary — the dialog family (modal-ui / drawer-ui /
197
+ // confirm-dialog-ui) consistently uses `slot="..."` for every
198
+ // structural region, NOT `part="..."`. Slots are the canonical
199
+ // light-DOM projection vocabulary (icon / heading / description /
200
+ // action / action-leading / body / footer / close); part= was
201
+ // legacy churn that confused theming with structural intent.
202
+ const modal = document.createElement('modal-ui');
203
+ modal.setAttribute('size', this.size || 'sm');
204
+ // §FB-Wave1-QA — DON'T pass [text] to modal-ui. modal-ui renders [text]
205
+ // via header::before which would duplicate the <text-ui slot="heading">
206
+ // we stamp below. The aria-labelledby id wiring in render() handles
207
+ // screen-reader labeling.
208
+
209
+ // ── Header slot — icon + heading + description (drawer/admin-shell
210
+ // canonical 3-row card-ui header vocabulary). ──
211
+ const header = document.createElement('header');
212
+ header.setAttribute('slot', 'header');
213
+
214
+ // Stamped icon (hidden by default; show when [icon] is set and no
215
+ // author-supplied [slot="icon"] override is present).
216
+ const iconEl = document.createElement('icon-ui');
217
+ iconEl.setAttribute('slot', 'icon');
218
+ iconEl.hidden = true;
219
+ header.appendChild(iconEl);
220
+ this.#iconHost = iconEl;
221
+
222
+ // Heading text — lives in header so aria-labelledby resolves
223
+ // inside the dialog's role=dialog subtree.
224
+ const titleEl = document.createElement('text-ui');
225
+ titleEl.setAttribute('slot', 'heading');
226
+ titleEl.id = this.#titleId;
227
+ if (this.title) titleEl.textContent = this.title;
228
+ header.appendChild(titleEl);
229
+ this.#titleEl = titleEl;
230
+ this.#headerHost = header;
231
+
232
+ // ── Body slot — description + default-slot content host. ──
233
+ const body = document.createElement('section');
234
+ body.setAttribute('slot', 'body');
235
+
236
+ // Description lives in the BODY (the section), NOT the header — the
237
+ // header stays a compact icon + title row and the description reads as
238
+ // the dialog's body copy (filling the body band instead of crowding the
239
+ // title). aria-describedby still resolves via the id wiring in render().
240
+ const descEl = document.createElement('text-ui');
241
+ descEl.setAttribute('slot', 'description');
242
+ descEl.id = this.#descId;
243
+ if (this.description) descEl.textContent = this.description;
244
+ else descEl.hidden = true;
245
+ body.appendChild(descEl);
246
+ this.#descEl = descEl;
247
+
248
+ // A container that consumer default-slot children migrate into.
249
+ const defaultHost = document.createElement('div');
250
+ defaultHost.setAttribute('data-extra', '');
251
+ body.appendChild(defaultHost);
252
+ this.#defaultSlotHost = defaultHost;
253
+ this.#bodyHost = body;
254
+
255
+ // ── Footer slot — cancel + confirm buttons.
256
+ // Both buttons go in slot="action" so they sit in the trailing
257
+ // cluster (cancel-then-confirm DOM order = left-to-right reading
258
+ // order). Use slot="action-leading" only for destructive opt-out
259
+ // actions placed on the inline-start edge (e.g. drawer-based
260
+ // Delete-then-Cancel/Save patterns). ──
261
+ const footer = document.createElement('footer');
262
+ footer.setAttribute('slot', 'footer');
263
+
264
+ const cancelBtn = document.createElement('button-ui');
265
+ cancelBtn.setAttribute('slot', 'action');
266
+ cancelBtn.setAttribute('data-confirm-action', 'cancel');
267
+ cancelBtn.setAttribute('variant', 'ghost');
268
+ cancelBtn.setAttribute('text', this.cancelText || 'Cancel');
269
+ footer.appendChild(cancelBtn);
270
+ this.#cancelBtn = cancelBtn;
271
+
272
+ const confirmBtn = document.createElement('button-ui');
273
+ confirmBtn.setAttribute('slot', 'action');
274
+ confirmBtn.setAttribute('data-confirm-action', 'confirm');
275
+ confirmBtn.setAttribute(
276
+ 'variant',
277
+ this.confirmVariant === 'ghost' ? 'ghost' : 'primary',
278
+ );
279
+ confirmBtn.setAttribute('text', this.confirmText || 'Confirm');
280
+ footer.appendChild(confirmBtn);
281
+ this.#confirmBtn = confirmBtn;
282
+ this.#footerHost = footer;
283
+
284
+ // Migrate authored default-slot children (anything WITHOUT a slot
285
+ // attribute) into the body's extra host before mounting modal-ui.
286
+ // [slot="icon"] children stay on the modal-ui and are projected
287
+ // into the header by the consumer-side CSS rule below.
288
+ const authoredChildren = [...this.children];
289
+ for (const child of authoredChildren) {
290
+ const slot = child.getAttribute('slot');
291
+ if (slot === 'icon') {
292
+ // Carry into the header alongside the stamped icon (which we
293
+ // hide in render() when an override is present).
294
+ header.appendChild(child);
295
+ } else if (!slot) {
296
+ defaultHost.appendChild(child);
297
+ }
298
+ // else: any other slot author used stays on host; the modal-ui
299
+ // won't see it via the named-slot light-DOM contract because we
300
+ // don't reference it. Treat as ignored.
301
+ }
302
+
303
+ // Assemble modal-ui — order matters for modal-ui's render() which
304
+ // detects authored [slot="body"] vs stamped.
305
+ modal.appendChild(header);
306
+ modal.appendChild(body);
307
+ modal.appendChild(footer);
308
+ this.appendChild(modal);
309
+ this.#modal = modal;
310
+ }
311
+
312
+ #wireListeners() {
313
+ // button-ui's #onClick handler intercepts native click and
314
+ // re-dispatches `press` on every activation (keyboard or pointer).
315
+ // Listen for `press` only — both event paths converge there, so
316
+ // listening on both would double-fire from the same user action.
317
+
318
+ // Confirm button — explicit affirmative.
319
+ const onConfirm = (e) => {
320
+ if (this.loading) return;
321
+ e.stopPropagation();
322
+ this.#suppressNextCloseCancel = true;
323
+ this.dispatchEvent(new CustomEvent('confirm', { bubbles: true, detail: {} }));
324
+ };
325
+ this.#confirmBtn.addEventListener('press', onConfirm);
326
+ this.#cleanups.push(() => this.#confirmBtn.removeEventListener('press', onConfirm));
327
+
328
+ // Cancel button — explicit negative; flips [open]=false so the
329
+ // inner modal-ui animates close, then fires `cancel` immediately
330
+ // (and we suppress the modal's close-driven cancel re-emit).
331
+ const onCancel = (e) => {
332
+ if (this.loading) return;
333
+ e.stopPropagation();
334
+ this.#suppressNextCloseCancel = true;
335
+ this.dispatchEvent(new CustomEvent('cancel', { bubbles: true, detail: {} }));
336
+ this.open = false;
337
+ };
338
+ this.#cancelBtn.addEventListener('press', onCancel);
339
+ this.#cleanups.push(() => this.#cancelBtn.removeEventListener('press', onCancel));
340
+
341
+ // Modal close — fires on Escape, backdrop click, or programmatic
342
+ // [open]=false. Always re-emit `close` on the host; conditionally
343
+ // emit `cancel` (skip when the close was triggered BY a Cancel/
344
+ // Confirm button activation, which already dispatched the explicit
345
+ // event).
346
+ const onCloseInner = (e) => {
347
+ e.stopPropagation();
348
+ this.open = false;
349
+ if (!this.#suppressNextCloseCancel) {
350
+ this.dispatchEvent(new CustomEvent('cancel', { bubbles: true, detail: {} }));
351
+ }
352
+ this.#suppressNextCloseCancel = false;
353
+ this.dispatchEvent(new CustomEvent('close', { bubbles: true, detail: {} }));
354
+ };
355
+ this.#modal.addEventListener('close', onCloseInner);
356
+ this.#cleanups.push(() => this.#modal.removeEventListener('close', onCloseInner));
357
+ }
358
+
359
+ #scheduleCancelFocus() {
360
+ // One-shot — schedule until the dialog is actually in the top layer
361
+ // (modal-ui sets [data-open] after a forced reflow). Re-trying once
362
+ // covers happy-dom (sync) and Safari/Chrome (async paint).
363
+ const attempt = () => {
364
+ if (!this.open || !this.#cancelBtn || this.loading) return;
365
+ try {
366
+ this.#cancelBtn.focus();
367
+ } catch { /* element might not be focusable in happy-dom; ignore */ }
368
+ };
369
+ if (typeof requestAnimationFrame === 'function') {
370
+ requestAnimationFrame(attempt);
371
+ } else {
372
+ queueMicrotask(attempt);
373
+ }
374
+ }
375
+
376
+ get #uid() {
377
+ return this.id || 'confirm-dialog';
378
+ }
379
+ }
380
+
381
+ if (!customElements.get('confirm-dialog-ui')) {
382
+ customElements.define('confirm-dialog-ui', ConfirmDialog);
383
+ }
@@ -0,0 +1,297 @@
1
+ # Edit this file; run `npm run build:components` to regenerate a2ui.json.
2
+ $schema: ../../../../scripts/schemas/component.yaml.schema.json
3
+ name: ConfirmDialog
4
+ tag: confirm-dialog-ui
5
+ status: stable
6
+ component: ConfirmDialog
7
+ category: feedback
8
+ version: 1
9
+ description: |
10
+ Non-destructive yes/no confirmation dialog — a thin composite over
11
+ <modal-ui> that pre-stamps the canonical confirm shape: header (title
12
+ + optional icon), body (description + optional rich slot), and
13
+ footer (cancel + confirm buttons). Wires Cancel-default focus,
14
+ confirm / cancel / close events, and the [loading] in-flight state.
15
+
16
+ Use confirm-dialog-ui for benign questions: "Save changes?", "Switch
17
+ theme?", "Apply preset?". For irreversible destructive operations
18
+ (delete account, drop database, deploy to prod) use <alert-dialog-ui>
19
+ instead — it carries role="alertdialog", danger chrome, and the
20
+ optional typed-name speed-bump.
21
+
22
+ Composed primitive: <modal-ui> owns the native <dialog>, focus trap,
23
+ backdrop, and Escape-dismiss. confirm-dialog-ui decorates; it does
24
+ not reimplement.
25
+
26
+ Spec: .claude/docs/specs/implementation-ready/SPEC-049-confirm-dialog-yes-no.md.
27
+
28
+ # Per ADR-0027 — primitives that programmatically create other primitives
29
+ # in their render() do NOT auto-import them. Consumer (or demo shell) must
30
+ # explicitly import each composed primitive.
31
+ composes:
32
+ - modal-ui
33
+ - button-ui
34
+ - icon-ui
35
+ - text-ui
36
+
37
+ props:
38
+ open:
39
+ description: |
40
+ Mirrors <modal-ui>[open]. Setting to true opens the dialog with
41
+ entry animation; setting to false animates close.
42
+ type: boolean
43
+ default: false
44
+ reflect: true
45
+
46
+ title:
47
+ description: |
48
+ Dialog title; rendered in the modal header via <modal-ui>[text]
49
+ and announced through aria-labelledby on the inner <dialog>.
50
+ type: string
51
+ default: ""
52
+ reflect: true
53
+
54
+ description:
55
+ description: |
56
+ Body description; rendered as a paragraph above any default-slot
57
+ content. Wired as the aria-describedby target.
58
+ type: string
59
+ default: ""
60
+ reflect: true
61
+
62
+ confirm-text:
63
+ description: Primary action button label. Defaults to "Confirm".
64
+ type: string
65
+ default: "Confirm"
66
+ reflect: true
67
+ attribute: confirm-text
68
+
69
+ cancel-text:
70
+ description: Cancel button label. Defaults to "Cancel".
71
+ type: string
72
+ default: "Cancel"
73
+ reflect: true
74
+ attribute: cancel-text
75
+
76
+ confirm-variant:
77
+ description: |
78
+ Tone of the confirm button. "primary" (default) for the typical
79
+ affirmative-action case; "ghost" when both choices are roughly
80
+ balanced and a tonal primary would over-weight the affirmative.
81
+ type: string
82
+ default: "primary"
83
+ enum:
84
+ - primary
85
+ - ghost
86
+ reflect: true
87
+ attribute: confirm-variant
88
+
89
+ size:
90
+ description: |
91
+ Forwarded to <modal-ui>[size]. sm = ~24rem, md = ~32rem,
92
+ lg = ~48rem. Confirm dialogs default to sm.
93
+ type: string
94
+ default: "sm"
95
+ enum:
96
+ - sm
97
+ - md
98
+ - lg
99
+ reflect: true
100
+
101
+ loading:
102
+ description: |
103
+ In-flight state. When set, both buttons disable and the confirm
104
+ button shows a spinner — the consumer toggles [loading] off and
105
+ closes via [open]=false when the underlying action resolves.
106
+ type: boolean
107
+ default: false
108
+ reflect: true
109
+
110
+ icon:
111
+ description: |
112
+ Optional Phosphor icon glyph stamped in the header. Opt-in —
113
+ consumer supplies the name (e.g. "info", "question"). When
114
+ empty, no icon disc renders.
115
+ type: string
116
+ default: ""
117
+ reflect: true
118
+
119
+ events:
120
+ confirm:
121
+ description: |
122
+ Fired when the user activates the confirm button. Bubbles.
123
+ Consumer wires the actual action handler off this event.
124
+ detail: {}
125
+
126
+ cancel:
127
+ description: |
128
+ Fired when the user activates the cancel button, presses Escape,
129
+ or clicks the backdrop. Bubbles.
130
+ detail: {}
131
+
132
+ close:
133
+ description: |
134
+ Re-emitted from the underlying <modal-ui> when the dialog
135
+ finishes closing via any path. Bubbles.
136
+ detail: {}
137
+
138
+ slots:
139
+ default:
140
+ description: |
141
+ Optional rich content rendered above the description — supplementary
142
+ lists, hints, or markup the [description] string can't carry.
143
+ icon:
144
+ description: |
145
+ Override the [icon] glyph with a custom node (e.g. a colored
146
+ <icon-ui>, an image, or arbitrary markup). Mutually exclusive
147
+ with [icon]; if a slot child is present, the [icon] attribute
148
+ is ignored.
149
+ heading:
150
+ description: >-
151
+ Override the stamped heading. Author-fillable when richer markup
152
+ than the plain [text] string is needed (inline links, code spans,
153
+ etc.).
154
+ description:
155
+ description: >-
156
+ Override the stamped description line below the heading. Author-fills
157
+ with richer markup when the [description] attribute isn't enough.
158
+ header:
159
+ description: >-
160
+ Wholesale override of the stamped header (icon + heading + description
161
+ cluster). Use only when full custom-header markup is needed; otherwise
162
+ prefer the granular icon/heading/description slot overrides.
163
+ body:
164
+ description: >-
165
+ Dialog body region (between header and footer). For rich content —
166
+ paragraphs, lists, embedded form fields. Distinct from [slot="default"]
167
+ which sits above [description] in the header region.
168
+ footer:
169
+ description: >-
170
+ Wholesale override of the stamped footer (Cancel / Confirm action
171
+ cluster). Use only for full custom-footer markup; otherwise the
172
+ host's [cancel-text] / [confirm-text] / [confirm-variant] attrs
173
+ drive the default footer rendering.
174
+
175
+ states:
176
+ - name: idle
177
+ description: Default — closed, no <dialog> in the top layer.
178
+ - name: open
179
+ description: Visible in the top layer; focus trapped; Cancel focused by default.
180
+ attribute: open
181
+ - name: loading
182
+ description: Both buttons disabled; confirm shows spinner.
183
+ attribute: loading
184
+
185
+ traits: []
186
+
187
+ tokens:
188
+ --confirm-dialog-icon-bg:
189
+ description: Optional header-icon disc background. Defaults to var(--a-bg-muted).
190
+ --confirm-dialog-icon-fg:
191
+ description: Optional header-icon glyph color. Defaults to var(--a-fg).
192
+ --confirm-dialog-confirm-bg:
193
+ description: |
194
+ Confirm-button background under [confirm-variant="primary"].
195
+ Defaults to var(--a-primary-bg). When [confirm-variant="ghost"]
196
+ the button uses transparent background regardless of this token.
197
+
198
+ a2ui:
199
+ rules:
200
+ - >-
201
+ ConfirmDialog is for NON-destructive yes/no questions ("Save
202
+ changes?", "Switch theme?", "Apply preset?"). For irreversible
203
+ destructive operations (delete, drop, deploy to prod) use
204
+ AlertDialog instead — its danger chrome + role="alertdialog" +
205
+ typed-name speed-bump are the WAI-APG pattern for that case.
206
+ - >-
207
+ Default focus lands on Cancel by default — this is intentional
208
+ (WAI guidance: the least-destructive action is the default). Do
209
+ NOT add autofocus to the confirm button or override the focus
210
+ model.
211
+ - >-
212
+ The confirm button is primary-toned by default. Use
213
+ [confirm-variant="ghost"] only when both options are roughly
214
+ equivalent in weight (e.g. "Apply preset" vs "Keep current"
215
+ where neither is strictly preferred).
216
+ - >-
217
+ Do NOT model the cancel option as a "Discard" action — if cancel
218
+ means "lose data", the operation is destructive and belongs in
219
+ AlertDialog. ConfirmDialog's cancel is a no-op return-to-prior-
220
+ state semantically.
221
+ - >-
222
+ Reflect dialog visibility via the [open] boolean attribute on
223
+ the ConfirmDialog host (open=true / open=false). Do NOT toggle
224
+ [hidden], CSS display, or wrap in a sibling visibility container
225
+ — the inner <modal-ui> owns the native <dialog> lifecycle.
226
+
227
+ anti_patterns:
228
+ - description: >-
229
+ Re-deriving the confirm shape from a raw <modal-ui> instead of using
230
+ ConfirmDialog. The header/body/footer triplet, the Cancel-default
231
+ focus, and the confirm/cancel event wiring all need to be re-authored,
232
+ and the WAI-APG contract drifts per consumer.
233
+ wrong: |
234
+ <modal-ui open size="sm" text="Save?">
235
+ <p>Save changes before closing?</p>
236
+ <footer>
237
+ <button-ui variant="ghost" text="Discard"></button-ui>
238
+ <button-ui variant="primary" text="Save"></button-ui>
239
+ </footer>
240
+ </modal-ui>
241
+ right: |
242
+ <confirm-dialog-ui open
243
+ title="Save changes?"
244
+ description="Save changes before closing?"
245
+ confirm-text="Save"
246
+ cancel-text="Discard">
247
+ </confirm-dialog-ui>
248
+ rule: >-
249
+ Reach for ConfirmDialog (not raw Modal) for non-destructive yes/no
250
+ prompts. The composite owns the contract.
251
+
252
+ - description: >-
253
+ Using ConfirmDialog for destructive irreversible operations. The
254
+ primary-toned confirm button reads as benign and primes the user
255
+ to click through; the WAI-APG `alertdialog` role + danger chrome
256
+ live in AlertDialog for that reason.
257
+ wrong: |
258
+ <confirm-dialog-ui open
259
+ title="Delete project?"
260
+ description="This is permanent."
261
+ confirm-text="Delete">
262
+ </confirm-dialog-ui>
263
+ right: |
264
+ <alert-dialog-ui open
265
+ title="Delete project?"
266
+ description="This is permanent."
267
+ confirm-text="Delete">
268
+ </alert-dialog-ui>
269
+ rule: >-
270
+ Destructive intent → AlertDialog. Non-destructive intent →
271
+ ConfirmDialog. The tag name carries the semantic; do not cross
272
+ the streams.
273
+
274
+ examples: []
275
+
276
+ keywords:
277
+ - confirm
278
+ - confirm-dialog
279
+ - dialog
280
+ - confirmation
281
+ - yes-no
282
+ - ok-cancel
283
+ - save-confirm
284
+ - prompt
285
+ - dialog-modal
286
+
287
+ synonyms:
288
+ confirm: [confirmation, prompt, ok, yes]
289
+ cancel: [dismiss, back, no]
290
+ dialog: [modal, popup]
291
+
292
+ related:
293
+ - Modal
294
+ - AlertDialog
295
+ - Button
296
+ - Icon
297
+ - Text
@@ -0,0 +1,10 @@
1
+ /**
2
+ * @adia-ai/web-modules/feedback — cluster barrel.
3
+ *
4
+ * Composite feedback / overlay primitives that compose a substrate
5
+ * primitive (typically <modal-ui>) and pre-stamp a canonical shape +
6
+ * focus model + event contract.
7
+ *
8
+ * SPEC-049 — confirm-dialog-ui (non-destructive yes/no).
9
+ */
10
+ export { ConfirmDialog } from './confirm-dialog/confirm-dialog.js';