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
@@ -0,0 +1,1395 @@
1
+ "use strict";
2
+
3
+ /**
4
+ * ColorPicker — a colour chosen by dragging, not by typing.
5
+ *
6
+ * The controls are the colour model made visible: a square where saturation
7
+ * runs across and brightness runs up, under a hue picked on its own scale.
8
+ * That is why the component stores hue, saturation, value and alpha rather
9
+ * than the string it hands back — a fully black colour is `#000` whatever hue
10
+ * and saturation produced it, so a picker that stored the output would lose
11
+ * the thumb the moment you dragged into a corner and could not put it back.
12
+ *
13
+ * Nothing about a drag crosses to JavaScript. The four channels are shared
14
+ * values; the hue behind the square, both thumb fills and the preview swatch
15
+ * are `backgroundColor`s computed from them on the UI thread, and the
16
+ * translucency ramp on the alpha track is a gradient used as a mask over a
17
+ * solid fill rather than a gradient whose colours have to be re-declared from
18
+ * React on every frame. The picked colour bridges back on change and again on
19
+ * release, the same as any other input here.
20
+ *
21
+ * ```tsx
22
+ * <ColorPicker defaultValue="#22c55e" onValueCommit={setBrand}>
23
+ * <ColorPicker.Area />
24
+ * <ColorPicker.Hue />
25
+ * <ColorPicker.Preview showValue />
26
+ * </ColorPicker>
27
+ * ```
28
+ *
29
+ * Composition is the API: a picker is whichever of the parts you put in it, so
30
+ * a control with no alpha is one that has no `ColorPicker.Alpha`, not one with
31
+ * a prop turned off.
32
+ *
33
+ * Works controlled (`value` + `onValueChange`) or uncontrolled (`defaultValue`).
34
+ */
35
+ import { createContext, forwardRef, useCallback, useContext, useEffect, useId, useMemo, useRef, useState } from 'react';
36
+ import { Pressable, StyleSheet, View } from 'react-native';
37
+ import MaskedView from '@react-native-masked-view/masked-view';
38
+ import { LinearGradient } from 'expo-linear-gradient';
39
+ import { Gesture, GestureDetector } from 'react-native-gesture-handler';
40
+ import Animated, { runOnJS, useAnimatedReaction, useAnimatedStyle, useSharedValue, withTiming } from 'react-native-reanimated';
41
+ import Svg, { Circle, Defs, Path, Pattern, RadialGradient, Rect, Stop } from 'react-native-svg';
42
+ import { tv } from 'tailwind-variants';
43
+ import { useCSSVariable } from 'uniwind';
44
+ import { useDirectionSign } from "../../hooks/use-direction.js";
45
+ import { Text } from "../../primitives/text.js";
46
+ import { formatColor, hsvToCss, hsvToHex, parseColor } from "../../utils/color.js";
47
+ import { selectionTick } from "../../utils/haptics.js";
48
+
49
+ /** Settles a thumb that was moved by something other than a finger. */
50
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
51
+ const TIMING = {
52
+ duration: 140
53
+ };
54
+
55
+ /**
56
+ * The hue scale, drawn once as a gradient rather than sampled per pixel. Six
57
+ * stops plus the wrap back to red — fewer would bend the ramp, more would not
58
+ * change it, because hue is piecewise linear in RGB and these are the corners.
59
+ */
60
+ const HUE_STOPS = ['#ff0000', '#ffff00', '#00ff00', '#00ffff', '#0000ff', '#ff00ff', '#ff0000'];
61
+
62
+ /** The same ramp for a right-to-left picker, where hue runs the other way. */
63
+ const HUE_STOPS_FLIPPED = [...HUE_STOPS].reverse();
64
+
65
+ /** How far one screen-reader increment moves a channel. */
66
+ const NUDGE = 0.05;
67
+ /** How far one screen-reader increment moves the hue, in degrees. */
68
+ const HUE_NUDGE = 10;
69
+ const colorPickerVariants = tv({
70
+ slots: {
71
+ root: 'w-full gap-3',
72
+ area: 'overflow-hidden rounded-2xl border border-border',
73
+ wheel: 'overflow-hidden rounded-full border border-border',
74
+ // Both thumbs are a white ring around a fill of the colour under them, so
75
+ // the ring reads against the dark end of the square as well as the light.
76
+ // Two points, not three: the ring is there to separate the fill from what
77
+ // is behind it, and any more of it starts hiding the colour it is showing.
78
+ thumb: 'absolute start-0 top-0 rounded-full border-2 border-white shadow-md',
79
+ thumbFill: 'flex-1 rounded-full',
80
+ track: 'w-full overflow-hidden rounded-full border border-border',
81
+ swatches: 'flex-row flex-wrap items-center gap-2',
82
+ swatch: 'items-center justify-center rounded-full',
83
+ swatchRing: 'absolute rounded-full border-2 border-foreground',
84
+ preview: 'flex-row items-center gap-3',
85
+ previewSwatch: 'overflow-hidden rounded-full border border-border',
86
+ previewValue: 'text-sm font-medium text-foreground',
87
+ // The header strip: a name on the leading edge, the value and a swatch on
88
+ // the trailing one.
89
+ field: 'w-full flex-row items-center gap-3 rounded-2xl bg-surface px-4 py-3',
90
+ fieldLabel: 'flex-1 text-base font-medium text-foreground',
91
+ fieldValue: 'text-base font-normal text-muted-foreground',
92
+ fieldSwatch: 'overflow-hidden rounded-full border border-border',
93
+ // The readout above a track — the number on one side, what it names on the
94
+ // other.
95
+ channel: 'w-full flex-row items-center justify-between gap-3',
96
+ channelValue: 'text-base font-normal text-muted-foreground',
97
+ channelLabel: 'text-base font-medium text-foreground'
98
+ },
99
+ variants: {
100
+ size: {
101
+ sm: {},
102
+ md: {},
103
+ lg: {}
104
+ },
105
+ disabled: {
106
+ true: {
107
+ root: 'opacity-50'
108
+ }
109
+ }
110
+ },
111
+ defaultVariants: {
112
+ size: 'md'
113
+ }
114
+ });
115
+ /** Height of the saturation/brightness square, per size. */
116
+ const AREA_HEIGHT = {
117
+ sm: 140,
118
+ md: 180,
119
+ lg: 220
120
+ };
121
+ /**
122
+ * Height of a channel track, per size. Tall enough that the knob sits *in* the
123
+ * track rather than on top of a hairline — a slider whose thumb overhangs a
124
+ * thin rule reads as a control laid over a decoration.
125
+ */
126
+ const TRACK_HEIGHT = {
127
+ sm: 16,
128
+ md: 20,
129
+ lg: 24
130
+ };
131
+ /** Thumb diameter on a channel track — wider than the track, so it reads as a knob. */
132
+ const TRACK_THUMB = {
133
+ sm: 22,
134
+ md: 26,
135
+ lg: 30
136
+ };
137
+ /** Thumb diameter on the square. */
138
+ const AREA_THUMB = {
139
+ sm: 20,
140
+ md: 24,
141
+ lg: 28
142
+ };
143
+ /** Preset swatch diameter. */
144
+ const SWATCH = {
145
+ sm: 26,
146
+ md: 30,
147
+ lg: 34
148
+ };
149
+ /** The swatch beside the value in `Preview`. */
150
+ const PREVIEW_SWATCH = {
151
+ sm: 28,
152
+ md: 36,
153
+ lg: 44
154
+ };
155
+ /** The smaller swatch on the end of the `Field` strip. */
156
+ const FIELD_SWATCH = {
157
+ sm: 22,
158
+ md: 28,
159
+ lg: 34
160
+ };
161
+ /** Diameter of the wheel, per size. */
162
+ const WHEEL_SIZE = {
163
+ sm: 180,
164
+ md: 240,
165
+ lg: 300
166
+ };
167
+ const ColorPickerContext = /*#__PURE__*/createContext(null);
168
+ function useColorPicker(part) {
169
+ const ctx = useContext(ColorPickerContext);
170
+ if (!ctx) throw new Error(`${part} must be used inside <ColorPicker>.`);
171
+ return ctx;
172
+ }
173
+
174
+ /** Where a picker starts when it is handed a value it cannot read. */
175
+ const FALLBACK = {
176
+ h: 0,
177
+ s: 1,
178
+ v: 1,
179
+ a: 1
180
+ };
181
+ const ColorPickerRoot = /*#__PURE__*/forwardRef(({
182
+ className,
183
+ value: valueProp,
184
+ defaultValue = '#ff0000',
185
+ onValueChange,
186
+ onValueCommit,
187
+ format = 'hex',
188
+ disabled = false,
189
+ haptics = false,
190
+ size = 'md',
191
+ children
192
+ }, ref) => {
193
+ const slots = colorPickerVariants({
194
+ size,
195
+ disabled
196
+ });
197
+
198
+ // Parsed once. After this the channels are the truth and the prop is only
199
+ // consulted when it changes to something we did not ourselves emit.
200
+ const initial = useMemo(() => parseColor(valueProp ?? defaultValue) ?? FALLBACK,
201
+ // eslint-disable-next-line react-hooks/exhaustive-deps
202
+ []);
203
+ const hue = useSharedValue(initial.h);
204
+ const saturation = useSharedValue(initial.s);
205
+ const brightness = useSharedValue(initial.v);
206
+ const opacity = useSharedValue(initial.a);
207
+ const changeRef = useRef(onValueChange);
208
+ changeRef.current = onValueChange;
209
+ const commitRef = useRef(onValueCommit);
210
+ commitRef.current = onValueCommit;
211
+ const formatRef = useRef(format);
212
+ formatRef.current = format;
213
+
214
+ /*
215
+ * The last string this picker produced. A controlled parent hands it
216
+ * straight back, and re-parsing it into the channels mid-drag would fight
217
+ * the finger: the round trip is lossy at the edges of the square, so the
218
+ * thumb would be pulled a pixel back on every frame.
219
+ */
220
+ const lastEmitted = useRef(null);
221
+ const emit = useCallback((h, s, v, a, commit) => {
222
+ const next = formatColor({
223
+ h,
224
+ s,
225
+ v,
226
+ a
227
+ }, formatRef.current);
228
+ lastEmitted.current = next;
229
+ changeRef.current?.(next);
230
+ if (commit) commitRef.current?.(next);
231
+ }, []);
232
+
233
+ // Keep the channels in step with a controlled value changed elsewhere. Set
234
+ // rather than animated: an external change is not a gesture, and springing
235
+ // the hue from 350° to 10° would run the long way round the wheel.
236
+ useEffect(() => {
237
+ if (valueProp === undefined) return;
238
+ if (valueProp === lastEmitted.current) return;
239
+ const parsed = parseColor(valueProp);
240
+ if (!parsed) return;
241
+ hue.value = parsed.h;
242
+ saturation.value = parsed.s;
243
+ brightness.value = parsed.v;
244
+ opacity.value = parsed.a;
245
+ }, [valueProp, hue, saturation, brightness, opacity]);
246
+ const context = useMemo(() => ({
247
+ hue,
248
+ saturation,
249
+ brightness,
250
+ opacity,
251
+ emit,
252
+ format,
253
+ disabled,
254
+ haptics,
255
+ size: size ?? 'md'
256
+ }), [hue, saturation, brightness, opacity, emit, format, disabled, haptics, size]);
257
+ return /*#__PURE__*/_jsx(ColorPickerContext.Provider, {
258
+ value: context,
259
+ children: /*#__PURE__*/_jsx(View, {
260
+ ref: ref,
261
+ className: slots.root({
262
+ className
263
+ }),
264
+ collapsable: false,
265
+ children: children
266
+ })
267
+ });
268
+ });
269
+ ColorPickerRoot.displayName = 'ColorPicker';
270
+
271
+ /* ------------------------------------------------------------------ *
272
+ * The checkerboard behind anything translucent.
273
+ * ------------------------------------------------------------------ */
274
+
275
+ /**
276
+ * The "nothing here" backing. A tiled SVG pattern rather than a grid of views:
277
+ * a 340-point track at eight-point squares is 170 squares, and every one of
278
+ * them would be a view laid out by Yoga on the frame the picker mounts.
279
+ */
280
+ function Checkerboard({
281
+ square = 7
282
+ }) {
283
+ const id = useId().replace(/[^a-zA-Z0-9]/g, '');
284
+ // The pattern is themed rather than the usual grey-on-white: a checkerboard
285
+ // is a statement that there is nothing there, and "nothing" is the page.
286
+ const light = String(useCSSVariable('--color-background') ?? '#ffffff');
287
+ const dark = String(useCSSVariable('--color-muted') ?? '#e5e5e5');
288
+ return /*#__PURE__*/_jsxs(Svg, {
289
+ style: StyleSheet.absoluteFill,
290
+ pointerEvents: "none",
291
+ children: [/*#__PURE__*/_jsx(Defs, {
292
+ children: /*#__PURE__*/_jsxs(Pattern, {
293
+ id: id,
294
+ width: square * 2,
295
+ height: square * 2,
296
+ patternUnits: "userSpaceOnUse",
297
+ children: [/*#__PURE__*/_jsx(Rect, {
298
+ width: square * 2,
299
+ height: square * 2,
300
+ fill: light
301
+ }), /*#__PURE__*/_jsx(Rect, {
302
+ width: square,
303
+ height: square,
304
+ fill: dark
305
+ }), /*#__PURE__*/_jsx(Rect, {
306
+ x: square,
307
+ y: square,
308
+ width: square,
309
+ height: square,
310
+ fill: dark
311
+ })]
312
+ })
313
+ }), /*#__PURE__*/_jsx(Rect, {
314
+ width: "100%",
315
+ height: "100%",
316
+ fill: `url(#${id})`
317
+ })]
318
+ });
319
+ }
320
+
321
+ /* ------------------------------------------------------------------ *
322
+ * Area — saturation across, brightness up.
323
+ * ------------------------------------------------------------------ */
324
+
325
+ const ColorPickerArea = /*#__PURE__*/forwardRef(({
326
+ className,
327
+ height,
328
+ thumbClassName
329
+ }, ref) => {
330
+ const ctx = useColorPicker('ColorPicker.Area');
331
+ const slots = colorPickerVariants({
332
+ size: ctx.size
333
+ });
334
+ const sign = useDirectionSign();
335
+ const boxHeight = height ?? AREA_HEIGHT[ctx.size];
336
+ const thumbSize = AREA_THUMB[ctx.size];
337
+ const width = useSharedValue(0);
338
+
339
+ /*
340
+ * The screen reader's copy of the value. Updated when a drag ends rather
341
+ * than while it runs: a `now` that re-rendered on every frame would be a
342
+ * hundred announcements for one gesture, and nobody is listening to the
343
+ * middle of a drag.
344
+ */
345
+ const [announced, setAnnounced] = useState(() => ({
346
+ s: ctx.saturation.value,
347
+ v: ctx.brightness.value
348
+ }));
349
+ const settle = useCallback((s, v) => {
350
+ setAnnounced({
351
+ s,
352
+ v
353
+ });
354
+ }, []);
355
+ const apply = (x, y, commit) => {
356
+ 'worklet';
357
+
358
+ const w = Math.max(width.value, 1);
359
+ // `x` is measured from the physical left edge either way, so a
360
+ // right-to-left picker counts saturation back from the far side.
361
+ const along = sign === 1 ? x : w - x;
362
+ const s = Math.min(Math.max(along / w, 0), 1);
363
+ const v = 1 - Math.min(Math.max(y / boxHeight, 0), 1);
364
+ ctx.saturation.value = s;
365
+ ctx.brightness.value = v;
366
+ runOnJS(ctx.emit)(ctx.hue.value, s, v, ctx.opacity.value, commit);
367
+ if (commit) {
368
+ runOnJS(settle)(s, v);
369
+ if (ctx.haptics) runOnJS(selectionTick)();
370
+ }
371
+ };
372
+
373
+ // Both axes belong to the square, so it takes the gesture on touch-down
374
+ // rather than after a threshold — a pad that needed ten points of travel
375
+ // before it responded would feel broken.
376
+ const pan = Gesture.Pan().enabled(!ctx.disabled).minDistance(0).shouldCancelWhenOutside(false).onBegin(event => apply(event.x, event.y, false)).onUpdate(event => apply(event.x, event.y, false)).onFinalize(event => apply(event.x, event.y, true));
377
+ const hueStyle = useAnimatedStyle(() => ({
378
+ backgroundColor: hsvToCss(ctx.hue.value, 1, 1, 1)
379
+ }));
380
+ const thumbStyle = useAnimatedStyle(() => {
381
+ // A transform is not laid out, so Yoga does not mirror it — the thumb
382
+ // has to travel the other way itself.
383
+ const along = ctx.saturation.value * width.value - thumbSize / 2;
384
+ return {
385
+ transform: [{
386
+ translateX: along * sign
387
+ }, {
388
+ translateY: (1 - ctx.brightness.value) * boxHeight - thumbSize / 2
389
+ }]
390
+ };
391
+ });
392
+ const thumbFillStyle = useAnimatedStyle(() => ({
393
+ backgroundColor: hsvToCss(ctx.hue.value, ctx.saturation.value, ctx.brightness.value, 1)
394
+ }));
395
+ const nudge = (channel, dir) => {
396
+ const next = Math.min(Math.max(ctx[channel].value + dir * NUDGE, 0), 1);
397
+ ctx[channel].value = withTiming(next, TIMING);
398
+ const s = channel === 'saturation' ? next : ctx.saturation.value;
399
+ const v = channel === 'brightness' ? next : ctx.brightness.value;
400
+ settle(s, v);
401
+ ctx.emit(ctx.hue.value, s, v, ctx.opacity.value, true);
402
+ };
403
+ const onAccessibilityAction = event => {
404
+ const action = event.nativeEvent.actionName;
405
+ if (action === 'increment') nudge('saturation', 1);else if (action === 'decrement') nudge('saturation', -1);else if (action === 'brighter') nudge('brightness', 1);else if (action === 'darker') nudge('brightness', -1);
406
+ };
407
+ return /*#__PURE__*/_jsx(GestureDetector, {
408
+ gesture: pan,
409
+ children: /*#__PURE__*/_jsxs(View, {
410
+ ref: ref,
411
+ style: {
412
+ height: boxHeight
413
+ },
414
+ className: "w-full",
415
+ onLayout: event => {
416
+ width.value = event.nativeEvent.layout.width;
417
+ },
418
+ accessible: true,
419
+ accessibilityRole: "adjustable",
420
+ accessibilityLabel: "Saturation and brightness",
421
+ accessibilityState: {
422
+ disabled: ctx.disabled
423
+ },
424
+ accessibilityValue: {
425
+ min: 0,
426
+ max: 100,
427
+ now: Math.round(announced.s * 100),
428
+ text: `${Math.round(announced.s * 100)}% saturation, ${Math.round(announced.v * 100)}% brightness`
429
+ },
430
+ accessibilityActions: [{
431
+ name: 'increment'
432
+ }, {
433
+ name: 'decrement'
434
+ }, {
435
+ name: 'brighter',
436
+ label: 'Brighter'
437
+ }, {
438
+ name: 'darker',
439
+ label: 'Darker'
440
+ }],
441
+ onAccessibilityAction: onAccessibilityAction,
442
+ children: [/*#__PURE__*/_jsxs(View, {
443
+ style: StyleSheet.absoluteFill,
444
+ className: slots.area({
445
+ className
446
+ }),
447
+ children: [/*#__PURE__*/_jsx(Animated.View, {
448
+ style: [StyleSheet.absoluteFill, hueStyle]
449
+ }), /*#__PURE__*/_jsx(LinearGradient, {
450
+ colors: ['rgba(255,255,255,1)', 'rgba(255,255,255,0)'],
451
+ start: {
452
+ x: sign === 1 ? 0 : 1,
453
+ y: 0.5
454
+ },
455
+ end: {
456
+ x: sign === 1 ? 1 : 0,
457
+ y: 0.5
458
+ },
459
+ style: StyleSheet.absoluteFill
460
+ }), /*#__PURE__*/_jsx(LinearGradient, {
461
+ colors: ['rgba(0,0,0,0)', 'rgba(0,0,0,1)'],
462
+ start: {
463
+ x: 0.5,
464
+ y: 0
465
+ },
466
+ end: {
467
+ x: 0.5,
468
+ y: 1
469
+ },
470
+ style: StyleSheet.absoluteFill
471
+ })]
472
+ }), /*#__PURE__*/_jsx(Animated.View, {
473
+ pointerEvents: "none",
474
+ style: [thumbStyle, {
475
+ width: thumbSize,
476
+ height: thumbSize
477
+ }],
478
+ className: slots.thumb({
479
+ className: thumbClassName
480
+ }),
481
+ children: /*#__PURE__*/_jsx(Animated.View, {
482
+ style: thumbFillStyle,
483
+ className: slots.thumbFill()
484
+ })
485
+ })]
486
+ })
487
+ });
488
+ });
489
+ ColorPickerArea.displayName = 'ColorPicker.Area';
490
+
491
+ /* ------------------------------------------------------------------ *
492
+ * The two channel tracks share everything but what is drawn in them.
493
+ * ------------------------------------------------------------------ */
494
+
495
+ function ChannelTrack({
496
+ children,
497
+ gesture,
498
+ thumbStyle,
499
+ thumbFillStyle,
500
+ onLayout,
501
+ size,
502
+ className,
503
+ thumbClassName,
504
+ label,
505
+ disabled,
506
+ accessibilityValue,
507
+ onAccessibilityAction,
508
+ innerRef
509
+ }) {
510
+ const slots = colorPickerVariants({
511
+ size
512
+ });
513
+ const trackHeight = TRACK_HEIGHT[size];
514
+ const thumbSize = TRACK_THUMB[size];
515
+ return /*#__PURE__*/_jsx(GestureDetector, {
516
+ gesture: gesture,
517
+ children: /*#__PURE__*/_jsxs(View, {
518
+ ref: innerRef,
519
+ style: {
520
+ height: thumbSize
521
+ },
522
+ className: "w-full justify-center",
523
+ onLayout: onLayout,
524
+ accessible: true,
525
+ accessibilityRole: "adjustable",
526
+ accessibilityLabel: label,
527
+ accessibilityState: {
528
+ disabled
529
+ },
530
+ accessibilityValue: accessibilityValue,
531
+ accessibilityActions: [{
532
+ name: 'increment'
533
+ }, {
534
+ name: 'decrement'
535
+ }],
536
+ onAccessibilityAction: onAccessibilityAction,
537
+ children: [/*#__PURE__*/_jsx(View, {
538
+ style: {
539
+ height: trackHeight
540
+ },
541
+ className: slots.track({
542
+ className
543
+ }),
544
+ children: children
545
+ }), /*#__PURE__*/_jsx(Animated.View, {
546
+ pointerEvents: "none",
547
+ style: [thumbStyle, {
548
+ width: thumbSize,
549
+ height: thumbSize
550
+ }],
551
+ className: slots.thumb({
552
+ className: thumbClassName
553
+ }),
554
+ children: /*#__PURE__*/_jsx(Animated.View, {
555
+ style: thumbFillStyle,
556
+ className: slots.thumbFill()
557
+ })
558
+ })]
559
+ })
560
+ });
561
+ }
562
+
563
+ /**
564
+ * Maps a touch anywhere on a track to a fraction of it, accounting for the
565
+ * thumb's own width: the thumb travels between the two edges rather than off
566
+ * them, so the usable run is the track minus one thumb.
567
+ */
568
+ function trackFraction(x, width, thumbSize, sign) {
569
+ 'worklet';
570
+
571
+ const along = sign === 1 ? x : width - x;
572
+ const travel = Math.max(width - thumbSize, 1);
573
+ return Math.min(Math.max((along - thumbSize / 2) / travel, 0), 1);
574
+ }
575
+
576
+ /* ------------------------------------------------------------------ *
577
+ * Hue.
578
+ * ------------------------------------------------------------------ */
579
+
580
+ const ColorPickerHue = /*#__PURE__*/forwardRef(({
581
+ className,
582
+ thumbClassName
583
+ }, ref) => {
584
+ const ctx = useColorPicker('ColorPicker.Hue');
585
+ const sign = useDirectionSign();
586
+ const thumbSize = TRACK_THUMB[ctx.size];
587
+ const width = useSharedValue(0);
588
+ const [announced, setAnnounced] = useState(() => Math.round(ctx.hue.value));
589
+ const settle = useCallback(h => setAnnounced(Math.round(h)), []);
590
+ const apply = (x, commit) => {
591
+ 'worklet';
592
+
593
+ const h = trackFraction(x, width.value, thumbSize, sign) * 360;
594
+ ctx.hue.value = h;
595
+ runOnJS(ctx.emit)(h, ctx.saturation.value, ctx.brightness.value, ctx.opacity.value, commit);
596
+ if (commit) {
597
+ runOnJS(settle)(h);
598
+ if (ctx.haptics) runOnJS(selectionTick)();
599
+ }
600
+ };
601
+
602
+ /*
603
+ * A thin horizontal control inside a scroller: the pan waits for six points
604
+ * of sideways travel so a vertical flick starting on the track still
605
+ * scrolls the page, and the tap handles the case where there was no travel
606
+ * at all.
607
+ */
608
+ const pan = Gesture.Pan().enabled(!ctx.disabled).activeOffsetX([-6, 6]).shouldCancelWhenOutside(false).onUpdate(event => apply(event.x, false)).onFinalize(event => apply(event.x, true));
609
+ const tap = Gesture.Tap().enabled(!ctx.disabled).maxDuration(250).onEnd(event => apply(event.x, true));
610
+ const gesture = Gesture.Race(pan, tap);
611
+ const thumbStyle = useAnimatedStyle(() => {
612
+ const travel = Math.max(width.value - thumbSize, 0);
613
+ return {
614
+ transform: [{
615
+ translateX: ctx.hue.value / 360 * travel * sign
616
+ }]
617
+ };
618
+ });
619
+ const thumbFillStyle = useAnimatedStyle(() => ({
620
+ backgroundColor: hsvToCss(ctx.hue.value, 1, 1, 1)
621
+ }));
622
+ const nudge = dir => {
623
+ const next = Math.min(Math.max(ctx.hue.value + dir * HUE_NUDGE, 0), 360);
624
+ ctx.hue.value = withTiming(next, TIMING);
625
+ settle(next);
626
+ ctx.emit(next, ctx.saturation.value, ctx.brightness.value, ctx.opacity.value, true);
627
+ };
628
+ return /*#__PURE__*/_jsx(ChannelTrack, {
629
+ innerRef: ref,
630
+ gesture: gesture,
631
+ thumbStyle: thumbStyle,
632
+ thumbFillStyle: thumbFillStyle,
633
+ onLayout: event => {
634
+ width.value = event.nativeEvent.layout.width;
635
+ },
636
+ size: ctx.size,
637
+ className: className,
638
+ thumbClassName: thumbClassName,
639
+ label: "Hue",
640
+ disabled: ctx.disabled,
641
+ accessibilityValue: {
642
+ min: 0,
643
+ max: 360,
644
+ now: announced,
645
+ text: `${announced}°`
646
+ },
647
+ onAccessibilityAction: event => {
648
+ if (event.nativeEvent.actionName === 'increment') nudge(1);else if (event.nativeEvent.actionName === 'decrement') nudge(-1);
649
+ },
650
+ children: /*#__PURE__*/_jsx(LinearGradient, {
651
+ colors: sign === 1 ? HUE_STOPS : HUE_STOPS_FLIPPED,
652
+ start: {
653
+ x: 0,
654
+ y: 0.5
655
+ },
656
+ end: {
657
+ x: 1,
658
+ y: 0.5
659
+ },
660
+ style: StyleSheet.absoluteFill
661
+ })
662
+ });
663
+ });
664
+ ColorPickerHue.displayName = 'ColorPicker.Hue';
665
+
666
+ /* ------------------------------------------------------------------ *
667
+ * Alpha.
668
+ * ------------------------------------------------------------------ */
669
+
670
+ const ColorPickerAlpha = /*#__PURE__*/forwardRef(({
671
+ className,
672
+ thumbClassName
673
+ }, ref) => {
674
+ const ctx = useColorPicker('ColorPicker.Alpha');
675
+ const sign = useDirectionSign();
676
+ const thumbSize = TRACK_THUMB[ctx.size];
677
+ const width = useSharedValue(0);
678
+ const [announced, setAnnounced] = useState(() => Math.round(ctx.opacity.value * 100));
679
+ const settle = useCallback(a => setAnnounced(Math.round(a * 100)), []);
680
+ const apply = (x, commit) => {
681
+ 'worklet';
682
+
683
+ const a = trackFraction(x, width.value, thumbSize, sign);
684
+ ctx.opacity.value = a;
685
+ runOnJS(ctx.emit)(ctx.hue.value, ctx.saturation.value, ctx.brightness.value, a, commit);
686
+ if (commit) {
687
+ runOnJS(settle)(a);
688
+ if (ctx.haptics) runOnJS(selectionTick)();
689
+ }
690
+ };
691
+ const pan = Gesture.Pan().enabled(!ctx.disabled).activeOffsetX([-6, 6]).shouldCancelWhenOutside(false).onUpdate(event => apply(event.x, false)).onFinalize(event => apply(event.x, true));
692
+ const tap = Gesture.Tap().enabled(!ctx.disabled).maxDuration(250).onEnd(event => apply(event.x, true));
693
+ const gesture = Gesture.Race(pan, tap);
694
+ const thumbStyle = useAnimatedStyle(() => {
695
+ const travel = Math.max(width.value - thumbSize, 0);
696
+ return {
697
+ transform: [{
698
+ translateX: ctx.opacity.value * travel * sign
699
+ }]
700
+ };
701
+ });
702
+ const solidStyle = useAnimatedStyle(() => ({
703
+ backgroundColor: hsvToCss(ctx.hue.value, ctx.saturation.value, ctx.brightness.value, 1)
704
+ }));
705
+ const thumbFillStyle = useAnimatedStyle(() => ({
706
+ backgroundColor: hsvToCss(ctx.hue.value, ctx.saturation.value, ctx.brightness.value, ctx.opacity.value)
707
+ }));
708
+ const nudge = dir => {
709
+ const next = Math.min(Math.max(ctx.opacity.value + dir * NUDGE, 0), 1);
710
+ ctx.opacity.value = withTiming(next, TIMING);
711
+ settle(next);
712
+ ctx.emit(ctx.hue.value, ctx.saturation.value, ctx.brightness.value, next, true);
713
+ };
714
+ return /*#__PURE__*/_jsxs(ChannelTrack, {
715
+ innerRef: ref,
716
+ gesture: gesture,
717
+ thumbStyle: thumbStyle,
718
+ thumbFillStyle: thumbFillStyle,
719
+ onLayout: event => {
720
+ width.value = event.nativeEvent.layout.width;
721
+ },
722
+ size: ctx.size,
723
+ className: className,
724
+ thumbClassName: thumbClassName,
725
+ label: "Opacity",
726
+ disabled: ctx.disabled,
727
+ accessibilityValue: {
728
+ min: 0,
729
+ max: 100,
730
+ now: announced,
731
+ text: `${announced}%`
732
+ },
733
+ onAccessibilityAction: event => {
734
+ if (event.nativeEvent.actionName === 'increment') nudge(1);else if (event.nativeEvent.actionName === 'decrement') nudge(-1);
735
+ },
736
+ children: [/*#__PURE__*/_jsx(Checkerboard, {}), /*#__PURE__*/_jsx(MaskedView, {
737
+ style: StyleSheet.absoluteFill,
738
+ pointerEvents: "none",
739
+ maskElement: /*#__PURE__*/_jsx(LinearGradient, {
740
+ colors: ['rgba(0,0,0,0)', 'rgba(0,0,0,1)'],
741
+ start: {
742
+ x: sign === 1 ? 0 : 1,
743
+ y: 0.5
744
+ },
745
+ end: {
746
+ x: sign === 1 ? 1 : 0,
747
+ y: 0.5
748
+ },
749
+ style: StyleSheet.absoluteFill
750
+ }),
751
+ children: /*#__PURE__*/_jsx(Animated.View, {
752
+ style: [StyleSheet.absoluteFill, solidStyle]
753
+ })
754
+ })]
755
+ });
756
+ });
757
+ ColorPickerAlpha.displayName = 'ColorPicker.Alpha';
758
+
759
+ /* ------------------------------------------------------------------ *
760
+ * Preview.
761
+ * ------------------------------------------------------------------ */
762
+
763
+ const ColorPickerPreview = /*#__PURE__*/forwardRef(({
764
+ className,
765
+ showValue = false,
766
+ swatchClassName,
767
+ valueClassName,
768
+ children
769
+ }, ref) => {
770
+ const ctx = useColorPicker('ColorPicker.Preview');
771
+ const slots = colorPickerVariants({
772
+ size: ctx.size
773
+ });
774
+ const diameter = PREVIEW_SWATCH[ctx.size];
775
+ const [printed, setPrinted] = useState(() => formatColor({
776
+ h: ctx.hue.value,
777
+ s: ctx.saturation.value,
778
+ v: ctx.brightness.value,
779
+ a: ctx.opacity.value
780
+ }, ctx.format));
781
+ const format = ctx.format;
782
+ useAnimatedReaction(() => formatColor({
783
+ h: ctx.hue.value,
784
+ s: ctx.saturation.value,
785
+ v: ctx.brightness.value,
786
+ a: ctx.opacity.value
787
+ }, format), (current, previous) => {
788
+ if (current !== previous) runOnJS(setPrinted)(current);
789
+ }, [format]);
790
+ const fillStyle = useAnimatedStyle(() => ({
791
+ backgroundColor: hsvToCss(ctx.hue.value, ctx.saturation.value, ctx.brightness.value, ctx.opacity.value)
792
+ }));
793
+ return /*#__PURE__*/_jsxs(View, {
794
+ ref: ref,
795
+ className: slots.preview({
796
+ className
797
+ }),
798
+ children: [/*#__PURE__*/_jsxs(View, {
799
+ style: {
800
+ width: diameter,
801
+ height: diameter
802
+ },
803
+ className: slots.previewSwatch({
804
+ className: swatchClassName
805
+ }),
806
+ children: [/*#__PURE__*/_jsx(Checkerboard, {
807
+ square: 5
808
+ }), /*#__PURE__*/_jsx(Animated.View, {
809
+ style: [StyleSheet.absoluteFill, fillStyle]
810
+ })]
811
+ }), showValue ? /*#__PURE__*/_jsx(Text, {
812
+ className: slots.previewValue({
813
+ className: valueClassName
814
+ }),
815
+ children: printed
816
+ }) : null, children]
817
+ });
818
+ });
819
+ ColorPickerPreview.displayName = 'ColorPicker.Preview';
820
+
821
+ /* ------------------------------------------------------------------ *
822
+ * Swatches.
823
+ * ------------------------------------------------------------------ */
824
+
825
+ const ColorPickerSwatches = /*#__PURE__*/forwardRef(({
826
+ className,
827
+ colors,
828
+ swatchSize,
829
+ swatchClassName
830
+ }, ref) => {
831
+ const ctx = useColorPicker('ColorPicker.Swatches');
832
+ const slots = colorPickerVariants({
833
+ size: ctx.size
834
+ });
835
+ const diameter = swatchSize ?? SWATCH[ctx.size];
836
+
837
+ // Unreadable strings are dropped here rather than rendered as black: a row
838
+ // of presets with a mystery black one in it is worse than a shorter row.
839
+ const parsed = useMemo(() => colors.map(raw => ({
840
+ raw,
841
+ color: parseColor(raw)
842
+ })).filter(entry => entry.color !== null), [colors]);
843
+ return /*#__PURE__*/_jsx(View, {
844
+ ref: ref,
845
+ className: slots.swatches({
846
+ className
847
+ }),
848
+ children: parsed.map((entry, index) => /*#__PURE__*/_jsx(Swatch, {
849
+ color: entry.color,
850
+ diameter: diameter,
851
+ className: swatchClassName,
852
+ ctx: ctx,
853
+ slots: slots
854
+ }, `${entry.raw}-${index}`))
855
+ });
856
+ });
857
+ ColorPickerSwatches.displayName = 'ColorPicker.Swatches';
858
+ function Swatch({
859
+ color,
860
+ diameter,
861
+ className,
862
+ ctx,
863
+ slots
864
+ }) {
865
+ const hex = hsvToHex(color.h, color.s, color.v, color.a);
866
+
867
+ /*
868
+ * Selected is an exact match on the written colour rather than a distance
869
+ * between two sets of channels. A near-black has an arbitrary hue and a
870
+ * meaningless saturation, so a per-channel comparison marks two visually
871
+ * identical blacks as different and two different hues of black as the same.
872
+ */
873
+ const ringStyle = useAnimatedStyle(() => {
874
+ const current = hsvToHex(ctx.hue.value, ctx.saturation.value, ctx.brightness.value, ctx.opacity.value);
875
+ return {
876
+ opacity: withTiming(current === hex ? 1 : 0, {
877
+ duration: 120
878
+ })
879
+ };
880
+ });
881
+ const onPress = () => {
882
+ ctx.hue.value = color.h;
883
+ ctx.saturation.value = color.s;
884
+ ctx.brightness.value = color.v;
885
+ ctx.opacity.value = color.a;
886
+ ctx.emit(color.h, color.s, color.v, color.a, true);
887
+ if (ctx.haptics) selectionTick();
888
+ };
889
+ return /*#__PURE__*/_jsxs(Pressable, {
890
+ onPress: onPress,
891
+ disabled: ctx.disabled,
892
+ accessibilityRole: "button",
893
+ accessibilityLabel: hex,
894
+ style: {
895
+ width: diameter,
896
+ height: diameter
897
+ },
898
+ className: slots.swatch({
899
+ className
900
+ }),
901
+ children: [/*#__PURE__*/_jsxs(View, {
902
+ style: {
903
+ width: diameter,
904
+ height: diameter
905
+ },
906
+ className: "absolute overflow-hidden rounded-full",
907
+ children: [color.a < 1 ? /*#__PURE__*/_jsx(Checkerboard, {
908
+ square: 4
909
+ }) : null, /*#__PURE__*/_jsx(View, {
910
+ style: [StyleSheet.absoluteFill, {
911
+ backgroundColor: hex
912
+ }]
913
+ })]
914
+ }), /*#__PURE__*/_jsx(Animated.View, {
915
+ pointerEvents: "none",
916
+ style: [ringStyle, {
917
+ width: diameter + 6,
918
+ height: diameter + 6
919
+ }],
920
+ className: slots.swatchRing()
921
+ })]
922
+ });
923
+ }
924
+
925
+ /* ------------------------------------------------------------------ *
926
+ * Field — the strip above the controls: what is being picked, and what
927
+ * it currently is.
928
+ * ------------------------------------------------------------------ */
929
+
930
+ /**
931
+ * A header for the picker below it.
932
+ *
933
+ * The value it prints is built on the UI thread and only crosses to
934
+ * JavaScript when the rounded string actually changes, the same as `Preview`.
935
+ * A drag through a hundred frames of the same hex costs one render.
936
+ */
937
+ const ColorPickerField = /*#__PURE__*/forwardRef(({
938
+ className,
939
+ label,
940
+ showValue = true,
941
+ swatchClassName,
942
+ children
943
+ }, ref) => {
944
+ const ctx = useColorPicker('ColorPicker.Field');
945
+ const slots = colorPickerVariants({
946
+ size: ctx.size
947
+ });
948
+ const diameter = FIELD_SWATCH[ctx.size];
949
+ const read = useCallback(() => formatColor({
950
+ h: ctx.hue.value,
951
+ s: ctx.saturation.value,
952
+ v: ctx.brightness.value,
953
+ a: ctx.opacity.value
954
+ }, ctx.format),
955
+ // Shared values are stable objects; `format` is the only real dependency.
956
+ // eslint-disable-next-line react-hooks/exhaustive-deps
957
+ [ctx.format]);
958
+ const [printed, setPrinted] = useState(read);
959
+ const format = ctx.format;
960
+ useAnimatedReaction(() => formatColor({
961
+ h: ctx.hue.value,
962
+ s: ctx.saturation.value,
963
+ v: ctx.brightness.value,
964
+ a: ctx.opacity.value
965
+ }, format), (current, previous) => {
966
+ if (current !== previous) runOnJS(setPrinted)(current);
967
+ }, [format]);
968
+ const fillStyle = useAnimatedStyle(() => ({
969
+ backgroundColor: hsvToCss(ctx.hue.value, ctx.saturation.value, ctx.brightness.value, ctx.opacity.value)
970
+ }));
971
+ return /*#__PURE__*/_jsxs(View, {
972
+ ref: ref,
973
+ className: slots.field({
974
+ className
975
+ })
976
+ // One thing being read out, not three: a label, a value and a swatch
977
+ // announced separately are three stops that each say a third of it.
978
+ ,
979
+ accessible: true,
980
+ accessibilityLabel: label ? `${label}, ${printed}` : printed,
981
+ children: [label ? /*#__PURE__*/_jsx(Text, {
982
+ className: slots.fieldLabel(),
983
+ children: label
984
+ }) : null, showValue ? /*#__PURE__*/_jsx(Text, {
985
+ className: slots.fieldValue(),
986
+ children: printed.toUpperCase()
987
+ }) : null, /*#__PURE__*/_jsxs(View, {
988
+ style: {
989
+ width: diameter,
990
+ height: diameter
991
+ },
992
+ className: slots.fieldSwatch({
993
+ className: swatchClassName
994
+ }),
995
+ children: [/*#__PURE__*/_jsx(Checkerboard, {
996
+ square: 4
997
+ }), /*#__PURE__*/_jsx(Animated.View, {
998
+ style: [StyleSheet.absoluteFill, fillStyle]
999
+ })]
1000
+ }), children]
1001
+ });
1002
+ });
1003
+ ColorPickerField.displayName = 'ColorPicker.Field';
1004
+
1005
+ /* ------------------------------------------------------------------ *
1006
+ * Channel — the readout that names the track under it.
1007
+ * ------------------------------------------------------------------ */
1008
+
1009
+ /** Which channel a readout is reading. */
1010
+
1011
+ /** What each channel is called, and how its number is written. */
1012
+ const CHANNEL_LABEL = {
1013
+ hue: 'Hue',
1014
+ saturation: 'Saturation',
1015
+ brightness: 'Brightness',
1016
+ alpha: 'Opacity'
1017
+ };
1018
+ /**
1019
+ * The line above a track: the number on the leading edge, what it names on the
1020
+ * trailing one.
1021
+ *
1022
+ * The number is the one thing in this component that has to reach JavaScript
1023
+ * during a drag, because text cannot be written from a worklet. It is rounded
1024
+ * first and only sent when the rounded value changes, so a slow drag across a
1025
+ * hue track sends 360 updates rather than one per frame.
1026
+ */
1027
+ const ColorPickerChannelReadout = /*#__PURE__*/forwardRef(({
1028
+ className,
1029
+ channel,
1030
+ label,
1031
+ format
1032
+ }, ref) => {
1033
+ const ctx = useColorPicker('ColorPicker.Channel');
1034
+ const slots = colorPickerVariants({
1035
+ size: ctx.size
1036
+ });
1037
+ const source = channel === 'hue' ? ctx.hue : channel === 'saturation' ? ctx.saturation : channel === 'brightness' ? ctx.brightness : ctx.opacity;
1038
+ const [value, setValue] = useState(() => Math.round(channel === 'hue' ? source.value : source.value * 100));
1039
+ useAnimatedReaction(() => Math.round(channel === 'hue' ? source.value : source.value * 100), (current, previous) => {
1040
+ if (current !== previous) runOnJS(setValue)(current);
1041
+ }, [channel]);
1042
+ const text = format ? format(value) : channel === 'hue' ? `${value}°` : `${value}%`;
1043
+ return /*#__PURE__*/_jsxs(View, {
1044
+ ref: ref,
1045
+ className: slots.channel({
1046
+ className
1047
+ }),
1048
+ children: [/*#__PURE__*/_jsx(Text, {
1049
+ className: slots.channelValue(),
1050
+ children: text
1051
+ }), /*#__PURE__*/_jsx(Text, {
1052
+ className: slots.channelLabel(),
1053
+ children: label ?? CHANNEL_LABEL[channel]
1054
+ })]
1055
+ });
1056
+ });
1057
+ ColorPickerChannelReadout.displayName = 'ColorPicker.Channel';
1058
+
1059
+ /* ------------------------------------------------------------------ *
1060
+ * Brightness — the third channel, as a track.
1061
+ * ------------------------------------------------------------------ */
1062
+
1063
+ /**
1064
+ * Black to the colour at full brightness.
1065
+ *
1066
+ * The square carries saturation and brightness together, so a picker built
1067
+ * around it never needs this. A wheel carries hue and saturation instead, and
1068
+ * leaves brightness with nowhere to live — this is where it goes.
1069
+ */
1070
+ const ColorPickerBrightness = /*#__PURE__*/forwardRef(({
1071
+ className,
1072
+ thumbClassName
1073
+ }, ref) => {
1074
+ const ctx = useColorPicker('ColorPicker.Brightness');
1075
+ const sign = useDirectionSign();
1076
+ const thumbSize = TRACK_THUMB[ctx.size];
1077
+ const width = useSharedValue(0);
1078
+ const [announced, setAnnounced] = useState(() => Math.round(ctx.brightness.value * 100));
1079
+ const settle = useCallback(v => setAnnounced(Math.round(v * 100)), []);
1080
+
1081
+ /*
1082
+ * The ramp's far end is the current hue at full brightness, so it has to be
1083
+ * re-declared from React when the hue moves. Unlike the hue ramp — which is
1084
+ * every hue and therefore never changes — this one is only two colours, so
1085
+ * rebuilding it is a two-element array rather than a gradient recompile.
1086
+ */
1087
+ const [hueEnd, setHueEnd] = useState(() => hsvToCss(ctx.hue.value, ctx.saturation.value, 1, 1));
1088
+ useAnimatedReaction(() => hsvToCss(ctx.hue.value, ctx.saturation.value, 1, 1), (current, previous) => {
1089
+ if (current !== previous) runOnJS(setHueEnd)(current);
1090
+ });
1091
+ const apply = (x, commit) => {
1092
+ 'worklet';
1093
+
1094
+ const v = trackFraction(x, width.value, thumbSize, sign);
1095
+ ctx.brightness.value = v;
1096
+ runOnJS(ctx.emit)(ctx.hue.value, ctx.saturation.value, v, ctx.opacity.value, commit);
1097
+ if (commit) {
1098
+ runOnJS(settle)(v);
1099
+ if (ctx.haptics) runOnJS(selectionTick)();
1100
+ }
1101
+ };
1102
+ const pan = Gesture.Pan().enabled(!ctx.disabled).activeOffsetX([-6, 6]).shouldCancelWhenOutside(false).onUpdate(event => apply(event.x, false)).onFinalize(event => apply(event.x, true));
1103
+ const tap = Gesture.Tap().enabled(!ctx.disabled).maxDuration(250).onEnd(event => apply(event.x, true));
1104
+ const gesture = Gesture.Race(pan, tap);
1105
+ const thumbStyle = useAnimatedStyle(() => {
1106
+ const travel = Math.max(width.value - thumbSize, 0);
1107
+ return {
1108
+ transform: [{
1109
+ translateX: ctx.brightness.value * travel * sign
1110
+ }]
1111
+ };
1112
+ });
1113
+ const thumbFillStyle = useAnimatedStyle(() => ({
1114
+ backgroundColor: hsvToCss(ctx.hue.value, ctx.saturation.value, ctx.brightness.value, 1)
1115
+ }));
1116
+ const nudge = dir => {
1117
+ const next = Math.min(Math.max(ctx.brightness.value + dir * NUDGE, 0), 1);
1118
+ ctx.brightness.value = withTiming(next, TIMING);
1119
+ settle(next);
1120
+ ctx.emit(ctx.hue.value, ctx.saturation.value, next, ctx.opacity.value, true);
1121
+ };
1122
+ return /*#__PURE__*/_jsx(ChannelTrack, {
1123
+ innerRef: ref,
1124
+ gesture: gesture,
1125
+ thumbStyle: thumbStyle,
1126
+ thumbFillStyle: thumbFillStyle,
1127
+ onLayout: event => {
1128
+ width.value = event.nativeEvent.layout.width;
1129
+ },
1130
+ size: ctx.size,
1131
+ className: className,
1132
+ thumbClassName: thumbClassName,
1133
+ label: "Brightness",
1134
+ disabled: ctx.disabled,
1135
+ accessibilityValue: {
1136
+ min: 0,
1137
+ max: 100,
1138
+ now: announced,
1139
+ text: `${announced}%`
1140
+ },
1141
+ onAccessibilityAction: event => {
1142
+ if (event.nativeEvent.actionName === 'increment') nudge(1);else if (event.nativeEvent.actionName === 'decrement') nudge(-1);
1143
+ },
1144
+ children: /*#__PURE__*/_jsx(LinearGradient, {
1145
+ colors: sign === 1 ? ['#000000', hueEnd] : [hueEnd, '#000000'],
1146
+ start: {
1147
+ x: 0,
1148
+ y: 0.5
1149
+ },
1150
+ end: {
1151
+ x: 1,
1152
+ y: 0.5
1153
+ },
1154
+ style: StyleSheet.absoluteFill
1155
+ })
1156
+ });
1157
+ });
1158
+ ColorPickerBrightness.displayName = 'ColorPicker.Brightness';
1159
+
1160
+ /* ------------------------------------------------------------------ *
1161
+ * Wheel — hue around, saturation out.
1162
+ * ------------------------------------------------------------------ */
1163
+
1164
+ /**
1165
+ * How many wedges the hue ring is cut into.
1166
+ *
1167
+ * There is no conic gradient here to draw a hue ring with, so it is
1168
+ * approximated by solid wedges. Three degrees each is under a pixel of colour
1169
+ * step at any size this is drawn at, and the wedges are given a hair of
1170
+ * overlap so the seams between them cannot show as lighter lines.
1171
+ */
1172
+ const WHEEL_WEDGES = 120;
1173
+ const WHEEL_OVERLAP = 0.6;
1174
+
1175
+ /** Radius of the drawing, in the wheel's own viewBox units. */
1176
+ const WHEEL_R = 50;
1177
+
1178
+ /** The hue ring, built once — it is the same wheel in every picker. */
1179
+ const WHEEL_PATHS = Array.from({
1180
+ length: WHEEL_WEDGES
1181
+ }, (_, index) => {
1182
+ const step = 360 / WHEEL_WEDGES;
1183
+ const from = index * step;
1184
+ const to = from + step + WHEEL_OVERLAP;
1185
+ const rad = deg => deg * Math.PI / 180;
1186
+ const x1 = WHEEL_R + WHEEL_R * Math.cos(rad(from));
1187
+ const y1 = WHEEL_R + WHEEL_R * Math.sin(rad(from));
1188
+ const x2 = WHEEL_R + WHEEL_R * Math.cos(rad(to));
1189
+ const y2 = WHEEL_R + WHEEL_R * Math.sin(rad(to));
1190
+ return {
1191
+ d: `M ${WHEEL_R} ${WHEEL_R} L ${x1} ${y1} A ${WHEEL_R} ${WHEEL_R} 0 0 1 ${x2} ${y2} Z`,
1192
+ fill: hsvToCss(from, 1, 1, 1)
1193
+ };
1194
+ });
1195
+ /**
1196
+ * The square, bent into a circle: hue is the angle and saturation is the
1197
+ * distance from the middle.
1198
+ *
1199
+ * It reads the same three shared values as `ColorPicker.Area`, so the two are
1200
+ * interchangeable — a picker is a wheel instead of a square, not a wheel as
1201
+ * well as one. What it does not carry is brightness, which has no third
1202
+ * dimension to occupy here; pair it with `ColorPicker.Brightness`.
1203
+ *
1204
+ * The wheel is deliberately not mirrored under RTL. A track has a start and an
1205
+ * end and so has a direction to read it in; a wheel has neither, and flipping
1206
+ * which way round the spectrum runs would only make the same colour sit
1207
+ * somewhere else.
1208
+ */
1209
+ const ColorPickerWheel = /*#__PURE__*/forwardRef(({
1210
+ className,
1211
+ size,
1212
+ thumbClassName
1213
+ }, ref) => {
1214
+ const ctx = useColorPicker('ColorPicker.Wheel');
1215
+ const slots = colorPickerVariants({
1216
+ size: ctx.size
1217
+ });
1218
+ const gradientId = useId().replace(/[^a-zA-Z0-9]/g, '');
1219
+ const diameter = size ?? WHEEL_SIZE[ctx.size];
1220
+ const thumbSize = AREA_THUMB[ctx.size];
1221
+ const radius = diameter / 2;
1222
+ const [announced, setAnnounced] = useState(() => ({
1223
+ h: Math.round(ctx.hue.value),
1224
+ s: Math.round(ctx.saturation.value * 100)
1225
+ }));
1226
+ const settle = useCallback((h, s) => {
1227
+ setAnnounced({
1228
+ h: Math.round(h),
1229
+ s: Math.round(s * 100)
1230
+ });
1231
+ }, []);
1232
+ const apply = (x, y, commit) => {
1233
+ 'worklet';
1234
+
1235
+ const dx = x - radius;
1236
+ const dy = y - radius;
1237
+ // Clockwise from three o'clock, which is what `atan2` already gives in a
1238
+ // coordinate space whose y runs downwards — so the wedges above and the
1239
+ // touch here agree without either of them correcting for the other.
1240
+ const degrees = Math.atan2(dy, dx) * 180 / Math.PI;
1241
+ const h = (degrees + 360) % 360;
1242
+ // Past the rim the colour is the one on the rim, rather than nothing:
1243
+ // a finger that overshoots the edge of a disc has still chosen a hue.
1244
+ const s = Math.min(Math.sqrt(dx * dx + dy * dy) / radius, 1);
1245
+ ctx.hue.value = h;
1246
+ ctx.saturation.value = s;
1247
+ runOnJS(ctx.emit)(h, s, ctx.brightness.value, ctx.opacity.value, commit);
1248
+ if (commit) {
1249
+ runOnJS(settle)(h, s);
1250
+ if (ctx.haptics) runOnJS(selectionTick)();
1251
+ }
1252
+ };
1253
+ const pan = Gesture.Pan().enabled(!ctx.disabled).minDistance(0).shouldCancelWhenOutside(false).onBegin(event => apply(event.x, event.y, false)).onUpdate(event => apply(event.x, event.y, false)).onFinalize(event => apply(event.x, event.y, true));
1254
+ const thumbStyle = useAnimatedStyle(() => {
1255
+ const angle = ctx.hue.value * Math.PI / 180;
1256
+ const r = ctx.saturation.value * radius;
1257
+ return {
1258
+ transform: [{
1259
+ translateX: radius + r * Math.cos(angle) - thumbSize / 2
1260
+ }, {
1261
+ translateY: radius + r * Math.sin(angle) - thumbSize / 2
1262
+ }]
1263
+ };
1264
+ });
1265
+ const thumbFillStyle = useAnimatedStyle(() => ({
1266
+ backgroundColor: hsvToCss(ctx.hue.value, ctx.saturation.value, ctx.brightness.value, 1)
1267
+ }));
1268
+
1269
+ // Brightness is not drawn into the wheel — it darkens the whole of it,
1270
+ // which is what brightness does.
1271
+ const dimStyle = useAnimatedStyle(() => ({
1272
+ opacity: 1 - ctx.brightness.value
1273
+ }));
1274
+ const nudge = (channel, dir) => {
1275
+ if (channel === 'hue') {
1276
+ const next = (ctx.hue.value + dir * HUE_NUDGE + 360) % 360;
1277
+ ctx.hue.value = next;
1278
+ settle(next, ctx.saturation.value);
1279
+ ctx.emit(next, ctx.saturation.value, ctx.brightness.value, ctx.opacity.value, true);
1280
+ return;
1281
+ }
1282
+ const next = Math.min(Math.max(ctx.saturation.value + dir * NUDGE, 0), 1);
1283
+ ctx.saturation.value = withTiming(next, TIMING);
1284
+ settle(ctx.hue.value, next);
1285
+ ctx.emit(ctx.hue.value, next, ctx.brightness.value, ctx.opacity.value, true);
1286
+ };
1287
+ const onAccessibilityAction = event => {
1288
+ const action = event.nativeEvent.actionName;
1289
+ if (action === 'increment') nudge('hue', 1);else if (action === 'decrement') nudge('hue', -1);else if (action === 'saturate') nudge('saturation', 1);else if (action === 'desaturate') nudge('saturation', -1);
1290
+ };
1291
+ return /*#__PURE__*/_jsx(GestureDetector, {
1292
+ gesture: pan,
1293
+ children: /*#__PURE__*/_jsxs(View, {
1294
+ ref: ref,
1295
+ style: {
1296
+ width: diameter,
1297
+ height: diameter
1298
+ },
1299
+ className: "self-center",
1300
+ accessible: true,
1301
+ accessibilityRole: "adjustable",
1302
+ accessibilityLabel: "Hue and saturation",
1303
+ accessibilityState: {
1304
+ disabled: ctx.disabled
1305
+ },
1306
+ accessibilityValue: {
1307
+ min: 0,
1308
+ max: 360,
1309
+ now: announced.h,
1310
+ text: `${announced.h}°, ${announced.s}% saturation`
1311
+ },
1312
+ accessibilityActions: [{
1313
+ name: 'increment'
1314
+ }, {
1315
+ name: 'decrement'
1316
+ }, {
1317
+ name: 'saturate',
1318
+ label: 'More saturated'
1319
+ }, {
1320
+ name: 'desaturate',
1321
+ label: 'Less saturated'
1322
+ }],
1323
+ onAccessibilityAction: onAccessibilityAction,
1324
+ children: [/*#__PURE__*/_jsxs(View, {
1325
+ style: {
1326
+ width: diameter,
1327
+ height: diameter
1328
+ },
1329
+ className: slots.wheel({
1330
+ className
1331
+ }),
1332
+ children: [/*#__PURE__*/_jsxs(Svg, {
1333
+ width: "100%",
1334
+ height: "100%",
1335
+ viewBox: "0 0 100 100",
1336
+ children: [/*#__PURE__*/_jsx(Defs, {
1337
+ children: /*#__PURE__*/_jsxs(RadialGradient, {
1338
+ id: gradientId,
1339
+ cx: "50%",
1340
+ cy: "50%",
1341
+ r: "50%",
1342
+ children: [/*#__PURE__*/_jsx(Stop, {
1343
+ offset: "0%",
1344
+ stopColor: "#ffffff",
1345
+ stopOpacity: 1
1346
+ }), /*#__PURE__*/_jsx(Stop, {
1347
+ offset: "100%",
1348
+ stopColor: "#ffffff",
1349
+ stopOpacity: 0
1350
+ })]
1351
+ })
1352
+ }), WHEEL_PATHS.map((wedge, index) => /*#__PURE__*/_jsx(Path, {
1353
+ d: wedge.d,
1354
+ fill: wedge.fill
1355
+ }, index)), /*#__PURE__*/_jsx(Circle, {
1356
+ cx: WHEEL_R,
1357
+ cy: WHEEL_R,
1358
+ r: WHEEL_R,
1359
+ fill: `url(#${gradientId})`
1360
+ })]
1361
+ }), /*#__PURE__*/_jsx(Animated.View, {
1362
+ pointerEvents: "none",
1363
+ style: [StyleSheet.absoluteFill, dimStyle],
1364
+ className: "bg-black"
1365
+ })]
1366
+ }), /*#__PURE__*/_jsx(Animated.View, {
1367
+ pointerEvents: "none",
1368
+ style: [thumbStyle, {
1369
+ width: thumbSize,
1370
+ height: thumbSize
1371
+ }],
1372
+ className: slots.thumb({
1373
+ className: thumbClassName
1374
+ }),
1375
+ children: /*#__PURE__*/_jsx(Animated.View, {
1376
+ style: thumbFillStyle,
1377
+ className: slots.thumbFill()
1378
+ })
1379
+ })]
1380
+ })
1381
+ });
1382
+ });
1383
+ ColorPickerWheel.displayName = 'ColorPicker.Wheel';
1384
+ export const ColorPicker = Object.assign(ColorPickerRoot, {
1385
+ Field: ColorPickerField,
1386
+ Area: ColorPickerArea,
1387
+ Wheel: ColorPickerWheel,
1388
+ Channel: ColorPickerChannelReadout,
1389
+ Hue: ColorPickerHue,
1390
+ Brightness: ColorPickerBrightness,
1391
+ Alpha: ColorPickerAlpha,
1392
+ Preview: ColorPickerPreview,
1393
+ Swatches: ColorPickerSwatches
1394
+ });
1395
+ //# sourceMappingURL=index.js.map