@adia-ai/web-components 0.7.23 → 0.7.25
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 +56 -25
- package/README.md +2 -2
- package/USAGE.md +60 -15
- package/components/accordion/accordion-item.a2ui.json +30 -1
- package/components/accordion/accordion-item.yaml +33 -0
- package/components/accordion/accordion.a2ui.json +9 -0
- package/components/accordion/accordion.class.js +53 -3
- package/components/accordion/accordion.css +61 -3
- package/components/accordion/accordion.d.ts +12 -0
- package/components/accordion/accordion.yaml +17 -0
- package/components/code/code.class.js +1 -1
- package/components/display-field/display-field.a2ui.json +185 -0
- package/components/display-field/display-field.css +91 -0
- package/components/display-field/display-field.d.ts +26 -0
- package/components/display-field/display-field.examples.md +44 -0
- package/components/display-field/display-field.js +141 -0
- package/components/display-field/display-field.test.js +115 -0
- package/components/display-field/display-field.yaml +191 -0
- package/components/drawer/drawer.class.js +2 -2
- package/components/feed/feed.a2ui.json +1 -1
- package/components/feed/feed.class.js +1 -1
- package/components/feed/feed.d.ts +1 -1
- package/components/feed/feed.yaml +2 -2
- package/components/field/field.class.js +1 -1
- package/components/icon/icon.class.js +34 -1
- package/components/index.js +1 -0
- package/components/loading-overlay/loading-overlay.class.js +1 -1
- package/components/loading-overlay/loading-overlay.yaml +1 -1
- package/components/modal/modal.class.js +1 -1
- package/components/nav/nav.class.js +1 -1
- package/components/nav/nav.examples.md +4 -4
- package/components/popover/popover.class.js +1 -1
- package/components/spinner/spinner.class.js +1 -1
- package/components/spinner/spinner.yaml +1 -1
- package/components/table/table.class.js +42 -11
- package/components/table/table.test.js +53 -0
- package/components/toast/toast.class.js +1 -1
- package/core/icons.js +14 -0
- package/custom-elements.json +5723 -2865
- package/dist/theme-provider.min.js +1 -1
- package/dist/web-components.min.css +1 -1
- package/dist/web-components.min.js +87 -79
- package/dist/web-components.sheet.js +1 -1
- package/package.json +3 -2
- package/styles/components.css +1 -0
- package/traits/CATEGORIES.md +1 -1
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* <display-field-ui label="Card on file" value="•••• 4242" variant="masked" icon="lock" hint="Expires 12/26"></display-field-ui>
|
|
3
|
+
*
|
|
4
|
+
* Display-only labeled value — a field-shaped read-out for a static or
|
|
5
|
+
* masked value, or a labeled slot that hosts a third-party element (a
|
|
6
|
+
* Stripe Element iframe) under a proper caption.
|
|
7
|
+
*
|
|
8
|
+
* Unlike <field-ui> it wraps NO interactive control: no focus, no
|
|
9
|
+
* contenteditable, no form participation, no [for]/id-minting. The label
|
|
10
|
+
* names the whole field for assistive tech via role="group" +
|
|
11
|
+
* aria-labelledby, so a slotted foreign element is announced with its
|
|
12
|
+
* caption.
|
|
13
|
+
*
|
|
14
|
+
* Value comes from EITHER the `value` attribute OR unnamed default-slot
|
|
15
|
+
* children (the foreign-element path) — slot content wins when both are
|
|
16
|
+
* present.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import { UIElement } from '../../core/element.js';
|
|
20
|
+
import { defineIfFree } from '../../core/register.js';
|
|
21
|
+
|
|
22
|
+
// Monotonic id source for label/hint association (aria-labelledby /
|
|
23
|
+
// aria-describedby need stable ids; multiple instances must not collide).
|
|
24
|
+
let uid = 0;
|
|
25
|
+
|
|
26
|
+
class UIDisplayField extends UIElement {
|
|
27
|
+
static properties = {
|
|
28
|
+
label: { type: String, default: '', reflect: true },
|
|
29
|
+
value: { type: String, default: '', reflect: true },
|
|
30
|
+
variant: { type: String, default: 'default', reflect: true },
|
|
31
|
+
hint: { type: String, default: '', reflect: true },
|
|
32
|
+
icon: { type: String, default: '', reflect: true },
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
static template = () => null;
|
|
36
|
+
|
|
37
|
+
#labelEl = null;
|
|
38
|
+
#valueEl = null;
|
|
39
|
+
#hintEl = null;
|
|
40
|
+
#iconEl = null;
|
|
41
|
+
|
|
42
|
+
connected() {
|
|
43
|
+
// A non-interactive labeled group — the label is the accessible name,
|
|
44
|
+
// which holds even when the value region is a foreign iframe.
|
|
45
|
+
this.setAttribute('role', 'group');
|
|
46
|
+
|
|
47
|
+
// The label element is the group's accessible-name source; create it
|
|
48
|
+
// once and mint a stable id for aria-labelledby. Prepend so it reads
|
|
49
|
+
// first for assistive tech (visual placement is grid-area driven).
|
|
50
|
+
this.#labelEl = this.querySelector(':scope > [slot="label"]');
|
|
51
|
+
if (!this.#labelEl) {
|
|
52
|
+
this.#labelEl = document.createElement('span');
|
|
53
|
+
this.#labelEl.setAttribute('slot', 'label');
|
|
54
|
+
this.insertBefore(this.#labelEl, this.firstChild);
|
|
55
|
+
}
|
|
56
|
+
if (!this.#labelEl.id) this.#labelEl.id = `display-field-label-${++uid}`;
|
|
57
|
+
|
|
58
|
+
// Re-bind any optional slot children that already exist in the light DOM.
|
|
59
|
+
// On a DOM move (disconnect → reconnect — e.g. UIElement.reconcile's
|
|
60
|
+
// insertBefore during a keyed list reorder) disconnected() nulled these
|
|
61
|
+
// fields but left the nodes in place; without re-binding, render() would
|
|
62
|
+
// see the null field and stamp a DUPLICATE. Query-or-leave-null mirrors
|
|
63
|
+
// the label handling above.
|
|
64
|
+
this.#valueEl = this.querySelector(':scope > [slot="value"]');
|
|
65
|
+
this.#hintEl = this.querySelector(':scope > [slot="hint"]');
|
|
66
|
+
this.#iconEl = this.querySelector(':scope > icon-ui[slot="icon"]');
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
render() {
|
|
70
|
+
if (!this.#labelEl) return;
|
|
71
|
+
|
|
72
|
+
// ── Label — the group's accessible name. ──
|
|
73
|
+
if (this.label) {
|
|
74
|
+
this.#labelEl.textContent = this.label;
|
|
75
|
+
this.#labelEl.hidden = false;
|
|
76
|
+
this.setAttribute('aria-labelledby', this.#labelEl.id);
|
|
77
|
+
} else {
|
|
78
|
+
this.#labelEl.hidden = true;
|
|
79
|
+
this.removeAttribute('aria-labelledby');
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// ── Value — attribute path. Suppressed when the consumer slotted
|
|
83
|
+
// foreign content (a Stripe mount, rich markup): that content owns
|
|
84
|
+
// the value region. ──
|
|
85
|
+
const foreign = this.querySelector(':scope > :not([slot])');
|
|
86
|
+
if (foreign) {
|
|
87
|
+
if (this.#valueEl) { this.#valueEl.remove(); this.#valueEl = null; }
|
|
88
|
+
} else {
|
|
89
|
+
if (!this.#valueEl) {
|
|
90
|
+
this.#valueEl = document.createElement('span');
|
|
91
|
+
this.#valueEl.setAttribute('slot', 'value');
|
|
92
|
+
this.appendChild(this.#valueEl);
|
|
93
|
+
}
|
|
94
|
+
this.#valueEl.textContent = this.value;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// ── Hint — optional caption; drives aria-describedby. Removed when
|
|
98
|
+
// absent so no empty grid row leaks a gap. ──
|
|
99
|
+
if (this.hint) {
|
|
100
|
+
if (!this.#hintEl) {
|
|
101
|
+
this.#hintEl = document.createElement('span');
|
|
102
|
+
this.#hintEl.setAttribute('slot', 'hint');
|
|
103
|
+
this.#hintEl.id = `display-field-hint-${++uid}`;
|
|
104
|
+
this.appendChild(this.#hintEl);
|
|
105
|
+
}
|
|
106
|
+
this.#hintEl.textContent = this.hint;
|
|
107
|
+
this.setAttribute('aria-describedby', this.#hintEl.id);
|
|
108
|
+
} else if (this.#hintEl) {
|
|
109
|
+
this.#hintEl.remove();
|
|
110
|
+
this.#hintEl = null;
|
|
111
|
+
this.removeAttribute('aria-describedby');
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// ── Icon — optional leading glyph. Removed when absent so the icon
|
|
115
|
+
// column collapses (the CSS gates the two-column layout on a
|
|
116
|
+
// present [slot="icon"] child). ──
|
|
117
|
+
if (this.icon) {
|
|
118
|
+
if (!this.#iconEl) {
|
|
119
|
+
this.#iconEl = document.createElement('icon-ui');
|
|
120
|
+
this.#iconEl.setAttribute('slot', 'icon');
|
|
121
|
+
this.#iconEl.setAttribute('aria-hidden', 'true');
|
|
122
|
+
this.appendChild(this.#iconEl);
|
|
123
|
+
}
|
|
124
|
+
this.#iconEl.setAttribute('name', this.icon);
|
|
125
|
+
} else if (this.#iconEl) {
|
|
126
|
+
this.#iconEl.remove();
|
|
127
|
+
this.#iconEl = null;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
disconnected() {
|
|
132
|
+
this.#labelEl = null;
|
|
133
|
+
this.#valueEl = null;
|
|
134
|
+
this.#hintEl = null;
|
|
135
|
+
this.#iconEl = null;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
defineIfFree('display-field-ui', UIDisplayField);
|
|
140
|
+
|
|
141
|
+
export { UIDisplayField };
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* display-field-ui — display-only labeled value (upstream finding F5).
|
|
3
|
+
*
|
|
4
|
+
* Covers the contract that makes it NOT a control: group-label a11y, the
|
|
5
|
+
* dual value path (attr vs foreign slot), and the optional icon/hint that
|
|
6
|
+
* are removed (not just hidden) when their attribute clears.
|
|
7
|
+
*/
|
|
8
|
+
import { describe, it, expect, beforeEach } from 'vitest';
|
|
9
|
+
import '../../core/element.js';
|
|
10
|
+
import './display-field.js';
|
|
11
|
+
import '../icon/icon.js';
|
|
12
|
+
|
|
13
|
+
const raf = () => new Promise((r) => requestAnimationFrame(() => requestAnimationFrame(r)));
|
|
14
|
+
|
|
15
|
+
function mount(html) {
|
|
16
|
+
const wrap = document.createElement('div');
|
|
17
|
+
wrap.innerHTML = html;
|
|
18
|
+
document.body.appendChild(wrap);
|
|
19
|
+
return wrap.firstElementChild;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
describe('display-field-ui', () => {
|
|
23
|
+
beforeEach(() => { document.body.innerHTML = ''; });
|
|
24
|
+
|
|
25
|
+
it('renders as a labeled group: role=group + aria-labelledby → the label element', async () => {
|
|
26
|
+
const el = mount('<display-field-ui label="Account ID" value="acct_1Q9xK2"></display-field-ui>');
|
|
27
|
+
await raf();
|
|
28
|
+
expect(el.getAttribute('role')).toBe('group');
|
|
29
|
+
const labelEl = el.querySelector(':scope > [slot="label"]');
|
|
30
|
+
expect(labelEl).not.toBeNull();
|
|
31
|
+
expect(labelEl.textContent).toBe('Account ID');
|
|
32
|
+
expect(labelEl.id).toBeTruthy();
|
|
33
|
+
expect(el.getAttribute('aria-labelledby')).toBe(labelEl.id);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it('carries no input/focus/form semantics (no contenteditable, not focusable, no tabindex)', async () => {
|
|
37
|
+
const el = mount('<display-field-ui label="Card" value="•••• 4242"></display-field-ui>');
|
|
38
|
+
await raf();
|
|
39
|
+
expect(el.getAttribute('contenteditable')).toBeNull();
|
|
40
|
+
expect(el.getAttribute('tabindex')).toBeNull();
|
|
41
|
+
const valueEl = el.querySelector(':scope > [slot="value"]');
|
|
42
|
+
expect(valueEl.getAttribute('contenteditable')).toBeNull();
|
|
43
|
+
expect(valueEl.textContent).toBe('•••• 4242');
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it('variant="masked" reflects and the value text is preserved', async () => {
|
|
47
|
+
const el = mount('<display-field-ui label="Card" value="•••• 4242" variant="masked"></display-field-ui>');
|
|
48
|
+
await raf();
|
|
49
|
+
expect(el.getAttribute('variant')).toBe('masked');
|
|
50
|
+
expect(el.querySelector(':scope > [slot="value"]').textContent).toBe('•••• 4242');
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it('a foreign default-slot child owns the value region — the attr value element is suppressed', async () => {
|
|
54
|
+
const el = mount('<display-field-ui label="Card number"><div id="mount">[stripe]</div></display-field-ui>');
|
|
55
|
+
await raf();
|
|
56
|
+
// No auto [slot="value"] element — the foreign child is the value.
|
|
57
|
+
expect(el.querySelector(':scope > [slot="value"]')).toBeNull();
|
|
58
|
+
const foreign = el.querySelector(':scope > #mount');
|
|
59
|
+
expect(foreign).not.toBeNull();
|
|
60
|
+
expect(foreign.getAttribute('slot')).toBeNull(); // genuinely unnamed
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it('icon attribute creates a leading icon-ui; clearing it removes the element (no phantom column)', async () => {
|
|
64
|
+
const el = mount('<display-field-ui label="Card" value="•••• 4242" icon="lock"></display-field-ui>');
|
|
65
|
+
await raf();
|
|
66
|
+
const iconEl = el.querySelector(':scope > icon-ui[slot="icon"]');
|
|
67
|
+
expect(iconEl).not.toBeNull();
|
|
68
|
+
expect(iconEl.getAttribute('name')).toBe('lock');
|
|
69
|
+
|
|
70
|
+
el.icon = '';
|
|
71
|
+
await raf();
|
|
72
|
+
expect(el.querySelector(':scope > [slot="icon"]')).toBeNull();
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it('hint attribute creates a hint element + aria-describedby; clearing it removes both', async () => {
|
|
76
|
+
const el = mount('<display-field-ui label="Card" value="•••• 4242" hint="Expires 12/26"></display-field-ui>');
|
|
77
|
+
await raf();
|
|
78
|
+
const hintEl = el.querySelector(':scope > [slot="hint"]');
|
|
79
|
+
expect(hintEl).not.toBeNull();
|
|
80
|
+
expect(hintEl.textContent).toBe('Expires 12/26');
|
|
81
|
+
expect(el.getAttribute('aria-describedby')).toBe(hintEl.id);
|
|
82
|
+
|
|
83
|
+
el.hint = '';
|
|
84
|
+
await raf();
|
|
85
|
+
expect(el.querySelector(':scope > [slot="hint"]')).toBeNull();
|
|
86
|
+
expect(el.getAttribute('aria-describedby')).toBeNull();
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it('surviving a DOM move (disconnect→reconnect) does NOT duplicate slot children', async () => {
|
|
90
|
+
const host1 = mount('<div></div>');
|
|
91
|
+
const el = document.createElement('display-field-ui');
|
|
92
|
+
el.setAttribute('label', 'Card');
|
|
93
|
+
el.setAttribute('value', '•••• 4242');
|
|
94
|
+
el.setAttribute('hint', 'Expires 12/26');
|
|
95
|
+
el.setAttribute('icon', 'lock');
|
|
96
|
+
host1.appendChild(el);
|
|
97
|
+
await raf();
|
|
98
|
+
expect(el.querySelectorAll(':scope > [slot="value"]').length).toBe(1);
|
|
99
|
+
expect(el.querySelectorAll(':scope > [slot="hint"]').length).toBe(1);
|
|
100
|
+
expect(el.querySelectorAll(':scope > icon-ui[slot="icon"]').length).toBe(1);
|
|
101
|
+
|
|
102
|
+
// Move to a new parent — fires disconnectedCallback then connectedCallback,
|
|
103
|
+
// exactly what UIElement.reconcile's insertBefore does on a keyed reorder.
|
|
104
|
+
const host2 = mount('<div></div>');
|
|
105
|
+
host2.appendChild(el);
|
|
106
|
+
await raf();
|
|
107
|
+
|
|
108
|
+
// No duplicates after the move — connected() re-binds the existing nodes.
|
|
109
|
+
expect(el.querySelectorAll(':scope > [slot="label"]').length).toBe(1);
|
|
110
|
+
expect(el.querySelectorAll(':scope > [slot="value"]').length).toBe(1);
|
|
111
|
+
expect(el.querySelectorAll(':scope > [slot="hint"]').length).toBe(1);
|
|
112
|
+
expect(el.querySelectorAll(':scope > icon-ui[slot="icon"]').length).toBe(1);
|
|
113
|
+
expect(el.querySelector(':scope > [slot="value"]').textContent).toBe('•••• 4242');
|
|
114
|
+
});
|
|
115
|
+
});
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
# Edit this file; run `npm run build:components` to regenerate a2ui.json.
|
|
2
|
+
$schema: ../../../../scripts/schemas/component.yaml.schema.json
|
|
3
|
+
name: UIDisplayField
|
|
4
|
+
tag: display-field-ui
|
|
5
|
+
status: stable
|
|
6
|
+
composes:
|
|
7
|
+
- icon-ui
|
|
8
|
+
component: DisplayField
|
|
9
|
+
category: form
|
|
10
|
+
version: 1
|
|
11
|
+
description: >-
|
|
12
|
+
Display-only labeled value — a field-shaped read-out for a static or
|
|
13
|
+
masked value (e.g. a card on file "•••• 4242"), or a labeled slot that
|
|
14
|
+
hosts a third-party element (a Stripe Element iframe) under a proper
|
|
15
|
+
caption. Unlike field-ui it wraps NO interactive control: no focus, no
|
|
16
|
+
contenteditable, no form participation, no [for]/id-minting. The label
|
|
17
|
+
names the whole field for assistive tech via role="group" +
|
|
18
|
+
aria-labelledby, so a slotted foreign element (iframe) is announced
|
|
19
|
+
with its caption. Reach for field-ui + input-ui when the value is
|
|
20
|
+
EDITABLE; stat-ui for a prominent metric/KPI; description-list-ui for
|
|
21
|
+
many read-only key/value pairs at once.
|
|
22
|
+
props:
|
|
23
|
+
label:
|
|
24
|
+
description: The field caption. Names the whole field for assistive tech.
|
|
25
|
+
required: true
|
|
26
|
+
type: string
|
|
27
|
+
default: ""
|
|
28
|
+
reflect: true
|
|
29
|
+
value:
|
|
30
|
+
description: >-
|
|
31
|
+
The static value text to display (e.g. "•••• 4242"). Ignored when
|
|
32
|
+
the default slot already carries content — slot a foreign element
|
|
33
|
+
OR set value=, not both.
|
|
34
|
+
type: string
|
|
35
|
+
default: ""
|
|
36
|
+
reflect: true
|
|
37
|
+
variant:
|
|
38
|
+
description: >-
|
|
39
|
+
Cosmetic value treatment. `default` renders the value in the UI
|
|
40
|
+
font; `masked` switches to the code/mono family with tabular
|
|
41
|
+
numerals and light letter-spacing so masked strings ("•••• 4242")
|
|
42
|
+
align cleanly. Tokens only — no layout change.
|
|
43
|
+
type: string
|
|
44
|
+
default: default
|
|
45
|
+
enum: [default, masked]
|
|
46
|
+
reflect: true
|
|
47
|
+
hint:
|
|
48
|
+
description: >-
|
|
49
|
+
Optional caption rendered below the value in subtle style (e.g.
|
|
50
|
+
"Expires 12/26"). Wired into the field's aria-describedby.
|
|
51
|
+
type: string
|
|
52
|
+
default: ""
|
|
53
|
+
reflect: true
|
|
54
|
+
icon:
|
|
55
|
+
description: >-
|
|
56
|
+
Optional leading icon name shown beside the value (e.g. a lock or
|
|
57
|
+
card-brand glyph). Lazily creates a single icon-ui child.
|
|
58
|
+
type: string
|
|
59
|
+
default: ""
|
|
60
|
+
reflect: true
|
|
61
|
+
events: {}
|
|
62
|
+
slots:
|
|
63
|
+
default:
|
|
64
|
+
description: >-
|
|
65
|
+
The value content. Slot a third-party element here — a Stripe
|
|
66
|
+
Element iframe mount, or rich markup — and it takes the value
|
|
67
|
+
region under the label, replacing the [value] attribute. The
|
|
68
|
+
host's role="group" + aria-labelledby names it for assistive tech.
|
|
69
|
+
label:
|
|
70
|
+
description: >-
|
|
71
|
+
The caption element. Auto-stamped from the [label] attribute; slot
|
|
72
|
+
your own [slot="label"] child to override with richer markup. It is
|
|
73
|
+
the group's accessible name (the aria-labelledby target).
|
|
74
|
+
value:
|
|
75
|
+
description: >-
|
|
76
|
+
The static value element. Auto-stamped from the [value] attribute;
|
|
77
|
+
suppressed when the default slot carries foreign content.
|
|
78
|
+
hint:
|
|
79
|
+
description: >-
|
|
80
|
+
The hint caption below the value. Auto-stamped from the [hint]
|
|
81
|
+
attribute; wired into aria-describedby.
|
|
82
|
+
icon:
|
|
83
|
+
description: >-
|
|
84
|
+
The leading icon (a single icon-ui). Auto-stamped from the [icon]
|
|
85
|
+
attribute.
|
|
86
|
+
states:
|
|
87
|
+
- name: idle
|
|
88
|
+
description: Default, non-interactive read-out.
|
|
89
|
+
traits: []
|
|
90
|
+
tokens:
|
|
91
|
+
--display-field-gap:
|
|
92
|
+
description: Vertical gap between the label, value, and hint rows.
|
|
93
|
+
--display-field-icon-gap:
|
|
94
|
+
description: Horizontal gap between the leading icon and the value.
|
|
95
|
+
--display-field-label-color:
|
|
96
|
+
description: Label foreground color.
|
|
97
|
+
--display-field-label-size:
|
|
98
|
+
description: Label font size.
|
|
99
|
+
--display-field-label-weight:
|
|
100
|
+
description: Label font weight.
|
|
101
|
+
--display-field-value-color:
|
|
102
|
+
description: Value foreground color.
|
|
103
|
+
--display-field-value-size:
|
|
104
|
+
description: Value font size.
|
|
105
|
+
--display-field-value-weight:
|
|
106
|
+
description: Value font weight.
|
|
107
|
+
--display-field-hint-color:
|
|
108
|
+
description: Hint foreground color.
|
|
109
|
+
--display-field-hint-size:
|
|
110
|
+
description: Hint font size.
|
|
111
|
+
--display-field-masked-font:
|
|
112
|
+
description: Font family applied to a masked value.
|
|
113
|
+
--display-field-masked-tracking:
|
|
114
|
+
description: Letter-spacing applied to a masked value.
|
|
115
|
+
a2ui:
|
|
116
|
+
rules:
|
|
117
|
+
- rule: "Use for a display-ONLY labeled value — a masked card number, a 'card on file' read-out, an account id — anywhere a value reads as a field but must not be editable."
|
|
118
|
+
reason: "field-ui wraps an editable control; display-field-ui carries no input/focus/form semantics."
|
|
119
|
+
- rule: "To host a third-party element (a Stripe Element iframe) under a label, slot it as the default child instead of setting value=. The label names it via role=group + aria-labelledby."
|
|
120
|
+
reason: "field-ui auto-mints id/[for] on a real control; a foreign iframe is not labelable, so group-labelling is the correct a11y path."
|
|
121
|
+
- rule: "For an editable field use field-ui + input-ui; for a prominent KPI use stat-ui; for many read-only pairs at once use description-list-ui."
|
|
122
|
+
reason: "Distinct roles — don't reach for display-field-ui when the value is editable or when it's a metric."
|
|
123
|
+
anti_patterns:
|
|
124
|
+
- description: >-
|
|
125
|
+
Using input-ui[readonly] to render a static masked value. A
|
|
126
|
+
readonly input still carries contenteditable/focus semantics and
|
|
127
|
+
tab-stops; a display read-out should not be a control.
|
|
128
|
+
wrong: |
|
|
129
|
+
<input-ui readonly value="•••• 4242"></input-ui>
|
|
130
|
+
right: |
|
|
131
|
+
<display-field-ui label="Card on file" value="•••• 4242" variant="masked"></display-field-ui>
|
|
132
|
+
rule: A non-editable masked value is display-field-ui, not a readonly input.
|
|
133
|
+
examples:
|
|
134
|
+
- name: card-on-file
|
|
135
|
+
description: A masked "card on file" read-out with a leading lock icon and an expiry hint.
|
|
136
|
+
a2ui: >-
|
|
137
|
+
[
|
|
138
|
+
{
|
|
139
|
+
"id": "root",
|
|
140
|
+
"component": "DisplayField",
|
|
141
|
+
"label": "Card on file",
|
|
142
|
+
"value": "•••• •••• •••• 4242",
|
|
143
|
+
"variant": "masked",
|
|
144
|
+
"icon": "lock",
|
|
145
|
+
"hint": "Expires 12/26"
|
|
146
|
+
}
|
|
147
|
+
]
|
|
148
|
+
- name: foreign-element-slot
|
|
149
|
+
description: A labeled slot hosting a third-party element (e.g. a Stripe Element mount).
|
|
150
|
+
a2ui: >-
|
|
151
|
+
[
|
|
152
|
+
{
|
|
153
|
+
"id": "root",
|
|
154
|
+
"component": "DisplayField",
|
|
155
|
+
"label": "Card number",
|
|
156
|
+
"children": ["mount"]
|
|
157
|
+
},
|
|
158
|
+
{ "id": "mount", "component": "Text", "text": "[Stripe Element mounts here]" }
|
|
159
|
+
]
|
|
160
|
+
keywords:
|
|
161
|
+
- display field
|
|
162
|
+
- readonly
|
|
163
|
+
- read-only
|
|
164
|
+
- masked
|
|
165
|
+
- mask
|
|
166
|
+
- static value
|
|
167
|
+
- card on file
|
|
168
|
+
- last four
|
|
169
|
+
- value
|
|
170
|
+
- readout
|
|
171
|
+
- stripe element
|
|
172
|
+
- iframe slot
|
|
173
|
+
- non-editable
|
|
174
|
+
synonyms:
|
|
175
|
+
masked:
|
|
176
|
+
- display field
|
|
177
|
+
- field
|
|
178
|
+
- value
|
|
179
|
+
readout:
|
|
180
|
+
- display field
|
|
181
|
+
- stat
|
|
182
|
+
- field
|
|
183
|
+
value:
|
|
184
|
+
- display field
|
|
185
|
+
- field
|
|
186
|
+
- stat
|
|
187
|
+
related:
|
|
188
|
+
- field
|
|
189
|
+
- input
|
|
190
|
+
- stat
|
|
191
|
+
- description-list
|
|
@@ -88,7 +88,7 @@ export class UIDrawer extends UIElement {
|
|
|
88
88
|
// after the property change, which Safari treats as outside the user-gesture
|
|
89
89
|
// window and silently no-ops the showModal. Wrap the auto-installed `open`
|
|
90
90
|
// setter so dialog state syncs in the same synchronous frame as the
|
|
91
|
-
// assignment. See docs/BROWSER-COMPAT.md §3a (Flavor C).
|
|
91
|
+
// assignment. See .claude/docs/BROWSER-COMPAT.md §3a (Flavor C).
|
|
92
92
|
const desc = Object.getOwnPropertyDescriptor(this, 'open');
|
|
93
93
|
if (desc?.set) {
|
|
94
94
|
const origSet = desc.set;
|
|
@@ -337,7 +337,7 @@ export class UIDrawer extends UIElement {
|
|
|
337
337
|
// requestAnimationFrame when a top-layer dialog is open, sometimes
|
|
338
338
|
// delaying [data-open] (and the slide-in transition) by tens of
|
|
339
339
|
// seconds. Forcing a reflow keeps the animation start in the same
|
|
340
|
-
// synchronous frame. See docs/BROWSER-COMPAT.md §3a (Flavor C).
|
|
340
|
+
// synchronous frame. See .claude/docs/BROWSER-COMPAT.md §3a (Flavor C).
|
|
341
341
|
void dialog.offsetHeight;
|
|
342
342
|
dialog.setAttribute('data-open', '');
|
|
343
343
|
// §FB-27 (P2): emit `opened` after the slide-in transition completes
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
3
|
"$id": "https://adiaui.dev/a2ui/v0_9/components/Feed.json",
|
|
4
4
|
"title": "Feed",
|
|
5
|
-
"description": "Shared top-layer feed channel. Per docs/specs/feed-channel.md (SPEC-FEED-CHANNEL-001). Per-position singletons mounted lazily into document.body via Popover API; consumers post via the static API (`UIFeed.post()`) or the global 'feed' CustomEvent.",
|
|
5
|
+
"description": "Shared top-layer feed channel. Per .claude/docs/specs/feed-channel.md (SPEC-FEED-CHANNEL-001). Per-position singletons mounted lazily into document.body via Popover API; consumers post via the static API (`UIFeed.post()`) or the global 'feed' CustomEvent.",
|
|
6
6
|
"type": "object",
|
|
7
7
|
"allOf": [
|
|
8
8
|
{
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
/**
|
|
15
15
|
* <feed-ui> + <feed-item-ui> — Shared top-layer feed channel.
|
|
16
16
|
*
|
|
17
|
-
* Per docs/specs/feed-channel.md (SPEC-FEED-CHANNEL-001).
|
|
17
|
+
* Per .claude/docs/specs/feed-channel.md (SPEC-FEED-CHANNEL-001).
|
|
18
18
|
*
|
|
19
19
|
* Phase 1 (skeleton) ships here:
|
|
20
20
|
* - Per-position singletons (`<feed-ui position="bottom-right">`)
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* `<feed-ui>` — Shared top-layer feed channel. Per docs/specs/feed-channel.md
|
|
2
|
+
* `<feed-ui>` — Shared top-layer feed channel. Per .claude/docs/specs/feed-channel.md
|
|
3
3
|
* (SPEC-FEED-CHANNEL-001). Per-position singletons mounted lazily into
|
|
4
4
|
* document.body via Popover API; consumers post via the static API
|
|
5
5
|
* (`UIFeed.post()`) or the global 'feed' CustomEvent.
|
|
@@ -8,7 +8,7 @@ component: Feed
|
|
|
8
8
|
category: container
|
|
9
9
|
version: 1
|
|
10
10
|
description: >-
|
|
11
|
-
Shared top-layer feed channel. Per docs/specs/feed-channel.md
|
|
11
|
+
Shared top-layer feed channel. Per .claude/docs/specs/feed-channel.md
|
|
12
12
|
(SPEC-FEED-CHANNEL-001). Per-position singletons mounted lazily into
|
|
13
13
|
document.body via Popover API; consumers post via the static API
|
|
14
14
|
(`UIFeed.post()`) or the global 'feed' CustomEvent.
|
|
@@ -60,7 +60,7 @@ related:
|
|
|
60
60
|
a2ui:
|
|
61
61
|
rules:
|
|
62
62
|
- rule: 'Top-layer notification feed channel — singleton per position (top-right, bottom-center, etc.) mounted lazily into document.body.'
|
|
63
|
-
reason: 'Per docs/specs/feed-channel.md.'
|
|
63
|
+
reason: 'Per .claude/docs/specs/feed-channel.md.'
|
|
64
64
|
- rule: 'Hosts <feed-item-ui> children programmatically via static API (UIFeed.post(...)). Do not place feed-items declaratively.'
|
|
65
65
|
reason: 'Imperative API ownership.'
|
|
66
66
|
- rule: 'For inline persistent alerts inside content regions use <alert-ui> instead; feed is ephemeral overlay.'
|
|
@@ -60,7 +60,7 @@ export class UIField extends UIElement {
|
|
|
60
60
|
// is the single source of truth — validity is a property of the value, not
|
|
61
61
|
// of the layout wrapper). field-ui mirrors the child's error string into
|
|
62
62
|
// its own [data-field-error] slot so the validation message renders below
|
|
63
|
-
// the control. See docs/specs/component-token-contract.md "Toggle state
|
|
63
|
+
// the control. See .claude/docs/specs/component-token-contract.md "Toggle state
|
|
64
64
|
// naming" / open decisions resolution log.
|
|
65
65
|
get error() {
|
|
66
66
|
const ctrl = this.#findControl();
|
|
@@ -12,7 +12,35 @@
|
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
14
|
import { UIElement } from '../../core/element.js';
|
|
15
|
-
import { getIcon, whenIconRegistryReady } from '../../core/icons.js';
|
|
15
|
+
import { getIcon, whenIconRegistryReady, iconRegistryUnwired } from '../../core/icons.js';
|
|
16
|
+
|
|
17
|
+
// Upstream finding F1: the zero-config Phosphor glob path can leave the icon
|
|
18
|
+
// registry completely unwired under Vite 8 / rolldown (the `import.meta.glob`
|
|
19
|
+
// macro doesn't transform from inside a published dependency), so every
|
|
20
|
+
// `<icon-ui>` renders blank. The loader layer warns, but the per-icon miss is
|
|
21
|
+
// silent (`getIcon → ''`). Fail loud ONCE from the component too, with the
|
|
22
|
+
// actionable fix. Deferred a macrotask so an app that calls
|
|
23
|
+
// `installIconLoaders()` at entry (the correct path) is never false-flagged.
|
|
24
|
+
let _unwiredCheckScheduled = false;
|
|
25
|
+
function warnIfIconRegistryUnwired() {
|
|
26
|
+
if (_unwiredCheckScheduled) return;
|
|
27
|
+
_unwiredCheckScheduled = true;
|
|
28
|
+
setTimeout(() => {
|
|
29
|
+
if (!iconRegistryUnwired()) return; // loaders arrived after all — fine
|
|
30
|
+
// eslint-disable-next-line no-console
|
|
31
|
+
console.error(
|
|
32
|
+
`[icon-ui] No icons are registered and no icon loaders were installed — every <icon-ui> renders blank.\n` +
|
|
33
|
+
` The zero-config import.meta.glob path does not work from inside a published dependency under Vite 8 / rolldown.\n` +
|
|
34
|
+
` Fix — from your APP entry (where import.meta.glob is transformed):\n` +
|
|
35
|
+
` npm i @phosphor-icons/core\n` +
|
|
36
|
+
` import { installIconLoaders } from '@adia-ai/web-components/core/icons';\n` +
|
|
37
|
+
` installIconLoaders({ regular: import.meta.glob(\n` +
|
|
38
|
+
` '/node_modules/@phosphor-icons/core/assets/regular/*.svg',\n` +
|
|
39
|
+
` { query: '?raw', import: 'default', eager: true }) });\n` +
|
|
40
|
+
` See USAGE.md § Icons.`,
|
|
41
|
+
);
|
|
42
|
+
}, 0);
|
|
43
|
+
}
|
|
16
44
|
|
|
17
45
|
export class UIIcon extends UIElement {
|
|
18
46
|
static properties = {
|
|
@@ -62,6 +90,11 @@ export class UIIcon extends UIElement {
|
|
|
62
90
|
if (this.label) this.setAttribute('aria-label', this.label);
|
|
63
91
|
const svg = getIcon(this.name, this.weight || 'regular');
|
|
64
92
|
if (svg && this.innerHTML !== svg) this.innerHTML = svg;
|
|
93
|
+
// A named icon that resolved to nothing AND a never-wired registry is the
|
|
94
|
+
// F1 symptom — schedule the one-shot fail-loud. A plain typo (registry
|
|
95
|
+
// populated, this one name absent) settles `iconRegistryUnwired()` false
|
|
96
|
+
// and is correctly NOT flagged here.
|
|
97
|
+
else if (this.name && !svg) warnIfIconRegistryUnwired();
|
|
65
98
|
|
|
66
99
|
// Pixel / rem / em passthrough — `size="48"` → 48px,
|
|
67
100
|
// `size="3.5rem"` → 3.5rem. Named-scale values (xs/sm/md/lg/xl/
|
package/components/index.js
CHANGED
|
@@ -71,6 +71,7 @@ export { UISwatch } from './swatch/swatch.js';
|
|
|
71
71
|
export { UICol } from './col/col.js';
|
|
72
72
|
export { UIField } from './field/field.js';
|
|
73
73
|
export { UIFields } from './fields/fields.js';
|
|
74
|
+
export { UIDisplayField } from './display-field/display-field.js';
|
|
74
75
|
export { UIRow } from './row/row.js';
|
|
75
76
|
export { UIGrid } from './grid/grid.js';
|
|
76
77
|
export { UIStack } from './stack/stack.js';
|
|
@@ -49,7 +49,7 @@
|
|
|
49
49
|
* changes are driven by the reactive [active] setter wrapper, which
|
|
50
50
|
* starts/cancels the timer + applies aria-busy when the timer fires.
|
|
51
51
|
*
|
|
52
|
-
* @see SPEC-034 (docs/specs/implementation-ready/SPEC-034-loading-overlay.md)
|
|
52
|
+
* @see SPEC-034 (.claude/docs/specs/implementation-ready/SPEC-034-loading-overlay.md)
|
|
53
53
|
*/
|
|
54
54
|
|
|
55
55
|
import { UIElement } from '../../core/element.js';
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# Hand-authored per SPEC-034 (docs/specs/implementation-ready/SPEC-034-loading-overlay.md).
|
|
1
|
+
# Hand-authored per SPEC-034 (.claude/docs/specs/implementation-ready/SPEC-034-loading-overlay.md).
|
|
2
2
|
# Edit this file; run `npm run build:components` to regenerate loading-overlay.a2ui.json.
|
|
3
3
|
$schema: ../../../../scripts/schemas/component.yaml.schema.json
|
|
4
4
|
name: UILoadingOverlay
|
|
@@ -212,7 +212,7 @@ export class UIModal extends UIElement {
|
|
|
212
212
|
// Sync open state. Synchronous reflow instead of rAF — Safari throttles
|
|
213
213
|
// requestAnimationFrame when a top-layer dialog is open, sometimes
|
|
214
214
|
// delaying [data-open] (and the scale-in transition) by tens of seconds.
|
|
215
|
-
// See docs/BROWSER-COMPAT.md §3a (Flavor C).
|
|
215
|
+
// See .claude/docs/BROWSER-COMPAT.md §3a (Flavor C).
|
|
216
216
|
if (this.open && !dialog.open) {
|
|
217
217
|
this.#closing = false;
|
|
218
218
|
this.#previousFocus = document.activeElement;
|
|
@@ -134,7 +134,7 @@ export class UINav extends UIElement {
|
|
|
134
134
|
// Safari macOS leaves :hover stuck on items the cursor passed through
|
|
135
135
|
// when the DOM mutates during click+route navigation. Toggling pointer-
|
|
136
136
|
// events forces re-evaluation of hover state on next paint.
|
|
137
|
-
// See docs/BROWSER-COMPAT.md §3a.
|
|
137
|
+
// See .claude/docs/BROWSER-COMPAT.md §3a.
|
|
138
138
|
#flushHoverState() {
|
|
139
139
|
this.style.pointerEvents = 'none';
|
|
140
140
|
requestAnimationFrame(() => {
|
|
@@ -18,10 +18,10 @@
|
|
|
18
18
|
|
|
19
19
|
```html
|
|
20
20
|
<nav-ui variant="section" heading="Documentation" style="width:240px;">
|
|
21
|
-
<nav-item-ui text="Overview" value="docs/overview" selected></nav-item-ui>
|
|
22
|
-
<nav-item-ui text="Quickstart" value="docs/quickstart"></nav-item-ui>
|
|
23
|
-
<nav-item-ui text="API reference" value="docs/api"></nav-item-ui>
|
|
24
|
-
<nav-item-ui text="Examples" value="docs/examples"></nav-item-ui>
|
|
21
|
+
<nav-item-ui text="Overview" value=".claude/docs/overview" selected></nav-item-ui>
|
|
22
|
+
<nav-item-ui text="Quickstart" value=".claude/docs/quickstart"></nav-item-ui>
|
|
23
|
+
<nav-item-ui text="API reference" value=".claude/docs/api"></nav-item-ui>
|
|
24
|
+
<nav-item-ui text="Examples" value=".claude/docs/examples"></nav-item-ui>
|
|
25
25
|
</nav-ui>
|
|
26
26
|
```
|
|
27
27
|
|
|
@@ -96,7 +96,7 @@ export class UIPopover extends UIElement {
|
|
|
96
96
|
// try/catch covers test envs (happy-dom) without the API. Known iOS
|
|
97
97
|
// Safari quirks at the supported floor (light-dismiss broken on
|
|
98
98
|
// 17.0–18.2, virtual-keyboard-on-input persists past 18.3) are
|
|
99
|
-
// documented in docs/BROWSER-COMPAT.md §3a.
|
|
99
|
+
// documented in .claude/docs/BROWSER-COMPAT.md §3a.
|
|
100
100
|
try { content.showPopover(); } catch { /* popover API unavailable — anchor positioning still runs */ }
|
|
101
101
|
}
|
|
102
102
|
|
|
@@ -36,7 +36,7 @@
|
|
|
36
36
|
* sets the three ARIA attributes once; render() keeps aria-valuetext in
|
|
37
37
|
* sync with the [label] prop.
|
|
38
38
|
*
|
|
39
|
-
* @see SPEC-001 (docs/specs/implementation-ready/SPEC-001-spinner-loader.md)
|
|
39
|
+
* @see SPEC-001 (.claude/docs/specs/implementation-ready/SPEC-001-spinner-loader.md)
|
|
40
40
|
*/
|
|
41
41
|
|
|
42
42
|
import { UIElement } from '../../core/element.js';
|