@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.
Files changed (80) hide show
  1. package/README.md +92 -2
  2. package/dist/components/Badge.d.ts +11 -0
  3. package/dist/components/Badge.d.ts.map +1 -0
  4. package/dist/components/Badge.js +33 -0
  5. package/dist/components/Badge.js.map +1 -0
  6. package/dist/components/Button.d.ts +13 -0
  7. package/dist/components/Button.d.ts.map +1 -0
  8. package/dist/components/Button.js +39 -0
  9. package/dist/components/Button.js.map +1 -0
  10. package/dist/components/Card.d.ts +20 -0
  11. package/dist/components/Card.d.ts.map +1 -0
  12. package/dist/components/Card.js +29 -0
  13. package/dist/components/Card.js.map +1 -0
  14. package/dist/components/Text.d.ts +10 -0
  15. package/dist/components/Text.d.ts.map +1 -0
  16. package/dist/components/Text.js +28 -0
  17. package/dist/components/Text.js.map +1 -0
  18. package/dist/entries/badge.d.ts +3 -0
  19. package/dist/entries/badge.d.ts.map +1 -0
  20. package/dist/entries/badge.js +2 -0
  21. package/dist/entries/badge.js.map +1 -0
  22. package/dist/entries/button.d.ts +3 -0
  23. package/dist/entries/button.d.ts.map +1 -0
  24. package/dist/entries/button.js +2 -0
  25. package/dist/entries/button.js.map +1 -0
  26. package/dist/entries/card.d.ts +3 -0
  27. package/dist/entries/card.d.ts.map +1 -0
  28. package/dist/entries/card.js +2 -0
  29. package/dist/entries/card.js.map +1 -0
  30. package/dist/entries/cn.d.ts +2 -0
  31. package/dist/entries/cn.d.ts.map +1 -0
  32. package/dist/entries/cn.js +2 -0
  33. package/dist/entries/cn.js.map +1 -0
  34. package/dist/entries/text.d.ts +3 -0
  35. package/dist/entries/text.d.ts.map +1 -0
  36. package/dist/entries/text.js +2 -0
  37. package/dist/entries/text.js.map +1 -0
  38. package/dist/entries/use-color-token.d.ts +2 -0
  39. package/dist/entries/use-color-token.d.ts.map +1 -0
  40. package/dist/entries/use-color-token.js +2 -0
  41. package/dist/entries/use-color-token.js.map +1 -0
  42. package/dist/hooks/useColorToken.d.ts +9 -0
  43. package/dist/hooks/useColorToken.d.ts.map +1 -0
  44. package/dist/hooks/useColorToken.js +13 -0
  45. package/dist/hooks/useColorToken.js.map +1 -0
  46. package/dist/index.d.ts +7 -0
  47. package/dist/index.d.ts.map +1 -0
  48. package/dist/index.js +9 -0
  49. package/dist/index.js.map +1 -0
  50. package/dist/lib/cn.d.ts +3 -0
  51. package/dist/lib/cn.d.ts.map +1 -0
  52. package/dist/lib/cn.js +6 -0
  53. package/dist/lib/cn.js.map +1 -0
  54. package/package.json +91 -3
  55. package/src/base.css +6 -0
  56. package/src/components/Badge.stories.tsx +47 -0
  57. package/src/components/Badge.test.tsx +11 -0
  58. package/src/components/Badge.tsx +42 -0
  59. package/src/components/Button.stories.tsx +87 -0
  60. package/src/components/Button.test.tsx +34 -0
  61. package/src/components/Button.tsx +70 -0
  62. package/src/components/Card.stories.tsx +55 -0
  63. package/src/components/Card.test.tsx +23 -0
  64. package/src/components/Card.tsx +55 -0
  65. package/src/components/Text.stories.tsx +60 -0
  66. package/src/components/Text.test.tsx +15 -0
  67. package/src/components/Text.tsx +37 -0
  68. package/src/components/_internal/Story.tsx +148 -0
  69. package/src/entries/badge.ts +2 -0
  70. package/src/entries/button.ts +2 -0
  71. package/src/entries/card.ts +8 -0
  72. package/src/entries/cn.ts +1 -0
  73. package/src/entries/text.ts +2 -0
  74. package/src/entries/use-color-token.ts +1 -0
  75. package/src/hooks/useColorToken.ts +13 -0
  76. package/src/index.ts +8 -0
  77. package/src/lib/cn.test.ts +9 -0
  78. package/src/lib/cn.ts +6 -0
  79. package/src/styles.css +12 -0
  80. 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
+ };