@finsweet/webflow-apps-utils 1.0.8 → 1.0.10

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 (51) hide show
  1. package/dist/types/customCode.d.ts +1 -1
  2. package/dist/ui/components/Loader.svelte +0 -2
  3. package/dist/ui/components/LoadingScreen.svelte +6 -2
  4. package/dist/ui/components/LoadingScreen.svelte.d.ts +1 -0
  5. package/dist/ui/components/breakpoints/BreakpointItem.svelte +2 -2
  6. package/dist/ui/components/button/Button.stories.svelte +0 -8
  7. package/dist/ui/components/button/Button.svelte +68 -76
  8. package/dist/ui/components/button/index.d.ts +1 -1
  9. package/dist/ui/components/button/index.js +1 -0
  10. package/dist/ui/components/color-picker/ColorPicker.stories.svelte +42 -0
  11. package/dist/ui/components/color-picker/ColorPicker.stories.svelte.d.ts +19 -0
  12. package/dist/ui/components/color-picker/ColorPicker.svelte +155 -0
  13. package/dist/ui/components/color-picker/ColorPicker.svelte.d.ts +8 -0
  14. package/dist/ui/components/color-picker/ColorSelect.stories.svelte +61 -0
  15. package/dist/ui/components/color-picker/ColorSelect.stories.svelte.d.ts +27 -0
  16. package/dist/ui/components/color-picker/ColorSelect.svelte +940 -0
  17. package/dist/ui/components/color-picker/ColorSelect.svelte.d.ts +7 -0
  18. package/dist/ui/components/color-picker/index.d.ts +1 -0
  19. package/dist/ui/components/color-picker/index.js +1 -0
  20. package/dist/ui/components/controlled-buttons/ControlledButtons.svelte +17 -7
  21. package/dist/ui/components/copy-text/CopyText.svelte +48 -39
  22. package/dist/ui/components/divider/Divider.stories.svelte +0 -4
  23. package/dist/ui/components/input/index.d.ts +1 -1
  24. package/dist/ui/components/input/index.js +1 -0
  25. package/dist/ui/components/layout/Layout.svelte +0 -5
  26. package/dist/ui/components/layout/examples/ExampleLayout.svelte +9 -8
  27. package/dist/ui/components/modal/Example.svelte +0 -7
  28. package/dist/ui/components/modal/Modal.stories.svelte +0 -2
  29. package/dist/ui/components/modal/Modal.svelte +1 -1
  30. package/dist/ui/components/notification/Notification.stories.svelte +0 -8
  31. package/dist/ui/components/notification/Notification.svelte +2 -2
  32. package/dist/ui/components/section/Section.stories.svelte +0 -1
  33. package/dist/ui/components/text/README.md +0 -2
  34. package/dist/ui/components/text/Text.stories.svelte +0 -6
  35. package/dist/ui/components/text/Text.svelte +0 -19
  36. package/dist/ui/components/tooltip/Tooltip.svelte +9 -5
  37. package/dist/ui/icons/FinsweetLogoOutlineIcon.svelte +17 -0
  38. package/dist/ui/icons/FinsweetLogoOutlineIcon.svelte.d.ts +26 -0
  39. package/dist/ui/icons/TriangleDownIconToggle.svelte +0 -1
  40. package/dist/ui/icons/index.d.ts +2 -1
  41. package/dist/ui/icons/index.js +2 -1
  42. package/dist/ui/index.css +1 -1
  43. package/dist/ui/providers/GlobalProviderDemo.svelte +0 -2
  44. package/dist/ui/router/Router.stories.js +7 -7
  45. package/dist/ui/router/examples/RouterExample.svelte +0 -9
  46. package/dist/ui/router/examples/pages/AboutPage.svelte +0 -4
  47. package/dist/ui/router/providers/Link.svelte +0 -2
  48. package/dist/ui/router/providers/Route.svelte +0 -3
  49. package/dist/ui/stores/form.d.ts +136 -3
  50. package/dist/ui/stores/form.js +310 -2
  51. package/package.json +1 -1
