@olympusoss/canvas 4.0.0 → 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 (297) hide show
  1. package/README.md +108 -0
  2. package/package.json +14 -3
  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/src/theme.ts +21 -0
  234. package/styles/canvas.css +128 -67
  235. package/tsconfig.json +4 -2
  236. package/src/cn.ts +0 -3
  237. package/styles/base.css +0 -17
  238. package/styles/components/alert.css +0 -66
  239. package/styles/components/app-shell.css +0 -46
  240. package/styles/components/avatar.css +0 -15
  241. package/styles/components/badge.css +0 -83
  242. package/styles/components/breadcrumb.css +0 -35
  243. package/styles/components/button-group.css +0 -23
  244. package/styles/components/button.css +0 -107
  245. package/styles/components/calendar.css +0 -73
  246. package/styles/components/card.css +0 -58
  247. package/styles/components/checkbox.css +0 -55
  248. package/styles/components/code-block.css +0 -18
  249. package/styles/components/combobox.css +0 -75
  250. package/styles/components/command.css +0 -94
  251. package/styles/components/data-table.css +0 -142
  252. package/styles/components/dialog.css +0 -72
  253. package/styles/components/dropdown.css +0 -54
  254. package/styles/components/empty-state.css +0 -17
  255. package/styles/components/field.css +0 -27
  256. package/styles/components/filter-panel.css +0 -58
  257. package/styles/components/form.css +0 -27
  258. package/styles/components/icon.css +0 -8
  259. package/styles/components/input-group.css +0 -45
  260. package/styles/components/input.css +0 -56
  261. package/styles/components/kbd.css +0 -15
  262. package/styles/components/page-header.css +0 -52
  263. package/styles/components/pagination.css +0 -48
  264. package/styles/components/popover.css +0 -14
  265. package/styles/components/radio.css +0 -28
  266. package/styles/components/row-menu.css +0 -69
  267. package/styles/components/section-card.css +0 -49
  268. package/styles/components/select.css +0 -57
  269. package/styles/components/separator.css +0 -32
  270. package/styles/components/sheet.css +0 -70
  271. package/styles/components/sidebar.css +0 -146
  272. package/styles/components/skeleton.css +0 -32
  273. package/styles/components/spinner.css +0 -26
  274. package/styles/components/stat-card.css +0 -71
  275. package/styles/components/stepper.css +0 -63
  276. package/styles/components/switch.css +0 -45
  277. package/styles/components/tabs.css +0 -40
  278. package/styles/components/textarea.css +0 -31
  279. package/styles/components/toast.css +0 -95
  280. package/styles/components/tooltip.css +0 -53
  281. package/styles/components/topbar.css +0 -24
  282. package/styles/components/typography.css +0 -105
  283. package/styles/patterns/backdrops.css +0 -35
  284. package/styles/patterns/density.css +0 -66
  285. package/styles/patterns/focus.css +0 -38
  286. package/styles/patterns/glass.css +0 -85
  287. package/styles/patterns/high-contrast.css +0 -70
  288. package/styles/patterns/reduced-motion.css +0 -12
  289. package/styles/patterns/scrollbar.css +0 -10
  290. package/styles/reset.css +0 -89
  291. package/styles/tokens/colors.css +0 -106
  292. package/styles/tokens/motion.css +0 -33
  293. package/styles/tokens/radius.css +0 -10
  294. package/styles/tokens/shadows.css +0 -35
  295. package/styles/tokens/spacing.css +0 -19
  296. package/styles/tokens/typography.css +0 -6
  297. package/styles/tokens/z-index.css +0 -12
