@raylang/elements 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 (95) hide show
  1. package/LICENSE +202 -0
  2. package/NOTICE +4 -0
  3. package/README.md +12 -2
  4. package/base.css +111 -0
  5. package/components/accordion/accordion.js +197 -0
  6. package/components/alert/alert.js +112 -0
  7. package/components/app-bar/app-bar.js +168 -0
  8. package/components/app-shell/app-shell.js +198 -0
  9. package/components/aspect-ratio/aspect-ratio.js +59 -0
  10. package/components/attachment/attachment.js +333 -0
  11. package/components/avatar/avatar.js +96 -0
  12. package/components/badge/badge.js +63 -0
  13. package/components/blockquote/blockquote.js +128 -0
  14. package/components/bottom-nav/bottom-nav.js +309 -0
  15. package/components/breadcrumbs/breadcrumbs.js +175 -0
  16. package/components/button/button.js +283 -0
  17. package/components/button-group/button-group.js +72 -0
  18. package/components/card/card.js +73 -0
  19. package/components/carousel/carousel.js +435 -0
  20. package/components/center/center.js +26 -0
  21. package/components/checkbox/checkbox.js +152 -0
  22. package/components/checkbox-group/checkbox-group.js +152 -0
  23. package/components/code/code.js +574 -0
  24. package/components/color-picker/color-picker.js +775 -0
  25. package/components/combobox/combobox.js +563 -0
  26. package/components/command-palette/command-palette.js +510 -0
  27. package/components/container/container.js +41 -0
  28. package/components/date-picker/date-picker.js +594 -0
  29. package/components/dialog/dialog.js +329 -0
  30. package/components/drawer/drawer.js +139 -0
  31. package/components/empty-state/empty-state.js +66 -0
  32. package/components/file-upload/file-upload.js +589 -0
  33. package/components/grid/grid.js +96 -0
  34. package/components/group/group.js +40 -0
  35. package/components/icon/icon.js +58 -0
  36. package/components/icon-button/icon-button.js +30 -0
  37. package/components/indicator/indicator.js +112 -0
  38. package/components/input/input.js +85 -0
  39. package/components/kbd/kbd.js +60 -0
  40. package/components/menu/menu.js +296 -0
  41. package/components/nav-link/nav-link.js +159 -0
  42. package/components/notification/notification.js +591 -0
  43. package/components/number-input/number-input.js +241 -0
  44. package/components/pagination/pagination.js +194 -0
  45. package/components/password-input/password-input.js +85 -0
  46. package/components/path-input/path-input.js +137 -0
  47. package/components/popover/popover.js +85 -0
  48. package/components/progress/progress.js +109 -0
  49. package/components/radio/radio.js +253 -0
  50. package/components/scroll-area/scroll-area.js +158 -0
  51. package/components/search/search.js +157 -0
  52. package/components/segmented-control/segmented-control.js +190 -0
  53. package/components/select/select.js +506 -0
  54. package/components/separator/separator.js +58 -0
  55. package/components/sidebar/sidebar.js +252 -0
  56. package/components/skeleton/skeleton.js +90 -0
  57. package/components/slider/slider.js +155 -0
  58. package/components/spinner/spinner.js +39 -0
  59. package/components/split/split.js +221 -0
  60. package/components/stack/stack.js +46 -0
  61. package/components/stat/stat.js +79 -0
  62. package/components/statusbar/statusbar.js +110 -0
  63. package/components/stepper/stepper.js +239 -0
  64. package/components/switch/switch.js +45 -0
  65. package/components/table/table.js +328 -0
  66. package/components/tabs/tabs.js +224 -0
  67. package/components/textarea/textarea.js +105 -0
  68. package/components/theme-toggle/theme-toggle.js +78 -0
  69. package/components/timeline/timeline.js +150 -0
  70. package/components/toggle/toggle.js +130 -0
  71. package/components/toggle-group/toggle-group.js +216 -0
  72. package/components/tooltip/tooltip.js +101 -0
  73. package/core/charts.js +107 -0
  74. package/core/color.js +110 -0
  75. package/core/core.js +556 -0
  76. package/core/dismiss.js +69 -0
  77. package/core/floating.js +195 -0
  78. package/core/focus.js +70 -0
  79. package/core/form.js +235 -0
  80. package/core/host.js +200 -0
  81. package/core/hotkey.js +40 -0
  82. package/core/icons.js +104 -0
  83. package/core/motion.js +73 -0
  84. package/core/position.js +91 -0
  85. package/core/raylang.js +167 -0
  86. package/core/scroll.js +58 -0
  87. package/core/styles.js +211 -0
  88. package/core.js +16 -0
  89. package/custom-elements.json +18545 -0
  90. package/index.js +74 -0
  91. package/package.json +312 -4
  92. package/ray-elements.d.ts +2570 -0
  93. package/tokens.css +796 -0
  94. package/tokens.json +2083 -0
  95. package/vscode.html-custom-data.json +5499 -0
