panelui-native 0.40.0 → 0.43.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 (92) hide show
  1. package/README.md +4 -2
  2. package/lib/module/components/card/index.js +172 -3
  3. package/lib/module/components/card/index.js.map +1 -1
  4. package/lib/module/components/color-picker/index.js +1395 -0
  5. package/lib/module/components/color-picker/index.js.map +1 -0
  6. package/lib/module/components/combobox/index.js +87 -19
  7. package/lib/module/components/combobox/index.js.map +1 -1
  8. package/lib/module/components/drawer/index.js +13 -4
  9. package/lib/module/components/drawer/index.js.map +1 -1
  10. package/lib/module/components/kpi-chart/index.js +648 -0
  11. package/lib/module/components/kpi-chart/index.js.map +1 -0
  12. package/lib/module/components/map/index.js +72 -12
  13. package/lib/module/components/map/index.js.map +1 -1
  14. package/lib/module/components/menu/index.js +40 -10
  15. package/lib/module/components/menu/index.js.map +1 -1
  16. package/lib/module/components/pagination/index.js +19 -9
  17. package/lib/module/components/pagination/index.js.map +1 -1
  18. package/lib/module/components/panelside/index.js +68 -18
  19. package/lib/module/components/panelside/index.js.map +1 -1
  20. package/lib/module/components/radar-chart/index.js +704 -0
  21. package/lib/module/components/radar-chart/index.js.map +1 -0
  22. package/lib/module/components/section-rail/index.js +32 -7
  23. package/lib/module/components/section-rail/index.js.map +1 -1
  24. package/lib/module/components/slider/index.js +240 -39
  25. package/lib/module/components/slider/index.js.map +1 -1
  26. package/lib/module/components/tabs/index.js +194 -10
  27. package/lib/module/components/tabs/index.js.map +1 -1
  28. package/lib/module/components/text-animation/index.js +857 -0
  29. package/lib/module/components/text-animation/index.js.map +1 -0
  30. package/lib/module/icons/index.js +25 -7
  31. package/lib/module/icons/index.js.map +1 -1
  32. package/lib/module/index.js +5 -0
  33. package/lib/module/index.js.map +1 -1
  34. package/lib/module/theme/use-theme.js +1 -1
  35. package/lib/module/utils/chart.js +44 -0
  36. package/lib/module/utils/chart.js.map +1 -1
  37. package/lib/module/utils/color.js +268 -0
  38. package/lib/module/utils/color.js.map +1 -0
  39. package/lib/typescript/src/components/card/index.d.ts +20 -0
  40. package/lib/typescript/src/components/card/index.d.ts.map +1 -1
  41. package/lib/typescript/src/components/color-picker/index.d.ts +258 -0
  42. package/lib/typescript/src/components/color-picker/index.d.ts.map +1 -0
  43. package/lib/typescript/src/components/combobox/index.d.ts.map +1 -1
  44. package/lib/typescript/src/components/drawer/index.d.ts +13 -1
  45. package/lib/typescript/src/components/drawer/index.d.ts.map +1 -1
  46. package/lib/typescript/src/components/kpi-chart/index.d.ts +490 -0
  47. package/lib/typescript/src/components/kpi-chart/index.d.ts.map +1 -0
  48. package/lib/typescript/src/components/map/index.d.ts +37 -2
  49. package/lib/typescript/src/components/map/index.d.ts.map +1 -1
  50. package/lib/typescript/src/components/menu/index.d.ts.map +1 -1
  51. package/lib/typescript/src/components/pagination/index.d.ts.map +1 -1
  52. package/lib/typescript/src/components/panelside/index.d.ts +50 -4
  53. package/lib/typescript/src/components/panelside/index.d.ts.map +1 -1
  54. package/lib/typescript/src/components/radar-chart/index.d.ts +214 -0
  55. package/lib/typescript/src/components/radar-chart/index.d.ts.map +1 -0
  56. package/lib/typescript/src/components/section-rail/index.d.ts +8 -1
  57. package/lib/typescript/src/components/section-rail/index.d.ts.map +1 -1
  58. package/lib/typescript/src/components/slider/index.d.ts +19 -0
  59. package/lib/typescript/src/components/slider/index.d.ts.map +1 -1
  60. package/lib/typescript/src/components/tabs/index.d.ts +11 -1
  61. package/lib/typescript/src/components/tabs/index.d.ts.map +1 -1
  62. package/lib/typescript/src/components/text-animation/index.d.ts +268 -0
  63. package/lib/typescript/src/components/text-animation/index.d.ts.map +1 -0
  64. package/lib/typescript/src/icons/index.d.ts +6 -0
  65. package/lib/typescript/src/icons/index.d.ts.map +1 -1
  66. package/lib/typescript/src/index.d.ts +8 -3
  67. package/lib/typescript/src/index.d.ts.map +1 -1
  68. package/lib/typescript/src/utils/chart.d.ts +21 -0
  69. package/lib/typescript/src/utils/chart.d.ts.map +1 -1
  70. package/lib/typescript/src/utils/color.d.ts +74 -0
  71. package/lib/typescript/src/utils/color.d.ts.map +1 -0
  72. package/package.json +2 -1
  73. package/src/components/card/index.tsx +227 -2
  74. package/src/components/color-picker/index.tsx +1618 -0
  75. package/src/components/combobox/index.tsx +89 -19
  76. package/src/components/drawer/index.tsx +31 -9
  77. package/src/components/kpi-chart/index.tsx +719 -0
  78. package/src/components/map/index.tsx +158 -14
  79. package/src/components/menu/index.tsx +51 -6
  80. package/src/components/pagination/index.tsx +18 -6
  81. package/src/components/panelside/index.tsx +134 -19
  82. package/src/components/radar-chart/index.tsx +910 -0
  83. package/src/components/section-rail/index.tsx +42 -8
  84. package/src/components/slider/index.tsx +283 -41
  85. package/src/components/tabs/index.tsx +263 -8
  86. package/src/components/text-animation/index.tsx +1093 -0
  87. package/src/icons/index.tsx +23 -4
  88. package/src/index.ts +73 -1
  89. package/src/theme/use-theme.ts +1 -1
  90. package/src/utils/chart.ts +52 -0
  91. package/src/utils/color.ts +246 -0
  92. package/theme.css +125 -112
