@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.
Files changed (255) hide show
  1. package/README.md +62 -24
  2. package/dist/components/Badge.js +1 -1
  3. package/dist/components/Badge.js.map +1 -1
  4. package/dist/components/Button.d.ts.map +1 -1
  5. package/dist/components/Button.js +27 -23
  6. package/dist/components/Button.js.map +1 -1
  7. package/dist/components/Card.d.ts.map +1 -1
  8. package/dist/components/Card.js +7 -4
  9. package/dist/components/Card.js.map +1 -1
  10. package/dist/components/Flex.d.ts +25 -0
  11. package/dist/components/Flex.d.ts.map +1 -0
  12. package/dist/components/Flex.js +24 -0
  13. package/dist/components/Flex.js.map +1 -0
  14. package/dist/components/Grid.d.ts +36 -0
  15. package/dist/components/Grid.d.ts.map +1 -0
  16. package/dist/components/Grid.js +68 -0
  17. package/dist/components/Grid.js.map +1 -0
  18. package/dist/components/Separator.d.ts +11 -0
  19. package/dist/components/Separator.d.ts.map +1 -0
  20. package/dist/components/Separator.js +12 -0
  21. package/dist/components/Separator.js.map +1 -0
  22. package/dist/components/Spinner.d.ts +14 -0
  23. package/dist/components/Spinner.d.ts.map +1 -0
  24. package/dist/components/Spinner.js +41 -0
  25. package/dist/components/Spinner.js.map +1 -0
  26. package/dist/components/Text.d.ts.map +1 -1
  27. package/dist/components/Text.js +5 -4
  28. package/dist/components/Text.js.map +1 -1
  29. package/dist/components/XStack.d.ts +4 -0
  30. package/dist/components/XStack.d.ts.map +1 -0
  31. package/dist/components/XStack.js +6 -0
  32. package/dist/components/XStack.js.map +1 -0
  33. package/dist/components/YStack.d.ts +4 -0
  34. package/dist/components/YStack.d.ts.map +1 -0
  35. package/dist/components/YStack.js +6 -0
  36. package/dist/components/YStack.js.map +1 -0
  37. package/dist/components/_internal/InputFrame.d.ts +13 -0
  38. package/dist/components/_internal/InputFrame.d.ts.map +1 -0
  39. package/dist/components/_internal/InputFrame.js +14 -0
  40. package/dist/components/_internal/InputFrame.js.map +1 -0
  41. package/dist/components/_internal/StoryItem.d.ts +6 -0
  42. package/dist/components/_internal/StoryItem.d.ts.map +1 -0
  43. package/dist/components/_internal/StoryItem.js +9 -0
  44. package/dist/components/_internal/StoryItem.js.map +1 -0
  45. package/dist/components/_internal/layout.d.ts +79 -0
  46. package/dist/components/_internal/layout.d.ts.map +1 -0
  47. package/dist/components/_internal/layout.js +73 -0
  48. package/dist/components/_internal/layout.js.map +1 -0
  49. package/dist/components/_internal/useControllableState.d.ts +7 -0
  50. package/dist/components/_internal/useControllableState.d.ts.map +1 -0
  51. package/dist/components/_internal/useControllableState.js +17 -0
  52. package/dist/components/_internal/useControllableState.js.map +1 -0
  53. package/dist/components/form/Checkbox.d.ts +26 -0
  54. package/dist/components/form/Checkbox.d.ts.map +1 -0
  55. package/dist/components/form/Checkbox.js +43 -0
  56. package/dist/components/form/Checkbox.js.map +1 -0
  57. package/dist/components/form/Form.context.d.ts +34 -0
  58. package/dist/components/form/Form.context.d.ts.map +1 -0
  59. package/dist/components/form/Form.context.js +53 -0
  60. package/dist/components/form/Form.context.js.map +1 -0
  61. package/dist/components/form/Form.d.ts +33 -0
  62. package/dist/components/form/Form.d.ts.map +1 -0
  63. package/dist/components/form/Form.js +36 -0
  64. package/dist/components/form/Form.js.map +1 -0
  65. package/dist/components/form/FormCheckbox.d.ts +7 -0
  66. package/dist/components/form/FormCheckbox.d.ts.map +1 -0
  67. package/dist/components/form/FormCheckbox.js +24 -0
  68. package/dist/components/form/FormCheckbox.js.map +1 -0
  69. package/dist/components/form/FormDescription.d.ts +4 -0
  70. package/dist/components/form/FormDescription.d.ts.map +1 -0
  71. package/dist/components/form/FormDescription.js +7 -0
  72. package/dist/components/form/FormDescription.js.map +1 -0
  73. package/dist/components/form/FormError.d.ts +5 -0
  74. package/dist/components/form/FormError.d.ts.map +1 -0
  75. package/dist/components/form/FormError.js +8 -0
  76. package/dist/components/form/FormError.js.map +1 -0
  77. package/dist/components/form/FormField.d.ts +17 -0
  78. package/dist/components/form/FormField.d.ts.map +1 -0
  79. package/dist/components/form/FormField.js +36 -0
  80. package/dist/components/form/FormField.js.map +1 -0
  81. package/dist/components/form/FormInput.d.ts +10 -0
  82. package/dist/components/form/FormInput.d.ts.map +1 -0
  83. package/dist/components/form/FormInput.js +36 -0
  84. package/dist/components/form/FormInput.js.map +1 -0
  85. package/dist/components/form/FormInputGroup.d.ts +8 -0
  86. package/dist/components/form/FormInputGroup.d.ts.map +1 -0
  87. package/dist/components/form/FormInputGroup.js +18 -0
  88. package/dist/components/form/FormInputGroup.js.map +1 -0
  89. package/dist/components/form/FormLabel.d.ts +4 -0
  90. package/dist/components/form/FormLabel.d.ts.map +1 -0
  91. package/dist/components/form/FormLabel.js +7 -0
  92. package/dist/components/form/FormLabel.js.map +1 -0
  93. package/dist/components/form/FormRadioGroup.d.ts +11 -0
  94. package/dist/components/form/FormRadioGroup.d.ts.map +1 -0
  95. package/dist/components/form/FormRadioGroup.js +24 -0
  96. package/dist/components/form/FormRadioGroup.js.map +1 -0
  97. package/dist/components/form/FormRoot.d.ts +13 -0
  98. package/dist/components/form/FormRoot.d.ts.map +1 -0
  99. package/dist/components/form/FormRoot.js +19 -0
  100. package/dist/components/form/FormRoot.js.map +1 -0
  101. package/dist/components/form/FormTextarea.d.ts +7 -0
  102. package/dist/components/form/FormTextarea.d.ts.map +1 -0
  103. package/dist/components/form/FormTextarea.js +27 -0
  104. package/dist/components/form/FormTextarea.js.map +1 -0
  105. package/dist/components/form/Input.d.ts +17 -0
  106. package/dist/components/form/Input.d.ts.map +1 -0
  107. package/dist/components/form/Input.js +32 -0
  108. package/dist/components/form/Input.js.map +1 -0
  109. package/dist/components/form/InputGroup.context.d.ts +10 -0
  110. package/dist/components/form/InputGroup.context.d.ts.map +1 -0
  111. package/dist/components/form/InputGroup.context.js +8 -0
  112. package/dist/components/form/InputGroup.context.js.map +1 -0
  113. package/dist/components/form/InputGroup.d.ts +31 -0
  114. package/dist/components/form/InputGroup.d.ts.map +1 -0
  115. package/dist/components/form/InputGroup.js +123 -0
  116. package/dist/components/form/InputGroup.js.map +1 -0
  117. package/dist/components/form/RadioGroup.d.ts +29 -0
  118. package/dist/components/form/RadioGroup.d.ts.map +1 -0
  119. package/dist/components/form/RadioGroup.js +42 -0
  120. package/dist/components/form/RadioGroup.js.map +1 -0
  121. package/dist/components/form/Textarea.d.ts +19 -0
  122. package/dist/components/form/Textarea.d.ts.map +1 -0
  123. package/dist/components/form/Textarea.js +26 -0
  124. package/dist/components/form/Textarea.js.map +1 -0
  125. package/dist/entries/checkbox.d.ts +3 -0
  126. package/dist/entries/checkbox.d.ts.map +1 -0
  127. package/dist/entries/checkbox.js +2 -0
  128. package/dist/entries/checkbox.js.map +1 -0
  129. package/dist/entries/flex.d.ts +3 -0
  130. package/dist/entries/flex.d.ts.map +1 -0
  131. package/dist/entries/flex.js +2 -0
  132. package/dist/entries/flex.js.map +1 -0
  133. package/dist/entries/form.d.ts +3 -0
  134. package/dist/entries/form.d.ts.map +1 -0
  135. package/dist/entries/form.js +2 -0
  136. package/dist/entries/form.js.map +1 -0
  137. package/dist/entries/grid.d.ts +3 -0
  138. package/dist/entries/grid.d.ts.map +1 -0
  139. package/dist/entries/grid.js +2 -0
  140. package/dist/entries/grid.js.map +1 -0
  141. package/dist/entries/input-group.d.ts +3 -0
  142. package/dist/entries/input-group.d.ts.map +1 -0
  143. package/dist/entries/input-group.js +2 -0
  144. package/dist/entries/input-group.js.map +1 -0
  145. package/dist/entries/input.d.ts +3 -0
  146. package/dist/entries/input.d.ts.map +1 -0
  147. package/dist/entries/input.js +2 -0
  148. package/dist/entries/input.js.map +1 -0
  149. package/dist/entries/radio-group.d.ts +3 -0
  150. package/dist/entries/radio-group.d.ts.map +1 -0
  151. package/dist/entries/radio-group.js +2 -0
  152. package/dist/entries/radio-group.js.map +1 -0
  153. package/dist/entries/separator.d.ts +3 -0
  154. package/dist/entries/separator.d.ts.map +1 -0
  155. package/dist/entries/separator.js +2 -0
  156. package/dist/entries/separator.js.map +1 -0
  157. package/dist/entries/spinner.d.ts +3 -0
  158. package/dist/entries/spinner.d.ts.map +1 -0
  159. package/dist/entries/spinner.js +2 -0
  160. package/dist/entries/spinner.js.map +1 -0
  161. package/dist/entries/textarea.d.ts +3 -0
  162. package/dist/entries/textarea.d.ts.map +1 -0
  163. package/dist/entries/textarea.js +2 -0
  164. package/dist/entries/textarea.js.map +1 -0
  165. package/dist/entries/x-stack.d.ts +3 -0
  166. package/dist/entries/x-stack.d.ts.map +1 -0
  167. package/dist/entries/x-stack.js +2 -0
  168. package/dist/entries/x-stack.js.map +1 -0
  169. package/dist/entries/y-stack.d.ts +3 -0
  170. package/dist/entries/y-stack.d.ts.map +1 -0
  171. package/dist/entries/y-stack.js +2 -0
  172. package/dist/entries/y-stack.js.map +1 -0
  173. package/dist/index.d.ts +12 -0
  174. package/dist/index.d.ts.map +1 -1
  175. package/dist/index.js +12 -0
  176. package/dist/index.js.map +1 -1
  177. package/package.json +8 -3
  178. package/src/base.css +23 -5
  179. package/src/components/Badge.tsx +1 -1
  180. package/src/components/Button.tsx +27 -23
  181. package/src/components/Card.tsx +12 -8
  182. package/src/components/Flex.stories.tsx +58 -0
  183. package/src/components/Flex.test.tsx +57 -0
  184. package/src/components/Flex.tsx +52 -0
  185. package/src/components/Grid.stories.tsx +55 -0
  186. package/src/components/Grid.test.tsx +140 -0
  187. package/src/components/Grid.tsx +166 -0
  188. package/src/components/Separator.stories.tsx +39 -0
  189. package/src/components/Separator.test.tsx +25 -0
  190. package/src/components/Separator.tsx +30 -0
  191. package/src/components/Spinner.stories.tsx +35 -0
  192. package/src/components/Spinner.test.tsx +30 -0
  193. package/src/components/Spinner.tsx +75 -0
  194. package/src/components/Text.stories.tsx +49 -0
  195. package/src/components/Text.tsx +5 -4
  196. package/src/components/XStack.stories.tsx +46 -0
  197. package/src/components/XStack.test.tsx +13 -0
  198. package/src/components/XStack.tsx +7 -0
  199. package/src/components/YStack.stories.tsx +46 -0
  200. package/src/components/YStack.test.tsx +13 -0
  201. package/src/components/YStack.tsx +7 -0
  202. package/src/components/_internal/InputFrame.tsx +50 -0
  203. package/src/components/_internal/StoryItem.tsx +24 -0
  204. package/src/components/_internal/layout.ts +103 -0
  205. package/src/components/_internal/useControllableState.ts +25 -0
  206. package/src/components/form/Checkbox.stories.tsx +67 -0
  207. package/src/components/form/Checkbox.test.tsx +88 -0
  208. package/src/components/form/Checkbox.tsx +167 -0
  209. package/src/components/form/Form.context.ts +91 -0
  210. package/src/components/form/Form.stories.tsx +152 -0
  211. package/src/components/form/Form.test.tsx +195 -0
  212. package/src/components/form/Form.tsx +51 -0
  213. package/src/components/form/FormCheckbox.tsx +42 -0
  214. package/src/components/form/FormDescription.tsx +14 -0
  215. package/src/components/form/FormError.tsx +20 -0
  216. package/src/components/form/FormField.tsx +82 -0
  217. package/src/components/form/FormInput.tsx +70 -0
  218. package/src/components/form/FormInputGroup.tsx +30 -0
  219. package/src/components/form/FormLabel.tsx +17 -0
  220. package/src/components/form/FormRadioGroup.tsx +48 -0
  221. package/src/components/form/FormRoot.tsx +41 -0
  222. package/src/components/form/FormTextarea.tsx +56 -0
  223. package/src/components/form/Input.stories.tsx +105 -0
  224. package/src/components/form/Input.test.tsx +76 -0
  225. package/src/components/form/Input.tsx +81 -0
  226. package/src/components/form/InputGroup.context.ts +11 -0
  227. package/src/components/form/InputGroup.stories.tsx +129 -0
  228. package/src/components/form/InputGroup.test.tsx +95 -0
  229. package/src/components/form/InputGroup.tsx +266 -0
  230. package/src/components/form/RadioGroup.stories.tsx +62 -0
  231. package/src/components/form/RadioGroup.test.tsx +98 -0
  232. package/src/components/form/RadioGroup.tsx +151 -0
  233. package/src/components/form/Textarea.stories.tsx +61 -0
  234. package/src/components/form/Textarea.test.tsx +35 -0
  235. package/src/components/form/Textarea.tsx +74 -0
  236. package/src/entries/checkbox.ts +2 -0
  237. package/src/entries/flex.ts +2 -0
  238. package/src/entries/form.ts +13 -0
  239. package/src/entries/grid.ts +2 -0
  240. package/src/entries/input-group.ts +8 -0
  241. package/src/entries/input.ts +2 -0
  242. package/src/entries/radio-group.ts +5 -0
  243. package/src/entries/separator.ts +2 -0
  244. package/src/entries/spinner.ts +2 -0
  245. package/src/entries/textarea.ts +2 -0
  246. package/src/entries/x-stack.ts +2 -0
  247. package/src/entries/y-stack.ts +2 -0
  248. package/src/fonts.test.tsx +244 -0
  249. package/src/index.ts +12 -0
  250. package/src/styles.css +12 -0
  251. package/dist/components/_internal/Spinner.d.ts +0 -9
  252. package/dist/components/_internal/Spinner.d.ts.map +0 -1
  253. package/dist/components/_internal/Spinner.js +0 -31
  254. package/dist/components/_internal/Spinner.js.map +0 -1
  255. package/src/components/_internal/Spinner.tsx +0 -49
