@adia-ai/web-components 0.4.7 → 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/README.md +39 -0
- package/USAGE.md +255 -2
- package/components/accordion/accordion.a2ui.json +3 -0
- package/components/accordion/accordion.d.ts +12 -2
- package/components/accordion/accordion.yaml +4 -0
- package/components/action-list/action-list.a2ui.json +18 -1
- package/components/action-list/action-list.d.ts +21 -2
- package/components/action-list/action-list.yaml +14 -0
- package/components/agent-artifact/agent-artifact.a2ui.json +11 -1
- package/components/agent-artifact/agent-artifact.d.ts +17 -2
- package/components/agent-artifact/agent-artifact.yaml +9 -0
- package/components/agent-feedback-bar/agent-feedback-bar.a2ui.json +10 -1
- package/components/agent-feedback-bar/agent-feedback-bar.d.ts +19 -2
- package/components/agent-feedback-bar/agent-feedback-bar.yaml +8 -0
- package/components/agent-questions/agent-questions.a2ui.json +14 -1
- package/components/agent-questions/agent-questions.d.ts +19 -2
- package/components/agent-questions/agent-questions.yaml +11 -0
- package/components/agent-reasoning/agent-reasoning.a2ui.json +29 -3
- package/components/agent-reasoning/agent-reasoning.d.ts +33 -2
- package/components/agent-reasoning/agent-reasoning.yaml +20 -0
- package/components/agent-suggestions/agent-suggestions.a2ui.json +18 -1
- package/components/agent-suggestions/agent-suggestions.d.ts +21 -2
- package/components/agent-suggestions/agent-suggestions.yaml +14 -0
- package/components/agent-trace/agent-trace.a2ui.json +8 -1
- package/components/agent-trace/agent-trace.d.ts +17 -2
- package/components/agent-trace/agent-trace.yaml +4 -0
- package/components/alert/alert.a2ui.json +1 -0
- package/components/alert/alert.d.ts +12 -2
- package/components/aside/aside.a2ui.json +1 -0
- package/components/avatar/avatar.a2ui.json +3 -0
- package/components/avatar/avatar.d.ts +3 -2
- package/components/avatar/avatar.yaml +4 -0
- package/components/badge/badge.a2ui.json +3 -0
- package/components/badge/badge.d.ts +3 -2
- package/components/badge/badge.yaml +4 -0
- package/components/block/block.a2ui.json +1 -0
- package/components/block/block.d.ts +3 -2
- package/components/breadcrumb/breadcrumb.a2ui.json +5 -0
- package/components/breadcrumb/breadcrumb.d.ts +3 -2
- package/components/breadcrumb/breadcrumb.yaml +6 -0
- package/components/button/button.a2ui.json +3 -0
- package/components/button/button.d.ts +12 -2
- package/components/button/button.yaml +5 -0
- package/components/calendar-picker/calendar-picker.a2ui.json +1 -0
- package/components/canvas/canvas.a2ui.json +1 -0
- package/components/canvas/canvas.d.ts +18 -2
- package/components/canvas/canvas.yaml +12 -0
- package/components/card/card.a2ui.json +1 -0
- package/components/card/card.d.ts +12 -2
- package/components/chart/chart.a2ui.json +4 -0
- package/components/chart/chart.d.ts +18 -2
- package/components/chart/chart.yaml +5 -0
- package/components/chart-legend/chart-legend.a2ui.json +19 -1
- package/components/chart-legend/chart-legend.d.ts +21 -2
- package/components/chart-legend/chart-legend.yaml +15 -0
- package/components/chat-thread/chat-thread.a2ui.json +12 -1
- package/components/chat-thread/chat-thread.d.ts +19 -2
- package/components/chat-thread/chat-thread.yaml +7 -0
- package/components/check/check.a2ui.json +1 -0
- package/components/code/code.a2ui.json +37 -7
- package/components/code/code.d.ts +30 -0
- package/components/code/code.yaml +29 -6
- package/components/col/col.a2ui.json +1 -0
- package/components/col/col.d.ts +3 -2
- package/components/color-picker/class.js +59 -1
- package/components/color-picker/color-picker.a2ui.json +37 -0
- package/components/color-picker/color-picker.d.ts +70 -8
- package/components/color-picker/color-picker.yaml +53 -0
- package/components/command/command.a2ui.json +12 -1
- package/components/command/command.d.ts +21 -2
- package/components/command/command.yaml +7 -0
- package/components/demo-toggle/demo-toggle.a2ui.json +8 -1
- package/components/demo-toggle/demo-toggle.d.ts +17 -2
- package/components/demo-toggle/demo-toggle.yaml +4 -0
- package/components/description-list/description-list.a2ui.json +1 -0
- package/components/description-list/description-list.d.ts +3 -2
- package/components/divider/divider.a2ui.json +1 -0
- package/components/divider/divider.d.ts +3 -2
- package/components/drawer/drawer.a2ui.json +1 -0
- package/components/drawer/drawer.d.ts +12 -2
- package/components/embed/embed.a2ui.json +1 -0
- package/components/embed/embed.d.ts +3 -2
- package/components/empty-state/empty-state.a2ui.json +3 -0
- package/components/empty-state/empty-state.d.ts +3 -2
- package/components/empty-state/empty-state.yaml +4 -0
- package/components/feed/feed.a2ui.json +9 -1
- package/components/feed/feed.d.ts +12 -2
- package/components/feed/feed.yaml +8 -1
- package/components/field/field.a2ui.json +1 -0
- package/components/field/field.d.ts +3 -2
- package/components/fields/fields.a2ui.json +1 -0
- package/components/fields/fields.d.ts +3 -2
- package/components/footer/footer.a2ui.json +1 -0
- package/components/grid/grid.a2ui.json +1 -0
- package/components/grid/grid.d.ts +3 -2
- package/components/header/header.a2ui.json +1 -0
- package/components/heatmap/heatmap.a2ui.json +12 -2
- package/components/heatmap/heatmap.d.ts +20 -2
- package/components/heatmap/heatmap.yaml +17 -2
- package/components/icon/icon.a2ui.json +1 -0
- package/components/icon/icon.d.ts +3 -2
- package/components/image/image.a2ui.json +3 -0
- package/components/image/image.d.ts +3 -2
- package/components/image/image.yaml +4 -0
- package/components/index.js +8 -0
- package/components/input/input.a2ui.json +4 -0
- package/components/input/input.yaml +6 -0
- package/components/inspector/inspector.a2ui.json +5 -0
- package/components/inspector/inspector.d.ts +3 -2
- package/components/inspector/inspector.yaml +6 -0
- package/components/kbd/kbd.a2ui.json +1 -0
- package/components/kbd/kbd.d.ts +3 -2
- package/components/link/link.a2ui.json +12 -1
- package/components/link/link.d.ts +19 -2
- package/components/link/link.yaml +7 -0
- package/components/list/list.a2ui.json +14 -1
- package/components/list/list.d.ts +19 -2
- package/components/list/list.yaml +11 -0
- package/components/menu/menu.a2ui.json +14 -1
- package/components/menu/menu.d.ts +19 -2
- package/components/menu/menu.yaml +11 -0
- package/components/modal/modal.a2ui.json +1 -0
- package/components/modal/modal.d.ts +12 -2
- package/components/nav/nav.a2ui.json +16 -1
- package/components/nav/nav.d.ts +21 -2
- package/components/nav/nav.yaml +10 -0
- package/components/nav-group/nav-group.a2ui.json +12 -1
- package/components/nav-group/nav-group.d.ts +19 -2
- package/components/nav-group/nav-group.yaml +7 -0
- package/components/nav-item/nav-item.a2ui.json +16 -1
- package/components/nav-item/nav-item.d.ts +21 -2
- package/components/nav-item/nav-item.yaml +10 -0
- package/components/noodles/noodles.a2ui.json +47 -2
- package/components/noodles/noodles.d.ts +42 -2
- package/components/noodles/noodles.yaml +32 -0
- package/components/option-card/option-card.a2ui.json +3 -0
- package/components/option-card/option-card.yaml +4 -0
- package/components/otp-input/otp-input.a2ui.json +15 -2
- package/components/otp-input/otp-input.d.ts +11 -0
- package/components/otp-input/otp-input.yaml +10 -2
- package/components/page/page.a2ui.json +1 -0
- package/components/page/page.d.ts +3 -2
- package/components/pagination/pagination.a2ui.json +8 -1
- package/components/pagination/pagination.d.ts +17 -2
- package/components/pagination/pagination.yaml +4 -0
- package/components/pane/pane.a2ui.json +8 -1
- package/components/pane/pane.d.ts +12 -2
- package/components/pane/pane.yaml +7 -1
- package/components/pipeline-status/pipeline-status.a2ui.json +1 -0
- package/components/pipeline-status/pipeline-status.d.ts +3 -2
- package/components/popover/popover.a2ui.json +1 -0
- package/components/popover/popover.d.ts +3 -2
- package/components/progress/progress.a2ui.json +1 -0
- package/components/progress/progress.d.ts +3 -2
- package/components/progress-row/progress-row.a2ui.json +3 -0
- package/components/progress-row/progress-row.d.ts +3 -2
- package/components/progress-row/progress-row.yaml +4 -0
- package/components/radio/radio.a2ui.json +1 -0
- package/components/range/range.a2ui.json +1 -0
- package/components/rating/rating.a2ui.json +1 -0
- package/components/richtext/richtext.a2ui.json +1 -0
- package/components/richtext/richtext.d.ts +3 -2
- package/components/row/row.a2ui.json +1 -0
- package/components/row/row.d.ts +12 -2
- package/components/search/search.a2ui.json +1 -0
- package/components/section/section.a2ui.json +1 -0
- package/components/segment/segment.a2ui.json +3 -0
- package/components/segment/segment.d.ts +3 -2
- package/components/segment/segment.yaml +4 -0
- package/components/segmented/segmented.a2ui.json +1 -0
- package/components/select/select.a2ui.json +3 -0
- package/components/select/select.yaml +4 -0
- package/components/skeleton/skeleton.a2ui.json +1 -0
- package/components/skeleton/skeleton.d.ts +3 -2
- package/components/slider/slider.a2ui.json +1 -0
- package/components/stack/stack.a2ui.json +1 -0
- package/components/stack/stack.d.ts +3 -2
- package/components/stat/stat.a2ui.json +1 -0
- package/components/step-progress/step-progress.a2ui.json +1 -0
- package/components/step-progress/step-progress.d.ts +3 -2
- package/components/stepper/stepper.a2ui.json +3 -0
- package/components/stepper/stepper.d.ts +3 -2
- package/components/stepper/stepper.yaml +4 -0
- package/components/stream/stream.a2ui.json +8 -1
- package/components/stream/stream.d.ts +21 -2
- package/components/stream/stream.yaml +4 -0
- package/components/swatch/class.js +362 -15
- package/components/swatch/swatch.a2ui.json +69 -1
- package/components/swatch/swatch.css +150 -0
- package/components/swatch/swatch.d.ts +46 -2
- package/components/swatch/swatch.yaml +67 -1
- package/components/swiper/swiper.a2ui.json +21 -2
- package/components/swiper/swiper.d.ts +28 -2
- package/components/swiper/swiper.yaml +15 -0
- package/components/switch/switch.a2ui.json +1 -0
- package/components/table/table.a2ui.json +87 -5
- package/components/table/table.d.ts +73 -2
- package/components/table/table.yaml +62 -2
- package/components/table-toolbar/table-toolbar.a2ui.json +12 -0
- package/components/table-toolbar/table-toolbar.d.ts +18 -2
- package/components/table-toolbar/table-toolbar.yaml +13 -0
- package/components/tabs/tabs.a2ui.json +10 -1
- package/components/tabs/tabs.d.ts +17 -2
- package/components/tabs/tabs.yaml +8 -0
- package/components/tag/tag.a2ui.json +12 -1
- package/components/tag/tag.d.ts +19 -2
- package/components/tag/tag.yaml +7 -0
- package/components/text/text.a2ui.json +1 -0
- package/components/text/text.d.ts +3 -2
- package/components/textarea/textarea.a2ui.json +1 -0
- package/components/timeline/timeline.a2ui.json +14 -1
- package/components/timeline/timeline.d.ts +17 -2
- package/components/timeline/timeline.yaml +11 -1
- package/components/toast/toast.a2ui.json +1 -0
- package/components/toast/toast.d.ts +12 -2
- package/components/toggle-group/toggle-group.a2ui.json +8 -1
- package/components/toggle-group/toggle-group.d.ts +17 -2
- package/components/toggle-group/toggle-group.yaml +4 -0
- package/components/toggle-scheme/toggle-scheme.a2ui.json +14 -1
- package/components/toggle-scheme/toggle-scheme.d.ts +19 -2
- package/components/toggle-scheme/toggle-scheme.yaml +11 -0
- package/components/toolbar/toolbar.a2ui.json +3 -0
- package/components/toolbar/toolbar.d.ts +3 -2
- package/components/toolbar/toolbar.yaml +4 -0
- package/components/tooltip/tooltip.a2ui.json +1 -0
- package/components/tooltip/tooltip.d.ts +3 -2
- package/components/tree/tree.a2ui.json +18 -1
- package/components/tree/tree.d.ts +21 -2
- package/components/tree/tree.yaml +14 -0
- package/components/upload/upload.a2ui.json +1 -0
- package/core/icons-phosphor.js +93 -0
- package/core/icons.js +92 -90
- package/core/index.js +5 -0
- package/index.d.ts +87 -79
- package/index.js +7 -0
- package/package.json +3 -2
|
@@ -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
|
}
|
|
@@ -13,6 +13,16 @@
|
|
|
13
13
|
}
|
|
14
14
|
],
|
|
15
15
|
"properties": {
|
|
16
|
+
"autoContrast": {
|
|
17
|
+
"description": "When set, computes the swatch color's OKLab L and switches the label /\ndetail color between light + dark so it remains legible against the\ntile background. Only applies to shape=\"block\" (where the label sits\nON the tile). Uses a 1px canvas probe to handle any CSS color form\n(oklch / hex / hsl / named / var() references).\n",
|
|
18
|
+
"type": "boolean",
|
|
19
|
+
"default": false
|
|
20
|
+
},
|
|
21
|
+
"badge": {
|
|
22
|
+
"description": "Optional marker(s) rendered in the upper-right of the tile. Single value\nOR comma/space-separated list of variants (v0.4.9+ multi-badge support).\nVariants: out-of-gamut (△), p3-only (✦), apca-pass (✓), apca-fail (✗).\nEach pip carries an aria-label so screen readers surface the semantic\n(\"Outside sRGB gamut\", \"Contrast passes APCA\", etc.). Unknown variants\nare silently dropped.\n",
|
|
23
|
+
"type": "string",
|
|
24
|
+
"default": ""
|
|
25
|
+
},
|
|
16
26
|
"color": {
|
|
17
27
|
"description": "Swatch color. Accepts any CSS color value or var() reference. Sets the internal --swatch-color custom property; consumers can also set --swatch-color via inline style.",
|
|
18
28
|
"type": "string",
|
|
@@ -21,11 +31,36 @@
|
|
|
21
31
|
"component": {
|
|
22
32
|
"const": "Swatch"
|
|
23
33
|
},
|
|
34
|
+
"copyable": {
|
|
35
|
+
"description": "When set, renders an inline copy-to-clipboard button. Defaults to copying [color]; override via [copy-value].",
|
|
36
|
+
"type": "boolean",
|
|
37
|
+
"default": false
|
|
38
|
+
},
|
|
39
|
+
"copyValue": {
|
|
40
|
+
"description": "Override what gets copied when [copyable] is set. Defaults to [color]. Not reflected (color strings are long; attr round-tripping reads poorly).",
|
|
41
|
+
"type": "string",
|
|
42
|
+
"default": ""
|
|
43
|
+
},
|
|
44
|
+
"detail": {
|
|
45
|
+
"description": "Optional secondary line of text rendered below the label. Typically the raw token value (e.g. \"oklch(0.53 0.18 240)\") when the swatch represents a design-token step.",
|
|
46
|
+
"type": "string",
|
|
47
|
+
"default": ""
|
|
48
|
+
},
|
|
24
49
|
"label": {
|
|
25
50
|
"description": "Optional label rendered next to (or below, for shape=\"block\") the swatch. Use the default slot for richer content.",
|
|
26
51
|
"type": "string",
|
|
27
52
|
"default": ""
|
|
28
53
|
},
|
|
54
|
+
"selectable": {
|
|
55
|
+
"description": "When set, makes the swatch keyboard-focusable + clickable. Sets role=\"button\" + tabindex=\"0\". Dispatches a \"select\" event on activation (click / Enter / Space).",
|
|
56
|
+
"type": "boolean",
|
|
57
|
+
"default": false
|
|
58
|
+
},
|
|
59
|
+
"selected": {
|
|
60
|
+
"description": "Reflected visual selected state. Pair with [selectable] to make the swatch behave like a radio-style picker. Sets aria-pressed.",
|
|
61
|
+
"type": "boolean",
|
|
62
|
+
"default": false
|
|
63
|
+
},
|
|
29
64
|
"shape": {
|
|
30
65
|
"description": "Visual shape — block (filled tile), dot (small circle), square (small filled square), line (solid hairline), dashed (dashed hairline).",
|
|
31
66
|
"type": "string",
|
|
@@ -56,7 +91,26 @@
|
|
|
56
91
|
"x-adiaui": {
|
|
57
92
|
"anti_patterns": [],
|
|
58
93
|
"category": "display",
|
|
59
|
-
"
|
|
94
|
+
"composes": [],
|
|
95
|
+
"events": {
|
|
96
|
+
"select": {
|
|
97
|
+
"description": "Fired when a selectable swatch is activated (click / Enter / Space). detail carries the swatch's value, color, and label.",
|
|
98
|
+
"detail": {
|
|
99
|
+
"color": {
|
|
100
|
+
"description": "The raw [color] attribute value.",
|
|
101
|
+
"type": "string"
|
|
102
|
+
},
|
|
103
|
+
"label": {
|
|
104
|
+
"description": "The [label] attribute value.",
|
|
105
|
+
"type": "string"
|
|
106
|
+
},
|
|
107
|
+
"value": {
|
|
108
|
+
"description": "The swatch's effective value — [color] when present, else [label].",
|
|
109
|
+
"type": "string"
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
},
|
|
60
114
|
"examples": [
|
|
61
115
|
{
|
|
62
116
|
"description": "A single dot swatch with text label, e.g. as a chart-legend row.",
|
|
@@ -93,6 +147,11 @@
|
|
|
93
147
|
{
|
|
94
148
|
"description": "Default, ready for display.",
|
|
95
149
|
"name": "idle"
|
|
150
|
+
},
|
|
151
|
+
{
|
|
152
|
+
"description": "Reflected when the swatch is in the selected state (pair with [selectable]).",
|
|
153
|
+
"attribute": "selected",
|
|
154
|
+
"name": "selected"
|
|
96
155
|
}
|
|
97
156
|
],
|
|
98
157
|
"synonyms": {
|
|
@@ -106,8 +165,17 @@
|
|
|
106
165
|
},
|
|
107
166
|
"tag": "swatch-ui",
|
|
108
167
|
"tokens": {
|
|
168
|
+
"--swatch-badge-fg": {
|
|
169
|
+
"description": "Color of the badge symbol. Defaults to chrome-foreground; overridden per-badge variant."
|
|
170
|
+
},
|
|
109
171
|
"--swatch-color": {
|
|
110
172
|
"description": "Resolved color of the swatch fill / line. Wins over the [color] attr if set inline."
|
|
173
|
+
},
|
|
174
|
+
"--swatch-detail-fg": {
|
|
175
|
+
"description": "Color of the secondary detail line. Defaults to subtle-foreground."
|
|
176
|
+
},
|
|
177
|
+
"--swatch-select-ring": {
|
|
178
|
+
"description": "Color of the focus + selected ring when [selectable] is set."
|
|
111
179
|
}
|
|
112
180
|
},
|
|
113
181
|
"traits": [],
|