panelui-native 0.7.0 → 0.9.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 (77) hide show
  1. package/lib/module/components/attachment/index.js +219 -0
  2. package/lib/module/components/attachment/index.js.map +1 -0
  3. package/lib/module/components/bottom-sheet/index.js +15 -1
  4. package/lib/module/components/bottom-sheet/index.js.map +1 -1
  5. package/lib/module/components/dialog/index.js +6 -2
  6. package/lib/module/components/dialog/index.js.map +1 -1
  7. package/lib/module/components/empty-state/index.js +94 -17
  8. package/lib/module/components/empty-state/index.js.map +1 -1
  9. package/lib/module/components/line-chart/index.js +1024 -0
  10. package/lib/module/components/line-chart/index.js.map +1 -0
  11. package/lib/module/components/marker/index.js +190 -0
  12. package/lib/module/components/marker/index.js.map +1 -0
  13. package/lib/module/components/message/index.js +24 -8
  14. package/lib/module/components/message/index.js.map +1 -1
  15. package/lib/module/components/message-scroller/index.js +451 -0
  16. package/lib/module/components/message-scroller/index.js.map +1 -0
  17. package/lib/module/components/popover/index.js +500 -0
  18. package/lib/module/components/popover/index.js.map +1 -0
  19. package/lib/module/components/radio-group/index.js +104 -16
  20. package/lib/module/components/radio-group/index.js.map +1 -1
  21. package/lib/module/components/separator/index.js +91 -0
  22. package/lib/module/components/separator/index.js.map +1 -0
  23. package/lib/module/components/surface/index.js +21 -2
  24. package/lib/module/components/surface/index.js.map +1 -1
  25. package/lib/module/icons/index.js +69 -0
  26. package/lib/module/icons/index.js.map +1 -1
  27. package/lib/module/index.js +7 -1
  28. package/lib/module/index.js.map +1 -1
  29. package/lib/module/primitives/scrim.js +72 -0
  30. package/lib/module/primitives/scrim.js.map +1 -0
  31. package/lib/typescript/src/components/attachment/index.d.ts +93 -0
  32. package/lib/typescript/src/components/attachment/index.d.ts.map +1 -0
  33. package/lib/typescript/src/components/bottom-sheet/index.d.ts +6 -1
  34. package/lib/typescript/src/components/bottom-sheet/index.d.ts.map +1 -1
  35. package/lib/typescript/src/components/dialog/index.d.ts +7 -1
  36. package/lib/typescript/src/components/dialog/index.d.ts.map +1 -1
  37. package/lib/typescript/src/components/empty-state/index.d.ts +152 -2
  38. package/lib/typescript/src/components/empty-state/index.d.ts.map +1 -1
  39. package/lib/typescript/src/components/line-chart/index.d.ts +247 -0
  40. package/lib/typescript/src/components/line-chart/index.d.ts.map +1 -0
  41. package/lib/typescript/src/components/marker/index.d.ts +137 -0
  42. package/lib/typescript/src/components/marker/index.d.ts.map +1 -0
  43. package/lib/typescript/src/components/message/index.d.ts +8 -1
  44. package/lib/typescript/src/components/message/index.d.ts.map +1 -1
  45. package/lib/typescript/src/components/message-scroller/index.d.ts +150 -0
  46. package/lib/typescript/src/components/message-scroller/index.d.ts.map +1 -0
  47. package/lib/typescript/src/components/popover/index.d.ts +130 -0
  48. package/lib/typescript/src/components/popover/index.d.ts.map +1 -0
  49. package/lib/typescript/src/components/radio-group/index.d.ts +32 -0
  50. package/lib/typescript/src/components/radio-group/index.d.ts.map +1 -1
  51. package/lib/typescript/src/components/separator/index.d.ts +47 -0
  52. package/lib/typescript/src/components/separator/index.d.ts.map +1 -0
  53. package/lib/typescript/src/components/surface/index.d.ts +51 -2
  54. package/lib/typescript/src/components/surface/index.d.ts.map +1 -1
  55. package/lib/typescript/src/icons/index.d.ts +6 -0
  56. package/lib/typescript/src/icons/index.d.ts.map +1 -1
  57. package/lib/typescript/src/index.d.ts +7 -1
  58. package/lib/typescript/src/index.d.ts.map +1 -1
  59. package/lib/typescript/src/primitives/scrim.d.ts +21 -0
  60. package/lib/typescript/src/primitives/scrim.d.ts.map +1 -0
  61. package/package.json +5 -1
  62. package/src/components/attachment/index.tsx +279 -0
  63. package/src/components/bottom-sheet/index.tsx +20 -0
  64. package/src/components/dialog/index.tsx +10 -1
  65. package/src/components/empty-state/index.tsx +60 -14
  66. package/src/components/line-chart/index.tsx +1224 -0
  67. package/src/components/marker/index.tsx +194 -0
  68. package/src/components/message/index.tsx +29 -5
  69. package/src/components/message-scroller/index.tsx +574 -0
  70. package/src/components/popover/index.tsx +599 -0
  71. package/src/components/radio-group/index.tsx +108 -17
  72. package/src/components/separator/index.tsx +97 -0
  73. package/src/components/surface/index.tsx +29 -3
  74. package/src/icons/index.tsx +48 -0
  75. package/src/index.ts +60 -0
  76. package/src/primitives/scrim.tsx +95 -0
  77. package/theme.css +58 -0