package/src/base.css CHANGED
@@ -1,11 +1,29 @@
1
1
  /**
2
2
  * Pickle's components as a content source, so Uniwind generates the classes
3
- * they use, and the type scale behind `Text`'s variants, shared with
4
- * `@pickle-ui/react`. `styles.css` imports this file; import it alone, after
5
- * `uniwind`, to bring your own token set instead of
6
- * `@pickle-ui/tokens/native.css`. The type scale only declares theme
7
- * variables, so it compiles in any order.
3
+ * they use, the type scale behind `Text`'s variants, shared with
4
+ * `@pickle-ui/react`, and the native font defaults. `styles.css` imports this
5
+ * file; import it alone, after `uniwind`, to bring your own token set instead
6
+ * of `@pickle-ui/tokens/native.css`. It only declares theme variables, so it
7
+ * compiles in any order.
8
8
  */
9
9
  @import '@pickle-ui/tokens/typography.css';
10
10
 
11
11
  @source './components/**/*.{ts,tsx}';
12
+
13
+ /*
14
+ * The components' text uses `font-sans`, and their headings `font-heading`.
15
+ * Until the app declares `--font-sans`, it reads a variable nothing declares,
16
+ * so it sets no family and text keeps the system font: an explicit system
17
+ * family would also drop the medium and semibold weights on Android.
18
+ * `default` lets any `--font-*` an app or its token set declares win, and
19
+ * replaces Tailwind's web font stacks, which native can't resolve.
20
+ *
21
+ * This default is Pickle's, not Uniwind's. Uniwind suggests per-platform
22
+ * values instead, but `@variant ios` doesn't compile inside `@theme`, and
23
+ * inside `@layer theme` it overrides the app's own `--font-sans`.
24
+ */
25
+ @theme default {
26
+ --font-sans: var(--pickle-no-font);
27
+ --font-heading: var(--font-sans);
28
+ --font-mono: var(--pickle-no-font);
29
+ }
@@ -35,7 +35,7 @@ const badgeVariants = cva(
35
35
  },
