panelui-native 0.7.0 → 0.12.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.
- package/README.md +5 -3
- package/lib/module/components/attachment/index.js +219 -0
- package/lib/module/components/attachment/index.js.map +1 -0
- package/lib/module/components/bottom-sheet/index.js +53 -3
- package/lib/module/components/bottom-sheet/index.js.map +1 -1
- package/lib/module/components/button/index.js +19 -1
- package/lib/module/components/button/index.js.map +1 -1
- package/lib/module/components/dialog/index.js +6 -2
- package/lib/module/components/dialog/index.js.map +1 -1
- package/lib/module/components/empty-state/index.js +94 -17
- package/lib/module/components/empty-state/index.js.map +1 -1
- package/lib/module/components/frame/index.js +128 -15
- package/lib/module/components/frame/index.js.map +1 -1
- package/lib/module/components/input/index.js +107 -15
- package/lib/module/components/input/index.js.map +1 -1
- package/lib/module/components/line-chart/index.js +1024 -0
- package/lib/module/components/line-chart/index.js.map +1 -0
- package/lib/module/components/marker/index.js +190 -0
- package/lib/module/components/marker/index.js.map +1 -0
- package/lib/module/components/message/index.js +24 -8
- package/lib/module/components/message/index.js.map +1 -1
- package/lib/module/components/message-scroller/index.js +451 -0
- package/lib/module/components/message-scroller/index.js.map +1 -0
- package/lib/module/components/popover/index.js +511 -0
- package/lib/module/components/popover/index.js.map +1 -0
- package/lib/module/components/progress/index.js +52 -2
- package/lib/module/components/progress/index.js.map +1 -1
- package/lib/module/components/radio-group/index.js +132 -17
- package/lib/module/components/radio-group/index.js.map +1 -1
- package/lib/module/components/section-rail/index.js +355 -0
- package/lib/module/components/section-rail/index.js.map +1 -0
- package/lib/module/components/select/index.js +245 -56
- package/lib/module/components/select/index.js.map +1 -1
- package/lib/module/components/separator/index.js +91 -0
- package/lib/module/components/separator/index.js.map +1 -0
- package/lib/module/components/slider/index.js +355 -0
- package/lib/module/components/slider/index.js.map +1 -0
- package/lib/module/components/surface/index.js +21 -2
- package/lib/module/components/surface/index.js.map +1 -1
- package/lib/module/components/switch/index.js +10 -1
- package/lib/module/components/switch/index.js.map +1 -1
- package/lib/module/components/tabs/index.js +186 -16
- package/lib/module/components/tabs/index.js.map +1 -1
- package/lib/module/components/toggle-button/index.js +297 -0
- package/lib/module/components/toggle-button/index.js.map +1 -0
- package/lib/module/hooks/index.js +1 -0
- package/lib/module/hooks/index.js.map +1 -1
- package/lib/module/hooks/use-keyboard-avoidance.js +57 -8
- package/lib/module/hooks/use-keyboard-avoidance.js.map +1 -1
- package/lib/module/hooks/use-scroll-sections.js +89 -0
- package/lib/module/hooks/use-scroll-sections.js.map +1 -0
- package/lib/module/icons/index.js +69 -0
- package/lib/module/icons/index.js.map +1 -1
- package/lib/module/index.js +11 -2
- package/lib/module/index.js.map +1 -1
- package/lib/module/native/index.js.map +1 -1
- package/lib/module/primitives/keyboard-avoider.js +2 -0
- package/lib/module/primitives/keyboard-avoider.js.map +1 -1
- package/lib/module/primitives/scrim.js +72 -0
- package/lib/module/primitives/scrim.js.map +1 -0
- package/lib/module/utils/haptics.js +57 -0
- package/lib/module/utils/haptics.js.map +1 -0
- package/lib/typescript/src/components/attachment/index.d.ts +93 -0
- package/lib/typescript/src/components/attachment/index.d.ts.map +1 -0
- package/lib/typescript/src/components/bottom-sheet/index.d.ts +6 -1
- package/lib/typescript/src/components/bottom-sheet/index.d.ts.map +1 -1
- package/lib/typescript/src/components/button/index.d.ts +9 -4
- package/lib/typescript/src/components/button/index.d.ts.map +1 -1
- package/lib/typescript/src/components/dialog/index.d.ts +7 -1
- package/lib/typescript/src/components/dialog/index.d.ts.map +1 -1
- package/lib/typescript/src/components/empty-state/index.d.ts +152 -2
- package/lib/typescript/src/components/empty-state/index.d.ts.map +1 -1
- package/lib/typescript/src/components/frame/index.d.ts +47 -7
- package/lib/typescript/src/components/frame/index.d.ts.map +1 -1
- package/lib/typescript/src/components/input/index.d.ts +113 -2
- package/lib/typescript/src/components/input/index.d.ts.map +1 -1
- package/lib/typescript/src/components/line-chart/index.d.ts +247 -0
- package/lib/typescript/src/components/line-chart/index.d.ts.map +1 -0
- package/lib/typescript/src/components/marker/index.d.ts +137 -0
- package/lib/typescript/src/components/marker/index.d.ts.map +1 -0
- package/lib/typescript/src/components/message/index.d.ts +8 -1
- package/lib/typescript/src/components/message/index.d.ts.map +1 -1
- package/lib/typescript/src/components/message-scroller/index.d.ts +150 -0
- package/lib/typescript/src/components/message-scroller/index.d.ts.map +1 -0
- package/lib/typescript/src/components/popover/index.d.ts +136 -0
- package/lib/typescript/src/components/popover/index.d.ts.map +1 -0
- package/lib/typescript/src/components/progress/index.d.ts +33 -0
- package/lib/typescript/src/components/progress/index.d.ts.map +1 -1
- package/lib/typescript/src/components/radio-group/index.d.ts +39 -0
- package/lib/typescript/src/components/radio-group/index.d.ts.map +1 -1
- package/lib/typescript/src/components/section-rail/index.d.ts +115 -0
- package/lib/typescript/src/components/section-rail/index.d.ts.map +1 -0
- package/lib/typescript/src/components/select/index.d.ts +46 -7
- package/lib/typescript/src/components/select/index.d.ts.map +1 -1
- package/lib/typescript/src/components/separator/index.d.ts +47 -0
- package/lib/typescript/src/components/separator/index.d.ts.map +1 -0
- package/lib/typescript/src/components/slider/index.d.ts +203 -0
- package/lib/typescript/src/components/slider/index.d.ts.map +1 -0
- package/lib/typescript/src/components/surface/index.d.ts +51 -2
- package/lib/typescript/src/components/surface/index.d.ts.map +1 -1
- package/lib/typescript/src/components/switch/index.d.ts.map +1 -1
- package/lib/typescript/src/components/tabs/index.d.ts +46 -4
- package/lib/typescript/src/components/tabs/index.d.ts.map +1 -1
- package/lib/typescript/src/components/toggle-button/index.d.ts +244 -0
- package/lib/typescript/src/components/toggle-button/index.d.ts.map +1 -0
- package/lib/typescript/src/hooks/index.d.ts +1 -0
- package/lib/typescript/src/hooks/index.d.ts.map +1 -1
- package/lib/typescript/src/hooks/use-keyboard-avoidance.d.ts +10 -1
- package/lib/typescript/src/hooks/use-keyboard-avoidance.d.ts.map +1 -1
- package/lib/typescript/src/hooks/use-scroll-sections.d.ts +31 -0
- package/lib/typescript/src/hooks/use-scroll-sections.d.ts.map +1 -0
- package/lib/typescript/src/icons/index.d.ts +6 -0
- package/lib/typescript/src/icons/index.d.ts.map +1 -1
- package/lib/typescript/src/index.d.ts +13 -4
- package/lib/typescript/src/index.d.ts.map +1 -1
- package/lib/typescript/src/native/index.d.ts +16 -1
- package/lib/typescript/src/native/index.d.ts.map +1 -1
- package/lib/typescript/src/primitives/keyboard-avoider.d.ts +8 -1
- package/lib/typescript/src/primitives/keyboard-avoider.d.ts.map +1 -1
- package/lib/typescript/src/primitives/scrim.d.ts +21 -0
- package/lib/typescript/src/primitives/scrim.d.ts.map +1 -0
- package/lib/typescript/src/utils/haptics.d.ts +12 -0
- package/lib/typescript/src/utils/haptics.d.ts.map +1 -0
- package/package.json +9 -1
- package/src/components/attachment/index.tsx +279 -0
- package/src/components/bottom-sheet/index.tsx +58 -2
- package/src/components/button/index.tsx +26 -5
- package/src/components/dialog/index.tsx +10 -1
- package/src/components/empty-state/index.tsx +60 -14
- package/src/components/frame/index.tsx +175 -29
- package/src/components/input/index.tsx +122 -16
- package/src/components/line-chart/index.tsx +1224 -0
- package/src/components/marker/index.tsx +194 -0
- package/src/components/message/index.tsx +29 -5
- package/src/components/message-scroller/index.tsx +574 -0
- package/src/components/popover/index.tsx +624 -0
- package/src/components/progress/index.tsx +81 -1
- package/src/components/radio-group/index.tsx +156 -18
- package/src/components/section-rail/index.tsx +459 -0
- package/src/components/select/index.tsx +265 -52
- package/src/components/separator/index.tsx +97 -0
- package/src/components/slider/index.tsx +371 -0
- package/src/components/surface/index.tsx +29 -3
- package/src/components/switch/index.tsx +8 -1
- package/src/components/tabs/index.tsx +208 -28
- package/src/components/toggle-button/index.tsx +374 -0
- package/src/hooks/index.ts +5 -0
- package/src/hooks/use-keyboard-avoidance.ts +73 -7
- package/src/hooks/use-scroll-sections.ts +127 -0
- package/src/icons/index.tsx +48 -0
- package/src/index.ts +97 -6
- package/src/native/index.ts +13 -1
- package/src/primitives/keyboard-avoider.tsx +9 -1
- package/src/primitives/scrim.tsx +95 -0
- package/src/utils/haptics.ts +63 -0
- package/theme.css +64 -2
- package/lib/module/components/inline-select/index.js +0 -284
- package/lib/module/components/inline-select/index.js.map +0 -1
- package/lib/typescript/src/components/inline-select/index.d.ts +0 -48
- package/lib/typescript/src/components/inline-select/index.d.ts.map +0 -1
- package/src/components/inline-select/index.tsx +0 -332
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
type ReactNode,
|
|
11
11
|
} from 'react';
|
|
12
12
|
import { Pressable, useWindowDimensions, View, type ViewProps } from 'react-native';
|
|
13
|
+
import { useCSSVariable } from 'uniwind';
|
|
13
14
|
import { Gesture, GestureDetector } from 'react-native-gesture-handler';
|
|
14
15
|
import Animated, {
|
|
15
16
|
FadeIn,
|
|
@@ -24,6 +25,7 @@ import Animated, {
|
|
|
24
25
|
} from 'react-native-reanimated';
|
|
25
26
|
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
|
26
27
|
import { getNativeUI } from '../../native';
|
|
28
|
+
import { XIcon } from '../../icons';
|
|
27
29
|
import { Portal } from '../../primitives/portal';
|
|
28
30
|
import { cn } from '../../utils/cn';
|
|
29
31
|
|
|
@@ -69,6 +71,39 @@ export interface BottomSheetProps {
|
|
|
69
71
|
snapPoints?: ('half' | 'full' | { fraction: number } | { height: number })[];
|
|
70
72
|
}
|
|
71
73
|
|
|
74
|
+
/**
|
|
75
|
+
* Roughly how tall the platform will make the sheet at a given detent, as a
|
|
76
|
+
* fraction of the screen. Approximate on purpose — it is used as a floor for
|
|
77
|
+
* the content, not as the sheet's real height, which the platform owns.
|
|
78
|
+
*/
|
|
79
|
+
const DETENT_FRACTION = { half: 0.5, full: 0.9 } as const;
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* The height the content should at least fill for a given set of detents.
|
|
83
|
+
*
|
|
84
|
+
* Without this the hosted content sizes to itself and the platform centres
|
|
85
|
+
* that smaller box inside the taller sheet, which is why a short sheet shows
|
|
86
|
+
* its content floating in the middle. Filling the detent leaves nothing to
|
|
87
|
+
* centre, so the content sits where it was written: at the top.
|
|
88
|
+
*/
|
|
89
|
+
function detentFloor(
|
|
90
|
+
snapPoints: BottomSheetProps['snapPoints'],
|
|
91
|
+
screenHeight: number
|
|
92
|
+
): number | undefined {
|
|
93
|
+
if (!snapPoints?.length) return undefined;
|
|
94
|
+
|
|
95
|
+
// The sheet opens at its first detent, so that is the one to fill.
|
|
96
|
+
const first = snapPoints[0];
|
|
97
|
+
if (first === 'half' || first === 'full') {
|
|
98
|
+
return screenHeight * DETENT_FRACTION[first];
|
|
99
|
+
}
|
|
100
|
+
if (typeof first === 'object' && 'fraction' in first) {
|
|
101
|
+
return screenHeight * first.fraction;
|
|
102
|
+
}
|
|
103
|
+
if (typeof first === 'object' && 'height' in first) return first.height;
|
|
104
|
+
return undefined;
|
|
105
|
+
}
|
|
106
|
+
|
|
72
107
|
/**
|
|
73
108
|
* Set by the root so Content knows the platform is drawing the sheet, and with
|
|
74
109
|
* which detents. Null means the styled sheet renders.
|
|
@@ -138,12 +173,18 @@ export interface BottomSheetContentProps extends ViewProps {
|
|
|
138
173
|
className?: string;
|
|
139
174
|
/** Tap on the backdrop closes the sheet. Default true. */
|
|
140
175
|
dismissible?: boolean;
|
|
176
|
+
/**
|
|
177
|
+
* Show a close button in the top-right corner. On by default for the styled
|
|
178
|
+
* sheet; ignored by the native sheet, which has its own dismiss affordances.
|
|
179
|
+
*/
|
|
180
|
+
showClose?: boolean;
|
|
141
181
|
children?: ReactNode;
|
|
142
182
|
}
|
|
143
183
|
|
|
144
184
|
function BottomSheetContent({
|
|
145
185
|
className,
|
|
146
186
|
dismissible = true,
|
|
187
|
+
showClose = true,
|
|
147
188
|
children,
|
|
148
189
|
...props
|
|
149
190
|
}: BottomSheetContentProps) {
|
|
@@ -153,6 +194,7 @@ function BottomSheetContent({
|
|
|
153
194
|
const { height: screenHeight, width: screenWidth } = useWindowDimensions();
|
|
154
195
|
const insets = useSafeAreaInsets();
|
|
155
196
|
const translateY = useSharedValue(0);
|
|
197
|
+
const closeTint = useCSSVariable('--color-muted-foreground');
|
|
156
198
|
|
|
157
199
|
const close = useCallback(() => setOpen(false), [setOpen]);
|
|
158
200
|
|
|
@@ -200,8 +242,10 @@ function BottomSheetContent({
|
|
|
200
242
|
{...props}
|
|
201
243
|
className={cn(
|
|
202
244
|
// The platform draws the container, but it hands us a bare
|
|
203
|
-
// box — padding and safe-area are still ours.
|
|
204
|
-
|
|
245
|
+
// box — padding and safe-area are still ours. The top
|
|
246
|
+
// padding has to clear the platform's grabber, which sits
|
|
247
|
+
// inside the sheet rather than above the content.
|
|
248
|
+
'justify-start gap-2 px-5 pb-2 pt-5',
|
|
205
249
|
className
|
|
206
250
|
)}
|
|
207
251
|
style={{
|
|
@@ -210,6 +254,7 @@ function BottomSheetContent({
|
|
|
210
254
|
// against, so `100%` measures against nothing and the
|
|
211
255
|
// content lays out wider than the sheet it sits in.
|
|
212
256
|
width: screenWidth,
|
|
257
|
+
minHeight: detentFloor(nativeSheet.snapPoints, screenHeight),
|
|
213
258
|
paddingBottom: Math.max(insets.bottom, 16),
|
|
214
259
|
}}
|
|
215
260
|
>
|
|
@@ -256,6 +301,17 @@ function BottomSheetContent({
|
|
|
256
301
|
<View className="mb-3 self-center">
|
|
257
302
|
<View className="h-1 w-10 rounded-full bg-muted-foreground/30" />
|
|
258
303
|
</View>
|
|
304
|
+
{showClose ? (
|
|
305
|
+
<Pressable
|
|
306
|
+
accessibilityRole="button"
|
|
307
|
+
accessibilityLabel="Close"
|
|
308
|
+
onPress={close}
|
|
309
|
+
hitSlop={8}
|
|
310
|
+
className="absolute right-4 top-3 h-8 w-8 items-center justify-center rounded-full bg-muted active:opacity-70"
|
|
311
|
+
>
|
|
312
|
+
<XIcon size={16} color={typeof closeTint === 'string' ? closeTint : undefined} />
|
|
313
|
+
</Pressable>
|
|
314
|
+
) : null}
|
|
259
315
|
{children}
|
|
260
316
|
</Animated.View>
|
|
261
317
|
</GestureDetector>
|
|
@@ -110,14 +110,35 @@ export interface ButtonProps
|
|
|
110
110
|
* optional `@expo/ui` package; without it this prop does nothing.
|
|
111
111
|
*
|
|
112
112
|
* **Theme tokens do not apply** — the platform draws the button, so
|
|
113
|
-
* `className`, `
|
|
114
|
-
*
|
|
115
|
-
*
|
|
116
|
-
*
|
|
113
|
+
* `className`, `fullWidth`, `startContent`, `endContent` and `loading` are
|
|
114
|
+
* all ignored. `variant` maps onto the nearest platform style:
|
|
115
|
+
* `primary`/`destructive` → filled, `outline` → outlined, everything else
|
|
116
|
+
* → text; `size` sets the height.
|
|
117
|
+
*
|
|
118
|
+
* A native button **fills the width it is given**, because the host is told
|
|
119
|
+
* its size rather than left to measure the platform's content — which is
|
|
120
|
+
* what made it collapse and then jump on first press. Put it in a row to
|
|
121
|
+
* share the width, or in a narrower parent to shrink it.
|
|
117
122
|
*/
|
|
118
123
|
native?: boolean;
|
|
119
124
|
}
|
|
120
125
|
|
|
126
|
+
/**
|
|
127
|
+
* Height given to the native host, matching the styled scale above.
|
|
128
|
+
*
|
|
129
|
+
* The host is told its size rather than asked to work it out. `matchContents`
|
|
130
|
+
* measures the platform's own content a frame late and again when that content
|
|
131
|
+
* changes — so the button renders at nothing, collapses against whatever sits
|
|
132
|
+
* above it, and jolts into place on the first press. Width comes from ordinary
|
|
133
|
+
* layout instead, which is why a native button fills its container.
|
|
134
|
+
*/
|
|
135
|
+
const NATIVE_HEIGHT: Record<NonNullable<ButtonVariantProps['size']>, number> = {
|
|
136
|
+
sm: 36,
|
|
137
|
+
md: 44,
|
|
138
|
+
lg: 48,
|
|
139
|
+
icon: 44,
|
|
140
|
+
};
|
|
141
|
+
|
|
121
142
|
/** PanelUI variants mapped onto the platform button styles. */
|
|
122
143
|
const NATIVE_VARIANT: Record<
|
|
123
144
|
NonNullable<ButtonVariantProps['variant']>,
|
|
@@ -178,7 +199,7 @@ export const Button = forwardRef<View, ButtonProps>(
|
|
|
178
199
|
const isStringLabel = typeof children === 'string';
|
|
179
200
|
|
|
180
201
|
return (
|
|
181
|
-
<Host
|
|
202
|
+
<Host style={{ height: NATIVE_HEIGHT[size ?? 'md'] }}>
|
|
182
203
|
<NativeButton
|
|
183
204
|
label={isStringLabel ? children : undefined}
|
|
184
205
|
variant={NATIVE_VARIANT[variant ?? 'primary']}
|
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
import { Pressable, View, type ViewProps } from 'react-native';
|
|
14
14
|
import Animated, { FadeIn, FadeOut, ZoomIn } from 'react-native-reanimated';
|
|
15
15
|
import { Portal } from '../../primitives/portal';
|
|
16
|
+
import { Scrim } from '../../primitives/scrim';
|
|
16
17
|
import { Text, type TextProps } from '../../primitives/text';
|
|
17
18
|
import { cn } from '../../utils/cn';
|
|
18
19
|
|
|
@@ -82,12 +83,19 @@ export interface DialogContentProps extends ViewProps {
|
|
|
82
83
|
className?: string;
|
|
83
84
|
/** Tap on the backdrop closes the dialog. Default true. */
|
|
84
85
|
dismissible?: boolean;
|
|
86
|
+
/**
|
|
87
|
+
* Frost the screen behind the dialog instead of dimming it. Uses `expo-blur`
|
|
88
|
+
* when installed and falls back to the dim when it is not, so it is safe to
|
|
89
|
+
* pass either way.
|
|
90
|
+
*/
|
|
91
|
+
blur?: boolean;
|
|
85
92
|
children?: ReactNode;
|
|
86
93
|
}
|
|
87
94
|
|
|
88
95
|
function DialogContent({
|
|
89
96
|
className,
|
|
90
97
|
dismissible = true,
|
|
98
|
+
blur = false,
|
|
91
99
|
children,
|
|
92
100
|
...props
|
|
93
101
|
}: DialogContentProps) {
|
|
@@ -106,9 +114,10 @@ function DialogContent({
|
|
|
106
114
|
exiting={FadeOut.duration(150)}
|
|
107
115
|
className="absolute inset-0 items-center justify-center p-6"
|
|
108
116
|
>
|
|
117
|
+
<Scrim blur={blur} />
|
|
109
118
|
<Pressable
|
|
110
119
|
accessibilityLabel="Close dialog"
|
|
111
|
-
className="absolute inset-0
|
|
120
|
+
className="absolute inset-0"
|
|
112
121
|
onPress={dismissible ? () => setOpen(false) : undefined}
|
|
113
122
|
/>
|
|
114
123
|
<Animated.View
|
|
@@ -6,26 +6,49 @@
|
|
|
6
6
|
* is a pair of CSS pseudo-elements; React Native has none, so it is two
|
|
7
7
|
* absolutely positioned siblings with static rotate transforms.
|
|
8
8
|
*/
|
|
9
|
-
import { forwardRef, type ReactNode } from 'react';
|
|
9
|
+
import { createContext, forwardRef, useContext, type ReactNode } from 'react';
|
|
10
10
|
import { View, type Text as RNText, type ViewProps } from 'react-native';
|
|
11
11
|
import { tv, type VariantProps } from 'tailwind-variants';
|
|
12
12
|
import { Text, type TextProps } from '../../primitives/text';
|
|
13
13
|
|
|
14
|
+
type EmptyStateSize = 'sm' | 'default' | 'lg';
|
|
15
|
+
|
|
16
|
+
/** Sub-components read the size so the title, description and media scale with the root. */
|
|
17
|
+
const EmptyStateContext = createContext<{ size: EmptyStateSize }>({ size: 'default' });
|
|
18
|
+
|
|
14
19
|
const emptyStateVariants = tv({
|
|
15
20
|
slots: {
|
|
16
|
-
root: 'w-full
|
|
21
|
+
root: 'w-full items-center justify-center',
|
|
17
22
|
header: 'w-full max-w-sm items-center gap-1',
|
|
18
|
-
title: 'text-center
|
|
19
|
-
description: 'text-center text-
|
|
23
|
+
title: 'text-center font-semibold text-foreground',
|
|
24
|
+
description: 'text-center text-muted-foreground',
|
|
20
25
|
content: 'w-full max-w-sm items-center gap-3',
|
|
21
26
|
},
|
|
27
|
+
variants: {
|
|
28
|
+
variant: {
|
|
29
|
+
// Fills whatever space it is given — a whole screen, or a flex parent.
|
|
30
|
+
default: { root: 'flex-1' },
|
|
31
|
+
// A self-contained block that sits inside content rather than filling a
|
|
32
|
+
// screen: bordered, on the card fill, sized to its contents.
|
|
33
|
+
card: { root: 'rounded-2xl border border-border bg-card' },
|
|
34
|
+
},
|
|
35
|
+
size: {
|
|
36
|
+
sm: { root: 'gap-4 px-4 py-8', title: 'text-base', description: 'text-sm' },
|
|
37
|
+
default: { root: 'gap-6 px-6 py-12', title: 'text-xl', description: 'text-sm' },
|
|
38
|
+
lg: { root: 'gap-6 px-6 py-16', title: 'text-2xl', description: 'text-base' },
|
|
39
|
+
},
|
|
40
|
+
},
|
|
41
|
+
defaultVariants: {
|
|
42
|
+
variant: 'default',
|
|
43
|
+
size: 'default',
|
|
44
|
+
},
|
|
22
45
|
});
|
|
23
46
|
|
|
24
47
|
const mediaVariants = tv({
|
|
25
48
|
slots: {
|
|
26
49
|
wrapper: 'items-center justify-center',
|
|
27
|
-
card: '
|
|
28
|
-
ghost: 'absolute
|
|
50
|
+
card: 'shrink-0 items-center justify-center',
|
|
51
|
+
ghost: 'absolute rounded-md border border-border bg-card',
|
|
29
52
|
},
|
|
30
53
|
variants: {
|
|
31
54
|
variant: {
|
|
@@ -37,21 +60,41 @@ const mediaVariants = tv({
|
|
|
37
60
|
card: 'rounded-md border border-border bg-card',
|
|
38
61
|
},
|
|
39
62
|
},
|
|
63
|
+
size: {
|
|
64
|
+
sm: { card: 'h-8 w-8', ghost: 'h-8 w-8' },
|
|
65
|
+
default: { card: 'h-9 w-9', ghost: 'h-9 w-9' },
|
|
66
|
+
lg: { card: 'h-11 w-11', ghost: 'h-11 w-11' },
|
|
67
|
+
},
|
|
40
68
|
},
|
|
41
69
|
defaultVariants: {
|
|
42
70
|
variant: 'default',
|
|
71
|
+
size: 'default',
|
|
43
72
|
},
|
|
44
73
|
});
|
|
45
74
|
|
|
46
|
-
export interface EmptyStateProps
|
|
75
|
+
export interface EmptyStateProps
|
|
76
|
+
extends ViewProps,
|
|
77
|
+
VariantProps<typeof emptyStateVariants> {
|
|
47
78
|
className?: string;
|
|
79
|
+
/**
|
|
80
|
+
* `default` fills its parent — a whole screen, or a flex slot. `card` is a
|
|
81
|
+
* self-contained bordered block for an empty state that sits inside content
|
|
82
|
+
* rather than owning the screen.
|
|
83
|
+
*/
|
|
84
|
+
variant?: 'default' | 'card';
|
|
85
|
+
/** Scales the padding, media, title and description together. */
|
|
86
|
+
size?: EmptyStateSize;
|
|
48
87
|
children?: ReactNode;
|
|
49
88
|
}
|
|
50
89
|
|
|
51
90
|
const EmptyStateRoot = forwardRef<View, EmptyStateProps>(
|
|
52
|
-
({ className, ...props }, ref) => {
|
|
53
|
-
const { root } = emptyStateVariants();
|
|
54
|
-
return
|
|
91
|
+
({ className, variant, size = 'default', ...props }, ref) => {
|
|
92
|
+
const { root } = emptyStateVariants({ variant, size });
|
|
93
|
+
return (
|
|
94
|
+
<EmptyStateContext.Provider value={{ size }}>
|
|
95
|
+
<View ref={ref} className={root({ className })} {...props} />
|
|
96
|
+
</EmptyStateContext.Provider>
|
|
97
|
+
);
|
|
55
98
|
}
|
|
56
99
|
);
|
|
57
100
|
EmptyStateRoot.displayName = 'EmptyState';
|
|
@@ -78,7 +121,8 @@ export interface EmptyStateMediaProps
|
|
|
78
121
|
*/
|
|
79
122
|
const EmptyStateMedia = forwardRef<View, EmptyStateMediaProps>(
|
|
80
123
|
({ className, variant = 'default', children, ...props }, ref) => {
|
|
81
|
-
const {
|
|
124
|
+
const { size } = useContext(EmptyStateContext);
|
|
125
|
+
const { wrapper, card, ghost } = mediaVariants({ variant, size });
|
|
82
126
|
|
|
83
127
|
return (
|
|
84
128
|
<View ref={ref} className={wrapper({ className: `mb-4 ${className ?? ''}` })} {...props}>
|
|
@@ -104,21 +148,23 @@ const EmptyStateMedia = forwardRef<View, EmptyStateMediaProps>(
|
|
|
104
148
|
EmptyStateMedia.displayName = 'EmptyState.Media';
|
|
105
149
|
|
|
106
150
|
const EmptyStateTitle = forwardRef<RNText, TextProps>(({ className, ...props }, ref) => {
|
|
107
|
-
const {
|
|
151
|
+
const { size } = useContext(EmptyStateContext);
|
|
152
|
+
const { title } = emptyStateVariants({ size });
|
|
108
153
|
return <Text ref={ref} className={title({ className })} {...props} />;
|
|
109
154
|
});
|
|
110
155
|
EmptyStateTitle.displayName = 'EmptyState.Title';
|
|
111
156
|
|
|
112
157
|
const EmptyStateDescription = forwardRef<RNText, TextProps>(
|
|
113
158
|
({ className, ...props }, ref) => {
|
|
114
|
-
const {
|
|
159
|
+
const { size } = useContext(EmptyStateContext);
|
|
160
|
+
const { description } = emptyStateVariants({ size });
|
|
115
161
|
return <Text ref={ref} className={description({ className })} {...props} />;
|
|
116
162
|
}
|
|
117
163
|
);
|
|
118
164
|
EmptyStateDescription.displayName = 'EmptyState.Description';
|
|
119
165
|
|
|
120
166
|
/** Slot below the header, for actions such as a primary button. */
|
|
121
|
-
const EmptyStateContent = forwardRef<View,
|
|
167
|
+
const EmptyStateContent = forwardRef<View, ViewProps & { className?: string }>(
|
|
122
168
|
({ className, ...props }, ref) => {
|
|
123
169
|
const { content } = emptyStateVariants();
|
|
124
170
|
return <View ref={ref} className={content({ className })} {...props} />;
|
|
@@ -15,28 +15,67 @@
|
|
|
15
15
|
* </Frame.Header>
|
|
16
16
|
* <Frame.Panel>
|
|
17
17
|
* <Frame.Row>…</Frame.Row>
|
|
18
|
-
* <Frame.Row
|
|
18
|
+
* <Frame.Row>…</Frame.Row>
|
|
19
19
|
* </Frame.Panel>
|
|
20
20
|
* <Frame.Footer>Updated 2 minutes ago</Frame.Footer>
|
|
21
21
|
* </Frame>
|
|
22
22
|
* ```
|
|
23
|
+
*
|
|
24
|
+
* The panel draws the hairlines between its own rows. React Native has no
|
|
25
|
+
* `:first-child`, so the alternative is every caller writing
|
|
26
|
+
* `divided={index > 0}` on every row and getting it wrong once.
|
|
23
27
|
*/
|
|
24
|
-
import { forwardRef, type ReactNode } from 'react';
|
|
25
|
-
import {
|
|
28
|
+
import { Children, cloneElement, forwardRef, isValidElement, type ReactNode } from 'react';
|
|
29
|
+
import {
|
|
30
|
+
Pressable,
|
|
31
|
+
View,
|
|
32
|
+
type PressableProps,
|
|
33
|
+
type Text as RNText,
|
|
34
|
+
type ViewProps,
|
|
35
|
+
} from 'react-native';
|
|
36
|
+
import { tv } from 'tailwind-variants';
|
|
37
|
+
import { ChevronRightIcon } from '../../icons';
|
|
26
38
|
import { Text, type TextProps } from '../../primitives/text';
|
|
27
39
|
import { cn } from '../../utils/cn';
|
|
28
40
|
|
|
41
|
+
const frameVariants = tv({
|
|
42
|
+
slots: {
|
|
43
|
+
root: '',
|
|
44
|
+
panel: 'overflow-hidden rounded-xl border border-border bg-card',
|
|
45
|
+
},
|
|
46
|
+
variants: {
|
|
47
|
+
variant: {
|
|
48
|
+
default: { root: 'rounded-2xl border border-border bg-surface p-1.5' },
|
|
49
|
+
// No shell: the panel is the whole widget. For a Frame nested inside a
|
|
50
|
+
// card that already draws a border, where the shell's own edge sitting
|
|
51
|
+
// just inside it reads as a double line.
|
|
52
|
+
plain: { root: '', panel: 'rounded-2xl' },
|
|
53
|
+
},
|
|
54
|
+
},
|
|
55
|
+
defaultVariants: {
|
|
56
|
+
variant: 'default',
|
|
57
|
+
},
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
export type FrameVariant = 'default' | 'plain';
|
|
61
|
+
|
|
29
62
|
export interface FrameProps extends ViewProps {
|
|
30
63
|
className?: string;
|
|
31
64
|
}
|
|
32
65
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
66
|
+
export interface FrameRootProps extends FrameProps {
|
|
67
|
+
/**
|
|
68
|
+
* `plain` drops the outer shell so the panel is the widget — for a Frame
|
|
69
|
+
* inside a container that already draws its own border.
|
|
70
|
+
*/
|
|
71
|
+
variant?: FrameVariant;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const FrameRoot = forwardRef<View, FrameRootProps>(
|
|
75
|
+
({ className, variant, ...props }, ref) => (
|
|
76
|
+
<View ref={ref} className={frameVariants({ variant }).root({ className })} {...props} />
|
|
77
|
+
)
|
|
78
|
+
);
|
|
40
79
|
FrameRoot.displayName = 'Frame';
|
|
41
80
|
|
|
42
81
|
export interface FrameHeaderProps extends FrameProps {
|
|
@@ -103,41 +142,147 @@ const FrameDescription = forwardRef<RNText, TextProps>(
|
|
|
103
142
|
);
|
|
104
143
|
FrameDescription.displayName = 'Frame.Description';
|
|
105
144
|
|
|
145
|
+
/**
|
|
146
|
+
* Marks the parts that take part in the panel's own divider bookkeeping —
|
|
147
|
+
* a Row draws a line above itself, a Section draws one above its heading.
|
|
148
|
+
* Anything else the panel is given is left alone.
|
|
149
|
+
*/
|
|
150
|
+
interface Dividable {
|
|
151
|
+
divided?: boolean;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Draws the hairline above every child but the first. An explicit `divided`
|
|
156
|
+
* on a child wins, so a row can still opt out or force one.
|
|
157
|
+
*/
|
|
158
|
+
function divideChildren(children: ReactNode) {
|
|
159
|
+
let seen = 0;
|
|
160
|
+
return Children.map(children, (child) => {
|
|
161
|
+
if (!isValidElement<Dividable>(child)) return child;
|
|
162
|
+
if (!DIVIDABLE.has(child.type)) return child;
|
|
163
|
+
|
|
164
|
+
const index = seen++;
|
|
165
|
+
if (child.props.divided !== undefined) return child;
|
|
166
|
+
return cloneElement(child, { divided: index > 0 });
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
export interface FramePanelProps extends FrameProps {
|
|
171
|
+
/**
|
|
172
|
+
* Set false to place the hairlines by hand instead — for a panel whose rows
|
|
173
|
+
* are generated somewhere the divider order is not obvious.
|
|
174
|
+
*/
|
|
175
|
+
dividers?: boolean;
|
|
176
|
+
children?: ReactNode;
|
|
177
|
+
}
|
|
178
|
+
|
|
106
179
|
/**
|
|
107
180
|
* The inset card holding the frame's content — the raised surface sitting in
|
|
108
181
|
* from the shell's edges, with its own smaller radius.
|
|
109
182
|
*/
|
|
110
|
-
const FramePanel = forwardRef<View,
|
|
111
|
-
|
|
112
|
-
ref={ref}
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
)
|
|
183
|
+
const FramePanel = forwardRef<View, FramePanelProps>(
|
|
184
|
+
({ className, dividers = true, children, ...props }, ref) => (
|
|
185
|
+
<View ref={ref} className={frameVariants().panel({ className })} {...props}>
|
|
186
|
+
{dividers ? divideChildren(children) : children}
|
|
187
|
+
</View>
|
|
188
|
+
)
|
|
189
|
+
);
|
|
117
190
|
FramePanel.displayName = 'Frame.Panel';
|
|
118
191
|
|
|
192
|
+
export interface FrameRowProps extends Omit<PressableProps, 'children'>, Dividable {
|
|
193
|
+
className?: string;
|
|
194
|
+
/**
|
|
195
|
+
* Draw a hairline above this row. `Frame.Panel` sets it for you; pass it
|
|
196
|
+
* explicitly to override the panel's decision either way.
|
|
197
|
+
*/
|
|
198
|
+
divided?: boolean;
|
|
199
|
+
/** Trailing chevron marking the row as leading somewhere. */
|
|
200
|
+
chevron?: boolean;
|
|
201
|
+
children?: ReactNode;
|
|
202
|
+
}
|
|
203
|
+
|
|
119
204
|
/**
|
|
120
|
-
* A row inside a Frame.Panel.
|
|
121
|
-
*
|
|
205
|
+
* A row inside a Frame.Panel. Give it an `onPress` and it becomes a real
|
|
206
|
+
* pressable — press feedback, a button role — rather than a View with a
|
|
207
|
+
* handler bolted on.
|
|
122
208
|
*/
|
|
123
|
-
|
|
209
|
+
const FrameRow = forwardRef<View, FrameRowProps>(
|
|
210
|
+
({ className, divided, chevron, children, onPress, ...props }, ref) => {
|
|
211
|
+
const classes = cn(
|
|
212
|
+
'flex-row items-center gap-3 px-4 py-3.5',
|
|
213
|
+
divided && 'border-t border-border',
|
|
214
|
+
onPress && 'active:bg-muted',
|
|
215
|
+
className
|
|
216
|
+
);
|
|
217
|
+
|
|
218
|
+
const body = (
|
|
219
|
+
<>
|
|
220
|
+
{children}
|
|
221
|
+
{chevron ? <ChevronRightIcon size={16} /> : null}
|
|
222
|
+
</>
|
|
223
|
+
);
|
|
224
|
+
|
|
225
|
+
if (!onPress) {
|
|
226
|
+
return (
|
|
227
|
+
<View ref={ref} className={classes} {...(props as ViewProps)}>
|
|
228
|
+
{body}
|
|
229
|
+
</View>
|
|
230
|
+
);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
return (
|
|
234
|
+
<Pressable
|
|
235
|
+
ref={ref}
|
|
236
|
+
accessibilityRole="button"
|
|
237
|
+
onPress={onPress}
|
|
238
|
+
className={classes}
|
|
239
|
+
{...props}
|
|
240
|
+
>
|
|
241
|
+
{body}
|
|
242
|
+
</Pressable>
|
|
243
|
+
);
|
|
244
|
+
}
|
|
245
|
+
);
|
|
246
|
+
FrameRow.displayName = 'Frame.Row';
|
|
247
|
+
|
|
248
|
+
export interface FrameSectionProps extends FrameProps, Dividable {
|
|
249
|
+
/** Heading above the rows. Strings are wrapped for you. */
|
|
250
|
+
title?: ReactNode;
|
|
124
251
|
divided?: boolean;
|
|
252
|
+
children?: ReactNode;
|
|
125
253
|
}
|
|
126
254
|
|
|
127
|
-
|
|
128
|
-
|
|
255
|
+
/**
|
|
256
|
+
* A labelled cluster of rows inside a Panel, for a widget holding more than
|
|
257
|
+
* one group. It divides its own rows the way the panel does, so the two nest
|
|
258
|
+
* without either having to know about the other.
|
|
259
|
+
*/
|
|
260
|
+
const FrameSection = forwardRef<View, FrameSectionProps>(
|
|
261
|
+
({ className, title, divided, children, ...props }, ref) => (
|
|
129
262
|
<View
|
|
130
263
|
ref={ref}
|
|
131
|
-
className={cn(
|
|
132
|
-
'flex-row items-center gap-3 px-4 py-3.5',
|
|
133
|
-
divided && 'border-t border-border',
|
|
134
|
-
className
|
|
135
|
-
)}
|
|
264
|
+
className={cn(divided && 'border-t border-border', className)}
|
|
136
265
|
{...props}
|
|
137
|
-
|
|
266
|
+
>
|
|
267
|
+
{title ? (
|
|
268
|
+
<View className="bg-muted/40 px-4 pb-1.5 pt-2.5">
|
|
269
|
+
{typeof title === 'string' ? (
|
|
270
|
+
<Text size="xs" weight="medium" muted className="uppercase tracking-wider">
|
|
271
|
+
{title}
|
|
272
|
+
</Text>
|
|
273
|
+
) : (
|
|
274
|
+
title
|
|
275
|
+
)}
|
|
276
|
+
</View>
|
|
277
|
+
) : null}
|
|
278
|
+
{divideChildren(children)}
|
|
279
|
+
</View>
|
|
138
280
|
)
|
|
139
281
|
);
|
|
140
|
-
|
|
282
|
+
FrameSection.displayName = 'Frame.Section';
|
|
283
|
+
|
|
284
|
+
/** Parts the panel divides. Declared after them, since it holds references. */
|
|
285
|
+
const DIVIDABLE = new Set<unknown>([FrameRow, FrameSection]);
|
|
141
286
|
|
|
142
287
|
export interface FrameFooterProps extends FrameProps {
|
|
143
288
|
children?: ReactNode;
|
|
@@ -169,6 +314,7 @@ export const Frame = Object.assign(FrameRoot, {
|
|
|
169
314
|
Action: FrameAction,
|
|
170
315
|
Description: FrameDescription,
|
|
171
316
|
Panel: FramePanel,
|
|
317
|
+
Section: FrameSection,
|
|
172
318
|
Row: FrameRow,
|
|
173
319
|
Footer: FrameFooter,
|
|
174
320
|
});
|