@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,158 @@
1
+ import { type DimensionValue } from "react-native";
2
+ import { View, Pressable, Text, useTheme, type StyleProp, type ViewStyle } from "../../style/index.js";
3
+ import * as s from "./stepper.styles.js";
4
+ import { type State, type StepperSkin } from "./stepper.styles.js";
5
+
6
+ // Shared Stepper shell. The structure (the numbered/check circles joined by
7
+ // connectors, plus the vertical and progress-bar layouts), the layout
8
+ // precedence, the per-step state derivation, the press handlers, and the
9
+ // accessibility all live here once; a platform file supplies only its skin (the
10
+ // circle/connector colors, the connector cap, the current-step emphasis, and
11
+ // the press feedback) and calls createStepper.
12
+ //
13
+ // Boolean-prop API: layout is a single axis with `progress` and `vertical`
14
+ // opting out of the default horizontal layout (first-match precedence,
15
+ // mirroring Divider). `progress` beats `vertical` beats the horizontal default.
16
+
17
+ export interface Step {
18
+ label: string;
19
+ description?: string;
20
+ }
21
+
22
+ export interface StepperProps {
23
+ /** Ordered steps to render. Each is a label with an optional one-line note. */
24
+ steps: Step[];
25
+ /** Index of the active step. Earlier steps read completed, later ones muted. */
26
+ current: number;
27
+ // Layout (pick one; default is horizontal).
28
+ vertical?: boolean;
29
+ /** Render a labeled percentage progress bar instead of discrete steps. */
30
+ progress?: boolean;
31
+ /** Progress mode only: filled fraction, 0-100 (clamped). Defaults to 0. */
32
+ value?: number;
33
+ /** Progress mode only: caption shown left of the percentage. */
34
+ label?: string;
35
+ /** When set, each step circle is pressable, reporting the step index. */
36
+ onStepPress?: (index: number) => void;
37
+ /** Escape hatch for layout/positioning composition (width, margins). */
38
+ style?: StyleProp<ViewStyle>;
39
+ }
40
+
41
+ type Layout = "horizontal" | "vertical" | "progress";
42
+
43
+ // First match wins when more than one layout flag is passed.
44
+ function layoutOf(p: StepperProps): Layout {
45
+ if (p.progress) return "progress";
46
+ if (p.vertical) return "vertical";
47
+ return "horizontal";
48
+ }
49
+
50
+ // Each step's visual state derives from its index relative to `current`.
51
+ function stateOf(index: number, current: number): State {
52
+ if (index < current) return "completed";
53
+ if (index === current) return "current";
54
+ return "upcoming";
55
+ }
56
+
57
+ /** Build a Stepper component from a platform skin. */
58
+ export function createStepper(skin: StepperSkin) {
59
+ // The numbered/check disc. Pressable (and so opacity-dim / ripple) only when an
60
+ // onStepPress handler is supplied; otherwise a plain View.
61
+ function Circle({ index, state, onPress }: { index: number; state: State; onPress?: () => void }) {
62
+ const { tokens } = useTheme();
63
+ const glyph = (
64
+ <Text style={[s.glyphBase, skin.glyphState(tokens, state)]}>
65
+ {state === "completed" ? "✓" : String(index + 1)}
66
+ </Text>
67
+ );
68
+ if (onPress) {
69
+ const ripple = skin.ripple ? skin.ripple(tokens, state) : undefined;
70
+ return (
71
+ <Pressable
72
+ style={({ pressed }) => [
73
+ s.circleBase,
74
+ skin.circleState(tokens, state),
75
+ skin.pressedOpacity != null && pressed ? { opacity: skin.pressedOpacity } : null,
76
+ ]}
77
+ android_ripple={ripple}
78
+ onPress={onPress}
79
+ accessibilityRole="button"
80
+ >
81
+ {glyph}
82
+ </Pressable>
83
+ );
84
+ }
85
+ return <View style={[s.circleBase, skin.circleState(tokens, state)]}>{glyph}</View>;
86
+ }
87
+
88
+ return function Stepper(props: StepperProps) {
89
+ const { steps, current, value, label, onStepPress, style } = props;
90
+ const { tokens } = useTheme();
91
+ const layout = layoutOf(props);
92
+
93
+ if (layout === "progress") {
94
+ const pct = Math.max(0, Math.min(100, Math.round(value ?? 0)));
95
+ return (
96
+ <View style={[s.fullWidth, style]}>
97
+ <View style={s.progressHeader}>
98
+ <Text style={skin.progressCaption(tokens)}>{label ?? "Setup progress"}</Text>
99
+ <Text style={skin.progressPercent(tokens)}>{pct}%</Text>
100
+ </View>
101
+ <View style={skin.progressTrack(tokens)}>
102
+ <View style={[skin.progressFill(tokens), { width: `${pct}%` as DimensionValue }]} />
103
+ </View>
104
+ </View>
105
+ );
106
+ }
107
+
108
+ if (layout === "vertical") {
109
+ return (
110
+ <View style={[s.fullWidth, style]}>
111
+ {steps.map((step, i) => {
112
+ const state = stateOf(i, current);
113
+ const isLast = i === steps.length - 1;
114
+ return (
115
+ <View key={i} style={s.verticalRow}>
116
+ <View style={s.verticalRail}>
117
+ <Circle index={i} state={state} onPress={onStepPress ? () => onStepPress(i) : undefined} />
118
+ {!isLast ? (
119
+ <View style={[s.verticalConnector, skin.connector(tokens, state === "completed")]} />
120
+ ) : null}
121
+ </View>
122
+ <View style={[s.flex1, !isLast ? s.verticalContentSpacing : null]}>
123
+ <Text style={[s.labelBase, skin.labelState(tokens, state)]}>{step.label}</Text>
124
+ {step.description != null ? (
125
+ <Text style={skin.verticalDescription(tokens)}>{step.description}</Text>
126
+ ) : null}
127
+ </View>
128
+ </View>
129
+ );
130
+ })}
131
+ </View>
132
+ );
133
+ }
134
+
135
+ // Horizontal: a row of circle + label columns, joined by flex-filling rules.
136
+ return (
137
+ <View style={[s.horizontalRow, style]}>
138
+ {steps.map((step, i) => {
139
+ const state = stateOf(i, current);
140
+ const isLast = i === steps.length - 1;
141
+ return (
142
+ <View key={i} style={[s.horizontalRow, !isLast ? s.flex1 : null]}>
143
+ <View style={s.horizontalColumn}>
144
+ <Circle index={i} state={state} onPress={onStepPress ? () => onStepPress(i) : undefined} />
145
+ <Text style={[s.labelBaseXs, skin.labelState(tokens, state)]}>{step.label}</Text>
146
+ </View>
147
+ {!isLast ? (
148
+ // The connector after a step is "filled" once that step is
149
+ // completed (i.e. the next step has been reached).
150
+ <View style={[s.horizontalConnector, skin.connector(tokens, i < current)]} />
151
+ ) : null}
152
+ </View>
153
+ );
154
+ })}
155
+ </View>
156
+ );
157
+ };
158
+ }
@@ -0,0 +1,280 @@
1
+ import { type ViewStyle, type TextStyle } from "react-native";
2
+ import { type ColorTokens, alpha } from "../../style/index.js";
3
+
4
+ // Co-located Stepper skins, one per platform. The Stepper is a LIGHT treatment:
5
+ // the same multi-step progress STRUCTURE on every platform (numbered/check
6
+ // circles joined by connectors, plus the vertical and progress-bar layouts),
7
+ // with only small native touches per OS. The BRAND survives everywhere (the
8
+ // indigo `primary` token and the semantic tokens, never a platform default);
9
+ // only the indicator shape, the connector cap, the current-step emphasis, and
10
+ // the press feedback change per OS:
11
+ // iOS (HIG): circular step indicators with ROUNDED connector caps; completed =
12
+ // filled `primary`; current = a `primary` ring on a transparent fill (a
13
+ // subtle inner ring via a 1.5px `ring`-token halo); press = opacity dim.
14
+ // Android (M3): step indicators with FLAT (square-capped) connectors; completed
15
+ // = filled `primary`; current = `primary` ring; press = android_ripple on
16
+ // interactive step circles.
17
+ // Web: the established Canvas look (lifted verbatim from the original file).
18
+ //
19
+ // The State axis (completed / current / upcoming) maps each colored part to a
20
+ // token set. Layout-only fragments are shared static objects (below); anything
21
+ // reading a color is a function of the active tokens.
22
+
23
+ export type State = "completed" | "current" | "upcoming";
24
+
25
+ // =============================================================================
26
+ // Shared layout fragments (color-free; identical across platforms).
27
+ // =============================================================================
28
+
29
+ // flex-1 (grow + shrink, zero basis) shared by connectors and the columns that
30
+ // fill the remaining row width.
31
+ export const flex1: ViewStyle = { flexGrow: 1, flexShrink: 1, flexBasis: "0%" };
32
+
33
+ // h-8 w-8 shrink-0 flex-row items-center justify-center rounded-full border-2
34
+ export const circleBase: ViewStyle = {
35
+ height: 32,
36
+ width: 32,
37
+ flexShrink: 0,
38
+ flexDirection: "row",
39
+ alignItems: "center",
40
+ justifyContent: "center",
41
+ borderRadius: 9999,
42
+ borderWidth: 2,
43
+ };
44
+
45
+ // text-sm font-medium (the check / number inside the circle).
46
+ export const glyphBase: TextStyle = { fontSize: 14, lineHeight: 20, fontWeight: "500" };
47
+
48
+ // text-sm font-medium (vertical layout label).
49
+ export const labelBase: TextStyle = { fontSize: 14, lineHeight: 20, fontWeight: "500" };
50
+
51
+ // text-xs font-medium (horizontal layout label), colored by labelState.
52
+ export const labelBaseXs: TextStyle = { fontSize: 12, lineHeight: 16, fontWeight: "500" };
53
+
54
+ // w-full
55
+ export const fullWidth: ViewStyle = { width: "100%" };
56
+
57
+ // mb-1.5 flex-row items-center justify-between
58
+ export const progressHeader: ViewStyle = {
59
+ marginBottom: 6,
60
+ flexDirection: "row",
61
+ alignItems: "center",
62
+ justifyContent: "space-between",
63
+ };
64
+
65
+ // flex-row gap-3 (the row pairing the rail with the content column).
66
+ export const verticalRow: ViewStyle = { flexDirection: "row", gap: 12 };
67
+
68
+ // items-center (the left rail: circle stacked over its connector).
69
+ export const verticalRail: ViewStyle = { alignItems: "center" };
70
+
71
+ // my-1 w-px flex-1 (the vertical connector) — color/cap come from the skin.
72
+ export const verticalConnector: ViewStyle = {
73
+ marginVertical: 4,
74
+ width: 1,
75
+ flexGrow: 1,
76
+ flexShrink: 1,
77
+ flexBasis: "0%",
78
+ };
79
+
80
+ // pb-6 (bottom inset on a content column that has a step after it).
81
+ export const verticalContentSpacing: ViewStyle = { paddingBottom: 24 };
82
+
83
+ // flex-row items-start (the outer row and each step item).
84
+ export const horizontalRow: ViewStyle = { flexDirection: "row", alignItems: "flex-start" };
85
+
86
+ // items-center gap-1.5 (the circle + label column).
87
+ export const horizontalColumn: ViewStyle = { alignItems: "center", gap: 6 };
88
+
89
+ // mx-2 mt-4 h-px flex-1 (the horizontal connector) — color/cap come from the skin.
90
+ export const horizontalConnector: ViewStyle = {
91
+ marginHorizontal: 8,
92
+ marginTop: 16,
93
+ height: 1,
94
+ flexGrow: 1,
95
+ flexShrink: 1,
96
+ flexBasis: "0%",
97
+ };
98
+
99
+ // =============================================================================
100
+ // The skin contract: only the colored, platform-varying parts.
101
+ // =============================================================================
102
+
103
+ export interface StepperSkin {
104
+ /** Fill + border of the numbered/check circle for a given step state. */
105
+ circleState: (t: ColorTokens, state: State) => ViewStyle;
106
+ /** Color of the glyph (check / number) inside the circle. */
107
+ glyphState: (t: ColorTokens, state: State) => TextStyle;
108
+ /** Color of the step label for a given state (shared across platforms). */
109
+ labelState: (t: ColorTokens, state: State) => TextStyle;
110
+ /** Background + line cap of a connector; `done` fills it primary. */
111
+ connector: (t: ColorTokens, done: boolean) => ViewStyle;
112
+ /** Caption left of the percentage in the progress layout. */
113
+ progressCaption: (t: ColorTokens) => TextStyle;
114
+ /** The percentage label in the progress layout. */
115
+ progressPercent: (t: ColorTokens) => TextStyle;
116
+ /** The progress track (unfilled). */
117
+ progressTrack: (t: ColorTokens) => ViewStyle;
118
+ /** The filled portion of the progress track (width set inline as %). */
119
+ progressFill: (t: ColorTokens) => ViewStyle;
120
+ /** The optional description line in the vertical layout. */
121
+ verticalDescription: (t: ColorTokens) => TextStyle;
122
+ /** iOS/web dim the circle on press; Android uses a ripple (null here). */
123
+ pressedOpacity: number | null;
124
+ /** Android ripple over a pressable circle; null on iOS/web. */
125
+ ripple: ((t: ColorTokens, state: State) => { color: string; borderless: boolean }) | null;
126
+ }
127
+
128
+ // --- shared brand mapping (label colors are identical across platforms) ------
129
+
130
+ // Upcoming steps drop to the muted foreground; completed/current stay foreground.
131
+ function labelState(t: ColorTokens, state: State): TextStyle {
132
+ switch (state) {
133
+ case "completed": return { color: t.foreground };
134
+ case "current": return { color: t.foreground };
135
+ case "upcoming": return { color: t["muted-foreground"] };
136
+ }
137
+ }
138
+
139
+ // =============================================================================
140
+ // Web: the established Canvas look (lifted verbatim from the original file).
141
+ // =============================================================================
142
+
143
+ export const webSkin: StepperSkin = {
144
+ // Completed fills primary; current is outlined in primary on a transparent fill;
145
+ // upcoming sits on the muted token behind a border-toned ring.
146
+ circleState(t, state) {
147
+ switch (state) {
148
+ case "completed": return { borderColor: t.primary, backgroundColor: t.primary };
149
+ case "current": return { borderColor: t.primary, backgroundColor: "transparent" };
150
+ case "upcoming": return { borderColor: t.border, backgroundColor: t.muted };
151
+ }
152
+ },
153
+ glyphState(t, state) {
154
+ switch (state) {
155
+ case "completed": return { color: t["primary-foreground"] };
156
+ case "current": return { color: t.primary };
157
+ case "upcoming": return { color: t["muted-foreground"] };
158
+ }
159
+ },
160
+ labelState,
161
+ // A connector fills primary once the step it leads out of is completed, else it
162
+ // tracks the border token. Square caps (web rules are plain rects).
163
+ connector(t, done) {
164
+ return { backgroundColor: done ? t.primary : t.border };
165
+ },
166
+ progressCaption(t) {
167
+ return { fontSize: 12, lineHeight: 16, fontWeight: "500", color: t.foreground };
168
+ },
169
+ progressPercent(t) {
170
+ return { fontSize: 12, lineHeight: 16, color: t["muted-foreground"] };
171
+ },
172
+ progressTrack(t) {
173
+ return { height: 6, overflow: "hidden", borderRadius: 9999, backgroundColor: t.muted };
174
+ },
175
+ progressFill(t) {
176
+ return { height: "100%", borderRadius: 9999, backgroundColor: t.primary };
177
+ },
178
+ verticalDescription(t) {
179
+ return { fontSize: 12, lineHeight: 16, color: t["muted-foreground"] };
180
+ },
181
+ pressedOpacity: 0.9,
182
+ ripple: null,
183
+ };
184
+
185
+ // =============================================================================
186
+ // iOS (HIG): circular indicators, rounded connector caps, primary ring current.
187
+ // =============================================================================
188
+
189
+ export const iosSkin: StepperSkin = {
190
+ circleState(t, state) {
191
+ switch (state) {
192
+ // Completed reads as a solid filled `primary` disc.
193
+ case "completed": return { borderColor: t.primary, backgroundColor: t.primary };
194
+ // Current = a clean `primary` ring on a transparent fill (no muted backing,
195
+ // so it reads as an open ring the way iOS step dots do).
196
+ case "current": return { borderColor: t.primary, backgroundColor: "transparent" };
197
+ // Upcoming = a soft muted disc behind a border-toned ring.
198
+ case "upcoming": return { borderColor: t.border, backgroundColor: t.muted };
199
+ }
200
+ },
201
+ glyphState(t, state) {
202
+ switch (state) {
203
+ case "completed": return { color: t["primary-foreground"] };
204
+ case "current": return { color: t.primary };
205
+ case "upcoming": return { color: t["muted-foreground"] };
206
+ }
207
+ },
208
+ labelState,
209
+ // iOS connectors get rounded caps so the rule reads as a soft rounded line.
210
+ connector(t, done) {
211
+ return { backgroundColor: done ? t.primary : t.border, borderRadius: 9999 };
212
+ },
213
+ progressCaption(t) {
214
+ return { fontSize: 13, lineHeight: 18, fontWeight: "600", color: t.foreground };
215
+ },
216
+ progressPercent(t) {
217
+ return { fontSize: 13, lineHeight: 18, color: t["muted-foreground"] };
218
+ },
219
+ progressTrack(t) {
220
+ return { height: 6, overflow: "hidden", borderRadius: 9999, backgroundColor: t.muted };
221
+ },
222
+ progressFill(t) {
223
+ return { height: "100%", borderRadius: 9999, backgroundColor: t.primary };
224
+ },
225
+ verticalDescription(t) {
226
+ return { fontSize: 13, lineHeight: 18, color: t["muted-foreground"] };
227
+ },
228
+ // HIG press = opacity dim.
229
+ pressedOpacity: 0.8,
230
+ ripple: null,
231
+ };
232
+
233
+ // =============================================================================
234
+ // Android (M3): flat connector caps, filled-primary complete, primary ring current.
235
+ // =============================================================================
236
+
237
+ export const androidSkin: StepperSkin = {
238
+ circleState(t, state) {
239
+ switch (state) {
240
+ // Completed = filled primary.
241
+ case "completed": return { borderColor: t.primary, backgroundColor: t.primary };
242
+ // Current = a primary ring; M3 keeps a faint tonal backing so the active
243
+ // indicator reads against the surface (secondaryContainer ≈ alpha(primary,.12)).
244
+ case "current": return { borderColor: t.primary, backgroundColor: alpha(t.primary, 0.12) };
245
+ // Upcoming = a muted disc behind a border-toned ring.
246
+ case "upcoming": return { borderColor: t.border, backgroundColor: t.muted };
247
+ }
248
+ },
249
+ glyphState(t, state) {
250
+ switch (state) {
251
+ case "completed": return { color: t["primary-foreground"] };
252
+ case "current": return { color: t.primary };
253
+ case "upcoming": return { color: t["muted-foreground"] };
254
+ }
255
+ },
256
+ labelState,
257
+ // M3 connectors are flat (square caps) — explicit radius 0 so the rule reads
258
+ // as a crisp Material divider, not a rounded line.
259
+ connector(t, done) {
260
+ return { backgroundColor: done ? t.primary : t.border, borderRadius: 0 };
261
+ },
262
+ progressCaption(t) {
263
+ return { fontSize: 12, lineHeight: 16, fontWeight: "500", color: t.foreground };
264
+ },
265
+ progressPercent(t) {
266
+ return { fontSize: 12, lineHeight: 16, color: t["muted-foreground"] };
267
+ },
268
+ progressTrack(t) {
269
+ return { height: 4, overflow: "hidden", borderRadius: 9999, backgroundColor: t.muted };
270
+ },
271
+ progressFill(t) {
272
+ return { height: "100%", borderRadius: 9999, backgroundColor: t.primary };
273
+ },
274
+ verticalDescription(t) {
275
+ return { fontSize: 12, lineHeight: 16, color: t["muted-foreground"] };
276
+ },
277
+ // M3 press = android_ripple over the interactive circle.
278
+ pressedOpacity: null,
279
+ ripple: (t) => ({ color: alpha(t.primary, 0.12), borderless: false }),
280
+ };
@@ -0,0 +1,6 @@
1
+ import { createStepper } from "./stepper.shared.js";
2
+ import { webSkin } from "./stepper.styles.js";
3
+
4
+ // Web Stepper (the base; Metro falls back to it on native, web bundlers resolve it).
5
+ export const Stepper = createStepper(webSkin);
6
+ export type { Step, StepperProps } from "./stepper.shared.js";
@@ -0,0 +1,6 @@
1
+ import { createTabs } from "./tabs.shared.js";
2
+ import { androidSkin } from "./tabs.styles.js";
3
+
4
+ // Material 3 (underline tabs) Tabs. Metro resolves this file on Android; the docs import it for preview.
5
+ export const Tabs = createTabs(androidSkin);
6
+ export type { TabsProps, TabItem } from "./tabs.shared.js";
@@ -0,0 +1,6 @@
1
+ import { createTabs } from "./tabs.shared.js";
2
+ import { iosSkin } from "./tabs.styles.js";
3
+
4
+ // iOS (HIG segmented tab strip) Tabs. Metro resolves this file on iOS; the docs import it for preview.
5
+ export const Tabs = createTabs(iosSkin);
6
+ export type { TabsProps, TabItem } from "./tabs.shared.js";
@@ -0,0 +1,127 @@
1
+ # Tabs
2
+
3
+ Underline, pill, vertical, with badges.
4
+
5
+ ## Usage
6
+
7
+ ```tsx
8
+ <Tabs
9
+ tabs={[
10
+ "General",
11
+ "Security",
12
+ "Notifications",
13
+ "Billing",
14
+ "Integrations"
15
+ ]}
16
+ active={0}
17
+ />
18
+ ```
19
+
20
+ ## Variants
21
+
22
+ ### Variant - pill
23
+
24
+ ```tsx
25
+ <Tabs pills tabs={["All", "Active", "Archived", "Deleted"]} active={0} />
26
+ ```
27
+
28
+ ### Variant - vertical
29
+
30
+ ```tsx
31
+ <Tabs
32
+ vertical
33
+ tabs={["General", "Security", "Notifications", "API Keys", "Billing"]}
34
+ active={0}
35
+ />
36
+ ```
37
+
38
+ ### Badge counts
39
+
40
+ ```tsx
41
+ <Tabs
42
+ tabs={[
43
+ { label: "All", badge: "142" },
44
+ { label: "Active", badge: "89" },
45
+ { label: "Pending", badge: "12" },
46
+ { label: "Archived", badge: "53" }
47
+ ]}
48
+ active={0}
49
+ />
50
+ ```
51
+
52
+ ## Do & Don't
53
+
54
+ ### Underline
55
+
56
+ **Do** — Underline and foreground-color only the active tab; leave the rest muted with no rule.
57
+
58
+ ```tsx
59
+ <Tabs tabs={["Overview", "Activity", "Settings"]} active={0} />
60
+ ```
61
+
62
+ **Don't** — Underlining every tab erases the active indicator: there is no way to tell which view is current.
63
+
64
+ ```tsx
65
+ <View style={{ flexDirection: "row", alignItems: "center", borderBottomWidth: 1, borderColor: tokens.border, alignSelf: "flex-start" }}>
66
+ <Pressable style={({ pressed }) => [{ flexDirection: "row", alignItems: "center", justifyContent: "center", paddingHorizontal: 16, paddingVertical: 10 }, pressed ? { opacity: 0.9 } : null]}>
67
+ <Text style={{ fontSize: 14, lineHeight: 20, fontWeight: "500", color: tokens.foreground }}>Overview</Text>
68
+ <View style={{ position: "absolute", bottom: 0, left: 0, right: 0, height: 2, borderRadius: 9999, backgroundColor: tokens.primary }} />
69
+ </Pressable>
70
+ <Pressable style={({ pressed }) => [{ flexDirection: "row", alignItems: "center", justifyContent: "center", paddingHorizontal: 16, paddingVertical: 10 }, pressed ? { opacity: 0.9 } : null]}>
71
+ <Text style={{ fontSize: 14, lineHeight: 20, fontWeight: "500", color: tokens.foreground }}>Activity</Text>
72
+ <View style={{ position: "absolute", bottom: 0, left: 0, right: 0, height: 2, borderRadius: 9999, backgroundColor: tokens.primary }} />
73
+ </Pressable>
74
+ <Pressable style={({ pressed }) => [{ flexDirection: "row", alignItems: "center", justifyContent: "center", paddingHorizontal: 16, paddingVertical: 10 }, pressed ? { opacity: 0.9 } : null]}>
75
+ <Text style={{ fontSize: 14, lineHeight: 20, fontWeight: "500", color: tokens.foreground }}>Settings</Text>
76
+ <View style={{ position: "absolute", bottom: 0, left: 0, right: 0, height: 2, borderRadius: 9999, backgroundColor: tokens.primary }} />
77
+ </Pressable>
78
+ </View>
79
+ ```
80
+
81
+ ### Pill
82
+
83
+ **Do** — Exactly one pill gets the elevated background; the rest sit flat on the muted track.
84
+
85
+ ```tsx
86
+ <Tabs tabs={["All", "Active", "Archived"]} active={0} pills />
87
+ ```
88
+
89
+ **Don't** — Giving every pill the raised background makes the group read as three buttons, not one selection.
90
+
91
+ ```tsx
92
+ <View style={{ flexDirection: "row", alignItems: "center", gap: 4, alignSelf: "flex-start", borderRadius: 8, backgroundColor: tokens.muted, padding: 4 }}>
93
+ <Pressable style={({ pressed }) => [{ flexDirection: "row", alignItems: "center", justifyContent: "center", borderRadius: 6, backgroundColor: tokens.background, ...shadow("sm"), paddingHorizontal: 12, paddingVertical: 6 }, pressed ? { opacity: 0.9 } : null]}>
94
+ <Text style={{ fontSize: 14, lineHeight: 20, fontWeight: "500", color: tokens.foreground }}>All</Text>
95
+ </Pressable>
96
+ <Pressable style={({ pressed }) => [{ flexDirection: "row", alignItems: "center", justifyContent: "center", borderRadius: 6, backgroundColor: tokens.background, ...shadow("sm"), paddingHorizontal: 12, paddingVertical: 6 }, pressed ? { opacity: 0.9 } : null]}>
97
+ <Text style={{ fontSize: 14, lineHeight: 20, fontWeight: "500", color: tokens.foreground }}>Active</Text>
98
+ </Pressable>
99
+ <Pressable style={({ pressed }) => [{ flexDirection: "row", alignItems: "center", justifyContent: "center", borderRadius: 6, backgroundColor: tokens.background, ...shadow("sm"), paddingHorizontal: 12, paddingVertical: 6 }, pressed ? { opacity: 0.9 } : null]}>
100
+ <Text style={{ fontSize: 14, lineHeight: 20, fontWeight: "500", color: tokens.foreground }}>Archived</Text>
101
+ </Pressable>
102
+ </View>
103
+ ```
104
+
105
+ ### Vertical
106
+
107
+ **Do** — Fill the active rail item with the accent background so the selected pane is unmistakable.
108
+
109
+ ```tsx
110
+ <Tabs tabs={["General", "Security", "Notifications"]} active={0} vertical />
111
+ ```
112
+
113
+ **Don't** — With no filled active item the rail collapses into a plain link list and loses its current selection.
114
+
115
+ ```tsx
116
+ <View style={{ flexDirection: "column", alignItems: "stretch", gap: 4, width: 180 }}>
117
+ <Pressable style={({ pressed }) => [{ width: "100%", flexDirection: "row", alignItems: "center", borderRadius: 6, backgroundColor: "transparent", paddingHorizontal: 12, paddingVertical: 8 }, pressed ? { opacity: 0.9 } : null]}>
118
+ <Text style={{ fontSize: 14, lineHeight: 20, fontWeight: "500", color: tokens["muted-foreground"] }}>General</Text>
119
+ </Pressable>
120
+ <Pressable style={({ pressed }) => [{ width: "100%", flexDirection: "row", alignItems: "center", borderRadius: 6, backgroundColor: "transparent", paddingHorizontal: 12, paddingVertical: 8 }, pressed ? { opacity: 0.9 } : null]}>
121
+ <Text style={{ fontSize: 14, lineHeight: 20, fontWeight: "500", color: tokens["muted-foreground"] }}>Security</Text>
122
+ </Pressable>
123
+ <Pressable style={({ pressed }) => [{ width: "100%", flexDirection: "row", alignItems: "center", borderRadius: 6, backgroundColor: "transparent", paddingHorizontal: 12, paddingVertical: 8 }, pressed ? { opacity: 0.9 } : null]}>
124
+ <Text style={{ fontSize: 14, lineHeight: 20, fontWeight: "500", color: tokens["muted-foreground"] }}>Notifications</Text>
125
+ </Pressable>
126
+ </View>
127
+ ```