@adia-ai/web-components 0.4.8 → 0.4.9
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 +255 -2
- package/components/action-list/action-list.a2ui.json +15 -1
- package/components/action-list/action-list.d.ts +10 -1
- package/components/action-list/action-list.yaml +10 -0
- package/components/agent-artifact/agent-artifact.a2ui.json +7 -1
- package/components/agent-artifact/agent-artifact.d.ts +6 -1
- package/components/agent-artifact/agent-artifact.yaml +4 -0
- package/components/agent-feedback-bar/agent-feedback-bar.a2ui.json +7 -1
- package/components/agent-feedback-bar/agent-feedback-bar.d.ts +6 -1
- package/components/agent-feedback-bar/agent-feedback-bar.yaml +4 -0
- package/components/agent-questions/agent-questions.a2ui.json +11 -1
- package/components/agent-questions/agent-questions.d.ts +8 -1
- package/components/agent-questions/agent-questions.yaml +7 -0
- package/components/agent-reasoning/agent-reasoning.a2ui.json +25 -3
- package/components/agent-reasoning/agent-reasoning.d.ts +20 -3
- package/components/agent-reasoning/agent-reasoning.yaml +15 -0
- package/components/agent-suggestions/agent-suggestions.a2ui.json +15 -1
- package/components/agent-suggestions/agent-suggestions.d.ts +10 -1
- package/components/agent-suggestions/agent-suggestions.yaml +10 -0
- package/components/agent-trace/agent-trace.a2ui.json +7 -1
- package/components/agent-trace/agent-trace.d.ts +6 -1
- package/components/agent-trace/agent-trace.yaml +4 -0
- package/components/canvas/canvas.yaml +9 -7
- package/components/chart/chart.a2ui.json +3 -0
- package/components/chart/chart.d.ts +2 -0
- package/components/chart/chart.yaml +5 -0
- package/components/chart-legend/chart-legend.a2ui.json +15 -1
- package/components/chart-legend/chart-legend.d.ts +10 -1
- package/components/chart-legend/chart-legend.yaml +10 -0
- package/components/chat-thread/chat-thread.a2ui.json +11 -1
- package/components/chat-thread/chat-thread.d.ts +8 -1
- package/components/chat-thread/chat-thread.yaml +7 -0
- package/components/code/code.a2ui.json +36 -7
- package/components/code/code.d.ts +30 -0
- package/components/code/code.yaml +29 -6
- package/components/color-picker/class.js +59 -1
- package/components/color-picker/color-picker.a2ui.json +34 -0
- package/components/color-picker/color-picker.d.ts +70 -8
- package/components/color-picker/color-picker.yaml +49 -0
- package/components/command/command.a2ui.json +11 -1
- package/components/command/command.d.ts +8 -1
- package/components/command/command.yaml +7 -0
- package/components/demo-toggle/demo-toggle.a2ui.json +7 -1
- package/components/demo-toggle/demo-toggle.d.ts +6 -1
- package/components/demo-toggle/demo-toggle.yaml +4 -0
- package/components/heatmap/heatmap.a2ui.json +11 -2
- package/components/heatmap/heatmap.d.ts +6 -0
- package/components/heatmap/heatmap.yaml +17 -2
- package/components/link/link.a2ui.json +11 -1
- package/components/link/link.d.ts +8 -1
- package/components/link/link.yaml +7 -0
- package/components/list/list.a2ui.json +11 -1
- package/components/list/list.d.ts +8 -1
- package/components/list/list.yaml +7 -0
- package/components/menu/menu.a2ui.json +11 -1
- package/components/menu/menu.d.ts +8 -1
- package/components/menu/menu.yaml +7 -0
- package/components/nav/nav.a2ui.json +15 -1
- package/components/nav/nav.d.ts +10 -1
- package/components/nav/nav.yaml +10 -0
- package/components/nav-group/nav-group.a2ui.json +11 -1
- package/components/nav-group/nav-group.d.ts +8 -1
- package/components/nav-group/nav-group.yaml +7 -0
- package/components/nav-item/nav-item.a2ui.json +15 -1
- package/components/nav-item/nav-item.d.ts +10 -1
- package/components/nav-item/nav-item.yaml +10 -0
- package/components/noodles/noodles.a2ui.json +46 -2
- package/components/noodles/noodles.d.ts +28 -2
- package/components/noodles/noodles.yaml +32 -0
- package/components/otp-input/otp-input.a2ui.json +14 -2
- package/components/otp-input/otp-input.d.ts +11 -0
- package/components/otp-input/otp-input.yaml +10 -2
- package/components/pagination/pagination.a2ui.json +7 -1
- package/components/pagination/pagination.d.ts +6 -1
- package/components/pagination/pagination.yaml +4 -0
- package/components/stream/stream.a2ui.json +7 -1
- package/components/stream/stream.d.ts +6 -1
- package/components/stream/stream.yaml +4 -0
- package/components/swatch/class.js +362 -15
- package/components/swatch/swatch.a2ui.json +68 -1
- package/components/swatch/swatch.css +150 -0
- package/components/swatch/swatch.d.ts +43 -0
- package/components/swatch/swatch.yaml +67 -1
- package/components/swiper/swiper.a2ui.json +18 -2
- package/components/swiper/swiper.d.ts +14 -2
- package/components/swiper/swiper.yaml +11 -0
- package/components/table/table.a2ui.json +80 -5
- package/components/table/table.d.ts +58 -5
- package/components/table/table.yaml +54 -2
- package/components/tabs/tabs.a2ui.json +7 -1
- package/components/tabs/tabs.d.ts +6 -1
- package/components/tabs/tabs.yaml +4 -0
- package/components/tag/tag.a2ui.json +11 -1
- package/components/tag/tag.d.ts +8 -1
- package/components/tag/tag.yaml +7 -0
- package/components/timeline/timeline.a2ui.json +3 -7
- package/components/timeline/timeline.d.ts +2 -4
- package/components/timeline/timeline.yaml +3 -6
- package/components/toggle-group/toggle-group.a2ui.json +7 -1
- package/components/toggle-group/toggle-group.d.ts +6 -1
- package/components/toggle-group/toggle-group.yaml +4 -0
- package/components/toggle-scheme/toggle-scheme.a2ui.json +11 -1
- package/components/toggle-scheme/toggle-scheme.d.ts +8 -1
- package/components/toggle-scheme/toggle-scheme.yaml +7 -0
- package/components/tree/tree.a2ui.json +15 -1
- package/components/tree/tree.d.ts +10 -1
- package/components/tree/tree.yaml +10 -0
- package/index.d.ts +9 -2
- package/package.json +1 -1
|
@@ -12,6 +12,16 @@ export interface OtpInputEventDetail {
|
|
|
12
12
|
}
|
|
13
13
|
export type OtpInputEvent = CustomEvent<OtpInputEventDetail>;
|
|
14
14
|
|
|
15
|
+
/**
|
|
16
|
+
* Detail payload for the `complete` event — fired exactly once when the
|
|
17
|
+
* user fills the last digit slot. `value` is the full combined string at
|
|
18
|
+
* the moment of completion.
|
|
19
|
+
*/
|
|
20
|
+
export interface OtpInputCompleteEventDetail {
|
|
21
|
+
value: string;
|
|
22
|
+
}
|
|
23
|
+
export type OtpInputCompleteEvent = CustomEvent<OtpInputCompleteEventDetail>;
|
|
24
|
+
|
|
15
25
|
export class UIOtpInput extends UIFormElement {
|
|
16
26
|
/** Number of digit slots. */
|
|
17
27
|
length: number;
|
|
@@ -22,4 +32,5 @@ export class UIOtpInput extends UIFormElement {
|
|
|
22
32
|
options?: boolean | AddEventListenerOptions,
|
|
23
33
|
): void;
|
|
24
34
|
addEventListener(type: 'input', listener: (ev: OtpInputEvent) => unknown, options?: boolean | AddEventListenerOptions): void;
|
|
35
|
+
addEventListener(type: 'complete', listener: (ev: OtpInputCompleteEvent) => unknown, options?: boolean | AddEventListenerOptions): void;
|
|
25
36
|
}
|
|
@@ -25,9 +25,17 @@ props:
|
|
|
25
25
|
default: ''
|
|
26
26
|
events:
|
|
27
27
|
complete:
|
|
28
|
-
description: Fired when
|
|
28
|
+
description: Fired exactly once when the user fills the last digit slot. detail.value is the combined string.
|
|
29
|
+
detail:
|
|
30
|
+
value:
|
|
31
|
+
type: string
|
|
32
|
+
description: Combined digits (e.g. "123456").
|
|
29
33
|
input:
|
|
30
|
-
description: Fired on every digit change
|
|
34
|
+
description: Fired on every digit change. detail.value is the current combined string.
|
|
35
|
+
detail:
|
|
36
|
+
value:
|
|
37
|
+
type: string
|
|
38
|
+
description: Combined digits at the moment of the change.
|
|
31
39
|
slots:
|
|
32
40
|
field:
|
|
33
41
|
description: Container for the digit input boxes and optional separator
|
|
@@ -47,7 +47,13 @@
|
|
|
47
47
|
"composes": [],
|
|
48
48
|
"events": {
|
|
49
49
|
"page-change": {
|
|
50
|
-
"description": "Fired when a page button is clicked. detail contains { page }."
|
|
50
|
+
"description": "Fired when a page button is clicked. detail contains { page }.",
|
|
51
|
+
"detail": {
|
|
52
|
+
"page": {
|
|
53
|
+
"description": "New page number.",
|
|
54
|
+
"type": "number"
|
|
55
|
+
}
|
|
56
|
+
}
|
|
51
57
|
}
|
|
52
58
|
},
|
|
53
59
|
"examples": [
|
|
@@ -12,7 +12,12 @@
|
|
|
12
12
|
|
|
13
13
|
import { UIElement } from '../../core/element.js';
|
|
14
14
|
|
|
15
|
-
export
|
|
15
|
+
export interface PaginationPageChangeEventDetail {
|
|
16
|
+
/** New page number. */
|
|
17
|
+
page: number;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export type PaginationPageChangeEvent = CustomEvent<PaginationPageChangeEventDetail>;
|
|
16
21
|
|
|
17
22
|
export class UIPagination extends UIElement {
|
|
18
23
|
/** Current active page number. */
|
|
@@ -45,7 +45,13 @@
|
|
|
45
45
|
"description": "Fired when stream completes naturally"
|
|
46
46
|
},
|
|
47
47
|
"stream-error": {
|
|
48
|
-
"description": "Fired on stream error, detail: { error }"
|
|
48
|
+
"description": "Fired on stream error, detail: { error }",
|
|
49
|
+
"detail": {
|
|
50
|
+
"error": {
|
|
51
|
+
"description": "Error object or message.",
|
|
52
|
+
"type": "object"
|
|
53
|
+
}
|
|
54
|
+
}
|
|
49
55
|
},
|
|
50
56
|
"stream-start": {
|
|
51
57
|
"description": "Fired when streaming begins"
|
|
@@ -13,7 +13,12 @@
|
|
|
13
13
|
import { UIElement } from '../../core/element.js';
|
|
14
14
|
|
|
15
15
|
export type StreamEndEvent = CustomEvent<unknown>;
|
|
16
|
-
export
|
|
16
|
+
export interface StreamErrorEventDetail {
|
|
17
|
+
/** Error object or message. */
|
|
18
|
+
error: Record<string, unknown>;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export type StreamErrorEvent = CustomEvent<StreamErrorEventDetail>;
|
|
17
22
|
export type StreamStartEvent = CustomEvent<unknown>;
|
|
18
23
|
|
|
19
24
|
export class UIStream extends UIElement {
|
|
@@ -25,6 +25,10 @@ events:
|
|
|
25
25
|
description: Fired when stream completes naturally
|
|
26
26
|
stream-error:
|
|
27
27
|
description: "Fired on stream error, detail: { error }"
|
|
28
|
+
detail:
|
|
29
|
+
error:
|
|
30
|
+
type: object
|
|
31
|
+
description: Error object or message.
|
|
28
32
|
stream-start:
|
|
29
33
|
description: Fired when streaming begins
|
|
30
34
|
slots:
|
|
@@ -19,59 +19,201 @@
|
|
|
19
19
|
* theme-preview pill. Replaces docs-only `<span data-swatch>` usage and
|
|
20
20
|
* the per-shape CSS that used to live inside chart-legend-ui.
|
|
21
21
|
*
|
|
22
|
+
* v0.4.9 extends the primitive with the design-token-tile use case
|
|
23
|
+
* (FEEDBACK-04 #2): badge marker, second-line detail, copy-to-clipboard
|
|
24
|
+
* button, keyboard-selectable + reflected `[selected]` state.
|
|
25
|
+
*
|
|
22
26
|
* Usage:
|
|
23
27
|
* <swatch-ui shape="dot" color="var(--a-data-0)" label="Revenue"></swatch-ui>
|
|
24
28
|
* <swatch-ui shape="block" size="lg" color="var(--a-neutral-50)" label="50"></swatch-ui>
|
|
25
29
|
* <swatch-ui shape="line" color="#0ea5e9">Forecast</swatch-ui>
|
|
26
30
|
*
|
|
31
|
+
* <!-- design-token tile with full feature set -->
|
|
32
|
+
* <swatch-ui
|
|
33
|
+
* shape="block" size="lg"
|
|
34
|
+
* color="oklch(0.53 0.18 240)"
|
|
35
|
+
* label="500"
|
|
36
|
+
* detail="oklch(0.53 0.18 240)"
|
|
37
|
+
* badge="apca-pass"
|
|
38
|
+
* copyable
|
|
39
|
+
* selectable
|
|
40
|
+
* ></swatch-ui>
|
|
41
|
+
*
|
|
27
42
|
* Attributes:
|
|
28
|
-
* shape
|
|
29
|
-
* size
|
|
30
|
-
* color
|
|
31
|
-
* label
|
|
43
|
+
* shape block | dot | square | line | dashed (default square)
|
|
44
|
+
* size sm | md | lg (default md)
|
|
45
|
+
* color any CSS color (or var() ref)
|
|
46
|
+
* label text label rendered beside / below the swatch
|
|
47
|
+
* detail secondary line of text (e.g. raw token value)
|
|
48
|
+
* badge out-of-gamut | p3-only | apca-pass | apca-fail (optional marker)
|
|
49
|
+
* copyable boolean — render an inline copy button
|
|
50
|
+
* copy-value string — value to copy (defaults to [color] when copyable)
|
|
51
|
+
* selectable boolean — make the swatch focusable + clickable
|
|
52
|
+
* selected boolean — reflected visual selected state
|
|
53
|
+
*
|
|
54
|
+
* Events:
|
|
55
|
+
* select dispatched when a selectable swatch is activated
|
|
56
|
+
* (click / Enter / Space). detail: { value, color, label }.
|
|
32
57
|
*
|
|
33
58
|
* Slots:
|
|
34
|
-
* default
|
|
59
|
+
* default rich label content; takes precedence over [label] attr
|
|
60
|
+
*
|
|
61
|
+
* Stamping is light-DOM with up to five children:
|
|
62
|
+
* <span data-tile></span> colored shape
|
|
63
|
+
* <span data-badge></span> gamut / contrast marker (optional)
|
|
64
|
+
* <span data-label></span> label text
|
|
65
|
+
* <span data-detail></span> secondary line (optional)
|
|
66
|
+
* <button data-copy></button> copy-to-clipboard (optional)
|
|
35
67
|
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
68
|
+
* Accessibility:
|
|
69
|
+
* - When [selectable], host is role="button" + tabindex="0";
|
|
70
|
+
* Enter / Space activate.
|
|
71
|
+
* - When [selected], aria-pressed="true" reflects the state.
|
|
72
|
+
* - The badge has aria-label (mapped per variant) so screen readers
|
|
73
|
+
* surface "out of gamut", "P3-only", etc.
|
|
39
74
|
*/
|
|
40
75
|
|
|
41
76
|
import { UIElement } from '../../core/element.js';
|
|
42
77
|
|
|
43
78
|
const SHAPES = new Set(['block', 'dot', 'square', 'line', 'dashed']);
|
|
44
79
|
const SIZES = new Set(['sm', 'md', 'lg']);
|
|
80
|
+
const BADGE_VARIANTS = new Set(['out-of-gamut', 'p3-only', 'apca-pass', 'apca-fail']);
|
|
81
|
+
|
|
82
|
+
const BADGE_SYMBOLS = {
|
|
83
|
+
'out-of-gamut': '△',
|
|
84
|
+
'p3-only': '✦',
|
|
85
|
+
'apca-pass': '✓',
|
|
86
|
+
'apca-fail': '✗',
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
const BADGE_LABELS = {
|
|
90
|
+
'out-of-gamut': 'Outside sRGB gamut',
|
|
91
|
+
'p3-only': 'Display-P3 only',
|
|
92
|
+
'apca-pass': 'Contrast passes APCA',
|
|
93
|
+
'apca-fail': 'Contrast fails APCA',
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Parse the [badge] attribute into a normalized array. Supports:
|
|
98
|
+
* badge="apca-pass" → ['apca-pass']
|
|
99
|
+
* badge="out-of-gamut, apca-fail" → ['out-of-gamut', 'apca-fail']
|
|
100
|
+
* badge="out-of-gamut apca-fail" → ['out-of-gamut', 'apca-fail']
|
|
101
|
+
* badge="" → []
|
|
102
|
+
* Drops unknown variants silently (consumer-typed-by-attr defense).
|
|
103
|
+
*/
|
|
104
|
+
function parseBadges(value) {
|
|
105
|
+
if (!value) return [];
|
|
106
|
+
return value
|
|
107
|
+
.split(/[\s,]+/)
|
|
108
|
+
.map((s) => s.trim())
|
|
109
|
+
.filter((s) => s && BADGE_VARIANTS.has(s));
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Compute perceived lightness (OKLCH L, 0–1) of a CSS color string. Returns
|
|
114
|
+
* null when the color isn't a hex / oklch / rgb / named CSS color that we
|
|
115
|
+
* can parse. Used to pick a light vs dark label color for auto-contrast.
|
|
116
|
+
*
|
|
117
|
+
* Strategy: render the color into a temporary canvas, read the sRGB pixel,
|
|
118
|
+
* convert to linear-light sRGB → OKLab L. This is ~3x cheaper than parsing
|
|
119
|
+
* the CSS color string ourselves and handles every CSS color form the
|
|
120
|
+
* browser supports (oklch, hsl, named, hex, currentColor via fallback).
|
|
121
|
+
*/
|
|
122
|
+
let _lumProbeCtx = null;
|
|
123
|
+
function colorLuminance(cssColor) {
|
|
124
|
+
if (!cssColor || typeof cssColor !== 'string') return null;
|
|
125
|
+
if (typeof document === 'undefined') return null;
|
|
126
|
+
try {
|
|
127
|
+
if (!_lumProbeCtx) {
|
|
128
|
+
const canvas = document.createElement('canvas');
|
|
129
|
+
canvas.width = 1;
|
|
130
|
+
canvas.height = 1;
|
|
131
|
+
_lumProbeCtx = canvas.getContext('2d');
|
|
132
|
+
}
|
|
133
|
+
_lumProbeCtx.fillStyle = '#000';
|
|
134
|
+
_lumProbeCtx.clearRect(0, 0, 1, 1);
|
|
135
|
+
_lumProbeCtx.fillStyle = cssColor;
|
|
136
|
+
_lumProbeCtx.fillRect(0, 0, 1, 1);
|
|
137
|
+
const [r, g, b] = _lumProbeCtx.getImageData(0, 0, 1, 1).data;
|
|
138
|
+
if (r === 0 && g === 0 && b === 0 && cssColor !== '#000' && cssColor !== 'black' && cssColor !== '#000000') {
|
|
139
|
+
// Canvas rejected the color string (e.g. var() that doesn't resolve in this context).
|
|
140
|
+
return null;
|
|
141
|
+
}
|
|
142
|
+
// sRGB → linear-light, then OKLab L (Björn Ottosson's approximation).
|
|
143
|
+
const lin = (c) => {
|
|
144
|
+
const x = c / 255;
|
|
145
|
+
return x <= 0.04045 ? x / 12.92 : Math.pow((x + 0.055) / 1.055, 2.4);
|
|
146
|
+
};
|
|
147
|
+
const lr = lin(r), lg = lin(g), lb = lin(b);
|
|
148
|
+
const l_ = Math.cbrt(0.4122214708 * lr + 0.5363325363 * lg + 0.0514459929 * lb);
|
|
149
|
+
const m_ = Math.cbrt(0.2119034982 * lr + 0.6806995451 * lg + 0.1073969566 * lb);
|
|
150
|
+
const s_ = Math.cbrt(0.0883024619 * lr + 0.2817188376 * lg + 0.6299787005 * lb);
|
|
151
|
+
return 0.2104542553 * l_ + 0.7936177850 * m_ - 0.0040720468 * s_;
|
|
152
|
+
} catch {
|
|
153
|
+
return null;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
45
156
|
|
|
46
157
|
export class UISwatch extends UIElement {
|
|
47
158
|
static properties = {
|
|
48
|
-
shape:
|
|
49
|
-
size:
|
|
50
|
-
color:
|
|
51
|
-
label:
|
|
159
|
+
shape: { type: String, default: 'square', reflect: true },
|
|
160
|
+
size: { type: String, default: 'md', reflect: true },
|
|
161
|
+
color: { type: String, default: '', reflect: true },
|
|
162
|
+
label: { type: String, default: '', reflect: true },
|
|
163
|
+
detail: { type: String, default: '', reflect: true },
|
|
164
|
+
badge: { type: String, default: '', reflect: true },
|
|
165
|
+
copyable: { type: Boolean, default: false, reflect: true },
|
|
166
|
+
copyValue: { type: String, default: '' }, // not reflected — long color strings as attrs read poorly
|
|
167
|
+
selectable: { type: Boolean, default: false, reflect: true },
|
|
168
|
+
selected: { type: Boolean, default: false, reflect: true },
|
|
169
|
+
autoContrast: { type: Boolean, default: false, reflect: true },
|
|
52
170
|
};
|
|
53
171
|
|
|
54
172
|
static template = () => null;
|
|
55
173
|
|
|
56
174
|
#tileEl = null;
|
|
57
175
|
#labelEl = null;
|
|
176
|
+
#detailEl = null;
|
|
177
|
+
#badgeEl = null;
|
|
178
|
+
#copyEl = null;
|
|
58
179
|
#stamped = false;
|
|
180
|
+
#lastColorForContrast = null;
|
|
181
|
+
|
|
182
|
+
// Bound handlers — held so we can remove them in disconnected().
|
|
183
|
+
#onHostClick = null;
|
|
184
|
+
#onHostKey = null;
|
|
185
|
+
#onCopyClick = null;
|
|
186
|
+
#copyResetTimer = null;
|
|
59
187
|
|
|
60
188
|
connected() {
|
|
61
189
|
this.#stamp();
|
|
190
|
+
this.#wireInteraction();
|
|
62
191
|
}
|
|
63
192
|
|
|
64
193
|
render() {
|
|
65
194
|
this.#stamp();
|
|
66
|
-
this.#
|
|
195
|
+
this.#syncCore();
|
|
196
|
+
this.#syncBadge();
|
|
197
|
+
this.#syncDetail();
|
|
198
|
+
this.#syncCopy();
|
|
199
|
+
this.#syncSelectable();
|
|
200
|
+
this.#syncAutoContrast();
|
|
67
201
|
}
|
|
68
202
|
|
|
203
|
+
// ── Stamping ──────────────────────────────────────────────────────
|
|
204
|
+
|
|
69
205
|
#stamp() {
|
|
70
206
|
if (this.#stamped) return;
|
|
71
207
|
// Capture pre-existing default-slot content so consumer-authored
|
|
72
208
|
// children (e.g. <swatch-ui>Forecast</swatch-ui>) survive stamping.
|
|
73
209
|
const slotted = Array.from(this.childNodes).filter(n =>
|
|
74
|
-
!(n.nodeType === 1 &&
|
|
210
|
+
!(n.nodeType === 1 && n.dataset && (
|
|
211
|
+
n.dataset.tile !== undefined ||
|
|
212
|
+
n.dataset.label !== undefined ||
|
|
213
|
+
n.dataset.detail !== undefined ||
|
|
214
|
+
n.dataset.badge !== undefined ||
|
|
215
|
+
n.dataset.copy !== undefined
|
|
216
|
+
))
|
|
75
217
|
);
|
|
76
218
|
this.innerHTML = '';
|
|
77
219
|
|
|
@@ -80,6 +222,13 @@ export class UISwatch extends UIElement {
|
|
|
80
222
|
this.#tileEl.setAttribute('aria-hidden', 'true');
|
|
81
223
|
this.appendChild(this.#tileEl);
|
|
82
224
|
|
|
225
|
+
// Badge container — holds one or more <span data-badge-variant="..."> children.
|
|
226
|
+
// Multi-badge support added in v0.4.9 §92 (FEEDBACK-04 follow-up).
|
|
227
|
+
this.#badgeEl = document.createElement('span');
|
|
228
|
+
this.#badgeEl.setAttribute('data-badge', '');
|
|
229
|
+
this.#badgeEl.setAttribute('hidden', '');
|
|
230
|
+
this.appendChild(this.#badgeEl);
|
|
231
|
+
|
|
83
232
|
this.#labelEl = document.createElement('span');
|
|
84
233
|
this.#labelEl.setAttribute('data-label', '');
|
|
85
234
|
if (slotted.length) {
|
|
@@ -87,10 +236,25 @@ export class UISwatch extends UIElement {
|
|
|
87
236
|
}
|
|
88
237
|
this.appendChild(this.#labelEl);
|
|
89
238
|
|
|
239
|
+
this.#detailEl = document.createElement('span');
|
|
240
|
+
this.#detailEl.setAttribute('data-detail', '');
|
|
241
|
+
this.#detailEl.setAttribute('hidden', '');
|
|
242
|
+
this.appendChild(this.#detailEl);
|
|
243
|
+
|
|
244
|
+
this.#copyEl = document.createElement('button');
|
|
245
|
+
this.#copyEl.setAttribute('data-copy', '');
|
|
246
|
+
this.#copyEl.setAttribute('type', 'button');
|
|
247
|
+
this.#copyEl.setAttribute('aria-label', 'Copy value');
|
|
248
|
+
this.#copyEl.setAttribute('hidden', '');
|
|
249
|
+
this.#copyEl.textContent = '⧉';
|
|
250
|
+
this.appendChild(this.#copyEl);
|
|
251
|
+
|
|
90
252
|
this.#stamped = true;
|
|
91
253
|
}
|
|
92
254
|
|
|
93
|
-
|
|
255
|
+
// ── Sync ──────────────────────────────────────────────────────────
|
|
256
|
+
|
|
257
|
+
#syncCore() {
|
|
94
258
|
if (!this.#tileEl || !this.#labelEl) return;
|
|
95
259
|
|
|
96
260
|
// Normalize enum values; fall back silently when a consumer sets a typo.
|
|
@@ -123,9 +287,192 @@ export class UISwatch extends UIElement {
|
|
|
123
287
|
this.#labelEl.toggleAttribute('hidden', !hasLabel);
|
|
124
288
|
}
|
|
125
289
|
|
|
290
|
+
#syncBadge() {
|
|
291
|
+
if (!this.#badgeEl) return;
|
|
292
|
+
const badges = parseBadges(this.badge);
|
|
293
|
+
if (badges.length === 0) {
|
|
294
|
+
this.#badgeEl.setAttribute('hidden', '');
|
|
295
|
+
this.#badgeEl.replaceChildren();
|
|
296
|
+
return;
|
|
297
|
+
}
|
|
298
|
+
this.#badgeEl.removeAttribute('hidden');
|
|
299
|
+
// Diff against existing children — replace only if the variant set changed.
|
|
300
|
+
const declared = Array.from(this.#badgeEl.children).map((c) => c.dataset.badgeVariant);
|
|
301
|
+
const same = declared.length === badges.length && declared.every((v, i) => v === badges[i]);
|
|
302
|
+
if (same) return;
|
|
303
|
+
const frag = document.createDocumentFragment();
|
|
304
|
+
for (const variant of badges) {
|
|
305
|
+
const pip = document.createElement('span');
|
|
306
|
+
pip.setAttribute('data-badge-variant', variant);
|
|
307
|
+
pip.setAttribute('role', 'img');
|
|
308
|
+
pip.setAttribute('aria-label', BADGE_LABELS[variant]);
|
|
309
|
+
pip.textContent = BADGE_SYMBOLS[variant];
|
|
310
|
+
frag.appendChild(pip);
|
|
311
|
+
}
|
|
312
|
+
this.#badgeEl.replaceChildren(frag);
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/**
|
|
316
|
+
* Auto-contrast — switch the label color between light/dark when the
|
|
317
|
+
* resolved swatch color crosses an OKLab L threshold. Uses a 1px canvas
|
|
318
|
+
* probe to handle any CSS color form (oklch / hex / hsl / named / var()).
|
|
319
|
+
* Pre-resolves currentColor by way of computed style so consumers can
|
|
320
|
+
* pass var() refs and get correct math.
|
|
321
|
+
*/
|
|
322
|
+
#syncAutoContrast() {
|
|
323
|
+
if (!this.#labelEl) return;
|
|
324
|
+
if (!this.autoContrast) {
|
|
325
|
+
this.#labelEl.removeAttribute('data-on-light');
|
|
326
|
+
this.#labelEl.removeAttribute('data-on-dark');
|
|
327
|
+
this.#lastColorForContrast = null;
|
|
328
|
+
return;
|
|
329
|
+
}
|
|
330
|
+
// Resolve the actual rendered color. For var(...) refs the inline
|
|
331
|
+
// [color] attr is what was set; we read from computed style so that
|
|
332
|
+
// CSS-resolved tokens (e.g. var(--a-accent)) get their final value.
|
|
333
|
+
const probe = this.color || getComputedStyle(this).getPropertyValue('--swatch-color').trim();
|
|
334
|
+
if (probe === this.#lastColorForContrast) return;
|
|
335
|
+
this.#lastColorForContrast = probe;
|
|
336
|
+
const L = colorLuminance(probe);
|
|
337
|
+
if (L == null) {
|
|
338
|
+
this.#labelEl.removeAttribute('data-on-light');
|
|
339
|
+
this.#labelEl.removeAttribute('data-on-dark');
|
|
340
|
+
return;
|
|
341
|
+
}
|
|
342
|
+
// Threshold ~0.62 OKLab L matches WCAG-aligned light/dark text choice
|
|
343
|
+
// for sRGB. Adjust by --swatch-auto-contrast-threshold if needed.
|
|
344
|
+
const onDark = L < 0.62;
|
|
345
|
+
this.#labelEl.toggleAttribute('data-on-dark', onDark);
|
|
346
|
+
this.#labelEl.toggleAttribute('data-on-light', !onDark);
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
#syncDetail() {
|
|
350
|
+
if (!this.#detailEl) return;
|
|
351
|
+
const text = (this.detail || '').trim();
|
|
352
|
+
if (!text) {
|
|
353
|
+
this.#detailEl.setAttribute('hidden', '');
|
|
354
|
+
this.#detailEl.textContent = '';
|
|
355
|
+
return;
|
|
356
|
+
}
|
|
357
|
+
this.#detailEl.removeAttribute('hidden');
|
|
358
|
+
if (this.#detailEl.textContent !== text) this.#detailEl.textContent = text;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
#syncCopy() {
|
|
362
|
+
if (!this.#copyEl) return;
|
|
363
|
+
if (!this.copyable) {
|
|
364
|
+
this.#copyEl.setAttribute('hidden', '');
|
|
365
|
+
return;
|
|
366
|
+
}
|
|
367
|
+
this.#copyEl.removeAttribute('hidden');
|
|
368
|
+
// aria-label reflects what will be copied so screen readers
|
|
369
|
+
// announce the target value.
|
|
370
|
+
const value = this.#copyTargetValue();
|
|
371
|
+
if (value) this.#copyEl.setAttribute('aria-label', `Copy ${value}`);
|
|
372
|
+
else this.#copyEl.setAttribute('aria-label', 'Copy value');
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
#syncSelectable() {
|
|
376
|
+
if (this.selectable) {
|
|
377
|
+
if (!this.hasAttribute('role')) this.setAttribute('role', 'button');
|
|
378
|
+
if (!this.hasAttribute('tabindex')) this.setAttribute('tabindex', '0');
|
|
379
|
+
this.setAttribute('aria-pressed', this.selected ? 'true' : 'false');
|
|
380
|
+
} else {
|
|
381
|
+
// Only remove ARIA we set; respect consumer-set roles.
|
|
382
|
+
if (this.getAttribute('role') === 'button') this.removeAttribute('role');
|
|
383
|
+
if (this.getAttribute('tabindex') === '0') this.removeAttribute('tabindex');
|
|
384
|
+
this.removeAttribute('aria-pressed');
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
// ── Interaction wiring ────────────────────────────────────────────
|
|
389
|
+
|
|
390
|
+
#wireInteraction() {
|
|
391
|
+
this.#onHostClick = (e) => {
|
|
392
|
+
if (!this.selectable) return;
|
|
393
|
+
// Ignore clicks that originated in the copy button — that's a
|
|
394
|
+
// separate affordance and should not also toggle selection.
|
|
395
|
+
const path = e.composedPath ? e.composedPath() : [];
|
|
396
|
+
if (path.includes(this.#copyEl)) return;
|
|
397
|
+
this.#activate();
|
|
398
|
+
};
|
|
399
|
+
this.#onHostKey = (e) => {
|
|
400
|
+
if (!this.selectable) return;
|
|
401
|
+
if (e.key !== 'Enter' && e.key !== ' ') return;
|
|
402
|
+
// Don't steal Space when focus is in the copy button.
|
|
403
|
+
if (e.target === this.#copyEl) return;
|
|
404
|
+
e.preventDefault();
|
|
405
|
+
this.#activate();
|
|
406
|
+
};
|
|
407
|
+
this.addEventListener('click', this.#onHostClick);
|
|
408
|
+
this.addEventListener('keydown', this.#onHostKey);
|
|
409
|
+
|
|
410
|
+
this.#onCopyClick = (e) => {
|
|
411
|
+
e.stopPropagation();
|
|
412
|
+
if (!this.copyable) return;
|
|
413
|
+
const value = this.#copyTargetValue();
|
|
414
|
+
if (!value) return;
|
|
415
|
+
this.#doCopy(value);
|
|
416
|
+
};
|
|
417
|
+
this.#copyEl.addEventListener('click', this.#onCopyClick);
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
#activate() {
|
|
421
|
+
this.dispatchEvent(new CustomEvent('select', {
|
|
422
|
+
bubbles: true,
|
|
423
|
+
detail: {
|
|
424
|
+
value: this.color || this.label,
|
|
425
|
+
color: this.color,
|
|
426
|
+
label: this.label,
|
|
427
|
+
},
|
|
428
|
+
}));
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
#copyTargetValue() {
|
|
432
|
+
if (this.copyValue) return this.copyValue;
|
|
433
|
+
if (this.color) return this.color;
|
|
434
|
+
return '';
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
async #doCopy(value) {
|
|
438
|
+
let ok = false;
|
|
439
|
+
try {
|
|
440
|
+
if (navigator?.clipboard?.writeText) {
|
|
441
|
+
await navigator.clipboard.writeText(value);
|
|
442
|
+
ok = true;
|
|
443
|
+
}
|
|
444
|
+
} catch { /* ignore — feedback below */ }
|
|
445
|
+
this.#flashCopyResult(ok);
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
#flashCopyResult(ok) {
|
|
449
|
+
if (!this.#copyEl) return;
|
|
450
|
+
if (this.#copyResetTimer != null) clearTimeout(this.#copyResetTimer);
|
|
451
|
+
this.#copyEl.setAttribute('data-copy-state', ok ? 'ok' : 'fail');
|
|
452
|
+
this.#copyEl.textContent = ok ? '✓' : '⚠';
|
|
453
|
+
this.#copyResetTimer = setTimeout(() => {
|
|
454
|
+
if (!this.#copyEl) return;
|
|
455
|
+
this.#copyEl.removeAttribute('data-copy-state');
|
|
456
|
+
this.#copyEl.textContent = '⧉';
|
|
457
|
+
this.#copyResetTimer = null;
|
|
458
|
+
}, 1200);
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
// ── Teardown ──────────────────────────────────────────────────────
|
|
462
|
+
|
|
126
463
|
disconnected() {
|
|
464
|
+
if (this.#copyResetTimer != null) {
|
|
465
|
+
clearTimeout(this.#copyResetTimer);
|
|
466
|
+
this.#copyResetTimer = null;
|
|
467
|
+
}
|
|
468
|
+
if (this.#onHostClick) this.removeEventListener('click', this.#onHostClick);
|
|
469
|
+
if (this.#onHostKey) this.removeEventListener('keydown', this.#onHostKey);
|
|
470
|
+
if (this.#copyEl && this.#onCopyClick) this.#copyEl.removeEventListener('click', this.#onCopyClick);
|
|
127
471
|
this.#tileEl = null;
|
|
128
472
|
this.#labelEl = null;
|
|
473
|
+
this.#detailEl = null;
|
|
474
|
+
this.#badgeEl = null;
|
|
475
|
+
this.#copyEl = null;
|
|
129
476
|
this.#stamped = false;
|
|
130
477
|
}
|
|
131
478
|
}
|