@spfx-fast/sharepoint-ui 0.4.2 → 0.5.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 (276) hide show
  1. package/QUICKSTART.md +18 -0
  2. package/README.md +19 -24
  3. package/components.json +240 -5694
  4. package/dist/accordion.d.ts +11 -0
  5. package/dist/accordion.js +4 -0
  6. package/dist/alert.d.ts +3 -0
  7. package/dist/alert.js +4 -0
  8. package/dist/attachment-list.d.ts +15 -0
  9. package/dist/attachment-list.js +14 -0
  10. package/dist/avatar-group.d.ts +12 -0
  11. package/dist/avatar-group.js +7 -0
  12. package/dist/avatar.d.ts +6 -0
  13. package/dist/avatar.js +5 -0
  14. package/dist/badge.d.ts +3 -0
  15. package/dist/badge.js +4 -0
  16. package/dist/breadcrumb.d.ts +10 -0
  17. package/dist/breadcrumb.js +9 -0
  18. package/dist/button-group.d.ts +6 -0
  19. package/dist/button-group.js +4 -0
  20. package/dist/button.d.ts +3 -0
  21. package/dist/button.js +4 -0
  22. package/dist/card.d.ts +3 -0
  23. package/dist/card.js +4 -0
  24. package/dist/carousel.d.ts +11 -0
  25. package/dist/carousel.js +6 -0
  26. package/dist/checkbox.d.ts +3 -0
  27. package/dist/checkbox.js +4 -0
  28. package/dist/combobox.d.ts +9 -0
  29. package/dist/combobox.js +6 -0
  30. package/dist/command-bar.d.ts +6 -0
  31. package/dist/command-bar.js +4 -0
  32. package/dist/copy-button.d.ts +7 -0
  33. package/dist/copy-button.js +19 -0
  34. package/dist/data-grid.d.ts +25 -0
  35. package/dist/data-grid.js +39 -0
  36. package/dist/date-range-picker.d.ts +12 -0
  37. package/dist/date-range-picker.js +4 -0
  38. package/dist/dialog.d.ts +9 -0
  39. package/dist/dialog.js +21 -0
  40. package/dist/divider.d.ts +3 -0
  41. package/dist/divider.js +4 -0
  42. package/dist/drawer.d.ts +6 -0
  43. package/dist/drawer.js +5 -0
  44. package/dist/empty-state.d.ts +7 -0
  45. package/dist/empty-state.js +5 -0
  46. package/dist/field.d.ts +15 -0
  47. package/dist/field.js +11 -0
  48. package/dist/file-dropzone.d.ts +15 -0
  49. package/dist/file-dropzone.js +21 -0
  50. package/dist/filter-bar.d.ts +9 -0
  51. package/dist/filter-bar.js +9 -0
  52. package/dist/image.d.ts +5 -0
  53. package/dist/image.js +4 -0
  54. package/dist/index.d.ts +68 -76
  55. package/dist/index.js +60 -66
  56. package/dist/input.d.ts +3 -0
  57. package/dist/input.js +4 -0
  58. package/dist/label.d.ts +3 -0
  59. package/dist/label.js +4 -0
  60. package/dist/link.d.ts +3 -0
  61. package/dist/link.js +9 -0
  62. package/dist/list-view.d.ts +29 -0
  63. package/dist/list-view.js +14 -0
  64. package/dist/list.d.ts +6 -0
  65. package/dist/list.js +5 -0
  66. package/dist/lookup-picker.d.ts +12 -0
  67. package/dist/lookup-picker.js +4 -0
  68. package/dist/menu.d.ts +6 -0
  69. package/dist/menu.js +4 -0
  70. package/dist/nav.d.ts +12 -0
  71. package/dist/nav.js +4 -0
  72. package/dist/pagination.d.ts +11 -0
  73. package/dist/pagination.js +5 -0
  74. package/dist/person-picker.d.ts +13 -0
  75. package/dist/person-picker.js +4 -0
  76. package/dist/persona.d.ts +7 -0
  77. package/dist/persona.js +5 -0
  78. package/dist/popover.d.ts +6 -0
  79. package/dist/popover.js +4 -0
  80. package/dist/progress-bar.d.ts +5 -0
  81. package/dist/progress-bar.js +5 -0
  82. package/dist/radio-group.d.ts +6 -0
  83. package/dist/radio-group.js +4 -0
  84. package/dist/radio.d.ts +3 -0
  85. package/dist/radio.js +4 -0
  86. package/dist/rating.d.ts +8 -0
  87. package/dist/rating.js +5 -0
  88. package/dist/react.d.ts +1 -4583
  89. package/dist/react.js +1 -6837
  90. package/dist/select.d.ts +3 -0
  91. package/dist/select.js +4 -0
  92. package/dist/sharepoint-field.d.ts +18 -0
  93. package/dist/sharepoint-field.js +54 -0
  94. package/dist/skeleton.d.ts +6 -0
  95. package/dist/skeleton.js +9 -0
  96. package/dist/spinner.d.ts +5 -0
  97. package/dist/spinner.js +4 -0
  98. package/dist/swatch-picker.d.ts +17 -0
  99. package/dist/swatch-picker.js +11 -0
  100. package/dist/switch.d.ts +3 -0
  101. package/dist/switch.js +4 -0
  102. package/dist/table.d.ts +6 -0
  103. package/dist/table.js +4 -0
  104. package/dist/tabs.d.ts +14 -0
  105. package/dist/tabs.js +30 -0
  106. package/dist/tag-group.d.ts +6 -0
  107. package/dist/tag-group.js +4 -0
  108. package/dist/tag-picker.d.ts +12 -0
  109. package/dist/tag-picker.js +4 -0
  110. package/dist/tag.d.ts +7 -0
  111. package/dist/tag.js +5 -0
  112. package/dist/text.d.ts +5 -0
  113. package/dist/text.js +4 -0
  114. package/dist/textarea.d.ts +3 -0
  115. package/dist/textarea.js +4 -0
  116. package/dist/theme.d.ts +15 -167
  117. package/dist/theme.js +36 -162
  118. package/dist/toast.d.ts +9 -0
  119. package/dist/toast.js +12 -0
  120. package/dist/tree.d.ts +13 -0
  121. package/dist/tree.js +9 -0
  122. package/dist/view-card.d.ts +8 -0
  123. package/dist/view-card.js +4 -0
  124. package/dist/view-switcher.d.ts +13 -0
  125. package/dist/view-switcher.js +7 -0
  126. package/dist/virtual-list.d.ts +11 -0
  127. package/dist/virtual-list.js +25 -0
  128. package/measurements.json +365 -0
  129. package/package.json +19 -42
  130. package/src/theme.css +125 -0
  131. package/AGENTS.md +0 -211
  132. package/dist/appearance.d.ts +0 -6
  133. package/dist/appearance.js +0 -217
  134. package/dist/elements/accordion.d.ts +0 -9
  135. package/dist/elements/accordion.js +0 -338
  136. package/dist/elements/alert.d.ts +0 -2
  137. package/dist/elements/alert.js +0 -62
  138. package/dist/elements/avatar-group-primitives.d.ts +0 -3
  139. package/dist/elements/avatar-group-primitives.js +0 -189
  140. package/dist/elements/avatar-group.d.ts +0 -2
  141. package/dist/elements/avatar-group.js +0 -110
  142. package/dist/elements/avatar.d.ts +0 -4
  143. package/dist/elements/avatar.js +0 -251
  144. package/dist/elements/badge.d.ts +0 -8
  145. package/dist/elements/badge.js +0 -182
  146. package/dist/elements/breadcrumb.d.ts +0 -4
  147. package/dist/elements/breadcrumb.js +0 -233
  148. package/dist/elements/button-group.d.ts +0 -1
  149. package/dist/elements/button-group.js +0 -20
  150. package/dist/elements/button-variants.d.ts +0 -6
  151. package/dist/elements/button-variants.js +0 -339
  152. package/dist/elements/button.d.ts +0 -5
  153. package/dist/elements/button.js +0 -233
  154. package/dist/elements/card-primitives.d.ts +0 -3
  155. package/dist/elements/card-primitives.js +0 -83
  156. package/dist/elements/card.d.ts +0 -5
  157. package/dist/elements/card.js +0 -213
  158. package/dist/elements/carousel.d.ts +0 -92
  159. package/dist/elements/carousel.js +0 -1112
  160. package/dist/elements/color-picker.d.ts +0 -24
  161. package/dist/elements/color-picker.js +0 -1004
  162. package/dist/elements/combobox.d.ts +0 -1
  163. package/dist/elements/combobox.js +0 -1012
  164. package/dist/elements/data-grid-model.d.ts +0 -117
  165. package/dist/elements/data-grid-model.js +0 -294
  166. package/dist/elements/data-grid.d.ts +0 -29
  167. package/dist/elements/data-grid.js +0 -1567
  168. package/dist/elements/date-picker.d.ts +0 -1
  169. package/dist/elements/date-picker.js +0 -486
  170. package/dist/elements/dialog.d.ts +0 -23
  171. package/dist/elements/dialog.js +0 -409
  172. package/dist/elements/divider.d.ts +0 -2
  173. package/dist/elements/divider.js +0 -66
  174. package/dist/elements/drawer.d.ts +0 -3
  175. package/dist/elements/drawer.js +0 -308
  176. package/dist/elements/field-composition.d.ts +0 -26
  177. package/dist/elements/field-composition.js +0 -133
  178. package/dist/elements/field.d.ts +0 -1
  179. package/dist/elements/field.js +0 -67
  180. package/dist/elements/file-upload.d.ts +0 -6
  181. package/dist/elements/file-upload.js +0 -211
  182. package/dist/elements/floating-position.d.ts +0 -26
  183. package/dist/elements/floating-position.js +0 -39
  184. package/dist/elements/image.d.ts +0 -28
  185. package/dist/elements/image.js +0 -38
  186. package/dist/elements/info.d.ts +0 -9
  187. package/dist/elements/info.js +0 -102
  188. package/dist/elements/input.d.ts +0 -5
  189. package/dist/elements/input.js +0 -255
  190. package/dist/elements/label.d.ts +0 -15
  191. package/dist/elements/label.js +0 -56
  192. package/dist/elements/link.d.ts +0 -1
  193. package/dist/elements/link.js +0 -92
  194. package/dist/elements/list.d.ts +0 -19
  195. package/dist/elements/list.js +0 -297
  196. package/dist/elements/menu.d.ts +0 -33
  197. package/dist/elements/menu.js +0 -1103
  198. package/dist/elements/message-bar.d.ts +0 -10
  199. package/dist/elements/message-bar.js +0 -299
  200. package/dist/elements/nav.d.ts +0 -32
  201. package/dist/elements/nav.js +0 -791
  202. package/dist/elements/overflow.d.ts +0 -9
  203. package/dist/elements/overflow.js +0 -218
  204. package/dist/elements/persona.d.ts +0 -3
  205. package/dist/elements/persona.js +0 -125
  206. package/dist/elements/popover.d.ts +0 -77
  207. package/dist/elements/popover.js +0 -840
  208. package/dist/elements/progress-bar.d.ts +0 -4
  209. package/dist/elements/progress-bar.js +0 -82
  210. package/dist/elements/radio-group.d.ts +0 -1
  211. package/dist/elements/radio-group.js +0 -303
  212. package/dist/elements/rating-display.d.ts +0 -2
  213. package/dist/elements/rating-display.js +0 -125
  214. package/dist/elements/rating.d.ts +0 -2
  215. package/dist/elements/rating.js +0 -206
  216. package/dist/elements/rich-text-editor.d.ts +0 -18
  217. package/dist/elements/rich-text-editor.js +0 -508
  218. package/dist/elements/search.d.ts +0 -3
  219. package/dist/elements/search.js +0 -207
  220. package/dist/elements/select.d.ts +0 -6
  221. package/dist/elements/select.js +0 -291
  222. package/dist/elements/shared.d.ts +0 -19
  223. package/dist/elements/shared.js +0 -396
  224. package/dist/elements/skeleton.d.ts +0 -6
  225. package/dist/elements/skeleton.js +0 -91
  226. package/dist/elements/slider.d.ts +0 -1
  227. package/dist/elements/slider.js +0 -248
  228. package/dist/elements/spin-button.d.ts +0 -7
  229. package/dist/elements/spin-button.js +0 -499
  230. package/dist/elements/spinner.d.ts +0 -4
  231. package/dist/elements/spinner.js +0 -124
  232. package/dist/elements/swatch-picker.d.ts +0 -5
  233. package/dist/elements/swatch-picker.js +0 -363
  234. package/dist/elements/table.d.ts +0 -21
  235. package/dist/elements/table.js +0 -261
  236. package/dist/elements/tabs.d.ts +0 -1
  237. package/dist/elements/tabs.js +0 -276
  238. package/dist/elements/tag-picker.d.ts +0 -29
  239. package/dist/elements/tag-picker.js +0 -697
  240. package/dist/elements/tags.d.ts +0 -21
  241. package/dist/elements/tags.js +0 -421
  242. package/dist/elements/teaching-popover.d.ts +0 -32
  243. package/dist/elements/teaching-popover.js +0 -412
  244. package/dist/elements/text.d.ts +0 -9
  245. package/dist/elements/text.js +0 -75
  246. package/dist/elements/textarea.d.ts +0 -1
  247. package/dist/elements/textarea.js +0 -182
  248. package/dist/elements/toast.d.ts +0 -25
  249. package/dist/elements/toast.js +0 -337
  250. package/dist/elements/toaster.d.ts +0 -6
  251. package/dist/elements/toaster.js +0 -356
  252. package/dist/elements/toggle.d.ts +0 -2
  253. package/dist/elements/toggle.js +0 -246
  254. package/dist/elements/toolbar.d.ts +0 -12
  255. package/dist/elements/toolbar.js +0 -381
  256. package/dist/elements/tooltip.d.ts +0 -76
  257. package/dist/elements/tooltip.js +0 -510
  258. package/dist/elements/tree-model.d.ts +0 -19
  259. package/dist/elements/tree-model.js +0 -34
  260. package/dist/elements/tree.d.ts +0 -5
  261. package/dist/elements/tree.js +0 -273
  262. package/dist/elements/view-card.d.ts +0 -2
  263. package/dist/elements/view-card.js +0 -78
  264. package/dist/elements/virtual-list.d.ts +0 -41
  265. package/dist/elements/virtual-list.js +0 -369
  266. package/dist/elements.d.ts +0 -72
  267. package/dist/elements.js +0 -207
  268. package/dist/virtualizer-hooks.d.ts +0 -80
  269. package/dist/virtualizer-hooks.js +0 -253
  270. package/scripts/generate-token-index.mjs +0 -43
  271. package/scripts/sync-component-catalog-signatures.mjs +0 -55
  272. package/scripts/validate-component-catalog.mjs +0 -44
  273. package/skills/sharepoint-ui/SKILL.md +0 -12
  274. package/skills/sharepoint-ui/references/choosing-components.md +0 -20
  275. package/skills/sharepoint-ui/references/sharepoint-patterns.md +0 -17
  276. package/tokens.json +0 -3777
@@ -1,1004 +0,0 @@
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
- }