@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,167 @@
1
+ import { type GestureResponderEvent } from "react-native";
2
+ import { View, Pressable, Text, useTheme, type ColorTokens, type StyleProp, type ViewStyle, type TextStyle } from "../../style/index.js";
3
+ import { Badge } from "../../atoms/badge/badge.js";
4
+ import { type Density, type Frame } from "./sidebar.styles.js";
5
+
6
+ // Shared Sidebar shell. The structure (the outer column, the titled sections, the
7
+ // nav rows with their leading icon / label / trailing badge, the single active
8
+ // highlight), the section normalization, the flat-index active matching, the
9
+ // density/frame precedence, the accessibility, and the select handler live here
10
+ // once; a platform file supplies only its skin (the row shape, the selected-row
11
+ // highlight + label/icon color, the section heading, the frame, the press
12
+ // feedback) and calls createSidebar.
13
+ //
14
+ // A sidebar is the vertical app-navigation panel that runs down the left of a
15
+ // layout: an optional list of titled sections, each holding nav rows. A row is a
16
+ // leading icon glyph + label, with at most one row carrying the active highlight
17
+ // and the rest sitting inactive. Rows may carry a trailing count <Badge>.
18
+ //
19
+ // Boolean-prop API: one boolean per option, grouped by axis, first-match
20
+ // precedence within an axis (mirrors Button's intentOf).
21
+ //
22
+ // - Density axis (pick one; default is the comfortable row): `compact` tightens
23
+ // each row's padding and drops the type a step for dense navigation.
24
+ // - Frame axis (pick one; default is the flush, right-bordered column that docks
25
+ // against the page): `bordered`/`floating` lift the panel into a fully bordered,
26
+ // rounded card surface. `bordered` wins over `floating` when both are passed.
27
+
28
+ // The platform-varying surface. Everything color/shape-bearing the rows, the
29
+ // frame, and the section heading need lives here, built from the active tokens
30
+ // (so each follows light/dark and the glass surface).
31
+ export interface SidebarSkin {
32
+ /** Web paints the accent fill on a pressed (non-active) row; iOS/Android don't. */
33
+ pressedFill: boolean;
34
+ /** iOS dims the row on press; web/Android don't (null). */
35
+ pressedOpacity: number | null;
36
+ /** Android ripple over a pressed row; null on iOS/web. */
37
+ ripple: ((t: ColorTokens) => { color: string; borderless: boolean }) | null;
38
+
39
+ /** The outer navigation column, per frame. */
40
+ column: (t: ColorTokens, frame: Frame) => ViewStyle;
41
+ /** A titled group of nav rows. */
42
+ group: ViewStyle;
43
+ /** The section heading above a group. */
44
+ sectionTitle: (t: ColorTokens) => TextStyle;
45
+ /** The nav-row container (shape + density padding). */
46
+ row: (t: ColorTokens, density: Density) => ViewStyle;
47
+ /** The selected-row highlight fill (null when not active). */
48
+ rowFill: (t: ColorTokens, active: boolean) => ViewStyle | null;
49
+ /** The row label (flex, type, color), per active state + density. */
50
+ label: (t: ColorTokens, active: boolean, density: Density) => TextStyle;
51
+ /** The leading icon glyph color/size, per active state. */
52
+ icon: (t: ColorTokens, active: boolean) => TextStyle;
53
+ }
54
+
55
+ /** One nav row: a label, an optional leading icon glyph, an optional count. */
56
+ export interface SidebarItem {
57
+ /** Row label (e.g. "Dashboard"). */
58
+ label: string;
59
+ /** Leading icon glyph rendered before the label (e.g. an emoji or symbol). */
60
+ icon?: string;
61
+ /** Trailing count rendered as a <Badge> (e.g. "12"). */
62
+ badge?: string;
63
+ }
64
+
65
+ /** A titled group of nav rows. */
66
+ export interface SidebarSection {
67
+ /** Optional muted heading shown above the group. */
68
+ title?: string;
69
+ /** Rows in this group. */
70
+ items: SidebarItem[];
71
+ }
72
+
73
+ export interface SidebarProps {
74
+ /** Titled sections of nav rows. Use this or the flat `items` array. */
75
+ sections?: SidebarSection[];
76
+ /** Flat list of nav rows, wrapped into a single untitled section. */
77
+ items?: SidebarItem[];
78
+ /** The active row, by label or by flat index across all rows. */
79
+ active?: string | number;
80
+ /** Fired with the selected row and its flat index across all sections. */
81
+ onSelect?: (item: SidebarItem, index: number, event: GestureResponderEvent) => void;
82
+ // Density (pick one; default is the comfortable row).
83
+ compact?: boolean;
84
+ // Frame (pick one; default is the flush right-bordered column).
85
+ bordered?: boolean;
86
+ floating?: boolean;
87
+ /** Escape hatch for layout/positioning composition (mainly width). */
88
+ style?: StyleProp<ViewStyle>;
89
+ }
90
+
91
+ // Density precedence when more than one is passed: first match wins.
92
+ function densityOf(p: SidebarProps): Density {
93
+ if (p.compact) return "compact";
94
+ return "default";
95
+ }
96
+
97
+ // Frame precedence when more than one is passed: first match wins.
98
+ function frameOf(p: SidebarProps): Frame {
99
+ if (p.bordered) return "bordered";
100
+ if (p.floating) return "bordered";
101
+ return "flush";
102
+ }
103
+
104
+ /** Build a Sidebar component from a platform skin. */
105
+ export function createSidebar(skin: SidebarSkin) {
106
+ return function Sidebar(props: SidebarProps) {
107
+ const { sections, items, active, onSelect, style } = props;
108
+ const density = densityOf(props);
109
+ const frame = frameOf(props);
110
+ const { tokens } = useTheme();
111
+
112
+ // Normalize to a sections list; a flat `items` array becomes one untitled
113
+ // section. Sections always win when both are supplied.
114
+ const groups: SidebarSection[] = sections ?? (items ? [{ items }] : []);
115
+
116
+ // Active match is by label or by flat index across every row in order.
117
+ const isActive = (item: SidebarItem, flatIndex: number): boolean => {
118
+ if (active == null) return false;
119
+ if (typeof active === "number") return active === flatIndex;
120
+ return active === item.label;
121
+ };
122
+
123
+ // Running flat index so label/index matching and onSelect agree across groups.
124
+ let flat = -1;
125
+
126
+ return (
127
+ <View style={[skin.column(tokens, frame), style]}>
128
+ {groups.map((section, gi) => (
129
+ <View key={gi} style={skin.group}>
130
+ {section.title != null ? (
131
+ <Text style={skin.sectionTitle(tokens)}>{section.title}</Text>
132
+ ) : null}
133
+ {section.items.map((item) => {
134
+ flat += 1;
135
+ const index = flat;
136
+ const activeRow = isActive(item, index);
137
+ return (
138
+ <Pressable
139
+ key={index}
140
+ android_ripple={skin.ripple ? skin.ripple(tokens) : undefined}
141
+ style={({ pressed }) => [
142
+ skin.row(tokens, density),
143
+ // The active row carries its highlight persistently; on web a
144
+ // press paints it too (the old `active:bg-accent`).
145
+ skin.rowFill(tokens, activeRow || (skin.pressedFill && pressed)),
146
+ skin.pressedOpacity != null && pressed ? { opacity: skin.pressedOpacity } : null,
147
+ ]}
148
+ onPress={(event) => onSelect?.(item, index, event)}
149
+ accessibilityRole="button"
150
+ accessibilityState={{ selected: activeRow }}
151
+ >
152
+ {item.icon != null ? (
153
+ <Text style={skin.icon(tokens, activeRow)}>{item.icon}</Text>
154
+ ) : null}
155
+ <Text style={skin.label(tokens, activeRow, density)} numberOfLines={1}>
156
+ {item.label}
157
+ </Text>
158
+ {item.badge != null ? <Badge secondary>{item.badge}</Badge> : null}
159
+ </Pressable>
160
+ );
161
+ })}
162
+ </View>
163
+ ))}
164
+ </View>
165
+ );
166
+ };
167
+ }
@@ -0,0 +1,262 @@
1
+ import { type ViewStyle, type TextStyle } from "react-native";
2
+ import { type ColorTokens, alpha } from "../../style/index.js";
3
+ import { type SidebarSkin } from "./sidebar.shared.js";
4
+
5
+ // Co-located Sidebar skins, one per platform. The shell resolves the density and
6
+ // frame axes, the section grouping, the flat-index active matching, the badges,
7
+ // and the select handler; the skin supplies only the native SHAPE of the nav row
8
+ // (its radius and height), the selected-row highlight (fill + label/icon color),
9
+ // the section heading, the frame (outer column), and the press feedback. The
10
+ // BRAND survives on every platform (the indigo `primary`/`accent` tokens, never a
11
+ // platform default), so each follows light/dark and the glass surface.
12
+ //
13
+ // iOS 27 (Liquid Glass) sidebar: grouped rows; the SELECTED row is a CAPSULE
14
+ // highlight (radius 9999) filled with the light-gray `accent`, label in
15
+ // `accent-foreground`, while the leading icon stays TINTED in `primary`;
16
+ // section headers ~13pt 600 muted; ~36pt rows; press = opacity dim (0.8).
17
+ // Android (M3 navigation drawer): each nav item is a fully-rounded PILL (radius
18
+ // 9999); the ACTIVE item is a tonal `alpha(primary, 0.12)` pill with the icon
19
+ // + label in brand `primary`; inactive transparent with `foreground`; ~56dp
20
+ // item height; press = android_ripple.
21
+ // Web: the established Canvas look (row radius 6, `accent` fill on the active OR
22
+ // pressed row, foreground/muted label + icon), lifted verbatim from the
23
+ // original file.
24
+
25
+ export type Density = "default" | "compact";
26
+ export type Frame = "flush" | "bordered";
27
+
28
+ // =============================================================================
29
+ // Web: the established Canvas look (lifted verbatim from the original file).
30
+ // =============================================================================
31
+
32
+ export const webSkin: SidebarSkin = {
33
+ // Web carries no row ripple; the active-row affordance is the accent fill, also
34
+ // applied while a row is pressed (the old `active:bg-accent`).
35
+ pressedFill: true,
36
+ pressedOpacity: null,
37
+ ripple: null,
38
+
39
+ // Fixed-width column with the standard inset (gap-4 p-2) on the background fill.
40
+ // The flush frame docks against the page with a single right hairline; the
41
+ // bordered/floating frame lifts it into a fully ruled, rounded, clipped card.
42
+ column(tokens, frame) {
43
+ const base: ViewStyle = {
44
+ width: 240,
45
+ backgroundColor: tokens.background,
46
+ gap: 16,
47
+ padding: 8,
48
+ };
49
+ if (frame === "bordered") {
50
+ return { ...base, borderRadius: 8, borderWidth: 1, borderColor: tokens.border, overflow: "hidden" };
51
+ }
52
+ return { ...base, borderRightWidth: 1, borderColor: tokens.border };
53
+ },
54
+
55
+ // A titled group of nav rows.
56
+ group: { gap: 4 },
57
+
58
+ // The uppercase, muted, wide-tracked section heading above a group.
59
+ sectionTitle(tokens) {
60
+ return {
61
+ paddingHorizontal: 12,
62
+ paddingVertical: 4,
63
+ fontSize: 12,
64
+ lineHeight: 16,
65
+ fontWeight: "500",
66
+ textTransform: "uppercase",
67
+ letterSpacing: 0.8,
68
+ color: tokens["muted-foreground"],
69
+ };
70
+ },
71
+
72
+ // Row container layout per density: a centered icon/label/badge row with the
73
+ // medium pill radius. The comfortable row matches the documented px-3 py-2;
74
+ // compact tightens the vertical inset for dense navigation.
75
+ row(_tokens, density) {
76
+ return {
77
+ flexDirection: "row",
78
+ alignItems: "center",
79
+ gap: 12,
80
+ borderRadius: 6,
81
+ paddingHorizontal: 12,
82
+ paddingVertical: density === "compact" ? 6 : 8,
83
+ };
84
+ },
85
+
86
+ // The accent fill carried by the active row (and the pressed affordance).
87
+ rowFill(tokens, active) {
88
+ return active ? { backgroundColor: tokens.accent } : null;
89
+ },
90
+
91
+ // Label fills the remaining row width (flex-1), truncates to one line, and is
92
+ // medium-weight foreground when active else muted.
93
+ label(tokens, active, density) {
94
+ return {
95
+ flexGrow: 1,
96
+ flexShrink: 1,
97
+ flexBasis: "0%",
98
+ ...(density === "compact" ? { fontSize: 12, lineHeight: 16 } : { fontSize: 14, lineHeight: 20 }),
99
+ ...(active ? { fontWeight: "500", color: tokens.foreground } : { color: tokens["muted-foreground"] }),
100
+ };
101
+ },
102
+
103
+ // The leading icon glyph: text-base, foreground when active else muted.
104
+ icon(tokens, active) {
105
+ return { fontSize: 16, lineHeight: 24, color: active ? tokens.foreground : tokens["muted-foreground"] };
106
+ },
107
+ };
108
+
109
+ // =============================================================================
110
+ // iOS 27 (Liquid Glass) sidebar: grouped rows; the selected row is a CAPSULE
111
+ // highlight (radius 9999) filled with the light-gray `accent`, label in
112
+ // `accent-foreground`, with the leading icon staying TINTED in `primary`. ~36pt
113
+ // rows, ~13pt 600 section headers, press = opacity dim.
114
+ // =============================================================================
115
+
116
+ export const iosSkin: SidebarSkin = {
117
+ pressedFill: false, // iOS dims on press instead of painting the accent fill
118
+ pressedOpacity: 0.8,
119
+ ripple: null,
120
+
121
+ column(tokens, frame) {
122
+ const base: ViewStyle = {
123
+ width: 240,
124
+ backgroundColor: tokens.background,
125
+ gap: 16,
126
+ padding: 8,
127
+ };
128
+ if (frame === "bordered") {
129
+ return { ...base, borderRadius: 10, borderWidth: 1, borderColor: tokens.border, overflow: "hidden" };
130
+ }
131
+ return { ...base, borderRightWidth: 1, borderColor: tokens.border };
132
+ },
133
+
134
+ group: { gap: 2 },
135
+
136
+ // HIG grouped-list header: ~13pt, semibold, secondary/muted, not uppercase.
137
+ sectionTitle(tokens) {
138
+ return {
139
+ paddingHorizontal: 12,
140
+ paddingVertical: 4,
141
+ fontSize: 13,
142
+ lineHeight: 18,
143
+ fontWeight: "600",
144
+ color: tokens["muted-foreground"],
145
+ };
146
+ },
147
+
148
+ // ~36pt row, fully-rounded CAPSULE (radius 9999) to match the iOS 27 highlight.
149
+ row(_tokens, density) {
150
+ return {
151
+ flexDirection: "row",
152
+ alignItems: "center",
153
+ gap: 10,
154
+ borderRadius: 9999,
155
+ paddingHorizontal: 12,
156
+ paddingVertical: density === "compact" ? 6 : 8,
157
+ };
158
+ },
159
+
160
+ // The selected row is a capsule highlight filled with the light-gray `accent`.
161
+ rowFill(tokens, active) {
162
+ return active ? { backgroundColor: tokens.accent } : null;
163
+ },
164
+
165
+ // Active label reads in `accent-foreground` (on the accent highlight); inactive
166
+ // sits in the standard foreground (HIG list rows are not muted when inactive).
167
+ label(tokens, active, density) {
168
+ return {
169
+ flexGrow: 1,
170
+ flexShrink: 1,
171
+ flexBasis: "0%",
172
+ ...(density === "compact" ? { fontSize: 14, lineHeight: 18 } : { fontSize: 15, lineHeight: 20 }),
173
+ fontWeight: active ? "600" : "400",
174
+ color: active ? tokens["accent-foreground"] : tokens.foreground,
175
+ };
176
+ },
177
+
178
+ // Icon glyph: always the brand `primary` tint, active or not. iOS 27 keeps the
179
+ // leading glyph tinted even on the selected capsule (HIG glyphs carry the tint).
180
+ icon(tokens, _active) {
181
+ return { fontSize: 17, lineHeight: 22, color: tokens.primary };
182
+ },
183
+ };
184
+
185
+ // =============================================================================
186
+ // Android (Material 3 navigation drawer): each nav item is a fully-rounded PILL
187
+ // (radius 9999); the active item is a tonal alpha(primary, 0.12) pill with the
188
+ // icon + label in brand `primary`; inactive transparent with `foreground`.
189
+ // ~56dp item height; press = android_ripple.
190
+ // =============================================================================
191
+
192
+ export const androidSkin: SidebarSkin = {
193
+ pressedFill: false, // Android uses a ripple instead of a pressed fill
194
+ pressedOpacity: null,
195
+ ripple: (tokens) => ({ color: alpha(tokens.primary, 0.12), borderless: false }),
196
+
197
+ column(tokens, frame) {
198
+ const base: ViewStyle = {
199
+ width: 240,
200
+ backgroundColor: tokens.background,
201
+ gap: 16,
202
+ // M3 drawer items inset 12dp from the sheet edge so the pill clears it.
203
+ paddingHorizontal: 12,
204
+ paddingVertical: 8,
205
+ };
206
+ if (frame === "bordered") {
207
+ return { ...base, borderRadius: 16, borderWidth: 1, borderColor: tokens.border, overflow: "hidden" };
208
+ }
209
+ return { ...base, borderRightWidth: 1, borderColor: tokens.border };
210
+ },
211
+
212
+ group: { gap: 2 },
213
+
214
+ // M3 drawer section header: titleSmall ~14sp, medium, muted, padded to align
215
+ // with the pill's inner edge.
216
+ sectionTitle(tokens) {
217
+ return {
218
+ paddingHorizontal: 16,
219
+ paddingVertical: 6,
220
+ fontSize: 14,
221
+ lineHeight: 20,
222
+ fontWeight: "500",
223
+ color: tokens["muted-foreground"],
224
+ };
225
+ },
226
+
227
+ // ~56dp item: a fully-rounded pill (radius 9999) with 16dp horizontal padding.
228
+ row(_tokens, density) {
229
+ return {
230
+ flexDirection: "row",
231
+ alignItems: "center",
232
+ gap: 12,
233
+ borderRadius: 9999,
234
+ paddingHorizontal: 16,
235
+ // ~56dp comfortable / ~48dp compact (paddingVertical + ~24 line box).
236
+ paddingVertical: density === "compact" ? 12 : 16,
237
+ };
238
+ },
239
+
240
+ // The active item is a tonal pill: secondaryContainer ~ alpha(primary, 0.12).
241
+ rowFill(tokens, active) {
242
+ return active ? { backgroundColor: alpha(tokens.primary, 0.12) } : null;
243
+ },
244
+
245
+ // M3 drawer label: labelLarge ~14sp medium; active carries the brand `primary`,
246
+ // inactive the on-surface `foreground`.
247
+ label(tokens, active, density) {
248
+ return {
249
+ flexGrow: 1,
250
+ flexShrink: 1,
251
+ flexBasis: "0%",
252
+ ...(density === "compact" ? { fontSize: 13, lineHeight: 18 } : { fontSize: 14, lineHeight: 20 }),
253
+ fontWeight: "500",
254
+ color: active ? tokens.primary : tokens.foreground,
255
+ };
256
+ },
257
+
258
+ // Icon glyph: brand `primary` when active, on-surface `foreground` when not.
259
+ icon(tokens, active) {
260
+ return { fontSize: 18, lineHeight: 24, color: active ? tokens.primary : tokens.foreground };
261
+ },
262
+ };
@@ -0,0 +1,6 @@
1
+ import { createSidebar } from "./sidebar.shared.js";
2
+ import { webSkin } from "./sidebar.styles.js";
3
+
4
+ // Web Sidebar (the base; Metro falls back to it on native, web bundlers resolve it).
5
+ export const Sidebar = createSidebar(webSkin);
6
+ export type { SidebarProps, SidebarItem, SidebarSection } from "./sidebar.shared.js";
@@ -0,0 +1,6 @@
1
+ import { createStepper } from "./stepper.shared.js";
2
+ import { androidSkin } from "./stepper.styles.js";
3
+
4
+ // Material 3 Stepper. Metro resolves this file on Android; the docs import it for preview.
5
+ export const Stepper = createStepper(androidSkin);
6
+ export type { Step, StepperProps } from "./stepper.shared.js";
@@ -0,0 +1,6 @@
1
+ import { createStepper } from "./stepper.shared.js";
2
+ import { iosSkin } from "./stepper.styles.js";
3
+
4
+ // iOS (HIG) Stepper. Metro resolves this file on iOS; the docs import it for preview.
5
+ export const Stepper = createStepper(iosSkin);
6
+ export type { Step, StepperProps } from "./stepper.shared.js";
@@ -0,0 +1,150 @@
1
+ # Steppers
2
+
3
+ Multi-step progress indicators: horizontal, vertical, with progress.
4
+
5
+ ## Usage
6
+
7
+ ```tsx
8
+ <Stepper
9
+ steps={[
10
+ { label: "Account", description: "Email verified and password set." },
11
+ { label: "Profile", description: "Add your name and avatar." },
12
+ { label: "Review", description: "Invite collaborators to your workspace." },
13
+ { label: "Done", description: "You're all set." }
14
+ ]}
15
+ current={1}
16
+ value={68}
17
+ label="Setup progress"
18
+ />
19
+ ```
20
+
21
+ ## Variants
22
+
23
+ ### Type - Vertical
24
+
25
+ ```tsx
26
+ <Stepper
27
+ steps={[
28
+ { label: "Account", description: "Email verified and password set." },
29
+ { label: "Profile", description: "Add your name and avatar." },
30
+ { label: "Review", description: "Invite collaborators to your workspace." },
31
+ { label: "Done", description: "You're all set." }
32
+ ]}
33
+ current={1}
34
+ vertical
35
+ value={68}
36
+ label="Setup progress"
37
+ />
38
+ ```
39
+
40
+ ### Type - Progress bar
41
+
42
+ ```tsx
43
+ <Stepper
44
+ steps={[
45
+ { label: "Account", description: "Email verified and password set." },
46
+ { label: "Profile", description: "Add your name and avatar." },
47
+ { label: "Review", description: "Invite collaborators to your workspace." },
48
+ { label: "Done", description: "You're all set." }
49
+ ]}
50
+ current={1}
51
+ progress
52
+ value={68}
53
+ label="Setup progress"
54
+ />
55
+ ```
56
+
57
+ ## Do & Don't
58
+
59
+ ### Horizontal
60
+
61
+ **Do** — Mark exactly one step active; show completed steps filled and the rest pending.
62
+
63
+ ```tsx
64
+ <Stepper current={1} steps={[
65
+ { label: "Account" },
66
+ { label: "Profile" },
67
+ { label: "Review" }
68
+ ]} />
69
+ ```
70
+
71
+ **Don't** — Styling several steps as active at once hides which step the user is actually on.
72
+
73
+ ```tsx
74
+ <View style={{ flexDirection: "row", alignItems: "flex-start" }}>
75
+ <View style={{ flexDirection: "row", alignItems: "flex-start", flexGrow: 1, flexShrink: 1, flexBasis: "0%" }}>
76
+ <View style={{ alignItems: "center", gap: 6 }}>
77
+ <View style={{ height: 32, width: 32, flexShrink: 0, flexDirection: "row", alignItems: "center", justifyContent: "center", borderRadius: 9999, borderWidth: 2, borderColor: tokens.primary, backgroundColor: "transparent" }}>
78
+ <Text style={{ fontSize: 14, lineHeight: 20, fontWeight: "500", color: tokens.primary }}>1</Text>
79
+ </View>
80
+ <Text style={{ fontSize: 12, lineHeight: 16, fontWeight: "500", color: tokens.foreground }}>Account</Text>
81
+ </View>
82
+ <View style={{ marginHorizontal: 8, marginTop: 16, height: 1, flexGrow: 1, flexShrink: 1, flexBasis: "0%", backgroundColor: tokens.border }} />
83
+ </View>
84
+ <View style={{ flexDirection: "row", alignItems: "flex-start", flexGrow: 1, flexShrink: 1, flexBasis: "0%" }}>
85
+ <View style={{ alignItems: "center", gap: 6 }}>
86
+ <View style={{ height: 32, width: 32, flexShrink: 0, flexDirection: "row", alignItems: "center", justifyContent: "center", borderRadius: 9999, borderWidth: 2, borderColor: tokens.primary, backgroundColor: "transparent" }}>
87
+ <Text style={{ fontSize: 14, lineHeight: 20, fontWeight: "500", color: tokens.primary }}>2</Text>
88
+ </View>
89
+ <Text style={{ fontSize: 12, lineHeight: 16, fontWeight: "500", color: tokens.foreground }}>Profile</Text>
90
+ </View>
91
+ <View style={{ marginHorizontal: 8, marginTop: 16, height: 1, flexGrow: 1, flexShrink: 1, flexBasis: "0%", backgroundColor: tokens.border }} />
92
+ </View>
93
+ <View style={{ flexDirection: "row", alignItems: "flex-start" }}>
94
+ <View style={{ alignItems: "center", gap: 6 }}>
95
+ <View style={{ height: 32, width: 32, flexShrink: 0, flexDirection: "row", alignItems: "center", justifyContent: "center", borderRadius: 9999, borderWidth: 2, borderColor: tokens.primary, backgroundColor: "transparent" }}>
96
+ <Text style={{ fontSize: 14, lineHeight: 20, fontWeight: "500", color: tokens.primary }}>3</Text>
97
+ </View>
98
+ <Text style={{ fontSize: 12, lineHeight: 16, fontWeight: "500", color: tokens.foreground }}>Review</Text>
99
+ </View>
100
+ </View>
101
+ </View>
102
+ ```
103
+
104
+ ### Vertical
105
+
106
+ **Do** — Pair each vertical step with a one-line description so the extra width earns its place.
107
+
108
+ ```tsx
109
+ <View style={{ maxWidth: 320 }}>
110
+ <Stepper vertical current={1} steps={[
111
+ { label: "Account created", description: "Email verified and password set." },
112
+ { label: "Profile setup", description: "Add your name and avatar." },
113
+ { label: "Team invite", description: "Invite collaborators to your workspace." },
114
+ { label: "Done", description: "You're all set." }
115
+ ]} />
116
+ </View>
117
+ ```
118
+
119
+ **Don't** — A vertical step with only a title wastes the space the layout is built to use.
120
+
121
+ ```tsx
122
+ <View style={{ maxWidth: 320 }}>
123
+ <Stepper vertical current={1} steps={[
124
+ { label: "Account created" },
125
+ { label: "Profile setup" },
126
+ { label: "Team invite" },
127
+ { label: "Done" }
128
+ ]} />
129
+ </View>
130
+ ```
131
+
132
+ ### Progress bar
133
+
134
+ **Do** — Label the bar and show the exact percentage so progress is legible at a glance.
135
+
136
+ ```tsx
137
+ <View style={{ maxWidth: 320 }}>
138
+ <Stepper progress current={0} steps={[]} label="Setup progress" value={68} />
139
+ </View>
140
+ ```
141
+
142
+ **Don't** — A bare progress bar with no percentage leaves users guessing how far along they are.
143
+
144
+ ```tsx
145
+ <View style={{ maxWidth: 320 }}>
146
+ <View style={{ height: 6, overflow: "hidden", borderRadius: 9999, backgroundColor: tokens.muted }}>
147
+ <View style={{ height: "100%", borderRadius: 9999, backgroundColor: tokens.primary, width: "68%" }} />
148
+ </View>
149
+ </View>
150
+ ```