@olympusoss/canvas 3.2.1 → 5.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 (302) hide show
  1. package/README.md +75 -65
  2. package/package.json +11 -5
  3. package/src/atoms/avatar/avatar.md +185 -0
  4. package/src/atoms/avatar/avatar.styles.ts +48 -0
  5. package/src/atoms/avatar/avatar.tsx +99 -0
  6. package/src/atoms/badge/badge.md +237 -0
  7. package/src/atoms/badge/badge.styles.ts +79 -0
  8. package/src/atoms/badge/badge.tsx +86 -0
  9. package/src/atoms/breadcrumb/breadcrumb.md +233 -0
  10. package/src/atoms/breadcrumb/breadcrumb.styles.ts +40 -0
  11. package/src/atoms/breadcrumb/breadcrumb.tsx +130 -0
  12. package/src/atoms/button/button.android.tsx +6 -0
  13. package/src/atoms/button/button.ios.tsx +6 -0
  14. package/src/atoms/button/button.md +184 -0
  15. package/src/atoms/button/button.shared.tsx +79 -0
  16. package/src/atoms/button/button.styles.ts +152 -0
  17. package/src/atoms/button/button.tsx +6 -0
  18. package/src/atoms/button-group/button-group.android.tsx +6 -0
  19. package/src/atoms/button-group/button-group.ios.tsx +6 -0
  20. package/src/atoms/button-group/button-group.md +120 -0
  21. package/src/atoms/button-group/button-group.shared.tsx +398 -0
  22. package/src/atoms/button-group/button-group.styles.ts +483 -0
  23. package/src/atoms/button-group/button-group.tsx +6 -0
  24. package/src/atoms/checkbox/checkbox.android.tsx +6 -0
  25. package/src/atoms/checkbox/checkbox.ios.tsx +6 -0
  26. package/src/atoms/checkbox/checkbox.md +150 -0
  27. package/src/atoms/checkbox/checkbox.shared.tsx +103 -0
  28. package/src/atoms/checkbox/checkbox.styles.ts +106 -0
  29. package/src/atoms/checkbox/checkbox.tsx +6 -0
  30. package/src/atoms/combobox/combobox.android.tsx +6 -0
  31. package/src/atoms/combobox/combobox.ios.tsx +6 -0
  32. package/src/atoms/combobox/combobox.md +213 -0
  33. package/src/atoms/combobox/combobox.shared.tsx +160 -0
  34. package/src/atoms/combobox/combobox.styles.ts +270 -0
  35. package/src/atoms/combobox/combobox.tsx +6 -0
  36. package/src/atoms/divider/divider.md +140 -0
  37. package/src/atoms/divider/divider.styles.ts +35 -0
  38. package/src/atoms/divider/divider.tsx +67 -0
  39. package/src/atoms/dropdown/dropdown.android.tsx +6 -0
  40. package/src/atoms/dropdown/dropdown.ios.tsx +6 -0
  41. package/src/atoms/dropdown/dropdown.md +221 -0
  42. package/src/atoms/dropdown/dropdown.shared.tsx +190 -0
  43. package/src/atoms/dropdown/dropdown.styles.ts +233 -0
  44. package/src/atoms/dropdown/dropdown.tsx +6 -0
  45. package/src/atoms/icon/icon.md +131 -0
  46. package/src/atoms/icon/icon.styles.ts +30 -0
  47. package/src/atoms/icon/icon.tsx +328 -0
  48. package/src/atoms/index.ts +24 -0
  49. package/src/atoms/input/input.android.tsx +6 -0
  50. package/src/atoms/input/input.ios.tsx +6 -0
  51. package/src/atoms/input/input.md +118 -0
  52. package/src/atoms/input/input.shared.tsx +203 -0
  53. package/src/atoms/input/input.styles.ts +286 -0
  54. package/src/atoms/input/input.tsx +6 -0
  55. package/src/atoms/kbd/kbd.md +91 -0
  56. package/src/atoms/kbd/kbd.styles.ts +33 -0
  57. package/src/atoms/kbd/kbd.tsx +27 -0
  58. package/src/atoms/listbox/listbox.md +177 -0
  59. package/src/atoms/listbox/listbox.styles.ts +60 -0
  60. package/src/atoms/listbox/listbox.tsx +113 -0
  61. package/src/atoms/pagination/pagination.android.tsx +6 -0
  62. package/src/atoms/pagination/pagination.ios.tsx +6 -0
  63. package/src/atoms/pagination/pagination.md +133 -0
  64. package/src/atoms/pagination/pagination.shared.tsx +289 -0
  65. package/src/atoms/pagination/pagination.styles.ts +245 -0
  66. package/src/atoms/pagination/pagination.tsx +6 -0
  67. package/src/atoms/popover/popover.android.tsx +8 -0
  68. package/src/atoms/popover/popover.ios.tsx +6 -0
  69. package/src/atoms/popover/popover.md +87 -0
  70. package/src/atoms/popover/popover.shared.tsx +124 -0
  71. package/src/atoms/popover/popover.styles.ts +144 -0
  72. package/src/atoms/popover/popover.tsx +6 -0
  73. package/src/atoms/radio/radio.android.tsx +6 -0
  74. package/src/atoms/radio/radio.ios.tsx +6 -0
  75. package/src/atoms/radio/radio.md +173 -0
  76. package/src/atoms/radio/radio.shared.tsx +98 -0
  77. package/src/atoms/radio/radio.styles.ts +109 -0
  78. package/src/atoms/radio/radio.tsx +6 -0
  79. package/src/atoms/select/select.android.tsx +6 -0
  80. package/src/atoms/select/select.ios.tsx +6 -0
  81. package/src/atoms/select/select.md +156 -0
  82. package/src/atoms/select/select.shared.tsx +143 -0
  83. package/src/atoms/select/select.styles.ts +310 -0
  84. package/src/atoms/select/select.tsx +6 -0
  85. package/src/atoms/skeleton/skeleton.md +135 -0
  86. package/src/atoms/skeleton/skeleton.styles.ts +117 -0
  87. package/src/atoms/skeleton/skeleton.tsx +145 -0
  88. package/src/atoms/spinner/spinner.android.tsx +7 -0
  89. package/src/atoms/spinner/spinner.ios.tsx +7 -0
  90. package/src/atoms/spinner/spinner.md +94 -0
  91. package/src/atoms/spinner/spinner.shared.tsx +92 -0
  92. package/src/atoms/spinner/spinner.styles.tsx +115 -0
  93. package/src/atoms/spinner/spinner.tsx +7 -0
  94. package/src/atoms/switch/switch.android.tsx +6 -0
  95. package/src/atoms/switch/switch.ios.tsx +6 -0
  96. package/src/atoms/switch/switch.md +91 -0
  97. package/src/atoms/switch/switch.shared.tsx +97 -0
  98. package/src/atoms/switch/switch.styles.ts +79 -0
  99. package/src/atoms/switch/switch.tsx +6 -0
  100. package/src/atoms/textarea/textarea.android.tsx +6 -0
  101. package/src/atoms/textarea/textarea.ios.tsx +6 -0
  102. package/src/atoms/textarea/textarea.md +140 -0
  103. package/src/atoms/textarea/textarea.shared.tsx +74 -0
  104. package/src/atoms/textarea/textarea.styles.ts +116 -0
  105. package/src/atoms/textarea/textarea.tsx +6 -0
  106. package/src/atoms/tooltip/tooltip.android.tsx +6 -0
  107. package/src/atoms/tooltip/tooltip.ios.tsx +7 -0
  108. package/src/atoms/tooltip/tooltip.md +122 -0
  109. package/src/atoms/tooltip/tooltip.shared.tsx +113 -0
  110. package/src/atoms/tooltip/tooltip.styles.ts +113 -0
  111. package/src/atoms/tooltip/tooltip.tsx +6 -0
  112. package/src/atoms/typography/typography.md +330 -0
  113. package/src/atoms/typography/typography.styles.ts +95 -0
  114. package/src/atoms/typography/typography.tsx +76 -0
  115. package/src/index.ts +12 -2
  116. package/src/molecules/action-panels/action-panels.md +133 -0
  117. package/src/molecules/action-panels/action-panels.styles.ts +39 -0
  118. package/src/molecules/action-panels/action-panels.tsx +113 -0
  119. package/src/molecules/alert/alert.md +119 -0
  120. package/src/molecules/alert/alert.styles.ts +88 -0
  121. package/src/molecules/alert/alert.tsx +74 -0
  122. package/src/molecules/alert-dialog/alert-dialog.android.tsx +6 -0
  123. package/src/molecules/alert-dialog/alert-dialog.ios.tsx +6 -0
  124. package/src/molecules/alert-dialog/alert-dialog.md +177 -0
  125. package/src/molecules/alert-dialog/alert-dialog.shared.tsx +187 -0
  126. package/src/molecules/alert-dialog/alert-dialog.styles.ts +248 -0
  127. package/src/molecules/alert-dialog/alert-dialog.tsx +6 -0
  128. package/src/molecules/card/card.md +190 -0
  129. package/src/molecules/card/card.styles.ts +67 -0
  130. package/src/molecules/card/card.tsx +176 -0
  131. package/src/molecules/code-block/code-block.md +159 -0
  132. package/src/molecules/code-block/code-block.styles.ts +167 -0
  133. package/src/molecules/code-block/code-block.tsx +176 -0
  134. package/src/molecules/description-lists/description-lists.md +129 -0
  135. package/src/molecules/description-lists/description-lists.styles.ts +102 -0
  136. package/src/molecules/description-lists/description-lists.tsx +133 -0
  137. package/src/molecules/empty-state/empty-state.md +218 -0
  138. package/src/molecules/empty-state/empty-state.styles.ts +63 -0
  139. package/src/molecules/empty-state/empty-state.tsx +77 -0
  140. package/src/molecules/feeds/feeds.md +102 -0
  141. package/src/molecules/feeds/feeds.styles.ts +120 -0
  142. package/src/molecules/feeds/feeds.tsx +167 -0
  143. package/src/molecules/field/field.md +117 -0
  144. package/src/molecules/field/field.styles.ts +85 -0
  145. package/src/molecules/field/field.tsx +175 -0
  146. package/src/molecules/fieldset/fieldset.md +141 -0
  147. package/src/molecules/fieldset/fieldset.styles.ts +79 -0
  148. package/src/molecules/fieldset/fieldset.tsx +182 -0
  149. package/src/molecules/form/form.md +137 -0
  150. package/src/molecules/form/form.styles.ts +39 -0
  151. package/src/molecules/form/form.tsx +246 -0
  152. package/src/molecules/grid-lists/grid-lists.md +114 -0
  153. package/src/molecules/grid-lists/grid-lists.styles.ts +79 -0
  154. package/src/molecules/grid-lists/grid-lists.tsx +157 -0
  155. package/src/molecules/index.ts +16 -0
  156. package/src/molecules/media-objects/media-objects.md +87 -0
  157. package/src/molecules/media-objects/media-objects.styles.ts +94 -0
  158. package/src/molecules/media-objects/media-objects.tsx +128 -0
  159. package/src/molecules/stacked-lists/stacked-lists.md +116 -0
  160. package/src/molecules/stacked-lists/stacked-lists.styles.ts +111 -0
  161. package/src/molecules/stacked-lists/stacked-lists.tsx +195 -0
  162. package/src/molecules/stats/stats.md +166 -0
  163. package/src/molecules/stats/stats.styles.ts +91 -0
  164. package/src/molecules/stats/stats.tsx +88 -0
  165. package/src/organisms/calendar/calendar.android.tsx +6 -0
  166. package/src/organisms/calendar/calendar.ios.tsx +6 -0
  167. package/src/organisms/calendar/calendar.md +114 -0
  168. package/src/organisms/calendar/calendar.shared.tsx +146 -0
  169. package/src/organisms/calendar/calendar.styles.ts +315 -0
  170. package/src/organisms/calendar/calendar.tsx +6 -0
  171. package/src/organisms/charts/charts.md +326 -0
  172. package/src/organisms/charts/charts.styles.ts +135 -0
  173. package/src/organisms/charts/charts.tsx +124 -0
  174. package/src/organisms/command/command.md +117 -0
  175. package/src/organisms/command/command.styles.ts +179 -0
  176. package/src/organisms/command/command.tsx +164 -0
  177. package/src/organisms/data-table/data-table.md +182 -0
  178. package/src/organisms/data-table/data-table.styles.ts +103 -0
  179. package/src/organisms/data-table/data-table.tsx +105 -0
  180. package/src/organisms/dialog/dialog.android.tsx +6 -0
  181. package/src/organisms/dialog/dialog.ios.tsx +6 -0
  182. package/src/organisms/dialog/dialog.md +271 -0
  183. package/src/organisms/dialog/dialog.shared.tsx +230 -0
  184. package/src/organisms/dialog/dialog.styles.ts +272 -0
  185. package/src/organisms/dialog/dialog.tsx +6 -0
  186. package/src/organisms/filter-panel/filter-panel.md +116 -0
  187. package/src/organisms/filter-panel/filter-panel.styles.ts +83 -0
  188. package/src/organisms/filter-panel/filter-panel.tsx +91 -0
  189. package/src/organisms/index.ts +13 -0
  190. package/src/organisms/navbars/navbars.android.tsx +6 -0
  191. package/src/organisms/navbars/navbars.ios.tsx +6 -0
  192. package/src/organisms/navbars/navbars.md +144 -0
  193. package/src/organisms/navbars/navbars.shared.tsx +137 -0
  194. package/src/organisms/navbars/navbars.styles.ts +251 -0
  195. package/src/organisms/navbars/navbars.tsx +6 -0
  196. package/src/organisms/overlays/overlays.android.tsx +6 -0
  197. package/src/organisms/overlays/overlays.ios.tsx +6 -0
  198. package/src/organisms/overlays/overlays.md +123 -0
  199. package/src/organisms/overlays/overlays.shared.tsx +175 -0
  200. package/src/organisms/overlays/overlays.styles.ts +309 -0
  201. package/src/organisms/overlays/overlays.tsx +6 -0
  202. package/src/organisms/row-menu/row-menu.android.tsx +6 -0
  203. package/src/organisms/row-menu/row-menu.ios.tsx +6 -0
  204. package/src/organisms/row-menu/row-menu.md +102 -0
  205. package/src/organisms/row-menu/row-menu.shared.tsx +105 -0
  206. package/src/organisms/row-menu/row-menu.styles.ts +262 -0
  207. package/src/organisms/row-menu/row-menu.tsx +6 -0
  208. package/src/organisms/sidebar/sidebar.android.tsx +6 -0
  209. package/src/organisms/sidebar/sidebar.ios.tsx +6 -0
  210. package/src/organisms/sidebar/sidebar.md +188 -0
  211. package/src/organisms/sidebar/sidebar.shared.tsx +167 -0
  212. package/src/organisms/sidebar/sidebar.styles.ts +262 -0
  213. package/src/organisms/sidebar/sidebar.tsx +6 -0
  214. package/src/organisms/stepper/stepper.android.tsx +6 -0
  215. package/src/organisms/stepper/stepper.ios.tsx +6 -0
  216. package/src/organisms/stepper/stepper.md +150 -0
  217. package/src/organisms/stepper/stepper.shared.tsx +158 -0
  218. package/src/organisms/stepper/stepper.styles.ts +280 -0
  219. package/src/organisms/stepper/stepper.tsx +6 -0
  220. package/src/organisms/tabs/tabs.android.tsx +6 -0
  221. package/src/organisms/tabs/tabs.ios.tsx +6 -0
  222. package/src/organisms/tabs/tabs.md +127 -0
  223. package/src/organisms/tabs/tabs.shared.tsx +281 -0
  224. package/src/organisms/tabs/tabs.styles.ts +398 -0
  225. package/src/organisms/tabs/tabs.tsx +6 -0
  226. package/src/style/color.ts +17 -0
  227. package/src/style/index.ts +14 -0
  228. package/src/style/primitives.ts +26 -0
  229. package/src/style/responsive.ts +45 -0
  230. package/src/style/shadow.ts +21 -0
  231. package/src/style/theme.tsx +56 -0
  232. package/src/style/tokens.ts +487 -0
  233. package/styles/canvas.css +127 -74
  234. package/tsconfig.json +4 -2
  235. package/src/cn.ts +0 -3
  236. package/styles/atoms/avatar.css +0 -22
  237. package/styles/atoms/badge.css +0 -83
  238. package/styles/atoms/breadcrumb.css +0 -35
  239. package/styles/atoms/button-group.css +0 -23
  240. package/styles/atoms/button.css +0 -107
  241. package/styles/atoms/checkbox.css +0 -55
  242. package/styles/atoms/combobox.css +0 -76
  243. package/styles/atoms/dropdown.css +0 -54
  244. package/styles/atoms/icon.css +0 -8
  245. package/styles/atoms/input-group.css +0 -45
  246. package/styles/atoms/input.css +0 -56
  247. package/styles/atoms/kbd.css +0 -15
  248. package/styles/atoms/pagination.css +0 -48
  249. package/styles/atoms/popover.css +0 -14
  250. package/styles/atoms/radio.css +0 -28
  251. package/styles/atoms/select.css +0 -57
  252. package/styles/atoms/separator.css +0 -32
  253. package/styles/atoms/skeleton.css +0 -32
  254. package/styles/atoms/spinner.css +0 -26
  255. package/styles/atoms/switch.css +0 -45
  256. package/styles/atoms/textarea.css +0 -31
  257. package/styles/atoms/tooltip.css +0 -53
  258. package/styles/atoms/typography.css +0 -105
  259. package/styles/base.css +0 -17
  260. package/styles/molecules/alert.css +0 -66
  261. package/styles/molecules/card.css +0 -58
  262. package/styles/molecules/code-block.css +0 -18
  263. package/styles/molecules/empty-state.css +0 -17
  264. package/styles/molecules/field.css +0 -27
  265. package/styles/molecules/form.css +0 -27
  266. package/styles/molecules/page-header.css +0 -52
  267. package/styles/molecules/section-card.css +0 -49
  268. package/styles/molecules/stat-card.css +0 -71
  269. package/styles/molecules/toast.css +0 -95
  270. package/styles/organisms/app-shell.css +0 -46
  271. package/styles/organisms/calendar.css +0 -73
  272. package/styles/organisms/command.css +0 -95
  273. package/styles/organisms/data-table.css +0 -142
  274. package/styles/organisms/dialog.css +0 -72
  275. package/styles/organisms/filter-panel.css +0 -58
  276. package/styles/organisms/row-menu.css +0 -69
  277. package/styles/organisms/sheet.css +0 -70
  278. package/styles/organisms/sidebar.css +0 -146
  279. package/styles/organisms/stepper.css +0 -63
  280. package/styles/organisms/tabs.css +0 -40
  281. package/styles/organisms/topbar.css +0 -24
  282. package/styles/patterns/backdrops.css +0 -35
  283. package/styles/patterns/density.css +0 -66
  284. package/styles/patterns/focus.css +0 -22
  285. package/styles/patterns/glass.css +0 -85
  286. package/styles/patterns/high-contrast.css +0 -70
  287. package/styles/patterns/reduced-motion.css +0 -12
  288. package/styles/patterns/scrollbar.css +0 -10
  289. package/styles/reset.css +0 -89
  290. package/styles/tokens/colors.css +0 -108
  291. package/styles/tokens/motion.css +0 -33
  292. package/styles/tokens/radius.css +0 -10
  293. package/styles/tokens/shadows.css +0 -35
  294. package/styles/tokens/spacing.css +0 -19
  295. package/styles/tokens/typography.css +0 -6
  296. package/styles/tokens/z-index.css +0 -12
  297. package/styles/utilities/display.css +0 -66
  298. package/styles/utilities/flexbox.css +0 -240
  299. package/styles/utilities/gap.css +0 -288
  300. package/styles/utilities/grid.css +0 -138
  301. package/styles/utilities/position.css +0 -78
  302. package/styles/utilities/sizing.css +0 -138