@@ -0,0 +1,483 @@
1
+ import { type ViewStyle, type TextStyle } from "react-native";
2
+ import { type ColorTokens, alpha, shadow } from "../../style/index.js";
3
+ import { type ButtonGroupSkin, type Size } from "./button-group.shared.js";
4
+
5
+ // Co-located ButtonGroup skins, one per platform. The group is laid out per
6
+ // segment in JS (there are no `first:`/`last:` style variants, so the joined-
7
+ // corner and shared-border math is computed here). The BRAND survives on every
8
+ // platform (the indigo `primary` token and the semantic tokens, never a platform
9
+ // default), and only the native SHAPE, sizing, structure, and press feedback
10
+ // change per OS:
11
+ // iOS (UISegmentedControl): a gray rounded CONTAINER (radius 8, muted fill,
12
+ // ~3px inset) holding segments; the SELECTED segment is a raised white pill
13
+ // (radius 6, small shadow) with NO visible dividers (iOS 13+ style); labels
14
+ // ~13pt. Press = opacity dim.
15
+ // Android (M3 SegmentedButton): the GROUP is a fully-rounded stadium (1dp
16
+ // `border` outline); segments share 1dp borders (no gap); the SELECTED
17
+ // segment is a tonal fill (alpha(primary, .12)) with a brand-indigo label and
18
+ // a leading check; press = android_ripple.
19
+ // Web: the established Canvas look (joined buttons; selected = solid primary
20
+ // fill; shared 1px borders overlapped by -1px), lifted verbatim.
21
+
22
+ // --- shared size scales (brand type/sizing, identical across platforms) ------
23
+
24
+ // Height + horizontal padding per size, mirroring the docs segSize scale
25
+ // (h-8 px-3 / h-9 px-4 / h-10 px-5).
26
+ export const sizeContainer: Record<Size, ViewStyle> = {
27
+ small: { height: 32, paddingHorizontal: 12 },
28
+ default: { height: 36, paddingHorizontal: 16 },
29
+ large: { height: 40, paddingHorizontal: 20 },
30
+ };
31
+
32
+ // Label type per size (text-xs for small, text-sm otherwise).
33
+ export const sizeLabel: Record<Size, TextStyle> = {
34
+ small: { fontSize: 12, lineHeight: 16 },
35
+ default: { fontSize: 14, lineHeight: 20 },
36
+ large: { fontSize: 14, lineHeight: 20 },
37
+ };
38
+
39
+ // Height-only per size, for cells whose padding differs from sizeContainer
40
+ // (the split chevron and the stepper's chevron cells: h-8 / h-9 / h-10).
41
+ export const sizeHeight: Record<Size, number> = {
42
+ small: 32,
43
+ default: 36,
44
+ large: 40,
45
+ };
46
+
47
+ // Chevron glyph px per size (small gets the tighter 14px arrow).
48
+ export const chevronSize: Record<Size, number> = {
49
+ small: 14,
50
+ default: 16,
51
+ large: 16,
52
+ };
53
+
54
+ // --- shared layout fragments (color-free; identical across platforms) --------
55
+
56
+ // Row of a centered label inside a cell. Border width is supplied by the skin
57
+ // (iOS segments are borderless; Android/web are 1px). Press feedback is applied
58
+ // by the component's Pressable.
59
+ export const segmentBase: ViewStyle = {
60
+ flexDirection: "row",
61
+ alignItems: "center",
62
+ justifyContent: "center",
63
+ };
64
+
65
+ // The dimmed look applied to a disabled group/segment (opacity-50).
66
+ export const dim: ViewStyle = { opacity: 0.5 };
67
+
68
+ // The split control's outer row, anchoring the (absolute) dropdown.
69
+ export const splitContainer: ViewStyle = {
70
+ position: "relative",
71
+ flexDirection: "row",
72
+ alignItems: "center",
73
+ alignSelf: "flex-start",
74
+ };
75
+
76
+ // When the split dropdown is open, the container is lifted into its own stacking
77
+ // context above sibling content. react-native-web gives every positioned View an
78
+ // implicit stacking context, so the menu's own `zIndex` is scoped INSIDE the
79
+ // `relative` container and cannot rise above a later sibling. Raising the
80
+ // container's zIndex while open lifts the whole control — buttons and menu
81
+ // together — above everything painted after it.
82
+ export const splitContainerLifted: ViewStyle = { zIndex: 50 };
83
+
84
+ // A split-menu dropdown row: padded, rounded; the pressed branch tints it.
85
+ export const splitMenuItem: ViewStyle = {
86
+ flexDirection: "row",
87
+ alignItems: "center",
88
+ borderRadius: 2,
89
+ paddingHorizontal: 8,
90
+ paddingVertical: 6,
91
+ };
92
+
93
+ // The stepper's outer row.
94
+ export const stepperContainer: ViewStyle = {
95
+ flexDirection: "row",
96
+ alignItems: "center",
97
+ alignSelf: "flex-start",
98
+ };
99
+
100
+ // A plain row of detached peers separated by a gap (gap-2).
101
+ export const spacedContainer: ViewStyle = {
102
+ flexDirection: "row",
103
+ alignItems: "center",
104
+ gap: 8,
105
+ };
106
+
107
+ // The attached-segment row (no gap; segments share borders).
108
+ export const segmentedContainer: ViewStyle = {
109
+ flexDirection: "row",
110
+ alignItems: "center",
111
+ };
112
+
113
+ // =============================================================================
114
+ // Web: the established Canvas look (lifted verbatim from the original file).
115
+ // =============================================================================
116
+
117
+ export const webSkin: ButtonGroupSkin = {
118
+ // No wrapper around segmented (the row is bare); selected lifts above its
119
+ // neighbors so its primary border wins on the shared edges.
120
+ segmentedWrap: () => null,
121
+ segmentBorderWidth: 1,
122
+ joinCorners(index, count) {
123
+ if (count === 1) return { borderRadius: 6 };
124
+ if (index === 0) return { borderTopLeftRadius: 6, borderBottomLeftRadius: 6 };
125
+ if (index === count - 1) return { borderTopRightRadius: 6, borderBottomRightRadius: 6 };
126
+ return {};
127
+ },
128
+ spacedCorners: { borderRadius: 6 },
129
+ // All but the leading segment overlap the previous border by 1px (-ml-px).
130
+ overlap: { marginLeft: -1 },
131
+ segmentSurface(t, selected) {
132
+ return selected
133
+ ? { zIndex: 10, borderColor: t.primary, backgroundColor: t.primary }
134
+ : { borderColor: t.input, backgroundColor: t.background };
135
+ },
136
+ segmentLabel(t, selected) {
137
+ return { fontWeight: "500", color: selected ? t["primary-foreground"] : t.foreground };
138
+ },
139
+ showSelectedCheck: false,
140
+
141
+ // --- split ---
142
+ splitPrimary(t) {
143
+ return {
144
+ flexDirection: "row",
145
+ alignItems: "center",
146
+ justifyContent: "center",
147
+ borderTopLeftRadius: 6,
148
+ borderBottomLeftRadius: 6,
149
+ backgroundColor: t.primary,
150
+ };
151
+ },
152
+ splitPrimaryLabel(t) {
153
+ return { fontWeight: "500", color: t["primary-foreground"] };
154
+ },
155
+ splitDivider(t, height) {
156
+ return { width: 1, height, backgroundColor: alpha(t["primary-foreground"], 0.2) };
157
+ },
158
+ splitTrigger(t, height) {
159
+ return {
160
+ flexDirection: "row",
161
+ alignItems: "center",
162
+ justifyContent: "center",
163
+ borderTopRightRadius: 6,
164
+ borderBottomRightRadius: 6,
165
+ backgroundColor: t.primary,
166
+ paddingHorizontal: 8,
167
+ height,
168
+ };
169
+ },
170
+ splitChevronColor: "primaryForeground",
171
+ splitMenu(t) {
172
+ return {
173
+ position: "absolute",
174
+ top: "100%",
175
+ right: 0,
176
+ zIndex: 50,
177
+ marginTop: 4,
178
+ minWidth: 180,
179
+ borderRadius: 6,
180
+ borderWidth: 1,
181
+ borderColor: t.border,
182
+ backgroundColor: t.popover,
183
+ padding: 4,
184
+ ...shadow("lg"),
185
+ };
186
+ },
187
+ splitMenuItemPressed(t) {
188
+ return { backgroundColor: t.accent };
189
+ },
190
+ splitMenuText(t) {
191
+ return { fontSize: 14, lineHeight: 20, color: t["popover-foreground"] };
192
+ },
193
+
194
+ // --- stepper ---
195
+ stepperArrow(t, height) {
196
+ return {
197
+ flexDirection: "row",
198
+ alignItems: "center",
199
+ justifyContent: "center",
200
+ borderWidth: 1,
201
+ borderColor: t.input,
202
+ backgroundColor: t.background,
203
+ paddingHorizontal: 8,
204
+ height,
205
+ };
206
+ },
207
+ stepperArrowLeft: { borderTopLeftRadius: 6, borderBottomLeftRadius: 6 },
208
+ stepperArrowRight: { marginLeft: -1, borderTopRightRadius: 6, borderBottomRightRadius: 6 },
209
+ stepperMiddle(t) {
210
+ return {
211
+ flexDirection: "row",
212
+ alignItems: "center",
213
+ justifyContent: "center",
214
+ borderWidth: 1,
215
+ marginLeft: -1,
216
+ borderColor: t.input,
217
+ backgroundColor: t.background,
218
+ };
219
+ },
220
+ stepperLabel(t) {
221
+ return { fontWeight: "500", color: t.foreground };
222
+ },
223
+ stepperChevronColor: "muted",
224
+
225
+ pressedOpacity: 0.9,
226
+ };
227
+
228
+ // =============================================================================
229
+ // iOS (UISegmentedControl): gray rounded container, raised white selected pill.
230
+ // =============================================================================
231
+
232
+ const IOS_PILL_SHADOW: ViewStyle = {
233
+ shadowColor: "#000000",
234
+ shadowOffset: { width: 0, height: 1 },
235
+ shadowOpacity: 0.18,
236
+ shadowRadius: 2,
237
+ elevation: 2,
238
+ };
239
+
240
+ export const iosSkin: ButtonGroupSkin = {
241
+ // The gray CAPSULE track that holds the segments (radius 9999, muted fill, a
242
+ // 3px inset so the selected pill floats inside). iOS 26+ (Liquid Glass) draws
243
+ // the segmented control as a capsule, not a rounded rectangle.
244
+ segmentedWrap(t) {
245
+ return {
246
+ flexDirection: "row",
247
+ alignItems: "center",
248
+ alignSelf: "flex-start",
249
+ padding: 3,
250
+ borderRadius: 9999,
251
+ backgroundColor: t.muted,
252
+ };
253
+ },
254
+ segmentBorderWidth: 0, // iOS 13+ has no visible dividers/borders
255
+ joinCorners() {
256
+ // Every segment is an independent CAPSULE pill (radius 9999) inside the
257
+ // track; the selected one floats as a raised white capsule.
258
+ return { borderRadius: 9999 };
259
+ },
260
+ spacedCorners: { borderRadius: 8 },
261
+ overlap: null, // no shared borders; the track's padding spaces them
262
+ segmentSurface(t, selected) {
263
+ return selected
264
+ ? { ...IOS_PILL_SHADOW, backgroundColor: t.background }
265
+ : { backgroundColor: "transparent" };
266
+ },
267
+ segmentLabel(t, selected) {
268
+ // ~13pt SF label; selected reads slightly heavier. Both stay on-foreground
269
+ // (the selected pill is white/elevated, not a brand fill).
270
+ return { fontWeight: selected ? "600" : "500", color: t.foreground };
271
+ },
272
+ showSelectedCheck: false,
273
+
274
+ // --- split (HIG: primary action + chevron, brand fill, rounded ~8) ---
275
+ splitPrimary(t) {
276
+ return {
277
+ flexDirection: "row",
278
+ alignItems: "center",
279
+ justifyContent: "center",
280
+ borderTopLeftRadius: 8,
281
+ borderBottomLeftRadius: 8,
282
+ backgroundColor: t.primary,
283
+ };
284
+ },
285
+ splitPrimaryLabel(t) {
286
+ return { fontWeight: "600", color: t["primary-foreground"] };
287
+ },
288
+ splitDivider(t, height) {
289
+ return { width: 1, height, backgroundColor: alpha(t["primary-foreground"], 0.2) };
290
+ },
291
+ splitTrigger(t, height) {
292
+ return {
293
+ flexDirection: "row",
294
+ alignItems: "center",
295
+ justifyContent: "center",
296
+ borderTopRightRadius: 8,
297
+ borderBottomRightRadius: 8,
298
+ backgroundColor: t.primary,
299
+ paddingHorizontal: 10,
300
+ height,
301
+ };
302
+ },
303
+ splitChevronColor: "primaryForeground",
304
+ splitMenu(t) {
305
+ return {
306
+ position: "absolute",
307
+ top: "100%",
308
+ right: 0,
309
+ zIndex: 50,
310
+ marginTop: 6,
311
+ minWidth: 200,
312
+ borderRadius: 12, // HIG menu sheet
313
+ borderWidth: 0,
314
+ borderColor: t.border,
315
+ backgroundColor: t.popover,
316
+ padding: 6,
317
+ ...shadow("lg"),
318
+ };
319
+ },
320
+ splitMenuItemPressed(t) {
321
+ return { backgroundColor: t.accent };
322
+ },
323
+ splitMenuText(t) {
324
+ return { fontSize: 15, lineHeight: 20, color: t["popover-foreground"] };
325
+ },
326
+
327
+ // --- stepper (HIG: gray-tracked prev/current/next, rounded 8) ---
328
+ stepperArrow(t, height) {
329
+ return {
330
+ flexDirection: "row",
331
+ alignItems: "center",
332
+ justifyContent: "center",
333
+ borderWidth: 0,
334
+ backgroundColor: t.muted,
335
+ paddingHorizontal: 10,
336
+ height,
337
+ };
338
+ },
339
+ stepperArrowLeft: { borderTopLeftRadius: 8, borderBottomLeftRadius: 8 },
340
+ stepperArrowRight: { marginLeft: 1, borderTopRightRadius: 8, borderBottomRightRadius: 8 },
341
+ stepperMiddle(t) {
342
+ return {
343
+ flexDirection: "row",
344
+ alignItems: "center",
345
+ justifyContent: "center",
346
+ borderWidth: 0,
347
+ marginLeft: 1,
348
+ backgroundColor: t.muted,
349
+ };
350
+ },
351
+ stepperLabel(t) {
352
+ return { fontWeight: "600", color: t.foreground };
353
+ },
354
+ stepperChevronColor: "foreground",
355
+
356
+ pressedOpacity: 0.8,
357
+ };
358
+
359
+ // =============================================================================
360
+ // Android (Material 3 SegmentedButton): stadium group, tonal selected fill.
361
+ // =============================================================================
362
+
363
+ export const androidSkin: ButtonGroupSkin = {
364
+ // The group is a fully-rounded stadium outlined in 1dp `border`; the segments
365
+ // sit inside and supply their own shared 1dp dividers.
366
+ segmentedWrap(t) {
367
+ return {
368
+ flexDirection: "row",
369
+ alignItems: "center",
370
+ alignSelf: "flex-start",
371
+ borderRadius: 9999,
372
+ borderWidth: 1,
373
+ borderColor: t.border,
374
+ overflow: "hidden",
375
+ };
376
+ },
377
+ segmentBorderWidth: 0, // the wrap draws the outer outline; dividers are drawn below
378
+ joinCorners() {
379
+ // The stadium wrap clips the corners; segments themselves are square.
380
+ return {};
381
+ },
382
+ spacedCorners: { borderRadius: 9999 },
383
+ // Each segment after the first draws a 1dp leading divider in the outline color.
384
+ overlap: null,
385
+ segmentDivider: (t) => ({ borderLeftWidth: 1, borderLeftColor: t.border }),
386
+ segmentSurface(t, selected) {
387
+ // Selected = tonal fill (secondaryContainer ≈ alpha(primary, .12)).
388
+ return selected ? { backgroundColor: alpha(t.primary, 0.12) } : { backgroundColor: "transparent" };
389
+ },
390
+ segmentLabel(t, selected) {
391
+ // labelMedium; selected reads in brand indigo (onSecondaryContainer ≈ primary).
392
+ return { fontWeight: "500", color: selected ? t.primary : t.foreground };
393
+ },
394
+ showSelectedCheck: true,
395
+
396
+ // --- split (M3: brand-filled primary + chevron, stadium) ---
397
+ splitPrimary(t) {
398
+ return {
399
+ flexDirection: "row",
400
+ alignItems: "center",
401
+ justifyContent: "center",
402
+ borderTopLeftRadius: 9999,
403
+ borderBottomLeftRadius: 9999,
404
+ backgroundColor: t.primary,
405
+ };
406
+ },
407
+ splitPrimaryLabel(t) {
408
+ return { fontWeight: "500", color: t["primary-foreground"] };
409
+ },
410
+ splitDivider(t, height) {
411
+ return { width: 1, height, backgroundColor: alpha(t["primary-foreground"], 0.24) };
412
+ },
413
+ splitTrigger(t, height) {
414
+ return {
415
+ flexDirection: "row",
416
+ alignItems: "center",
417
+ justifyContent: "center",
418
+ borderTopRightRadius: 9999,
419
+ borderBottomRightRadius: 9999,
420
+ backgroundColor: t.primary,
421
+ paddingHorizontal: 10,
422
+ height,
423
+ };
424
+ },
425
+ splitChevronColor: "primaryForeground",
426
+ splitMenu(t) {
427
+ return {
428
+ position: "absolute",
429
+ top: "100%",
430
+ right: 0,
431
+ zIndex: 50,
432
+ marginTop: 4,
433
+ minWidth: 200,
434
+ borderRadius: 4, // M3 menu container
435
+ borderWidth: 0,
436
+ borderColor: t.border,
437
+ backgroundColor: t.popover,
438
+ paddingVertical: 8,
439
+ ...shadow("lg"),
440
+ };
441
+ },
442
+ splitMenuItemPressed(t) {
443
+ return { backgroundColor: alpha(t.primary, 0.12) };
444
+ },
445
+ splitMenuText(t) {
446
+ return { fontSize: 14, lineHeight: 20, color: t["popover-foreground"] };
447
+ },
448
+
449
+ // --- stepper (M3: outlined stadium prev/current/next) ---
450
+ stepperArrow(t, height) {
451
+ return {
452
+ flexDirection: "row",
453
+ alignItems: "center",
454
+ justifyContent: "center",
455
+ borderWidth: 1,
456
+ borderColor: t.border,
457
+ backgroundColor: "transparent",
458
+ paddingHorizontal: 10,
459
+ height,
460
+ };
461
+ },
462
+ stepperArrowLeft: { borderTopLeftRadius: 9999, borderBottomLeftRadius: 9999 },
463
+ stepperArrowRight: { marginLeft: -1, borderTopRightRadius: 9999, borderBottomRightRadius: 9999 },
464
+ stepperMiddle(t) {
465
+ return {
466
+ flexDirection: "row",
467
+ alignItems: "center",
468
+ justifyContent: "center",
469
+ borderTopWidth: 1,
470
+ borderBottomWidth: 1,
471
+ marginLeft: -1,
472
+ borderColor: t.border,
473
+ backgroundColor: "transparent",
474
+ };
475
+ },
476
+ stepperLabel(t) {
477
+ return { fontWeight: "500", color: t.foreground };
478
+ },
479
+ stepperChevronColor: "muted",
480
+
481
+ pressedOpacity: null, // Android uses a ripple instead
482
+ ripple: (t) => ({ color: alpha(t.primary, 0.12), borderless: false }),
483
+ };
@@ -0,0 +1,6 @@
1
+ import { createButtonGroup } from "./button-group.shared.js";
2
+ import { webSkin } from "./button-group.styles.js";
3
+
4
+ // Web ButtonGroup (the base; Metro falls back to it on native, web bundlers resolve it).
5
+ export const ButtonGroup = createButtonGroup(webSkin);
6
+ export type { ButtonGroupProps } from "./button-group.shared.js";
@@ -0,0 +1,6 @@
1
+ import { createCheckbox } from "./checkbox.shared.js";
2
+ import { androidSkin } from "./checkbox.styles.js";
3
+
4
+ // Material 3 Checkbox. Metro resolves this file on Android; the docs import it for preview.
5
+ export const Checkbox = createCheckbox(androidSkin);
6
+ export type { CheckboxProps } from "./checkbox.shared.js";
@@ -0,0 +1,6 @@
1
+ import { createCheckbox } from "./checkbox.shared.js";
2
+ import { iosSkin } from "./checkbox.styles.js";
3
+
4
+ // iOS (HIG) Checkbox. Metro resolves this file on iOS; the docs import it for preview.
5
+ export const Checkbox = createCheckbox(iosSkin);
6
+ export type { CheckboxProps } from "./checkbox.shared.js";
@@ -0,0 +1,150 @@
1
+ # Checkboxes
2
+
3
+ Multi-select option, single yes/no, grouped lists.
4
+
5
+ ## Usage
6
+
7
+ ```tsx
8
+ <View style={{ flexDirection: "row", alignItems: "flex-start", gap: 8 }}>
9
+ <Checkbox checked />
10
+ <View style={{ gap: 2 }}>
11
+ <Text style={{ fontSize: 14, lineHeight: 20, fontWeight: "500", color: tokens.foreground }}>Email notifications</Text>
12
+ <Text style={{ fontSize: 12, lineHeight: 16, color: tokens["muted-foreground"] }}>Get notified when activity happens on your account.</Text>
13
+ </View>
14
+ </View>
15
+ ```
16
+
17
+ ## Variants
18
+
19
+ ### State - unchecked
20
+
21
+ ```tsx
22
+ <View style={{ flexDirection: "row", alignItems: "flex-start", gap: 8 }}>
23
+ <Checkbox />
24
+ <View style={{ gap: 2 }}>
25
+ <Text style={{ fontSize: 14, lineHeight: 20, fontWeight: "500", color: tokens.foreground }}>Email notifications</Text>
26
+ <Text style={{ fontSize: 12, lineHeight: 16, color: tokens["muted-foreground"] }}>Get notified when activity happens on your account.</Text>
27
+ </View>
28
+ </View>
29
+ ```
30
+
31
+ ### State - disabled
32
+
33
+ ```tsx
34
+ <View style={{ flexDirection: "row", alignItems: "flex-start", gap: 8 }}>
35
+ <Checkbox disabled />
36
+ <View style={{ gap: 2 }}>
37
+ <Text style={{ fontSize: 14, lineHeight: 20, fontWeight: "500", color: tokens.foreground }}>Email notifications</Text>
38
+ <Text style={{ fontSize: 12, lineHeight: 16, color: tokens["muted-foreground"] }}>Get notified when activity happens on your account.</Text>
39
+ </View>
40
+ </View>
41
+ ```
42
+
43
+ ## Do & Don't
44
+
45
+ ### Unchecked
46
+
47
+ **Do** — Leave opt-in consent unchecked so agreeing is a deliberate act the user takes.
48
+
49
+ ```tsx
50
+ <Checkbox>Email me product news, offers, and survey invitations.</Checkbox>
51
+ ```
52
+
53
+ **Don't** — A consent box that starts checked opts users in by default; under GDPR pre-ticked consent is not consent.
54
+
55
+ ```tsx
56
+ <Checkbox checked>Email me product news, offers, and survey invitations.</Checkbox>
57
+ ```
58
+
59
+ ### Checked
60
+
61
+ **Do** — Show the parent indeterminate (a dash, not a tick) when only some children are checked.
62
+
63
+ ```tsx
64
+ <View style={{ gap: 8 }}>
65
+ <Checkbox indeterminate>Select all</Checkbox>
66
+ <View style={{ marginLeft: 24, gap: 8 }}>
67
+ <Checkbox checked>Read</Checkbox>
68
+ <Checkbox>Write</Checkbox>
69
+ <Checkbox>Delete</Checkbox>
70
+ </View>
71
+ </View>
72
+ ```
73
+
74
+ **Don't** — A fully checked parent claims every child is selected when only one is, so the state reads as a lie.
75
+
76
+ ```tsx
77
+ <View style={{ gap: 8 }}>
78
+ <Checkbox checked>Select all</Checkbox>
79
+ <View style={{ marginLeft: 24, gap: 8 }}>
80
+ <Checkbox checked>Read</Checkbox>
81
+ <Checkbox>Write</Checkbox>
82
+ <Checkbox>Delete</Checkbox>
83
+ </View>
84
+ </View>
85
+ ```
86
+
87
+ ### Disabled
88
+
89
+ **Do** — Say why it's unavailable, like a plan gate, or don't show it at all.
90
+
91
+ ```tsx
92
+ <View style={{ flexDirection: "row", alignItems: "center", gap: 6 }}>
93
+ <Checkbox disabled>Export to CSV</Checkbox>
94
+ <Text style={{ fontSize: 12, lineHeight: 16, color: tokens["muted-foreground"] }}>(Pro plan)</Text>
95
+ </View>
96
+ ```
97
+
98
+ **Don't** — A disabled option with no reason leaves users stuck and guessing.
99
+
100
+ ```tsx
101
+ <Checkbox disabled>Export to CSV</Checkbox>
102
+ ```
103
+
104
+ ### Selection
105
+
106
+ **Do** — Radios for one-of-many; reserve checkboxes for independent multi-select.
107
+
108
+ ```tsx
109
+ <View style={{ gap: 8 }}>
110
+ <Text style={{ marginBottom: 4, fontSize: 14, lineHeight: 20, fontWeight: "600", color: tokens.foreground }}>Plan</Text>
111
+ <Radio>Free</Radio>
112
+ <Radio checked>Pro</Radio>
113
+ <Radio>Enterprise</Radio>
114
+ </View>
115
+ ```
116
+
117
+ **Don't** — Checkboxes allow multiple selections; for a one-of choice they let users pick contradictory options.
118
+
119
+ ```tsx
120
+ <View style={{ gap: 8 }}>
121
+ <Text style={{ marginBottom: 4, fontSize: 14, lineHeight: 20, fontWeight: "600", color: tokens.foreground }}>Plan</Text>
122
+ <Checkbox>Free</Checkbox>
123
+ <Checkbox checked>Pro</Checkbox>
124
+ <Checkbox>Enterprise</Checkbox>
125
+ </View>
126
+ ```
127
+
128
+ ### With description
129
+
130
+ **Do** — Wrap the box, label, and description in a <label> so the whole row toggles.
131
+
132
+ ```tsx
133
+ <Checkbox checked>
134
+ <Text style={{ fontSize: 14, lineHeight: 20, fontWeight: "500", color: tokens.foreground }}>Email notifications</Text>
135
+ <Text style={{ fontSize: 12, lineHeight: 16, fontWeight: "400", color: tokens["muted-foreground"] }}>
136
+ Get notified when activity happens on your account.</Text>
137
+ </Checkbox>
138
+ ```
139
+
140
+ **Don't** — A bare div makes only the 16px box clickable; the label text does nothing.
141
+
142
+ ```tsx
143
+ <View style={{ flexDirection: "row", alignItems: "flex-start", gap: 8 }}>
144
+ <Checkbox checked />
145
+ <View>
146
+ <Text style={{ fontSize: 14, lineHeight: 20, fontWeight: "500", color: tokens.foreground }}>Email notifications</Text>
147
+ <Text style={{ fontSize: 12, lineHeight: 16, color: tokens["muted-foreground"] }}>Get notified when activity happens on your account.</Text>
148
+ </View>
149
+ </View>
150
+ ```