panelui-native 0.75.1 → 0.76.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/ai-input/ai-input-growth.js +62 -0
- package/lib/module/components/ai-input/ai-input-growth.js.map +1 -0
- package/lib/module/components/ai-input/index.js +1568 -0
- package/lib/module/components/ai-input/index.js.map +1 -0
- package/lib/module/components/alert/index.js +1 -1
- package/lib/module/components/alert/index.js.map +1 -1
- package/lib/module/components/avatar/avatar-source.js +15 -0
- package/lib/module/components/avatar/avatar-source.js.map +1 -0
- package/lib/module/components/avatar/index.js +32 -31
- package/lib/module/components/avatar/index.js.map +1 -1
- package/lib/module/components/bottom-sheet/index.js +3 -2
- package/lib/module/components/bottom-sheet/index.js.map +1 -1
- package/lib/module/components/button-group/index.js +2 -3
- package/lib/module/components/button-group/index.js.map +1 -1
- package/lib/module/components/carousel/index.js +10 -7
- package/lib/module/components/carousel/index.js.map +1 -1
- package/lib/module/components/collapsible/index.js +1 -1
- package/lib/module/components/collapsible/index.js.map +1 -1
- package/lib/module/components/field/index.js +7 -8
- package/lib/module/components/field/index.js.map +1 -1
- package/lib/module/components/label/index.js +1 -1
- package/lib/module/components/label/index.js.map +1 -1
- package/lib/module/components/marquee/index.js +36 -11
- package/lib/module/components/marquee/index.js.map +1 -1
- package/lib/module/components/marquee/marquee-math.js +22 -6
- package/lib/module/components/marquee/marquee-math.js.map +1 -1
- package/lib/module/components/plan/index.js +2 -1
- package/lib/module/components/plan/index.js.map +1 -1
- package/lib/module/components/plot/index.js +27 -9
- package/lib/module/components/plot/index.js.map +1 -1
- package/lib/module/components/search-bar/index.js +11 -3
- package/lib/module/components/search-bar/index.js.map +1 -1
- package/lib/module/components/split-view/index.js +26 -27
- package/lib/module/components/split-view/index.js.map +1 -1
- package/lib/module/components/split-view/split-view-lifecycle.js +32 -0
- package/lib/module/components/split-view/split-view-lifecycle.js.map +1 -0
- package/lib/module/components/splitter/index.js +7 -13
- package/lib/module/components/splitter/index.js.map +1 -1
- package/lib/module/components/splitter/splitter-math.js +14 -0
- package/lib/module/components/splitter/splitter-math.js.map +1 -1
- package/lib/module/components/tabs/index.js +1 -1
- package/lib/module/components/tabs/index.js.map +1 -1
- package/lib/module/components/timeline/index.js +156 -42
- package/lib/module/components/timeline/index.js.map +1 -1
- package/lib/module/components/timeline/timeline-geometry.js +11 -0
- package/lib/module/components/timeline/timeline-geometry.js.map +1 -1
- package/lib/module/components/toast/index.js +1 -1
- package/lib/module/components/toast/index.js.map +1 -1
- package/lib/module/components/tooltip/index.js +2 -2
- package/lib/module/components/tooltip/index.js.map +1 -1
- package/lib/module/components/tree/index.js +2 -2
- package/lib/module/components/tree/index.js.map +1 -1
- package/lib/module/components/typography/index.js +2 -1
- package/lib/module/components/typography/index.js.map +1 -1
- package/lib/module/components/waterfall-chart/index.js +22 -3
- package/lib/module/components/waterfall-chart/index.js.map +1 -1
- package/lib/module/hooks/use-disclosure.js +10 -8
- package/lib/module/hooks/use-disclosure.js.map +1 -1
- package/lib/module/icons/index.js +135 -0
- package/lib/module/icons/index.js.map +1 -1
- package/lib/module/index.js +5 -1
- package/lib/module/index.js.map +1 -1
- package/lib/module/primitives/controllable-state.js +61 -0
- package/lib/module/primitives/controllable-state.js.map +1 -0
- package/lib/module/primitives/finite-chart.js +15 -0
- package/lib/module/primitives/finite-chart.js.map +1 -0
- package/lib/module/primitives/glass.js +136 -0
- package/lib/module/primitives/glass.js.map +1 -0
- package/lib/module/primitives/scrim.js +8 -1
- package/lib/module/primitives/scrim.js.map +1 -1
- package/lib/typescript/src/components/ai-input/ai-input-growth.d.ts +59 -0
- package/lib/typescript/src/components/ai-input/ai-input-growth.d.ts.map +1 -0
- package/lib/typescript/src/components/ai-input/index.d.ts +423 -0
- package/lib/typescript/src/components/ai-input/index.d.ts.map +1 -0
- package/lib/typescript/src/components/avatar/avatar-source.d.ts +4 -0
- package/lib/typescript/src/components/avatar/avatar-source.d.ts.map +1 -0
- package/lib/typescript/src/components/avatar/index.d.ts.map +1 -1
- package/lib/typescript/src/components/bottom-sheet/index.d.ts +10 -1
- package/lib/typescript/src/components/bottom-sheet/index.d.ts.map +1 -1
- package/lib/typescript/src/components/carousel/index.d.ts.map +1 -1
- package/lib/typescript/src/components/marquee/index.d.ts +15 -6
- package/lib/typescript/src/components/marquee/index.d.ts.map +1 -1
- package/lib/typescript/src/components/marquee/marquee-math.d.ts +6 -0
- package/lib/typescript/src/components/marquee/marquee-math.d.ts.map +1 -1
- package/lib/typescript/src/components/plan/index.d.ts.map +1 -1
- package/lib/typescript/src/components/plot/index.d.ts +2 -1
- package/lib/typescript/src/components/plot/index.d.ts.map +1 -1
- package/lib/typescript/src/components/search-bar/index.d.ts.map +1 -1
- package/lib/typescript/src/components/split-view/index.d.ts.map +1 -1
- package/lib/typescript/src/components/split-view/split-view-lifecycle.d.ts +14 -0
- package/lib/typescript/src/components/split-view/split-view-lifecycle.d.ts.map +1 -0
- package/lib/typescript/src/components/splitter/index.d.ts +2 -2
- package/lib/typescript/src/components/splitter/index.d.ts.map +1 -1
- package/lib/typescript/src/components/splitter/splitter-math.d.ts +2 -0
- package/lib/typescript/src/components/splitter/splitter-math.d.ts.map +1 -1
- package/lib/typescript/src/components/timeline/index.d.ts +24 -2
- package/lib/typescript/src/components/timeline/index.d.ts.map +1 -1
- package/lib/typescript/src/components/timeline/timeline-geometry.d.ts +2 -0
- package/lib/typescript/src/components/timeline/timeline-geometry.d.ts.map +1 -1
- package/lib/typescript/src/components/toast/index.d.ts.map +1 -1
- package/lib/typescript/src/components/waterfall-chart/index.d.ts +2 -1
- package/lib/typescript/src/components/waterfall-chart/index.d.ts.map +1 -1
- package/lib/typescript/src/hooks/use-disclosure.d.ts.map +1 -1
- package/lib/typescript/src/icons/index.d.ts +18 -0
- package/lib/typescript/src/icons/index.d.ts.map +1 -1
- package/lib/typescript/src/index.d.ts +3 -1
- package/lib/typescript/src/index.d.ts.map +1 -1
- package/lib/typescript/src/primitives/controllable-state.d.ts +33 -0
- package/lib/typescript/src/primitives/controllable-state.d.ts.map +1 -0
- package/lib/typescript/src/primitives/finite-chart.d.ts +5 -0
- package/lib/typescript/src/primitives/finite-chart.d.ts.map +1 -0
- package/lib/typescript/src/primitives/glass.d.ts +43 -0
- package/lib/typescript/src/primitives/glass.d.ts.map +1 -0
- package/lib/typescript/src/primitives/scrim.d.ts +7 -0
- package/lib/typescript/src/primitives/scrim.d.ts.map +1 -1
- package/package.json +7 -2
- package/src/components/ai-input/ai-input-growth.ts +65 -0
- package/src/components/ai-input/index.tsx +1937 -0
- package/src/components/alert/index.tsx +1 -1
- package/src/components/avatar/avatar-source.ts +18 -0
- package/src/components/avatar/index.tsx +35 -27
- package/src/components/bottom-sheet/index.tsx +15 -3
- package/src/components/button-group/index.tsx +1 -1
- package/src/components/carousel/index.tsx +10 -11
- package/src/components/collapsible/index.tsx +1 -1
- package/src/components/field/index.tsx +4 -4
- package/src/components/label/index.tsx +1 -1
- package/src/components/marquee/index.tsx +56 -12
- package/src/components/marquee/marquee-math.ts +26 -3
- package/src/components/plan/index.tsx +6 -2
- package/src/components/plot/index.tsx +37 -11
- package/src/components/search-bar/index.tsx +9 -3
- package/src/components/split-view/index.tsx +24 -33
- package/src/components/split-view/split-view-lifecycle.ts +43 -0
- package/src/components/splitter/index.tsx +7 -11
- package/src/components/splitter/splitter-math.ts +28 -0
- package/src/components/tabs/index.tsx +1 -1
- package/src/components/timeline/index.tsx +189 -34
- package/src/components/timeline/timeline-geometry.ts +11 -0
- package/src/components/toast/index.tsx +6 -1
- package/src/components/tooltip/index.tsx +1 -1
- package/src/components/tree/index.tsx +2 -2
- package/src/components/typography/index.tsx +2 -2
- package/src/components/waterfall-chart/index.tsx +37 -4
- package/src/hooks/use-disclosure.ts +7 -12
- package/src/icons/index.tsx +106 -0
- package/src/index.ts +27 -0
- package/src/primitives/controllable-state.ts +112 -0
- package/src/primitives/finite-chart.ts +14 -0
- package/src/primitives/glass.tsx +171 -0
- package/src/primitives/scrim.tsx +7 -1
|
@@ -294,28 +294,31 @@ export const SearchBar = forwardRef<TextInput, SearchBarProps>(
|
|
|
294
294
|
|
|
295
295
|
const handleSubmit = useCallback(
|
|
296
296
|
(event: NativeSyntheticEvent<TextInputSubmitEditingEventData>) => {
|
|
297
|
+
if (disabled) return;
|
|
297
298
|
// A return key is somebody saying they are done waiting, so the
|
|
298
299
|
// pending pause is spent rather than waited out.
|
|
299
300
|
flushSearchBarDebounce(debounceTimerRef, debouncedRef.current, text);
|
|
300
301
|
onSubmit?.(text);
|
|
301
302
|
onSubmitEditing?.(event);
|
|
302
303
|
},
|
|
303
|
-
[onSubmit, onSubmitEditing, text]
|
|
304
|
+
[disabled, onSubmit, onSubmitEditing, text]
|
|
304
305
|
);
|
|
305
306
|
|
|
306
307
|
const handleClear = useCallback(() => {
|
|
308
|
+
if (disabled) return;
|
|
307
309
|
setText('');
|
|
308
310
|
onClear?.();
|
|
309
311
|
// The keyboard stays: clearing a query is the start of the next one.
|
|
310
312
|
// Android takes focus away with the press, so it is asked back.
|
|
311
313
|
inputRef.current?.focus();
|
|
312
|
-
}, [onClear, setText]);
|
|
314
|
+
}, [disabled, onClear, setText]);
|
|
313
315
|
|
|
314
316
|
const handleCancel = useCallback(() => {
|
|
317
|
+
if (disabled) return;
|
|
315
318
|
setText('');
|
|
316
319
|
inputRef.current?.blur();
|
|
317
320
|
onCancel?.();
|
|
318
|
-
}, [onCancel, setText]);
|
|
321
|
+
}, [disabled, onCancel, setText]);
|
|
319
322
|
|
|
320
323
|
/*
|
|
321
324
|
* Cancel's width is measured once and driven from a shared value, so the
|
|
@@ -426,8 +429,11 @@ export const SearchBar = forwardRef<TextInput, SearchBarProps>(
|
|
|
426
429
|
<AnimatedPressable
|
|
427
430
|
onLayout={handleCancelLayout}
|
|
428
431
|
onPress={handleCancel}
|
|
432
|
+
disabled={disabled}
|
|
433
|
+
focusable={!disabled}
|
|
429
434
|
hitSlop={8}
|
|
430
435
|
accessibilityRole="button"
|
|
436
|
+
accessibilityState={{ disabled: !!disabled }}
|
|
431
437
|
className={slots.cancelButton()}
|
|
432
438
|
>
|
|
433
439
|
<Text className={slots.cancelLabel()}>{cancelLabel}</Text>
|
|
@@ -56,10 +56,10 @@ import {
|
|
|
56
56
|
DEFAULT_MIN_HEIGHT,
|
|
57
57
|
clamp,
|
|
58
58
|
nearestSnapIndex,
|
|
59
|
-
normalizeSnapIndex,
|
|
60
59
|
resolveLength,
|
|
61
60
|
resolveSnapPoints,
|
|
62
61
|
} from './split-view-math';
|
|
62
|
+
import { useSplitViewIndexLifecycle } from './split-view-lifecycle';
|
|
63
63
|
|
|
64
64
|
/** Settles the pane onto a snap point without overshooting past its limits. */
|
|
65
65
|
const SPRING = {
|
|
@@ -189,14 +189,16 @@ function SplitViewRoot({
|
|
|
189
189
|
[pointsKey]
|
|
190
190
|
);
|
|
191
191
|
|
|
192
|
-
const
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
192
|
+
const {
|
|
193
|
+
index: snapIndex,
|
|
194
|
+
requestIndex,
|
|
195
|
+
requestToken,
|
|
196
|
+
} = useSplitViewIndexLifecycle({
|
|
197
|
+
snapIndex: snapIndexProp,
|
|
198
|
+
defaultSnapIndex,
|
|
199
|
+
count: points.length,
|
|
200
|
+
onSnapIndexChange,
|
|
201
|
+
});
|
|
200
202
|
|
|
201
203
|
const onSnapRef = useRef(onSnap);
|
|
202
204
|
onSnapRef.current = onSnap;
|
|
@@ -207,30 +209,15 @@ function SplitViewRoot({
|
|
|
207
209
|
}, []);
|
|
208
210
|
|
|
209
211
|
/*
|
|
210
|
-
*
|
|
211
|
-
*
|
|
212
|
-
* to spring, to report, and to leave a controlled caller's index alone.
|
|
212
|
+
* Every modality makes the same request. The synchronization effect below
|
|
213
|
+
* moves only to the index the owner accepts, then reports that settlement.
|
|
213
214
|
*/
|
|
214
215
|
const moveTo = useCallback(
|
|
215
|
-
(index: number
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
if (!controlled) setInternalIndex(index);
|
|
220
|
-
onSnapIndexChange?.(index);
|
|
221
|
-
|
|
222
|
-
const finish = () => report(index, target);
|
|
223
|
-
if (options?.animated === false || !animate) {
|
|
224
|
-
topHeight.value = target;
|
|
225
|
-
finish();
|
|
226
|
-
return;
|
|
227
|
-
}
|
|
228
|
-
topHeight.value = withSpring(target, SPRING, (finished) => {
|
|
229
|
-
'worklet';
|
|
230
|
-
if (finished) runOnJS(finish)();
|
|
231
|
-
});
|
|
216
|
+
(index: number) => {
|
|
217
|
+
if (points[index] === undefined) return;
|
|
218
|
+
requestIndex(index);
|
|
232
219
|
},
|
|
233
|
-
[
|
|
220
|
+
[points, requestIndex]
|
|
234
221
|
);
|
|
235
222
|
|
|
236
223
|
const snapTo = useCallback((index: number) => moveTo(index), [moveTo]);
|
|
@@ -260,11 +247,15 @@ function SplitViewRoot({
|
|
|
260
247
|
report(snapIndex, target);
|
|
261
248
|
return;
|
|
262
249
|
}
|
|
263
|
-
|
|
250
|
+
const finish = () => report(snapIndex, target);
|
|
251
|
+
topHeight.value = withSpring(target, SPRING, (finished) => {
|
|
252
|
+
'worklet';
|
|
253
|
+
if (finished) runOnJS(finish)();
|
|
254
|
+
});
|
|
264
255
|
// Only when the resolved layout moves — a re-render that changes nothing
|
|
265
256
|
// must not restart a spring the reader is watching.
|
|
266
257
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
267
|
-
}, [points, snapIndex, animate, animateOnMount]);
|
|
258
|
+
}, [points, snapIndex, animate, animateOnMount, requestToken]);
|
|
268
259
|
|
|
269
260
|
const handleLayout = useCallback(
|
|
270
261
|
(event: LayoutChangeEvent) => {
|
|
@@ -471,6 +462,7 @@ function SplitViewDragArea({
|
|
|
471
462
|
return (
|
|
472
463
|
<GestureDetector gesture={pan}>
|
|
473
464
|
<View
|
|
465
|
+
{...props}
|
|
474
466
|
accessible
|
|
475
467
|
accessibilityRole="adjustable"
|
|
476
468
|
accessibilityLabel={accessibilityLabel}
|
|
@@ -493,7 +485,6 @@ function SplitViewDragArea({
|
|
|
493
485
|
}
|
|
494
486
|
className={dragArea({ className })}
|
|
495
487
|
onLayout={handleLayout}
|
|
496
|
-
{...props}
|
|
497
488
|
>
|
|
498
489
|
{children}
|
|
499
490
|
</View>
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { useCallback, useState } from 'react';
|
|
2
|
+
|
|
3
|
+
function normalizeIndex(index: number, count: number): number {
|
|
4
|
+
if (!Number.isFinite(index) || count <= 0) return 0;
|
|
5
|
+
return Math.max(0, Math.min(Math.round(index), count - 1));
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
interface SplitViewIndexLifecycleOptions {
|
|
9
|
+
snapIndex: number | undefined;
|
|
10
|
+
defaultSnapIndex: number;
|
|
11
|
+
count: number;
|
|
12
|
+
onSnapIndexChange?: (index: number) => void;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** Keeps requested indices intact until the measured snap-point list exists. */
|
|
16
|
+
export function useSplitViewIndexLifecycle({
|
|
17
|
+
snapIndex,
|
|
18
|
+
defaultSnapIndex,
|
|
19
|
+
count,
|
|
20
|
+
onSnapIndexChange,
|
|
21
|
+
}: SplitViewIndexLifecycleOptions) {
|
|
22
|
+
const controlled = snapIndex !== undefined;
|
|
23
|
+
const [internalIndex, setInternalIndex] = useState(defaultSnapIndex);
|
|
24
|
+
// Every request must settle, including one that resolves to the current index
|
|
25
|
+
// after a drag and a controlled request its owner rejects.
|
|
26
|
+
const [requestToken, setRequestToken] = useState(0);
|
|
27
|
+
|
|
28
|
+
const index = normalizeIndex(
|
|
29
|
+
controlled ? snapIndex : internalIndex,
|
|
30
|
+
count || 1
|
|
31
|
+
);
|
|
32
|
+
|
|
33
|
+
const requestIndex = useCallback(
|
|
34
|
+
(next: number) => {
|
|
35
|
+
if (!controlled) setInternalIndex(next);
|
|
36
|
+
setRequestToken((token) => token + 1);
|
|
37
|
+
onSnapIndexChange?.(next);
|
|
38
|
+
},
|
|
39
|
+
[controlled, onSnapIndexChange]
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
return { index, requestIndex, requestToken };
|
|
43
|
+
}
|
|
@@ -58,6 +58,7 @@ import {
|
|
|
58
58
|
isCollapsed,
|
|
59
59
|
layoutOffset,
|
|
60
60
|
normalizeConstraint,
|
|
61
|
+
resetLayout,
|
|
61
62
|
resizeLayout,
|
|
62
63
|
resolveLayout,
|
|
63
64
|
type SplitterConstraint,
|
|
@@ -388,6 +389,7 @@ function SplitterHandle({
|
|
|
388
389
|
disabled,
|
|
389
390
|
withGrip = true,
|
|
390
391
|
accessibilityLabel = 'Resize panels',
|
|
392
|
+
style,
|
|
391
393
|
...props
|
|
392
394
|
}: SplitterHandleProps) {
|
|
393
395
|
const context = useSplitter('Splitter.Handle');
|
|
@@ -468,17 +470,10 @@ function SplitterHandle({
|
|
|
468
470
|
* they already have between them.
|
|
469
471
|
*/
|
|
470
472
|
const reset = useCallback(() => {
|
|
471
|
-
const
|
|
472
|
-
const next = layout.value.slice();
|
|
473
|
-
const room = (next[pair] ?? 0) + (next[pair + 1] ?? 0);
|
|
474
|
-
const first = initial[pair] ?? 0;
|
|
475
|
-
const span = first + (initial[pair + 1] ?? 0);
|
|
476
|
-
if (span <= 0) return;
|
|
477
|
-
next[pair] = (first / span) * room;
|
|
478
|
-
next[pair + 1] = room - next[pair]!;
|
|
473
|
+
const next = resetLayout(layout.value, initial, boundary, constraints);
|
|
479
474
|
layout.value = next;
|
|
480
475
|
commit(next);
|
|
481
|
-
}, [boundary, commit, initial, layout]);
|
|
476
|
+
}, [boundary, commit, constraints, initial, layout]);
|
|
482
477
|
|
|
483
478
|
const doubleTap = useMemo(
|
|
484
479
|
() =>
|
|
@@ -548,6 +543,7 @@ function SplitterHandle({
|
|
|
548
543
|
return (
|
|
549
544
|
<GestureDetector gesture={gesture}>
|
|
550
545
|
<Animated.View
|
|
546
|
+
{...props}
|
|
551
547
|
accessible
|
|
552
548
|
accessibilityRole="adjustable"
|
|
553
549
|
accessibilityLabel={accessibilityLabel}
|
|
@@ -556,8 +552,7 @@ function SplitterHandle({
|
|
|
556
552
|
accessibilityActions={frozen ? undefined : [{ name: 'increment' }, { name: 'decrement' }]}
|
|
557
553
|
onAccessibilityAction={frozen ? undefined : onAccessibilityAction}
|
|
558
554
|
className={handle({ className })}
|
|
559
|
-
style={animatedStyle}
|
|
560
|
-
{...props}
|
|
555
|
+
style={[style, animatedStyle]}
|
|
561
556
|
>
|
|
562
557
|
<View className={line()} />
|
|
563
558
|
{withGrip ? <View className={grip()} /> : null}
|
|
@@ -582,6 +577,7 @@ export {
|
|
|
582
577
|
layoutOffset,
|
|
583
578
|
normalizeConstraint,
|
|
584
579
|
panelFloor,
|
|
580
|
+
resetLayout,
|
|
585
581
|
resizeLayout,
|
|
586
582
|
resolveLayout,
|
|
587
583
|
type SplitterConstraint,
|
|
@@ -227,6 +227,34 @@ export function resizeLayout(
|
|
|
227
227
|
return next;
|
|
228
228
|
}
|
|
229
229
|
|
|
230
|
+
/** Restores one pair's initial ratio without escaping its current constraints. */
|
|
231
|
+
export function resetLayout(
|
|
232
|
+
layout: number[],
|
|
233
|
+
initial: number[],
|
|
234
|
+
boundary: number,
|
|
235
|
+
constraints: SplitterConstraint[]
|
|
236
|
+
): number[] {
|
|
237
|
+
const next = layout.slice();
|
|
238
|
+
if (boundary < 0 || boundary + 1 >= next.length) return next;
|
|
239
|
+
|
|
240
|
+
const pair = next[boundary]! + next[boundary + 1]!;
|
|
241
|
+
const first = initial[boundary];
|
|
242
|
+
const second = initial[boundary + 1];
|
|
243
|
+
if (
|
|
244
|
+
!Number.isFinite(pair) ||
|
|
245
|
+
typeof first !== 'number' ||
|
|
246
|
+
!Number.isFinite(first) ||
|
|
247
|
+
typeof second !== 'number' ||
|
|
248
|
+
!Number.isFinite(second) ||
|
|
249
|
+
first + second <= 0
|
|
250
|
+
) {
|
|
251
|
+
return next;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
const target = (first / (first + second)) * pair;
|
|
255
|
+
return resizeLayout(next, boundary, target - next[boundary]!, constraints);
|
|
256
|
+
}
|
|
257
|
+
|
|
230
258
|
/** Where a seam sits, in percent, measured from the start of the splitter. */
|
|
231
259
|
export function layoutOffset(layout: number[], boundary: number): number {
|
|
232
260
|
'worklet';
|
|
@@ -778,7 +778,7 @@ function TabsList({ className, scrollable = false, children, ...props }: TabsLis
|
|
|
778
778
|
}, [scrollable, activeLayout?.x, activeLayout]);
|
|
779
779
|
|
|
780
780
|
const row = (
|
|
781
|
-
<View accessibilityRole="tablist" className={cn(list(), className)}
|
|
781
|
+
<View {...props} accessibilityRole="tablist" className={cn(list(), className)}>
|
|
782
782
|
{/* Nothing to slide: in `expanding` every tab draws its own pill, and the
|
|
783
783
|
open one is the shape that moves. */}
|
|
784
784
|
{variant === 'expanding' ? null : <TabsIndicator />}
|
|
@@ -45,14 +45,23 @@
|
|
|
45
45
|
*/
|
|
46
46
|
import {
|
|
47
47
|
Children,
|
|
48
|
+
cloneElement,
|
|
48
49
|
createContext,
|
|
49
50
|
forwardRef,
|
|
50
51
|
isValidElement,
|
|
51
52
|
useContext,
|
|
52
53
|
useMemo,
|
|
54
|
+
type ReactElement,
|
|
53
55
|
type ReactNode,
|
|
54
56
|
} from 'react';
|
|
55
|
-
import {
|
|
57
|
+
import {
|
|
58
|
+
ScrollView,
|
|
59
|
+
View,
|
|
60
|
+
type FlatListProps,
|
|
61
|
+
type ListRenderItemInfo,
|
|
62
|
+
type Text as RNText,
|
|
63
|
+
type ViewProps,
|
|
64
|
+
} from 'react-native';
|
|
56
65
|
import Animated, {
|
|
57
66
|
interpolate,
|
|
58
67
|
useAnimatedScrollHandler,
|
|
@@ -65,7 +74,7 @@ import { tv } from 'tailwind-variants';
|
|
|
65
74
|
import { useCSSVariable } from 'uniwind';
|
|
66
75
|
import { IconColorProvider } from '../../icons';
|
|
67
76
|
import { Text, type TextProps, textChildren } from '../../primitives/text';
|
|
68
|
-
import { timelineColumnWidth } from './timeline-geometry';
|
|
77
|
+
import { TIMELINE_WIDE_COLUMN, timelineColumnOffsets, timelineColumnWidth } from './timeline-geometry';
|
|
69
78
|
|
|
70
79
|
export type TimelineVariant = 'dot' | 'icon' | 'numbered' | 'card' | 'compact';
|
|
71
80
|
/** Semantic colour for a single event, independent of progress. */
|
|
@@ -74,6 +83,7 @@ export type TimelineTone = 'default' | 'info' | 'success' | 'warning' | 'danger'
|
|
|
74
83
|
export type TimelineOrientation = 'vertical' | 'horizontal';
|
|
75
84
|
|
|
76
85
|
const AnimatedScrollView = Animated.createAnimatedComponent(ScrollView);
|
|
86
|
+
const TimelineColumnOffsetContext = createContext<number | null>(null);
|
|
77
87
|
|
|
78
88
|
/*
|
|
79
89
|
* Horizontal geometry.
|
|
@@ -252,6 +262,8 @@ interface TimelineContextValue {
|
|
|
252
262
|
offsets: number[];
|
|
253
263
|
/** Horizontal only: false when the reader has asked for less movement. */
|
|
254
264
|
animate: boolean;
|
|
265
|
+
/** A virtualized list draws one rail segment per mounted cell. */
|
|
266
|
+
virtualized: boolean;
|
|
255
267
|
}
|
|
256
268
|
|
|
257
269
|
interface TimelineItemContextValue {
|
|
@@ -330,17 +342,28 @@ const TimelineRoot = forwardRef<View, TimelineProps>(
|
|
|
330
342
|
* themselves by. These are the snap points, and they are what an item reads
|
|
331
343
|
* to know how far it is from the reading edge.
|
|
332
344
|
*/
|
|
333
|
-
const
|
|
334
|
-
if (!horizontal) return [];
|
|
335
|
-
const
|
|
336
|
-
let sum = 0;
|
|
345
|
+
const columns = useMemo(() => {
|
|
346
|
+
if (!horizontal) return { offsets: [], children };
|
|
347
|
+
const items: ReactElement<TimelineItemProps>[] = [];
|
|
337
348
|
Children.forEach(children, (child) => {
|
|
338
|
-
if (
|
|
339
|
-
|
|
340
|
-
|
|
349
|
+
if (isValidElement(child) && child.type === TimelineItem) {
|
|
350
|
+
items.push(child as ReactElement<TimelineItemProps>);
|
|
351
|
+
}
|
|
341
352
|
});
|
|
342
|
-
|
|
353
|
+
const offsets = timelineColumnOffsets(items.map((item) => itemWidth(item.props)));
|
|
354
|
+
let position = 0;
|
|
355
|
+
const rendered = Children.map(children, (child) => {
|
|
356
|
+
if (!isValidElement(child) || child.type !== TimelineItem) return child;
|
|
357
|
+
const offset = offsets[position++] ?? 0;
|
|
358
|
+
return (
|
|
359
|
+
<TimelineColumnOffsetContext.Provider value={offset}>
|
|
360
|
+
{child}
|
|
361
|
+
</TimelineColumnOffsetContext.Provider>
|
|
362
|
+
);
|
|
363
|
+
});
|
|
364
|
+
return { offsets, children: rendered };
|
|
343
365
|
}, [children, horizontal]);
|
|
366
|
+
const { offsets } = columns;
|
|
344
367
|
|
|
345
368
|
const context = useMemo(
|
|
346
369
|
() => ({
|
|
@@ -350,6 +373,7 @@ const TimelineRoot = forwardRef<View, TimelineProps>(
|
|
|
350
373
|
scrollX,
|
|
351
374
|
offsets,
|
|
352
375
|
animate: !reducedMotion,
|
|
376
|
+
virtualized: false,
|
|
353
377
|
}),
|
|
354
378
|
[value, variant, orientation, scrollX, offsets, reducedMotion]
|
|
355
379
|
);
|
|
@@ -402,7 +426,7 @@ const TimelineRoot = forwardRef<View, TimelineProps>(
|
|
|
402
426
|
borderColor: typeof border === 'string' ? border : undefined,
|
|
403
427
|
}}
|
|
404
428
|
/>
|
|
405
|
-
{textChildren(children)}
|
|
429
|
+
{textChildren(columns.children)}
|
|
406
430
|
</View>
|
|
407
431
|
</AnimatedScrollView>
|
|
408
432
|
</View>
|
|
@@ -412,6 +436,128 @@ const TimelineRoot = forwardRef<View, TimelineProps>(
|
|
|
412
436
|
);
|
|
413
437
|
TimelineRoot.displayName = 'Timeline';
|
|
414
438
|
|
|
439
|
+
export interface TimelineListProps<T>
|
|
440
|
+
extends Omit<
|
|
441
|
+
FlatListProps<T>,
|
|
442
|
+
| 'data'
|
|
443
|
+
| 'renderItem'
|
|
444
|
+
| 'horizontal'
|
|
445
|
+
| 'getItemLayout'
|
|
446
|
+
| 'onScroll'
|
|
447
|
+
| 'snapToOffsets'
|
|
448
|
+
| 'CellRendererComponent'
|
|
449
|
+
> {
|
|
450
|
+
/** Complete event collection; rows outside the native window stay unmounted. */
|
|
451
|
+
data: readonly T[];
|
|
452
|
+
/** Render one `Timeline.Item`. Its step, width, and last marker are owned by the list. */
|
|
453
|
+
renderItem: (info: ListRenderItemInfo<T>) => ReactElement<TimelineItemProps>;
|
|
454
|
+
/** Width of each column, or a resolver for mixed-width histories. */
|
|
455
|
+
itemWidth?: number | ((item: T, index: number) => number);
|
|
456
|
+
/** Steps at or below this index render as completed. */
|
|
457
|
+
value?: number;
|
|
458
|
+
variant?: TimelineVariant;
|
|
459
|
+
snap?: boolean;
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
/**
|
|
463
|
+
* Opt-in bounded mount path for long horizontal histories. The compound
|
|
464
|
+
* `Timeline` API remains the simpler choice for short or mixed-content lists.
|
|
465
|
+
*/
|
|
466
|
+
function TimelineList<T>({
|
|
467
|
+
data,
|
|
468
|
+
renderItem,
|
|
469
|
+
itemWidth: requestedWidth = TIMELINE_WIDE_COLUMN,
|
|
470
|
+
value = 0,
|
|
471
|
+
variant = 'dot',
|
|
472
|
+
snap = true,
|
|
473
|
+
initialNumToRender = 6,
|
|
474
|
+
maxToRenderPerBatch = 6,
|
|
475
|
+
windowSize = 5,
|
|
476
|
+
className,
|
|
477
|
+
...props
|
|
478
|
+
}: TimelineListProps<T>) {
|
|
479
|
+
const reducedMotion = useReducedMotion();
|
|
480
|
+
const scrollX = useSharedValue(0);
|
|
481
|
+
const widths = useMemo(
|
|
482
|
+
() =>
|
|
483
|
+
data.map((item, index) =>
|
|
484
|
+
timelineColumnWidth(
|
|
485
|
+
typeof requestedWidth === 'function' ? requestedWidth(item, index) : requestedWidth,
|
|
486
|
+
true
|
|
487
|
+
)
|
|
488
|
+
),
|
|
489
|
+
[data, requestedWidth]
|
|
490
|
+
);
|
|
491
|
+
const offsets = useMemo(() => {
|
|
492
|
+
let offset = 0;
|
|
493
|
+
return widths.map((width) => {
|
|
494
|
+
const current = offset;
|
|
495
|
+
offset += width;
|
|
496
|
+
return current;
|
|
497
|
+
});
|
|
498
|
+
}, [widths]);
|
|
499
|
+
const context = useMemo(
|
|
500
|
+
() => ({
|
|
501
|
+
activeStep: value,
|
|
502
|
+
variant,
|
|
503
|
+
orientation: 'horizontal' as const,
|
|
504
|
+
scrollX,
|
|
505
|
+
offsets,
|
|
506
|
+
animate: !reducedMotion,
|
|
507
|
+
virtualized: true,
|
|
508
|
+
}),
|
|
509
|
+
[value, variant, scrollX, offsets, reducedMotion]
|
|
510
|
+
);
|
|
511
|
+
const { root } = timelineVariants({ variant, orientation: 'horizontal' });
|
|
512
|
+
const onScroll = useAnimatedScrollHandler((event) => {
|
|
513
|
+
scrollX.value = event.contentOffset.x;
|
|
514
|
+
});
|
|
515
|
+
|
|
516
|
+
return (
|
|
517
|
+
<TimelineContext.Provider value={context}>
|
|
518
|
+
<Animated.FlatList
|
|
519
|
+
data={data}
|
|
520
|
+
horizontal
|
|
521
|
+
accessibilityRole="list"
|
|
522
|
+
showsHorizontalScrollIndicator={false}
|
|
523
|
+
decelerationRate="fast"
|
|
524
|
+
snapToAlignment="start"
|
|
525
|
+
initialNumToRender={initialNumToRender}
|
|
526
|
+
maxToRenderPerBatch={maxToRenderPerBatch}
|
|
527
|
+
windowSize={windowSize}
|
|
528
|
+
removeClippedSubviews
|
|
529
|
+
className={root({ className })}
|
|
530
|
+
{...props}
|
|
531
|
+
getItemLayout={(_, index) => ({
|
|
532
|
+
index,
|
|
533
|
+
length: widths[index] ?? TIMELINE_WIDE_COLUMN,
|
|
534
|
+
offset: offsets[index] ?? 0,
|
|
535
|
+
})}
|
|
536
|
+
snapToOffsets={snap ? offsets : undefined}
|
|
537
|
+
onScroll={onScroll}
|
|
538
|
+
scrollEventThrottle={16}
|
|
539
|
+
renderItem={(info) => (
|
|
540
|
+
/*
|
|
541
|
+
* The list owns the order, so it hands each column its own left edge
|
|
542
|
+
* the same way the compound root does. An item reads its offset from
|
|
543
|
+
* here rather than indexing by `step`, and a virtualized item that
|
|
544
|
+
* was given none would sit at zero and never recede.
|
|
545
|
+
*/
|
|
546
|
+
<TimelineColumnOffsetContext.Provider value={offsets[info.index] ?? 0}>
|
|
547
|
+
{cloneElement(renderItem(info), {
|
|
548
|
+
step: info.index,
|
|
549
|
+
width: widths[info.index],
|
|
550
|
+
last: info.index === data.length - 1,
|
|
551
|
+
role: 'listitem',
|
|
552
|
+
})}
|
|
553
|
+
</TimelineColumnOffsetContext.Provider>
|
|
554
|
+
)}
|
|
555
|
+
/>
|
|
556
|
+
</TimelineContext.Provider>
|
|
557
|
+
);
|
|
558
|
+
}
|
|
559
|
+
TimelineList.displayName = 'Timeline.List';
|
|
560
|
+
|
|
415
561
|
export interface TimelineItemProps extends ViewProps {
|
|
416
562
|
className?: string;
|
|
417
563
|
/** Position in the sequence, zero-based. */
|
|
@@ -450,8 +596,16 @@ const TimelineItem = forwardRef<View, TimelineItemProps>(
|
|
|
450
596
|
},
|
|
451
597
|
ref
|
|
452
598
|
) => {
|
|
453
|
-
const { activeStep, variant, orientation, scrollX,
|
|
599
|
+
const { activeStep, variant, orientation, scrollX, animate, virtualized } =
|
|
454
600
|
useTimeline('Timeline.Item');
|
|
601
|
+
/*
|
|
602
|
+
* Where this column starts, from its position among the rendered items
|
|
603
|
+
* rather than from `step`. `step` is the semantic progress value: it may
|
|
604
|
+
* be sparse, repeated or reordered, and indexing the offsets by it lands a
|
|
605
|
+
* column on another column's snap point.
|
|
606
|
+
*/
|
|
607
|
+
const columnOffset = useContext(TimelineColumnOffsetContext);
|
|
608
|
+
const border = useCSSVariable('--color-border');
|
|
455
609
|
const isCompleted = completed ?? step <= activeStep;
|
|
456
610
|
const { item } = timelineVariants({
|
|
457
611
|
variant,
|
|
@@ -464,7 +618,7 @@ const TimelineItem = forwardRef<View, TimelineItemProps>(
|
|
|
464
618
|
const columnWidth = horizontal
|
|
465
619
|
? itemWidth({ step, width, children } as TimelineItemProps)
|
|
466
620
|
: undefined;
|
|
467
|
-
const offset =
|
|
621
|
+
const offset = columnOffset ?? 0;
|
|
468
622
|
|
|
469
623
|
const context = useMemo(
|
|
470
624
|
() => ({
|
|
@@ -479,8 +633,8 @@ const TimelineItem = forwardRef<View, TimelineItemProps>(
|
|
|
479
633
|
);
|
|
480
634
|
|
|
481
635
|
/*
|
|
482
|
-
* A column
|
|
483
|
-
*
|
|
636
|
+
* A column scales and drops as it leaves the reading edge, so the one being
|
|
637
|
+
* read is distinct without fading informative text below its contrast.
|
|
484
638
|
*
|
|
485
639
|
* One column's width either side, not two: a window wide enough to hold
|
|
486
640
|
* three columns near full strength is a row where nothing is picked out,
|
|
@@ -494,12 +648,11 @@ const TimelineItem = forwardRef<View, TimelineItemProps>(
|
|
|
494
648
|
* the rest.
|
|
495
649
|
*/
|
|
496
650
|
const columnStyle = useAnimatedStyle(() => {
|
|
497
|
-
if (!horizontal || !animate) return {
|
|
651
|
+
if (!horizontal || !animate) return { transform: [] };
|
|
498
652
|
const distance = Math.abs(scrollX.value - offset);
|
|
499
653
|
const window = Math.max(columnWidth ?? timelineColumnWidth(undefined, true), 1);
|
|
500
654
|
const away = interpolate(distance, [0, window], [0, 1], 'clamp');
|
|
501
655
|
return {
|
|
502
|
-
opacity: 1 - away * 0.4,
|
|
503
656
|
transform: [{ scale: 1 - away * 0.04 }, { translateY: away * 4 }],
|
|
504
657
|
};
|
|
505
658
|
});
|
|
@@ -522,6 +675,22 @@ const TimelineItem = forwardRef<View, TimelineItemProps>(
|
|
|
522
675
|
{...props}
|
|
523
676
|
style={[style, { width: columnWidth }, columnStyle]}
|
|
524
677
|
>
|
|
678
|
+
{virtualized && !last ? (
|
|
679
|
+
<View
|
|
680
|
+
accessibilityElementsHidden
|
|
681
|
+
importantForAccessibility="no-hide-descendants"
|
|
682
|
+
pointerEvents="none"
|
|
683
|
+
style={{
|
|
684
|
+
position: 'absolute',
|
|
685
|
+
left: 0,
|
|
686
|
+
right: 0,
|
|
687
|
+
top: HORIZONTAL_RAIL_TOP,
|
|
688
|
+
borderTopWidth: 1,
|
|
689
|
+
borderStyle: 'dashed',
|
|
690
|
+
borderColor: typeof border === 'string' ? border : undefined,
|
|
691
|
+
}}
|
|
692
|
+
/>
|
|
693
|
+
) : null}
|
|
525
694
|
{textChildren(children)}
|
|
526
695
|
</Animated.View>
|
|
527
696
|
</TimelineItemContext.Provider>
|
|
@@ -678,22 +847,7 @@ const TimelineContent = forwardRef<View, ViewProps & { className?: string }>(
|
|
|
678
847
|
const { body, panel } = timelineVariants({ variant, tone, completed, orientation });
|
|
679
848
|
const horizontal = orientation === 'horizontal';
|
|
680
849
|
|
|
681
|
-
/*
|
|
682
|
-
* Horizontal: the body recedes further than the column around it.
|
|
683
|
-
*
|
|
684
|
-
* The rail is a continuous thing and has to stay readable across the whole
|
|
685
|
-
* track — the years either side of the one being read are half of what a
|
|
686
|
-
* timeline is for. The prose under them is not: several columns of it at
|
|
687
|
-
* equal weight is a wall, and no amount of fading the column as a whole
|
|
688
|
-
* fixes that without taking the years down with it. So the body takes a
|
|
689
|
-
* second, steeper curve and the dates keep the first one.
|
|
690
|
-
*/
|
|
691
|
-
const bodyStyle = useAnimatedStyle(() => {
|
|
692
|
-
if (!horizontal || !animate) return { opacity: 1 };
|
|
693
|
-
const distance = Math.abs(scrollX.value - offset);
|
|
694
|
-
const window = Math.max(columnWidth, 1);
|
|
695
|
-
return { opacity: interpolate(distance, [0, window], [1, 0.3], 'clamp') };
|
|
696
|
-
});
|
|
850
|
+
/* Horizontal content stays fully opaque; scale and translation provide focus. */
|
|
697
851
|
|
|
698
852
|
const inner =
|
|
699
853
|
variant === 'card' ? (
|
|
@@ -715,7 +869,7 @@ const TimelineContent = forwardRef<View, ViewProps & { className?: string }>(
|
|
|
715
869
|
ref={ref}
|
|
716
870
|
className={body({ className })}
|
|
717
871
|
{...props}
|
|
718
|
-
style={
|
|
872
|
+
style={style}
|
|
719
873
|
>
|
|
720
874
|
{inner}
|
|
721
875
|
</Animated.View>
|
|
@@ -825,6 +979,7 @@ const TimelineStat = forwardRef<View, TimelineStatProps>(
|
|
|
825
979
|
TimelineStat.displayName = 'Timeline.Stat';
|
|
826
980
|
|
|
827
981
|
export const Timeline = Object.assign(TimelineRoot, {
|
|
982
|
+
List: TimelineList,
|
|
828
983
|
Item: TimelineItem,
|
|
829
984
|
Aside: TimelineAside,
|
|
830
985
|
Indicator: TimelineIndicator,
|
|
@@ -7,3 +7,14 @@ export function timelineColumnWidth(requested: number | undefined, filled: boole
|
|
|
7
7
|
}
|
|
8
8
|
return filled ? TIMELINE_WIDE_COLUMN : TIMELINE_NARROW_COLUMN;
|
|
9
9
|
}
|
|
10
|
+
|
|
11
|
+
/** Left edge of each column in render order. */
|
|
12
|
+
export function timelineColumnOffsets(widths: readonly number[]): number[] {
|
|
13
|
+
const offsets: number[] = [];
|
|
14
|
+
let sum = 0;
|
|
15
|
+
for (const width of widths) {
|
|
16
|
+
offsets.push(sum);
|
|
17
|
+
sum += width;
|
|
18
|
+
}
|
|
19
|
+
return offsets;
|
|
20
|
+
}
|
|
@@ -175,7 +175,12 @@ const ToastRoot = ({ className, variant = 'default', onHide, children, ...props
|
|
|
175
175
|
|
|
176
176
|
return (
|
|
177
177
|
<ToastContext.Provider value={context}>
|
|
178
|
-
<View
|
|
178
|
+
<View
|
|
179
|
+
{...props}
|
|
180
|
+
accessibilityRole="alert"
|
|
181
|
+
accessibilityLiveRegion="polite"
|
|
182
|
+
className={root({ className })}
|
|
183
|
+
>
|
|
179
184
|
{textChildren(children)}
|
|
180
185
|
</View>
|
|
181
186
|
</ToastContext.Provider>
|
|
@@ -623,7 +623,7 @@ function TooltipTitle({ className, ...props }: TooltipTitleProps) {
|
|
|
623
623
|
const { title } = tooltipVariants({ variant });
|
|
624
624
|
|
|
625
625
|
return (
|
|
626
|
-
<Text accessibilityRole="header" className={cn(title(), className)}
|
|
626
|
+
<Text {...props} accessibilityRole="header" className={cn(title(), className)} />
|
|
627
627
|
);
|
|
628
628
|
}
|
|
629
629
|
|