@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,130 @@
1
+ import { type ReactNode } from "react";
2
+ import { type GestureResponderEvent } from "react-native";
3
+ import { View, Pressable, Text, useTheme, type StyleProp, type ViewStyle, type TextStyle } from "../../style/index.js";
4
+ import { Icon } from "../icon/icon.js";
5
+ import * as s from "./breadcrumb.styles.js";
6
+
7
+ // A breadcrumb is a horizontal trail of links separated by a divider glyph, with
8
+ // the last item current (non-link, emphasized). Ancestors are muted links; the
9
+ // page you are on is plain foreground text at the end of the trail.
10
+ //
11
+ // Boolean-prop API, one axis: the separator style. Pick one of `chevron`
12
+ // (default), `slash`, or `dot`; first match wins (mirrors Button's intentOf). The
13
+ // foundation has no SVG utility here, so the chevron is a reading-direction glyph
14
+ // (the single-guillemet "›") rather than an icon, keeping every separator a Text
15
+ // glyph pointing in the reading direction.
16
+
17
+ export interface BreadcrumbProps {
18
+ /**
19
+ * The trail, ancestor-first. The last entry renders as the current page (plain
20
+ * emphasized text); the rest render as muted links.
21
+ */
22
+ items?: string[];
23
+ // Separator style (pick one; default is the chevron glyph).
24
+ chevron?: boolean;
25
+ slash?: boolean;
26
+ dot?: boolean;
27
+ /**
28
+ * Leading affordance: prepend a muted home-icon crumb (aria-labelled "Home")
29
+ * before the trail, followed by the separator. Off by default.
30
+ */
31
+ homeIcon?: boolean;
32
+ /** Fired with the crumb label and its index when a link (non-last) is pressed. */
33
+ onItemPress?: (item: string, index: number) => void;
34
+ /** Escape hatch for layout/positioning composition. */
35
+ style?: StyleProp<ViewStyle>;
36
+ }
37
+
38
+ type Separator = "chevron" | "slash" | "dot";
39
+
40
+ // Separator precedence when more than one flag is passed: first match wins.
41
+ function separatorOf(p: BreadcrumbProps): Separator {
42
+ if (p.chevron) return "chevron";
43
+ if (p.slash) return "slash";
44
+ if (p.dot) return "dot";
45
+ return "chevron";
46
+ }
47
+
48
+ // The divider glyph per style. A centered middot for `dot`, a forward slash for
49
+ // `slash`, and a reading-direction single guillemet for `chevron` (stands in for
50
+ // the SVG chevron not rendered here).
51
+ const SEPARATOR_GLYPH: Record<Separator, string> = {
52
+ chevron: "›",
53
+ slash: "/",
54
+ dot: "·",
55
+ };
56
+
57
+ export interface BreadcrumbItemProps {
58
+ children?: ReactNode;
59
+ /** Render as the current page: emphasized foreground text, non-interactive. */
60
+ current?: boolean;
61
+ onPress?: (event: GestureResponderEvent) => void;
62
+ /** Escape hatch for layout/positioning composition. */
63
+ style?: StyleProp<TextStyle>;
64
+ }
65
+
66
+ // A single crumb. Current crumbs are plain emphasized text; ancestors are muted,
67
+ // pressable links.
68
+ export function BreadcrumbItem(props: BreadcrumbItemProps) {
69
+ const { children, current, onPress, style } = props;
70
+ const { tokens } = useTheme();
71
+ if (children == null) return null;
72
+ if (current) {
73
+ return <Text style={[s.current(tokens), style]} accessibilityRole="text">{children}</Text>;
74
+ }
75
+ return (
76
+ <Pressable onPress={onPress} accessibilityRole="link">
77
+ {({ pressed }) => (
78
+ <Text style={[s.link(tokens), pressed ? { opacity: 0.7 } : null, style]}>{children}</Text>
79
+ )}
80
+ </Pressable>
81
+ );
82
+ }
83
+
84
+ export function Breadcrumb(props: BreadcrumbProps) {
85
+ const { items, homeIcon, onItemPress, style } = props;
86
+ const { tokens } = useTheme();
87
+ const trail = items ?? [];
88
+ const separator = separatorOf(props);
89
+ const glyph = SEPARATOR_GLYPH[separator];
90
+
91
+ return (
92
+ <View style={[s.nav, style]} accessibilityRole="header">
93
+ {homeIcon ? (
94
+ <View style={s.crumb}>
95
+ <Pressable
96
+ onPress={() => onItemPress?.("Home", 0)}
97
+ accessibilityRole="link"
98
+ accessibilityLabel="Home"
99
+ style={({ pressed }) => (pressed ? { opacity: 0.7 } : null)}
100
+ >
101
+ <Icon home muted size={14} />
102
+ </Pressable>
103
+ {trail.length === 0 ? null : (
104
+ <Text style={s.separator(tokens)} accessibilityElementsHidden>
105
+ {glyph}
106
+ </Text>
107
+ )}
108
+ </View>
109
+ ) : null}
110
+ {trail.map((item, index) => {
111
+ const last = index === trail.length - 1;
112
+ return (
113
+ <View key={`${index}-${item}`} style={s.crumb}>
114
+ <BreadcrumbItem
115
+ current={last}
116
+ onPress={last ? undefined : () => onItemPress?.(item, index)}
117
+ >
118
+ {item}
119
+ </BreadcrumbItem>
120
+ {last ? null : (
121
+ <Text style={s.separator(tokens)} accessibilityElementsHidden>
122
+ {glyph}
123
+ </Text>
124
+ )}
125
+ </View>
126
+ );
127
+ })}
128
+ </View>
129
+ );
130
+ }
@@ -0,0 +1,6 @@
1
+ import { createButton } from "./button.shared.js";
2
+ import { androidSkin } from "./button.styles.js";
3
+
4
+ // Material 3 Button. Metro resolves this file on Android; the docs import it for preview.
5
+ export const Button = createButton(androidSkin);
6
+ export type { ButtonProps } from "./button.shared.js";
@@ -0,0 +1,6 @@
1
+ import { createButton } from "./button.shared.js";
2
+ import { iosSkin } from "./button.styles.js";
3
+
4
+ // iOS (HIG) Button. Metro resolves this file on iOS; the docs import it for preview.
5
+ export const Button = createButton(iosSkin);
6
+ export type { ButtonProps } from "./button.shared.js";
@@ -0,0 +1,184 @@
1
+ # Buttons
2
+
3
+ Six variants × four sizes × disabled / focus / hover states. Always semantic: variant communicates intent (default = primary action, destructive = irreversible, ghost = chrome).
4
+
5
+ ## Usage
6
+
7
+ ```tsx
8
+ <Button primary>Save changes</Button>
9
+ ```
10
+
11
+ ## Variants
12
+
13
+ ### Variant - outline
14
+
15
+ ```tsx
16
+ <Button outline>Save changes</Button>
17
+ ```
18
+
19
+ ### Variant - secondary
20
+
21
+ ```tsx
22
+ <Button secondary>Save changes</Button>
23
+ ```
24
+
25
+ ### Variant - ghost
26
+
27
+ ```tsx
28
+ <Button ghost>Save changes</Button>
29
+ ```
30
+
31
+ ### Variant - destructive
32
+
33
+ ```tsx
34
+ <Button destructive>Save changes</Button>
35
+ ```
36
+
37
+ ### Variant - link
38
+
39
+ ```tsx
40
+ <Button link>Save changes</Button>
41
+ ```
42
+
43
+ ### Size - sm
44
+
45
+ ```tsx
46
+ <Button primary small>Save changes</Button>
47
+ ```
48
+
49
+ ### Size - lg
50
+
51
+ ```tsx
52
+ <Button primary large>Save changes</Button>
53
+ ```
54
+
55
+ ### Size - icon
56
+
57
+ ```tsx
58
+ <Button primary icon>+</Button>
59
+ ```
60
+
61
+ ### Disabled
62
+
63
+ ```tsx
64
+ <Button primary disabled>Save changes</Button>
65
+ ```
66
+
67
+ ### With icon
68
+
69
+ ```tsx
70
+ <Button primary>+ Save changes</Button>
71
+ ```
72
+
73
+ ## Do & Don't
74
+
75
+ ### Default (primary)
76
+
77
+ **Do** — One clear primary action; everything else is supporting.
78
+
79
+ ```tsx
80
+ <View style={{ flexDirection: "row", alignItems: "center", gap: 8 }}>
81
+ <Button primary>Save</Button>
82
+ <Button outline>Cancel</Button>
83
+ </View>
84
+ ```
85
+
86
+ **Don't** — Multiple primaries compete; nothing stands out.
87
+
88
+ ```tsx
89
+ <View style={{ flexDirection: "row", alignItems: "center", gap: 8 }}>
90
+ <Button primary>Save</Button>
91
+ <Button primary>Apply</Button>
92
+ <Button primary>Continue</Button>
93
+ </View>
94
+ ```
95
+
96
+ ### Outline
97
+
98
+ **Do** — Promote the main action to default; keep the rest outline.
99
+
100
+ ```tsx
101
+ <View style={{ flexDirection: "row", alignItems: "center", gap: 8 }}>
102
+ <Button primary>Publish</Button>
103
+ <Button outline>Save draft</Button>
104
+ <Button outline>Schedule</Button>
105
+ </View>
106
+ ```
107
+
108
+ **Don't** — All-outline leaves no signal which action is primary.
109
+
110
+ ```tsx
111
+ <View style={{ flexDirection: "row", alignItems: "center", gap: 8 }}>
112
+ <Button outline>Save</Button>
113
+ <Button outline>Publish</Button>
114
+ <Button outline>Schedule</Button>
115
+ </View>
116
+ ```
117
+
118
+ ### Secondary
119
+
120
+ **Do** — Default for the primary action; secondary for the next one down.
121
+
122
+ ```tsx
123
+ <View style={{ flexDirection: "row", alignItems: "center", gap: 8 }}>
124
+ <Button primary>Create account</Button>
125
+ <Button secondary>Import instead</Button>
126
+ </View>
127
+ ```
128
+
129
+ **Don't** — A secondary button as the main call to action under-sells it.
130
+
131
+ ```tsx
132
+ <Button secondary>Create account</Button>
133
+ ```
134
+
135
+ ### Ghost
136
+
137
+ **Do** — Use ghost for tertiary and toolbar actions; keep the CTA filled.
138
+
139
+ ```tsx
140
+ <View style={{ flexDirection: "row", alignItems: "center", gap: 8 }}>
141
+ <Button ghost>Cancel</Button>
142
+ <Button primary>Save changes</Button>
143
+ </View>
144
+ ```
145
+
146
+ **Don't** — A ghost button is too quiet to carry the primary action.
147
+
148
+ ```tsx
149
+ <Button ghost>Save changes</Button>
150
+ ```
151
+
152
+ ### Destructive
153
+
154
+ **Do** — Reserve the destructive variant for irreversible actions like delete.
155
+
156
+ ```tsx
157
+ <View style={{ flexDirection: "row", alignItems: "center", gap: 8 }}>
158
+ <Button primary>Save changes</Button>
159
+ <Button destructive>Delete account</Button>
160
+ </View>
161
+ ```
162
+
163
+ **Don't** — Red on a safe action cries wolf; users learn to ignore it.
164
+
165
+ ```tsx
166
+ <Button destructive>Save changes</Button>
167
+ ```
168
+
169
+ ### Link
170
+
171
+ **Do** — Link variant for inline navigation; a filled button for the submit.
172
+
173
+ ```tsx
174
+ <View style={{ flexDirection: "row", alignItems: "center", gap: 12 }}>
175
+ <Button primary>Submit</Button>
176
+ <Button link>Learn more</Button>
177
+ </View>
178
+ ```
179
+
180
+ **Don't** — A link-styled submit doesn't look pressable and gets lost.
181
+
182
+ ```tsx
183
+ <Button link>Submit form</Button>
184
+ ```
@@ -0,0 +1,79 @@
1
+ import { type ReactNode } from "react";
2
+ import { ActivityIndicator, type GestureResponderEvent } from "react-native";
3
+ import { Pressable, Text, useTheme, type StyleProp, type ViewStyle } from "../../style/index.js";
4
+ import { type ButtonSkin, type Intent, type Size, FG_TOKEN } from "./button.styles.js";
5
+
6
+ // Shared Button shell. The structure (Pressable + optional loading spinner +
7
+ // label), the accessibility, and the intent/size precedence live here once; a
8
+ // platform file supplies only its skin (shape, sizing, label weight, press
9
+ // feedback) and calls createButton.
10
+
11
+ export interface ButtonProps {
12
+ children?: ReactNode;
13
+ onPress?: (event: GestureResponderEvent) => void;
14
+ // Intent (pick one; default is the primary action).
15
+ primary?: boolean;
16
+ secondary?: boolean;
17
+ destructive?: boolean;
18
+ outline?: boolean;
19
+ ghost?: boolean;
20
+ link?: boolean;
21
+ // Size (pick one).
22
+ small?: boolean;
23
+ large?: boolean;
24
+ icon?: boolean;
25
+ // Layout and state.
26
+ block?: boolean;
27
+ loading?: boolean;
28
+ disabled?: boolean;
29
+ /** Escape hatch for layout/positioning composition (margins, alignment). */
30
+ style?: StyleProp<ViewStyle>;
31
+ }
32
+
33
+ // Intent precedence when more than one is passed: first match wins.
34
+ function intentOf(p: ButtonProps): Intent {
35
+ if (p.primary) return "primary";
36
+ if (p.destructive) return "destructive";
37
+ if (p.secondary) return "secondary";
38
+ if (p.outline) return "outline";
39
+ if (p.ghost) return "ghost";
40
+ if (p.link) return "link";
41
+ return "primary";
42
+ }
43
+
44
+ // Size precedence when more than one is passed: first match wins.
45
+ function sizeOf(p: ButtonProps): Size {
46
+ if (p.small) return "small";
47
+ if (p.large) return "large";
48
+ return "base";
49
+ }
50
+
51
+ /** Build a Button component from a platform skin. */
52
+ export function createButton(skin: ButtonSkin) {
53
+ return function Button(props: ButtonProps) {
54
+ const { children, onPress, loading, disabled, block, icon, style } = props;
55
+ const { tokens } = useTheme();
56
+ const intent = intentOf(props);
57
+ const size = sizeOf(props);
58
+
59
+ const container = skin.container(tokens, intent, size, { icon: !!icon, block: !!block, dim: !!(disabled || loading) });
60
+ const ripple = skin.ripple ? skin.ripple(tokens, intent) : undefined;
61
+
62
+ return (
63
+ <Pressable
64
+ onPress={onPress}
65
+ disabled={disabled || loading}
66
+ accessibilityRole="button"
67
+ android_ripple={ripple}
68
+ style={({ pressed }) => [
69
+ container,
70
+ skin.pressedOpacity != null && pressed ? { opacity: skin.pressedOpacity } : null,
71
+ style,
72
+ ]}
73
+ >
74
+ {loading ? <ActivityIndicator size="small" color={tokens[FG_TOKEN[intent]]} /> : null}
75
+ {children != null ? <Text style={skin.label(tokens, intent, size)}>{children}</Text> : null}
76
+ </Pressable>
77
+ );
78
+ };
79
+ }
@@ -0,0 +1,152 @@
1
+ import { type ViewStyle, type TextStyle } from "react-native";
2
+ import { type ColorTokens } from "../../style/index.js";
3
+
4
+ // Co-located Button skins, one per platform. The BRAND survives on every platform
5
+ // (fills/labels use the indigo `primary` and the semantic tokens, never a platform
6
+ // default); only the native SHAPE, sizing, label weight, and press feedback change:
7
+ // iOS (HIG / iOS 26+ Liquid Glass): capsule (fully rounded), semibold SF-scale label, dim-on-press.
8
+ // Android (Material 3): fully-rounded pill, medium label, flat, ripple.
9
+ // Web: the established Canvas look (rounded-md, medium label, opacity press).
10
+
11
+ export type Intent = "primary" | "secondary" | "destructive" | "outline" | "ghost" | "link";
12
+ export type Size = "small" | "base" | "large";
13
+
14
+ export interface ButtonSkinOpts {
15
+ icon: boolean;
16
+ block: boolean;
17
+ /** disabled or loading: dim the control. */
18
+ dim: boolean;
19
+ }
20
+
21
+ export interface ButtonSkin {
22
+ container: (t: ColorTokens, intent: Intent, size: Size, opts: ButtonSkinOpts) => ViewStyle;
23
+ label: (t: ColorTokens, intent: Intent, size: Size) => TextStyle;
24
+ /** iOS/web dim the fill on press; Android uses a ripple instead (null). */
25
+ pressedOpacity: number | null;
26
+ ripple: ((t: ColorTokens, intent: Intent) => { color: string; borderless: boolean }) | null;
27
+ }
28
+
29
+ // --- shared brand mapping (identical across platforms) ----------------------
30
+
31
+ /** Foreground token per intent: the label color and the loading-spinner color. */
32
+ export const FG_TOKEN: Record<Intent, keyof ColorTokens> = {
33
+ primary: "primary-foreground",
34
+ secondary: "secondary-foreground",
35
+ destructive: "destructive-foreground",
36
+ outline: "foreground",
37
+ ghost: "foreground",
38
+ link: "primary",
39
+ };
40
+
41
+ const DARK_FILL = new Set<Intent>(["primary", "destructive"]);
42
+
43
+ // Container fill/border per intent (brand colors, shared by all platforms).
44
+ function fill(t: ColorTokens, intent: Intent): ViewStyle {
45
+ switch (intent) {
46
+ case "primary": return { backgroundColor: t.primary };
47
+ case "secondary": return { backgroundColor: t.secondary };
48
+ case "destructive": return { backgroundColor: t.destructive };
49
+ case "outline": return { backgroundColor: "transparent", borderWidth: 1, borderColor: t.input };
50
+ case "ghost": return { backgroundColor: "transparent" };
51
+ case "link": return { backgroundColor: "transparent" };
52
+ }
53
+ }
54
+
55
+ // Label color (+ underline for link) per intent.
56
+ function labelColor(t: ColorTokens, intent: Intent): TextStyle {
57
+ switch (intent) {
58
+ case "primary": return { color: t["primary-foreground"] };
59
+ case "secondary": return { color: t["secondary-foreground"] };
60
+ case "destructive": return { color: t["destructive-foreground"] };
61
+ case "outline": return { color: t.foreground };
62
+ case "ghost": return { color: t.foreground };
63
+ case "link": return { color: t.primary, textDecorationLine: "underline" };
64
+ }
65
+ }
66
+
67
+ // Android ripple over a fill: a light ripple on dark fills, a dark one on light/
68
+ // transparent fills, so the Material press feedback reads on every variant.
69
+ function androidRipple(_t: ColorTokens, intent: Intent) {
70
+ return { color: DARK_FILL.has(intent) ? "rgba(255, 255, 255, 0.24)" : "rgba(0, 0, 0, 0.10)", borderless: false };
71
+ }
72
+
73
+ const ROW: ViewStyle = { flexDirection: "row", alignItems: "center", justifyContent: "center" };
74
+
75
+ // ---------- Web: the established Canvas look ----------
76
+ export const webSkin: ButtonSkin = {
77
+ container: (t, intent, size, o) => ({
78
+ ...ROW,
79
+ gap: 8,
80
+ borderRadius: 6,
81
+ ...(o.icon
82
+ ? sq(size === "small" ? 32 : size === "large" ? 48 : 40)
83
+ : size === "small" ? { paddingHorizontal: 12, paddingVertical: 6 }
84
+ : size === "large" ? { paddingHorizontal: 24, paddingVertical: 12 }
85
+ : { paddingHorizontal: 16, paddingVertical: 8 }),
86
+ ...fill(t, intent),
87
+ ...(o.block ? { width: "100%" } : null),
88
+ ...(o.dim ? { opacity: 0.5 } : null),
89
+ }),
90
+ label: (t, intent, size) => ({
91
+ fontWeight: "500",
92
+ ...(size === "large" ? FS(16, 24) : size === "small" ? FS(12, 16) : FS(14, 20)),
93
+ ...labelColor(t, intent),
94
+ }),
95
+ pressedOpacity: 0.9,
96
+ ripple: null,
97
+ };
98
+
99
+ // ---------- iOS (HIG / iOS 26+ Liquid Glass): capsule, semibold, dim on press ----------
100
+ export const iosSkin: ButtonSkin = {
101
+ container: (t, intent, size, o) => ({
102
+ ...ROW,
103
+ gap: 6,
104
+ borderRadius: 9999, // iOS 27 prominent buttons are capsules (full pill at every size); icon = circle
105
+ ...(o.icon
106
+ ? sq(size === "small" ? 36 : size === "large" ? 52 : 44)
107
+ : size === "small" ? { paddingHorizontal: 14, paddingVertical: 8 }
108
+ : size === "large" ? { paddingHorizontal: 24, paddingVertical: 15 }
109
+ : { paddingHorizontal: 20, paddingVertical: 12 }),
110
+ ...fill(t, intent),
111
+ ...(o.block ? { width: "100%" } : null),
112
+ ...(o.dim ? { opacity: 0.5 } : null),
113
+ }),
114
+ label: (t, intent, size) => ({
115
+ fontWeight: "600",
116
+ ...(size === "small" ? FS(15, 20) : FS(17, 22)),
117
+ ...labelColor(t, intent),
118
+ }),
119
+ pressedOpacity: 0.8,
120
+ ripple: null,
121
+ };
122
+
123
+ // ---------- Android (Material 3 filled): pill, medium label, flat, ripple ----------
124
+ export const androidSkin: ButtonSkin = {
125
+ container: (t, intent, size, o) => ({
126
+ ...ROW,
127
+ gap: 8,
128
+ borderRadius: 9999, // M3 filled button = fully rounded (stadium); icon = circle
129
+ ...(o.icon
130
+ ? sq(size === "small" ? 32 : size === "large" ? 48 : 40)
131
+ : size === "small" ? { paddingHorizontal: 16, paddingVertical: 6 }
132
+ : size === "large" ? { paddingHorizontal: 24, paddingVertical: 13 }
133
+ : { paddingHorizontal: 24, paddingVertical: 10 }),
134
+ ...fill(t, intent),
135
+ ...(o.block ? { width: "100%" } : null),
136
+ ...(o.dim ? { opacity: 0.38 } : null), // M3 disabled opacity
137
+ }),
138
+ label: (t, intent, size) => ({
139
+ fontWeight: "500",
140
+ ...(size === "small" ? FS(13, 18) : FS(14, 20)),
141
+ ...labelColor(t, intent),
142
+ }),
143
+ pressedOpacity: null,
144
+ ripple: androidRipple,
145
+ };
146
+
147
+ function sq(d: number): ViewStyle {
148
+ return { width: d, height: d };
149
+ }
150
+ function FS(fontSize: number, lineHeight: number): TextStyle {
151
+ return { fontSize, lineHeight };
152
+ }
@@ -0,0 +1,6 @@
1
+ import { createButton } from "./button.shared.js";
2
+ import { webSkin } from "./button.styles.js";
3
+
4
+ // Web Button (the base; Metro falls back to it on native, web bundlers resolve it).
5
+ export const Button = createButton(webSkin);
6
+ export type { ButtonProps } from "./button.shared.js";
@@ -0,0 +1,6 @@
1
+ import { createButtonGroup } from "./button-group.shared.js";
2
+ import { androidSkin } from "./button-group.styles.js";
3
+
4
+ // Material 3 SegmentedButton ButtonGroup. Metro resolves this file on Android; the docs import it for preview.
5
+ export const ButtonGroup = createButtonGroup(androidSkin);
6
+ export type { ButtonGroupProps } from "./button-group.shared.js";
@@ -0,0 +1,6 @@
1
+ import { createButtonGroup } from "./button-group.shared.js";
2
+ import { iosSkin } from "./button-group.styles.js";
3
+
4
+ // iOS (UISegmentedControl) ButtonGroup. Metro resolves this file on iOS; the docs import it for preview.
5
+ export const ButtonGroup = createButtonGroup(iosSkin);
6
+ export type { ButtonGroupProps } from "./button-group.shared.js";