@@ -15,6 +15,12 @@
15
15
  *
16
16
  * Brand marks (Google, Facebook, Apple) are the exception: they carry their
17
17
  * own colours and ignore the context.
18
+ *
19
+ * A glyph whose *meaning* is a horizontal direction mirrors in a right-to-left
20
+ * subtree — the chevrons, the outward arrow, the send plane. Everything else
21
+ * is drawn once and left alone, including the vertical arrows, since the
22
+ * vertical axis does not mirror, and including asymmetric glyphs that are not
23
+ * directions (a magnifier, a pencil, a play triangle).
18
24
  */
19
25
  import { createContext, useContext, type ReactNode } from 'react';
20
26
  import Svg, { Circle, G, Path, type SvgProps } from 'react-native-svg';
@@ -226,11 +232,20 @@ export function ChevronDownIcon({ size = 16, color, ...props }: IconProps) {
226
232
  *
227
233
  * Only for glyphs whose meaning *is* a direction. An icon that happens to be
228
234
  * asymmetric — a pencil, a magnifier — means the same thing either way round,
229
- * and flipping it is just a wrong drawing.
235
+ * and flipping it is just a wrong drawing. The vertical axis never mirrors, so
236
+ * the up and down glyphs stay as they are drawn.
237
+ *
238
+ * Left-to-right gets an identity transform rather than no transform at all.
239
+ * Dropping the prop leaves the last matrix the view was given in place, so a
240
+ * glyph mirrored once stays mirrored when the direction flips back — the
241
+ * arrows in an app that can switch direction at runtime end up pointing at the
242
+ * text one toggle in, and never recover.
230
243
  */
231
- function useFlip(): { transform?: string } {
232
- const rtl = useDirection() === 'rtl';
233
- return rtl ? { transform: 'scale(-1 1) translate(-24 0)' } : {};
244
+ const IDENTITY = 'translate(0 0)';
245
+ const MIRROR = 'scale(-1 1) translate(-24 0)';
246
+
247
+ function useFlip(): { transform: string } {
248
+ return { transform: useDirection() === 'rtl' ? MIRROR : IDENTITY };
234
249
  }
235
250
 
236
251
  export function ChevronLeftIcon({ size = 16, color, ...props }: IconProps) {
@@ -325,6 +340,7 @@ export function SearchIcon({ size = 16, color, ...props }: IconProps) {
325
340
 
326
341
  export function ArrowUpRightIcon({ size = 20, color, ...props }: IconProps) {
327
342
  const resolved = useResolvedColor(color, '#fff');
343
+ const flip = useFlip();
328
344
  return (
329
345
  <Svg width={size} height={size} viewBox="0 0 24 24" fill="none" {...props}>
330
346
  <Path
@@ -333,6 +349,7 @@ export function ArrowUpRightIcon({ size = 20, color, ...props }: IconProps) {
333
349
  strokeWidth={2}
334
350
  strokeLinecap="round"
335
351
  strokeLinejoin="round"
352
+ {...flip}
336
353
  />
337
354
  </Svg>
338
355
  );
@@ -618,6 +635,7 @@ export function ReceiptIcon({ size = 16, color, ...props }: IconProps) {
618
635
  /** Paper plane — sent, submitted, approved-and-forwarded. */
619
636
  export function SendIcon({ size = 16, color, ...props }: IconProps) {
620
637
  const resolved = useResolvedColor(color, '#737373');
638
+ const flip = useFlip();
621
639
  return (
622
640
  <Svg width={size} height={size} viewBox="0 0 24 24" fill="none" {...props}>
623
641
  <Path
@@ -625,6 +643,7 @@ export function SendIcon({ size = 16, color, ...props }: IconProps) {
625
643
  stroke={resolved}
626
644
  strokeWidth={2}
627
645
  strokeLinejoin="round"
646
+ {...flip}
628
647
  />
629
648
  </Svg>
630
649
  );
package/src/index.ts CHANGED
@@ -231,6 +231,19 @@ export {
231
231
  type LngLatBounds,
232
232
  type ViewState,
233
233
  } from './components/map';
234
+ export {
235
+ RadarChart,
236
+ useRadarChart,
237
+ type RadarChartProps,
238
+ type RadarChartHandle,
239
+ type RadarChartHeaderProps,
240
+ type RadarChartGridProps,
241
+ type RadarChartAxisProps,
242
+ type RadarChartSeriesProps,
243
+ type RadarChartLegendProps,
244
+ type RadarChartDatum,
245
+ type RadarChartStatus,
246
+ } from './components/radar-chart';
234
247
  export {
235
248
  Select,
236
249
  type SelectProps,
@@ -296,7 +309,7 @@ export {
296
309
  type CalendarSelection,
297
310
  type DateRange,
298
311
  } from './components/calendar';
299
- export { Card, type CardProps } from './components/card';
312
+ export { Card, type CardProps, type CardWashProps } from './components/card';
300
313
  export { Checkbox, type CheckboxProps } from './components/checkbox';
301
314
  export {
302
315
  Chip,
@@ -306,6 +319,21 @@ export {
306
319
  type ChipVariant,
307
320
  type ChipSize,
308
321
  } from './components/chip';
322
+ export {
323
+ ColorPicker,
324
+ type ColorPickerProps,
325
+ type ColorPickerFieldProps,
326
+ type ColorPickerAreaProps,
327
+ type ColorPickerWheelProps,
328
+ type ColorPickerChannelProps,
329
+ type ColorPickerChannel,
330
+ type ColorPickerHueProps,
331
+ type ColorPickerBrightnessProps,
332
+ type ColorPickerAlphaProps,
333
+ type ColorPickerPreviewProps,
334
+ type ColorPickerSwatchesProps,
335
+ type ColorPickerSize,
336
+ } from './components/color-picker';
309
337
  export {
310
338
  Combobox,
311
339
  type ComboboxProps,
@@ -326,6 +354,7 @@ export {
326
354
  type DrawerCloseProps,
327
355
  type DrawerSide,
328
356
  type DrawerSize,
357
+ type DrawerCloseSide,
329
358
  } from './components/drawer';
330
359
  export {
331
360
  EmptyState,
@@ -390,6 +419,8 @@ export {
390
419
  type PanelsideTriggerProps,
391
420
  type PanelsideMode,
392
421
  type PanelsideSwipeFrom,
422
+ type PanelsideItemSize,
423
+ type PanelsideCtaSize,
393
424
  type UsePanelsideResult,
394
425
  } from './components/panelside';
395
426
  export {
@@ -420,6 +451,26 @@ export {
420
451
  type ItemHeaderProps,
421
452
  type ItemFooterProps,
422
453
  } from './components/item';
454
+ export {
455
+ KpiChart,
456
+ type KpiChartProps,
457
+ type KpiChartHeaderProps,
458
+ type KpiChartIconProps,
459
+ type KpiChartTitleProps,
460
+ type KpiChartStatProps,
461
+ type KpiChartActionsProps,
462
+ type KpiChartContentProps,
463
+ type KpiChartValueProps,
464
+ type KpiChartTrendProps,
465
+ type KpiChartSparklineProps,
466
+ type KpiChartProgressProps,
467
+ type KpiChartFooterProps,
468
+ type KpiChartSeparatorProps,
469
+ type KpiChartGroupProps,
470
+ type KpiChartGroupOrientation,
471
+ type KpiGoodDirection,
472
+ type KpiTone,
473
+ } from './components/kpi-chart';
423
474
  export { Label, type LabelProps, type LabelTextProps } from './components/label';
424
475
  export {
425
476
  LineChart,
@@ -621,6 +672,15 @@ export {
621
672
  type ScrollTextEffect,
622
673
  type ScrollTextSplit,
623
674
  } from './components/scroll-text';
675
+ export {
676
+ TextAnimation,
677
+ type TextAnimationProps,
678
+ type TextAnimationTypingProps,
679
+ type TextAnimationRotatingProps,
680
+ type TextAnimationCountingProps,
681
+ type TextAnimationSlidingProps,
682
+ type TextAnimationScrollingProps,
683
+ } from './components/text-animation';
624
684
  export {
625
685
  ScrollCanvas,
626
686
  type ScrollCanvasProps,
@@ -782,6 +842,18 @@ export * from './hooks';
782
842
 
783
843
  // Utils
784
844
  export { cn } from './utils/cn';
845
+ export {
846
+ formatColor,
847
+ hsvToCss,
848
+ hsvToHex,
849
+ hsvToHsl,
850
+ hsvToRgb,
851
+ isValidColor,
852
+ parseColor,
853
+ rgbToHsv,
854
+ type ColorFormat,
855
+ type HsvaColor,
856
+ } from './utils/color';
785
857
  export {
786
858
  clampTime,
787
859
  compareTime,
@@ -55,7 +55,7 @@ export const PANEL_THEMES: readonly PanelThemeFamily[] = [
55
55
  name: 'Moon',
56
56
  light: 'moon',
57
57
  dark: 'moon-dark',
58
- swatch: ['#0070f3', '#0070f3'],
58
+ swatch: ['#5e6ad2', '#5e6ad2'],
59
59
  },
60
60
  {
61
61
  id: 'grass',
@@ -342,6 +342,58 @@ export function arcPath(
342
342
  return `M${x1},${y1}A${radius},${radius} 0 ${sweep > 0.5 ? 1 : 0} 1 ${x2},${y2}`;
343
343
  }
344
344
 
345
+ /**
346
+ * A point at `radius` from the centre, `turn` of the way round.
347
+ *
348
+ * Turns from twelve o'clock, clockwise, matching `arcPath` — a radar's axes
349
+ * and a ring's arcs are described the same way, and two polar conventions in
350
+ * one file is how a spoke ends up a quarter turn from its own label.
351
+ */
352
+ export function polarPoint(
353
+ cx: number,
354
+ cy: number,
355
+ radius: number,
356
+ turn: number
357
+ ): { x: number; y: number } {
358
+ 'worklet';
359
+ const angle = (turn - 0.25) * Math.PI * 2;
360
+ return { x: cx + radius * Math.cos(angle), y: cy + radius * Math.sin(angle) };
361
+ }
362
+
363
+ /**
364
+ * A closed polygon through evenly spaced spokes.
365
+ *
366
+ * `values` are already scaled to 0…1 — the fraction of the full radius each
367
+ * axis reaches. A `null` is a missing reading rather than a zero: a radar with
368
+ * a hole punched through to its centre says "none of this" when what happened
369
+ * was "we did not measure this", so the gap is bridged by its neighbours and
370
+ * the shape stays a shape.
371
+ */
372
+ export function radarPath(
373
+ values: (number | null)[],
374
+ cx: number,
375
+ cy: number,
376
+ radius: number
377
+ ): string {
378
+ 'worklet';
379
+ const count = values.length;
380
+ if (count < 3 || radius <= 0) return '';
381
+
382
+ let path = '';
383
+ let drawn = 0;
384
+
385
+ for (let index = 0; index < count; index += 1) {
386
+ const value = values[index];
387
+ if (value === null || value === undefined) continue;
388
+ const point = polarPoint(cx, cy, radius * value, index / count);
389
+ path += `${drawn === 0 ? 'M' : 'L'}${point.x},${point.y}`;
390
+ drawn += 1;
391
+ }
392
+
393
+ if (drawn < 3) return '';
394
+ return `${path}Z`;
395
+ }
396
+
345
397
  /** The numbers of one column, with anything unusable left as a gap. */
346
398
  export function columnValues(
347
399
  data: Record<string, unknown>[],
@@ -0,0 +1,246 @@
1
+ /**
2
+ * Colour arithmetic, in the model a picker is shaped like.
3
+ *
4
+ * A picker's controls *are* HSV: a square of saturation against brightness
5
+ * under one hue, with the hue itself on a separate scale. Storing anything
6
+ * else means converting on every frame of a drag and, worse, losing the
7
+ * position of the thumb — a fully black colour is `#000` whatever its hue and
8
+ * saturation were, so a round trip through RGB throws away where the finger
9
+ * was. HSV is kept as the truth and everything here converts *out* of it.
10
+ *
11
+ * Every function is a worklet. The drag never leaves the UI thread, so the
12
+ * hue behind the square, the thumb fill and the preview swatch all need these
13
+ * on the same frame the finger moved, and a JS-only helper would put a bridge
14
+ * hop in the middle of that.
15
+ */
16
+
17
+ /** A colour in the model the picker stores: hue, saturation, value, alpha. */
18
+ export interface HsvaColor {
19
+ /** Hue in degrees, `0`–`360`. */
20
+ h: number;
21
+ /** Saturation, `0`–`1`. */
22
+ s: number;
23
+ /** Value — brightness — `0`–`1`. */
24
+ v: number;
25
+ /** Alpha, `0`–`1`. */
26
+ a: number;
27
+ }
28
+
29
+ /** How a colour is written when it leaves the picker. */
30
+ export type ColorFormat = 'hex' | 'rgb' | 'hsl';
31
+
32
+ function clamp01(n: number) {
33
+ 'worklet';
34
+ return n < 0 ? 0 : n > 1 ? 1 : n;
35
+ }
36
+
37
+ /** Hue wraps rather than clamps — 370° is 10°, and -10° is 350°. */
38
+ function wrapHue(h: number) {
39
+ 'worklet';
40
+ return ((h % 360) + 360) % 360;
41
+ }
42
+
43
+ /** HSV to 8-bit RGB. */
44
+ export function hsvToRgb(h: number, s: number, v: number) {
45
+ 'worklet';
46
+ const hue = wrapHue(h) / 60;
47
+ const sat = clamp01(s);
48
+ const val = clamp01(v);
49
+ const c = val * sat;
50
+ const x = c * (1 - Math.abs((hue % 2) - 1));
51
+ const m = val - c;
52
+
53
+ let r = 0;
54
+ let g = 0;
55
+ let b = 0;
56
+ if (hue < 1) {
57
+ r = c;
58
+ g = x;
59
+ } else if (hue < 2) {
60
+ r = x;
61
+ g = c;
62
+ } else if (hue < 3) {
63
+ g = c;
64
+ b = x;
65
+ } else if (hue < 4) {
66
+ g = x;
67
+ b = c;
68
+ } else if (hue < 5) {
69
+ r = x;
70
+ b = c;
71
+ } else {
72
+ r = c;
73
+ b = x;
74
+ }
75
+
76
+ return {
77
+ r: Math.round((r + m) * 255),
78
+ g: Math.round((g + m) * 255),
79
+ b: Math.round((b + m) * 255),
80
+ };
81
+ }
82
+
83
+ /** 8-bit RGB to HSV. Hue is undefined for a grey, and comes back as `0`. */
84
+ export function rgbToHsv(r: number, g: number, b: number) {
85
+ 'worklet';
86
+ const rr = clamp01(r / 255);
87
+ const gg = clamp01(g / 255);
88
+ const bb = clamp01(b / 255);
89
+ const max = Math.max(rr, gg, bb);
90
+ const min = Math.min(rr, gg, bb);
91
+ const d = max - min;
92
+
93
+ let h = 0;
94
+ if (d !== 0) {
95
+ if (max === rr) h = 60 * (((gg - bb) / d) % 6);
96
+ else if (max === gg) h = 60 * ((bb - rr) / d + 2);
97
+ else h = 60 * ((rr - gg) / d + 4);
98
+ }
99
+
100
+ return { h: wrapHue(h), s: max === 0 ? 0 : d / max, v: max };
101
+ }
102
+
103
+ /** HSV to HSL, for `format="hsl"`. Both are cylinders; only the axis differs. */
104
+ export function hsvToHsl(h: number, s: number, v: number) {
105
+ 'worklet';
106
+ const val = clamp01(v);
107
+ const sat = clamp01(s);
108
+ const l = val * (1 - sat / 2);
109
+ const denominator = Math.min(l, 1 - l);
110
+ return { h: wrapHue(h), s: denominator === 0 ? 0 : (val - l) / denominator, l };
111
+ }
112
+
113
+ /**
114
+ * An `rgba()` string, which is what a style wants. Used for every animated
115
+ * fill in the picker, so it stays cheap: no allocation beyond the string, and
116
+ * no branch on the alpha.
117
+ */
118
+ export function hsvToCss(h: number, s: number, v: number, a = 1) {
119
+ 'worklet';
120
+ const { r, g, b } = hsvToRgb(h, s, v);
121
+ return `rgba(${r}, ${g}, ${b}, ${clamp01(a)})`;
122
+ }
123
+
124
+ function toHexPair(n: number) {
125
+ 'worklet';
126
+ const clamped = n < 0 ? 0 : n > 255 ? 255 : Math.round(n);
127
+ return clamped < 16 ? `0${clamped.toString(16)}` : clamped.toString(16);
128
+ }
129
+
130
+ /**
131
+ * A `#rrggbb` string, gaining a `#rrggbbaa` alpha pair only when the colour is
132
+ * actually translucent — an opaque colour written with a trailing `ff` is the
133
+ * same colour spelled in a way half the CSS parsers in the world reject.
134
+ */
135
+ export function hsvToHex(h: number, s: number, v: number, a = 1) {
136
+ 'worklet';
137
+ const { r, g, b } = hsvToRgb(h, s, v);
138
+ const base = `#${toHexPair(r)}${toHexPair(g)}${toHexPair(b)}`;
139
+ return a >= 1 ? base : `${base}${toHexPair(clamp01(a) * 255)}`;
140
+ }
141
+
142
+ /** Writes a stored colour out in one of the three formats a consumer asks for. */
143
+ export function formatColor(color: HsvaColor, format: ColorFormat = 'hex') {
144
+ 'worklet';
145
+ const { h, s, v, a } = color;
146
+ const alpha = clamp01(a);
147
+
148
+ if (format === 'rgb') {
149
+ const { r, g, b } = hsvToRgb(h, s, v);
150
+ return alpha >= 1
151
+ ? `rgb(${r}, ${g}, ${b})`
152
+ : `rgba(${r}, ${g}, ${b}, ${Math.round(alpha * 100) / 100})`;
153
+ }
154
+
155
+ if (format === 'hsl') {
156
+ const hsl = hsvToHsl(h, s, v);
157
+ const sat = Math.round(hsl.s * 100);
158
+ const light = Math.round(hsl.l * 100);
159
+ const hue = Math.round(hsl.h);
160
+ return alpha >= 1
161
+ ? `hsl(${hue}, ${sat}%, ${light}%)`
162
+ : `hsla(${hue}, ${sat}%, ${light}%, ${Math.round(alpha * 100) / 100})`;
163
+ }
164
+
165
+ return hsvToHex(h, s, v, alpha);
166
+ }
167
+
168
+ function expandShorthand(hex: string) {
169
+ let out = '';
170
+ for (let i = 0; i < hex.length; i++) out += hex[i]! + hex[i]!;
171
+ return out;
172
+ }
173
+
174
+ /**
175
+ * Reads `#rgb`, `#rgba`, `#rrggbb`, `#rrggbbaa`, `rgb()`, `rgba()`, `hsl()`
176
+ * and `hsla()`, and returns `null` for anything else — including the named
177
+ * colours, which would mean shipping a lookup table of 148 words to parse a
178
+ * string a caller almost never writes.
179
+ *
180
+ * `null` rather than a thrown error or a silent black: a picker handed a value
181
+ * it cannot read should keep the one it has, and only a distinguishable "no"
182
+ * lets the caller do that.
183
+ */
184
+ export function parseColor(input: string): HsvaColor | null {
185
+ const value = input.trim().toLowerCase();
186
+
187
+ if (value.startsWith('#')) {
188
+ let body = value.slice(1);
189
+ if (body.length === 3 || body.length === 4) body = expandShorthand(body);
190
+ if ((body.length !== 6 && body.length !== 8) || /[^0-9a-f]/.test(body)) return null;
191
+
192
+ const r = parseInt(body.slice(0, 2), 16);
193
+ const g = parseInt(body.slice(2, 4), 16);
194
+ const b = parseInt(body.slice(4, 6), 16);
195
+ const a = body.length === 8 ? parseInt(body.slice(6, 8), 16) / 255 : 1;
196
+ const { h, s, v } = rgbToHsv(r, g, b);
197
+ return { h, s, v, a };
198
+ }
199
+
200
+ const fn = /^(rgba?|hsla?)\(([^)]+)\)$/.exec(value);
201
+ if (!fn) return null;
202
+
203
+ // Both the comma and the space syntaxes, and the `/` that separates alpha in
204
+ // the space one.
205
+ const parts = fn[2]!.split(/[\s,/]+/).filter(Boolean);
206
+ if (parts.length < 3) return null;
207
+
208
+ const num = (raw: string, scale: number) => {
209
+ const n = parseFloat(raw);
210
+ if (Number.isNaN(n)) return null;
211
+ return raw.endsWith('%') ? (n / 100) * scale : n;
212
+ };
213
+
214
+ const a = parts[3] === undefined ? 1 : num(parts[3], 1);
215
+ if (a === null) return null;
216
+
217
+ if (fn[1]!.startsWith('rgb')) {
218
+ const r = num(parts[0]!, 255);
219
+ const g = num(parts[1]!, 255);
220
+ const b = num(parts[2]!, 255);
221
+ if (r === null || g === null || b === null) return null;
222
+ const { h, s, v } = rgbToHsv(r, g, b);
223
+ return { h, s, v, a: clamp01(a) };
224
+ }
225
+
226
+ const h = num(parts[0]!, 360);
227
+ const sl = num(parts[1]!, 1);
228
+ const l = num(parts[2]!, 1);
229
+ if (h === null || sl === null || l === null) return null;
230
+
231
+ // HSL to HSV, so the picker only ever stores the one model.
232
+ const light = clamp01(l);
233
+ const sat = clamp01(parts[1]!.endsWith('%') ? sl : sl > 1 ? sl / 100 : sl);
234
+ const v = light + sat * Math.min(light, 1 - light);
235
+ return {
236
+ h: wrapHue(h),
237
+ s: v === 0 ? 0 : 2 * (1 - light / v),
238
+ v,
239
+ a: clamp01(a),
240
+ };
241
+ }
242
+
243
+ /** Whether `parseColor` can read this string. */
244
+ export function isValidColor(input: string) {
245
+ return parseColor(input) !== null;
246
+ }