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.
Files changed (51) hide show
  1. package/README.md +16 -8
  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 +658 -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 +482 -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
@@ -0,0 +1,482 @@
1
+ /**
2
+ * Timeline — a vertical sequence of events.
3
+ *
4
+ * Adapted from: origin-space/originui apps/origin/registry/default/ui/timeline.tsx
5
+ * (the Item / Indicator / Separator / Header / Date / Title / Content anatomy
6
+ * and the completed-up-to-active state rule), shaped to match this repo's
7
+ * `Steps` component so the two read as siblings.
8
+ *
9
+ * Upstream drives styling through `data-*` attributes, which React Native has
10
+ * no equivalent for; state is resolved in JS and passed through context into
11
+ * `tv()` variants instead.
12
+ *
13
+ * Vertical only — a horizontal timeline gives each item roughly a fifth of a
14
+ * phone's width, which is not enough for a date and a title.
15
+ *
16
+ * ```tsx
17
+ * <Timeline variant="icon" value={2}>
18
+ * <Timeline.Item step={0} tone="info">
19
+ * <Timeline.Aside>
20
+ * <Timeline.Date>09:12</Timeline.Date>
21
+ * <Timeline.Label>Design</Timeline.Label>
22
+ * </Timeline.Aside>
23
+ * <Timeline.Indicator><SendIcon /></Timeline.Indicator>
24
+ * <Timeline.Content>
25
+ * <Timeline.Header>
26
+ * <Timeline.Title>Checkout language approved</Timeline.Title>
27
+ * <Timeline.Trailing>10:18</Timeline.Trailing>
28
+ * </Timeline.Header>
29
+ * <Timeline.Description>…</Timeline.Description>
30
+ * </Timeline.Content>
31
+ * </Timeline.Item>
32
+ * </Timeline>
33
+ * ```
34
+ */
35
+ import {
36
+ createContext,
37
+ forwardRef,
38
+ useContext,
39
+ useMemo,
40
+ type ReactNode,
41
+ } from 'react';
42
+ import { View, type Text as RNText, type ViewProps } from 'react-native';
43
+ import { tv } from 'tailwind-variants';
44
+ import { useCSSVariable } from 'uniwind';
45
+ import { IconColorProvider } from '../../icons';
46
+ import { Text, type TextProps } from '../../primitives/text';
47
+
48
+ export type TimelineVariant = 'dot' | 'icon' | 'numbered' | 'card' | 'compact';
49
+ /** Semantic colour for a single event, independent of progress. */
50
+ export type TimelineTone = 'default' | 'info' | 'success' | 'warning' | 'danger';
51
+
52
+ /** Variants whose node is a filled disc rather than an outlined ring. */
53
+ const SOLID_VARIANTS: TimelineVariant[] = ['dot', 'card'];
54
+
55
+ const timelineVariants = tv({
56
+ slots: {
57
+ root: 'w-full flex-col',
58
+ item: 'w-full flex-row gap-3',
59
+ aside: 'w-20 items-end gap-0.5 pt-0.5',
60
+ rail: 'items-center',
61
+ // Flat discs, no ring — the same restraint Steps uses. Rings on every node
62
+ // were most of what made the timeline read as noisy.
63
+ indicator: 'items-center justify-center rounded-full',
64
+ indicatorLabel: 'text-xs font-medium',
65
+ separator: 'w-0.5 flex-1 rounded-full',
66
+ body: 'flex-1 pb-6',
67
+ panel: '',
68
+ header: 'flex-row items-center gap-2',
69
+ heading: 'flex-1 gap-0.5',
70
+ date: 'text-xs font-medium text-muted-foreground',
71
+ label: 'text-xs font-medium',
72
+ meta: 'text-xs text-muted-foreground',
73
+ title: 'text-sm font-medium text-foreground',
74
+ trailing: 'text-xs text-muted-foreground',
75
+ content: 'pt-1 text-sm text-muted-foreground',
76
+ stats: 'mt-2 flex-row gap-6 rounded-xl border border-border px-3.5 py-2.5',
77
+ statLabel: 'text-xs text-muted-foreground',
78
+ statValue: 'text-sm font-medium text-foreground',
79
+ },
80
+ variants: {
81
+ variant: {
82
+ dot: { indicator: 'h-4 w-4' },
83
+ icon: { indicator: 'h-8 w-8' },
84
+ numbered: { indicator: 'h-7 w-7' },
85
+ card: {
86
+ indicator: 'h-4 w-4',
87
+ // The gap stays on `body` so the connector runs through it; the card
88
+ // chrome lives on an inner panel. Putting the border on `body` made
89
+ // consecutive cards sit flush against each other.
90
+ panel: 'rounded-xl border border-border bg-card p-3.5',
91
+ },
92
+ compact: {
93
+ indicator: 'h-6 w-6',
94
+ body: 'flex-1 pb-3',
95
+ title: 'text-base',
96
+ },
97
+ },
98
+ tone: {
99
+ default: {},
100
+ info: { label: 'text-info-foreground' },
101
+ success: { label: 'text-success-foreground' },
102
+ warning: { label: 'text-warning-foreground' },
103
+ danger: { label: 'text-destructive-foreground' },
104
+ },
105
+ completed: {
106
+ true: { separator: 'bg-primary' },
107
+ false: { separator: 'bg-muted' },
108
+ },
109
+ },
110
+ defaultVariants: {
111
+ variant: 'dot',
112
+ tone: 'default',
113
+ completed: false,
114
+ },
115
+ });
116
+
117
+ /*
118
+ * Colour rule, matching the Steps component: progress is solid, event kind is
119
+ * tinted.
120
+ *
121
+ * - untoned pending → bg-muted (Steps' inactive)
122
+ * - untoned completed → bg-primary (Steps' completed)
123
+ * - toned → bg-{tone}-soft, content in the tone's foreground
124
+ *
125
+ * The `-soft` fills are the same tinted tokens Alert uses, so a toned node sits
126
+ * in the same family as the rest of the library instead of shouting in raw
127
+ * brand colour.
128
+ *
129
+ * Nodes that hold something — an icon or a number — get a hairline so they read
130
+ * as a container. The `dot` and `card` discs stay bare: at 16px an outline is
131
+ * most of what made the original look busy. The border therefore lives on the
132
+ * tone class rather than the shared `indicator` slot, so a solid disc cannot
133
+ * pick one up by accident.
134
+ */
135
+ const TONE_NODE: Record<TimelineTone, { solid: string; tinted: string }> = {
136
+ default: { solid: 'bg-primary', tinted: 'border border-border bg-muted' },
137
+ info: { solid: 'bg-info', tinted: 'border border-info/32 bg-info-soft' },
138
+ success: { solid: 'bg-success', tinted: 'border border-success/32 bg-success-soft' },
139
+ warning: { solid: 'bg-warning', tinted: 'border border-warning/32 bg-warning-soft' },
140
+ danger: {
141
+ solid: 'bg-destructive',
142
+ tinted: 'border border-destructive/32 bg-destructive-soft',
143
+ },
144
+ };
145
+
146
+ /**
147
+ * The node an incomplete, untoned step gets — bordered when it holds an icon
148
+ * or a number, bare when it is a status disc.
149
+ */
150
+ const PENDING_NODE = {
151
+ solid: 'bg-muted',
152
+ tinted: 'border border-border bg-muted',
153
+ } as const;
154
+
155
+ /**
156
+ * CSS variable a tinted node's contents take their colour from. The
157
+ * `-foreground` tokens are the ones tuned to read in both light and dark,
158
+ * which is what sits on a soft tint.
159
+ */
160
+ const TONE_ICON_VAR: Record<TimelineTone, string> = {
161
+ default: '--color-primary',
162
+ info: '--color-info-foreground',
163
+ success: '--color-success-foreground',
164
+ warning: '--color-warning-foreground',
165
+ danger: '--color-destructive-foreground',
166
+ };
167
+
168
+ interface TimelineContextValue {
169
+ activeStep: number;
170
+ variant: TimelineVariant;
171
+ }
172
+
173
+ interface TimelineItemContextValue {
174
+ step: number;
175
+ completed: boolean;
176
+ tone: TimelineTone;
177
+ /** False on the last item, so its rail does not trail into nothing. */
178
+ showSeparator: boolean;
179
+ }
180
+
181
+ const TimelineContext = createContext<TimelineContextValue | null>(null);
182
+ const TimelineItemContext = createContext<TimelineItemContextValue | null>(null);
183
+
184
+ function useTimeline(component: string): TimelineContextValue {
185
+ const context = useContext(TimelineContext);
186
+ if (!context) throw new Error(`${component} must be used within a <Timeline>`);
187
+ return context;
188
+ }
189
+
190
+ function useTimelineItem(component: string): TimelineItemContextValue {
191
+ const context = useContext(TimelineItemContext);
192
+ if (!context) throw new Error(`${component} must be used within a <Timeline.Item>`);
193
+ return context;
194
+ }
195
+
196
+ export interface TimelineProps extends ViewProps {
197
+ className?: string;
198
+ /** Steps at or below this index render as completed. */
199
+ value?: number;
200
+ variant?: TimelineVariant;
201
+ children?: ReactNode;
202
+ }
203
+
204
+ const TimelineRoot = forwardRef<View, TimelineProps>(
205
+ ({ className, value = 0, variant = 'dot', children, ...props }, ref) => {
206
+ const { root } = timelineVariants({ variant });
207
+ const context = useMemo(() => ({ activeStep: value, variant }), [value, variant]);
208
+
209
+ return (
210
+ <TimelineContext.Provider value={context}>
211
+ <View ref={ref} className={root({ className })} {...props}>
212
+ {children}
213
+ </View>
214
+ </TimelineContext.Provider>
215
+ );
216
+ }
217
+ );
218
+ TimelineRoot.displayName = 'Timeline';
219
+
220
+ export interface TimelineItemProps extends ViewProps {
221
+ className?: string;
222
+ /** Position in the sequence, zero-based. */
223
+ step: number;
224
+ /** Force the completed state regardless of the timeline's value. */
225
+ completed?: boolean;
226
+ /** Colours the node and label — for event kind rather than progress. */
227
+ tone?: TimelineTone;
228
+ /** Set on the final item so its rail stops at the indicator. */
229
+ last?: boolean;
230
+ children?: ReactNode;
231
+ }
232
+
233
+ const TimelineItem = forwardRef<View, TimelineItemProps>(
234
+ (
235
+ { className, step, completed, tone = 'default', last = false, children, ...props },
236
+ ref
237
+ ) => {
238
+ const { activeStep, variant } = useTimeline('Timeline.Item');
239
+ const isCompleted = completed ?? step <= activeStep;
240
+ const { item } = timelineVariants({ variant, tone, completed: isCompleted });
241
+
242
+ const context = useMemo(
243
+ () => ({ step, completed: isCompleted, tone, showSeparator: !last }),
244
+ [step, isCompleted, tone, last]
245
+ );
246
+
247
+ return (
248
+ <TimelineItemContext.Provider value={context}>
249
+ <View ref={ref} className={item({ className })} {...props}>
250
+ {children}
251
+ </View>
252
+ </TimelineItemContext.Provider>
253
+ );
254
+ }
255
+ );
256
+ TimelineItem.displayName = 'Timeline.Item';
257
+
258
+ /**
259
+ * Right-aligned meta column to the left of the rail — a time, a category, a
260
+ * person. Place it before `Timeline.Indicator`.
261
+ */
262
+ const TimelineAside = forwardRef<View, ViewProps & { className?: string }>(
263
+ ({ className, ...props }, ref) => {
264
+ const { variant } = useTimeline('Timeline.Aside');
265
+ const { aside } = timelineVariants({ variant });
266
+ return <View ref={ref} className={aside({ className })} {...props} />;
267
+ }
268
+ );
269
+ TimelineAside.displayName = 'Timeline.Aside';
270
+
271
+ export interface TimelineIndicatorProps extends ViewProps {
272
+ className?: string;
273
+ /** Replaces the default node contents — an icon, say. */
274
+ children?: ReactNode;
275
+ }
276
+
277
+ /**
278
+ * The node on the rail, with the connector running below it.
279
+ *
280
+ * Children are wrapped in an `IconColorProvider` carrying the colour that
281
+ * reads against this node, so an icon inside follows the theme instead of
282
+ * disappearing when the fill inverts.
283
+ */
284
+ const TimelineIndicator = forwardRef<View, TimelineIndicatorProps>(
285
+ ({ className, children, ...props }, ref) => {
286
+ const { variant } = useTimeline('Timeline.Indicator');
287
+ const { step, completed, tone, showSeparator } =
288
+ useTimelineItem('Timeline.Indicator');
289
+ const { rail, indicator, indicatorLabel, separator } = timelineVariants({
290
+ variant,
291
+ tone,
292
+ completed,
293
+ });
294
+
295
+ // dot/card nodes are small discs with nothing inside, so they keep full
296
+ // saturation — a soft tint at 16px would disappear. Nodes that hold an
297
+ // icon or a number take the tinted fill instead.
298
+ const isDisc = SOLID_VARIANTS.includes(variant);
299
+ const toned = tone !== 'default';
300
+
301
+ const fill = isDisc ? 'solid' : 'tinted';
302
+ const nodeClass = toned
303
+ ? TONE_NODE[tone][fill]
304
+ : completed
305
+ ? TONE_NODE.default.solid
306
+ : PENDING_NODE[fill];
307
+
308
+ // Contents sit on the tint (or on primary once complete), so they take the
309
+ // colour tuned to read against it.
310
+ const tintedContent = useCSSVariable(
311
+ toned ? TONE_ICON_VAR[tone] : '--color-muted-foreground'
312
+ );
313
+ const solidContent = useCSSVariable('--color-primary-foreground');
314
+
315
+ const resolved = (value: unknown) =>
316
+ typeof value === 'string' ? value : undefined;
317
+
318
+ const contentColor =
319
+ !toned && completed ? resolved(solidContent) : resolved(tintedContent);
320
+
321
+ return (
322
+ <View className={rail()}>
323
+ <IconColorProvider color={contentColor}>
324
+ <View ref={ref} className={indicator({ className: `${nodeClass} ${className ?? ''}` })} {...props}>
325
+ {variant === 'numbered' ? (
326
+ <Text className={indicatorLabel()} style={{ color: contentColor }}>
327
+ {step + 1}
328
+ </Text>
329
+ ) : variant === 'icon' || variant === 'compact' ? (
330
+ children
331
+ ) : null}
332
+ </View>
333
+ </IconColorProvider>
334
+ {showSeparator ? (
335
+ <View
336
+ accessibilityElementsHidden
337
+ importantForAccessibility="no-hide-descendants"
338
+ className={separator()}
339
+ />
340
+ ) : null}
341
+ </View>
342
+ );
343
+ }
344
+ );
345
+ TimelineIndicator.displayName = 'Timeline.Indicator';
346
+
347
+ /**
348
+ * Everything to the right of the rail. Under `variant="card"` the children are
349
+ * wrapped in the card panel, while the spacing between items stays outside it
350
+ * so the connector runs unbroken.
351
+ */
352
+ const TimelineContent = forwardRef<View, ViewProps & { className?: string }>(
353
+ ({ className, children, ...props }, ref) => {
354
+ const { variant } = useTimeline('Timeline.Content');
355
+ const { completed, tone } = useTimelineItem('Timeline.Content');
356
+ const { body, panel } = timelineVariants({ variant, tone, completed });
357
+
358
+ return (
359
+ <View ref={ref} className={body({ className })} {...props}>
360
+ {variant === 'card' ? <View className={panel()}>{children}</View> : children}
361
+ </View>
362
+ );
363
+ }
364
+ );
365
+ TimelineContent.displayName = 'Timeline.Content';
366
+
367
+ /** Title row: heading on the left, `Timeline.Trailing` on the right. */
368
+ const TimelineHeader = forwardRef<View, ViewProps & { className?: string }>(
369
+ ({ className, ...props }, ref) => {
370
+ const { variant } = useTimeline('Timeline.Header');
371
+ const { header } = timelineVariants({ variant });
372
+ return <View ref={ref} className={header({ className })} {...props} />;
373
+ }
374
+ );
375
+ TimelineHeader.displayName = 'Timeline.Header';
376
+
377
+ /** Wraps a title and anything stacked under it inside the header row. */
378
+ const TimelineHeading = forwardRef<View, ViewProps & { className?: string }>(
379
+ ({ className, ...props }, ref) => {
380
+ const { variant } = useTimeline('Timeline.Heading');
381
+ const { heading } = timelineVariants({ variant });
382
+ return <View ref={ref} className={heading({ className })} {...props} />;
383
+ }
384
+ );
385
+ TimelineHeading.displayName = 'Timeline.Heading';
386
+
387
+ const TimelineDate = forwardRef<RNText, TextProps>(({ className, ...props }, ref) => {
388
+ const { variant } = useTimeline('Timeline.Date');
389
+ const { date } = timelineVariants({ variant });
390
+ return <Text ref={ref} className={date({ className })} {...props} />;
391
+ });
392
+ TimelineDate.displayName = 'Timeline.Date';
393
+
394
+ /** Category line in the aside, coloured by the item's tone. */
395
+ const TimelineLabel = forwardRef<RNText, TextProps>(({ className, ...props }, ref) => {
396
+ const { variant } = useTimeline('Timeline.Label');
397
+ const { tone } = useTimelineItem('Timeline.Label');
398
+ const { label } = timelineVariants({ variant, tone });
399
+ return <Text ref={ref} className={label({ className })} {...props} />;
400
+ });
401
+ TimelineLabel.displayName = 'Timeline.Label';
402
+
403
+ /** Muted supporting line — a person's name, a source. */
404
+ const TimelineMeta = forwardRef<RNText, TextProps>(({ className, ...props }, ref) => {
405
+ const { variant } = useTimeline('Timeline.Meta');
406
+ const { meta } = timelineVariants({ variant });
407
+ return <Text ref={ref} className={meta({ className })} {...props} />;
408
+ });
409
+ TimelineMeta.displayName = 'Timeline.Meta';
410
+
411
+ const TimelineTitle = forwardRef<RNText, TextProps>(({ className, ...props }, ref) => {
412
+ const { variant } = useTimeline('Timeline.Title');
413
+ const { title } = timelineVariants({ variant });
414
+ return <Text ref={ref} className={title({ className })} {...props} />;
415
+ });
416
+ TimelineTitle.displayName = 'Timeline.Title';
417
+
418
+ /** Right-hand slot in the header row — a timestamp, usually. */
419
+ const TimelineTrailing = forwardRef<RNText, TextProps>(
420
+ ({ className, ...props }, ref) => {
421
+ const { variant } = useTimeline('Timeline.Trailing');
422
+ const { trailing } = timelineVariants({ variant });
423
+ return <Text ref={ref} className={trailing({ className })} {...props} />;
424
+ }
425
+ );
426
+ TimelineTrailing.displayName = 'Timeline.Trailing';
427
+
428
+ const TimelineDescription = forwardRef<RNText, TextProps>(
429
+ ({ className, ...props }, ref) => {
430
+ const { variant } = useTimeline('Timeline.Description');
431
+ const { content } = timelineVariants({ variant });
432
+ return <Text ref={ref} className={content({ className })} {...props} />;
433
+ }
434
+ );
435
+ TimelineDescription.displayName = 'Timeline.Description';
436
+
437
+ /** Bordered strip of label/value pairs under a title. */
438
+ const TimelineStats = forwardRef<View, ViewProps & { className?: string }>(
439
+ ({ className, ...props }, ref) => {
440
+ const { variant } = useTimeline('Timeline.Stats');
441
+ const { stats } = timelineVariants({ variant });
442
+ return <View ref={ref} className={stats({ className })} {...props} />;
443
+ }
444
+ );
445
+ TimelineStats.displayName = 'Timeline.Stats';
446
+
447
+ export interface TimelineStatProps extends ViewProps {
448
+ className?: string;
449
+ label: string;
450
+ value: string;
451
+ }
452
+
453
+ const TimelineStat = forwardRef<View, TimelineStatProps>(
454
+ ({ className, label, value, ...props }, ref) => {
455
+ const { variant } = useTimeline('Timeline.Stat');
456
+ const { statLabel, statValue } = timelineVariants({ variant });
457
+ return (
458
+ <View ref={ref} className={className} {...props}>
459
+ <Text className={statLabel()}>{label}</Text>
460
+ <Text className={statValue()}>{value}</Text>
461
+ </View>
462
+ );
463
+ }
464
+ );
465
+ TimelineStat.displayName = 'Timeline.Stat';
466
+
467
+ export const Timeline = Object.assign(TimelineRoot, {
468
+ Item: TimelineItem,
469
+ Aside: TimelineAside,
470
+ Indicator: TimelineIndicator,
471
+ Content: TimelineContent,
472
+ Header: TimelineHeader,
473
+ Heading: TimelineHeading,
474
+ Date: TimelineDate,
475
+ Label: TimelineLabel,
476
+ Meta: TimelineMeta,
477
+ Title: TimelineTitle,
478
+ Trailing: TimelineTrailing,
479
+ Description: TimelineDescription,
480
+ Stats: TimelineStats,
481
+ Stat: TimelineStat,
482
+ });