@adia-ai/web-components 0.8.38 → 0.8.40
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 +30 -0
- package/MIGRATION.md +243 -2
- package/components/agent-artifact/agent-artifact.css +1 -1
- package/components/agent-reasoning/agent-reasoning.css +2 -2
- package/components/agent-trace/agent-trace.css +3 -3
- package/components/alert/alert.css +1 -1
- package/components/avatar/avatar.css +1 -1
- package/components/badge/badge.css +1 -1
- package/components/button/button.css +6 -5
- package/components/calendar-picker/calendar-picker.css +5 -2
- package/components/card/card.css +3 -3
- package/components/chart/chart.a2ui.json +5 -5
- package/components/chart/chart.class.js +4 -4
- package/components/chart/chart.css +6 -6
- package/components/chart/chart.d.ts +2 -2
- package/components/chart/chart.yaml +3 -2
- package/components/chat-thread/chat-thread.css +1 -1
- package/components/code/code.css +6 -6
- package/components/combobox/combobox.css +8 -2
- package/components/command/command.a2ui.json +3 -0
- package/components/command/command.class.js +28 -6
- package/components/command/command.css +17 -6
- package/components/command/command.yaml +5 -0
- package/components/date-range-picker/date-range-picker.css +7 -1
- package/components/datetime-picker/datetime-picker.css +5 -1
- package/components/description-list/description-list.css +2 -2
- package/components/display-field/display-field.css +3 -3
- package/components/divider/divider.css +1 -1
- package/components/drawer/drawer.css +2 -2
- package/components/drilldown/drilldown.a2ui.json +3 -1
- package/components/drilldown/drilldown.class.js +12 -4
- package/components/drilldown/drilldown.css +9 -10
- package/components/drilldown/drilldown.yaml +2 -0
- package/components/empty-state/empty-state.css +2 -2
- package/components/field/field.css +1 -1
- package/components/heatmap/heatmap.css +2 -2
- package/components/inline-message/inline-message.css +1 -1
- package/components/input/input.css +5 -4
- package/components/integration-card/integration-card.css +4 -4
- package/components/kbd/kbd.css +1 -1
- package/components/list/list.css +2 -2
- package/components/modal/modal.class.js +68 -4
- package/components/nav/nav.a2ui.json +5 -0
- package/components/nav/nav.class.js +24 -4
- package/components/nav/nav.css +2 -2
- package/components/nav/nav.d.ts +2 -0
- package/components/nav/nav.yaml +18 -0
- package/components/nav-group/nav-group.css +3 -3
- package/components/nav-item/nav-item.css +2 -2
- package/components/option-card/option-card.css +1 -1
- package/components/otp-input/otp-input.css +1 -1
- package/components/page/page.css +2 -2
- package/components/pagination/pagination.css +1 -1
- package/components/pane/pane.css +1 -1
- package/components/progress-row/progress-row.css +2 -2
- package/components/qr-code/qr-code.a2ui.json +10 -10
- package/components/qr-code/qr-code.class.js +3 -3
- package/components/qr-code/qr-code.d.ts +10 -7
- package/components/qr-code/qr-code.yaml +13 -9
- package/components/range/range.css +1 -1
- package/components/rating/rating.css +1 -1
- package/components/richtext/richtext.css +3 -3
- package/components/segment/segment.css +5 -4
- package/components/select/select.class.js +30 -12
- package/components/select/select.css +14 -5
- package/components/spinner/spinner.css +1 -1
- package/components/stat/stat.css +3 -3
- package/components/step-progress/step-progress.css +1 -1
- package/components/stepper/stepper.css +2 -2
- package/components/swatch/swatch.css +8 -6
- package/components/table/table.a2ui.json +0 -10
- package/components/table/table.class.js +4 -4
- package/components/table/table.css +6 -9
- package/components/table/table.d.ts +0 -2
- package/components/table/table.yaml +0 -9
- package/components/table-toolbar/table-toolbar.css +1 -1
- package/components/tabs/tabs.css +1 -1
- package/components/tag/tag.css +2 -2
- package/components/tags-input/tags-input.css +1 -1
- package/components/textarea/textarea.css +1 -1
- package/components/time-picker/time-picker.css +1 -1
- package/components/timeline/timeline.css +4 -4
- package/components/toc/toc.css +1 -1
- package/components/toggle-group/toggle-group.class.js +13 -11
- package/components/toggle-group/toggle-group.css +14 -8
- package/components/toggle-group/toggle-group.d.ts +5 -0
- package/components/toggle-group/toggle-group.yaml +7 -0
- package/components/toggle-group/toggle-option.a2ui.json +5 -0
- package/components/toggle-group/toggle-option.yaml +14 -2
- package/components/tooltip/tooltip.css +1 -1
- package/components/tour/tour.css +2 -2
- package/components/tree/tree.css +1 -1
- package/custom-elements.json +101 -89
- package/dist/host.min.css +1 -1
- package/dist/host.sheet.js +1 -1
- package/dist/theme-provider.min.js +3 -3
- package/dist/web-components.min.css +1 -1
- package/dist/web-components.min.js +97 -94
- package/dist/web-components.sheet.js +1 -1
- package/package.json +1 -1
- package/styles/type/scale.css +6 -7
|
@@ -43,6 +43,7 @@
|
|
|
43
43
|
*/
|
|
44
44
|
|
|
45
45
|
import { UIElement } from '../../core/element.js';
|
|
46
|
+
import { logicalSlotted } from '../../core/logical-children.js';
|
|
46
47
|
|
|
47
48
|
export class UIModal extends UIElement {
|
|
48
49
|
#bound = false;
|
|
@@ -51,6 +52,14 @@ export class UIModal extends UIElement {
|
|
|
51
52
|
#closeTimer = null;
|
|
52
53
|
#dialogRef = null;
|
|
53
54
|
|
|
55
|
+
// Guards `close` against firing twice for one dismissal — #animateClose's
|
|
56
|
+
// timer dispatches it directly (the authoritative path, gh#1295) and the
|
|
57
|
+
// native <dialog> 'close' listener also calls in as a redundant safety
|
|
58
|
+
// net for a consumer that closes the raw <dialog> element itself, and
|
|
59
|
+
// disconnected()'s teardown fallback (below) is a third path into the
|
|
60
|
+
// same guard. Reset whenever the dialog (re)opens.
|
|
61
|
+
#closeDispatched = false;
|
|
62
|
+
|
|
54
63
|
// Monotonic, instance-agnostic counter for the fallback aria-labelledby
|
|
55
64
|
// id stamped onto an author's [slot="heading"] (see render()) — multiple
|
|
56
65
|
// <modal-ui> instances on one page must not collide on the same id.
|
|
@@ -93,6 +102,19 @@ export class UIModal extends UIElement {
|
|
|
93
102
|
};
|
|
94
103
|
|
|
95
104
|
#onDialogClose = () => {
|
|
105
|
+
// Native <dialog> 'close' — fires as a browser-QUEUED task (per the
|
|
106
|
+
// HTML spec, dialog.close() queues a task; it is not synchronous), so
|
|
107
|
+
// it can be lost if a consumer tears the listener down (removes this
|
|
108
|
+
// element) before that task runs. #animateClose's own timer is the
|
|
109
|
+
// authoritative dispatch path (gh#1295); this stays wired as a
|
|
110
|
+
// redundant net for a consumer calling the raw <dialog> element's
|
|
111
|
+
// own `.close()` directly.
|
|
112
|
+
this.#emitCloseOnce();
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
#emitCloseOnce = () => {
|
|
116
|
+
if (this.#closeDispatched) return;
|
|
117
|
+
this.#closeDispatched = true;
|
|
96
118
|
this.open = false;
|
|
97
119
|
this.#previousFocus?.focus();
|
|
98
120
|
this.#previousFocus = null;
|
|
@@ -128,6 +150,32 @@ export class UIModal extends UIElement {
|
|
|
128
150
|
clearTimeout(this.#closeTimer);
|
|
129
151
|
this.#closeTimer = null;
|
|
130
152
|
}
|
|
153
|
+
// Teardown fallback — the ROOT CAUSE of gh#1295 (adiav2 ADIA2-8581).
|
|
154
|
+
// Every internal close path (the native <dialog> 'close' listener,
|
|
155
|
+
// #animateClose's own timer) only ever runs from THIS element's own
|
|
156
|
+
// render effect (element.js's connectedCallback), which is set up as
|
|
157
|
+
// a signals.js effect that reacts to the `open` property write via a
|
|
158
|
+
// MICROTASK — the property setter itself only marks the signal dirty;
|
|
159
|
+
// it doesn't render synchronously. `disconnectedCallback()` disposes
|
|
160
|
+
// every effect (including that one) BEFORE disconnected() runs. A
|
|
161
|
+
// consumer whose own re-render removes <modal-ui> from the DOM in the
|
|
162
|
+
// SAME tick as setting `open = false` — an extremely common framework
|
|
163
|
+
// shape: a Confirm/Cancel handler flips reactive state that ALSO
|
|
164
|
+
// unmounts the modal on that same pass — tears down the render effect
|
|
165
|
+
// before it ever gets a chance to run, so #animateClose's timer is
|
|
166
|
+
// never even CREATED, let alone fired. No error, no warning: the
|
|
167
|
+
// ENTIRE close pipeline silently never starts, and any caller awaiting
|
|
168
|
+
// `close` (showConfirmDialog-style APIs) hangs forever. Confirmed via
|
|
169
|
+
// browser reproduction — vitest/happy-dom's synchronous effect
|
|
170
|
+
// scheduling masked this (see modal.test.js "parent-driven teardown").
|
|
171
|
+
// Disconnection always resolves an open-or-mid-close modal's `close`
|
|
172
|
+
// obligation as a last resort, regardless of which (if any) internal
|
|
173
|
+
// path got a chance to run first — #emitCloseOnce's guard makes this
|
|
174
|
+
// safe to call unconditionally alongside those other paths.
|
|
175
|
+
if (this.open || this.#closing || this.#dialogRef?.open) {
|
|
176
|
+
this.#emitCloseOnce();
|
|
177
|
+
}
|
|
178
|
+
this.#closing = false;
|
|
131
179
|
this.#bound = false;
|
|
132
180
|
this.#dialogRef = null;
|
|
133
181
|
}
|
|
@@ -136,10 +184,15 @@ export class UIModal extends UIElement {
|
|
|
136
184
|
// Read the computed `--modal-duration` (= --a-duration = 250ms unless a
|
|
137
185
|
// consumer overrides) so the JS close timer matches the CSS exit animation
|
|
138
186
|
// exactly — a stale/unset value clipped the animation ~50ms early
|
|
139
|
-
// (bug class shared with swiper bug-29).
|
|
187
|
+
// (bug class shared with swiper bug-29). `parseFloat` alone silently
|
|
188
|
+
// drops a `s` vs `ms` unit (gh#1295 — a consumer-computed value observed
|
|
189
|
+
// as `.25s` parsed to 0.25, a 0.25ms timer instead of 250ms), so a
|
|
190
|
+
// trailing `s` (and not `ms`) is converted explicitly rather than
|
|
191
|
+
// trusted to already be milliseconds.
|
|
140
192
|
const cs = getComputedStyle(this);
|
|
141
193
|
const raw = cs.getPropertyValue('--modal-duration').trim();
|
|
142
|
-
|
|
194
|
+
const ms = /ms\s*$/.test(raw) ? parseFloat(raw) : /s\s*$/.test(raw) ? parseFloat(raw) * 1000 : parseFloat(raw);
|
|
195
|
+
return ms > 0 ? ms : 200;
|
|
143
196
|
}
|
|
144
197
|
|
|
145
198
|
render() {
|
|
@@ -195,8 +248,12 @@ export class UIModal extends UIElement {
|
|
|
195
248
|
// dialog-modal pattern's accessible-name requirement. Re-evaluated
|
|
196
249
|
// every render so a later `text=` set, or the heading being removed,
|
|
197
250
|
// clears the reference instead of leaving it stale. (gh#947, from
|
|
198
|
-
// PR #944.)
|
|
199
|
-
|
|
251
|
+
// PR #944.) `logicalSlotted` (not a `:scope >` direct-child query)
|
|
252
|
+
// pierces the template engine's display:contents/role=presentation
|
|
253
|
+
// wrapper spans, so a consumer that renders the heading conditionally
|
|
254
|
+
// (`${title ? html\`<span slot="heading">…\` : null}`) still gets an
|
|
255
|
+
// aria-labelledby — the wrapper-trap class, gh#1301.
|
|
256
|
+
const authoredHeading = logicalSlotted(header, 'heading')[0];
|
|
200
257
|
if (!this.text && authoredHeading) {
|
|
201
258
|
if (!authoredHeading.id) authoredHeading.id = `modal-heading-${++UIModal.#headingIdSeq}`;
|
|
202
259
|
if (dialog.getAttribute('aria-labelledby') !== authoredHeading.id) {
|
|
@@ -261,6 +318,7 @@ export class UIModal extends UIElement {
|
|
|
261
318
|
// See .claude/docs/BROWSER-COMPAT.md §3a (Flavor C).
|
|
262
319
|
if (this.open && !dialog.open) {
|
|
263
320
|
this.#closing = false;
|
|
321
|
+
this.#closeDispatched = false;
|
|
264
322
|
this.#previousFocus = document.activeElement;
|
|
265
323
|
dialog.showModal();
|
|
266
324
|
void dialog.offsetHeight;
|
|
@@ -285,6 +343,12 @@ export class UIModal extends UIElement {
|
|
|
285
343
|
this.#closing = false;
|
|
286
344
|
dialog.removeAttribute('data-closing');
|
|
287
345
|
if (dialog.open) dialog.close();
|
|
346
|
+
// Authoritative dispatch (gh#1295) — don't rely solely on the native
|
|
347
|
+
// <dialog> 'close' listener above, whose task can be lost if this
|
|
348
|
+
// element (or an ancestor) is torn down before it runs.
|
|
349
|
+
// #emitCloseOnce guards so a close event ALSO reaching
|
|
350
|
+
// #onDialogClose is a no-op.
|
|
351
|
+
this.#emitCloseOnce();
|
|
288
352
|
}, this.#getDuration());
|
|
289
353
|
}
|
|
290
354
|
|
|
@@ -31,6 +31,11 @@
|
|
|
31
31
|
"type": "string",
|
|
32
32
|
"default": ""
|
|
33
33
|
},
|
|
34
|
+
"multiExpand": {
|
|
35
|
+
"description": "Opt out of the default single-expanded-group behavior. When unset, selecting a nav-item-ui collapses every nav-group-ui except the one containing the newly selected item (all, when the selection is ungrouped); manual multi-expansion still works up until the next selection.",
|
|
36
|
+
"type": "boolean",
|
|
37
|
+
"default": false
|
|
38
|
+
},
|
|
34
39
|
"variant": {
|
|
35
40
|
"description": "Visual treatment. primary = app sidebar; section = subnav rail.",
|
|
36
41
|
"type": "string",
|
|
@@ -45,10 +45,14 @@ import { UIElement } from '../../core/element.js';
|
|
|
45
45
|
|
|
46
46
|
export class UINav extends UIElement {
|
|
47
47
|
static properties = {
|
|
48
|
-
variant:
|
|
49
|
-
collapsed:
|
|
50
|
-
divider:
|
|
51
|
-
heading:
|
|
48
|
+
variant: { type: String, default: 'primary', reflect: true },
|
|
49
|
+
collapsed: { type: Boolean, default: false, reflect: true },
|
|
50
|
+
divider: { type: Boolean, default: false, reflect: true },
|
|
51
|
+
heading: { type: String, default: '', reflect: true },
|
|
52
|
+
// gh#1306: selection is the cleanup event for manually multi-expanded
|
|
53
|
+
// groups — default-on; opt out to let multiple groups stay open across
|
|
54
|
+
// selections (the operator's ruling, 2026-08-14).
|
|
55
|
+
multiExpand: { type: Boolean, default: false, reflect: true, attribute: 'multi-expand' },
|
|
52
56
|
};
|
|
53
57
|
|
|
54
58
|
static template = () => null;
|
|
@@ -99,6 +103,7 @@ export class UINav extends UIElement {
|
|
|
99
103
|
if (item) {
|
|
100
104
|
item.setAttribute('selected', '');
|
|
101
105
|
this.#flushHoverState();
|
|
106
|
+
this.#collapseSiblingGroups(item);
|
|
102
107
|
this.dispatchEvent(new CustomEvent('nav-select', {
|
|
103
108
|
bubbles: true,
|
|
104
109
|
detail: { item, text: item.text, value: item.value },
|
|
@@ -106,6 +111,21 @@ export class UINav extends UIElement {
|
|
|
106
111
|
}
|
|
107
112
|
}
|
|
108
113
|
|
|
114
|
+
// gh#1306: on selection, collapse every top-level group except the one
|
|
115
|
+
// containing the newly selected item (none, when the selection is
|
|
116
|
+
// ungrouped — every group collapses). Manual multi-expansion stays
|
|
117
|
+
// allowed right up until the next selection; [multi-expand] opts out
|
|
118
|
+
// entirely. Runs for every select() caller — click, keyboard, and the
|
|
119
|
+
// collapsed-rail popover path (nav-group.class.js's showPopover(),
|
|
120
|
+
// which also calls nav.select()).
|
|
121
|
+
#collapseSiblingGroups(item) {
|
|
122
|
+
if (this.multiExpand) return;
|
|
123
|
+
const activeGroup = item.closest('nav-group-ui');
|
|
124
|
+
for (const group of this.querySelectorAll(':scope > nav-group-ui')) {
|
|
125
|
+
if (group !== activeGroup) group.open = false;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
109
129
|
toggle() {
|
|
110
130
|
if (this.variant === 'section') return; // no-op for section variant
|
|
111
131
|
this.collapsed = !this.collapsed;
|
package/components/nav/nav.css
CHANGED
|
@@ -80,7 +80,7 @@
|
|
|
80
80
|
font-weight: var(--nav-label-weight);
|
|
81
81
|
color: var(--nav-label-fg);
|
|
82
82
|
text-transform: uppercase;
|
|
83
|
-
letter-spacing:
|
|
83
|
+
letter-spacing: var(--a-font-tracking-wider);
|
|
84
84
|
white-space: nowrap;
|
|
85
85
|
overflow: hidden;
|
|
86
86
|
}
|
|
@@ -134,7 +134,7 @@
|
|
|
134
134
|
font-weight: var(--nav-label-weight);
|
|
135
135
|
color: var(--nav-label-fg);
|
|
136
136
|
text-transform: uppercase;
|
|
137
|
-
letter-spacing:
|
|
137
|
+
letter-spacing: var(--a-font-tracking-wider);
|
|
138
138
|
white-space: nowrap;
|
|
139
139
|
overflow: hidden;
|
|
140
140
|
}
|
package/components/nav/nav.d.ts
CHANGED
|
@@ -38,6 +38,8 @@ export class UINav extends UIElement {
|
|
|
38
38
|
divider: boolean;
|
|
39
39
|
/** Optional kicker label. Section variant renders it via ::before; primary uses it as aria-label only. */
|
|
40
40
|
heading: string;
|
|
41
|
+
/** Opt out of the default single-expanded-group behavior. When unset, selecting a nav-item-ui collapses every nav-group-ui except the one containing the newly selected item (all, when the selection is ungrouped); manual multi-expansion still works up until the next selection. */
|
|
42
|
+
multiExpand: boolean;
|
|
41
43
|
/** Visual treatment. primary = app sidebar; section = subnav rail. */
|
|
42
44
|
variant: 'primary' | 'section';
|
|
43
45
|
|
package/components/nav/nav.yaml
CHANGED
|
@@ -39,6 +39,12 @@ props:
|
|
|
39
39
|
type: string
|
|
40
40
|
default: ''
|
|
41
41
|
description: "Optional kicker label. Section variant renders it via ::before; primary uses it as aria-label only."
|
|
42
|
+
multiExpand:
|
|
43
|
+
type: boolean
|
|
44
|
+
default: false
|
|
45
|
+
reflect: true
|
|
46
|
+
attribute: multi-expand
|
|
47
|
+
description: "Opt out of the default single-expanded-group behavior. When unset, selecting a nav-item-ui collapses every nav-group-ui except the one containing the newly selected item (all, when the selection is ungrouped); manual multi-expansion still works up until the next selection."
|
|
42
48
|
|
|
43
49
|
events:
|
|
44
50
|
nav-select:
|
|
@@ -91,6 +97,18 @@ a2ui:
|
|
|
91
97
|
or anchor) → <nav-ui>. If the user switches VIEWS within the same
|
|
92
98
|
logical page → <tabs-ui>. Never use <nav-ui> as an in-page section
|
|
93
99
|
switcher.
|
|
100
|
+
- >-
|
|
101
|
+
Group expansion on selection: selecting a <nav-item-ui> collapses
|
|
102
|
+
every <nav-group-ui> except the one containing the newly selected
|
|
103
|
+
item (all groups, when the selection is ungrouped). Manual
|
|
104
|
+
multi-expansion is allowed right up until the next selection. Set
|
|
105
|
+
[multi-expand] on <nav-ui> to opt out and keep multiple groups open
|
|
106
|
+
across selections. This clears the JS [open] state (and
|
|
107
|
+
aria-expanded) on every collapsed group in BOTH variants — primary
|
|
108
|
+
visually hides that group's children as a result; section variant's
|
|
109
|
+
CSS keeps children visible regardless of [open] (per the
|
|
110
|
+
section-variant cascade rule above), so only the accessibility
|
|
111
|
+
state resets there, with no visible change.
|
|
94
112
|
- >-
|
|
95
113
|
Anti-patterns: do NOT wrap <nav-ui> children in <col-ui> /
|
|
96
114
|
<row-ui> — wrapping breaks selection bubbling + the variant
|
|
@@ -169,7 +169,7 @@
|
|
|
169
169
|
background: var(--nav-group-badge-bg);
|
|
170
170
|
padding: 1px var(--nav-group-badge-px);
|
|
171
171
|
border-radius: var(--nav-group-badge-radius);
|
|
172
|
-
line-height:
|
|
172
|
+
line-height: var(--a-font-leading-normal);
|
|
173
173
|
}
|
|
174
174
|
|
|
175
175
|
[slot="header"] [slot="caret"] {
|
|
@@ -313,7 +313,7 @@ nav-group-ui [slot="popover-label"] {
|
|
|
313
313
|
color: var(--nav-group-fg-muted);
|
|
314
314
|
font-size: var(--a-ui-tiny);
|
|
315
315
|
text-transform: uppercase;
|
|
316
|
-
letter-spacing:
|
|
316
|
+
letter-spacing: var(--a-font-tracking-wider);
|
|
317
317
|
}
|
|
318
318
|
|
|
319
319
|
nav-group-ui [slot="popover"] [role="option"] {
|
|
@@ -364,7 +364,7 @@ nav-ui[variant="section"] > nav-group-ui:not([variant]) > [slot="header"] {
|
|
|
364
364
|
font-weight: var(--a-weight-medium);
|
|
365
365
|
color: var(--nav-group-fg-muted);
|
|
366
366
|
text-transform: uppercase;
|
|
367
|
-
letter-spacing:
|
|
367
|
+
letter-spacing: var(--a-font-tracking-wider);
|
|
368
368
|
cursor: default;
|
|
369
369
|
border-radius: 0;
|
|
370
370
|
}
|
|
@@ -136,7 +136,7 @@ nav-item-ui[selected] [slot="icon"]:empty::before {
|
|
|
136
136
|
background: var(--nav-item-badge-bg);
|
|
137
137
|
padding: 1px var(--nav-item-badge-px);
|
|
138
138
|
border-radius: var(--nav-item-badge-radius);
|
|
139
|
-
line-height:
|
|
139
|
+
line-height: var(--a-font-leading-normal);
|
|
140
140
|
margin-inline-start: auto;
|
|
141
141
|
}
|
|
142
142
|
|
|
@@ -147,7 +147,7 @@ nav-item-ui[selected] [slot="icon"]:empty::before {
|
|
|
147
147
|
border: 1px solid var(--nav-item-trailing-border);
|
|
148
148
|
border-radius: var(--nav-item-trailing-radius);
|
|
149
149
|
padding: 0 var(--nav-item-trailing-px);
|
|
150
|
-
line-height:
|
|
150
|
+
line-height: var(--a-font-leading-loose);
|
|
151
151
|
flex-shrink: 0;
|
|
152
152
|
}
|
|
153
153
|
|
|
@@ -77,7 +77,7 @@ option-card-ui[checked] [slot="icon"] {
|
|
|
77
77
|
--option-card-heading-size: var(--a-ui-size);
|
|
78
78
|
--option-card-desc-color: var(--a-fg-muted);
|
|
79
79
|
--option-card-desc-size: var(--a-ui-sm);
|
|
80
|
-
--option-card-desc-line-height:
|
|
80
|
+
--option-card-desc-line-height: var(--a-font-leading-relaxed);
|
|
81
81
|
--option-card-icon-color: var(--md-sys-color-neutral-on-surface-variant);
|
|
82
82
|
--option-card-icon-color-checked: var(--a-fg-strong);
|
|
83
83
|
--option-card-icon-size: 1.5rem;
|
|
@@ -56,7 +56,7 @@
|
|
|
56
56
|
color: var(--otp-input-fg);
|
|
57
57
|
font-family: var(--otp-input-font);
|
|
58
58
|
font-size: var(--otp-input-font-size);
|
|
59
|
-
line-height:
|
|
59
|
+
line-height: var(--a-font-leading-none);
|
|
60
60
|
outline: none;
|
|
61
61
|
transition: border-color var(--otp-input-duration) var(--otp-input-easing);
|
|
62
62
|
/* Hide number spinner */
|
package/components/page/page.css
CHANGED
|
@@ -183,7 +183,7 @@
|
|
|
183
183
|
:scope > :where(header, header-ui) > :is(text-ui[variant="display"], text-ui[variant="title"], text-ui[variant="heading"], text-ui[variant="subsection"]):not([slot]),
|
|
184
184
|
:scope > :where(header, header-ui) > [slot="heading"] :is(h1, h2, h3, h4, h5, h6) {
|
|
185
185
|
grid-row: 1;
|
|
186
|
-
line-height:
|
|
186
|
+
line-height: var(--a-font-leading-normal);
|
|
187
187
|
margin: 0;
|
|
188
188
|
}
|
|
189
189
|
|
|
@@ -210,7 +210,7 @@
|
|
|
210
210
|
:scope > :where(header, header-ui) > :is(text-ui[variant="body"], text-ui[variant="caption"]):not([slot]) {
|
|
211
211
|
grid-row: 2;
|
|
212
212
|
grid-column: 1 / -1;
|
|
213
|
-
line-height:
|
|
213
|
+
line-height: var(--a-font-leading-relaxed);
|
|
214
214
|
margin: 0;
|
|
215
215
|
}
|
|
216
216
|
:scope > :where(header, header-ui):has(> [slot="icon"]) > :is([slot="description"], p:not([slot]), small:not([slot])) { grid-column: 2 / -1; }
|
package/components/pane/pane.css
CHANGED
|
@@ -141,7 +141,7 @@
|
|
|
141
141
|
color: var(--pane-section-header-fg);
|
|
142
142
|
padding-bottom: var(--pane-col-gap);
|
|
143
143
|
text-transform: uppercase;
|
|
144
|
-
letter-spacing:
|
|
144
|
+
letter-spacing: var(--a-font-tracking-wider);
|
|
145
145
|
font-weight: var(--pane-section-header-weight);
|
|
146
146
|
}
|
|
147
147
|
|
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
grid-area: label;
|
|
32
32
|
font-size: var(--progress-row-label-size);
|
|
33
33
|
color: var(--progress-row-label-fg);
|
|
34
|
-
line-height:
|
|
34
|
+
line-height: var(--a-font-leading-normal);
|
|
35
35
|
min-width: 0;
|
|
36
36
|
overflow: hidden;
|
|
37
37
|
text-overflow: ellipsis;
|
|
@@ -44,7 +44,7 @@
|
|
|
44
44
|
color: var(--progress-row-meta-fg);
|
|
45
45
|
font-variant-numeric: tabular-nums;
|
|
46
46
|
justify-self: end;
|
|
47
|
-
line-height:
|
|
47
|
+
line-height: var(--a-font-leading-normal);
|
|
48
48
|
}
|
|
49
49
|
|
|
50
50
|
progress-ui {
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
3
|
"$id": "https://adiaui.dev/a2ui/v0_9/components/QRCode.json",
|
|
4
4
|
"title": "QRCode",
|
|
5
|
-
"description": "Inline QR code generator — renders a scannable code as inline SVG.\nTwo modes:\n\n**A. Auto-encode (default).** Set `[value]` to a string (URL, plain\ntext, share link, 2FA secret). The component encodes the data with\nthe built-in zero-deps QR encoder (byte mode, ECC level configurable,\nversions 1–10 up to ~150 byte chars) and renders an SVG.\n\n**B. Bring-your-own matrix.** Set `[matrix]` to a JSON string of a\n2-D 0/1 array (`[[1,0,1,...],[...],...]`). The component renders the\nsupplied matrix directly without invoking its own encoder. Use this\nwhen you need versions 11–40, kanji/alphanumeric mode, or any\nencoder feature beyond the built-in. When both `[value]` and\n`[matrix]` are set, the matrix wins (BYO override).\n\nOutput is a single `<svg>` with `viewBox=\"0 0 N N\"`. Crisp-edges\nrendering preserves the scannable cell boundaries at any size.\nSizing is controlled by the `[size]` prop (host CSS-pixel dimension);\ninternal cell size is computed from `size / (matrix-size + 2 ·\
|
|
5
|
+
"description": "Inline QR code generator — renders a scannable code as inline SVG.\nTwo modes:\n\n**A. Auto-encode (default).** Set `[value]` to a string (URL, plain\ntext, share link, 2FA secret). The component encodes the data with\nthe built-in zero-deps QR encoder (byte mode, ECC level configurable,\nversions 1–10 up to ~150 byte chars) and renders an SVG.\n\n**B. Bring-your-own matrix.** Set `[matrix]` to a JSON string of a\n2-D 0/1 array (`[[1,0,1,...],[...],...]`). The component renders the\nsupplied matrix directly without invoking its own encoder. Use this\nwhen you need versions 11–40, kanji/alphanumeric mode, or any\nencoder feature beyond the built-in. When both `[value]` and\n`[matrix]` are set, the matrix wins (BYO override).\n\nOutput is a single `<svg>` with `viewBox=\"0 0 N N\"`. Crisp-edges\nrendering preserves the scannable cell boundaries at any size.\nSizing is controlled by the `[size]` prop (host CSS-pixel dimension);\ninternal cell size is computed from `size / (matrix-size + 2 ·\nquiet-zone)`. ARIA: `role=\"img\"` with `aria-label` (default \"QR code\"\nor the `[label]` prop).\n",
|
|
6
6
|
"type": "object",
|
|
7
7
|
"allOf": [
|
|
8
8
|
{
|
|
@@ -42,18 +42,18 @@
|
|
|
42
42
|
"type": "string",
|
|
43
43
|
"default": "QR code"
|
|
44
44
|
},
|
|
45
|
-
"margin": {
|
|
46
|
-
"description": "Quiet zone in cells around the QR pattern. QR spec requires a\nminimum 4-cell margin for reliable scanning; smaller values may\nnot scan on some devices. Default `4`.\n",
|
|
47
|
-
"type": "number",
|
|
48
|
-
"default": 4
|
|
49
|
-
},
|
|
50
45
|
"matrix": {
|
|
51
46
|
"description": "BYO precomputed bit matrix as JSON — a 2-D array of 0/1\n(`[[1,0,...],[0,1,...],...]`). When set, the built-in encoder is\nbypassed and this matrix renders directly. Useful for QR variants\nthe built-in doesn't support (versions 11–40, alphanumeric mode,\nkanji, etc.) — bring your own encoder, pass the matrix here.\n",
|
|
52
47
|
"type": "string",
|
|
53
48
|
"default": ""
|
|
54
49
|
},
|
|
50
|
+
"quietZone": {
|
|
51
|
+
"description": "Quiet zone in cells around the QR pattern. QR spec requires a\nminimum 4-cell quiet zone for reliable scanning; smaller values\nmay not scan on some devices. Default `4`. (Renamed from\n`[margin]` — that name collided with the framework's global\n`[margin]` Scale attribute; `margin` on `qr-code-ui` now means\nthe global spacing attribute, per ADR-0053.)\n",
|
|
52
|
+
"type": "number",
|
|
53
|
+
"default": 4
|
|
54
|
+
},
|
|
55
55
|
"size": {
|
|
56
|
-
"description": "Host display size in CSS pixels (width and height; the SVG is\nalways square). Default 200. Internal cell size is computed from\nthis divided by `matrix-size + 2·
|
|
56
|
+
"description": "Host display size in CSS pixels (width and height; the SVG is\nalways square). Default 200. Internal cell size is computed from\nthis divided by `matrix-size + 2·quiet-zone`.\n",
|
|
57
57
|
"type": "number",
|
|
58
58
|
"default": 200
|
|
59
59
|
},
|
|
@@ -75,9 +75,9 @@
|
|
|
75
75
|
"wrong": "<qr-code-ui value=\"https://very.long.url.that.exceeds.150.bytes.of.utf8.encoded.length...\"></qr-code-ui>\n"
|
|
76
76
|
},
|
|
77
77
|
{
|
|
78
|
-
"fix": "<qr-code-ui value=\"...\"
|
|
79
|
-
"why": "Zero
|
|
80
|
-
"wrong": "<qr-code-ui value=\"...\"
|
|
78
|
+
"fix": "<qr-code-ui value=\"...\" quiet-zone=\"4\"></qr-code-ui>\n",
|
|
79
|
+
"why": "Zero quiet zone breaks scanning. QR spec requires a 4-cell quiet\nzone for reliable detection.\n",
|
|
80
|
+
"wrong": "<qr-code-ui value=\"...\" quiet-zone=\"0\"></qr-code-ui>\n"
|
|
81
81
|
}
|
|
82
82
|
],
|
|
83
83
|
"category": "display",
|
|
@@ -33,7 +33,7 @@ export class UIQRCode extends UIElement {
|
|
|
33
33
|
matrix: { type: String, default: '', reflect: true },
|
|
34
34
|
size: { type: Number, default: 200, reflect: true },
|
|
35
35
|
errorCorrection: { type: String, default: 'M', reflect: true, attribute: 'error-correction' },
|
|
36
|
-
|
|
36
|
+
quietZone: { type: Number, default: 4, reflect: true, attribute: 'quiet-zone' },
|
|
37
37
|
color: { type: String, default: '', reflect: true },
|
|
38
38
|
background: { type: String, default: '', reflect: true },
|
|
39
39
|
label: { type: String, default: 'QR code', reflect: true },
|
|
@@ -91,7 +91,7 @@ export class UIQRCode extends UIElement {
|
|
|
91
91
|
// Cheap idempotence: compute a signature of inputs, skip re-render
|
|
92
92
|
// if nothing changed (encoding is non-trivial — version selection +
|
|
93
93
|
// RS ECC + 8 mask evaluations).
|
|
94
|
-
const sig = `${this.value}|${this.matrix}|${this.size}|${this.errorCorrection}|${this.
|
|
94
|
+
const sig = `${this.value}|${this.matrix}|${this.size}|${this.errorCorrection}|${this.quietZone}|${this.color}|${this.background}`;
|
|
95
95
|
if (sig === this.#lastSig && this.querySelector('svg')) return;
|
|
96
96
|
this.#lastSig = sig;
|
|
97
97
|
|
|
@@ -106,7 +106,7 @@ export class UIQRCode extends UIElement {
|
|
|
106
106
|
|
|
107
107
|
const svg = matrixToSVG(matrix, {
|
|
108
108
|
cellSize: 8, // arbitrary internal unit; host CSS controls actual size via [size]
|
|
109
|
-
margin: Number.isFinite(this.
|
|
109
|
+
margin: Number.isFinite(this.quietZone) ? this.quietZone : 4,
|
|
110
110
|
// Hardcoded fallback to black-on-white — the scannable convention.
|
|
111
111
|
// Theme-aware `currentColor` produces light-on-dark in dark mode,
|
|
112
112
|
// which most phone cameras (iPhone Camera in particular) refuse to
|
|
@@ -18,7 +18,7 @@ Output is a single `<svg>` with `viewBox="0 0 N N"`. Crisp-edges
|
|
|
18
18
|
rendering preserves the scannable cell boundaries at any size.
|
|
19
19
|
Sizing is controlled by the `[size]` prop (host CSS-pixel dimension);
|
|
20
20
|
internal cell size is computed from `size / (matrix-size + 2 ·
|
|
21
|
-
|
|
21
|
+
quiet-zone)`. ARIA: `role="img"` with `aria-label` (default "QR code"
|
|
22
22
|
or the `[label]` prop).
|
|
23
23
|
|
|
24
24
|
*
|
|
@@ -58,11 +58,6 @@ from more damage at the cost of QR-version size growth.
|
|
|
58
58
|
better AT context (e.g. "Share link QR code", "2FA setup QR").
|
|
59
59
|
*/
|
|
60
60
|
label: string;
|
|
61
|
-
/** Quiet zone in cells around the QR pattern. QR spec requires a
|
|
62
|
-
minimum 4-cell margin for reliable scanning; smaller values may
|
|
63
|
-
not scan on some devices. Default `4`.
|
|
64
|
-
*/
|
|
65
|
-
margin: number;
|
|
66
61
|
/** BYO precomputed bit matrix as JSON — a 2-D array of 0/1
|
|
67
62
|
(`[[1,0,...],[0,1,...],...]`). When set, the built-in encoder is
|
|
68
63
|
bypassed and this matrix renders directly. Useful for QR variants
|
|
@@ -70,9 +65,17 @@ the built-in doesn't support (versions 11–40, alphanumeric mode,
|
|
|
70
65
|
kanji, etc.) — bring your own encoder, pass the matrix here.
|
|
71
66
|
*/
|
|
72
67
|
matrix: string;
|
|
68
|
+
/** Quiet zone in cells around the QR pattern. QR spec requires a
|
|
69
|
+
minimum 4-cell quiet zone for reliable scanning; smaller values
|
|
70
|
+
may not scan on some devices. Default `4`. (Renamed from
|
|
71
|
+
`[margin]` — that name collided with the framework's global
|
|
72
|
+
`[margin]` Scale attribute; `margin` on `qr-code-ui` now means
|
|
73
|
+
the global spacing attribute, per ADR-0053.)
|
|
74
|
+
*/
|
|
75
|
+
quietZone: number;
|
|
73
76
|
/** Host display size in CSS pixels (width and height; the SVG is
|
|
74
77
|
always square). Default 200. Internal cell size is computed from
|
|
75
|
-
this divided by `matrix-size + 2·
|
|
78
|
+
this divided by `matrix-size + 2·quiet-zone`.
|
|
76
79
|
*/
|
|
77
80
|
size: number;
|
|
78
81
|
/** The string to encode (URL / text / share link / 2FA seed). When
|
|
@@ -25,7 +25,7 @@ description: |
|
|
|
25
25
|
rendering preserves the scannable cell boundaries at any size.
|
|
26
26
|
Sizing is controlled by the `[size]` prop (host CSS-pixel dimension);
|
|
27
27
|
internal cell size is computed from `size / (matrix-size + 2 ·
|
|
28
|
-
|
|
28
|
+
quiet-zone)`. ARIA: `role="img"` with `aria-label` (default "QR code"
|
|
29
29
|
or the `[label]` prop).
|
|
30
30
|
props:
|
|
31
31
|
value:
|
|
@@ -50,7 +50,7 @@ props:
|
|
|
50
50
|
description: |
|
|
51
51
|
Host display size in CSS pixels (width and height; the SVG is
|
|
52
52
|
always square). Default 200. Internal cell size is computed from
|
|
53
|
-
this divided by `matrix-size + 2·
|
|
53
|
+
this divided by `matrix-size + 2·quiet-zone`.
|
|
54
54
|
type: number
|
|
55
55
|
default: 200
|
|
56
56
|
reflect: true
|
|
@@ -64,14 +64,18 @@ props:
|
|
|
64
64
|
enum: [L, M, Q, H]
|
|
65
65
|
reflect: true
|
|
66
66
|
attribute: error-correction
|
|
67
|
-
|
|
67
|
+
quietZone:
|
|
68
68
|
description: |
|
|
69
69
|
Quiet zone in cells around the QR pattern. QR spec requires a
|
|
70
|
-
minimum 4-cell
|
|
71
|
-
not scan on some devices. Default `4`.
|
|
70
|
+
minimum 4-cell quiet zone for reliable scanning; smaller values
|
|
71
|
+
may not scan on some devices. Default `4`. (Renamed from
|
|
72
|
+
`[margin]` — that name collided with the framework's global
|
|
73
|
+
`[margin]` Scale attribute; `margin` on `qr-code-ui` now means
|
|
74
|
+
the global spacing attribute, per ADR-0053.)
|
|
72
75
|
type: number
|
|
73
76
|
default: 4
|
|
74
77
|
reflect: true
|
|
78
|
+
attribute: quiet-zone
|
|
75
79
|
color:
|
|
76
80
|
description: |
|
|
77
81
|
Foreground (dark-cell) color. Any CSS color string (hex, rgb,
|
|
@@ -125,7 +129,7 @@ a2ui:
|
|
|
125
129
|
reason: "Encoder capacity contract."
|
|
126
130
|
- rule: "When QR sits over a non-white surface, set [background] to a solid color. White-on-image QR backgrounds are unreliable for scanners — quiet-zone contrast against the dark cells is the load-bearing visual."
|
|
127
131
|
reason: "Scannability constraint."
|
|
128
|
-
- rule: "The [
|
|
132
|
+
- rule: "The [quiet-zone] defaults to 4 cells per QR spec. Smaller values may scan on some devices but not others; do not go below 2."
|
|
129
133
|
reason: "Quiet-zone requirement."
|
|
130
134
|
anti_patterns:
|
|
131
135
|
- wrong: |
|
|
@@ -140,12 +144,12 @@ anti_patterns:
|
|
|
140
144
|
with a BYO encoder library + pass the [matrix]:
|
|
141
145
|
`<qr-code-ui matrix="[[1,0,1,...],...]"></qr-code-ui>`
|
|
142
146
|
- wrong: |
|
|
143
|
-
<qr-code-ui value="..."
|
|
147
|
+
<qr-code-ui value="..." quiet-zone="0"></qr-code-ui>
|
|
144
148
|
why: |
|
|
145
|
-
Zero
|
|
149
|
+
Zero quiet zone breaks scanning. QR spec requires a 4-cell quiet
|
|
146
150
|
zone for reliable detection.
|
|
147
151
|
fix: |
|
|
148
|
-
<qr-code-ui value="..."
|
|
152
|
+
<qr-code-ui value="..." quiet-zone="4"></qr-code-ui>
|
|
149
153
|
examples:
|
|
150
154
|
- name: default
|
|
151
155
|
description: Plain URL encoding at default size + ECC-M.
|
|
@@ -69,7 +69,7 @@ range-ui:not([disabled]) [slot="field"]:hover [data-layer="fill"] {
|
|
|
69
69
|
/* font-family floor — see segment.css rationale. */
|
|
70
70
|
font-family: var(--range-font-family);
|
|
71
71
|
font-size: var(--range-font-size);
|
|
72
|
-
line-height:
|
|
72
|
+
line-height: var(--a-font-leading-none);
|
|
73
73
|
transition:
|
|
74
74
|
border-color var(--range-duration) var(--range-easing),
|
|
75
75
|
background var(--range-duration) var(--range-easing);
|
|
@@ -164,7 +164,7 @@
|
|
|
164
164
|
font-weight: var(--richtext-h4-weight);
|
|
165
165
|
color: var(--richtext-fg-muted);
|
|
166
166
|
text-transform: uppercase;
|
|
167
|
-
letter-spacing:
|
|
167
|
+
letter-spacing: var(--a-font-tracking-wide);
|
|
168
168
|
margin: var(--richtext-h4-margin-top) 0 var(--richtext-h4-margin-bottom);
|
|
169
169
|
}
|
|
170
170
|
|
|
@@ -173,7 +173,7 @@
|
|
|
173
173
|
font-weight: var(--richtext-h4-weight);
|
|
174
174
|
color: var(--richtext-fg-muted);
|
|
175
175
|
text-transform: uppercase;
|
|
176
|
-
letter-spacing:
|
|
176
|
+
letter-spacing: var(--a-font-tracking-wider);
|
|
177
177
|
margin: var(--richtext-h4-margin-top) 0 var(--richtext-h4-margin-bottom);
|
|
178
178
|
}
|
|
179
179
|
|
|
@@ -212,7 +212,7 @@
|
|
|
212
212
|
padding: var(--richtext-pre-inset);
|
|
213
213
|
overflow-x: auto;
|
|
214
214
|
font-size: var(--richtext-code-size);
|
|
215
|
-
line-height:
|
|
215
|
+
line-height: var(--a-font-leading-relaxed);
|
|
216
216
|
margin: var(--richtext-pre-my) 0;
|
|
217
217
|
}
|
|
218
218
|
|
|
@@ -23,9 +23,10 @@ segment-ui[selected] {
|
|
|
23
23
|
`line-height: 1` is deliberate, but the label's ::after clips to its
|
|
24
24
|
own line box, so inheriting 1 makes half-leading negative and shears
|
|
25
25
|
descenders (g/j/p/q/y, parentheses) by ~0.93px at the default size —
|
|
26
|
-
gh#906.
|
|
27
|
-
mirrors --button-label-line-height.
|
|
28
|
-
|
|
26
|
+
gh#906. Normal (1.35) contains the font's content box at every
|
|
27
|
+
[size]; mirrors --button-label-line-height. Value-nearest migration
|
|
28
|
+
from the retired --a-leading-snug (1.3) — ADR-0052. */
|
|
29
|
+
--segment-label-line-height: var(--a-font-leading-normal);
|
|
29
30
|
|
|
30
31
|
/* ── Colors ── */
|
|
31
32
|
--segment-fg: var(--a-ui-text-subtle);
|
|
@@ -60,7 +61,7 @@ segment-ui[selected] {
|
|
|
60
61
|
font-family: var(--segment-font-family);
|
|
61
62
|
font-size: var(--segment-font-size);
|
|
62
63
|
font-weight: var(--segment-font-weight);
|
|
63
|
-
line-height:
|
|
64
|
+
line-height: var(--a-font-leading-none);
|
|
64
65
|
color: var(--segment-fg);
|
|
65
66
|
white-space: nowrap;
|
|
66
67
|
overflow: hidden;
|