@pickle-ui/react-native 1.0.0-beta.0 → 1.0.0-beta.2
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 +70 -11
- package/dist/components/Badge.d.ts +6 -3
- package/dist/components/Badge.d.ts.map +1 -1
- package/dist/components/Badge.js +36 -7
- package/dist/components/Badge.js.map +1 -1
- package/dist/components/Button.d.ts +11 -4
- package/dist/components/Button.d.ts.map +1 -1
- package/dist/components/Button.js +100 -15
- package/dist/components/Button.js.map +1 -1
- package/dist/components/Card.d.ts +15 -5
- package/dist/components/Card.d.ts.map +1 -1
- package/dist/components/Card.js +28 -10
- 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 +7 -3
- package/dist/components/Text.d.ts.map +1 -1
- package/dist/components/Text.js +23 -11
- 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 +41 -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/card.d.ts +1 -1
- package/dist/entries/card.d.ts.map +1 -1
- 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/theme.d.ts +3 -0
- package/dist/entries/theme.d.ts.map +1 -0
- package/dist/entries/theme.js +2 -0
- package/dist/entries/theme.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 +13 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +13 -0
- package/dist/index.js.map +1 -1
- package/dist/lib/cn.d.ts.map +1 -1
- package/dist/lib/cn.js +13 -1
- package/dist/lib/cn.js.map +1 -1
- package/package.json +10 -3
- package/src/base.css +7 -2
- package/src/components/Badge.stories.tsx +51 -6
- package/src/components/Badge.test.tsx +15 -0
- package/src/components/Badge.tsx +73 -17
- package/src/components/Button.stories.tsx +96 -8
- package/src/components/Button.test.tsx +69 -0
- package/src/components/Button.tsx +159 -18
- package/src/components/Card.stories.tsx +17 -4
- package/src/components/Card.test.tsx +24 -1
- package/src/components/Card.tsx +71 -13
- 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 +52 -2
- package/src/components/Text.test.tsx +35 -3
- package/src/components/Text.tsx +42 -12
- 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/Story.tsx +34 -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 +141 -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 +17 -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/card.ts +2 -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/theme.ts +7 -0
- package/src/entries/x-stack.ts +2 -0
- package/src/entries/y-stack.ts +2 -0
- package/src/index.ts +13 -0
- package/src/lib/cn.test.ts +6 -0
- package/src/lib/cn.ts +14 -1
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { fireEvent, render, screen } from '@testing-library/react-native';
|
|
2
|
+
import { View } from 'react-native';
|
|
2
3
|
|
|
3
4
|
import { Button } from './Button';
|
|
4
5
|
|
|
@@ -31,4 +32,72 @@ describe('Button', () => {
|
|
|
31
32
|
expect(screen.getByRole('button')).toBeDisabled();
|
|
32
33
|
expect(onPress).not.toHaveBeenCalled();
|
|
33
34
|
});
|
|
35
|
+
|
|
36
|
+
it('renders icons next to the label', async () => {
|
|
37
|
+
await render(
|
|
38
|
+
<Button>
|
|
39
|
+
<View testID="icon" />
|
|
40
|
+
Add project
|
|
41
|
+
</Button>,
|
|
42
|
+
);
|
|
43
|
+
|
|
44
|
+
expect(screen.getByRole('button')).toContainElement(
|
|
45
|
+
screen.getByTestId('icon'),
|
|
46
|
+
);
|
|
47
|
+
expect(screen.getByText('Add project')).toBeOnTheScreen();
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('names an icon-only button with accessibilityLabel', async () => {
|
|
51
|
+
await render(
|
|
52
|
+
<Button accessibilityLabel="Add project" size="icon">
|
|
53
|
+
<View />
|
|
54
|
+
</Button>,
|
|
55
|
+
);
|
|
56
|
+
|
|
57
|
+
expect(
|
|
58
|
+
screen.getByRole('button', { name: 'Add project' }),
|
|
59
|
+
).toBeOnTheScreen();
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('is busy and disabled while loading', async () => {
|
|
63
|
+
const onPress = jest.fn();
|
|
64
|
+
await render(
|
|
65
|
+
<Button loading onPress={onPress}>
|
|
66
|
+
Save
|
|
67
|
+
</Button>,
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
await fireEvent.press(screen.getByRole('button'));
|
|
71
|
+
|
|
72
|
+
expect(screen.getByRole('button')).toBeBusy();
|
|
73
|
+
expect(screen.getByRole('button')).toBeDisabled();
|
|
74
|
+
expect(onPress).not.toHaveBeenCalled();
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it('stretches every size to a 44pt touch target', async () => {
|
|
78
|
+
await render(
|
|
79
|
+
<>
|
|
80
|
+
<Button size="sm">Small</Button>
|
|
81
|
+
<Button accessibilityLabel="Add" size="icon">
|
|
82
|
+
+
|
|
83
|
+
</Button>
|
|
84
|
+
<Button hitSlop={0} size="xs">
|
|
85
|
+
Extra small
|
|
86
|
+
</Button>
|
|
87
|
+
</>,
|
|
88
|
+
);
|
|
89
|
+
|
|
90
|
+
expect(screen.getByRole('button', { name: 'Small' })).toHaveProp(
|
|
91
|
+
'hitSlop',
|
|
92
|
+
{ bottom: 8, top: 8 },
|
|
93
|
+
);
|
|
94
|
+
expect(screen.getByRole('button', { name: 'Add' })).toHaveProp(
|
|
95
|
+
'hitSlop',
|
|
96
|
+
6,
|
|
97
|
+
);
|
|
98
|
+
expect(screen.getByRole('button', { name: 'Extra small' })).toHaveProp(
|
|
99
|
+
'hitSlop',
|
|
100
|
+
0,
|
|
101
|
+
);
|
|
102
|
+
});
|
|
34
103
|
});
|
|
@@ -1,70 +1,211 @@
|
|
|
1
1
|
import { cva, type VariantProps } from 'class-variance-authority';
|
|
2
|
-
import {
|
|
2
|
+
import { Children, isValidElement } from 'react';
|
|
3
|
+
import { type Insets, Pressable, Text, View } from 'react-native';
|
|
3
4
|
|
|
4
5
|
import { cn } from '../lib/cn';
|
|
6
|
+
import { Spinner } from './Spinner';
|
|
5
7
|
|
|
6
|
-
// Same
|
|
8
|
+
// Same variants, sizes and dimensions as `Button` in @pickle-ui/react on a
|
|
9
|
+
// phone, with its text sizes from `sm` up. Like the web's `pointer-coarse` hit area, `hitSlop` stretches every
|
|
10
|
+
// size to a 44pt touch target. Paddings leave room for the 1pt border.
|
|
7
11
|
const buttonVariants = cva(
|
|
8
|
-
'flex-row items-center justify-center gap-2 rounded-lg border
|
|
12
|
+
'flex-row items-center justify-center gap-2 rounded-lg border disabled:opacity-64',
|
|
9
13
|
{
|
|
10
14
|
defaultVariants: { size: 'md', variant: 'default' },
|
|
11
15
|
variants: {
|
|
12
16
|
size: {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
sm: '
|
|
17
|
+
icon: 'size-8',
|
|
18
|
+
'icon-lg': 'size-9',
|
|
19
|
+
'icon-sm': 'size-7',
|
|
20
|
+
'icon-xl': 'size-10',
|
|
21
|
+
'icon-xs': 'size-6 rounded-md',
|
|
22
|
+
lg: 'h-9 px-[13px]',
|
|
23
|
+
md: 'h-8 px-[11px]',
|
|
24
|
+
sm: 'h-7 gap-1.5 px-[9px]',
|
|
25
|
+
xl: 'h-10 px-[15px]',
|
|
26
|
+
xs: 'h-6 gap-1 rounded-md px-[7px]',
|
|
16
27
|
},
|
|
17
28
|
variant: {
|
|
18
|
-
default:
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
29
|
+
default:
|
|
30
|
+
'border-primary bg-primary shadow-primary/24 shadow-xs active:bg-primary/90 disabled:shadow-none',
|
|
31
|
+
destructive:
|
|
32
|
+
'border-destructive bg-destructive shadow-destructive/24 shadow-xs active:bg-destructive/90 disabled:shadow-none',
|
|
33
|
+
'destructive-outline':
|
|
34
|
+
'border-input bg-popover active:border-destructive/32 active:bg-destructive/4 dark:bg-input/32 shadow-xs shadow-black/5 disabled:shadow-none',
|
|
35
|
+
ghost: 'active:bg-accent border-transparent',
|
|
36
|
+
link: 'border-transparent',
|
|
37
|
+
outline:
|
|
38
|
+
'border-input bg-popover active:bg-accent/50 dark:bg-input/32 dark:active:bg-input/64 shadow-xs shadow-black/5 disabled:shadow-none',
|
|
39
|
+
secondary: 'bg-secondary active:bg-secondary/80 border-transparent',
|
|
23
40
|
},
|
|
24
41
|
},
|
|
25
42
|
},
|
|
26
43
|
);
|
|
27
44
|
|
|
28
45
|
const labelVariants = cva('font-medium', {
|
|
46
|
+
compoundVariants: [
|
|
47
|
+
{ className: 'underline', pressed: true, variant: 'link' },
|
|
48
|
+
],
|
|
29
49
|
defaultVariants: { size: 'md', variant: 'default' },
|
|
30
50
|
variants: {
|
|
31
|
-
|
|
51
|
+
pressed: { false: '', true: '' },
|
|
52
|
+
// The web's sizes from `sm` up, not its larger phone sizes, which only
|
|
53
|
+
// stop iOS Safari zooming into focused fields.
|
|
54
|
+
size: {
|
|
55
|
+
icon: 'text-sm',
|
|
56
|
+
'icon-lg': 'text-sm',
|
|
57
|
+
'icon-sm': 'text-sm',
|
|
58
|
+
'icon-xl': 'text-sm',
|
|
59
|
+
'icon-xs': 'text-sm',
|
|
60
|
+
lg: 'text-sm',
|
|
61
|
+
md: 'text-sm',
|
|
62
|
+
sm: 'text-sm',
|
|
63
|
+
xl: 'text-base',
|
|
64
|
+
xs: 'text-xs',
|
|
65
|
+
},
|
|
66
|
+
variant: {
|
|
67
|
+
default: 'text-primary-foreground',
|
|
68
|
+
destructive: 'text-destructive-foreground',
|
|
69
|
+
'destructive-outline': 'text-destructive-subtle-foreground',
|
|
70
|
+
ghost: 'text-foreground',
|
|
71
|
+
link: 'text-foreground',
|
|
72
|
+
outline: 'text-foreground',
|
|
73
|
+
secondary: 'text-secondary-foreground',
|
|
74
|
+
},
|
|
75
|
+
},
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
// Sized and dimmed like the web button's icons, in the label's color.
|
|
79
|
+
const spinnerVariants = cva('size-4 opacity-80', {
|
|
80
|
+
variants: {
|
|
32
81
|
variant: {
|
|
33
82
|
default: 'text-primary-foreground',
|
|
34
83
|
destructive: 'text-destructive-foreground',
|
|
84
|
+
'destructive-outline': 'text-foreground',
|
|
35
85
|
ghost: 'text-foreground',
|
|
86
|
+
link: 'text-foreground',
|
|
36
87
|
outline: 'text-foreground',
|
|
37
88
|
secondary: 'text-secondary-foreground',
|
|
38
89
|
},
|
|
39
90
|
},
|
|
40
91
|
});
|
|
41
92
|
|
|
93
|
+
// The web's 1px inner highlight on solid buttons. Uniwind draws
|
|
94
|
+
// `inset-shadow` outside the view, so a top border on an overlay stands in.
|
|
95
|
+
const highlightVariants = cva('absolute inset-0 rounded-lg border-t', {
|
|
96
|
+
variants: {
|
|
97
|
+
pressed: { false: 'border-white/16', true: 'border-black/8' },
|
|
98
|
+
size: {
|
|
99
|
+
icon: '',
|
|
100
|
+
'icon-lg': '',
|
|
101
|
+
'icon-sm': '',
|
|
102
|
+
'icon-xl': '',
|
|
103
|
+
'icon-xs': 'rounded-md',
|
|
104
|
+
lg: '',
|
|
105
|
+
md: '',
|
|
106
|
+
sm: '',
|
|
107
|
+
xl: '',
|
|
108
|
+
xs: 'rounded-md',
|
|
109
|
+
},
|
|
110
|
+
},
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
type ButtonSize = NonNullable<VariantProps<typeof buttonVariants>['size']>;
|
|
114
|
+
|
|
115
|
+
// Half the gap between each size and a 44pt touch target, vertical only for
|
|
116
|
+
// buttons with a label, whose width already reaches it.
|
|
117
|
+
const HIT_SLOP: Record<ButtonSize, Insets | number> = {
|
|
118
|
+
icon: 6,
|
|
119
|
+
'icon-lg': 4,
|
|
120
|
+
'icon-sm': 8,
|
|
121
|
+
'icon-xl': 2,
|
|
122
|
+
'icon-xs': 10,
|
|
123
|
+
lg: { bottom: 4, top: 4 },
|
|
124
|
+
md: { bottom: 6, top: 6 },
|
|
125
|
+
sm: { bottom: 8, top: 8 },
|
|
126
|
+
xl: { bottom: 2, top: 2 },
|
|
127
|
+
xs: { bottom: 10, top: 10 },
|
|
128
|
+
};
|
|
129
|
+
|
|
42
130
|
export type ButtonProps = Omit<
|
|
43
131
|
React.ComponentProps<typeof Pressable>,
|
|
44
132
|
'children'
|
|
45
133
|
> &
|
|
46
134
|
VariantProps<typeof buttonVariants> & {
|
|
47
|
-
|
|
135
|
+
/**
|
|
136
|
+
* Text is wrapped in the label; other children, such as icons, render as
|
|
137
|
+
* they are. Icon-only buttons need an `icon*` size and an
|
|
138
|
+
* `accessibilityLabel`.
|
|
139
|
+
*/
|
|
140
|
+
children: React.ReactNode;
|
|
48
141
|
labelClassName?: string;
|
|
142
|
+
/** Shows a spinner in place of the content and disables the button. */
|
|
143
|
+
loading?: boolean;
|
|
49
144
|
};
|
|
50
145
|
|
|
51
146
|
export function Button({
|
|
147
|
+
accessibilityState,
|
|
52
148
|
children,
|
|
53
149
|
className,
|
|
150
|
+
disabled,
|
|
54
151
|
labelClassName,
|
|
55
|
-
|
|
56
|
-
|
|
152
|
+
loading = false,
|
|
153
|
+
size = 'md',
|
|
154
|
+
variant = 'default',
|
|
57
155
|
...props
|
|
58
156
|
}: ButtonProps) {
|
|
157
|
+
const isDisabled = Boolean(disabled || loading);
|
|
158
|
+
const isSolid = variant === 'default' || variant === 'destructive';
|
|
159
|
+
|
|
59
160
|
return (
|
|
60
161
|
<Pressable
|
|
61
162
|
accessibilityRole="button"
|
|
163
|
+
accessibilityState={{
|
|
164
|
+
...accessibilityState,
|
|
165
|
+
busy: loading,
|
|
166
|
+
disabled: isDisabled,
|
|
167
|
+
}}
|
|
62
168
|
className={cn(buttonVariants({ size, variant }), className)}
|
|
169
|
+
disabled={isDisabled}
|
|
170
|
+
hitSlop={size ? HIT_SLOP[size] : undefined}
|
|
63
171
|
{...props}
|
|
64
172
|
>
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
173
|
+
{({ pressed }) => (
|
|
174
|
+
<>
|
|
175
|
+
{isSolid && !isDisabled ? (
|
|
176
|
+
<View
|
|
177
|
+
className={highlightVariants({ pressed, size })}
|
|
178
|
+
pointerEvents="none"
|
|
179
|
+
/>
|
|
180
|
+
) : null}
|
|
181
|
+
{/* While loading, the content stays in place, invisible, so the
|
|
182
|
+
button keeps its width under the spinner. */}
|
|
183
|
+
{Children.map(children, (child) =>
|
|
184
|
+
typeof child === 'string' || typeof child === 'number' ? (
|
|
185
|
+
<Text
|
|
186
|
+
className={cn(
|
|
187
|
+
labelVariants({ pressed, size, variant }),
|
|
188
|
+
loading && 'opacity-0',
|
|
189
|
+
labelClassName,
|
|
190
|
+
)}
|
|
191
|
+
>
|
|
192
|
+
{child}
|
|
193
|
+
</Text>
|
|
194
|
+
) : isValidElement(child) ? (
|
|
195
|
+
loading ? (
|
|
196
|
+
<View className="opacity-0">{child}</View>
|
|
197
|
+
) : (
|
|
198
|
+
child
|
|
199
|
+
)
|
|
200
|
+
) : null,
|
|
201
|
+
)}
|
|
202
|
+
{loading ? (
|
|
203
|
+
<View className="absolute">
|
|
204
|
+
<Spinner aria-hidden className={spinnerVariants({ variant })} />
|
|
205
|
+
</View>
|
|
206
|
+
) : null}
|
|
207
|
+
</>
|
|
208
|
+
)}
|
|
68
209
|
</Pressable>
|
|
69
210
|
);
|
|
70
211
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { Meta, StoryObj } from '@storybook/react-native-web-vite';
|
|
2
2
|
|
|
3
|
+
import { IconArrowUpRight, IconDots } from '@tabler/icons-react-native';
|
|
3
4
|
import { View } from 'react-native';
|
|
4
5
|
|
|
5
6
|
import { Story } from './_internal/Story';
|
|
@@ -27,14 +28,26 @@ export const All: StoryDefinition = {
|
|
|
27
28
|
<Card.Description>
|
|
28
29
|
Changes made in the last week.
|
|
29
30
|
</Card.Description>
|
|
31
|
+
<Card.Action>
|
|
32
|
+
<Button
|
|
33
|
+
accessibilityLabel="More options"
|
|
34
|
+
size="icon"
|
|
35
|
+
variant="ghost"
|
|
36
|
+
>
|
|
37
|
+
<Story.Icon icon={IconDots} />
|
|
38
|
+
</Button>
|
|
39
|
+
</Card.Action>
|
|
30
40
|
</Card.Header>
|
|
31
41
|
<Card.Content className="gap-2">
|
|
32
42
|
<Text variant="h1">1,284</Text>
|
|
33
43
|
<Text tone="muted">Events across 8 active projects</Text>
|
|
34
|
-
|
|
44
|
+
</Card.Content>
|
|
45
|
+
<Card.Footer>
|
|
46
|
+
<Button className="ml-auto" size="sm" variant="ghost">
|
|
35
47
|
View report
|
|
48
|
+
<Story.Icon icon={IconArrowUpRight} />
|
|
36
49
|
</Button>
|
|
37
|
-
</Card.
|
|
50
|
+
</Card.Footer>
|
|
38
51
|
</Card>
|
|
39
52
|
|
|
40
53
|
<Card>
|
|
@@ -44,9 +57,9 @@ export const All: StoryDefinition = {
|
|
|
44
57
|
Parts can be omitted when the content needs less structure.
|
|
45
58
|
</Card.Description>
|
|
46
59
|
</Card.Header>
|
|
47
|
-
<Card.
|
|
60
|
+
<Card.Footer>
|
|
48
61
|
<Button>Continue</Button>
|
|
49
|
-
</Card.
|
|
62
|
+
</Card.Footer>
|
|
50
63
|
</Card>
|
|
51
64
|
</View>
|
|
52
65
|
</Story.Section>
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { render, screen } from '@testing-library/react-native';
|
|
2
2
|
|
|
3
|
+
import { Button } from './Button';
|
|
3
4
|
import { Card } from './Card';
|
|
4
5
|
|
|
5
6
|
describe('Card', () => {
|
|
@@ -11,13 +12,35 @@ describe('Card', () => {
|
|
|
11
12
|
<Card.Description>Everything in flight</Card.Description>
|
|
12
13
|
</Card.Header>
|
|
13
14
|
<Card.Content testID="content" />
|
|
15
|
+
<Card.Footer testID="footer" />
|
|
14
16
|
</Card>,
|
|
15
17
|
);
|
|
16
18
|
|
|
17
19
|
expect(screen.getByTestId('card')).toContainElement(
|
|
18
|
-
screen.
|
|
20
|
+
screen.getByRole('header', { name: 'Projects' }),
|
|
19
21
|
);
|
|
20
22
|
expect(screen.getByText('Everything in flight')).toBeOnTheScreen();
|
|
21
23
|
expect(screen.getByTestId('content')).toBeOnTheScreen();
|
|
24
|
+
expect(screen.getByTestId('footer')).toBeOnTheScreen();
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it('places Card.Action in the header', async () => {
|
|
28
|
+
await render(
|
|
29
|
+
<Card>
|
|
30
|
+
<Card.Header testID="header">
|
|
31
|
+
<Card.Title>Projects</Card.Title>
|
|
32
|
+
<Card.Action>
|
|
33
|
+
<Button size="sm" variant="outline">
|
|
34
|
+
View all
|
|
35
|
+
</Button>
|
|
36
|
+
</Card.Action>
|
|
37
|
+
</Card.Header>
|
|
38
|
+
</Card>,
|
|
39
|
+
);
|
|
40
|
+
|
|
41
|
+
expect(screen.getByTestId('header')).toContainElement(
|
|
42
|
+
screen.getByRole('button', { name: 'View all' }),
|
|
43
|
+
);
|
|
44
|
+
expect(screen.getByText('Projects')).toBeOnTheScreen();
|
|
22
45
|
});
|
|
23
46
|
});
|
package/src/components/Card.tsx
CHANGED
|
@@ -1,31 +1,81 @@
|
|
|
1
|
+
import { Children, isValidElement } from 'react';
|
|
1
2
|
import { View } from 'react-native';
|
|
2
3
|
|
|
3
4
|
import { cn } from '../lib/cn';
|
|
4
|
-
import { Text } from './Text';
|
|
5
|
+
import { Text, type TextProps } from './Text';
|
|
5
6
|
|
|
7
|
+
export type CardActionProps = React.ComponentProps<typeof View>;
|
|
6
8
|
export type CardContentProps = React.ComponentProps<typeof View>;
|
|
7
|
-
export type CardDescriptionProps =
|
|
9
|
+
export type CardDescriptionProps = Omit<TextProps, 'tone' | 'variant'>;
|
|
10
|
+
export type CardFooterProps = React.ComponentProps<typeof View>;
|
|
8
11
|
export type CardHeaderProps = React.ComponentProps<typeof View>;
|
|
9
12
|
export type CardProps = React.ComponentProps<typeof View>;
|
|
10
|
-
export type CardTitleProps =
|
|
13
|
+
export type CardTitleProps = Omit<TextProps, 'tone' | 'variant'>;
|
|
14
|
+
|
|
15
|
+
// The parts sit in a column with `gap-4` and their own side padding, which
|
|
16
|
+
// reproduces the web's spacing between header, content and footer.
|
|
17
|
+
|
|
18
|
+
function CardAction({ className, ...props }: CardActionProps) {
|
|
19
|
+
return <View className={cn('self-start', className)} {...props} />;
|
|
20
|
+
}
|
|
11
21
|
|
|
12
22
|
function CardContent({ className, ...props }: CardContentProps) {
|
|
13
|
-
return <View className={cn('
|
|
23
|
+
return <View className={cn('px-6', className)} {...props} />;
|
|
14
24
|
}
|
|
15
25
|
|
|
16
26
|
function CardDescription({ className, ...props }: CardDescriptionProps) {
|
|
17
|
-
return
|
|
27
|
+
return (
|
|
28
|
+
<Text
|
|
29
|
+
className={cn('text-sm', className)}
|
|
30
|
+
tone="muted"
|
|
31
|
+
variant="body"
|
|
32
|
+
{...props}
|
|
33
|
+
/>
|
|
34
|
+
);
|
|
18
35
|
}
|
|
19
36
|
|
|
20
|
-
function
|
|
21
|
-
return
|
|
37
|
+
function CardFooter({ className, ...props }: CardFooterProps) {
|
|
38
|
+
return (
|
|
39
|
+
<View className={cn('flex-row items-center px-6', className)} {...props} />
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Lays a `Card.Action` out beside the title and description, top-aligned,
|
|
45
|
+
* like the web header's grid.
|
|
46
|
+
*/
|
|
47
|
+
function CardHeader({ children, className, ...props }: CardHeaderProps) {
|
|
48
|
+
const parts = Children.toArray(children);
|
|
49
|
+
const actions = parts.filter(
|
|
50
|
+
(child) => isValidElement(child) && child.type === CardAction,
|
|
51
|
+
);
|
|
52
|
+
|
|
53
|
+
if (actions.length === 0) {
|
|
54
|
+
return (
|
|
55
|
+
<View className={cn('gap-1.5 px-6', className)} {...props}>
|
|
56
|
+
{children}
|
|
57
|
+
</View>
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return (
|
|
62
|
+
<View
|
|
63
|
+
className={cn('flex-row items-start gap-4 px-6', className)}
|
|
64
|
+
{...props}
|
|
65
|
+
>
|
|
66
|
+
<View className="flex-1 gap-1.5">
|
|
67
|
+
{parts.filter((child) => !actions.includes(child))}
|
|
68
|
+
</View>
|
|
69
|
+
{actions}
|
|
70
|
+
</View>
|
|
71
|
+
);
|
|
22
72
|
}
|
|
23
73
|
|
|
24
74
|
function CardRoot({ className, ...props }: CardProps) {
|
|
25
75
|
return (
|
|
26
76
|
<View
|
|
27
77
|
className={cn(
|
|
28
|
-
'border-border bg-card gap-4 rounded-
|
|
78
|
+
'border-border bg-card gap-4 rounded-2xl border py-6 shadow-xs shadow-black/5',
|
|
29
79
|
className,
|
|
30
80
|
)}
|
|
31
81
|
{...props}
|
|
@@ -36,20 +86,28 @@ function CardRoot({ className, ...props }: CardProps) {
|
|
|
36
86
|
function CardTitle({ className, ...props }: CardTitleProps) {
|
|
37
87
|
return (
|
|
38
88
|
<Text
|
|
39
|
-
className={cn('text-card-foreground', className)}
|
|
40
|
-
variant="
|
|
89
|
+
className={cn('text-card-foreground leading-none', className)}
|
|
90
|
+
variant="h3"
|
|
41
91
|
{...props}
|
|
42
92
|
/>
|
|
43
93
|
);
|
|
44
94
|
}
|
|
45
95
|
|
|
46
|
-
// Same compound shape as `Card` in @pickle-ui/react. The parts are exported
|
|
47
|
-
// so Fast Refresh keeps their state; the public entry only exports `Card`.
|
|
48
96
|
const Card = Object.assign(CardRoot, {
|
|
97
|
+
Action: CardAction,
|
|
49
98
|
Content: CardContent,
|
|
50
99
|
Description: CardDescription,
|
|
100
|
+
Footer: CardFooter,
|
|
51
101
|
Header: CardHeader,
|
|
52
102
|
Title: CardTitle,
|
|
53
103
|
});
|
|
54
104
|
|
|
55
|
-
export {
|
|
105
|
+
export {
|
|
106
|
+
Card,
|
|
107
|
+
CardAction,
|
|
108
|
+
CardContent,
|
|
109
|
+
CardDescription,
|
|
110
|
+
CardFooter,
|
|
111
|
+
CardHeader,
|
|
112
|
+
CardTitle,
|
|
113
|
+
};
|
|
@@ -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
|
+
}
|