panelui-native 0.4.0 → 0.5.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.
Files changed (51) hide show
  1. package/README.md +14 -7
  2. package/lib/module/components/accordion/index.js +312 -0
  3. package/lib/module/components/accordion/index.js.map +1 -0
  4. package/lib/module/components/avatar/index.js +58 -17
  5. package/lib/module/components/avatar/index.js.map +1 -1
  6. package/lib/module/components/badge/index.js +26 -5
  7. package/lib/module/components/badge/index.js.map +1 -1
  8. package/lib/module/components/button/index.js +51 -19
  9. package/lib/module/components/button/index.js.map +1 -1
  10. package/lib/module/components/checkbox/index.js +42 -14
  11. package/lib/module/components/checkbox/index.js.map +1 -1
  12. package/lib/module/components/frame/index.js +64 -13
  13. package/lib/module/components/frame/index.js.map +1 -1
  14. package/lib/module/components/timeline/index.js +633 -0
  15. package/lib/module/components/timeline/index.js.map +1 -0
  16. package/lib/module/components/toast/index.js +145 -55
  17. package/lib/module/components/toast/index.js.map +1 -1
  18. package/lib/module/icons/index.js +388 -27
  19. package/lib/module/icons/index.js.map +1 -1
  20. package/lib/module/index.js +3 -1
  21. package/lib/module/index.js.map +1 -1
  22. package/lib/typescript/src/components/accordion/index.d.ts +56 -0
  23. package/lib/typescript/src/components/accordion/index.d.ts.map +1 -0
  24. package/lib/typescript/src/components/avatar/index.d.ts +8 -1
  25. package/lib/typescript/src/components/avatar/index.d.ts.map +1 -1
  26. package/lib/typescript/src/components/badge/index.d.ts +38 -0
  27. package/lib/typescript/src/components/badge/index.d.ts.map +1 -1
  28. package/lib/typescript/src/components/button/index.d.ts +18 -0
  29. package/lib/typescript/src/components/button/index.d.ts.map +1 -1
  30. package/lib/typescript/src/components/checkbox/index.d.ts +42 -0
  31. package/lib/typescript/src/components/checkbox/index.d.ts.map +1 -1
  32. package/lib/typescript/src/components/frame/index.d.ts +39 -5
  33. package/lib/typescript/src/components/frame/index.d.ts.map +1 -1
  34. package/lib/typescript/src/components/timeline/index.d.ts +96 -0
  35. package/lib/typescript/src/components/timeline/index.d.ts.map +1 -0
  36. package/lib/typescript/src/components/toast/index.d.ts.map +1 -1
  37. package/lib/typescript/src/icons/index.d.ts +38 -3
  38. package/lib/typescript/src/icons/index.d.ts.map +1 -1
  39. package/lib/typescript/src/index.d.ts +4 -2
  40. package/lib/typescript/src/index.d.ts.map +1 -1
  41. package/package.json +1 -1
  42. package/src/components/accordion/index.tsx +344 -0
  43. package/src/components/avatar/index.tsx +68 -20
  44. package/src/components/badge/index.tsx +35 -6
  45. package/src/components/button/index.tsx +63 -20
  46. package/src/components/checkbox/index.tsx +49 -11
  47. package/src/components/frame/index.tsx +101 -31
  48. package/src/components/timeline/index.tsx +456 -0
  49. package/src/components/toast/index.tsx +166 -45
  50. package/src/icons/index.tsx +292 -27
  51. 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 are dropped rather than stacking off-screen. */
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({ item, depth }: { item: ToastItem; depth: number }) {
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
- // Gesture transform only — the layout animations below own opacity and
336
- // their own translate, and Reanimated cannot drive a property from both.
337
- const gestureStyle = useAnimatedStyle(() => ({
338
- transform: [{ translateY: translateY.value }, { scale: scale.value }],
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
- // Toasts further down the stack sit back a little.
348
- style={{ opacity: 1 - depth * 0.15, transform: [{ scale: 1 - depth * 0.03 }] }}
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={gestureStyle}>
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
- /** Absolutely positioned stack for one edge of the screen. */
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
- // Only the newest few are shown; older ones are dropped rather than piling
391
- // up off-screen. Newest sits closest to the edge it entered from.
392
- const visible = items.slice(-MAX_VISIBLE);
393
- const ordered = placement === 'top' ? visible : [...visible].reverse();
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={cn(
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
- paddingTop: placement === 'top' ? insets.top + 8 : 0,
404
- paddingBottom: placement === 'bottom' ? insets.bottom + 8 : 0,
516
+ [placement === 'top' ? 'top' : 'bottom']:
517
+ (placement === 'top' ? insets.top : insets.bottom) + EDGE_INSET,
518
+ height: 0,
405
519
  }}
406
520
  >
407
- {ordered.map((item, index) => (
408
- <ToastSlot key={item.id} item={item} depth={index} />
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
  );