@@ -0,0 +1,775 @@
1
+ // <ray-color-picker>: an inline color picker (Mantine's ColorPicker): a
2
+ // saturation/brightness area, a hue slider, an alpha slider when the format
3
+ // has one, a preview and optional swatches. Every part works with the
4
+ // pointer and the keyboard (arrows; Shift for bigger steps).
5
+ //
6
+ // <ray-color-input>: a text field for a color (Mantine's ColorInput): type
7
+ // any CSS color, or pick it in a dropdown with a ray-color-picker; a preview
8
+ // swatch on the left and, where the browser has one, an eyedropper.
9
+ //
10
+ // Both are form-associated and fire `input` while the color moves and
11
+ // `change` when it settles (pointer released, key pressed, swatch chosen).
12
+ // `value` is a string in `format`: hex (#7c3aed), hexa (#7c3aedcc), rgb,
13
+ // rgba, hsl or hsla.
14
+
15
+ import { css, define } from "../../core/core.js";
16
+ import { FormControl, FieldControl, fieldStyles, boxStyles, fieldTemplate } from "../../core/form.js";
17
+ import { sizeStyles, radiusStyles } from "../../core/styles.js";
18
+ import { parseColor } from "../../core/color.js";
19
+ import { place, autoUpdate } from "../../core/position.js";
20
+ import { dismissable } from "../../core/dismiss.js";
21
+ import { play, POP_OUT } from "../../core/motion.js";
22
+
23
+ // --- color model ----------------------------------------------------------------
24
+
25
+ const FORMATS = ["hex", "hexa", "rgb", "rgba", "hsl", "hsla"];
26
+ const clamp = (x, lo = 0, hi = 1) => Math.min(hi, Math.max(lo, x));
27
+
28
+ /** Whether a format carries alpha. */
29
+ const hasAlpha = (format) => /a$/.test(format);
30
+
31
+ function rgbToHsv({ r, g, b }) {
32
+ const max = Math.max(r, g, b);
33
+ const d = max - Math.min(r, g, b);
34
+ let h = 0;
35
+ if (d) {
36
+ if (max === r) h = ((g - b) / d) % 6;
37
+ else if (max === g) h = (b - r) / d + 2;
38
+ else h = (r - g) / d + 4;
39
+ h = (h * 60 + 360) % 360;
40
+ }
41
+ return { h, s: max ? d / max : 0, v: max };
42
+ }
43
+
44
+ function hsvToRgb({ h, s, v }) {
45
+ const f = (n) => {
46
+ const k = (n + h / 60) % 6;
47
+ return v - v * s * Math.max(0, Math.min(k, 4 - k, 1));
48
+ };
49
+ return { r: f(5), g: f(3), b: f(1) };
50
+ }
51
+
52
+ function rgbToHsl({ r, g, b }) {
53
+ const max = Math.max(r, g, b);
54
+ const min = Math.min(r, g, b);
55
+ const l = (max + min) / 2;
56
+ const d = max - min;
57
+ const s = d ? d / (1 - Math.abs(2 * l - 1)) : 0;
58
+ return { h: rgbToHsv({ r, g, b }).h, s, l };
59
+ }
60
+
61
+ function hslToRgb({ h, s, l }) {
62
+ const a = s * Math.min(l, 1 - l);
63
+ const f = (n) => {
64
+ const k = (n + h / 30) % 12;
65
+ return l - a * Math.max(-1, Math.min(k - 3, 9 - k, 1));
66
+ };
67
+ return { r: f(0), g: f(8), b: f(4) };
68
+ }
69
+
70
+ // "50%" → 0.5 of `scale`; "128" → 128 / scale.
71
+ function channel(token, scale) {
72
+ const t = String(token).trim();
73
+ if (t.endsWith("%")) return clamp(parseFloat(t) / 100);
74
+ return clamp(parseFloat(t) / scale);
75
+ }
76
+
77
+ function alphaOf(token) {
78
+ if (token === undefined || token === "") return 1;
79
+ const t = String(token).trim();
80
+ return clamp(t.endsWith("%") ? parseFloat(t) / 100 : parseFloat(t));
81
+ }
82
+
83
+ /**
84
+ * A color string as sRGB channels and alpha, all 0…1, or null. Reads hex
85
+ * (3, 4, 6 or 8 digits, # optional), rgb()/rgba(), hsl()/hsla() (commas or
86
+ * spaces, alpha after a slash or a fourth value) and any other CSS color the
87
+ * browser knows (named colors, oklch()…; alpha 1).
88
+ */
89
+ export function parseColorValue(input) {
90
+ const s = String(input ?? "").trim().toLowerCase();
91
+ if (!s) return null;
92
+ const hex = /^#?([0-9a-f]{3,4}|[0-9a-f]{6}|[0-9a-f]{8})$/.exec(s);
93
+ if (hex) {
94
+ let h = hex[1];
95
+ if (h.length <= 4) h = [...h].map((c) => c + c).join("");
96
+ const n = (i) => parseInt(h.slice(i, i + 2), 16) / 255;
97
+ return { r: n(0), g: n(2), b: n(4), a: h.length === 8 ? n(6) : 1 };
98
+ }
99
+ const fn = /^(rgba?|hsla?)\(([^)]*)\)$/.exec(s);
100
+ if (fn) {
101
+ const [main, slash] = fn[2].split("/");
102
+ const parts = main.split(/[\s,]+/).filter(Boolean);
103
+ if (parts.length < 3 || parts.some((p) => Number.isNaN(parseFloat(p)))) return null;
104
+ const a = alphaOf(slash ?? parts[3]);
105
+ if (Number.isNaN(a)) return null;
106
+ if (fn[1].startsWith("rgb")) return { r: channel(parts[0], 255), g: channel(parts[1], 255), b: channel(parts[2], 255), a };
107
+ const h = ((parseFloat(parts[0]) % 360) + 360) % 360;
108
+ return { ...hslToRgb({ h, s: channel(parts[1], 100), l: channel(parts[2], 100) }), a };
109
+ }
110
+ const rgb = parseColor(s);
111
+ return rgb ? { ...rgb, a: 1 } : null;
112
+ }
113
+
114
+ const round = (x, d = 0) => Number(x.toFixed(d));
115
+
116
+ /** sRGB channels and alpha (0…1) as a string in `format`. */
117
+ export function formatColor({ r, g, b, a = 1 }, format = "hex") {
118
+ const byte = (x) => Math.round(clamp(x) * 255);
119
+ const hex2 = (x) => byte(x).toString(16).padStart(2, "0");
120
+ const alpha = round(clamp(a), 2);
121
+ switch (format) {
122
+ case "hexa": return `#${hex2(r)}${hex2(g)}${hex2(b)}${hex2(a)}`;
123
+ case "rgb": return `rgb(${byte(r)}, ${byte(g)}, ${byte(b)})`;
124
+ case "rgba": return `rgba(${byte(r)}, ${byte(g)}, ${byte(b)}, ${alpha})`;
125
+ case "hsl":
126
+ case "hsla": {
127
+ const { h, s, l } = rgbToHsl({ r, g, b });
128
+ const body = `${Math.round(h)}, ${Math.round(s * 100)}%, ${Math.round(l * 100)}%`;
129
+ return format === "hsl" ? `hsl(${body})` : `hsla(${body}, ${alpha})`;
130
+ }
131
+ default: return `#${hex2(r)}${hex2(g)}${hex2(b)}`;
132
+ }
133
+ }
134
+
135
+ // "#fff, rgb(1, 2, 3), red" → three colors (commas inside parentheses kept).
136
+ function splitColors(list) {
137
+ if (Array.isArray(list)) return list.map(String);
138
+ const out = [];
139
+ let depth = 0;
140
+ let cur = "";
141
+ for (const ch of String(list ?? "")) {
142
+ if (ch === "(") depth++;
143
+ if (ch === ")") depth--;
144
+ if (ch === "," && depth === 0) {
145
+ out.push(cur.trim());
146
+ cur = "";
147
+ } else {
148
+ cur += ch;
149
+ }
150
+ }
151
+ out.push(cur.trim());
152
+ return out.filter(Boolean);
153
+ }
154
+
155
+ // --- ray-color-picker ------------------------------------------------------------
156
+
157
+ const CHECKER = "repeating-conic-gradient(#c8c8c8 0 25%, #fff 0 50%) 0 0 / 8px 8px";
158
+
159
+ const pickerStyles = css`
160
+ :host { display: inline-block; --_w: 15rem; --_thumb: 0.875rem; --_bar: 0.75rem; }
161
+ :host([size="xs"]) { --_w: 11rem; --_thumb: 0.75rem; --_bar: 0.625rem; }
162
+ :host([size="sm"]) { --_w: 13rem; }
163
+ :host([size="lg"]) { --_w: 17.5rem; --_thumb: 1rem; --_bar: 0.875rem; }
164
+ :host([size="xl"]) { --_w: 20rem; --_thumb: 1.125rem; --_bar: 1rem; }
165
+ :host([full-width]) { display: block; --_w: 100%; }
166
+ :host([disabled]) { opacity: 0.5; pointer-events: none; }
167
+ .picker { display: grid; gap: var(--ray-space-2); width: var(--_w); }
168
+ :host([swatches-only]) .area, :host([swatches-only]) .row { display: none; }
169
+
170
+ .area {
171
+ position: relative;
172
+ height: calc(var(--_w) * 0.62);
173
+ background:
174
+ linear-gradient(to top, #000, transparent),
175
+ linear-gradient(to right, #fff, hsl(var(--_hue) 100% 50%));
176
+ border-radius: var(--ray-radius-sm);
177
+ cursor: crosshair;
178
+ touch-action: none;
179
+ }
180
+ :host([full-width]) .area { height: auto; aspect-ratio: 5 / 3; }
181
+ .area-thumb {
182
+ position: absolute;
183
+ left: var(--_s);
184
+ top: calc(100% - var(--_v));
185
+ width: var(--_thumb);
186
+ height: var(--_thumb);
187
+ box-sizing: border-box;
188
+ background: var(--_solid);
189
+ border: 2px solid #fff;
190
+ border-radius: 50%;
191
+ box-shadow: 0 0 0 1px rgb(0 0 0 / 0.35), 0 1px 3px rgb(0 0 0 / 0.35);
192
+ transform: translate(-50%, -50%);
193
+ pointer-events: none;
194
+ }
195
+
196
+ .row { display: flex; align-items: center; gap: var(--ray-space-2); }
197
+ .sliders { display: grid; flex: 1; gap: var(--ray-space-2); min-width: 0; }
198
+ .slider {
199
+ position: relative;
200
+ height: var(--_bar);
201
+ margin-inline: calc(var(--_thumb) / 2);
202
+ border-radius: 999px;
203
+ cursor: pointer;
204
+ touch-action: none;
205
+ }
206
+ .slider::before {
207
+ content: "";
208
+ position: absolute;
209
+ inset: 0 calc(var(--_thumb) / -2);
210
+ border-radius: inherit;
211
+ }
212
+ .hue::before { background: linear-gradient(to right, #f00, #ff0, #0f0, #0ff, #00f, #f0f, #f00); }
213
+ .alpha::before { background: linear-gradient(to right, transparent, var(--_solid)), ${CHECKER}; }
214
+ :host(:not([data-alpha])) .alpha { display: none; }
215
+ .slider-thumb {
216
+ position: absolute;
217
+ top: 50%;
218
+ left: var(--_pos);
219
+ width: var(--_thumb);
220
+ height: var(--_thumb);
221
+ box-sizing: border-box;
222
+ border: 2px solid #fff;
223
+ border-radius: 50%;
224
+ box-shadow: 0 0 0 1px rgb(0 0 0 / 0.35), 0 1px 3px rgb(0 0 0 / 0.35);
225
+ transform: translate(-50%, -50%);
226
+ pointer-events: none;
227
+ }
228
+ .hue .slider-thumb { --_pos: calc(var(--_hue) / 360 * 100%); background: hsl(var(--_hue) 100% 50%); }
229
+ .alpha .slider-thumb { --_pos: var(--_alpha); background: linear-gradient(var(--_color), var(--_color)), ${CHECKER}; }
230
+
231
+ .preview {
232
+ flex: none;
233
+ width: calc(var(--_bar) * 2 + var(--ray-space-2));
234
+ aspect-ratio: 1;
235
+ background: linear-gradient(var(--_color), var(--_color)), ${CHECKER};
236
+ border-radius: var(--ray-radius-sm);
237
+ box-shadow: inset 0 0 0 1px rgb(0 0 0 / 0.12);
238
+ }
239
+ :host(:not([data-alpha])) .preview { width: calc(var(--_bar) * 1.75); }
240
+
241
+ .swatches { display: grid; grid-template-columns: repeat(var(--_per-row, 7), minmax(0, 1fr)); gap: var(--ray-space-1); }
242
+ .swatches:empty { display: none; }
243
+ .swatch {
244
+ aspect-ratio: 1;
245
+ padding: 0;
246
+ background: linear-gradient(var(--_c), var(--_c)), ${CHECKER};
247
+ border: 0;
248
+ border-radius: var(--ray-radius-sm);
249
+ box-shadow: inset 0 0 0 1px rgb(0 0 0 / 0.12);
250
+ cursor: pointer;
251
+ }
252
+ .swatch[aria-pressed="true"] { box-shadow: inset 0 0 0 1px rgb(0 0 0 / 0.12), 0 0 0 2px var(--ray-background), 0 0 0 4px var(--_c); }
253
+
254
+ .area:focus-visible, .slider:focus-visible, .swatch:focus-visible {
255
+ outline: var(--ray-focus-ring-width) solid var(--ray-ring);
256
+ outline-offset: 2px;
257
+ }
258
+ `;
259
+
260
+ export class RayColorPicker extends FormControl {
261
+ static properties = {
262
+ value: { type: String },
263
+ format: { type: String, reflect: true, default: "hex" },
264
+ swatches: { type: String },
265
+ swatchesPerRow: { type: Number, default: 7 },
266
+ swatchesOnly: { type: Boolean, reflect: true },
267
+ size: { type: String, reflect: true, default: "md" },
268
+ fullWidth: { type: Boolean, reflect: true },
269
+ name: { type: String, reflect: true },
270
+ disabled: { type: Boolean, reflect: true },
271
+ label: { type: String },
272
+ };
273
+ static styles = [pickerStyles];
274
+ static template = `
275
+ <div class="picker" part="picker">
276
+ <div class="area" part="saturation" tabindex="0" role="slider" aria-label="Saturation and brightness"><div class="area-thumb"></div></div>
277
+ <div class="row">
278
+ <div class="sliders">
279
+ <div class="slider hue" part="hue" tabindex="0" role="slider" aria-label="Hue" aria-valuemin="0" aria-valuemax="360"><div class="slider-thumb"></div></div>
280
+ <div class="slider alpha" part="alpha" tabindex="0" role="slider" aria-label="Opacity" aria-valuemin="0" aria-valuemax="100"><div class="slider-thumb"></div></div>
281
+ </div>
282
+ <div class="preview" part="preview" aria-hidden="true"></div>
283
+ </div>
284
+ <div class="swatches" part="swatches" role="group" aria-label="Swatches"></div>
285
+ </div>`;
286
+
287
+ /** h 0…360, s/v/a 0…1: kept apart from value so hue survives grays. */
288
+ #hsva = { h: 0, s: 0, v: 0, a: 1 };
289
+ #shown = null;
290
+ #default = "";
291
+ #dragged = false;
292
+
293
+ constructor() {
294
+ super();
295
+ this.area = this.$(".area");
296
+ this.hue = this.$(".hue");
297
+ this.alpha = this.$(".alpha");
298
+ this.#drag(this.area, (x, y) => ({ s: x, v: 1 - y }));
299
+ this.#drag(this.hue, (x) => ({ h: x * 360 }));
300
+ this.#drag(this.alpha, (x) => ({ a: x }));
301
+ this.area.addEventListener("keydown", (e) => {
302
+ const step = e.shiftKey ? 0.1 : 0.01;
303
+ const moves = { ArrowLeft: { s: -step }, ArrowRight: { s: step }, ArrowDown: { v: -step }, ArrowUp: { v: step } };
304
+ this.#key(e, moves[e.key]);
305
+ });
306
+ this.hue.addEventListener("keydown", (e) => {
307
+ const step = e.shiftKey ? 10 : 1;
308
+ const moves = { ArrowLeft: { h: -step }, ArrowDown: { h: -step }, ArrowRight: { h: step }, ArrowUp: { h: step }, Home: { h: -360 }, End: { h: 360 } };
309
+ this.#key(e, moves[e.key]);
310
+ });
311
+ this.alpha.addEventListener("keydown", (e) => {
312
+ const step = e.shiftKey ? 0.1 : 0.01;
313
+ const moves = { ArrowLeft: { a: -step }, ArrowDown: { a: -step }, ArrowRight: { a: step }, ArrowUp: { a: step }, Home: { a: -1 }, End: { a: 1 } };
314
+ this.#key(e, moves[e.key]);
315
+ });
316
+ this.$(".swatches").addEventListener("click", (e) => {
317
+ const swatch = e.target.closest(".swatch");
318
+ if (!swatch) return;
319
+ const rgba = parseColorValue(swatch.dataset.color);
320
+ if (!rgba) return;
321
+ this.#setRgba(rgba);
322
+ this.#emit(true);
323
+ });
324
+ }
325
+
326
+ connectedCallback() {
327
+ this.#default = this.getAttribute("value") ?? "";
328
+ super.connectedCallback();
329
+ }
330
+
331
+ /** The current color as sRGB channels and alpha (0…1). */
332
+ get rgba() {
333
+ return { ...hsvToRgb(this.#hsva), a: this.#hsva.a };
334
+ }
335
+
336
+ update(changed) {
337
+ const format = FORMATS.includes(this.format) ? this.format : "hex";
338
+ this.toggleAttribute("data-alpha", hasAlpha(format));
339
+ // A value from outside: read it, unless it is what we just wrote.
340
+ if (this.value !== this.#shown) {
341
+ const rgba = parseColorValue(this.value);
342
+ if (rgba) this.#setRgba(rgba);
343
+ this.#shown = this.value;
344
+ }
345
+ if (changed.has("format") && this.value) this.#write();
346
+ this.#paint();
347
+ this.#swatches();
348
+ this.style.setProperty("--_per-row", String(this.swatchesPerRow || 7));
349
+ const off = this.disabled || this.effectivelyDisabled;
350
+ for (const el of [this.area, this.hue, this.alpha]) el.tabIndex = off ? -1 : 0;
351
+ if (this.label) this.$(".picker").setAttribute("aria-label", this.label);
352
+ this.$(".picker").setAttribute("role", "group");
353
+ this.internals.setFormValue(this.name && this.value ? this.value : null);
354
+ }
355
+
356
+ #setRgba(rgba) {
357
+ const hsv = rgbToHsv(rgba);
358
+ // A gray has no hue and black no saturation: keep the previous ones.
359
+ this.#hsva = {
360
+ h: hsv.s === 0 || hsv.v === 0 ? this.#hsva.h : hsv.h,
361
+ s: hsv.v === 0 ? this.#hsva.s : hsv.s,
362
+ v: hsv.v,
363
+ a: rgba.a,
364
+ };
365
+ }
366
+
367
+ #write() {
368
+ const format = FORMATS.includes(this.format) ? this.format : "hex";
369
+ const v = formatColor(this.rgba, format);
370
+ this.#shown = v;
371
+ this.commitProperty("value", v);
372
+ this.internals.setFormValue(this.name ? v : null);
373
+ }
374
+
375
+ #paint() {
376
+ const { h, s, v, a } = this.#hsva;
377
+ const rgb = hsvToRgb(this.#hsva);
378
+ const solid = formatColor(rgb, "rgb");
379
+ const st = this.style;
380
+ st.setProperty("--_hue", String(Math.round(h * 10) / 10));
381
+ st.setProperty("--_s", `${s * 100}%`);
382
+ st.setProperty("--_v", `${v * 100}%`);
383
+ st.setProperty("--_alpha", `${a * 100}%`);
384
+ st.setProperty("--_solid", solid);
385
+ st.setProperty("--_color", formatColor({ ...rgb, a }, "rgba"));
386
+ this.area.setAttribute("aria-valuetext", `Saturation ${Math.round(s * 100)}%, brightness ${Math.round(v * 100)}%`);
387
+ this.area.setAttribute("aria-valuenow", String(Math.round(s * 100)));
388
+ this.hue.setAttribute("aria-valuenow", String(Math.round(h)));
389
+ this.hue.setAttribute("aria-valuetext", `${Math.round(h)} degrees`);
390
+ this.alpha.setAttribute("aria-valuenow", String(Math.round(a * 100)));
391
+ this.alpha.setAttribute("aria-valuetext", `${Math.round(a * 100)}%`);
392
+ const current = (this.value ?? "").toLowerCase();
393
+ for (const sw of this.$$swatches()) {
394
+ const c = parseColorValue(sw.dataset.color);
395
+ sw.setAttribute("aria-pressed", String(Boolean(c) && formatColor(c, "hexa") === formatColor(this.rgba, "hexa") && current !== ""));
396
+ }
397
+ }
398
+
399
+ $$swatches() {
400
+ return [...this.$(".swatches").children];
401
+ }
402
+
403
+ #swatches() {
404
+ const list = splitColors(this.swatches);
405
+ const box = this.$(".swatches");
406
+ if (box.dataset.list === list.join("|")) return;
407
+ box.dataset.list = list.join("|");
408
+ box.replaceChildren(...list.map((c) => {
409
+ const b = document.createElement("button");
410
+ b.type = "button";
411
+ b.className = "swatch";
412
+ b.dataset.color = c;
413
+ b.style.setProperty("--_c", c);
414
+ b.setAttribute("aria-label", c);
415
+ b.title = c;
416
+ return b;
417
+ }));
418
+ this.#paint();
419
+ }
420
+
421
+ // Pointer drags over an area or a slider: `map` turns the pointer's
422
+ // position (0…1 on each axis) into channel values.
423
+ #drag(el, map) {
424
+ const at = (e) => {
425
+ const r = el.getBoundingClientRect();
426
+ const x = clamp((e.clientX - r.left) / r.width);
427
+ const y = clamp((e.clientY - r.top) / r.height);
428
+ Object.assign(this.#hsva, map(x, y));
429
+ this.#emit(false);
430
+ };
431
+ el.addEventListener("pointerdown", (e) => {
432
+ if (e.button !== 0 || this.disabled) return;
433
+ e.preventDefault();
434
+ el.focus({ preventScroll: true });
435
+ el.setPointerCapture(e.pointerId);
436
+ this.#dragged = true;
437
+ at(e);
438
+ });
439
+ el.addEventListener("pointermove", (e) => {
440
+ if (el.hasPointerCapture(e.pointerId)) at(e);
441
+ });
442
+ const end = (e) => {
443
+ if (!el.hasPointerCapture?.(e.pointerId) && !this.#dragged) return;
444
+ this.#dragged = false;
445
+ this.dispatchEvent(new Event("change", { bubbles: true }));
446
+ };
447
+ el.addEventListener("pointerup", end);
448
+ el.addEventListener("lostpointercapture", (e) => { if (this.#dragged) end(e); });
449
+ }
450
+
451
+ #key(e, move) {
452
+ if (!move) return;
453
+ e.preventDefault();
454
+ const c = this.#hsva;
455
+ if ("h" in move) c.h = clamp(c.h + move.h, 0, 360);
456
+ if ("s" in move) c.s = clamp(c.s + move.s);
457
+ if ("v" in move) c.v = clamp(c.v + move.v);
458
+ if ("a" in move) c.a = clamp(c.a + move.a);
459
+ this.#emit(true);
460
+ }
461
+
462
+ // The color moved: write value, repaint, tell (input; change when settled).
463
+ #emit(settled) {
464
+ this.#write();
465
+ this.#paint();
466
+ this.dispatchEvent(new Event("input", { bubbles: true, composed: true }));
467
+ if (settled) this.dispatchEvent(new Event("change", { bubbles: true }));
468
+ }
469
+
470
+ formResetCallback() {
471
+ this.value = this.#default;
472
+ }
473
+
474
+ formStateRestoreCallback(state) {
475
+ this.value = String(state ?? "");
476
+ }
477
+ }
478
+
479
+ define("ray-color-picker", RayColorPicker);
480
+
481
+ // --- ray-color-input --------------------------------------------------------------
482
+
483
+ const inputStyles = css`
484
+ .preview {
485
+ flex: none;
486
+ width: calc(var(--_h) * 0.55);
487
+ height: calc(var(--_h) * 0.55);
488
+ margin-inline-start: calc(var(--_px) * 0.6);
489
+ padding: 0;
490
+ background: linear-gradient(var(--_color, transparent), var(--_color, transparent)), ${CHECKER};
491
+ border: 0;
492
+ border-radius: calc(var(--_r) * 0.6);
493
+ box-shadow: inset 0 0 0 1px rgb(0 0 0 / 0.15);
494
+ cursor: pointer;
495
+ }
496
+ .control { font-variant-numeric: tabular-nums; }
497
+ .dropper {
498
+ display: inline-flex;
499
+ flex: none;
500
+ align-items: center;
501
+ justify-content: center;
502
+ width: max(1.5rem, calc(var(--_h) * 0.65));
503
+ height: max(1.5rem, calc(var(--_h) * 0.65));
504
+ margin-inline-end: calc(var(--_px) * 0.4);
505
+ padding: 0;
506
+ color: var(--ray-muted-foreground);
507
+ background: transparent;
508
+ border: 0;
509
+ border-radius: var(--ray-radius-sm);
510
+ cursor: pointer;
511
+ }
512
+ .dropper:hover { color: var(--ray-foreground); background: var(--ray-default-hover); }
513
+ .dropper svg { width: 1em; height: 1em; }
514
+ .panel {
515
+ box-sizing: border-box;
516
+ margin: 0;
517
+ padding: var(--ray-space-3);
518
+ color: var(--ray-popover-foreground);
519
+ background: var(--ray-popover);
520
+ border: 1px solid var(--ray-border);
521
+ border-radius: var(--ray-radius-md);
522
+ box-shadow: var(--ray-shadow-md);
523
+ }
524
+ .panel:popover-open { animation: ray-pop-in var(--ray-duration-fast) var(--ray-easing-out); }
525
+ @keyframes ray-pop-in { from { opacity: 0; transform: translateY(-2px); } }
526
+ `;
527
+
528
+ export class RayColorInput extends FieldControl {
529
+ static properties = {
530
+ ...FieldControl.fieldProperties,
531
+ value: { type: String },
532
+ format: { type: String, reflect: true, default: "hex" },
533
+ swatches: { type: String },
534
+ swatchesPerRow: { type: Number, default: 7 },
535
+ placeholder: { type: String },
536
+ hideEyedropper: { type: Boolean, reflect: true },
537
+ open: { type: Boolean, reflect: true },
538
+ };
539
+ static styles = [sizeStyles, radiusStyles, fieldStyles, boxStyles, inputStyles];
540
+ static template = fieldTemplate(
541
+ `<button type="button" class="preview" part="preview" tabindex="-1" aria-hidden="true"></button>` +
542
+ `<input id="control" class="control" part="input" autocomplete="off" spellcheck="false" role="combobox" aria-autocomplete="none" aria-haspopup="dialog" aria-expanded="false" aria-controls="panel">` +
543
+ `<button type="button" class="dropper focus-ring" part="eyedropper" aria-label="Pick a color from the screen" hidden><span data-ray-icon="pipette" aria-hidden="true"></span></button>`,
544
+ { sections: false },
545
+ ) + `<div class="panel" id="panel" part="dropdown" popover="manual" role="dialog" aria-label="Choose a color"><ray-color-picker part="picker"></ray-color-picker></div>`;
546
+
547
+ #undismiss = null;
548
+ #exit = null;
549
+ #stopTracking = null;
550
+ #default = "";
551
+ /** The value at the last change event: change fires only when it moved. */
552
+ #settled = "";
553
+
554
+ constructor() {
555
+ super();
556
+ /** The dropdown ([popover]) holding the picker. */
557
+ this.panel = this.$("#panel");
558
+ /** The <ray-color-picker> in the dropdown. */
559
+ this.picker = this.$("ray-color-picker");
560
+ }
561
+
562
+ wire() {
563
+ this.#default = this.getAttribute("value") ?? "";
564
+ this.#settled = this.#default;
565
+ const c = this.control;
566
+ c.addEventListener("click", () => this.toggle(true));
567
+ this.$(".preview").addEventListener("click", () => { c.focus(); this.toggle(); });
568
+ c.addEventListener("keydown", (e) => {
569
+ if (e.key === "ArrowDown" && !this.open) {
570
+ e.preventDefault();
571
+ this.toggle(true);
572
+ } else if (e.key === "Enter") {
573
+ this.#fix();
574
+ this.open = false;
575
+ }
576
+ });
577
+ // Typing: a valid color is the value at once; the text is fixed on blur.
578
+ c.addEventListener("input", () => {
579
+ const rgba = parseColorValue(c.value);
580
+ c.setCustomValidity(c.value && !rgba ? "Enter a valid color." : "");
581
+ if (rgba) {
582
+ this.commitProperty("value", formatColor(rgba, this.#format()));
583
+ this.picker.value = this.value;
584
+ } else if (!c.value) {
585
+ this.commitProperty("value", "");
586
+ }
587
+ this._commit();
588
+ this.#paint();
589
+ this.dispatchEvent(new Event("input", { bubbles: true, composed: true }));
590
+ });
591
+ c.addEventListener("change", () => this.#fix());
592
+ c.addEventListener("blur", () => this.#fix());
593
+ this.picker.addEventListener("input", (e) => {
594
+ e.stopPropagation();
595
+ this.commitProperty("value", this.picker.value);
596
+ c.value = this.value;
597
+ c.setCustomValidity("");
598
+ this._commit();
599
+ this.#paint();
600
+ this.dispatchEvent(new Event("input", { bubbles: true, composed: true }));
601
+ });
602
+ this.picker.addEventListener("change", (e) => {
603
+ e.stopPropagation();
604
+ this.#settle();
605
+ });
606
+ const dropper = this.$(".dropper");
607
+ dropper.hidden = !("EyeDropper" in window);
608
+ dropper.addEventListener("click", async () => {
609
+ try {
610
+ const { sRGBHex } = await new window.EyeDropper().open();
611
+ this.#userSet(sRGBHex);
612
+ } catch {
613
+ // Cancelled (Escape) or refused: nothing to do.
614
+ }
615
+ });
616
+ this.panel.addEventListener("toggle", (e) => this.#onToggle(e.newState === "open"));
617
+ }
618
+
619
+ disconnectedCallback() {
620
+ super.disconnectedCallback();
621
+ this.#stopTracking?.();
622
+ this.#stopTracking = null;
623
+ this.#undismiss?.();
624
+ this.#undismiss = null;
625
+ }
626
+
627
+ #format() {
628
+ return FORMATS.includes(this.format) ? this.format : "hex";
629
+ }
630
+
631
+ update(changed) {
632
+ const c = this.control;
633
+ c.placeholder = this.placeholder ?? "";
634
+ // A value from outside, in the field's format.
635
+ if (changed.has("value") || changed.has("format")) {
636
+ const rgba = parseColorValue(this.value);
637
+ const v = rgba ? formatColor(rgba, this.#format()) : (this.value ?? "");
638
+ if (v !== this.value) this.commitProperty("value", v);
639
+ if (this.shadowRoot.activeElement !== c || !c.value) c.value = v;
640
+ c.setCustomValidity("");
641
+ }
642
+ const p = this.picker;
643
+ p.format = this.#format();
644
+ p.swatches = this.swatches;
645
+ p.swatchesPerRow = this.swatchesPerRow;
646
+ p.size = this.size === "xs" ? "xs" : this.size === "lg" || this.size === "xl" ? "md" : "sm";
647
+ if (this.value && p.value !== this.value) p.value = this.value;
648
+ const dropper = this.$(".dropper");
649
+ if (this.hideEyedropper || !("EyeDropper" in window)) dropper.hidden = true;
650
+ else dropper.hidden = false;
651
+ dropper.disabled = this.effectivelyDisabled;
652
+ this.syncField();
653
+ this._commit();
654
+ this.#paint();
655
+ if (changed.has("open")) this.#setOpen(Boolean(this.open));
656
+ }
657
+
658
+ _commit() {
659
+ this.internals.setFormValue(this.value || null);
660
+ this.syncValidity();
661
+ }
662
+
663
+ #paint() {
664
+ const rgba = parseColorValue(this.value);
665
+ if (rgba) this.style.setProperty("--_color", formatColor(rgba, "rgba"));
666
+ else this.style.removeProperty("--_color");
667
+ }
668
+
669
+ // Leaving the field: invalid or partial text goes back to the value.
670
+ #fix() {
671
+ const c = this.control;
672
+ if (c.value !== (this.value ?? "")) c.value = this.value ?? "";
673
+ c.setCustomValidity("");
674
+ this._commit();
675
+ this.#settle();
676
+ }
677
+
678
+ #settle() {
679
+ if ((this.value ?? "") === this.#settled) return;
680
+ this.#settled = this.value ?? "";
681
+ this.dispatchEvent(new Event("change", { bubbles: true }));
682
+ }
683
+
684
+ // A color chosen by the user outside the text: value, field, picker, events.
685
+ #userSet(color) {
686
+ const rgba = parseColorValue(color);
687
+ if (!rgba) return;
688
+ this.value = formatColor(rgba, this.#format());
689
+ this.control.value = this.value;
690
+ this.picker.value = this.value;
691
+ this._commit();
692
+ this.#paint();
693
+ this.dispatchEvent(new Event("input", { bubbles: true, composed: true }));
694
+ this.#settle();
695
+ }
696
+
697
+ /** Opens or closes the dropdown. */
698
+ toggle(force) {
699
+ if (this.effectivelyDisabled) return;
700
+ this.open = force ?? !this.open;
701
+ }
702
+
703
+ #setOpen(open) {
704
+ const p = this.panel;
705
+ if (!this.isConnected) return;
706
+ const shown = p.matches(":popover-open");
707
+ if (open) {
708
+ this.#exit?.cancel();
709
+ this.#exit = null;
710
+ p.style.pointerEvents = "";
711
+ if (!shown) {
712
+ try {
713
+ p.showPopover();
714
+ } catch {
715
+ this.commitProperty("open", false);
716
+ return;
717
+ }
718
+ }
719
+ this.#undismiss ??= dismissable({
720
+ panel: p,
721
+ owner: this,
722
+ trigger: this.$(".box"),
723
+ close: (reason) => {
724
+ this.open = false;
725
+ if (reason === "escape") this.control.focus();
726
+ },
727
+ });
728
+ } else if (shown && !this.#exit) {
729
+ this.#leave();
730
+ }
731
+ }
732
+
733
+ async #leave() {
734
+ const p = this.panel;
735
+ this.#undismiss?.();
736
+ this.#undismiss = null;
737
+ this.control.setAttribute("aria-expanded", "false");
738
+ const exit = play(p, POP_OUT, { speed: "fast" });
739
+ this.#exit = exit;
740
+ p.style.pointerEvents = "none";
741
+ await exit.finished;
742
+ p.style.pointerEvents = "";
743
+ if (this.#exit !== exit) return;
744
+ this.#exit = null;
745
+ if (!this.open && p.matches(":popover-open")) p.hidePopover();
746
+ exit.cancel();
747
+ }
748
+
749
+ #onToggle(open) {
750
+ this.control.setAttribute("aria-expanded", String(open));
751
+ if (open) {
752
+ const box = this.$(".box");
753
+ const update = () => place(box.getBoundingClientRect(), this.panel, { placement: "bottom-start", offset: 4 });
754
+ update();
755
+ this.#stopTracking?.();
756
+ this.#stopTracking = autoUpdate(box, this.panel, update);
757
+ } else {
758
+ this.#stopTracking?.();
759
+ this.#stopTracking = null;
760
+ }
761
+ if (Boolean(this.open) !== open) this.commitProperty("open", open);
762
+ this.emit(open ? "ray-open" : "ray-close");
763
+ }
764
+
765
+ formResetCallback() {
766
+ this.value = this.#default;
767
+ this.#settled = this.#default;
768
+ }
769
+
770
+ formStateRestoreCallback(state) {
771
+ this.value = String(state ?? "");
772
+ }
773
+ }
774
+
775
+ define("ray-color-input", RayColorInput);