@adia-ai/web-components 0.5.13 → 0.5.15
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 +241 -0
- package/components/action-list/action-list.css +1 -1
- package/components/calendar-picker/calendar-picker.css +1 -1
- package/components/chat-thread/chat-input.css +3 -3
- package/components/check/check.css +1 -1
- package/components/heatmap/class.js +4 -0
- package/components/heatmap/heatmap.css +1 -1
- package/components/input/input.css +3 -3
- package/components/list/list.css +6 -6
- package/components/menu/menu.css +2 -2
- package/components/nav-group/nav-group.css +8 -8
- package/components/option-card/option-card.css +2 -1
- package/components/otp-input/class.js +4 -1
- package/components/otp-input/otp-input.a2ui.json +9 -0
- package/components/otp-input/otp-input.d.ts +3 -0
- package/components/otp-input/otp-input.yaml +6 -0
- package/components/page/page.css +1 -1
- package/components/pagination/pagination.css +3 -1
- package/components/radio/radio.css +1 -1
- package/components/search/class.js +2 -0
- package/components/search/search.a2ui.json +9 -0
- package/components/search/search.d.ts +2 -0
- package/components/search/search.yaml +6 -0
- package/components/segment/segment.css +3 -3
- package/components/select/select.css +12 -11
- package/components/stepper/stepper.css +24 -24
- package/components/swatch/class.js +56 -1
- package/components/swatch/swatch.css +2 -2
- package/components/swatch/swatch.test.js +163 -0
- package/components/switch/switch.css +1 -1
- package/components/tabs/tabs.css +1 -1
- package/components/text/text.a2ui.json +16 -2
- package/components/text/text.d.ts +28 -45
- package/components/text/text.yaml +25 -1
- package/components/textarea/textarea.css +6 -2
- package/components/toolbar/toolbar.css +1 -1
- package/components/upload/upload.css +2 -1
- package/package.json +1 -1
- package/styles/colors/semantics.css +7 -84
- package/styles/typography.css +1 -1
|
@@ -39,6 +39,12 @@ events:
|
|
|
39
39
|
description: Fired when the clear button is clicked
|
|
40
40
|
input:
|
|
41
41
|
description: "Fired on each input change during typing."
|
|
42
|
+
change:
|
|
43
|
+
description: Fired alongside `input` on every keystroke + on clear. Provides form-bearing-primitive event-shape parity (every UIFormElement emits `change` with `detail.value`).
|
|
44
|
+
detail:
|
|
45
|
+
value:
|
|
46
|
+
type: string
|
|
47
|
+
description: Current search-input value.
|
|
42
48
|
search:
|
|
43
49
|
description: Debounced CustomEvent with detail.query containing the search string
|
|
44
50
|
slots:
|
|
@@ -20,10 +20,10 @@ segment-ui[selected] {
|
|
|
20
20
|
--segment-font-weight: var(--a-ui-weight);
|
|
21
21
|
|
|
22
22
|
/* ── Colors ── */
|
|
23
|
-
--segment-fg: var(--a-
|
|
24
|
-
--segment-fg-hover: var(--a-
|
|
23
|
+
--segment-fg: var(--a-ui-text-subtle);
|
|
24
|
+
--segment-fg-hover: var(--a-ui-text-hover);
|
|
25
25
|
--segment-fg-selected: var(--a-chrome-dark);
|
|
26
|
-
--segment-fg-disabled: var(--a-
|
|
26
|
+
--segment-fg-disabled: var(--a-ui-text-disabled);
|
|
27
27
|
|
|
28
28
|
/* ── Transition ── */
|
|
29
29
|
--segment-duration: var(--a-duration-fast);
|
|
@@ -24,6 +24,7 @@
|
|
|
24
24
|
--select-placeholder-fg: var(--a-ui-text-placeholder);
|
|
25
25
|
--select-caret-fg: var(--a-ui-text-muted);
|
|
26
26
|
--select-fg: var(--a-ui-text);
|
|
27
|
+
--select-fg-hover: var(--a-ui-text-hover);
|
|
27
28
|
--select-fg-selected: var(--a-ui-text-selected);
|
|
28
29
|
--select-fg-subtle: var(--a-ui-text-subtle);
|
|
29
30
|
--select-fg-muted: var(--a-ui-text-muted);
|
|
@@ -38,7 +39,7 @@
|
|
|
38
39
|
--select-option-px: var(--a-ui-px);
|
|
39
40
|
--select-option-py: var(--a-ui-py);
|
|
40
41
|
--select-option-bg-hover: var(--a-bg-hover);
|
|
41
|
-
--select-option-fg-active: var(--a-
|
|
42
|
+
--select-option-fg-active: var(--a-ui-text-active);
|
|
42
43
|
--select-option-fg-disabled: var(--a-ui-text-disabled);
|
|
43
44
|
--select-group-label-size: var(--a-ui-size);
|
|
44
45
|
}
|
|
@@ -195,7 +196,7 @@ select-ui [slot="listbox"] {
|
|
|
195
196
|
overflow-y: auto;
|
|
196
197
|
font-family: inherit;
|
|
197
198
|
font-size: var(--a-ui-size);
|
|
198
|
-
color: var(--
|
|
199
|
+
color: var(--select-fg);
|
|
199
200
|
|
|
200
201
|
/* Positioned by JS (#positionListbox) — fixed to viewport */
|
|
201
202
|
width: max-content;
|
|
@@ -223,7 +224,7 @@ select-ui [role="option"] {
|
|
|
223
224
|
padding: var(--a-space-1) var(--a-ui-px);
|
|
224
225
|
border-radius: var(--a-radius-sm);
|
|
225
226
|
white-space: nowrap;
|
|
226
|
-
color: var(--
|
|
227
|
+
color: var(--select-fg-subtle);
|
|
227
228
|
cursor: pointer;
|
|
228
229
|
transition:
|
|
229
230
|
background var(--a-duration-fast) var(--a-easing),
|
|
@@ -231,8 +232,8 @@ select-ui [role="option"] {
|
|
|
231
232
|
}
|
|
232
233
|
select-ui [role="option"]:hover,
|
|
233
234
|
select-ui [role="option"][data-focused] {
|
|
234
|
-
background: var(--
|
|
235
|
-
color: var(--
|
|
235
|
+
background: var(--select-option-bg-hover);
|
|
236
|
+
color: var(--select-fg-hover);
|
|
236
237
|
}
|
|
237
238
|
select-ui [role="option"][aria-selected="true"] {
|
|
238
239
|
color: var(--select-fg-selected);
|
|
@@ -240,7 +241,7 @@ select-ui [role="option"][aria-selected="true"] {
|
|
|
240
241
|
font-weight: var(--a-ui-weight);
|
|
241
242
|
}
|
|
242
243
|
select-ui [role="option"][aria-disabled="true"] {
|
|
243
|
-
color: var(--
|
|
244
|
+
color: var(--select-option-fg-disabled);
|
|
244
245
|
cursor: not-allowed;
|
|
245
246
|
}
|
|
246
247
|
select-ui [role="option"][aria-disabled="true"]:hover {
|
|
@@ -250,7 +251,7 @@ select-ui [role="option"][aria-disabled="true"]:hover {
|
|
|
250
251
|
/* Option with icon */
|
|
251
252
|
select-ui [role="option"] icon-ui {
|
|
252
253
|
--a-icon-size: var(--a-ui-size);
|
|
253
|
-
color: var(--
|
|
254
|
+
color: var(--select-fg-muted);
|
|
254
255
|
margin-inline-end: var(--a-space-1);
|
|
255
256
|
vertical-align: -0.125em;
|
|
256
257
|
}
|
|
@@ -282,21 +283,21 @@ select-ui [data-menu-header] img {
|
|
|
282
283
|
select-ui [data-menu-header] strong {
|
|
283
284
|
display: block;
|
|
284
285
|
font-weight: var(--a-weight-medium);
|
|
285
|
-
color: var(--
|
|
286
|
+
color: var(--select-fg);
|
|
286
287
|
font-size: var(--a-ui-size);
|
|
287
288
|
}
|
|
288
289
|
|
|
289
290
|
select-ui [data-menu-header] span {
|
|
290
291
|
display: block;
|
|
291
292
|
font-size: var(--a-ui-sm);
|
|
292
|
-
color: var(--
|
|
293
|
+
color: var(--select-fg-muted);
|
|
293
294
|
}
|
|
294
295
|
|
|
295
296
|
select-ui [role="group"] [slot="group-label"] {
|
|
296
297
|
padding: var(--a-space-1) var(--a-ui-px);
|
|
297
298
|
font-size: var(--a-ui-tiny);
|
|
298
299
|
font-weight: var(--a-ui-weight);
|
|
299
|
-
color: var(--
|
|
300
|
+
color: var(--select-fg-muted);
|
|
300
301
|
text-transform: uppercase;
|
|
301
302
|
letter-spacing: 0.05em;
|
|
302
303
|
white-space: nowrap;
|
|
@@ -315,6 +316,6 @@ select-ui > [slot="hint"] {
|
|
|
315
316
|
display: block;
|
|
316
317
|
margin-top: var(--select-hint-mt, var(--a-space-1));
|
|
317
318
|
font-size: var(--select-hint-size, var(--a-ui-xs));
|
|
318
|
-
color: var(--select-hint-fg, var(--
|
|
319
|
+
color: var(--select-hint-fg, var(--select-fg-muted));
|
|
319
320
|
line-height: var(--select-hint-lh, 1.4);
|
|
320
321
|
}
|
|
@@ -20,13 +20,13 @@
|
|
|
20
20
|
--stepper-weight: var(--a-weight-medium);
|
|
21
21
|
--stepper-border-size: 2px;
|
|
22
22
|
|
|
23
|
-
--stepper-active
|
|
24
|
-
--stepper-active
|
|
25
|
-
--stepper-active
|
|
23
|
+
--stepper-bg-active: var(--a-bg);
|
|
24
|
+
--stepper-border-active: var(--a-accent);
|
|
25
|
+
--stepper-fg-active: var(--a-accent);
|
|
26
26
|
|
|
27
|
-
--stepper-done
|
|
28
|
-
--stepper-done
|
|
29
|
-
--stepper-done
|
|
27
|
+
--stepper-bg-done: var(--a-accent);
|
|
28
|
+
--stepper-border-done: var(--a-accent);
|
|
29
|
+
--stepper-fg-done: var(--a-accent-fg);
|
|
30
30
|
|
|
31
31
|
--stepper-line: var(--a-border-subtle);
|
|
32
32
|
--stepper-line-done: var(--a-accent);
|
|
@@ -76,13 +76,13 @@
|
|
|
76
76
|
--stepper-item-weight: var(--stepper-weight, var(--a-weight-medium));
|
|
77
77
|
--stepper-item-border-size: var(--stepper-border-size, 2px);
|
|
78
78
|
|
|
79
|
-
--stepper-item-active
|
|
80
|
-
--stepper-item-active
|
|
81
|
-
--stepper-item-active
|
|
79
|
+
--stepper-item-bg-active: var(--stepper-bg-active, var(--a-bg));
|
|
80
|
+
--stepper-item-border-active: var(--stepper-border-active, var(--a-accent));
|
|
81
|
+
--stepper-item-fg-active: var(--stepper-fg-active, var(--a-accent));
|
|
82
82
|
|
|
83
|
-
--stepper-item-done
|
|
84
|
-
--stepper-item-done
|
|
85
|
-
--stepper-item-done
|
|
83
|
+
--stepper-item-bg-done: var(--stepper-bg-done, var(--a-accent));
|
|
84
|
+
--stepper-item-border-done: var(--stepper-border-done, var(--a-accent));
|
|
85
|
+
--stepper-item-fg-done: var(--stepper-fg-done, var(--a-accent-fg));
|
|
86
86
|
|
|
87
87
|
--stepper-item-line: var(--stepper-line, var(--a-border-subtle));
|
|
88
88
|
--stepper-item-line-done: var(--stepper-line-done, var(--a-accent));
|
|
@@ -158,9 +158,9 @@
|
|
|
158
158
|
|
|
159
159
|
/* Active state */
|
|
160
160
|
:scope[status="active"]::after {
|
|
161
|
-
border-color: var(--stepper-item-active
|
|
162
|
-
color: var(--stepper-item-active
|
|
163
|
-
background: var(--stepper-item-active
|
|
161
|
+
border-color: var(--stepper-item-border-active);
|
|
162
|
+
color: var(--stepper-item-fg-active);
|
|
163
|
+
background: var(--stepper-item-bg-active);
|
|
164
164
|
}
|
|
165
165
|
|
|
166
166
|
:scope[status="active"] [slot="label"] {
|
|
@@ -171,9 +171,9 @@
|
|
|
171
171
|
/* Completed state — checkmark replaces number */
|
|
172
172
|
:scope[status="completed"]::after {
|
|
173
173
|
content: '\2713';
|
|
174
|
-
background: var(--stepper-item-done
|
|
175
|
-
border-color: var(--stepper-item-done
|
|
176
|
-
color: var(--stepper-item-done
|
|
174
|
+
background: var(--stepper-item-bg-done);
|
|
175
|
+
border-color: var(--stepper-item-border-done);
|
|
176
|
+
color: var(--stepper-item-fg-done);
|
|
177
177
|
}
|
|
178
178
|
|
|
179
179
|
:scope[status="completed"]::before {
|
|
@@ -214,15 +214,15 @@
|
|
|
214
214
|
}
|
|
215
215
|
|
|
216
216
|
:scope[status="active"] [slot="icon"] {
|
|
217
|
-
border-color: var(--stepper-item-active
|
|
218
|
-
color: var(--stepper-item-active
|
|
219
|
-
background: var(--stepper-item-active
|
|
217
|
+
border-color: var(--stepper-item-border-active);
|
|
218
|
+
color: var(--stepper-item-fg-active);
|
|
219
|
+
background: var(--stepper-item-bg-active);
|
|
220
220
|
}
|
|
221
221
|
|
|
222
222
|
:scope[status="completed"] [slot="icon"] {
|
|
223
|
-
background: var(--stepper-item-done
|
|
224
|
-
border-color: var(--stepper-item-done
|
|
225
|
-
color: var(--stepper-item-done
|
|
223
|
+
background: var(--stepper-item-bg-done);
|
|
224
|
+
border-color: var(--stepper-item-border-done);
|
|
225
|
+
color: var(--stepper-item-fg-done);
|
|
226
226
|
}
|
|
227
227
|
|
|
228
228
|
/* ── Content slots ── */
|
|
@@ -226,6 +226,17 @@ export class UISwatch extends UIElement {
|
|
|
226
226
|
// Capture pre-existing default-slot content so consumer-authored
|
|
227
227
|
// children (e.g. <swatch-ui>Forecast</swatch-ui>) survive stamping.
|
|
228
228
|
// `[slot="chrome"]` children are already removed above, so won't appear here.
|
|
229
|
+
//
|
|
230
|
+
// §325 (v0.5.16, FB-36): filter pure-whitespace text nodes from the
|
|
231
|
+
// capture. Pre-§325, indented multi-line HTML (`<swatch-ui
|
|
232
|
+
// label="500">\n <span>tip</span>\n</swatch-ui>`) captured the
|
|
233
|
+
// leading whitespace text node, which then made #labelEl.firstChild
|
|
234
|
+
// a nodeType=3 text node. #syncCore()'s "stale-text-node" branch
|
|
235
|
+
// matched the whitespace + did textContent = this.label, wiping
|
|
236
|
+
// BOTH the whitespace AND the consumer's <span>. Filter here so
|
|
237
|
+
// #labelEl.firstChild is the actual element/non-whitespace text,
|
|
238
|
+
// and #syncCore's stale-text branch only fires for genuinely
|
|
239
|
+
// attr-driven cases.
|
|
229
240
|
const slotted = Array.from(this.childNodes).filter(n =>
|
|
230
241
|
!(n.nodeType === 1 && n.dataset && (
|
|
231
242
|
n.dataset.tile !== undefined ||
|
|
@@ -233,7 +244,7 @@ export class UISwatch extends UIElement {
|
|
|
233
244
|
n.dataset.detail !== undefined ||
|
|
234
245
|
n.dataset.badge !== undefined ||
|
|
235
246
|
n.dataset.copy !== undefined
|
|
236
|
-
))
|
|
247
|
+
)) && !(n.nodeType === 3 && !n.textContent.trim())
|
|
237
248
|
);
|
|
238
249
|
this.innerHTML = '';
|
|
239
250
|
|
|
@@ -282,9 +293,53 @@ export class UISwatch extends UIElement {
|
|
|
282
293
|
|
|
283
294
|
// ── Sync ──────────────────────────────────────────────────────────
|
|
284
295
|
|
|
296
|
+
/**
|
|
297
|
+
* §326 (v0.5.16, FB-37): Late-arriving `[slot="chrome"]` children may
|
|
298
|
+
* land on the host AFTER `#stamp()` runs — happens in any template
|
|
299
|
+
* engine (lit-html, AdiaUI `html\`\``, etc.) that batches child appends
|
|
300
|
+
* separately from element creation. `#stamp()` is gated by
|
|
301
|
+
* `this.#stamped` and won't re-run; this method picks up the late
|
|
302
|
+
* arrivals on every `render()` pass + moves them to the canonical
|
|
303
|
+
* sibling-of-tile position (between `#tileEl` and `#badgeEl`).
|
|
304
|
+
*
|
|
305
|
+
* Idempotent: chrome children already in the right slot are left
|
|
306
|
+
* alone (the `insertBefore` only moves them if they're not the
|
|
307
|
+
* #badgeEl's immediate previousSibling chain).
|
|
308
|
+
*/
|
|
309
|
+
#absorbChromeSlot() {
|
|
310
|
+
if (!this.#tileEl || !this.#badgeEl) return;
|
|
311
|
+
const chromeChildren = [];
|
|
312
|
+
for (const child of this.children) {
|
|
313
|
+
if (child === this.#tileEl) continue;
|
|
314
|
+
if (child === this.#badgeEl) break; // hit the badge — done scanning the head region
|
|
315
|
+
if (child.getAttribute?.('slot') === 'chrome') chromeChildren.push(child);
|
|
316
|
+
}
|
|
317
|
+
// Walk the rest of the children looking for chrome children that landed
|
|
318
|
+
// AFTER the internal structure (the common template-late-append case).
|
|
319
|
+
for (const child of Array.from(this.children)) {
|
|
320
|
+
if (
|
|
321
|
+
child !== this.#tileEl &&
|
|
322
|
+
child !== this.#badgeEl &&
|
|
323
|
+
child !== this.#labelEl &&
|
|
324
|
+
child !== this.#detailEl &&
|
|
325
|
+
child !== this.#copyEl &&
|
|
326
|
+
child.getAttribute?.('slot') === 'chrome'
|
|
327
|
+
) {
|
|
328
|
+
// Move to the canonical position: immediately after #tileEl.
|
|
329
|
+
// insertBefore is idempotent — moving an already-positioned child
|
|
330
|
+
// to the same spot is a no-op.
|
|
331
|
+
this.insertBefore(child, this.#badgeEl);
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
|
|
285
336
|
#syncCore() {
|
|
286
337
|
if (!this.#tileEl || !this.#labelEl) return;
|
|
287
338
|
|
|
339
|
+
// §326 (v0.5.16, FB-37): pick up `[slot="chrome"]` children that
|
|
340
|
+
// landed after #stamp() ran (template-render path).
|
|
341
|
+
this.#absorbChromeSlot();
|
|
342
|
+
|
|
288
343
|
// Normalize enum values; fall back silently when a consumer sets a typo.
|
|
289
344
|
const shape = SHAPES.has(this.shape) ? this.shape : 'square';
|
|
290
345
|
const size = SIZES.has(this.size) ? this.size : 'md';
|
|
@@ -291,7 +291,7 @@
|
|
|
291
291
|
line-height: 1;
|
|
292
292
|
cursor: pointer;
|
|
293
293
|
border-radius: var(--a-radius-xs);
|
|
294
|
-
transition: color
|
|
294
|
+
transition: color var(--a-duration-fast) var(--a-easing-out), background var(--a-duration-fast) var(--a-easing-out);
|
|
295
295
|
}
|
|
296
296
|
:scope > [data-copy]:hover { color: var(--a-fg); background: var(--a-bg-muted); }
|
|
297
297
|
:scope > [data-copy]:focus-visible {
|
|
@@ -308,7 +308,7 @@
|
|
|
308
308
|
:scope[selectable] {
|
|
309
309
|
cursor: pointer;
|
|
310
310
|
border-radius: var(--a-radius-sm);
|
|
311
|
-
transition: box-shadow
|
|
311
|
+
transition: box-shadow var(--a-duration-fast) var(--a-easing-out);
|
|
312
312
|
}
|
|
313
313
|
:scope[selectable]:focus { outline: none; }
|
|
314
314
|
:scope[selectable]:focus-visible {
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* <swatch-ui> behavioral tests.
|
|
3
|
+
*
|
|
4
|
+
* Currently scoped to FB-36 / §325 regression coverage. Future tests for
|
|
5
|
+
* #stamp() lifecycle, chrome-slot composition, auto-contrast probe,
|
|
6
|
+
* etc. land here.
|
|
7
|
+
*
|
|
8
|
+
* Test setup note: happy-dom connects custom elements synchronously
|
|
9
|
+
* during innerHTML parsing — BEFORE sibling children inside the same
|
|
10
|
+
* element are appended. Real browsers connect after the full innerHTML
|
|
11
|
+
* is parsed. To exercise the FB-36 path (children present at connect
|
|
12
|
+
* time), tests use `document.createElement` + `appendChild` to build
|
|
13
|
+
* the full subtree FIRST, then attach to the document — guaranteeing
|
|
14
|
+
* the children are in `this.childNodes` when `connectedCallback()` fires.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { describe, it, expect, beforeAll, beforeEach } from 'vitest';
|
|
18
|
+
|
|
19
|
+
beforeAll(async () => {
|
|
20
|
+
await import('./swatch.js');
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
function buildSwatch({ label, defaultSlot, shape = 'block', labelPosition = 'overlay', color = '#3b82f6' }) {
|
|
24
|
+
const swatch = document.createElement('swatch-ui');
|
|
25
|
+
swatch.setAttribute('shape', shape);
|
|
26
|
+
swatch.setAttribute('label-position', labelPosition);
|
|
27
|
+
swatch.setAttribute('color', color);
|
|
28
|
+
if (label !== undefined) swatch.setAttribute('label', label);
|
|
29
|
+
if (defaultSlot) {
|
|
30
|
+
// Mimic indented HTML's whitespace text nodes — the FB-36 trigger.
|
|
31
|
+
swatch.appendChild(document.createTextNode('\n '));
|
|
32
|
+
const child = document.createElement('span');
|
|
33
|
+
child.id = defaultSlot.id;
|
|
34
|
+
child.textContent = defaultSlot.text;
|
|
35
|
+
swatch.appendChild(child);
|
|
36
|
+
swatch.appendChild(document.createTextNode('\n '));
|
|
37
|
+
}
|
|
38
|
+
return swatch;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
describe('<swatch-ui> default-slot + label-attr precedence (FB-36 / §325)', () => {
|
|
42
|
+
let host;
|
|
43
|
+
|
|
44
|
+
beforeEach(() => {
|
|
45
|
+
host = document.createElement('div');
|
|
46
|
+
document.body.appendChild(host);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('preserves default-slot element when label="X" is also present (FB-36 §325 fix)', async () => {
|
|
50
|
+
const swatch = buildSwatch({
|
|
51
|
+
label: '500',
|
|
52
|
+
defaultSlot: { id: 'probe-default', text: 'richlabel' },
|
|
53
|
+
});
|
|
54
|
+
host.appendChild(swatch);
|
|
55
|
+
await new Promise((r) => setTimeout(r, 80));
|
|
56
|
+
|
|
57
|
+
const label = swatch.querySelector('[data-label]');
|
|
58
|
+
const span = swatch.querySelector('#probe-default');
|
|
59
|
+
|
|
60
|
+
// Pre-§325 the span was wiped + label="500" took precedence.
|
|
61
|
+
// Post-§325 the default-slot wins (yaml spec: "richer content").
|
|
62
|
+
expect(span).not.toBeNull();
|
|
63
|
+
expect(label.textContent.trim()).toBe('richlabel');
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it('renders label-attr text when no default-slot is present', async () => {
|
|
67
|
+
const swatch = buildSwatch({ label: '500' });
|
|
68
|
+
host.appendChild(swatch);
|
|
69
|
+
await new Promise((r) => setTimeout(r, 80));
|
|
70
|
+
const label = swatch.querySelector('[data-label]');
|
|
71
|
+
expect(label.textContent.trim()).toBe('500');
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it('renders default-slot text when no label-attr is present', async () => {
|
|
75
|
+
const swatch = buildSwatch({
|
|
76
|
+
defaultSlot: { id: 'probe-only', text: 'defaultonly' },
|
|
77
|
+
});
|
|
78
|
+
host.appendChild(swatch);
|
|
79
|
+
await new Promise((r) => setTimeout(r, 80));
|
|
80
|
+
const label = swatch.querySelector('[data-label]');
|
|
81
|
+
const span = swatch.querySelector('#probe-only');
|
|
82
|
+
expect(span).not.toBeNull();
|
|
83
|
+
expect(label.textContent.trim()).toBe('defaultonly');
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it('syncs label-attr changes when there is no default-slot (no whitespace regression)', async () => {
|
|
87
|
+
const swatch = buildSwatch({ label: '500' });
|
|
88
|
+
host.appendChild(swatch);
|
|
89
|
+
await new Promise((r) => setTimeout(r, 80));
|
|
90
|
+
swatch.label = '600';
|
|
91
|
+
await new Promise((r) => setTimeout(r, 80));
|
|
92
|
+
const label = swatch.querySelector('[data-label]');
|
|
93
|
+
expect(label.textContent.trim()).toBe('600');
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
describe('<swatch-ui> late-arriving [slot="chrome"] children (FB-37 / §326)', () => {
|
|
98
|
+
let host;
|
|
99
|
+
|
|
100
|
+
beforeEach(() => {
|
|
101
|
+
host = document.createElement('div');
|
|
102
|
+
document.body.appendChild(host);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it('absorbs chrome child appended AFTER connectedCallback (template-render path)', async () => {
|
|
106
|
+
// Mimic the template-engine timing: create the swatch + connect it FIRST
|
|
107
|
+
// (children empty), then append the chrome child + trigger a render.
|
|
108
|
+
const swatch = document.createElement('swatch-ui');
|
|
109
|
+
swatch.setAttribute('shape', 'block');
|
|
110
|
+
swatch.setAttribute('label-position', 'overlay');
|
|
111
|
+
swatch.setAttribute('color', '#3b82f6');
|
|
112
|
+
swatch.setAttribute('label', '500');
|
|
113
|
+
host.appendChild(swatch);
|
|
114
|
+
await new Promise((r) => setTimeout(r, 80));
|
|
115
|
+
|
|
116
|
+
// At this point #stamp() has run + chromeSlot was empty.
|
|
117
|
+
// Append a chrome child LATER — this is what template engines do.
|
|
118
|
+
const chrome = document.createElement('span');
|
|
119
|
+
chrome.setAttribute('slot', 'chrome');
|
|
120
|
+
chrome.id = 'late-chrome';
|
|
121
|
+
chrome.textContent = '★';
|
|
122
|
+
swatch.appendChild(chrome);
|
|
123
|
+
|
|
124
|
+
// Trigger a re-render via property mutation (template engines typically
|
|
125
|
+
// set props after appending — this nudge fires render → #syncCore →
|
|
126
|
+
// #absorbChromeSlot picks up the late chrome child). Must use a value
|
|
127
|
+
// DIFFERENT from current to actually fire the signal write.
|
|
128
|
+
swatch.label = '999';
|
|
129
|
+
await new Promise((r) => setTimeout(r, 80));
|
|
130
|
+
|
|
131
|
+
const found = swatch.querySelector('#late-chrome');
|
|
132
|
+
expect(found).not.toBeNull();
|
|
133
|
+
// Chrome child should be a direct host sibling positioned between tile + badge.
|
|
134
|
+
const tile = swatch.querySelector('[data-tile]');
|
|
135
|
+
const badge = swatch.querySelector('[data-badge]');
|
|
136
|
+
expect(found.previousElementSibling).toBe(tile);
|
|
137
|
+
expect(found.nextElementSibling).toBe(badge);
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it('still handles chrome child appended BEFORE connect (static-HTML path)', async () => {
|
|
141
|
+
// Build swatch with chrome child present at connect time — the canonical
|
|
142
|
+
// FB-34 / §314 path should still work post-§326.
|
|
143
|
+
const swatch = document.createElement('swatch-ui');
|
|
144
|
+
swatch.setAttribute('shape', 'block');
|
|
145
|
+
swatch.setAttribute('label-position', 'overlay');
|
|
146
|
+
swatch.setAttribute('color', '#3b82f6');
|
|
147
|
+
swatch.setAttribute('label', '500');
|
|
148
|
+
const chrome = document.createElement('span');
|
|
149
|
+
chrome.setAttribute('slot', 'chrome');
|
|
150
|
+
chrome.id = 'early-chrome';
|
|
151
|
+
chrome.textContent = '⚑';
|
|
152
|
+
swatch.appendChild(chrome);
|
|
153
|
+
host.appendChild(swatch);
|
|
154
|
+
await new Promise((r) => setTimeout(r, 80));
|
|
155
|
+
|
|
156
|
+
const found = swatch.querySelector('#early-chrome');
|
|
157
|
+
expect(found).not.toBeNull();
|
|
158
|
+
const tile = swatch.querySelector('[data-tile]');
|
|
159
|
+
const badge = swatch.querySelector('[data-badge]');
|
|
160
|
+
expect(found.previousElementSibling).toBe(tile);
|
|
161
|
+
expect(found.nextElementSibling).toBe(badge);
|
|
162
|
+
});
|
|
163
|
+
});
|
|
@@ -59,7 +59,7 @@ switch-ui[checked] [slot="thumb"] {
|
|
|
59
59
|
--switch-thumb-travel: calc(var(--switch-track-width) - var(--switch-thumb-size) - 2 * var(--switch-thumb-inset) - 2px);
|
|
60
60
|
|
|
61
61
|
/* ── State: disabled ── */
|
|
62
|
-
--switch-fg-disabled: var(--a-
|
|
62
|
+
--switch-fg-disabled: var(--a-ui-text-disabled);
|
|
63
63
|
}
|
|
64
64
|
|
|
65
65
|
:scope {
|
package/components/tabs/tabs.css
CHANGED
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
--tabs-fg: var(--a-fg-subtle);
|
|
14
14
|
--tabs-fg-hover: var(--a-fg);
|
|
15
15
|
--tabs-fg-active: var(--a-fg-selected);
|
|
16
|
-
--tabs-fg-disabled: var(--a-
|
|
16
|
+
--tabs-fg-disabled: var(--a-ui-text-disabled);
|
|
17
17
|
|
|
18
18
|
/* ── Spacing ── */
|
|
19
19
|
--tabs-vertical-gap: var(--a-space-4);
|
|
@@ -36,7 +36,7 @@
|
|
|
36
36
|
"default": false
|
|
37
37
|
},
|
|
38
38
|
"variant": {
|
|
39
|
-
"description": "Typography variant — sets role tokens (size/weight/
|
|
39
|
+
"description": "Typography variant — sets design-role tokens (size / weight /\ntracking / color / leading) per the L0 typography token family.\n**PRESENTATIONAL-ONLY (§247, FB-23 §2).** `<text-ui variant=\"heading\">`\ndoes NOT set `role=\"heading\"` + `aria-level` on the host. Document-\noutline assistive technology will treat the element as `role=\"generic\"`.\nFor semantic headings, wrap with native `<h1>`-`<h6>` OR add\n`role=\"heading\" aria-level=\"N\"` on the host directly. For visual-only\nlabels (eyebrows, kickers, captions, deck), the presentational default\nis correct. The §221k chooser guide in USAGE.md documents picker heuristics.",
|
|
40
40
|
"type": "string",
|
|
41
41
|
"enum": [
|
|
42
42
|
"body",
|
|
@@ -52,7 +52,21 @@
|
|
|
52
52
|
"metric",
|
|
53
53
|
"code"
|
|
54
54
|
],
|
|
55
|
-
"default": "body"
|
|
55
|
+
"default": "body",
|
|
56
|
+
"enum_descriptions": {
|
|
57
|
+
"title": "Page title (visual rank H1). Largest under display. Use at the top of an authoritative page or dialog.",
|
|
58
|
+
"body": "Default body copy. 14px / regular. Paragraphs, multi-line content, running prose.",
|
|
59
|
+
"caption": "Annotation under a primary line — smaller + muted. Use for image captions, footnotes.",
|
|
60
|
+
"code": "Inline monospace code reference. Use for inline code within prose.",
|
|
61
|
+
"deck": "Sub-title under a `title`. One-line lead, slightly larger than body. Use for the lead sentence after a title.",
|
|
62
|
+
"display": "Top-level hero / brand display. Tallest visual rank. Use for page-level hero one-liners.",
|
|
63
|
+
"heading": "Major page heading (visual rank H2). 16-18px / bold. Use for major sub-section dividers.",
|
|
64
|
+
"kicker": "Eyebrow text above a `title`. UPPERCASE + small + tracking. Use for content eyebrows (NOT form labels — use `label` for those).",
|
|
65
|
+
"label": "Form-control label (above an `<input-ui>` / `<select-ui>` etc). UI-sized + medium-weight. Use for field labels bound to form controls.",
|
|
66
|
+
"metric": "Numeric KPI / big-number stat. Bold + large. Use for dashboard metric numbers.",
|
|
67
|
+
"section": "Inline form-group / navlist heading (visual rank H4). Small-cap. Use for form group labels, nav list headings.",
|
|
68
|
+
"subsection": "Sub-landmark within a section (visual rank H3). 14px / semibold. Use for card titles within a section."
|
|
69
|
+
}
|
|
56
70
|
}
|
|
57
71
|
},
|
|
58
72
|
"required": [
|
|
@@ -1,41 +1,28 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* `<text-ui>` — Typography wrapper that applies role tokens. Supports
|
|
3
|
-
* truncation and line clamping.
|
|
2
|
+
* `<text-ui>` — Typography wrapper that applies role tokens. Supports truncation and line clamping.
|
|
4
3
|
*
|
|
5
4
|
* @see https://ui-kit.exe.xyz/site/components/text
|
|
6
5
|
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
* Also carries the FB-23 §2 ARIA-role disclaimer on the property doc.
|
|
13
|
-
* `dts-codegen.mjs` keeps this file in its `HAND_AUTHORED_DTS` skip list.
|
|
6
|
+
* Type declarations generated by scripts/build/dts-codegen.mjs from
|
|
7
|
+
* the component's `.a2ui.json` sidecar(s). Edit the source `.yaml`,
|
|
8
|
+
* run `npm run build:components`, then `npm run codegen:dts` to
|
|
9
|
+
* regenerate; or hand-author this file fully if rich event types are
|
|
10
|
+
* needed beyond what the yaml `events:` block can express.
|
|
14
11
|
*/
|
|
15
12
|
|
|
16
13
|
import { UIElement } from '../../core/element.js';
|
|
17
14
|
|
|
18
|
-
/**
|
|
19
|
-
* Typography variants — visual rank from `display` (largest, hero) to
|
|
20
|
-
* `code` (inline monospace). Pick by intent, not by visual size — every
|
|
21
|
-
* variant has a distinct typographic role per §221k chooser guide.
|
|
22
|
-
*
|
|
23
|
-
* See `packages/web-components/USAGE.md` "v0.5.9 — Consumer-feedback
|
|
24
|
-
* discoverability sweep" §221k for the picker heuristics.
|
|
25
|
-
*/
|
|
26
15
|
export type UITextVariant =
|
|
27
16
|
/** Default body copy. 14px / regular. Paragraphs, multi-line content, running prose. */
|
|
28
17
|
| 'body'
|
|
29
|
-
/** Top-level hero / brand display. Tallest visual rank. Use for page-level hero one-liners. */
|
|
30
|
-
| 'display'
|
|
31
|
-
/** Page title (visual rank H1). Largest under display. Use at the top of an authoritative page or dialog. */
|
|
32
|
-
| 'title'
|
|
33
18
|
/** Major page heading (visual rank H2). 16-18px / bold. Use for major sub-section dividers. */
|
|
34
19
|
| 'heading'
|
|
20
|
+
/** Page title (visual rank H1). Largest under display. Use at the top of an authoritative page or dialog. */
|
|
21
|
+
| 'title'
|
|
35
22
|
/** Sub-landmark within a section (visual rank H3). 14px / semibold. Use for card titles within a section. */
|
|
36
23
|
| 'subsection'
|
|
37
|
-
/**
|
|
38
|
-
| '
|
|
24
|
+
/** Top-level hero / brand display. Tallest visual rank. Use for page-level hero one-liners. */
|
|
25
|
+
| 'display'
|
|
39
26
|
/** Annotation under a primary line — smaller + muted. Use for image captions, footnotes. */
|
|
40
27
|
| 'caption'
|
|
41
28
|
/** Form-control label (above an `<input-ui>` / `<select-ui>` etc). UI-sized + medium-weight. Use for field labels bound to form controls. */
|
|
@@ -44,34 +31,30 @@ export type UITextVariant =
|
|
|
44
31
|
| 'kicker'
|
|
45
32
|
/** Sub-title under a `title`. One-line lead, slightly larger than body. Use for the lead sentence after a title. */
|
|
46
33
|
| 'deck'
|
|
34
|
+
/** Inline form-group / navlist heading (visual rank H4). Small-cap. Use for form group labels, nav list headings. */
|
|
35
|
+
| 'section'
|
|
47
36
|
/** Numeric KPI / big-number stat. Bold + large. Use for dashboard metric numbers. */
|
|
48
37
|
| 'metric'
|
|
49
|
-
/** Inline monospace code reference. Use for
|
|
38
|
+
/** Inline monospace code reference. Use for inline code within prose. */
|
|
50
39
|
| 'code';
|
|
51
40
|
|
|
52
41
|
export class UIText extends UIElement {
|
|
53
|
-
/**
|
|
54
|
-
* Typography variant — sets design-role tokens (size / weight / tracking /
|
|
55
|
-
* color / leading) per the L0 typography token family.
|
|
56
|
-
*
|
|
57
|
-
* **PRESENTATIONAL-ONLY (§247, FB-23 §2).** `<text-ui variant="heading">`
|
|
58
|
-
* does NOT set `role="heading"` + `aria-level` on the host. Document-outline
|
|
59
|
-
* assistive technology will treat the element as `role="generic"`. For
|
|
60
|
-
* **semantic** headings (screen-reader heading-navigation, document
|
|
61
|
-
* outline), wrap with native `<h1>`-`<h6>` OR add
|
|
62
|
-
* `role="heading" aria-level="N"` on the host element directly.
|
|
63
|
-
*
|
|
64
|
-
* For visual-only labels (eyebrows, kickers, captions, deck), the
|
|
65
|
-
* presentational default is correct. The §221k chooser guide in USAGE.md
|
|
66
|
-
* documents the picker heuristics.
|
|
67
|
-
*/
|
|
68
|
-
variant: UITextVariant;
|
|
69
|
-
/** When true, applies stronger emphasis (heavier weight + accent color). Styled via `:scope[strong]` in text.css. Use instead of `variant="heading"` when you want a single emphasized word inline in body copy. */
|
|
70
|
-
strong: boolean;
|
|
71
|
-
/** Single-line truncation with ellipsis. Ignored when `lines` is set. */
|
|
72
|
-
truncate: boolean;
|
|
73
|
-
/** Multi-line clamp count (0 = no clamp). Sets `--_text-lines` for `-webkit-line-clamp`. */
|
|
42
|
+
/** Multi-line clamp count (0 = no clamp) */
|
|
74
43
|
lines: number;
|
|
44
|
+
/** When true, applies stronger emphasis (heavier weight + accent color). Styled via :scope[strong] in text.css. Use instead of variant=heading when you want a single emphasized word inline in body copy. */
|
|
45
|
+
strong: boolean;
|
|
75
46
|
/** Display text content. The main payload field for Text components extracted from HTML. */
|
|
76
47
|
textContent: string;
|
|
48
|
+
/** Single-line truncation with ellipsis. Ignored when `lines` is set. */
|
|
49
|
+
truncate: boolean;
|
|
50
|
+
/** Typography variant — sets design-role tokens (size / weight /
|
|
51
|
+
tracking / color / leading) per the L0 typography token family.
|
|
52
|
+
**PRESENTATIONAL-ONLY (§247, FB-23 §2).** `<text-ui variant="heading">`
|
|
53
|
+
does NOT set `role="heading"` + `aria-level` on the host. Document-
|
|
54
|
+
outline assistive technology will treat the element as `role="generic"`.
|
|
55
|
+
For semantic headings, wrap with native `<h1>`-`<h6>` OR add
|
|
56
|
+
`role="heading" aria-level="N"` on the host directly. For visual-only
|
|
57
|
+
labels (eyebrows, kickers, captions, deck), the presentational default
|
|
58
|
+
is correct. The §221k chooser guide in USAGE.md documents picker heuristics. */
|
|
59
|
+
variant: UITextVariant;
|
|
77
60
|
}
|