@pickle-ui/react-native 1.0.0-beta.1 → 1.0.0-beta.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +62 -24
- package/dist/components/Badge.js +1 -1
- package/dist/components/Badge.js.map +1 -1
- package/dist/components/Button.d.ts.map +1 -1
- package/dist/components/Button.js +27 -23
- package/dist/components/Button.js.map +1 -1
- package/dist/components/Card.d.ts.map +1 -1
- package/dist/components/Card.js +7 -4
- package/dist/components/Card.js.map +1 -1
- package/dist/components/Flex.d.ts +25 -0
- package/dist/components/Flex.d.ts.map +1 -0
- package/dist/components/Flex.js +24 -0
- package/dist/components/Flex.js.map +1 -0
- package/dist/components/Grid.d.ts +36 -0
- package/dist/components/Grid.d.ts.map +1 -0
- package/dist/components/Grid.js +68 -0
- package/dist/components/Grid.js.map +1 -0
- package/dist/components/Separator.d.ts +11 -0
- package/dist/components/Separator.d.ts.map +1 -0
- package/dist/components/Separator.js +12 -0
- package/dist/components/Separator.js.map +1 -0
- package/dist/components/Spinner.d.ts +14 -0
- package/dist/components/Spinner.d.ts.map +1 -0
- package/dist/components/Spinner.js +41 -0
- package/dist/components/Spinner.js.map +1 -0
- package/dist/components/Text.d.ts.map +1 -1
- package/dist/components/Text.js +5 -4
- package/dist/components/Text.js.map +1 -1
- package/dist/components/XStack.d.ts +4 -0
- package/dist/components/XStack.d.ts.map +1 -0
- package/dist/components/XStack.js +6 -0
- package/dist/components/XStack.js.map +1 -0
- package/dist/components/YStack.d.ts +4 -0
- package/dist/components/YStack.d.ts.map +1 -0
- package/dist/components/YStack.js +6 -0
- package/dist/components/YStack.js.map +1 -0
- package/dist/components/_internal/InputFrame.d.ts +13 -0
- package/dist/components/_internal/InputFrame.d.ts.map +1 -0
- package/dist/components/_internal/InputFrame.js +14 -0
- package/dist/components/_internal/InputFrame.js.map +1 -0
- package/dist/components/_internal/StoryItem.d.ts +6 -0
- package/dist/components/_internal/StoryItem.d.ts.map +1 -0
- package/dist/components/_internal/StoryItem.js +9 -0
- package/dist/components/_internal/StoryItem.js.map +1 -0
- package/dist/components/_internal/layout.d.ts +79 -0
- package/dist/components/_internal/layout.d.ts.map +1 -0
- package/dist/components/_internal/layout.js +73 -0
- package/dist/components/_internal/layout.js.map +1 -0
- package/dist/components/_internal/useControllableState.d.ts +7 -0
- package/dist/components/_internal/useControllableState.d.ts.map +1 -0
- package/dist/components/_internal/useControllableState.js +17 -0
- package/dist/components/_internal/useControllableState.js.map +1 -0
- package/dist/components/form/Checkbox.d.ts +26 -0
- package/dist/components/form/Checkbox.d.ts.map +1 -0
- package/dist/components/form/Checkbox.js +43 -0
- package/dist/components/form/Checkbox.js.map +1 -0
- package/dist/components/form/Form.context.d.ts +34 -0
- package/dist/components/form/Form.context.d.ts.map +1 -0
- package/dist/components/form/Form.context.js +53 -0
- package/dist/components/form/Form.context.js.map +1 -0
- package/dist/components/form/Form.d.ts +33 -0
- package/dist/components/form/Form.d.ts.map +1 -0
- package/dist/components/form/Form.js +36 -0
- package/dist/components/form/Form.js.map +1 -0
- package/dist/components/form/FormCheckbox.d.ts +7 -0
- package/dist/components/form/FormCheckbox.d.ts.map +1 -0
- package/dist/components/form/FormCheckbox.js +24 -0
- package/dist/components/form/FormCheckbox.js.map +1 -0
- package/dist/components/form/FormDescription.d.ts +4 -0
- package/dist/components/form/FormDescription.d.ts.map +1 -0
- package/dist/components/form/FormDescription.js +7 -0
- package/dist/components/form/FormDescription.js.map +1 -0
- package/dist/components/form/FormError.d.ts +5 -0
- package/dist/components/form/FormError.d.ts.map +1 -0
- package/dist/components/form/FormError.js +8 -0
- package/dist/components/form/FormError.js.map +1 -0
- package/dist/components/form/FormField.d.ts +17 -0
- package/dist/components/form/FormField.d.ts.map +1 -0
- package/dist/components/form/FormField.js +36 -0
- package/dist/components/form/FormField.js.map +1 -0
- package/dist/components/form/FormInput.d.ts +10 -0
- package/dist/components/form/FormInput.d.ts.map +1 -0
- package/dist/components/form/FormInput.js +36 -0
- package/dist/components/form/FormInput.js.map +1 -0
- package/dist/components/form/FormInputGroup.d.ts +8 -0
- package/dist/components/form/FormInputGroup.d.ts.map +1 -0
- package/dist/components/form/FormInputGroup.js +18 -0
- package/dist/components/form/FormInputGroup.js.map +1 -0
- package/dist/components/form/FormLabel.d.ts +4 -0
- package/dist/components/form/FormLabel.d.ts.map +1 -0
- package/dist/components/form/FormLabel.js +7 -0
- package/dist/components/form/FormLabel.js.map +1 -0
- package/dist/components/form/FormRadioGroup.d.ts +11 -0
- package/dist/components/form/FormRadioGroup.d.ts.map +1 -0
- package/dist/components/form/FormRadioGroup.js +24 -0
- package/dist/components/form/FormRadioGroup.js.map +1 -0
- package/dist/components/form/FormRoot.d.ts +13 -0
- package/dist/components/form/FormRoot.d.ts.map +1 -0
- package/dist/components/form/FormRoot.js +19 -0
- package/dist/components/form/FormRoot.js.map +1 -0
- package/dist/components/form/FormTextarea.d.ts +7 -0
- package/dist/components/form/FormTextarea.d.ts.map +1 -0
- package/dist/components/form/FormTextarea.js +27 -0
- package/dist/components/form/FormTextarea.js.map +1 -0
- package/dist/components/form/Input.d.ts +17 -0
- package/dist/components/form/Input.d.ts.map +1 -0
- package/dist/components/form/Input.js +32 -0
- package/dist/components/form/Input.js.map +1 -0
- package/dist/components/form/InputGroup.context.d.ts +10 -0
- package/dist/components/form/InputGroup.context.d.ts.map +1 -0
- package/dist/components/form/InputGroup.context.js +8 -0
- package/dist/components/form/InputGroup.context.js.map +1 -0
- package/dist/components/form/InputGroup.d.ts +31 -0
- package/dist/components/form/InputGroup.d.ts.map +1 -0
- package/dist/components/form/InputGroup.js +123 -0
- package/dist/components/form/InputGroup.js.map +1 -0
- package/dist/components/form/RadioGroup.d.ts +29 -0
- package/dist/components/form/RadioGroup.d.ts.map +1 -0
- package/dist/components/form/RadioGroup.js +42 -0
- package/dist/components/form/RadioGroup.js.map +1 -0
- package/dist/components/form/Textarea.d.ts +19 -0
- package/dist/components/form/Textarea.d.ts.map +1 -0
- package/dist/components/form/Textarea.js +26 -0
- package/dist/components/form/Textarea.js.map +1 -0
- package/dist/entries/checkbox.d.ts +3 -0
- package/dist/entries/checkbox.d.ts.map +1 -0
- package/dist/entries/checkbox.js +2 -0
- package/dist/entries/checkbox.js.map +1 -0
- package/dist/entries/flex.d.ts +3 -0
- package/dist/entries/flex.d.ts.map +1 -0
- package/dist/entries/flex.js +2 -0
- package/dist/entries/flex.js.map +1 -0
- package/dist/entries/form.d.ts +3 -0
- package/dist/entries/form.d.ts.map +1 -0
- package/dist/entries/form.js +2 -0
- package/dist/entries/form.js.map +1 -0
- package/dist/entries/grid.d.ts +3 -0
- package/dist/entries/grid.d.ts.map +1 -0
- package/dist/entries/grid.js +2 -0
- package/dist/entries/grid.js.map +1 -0
- package/dist/entries/input-group.d.ts +3 -0
- package/dist/entries/input-group.d.ts.map +1 -0
- package/dist/entries/input-group.js +2 -0
- package/dist/entries/input-group.js.map +1 -0
- package/dist/entries/input.d.ts +3 -0
- package/dist/entries/input.d.ts.map +1 -0
- package/dist/entries/input.js +2 -0
- package/dist/entries/input.js.map +1 -0
- package/dist/entries/radio-group.d.ts +3 -0
- package/dist/entries/radio-group.d.ts.map +1 -0
- package/dist/entries/radio-group.js +2 -0
- package/dist/entries/radio-group.js.map +1 -0
- package/dist/entries/separator.d.ts +3 -0
- package/dist/entries/separator.d.ts.map +1 -0
- package/dist/entries/separator.js +2 -0
- package/dist/entries/separator.js.map +1 -0
- package/dist/entries/spinner.d.ts +3 -0
- package/dist/entries/spinner.d.ts.map +1 -0
- package/dist/entries/spinner.js +2 -0
- package/dist/entries/spinner.js.map +1 -0
- package/dist/entries/textarea.d.ts +3 -0
- package/dist/entries/textarea.d.ts.map +1 -0
- package/dist/entries/textarea.js +2 -0
- package/dist/entries/textarea.js.map +1 -0
- package/dist/entries/x-stack.d.ts +3 -0
- package/dist/entries/x-stack.d.ts.map +1 -0
- package/dist/entries/x-stack.js +2 -0
- package/dist/entries/x-stack.js.map +1 -0
- package/dist/entries/y-stack.d.ts +3 -0
- package/dist/entries/y-stack.d.ts.map +1 -0
- package/dist/entries/y-stack.js +2 -0
- package/dist/entries/y-stack.js.map +1 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +12 -0
- package/dist/index.js.map +1 -1
- package/package.json +8 -3
- package/src/base.css +23 -5
- package/src/components/Badge.tsx +1 -1
- package/src/components/Button.tsx +27 -23
- package/src/components/Card.tsx +12 -8
- package/src/components/Flex.stories.tsx +58 -0
- package/src/components/Flex.test.tsx +57 -0
- package/src/components/Flex.tsx +52 -0
- package/src/components/Grid.stories.tsx +55 -0
- package/src/components/Grid.test.tsx +140 -0
- package/src/components/Grid.tsx +166 -0
- package/src/components/Separator.stories.tsx +39 -0
- package/src/components/Separator.test.tsx +25 -0
- package/src/components/Separator.tsx +30 -0
- package/src/components/Spinner.stories.tsx +35 -0
- package/src/components/Spinner.test.tsx +30 -0
- package/src/components/Spinner.tsx +75 -0
- package/src/components/Text.stories.tsx +49 -0
- package/src/components/Text.tsx +5 -4
- package/src/components/XStack.stories.tsx +46 -0
- package/src/components/XStack.test.tsx +13 -0
- package/src/components/XStack.tsx +7 -0
- package/src/components/YStack.stories.tsx +46 -0
- package/src/components/YStack.test.tsx +13 -0
- package/src/components/YStack.tsx +7 -0
- package/src/components/_internal/InputFrame.tsx +50 -0
- package/src/components/_internal/StoryItem.tsx +24 -0
- package/src/components/_internal/layout.ts +103 -0
- package/src/components/_internal/useControllableState.ts +25 -0
- package/src/components/form/Checkbox.stories.tsx +67 -0
- package/src/components/form/Checkbox.test.tsx +88 -0
- package/src/components/form/Checkbox.tsx +167 -0
- package/src/components/form/Form.context.ts +91 -0
- package/src/components/form/Form.stories.tsx +152 -0
- package/src/components/form/Form.test.tsx +195 -0
- package/src/components/form/Form.tsx +51 -0
- package/src/components/form/FormCheckbox.tsx +42 -0
- package/src/components/form/FormDescription.tsx +14 -0
- package/src/components/form/FormError.tsx +20 -0
- package/src/components/form/FormField.tsx +82 -0
- package/src/components/form/FormInput.tsx +70 -0
- package/src/components/form/FormInputGroup.tsx +30 -0
- package/src/components/form/FormLabel.tsx +17 -0
- package/src/components/form/FormRadioGroup.tsx +48 -0
- package/src/components/form/FormRoot.tsx +41 -0
- package/src/components/form/FormTextarea.tsx +56 -0
- package/src/components/form/Input.stories.tsx +105 -0
- package/src/components/form/Input.test.tsx +76 -0
- package/src/components/form/Input.tsx +81 -0
- package/src/components/form/InputGroup.context.ts +11 -0
- package/src/components/form/InputGroup.stories.tsx +129 -0
- package/src/components/form/InputGroup.test.tsx +95 -0
- package/src/components/form/InputGroup.tsx +266 -0
- package/src/components/form/RadioGroup.stories.tsx +62 -0
- package/src/components/form/RadioGroup.test.tsx +98 -0
- package/src/components/form/RadioGroup.tsx +151 -0
- package/src/components/form/Textarea.stories.tsx +61 -0
- package/src/components/form/Textarea.test.tsx +35 -0
- package/src/components/form/Textarea.tsx +74 -0
- package/src/entries/checkbox.ts +2 -0
- package/src/entries/flex.ts +2 -0
- package/src/entries/form.ts +13 -0
- package/src/entries/grid.ts +2 -0
- package/src/entries/input-group.ts +8 -0
- package/src/entries/input.ts +2 -0
- package/src/entries/radio-group.ts +5 -0
- package/src/entries/separator.ts +2 -0
- package/src/entries/spinner.ts +2 -0
- package/src/entries/textarea.ts +2 -0
- package/src/entries/x-stack.ts +2 -0
- package/src/entries/y-stack.ts +2 -0
- package/src/fonts.test.tsx +244 -0
- package/src/index.ts +12 -0
- package/src/styles.css +12 -0
- package/dist/components/_internal/Spinner.d.ts +0 -9
- package/dist/components/_internal/Spinner.d.ts.map +0 -1
- package/dist/components/_internal/Spinner.js +0 -31
- package/dist/components/_internal/Spinner.js.map +0 -1
- package/src/components/_internal/Spinner.tsx +0 -49
package/src/base.css
CHANGED
|
@@ -1,11 +1,29 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Pickle's components as a content source, so Uniwind generates the classes
|
|
3
|
-
* they use,
|
|
4
|
-
* `@pickle-ui/react
|
|
5
|
-
* `uniwind`, to bring your own token set instead
|
|
6
|
-
* `@pickle-ui/tokens/native.css`.
|
|
7
|
-
*
|
|
3
|
+
* they use, the type scale behind `Text`'s variants, shared with
|
|
4
|
+
* `@pickle-ui/react`, and the native font defaults. `styles.css` imports this
|
|
5
|
+
* file; import it alone, after `uniwind`, to bring your own token set instead
|
|
6
|
+
* of `@pickle-ui/tokens/native.css`. It only declares theme variables, so it
|
|
7
|
+
* compiles in any order.
|
|
8
8
|
*/
|
|
9
9
|
@import '@pickle-ui/tokens/typography.css';
|
|
10
10
|
|
|
11
11
|
@source './components/**/*.{ts,tsx}';
|
|
12
|
+
|
|
13
|
+
/*
|
|
14
|
+
* The components' text uses `font-sans`, and their headings `font-heading`.
|
|
15
|
+
* Until the app declares `--font-sans`, it reads a variable nothing declares,
|
|
16
|
+
* so it sets no family and text keeps the system font: an explicit system
|
|
17
|
+
* family would also drop the medium and semibold weights on Android.
|
|
18
|
+
* `default` lets any `--font-*` an app or its token set declares win, and
|
|
19
|
+
* replaces Tailwind's web font stacks, which native can't resolve.
|
|
20
|
+
*
|
|
21
|
+
* This default is Pickle's, not Uniwind's. Uniwind suggests per-platform
|
|
22
|
+
* values instead, but `@variant ios` doesn't compile inside `@theme`, and
|
|
23
|
+
* inside `@layer theme` it overrides the app's own `--font-sans`.
|
|
24
|
+
*/
|
|
25
|
+
@theme default {
|
|
26
|
+
--font-sans: var(--pickle-no-font);
|
|
27
|
+
--font-heading: var(--font-sans);
|
|
28
|
+
--font-mono: var(--pickle-no-font);
|
|
29
|
+
}
|
package/src/components/Badge.tsx
CHANGED
|
@@ -3,11 +3,12 @@ import { Children, isValidElement } from 'react';
|
|
|
3
3
|
import { type Insets, Pressable, Text, View } from 'react-native';
|
|
4
4
|
|
|
5
5
|
import { cn } from '../lib/cn';
|
|
6
|
-
import { Spinner } from './
|
|
6
|
+
import { Spinner } from './Spinner';
|
|
7
7
|
|
|
8
8
|
// Same variants, sizes and dimensions as `Button` in @pickle-ui/react on a
|
|
9
|
-
// phone
|
|
10
|
-
//
|
|
9
|
+
// phone, with its text sizes from `sm` up. `hitSlop` stretches every size to a
|
|
10
|
+
// 44pt touch target, like the web's `pointer-coarse` hit area. Paddings leave
|
|
11
|
+
// room for the 1pt border.
|
|
11
12
|
const buttonVariants = cva(
|
|
12
13
|
'flex-row items-center justify-center gap-2 rounded-lg border disabled:opacity-64',
|
|
13
14
|
{
|
|
@@ -42,24 +43,26 @@ const buttonVariants = cva(
|
|
|
42
43
|
},
|
|
43
44
|
);
|
|
44
45
|
|
|
45
|
-
const labelVariants = cva('font-medium', {
|
|
46
|
+
const labelVariants = cva('font-sans font-medium', {
|
|
46
47
|
compoundVariants: [
|
|
47
48
|
{ className: 'underline', pressed: true, variant: 'link' },
|
|
48
49
|
],
|
|
49
50
|
defaultVariants: { size: 'md', variant: 'default' },
|
|
50
51
|
variants: {
|
|
51
52
|
pressed: { false: '', true: '' },
|
|
53
|
+
// The web's sizes from `sm` up, not its larger phone sizes, which only
|
|
54
|
+
// stop iOS Safari zooming into focused fields.
|
|
52
55
|
size: {
|
|
53
|
-
icon: 'text-
|
|
54
|
-
'icon-lg': 'text-
|
|
55
|
-
'icon-sm': 'text-
|
|
56
|
-
'icon-xl': 'text-
|
|
57
|
-
'icon-xs': 'text-
|
|
58
|
-
lg: 'text-
|
|
59
|
-
md: 'text-
|
|
60
|
-
sm: 'text-
|
|
61
|
-
xl: 'text-
|
|
62
|
-
xs: 'text-
|
|
56
|
+
icon: 'text-sm',
|
|
57
|
+
'icon-lg': 'text-sm',
|
|
58
|
+
'icon-sm': 'text-sm',
|
|
59
|
+
'icon-xl': 'text-sm',
|
|
60
|
+
'icon-xs': 'text-sm',
|
|
61
|
+
lg: 'text-sm',
|
|
62
|
+
md: 'text-sm',
|
|
63
|
+
sm: 'text-sm',
|
|
64
|
+
xl: 'text-base',
|
|
65
|
+
xs: 'text-xs',
|
|
63
66
|
},
|
|
64
67
|
variant: {
|
|
65
68
|
default: 'text-primary-foreground',
|
|
@@ -73,16 +76,17 @@ const labelVariants = cva('font-medium', {
|
|
|
73
76
|
},
|
|
74
77
|
});
|
|
75
78
|
|
|
76
|
-
|
|
79
|
+
// Sized and dimmed like the web button's icons, in the label's color.
|
|
80
|
+
const spinnerVariants = cva('size-4 opacity-80', {
|
|
77
81
|
variants: {
|
|
78
82
|
variant: {
|
|
79
|
-
default: '
|
|
80
|
-
destructive: '
|
|
81
|
-
'destructive-outline': '
|
|
82
|
-
ghost: '
|
|
83
|
-
link: '
|
|
84
|
-
outline: '
|
|
85
|
-
secondary: '
|
|
83
|
+
default: 'text-primary-foreground',
|
|
84
|
+
destructive: 'text-destructive-foreground',
|
|
85
|
+
'destructive-outline': 'text-foreground',
|
|
86
|
+
ghost: 'text-foreground',
|
|
87
|
+
link: 'text-foreground',
|
|
88
|
+
outline: 'text-foreground',
|
|
89
|
+
secondary: 'text-secondary-foreground',
|
|
86
90
|
},
|
|
87
91
|
},
|
|
88
92
|
});
|
|
@@ -198,7 +202,7 @@ export function Button({
|
|
|
198
202
|
)}
|
|
199
203
|
{loading ? (
|
|
200
204
|
<View className="absolute">
|
|
201
|
-
<Spinner className={spinnerVariants({ variant })} />
|
|
205
|
+
<Spinner aria-hidden className={spinnerVariants({ variant })} />
|
|
202
206
|
</View>
|
|
203
207
|
) : null}
|
|
204
208
|
</>
|
package/src/components/Card.tsx
CHANGED
|
@@ -45,10 +45,13 @@ function CardFooter({ className, ...props }: CardFooterProps) {
|
|
|
45
45
|
* like the web header's grid.
|
|
46
46
|
*/
|
|
47
47
|
function CardHeader({ children, className, ...props }: CardHeaderProps) {
|
|
48
|
-
const
|
|
49
|
-
const
|
|
50
|
-
|
|
51
|
-
|
|
48
|
+
const actions: React.ReactNode[] = [];
|
|
49
|
+
const rest: React.ReactNode[] = [];
|
|
50
|
+
for (const child of Children.toArray(children)) {
|
|
51
|
+
(isValidElement(child) && child.type === CardAction ? actions : rest).push(
|
|
52
|
+
child,
|
|
53
|
+
);
|
|
54
|
+
}
|
|
52
55
|
|
|
53
56
|
if (actions.length === 0) {
|
|
54
57
|
return (
|
|
@@ -63,9 +66,7 @@ function CardHeader({ children, className, ...props }: CardHeaderProps) {
|
|
|
63
66
|
className={cn('flex-row items-start gap-4 px-6', className)}
|
|
64
67
|
{...props}
|
|
65
68
|
>
|
|
66
|
-
<View className="flex-1 gap-1.5">
|
|
67
|
-
{parts.filter((child) => !actions.includes(child))}
|
|
68
|
-
</View>
|
|
69
|
+
<View className="flex-1 gap-1.5">{rest}</View>
|
|
69
70
|
{actions}
|
|
70
71
|
</View>
|
|
71
72
|
);
|
|
@@ -86,7 +87,10 @@ function CardRoot({ className, ...props }: CardProps) {
|
|
|
86
87
|
function CardTitle({ className, ...props }: CardTitleProps) {
|
|
87
88
|
return (
|
|
88
89
|
<Text
|
|
89
|
-
className={cn(
|
|
90
|
+
className={cn(
|
|
91
|
+
'text-card-foreground font-heading leading-none',
|
|
92
|
+
className,
|
|
93
|
+
)}
|
|
90
94
|
variant="h3"
|
|
91
95
|
{...props}
|
|
92
96
|
/>
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/react-native-web-vite';
|
|
2
|
+
|
|
3
|
+
import { Story } from './_internal/Story';
|
|
4
|
+
import { StoryItem as Item } from './_internal/StoryItem';
|
|
5
|
+
import { Flex } from './Flex';
|
|
6
|
+
|
|
7
|
+
const meta = {
|
|
8
|
+
component: Flex,
|
|
9
|
+
parameters: { layout: 'fullscreen' },
|
|
10
|
+
title: 'components/Flex',
|
|
11
|
+
} satisfies Meta<typeof Flex>;
|
|
12
|
+
|
|
13
|
+
export default meta;
|
|
14
|
+
type StoryDefinition = StoryObj<typeof meta>;
|
|
15
|
+
|
|
16
|
+
export const All: StoryDefinition = {
|
|
17
|
+
render: () => (
|
|
18
|
+
<Story.Layout className="max-w-6xl" title="Flex">
|
|
19
|
+
<Story.Section title="Direction and spacing">
|
|
20
|
+
<Flex direction="column" gap={3}>
|
|
21
|
+
<Item className="min-h-16">First</Item>
|
|
22
|
+
<Item className="min-h-16">Second</Item>
|
|
23
|
+
<Item className="min-h-16">Third</Item>
|
|
24
|
+
</Flex>
|
|
25
|
+
</Story.Section>
|
|
26
|
+
|
|
27
|
+
<Story.Section title="Alignment and distribution">
|
|
28
|
+
<Flex
|
|
29
|
+
align="center"
|
|
30
|
+
className="border-border min-h-40 rounded-md border border-dashed p-4"
|
|
31
|
+
gap={4}
|
|
32
|
+
justify="between"
|
|
33
|
+
>
|
|
34
|
+
<Item className="min-h-16 min-w-20">Start</Item>
|
|
35
|
+
<Item className="min-h-16 min-w-20">Center</Item>
|
|
36
|
+
<Item className="min-h-16 min-w-20">End</Item>
|
|
37
|
+
</Flex>
|
|
38
|
+
</Story.Section>
|
|
39
|
+
|
|
40
|
+
<Story.Section title="Wrapping">
|
|
41
|
+
<Flex gap={3} wrap="wrap">
|
|
42
|
+
{['One', 'Two', 'Three', 'Four', 'Five', 'Six'].map((label) => (
|
|
43
|
+
<Item className="min-h-16 min-w-24" key={label}>
|
|
44
|
+
{label}
|
|
45
|
+
</Item>
|
|
46
|
+
))}
|
|
47
|
+
</Flex>
|
|
48
|
+
</Story.Section>
|
|
49
|
+
|
|
50
|
+
<Story.Section title="Responsive override">
|
|
51
|
+
<Flex className="flex-col md:flex-row" gap={3}>
|
|
52
|
+
<Item className="min-h-16">Column on small screens</Item>
|
|
53
|
+
<Item className="min-h-16">Row from md</Item>
|
|
54
|
+
</Flex>
|
|
55
|
+
</Story.Section>
|
|
56
|
+
</Story.Layout>
|
|
57
|
+
),
|
|
58
|
+
};
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { render, screen } from '@testing-library/react-native';
|
|
2
|
+
|
|
3
|
+
import { Flex } from './Flex';
|
|
4
|
+
|
|
5
|
+
function classesOf(testID: string) {
|
|
6
|
+
return String(screen.getByTestId(testID).props.className).split(' ');
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
describe('Flex', () => {
|
|
10
|
+
it('lays its children out in a row, like the web', async () => {
|
|
11
|
+
await render(<Flex testID="flex" />);
|
|
12
|
+
|
|
13
|
+
expect(classesOf('flex')).toContain('flex-row');
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it('renders layout props as Tailwind classes', async () => {
|
|
17
|
+
await render(
|
|
18
|
+
<Flex
|
|
19
|
+
align="center"
|
|
20
|
+
direction="column"
|
|
21
|
+
gap={4}
|
|
22
|
+
gapX={2}
|
|
23
|
+
gapY={3}
|
|
24
|
+
justify="between"
|
|
25
|
+
testID="flex"
|
|
26
|
+
wrap="wrap"
|
|
27
|
+
/>,
|
|
28
|
+
);
|
|
29
|
+
|
|
30
|
+
expect(classesOf('flex')).toEqual(
|
|
31
|
+
expect.arrayContaining([
|
|
32
|
+
'flex-col',
|
|
33
|
+
'flex-wrap',
|
|
34
|
+
'items-center',
|
|
35
|
+
'justify-between',
|
|
36
|
+
'gap-4',
|
|
37
|
+
'gap-x-2',
|
|
38
|
+
'gap-y-3',
|
|
39
|
+
]),
|
|
40
|
+
);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('lets className override the props', async () => {
|
|
44
|
+
await render(
|
|
45
|
+
<Flex
|
|
46
|
+
className="custom-class flex-row"
|
|
47
|
+
direction="column"
|
|
48
|
+
testID="flex"
|
|
49
|
+
/>,
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
expect(classesOf('flex')).toEqual(
|
|
53
|
+
expect.arrayContaining(['flex-row', 'custom-class']),
|
|
54
|
+
);
|
|
55
|
+
expect(classesOf('flex')).not.toContain('flex-col');
|
|
56
|
+
});
|
|
57
|
+
});
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { View } from 'react-native';
|
|
2
|
+
|
|
3
|
+
import { cn } from '../lib/cn';
|
|
4
|
+
import { getLayoutClasses, type LayoutStyleProps } from './_internal/layout';
|
|
5
|
+
|
|
6
|
+
const directionClasses = {
|
|
7
|
+
column: 'flex-col',
|
|
8
|
+
'column-reverse': 'flex-col-reverse',
|
|
9
|
+
row: 'flex-row',
|
|
10
|
+
'row-reverse': 'flex-row-reverse',
|
|
11
|
+
} as const;
|
|
12
|
+
|
|
13
|
+
const wrapClasses = {
|
|
14
|
+
nowrap: 'flex-nowrap',
|
|
15
|
+
wrap: 'flex-wrap',
|
|
16
|
+
'wrap-reverse': 'flex-wrap-reverse',
|
|
17
|
+
} as const;
|
|
18
|
+
|
|
19
|
+
export type FlexProps = LayoutStyleProps &
|
|
20
|
+
Omit<React.ComponentProps<typeof View>, keyof LayoutStyleProps> & {
|
|
21
|
+
direction?: keyof typeof directionClasses;
|
|
22
|
+
wrap?: keyof typeof wrapClasses;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* A `View` laid out with flexbox props, like `Flex` in @pickle-ui/react. It
|
|
27
|
+
* defaults to a row, as on the web, where React Native's `View` stacks its
|
|
28
|
+
* children in a column.
|
|
29
|
+
*/
|
|
30
|
+
export function Flex({
|
|
31
|
+
align,
|
|
32
|
+
className,
|
|
33
|
+
direction = 'row',
|
|
34
|
+
gap,
|
|
35
|
+
gapX,
|
|
36
|
+
gapY,
|
|
37
|
+
justify,
|
|
38
|
+
wrap,
|
|
39
|
+
...props
|
|
40
|
+
}: FlexProps) {
|
|
41
|
+
return (
|
|
42
|
+
<View
|
|
43
|
+
className={cn(
|
|
44
|
+
getLayoutClasses({ align, gap, gapX, gapY, justify }),
|
|
45
|
+
directionClasses[direction],
|
|
46
|
+
wrap === undefined ? undefined : wrapClasses[wrap],
|
|
47
|
+
className,
|
|
48
|
+
)}
|
|
49
|
+
{...props}
|
|
50
|
+
/>
|
|
51
|
+
);
|
|
52
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/react-native-web-vite';
|
|
2
|
+
|
|
3
|
+
import { Story } from './_internal/Story';
|
|
4
|
+
import { StoryItem as Item } from './_internal/StoryItem';
|
|
5
|
+
import { Grid } from './Grid';
|
|
6
|
+
|
|
7
|
+
const meta = {
|
|
8
|
+
component: Grid,
|
|
9
|
+
parameters: { layout: 'fullscreen' },
|
|
10
|
+
title: 'components/Grid',
|
|
11
|
+
} satisfies Meta<typeof Grid>;
|
|
12
|
+
|
|
13
|
+
export default meta;
|
|
14
|
+
type StoryDefinition = StoryObj<typeof meta>;
|
|
15
|
+
|
|
16
|
+
export const All: StoryDefinition = {
|
|
17
|
+
render: () => (
|
|
18
|
+
<Story.Layout className="max-w-6xl" title="Grid">
|
|
19
|
+
<Story.Section title="Columns and gaps">
|
|
20
|
+
<Grid columns={3} gap={4}>
|
|
21
|
+
{['One', 'Two', 'Three', 'Four', 'Five', 'Six', 'Seven'].map(
|
|
22
|
+
(label) => (
|
|
23
|
+
<Item className="min-h-20" key={label}>
|
|
24
|
+
{label}
|
|
25
|
+
</Item>
|
|
26
|
+
),
|
|
27
|
+
)}
|
|
28
|
+
</Grid>
|
|
29
|
+
</Story.Section>
|
|
30
|
+
|
|
31
|
+
<Story.Section title="Rows and column flow">
|
|
32
|
+
<Grid flow="column" gapX={4} gapY={2} rows={2}>
|
|
33
|
+
{['A', 'B', 'C', 'D', 'E', 'F'].map((label) => (
|
|
34
|
+
<Item className="min-h-20" key={label}>
|
|
35
|
+
{label}
|
|
36
|
+
</Item>
|
|
37
|
+
))}
|
|
38
|
+
</Grid>
|
|
39
|
+
</Story.Section>
|
|
40
|
+
|
|
41
|
+
<Story.Section title="Placement">
|
|
42
|
+
<Grid
|
|
43
|
+
className="border-border rounded-md border border-dashed p-4"
|
|
44
|
+
columns={3}
|
|
45
|
+
gap={4}
|
|
46
|
+
placeItems="center"
|
|
47
|
+
>
|
|
48
|
+
<Item className="min-h-40">Tall item</Item>
|
|
49
|
+
<Item>Centered</Item>
|
|
50
|
+
<Item className="flex-1">Full height</Item>
|
|
51
|
+
</Grid>
|
|
52
|
+
</Story.Section>
|
|
53
|
+
</Story.Layout>
|
|
54
|
+
),
|
|
55
|
+
};
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import { render, screen, within } from '@testing-library/react-native';
|
|
2
|
+
import { Text } from 'react-native';
|
|
3
|
+
|
|
4
|
+
import { Grid } from './Grid';
|
|
5
|
+
|
|
6
|
+
const LABELS = ['A', 'B', 'C', 'D', 'E'];
|
|
7
|
+
|
|
8
|
+
/** The labels of each grid row, empty cells as `''`. */
|
|
9
|
+
function gridRows() {
|
|
10
|
+
return screen
|
|
11
|
+
.getByTestId('grid')
|
|
12
|
+
.children.map((row) =>
|
|
13
|
+
typeof row === 'string'
|
|
14
|
+
? []
|
|
15
|
+
: row.children.map((cell) =>
|
|
16
|
+
typeof cell === 'string'
|
|
17
|
+
? cell
|
|
18
|
+
: (within(cell).queryByText(/./)?.props.children ?? ''),
|
|
19
|
+
),
|
|
20
|
+
);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function renderGrid(props: React.ComponentProps<typeof Grid>) {
|
|
24
|
+
return render(
|
|
25
|
+
<Grid testID="grid" {...props}>
|
|
26
|
+
{LABELS.map((label) => (
|
|
27
|
+
<Text key={label}>{label}</Text>
|
|
28
|
+
))}
|
|
29
|
+
</Grid>,
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
describe('Grid', () => {
|
|
34
|
+
it('fills rows first, keeping empty cells in the last row', async () => {
|
|
35
|
+
await renderGrid({ columns: 3 });
|
|
36
|
+
|
|
37
|
+
expect(gridRows()).toEqual([
|
|
38
|
+
['A', 'B', 'C'],
|
|
39
|
+
['D', 'E', ''],
|
|
40
|
+
]);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('fills columns first with flow="column"', async () => {
|
|
44
|
+
await renderGrid({ flow: 'column', rows: 2 });
|
|
45
|
+
|
|
46
|
+
expect(gridRows()).toEqual([
|
|
47
|
+
['A', 'C', 'E'],
|
|
48
|
+
['B', 'D', ''],
|
|
49
|
+
]);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('keeps the rows it is given, even empty ones', async () => {
|
|
53
|
+
await renderGrid({ flow: 'column', rows: 3 });
|
|
54
|
+
|
|
55
|
+
expect(gridRows()).toEqual([
|
|
56
|
+
['A', 'D'],
|
|
57
|
+
['B', 'E'],
|
|
58
|
+
['C', ''],
|
|
59
|
+
]);
|
|
60
|
+
|
|
61
|
+
await renderGrid({ columns: 2, rows: 4 });
|
|
62
|
+
|
|
63
|
+
expect(gridRows()).toEqual([
|
|
64
|
+
['A', 'B'],
|
|
65
|
+
['C', 'D'],
|
|
66
|
+
['E', ''],
|
|
67
|
+
['', ''],
|
|
68
|
+
]);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it('places the dense flows like row and column', async () => {
|
|
72
|
+
await renderGrid({ flow: 'column-dense', rows: 2 });
|
|
73
|
+
|
|
74
|
+
expect(gridRows()).toEqual([
|
|
75
|
+
['A', 'C', 'E'],
|
|
76
|
+
['B', 'D', ''],
|
|
77
|
+
]);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it('gives each child of a fragment its own cell', async () => {
|
|
81
|
+
await render(
|
|
82
|
+
<Grid columns={2} testID="grid">
|
|
83
|
+
<>
|
|
84
|
+
<Text>A</Text>
|
|
85
|
+
<Text>B</Text>
|
|
86
|
+
</>
|
|
87
|
+
<Text>C</Text>
|
|
88
|
+
</Grid>,
|
|
89
|
+
);
|
|
90
|
+
|
|
91
|
+
expect(gridRows()).toEqual([
|
|
92
|
+
['A', 'B'],
|
|
93
|
+
['C', ''],
|
|
94
|
+
]);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it('distributes rows and cells with placeContent', async () => {
|
|
98
|
+
await renderGrid({ columns: 2, justify: 'end', placeContent: 'stretch' });
|
|
99
|
+
|
|
100
|
+
const grid = screen.getByTestId('grid');
|
|
101
|
+
expect(grid).toHaveProp('className', 'justify-start');
|
|
102
|
+
expect(grid.children[0]).toHaveProp(
|
|
103
|
+
'className',
|
|
104
|
+
'flex-row justify-start grow',
|
|
105
|
+
);
|
|
106
|
+
|
|
107
|
+
await renderGrid({ columns: 2, justify: 'end' });
|
|
108
|
+
|
|
109
|
+
expect(screen.getByTestId('grid').children[0]).toHaveProp(
|
|
110
|
+
'className',
|
|
111
|
+
'flex-row justify-end',
|
|
112
|
+
);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it('stacks its children in one column by default', async () => {
|
|
116
|
+
await renderGrid({});
|
|
117
|
+
|
|
118
|
+
expect(gridRows()).toEqual([['A'], ['B'], ['C'], ['D'], ['E']]);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it('spaces rows with gapY and columns with gapX', async () => {
|
|
122
|
+
await renderGrid({ columns: 2, gap: 4, gapX: 2 });
|
|
123
|
+
|
|
124
|
+
const grid = screen.getByTestId('grid');
|
|
125
|
+
expect(grid).toHaveProp('className', 'gap-y-4');
|
|
126
|
+
expect(grid.children[0]).toHaveProp('className', 'flex-row gap-x-2');
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it('places items in their cells', async () => {
|
|
130
|
+
await renderGrid({ align: 'end', columns: 2 });
|
|
131
|
+
const cell = screen.getByText('A').parent;
|
|
132
|
+
expect(cell).toHaveProp('className', 'flex-1 justify-end');
|
|
133
|
+
|
|
134
|
+
await renderGrid({ align: 'end', columns: 2, placeItems: 'center' });
|
|
135
|
+
expect(screen.getByText('A').parent).toHaveProp(
|
|
136
|
+
'className',
|
|
137
|
+
'flex-1 items-center justify-center',
|
|
138
|
+
);
|
|
139
|
+
});
|
|
140
|
+
});
|