@spfx-fast/sharepoint-ui 0.0.0-stage → 0.4.0

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 (158) hide show
  1. package/AGENTS.md +211 -0
  2. package/LICENSE +21 -0
  3. package/README.md +897 -2
  4. package/components.json +5810 -0
  5. package/dist/appearance.d.ts +6 -0
  6. package/dist/appearance.js +217 -0
  7. package/dist/elements/accordion.d.ts +9 -0
  8. package/dist/elements/accordion.js +338 -0
  9. package/dist/elements/alert.d.ts +2 -0
  10. package/dist/elements/alert.js +62 -0
  11. package/dist/elements/avatar-group-primitives.d.ts +3 -0
  12. package/dist/elements/avatar-group-primitives.js +189 -0
  13. package/dist/elements/avatar-group.d.ts +2 -0
  14. package/dist/elements/avatar-group.js +110 -0
  15. package/dist/elements/avatar.d.ts +4 -0
  16. package/dist/elements/avatar.js +251 -0
  17. package/dist/elements/badge.d.ts +8 -0
  18. package/dist/elements/badge.js +182 -0
  19. package/dist/elements/breadcrumb.d.ts +4 -0
  20. package/dist/elements/breadcrumb.js +233 -0
  21. package/dist/elements/button-group.d.ts +1 -0
  22. package/dist/elements/button-group.js +20 -0
  23. package/dist/elements/button-variants.d.ts +6 -0
  24. package/dist/elements/button-variants.js +339 -0
  25. package/dist/elements/button.d.ts +5 -0
  26. package/dist/elements/button.js +233 -0
  27. package/dist/elements/card-primitives.d.ts +3 -0
  28. package/dist/elements/card-primitives.js +83 -0
  29. package/dist/elements/card.d.ts +5 -0
  30. package/dist/elements/card.js +213 -0
  31. package/dist/elements/carousel.d.ts +92 -0
  32. package/dist/elements/carousel.js +1112 -0
  33. package/dist/elements/color-picker.d.ts +24 -0
  34. package/dist/elements/color-picker.js +1004 -0
  35. package/dist/elements/combobox.d.ts +1 -0
  36. package/dist/elements/combobox.js +1012 -0
  37. package/dist/elements/data-grid-model.d.ts +117 -0
  38. package/dist/elements/data-grid-model.js +294 -0
  39. package/dist/elements/data-grid.d.ts +29 -0
  40. package/dist/elements/data-grid.js +1567 -0
  41. package/dist/elements/date-picker.d.ts +1 -0
  42. package/dist/elements/date-picker.js +486 -0
  43. package/dist/elements/dialog.d.ts +23 -0
  44. package/dist/elements/dialog.js +409 -0
  45. package/dist/elements/divider.d.ts +2 -0
  46. package/dist/elements/divider.js +66 -0
  47. package/dist/elements/drawer.d.ts +3 -0
  48. package/dist/elements/drawer.js +308 -0
  49. package/dist/elements/field-composition.d.ts +26 -0
  50. package/dist/elements/field-composition.js +133 -0
  51. package/dist/elements/field.d.ts +1 -0
  52. package/dist/elements/field.js +67 -0
  53. package/dist/elements/file-upload.d.ts +6 -0
  54. package/dist/elements/file-upload.js +211 -0
  55. package/dist/elements/floating-position.d.ts +26 -0
  56. package/dist/elements/floating-position.js +39 -0
  57. package/dist/elements/image.d.ts +28 -0
  58. package/dist/elements/image.js +38 -0
  59. package/dist/elements/info.d.ts +9 -0
  60. package/dist/elements/info.js +102 -0
  61. package/dist/elements/input.d.ts +5 -0
  62. package/dist/elements/input.js +255 -0
  63. package/dist/elements/label.d.ts +15 -0
  64. package/dist/elements/label.js +56 -0
  65. package/dist/elements/link.d.ts +1 -0
  66. package/dist/elements/link.js +92 -0
  67. package/dist/elements/list.d.ts +19 -0
  68. package/dist/elements/list.js +297 -0
  69. package/dist/elements/menu.d.ts +33 -0
  70. package/dist/elements/menu.js +1103 -0
  71. package/dist/elements/message-bar.d.ts +10 -0
  72. package/dist/elements/message-bar.js +299 -0
  73. package/dist/elements/nav.d.ts +32 -0
  74. package/dist/elements/nav.js +791 -0
  75. package/dist/elements/overflow.d.ts +9 -0
  76. package/dist/elements/overflow.js +218 -0
  77. package/dist/elements/persona.d.ts +3 -0
  78. package/dist/elements/persona.js +124 -0
  79. package/dist/elements/popover.d.ts +77 -0
  80. package/dist/elements/popover.js +840 -0
  81. package/dist/elements/progress-bar.d.ts +4 -0
  82. package/dist/elements/progress-bar.js +82 -0
  83. package/dist/elements/radio-group.d.ts +1 -0
  84. package/dist/elements/radio-group.js +303 -0
  85. package/dist/elements/rating-display.d.ts +2 -0
  86. package/dist/elements/rating-display.js +125 -0
  87. package/dist/elements/rating.d.ts +2 -0
  88. package/dist/elements/rating.js +206 -0
  89. package/dist/elements/rich-text-editor.d.ts +18 -0
  90. package/dist/elements/rich-text-editor.js +508 -0
  91. package/dist/elements/search.d.ts +3 -0
  92. package/dist/elements/search.js +207 -0
  93. package/dist/elements/select.d.ts +6 -0
  94. package/dist/elements/select.js +291 -0
  95. package/dist/elements/shared.d.ts +19 -0
  96. package/dist/elements/shared.js +396 -0
  97. package/dist/elements/skeleton.d.ts +6 -0
  98. package/dist/elements/skeleton.js +91 -0
  99. package/dist/elements/slider.d.ts +1 -0
  100. package/dist/elements/slider.js +248 -0
  101. package/dist/elements/spin-button.d.ts +7 -0
  102. package/dist/elements/spin-button.js +499 -0
  103. package/dist/elements/spinner.d.ts +4 -0
  104. package/dist/elements/spinner.js +124 -0
  105. package/dist/elements/swatch-picker.d.ts +5 -0
  106. package/dist/elements/swatch-picker.js +363 -0
  107. package/dist/elements/table.d.ts +21 -0
  108. package/dist/elements/table.js +261 -0
  109. package/dist/elements/tabs.d.ts +1 -0
  110. package/dist/elements/tabs.js +276 -0
  111. package/dist/elements/tag-picker.d.ts +29 -0
  112. package/dist/elements/tag-picker.js +697 -0
  113. package/dist/elements/tags.d.ts +21 -0
  114. package/dist/elements/tags.js +421 -0
  115. package/dist/elements/teaching-popover.d.ts +32 -0
  116. package/dist/elements/teaching-popover.js +412 -0
  117. package/dist/elements/text.d.ts +5 -0
  118. package/dist/elements/text.js +69 -0
  119. package/dist/elements/textarea.d.ts +1 -0
  120. package/dist/elements/textarea.js +182 -0
  121. package/dist/elements/toast.d.ts +25 -0
  122. package/dist/elements/toast.js +337 -0
  123. package/dist/elements/toaster.d.ts +6 -0
  124. package/dist/elements/toaster.js +356 -0
  125. package/dist/elements/toggle.d.ts +2 -0
  126. package/dist/elements/toggle.js +246 -0
  127. package/dist/elements/toolbar.d.ts +12 -0
  128. package/dist/elements/toolbar.js +381 -0
  129. package/dist/elements/tooltip.d.ts +76 -0
  130. package/dist/elements/tooltip.js +510 -0
  131. package/dist/elements/tree-model.d.ts +19 -0
  132. package/dist/elements/tree-model.js +34 -0
  133. package/dist/elements/tree.d.ts +5 -0
  134. package/dist/elements/tree.js +273 -0
  135. package/dist/elements/view-card.d.ts +2 -0
  136. package/dist/elements/view-card.js +78 -0
  137. package/dist/elements/virtual-list.d.ts +41 -0
  138. package/dist/elements/virtual-list.js +369 -0
  139. package/dist/elements.d.ts +72 -0
  140. package/dist/elements.js +207 -0
  141. package/dist/index.d.ts +76 -0
  142. package/dist/index.js +66 -0
  143. package/dist/react-theme-styles.d.ts +2 -0
  144. package/dist/react-theme-styles.js +75 -0
  145. package/dist/react.d.ts +4540 -0
  146. package/dist/react.js +6799 -0
  147. package/dist/theme.d.ts +169 -0
  148. package/dist/theme.js +167 -0
  149. package/dist/virtualizer-hooks.d.ts +80 -0
  150. package/dist/virtualizer-hooks.js +253 -0
  151. package/package.json +87 -4
  152. package/scripts/generate-token-index.mjs +43 -0
  153. package/scripts/sync-component-catalog-signatures.mjs +55 -0
  154. package/scripts/validate-component-catalog.mjs +44 -0
  155. package/skills/sharepoint-ui/SKILL.md +12 -0
  156. package/skills/sharepoint-ui/references/choosing-components.md +20 -0
  157. package/skills/sharepoint-ui/references/sharepoint-patterns.md +17 -0
  158. package/tokens.json +3777 -0