@@ -0,0 +1,79 @@
1
+ import { type ViewStyle, type TextStyle } from "react-native";
2
+ import { type ColorTokens } from "../../style/index.js";
3
+
4
+ // Co-located Fieldset styles. Layout-only parts are static objects; anything that
5
+ // reads a color is a function of the active tokens (so the bordered surface
6
+ // follows light/dark and reads as glass when the ThemeProvider's surface is
7
+ // "glass", since tokens.card is swapped translucent at the theming level). The
8
+ // responsive collapse (two-column -> single column at the `sm` breakpoint) is
9
+ // driven by useResponsive in the component.
10
+
11
+ export type Surface = "bordered" | "plain";
12
+
13
+ // Outer container: full width, capped at 576px. (w-full max-w-[576px])
14
+ export const containerBase: ViewStyle = { width: "100%", maxWidth: 576 };
15
+
16
+ // Surface fill/border per axis. Bordered is a card-like section with border,
17
+ // radius, and padding (rounded-lg border border-border bg-card p-5); plain has
18
+ // no chrome.
19
+ export function surface(tokens: ColorTokens, s: Surface): ViewStyle {
20
+ if (s === "bordered") {
21
+ return {
22
+ borderRadius: 8,
23
+ borderWidth: 1,
24
+ borderColor: tokens.border,
25
+ backgroundColor: tokens.card,
26
+ padding: 20,
27
+ };
28
+ }
29
+ return {};
30
+ }
31
+
32
+ // The disabled dim applied to the whole group. (opacity-60)
33
+ export const disabledDim: ViewStyle = { opacity: 0.6 };
34
+
35
+ // --- header -----------------------------------------------------------------
36
+
37
+ // Header block above the controls. (mb-4)
38
+ export const header: ViewStyle = { marginBottom: 16 };
39
+
40
+ // Legend heading. (text-base font-semibold text-foreground)
41
+ export function legend(tokens: ColorTokens): TextStyle {
42
+ return { fontSize: 16, lineHeight: 24, fontWeight: "600", color: tokens.foreground };
43
+ }
44
+
45
+ // Muted supporting line under the legend. (mt-1 text-xs text-muted-foreground)
46
+ export function description(tokens: ColorTokens): TextStyle {
47
+ return { marginTop: 4, fontSize: 12, lineHeight: 16, color: tokens["muted-foreground"] };
48
+ }
49
+
50
+ // --- field rows -------------------------------------------------------------
51
+
52
+ // A field's persistent label above its control. (mb-1.5 text-sm font-medium text-foreground)
53
+ export function fieldLabel(tokens: ColorTokens): TextStyle {
54
+ return { marginBottom: 6, fontSize: 14, lineHeight: 20, fontWeight: "500", color: tokens.foreground };
55
+ }
56
+
57
+ // Muted help line beneath the control. (mt-1.5 text-xs text-muted-foreground)
58
+ export function fieldHelp(tokens: ColorTokens): TextStyle {
59
+ return { marginTop: 6, fontSize: 12, lineHeight: 16, color: tokens["muted-foreground"] };
60
+ }
61
+
62
+ // Inline error beneath the control. (mt-1.5 text-xs text-destructive)
63
+ export function fieldError(tokens: ColorTokens): TextStyle {
64
+ return { marginTop: 6, fontSize: 12, lineHeight: 16, color: tokens.destructive };
65
+ }
66
+
67
+ // A single field's wrapper. (w-full)
68
+ export const fieldWrap: ViewStyle = { width: "100%" };
69
+
70
+ // --- groups -----------------------------------------------------------------
71
+
72
+ // The checkbox group stack. (gap-2)
73
+ export const checkboxGroup: ViewStyle = { gap: 8 };
74
+
75
+ // The single-column field group. (flex-col gap-4)
76
+ export const groupColumn: ViewStyle = { flexDirection: "column", gap: 16 };
77
+
78
+ // `grow` on a two-column item. (grow)
79
+ export const itemGrow: ViewStyle = { flexGrow: 1 };
@@ -0,0 +1,182 @@
1
+ import { type ReactNode } from "react";
2
+ import { type DimensionValue } from "react-native";
3
+ import { View, Text, useTheme, useResponsive, type StyleProp, type ViewStyle } from "../../style/index.js";
4
+ import { Input } from "../../atoms/input/input.js";
5
+ import { Checkbox } from "../../atoms/checkbox/checkbox.js";
6
+ import * as s from "./fieldset.styles.js";
7
+ import { type Surface } from "./fieldset.styles.js";
8
+
9
+ // Fieldset: a labeled group of related form controls. A legend names the group,
10
+ // an optional description explains it, and a field group stacks the controls so
11
+ // the set reads as one labeled unit. For the docs playground (data-only, no JSX
12
+ // children) it also accepts a flat `items` array that renders stacked
13
+ // label + <Input> rows, or a `checkboxes` array that renders a checkbox group.
14
+ //
15
+ // Boolean-prop API: one boolean per option, grouped by axis, first-match
16
+ // precedence within an axis (mirrors Card's surfaceOf). Axes:
17
+ //
18
+ // - Surface: `bordered` (a bordered, padded card-like section) vs. the plain
19
+ // default (a borderless group).
20
+ // - Columns: `twoColumn` lays the field group out in two columns (it collapses
21
+ // to one column on small screens); omit for the single-column stack.
22
+ // - State: `disabled` dims the whole group and disables every control inside.
23
+ // - `error` flags a validation problem on the field group (shown via a default
24
+ // inline message when `items` are rendered and no per-item error is set).
25
+
26
+ export interface FieldsetItem {
27
+ /** The field's persistent label, shown above the control. */
28
+ label: string;
29
+ /** Optional example/format hint shown inside the control. */
30
+ placeholder?: string;
31
+ /** Optional value rendered into the control. */
32
+ value?: string;
33
+ /** Optional muted help line beneath the control. */
34
+ help?: string;
35
+ /** Optional inline error beneath the control (takes precedence over help). */
36
+ error?: string;
37
+ }
38
+
39
+ export interface FieldsetProps {
40
+ children?: ReactNode;
41
+ /** The group's name, rendered as the legend heading. */
42
+ legend?: string;
43
+ /** A muted supporting line beneath the legend. */
44
+ description?: string;
45
+ /** Data-only field rows: each renders a label + <Input>. */
46
+ items?: FieldsetItem[];
47
+ /** Data-only checkbox rows: each renders a <Checkbox> with the label. */
48
+ checkboxes?: { label: string; checked?: boolean }[];
49
+ // Surface (pick one; default is the plain, borderless group).
50
+ bordered?: boolean;
51
+ // Columns (orthogonal): two-column field grid, collapses to one when narrow.
52
+ twoColumn?: boolean;
53
+ // State (orthogonal booleans).
54
+ disabled?: boolean;
55
+ /** Flags a validation problem on the group. */
56
+ error?: boolean;
57
+ /** Escape hatch for layout/positioning composition (mainly width). */
58
+ style?: StyleProp<ViewStyle>;
59
+ }
60
+
61
+ // Surface precedence when more than one is passed: first match wins.
62
+ function surfaceOf(p: FieldsetProps): Surface {
63
+ if (p.bordered) return "bordered";
64
+ return "plain";
65
+ }
66
+
67
+ // One labeled field: label + control + optional help/error.
68
+ function Field({
69
+ item,
70
+ disabled,
71
+ error,
72
+ }: {
73
+ item: FieldsetItem;
74
+ disabled?: boolean;
75
+ error?: boolean;
76
+ }) {
77
+ const { tokens } = useTheme();
78
+ const msg = item.error ?? (error ? "Enter a valid value" : "");
79
+ return (
80
+ <View style={s.fieldWrap}>
81
+ {item.label ? <Text style={s.fieldLabel(tokens)}>{item.label}</Text> : null}
82
+ <Input value={item.value} placeholder={item.placeholder} disabled={disabled} error={!!msg} block />
83
+ {msg ? (
84
+ <Text style={s.fieldError(tokens)}>{msg}</Text>
85
+ ) : item.help ? (
86
+ <Text style={s.fieldHelp(tokens)}>{item.help}</Text>
87
+ ) : null}
88
+ </View>
89
+ );
90
+ }
91
+
92
+ // The two-column field group: rows flow into a wrapping row that collapses to a
93
+ // single column on small screens (flex-row flex-wrap sm:flex-col), and each item
94
+ // takes ~47% width and grows, going full width when stacked (w-[47%] grow sm:w-full).
95
+ function TwoColumnGroup({
96
+ rows,
97
+ disabled,
98
+ error,
99
+ }: {
100
+ rows: FieldsetItem[];
101
+ disabled?: boolean;
102
+ error?: boolean;
103
+ }) {
104
+ const direction = useResponsive<"row" | "column">({ base: "row", sm: "column" });
105
+ const itemWidth = useResponsive<DimensionValue>({ base: "47%", sm: "100%" });
106
+ return (
107
+ <View style={{ flexDirection: direction, flexWrap: "wrap", gap: 16 }}>
108
+ {rows.map((item, i) => (
109
+ <View key={i} style={[{ width: itemWidth }, s.itemGrow]}>
110
+ <Field item={item} disabled={disabled} error={error} />
111
+ </View>
112
+ ))}
113
+ </View>
114
+ );
115
+ }
116
+
117
+ export function Fieldset(props: FieldsetProps) {
118
+ const { children, legend, description, items, checkboxes, twoColumn, disabled, error, style } = props;
119
+ const { tokens } = useTheme();
120
+ const surface = surfaceOf(props);
121
+
122
+ const container: StyleProp<ViewStyle> = [
123
+ s.containerBase,
124
+ s.surface(tokens, surface),
125
+ disabled ? s.disabledDim : null,
126
+ style,
127
+ ];
128
+
129
+ const header =
130
+ legend != null || description != null ? (
131
+ <View style={s.header}>
132
+ {legend != null ? <Text style={s.legend(tokens)}>{legend}</Text> : null}
133
+ {description != null ? <Text style={s.description(tokens)}>{description}</Text> : null}
134
+ </View>
135
+ ) : null;
136
+
137
+ // Children win: when composed, render exactly what the caller passed.
138
+ if (children != null) {
139
+ return (
140
+ <View style={container}>
141
+ {header}
142
+ {children}
143
+ </View>
144
+ );
145
+ }
146
+
147
+ // Checkbox group: a stacked set of labeled checkboxes.
148
+ if (checkboxes != null) {
149
+ return (
150
+ <View style={container}>
151
+ {header}
152
+ <View style={s.checkboxGroup}>
153
+ {checkboxes.map((c, i) => (
154
+ <Checkbox key={i} checked={c.checked} disabled={disabled}>
155
+ {c.label}
156
+ </Checkbox>
157
+ ))}
158
+ </View>
159
+ </View>
160
+ );
161
+ }
162
+
163
+ // Field group: stacked rows, or a two-column wrap that collapses when narrow.
164
+ const rows = items ?? [];
165
+
166
+ return (
167
+ <View style={container}>
168
+ {header}
169
+ {twoColumn ? (
170
+ <TwoColumnGroup rows={rows} disabled={disabled} error={error} />
171
+ ) : (
172
+ <View style={s.groupColumn}>
173
+ {rows.map((item, i) => (
174
+ <View key={i} style={s.fieldWrap}>
175
+ <Field item={item} disabled={disabled} error={error} />
176
+ </View>
177
+ ))}
178
+ </View>
179
+ )}
180
+ </View>
181
+ );
182
+ }
@@ -0,0 +1,137 @@
1
+ # Form Layouts
2
+
3
+ Stacked, two-column, with sidebar description.
4
+
5
+ ## Usage
6
+
7
+ ```tsx
8
+ <Form
9
+ stacked
10
+ fields={[
11
+ { label: "Email", placeholder: "you@example.com" },
12
+ { label: "Password" }
13
+ ]}
14
+ submitLabel="Sign in"
15
+ style={{ maxWidth: 360 }}
16
+ />
17
+ ```
18
+
19
+ ## Variants
20
+
21
+ ### Layout - two-column
22
+
23
+ ```tsx
24
+ <Form
25
+ twoColumn
26
+ fields={[
27
+ { label: "First name", placeholder: "Ada" },
28
+ { label: "Last name", placeholder: "King" },
29
+ { label: "Email", placeholder: "ada@example.com" }
30
+ ]}
31
+ submitLabel="Create"
32
+ cancelLabel="Cancel"
33
+ style={{ maxWidth: 560 }}
34
+ />
35
+ ```
36
+
37
+ ### Layout - sidebar
38
+
39
+ ```tsx
40
+ <Form
41
+ sidebar
42
+ sections={[
43
+ { title: "Personal info", description: "This information will be displayed on your public profile.", fields: [
44
+ { label: "Full name", value: "Rachel Chen" },
45
+ { label: "Email", value: "rachel@example.com" }
46
+ ] },
47
+ { title: "Notifications", description: "Choose how you'd like to be notified.", checkboxes: [
48
+ { label: "Email notifications", checked: true },
49
+ { label: "SMS alerts" }
50
+ ] }
51
+ ]}
52
+ submitLabel="Save"
53
+ style={{ maxWidth: 720 }}
54
+ />
55
+ ```
56
+
57
+ ## Do & Don't
58
+
59
+ ### Stacked
60
+
61
+ **Do** — Keep short forms one field per row so each label sits directly above its input and the eye flows straight down.
62
+
63
+ ```tsx
64
+ <Form stacked submitLabel="Sign in" style={{ maxWidth: 360 }} fields={[
65
+ { label: "Email", placeholder: "you@example.com" },
66
+ { label: "Password" }
67
+ ]} />
68
+ ```
69
+
70
+ **Don't** — Pairing an email and password side by side cramps a sign-in form and breaks the natural top-to-bottom reading order.
71
+
72
+ ```tsx
73
+ <Form twoColumn submitLabel="Sign in" style={{ maxWidth: 360 }} fields={[
74
+ { label: "Email", placeholder: "you@example.com" },
75
+ { label: "Password" }
76
+ ]} />
77
+ ```
78
+
79
+ ### Two-column
80
+
81
+ **Do** — Pair fields of similar width (city / ZIP) in a row and give a full-width field like the street its own line.
82
+
83
+ ```tsx
84
+ <View style={{ maxWidth: 560 }}>
85
+ <View style={{ marginBottom: 12 }}>
86
+ <Text style={{ marginBottom: 6, fontSize: 14, lineHeight: 20, fontWeight: "500", color: tokens.foreground }}>Street address</Text>
87
+ <Input placeholder="123 Market St" />
88
+ </View>
89
+ <View style={{ flexDirection: "row", gap: 12 }}>
90
+ <View style={{ flexGrow: 1, flexShrink: 1, flexBasis: "0%" }}>
91
+ <Text style={{ marginBottom: 6, fontSize: 14, lineHeight: 20, fontWeight: "500", color: tokens.foreground }}>City</Text>
92
+ <Input placeholder="San Francisco" />
93
+ </View>
94
+ <View style={{ flexGrow: 1, flexShrink: 1, flexBasis: "0%" }}>
95
+ <Text style={{ marginBottom: 6, fontSize: 14, lineHeight: 20, fontWeight: "500", color: tokens.foreground }}>ZIP</Text>
96
+ <Input placeholder="94103" />
97
+ </View>
98
+ </View>
99
+ </View>
100
+ ```
101
+
102
+ **Don't** — Putting a wide field next to a tiny one in the same two-column row leaves the short input awkwardly oversized.
103
+
104
+ ```tsx
105
+ <Form twoColumn submitLabel="Save" style={{ maxWidth: 560 }} fields={[
106
+ { label: "Street address", placeholder: "123 Market St" },
107
+ { label: "ZIP", placeholder: "94103" }
108
+ ]} />
109
+ ```
110
+
111
+ ### Sidebar
112
+
113
+ **Do** — Pair each sidebar heading with a line of helper text so the left column explains what the section's fields are for.
114
+
115
+ ```tsx
116
+ <Form sidebar submitLabel="Save" style={{ maxWidth: 720 }} sections={[
117
+ { title: "Personal info", description: "Displayed on your public profile.", fields: [
118
+ { label: "Full name", value: "Rachel Chen" }
119
+ ] },
120
+ { title: "Billing", description: "Used for invoices and receipts.", fields: [
121
+ { label: "Card number", value: "•••• 4242" }
122
+ ] }
123
+ ]} />
124
+ ```
125
+
126
+ **Don't** — A bare section heading with no helper text wastes the sidebar column and gives the user no context for the group.
127
+
128
+ ```tsx
129
+ <Form sidebar submitLabel="Save" style={{ maxWidth: 720 }} sections={[
130
+ { title: "Personal info", fields: [
131
+ { label: "Full name", value: "Rachel Chen" }
132
+ ] },
133
+ { title: "Billing", fields: [
134
+ { label: "Card number", value: "•••• 4242" }
135
+ ] }
136
+ ]} />
137
+ ```
@@ -0,0 +1,39 @@
1
+ import { type ViewStyle, type TextStyle } from "react-native";
2
+ import { type ColorTokens } from "../../style/index.js";
3
+
4
+ // Co-located Form styles. The responsive collapse (side-by-side -> stacked at the
5
+ // `sm` breakpoint) is driven by useResponsive in the component; these are the
6
+ // static fragments and the token-colored text it composes.
7
+
8
+ export function label(tokens: ColorTokens): TextStyle {
9
+ return { fontSize: 14, lineHeight: 20, fontWeight: "500", color: tokens.foreground };
10
+ }
11
+
12
+ export function helper(tokens: ColorTokens): TextStyle {
13
+ return { marginTop: 6, fontSize: 12, lineHeight: 16, color: tokens["muted-foreground"] };
14
+ }
15
+
16
+ export function sectionDescription(tokens: ColorTokens): TextStyle {
17
+ return { marginTop: 4, fontSize: 12, lineHeight: 16, color: tokens["muted-foreground"] };
18
+ }
19
+
20
+ // The hairline under every section except the last.
21
+ export function sectionDivider(tokens: ColorTokens): ViewStyle {
22
+ return { borderBottomWidth: 1, borderColor: tokens.border, paddingBottom: 24 };
23
+ }
24
+
25
+ // Field-label inset above its input.
26
+ export const labelSpacing: TextStyle = { marginBottom: 6 };
27
+
28
+ // Section/sidebar heading: bumps the label weight from medium to semibold.
29
+ export const headingWeight: TextStyle = { fontWeight: "600" };
30
+
31
+ // flex-1 vs flex-auto (the two-column item basis, base vs sm).
32
+ export const flex1: ViewStyle = { flexGrow: 1, flexShrink: 1, flexBasis: "0%" };
33
+ export const flexAuto: ViewStyle = { flexGrow: 1, flexShrink: 1, flexBasis: "auto" };
34
+
35
+ export const actions: ViewStyle = { marginTop: 8, flexDirection: "row", justifyContent: "flex-end", gap: 8 };
36
+
37
+ // Outer stacks, by layout.
38
+ export const stackGap4: ViewStyle = { gap: 16 };
39
+ export const stackGap6: ViewStyle = { gap: 24 };
@@ -0,0 +1,246 @@
1
+ import { type ReactNode } from "react";
2
+ import { type DimensionValue } from "react-native";
3
+ import { View, Text, useTheme, useResponsive, type ViewStyle } from "../../style/index.js";
4
+ import { Button } from "../../atoms/button/button.js";
5
+ import { Checkbox } from "../../atoms/checkbox/checkbox.js";
6
+ import { Input } from "../../atoms/input/input.js";
7
+ import * as s from "./form.styles.js";
8
+
9
+ /** A single labeled field in a form. */
10
+ export interface FormField {
11
+ /** Visible label above (or beside) the input. */
12
+ label: string;
13
+ /** Placeholder shown while the field is empty. */
14
+ placeholder?: string;
15
+ /** Pre-filled value shown in the input (controlled). */
16
+ value?: string;
17
+ /** Optional helper text rendered below the input. */
18
+ helper?: string;
19
+ }
20
+
21
+ /** A checkbox row in a form section's checkbox group. */
22
+ export interface FormCheckbox {
23
+ /** Visible label beside the box. */
24
+ label: string;
25
+ /** Whether the box starts ticked. */
26
+ checked?: boolean;
27
+ }
28
+
29
+ /**
30
+ * A titled section of a sectioned (sidebar) form: a heading + description in the
31
+ * left column spanning a group of fields, or a checkbox group, on the right.
32
+ */
33
+ export interface FormSection {
34
+ /** Section heading (left column). */
35
+ title: string;
36
+ /** Muted supporting line under the heading. */
37
+ description?: string;
38
+ /** Input fields stacked in the right column. */
39
+ fields?: FormField[];
40
+ /** Checkbox group in the right column (mutually exclusive with fields). */
41
+ checkboxes?: FormCheckbox[];
42
+ }
43
+
44
+ export interface FormProps {
45
+ /** The labeled fields to render, in order. */
46
+ fields: FormField[];
47
+ /** Label for the primary submit button (defaults to "Submit"). */
48
+ submitLabel?: string;
49
+ /** When set, renders an outline cancel button before the submit button. */
50
+ cancelLabel?: string;
51
+ // Layout (pick one; first match wins). Default is the stacked layout.
52
+ /** Stacked: each label sits directly above its full-width input. */
53
+ stacked?: boolean;
54
+ /** Two-column: fields flow into a two-up grid that collapses on phones. */
55
+ twoColumn?: boolean;
56
+ /** Sidebar: label and helper sit in a left column, input on the right. */
57
+ sidebar?: boolean;
58
+ /**
59
+ * Sectioned sidebar layout: each section's heading + description sit in the
60
+ * left column and span a group of fields (or a checkbox group) on the right,
61
+ * separated by hairline dividers. Takes effect with the sidebar layout and
62
+ * replaces the per-field rows. The actions row renders after the last section.
63
+ */
64
+ sections?: FormSection[];
65
+ /** Escape hatch for layout/positioning composition (mainly width, e.g. maxWidth). */
66
+ style?: ViewStyle;
67
+ onSubmit?: () => void;
68
+ onCancel?: () => void;
69
+ }
70
+
71
+ type Layout = "stacked" | "twoColumn" | "sidebar";
72
+
73
+ // Layout precedence when more than one is passed: first match wins.
74
+ function layoutOf(p: FormProps): Layout {
75
+ if (p.stacked) return "stacked";
76
+ if (p.twoColumn) return "twoColumn";
77
+ if (p.sidebar) return "sidebar";
78
+ return "stacked";
79
+ }
80
+
81
+ function Helper({ text }: { text?: string }) {
82
+ const { tokens } = useTheme();
83
+ if (!text) return null;
84
+ return <Text style={s.helper(tokens)}>{text}</Text>;
85
+ }
86
+
87
+ // A label sitting above its input (stacked and two-column layouts).
88
+ function StackedField({ field }: { field: FormField }) {
89
+ const { tokens } = useTheme();
90
+ return (
91
+ <View>
92
+ <Text style={[s.label(tokens), s.labelSpacing]}>{field.label}</Text>
93
+ <Input placeholder={field.placeholder} value={field.value} />
94
+ <Helper text={field.helper} />
95
+ </View>
96
+ );
97
+ }
98
+
99
+ // One titled section of a sectioned sidebar form: heading + description on the
100
+ // left, a group of inputs or a checkbox group on the right. Desktop-first:
101
+ // side-by-side on wide viewports, collapsing to stacked on small screens. The
102
+ // hairline divider sits on every section except the last.
103
+ function Section({ section, last }: { section: FormSection; last: boolean }) {
104
+ const { tokens } = useTheme();
105
+ const row = useResponsive<ViewStyle>({
106
+ base: { flexDirection: "row", gap: 32 },
107
+ sm: { flexDirection: "column", gap: 12 },
108
+ });
109
+ const leftWidth = useResponsive<DimensionValue>({ base: 200, sm: "100%" });
110
+ const rightFull = useResponsive<ViewStyle>({ base: {}, sm: { width: "100%" } });
111
+ return (
112
+ <View style={[{ alignItems: "flex-start" }, row, last ? null : s.sectionDivider(tokens)]}>
113
+ <View style={{ width: leftWidth }}>
114
+ <Text style={[s.label(tokens), s.headingWeight]}>{section.title}</Text>
115
+ {section.description ? <Text style={s.sectionDescription(tokens)}>{section.description}</Text> : null}
116
+ </View>
117
+ <View style={[s.flex1, { gap: 12 }, rightFull]}>
118
+ {section.checkboxes
119
+ ? section.checkboxes.map((c, i) => (
120
+ <Checkbox key={i} checked={c.checked}>
121
+ {c.label}
122
+ </Checkbox>
123
+ ))
124
+ : (section.fields ?? []).map((field, i) => (
125
+ <View key={i}>
126
+ <Text style={[s.label(tokens), s.labelSpacing]}>{field.label}</Text>
127
+ <Input placeholder={field.placeholder} value={field.value} />
128
+ <Helper text={field.helper} />
129
+ </View>
130
+ ))}
131
+ </View>
132
+ </View>
133
+ );
134
+ }
135
+
136
+ // A label/helper column on the left with the input on the right. Desktop-first:
137
+ // side-by-side on wide viewports, collapsing to stacked on small screens.
138
+ function SidebarField({ field }: { field: FormField }) {
139
+ const { tokens } = useTheme();
140
+ const row = useResponsive<ViewStyle>({
141
+ base: { flexDirection: "row", gap: 32 },
142
+ sm: { flexDirection: "column", gap: 6 },
143
+ });
144
+ const leftWidth = useResponsive<DimensionValue>({ base: "33.3333%", sm: "100%" });
145
+ const rightFull = useResponsive<ViewStyle>({ base: {}, sm: { width: "100%" } });
146
+ return (
147
+ <View style={[{ alignItems: "flex-start" }, row]}>
148
+ <View style={{ width: leftWidth }}>
149
+ <Text style={[s.label(tokens), s.headingWeight]}>{field.label}</Text>
150
+ <Helper text={field.helper} />
151
+ </View>
152
+ <View style={[s.flex1, rightFull]}>
153
+ <Input placeholder={field.placeholder} value={field.value} />
154
+ </View>
155
+ </View>
156
+ );
157
+ }
158
+
159
+ // The two-column body: fields flow into a wrapping row that collapses to a
160
+ // single column on small screens; each item is flex-1 (flex-auto when stacked).
161
+ function TwoColumnBody({ fields }: { fields: FormField[] }) {
162
+ const direction = useResponsive<"row" | "column">({ base: "row", sm: "column" });
163
+ const itemBasis = useResponsive<ViewStyle>({ base: s.flex1, sm: s.flexAuto });
164
+ return (
165
+ <View style={{ flexDirection: direction, flexWrap: "wrap", gap: 16 }}>
166
+ {fields.map((field, i) => (
167
+ <View key={i} style={[itemBasis, { minWidth: 200 }]}>
168
+ <StackedField field={field} />
169
+ </View>
170
+ ))}
171
+ </View>
172
+ );
173
+ }
174
+
175
+ function Actions({
176
+ submitLabel,
177
+ cancelLabel,
178
+ onSubmit,
179
+ onCancel,
180
+ }: {
181
+ submitLabel: string;
182
+ cancelLabel?: string;
183
+ onSubmit?: () => void;
184
+ onCancel?: () => void;
185
+ }): ReactNode {
186
+ return (
187
+ <View style={s.actions}>
188
+ {cancelLabel ? (
189
+ <Button outline onPress={onCancel}>
190
+ {cancelLabel}
191
+ </Button>
192
+ ) : null}
193
+ <Button primary onPress={onSubmit}>
194
+ {submitLabel}
195
+ </Button>
196
+ </View>
197
+ );
198
+ }
199
+
200
+ export function Form(props: FormProps) {
201
+ const { fields, submitLabel = "Submit", cancelLabel, style, onSubmit, onCancel } = props;
202
+ const layout = layoutOf(props);
203
+
204
+ if (layout === "twoColumn") {
205
+ return (
206
+ <View style={[s.stackGap4, style]}>
207
+ <TwoColumnBody fields={fields} />
208
+ <Actions submitLabel={submitLabel} cancelLabel={cancelLabel} onSubmit={onSubmit} onCancel={onCancel} />
209
+ </View>
210
+ );
211
+ }
212
+
213
+ if (layout === "sidebar") {
214
+ // Sectioned sidebar: section headings span a group of fields / a checkbox
215
+ // group. Falls back to the per-field sidebar when no sections are given.
216
+ const sections = props.sections;
217
+ if (sections && sections.length > 0) {
218
+ return (
219
+ <View style={[s.stackGap6, style]}>
220
+ {sections.map((section, i) => (
221
+ <Section key={i} section={section} last={i === sections.length - 1} />
222
+ ))}
223
+ <Actions submitLabel={submitLabel} cancelLabel={cancelLabel} onSubmit={onSubmit} onCancel={onCancel} />
224
+ </View>
225
+ );
226
+ }
227
+ return (
228
+ <View style={[s.stackGap6, style]}>
229
+ {fields.map((field, i) => (
230
+ <SidebarField key={i} field={field} />
231
+ ))}
232
+ <Actions submitLabel={submitLabel} cancelLabel={cancelLabel} onSubmit={onSubmit} onCancel={onCancel} />
233
+ </View>
234
+ );
235
+ }
236
+
237
+ // stacked (default): one field per row, full width, label above input.
238
+ return (
239
+ <View style={[s.stackGap4, style]}>
240
+ {fields.map((field, i) => (
241
+ <StackedField key={i} field={field} />
242
+ ))}
243
+ <Actions submitLabel={submitLabel} cancelLabel={cancelLabel} onSubmit={onSubmit} onCancel={onCancel} />
244
+ </View>
245
+ );
246
+ }