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.
- package/README.md +4 -2
- package/lib/module/components/card/index.js +172 -3
- package/lib/module/components/card/index.js.map +1 -1
- package/lib/module/components/color-picker/index.js +1395 -0
- package/lib/module/components/color-picker/index.js.map +1 -0
- package/lib/module/components/combobox/index.js +87 -19
- package/lib/module/components/combobox/index.js.map +1 -1
- package/lib/module/components/drawer/index.js +13 -4
- package/lib/module/components/drawer/index.js.map +1 -1
- package/lib/module/components/kpi-chart/index.js +648 -0
- package/lib/module/components/kpi-chart/index.js.map +1 -0
- package/lib/module/components/map/index.js +72 -12
- package/lib/module/components/map/index.js.map +1 -1
- package/lib/module/components/menu/index.js +40 -10
- package/lib/module/components/menu/index.js.map +1 -1
- package/lib/module/components/pagination/index.js +19 -9
- package/lib/module/components/pagination/index.js.map +1 -1
- package/lib/module/components/panelside/index.js +68 -18
- package/lib/module/components/panelside/index.js.map +1 -1
- package/lib/module/components/radar-chart/index.js +704 -0
- package/lib/module/components/radar-chart/index.js.map +1 -0
- package/lib/module/components/section-rail/index.js +32 -7
- package/lib/module/components/section-rail/index.js.map +1 -1
- package/lib/module/components/slider/index.js +240 -39
- package/lib/module/components/slider/index.js.map +1 -1
- package/lib/module/components/tabs/index.js +194 -10
- package/lib/module/components/tabs/index.js.map +1 -1
- package/lib/module/components/text-animation/index.js +857 -0
- package/lib/module/components/text-animation/index.js.map +1 -0
- package/lib/module/icons/index.js +25 -7
- package/lib/module/icons/index.js.map +1 -1
- package/lib/module/index.js +5 -0
- package/lib/module/index.js.map +1 -1
- package/lib/module/theme/use-theme.js +1 -1
- package/lib/module/utils/chart.js +44 -0
- package/lib/module/utils/chart.js.map +1 -1
- package/lib/module/utils/color.js +268 -0
- package/lib/module/utils/color.js.map +1 -0
- package/lib/typescript/src/components/card/index.d.ts +20 -0
- package/lib/typescript/src/components/card/index.d.ts.map +1 -1
- package/lib/typescript/src/components/color-picker/index.d.ts +258 -0
- package/lib/typescript/src/components/color-picker/index.d.ts.map +1 -0
- package/lib/typescript/src/components/combobox/index.d.ts.map +1 -1
- package/lib/typescript/src/components/drawer/index.d.ts +13 -1
- package/lib/typescript/src/components/drawer/index.d.ts.map +1 -1
- package/lib/typescript/src/components/kpi-chart/index.d.ts +490 -0
- package/lib/typescript/src/components/kpi-chart/index.d.ts.map +1 -0
- package/lib/typescript/src/components/map/index.d.ts +37 -2
- package/lib/typescript/src/components/map/index.d.ts.map +1 -1
- package/lib/typescript/src/components/menu/index.d.ts.map +1 -1
- package/lib/typescript/src/components/pagination/index.d.ts.map +1 -1
- package/lib/typescript/src/components/panelside/index.d.ts +50 -4
- package/lib/typescript/src/components/panelside/index.d.ts.map +1 -1
- package/lib/typescript/src/components/radar-chart/index.d.ts +214 -0
- package/lib/typescript/src/components/radar-chart/index.d.ts.map +1 -0
- package/lib/typescript/src/components/section-rail/index.d.ts +8 -1
- package/lib/typescript/src/components/section-rail/index.d.ts.map +1 -1
- package/lib/typescript/src/components/slider/index.d.ts +19 -0
- package/lib/typescript/src/components/slider/index.d.ts.map +1 -1
- package/lib/typescript/src/components/tabs/index.d.ts +11 -1
- package/lib/typescript/src/components/tabs/index.d.ts.map +1 -1
- package/lib/typescript/src/components/text-animation/index.d.ts +268 -0
- package/lib/typescript/src/components/text-animation/index.d.ts.map +1 -0
- package/lib/typescript/src/icons/index.d.ts +6 -0
- package/lib/typescript/src/icons/index.d.ts.map +1 -1
- package/lib/typescript/src/index.d.ts +8 -3
- package/lib/typescript/src/index.d.ts.map +1 -1
- package/lib/typescript/src/utils/chart.d.ts +21 -0
- package/lib/typescript/src/utils/chart.d.ts.map +1 -1
- package/lib/typescript/src/utils/color.d.ts +74 -0
- package/lib/typescript/src/utils/color.d.ts.map +1 -0
- package/package.json +2 -1
- package/src/components/card/index.tsx +227 -2
- package/src/components/color-picker/index.tsx +1618 -0
- package/src/components/combobox/index.tsx +89 -19
- package/src/components/drawer/index.tsx +31 -9
- package/src/components/kpi-chart/index.tsx +719 -0
- package/src/components/map/index.tsx +158 -14
- package/src/components/menu/index.tsx +51 -6
- package/src/components/pagination/index.tsx +18 -6
- package/src/components/panelside/index.tsx +134 -19
- package/src/components/radar-chart/index.tsx +910 -0
- package/src/components/section-rail/index.tsx +42 -8
- package/src/components/slider/index.tsx +283 -41
- package/src/components/tabs/index.tsx +263 -8
- package/src/components/text-animation/index.tsx +1093 -0
- package/src/icons/index.tsx +23 -4
- package/src/index.ts +73 -1
- package/src/theme/use-theme.ts +1 -1
- package/src/utils/chart.ts +52 -0
- package/src/utils/color.ts +246 -0
- package/theme.css +125 -112
package/src/icons/index.tsx
CHANGED
|
@@ -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
|
-
|
|
232
|
-
|
|
233
|
-
|
|
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,
|
package/src/theme/use-theme.ts
CHANGED
package/src/utils/chart.ts
CHANGED
|
@@ -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
|
+
}
|