@adia-ai/web-components 0.8.41 → 0.8.42
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 +21 -0
- package/MIGRATION.md +187 -0
- package/USAGE.md +4 -4
- package/components/action-list/action-item.a2ui.json +8 -3
- package/components/action-list/action-item.yaml +25 -7
- package/components/action-list/action-list.class.js +68 -6
- package/components/action-list/action-list.d.ts +3 -1
- package/components/chart/chart.a2ui.json +18 -3
- package/components/chart/chart.class.js +58 -16
- package/components/chart/chart.d.ts +9 -3
- package/components/chart/chart.yaml +35 -3
- package/components/chart-legend/chart-legend.a2ui.json +6 -1
- package/components/chart-legend/chart-legend.class.js +59 -6
- package/components/chart-legend/chart-legend.css +5 -2
- package/components/chart-legend/chart-legend.d.ts +3 -1
- package/components/chart-legend/chart-legend.yaml +15 -1
- package/components/color-area/color-area.a2ui.json +148 -0
- package/components/color-area/color-area.class.js +657 -0
- package/components/color-area/color-area.css +188 -0
- package/components/color-area/color-area.d.ts +66 -0
- package/components/color-area/color-area.examples.md +19 -0
- package/components/color-area/color-area.js +17 -0
- package/components/color-area/color-area.yaml +183 -0
- package/components/color-input/color-input.a2ui.json +5 -5
- package/components/color-input/color-input.class.js +9 -6
- package/components/color-input/color-input.css +1 -1
- package/components/color-input/color-input.js +2 -2
- package/components/color-input/color-input.yaml +14 -12
- package/components/color-picker/color-picker.a2ui.json +15 -12
- package/components/color-picker/color-picker.class.js +24 -633
- package/components/color-picker/color-picker.css +14 -182
- package/components/color-picker/color-picker.yaml +41 -41
- package/components/description-list/description-list.a2ui.json +1 -1
- package/components/description-list/description-list.css +1 -1
- package/components/description-list/description-list.d.ts +1 -1
- package/components/description-list/description-list.yaml +1 -1
- package/components/field/field.a2ui.json +1 -1
- package/components/field/field.class.js +4 -1
- package/components/field/field.yaml +1 -1
- package/components/index.js +1 -0
- package/components/integration-card/integration-card.examples.md +2 -1
- package/components/menu/menu-item.a2ui.json +8 -3
- package/components/menu/menu-item.yaml +24 -3
- package/components/menu/menu.class.js +34 -6
- package/components/menu/menu.d.ts +3 -1
- package/components/pane/pane.a2ui.json +11 -1
- package/components/pane/pane.class.js +32 -3
- package/components/pane/pane.css +15 -10
- package/components/pane/pane.d.ts +10 -7
- package/components/pane/pane.yaml +15 -1
- package/components/swatch/swatch.yaml +1 -1
- package/components/table-toolbar/table-toolbar.class.js +15 -3
- package/core/icons.js +5 -0
- package/custom-elements.json +191 -16
- package/dist/theme-provider.min.js +1 -1
- package/dist/web-components.min.css +1 -1
- package/dist/web-components.min.js +89 -89
- package/dist/web-components.sheet.js +1 -1
- package/index.d.ts +12 -1
- package/package.json +1 -1
- package/patterns/admin-shell/admin-shell.examples.html +2 -2
- package/patterns/form-system/form-system.examples.html +1 -1
- package/styles/components.css +1 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Non-side-effect class export for `<color-picker-ui>`.
|
|
3
3
|
*
|
|
4
|
-
* Importing this file gives you the class
|
|
4
|
+
* Importing this file gives you the class without auto-registering the tag.
|
|
5
5
|
* Useful for test isolation, subclassing with tag-name override, or selective
|
|
6
6
|
* composition.
|
|
7
7
|
*
|
|
@@ -12,644 +12,35 @@
|
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
14
|
/**
|
|
15
|
-
* <color-picker-ui
|
|
15
|
+
* <color-picker-ui> — DEPRECATED (ADR-0063, gh#1563, gh#1377 B11).
|
|
16
16
|
*
|
|
17
|
-
*
|
|
17
|
+
* `-picker` is reserved for the outer, trigger + popover, form-associated
|
|
18
|
+
* composite shape (`calendar-picker-ui` / `date-range-picker-ui` /
|
|
19
|
+
* `datetime-picker-ui` / `time-picker-ui`) — `color-picker-ui` was the sole
|
|
20
|
+
* violator, playing `calendar-grid-ui`'s inline-substrate role under a
|
|
21
|
+
* `-picker` name. The real implementation moved to `<color-area-ui>`
|
|
22
|
+
* (mirrors `calendar-grid-ui`'s own plain, shape-describing substrate
|
|
23
|
+
* naming). `<color-picker-ui>` is now a thin compatibility alias per
|
|
24
|
+
* ADR-0057's `status: deprecated` shape (same pattern as
|
|
25
|
+
* `<datetime-picker-ui>` → `<calendar-picker-ui>`, gh#1372): it subclasses
|
|
26
|
+
* `UIColorArea` directly rather than re-implementing any behavior.
|
|
18
27
|
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
* L [────────────── slider ──────────────] 0.623
|
|
24
|
-
* [───────────────── hue track ──────────────────]
|
|
25
|
-
* oklch(0.62 0.188 260) [copy]
|
|
26
|
-
* #3b82f6 [copy]
|
|
28
|
+
* Migrate: `<color-picker-ui>` → `<color-area-ui>`. This tag keeps
|
|
29
|
+
* working — it IS `<color-area-ui>` under the hood — but will be removed
|
|
30
|
+
* in a future major; new markup should reach for `<color-area-ui>`
|
|
31
|
+
* directly.
|
|
27
32
|
*
|
|
28
|
-
*
|
|
29
|
-
* Form-associated via UIFormElement + ElementInternals.
|
|
33
|
+
* @see ../color-area/color-area.class.js
|
|
30
34
|
*/
|
|
31
35
|
|
|
32
|
-
import {
|
|
36
|
+
import { UIColorArea } from '../color-area/color-area.class.js';
|
|
33
37
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
const hRad = H * Math.PI / 180;
|
|
40
|
-
return { L, a: C * Math.cos(hRad), b: C * Math.sin(hRad) };
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
function oklabToLinearSrgb(L, a, b) {
|
|
44
|
-
const l_ = L + 0.3963377774 * a + 0.2158037573 * b;
|
|
45
|
-
const m_ = L - 0.1055613458 * a - 0.0638541728 * b;
|
|
46
|
-
const s_ = L - 0.0894841775 * a - 1.2914855480 * b;
|
|
47
|
-
const l = l_ * l_ * l_;
|
|
48
|
-
const m = m_ * m_ * m_;
|
|
49
|
-
const s = s_ * s_ * s_;
|
|
50
|
-
return [
|
|
51
|
-
+4.0767416621 * l - 3.3077115913 * m + 0.2309699292 * s,
|
|
52
|
-
-1.2684380046 * l + 2.6097574011 * m - 0.3413193965 * s,
|
|
53
|
-
-0.0041960863 * l - 0.7034186147 * m + 1.7076147010 * s,
|
|
54
|
-
];
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
function linearToSrgb(c) {
|
|
58
|
-
return c <= 0.0031308 ? 12.92 * c : 1.055 * Math.pow(c, 1 / 2.4) - 0.055;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
function srgbToLinear(c) {
|
|
62
|
-
return c <= 0.04045 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4);
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
function oklchToRgb(L, C, H) {
|
|
66
|
-
const { a, b } = oklchToOklab(L, C, H);
|
|
67
|
-
const [lr, lg, lb] = oklabToLinearSrgb(L, a, b);
|
|
68
|
-
return [
|
|
69
|
-
Math.max(0, Math.min(1, linearToSrgb(lr))),
|
|
70
|
-
Math.max(0, Math.min(1, linearToSrgb(lg))),
|
|
71
|
-
Math.max(0, Math.min(1, linearToSrgb(lb))),
|
|
72
|
-
];
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
function rgbToHex(r, g, b) {
|
|
76
|
-
const h = c => Math.round(c * 255).toString(16).padStart(2, '0');
|
|
77
|
-
return `#${h(r)}${h(g)}${h(b)}`;
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
function oklchToHex(L, C, H) {
|
|
81
|
-
const [r, g, b] = oklchToRgb(L, C, H);
|
|
82
|
-
return rgbToHex(r, g, b);
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
function hexToOklch(hex) {
|
|
86
|
-
hex = hex.replace('#', '');
|
|
87
|
-
if (hex.length === 3) hex = hex[0]+hex[0]+hex[1]+hex[1]+hex[2]+hex[2];
|
|
88
|
-
const r = srgbToLinear(parseInt(hex.slice(0,2),16)/255);
|
|
89
|
-
const g = srgbToLinear(parseInt(hex.slice(2,4),16)/255);
|
|
90
|
-
const b = srgbToLinear(parseInt(hex.slice(4,6),16)/255);
|
|
91
|
-
const l_ = Math.cbrt(0.4122214708*r + 0.5363325363*g + 0.0514459929*b);
|
|
92
|
-
const m_ = Math.cbrt(0.2119034982*r + 0.6806995451*g + 0.1073969566*b);
|
|
93
|
-
const s_ = Math.cbrt(0.0883024619*r + 0.2817188376*g + 0.6299787005*b);
|
|
94
|
-
const L = 0.2104542553*l_ + 0.7936177850*m_ - 0.0040720468*s_;
|
|
95
|
-
const a = 1.9779984951*l_ - 2.4285922050*m_ + 0.4505937099*s_;
|
|
96
|
-
const bv = 0.0259040371*l_ + 0.7827717662*m_ - 0.8086757660*s_;
|
|
97
|
-
const C = Math.sqrt(a*a + bv*bv);
|
|
98
|
-
let H = Math.atan2(bv, a) * 180 / Math.PI;
|
|
99
|
-
if (H < 0) H += 360;
|
|
100
|
-
return { L, C, H };
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
function isInGamut(r, g, b) {
|
|
104
|
-
const e = 0.001;
|
|
105
|
-
return r >= -e && r <= 1+e && g >= -e && g <= 1+e && b >= -e && b <= 1+e;
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
function gamutMapChroma(L, C, H) {
|
|
109
|
-
const { a, b } = oklchToOklab(L, C, H);
|
|
110
|
-
const [lr, lg, lb] = oklabToLinearSrgb(L, a, b);
|
|
111
|
-
if (isInGamut(linearToSrgb(lr), linearToSrgb(lg), linearToSrgb(lb))) return C;
|
|
112
|
-
let lo = 0, hi = C;
|
|
113
|
-
for (let i = 0; i < 8; i++) {
|
|
114
|
-
const mid = (lo + hi) / 2;
|
|
115
|
-
const { a: ma, b: mb } = oklchToOklab(L, mid, H);
|
|
116
|
-
const [mr, mg, mbb] = oklabToLinearSrgb(L, ma, mb);
|
|
117
|
-
if (isInGamut(linearToSrgb(mr), linearToSrgb(mg), linearToSrgb(mbb))) lo = mid;
|
|
118
|
-
else hi = mid;
|
|
119
|
-
}
|
|
120
|
-
return lo;
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
// ── Component ────────────────────────────────────────────
|
|
124
|
-
|
|
125
|
-
export class UIColorPicker extends UIFormElement {
|
|
126
|
-
// §154 (v0.5.3): Phosphor icons this primitive auto-stamps (without
|
|
127
|
-
// consumer markup). Aggregated by installIconLoadersForRegistered()
|
|
128
|
-
// across all defined elements. Audited by check-required-icons.mjs
|
|
129
|
-
// (slot 11). Per FEEDBACK-06 §4 + FEEDBACK-07 §4.
|
|
38
|
+
export class UIColorPicker extends UIColorArea {
|
|
39
|
+
// Same value as UIColorArea's — the alias stamps no icon of its own —
|
|
40
|
+
// but restated literally because scripts/release/check-required-icons.mjs
|
|
41
|
+
// text-scans each component's own class.js and cannot resolve inheritance
|
|
42
|
+
// (gh#1503, same rationale datetime-picker.class.js documents).
|
|
130
43
|
static requiredIcons = ['copy', 'check', 'warning'];
|
|
131
44
|
|
|
132
|
-
|
|
133
|
-
// input. Matches the v0.5.5 §184 §8 button-ui icon-only safety-net pattern
|
|
134
|
-
// + v0.5.7 §215 select-ui parseOptions pattern. Per FEEDBACK-13 §1 +
|
|
135
|
-
// FEEDBACK-14 §2 (co-credited).
|
|
136
|
-
static #warnedBadParse = new WeakSet();
|
|
137
|
-
|
|
138
|
-
static properties = {
|
|
139
|
-
...UIFormElement.properties,
|
|
140
|
-
value: { type: String, default: '#3b82f6', reflect: true },
|
|
141
|
-
format: { type: String, default: 'hex', reflect: true },
|
|
142
|
-
disabled: { type: Boolean, default: false, reflect: true },
|
|
143
|
-
/**
|
|
144
|
-
* v0.4.9 §99h — generation-constraint props (FEEDBACK-02 #7).
|
|
145
|
-
* When set, the picker clamps OKLCH channels to the consumer's
|
|
146
|
-
* declared bounds before committing. Out-of-bound mutations
|
|
147
|
-
* round-trip to the nearest in-bound equivalent + fire a
|
|
148
|
-
* `constraint-clamp` event with `{ axis, requested, clamped, reason }`
|
|
149
|
-
* so the consumer can surface UX feedback (toast, validity warning,
|
|
150
|
-
* scope-drift indicator). Default values disable the constraint.
|
|
151
|
-
*/
|
|
152
|
-
maxChroma: { type: Number, default: Infinity, reflect: true, attribute: 'max-chroma' },
|
|
153
|
-
maxL: { type: Number, default: 1, reflect: true, attribute: 'max-l' },
|
|
154
|
-
minL: { type: Number, default: 0, reflect: true, attribute: 'min-l' },
|
|
155
|
-
/** Maximum allowed hue deviation in degrees from the picker's `baseHue`. NaN = no constraint. */
|
|
156
|
-
hueDriftMax: { type: Number, default: NaN, reflect: true, attribute: 'hue-drift-max' },
|
|
157
|
-
/** Reference hue (degrees) for [hue-drift-max]. NaN = use the value parsed at first commit. */
|
|
158
|
-
baseHue: { type: Number, default: NaN, reflect: true, attribute: 'base-hue' },
|
|
159
|
-
};
|
|
160
|
-
|
|
161
|
-
#L = 0.6; #C = 0.15; #H = 230;
|
|
162
|
-
/** Cached reference hue for hue-drift constraint. Set on first commit if [base-hue] isn't authored. */
|
|
163
|
-
#resolvedBaseHue = NaN;
|
|
164
|
-
#bound = false;
|
|
165
|
-
#dragging = null; // 'area' | 'hue' | null
|
|
166
|
-
#internalUpdate = false;
|
|
167
|
-
|
|
168
|
-
// DOM refs
|
|
169
|
-
#canvas = null;
|
|
170
|
-
#ctx = null;
|
|
171
|
-
#areaEl = null;
|
|
172
|
-
#areaThumb = null;
|
|
173
|
-
#hueTrack = null;
|
|
174
|
-
#hueThumb = null;
|
|
175
|
-
#oklchVal = null;
|
|
176
|
-
#hexVal = null;
|
|
177
|
-
#areaResizeObs = null;
|
|
178
|
-
#hueSlider = null;
|
|
179
|
-
#chromaSlider = null;
|
|
180
|
-
#lightnessSlider = null;
|
|
181
|
-
#copyButtons = []; // [{ el, handler }]
|
|
182
|
-
#copyTimer = null;
|
|
183
|
-
|
|
184
|
-
static template = () => null;
|
|
185
|
-
|
|
186
|
-
// ── Stable listeners ──
|
|
187
|
-
|
|
188
|
-
#onSliderInput = (e) => {
|
|
189
|
-
const slider = e.currentTarget;
|
|
190
|
-
const channel = slider.getAttribute('data-channel');
|
|
191
|
-
if (channel === 'hue') this.#H = +slider.value;
|
|
192
|
-
else if (channel === 'chroma') this.#C = +slider.value;
|
|
193
|
-
else if (channel === 'lightness') this.#L = +slider.value;
|
|
194
|
-
this.#commit('input');
|
|
195
|
-
};
|
|
196
|
-
|
|
197
|
-
#onAreaPointerDown = (e) => {
|
|
198
|
-
if (this.disabled) return;
|
|
199
|
-
this.#dragging = 'area';
|
|
200
|
-
this.#areaEl.setPointerCapture(e.pointerId);
|
|
201
|
-
this.#updateAreaFromPointer(e);
|
|
202
|
-
};
|
|
203
|
-
|
|
204
|
-
#onAreaPointerMove = (e) => {
|
|
205
|
-
if (this.#dragging === 'area') this.#updateAreaFromPointer(e);
|
|
206
|
-
};
|
|
207
|
-
|
|
208
|
-
#onAreaPointerUp = () => {
|
|
209
|
-
if (this.#dragging === 'area') { this.#dragging = null; this.#commit('change'); }
|
|
210
|
-
};
|
|
211
|
-
|
|
212
|
-
#onHuePointerDown = (e) => {
|
|
213
|
-
if (this.disabled) return;
|
|
214
|
-
this.#dragging = 'hue';
|
|
215
|
-
this.#hueTrack.setPointerCapture(e.pointerId);
|
|
216
|
-
this.#updateHueFromPointer(e);
|
|
217
|
-
};
|
|
218
|
-
|
|
219
|
-
#onHuePointerMove = (e) => {
|
|
220
|
-
if (this.#dragging === 'hue') this.#updateHueFromPointer(e);
|
|
221
|
-
};
|
|
222
|
-
|
|
223
|
-
#onHuePointerUp = () => {
|
|
224
|
-
if (this.#dragging === 'hue') { this.#dragging = null; this.#commit('change'); }
|
|
225
|
-
};
|
|
226
|
-
|
|
227
|
-
#onAreaKeydown = (e) => {
|
|
228
|
-
if (this.disabled) return;
|
|
229
|
-
const step = e.shiftKey ? 0.05 : 0.01;
|
|
230
|
-
const cStep = e.shiftKey ? 0.04 : 0.004;
|
|
231
|
-
switch (e.key) {
|
|
232
|
-
case 'ArrowRight': e.preventDefault(); this.#C = Math.min(MAX_CHROMA, this.#C + cStep); break;
|
|
233
|
-
case 'ArrowLeft': e.preventDefault(); this.#C = Math.max(0, this.#C - cStep); break;
|
|
234
|
-
case 'ArrowUp': e.preventDefault(); this.#L = Math.min(1, this.#L + step); break;
|
|
235
|
-
case 'ArrowDown': e.preventDefault(); this.#L = Math.max(0, this.#L - step); break;
|
|
236
|
-
default: return;
|
|
237
|
-
}
|
|
238
|
-
this.#commit('change');
|
|
239
|
-
};
|
|
240
|
-
|
|
241
|
-
#onHueKeydown = (e) => {
|
|
242
|
-
if (this.disabled) return;
|
|
243
|
-
const step = e.shiftKey ? 10 : 1;
|
|
244
|
-
switch (e.key) {
|
|
245
|
-
case 'ArrowRight': case 'ArrowUp': e.preventDefault(); this.#H = (this.#H + step) % 360; break;
|
|
246
|
-
case 'ArrowLeft': case 'ArrowDown': e.preventDefault(); this.#H = (this.#H - step + 360) % 360; break;
|
|
247
|
-
default: return;
|
|
248
|
-
}
|
|
249
|
-
this.#commit('change');
|
|
250
|
-
};
|
|
251
|
-
|
|
252
|
-
connected() {
|
|
253
|
-
super.connected();
|
|
254
|
-
this.#parseValue(this.value);
|
|
255
|
-
|
|
256
|
-
if (this.#bound) return;
|
|
257
|
-
this.#bound = true;
|
|
258
|
-
|
|
259
|
-
// ── Build DOM ──
|
|
260
|
-
|
|
261
|
-
// Color area
|
|
262
|
-
this.#areaEl = document.createElement('div');
|
|
263
|
-
this.#areaEl.setAttribute('data-area', '');
|
|
264
|
-
this.#areaEl.setAttribute('tabindex', '0');
|
|
265
|
-
this.#areaEl.setAttribute('role', 'slider');
|
|
266
|
-
this.#areaEl.setAttribute('aria-label', 'Color area: chroma and lightness');
|
|
267
|
-
|
|
268
|
-
this.#canvas = document.createElement('canvas');
|
|
269
|
-
this.#canvas.style.cssText = 'width:100%;height:100%;display:block;border-radius:inherit;';
|
|
270
|
-
this.#areaEl.appendChild(this.#canvas);
|
|
271
|
-
this.#ctx = this.#canvas.getContext('2d', { willReadFrequently: true });
|
|
272
|
-
|
|
273
|
-
this.#areaThumb = document.createElement('div');
|
|
274
|
-
this.#areaThumb.setAttribute('data-area-thumb', '');
|
|
275
|
-
this.#areaEl.appendChild(this.#areaThumb);
|
|
276
|
-
|
|
277
|
-
this.appendChild(this.#areaEl);
|
|
278
|
-
|
|
279
|
-
// Slider rows
|
|
280
|
-
const sliders = document.createElement('div');
|
|
281
|
-
sliders.setAttribute('data-sliders', '');
|
|
282
|
-
|
|
283
|
-
const makeSlider = (label, channel, min, max, step) => {
|
|
284
|
-
const slider = document.createElement('slider-ui');
|
|
285
|
-
slider.setAttribute('data-channel', channel);
|
|
286
|
-
slider.setAttribute('label', label);
|
|
287
|
-
slider.setAttribute('min', min);
|
|
288
|
-
slider.setAttribute('max', max);
|
|
289
|
-
slider.setAttribute('step', step);
|
|
290
|
-
|
|
291
|
-
slider.addEventListener('input', this.#onSliderInput);
|
|
292
|
-
|
|
293
|
-
sliders.appendChild(slider);
|
|
294
|
-
return slider;
|
|
295
|
-
};
|
|
296
|
-
|
|
297
|
-
this.#hueSlider = makeSlider('H', 'hue', 0, 360, 1);
|
|
298
|
-
this.#chromaSlider = makeSlider('C', 'chroma', 0, MAX_CHROMA, 0.001);
|
|
299
|
-
this.#lightnessSlider = makeSlider('L', 'lightness', 0, 1, 0.001);
|
|
300
|
-
this.appendChild(sliders);
|
|
301
|
-
|
|
302
|
-
// Hue gradient track
|
|
303
|
-
this.#hueTrack = document.createElement('div');
|
|
304
|
-
this.#hueTrack.setAttribute('data-hue-track', '');
|
|
305
|
-
this.#hueTrack.setAttribute('tabindex', '0');
|
|
306
|
-
this.#hueTrack.setAttribute('role', 'slider');
|
|
307
|
-
this.#hueTrack.setAttribute('aria-label', 'Hue');
|
|
308
|
-
this.#hueThumb = document.createElement('div');
|
|
309
|
-
this.#hueThumb.setAttribute('data-hue-thumb', '');
|
|
310
|
-
this.#hueTrack.appendChild(this.#hueThumb);
|
|
311
|
-
this.appendChild(this.#hueTrack);
|
|
312
|
-
|
|
313
|
-
// Output rows
|
|
314
|
-
const output = document.createElement('div');
|
|
315
|
-
output.setAttribute('data-output', '');
|
|
316
|
-
|
|
317
|
-
const makeOutput = (format) => {
|
|
318
|
-
const group = document.createElement('div');
|
|
319
|
-
group.setAttribute('data-output-group', '');
|
|
320
|
-
|
|
321
|
-
const val = document.createElement('span');
|
|
322
|
-
val.setAttribute('data-output-value', '');
|
|
323
|
-
val.setAttribute('data-format', format);
|
|
324
|
-
|
|
325
|
-
const btn = document.createElement('div');
|
|
326
|
-
btn.setAttribute('role', 'button');
|
|
327
|
-
btn.setAttribute('tabindex', '0');
|
|
328
|
-
btn.setAttribute('data-copy', format);
|
|
329
|
-
btn.setAttribute('aria-label', `Copy ${format}`);
|
|
330
|
-
btn.innerHTML = '<icon-ui name="copy"></icon-ui>';
|
|
331
|
-
const handler = () => this.#copy(format, btn);
|
|
332
|
-
btn.addEventListener('click', handler);
|
|
333
|
-
this.#copyButtons.push({ el: btn, handler });
|
|
334
|
-
|
|
335
|
-
group.appendChild(val);
|
|
336
|
-
group.appendChild(btn);
|
|
337
|
-
output.appendChild(group);
|
|
338
|
-
return val;
|
|
339
|
-
};
|
|
340
|
-
|
|
341
|
-
this.#oklchVal = makeOutput('oklch');
|
|
342
|
-
this.#hexVal = makeOutput('hex');
|
|
343
|
-
this.appendChild(output);
|
|
344
|
-
|
|
345
|
-
// ── Area drag ──
|
|
346
|
-
|
|
347
|
-
this.#areaEl.addEventListener('pointerdown', this.#onAreaPointerDown);
|
|
348
|
-
this.#areaEl.addEventListener('pointermove', this.#onAreaPointerMove);
|
|
349
|
-
this.#areaEl.addEventListener('pointerup', this.#onAreaPointerUp);
|
|
350
|
-
|
|
351
|
-
// ── Hue track drag ──
|
|
352
|
-
|
|
353
|
-
this.#hueTrack.addEventListener('pointerdown', this.#onHuePointerDown);
|
|
354
|
-
this.#hueTrack.addEventListener('pointermove', this.#onHuePointerMove);
|
|
355
|
-
this.#hueTrack.addEventListener('pointerup', this.#onHuePointerUp);
|
|
356
|
-
|
|
357
|
-
// ── Keyboard on area ──
|
|
358
|
-
|
|
359
|
-
this.#areaEl.addEventListener('keydown', this.#onAreaKeydown);
|
|
360
|
-
|
|
361
|
-
// ── Keyboard on hue ──
|
|
362
|
-
|
|
363
|
-
this.#hueTrack.addEventListener('keydown', this.#onHueKeydown);
|
|
364
|
-
|
|
365
|
-
// Observe area resize to redraw canvas.
|
|
366
|
-
// gh#285 — SSR DOM shims (linkedom) have no ResizeObserver global.
|
|
367
|
-
if (typeof ResizeObserver !== 'undefined') {
|
|
368
|
-
this.#areaResizeObs = new ResizeObserver(() => this.#drawArea());
|
|
369
|
-
this.#areaResizeObs.observe(this.#areaEl);
|
|
370
|
-
}
|
|
371
|
-
}
|
|
372
|
-
|
|
373
|
-
disconnected() {
|
|
374
|
-
super.disconnected();
|
|
375
|
-
if (this.#copyTimer != null) {
|
|
376
|
-
clearTimeout(this.#copyTimer);
|
|
377
|
-
this.#copyTimer = null;
|
|
378
|
-
}
|
|
379
|
-
if (this.#areaResizeObs) {
|
|
380
|
-
this.#areaResizeObs.disconnect();
|
|
381
|
-
this.#areaResizeObs = null;
|
|
382
|
-
}
|
|
383
|
-
if (this.#areaEl) {
|
|
384
|
-
this.#areaEl.removeEventListener('pointerdown', this.#onAreaPointerDown);
|
|
385
|
-
this.#areaEl.removeEventListener('pointermove', this.#onAreaPointerMove);
|
|
386
|
-
this.#areaEl.removeEventListener('pointerup', this.#onAreaPointerUp);
|
|
387
|
-
this.#areaEl.removeEventListener('keydown', this.#onAreaKeydown);
|
|
388
|
-
}
|
|
389
|
-
if (this.#hueTrack) {
|
|
390
|
-
this.#hueTrack.removeEventListener('pointerdown', this.#onHuePointerDown);
|
|
391
|
-
this.#hueTrack.removeEventListener('pointermove', this.#onHuePointerMove);
|
|
392
|
-
this.#hueTrack.removeEventListener('pointerup', this.#onHuePointerUp);
|
|
393
|
-
this.#hueTrack.removeEventListener('keydown', this.#onHueKeydown);
|
|
394
|
-
}
|
|
395
|
-
this.#hueSlider?.removeEventListener('input', this.#onSliderInput);
|
|
396
|
-
this.#chromaSlider?.removeEventListener('input', this.#onSliderInput);
|
|
397
|
-
this.#lightnessSlider?.removeEventListener('input', this.#onSliderInput);
|
|
398
|
-
for (const { el, handler } of this.#copyButtons) {
|
|
399
|
-
el.removeEventListener('click', handler);
|
|
400
|
-
}
|
|
401
|
-
this.#copyButtons = [];
|
|
402
|
-
this.#bound = false;
|
|
403
|
-
}
|
|
404
|
-
|
|
405
|
-
// ── Pointer helpers ──
|
|
406
|
-
|
|
407
|
-
#updateAreaFromPointer(e) {
|
|
408
|
-
const rect = this.#areaEl.getBoundingClientRect();
|
|
409
|
-
const x = Math.max(0, Math.min(1, (e.clientX - rect.left) / rect.width));
|
|
410
|
-
const y = Math.max(0, Math.min(1, (e.clientY - rect.top) / rect.height));
|
|
411
|
-
this.#C = x * MAX_CHROMA;
|
|
412
|
-
this.#L = 1 - y;
|
|
413
|
-
this.#commit('input');
|
|
414
|
-
}
|
|
415
|
-
|
|
416
|
-
#updateHueFromPointer(e) {
|
|
417
|
-
const rect = this.#hueTrack.getBoundingClientRect();
|
|
418
|
-
const x = Math.max(0, Math.min(1, (e.clientX - rect.left) / rect.width));
|
|
419
|
-
this.#H = x * 360;
|
|
420
|
-
this.#commit('input');
|
|
421
|
-
}
|
|
422
|
-
|
|
423
|
-
// ── Value parsing ──
|
|
424
|
-
|
|
425
|
-
#parseValue(val) {
|
|
426
|
-
if (!val) return;
|
|
427
|
-
if (val.startsWith('#')) {
|
|
428
|
-
const o = hexToOklch(val);
|
|
429
|
-
this.#L = o.L; this.#C = o.C; this.#H = o.H;
|
|
430
|
-
return;
|
|
431
|
-
}
|
|
432
|
-
if (val.startsWith('oklch(')) {
|
|
433
|
-
// §201 (v0.5.7, FEEDBACK-13 §1 + FEEDBACK-14 §2): accept floats, percent
|
|
434
|
-
// on L, NaN (culori chromaless convention), and 'none' (CSS Color L4
|
|
435
|
-
// powerless-component syntax). Channel-level normalization: NaN/none → 0;
|
|
436
|
-
// percent on L → divide by 100. Per the CSS Color L4 "powerless" cascade
|
|
437
|
-
// (https://www.w3.org/TR/css-color-4/#powerless), zeroing the hue channel
|
|
438
|
-
// is the spec-defined resolution when chroma is 0 or near-0.
|
|
439
|
-
const m = val.match(
|
|
440
|
-
/oklch\(\s*([\d.]+%?|NaN|none)\s+([\d.]+%?|NaN|none)\s+([\d.]+|NaN|none)/i,
|
|
441
|
-
);
|
|
442
|
-
if (m) {
|
|
443
|
-
const parseChan = (s, isL) => {
|
|
444
|
-
if (s === 'none' || /^NaN$/i.test(s)) return 0;
|
|
445
|
-
if (isL && s.endsWith('%')) return +s.slice(0, -1) / 100;
|
|
446
|
-
if (s.endsWith('%')) return +s.slice(0, -1) / 100;
|
|
447
|
-
return +s;
|
|
448
|
-
};
|
|
449
|
-
this.#L = parseChan(m[1], true);
|
|
450
|
-
this.#C = parseChan(m[2], false);
|
|
451
|
-
this.#H = parseChan(m[3], false);
|
|
452
|
-
return;
|
|
453
|
-
}
|
|
454
|
-
// No regex match — warn once per element so consumers can correct.
|
|
455
|
-
// WeakSet dedup matches v0.5.5 §184 §8 button-ui icon-only pattern.
|
|
456
|
-
if (!UIColorPicker.#warnedBadParse.has(this)) {
|
|
457
|
-
UIColorPicker.#warnedBadParse.add(this);
|
|
458
|
-
// eslint-disable-next-line no-console
|
|
459
|
-
console.warn(
|
|
460
|
-
`<color-picker-ui>: could not parse value=${JSON.stringify(val)}. ` +
|
|
461
|
-
`Expected #rrggbb or oklch(L C H) with numeric, NaN, 'none' (CSS L4 ` +
|
|
462
|
-
`powerless), or % channels. Picker is keeping prior state.`,
|
|
463
|
-
);
|
|
464
|
-
}
|
|
465
|
-
}
|
|
466
|
-
}
|
|
467
|
-
|
|
468
|
-
// ── Commit: clamp to consumer constraints + gamut-map + update value + fire events ──
|
|
469
|
-
|
|
470
|
-
#commit(eventType) {
|
|
471
|
-
// Apply consumer-declared constraints BEFORE gamut mapping. Each constraint
|
|
472
|
-
// axis records a `constraint-clamp` event-payload entry when it actually
|
|
473
|
-
// moved a value; we dispatch one event per commit so the consumer can
|
|
474
|
-
// surface a single UX response (toast / validity warning / etc.).
|
|
475
|
-
const clamps = [];
|
|
476
|
-
if (this.#L > this.maxL) {
|
|
477
|
-
clamps.push({ axis: 'l', requested: this.#L, clamped: this.maxL, reason: 'max-l' });
|
|
478
|
-
this.#L = this.maxL;
|
|
479
|
-
}
|
|
480
|
-
if (this.#L < this.minL) {
|
|
481
|
-
clamps.push({ axis: 'l', requested: this.#L, clamped: this.minL, reason: 'min-l' });
|
|
482
|
-
this.#L = this.minL;
|
|
483
|
-
}
|
|
484
|
-
if (this.#C > this.maxChroma) {
|
|
485
|
-
clamps.push({ axis: 'c', requested: this.#C, clamped: this.maxChroma, reason: 'max-chroma' });
|
|
486
|
-
this.#C = this.maxChroma;
|
|
487
|
-
}
|
|
488
|
-
if (Number.isFinite(this.hueDriftMax)) {
|
|
489
|
-
const base = Number.isFinite(this.baseHue)
|
|
490
|
-
? this.baseHue
|
|
491
|
-
: (Number.isFinite(this.#resolvedBaseHue) ? this.#resolvedBaseHue : this.#H);
|
|
492
|
-
if (!Number.isFinite(this.#resolvedBaseHue)) this.#resolvedBaseHue = base;
|
|
493
|
-
// Hue is circular — find the signed shortest-path drift in [-180, 180].
|
|
494
|
-
let drift = ((this.#H - base + 540) % 360) - 180;
|
|
495
|
-
const limit = this.hueDriftMax;
|
|
496
|
-
if (drift > limit) {
|
|
497
|
-
const requestedH = this.#H;
|
|
498
|
-
this.#H = (base + limit + 360) % 360;
|
|
499
|
-
clamps.push({ axis: 'h', requested: requestedH, clamped: this.#H, reason: 'hue-drift-max' });
|
|
500
|
-
} else if (drift < -limit) {
|
|
501
|
-
const requestedH = this.#H;
|
|
502
|
-
this.#H = (base - limit + 360) % 360;
|
|
503
|
-
clamps.push({ axis: 'h', requested: requestedH, clamped: this.#H, reason: 'hue-drift-max' });
|
|
504
|
-
}
|
|
505
|
-
}
|
|
506
|
-
|
|
507
|
-
const clampedC = gamutMapChroma(this.#L, this.#C, this.#H);
|
|
508
|
-
const hexVal = oklchToHex(this.#L, clampedC, this.#H);
|
|
509
|
-
const oklchStr = this.#oklchStr();
|
|
510
|
-
|
|
511
|
-
this.#internalUpdate = true;
|
|
512
|
-
this.value = this.format === 'oklch' ? oklchStr : hexVal;
|
|
513
|
-
this.syncValue();
|
|
514
|
-
|
|
515
|
-
if (clamps.length) {
|
|
516
|
-
this.dispatchEvent(new CustomEvent('constraint-clamp', { bubbles: true, detail: { clamps } }));
|
|
517
|
-
}
|
|
518
|
-
|
|
519
|
-
const detail = { value: this.value, l: this.#L, c: this.#C, h: this.#H, hex: hexVal, oklch: oklchStr };
|
|
520
|
-
this.dispatchEvent(new CustomEvent(eventType, { bubbles: true, detail }));
|
|
521
|
-
}
|
|
522
|
-
|
|
523
|
-
#oklchStr() {
|
|
524
|
-
return `oklch(${this.#L.toFixed(2)} ${this.#C.toFixed(3)} ${Math.round(this.#H)})`;
|
|
525
|
-
}
|
|
526
|
-
|
|
527
|
-
// ── Copy ──
|
|
528
|
-
|
|
529
|
-
#copy(fmt, btn) {
|
|
530
|
-
const text = fmt === 'oklch'
|
|
531
|
-
? this.#oklchStr()
|
|
532
|
-
: oklchToHex(this.#L, gamutMapChroma(this.#L, this.#C, this.#H), this.#H);
|
|
533
|
-
|
|
534
|
-
const iconEl = btn.querySelector('icon-ui');
|
|
535
|
-
const onDone = (ok) => {
|
|
536
|
-
if (iconEl) {
|
|
537
|
-
iconEl.setAttribute('name', ok ? 'check' : 'warning');
|
|
538
|
-
if (this.#copyTimer != null) clearTimeout(this.#copyTimer);
|
|
539
|
-
this.#copyTimer = setTimeout(() => {
|
|
540
|
-
this.#copyTimer = null;
|
|
541
|
-
iconEl.setAttribute('name', 'copy');
|
|
542
|
-
}, 1500);
|
|
543
|
-
}
|
|
544
|
-
};
|
|
545
|
-
|
|
546
|
-
if (navigator.clipboard?.writeText) {
|
|
547
|
-
navigator.clipboard.writeText(text).then(
|
|
548
|
-
() => { if (!this.isConnected) return; onDone(true); },
|
|
549
|
-
() => { if (!this.isConnected) return; onDone(false); },
|
|
550
|
-
);
|
|
551
|
-
} else {
|
|
552
|
-
const ta = Object.assign(document.createElement('textarea'), { value: text });
|
|
553
|
-
ta.style.cssText = 'position:fixed;opacity:0';
|
|
554
|
-
document.body.appendChild(ta);
|
|
555
|
-
ta.select();
|
|
556
|
-
onDone(document.execCommand('copy'));
|
|
557
|
-
ta.remove();
|
|
558
|
-
}
|
|
559
|
-
}
|
|
560
|
-
|
|
561
|
-
// ── Canvas rendering ──
|
|
562
|
-
|
|
563
|
-
#drawArea() {
|
|
564
|
-
if (!this.#canvas || !this.#ctx || !this.#areaEl) return;
|
|
565
|
-
|
|
566
|
-
const dpr = Math.min(window.devicePixelRatio || 1, 2) * 0.5;
|
|
567
|
-
const w = Math.round(this.#areaEl.clientWidth * dpr);
|
|
568
|
-
const h = Math.round(this.#areaEl.clientHeight * dpr);
|
|
569
|
-
if (w <= 0 || h <= 0) return;
|
|
570
|
-
|
|
571
|
-
this.#canvas.width = w;
|
|
572
|
-
this.#canvas.height = h;
|
|
573
|
-
|
|
574
|
-
const img = this.#ctx.createImageData(w, h);
|
|
575
|
-
const data = img.data;
|
|
576
|
-
const hue = this.#H;
|
|
577
|
-
|
|
578
|
-
for (let y = 0; y < h; y++) {
|
|
579
|
-
const L = 1 - y / (h - 1);
|
|
580
|
-
for (let x = 0; x < w; x++) {
|
|
581
|
-
const C = (x / (w - 1)) * MAX_CHROMA;
|
|
582
|
-
const clampedC = gamutMapChroma(L, C, hue);
|
|
583
|
-
const [r, g, b] = oklchToRgb(L, clampedC, hue);
|
|
584
|
-
const i = (y * w + x) * 4;
|
|
585
|
-
data[i] = Math.round(r * 255);
|
|
586
|
-
data[i + 1] = Math.round(g * 255);
|
|
587
|
-
data[i + 2] = Math.round(b * 255);
|
|
588
|
-
data[i + 3] = 255;
|
|
589
|
-
}
|
|
590
|
-
}
|
|
591
|
-
|
|
592
|
-
this.#ctx.putImageData(img, 0, 0);
|
|
593
|
-
}
|
|
594
|
-
|
|
595
|
-
// ── Render ──
|
|
596
|
-
|
|
597
|
-
render() {
|
|
598
|
-
if (this.#internalUpdate) {
|
|
599
|
-
this.#internalUpdate = false;
|
|
600
|
-
} else {
|
|
601
|
-
this.#parseValue(this.value);
|
|
602
|
-
}
|
|
603
|
-
|
|
604
|
-
this.#drawArea();
|
|
605
|
-
|
|
606
|
-
// Area thumb
|
|
607
|
-
if (this.#areaThumb) {
|
|
608
|
-
const xPct = (this.#C / MAX_CHROMA) * 100;
|
|
609
|
-
const yPct = (1 - this.#L) * 100;
|
|
610
|
-
this.#areaThumb.style.left = `${xPct}%`;
|
|
611
|
-
this.#areaThumb.style.top = `${yPct}%`;
|
|
612
|
-
this.#areaThumb.style.background = oklchToHex(this.#L, gamutMapChroma(this.#L, this.#C, this.#H), this.#H);
|
|
613
|
-
}
|
|
614
|
-
|
|
615
|
-
// Hue track gradient
|
|
616
|
-
if (this.#hueTrack) {
|
|
617
|
-
const stops = [];
|
|
618
|
-
for (let h = 0; h <= 360; h += 30) stops.push(oklchToHex(0.7, 0.15, h));
|
|
619
|
-
this.#hueTrack.style.background = `linear-gradient(to right, ${stops.join(', ')})`;
|
|
620
|
-
this.#hueTrack.setAttribute('aria-valuenow', String(Math.round(this.#H)));
|
|
621
|
-
}
|
|
622
|
-
|
|
623
|
-
// Hue thumb
|
|
624
|
-
if (this.#hueThumb) {
|
|
625
|
-
this.#hueThumb.style.left = `${(this.#H / 360) * 100}%`;
|
|
626
|
-
this.#hueThumb.style.background = oklchToHex(0.7, 0.15, this.#H);
|
|
627
|
-
}
|
|
628
|
-
|
|
629
|
-
// ARIA
|
|
630
|
-
if (this.#areaEl) {
|
|
631
|
-
this.#areaEl.setAttribute('aria-valuetext', this.#oklchStr());
|
|
632
|
-
}
|
|
633
|
-
|
|
634
|
-
// Slider values
|
|
635
|
-
const hueSlider = this.querySelector('slider-ui[data-channel="hue"]');
|
|
636
|
-
const chromaSlider = this.querySelector('slider-ui[data-channel="chroma"]');
|
|
637
|
-
const lightnessSlider = this.querySelector('slider-ui[data-channel="lightness"]');
|
|
638
|
-
if (hueSlider) hueSlider.value = Math.round(this.#H);
|
|
639
|
-
if (chromaSlider) chromaSlider.value = +this.#C.toFixed(3);
|
|
640
|
-
if (lightnessSlider) lightnessSlider.value = +this.#L.toFixed(3);
|
|
641
|
-
|
|
642
|
-
// Disabled
|
|
643
|
-
if (this.disabled) this.setAttribute('aria-disabled', 'true');
|
|
644
|
-
else this.removeAttribute('aria-disabled');
|
|
645
|
-
|
|
646
|
-
// Output values
|
|
647
|
-
const hex = oklchToHex(this.#L, gamutMapChroma(this.#L, this.#C, this.#H), this.#H);
|
|
648
|
-
if (this.#oklchVal) this.#oklchVal.textContent = this.#oklchStr();
|
|
649
|
-
if (this.#hexVal) this.#hexVal.textContent = hex;
|
|
650
|
-
}
|
|
651
|
-
|
|
652
|
-
onFormReset() {
|
|
653
|
-
this.value = '#3b82f6';
|
|
654
|
-
}
|
|
45
|
+
static properties = { ...UIColorArea.properties };
|
|
655
46
|
}
|