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
@@ -0,0 +1,325 @@
1
+ /**
2
+ * Plan — what an agent intends to do, before it does it.
3
+ *
4
+ * A card rather than a run of rows, because a plan is a thing the reader is
5
+ * being asked to approve: it needs an edge around it so that where it starts
6
+ * and stops is not a matter of interpretation, and a footer that can hold the
7
+ * button that approves it.
8
+ *
9
+ * It streams. `isStreaming` puts a shimmer on the title and the description,
10
+ * which is the honest way to render a heading that is still arriving — the
11
+ * alternative is a title that grows a word at a time and reads as finished at
12
+ * every intermediate length.
13
+ *
14
+ * ```tsx
15
+ * <Plan isStreaming={streaming}>
16
+ * <Plan.Header>
17
+ * <Plan.Title>Migrate the calendar</Plan.Title>
18
+ * <Plan.Description>Four files, no API change.</Plan.Description>
19
+ * <Plan.Action><Plan.Trigger /></Plan.Action>
20
+ * </Plan.Header>
21
+ * <Plan.Content>…</Plan.Content>
22
+ * <Plan.Footer><Button>Approve</Button></Plan.Footer>
23
+ * </Plan>
24
+ * ```
25
+ *
26
+ * ## Where the props come from
27
+ *
28
+ * With the AI SDK a plan is usually an `experimental_useObject` stream, where
29
+ * `isStreaming` is the hook's `isLoading` and the fields arrive one at a time —
30
+ * which is exactly the case the shimmer exists for, since a partial object has
31
+ * a title before it has anything else.
32
+ */
33
+ import {
34
+ createContext,
35
+ useCallback,
36
+ useContext,
37
+ useEffect,
38
+ useMemo,
39
+ useState,
40
+ type ReactNode,
41
+ } from 'react';
42
+ import { Pressable, View, type PressableProps, type ViewProps } from 'react-native';
43
+ import Animated, {
44
+ useAnimatedStyle,
45
+ useReducedMotion,
46
+ useSharedValue,
47
+ withTiming,
48
+ } from 'react-native-reanimated';
49
+ import { tv } from 'tailwind-variants';
50
+ import { ChevronDownIcon } from '../../icons';
51
+ import { Collapse } from '../../primitives/collapse';
52
+ import { Text, textChildren } from '../../primitives/text';
53
+ import { cn } from '../../utils/cn';
54
+ import { Shimmer } from '../shimmer';
55
+
56
+ const planVariants = tv({
57
+ slots: {
58
+ root: 'w-full overflow-hidden rounded-2xl border border-border bg-card',
59
+ header: 'flex-row items-start gap-3 p-4',
60
+ heading: 'min-w-0 flex-1 gap-1',
61
+ title: 'text-base font-semibold text-card-foreground',
62
+ description: 'text-sm text-muted-foreground',
63
+ action: 'shrink-0 flex-row items-center gap-1',
64
+ trigger: 'h-8 w-8 items-center justify-center rounded-full active:bg-accent',
65
+ content: 'gap-2 px-4 pb-4',
66
+ footer: 'flex-row items-center justify-end gap-2 border-t border-border p-4',
67
+ },
68
+ });
69
+
70
+ interface PlanContextValue {
71
+ isStreaming: boolean;
72
+ open: boolean;
73
+ setOpen: (open: boolean) => void;
74
+ }
75
+
76
+ const PlanContext = createContext<PlanContextValue | null>(null);
77
+
78
+ function usePlan(component: string): PlanContextValue {
79
+ const context = useContext(PlanContext);
80
+ if (!context) {
81
+ throw new Error(`${component} must be used within a <Plan>`);
82
+ }
83
+ return context;
84
+ }
85
+
86
+ export interface PlanProps extends Omit<ViewProps, 'children'> {
87
+ className?: string;
88
+ /** Whether the plan is still being written. Shimmers the title and description. */
89
+ isStreaming?: boolean;
90
+ /** Controlled open state of the body. */
91
+ open?: boolean;
92
+ /** Initial state when uncontrolled. */
93
+ defaultOpen?: boolean;
94
+ onOpenChange?: (open: boolean) => void;
95
+ children?: ReactNode;
96
+ }
97
+
98
+ function PlanRoot({
99
+ className,
100
+ isStreaming = false,
101
+ open: openProp,
102
+ defaultOpen = true,
103
+ onOpenChange,
104
+ children,
105
+ ...props
106
+ }: PlanProps) {
107
+ const { root } = planVariants();
108
+ const [internalOpen, setInternalOpen] = useState(defaultOpen);
109
+ const isControlled = openProp !== undefined;
110
+ const open = isControlled ? openProp : internalOpen;
111
+
112
+ const setOpen = useCallback(
113
+ (next: boolean) => {
114
+ if (!isControlled) setInternalOpen(next);
115
+ onOpenChange?.(next);
116
+ },
117
+ [isControlled, onOpenChange]
118
+ );
119
+
120
+ const context = useMemo(
121
+ () => ({ isStreaming, open, setOpen }),
122
+ [isStreaming, open, setOpen]
123
+ );
124
+
125
+ return (
126
+ <PlanContext.Provider value={context}>
127
+ <View {...props} className={cn(root(), className)}>
128
+ {children}
129
+ </View>
130
+ </PlanContext.Provider>
131
+ );
132
+ }
133
+ PlanRoot.displayName = 'Plan';
134
+
135
+ export interface PlanHeaderProps extends ViewProps {
136
+ className?: string;
137
+ children?: ReactNode;
138
+ }
139
+
140
+ /**
141
+ * The title, the description and whatever acts on them.
142
+ *
143
+ * The heading is wrapped in a column of its own so the action stays pinned to
144
+ * the trailing edge as the title wraps, rather than riding down with it.
145
+ */
146
+ function PlanHeader({ className, children, ...props }: PlanHeaderProps) {
147
+ const { header, heading } = planVariants();
148
+
149
+ const headings: ReactNode[] = [];
150
+ const actions: ReactNode[] = [];
151
+ for (const child of Array.isArray(children) ? children : [children]) {
152
+ if (isPlanAction(child)) actions.push(child);
153
+ else headings.push(child);
154
+ }
155
+
156
+ return (
157
+ <View {...props} className={cn(header(), className)}>
158
+ <View className={heading()}>{headings}</View>
159
+ {actions}
160
+ </View>
161
+ );
162
+ }
163
+
164
+ function isPlanAction(child: ReactNode): boolean {
165
+ return (
166
+ typeof child === 'object' &&
167
+ child !== null &&
168
+ 'type' in child &&
169
+ (child as { type?: unknown }).type === PlanAction
170
+ );
171
+ }
172
+
173
+ export interface PlanTitleProps extends ViewProps {
174
+ className?: string;
175
+ children?: ReactNode;
176
+ }
177
+
178
+ /** The plan's name. Shimmers while it is still being written. */
179
+ function PlanTitle({ className, children, ...props }: PlanTitleProps) {
180
+ const { isStreaming } = usePlan('Plan.Title');
181
+ const { title } = planVariants();
182
+
183
+ if (isStreaming && typeof children === 'string') {
184
+ return <Shimmer textClassName={cn(title(), className)}>{children}</Shimmer>;
185
+ }
186
+
187
+ return (
188
+ <Text accessibilityRole="header" className={cn(title(), className)} {...props}>
189
+ {children}
190
+ </Text>
191
+ );
192
+ }
193
+
194
+ export interface PlanDescriptionProps extends ViewProps {
195
+ className?: string;
196
+ children?: ReactNode;
197
+ }
198
+
199
+ function PlanDescription({ className, children, ...props }: PlanDescriptionProps) {
200
+ const { isStreaming } = usePlan('Plan.Description');
201
+ const { description } = planVariants();
202
+
203
+ if (isStreaming && typeof children === 'string') {
204
+ return <Shimmer textClassName={cn(description(), className)}>{children}</Shimmer>;
205
+ }
206
+
207
+ return (
208
+ <Text className={cn(description(), className)} {...props}>
209
+ {children}
210
+ </Text>
211
+ );
212
+ }
213
+
214
+ export interface PlanActionProps extends ViewProps {
215
+ className?: string;
216
+ children?: ReactNode;
217
+ }
218
+
219
+ /** Pinned to the header's trailing edge — the toggle, a badge, a menu. */
220
+ function PlanAction({ className, children, ...props }: PlanActionProps) {
221
+ const { action } = planVariants();
222
+ return (
223
+ <View {...props} className={cn(action(), className)}>
224
+ {children}
225
+ </View>
226
+ );
227
+ }
228
+
229
+ export interface PlanTriggerProps extends Omit<PressableProps, 'children' | 'style'> {
230
+ className?: string;
231
+ children?: ReactNode;
232
+ }
233
+
234
+ /** Folds the body away. Its chevron turns to point at the state it will reach. */
235
+ function PlanTrigger({ className, children, onPress, ...props }: PlanTriggerProps) {
236
+ const { open, setOpen } = usePlan('Plan.Trigger');
237
+ const { trigger } = planVariants();
238
+ const reducedMotion = useReducedMotion();
239
+ const progress = useSharedValue(open ? 1 : 0);
240
+
241
+ useEffect(() => {
242
+ progress.value = reducedMotion
243
+ ? open
244
+ ? 1
245
+ : 0
246
+ : withTiming(open ? 1 : 0, { duration: 180 });
247
+ }, [open, reducedMotion, progress]);
248
+
249
+ const chevronStyle = useAnimatedStyle(() => ({
250
+ transform: [{ rotate: `${progress.value * 180}deg` }],
251
+ }));
252
+
253
+ return (
254
+ <Pressable
255
+ accessibilityRole="button"
256
+ accessibilityLabel={open ? 'Collapse plan' : 'Expand plan'}
257
+ accessibilityState={{ expanded: open }}
258
+ hitSlop={6}
259
+ onPress={(event) => {
260
+ onPress?.(event);
261
+ setOpen(!open);
262
+ }}
263
+ className={cn(trigger(), className)}
264
+ {...props}
265
+ >
266
+ {children ?? (
267
+ <Animated.View style={chevronStyle}>
268
+ <ChevronDownIcon size={16} />
269
+ </Animated.View>
270
+ )}
271
+ </Pressable>
272
+ );
273
+ }
274
+
275
+ export interface PlanContentProps extends Omit<ViewProps, 'children'> {
276
+ className?: string;
277
+ children?: ReactNode;
278
+ }
279
+
280
+ /** The steps. Collapses rather than unmounting, so it can still be growing. */
281
+ function PlanContent({ className, children, ...props }: PlanContentProps) {
282
+ const { open } = usePlan('Plan.Content');
283
+ const { content, description } = planVariants();
284
+
285
+ return (
286
+ <Collapse open={open} className={cn(content(), className)} {...props}>
287
+ {textChildren(children, (text) => (
288
+ <Text className={description()}>{text}</Text>
289
+ ))}
290
+ </Collapse>
291
+ );
292
+ }
293
+
294
+ export interface PlanFooterProps extends ViewProps {
295
+ className?: string;
296
+ children?: ReactNode;
297
+ }
298
+
299
+ /** Where the button that approves the plan goes. */
300
+ function PlanFooter({ className, children, ...props }: PlanFooterProps) {
301
+ const { footer } = planVariants();
302
+ return (
303
+ <View {...props} className={cn(footer(), className)}>
304
+ {children}
305
+ </View>
306
+ );
307
+ }
308
+
309
+ PlanHeader.displayName = 'Plan.Header';
310
+ PlanTitle.displayName = 'Plan.Title';
311
+ PlanDescription.displayName = 'Plan.Description';
312
+ PlanAction.displayName = 'Plan.Action';
313
+ PlanTrigger.displayName = 'Plan.Trigger';
314
+ PlanContent.displayName = 'Plan.Content';
315
+ PlanFooter.displayName = 'Plan.Footer';
316
+
317
+ export const Plan = Object.assign(PlanRoot, {
318
+ Header: PlanHeader,
319
+ Title: PlanTitle,
320
+ Description: PlanDescription,
321
+ Action: PlanAction,
322
+ Trigger: PlanTrigger,
323
+ Content: PlanContent,
324
+ Footer: PlanFooter,
325
+ });
@@ -44,6 +44,7 @@ import {
44
44
  } from 'react';
