panelui-native 0.6.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.
- package/README.md +1 -1
- package/lib/module/components/accordion/index.js +7 -5
- package/lib/module/components/accordion/index.js.map +1 -1
- package/lib/module/components/alert/index.js +3 -6
- package/lib/module/components/alert/index.js.map +1 -1
- 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 +82 -4
- package/lib/module/components/bottom-sheet/index.js.map +1 -1
- package/lib/module/components/button/index.js +36 -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 +98 -22
- package/lib/module/components/empty-state/index.js.map +1 -1
- package/lib/module/components/inline-select/index.js +171 -46
- package/lib/module/components/inline-select/index.js.map +1 -1
- package/lib/module/components/input/index.js +28 -5
- package/lib/module/components/input/index.js.map +1 -1
- package/lib/module/components/input-group/index.js +0 -1
- package/lib/module/components/input-group/index.js.map +1 -1
- package/lib/module/components/item/index.js +359 -0
- package/lib/module/components/item/index.js.map +1 -0
- package/lib/module/components/label/index.js +5 -3
- package/lib/module/components/label/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 +334 -0
- package/lib/module/components/message/index.js.map +1 -0
- 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 +500 -0
- package/lib/module/components/popover/index.js.map +1 -0
- package/lib/module/components/radio-group/index.js +104 -16
- package/lib/module/components/radio-group/index.js.map +1 -1
- package/lib/module/components/scroll-fade/index.js +119 -71
- package/lib/module/components/scroll-fade/index.js.map +1 -1
- package/lib/module/components/select/index.js +35 -6
- 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/shimmer/index.js +111 -64
- package/lib/module/components/shimmer/index.js.map +1 -1
- package/lib/module/components/surface/index.js +24 -4
- package/lib/module/components/surface/index.js.map +1 -1
- package/lib/module/components/switch/index.js +20 -1
- package/lib/module/components/switch/index.js.map +1 -1
- package/lib/module/components/toast/index.js +7 -8
- package/lib/module/components/toast/index.js.map +1 -1
- package/lib/module/components/typography/index.js +3 -3
- package/lib/module/hooks/index.js +1 -0
- package/lib/module/hooks/index.js.map +1 -1
- package/lib/module/hooks/use-breakpoint.js +2 -2
- package/lib/module/hooks/use-copy-to-clipboard.js +0 -2
- package/lib/module/hooks/use-copy-to-clipboard.js.map +1 -1
- package/lib/module/hooks/use-keyboard-avoidance.js +120 -0
- package/lib/module/hooks/use-keyboard-avoidance.js.map +1 -0
- package/lib/module/icons/index.js +73 -4
- package/lib/module/icons/index.js.map +1 -1
- package/lib/module/index.js +13 -1
- package/lib/module/index.js.map +1 -1
- package/lib/module/native/index.js +53 -0
- package/lib/module/native/index.js.map +1 -0
- package/lib/module/primitives/keyboard-avoider.js +40 -0
- package/lib/module/primitives/keyboard-avoider.js.map +1 -0
- package/lib/module/primitives/scrim.js +72 -0
- package/lib/module/primitives/scrim.js.map +1 -0
- package/lib/module/providers/panel-ui-provider.js +24 -4
- package/lib/module/providers/panel-ui-provider.js.map +1 -1
- package/lib/module/theme/use-theme.js +1 -1
- package/lib/typescript/src/components/accordion/index.d.ts +7 -5
- package/lib/typescript/src/components/accordion/index.d.ts.map +1 -1
- package/lib/typescript/src/components/alert/index.d.ts +3 -6
- package/lib/typescript/src/components/alert/index.d.ts.map +1 -1
- 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 +27 -2
- package/lib/typescript/src/components/bottom-sheet/index.d.ts.map +1 -1
- package/lib/typescript/src/components/button/index.d.ts +11 -0
- 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 +156 -7
- package/lib/typescript/src/components/empty-state/index.d.ts.map +1 -1
- package/lib/typescript/src/components/inline-select/index.d.ts +29 -6
- package/lib/typescript/src/components/inline-select/index.d.ts.map +1 -1
- package/lib/typescript/src/components/input/index.d.ts +13 -0
- package/lib/typescript/src/components/input/index.d.ts.map +1 -1
- package/lib/typescript/src/components/input-group/index.d.ts +0 -1
- package/lib/typescript/src/components/input-group/index.d.ts.map +1 -1
- package/lib/typescript/src/components/item/index.d.ts +286 -0
- package/lib/typescript/src/components/item/index.d.ts.map +1 -0
- package/lib/typescript/src/components/label/index.d.ts +5 -3
- package/lib/typescript/src/components/label/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 +177 -0
- package/lib/typescript/src/components/message/index.d.ts.map +1 -0
- 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 +130 -0
- package/lib/typescript/src/components/popover/index.d.ts.map +1 -0
- package/lib/typescript/src/components/radio-group/index.d.ts +32 -0
- package/lib/typescript/src/components/radio-group/index.d.ts.map +1 -1
- package/lib/typescript/src/components/scroll-fade/index.d.ts +18 -9
- package/lib/typescript/src/components/scroll-fade/index.d.ts.map +1 -1
- package/lib/typescript/src/components/select/index.d.ts +16 -1
- 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/shimmer/index.d.ts +38 -20
- package/lib/typescript/src/components/shimmer/index.d.ts.map +1 -1
- 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 +10 -0
- package/lib/typescript/src/components/switch/index.d.ts.map +1 -1
- package/lib/typescript/src/components/toast/index.d.ts +4 -4
- package/lib/typescript/src/components/toast/index.d.ts.map +1 -1
- 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-breakpoint.d.ts +2 -2
- package/lib/typescript/src/hooks/use-copy-to-clipboard.d.ts +0 -2
- package/lib/typescript/src/hooks/use-copy-to-clipboard.d.ts.map +1 -1
- package/lib/typescript/src/hooks/use-keyboard-avoidance.d.ts +20 -0
- package/lib/typescript/src/hooks/use-keyboard-avoidance.d.ts.map +1 -0
- package/lib/typescript/src/icons/index.d.ts +10 -4
- package/lib/typescript/src/icons/index.d.ts.map +1 -1
- package/lib/typescript/src/index.d.ts +11 -1
- package/lib/typescript/src/index.d.ts.map +1 -1
- package/lib/typescript/src/native/index.d.ts +35 -0
- package/lib/typescript/src/native/index.d.ts.map +1 -0
- package/lib/typescript/src/primitives/keyboard-avoider.d.ts +22 -0
- package/lib/typescript/src/primitives/keyboard-avoider.d.ts.map +1 -0
- 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/providers/panel-ui-provider.d.ts +1 -1
- package/lib/typescript/src/providers/panel-ui-provider.d.ts.map +1 -1
- package/lib/typescript/src/theme/use-theme.d.ts +1 -1
- package/package.json +16 -3
- package/src/components/accordion/index.tsx +7 -5
- package/src/components/alert/index.tsx +3 -6
- package/src/components/attachment/index.tsx +279 -0
- package/src/components/bottom-sheet/index.tsx +102 -2
- package/src/components/button/index.tsx +52 -0
- package/src/components/dialog/index.tsx +10 -1
- package/src/components/empty-state/index.tsx +64 -19
- package/src/components/inline-select/index.tsx +191 -44
- package/src/components/input/index.tsx +39 -3
- package/src/components/input-group/index.tsx +0 -1
- package/src/components/item/index.tsx +387 -0
- package/src/components/label/index.tsx +5 -3
- package/src/components/line-chart/index.tsx +1224 -0
- package/src/components/marker/index.tsx +194 -0
- package/src/components/message/index.tsx +336 -0
- package/src/components/message-scroller/index.tsx +574 -0
- package/src/components/popover/index.tsx +599 -0
- package/src/components/radio-group/index.tsx +108 -17
- package/src/components/scroll-fade/index.tsx +163 -51
- package/src/components/select/index.tsx +54 -9
- package/src/components/separator/index.tsx +97 -0
- package/src/components/shimmer/index.tsx +151 -58
- package/src/components/surface/index.tsx +32 -5
- package/src/components/switch/index.tsx +27 -1
- package/src/components/toast/index.tsx +7 -8
- package/src/components/typography/index.tsx +3 -3
- package/src/hooks/index.ts +5 -0
- package/src/hooks/use-breakpoint.ts +2 -2
- package/src/hooks/use-copy-to-clipboard.ts +0 -2
- package/src/hooks/use-keyboard-avoidance.ts +140 -0
- package/src/icons/index.tsx +52 -4
- package/src/index.ts +92 -0
- package/src/native/index.ts +79 -0
- package/src/primitives/keyboard-avoider.tsx +49 -0
- package/src/primitives/scrim.tsx +95 -0
- package/src/providers/panel-ui-provider.tsx +30 -9
- package/src/theme/use-theme.ts +1 -1
- package/theme.css +63 -3
|
@@ -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
|
|
@@ -1,32 +1,54 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* EmptyState — the placeholder shown when a list or view has no content.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* positioned siblings with static rotate transforms.
|
|
4
|
+
* Centred Header / Media / Title / Description / Content, with an `icon` media
|
|
5
|
+
* variant that stacks two rotated ghost cards behind the icon. On the web that
|
|
6
|
+
* is a pair of CSS pseudo-elements; React Native has none, so it is two
|
|
7
|
+
* absolutely positioned siblings with static rotate transforms.
|
|
9
8
|
*/
|
|
10
|
-
import { forwardRef, type ReactNode } from 'react';
|
|
9
|
+
import { createContext, forwardRef, useContext, type ReactNode } from 'react';
|
|
11
10
|
import { View, type Text as RNText, type ViewProps } from 'react-native';
|
|
12
11
|
import { tv, type VariantProps } from 'tailwind-variants';
|
|
13
12
|
import { Text, type TextProps } from '../../primitives/text';
|
|
14
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
|
+
|
|
15
19
|
const emptyStateVariants = tv({
|
|
16
20
|
slots: {
|
|
17
|
-
root: 'w-full
|
|
21
|
+
root: 'w-full items-center justify-center',
|
|
18
22
|
header: 'w-full max-w-sm items-center gap-1',
|
|
19
|
-
title: 'text-center
|
|
20
|
-
description: 'text-center text-
|
|
23
|
+
title: 'text-center font-semibold text-foreground',
|
|
24
|
+
description: 'text-center text-muted-foreground',
|
|
21
25
|
content: 'w-full max-w-sm items-center gap-3',
|
|
22
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
|
+
},
|
|
23
45
|
});
|
|
24
46
|
|
|
25
47
|
const mediaVariants = tv({
|
|
26
48
|
slots: {
|
|
27
49
|
wrapper: 'items-center justify-center',
|
|
28
|
-
card: '
|
|
29
|
-
ghost: 'absolute
|
|
50
|
+
card: 'shrink-0 items-center justify-center',
|
|
51
|
+
ghost: 'absolute rounded-md border border-border bg-card',
|
|
30
52
|
},
|
|
31
53
|
variants: {
|
|
32
54
|
variant: {
|
|
@@ -38,21 +60,41 @@ const mediaVariants = tv({
|
|
|
38
60
|
card: 'rounded-md border border-border bg-card',
|
|
39
61
|
},
|
|
40
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
|
+
},
|
|
41
68
|
},
|
|
42
69
|
defaultVariants: {
|
|
43
70
|
variant: 'default',
|
|
71
|
+
size: 'default',
|
|
44
72
|
},
|
|
45
73
|
});
|
|
46
74
|
|
|
47
|
-
export interface EmptyStateProps
|
|
75
|
+
export interface EmptyStateProps
|
|
76
|
+
extends ViewProps,
|
|
77
|
+
VariantProps<typeof emptyStateVariants> {
|
|
48
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;
|
|
49
87
|
children?: ReactNode;
|
|
50
88
|
}
|
|
51
89
|
|
|
52
90
|
const EmptyStateRoot = forwardRef<View, EmptyStateProps>(
|
|
53
|
-
({ className, ...props }, ref) => {
|
|
54
|
-
const { root } = emptyStateVariants();
|
|
55
|
-
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
|
+
);
|
|
56
98
|
}
|
|
57
99
|
);
|
|
58
100
|
EmptyStateRoot.displayName = 'EmptyState';
|
|
@@ -79,7 +121,8 @@ export interface EmptyStateMediaProps
|
|
|
79
121
|
*/
|
|
80
122
|
const EmptyStateMedia = forwardRef<View, EmptyStateMediaProps>(
|
|
81
123
|
({ className, variant = 'default', children, ...props }, ref) => {
|
|
82
|
-
const {
|
|
124
|
+
const { size } = useContext(EmptyStateContext);
|
|
125
|
+
const { wrapper, card, ghost } = mediaVariants({ variant, size });
|
|
83
126
|
|
|
84
127
|
return (
|
|
85
128
|
<View ref={ref} className={wrapper({ className: `mb-4 ${className ?? ''}` })} {...props}>
|
|
@@ -105,21 +148,23 @@ const EmptyStateMedia = forwardRef<View, EmptyStateMediaProps>(
|
|
|
105
148
|
EmptyStateMedia.displayName = 'EmptyState.Media';
|
|
106
149
|
|
|
107
150
|
const EmptyStateTitle = forwardRef<RNText, TextProps>(({ className, ...props }, ref) => {
|
|
108
|
-
const {
|
|
151
|
+
const { size } = useContext(EmptyStateContext);
|
|
152
|
+
const { title } = emptyStateVariants({ size });
|
|
109
153
|
return <Text ref={ref} className={title({ className })} {...props} />;
|
|
110
154
|
});
|
|
111
155
|
EmptyStateTitle.displayName = 'EmptyState.Title';
|
|
112
156
|
|
|
113
157
|
const EmptyStateDescription = forwardRef<RNText, TextProps>(
|
|
114
158
|
({ className, ...props }, ref) => {
|
|
115
|
-
const {
|
|
159
|
+
const { size } = useContext(EmptyStateContext);
|
|
160
|
+
const { description } = emptyStateVariants({ size });
|
|
116
161
|
return <Text ref={ref} className={description({ className })} {...props} />;
|
|
117
162
|
}
|
|
118
163
|
);
|
|
119
164
|
EmptyStateDescription.displayName = 'EmptyState.Description';
|
|
120
165
|
|
|
121
166
|
/** Slot below the header, for actions such as a primary button. */
|
|
122
|
-
const EmptyStateContent = forwardRef<View,
|
|
167
|
+
const EmptyStateContent = forwardRef<View, ViewProps & { className?: string }>(
|
|
123
168
|
({ className, ...props }, ref) => {
|
|
124
169
|
const { content } = emptyStateVariants();
|
|
125
170
|
return <View ref={ref} className={content({ className })} {...props} />;
|
|
@@ -1,3 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* InlineSelect — a select that reveals its options next to the trigger instead
|
|
3
|
+
* of taking over the screen with a sheet.
|
|
4
|
+
*
|
|
5
|
+
* Two presentations, because the right one depends on what surrounds it:
|
|
6
|
+
*
|
|
7
|
+
* - Inline (default) expands the list in normal layout flow. Everything below
|
|
8
|
+
* moves down. Fine inside a settings list where that reads as the row
|
|
9
|
+
* growing, wrong anywhere the shift is jarring.
|
|
10
|
+
* - `overlay` floats the list above the page through a portal, anchored to the
|
|
11
|
+
* trigger and flipped above it when there is no room below. Nothing else on
|
|
12
|
+
* the screen moves.
|
|
13
|
+
*/
|
|
1
14
|
import {
|
|
2
15
|
Children,
|
|
3
16
|
createContext,
|
|
@@ -5,10 +18,17 @@ import {
|
|
|
5
18
|
useCallback,
|
|
6
19
|
useContext,
|
|
7
20
|
useMemo,
|
|
21
|
+
useRef,
|
|
8
22
|
useState,
|
|
9
23
|
type ReactNode,
|
|
10
24
|
} from 'react';
|
|
11
|
-
import {
|
|
25
|
+
import {
|
|
26
|
+
Pressable,
|
|
27
|
+
ScrollView,
|
|
28
|
+
useWindowDimensions,
|
|
29
|
+
View,
|
|
30
|
+
type LayoutChangeEvent,
|
|
31
|
+
} from 'react-native';
|
|
12
32
|
import Animated, {
|
|
13
33
|
FadeIn,
|
|
14
34
|
FadeOut,
|
|
@@ -19,6 +39,7 @@ import Animated, {
|
|
|
19
39
|
import { tv } from 'tailwind-variants';
|
|
20
40
|
import { useCSSVariable } from 'uniwind';
|
|
21
41
|
import { CheckIcon, ChevronDownIcon } from '../../icons';
|
|
42
|
+
import { Portal } from '../../primitives/portal';
|
|
22
43
|
import { Text } from '../../primitives/text';
|
|
23
44
|
|
|
24
45
|
const inlineSelectVariants = tv({
|
|
@@ -27,7 +48,7 @@ const inlineSelectVariants = tv({
|
|
|
27
48
|
'w-full flex-row items-center justify-between gap-3 rounded-2xl border border-input bg-background px-4 py-3.5',
|
|
28
49
|
triggerLabel: 'flex-1 text-base font-medium text-foreground',
|
|
29
50
|
placeholder: 'flex-1 text-base text-muted-foreground',
|
|
30
|
-
list: '
|
|
51
|
+
list: 'overflow-hidden rounded-2xl border border-border bg-popover p-2 shadow-sm',
|
|
31
52
|
item: 'flex-row items-center gap-2 rounded-xl px-3 py-3',
|
|
32
53
|
itemLabel: 'flex-1 text-base font-medium text-foreground',
|
|
33
54
|
itemIndicator: 'h-5 w-5 items-center justify-center',
|
|
@@ -39,6 +60,13 @@ const inlineSelectVariants = tv({
|
|
|
39
60
|
disabled: {
|
|
40
61
|
true: { trigger: 'opacity-[0.64]' },
|
|
41
62
|
},
|
|
63
|
+
presentation: {
|
|
64
|
+
inline: { list: 'mt-2' },
|
|
65
|
+
overlay: { list: 'shadow-lg' },
|
|
66
|
+
},
|
|
67
|
+
},
|
|
68
|
+
defaultVariants: {
|
|
69
|
+
presentation: 'inline',
|
|
42
70
|
},
|
|
43
71
|
});
|
|
44
72
|
|
|
@@ -54,7 +82,7 @@ export interface InlineSelectItemProps {
|
|
|
54
82
|
label: string;
|
|
55
83
|
}
|
|
56
84
|
|
|
57
|
-
/** Option row.
|
|
85
|
+
/** Option row. */
|
|
58
86
|
function InlineSelectItem({ value, label }: InlineSelectItemProps) {
|
|
59
87
|
const context = useContext(InlineSelectContext);
|
|
60
88
|
if (!context) {
|
|
@@ -85,29 +113,55 @@ function InlineSelectItem({ value, label }: InlineSelectItemProps) {
|
|
|
85
113
|
);
|
|
86
114
|
}
|
|
87
115
|
|
|
116
|
+
/** Trigger frame in window coordinates, measured when the list opens. */
|
|
117
|
+
interface Anchor {
|
|
118
|
+
x: number;
|
|
119
|
+
y: number;
|
|
120
|
+
width: number;
|
|
121
|
+
height: number;
|
|
122
|
+
}
|
|
123
|
+
|
|
88
124
|
export interface InlineSelectProps {
|
|
89
125
|
className?: string;
|
|
90
126
|
value?: string;
|
|
91
127
|
onValueChange: (value: string) => void;
|
|
92
128
|
placeholder?: string;
|
|
93
129
|
disabled?: boolean;
|
|
130
|
+
/**
|
|
131
|
+
* Float the list above the page instead of expanding it in layout flow, so
|
|
132
|
+
* opening the select does not push the content below it down.
|
|
133
|
+
*/
|
|
134
|
+
overlay?: boolean;
|
|
135
|
+
/**
|
|
136
|
+
* Width of the floating list. `trigger` matches the trigger, `content` sizes
|
|
137
|
+
* to the longest option, or pass a pixel value. Ignored without `overlay`.
|
|
138
|
+
*/
|
|
139
|
+
overlayWidth?: 'trigger' | 'content' | number;
|
|
140
|
+
/** Gap between the trigger and the floating list. Ignored without `overlay`. */
|
|
141
|
+
overlayOffset?: number;
|
|
142
|
+
/** Called when the list opens or closes. */
|
|
143
|
+
onOpenChange?: (open: boolean) => void;
|
|
94
144
|
children: ReactNode;
|
|
95
145
|
}
|
|
96
146
|
|
|
97
|
-
/**
|
|
98
|
-
* A select that expands its options inline instead of opening a bottom sheet.
|
|
99
|
-
* Useful inside forms and scroll views where an overlay would be disruptive.
|
|
100
|
-
*/
|
|
101
147
|
function InlineSelectRoot({
|
|
102
148
|
className,
|
|
103
149
|
value,
|
|
104
150
|
onValueChange,
|
|
105
151
|
placeholder = 'Select an option',
|
|
106
152
|
disabled,
|
|
153
|
+
overlay = false,
|
|
154
|
+
overlayWidth = 'trigger',
|
|
155
|
+
overlayOffset = 8,
|
|
156
|
+
onOpenChange,
|
|
107
157
|
children,
|
|
108
158
|
}: InlineSelectProps) {
|
|
109
159
|
const [open, setOpen] = useState(false);
|
|
160
|
+
const [anchor, setAnchor] = useState<Anchor | null>(null);
|
|
161
|
+
const [listHeight, setListHeight] = useState(0);
|
|
162
|
+
const triggerRef = useRef<View>(null);
|
|
110
163
|
const chevron = useSharedValue(0);
|
|
164
|
+
const { height: screenHeight } = useWindowDimensions();
|
|
111
165
|
|
|
112
166
|
const selectedLabel = useMemo(() => {
|
|
113
167
|
let label: string | undefined;
|
|
@@ -119,63 +173,156 @@ function InlineSelectRoot({
|
|
|
119
173
|
return label;
|
|
120
174
|
}, [children, value]);
|
|
121
175
|
|
|
176
|
+
const close = useCallback(() => {
|
|
177
|
+
chevron.value = withTiming(0, { duration: 160 });
|
|
178
|
+
setOpen(false);
|
|
179
|
+
onOpenChange?.(false);
|
|
180
|
+
}, [chevron, onOpenChange]);
|
|
181
|
+
|
|
122
182
|
const toggle = useCallback(() => {
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
return
|
|
183
|
+
if (open) {
|
|
184
|
+
close();
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const show = () => {
|
|
189
|
+
chevron.value = withTiming(1, { duration: 160 });
|
|
190
|
+
setOpen(true);
|
|
191
|
+
onOpenChange?.(true);
|
|
192
|
+
};
|
|
193
|
+
|
|
194
|
+
if (!overlay) {
|
|
195
|
+
show();
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// The list is positioned in window coordinates, so it has to know where
|
|
200
|
+
// the trigger actually landed — not where layout said it would.
|
|
201
|
+
triggerRef.current?.measureInWindow((x, y, width, height) => {
|
|
202
|
+
setAnchor({ x, y, width, height });
|
|
203
|
+
show();
|
|
126
204
|
});
|
|
127
|
-
}, [chevron]);
|
|
205
|
+
}, [chevron, close, onOpenChange, open, overlay]);
|
|
128
206
|
|
|
129
207
|
const context = useMemo<InlineSelectContextValue>(
|
|
130
208
|
() => ({
|
|
131
209
|
value,
|
|
132
210
|
onSelect: (next) => {
|
|
133
211
|
onValueChange(next);
|
|
134
|
-
|
|
135
|
-
setOpen(false);
|
|
212
|
+
close();
|
|
136
213
|
},
|
|
137
214
|
}),
|
|
138
|
-
[value, onValueChange,
|
|
215
|
+
[value, onValueChange, close]
|
|
139
216
|
);
|
|
140
217
|
|
|
141
|
-
const slots = inlineSelectVariants({
|
|
218
|
+
const slots = inlineSelectVariants({
|
|
219
|
+
disabled: !!disabled,
|
|
220
|
+
presentation: overlay ? 'overlay' : 'inline',
|
|
221
|
+
});
|
|
142
222
|
const chevronColor = useCSSVariable('--color-muted-foreground');
|
|
143
223
|
|
|
144
224
|
const chevronStyle = useAnimatedStyle(() => ({
|
|
145
225
|
transform: [{ rotate: `${chevron.value * 180}deg` }],
|
|
146
226
|
}));
|
|
147
227
|
|
|
228
|
+
const trigger = (
|
|
229
|
+
<Pressable
|
|
230
|
+
ref={triggerRef}
|
|
231
|
+
accessibilityRole="button"
|
|
232
|
+
accessibilityState={{ disabled: !!disabled, expanded: open }}
|
|
233
|
+
disabled={disabled}
|
|
234
|
+
onPress={toggle}
|
|
235
|
+
className={slots.trigger()}
|
|
236
|
+
>
|
|
237
|
+
{selectedLabel ? (
|
|
238
|
+
<Text className={slots.triggerLabel()}>{selectedLabel}</Text>
|
|
239
|
+
) : (
|
|
240
|
+
<Text className={slots.placeholder()}>{placeholder}</Text>
|
|
241
|
+
)}
|
|
242
|
+
<Animated.View style={chevronStyle}>
|
|
243
|
+
<ChevronDownIcon
|
|
244
|
+
color={typeof chevronColor === 'string' ? chevronColor : '#737373'}
|
|
245
|
+
/>
|
|
246
|
+
</Animated.View>
|
|
247
|
+
</Pressable>
|
|
248
|
+
);
|
|
249
|
+
|
|
250
|
+
if (!overlay) {
|
|
251
|
+
return (
|
|
252
|
+
<InlineSelectContext.Provider value={context}>
|
|
253
|
+
<View className={className}>
|
|
254
|
+
{trigger}
|
|
255
|
+
{open ? (
|
|
256
|
+
<Animated.View
|
|
257
|
+
entering={FadeIn.duration(140)}
|
|
258
|
+
exiting={FadeOut.duration(120)}
|
|
259
|
+
className={slots.list()}
|
|
260
|
+
>
|
|
261
|
+
{children}
|
|
262
|
+
</Animated.View>
|
|
263
|
+
) : null}
|
|
264
|
+
</View>
|
|
265
|
+
</InlineSelectContext.Provider>
|
|
266
|
+
);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// Flip above the trigger when the list would run off the bottom. listHeight
|
|
270
|
+
// is 0 on the first frame, so the list opens downwards and corrects itself
|
|
271
|
+
// once measured — which is invisible inside the 140ms fade.
|
|
272
|
+
const spaceBelow = anchor
|
|
273
|
+
? screenHeight - (anchor.y + anchor.height) - overlayOffset
|
|
274
|
+
: 0;
|
|
275
|
+
const flip = !!anchor && listHeight > 0 && listHeight > spaceBelow;
|
|
276
|
+
|
|
277
|
+
const overlayPosition = anchor
|
|
278
|
+
? {
|
|
279
|
+
position: 'absolute' as const,
|
|
280
|
+
left: anchor.x,
|
|
281
|
+
...(flip
|
|
282
|
+
? { bottom: screenHeight - anchor.y + overlayOffset }
|
|
283
|
+
: { top: anchor.y + anchor.height + overlayOffset }),
|
|
284
|
+
...(overlayWidth === 'trigger'
|
|
285
|
+
? { width: anchor.width }
|
|
286
|
+
: typeof overlayWidth === 'number'
|
|
287
|
+
? { width: overlayWidth }
|
|
288
|
+
: { minWidth: anchor.width }),
|
|
289
|
+
// Never collapse to nothing in a cramped viewport — the list scrolls.
|
|
290
|
+
maxHeight: Math.max((flip ? anchor.y : spaceBelow) - overlayOffset, 160),
|
|
291
|
+
}
|
|
292
|
+
: null;
|
|
293
|
+
|
|
294
|
+
const onListLayout = (event: LayoutChangeEvent) => {
|
|
295
|
+
setListHeight(event.nativeEvent.layout.height);
|
|
296
|
+
};
|
|
297
|
+
|
|
148
298
|
return (
|
|
149
299
|
<InlineSelectContext.Provider value={context}>
|
|
150
|
-
<View className={className}>
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
</Animated.View>
|
|
177
|
-
) : null}
|
|
178
|
-
</View>
|
|
300
|
+
<View className={className}>{trigger}</View>
|
|
301
|
+
|
|
302
|
+
{open && overlayPosition ? (
|
|
303
|
+
<Portal>
|
|
304
|
+
{/* Full-screen catcher so a press anywhere else dismisses the list. */}
|
|
305
|
+
<Pressable
|
|
306
|
+
accessibilityRole="button"
|
|
307
|
+
accessibilityLabel="Close"
|
|
308
|
+
onPress={close}
|
|
309
|
+
style={{ position: 'absolute', top: 0, left: 0, right: 0, bottom: 0 }}
|
|
310
|
+
/>
|
|
311
|
+
<InlineSelectContext.Provider value={context}>
|
|
312
|
+
<Animated.View
|
|
313
|
+
entering={FadeIn.duration(140)}
|
|
314
|
+
exiting={FadeOut.duration(120)}
|
|
315
|
+
onLayout={onListLayout}
|
|
316
|
+
style={overlayPosition}
|
|
317
|
+
className={slots.list()}
|
|
318
|
+
>
|
|
319
|
+
<ScrollView bounces={false} showsVerticalScrollIndicator={false}>
|
|
320
|
+
{children}
|
|
321
|
+
</ScrollView>
|
|
322
|
+
</Animated.View>
|
|
323
|
+
</InlineSelectContext.Provider>
|
|
324
|
+
</Portal>
|
|
325
|
+
) : null}
|
|
179
326
|
</InlineSelectContext.Provider>
|
|
180
327
|
);
|
|
181
328
|
}
|
|
@@ -2,6 +2,7 @@ import { forwardRef, useCallback, useState } from 'react';
|
|
|
2
2
|
import { TextInput, View, type TextInputProps } from 'react-native';
|
|
3
3
|
import { tv } from 'tailwind-variants';
|
|
4
4
|
import { useCSSVariable } from 'uniwind';
|
|
5
|
+
import { KeyboardAvoider } from '../../primitives/keyboard-avoider';
|
|
5
6
|
import { Text } from '../../primitives/text';
|
|
6
7
|
|
|
7
8
|
const inputVariants = tv({
|
|
@@ -34,6 +35,19 @@ export interface InputProps extends TextInputProps {
|
|
|
34
35
|
/** Error message. When set, the field renders in its invalid state. */
|
|
35
36
|
errorMessage?: string;
|
|
36
37
|
disabled?: boolean;
|
|
38
|
+
/**
|
|
39
|
+
* Lift the field above the software keyboard when it would otherwise be
|
|
40
|
+
* covered. Moves by exactly the overlap, and not at all when the field is
|
|
41
|
+
* already clear.
|
|
42
|
+
*
|
|
43
|
+
* Install `react-native-keyboard-controller` for this to behave on Android.
|
|
44
|
+
*
|
|
45
|
+
* Do not toggle this at runtime — it changes which component renders the
|
|
46
|
+
* container, which would remount the field and drop focus.
|
|
47
|
+
*/
|
|
48
|
+
avoidKeyboard?: boolean;
|
|
49
|
+
/** Gap kept between the field and the keyboard when `avoidKeyboard` is set. */
|
|
50
|
+
keyboardOffset?: number;
|
|
37
51
|
}
|
|
38
52
|
|
|
39
53
|
export const Input = forwardRef<TextInput, InputProps>(
|
|
@@ -45,6 +59,8 @@ export const Input = forwardRef<TextInput, InputProps>(
|
|
|
45
59
|
description,
|
|
46
60
|
errorMessage,
|
|
47
61
|
disabled,
|
|
62
|
+
avoidKeyboard = false,
|
|
63
|
+
keyboardOffset = 16,
|
|
48
64
|
onFocus,
|
|
49
65
|
onBlur,
|
|
50
66
|
...props
|
|
@@ -75,8 +91,8 @@ export const Input = forwardRef<TextInput, InputProps>(
|
|
|
75
91
|
[onBlur]
|
|
76
92
|
);
|
|
77
93
|
|
|
78
|
-
|
|
79
|
-
|
|
94
|
+
const body = (
|
|
95
|
+
<>
|
|
80
96
|
{label ? <Text className={slots.label()}>{label}</Text> : null}
|
|
81
97
|
<TextInput
|
|
82
98
|
ref={ref}
|
|
@@ -96,8 +112,28 @@ export const Input = forwardRef<TextInput, InputProps>(
|
|
|
96
112
|
) : description ? (
|
|
97
113
|
<Text className={slots.description()}>{description}</Text>
|
|
98
114
|
) : null}
|
|
99
|
-
|
|
115
|
+
</>
|
|
100
116
|
);
|
|
117
|
+
|
|
118
|
+
const containerClasses = slots.container({ className: containerClassName });
|
|
119
|
+
|
|
120
|
+
/*
|
|
121
|
+
* The keyboard hook is deliberately behind a component boundary rather
|
|
122
|
+
* than an `enabled` flag. Calling it at all has global consequences —
|
|
123
|
+
* without the keyboard controller installed it falls back to Reanimated's
|
|
124
|
+
* useAnimatedKeyboard, which switches Android out of adjustResize for the
|
|
125
|
+
* whole app. A field that never asked to avoid the keyboard must not do
|
|
126
|
+
* that to every other screen.
|
|
127
|
+
*/
|
|
128
|
+
if (avoidKeyboard) {
|
|
129
|
+
return (
|
|
130
|
+
<KeyboardAvoider offset={keyboardOffset} className={containerClasses}>
|
|
131
|
+
{body}
|
|
132
|
+
</KeyboardAvoider>
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return <View className={containerClasses}>{body}</View>;
|
|
101
137
|
}
|
|
102
138
|
);
|
|
103
139
|
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* InputGroup — an Input with leading and/or trailing decorators.
|
|
3
3
|
*
|
|
4
|
-
* Adapted from: heroui-inc/heroui-native src/components/input-group/
|
|
5
4
|
* Prefix and Suffix are absolutely positioned and their measured widths become
|
|
6
5
|
* padding on the Input, so text never runs underneath a decorator regardless
|
|
7
6
|
* of how wide that decorator turns out to be.
|