@@ -1,3 +1,23 @@
1
+ /**
2
+ * RadioGroup — one choice from a set.
3
+ *
4
+ * The group owns the value and hands each item its selected state through
5
+ * context, so an item is a thin pressable that reports its own value up and
6
+ * reads selection back down.
7
+ *
8
+ * Two shapes, chosen on the group and inherited by every item: `dot` is the
9
+ * classic label-beside-a-disc row; `card` turns the whole surface into the
10
+ * target — for pickable options where the disc is a confirmation rather than
11
+ * the thing you aim at. The card is the same treatment `Checkbox` offers, so a
12
+ * form mixing single- and multi-select choices reads as one family.
13
+ *
14
+ * ```tsx
15
+ * <RadioGroup value={plan} onValueChange={setPlan} variant="card">
16
+ * <RadioGroup.Item value="pro" label="Pro" description="For growing teams" />
17
+ * <RadioGroup.Item value="max" label="Max" description="Everything, uncapped" />
18
+ * </RadioGroup>
19
+ * ```
20
+ */
1
21
  import {
2
22
  createContext,
3
23
  forwardRef,
@@ -13,13 +33,50 @@ import Animated, {
13
33
  withSpring,
14
34
  withTiming,
15
35
  } from 'react-native-reanimated';
36
+ import { tv, type VariantProps } from 'tailwind-variants';
16
37
  import { cn } from '../../utils/cn';
17
38
  import { Text } from '../../primitives/text';
18
39
 
40
+ type RadioVariant = 'dot' | 'card';
41
+
42
+ const itemVariants = tv({
43
+ slots: {
44
+ row: 'flex-row items-center gap-2.5',
45
+ indicator:
46
+ 'h-5 w-5 items-center justify-center rounded-full border border-input bg-background',
47
+ label: 'text-sm text-foreground',
48
+ description: 'text-xs text-muted-foreground',
49
+ },
50
+ variants: {
51
+ variant: {
52
+ dot: {},
53
+ card: {
54
+ // The whole surface is the target; the disc moves to the trailing edge
55
+ // as a confirmation of the row, not the affordance for it.
56
+ row: 'w-full items-start justify-between gap-3 rounded-xl border border-border bg-card p-4',
57
+ label: 'text-base font-medium',
58
+ },
59
+ },
60
+ selected: {
61
+ true: {},
62
+ },
63
+ disabled: {
64
+ true: { row: 'opacity-50' },
65
+ },
66
+ },
67
+ compoundVariants: [
68
+ { variant: 'card', selected: true, class: { row: 'border-primary bg-accent' } },
69
+ ],
70
+ defaultVariants: {
71
+ variant: 'dot',
72
+ },
73
+ });
74
+
19
75
  interface RadioGroupContextValue {
20
76
  value: string | undefined;
21
77
  onValueChange: (value: string) => void;
22
78
  disabled?: boolean;
79
+ variant: RadioVariant;
23
80
  }
24
81
 
25
82
  const RadioGroupContext = createContext<RadioGroupContextValue | null>(null);
@@ -29,14 +86,20 @@ export interface RadioGroupProps extends ViewProps {
29
86
  value?: string;
30
87
  onValueChange: (value: string) => void;
31
88
  disabled?: boolean;
89
+ /**
90
+ * `dot` is the label-beside-a-disc row. `card` makes the whole surface the
91
+ * target and highlights the selected option — for a plan picker or a
92
+ * settings choice where each option carries a description.
93
+ */
94
+ variant?: RadioVariant;
32
95
  children: ReactNode;
33
96
  }
34
97
 
35
98
  const RadioGroupRoot = forwardRef<View, RadioGroupProps>(
36
- ({ className, value, onValueChange, disabled, children, ...props }, ref) => {
99
+ ({ className, value, onValueChange, disabled, variant = 'dot', children, ...props }, ref) => {
37
100
  const context = useMemo(
38
- () => ({ value, onValueChange, disabled }),
39
- [value, onValueChange, disabled]
101
+ () => ({ value, onValueChange, disabled, variant }),
102
+ [value, onValueChange, disabled, variant]
40
103
  );
41
104
 
42
105
  return (
@@ -59,12 +122,19 @@ export interface RadioGroupItemProps {
59
122
  className?: string;
60
123
  value: string;
61
124
  label?: string;
125
+ /** Secondary line under the label. Most at home in the `card` variant. */
126
+ description?: string;
62
127
  disabled?: boolean;
128
+ /** Hide the disc entirely — for a card whose selected fill is enough. */
129
+ hideIndicator?: boolean;
63
130
  children?: ReactNode;
64
131
  }
65
132
 
66
133
  const RadioGroupItem = forwardRef<View, RadioGroupItemProps>(
67
- ({ className, value, label, disabled: itemDisabled, children }, ref) => {
134
+ (
135
+ { className, value, label, description, disabled: itemDisabled, hideIndicator, children },
136
+ ref
137
+ ) => {
68
138
  const context = useContext(RadioGroupContext);
69
139
  if (!context) {
70
140
  throw new Error('RadioGroup.Item must be used within a <RadioGroup>');
@@ -72,6 +142,7 @@ const RadioGroupItem = forwardRef<View, RadioGroupItemProps>(
72
142
 
73
143
  const selected = context.value === value;
74
144
  const disabled = itemDisabled || context.disabled;
145
+ const variant = context.variant;
75
146
  const progress = useSharedValue(selected ? 1 : 0);
76
147
 
77
148
  useEffect(() => {
@@ -85,6 +156,34 @@ const RadioGroupItem = forwardRef<View, RadioGroupItemProps>(
85
156
  transform: [{ scale: progress.value }],
86
157
  }));
87
158
 
159
+ const slots = itemVariants({ variant, selected, disabled: !!disabled });
160
+
161
+ const indicator = hideIndicator ? null : (
162
+ <View className={slots.indicator()}>
163
+ <Animated.View style={dotStyle} className="h-2.5 w-2.5 rounded-full bg-primary" />
164
+ </View>
165
+ );
166
+
167
+ // The card lays its text out first so the disc sits at the trailing edge;
168
+ // the dot row keeps the classic disc-then-label order.
169
+ const body =
170
+ variant === 'card' ? (
171
+ <>
172
+ <View className="flex-1 gap-1">
173
+ {label ? <Text className={slots.label()}>{label}</Text> : children}
174
+ {description ? (
175
+ <Text className={slots.description()}>{description}</Text>
176
+ ) : null}
177
+ </View>
178
+ {indicator}
179
+ </>
180
+ ) : (
181
+ <>
182
+ {indicator}
183
+ {label ? <Text className={slots.label()}>{label}</Text> : children}
184
+ </>
185
+ );
186
+
88
187
  return (
89
188
  <Pressable
90
189
  ref={ref}
@@ -93,20 +192,10 @@ const RadioGroupItem = forwardRef<View, RadioGroupItemProps>(
93
192
  accessibilityLabel={label}
94
193
  disabled={disabled}
95
194
  onPress={() => context.onValueChange(value)}
96
- hitSlop={8}
97
- className={cn(
98
- 'flex-row items-center gap-2.5',
99
- disabled && 'opacity-50',
100
- className
101
- )}
195
+ hitSlop={variant === 'card' ? undefined : 8}
196
+ className={slots.row({ className })}
102
197
  >
103
- <View className="h-5 w-5 items-center justify-center rounded-full border border-input bg-background">
104
- <Animated.View
105
- style={dotStyle}
106
- className="h-2.5 w-2.5 rounded-full bg-primary"
107
- />
108
- </View>
109
- {label ? <Text className="text-sm text-foreground">{label}</Text> : children}
198
+ {body}
110
199
  </Pressable>
111
200
  );
112
201
  }
@@ -116,3 +205,5 @@ RadioGroupItem.displayName = 'RadioGroup.Item';
116
205
  export const RadioGroup = Object.assign(RadioGroupRoot, {
117
206
  Item: RadioGroupItem,
118
207
  });
208
+
209
+ export type { RadioVariant };
@@ -0,0 +1,97 @@
1
+ /**
2
+ * Separator — a rule between content.
3
+ *
4
+ * React Native has no `<hr>` and no border shorthand that survives a flex row,
5
+ * so a separator here is a view with a size on one axis and a stretch on the
6
+ * other. That is why orientation is a prop rather than something the layout
7
+ * infers: the component has to know which axis carries the thickness.
8
+ *
9
+ * ```tsx
10
+ * <Separator />
11
+ * <Separator orientation="vertical" />
12
+ * ```
13
+ *
14
+ * A vertical separator takes its length from the parent, so the parent needs a
15
+ * height — inside a `flex-row` with `items-stretch`, or with an explicit `h-*`
16
+ * on either the row or the separator itself. Without one it measures zero and
17
+ * nothing draws.
18
+ */
19
+ import { forwardRef } from 'react';
20
+ import { View, type ViewProps } from 'react-native';
21
+ import { tv, type VariantProps } from 'tailwind-variants';
22
+
23
+ const separatorVariants = tv({
24
+ base: 'bg-border',
25
+ variants: {
26
+ orientation: {
27
+ horizontal: 'w-full',
28
+ vertical: 'h-full',
29
+ },
30
+ variant: {
31
+ thin: '',
32
+ thick: '',
33
+ },
34
+ },
35
+ compoundVariants: [
36
+ // Thickness is on whichever axis is *not* the length, so it cannot be
37
+ // expressed by the orientation and variant classes independently.
38
+ { orientation: 'horizontal', variant: 'thin', class: 'h-px' },
39
+ { orientation: 'horizontal', variant: 'thick', class: 'h-1' },
40
+ { orientation: 'vertical', variant: 'thin', class: 'w-px' },
41
+ { orientation: 'vertical', variant: 'thick', class: 'w-1' },
42
+ ],
43
+ defaultVariants: {
44
+ orientation: 'horizontal',
45
+ variant: 'thin',
46
+ },
47
+ });
48
+
49
+ export interface SeparatorProps
50
+ extends ViewProps,
51
+ VariantProps<typeof separatorVariants> {
52
+ className?: string;
53
+ /**
54
+ * Thickness in pixels, overriding the variant. Sets the height of a
55
+ * horizontal separator and the width of a vertical one.
56
+ */
57
+ thickness?: number;
58
+ /**
59
+ * Whether the separator is only visual. A decorative separator is skipped by
60
+ * screen readers; set false when the split itself carries meaning — between
61
+ * two groups of menu items, say — and it is announced instead.
62
+ */
63
+ decorative?: boolean;
64
+ }
65
+
66
+ export const Separator = forwardRef<View, SeparatorProps>(
67
+ (
68
+ {
69
+ className,
70
+ orientation = 'horizontal',
71
+ variant,
72
+ thickness,
73
+ decorative = true,
74
+ style,
75
+ ...props
76
+ },
77
+ ref
78
+ ) => (
79
+ <View
80
+ ref={ref}
81
+ // `role` rather than `accessibilityRole` — React Native's older role list
82
+ // has no separator, the ARIA-aligned one does.
83
+ role={decorative ? 'none' : 'separator'}
84
+ accessible={!decorative}
85
+ aria-orientation={decorative ? undefined : orientation}
86
+ className={separatorVariants({ orientation, variant, className })}
87
+ style={
88
+ thickness !== undefined
89
+ ? [orientation === 'horizontal' ? { height: thickness } : { width: thickness }, style]
90
+ : style
91
+ }
92
+ {...props}
93
+ />
94
+ )
95
+ );
96
+
97
+ Separator.displayName = 'Separator';
@@ -23,7 +23,7 @@ import { StyleSheet, View, type ViewProps } from 'react-native';
23
23
  import { tv, type VariantProps } from 'tailwind-variants';
24
24
 
25
25
  const surfaceVariants = tv({
26
- base: 'overflow-hidden rounded-3xl p-4',
26
+ base: 'overflow-hidden rounded-3xl',
27
27
  variants: {
28
28
  variant: {
29
29
  default: 'bg-surface',
@@ -31,9 +31,22 @@ const surfaceVariants = tv({
31
31
  tertiary: 'bg-surface-tertiary',
32
32
  transparent: 'bg-transparent',
33
33
  },
34
+ padding: {
35
+ none: '',
36
+ sm: 'p-2.5',
37
+ default: 'p-4',
38
+ lg: 'p-6',
39
+ },
40
+ bordered: {
41
+ true: 'border border-border',
42
+ },
43
+ elevated: {
44
+ true: 'shadow-sm',
45
+ },
34
46
  },
35
47
  defaultVariants: {
36
48
  variant: 'default',
49
+ padding: 'default',
37
50
  },
38
51
  });
39
52
 
@@ -41,17 +54,30 @@ export interface SurfaceProps
41
54
  extends ViewProps,
42
55
  VariantProps<typeof surfaceVariants> {
43
56
  className?: string;
57
+ /**
58
+ * A hairline border. A surface the same colour as what it sits on needs one
59
+ * to read as a distinct plane rather than dissolving into the background.
60
+ */
61
+ bordered?: boolean;
62
+ /**
63
+ * A soft shadow lifting the surface off the page. Off by default because a
64
+ * nested surface reads its depth from its fill, not from a shadow it would
65
+ * only cast onto its parent.
66
+ */
67
+ elevated?: boolean;
68
+ /** Inner spacing. `none` is for a surface wrapping a bled image or a chart. */
69
+ padding?: 'none' | 'sm' | 'default' | 'lg';
44
70
  }
45
71
 
46
72
  export const Surface = forwardRef<View, SurfaceProps>(
47
- ({ className, variant, style, ...props }, ref) => (
73
+ ({ className, variant, padding, bordered, elevated, style, ...props }, ref) => (
48
74
  <View
49
75
  ref={ref}
50
76
  // `borderCurve` has no Tailwind equivalent. On iOS it gives the
51
77
  // continuous (squircle) corner Apple uses, which is visibly smoother
52
78
  // than a circular arc at this radius; Android ignores it.
53
79
  style={[styles.root, style]}
54
- className={surfaceVariants({ variant, className })}
80
+ className={surfaceVariants({ variant, padding, bordered, elevated, className })}
55
81
  {...props}
56
82
  />
57
83
  )
@@ -462,3 +462,51 @@ export function SendIcon({ size = 16, color, ...props }: IconProps) {
462
462
  </Svg>
463
463
  );
464
464
  }
465
+
466
+ /** Document with a folded corner — a file attachment. */
467
+ export function FileIcon({ size = 16, color, ...props }: IconProps) {
468
+ const resolved = useResolvedColor(color, '#737373');
469
+ return (
470
+ <Svg width={size} height={size} viewBox="0 0 24 24" fill="none" {...props}>
471
+ <Path
472
+ d="M14 3H7a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V8zM14 3v5h5"
473
+ stroke={resolved}
474
+ strokeWidth={2}
475
+ strokeLinecap="round"
476
+ strokeLinejoin="round"
477
+ />
478
+ </Svg>
479
+ );
480
+ }
481
+
482
+ /** Framed picture with a sun and a corner peak — an image attachment. */
483
+ export function ImageIcon({ size = 16, color, ...props }: IconProps) {
484
+ const resolved = useResolvedColor(color, '#737373');
485
+ return (
486
+ <Svg width={size} height={size} viewBox="0 0 24 24" fill="none" {...props}>
487
+ <Path
488
+ d="M4 4h16v16H4z M8.5 10a1.5 1.5 0 1 0 0-3 1.5 1.5 0 0 0 0 3zM20 15l-4.5-4.5L6 20"
489
+ stroke={resolved}
490
+ strokeWidth={2}
491
+ strokeLinecap="round"
492
+ strokeLinejoin="round"
493
+ />
494
+ </Svg>
495
+ );
496
+ }
497
+
498
+ /** A paperclip — attaching a file to a composer. */
499
+ export function PaperclipIcon({ size = 16, color, ...props }: IconProps) {
500
+ const resolved = useResolvedColor(color, '#737373');
501
+ return (
502
+ <Svg width={size} height={size} viewBox="0 0 24 24" fill="none" {...props}>
503
+ <Path
504
+ d="M21 8.5 11.3 18.2a4 4 0 0 1-5.6-5.6l9-9a2.5 2.5 0 0 1 3.6 3.6l-9 9a1 1 0 0 1-1.5-1.5l8.3-8.3"
505
+ stroke={resolved}
506
+ strokeWidth={2}
507
+ strokeLinecap="round"
508
+ strokeLinejoin="round"
509
+ />
510
+ </Svg>
511
+ );
512
+ }
package/src/index.ts CHANGED
@@ -50,6 +50,18 @@ export {
50
50
  type AvatarProps,
51
51
  type AvatarBadgeProps,
52
52
  } from './components/avatar';
53
+ export {
54
+ Attachment,
55
+ type AttachmentProps,
56
+ type AttachmentGroupProps,
57
+ type AttachmentMediaProps,
58
+ type AttachmentContentProps,
59
+ type AttachmentTitleProps,
60
+ type AttachmentDescriptionProps,
61
+ type AttachmentActionsProps,
62
+ type AttachmentActionProps,
63
+ type AttachmentState,
64
+ } from './components/attachment';
53
65
  export { Badge, type BadgeProps } from './components/badge';
54
66
  export {
55
67
  BottomSheet,
@@ -104,6 +116,39 @@ export {
104
116
  type ItemFooterProps,
105
117
  } from './components/item';
106
118
  export { Label, type LabelProps, type LabelTextProps } from './components/label';
119
+ export {
120
+ LineChart,
121
+ useLineChart,
122
+ type LineChartProps,
123
+ type LineChartHandle,
124
+ type LineChartGridProps,
125
+ type LineChartLineProps,
126
+ type LineChartAreaProps,
127
+ type LineChartSkeletonProps,
128
+ type LineChartXAxisProps,
129
+ type LineChartTooltipProps,
130
+ type LineChartLegendProps,
131
+ type LineChartDatum,
132
+ type LineChartStatus,
133
+ type LineChartCurve,
134
+ } from './components/line-chart';
135
+ export {
136
+ Marker,
137
+ type MarkerProps,
138
+ type MarkerIconProps,
139
+ type MarkerContentProps,
140
+ } from './components/marker';
141
+ export {
142
+ MessageScroller,
143
+ useMessageScroller,
144
+ useMessageScrollerVisibility,
145
+ type MessageScrollerProps,
146
+ type MessageScrollerViewportProps,
147
+ type MessageScrollerContentProps,
148
+ type MessageScrollerItemProps,
149
+ type MessageScrollerButtonProps,
150
+ type MessageScrollerPosition,
151
+ } from './components/message-scroller';
107
152
  export {
108
153
  Message,
109
154
  type MessageProps,
@@ -116,6 +161,17 @@ export {
116
161
  type MessageFooterProps,
117
162
  type MessageActionsProps,
118
163
  } from './components/message';
164
+ export {
165
+ Popover,
166
+ type PopoverProps,
167
+ type PopoverTriggerProps,
168
+ type PopoverContentProps,
169
+ type PopoverArrowProps,
170
+ type PopoverCloseProps,
171
+ type PopoverPlacement,
172
+ type PopoverAlign,
173
+ type PopoverPresentation,
174
+ } from './components/popover';
119
175
  export { Progress, type ProgressProps } from './components/progress';
120
176
  export {
121
177
  RadioGroup,
@@ -126,6 +182,7 @@ export {
126
182
  ScrollFade,
127
183
  type ScrollFadeProps,
128
184
  } from './components/scroll-fade';
185
+ export { Separator, type SeparatorProps } from './components/separator';
129
186
  export { Shimmer, type ShimmerProps } from './components/shimmer';
130
187
  export { Skeleton, type SkeletonProps } from './components/skeleton';
131
188
  export { Spinner, type SpinnerProps } from './components/spinner';
@@ -186,11 +243,14 @@ export {
186
243
  ChevronLeftIcon,
187
244
  ChevronRightIcon,
188
245
  FacebookIcon,
246
+ FileIcon,
189
247
  GoogleIcon,
190
248
  IconColorProvider,
191
249
  InfoIcon,
250
+ ImageIcon,
192
251
  MoonIcon,
193
252
  PackageIcon,
253
+ PaperclipIcon,
194
254
  PlusSquareIcon,
195
255
  ReceiptIcon,
196
256
  SearchIcon,
@@ -0,0 +1,95 @@
1
+ /**
2
+ * Scrim — the layer behind an overlay.
3
+ *
4
+ * An overlay needs the screen behind it to recede. There are two honest ways to
5
+ * do that: dim it, or blur it. Dimming is free and always works; blurring reads
6
+ * as more physical — the content behind stays legible as shape and colour while
7
+ * losing its detail — but it needs a native view to do it.
8
+ *
9
+ * `expo-blur` is resolved lazily and only when `blur` is asked for, so a project
10
+ * that never blurs anything installs nothing. When it is asked for and the
11
+ * package is missing, this falls back to a dim rather than failing: a blur you
12
+ * cannot draw is better shown as a darkened screen than as a crash.
13
+ *
14
+ * It fills its parent and does not intercept touches itself — the overlay layers
15
+ * its own dismiss `Pressable` over it — so it is purely the visual backdrop.
16
+ */
17
+ import { type ComponentType } from 'react';
18
+ import { StyleSheet, View, type ViewProps } from 'react-native';
19
+ import Animated, { FadeIn, FadeOut } from 'react-native-reanimated';
20
+
21
+ interface BlurViewProps {
22
+ intensity?: number;
23
+ tint?: 'light' | 'dark' | 'default' | 'systemMaterial';
24
+ style?: unknown;
25
+ children?: React.ReactNode;
26
+ }
27
+
28
+ /**
29
+ * `expo-blur`'s BlurView, or null when it is not installed. Resolved once at
30
+ * module load — the require is cheap and caching it avoids a try/catch on every
31
+ * render.
32
+ */
33
+ const BlurView: ComponentType<BlurViewProps> | null = (() => {
34
+ try {
35
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
36
+ const mod = require('expo-blur');
37
+ return (mod?.BlurView as ComponentType<BlurViewProps>) ?? null;
38
+ } catch {
39
+ return null;
40
+ }
41
+ })();
42
+
43
+ /** True when a real blur can be drawn — for a caller that wants to know. */
44
+ export const hasBlur = BlurView !== null;
45
+
46
+ export interface ScrimProps extends Omit<ViewProps, 'children'> {
47
+ /** Frost the backdrop instead of dimming it. Falls back to a dim if it can't. */
48
+ blur?: boolean;
49
+ /** Blur strength, 0–100. */
50
+ intensity?: number;
51
+ /** Which way the blur tints. */
52
+ tint?: 'light' | 'dark' | 'default' | 'systemMaterial';
53
+ /**
54
+ * The dim used when not blurring — and when blurring is asked for but
55
+ * unavailable. A popover passes a lighter one than a dialog.
56
+ */
57
+ dimClassName?: string;
58
+ }
59
+
60
+ export function Scrim({
61
+ blur = false,
62
+ intensity = 24,
63
+ tint = 'default',
64
+ dimClassName = 'bg-black/50',
65
+ style,
66
+ ...props
67
+ }: ScrimProps) {
68
+ if (blur && BlurView) {
69
+ return (
70
+ <Animated.View
71
+ entering={FadeIn.duration(180)}
72
+ exiting={FadeOut.duration(150)}
73
+ style={[StyleSheet.absoluteFill, style]}
74
+ {...props}
75
+ >
76
+ {/* A faint dim under the blur so the frost has something to sit on —
77
+ a pure blur over a dark scene is nearly invisible. */}
78
+ <BlurView intensity={intensity} tint={tint} style={StyleSheet.absoluteFill} />
79
+ <View className="absolute inset-0 bg-black/10" />
80
+ </Animated.View>
81
+ );
82
+ }
83
+
84
+ return (
85
+ <Animated.View
86
+ entering={FadeIn.duration(150)}
87
+ exiting={FadeOut.duration(150)}
88
+ style={[StyleSheet.absoluteFill, style]}
89
+ className={dimClassName}
90
+ {...props}
91
+ />
92
+ );
93
+ }
94
+
95
+ Scrim.displayName = 'Scrim';
package/theme.css CHANGED
@@ -67,6 +67,22 @@
67
67
  --color-destructive-soft: unset;
68
68
  --color-destructive-subtle: unset;
69
69
 
70
+ /*
71
+ * Chart series. A separate ramp from the status colours because they answer
72
+ * a different question: a status colour means something (this failed, this
73
+ * is fine), a series colour only has to be told apart from the other four.
74
+ * Ordered by prominence — chart-1 is the series the chart is about, and each
75
+ * theme starts it on that family's own accent.
76
+ *
77
+ * These are the tokens to override in your app's global.css, after the
78
+ * `panelui-native/theme.css` import, to put a chart on brand.
79
+ */
80
+ --color-chart-1: unset;
81
+ --color-chart-2: unset;
82
+ --color-chart-3: unset;
83
+ --color-chart-4: unset;
84
+ --color-chart-5: unset;
85
+
70
86
  /*
71
87
  * Radius scale. Themed rather than static: a theme is a shape as well as a
72
88
  * palette, so each family sets its own corner rounding. --radius-3xl is
@@ -143,6 +159,13 @@
143
159
  --color-warning-subtle: rgba(245, 158, 11, 0.08);
144
160
  --color-destructive-soft: rgba(239, 68, 68, 0.1);
145
161
  --color-destructive-subtle: rgba(239, 68, 68, 0.08);
162
+
163
+ /* chart series, in order of prominence */
164
+ --color-chart-1: #262626;
165
+ --color-chart-2: #3b82f6;
166
+ --color-chart-3: #10b981;
167
+ --color-chart-4: #f59e0b;
168
+ --color-chart-5: #8b5cf6;
146
169
 
147
170
  /* Panel shape scale */
148
171
  --radius-xs: 4px;
@@ -216,6 +239,13 @@
216
239
  --color-warning-subtle: rgba(245, 158, 11, 0.16);
217
240
  --color-destructive-soft: rgba(241, 87, 87, 0.06);
218
241
  --color-destructive-subtle: rgba(241, 87, 87, 0.16);
242
+
243
+ /* chart series, in order of prominence */
244
+ --color-chart-1: #fafafa;
245
+ --color-chart-2: #60a5fa;
246
+ --color-chart-3: #34d399;
247
+ --color-chart-4: #fbbf24;
248
+ --color-chart-5: #a78bfa;
219
249
 
220
250
  /* Panel shape scale */
221
251
  --radius-xs: 4px;
@@ -298,6 +328,13 @@
298
328
  --color-warning-subtle: rgba(245, 166, 35, 0.08);
299
329
  --color-destructive-soft: rgba(229, 72, 77, 0.1);
300
330
  --color-destructive-subtle: rgba(229, 72, 77, 0.08);
331
+
332
+ /* chart series, in order of prominence */
333
+ --color-chart-1: #0070f3;
334
+ --color-chart-2: #171717;
335
+ --color-chart-3: #7928ca;
336
+ --color-chart-4: #f5a623;
337
+ --color-chart-5: #0a9396;
301
338
 
302
339
  /* Moon shape scale */
303
340
  --radius-xs: 2px;
@@ -360,6 +397,13 @@
360
397
  --color-warning-subtle: rgba(245, 166, 35, 0.16);
361
398
  --color-destructive-soft: rgba(229, 72, 77, 0.1);
362
399
  --color-destructive-subtle: rgba(229, 72, 77, 0.16);
400
+
401
+ /* chart series, in order of prominence */
402
+ --color-chart-1: #3291ff;
403
+ --color-chart-2: #ededed;
404
+ --color-chart-3: #a855f7;
405
+ --color-chart-4: #fbbf24;
406
+ --color-chart-5: #2dd4bf;
363
407
 
364
408
  /* Moon shape scale */
365
409
  --radius-xs: 2px;
@@ -425,6 +469,13 @@
425
469
  --color-warning-subtle: rgba(245, 158, 11, 0.08);
426
470
  --color-destructive-soft: rgba(239, 68, 68, 0.1);
427
471
  --color-destructive-subtle: rgba(239, 68, 68, 0.08);
472
+
473
+ /* chart series, in order of prominence */
474
+ --color-chart-1: #24b47e;
475
+ --color-chart-2: #1f2937;
476
+ --color-chart-3: #2563eb;
477
+ --color-chart-4: #f59e0b;
478
+ --color-chart-5: #db2777;
428
479
 
429
480
  /* Grass shape scale */
430
481
  --radius-xs: 6px;
@@ -487,6 +538,13 @@
487
538
  --color-warning-subtle: rgba(245, 158, 11, 0.16);
488
539
  --color-destructive-soft: rgba(241, 87, 87, 0.08);
489
540
  --color-destructive-subtle: rgba(241, 87, 87, 0.16);
541
+
542
+ /* chart series, in order of prominence */
543
+ --color-chart-1: #3ecf8e;
544
+ --color-chart-2: #ededed;
545
+ --color-chart-3: #60a5fa;
546
+ --color-chart-4: #fbbf24;
547
+ --color-chart-5: #f472b6;
490
548
 
491
549
  /* Grass shape scale */
492
550
  --radius-xs: 6px;