panelui-native 0.28.1 → 0.30.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.
Files changed (82) hide show
  1. package/README.md +18 -1
  2. package/lib/module/components/calendar/index.js +350 -115
  3. package/lib/module/components/calendar/index.js.map +1 -1
  4. package/lib/module/components/code-block/highlight.js +363 -0
  5. package/lib/module/components/code-block/highlight.js.map +1 -0
  6. package/lib/module/components/code-block/index.js +313 -0
  7. package/lib/module/components/code-block/index.js.map +1 -0
  8. package/lib/module/components/date-picker/index.js +8 -1
  9. package/lib/module/components/date-picker/index.js.map +1 -1
  10. package/lib/module/components/menu/index.js +714 -0
  11. package/lib/module/components/menu/index.js.map +1 -0
  12. package/lib/module/components/plan/index.js +296 -0
  13. package/lib/module/components/plan/index.js.map +1 -0
  14. package/lib/module/components/popover/index.js +35 -4
  15. package/lib/module/components/popover/index.js.map +1 -1
  16. package/lib/module/components/radio-group/index.js +13 -8
  17. package/lib/module/components/radio-group/index.js.map +1 -1
  18. package/lib/module/components/reasoning/index.js +244 -0
  19. package/lib/module/components/reasoning/index.js.map +1 -0
  20. package/lib/module/components/sources/index.js +221 -0
  21. package/lib/module/components/sources/index.js.map +1 -0
  22. package/lib/module/components/task/index.js +300 -0
  23. package/lib/module/components/task/index.js.map +1 -0
  24. package/lib/module/components/textarea/index.js +295 -0
  25. package/lib/module/components/textarea/index.js.map +1 -0
  26. package/lib/module/components/tooltip/index.js +179 -23
  27. package/lib/module/components/tooltip/index.js.map +1 -1
  28. package/lib/module/icons/index.js +139 -0
  29. package/lib/module/icons/index.js.map +1 -1
  30. package/lib/module/index.js +8 -0
  31. package/lib/module/index.js.map +1 -1
  32. package/lib/module/primitives/collapse.js +112 -0
  33. package/lib/module/primitives/collapse.js.map +1 -0
  34. package/lib/typescript/src/components/calendar/index.d.ts +68 -3
  35. package/lib/typescript/src/components/calendar/index.d.ts.map +1 -1
  36. package/lib/typescript/src/components/code-block/highlight.d.ts +39 -0
  37. package/lib/typescript/src/components/code-block/highlight.d.ts.map +1 -0
  38. package/lib/typescript/src/components/code-block/index.d.ts +113 -0
  39. package/lib/typescript/src/components/code-block/index.d.ts.map +1 -0
  40. package/lib/typescript/src/components/date-picker/index.d.ts +7 -1
  41. package/lib/typescript/src/components/date-picker/index.d.ts.map +1 -1
  42. package/lib/typescript/src/components/menu/index.d.ts +261 -0
  43. package/lib/typescript/src/components/menu/index.d.ts.map +1 -0
  44. package/lib/typescript/src/components/plan/index.d.ts +127 -0
  45. package/lib/typescript/src/components/plan/index.d.ts.map +1 -0
  46. package/lib/typescript/src/components/popover/index.d.ts +35 -1
  47. package/lib/typescript/src/components/popover/index.d.ts.map +1 -1
  48. package/lib/typescript/src/components/radio-group/index.d.ts.map +1 -1
  49. package/lib/typescript/src/components/reasoning/index.d.ts +97 -0
  50. package/lib/typescript/src/components/reasoning/index.d.ts.map +1 -0
  51. package/lib/typescript/src/components/sources/index.d.ts +85 -0
  52. package/lib/typescript/src/components/sources/index.d.ts.map +1 -0
  53. package/lib/typescript/src/components/task/index.d.ts +103 -0
  54. package/lib/typescript/src/components/task/index.d.ts.map +1 -0
  55. package/lib/typescript/src/components/textarea/index.d.ts +175 -0
  56. package/lib/typescript/src/components/textarea/index.d.ts.map +1 -0
  57. package/lib/typescript/src/components/tooltip/index.d.ts +63 -8
  58. package/lib/typescript/src/components/tooltip/index.d.ts.map +1 -1
  59. package/lib/typescript/src/icons/index.d.ts +14 -0
  60. package/lib/typescript/src/icons/index.d.ts.map +1 -1
  61. package/lib/typescript/src/index.d.ts +10 -2
  62. package/lib/typescript/src/index.d.ts.map +1 -1
  63. package/lib/typescript/src/primitives/collapse.d.ts +42 -0
  64. package/lib/typescript/src/primitives/collapse.d.ts.map +1 -0
  65. package/package.json +1 -1
  66. package/src/components/calendar/index.tsx +389 -122
  67. package/src/components/code-block/highlight.ts +278 -0
  68. package/src/components/code-block/index.tsx +341 -0
  69. package/src/components/date-picker/index.tsx +19 -1
  70. package/src/components/menu/index.tsx +812 -0
  71. package/src/components/plan/index.tsx +325 -0
  72. package/src/components/popover/index.tsx +69 -2
  73. package/src/components/radio-group/index.tsx +11 -2
  74. package/src/components/reasoning/index.tsx +297 -0
  75. package/src/components/sources/index.tsx +278 -0
  76. package/src/components/task/index.tsx +329 -0
  77. package/src/components/textarea/index.tsx +381 -0
  78. package/src/components/tooltip/index.tsx +203 -20
  79. package/src/icons/index.tsx +102 -0
  80. package/src/index.ts +71 -0
  81. package/src/primitives/collapse.tsx +128 -0
  82. package/theme.css +112 -0
