@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,120 @@
1
+ # Button Groups
2
+
3
+ Segmented controls, split buttons, attached groups.
4
+
5
+ ## Usage
6
+
7
+ ```tsx
8
+ <ButtonGroup segmented active={0} items={["Day", "Week", "Month"]} small />
9
+ ```
10
+
11
+ ## Variants
12
+
13
+ ### Variant - attached
14
+
15
+ ```tsx
16
+ <ButtonGroup
17
+ stepper
18
+ items={[
19
+ "May 21",
20
+ "May 22",
21
+ "May 23",
22
+ "Today",
23
+ "May 25",
24
+ "May 26",
25
+ "May 27"
26
+ ]}
27
+ active={3}
28
+ small
29
+ />
30
+ ```
31
+
32
+ ### Variant - split
33
+
34
+ ```tsx
35
+ <ButtonGroup
36
+ split
37
+ items={["Save"]}
38
+ menu={["Save as draft", "Save and close", "Save a copy"]}
39
+ small
40
+ />
41
+ ```
42
+
43
+ ### Size - default
44
+
45
+ ```tsx
46
+ <ButtonGroup segmented active={0} items={["Day", "Week", "Month"]} />
47
+ ```
48
+
49
+ ### Size - lg
50
+
51
+ ```tsx
52
+ <ButtonGroup segmented active={0} items={["Day", "Week", "Month"]} large />
53
+ ```
54
+
55
+ ### Disabled
56
+
57
+ ```tsx
58
+ <ButtonGroup segmented active={0} items={["Day", "Week", "Month"]} disabled small />
59
+ ```
60
+
61
+ ## Do & Don't
62
+
63
+ ### Segmented
64
+
65
+ **Do** — Keep a segmented control to a few mutually-exclusive views.
66
+
67
+ ```tsx
68
+ <ButtonGroup segmented active={0} items={["Day", "Week", "Month"]} />
69
+ ```
70
+
71
+ **Don't** — Past ~4 options a segmented control gets cramped and hard to scan; reach for a select.
72
+
73
+ ```tsx
74
+ <ButtonGroup segmented active={0} items={["Day", "Week", "Month", "Quarter", "Year", "5Y", "All"]} />
75
+ ```
76
+
77
+ ### Attached
78
+
79
+ **Do** — Reserve attached groups for closely-related actions like prev / today / next.
80
+
81
+ ```tsx
82
+ <ButtonGroup stepper active={1} items={["Yesterday", "Today", "Tomorrow"]} />
83
+ ```
84
+
85
+ **Don't** — Attaching unrelated actions implies they belong to one control.
86
+
87
+ ```tsx
88
+ <ButtonGroup segmented active={-1} items={["Save", "Delete", "Export"]} />
89
+ ```
90
+
91
+ ### Split
92
+
93
+ **Do** — Separate the chevron with a hairline so the secondary menu reads as distinct.
94
+
95
+ ```tsx
96
+ <ButtonGroup split items={["Save"]} menu={["Save as draft", "Save and close", "Save a copy"]} />
97
+ ```
98
+
99
+ **Don't** — With no divider the chevron looks like part of one button, hiding the menu.
100
+
101
+ ```tsx
102
+ <View style={{ flexDirection: "row", alignItems: "center", alignSelf: "flex-start" }}>
103
+ <Pressable
104
+ style={({ pressed }) => [
105
+ { flexDirection: "row", alignItems: "center", justifyContent: "center", height: 36, paddingHorizontal: 16, borderTopLeftRadius: 6, borderBottomLeftRadius: 6, borderTopRightRadius: 0, borderBottomRightRadius: 0, backgroundColor: tokens.primary },
106
+ pressed ? { opacity: 0.9 } : null
107
+ ]}
108
+ >
109
+ <Text style={{ fontWeight: "500", fontSize: 14, lineHeight: 20, color: tokens["primary-foreground"] }}>Save</Text>
110
+ </Pressable>
111
+ <Pressable
112
+ style={({ pressed }) => [
113
+ { flexDirection: "row", alignItems: "center", justifyContent: "center", height: 36, paddingHorizontal: 8, borderTopRightRadius: 6, borderBottomRightRadius: 6, borderTopLeftRadius: 0, borderBottomLeftRadius: 0, backgroundColor: tokens.primary },
114
+ pressed ? { opacity: 0.9 } : null
115
+ ]}
116
+ >
117
+ <Icon chevronDown primaryForeground size={16} />
118
+ </Pressable>
119
+ </View>
120
+ ```
@@ -0,0 +1,398 @@
1
+ import { useState } from "react";
2
+ import { type GestureResponderEvent } from "react-native";
3
+ import { View, Pressable, Text, useTheme, type ColorTokens, type StyleProp, type ViewStyle, type TextStyle } from "../../style/index.js";
4
+ import { Icon } from "../icon/icon.js";
5
+ import * as s from "./button-group.styles.js";
6
+
7
+ // Shared ButtonGroup shell. The structure (the four kinds, their layout, the
8
+ // uncontrolled stepper position, the split dropdown), the accessibility, the
9
+ // kind/size precedence, and the handlers live here once; a platform file supplies
10
+ // only its skin (the native container shape, selected-segment treatment, label
11
+ // color, dividers, press feedback) and calls createButtonGroup.
12
+ //
13
+ // A button group is a horizontal row of buttons that read as one control.
14
+ //
15
+ // Four kinds, picked by boolean prop (first match wins):
16
+ // - `segmented` (default): attached segments sharing one control; one segment
17
+ // reads selected via `active`. Use for mutually exclusive views (Day / Week /
18
+ // Month). The native shape differs per OS: iOS draws a gray track with a
19
+ // raised white pill on the selected segment; Android a stadium-outlined group
20
+ // with a tonal selected fill; web the joined-buttons look with a solid fill.
21
+ // - `split`: a primary action attached to a chevron trigger, divided by a
22
+ // hairline; the chevron opens a dropdown of related actions (`menu`). Use
23
+ // for one primary action with a few related variants.
24
+ // - `stepper`: a prev / current / next control whose chevrons are built in;
25
+ // `items` is the list it cycles through (wrapping at the ends) and the middle
26
+ // label tracks the position. Use for stepping an ordered set (dates, pages).
27
+ // - `spaced`: a plain row of detached buttons separated by a gap. Use for
28
+ // a few peer actions that do not form a single control.
29
+ //
30
+ // Because there are no `first:` / `last:` style variants, the joined-corner and
31
+ // shared-border math is computed per segment in the skin rather than in markup.
32
+
33
+ export type Kind = "segmented" | "split" | "stepper" | "spaced";
34
+ export type Size = "small" | "default" | "large";
35
+
36
+ // Icon color axis (semantic boolean props), chosen by the skin per platform.
37
+ export type IconColor = "primary" | "primaryForeground" | "muted" | "foreground";
38
+
39
+ // Spread the chosen Icon color as its boolean prop (foreground is the default,
40
+ // so it needs none). Keeps the semantic prop API: no raw color strings.
41
+ function iconColorProps(c: IconColor) {
42
+ switch (c) {
43
+ case "primary": return { primary: true } as const;
44
+ case "primaryForeground": return { primaryForeground: true } as const;
45
+ case "muted": return { muted: true } as const;
46
+ case "foreground": return {} as const;
47
+ }
48
+ }
49
+
50
+ // The platform-varying surface. Everything color/shape-bearing the four kinds
51
+ // need lives here, built from the active tokens (so each follows light/dark/glass).
52
+ export interface ButtonGroupSkin {
53
+ // --- segmented / spaced ---
54
+ /** Optional gray track wrapping the segmented row (iOS); null = bare row. */
55
+ segmentedWrap: (t: ColorTokens) => ViewStyle | null;
56
+ /** Border width on each segment cell (0 on iOS/Android, 1 on web). */
57
+ segmentBorderWidth: number;
58
+ /** Corner radii for an attached segment given its position in the row. */
59
+ joinCorners: (index: number, count: number) => ViewStyle;
60
+ /** Corner radii for a detached (spaced) peer. */
61
+ spacedCorners: ViewStyle;
62
+ /** Shared-border overlap applied to every non-leading segment; null = none. */
63
+ overlap: ViewStyle | null;
64
+ /** Optional leading divider on every non-leading segment (Android stadium). */
65
+ segmentDivider?: (t: ColorTokens) => ViewStyle;
66
+ /** Selected vs. unselected segment fill/border. */
67
+ segmentSurface: (t: ColorTokens, selected: boolean) => ViewStyle;
68
+ /** Segment label color/weight for selected vs. unselected. */
69
+ segmentLabel: (t: ColorTokens, selected: boolean) => TextStyle;
70
+ /** Show a leading check glyph on the selected segment (Android M3). */
71
+ showSelectedCheck: boolean;
72
+
73
+ // --- split ---
74
+ splitPrimary: (t: ColorTokens) => ViewStyle;
75
+ splitPrimaryLabel: (t: ColorTokens) => TextStyle;
76
+ splitDivider: (t: ColorTokens, height: number) => ViewStyle;
77
+ splitTrigger: (t: ColorTokens, height: number) => ViewStyle;
78
+ splitChevronColor: IconColor;
79
+ splitMenu: (t: ColorTokens) => ViewStyle;
80
+ splitMenuItemPressed: (t: ColorTokens) => ViewStyle;
81
+ splitMenuText: (t: ColorTokens) => TextStyle;
82
+
83
+ // --- stepper ---
84
+ stepperArrow: (t: ColorTokens, height: number) => ViewStyle;
85
+ stepperArrowLeft: ViewStyle;
86
+ stepperArrowRight: ViewStyle;
87
+ stepperMiddle: (t: ColorTokens) => ViewStyle;
88
+ stepperLabel: (t: ColorTokens) => TextStyle;
89
+ stepperChevronColor: IconColor;
90
+
91
+ // --- feedback ---
92
+ /** iOS/web dim the cell on press; Android uses a ripple instead (null). */
93
+ pressedOpacity: number | null;
94
+ /** Android ripple over a pressed cell; null on iOS/web. */
95
+ ripple?: (t: ColorTokens) => { color: string; borderless: boolean };
96
+ }
97
+
98
+ export interface ButtonGroupProps {
99
+ /** Segment labels for segmented/spaced; the values the stepper cycles through. */
100
+ items?: string[];
101
+ /** Selected segment index (segmented), or the stepper's initial index. */
102
+ active?: number;
103
+ /** Called with the pressed/selected index and item (and, for the stepper, the new index). */
104
+ onSelect?: (index: number, item: string, event: GestureResponderEvent) => void;
105
+
106
+ // Kind (pick one; default is segmented).
107
+ segmented?: boolean;
108
+ split?: boolean;
109
+ stepper?: boolean;
110
+ spaced?: boolean;
111
+
112
+ /** Related actions shown in the split kind's chevron dropdown. */
113
+ menu?: string[];
114
+
115
+ // Size (pick one; default is the medium size).
116
+ small?: boolean;
117
+ large?: boolean;
118
+
119
+ disabled?: boolean;
120
+ /** Escape hatch for layout/positioning composition (margins, alignment). */
121
+ style?: StyleProp<ViewStyle>;
122
+ }
123
+
124
+ // Kind precedence when more than one is passed: first match wins.
125
+ function kindOf(p: ButtonGroupProps): Kind {
126
+ if (p.segmented) return "segmented";
127
+ if (p.split) return "split";
128
+ if (p.stepper) return "stepper";
129
+ if (p.spaced) return "spaced";
130
+ return "segmented";
131
+ }
132
+
133
+ // Size precedence when more than one is passed: first match wins.
134
+ function sizeOf(p: ButtonGroupProps): Size {
135
+ if (p.small) return "small";
136
+ if (p.large) return "large";
137
+ return "default";
138
+ }
139
+
140
+ const DEFAULT_ITEMS = ["Day", "Week", "Month"];
141
+ const DEFAULT_MENU = ["Save as draft", "Save and close", "Save a copy"];
142
+
143
+ /** Build a ButtonGroup component from a platform skin. */
144
+ export function createButtonGroup(skin: ButtonGroupSkin) {
145
+ const ripple = skin.ripple;
146
+
147
+ interface SegmentProps {
148
+ label: string;
149
+ selected: boolean;
150
+ /** Corner radii for this segment given its position in the row. */
151
+ corners: ViewStyle;
152
+ /** This segment overlaps the previous border / draws a leading divider. */
153
+ leading: boolean;
154
+ size: Size;
155
+ disabled?: boolean;
156
+ onPress?: (event: GestureResponderEvent) => void;
157
+ }
158
+
159
+ function Segment({ label, selected, corners, leading, size, disabled, onPress }: SegmentProps) {
160
+ const { tokens } = useTheme();
161
+ const container: StyleProp<ViewStyle> = [
162
+ s.segmentBase,
163
+ { borderWidth: skin.segmentBorderWidth },
164
+ s.sizeContainer[size],
165
+ corners,
166
+ leading && skin.overlap ? skin.overlap : null,
167
+ leading && skin.segmentDivider ? skin.segmentDivider(tokens) : null,
168
+ skin.segmentSurface(tokens, selected),
169
+ disabled ? s.dim : null,
170
+ ];
171
+ return (
172
+ <Pressable
173
+ style={({ pressed }) => [container, skin.pressedOpacity != null && pressed ? { opacity: skin.pressedOpacity } : null]}
174
+ onPress={onPress}
175
+ disabled={disabled}
176
+ android_ripple={ripple ? ripple(tokens) : undefined}
177
+ accessibilityRole="button"
178
+ accessibilityState={{ selected, disabled: !!disabled }}
179
+ >
180
+ {skin.showSelectedCheck && selected ? (
181
+ <Icon check primary size={s.chevronSize[size]} style={{ marginRight: 6 }} />
182
+ ) : null}
183
+ <Text style={[s.sizeLabel[size], skin.segmentLabel(tokens, selected)]}>{label}</Text>
184
+ </Pressable>
185
+ );
186
+ }
187
+
188
+ // The split kind's secondary control: a chevron that toggles a floating dropdown
189
+ // of related actions, anchored to the right edge of the primary button. The
190
+ // menu floats (absolute) so it overflows the group rather than growing it.
191
+ function SplitButton({
192
+ primary,
193
+ menu,
194
+ size,
195
+ disabled,
196
+ onSelect,
197
+ style,
198
+ }: {
199
+ primary: string;
200
+ menu: string[];
201
+ size: Size;
202
+ disabled?: boolean;
203
+ onSelect?: (index: number, item: string, event: GestureResponderEvent) => void;
204
+ style?: StyleProp<ViewStyle>;
205
+ }) {
206
+ const { tokens } = useTheme();
207
+ const [open, setOpen] = useState(false);
208
+ const triggerHeight = s.sizeHeight[size];
209
+ return (
210
+ <View style={[s.splitContainer, open ? s.splitContainerLifted : null, disabled ? s.dim : null, style]}>
211
+ <Pressable
212
+ style={({ pressed }) => [skin.splitPrimary(tokens), s.sizeContainer[size], skin.pressedOpacity != null && pressed ? { opacity: skin.pressedOpacity } : null]}
213
+ onPress={(e) => onSelect?.(0, primary, e)}
214
+ disabled={disabled}
215
+ android_ripple={ripple ? ripple(tokens) : undefined}
216
+ accessibilityRole="button"
217
+ >
218
+ <Text style={[skin.splitPrimaryLabel(tokens), s.sizeLabel[size]]}>{primary}</Text>
219
+ </Pressable>
220
+ {/* Hairline divider so the chevron reads as a distinct trigger. */}
221
+ <View style={skin.splitDivider(tokens, triggerHeight)} />
222
+ <Pressable
223
+ style={({ pressed }) => [skin.splitTrigger(tokens, triggerHeight), skin.pressedOpacity != null && pressed ? { opacity: skin.pressedOpacity } : null]}
224
+ onPress={() => setOpen((o) => !o)}
225
+ disabled={disabled}
226
+ android_ripple={ripple ? ripple(tokens) : undefined}
227
+ accessibilityRole="button"
228
+ accessibilityState={{ expanded: open }}
229
+ accessibilityLabel="More actions"
230
+ >
231
+ <View style={{ transform: [{ rotate: open ? "180deg" : "0deg" }] }}>
232
+ <Icon chevronDown size={s.chevronSize[size]} {...iconColorProps(skin.splitChevronColor)} />
233
+ </View>
234
+ </Pressable>
235
+ {open ? (
236
+ <View style={skin.splitMenu(tokens)}>
237
+ {menu.map((item, i) => (
238
+ <Pressable
239
+ key={`${item}-${i}`}
240
+ style={({ pressed }) => [s.splitMenuItem, pressed ? skin.splitMenuItemPressed(tokens) : null]}
241
+ onPress={(e) => {
242
+ onSelect?.(i + 1, item, e);
243
+ setOpen(false);
244
+ }}
245
+ accessibilityRole="menuitem"
246
+ >
247
+ <Text style={skin.splitMenuText(tokens)}>{item}</Text>
248
+ </Pressable>
249
+ ))}
250
+ </View>
251
+ ) : null}
252
+ </View>
253
+ );
254
+ }
255
+
256
+ // Stepper: a prev / current / next control. The chevrons are built in here; the
257
+ // `items` array is what it cycles through. Uncontrolled, it tracks its own
258
+ // position from the initial index, wraps at the ends, and reports each change
259
+ // through onSelect. The middle cell is a passive label showing the current item.
260
+ function Stepper({
261
+ items,
262
+ initial,
263
+ size,
264
+ disabled,
265
+ onSelect,
266
+ style,
267
+ }: {
268
+ items: string[];
269
+ initial: number;
270
+ size: Size;
271
+ disabled?: boolean;
272
+ onSelect?: (index: number, item: string, event: GestureResponderEvent) => void;
273
+ style?: StyleProp<ViewStyle>;
274
+ }) {
275
+ const { tokens } = useTheme();
276
+ const count = items.length;
277
+ const clamp = (n: number) => (count > 0 ? Math.min(Math.max(0, n), count - 1) : 0);
278
+ const [index, setIndex] = useState(() => clamp(initial));
279
+ const i = clamp(index);
280
+ const chevron = s.chevronSize[size];
281
+ const height = s.sizeHeight[size];
282
+ const step = (dir: number, e: GestureResponderEvent) => {
283
+ if (count === 0) return;
284
+ const next = (i + dir + count) % count;
285
+ setIndex(next);
286
+ onSelect?.(next, items[next], e);
287
+ };
288
+ return (
289
+ <View style={[s.stepperContainer, disabled ? s.dim : null, style]}>
290
+ <Pressable
291
+ style={({ pressed }) => [skin.stepperArrow(tokens, height), skin.stepperArrowLeft, skin.pressedOpacity != null && pressed ? { opacity: skin.pressedOpacity } : null]}
292
+ onPress={(e) => step(-1, e)}
293
+ disabled={disabled}
294
+ android_ripple={ripple ? ripple(tokens) : undefined}
295
+ accessibilityRole="button"
296
+ accessibilityLabel="Previous"
297
+ >
298
+ <Icon chevronLeft size={chevron} {...iconColorProps(skin.stepperChevronColor)} />
299
+ </Pressable>
300
+ <View style={[skin.stepperMiddle(tokens), s.sizeContainer[size]]}>
301
+ <Text style={[skin.stepperLabel(tokens), s.sizeLabel[size]]}>{items[i] ?? ""}</Text>
302
+ </View>
303
+ <Pressable
304
+ style={({ pressed }) => [skin.stepperArrow(tokens, height), skin.stepperArrowRight, skin.pressedOpacity != null && pressed ? { opacity: skin.pressedOpacity } : null]}
305
+ onPress={(e) => step(1, e)}
306
+ disabled={disabled}
307
+ android_ripple={ripple ? ripple(tokens) : undefined}
308
+ accessibilityRole="button"
309
+ accessibilityLabel="Next"
310
+ >
311
+ <Icon chevronRight size={chevron} {...iconColorProps(skin.stepperChevronColor)} />
312
+ </Pressable>
313
+ </View>
314
+ );
315
+ }
316
+
317
+ return function ButtonGroup(props: ButtonGroupProps) {
318
+ const { items = DEFAULT_ITEMS, active = 0, onSelect, disabled, style } = props;
319
+ const { tokens } = useTheme();
320
+ const kind = kindOf(props);
321
+ const size = sizeOf(props);
322
+
323
+ // Spaced: detached peers separated by a gap, each with full rounding.
324
+ if (kind === "spaced") {
325
+ return (
326
+ <View style={[s.spacedContainer, style]}>
327
+ {items.map((item, i) => (
328
+ <Segment
329
+ key={`${item}-${i}`}
330
+ label={item}
331
+ selected={false}
332
+ corners={skin.spacedCorners}
333
+ leading={false}
334
+ size={size}
335
+ disabled={disabled}
336
+ onPress={(e) => onSelect?.(i, item, e)}
337
+ />
338
+ ))}
339
+ </View>
340
+ );
341
+ }
342
+
343
+ // Split: a primary action attached to a chevron that opens a dropdown of
344
+ // related actions.
345
+ if (kind === "split") {
346
+ const labels = items.length > 0 ? items : DEFAULT_ITEMS;
347
+ const primary = labels[0] ?? "Save";
348
+ return (
349
+ <SplitButton
350
+ primary={primary}
351
+ menu={props.menu && props.menu.length > 0 ? props.menu : DEFAULT_MENU}
352
+ size={size}
353
+ disabled={disabled}
354
+ onSelect={onSelect}
355
+ style={style}
356
+ />
357
+ );
358
+ }
359
+
360
+ // Stepper: a prev / current / next control that cycles through items; the
361
+ // component owns the chevrons and the position.
362
+ if (kind === "stepper") {
363
+ const list = items.length > 0 ? items : DEFAULT_ITEMS;
364
+ return (
365
+ <Stepper
366
+ items={list}
367
+ initial={active}
368
+ size={size}
369
+ disabled={disabled}
370
+ onSelect={onSelect}
371
+ style={style}
372
+ />
373
+ );
374
+ }
375
+
376
+ // Segmented (default): attached segments, one selected. iOS wraps the row in
377
+ // a gray track; Android/web render the row bare (the stadium/joined borders
378
+ // are drawn by the segments themselves).
379
+ const count = items.length;
380
+ const wrap = skin.segmentedWrap(tokens);
381
+ const row = items.map((item, i) => (
382
+ <Segment
383
+ key={`${item}-${i}`}
384
+ label={item}
385
+ selected={i === active}
386
+ corners={skin.joinCorners(i, count)}
387
+ leading={i > 0}
388
+ size={size}
389
+ disabled={disabled}
390
+ onPress={(e) => onSelect?.(i, item, e)}
391
+ />
392
+ ));
393
+ if (wrap) {
394
+ return <View style={[wrap, style]}>{row}</View>;
395
+ }
396
+ return <View style={[s.segmentedContainer, style]}>{row}</View>;
397
+ };
398
+ }