@@ -0,0 +1,1004 @@
1
+ import { base, syncHostAccessibleName } from "./shared.js";
2
+ function pickerFor(control) { return control.closest("spui-color-picker"); }
3
+ const standaloneControls = new WeakMap();
4
+ function parseControlColor(raw) {
5
+ if (!raw)
6
+ return undefined;
7
+ try {
8
+ const value = JSON.parse(raw);
9
+ if (!value || typeof value !== "object")
10
+ return undefined;
11
+ const { h, s, v, a } = value;
12
+ if (![h, s, v].every(channel => typeof channel === "number" && Number.isFinite(channel)))
13
+ return undefined;
14
+ return { h: ((h % 360) + 360) % 360, s: Math.max(0, Math.min(1, s)), v: Math.max(0, Math.min(1, v)), a: typeof a === "number" && Number.isFinite(a) ? Math.max(0, Math.min(1, a)) : 1 };
15
+ }
16
+ catch {
17
+ return undefined;
18
+ }
19
+ }
20
+ function standaloneController(control) {
21
+ const state = standaloneControls.get(control) ?? { color: { h: 0, s: 0, v: 0, a: 1 }, dirty: false };
22
+ standaloneControls.set(control, state);
23
+ const read = parseControlColor;
24
+ const apply = (color, emit, sourceEvent) => {
25
+ const normalized = read(JSON.stringify(color));
26
+ if (!normalized)
27
+ return;
28
+ state.color = normalized;
29
+ state.dirty = state.dirty || emit;
30
+ control.syncFromPicker?.();
31
+ if (emit)
32
+ control.dispatchEvent(new CustomEvent("spui-color-change", { detail: { color: { ...normalized }, event: sourceEvent, type: "change" }, bubbles: true, composed: true }));
33
+ };
34
+ const initial = read(control.getAttribute("color")) ?? read(control.getAttribute("default-color"));
35
+ if (initial && !state.dirty)
36
+ state.color = initial;
37
+ return Object.assign(control, {
38
+ getColorState: () => {
39
+ const hex = hsvToHex(state.color.h, state.color.s, state.color.v);
40
+ return { value: hex, alpha: state.color.a ?? 1, hue: state.color.h, saturation: state.color.s, brightness: state.color.v, color: { ...state.color }, disabled: control.hasAttribute("disabled"), readOnly: control.hasAttribute("read-only") };
41
+ },
42
+ setHsvFromControl: (h, s, v, _source, sourceEvent) => apply({ ...state.color, h, s, v }, true, sourceEvent),
43
+ setAlphaFromControl: (a, _source, sourceEvent) => apply({ ...state.color, a }, true, sourceEvent),
44
+ setControlColor: (color) => apply(color, false),
45
+ getControlLabel: (name) => control.getAttribute(name) ?? "",
46
+ });
47
+ }
48
+ function controlController(control) { return pickerFor(control) ?? standaloneController(control); }
49
+ export function defineColorArea() {
50
+ if (customElements.get("spui-color-area"))
51
+ return;
52
+ class SpuiColorArea extends HTMLElement {
53
+ static observedAttributes = ["aria-label", "shape", "disabled", "color", "default-color"];
54
+ picker = null;
55
+ area;
56
+ inputX;
57
+ inputY;
58
+ boundInputX;
59
+ boundInputY;
60
+ connectedCallback() {
61
+ if (!this.shadowRoot)
62
+ this.render();
63
+ this.picker = controlController(this);
64
+ this.syncFromPicker();
65
+ }
66
+ get color() { return this.picker?.getColorState().color ?? { h: 0, s: 0, v: 0, a: 1 }; }
67
+ set color(value) { const controller = (this.picker ?? controlController(this)); controller.setControlColor?.(value); }
68
+ get defaultColor() { return parseControlColor(this.getAttribute("default-color") || this.getAttribute("color")) ?? this.color; }
69
+ set defaultColor(value) { this.setAttribute("default-color", JSON.stringify(value)); }
70
+ focus(options) { this.inputX?.focus(options); }
71
+ syncFromPicker() {
72
+ const state = this.picker?.getColorState();
73
+ if (!state || !this.area)
74
+ return;
75
+ this.inputX = this.axisInput("x");
76
+ this.inputY = this.axisInput("y");
77
+ this.area.style.setProperty("--picker-hue", `${state.hue}deg`);
78
+ this.area.style.setProperty("--picker-saturation", `${state.saturation * 100}%`);
79
+ this.area.style.setProperty("--picker-brightness", `${(1 - state.brightness) * 100}%`);
80
+ const shape = this.getAttribute("shape") || this.picker?.getAttribute("shape");
81
+ this.style.setProperty("--picker-radius", shape === "square" ? "var(--spui-control-radius,4px)" : "10px");
82
+ this.area.removeAttribute("aria-valuenow");
83
+ this.area.removeAttribute("aria-valuetext");
84
+ this.area.setAttribute("aria-label", this.getAttribute("aria-label") || this.picker?.getControlLabel("area-label") || "Saturation and brightness");
85
+ this.area.setAttribute("aria-disabled", String(state.disabled || state.readOnly || this.hasAttribute("disabled")));
86
+ this.area.removeAttribute("aria-readonly");
87
+ this.inputX.value = String(Math.round(state.saturation * 100));
88
+ this.inputY.value = String(Math.round(state.brightness * 100));
89
+ this.inputX.disabled = this.inputY.disabled = state.disabled || state.readOnly || this.hasAttribute("disabled");
90
+ this.inputX.setAttribute("aria-valuetext", String(Math.round(state.saturation * 100)) + "% saturation");
91
+ this.inputY.setAttribute("aria-valuetext", String(Math.round(state.brightness * 100)) + "% brightness");
92
+ this.inputX.setAttribute("aria-valuenow", String(Math.round(state.saturation * 100)));
93
+ this.inputY.setAttribute("aria-valuenow", String(Math.round(state.brightness * 100)));
94
+ this.inputX.setAttribute("aria-readonly", String(state.readOnly));
95
+ this.inputY.setAttribute("aria-readonly", String(state.readOnly));
96
+ const thumb = this.shadowRoot?.querySelector(".thumb") ?? this.shadowRoot?.querySelector('slot[name="thumb"]')?.assignedElements()[0];
97
+ if (thumb) {
98
+ thumb.style.insetInlineStart = String(state.saturation * 100) + "%";
99
+ thumb.style.insetBlockStart = String((1 - state.brightness) * 100) + "%";
100
+ }
101
+ }
102
+ axisInput(axis) {
103
+ const slot = this.shadowRoot?.querySelector(`slot[name="input${axis.toUpperCase()}"]`);
104
+ const fallback = this.shadowRoot?.querySelector(`.input-${axis}`);
105
+ const assigned = slot?.assignedElements().find((element) => element instanceof HTMLInputElement);
106
+ const input = assigned ?? fallback;
107
+ if (input) {
108
+ input.type = "range";
109
+ input.min = "0";
110
+ input.max = "100";
111
+ input.step = "1";
112
+ if (!input.hasAttribute("aria-label") && !input.hasAttribute("aria-labelledby"))
113
+ input.setAttribute("aria-label", this.picker?.getControlLabel(axis === "x" ? "saturation-label" : "value-label") || (axis === "x" ? "Saturation" : "Brightness"));
114
+ fallback.hidden = Boolean(assigned);
115
+ fallback.setAttribute("aria-hidden", String(Boolean(assigned)));
116
+ const bound = axis === "x" ? this.boundInputX : this.boundInputY;
117
+ if (bound !== input) {
118
+ bound?.removeEventListener("input", axis === "x" ? this.handleAxisX : this.handleAxisY);
119
+ input.addEventListener("input", axis === "x" ? this.handleAxisX : this.handleAxisY);
120
+ if (axis === "x")
121
+ this.boundInputX = input;
122
+ else
123
+ this.boundInputY = input;
124
+ }
125
+ }
126
+ return input;
127
+ }
128
+ handleAxisX = (event) => { if (this.blocked())
129
+ return; const state = this.picker?.getColorState(); if (state)
130
+ this.picker?.setHsvFromControl(state.hue, Number(event.currentTarget.value) / 100, state.brightness, this, event); };
131
+ handleAxisY = (event) => { if (this.blocked())
132
+ return; const state = this.picker?.getColorState(); if (state)
133
+ this.picker?.setHsvFromControl(state.hue, state.saturation, Number(event.currentTarget.value) / 100, this, event); };
134
+ attributeChangedCallback(name) { if (name === "color" || name === "default-color")
135
+ this.color = this.defaultColor; this.syncFromPicker(); }
136
+ render() {
137
+ const shadow = this.attachShadow({ mode: "open" });
138
+ const style = document.createElement("style");
139
+ style.textContent = `${base}:host{display:block;min-width:0}.area{position:relative;width:min(260px,100%);height:160px;border:1px solid var(--spui-ui-stroke,var(--colorNeutralStroke1,#8a8886));border-radius:var(--picker-radius,var(--spui-control-radius,8px));background-image:linear-gradient(to top,#000,transparent),linear-gradient(to right,#fff,hsl(var(--picker-hue,0deg) 100% 50%));touch-action:none;cursor:crosshair}.thumb{position:absolute;inset-inline-start:var(--picker-saturation,0%);top:var(--picker-brightness,100%);width:14px;height:14px;border:2px solid #fff;border-radius:50%;box-shadow:0 0 0 1px #000;transform:translate(-50%,-50%);pointer-events:none}.area:focus-within{outline:2px solid var(--spui-focus-ring,var(--spui-ui-accent,#0078d4));outline-offset:3px}.inputs{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip-path:inset(50%);white-space:nowrap;border:0}.inputs slot{display:contents}.inputs input{position:absolute;width:1px;height:1px}@media(forced-colors:active){.area{border-color:FieldText}.area:focus-within{outline-color:Highlight}.thumb{border-color:Canvas;box-shadow:0 0 0 1px CanvasText}}`;
140
+ this.area = document.createElement("div");
141
+ this.area.className = "area";
142
+ this.area.setAttribute("role", "group");
143
+ this.area.setAttribute("aria-label", this.getAttribute("aria-label") || "Saturation and brightness");
144
+ this.area.part.add("control");
145
+ this.area.addEventListener("pointerdown", event => { if (this.blocked())
146
+ return; this.area.setPointerCapture(event.pointerId); this.updateFromPointer(event); });
147
+ this.area.addEventListener("pointermove", event => { if (this.area.hasPointerCapture(event.pointerId))
148
+ this.updateFromPointer(event); });
149
+ const thumb = document.createElement("span");
150
+ thumb.className = "thumb";
151
+ thumb.setAttribute("aria-hidden", "true");
152
+ const thumbSlot = document.createElement("slot");
153
+ thumbSlot.name = "thumb";
154
+ thumbSlot.append(thumb);
155
+ thumbSlot.addEventListener("slotchange", () => this.syncFromPicker());
156
+ this.area.append(thumbSlot);
157
+ const inputs = document.createElement("div");
158
+ inputs.className = "inputs";
159
+ const makeInput = (axis) => { const input = document.createElement("input"); input.type = "range"; input.min = "0"; input.max = "100"; input.step = "1"; input.className = `input-${axis}`; return input; };
160
+ this.inputX = makeInput("x");
161
+ this.inputY = makeInput("y");
162
+ const inputXSlot = document.createElement("slot");
163
+ inputXSlot.name = "inputX";
164
+ inputXSlot.append(this.inputX);
165
+ inputXSlot.addEventListener("slotchange", () => this.syncFromPicker());
166
+ const inputYSlot = document.createElement("slot");
167
+ inputYSlot.name = "inputY";
168
+ inputYSlot.append(this.inputY);
169
+ inputYSlot.addEventListener("slotchange", () => this.syncFromPicker());
170
+ inputs.append(inputXSlot, inputYSlot);
171
+ shadow.append(style, this.area, inputs);
172
+ }
173
+ blocked() { const state = this.picker?.getColorState(); return !state || state.disabled || state.readOnly || this.hasAttribute("disabled"); }
174
+ updateFromPointer(event) {
175
+ const state = this.picker?.getColorState(), bounds = this.area.getBoundingClientRect();
176
+ if (!state || !bounds.width || !bounds.height)
177
+ return;
178
+ const x = Math.max(0, Math.min(1, (event.clientX - bounds.left) / bounds.width));
179
+ this.picker?.setHsvFromControl(state.hue, x, 1 - Math.max(0, Math.min(1, (event.clientY - bounds.top) / bounds.height)), this, event);
180
+ }
181
+ }
182
+ customElements.define("spui-color-area", SpuiColorArea);
183
+ }
184
+ function defineRangeControl(tag, alpha) {
185
+ if (customElements.get(tag))
186
+ return;
187
+ class SpuiColorRangeControl extends HTMLElement {
188
+ static observedAttributes = ["channel", "vertical", "transparency", "aria-label", "disabled", "shape", "color", "default-color"];
189
+ picker = null;
190
+ input;
191
+ boundInput;
192
+ track;
193
+ connectedCallback() { if (!this.shadowRoot)
194
+ this.render(); this.picker = controlController(this); this.syncFromPicker(); }
195
+ get color() { return this.picker?.getColorState().color ?? { h: 0, s: 0, v: 0, a: 1 }; }
196
+ set color(value) { const controller = (this.picker ?? controlController(this)); controller.setControlColor?.(value); }
197
+ get defaultColor() { return parseControlColor(this.getAttribute("default-color") || this.getAttribute("color")) ?? this.color; }
198
+ set defaultColor(value) { this.setAttribute("default-color", JSON.stringify(value)); }
199
+ focus(options) { this.input?.focus(options); }
200
+ syncFromPicker() {
201
+ const state = this.picker?.getColorState();
202
+ if (!state || !this.input)
203
+ return;
204
+ this.input = this.getInput();
205
+ const channel = this.getAttribute("channel") === "saturation" || this.getAttribute("channel") === "value" ? this.getAttribute("channel") : "hue";
206
+ this.input.max = alpha ? "100" : channel === "hue" ? "360" : "100";
207
+ this.input.classList.remove("hue", "saturation", "value", "alpha");
208
+ this.input.classList.add(alpha ? "alpha" : channel);
209
+ const transparency = alpha && this.hasAttribute("transparency");
210
+ const value = alpha ? (transparency ? 1 - state.alpha : state.alpha) * 100 : channel === "hue" ? state.hue : (channel === "saturation" ? state.saturation : state.brightness) * 100;
211
+ this.input.value = String(Math.round(value));
212
+ this.input.setAttribute("aria-valuenow", this.input.value);
213
+ this.input.setAttribute("aria-valuetext", `${this.input.value}${alpha ? "%" : channel === "hue" ? "°" : "%"}`);
214
+ this.input.setAttribute("aria-label", this.input.getAttribute("aria-label") || this.getAttribute("aria-label") || (alpha ? this.picker?.getControlLabel("alpha-label") : channel === "hue" ? this.picker?.getControlLabel("hue-label") : this.picker?.getControlLabel(channel === "saturation" ? "saturation-label" : "value-label")) || (alpha ? "Opacity" : "Hue"));
215
+ this.input.setAttribute("aria-orientation", this.hasAttribute("vertical") ? "vertical" : "horizontal");
216
+ this.input.disabled = state.disabled || state.readOnly || this.hasAttribute("disabled");
217
+ this.input.style.writingMode = this.hasAttribute("vertical") ? "vertical-lr" : "horizontal-tb";
218
+ this.input.style.direction = this.hasAttribute("vertical") ? "rtl" : getComputedStyle(this).direction;
219
+ this.track.classList.toggle("vertical", this.hasAttribute("vertical"));
220
+ this.track.classList.toggle("horizontal", !this.hasAttribute("vertical"));
221
+ const progress = Math.max(0, Math.min(1, Number(this.input.value) / Number(this.input.max || 100)));
222
+ this.track.style.setProperty("--color-range-progress", String(progress * 100) + "%");
223
+ const railSlot = this.shadowRoot?.querySelector('slot[name="rail"]');
224
+ const rail = railSlot?.assignedElements()[0] ?? this.shadowRoot?.querySelector(".rail");
225
+ if (rail && rail === this.shadowRoot?.querySelector(".rail"))
226
+ rail.style.backgroundImage = getComputedStyle(this.input).backgroundImage;
227
+ const thumbSlot = this.shadowRoot?.querySelector('slot[name="thumb"]');
228
+ const thumb = thumbSlot?.assignedElements()[0] ?? this.shadowRoot?.querySelector(".thumb");
229
+ if (thumb) {
230
+ thumb.style.insetInlineStart = String(progress * 100) + "%";
231
+ thumb.style.insetBlockStart = this.hasAttribute("vertical") ? String((1 - progress) * 100) + "%" : "50%";
232
+ }
233
+ const hex = state.value;
234
+ this.style.setProperty("--picker-current-color", `#${hex.slice(1)}`);
235
+ this.style.setProperty("--picker-hue", `${state.hue}deg`);
236
+ const shape = this.getAttribute("shape") || this.picker?.getAttribute("shape");
237
+ this.style.setProperty("--picker-radius", shape === "square" ? "var(--spui-control-radius,4px)" : "10px");
238
+ }
239
+ attributeChangedCallback(name) { if (name === "color" || name === "default-color")
240
+ this.color = this.defaultColor; this.syncFromPicker(); }
241
+ getInput() {
242
+ const slot = this.shadowRoot?.querySelector('slot[name="input"]');
243
+ const fallback = this.shadowRoot?.querySelector(".input-fallback");
244
+ const assigned = slot?.assignedElements().find((element) => element instanceof HTMLInputElement);
245
+ const input = assigned ?? fallback;
246
+ if (input) {
247
+ input.type = "range";
248
+ input.min = "0";
249
+ input.step = "1";
250
+ fallback.hidden = Boolean(assigned);
251
+ fallback.setAttribute("aria-hidden", String(Boolean(assigned)));
252
+ if (this.boundInput !== input) {
253
+ this.boundInput?.removeEventListener("input", this.handleInput);
254
+ input.addEventListener("input", this.handleInput);
255
+ this.boundInput = input;
256
+ }
257
+ }
258
+ return input;
259
+ }
260
+ handleInput = (event) => {
261
+ const state = this.picker?.getColorState();
262
+ if (!state || state.disabled || state.readOnly)
263
+ return;
264
+ const value = Number(event.currentTarget.value);
265
+ if (alpha)
266
+ this.picker?.setAlphaFromControl((this.hasAttribute("transparency") ? 100 - value : value) / 100, this, event);
267
+ else {
268
+ const channel = this.getAttribute("channel");
269
+ this.picker?.setHsvFromControl(channel === "hue" || !channel ? value : state.hue, channel === "saturation" ? value / 100 : state.saturation, channel === "value" ? value / 100 : state.brightness, this, event);
270
+ }
271
+ };
272
+ render() {
273
+ const shadow = this.attachShadow({ mode: "open" });
274
+ const style = document.createElement("style");
275
+ style.textContent = `${base}:host{display:block;width:min(260px,100%);min-width:80px}.track{position:relative;width:100%;height:32px;touch-action:none}.track.vertical{width:32px;height:160px}.rail{position:absolute;inset:8px 0;border:1px solid var(--spui-ui-stroke,#8a8886);border-radius:var(--picker-radius,999px);background-color:var(--spui-ui-canvas,#fff);background-size:100% 100%}.track.vertical .rail{inset:0 8px}.thumb{position:absolute;inset-inline-start:var(--color-range-progress,0%);top:50%;width:14px;height:14px;border:2px solid #fff;border-radius:50%;box-shadow:0 0 0 1px #000;background:var(--picker-current-color,#fff);transform:translate(-50%,-50%);pointer-events:none}.track.vertical .thumb{inset-inline-start:50%;top:calc(100% - var(--color-range-progress,0%))}slot[name=thumb]::slotted(*){position:absolute;inset-inline-start:var(--color-range-progress,0%);top:50%;transform:translate(-50%,-50%)}.track.vertical slot[name=thumb]::slotted(*){inset-inline-start:50%;top:calc(100% - var(--color-range-progress,0%))}input{box-sizing:border-box;position:absolute;z-index:1;inset:0;width:100%;height:100%;min-height:0;margin:0;opacity:.001;cursor:pointer;accent-color:var(--spui-ui-accent,#78716c);direction:ltr}input:focus-visible{outline:2px solid var(--spui-focus-ring,var(--spui-ui-accent,#0078d4));outline-offset:2px}.track.vertical input{writing-mode:vertical-lr;direction:rtl}input.hue{background-image:linear-gradient(to right,#f00,#ff0,#0f0,#0ff,#00f,#f0f,#f00)}input.saturation{background-image:linear-gradient(to right,#888,hsl(var(--picker-hue,0deg) 100% 50%))}input.value{background-image:linear-gradient(to right,#000,var(--picker-current-color,#fff))}input.alpha{background-image:linear-gradient(to right,transparent,var(--picker-current-color,#000))}@media(forced-colors:active){.rail{border-color:CanvasText;background:Canvas}.thumb{border-color:Canvas;box-shadow:0 0 0 1px CanvasText}.track:focus-within{outline:2px solid Highlight}}`;
276
+ style.textContent += `slot[name=rail]::slotted(*){position:absolute;inset:8px 0;border:1px solid var(--spui-ui-stroke,#8a8886);border-radius:var(--picker-radius,999px)}.track.vertical slot[name=rail]::slotted(*){inset:0 8px}`;
277
+ this.track = document.createElement("div");
278
+ this.track.className = "track";
279
+ const railFallback = document.createElement("span");
280
+ railFallback.className = "rail";
281
+ const rail = document.createElement("slot");
282
+ rail.name = "rail";
283
+ rail.append(railFallback);
284
+ rail.addEventListener("slotchange", () => this.syncFromPicker());
285
+ const thumbFallback = document.createElement("span");
286
+ thumbFallback.className = "thumb";
287
+ thumbFallback.setAttribute("aria-hidden", "true");
288
+ const thumb = document.createElement("slot");
289
+ thumb.name = "thumb";
290
+ thumb.append(thumbFallback);
291
+ thumb.addEventListener("slotchange", () => this.syncFromPicker());
292
+ this.input = document.createElement("input");
293
+ this.input.className = "input-fallback";
294
+ this.input.type = "range";
295
+ this.input.min = "0";
296
+ this.input.step = "1";
297
+ this.input.part.add("input");
298
+ const input = document.createElement("slot");
299
+ input.name = "input";
300
+ input.append(this.input);
301
+ input.addEventListener("slotchange", () => this.syncFromPicker());
302
+ this.track.append(rail, thumb, input);
303
+ shadow.append(style, this.track);
304
+ }
305
+ }
306
+ customElements.define(tag, SpuiColorRangeControl);
307
+ }
308
+ export function defineColorSlider() { defineRangeControl("spui-color-slider", false); }
309
+ export function defineAlphaSlider() { defineRangeControl("spui-alpha-slider", true); }
310
+ function normalizeHex(value) {
311
+ const hex = value.trim().replace(/^#/, "");
312
+ if (/^[\da-f]{3}$/i.test(hex))
313
+ return `#${[...hex].map(character => character + character).join("").toUpperCase()}`;
314
+ return /^[\da-f]{6}$/i.test(hex) ? `#${hex.toUpperCase()}` : undefined;
315
+ }
316
+ function colorToHex(color) {
317
+ const channels = color.match(/[\d.]+/g)?.map(Number);
318
+ if (!channels || channels.length < 3 || channels.slice(0, 3).some(channel => !Number.isFinite(channel)))
319
+ return undefined;
320
+ return `#${channels.slice(0, 3).map(channel => Math.max(0, Math.min(255, Math.round(channel))).toString(16).padStart(2, "0")).join("").toUpperCase()}`;
321
+ }
322
+ function hexToHsv(hex) {
323
+ const channels = [1, 3, 5].map(index => parseInt(hex.slice(index, index + 2), 16) / 255);
324
+ const [r, g, b] = channels;
325
+ const max = Math.max(r, g, b), min = Math.min(r, g, b), delta = max - min;
326
+ let hue = 0;
327
+ if (delta)
328
+ hue = max === r ? 60 * (((g - b) / delta) % 6) : max === g ? 60 * ((b - r) / delta + 2) : 60 * ((r - g) / delta + 4);
329
+ return [(hue + 360) % 360, max ? delta / max : 0, max];
330
+ }
331
+ function hsvToHex(hue, saturation, brightness) {
332
+ const chroma = brightness * saturation;
333
+ const x = chroma * (1 - Math.abs((hue / 60) % 2 - 1));
334
+ const offset = brightness - chroma;
335
+ const [r, g, b] = hue < 60 ? [chroma, x, 0] : hue < 120 ? [x, chroma, 0] : hue < 180 ? [0, chroma, x] : hue < 240 ? [0, x, chroma] : hue < 300 ? [x, 0, chroma] : [chroma, 0, x];
336
+ return `#${[r, g, b].map(channel => Math.round((channel + offset) * 255).toString(16).padStart(2, "0")).join("").toUpperCase()}`;
337
+ }
338
+ function rgbToHex(red, green, blue) {
339
+ return `#${[red, green, blue].map(channel => Math.max(0, Math.min(255, Math.round(channel))).toString(16).padStart(2, "0")).join("").toUpperCase()}`;
340
+ }
341
+ export function defineColorPicker() {
342
+ defineColorArea();
343
+ defineColorSlider();
344
+ defineAlphaSlider();
345
+ if (customElements.get("spui-color-picker"))
346
+ return;
347
+ class SpuiColorPicker extends HTMLElement {
348
+ static formAssociated = true;
349
+ static observedAttributes = ["label", "value", "default-value", "default-color", "alpha", "default-alpha", "disabled", "required", "read-only", "name", "size", "shape", "colors", "color-format", "aria-label", "aria-labelledby", "aria-description", "aria-describedby", "aria-invalid", "color-input-label", "hex-input-label", "hue-label", "area-label", "area-value-text", "format-label", "rgb-label", "red-label", "green-label", "blue-label", "alpha-label", "saturation-label", "value-label", "swatches-label", "swatch-label", "invalid-message"];
350
+ internals;
351
+ currentValue = "#000000";
352
+ currentAlpha = 1;
353
+ hue = 0;
354
+ saturation = 0;
355
+ brightness = 0;
356
+ dirty = false;
357
+ formDisabled = false;
358
+ customError = "";
359
+ colorOptions;
360
+ themeObserver;
361
+ get form() { return this.internals?.form ?? null; }
362
+ constructor() {
363
+ super();
364
+ try {
365
+ this.internals = this.attachInternals();
366
+ }
367
+ catch { /* Native color selection still works without form association. */ }
368
+ }
369
+ get value() { return this.currentValue; }
370
+ set value(value) { this.setColor(value, this.currentAlpha, true); }
371
+ get color() { return this.currentColor(); }
372
+ set color(value) { this.setHsvColor(value, true); }
373
+ getColorState() { return { value: this.currentValue, alpha: this.currentAlpha, hue: this.hue, saturation: this.saturation, brightness: this.brightness, color: this.currentColor(), disabled: this.formDisabled || this.hasAttribute("disabled"), readOnly: this.hasAttribute("read-only") }; }
374
+ getControlLabel(name) { return this.getAttribute(name) || ({ "area-label": "Saturation and brightness", "hue-label": "Hue", "alpha-label": "Opacity", "saturation-label": "Saturation", "value-label": "Brightness" }[name]); }
375
+ setHsvFromControl(hue, saturation, brightness, source, sourceEvent) {
376
+ const state = this.getColorState();
377
+ if (state.disabled || state.readOnly)
378
+ return;
379
+ this.hue = ((hue % 360) + 360) % 360;
380
+ this.saturation = Math.max(0, Math.min(1, saturation));
381
+ this.brightness = Math.max(0, Math.min(1, brightness));
382
+ this.commitHsv(sourceEvent);
383
+ if (source)
384
+ source.dispatchEvent(new CustomEvent("spui-color-change", { detail: { color: this.currentColor(), event: sourceEvent, type: "change" } }));
385
+ }
386
+ setAlphaFromControl(alpha, source, sourceEvent) {
387
+ const state = this.getColorState();
388
+ if (state.disabled || state.readOnly)
389
+ return;
390
+ this.currentAlpha = Number.isFinite(alpha) ? Math.max(0, Math.min(1, alpha)) : 1;
391
+ this.dirty = true;
392
+ const input = this.shadowRoot?.querySelector(".alpha");
393
+ if (input)
394
+ input.value = String(Math.round(this.currentAlpha * 100));
395
+ const preview = this.shadowRoot?.querySelector(".preview");
396
+ if (preview)
397
+ preview.style.backgroundColor = this.cssColor();
398
+ this.syncComposedControls();
399
+ this.syncForm();
400
+ this.dispatchChange(sourceEvent);
401
+ if (source)
402
+ source.dispatchEvent(new CustomEvent("spui-color-change", { detail: { color: this.currentColor(), event: sourceEvent, type: "change" } }));
403
+ }
404
+ get alpha() { return this.currentAlpha; }
405
+ set alpha(value) { this.setColor(this.currentValue, value, true); }
406
+ get format() { return this.getAttribute("color-format") === "rgb" ? "rgb" : "hex"; }
407
+ set format(value) { this.setAttribute("color-format", value === "rgb" ? "rgb" : "hex"); }
408
+ get name() { return this.getAttribute("name") ?? ""; }
409
+ set name(value) { this.setAttribute("name", value); }
410
+ get disabled() { return this.hasAttribute("disabled"); }
411
+ set disabled(value) { this.toggleAttribute("disabled", Boolean(value)); }
412
+ get required() { return this.hasAttribute("required"); }
413
+ set required(value) { this.toggleAttribute("required", Boolean(value)); }
414
+ get readOnly() { return this.hasAttribute("read-only"); }
415
+ set readOnly(value) { this.toggleAttribute("read-only", Boolean(value)); }
416
+ get validity() { return this.internals?.validity ?? this.validationInput()?.validity; }
417
+ get validationMessage() { return this.internals?.validationMessage ?? this.validationInput()?.validationMessage ?? ""; }
418
+ get willValidate() { return this.internals?.willValidate ?? Boolean(this.validationInput() && !this.disabled && !this.readOnly && !this.formDisabled); }
419
+ get defaultAlpha() { return this.readAlpha(this.getAttribute("default-alpha") ?? this.getAttribute("alpha")); }
420
+ set defaultAlpha(value) { this.setAttribute("default-alpha", String(value)); }
421
+ get defaultValue() { return normalizeHex(this.getAttribute("default-value") ?? this.getAttribute("value") ?? "#000000") ?? "#000000"; }
422
+ set defaultValue(value) { this.setAttribute("default-value", value); }
423
+ get defaultColor() {
424
+ const color = this.readHsv(this.getAttribute("default-color"));
425
+ if (color)
426
+ return { ...color, a: color.a ?? this.defaultAlpha };
427
+ const [h, s, v] = hexToHsv(this.defaultValue);
428
+ return { h, s, v, a: this.defaultAlpha };
429
+ }
430
+ set defaultColor(value) { this.setAttribute("default-color", JSON.stringify(value)); }
431
+ currentColor() { return { h: this.hue, s: this.saturation, v: this.brightness, a: this.currentAlpha }; }
432
+ readHsv(value) {
433
+ if (!value)
434
+ return undefined;
435
+ try {
436
+ const color = JSON.parse(value);
437
+ if (!color || typeof color !== "object")
438
+ return undefined;
439
+ const { h, s, v, a } = color;
440
+ if (![h, s, v].every(channel => typeof channel === "number" && Number.isFinite(channel)))
441
+ return undefined;
442
+ return { h: ((h % 360) + 360) % 360, s: Math.max(0, Math.min(1, s)), v: Math.max(0, Math.min(1, v)), ...(typeof a === "number" && Number.isFinite(a) ? { a: Math.max(0, Math.min(1, a)) } : {}) };
443
+ }
444
+ catch {
445
+ return undefined;
446
+ }
447
+ }
448
+ setHsvColor(color, dirty) {
449
+ const normalized = this.readHsv(JSON.stringify(color));
450
+ if (!normalized)
451
+ return;
452
+ this.hue = normalized.h;
453
+ this.saturation = normalized.s;
454
+ this.brightness = normalized.v;
455
+ this.currentAlpha = normalized.a ?? (dirty ? 1 : this.defaultAlpha);
456
+ this.currentValue = hsvToHex(this.hue, this.saturation, this.brightness);
457
+ if (dirty)
458
+ this.dirty = true;
459
+ this.syncValueInputs();
460
+ const preview = this.shadowRoot?.querySelector(".preview");
461
+ if (preview)
462
+ preview.style.backgroundColor = this.cssColor();
463
+ this.syncPickerArea();
464
+ this.syncForm();
465
+ this.syncComposedControls();
466
+ }
467
+ changeDetail() { return { value: this.currentValue, alpha: this.currentAlpha, color: this.currentColor() }; }
468
+ get colors() { return this.colorOptions ?? this.parseColors(this.getAttribute("colors")); }
469
+ set colors(value) { this.colorOptions = value.map(normalizeHex).filter((color) => Boolean(color)); this.renderSwatches(); }
470
+ checkValidity() { return this.internals?.checkValidity() ?? this.validationInput()?.checkValidity() ?? true; }
471
+ reportValidity() { return this.internals?.reportValidity() ?? this.validationInput()?.reportValidity() ?? true; }
472
+ setCustomValidity(message) { this.customError = message; this.syncForm(); }
473
+ connectedCallback() {
474
+ if (!this.shadowRoot) {
475
+ this.currentValue = this.readInitialValue();
476
+ this.currentAlpha = this.defaultAlpha;
477
+ [this.hue, this.saturation, this.brightness] = hexToHsv(this.currentValue);
478
+ const defaultColor = this.readHsv(this.getAttribute("default-color"));
479
+ if (defaultColor) {
480
+ this.hue = defaultColor.h;
481
+ this.saturation = defaultColor.s;
482
+ this.brightness = defaultColor.v;
483
+ this.currentAlpha = defaultColor.a ?? this.defaultAlpha;
484
+ this.currentValue = hsvToHex(this.hue, this.saturation, this.brightness);
485
+ }
486
+ this.render();
487
+ }
488
+ this.observeThemeColors();
489
+ }
490
+ disconnectedCallback() { this.themeObserver?.disconnect(); }
491
+ attributeChangedCallback(name, _oldValue, value) {
492
+ if (!this.isConnected || !this.shadowRoot)
493
+ return;
494
+ if (name === "value" || name === "default-value") {
495
+ if (!this.dirty)
496
+ this.setColor(this.readInitialValue(), this.currentAlpha, false);
497
+ }
498
+ else if (name === "alpha" || name === "default-alpha") {
499
+ const alpha = this.readAlpha(value);
500
+ if (!this.dirty)
501
+ this.setColor(this.currentValue, alpha, false);
502
+ }
503
+ else if (name === "default-color") {
504
+ const color = this.readHsv(value);
505
+ if (!this.dirty && color)
506
+ this.setHsvColor(color, false);
507
+ }
508
+ else if (name === "label") {
509
+ const label = this.shadowRoot.querySelector("label");
510
+ if (label)
511
+ label.textContent = value ?? "";
512
+ this.syncInputLabels();
513
+ }
514
+ else if (name === "disabled" || name === "read-only")
515
+ this.syncDisabled();
516
+ else if (name === "size")
517
+ this.shadowRoot.querySelector(".picker").dataset.size = value || "medium";
518
+ else if (name === "shape")
519
+ this.shadowRoot.querySelector(".picker").dataset.shape = value || "rounded";
520
+ else if (name === "colors")
521
+ this.renderSwatches();
522
+ else if (name === "color-format") {
523
+ this.syncFormat();
524
+ this.syncForm();
525
+ }
526
+ else if (["color-input-label", "hex-input-label", "hue-label", "area-label", "aria-label", "aria-labelledby", "aria-description", "aria-describedby", "aria-invalid", "rgb-label", "red-label", "green-label", "blue-label"].includes(name))
527
+ this.syncInputLabels();
528
+ else if (name === "area-value-text")
529
+ this.syncPickerArea();
530
+ else if (name === "format-label")
531
+ this.syncFormat();
532
+ else if (["alpha-label", "swatches-label"].includes(name))
533
+ this.syncGroupLabels();
534
+ else if (name === "swatch-label")
535
+ this.renderSwatches();
536
+ else if (name === "invalid-message")
537
+ this.syncForm();
538
+ this.syncComposedControls();
539
+ }
540
+ formDisabledCallback(disabled) { this.formDisabled = disabled; this.syncDisabled(); }
541
+ formResetCallback() { this.dirty = false; const color = this.readHsv(this.getAttribute("default-color")); if (color)
542
+ this.setHsvColor(color, false);
543
+ else
544
+ this.setColor(this.defaultValue, this.defaultAlpha, false); }
545
+ formStateRestoreCallback(state) {
546
+ if (typeof state !== "string")
547
+ return;
548
+ const [value, alpha] = state.split("|");
549
+ this.setColor(value, alpha === undefined ? 1 : Number(alpha), false);
550
+ }
551
+ readInitialValue() { return normalizeHex(this.getAttribute("value") ?? this.getAttribute("default-value") ?? "#000000") ?? "#000000"; }
552
+ readAlpha(value) { const alpha = Number(value ?? 1); return Number.isFinite(alpha) ? Math.max(0, Math.min(1, alpha)) : 1; }
553
+ parseColors(value) { return (value ?? "").split(/[\s,]+/).map(normalizeHex).filter((color) => Boolean(color)); }
554
+ syncValueInputs(preserveHex = false) {
555
+ const color = this.shadowRoot?.querySelector('input[type="color"]');
556
+ const hex = this.shadowRoot?.querySelector(".hex");
557
+ const channels = this.shadowRoot?.querySelectorAll(".channel-value");
558
+ if (color)
559
+ color.value = this.currentValue.toLowerCase();
560
+ if (hex && !preserveHex) {
561
+ hex.value = this.currentValue;
562
+ hex.setCustomValidity("");
563
+ }
564
+ if (channels?.length === 3) {
565
+ const values = [1, 3, 5].map(index => parseInt(this.currentValue.slice(index, index + 2), 16));
566
+ channels.forEach((channel, index) => { channel.value = String(values[index]); });
567
+ }
568
+ const hue = this.shadowRoot?.querySelector(".hue");
569
+ if (hue)
570
+ hue.value = String(Math.round(this.hue));
571
+ }
572
+ syncFormat() {
573
+ const select = this.shadowRoot?.querySelector(".format");
574
+ const hex = this.shadowRoot?.querySelector(".hex");
575
+ const channels = this.shadowRoot?.querySelector(".channels");
576
+ if (select)
577
+ select.value = this.format;
578
+ if (hex)
579
+ hex.hidden = this.format !== "hex";
580
+ if (channels)
581
+ channels.hidden = this.format !== "rgb";
582
+ }
583
+ validationInput() {
584
+ if (this.format === "rgb")
585
+ return [...(this.shadowRoot?.querySelectorAll(".channel-value") ?? [])].find(input => !input.validity.valid) ?? this.shadowRoot?.querySelector(".channel-value") ?? undefined;
586
+ return this.shadowRoot?.querySelector(".hex") ?? undefined;
587
+ }
588
+ setColor(value, alpha, dirty) {
589
+ const normalized = normalizeHex(value);
590
+ if (normalized)
591
+ this.currentValue = normalized;
592
+ this.currentAlpha = Number.isFinite(alpha) ? Math.max(0, Math.min(1, alpha)) : 1;
593
+ if (dirty)
594
+ this.dirty = true;
595
+ const range = this.shadowRoot?.querySelector(".alpha");
596
+ [this.hue, this.saturation, this.brightness] = hexToHsv(this.currentValue);
597
+ this.syncValueInputs();
598
+ if (range)
599
+ range.value = String(Math.round(this.currentAlpha * 100));
600
+ const preview = this.shadowRoot?.querySelector(".preview");
601
+ if (preview)
602
+ preview.style.backgroundColor = this.cssColor();
603
+ this.syncPickerArea();
604
+ this.syncForm();
605
+ this.syncComposedControls();
606
+ }
607
+ syncPickerArea() {
608
+ const area = this.shadowRoot?.querySelector(".area");
609
+ if (!area)
610
+ return;
611
+ area.style.setProperty("--picker-hue", `${this.hue}deg`);
612
+ area.style.setProperty("--picker-saturation", `${this.saturation * 100}%`);
613
+ area.style.setProperty("--picker-brightness", `${(1 - this.brightness) * 100}%`);
614
+ area.setAttribute("aria-valuenow", String(Math.round(this.saturation * 100)));
615
+ area.setAttribute("aria-valuetext", (this.getAttribute("area-value-text") || "{saturation}% saturation, {brightness}% brightness").replaceAll("{saturation}", String(Math.round(this.saturation * 100))).replaceAll("{brightness}", String(Math.round(this.brightness * 100))));
616
+ }
617
+ commitHsv(sourceEvent) {
618
+ this.currentValue = hsvToHex(this.hue, this.saturation, this.brightness);
619
+ this.dirty = true;
620
+ this.syncValueInputs();
621
+ const preview = this.shadowRoot?.querySelector(".preview");
622
+ if (preview)
623
+ preview.style.backgroundColor = this.cssColor();
624
+ this.syncPickerArea();
625
+ this.syncForm();
626
+ this.syncComposedControls();
627
+ this.dispatchChange(sourceEvent);
628
+ }
629
+ commitRgb(sourceEvent) {
630
+ const channels = [...(this.shadowRoot?.querySelectorAll(".channel-value") ?? [])];
631
+ if (channels.length !== 3 || channels.some(channel => !channel.value || !channel.validity.valid)) {
632
+ this.syncForm();
633
+ return;
634
+ }
635
+ const [red, green, blue] = channels.map(channel => Number(channel.value));
636
+ this.setColor(rgbToHex(red, green, blue), this.currentAlpha, true);
637
+ this.dispatchChange(sourceEvent);
638
+ }
639
+ updateAreaFromPointer(event, area) {
640
+ const bounds = area.getBoundingClientRect();
641
+ if (!bounds.width || !bounds.height)
642
+ return;
643
+ this.saturation = Math.max(0, Math.min(1, (event.clientX - bounds.left) / bounds.width));
644
+ this.brightness = 1 - Math.max(0, Math.min(1, (event.clientY - bounds.top) / bounds.height));
645
+ this.commitHsv(event);
646
+ }
647
+ cssColor() {
648
+ const alpha = Math.round(this.currentAlpha * 1000) / 1000;
649
+ return `rgb(${parseInt(this.currentValue.slice(1, 3), 16)} ${parseInt(this.currentValue.slice(3, 5), 16)} ${parseInt(this.currentValue.slice(5, 7), 16)} / ${alpha})`;
650
+ }
651
+ syncForm() {
652
+ const activeInput = this.validationInput();
653
+ const validity = activeInput?.validity;
654
+ const valid = Boolean(validity?.valid) && !this.customError;
655
+ const alphaHex = Math.round(this.currentAlpha * 255).toString(16).padStart(2, "0").toUpperCase();
656
+ this.internals?.setFormValue(this.currentAlpha < 1 ? `${this.currentValue}${alphaHex}` : this.currentValue, `${this.currentValue}|${this.currentAlpha}`);
657
+ try {
658
+ this.internals?.setValidity(valid ? {} : { badInput: Boolean(validity?.badInput), patternMismatch: Boolean(validity?.patternMismatch), rangeOverflow: Boolean(validity?.rangeOverflow), rangeUnderflow: Boolean(validity?.rangeUnderflow), stepMismatch: Boolean(validity?.stepMismatch), valueMissing: Boolean(validity?.valueMissing), customError: Boolean(this.customError || validity?.customError) }, valid ? "" : (this.customError || activeInput?.validationMessage) ?? "", activeInput ?? undefined);
659
+ }
660
+ catch { /* Native input validity remains available if the browser rejects a shadow validity anchor. */ }
661
+ }
662
+ syncDisabled() {
663
+ const disabled = this.formDisabled || this.hasAttribute("disabled");
664
+ for (const input of this.shadowRoot?.querySelectorAll("input") ?? [])
665
+ input.disabled = disabled || this.hasAttribute("read-only");
666
+ for (const button of this.shadowRoot?.querySelectorAll("button") ?? [])
667
+ button.disabled = disabled || this.hasAttribute("read-only");
668
+ const format = this.shadowRoot?.querySelector(".format");
669
+ if (format)
670
+ format.disabled = disabled || this.hasAttribute("read-only");
671
+ const area = this.shadowRoot?.querySelector(".area");
672
+ if (area) {
673
+ area.tabIndex = disabled ? -1 : 0;
674
+ area.setAttribute("aria-disabled", String(disabled || this.hasAttribute("read-only")));
675
+ area.setAttribute("aria-readonly", String(this.hasAttribute("read-only")));
676
+ }
677
+ this.syncComposedControls();
678
+ }
679
+ syncInputLabels() {
680
+ const color = this.shadowRoot?.querySelector("input[type=color]");
681
+ const hex = this.shadowRoot?.querySelector(".hex");
682
+ const colorLabel = this.getAttribute("color-input-label") || "Choose color";
683
+ if (color)
684
+ color.setAttribute("aria-label", colorLabel);
685
+ if (hex)
686
+ hex.setAttribute("aria-label", this.getAttribute("hex-input-label") || "Hex color");
687
+ const picker = this.shadowRoot?.querySelector(".picker");
688
+ if (picker)
689
+ syncHostAccessibleName(this, picker);
690
+ if (picker) {
691
+ const invalid = this.getAttribute("aria-invalid");
692
+ if (invalid === null)
693
+ picker.removeAttribute("aria-invalid");
694
+ else
695
+ picker.setAttribute("aria-invalid", invalid);
696
+ }
697
+ const describedBy = this.getAttribute("aria-describedby");
698
+ const hue = this.shadowRoot?.querySelector(".hue");
699
+ const area = this.shadowRoot?.querySelector(".area");
700
+ const alpha = this.shadowRoot?.querySelector(".alpha");
701
+ const channels = this.shadowRoot?.querySelectorAll(".channel-value");
702
+ const format = this.shadowRoot?.querySelector(".format");
703
+ if (hue)
704
+ hue.setAttribute("aria-label", this.getAttribute("hue-label") || "Hue");
705
+ if (area)
706
+ area.setAttribute("aria-label", this.getAttribute("area-label") || "Saturation and brightness");
707
+ if (channels?.length === 3)
708
+ ["red-label", "green-label", "blue-label"].forEach((name, index) => channels[index].setAttribute("aria-label", this.getAttribute(name) || ["Red", "Green", "Blue"][index]));
709
+ if (format)
710
+ format.setAttribute("aria-label", this.getAttribute("format-label") || "Color format");
711
+ for (const input of [color, hex, hue, alpha, area, format, ...(channels ?? [])]) {
712
+ if (!input)
713
+ continue;
714
+ if (describedBy)
715
+ input.setAttribute("aria-describedby", describedBy);
716
+ else
717
+ input.removeAttribute("aria-describedby");
718
+ const description = this.getAttribute("aria-description");
719
+ if (description)
720
+ input.setAttribute("aria-description", description);
721
+ else
722
+ input.removeAttribute("aria-description");
723
+ const invalid = this.getAttribute("aria-invalid");
724
+ if (invalid)
725
+ input.setAttribute("aria-invalid", invalid);
726
+ else
727
+ input.removeAttribute("aria-invalid");
728
+ }
729
+ if (picker) {
730
+ if (describedBy)
731
+ picker.setAttribute("aria-describedby", describedBy);
732
+ else
733
+ picker.removeAttribute("aria-describedby");
734
+ }
735
+ }
736
+ syncGroupLabels() {
737
+ const alpha = this.shadowRoot?.querySelector(".alpha");
738
+ const swatches = this.shadowRoot?.querySelector(".swatches");
739
+ const channels = this.shadowRoot?.querySelector(".channels");
740
+ alpha?.setAttribute("aria-label", this.getAttribute("alpha-label") || "Opacity");
741
+ swatches?.setAttribute("aria-label", this.getAttribute("swatches-label") || "Color swatches");
742
+ channels?.setAttribute("aria-label", this.getAttribute("rgb-label") || "RGB color values");
743
+ }
744
+ renderSwatches() {
745
+ const root = this.shadowRoot?.querySelector(".swatches");
746
+ if (!root)
747
+ return;
748
+ const labelTemplate = this.getAttribute("swatch-label") || "Select {color}";
749
+ const theme = [
750
+ ["--spui-ui-accent", "#78716C"], ["--spui-ui-success", "#107C10"],
751
+ ["--spui-ui-warning", "#835B00"], ["--spui-ui-danger", "#B10E1C"]
752
+ ];
753
+ const buttons = theme.map(([token, fallback]) => {
754
+ const button = document.createElement("button");
755
+ button.type = "button";
756
+ button.dataset.token = token;
757
+ button.style.backgroundColor = `var(${token},${fallback})`;
758
+ button.setAttribute("aria-label", labelTemplate.replaceAll("{color}", fallback));
759
+ button.addEventListener("click", event => this.commitSwatch(colorToHex(getComputedStyle(button).backgroundColor) ?? fallback, event));
760
+ return button;
761
+ });
762
+ for (const color of this.colors) {
763
+ const button = document.createElement("button");
764
+ button.type = "button";
765
+ button.style.backgroundColor = color;
766
+ button.setAttribute("aria-label", labelTemplate.replaceAll("{color}", color));
767
+ button.addEventListener("click", event => this.commitSwatch(color, event));
768
+ buttons.push(button);
769
+ }
770
+ root.replaceChildren(...buttons);
771
+ for (const button of root.querySelectorAll("button[data-token]")) {
772
+ const color = colorToHex(getComputedStyle(button).backgroundColor) ?? "#78716C";
773
+ button.setAttribute("aria-label", labelTemplate.replaceAll("{color}", color));
774
+ }
775
+ }
776
+ observeThemeColors() {
777
+ this.themeObserver?.disconnect();
778
+ this.themeObserver = new MutationObserver(() => {
779
+ const template = this.getAttribute("swatch-label") || "Select {color}";
780
+ for (const button of this.shadowRoot?.querySelectorAll("button[data-token]") ?? []) {
781
+ const color = colorToHex(getComputedStyle(button).backgroundColor);
782
+ if (color)
783
+ button.setAttribute("aria-label", template.replaceAll("{color}", color));
784
+ }
785
+ });
786
+ for (let current = this; current; current = current.parentElement) {
787
+ this.themeObserver.observe(current, { attributes: true, attributeFilter: ["style", "class", "data-spui-appearance", "data-spui-base-appearance"] });
788
+ }
789
+ }
790
+ commitSwatch(color, sourceEvent) {
791
+ this.setColor(color, this.currentAlpha, true);
792
+ this.dispatchChange(sourceEvent);
793
+ }
794
+ dispatchChange(sourceEvent) {
795
+ this.dispatchEvent(new CustomEvent("spui-change", { detail: { ...this.changeDetail(), event: sourceEvent, type: "change" }, bubbles: true, composed: true }));
796
+ }
797
+ syncComposedControls() {
798
+ for (const control of this.querySelectorAll("spui-color-area,spui-color-slider,spui-alpha-slider"))
799
+ control.syncFromPicker?.();
800
+ }
801
+ render() {
802
+ const shadow = this.shadowRoot ?? this.attachShadow({ mode: "open" });
803
+ const id = `color-${crypto.randomUUID()}`;
804
+ const label = document.createElement("label");
805
+ label.htmlFor = `${id}-color`;
806
+ label.part.add("label");
807
+ label.textContent = this.getAttribute("label") ?? "";
808
+ const picker = document.createElement("div");
809
+ picker.className = "picker";
810
+ picker.dataset.size = this.getAttribute("size") || "medium";
811
+ picker.dataset.shape = this.getAttribute("shape") || "rounded";
812
+ picker.setAttribute("role", "group");
813
+ picker.setAttribute("aria-label", this.getAttribute("aria-label") || this.getAttribute("label") || "Color picker");
814
+ for (const name of ["aria-labelledby", "aria-description", "aria-describedby", "aria-invalid"]) {
815
+ const value = this.getAttribute(name);
816
+ if (value !== null)
817
+ picker.setAttribute(name, value);
818
+ }
819
+ const color = document.createElement("input");
820
+ color.type = "color";
821
+ color.id = `${id}-color`;
822
+ color.value = this.currentValue.toLowerCase();
823
+ color.part.add("color-input");
824
+ const hex = document.createElement("input");
825
+ hex.type = "text";
826
+ hex.id = `${id}-hex`;
827
+ hex.className = "hex";
828
+ hex.value = this.currentValue;
829
+ hex.pattern = "#(?:[\\da-fA-F]{3}|[\\da-fA-F]{6})";
830
+ hex.required = this.hasAttribute("required");
831
+ hex.autocomplete = "off";
832
+ hex.spellcheck = false;
833
+ hex.part.add("hex-input");
834
+ const formatLabel = document.createElement("label");
835
+ formatLabel.className = "format-label";
836
+ formatLabel.append(document.createTextNode(`${this.getAttribute("format-label") || "Color format"} `));
837
+ const format = document.createElement("select");
838
+ format.className = "format";
839
+ format.part.add("format-select");
840
+ for (const [value, text] of [["hex", "HEX"], ["rgb", "RGB"]]) {
841
+ const option = document.createElement("option");
842
+ option.value = value;
843
+ option.textContent = text;
844
+ format.append(option);
845
+ }
846
+ formatLabel.append(format);
847
+ const channels = document.createElement("div");
848
+ channels.className = "channels";
849
+ channels.setAttribute("role", "group");
850
+ channels.part.add("rgb-inputs");
851
+ for (const [key, labelText, index] of [["red", "Red", 0], ["green", "Green", 1], ["blue", "Blue", 2]]) {
852
+ const field = document.createElement("label");
853
+ field.className = "channel";
854
+ field.htmlFor = `${id}-${key}`;
855
+ field.textContent = this.getAttribute(`${key}-label`) || labelText;
856
+ const input = document.createElement("input");
857
+ input.id = `${id}-${key}`;
858
+ input.className = "channel-value";
859
+ input.type = "number";
860
+ input.min = "0";
861
+ input.max = "255";
862
+ input.step = "1";
863
+ input.required = this.hasAttribute("required");
864
+ input.inputMode = "numeric";
865
+ input.value = String(parseInt(this.currentValue.slice(1 + index * 2, 3 + index * 2), 16));
866
+ input.part.add(`${key}-input`);
867
+ field.append(input);
868
+ channels.append(field);
869
+ }
870
+ const preview = document.createElement("span");
871
+ preview.className = "preview";
872
+ preview.setAttribute("aria-hidden", "true");
873
+ preview.part.add("preview");
874
+ preview.style.backgroundColor = this.cssColor();
875
+ const alpha = document.createElement("input");
876
+ alpha.type = "range";
877
+ alpha.className = "alpha";
878
+ alpha.min = "0";
879
+ alpha.max = "100";
880
+ alpha.value = String(Math.round(this.currentAlpha * 100));
881
+ alpha.part.add("alpha-input");
882
+ const alphaRow = document.createElement("div");
883
+ alphaRow.className = "alpha-row";
884
+ alphaRow.append(alpha);
885
+ const hue = document.createElement("input");
886
+ hue.type = "range";
887
+ hue.className = "hue";
888
+ hue.min = "0";
889
+ hue.max = "360";
890
+ hue.value = String(Math.round(this.hue));
891
+ hue.part.add("hue-input");
892
+ const area = document.createElement("div");
893
+ area.className = "area";
894
+ area.tabIndex = 0;
895
+ area.setAttribute("role", "slider");
896
+ area.setAttribute("aria-valuemin", "0");
897
+ area.setAttribute("aria-valuemax", "100");
898
+ area.part.add("color-area");
899
+ const areaSlot = document.createElement("slot");
900
+ areaSlot.name = "area";
901
+ areaSlot.append(area);
902
+ const sliderSlot = document.createElement("slot");
903
+ sliderSlot.name = "sliders";
904
+ sliderSlot.append(hue);
905
+ const alphaSlot = document.createElement("slot");
906
+ alphaSlot.name = "alpha";
907
+ alphaSlot.append(alphaRow);
908
+ const swatches = document.createElement("div");
909
+ swatches.className = "swatches";
910
+ swatches.part.add("swatches");
911
+ color.setAttribute("aria-label", this.getAttribute("color-input-label") || "Choose color");
912
+ hex.setAttribute("aria-label", this.getAttribute("hex-input-label") || "Hex color");
913
+ alpha.setAttribute("aria-label", this.getAttribute("alpha-label") || "Opacity");
914
+ swatches.setAttribute("role", "group");
915
+ swatches.setAttribute("aria-label", this.getAttribute("swatches-label") || "Color swatches");
916
+ for (const name of ["aria-describedby"]) {
917
+ const value = this.getAttribute(name);
918
+ if (value) {
919
+ color.setAttribute(name, value);
920
+ hex.setAttribute(name, value);
921
+ alpha.setAttribute(name, value);
922
+ }
923
+ }
924
+ color.addEventListener("input", event => this.commitInput(color.value, hex, event));
925
+ hex.addEventListener("input", event => {
926
+ const normalized = normalizeHex(hex.value);
927
+ hex.setCustomValidity(normalized ? "" : this.getAttribute("invalid-message") || "Enter a 3 or 6 digit hex color, for example #336699.");
928
+ if (normalized)
929
+ this.commitInput(normalized, hex, event, true);
930
+ else
931
+ this.syncForm();
932
+ });
933
+ hex.addEventListener("blur", () => { const normalized = normalizeHex(hex.value); if (normalized)
934
+ hex.value = normalized; });
935
+ alpha.addEventListener("input", event => this.setAlphaFromControl(Number(alpha.value) / 100, undefined, event));
936
+ format.value = this.format;
937
+ format.addEventListener("change", () => { this.format = format.value; this.dispatchEvent(new CustomEvent("spui-format-change", { detail: { format: this.format }, bubbles: true, composed: true })); });
938
+ for (const input of channels.querySelectorAll(".channel-value"))
939
+ input.addEventListener("input", event => this.commitRgb(event));
940
+ hue.addEventListener("input", event => this.setHsvFromControl(Number(hue.value), this.saturation, this.brightness, undefined, event));
941
+ area.addEventListener("pointerdown", event => { if (this.hasAttribute("disabled") || this.hasAttribute("read-only") || this.formDisabled)
942
+ return; area.setPointerCapture(event.pointerId); this.updateAreaFromPointer(event, area); });
943
+ area.addEventListener("pointermove", event => { if (area.hasPointerCapture(event.pointerId))
944
+ this.updateAreaFromPointer(event, area); });
945
+ area.addEventListener("keydown", event => {
946
+ if (this.hasAttribute("disabled") || this.hasAttribute("read-only") || this.formDisabled)
947
+ return;
948
+ const step = event.shiftKey ? 0.1 : 0.01;
949
+ const rtl = getComputedStyle(area).direction === "rtl";
950
+ if (event.key === "ArrowLeft")
951
+ this.saturation = Math.max(0, Math.min(1, this.saturation + (rtl ? step : -step)));
952
+ else if (event.key === "ArrowRight")
953
+ this.saturation = Math.max(0, Math.min(1, this.saturation + (rtl ? -step : step)));
954
+ else if (event.key === "ArrowUp")
955
+ this.brightness = Math.min(1, this.brightness + step);
956
+ else if (event.key === "ArrowDown")
957
+ this.brightness = Math.max(0, this.brightness - step);
958
+ else if (event.key === "Home")
959
+ this.saturation = 0;
960
+ else if (event.key === "End")
961
+ this.saturation = 1;
962
+ else
963
+ return;
964
+ event.preventDefault();
965
+ this.commitHsv(event);
966
+ });
967
+ picker.append(color, hex, preview);
968
+ shadow.replaceChildren();
969
+ const style = document.createElement("style");
970
+ style.textContent = `${base}:host{display:grid;gap:var(--spui-control-gap,8px);min-width:0}label:empty{display:none}label{font-size:14px;font-weight:600;color:var(--spui-ui-muted,var(--spui-semantic-body-text,inherit))}.picker{display:flex;align-items:center;gap:var(--spui-control-gap,8px);min-width:0}.picker[data-size=small]{--picker-height:32px}.picker[data-size=medium]{--picker-height:40px}.picker[data-size=large]{--picker-height:48px}.picker[data-shape=square]{--picker-radius:var(--spui-control-radius,4px)}.picker[data-shape=rounded]{--picker-radius:10px}.picker:focus-within{outline:2px solid var(--spui-focus-ring,var(--spui-ui-accent,#78716c));outline-offset:2px;border-radius:var(--picker-radius)}input,select{box-sizing:border-box;min-height:var(--picker-height,40px);border:1px solid var(--spui-control-border,var(--spui-ui-stroke,var(--spui-semantic-input-border,#8a8886)));border-radius:var(--picker-radius,10px);background:var(--spui-control-background,var(--spui-ui-surface,var(--spui-semantic-input-background,#fff)));color:var(--spui-ui-text,var(--spui-semantic-body-text,#242424));font:inherit}input[type=color]{flex:none;width:var(--picker-height,40px);padding:4px;cursor:pointer}input[type=color]::-webkit-color-swatch-wrapper{padding:2px}input[type=color]::-webkit-color-swatch{border:1px solid var(--spui-ui-stroke,#8a8886);border-radius:calc(var(--picker-radius,10px) - 3px)}input.hex{width:10ch;max-width:42vw;padding-inline:10px;font-variant-numeric:tabular-nums;text-transform:uppercase}input:disabled,select:disabled{opacity:.55;cursor:not-allowed}input.hex:invalid{border-color:var(--spui-ui-danger,var(--spui-semantic-error-text,#b10e1c))}.format-label{display:flex;align-items:center;gap:8px}.format{min-height:32px;padding-inline:8px}.channels{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:8px;width:min(320px,100%)}.channels[hidden],.hex[hidden]{display:none!important}.channel{display:grid;gap:3px;font-size:12px;color:var(--spui-ui-muted,var(--spui-semantic-body-subtext,#616161))}.channel-value{width:100%;min-height:32px;padding-inline:6px;direction:ltr;text-align:start;font-variant-numeric:tabular-nums}.preview{flex:none;width:var(--picker-height,40px);height:var(--picker-height,40px);border:1px solid var(--spui-ui-stroke,#8a8886);border-radius:var(--picker-radius,10px)}.area{position:relative;width:min(260px,100%);height:160px;border:1px solid var(--spui-ui-stroke,#8a8886);border-radius:var(--picker-radius,10px);background-image:linear-gradient(to top,#000,transparent),linear-gradient(to right,#fff,hsl(var(--picker-hue,0deg) 100% 50%));touch-action:none;cursor:crosshair}.area::after{content:"";position:absolute;left:var(--picker-saturation,0%);top:var(--picker-brightness,100%);width:14px;height:14px;border:2px solid white;border-radius:50%;box-shadow:0 0 0 1px #000;transform:translate(-50%,-50%);pointer-events:none}.area:focus-visible{outline:2px solid var(--spui-focus-ring,var(--spui-ui-accent,#78716c));outline-offset:3px}.hue{direction:ltr;max-width:260px;background:linear-gradient(to right,#f00,#ff0,#0f0,#0ff,#00f,#f0f,#f00)}.hue::-webkit-slider-runnable-track{height:8px;border-radius:999px;background:linear-gradient(to right,#f00,#ff0,#0f0,#0ff,#00f,#f0f,#f00)}.hue::-moz-range-track{height:8px;border-radius:999px;background:linear-gradient(to right,#f00,#ff0,#0f0,#0ff,#00f,#f0f,#f00)}.alpha-row{display:flex;align-items:center;gap:8px;max-width:320px}input[type=range]{width:100%;min-height:32px;accent-color:var(--spui-ui-accent,#78716c)}.swatches{display:flex;flex-wrap:wrap;gap:8px}.swatches button{box-sizing:border-box;width:36px;height:36px;padding:0;border:1px solid var(--spui-ui-stroke,#8a8886);border-radius:10px;cursor:pointer}:host([shape=square]) .swatches button{border-radius:var(--spui-control-radius,4px)}.swatches button:hover{outline:2px solid var(--spui-ui-accent,#78716c);outline-offset:1px}.swatches button:focus-visible,input:focus-visible,select:focus-visible{outline:2px solid var(--spui-focus-ring,var(--spui-ui-accent,#78716c));outline-offset:2px}@media(forced-colors:active){input,select,.area,.swatches button{border-color:FieldText}.area:focus-visible,.swatches button:focus-visible,input:focus-visible,select:focus-visible{outline-color:Highlight}}@media(prefers-reduced-motion:reduce){.area::after{transition:none}}`;
971
+ style.textContent += ".picker:focus-within{outline:none}input.hex{direction:ltr;text-align:left}";
972
+ style.textContent += "slot[name=area],slot[name=sliders],slot[name=alpha]{display:block;max-width:100%}slot[name=sliders]{display:flex;align-items:center;gap:var(--spui-control-gap,8px);flex-wrap:wrap}slot[name=area]>spui-color-area,slot[name=sliders]>spui-color-slider,slot[name=alpha]>spui-alpha-slider{max-width:100%}";
973
+ shadow.append(style, label, picker, areaSlot, sliderSlot, formatLabel, channels, alphaSlot, swatches);
974
+ syncHostAccessibleName(this, picker);
975
+ this.syncInputLabels();
976
+ this.syncGroupLabels();
977
+ this.syncDisabled();
978
+ this.renderSwatches();
979
+ this.syncPickerArea();
980
+ this.syncFormat();
981
+ this.syncForm();
982
+ this.syncComposedControls();
983
+ }
984
+ commitInput(value, hex, sourceEvent, preserveHex = false) {
985
+ const normalized = normalizeHex(value);
986
+ if (!normalized)
987
+ return;
988
+ this.currentValue = normalized;
989
+ this.dirty = true;
990
+ if (!preserveHex)
991
+ hex.value = normalized;
992
+ hex.setCustomValidity("");
993
+ const preview = this.shadowRoot?.querySelector(".preview");
994
+ if (preview)
995
+ preview.style.backgroundColor = this.cssColor();
996
+ [this.hue, this.saturation, this.brightness] = hexToHsv(normalized);
997
+ this.syncValueInputs(preserveHex);
998
+ this.syncPickerArea();
999
+ this.syncForm();
1000
+ this.dispatchChange(sourceEvent);
1001
+ }
1002
+ }
1003
+ customElements.define("spui-color-picker", SpuiColorPicker);
1004
+ }