@@ -3,9 +3,15 @@
3
3
  *
4
4
  * A popover is a panel you open and deal with; a tooltip is a whisper. It
5
5
  * carries a word or two about what a control does, appears without taking the
6
- * screen, and goes away on its own. That is why it is inverted rather than
7
- * surface-coloured, why it is not dismissible with a scrim, and why it closes
8
- * after a beat instead of waiting to be told.
6
+ * screen, and goes away on its own. That is why it is inverted by default, why
7
+ * it is not dismissible with a scrim, and why it closes after a beat instead of
8
+ * waiting to be told.
9
+ *
10
+ * The inversion is a default rather than a rule, because the whisper has a
11
+ * larger sibling: a tooltip that carries a heading and a sentence stops reading
12
+ * as a different layer and starts reading as a panel with the wrong colours.
13
+ * `variant="surface"` makes it one, and the sizing props — `width`,
14
+ * `minWidth`, `maxHeight`, `scrollable` — are what let it hold that much.
9
15
  *
10
16
  * On touch there is no hover to open it, so the gesture is a long press by
11
17
  * default — the platform's own "tell me more" gesture — with `openOn="press"`
@@ -44,6 +50,7 @@ import {
44
50
  } from 'react';
45
51
  import {
46
52
  Pressable,
53
+ ScrollView,
47
54
  useWindowDimensions,
48
55
  View,
49
56
  type LayoutChangeEvent,
@@ -58,6 +65,7 @@ import Animated, {
58
65
  withTiming,
59
66
  } from 'react-native-reanimated';
60
67
  import { useSafeAreaInsets } from 'react-native-safe-area-context';
68
+ import { tv } from 'tailwind-variants';
61
69
  import { Portal } from '../../primitives/portal';
62
70
  import { Text, type TextProps } from '../../primitives/text';
63
71
  import { cn } from '../../utils/cn';
@@ -73,6 +81,57 @@ const DEFAULT_DURATION = 1500;
73
81
 
74
82
  export type TooltipPlacement = 'top' | 'bottom' | 'left' | 'right';
75
83
  export type TooltipAlign = 'start' | 'center' | 'end';
84
+ export type TooltipVariant = 'inverted' | 'surface';
85
+
86
+ /**
87
+ * Every colour the tooltip has, in one place.
88
+ *
89
+ * They were three literals at three call sites — the panel, the arrow and the
90
+ * default text — which meant retheming a tooltip took three `className`
91
+ * overrides that each had to be kept in step with the others. As a variant it
92
+ * is one prop, and the arrow and the text read it off the context rather than
93
+ * being told again.
94
+ */
95
+ const tooltipVariants = tv({
96
+ slots: {
97
+ content: 'rounded-lg px-2.5 py-1.5 shadow-md',
98
+ text: 'text-sm font-medium',
99
+ title: 'text-sm font-semibold',
100
+ description: 'text-sm',
101
+ arrow: 'rounded-[1px]',
102
+ },
103
+ variants: {
104
+ variant: {
105
+ /*
106
+ * The default, and deliberately not a surface colour: a whisper over the
107
+ * page should read as a different layer rather than as another panel of
108
+ * it. It is the treatment a one-line label wants.
109
+ */
110
+ inverted: {
111
+ content: 'bg-foreground',
112
+ text: 'text-background',
113
+ title: 'text-background',
114
+ description: 'text-background/70',
115
+ arrow: 'bg-foreground',
116
+ },
117
+ /*
118
+ * For a tooltip carrying more than a label — a heading, a sentence, a row
119
+ * of controls. At that size the inversion stops reading as a whisper and
120
+ * starts reading as a panel with the wrong colours, so it becomes one.
121
+ */
122
+ surface: {
123
+ content: 'border border-border bg-popover',
124
+ text: 'text-popover-foreground',
125
+ title: 'text-popover-foreground',
126
+ description: 'text-muted-foreground',
127
+ arrow: 'border-border bg-popover',
128
+ },
129
+ },
130
+ },
131
+ defaultVariants: {
132
+ variant: 'inverted',
133
+ },
134
+ });
76
135
 
77
136
  interface TriggerRect {
78
137
  x: number;
@@ -97,6 +156,9 @@ interface TooltipContextValue {
97
156
  openOn: TooltipOpenOn;
98
157
  /** Accessibility label carried onto the trigger. */
99
158
  label?: string;
159
+ /** Which set of colours the panel, arrow and text draw from. */
160
+ variant: TooltipVariant;
161
+ setVariant: (variant: TooltipVariant) => void;
100
162
  }
101
163
 
102
164
  export type TooltipOpenOn = 'longPress' | 'press';
@@ -149,6 +211,12 @@ function TooltipRoot({
149
211
  const [trigger, setTrigger] = useState<TriggerRect | null>(null);
150
212
  const [placement, setPlacement] = useState<TooltipPlacement>('top');
151
213
  const [arrowOffset, setArrowOffset] = useState(0);
214
+ /*
215
+ * Published by Content rather than passed to it, because the arrow and the
216
+ * text sit beside the panel in the tree and would otherwise each have to be
217
+ * told which colours to use.
218
+ */
219
+ const [variant, setVariant] = useState<TooltipVariant>('inverted');
152
220
 
153
221
  const isControlled = open !== undefined;
154
222
  const resolvedOpen = isControlled ? open : internalOpen;
@@ -186,8 +254,10 @@ function TooltipRoot({
186
254
  setArrowOffset,
187
255
  openOn,
188
256
  label,
257
+ variant,
258
+ setVariant,
189
259
  }),
190
- [resolvedOpen, show, hide, trigger, placement, arrowOffset, openOn, label]
260
+ [resolvedOpen, show, hide, trigger, placement, arrowOffset, openOn, label, variant]
191
261
  );
192
262
 
193
263
  return <TooltipContext.Provider value={context}>{children}</TooltipContext.Provider>;
@@ -282,6 +352,34 @@ export interface TooltipContentProps extends ViewProps {
282
352
  offset?: number;
283
353
  /** Nudge along the alignment axis, in pixels. */
284
354
  alignOffset?: number;
355
+ /**
356
+ * Which set of colours the panel, its arrow and its text draw from.
357
+ *
358
+ * `inverted` is the default and right for a label: a whisper over the page
359
+ * should read as a different layer rather than as another panel of it.
360
+ * `surface` matches the popover — reach for it once the tooltip carries a
361
+ * heading and a sentence, where the inversion stops reading as a whisper.
362
+ */
363
+ variant?: TooltipVariant;
364
+ /**
365
+ * `content-fit` sizes to the content, `trigger` matches the trigger's width,
366
+ * `full` spans the safe area, and a number is that many pixels. Worth setting
367
+ * for anything longer than a label, which would otherwise run to whatever
368
+ * width the sentence happens to want.
369
+ */
370
+ width?: number | 'trigger' | 'full' | 'content-fit';
371
+ /** Floor for the panel's width, in pixels. */
372
+ minWidth?: number;
373
+ /**
374
+ * Ceiling for the panel's height, in pixels. Always clamped to the room
375
+ * inside the safe area, which is also the default.
376
+ */
377
+ maxHeight?: number;
378
+ /**
379
+ * Scroll the body when it is taller than `maxHeight`. Off by default — a
380
+ * label has nothing to scroll, and a scroller around one only adds a bounce.
381
+ */
382
+ scrollable?: boolean;
285
383
  children?: ReactNode;
286
384
  }
287
385
 
@@ -291,13 +389,25 @@ function TooltipContent({
291
389
  align = 'center',
292
390
  offset = DEFAULT_OFFSET,
293
391
  alignOffset = 0,
392
+ variant = 'inverted',
393
+ width = 'content-fit',
394
+ minWidth,
395
+ maxHeight,
396
+ scrollable = false,
294
397
  children,
295
398
  onLayout: onLayoutProp,
296
399
  style,
297
400
  ...props
298
401
  }: TooltipContentProps) {
299
402
  const context = useTooltip('Tooltip.Content');
300
- const { open, hide, trigger, setPlacement, setArrowOffset } = context;
403
+ const { open, hide, trigger, setPlacement, setArrowOffset, setVariant } = context;
404
+ const slots = tooltipVariants({ variant });
405
+
406
+ // The arrow and the text sit beside this panel in the tree, so the chosen
407
+ // colours are published rather than passed down.
408
+ useEffect(() => {
409
+ setVariant(variant);
410
+ }, [variant, setVariant]);
301
411
  const { width: screenWidth, height: screenHeight } = useWindowDimensions();
302
412
  const insets = useSafeAreaInsets();
303
413
  const [size, setSize] = useState<{ width: number; height: number } | null>(null);
@@ -332,6 +442,28 @@ function TooltipContent({
332
442
  bottom: screenHeight - insets.bottom - SCREEN_MARGIN,
333
443
  };
334
444
 
445
+ const available = bounds.right - bounds.left;
446
+ const requestedWidth =
447
+ width === 'content-fit'
448
+ ? undefined
449
+ : width === 'trigger'
450
+ ? trigger?.width
451
+ : width === 'full'
452
+ ? available
453
+ : width;
454
+
455
+ // The floor never wins past the space there actually is — a panel wider than
456
+ // the screen is worse than a cramped one.
457
+ const resolvedWidth =
458
+ requestedWidth === undefined
459
+ ? minWidth === undefined
460
+ ? undefined
461
+ : Math.min(minWidth, available)
462
+ : Math.min(Math.max(requestedWidth, minWidth ?? 0), available);
463
+
464
+ const room = bounds.bottom - bounds.top;
465
+ const resolvedMaxHeight = Math.min(maxHeight ?? room, room);
466
+
335
467
  const position =
336
468
  trigger && size
337
469
  ? place({ trigger, size, placement, align, offset, alignOffset, bounds })
@@ -418,14 +550,20 @@ function TooltipContent({
418
550
  // so it is laid out off-screen rather than at the origin.
419
551
  top: position?.top ?? -9999,
420
552
  left: position?.left ?? -9999,
421
- maxWidth: bounds.right - bounds.left,
553
+ maxWidth: available,
554
+ maxHeight: resolvedMaxHeight,
555
+ width: resolvedWidth,
422
556
  }}
423
557
  >
424
558
  <Animated.View
425
- accessibilityRole="text"
559
+ // A panel with a heading and a paragraph is a group, not a run of
560
+ // text — the flat role would have a screen reader read it as one
561
+ // string with no structure.
562
+ accessibilityRole={scrollable || width !== 'content-fit' ? undefined : 'text'}
426
563
  style={[labelStyle, style]}
427
564
  className={cn(
428
- 'rounded-lg bg-foreground px-2.5 py-1.5 shadow-md',
565
+ slots.content(),
566
+ scrollable ? 'overflow-hidden' : undefined,
429
567
  className
430
568
  )}
431
569
  {...props}
@@ -438,12 +576,12 @@ function TooltipContent({
438
576
  view bare. Only the text nodes need the treatment; an element
439
577
  is already responsible for itself.
440
578
  */}
441
- {Children.map(children, (child) =>
442
- typeof child === 'string' || typeof child === 'number' ? (
443
- <TooltipText>{child}</TooltipText>
444
- ) : (
445
- child
446
- )
579
+ {scrollable ? (
580
+ <ScrollView bounces={false} showsVerticalScrollIndicator={false}>
581
+ {wrapTooltipText(children)}
582
+ </ScrollView>
583
+ ) : (
584
+ wrapTooltipText(children)
447
585
  )}
448
586
  </Animated.View>
449
587
  </Animated.View>
@@ -453,11 +591,53 @@ function TooltipContent({
453
591
  );
454
592
  }
455
593
 
456
- /** The label's default text: small and inverted against the dark surface. */
457
- const TooltipText = ({ className, ...props }: TextProps) => (
458
- <Text size="sm" weight="medium" className={cn('text-background', className)} {...props} />
459
- );
594
+ /** Bare strings become the label's default text; elements speak for themselves. */
595
+ function wrapTooltipText(children: ReactNode) {
596
+ return Children.map(children, (child) =>
597
+ typeof child === 'string' || typeof child === 'number' ? (
598
+ <TooltipText>{child}</TooltipText>
599
+ ) : (
600
+ child
601
+ )
602
+ );
603
+ }
604
+
605
+ /** The label's default text, coloured to whatever the panel is made of. */
606
+ function TooltipText({ className, ...props }: TextProps) {
607
+ const { variant } = useTooltip('Tooltip.Text');
608
+ const { text } = tooltipVariants({ variant });
609
+
610
+ return <Text className={cn(text(), className)} {...props} />;
611
+ }
460
612
  TooltipText.displayName = 'Tooltip.Text';
613
+ TooltipTitle.displayName = 'Tooltip.Title';
614
+ TooltipDescription.displayName = 'Tooltip.Description';
615
+
616
+ export interface TooltipTitleProps extends TextProps {
617
+ className?: string;
618
+ }
619
+
620
+ /** A heading, for a tooltip carrying more than a label. */
621
+ function TooltipTitle({ className, ...props }: TooltipTitleProps) {
622
+ const { variant } = useTooltip('Tooltip.Title');
623
+ const { title } = tooltipVariants({ variant });
624
+
625
+ return (
626
+ <Text accessibilityRole="header" className={cn(title(), className)} {...props} />
627
+ );
628
+ }
629
+
630
+ export interface TooltipDescriptionProps extends TextProps {
631
+ className?: string;
632
+ }
633
+
634
+ /** The sentence under a `Tooltip.Title`, in the panel's secondary colour. */
635
+ function TooltipDescription({ className, ...props }: TooltipDescriptionProps) {
636
+ const { variant } = useTooltip('Tooltip.Description');
637
+ const { description } = tooltipVariants({ variant });
638
+
639
+ return <Text className={cn(description(), className)} {...props} />;
640
+ }
461
641
 
462
642
  export interface TooltipArrowProps extends ViewProps {
463
643
  className?: string;
@@ -471,7 +651,8 @@ export interface TooltipArrowProps extends ViewProps {
471
651
  * the trigger rather than over the label's middle.
472
652
  */
473
653
  function TooltipArrow({ className, style, ...props }: TooltipArrowProps) {
474
- const { trigger, placement, arrowOffset } = useTooltip('Tooltip.Arrow');
654
+ const { trigger, placement, arrowOffset, variant } = useTooltip('Tooltip.Arrow');
655
+ const { arrow } = tooltipVariants({ variant });
475
656
  if (!trigger) return null;
476
657
 
477
658
  const vertical = placement === 'top' || placement === 'bottom';
@@ -499,7 +680,7 @@ function TooltipArrow({ className, style, ...props }: TooltipArrowProps) {
499
680
  },
500
681
  style,
501
682
  ]}
502
- className={cn('rounded-[1px] bg-foreground', className)}
683
+ className={cn(arrow(), className)}
503
684
  {...props}
504
685
  />
505
686
  );
@@ -606,5 +787,7 @@ export const Tooltip = Object.assign(TooltipRoot, {
606
787
  Trigger: TooltipTrigger,
607
788
  Content: TooltipContent,
608
789
  Arrow: TooltipArrow,
790
+ Title: TooltipTitle,
791
+ Description: TooltipDescription,
609
792
  Text: TooltipText,
610
793
  });
@@ -872,3 +872,105 @@ export function UnlockIcon({ size = 16, color, ...props }: IconProps) {
872
872
  </Svg>
873
873
  );
874
874
  }
875
+
876
+ /**
877
+ * Four-pointed sparkles. The mark for a model doing something on your behalf —
878
+ * a reasoning step, a generated plan — where a gear or a brain reads as
879
+ * machinery rather than as assistance.
880
+ */
881
+ export function SparklesIcon({ size = 16, color, ...props }: IconProps) {
882
+ const resolved = useResolvedColor(color, '#737373');
883
+ return (
884
+ <Svg width={size} height={size} viewBox="0 0 24 24" fill="none" {...props}>
885
+ <Path
886
+ d="M11 3 12.6 8.4 18 10l-5.4 1.6L11 17l-1.6-5.4L4 10l5.4-1.6z"
887
+ stroke={resolved}
888
+ strokeWidth={2}
889
+ strokeLinejoin="round"
890
+ />
891
+ <Path
892
+ d="M18 15.5 18.7 18l2.3.7-2.3.7-.7 2.3-.7-2.3-2.3-.7 2.3-.7z"
893
+ stroke={resolved}
894
+ strokeWidth={1.6}
895
+ strokeLinejoin="round"
896
+ />
897
+ </Svg>
898
+ );
899
+ }
900
+
901
+ /** Two stacked sheets — the copy action, paired with CheckIcon once it has run. */
902
+ export function CopyIcon({ size = 16, color, ...props }: IconProps) {
903
+ const resolved = useResolvedColor(color, '#737373');
904
+ return (
905
+ <Svg width={size} height={size} viewBox="0 0 24 24" fill="none" {...props}>
906
+ <Path
907
+ d="M9 9h10v12H9z"
908
+ stroke={resolved}
909
+ strokeWidth={2}
910
+ strokeLinejoin="round"
911
+ />
912
+ <Path
913
+ d="M15 5H5v12h2"
914
+ stroke={resolved}
915
+ strokeWidth={2}
916
+ strokeLinecap="round"
917
+ strokeLinejoin="round"
918
+ />
919
+ </Svg>
920
+ );
921
+ }
922
+
923
+ /** Two links of a chain, for a row that opens somewhere outside the app. */
924
+ export function LinkIcon({ size = 16, color, ...props }: IconProps) {
925
+ const resolved = useResolvedColor(color, '#737373');
926
+ return (
927
+ <Svg width={size} height={size} viewBox="0 0 24 24" fill="none" {...props}>
928
+ <Path
929
+ d="M10 13a5 5 0 0 0 7.5.5l3-3a5 5 0 0 0-7-7l-1.7 1.7"
930
+ stroke={resolved}
931
+ strokeWidth={2}
932
+ strokeLinecap="round"
933
+ strokeLinejoin="round"
934
+ />
935
+ <Path
936
+ d="M14 11a5 5 0 0 0-7.5-.5l-3 3a5 5 0 0 0 7 7l1.7-1.7"
937
+ stroke={resolved}
938
+ strokeWidth={2}
939
+ strokeLinecap="round"
940
+ strokeLinejoin="round"
941
+ />
942
+ </Svg>
943
+ );
944
+ }
945
+
946
+ /** A list with its items ticked off — a plan, or a run of steps. */
947
+ export function ListChecksIcon({ size = 16, color, ...props }: IconProps) {
948
+ const resolved = useResolvedColor(color, '#737373');
949
+ return (
950
+ <Svg width={size} height={size} viewBox="0 0 24 24" fill="none" {...props}>
951
+ <Path
952
+ d="m3 6 1.8 1.8L8 4.5M3 16l1.8 1.8L8 14.5"
953
+ stroke={resolved}
954
+ strokeWidth={2}
955
+ strokeLinecap="round"
956
+ strokeLinejoin="round"
957
+ />
958
+ <Path
959
+ d="M12 6.5h9M12 17.5h9"
960
+ stroke={resolved}
961
+ strokeWidth={2}
962
+ strokeLinecap="round"
963
+ />
964
+ </Svg>
965
+ );
966
+ }
967
+
968
+ /** A hollow ring, for a step that has not started. */
969
+ export function CircleIcon({ size = 16, color, ...props }: IconProps) {
970
+ const resolved = useResolvedColor(color, '#737373');
971
+ return (
972
+ <Svg width={size} height={size} viewBox="0 0 24 24" fill="none" {...props}>
973
+ <Circle cx={12} cy={12} r={8} stroke={resolved} strokeWidth={2} />
974
+ </Svg>
975
+ );
976
+ }
package/src/index.ts CHANGED
@@ -21,6 +21,7 @@ export { Portal, PortalHost, PortalProvider } from './primitives/portal';
21
21
  // graceful fall back from blur to dim — as the built-in ones.
22
22
  export { Scrim, hasBlur, type ScrimProps } from './primitives/scrim';
23
23
  export { Text, type TextProps } from './primitives/text';
24
+ export { Collapse, type CollapseProps } from './primitives/collapse';
24
25
  export {
25
26
  KeyboardAvoider,
26
27
  type KeyboardAvoiderProps,
@@ -271,6 +272,8 @@ export {
271
272
  export {
272
273
  Calendar,
273
274
  type CalendarProps,
275
+ type CalendarHeaderProps,
276
+ type CalendarNavProps,
274
277
  type CalendarMode,
275
278
  type CalendarCaptionLayout,
276
279
  type CalendarDisabled,
@@ -332,6 +335,7 @@ export {
332
335
  type NumberInputProps,
333
336
  } from './components/number-input';
334
337
  export { OtpInput, type OtpInputProps } from './components/otp-input';
338
+ export { Textarea, type TextareaProps } from './components/textarea';
335
339
  export {
336
340
  Item,
337
341
  type ItemProps,
@@ -375,6 +379,24 @@ export {
375
379
  type MarkerIconProps,
376
380
  type MarkerContentProps,
377
381
  } from './components/marker';
382
+ export {
383
+ Menu,
384
+ type MenuProps,
385
+ type MenuTriggerProps,
386
+ type MenuContentProps,
387
+ type MenuBackgroundProps,
388
+ type MenuLabelProps,
389
+ type MenuItemProps,
390
+ type MenuCheckboxItemProps,
391
+ type MenuRadioGroupProps,
392
+ type MenuRadioItemProps,
393
+ type MenuSeparatorProps,
394
+ type MenuSubProps,
395
+ type MenuSubTriggerProps,
396
+ type MenuSubContentProps,
397
+ type MenuItemVariant,
398
+ type MenuRadioIndicator,
399
+ } from './components/menu';
378
400
  export {
379
401
  MessageScroller,
380
402
  useMessageScroller,
@@ -448,6 +470,52 @@ export {
448
470
  type SignatureButtonProps,
449
471
  } from './components/signature';
450
472
  export { Shimmer, type ShimmerProps } from './components/shimmer';
473
+ export {
474
+ Reasoning,
475
+ type ReasoningProps,
476
+ type ReasoningTriggerProps,
477
+ type ReasoningContentProps,
478
+ } from './components/reasoning';
479
+ export {
480
+ Sources,
481
+ type SourcesProps,
482
+ type SourcesTriggerProps,
483
+ type SourcesContentProps,
484
+ type SourcesSourceProps,
485
+ } from './components/sources';
486
+ export {
487
+ Task,
488
+ type TaskProps,
489
+ type TaskTriggerProps,
490
+ type TaskContentProps,
491
+ type TaskItemProps,
492
+ type TaskFileProps,
493
+ type TaskStatus,
494
+ } from './components/task';
495
+ export {
496
+ CodeBlock,
497
+ resolveLanguage,
498
+ type CodeBlockProps,
499
+ type CodeBlockHeaderProps,
500
+ type CodeBlockFilenameProps,
501
+ type CodeBlockLanguageProps,
502
+ type CodeBlockActionsProps,
503
+ type CodeBlockCopyButtonProps,
504
+ type CodeLanguage,
505
+ type Token,
506
+ type TokenKind,
507
+ } from './components/code-block';
508
+ export {
509
+ Plan,
510
+ type PlanProps,
511
+ type PlanHeaderProps,
512
+ type PlanTitleProps,
513
+ type PlanDescriptionProps,
514
+ type PlanActionProps,
515
+ type PlanTriggerProps,
516
+ type PlanContentProps,
517
+ type PlanFooterProps,
518
+ } from './components/plan';
451
519
  export {
452
520
  ScrollText,
453
521
  type ScrollTextProps,
@@ -508,9 +576,12 @@ export {
508
576
  type TooltipTriggerProps,
509
577
  type TooltipContentProps,
510
578
  type TooltipArrowProps,
579
+ type TooltipTitleProps,
580
+ type TooltipDescriptionProps,
511
581
  type TooltipPlacement,
512
582
  type TooltipAlign,
513
583
  type TooltipOpenOn,
584
+ type TooltipVariant,
514
585
  } from './components/tooltip';
515
586
  export {
516
587
  Typography,
@@ -0,0 +1,128 @@
1
+ /**
2
+ * Collapse — a body that opens and closes by animating its own height.
3
+ *
4
+ * Height cannot be animated from `auto`, so the content is measured first and
5
+ * the measurement is what gets animated to. While that is happening the content
6
+ * is taken out of the flow, because a child of a view whose height is
7
+ * mid-animation reports that animated height back and the panel then settles on
8
+ * whatever it happened to measure.
9
+ *
10
+ * The moment the animation finishes, both go away and the content lays out
11
+ * normally again. That is what lets a Collapse hold another Collapse — or hold
12
+ * content that is still streaming in — without either of them being clipped to
13
+ * a number that was true a frame ago.
14
+ *
15
+ * ```tsx
16
+ * <Collapse open={open}>
17
+ * <Text>Anything, of any height.</Text>
18
+ * </Collapse>
19
+ * ```
20
+ *
21
+ * The alternative — unmounting the body and letting a layout transition on the
22
+ * parent carry the change — is what `Accordion` does, and it is right when the
23
+ * closed state should cost nothing. This is for a body that opens and closes
24
+ * repeatedly and often while its content is still arriving, where remounting
25
+ * would throw away scroll position and restart every animation inside it.
26
+ */
27
+ import { useEffect, useRef, useState, type ReactNode } from 'react';
28
+ import { View, type LayoutChangeEvent, type ViewProps } from 'react-native';
29
+ import Animated, {
30
+ runOnJS,
31
+ useAnimatedStyle,
32
+ useReducedMotion,
33
+ useSharedValue,
34
+ withTiming,
35
+ } from 'react-native-reanimated';
36
+ import { cn } from '../utils/cn';
37
+
38
+ /** Long enough to read as the panel opening, short enough not to be waited on. */
39
+ export const COLLAPSE_DURATION = 200;
40
+
41
+ export interface CollapseProps extends Omit<ViewProps, 'children'> {
42
+ open: boolean;
43
+ /** Classes on the measured content, not on the clipping frame. */
44
+ className?: string;
45
+ duration?: number;
46
+ children?: ReactNode;
47
+ }
48
+
49
+ export function Collapse({
50
+ open,
51
+ className,
52
+ duration = COLLAPSE_DURATION,
53
+ children,
54
+ ...props
55
+ }: CollapseProps) {
56
+ const reducedMotion = useReducedMotion();
57
+ const [height, setHeight] = useState(0);
58
+ const [animating, setAnimating] = useState(false);
59
+ const progress = useSharedValue(open ? 1 : 0);
60
+ const mounted = useRef(false);
61
+
62
+ useEffect(() => {
63
+ // The first pass is not a transition — a body that starts open should be
64
+ // open, not play its own entrance at nobody.
65
+ if (!mounted.current) {
66
+ mounted.current = true;
67
+ progress.value = open ? 1 : 0;
68
+ return;
69
+ }
70
+ if (reducedMotion) {
71
+ progress.value = open ? 1 : 0;
72
+ setAnimating(false);
73
+ return;
74
+ }
75
+ setAnimating(true);
76
+ progress.value = withTiming(open ? 1 : 0, { duration }, (finished) => {
77
+ if (finished) runOnJS(setAnimating)(false);
78
+ });
79
+ }, [open, reducedMotion, duration, progress]);
80
+
81
+ const style = useAnimatedStyle(() => ({
82
+ height: progress.value * height,
83
+ opacity: progress.value,
84
+ }));
85
+
86
+ // Re-measured rather than measured once: a body whose content is still
87
+ // streaming in grows, and a height captured on the first frame would crop it.
88
+ const onLayout = (event: LayoutChangeEvent) => {
89
+ const next = event.nativeEvent.layout.height;
90
+ setHeight((current) => (Math.abs(current - next) < 1 ? current : next));
91
+ };
92
+
93
+ /*
94
+ * Once it has finished opening, the measured height and the absolute
95
+ * positioning are both dropped and the content lays out normally.
96
+ *
97
+ * They are only needed *while* the height is being animated to a number, and
98
+ * keeping them costs real bugs: a Collapse whose content is itself a Collapse
99
+ * measures a child that has not settled yet, and settles on that wrong
100
+ * number, so the inner body ends up clipped to nothing. Content that is still
101
+ * streaming in has the same problem against itself. In natural flow both are
102
+ * simply laid out, and the measurement carries on in the background against
103
+ * the next time it closes.
104
+ */
105
+ const natural = open && !animating;
106
+
107
+ return (
108
+ <Animated.View
109
+ style={natural ? undefined : style}
110
+ className={natural ? undefined : 'overflow-hidden'}
111
+ >
112
+ {/*
113
+ The same two elements in both modes, so switching between them is a
114
+ style change rather than a remount — a remount would throw away the
115
+ state of everything inside.
116
+ */}
117
+ <View
118
+ onLayout={onLayout}
119
+ style={natural ? undefined : { position: 'absolute', left: 0, right: 0, top: 0 }}
120
+ className={cn(className)}
121
+ {...props}
122
+ >
123
+ {children}
124
+ </View>
125
+ </Animated.View>
126
+ );
127
+ }
128
+ Collapse.displayName = 'Collapse';