@finsweet/webflow-apps-utils 1.0.16 → 1.0.18

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.
@@ -39,19 +39,28 @@
39
39
  });
40
40
  </script>
41
41
 
42
- <Story name="Default" args={{}} />
42
+ <Story name="Default" args={{ oncolorchange: handleFullColorChange }} />
43
43
 
44
- <Story name="With Initial Color" args={{ color: '#00ff00' }} />
44
+ <Story
45
+ name="With Initial Color"
46
+ args={{ color: '#00ff00', oncolorchange: handleFullColorChange }}
47
+ />
45
48
 
46
- <Story name="Edge: Invalid Color" args={{ color: '#xyzxyz' }} />
49
+ <Story
50
+ name="Edge: Invalid Color"
51
+ args={{ color: '#xyzxyz', oncolorchange: handleFullColorChange }}
52
+ />
47
53
 
48
- <Story name="Edge: No Color" args={{ color: undefined }} />
54
+ <Story name="Edge: No Color" args={{ color: undefined, oncolorchange: handleFullColorChange }} />
49
55
 
50
- <Story name="Disabled Picker" args={{ color: '#ebebeb', disabled: true }} />
56
+ <Story
57
+ name="Disabled Picker"
58
+ args={{ color: '#ebebeb', disabled: true, oncolorchange: handleFullColorChange }}
59
+ />
51
60
 
52
61
  <Story
53
62
  name="Full Color Object Demo"
