@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,175 @@
1
+ import { useState } from "react";
2
+ import { View, Text, Pressable, useTheme, type StyleProp, type ViewStyle } from "../../style/index.js";
3
+ import { Button } from "../../atoms/button/button.js";
4
+ import { Icon } from "../../atoms/icon/icon.js";
5
+ import * as s from "./overlays.styles.js";
6
+ import { type Placement, type OverlaySkin } from "./overlays.styles.js";
7
+
8
+ // Shared Overlay shell. The structure (the optional trigger plus a CONTAINED dim
9
+ // backdrop holding the surface positioned per placement), the public
10
+ // boolean-prop API, the placement precedence, the controlled/uncontrolled open
11
+ // state, the trigger/Done handlers, and the footer action all live here once. A
12
+ // platform file supplies only its skin (the surface shape/fill/sizing and the
13
+ // grabber/drag handle) and calls createOverlay.
14
+ //
15
+ // Overlay: a dim backdrop with a surface anchored to a side or the center,
16
+ // rendered INLINE for the docs preview. Rather than a full-screen portal/Modal,
17
+ // this is a CONTAINED backdrop View (a rounded, clipped scrim with explicit
18
+ // presence in the preview) that holds the overlay surface positioned inside it:
19
+ // a right-side drawer, a bottom sheet, or a centered modal/panel.
20
+ //
21
+ // Boolean-prop API: one boolean per option, grouped by axis, first-match
22
+ // precedence within an axis (mirrors Dialog/Button). Placement axis (pick one):
23
+ //
24
+ // - `drawer`: a panel anchored to the right edge, full height (the default).
25
+ // - `sheet`: a panel anchored to the bottom edge, spanning the width.
26
+ // - `modal`: a centered card floating over the backdrop.
27
+
28
+ export interface OverlayProps {
29
+ // Content (strings).
30
+ title?: string;
31
+ description?: string;
32
+ // Trigger button label. When set, the overlay renders the button and opens
33
+ // itself on press (uncontrolled). Omit when you drive `open` yourself.
34
+ trigger?: string;
35
+ // Controlled open state. Omit for uncontrolled (the trigger opens it).
36
+ open?: boolean;
37
+ // Fired when the open state changes (trigger press, done).
38
+ onOpenChange?: (open: boolean) => void;
39
+ // Placement (pick one; first match wins, default is the right-side drawer).
40
+ drawer?: boolean;
41
+ sheet?: boolean;
42
+ modal?: boolean;
43
+ // Footer action label.
44
+ doneLabel?: string;
45
+ // Action handler.
46
+ onDone?: () => void;
47
+ /** Escape hatch for layout/positioning composition on the overlay surface. */
48
+ style?: StyleProp<ViewStyle>;
49
+ }
50
+
51
+ // Placement precedence when more than one is passed: first match wins.
52
+ function placementOf(p: OverlayProps): Placement {
53
+ if (p.drawer) return "drawer";
54
+ if (p.sheet) return "sheet";
55
+ if (p.modal) return "modal";
56
+ return "drawer";
57
+ }
58
+
59
+ /** Build an Overlay component from a platform skin. */
60
+ export function createOverlay(skin: OverlaySkin) {
61
+ return function Overlay(props: OverlayProps) {
62
+ const { title, description, trigger, open: openProp, onOpenChange, doneLabel = "Done", onDone, style } = props;
63
+ const { tokens } = useTheme();
64
+
65
+ // Uncontrolled by default: the trigger opens the overlay and Done closes it;
66
+ // a controlled `open` prop overrides this.
67
+ const [internalOpen, setInternalOpen] = useState(false);
68
+ const open = openProp ?? internalOpen;
69
+ const setOpen = (next: boolean) => {
70
+ if (openProp === undefined) setInternalOpen(next);
71
+ onOpenChange?.(next);
72
+ };
73
+
74
+ const placement = placementOf(props);
75
+
76
+ // The grabber/drag handle renders on the `sheet` placement only, and only
77
+ // when the platform skin draws one (iOS/Android, not the web).
78
+ const showHandle = placement === "sheet" && skin.handle != null;
79
+
80
+ // The iOS 27 sheet header toolbar (leading circular close, centered title,
81
+ // trailing circular action) renders on the `sheet` placement only, and only
82
+ // when the platform skin supplies the header pieces (iOS, not web/Android).
83
+ // Web/Android leave `sheetHeader` undefined and keep the title/description/
84
+ // footer stack below unchanged.
85
+ const showSheetHeader =
86
+ placement === "sheet" && skin.sheetHeader != null && skin.headerButton != null;
87
+
88
+ const close = () => setOpen(false);
89
+ const done = () => {
90
+ onDone?.();
91
+ setOpen(false);
92
+ };
93
+
94
+ // Optional trigger button plus the overlay. The overlay is a contained dim
95
+ // backdrop: a rounded, clipped scrim with explicit presence in the preview
96
+ // (minHeight) so the surface reads as an overlay within the area.
97
+ return (
98
+ <View style={s.root}>
99
+ {trigger != null ? (
100
+ <View style={s.triggerWrap}>
101
+ <Button outline small onPress={() => setOpen(true)}>
102
+ {trigger}
103
+ </Button>
104
+ </View>
105
+ ) : null}
106
+ {open ? (
107
+ <View
108
+ style={[
109
+ trigger != null ? s.backdropWithTrigger : null,
110
+ s.backdrop(),
111
+ s.backdropPlacement[placement],
112
+ { minHeight: 280 },
113
+ ]}
114
+ >
115
+ <View style={[skin.surface(tokens, placement), style]}>
116
+ {showHandle ? (
117
+ <View style={s.handleWrap}>
118
+ <View style={skin.handle!(tokens)} />
119
+ </View>
120
+ ) : null}
121
+ {showSheetHeader ? (
122
+ // iOS 27 sheet header toolbar: leading circular close (X), centered
123
+ // title, trailing circular accent action. The title centers in the
124
+ // row; the side controls are equal-width slots so it stays centered.
125
+ <View style={skin.sheetHeader}>
126
+ <Pressable
127
+ accessibilityRole="button"
128
+ accessibilityLabel="Close"
129
+ onPress={close}
130
+ style={({ pressed }) => [
131
+ skin.headerButton!(tokens),
132
+ pressed ? { opacity: 0.8 } : null,
133
+ ]}
134
+ >
135
+ <Icon x muted size={20} />
136
+ </Pressable>
137
+ <View style={s.sheetHeaderTitleWrap}>
138
+ {title != null ? (
139
+ <Text numberOfLines={1} style={(skin.headerTitle ?? skin.title)(tokens)}>
140
+ {title}
141
+ </Text>
142
+ ) : null}
143
+ </View>
144
+ <Pressable
145
+ accessibilityRole="button"
146
+ accessibilityLabel={doneLabel}
147
+ onPress={done}
148
+ style={({ pressed }) => [
149
+ (skin.headerButtonAccent ?? skin.headerButton)!(tokens),
150
+ pressed ? { opacity: 0.8 } : null,
151
+ ]}
152
+ >
153
+ <Icon upload primaryForeground size={20} />
154
+ </Pressable>
155
+ </View>
156
+ ) : (
157
+ <>
158
+ {title != null ? <Text style={skin.title(tokens)}>{title}</Text> : null}
159
+ {description != null ? (
160
+ <Text style={skin.description(tokens)}>{description}</Text>
161
+ ) : null}
162
+ <View style={skin.footer}>
163
+ <Button primary small onPress={done}>
164
+ {doneLabel}
165
+ </Button>
166
+ </View>
167
+ </>
168
+ )}
169
+ </View>
170
+ </View>
171
+ ) : null}
172
+ </View>
173
+ );
174
+ };
175
+ }
@@ -0,0 +1,309 @@
1
+ import { type ViewStyle, type TextStyle } from "react-native";
2
+ import { type ColorTokens, shadow, alpha } from "../../style/index.js";
3
+
4
+ // Co-located Overlay skins, one per platform, all driven by the brand tokens
5
+ // (passed in from useTheme so they follow light/dark and read as glass when
6
+ // tokens.popover is swapped translucent at the theming level). The BRAND
7
+ // survives on every platform (the indigo `primary` Done action stays the same);
8
+ // only the native SHAPE, sizing, surface treatment, and the grabber/handle change
9
+ // per OS:
10
+ // iOS (iOS 27 / Liquid Glass sheet): a sheet with the TOP corners rounded ~38
11
+ // presenting from the bottom (flat bottom), a small grabber handle (a ~36x5
12
+ // rounded `muted-foreground` pill) centered at the top, and an iOS 27 header
13
+ // toolbar directly below it: a leading CIRCULAR tinted button holding the close
14
+ // (X) glyph, a centered title, and a trailing CIRCULAR action button (a filled
15
+ // `primary` accent circle holding the action glyph) — both are ~40pt circles,
16
+ // not bare icons. The centered `modal` becomes a rounded form-sheet card
17
+ // (~13 radius). Press feedback on the surface is none (the surface itself is
18
+ // not pressable); the circular header buttons keep the Button's iOS press =
19
+ // opacity dim.
20
+ // Android (Material 3 bottom sheet): the `sheet` has its TOP corners rounded 28
21
+ // with a centered drag handle (~32x4 rounded `muted-foreground` pill), the
22
+ // `popover` surface, and M3 elevation; the `drawer` and `modal` keep the M3
23
+ // low-radius/elevated treatment.
24
+ // Web: the established Canvas look (the current overlay, lifted verbatim) — the
25
+ // contained dim scrim with the drawer (right, full height, border-l), sheet
26
+ // (bottom, rounded-t-lg, border-t), and modal (centered card, rounded-lg
27
+ // border) surfaces, each `popover` filled with a border and shadow-xl.
28
+
29
+ export type Placement = "drawer" | "sheet" | "modal";
30
+
31
+ // The contract a platform skin fulfills. The shell renders the contained dim
32
+ // backdrop, the surface positioned inside it per placement, an optional top
33
+ // grabber/drag handle (sheet only), the title, the description, and the footer
34
+ // action row; the skin maps the active platform's shape/fill/sizing onto each
35
+ // piece. `backdrop` and the contents type styles read the tokens so light/dark/
36
+ // glass keep working.
37
+ export interface OverlaySkin {
38
+ /** The overlay surface per placement: shape, fill, border, padding, shadow. */
39
+ surface: (t: ColorTokens, placement: Placement) => ViewStyle;
40
+ /** The top grabber/drag handle rendered on the `sheet` placement only. Null on
41
+ * platforms that do not draw one (web). The shell centers it; this is the pill. */
42
+ handle: ((t: ColorTokens) => ViewStyle) | null;
43
+ /** The title type/color. */
44
+ title: (t: ColorTokens) => TextStyle;
45
+ /** The description type/color. */
46
+ description: (t: ColorTokens) => TextStyle;
47
+ /** The footer action row layout. */
48
+ footer: ViewStyle;
49
+
50
+ // --- iOS 27 sheet header (optional; iOS skin only) ------------------------
51
+ // When a skin supplies `sheetHeader`, the shell renders an iOS 27 sheet header
52
+ // toolbar on the `sheet` placement instead of the title/description/footer
53
+ // stack: a leading CIRCULAR close (X) button, a centered title, and a trailing
54
+ // CIRCULAR action button. Web/Android leave these undefined and keep the plain
55
+ // title/description/footer layout byte-for-byte.
56
+
57
+ /** The header toolbar row (3-column: leading circle, centered title, trailing
58
+ * circle). Present only on platforms that draw the iOS 27 sheet header. */
59
+ sheetHeader?: ViewStyle;
60
+ /** The circular tinted header button (a ~40pt `muted` circle behind a glyph). */
61
+ headerButton?: (t: ColorTokens) => ViewStyle;
62
+ /** The filled-accent trailing header button (a ~40pt `primary` circle). */
63
+ headerButtonAccent?: (t: ColorTokens) => ViewStyle;
64
+ /** The centered header title type/color (the iOS 27 sheet navigation title). */
65
+ headerTitle?: (t: ColorTokens) => TextStyle;
66
+ }
67
+
68
+ // --- outer wrapper + trigger (identical across platforms) -------------------
69
+
70
+ // w-full: the overlay occupies the full width of its slot.
71
+ export const root: ViewStyle = { width: "100%" };
72
+
73
+ // self-start: the trigger button hugs the leading edge.
74
+ export const triggerWrap: ViewStyle = { alignSelf: "flex-start" };
75
+
76
+ // --- backdrop (identical across platforms) ----------------------------------
77
+
78
+ // The contained dim scrim: relative w-full overflow-hidden rounded-lg bg-black/50.
79
+ // (mt-3 is applied conditionally by the shell when a trigger is present.)
80
+ export function backdrop(): ViewStyle {
81
+ return {
82
+ position: "relative",
83
+ width: "100%",
84
+ overflow: "hidden",
85
+ borderRadius: 8,
86
+ backgroundColor: alpha("#000000", 0.5),
87
+ };
88
+ }
89
+
90
+ // mt-3: gap below the trigger button, applied only when a trigger is rendered.
91
+ export const backdropWithTrigger: ViewStyle = { marginTop: 12 };
92
+
93
+ // The backdrop's alignment per placement. The modal centers its card; the
94
+ // drawer and sheet let the absolutely-positioned surface anchor itself.
95
+ export const backdropPlacement: Record<Placement, ViewStyle> = {
96
+ drawer: {},
97
+ sheet: {},
98
+ // items-center justify-center
99
+ modal: { alignItems: "center", justifyContent: "center" },
100
+ };
101
+
102
+ // The grabber/handle row: centered at the top of the sheet surface. The skin
103
+ // supplies the pill dimensions/color; this centers it and spaces it from the
104
+ // content below.
105
+ export const handleWrap: ViewStyle = { alignItems: "center", marginBottom: 12 };
106
+
107
+ // The centered title slot in the iOS 27 sheet header toolbar: it flexes to fill
108
+ // the space between the two equal-size circular controls so the title stays
109
+ // optically centered in the row. Used by the shell only when the active skin
110
+ // draws the iOS sheet header.
111
+ export const sheetHeaderTitleWrap: ViewStyle = { flex: 1, alignItems: "center", paddingHorizontal: 8 };
112
+
113
+ // --- shared surface fragments -----------------------------------------------
114
+
115
+ // The drawer anchors to the right edge, full height. Shared anchoring; the skin
116
+ // owns fill/border/radius/padding/shadow.
117
+ const DRAWER_ANCHOR: ViewStyle = { position: "absolute", top: 0, right: 0, bottom: 0, width: 300 };
118
+
119
+ // The sheet anchors to the bottom edge, spanning the width. Shared anchoring.
120
+ const SHEET_ANCHOR: ViewStyle = { position: "absolute", left: 0, right: 0, bottom: 0 };
121
+
122
+ // The modal is a self-centered floating card.
123
+ const MODAL_BOX: ViewStyle = { alignSelf: "center", marginVertical: 48, width: "100%", maxWidth: 420 };
124
+
125
+ // ---------- Web: the established Canvas look (lifted verbatim) ----------
126
+ // The current overlay surfaces: the drawer (absolute top-0 right-0 bottom-0
127
+ // w-[300px] bg-popover border-l border-border p-5 shadow-xl), the sheet (absolute
128
+ // left-0 right-0 bottom-0 bg-popover border-t border-border rounded-t-lg p-5
129
+ // shadow-xl), and the modal (self-center my-12 w-full max-w-[420px] bg-popover
130
+ // border border-border rounded-lg p-6 shadow-xl). No grabber handle on the web.
131
+ export const webSkin: OverlaySkin = {
132
+ surface: (t, placement) => {
133
+ switch (placement) {
134
+ case "drawer":
135
+ return {
136
+ ...DRAWER_ANCHOR,
137
+ backgroundColor: t.popover,
138
+ borderLeftWidth: 1,
139
+ borderColor: t.border,
140
+ padding: 20,
141
+ ...shadow("xl"),
142
+ };
143
+ case "sheet":
144
+ return {
145
+ ...SHEET_ANCHOR,
146
+ backgroundColor: t.popover,
147
+ borderTopWidth: 1,
148
+ borderColor: t.border,
149
+ borderTopLeftRadius: 8,
150
+ borderTopRightRadius: 8,
151
+ padding: 20,
152
+ ...shadow("xl"),
153
+ };
154
+ case "modal":
155
+ return {
156
+ ...MODAL_BOX,
157
+ backgroundColor: t.popover,
158
+ borderWidth: 1,
159
+ borderColor: t.border,
160
+ borderRadius: 8,
161
+ padding: 24,
162
+ ...shadow("xl"),
163
+ };
164
+ }
165
+ },
166
+ handle: null,
167
+ // text-base font-semibold text-popover-foreground
168
+ title: (t) => ({ fontSize: 16, lineHeight: 24, fontWeight: "600", color: t["popover-foreground"] }),
169
+ // text-sm text-muted-foreground mt-2
170
+ description: (t) => ({ fontSize: 14, lineHeight: 20, color: t["muted-foreground"], marginTop: 8 }),
171
+ // flex-row justify-end mt-6
172
+ footer: { flexDirection: "row", justifyContent: "flex-end", marginTop: 24 },
173
+ };
174
+
175
+ // ---------- iOS 27 (Liquid Glass sheet): top corners ~38, grabber + header toolbar ----------
176
+ // The iOS 27 sheet presents from the bottom with the TOP corners rounded ~38 (the
177
+ // large continuous-corner radius of the Liquid Glass sheet) and a flat bottom; a
178
+ // small grabber (a ~36x5 rounded muted-foreground pill) is centered at the top,
179
+ // and an iOS 27 header toolbar sits directly below it: a leading CIRCULAR close (X)
180
+ // button, a centered title, and a trailing CIRCULAR action button (a filled
181
+ // `primary` accent circle). The right `drawer` keeps the lineless rounded-left
182
+ // treatment; the centered `modal` becomes a rounded form-sheet card (~13 radius).
183
+ // The iOS sheet is lineless: no visible border, a soft elevation.
184
+ const IOS_SHEET_RADIUS = 38;
185
+ const IOS_CARD_RADIUS = 13;
186
+ // The header toolbar's circular control: a ~40pt circle behind the glyph.
187
+ const IOS_HEADER_BUTTON = 40;
188
+ export const iosSkin: OverlaySkin = {
189
+ surface: (t, placement) => {
190
+ switch (placement) {
191
+ case "drawer":
192
+ return {
193
+ ...DRAWER_ANCHOR,
194
+ backgroundColor: t.popover,
195
+ // The leading edge rounds (the panel slides in from the right edge).
196
+ borderTopLeftRadius: IOS_SHEET_RADIUS,
197
+ borderBottomLeftRadius: IOS_SHEET_RADIUS,
198
+ padding: 20,
199
+ ...shadow("xl"),
200
+ };
201
+ case "sheet":
202
+ return {
203
+ ...SHEET_ANCHOR,
204
+ backgroundColor: t.popover,
205
+ // Top corners rounded ~38, flat bottom (presents from the bottom edge).
206
+ borderTopLeftRadius: IOS_SHEET_RADIUS,
207
+ borderTopRightRadius: IOS_SHEET_RADIUS,
208
+ paddingHorizontal: 16,
209
+ // A little more top padding to seat the grabber above the header toolbar.
210
+ paddingTop: 8,
211
+ paddingBottom: 24,
212
+ ...shadow("xl"),
213
+ };
214
+ case "modal":
215
+ return {
216
+ ...MODAL_BOX,
217
+ // The iOS form sheet is a rounded card, lineless.
218
+ backgroundColor: t.popover,
219
+ borderRadius: IOS_CARD_RADIUS,
220
+ padding: 24,
221
+ ...shadow("xl"),
222
+ };
223
+ }
224
+ },
225
+ // The grabber: a ~36x5 rounded muted-foreground pill centered at the top.
226
+ handle: (t) => ({ width: 36, height: 5, borderRadius: 2.5, backgroundColor: t["muted-foreground"] }),
227
+ // SF-scale title: 17pt semibold (the iOS body emphasized).
228
+ title: (t) => ({ fontSize: 17, lineHeight: 22, fontWeight: "600", color: t["popover-foreground"] }),
229
+ // 15pt secondary text.
230
+ description: (t) => ({ fontSize: 15, lineHeight: 20, color: t["muted-foreground"], marginTop: 8 }),
231
+ footer: { flexDirection: "row", justifyContent: "flex-end", marginTop: 24 },
232
+ // iOS 27 sheet header toolbar: a 3-column row seating the leading/trailing
233
+ // circular controls and the centered title, just below the grabber.
234
+ sheetHeader: { flexDirection: "row", alignItems: "center", marginBottom: 12 },
235
+ // The circular tinted header button: a ~40pt `muted` circle centering its glyph.
236
+ headerButton: (t) => ({
237
+ width: IOS_HEADER_BUTTON,
238
+ height: IOS_HEADER_BUTTON,
239
+ borderRadius: IOS_HEADER_BUTTON / 2,
240
+ backgroundColor: t.muted,
241
+ alignItems: "center",
242
+ justifyContent: "center",
243
+ }),
244
+ // The filled-accent trailing header button: a ~40pt brand-`primary` circle.
245
+ headerButtonAccent: (t) => ({
246
+ width: IOS_HEADER_BUTTON,
247
+ height: IOS_HEADER_BUTTON,
248
+ borderRadius: IOS_HEADER_BUTTON / 2,
249
+ backgroundColor: t.primary,
250
+ alignItems: "center",
251
+ justifyContent: "center",
252
+ }),
253
+ // The centered header title: 17pt semibold, the iOS 27 sheet navigation title.
254
+ headerTitle: (t) => ({ fontSize: 17, lineHeight: 22, fontWeight: "600", color: t["popover-foreground"] }),
255
+ };
256
+
257
+ // ---------- Android (Material 3 bottom sheet): top corners 28, drag handle ----------
258
+ // M3 modal bottom sheet: the `sheet` has its TOP corners rounded 28 (the M3
259
+ // extra-large shape) with a centered drag handle (a ~32x4 rounded
260
+ // muted-foreground pill), the `popover` surface, and M3 elevation. The right
261
+ // `drawer` (navigation-drawer-like) keeps a large trailing radius; the centered
262
+ // `modal` is the M3 dialog shape (28 radius, elevated, no border).
263
+ const ANDROID_SHEET_RADIUS = 28;
264
+ const ANDROID_DIALOG_RADIUS = 28;
265
+ export const androidSkin: OverlaySkin = {
266
+ surface: (t, placement) => {
267
+ switch (placement) {
268
+ case "drawer":
269
+ return {
270
+ ...DRAWER_ANCHOR,
271
+ backgroundColor: t.popover,
272
+ // M3 navigation drawer: the trailing (leading-in) edge gets the large radius.
273
+ borderTopLeftRadius: ANDROID_SHEET_RADIUS,
274
+ borderBottomLeftRadius: ANDROID_SHEET_RADIUS,
275
+ padding: 24,
276
+ ...shadow("lg"),
277
+ };
278
+ case "sheet":
279
+ return {
280
+ ...SHEET_ANCHOR,
281
+ backgroundColor: t.popover,
282
+ // Top corners radius 28, flat bottom (M3 modal bottom sheet).
283
+ borderTopLeftRadius: ANDROID_SHEET_RADIUS,
284
+ borderTopRightRadius: ANDROID_SHEET_RADIUS,
285
+ paddingHorizontal: 24,
286
+ // Top padding seats the drag handle; M3 bottom-sheet content inset.
287
+ paddingTop: 16,
288
+ paddingBottom: 24,
289
+ ...shadow("lg"),
290
+ };
291
+ case "modal":
292
+ return {
293
+ ...MODAL_BOX,
294
+ // M3 dialog shape: 28 radius, elevated, no border.
295
+ backgroundColor: t.popover,
296
+ borderRadius: ANDROID_DIALOG_RADIUS,
297
+ padding: 24,
298
+ ...shadow("lg"),
299
+ };
300
+ }
301
+ },
302
+ // The M3 drag handle: a ~32x4 rounded muted-foreground pill centered at the top.
303
+ handle: (t) => ({ width: 32, height: 4, borderRadius: 2, backgroundColor: alpha(t["muted-foreground"], 0.4) }),
304
+ // M3 title: title-large-ish, 16sp medium on the surface.
305
+ title: (t) => ({ fontSize: 16, lineHeight: 24, fontWeight: "500", color: t["popover-foreground"] }),
306
+ // M3 body-medium supporting text.
307
+ description: (t) => ({ fontSize: 14, lineHeight: 20, color: t["muted-foreground"], marginTop: 8 }),
308
+ footer: { flexDirection: "row", justifyContent: "flex-end", marginTop: 24 },
309
+ };
@@ -0,0 +1,6 @@
1
+ import { createOverlay } from "./overlays.shared.js";
2
+ import { webSkin } from "./overlays.styles.js";
3
+
4
+ // Web Overlay (the base; Metro falls back to it on native, web bundlers resolve it).
5
+ export const Overlay = createOverlay(webSkin);
6
+ export type { OverlayProps } from "./overlays.shared.js";
@@ -0,0 +1,6 @@
1
+ import { createRowMenu } from "./row-menu.shared.js";
2
+ import { androidSkin } from "./row-menu.styles.js";
3
+
4
+ // Material 3 (menu) RowMenu. Metro resolves this file on Android; the docs import it for preview.
5
+ export const RowMenu = createRowMenu(androidSkin);
6
+ export type { RowMenuProps, RowMenuItem } from "./row-menu.shared.js";
@@ -0,0 +1,6 @@
1
+ import { createRowMenu } from "./row-menu.shared.js";
2
+ import { iosSkin } from "./row-menu.styles.js";
3
+
4
+ // iOS (HIG context menu) RowMenu. Metro resolves this file on iOS; the docs import it for preview.
5
+ export const RowMenu = createRowMenu(iosSkin);
6
+ export type { RowMenuProps, RowMenuItem } from "./row-menu.shared.js";
@@ -0,0 +1,102 @@
1
+ # Row Menu
2
+
3
+ Vertical action menu items and navigation links.
4
+
5
+ ## Usage
6
+
7
+ ```tsx
8
+ <RowMenu
9
+ items={[
10
+ { label: "Edit" },
11
+ { label: "Duplicate" },
12
+ { label: "Delete", destructive: true, separatorBefore: true }
13
+ ]}
14
+ />
15
+ ```
16
+
17
+ ## Variants
18
+
19
+ ### Kind - links
20
+
21
+ ```tsx
22
+ <RowMenu
23
+ links
24
+ items={[
25
+ { label: "Profile" },
26
+ { label: "Billing" },
27
+ { label: "Members" },
28
+ { label: "Settings" }
29
+ ]}
30
+ />
31
+ ```
32
+
33
+ ### Section label
34
+
35
+ ```tsx
36
+ <RowMenu
37
+ sectionLabel="Actions"
38
+ items={[
39
+ { label: "Edit" },
40
+ { label: "Duplicate" },
41
+ { label: "Delete", destructive: true, separatorBefore: true }
42
+ ]}
43
+ />
44
+ ```
45
+
46
+ ### Leading icons
47
+
48
+ ```tsx
49
+ <RowMenu
50
+ items={[
51
+ { label: "Edit", icon: "✎" },
52
+ { label: "Duplicate", icon: "⧉" },
53
+ { label: "Delete", icon: "🗑", destructive: true, separatorBefore: true }
54
+ ]}
55
+ />
56
+ ```
57
+
58
+ ## Do & Don't
59
+
60
+ ### Actions
61
+
62
+ **Do** — Click an item: place destructive actions last, color them, and split them off with a divider.
63
+
64
+ ```tsx
65
+ <RowMenu open items={[
66
+ { label: "Edit" },
67
+ { label: "Duplicate" },
68
+ { label: "Delete", destructive: true, separatorBefore: true }
69
+ ]} />
70
+ ```
71
+
72
+ **Don't** — Click an item: a destructive action sandwiched between routine ones invites a costly misclick.
73
+
74
+ ```tsx
75
+ <RowMenu open items={[
76
+ { label: "Edit" },
77
+ { label: "Delete", destructive: true },
78
+ { label: "Duplicate" }
79
+ ]} />
80
+ ```
81
+
82
+ ### Links
83
+
84
+ **Do** — Use <a href> anchors so links behave like links, and mark the current page with an active highlight.
85
+
86
+ ```tsx
87
+ <RowMenu open links items={[
88
+ { label: "Profile" },
89
+ { label: "Billing" },
90
+ { label: "Members" }
91
+ ]} />
92
+ ```
93
+
94
+ **Don't** — Buttons can't be opened in a new tab, bookmarked, or middle-clicked, navigation needs real links.
95
+
96
+ ```tsx
97
+ <RowMenu open items={[
98
+ { label: "Profile" },
99
+ { label: "Billing" },
100
+ { label: "Members" }
101
+ ]} />
102
+ ```