@adia-ai/web-components 0.8.43 → 0.8.45
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 +54 -0
- package/components/avatar/avatar.class.js +12 -1
- package/components/button/button.d.ts +4 -0
- package/components/chart/chart.a2ui.json +26 -0
- package/components/chart/chart.class.js +1304 -222
- package/components/chart/chart.css +151 -1
- package/components/chart/chart.d.ts +6 -0
- package/components/chart/chart.yaml +57 -0
- package/components/chart-legend/chart-legend.a2ui.json +17 -2
- package/components/chart-legend/chart-legend.class.js +69 -4
- package/components/chart-legend/chart-legend.css +65 -1
- package/components/chart-legend/chart-legend.d.ts +4 -2
- package/components/chart-legend/chart-legend.examples.md +4 -4
- package/components/chart-legend/chart-legend.yaml +43 -3
- package/components/combobox/combobox.a2ui.json +3 -0
- package/components/combobox/combobox.class.js +9 -8
- package/components/combobox/combobox.css +25 -4
- package/components/combobox/combobox.yaml +6 -0
- package/components/command/command.css +17 -1
- package/components/context-menu/context-menu.a2ui.json +8 -3
- package/components/context-menu/context-menu.class.js +46 -5
- package/components/context-menu/context-menu.d.ts +8 -3
- package/components/context-menu/context-menu.examples.md +2 -2
- package/components/context-menu/context-menu.yaml +22 -5
- package/components/heatmap/heatmap.a2ui.json +5 -0
- package/components/heatmap/heatmap.d.ts +2 -0
- package/components/heatmap/heatmap.yaml +8 -0
- package/components/input/input.a2ui.json +2 -2
- package/components/input/input.class.js +12 -7
- package/components/input/input.yaml +6 -5
- package/components/integration-card/integration-card.d.ts +2 -0
- package/components/nav/nav.a2ui.json +2 -2
- package/components/nav/nav.css +1 -1
- package/components/nav/nav.d.ts +1 -1
- package/components/nav/nav.yaml +14 -3
- package/components/nav-group/nav-group.css +37 -3
- package/components/noodles/noodles.a2ui.json +0 -15
- package/components/noodles/noodles.yaml +0 -12
- package/components/number-format/number-format.class.js +10 -0
- package/components/pagination/pagination.class.js +140 -26
- package/components/popover/popover.d.ts +2 -0
- package/components/progress/progress.class.js +11 -6
- package/components/range/range.class.js +9 -1
- package/components/search/search.class.js +39 -5
- package/components/select/select.a2ui.json +10 -1
- package/components/select/select.class.js +34 -14
- package/components/select/select.css +39 -1
- package/components/select/select.d.ts +2 -0
- package/components/select/select.yaml +19 -1
- package/components/stream/stream.d.ts +2 -0
- package/components/table/cell-types.js +9 -0
- package/components/table/table.a2ui.json +6 -1
- package/components/table/table.class.js +265 -34
- package/components/table/table.css +38 -10
- package/components/table/table.d.ts +3 -1
- package/components/table/table.examples.md +2 -2
- package/components/table/table.yaml +15 -2
- package/components/table-toolbar/table-toolbar.a2ui.json +43 -4
- package/components/table-toolbar/table-toolbar.class.js +407 -28
- package/components/table-toolbar/table-toolbar.css +299 -0
- package/components/table-toolbar/table-toolbar.d.ts +15 -1
- package/components/table-toolbar/table-toolbar.yaml +162 -15
- package/components/tag/tag.class.js +13 -1
- package/components/text/text.a2ui.json +3 -3
- package/components/text/text.d.ts +5 -3
- package/components/text/text.yaml +3 -3
- package/components/textarea/textarea.class.js +9 -1
- package/components/tour/tour.d.ts +10 -0
- package/core/a11y.d.ts +22 -0
- package/core/a11y.js +64 -0
- package/core/anchor.js +9 -5
- package/core/data-stream.js +37 -2
- package/core/element.js +1 -1
- package/core/index.d.ts +1 -0
- package/core/index.js +1 -0
- package/core/provider.d.ts +9 -13
- package/core/provider.js +9 -113
- package/core/store.d.ts +46 -0
- package/core/store.js +89 -0
- package/custom-elements.json +127 -33
- package/dist/host.min.css +1 -1
- package/dist/host.sheet.js +1 -1
- package/dist/theme-provider.min.js +1 -1
- package/dist/web-components.min.css +1 -1
- package/dist/web-components.min.js +119 -119
- package/dist/web-components.sheet.js +1 -1
- package/package.json +1 -1
- package/patterns/access-requests/access-requests.examples.html +1 -1
- package/patterns/admin-keys-and-export/admin-keys-and-export.examples.html +7 -7
- package/patterns/agent-cost/agent-cost.examples.html +6 -6
- package/patterns/agent-memory/agent-memory.examples.html +3 -3
- package/patterns/agent-prompt-library/agent-prompt-library.examples.html +8 -8
- package/patterns/agent-tool-call/agent-tool-call.examples.html +1 -1
- package/patterns/approvals/approvals.examples.html +6 -6
- package/patterns/audit-log/audit-log.examples.html +1 -1
- package/patterns/bulk-action-toolbar/bulk-action-toolbar.examples.html +4 -4
- package/patterns/chart-in-card/chart-in-card.examples.html +114 -0
- package/patterns/chart-in-card/chart-in-card.examples.js +37 -0
- package/patterns/comments-and-collaboration/comments-and-collaboration.examples.html +5 -5
- package/patterns/data-tables-inline-edit-and-tree/data-tables-inline-edit-and-tree.examples.html +2 -2
- package/patterns/diff-review/diff-review.examples.html +6 -6
- package/patterns/export-flow/export-flow.examples.html +2 -2
- package/patterns/filter-bar/filter-bar.examples.html +2 -2
- package/patterns/forms-conditional-and-autocomplete/forms-conditional-and-autocomplete.examples.html +2 -2
- package/patterns/inline-dialog/inline-dialog.examples.html +2 -2
- package/patterns/marketing-engagement/marketing-engagement.examples.html +4 -4
- package/patterns/notifications-bell-and-digest/notifications-bell-and-digest.examples.html +1 -1
- package/patterns/permissions-matrix/permissions-matrix.examples.html +1 -1
- package/patterns/permissions-sharing/permissions-sharing.examples.html +2 -2
- package/patterns/profile-public-and-verification/profile-public-and-verification.examples.html +4 -4
- package/patterns/search-discovery/search-discovery.examples.html +2 -2
- package/styles/api/sizing.css +46 -0
- package/styles/type/roles.css +29 -9
|
@@ -96,10 +96,10 @@
|
|
|
96
96
|
"deck": "Sub-title under a `title`. One-line lead, slightly larger than body. Use for the lead sentence after a title.",
|
|
97
97
|
"display": "Top-level hero / brand display. Tallest visual rank. Use for page-level hero one-liners.",
|
|
98
98
|
"heading": "Major page heading (visual rank H2). 16-18px / bold. Use for major sub-section dividers.",
|
|
99
|
-
"kicker": "Eyebrow text above a `title`. UPPERCASE + small + tracking. Use for content eyebrows (NOT form labels — use `label` for those).",
|
|
100
|
-
"label": "Form-control label (above an `<input-ui>` / `<select-ui>` etc). UI-sized
|
|
99
|
+
"kicker": "Eyebrow text above a `title`. UPPERCASE + small + tracking, mono font stack at regular (400) weight. Use for content eyebrows (NOT form labels — use `label` for those).",
|
|
100
|
+
"label": "Form-control label (above an `<input-ui>` / `<select-ui>` etc). UI-sized, mono font stack at regular (400) weight. Use for field labels bound to form controls.",
|
|
101
101
|
"metric": "Numeric KPI / big-number stat. Bold + large. Use for dashboard metric numbers.",
|
|
102
|
-
"section": "Inline form-group / navlist heading (visual rank H4).
|
|
102
|
+
"section": "Inline form-group / navlist heading (visual rank H4). Mono font stack at medium (500) weight, normal casing. Use for form group labels, nav list headings.",
|
|
103
103
|
"subsection": "Sub-landmark within a section (visual rank H3). 14px / semibold. Use for card titles within a section."
|
|
104
104
|
}
|
|
105
105
|
},
|
|
@@ -25,13 +25,13 @@ export type UITextVariant =
|
|
|
25
25
|
| 'display'
|
|
26
26
|
/** Annotation under a primary line — smaller + muted. Use for image captions, footnotes. */
|
|
27
27
|
| 'caption'
|
|
28
|
-
/** Form-control label (above an `<input-ui>` / `<select-ui>` etc). UI-sized
|
|
28
|
+
/** Form-control label (above an `<input-ui>` / `<select-ui>` etc). UI-sized, mono font stack at regular (400) weight. Use for field labels bound to form controls. */
|
|
29
29
|
| 'label'
|
|
30
|
-
/** Eyebrow text above a `title`. UPPERCASE + small + tracking. Use for content eyebrows (NOT form labels — use `label` for those). */
|
|
30
|
+
/** Eyebrow text above a `title`. UPPERCASE + small + tracking, mono font stack at regular (400) weight. Use for content eyebrows (NOT form labels — use `label` for those). */
|
|
31
31
|
| 'kicker'
|
|
32
32
|
/** Sub-title under a `title`. One-line lead, slightly larger than body. Use for the lead sentence after a title. */
|
|
33
33
|
| 'deck'
|
|
34
|
-
/** Inline form-group / navlist heading (visual rank H4).
|
|
34
|
+
/** Inline form-group / navlist heading (visual rank H4). Mono font stack at medium (500) weight, normal casing. Use for form group labels, nav list headings. */
|
|
35
35
|
| 'section'
|
|
36
36
|
/** Numeric KPI / big-number stat. Bold + large. Use for dashboard metric numbers. */
|
|
37
37
|
| 'metric'
|
|
@@ -47,6 +47,8 @@ export class UIText extends UIElement {
|
|
|
47
47
|
size: 'sm' | 'md' | 'lg';
|
|
48
48
|
/** When true, applies stronger emphasis (heavier weight + accent color). Styled via :scope[strong] in text.css. Use instead of variant=heading when you want a single emphasized word inline in body copy. */
|
|
49
49
|
strong: boolean;
|
|
50
|
+
/** Override text alignment. Note: text-ui defaults to display:inline, so this only takes effect when text-ui is block-like (wrapping or parent display:block/grid). Added v0.6.18 (FB-10). Converged onto the global Enum·semantic `[text-align]` physical vocabulary (gh#1335) — `start`/`end` renamed to `left`/`right`; `justify` kept (a genuine physical alignment value, now also supported by the global grammar). */
|
|
51
|
+
textAlign: 'left' | 'center' | 'right' | 'justify';
|
|
50
52
|
/** Display text content. The main payload field for Text components extracted from HTML. */
|
|
51
53
|
textContent: string;
|
|
52
54
|
/** Single-line truncation with ellipsis. Ignored when `lines` is set. */
|
|
@@ -103,10 +103,10 @@ props:
|
|
|
103
103
|
title: Page title (visual rank H1). Largest under display. Use at the top of an authoritative page or dialog.
|
|
104
104
|
heading: Major page heading (visual rank H2). 16-18px / bold. Use for major sub-section dividers.
|
|
105
105
|
subsection: Sub-landmark within a section (visual rank H3). 14px / semibold. Use for card titles within a section.
|
|
106
|
-
section: Inline form-group / navlist heading (visual rank H4).
|
|
106
|
+
section: Inline form-group / navlist heading (visual rank H4). Mono font stack at medium (500) weight, normal casing. Use for form group labels, nav list headings.
|
|
107
107
|
caption: Annotation under a primary line — smaller + muted. Use for image captions, footnotes.
|
|
108
|
-
label: Form-control label (above an `<input-ui>` / `<select-ui>` etc). UI-sized
|
|
109
|
-
kicker: Eyebrow text above a `title`. UPPERCASE + small + tracking. Use for content eyebrows (NOT form labels — use `label` for those).
|
|
108
|
+
label: Form-control label (above an `<input-ui>` / `<select-ui>` etc). UI-sized, mono font stack at regular (400) weight. Use for field labels bound to form controls.
|
|
109
|
+
kicker: Eyebrow text above a `title`. UPPERCASE + small + tracking, mono font stack at regular (400) weight. Use for content eyebrows (NOT form labels — use `label` for those).
|
|
110
110
|
deck: Sub-title under a `title`. One-line lead, slightly larger than body. Use for the lead sentence after a title.
|
|
111
111
|
metric: Numeric KPI / big-number stat. Bold + large. Use for dashboard metric numbers.
|
|
112
112
|
code: Inline monospace code reference. Use for inline code within prose.
|
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
*/
|
|
19
19
|
|
|
20
20
|
import { UIFormElement } from '../../core/form.js';
|
|
21
|
+
import { syncAutoAriaLabel } from '../../core/a11y.js';
|
|
21
22
|
|
|
22
23
|
export class UITextarea extends UIFormElement {
|
|
23
24
|
static labelDeprecated = false; // §170 (v0.5.4): label is first-class per textarea.yaml
|
|
@@ -32,6 +33,10 @@ export class UITextarea extends UIFormElement {
|
|
|
32
33
|
static template = () => null;
|
|
33
34
|
|
|
34
35
|
#textEl = null;
|
|
36
|
+
// gh#1647: tracks the aria-label VALUE this element itself last derived
|
|
37
|
+
// from `label`/`placeholder` (undefined when none) — value-tracking
|
|
38
|
+
// guard ported from tag.class.js/select.class.js (gh#1644/#1646).
|
|
39
|
+
#lastAutoAriaLabel = undefined;
|
|
35
40
|
|
|
36
41
|
connected() {
|
|
37
42
|
super.connected();
|
|
@@ -82,7 +87,10 @@ export class UITextarea extends UIFormElement {
|
|
|
82
87
|
const label = this.querySelector('[slot="label"]');
|
|
83
88
|
if (label && this.label) label.setAttribute('label', this.label);
|
|
84
89
|
|
|
85
|
-
|
|
90
|
+
// A11y (gh#1647): never clobber a consumer's own aria-label
|
|
91
|
+
// (value-tracking guard, gh#1644/#1646).
|
|
92
|
+
const ariaSource = this.label || this.placeholder || '';
|
|
93
|
+
this.#lastAutoAriaLabel = syncAutoAriaLabel(this, this.#lastAutoAriaLabel, ariaSource);
|
|
86
94
|
}
|
|
87
95
|
|
|
88
96
|
#onInput = () => {
|
|
@@ -43,10 +43,20 @@ export class UITour extends UIElement {
|
|
|
43
43
|
the spotlight + popover; setting to `false` tears down.
|
|
44
44
|
*/
|
|
45
45
|
active: boolean;
|
|
46
|
+
/** Start the tour automatically when the element connects. Useful for
|
|
47
|
+
first-run flows gated by a storage flag on the consumer side.
|
|
48
|
+
*/
|
|
49
|
+
autoStart: boolean;
|
|
46
50
|
/** Current step index (0-based). Setting this property advances the
|
|
47
51
|
tour to that step (updating spotlight + popover position).
|
|
48
52
|
*/
|
|
49
53
|
step: number;
|
|
54
|
+
/** Optional localStorage key. When set, the tour records its
|
|
55
|
+
completion state (`"done"`) to that key on finish/skip — and
|
|
56
|
+
refuses to auto-start on subsequent loads if the key is "done".
|
|
57
|
+
Useful for "show this tour once per user" flows.
|
|
58
|
+
*/
|
|
59
|
+
storageKey: string;
|
|
50
60
|
|
|
51
61
|
addEventListener(type: 'tour-finish', listener: (ev: TourFinishEvent) => unknown, options?: boolean | AddEventListenerOptions): void;
|
|
52
62
|
addEventListener(type: 'tour-skip', listener: (ev: TourSkipEvent) => unknown, options?: boolean | AddEventListenerOptions): void;
|
package/core/a11y.d.ts
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* core/a11y.js — shared accessible-name authorship guard.
|
|
3
|
+
*
|
|
4
|
+
* See the runtime module's own JSDoc for the full contract and the usage
|
|
5
|
+
* pattern (gh#1644 / gh#1646 / gh#1647).
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Reconcile an auto-derived `aria-label` against a possible consumer
|
|
10
|
+
* override, using a value-tracking guard rather than a plain boolean flag.
|
|
11
|
+
*
|
|
12
|
+
* @param el - the host element.
|
|
13
|
+
* @param lastAuto - the value this component wrote last time (`undefined`
|
|
14
|
+
* = no active auto-derived value, either never set or consumer-owned).
|
|
15
|
+
* @param source - the label this render would derive; falsy = no auto name.
|
|
16
|
+
* @returns the new `lastAuto` to store back in the caller's own field.
|
|
17
|
+
*/
|
|
18
|
+
export function syncAutoAriaLabel(
|
|
19
|
+
el: HTMLElement,
|
|
20
|
+
lastAuto: string | undefined,
|
|
21
|
+
source: string,
|
|
22
|
+
): string | undefined;
|
package/core/a11y.js
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* core/a11y.js — shared accessible-name authorship guard.
|
|
3
|
+
*
|
|
4
|
+
* Ports the value-tracking guard proven on `tag.class.js`'s
|
|
5
|
+
* `#syncAccessibleName` (gh#1644 / PR #1645) and `select.class.js`'s
|
|
6
|
+
* `labeledElsewhere` variant (gh#1646 / PR #1648) into one shared helper,
|
|
7
|
+
* so every primitive that derives its own `aria-label` doesn't need to
|
|
8
|
+
* reimplement the compare-against-what-we-last-wrote logic (gh#1647).
|
|
9
|
+
*
|
|
10
|
+
* A plain boolean "did we ever set it" flag isn't enough: a consumer can
|
|
11
|
+
* overwrite `aria-label` directly via `setAttribute` at any point BETWEEN
|
|
12
|
+
* renders, bypassing the flag entirely. This guard instead compares the
|
|
13
|
+
* CURRENT attribute value against the value THIS component last wrote —
|
|
14
|
+
* a mismatch means a consumer has taken ownership, and auto-derivation
|
|
15
|
+
* stops until the source value the caller passes in changes again.
|
|
16
|
+
*
|
|
17
|
+
* Per-component gating (e.g. select-ui's `labeledElsewhere` — don't
|
|
18
|
+
* auto-name when a visible `label` or `aria-labelledby` already names the
|
|
19
|
+
* element, or don't auto-name off a default/placeholder-only value) is
|
|
20
|
+
* the CALLER's job: resolve the source string to '' when no auto name
|
|
21
|
+
* should apply this render, and pass the real derived string otherwise.
|
|
22
|
+
* This helper only ever reconciles "what we'd write" against "what's
|
|
23
|
+
* there now, and did we write it" — it carries no per-component policy.
|
|
24
|
+
*
|
|
25
|
+
* Usage (per component):
|
|
26
|
+
*
|
|
27
|
+
* import { syncAutoAriaLabel } from '../../core/a11y.js';
|
|
28
|
+
*
|
|
29
|
+
* #lastAutoAriaLabel = undefined;
|
|
30
|
+
*
|
|
31
|
+
* #syncAccessibleName() {
|
|
32
|
+
* const source = <derive the label this render, or '' for none>;
|
|
33
|
+
* this.#lastAutoAriaLabel = syncAutoAriaLabel(this, this.#lastAutoAriaLabel, source);
|
|
34
|
+
* }
|
|
35
|
+
*
|
|
36
|
+
* Call `#syncAccessibleName()` early in `render()`, before any other
|
|
37
|
+
* aria-* bookkeeping that might read the (possibly just-changed)
|
|
38
|
+
* `aria-label` attribute.
|
|
39
|
+
*
|
|
40
|
+
* @param {HTMLElement} el
|
|
41
|
+
* @param {string | undefined} lastAuto - the value this component wrote
|
|
42
|
+
* last time (`undefined` = the component has never derived a name for
|
|
43
|
+
* the CURRENT attribute value, either because it never has, or because
|
|
44
|
+
* a consumer has since taken ownership).
|
|
45
|
+
* @param {string} source - the label the component would derive right
|
|
46
|
+
* now. Empty string (or any other falsy value) means "no auto name" —
|
|
47
|
+
* any auto-set label is removed.
|
|
48
|
+
* @returns {string | undefined} the new `lastAuto` value — store this
|
|
49
|
+
* back into the caller's own private field.
|
|
50
|
+
*/
|
|
51
|
+
export function syncAutoAriaLabel(el, lastAuto, source) {
|
|
52
|
+
const current = el.getAttribute('aria-label');
|
|
53
|
+
if (lastAuto !== undefined && current !== lastAuto) {
|
|
54
|
+
lastAuto = undefined; // a consumer took ownership between renders
|
|
55
|
+
}
|
|
56
|
+
const authored = current !== null && lastAuto === undefined;
|
|
57
|
+
if (authored) return lastAuto;
|
|
58
|
+
if (source) {
|
|
59
|
+
if (current !== source) el.setAttribute('aria-label', source);
|
|
60
|
+
return source;
|
|
61
|
+
}
|
|
62
|
+
if (current !== null) el.removeAttribute('aria-label');
|
|
63
|
+
return undefined;
|
|
64
|
+
}
|
package/core/anchor.js
CHANGED
|
@@ -127,11 +127,15 @@ function anchorNative(anchor, popover, { placement, gap, matchWidth }) {
|
|
|
127
127
|
};
|
|
128
128
|
}
|
|
129
129
|
|
|
130
|
-
// Resolve a
|
|
131
|
-
// declare px
|
|
132
|
-
//
|
|
133
|
-
//
|
|
134
|
-
|
|
130
|
+
// Resolve a px/rem CSS custom-property declaration to px. Originally scoped
|
|
131
|
+
// to --popover-max-height (consumers declare px, JS-measured caps, or rem,
|
|
132
|
+
// stylesheet defaults); exported (ADR-0076, table-toolbar-ui's stage-
|
|
133
|
+
// resolution breakpoints, PLAN step 7) since a second, unrelated consumer
|
|
134
|
+
// needs the identical px/rem-only resolution — anything else (calc, var
|
|
135
|
+
// chains, empty) returns NaN and the caller falls back to its own default.
|
|
136
|
+
// Kept deliberately narrow — this feeds a classification DECISION, not
|
|
137
|
+
// layout, in both consumers.
|
|
138
|
+
export function resolveLengthPx(raw) {
|
|
135
139
|
const v = String(raw || '').trim();
|
|
136
140
|
if (!v) return NaN;
|
|
137
141
|
const m = /^(-?\d+(?:\.\d+)?)(px|rem)$/.exec(v);
|
package/core/data-stream.js
CHANGED
|
@@ -37,6 +37,14 @@
|
|
|
37
37
|
* stream-load — first signal value received for this element
|
|
38
38
|
* stream-update — each subsequent value, detail.data = the new value
|
|
39
39
|
* stream-error — transport-level error, detail.error = message
|
|
40
|
+
*
|
|
41
|
+
* Opting out (gh#1760):
|
|
42
|
+
* `data-stream-managed="false"` tells the document-level observer to
|
|
43
|
+
* never claim this element, even though it carries `data-stream-src`.
|
|
44
|
+
* For elements that own their own imperative fetch (e.g. the billing
|
|
45
|
+
* composites' `refresh()`) and must not also be claimed by this module —
|
|
46
|
+
* never mix an imperative fetch with an unmanaged `data-stream-src` on
|
|
47
|
+
* the same element (data-stream-protocol.md §10.1).
|
|
40
48
|
*/
|
|
41
49
|
|
|
42
50
|
import { signal, effect, untracked } from './signals.js';
|
|
@@ -54,6 +62,11 @@ const ATTRS = {
|
|
|
54
62
|
merge: 'data-stream-merge',
|
|
55
63
|
format: 'data-stream-format',
|
|
56
64
|
id: 'data-stream-id',
|
|
65
|
+
// gh#1760 — opt-out for elements that carry `data-stream-src` but manage
|
|
66
|
+
// their own fetch (e.g. the billing composites' hand-rolled `refresh()`).
|
|
67
|
+
// `"false"` tells the document-level observer to never claim this element,
|
|
68
|
+
// so exactly one owner (the element's own imperative fetch) ever runs.
|
|
69
|
+
managed: 'data-stream-managed',
|
|
57
70
|
};
|
|
58
71
|
|
|
59
72
|
const STREAMS = new Map(); /* streamId → { signal, refs, transport, opts } */
|
|
@@ -386,6 +399,7 @@ function applyData(el, raw, opts) {
|
|
|
386
399
|
export function start(el) {
|
|
387
400
|
stop(el);
|
|
388
401
|
if (!el.isConnected) return;
|
|
402
|
+
if (attr(el, 'managed') === 'false') return; // gh#1760 — self-managed opt-out
|
|
389
403
|
const src = attr(el, 'src');
|
|
390
404
|
if (!src) return;
|
|
391
405
|
|
|
@@ -442,7 +456,8 @@ export function stop(el) {
|
|
|
442
456
|
const ATTR_FILTER = Object.values(ATTRS);
|
|
443
457
|
|
|
444
458
|
function isStreamingEl(node) {
|
|
445
|
-
return node && node.nodeType === 1 && node.hasAttribute && node.hasAttribute(ATTRS.src)
|
|
459
|
+
return !!(node && node.nodeType === 1 && node.hasAttribute && node.hasAttribute(ATTRS.src)
|
|
460
|
+
&& node.getAttribute(ATTRS.managed) !== 'false');
|
|
446
461
|
}
|
|
447
462
|
|
|
448
463
|
function visitSubtree(root, fn) {
|
|
@@ -476,11 +491,31 @@ const observer = typeof MutationObserver !== 'undefined'
|
|
|
476
491
|
|
|
477
492
|
function bootstrap() {
|
|
478
493
|
if (typeof document === 'undefined') return;
|
|
494
|
+
// Register the observer synchronously — no live mutation is missed.
|
|
479
495
|
observer?.observe(document.documentElement, {
|
|
480
496
|
childList: true, subtree: true,
|
|
481
497
|
attributes: true, attributeFilter: ATTR_FILTER,
|
|
482
498
|
});
|
|
483
|
-
|
|
499
|
+
// gh#1760 — defer the INITIAL sweep of already-present markup by one
|
|
500
|
+
// microtask. Without this, a page whose entry module imports the core
|
|
501
|
+
// barrel (this module) BEFORE a self-managed composite's own module
|
|
502
|
+
// (which sets `data-stream-managed="false"` synchronously from its
|
|
503
|
+
// upgrade-triggered `connectedCallback`) races: this sweep would claim
|
|
504
|
+
// pre-existing SSR markup before the composite ever gets a chance to
|
|
505
|
+
// opt out. Deferring to a microtask lets the rest of the SAME
|
|
506
|
+
// synchronous module-evaluation phase — including every sibling
|
|
507
|
+
// module's own top-level side effects — finish first, so by the time
|
|
508
|
+
// this sweep runs, every already-loaded composite has already set its
|
|
509
|
+
// marker. (A composite loaded asynchronously — a dynamic `import()` —
|
|
510
|
+
// after this microtask has already fired is not covered by this
|
|
511
|
+
// deferral alone; its own `connected()` marker-set plus the observer's
|
|
512
|
+
// live attribute-mutation handling still tears down a wrongly-started
|
|
513
|
+
// stream, bounding the damage to at most one extra fetch.)
|
|
514
|
+
queueMicrotask(() => {
|
|
515
|
+
document.querySelectorAll(`[${ATTRS.src}]`).forEach((el) => {
|
|
516
|
+
if (isStreamingEl(el)) start(el);
|
|
517
|
+
});
|
|
518
|
+
});
|
|
484
519
|
}
|
|
485
520
|
|
|
486
521
|
if (typeof document !== 'undefined') {
|
package/core/element.js
CHANGED
|
@@ -91,7 +91,7 @@ function reflect(el, a, v, t) {
|
|
|
91
91
|
}
|
|
92
92
|
|
|
93
93
|
const parseAttr = (v, t) =>
|
|
94
|
-
t === Boolean ? v !== null : t === Number ? (v === null ? null : +v) : v;
|
|
94
|
+
t === Boolean ? v !== null && v !== 'false' : t === Number ? (v === null ? null : +v) : v;
|
|
95
95
|
|
|
96
96
|
function adoptStyles(ctor) {
|
|
97
97
|
if (Object.hasOwn(ctor, '_sa')) return;
|
package/core/index.d.ts
CHANGED
|
@@ -9,6 +9,7 @@ export * from './template.js';
|
|
|
9
9
|
export * from './element.js';
|
|
10
10
|
export * from './form.js';
|
|
11
11
|
export * from './register.js';
|
|
12
|
+
export * from './store.js';
|
|
12
13
|
// Mirror the explicit re-export in core/index.js — `streams` + `whenStream`
|
|
13
14
|
// (the public surface of the data-stream attribute-driven ingestion module).
|
|
14
15
|
// `export *` from data-stream.js would also expose internals; keep the
|
package/core/index.js
CHANGED
package/core/provider.d.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* `<router-ui>` provider — declarative + imperative client-side
|
|
3
|
-
* routing built on {@link RouteController} (re-
|
|
4
|
-
*
|
|
3
|
+
* routing built on {@link RouteController} (re-exported here for
|
|
4
|
+
* back-compat — gh#1766 — the canonical implementation lives in
|
|
5
|
+
* `core/controller.js`).
|
|
5
6
|
*
|
|
6
7
|
* Two consumer paths:
|
|
7
8
|
*
|
|
@@ -25,22 +26,17 @@
|
|
|
25
26
|
*/
|
|
26
27
|
|
|
27
28
|
import { UIElement } from './element.js';
|
|
28
|
-
import type { Route
|
|
29
|
-
import {
|
|
29
|
+
import type { Route } from './controller.js';
|
|
30
|
+
import { RouteController } from './controller.js';
|
|
30
31
|
|
|
31
32
|
/**
|
|
32
|
-
* Routing-state controller
|
|
33
|
-
*
|
|
34
|
-
*
|
|
33
|
+
* Routing-state controller, re-exported from `core/controller.js`
|
|
34
|
+
* (gh#1766) for back-compat — consumers can import either path.
|
|
35
|
+
* New code should prefer
|
|
35
36
|
* `import { RouteController } from '@adia-ai/web-components/core/controller'`
|
|
36
37
|
* for the canonical path.
|
|
37
38
|
*/
|
|
38
|
-
export
|
|
39
|
-
static schema: ControllerSchema;
|
|
40
|
-
constructor(options?: RouteControllerOptions);
|
|
41
|
-
getState(): RouteState;
|
|
42
|
-
commands: RouteCommands;
|
|
43
|
-
}
|
|
39
|
+
export { RouteController };
|
|
44
40
|
|
|
45
41
|
/**
|
|
46
42
|
* Optional async transform applied to fetched route content before
|
package/core/provider.js
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* AdiaUI Router — Self-contained vanilla JS bundle.
|
|
3
|
-
* Includes:
|
|
3
|
+
* Includes: router-ui component; re-exports `RouteController` for
|
|
4
|
+
* back-compat (gh#1766 — the canonical definition lives in
|
|
5
|
+
* `core/controller.js`, alongside its `BaseController` superclass).
|
|
4
6
|
*
|
|
5
|
-
* Depends on:
|
|
7
|
+
* Depends on: element.js (UIElement), controller.js (RouteController)
|
|
6
8
|
*
|
|
7
9
|
* Usage:
|
|
8
10
|
* import { UIElement } from './core.js';
|
|
@@ -24,7 +26,7 @@
|
|
|
24
26
|
|
|
25
27
|
import { UIElement } from './element.js';
|
|
26
28
|
import { defineIfFree } from './register.js';
|
|
27
|
-
import {
|
|
29
|
+
import { RouteController } from './controller.js';
|
|
28
30
|
import { viewTransition } from '../traits/view-transition/view-transition.js';
|
|
29
31
|
|
|
30
32
|
// ═══════════════════════════════════════════════════════════════
|
|
@@ -46,118 +48,12 @@ class UIProvider extends UIElement {
|
|
|
46
48
|
}
|
|
47
49
|
|
|
48
50
|
// ═══════════════════════════════════════════════════════════════
|
|
49
|
-
// ROUTE CONTROLLER
|
|
51
|
+
// ROUTE CONTROLLER — re-exported for back-compat (gh#1766); the
|
|
52
|
+
// canonical definition lives in `core/controller.js`, which is
|
|
53
|
+
// where `BaseController` (its own superclass) already lives.
|
|
50
54
|
// ═══════════════════════════════════════════════════════════════
|
|
51
55
|
|
|
52
|
-
export
|
|
53
|
-
static schema = {
|
|
54
|
-
name: 'route',
|
|
55
|
-
state: { path: 'string', params: 'object', route: 'object', previous: 'string' },
|
|
56
|
-
commands: ['navigate', 'replace', 'back', 'forward', 'setRoutes'],
|
|
57
|
-
attributes: ['data-route-path'],
|
|
58
|
-
};
|
|
59
|
-
|
|
60
|
-
#routes = [];
|
|
61
|
-
#path = '';
|
|
62
|
-
#previous = '';
|
|
63
|
-
#params = {};
|
|
64
|
-
#route = null;
|
|
65
|
-
#historySync = true;
|
|
66
|
-
#boundPopState = null;
|
|
67
|
-
|
|
68
|
-
constructor({ routes = [], initial, historySync = true } = {}) {
|
|
69
|
-
super();
|
|
70
|
-
this.#routes = routes;
|
|
71
|
-
this.#historySync = historySync;
|
|
72
|
-
this.#path = initial ?? location.pathname;
|
|
73
|
-
this.#match();
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
getState() {
|
|
77
|
-
return {
|
|
78
|
-
path: this.#path,
|
|
79
|
-
params: { ...this.#params },
|
|
80
|
-
route: this.#route,
|
|
81
|
-
previous: this.#previous,
|
|
82
|
-
};
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
reflect() {
|
|
86
|
-
const host = this.host;
|
|
87
|
-
if (!host) return;
|
|
88
|
-
host.setAttribute('data-route-path', this.#path);
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
onConnect(host) {
|
|
92
|
-
if (this.#historySync) {
|
|
93
|
-
this.#boundPopState = () => {
|
|
94
|
-
this.#previous = this.#path;
|
|
95
|
-
this.#path = location.pathname;
|
|
96
|
-
this.#match();
|
|
97
|
-
this.notify();
|
|
98
|
-
};
|
|
99
|
-
window.addEventListener('popstate', this.#boundPopState);
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
onDisconnect() {
|
|
104
|
-
if (this.#boundPopState) {
|
|
105
|
-
window.removeEventListener('popstate', this.#boundPopState);
|
|
106
|
-
this.#boundPopState = null;
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
#match() {
|
|
111
|
-
this.#params = {};
|
|
112
|
-
this.#route = null;
|
|
113
|
-
for (const route of this.#routes) {
|
|
114
|
-
const match = this.#matchPath(route.path, this.#path);
|
|
115
|
-
if (match) {
|
|
116
|
-
this.#params = match.params;
|
|
117
|
-
this.#route = route;
|
|
118
|
-
return;
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
#matchPath(pattern, path) {
|
|
124
|
-
const patternParts = pattern.split('/').filter(Boolean);
|
|
125
|
-
const pathParts = path.split('/').filter(Boolean);
|
|
126
|
-
if (!pattern.includes(':')) return pattern === path ? { params: {} } : null;
|
|
127
|
-
if (patternParts.length !== pathParts.length) return null;
|
|
128
|
-
const params = {};
|
|
129
|
-
for (let i = 0; i < patternParts.length; i++) {
|
|
130
|
-
if (patternParts[i].startsWith(':')) params[patternParts[i].slice(1)] = pathParts[i];
|
|
131
|
-
else if (patternParts[i] !== pathParts[i]) return null;
|
|
132
|
-
}
|
|
133
|
-
return { params };
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
commands = {
|
|
137
|
-
navigate: (path) => {
|
|
138
|
-
if (path === this.#path) return;
|
|
139
|
-
this.#previous = this.#path;
|
|
140
|
-
this.#path = path;
|
|
141
|
-
this.#match();
|
|
142
|
-
if (this.#historySync) history.pushState(null, '', path);
|
|
143
|
-
this.notify();
|
|
144
|
-
},
|
|
145
|
-
replace: (path) => {
|
|
146
|
-
this.#previous = this.#path;
|
|
147
|
-
this.#path = path;
|
|
148
|
-
this.#match();
|
|
149
|
-
if (this.#historySync) history.replaceState(null, '', path);
|
|
150
|
-
this.notify();
|
|
151
|
-
},
|
|
152
|
-
back: () => { if (this.#historySync) history.back(); },
|
|
153
|
-
forward: () => { if (this.#historySync) history.forward(); },
|
|
154
|
-
setRoutes: (routes) => {
|
|
155
|
-
this.#routes = routes;
|
|
156
|
-
this.#match();
|
|
157
|
-
this.notify();
|
|
158
|
-
},
|
|
159
|
-
};
|
|
160
|
-
}
|
|
56
|
+
export { RouteController };
|
|
161
57
|
|
|
162
58
|
// ═══════════════════════════════════════════════════════════════
|
|
163
59
|
// ROUTER-UI — content fragment renderer
|
package/core/store.d.ts
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared app store — the blessed R2 primitive. Signal-backed, with a
|
|
3
|
+
* Set-of-listeners-compatible `subscribe()` channel for imperative
|
|
4
|
+
* (non-`effect()`) consumers.
|
|
5
|
+
*
|
|
6
|
+
* @see ./store.js (runtime SoT)
|
|
7
|
+
* @see ../USAGE.md
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* A `signal()`-backed store. `.value` composes with `computed()`/`effect()`
|
|
12
|
+
* exactly like a plain signal; `subscribe()` is the extra imperative
|
|
13
|
+
* channel the seven hand-rolled app-layer stores already share (see
|
|
14
|
+
* `store.js`'s header for the full rationale and the rejected
|
|
15
|
+
* BaseController alternative).
|
|
16
|
+
*/
|
|
17
|
+
export interface Store<T> {
|
|
18
|
+
/** Tracked read/write, same semantics as `Signal<T>.value`. */
|
|
19
|
+
value: T;
|
|
20
|
+
/** Read without subscribing the surrounding `effect()`. */
|
|
21
|
+
peek(): T;
|
|
22
|
+
/**
|
|
23
|
+
* Subscribe to changes. `cb` receives the new value on every write that
|
|
24
|
+
* doesn't fail the `Object.is` no-notify guard. Returns an unsubscribe
|
|
25
|
+
* function — safe to call more than once. Compatible with
|
|
26
|
+
* `UIElement`'s `controller` setter (`element.controller = store`).
|
|
27
|
+
*/
|
|
28
|
+
subscribe(cb: (value: T) => void): () => void;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Create a shared app store.
|
|
33
|
+
*
|
|
34
|
+
* @example
|
|
35
|
+
* const count = createStore(0);
|
|
36
|
+
* const stop = count.subscribe((v) => console.log('count is', v));
|
|
37
|
+
* count.value = 1; // logs "count is 1"
|
|
38
|
+
* count.value = 1; // no-op — Object.is guard, no log
|
|
39
|
+
* stop();
|
|
40
|
+
*
|
|
41
|
+
* @example Controller-setter interop
|
|
42
|
+
* class MyEl extends UIElement {}
|
|
43
|
+
* const el = new MyEl();
|
|
44
|
+
* el.controller = createStore({ ready: false }); // re-renders on change
|
|
45
|
+
*/
|
|
46
|
+
export function createStore<T>(initial: T): Store<T>;
|
package/core/store.js
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AdiaUI shared app store — the blessed R2 primitive.
|
|
3
|
+
*
|
|
4
|
+
* A signal-backed replacement for the Set-of-listeners pub/sub pattern
|
|
5
|
+
* hand-rolled across the app layer: plan-store, data-client, patient-visit
|
|
6
|
+
* record, persona store, task-service, a2ui BaseController, a2ui Surface
|
|
7
|
+
* watchers (reactivity review, .claude/docs/reports/2026-08-20-reactivity-
|
|
8
|
+
* review/03-app-layer-stores.md §4). Doctrine's pattern menu row 1 says
|
|
9
|
+
* reactive state -> signals; `createStore` is `signal()` plus the one thing
|
|
10
|
+
* those seven stores actually needed on top of it: an imperative
|
|
11
|
+
* `subscribe(cb) -> unsubscribe` channel for consumers that are closures or
|
|
12
|
+
* classes rather than `effect()` bodies.
|
|
13
|
+
*
|
|
14
|
+
* `.value` reads/writes pass straight through to a real `signal()`, so a
|
|
15
|
+
* store composes with `computed()`/`effect()` exactly like any other signal
|
|
16
|
+
* (tracked reads, `Object.is` no-notify, microtask-batched effect re-runs).
|
|
17
|
+
* `subscribe()` is the separate, synchronous, imperative channel the seven
|
|
18
|
+
* hand-rolled stores already share — same shape, so migrating one onto this
|
|
19
|
+
* primitive is a mechanical swap (opportunistic, later, per store; this
|
|
20
|
+
* primitive lands with NO migration).
|
|
21
|
+
*
|
|
22
|
+
* Interops with `UIElement`'s `controller` setter (`core/element.js`)
|
|
23
|
+
* out of the box: `element.controller = store` wires `store.subscribe(fn)`
|
|
24
|
+
* into the element's own render-trigger machinery — the setter calls
|
|
25
|
+
* `c.connect?.(this)` / `c.subscribe?.(...)` / `c.disconnect?.(this)`, all
|
|
26
|
+
* optional-chained, and `subscribe()` alone satisfies that contract.
|
|
27
|
+
*
|
|
28
|
+
* Rejected alternative: promoting/absorbing `BaseController`
|
|
29
|
+
* (`core/controller.js`) into this role, per the reactivity review's F5.
|
|
30
|
+
* BaseController's `connect()` bakes in schema/`getState()` validation
|
|
31
|
+
* (dev-warns without a static `schema`, dev-errors without an overridden
|
|
32
|
+
* `getState()`) that exists to serve DOM-reflecting UI controllers
|
|
33
|
+
* (`RouteController`'s attributes/commands surface) — semantics a plain
|
|
34
|
+
* data store has no use for and would either have to satisfy with a dummy
|
|
35
|
+
* schema or suppress. The one thing BaseController actually offers this
|
|
36
|
+
* role — the `subscribe(fn) -> unsubscribe` shape the controller setter
|
|
37
|
+
* expects — costs nothing to reimplement directly on `signal()`, and doing
|
|
38
|
+
* so keeps the store from inheriting a validation path built for a
|
|
39
|
+
* different kind of thing. BaseController stays as-is; still one consumer
|
|
40
|
+
* (`RouteController` / `<router-ui>`), still F5's to resolve separately.
|
|
41
|
+
*
|
|
42
|
+
* @see ../USAGE.md
|
|
43
|
+
* @see ./element.js (the `controller` setter this interops with)
|
|
44
|
+
* @see ./controller.js (BaseController — the rejected alternative above)
|
|
45
|
+
*/
|
|
46
|
+
|
|
47
|
+
import { signal } from './signals.js';
|
|
48
|
+
|
|
49
|
+
export function createStore(initial) {
|
|
50
|
+
const s = signal(initial);
|
|
51
|
+
const listeners = new Set();
|
|
52
|
+
|
|
53
|
+
return {
|
|
54
|
+
get value() {
|
|
55
|
+
return s.value;
|
|
56
|
+
},
|
|
57
|
+
set value(next) {
|
|
58
|
+
// Object.is no-notify guard — matches persona-store's proven
|
|
59
|
+
// behavior (packages/llm/persona/src/store.ts) and the underlying
|
|
60
|
+
// signal's own equality cutoff. Checked here, not inferred from
|
|
61
|
+
// signal()'s silent no-op, so an identical write never reaches
|
|
62
|
+
// `listeners` either.
|
|
63
|
+
const changed = !Object.is(s.peek(), next);
|
|
64
|
+
s.value = next;
|
|
65
|
+
if (changed) {
|
|
66
|
+
// Pass the captured `next`, never re-read `s.peek()` here — a
|
|
67
|
+
// listener that re-entrantly writes `store.value` during this
|
|
68
|
+
// loop would otherwise make every later listener observe THAT
|
|
69
|
+
// write's value instead of this one's, silently skipping the
|
|
70
|
+
// value this write actually delivered.
|
|
71
|
+
for (const cb of listeners) cb(next);
|
|
72
|
+
}
|
|
73
|
+
},
|
|
74
|
+
/** Read without subscribing the surrounding `effect()`. */
|
|
75
|
+
peek() {
|
|
76
|
+
return s.peek();
|
|
77
|
+
},
|
|
78
|
+
/**
|
|
79
|
+
* Set-of-listeners-compatible subscribe. `cb` receives the new value;
|
|
80
|
+
* callers that only care that *something* changed (the
|
|
81
|
+
* `UIElement.controller` setter among them) simply ignore the
|
|
82
|
+
* argument. Returns an unsubscribe function.
|
|
83
|
+
*/
|
|
84
|
+
subscribe(cb) {
|
|
85
|
+
listeners.add(cb);
|
|
86
|
+
return () => listeners.delete(cb);
|
|
87
|
+
},
|
|
88
|
+
};
|
|
89
|
+
}
|