@pickle-ui/react-native 0.0.0-stage → 0.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +92 -2
- package/dist/components/Badge.d.ts +11 -0
- package/dist/components/Badge.d.ts.map +1 -0
- package/dist/components/Badge.js +33 -0
- package/dist/components/Badge.js.map +1 -0
- package/dist/components/Button.d.ts +13 -0
- package/dist/components/Button.d.ts.map +1 -0
- package/dist/components/Button.js +39 -0
- package/dist/components/Button.js.map +1 -0
- package/dist/components/Card.d.ts +20 -0
- package/dist/components/Card.d.ts.map +1 -0
- package/dist/components/Card.js +29 -0
- package/dist/components/Card.js.map +1 -0
- package/dist/components/Text.d.ts +10 -0
- package/dist/components/Text.d.ts.map +1 -0
- package/dist/components/Text.js +28 -0
- package/dist/components/Text.js.map +1 -0
- package/dist/entries/badge.d.ts +3 -0
- package/dist/entries/badge.d.ts.map +1 -0
- package/dist/entries/badge.js +2 -0
- package/dist/entries/badge.js.map +1 -0
- package/dist/entries/button.d.ts +3 -0
- package/dist/entries/button.d.ts.map +1 -0
- package/dist/entries/button.js +2 -0
- package/dist/entries/button.js.map +1 -0
- package/dist/entries/card.d.ts +3 -0
- package/dist/entries/card.d.ts.map +1 -0
- package/dist/entries/card.js +2 -0
- package/dist/entries/card.js.map +1 -0
- package/dist/entries/cn.d.ts +2 -0
- package/dist/entries/cn.d.ts.map +1 -0
- package/dist/entries/cn.js +2 -0
- package/dist/entries/cn.js.map +1 -0
- package/dist/entries/text.d.ts +3 -0
- package/dist/entries/text.d.ts.map +1 -0
- package/dist/entries/text.js +2 -0
- package/dist/entries/text.js.map +1 -0
- package/dist/entries/use-color-token.d.ts +2 -0
- package/dist/entries/use-color-token.d.ts.map +1 -0
- package/dist/entries/use-color-token.js +2 -0
- package/dist/entries/use-color-token.js.map +1 -0
- package/dist/hooks/useColorToken.d.ts +9 -0
- package/dist/hooks/useColorToken.d.ts.map +1 -0
- package/dist/hooks/useColorToken.js +13 -0
- package/dist/hooks/useColorToken.js.map +1 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +9 -0
- package/dist/index.js.map +1 -0
- package/dist/lib/cn.d.ts +3 -0
- package/dist/lib/cn.d.ts.map +1 -0
- package/dist/lib/cn.js +6 -0
- package/dist/lib/cn.js.map +1 -0
- package/package.json +91 -3
- package/src/base.css +6 -0
- package/src/components/Badge.stories.tsx +47 -0
- package/src/components/Badge.test.tsx +11 -0
- package/src/components/Badge.tsx +42 -0
- package/src/components/Button.stories.tsx +87 -0
- package/src/components/Button.test.tsx +34 -0
- package/src/components/Button.tsx +70 -0
- package/src/components/Card.stories.tsx +55 -0
- package/src/components/Card.test.tsx +23 -0
- package/src/components/Card.tsx +55 -0
- package/src/components/Text.stories.tsx +60 -0
- package/src/components/Text.test.tsx +15 -0
- package/src/components/Text.tsx +37 -0
- package/src/components/_internal/Story.tsx +148 -0
- package/src/entries/badge.ts +2 -0
- package/src/entries/button.ts +2 -0
- package/src/entries/card.ts +8 -0
- package/src/entries/cn.ts +1 -0
- package/src/entries/text.ts +2 -0
- package/src/entries/use-color-token.ts +1 -0
- package/src/hooks/useColorToken.ts +13 -0
- package/src/index.ts +8 -0
- package/src/lib/cn.test.ts +9 -0
- package/src/lib/cn.ts +6 -0
- package/src/styles.css +12 -0
- package/src/uniwind-types.d.ts +10 -0
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/react-native-web-vite';
|
|
2
|
+
|
|
3
|
+
import { type ComponentMatrixCellProps, Story } from './_internal/Story';
|
|
4
|
+
import { Badge, type BadgeProps } from './Badge';
|
|
5
|
+
|
|
6
|
+
const meta = {
|
|
7
|
+
component: Badge,
|
|
8
|
+
parameters: { layout: 'fullscreen' },
|
|
9
|
+
title: 'components/Badge',
|
|
10
|
+
} satisfies Meta<typeof Badge>;
|
|
11
|
+
|
|
12
|
+
export default meta;
|
|
13
|
+
type StoryDefinition = StoryObj<typeof meta>;
|
|
14
|
+
|
|
15
|
+
const DISPLAY_COLUMNS = [{ key: 'label', label: 'Label only' }];
|
|
16
|
+
|
|
17
|
+
const SEMANTIC_ROWS = [
|
|
18
|
+
{ key: 'default', label: 'Default' },
|
|
19
|
+
{ key: 'secondary', label: 'Secondary' },
|
|
20
|
+
{ key: 'inProgress', label: 'In progress' },
|
|
21
|
+
{ key: 'inReview', label: 'In review' },
|
|
22
|
+
{ key: 'success', label: 'Success' },
|
|
23
|
+
];
|
|
24
|
+
|
|
25
|
+
function SemanticBadgeCell({ row }: ComponentMatrixCellProps) {
|
|
26
|
+
return (
|
|
27
|
+
<Badge variant={row.key as NonNullable<BadgeProps['variant']>}>
|
|
28
|
+
{row.label}
|
|
29
|
+
</Badge>
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export const All: StoryDefinition = {
|
|
34
|
+
args: { children: 'Badge' },
|
|
35
|
+
render: () => (
|
|
36
|
+
<Story.Layout className="max-w-4xl" title="Badge">
|
|
37
|
+
<Story.Section title="Semantic badges">
|
|
38
|
+
<Story.Matrix
|
|
39
|
+
Cell={SemanticBadgeCell}
|
|
40
|
+
cellClassName="min-h-16"
|
|
41
|
+
columns={DISPLAY_COLUMNS}
|
|
42
|
+
rows={SEMANTIC_ROWS}
|
|
43
|
+
/>
|
|
44
|
+
</Story.Section>
|
|
45
|
+
</Story.Layout>
|
|
46
|
+
),
|
|
47
|
+
};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { render, screen } from '@testing-library/react-native';
|
|
2
|
+
|
|
3
|
+
import { Badge } from './Badge';
|
|
4
|
+
|
|
5
|
+
describe('Badge', () => {
|
|
6
|
+
it('renders its label', async () => {
|
|
7
|
+
await render(<Badge variant="success">Shipped</Badge>);
|
|
8
|
+
|
|
9
|
+
expect(screen.getByText('Shipped')).toBeOnTheScreen();
|
|
10
|
+
});
|
|
11
|
+
});
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { cva, type VariantProps } from 'class-variance-authority';
|
|
2
|
+
import { Text, View } from 'react-native';
|
|
3
|
+
|
|
4
|
+
import { cn } from '../lib/cn';
|
|
5
|
+
|
|
6
|
+
// Status variants use the `-subtle` token pairs, like `Badge` on the web.
|
|
7
|
+
const badgeVariants = cva('self-start rounded-sm px-1.5 py-0.5', {
|
|
8
|
+
defaultVariants: { variant: 'default' },
|
|
9
|
+
variants: {
|
|
10
|
+
variant: {
|
|
11
|
+
default: 'bg-primary',
|
|
12
|
+
inProgress: 'bg-info-subtle',
|
|
13
|
+
inReview: 'bg-warning-subtle',
|
|
14
|
+
secondary: 'bg-secondary',
|
|
15
|
+
success: 'bg-success-subtle',
|
|
16
|
+
},
|
|
17
|
+
},
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
const labelVariants = cva('text-xs font-medium', {
|
|
21
|
+
defaultVariants: { variant: 'default' },
|
|
22
|
+
variants: {
|
|
23
|
+
variant: {
|
|
24
|
+
default: 'text-primary-foreground',
|
|
25
|
+
inProgress: 'text-info-subtle-foreground',
|
|
26
|
+
inReview: 'text-warning-subtle-foreground',
|
|
27
|
+
secondary: 'text-secondary-foreground',
|
|
28
|
+
success: 'text-success-subtle-foreground',
|
|
29
|
+
},
|
|
30
|
+
},
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
export type BadgeProps = React.ComponentProps<typeof View> &
|
|
34
|
+
VariantProps<typeof badgeVariants> & { children: string };
|
|
35
|
+
|
|
36
|
+
export function Badge({ children, className, variant, ...props }: BadgeProps) {
|
|
37
|
+
return (
|
|
38
|
+
<View className={cn(badgeVariants({ variant }), className)} {...props}>
|
|
39
|
+
<Text className={labelVariants({ variant })}>{children}</Text>
|
|
40
|
+
</View>
|
|
41
|
+
);
|
|
42
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/react-native-web-vite';
|
|
2
|
+
|
|
3
|
+
import { type ComponentMatrixCellProps, Story } from './_internal/Story';
|
|
4
|
+
import { Button, type ButtonProps } from './Button';
|
|
5
|
+
|
|
6
|
+
const meta = {
|
|
7
|
+
component: Button,
|
|
8
|
+
parameters: {
|
|
9
|
+
layout: 'fullscreen',
|
|
10
|
+
},
|
|
11
|
+
title: 'components/Button',
|
|
12
|
+
} satisfies Meta<typeof Button>;
|
|
13
|
+
|
|
14
|
+
export default meta;
|
|
15
|
+
type ButtonSize = NonNullable<ButtonProps['size']>;
|
|
16
|
+
type ButtonVariant = NonNullable<ButtonProps['variant']>;
|
|
17
|
+
type StoryDefinition = StoryObj<typeof meta>;
|
|
18
|
+
|
|
19
|
+
const STATE_COLUMNS: {
|
|
20
|
+
key: string;
|
|
21
|
+
label: string;
|
|
22
|
+
props: Partial<ButtonProps>;
|
|
23
|
+
}[] = [
|
|
24
|
+
{ key: 'default', label: 'Default', props: {} },
|
|
25
|
+
{ key: 'disabled', label: 'Disabled', props: { disabled: true } },
|
|
26
|
+
];
|
|
27
|
+
|
|
28
|
+
const STATE_PROPS = Object.fromEntries(
|
|
29
|
+
STATE_COLUMNS.map((state) => [state.key, state.props]),
|
|
30
|
+
) as Record<string, Partial<ButtonProps>>;
|
|
31
|
+
|
|
32
|
+
const VARIANT_ROWS: {
|
|
33
|
+
key: ButtonVariant;
|
|
34
|
+
label: string;
|
|
35
|
+
}[] = [
|
|
36
|
+
{ key: 'default', label: 'Default' },
|
|
37
|
+
{ key: 'secondary', label: 'Secondary' },
|
|
38
|
+
{ key: 'destructive', label: 'Destructive' },
|
|
39
|
+
{ key: 'outline', label: 'Outline' },
|
|
40
|
+
{ key: 'ghost', label: 'Ghost' },
|
|
41
|
+
];
|
|
42
|
+
|
|
43
|
+
const SIZE_COLUMNS: { description: string; key: ButtonSize; label: string }[] =
|
|
44
|
+
[
|
|
45
|
+
{ description: 'h-8', key: 'sm', label: 'Small' },
|
|
46
|
+
{ description: 'h-10', key: 'md', label: 'Medium' },
|
|
47
|
+
{ description: 'h-11', key: 'lg', label: 'Large' },
|
|
48
|
+
];
|
|
49
|
+
|
|
50
|
+
function ButtonSizeCell({ column, row }: ComponentMatrixCellProps) {
|
|
51
|
+
return (
|
|
52
|
+
<Button size={column.key as ButtonSize} variant={row.key as ButtonVariant}>
|
|
53
|
+
Add
|
|
54
|
+
</Button>
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function ButtonStateCell({ column, row }: ComponentMatrixCellProps) {
|
|
59
|
+
return (
|
|
60
|
+
<Button variant={row.key as ButtonVariant} {...STATE_PROPS[column.key]}>
|
|
61
|
+
Button
|
|
62
|
+
</Button>
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export const All: StoryDefinition = {
|
|
67
|
+
args: { children: 'Button' },
|
|
68
|
+
render: () => (
|
|
69
|
+
<Story.Layout className="max-w-6xl" title="Button">
|
|
70
|
+
<Story.Section title="Variants and states">
|
|
71
|
+
<Story.Matrix
|
|
72
|
+
Cell={ButtonStateCell}
|
|
73
|
+
columns={STATE_COLUMNS}
|
|
74
|
+
rows={VARIANT_ROWS}
|
|
75
|
+
/>
|
|
76
|
+
</Story.Section>
|
|
77
|
+
|
|
78
|
+
<Story.Section title="Sizes">
|
|
79
|
+
<Story.Matrix
|
|
80
|
+
Cell={ButtonSizeCell}
|
|
81
|
+
columns={SIZE_COLUMNS}
|
|
82
|
+
rows={VARIANT_ROWS}
|
|
83
|
+
/>
|
|
84
|
+
</Story.Section>
|
|
85
|
+
</Story.Layout>
|
|
86
|
+
),
|
|
87
|
+
};
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { fireEvent, render, screen } from '@testing-library/react-native';
|
|
2
|
+
|
|
3
|
+
import { Button } from './Button';
|
|
4
|
+
|
|
5
|
+
describe('Button', () => {
|
|
6
|
+
it('renders a button named by its label', async () => {
|
|
7
|
+
await render(<Button>Save</Button>);
|
|
8
|
+
|
|
9
|
+
expect(screen.getByRole('button', { name: 'Save' })).toBeOnTheScreen();
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
it('calls onPress', async () => {
|
|
13
|
+
const onPress = jest.fn();
|
|
14
|
+
await render(<Button onPress={onPress}>Save</Button>);
|
|
15
|
+
|
|
16
|
+
await fireEvent.press(screen.getByRole('button'));
|
|
17
|
+
|
|
18
|
+
expect(onPress).toHaveBeenCalledTimes(1);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it('does not call onPress when disabled', async () => {
|
|
22
|
+
const onPress = jest.fn();
|
|
23
|
+
await render(
|
|
24
|
+
<Button disabled onPress={onPress}>
|
|
25
|
+
Save
|
|
26
|
+
</Button>,
|
|
27
|
+
);
|
|
28
|
+
|
|
29
|
+
await fireEvent.press(screen.getByRole('button'));
|
|
30
|
+
|
|
31
|
+
expect(screen.getByRole('button')).toBeDisabled();
|
|
32
|
+
expect(onPress).not.toHaveBeenCalled();
|
|
33
|
+
});
|
|
34
|
+
});
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { cva, type VariantProps } from 'class-variance-authority';
|
|
2
|
+
import { Pressable, Text } from 'react-native';
|
|
3
|
+
|
|
4
|
+
import { cn } from '../lib/cn';
|
|
5
|
+
|
|
6
|
+
// Same variant and size names as `Button` in @pickle-ui/react.
|
|
7
|
+
const buttonVariants = cva(
|
|
8
|
+
'flex-row items-center justify-center gap-2 rounded-lg border active:opacity-80 disabled:opacity-60',
|
|
9
|
+
{
|
|
10
|
+
defaultVariants: { size: 'md', variant: 'default' },
|
|
11
|
+
variants: {
|
|
12
|
+
size: {
|
|
13
|
+
lg: 'h-11 px-5',
|
|
14
|
+
md: 'h-10 px-4',
|
|
15
|
+
sm: 'h-8 px-3',
|
|
16
|
+
},
|
|
17
|
+
variant: {
|
|
18
|
+
default: 'border-primary bg-primary',
|
|
19
|
+
destructive: 'border-destructive bg-destructive',
|
|
20
|
+
ghost: 'border-transparent active:bg-accent',
|
|
21
|
+
outline: 'border-input bg-popover',
|
|
22
|
+
secondary: 'border-transparent bg-secondary',
|
|
23
|
+
},
|
|
24
|
+
},
|
|
25
|
+
},
|
|
26
|
+
);
|
|
27
|
+
|
|
28
|
+
const labelVariants = cva('font-medium', {
|
|
29
|
+
defaultVariants: { size: 'md', variant: 'default' },
|
|
30
|
+
variants: {
|
|
31
|
+
size: { lg: 'text-base', md: 'text-sm', sm: 'text-xs' },
|
|
32
|
+
variant: {
|
|
33
|
+
default: 'text-primary-foreground',
|
|
34
|
+
destructive: 'text-destructive-foreground',
|
|
35
|
+
ghost: 'text-foreground',
|
|
36
|
+
outline: 'text-foreground',
|
|
37
|
+
secondary: 'text-secondary-foreground',
|
|
38
|
+
},
|
|
39
|
+
},
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
export type ButtonProps = Omit<
|
|
43
|
+
React.ComponentProps<typeof Pressable>,
|
|
44
|
+
'children'
|
|
45
|
+
> &
|
|
46
|
+
VariantProps<typeof buttonVariants> & {
|
|
47
|
+
children: string;
|
|
48
|
+
labelClassName?: string;
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
export function Button({
|
|
52
|
+
children,
|
|
53
|
+
className,
|
|
54
|
+
labelClassName,
|
|
55
|
+
size,
|
|
56
|
+
variant,
|
|
57
|
+
...props
|
|
58
|
+
}: ButtonProps) {
|
|
59
|
+
return (
|
|
60
|
+
<Pressable
|
|
61
|
+
accessibilityRole="button"
|
|
62
|
+
className={cn(buttonVariants({ size, variant }), className)}
|
|
63
|
+
{...props}
|
|
64
|
+
>
|
|
65
|
+
<Text className={cn(labelVariants({ size, variant }), labelClassName)}>
|
|
66
|
+
{children}
|
|
67
|
+
</Text>
|
|
68
|
+
</Pressable>
|
|
69
|
+
);
|
|
70
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/react-native-web-vite';
|
|
2
|
+
|
|
3
|
+
import { View } from 'react-native';
|
|
4
|
+
|
|
5
|
+
import { Story } from './_internal/Story';
|
|
6
|
+
import { Button } from './Button';
|
|
7
|
+
import { Card } from './Card';
|
|
8
|
+
import { Text } from './Text';
|
|
9
|
+
|
|
10
|
+
const meta = {
|
|
11
|
+
component: Card,
|
|
12
|
+
parameters: { layout: 'fullscreen' },
|
|
13
|
+
title: 'components/Card',
|
|
14
|
+
} satisfies Meta<typeof Card>;
|
|
15
|
+
|
|
16
|
+
export default meta;
|
|
17
|
+
type StoryDefinition = StoryObj<typeof meta>;
|
|
18
|
+
|
|
19
|
+
export const All: StoryDefinition = {
|
|
20
|
+
render: () => (
|
|
21
|
+
<Story.Layout className="max-w-5xl" title="Card">
|
|
22
|
+
<Story.Section title="Composition">
|
|
23
|
+
<View className="max-w-lg gap-6">
|
|
24
|
+
<Card>
|
|
25
|
+
<Card.Header>
|
|
26
|
+
<Card.Title>Project activity</Card.Title>
|
|
27
|
+
<Card.Description>
|
|
28
|
+
Changes made in the last week.
|
|
29
|
+
</Card.Description>
|
|
30
|
+
</Card.Header>
|
|
31
|
+
<Card.Content className="gap-2">
|
|
32
|
+
<Text variant="h1">1,284</Text>
|
|
33
|
+
<Text tone="muted">Events across 8 active projects</Text>
|
|
34
|
+
<Button className="self-end" size="sm" variant="ghost">
|
|
35
|
+
View report
|
|
36
|
+
</Button>
|
|
37
|
+
</Card.Content>
|
|
38
|
+
</Card>
|
|
39
|
+
|
|
40
|
+
<Card>
|
|
41
|
+
<Card.Header>
|
|
42
|
+
<Card.Title>Simple card</Card.Title>
|
|
43
|
+
<Card.Description>
|
|
44
|
+
Parts can be omitted when the content needs less structure.
|
|
45
|
+
</Card.Description>
|
|
46
|
+
</Card.Header>
|
|
47
|
+
<Card.Content>
|
|
48
|
+
<Button>Continue</Button>
|
|
49
|
+
</Card.Content>
|
|
50
|
+
</Card>
|
|
51
|
+
</View>
|
|
52
|
+
</Story.Section>
|
|
53
|
+
</Story.Layout>
|
|
54
|
+
),
|
|
55
|
+
};
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { render, screen } from '@testing-library/react-native';
|
|
2
|
+
|
|
3
|
+
import { Card } from './Card';
|
|
4
|
+
|
|
5
|
+
describe('Card', () => {
|
|
6
|
+
it('renders its compound parts', async () => {
|
|
7
|
+
await render(
|
|
8
|
+
<Card testID="card">
|
|
9
|
+
<Card.Header>
|
|
10
|
+
<Card.Title>Projects</Card.Title>
|
|
11
|
+
<Card.Description>Everything in flight</Card.Description>
|
|
12
|
+
</Card.Header>
|
|
13
|
+
<Card.Content testID="content" />
|
|
14
|
+
</Card>,
|
|
15
|
+
);
|
|
16
|
+
|
|
17
|
+
expect(screen.getByTestId('card')).toContainElement(
|
|
18
|
+
screen.getByText('Projects'),
|
|
19
|
+
);
|
|
20
|
+
expect(screen.getByText('Everything in flight')).toBeOnTheScreen();
|
|
21
|
+
expect(screen.getByTestId('content')).toBeOnTheScreen();
|
|
22
|
+
});
|
|
23
|
+
});
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { View } from 'react-native';
|
|
2
|
+
|
|
3
|
+
import { cn } from '../lib/cn';
|
|
4
|
+
import { Text } from './Text';
|
|
5
|
+
|
|
6
|
+
export type CardContentProps = React.ComponentProps<typeof View>;
|
|
7
|
+
export type CardDescriptionProps = React.ComponentProps<typeof Text>;
|
|
8
|
+
export type CardHeaderProps = React.ComponentProps<typeof View>;
|
|
9
|
+
export type CardProps = React.ComponentProps<typeof View>;
|
|
10
|
+
export type CardTitleProps = React.ComponentProps<typeof Text>;
|
|
11
|
+
|
|
12
|
+
function CardContent({ className, ...props }: CardContentProps) {
|
|
13
|
+
return <View className={cn('gap-3 px-5', className)} {...props} />;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function CardDescription({ className, ...props }: CardDescriptionProps) {
|
|
17
|
+
return <Text className={className} tone="muted" {...props} />;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function CardHeader({ className, ...props }: CardHeaderProps) {
|
|
21
|
+
return <View className={cn('gap-1 px-5', className)} {...props} />;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function CardRoot({ className, ...props }: CardProps) {
|
|
25
|
+
return (
|
|
26
|
+
<View
|
|
27
|
+
className={cn(
|
|
28
|
+
'border-border bg-card gap-4 rounded-xl border py-5',
|
|
29
|
+
className,
|
|
30
|
+
)}
|
|
31
|
+
{...props}
|
|
32
|
+
/>
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function CardTitle({ className, ...props }: CardTitleProps) {
|
|
37
|
+
return (
|
|
38
|
+
<Text
|
|
39
|
+
className={cn('text-card-foreground', className)}
|
|
40
|
+
variant="h4"
|
|
41
|
+
{...props}
|
|
42
|
+
/>
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
|
|
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
|
+
const Card = Object.assign(CardRoot, {
|
|
49
|
+
Content: CardContent,
|
|
50
|
+
Description: CardDescription,
|
|
51
|
+
Header: CardHeader,
|
|
52
|
+
Title: CardTitle,
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
export { Card, CardContent, CardDescription, CardHeader, CardTitle };
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/react-native-web-vite';
|
|
2
|
+
|
|
3
|
+
import { type ComponentMatrixCellProps, Story } from './_internal/Story';
|
|
4
|
+
import { Text, type TextProps } from './Text';
|
|
5
|
+
|
|
6
|
+
const meta = {
|
|
7
|
+
component: Text,
|
|
8
|
+
parameters: {
|
|
9
|
+
layout: 'fullscreen',
|
|
10
|
+
},
|
|
11
|
+
title: 'components/Text',
|
|
12
|
+
} satisfies Meta<typeof Text>;
|
|
13
|
+
|
|
14
|
+
export default meta;
|
|
15
|
+
type StoryDefinition = StoryObj<typeof meta>;
|
|
16
|
+
|
|
17
|
+
type TextTone = NonNullable<TextProps['tone']>;
|
|
18
|
+
type TextVariant = NonNullable<TextProps['variant']>;
|
|
19
|
+
|
|
20
|
+
const VARIANT_ROWS: { key: TextVariant; label: string }[] = [
|
|
21
|
+
{ key: 'h1', label: 'H1' },
|
|
22
|
+
{ key: 'h2', label: 'H2' },
|
|
23
|
+
{ key: 'h3', label: 'H3' },
|
|
24
|
+
{ key: 'h4', label: 'H4' },
|
|
25
|
+
{ key: 'lead', label: 'Lead' },
|
|
26
|
+
{ key: 'body', label: 'Body' },
|
|
27
|
+
{ key: 'small', label: 'Small' },
|
|
28
|
+
];
|
|
29
|
+
|
|
30
|
+
const TONE_COLUMNS: { key: 'default' | TextTone; label: string }[] = [
|
|
31
|
+
{ key: 'default', label: 'Default' },
|
|
32
|
+
{ key: 'primary', label: 'Primary' },
|
|
33
|
+
{ key: 'muted', label: 'Muted' },
|
|
34
|
+
{ key: 'destructive', label: 'Destructive' },
|
|
35
|
+
];
|
|
36
|
+
|
|
37
|
+
function ToneCell({ column, row }: ComponentMatrixCellProps) {
|
|
38
|
+
return (
|
|
39
|
+
<Text
|
|
40
|
+
tone={column.key === 'default' ? undefined : (column.key as TextTone)}
|
|
41
|
+
variant={row.key as TextVariant}
|
|
42
|
+
>
|
|
43
|
+
Sample text
|
|
44
|
+
</Text>
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export const All: StoryDefinition = {
|
|
49
|
+
render: () => (
|
|
50
|
+
<Story.Layout className="max-w-6xl" title="Text">
|
|
51
|
+
<Story.Section title="Variants and tones">
|
|
52
|
+
<Story.Matrix
|
|
53
|
+
Cell={ToneCell}
|
|
54
|
+
columns={TONE_COLUMNS}
|
|
55
|
+
rows={VARIANT_ROWS}
|
|
56
|
+
/>
|
|
57
|
+
</Story.Section>
|
|
58
|
+
</Story.Layout>
|
|
59
|
+
),
|
|
60
|
+
};
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { render, screen } from '@testing-library/react-native';
|
|
2
|
+
|
|
3
|
+
import { Text } from './Text';
|
|
4
|
+
|
|
5
|
+
describe('Text', () => {
|
|
6
|
+
it('renders its children', async () => {
|
|
7
|
+
await render(
|
|
8
|
+
<Text tone="muted" variant="h1">
|
|
9
|
+
Hello
|
|
10
|
+
</Text>,
|
|
11
|
+
);
|
|
12
|
+
|
|
13
|
+
expect(screen.getByText('Hello')).toBeOnTheScreen();
|
|
14
|
+
});
|
|
15
|
+
});
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { cva, type VariantProps } from 'class-variance-authority';
|
|
2
|
+
import { Text as NativeText } from 'react-native';
|
|
3
|
+
|
|
4
|
+
import { cn } from '../lib/cn';
|
|
5
|
+
|
|
6
|
+
// Same variant and tone names as `Text` in @pickle-ui/react.
|
|
7
|
+
const textVariants = cva('text-foreground', {
|
|
8
|
+
defaultVariants: { variant: 'body' },
|
|
9
|
+
variants: {
|
|
10
|
+
tone: {
|
|
11
|
+
destructive: 'text-destructive',
|
|
12
|
+
muted: 'text-muted-foreground',
|
|
13
|
+
primary: 'text-primary',
|
|
14
|
+
},
|
|
15
|
+
variant: {
|
|
16
|
+
body: 'text-sm',
|
|
17
|
+
h1: 'text-3xl font-semibold tracking-tight',
|
|
18
|
+
h2: 'text-2xl font-semibold tracking-tight',
|
|
19
|
+
h3: 'text-xl font-semibold',
|
|
20
|
+
h4: 'text-lg font-medium',
|
|
21
|
+
lead: 'text-base',
|
|
22
|
+
small: 'text-xs',
|
|
23
|
+
},
|
|
24
|
+
},
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
export type TextProps = React.ComponentProps<typeof NativeText> &
|
|
28
|
+
VariantProps<typeof textVariants>;
|
|
29
|
+
|
|
30
|
+
export function Text({ className, tone, variant, ...props }: TextProps) {
|
|
31
|
+
return (
|
|
32
|
+
<NativeText
|
|
33
|
+
className={cn(textVariants({ tone, variant }), className)}
|
|
34
|
+
{...props}
|
|
35
|
+
/>
|
|
36
|
+
);
|
|
37
|
+
}
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import { type ComponentType, type ReactNode } from 'react';
|
|
2
|
+
import { ScrollView, View } from 'react-native';
|
|
3
|
+
|
|
4
|
+
import { cn } from '../../lib/cn';
|
|
5
|
+
import { Text } from '../Text';
|
|
6
|
+
|
|
7
|
+
type ComponentMatrixCellProps = {
|
|
8
|
+
column: ComponentMatrixColumn;
|
|
9
|
+
row: ComponentMatrixRow;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
type ComponentMatrixColumn = {
|
|
13
|
+
description?: string;
|
|
14
|
+
key: string;
|
|
15
|
+
label: string;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
type ComponentMatrixProps = {
|
|
19
|
+
Cell: ComponentType<ComponentMatrixCellProps>;
|
|
20
|
+
cellClassName?: string;
|
|
21
|
+
className?: string;
|
|
22
|
+
columns: ComponentMatrixColumn[];
|
|
23
|
+
rows: ComponentMatrixRow[];
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
type ComponentMatrixRow = {
|
|
27
|
+
description?: string;
|
|
28
|
+
key: string;
|
|
29
|
+
label: string;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
// Same layout as `Story` in @pickle-ui/react, built on React Native
|
|
33
|
+
// primitives: a 12rem column of row labels, then one column per matrix
|
|
34
|
+
// column that grows from 12rem, like the web's `minmax(12rem, 1fr)`, since
|
|
35
|
+
// React Native has no CSS grid.
|
|
36
|
+
function Layout({
|
|
37
|
+
children,
|
|
38
|
+
className,
|
|
39
|
+
title,
|
|
40
|
+
}: {
|
|
41
|
+
children: ReactNode;
|
|
42
|
+
className?: string;
|
|
43
|
+
title: string;
|
|
44
|
+
}) {
|
|
45
|
+
return (
|
|
46
|
+
<View className="bg-background min-h-screen">
|
|
47
|
+
<View className={cn('px-8 py-12', className)}>
|
|
48
|
+
<View className="mb-12">
|
|
49
|
+
<Text variant="h1">{title}</Text>
|
|
50
|
+
</View>
|
|
51
|
+
<View className="gap-16">{children}</View>
|
|
52
|
+
</View>
|
|
53
|
+
</View>
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function Matrix({
|
|
58
|
+
Cell,
|
|
59
|
+
cellClassName,
|
|
60
|
+
className,
|
|
61
|
+
columns,
|
|
62
|
+
rows,
|
|
63
|
+
}: ComponentMatrixProps) {
|
|
64
|
+
return (
|
|
65
|
+
<ScrollView
|
|
66
|
+
className={cn('border-border bg-card/20 rounded-md border', className)}
|
|
67
|
+
contentContainerClassName="min-w-full"
|
|
68
|
+
horizontal
|
|
69
|
+
>
|
|
70
|
+
<View className="flex-1">
|
|
71
|
+
<View className="flex-row">
|
|
72
|
+
<View className="border-border bg-muted/30 w-48 border-r border-b p-3" />
|
|
73
|
+
{columns.map((column, columnIndex) => (
|
|
74
|
+
<View
|
|
75
|
+
className={cn(
|
|
76
|
+
'border-border bg-muted/30 min-w-48 flex-1 border-b p-3',
|
|
77
|
+
columnIndex < columns.length - 1 && 'border-r',
|
|
78
|
+
)}
|
|
79
|
+
key={column.key}
|
|
80
|
+
>
|
|
81
|
+
<Text className="font-medium">{column.label}</Text>
|
|
82
|
+
{column.description ? (
|
|
83
|
+
<Text className="mt-1" tone="muted" variant="small">
|
|
84
|
+
{column.description}
|
|
85
|
+
</Text>
|
|
86
|
+
) : null}
|
|
87
|
+
</View>
|
|
88
|
+
))}
|
|
89
|
+
</View>
|
|
90
|
+
|
|
91
|
+
{rows.map((row, rowIndex) => (
|
|
92
|
+
<View className="flex-row" key={row.key}>
|
|
93
|
+
<View
|
|
94
|
+
className={cn(
|
|
95
|
+
'border-border bg-background/80 w-48 border-r p-3',
|
|
96
|
+
rowIndex < rows.length - 1 && 'border-b',
|
|
97
|
+
)}
|
|
98
|
+
>
|
|
99
|
+
<Text className="font-medium">{row.label}</Text>
|
|
100
|
+
{row.description ? (
|
|
101
|
+
<Text className="mt-1 max-w-40" tone="muted" variant="small">
|
|
102
|
+
{row.description}
|
|
103
|
+
</Text>
|
|
104
|
+
) : null}
|
|
105
|
+
</View>
|
|
106
|
+
{columns.map((column, columnIndex) => (
|
|
107
|
+
<View
|
|
108
|
+
className={cn(
|
|
109
|
+
'border-border bg-background/55 min-h-24 min-w-48 flex-1 items-center justify-center p-4',
|
|
110
|
+
rowIndex < rows.length - 1 && 'border-b',
|
|
111
|
+
columnIndex < columns.length - 1 && 'border-r',
|
|
112
|
+
cellClassName,
|
|
113
|
+
)}
|
|
114
|
+
key={`${row.key}-${column.key}`}
|
|
115
|
+
>
|
|
116
|
+
<Cell column={column} row={row} />
|
|
117
|
+
</View>
|
|
118
|
+
))}
|
|
119
|
+
</View>
|
|
120
|
+
))}
|
|
121
|
+
</View>
|
|
122
|
+
</ScrollView>
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function Section({ children, title }: { children: ReactNode; title: string }) {
|
|
127
|
+
return (
|
|
128
|
+
<View>
|
|
129
|
+
<Text className="mb-6" variant="h3">
|
|
130
|
+
{title}
|
|
131
|
+
</Text>
|
|
132
|
+
{children}
|
|
133
|
+
</View>
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const Story = Object.assign(Layout, {
|
|
138
|
+
Layout,
|
|
139
|
+
Matrix,
|
|
140
|
+
Section,
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
export { Story };
|
|
144
|
+
export type {
|
|
145
|
+
ComponentMatrixCellProps,
|
|
146
|
+
ComponentMatrixColumn,
|
|
147
|
+
ComponentMatrixRow,
|
|
148
|
+
};
|