@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
|
@@ -4,12 +4,44 @@ import { Text } from './Text';
|
|
|
4
4
|
|
|
5
5
|
describe('Text', () => {
|
|
6
6
|
it('renders its children', async () => {
|
|
7
|
+
await render(<Text tone="muted">Hello</Text>);
|
|
8
|
+
|
|
9
|
+
expect(screen.getByText('Hello')).toBeOnTheScreen();
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
it('announces heading variants as headers', async () => {
|
|
13
|
+
await render(
|
|
14
|
+
<>
|
|
15
|
+
<Text variant="h2">Projects</Text>
|
|
16
|
+
<Text variant="lead">Everything in flight</Text>
|
|
17
|
+
</>,
|
|
18
|
+
);
|
|
19
|
+
|
|
20
|
+
expect(screen.getByRole('header', { name: 'Projects' })).toBeOnTheScreen();
|
|
21
|
+
expect(
|
|
22
|
+
screen.queryByRole('header', { name: 'Everything in flight' }),
|
|
23
|
+
).toBeNull();
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it('cuts truncated text to one line', async () => {
|
|
27
|
+
await render(<Text truncate>A long project name</Text>);
|
|
28
|
+
|
|
29
|
+
expect(screen.getByText('A long project name')).toHaveProp(
|
|
30
|
+
'numberOfLines',
|
|
31
|
+
1,
|
|
32
|
+
);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it('sets the code variant in a monospace font', async () => {
|
|
7
36
|
await render(
|
|
8
|
-
<Text
|
|
9
|
-
|
|
37
|
+
<Text style={{ color: 'red' }} variant="code">
|
|
38
|
+
pnpm test
|
|
10
39
|
</Text>,
|
|
11
40
|
);
|
|
12
41
|
|
|
13
|
-
expect(screen.getByText('
|
|
42
|
+
expect(screen.getByText('pnpm test')).toHaveStyle({
|
|
43
|
+
color: 'red',
|
|
44
|
+
fontFamily: expect.any(String),
|
|
45
|
+
});
|
|
14
46
|
});
|
|
15
47
|
});
|
package/src/components/Text.tsx
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { cva, type VariantProps } from 'class-variance-authority';
|
|
2
|
-
import { Text as NativeText } from 'react-native';
|
|
2
|
+
import { Text as NativeText, Platform, StyleSheet } from 'react-native';
|
|
3
3
|
|
|
4
4
|
import { cn } from '../lib/cn';
|
|
5
5
|
|
|
6
|
-
// Same
|
|
6
|
+
// Same variants, tones and weights as `Text` in @pickle-ui/react, sized by the
|
|
7
|
+
// shared type scale (`@pickle-ui/tokens/typography.css`).
|
|
7
8
|
const textVariants = cva('text-foreground', {
|
|
8
9
|
defaultVariants: { variant: 'body' },
|
|
9
10
|
variants: {
|
|
@@ -13,24 +14,53 @@ const textVariants = cva('text-foreground', {
|
|
|
13
14
|
primary: 'text-primary',
|
|
14
15
|
},
|
|
15
16
|
variant: {
|
|
16
|
-
body: 'text-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
17
|
+
body: 'text-body',
|
|
18
|
+
code: 'text-code bg-muted self-start overflow-hidden rounded px-2 py-1',
|
|
19
|
+
h1: 'text-h1 font-semibold tracking-tight',
|
|
20
|
+
h2: 'text-h2 font-semibold tracking-tight',
|
|
21
|
+
h3: 'text-h3 font-semibold',
|
|
22
|
+
h4: 'text-h4 font-medium',
|
|
23
|
+
lead: 'text-lead',
|
|
24
|
+
small: 'text-small',
|
|
25
|
+
},
|
|
26
|
+
weight: {
|
|
27
|
+
bold: 'font-semibold',
|
|
28
|
+
normal: 'font-normal',
|
|
23
29
|
},
|
|
24
30
|
},
|
|
25
31
|
});
|
|
26
32
|
|
|
33
|
+
const HEADINGS = new Set(['h1', 'h2', 'h3', 'h4']);
|
|
34
|
+
|
|
35
|
+
// Uniwind passes `font-mono`'s web font stack through as is, which native
|
|
36
|
+
// fonts don't resolve, so `code` names each platform's monospace font.
|
|
37
|
+
const styles = StyleSheet.create({
|
|
38
|
+
code: { fontFamily: Platform.select({ default: 'monospace', ios: 'Menlo' }) },
|
|
39
|
+
});
|
|
40
|
+
|
|
27
41
|
export type TextProps = React.ComponentProps<typeof NativeText> &
|
|
28
|
-
VariantProps<typeof textVariants
|
|
42
|
+
VariantProps<typeof textVariants> & {
|
|
43
|
+
/** Cuts the text to one line with an ellipsis. */
|
|
44
|
+
truncate?: boolean;
|
|
45
|
+
};
|
|
29
46
|
|
|
30
|
-
export function Text({
|
|
47
|
+
export function Text({
|
|
48
|
+
className,
|
|
49
|
+
style,
|
|
50
|
+
tone,
|
|
51
|
+
truncate,
|
|
52
|
+
variant = 'body',
|
|
53
|
+
weight,
|
|
54
|
+
...props
|
|
55
|
+
}: TextProps) {
|
|
31
56
|
return (
|
|
32
57
|
<NativeText
|
|
33
|
-
|
|
58
|
+
accessibilityRole={
|
|
59
|
+
variant && HEADINGS.has(variant) ? 'header' : undefined
|
|
60
|
+
}
|
|
61
|
+
className={cn(textVariants({ tone, variant, weight }), className)}
|
|
62
|
+
numberOfLines={truncate ? 1 : undefined}
|
|
63
|
+
style={variant === 'code' ? [styles.code, style] : style}
|
|
34
64
|
{...props}
|
|
35
65
|
/>
|
|
36
66
|
);
|
|
@@ -0,0 +1,46 @@
|
|
|
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 { XStack } from './XStack';
|
|
6
|
+
|
|
7
|
+
const meta = {
|
|
8
|
+
component: XStack,
|
|
9
|
+
parameters: { layout: 'fullscreen' },
|
|
10
|
+
title: 'components/XStack',
|
|
11
|
+
} satisfies Meta<typeof XStack>;
|
|
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="XStack">
|
|
19
|
+
<Story.Section title="Horizontal spacing">
|
|
20
|
+
<XStack gap={3}>
|
|
21
|
+
<Item>Avatar</Item>
|
|
22
|
+
<Item>Profile details</Item>
|
|
23
|
+
<Item>Status</Item>
|
|
24
|
+
</XStack>
|
|
25
|
+
</Story.Section>
|
|
26
|
+
|
|
27
|
+
<Story.Section title="Distribution">
|
|
28
|
+
<XStack
|
|
29
|
+
align="center"
|
|
30
|
+
className="border-border min-h-48 rounded-md border border-dashed p-4"
|
|
31
|
+
justify="between"
|
|
32
|
+
>
|
|
33
|
+
<Item>Leading action</Item>
|
|
34
|
+
<Item>Trailing action</Item>
|
|
35
|
+
</XStack>
|
|
36
|
+
</Story.Section>
|
|
37
|
+
|
|
38
|
+
<Story.Section title="Responsive override">
|
|
39
|
+
<XStack className="max-sm:flex-col" gap={3}>
|
|
40
|
+
<Item>Horizontal by default</Item>
|
|
41
|
+
<Item>Vertical below sm</Item>
|
|
42
|
+
</XStack>
|
|
43
|
+
</Story.Section>
|
|
44
|
+
</Story.Layout>
|
|
45
|
+
),
|
|
46
|
+
};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { render, screen } from '@testing-library/react-native';
|
|
2
|
+
|
|
3
|
+
import { XStack } from './XStack';
|
|
4
|
+
|
|
5
|
+
describe('XStack', () => {
|
|
6
|
+
it('renders a row with the Flex props', async () => {
|
|
7
|
+
await render(<XStack align="center" gap={2} testID="stack" />);
|
|
8
|
+
|
|
9
|
+
expect(
|
|
10
|
+
String(screen.getByTestId('stack').props.className).split(' '),
|
|
11
|
+
).toEqual(expect.arrayContaining(['flex-row', 'items-center', 'gap-2']));
|
|
12
|
+
});
|
|
13
|
+
});
|
|
@@ -0,0 +1,46 @@
|
|
|
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 { YStack } from './YStack';
|
|
6
|
+
|
|
7
|
+
const meta = {
|
|
8
|
+
component: YStack,
|
|
9
|
+
parameters: { layout: 'fullscreen' },
|
|
10
|
+
title: 'components/YStack',
|
|
11
|
+
} satisfies Meta<typeof YStack>;
|
|
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="YStack">
|
|
19
|
+
<Story.Section title="Vertical spacing">
|
|
20
|
+
<YStack gap={3}>
|
|
21
|
+
<Item>Avatar</Item>
|
|
22
|
+
<Item>Profile details</Item>
|
|
23
|
+
<Item>Status</Item>
|
|
24
|
+
</YStack>
|
|
25
|
+
</Story.Section>
|
|
26
|
+
|
|
27
|
+
<Story.Section title="Distribution">
|
|
28
|
+
<YStack
|
|
29
|
+
align="center"
|
|
30
|
+
className="border-border min-h-48 rounded-md border border-dashed p-4"
|
|
31
|
+
justify="between"
|
|
32
|
+
>
|
|
33
|
+
<Item>Leading action</Item>
|
|
34
|
+
<Item>Trailing action</Item>
|
|
35
|
+
</YStack>
|
|
36
|
+
</Story.Section>
|
|
37
|
+
|
|
38
|
+
<Story.Section title="Responsive override">
|
|
39
|
+
<YStack className="sm:flex-row" gap={3}>
|
|
40
|
+
<Item>Vertical by default</Item>
|
|
41
|
+
<Item>Horizontal from sm</Item>
|
|
42
|
+
</YStack>
|
|
43
|
+
</Story.Section>
|
|
44
|
+
</Story.Layout>
|
|
45
|
+
),
|
|
46
|
+
};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { render, screen } from '@testing-library/react-native';
|
|
2
|
+
|
|
3
|
+
import { YStack } from './YStack';
|
|
4
|
+
|
|
5
|
+
describe('YStack', () => {
|
|
6
|
+
it('renders a column with the Flex props', async () => {
|
|
7
|
+
await render(<YStack gap={2} justify="center" testID="stack" />);
|
|
8
|
+
|
|
9
|
+
expect(
|
|
10
|
+
String(screen.getByTestId('stack').props.className).split(' '),
|
|
11
|
+
).toEqual(expect.arrayContaining(['flex-col', 'justify-center', 'gap-2']));
|
|
12
|
+
});
|
|
13
|
+
});
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { View } from 'react-native';
|
|
2
|
+
|
|
3
|
+
import { cn } from '../../lib/cn';
|
|
4
|
+
|
|
5
|
+
export type InputFrameProps = React.ComponentProps<typeof View> & {
|
|
6
|
+
disabled?: boolean;
|
|
7
|
+
focused: boolean;
|
|
8
|
+
invalid?: boolean;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* The border, background and focus ring the web's `Input`, `Textarea` and
|
|
13
|
+
* `InputGroup` draw around their field. React Native has no `ring`, so a
|
|
14
|
+
* 3pt outline sits outside the border while the field has focus.
|
|
15
|
+
*/
|
|
16
|
+
export function InputFrame({
|
|
17
|
+
children,
|
|
18
|
+
className,
|
|
19
|
+
disabled,
|
|
20
|
+
focused,
|
|
21
|
+
invalid,
|
|
22
|
+
...props
|
|
23
|
+
}: InputFrameProps) {
|
|
24
|
+
return (
|
|
25
|
+
<View
|
|
26
|
+
className={cn(
|
|
27
|
+
'border-input bg-background dark:bg-input/32 w-full flex-row rounded-lg border shadow-xs shadow-black/5',
|
|
28
|
+
invalid && 'border-destructive/36',
|
|
29
|
+
focused && (invalid ? 'border-destructive/64' : 'border-ring'),
|
|
30
|
+
(disabled || focused || invalid) && 'shadow-none',
|
|
31
|
+
disabled && 'opacity-64',
|
|
32
|
+
className,
|
|
33
|
+
)}
|
|
34
|
+
{...props}
|
|
35
|
+
>
|
|
36
|
+
{children}
|
|
37
|
+
{focused ? (
|
|
38
|
+
<View
|
|
39
|
+
className={cn(
|
|
40
|
+
'absolute -inset-1 rounded-[calc(var(--radius)+3px)] border-[3px]',
|
|
41
|
+
invalid
|
|
42
|
+
? 'border-destructive/16 dark:border-destructive/24'
|
|
43
|
+
: 'border-ring/24',
|
|
44
|
+
)}
|
|
45
|
+
pointerEvents="none"
|
|
46
|
+
/>
|
|
47
|
+
) : null}
|
|
48
|
+
</View>
|
|
49
|
+
);
|
|
50
|
+
}
|
|
@@ -1,5 +1,7 @@
|
|
|
1
|
+
import { type Icon as TablerIconComponent } from '@tabler/icons-react-native';
|
|
1
2
|
import { type ComponentType, type ReactNode } from 'react';
|
|
2
3
|
import { ScrollView, View } from 'react-native';
|
|
4
|
+
import { useResolveClassNames } from 'uniwind';
|
|
3
5
|
|
|
4
6
|
import { cn } from '../../lib/cn';
|
|
5
7
|
import { Text } from '../Text';
|
|
@@ -29,6 +31,37 @@ type ComponentMatrixRow = {
|
|
|
29
31
|
label: string;
|
|
30
32
|
};
|
|
31
33
|
|
|
34
|
+
/**
|
|
35
|
+
* A Tabler icon, the library the web stories use, dimmed and pulled in like
|
|
36
|
+
* the web `Button` styles its `svg`s. React Native has no `currentColor`, so
|
|
37
|
+
* `className` sets its color with the label's class (`text-primary-foreground`,
|
|
38
|
+
* …) and `size` follows the button's (14 for `xs`, 18 for `xl`).
|
|
39
|
+
*/
|
|
40
|
+
function Icon({
|
|
41
|
+
className,
|
|
42
|
+
icon: TablerIcon,
|
|
43
|
+
size = 16,
|
|
44
|
+
}: {
|
|
45
|
+
className?: string;
|
|
46
|
+
icon: TablerIconComponent;
|
|
47
|
+
size?: number;
|
|
48
|
+
}) {
|
|
49
|
+
const style = useResolveClassNames(
|
|
50
|
+
cn('text-foreground -mx-0.5 opacity-80', className),
|
|
51
|
+
);
|
|
52
|
+
|
|
53
|
+
return (
|
|
54
|
+
<TablerIcon
|
|
55
|
+
accessibilityElementsHidden
|
|
56
|
+
color={style.color}
|
|
57
|
+
importantForAccessibility="no-hide-descendants"
|
|
58
|
+
size={size}
|
|
59
|
+
strokeWidth={2}
|
|
60
|
+
style={style}
|
|
61
|
+
/>
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
|
|
32
65
|
// Same layout as `Story` in @pickle-ui/react, built on React Native
|
|
33
66
|
// primitives: a 12rem column of row labels, then one column per matrix
|
|
34
67
|
// column that grows from 12rem, like the web's `minmax(12rem, 1fr)`, since
|
|
@@ -135,6 +168,7 @@ function Section({ children, title }: { children: ReactNode; title: string }) {
|
|
|
135
168
|
}
|
|
136
169
|
|
|
137
170
|
const Story = Object.assign(Layout, {
|
|
171
|
+
Icon,
|
|
138
172
|
Layout,
|
|
139
173
|
Matrix,
|
|
140
174
|
Section,
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { View } from 'react-native';
|
|
2
|
+
|
|
3
|
+
import { cn } from '../../lib/cn';
|
|
4
|
+
import { Text } from '../Text';
|
|
5
|
+
|
|
6
|
+
/** A labelled box for the layout stories. */
|
|
7
|
+
export function StoryItem({
|
|
8
|
+
children,
|
|
9
|
+
className,
|
|
10
|
+
}: {
|
|
11
|
+
children: string;
|
|
12
|
+
className?: string;
|
|
13
|
+
}) {
|
|
14
|
+
return (
|
|
15
|
+
<View
|
|
16
|
+
className={cn(
|
|
17
|
+
'border-border bg-muted items-center justify-center rounded-md border px-4 py-3',
|
|
18
|
+
className,
|
|
19
|
+
)}
|
|
20
|
+
>
|
|
21
|
+
<Text>{children}</Text>
|
|
22
|
+
</View>
|
|
23
|
+
);
|
|
24
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { cn } from '../../lib/cn';
|
|
2
|
+
|
|
3
|
+
// Same props and spacing scale as the layout helper of @pickle-ui/react. Each
|
|
4
|
+
// class is written out in full so Uniwind finds it in the source.
|
|
5
|
+
|
|
6
|
+
const alignClasses = {
|
|
7
|
+
baseline: 'items-baseline',
|
|
8
|
+
center: 'items-center',
|
|
9
|
+
end: 'items-end',
|
|
10
|
+
start: 'items-start',
|
|
11
|
+
stretch: 'items-stretch',
|
|
12
|
+
} as const;
|
|
13
|
+
|
|
14
|
+
const gapClasses = {
|
|
15
|
+
0: 'gap-0',
|
|
16
|
+
0.5: 'gap-0.5',
|
|
17
|
+
1: 'gap-1',
|
|
18
|
+
1.5: 'gap-1.5',
|
|
19
|
+
2: 'gap-2',
|
|
20
|
+
2.5: 'gap-2.5',
|
|
21
|
+
3: 'gap-3',
|
|
22
|
+
3.5: 'gap-3.5',
|
|
23
|
+
4: 'gap-4',
|
|
24
|
+
5: 'gap-5',
|
|
25
|
+
6: 'gap-6',
|
|
26
|
+
8: 'gap-8',
|
|
27
|
+
10: 'gap-10',
|
|
28
|
+
12: 'gap-12',
|
|
29
|
+
16: 'gap-16',
|
|
30
|
+
} as const;
|
|
31
|
+
|
|
32
|
+
export const gapXClasses = {
|
|
33
|
+
0: 'gap-x-0',
|
|
34
|
+
0.5: 'gap-x-0.5',
|
|
35
|
+
1: 'gap-x-1',
|
|
36
|
+
1.5: 'gap-x-1.5',
|
|
37
|
+
2: 'gap-x-2',
|
|
38
|
+
2.5: 'gap-x-2.5',
|
|
39
|
+
3: 'gap-x-3',
|
|
40
|
+
3.5: 'gap-x-3.5',
|
|
41
|
+
4: 'gap-x-4',
|
|
42
|
+
5: 'gap-x-5',
|
|
43
|
+
6: 'gap-x-6',
|
|
44
|
+
8: 'gap-x-8',
|
|
45
|
+
10: 'gap-x-10',
|
|
46
|
+
12: 'gap-x-12',
|
|
47
|
+
16: 'gap-x-16',
|
|
48
|
+
} as const;
|
|
49
|
+
|
|
50
|
+
export const gapYClasses = {
|
|
51
|
+
0: 'gap-y-0',
|
|
52
|
+
0.5: 'gap-y-0.5',
|
|
53
|
+
1: 'gap-y-1',
|
|
54
|
+
1.5: 'gap-y-1.5',
|
|
55
|
+
2: 'gap-y-2',
|
|
56
|
+
2.5: 'gap-y-2.5',
|
|
57
|
+
3: 'gap-y-3',
|
|
58
|
+
3.5: 'gap-y-3.5',
|
|
59
|
+
4: 'gap-y-4',
|
|
60
|
+
5: 'gap-y-5',
|
|
61
|
+
6: 'gap-y-6',
|
|
62
|
+
8: 'gap-y-8',
|
|
63
|
+
10: 'gap-y-10',
|
|
64
|
+
12: 'gap-y-12',
|
|
65
|
+
16: 'gap-y-16',
|
|
66
|
+
} as const;
|
|
67
|
+
|
|
68
|
+
const justifyClasses = {
|
|
69
|
+
around: 'justify-around',
|
|
70
|
+
between: 'justify-between',
|
|
71
|
+
center: 'justify-center',
|
|
72
|
+
end: 'justify-end',
|
|
73
|
+
evenly: 'justify-evenly',
|
|
74
|
+
start: 'justify-start',
|
|
75
|
+
} as const;
|
|
76
|
+
|
|
77
|
+
export type LayoutAlign = keyof typeof alignClasses;
|
|
78
|
+
export type LayoutGap = keyof typeof gapClasses;
|
|
79
|
+
export type LayoutJustify = keyof typeof justifyClasses;
|
|
80
|
+
|
|
81
|
+
export type LayoutStyleProps = {
|
|
82
|
+
align?: LayoutAlign;
|
|
83
|
+
gap?: LayoutGap;
|
|
84
|
+
gapX?: LayoutGap;
|
|
85
|
+
gapY?: LayoutGap;
|
|
86
|
+
justify?: LayoutJustify;
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
export function getLayoutClasses({
|
|
90
|
+
align,
|
|
91
|
+
gap,
|
|
92
|
+
gapX,
|
|
93
|
+
gapY,
|
|
94
|
+
justify,
|
|
95
|
+
}: LayoutStyleProps) {
|
|
96
|
+
return cn(
|
|
97
|
+
align === undefined ? undefined : alignClasses[align],
|
|
98
|
+
gap === undefined ? undefined : gapClasses[gap],
|
|
99
|
+
gapX === undefined ? undefined : gapXClasses[gapX],
|
|
100
|
+
gapY === undefined ? undefined : gapYClasses[gapY],
|
|
101
|
+
justify === undefined ? undefined : justifyClasses[justify],
|
|
102
|
+
);
|
|
103
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { useCallback, useState } from 'react';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* State a component owns unless its parent controls it, like Base UI's
|
|
5
|
+
* `checked` / `defaultChecked` / `onCheckedChange` trios: `value` wins when
|
|
6
|
+
* defined, and every change is reported through `onChange`.
|
|
7
|
+
*/
|
|
8
|
+
export function useControllableState<T>(
|
|
9
|
+
value: T | undefined,
|
|
10
|
+
defaultValue: T,
|
|
11
|
+
onChange?: (value: T) => void,
|
|
12
|
+
) {
|
|
13
|
+
const [uncontrolled, setUncontrolled] = useState(defaultValue);
|
|
14
|
+
const controlled = value !== undefined;
|
|
15
|
+
|
|
16
|
+
const setValue = useCallback(
|
|
17
|
+
(next: T) => {
|
|
18
|
+
if (!controlled) setUncontrolled(next);
|
|
19
|
+
onChange?.(next);
|
|
20
|
+
},
|
|
21
|
+
[controlled, onChange],
|
|
22
|
+
);
|
|
23
|
+
|
|
24
|
+
return [controlled ? value : uncontrolled, setValue] as const;
|
|
25
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/react-native-web-vite';
|
|
2
|
+
|
|
3
|
+
import { type ComponentMatrixCellProps, Story } from '../_internal/Story';
|
|
4
|
+
import { Checkbox, type CheckboxProps } from './Checkbox';
|
|
5
|
+
|
|
6
|
+
const meta = {
|
|
7
|
+
component: Checkbox,
|
|
8
|
+
parameters: { layout: 'fullscreen' },
|
|
9
|
+
title: 'components/form/Checkbox',
|
|
10
|
+
} satisfies Meta<typeof Checkbox>;
|
|
11
|
+
|
|
12
|
+
export default meta;
|
|
13
|
+
type CheckboxSize = NonNullable<CheckboxProps['size']>;
|
|
14
|
+
type StoryDefinition = StoryObj<typeof meta>;
|
|
15
|
+
|
|
16
|
+
const STATE_COLUMNS: {
|
|
17
|
+
key: string;
|
|
18
|
+
label: string;
|
|
19
|
+
props: Partial<CheckboxProps>;
|
|
20
|
+
}[] = [
|
|
21
|
+
{ key: 'unchecked', label: 'Unchecked', props: {} },
|
|
22
|
+
{ key: 'checked', label: 'Checked', props: { defaultChecked: true } },
|
|
23
|
+
{ key: 'indeterminate', label: 'Mixed', props: { indeterminate: true } },
|
|
24
|
+
{
|
|
25
|
+
key: 'invalid',
|
|
26
|
+
label: 'Invalid',
|
|
27
|
+
props: { defaultChecked: true, invalid: true },
|
|
28
|
+
},
|
|
29
|
+
{ key: 'disabled', label: 'Disabled', props: { disabled: true } },
|
|
30
|
+
];
|
|
31
|
+
|
|
32
|
+
const SIZE_ROWS: { key: CheckboxSize; label: string }[] = [
|
|
33
|
+
{ key: 'md', label: 'Medium' },
|
|
34
|
+
{ key: 'sm', label: 'Small' },
|
|
35
|
+
];
|
|
36
|
+
|
|
37
|
+
const STATE_PROPS = Object.fromEntries(
|
|
38
|
+
STATE_COLUMNS.map((state) => [state.key, state.props]),
|
|
39
|
+
) as Record<string, Partial<CheckboxProps>>;
|
|
40
|
+
|
|
41
|
+
function CheckboxStateCell({ column, row }: ComponentMatrixCellProps) {
|
|
42
|
+
return (
|
|
43
|
+
<Checkbox
|
|
44
|
+
accessibilityLabel={`${row.label} ${column.label}`}
|
|
45
|
+
size={row.key as CheckboxSize}
|
|
46
|
+
{...STATE_PROPS[column.key]}
|
|
47
|
+
/>
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export const All: StoryDefinition = {
|
|
52
|
+
render: () => (
|
|
53
|
+
<Story.Layout className="max-w-5xl" title="Checkbox">
|
|
54
|
+
<Story.Section title="Sizes and states">
|
|
55
|
+
<Story.Matrix
|
|
56
|
+
Cell={CheckboxStateCell}
|
|
57
|
+
columns={STATE_COLUMNS}
|
|
58
|
+
rows={SIZE_ROWS}
|
|
59
|
+
/>
|
|
60
|
+
</Story.Section>
|
|
61
|
+
|
|
62
|
+
<Story.Section title="Label">
|
|
63
|
+
<Checkbox label="Accept terms" />
|
|
64
|
+
</Story.Section>
|
|
65
|
+
</Story.Layout>
|
|
66
|
+
),
|
|
67
|
+
};
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { fireEvent, render, screen } from '@testing-library/react-native';
|
|
2
|
+
|
|
3
|
+
import { Checkbox } from './Checkbox';
|
|
4
|
+
|
|
5
|
+
describe('Checkbox', () => {
|
|
6
|
+
it('renders unchecked by default', async () => {
|
|
7
|
+
await render(<Checkbox accessibilityLabel="Accept" />);
|
|
8
|
+
|
|
9
|
+
expect(screen.getByRole('checkbox', { name: 'Accept' })).not.toBeChecked();
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
it('toggles on press', async () => {
|
|
13
|
+
await render(<Checkbox accessibilityLabel="Accept" />);
|
|
14
|
+
const checkbox = screen.getByRole('checkbox', { name: 'Accept' });
|
|
15
|
+
|
|
16
|
+
await fireEvent.press(checkbox);
|
|
17
|
+
expect(checkbox).toBeChecked();
|
|
18
|
+
|
|
19
|
+
await fireEvent.press(checkbox);
|
|
20
|
+
expect(checkbox).not.toBeChecked();
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it('is named by its label, which toggles it', async () => {
|
|
24
|
+
const onCheckedChange = jest.fn();
|
|
25
|
+
await render(
|
|
26
|
+
<Checkbox label="Accept terms" onCheckedChange={onCheckedChange} />,
|
|
27
|
+
);
|
|
28
|
+
|
|
29
|
+
await fireEvent.press(screen.getByText('Accept terms'));
|
|
30
|
+
|
|
31
|
+
expect(
|
|
32
|
+
screen.getByRole('checkbox', { name: 'Accept terms' }),
|
|
33
|
+
).toBeChecked();
|
|
34
|
+
expect(onCheckedChange).toHaveBeenCalledWith(true);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('follows the checked prop when controlled', async () => {
|
|
38
|
+
const onCheckedChange = jest.fn();
|
|
39
|
+
await render(
|
|
40
|
+
<Checkbox
|
|
41
|
+
accessibilityLabel="Accept"
|
|
42
|
+
checked={false}
|
|
43
|
+
onCheckedChange={onCheckedChange}
|
|
44
|
+
/>,
|
|
45
|
+
);
|
|
46
|
+
|
|
47
|
+
await fireEvent.press(screen.getByRole('checkbox'));
|
|
48
|
+
|
|
49
|
+
expect(onCheckedChange).toHaveBeenCalledWith(true);
|
|
50
|
+
expect(screen.getByRole('checkbox')).not.toBeChecked();
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it('reports the mixed state when indeterminate', async () => {
|
|
54
|
+
await render(<Checkbox accessibilityLabel="Partial" indeterminate />);
|
|
55
|
+
|
|
56
|
+
expect(
|
|
57
|
+
screen.getByRole('checkbox', { name: 'Partial' }),
|
|
58
|
+
).toBePartiallyChecked();
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it('is disabled and ignores presses', async () => {
|
|
62
|
+
await render(<Checkbox accessibilityLabel="Accept" disabled />);
|
|
63
|
+
const checkbox = screen.getByRole('checkbox', { name: 'Accept' });
|
|
64
|
+
|
|
65
|
+
await fireEvent.press(checkbox);
|
|
66
|
+
|
|
67
|
+
expect(checkbox).toBeDisabled();
|
|
68
|
+
expect(checkbox).not.toBeChecked();
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it('reaches a 44pt touch target', async () => {
|
|
72
|
+
await render(
|
|
73
|
+
<>
|
|
74
|
+
<Checkbox accessibilityLabel="Box" />
|
|
75
|
+
<Checkbox label="Row" size="sm" />
|
|
76
|
+
</>,
|
|
77
|
+
);
|
|
78
|
+
|
|
79
|
+
expect(screen.getByRole('checkbox', { name: 'Box' })).toHaveProp(
|
|
80
|
+
'hitSlop',
|
|
81
|
+
13,
|
|
82
|
+
);
|
|
83
|
+
expect(screen.getByRole('checkbox', { name: 'Row' })).toHaveProp(
|
|
84
|
+
'hitSlop',
|
|
85
|
+
{ bottom: 15, top: 15 },
|
|
86
|
+
);
|
|
87
|
+
});
|
|
88
|
+
});
|