panelui-native 0.61.1 → 0.63.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/lib/module/components/area-chart/index.js +14 -4
- package/lib/module/components/area-chart/index.js.map +1 -1
- package/lib/module/components/bar-chart/index.js +14 -4
- package/lib/module/components/bar-chart/index.js.map +1 -1
- package/lib/module/components/candlestick-chart/index.js +14 -4
- package/lib/module/components/candlestick-chart/index.js.map +1 -1
- package/lib/module/components/card/index.js +26 -1
- package/lib/module/components/card/index.js.map +1 -1
- package/lib/module/components/dialog/index.js +49 -3
- package/lib/module/components/dialog/index.js.map +1 -1
- package/lib/module/components/hex-chart/index.js +2 -2
- package/lib/module/components/hex-chart/index.js.map +1 -1
- package/lib/module/components/line-chart/index.js +14 -4
- package/lib/module/components/line-chart/index.js.map +1 -1
- package/lib/module/components/live-line-chart/index.js +1063 -0
- package/lib/module/components/live-line-chart/index.js.map +1 -0
- package/lib/module/components/pie-chart/index.js +2 -2
- package/lib/module/components/pie-chart/index.js.map +1 -1
- package/lib/module/components/plot/index.js +1329 -0
- package/lib/module/components/plot/index.js.map +1 -0
- package/lib/module/components/polar-area-chart/index.js +719 -0
- package/lib/module/components/polar-area-chart/index.js.map +1 -0
- package/lib/module/components/radar-chart/index.js +14 -4
- package/lib/module/components/radar-chart/index.js.map +1 -1
- package/lib/module/components/ring-chart/index.js +2 -2
- package/lib/module/components/ring-chart/index.js.map +1 -1
- package/lib/module/components/section-rail/index.js +120 -15
- package/lib/module/components/section-rail/index.js.map +1 -1
- package/lib/module/components/selection-mode/index.js +129 -18
- package/lib/module/components/selection-mode/index.js.map +1 -1
- package/lib/module/components/sortable/index.js +15 -5
- package/lib/module/components/sortable/index.js.map +1 -1
- package/lib/module/index.js +9 -0
- package/lib/module/index.js.map +1 -1
- package/lib/module/utils/chart.js +40 -0
- package/lib/module/utils/chart.js.map +1 -1
- package/lib/typescript/src/components/area-chart/index.d.ts.map +1 -1
- package/lib/typescript/src/components/bar-chart/index.d.ts.map +1 -1
- package/lib/typescript/src/components/candlestick-chart/index.d.ts +6 -3
- package/lib/typescript/src/components/candlestick-chart/index.d.ts.map +1 -1
- package/lib/typescript/src/components/card/index.d.ts +48 -1
- package/lib/typescript/src/components/card/index.d.ts.map +1 -1
- package/lib/typescript/src/components/dialog/index.d.ts +47 -2
- package/lib/typescript/src/components/dialog/index.d.ts.map +1 -1
- package/lib/typescript/src/components/line-chart/index.d.ts.map +1 -1
- package/lib/typescript/src/components/live-line-chart/index.d.ts +293 -0
- package/lib/typescript/src/components/live-line-chart/index.d.ts.map +1 -0
- package/lib/typescript/src/components/plot/index.d.ts +471 -0
- package/lib/typescript/src/components/plot/index.d.ts.map +1 -0
- package/lib/typescript/src/components/polar-area-chart/index.d.ts +259 -0
- package/lib/typescript/src/components/polar-area-chart/index.d.ts.map +1 -0
- package/lib/typescript/src/components/radar-chart/index.d.ts.map +1 -1
- 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/selection-mode/index.d.ts +82 -6
- package/lib/typescript/src/components/selection-mode/index.d.ts.map +1 -1
- package/lib/typescript/src/components/sortable/index.d.ts.map +1 -1
- package/lib/typescript/src/index.d.ts +4 -0
- package/lib/typescript/src/index.d.ts.map +1 -1
- package/lib/typescript/src/utils/chart.d.ts +15 -14
- package/lib/typescript/src/utils/chart.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/components/area-chart/index.tsx +14 -4
- package/src/components/bar-chart/index.tsx +14 -4
- package/src/components/candlestick-chart/index.tsx +20 -7
- package/src/components/card/index.tsx +32 -7
- package/src/components/dialog/index.tsx +55 -2
- package/src/components/hex-chart/index.tsx +2 -2
- package/src/components/line-chart/index.tsx +14 -4
- package/src/components/live-line-chart/index.tsx +1294 -0
- package/src/components/pie-chart/index.tsx +2 -2
- package/src/components/plot/index.tsx +1564 -0
- package/src/components/polar-area-chart/index.tsx +928 -0
- package/src/components/radar-chart/index.tsx +14 -4
- package/src/components/ring-chart/index.tsx +2 -2
- package/src/components/section-rail/index.tsx +133 -14
- package/src/components/selection-mode/index.tsx +163 -26
- package/src/components/sortable/index.tsx +15 -5
- package/src/index.ts +81 -0
- package/src/utils/chart.ts +44 -0
- package/theme.css +24 -0
|
@@ -85,7 +85,7 @@ const LABEL_ROOM = { x: 62, y: 26 };
|
|
|
85
85
|
const BARE_ROOM = { x: 6, y: 6 };
|
|
86
86
|
|
|
87
87
|
/** How long the polygons take to grow out of the centre. */
|
|
88
|
-
const REVEAL_DURATION =
|
|
88
|
+
const REVEAL_DURATION = 620;
|
|
89
89
|
/** …and how long one profile takes to travel to the next when the data changes. */
|
|
90
90
|
const MORPH_DURATION = 420;
|
|
91
91
|
|
|
@@ -312,17 +312,27 @@ const RadarChartRoot = forwardRef<RadarChartHandle, RadarChartProps>(
|
|
|
312
312
|
reveal.value = 0;
|
|
313
313
|
reveal.value = withTiming(1, {
|
|
314
314
|
duration: animationDuration,
|
|
315
|
-
easing: Easing.
|
|
315
|
+
easing: Easing.out(Easing.cubic),
|
|
316
316
|
});
|
|
317
317
|
},
|
|
318
318
|
[reducedMotion, animationDuration, reveal]
|
|
319
319
|
);
|
|
320
320
|
|
|
321
321
|
useEffect(() => {
|
|
322
|
-
|
|
322
|
+
/*
|
|
323
|
+
* Going back to `loading` arms the reveal again. Without this a chart
|
|
324
|
+
* that is refetched comes back fully drawn on the frame the data lands,
|
|
325
|
+
* which reads as the loading state having been for nothing.
|
|
326
|
+
*/
|
|
327
|
+
if (loading) {
|
|
328
|
+
revealed.current = false;
|
|
329
|
+
reveal.value = 0;
|
|
330
|
+
return;
|
|
331
|
+
}
|
|
332
|
+
if (revealed.current || radius <= 0 || !data.length) return;
|
|
323
333
|
revealed.current = true;
|
|
324
334
|
playReveal();
|
|
325
|
-
}, [loading, radius, data.length, playReveal]);
|
|
335
|
+
}, [loading, radius, data.length, playReveal, reveal]);
|
|
326
336
|
|
|
327
337
|
useImperativeHandle(ref, () => ({ replay: playReveal }), [playReveal]);
|
|
328
338
|
|
|
@@ -167,7 +167,7 @@ const RingChartRoot = forwardRef<RingChartHandle, RingChartProps>(function RingC
|
|
|
167
167
|
ringGap = 6,
|
|
168
168
|
startAngle = 0,
|
|
169
169
|
endAngle = 360,
|
|
170
|
-
animationDuration =
|
|
170
|
+
animationDuration = 700,
|
|
171
171
|
activeIndex: activeIndexProp,
|
|
172
172
|
onActiveIndexChange,
|
|
173
173
|
children,
|
|
@@ -219,7 +219,7 @@ const RingChartRoot = forwardRef<RingChartHandle, RingChartProps>(function RingC
|
|
|
219
219
|
reveal.value = 0;
|
|
220
220
|
reveal.value = withTiming(1, {
|
|
221
221
|
duration: animationDuration,
|
|
222
|
-
easing: Easing.
|
|
222
|
+
easing: Easing.out(Easing.cubic),
|
|
223
223
|
});
|
|
224
224
|
},
|
|
225
225
|
[reducedMotion, animationDuration, reveal]
|
|
@@ -28,7 +28,9 @@
|
|
|
28
28
|
* everywhere it is not drawing, so the content underneath still scrolls.
|
|
29
29
|
*/
|
|
30
30
|
import {
|
|
31
|
+
Children,
|
|
31
32
|
createContext,
|
|
33
|
+
isValidElement,
|
|
32
34
|
useCallback,
|
|
33
35
|
useContext,
|
|
34
36
|
useEffect,
|
|
@@ -59,7 +61,28 @@ const SPRING = { damping: 20, stiffness: 260, mass: 0.6 } as const;
|
|
|
59
61
|
const BAR_WIDTH = 16;
|
|
60
62
|
const BAR_LEVEL_STEP = 4;
|
|
61
63
|
/** How much wider the active bar gets, so position is readable at a glance. */
|
|
62
|
-
const BAR_ACTIVE_EXTRA =
|
|
64
|
+
const BAR_ACTIVE_EXTRA = 12;
|
|
65
|
+
/**
|
|
66
|
+
* How much of that extra the bars either side of the active one keep.
|
|
67
|
+
*
|
|
68
|
+
* The rail is read by shape, and one long bar in a column of identical short
|
|
69
|
+
* ones only says *which* one — it takes a second look to see where that is in
|
|
70
|
+
* the run. A step down on each side gives the active bar a slope to sit on, so
|
|
71
|
+
* position is legible from the silhouette alone.
|
|
72
|
+
*
|
|
73
|
+
* Only the immediate neighbours. Two steps of falloff is a taper down the whole
|
|
74
|
+
* rail, which reads as a gradient the bars happen to sit in rather than as a
|
|
75
|
+
* mark on one of them.
|
|
76
|
+
*/
|
|
77
|
+
const BAR_NEIGHBOUR = 0.45;
|
|
78
|
+
/** Opacity of a bar with nothing near it, and how much proximity adds. */
|
|
79
|
+
const BAR_REST_OPACITY = 0.32;
|
|
80
|
+
/**
|
|
81
|
+
* How long a jump from the panel is given to arrive before the rail starts
|
|
82
|
+
* ticking again. Long enough for a scroll across a whole screen, short enough
|
|
83
|
+
* that a jump which never lands does not mute the next one.
|
|
84
|
+
*/
|
|
85
|
+
const JUMP_TIMEOUT = 900;
|
|
63
86
|
/** Indent per level in the expanded panel. */
|
|
64
87
|
const ITEM_INDENT = 12;
|
|
65
88
|
/**
|
|
@@ -93,6 +116,18 @@ interface SectionRailContextValue {
|
|
|
93
116
|
|
|
94
117
|
const SectionRailContext = createContext<SectionRailContextValue | null>(null);
|
|
95
118
|
|
|
119
|
+
/**
|
|
120
|
+
* The bars' values, in the order they are written.
|
|
121
|
+
*
|
|
122
|
+
* A bar knows whether it is the active one; it cannot know how far it is *from*
|
|
123
|
+
* the active one, which is what the falloff either side needs. The trigger
|
|
124
|
+
* reads it off its own children — the only place in the tree where the run is
|
|
125
|
+
* visible at all — rather than having each bar register itself, since a
|
|
126
|
+
* registration order is whatever order the rows happened to mount in and the
|
|
127
|
+
* rail is drawn in the order they were written.
|
|
128
|
+
*/
|
|
129
|
+
const SectionRailBarsContext = createContext<string[]>([]);
|
|
130
|
+
|
|
96
131
|
function useSectionRail(component: string): SectionRailContextValue {
|
|
97
132
|
const context = useContext(SectionRailContext);
|
|
98
133
|
if (!context) {
|
|
@@ -157,6 +192,7 @@ function SectionRailRoot({
|
|
|
157
192
|
const [internalOpen, setInternalOpen] = useState(defaultOpen);
|
|
158
193
|
const insets = useSafeAreaInsets();
|
|
159
194
|
const timer = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
195
|
+
const jumpTimer = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
160
196
|
|
|
161
197
|
const isControlled = valueProp !== undefined;
|
|
162
198
|
const value = isControlled ? valueProp : internalValue;
|
|
@@ -177,16 +213,46 @@ function SectionRailRoot({
|
|
|
177
213
|
|
|
178
214
|
const close = useCallback(() => setOpen(false), [setOpen]);
|
|
179
215
|
|
|
180
|
-
// A pending close must not fire after the
|
|
216
|
+
// A pending close, or a jump still being waited on, must not fire after the
|
|
217
|
+
// rail has gone.
|
|
181
218
|
useEffect(
|
|
182
219
|
() => () => {
|
|
183
220
|
if (timer.current) clearTimeout(timer.current);
|
|
221
|
+
if (jumpTimer.current) clearTimeout(jumpTimer.current);
|
|
184
222
|
},
|
|
185
223
|
[]
|
|
186
224
|
);
|
|
187
225
|
|
|
226
|
+
/*
|
|
227
|
+
* The section a tap asked for, while the screen is still travelling to it.
|
|
228
|
+
*
|
|
229
|
+
* A jump is animated, so the scroll handler driving `value` reports every
|
|
230
|
+
* section the screen passes on the way — each of which is a change of section
|
|
231
|
+
* as far as the tick below can tell. One tap became two or three ticks, and
|
|
232
|
+
* the panel lit up a row nobody chose. Nothing between the tap and the
|
|
233
|
+
* arrival is a section the reader went to, so nothing between them ticks.
|
|
234
|
+
*/
|
|
235
|
+
const jumpTo = useRef<string | null>(null);
|
|
236
|
+
|
|
237
|
+
const endJump = useCallback(() => {
|
|
238
|
+
jumpTo.current = null;
|
|
239
|
+
if (jumpTimer.current) {
|
|
240
|
+
clearTimeout(jumpTimer.current);
|
|
241
|
+
jumpTimer.current = null;
|
|
242
|
+
}
|
|
243
|
+
}, []);
|
|
244
|
+
|
|
188
245
|
const handleValueChange = useCallback(
|
|
189
246
|
(next: string) => {
|
|
247
|
+
jumpTo.current = next;
|
|
248
|
+
if (jumpTimer.current) clearTimeout(jumpTimer.current);
|
|
249
|
+
/*
|
|
250
|
+
* A backstop, not the normal way out. A jump to a section the scroller
|
|
251
|
+
* cannot reach — the last one on a screen shorter than the viewport —
|
|
252
|
+
* never arrives, and without this the rail would stay silent for good.
|
|
253
|
+
*/
|
|
254
|
+
jumpTimer.current = setTimeout(endJump, JUMP_TIMEOUT);
|
|
255
|
+
|
|
190
256
|
if (!isControlled) setInternalValue(next);
|
|
191
257
|
onValueChange?.(next);
|
|
192
258
|
|
|
@@ -197,7 +263,7 @@ function SectionRailRoot({
|
|
|
197
263
|
if (timer.current) clearTimeout(timer.current);
|
|
198
264
|
timer.current = setTimeout(() => setOpen(false), closeDelay);
|
|
199
265
|
},
|
|
200
|
-
[isControlled, onValueChange, closeDelay, setOpen]
|
|
266
|
+
[isControlled, onValueChange, closeDelay, setOpen, endJump]
|
|
201
267
|
);
|
|
202
268
|
|
|
203
269
|
/*
|
|
@@ -208,13 +274,21 @@ function SectionRailRoot({
|
|
|
208
274
|
*/
|
|
209
275
|
const ticked = useRef(false);
|
|
210
276
|
useEffect(() => {
|
|
211
|
-
if (!haptics) return;
|
|
212
277
|
if (!ticked.current) {
|
|
213
278
|
ticked.current = true;
|
|
214
279
|
return;
|
|
215
280
|
}
|
|
216
|
-
|
|
217
|
-
|
|
281
|
+
|
|
282
|
+
const target = jumpTo.current;
|
|
283
|
+
if (target !== null) {
|
|
284
|
+
// Still on the way. Only the section that was asked for ends the jump,
|
|
285
|
+
// and only it is worth feeling.
|
|
286
|
+
if (value !== target) return;
|
|
287
|
+
endJump();
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
if (haptics) selectionTick();
|
|
291
|
+
}, [value, haptics, endJump]);
|
|
218
292
|
|
|
219
293
|
const context = useMemo(
|
|
220
294
|
() => ({
|
|
@@ -269,6 +343,22 @@ export interface SectionRailTriggerProps extends ViewProps {
|
|
|
269
343
|
function SectionRailTrigger({ className, children, ...props }: SectionRailTriggerProps) {
|
|
270
344
|
const { open, setOpen, placement } = useSectionRail('SectionRail.Trigger');
|
|
271
345
|
|
|
346
|
+
/*
|
|
347
|
+
* The run of bars, read off the children. Only the values are taken, so a bar
|
|
348
|
+
* wrapped in anything of the caller's is simply not found and falls back to
|
|
349
|
+
* drawing itself from `selected` alone.
|
|
350
|
+
*/
|
|
351
|
+
const values = useMemo(() => {
|
|
352
|
+
const found: string[] = [];
|
|
353
|
+
Children.forEach(children, (child) => {
|
|
354
|
+
if (isValidElement(child) && child.type === SectionRailBar) {
|
|
355
|
+
const value = (child.props as SectionRailBarProps).value;
|
|
356
|
+
if (typeof value === 'string') found.push(value);
|
|
357
|
+
}
|
|
358
|
+
});
|
|
359
|
+
return found;
|
|
360
|
+
}, [children]);
|
|
361
|
+
|
|
272
362
|
return (
|
|
273
363
|
<Pressable
|
|
274
364
|
accessibilityRole="button"
|
|
@@ -284,7 +374,9 @@ function SectionRailTrigger({ className, children, ...props }: SectionRailTrigge
|
|
|
284
374
|
)}
|
|
285
375
|
{...props}
|
|
286
376
|
>
|
|
287
|
-
{
|
|
377
|
+
<SectionRailBarsContext.Provider value={values}>
|
|
378
|
+
{textChildren(children)}
|
|
379
|
+
</SectionRailBarsContext.Provider>
|
|
288
380
|
</Pressable>
|
|
289
381
|
);
|
|
290
382
|
}
|
|
@@ -297,29 +389,56 @@ export interface SectionRailBarProps {
|
|
|
297
389
|
level?: number;
|
|
298
390
|
}
|
|
299
391
|
|
|
300
|
-
/**
|
|
392
|
+
/**
|
|
393
|
+
* One section, drawn as a bar.
|
|
394
|
+
*
|
|
395
|
+
* Three lengths rather than two: the active bar is longest and brightest, the
|
|
396
|
+
* bars either side of it keep a share of that, and everything further away sits
|
|
397
|
+
* at the resting length. What the reader gets from the extra step is *where* in
|
|
398
|
+
* the run they are without counting bars — the slope points at the middle of it.
|
|
399
|
+
*/
|
|
301
400
|
function SectionRailBar({ className, value, level = 0 }: SectionRailBarProps) {
|
|
302
401
|
const { value: active } = useSectionRail('SectionRail.Bar');
|
|
402
|
+
const values = useContext(SectionRailBarsContext);
|
|
303
403
|
const selected = active === value;
|
|
304
404
|
|
|
305
405
|
const restColor = useCSSVariable('--color-muted-foreground');
|
|
306
406
|
const activeColor = useCSSVariable('--color-foreground');
|
|
307
407
|
|
|
408
|
+
/*
|
|
409
|
+
* How near this bar is to the one that is active: 1 for the active bar itself,
|
|
410
|
+
* a share of it for its neighbours, 0 for the rest. A bar the trigger did not
|
|
411
|
+
* find — one the caller wrapped in something of their own — has no position to
|
|
412
|
+
* measure from, so it falls back to the plain selected-or-not it always had.
|
|
413
|
+
*/
|
|
414
|
+
const index = values.indexOf(value);
|
|
415
|
+
const activeIndex = active === undefined ? -1 : values.indexOf(active);
|
|
416
|
+
const proximity =
|
|
417
|
+
index < 0 || activeIndex < 0
|
|
418
|
+
? selected
|
|
419
|
+
? 1
|
|
420
|
+
: 0
|
|
421
|
+
: Math.abs(index - activeIndex) === 0
|
|
422
|
+
? 1
|
|
423
|
+
: Math.abs(index - activeIndex) === 1
|
|
424
|
+
? BAR_NEIGHBOUR
|
|
425
|
+
: 0;
|
|
426
|
+
|
|
308
427
|
const base = Math.max(BAR_WIDTH - level * BAR_LEVEL_STEP, 6);
|
|
309
|
-
const progress = useSharedValue(
|
|
428
|
+
const progress = useSharedValue(proximity);
|
|
310
429
|
|
|
311
430
|
useEffect(() => {
|
|
312
|
-
progress.value = withSpring(
|
|
313
|
-
}, [
|
|
431
|
+
progress.value = withSpring(proximity, SPRING);
|
|
432
|
+
}, [proximity, progress]);
|
|
314
433
|
|
|
315
434
|
const idle = typeof restColor === 'string' ? restColor : '#818181';
|
|
316
435
|
const on = typeof activeColor === 'string' ? activeColor : '#f5f5f5';
|
|
317
436
|
|
|
318
437
|
const style = useAnimatedStyle(() => ({
|
|
319
438
|
width: base + progress.value * BAR_ACTIVE_EXTRA,
|
|
320
|
-
// The
|
|
321
|
-
//
|
|
322
|
-
opacity:
|
|
439
|
+
// The far bars are dim on purpose — the rail is a position indicator, so
|
|
440
|
+
// only the part of it the reader is in is meant to be read.
|
|
441
|
+
opacity: BAR_REST_OPACITY + progress.value * (1 - BAR_REST_OPACITY),
|
|
323
442
|
backgroundColor: interpolateColor(progress.value, [0, 1], [idle, on]),
|
|
324
443
|
}));
|
|
325
444
|
|
|
@@ -66,7 +66,7 @@ import {
|
|
|
66
66
|
useState,
|
|
67
67
|
type ReactNode,
|
|
68
68
|
} from 'react';
|
|
69
|
-
import { Pressable, View, type ViewProps } from 'react-native';
|
|
69
|
+
import { Pressable, ScrollView, View, type ViewProps } from 'react-native';
|
|
70
70
|
import Animated, {
|
|
71
71
|
FadeIn,
|
|
72
72
|
FadeOut,
|
|
@@ -83,6 +83,7 @@ import { useCSSVariable } from 'uniwind';
|
|
|
83
83
|
import { useBackHandler } from '../../hooks/use-back-handler';
|
|
84
84
|
import { CheckIcon, IconColorProvider, XIcon } from '../../icons';
|
|
85
85
|
import { AnimatedPressable } from '../../primitives/animated-pressable';
|
|
86
|
+
import { Collapse } from '../../primitives/collapse';
|
|
86
87
|
import { Text, textChildren } from '../../primitives/text';
|
|
87
88
|
import { cn } from '../../utils/cn';
|
|
88
89
|
import { selectionTick } from '../../utils/haptics';
|
|
@@ -91,6 +92,15 @@ import { BottomSheet } from '../bottom-sheet';
|
|
|
91
92
|
/** How long the circle takes to come and go, in milliseconds. */
|
|
92
93
|
const REVEAL_DURATION = 180;
|
|
93
94
|
|
|
95
|
+
/**
|
|
96
|
+
* The circle's width, and the gap after it — both in points, and both mirrors
|
|
97
|
+
* of classes on the parts below (`h-6 w-6`, `gap-3`). Written out because the
|
|
98
|
+
* circle's slot is animated between nothing and its full size, and an animation
|
|
99
|
+
* needs the number rather than the class.
|
|
100
|
+
*/
|
|
101
|
+
const INDICATOR_SIZE = 24;
|
|
102
|
+
const ROW_GAP = 12;
|
|
103
|
+
|
|
94
104
|
/** The spring the tick lands with — the same one the checkbox uses. */
|
|
95
105
|
const TICK_SPRING = { damping: 15, stiffness: 300, mass: 0.5 } as const;
|
|
96
106
|
|
|
@@ -120,11 +130,23 @@ const selectionVariants = tv({
|
|
|
120
130
|
bar: 'flex-row items-stretch',
|
|
121
131
|
action: 'flex-1 items-center justify-center gap-1.5 px-2 py-3',
|
|
122
132
|
actionLabel: 'text-xs font-medium text-foreground',
|
|
133
|
+
groupLabel: 'pb-2 ps-1',
|
|
123
134
|
},
|
|
124
135
|
variants: {
|
|
125
136
|
selected: {
|
|
126
137
|
true: { circle: 'border-primary', ring: 'border-foreground' },
|
|
127
138
|
},
|
|
139
|
+
/**
|
|
140
|
+
* The action laid out along its label instead of above it.
|
|
141
|
+
*
|
|
142
|
+
* A screen's bar carries three or four actions side by side, so each one is
|
|
143
|
+
* a narrow column and the label belongs under the glyph. A sheet's footer
|
|
144
|
+
* usually carries one, full width — stacked there it is a tall block that
|
|
145
|
+
* costs the list a row of its own for no gain.
|
|
146
|
+
*/
|
|
147
|
+
compact: {
|
|
148
|
+
true: { action: 'flex-row gap-2 px-4 py-2.5', actionLabel: 'text-sm' },
|
|
149
|
+
},
|
|
128
150
|
destructive: {
|
|
129
151
|
true: { actionLabel: 'text-destructive' },
|
|
130
152
|
},
|
|
@@ -485,6 +507,33 @@ function SelectionModeItem({
|
|
|
485
507
|
|
|
486
508
|
const context = useMemo(() => ({ value }), [value]);
|
|
487
509
|
|
|
510
|
+
/*
|
|
511
|
+
* The circle's arrival and departure, as width rather than as opacity.
|
|
512
|
+
*
|
|
513
|
+
* Fading a circle that has already been taken out of the row's layout fades
|
|
514
|
+
* it over content that has finished moving: the row snaps left the frame the
|
|
515
|
+
* mode ends, and a ghost of the circle dissolves where it used to be. The
|
|
516
|
+
* space is what the reader sees change, so the space is what animates, and
|
|
517
|
+
* the negative margin takes the row's own gap with it — a zero-width child
|
|
518
|
+
* still costs a gap, so without it the row would still jump the last twelve
|
|
519
|
+
* points.
|
|
520
|
+
*/
|
|
521
|
+
const reveal = useSharedValue(showing ? 1 : 0);
|
|
522
|
+
|
|
523
|
+
useEffect(() => {
|
|
524
|
+
if (reducedMotion) {
|
|
525
|
+
reveal.value = showing ? 1 : 0;
|
|
526
|
+
return;
|
|
527
|
+
}
|
|
528
|
+
reveal.value = withTiming(showing ? 1 : 0, { duration: REVEAL_DURATION });
|
|
529
|
+
}, [showing, reducedMotion, reveal]);
|
|
530
|
+
|
|
531
|
+
const slotStyle = useAnimatedStyle(() => ({
|
|
532
|
+
width: reveal.value * INDICATOR_SIZE,
|
|
533
|
+
opacity: reveal.value,
|
|
534
|
+
marginEnd: (reveal.value - 1) * ROW_GAP,
|
|
535
|
+
}));
|
|
536
|
+
|
|
488
537
|
return (
|
|
489
538
|
<SelectionModeItemContext.Provider value={context}>
|
|
490
539
|
<Pressable
|
|
@@ -513,14 +562,9 @@ function SelectionModeItem({
|
|
|
513
562
|
>
|
|
514
563
|
{indicator === 'leading' ? (
|
|
515
564
|
<>
|
|
516
|
-
{
|
|
517
|
-
<
|
|
518
|
-
|
|
519
|
-
exiting={reducedMotion ? undefined : FadeOut.duration(REVEAL_DURATION)}
|
|
520
|
-
>
|
|
521
|
-
<SelectionModeIndicator value={value} />
|
|
522
|
-
</Animated.View>
|
|
523
|
-
) : null}
|
|
565
|
+
<Animated.View style={slotStyle} className="overflow-hidden">
|
|
566
|
+
<SelectionModeIndicator value={value} />
|
|
567
|
+
</Animated.View>
|
|
524
568
|
{/*
|
|
525
569
|
* `minWidth: 0` as well as growing. Without it a long title refuses
|
|
526
570
|
* to be narrower than its own text, pushes the row past the screen
|
|
@@ -555,11 +599,37 @@ export interface SelectionModeGroupProps extends ViewProps {
|
|
|
555
599
|
* For things recognised by sight rather than read — swatches, thumbnails,
|
|
556
600
|
* slides. A grid of six colours is one glance; the same six as rows is a
|
|
557
601
|
* scroll.
|
|
602
|
+
*
|
|
603
|
+
* Ignored when `horizontal` is set.
|
|
558
604
|
*/
|
|
559
605
|
columns?: number;
|
|
560
|
-
/**
|
|
606
|
+
/**
|
|
607
|
+
* Lay the items out in one row that scrolls sideways.
|
|
608
|
+
*
|
|
609
|
+
* For a strip of small things next to other controls — swatches above a
|
|
610
|
+
* slider, filters above a list. A grid of the same items claims as many rows
|
|
611
|
+
* as it needs and pushes everything below it off the sheet; a strip costs one
|
|
612
|
+
* row whatever the count.
|
|
613
|
+
*
|
|
614
|
+
* Wins over `columns`, which asks for the opposite arrangement.
|
|
615
|
+
*/
|
|
616
|
+
horizontal?: boolean;
|
|
617
|
+
/** How wide each item is in a horizontal strip, in points. */
|
|
618
|
+
itemWidth?: number;
|
|
619
|
+
/** Space between items in a grid or a strip, in points. */
|
|
561
620
|
gap?: number;
|
|
562
|
-
/**
|
|
621
|
+
/**
|
|
622
|
+
* A caption above the items, on the leading edge.
|
|
623
|
+
*
|
|
624
|
+
* Worth setting on anything picked by sight. A strip of colours with nothing
|
|
625
|
+
* in front of it is a row of circles the reader has to work out the purpose
|
|
626
|
+
* of, and a screen reader has nothing at all to announce it by — so this is
|
|
627
|
+
* also the group's accessibility label.
|
|
628
|
+
*/
|
|
629
|
+
label?: string;
|
|
630
|
+
/** Extra classes for that caption. */
|
|
631
|
+
labelClassName?: string;
|
|
632
|
+
/** Hairlines between stacked items. On by default; off in a grid or a strip. */
|
|
563
633
|
separators?: boolean;
|
|
564
634
|
children: ReactNode;
|
|
565
635
|
}
|
|
@@ -577,7 +647,11 @@ export interface SelectionModeGroupProps extends ViewProps {
|
|
|
577
647
|
function SelectionModeGroup({
|
|
578
648
|
className,
|
|
579
649
|
columns,
|
|
650
|
+
horizontal = false,
|
|
651
|
+
itemWidth = 44,
|
|
580
652
|
gap = 12,
|
|
653
|
+
label,
|
|
654
|
+
labelClassName,
|
|
581
655
|
separators = true,
|
|
582
656
|
children,
|
|
583
657
|
style,
|
|
@@ -586,6 +660,49 @@ function SelectionModeGroup({
|
|
|
586
660
|
const items = Children.toArray(children).filter(Boolean);
|
|
587
661
|
const slots = selectionVariants({});
|
|
588
662
|
|
|
663
|
+
/**
|
|
664
|
+
* The caption, and the wrapper that carries it. A group with no `label` is
|
|
665
|
+
* the view it always was, so nothing gains a level of nesting for a prop it
|
|
666
|
+
* did not pass.
|
|
667
|
+
*/
|
|
668
|
+
const captioned = (content: ReactNode) =>
|
|
669
|
+
label === undefined ? (
|
|
670
|
+
content
|
|
671
|
+
) : (
|
|
672
|
+
<View accessibilityLabel={label}>
|
|
673
|
+
<Text size="sm" weight="medium" muted className={cn(slots.groupLabel(), labelClassName)}>
|
|
674
|
+
{label}
|
|
675
|
+
</Text>
|
|
676
|
+
{content}
|
|
677
|
+
</View>
|
|
678
|
+
);
|
|
679
|
+
|
|
680
|
+
if (horizontal) {
|
|
681
|
+
/*
|
|
682
|
+
* `gap` on the row rather than padding inside each cell, which is the
|
|
683
|
+
* opposite of the grid below. A strip is not dividing a fixed width between
|
|
684
|
+
* its items, so there is no wrap to protect against — and the gap has to be
|
|
685
|
+
* between them rather than around them, or the strip starts inset from
|
|
686
|
+
* whatever it is in.
|
|
687
|
+
*/
|
|
688
|
+
return captioned(
|
|
689
|
+
<ScrollView
|
|
690
|
+
horizontal
|
|
691
|
+
showsHorizontalScrollIndicator={false}
|
|
692
|
+
contentContainerStyle={{ flexDirection: 'row', gap }}
|
|
693
|
+
style={style}
|
|
694
|
+
className={className}
|
|
695
|
+
{...props}
|
|
696
|
+
>
|
|
697
|
+
{items.map((item, index) => (
|
|
698
|
+
<View key={index} style={{ width: itemWidth }}>
|
|
699
|
+
{item}
|
|
700
|
+
</View>
|
|
701
|
+
))}
|
|
702
|
+
</ScrollView>
|
|
703
|
+
);
|
|
704
|
+
}
|
|
705
|
+
|
|
589
706
|
if (columns && columns > 0) {
|
|
590
707
|
/*
|
|
591
708
|
* The gap is padding inside each cell, not `gap` on the row.
|
|
@@ -597,7 +714,7 @@ function SelectionModeGroup({
|
|
|
597
714
|
* sits flush against whatever it is in.
|
|
598
715
|
*/
|
|
599
716
|
const half = gap / 2;
|
|
600
|
-
return (
|
|
717
|
+
return captioned(
|
|
601
718
|
<View
|
|
602
719
|
style={[{ flexDirection: 'row', flexWrap: 'wrap', margin: -half }, style]}
|
|
603
720
|
className={className}
|
|
@@ -612,7 +729,7 @@ function SelectionModeGroup({
|
|
|
612
729
|
);
|
|
613
730
|
}
|
|
614
731
|
|
|
615
|
-
return (
|
|
732
|
+
return captioned(
|
|
616
733
|
<View className={cn(slots.group(), className)} style={style} {...props}>
|
|
617
734
|
{items.map((item, index) => (
|
|
618
735
|
<View key={index}>
|
|
@@ -659,16 +776,23 @@ function SelectionModeHeader({
|
|
|
659
776
|
}: SelectionModeHeaderProps) {
|
|
660
777
|
const { active, exit, count, total, allSelected, selectAll, clear, sheet } =
|
|
661
778
|
useSelectionMode();
|
|
662
|
-
const reducedMotion = useReducedMotion();
|
|
663
779
|
const slots = selectionVariants({});
|
|
664
780
|
|
|
665
|
-
|
|
666
|
-
|
|
781
|
+
/*
|
|
782
|
+
* Collapsed rather than unmounted.
|
|
783
|
+
*
|
|
784
|
+
* A fade on a view that has already been taken out of the flow fades nothing
|
|
785
|
+
* — the list under it has jumped up 56 points on the frame the mode ended,
|
|
786
|
+
* and what is left dissolving is a header nobody is looking at any more.
|
|
787
|
+
* Giving up the height *is* the transition, so that is the part that animates.
|
|
788
|
+
*/
|
|
667
789
|
return (
|
|
668
|
-
<
|
|
669
|
-
|
|
670
|
-
|
|
790
|
+
<Collapse
|
|
791
|
+
open={active}
|
|
792
|
+
duration={REVEAL_DURATION}
|
|
671
793
|
className={cn(slots.header(), className)}
|
|
794
|
+
accessibilityElementsHidden={!active}
|
|
795
|
+
importantForAccessibility={active ? 'auto' : 'no-hide-descendants'}
|
|
672
796
|
{...props}
|
|
673
797
|
>
|
|
674
798
|
{children ? (
|
|
@@ -718,7 +842,7 @@ function SelectionModeHeader({
|
|
|
718
842
|
</View>
|
|
719
843
|
</>
|
|
720
844
|
)}
|
|
721
|
-
</
|
|
845
|
+
</Collapse>
|
|
722
846
|
);
|
|
723
847
|
}
|
|
724
848
|
|
|
@@ -854,12 +978,13 @@ function SelectionModeAction({
|
|
|
854
978
|
children,
|
|
855
979
|
...props
|
|
856
980
|
}: SelectionModeActionProps) {
|
|
857
|
-
const { selected, exit, count } = useSelectionMode();
|
|
981
|
+
const { selected, exit, count, sheet } = useSelectionMode();
|
|
858
982
|
const destructiveColor = useCSSVariable('--color-destructive');
|
|
859
983
|
// Nothing picked is nothing to act on, so the action is off rather than
|
|
860
984
|
// pressable-and-inert.
|
|
861
985
|
const off = disabled || count === 0;
|
|
862
|
-
|
|
986
|
+
// A sheet's footer is one action wide, not four, so it lies down.
|
|
987
|
+
const slots = selectionVariants({ destructive, disabled: off, compact: sheet });
|
|
863
988
|
|
|
864
989
|
return (
|
|
865
990
|
<AnimatedPressable
|
|
@@ -902,10 +1027,15 @@ export interface SelectionModeSheetProps {
|
|
|
902
1027
|
/**
|
|
903
1028
|
* How tall the sheet opens.
|
|
904
1029
|
*
|
|
905
|
-
* `
|
|
1030
|
+
* `full` by default, and deliberately not `auto`. A sheet that sizes to its
|
|
906
1031
|
* content gives its scrolling body no height to fill, and a list inside a box
|
|
907
1032
|
* of no height draws nothing — which looks like an empty sheet rather than
|
|
908
|
-
* like a missing style.
|
|
1033
|
+
* like a missing style.
|
|
1034
|
+
*
|
|
1035
|
+
* Full rather than half because a picker spends a header and a footer before
|
|
1036
|
+
* it draws a single row. At half a screen that leaves four or five rows for
|
|
1037
|
+
* the thing the sheet was opened to do, and the reader scrolls a list that
|
|
1038
|
+
* would have fitted. Pass `half` for a sheet of two or three choices.
|
|
909
1039
|
*/
|
|
910
1040
|
size?: 'auto' | 'half' | 'full';
|
|
911
1041
|
/**
|
|
@@ -949,7 +1079,7 @@ function SelectionModeSheet({
|
|
|
949
1079
|
onOpenChange,
|
|
950
1080
|
title = 'Select',
|
|
951
1081
|
hideSelectAll = false,
|
|
952
|
-
size = '
|
|
1082
|
+
size = 'full',
|
|
953
1083
|
children,
|
|
954
1084
|
}: SelectionModeSheetProps) {
|
|
955
1085
|
const parent = useSelectionMode();
|
|
@@ -978,7 +1108,14 @@ function SelectionModeSheet({
|
|
|
978
1108
|
|
|
979
1109
|
return (
|
|
980
1110
|
<BottomSheet open={open} defaultOpen={defaultOpen} onOpenChange={onOpenChange}>
|
|
981
|
-
|
|
1111
|
+
{/*
|
|
1112
|
+
* No close button. The sheet's own sits in the top trailing corner, which
|
|
1113
|
+
* is where the header puts "All" — two targets a few points apart, one of
|
|
1114
|
+
* which throws the selection away. The sheet is still dismissed by its
|
|
1115
|
+
* handle, by the scrim and by the back gesture, which is the same reason
|
|
1116
|
+
* the header does not draw an X of its own in here.
|
|
1117
|
+
*/}
|
|
1118
|
+
<BottomSheet.Content size={size} showClose={false} className={className}>
|
|
982
1119
|
{/*
|
|
983
1120
|
* The provider goes *inside* the sheet's content, not around the sheet.
|
|
984
1121
|
*
|
|
@@ -563,16 +563,26 @@ function SortableRoot({
|
|
|
563
563
|
* the transforms that were holding rows in their new slots are no longer
|
|
564
564
|
* needed — the layout puts them there now. Both orders are reset together,
|
|
565
565
|
* which is what makes the re-render after a drop invisible.
|
|
566
|
+
*
|
|
567
|
+
* During render, and not in an effect. An effect runs *after* the commit that
|
|
568
|
+
* moved the rows, so for the frame in between, the dropped row sat in its new
|
|
569
|
+
* slot still carrying the transform that had carried it there — drawn a whole
|
|
570
|
+
* row's height away from where it belongs, which is what the flash after a
|
|
571
|
+
* drop was. Written here it lands on the same commit as the reorder, and there
|
|
572
|
+
* is no frame in which the two disagree.
|
|
573
|
+
*
|
|
574
|
+
* Writing a shared value is not React state, so this is not a render with a
|
|
575
|
+
* side effect on the tree; the ref is only there to make it happen once per
|
|
576
|
+
* change of order rather than on every render of the screen around it.
|
|
566
577
|
*/
|
|
567
|
-
|
|
578
|
+
const applied = useRef(key);
|
|
579
|
+
if (applied.current !== key) {
|
|
580
|
+
applied.current = key;
|
|
568
581
|
order.value = value;
|
|
569
582
|
rendered.value = value;
|
|
570
583
|
translate.value = 0;
|
|
571
584
|
lift.value = 0;
|
|
572
|
-
|
|
573
|
-
// this on every render of the screen around it.
|
|
574
|
-
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
575
|
-
}, [key, order, rendered, translate, lift]);
|
|
585
|
+
}
|
|
576
586
|
|
|
577
587
|
const indices = useMemo(() => {
|
|
578
588
|
const map = new Map<string, number>();
|