@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.
Files changed (271) hide show
  1. package/dist/base-styles.css +8 -0
  2. package/dist/component-styles.css +147 -0
  3. package/dist/controls/_base/index.d.ts +3 -0
  4. package/dist/controls/_base/index.js +9 -0
  5. package/dist/controls/_base/indicator-element.d.ts +132 -0
  6. package/dist/controls/_base/indicator-element.js +158 -0
  7. package/dist/controls/_base/listbox-element.d.ts +70 -0
  8. package/dist/controls/_base/listbox-element.js +74 -0
  9. package/dist/controls/_base/range-element.d.ts +120 -0
  10. package/dist/controls/_base/range-element.js +158 -0
  11. package/dist/controls/_surface/container-box.css +301 -0
  12. package/dist/controls/_surface/container.css +156 -0
  13. package/dist/controls/_token-surface/token-surface.d.ts +35 -0
  14. package/dist/controls/_token-surface/token-surface.js +97 -0
  15. package/dist/controls/attachment/attachment-meta.d.ts +8 -0
  16. package/dist/controls/attachment/attachment-meta.js +84 -0
  17. package/dist/controls/attachment/attachment.css +91 -0
  18. package/dist/controls/attachment/attachment.d.ts +40 -0
  19. package/dist/controls/attachment/attachment.js +70 -0
  20. package/dist/controls/avatar/avatar-initials.d.ts +4 -0
  21. package/dist/controls/avatar/avatar-initials.js +30 -0
  22. package/dist/controls/avatar/avatar.css +87 -0
  23. package/dist/controls/avatar/avatar.d.ts +40 -0
  24. package/dist/controls/avatar/avatar.js +77 -0
  25. package/dist/controls/badge/badge.css +175 -0
  26. package/dist/controls/badge/badge.d.ts +36 -0
  27. package/dist/controls/badge/badge.js +71 -0
  28. package/dist/controls/bar-chart/bar-chart.css +110 -0
  29. package/dist/controls/bar-chart/bar-chart.d.ts +26 -0
  30. package/dist/controls/bar-chart/bar-chart.js +80 -0
  31. package/dist/controls/bar-chart/bar-math.d.ts +31 -0
  32. package/dist/controls/bar-chart/bar-math.js +106 -0
  33. package/dist/controls/button/button.css +263 -0
  34. package/dist/controls/button/button.d.ts +61 -0
  35. package/dist/controls/button/button.js +115 -0
  36. package/dist/controls/calendar/calendar.css +495 -0
  37. package/dist/controls/calendar/calendar.d.ts +161 -0
  38. package/dist/controls/calendar/calendar.js +1004 -0
  39. package/dist/controls/card/card-content.d.ts +24 -0
  40. package/dist/controls/card/card-content.js +133 -0
  41. package/dist/controls/card/card-footer.d.ts +3 -0
  42. package/dist/controls/card/card-footer.js +16 -0
  43. package/dist/controls/card/card-header.d.ts +3 -0
  44. package/dist/controls/card/card-header.js +18 -0
  45. package/dist/controls/card/card.css +445 -0
  46. package/dist/controls/card/card.d.ts +51 -0
  47. package/dist/controls/card/card.js +61 -0
  48. package/dist/controls/checkbox/checkbox.css +204 -0
  49. package/dist/controls/checkbox/checkbox.d.ts +114 -0
  50. package/dist/controls/checkbox/checkbox.js +130 -0
  51. package/dist/controls/code/code.css +72 -0
  52. package/dist/controls/code/code.d.ts +23 -0
  53. package/dist/controls/code/code.js +40 -0
  54. package/dist/controls/color-picker/color-picker.css +209 -0
  55. package/dist/controls/color-picker/color-picker.d.ts +84 -0
  56. package/dist/controls/color-picker/color-picker.js +545 -0
  57. package/dist/controls/color-picker/color.d.ts +40 -0
  58. package/dist/controls/color-picker/color.js +159 -0
  59. package/dist/controls/column/column.css +151 -0
  60. package/dist/controls/column/column.d.ts +107 -0
  61. package/dist/controls/column/column.js +57 -0
  62. package/dist/controls/combo-box/combo-box.css +397 -0
  63. package/dist/controls/combo-box/combo-box.d.ts +133 -0
  64. package/dist/controls/combo-box/combo-box.js +680 -0
  65. package/dist/controls/command-modal/command-modal.css +226 -0
  66. package/dist/controls/command-modal/command-modal.d.ts +42 -0
  67. package/dist/controls/command-modal/command-modal.js +315 -0
  68. package/dist/controls/disclosure/disclosure.css +162 -0
  69. package/dist/controls/disclosure/disclosure.d.ts +37 -0
  70. package/dist/controls/disclosure/disclosure.js +207 -0
  71. package/dist/controls/field/field.css +140 -0
  72. package/dist/controls/field/field.d.ts +27 -0
  73. package/dist/controls/field/field.js +228 -0
  74. package/dist/controls/form-provider/form-provider.css +40 -0
  75. package/dist/controls/form-provider/form-provider.d.ts +42 -0
  76. package/dist/controls/form-provider/form-provider.js +105 -0
  77. package/dist/controls/grid/grid.css +72 -0
  78. package/dist/controls/grid/grid.d.ts +60 -0
  79. package/dist/controls/grid/grid.js +56 -0
  80. package/dist/controls/icon/icon.css +37 -0
  81. package/dist/controls/icon/icon.d.ts +25 -0
  82. package/dist/controls/icon/icon.js +55 -0
  83. package/dist/controls/index.d.ts +57 -0
  84. package/dist/controls/index.js +136 -0
  85. package/dist/controls/ladder/ladder.css +89 -0
  86. package/dist/controls/ladder/ladder.d.ts +27 -0
  87. package/dist/controls/ladder/ladder.js +78 -0
  88. package/dist/controls/list/list.css +118 -0
  89. package/dist/controls/list/list.d.ts +84 -0
  90. package/dist/controls/list/list.js +48 -0
  91. package/dist/controls/menu/menu.css +195 -0
  92. package/dist/controls/menu/menu.d.ts +44 -0
  93. package/dist/controls/menu/menu.js +248 -0
  94. package/dist/controls/modal/modal.css +99 -0
  95. package/dist/controls/modal/modal.d.ts +61 -0
  96. package/dist/controls/modal/modal.js +155 -0
  97. package/dist/controls/popover/popover.css +83 -0
  98. package/dist/controls/popover/popover.d.ts +44 -0
  99. package/dist/controls/popover/popover.js +164 -0
  100. package/dist/controls/progress/progress.css +106 -0
  101. package/dist/controls/progress/progress.d.ts +28 -0
  102. package/dist/controls/progress/progress.js +80 -0
  103. package/dist/controls/radio/radio-group.css +84 -0
  104. package/dist/controls/radio/radio-group.d.ts +152 -0
  105. package/dist/controls/radio/radio-group.js +350 -0
  106. package/dist/controls/radio/radio.css +219 -0
  107. package/dist/controls/radio/radio.d.ts +19 -0
  108. package/dist/controls/radio/radio.js +75 -0
  109. package/dist/controls/ramp/ramp.css +75 -0
  110. package/dist/controls/ramp/ramp.d.ts +29 -0
  111. package/dist/controls/ramp/ramp.js +72 -0
  112. package/dist/controls/row/row.css +152 -0
  113. package/dist/controls/row/row.d.ts +95 -0
  114. package/dist/controls/row/row.js +45 -0
  115. package/dist/controls/segment/segment.css +66 -0
  116. package/dist/controls/segment/segment.d.ts +3 -0
  117. package/dist/controls/segment/segment.js +26 -0
  118. package/dist/controls/segmented-control/segmented-control.css +189 -0
  119. package/dist/controls/segmented-control/segmented-control.d.ts +20 -0
  120. package/dist/controls/segmented-control/segmented-control.js +64 -0
  121. package/dist/controls/select/select.css +467 -0
  122. package/dist/controls/select/select.d.ts +147 -0
  123. package/dist/controls/select/select.js +538 -0
  124. package/dist/controls/slider/slider.css +231 -0
  125. package/dist/controls/slider/slider.d.ts +111 -0
  126. package/dist/controls/slider/slider.js +60 -0
  127. package/dist/controls/slider-multi/slider-multi.css +197 -0
  128. package/dist/controls/slider-multi/slider-multi.d.ts +149 -0
  129. package/dist/controls/slider-multi/slider-multi.js +380 -0
  130. package/dist/controls/sparkline/sparkline-math.d.ts +23 -0
  131. package/dist/controls/sparkline/sparkline-math.js +79 -0
  132. package/dist/controls/sparkline/sparkline.css +61 -0
  133. package/dist/controls/sparkline/sparkline.d.ts +27 -0
  134. package/dist/controls/sparkline/sparkline.js +74 -0
  135. package/dist/controls/split/constrain.d.ts +44 -0
  136. package/dist/controls/split/constrain.js +126 -0
  137. package/dist/controls/split/split-pane.css +63 -0
  138. package/dist/controls/split/split-pane.d.ts +49 -0
  139. package/dist/controls/split/split-pane.js +42 -0
  140. package/dist/controls/split/split.css +127 -0
  141. package/dist/controls/split/split.d.ts +42 -0
  142. package/dist/controls/split/split.js +364 -0
  143. package/dist/controls/stat/stat-model.d.ts +22 -0
  144. package/dist/controls/stat/stat-model.js +69 -0
  145. package/dist/controls/stat/stat.css +101 -0
  146. package/dist/controls/stat/stat.d.ts +30 -0
  147. package/dist/controls/stat/stat.js +73 -0
  148. package/dist/controls/status-stream/status-stream.css +151 -0
  149. package/dist/controls/status-stream/status-stream.d.ts +108 -0
  150. package/dist/controls/status-stream/status-stream.js +417 -0
  151. package/dist/controls/swatch/swatch.css +66 -0
  152. package/dist/controls/swatch/swatch.d.ts +28 -0
  153. package/dist/controls/swatch/swatch.js +74 -0
  154. package/dist/controls/swiper/swiper-item.css +17 -0
  155. package/dist/controls/swiper/swiper-item.d.ts +31 -0
  156. package/dist/controls/swiper/swiper-item.js +47 -0
  157. package/dist/controls/swiper/swiper-label.css +10 -0
  158. package/dist/controls/swiper/swiper-label.d.ts +4 -0
  159. package/dist/controls/swiper/swiper-label.js +20 -0
  160. package/dist/controls/swiper/swiper-paddles.css +26 -0
  161. package/dist/controls/swiper/swiper-paddles.d.ts +10 -0
  162. package/dist/controls/swiper/swiper-paddles.js +62 -0
  163. package/dist/controls/swiper/swiper-pagination.css +60 -0
  164. package/dist/controls/swiper/swiper-pagination.d.ts +31 -0
  165. package/dist/controls/swiper/swiper-pagination.js +78 -0
  166. package/dist/controls/swiper/swiper.css +153 -0
  167. package/dist/controls/swiper/swiper.d.ts +158 -0
  168. package/dist/controls/swiper/swiper.js +659 -0
  169. package/dist/controls/switch/switch.css +194 -0
  170. package/dist/controls/switch/switch.d.ts +5 -0
  171. package/dist/controls/switch/switch.js +18 -0
  172. package/dist/controls/table/table-model.d.ts +43 -0
  173. package/dist/controls/table/table-model.js +124 -0
  174. package/dist/controls/table/table.css +90 -0
  175. package/dist/controls/table/table.d.ts +29 -0
  176. package/dist/controls/table/table.js +178 -0
  177. package/dist/controls/tabs/tab-panel.d.ts +10 -0
  178. package/dist/controls/tabs/tab-panel.js +89 -0
  179. package/dist/controls/tabs/tab.d.ts +26 -0
  180. package/dist/controls/tabs/tab.js +67 -0
  181. package/dist/controls/tabs/tabs.css +181 -0
  182. package/dist/controls/tabs/tabs.d.ts +61 -0
  183. package/dist/controls/tabs/tabs.js +176 -0
  184. package/dist/controls/text/href.d.ts +26 -0
  185. package/dist/controls/text/href.js +48 -0
  186. package/dist/controls/text/text.css +328 -0
  187. package/dist/controls/text/text.d.ts +85 -0
  188. package/dist/controls/text/text.js +273 -0
  189. package/dist/controls/text-field/text-field.css +498 -0
  190. package/dist/controls/text-field/text-field.d.ts +226 -0
  191. package/dist/controls/text-field/text-field.js +905 -0
  192. package/dist/controls/textarea/textarea.css +251 -0
  193. package/dist/controls/textarea/textarea.d.ts +136 -0
  194. package/dist/controls/textarea/textarea.js +312 -0
  195. package/dist/controls/theme-provider/theme-provider.css +48 -0
  196. package/dist/controls/theme-provider/theme-provider.d.ts +59 -0
  197. package/dist/controls/theme-provider/theme-provider.js +40 -0
  198. package/dist/controls/timeline/timeline.css +33 -0
  199. package/dist/controls/timeline/timeline.d.ts +39 -0
  200. package/dist/controls/timeline/timeline.js +52 -0
  201. package/dist/controls/timeline-item/timeline-item.css +495 -0
  202. package/dist/controls/timeline-item/timeline-item.d.ts +79 -0
  203. package/dist/controls/timeline-item/timeline-item.js +360 -0
  204. package/dist/controls/toast/toast-region.css +56 -0
  205. package/dist/controls/toast/toast-region.d.ts +20 -0
  206. package/dist/controls/toast/toast-region.js +113 -0
  207. package/dist/controls/toast/toast.css +96 -0
  208. package/dist/controls/toast/toast.d.ts +28 -0
  209. package/dist/controls/toast/toast.js +191 -0
  210. package/dist/controls/toolbar/toolbar.css +125 -0
  211. package/dist/controls/toolbar/toolbar.d.ts +108 -0
  212. package/dist/controls/toolbar/toolbar.js +68 -0
  213. package/dist/controls/tooltip/tooltip.css +80 -0
  214. package/dist/controls/tooltip/tooltip.d.ts +57 -0
  215. package/dist/controls/tooltip/tooltip.js +205 -0
  216. package/dist/descriptor/component-descriptor.d.ts +112 -0
  217. package/dist/descriptor/component-descriptor.js +492 -0
  218. package/dist/descriptor/index.d.ts +1 -0
  219. package/dist/descriptor/index.js +5 -0
  220. package/dist/dom/container.d.ts +45 -0
  221. package/dist/dom/container.js +60 -0
  222. package/dist/dom/element.d.ts +98 -0
  223. package/dist/dom/element.js +221 -0
  224. package/dist/dom/form.d.ts +240 -0
  225. package/dist/dom/form.js +399 -0
  226. package/dist/dom/index.d.ts +10 -0
  227. package/dist/dom/index.js +31 -0
  228. package/dist/dom/props.d.ts +56 -0
  229. package/dist/dom/props.js +231 -0
  230. package/dist/dom/repeat.d.ts +10 -0
  231. package/dist/dom/repeat.js +202 -0
  232. package/dist/dom/template.d.ts +161 -0
  233. package/dist/dom/template.js +634 -0
  234. package/dist/dom/watch.d.ts +11 -0
  235. package/dist/dom/watch.js +82 -0
  236. package/dist/foundation-styles.css +9 -0
  237. package/dist/index.d.ts +2 -0
  238. package/dist/index.js +3 -0
  239. package/dist/reactive/graph.d.ts +65 -0
  240. package/dist/reactive/graph.js +310 -0
  241. package/dist/reactive/index.d.ts +3 -0
  242. package/dist/reactive/index.js +4 -0
  243. package/dist/reactive/scheduler.d.ts +13 -0
  244. package/dist/reactive/scheduler.js +49 -0
  245. package/dist/traits/area-drag.d.ts +12 -0
  246. package/dist/traits/area-drag.js +70 -0
  247. package/dist/traits/form-registry.d.ts +36 -0
  248. package/dist/traits/form-registry.js +108 -0
  249. package/dist/traits/index.d.ts +24 -0
  250. package/dist/traits/index.js +19 -0
  251. package/dist/traits/overlay.d.ts +50 -0
  252. package/dist/traits/overlay.js +277 -0
  253. package/dist/traits/pane-resize.d.ts +26 -0
  254. package/dist/traits/pane-resize.js +103 -0
  255. package/dist/traits/press-activation.d.ts +11 -0
  256. package/dist/traits/press-activation.js +55 -0
  257. package/dist/traits/roving-focus.d.ts +52 -0
  258. package/dist/traits/roving-focus.js +214 -0
  259. package/dist/traits/scroll-fade.d.ts +28 -0
  260. package/dist/traits/scroll-fade.js +120 -0
  261. package/dist/traits/selection-commit.d.ts +30 -0
  262. package/dist/traits/selection-commit.js +146 -0
  263. package/dist/traits/tabbable.d.ts +12 -0
  264. package/dist/traits/tabbable.js +59 -0
  265. package/dist/traits/track-user-invalid.d.ts +36 -0
  266. package/dist/traits/track-user-invalid.js +52 -0
  267. package/dist/traits/value-codec.d.ts +120 -0
  268. package/dist/traits/value-codec.js +497 -0
  269. package/dist/traits/value-drag.d.ts +18 -0
  270. package/dist/traits/value-drag.js +84 -0
  271. 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;