54
- args={{ color: '#ff6600' }}
63
+ args={{ color: '#ff6600', oncolorchange: handleFullColorChange }}
55
64
  story={{
56
65
  parameters: {
57
66
  docs: {
@@ -68,15 +77,7 @@
68
77
  name="Alpha Channel Demo"
69
78
  args={{
70
79
  color: '#ff0000',
71
- oncolorchange: (fullColor: {
72
- hex: string;
73
- rgb: { r: number; g: number; b: number; value: string };
74
- rgba: { r: number; g: number; b: number; a: number; value: string };
75
- hsb: { h: number; s: number; b: number; value: string };
76
- alpha: number;
77
- }) => {
78
- console.log('Full color object:', fullColor);
79
- }
80
+ oncolorchange: handleFullColorChange
80
81
  }}
81
82
  story={{
82
83
  parameters: {
@@ -1,4 +1,5 @@
1
1
  <script lang="ts">
2
+ import { normalizeHex } from '../../utils';
2
3
  import Tooltip from '../tooltip/Tooltip.svelte';
3
4
  import ColorSelect from './ColorSelect.svelte';
4
5
 
@@ -28,23 +29,48 @@
28
29
  * Whether the picker is disabled.
29
30
  */
30
31
  disabled?: boolean;
32
+
33
+ /**
34
+ * The id of the picker.
35
+ */
36
+ id?: string;
37
+
38
+ /**
39
+ * Whether the color select should be shown by default.
40
+ */
41
+ defaultShowColorSelect?: boolean;
31
42
  }
32
43
 
33
44
  let {
34
45
  color = $bindable('#fff'),
35
46
  oncolorchange,
36
47
  width = '80px',
37
- disabled = false
48
+ disabled = false,
49
+ defaultShowColorSelect = false,
50
+ id
38
51
  }: Props = $props();
39
52
 
40
- function normalizeHex(value: string): string {
41
- let v = value.trim();
42
-
43
- if (!v.startsWith('#')) v = `#${v}`;
44
-
45
- // Keep original case for consistency with tests
46
- return v;
47
- }
53
+ // Remove local inputValue state
54
+ // let inputValue = $state(color);
55
+
56
+ // Helper: normalize hex to 6-digit uppercase
57
+ // function normalizeHex(value: string): string {
58
+ // if (/^#[A-Fa-f0-9]{3}$/.test(value)) {
59
+ // return (
60
+ // '#' +
61
+ // value
62
+ // .slice(1)
63
+ // .split('')
64
+ // .map((c) => c + c)
65
+ // .join('')
66
+ // .toUpperCase()
67
+ // );
68
+ // }
69
+ // if (/^#[A-Fa-f0-9]{6}$/.test(value)) {
70
+ // return value.toUpperCase();
71
+ // }
72
+ // return value;
73
+ // }
48
74
 
49
75
  function isValidColor(value: string): boolean {
50
76
  const hexRegex = /^#?([A-Fa-f0-9]{3}|[A-Fa-f0-9]{6}|[A-Fa-f0-9]{8})$/;
@@ -107,6 +133,41 @@
107
133
  };
108
134
  }
109
135
 
136
+ function normalizeHexTo6Upper(value: string): string {
137
+ const hex = value.startsWith('#') ? value : `#${value}`;
138
+ if (/^#[A-Fa-f0-9]{3}$/.test(hex)) {
139
+ return (
140
+ '#' +
141
+ hex
142
+ .slice(1)
143
+ .split('')
144
+ .map((c) => c + c)
145
+ .join('')
146
+ .toUpperCase()
147
+ );
148
+ }
149
+ if (/^#[A-Fa-f0-9]{6}$/.test(hex)) {
150
+ return hex.toUpperCase();
151
+ }
152
+ return value;
153
+ }
154
+
155
+ $effect(() => {
156
+ // Only update inputValue if color changes externally (not from input)
157
+ // if (
158
+ // color !== inputValue &&
159
+ // document.activeElement !== null &&
160
+ // document.activeElement !== inputEl
161
+ // ) {
162
+ // inputValue = color;
163
+ // }
164
+ });
165
+
166
+ function handleInput(event: Event) {
167
+ const target = event.target as HTMLInputElement;
168
+ color = target.value;
169
+ }
170
+
110
171
  function handleInputPaste(event: ClipboardEvent) {
111
172
  event.preventDefault();
112
173
 
@@ -116,23 +177,9 @@
116
177
 
117
178
  cleanText = cleanText.substring(0, 9);
118
179
 
119
- if (isValidColor(cleanText)) {
120
- const normalizedValue = normalizeHex(cleanText);
180
+ const normalizedValue = normalizeHexTo6Upper(cleanText);
181
+ if (/^#[A-F0-9]{6}$/.test(normalizedValue)) {
121
182
  color = normalizedValue;
122
- // Call oncolorchange with the new color
123
- const colorObject = createColorObject(normalizedValue);
124
- oncolorchange?.(colorObject);
125
- }
126
- }
127
-
128
- function handleInputChange(event: Event) {
129
- const target = event.target as HTMLInputElement;
130
- const value = target.value.trim();
131
-
132
- if (isValidColor(value)) {
133
- const normalizedValue = normalizeHex(value);
134
- color = normalizedValue;
135
- // Call oncolorchange with the new color
136
183
  const colorObject = createColorObject(normalizedValue);
137
184
  oncolorchange?.(colorObject);
138
185
  }
@@ -140,18 +187,27 @@
140
187
 
141
188
  function handleInputKeydown(event: KeyboardEvent) {
142
189
  if (event.key === 'Enter') {
143
- (event.target as HTMLInputElement).blur();
190
+ // (event.target as HTMLInputElement).blur(); // Removed local inputValue update
144
191
  }
145
192
  }
146
193
 
147
- function handleColorChange(newColor: string) {
148
- const normalizedValue = normalizeHex(newColor);
149
- color = normalizedValue;
194
+ function handleBlur(event: Event) {
195
+ const target = event.target as HTMLInputElement;
196
+ const normalized = normalizeHex(target.value);
197
+ if (/^#[A-F0-9]{6}$/.test(normalized)) {
198
+ color = normalized;
199
+ const colorObject = createColorObject(normalized);
200
+ oncolorchange?.(colorObject);
201
+ }
150
202
  }
151
203
 
152
204
  function handleFullColorChange(fullColor: ColorObject) {
153
205
  oncolorchange?.(fullColor);
206
+ const normalizedValue = normalizeHex(fullColor.hex);
207
+ color = normalizedValue;
154
208
  }
209
+
210
+ let showColorSelect = $state(defaultShowColorSelect);
155
211
  </script>
156
212
 
157
213
  <div class="color-picker">
@@ -164,6 +220,8 @@
164
220
  stopPropagation={true}
165
221
  width="241px"
166
222
  placement="bottom"
223
+ onshow={() => (showColorSelect = true)}
224
+ onclose={() => (showColorSelect = false)}
167
225
  fallbackPlacements={['top-end', 'top', 'bottom-end', 'bottom', 'top-start', 'bottom-start']}
168
226
  >
169
227
  {#snippet target()}
@@ -172,7 +230,9 @@
172
230
  </div>
173
231
  {/snippet}
174
232
  {#snippet tooltip()}
175
- <ColorSelect {color} oncolorchange={handleFullColorChange} />
233
+ {#if showColorSelect}
234
+ <ColorSelect bind:color oncolorchange={handleFullColorChange} />
235
+ {/if}
176
236
  {/snippet}
177
237
  </Tooltip>
178
238
 
@@ -183,12 +243,14 @@
183
243
  bind:value={color}
184
244
  {disabled}
185
245
  readonly={disabled}
186
- oninput={handleInputChange}
246
+ oninput={handleInput}
187
247
  onkeydown={handleInputKeydown}
188
248
  onpaste={handleInputPaste}
249
+ onblur={handleBlur}
189
250
  placeholder="#ffffff"
190
251
  aria-label="Color hex value"
191
252
  style="width: {width}"
253
+ {id}
192
254
  />
193
255
  </div>
194
256
 
@@ -38,6 +38,14 @@ interface Props {
38
38
  * Whether the picker is disabled.
39
39
  */
40
40
  disabled?: boolean;
41
+ /**
42
+ * The id of the picker.
43
+ */
44
+ id?: string;
45
+ /**
46
+ * Whether the color select should be shown by default.
47
+ */
48
+ defaultShowColorSelect?: boolean;
41
49
  }
42
50
  declare const ColorPicker: import("svelte").Component<Props, {}, "color">;
43
51
  type ColorPicker = ReturnType<typeof ColorPicker>;
@@ -0,0 +1,14 @@
1
+ <script lang="ts">
2
+ import ColorPicker from './ColorPicker.svelte';
3
+ let { initialColor = '#000000', oncolorchange } = $props();
4
+ let color = $state(initialColor);
5
+
6
+ //eslint-disable-next-line @typescript-eslint/no-explicit-any
7
+ function handleColorChange(fullColor: any) {
8
+ color = fullColor.hex;
9
+ oncolorchange?.(fullColor);
10
+ }
11
+ </script>
12
+
13
+ <label for="color-input">Color hex value</label>
14
+ <ColorPicker id="color-input" bind:color oncolorchange={handleColorChange} />
@@ -0,0 +1,6 @@
1
+ declare const ColorPickerWrapper: import("svelte").Component<{
2
+ initialColor?: string;
3
+ oncolorchange: any;
4
+ }, {}, "">;
5
+ type ColorPickerWrapper = ReturnType<typeof ColorPickerWrapper>;
6
+ export default ColorPickerWrapper;
@@ -1,5 +1,7 @@
1
1
  <script lang="ts">
2
- import { onMount } from 'svelte';
2
+ import { onMount, tick } from 'svelte';
3
+
4
+ import { normalizeHex } from '../../utils/color-utils';
3
5
 
4
6
  // Color object type definition
5
7
  interface ColorObject {
@@ -83,6 +85,29 @@
83
85
  let dragStartPosition = $state<{ x: number; y: number } | null>(null);
84
86
  const DRAG_THRESHOLD = 3; // pixels of movement before considering it a drag
85
87
 
88
+ // Helper function to normalize color input to hex
89
+ function normalizeColorToHex(colorInput: string): string {
90
+ // If it's already a valid hex, normalize it
91
+ if (colorInput.startsWith('#')) {
92
+ return normalizeHex(colorInput);
93
+ }
94
+
95
+ // For named colors, use a canvas to convert to hex
96
+ const canvas = document.createElement('canvas');
97
+ const ctx = canvas.getContext('2d');
98
+ if (ctx) {
99
+ ctx.fillStyle = colorInput;
100
+ const computedColor = ctx.fillStyle;
101
+ // ctx.fillStyle returns hex format for valid colors
102
+ if (computedColor.startsWith('#')) {
103
+ return computedColor.toUpperCase();
104
+ }
105
+ }
106
+
107
+ // Fallback to original value if conversion fails
108
+ return normalizeHex(colorInput);
109
+ }
110
+
86
111
  // Color conversion utilities
87
112
  function hsbToRgb(h: number, s: number, b: number): [number, number, number] {
88
113
  h = h / 360;
@@ -211,10 +236,26 @@
211
236
  return result;
212
237
  }
213
238
 
214
- // For non-hex colors, only accept valid hex patterns (without #)
215
- const hexRegex = /^([0-9A-Fa-f]{3}|[0-9A-Fa-f]{4}|[0-9A-Fa-f]{6}|[0-9A-Fa-f]{8})$/;
216
- const result = hexRegex.test(value);
217
- return result;
239
+ // For non-hex colors, try to validate using canvas
240
+ try {
241
+ const canvas = document.createElement('canvas');
242
+ const ctx = canvas.getContext('2d');
243
+ if (ctx) {
244
+ ctx.fillStyle = value;
245
+ return (
246
+ ctx.fillStyle !== '#000000' ||
247
+ value === 'black' ||
248
+ value === '#000000' ||
249
+ value === '#000'
250
+ );
251
+ }
252
+ } catch (e) {
253
+ // Fallback to regex for hex without #
254
+ const hexRegex = /^([0-9A-Fa-f]{3}|[0-9A-Fa-f]{4}|[0-9A-Fa-f]{6}|[0-9A-Fa-f]{8})$/;
255
+ return hexRegex.test(value);
256
+ }
257
+
258
+ return false;
218
259
  }
219
260
 
220
261
  function rgbToHsb(r: number, g: number, b: number): [number, number, number] {
@@ -266,8 +307,16 @@
266
307
  const rect = colorWell.getBoundingClientRect();
267
308
  const x = Math.max(0, Math.min(rect.width, event.clientX - rect.left));
268
309
  const y = Math.max(0, Math.min(rect.height, event.clientY - rect.top));
310
+
311
+ // Always update HSB values first
269
312
  saturation = Math.round((x / rect.width) * 100);
270
313
  brightness = Math.round(100 - (y / rect.height) * 100);
314
+
315
+ // If in RGB mode, sync the RGB values with the new HSB values
316
+ if (mode === 'RGB') {
317
+ [rgbRed, rgbGreen, rgbBlue] = hsbToRgb(hue, saturation, brightness);
318
+ }
319
+
271
320
  updateColor();
272
321
  updateColorPickerPosition();
273
322
  }
@@ -276,16 +325,31 @@
276
325
  if (!hueBar) return;
277
326
  const rect = hueBar.getBoundingClientRect();
278
327
  const x = Math.max(0, Math.min(rect.width, event.clientX - rect.left));
328
+
329
+ // Always update HSB hue first
279
330
  hue = Math.round((x / rect.width) * 360);
331
+
332
+ // If in RGB mode, sync the RGB values with the new HSB values
333
+ if (mode === 'RGB') {
334
+ [rgbRed, rgbGreen, rgbBlue] = hsbToRgb(hue, saturation, brightness);
335
+ }
336
+
280
337
  updateColor();
281
338
  }
282
339
 
283
340
  function handleAlphaBarInteraction(event: MouseEvent) {
284
341
  if (!alphaBar) return;
342
+
285
343
  const rect = alphaBar.getBoundingClientRect();
286
344
  const x = Math.max(0, Math.min(rect.width, event.clientX - rect.left));
287
- alpha = Math.round((x / rect.width) * 100);
288
- emitColorChange(hexValue, alpha);
345
+
346
+ let newAlpha = Math.round((x / rect.width) * 100);
347
+
348
+ if (isNaN(newAlpha) || typeof newAlpha !== 'number') newAlpha = 100;
349
+
350
+ alpha = newAlpha;
351
+
352
+ updateColor();
289
353
  }
290
354
 
291
355
  function handleHexChange(event: Event) {
@@ -442,18 +506,18 @@
442
506
  return min + (value * (max - min)) / range;
443
507
  }
444
508
 
445
- // Watch for prop changes
509
+ // Watch for prop changes - FIXED: Better color normalization
446
510
  $effect(() => {
447
- if (color !== hexValue) {
448
- if (isValidColor(color)) {
449
- hexValue = color;
450
- const [h, s, b] = hexToHsb(color);
451
- hue = h;
452
- saturation = s;
453
- brightness = b;
454
- [rgbRed, rgbGreen, rgbBlue] = hsbToRgb(hue, saturation, brightness);
455
- updateColorPickerPosition();
456
- }
511
+ const normalized = normalizeColorToHex(color);
512
+ if (normalized !== hexValue) {
513
+ hexValue = normalized;
514
+ const [h, s, b] = hexToHsb(normalized);
515
+ hue = h;
516
+ saturation = s;
517
+ brightness = b;
518
+ [rgbRed, rgbGreen, rgbBlue] = hsbToRgb(hue, saturation, brightness);
519
+ // Force position update after state changes
520
+ setTimeout(() => updateColorPickerPosition(), 0);
457
521
  }
458
522
  });
459
523
 
@@ -462,18 +526,19 @@
462
526
  );
463
527
 
464
528
  onMount(() => {
465
- // Initialize from color prop
466
- if (isValidColor(color)) {
467
- hexValue = color;
468
- const [h, s, b] = hexToHsb(color);
469
- hue = h;
470
- saturation = s;
471
- brightness = b;
472
- [rgbRed, rgbGreen, rgbBlue] = hsbToRgb(hue, saturation, brightness);
473
- }
474
-
529
+ const normalized = normalizeColorToHex(color);
530
+ hexValue = normalized;
531
+ const [h, s, b] = hexToHsb(normalized);
532
+ hue = h;
533
+ saturation = s;
534
+ brightness = b;
535
+ [rgbRed, rgbGreen, rgbBlue] = hsbToRgb(hue, saturation, brightness);
536
+
537
+ console.log('ColorSelect mounted', { color });
475
538
  updateColor();
476
- updateColorPickerPosition();
539
+ // Delay position update to ensure DOM is ready
540
+
541
+ setTimeout(() => updateColorPickerPosition(), 5);
477
542
 
478
543
  document.addEventListener('mousemove', handleMouseMove);
479
544
  document.addEventListener('mouseup', handleMouseUp);
@@ -511,6 +576,7 @@
511
576
  }
512
577
  }
513
578
 
579
+ // Add an $effect to always update the color well and picker position after state changes
514
580
  $effect(() => {
515
581
  updateColorPickerPosition();
516
582
  });
@@ -744,7 +810,9 @@
744
810
  oninput={(e) => {
745
811
  const target = e.target as HTMLInputElement;
746
812
  if (!target) return;
747
- alpha = +target.value;
813
+ let newAlpha = +target.value;
814
+ if (isNaN(newAlpha) || typeof newAlpha !== 'number') newAlpha = 100;
815
+ alpha = newAlpha;
748
816
  emitColorChange(hexValue, alpha);
749
817
  }}
750
818
  />
@@ -928,7 +996,7 @@
928
996
  /* Controls */
929
997
  .controls {
930
998
  display: grid;
931
- grid-template-columns: 70px 1fr 30px;
999
+ grid-template-columns: 80px 1fr 30px;
932
1000
  gap: 7px;
933
1001
  width: 100%;
934
1002
  }
@@ -988,7 +1056,7 @@
988
1056
  }
989
1057
 
990
1058
  .input--hex {
991
- width: 70px;
1059
+ width: 80px;
992
1060
  text-align: left;
993
1061
  }
994
1062
 
@@ -17,38 +17,10 @@
17
17
  import { cleanupTooltipMessage } from '../../../utils';
18
18
 
19
19
  import Text from '../text/Text.svelte';
20
+ import type { TooltipProps } from './types';
20
21
 
21
22
  const activeTooltips = writable<string[]>([]);
22
23
 
23
- interface Props {
24
- message?: string;
25
- listener?: 'click' | 'hover';
26
- listenerout?: 'click' | 'hover';
27
- placement?: Placement;
28
- position?: string;
29
- showArrow?: boolean;
30
- offsetVal?: number;
31
- hidden?: boolean;
32
- disabled?: boolean;
33
- tooltipIcon?: Component | null;
34
- tooltipIconColor?: string;
35
- width?: string;
36
- padding?: string;
37
- raw?: boolean;
38
- isActive?: boolean;
39
- fallbackPlacements?: Placement[];
40
- stopPropagation?: boolean;
41
- fontColor?: string;
42
- bgColor?: string;
43
- class?: string;
44
- /** Target element snippet */
45
- target?: Snippet;
46
- /** Tooltip content snippet */
47
- tooltip?: Snippet;
48
- onshow?: (event: boolean) => void;
49
- onclose?: (event: boolean) => void;
50
- }
51
-
52
24
  let {
53
25
  message = '',
54
26
  listener = 'hover',
@@ -70,11 +42,12 @@
70
42
  fontColor = 'var(--text2)',
71
43
  bgColor = '',
72
44
  class: className = '',
45
+ targetClassName = '',
73
46
  target,
74
47
  tooltip,
75
48
  onshow,
76
49
  onclose
77
- }: Props = $props();
50
+ }: TooltipProps = $props();
78
51
 
79
52
  type TooltipInstance = {
80
53
  toggle: HTMLElement;
@@ -335,7 +308,7 @@
335
308
  });
336
309
  </script>
337
310
 
338
- <div class="target" bind:this={targetElement} aria-describedby={tooltipId}>
311
+ <div class="target {targetClassName}" bind:this={targetElement} aria-describedby={tooltipId}>
339
312
  {#if target}
340
313
  {@render target()}
341
314
  {:else}
@@ -391,6 +364,9 @@
391
364
  font-weight: 500;
392
365
  font-size: 11px;
393
366
  }
367
+ .target {
368
+ display: inline-flex;
369
+ }
394
370
  .tooltip {
395
371
  display: none;
396
372
  top: 0;
@@ -1,34 +1,6 @@
1
- import { type Placement } from '@floating-ui/dom';
2
- import { type Component, type Snippet } from 'svelte';
3
- interface Props {
4
- message?: string;
5
- listener?: 'click' | 'hover';
6
- listenerout?: 'click' | 'hover';
7
- placement?: Placement;
8
- position?: string;
9
- showArrow?: boolean;
10
- offsetVal?: number;
11
- hidden?: boolean;
12
- disabled?: boolean;
13
- tooltipIcon?: Component | null;
14
- tooltipIconColor?: string;
15
- width?: string;
16
- padding?: string;
17
- raw?: boolean;
18
- isActive?: boolean;
19
- fallbackPlacements?: Placement[];
20
- stopPropagation?: boolean;
21
- fontColor?: string;
22
- bgColor?: string;
23
- class?: string;
24
- /** Target element snippet */
25
- target?: Snippet;
26
- /** Tooltip content snippet */
27
- tooltip?: Snippet;
28
- onshow?: (event: boolean) => void;
29
- onclose?: (event: boolean) => void;
30
- }
31
- declare const Tooltip: Component<Props, {
1
+ import { type Component } from 'svelte';
2
+ import type { TooltipProps } from './types';
3
+ declare const Tooltip: Component<TooltipProps, {
32
4
  show: () => void | undefined;
33
5
  hide: () => void | undefined;
34
6
  }, "hidden" | "isActive">;
@@ -2,6 +2,14 @@ import type { Placement } from '@floating-ui/dom';
2
2
  import type { Component, Snippet } from 'svelte';
3
3
  export type TooltipListener = 'click' | 'hover';
4
4
  export interface TooltipProps {
5
+ /**
6
+ * Event handler for tooltip show
7
+ */
8
+ onshow?: (event: boolean) => void;
9
+ /**
10
+ * Event handler for tooltip show
11
+ */
12
+ onclose?: (event: boolean) => void;
5
13
  /**
6
14
  * The tooltip message content
7
15
  */
@@ -74,6 +82,10 @@ export interface TooltipProps {
74
82
  * Font color for tooltip text
75
83
  */
76
84
  fontColor?: string;
85
+ /**
86
+ * Target element class name
87
+ */
88
+ targetClassName?: string;
77
89
  /**
78
90
  * Target element snippet (what triggers the tooltip)
79
91
  */
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Convert a color name to a hex value
3
+ * @param colorName - The color name to convert
4
+ * @returns The hex value of the color
5
+ */
6
+ export declare function colorNameToHex(colorName: string): string | null;
7
+ /**
8
+ * Normalize a hex value
9
+ * @param value - The hex value to normalize
10
+ * @returns The normalized hex value
11
+ */
12
+ export declare function normalizeHex(value: string): string;
@@ -0,0 +1,50 @@
1
+ /**
2
+ * Convert a color name to a hex value
3
+ * @param colorName - The color name to convert
4
+ * @returns The hex value of the color
5
+ */
6
+ export function colorNameToHex(colorName) {
7
+ const tempDiv = document.createElement('div');
8
+ tempDiv.style.color = colorName;
9
+ document.body.appendChild(tempDiv);
10
+ const rgbColor = window.getComputedStyle(tempDiv).color;
11
+ document.body.removeChild(tempDiv);
12
+ const match = rgbColor.match(/^rgb\((\d+),\s*(\d+),\s*(\d+)\)$/);
13
+ if (match) {
14
+ const r = parseInt(match[1]);
15
+ const g = parseInt(match[2]);
16
+ const b = parseInt(match[3]);
17
+ const toHex = (c) => {
18
+ const hex = c.toString(16);
19
+ return hex.length === 1 ? '0' + hex : hex;
20
+ };
21
+ return `#${toHex(r)}${toHex(g)}${toHex(b)}`;
22
+ }
23
+ else {
24
+ return null;
25
+ }
26
+ }
27
+ /**
28
+ * Normalize a hex value
29
+ * @param value - The hex value to normalize
30
+ * @returns The normalized hex value
31
+ */
32
+ export function normalizeHex(value) {
33
+ let v = value.trim();
34
+ if (!v.startsWith('#')) {
35
+ const hex = colorNameToHex(v);
36
+ if (hex)
37
+ v = hex;
38
+ else
39
+ v = `#${v}`;
40
+ }
41
+ // Expand 3-digit hex to 6-digit
42
+ if (/^#[A-Fa-f0-9]{3}$/.test(v)) {
43
+ v = '#' + v[1] + v[1] + v[2] + v[2] + v[3] + v[3];
44
+ }
45
+ // Only accept valid 6-digit hex
46
+ if (/^#[A-Fa-f0-9]{6}$/.test(v)) {
47
+ return v.toUpperCase();
48
+ }
49
+ return value;
50
+ }
@@ -1,3 +1,4 @@
1
1
  export * from './auth';
2
2
  export * from './diff-mapper';
3
3
  export * from './helpers';
4
+ export * from './color-utils';
@@ -1,3 +1,4 @@
1
1
  export * from './auth';
2
2
  export * from './diff-mapper';
3
3
  export * from './helpers';
4
+ export * from './color-utils';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@finsweet/webflow-apps-utils",
3
- "version": "1.0.16",
3
+ "version": "1.0.18",
4
4
  "description": "Shared utilities for Webflow apps",
5
5
  "homepage": "https://github.com/finsweet/webflow-apps-utils",
6
6
  "repository": {