@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,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';
|