36
36
  );
37
37
 
38
- const labelVariants = cva('font-medium', {
38
+ const labelVariants = cva('font-sans font-medium', {
39
39
  defaultVariants: { size: 'md', variant: 'default' },
40
40
  variants: {
41
41
  size: {
@@ -3,11 +3,12 @@ import { Children, isValidElement } from 'react';
3
3
  import { type Insets, Pressable, Text, View } from 'react-native';
4
4
 
5
5
  import { cn } from '../lib/cn';
6
- import { Spinner } from './_internal/Spinner';
6
+ import { Spinner } from './Spinner';
7
7
 
8
8
  // Same variants, sizes and dimensions as `Button` in @pickle-ui/react on a
9
- // phone. Like the web's `pointer-coarse` hit area, `hitSlop` stretches every
10
- // size to a 44pt touch target. Paddings leave room for the 1pt border.
9
+ // phone, with its text sizes from `sm` up. `hitSlop` stretches every size to a
10
+ // 44pt touch target, like the web's `pointer-coarse` hit area. Paddings leave
11
+ // room for the 1pt border.
11
12
  const buttonVariants = cva(
12
13
  'flex-row items-center justify-center gap-2 rounded-lg border disabled:opacity-64',
13
14
  {
@@ -42,24 +43,26 @@ const buttonVariants = cva(
42
43
  },
43
44
  );
44
45
 
45
- const labelVariants = cva('font-medium', {
46
+ const labelVariants = cva('font-sans font-medium', {
46
47
  compoundVariants: [
47
48
  { className: 'underline', pressed: true, variant: 'link' },
48
49
  ],
49
50
  defaultVariants: { size: 'md', variant: 'default' },
50
51
  variants: {
51
52
  pressed: { false: '', true: '' },
53
+ // The web's sizes from `sm` up, not its larger phone sizes, which only
54
+ // stop iOS Safari zooming into focused fields.
52
55
  size: {
53
- icon: 'text-base',
54
- 'icon-lg': 'text-base',
55
- 'icon-sm': 'text-base',
56
- 'icon-xl': 'text-base',
57
- 'icon-xs': 'text-base',
58
- lg: 'text-base',
59
- md: 'text-base',
60
- sm: 'text-base',
61
- xl: 'text-lg',
62
- xs: 'text-sm',
56
+ icon: 'text-sm',
57
+ 'icon-lg': 'text-sm',
58
+ 'icon-sm': 'text-sm',
59
+ 'icon-xl': 'text-sm',
60
+ 'icon-xs': 'text-sm',
61
+ lg: 'text-sm',
62
+ md: 'text-sm',
63
+ sm: 'text-sm',
64
+ xl: 'text-base',
65
+ xs: 'text-xs',
63
66
  },
64
67
  variant: {
65
68
  default: 'text-primary-foreground',
@@ -73,16 +76,17 @@ const labelVariants = cva('font-medium', {
73
76
  },
74
77
  });
75
78
 
76
- const spinnerVariants = cva('', {
79
+ // Sized and dimmed like the web button's icons, in the label's color.
80
+ const spinnerVariants = cva('size-4 opacity-80', {
77
81
  variants: {
78
82
  variant: {
79
- default: 'border-primary-foreground',
80
- destructive: 'border-destructive-foreground',
81
- 'destructive-outline': 'border-foreground',
82
- ghost: 'border-foreground',
83
- link: 'border-foreground',
84
- outline: 'border-foreground',
85
- secondary: 'border-secondary-foreground',
83
+ default: 'text-primary-foreground',
84
+ destructive: 'text-destructive-foreground',
85
+ 'destructive-outline': 'text-foreground',
86
+ ghost: 'text-foreground',
87
+ link: 'text-foreground',
88
+ outline: 'text-foreground',
89
+ secondary: 'text-secondary-foreground',
86
90
  },
87
91
  },
88
92
  });
@@ -198,7 +202,7 @@ export function Button({
198
202
  )}
199
203
  {loading ? (
200
204
  <View className="absolute">
201
- <Spinner className={spinnerVariants({ variant })} />
205
+ <Spinner aria-hidden className={spinnerVariants({ variant })} />
202
206
  </View>
203
207
  ) : null}
204
208
  </>
@@ -45,10 +45,13 @@ function CardFooter({ className, ...props }: CardFooterProps) {
45
45
  * like the web header's grid.
46
46
  */
47
47
  function CardHeader({ children, className, ...props }: CardHeaderProps) {
48
- const parts = Children.toArray(children);
49
- const actions = parts.filter(
50
- (child) => isValidElement(child) && child.type === CardAction,
51
- );
48
+ const actions: React.ReactNode[] = [];
49
+ const rest: React.ReactNode[] = [];
50
+ for (const child of Children.toArray(children)) {
51
+ (isValidElement(child) && child.type === CardAction ? actions : rest).push(
52
+ child,
53
+ );
54
+ }
52
55
 
53
56
  if (actions.length === 0) {
54
57
  return (
@@ -63,9 +66,7 @@ function CardHeader({ children, className, ...props }: CardHeaderProps) {
63
66
  className={cn('flex-row items-start gap-4 px-6', className)}
64
67
  {...props}
65
68
  >
66
- <View className="flex-1 gap-1.5">
67
- {parts.filter((child) => !actions.includes(child))}
68
- </View>
69
+ <View className="flex-1 gap-1.5">{rest}</View>
69
70
  {actions}
70
71
  </View>
71
72
  );
@@ -86,7 +87,10 @@ function CardRoot({ className, ...props }: CardProps) {
86
87
  function CardTitle({ className, ...props }: CardTitleProps) {
87
88
  return (
88
89
  <Text
89
- className={cn('text-card-foreground leading-none', className)}
90
+ className={cn(
91
+ 'text-card-foreground font-heading leading-none',
92
+ className,
93
+ )}
90
94
  variant="h3"
91
95
  {...props}
92
96
  />
@@ -0,0 +1,58 @@
1
+ import type { Meta, StoryObj } from '@storybook/react-native-web-vite';
2
+
3
+ import { Story } from './_internal/Story';
4
+ import { StoryItem as Item } from './_internal/StoryItem';
5
+ import { Flex } from './Flex';
6
+
7
+ const meta = {
8
+ component: Flex,
9
+ parameters: { layout: 'fullscreen' },
10
+ title: 'components/Flex',
11
+ } satisfies Meta<typeof Flex>;
12
+
13
+ export default meta;
14
+ type StoryDefinition = StoryObj<typeof meta>;
15
+
16
+ export const All: StoryDefinition = {
17
+ render: () => (
18
+ <Story.Layout className="max-w-6xl" title="Flex">
19
+ <Story.Section title="Direction and spacing">
20
+ <Flex direction="column" gap={3}>
21
+ <Item className="min-h-16">First</Item>
22
+ <Item className="min-h-16">Second</Item>
23
+ <Item className="min-h-16">Third</Item>
24
+ </Flex>
25
+ </Story.Section>
26
+
27
+ <Story.Section title="Alignment and distribution">
28
+ <Flex
29
+ align="center"
30
+ className="border-border min-h-40 rounded-md border border-dashed p-4"
31
+ gap={4}
32
+ justify="between"
33
+ >
34
+ <Item className="min-h-16 min-w-20">Start</Item>
35
+ <Item className="min-h-16 min-w-20">Center</Item>
36
+ <Item className="min-h-16 min-w-20">End</Item>
37
+ </Flex>
38
+ </Story.Section>
39
+
40
+ <Story.Section title="Wrapping">
41
+ <Flex gap={3} wrap="wrap">
42
+ {['One', 'Two', 'Three', 'Four', 'Five', 'Six'].map((label) => (
43
+ <Item className="min-h-16 min-w-24" key={label}>
44
+ {label}
45
+ </Item>
46
+ ))}
47
+ </Flex>
48
+ </Story.Section>
49
+
50
+ <Story.Section title="Responsive override">
51
+ <Flex className="flex-col md:flex-row" gap={3}>
52
+ <Item className="min-h-16">Column on small screens</Item>
53
+ <Item className="min-h-16">Row from md</Item>
54
+ </Flex>
55
+ </Story.Section>
56
+ </Story.Layout>
57
+ ),
58
+ };
@@ -0,0 +1,57 @@
1
+ import { render, screen } from '@testing-library/react-native';
2
+
3
+ import { Flex } from './Flex';
4
+
5
+ function classesOf(testID: string) {
6
+ return String(screen.getByTestId(testID).props.className).split(' ');
7
+ }
8
+
9
+ describe('Flex', () => {
10
+ it('lays its children out in a row, like the web', async () => {
11
+ await render(<Flex testID="flex" />);
12
+
13
+ expect(classesOf('flex')).toContain('flex-row');
14
+ });
15
+
16
+ it('renders layout props as Tailwind classes', async () => {
17
+ await render(
18
+ <Flex
19
+ align="center"
20
+ direction="column"
21
+ gap={4}
22
+ gapX={2}
23
+ gapY={3}
24
+ justify="between"
25
+ testID="flex"
26
+ wrap="wrap"
27
+ />,
28
+ );
29
+
30
+ expect(classesOf('flex')).toEqual(
31
+ expect.arrayContaining([
32
+ 'flex-col',
33
+ 'flex-wrap',
34
+ 'items-center',
35
+ 'justify-between',
36
+ 'gap-4',
37
+ 'gap-x-2',
38
+ 'gap-y-3',
39
+ ]),
40
+ );
41
+ });
42
+
43
+ it('lets className override the props', async () => {
44
+ await render(
45
+ <Flex
46
+ className="custom-class flex-row"
47
+ direction="column"
48
+ testID="flex"
49
+ />,
50
+ );
51
+
52
+ expect(classesOf('flex')).toEqual(
53
+ expect.arrayContaining(['flex-row', 'custom-class']),
54
+ );
55
+ expect(classesOf('flex')).not.toContain('flex-col');
56
+ });
57
+ });
@@ -0,0 +1,52 @@
1
+ import { View } from 'react-native';
2
+
3
+ import { cn } from '../lib/cn';
4
+ import { getLayoutClasses, type LayoutStyleProps } from './_internal/layout';
5
+
6
+ const directionClasses = {
7
+ column: 'flex-col',
8
+ 'column-reverse': 'flex-col-reverse',
9
+ row: 'flex-row',
10
+ 'row-reverse': 'flex-row-reverse',
11
+ } as const;
12
+
13
+ const wrapClasses = {
14
+ nowrap: 'flex-nowrap',
15
+ wrap: 'flex-wrap',
16
+ 'wrap-reverse': 'flex-wrap-reverse',
17
+ } as const;
18
+
19
+ export type FlexProps = LayoutStyleProps &
20
+ Omit<React.ComponentProps<typeof View>, keyof LayoutStyleProps> & {
21
+ direction?: keyof typeof directionClasses;
22
+ wrap?: keyof typeof wrapClasses;
23
+ };
24
+
25
+ /**
26
+ * A `View` laid out with flexbox props, like `Flex` in @pickle-ui/react. It
27
+ * defaults to a row, as on the web, where React Native's `View` stacks its
28
+ * children in a column.
29
+ */
30
+ export function Flex({
31
+ align,
32
+ className,
33
+ direction = 'row',
34
+ gap,
35
+ gapX,
36
+ gapY,
37
+ justify,
38
+ wrap,
39
+ ...props
40
+ }: FlexProps) {
41
+ return (
42
+ <View
43
+ className={cn(
44
+ getLayoutClasses({ align, gap, gapX, gapY, justify }),
45
+ directionClasses[direction],
46
+ wrap === undefined ? undefined : wrapClasses[wrap],
47
+ className,
48
+ )}
49
+ {...props}
50
+ />
51
+ );
52
+ }
@@ -0,0 +1,55 @@
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 { Grid } from './Grid';
6
+
7
+ const meta = {
8
+ component: Grid,
9
+ parameters: { layout: 'fullscreen' },
10
+ title: 'components/Grid',
11
+ } satisfies Meta<typeof Grid>;
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="Grid">
19
+ <Story.Section title="Columns and gaps">
20
+ <Grid columns={3} gap={4}>
21
+ {['One', 'Two', 'Three', 'Four', 'Five', 'Six', 'Seven'].map(
22
+ (label) => (
23
+ <Item className="min-h-20" key={label}>
24
+ {label}
25
+ </Item>
26
+ ),
27
+ )}
28
+ </Grid>
29
+ </Story.Section>
30
+
31
+ <Story.Section title="Rows and column flow">
32
+ <Grid flow="column" gapX={4} gapY={2} rows={2}>
33
+ {['A', 'B', 'C', 'D', 'E', 'F'].map((label) => (
34
+ <Item className="min-h-20" key={label}>
35
+ {label}
36
+ </Item>
37
+ ))}
38
+ </Grid>
39
+ </Story.Section>
40
+
41
+ <Story.Section title="Placement">
42
+ <Grid
43
+ className="border-border rounded-md border border-dashed p-4"
44
+ columns={3}
45
+ gap={4}
46
+ placeItems="center"
47
+ >
48
+ <Item className="min-h-40">Tall item</Item>
49
+ <Item>Centered</Item>
50
+ <Item className="flex-1">Full height</Item>
51
+ </Grid>
52
+ </Story.Section>
53
+ </Story.Layout>
54
+ ),
55
+ };
@@ -0,0 +1,140 @@
1
+ import { render, screen, within } from '@testing-library/react-native';
2
+ import { Text } from 'react-native';
3
+
4
+ import { Grid } from './Grid';
5
+
6
+ const LABELS = ['A', 'B', 'C', 'D', 'E'];
7
+
8
+ /** The labels of each grid row, empty cells as `''`. */
9
+ function gridRows() {
10
+ return screen
11
+ .getByTestId('grid')
12
+ .children.map((row) =>
13
+ typeof row === 'string'
14
+ ? []
15
+ : row.children.map((cell) =>
16
+ typeof cell === 'string'
17
+ ? cell
18
+ : (within(cell).queryByText(/./)?.props.children ?? ''),
19
+ ),
20
+ );
21
+ }
22
+
23
+ function renderGrid(props: React.ComponentProps<typeof Grid>) {
24
+ return render(
25
+ <Grid testID="grid" {...props}>
26
+ {LABELS.map((label) => (
27
+ <Text key={label}>{label}</Text>
28
+ ))}
29
+ </Grid>,
30
+ );
31
+ }
32
+
33
+ describe('Grid', () => {
34
+ it('fills rows first, keeping empty cells in the last row', async () => {
35
+ await renderGrid({ columns: 3 });
36
+
37
+ expect(gridRows()).toEqual([
38
+ ['A', 'B', 'C'],
39
+ ['D', 'E', ''],
40
+ ]);
41
+ });
42
+
43
+ it('fills columns first with flow="column"', async () => {
44
+ await renderGrid({ flow: 'column', rows: 2 });
45
+
46
+ expect(gridRows()).toEqual([
47
+ ['A', 'C', 'E'],
48
+ ['B', 'D', ''],
49
+ ]);
50
+ });
51
+
52
+ it('keeps the rows it is given, even empty ones', async () => {
53
+ await renderGrid({ flow: 'column', rows: 3 });
54
+
55
+ expect(gridRows()).toEqual([
56
+ ['A', 'D'],
57
+ ['B', 'E'],
58
+ ['C', ''],
59
+ ]);
60
+
61
+ await renderGrid({ columns: 2, rows: 4 });
62
+
63
+ expect(gridRows()).toEqual([
64
+ ['A', 'B'],
65
+ ['C', 'D'],
66
+ ['E', ''],
67
+ ['', ''],
68
+ ]);
69
+ });
70
+
71
+ it('places the dense flows like row and column', async () => {
72
+ await renderGrid({ flow: 'column-dense', rows: 2 });
73
+
74
+ expect(gridRows()).toEqual([
75
+ ['A', 'C', 'E'],
76
+ ['B', 'D', ''],
77
+ ]);
78
+ });
79
+
80
+ it('gives each child of a fragment its own cell', async () => {
81
+ await render(
82
+ <Grid columns={2} testID="grid">
83
+ <>
84
+ <Text>A</Text>
85
+ <Text>B</Text>
86
+ </>
87
+ <Text>C</Text>
88
+ </Grid>,
89
+ );
90
+
91
+ expect(gridRows()).toEqual([
92
+ ['A', 'B'],
93
+ ['C', ''],
94
+ ]);
95
+ });
96
+
97
+ it('distributes rows and cells with placeContent', async () => {
98
+ await renderGrid({ columns: 2, justify: 'end', placeContent: 'stretch' });
99
+
100
+ const grid = screen.getByTestId('grid');
101
+ expect(grid).toHaveProp('className', 'justify-start');
102
+ expect(grid.children[0]).toHaveProp(
103
+ 'className',
104
+ 'flex-row justify-start grow',
105
+ );
106
+
107
+ await renderGrid({ columns: 2, justify: 'end' });
108
+
109
+ expect(screen.getByTestId('grid').children[0]).toHaveProp(
110
+ 'className',
111
+ 'flex-row justify-end',
112
+ );
113
+ });
114
+
115
+ it('stacks its children in one column by default', async () => {
116
+ await renderGrid({});
117
+
118
+ expect(gridRows()).toEqual([['A'], ['B'], ['C'], ['D'], ['E']]);
119
+ });
120
+
121
+ it('spaces rows with gapY and columns with gapX', async () => {
122
+ await renderGrid({ columns: 2, gap: 4, gapX: 2 });
123
+
124
+ const grid = screen.getByTestId('grid');
125
+ expect(grid).toHaveProp('className', 'gap-y-4');
126
+ expect(grid.children[0]).toHaveProp('className', 'flex-row gap-x-2');
127
+ });
128
+
129
+ it('places items in their cells', async () => {
130
+ await renderGrid({ align: 'end', columns: 2 });
131
+ const cell = screen.getByText('A').parent;
132
+ expect(cell).toHaveProp('className', 'flex-1 justify-end');
133
+
134
+ await renderGrid({ align: 'end', columns: 2, placeItems: 'center' });
135
+ expect(screen.getByText('A').parent).toHaveProp(
136
+ 'className',
137
+ 'flex-1 items-center justify-center',
138
+ );
139
+ });
140
+ });