@pickle-ui/react-native 1.0.0-beta.1 → 1.0.0-beta.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +62 -24
- package/dist/components/Badge.js +1 -1
- package/dist/components/Badge.js.map +1 -1
- package/dist/components/Button.d.ts.map +1 -1
- package/dist/components/Button.js +27 -23
- package/dist/components/Button.js.map +1 -1
- package/dist/components/Card.d.ts.map +1 -1
- package/dist/components/Card.js +7 -4
- package/dist/components/Card.js.map +1 -1
- package/dist/components/Flex.d.ts +25 -0
- package/dist/components/Flex.d.ts.map +1 -0
- package/dist/components/Flex.js +24 -0
- package/dist/components/Flex.js.map +1 -0
- package/dist/components/Grid.d.ts +36 -0
- package/dist/components/Grid.d.ts.map +1 -0
- package/dist/components/Grid.js +68 -0
- package/dist/components/Grid.js.map +1 -0
- package/dist/components/Separator.d.ts +11 -0
- package/dist/components/Separator.d.ts.map +1 -0
- package/dist/components/Separator.js +12 -0
- package/dist/components/Separator.js.map +1 -0
- package/dist/components/Spinner.d.ts +14 -0
- package/dist/components/Spinner.d.ts.map +1 -0
- package/dist/components/Spinner.js +41 -0
- package/dist/components/Spinner.js.map +1 -0
- package/dist/components/Text.d.ts.map +1 -1
- package/dist/components/Text.js +5 -4
- package/dist/components/Text.js.map +1 -1
- package/dist/components/XStack.d.ts +4 -0
- package/dist/components/XStack.d.ts.map +1 -0
- package/dist/components/XStack.js +6 -0
- package/dist/components/XStack.js.map +1 -0
- package/dist/components/YStack.d.ts +4 -0
- package/dist/components/YStack.d.ts.map +1 -0
- package/dist/components/YStack.js +6 -0
- package/dist/components/YStack.js.map +1 -0
- package/dist/components/_internal/InputFrame.d.ts +13 -0
- package/dist/components/_internal/InputFrame.d.ts.map +1 -0
- package/dist/components/_internal/InputFrame.js +14 -0
- package/dist/components/_internal/InputFrame.js.map +1 -0
- package/dist/components/_internal/StoryItem.d.ts +6 -0
- package/dist/components/_internal/StoryItem.d.ts.map +1 -0
- package/dist/components/_internal/StoryItem.js +9 -0
- package/dist/components/_internal/StoryItem.js.map +1 -0
- package/dist/components/_internal/layout.d.ts +79 -0
- package/dist/components/_internal/layout.d.ts.map +1 -0
- package/dist/components/_internal/layout.js +73 -0
- package/dist/components/_internal/layout.js.map +1 -0
- package/dist/components/_internal/useControllableState.d.ts +7 -0
- package/dist/components/_internal/useControllableState.d.ts.map +1 -0
- package/dist/components/_internal/useControllableState.js +17 -0
- package/dist/components/_internal/useControllableState.js.map +1 -0
- package/dist/components/form/Checkbox.d.ts +26 -0
- package/dist/components/form/Checkbox.d.ts.map +1 -0
- package/dist/components/form/Checkbox.js +43 -0
- package/dist/components/form/Checkbox.js.map +1 -0
- package/dist/components/form/Form.context.d.ts +34 -0
- package/dist/components/form/Form.context.d.ts.map +1 -0
- package/dist/components/form/Form.context.js +53 -0
- package/dist/components/form/Form.context.js.map +1 -0
- package/dist/components/form/Form.d.ts +33 -0
- package/dist/components/form/Form.d.ts.map +1 -0
- package/dist/components/form/Form.js +36 -0
- package/dist/components/form/Form.js.map +1 -0
- package/dist/components/form/FormCheckbox.d.ts +7 -0
- package/dist/components/form/FormCheckbox.d.ts.map +1 -0
- package/dist/components/form/FormCheckbox.js +24 -0
- package/dist/components/form/FormCheckbox.js.map +1 -0
- package/dist/components/form/FormDescription.d.ts +4 -0
- package/dist/components/form/FormDescription.d.ts.map +1 -0
- package/dist/components/form/FormDescription.js +7 -0
- package/dist/components/form/FormDescription.js.map +1 -0
- package/dist/components/form/FormError.d.ts +5 -0
- package/dist/components/form/FormError.d.ts.map +1 -0
- package/dist/components/form/FormError.js +8 -0
- package/dist/components/form/FormError.js.map +1 -0
- package/dist/components/form/FormField.d.ts +17 -0
- package/dist/components/form/FormField.d.ts.map +1 -0
- package/dist/components/form/FormField.js +36 -0
- package/dist/components/form/FormField.js.map +1 -0
- package/dist/components/form/FormInput.d.ts +10 -0
- package/dist/components/form/FormInput.d.ts.map +1 -0
- package/dist/components/form/FormInput.js +36 -0
- package/dist/components/form/FormInput.js.map +1 -0
- package/dist/components/form/FormInputGroup.d.ts +8 -0
- package/dist/components/form/FormInputGroup.d.ts.map +1 -0
- package/dist/components/form/FormInputGroup.js +18 -0
- package/dist/components/form/FormInputGroup.js.map +1 -0
- package/dist/components/form/FormLabel.d.ts +4 -0
- package/dist/components/form/FormLabel.d.ts.map +1 -0
- package/dist/components/form/FormLabel.js +7 -0
- package/dist/components/form/FormLabel.js.map +1 -0
- package/dist/components/form/FormRadioGroup.d.ts +11 -0
- package/dist/components/form/FormRadioGroup.d.ts.map +1 -0
- package/dist/components/form/FormRadioGroup.js +24 -0
- package/dist/components/form/FormRadioGroup.js.map +1 -0
- package/dist/components/form/FormRoot.d.ts +13 -0
- package/dist/components/form/FormRoot.d.ts.map +1 -0
- package/dist/components/form/FormRoot.js +19 -0
- package/dist/components/form/FormRoot.js.map +1 -0
- package/dist/components/form/FormTextarea.d.ts +7 -0
- package/dist/components/form/FormTextarea.d.ts.map +1 -0
- package/dist/components/form/FormTextarea.js +27 -0
- package/dist/components/form/FormTextarea.js.map +1 -0
- package/dist/components/form/Input.d.ts +17 -0
- package/dist/components/form/Input.d.ts.map +1 -0
- package/dist/components/form/Input.js +32 -0
- package/dist/components/form/Input.js.map +1 -0
- package/dist/components/form/InputGroup.context.d.ts +10 -0
- package/dist/components/form/InputGroup.context.d.ts.map +1 -0
- package/dist/components/form/InputGroup.context.js +8 -0
- package/dist/components/form/InputGroup.context.js.map +1 -0
- package/dist/components/form/InputGroup.d.ts +31 -0
- package/dist/components/form/InputGroup.d.ts.map +1 -0
- package/dist/components/form/InputGroup.js +123 -0
- package/dist/components/form/InputGroup.js.map +1 -0
- package/dist/components/form/RadioGroup.d.ts +29 -0
- package/dist/components/form/RadioGroup.d.ts.map +1 -0
- package/dist/components/form/RadioGroup.js +42 -0
- package/dist/components/form/RadioGroup.js.map +1 -0
- package/dist/components/form/Textarea.d.ts +19 -0
- package/dist/components/form/Textarea.d.ts.map +1 -0
- package/dist/components/form/Textarea.js +26 -0
- package/dist/components/form/Textarea.js.map +1 -0
- package/dist/entries/checkbox.d.ts +3 -0
- package/dist/entries/checkbox.d.ts.map +1 -0
- package/dist/entries/checkbox.js +2 -0
- package/dist/entries/checkbox.js.map +1 -0
- package/dist/entries/flex.d.ts +3 -0
- package/dist/entries/flex.d.ts.map +1 -0
- package/dist/entries/flex.js +2 -0
- package/dist/entries/flex.js.map +1 -0
- package/dist/entries/form.d.ts +3 -0
- package/dist/entries/form.d.ts.map +1 -0
- package/dist/entries/form.js +2 -0
- package/dist/entries/form.js.map +1 -0
- package/dist/entries/grid.d.ts +3 -0
- package/dist/entries/grid.d.ts.map +1 -0
- package/dist/entries/grid.js +2 -0
- package/dist/entries/grid.js.map +1 -0
- package/dist/entries/input-group.d.ts +3 -0
- package/dist/entries/input-group.d.ts.map +1 -0
- package/dist/entries/input-group.js +2 -0
- package/dist/entries/input-group.js.map +1 -0
- package/dist/entries/input.d.ts +3 -0
- package/dist/entries/input.d.ts.map +1 -0
- package/dist/entries/input.js +2 -0
- package/dist/entries/input.js.map +1 -0
- package/dist/entries/radio-group.d.ts +3 -0
- package/dist/entries/radio-group.d.ts.map +1 -0
- package/dist/entries/radio-group.js +2 -0
- package/dist/entries/radio-group.js.map +1 -0
- package/dist/entries/separator.d.ts +3 -0
- package/dist/entries/separator.d.ts.map +1 -0
- package/dist/entries/separator.js +2 -0
- package/dist/entries/separator.js.map +1 -0
- package/dist/entries/spinner.d.ts +3 -0
- package/dist/entries/spinner.d.ts.map +1 -0
- package/dist/entries/spinner.js +2 -0
- package/dist/entries/spinner.js.map +1 -0
- package/dist/entries/textarea.d.ts +3 -0
- package/dist/entries/textarea.d.ts.map +1 -0
- package/dist/entries/textarea.js +2 -0
- package/dist/entries/textarea.js.map +1 -0
- package/dist/entries/x-stack.d.ts +3 -0
- package/dist/entries/x-stack.d.ts.map +1 -0
- package/dist/entries/x-stack.js +2 -0
- package/dist/entries/x-stack.js.map +1 -0
- package/dist/entries/y-stack.d.ts +3 -0
- package/dist/entries/y-stack.d.ts.map +1 -0
- package/dist/entries/y-stack.js +2 -0
- package/dist/entries/y-stack.js.map +1 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +12 -0
- package/dist/index.js.map +1 -1
- package/package.json +8 -3
- package/src/base.css +23 -5
- package/src/components/Badge.tsx +1 -1
- package/src/components/Button.tsx +27 -23
- package/src/components/Card.tsx +12 -8
- package/src/components/Flex.stories.tsx +58 -0
- package/src/components/Flex.test.tsx +57 -0
- package/src/components/Flex.tsx +52 -0
- package/src/components/Grid.stories.tsx +55 -0
- package/src/components/Grid.test.tsx +140 -0
- package/src/components/Grid.tsx +166 -0
- package/src/components/Separator.stories.tsx +39 -0
- package/src/components/Separator.test.tsx +25 -0
- package/src/components/Separator.tsx +30 -0
- package/src/components/Spinner.stories.tsx +35 -0
- package/src/components/Spinner.test.tsx +30 -0
- package/src/components/Spinner.tsx +75 -0
- package/src/components/Text.stories.tsx +49 -0
- package/src/components/Text.tsx +5 -4
- package/src/components/XStack.stories.tsx +46 -0
- package/src/components/XStack.test.tsx +13 -0
- package/src/components/XStack.tsx +7 -0
- package/src/components/YStack.stories.tsx +46 -0
- package/src/components/YStack.test.tsx +13 -0
- package/src/components/YStack.tsx +7 -0
- package/src/components/_internal/InputFrame.tsx +50 -0
- package/src/components/_internal/StoryItem.tsx +24 -0
- package/src/components/_internal/layout.ts +103 -0
- package/src/components/_internal/useControllableState.ts +25 -0
- package/src/components/form/Checkbox.stories.tsx +67 -0
- package/src/components/form/Checkbox.test.tsx +88 -0
- package/src/components/form/Checkbox.tsx +167 -0
- package/src/components/form/Form.context.ts +91 -0
- package/src/components/form/Form.stories.tsx +152 -0
- package/src/components/form/Form.test.tsx +195 -0
- package/src/components/form/Form.tsx +51 -0
- package/src/components/form/FormCheckbox.tsx +42 -0
- package/src/components/form/FormDescription.tsx +14 -0
- package/src/components/form/FormError.tsx +20 -0
- package/src/components/form/FormField.tsx +82 -0
- package/src/components/form/FormInput.tsx +70 -0
- package/src/components/form/FormInputGroup.tsx +30 -0
- package/src/components/form/FormLabel.tsx +17 -0
- package/src/components/form/FormRadioGroup.tsx +48 -0
- package/src/components/form/FormRoot.tsx +41 -0
- package/src/components/form/FormTextarea.tsx +56 -0
- package/src/components/form/Input.stories.tsx +105 -0
- package/src/components/form/Input.test.tsx +76 -0
- package/src/components/form/Input.tsx +81 -0
- package/src/components/form/InputGroup.context.ts +11 -0
- package/src/components/form/InputGroup.stories.tsx +129 -0
- package/src/components/form/InputGroup.test.tsx +95 -0
- package/src/components/form/InputGroup.tsx +266 -0
- package/src/components/form/RadioGroup.stories.tsx +62 -0
- package/src/components/form/RadioGroup.test.tsx +98 -0
- package/src/components/form/RadioGroup.tsx +151 -0
- package/src/components/form/Textarea.stories.tsx +61 -0
- package/src/components/form/Textarea.test.tsx +35 -0
- package/src/components/form/Textarea.tsx +74 -0
- package/src/entries/checkbox.ts +2 -0
- package/src/entries/flex.ts +2 -0
- package/src/entries/form.ts +13 -0
- package/src/entries/grid.ts +2 -0
- package/src/entries/input-group.ts +8 -0
- package/src/entries/input.ts +2 -0
- package/src/entries/radio-group.ts +5 -0
- package/src/entries/separator.ts +2 -0
- package/src/entries/spinner.ts +2 -0
- package/src/entries/textarea.ts +2 -0
- package/src/entries/x-stack.ts +2 -0
- package/src/entries/y-stack.ts +2 -0
- package/src/fonts.test.tsx +244 -0
- package/src/index.ts +12 -0
- package/src/styles.css +12 -0
- package/dist/components/_internal/Spinner.d.ts +0 -9
- package/dist/components/_internal/Spinner.d.ts.map +0 -1
- package/dist/components/_internal/Spinner.js +0 -31
- package/dist/components/_internal/Spinner.js.map +0 -1
- package/src/components/_internal/Spinner.tsx +0 -49
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
import { Children, Fragment, isValidElement, type ReactNode } from 'react';
|
|
2
|
+
import { View } from 'react-native';
|
|
3
|
+
|
|
4
|
+
import { cn } from '../lib/cn';
|
|
5
|
+
import {
|
|
6
|
+
gapXClasses,
|
|
7
|
+
gapYClasses,
|
|
8
|
+
type LayoutAlign,
|
|
9
|
+
type LayoutGap,
|
|
10
|
+
type LayoutJustify,
|
|
11
|
+
} from './_internal/layout';
|
|
12
|
+
|
|
13
|
+
// React Native has no CSS grid, so `Grid` places its children itself: one row
|
|
14
|
+
// of equal `flex-1` cells per grid row, spaced by `gapX`, and the rows stacked
|
|
15
|
+
// and spaced by `gapY`. Empty cells and rows keep their size, so every column
|
|
16
|
+
// stays the same width and `rows` keeps its tracks, as on the web.
|
|
17
|
+
|
|
18
|
+
type GridFlow = 'column' | 'column-dense' | 'dense' | 'row' | 'row-dense';
|
|
19
|
+
type GridPlaceContent = 'baseline' | 'stretch' | LayoutJustify;
|
|
20
|
+
type GridTrackCount = 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12;
|
|
21
|
+
|
|
22
|
+
// Cells are columns: `items-*` places an item across the cell's width and
|
|
23
|
+
// `justify-*` down its height. A cell can't stretch an item vertically, so
|
|
24
|
+
// `stretch` and `baseline` place it at the top; give the item `flex-1` to fill
|
|
25
|
+
// its cell's height.
|
|
26
|
+
const horizontalClasses = {
|
|
27
|
+
baseline: 'items-start',
|
|
28
|
+
center: 'items-center',
|
|
29
|
+
end: 'items-end',
|
|
30
|
+
start: 'items-start',
|
|
31
|
+
stretch: 'items-stretch',
|
|
32
|
+
} as const;
|
|
33
|
+
|
|
34
|
+
const verticalClasses = {
|
|
35
|
+
baseline: 'justify-start',
|
|
36
|
+
center: 'justify-center',
|
|
37
|
+
end: 'justify-end',
|
|
38
|
+
start: 'justify-start',
|
|
39
|
+
stretch: 'justify-start',
|
|
40
|
+
} as const;
|
|
41
|
+
|
|
42
|
+
// Distributes the rows down the grid and the cells along each row, like CSS
|
|
43
|
+
// `align-content` and `justify-content`. Cells share each row's width, so only
|
|
44
|
+
// a grid taller than its rows shows the difference; `stretch` grows the rows
|
|
45
|
+
// into that height instead.
|
|
46
|
+
const contentClasses = {
|
|
47
|
+
around: 'justify-around',
|
|
48
|
+
baseline: 'justify-start',
|
|
49
|
+
between: 'justify-between',
|
|
50
|
+
center: 'justify-center',
|
|
51
|
+
end: 'justify-end',
|
|
52
|
+
evenly: 'justify-evenly',
|
|
53
|
+
start: 'justify-start',
|
|
54
|
+
stretch: 'justify-start',
|
|
55
|
+
} as const;
|
|
56
|
+
|
|
57
|
+
export type GridProps = React.ComponentProps<typeof View> & {
|
|
58
|
+
/**
|
|
59
|
+
* Places each item down its cell, like CSS `align-items`. `stretch` and
|
|
60
|
+
* `baseline` place it at the top: give the item `flex-1` to fill the cell.
|
|
61
|
+
*/
|
|
62
|
+
align?: LayoutAlign;
|
|
63
|
+
columns?: GridTrackCount;
|
|
64
|
+
/**
|
|
65
|
+
* Fills rows first (`row`) or columns first (`column`). Items can't span
|
|
66
|
+
* cells on native, so the `dense` flows place them like `row` and `column`.
|
|
67
|
+
*/
|
|
68
|
+
flow?: GridFlow;
|
|
69
|
+
gap?: LayoutGap;
|
|
70
|
+
gapX?: LayoutGap;
|
|
71
|
+
gapY?: LayoutGap;
|
|
72
|
+
/** Distributes the cells along each row, like CSS `justify-content`. */
|
|
73
|
+
justify?: LayoutJustify;
|
|
74
|
+
/** Distributes the rows and the cells, like CSS `place-content`. */
|
|
75
|
+
placeContent?: GridPlaceContent;
|
|
76
|
+
/** Places each item across and down its cell, like CSS `place-items`. */
|
|
77
|
+
placeItems?: LayoutAlign;
|
|
78
|
+
rows?: GridTrackCount;
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Lays its children out in equal columns, like `Grid` in @pickle-ui/react.
|
|
83
|
+
* Items fill their cell's width by default and the cells of a row share its
|
|
84
|
+
* height.
|
|
85
|
+
*/
|
|
86
|
+
export function Grid({
|
|
87
|
+
align,
|
|
88
|
+
children,
|
|
89
|
+
className,
|
|
90
|
+
columns,
|
|
91
|
+
flow = 'row',
|
|
92
|
+
gap,
|
|
93
|
+
gapX = gap,
|
|
94
|
+
gapY = gap,
|
|
95
|
+
justify,
|
|
96
|
+
placeContent,
|
|
97
|
+
placeItems,
|
|
98
|
+
rows,
|
|
99
|
+
...props
|
|
100
|
+
}: GridProps) {
|
|
101
|
+
const items = flattenChildren(children);
|
|
102
|
+
const byColumn = flow === 'column' || flow === 'column-dense';
|
|
103
|
+
const rowCount = byColumn
|
|
104
|
+
? (rows ?? 1)
|
|
105
|
+
: Math.max(rows ?? 0, Math.ceil(items.length / (columns ?? 1)));
|
|
106
|
+
const columnCount = byColumn
|
|
107
|
+
? Math.max(columns ?? 1, Math.ceil(items.length / rowCount))
|
|
108
|
+
: (columns ?? 1);
|
|
109
|
+
const vertical = placeItems ?? align;
|
|
110
|
+
const distribution = placeContent ?? justify;
|
|
111
|
+
|
|
112
|
+
return (
|
|
113
|
+
<View
|
|
114
|
+
className={cn(
|
|
115
|
+
gapY === undefined ? undefined : gapYClasses[gapY],
|
|
116
|
+
placeContent === undefined ? undefined : contentClasses[placeContent],
|
|
117
|
+
className,
|
|
118
|
+
)}
|
|
119
|
+
{...props}
|
|
120
|
+
>
|
|
121
|
+
{Array.from({ length: rowCount }, (_, row) => (
|
|
122
|
+
<View
|
|
123
|
+
className={cn(
|
|
124
|
+
'flex-row',
|
|
125
|
+
gapX === undefined ? undefined : gapXClasses[gapX],
|
|
126
|
+
distribution === undefined
|
|
127
|
+
? undefined
|
|
128
|
+
: contentClasses[distribution],
|
|
129
|
+
placeContent === 'stretch' ? 'grow' : undefined,
|
|
130
|
+
)}
|
|
131
|
+
key={row}
|
|
132
|
+
>
|
|
133
|
+
{Array.from({ length: columnCount }, (_, column) => (
|
|
134
|
+
<View
|
|
135
|
+
className={cn(
|
|
136
|
+
'flex-1',
|
|
137
|
+
placeItems === undefined
|
|
138
|
+
? undefined
|
|
139
|
+
: horizontalClasses[placeItems],
|
|
140
|
+
vertical === undefined ? undefined : verticalClasses[vertical],
|
|
141
|
+
)}
|
|
142
|
+
key={column}
|
|
143
|
+
>
|
|
144
|
+
{
|
|
145
|
+
items[
|
|
146
|
+
byColumn
|
|
147
|
+
? column * rowCount + row
|
|
148
|
+
: row * columnCount + column
|
|
149
|
+
]
|
|
150
|
+
}
|
|
151
|
+
</View>
|
|
152
|
+
))}
|
|
153
|
+
</View>
|
|
154
|
+
))}
|
|
155
|
+
</View>
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/** The grid items, with fragments opened up as on the web. */
|
|
160
|
+
function flattenChildren(children: ReactNode): ReactNode[] {
|
|
161
|
+
return Children.toArray(children).flatMap((child) =>
|
|
162
|
+
isValidElement<{ children?: ReactNode }>(child) && child.type === Fragment
|
|
163
|
+
? flattenChildren(child.props.children)
|
|
164
|
+
: [child],
|
|
165
|
+
);
|
|
166
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/react-native-web-vite';
|
|
2
|
+
|
|
3
|
+
import { Story } from './_internal/Story';
|
|
4
|
+
import { Separator } from './Separator';
|
|
5
|
+
import { Text } from './Text';
|
|
6
|
+
import { XStack } from './XStack';
|
|
7
|
+
import { YStack } from './YStack';
|
|
8
|
+
|
|
9
|
+
const meta = {
|
|
10
|
+
component: Separator,
|
|
11
|
+
parameters: { layout: 'fullscreen' },
|
|
12
|
+
title: 'components/Separator',
|
|
13
|
+
} satisfies Meta<typeof Separator>;
|
|
14
|
+
|
|
15
|
+
export default meta;
|
|
16
|
+
type StoryDefinition = StoryObj<typeof meta>;
|
|
17
|
+
|
|
18
|
+
export const All: StoryDefinition = {
|
|
19
|
+
render: () => (
|
|
20
|
+
<Story.Layout className="max-w-3xl" title="Separator">
|
|
21
|
+
<Story.Section title="Horizontal">
|
|
22
|
+
<YStack className="max-w-sm" gap={3}>
|
|
23
|
+
<Text>Account</Text>
|
|
24
|
+
<Separator />
|
|
25
|
+
<Text tone="muted">Manage your profile and preferences.</Text>
|
|
26
|
+
</YStack>
|
|
27
|
+
</Story.Section>
|
|
28
|
+
<Story.Section title="Vertical">
|
|
29
|
+
<XStack align="center" className="h-5" gap={3}>
|
|
30
|
+
<Text>Docs</Text>
|
|
31
|
+
<Separator orientation="vertical" />
|
|
32
|
+
<Text>Components</Text>
|
|
33
|
+
<Separator orientation="vertical" />
|
|
34
|
+
<Text>Theming</Text>
|
|
35
|
+
</XStack>
|
|
36
|
+
</Story.Section>
|
|
37
|
+
</Story.Layout>
|
|
38
|
+
),
|
|
39
|
+
};
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { render, screen } from '@testing-library/react-native';
|
|
2
|
+
|
|
3
|
+
import { Separator } from './Separator';
|
|
4
|
+
|
|
5
|
+
describe('Separator', () => {
|
|
6
|
+
it('renders a horizontal separator by default', async () => {
|
|
7
|
+
await render(<Separator testID="separator" />);
|
|
8
|
+
|
|
9
|
+
const separator = screen.getByTestId('separator');
|
|
10
|
+
expect(separator).toHaveProp('role', 'separator');
|
|
11
|
+
expect(separator).toHaveProp(
|
|
12
|
+
'className',
|
|
13
|
+
expect.stringContaining('h-px w-full'),
|
|
14
|
+
);
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it('renders a vertical separator', async () => {
|
|
18
|
+
await render(<Separator orientation="vertical" testID="separator" />);
|
|
19
|
+
|
|
20
|
+
expect(screen.getByTestId('separator')).toHaveProp(
|
|
21
|
+
'className',
|
|
22
|
+
expect.stringContaining('w-px self-stretch'),
|
|
23
|
+
);
|
|
24
|
+
});
|
|
25
|
+
});
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { View } from 'react-native';
|
|
2
|
+
|
|
3
|
+
import { cn } from '../lib/cn';
|
|
4
|
+
|
|
5
|
+
export type SeparatorProps = React.ComponentProps<typeof View> & {
|
|
6
|
+
orientation?: 'horizontal' | 'vertical';
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* A 1px rule in the border color, like `Separator` in @pickle-ui/react. Its
|
|
11
|
+
* `separator` role reaches screen readers on the web; iOS and Android have no
|
|
12
|
+
* separator trait, so they skip it like any non-focusable view.
|
|
13
|
+
*/
|
|
14
|
+
export function Separator({
|
|
15
|
+
className,
|
|
16
|
+
orientation = 'horizontal',
|
|
17
|
+
...props
|
|
18
|
+
}: SeparatorProps) {
|
|
19
|
+
return (
|
|
20
|
+
<View
|
|
21
|
+
className={cn(
|
|
22
|
+
'bg-border shrink-0',
|
|
23
|
+
orientation === 'horizontal' ? 'h-px w-full' : 'w-px self-stretch',
|
|
24
|
+
className,
|
|
25
|
+
)}
|
|
26
|
+
role="separator"
|
|
27
|
+
{...props}
|
|
28
|
+
/>
|
|
29
|
+
);
|
|
30
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/react-native-web-vite';
|
|
2
|
+
|
|
3
|
+
import { Story } from './_internal/Story';
|
|
4
|
+
import { Spinner } from './Spinner';
|
|
5
|
+
import { Text } from './Text';
|
|
6
|
+
import { XStack } from './XStack';
|
|
7
|
+
|
|
8
|
+
const meta = {
|
|
9
|
+
component: Spinner,
|
|
10
|
+
parameters: { layout: 'fullscreen' },
|
|
11
|
+
title: 'components/Spinner',
|
|
12
|
+
} satisfies Meta<typeof Spinner>;
|
|
13
|
+
|
|
14
|
+
export default meta;
|
|
15
|
+
type StoryDefinition = StoryObj<typeof meta>;
|
|
16
|
+
|
|
17
|
+
export const All: StoryDefinition = {
|
|
18
|
+
render: () => (
|
|
19
|
+
<Story.Layout className="max-w-3xl" title="Spinner">
|
|
20
|
+
<Story.Section title="Sizes">
|
|
21
|
+
<XStack align="center" gap={6}>
|
|
22
|
+
<Spinner className="size-4" />
|
|
23
|
+
<Spinner className="size-6" />
|
|
24
|
+
<Spinner className="size-8" />
|
|
25
|
+
</XStack>
|
|
26
|
+
</Story.Section>
|
|
27
|
+
<Story.Section title="With a label">
|
|
28
|
+
<XStack align="center" gap={2}>
|
|
29
|
+
<Spinner aria-hidden className="text-muted-foreground size-4" />
|
|
30
|
+
<Text tone="muted">Loading your workspace…</Text>
|
|
31
|
+
</XStack>
|
|
32
|
+
</Story.Section>
|
|
33
|
+
</Story.Layout>
|
|
34
|
+
),
|
|
35
|
+
};
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { render, screen } from '@testing-library/react-native';
|
|
2
|
+
|
|
3
|
+
import { Spinner } from './Spinner';
|
|
4
|
+
|
|
5
|
+
describe('Spinner', () => {
|
|
6
|
+
it('announces that something is loading', async () => {
|
|
7
|
+
await render(<Spinner />);
|
|
8
|
+
|
|
9
|
+
expect(
|
|
10
|
+
screen.getByRole('progressbar', { name: 'Loading' }),
|
|
11
|
+
).toBeOnTheScreen();
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
it('accepts a custom accessible name', async () => {
|
|
15
|
+
await render(<Spinner accessibilityLabel="Saving" />);
|
|
16
|
+
|
|
17
|
+
expect(
|
|
18
|
+
screen.getByRole('progressbar', { name: 'Saving' }),
|
|
19
|
+
).toBeOnTheScreen();
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it('is decorative when aria-hidden', async () => {
|
|
23
|
+
await render(<Spinner aria-hidden testID="spinner" />);
|
|
24
|
+
|
|
25
|
+
expect(screen.queryByRole('progressbar')).toBeNull();
|
|
26
|
+
expect(
|
|
27
|
+
screen.getByTestId('spinner', { includeHiddenElements: true }),
|
|
28
|
+
).not.toHaveProp('accessibilityLabel');
|
|
29
|
+
});
|
|
30
|
+
});
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { useEffect, useState } from 'react';
|
|
2
|
+
import { Animated, Easing, Platform, View } from 'react-native';
|
|
3
|
+
import { useResolveClassNames } from 'uniwind';
|
|
4
|
+
|
|
5
|
+
import { cn } from '../lib/cn';
|
|
6
|
+
|
|
7
|
+
export type SpinnerProps = Omit<
|
|
8
|
+
React.ComponentProps<typeof View>,
|
|
9
|
+
'children'
|
|
10
|
+
> & {
|
|
11
|
+
/** Hides the spinner from screen readers, when visible text already says what is loading. */
|
|
12
|
+
'aria-hidden'?: boolean;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* The web `Spinner`'s look on native: a three-quarter ring turning once a
|
|
17
|
+
* second, drawn inside a `size-6` box like the web icon. Size it with
|
|
18
|
+
* `size-*` and color it with `text-*`, as on the web. Announced as "Loading"
|
|
19
|
+
* by default; `accessibilityLabel` replaces the name and `aria-hidden` makes
|
|
20
|
+
* it decorative.
|
|
21
|
+
*/
|
|
22
|
+
export function Spinner({
|
|
23
|
+
accessibilityLabel = 'Loading',
|
|
24
|
+
'aria-hidden': ariaHidden = false,
|
|
25
|
+
className,
|
|
26
|
+
style,
|
|
27
|
+
...props
|
|
28
|
+
}: SpinnerProps) {
|
|
29
|
+
const [turn] = useState(() => new Animated.Value(0));
|
|
30
|
+
const box = cn('text-foreground size-6', className);
|
|
31
|
+
// React Native has no `currentColor`: the ring reads the `text-*` color, and
|
|
32
|
+
// its stroke follows the box like the icon's (2px at 24px).
|
|
33
|
+
const { color, width } = useResolveClassNames(box);
|
|
34
|
+
const strokeWidth = typeof width === 'number' ? width / 12 : 2;
|
|
35
|
+
|
|
36
|
+
useEffect(() => {
|
|
37
|
+
const loop = Animated.loop(
|
|
38
|
+
Animated.timing(turn, {
|
|
39
|
+
duration: 1000,
|
|
40
|
+
easing: Easing.linear,
|
|
41
|
+
toValue: 1,
|
|
42
|
+
// react-native-web has no native driver and warns when asked for it.
|
|
43
|
+
useNativeDriver: Platform.OS !== 'web',
|
|
44
|
+
}),
|
|
45
|
+
);
|
|
46
|
+
loop.start();
|
|
47
|
+
return () => loop.stop();
|
|
48
|
+
}, [turn]);
|
|
49
|
+
|
|
50
|
+
const rotate = turn.interpolate({
|
|
51
|
+
inputRange: [0, 1],
|
|
52
|
+
outputRange: ['0deg', '360deg'],
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
return (
|
|
56
|
+
<Animated.View
|
|
57
|
+
accessibilityLabel={ariaHidden ? undefined : accessibilityLabel}
|
|
58
|
+
accessibilityRole={ariaHidden ? undefined : 'progressbar'}
|
|
59
|
+
accessible={!ariaHidden}
|
|
60
|
+
aria-hidden={ariaHidden}
|
|
61
|
+
className={cn('items-center justify-center', box)}
|
|
62
|
+
style={[style, { transform: [{ rotate }] }]}
|
|
63
|
+
{...props}
|
|
64
|
+
>
|
|
65
|
+
<View
|
|
66
|
+
className="size-3/4 rounded-full"
|
|
67
|
+
style={{
|
|
68
|
+
borderColor: typeof color === 'string' ? color : undefined,
|
|
69
|
+
borderTopColor: 'transparent',
|
|
70
|
+
borderWidth: strokeWidth,
|
|
71
|
+
}}
|
|
72
|
+
/>
|
|
73
|
+
</Animated.View>
|
|
74
|
+
);
|
|
75
|
+
}
|
|
@@ -1,8 +1,13 @@
|
|
|
1
1
|
import type { Meta, StoryObj } from '@storybook/react-native-web-vite';
|
|
2
2
|
|
|
3
3
|
import { View } from 'react-native';
|
|
4
|
+
import { ScopedVariables } from 'uniwind';
|
|
4
5
|
|
|
5
6
|
import { type ComponentMatrixCellProps, Story } from './_internal/Story';
|
|
7
|
+
import { Badge } from './Badge';
|
|
8
|
+
import { Button } from './Button';
|
|
9
|
+
import { Card } from './Card';
|
|
10
|
+
import { Input } from './form/Input';
|
|
6
11
|
import { Text, type TextProps } from './Text';
|
|
7
12
|
|
|
8
13
|
const meta = {
|
|
@@ -38,11 +43,47 @@ const TONE_COLUMNS: { key: 'default' | TextTone; label: string }[] = [
|
|
|
38
43
|
{ key: 'destructive', label: 'Destructive' },
|
|
39
44
|
];
|
|
40
45
|
|
|
46
|
+
// An app sets `--font-sans` in its stylesheet; `ScopedVariables` sets it here
|
|
47
|
+
// so both fonts sit side by side. On the web, `--font-heading` resolves
|
|
48
|
+
// where it's declared, so the scope sets it too; native resolves it in place.
|
|
49
|
+
const FONT_COLUMNS = [
|
|
50
|
+
{ description: 'No --font-sans', key: '', label: 'System font' },
|
|
51
|
+
{
|
|
52
|
+
description: "--font-sans: 'Georgia'",
|
|
53
|
+
key: 'Georgia',
|
|
54
|
+
label: 'App font',
|
|
55
|
+
},
|
|
56
|
+
];
|
|
57
|
+
|
|
41
58
|
const WEIGHT_ROWS: { key: TextWeight; label: string }[] = [
|
|
42
59
|
{ key: 'normal', label: 'Normal' },
|
|
43
60
|
{ key: 'bold', label: 'Bold' },
|
|
44
61
|
];
|
|
45
62
|
|
|
63
|
+
function FontCell({ column }: ComponentMatrixCellProps) {
|
|
64
|
+
return (
|
|
65
|
+
<ScopedVariables
|
|
66
|
+
variables={
|
|
67
|
+
column.key
|
|
68
|
+
? { '--font-heading': column.key, '--font-sans': column.key }
|
|
69
|
+
: {}
|
|
70
|
+
}
|
|
71
|
+
>
|
|
72
|
+
<Card className="w-80">
|
|
73
|
+
<Card.Header>
|
|
74
|
+
<Card.Title>Weekly report</Card.Title>
|
|
75
|
+
<Card.Description>Every project, every Monday.</Card.Description>
|
|
76
|
+
</Card.Header>
|
|
77
|
+
<Card.Content className="items-start gap-3">
|
|
78
|
+
<Badge variant="success">Shipped</Badge>
|
|
79
|
+
<Input accessibilityLabel="Email" placeholder="ada@example.com" />
|
|
80
|
+
<Button>Subscribe</Button>
|
|
81
|
+
</Card.Content>
|
|
82
|
+
</Card>
|
|
83
|
+
</ScopedVariables>
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
|
|
46
87
|
function ToneCell({ column, row }: ComponentMatrixCellProps) {
|
|
47
88
|
return (
|
|
48
89
|
<Text
|
|
@@ -98,6 +139,14 @@ export const All: StoryDefinition = {
|
|
|
98
139
|
/>
|
|
99
140
|
</Story.Section>
|
|
100
141
|
|
|
142
|
+
<Story.Section title="App font">
|
|
143
|
+
<Story.Matrix
|
|
144
|
+
Cell={FontCell}
|
|
145
|
+
columns={FONT_COLUMNS}
|
|
146
|
+
rows={[{ key: 'card', label: 'Components' }]}
|
|
147
|
+
/>
|
|
148
|
+
</Story.Section>
|
|
149
|
+
|
|
101
150
|
<Story.Section title="Truncate">
|
|
102
151
|
<View className="border-border w-64 rounded-md border p-3">
|
|
103
152
|
<Text truncate>
|
package/src/components/Text.tsx
CHANGED
|
@@ -5,7 +5,7 @@ import { cn } from '../lib/cn';
|
|
|
5
5
|
|
|
6
6
|
// Same variants, tones and weights as `Text` in @pickle-ui/react, sized by the
|
|
7
7
|
// shared type scale (`@pickle-ui/tokens/typography.css`).
|
|
8
|
-
const textVariants = cva('text-foreground', {
|
|
8
|
+
const textVariants = cva('text-foreground font-sans', {
|
|
9
9
|
defaultVariants: { variant: 'body' },
|
|
10
10
|
variants: {
|
|
11
11
|
tone: {
|
|
@@ -16,8 +16,8 @@ const textVariants = cva('text-foreground', {
|
|
|
16
16
|
variant: {
|
|
17
17
|
body: 'text-body',
|
|
18
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',
|
|
19
|
+
h1: 'font-heading text-h1 font-semibold tracking-tight',
|
|
20
|
+
h2: 'font-heading text-h2 font-semibold tracking-tight',
|
|
21
21
|
h3: 'text-h3 font-semibold',
|
|
22
22
|
h4: 'text-h4 font-medium',
|
|
23
23
|
lead: 'text-lead',
|
|
@@ -33,7 +33,8 @@ const textVariants = cva('text-foreground', {
|
|
|
33
33
|
const HEADINGS = new Set(['h1', 'h2', 'h3', 'h4']);
|
|
34
34
|
|
|
35
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.
|
|
36
|
+
// fonts don't resolve, so `code` names each platform's monospace font. The
|
|
37
|
+
// `style` prop wins over `font-sans` from `className`.
|
|
37
38
|
const styles = StyleSheet.create({
|
|
38
39
|
code: { fontFamily: Platform.select({ default: 'monospace', ios: 'Menlo' }) },
|
|
39
40
|
});
|
|
@@ -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
|
+
});
|