@adia-ai/web-components 0.8.33 → 0.8.34
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 +24 -0
- package/components/agent-reasoning/agent-reasoning.a2ui.json +1 -1
- package/components/agent-reasoning/agent-reasoning.yaml +3 -3
- package/components/badge/badge.css +6 -4
- package/components/button/button.css +12 -0
- package/components/combobox/combobox.a2ui.json +1 -1
- package/components/combobox/combobox.yaml +3 -0
- package/components/command/command.a2ui.json +4 -0
- package/components/command/command.class.js +54 -4
- package/components/command/command.d.ts +2 -0
- package/components/command/command.yaml +25 -3
- package/components/date-range-picker/date-range-picker.a2ui.json +1 -1
- package/components/date-range-picker/date-range-picker.class.js +42 -43
- package/components/date-range-picker/date-range-picker.css +11 -0
- package/components/date-range-picker/date-range-picker.yaml +6 -3
- package/components/description-list/description-list.css +16 -4
- package/components/modal/modal.class.js +43 -2
- package/components/modal/modal.css +23 -0
- package/components/nav-group/nav-group.css +15 -4
- package/components/nav-item/nav-item.css +11 -3
- package/components/richtext/richtext.css +5 -1
- package/components/segment/segment.css +10 -0
- package/components/step-progress/step-progress.css +16 -3
- package/components/table/cell-types.js +8 -0
- package/components/table/table.a2ui.json +1 -1
- package/components/table/table.class.js +11 -0
- package/components/table/table.css +9 -0
- package/components/table/table.d.ts +1 -1
- package/components/table/table.yaml +1 -1
- package/components/tabs/tabs.css +3 -1
- package/components/tag/tag.css +7 -4
- package/core/element.js +31 -0
- package/core/signals.js +3 -1
- package/dist/theme-provider.min.js +2 -2
- package/dist/web-components.min.css +1 -1
- package/dist/web-components.min.js +40 -40
- package/dist/web-components.sheet.js +1 -1
- package/package.json +1 -1
- package/patterns/responsive-shell-sidebar/responsive-shell-sidebar.examples.html +135 -147
- package/styles/colors/semantics/core.css +22 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,29 @@
|
|
|
1
1
|
# Changelog — @adia-ai/web-components
|
|
2
2
|
|
|
3
|
+
## [0.8.34] — 2026-08-11
|
|
4
|
+
|
|
5
|
+
### Fixed
|
|
6
|
+
|
|
7
|
+
- **`date-range-picker-ui` — both calendar grids are now ONE click-start-click-end selection surface (gh#948, reported from adiav2 ADIA2-8020).** The change handlers were role-locked per pane (`from`-pane clicks could only ever write the range start), so a same-month range required paging the right pane back a month. First click = start, second = end in either grid; a second click earlier than the pending start restarts there; a click after a committed range starts fresh. Popover content-sizing (`width: max-content` + viewport/token caps) moved into the stylesheet so the JS anchor fallback and anchoring failures can no longer render a viewport-huge panel.
|
|
8
|
+
- **`UIElement` — `reflect:true` booleans with a TRUE default now stamp their attribute on connect (gh#961).** installProps seeds the signal but can't write attributes (constructor restriction), and the setter's `Object.is` short-circuit skips reflection on same-value sets — so attribute-keyed CSS (`chat-thread:not([empty]) > chat-empty`, `editor-canvas:not([empty]) > editor-canvas-empty`, password-strength's `:not([show-label])`) read wrong state forever; 9 components carried the latent shape. The connect-time pre-sync loop now runs inside `untracked()` — its property reads otherwise subscribe a PARENT's render effect to template-created children's signals, the unbounded re-stamp oscillation the signals drain guard traps (the guard now names the still-dirty effect hosts).
|
|
9
|
+
- **`modal-ui` — `aria-labelledby` falls back to an authored `[slot="heading"]` when `text=` is absent (gh#947), and an authored bare `<header>` no longer displaces into the body (gh#941).** Header icon+heading row layout is by tag + DOM order: the last non-button child claims the row, earlier content stays fixed-width.
|
|
10
|
+
- **`table-ui` — composite-cell wrapper flex generalized to CSS (gh#940):** `[data-body] [role="gridcell"] > row-ui { flex: 1 }` covers progress AND avatar cells; the progress bar no longer collapses to ~33px. `<col-def meta>` JSON attribute carries per-column config (badge variant maps) declaratively.
|
|
11
|
+
- **`step-progress-ui` — direction-agnostic sizing (gh#942):** `inline-size: 18rem` + `max-inline-size: 100%` replace the direction-fragile `flex-basis`, so a column-flex parent no longer inflates the component to 288px tall.
|
|
12
|
+
- **`command-ui` / `combobox-ui` — `options` catalog schema corrected to rich objects (`DynamicObjectList`).** Both yamls declare `{value, label, icon?, …}` objects but omitted `items.type`, mapping to the string-list default — the dialect validator rejected every harvested rich-options palette (caught by exit-gate.corpus at release pre-flight).
|
|
13
|
+
|
|
14
|
+
### Changed
|
|
15
|
+
|
|
16
|
+
- **`richtext` — inline `<code>` chip background softened to container-low (gh#972):** the opaque `--a-canvas-2` chip out-competed body text where several tag references cluster; the 10% alpha tier recedes behind prose and composites consistently on stacked surfaces. The docs site's own `admin-page code` rule moves to the same tier (superseding its earlier well-strong non-text-contrast ruling — inline code is text formatting, not a UI component).
|
|
17
|
+
- **`nav-item-ui` / `nav-group-ui` — state color spec (gh#968, operator ruling):** idle = neutral icons + neutral-high text on transparent; hover = on-surface on container-low; selected = on-surface on container. Selected glyphs drop the primary accent (supersedes gh#522's glyph half — the accent BAR keeps primary); `neutral/surfaceBright` was considered for the selected fill and ruled out.
|
|
18
|
+
- **`tag-ui` / `badge-ui` / `tabs-ui` — font scale steps one rung (gh#965):** tag/badge default `--a-ui-sm` → `--a-ui-xs` with tiers shifting to `--a-ui-2xs`/`--a-ui-sm` (first consumer of the pre-staged 10px token); tabs `--a-ui-md` → `--a-ui-lg`.
|
|
19
|
+
- **`description-list-ui` — `dt` renders as a kicker/data-label** (code family at `--a-label-size`, normal weight); **`dd` is semibold at context-inherited size** (`1em`) so the value leads the pair and tracks the surrounding prose.
|
|
20
|
+
- **Token docs — the `-strong` semantics split documented (gh#969):** `--a-canvas-strong` is a surface-tonal tier while interactive families' `-strong` is the pressed/high-emphasis foreground — noted at the core.css declarations and in component-token-contract.md. No value changes.
|
|
21
|
+
|
|
22
|
+
### Maintenance
|
|
23
|
+
- **`dist/` bundles rebuilt** in this cut's window (4 file(s)) — regenerated from the source changes described above, not independent edits.
|
|
24
|
+
- **`patterns/` touched in this release window** (1 file(s), e.g. `responsive-shell-sidebar/responsive-shell-sidebar.examples.html`) — carried by the entries above.
|
|
25
|
+
- **`styles/` touched in this release window** (1 file(s), e.g. `semantics/core.css`) — carried by the entries above.
|
|
26
|
+
|
|
3
27
|
## [0.8.33] — 2026-08-11
|
|
4
28
|
|
|
5
29
|
### Added
|
|
@@ -111,7 +111,7 @@
|
|
|
111
111
|
],
|
|
112
112
|
"slots": {
|
|
113
113
|
"default": {
|
|
114
|
-
"description": "Default slot —
|
|
114
|
+
"description": "Default slot — NOT a step-authoring surface: the component rebuilds its body from the addStep()/addThought() pipeline and does not consume authored children (gh#938)."
|
|
115
115
|
}
|
|
116
116
|
},
|
|
117
117
|
"states": [
|
|
@@ -57,7 +57,7 @@ events:
|
|
|
57
57
|
description: Whether the reasoning section is now collapsed.
|
|
58
58
|
slots:
|
|
59
59
|
default:
|
|
60
|
-
description: "Default slot —
|
|
60
|
+
description: "Default slot — NOT a step-authoring surface: the component rebuilds its body from the addStep()/addThought() pipeline and does not consume authored children (gh#938)."
|
|
61
61
|
states:
|
|
62
62
|
- name: idle
|
|
63
63
|
description: Default, ready for interaction.
|
|
@@ -72,8 +72,8 @@ a2ui:
|
|
|
72
72
|
rules:
|
|
73
73
|
- rule: 'Agent inner-monologue + pipeline viewer with steps, thoughts, plans, and iterations.'
|
|
74
74
|
reason: 'Reasoning visualization surface.'
|
|
75
|
-
- rule: '
|
|
76
|
-
reason: 'Composition contract.'
|
|
75
|
+
- rule: 'JS-property-driven ONLY — steps arrive via addStep()/addThought(); the component renders them internally as <timeline-item-ui> rows. Authored light-DOM children are NOT consumed (gh#938) — for a static/harvestable reasoning-steps surface, author a plain <timeline-ui> with <timeline-item-ui> children instead.'
|
|
76
|
+
reason: 'Composition contract — the previous wording ("pass reasoning steps as children") read as declarative-children support the class never had; a stateful pipeline (timers, spine layout, iterations) has no static form.'
|
|
77
77
|
- rule: 'noAutocollapse attribute prevents the auto-collapse-on-complete behavior; useful for debug surfaces.'
|
|
78
78
|
reason: 'Behavior knob.'
|
|
79
79
|
- rule: 'Different from <agent-trace-ui> (metrics + tool-call summary) — reasoning is narrative, trace is structured-data.'
|
|
@@ -22,7 +22,9 @@
|
|
|
22
22
|
--badge-px: var(--a-space-1-5);
|
|
23
23
|
--badge-py: var(--a-space-0-5);
|
|
24
24
|
--badge-gap: var(--a-space-1);
|
|
25
|
-
|
|
25
|
+
/* One rung down the --a-ui-* scale (gh#965) — ladder shifts with it
|
|
26
|
+
(sm=2xs, lg=sm) so tiers stay one rung apart. Mirrors tag-ui. */
|
|
27
|
+
--badge-font-size: var(--a-ui-xs);
|
|
26
28
|
/* ── Typographic register (issue 845; weight revised gh#866) — mono
|
|
27
29
|
TRIAL taken: kbd-ui's own stack (--a-font-family-code, kbd.css) held
|
|
28
30
|
up at small badge sizes (roughly a dozen logical pixels) in both
|
|
@@ -45,8 +47,8 @@
|
|
|
45
47
|
--badge-px-lg: var(--a-space-2);
|
|
46
48
|
--badge-py-sm: var(--a-space-px);
|
|
47
49
|
--badge-py-lg: var(--a-space-1);
|
|
48
|
-
--badge-font-size-sm: var(--a-ui-
|
|
49
|
-
--badge-font-size-lg: var(--a-ui-
|
|
50
|
+
--badge-font-size-sm: var(--a-ui-2xs);
|
|
51
|
+
--badge-font-size-lg: var(--a-ui-sm);
|
|
50
52
|
text-align: start; /* §text-align-reset — blocks inheritance from centered ancestors */
|
|
51
53
|
}
|
|
52
54
|
|
|
@@ -241,7 +243,7 @@
|
|
|
241
243
|
:scope[size="md"] {
|
|
242
244
|
--badge-px: var(--a-space-1-5);
|
|
243
245
|
--badge-py: var(--a-space-0-5);
|
|
244
|
-
--badge-font-size: var(--a-ui-
|
|
246
|
+
--badge-font-size: var(--a-ui-xs);
|
|
245
247
|
}
|
|
246
248
|
:scope[size="lg"] {
|
|
247
249
|
--badge-px: var(--badge-px-lg);
|
|
@@ -84,6 +84,15 @@ button-ui[variant="ghost"][color="warning"]:not([disabled]):hover { --button-f
|
|
|
84
84
|
--button-font-size: var(--a-ui-size);
|
|
85
85
|
--button-font-weight: var(--a-ui-weight);
|
|
86
86
|
--button-font-family: var(--a-font-family-ui);
|
|
87
|
+
/* Leading for the [text] label box ONLY — never the host. The host's
|
|
88
|
+
`line-height: 1` is deliberate (it sizes the icon/[slot="trailing"]
|
|
89
|
+
blocks), but the label's ::after clips to its own line box, so
|
|
90
|
+
inheriting 1 makes half-leading negative and shears descenders
|
|
91
|
+
(g/j/p/q/y, parentheses) by ~0.93px at the default size — gh#906.
|
|
92
|
+
Snug (1.3) contains the font's content box at every [size]; the
|
|
93
|
+
precedent is stat.css:174,198 setting leading on the truncating
|
|
94
|
+
box rather than on the component root. */
|
|
95
|
+
--button-label-line-height: var(--a-leading-snug);
|
|
87
96
|
--button-gap: var(--a-space-1);
|
|
88
97
|
--button-bg-hover: var(--a-bg-strong); /* neutral fill → neutral -strong surface on hover */
|
|
89
98
|
--button-fg-hover: var(--a-ui-text-hover);
|
|
@@ -167,6 +176,9 @@ button-ui[variant="ghost"][color="warning"]:not([disabled]):hover { --button-f
|
|
|
167
176
|
overflow: hidden;
|
|
168
177
|
text-overflow: ellipsis;
|
|
169
178
|
min-width: 0;
|
|
179
|
+
/* `overflow: hidden` (needed for the ellipsis) makes this line box the
|
|
180
|
+
clip box — give it leading tall enough to contain the descenders. */
|
|
181
|
+
line-height: var(--button-label-line-height);
|
|
170
182
|
}
|
|
171
183
|
|
|
172
184
|
/* Display convention (ADR-0037): block-level by default — a text button used
|
|
@@ -98,7 +98,7 @@
|
|
|
98
98
|
},
|
|
99
99
|
"options": {
|
|
100
100
|
"description": "Programmatic option list. Array of {value, label, disabled?, group?} or grouped {label, options:[…]}. Alternative to declarative <option> / <optgroup> children.",
|
|
101
|
-
"$ref": "common_types.json#/$defs/
|
|
101
|
+
"$ref": "common_types.json#/$defs/DynamicObjectList"
|
|
102
102
|
},
|
|
103
103
|
"placeholder": {
|
|
104
104
|
"description": "Placeholder text in the input when value is empty",
|
|
@@ -85,6 +85,9 @@ props:
|
|
|
85
85
|
or grouped {label, options:[…]}. Alternative to declarative
|
|
86
86
|
<option> / <optgroup> children.
|
|
87
87
|
type: array
|
|
88
|
+
# Rich option objects — same DynamicObjectList mapping fix as command-ui.
|
|
89
|
+
items:
|
|
90
|
+
type: object
|
|
88
91
|
default: []
|
|
89
92
|
dynamic: true # JS-set collection prop with a custom setter — deliberately not in static properties
|
|
90
93
|
open:
|
|
@@ -21,6 +21,10 @@
|
|
|
21
21
|
"type": "boolean",
|
|
22
22
|
"default": false
|
|
23
23
|
},
|
|
24
|
+
"options": {
|
|
25
|
+
"description": "Programmatic item list. Array of {value, label, icon?, shortcut?, disabled?} or grouped {label, options:[…]}. `icon` renders as a leading icon-ui, `shortcut` as a trailing keyboard-shortcut hint — the same fields as the data-icon/data-shortcut attributes on declarative <option> children. Alternative to declarative <option data-icon data-shortcut> / <optgroup label> children.",
|
|
26
|
+
"$ref": "common_types.json#/$defs/DynamicObjectList"
|
|
27
|
+
},
|
|
24
28
|
"placeholder": {
|
|
25
29
|
"description": "Placeholder text for the search input",
|
|
26
30
|
"type": "string",
|
|
@@ -85,10 +85,15 @@ export class UICommand extends UIElement {
|
|
|
85
85
|
render() {
|
|
86
86
|
// Stamp internal structure once
|
|
87
87
|
if (!this.#inputEl) {
|
|
88
|
-
// Parse
|
|
89
|
-
//
|
|
90
|
-
//
|
|
91
|
-
|
|
88
|
+
// Parse declarative <option>/<optgroup> children once, unless
|
|
89
|
+
// `.options` was already set programmatically before first render
|
|
90
|
+
// (mirrors combobox-ui's connected()-time guard) + capture
|
|
91
|
+
// author-provided slot="empty" content before replacing innerHTML
|
|
92
|
+
// (dogfood-status yaml-drift finding: yaml documented an overridable
|
|
93
|
+
// empty state that was never wired).
|
|
94
|
+
if (this.#items.length === 0) {
|
|
95
|
+
this.#parseOptions();
|
|
96
|
+
}
|
|
92
97
|
this.#emptyNodes = logicalSlotted(this, 'empty');
|
|
93
98
|
|
|
94
99
|
this.innerHTML = `
|
|
@@ -143,6 +148,51 @@ export class UICommand extends UIElement {
|
|
|
143
148
|
this.#renderItems();
|
|
144
149
|
}
|
|
145
150
|
|
|
151
|
+
// ── Programmatic options (gh#887) ──
|
|
152
|
+
//
|
|
153
|
+
// Public API mirroring combobox-ui's `options` prop: an array of flat
|
|
154
|
+
// {value, label, icon?, shortcut?, disabled?} entries, or grouped
|
|
155
|
+
// {label, options:[…]} entries — same field names as the data-icon/
|
|
156
|
+
// data-shortcut attributes on declarative <option> children. Normalized
|
|
157
|
+
// into the internal #items shape #renderItems() already consumes (flat
|
|
158
|
+
// items carry `category`; groups carry `.items` not `.options`), so
|
|
159
|
+
// programmatic and declarative authoring render identically.
|
|
160
|
+
|
|
161
|
+
set options(list) {
|
|
162
|
+
this.#items = this.#normalizeOptions(Array.isArray(list) ? list : []);
|
|
163
|
+
// Only re-render if the internal list has already been stamped —
|
|
164
|
+
// otherwise the pending #items feed the first render() pass directly.
|
|
165
|
+
if (this.#listEl) this.#renderItems(this.#inputEl?.value || '');
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
get options() { return this.#items; }
|
|
169
|
+
|
|
170
|
+
#normalizeOptions(list) {
|
|
171
|
+
const out = [];
|
|
172
|
+
for (const entry of list) {
|
|
173
|
+
if (entry && Array.isArray(entry.options)) {
|
|
174
|
+
const label = entry.label || '';
|
|
175
|
+
out.push({ label, items: entry.options.map((opt) => this.#optionObjectToItem(opt, label)) });
|
|
176
|
+
} else if (entry) {
|
|
177
|
+
out.push(this.#optionObjectToItem(entry, ''));
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
return out;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/** Plain-object counterpart of #optionToItem (which reads a DOM <option>). */
|
|
184
|
+
#optionObjectToItem(opt, category = '') {
|
|
185
|
+
return {
|
|
186
|
+
value: opt.value,
|
|
187
|
+
label: opt.label ?? opt.value,
|
|
188
|
+
icon: opt.icon || '',
|
|
189
|
+
shortcut: opt.shortcut || '',
|
|
190
|
+
keywords: opt.keywords || '',
|
|
191
|
+
disabled: !!opt.disabled,
|
|
192
|
+
category,
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
|
|
146
196
|
// ── Parse declarative <option>/<optgroup> ──
|
|
147
197
|
|
|
148
198
|
// Wrapper-piercing walk for consumer-authored <option>/<optgroup> children.
|
|
@@ -35,6 +35,8 @@ export type CommandSelectEvent = CustomEvent<CommandSelectEventDetail>;
|
|
|
35
35
|
export class UICommand extends UIElement {
|
|
36
36
|
/** Whether the command palette is visible */
|
|
37
37
|
open: boolean;
|
|
38
|
+
/** Programmatic item list. Array of {value, label, icon?, shortcut?, disabled?} or grouped {label, options:[…]}. `icon` renders as a leading icon-ui, `shortcut` as a trailing keyboard-shortcut hint — the same fields as the data-icon/data-shortcut attributes on declarative <option> children. Alternative to declarative <option data-icon data-shortcut> / <optgroup label> children. */
|
|
39
|
+
options: string;
|
|
38
40
|
/** Placeholder text for the search input */
|
|
39
41
|
placeholder: string;
|
|
40
42
|
|
|
@@ -24,6 +24,24 @@ props:
|
|
|
24
24
|
description: Placeholder text for the search input
|
|
25
25
|
type: string
|
|
26
26
|
default: Type a command...
|
|
27
|
+
options:
|
|
28
|
+
description: >-
|
|
29
|
+
Programmatic item list. Array of {value, label, icon?, shortcut?,
|
|
30
|
+
disabled?} or grouped {label, options:[…]}. `icon` renders as a
|
|
31
|
+
leading icon-ui, `shortcut` as a trailing keyboard-shortcut hint —
|
|
32
|
+
the same fields as the data-icon/data-shortcut attributes on
|
|
33
|
+
declarative <option> children. Alternative to declarative
|
|
34
|
+
<option data-icon data-shortcut> / <optgroup label> children.
|
|
35
|
+
type: array
|
|
36
|
+
# Rich option OBJECTS ({value, label, icon?, …} / grouped) — declares
|
|
37
|
+
# items.type so the a2ui catalog maps to DynamicObjectList, not the
|
|
38
|
+
# string-list default (components.mjs TKT-0010 mapping). Without this
|
|
39
|
+
# the dialect validator rejected every harvested rich-options palette
|
|
40
|
+
# (exit-gate.corpus caught it at release pre-flight).
|
|
41
|
+
items:
|
|
42
|
+
type: object
|
|
43
|
+
default: []
|
|
44
|
+
dynamic: true # JS-set collection prop with a custom setter — mirrors combobox-ui's `options`
|
|
27
45
|
events:
|
|
28
46
|
dismiss:
|
|
29
47
|
description: Fired when Escape is pressed
|
|
@@ -69,9 +87,13 @@ a2ui:
|
|
|
69
87
|
shortcut. Do NOT hand-roll a <dialog> + Cmd+K key listener.
|
|
70
88
|
- >-
|
|
71
89
|
Author items as native <option value data-icon data-shortcut>
|
|
72
|
-
elements inside <optgroup label="…"> for grouped sections
|
|
73
|
-
`
|
|
74
|
-
|
|
90
|
+
elements inside <optgroup label="…"> for grouped sections, OR set
|
|
91
|
+
`.options` programmatically as an array of
|
|
92
|
+
`{value, label, icon?, shortcut?, disabled?}` (grouped form:
|
|
93
|
+
`{label, options:[…]}`) — same fields, alternative to declarative
|
|
94
|
+
children. The `select` event's detail.category mirrors the parent
|
|
95
|
+
optgroup's label (or the group's `label` when authored via
|
|
96
|
+
`options`). Detail = { value, label, category }.
|
|
75
97
|
- >-
|
|
76
98
|
Decision rule vs adjacent surfaces. Use <menu-ui> for small
|
|
77
99
|
NON-searchable popover menus (≤10 actions, triggered by a
|
|
@@ -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/DateRangePicker.json",
|
|
4
4
|
"title": "DateRangePicker",
|
|
5
|
-
"description": "Compound form primitive for selecting a start + end date pair with optional named preset shortcuts (\"Today\", \"Last 7 days\", \"This month\", ...). Default presentation is a trigger button showing the formatted range; on activation a popover opens with two synchronized calendar panes + a preset side rail. Composes <calendar-
|
|
5
|
+
"description": "Compound form primitive for selecting a start + end date pair with optional named preset shortcuts (\"Today\", \"Last 7 days\", \"This month\", ...). Default presentation is a trigger button showing the formatted range; on activation a popover opens with two synchronized calendar panes + a preset side rail. Composes <calendar-grid-ui> twice; emits {from, to} ISO 8601 dates. Both panes are ONE selection surface: first click = start, second click = end, in either grid; a second click earlier than the start restarts the range there. Optional comparison-range mode emits a secondary range for analytics surfaces.",
|
|
6
6
|
"type": "object",
|
|
7
7
|
"allOf": [
|
|
8
8
|
{
|
|
@@ -15,9 +15,12 @@
|
|
|
15
15
|
* <date-range-picker-ui>
|
|
16
16
|
*
|
|
17
17
|
* Compound form primitive for selecting a start + end date pair with
|
|
18
|
-
* optional preset shortcuts. Composes <calendar-
|
|
19
|
-
*
|
|
20
|
-
* trigger and preset rail.
|
|
18
|
+
* optional preset shortcuts. Composes <calendar-grid-ui> (two panes
|
|
19
|
+
* showing adjacent months) + <popover-ui> + <button-ui> for the
|
|
20
|
+
* trigger and preset rail. Selection is click-start-then-click-end
|
|
21
|
+
* across BOTH panes as one surface (gh#948) — the panes differ only
|
|
22
|
+
* in which month they display, never in which half of the range a
|
|
23
|
+
* click writes.
|
|
21
24
|
*
|
|
22
25
|
* Form participation:
|
|
23
26
|
* ElementInternals serializes the selected range as JSON:
|
|
@@ -345,8 +348,8 @@ export class UIDateRangePicker extends UIFormElement {
|
|
|
345
348
|
this.#triggerRef.addEventListener('keydown', this.#onTriggerKey, true);
|
|
346
349
|
this.#popoverRef.addEventListener('click', this.#onPopoverClick);
|
|
347
350
|
this.#popoverRef.addEventListener('keydown', this.#onPopoverKey);
|
|
348
|
-
this.#calFromRef.addEventListener('change', this.#
|
|
349
|
-
this.#calToRef.addEventListener('change', this.#
|
|
351
|
+
this.#calFromRef.addEventListener('change', this.#onDaySelect);
|
|
352
|
+
this.#calToRef.addEventListener('change', this.#onDaySelect);
|
|
350
353
|
}
|
|
351
354
|
// Wire form value on first connect.
|
|
352
355
|
this.syncValue();
|
|
@@ -364,8 +367,8 @@ export class UIDateRangePicker extends UIFormElement {
|
|
|
364
367
|
this.#popoverRef.removeEventListener('keydown', this.#onPopoverKey);
|
|
365
368
|
this.#popoverRef.hidePopover?.();
|
|
366
369
|
}
|
|
367
|
-
if (this.#calFromRef) this.#calFromRef.removeEventListener('change', this.#
|
|
368
|
-
if (this.#calToRef) this.#calToRef.removeEventListener('change', this.#
|
|
370
|
+
if (this.#calFromRef) this.#calFromRef.removeEventListener('change', this.#onDaySelect);
|
|
371
|
+
if (this.#calToRef) this.#calToRef.removeEventListener('change', this.#onDaySelect);
|
|
369
372
|
document.removeEventListener('pointerdown', this.#onOutside);
|
|
370
373
|
document.removeEventListener('keydown', this.#onDocKey);
|
|
371
374
|
this.#anchorCleanup?.();
|
|
@@ -449,14 +452,16 @@ export class UIDateRangePicker extends UIFormElement {
|
|
|
449
452
|
// Cascade min/max + selection state into the calendar panes.
|
|
450
453
|
const range = parseRange(this.value);
|
|
451
454
|
const pending = this.#pending;
|
|
452
|
-
// Effective endpoints —
|
|
453
|
-
// value
|
|
454
|
-
//
|
|
455
|
-
//
|
|
456
|
-
//
|
|
457
|
-
//
|
|
458
|
-
|
|
459
|
-
|
|
455
|
+
// Effective endpoints — an in-flight pending selection REPLACES the
|
|
456
|
+
// committed value wholesale (no per-half fallback: mixing the new
|
|
457
|
+
// start with the old committed end would light a range the user
|
|
458
|
+
// never picked, gh#948). Push the SAME from/to onto BOTH grids so
|
|
459
|
+
// each independently lights up its in-range cells via
|
|
460
|
+
// `[data-in-range]`. Without this, only the two endpoint cells got
|
|
461
|
+
// `[data-selected]` and the days between them rendered as plain
|
|
462
|
+
// background — the "fill the cells between start and end dates" trap.
|
|
463
|
+
const effFrom = pending ? pending.from : (range?.from || '');
|
|
464
|
+
const effTo = pending ? pending.to : (range?.to || '');
|
|
460
465
|
const setRangeAttrs = (cal) => {
|
|
461
466
|
if (effFrom) cal.setAttribute('range-start', effFrom);
|
|
462
467
|
else cal.removeAttribute('range-start');
|
|
@@ -466,13 +471,13 @@ export class UIDateRangePicker extends UIFormElement {
|
|
|
466
471
|
if (this.#calFromRef) {
|
|
467
472
|
if (this.min) this.#calFromRef.setAttribute('min', this.min);
|
|
468
473
|
if (this.max) this.#calFromRef.setAttribute('max', this.max);
|
|
469
|
-
this.#calFromRef.value =
|
|
474
|
+
this.#calFromRef.value = effFrom;
|
|
470
475
|
setRangeAttrs(this.#calFromRef);
|
|
471
476
|
}
|
|
472
477
|
if (this.#calToRef) {
|
|
473
478
|
if (this.min) this.#calToRef.setAttribute('min', this.min);
|
|
474
479
|
if (this.max) this.#calToRef.setAttribute('max', this.max);
|
|
475
|
-
this.#calToRef.value =
|
|
480
|
+
this.#calToRef.value = effTo;
|
|
476
481
|
setRangeAttrs(this.#calToRef);
|
|
477
482
|
}
|
|
478
483
|
|
|
@@ -680,37 +685,31 @@ export class UIDateRangePicker extends UIFormElement {
|
|
|
680
685
|
}
|
|
681
686
|
};
|
|
682
687
|
|
|
683
|
-
|
|
688
|
+
// Unified day selection across BOTH grids — first click = start,
|
|
689
|
+
// second click = end, whichever pane the click lands in (gh#948).
|
|
690
|
+
// The panes' [data-cal-from]/[data-cal-to] roles are display-only
|
|
691
|
+
// (left month / next month); they no longer lock which half of the
|
|
692
|
+
// range a click writes. A second click EARLIER than the pending start
|
|
693
|
+
// restarts the selection at that day (the industry-standard dual-pane
|
|
694
|
+
// model) instead of emitting a reversed-range `invalid`. A click
|
|
695
|
+
// while a range is already committed starts a fresh selection.
|
|
696
|
+
#onDaySelect = (e) => {
|
|
684
697
|
if (this.readonly) return;
|
|
685
698
|
e.stopPropagation();
|
|
686
|
-
const iso = e.detail?.value ||
|
|
699
|
+
const iso = e.detail?.value || '';
|
|
687
700
|
if (!iso) return;
|
|
688
|
-
const
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
if (toCandidate) {
|
|
697
|
-
this.#commitRange({ from: iso, to: toCandidate });
|
|
701
|
+
const startFresh =
|
|
702
|
+
!this.#pending?.from || // nothing pending (incl. committed-value state)
|
|
703
|
+
this.#pending.to || // previous selection fully picked
|
|
704
|
+
iso < this.#pending.from; // earlier than pending start → restart there
|
|
705
|
+
if (startFresh) {
|
|
706
|
+
this.#pending = { from: iso, to: '' };
|
|
707
|
+
this.dispatchEvent(new CustomEvent('input', { bubbles: true, detail: { value: { from: iso, to: null } } }));
|
|
708
|
+
return;
|
|
698
709
|
}
|
|
699
|
-
};
|
|
700
|
-
|
|
701
|
-
#onCalToChange = (e) => {
|
|
702
|
-
if (this.readonly) return;
|
|
703
|
-
e.stopPropagation();
|
|
704
|
-
const iso = e.detail?.value || this.#calToRef?.value || '';
|
|
705
|
-
if (!iso) return;
|
|
706
|
-
const existing = parseRange(this.value);
|
|
707
|
-
if (!this.#pending) this.#pending = { from: '', to: '' };
|
|
708
710
|
this.#pending.to = iso;
|
|
709
|
-
this.dispatchEvent(new CustomEvent('input', { bubbles: true, detail: { value: { from: this.#pending.from
|
|
710
|
-
|
|
711
|
-
if (fromCandidate) {
|
|
712
|
-
this.#commitRange({ from: fromCandidate, to: iso });
|
|
713
|
-
}
|
|
711
|
+
this.dispatchEvent(new CustomEvent('input', { bubbles: true, detail: { value: { from: this.#pending.from, to: iso } } }));
|
|
712
|
+
this.#commitRange({ from: this.#pending.from, to: iso });
|
|
714
713
|
};
|
|
715
714
|
|
|
716
715
|
#onOutside = (e) => {
|
|
@@ -158,6 +158,17 @@ date-range-picker-ui [slot="popover"] {
|
|
|
158
158
|
/* Opt into a wider anchor.js max-width cap so both month panes fit side-by-side
|
|
159
159
|
(the 32rem default clipped the second month). Viewport safety still applies. */
|
|
160
160
|
--popover-max-width: var(--date-range-picker-popover-max-width);
|
|
161
|
+
/* Content sizing lives in the STYLESHEET, not only in anchor.js inline
|
|
162
|
+
styles (gh#948): only the native-anchor path applies width/max-width
|
|
163
|
+
hints inline — the JS fallback (Safari/Firefox) and any placement
|
|
164
|
+
where anchoring fails otherwise leave the popover's size to whatever
|
|
165
|
+
the surrounding cascade produces, which is how the viewport-huge
|
|
166
|
+
mostly-empty panel renders. These caps make the popover content-sized
|
|
167
|
+
in every path; anchor.js inline styles still win where they apply. */
|
|
168
|
+
width: max-content;
|
|
169
|
+
max-width: min(calc(100vw - 1rem), var(--date-range-picker-popover-max-width));
|
|
170
|
+
max-height: calc(100vh - 2rem);
|
|
171
|
+
overflow: auto;
|
|
161
172
|
padding: var(--date-range-picker-py) var(--date-range-picker-px);
|
|
162
173
|
border: 1px solid var(--date-range-picker-popover-border);
|
|
163
174
|
border-radius: var(--date-range-picker-popover-radius);
|
|
@@ -10,9 +10,12 @@ description: >-
|
|
|
10
10
|
optional named preset shortcuts ("Today", "Last 7 days", "This month",
|
|
11
11
|
...). Default presentation is a trigger button showing the formatted
|
|
12
12
|
range; on activation a popover opens with two synchronized calendar
|
|
13
|
-
panes + a preset side rail. Composes <calendar-
|
|
14
|
-
emits {from, to} ISO 8601 dates.
|
|
15
|
-
|
|
13
|
+
panes + a preset side rail. Composes <calendar-grid-ui> twice;
|
|
14
|
+
emits {from, to} ISO 8601 dates. Both panes are ONE selection
|
|
15
|
+
surface: first click = start, second click = end, in either grid;
|
|
16
|
+
a second click earlier than the start restarts the range there.
|
|
17
|
+
Optional comparison-range mode emits a secondary range for
|
|
18
|
+
analytics surfaces.
|
|
16
19
|
# Per ADR-0027 — primitives that programmatically create other primitives
|
|
17
20
|
# do NOT auto-import them. Consumer (or demo shell) must explicitly import.
|
|
18
21
|
composes:
|
|
@@ -6,10 +6,21 @@
|
|
|
6
6
|
--description-list-term-min-width: 8rem;
|
|
7
7
|
|
|
8
8
|
/* ── Typography ── */
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
9
|
+
/* Term reads as a kicker/data-label (operator ruling 2026-08-11,
|
|
10
|
+
matching the docs site's [data-label] convention): code family at
|
|
11
|
+
label size, normal weight — a quiet technical eyebrow the medium-
|
|
12
|
+
weight desc answers. */
|
|
13
|
+
--description-list-term-font-family: var(--a-font-family-code);
|
|
14
|
+
--description-list-term-font-size: var(--a-label-size);
|
|
15
|
+
--description-list-term-weight: var(--a-weight-normal);
|
|
16
|
+
/* 1em, not a fixed step — the desc inherits the surrounding body/
|
|
17
|
+
context size (operator ruling 2026-08-11), so a dl inside prose
|
|
18
|
+
matches the prose and inside a compact card matches the card. */
|
|
19
|
+
--description-list-desc-font-size: 1em;
|
|
20
|
+
/* Semibold — the desc (value) carries the pair's content and reads
|
|
21
|
+
clearly bolder than the muted normal-weight kicker term (operator
|
|
22
|
+
rulings 2026-08-11: first normal → medium, then medium → semibold). */
|
|
23
|
+
--description-list-desc-weight: var(--a-weight-semibold);
|
|
13
24
|
|
|
14
25
|
/* ── Colors ── */
|
|
15
26
|
--description-list-term-fg: var(--a-fg-muted);
|
|
@@ -29,6 +40,7 @@
|
|
|
29
40
|
[data-dl-term],
|
|
30
41
|
dt {
|
|
31
42
|
margin: 0;
|
|
43
|
+
font-family: var(--description-list-term-font-family);
|
|
32
44
|
font-size: var(--description-list-term-font-size);
|
|
33
45
|
font-weight: var(--description-list-term-weight);
|
|
34
46
|
color: var(--description-list-term-fg);
|
|
@@ -51,6 +51,12 @@ export class UIModal extends UIElement {
|
|
|
51
51
|
#closeTimer = null;
|
|
52
52
|
#dialogRef = null;
|
|
53
53
|
|
|
54
|
+
// Monotonic, instance-agnostic counter for the fallback aria-labelledby
|
|
55
|
+
// id stamped onto an author's [slot="heading"] (see render()) — multiple
|
|
56
|
+
// <modal-ui> instances on one page must not collide on the same id.
|
|
57
|
+
// (Extracted from PR #944 — gh#947.)
|
|
58
|
+
static #headingIdSeq = 0;
|
|
59
|
+
|
|
54
60
|
// Phosphor icons this primitive auto-stamps (without consumer markup).
|
|
55
61
|
// Aggregated by installIconLoadersForRegistered() across all defined
|
|
56
62
|
// elements. Audited by check-required-icons.mjs.
|
|
@@ -150,8 +156,25 @@ export class UIModal extends UIElement {
|
|
|
150
156
|
const panel = this.ensure('panel');
|
|
151
157
|
if (panel.parentElement !== dialog) dialog.appendChild(panel);
|
|
152
158
|
|
|
153
|
-
|
|
154
|
-
|
|
159
|
+
// Header — accept an authored bare <header> or [slot="header"] child
|
|
160
|
+
// (card-ui style), mirroring the footer contract below. Without this
|
|
161
|
+
// branch an authored <header> fell through to the loose-children
|
|
162
|
+
// migration and rendered inside the BODY, displacing icon/heading to
|
|
163
|
+
// the bottom of the modal (gh#941).
|
|
164
|
+
const userHeader = [...this.children].find(c =>
|
|
165
|
+
c.getAttribute('slot') === 'header' || (!c.getAttribute('slot') && c.localName === 'header')
|
|
166
|
+
);
|
|
167
|
+
let header;
|
|
168
|
+
if (userHeader) {
|
|
169
|
+
if (userHeader.getAttribute('slot') !== 'header') userHeader.setAttribute('slot', 'header');
|
|
170
|
+
if (userHeader.parentElement !== panel) panel.prepend(userHeader);
|
|
171
|
+
panel.querySelector(':scope > [slot="header"][data-stamped]')?.remove();
|
|
172
|
+
header = userHeader;
|
|
173
|
+
} else {
|
|
174
|
+
header = this.ensure('header');
|
|
175
|
+
header.setAttribute('data-stamped', '');
|
|
176
|
+
if (header.parentElement !== panel) panel.prepend(header);
|
|
177
|
+
}
|
|
155
178
|
// Only set `text=` when actually present — the CSS rule
|
|
156
179
|
// `:scope [slot="header"][text]:not([text=""])::before { content: attr(text); flex: 1 }`
|
|
157
180
|
// generates a flex:1 pseudo-element. Setting `text=""` unconditionally
|
|
@@ -165,6 +188,24 @@ export class UIModal extends UIElement {
|
|
|
165
188
|
header.removeAttribute('text');
|
|
166
189
|
}
|
|
167
190
|
|
|
191
|
+
// Accessible name — [text] stamps aria-label (host + dialog), the
|
|
192
|
+
// long-standing contract above. Absent that, fall back to the author's
|
|
193
|
+
// [slot="heading"] via aria-labelledby so a heading-only modal (e.g.
|
|
194
|
+
// destructive-confirm-modal, gh#941) still satisfies the WAI-ARIA APG
|
|
195
|
+
// dialog-modal pattern's accessible-name requirement. Re-evaluated
|
|
196
|
+
// every render so a later `text=` set, or the heading being removed,
|
|
197
|
+
// clears the reference instead of leaving it stale. (gh#947, from
|
|
198
|
+
// PR #944.)
|
|
199
|
+
const authoredHeading = header.querySelector(':scope > [slot="heading"]');
|
|
200
|
+
if (!this.text && authoredHeading) {
|
|
201
|
+
if (!authoredHeading.id) authoredHeading.id = `modal-heading-${++UIModal.#headingIdSeq}`;
|
|
202
|
+
if (dialog.getAttribute('aria-labelledby') !== authoredHeading.id) {
|
|
203
|
+
dialog.setAttribute('aria-labelledby', authoredHeading.id);
|
|
204
|
+
}
|
|
205
|
+
} else {
|
|
206
|
+
dialog.removeAttribute('aria-labelledby');
|
|
207
|
+
}
|
|
208
|
+
|
|
168
209
|
if (!this.permanent) {
|
|
169
210
|
const close = this.ensure('close');
|
|
170
211
|
if (close.parentElement !== header) header.appendChild(close);
|
|
@@ -131,6 +131,29 @@
|
|
|
131
131
|
flex: 1;
|
|
132
132
|
}
|
|
133
133
|
|
|
134
|
+
/* ═══════ Header icon + heading pair (drawer-ui/card-ui parity) ═══════
|
|
135
|
+
Author-supplied icon/heading content nested inside <header> — an
|
|
136
|
+
alternative to [text]. Positioned by tag + DOM order, not by reading
|
|
137
|
+
[slot="icon"]/[slot="heading"] (AGENTS.md: "slot= attributes are
|
|
138
|
+
decorative metadata; positioning is CSS by tag + ancestor + DOM
|
|
139
|
+
order") — `button-ui` identifies the stamped auto-close affordance by
|
|
140
|
+
TAG, and render() guarantees it's the header's trailing child when
|
|
141
|
+
present. The header's last non-button child claims the row's free
|
|
142
|
+
space (heading position); any earlier content child (a leading
|
|
143
|
+
icon/avatar) stays fixed-width. (gh#947, from PR #944.) */
|
|
144
|
+
:scope [slot="header"] > :not(button-ui):has(~ :not(button-ui)) {
|
|
145
|
+
display: flex;
|
|
146
|
+
align-items: center;
|
|
147
|
+
flex-shrink: 0;
|
|
148
|
+
}
|
|
149
|
+
:scope [slot="header"] > :not(button-ui):not(:has(~ :not(button-ui))) {
|
|
150
|
+
display: flex;
|
|
151
|
+
align-items: center;
|
|
152
|
+
gap: var(--a-space-2);
|
|
153
|
+
flex: 1;
|
|
154
|
+
min-width: 0;
|
|
155
|
+
}
|
|
156
|
+
|
|
134
157
|
/* ═══════ Close button (button-ui) ═══════ */
|
|
135
158
|
:scope [slot="close"] {
|
|
136
159
|
flex-shrink: 0;
|