@@ -0,0 +1,940 @@
1
+ <script lang="ts">
2
+ import { onMount } from 'svelte';
3
+
4
+ let { color = $bindable('#fff'), onchange } = $props<{
5
+ color?: string;
6
+ onchange?: (color: string) => void;
7
+ }>();
8
+
9
+ function setColor(newColor: string) {
10
+ color = newColor;
11
+ onchange?.(newColor);
12
+ }
13
+
14
+ let hue = $state(0);
15
+ let saturation = $state(0);
16
+ let brightness = $state(0);
17
+ let alpha = $state(100);
18
+ let hexValue = $state(color);
19
+ let mode = $state<'HSB' | 'RGB'>('HSB');
20
+
21
+ // RGB values for RGB mode
22
+ let rgbRed = $state(0);
23
+ let rgbGreen = $state(0);
24
+ let rgbBlue = $state(0);
25
+
26
+ // DOM references
27
+ let colorWell: HTMLElement;
28
+ let colorPicker: HTMLElement;
29
+ let hueBar: HTMLElement;
30
+ let alphaBar: HTMLElement;
31
+
32
+ // Interaction state
33
+ let isDragging = $state(false);
34
+ let dragTarget = $state<'well' | 'hue' | 'alpha' | null>(null);
35
+
36
+ // Drag detection state
37
+ let dragStartPosition = $state<{ x: number; y: number } | null>(null);
38
+ const DRAG_THRESHOLD = 3; // pixels of movement before considering it a drag
39
+
40
+ // Color conversion utilities
41
+ function hsbToRgb(h: number, s: number, b: number): [number, number, number] {
42
+ h = h / 360;
43
+ s = s / 100;
44
+ b = b / 100;
45
+ const c = b * s;
46
+ const x = c * (1 - Math.abs(((h * 6) % 2) - 1));
47
+ const m = b - c;
48
+ let r = 0,
49
+ g = 0,
50
+ bl = 0;
51
+
52
+ if (0 <= h && h < 1 / 6) {
53
+ r = c;
54
+ g = x;
55
+ bl = 0;
56
+ } else if (1 / 6 <= h && h < 2 / 6) {
57
+ r = x;
58
+ g = c;
59
+ bl = 0;
60
+ } else if (2 / 6 <= h && h < 3 / 6) {
61
+ r = 0;
62
+ g = c;
63
+ bl = x;
64
+ } else if (3 / 6 <= h && h < 4 / 6) {
65
+ r = 0;
66
+ g = x;
67
+ bl = c;
68
+ } else if (4 / 6 <= h && h < 5 / 6) {
69
+ r = x;
70
+ g = 0;
71
+ bl = c;
72
+ } else if (5 / 6 <= h && h < 1) {
73
+ r = c;
74
+ g = 0;
75
+ bl = x;
76
+ }
77
+
78
+ return [Math.round((r + m) * 255), Math.round((g + m) * 255), Math.round((bl + m) * 255)];
79
+ }
80
+
81
+ function rgbToHex(r: number, g: number, b: number): string {
82
+ return (
83
+ '#' +
84
+ [r, g, b]
85
+ .map((x) => {
86
+ const hex = x.toString(16);
87
+ return hex.length === 1 ? '0' + hex : hex;
88
+ })
89
+ .join('')
90
+ );
91
+ }
92
+
93
+ function hexToHsb(hex: string): [number, number, number] {
94
+ // Handle different hex formats
95
+ let normalizedHex = hex;
96
+ if (hex.startsWith('#')) {
97
+ // Expand 3-digit hex to 6-digit
98
+ if (hex.length === 4) {
99
+ normalizedHex = '#' + hex[1] + hex[1] + hex[2] + hex[2] + hex[3] + hex[3];
100
+ }
101
+ // Remove alpha channel if present (8-digit hex)
102
+ if (hex.length === 9) {
103
+ normalizedHex = hex.substring(0, 7);
104
+ }
105
+ }
106
+
107
+ const r = parseInt(normalizedHex.slice(1, 3), 16) / 255;
108
+ const g = parseInt(normalizedHex.slice(3, 5), 16) / 255;
109
+ const b = parseInt(normalizedHex.slice(5, 7), 16) / 255;
110
+ const max = Math.max(r, g, b);
111
+ const min = Math.min(r, g, b);
112
+ const diff = max - min;
113
+ let h = 0;
114
+ let s = max === 0 ? 0 : diff / max;
115
+ let v = max;
116
+
117
+ if (diff !== 0) {
118
+ if (max === r) {
119
+ h = ((g - b) / diff) % 6;
120
+ } else if (max === g) {
121
+ h = (b - r) / diff + 2;
122
+ } else {
123
+ h = (r - g) / diff + 4;
124
+ }
125
+ }
126
+
127
+ h = Math.round(h * 60);
128
+ if (h < 0) h += 360;
129
+ return [h, Math.round(s * 100), Math.round(v * 100)];
130
+ }
131
+
132
+ // Color validation utility using CSS.supports()
133
+ function isValidColor(value: unknown): boolean {
134
+ // Ensure value is a string
135
+ if (typeof value !== 'string') {
136
+ return false;
137
+ }
138
+
139
+ // Handle hex colors specifically
140
+ if (value.startsWith('#')) {
141
+ // Check for valid hex format: #RGB, #RRGGBB, #RRGGBBAA
142
+ const hexRegex = /^#([0-9A-Fa-f]{3}|[0-9A-Fa-f]{4}|[0-9A-Fa-f]{6}|[0-9A-Fa-f]{8})$/;
143
+ const result = hexRegex.test(value);
144
+ return result;
145
+ }
146
+
147
+ // For non-hex colors, only accept valid hex patterns (without #)
148
+ const hexRegex = /^([0-9A-Fa-f]{3}|[0-9A-Fa-f]{4}|[0-9A-Fa-f]{6}|[0-9A-Fa-f]{8})$/;
149
+ const result = hexRegex.test(value);
150
+ return result;
151
+ }
152
+
153
+ function rgbToHsb(r: number, g: number, b: number): [number, number, number] {
154
+ return hexToHsb(rgbToHex(r, g, b));
155
+ }
156
+
157
+ // Update functions
158
+ function updateColor() {
159
+ let r, g, b;
160
+ if (mode === 'HSB') {
161
+ [r, g, b] = hsbToRgb(hue, saturation, brightness);
162
+ } else {
163
+ r = rgbRed;
164
+ g = rgbGreen;
165
+ b = rgbBlue;
166
+ }
167
+
168
+ hexValue = rgbToHex(r, g, b).toUpperCase();
169
+ setColor(hexValue);
170
+ updateColorPickerPosition();
171
+ }
172
+
173
+ function syncModeValues(newMode: 'HSB' | 'RGB') {
174
+ if (newMode === 'RGB') {
175
+ [rgbRed, rgbGreen, rgbBlue] = hsbToRgb(hue, saturation, brightness);
176
+ } else {
177
+ [hue, saturation, brightness] = rgbToHsb(rgbRed, rgbGreen, rgbBlue);
178
+ }
179
+ }
180
+
181
+ function toggleMode() {
182
+ mode = mode === 'HSB' ? 'RGB' : 'HSB';
183
+ syncModeValues(mode);
184
+ }
185
+
186
+ function updateColorPickerPosition() {
187
+ if (!colorPicker || !colorWell) return;
188
+ const wellRect = colorWell.getBoundingClientRect();
189
+ const x = (saturation / 100) * wellRect.width;
190
+ const y = ((100 - brightness) / 100) * wellRect.height;
191
+ colorPicker.style.left = `${x - 6}px`;
192
+ colorPicker.style.top = `${y - 6}px`;
193
+ }
194
+
195
+ function handleColorWellInteraction(event: MouseEvent) {
196
+ if (!colorWell) return;
197
+ const rect = colorWell.getBoundingClientRect();
198
+ const x = Math.max(0, Math.min(rect.width, event.clientX - rect.left));
199
+ const y = Math.max(0, Math.min(rect.height, event.clientY - rect.top));
200
+ saturation = Math.round((x / rect.width) * 100);
201
+ brightness = Math.round(100 - (y / rect.height) * 100);
202
+ updateColor();
203
+ updateColorPickerPosition();
204
+ }
205
+
206
+ function handleHueBarInteraction(event: MouseEvent) {
207
+ if (!hueBar) return;
208
+ const rect = hueBar.getBoundingClientRect();
209
+ const x = Math.max(0, Math.min(rect.width, event.clientX - rect.left));
210
+ hue = Math.round((x / rect.width) * 360);
211
+ updateColor();
212
+ }
213
+
214
+ function handleAlphaBarInteraction(event: MouseEvent) {
215
+ if (!alphaBar) return;
216
+ const rect = alphaBar.getBoundingClientRect();
217
+ const x = Math.max(0, Math.min(rect.width, event.clientX - rect.left));
218
+ alpha = Math.round((x / rect.width) * 100);
219
+ if (onchange) onchange(hexValue);
220
+ }
221
+
222
+ function handleHexChange(event: Event) {
223
+ const target = event.target as HTMLInputElement;
224
+ let value = target.value;
225
+
226
+ if (!value.startsWith('#')) {
227
+ value = '#' + value;
228
+ }
229
+
230
+ if (isValidColor(value)) {
231
+ hexValue = value.toUpperCase();
232
+ setColor(hexValue);
233
+ const [h, s, b] = hexToHsb(value);
234
+ hue = h;
235
+ saturation = s;
236
+ brightness = b;
237
+ [rgbRed, rgbGreen, rgbBlue] = hsbToRgb(hue, saturation, brightness);
238
+ updateColorPickerPosition();
239
+ }
240
+ }
241
+
242
+ let hexInputValue = $state('');
243
+ let isHexEditing = $state(false);
244
+ let prevHexValue = $state('');
245
+
246
+ function handleHexFocus(event: FocusEvent) {
247
+ prevHexValue = hexValue;
248
+ hexInputValue = hexValue;
249
+ isHexEditing = true;
250
+ }
251
+
252
+ function handleHexInput(event: Event) {
253
+ hexInputValue = (event.target as HTMLInputElement).value;
254
+ }
255
+
256
+ function handleHexBlur(event: FocusEvent) {
257
+ isHexEditing = false;
258
+ const value = hexInputValue.trim();
259
+
260
+ if (!value || !isValidColor(value)) {
261
+ hexInputValue = prevHexValue;
262
+ isHexEditing = true;
263
+
264
+ import('svelte').then(({ tick }) => {
265
+ tick().then(() => {
266
+ isHexEditing = false;
267
+ });
268
+ });
269
+ return;
270
+ }
271
+ const normalizedValue = value.startsWith('#') ? value : `#${value}`;
272
+
273
+ hexValue = normalizedValue.toUpperCase();
274
+
275
+ setColor(hexValue);
276
+ prevHexValue = hexValue;
277
+
278
+ const hsb = hexToHsb(hexValue);
279
+
280
+ if (Array.isArray(hsb)) {
281
+ const [h, s, b] = hsb;
282
+ hue = h;
283
+ saturation = s;
284
+ brightness = b;
285
+ const rgb = hsbToRgb(hue, saturation, brightness);
286
+ if (Array.isArray(rgb)) {
287
+ [rgbRed, rgbGreen, rgbBlue] = rgb;
288
+ }
289
+ }
290
+ updateColorPickerPosition();
291
+ }
292
+
293
+ function handleHexKeydown(event: KeyboardEvent) {
294
+ if (event.key === 'Enter') {
295
+ (event.target as HTMLInputElement).blur();
296
+ }
297
+ }
298
+
299
+ $effect(() => {
300
+ if (!isHexEditing) {
301
+ hexInputValue = hexValue;
302
+ }
303
+ });
304
+
305
+ // Mouse event handlers with improved drag detection
306
+ function handleMouseDown(event: MouseEvent, target: 'well' | 'hue' | 'alpha') {
307
+ // Record the starting position for drag detection
308
+ dragStartPosition = { x: event.clientX, y: event.clientY };
309
+
310
+ // Always handle the initial interaction
311
+ if (target === 'well') {
312
+ handleColorWellInteraction(event);
313
+ } else if (target === 'hue') {
314
+ handleHueBarInteraction(event);
315
+ } else if (target === 'alpha') {
316
+ handleAlphaBarInteraction(event);
317
+ }
318
+ }
319
+
320
+ function handleMouseMove(event: MouseEvent) {
321
+ if (!dragStartPosition) return;
322
+
323
+ // Calculate distance moved
324
+ const deltaX = Math.abs(event.clientX - dragStartPosition.x);
325
+ const deltaY = Math.abs(event.clientY - dragStartPosition.y);
326
+ const distance = Math.sqrt(deltaX * deltaX + deltaY * deltaY);
327
+
328
+ // If we haven't started dragging yet and we've moved beyond threshold
329
+ if (!isDragging && distance > DRAG_THRESHOLD) {
330
+ isDragging = true;
331
+ // Determine drag target based on which element was initially clicked
332
+ if (dragTarget === null) {
333
+ // This shouldn't happen, but fallback to well
334
+ dragTarget = 'well';
335
+ }
336
+ }
337
+
338
+ // If we're dragging, handle the interaction
339
+ if (isDragging && dragTarget) {
340
+ if (dragTarget === 'well') {
341
+ handleColorWellInteraction(event);
342
+ } else if (dragTarget === 'hue') {
343
+ handleHueBarInteraction(event);
344
+ } else if (dragTarget === 'alpha') {
345
+ handleAlphaBarInteraction(event);
346
+ }
347
+ }
348
+ }
349
+
350
+ function handleMouseUp(event: MouseEvent) {
351
+ // If we were dragging, this is the end of the drag
352
+ if (isDragging) {
353
+ isDragging = false;
354
+ dragTarget = null;
355
+ }
356
+
357
+ // Reset drag detection state
358
+ dragStartPosition = null;
359
+ }
360
+
361
+ /**
362
+ * Calculate the position of the handle on the color bar
363
+ * @param value
364
+ * @param min
365
+ * @param max
366
+ * @param range
367
+ */
368
+ function percentHandlePosition(value: number, min: number, max: number, range: number): number {
369
+ return min + (value * (max - min)) / range;
370
+ }
371
+
372
+ // Watch for prop changes
373
+ $effect(() => {
374
+ if (color !== hexValue) {
375
+ if (isValidColor(color)) {
376
+ hexValue = color;
377
+ const [h, s, b] = hexToHsb(color);
378
+ hue = h;
379
+ saturation = s;
380
+ brightness = b;
381
+ [rgbRed, rgbGreen, rgbBlue] = hsbToRgb(hue, saturation, brightness);
382
+ updateColorPickerPosition();
383
+ }
384
+ }
385
+ });
386
+
387
+ let currentColor = $derived(
388
+ `rgba(${hsbToRgb(hue, saturation, brightness).join(', ')}, ${alpha / 100})`
389
+ );
390
+
391
+ onMount(() => {
392
+ // Initialize from color prop
393
+ if (isValidColor(color)) {
394
+ hexValue = color;
395
+ const [h, s, b] = hexToHsb(color);
396
+ hue = h;
397
+ saturation = s;
398
+ brightness = b;
399
+ [rgbRed, rgbGreen, rgbBlue] = hsbToRgb(hue, saturation, brightness);
400
+ }
401
+
402
+ updateColor();
403
+ updateColorPickerPosition();
404
+
405
+ document.addEventListener('mousemove', handleMouseMove);
406
+ document.addEventListener('mouseup', handleMouseUp);
407
+
408
+ return () => {
409
+ document.removeEventListener('mousemove', handleMouseMove);
410
+ document.removeEventListener('mouseup', handleMouseUp);
411
+ };
412
+ });
413
+
414
+ // EyeDropper API handler
415
+ async function handleEyedropper() {
416
+ if ('EyeDropper' in window) {
417
+ try {
418
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
419
+ const eyeDropper = new (window as any).EyeDropper();
420
+ const result = await eyeDropper.open();
421
+ if (result && result.sRGBHex) {
422
+ hexValue = result.sRGBHex.toUpperCase();
423
+ setColor(hexValue);
424
+ const [h, s, b] = hexToHsb(hexValue);
425
+ hue = h;
426
+ saturation = s;
427
+ brightness = b;
428
+ [rgbRed, rgbGreen, rgbBlue] = hsbToRgb(hue, saturation, brightness);
429
+ updateColorPickerPosition();
430
+ }
431
+ } catch (e) {
432
+ // User cancelled or error
433
+ }
434
+ } else {
435
+ alert('EyeDropper API is not supported in this browser.');
436
+ }
437
+ }
438
+
439
+ $effect(() => {
440
+ updateColorPickerPosition();
441
+ });
442
+ </script>
443
+
444
+ <div class="color-picker">
445
+ <div class="color-picker__container">
446
+ <!-- Color Well -->
447
+ <div
448
+ class="color-well"
449
+ tabindex="0"
450
+ bind:this={colorWell}
451
+ onmousedown={(e) => {
452
+ dragTarget = 'well';
453
+ handleMouseDown(e, 'well');
454
+ }}
455
+ role="button"
456
+ aria-label="Color selection well"
457
+ style={`background: linear-gradient(rgba(0, 0, 0, 0), black), linear-gradient(to right, white, rgba(255, 255, 255, 0)) rgb(${hsbToRgb(hue, 100, 100).join(',')});`}
458
+ >
459
+ <div class="color-well__picker" bind:this={colorPicker}></div>
460
+ </div>
461
+
462
+ <!-- Color Bars Section -->
463
+ <div class="color-bars">
464
+ <button
465
+ class="eyedropper-btn"
466
+ type="button"
467
+ onpointerdown={() => {
468
+ handleEyedropper();
469
+ }}
470
+ aria-label="Pick color from screen"
471
+ >
472
+ <div class="eyedropper-btn__icon">
473
+ <svg
474
+ data-wf-icon="EyedropperMediumIcon"
475
+ width="16"
476
+ height="16"
477
+ viewBox="0 0 16 16"
478
+ fill="none"
479
+ xmlns="http://www.w3.org/2000/svg"
480
+ ><path
481
+ d="M6.85355 4.14664C6.65829 4.3419 6.65829 4.65848 6.85355 4.85374L11.1464 9.14664C11.3417 9.3419 11.6583 9.3419 11.8536 9.14664L12.1464 8.85374C12.3417 8.65848 12.3417 8.3419 12.1464 8.14664L11 7.00019L12.75 5.25019C13.4404 4.55983 13.4404 3.44055 12.75 2.75019C12.0596 2.05983 10.9404 2.05983 10.25 2.75019L8.5 4.50019L7.85355 3.85374C7.65829 3.65848 7.34171 3.65848 7.14645 3.85374L6.85355 4.14664Z"
482
+ fill="currentColor"
483
+ ></path><path
484
+ d="M3 12.0002L2.5 12.5002L3.5 13.5002L4 13.0002H5L9 9.00019L7 7.00019L3 11.0002V12.0002Z"
485
+ fill="currentColor"
486
+ ></path></svg
487
+ >
488
+ </div>
489
+ </button>
490
+
491
+ <div class="color-bars__container">
492
+ <!-- Hue Bar -->
493
+ <div class="color-bar">
494
+ <div
495
+ class="color-bar__hue"
496
+ bind:this={hueBar}
497
+ onmousedown={(e) => {
498
+ dragTarget = 'hue';
499
+ handleMouseDown(e, 'hue');
500
+ }}
501
+ role="slider"
502
+ aria-label="Hue slider"
503
+ aria-valuenow={hue}
504
+ aria-valuemin="0"
505
+ aria-valuemax="360"
506
+ tabindex="0"
507
+ style="pointer-events: auto; position: relative;"
508
+ >
509
+ <div
510
+ class="color-bar__hue-handle color-bar__handle"
511
+ style="left: {percentHandlePosition(hue, 2, 98, 360)}%;
512
+ "
513
+ ></div>
514
+ </div>
515
+ </div>
516
+
517
+ <!-- Alpha Bar -->
518
+ <div class="color-bar">
519
+ <div
520
+ class="color-bar__alpha"
521
+ bind:this={alphaBar}
522
+ onmousedown={(e) => {
523
+ dragTarget = 'alpha';
524
+ handleMouseDown(e, 'alpha');
525
+ }}
526
+ role="slider"
527
+ aria-label="Alpha slider"
528
+ aria-valuenow={alpha}
529
+ aria-valuemin="0"
530
+ aria-valuemax="100"
531
+ tabindex="0"
532
+ style="pointer-events: auto; position: relative;"
533
+ >
534
+ <div class="color-bar__alpha-bg" style="pointer-events: none;"></div>
535
+ <div
536
+ class="color-bar__alpha-gradient"
537
+ style="background: linear-gradient(90deg, transparent 0%, {`rgba(${hsbToRgb(hue, saturation, brightness).join(',')},1)`} 100%); pointer-events: none;"
538
+ ></div>
539
+ <div
540
+ class="color-bar__alpha-handle color-bar__handle"
541
+ style="left: {percentHandlePosition(alpha, 2, 98, 100)}%;
542
+
543
+ "
544
+ ></div>
545
+ </div>
546
+ </div>
547
+ </div>
548
+ </div>
549
+
550
+ <div class="divider"></div>
551
+
552
+ <!-- Controls Section -->
553
+ <div class="controls">
554
+ <!-- Hex Input -->
555
+ <div class="control-group">
556
+ <div class="input-wrapper">
557
+ <input
558
+ id="hex-input"
559
+ type="text"
560
+ class="input input--hex"
561
+ value={hexInputValue}
562
+ oninput={handleHexInput}
563
+ onfocus={handleHexFocus}
564
+ onblur={handleHexBlur}
565
+ onkeydown={handleHexKeydown}
566
+ placeholder="#000000"
567
+ />
568
+ </div>
569
+ <label class="label" for="hex-input">HEX</label>
570
+ </div>
571
+
572
+ <!-- HSB/RGB Inputs -->
573
+ <div class="control-group control-group--main">
574
+ <div class="input-row">
575
+ <div class="input-wrapper">
576
+ <input
577
+ class="input input--number"
578
+ value={mode === 'HSB' ? hue : rgbRed}
579
+ min="0"
580
+ max={mode === 'HSB' ? 360 : 255}
581
+ role="spinbutton"
582
+ aria-label={mode === 'HSB' ? 'Hue' : 'Red'}
583
+ oninput={(e) => {
584
+ const target = e.target as HTMLInputElement;
585
+ if (!target) return;
586
+ if (mode === 'HSB') {
587
+ hue = +target.value;
588
+ } else {
589
+ rgbRed = +target.value;
590
+ }
591
+ updateColor();
592
+ }}
593
+ />
594
+ </div>
595
+ <div class="input-wrapper">
596
+ <input
597
+ class="input input--number"
598
+ value={mode === 'HSB' ? saturation : rgbGreen}
599
+ min="0"
600
+ max={mode === 'HSB' ? 100 : 255}
601
+ role="spinbutton"
602
+ aria-label={mode === 'HSB' ? 'Saturation' : 'Green'}
603
+ oninput={(e) => {
604
+ const target = e.target as HTMLInputElement;
605
+ if (!target) return;
606
+ if (mode === 'HSB') {
607
+ saturation = +target.value;
608
+ } else {
609
+ rgbGreen = +target.value;
610
+ }
611
+ updateColor();
612
+ }}
613
+ />
614
+ </div>
615
+ <div class="input-wrapper">
616
+ <input
617
+ class="input input--number"
618
+ value={mode === 'HSB' ? brightness : rgbBlue}
619
+ min="0"
620
+ max={mode === 'HSB' ? 100 : 255}
621
+ role="spinbutton"
622
+ aria-label={mode === 'HSB' ? 'Brightness' : 'Blue'}
623
+ oninput={(e) => {
624
+ const target = e.target as HTMLInputElement;
625
+ if (!target) return;
626
+ if (mode === 'HSB') {
627
+ brightness = +target.value;
628
+ } else {
629
+ rgbBlue = +target.value;
630
+ }
631
+ updateColor();
632
+ }}
633
+ />
634
+ </div>
635
+ </div>
636
+
637
+ <div class="label-row--wrapper">
638
+ <div
639
+ class="label-row"
640
+ onpointerdown={toggleMode}
641
+ role="button"
642
+ tabindex="0"
643
+ onkeydown={() => {}}
644
+ aria-label="Mode toggle"
645
+ >
646
+ <span class="label label--clickable">
647
+ {mode === 'HSB' ? 'H' : 'R'}
648
+ </span>
649
+ <span class="label label--clickable">
650
+ {mode === 'HSB' ? 'S' : 'G'}
651
+ </span>
652
+ <span class="label label--clickable">
653
+ {mode === 'HSB' ? 'B' : 'B'}
654
+ </span>
655
+ </div>
656
+ </div>
657
+ </div>
658
+
659
+ <!-- Alpha Input -->
660
+ <div class="control-group">
661
+ <div class="input-wrapper">
662
+ <input
663
+ class="input input--number"
664
+ bind:value={alpha}
665
+ min="0"
666
+ max="100"
667
+ role="spinbutton"
668
+ aria-label="Alpha"
669
+ />
670
+ </div>
671
+ <span class="label">A</span>
672
+ </div>
673
+ </div>
674
+ </div>
675
+ </div>
676
+
677
+ <style>
678
+ /* Main Container */
679
+ .color-picker {
680
+ width: 241px;
681
+ height: auto;
682
+ box-shadow: 0px 3px 6px -2px rgba(0, 0, 0, 0.36);
683
+ display: inline-flex;
684
+ }
685
+
686
+ .color-picker__container {
687
+ width: 241px;
688
+ padding: 8px;
689
+ background: var(--background4);
690
+ border-radius: 4px;
691
+ display: flex;
692
+ flex-direction: column;
693
+ gap: 8px;
694
+ }
695
+
696
+ /* Color Well */
697
+ .color-well {
698
+ width: 225px;
699
+ height: 150px;
700
+ position: relative;
701
+ border-radius: 2px;
702
+ outline: 1px solid var(--border1, rgba(255, 255, 255, 0.1));
703
+ outline-offset: -1px;
704
+ cursor: crosshair;
705
+ }
706
+
707
+ .color-well__picker {
708
+ position: absolute;
709
+ width: 12px;
710
+ height: 12px;
711
+ border: 2px solid white;
712
+ border-radius: 50%;
713
+ box-shadow: 0 0 3px rgba(0, 0, 0, 0.5);
714
+ pointer-events: none;
715
+ z-index: 10;
716
+ }
717
+
718
+ /* Color Bars */
719
+ .color-bars {
720
+ width: 225px;
721
+ display: flex;
722
+ align-items: center;
723
+ gap: 6px;
724
+ }
725
+ .color-bar__handle {
726
+ position: absolute;
727
+ top: 1px !important;
728
+ transform: translateX(-50%);
729
+ pointer-events: none;
730
+ height: 11px;
731
+ width: 6px;
732
+ border-radius: 1px;
733
+ box-shadow:
734
+ 0 0 0 2px var(--text1),
735
+ 0 0 0 3px rgba(0, 0, 0, 0.3),
736
+ inset 0 0 0 1px rgba(0, 0, 0, 0.3);
737
+ }
738
+
739
+ .eyedropper-btn {
740
+ width: 32px;
741
+ height: 32px;
742
+ padding: 4px;
743
+ background:
744
+ linear-gradient(180deg, rgba(255, 255, 255, 0.12) 0%, rgba(255, 255, 255, 0.1) 100%),
745
+ rgba(255, 255, 255, 0.08);
746
+ box-shadow: 0px 0.5px 1px black;
747
+ border-radius: 4px;
748
+ border: none;
749
+ cursor: pointer;
750
+ display: flex;
751
+ align-items: center;
752
+ justify-content: center;
753
+ }
754
+
755
+ .eyedropper-btn__icon {
756
+ width: 16px;
757
+ height: 16px;
758
+ }
759
+
760
+ .eyedropper-btn__icon svg {
761
+ color: var(--text1, #ebebeb);
762
+ }
763
+ .color-bars__container {
764
+ flex: 1;
765
+ display: flex;
766
+ flex-direction: column;
767
+ gap: 4px;
768
+ }
769
+
770
+ .color-bar {
771
+ padding: 1px 0;
772
+ display: flex;
773
+ }
774
+
775
+ .color-bar__hue {
776
+ width: 186px;
777
+ height: 12px;
778
+ background: linear-gradient(
779
+ 90deg,
780
+ #ff0000 0%,
781
+ #ff8a00 8%,
782
+ #fff500 18%,
783
+ #00ff47 39%,
784
+ #00f0ff 51%,
785
+ #0019fb 64%,
786
+ #fa00ff 84%,
787
+ #ff0000 100%
788
+ );
789
+ border-radius: 2px;
790
+ outline: 1px solid var(--border1, rgba(255, 255, 255, 0.1));
791
+ cursor: pointer;
792
+ }
793
+
794
+ .color-bar__alpha {
795
+ width: 186px;
796
+ border-radius: 2px;
797
+ height: 12px;
798
+ position: relative;
799
+ cursor: pointer;
800
+ }
801
+
802
+ .color-bar__alpha-bg {
803
+ position: absolute;
804
+ width: 186px;
805
+ inset: 0;
806
+ border-radius: 2px;
807
+ overflow: hidden;
808
+ background-image:
809
+ linear-gradient(45deg, #ccc 25%, transparent 25%),
810
+ linear-gradient(-45deg, #ccc 25%, transparent 25%),
811
+ linear-gradient(45deg, transparent 75%, #ccc 75%),
812
+ linear-gradient(-45deg, transparent 75%, #ccc 75%);
813
+ background-size: 8px 8px;
814
+ background-position:
815
+ 0 0,
816
+ 0 4px,
817
+ 4px -4px,
818
+ -4px 0px;
819
+ }
820
+
821
+ .color-bar__alpha-gradient {
822
+ position: absolute;
823
+ inset: 0;
824
+ }
825
+
826
+ .color-bar__hue-handle {
827
+ position: absolute;
828
+ top: -1px;
829
+ transform: translateX(-50%);
830
+ pointer-events: none;
831
+ }
832
+
833
+ .color-bar__alpha-handle {
834
+ position: absolute;
835
+ top: -1px;
836
+ transform: translateX(-50%);
837
+ pointer-events: none;
838
+ }
839
+
840
+ /* Divider */
841
+ .divider {
842
+ width: 225px;
843
+ height: 1px;
844
+ background: var(--border1);
845
+ }
846
+
847
+ /* Controls */
848
+ .controls {
849
+ display: grid;
850
+ grid-template-columns: 70px 1fr 30px;
851
+ gap: 7px;
852
+ width: 100%;
853
+ }
854
+
855
+ .control-group {
856
+ display: grid;
857
+ grid-template-rows: 26px 20px;
858
+ gap: 8px;
859
+ }
860
+
861
+ .control-group--main {
862
+ flex: 1;
863
+ }
864
+
865
+ .input-row {
866
+ display: flex;
867
+ gap: 4px;
868
+ }
869
+
870
+ .label-row {
871
+ display: flex;
872
+ background: #4e4e4e;
873
+ border-radius: 4px;
874
+ cursor: pointer;
875
+ width: 100%;
876
+ box-shadow:
877
+ 0px 0.5px 1px 0px #000,
878
+ 0px 0.5px 0.5px 0px rgba(255, 255, 255, 0.12) inset;
879
+ }
880
+
881
+ .label-row--wrapper {
882
+ display: flex;
883
+ gap: 4px;
884
+ width: 100%;
885
+ flex: 1;
886
+ align-self: stretch;
887
+ justify-content: space-between;
888
+ }
889
+
890
+ .input-wrapper {
891
+ flex: 1;
892
+ }
893
+
894
+ .input {
895
+ width: 100%;
896
+ padding: 4px;
897
+ background: var(--backgroundInput);
898
+ border: 1px solid var(--border2);
899
+ border-radius: 4px;
900
+ color: var(--text1, #ebebeb);
901
+ font-size: 11px;
902
+ font-family: Inter, sans-serif;
903
+ font-weight: 400;
904
+ line-height: 16px;
905
+ outline: none;
906
+ box-shadow: 0px 1px 1px -1px rgba(0, 0, 0, 0.13) inset;
907
+ }
908
+
909
+ .input--hex {
910
+ width: 70px;
911
+ text-align: left;
912
+ }
913
+
914
+ .input--number {
915
+ width: 30px;
916
+ text-align: center;
917
+ }
918
+
919
+ .label {
920
+ color: var(--text1, #ebebeb);
921
+ font-size: 11px;
922
+ font-family: Inter, sans-serif;
923
+ font-weight: 400;
924
+ line-height: 16px;
925
+ text-align: center;
926
+ display: flex;
927
+ align-items: center;
928
+ justify-content: center;
929
+ }
930
+
931
+ .label--clickable {
932
+ flex: 1;
933
+ padding: 4px;
934
+ cursor: pointer;
935
+ }
936
+
937
+ .control-group--main .input--number {
938
+ width: 100%;
939
+ }
940
+ </style>