panelui-native 0.4.0 → 0.5.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +16 -8
- package/lib/module/components/accordion/index.js +312 -0
- package/lib/module/components/accordion/index.js.map +1 -0
- package/lib/module/components/avatar/index.js +58 -17
- package/lib/module/components/avatar/index.js.map +1 -1
- package/lib/module/components/badge/index.js +26 -5
- package/lib/module/components/badge/index.js.map +1 -1
- package/lib/module/components/button/index.js +51 -19
- package/lib/module/components/button/index.js.map +1 -1
- package/lib/module/components/checkbox/index.js +42 -14
- package/lib/module/components/checkbox/index.js.map +1 -1
- package/lib/module/components/frame/index.js +64 -13
- package/lib/module/components/frame/index.js.map +1 -1
- package/lib/module/components/timeline/index.js +658 -0
- package/lib/module/components/timeline/index.js.map +1 -0
- package/lib/module/components/toast/index.js +145 -55
- package/lib/module/components/toast/index.js.map +1 -1
- package/lib/module/icons/index.js +388 -27
- package/lib/module/icons/index.js.map +1 -1
- package/lib/module/index.js +3 -1
- package/lib/module/index.js.map +1 -1
- package/lib/typescript/src/components/accordion/index.d.ts +56 -0
- package/lib/typescript/src/components/accordion/index.d.ts.map +1 -0
- package/lib/typescript/src/components/avatar/index.d.ts +8 -1
- package/lib/typescript/src/components/avatar/index.d.ts.map +1 -1
- package/lib/typescript/src/components/badge/index.d.ts +38 -0
- package/lib/typescript/src/components/badge/index.d.ts.map +1 -1
- package/lib/typescript/src/components/button/index.d.ts +18 -0
- package/lib/typescript/src/components/button/index.d.ts.map +1 -1
- package/lib/typescript/src/components/checkbox/index.d.ts +42 -0
- package/lib/typescript/src/components/checkbox/index.d.ts.map +1 -1
- package/lib/typescript/src/components/frame/index.d.ts +39 -5
- package/lib/typescript/src/components/frame/index.d.ts.map +1 -1
- package/lib/typescript/src/components/timeline/index.d.ts +96 -0
- package/lib/typescript/src/components/timeline/index.d.ts.map +1 -0
- package/lib/typescript/src/components/toast/index.d.ts.map +1 -1
- package/lib/typescript/src/icons/index.d.ts +38 -3
- package/lib/typescript/src/icons/index.d.ts.map +1 -1
- package/lib/typescript/src/index.d.ts +4 -2
- package/lib/typescript/src/index.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/components/accordion/index.tsx +344 -0
- package/src/components/avatar/index.tsx +68 -20
- package/src/components/badge/index.tsx +35 -6
- package/src/components/button/index.tsx +63 -20
- package/src/components/checkbox/index.tsx +49 -11
- package/src/components/frame/index.tsx +101 -31
- package/src/components/timeline/index.tsx +482 -0
- package/src/components/toast/index.tsx +166 -45
- package/src/icons/index.tsx +292 -27
- package/src/index.ts +38 -1
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
createContext,
|
|
14
14
|
useCallback,
|
|
15
15
|
useContext,
|
|
16
|
+
useEffect,
|
|
16
17
|
useMemo,
|
|
17
18
|
useSyncExternalStore,
|
|
18
19
|
type ReactNode,
|
|
@@ -32,6 +33,7 @@ import Animated, {
|
|
|
32
33
|
withDecay,
|
|
33
34
|
withSpring,
|
|
34
35
|
withTiming,
|
|
36
|
+
type SharedValue,
|
|
35
37
|
} from 'react-native-reanimated';
|
|
36
38
|
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
|
37
39
|
import { tv, type VariantProps } from 'tailwind-variants';
|
|
@@ -59,8 +61,14 @@ const DISMISS_VELOCITY = 500;
|
|
|
59
61
|
const RUBBER_BAND_LIMIT = 40;
|
|
60
62
|
/** How far a toast travels on entry. Short, so the spring has little to settle. */
|
|
61
63
|
const ENTER_OFFSET = 100;
|
|
62
|
-
/** Newest toasts only; older ones
|
|
64
|
+
/** Newest toasts only; older ones fade out rather than stacking off-screen. */
|
|
63
65
|
const MAX_VISIBLE = 3;
|
|
66
|
+
/** How far each toast behind the front one peeks out. */
|
|
67
|
+
const STACK_OFFSET = 10;
|
|
68
|
+
/** Scale of the toast one step back in the deck. */
|
|
69
|
+
const STACK_SCALE_STEP = 0.97;
|
|
70
|
+
/** Gap between the deck and the screen edges, on top of the safe-area inset. */
|
|
71
|
+
const EDGE_INSET = 16;
|
|
64
72
|
|
|
65
73
|
/*
|
|
66
74
|
* Entering/exiting animations, from heroui-inc/heroui-native
|
|
@@ -279,12 +287,41 @@ export const Toast = Object.assign(ToastRoot, {
|
|
|
279
287
|
* toast came from dismisses it, dragging the other way rubber-bands against a
|
|
280
288
|
* hard limit. Matches heroui-native's toast.animation.ts.
|
|
281
289
|
*/
|
|
282
|
-
function ToastSlot({
|
|
290
|
+
function ToastSlot({
|
|
291
|
+
item,
|
|
292
|
+
index,
|
|
293
|
+
total,
|
|
294
|
+
frontId,
|
|
295
|
+
heights,
|
|
296
|
+
}: {
|
|
297
|
+
item: ToastItem;
|
|
298
|
+
/** Position in the stack; the newest toast is `total - 1`. */
|
|
299
|
+
index: number;
|
|
300
|
+
total: number;
|
|
301
|
+
/** Id of the newest toast — its height sets the size of the whole deck. */
|
|
302
|
+
frontId: string;
|
|
303
|
+
/** Measured natural height per toast id, shared with the UI thread. */
|
|
304
|
+
heights: SharedValue<Record<string, number>>;
|
|
305
|
+
}) {
|
|
283
306
|
const placement = item.placement ?? 'bottom';
|
|
284
307
|
const translateY = useSharedValue(0);
|
|
285
308
|
const scale = useSharedValue(1);
|
|
286
309
|
const hide = useCallback(() => toastStore.hide(item.id), [item.id]);
|
|
287
310
|
|
|
311
|
+
// Drop this toast's measurement when it leaves, so the map cannot grow
|
|
312
|
+
// without bound over a long session.
|
|
313
|
+
useEffect(
|
|
314
|
+
() => () => {
|
|
315
|
+
heights.modify((value: Record<string, number>) => {
|
|
316
|
+
'worklet';
|
|
317
|
+
const next = { ...value };
|
|
318
|
+
delete next[item.id];
|
|
319
|
+
return next;
|
|
320
|
+
});
|
|
321
|
+
},
|
|
322
|
+
[heights, item.id]
|
|
323
|
+
);
|
|
324
|
+
|
|
288
325
|
/** +1 when a downward drag dismisses (bottom), -1 when an upward one does. */
|
|
289
326
|
const dismissSign = placement === 'top' ? -1 : 1;
|
|
290
327
|
|
|
@@ -332,51 +369,122 @@ function ToastSlot({ item, depth }: { item: ToastItem; depth: number }) {
|
|
|
332
369
|
}
|
|
333
370
|
});
|
|
334
371
|
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
372
|
+
/** Older toasts slide away from the edge the stack is pinned to. */
|
|
373
|
+
const stackSign = placement === 'top' ? 1 : -1;
|
|
374
|
+
|
|
375
|
+
/**
|
|
376
|
+
* Stack position plus the gesture transform.
|
|
377
|
+
*
|
|
378
|
+
* The whole deck takes the *newest* toast's height so the cards behind read
|
|
379
|
+
* as a uniform stack rather than a ragged pile — matching heroui-native.
|
|
380
|
+
*/
|
|
381
|
+
const stackStyle = useAnimatedStyle(() => {
|
|
382
|
+
// Fall back to this toast's own height until the front one is measured.
|
|
383
|
+
const frontHeight = heights.value[frontId] ?? heights.value[item.id];
|
|
384
|
+
|
|
385
|
+
// Newest sits at 0; each one behind steps back by STACK_OFFSET and shrinks.
|
|
386
|
+
const inputRange = [total - 1, total - 2];
|
|
387
|
+
const offset = interpolate(index, inputRange, [0, STACK_OFFSET * stackSign], {
|
|
388
|
+
extrapolateLeft: Extrapolation.CLAMP,
|
|
389
|
+
});
|
|
390
|
+
const stackScale = interpolate(index, inputRange, [1, STACK_SCALE_STEP], {
|
|
391
|
+
extrapolateLeft: Extrapolation.CLAMP,
|
|
392
|
+
});
|
|
393
|
+
// Anything past MAX_VISIBLE fades out rather than piling up forever.
|
|
394
|
+
const opacity = interpolate(
|
|
395
|
+
index,
|
|
396
|
+
[total - MAX_VISIBLE, total - MAX_VISIBLE - 1],
|
|
397
|
+
[1, 0],
|
|
398
|
+
Extrapolation.CLAMP
|
|
399
|
+
);
|
|
400
|
+
|
|
401
|
+
return {
|
|
402
|
+
opacity,
|
|
403
|
+
height: frontHeight,
|
|
404
|
+
transform: [
|
|
405
|
+
{ translateY: offset + translateY.value },
|
|
406
|
+
{ scale: stackScale * scale.value },
|
|
407
|
+
],
|
|
408
|
+
};
|
|
409
|
+
});
|
|
340
410
|
|
|
341
411
|
const handle: ToastHandle = { id: item.id, hide };
|
|
342
412
|
|
|
413
|
+
const content = item.component ? (
|
|
414
|
+
item.component(handle)
|
|
415
|
+
) : (
|
|
416
|
+
<Toast variant={item.variant ?? 'default'} onHide={hide}>
|
|
417
|
+
{item.icon === null ? null : (
|
|
418
|
+
<Toast.Indicator>{item.icon ?? undefined}</Toast.Indicator>
|
|
419
|
+
)}
|
|
420
|
+
<Toast.Content>
|
|
421
|
+
{item.label ? <Toast.Title>{item.label}</Toast.Title> : null}
|
|
422
|
+
{item.description ? (
|
|
423
|
+
<Toast.Description>{item.description}</Toast.Description>
|
|
424
|
+
) : null}
|
|
425
|
+
</Toast.Content>
|
|
426
|
+
{item.actionLabel ? (
|
|
427
|
+
<Toast.Action onPress={() => item.onActionPress?.(handle)}>
|
|
428
|
+
{item.actionLabel}
|
|
429
|
+
</Toast.Action>
|
|
430
|
+
) : null}
|
|
431
|
+
{(item.closable ?? !item.actionLabel) ? <Toast.Close /> : null}
|
|
432
|
+
</Toast>
|
|
433
|
+
);
|
|
434
|
+
|
|
343
435
|
return (
|
|
344
436
|
<Animated.View
|
|
345
437
|
entering={placement === 'top' ? enteringTop : enteringBottom}
|
|
346
438
|
exiting={placement === 'top' ? exitingTop : exitingBottom}
|
|
347
|
-
|
|
348
|
-
|
|
439
|
+
pointerEvents="box-none"
|
|
440
|
+
// Every toast is pinned to the same edge, so they overlap as a deck.
|
|
441
|
+
// Newest on top. Horizontal inset lives here rather than as padding on
|
|
442
|
+
// the anchor: Yoga positions absolute children against the border box,
|
|
443
|
+
// so padding on the parent would not move them.
|
|
444
|
+
className="absolute"
|
|
445
|
+
style={{
|
|
446
|
+
left: EDGE_INSET,
|
|
447
|
+
right: EDGE_INSET,
|
|
448
|
+
[placement === 'top' ? 'top' : 'bottom']: 0,
|
|
449
|
+
zIndex: index,
|
|
450
|
+
}}
|
|
349
451
|
>
|
|
350
452
|
<GestureDetector gesture={pan}>
|
|
351
|
-
<Animated.View style={
|
|
352
|
-
{item.component ? (
|
|
353
|
-
item.component(handle)
|
|
354
|
-
) : (
|
|
355
|
-
<Toast variant={item.variant ?? 'default'} onHide={hide}>
|
|
356
|
-
{item.icon === null ? null : (
|
|
357
|
-
<Toast.Indicator>{item.icon ?? undefined}</Toast.Indicator>
|
|
358
|
-
)}
|
|
359
|
-
<Toast.Content>
|
|
360
|
-
{item.label ? <Toast.Title>{item.label}</Toast.Title> : null}
|
|
361
|
-
{item.description ? (
|
|
362
|
-
<Toast.Description>{item.description}</Toast.Description>
|
|
363
|
-
) : null}
|
|
364
|
-
</Toast.Content>
|
|
365
|
-
{item.actionLabel ? (
|
|
366
|
-
<Toast.Action onPress={() => item.onActionPress?.(handle)}>
|
|
367
|
-
{item.actionLabel}
|
|
368
|
-
</Toast.Action>
|
|
369
|
-
) : null}
|
|
370
|
-
{(item.closable ?? !item.actionLabel) ? <Toast.Close /> : null}
|
|
371
|
-
</Toast>
|
|
372
|
-
)}
|
|
373
|
-
</Animated.View>
|
|
453
|
+
<Animated.View style={stackStyle}>{content}</Animated.View>
|
|
374
454
|
</GestureDetector>
|
|
455
|
+
|
|
456
|
+
{/*
|
|
457
|
+
A hidden copy, purely to measure. The visible one above has its height
|
|
458
|
+
forced by the animated style, so it can never report its natural size.
|
|
459
|
+
*/}
|
|
460
|
+
<View
|
|
461
|
+
pointerEvents="none"
|
|
462
|
+
accessibilityElementsHidden
|
|
463
|
+
importantForAccessibility="no-hide-descendants"
|
|
464
|
+
className="absolute left-0 right-0 opacity-0"
|
|
465
|
+
onLayout={(event) => {
|
|
466
|
+
const measured = event.nativeEvent.layout.height;
|
|
467
|
+
heights.modify((value: Record<string, number>) => {
|
|
468
|
+
'worklet';
|
|
469
|
+
if (value[item.id] === measured) return value;
|
|
470
|
+
return { ...value, [item.id]: measured };
|
|
471
|
+
});
|
|
472
|
+
}}
|
|
473
|
+
>
|
|
474
|
+
{content}
|
|
475
|
+
</View>
|
|
375
476
|
</Animated.View>
|
|
376
477
|
);
|
|
377
478
|
}
|
|
378
479
|
|
|
379
|
-
|
|
480
|
+
|
|
481
|
+
/**
|
|
482
|
+
* The deck for one edge of the screen.
|
|
483
|
+
*
|
|
484
|
+
* Toasts are not laid out in flow — each one is absolutely pinned to the same
|
|
485
|
+
* edge and offset by its stack position, so they overlap like a deck of cards
|
|
486
|
+
* with only the newest fully visible.
|
|
487
|
+
*/
|
|
380
488
|
function ToastStack({
|
|
381
489
|
items,
|
|
382
490
|
placement,
|
|
@@ -385,27 +493,40 @@ function ToastStack({
|
|
|
385
493
|
placement: ToastPlacement;
|
|
386
494
|
}) {
|
|
387
495
|
const insets = useSafeAreaInsets();
|
|
496
|
+
// Natural heights, keyed by toast id. Written from the UI thread by each
|
|
497
|
+
// slot's hidden measuring copy.
|
|
498
|
+
const heights = useSharedValue<Record<string, number>>({});
|
|
499
|
+
|
|
388
500
|
if (items.length === 0) return null;
|
|
389
501
|
|
|
390
|
-
//
|
|
391
|
-
//
|
|
392
|
-
const visible = items.slice(-MAX_VISIBLE);
|
|
393
|
-
const
|
|
502
|
+
// Keep a couple beyond MAX_VISIBLE mounted so the ones fading out still
|
|
503
|
+
// animate rather than popping.
|
|
504
|
+
const visible = items.slice(-(MAX_VISIBLE + 1));
|
|
505
|
+
const frontId = visible[visible.length - 1]!.id;
|
|
394
506
|
|
|
395
507
|
return (
|
|
508
|
+
// A zero-height anchor sitting at the safe-area edge. The toasts hang off
|
|
509
|
+
// it — upward for bottom placement, downward for top — so the inset is
|
|
510
|
+
// part of the anchor's position rather than padding, which Yoga would
|
|
511
|
+
// ignore for absolutely positioned children.
|
|
396
512
|
<View
|
|
397
513
|
pointerEvents="box-none"
|
|
398
|
-
className=
|
|
399
|
-
'absolute left-0 right-0 gap-2 px-4',
|
|
400
|
-
placement === 'top' ? 'top-0' : 'bottom-0'
|
|
401
|
-
)}
|
|
514
|
+
className="absolute left-0 right-0"
|
|
402
515
|
style={{
|
|
403
|
-
|
|
404
|
-
|
|
516
|
+
[placement === 'top' ? 'top' : 'bottom']:
|
|
517
|
+
(placement === 'top' ? insets.top : insets.bottom) + EDGE_INSET,
|
|
518
|
+
height: 0,
|
|
405
519
|
}}
|
|
406
520
|
>
|
|
407
|
-
{
|
|
408
|
-
<ToastSlot
|
|
521
|
+
{visible.map((item, index) => (
|
|
522
|
+
<ToastSlot
|
|
523
|
+
key={item.id}
|
|
524
|
+
item={item}
|
|
525
|
+
index={index}
|
|
526
|
+
total={visible.length}
|
|
527
|
+
frontId={frontId}
|
|
528
|
+
heights={heights}
|
|
529
|
+
/>
|
|
409
530
|
))}
|
|
410
531
|
</View>
|
|
411
532
|
);
|