@agent-ui-kit/components 0.0.3
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/dist/base-styles.css +8 -0
- package/dist/component-styles.css +147 -0
- package/dist/controls/_base/index.d.ts +3 -0
- package/dist/controls/_base/index.js +9 -0
- package/dist/controls/_base/indicator-element.d.ts +132 -0
- package/dist/controls/_base/indicator-element.js +158 -0
- package/dist/controls/_base/listbox-element.d.ts +70 -0
- package/dist/controls/_base/listbox-element.js +74 -0
- package/dist/controls/_base/range-element.d.ts +120 -0
- package/dist/controls/_base/range-element.js +158 -0
- package/dist/controls/_surface/container-box.css +301 -0
- package/dist/controls/_surface/container.css +156 -0
- package/dist/controls/_token-surface/token-surface.d.ts +35 -0
- package/dist/controls/_token-surface/token-surface.js +97 -0
- package/dist/controls/attachment/attachment-meta.d.ts +8 -0
- package/dist/controls/attachment/attachment-meta.js +84 -0
- package/dist/controls/attachment/attachment.css +91 -0
- package/dist/controls/attachment/attachment.d.ts +40 -0
- package/dist/controls/attachment/attachment.js +70 -0
- package/dist/controls/avatar/avatar-initials.d.ts +4 -0
- package/dist/controls/avatar/avatar-initials.js +30 -0
- package/dist/controls/avatar/avatar.css +87 -0
- package/dist/controls/avatar/avatar.d.ts +40 -0
- package/dist/controls/avatar/avatar.js +77 -0
- package/dist/controls/badge/badge.css +175 -0
- package/dist/controls/badge/badge.d.ts +36 -0
- package/dist/controls/badge/badge.js +71 -0
- package/dist/controls/bar-chart/bar-chart.css +110 -0
- package/dist/controls/bar-chart/bar-chart.d.ts +26 -0
- package/dist/controls/bar-chart/bar-chart.js +80 -0
- package/dist/controls/bar-chart/bar-math.d.ts +31 -0
- package/dist/controls/bar-chart/bar-math.js +106 -0
- package/dist/controls/button/button.css +263 -0
- package/dist/controls/button/button.d.ts +61 -0
- package/dist/controls/button/button.js +115 -0
- package/dist/controls/calendar/calendar.css +495 -0
- package/dist/controls/calendar/calendar.d.ts +161 -0
- package/dist/controls/calendar/calendar.js +1004 -0
- package/dist/controls/card/card-content.d.ts +24 -0
- package/dist/controls/card/card-content.js +133 -0
- package/dist/controls/card/card-footer.d.ts +3 -0
- package/dist/controls/card/card-footer.js +16 -0
- package/dist/controls/card/card-header.d.ts +3 -0
- package/dist/controls/card/card-header.js +18 -0
- package/dist/controls/card/card.css +445 -0
- package/dist/controls/card/card.d.ts +51 -0
- package/dist/controls/card/card.js +61 -0
- package/dist/controls/checkbox/checkbox.css +204 -0
- package/dist/controls/checkbox/checkbox.d.ts +114 -0
- package/dist/controls/checkbox/checkbox.js +130 -0
- package/dist/controls/code/code.css +72 -0
- package/dist/controls/code/code.d.ts +23 -0
- package/dist/controls/code/code.js +40 -0
- package/dist/controls/color-picker/color-picker.css +209 -0
- package/dist/controls/color-picker/color-picker.d.ts +84 -0
- package/dist/controls/color-picker/color-picker.js +545 -0
- package/dist/controls/color-picker/color.d.ts +40 -0
- package/dist/controls/color-picker/color.js +159 -0
- package/dist/controls/column/column.css +151 -0
- package/dist/controls/column/column.d.ts +107 -0
- package/dist/controls/column/column.js +57 -0
- package/dist/controls/combo-box/combo-box.css +397 -0
- package/dist/controls/combo-box/combo-box.d.ts +133 -0
- package/dist/controls/combo-box/combo-box.js +680 -0
- package/dist/controls/command-modal/command-modal.css +226 -0
- package/dist/controls/command-modal/command-modal.d.ts +42 -0
- package/dist/controls/command-modal/command-modal.js +315 -0
- package/dist/controls/disclosure/disclosure.css +162 -0
- package/dist/controls/disclosure/disclosure.d.ts +37 -0
- package/dist/controls/disclosure/disclosure.js +207 -0
- package/dist/controls/field/field.css +140 -0
- package/dist/controls/field/field.d.ts +27 -0
- package/dist/controls/field/field.js +228 -0
- package/dist/controls/form-provider/form-provider.css +40 -0
- package/dist/controls/form-provider/form-provider.d.ts +42 -0
- package/dist/controls/form-provider/form-provider.js +105 -0
- package/dist/controls/grid/grid.css +72 -0
- package/dist/controls/grid/grid.d.ts +60 -0
- package/dist/controls/grid/grid.js +56 -0
- package/dist/controls/icon/icon.css +37 -0
- package/dist/controls/icon/icon.d.ts +25 -0
- package/dist/controls/icon/icon.js +55 -0
- package/dist/controls/index.d.ts +57 -0
- package/dist/controls/index.js +136 -0
- package/dist/controls/ladder/ladder.css +89 -0
- package/dist/controls/ladder/ladder.d.ts +27 -0
- package/dist/controls/ladder/ladder.js +78 -0
- package/dist/controls/list/list.css +118 -0
- package/dist/controls/list/list.d.ts +84 -0
- package/dist/controls/list/list.js +48 -0
- package/dist/controls/menu/menu.css +195 -0
- package/dist/controls/menu/menu.d.ts +44 -0
- package/dist/controls/menu/menu.js +248 -0
- package/dist/controls/modal/modal.css +99 -0
- package/dist/controls/modal/modal.d.ts +61 -0
- package/dist/controls/modal/modal.js +155 -0
- package/dist/controls/popover/popover.css +83 -0
- package/dist/controls/popover/popover.d.ts +44 -0
- package/dist/controls/popover/popover.js +164 -0
- package/dist/controls/progress/progress.css +106 -0
- package/dist/controls/progress/progress.d.ts +28 -0
- package/dist/controls/progress/progress.js +80 -0
- package/dist/controls/radio/radio-group.css +84 -0
- package/dist/controls/radio/radio-group.d.ts +152 -0
- package/dist/controls/radio/radio-group.js +350 -0
- package/dist/controls/radio/radio.css +219 -0
- package/dist/controls/radio/radio.d.ts +19 -0
- package/dist/controls/radio/radio.js +75 -0
- package/dist/controls/ramp/ramp.css +75 -0
- package/dist/controls/ramp/ramp.d.ts +29 -0
- package/dist/controls/ramp/ramp.js +72 -0
- package/dist/controls/row/row.css +152 -0
- package/dist/controls/row/row.d.ts +95 -0
- package/dist/controls/row/row.js +45 -0
- package/dist/controls/segment/segment.css +66 -0
- package/dist/controls/segment/segment.d.ts +3 -0
- package/dist/controls/segment/segment.js +26 -0
- package/dist/controls/segmented-control/segmented-control.css +189 -0
- package/dist/controls/segmented-control/segmented-control.d.ts +20 -0
- package/dist/controls/segmented-control/segmented-control.js +64 -0
- package/dist/controls/select/select.css +467 -0
- package/dist/controls/select/select.d.ts +147 -0
- package/dist/controls/select/select.js +538 -0
- package/dist/controls/slider/slider.css +231 -0
- package/dist/controls/slider/slider.d.ts +111 -0
- package/dist/controls/slider/slider.js +60 -0
- package/dist/controls/slider-multi/slider-multi.css +197 -0
- package/dist/controls/slider-multi/slider-multi.d.ts +149 -0
- package/dist/controls/slider-multi/slider-multi.js +380 -0
- package/dist/controls/sparkline/sparkline-math.d.ts +23 -0
- package/dist/controls/sparkline/sparkline-math.js +79 -0
- package/dist/controls/sparkline/sparkline.css +61 -0
- package/dist/controls/sparkline/sparkline.d.ts +27 -0
- package/dist/controls/sparkline/sparkline.js +74 -0
- package/dist/controls/split/constrain.d.ts +44 -0
- package/dist/controls/split/constrain.js +126 -0
- package/dist/controls/split/split-pane.css +63 -0
- package/dist/controls/split/split-pane.d.ts +49 -0
- package/dist/controls/split/split-pane.js +42 -0
- package/dist/controls/split/split.css +127 -0
- package/dist/controls/split/split.d.ts +42 -0
- package/dist/controls/split/split.js +364 -0
- package/dist/controls/stat/stat-model.d.ts +22 -0
- package/dist/controls/stat/stat-model.js +69 -0
- package/dist/controls/stat/stat.css +101 -0
- package/dist/controls/stat/stat.d.ts +30 -0
- package/dist/controls/stat/stat.js +73 -0
- package/dist/controls/status-stream/status-stream.css +151 -0
- package/dist/controls/status-stream/status-stream.d.ts +108 -0
- package/dist/controls/status-stream/status-stream.js +417 -0
- package/dist/controls/swatch/swatch.css +66 -0
- package/dist/controls/swatch/swatch.d.ts +28 -0
- package/dist/controls/swatch/swatch.js +74 -0
- package/dist/controls/swiper/swiper-item.css +17 -0
- package/dist/controls/swiper/swiper-item.d.ts +31 -0
- package/dist/controls/swiper/swiper-item.js +47 -0
- package/dist/controls/swiper/swiper-label.css +10 -0
- package/dist/controls/swiper/swiper-label.d.ts +4 -0
- package/dist/controls/swiper/swiper-label.js +20 -0
- package/dist/controls/swiper/swiper-paddles.css +26 -0
- package/dist/controls/swiper/swiper-paddles.d.ts +10 -0
- package/dist/controls/swiper/swiper-paddles.js +62 -0
- package/dist/controls/swiper/swiper-pagination.css +60 -0
- package/dist/controls/swiper/swiper-pagination.d.ts +31 -0
- package/dist/controls/swiper/swiper-pagination.js +78 -0
- package/dist/controls/swiper/swiper.css +153 -0
- package/dist/controls/swiper/swiper.d.ts +158 -0
- package/dist/controls/swiper/swiper.js +659 -0
- package/dist/controls/switch/switch.css +194 -0
- package/dist/controls/switch/switch.d.ts +5 -0
- package/dist/controls/switch/switch.js +18 -0
- package/dist/controls/table/table-model.d.ts +43 -0
- package/dist/controls/table/table-model.js +124 -0
- package/dist/controls/table/table.css +90 -0
- package/dist/controls/table/table.d.ts +29 -0
- package/dist/controls/table/table.js +178 -0
- package/dist/controls/tabs/tab-panel.d.ts +10 -0
- package/dist/controls/tabs/tab-panel.js +89 -0
- package/dist/controls/tabs/tab.d.ts +26 -0
- package/dist/controls/tabs/tab.js +67 -0
- package/dist/controls/tabs/tabs.css +181 -0
- package/dist/controls/tabs/tabs.d.ts +61 -0
- package/dist/controls/tabs/tabs.js +176 -0
- package/dist/controls/text/href.d.ts +26 -0
- package/dist/controls/text/href.js +48 -0
- package/dist/controls/text/text.css +328 -0
- package/dist/controls/text/text.d.ts +85 -0
- package/dist/controls/text/text.js +273 -0
- package/dist/controls/text-field/text-field.css +498 -0
- package/dist/controls/text-field/text-field.d.ts +226 -0
- package/dist/controls/text-field/text-field.js +905 -0
- package/dist/controls/textarea/textarea.css +251 -0
- package/dist/controls/textarea/textarea.d.ts +136 -0
- package/dist/controls/textarea/textarea.js +312 -0
- package/dist/controls/theme-provider/theme-provider.css +48 -0
- package/dist/controls/theme-provider/theme-provider.d.ts +59 -0
- package/dist/controls/theme-provider/theme-provider.js +40 -0
- package/dist/controls/timeline/timeline.css +33 -0
- package/dist/controls/timeline/timeline.d.ts +39 -0
- package/dist/controls/timeline/timeline.js +52 -0
- package/dist/controls/timeline-item/timeline-item.css +495 -0
- package/dist/controls/timeline-item/timeline-item.d.ts +79 -0
- package/dist/controls/timeline-item/timeline-item.js +360 -0
- package/dist/controls/toast/toast-region.css +56 -0
- package/dist/controls/toast/toast-region.d.ts +20 -0
- package/dist/controls/toast/toast-region.js +113 -0
- package/dist/controls/toast/toast.css +96 -0
- package/dist/controls/toast/toast.d.ts +28 -0
- package/dist/controls/toast/toast.js +191 -0
- package/dist/controls/toolbar/toolbar.css +125 -0
- package/dist/controls/toolbar/toolbar.d.ts +108 -0
- package/dist/controls/toolbar/toolbar.js +68 -0
- package/dist/controls/tooltip/tooltip.css +80 -0
- package/dist/controls/tooltip/tooltip.d.ts +57 -0
- package/dist/controls/tooltip/tooltip.js +205 -0
- package/dist/descriptor/component-descriptor.d.ts +112 -0
- package/dist/descriptor/component-descriptor.js +492 -0
- package/dist/descriptor/index.d.ts +1 -0
- package/dist/descriptor/index.js +5 -0
- package/dist/dom/container.d.ts +45 -0
- package/dist/dom/container.js +60 -0
- package/dist/dom/element.d.ts +98 -0
- package/dist/dom/element.js +221 -0
- package/dist/dom/form.d.ts +240 -0
- package/dist/dom/form.js +399 -0
- package/dist/dom/index.d.ts +10 -0
- package/dist/dom/index.js +31 -0
- package/dist/dom/props.d.ts +56 -0
- package/dist/dom/props.js +231 -0
- package/dist/dom/repeat.d.ts +10 -0
- package/dist/dom/repeat.js +202 -0
- package/dist/dom/template.d.ts +161 -0
- package/dist/dom/template.js +634 -0
- package/dist/dom/watch.d.ts +11 -0
- package/dist/dom/watch.js +82 -0
- package/dist/foundation-styles.css +9 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +3 -0
- package/dist/reactive/graph.d.ts +65 -0
- package/dist/reactive/graph.js +310 -0
- package/dist/reactive/index.d.ts +3 -0
- package/dist/reactive/index.js +4 -0
- package/dist/reactive/scheduler.d.ts +13 -0
- package/dist/reactive/scheduler.js +49 -0
- package/dist/traits/area-drag.d.ts +12 -0
- package/dist/traits/area-drag.js +70 -0
- package/dist/traits/form-registry.d.ts +36 -0
- package/dist/traits/form-registry.js +108 -0
- package/dist/traits/index.d.ts +24 -0
- package/dist/traits/index.js +19 -0
- package/dist/traits/overlay.d.ts +50 -0
- package/dist/traits/overlay.js +277 -0
- package/dist/traits/pane-resize.d.ts +26 -0
- package/dist/traits/pane-resize.js +103 -0
- package/dist/traits/press-activation.d.ts +11 -0
- package/dist/traits/press-activation.js +55 -0
- package/dist/traits/roving-focus.d.ts +52 -0
- package/dist/traits/roving-focus.js +214 -0
- package/dist/traits/scroll-fade.d.ts +28 -0
- package/dist/traits/scroll-fade.js +120 -0
- package/dist/traits/selection-commit.d.ts +30 -0
- package/dist/traits/selection-commit.js +146 -0
- package/dist/traits/tabbable.d.ts +12 -0
- package/dist/traits/tabbable.js +59 -0
- package/dist/traits/track-user-invalid.d.ts +36 -0
- package/dist/traits/track-user-invalid.js +52 -0
- package/dist/traits/value-codec.d.ts +120 -0
- package/dist/traits/value-codec.js +497 -0
- package/dist/traits/value-drag.d.ts +18 -0
- package/dist/traits/value-drag.js +84 -0
- package/package.json +263 -0
|
@@ -0,0 +1,545 @@
|
|
|
1
|
+
// color-picker.ts — UIColorPickerElement, the standalone `ui-color-picker` control (LLD-C1..C7,
|
|
2
|
+
// color-picker.lld.md · ADR-0123 · SPEC-R1…R9/R12/R13). A Pattern-class `UIFormElement` composite that
|
|
3
|
+
// edits ONE color value through a 2-axis pad + hue/chroma/lightness channel sliders + an editable numeric
|
|
4
|
+
// readout, on an OKLCH-internal model whose `value` serializes to a `format`-selected syntax (hex default,
|
|
5
|
+
// oklch opt-in), gamut-mapped into sRGB. It composes `ui-slider` (channels — the accessible spine, SPEC-R6),
|
|
6
|
+
// `ui-text-field` (the readout) and `ui-swatch` (the preview) — the ADR-0118 fence honored: this control
|
|
7
|
+
// EDITS, `ui-swatch` shows; the preview is always a composed `<ui-swatch>`, never a bespoke color div.
|
|
8
|
+
//
|
|
9
|
+
// The pad ([data-part=pad]) is a pointer/keyboard ACCELERATOR over the chroma(X)/lightness(Y) plane at the
|
|
10
|
+
// current hue — NOT the accessible spine. It wires the NEW `area-drag` trait (the 2-axis sibling of
|
|
11
|
+
// value-drag, traits/area-drag.ts) for pointer gesture, and a bespoke keydown handler (the ui-calendar
|
|
12
|
+
// #handleGridKey precedent) for 2-axis keyboard stepping. Its own a11y rides `role=slider` +
|
|
13
|
+
// `aria-roledescription="2D slider"` + a cross-announcing `aria-valuetext` on the PART (the ui-calendar
|
|
14
|
+
// role-on-a-part precedent — the host is the form element, the role lives on the part).
|
|
15
|
+
//
|
|
16
|
+
// Commit choke point (`#commit`): every gesture serializes the OKLCH model through `format` (gamut-mapping
|
|
17
|
+
// before hex), writes `this.value`, repaints every composed part, and emits the event. The `#lastCommittedValue`
|
|
18
|
+
// echo-guard (compared by VALUE, not a one-shot flag — a stale one-shot flag would wrongly swallow a later
|
|
19
|
+
// genuine external `value` set whenever a commit's write happened to be an Object.is no-op) tells the
|
|
20
|
+
// model→surface effect not to re-parse (and thereby round-trip-quantize) its own just-serialized write —
|
|
21
|
+
// preserving full drag precision mid-gesture (the adia `#internalUpdate` precedent, LLD-C2).
|
|
22
|
+
//
|
|
23
|
+
// Layer: controls/ → dom + traits (inward-only ✓). Composes sibling controls (ui-slider/ui-text-field/
|
|
24
|
+
// ui-swatch) via a static sibling-control import — the sanctioned command-modal→ui-modal /
|
|
25
|
+
// swiper-paddles→ui-button precedent. erasableSyntaxOnly ✓. verbatimModuleSyntax ✓.
|
|
26
|
+
import { UIFormElement, prop } from "../../dom/index.js";
|
|
27
|
+
import { signal } from "../../reactive/index.js";
|
|
28
|
+
import { trackUserInvalid } from "../../traits/track-user-invalid.js";
|
|
29
|
+
import { areaDrag } from "../../traits/area-drag.js";
|
|
30
|
+
import { UISliderElement } from "../slider/slider.js";
|
|
31
|
+
import { UITextFieldElement } from "../text-field/text-field.js";
|
|
32
|
+
import { UISwatchElement } from "../swatch/swatch.js";
|
|
33
|
+
import { MAX_CHROMA, oklchToRgb, gamutMapChroma, parseColor, serializeColor, colorCodecOptions, } from "./color.js";
|
|
34
|
+
// ── props (SPEC-R2 — exactly five reflected attributes: value/format own two + the three formProps) ────
|
|
35
|
+
const props = {
|
|
36
|
+
...UIFormElement.formProps, // name / disabled / required
|
|
37
|
+
value: { ...prop.string(''), reflect: true }, // serialized per `format`; '' = unset (SPEC §2 unset-display)
|
|
38
|
+
format: { ...prop.enum(['hex', 'oklch'], 'hex'), reflect: true }, // selects `value` SERIALIZATION only
|
|
39
|
+
};
|
|
40
|
+
// ── the default working color (§2 unset-display: a fresh, untouched control still PAINTS a color) ──────
|
|
41
|
+
const DEFAULT_L = 0.62;
|
|
42
|
+
const DEFAULT_C = 0.12;
|
|
43
|
+
const DEFAULT_H = 260;
|
|
44
|
+
export class UIColorPickerElement extends UIFormElement {
|
|
45
|
+
static props = props;
|
|
46
|
+
// ── the internal OKLCH working model (NOT reactive props — ephemeral, the adia #L/#C/#H precedent) ──
|
|
47
|
+
#L = DEFAULT_L;
|
|
48
|
+
#C = DEFAULT_C;
|
|
49
|
+
#H = DEFAULT_H;
|
|
50
|
+
// Shell parts, created ONCE (idempotent across disconnect/reconnect — the ui-calendar shell precedent).
|
|
51
|
+
#pad = null;
|
|
52
|
+
#padThumb = null;
|
|
53
|
+
#canvas = null;
|
|
54
|
+
#hueSlider = null;
|
|
55
|
+
#chromaSlider = null;
|
|
56
|
+
#lightnessSlider = null;
|
|
57
|
+
// Visible per-channel numeral (ADR-0123 cl.8 / SPEC-R8 AC2 — "every channel prints a numeric value, so
|
|
58
|
+
// the control never signifies by hue alone"; a plain composed ui-slider carries no VISIBLE text of its
|
|
59
|
+
// own, only ARIA, so this control adds the printed non-color signifier itself).
|
|
60
|
+
#hueValueEl = null;
|
|
61
|
+
#chromaValueEl = null;
|
|
62
|
+
#lightnessValueEl = null;
|
|
63
|
+
#readoutField = null;
|
|
64
|
+
#swatch = null;
|
|
65
|
+
// The HTML-authored initial value, captured at FIRST connect (the calendar formReset precedent — `value`
|
|
66
|
+
// reflects, so getAttribute('value') later would read the CURRENT value, not the original).
|
|
67
|
+
#initialValue = '';
|
|
68
|
+
// Readout parse-error state (LLD-C6 — the STANDALONE control reads this directly; it does NOT instantiate
|
|
69
|
+
// the `valueCodec` controller, that is the `type=color` text-field leg's job, LLD-C9). A signal so the
|
|
70
|
+
// base's merged-validity effect (form.ts) re-runs formValidity() when it flips.
|
|
71
|
+
#readoutError = signal(false);
|
|
72
|
+
// The echo-guard: the LAST value this control itself wrote via #commit. Compared by VALUE (not consumed
|
|
73
|
+
// as a one-shot flag) so a same-string re-commit (an Object.is no-op that never re-runs the tracking
|
|
74
|
+
// effect) can never leave a stale "skip reparse" signal that would swallow a LATER genuine external set.
|
|
75
|
+
#lastCommittedValue = null;
|
|
76
|
+
// Canvas paint cache — redraw only on hue change (or resize), never on every chroma/lightness tick
|
|
77
|
+
// (LLD §12 risk: "the thumb moves, the field does not").
|
|
78
|
+
#lastPaintedHue = null;
|
|
79
|
+
#resizeObserver = null;
|
|
80
|
+
// Per-drag baseline (for the pad's OWN pointer-up `change`, distinct from the channel sliders' native
|
|
81
|
+
// blur-based commit — SPEC-R5 AC1: "a drag that returns to its start fires no change"). Captured
|
|
82
|
+
// AFTER the drag's own first jump — see the connected() wiring's doc for why.
|
|
83
|
+
#dragBaselineValue = null;
|
|
84
|
+
#dragJustStarted = false;
|
|
85
|
+
#areaRelease = null;
|
|
86
|
+
#userInvalid = null;
|
|
87
|
+
#warnedBadParse = false;
|
|
88
|
+
#fieldLabelText = null;
|
|
89
|
+
// ── form seams (LLD-C6) ─────────────────────────────────────────────────────────────────────
|
|
90
|
+
formValue() {
|
|
91
|
+
return this.value !== '' ? this.value : null; // SPEC-R9 AC4: unset submits null even though it PAINTS a color
|
|
92
|
+
}
|
|
93
|
+
formValidity() {
|
|
94
|
+
if (this.required && this.value === '') {
|
|
95
|
+
return { valid: false, flags: { valueMissing: true }, message: 'Please select a color.' };
|
|
96
|
+
}
|
|
97
|
+
if (this.#readoutError.value) {
|
|
98
|
+
return { valid: false, flags: { customError: true }, message: 'Please enter a valid color.' };
|
|
99
|
+
}
|
|
100
|
+
return { valid: true };
|
|
101
|
+
}
|
|
102
|
+
formReset() {
|
|
103
|
+
this.value = this.#initialValue;
|
|
104
|
+
this.#readoutError.value = false;
|
|
105
|
+
this.#userInvalid?.reset();
|
|
106
|
+
}
|
|
107
|
+
formStateRestore(state) {
|
|
108
|
+
if (typeof state === 'string')
|
|
109
|
+
this.value = state;
|
|
110
|
+
}
|
|
111
|
+
formUserInvalid() {
|
|
112
|
+
return this.#userInvalid?.userInvalid() ?? false;
|
|
113
|
+
}
|
|
114
|
+
/** The pad is the labelled part (the ui-calendar grid precedent — the host is the form element, the
|
|
115
|
+
* role/name ride the PART). Merges the field's label text into the pad's own fixed axis description
|
|
116
|
+
* (string concat — the pad has no stable self-describing id'd node to combine via aria-labelledby, unlike
|
|
117
|
+
* calendar's title span, so a text merge is the pragmatic mechanism here). */
|
|
118
|
+
applyFieldLabelling(refs) {
|
|
119
|
+
const pad = this.#pad;
|
|
120
|
+
if (!pad)
|
|
121
|
+
return;
|
|
122
|
+
this.#fieldLabelText = refs?.label?.textContent ?? null;
|
|
123
|
+
this.#updatePadAriaLabel();
|
|
124
|
+
if (refs === null) {
|
|
125
|
+
pad.removeAttribute('aria-describedby');
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
const described = [refs.description, refs.error].filter((el) => el !== null);
|
|
129
|
+
if (described.length > 0)
|
|
130
|
+
pad.setAttribute('aria-describedby', described.map((el) => el.id).join(' '));
|
|
131
|
+
else
|
|
132
|
+
pad.removeAttribute('aria-describedby');
|
|
133
|
+
}
|
|
134
|
+
#updatePadAriaLabel() {
|
|
135
|
+
const pad = this.#pad;
|
|
136
|
+
if (!pad)
|
|
137
|
+
return;
|
|
138
|
+
const axisLabel = 'Chroma and lightness'; // F1 ruling = OKLCH vocabulary (an HSV ruling would read "Saturation and value")
|
|
139
|
+
pad.setAttribute('aria-label', this.#fieldLabelText ? `${this.#fieldLabelText}, ${axisLabel}` : axisLabel);
|
|
140
|
+
}
|
|
141
|
+
// ── connection lifecycle ────────────────────────────────────────────────────────────────────
|
|
142
|
+
connected() {
|
|
143
|
+
const { pad } = this.#ensureShell();
|
|
144
|
+
if (this.#initialValue === '' && this.getAttribute('value')) {
|
|
145
|
+
this.#initialValue = this.getAttribute('value');
|
|
146
|
+
}
|
|
147
|
+
// The initial `value` (if any) is seeded into the model by the model→surface effect's first run,
|
|
148
|
+
// below — its first pass always parses (since `#lastCommittedValue` starts null); no separate seed
|
|
149
|
+
// step is needed here (unset stays the default working color, §2).
|
|
150
|
+
// ── user-invalid TIMING controller (ADR-0051 — the calendar/text-field precedent) ──────────
|
|
151
|
+
const invalidController = trackUserInvalid(this, { invalid: () => !this.formValidity().valid });
|
|
152
|
+
this.#userInvalid = invalidController;
|
|
153
|
+
this.effect(() => {
|
|
154
|
+
if (invalidController.userInvalid())
|
|
155
|
+
this.internals.states?.add('user-invalid');
|
|
156
|
+
else
|
|
157
|
+
this.internals.states?.delete('user-invalid');
|
|
158
|
+
});
|
|
159
|
+
// ── the pad: area-drag (pointer) + a baseline/pointerup listener for the PAD's own `change`
|
|
160
|
+
// (SPEC-R5 AC1 — distinct from the channels' blur-based commit). The baseline is captured AFTER
|
|
161
|
+
// the drag's OWN first jump (inside onValue, on the first call of a fresh press) — matching the
|
|
162
|
+
// ui-slider precedent exactly: UIRangeElement's #committed baseline is captured on 'focus', which
|
|
163
|
+
// native mousedown default-action ordering fires AFTER pointerdown's own dispatch (so AFTER
|
|
164
|
+
// valueDrag's own onValue jump) — "a drag that returns to its start" means the position the drag
|
|
165
|
+
// ITSELF started from (post-jump), not whatever the control held before the user ever touched it. ──
|
|
166
|
+
this.listen(pad, 'pointerdown', () => { this.#dragJustStarted = true; });
|
|
167
|
+
this.listen(pad, 'pointerup', () => {
|
|
168
|
+
if (this.#dragBaselineValue !== null && this.#dragBaselineValue !== this.value)
|
|
169
|
+
this.emit('change');
|
|
170
|
+
this.#dragBaselineValue = null;
|
|
171
|
+
});
|
|
172
|
+
this.#areaRelease = areaDrag(this, {
|
|
173
|
+
area: () => this.#pad,
|
|
174
|
+
onValue: (x, y) => {
|
|
175
|
+
if (this.effectiveDisabled())
|
|
176
|
+
return;
|
|
177
|
+
this.#C = x * MAX_CHROMA;
|
|
178
|
+
this.#L = 1 - y;
|
|
179
|
+
this.#commit('input');
|
|
180
|
+
if (this.#dragJustStarted) {
|
|
181
|
+
this.#dragBaselineValue = this.value; // the drag's OWN start — after its first jump
|
|
182
|
+
this.#dragJustStarted = false;
|
|
183
|
+
}
|
|
184
|
+
},
|
|
185
|
+
});
|
|
186
|
+
this.listen(pad, 'keydown', (event) => { this.#handlePadKey(event); });
|
|
187
|
+
// ── channel sliders — the EVENT-BOUNDARY GUARD (the ADR-0048 calendar precedent): stop each
|
|
188
|
+
// composed slider's OWN input/change from bubbling out of ui-color-picker (it would otherwise
|
|
189
|
+
// double-emit alongside the picker's own explicit emit below), then drive the model + re-emit
|
|
190
|
+
// the picker's OWN input/change. ─────────────────────────────────────────────────────────────
|
|
191
|
+
const hue = this.#hueSlider;
|
|
192
|
+
const chroma = this.#chromaSlider;
|
|
193
|
+
const lightness = this.#lightnessSlider;
|
|
194
|
+
this.listen(hue, 'input', (e) => { e.stopPropagation(); this.#H = hue.value ?? 0; this.#commit('input'); });
|
|
195
|
+
this.listen(hue, 'change', (e) => { e.stopPropagation(); this.#commit('change'); });
|
|
196
|
+
this.listen(chroma, 'input', (e) => { e.stopPropagation(); this.#C = chroma.value ?? 0; this.#commit('input'); });
|
|
197
|
+
this.listen(chroma, 'change', (e) => { e.stopPropagation(); this.#commit('change'); });
|
|
198
|
+
this.listen(lightness, 'input', (e) => { e.stopPropagation(); this.#L = lightness.value ?? 0; this.#commit('input'); });
|
|
199
|
+
this.listen(lightness, 'change', (e) => { e.stopPropagation(); this.#commit('change'); });
|
|
200
|
+
// ── readout — boundary guard (attached directly to the field, the calendar-in-text-field precedent);
|
|
201
|
+
// only `change` (blur/Enter commit) parses through colorCodecOptions ──────────────────────────────
|
|
202
|
+
const readoutField = this.#readoutField;
|
|
203
|
+
this.listen(readoutField, 'input', (e) => { e.stopPropagation(); });
|
|
204
|
+
this.listen(readoutField, 'change', (e) => {
|
|
205
|
+
e.stopPropagation();
|
|
206
|
+
this.#commitReadout(readoutField.value);
|
|
207
|
+
});
|
|
208
|
+
// ── model → surface: parse EXTERNAL value/format changes into the model (the echo-guard skips
|
|
209
|
+
// re-parsing a write this control JUST made itself — see #lastCommittedValue's own doc), then
|
|
210
|
+
// NORMALIZE `value` to the current model+format (SPEC-R2 AC2 — a format switch alone re-serializes;
|
|
211
|
+
// SPEC-R3 — value ALWAYS reads through `format`, whatever syntax it was last written in) — a plain
|
|
212
|
+
// re-entrant prop write, NEVER an event (the UIRangeElement normalizer-effect precedent, range-
|
|
213
|
+
// element.ts: "if (!Object.is(normalized, raw)) this.value = normalized", the same self-write-inside-
|
|
214
|
+
// its-own-effect pattern, converging in one extra pass since the 2nd run's value then equals
|
|
215
|
+
// #lastCommittedValue). Always repaints every composed part last. ────────────────────────────────
|
|
216
|
+
this.effect(() => {
|
|
217
|
+
const value = this.value;
|
|
218
|
+
const format = this.format;
|
|
219
|
+
if (value === '') {
|
|
220
|
+
// unset — nothing to parse or normalize; keep painting the current (default/last-set) color (§2)
|
|
221
|
+
}
|
|
222
|
+
else if (value === this.#lastCommittedValue) {
|
|
223
|
+
// our own echo — the model is already authoritative, skip reparse (precision guard, LLD-C2)
|
|
224
|
+
}
|
|
225
|
+
else {
|
|
226
|
+
const parsed = parseColor(value);
|
|
227
|
+
if (parsed) {
|
|
228
|
+
this.#L = parsed.L;
|
|
229
|
+
this.#C = parsed.C;
|
|
230
|
+
this.#H = parsed.H;
|
|
231
|
+
}
|
|
232
|
+
else
|
|
233
|
+
this.#warnOnce(value); // SPEC-R3 AC4 — leave the prior model intact, no throw
|
|
234
|
+
}
|
|
235
|
+
if (value !== '') {
|
|
236
|
+
const normalized = serializeColor({ L: this.#L, C: this.#C, H: this.#H }, format);
|
|
237
|
+
if (normalized !== this.value) {
|
|
238
|
+
this.#lastCommittedValue = normalized;
|
|
239
|
+
this.value = normalized;
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
this.#paint();
|
|
243
|
+
});
|
|
244
|
+
// ── disabled channel — forward effectiveDisabled() onto every composed child (each is an
|
|
245
|
+
// independent custom element with its OWN `disabled` prop, defaulting false regardless of the
|
|
246
|
+
// picker's own state) + the pad's tabindex (out of the tab order while disabled, ADR-0010). ──────
|
|
247
|
+
this.effect(() => {
|
|
248
|
+
const dis = this.effectiveDisabled();
|
|
249
|
+
hue.disabled = dis;
|
|
250
|
+
chroma.disabled = dis;
|
|
251
|
+
lightness.disabled = dis;
|
|
252
|
+
if (this.#readoutField)
|
|
253
|
+
this.#readoutField.disabled = dis;
|
|
254
|
+
// removeAttribute, not '-1' — the TKT-0068 item 2 ruling: a disabled part is not even
|
|
255
|
+
// programmatically focusable (native parity; the textarea/text-field/combo-box editor shape).
|
|
256
|
+
if (dis)
|
|
257
|
+
pad.removeAttribute('tabindex');
|
|
258
|
+
else
|
|
259
|
+
pad.setAttribute('tabindex', '0');
|
|
260
|
+
});
|
|
261
|
+
// ResizeObserver — redraw the pad canvas on resize (feature-detected; jsdom lacks it — LLD §12).
|
|
262
|
+
if (typeof ResizeObserver !== 'undefined') {
|
|
263
|
+
this.#resizeObserver = new ResizeObserver(() => { this.#lastPaintedHue = null; this.#drawPad(); });
|
|
264
|
+
this.#resizeObserver.observe(pad);
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
disconnected() {
|
|
268
|
+
this.#userInvalid?.release();
|
|
269
|
+
this.#userInvalid = null;
|
|
270
|
+
this.#areaRelease?.();
|
|
271
|
+
this.#areaRelease = null;
|
|
272
|
+
this.#resizeObserver?.disconnect();
|
|
273
|
+
this.#resizeObserver = null;
|
|
274
|
+
}
|
|
275
|
+
// ── shell creation (idempotent — SPEC-R1 AC2) ───────────────────────────────────────────────
|
|
276
|
+
#ensureShell() {
|
|
277
|
+
if (this.#pad && this.#hueSlider && this.#chromaSlider && this.#lightnessSlider && this.#readoutField && this.#swatch) {
|
|
278
|
+
return { pad: this.#pad, readout: this.#readoutField.parentElement };
|
|
279
|
+
}
|
|
280
|
+
// ── the pad ([data-part=pad]) — the 2-axis chroma×lightness area (F1 sub-fork: canvas paint) ────
|
|
281
|
+
const pad = document.createElement('div');
|
|
282
|
+
pad.setAttribute('data-part', 'pad');
|
|
283
|
+
pad.setAttribute('tabindex', '0');
|
|
284
|
+
pad.setAttribute('role', 'slider');
|
|
285
|
+
pad.setAttribute('aria-roledescription', '2D slider');
|
|
286
|
+
pad.style.touchAction = 'none';
|
|
287
|
+
const canvas = document.createElement('canvas');
|
|
288
|
+
canvas.setAttribute('data-part', 'pad-canvas');
|
|
289
|
+
pad.appendChild(canvas);
|
|
290
|
+
this.#canvas = canvas;
|
|
291
|
+
const thumb = document.createElement('div');
|
|
292
|
+
thumb.setAttribute('data-part', 'pad-thumb');
|
|
293
|
+
pad.appendChild(thumb);
|
|
294
|
+
this.#padThumb = thumb;
|
|
295
|
+
this.appendChild(pad);
|
|
296
|
+
this.#pad = pad;
|
|
297
|
+
this.#updatePadAriaLabel();
|
|
298
|
+
// ── channels ([data-part=channels]) — hue always; chroma + lightness mirrored (SPEC-R6 spine).
|
|
299
|
+
// Each channel is a labeled row: a text label, the composed ui-slider, and a printed numeral
|
|
300
|
+
// (ADR-0123 cl.8 — a plain ui-slider carries no visible text of its own, only ARIA; this control
|
|
301
|
+
// adds the printed non-color signifier itself, SPEC-R8 AC2). ──────────────────────────────────
|
|
302
|
+
const channels = document.createElement('div');
|
|
303
|
+
channels.setAttribute('data-part', 'channels');
|
|
304
|
+
const { row: hueRow, slider: hueSlider, valueEl: hueValueEl } = this.#makeChannelRow('Hue', 'hue');
|
|
305
|
+
hueSlider.min = 0;
|
|
306
|
+
hueSlider.max = 360;
|
|
307
|
+
hueSlider.step = 1;
|
|
308
|
+
channels.appendChild(hueRow);
|
|
309
|
+
this.#hueSlider = hueSlider;
|
|
310
|
+
this.#hueValueEl = hueValueEl;
|
|
311
|
+
const { row: chromaRow, slider: chromaSlider, valueEl: chromaValueEl } = this.#makeChannelRow('Chroma', 'chroma');
|
|
312
|
+
chromaSlider.min = 0;
|
|
313
|
+
chromaSlider.max = MAX_CHROMA;
|
|
314
|
+
chromaSlider.step = 0.001;
|
|
315
|
+
channels.appendChild(chromaRow);
|
|
316
|
+
this.#chromaSlider = chromaSlider;
|
|
317
|
+
this.#chromaValueEl = chromaValueEl;
|
|
318
|
+
const { row: lightnessRow, slider: lightnessSlider, valueEl: lightnessValueEl } = this.#makeChannelRow('Lightness', 'lightness');
|
|
319
|
+
lightnessSlider.min = 0;
|
|
320
|
+
lightnessSlider.max = 1;
|
|
321
|
+
lightnessSlider.step = 0.001;
|
|
322
|
+
channels.appendChild(lightnessRow);
|
|
323
|
+
this.#lightnessSlider = lightnessSlider;
|
|
324
|
+
this.#lightnessValueEl = lightnessValueEl;
|
|
325
|
+
this.appendChild(channels);
|
|
326
|
+
// ── readout ([data-part=readout]) — editable ui-text-field + composed ui-swatch preview ─────────
|
|
327
|
+
const readout = document.createElement('div');
|
|
328
|
+
readout.setAttribute('data-part', 'readout');
|
|
329
|
+
const readoutField = document.createElement('ui-text-field');
|
|
330
|
+
readoutField.label = 'Color value';
|
|
331
|
+
readout.appendChild(readoutField);
|
|
332
|
+
this.#readoutField = readoutField;
|
|
333
|
+
const swatch = document.createElement('ui-swatch');
|
|
334
|
+
swatch.label = 'Selected color';
|
|
335
|
+
readout.appendChild(swatch);
|
|
336
|
+
this.#swatch = swatch;
|
|
337
|
+
// ── EyeDropper (SPEC-R13/F7) — feature-detected progressive enhancement, no polyfill, no layout
|
|
338
|
+
// hole where absent. No dedicated icon asset exists in @agent-ui-kit/icons for this glyph (the pack
|
|
339
|
+
// carries a small hand-curated set) — a plain text-labeled button avoids inventing a new icon
|
|
340
|
+
// outside this control's scope (a judgment call, flagged in the handoff). ─────────────────────
|
|
341
|
+
if ('EyeDropper' in window) {
|
|
342
|
+
const eyedropperBtn = document.createElement('button');
|
|
343
|
+
eyedropperBtn.type = 'button';
|
|
344
|
+
eyedropperBtn.setAttribute('data-part', 'eyedropper');
|
|
345
|
+
eyedropperBtn.setAttribute('aria-label', 'Pick color from screen');
|
|
346
|
+
eyedropperBtn.textContent = 'Pick';
|
|
347
|
+
// Node-lifetime listener, deliberately NOT `this.listen` (ratified, TKT-0065 lateral review):
|
|
348
|
+
// the button is a build-once part — the listener lives and dies with the node, so it survives
|
|
349
|
+
// reconnect by construction (the settings.ts/entry-list.ts tier, the inverse of the TKT-0056 bug).
|
|
350
|
+
eyedropperBtn.addEventListener('click', () => { this.#openEyedropper(); });
|
|
351
|
+
readout.appendChild(eyedropperBtn);
|
|
352
|
+
}
|
|
353
|
+
this.appendChild(readout);
|
|
354
|
+
return { pad, readout };
|
|
355
|
+
}
|
|
356
|
+
/** Build one labeled channel row: a text label, a composed `<ui-slider>`, and a printed numeral span
|
|
357
|
+
* (ADR-0123 cl.8 non-color signifier — see the field doc above `#hueValueEl`). */
|
|
358
|
+
#makeChannelRow(label, channel) {
|
|
359
|
+
const row = document.createElement('div');
|
|
360
|
+
row.setAttribute('data-part', 'channel-row');
|
|
361
|
+
row.setAttribute('data-channel', channel);
|
|
362
|
+
const labelEl = document.createElement('span');
|
|
363
|
+
labelEl.setAttribute('data-part', 'channel-label');
|
|
364
|
+
labelEl.textContent = label;
|
|
365
|
+
row.appendChild(labelEl);
|
|
366
|
+
const slider = document.createElement('ui-slider');
|
|
367
|
+
slider.setAttribute('data-channel', channel);
|
|
368
|
+
slider.setAttribute('aria-label', label);
|
|
369
|
+
row.appendChild(slider);
|
|
370
|
+
const valueEl = document.createElement('span');
|
|
371
|
+
valueEl.setAttribute('data-part', 'channel-value');
|
|
372
|
+
row.appendChild(valueEl);
|
|
373
|
+
return { row, slider, valueEl };
|
|
374
|
+
}
|
|
375
|
+
// ── model → surface paint (LLD-C2) ──────────────────────────────────────────────────────────
|
|
376
|
+
#paint() {
|
|
377
|
+
const pad = this.#pad;
|
|
378
|
+
const thumb = this.#padThumb;
|
|
379
|
+
if (pad && thumb) {
|
|
380
|
+
const xPct = (this.#C / MAX_CHROMA) * 100;
|
|
381
|
+
const yPct = (1 - this.#L) * 100;
|
|
382
|
+
thumb.style.left = `${xPct}%`;
|
|
383
|
+
thumb.style.top = `${yPct}%`;
|
|
384
|
+
pad.setAttribute('aria-valuetext', `Chroma ${this.#C.toFixed(2)}, Lightness ${this.#L.toFixed(2)}`);
|
|
385
|
+
if (this.#lastPaintedHue !== this.#H) {
|
|
386
|
+
this.#lastPaintedHue = this.#H;
|
|
387
|
+
this.#drawPad();
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
if (this.#hueSlider)
|
|
391
|
+
this.#hueSlider.value = Math.round(this.#H);
|
|
392
|
+
if (this.#chromaSlider)
|
|
393
|
+
this.#chromaSlider.value = Number(this.#C.toFixed(3));
|
|
394
|
+
if (this.#lightnessSlider)
|
|
395
|
+
this.#lightnessSlider.value = Number(this.#L.toFixed(3));
|
|
396
|
+
if (this.#hueValueEl)
|
|
397
|
+
this.#hueValueEl.textContent = String(Math.round(this.#H));
|
|
398
|
+
if (this.#chromaValueEl)
|
|
399
|
+
this.#chromaValueEl.textContent = this.#C.toFixed(3);
|
|
400
|
+
if (this.#lightnessValueEl)
|
|
401
|
+
this.#lightnessValueEl.textContent = this.#L.toFixed(2);
|
|
402
|
+
if (this.#readoutField)
|
|
403
|
+
this.#readoutField.value = this.value;
|
|
404
|
+
if (this.#swatch)
|
|
405
|
+
this.#swatch.color = this.value;
|
|
406
|
+
}
|
|
407
|
+
/** Per-pixel OKLCH→sRGB canvas paint of the chroma(X)×lightness(Y) plane at the current hue (the adia
|
|
408
|
+
* `#drawArea` mechanism, promoted). DPR-capped `min(dpr,2)*0.5`. A no-op under jsdom — `getContext('2d')`
|
|
409
|
+
* returns null there (no canvas polyfill installed); the canvas pixels are NOT the test contract
|
|
410
|
+
* (LLD §11 — the browser leg asserts thumb position + emitted value, never the raster). */
|
|
411
|
+
#drawPad() {
|
|
412
|
+
const canvas = this.#canvas;
|
|
413
|
+
const pad = this.#pad;
|
|
414
|
+
if (!canvas || !pad)
|
|
415
|
+
return;
|
|
416
|
+
const ctx = canvas.getContext('2d');
|
|
417
|
+
if (!ctx)
|
|
418
|
+
return;
|
|
419
|
+
const dpr = Math.min(window.devicePixelRatio || 1, 2) * 0.5;
|
|
420
|
+
const w = Math.round(pad.clientWidth * dpr);
|
|
421
|
+
const h = Math.round(pad.clientHeight * dpr);
|
|
422
|
+
if (w <= 0 || h <= 0)
|
|
423
|
+
return;
|
|
424
|
+
canvas.width = w;
|
|
425
|
+
canvas.height = h;
|
|
426
|
+
const img = ctx.createImageData(w, h);
|
|
427
|
+
const data = img.data;
|
|
428
|
+
const hue = this.#H;
|
|
429
|
+
for (let y = 0; y < h; y++) {
|
|
430
|
+
const L = h <= 1 ? 1 : 1 - y / (h - 1);
|
|
431
|
+
for (let x = 0; x < w; x++) {
|
|
432
|
+
const C = w <= 1 ? 0 : (x / (w - 1)) * MAX_CHROMA;
|
|
433
|
+
const mappedC = gamutMapChroma(L, C, hue);
|
|
434
|
+
const [r, g, b] = oklchToRgb(L, mappedC, hue);
|
|
435
|
+
const i = (y * w + x) * 4;
|
|
436
|
+
data[i] = Math.round(r * 255);
|
|
437
|
+
data[i + 1] = Math.round(g * 255);
|
|
438
|
+
data[i + 2] = Math.round(b * 255);
|
|
439
|
+
data[i + 3] = 255;
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
ctx.putImageData(img, 0, 0);
|
|
443
|
+
}
|
|
444
|
+
// ── commit choke point (LLD-C2) ─────────────────────────────────────────────────────────────
|
|
445
|
+
#commit(kind) {
|
|
446
|
+
const serialized = serializeColor({ L: this.#L, C: this.#C, H: this.#H }, this.format);
|
|
447
|
+
this.#lastCommittedValue = serialized;
|
|
448
|
+
this.value = serialized;
|
|
449
|
+
// LLD-C6: cleared on a successful parse OR a pad/channel commit — a pad drag or channel step after a
|
|
450
|
+
// stuck-invalid readout entry must clear the customError, not leave the control invalid forever.
|
|
451
|
+
this.#readoutError.value = false;
|
|
452
|
+
this.#paint();
|
|
453
|
+
this.emit(kind);
|
|
454
|
+
}
|
|
455
|
+
/** The readout's committed (blur/Enter) entry — parses through `colorCodecOptions(format)` (LLD-C6: the
|
|
456
|
+
* standalone control does NOT instantiate the `valueCodec` controller; validity reads `#readoutError`
|
|
457
|
+
* directly). An unparseable entry surfaces `customError` and fires no `change` (SPEC-R5 AC2). */
|
|
458
|
+
#commitReadout(raw) {
|
|
459
|
+
const codec = colorCodecOptions(this.format);
|
|
460
|
+
const serialized = codec.parse(raw);
|
|
461
|
+
if (serialized === null) {
|
|
462
|
+
this.#readoutError.value = true;
|
|
463
|
+
return;
|
|
464
|
+
}
|
|
465
|
+
this.#readoutError.value = false;
|
|
466
|
+
const oklch = parseColor(serialized); // always parseable — it is the codec's own output
|
|
467
|
+
if (oklch) {
|
|
468
|
+
this.#L = oklch.L;
|
|
469
|
+
this.#C = oklch.C;
|
|
470
|
+
this.#H = oklch.H;
|
|
471
|
+
}
|
|
472
|
+
this.#commit('change');
|
|
473
|
+
}
|
|
474
|
+
// ── the pad's 2-axis keyboard (LLD-C5, SPEC-R7) ─────────────────────────────────────────────
|
|
475
|
+
#handlePadKey(event) {
|
|
476
|
+
if (this.effectiveDisabled())
|
|
477
|
+
return;
|
|
478
|
+
const key = event.key;
|
|
479
|
+
const NAV = new Set(['ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown', 'Home', 'End', 'PageUp', 'PageDown']);
|
|
480
|
+
if (!NAV.has(key))
|
|
481
|
+
return;
|
|
482
|
+
event.preventDefault(); // suppresses scroll + (in the overlay leg) anchor re-activation — ADR-0045/0048
|
|
483
|
+
const coarse = event.shiftKey;
|
|
484
|
+
const cStep = coarse ? 0.04 : 0.004; // the adia keyboard-step constants, promoted
|
|
485
|
+
const lStep = coarse ? 0.05 : 0.01;
|
|
486
|
+
switch (key) {
|
|
487
|
+
case 'ArrowRight':
|
|
488
|
+
this.#C = Math.min(MAX_CHROMA, this.#C + cStep);
|
|
489
|
+
break;
|
|
490
|
+
case 'ArrowLeft':
|
|
491
|
+
this.#C = Math.max(0, this.#C - cStep);
|
|
492
|
+
break;
|
|
493
|
+
case 'ArrowUp':
|
|
494
|
+
this.#L = Math.min(1, this.#L + lStep);
|
|
495
|
+
break;
|
|
496
|
+
case 'ArrowDown':
|
|
497
|
+
this.#L = Math.max(0, this.#L - lStep);
|
|
498
|
+
break;
|
|
499
|
+
case 'Home':
|
|
500
|
+
this.#C = 0;
|
|
501
|
+
break;
|
|
502
|
+
case 'End':
|
|
503
|
+
this.#C = MAX_CHROMA;
|
|
504
|
+
break;
|
|
505
|
+
case 'PageUp':
|
|
506
|
+
this.#L = 1;
|
|
507
|
+
break;
|
|
508
|
+
case 'PageDown':
|
|
509
|
+
this.#L = 0;
|
|
510
|
+
break;
|
|
511
|
+
default: return;
|
|
512
|
+
}
|
|
513
|
+
// SPEC-R7 AC2 — each key-step fires BOTH input and change (an atomic committed step, unlike the
|
|
514
|
+
// continuous pointer-drag gesture which streams input and commits change once on pointer-up).
|
|
515
|
+
this.#commit('input');
|
|
516
|
+
this.#commit('change');
|
|
517
|
+
}
|
|
518
|
+
/** The EyeDropper affordance's click handler (SPEC-R13) — opens the OS picker; a resolved sample commits
|
|
519
|
+
* through the model (`change`). Never a hard dependency: only wired when `'EyeDropper' in window`. */
|
|
520
|
+
#openEyedropper() {
|
|
521
|
+
const EyeDropperCtor = window.EyeDropper;
|
|
522
|
+
if (!EyeDropperCtor)
|
|
523
|
+
return;
|
|
524
|
+
const instance = new EyeDropperCtor();
|
|
525
|
+
instance.open().then((result) => {
|
|
526
|
+
const parsed = parseColor(result.sRGBHex);
|
|
527
|
+
if (parsed) {
|
|
528
|
+
this.#L = parsed.L;
|
|
529
|
+
this.#C = parsed.C;
|
|
530
|
+
this.#H = parsed.H;
|
|
531
|
+
this.#commit('change');
|
|
532
|
+
}
|
|
533
|
+
}).catch(() => { });
|
|
534
|
+
}
|
|
535
|
+
/** A once-per-element dev warning for an unparseable EXTERNAL `value` set (SPEC-R3 AC4) — the model is
|
|
536
|
+
* left intact, no throw. */
|
|
537
|
+
#warnOnce(raw) {
|
|
538
|
+
if (this.#warnedBadParse)
|
|
539
|
+
return;
|
|
540
|
+
this.#warnedBadParse = true;
|
|
541
|
+
console.warn(`<ui-color-picker>: could not parse value=${JSON.stringify(raw)}. Expected #rrggbb/#rgb or oklch(L C H). Keeping the prior color.`);
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
if (!customElements.get('ui-color-picker'))
|
|
545
|
+
customElements.define('ui-color-picker', UIColorPickerElement);
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
export declare const MAX_CHROMA = 0.4;
|
|
2
|
+
export interface Oklch {
|
|
3
|
+
L: number;
|
|
4
|
+
C: number;
|
|
5
|
+
H: number;
|
|
6
|
+
}
|
|
7
|
+
/** Convert an OKLCH triple to sRGB (0–1, clamped) — no gamut mapping applied. */
|
|
8
|
+
export declare function oklchToRgb(L: number, C: number, H: number): [number, number, number];
|
|
9
|
+
/** Format sRGB (0–1 channels) as a `#rrggbb` hex string. */
|
|
10
|
+
export declare function rgbToHex(r: number, g: number, b: number): string;
|
|
11
|
+
/** Convert an OKLCH triple directly to a `#rrggbb` hex string (no gamut mapping — call gamutMapChroma first). */
|
|
12
|
+
export declare function oklchToHex(L: number, C: number, H: number): string;
|
|
13
|
+
/** Parse a `#rgb`/`#rrggbb` hex string to an OKLCH triple. Assumes a well-formed 3/6-digit hex (validated by the caller). */
|
|
14
|
+
export declare function hexToOklch(hex: string): Oklch;
|
|
15
|
+
/**
|
|
16
|
+
* Reduce chroma until the OKLCH triple falls inside the sRGB gamut (8-iteration binary search — a
|
|
17
|
+
* ~MAX_CHROMA/256 chroma resolution, imperceptible and deterministic; the adia constant, LLD §12 risk).
|
|
18
|
+
* A no-op (returns `C` unchanged) when the triple is already in-gamut.
|
|
19
|
+
*/
|
|
20
|
+
export declare function gamutMapChroma(L: number, C: number, H: number): number;
|
|
21
|
+
/** Parse any accepted color syntax (`#rgb`/`#rrggbb`/`oklch(L C H)`) into an OKLCH triple, or `null` if unparseable. */
|
|
22
|
+
export declare function parseColor(input: string): Oklch | null;
|
|
23
|
+
/** The color-codec dialect: parse (any accepted syntax → the `format`-serialized canonical) / format
|
|
24
|
+
* (idempotent re-normalization) / errorMessage. Mirrors traits/value-codec.ts's `ValueCodecOptions` shape
|
|
25
|
+
* (LLD-C4/C5 frozen-interface table) without importing it — pure, zero-DOM (SPEC-R3 AC5). */
|
|
26
|
+
export interface ColorCodecOptions {
|
|
27
|
+
parse(display: string): string | null;
|
|
28
|
+
format(value: string): string;
|
|
29
|
+
errorMessage: string;
|
|
30
|
+
}
|
|
31
|
+
/** Serialize an OKLCH triple through `format` — gamut-mapped sRGB hex for `'hex'`, authored-chroma `oklch(L C H)` for `'oklch'`. */
|
|
32
|
+
export declare function serializeColor(oklch: Oklch, format: 'hex' | 'oklch'): string;
|
|
33
|
+
/**
|
|
34
|
+
* The color-codec dialect factory (LLD-C3): `parse(display) → canonical | null` accepts any recognized
|
|
35
|
+
* syntax and re-serializes into the requested `format`; `format(canonical) → canonical` re-normalizes
|
|
36
|
+
* (idempotent — parses then re-serializes in the same format). Pure — no DOM (SPEC-R3 AC5); reused as
|
|
37
|
+
* plain functions by the standalone control AND wired through the `valueCodec` trait by the `type=color`
|
|
38
|
+
* text-field leg (LLD-C9).
|
|
39
|
+
*/
|
|
40
|
+
export declare function colorCodecOptions(format: 'hex' | 'oklch'): ColorCodecOptions;
|