@adia-ai/web-components 0.5.2 → 0.5.3
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/USAGE.md +42 -0
- package/components/accordion/accordion.a2ui.json +8 -2
- package/components/accordion/accordion.d.ts +7 -2
- package/components/accordion/accordion.yaml +8 -2
- package/components/accordion/class.js +6 -0
- package/components/agent-questions/agent-questions.yaml +2 -0
- package/components/agent-questions/class.js +6 -0
- package/components/agent-reasoning/agent-reasoning.yaml +5 -0
- package/components/agent-reasoning/class.js +6 -0
- package/components/agent-trace/agent-trace.js +6 -0
- package/components/agent-trace/agent-trace.yaml +3 -0
- package/components/calendar-picker/calendar-picker.yaml +4 -0
- package/components/calendar-picker/class.js +6 -0
- package/components/canvas/canvas.a2ui.json +1 -10
- package/components/canvas/canvas.d.ts +0 -6
- package/components/canvas/canvas.yaml +1 -7
- package/components/card/card.a2ui.json +1 -5
- package/components/card/card.d.ts +0 -9
- package/components/card/card.yaml +1 -3
- package/components/color-picker/class.js +6 -0
- package/components/color-picker/color-picker.yaml +2 -0
- package/components/command/class.js +6 -0
- package/components/command/command.yaml +2 -0
- package/components/drawer/class.js +25 -3
- package/components/drawer/drawer.a2ui.json +13 -1
- package/components/drawer/drawer.d.ts +6 -1
- package/components/drawer/drawer.yaml +11 -1
- package/components/pane/class.js +6 -0
- package/components/pane/pane.yaml +2 -0
- package/components/row/row.a2ui.json +1 -5
- package/components/row/row.d.ts +0 -9
- package/components/row/row.yaml +1 -3
- package/components/select/class.js +6 -0
- package/components/select/select.yaml +2 -0
- package/components/slider/class.js +25 -0
- package/components/table/class.js +6 -0
- package/components/table/table.a2ui.json +13 -0
- package/components/table/table.d.ts +9 -0
- package/components/table/table.yaml +13 -0
- package/components/tag/class.js +6 -0
- package/components/tag/tag.yaml +2 -0
- package/components/tree/class.js +6 -0
- package/components/tree/tree.yaml +2 -0
- package/core/icons.js +148 -5
- package/core/icons.test.js +187 -0
- package/core/template.js +59 -3
- package/package.json +16 -2
package/USAGE.md
CHANGED
|
@@ -134,6 +134,33 @@ const tpl3 = html`<slider-ui .value=${minL.value * 100} min="0" max="100"></slid
|
|
|
134
134
|
|
|
135
135
|
The `.prop=` syntax sets the JS property (not the attribute). The `attr=` syntax sets an attribute. Both round-trip through the signal-backed setter, so either works for reactive updates from your side.
|
|
136
136
|
|
|
137
|
+
### Template attribute bindings — full / property / no-partial (§152, v0.5.3)
|
|
138
|
+
|
|
139
|
+
The `html\`` template literal supports three attribute-binding modes:
|
|
140
|
+
|
|
141
|
+
| Syntax | Behavior |
|
|
142
|
+
|---|---|
|
|
143
|
+
| `attr="${value}"` (or just `attr=${value}`) | **Full attribute replacement.** The entire attribute value is the placeholder. The string `value` becomes the attribute value via `setAttribute()`. Works for any string-typed attribute. |
|
|
144
|
+
| `.prop=${value}` (leading dot) | **Property assignment.** Sets `element.prop = value` directly (bypasses attributes). Preferred for objects, functions, arrays, signals — anything that doesn't serialize to a string. |
|
|
145
|
+
| `attr="prefix-${value}-suffix"` | **NOT SUPPORTED.** Partial interpolation is not implemented — the attribute-part contract is whole-value-only. The framework will `console.warn` at scan time. Compute the full attribute value as a single expression and interpolate the whole. |
|
|
146
|
+
|
|
147
|
+
Example of the partial-interpolation pitfall and its fix:
|
|
148
|
+
|
|
149
|
+
```js
|
|
150
|
+
// ❌ NOT SUPPORTED — partial attribute interpolation
|
|
151
|
+
// Will console.warn at scan time + the `{{p:0}}` placeholder text reaches the DOM as literal.
|
|
152
|
+
html`<div style="background:${bgCss}; color:${labelColor}">…</div>`;
|
|
153
|
+
|
|
154
|
+
// ✅ Compute the full attribute as one expression
|
|
155
|
+
const swatchStyle = `background:${bgCss}; color:${labelColor};`;
|
|
156
|
+
html`<div style="${swatchStyle}">…</div>`;
|
|
157
|
+
|
|
158
|
+
// ✅ Or use property binding with an object (CSSStyleDeclaration-like)
|
|
159
|
+
html`<div .style=${{ background: bgCss, color: labelColor }}>…</div>`;
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
**Why the constraint exists**: the engine pre-processes static-string chunks to insert placeholder markers (`{{p:N}}` for attribute positions, `<!--p:N-->` for content positions). At scan time, the post-substitution attribute value must match `^\{\{p:N\}\}$` for the placeholder-to-part mapping to register. Sub-string placeholders break this contract. The runtime guard (added v0.5.3 §152) makes the failure mode explicit instead of silent.
|
|
163
|
+
|
|
137
164
|
### Using React, Lit, Vue, Svelte, etc.
|
|
138
165
|
|
|
139
166
|
When the binding system isn't AdiaUI's `html` tag, the rule generalizes:
|
|
@@ -186,6 +213,21 @@ html`<slider-ui .value=${(v) => v * 100} />`
|
|
|
186
213
|
|
|
187
214
|
The rule: **inside `.prop=${…}`, anything that reads `.value` directly is a snapshot. Wrap in `() => …` to get reactivity.**
|
|
188
215
|
|
|
216
|
+
**Dev-mode guard (v0.5.3 §153 / FEEDBACK-07 §3):** primitives that read their value synchronously (e.g. `<slider-ui>`) emit a `console.warn` when their `.value` is assigned a function — this catches the case where the function wasn't wrapped by the template engine (e.g. assigned imperatively `sliderEl.value = fn`, or returned by a framework binding that doesn't recognize the AdiaUI reactivity contract). The warn names the two correct patterns:
|
|
217
|
+
|
|
218
|
+
```
|
|
219
|
+
[slider-ui] .value received a function. Did you mean:
|
|
220
|
+
.value=${fn()} ← call the function to get the current value
|
|
221
|
+
.value=${signal.value} ← read the signal's current value
|
|
222
|
+
Functions are not auto-invoked at render time; the slider reads
|
|
223
|
+
.value synchronously. See USAGE.md "Reactive binding" for the
|
|
224
|
+
parent-template-re-read pattern that works across frameworks.
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
If you see this warn in dev console, either:
|
|
228
|
+
1. You're using AdiaUI's `html\``: ensure the function reads at least one signal value (otherwise the function isn't reactive, just expensive). Pattern A above with `() => signal.value * 100` is correct.
|
|
229
|
+
2. You're using a different framework (React/Lit/Vue): the binding system isn't AdiaUI's `html` engine, so functions don't auto-wrap. Use Pattern B from the parent-template-re-read recipe — read the signal in the parent's render, pass the extracted value imperatively or via your framework's binding.
|
|
230
|
+
|
|
189
231
|
For more complex derived values, `computed()` gives you a named reactive holder you can pass directly or read in templates:
|
|
190
232
|
|
|
191
233
|
```js
|
|
@@ -33,8 +33,14 @@
|
|
|
33
33
|
"icon-ui"
|
|
34
34
|
],
|
|
35
35
|
"events": {
|
|
36
|
-
"
|
|
37
|
-
"description": "Fired when the
|
|
36
|
+
"toggle": {
|
|
37
|
+
"description": "Fired when the accordion panel opens or closes (single-pane accordion-ui dispatch — multi-pane group emits the same event per child).",
|
|
38
|
+
"detail": {
|
|
39
|
+
"open": {
|
|
40
|
+
"description": "New open state of the panel.",
|
|
41
|
+
"type": "boolean"
|
|
42
|
+
}
|
|
43
|
+
}
|
|
38
44
|
}
|
|
39
45
|
},
|
|
40
46
|
"examples": [
|
|
@@ -12,7 +12,12 @@
|
|
|
12
12
|
|
|
13
13
|
import { UIElement } from '../../core/element.js';
|
|
14
14
|
|
|
15
|
-
export
|
|
15
|
+
export interface AccordionToggleEventDetail {
|
|
16
|
+
/** New open state of the panel. */
|
|
17
|
+
open: boolean;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export type AccordionToggleEvent = CustomEvent<AccordionToggleEventDetail>;
|
|
16
21
|
|
|
17
22
|
export class UIAccordion extends UIElement {
|
|
18
23
|
/** Allow multiple panels to be open simultaneously */
|
|
@@ -23,5 +28,5 @@ export class UIAccordion extends UIElement {
|
|
|
23
28
|
listener: (this: UIAccordion, ev: HTMLElementEventMap[K]) => unknown,
|
|
24
29
|
options?: boolean | AddEventListenerOptions,
|
|
25
30
|
): void;
|
|
26
|
-
addEventListener(type: '
|
|
31
|
+
addEventListener(type: 'toggle', listener: (ev: AccordionToggleEvent) => unknown, options?: boolean | AddEventListenerOptions): void;
|
|
27
32
|
}
|
|
@@ -17,8 +17,12 @@ props:
|
|
|
17
17
|
type: boolean
|
|
18
18
|
default: false
|
|
19
19
|
events:
|
|
20
|
-
|
|
21
|
-
description: Fired when the
|
|
20
|
+
toggle:
|
|
21
|
+
description: Fired when the accordion panel opens or closes (single-pane accordion-ui dispatch — multi-pane group emits the same event per child).
|
|
22
|
+
detail:
|
|
23
|
+
open:
|
|
24
|
+
type: boolean
|
|
25
|
+
description: New open state of the panel.
|
|
22
26
|
slots:
|
|
23
27
|
default:
|
|
24
28
|
description: pane-ui children
|
|
@@ -27,6 +31,8 @@ states:
|
|
|
27
31
|
description: Default, ready for interaction.
|
|
28
32
|
traits: []
|
|
29
33
|
tokens: {}
|
|
34
|
+
requiredIcons:
|
|
35
|
+
- caret-down
|
|
30
36
|
a2ui:
|
|
31
37
|
rules: []
|
|
32
38
|
anti_patterns: []
|
|
@@ -30,6 +30,12 @@ import { UIElement } from '../../core/element.js';
|
|
|
30
30
|
// ── Accordion Container ────────────────────────────────────────
|
|
31
31
|
|
|
32
32
|
export class UIAccordion extends UIElement {
|
|
33
|
+
// §154 (v0.5.3): Phosphor icons this primitive auto-stamps (without
|
|
34
|
+
// consumer markup). Aggregated by installIconLoadersForRegistered()
|
|
35
|
+
// across all defined elements. Audited by check-required-icons.mjs
|
|
36
|
+
// (slot 11). Per FEEDBACK-06 §4 + FEEDBACK-07 §4.
|
|
37
|
+
static requiredIcons = ['caret-down'];
|
|
38
|
+
|
|
33
39
|
static properties = {
|
|
34
40
|
multiple: { type: Boolean, default: false, reflect: true },
|
|
35
41
|
};
|
|
@@ -50,6 +50,12 @@ function escapeAttr(s) {
|
|
|
50
50
|
}
|
|
51
51
|
|
|
52
52
|
export class UIAgentQuestions extends UIElement {
|
|
53
|
+
// §154 (v0.5.3): Phosphor icons this primitive auto-stamps (without
|
|
54
|
+
// consumer markup). Aggregated by installIconLoadersForRegistered()
|
|
55
|
+
// across all defined elements. Audited by check-required-icons.mjs
|
|
56
|
+
// (slot 11). Per FEEDBACK-06 §4 + FEEDBACK-07 §4.
|
|
57
|
+
static requiredIcons = ['check'];
|
|
58
|
+
|
|
53
59
|
static properties = {
|
|
54
60
|
multi: { type: Boolean, default: false, reflect: true },
|
|
55
61
|
question: { type: String, default: '', reflect: true },
|
|
@@ -77,6 +77,12 @@ function formatDuration(ms) {
|
|
|
77
77
|
}
|
|
78
78
|
|
|
79
79
|
export class UIAgentReasoning extends UIElement {
|
|
80
|
+
// §154 (v0.5.3): Phosphor icons this primitive auto-stamps (without
|
|
81
|
+
// consumer markup). Aggregated by installIconLoadersForRegistered()
|
|
82
|
+
// across all defined elements. Audited by check-required-icons.mjs
|
|
83
|
+
// (slot 11). Per FEEDBACK-06 §4 + FEEDBACK-07 §4.
|
|
84
|
+
static requiredIcons = ['check-circle', 'circle', 'warning', 'warning-circle'];
|
|
85
|
+
|
|
80
86
|
static properties = {
|
|
81
87
|
collapsed: { type: Boolean, default: false, reflect: true },
|
|
82
88
|
noAutocollapse: { type: Boolean, default: false, reflect: true, attribute: 'no-autocollapse' },
|
|
@@ -37,6 +37,12 @@
|
|
|
37
37
|
import { UIElement } from '../../core/element.js';
|
|
38
38
|
|
|
39
39
|
class UIAgentTrace extends UIElement {
|
|
40
|
+
// §154 (v0.5.3): Phosphor icons this primitive auto-stamps (without
|
|
41
|
+
// consumer markup). Aggregated by installIconLoadersForRegistered()
|
|
42
|
+
// across all defined elements. Audited by check-required-icons.mjs
|
|
43
|
+
// (slot 11). Per FEEDBACK-06 §4 + FEEDBACK-07 §4.
|
|
44
|
+
static requiredIcons = ['caret-right', 'circle-fill'];
|
|
45
|
+
|
|
40
46
|
static properties = {
|
|
41
47
|
// agent-* family is default-visible disclosure (see component-token-contract.md
|
|
42
48
|
// §"Toggle state naming"). Use `collapsed` to hide; bare element shows.
|
|
@@ -53,6 +53,12 @@ function sameDay(a, b) {
|
|
|
53
53
|
}
|
|
54
54
|
|
|
55
55
|
export class UICalendarPicker extends UIFormElement {
|
|
56
|
+
// §154 (v0.5.3): Phosphor icons this primitive auto-stamps (without
|
|
57
|
+
// consumer markup). Aggregated by installIconLoadersForRegistered()
|
|
58
|
+
// across all defined elements. Audited by check-required-icons.mjs
|
|
59
|
+
// (slot 11). Per FEEDBACK-06 §4 + FEEDBACK-07 §4.
|
|
60
|
+
static requiredIcons = ['calendar', 'caret-left', 'caret-right'];
|
|
61
|
+
|
|
56
62
|
static properties = {
|
|
57
63
|
...UIFormElement.properties,
|
|
58
64
|
label: { type: String, default: '', reflect: true },
|
|
@@ -32,16 +32,7 @@
|
|
|
32
32
|
"composes": [],
|
|
33
33
|
"events": {
|
|
34
34
|
"canvas-interaction": {
|
|
35
|
-
"description": "Fired on canvas
|
|
36
|
-
},
|
|
37
|
-
"change": {
|
|
38
|
-
"description": "Fired when the value changes (on blur for inputs, on selection for pickers)."
|
|
39
|
-
},
|
|
40
|
-
"click": {
|
|
41
|
-
"description": "Fired on pointer click."
|
|
42
|
-
},
|
|
43
|
-
"input": {
|
|
44
|
-
"description": "Fired on each input change during typing."
|
|
35
|
+
"description": "Fired on canvas interactions (focus, blur, pointer events). Dispatched at canvas.js:50 + :167."
|
|
45
36
|
}
|
|
46
37
|
},
|
|
47
38
|
"examples": [
|
|
@@ -13,9 +13,6 @@
|
|
|
13
13
|
import { UIElement } from '../../core/element.js';
|
|
14
14
|
|
|
15
15
|
export type CanvasInteractionEvent = CustomEvent<unknown>;
|
|
16
|
-
export type CanvasChangeEvent = CustomEvent<unknown>;
|
|
17
|
-
export type CanvasClickEvent = CustomEvent<unknown>;
|
|
18
|
-
export type CanvasInputEvent = CustomEvent<unknown>;
|
|
19
16
|
|
|
20
17
|
export class UICanvas extends UIElement {
|
|
21
18
|
/** Component property: theme. */
|
|
@@ -27,7 +24,4 @@ export class UICanvas extends UIElement {
|
|
|
27
24
|
options?: boolean | AddEventListenerOptions,
|
|
28
25
|
): void;
|
|
29
26
|
addEventListener(type: 'canvas-interaction', listener: (ev: CanvasInteractionEvent) => unknown, options?: boolean | AddEventListenerOptions): void;
|
|
30
|
-
addEventListener(type: 'change', listener: (ev: CanvasChangeEvent) => unknown, options?: boolean | AddEventListenerOptions): void;
|
|
31
|
-
addEventListener(type: 'click', listener: (ev: CanvasClickEvent) => unknown, options?: boolean | AddEventListenerOptions): void;
|
|
32
|
-
addEventListener(type: 'input', listener: (ev: CanvasInputEvent) => unknown, options?: boolean | AddEventListenerOptions): void;
|
|
33
27
|
}
|
|
@@ -24,13 +24,7 @@ props:
|
|
|
24
24
|
default: ''
|
|
25
25
|
events:
|
|
26
26
|
canvas-interaction:
|
|
27
|
-
description: Fired on canvas
|
|
28
|
-
change:
|
|
29
|
-
description: Fired when the value changes (on blur for inputs, on selection for pickers).
|
|
30
|
-
click:
|
|
31
|
-
description: Fired on pointer click.
|
|
32
|
-
input:
|
|
33
|
-
description: Fired on each input change during typing.
|
|
27
|
+
description: Fired on canvas interactions (focus, blur, pointer events). Dispatched at canvas.js:50 + :167.
|
|
34
28
|
slots:
|
|
35
29
|
default:
|
|
36
30
|
description: Default slot — primary child content.
|
|
@@ -76,11 +76,7 @@
|
|
|
76
76
|
"anti_patterns": [],
|
|
77
77
|
"category": "container",
|
|
78
78
|
"composes": [],
|
|
79
|
-
"events": {
|
|
80
|
-
"drag-end": {
|
|
81
|
-
"description": "Fired when a drag completes."
|
|
82
|
-
}
|
|
83
|
-
},
|
|
79
|
+
"events": {},
|
|
84
80
|
"examples": [
|
|
85
81
|
{
|
|
86
82
|
"description": "Chat interface with message bubbles containing avatar and text pairs, plus an input footer.",
|
|
@@ -12,8 +12,6 @@
|
|
|
12
12
|
|
|
13
13
|
import { UIElement } from '../../core/element.js';
|
|
14
14
|
|
|
15
|
-
export type CardDragEndEvent = CustomEvent<unknown>;
|
|
16
|
-
|
|
17
15
|
export class UICard extends UIElement {
|
|
18
16
|
/** Enables drag handle + cursor:grab. Wires the draggable trait; dispatches drag-end. */
|
|
19
17
|
draggable: boolean;
|
|
@@ -27,11 +25,4 @@ export class UICard extends UIElement {
|
|
|
27
25
|
size: 'sm' | 'md' | 'lg';
|
|
28
26
|
/** Visual style. `outline` is an alias for `outlined`; `flat` removes shadow; `soft`/`primary` apply tinted surfaces. */
|
|
29
27
|
variant: 'default' | 'outlined' | 'outline' | 'filled' | 'ghost' | 'flat' | 'soft' | 'primary';
|
|
30
|
-
|
|
31
|
-
addEventListener<K extends keyof HTMLElementEventMap>(
|
|
32
|
-
type: K,
|
|
33
|
-
listener: (this: UICard, ev: HTMLElementEventMap[K]) => unknown,
|
|
34
|
-
options?: boolean | AddEventListenerOptions,
|
|
35
|
-
): void;
|
|
36
|
-
addEventListener(type: 'drag-end', listener: (ev: CardDragEndEvent) => unknown, options?: boolean | AddEventListenerOptions): void;
|
|
37
28
|
}
|
|
@@ -123,6 +123,12 @@ function gamutMapChroma(L, C, H) {
|
|
|
123
123
|
// ── Component ────────────────────────────────────────────
|
|
124
124
|
|
|
125
125
|
export class UIColorPicker extends UIFormElement {
|
|
126
|
+
// §154 (v0.5.3): Phosphor icons this primitive auto-stamps (without
|
|
127
|
+
// consumer markup). Aggregated by installIconLoadersForRegistered()
|
|
128
|
+
// across all defined elements. Audited by check-required-icons.mjs
|
|
129
|
+
// (slot 11). Per FEEDBACK-06 §4 + FEEDBACK-07 §4.
|
|
130
|
+
static requiredIcons = ['copy'];
|
|
131
|
+
|
|
126
132
|
static properties = {
|
|
127
133
|
...UIFormElement.properties,
|
|
128
134
|
value: { type: String, default: '#3b82f6', reflect: true },
|
|
@@ -34,6 +34,12 @@ import { UIElement } from '../../core/element.js';
|
|
|
34
34
|
* dismiss — Escape pressed
|
|
35
35
|
*/
|
|
36
36
|
export class UICommand extends UIElement {
|
|
37
|
+
// §154 (v0.5.3): Phosphor icons this primitive auto-stamps (without
|
|
38
|
+
// consumer markup). Aggregated by installIconLoadersForRegistered()
|
|
39
|
+
// across all defined elements. Audited by check-required-icons.mjs
|
|
40
|
+
// (slot 11). Per FEEDBACK-06 §4 + FEEDBACK-07 §4.
|
|
41
|
+
static requiredIcons = ['magnifying-glass'];
|
|
42
|
+
|
|
37
43
|
static properties = {
|
|
38
44
|
placeholder: { type: String, default: 'Type a command...', reflect: true },
|
|
39
45
|
open: { type: Boolean, default: false, reflect: true },
|
|
@@ -50,6 +50,13 @@ export class UIDrawer extends UIElement {
|
|
|
50
50
|
#previousFocus = null;
|
|
51
51
|
#closeTimer = null;
|
|
52
52
|
#dialogRef = null;
|
|
53
|
+
// §156 (v0.5.3): track the reason the drawer closed so the dispatched
|
|
54
|
+
// `close` CustomEvent carries `detail.reason`. Set at each entry point
|
|
55
|
+
// (escape → 'escape', backdrop → 'backdrop', close-button → 'close-button',
|
|
56
|
+
// any other path defaults to 'programmatic'). Reset to 'programmatic'
|
|
57
|
+
// after dispatch so a subsequent .open=false from consumer code
|
|
58
|
+
// doesn't carry a stale reason. Per FEEDBACK-06 §2.
|
|
59
|
+
#closeReason = 'programmatic';
|
|
53
60
|
|
|
54
61
|
static properties = {
|
|
55
62
|
text: { type: String, default: '', reflect: true },
|
|
@@ -95,11 +102,15 @@ export class UIDrawer extends UIElement {
|
|
|
95
102
|
}
|
|
96
103
|
|
|
97
104
|
#onPress = (e) => {
|
|
98
|
-
if (e.target.closest('[slot="close"]'))
|
|
105
|
+
if (e.target.closest('[slot="close"]')) {
|
|
106
|
+
this.#closeReason = 'close-button';
|
|
107
|
+
this.open = false;
|
|
108
|
+
}
|
|
99
109
|
};
|
|
100
110
|
|
|
101
111
|
#onDialogCancel = (e) => {
|
|
102
112
|
e.preventDefault();
|
|
113
|
+
this.#closeReason = 'escape';
|
|
103
114
|
if (!this.permanent) this.open = false;
|
|
104
115
|
};
|
|
105
116
|
|
|
@@ -107,11 +118,22 @@ export class UIDrawer extends UIElement {
|
|
|
107
118
|
this.open = false;
|
|
108
119
|
this.#previousFocus?.focus();
|
|
109
120
|
this.#previousFocus = null;
|
|
110
|
-
|
|
121
|
+
// §156 (v0.5.3): emit a CustomEvent with typed detail.reason. Capture
|
|
122
|
+
// and reset #closeReason so a subsequent programmatic close doesn't
|
|
123
|
+
// inherit the prior reason. Per FEEDBACK-06 §2.
|
|
124
|
+
const reason = this.#closeReason;
|
|
125
|
+
this.#closeReason = 'programmatic';
|
|
126
|
+
this.dispatchEvent(new CustomEvent('close', {
|
|
127
|
+
detail: { reason },
|
|
128
|
+
bubbles: true,
|
|
129
|
+
}));
|
|
111
130
|
};
|
|
112
131
|
|
|
113
132
|
#onDialogClick = (e) => {
|
|
114
|
-
if (e.target === this.#dialogRef && !this.permanent)
|
|
133
|
+
if (e.target === this.#dialogRef && !this.permanent) {
|
|
134
|
+
this.#closeReason = 'backdrop';
|
|
135
|
+
this.open = false;
|
|
136
|
+
}
|
|
115
137
|
};
|
|
116
138
|
|
|
117
139
|
connected() {
|
|
@@ -63,7 +63,19 @@
|
|
|
63
63
|
"composes": [],
|
|
64
64
|
"events": {
|
|
65
65
|
"close": {
|
|
66
|
-
"description": "Fired when the drawer
|
|
66
|
+
"description": "Fired when the drawer closes via any path (close button, backdrop, Escape key, or programmatic `.open = false`). The `detail.reason` field distinguishes which.",
|
|
67
|
+
"detail": {
|
|
68
|
+
"reason": {
|
|
69
|
+
"description": "`'escape'` (Escape key) / `'backdrop'` (backdrop click) / `'close-button'` ([slot=\"close\"] button click) / `'programmatic'` (consumer set `.open = false` from JS). Defaults to `'programmatic'` when no event-driven path captured a reason. Added v0.5.3 §156 per FEEDBACK-06 §2.",
|
|
70
|
+
"type": "string",
|
|
71
|
+
"enum": [
|
|
72
|
+
"escape",
|
|
73
|
+
"backdrop",
|
|
74
|
+
"close-button",
|
|
75
|
+
"programmatic"
|
|
76
|
+
]
|
|
77
|
+
}
|
|
78
|
+
}
|
|
67
79
|
}
|
|
68
80
|
},
|
|
69
81
|
"examples": [
|
|
@@ -12,7 +12,12 @@
|
|
|
12
12
|
|
|
13
13
|
import { UIElement } from '../../core/element.js';
|
|
14
14
|
|
|
15
|
-
export
|
|
15
|
+
export interface DrawerCloseEventDetail {
|
|
16
|
+
/** `'escape'` (Escape key) / `'backdrop'` (backdrop click) / `'close-button'` ([slot="close"] button click) / `'programmatic'` (consumer set `.open = false` from JS). Defaults to `'programmatic'` when no event-driven path captured a reason. Added v0.5.3 §156 per FEEDBACK-06 §2. */
|
|
17
|
+
reason: 'escape' | 'backdrop' | 'close-button' | 'programmatic';
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export type DrawerCloseEvent = CustomEvent<DrawerCloseEventDetail>;
|
|
16
21
|
|
|
17
22
|
export class UIDrawer extends UIElement {
|
|
18
23
|
/** Controls visibility. When false, backdrop and panel are removed from DOM. */
|
|
@@ -49,7 +49,17 @@ props:
|
|
|
49
49
|
default: ""
|
|
50
50
|
events:
|
|
51
51
|
close:
|
|
52
|
-
description: Fired when the drawer
|
|
52
|
+
description: Fired when the drawer closes via any path (close button, backdrop, Escape key, or programmatic `.open = false`). The `detail.reason` field distinguishes which.
|
|
53
|
+
detail:
|
|
54
|
+
reason:
|
|
55
|
+
type: string
|
|
56
|
+
enum: [escape, backdrop, close-button, programmatic]
|
|
57
|
+
description: >-
|
|
58
|
+
`'escape'` (Escape key) / `'backdrop'` (backdrop click) /
|
|
59
|
+
`'close-button'` ([slot="close"] button click) /
|
|
60
|
+
`'programmatic'` (consumer set `.open = false` from JS).
|
|
61
|
+
Defaults to `'programmatic'` when no event-driven path
|
|
62
|
+
captured a reason. Added v0.5.3 §156 per FEEDBACK-06 §2.
|
|
53
63
|
slots:
|
|
54
64
|
backdrop:
|
|
55
65
|
description: Scrim overlay behind the drawer (stamped by the component).
|
package/components/pane/class.js
CHANGED
|
@@ -47,6 +47,12 @@
|
|
|
47
47
|
import { UIElement } from '../../core/element.js';
|
|
48
48
|
|
|
49
49
|
export class UIPane extends UIElement {
|
|
50
|
+
// §154 (v0.5.3): Phosphor icons this primitive auto-stamps (without
|
|
51
|
+
// consumer markup). Aggregated by installIconLoadersForRegistered()
|
|
52
|
+
// across all defined elements. Audited by check-required-icons.mjs
|
|
53
|
+
// (slot 11). Per FEEDBACK-06 §4 + FEEDBACK-07 §4.
|
|
54
|
+
static requiredIcons = ['caret-right'];
|
|
55
|
+
|
|
50
56
|
static properties = {
|
|
51
57
|
collapsed: { type: Boolean, default: false, reflect: true },
|
|
52
58
|
resizable: { type: Boolean, default: false, reflect: true },
|
|
@@ -55,11 +55,7 @@
|
|
|
55
55
|
"anti_patterns": [],
|
|
56
56
|
"category": "layout",
|
|
57
57
|
"composes": [],
|
|
58
|
-
"events": {
|
|
59
|
-
"drag-end": {
|
|
60
|
-
"description": "Fired when a drag completes."
|
|
61
|
-
}
|
|
62
|
-
},
|
|
58
|
+
"events": {},
|
|
63
59
|
"examples": [
|
|
64
60
|
{
|
|
65
61
|
"description": "Chat interface with message bubbles containing avatar and text pairs, plus an input footer.",
|
package/components/row/row.d.ts
CHANGED
|
@@ -12,8 +12,6 @@
|
|
|
12
12
|
|
|
13
13
|
import { UIElement } from '../../core/element.js';
|
|
14
14
|
|
|
15
|
-
export type RowDragEndEvent = CustomEvent<unknown>;
|
|
16
|
-
|
|
17
15
|
export class UIRow extends UIElement {
|
|
18
16
|
/** Align items */
|
|
19
17
|
align: string;
|
|
@@ -27,11 +25,4 @@ export class UIRow extends UIElement {
|
|
|
27
25
|
justify: string;
|
|
28
26
|
/** Enable flex wrap */
|
|
29
27
|
wrap: boolean;
|
|
30
|
-
|
|
31
|
-
addEventListener<K extends keyof HTMLElementEventMap>(
|
|
32
|
-
type: K,
|
|
33
|
-
listener: (this: UIRow, ev: HTMLElementEventMap[K]) => unknown,
|
|
34
|
-
options?: boolean | AddEventListenerOptions,
|
|
35
|
-
): void;
|
|
36
|
-
addEventListener(type: 'drag-end', listener: (ev: RowDragEndEvent) => unknown, options?: boolean | AddEventListenerOptions): void;
|
|
37
28
|
}
|
package/components/row/row.yaml
CHANGED
|
@@ -19,6 +19,12 @@ function escapeHTML(s) {
|
|
|
19
19
|
}
|
|
20
20
|
|
|
21
21
|
export class UISelect extends UIFormElement {
|
|
22
|
+
// §154 (v0.5.3): Phosphor icons this primitive auto-stamps (without
|
|
23
|
+
// consumer markup). Aggregated by installIconLoadersForRegistered()
|
|
24
|
+
// across all defined elements. Audited by check-required-icons.mjs
|
|
25
|
+
// (slot 11). Per FEEDBACK-06 §4 + FEEDBACK-07 §4.
|
|
26
|
+
static requiredIcons = ['caret-up-down'];
|
|
27
|
+
|
|
22
28
|
static properties = {
|
|
23
29
|
...UIFormElement.properties,
|
|
24
30
|
placeholder: { type: String, default: 'Select...', reflect: true },
|
|
@@ -95,6 +95,31 @@ export class UISlider extends UIFormElement {
|
|
|
95
95
|
render() {
|
|
96
96
|
if (!this.#trackEl) return;
|
|
97
97
|
|
|
98
|
+
// §153 (v0.5.3): function-typed `.value` runtime guard. Per FEEDBACK-07
|
|
99
|
+
// §3, consumers sometimes pass `() => signal.value * 100` expecting
|
|
100
|
+
// auto-subscribe; AdiaUI's reactive system (template.js `isFn` branch)
|
|
101
|
+
// does wrap functions in effects + call them per dep change, but the
|
|
102
|
+
// result of `v()` must be a number for `#pct` / `#format` to work.
|
|
103
|
+
// When the function returns non-number (e.g. doesn't read a signal,
|
|
104
|
+
// returns object/string) OR when consumers bypass the template engine
|
|
105
|
+
// (manual `sliderEl.value = someFunction`), `this.value` ends up as
|
|
106
|
+
// the function itself — `#pct` does NaN math, thumb stays at 0%,
|
|
107
|
+
// silent fail. Warn loudly + skip render so the bug class is
|
|
108
|
+
// diagnosable in dev. See USAGE.md "Reactive binding" for the
|
|
109
|
+
// documented patterns.
|
|
110
|
+
if (typeof this.value === 'function') {
|
|
111
|
+
// eslint-disable-next-line no-console
|
|
112
|
+
console.warn(
|
|
113
|
+
'[slider-ui] .value received a function. Did you mean:\n' +
|
|
114
|
+
' .value=${fn()} ← call the function to get the current value\n' +
|
|
115
|
+
' .value=${signal.value} ← read the signal\'s current value\n' +
|
|
116
|
+
'Functions are not auto-invoked at render time; the slider reads\n' +
|
|
117
|
+
'.value synchronously. See USAGE.md "Reactive binding" for the\n' +
|
|
118
|
+
'parent-template-re-read pattern that works across frameworks.'
|
|
119
|
+
);
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
|
|
98
123
|
const pct = this.#pct;
|
|
99
124
|
const fill = this.querySelector('[slot="fill"]');
|
|
100
125
|
if (fill) fill.style.width = `${pct}%`;
|
|
@@ -91,6 +91,12 @@ function csvEscape(val) {
|
|
|
91
91
|
// ── Component ────────────────────────────────────────────────────────────────
|
|
92
92
|
|
|
93
93
|
export class UITable extends UIElement {
|
|
94
|
+
// §154 (v0.5.3): Phosphor icons this primitive auto-stamps (without
|
|
95
|
+
// consumer markup). Aggregated by installIconLoadersForRegistered()
|
|
96
|
+
// across all defined elements. Audited by check-required-icons.mjs
|
|
97
|
+
// (slot 11). Per FEEDBACK-06 §4 + FEEDBACK-07 §4.
|
|
98
|
+
static requiredIcons = ['caret-right', 'caret-up-down', 'table'];
|
|
99
|
+
|
|
94
100
|
static properties = {
|
|
95
101
|
sortable: { type: Boolean, default: false, reflect: true },
|
|
96
102
|
selectable: { type: Boolean, default: false, reflect: true },
|
|
@@ -143,6 +143,19 @@
|
|
|
143
143
|
}
|
|
144
144
|
}
|
|
145
145
|
},
|
|
146
|
+
"row-collapse": {
|
|
147
|
+
"description": "Fired when an already-expanded row's chevron is activated (collapses the row). Mirror of row-expand.",
|
|
148
|
+
"detail": {
|
|
149
|
+
"index": {
|
|
150
|
+
"description": "Row index in the underlying data array.",
|
|
151
|
+
"type": "number"
|
|
152
|
+
},
|
|
153
|
+
"row": {
|
|
154
|
+
"description": "Row data object.",
|
|
155
|
+
"type": "object"
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
},
|
|
146
159
|
"row-expand": {
|
|
147
160
|
"description": "Fired when an expandable row's chevron is activated. detail.index is the row position; detail.row is the row data.",
|
|
148
161
|
"detail": {
|
|
@@ -44,6 +44,14 @@ export interface TableResizeEventDetail {
|
|
|
44
44
|
}
|
|
45
45
|
|
|
46
46
|
export type TableResizeEvent = CustomEvent<TableResizeEventDetail>;
|
|
47
|
+
export interface TableRowCollapseEventDetail {
|
|
48
|
+
/** Row index in the underlying data array. */
|
|
49
|
+
index: number;
|
|
50
|
+
/** Row data object. */
|
|
51
|
+
row: Record<string, unknown>;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export type TableRowCollapseEvent = CustomEvent<TableRowCollapseEventDetail>;
|
|
47
55
|
export interface TableRowExpandEventDetail {
|
|
48
56
|
/** Row index in the underlying data array. */
|
|
49
57
|
index: number;
|
|
@@ -102,6 +110,7 @@ export class UITable extends UIElement {
|
|
|
102
110
|
addEventListener(type: 'filter-change', listener: (ev: TableFilterChangeEvent) => unknown, options?: boolean | AddEventListenerOptions): void;
|
|
103
111
|
addEventListener(type: 'page', listener: (ev: TablePageEvent) => unknown, options?: boolean | AddEventListenerOptions): void;
|
|
104
112
|
addEventListener(type: 'resize', listener: (ev: TableResizeEvent) => unknown, options?: boolean | AddEventListenerOptions): void;
|
|
113
|
+
addEventListener(type: 'row-collapse', listener: (ev: TableRowCollapseEvent) => unknown, options?: boolean | AddEventListenerOptions): void;
|
|
105
114
|
addEventListener(type: 'row-expand', listener: (ev: TableRowExpandEvent) => unknown, options?: boolean | AddEventListenerOptions): void;
|
|
106
115
|
addEventListener(type: 'select', listener: (ev: TableSelectEvent) => unknown, options?: boolean | AddEventListenerOptions): void;
|
|
107
116
|
addEventListener(type: 'sort', listener: (ev: TableSortEvent) => unknown, options?: boolean | AddEventListenerOptions): void;
|
|
@@ -108,6 +108,15 @@ events:
|
|
|
108
108
|
width:
|
|
109
109
|
type: number
|
|
110
110
|
description: New column width in pixels.
|
|
111
|
+
row-collapse:
|
|
112
|
+
description: Fired when an already-expanded row's chevron is activated (collapses the row). Mirror of row-expand.
|
|
113
|
+
detail:
|
|
114
|
+
index:
|
|
115
|
+
type: number
|
|
116
|
+
description: Row index in the underlying data array.
|
|
117
|
+
row:
|
|
118
|
+
type: object
|
|
119
|
+
description: Row data object.
|
|
111
120
|
row-expand:
|
|
112
121
|
description: Fired when an expandable row's chevron is activated. detail.index is the row position; detail.row is the row data.
|
|
113
122
|
detail:
|
|
@@ -187,6 +196,10 @@ tokens:
|
|
|
187
196
|
description: Sort arrow color
|
|
188
197
|
--table-transition:
|
|
189
198
|
description: Transition timing
|
|
199
|
+
requiredIcons:
|
|
200
|
+
- caret-right
|
|
201
|
+
- caret-up-down
|
|
202
|
+
- table
|
|
190
203
|
a2ui:
|
|
191
204
|
rules: []
|
|
192
205
|
anti_patterns: []
|
package/components/tag/class.js
CHANGED
|
@@ -27,6 +27,12 @@
|
|
|
27
27
|
import { UIElement } from '../../core/element.js';
|
|
28
28
|
|
|
29
29
|
export class UITag extends UIElement {
|
|
30
|
+
// §154 (v0.5.3): Phosphor icons this primitive auto-stamps (without
|
|
31
|
+
// consumer markup). Aggregated by installIconLoadersForRegistered()
|
|
32
|
+
// across all defined elements. Audited by check-required-icons.mjs
|
|
33
|
+
// (slot 11). Per FEEDBACK-06 §4 + FEEDBACK-07 §4.
|
|
34
|
+
static requiredIcons = ['x'];
|
|
35
|
+
|
|
30
36
|
static properties = {
|
|
31
37
|
text: { type: String, default: '', reflect: true },
|
|
32
38
|
textContent: { type: String, default: '' },
|
package/components/tag/tag.yaml
CHANGED
package/components/tree/class.js
CHANGED
|
@@ -35,6 +35,12 @@
|
|
|
35
35
|
import { UIElement } from '../../core/element.js';
|
|
36
36
|
|
|
37
37
|
export class UITree extends UIElement {
|
|
38
|
+
// §154 (v0.5.3): Phosphor icons this primitive auto-stamps (without
|
|
39
|
+
// consumer markup). Aggregated by installIconLoadersForRegistered()
|
|
40
|
+
// across all defined elements. Audited by check-required-icons.mjs
|
|
41
|
+
// (slot 11). Per FEEDBACK-06 §4 + FEEDBACK-07 §4.
|
|
42
|
+
static requiredIcons = ['caret-right'];
|
|
43
|
+
|
|
38
44
|
static template = () => null;
|
|
39
45
|
|
|
40
46
|
connected() {
|
package/core/icons.js
CHANGED
|
@@ -140,6 +140,104 @@ export function installIconLoaders(modules) {
|
|
|
140
140
|
resolveRegistryReady();
|
|
141
141
|
}
|
|
142
142
|
|
|
143
|
+
/**
|
|
144
|
+
* Aggregate `static requiredIcons` declarations from all currently-defined
|
|
145
|
+
* `customElements`, install loaders for the union, and return the list of
|
|
146
|
+
* discovered icon names. Replaces the trial-and-error icon-glob authoring
|
|
147
|
+
* loop FEEDBACK-06 §4 + FEEDBACK-07 §4 reported.
|
|
148
|
+
*
|
|
149
|
+
* Each AdiaUI primitive that auto-stamps icons declares them via:
|
|
150
|
+
*
|
|
151
|
+
* export class UISelect extends UIFormElement {
|
|
152
|
+
* static requiredIcons = ['caret-up-down'];
|
|
153
|
+
* // ...
|
|
154
|
+
* }
|
|
155
|
+
*
|
|
156
|
+
* Consumer usage:
|
|
157
|
+
*
|
|
158
|
+
* import '@adia-ai/web-components'; // side-effect — defines all primitives
|
|
159
|
+
* import { installIconLoadersForRegistered } from '@adia-ai/web-components/core/icons';
|
|
160
|
+
*
|
|
161
|
+
* // Build the Phosphor loader map from your bundler (Vite, Webpack, etc.).
|
|
162
|
+
* const modules = import.meta.glob('/node_modules/@phosphor-icons/core/assets/regular/*.svg', { query: '?raw' });
|
|
163
|
+
*
|
|
164
|
+
* // Aggregate requiredIcons across all defined primitives + install.
|
|
165
|
+
* const discovered = installIconLoadersForRegistered(modules);
|
|
166
|
+
*
|
|
167
|
+
* Or, to limit the icon set to ONLY what's needed (smaller bundle):
|
|
168
|
+
*
|
|
169
|
+
* const discovered = installIconLoadersForRegistered.discover();
|
|
170
|
+
* // discovered: ['caret-up-down', 'x', 'caret-right', ...]
|
|
171
|
+
* // Now build a narrower glob from this list.
|
|
172
|
+
*
|
|
173
|
+
* Added v0.5.3 §154 per FEEDBACK-06 §4 + FEEDBACK-07 §4.
|
|
174
|
+
*
|
|
175
|
+
* @param {Record<string, () => Promise<string>>} modules - Phosphor loader map
|
|
176
|
+
* @returns {string[]} The union of all primitives' `requiredIcons`
|
|
177
|
+
*/
|
|
178
|
+
export function installIconLoadersForRegistered(modules) {
|
|
179
|
+
const discovered = discoverRequiredIcons();
|
|
180
|
+
installIconLoaders(modules);
|
|
181
|
+
return discovered;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Walk customElements and collect the union of `static requiredIcons` across
|
|
186
|
+
* all defined primitives. Useful for building a Vite glob brace-list that
|
|
187
|
+
* matches exactly what the registered primitives need.
|
|
188
|
+
*
|
|
189
|
+
* Returns an empty array if `customElements` is unavailable (Node SSR)
|
|
190
|
+
* or if no defined primitive exposes `requiredIcons`.
|
|
191
|
+
*/
|
|
192
|
+
function discoverRequiredIcons() {
|
|
193
|
+
const union = new Set();
|
|
194
|
+
if (typeof customElements === 'undefined') return [];
|
|
195
|
+
// customElements has no Object.entries-style iterator in the standard API.
|
|
196
|
+
// We need to track which tag names have been defined. The AdiaUI convention:
|
|
197
|
+
// every primitive's tag ends in `-ui` (or `-n` per yaml `tag:` pattern). We
|
|
198
|
+
// walk a known list of tag names — passed via opt-in. For SSR safety the
|
|
199
|
+
// consumer can also pass `tags` explicitly:
|
|
200
|
+
// discoverRequiredIcons(['select-ui', 'tag-ui', ...]).
|
|
201
|
+
//
|
|
202
|
+
// For browser usage with the default barrel import, the consumer's
|
|
203
|
+
// `import '@adia-ai/web-components'` defines all 93+ primitives at the
|
|
204
|
+
// global custom-element registry; we walk the DOM-known tag names by
|
|
205
|
+
// checking customElements.get() on the AdiaUI prefix convention.
|
|
206
|
+
//
|
|
207
|
+
// Strategy: query all defined tags by walking the document's primitive
|
|
208
|
+
// surface OR accept an explicit tag list. The simpler API: iterate over
|
|
209
|
+
// a known whitelist of tag names. We embed the whitelist for known
|
|
210
|
+
// auto-stamping primitives.
|
|
211
|
+
const KNOWN_AUTO_STAMPING_TAGS = [
|
|
212
|
+
'accordion-ui',
|
|
213
|
+
'agent-questions-ui',
|
|
214
|
+
'agent-reasoning-ui',
|
|
215
|
+
'agent-trace-ui',
|
|
216
|
+
'calendar-picker-ui',
|
|
217
|
+
'color-picker-ui',
|
|
218
|
+
'command-ui',
|
|
219
|
+
'pane-ui',
|
|
220
|
+
'select-ui',
|
|
221
|
+
'table-ui',
|
|
222
|
+
'tag-ui',
|
|
223
|
+
'tree-ui',
|
|
224
|
+
];
|
|
225
|
+
for (const tag of KNOWN_AUTO_STAMPING_TAGS) {
|
|
226
|
+
const ctor = customElements.get(tag);
|
|
227
|
+
if (ctor && Array.isArray(ctor.requiredIcons)) {
|
|
228
|
+
for (const name of ctor.requiredIcons) union.add(name);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
return [...union];
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Public discovery — same shape as installIconLoadersForRegistered but
|
|
236
|
+
* doesn't install. Returns the union of `static requiredIcons` across
|
|
237
|
+
* all currently-defined auto-stamping AdiaUI primitives.
|
|
238
|
+
*/
|
|
239
|
+
installIconLoadersForRegistered.discover = discoverRequiredIcons;
|
|
240
|
+
|
|
143
241
|
/* Track which (name, weight) pairs we've already warned about so the
|
|
144
242
|
console stays readable even when a missing icon is used many times on
|
|
145
243
|
a page (e.g. a broken Phosphor name in a list row repeated 50×). */
|
|
@@ -168,17 +266,62 @@ function filenameFor(name, weight) {
|
|
|
168
266
|
return `${name}-${weight}.svg`;
|
|
169
267
|
}
|
|
170
268
|
|
|
269
|
+
/* Resolve a loader by name + weight, tolerant of path-prefix variation.
|
|
270
|
+
*
|
|
271
|
+
* Two-step lookup, fast-path first:
|
|
272
|
+
*
|
|
273
|
+
* 1. **Fast path**: try the canonical `/node_modules/@phosphor-icons/core/
|
|
274
|
+
* assets/<weight>/<filename>` key directly. This is what `icons-phosphor.js`
|
|
275
|
+
* installs when Vite's project root sits above an npm-style node_modules,
|
|
276
|
+
* and what the chat-ui dev convention assumes.
|
|
277
|
+
*
|
|
278
|
+
* 2. **Suffix fallback**: if the fast path misses, scan `modules` keys for
|
|
279
|
+
* one ending with `/<filename>`. Handles:
|
|
280
|
+
* - pnpm: `/node_modules/.pnpm/@phosphor-icons+core@.../node_modules/.../assets/...`
|
|
281
|
+
* - yarn berry / PnP: virtual paths like `/.yarn/cache/...`
|
|
282
|
+
* - custom Vite root: `/packages/my-app/node_modules/...`
|
|
283
|
+
* - brace-list globs with absolute paths
|
|
284
|
+
* - manually-built loader maps with arbitrary keys
|
|
285
|
+
*
|
|
286
|
+
* The fast path stays O(1); the fallback is O(N) over the weight's module
|
|
287
|
+
* map but only runs once per (name, weight) miss (results aren't cached, but
|
|
288
|
+
* misses fall through to a registry hit via `loadIcon`'s `registry.set`).
|
|
289
|
+
*
|
|
290
|
+
* Same two-step shape applies to the `-fill` back-compat name suffix.
|
|
291
|
+
*
|
|
292
|
+
* Fix history: pre-§140 the function was fast-path-only. FEEDBACK-06 from
|
|
293
|
+
* color-app reported `installIconLoaders` → `warnMissingIcon` firing for
|
|
294
|
+
* `caret-right` / `x` / `caret-up-down` even though the consumer had installed
|
|
295
|
+
* a loader map. Root cause: color-app's `modules` keys didn't match the
|
|
296
|
+
* canonical `/node_modules/...` prefix (different package-manager or Vite
|
|
297
|
+
* root). The suffix fallback closes that gap without changing the public API.
|
|
298
|
+
*/
|
|
171
299
|
function resolveLoader(name, weight = DEFAULT_WEIGHT) {
|
|
172
300
|
const modules = weightModules[weight];
|
|
173
301
|
if (!modules) return null;
|
|
174
|
-
const
|
|
175
|
-
|
|
302
|
+
const filename = filenameFor(name, weight);
|
|
303
|
+
|
|
304
|
+
// 1. Fast path — canonical phosphor npm-style path.
|
|
305
|
+
const fastKey = `/node_modules/@phosphor-icons/core/assets/${weight}/${filename}`;
|
|
306
|
+
if (modules[fastKey]) return modules[fastKey];
|
|
307
|
+
|
|
308
|
+
// 2. Suffix fallback — match any key ending with the filename.
|
|
309
|
+
const suffix = `/${filename}`;
|
|
310
|
+
for (const key in modules) {
|
|
311
|
+
if (key.endsWith(suffix)) return modules[key];
|
|
312
|
+
}
|
|
176
313
|
|
|
177
314
|
// Back-compat: allow the caller to put the -fill suffix directly in the
|
|
178
|
-
// name (e.g. `name="circle-fill"`) without specifying weight.
|
|
315
|
+
// name (e.g. `name="circle-fill"`) without specifying weight. Two-step
|
|
316
|
+
// lookup applies here too.
|
|
179
317
|
if (weight === DEFAULT_WEIGHT && name.endsWith('-fill')) {
|
|
180
|
-
const
|
|
181
|
-
|
|
318
|
+
const fillFilename = `${name}.svg`;
|
|
319
|
+
const fillFastKey = `/node_modules/@phosphor-icons/core/assets/fill/${fillFilename}`;
|
|
320
|
+
if (weightModules.fill[fillFastKey]) return weightModules.fill[fillFastKey];
|
|
321
|
+
const fillSuffix = `/${fillFilename}`;
|
|
322
|
+
for (const key in weightModules.fill) {
|
|
323
|
+
if (key.endsWith(fillSuffix)) return weightModules.fill[key];
|
|
324
|
+
}
|
|
182
325
|
}
|
|
183
326
|
return null;
|
|
184
327
|
}
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach } from 'vitest';
|
|
2
|
+
import {
|
|
3
|
+
installIconLoaders,
|
|
4
|
+
registerIcon,
|
|
5
|
+
hasIcon,
|
|
6
|
+
isIconName,
|
|
7
|
+
getIcon,
|
|
8
|
+
listIcons,
|
|
9
|
+
} from './icons.js';
|
|
10
|
+
|
|
11
|
+
// Tests for `installIconLoaders` + `resolveLoader` + the §140 suffix-
|
|
12
|
+
// fallback fix for FEEDBACK-06 (color-app's `installIconLoaders` →
|
|
13
|
+
// `warnMissingIcon` failures on `caret-right` / `x` / `caret-up-down`
|
|
14
|
+
// when the consumer's `modules` keys didn't match the canonical
|
|
15
|
+
// `/node_modules/@phosphor-icons/core/assets/<weight>/<name>.svg` path.
|
|
16
|
+
//
|
|
17
|
+
// `isIconName` is the SYNC probe — it calls `resolveLoader` without
|
|
18
|
+
// triggering an async load. That's what we need to assert the fallback
|
|
19
|
+
// logic finds the loader regardless of path shape.
|
|
20
|
+
|
|
21
|
+
const CARET_RIGHT_SVG = '<svg data-icon="caret-right"/>';
|
|
22
|
+
const X_SVG = '<svg data-icon="x"/>';
|
|
23
|
+
const CARET_UP_DOWN_SVG = '<svg data-icon="caret-up-down"/>';
|
|
24
|
+
|
|
25
|
+
describe('installIconLoaders + resolveLoader (§140 suffix fallback)', () => {
|
|
26
|
+
beforeEach(() => {
|
|
27
|
+
// Reset to a known state. Note: the registry Map itself is module-
|
|
28
|
+
// singleton + doesn't reset between tests, so we keep test SVG
|
|
29
|
+
// contents distinct from any real phosphor names just in case.
|
|
30
|
+
installIconLoaders({ regular: {}, thin: {}, light: {}, bold: {}, fill: {}, duotone: {} });
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it('resolves by canonical phosphor path (fast path)', () => {
|
|
34
|
+
installIconLoaders({
|
|
35
|
+
regular: {
|
|
36
|
+
'/node_modules/@phosphor-icons/core/assets/regular/caret-right.svg': CARET_RIGHT_SVG,
|
|
37
|
+
'/node_modules/@phosphor-icons/core/assets/regular/x.svg': X_SVG,
|
|
38
|
+
},
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
expect(isIconName('caret-right')).toBe(true);
|
|
42
|
+
expect(isIconName('x')).toBe(true);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it('resolves by filename suffix when key has a pnpm-style prefix', () => {
|
|
46
|
+
// pnpm hoists phosphor inside `.pnpm/<pkg>@<ver>/node_modules/...`
|
|
47
|
+
installIconLoaders({
|
|
48
|
+
regular: {
|
|
49
|
+
'/node_modules/.pnpm/@phosphor-icons+core@2.1.1/node_modules/@phosphor-icons/core/assets/regular/caret-right.svg':
|
|
50
|
+
CARET_RIGHT_SVG,
|
|
51
|
+
},
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
expect(isIconName('caret-right')).toBe(true);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it('resolves by filename suffix when key is a custom Vite-root absolute path', () => {
|
|
58
|
+
// Custom Vite root puts node_modules at a deeper location.
|
|
59
|
+
installIconLoaders({
|
|
60
|
+
regular: {
|
|
61
|
+
'/packages/my-app/node_modules/@phosphor-icons/core/assets/regular/caret-up-down.svg':
|
|
62
|
+
CARET_UP_DOWN_SVG,
|
|
63
|
+
},
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
expect(isIconName('caret-up-down')).toBe(true);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it('resolves by filename suffix for an arbitrary manually-built loader map', () => {
|
|
70
|
+
// Manual loader maps may have any keys at all — only the filename matters.
|
|
71
|
+
installIconLoaders({
|
|
72
|
+
regular: {
|
|
73
|
+
'/anywhere/at/all/caret-right.svg': CARET_RIGHT_SVG,
|
|
74
|
+
'/another/path/entirely/x.svg': X_SVG,
|
|
75
|
+
},
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
expect(isIconName('caret-right')).toBe(true);
|
|
79
|
+
expect(isIconName('x')).toBe(true);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it('returns false on miss across both paths', () => {
|
|
83
|
+
installIconLoaders({
|
|
84
|
+
regular: {
|
|
85
|
+
'/node_modules/@phosphor-icons/core/assets/regular/caret-right.svg': CARET_RIGHT_SVG,
|
|
86
|
+
},
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
expect(isIconName('not-a-real-icon-name')).toBe(false);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it('resolves `-fill` back-compat suffix in name via fast path', () => {
|
|
93
|
+
const STAR_FILL_SVG = '<svg data-icon="star-fill"/>';
|
|
94
|
+
installIconLoaders({
|
|
95
|
+
regular: {},
|
|
96
|
+
fill: {
|
|
97
|
+
'/node_modules/@phosphor-icons/core/assets/fill/star-fill.svg': STAR_FILL_SVG,
|
|
98
|
+
},
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
// `name="star-fill"` with default weight=regular falls back to fill weight.
|
|
102
|
+
expect(isIconName('star-fill')).toBe(true);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it('resolves `-fill` back-compat suffix in name via suffix fallback', () => {
|
|
106
|
+
const STAR_FILL_SVG = '<svg data-icon="star-fill"/>';
|
|
107
|
+
installIconLoaders({
|
|
108
|
+
regular: {},
|
|
109
|
+
fill: {
|
|
110
|
+
'/anywhere/star-fill.svg': STAR_FILL_SVG,
|
|
111
|
+
},
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
expect(isIconName('star-fill')).toBe(true);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it('resolves non-regular weights (bold, fill, etc.) via suffix fallback', () => {
|
|
118
|
+
const HEART_BOLD_SVG = '<svg data-icon="heart-bold"/>';
|
|
119
|
+
installIconLoaders({
|
|
120
|
+
regular: {},
|
|
121
|
+
bold: {
|
|
122
|
+
'/anywhere/heart-bold.svg': HEART_BOLD_SVG,
|
|
123
|
+
},
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
expect(isIconName('heart', 'bold')).toBe(true);
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it('isIconName returns true via alias resolution + suffix fallback', () => {
|
|
130
|
+
// `send` → `paper-plane-right` in ICON_ALIASES. Suffix fallback should
|
|
131
|
+
// still match the aliased filename.
|
|
132
|
+
const PAPER_PLANE_SVG = '<svg data-icon="paper-plane-right"/>';
|
|
133
|
+
installIconLoaders({
|
|
134
|
+
regular: {
|
|
135
|
+
'/anywhere/paper-plane-right.svg': PAPER_PLANE_SVG,
|
|
136
|
+
},
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
expect(isIconName('send')).toBe(true);
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it('end-to-end via DOM re-stamp: installIconLoaders auto-loads existing <icon-ui[name]>', async () => {
|
|
143
|
+
// The §140 fix's primary symptom: `installIconLoaders` walks existing
|
|
144
|
+
// `<icon-ui[name]>` elements + calls `loadIcon` for each. Without the
|
|
145
|
+
// suffix fallback, `loadIcon` → `resolveLoader` returns null →
|
|
146
|
+
// `warnMissingIcon` fires. With the fix, the load completes.
|
|
147
|
+
const PAW_SVG = '<svg data-icon="paw"/>';
|
|
148
|
+
|
|
149
|
+
// Plant an `<icon-ui[name="paw"]>` element. We don't need it to
|
|
150
|
+
// actually upgrade; we just need `querySelectorAll('icon-ui[name]')`
|
|
151
|
+
// to find it.
|
|
152
|
+
const el = document.createElement('icon-ui');
|
|
153
|
+
el.setAttribute('name', 'paw');
|
|
154
|
+
document.body.appendChild(el);
|
|
155
|
+
|
|
156
|
+
// Install a loader map with a pnpm-style path (suffix fallback exercise).
|
|
157
|
+
installIconLoaders({
|
|
158
|
+
regular: {
|
|
159
|
+
'/some/non-canonical/prefix/paw.svg': PAW_SVG,
|
|
160
|
+
},
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
// `loadIcon` runs sync (loader is a string, not a function), so the
|
|
164
|
+
// registry is written by the time `installIconLoaders` returns.
|
|
165
|
+
expect(hasIcon('paw')).toBe(true);
|
|
166
|
+
|
|
167
|
+
document.body.removeChild(el);
|
|
168
|
+
});
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
describe('registerIcon / hasIcon — unchanged by §140', () => {
|
|
172
|
+
it('registerIcon adds an icon by name without needing a loader map', () => {
|
|
173
|
+
const CUSTOM_SVG = '<svg data-icon="custom-§140-marker"/>';
|
|
174
|
+
registerIcon('my-§140-custom-icon', CUSTOM_SVG);
|
|
175
|
+
|
|
176
|
+
expect(hasIcon('my-§140-custom-icon')).toBe(true);
|
|
177
|
+
expect(getIcon('my-§140-custom-icon')).toBe(CUSTOM_SVG);
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
it('listIcons returns registered names', () => {
|
|
181
|
+
registerIcon('§140-listIcons-marker-one', '<svg/>');
|
|
182
|
+
registerIcon('§140-listIcons-marker-two', '<svg/>');
|
|
183
|
+
|
|
184
|
+
expect(listIcons().some((k) => k.includes('§140-listIcons-marker-one'))).toBe(true);
|
|
185
|
+
expect(listIcons().some((k) => k.includes('§140-listIcons-marker-two'))).toBe(true);
|
|
186
|
+
});
|
|
187
|
+
});
|
package/core/template.js
CHANGED
|
@@ -31,15 +31,48 @@ export function css(strings, ...values) {
|
|
|
31
31
|
|
|
32
32
|
const templateCache = new WeakMap();
|
|
33
33
|
|
|
34
|
+
// §155 (v0.5.3): walks the accumulated static-string chunk to decide
|
|
35
|
+
// whether each ${…} placeholder is in tag-position (substitute {{p:N}})
|
|
36
|
+
// or content-position (substitute <!--p:N-->). The walker tracks `<`,
|
|
37
|
+
// `>`, `'`, `"` for that decision.
|
|
38
|
+
//
|
|
39
|
+
// FEEDBACK-06 §1 P0: HTML comments must be treated as opaque. An
|
|
40
|
+
// apostrophe inside `<!-- foo's bar -->` previously put `q = 1`
|
|
41
|
+
// (single-quoted-string state); the `-->` closer was then treated as
|
|
42
|
+
// string content; all subsequent `<` / `>` chars were masked. Result:
|
|
43
|
+
// every placeholder after the comment got mis-classified, producing
|
|
44
|
+
// `<segmented-ui .value=<!--p:10--> …>` (which the DOM parser eats),
|
|
45
|
+
// `scan()` finds no part registered, and `update()` falls through
|
|
46
|
+
// `isFn` branch — invoking the consumer's arrow handler with no
|
|
47
|
+
// arguments → "Cannot read properties of undefined (reading 'detail')"
|
|
48
|
+
// crash at mount with a stack trace pointing at user code.
|
|
49
|
+
//
|
|
50
|
+
// Fix: skip HTML comments wholesale before the quote/tag walker runs.
|
|
51
|
+
// Comments cannot open a tag and cannot contain real string delimiters
|
|
52
|
+
// as far as the live HTML parser is concerned. Unterminated comments
|
|
53
|
+
// fall through to `last = 0` (content-position) which is the safe
|
|
54
|
+
// default.
|
|
34
55
|
function inTag(m) {
|
|
35
|
-
let q = 0, last = 0;
|
|
36
|
-
|
|
56
|
+
let q = 0, last = 0, i = 0;
|
|
57
|
+
while (i < m.length) {
|
|
58
|
+
if (q === 0 &&
|
|
59
|
+
m.charCodeAt(i) === 60 /* < */ &&
|
|
60
|
+
m.charCodeAt(i + 1) === 33 /* ! */ &&
|
|
61
|
+
m.charCodeAt(i + 2) === 45 /* - */ &&
|
|
62
|
+
m.charCodeAt(i + 3) === 45 /* - */) {
|
|
63
|
+
const end = m.indexOf('-->', i + 4);
|
|
64
|
+
if (end === -1) { last = 0; break; }
|
|
65
|
+
i = end + 3;
|
|
66
|
+
last = 0;
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
37
69
|
const c = m.charCodeAt(i);
|
|
38
70
|
if (q) { if (c === (q === 1 ? 39 : 34)) q = 0; }
|
|
39
71
|
else if (c === 39) q = 1;
|
|
40
72
|
else if (c === 34) q = 2;
|
|
41
73
|
else if (c === 60) last = 1;
|
|
42
74
|
else if (c === 62) last = 0;
|
|
75
|
+
i++;
|
|
43
76
|
}
|
|
44
77
|
return last === 1;
|
|
45
78
|
}
|
|
@@ -93,7 +126,30 @@ function scan(fragment, count) {
|
|
|
93
126
|
} else if (n.nodeType === 1) {
|
|
94
127
|
for (const attr of [...n.attributes]) {
|
|
95
128
|
const m = attr.value.match(/^\{\{p:(\d+)\}\}$/);
|
|
96
|
-
if (!m)
|
|
129
|
+
if (!m) {
|
|
130
|
+
// §152 (v0.5.3): partial-attribute interpolation runtime guard.
|
|
131
|
+
// The attribute-part contract is whole-value-only (one placeholder
|
|
132
|
+
// per attribute, full replacement). Partial like `style="bg:${a};
|
|
133
|
+
// color:${b}"` produces post-concat `bg:{{p:0}}; color:{{p:1}}`
|
|
134
|
+
// which fails the ^…$ regex above. Without this guard the literal
|
|
135
|
+
// {{p:N}} text reaches the DOM as the attribute's static value —
|
|
136
|
+
// silent-garbage class. Warn loudly so consumers don't ship the
|
|
137
|
+
// bug. Hot template paths fire many times, so warn (not throw):
|
|
138
|
+
// a thrown error would crash render; warn surfaces the bug class.
|
|
139
|
+
if (attr.value.includes('{{p:')) {
|
|
140
|
+
// eslint-disable-next-line no-console
|
|
141
|
+
console.warn(
|
|
142
|
+
`[template] Partial attribute interpolation is not supported.\n` +
|
|
143
|
+
` Element: <${n.tagName.toLowerCase()}>\n` +
|
|
144
|
+
` Attribute: ${attr.name}="${attr.value.slice(0, 80)}${attr.value.length > 80 ? '…' : ''}"\n` +
|
|
145
|
+
` Use one of:\n` +
|
|
146
|
+
` ${attr.name}="\${expression}" ← full replacement (whole attr is the placeholder)\n` +
|
|
147
|
+
` .${attr.name}=\${expression} ← property assignment (preferred for objects/functions)\n` +
|
|
148
|
+
` Compute the full attr value as a single expression and interpolate the whole.`
|
|
149
|
+
);
|
|
150
|
+
}
|
|
151
|
+
continue;
|
|
152
|
+
}
|
|
97
153
|
const i = +m[1], name = attr.name;
|
|
98
154
|
if (name[0] === '@') {
|
|
99
155
|
n.removeAttribute(name);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@adia-ai/web-components",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.3",
|
|
4
4
|
"description": "AdiaUI web components — vanilla custom elements. A2UI runtime (renderer, registry, streams, wiring) lives in @adia-ai/a2ui-runtime.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"types": "./index.d.ts",
|
|
@@ -58,7 +58,21 @@
|
|
|
58
58
|
"./core/icons-phosphor.js"
|
|
59
59
|
],
|
|
60
60
|
"dependencies": {
|
|
61
|
-
"@adia-ai/a2ui-runtime": "^0.5.0"
|
|
61
|
+
"@adia-ai/a2ui-runtime": "^0.5.0",
|
|
62
|
+
"@codemirror/commands": "^6.10.3",
|
|
63
|
+
"@codemirror/language": "^6.12.3",
|
|
64
|
+
"@codemirror/lint": "^6.9.5",
|
|
65
|
+
"@codemirror/state": "^6.6.0",
|
|
66
|
+
"@codemirror/view": "^6.41.1",
|
|
67
|
+
"@lezer/highlight": "^1.2.3"
|
|
68
|
+
},
|
|
69
|
+
"optionalDependencies": {
|
|
70
|
+
"@codemirror/lang-css": "^6.3.1",
|
|
71
|
+
"@codemirror/lang-html": "^6.4.11",
|
|
72
|
+
"@codemirror/lang-javascript": "^6.2.5",
|
|
73
|
+
"@codemirror/lang-json": "^6.0.2",
|
|
74
|
+
"@codemirror/lang-markdown": "^6.5.0",
|
|
75
|
+
"@codemirror/lang-yaml": "^6.1.3"
|
|
62
76
|
},
|
|
63
77
|
"publishConfig": {
|
|
64
78
|
"access": "public",
|