panelui-native 0.78.0 → 0.79.1
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/lib/module/components/accordion/index.js +13 -1
- package/lib/module/components/accordion/index.js.map +1 -1
- package/lib/module/components/map/index.js +63 -39
- package/lib/module/components/map/index.js.map +1 -1
- package/lib/module/components/map/map-children.js +54 -0
- package/lib/module/components/map/map-children.js.map +1 -0
- package/lib/module/components/map/maplibre.js +31 -3
- package/lib/module/components/map/maplibre.js.map +1 -1
- package/lib/module/components/markdown-editor/index.js +288 -34
- package/lib/module/components/markdown-editor/index.js.map +1 -1
- package/lib/module/components/markdown-editor/markdown-transforms.js +272 -9
- package/lib/module/components/markdown-editor/markdown-transforms.js.map +1 -1
- package/lib/module/components/marquee/index.js +119 -46
- package/lib/module/components/marquee/index.js.map +1 -1
- package/lib/module/components/panelside/index.js +162 -34
- package/lib/module/components/panelside/index.js.map +1 -1
- package/lib/module/components/progress-button/index.js +279 -52
- package/lib/module/components/progress-button/index.js.map +1 -1
- package/lib/module/components/progress-button/progress-button-hold.js +40 -6
- package/lib/module/components/progress-button/progress-button-hold.js.map +1 -1
- package/lib/module/components/theme-selector/index.js +239 -0
- package/lib/module/components/theme-selector/index.js.map +1 -0
- package/lib/module/components/theme-selector/theme-preview.js +320 -0
- package/lib/module/components/theme-selector/theme-preview.js.map +1 -0
- package/lib/module/index.js +1 -0
- package/lib/module/index.js.map +1 -1
- package/lib/typescript/src/components/accordion/index.d.ts.map +1 -1
- package/lib/typescript/src/components/map/index.d.ts +8 -4
- package/lib/typescript/src/components/map/index.d.ts.map +1 -1
- package/lib/typescript/src/components/map/map-children.d.ts +41 -0
- package/lib/typescript/src/components/map/map-children.d.ts.map +1 -0
- package/lib/typescript/src/components/map/maplibre.d.ts +8 -4
- package/lib/typescript/src/components/map/maplibre.d.ts.map +1 -1
- package/lib/typescript/src/components/markdown-editor/index.d.ts +64 -5
- package/lib/typescript/src/components/markdown-editor/index.d.ts.map +1 -1
- package/lib/typescript/src/components/markdown-editor/markdown-transforms.d.ts +33 -5
- package/lib/typescript/src/components/markdown-editor/markdown-transforms.d.ts.map +1 -1
- package/lib/typescript/src/components/marquee/index.d.ts.map +1 -1
- package/lib/typescript/src/components/panelside/index.d.ts +79 -4
- package/lib/typescript/src/components/panelside/index.d.ts.map +1 -1
- package/lib/typescript/src/components/progress-button/index.d.ts +29 -13
- package/lib/typescript/src/components/progress-button/index.d.ts.map +1 -1
- package/lib/typescript/src/components/progress-button/progress-button-hold.d.ts +26 -6
- package/lib/typescript/src/components/progress-button/progress-button-hold.d.ts.map +1 -1
- package/lib/typescript/src/components/theme-selector/index.d.ts +107 -0
- package/lib/typescript/src/components/theme-selector/index.d.ts.map +1 -0
- package/lib/typescript/src/components/theme-selector/theme-preview.d.ts +32 -0
- package/lib/typescript/src/components/theme-selector/theme-preview.d.ts.map +1 -0
- package/lib/typescript/src/index.d.ts +4 -3
- package/lib/typescript/src/index.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/components/accordion/index.tsx +13 -0
- package/src/components/map/index.tsx +63 -36
- package/src/components/map/map-children.ts +66 -0
- package/src/components/map/maplibre.ts +32 -4
- package/src/components/markdown-editor/index.tsx +413 -35
- package/src/components/markdown-editor/markdown-transforms.ts +274 -10
- package/src/components/marquee/index.tsx +128 -55
- package/src/components/panelside/index.tsx +179 -40
- package/src/components/progress-button/index.tsx +264 -45
- package/src/components/progress-button/progress-button-hold.ts +37 -6
- package/src/components/theme-selector/index.tsx +281 -0
- package/src/components/theme-selector/theme-preview.tsx +205 -0
- package/src/index.ts +15 -0
|
@@ -41,8 +41,10 @@
|
|
|
41
41
|
* is a broken button — so this is a coarser indicator, not the absence of one.
|
|
42
42
|
*/
|
|
43
43
|
import {
|
|
44
|
+
Children,
|
|
44
45
|
createContext,
|
|
45
46
|
forwardRef,
|
|
47
|
+
isValidElement,
|
|
46
48
|
useCallback,
|
|
47
49
|
useContext,
|
|
48
50
|
useEffect,
|
|
@@ -62,20 +64,23 @@ import Animated, {
|
|
|
62
64
|
Easing,
|
|
63
65
|
cancelAnimation,
|
|
64
66
|
runOnJS,
|
|
67
|
+
runOnUI,
|
|
65
68
|
useAnimatedReaction,
|
|
66
69
|
useAnimatedStyle,
|
|
67
70
|
useReducedMotion,
|
|
68
71
|
useSharedValue,
|
|
72
|
+
withSpring,
|
|
69
73
|
withTiming,
|
|
70
74
|
type SharedValue,
|
|
71
75
|
} from 'react-native-reanimated';
|
|
72
76
|
import { tv, type VariantProps } from 'tailwind-variants';
|
|
77
|
+
import { useCSSVariable } from 'uniwind';
|
|
78
|
+
import { CheckIcon, IconColorProvider } from '../../icons';
|
|
73
79
|
import { Text, textChildren } from '../../primitives/text';
|
|
74
|
-
import { cn } from '../../utils/cn';
|
|
75
80
|
import { impactKnock, selectionTick } from '../../utils/haptics';
|
|
76
81
|
import {
|
|
77
82
|
DEFAULT_AUTO_RESET_DELAY,
|
|
78
|
-
|
|
83
|
+
fillDuration,
|
|
79
84
|
releaseDuration,
|
|
80
85
|
resolveHoldDuration,
|
|
81
86
|
} from './progress-button-hold';
|
|
@@ -83,6 +88,32 @@ import {
|
|
|
83
88
|
/** How many steps the reduced-motion fill advances in. */
|
|
84
89
|
const REDUCED_STEPS = 5;
|
|
85
90
|
|
|
91
|
+
/**
|
|
92
|
+
* The arrival of the completed drawing.
|
|
93
|
+
*
|
|
94
|
+
* A spring rather than a timing, and slightly overshooting: the fill has just
|
|
95
|
+
* spent two seconds moving at a constant rate, and something that lands with a
|
|
96
|
+
* little weight is what tells the reader the waiting part is over.
|
|
97
|
+
*/
|
|
98
|
+
const DONE_SPRING = { damping: 14, stiffness: 220, mass: 0.6 } as const;
|
|
99
|
+
|
|
100
|
+
/** How long the completed drawing takes to leave again on a reset. */
|
|
101
|
+
const DONE_EXIT = 140;
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Which token the tick is drawn in, per variant.
|
|
105
|
+
*
|
|
106
|
+
* It sits on the finished fill, so it takes that fill's own foreground rather
|
|
107
|
+
* than the button's — the same pairing `fillLabel` uses, which is what keeps
|
|
108
|
+
* the contrast right in both themes without a hardcoded colour.
|
|
109
|
+
*/
|
|
110
|
+
const DONE_TINT = {
|
|
111
|
+
primary: '--color-primary-foreground',
|
|
112
|
+
secondary: '--color-background',
|
|
113
|
+
destructive: '--color-destructive-solid-foreground',
|
|
114
|
+
success: '--color-success-solid-foreground',
|
|
115
|
+
} as const;
|
|
116
|
+
|
|
86
117
|
const progressButtonVariants = tv({
|
|
87
118
|
slots: {
|
|
88
119
|
/*
|
|
@@ -94,7 +125,22 @@ const progressButtonVariants = tv({
|
|
|
94
125
|
* a rectangle sliding out from under the button rather than as the button
|
|
95
126
|
* filling up.
|
|
96
127
|
*/
|
|
97
|
-
|
|
128
|
+
/*
|
|
129
|
+
* Every variant rests on the same secondary surface, and `variant` decides
|
|
130
|
+
* only what colour comes across it.
|
|
131
|
+
*
|
|
132
|
+
* Drawn as outlines they were four different buttons before anything had
|
|
133
|
+
* happened, and the one thing they all do — wait to be held — was the
|
|
134
|
+
* thing the drawing did not say. A solid ground says it: the button is
|
|
135
|
+
* unfilled, and the fill is what the hold produces.
|
|
136
|
+
*
|
|
137
|
+
* The variant is still legible at rest, because the label carries its
|
|
138
|
+
* colour. That is the half worth keeping — a destructive hold should not
|
|
139
|
+
* look like an ordinary one before it is touched — and it is also the half
|
|
140
|
+
* that survives the wipe, since the fill covers the ground the label was
|
|
141
|
+
* standing on and the second copy takes over.
|
|
142
|
+
*/
|
|
143
|
+
root: 'relative overflow-hidden rounded-full border border-transparent bg-secondary',
|
|
98
144
|
/*
|
|
99
145
|
* The row inside the button. It is separate from `root` because the fill
|
|
100
146
|
* has to sit over the whole button including its padding — a wipe that
|
|
@@ -110,25 +156,21 @@ const progressButtonVariants = tv({
|
|
|
110
156
|
variants: {
|
|
111
157
|
variant: {
|
|
112
158
|
primary: {
|
|
113
|
-
root: 'border-primary bg-transparent',
|
|
114
159
|
label: 'text-primary',
|
|
115
160
|
fill: 'bg-primary',
|
|
116
161
|
fillLabel: 'text-primary-foreground',
|
|
117
162
|
},
|
|
118
163
|
secondary: {
|
|
119
|
-
root: 'border-transparent bg-secondary',
|
|
120
164
|
label: 'text-secondary-foreground',
|
|
121
165
|
fill: 'bg-foreground',
|
|
122
166
|
fillLabel: 'text-background',
|
|
123
167
|
},
|
|
124
168
|
destructive: {
|
|
125
|
-
root: 'border-destructive bg-transparent',
|
|
126
169
|
label: 'text-destructive',
|
|
127
170
|
fill: 'bg-destructive',
|
|
128
171
|
fillLabel: 'text-destructive-solid-foreground',
|
|
129
172
|
},
|
|
130
173
|
success: {
|
|
131
|
-
root: 'border-success bg-transparent',
|
|
132
174
|
label: 'text-success',
|
|
133
175
|
fill: 'bg-success',
|
|
134
176
|
fillLabel: 'text-success-solid-foreground',
|
|
@@ -183,6 +225,12 @@ export type ProgressButtonSize = NonNullable<ProgressButtonVariantProps['size']>
|
|
|
183
225
|
interface ProgressButtonContextValue {
|
|
184
226
|
/** `0` to `1` across the hold. */
|
|
185
227
|
progress: SharedValue<number>;
|
|
228
|
+
/**
|
|
229
|
+
* `0` to `1` across the arrival of the completed drawing. Separate from
|
|
230
|
+
* `progress` because the fill has finished by the time this starts, and the
|
|
231
|
+
* two would otherwise have to share one clock running at two speeds.
|
|
232
|
+
*/
|
|
233
|
+
done: SharedValue<number>;
|
|
186
234
|
/** Width of the button, so the fill's copy of the label can match it. */
|
|
187
235
|
width: number;
|
|
188
236
|
completed: boolean;
|
|
@@ -224,6 +272,10 @@ export interface ProgressButtonProps
|
|
|
224
272
|
autoReset?: boolean;
|
|
225
273
|
/** Milliseconds to stay completed before resetting. Defaults to `1000`. */
|
|
226
274
|
autoResetDelay?: number;
|
|
275
|
+
/**
|
|
276
|
+
* Dim the button and refuse the hold outright. The fill never starts, so
|
|
277
|
+
* there is no half-finished state to explain.
|
|
278
|
+
*/
|
|
227
279
|
disabled?: boolean;
|
|
228
280
|
/**
|
|
229
281
|
* A tick as the hold takes, and a knock when it completes. Off by default:
|
|
@@ -258,6 +310,7 @@ const ProgressButtonRoot = forwardRef<View, ProgressButtonProps>(function Progre
|
|
|
258
310
|
const reducedMotion = useReducedMotion();
|
|
259
311
|
|
|
260
312
|
const progress = useSharedValue(0);
|
|
313
|
+
const done = useSharedValue(0);
|
|
261
314
|
const [width, setWidth] = useState(0);
|
|
262
315
|
const [internalCompleted, setInternalCompleted] = useState(false);
|
|
263
316
|
const resetTimer = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
@@ -265,7 +318,19 @@ const ProgressButtonRoot = forwardRef<View, ProgressButtonProps>(function Progre
|
|
|
265
318
|
const isControlled = completedProp !== undefined;
|
|
266
319
|
const completed = isControlled ? completedProp : internalCompleted;
|
|
267
320
|
|
|
321
|
+
/*
|
|
322
|
+
* The reaction below watches a shared value, and a shared value can reach the
|
|
323
|
+
* end for reasons other than a hold — a controlled button being told it is
|
|
324
|
+
* complete fills instantly. Read from a ref rather than from `completed` so
|
|
325
|
+
* the guard sees the current answer without the reaction being rebuilt.
|
|
326
|
+
*/
|
|
327
|
+
const completedRef = useRef(completed);
|
|
328
|
+
completedRef.current = completed;
|
|
329
|
+
|
|
268
330
|
const finish = useCallback(() => {
|
|
331
|
+
// Already complete: the fill was set from outside, and firing the action
|
|
332
|
+
// again would run it a second time for something the reader did not do.
|
|
333
|
+
if (completedRef.current) return;
|
|
269
334
|
if (!isControlled) setInternalCompleted(true);
|
|
270
335
|
onCompletedChange?.(true);
|
|
271
336
|
onComplete?.();
|
|
@@ -288,45 +353,91 @@ const ProgressButtonRoot = forwardRef<View, ProgressButtonProps>(function Progre
|
|
|
288
353
|
}
|
|
289
354
|
);
|
|
290
355
|
|
|
356
|
+
/*
|
|
357
|
+
* Both directions are started on the UI thread, and that is not a detail.
|
|
358
|
+
*
|
|
359
|
+
* A shared value animated on the UI thread does not report back to
|
|
360
|
+
* JavaScript, so `progress.value` read from a press handler is the value
|
|
361
|
+
* from before the hold began — zero. The release computed from it either did
|
|
362
|
+
* nothing or, worse, `cancelAnimation` wrote that stale zero back and the
|
|
363
|
+
* fill vanished on touch-up instead of travelling home.
|
|
364
|
+
*
|
|
365
|
+
* `runOnUI` puts the read where the value actually lives.
|
|
366
|
+
*/
|
|
291
367
|
const begin = useCallback(() => {
|
|
292
368
|
if (disabled || completed) return;
|
|
293
369
|
if (haptics) selectionTick();
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
* much longer to wait — take that away and the control asks for a hold
|
|
300
|
-
* with nothing on screen to say why.
|
|
301
|
-
*/
|
|
370
|
+
runOnUI(() => {
|
|
371
|
+
'worklet';
|
|
372
|
+
cancelAnimation(progress);
|
|
373
|
+
const from = progress.value;
|
|
374
|
+
if (from >= 1) return;
|
|
302
375
|
progress.value = withTiming(1, {
|
|
303
|
-
|
|
304
|
-
|
|
376
|
+
// The distance still ahead, at the fill's own rate — so a press that
|
|
377
|
+
// catches the fill on its way back carries on from there rather than
|
|
378
|
+
// restarting the clock.
|
|
379
|
+
duration: fillDuration(from, duration),
|
|
380
|
+
/*
|
|
381
|
+
* Linear. A fill is constant motion, and an eased one misreports the
|
|
382
|
+
* wait: it races the first half and crawls the second, or the reverse.
|
|
383
|
+
*
|
|
384
|
+
* Under reduced motion it steps instead, and is still a real
|
|
385
|
+
* indicator — what that setting is about is continuous movement, not
|
|
386
|
+
* the button saying how much longer to wait. Take that away and the
|
|
387
|
+
* control asks for a hold with nothing on screen to say why.
|
|
388
|
+
*/
|
|
389
|
+
easing: reducedMotion ? Easing.steps(REDUCED_STEPS, true) : Easing.linear,
|
|
305
390
|
});
|
|
306
|
-
|
|
307
|
-
}
|
|
308
|
-
// Linear. A fill is constant motion, and an eased one misreports the wait:
|
|
309
|
-
// it races the first half and crawls the second, or the reverse.
|
|
310
|
-
progress.value = withTiming(1, { duration, easing: Easing.linear });
|
|
391
|
+
})();
|
|
311
392
|
}, [disabled, completed, haptics, progress, reducedMotion, duration]);
|
|
312
393
|
|
|
394
|
+
/*
|
|
395
|
+
* The fill, played backwards.
|
|
396
|
+
*
|
|
397
|
+
* Same rate, same easing, same stepping under reduced motion — only the
|
|
398
|
+
* direction differs. Let go at nine tenths of a two-second hold and the fill
|
|
399
|
+
* takes 1.8 seconds to travel home, which is the 1.8 seconds it took to get
|
|
400
|
+
* there. A fill that vanishes has been deleted; a fill that travels back has
|
|
401
|
+
* been let go, and telling those apart is the whole reason the wait is drawn
|
|
402
|
+
* on the button.
|
|
403
|
+
*
|
|
404
|
+
* Every path that empties the fill goes through here — a hold let go, an
|
|
405
|
+
* `autoReset` landing, a controlled button told it is no longer complete.
|
|
406
|
+
* They were three separate assignments and two of them snapped.
|
|
407
|
+
*/
|
|
408
|
+
const rewind = useCallback(() => {
|
|
409
|
+
runOnUI(() => {
|
|
410
|
+
'worklet';
|
|
411
|
+
cancelAnimation(progress);
|
|
412
|
+
const from = progress.value;
|
|
413
|
+
if (from <= 0) {
|
|
414
|
+
progress.value = 0;
|
|
415
|
+
return;
|
|
416
|
+
}
|
|
417
|
+
progress.value = withTiming(0, {
|
|
418
|
+
duration: releaseDuration(from, duration),
|
|
419
|
+
easing: reducedMotion ? Easing.steps(REDUCED_STEPS, true) : Easing.linear,
|
|
420
|
+
});
|
|
421
|
+
})();
|
|
422
|
+
}, [progress, duration, reducedMotion]);
|
|
423
|
+
|
|
313
424
|
const abandon = useCallback(() => {
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
425
|
+
/*
|
|
426
|
+
* A completed hold has nothing to abandon. Without this the fill drains on
|
|
427
|
+
* the release that follows a successful hold — the button empties, looks
|
|
428
|
+
* untouched, and then refuses every press after it, because `begin` bails
|
|
429
|
+
* on a completed button. That combination is a control that has silently
|
|
430
|
+
* stopped working.
|
|
431
|
+
*/
|
|
432
|
+
if (completedRef.current) return;
|
|
433
|
+
rewind();
|
|
434
|
+
}, [rewind]);
|
|
323
435
|
|
|
324
436
|
const reset = useCallback(() => {
|
|
325
|
-
|
|
326
|
-
progress.value = 0;
|
|
437
|
+
rewind();
|
|
327
438
|
if (!isControlled) setInternalCompleted(false);
|
|
328
439
|
onCompletedChange?.(false);
|
|
329
|
-
}, [
|
|
440
|
+
}, [rewind, isControlled, onCompletedChange]);
|
|
330
441
|
|
|
331
442
|
useEffect(() => {
|
|
332
443
|
if (!completed || !autoReset) return;
|
|
@@ -337,19 +448,42 @@ const ProgressButtonRoot = forwardRef<View, ProgressButtonProps>(function Progre
|
|
|
337
448
|
};
|
|
338
449
|
}, [completed, autoReset, autoResetDelay, reset]);
|
|
339
450
|
|
|
340
|
-
|
|
341
|
-
|
|
451
|
+
/*
|
|
452
|
+
* The fill follows the completed state, in both directions.
|
|
453
|
+
*
|
|
454
|
+
* A controlled button told it is complete fills without a hold, and one told
|
|
455
|
+
* it is no longer complete empties — otherwise the next hold would start from
|
|
456
|
+
* a bar that is already full.
|
|
457
|
+
*/
|
|
458
|
+
useEffect(() => {
|
|
459
|
+
if (completed) {
|
|
460
|
+
cancelAnimation(progress);
|
|
461
|
+
progress.value = 1;
|
|
462
|
+
return;
|
|
463
|
+
}
|
|
464
|
+
rewind();
|
|
465
|
+
}, [completed, progress, rewind]);
|
|
466
|
+
|
|
467
|
+
/*
|
|
468
|
+
* And the completed drawing follows it too. Under reduced motion it is a
|
|
469
|
+
* swap: what that setting is about is movement, and a tick that grows is
|
|
470
|
+
* movement with nothing to report.
|
|
471
|
+
*/
|
|
342
472
|
useEffect(() => {
|
|
343
|
-
if (
|
|
344
|
-
|
|
345
|
-
|
|
473
|
+
if (reducedMotion) {
|
|
474
|
+
done.value = completed ? 1 : 0;
|
|
475
|
+
return;
|
|
476
|
+
}
|
|
477
|
+
done.value = completed ? withSpring(1, DONE_SPRING) : withTiming(0, { duration: DONE_EXIT });
|
|
478
|
+
}, [completed, done, reducedMotion]);
|
|
346
479
|
|
|
347
480
|
useEffect(
|
|
348
481
|
() => () => {
|
|
349
482
|
cancelAnimation(progress);
|
|
483
|
+
cancelAnimation(done);
|
|
350
484
|
if (resetTimer.current) clearTimeout(resetTimer.current);
|
|
351
485
|
},
|
|
352
|
-
[progress]
|
|
486
|
+
[progress, done]
|
|
353
487
|
);
|
|
354
488
|
|
|
355
489
|
const onLayout = (event: LayoutChangeEvent) => {
|
|
@@ -358,14 +492,32 @@ const ProgressButtonRoot = forwardRef<View, ProgressButtonProps>(function Progre
|
|
|
358
492
|
props.onLayout?.(event);
|
|
359
493
|
};
|
|
360
494
|
|
|
495
|
+
/*
|
|
496
|
+
* `slots` is rebuilt every render, so it is deliberately not a dependency —
|
|
497
|
+
* including it would make this memo re-run every time and hand every consumer
|
|
498
|
+
* a new object for no change. The variant and size it is derived from are the
|
|
499
|
+
* dependencies instead.
|
|
500
|
+
*/
|
|
361
501
|
const context = useMemo<ProgressButtonContextValue>(
|
|
362
|
-
() => ({ progress, width, completed, variant, size, slots }),
|
|
363
|
-
|
|
502
|
+
() => ({ progress, done, width, completed, variant, size, slots }),
|
|
503
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
504
|
+
[progress, done, width, completed, variant, size]
|
|
364
505
|
);
|
|
365
506
|
|
|
366
|
-
|
|
507
|
+
/*
|
|
508
|
+
* The completed drawing is part of the button rather than something every
|
|
509
|
+
* call site has to remember, so one is added unless the caller wrote their
|
|
510
|
+
* own. A hold that lands and shows nothing is the reader wondering whether it
|
|
511
|
+
* worked, which is the thing this control exists to remove.
|
|
512
|
+
*/
|
|
513
|
+
const written = Children.toArray(children);
|
|
514
|
+
const hasDone = written.some(
|
|
515
|
+
(child) => isValidElement(child) && child.type === ProgressButtonDone
|
|
516
|
+
);
|
|
517
|
+
const body = (
|
|
367
518
|
<>
|
|
368
|
-
<ProgressButtonLabel>Hold to confirm</ProgressButtonLabel>
|
|
519
|
+
{written.length > 0 ? written : <ProgressButtonLabel>Hold to confirm</ProgressButtonLabel>}
|
|
520
|
+
{hasDone ? null : <ProgressButtonDone />}
|
|
369
521
|
</>
|
|
370
522
|
);
|
|
371
523
|
|
|
@@ -387,12 +539,18 @@ const ProgressButtonRoot = forwardRef<View, ProgressButtonProps>(function Progre
|
|
|
387
539
|
checked: completed,
|
|
388
540
|
}}
|
|
389
541
|
disabled={disabled}
|
|
542
|
+
/*
|
|
543
|
+
* The spread sits above the hold rather than below it. Underneath, a
|
|
544
|
+
* caller passing `onPressIn` — reasonably, to log a tap — would replace
|
|
545
|
+
* the gesture the whole control is, and the button would simply never
|
|
546
|
+
* fill.
|
|
547
|
+
*/
|
|
548
|
+
{...props}
|
|
390
549
|
onPressIn={begin}
|
|
391
550
|
onPressOut={abandon}
|
|
392
551
|
// A few points of drift should not abandon a hold the reader is
|
|
393
552
|
// still making. Fingers move.
|
|
394
553
|
pressRetentionOffset={16}
|
|
395
|
-
{...props}
|
|
396
554
|
onLayout={onLayout}
|
|
397
555
|
className={slots.root({ className })}
|
|
398
556
|
>
|
|
@@ -449,16 +607,77 @@ function ProgressButtonLabel({ className, children, ...props }: ProgressButtonLa
|
|
|
449
607
|
}
|
|
450
608
|
ProgressButtonLabel.displayName = 'ProgressButton.Label';
|
|
451
609
|
|
|
610
|
+
export interface ProgressButtonDoneProps extends ViewProps {
|
|
611
|
+
className?: string;
|
|
612
|
+
/**
|
|
613
|
+
* What the button shows once the hold has landed. A tick on its own by
|
|
614
|
+
* default; children replace it, so a word beside one is
|
|
615
|
+
* `<ProgressButton.Done><CheckIcon /><Text>Paid</Text></ProgressButton.Done>`.
|
|
616
|
+
*/
|
|
617
|
+
children?: ReactNode;
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
/**
|
|
621
|
+
* The drawing the button lands on.
|
|
622
|
+
*
|
|
623
|
+
* It sits over the finished fill rather than beside the label, so the button
|
|
624
|
+
* does not change width at the moment it completes — a control that resizes as
|
|
625
|
+
* it succeeds moves everything under it, and the reader's eye is on the button.
|
|
626
|
+
*
|
|
627
|
+
* One is added for you unless you write your own, because a hold that lands and
|
|
628
|
+
* shows nothing leaves the reader checking whether it worked.
|
|
629
|
+
*/
|
|
630
|
+
function ProgressButtonDone({ className, children, ...props }: ProgressButtonDoneProps) {
|
|
631
|
+
const { done, completed, variant, slots } = useProgressButtonContext('ProgressButton.Done');
|
|
632
|
+
const tint = useCSSVariable(DONE_TINT[variant]);
|
|
633
|
+
|
|
634
|
+
/*
|
|
635
|
+
* It carries the fill's own colour and covers the button edge to edge.
|
|
636
|
+
*
|
|
637
|
+
* The alternative — fading the label out from under it — takes the fill with
|
|
638
|
+
* it, because the fill is drawn inside the label so that the two copies of
|
|
639
|
+
* the text line up. The button would empty at the exact moment it succeeded,
|
|
640
|
+
* and the tick, drawn in the colour that reads against a full fill, would be
|
|
641
|
+
* left standing on nothing.
|
|
642
|
+
*/
|
|
643
|
+
const style = useAnimatedStyle(() => ({ opacity: done.value }));
|
|
644
|
+
// The tick arrives from slightly under full size. The ground it lands on
|
|
645
|
+
// does not: a background box that grows reads as the button resizing.
|
|
646
|
+
const markStyle = useAnimatedStyle(() => ({ transform: [{ scale: 0.7 + done.value * 0.3 }] }));
|
|
647
|
+
|
|
648
|
+
return (
|
|
649
|
+
<Animated.View
|
|
650
|
+
{...props}
|
|
651
|
+
// Never takes the press: the button underneath still owns it, which is
|
|
652
|
+
// what lets a completed button be reset and held again.
|
|
653
|
+
pointerEvents="none"
|
|
654
|
+
accessibilityElementsHidden={!completed}
|
|
655
|
+
importantForAccessibility={completed ? 'auto' : 'no-hide-descendants'}
|
|
656
|
+
style={[{ position: 'absolute', top: 0, right: 0, bottom: 0, left: 0 }, style]}
|
|
657
|
+
className={slots.fill({ className: slots.content({ className }) })}
|
|
658
|
+
>
|
|
659
|
+
<Animated.View style={markStyle} className={slots.content()}>
|
|
660
|
+
<IconColorProvider color={typeof tint === 'string' ? tint : undefined}>
|
|
661
|
+
{children ?? <CheckIcon size={18} />}
|
|
662
|
+
</IconColorProvider>
|
|
663
|
+
</Animated.View>
|
|
664
|
+
</Animated.View>
|
|
665
|
+
);
|
|
666
|
+
}
|
|
667
|
+
ProgressButtonDone.displayName = 'ProgressButton.Done';
|
|
668
|
+
|
|
452
669
|
ProgressButtonRoot.displayName = 'ProgressButton';
|
|
453
670
|
|
|
454
671
|
export const ProgressButton = Object.assign(ProgressButtonRoot, {
|
|
455
672
|
Label: ProgressButtonLabel,
|
|
673
|
+
Done: ProgressButtonDone,
|
|
456
674
|
});
|
|
457
675
|
|
|
458
676
|
export {
|
|
459
677
|
DEFAULT_AUTO_RESET_DELAY,
|
|
460
678
|
DEFAULT_HOLD_DURATION,
|
|
461
679
|
DEFAULT_RELEASE_DURATION,
|
|
680
|
+
fillDuration,
|
|
462
681
|
isComplete,
|
|
463
682
|
releaseDuration,
|
|
464
683
|
resolveHoldDuration,
|
|
@@ -8,8 +8,15 @@
|
|
|
8
8
|
/** Milliseconds a hold has to be sustained before it counts. */
|
|
9
9
|
export const DEFAULT_HOLD_DURATION = 2000;
|
|
10
10
|
|
|
11
|
-
/**
|
|
12
|
-
|
|
11
|
+
/**
|
|
12
|
+
* Milliseconds a complete fill takes to rewind, when the hold length is not
|
|
13
|
+
* known — the default only exists so {@link releaseDuration} has one.
|
|
14
|
+
*
|
|
15
|
+
* The component always passes the hold's own duration instead: the drain is
|
|
16
|
+
* the fill running backwards at the same rate, so a two-second hold takes two
|
|
17
|
+
* seconds to give back.
|
|
18
|
+
*/
|
|
19
|
+
export const DEFAULT_RELEASE_DURATION = DEFAULT_HOLD_DURATION;
|
|
13
20
|
|
|
14
21
|
/** Milliseconds before an `autoReset` button offers itself again. */
|
|
15
22
|
export const DEFAULT_AUTO_RESET_DELAY = 1000;
|
|
@@ -31,15 +38,24 @@ export function resolveHoldDuration(duration: number | undefined): number {
|
|
|
31
38
|
/**
|
|
32
39
|
* How long the fill should take to drain from where it is.
|
|
33
40
|
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
41
|
+
* The fill, run backwards. `full` is the hold's own duration, so the drain
|
|
42
|
+
* covers the distance left at exactly the rate it was filled at: let go at
|
|
43
|
+
* nine tenths of a two-second hold and it takes 1.8 seconds to give back, the
|
|
44
|
+
* same 1.8 seconds it took to earn.
|
|
45
|
+
*
|
|
46
|
+
* That is the point of it. The wait was drawn on the button, so undoing the
|
|
47
|
+
* wait is worth drawing too — a fill that vanishes has been deleted, and a
|
|
48
|
+
* fill that travels back has been let go.
|
|
38
49
|
*/
|
|
39
50
|
export function releaseDuration(
|
|
40
51
|
progress: number,
|
|
41
52
|
full: number = DEFAULT_RELEASE_DURATION
|
|
42
53
|
): number {
|
|
54
|
+
// A worklet, because the only runtime that knows how far the fill actually
|
|
55
|
+
// got is the one animating it. Reading a shared value from JavaScript gives
|
|
56
|
+
// the value before the animation started, so the release has to be started
|
|
57
|
+
// from the UI thread — and that means this has to run there too.
|
|
58
|
+
'worklet';
|
|
43
59
|
const travelled = Math.min(1, Math.max(0, progress));
|
|
44
60
|
return Math.max(80, full * travelled);
|
|
45
61
|
}
|
|
@@ -53,5 +69,20 @@ export function releaseDuration(
|
|
|
53
69
|
* button sometimes fires when the reader let go early on purpose.
|
|
54
70
|
*/
|
|
55
71
|
export function isComplete(progress: number): boolean {
|
|
72
|
+
'worklet';
|
|
56
73
|
return progress >= 1;
|
|
57
74
|
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* How long it takes to cover the distance still ahead, at the fill's own rate.
|
|
78
|
+
*
|
|
79
|
+
* A press that arrives while the fill is on its way back should carry on from
|
|
80
|
+
* where it is rather than restarting the clock — otherwise a fill picked up at
|
|
81
|
+
* halfway takes the whole hold to cover the half that is left, and the second
|
|
82
|
+
* attempt is twice as slow as the first for no reason the reader can see.
|
|
83
|
+
*/
|
|
84
|
+
export function fillDuration(from: number, full: number): number {
|
|
85
|
+
'worklet';
|
|
86
|
+
const remaining = Math.min(1, Math.max(0, 1 - from));
|
|
87
|
+
return Math.max(80, full * remaining);
|
|
88
|
+
}
|