45
45
  import {
46
46
  Pressable,
47
+ ScrollView,
47
48
  useWindowDimensions,
48
49
  View,
49
50
  type LayoutChangeEvent,
@@ -228,6 +229,40 @@ export interface PopoverContentProps extends ViewProps {
228
229
  * content into a column.
229
230
  */
230
231
  minWidth?: number;
232
+ /**
233
+ * Ceiling for the panel's height, in pixels. Always clamped to the room
234
+ * inside the safe area, which is also the default — a panel is never
235
+ * positioned so that part of it falls off the screen, because the part that
236
+ * falls off cannot be scrolled back into view.
237
+ */
238
+ maxHeight?: number;
239
+ /**
240
+ * Scroll the panel's body when it is taller than `maxHeight`.
241
+ *
242
+ * Off by default, because a popover is usually a paragraph or a short form
243
+ * and a scroller around either one only adds a bounce. Worth turning on for
244
+ * a list of unknown length, which is the case where the cap actually bites.
245
+ *
246
+ * The spacing between children moves to the scroller's content when this is
247
+ * set; `className` still dresses the panel itself.
248
+ */
249
+ scrollable?: boolean;
250
+ /**
251
+ * Drop the panel's own surface — its background, border, radius, padding and
252
+ * shadow — and keep only its position and its size. For a caller that draws
253
+ * the surface itself, so that something can be put *behind* the content
254
+ * rather than layered on top of a background that is already painted.
255
+ *
256
+ * The panel is still clipped to a rounded rectangle, because a surface drawn
257
+ * inside it has to have something to be clipped by.
258
+ */
259
+ unstyled?: boolean;
260
+ /**
261
+ * A layer drawn inside the panel, behind its content — and, crucially,
262
+ * outside its scroller, so that a surface does not scroll away with the rows
263
+ * on top of it. Pair it with `unstyled` to own the panel's appearance.
264
+ */
265
+ background?: ReactNode;
231
266
  /** Tap outside the panel closes it. Default true. */
232
267
  dismissible?: boolean;
233
268
  /**
@@ -247,6 +282,10 @@ function PopoverContent({
247
282
  alignOffset = 0,
248
283
  width = 'content-fit',
249
284
  minWidth,
285
+ maxHeight,
286
+ scrollable = false,
287
+ unstyled = false,
288
+ background,
250
289
  dismissible = true,
251
290
  blur = false,
252
291
  children,
@@ -303,6 +342,17 @@ function PopoverContent({
303
342
  ? Math.min(Math.max(requestedWidth, minWidth), available)
304
343
  : requestedWidth;
305
344
 
345
+ /*
346
+ * The cap is what keeps a tall panel reachable. `place` clamps the panel
347
+ * inside the bounds, but a panel taller than the bounds cannot be clamped
348
+ * into them — it gets pinned to the top edge and the rest runs off the
349
+ * bottom of the screen, where there is no way to get at it. Capping the
350
+ * height first means the clamp always has a solution, and `scrollable`
351
+ * hands the overflow back to the finger.
352
+ */
353
+ const room = bounds.bottom - bounds.top;
354
+ const resolvedMaxHeight = maxHeight === undefined ? room : Math.min(maxHeight, room);
355
+
306
356
  const position =
307
357
  trigger && size
308
358
  ? place({ trigger, size, placement, align, offset, alignOffset, bounds })
@@ -434,6 +484,7 @@ function PopoverContent({
434
484
  top: position?.top ?? -9999,
435
485
  left: position?.left ?? -9999,
436
486
  maxWidth: bounds.right - bounds.left,
487
+ maxHeight: resolvedMaxHeight,
437
488
  width: resolvedWidth,
438
489
  }}
439
490
  >
@@ -441,12 +492,28 @@ function PopoverContent({
441
492
  accessibilityViewIsModal
442
493
  style={[panelStyle, style]}
443
494
  className={cn(
444
- 'gap-1.5 rounded-2xl border border-border bg-popover p-4 shadow-lg',
495
+ // `unstyled` keeps the clip and drops everything that paints,
496
+ // so a caller can put its own layer underneath the content.
497
+ unstyled
498
+ ? 'overflow-hidden rounded-2xl'
499
+ : 'rounded-2xl border border-border bg-popover p-4 shadow-lg',
500
+ // The gap belongs to whichever view actually holds the
501
+ // children — on the panel normally, on the scroller's content
502
+ // when there is one, since the scroller is then the panel's
503
+ // only child and a gap around it spaces nothing.
504
+ scrollable ? 'overflow-hidden' : 'gap-1.5',
445
505
  className
446
506
  )}
447
507
  {...props}
448
508
  >
449
- {textChildren(children)}
509
+ {background}
510
+ {scrollable ? (
511
+ <ScrollView bounces={false} showsVerticalScrollIndicator={false}>
512
+ <View className="gap-1.5">{textChildren(children)}</View>
513
+ </ScrollView>
514
+ ) : (
515
+ textChildren(children)
516
+ )}
450
517
  </Animated.View>
451
518
  </Animated.View>
452
519
  </View>
@@ -211,13 +211,22 @@ const RadioGroupItem = forwardRef<View, RadioGroupItemProps>(
211
211
  </View>
212
212
  );
213
213
 
214
+ // Either way the label is a string in a row that is otherwise all views,
215
+ // so children given instead of the prop are dressed the same way rather
216
+ // than reaching the pressable bare.
217
+ const labelled = label ? (
218
+ <Text className={slots.label()}>{label}</Text>
219
+ ) : (
220
+ textChildren(children, (text) => <Text className={slots.label()}>{text}</Text>)
221
+ );
222
+
214
223
  // The card lays its text out first so the disc sits at the trailing edge;
215
224
  // the dot row keeps the classic disc-then-label order.
216
225
  const body =
217
226
  variant === 'card' ? (
218
227
  <>
219
228
  <View className="flex-1 gap-1">
220
- {label ? <Text className={slots.label()}>{label}</Text> : children}
229
+ {labelled}
221
230
  {description ? (
222
231
  <Text className={slots.description()}>{description}</Text>
223
232
  ) : null}
@@ -227,7 +236,7 @@ const RadioGroupItem = forwardRef<View, RadioGroupItemProps>(
227
236
  ) : (
228
237
  <>
229
238
  {indicator}
230
- {label ? <Text className={slots.label()}>{label}</Text> : children}
239
+ {labelled}
231
240
  </>
232
241
  );
233
242