@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,187 @@
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 { Input } from "../../atoms/input/input.js";
5
+ import * as s from "./alert-dialog.styles.js";
6
+ import { type Width, type AlertDialogSkin } from "./alert-dialog.styles.js";
7
+
8
+ // Shared AlertDialog shell. The structure (optional trigger + dim backdrop +
9
+ // centered card + title/description + optional confirmation field + action row),
10
+ // the uncontrolled/controlled open state, the width/destructive precedence, and
11
+ // the confirm/cancel handlers live here once; a platform file supplies only its
12
+ // skin (card shape, type, action layout, press feedback) and calls
13
+ // createAlertDialog.
14
+ //
15
+ // AlertDialog: a terse yes/no confirmation modal, the compact sibling of Dialog.
16
+ // It poses a question (title), an optional short description, and an action row of
17
+ // a Cancel plus a single confirm. Reserve it for decisions that must block the
18
+ // rest of the app, especially irreversible ones (pass `destructive` to render the
19
+ // confirm as a red, destructive action).
20
+ //
21
+ // In the docs preview the overlay is rendered INLINE: a contained dim backdrop
22
+ // View wraps the centered card, so it reads as a modal within the preview area
23
+ // rather than a full-screen portal that would cover it.
24
+ //
25
+ // Boolean-prop API: one boolean per option, grouped by axis, first-match
26
+ // precedence within an axis (mirrors Button's intentOf). Axes:
27
+ //
28
+ // - Width: `narrow` < `small` < (default medium) < `large`, the panel max-width.
29
+ // Pass at most one; first match wins in that order (narrow, small, large).
30
+ // - Confirm intent: `destructive` renders the confirm action as a destructive
31
+ // action (for an irreversible action); omit for the default primary confirm.
32
+
33
+ export interface AlertDialogProps {
34
+ // Content (strings).
35
+ title?: string;
36
+ description?: string;
37
+ // Trigger button label. When set, the dialog renders the button and opens
38
+ // itself on press (uncontrolled). Omit when you drive `open` yourself.
39
+ trigger?: string;
40
+ // Action labels.
41
+ confirmLabel?: string;
42
+ cancelLabel?: string;
43
+ // Controlled open state. Omit for uncontrolled (the trigger opens it).
44
+ open?: boolean;
45
+ // Fired when the open state changes (trigger press, confirm, cancel).
46
+ onOpenChange?: (open: boolean) => void;
47
+ // Width (pick one; default is the medium panel).
48
+ narrow?: boolean;
49
+ small?: boolean;
50
+ large?: boolean;
51
+ // Body: render a confirmation field ("Type DELETE to confirm") in the panel.
52
+ withInput?: boolean;
53
+ // Confirm intent (default is a primary confirm).
54
+ destructive?: boolean;
55
+ // Action handlers.
56
+ onConfirm?: () => void;
57
+ onCancel?: () => void;
58
+ /** Escape hatch for layout/positioning composition (mainly width). */
59
+ style?: StyleProp<ViewStyle>;
60
+ }
61
+
62
+ // Width precedence when more than one is passed: first match wins.
63
+ function widthOf(p: AlertDialogProps): Width {
64
+ if (p.narrow) return "narrow";
65
+ if (p.small) return "small";
66
+ if (p.large) return "large";
67
+ return "medium";
68
+ }
69
+
70
+ /** Build an AlertDialog component from a platform skin. */
71
+ export function createAlertDialog(skin: AlertDialogSkin) {
72
+ return function AlertDialog(props: AlertDialogProps) {
73
+ const {
74
+ title,
75
+ description,
76
+ confirmLabel = "Continue",
77
+ cancelLabel = "Cancel",
78
+ trigger,
79
+ open: openProp,
80
+ onOpenChange,
81
+ withInput,
82
+ destructive,
83
+ onConfirm,
84
+ onCancel,
85
+ style,
86
+ } = props;
87
+
88
+ const { tokens } = useTheme();
89
+
90
+ // Uncontrolled by default: the trigger opens the dialog and an action closes
91
+ // it; a controlled `open` prop overrides this.
92
+ const [internalOpen, setInternalOpen] = useState(false);
93
+ const open = openProp ?? internalOpen;
94
+ const setOpen = (next: boolean) => {
95
+ if (openProp === undefined) setInternalOpen(next);
96
+ onOpenChange?.(next);
97
+ };
98
+
99
+ const width = widthOf(props);
100
+
101
+ const handleConfirm = () => {
102
+ onConfirm?.();
103
+ setOpen(false);
104
+ };
105
+ const handleCancel = () => {
106
+ onCancel?.();
107
+ setOpen(false);
108
+ };
109
+
110
+ // The action row. iOS renders two capsule buttons side by side (no divider)
111
+ // drawn by the skin; web/Android render a right-aligned row of the shell's
112
+ // Buttons.
113
+ const ripple = skin.ripple ? skin.ripple(tokens) : undefined;
114
+ const actionRow =
115
+ skin.actionLayout === "capsule" ? (
116
+ <View style={skin.capsuleRow!}>
117
+ <Pressable
118
+ onPress={handleCancel}
119
+ accessibilityRole="button"
120
+ android_ripple={ripple}
121
+ style={({ pressed }) => [
122
+ skin.capsuleCell!,
123
+ skin.cancelFill!(tokens),
124
+ skin.pressedOpacity != null && pressed ? { opacity: skin.pressedOpacity } : null,
125
+ ]}
126
+ >
127
+ <Text style={skin.cancelLabelStyle!(tokens)}>{cancelLabel}</Text>
128
+ </Pressable>
129
+ <Pressable
130
+ onPress={handleConfirm}
131
+ accessibilityRole="button"
132
+ android_ripple={ripple}
133
+ style={({ pressed }) => [
134
+ skin.capsuleCell!,
135
+ skin.confirmFill!(tokens, !!destructive),
136
+ skin.pressedOpacity != null && pressed ? { opacity: skin.pressedOpacity } : null,
137
+ ]}
138
+ >
139
+ <Text style={skin.confirmLabelStyle!(tokens, !!destructive)}>{confirmLabel}</Text>
140
+ </Pressable>
141
+ </View>
142
+ ) : (
143
+ <View style={skin.actions}>
144
+ <Button {...skin.cancelButton} small={skin.buttonSmall} onPress={handleCancel}>
145
+ {cancelLabel}
146
+ </Button>
147
+ {destructive ? (
148
+ <Button destructive small={skin.buttonSmall} onPress={handleConfirm}>
149
+ {confirmLabel}
150
+ </Button>
151
+ ) : (
152
+ <Button primary small={skin.buttonSmall} onPress={handleConfirm}>
153
+ {confirmLabel}
154
+ </Button>
155
+ )}
156
+ </View>
157
+ );
158
+
159
+ // Optional trigger button plus the modal. The modal is a contained dim
160
+ // backdrop: a centered, rounded scrim with presence in the preview (explicit
161
+ // minHeight) so the card reads as a modal within the area.
162
+ return (
163
+ <View style={s.root}>
164
+ {trigger != null ? (
165
+ <Button outline small onPress={() => setOpen(true)}>
166
+ {trigger}
167
+ </Button>
168
+ ) : null}
169
+ {open ? (
170
+ <View style={[skin.backdrop, trigger != null ? s.triggerGap : null, { minHeight: 200 }]}>
171
+ <View style={[s.cardBase, skin.card(tokens), s.panelWidth[width], style]}>
172
+ {title != null ? <Text style={skin.title(tokens)}>{title}</Text> : null}
173
+ {description != null ? <Text style={skin.description(tokens)}>{description}</Text> : null}
174
+ {withInput ? (
175
+ <View style={skin.inputBlock}>
176
+ <Text style={skin.inputLabel(tokens)}>Type DELETE to confirm</Text>
177
+ <Input placeholder="DELETE" />
178
+ </View>
179
+ ) : null}
180
+ {actionRow}
181
+ </View>
182
+ </View>
183
+ ) : null}
184
+ </View>
185
+ );
186
+ };
187
+ }
@@ -0,0 +1,248 @@
1
+ import { type ViewStyle, type TextStyle } from "react-native";
2
+ import { type ColorTokens, shadow, alpha } from "../../style/index.js";
3
+
4
+ // Co-located AlertDialog 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 survives
7
+ // on every platform (the `primary` confirm and the `destructive` red); only the
8
+ // native SHAPE, sizing, title alignment, action layout, and press feedback change
9
+ // per OS:
10
+ // iOS (iOS 27 / Liquid Glass alert): a rounded card (~28 radius, `popover`
11
+ // fill, soft shadow, no border), a LEFT-aligned bold title and a left-aligned
12
+ // `muted-foreground` message; exactly two actions rendered as two CAPSULES
13
+ // side by side (no divider) — a gray Cancel capsule (`secondary` fill) and a
14
+ // filled Confirm capsule (`primary`, or `destructive` when destructive) with
15
+ // a white label. Press = opacity dim (~0.85).
16
+ // Android (Material 3 AlertDialog): an elevated surface (28 radius, `popover`
17
+ // fill, soft shadow), a LEFT-aligned title and left-aligned body; two M3 TEXT
18
+ // buttons bottom-right (Cancel then Confirm/Delete), the confirm tinted with
19
+ // the brand `primary` and a destructive confirm in the `destructive` red.
20
+ // Press = android_ripple (brand state layer).
21
+ // Web: the established Canvas look (the current alert-dialog, lifted verbatim) —
22
+ // a bordered, dim-backed card (8 radius, 1px `border`, `popover` fill,
23
+ // shadow-xl, padding 24), a left-aligned 16pt/600 title and 14pt
24
+ // `muted-foreground` description, with a right-aligned action row of an
25
+ // `outline` Cancel Button plus a `primary`/`destructive` confirm Button.
26
+
27
+ export type Width = "narrow" | "small" | "medium" | "large";
28
+
29
+ // Panel max-width per width axis (mirroring Tailwind's max-w-xs..lg). Shared by
30
+ // every platform; the native skins narrow the medium/large footprint a touch but
31
+ // the axis mapping is one source of truth.
32
+ export const panelWidth: Record<Width, ViewStyle> = {
33
+ narrow: { maxWidth: 320 },
34
+ small: { maxWidth: 384 },
35
+ medium: { maxWidth: 448 },
36
+ large: { maxWidth: 512 },
37
+ };
38
+
39
+ // How the action row is structured. "buttons" renders the shell's Button-based
40
+ // right-aligned row (web/Android); "capsule" renders the iOS side-by-side pair of
41
+ // pill buttons drawn by the skin's own action styles (no divider).
42
+ export type ActionLayout = "buttons" | "capsule";
43
+
44
+ // The contract a platform skin fulfills. The shell renders the wrapper, the dim
45
+ // backdrop, the card, the title/description, the optional confirmation field, and
46
+ // the action row; the skin maps the active platform's shape/fill/sizing/type/
47
+ // feedback onto each piece.
48
+ export interface AlertDialogSkin {
49
+ /** The contained dim scrim that centers the card within the preview area. */
50
+ backdrop: ViewStyle;
51
+ /** The card base: shape, border (or lack of), padding, shadow. tokens drive fill. */
52
+ card: (t: ColorTokens) => ViewStyle;
53
+ /** Title type + alignment (centered on iOS, left on Android/web). */
54
+ title: (t: ColorTokens) => TextStyle;
55
+ /** Description type + alignment + color. */
56
+ description: (t: ColorTokens) => TextStyle;
57
+ /** The confirmation-field block container ("Type DELETE to confirm" + input). */
58
+ inputBlock: ViewStyle;
59
+ /** The confirmation-field label. */
60
+ inputLabel: (t: ColorTokens) => TextStyle;
61
+ /** Which action structure the shell renders. */
62
+ actionLayout: ActionLayout;
63
+ // --- "buttons" layout (web/Android): a right-aligned row of shell Buttons ----
64
+ /** The right-aligned Button row container (the "buttons" layout). */
65
+ actions: ViewStyle;
66
+ /** The size prop passed to the shell's Cancel/Confirm Buttons. */
67
+ buttonSmall: boolean;
68
+ /** The intent props for the Cancel Button (web = outline; Android = ghost/text). */
69
+ cancelButton: { outline?: boolean; ghost?: boolean };
70
+ // --- "capsule" layout (iOS): two side-by-side pill buttons, no divider --------
71
+ /** The action row that holds the two equal-width capsules with a gap. */
72
+ capsuleRow: ViewStyle | null;
73
+ /** A single capsule cell base (shape/sizing); fill comes from the *Fill helpers. */
74
+ capsuleCell: ViewStyle | null;
75
+ /** The Cancel (gray) capsule fill. */
76
+ cancelFill: ((t: ColorTokens) => ViewStyle) | null;
77
+ /** The Confirm capsule fill; `destructive` swaps `primary` for `destructive`. */
78
+ confirmFill: ((t: ColorTokens, destructive: boolean) => ViewStyle) | null;
79
+ /** The Cancel capsule label (regular weight, on-secondary color). */
80
+ cancelLabelStyle: ((t: ColorTokens) => TextStyle) | null;
81
+ /** The Confirm capsule label (weight 600, on-fill foreground). */
82
+ confirmLabelStyle: ((t: ColorTokens, destructive: boolean) => TextStyle) | null;
83
+ /** iOS/web dim on press; Android uses a ripple instead (null). */
84
+ pressedOpacity: number | null;
85
+ /** Android ripple over the action cells; null on iOS/web. */
86
+ ripple: ((t: ColorTokens) => { color: string; borderless: boolean }) | null;
87
+ }
88
+
89
+ // --- shared layout fragments (identical across platforms) -------------------
90
+
91
+ // The outer wrapper aligns the dialog (and its optional trigger) to the start.
92
+ export const root: ViewStyle = { alignSelf: "flex-start" };
93
+
94
+ // Gap between the trigger button and the modal when the trigger renders above it.
95
+ export const triggerGap: ViewStyle = { marginTop: 12 };
96
+
97
+ // The card is full-width within its max-width; the per-platform skin supplies the
98
+ // shape/fill/padding/shadow on top of this.
99
+ export const cardBase: ViewStyle = { width: "100%" };
100
+
101
+ // ---------- Web: the established Canvas look (lifted verbatim) ----------
102
+ // The current alert-dialog: a contained dim scrim (rounded-md, black/50) centering
103
+ // a bordered card (rounded-md border bg-popover p-6 shadow-xl); a left-aligned
104
+ // 16pt/600 title and 14pt muted description; a right-aligned action row (gap-2,
105
+ // mt-6) of an outline Cancel Button plus a primary/destructive confirm Button.
106
+ export const webSkin: AlertDialogSkin = {
107
+ backdrop: {
108
+ alignItems: "center",
109
+ justifyContent: "center",
110
+ borderRadius: 8,
111
+ backgroundColor: alpha("#000000", 0.5),
112
+ padding: 32,
113
+ },
114
+ card: (t) => ({
115
+ borderRadius: 8,
116
+ borderWidth: 1,
117
+ borderColor: t.border,
118
+ backgroundColor: t.popover,
119
+ padding: 24,
120
+ ...shadow("xl"),
121
+ }),
122
+ title: (t) => ({ fontSize: 16, lineHeight: 24, fontWeight: "600", color: t["popover-foreground"] }),
123
+ description: (t) => ({ marginTop: 8, fontSize: 14, lineHeight: 20, color: t["muted-foreground"] }),
124
+ inputBlock: { marginTop: 16 },
125
+ inputLabel: (t) => ({ marginBottom: 6, fontSize: 14, lineHeight: 20, fontWeight: "500", color: t.foreground }),
126
+ actionLayout: "buttons",
127
+ actions: { flexDirection: "row", justifyContent: "flex-end", gap: 8, marginTop: 24 },
128
+ buttonSmall: true,
129
+ cancelButton: { outline: true },
130
+ capsuleRow: null,
131
+ capsuleCell: null,
132
+ cancelFill: null,
133
+ confirmFill: null,
134
+ cancelLabelStyle: null,
135
+ confirmLabelStyle: null,
136
+ pressedOpacity: null,
137
+ ripple: null,
138
+ };
139
+
140
+ // ---------- iOS 27 (Liquid Glass alert): rounded card, left text, capsule actions ----------
141
+ // iOS 26+/27 alert (per Apple's design kit): a rounded card (~28pt radius) over the
142
+ // `popover` fill with a soft shadow and NO border; a LEFT-aligned bold title and a
143
+ // LEFT-aligned `muted-foreground` message. The two actions are CAPSULES side by
144
+ // side (no hairline divider): a gray Cancel capsule (`secondary` fill,
145
+ // `secondary-foreground` text) and a filled Confirm capsule (`primary`, or
146
+ // `destructive` when destructive) with white/`*-foreground` label. The brand
147
+ // survives: the iOS system blue becomes the indigo `primary` token. Press = opacity
148
+ // dim (~0.85).
149
+ const IOS_RADIUS = 28;
150
+ const IOS_CAPSULE_RADIUS = 999;
151
+ export const iosSkin: AlertDialogSkin = {
152
+ backdrop: {
153
+ alignItems: "center",
154
+ justifyContent: "center",
155
+ borderRadius: 8,
156
+ backgroundColor: alpha("#000000", 0.4),
157
+ padding: 32,
158
+ },
159
+ // Rounded card with content padding; no border, soft shadow.
160
+ card: (t) => ({
161
+ borderRadius: IOS_RADIUS,
162
+ backgroundColor: t.popover,
163
+ padding: 24,
164
+ ...shadow("lg"),
165
+ }),
166
+ title: (t) => ({
167
+ fontSize: 20,
168
+ lineHeight: 25,
169
+ fontWeight: "700",
170
+ color: t["popover-foreground"],
171
+ }),
172
+ description: (t) => ({
173
+ marginTop: 8,
174
+ fontSize: 16,
175
+ lineHeight: 21,
176
+ color: t["muted-foreground"],
177
+ }),
178
+ inputBlock: { marginTop: 16 },
179
+ inputLabel: (t) => ({ marginBottom: 6, fontSize: 14, lineHeight: 18, fontWeight: "600", color: t.foreground }),
180
+ actionLayout: "capsule",
181
+ // Unused in the capsule layout, but the contract requires concrete values.
182
+ actions: { flexDirection: "row" },
183
+ buttonSmall: true,
184
+ cancelButton: {},
185
+ // Two equal-width capsules side by side, separated by a gap (no divider).
186
+ capsuleRow: { flexDirection: "row", gap: 12, marginTop: 24 },
187
+ capsuleCell: {
188
+ flex: 1,
189
+ alignItems: "center",
190
+ justifyContent: "center",
191
+ paddingVertical: 14,
192
+ paddingHorizontal: 16,
193
+ minHeight: 50,
194
+ borderRadius: IOS_CAPSULE_RADIUS,
195
+ },
196
+ cancelFill: (t) => ({ backgroundColor: t.secondary }),
197
+ confirmFill: (t, destructive) => ({ backgroundColor: destructive ? t.destructive : t.primary }),
198
+ cancelLabelStyle: (t) => ({ fontSize: 17, lineHeight: 22, fontWeight: "600", color: t["secondary-foreground"] }),
199
+ confirmLabelStyle: (t, destructive) => ({
200
+ fontSize: 17,
201
+ lineHeight: 22,
202
+ fontWeight: "700",
203
+ color: destructive ? t["destructive-foreground"] : t["primary-foreground"],
204
+ }),
205
+ pressedOpacity: 0.85,
206
+ ripple: null,
207
+ };
208
+
209
+ // ---------- Android (Material 3 AlertDialog): elevated surface, text buttons ----------
210
+ // M3 basic dialog: an elevated surface (28dp radius) over `popover` with a soft
211
+ // shadow; a LEFT-aligned 24sp/headline-small title and a left-aligned 14sp body in
212
+ // `muted-foreground`. Two M3 TEXT buttons sit bottom-right (Cancel then Confirm/
213
+ // Delete) — the confirm carries the brand `primary`, a destructive confirm the
214
+ // `destructive` red. The shell renders these with its ghost (text) Buttons; press =
215
+ // android_ripple, supplied here as the brand state layer.
216
+ export const androidSkin: AlertDialogSkin = {
217
+ backdrop: {
218
+ alignItems: "center",
219
+ justifyContent: "center",
220
+ borderRadius: 8,
221
+ backgroundColor: alpha("#000000", 0.32),
222
+ padding: 32,
223
+ },
224
+ card: (t) => ({
225
+ borderRadius: 28,
226
+ backgroundColor: t.popover,
227
+ padding: 24,
228
+ ...shadow("md"),
229
+ }),
230
+ title: (t) => ({ fontSize: 24, lineHeight: 32, fontWeight: "400", color: t["popover-foreground"] }),
231
+ description: (t) => ({ marginTop: 16, fontSize: 14, lineHeight: 20, color: t["muted-foreground"] }),
232
+ inputBlock: { marginTop: 16 },
233
+ inputLabel: (t) => ({ marginBottom: 6, fontSize: 14, lineHeight: 20, fontWeight: "500", color: t.foreground }),
234
+ actionLayout: "buttons",
235
+ // M3 places the action buttons bottom-right with 8dp gap and 24dp above.
236
+ actions: { flexDirection: "row", justifyContent: "flex-end", gap: 8, marginTop: 24 },
237
+ buttonSmall: true,
238
+ // M3 dialog actions are text (ghost) buttons, not outlined.
239
+ cancelButton: { ghost: true },
240
+ capsuleRow: null,
241
+ capsuleCell: null,
242
+ cancelFill: null,
243
+ confirmFill: null,
244
+ cancelLabelStyle: null,
245
+ confirmLabelStyle: null,
246
+ pressedOpacity: null,
247
+ ripple: (t) => ({ color: alpha(t.primary, 0.12), borderless: false }),
248
+ };
@@ -0,0 +1,6 @@
1
+ import { createAlertDialog } from "./alert-dialog.shared.js";
2
+ import { webSkin } from "./alert-dialog.styles.js";
3
+
4
+ // Web AlertDialog (the base; Metro falls back to it on native, web bundlers resolve it).
5
+ export const AlertDialog = createAlertDialog(webSkin);
6
+ export type { AlertDialogProps } from "./alert-dialog.shared.js";
@@ -0,0 +1,190 @@
1
+ # Cards
2
+
3
+ Three families. `StatCard` = a single metric, big number + delta. `SectionCard` = a labeled content surface with optional header and divider. Generic `card` = bring your own structure. Density: pass `compact` or `comfortable` to tighten or relax the card's own padding and the gap between flat children (`compact` takes precedence, and a density prop pads the surface on its own).
4
+
5
+ ## Usage
6
+
7
+ ```tsx
8
+ <Card style={{ width: 280, padding: 20 }}>
9
+ <View style={{ flexDirection: "row", alignItems: "flex-start", justifyContent: "space-between" }}>
10
+ <View>
11
+ <Text style={{ fontSize: 12, lineHeight: 16, fontWeight: "500", textTransform: "uppercase", letterSpacing: 0.4, color: tokens["muted-foreground"] }}>Active identities</Text>
12
+ <Text style={{ marginTop: 4, fontSize: 24, lineHeight: 32, fontWeight: "700", color: tokens["card-foreground"] }}>12,348</Text>
13
+ <Text style={{ marginTop: 2, fontSize: 11, color: tokens["muted-foreground"] }}>+142 today</Text>
14
+ </View>
15
+ <View style={{ height: 40, width: 40, alignItems: "center", justifyContent: "center", borderRadius: 8, backgroundColor: alpha(palette["blue-500"], 0.1), color: palette["blue-600"] }}>
16
+ <Text style={{ fontSize: 14, lineHeight: 20, fontWeight: "600", color: palette["blue-600"] }}>U</Text>
17
+ </View>
18
+ </View>
19
+ </Card>
20
+ ```
21
+
22
+ ## Variants
23
+
24
+ ### Type - section
25
+
26
+ ```tsx
27
+ <Card
28
+ title="Recent activity"
29
+ body="A labeled content surface. Drop fields, a list, or any module of content here."
30
+ />
31
+ ```
32
+
33
+ ### Type - generic
34
+
35
+ ```tsx
36
+ <Card
37
+ padded
38
+ title="Anything goes here"
39
+ body="The card surface gives you the border, radius, and shadow. You bring the content."
40
+ />
41
+ ```
42
+
43
+ ### Icon tone - success
44
+
45
+ ```tsx
46
+ <Card style={{ width: 280, padding: 20 }}>
47
+ <View style={{ flexDirection: "row", alignItems: "flex-start", justifyContent: "space-between" }}>
48
+ <View>
49
+ <Text style={{ fontSize: 12, lineHeight: 16, fontWeight: "500", textTransform: "uppercase", letterSpacing: 0.4, color: tokens["muted-foreground"] }}>Active identities</Text>
50
+ <Text style={{ marginTop: 4, fontSize: 24, lineHeight: 32, fontWeight: "700", color: tokens["card-foreground"] }}>12,348</Text>
51
+ <Text style={{ marginTop: 2, fontSize: 11, color: tokens["muted-foreground"] }}>+142 today</Text>
52
+ </View>
53
+ <View style={{ height: 40, width: 40, alignItems: "center", justifyContent: "center", borderRadius: 8, backgroundColor: alpha(palette["green-500"], 0.1), color: palette["green-600"] }}>
54
+ <Text style={{ fontSize: 14, lineHeight: 20, fontWeight: "600", color: palette["green-600"] }}>S</Text>
55
+ </View>
56
+ </View>
57
+ </Card>
58
+ ```
59
+
60
+ ### Icon tone - purple
61
+
62
+ ```tsx
63
+ <Card style={{ width: 280, padding: 20 }}>
64
+ <View style={{ flexDirection: "row", alignItems: "flex-start", justifyContent: "space-between" }}>
65
+ <View>
66
+ <Text style={{ fontSize: 12, lineHeight: 16, fontWeight: "500", textTransform: "uppercase", letterSpacing: 0.4, color: tokens["muted-foreground"] }}>Active identities</Text>
67
+ <Text style={{ marginTop: 4, fontSize: 24, lineHeight: 32, fontWeight: "700", color: tokens["card-foreground"] }}>12,348</Text>
68
+ <Text style={{ marginTop: 2, fontSize: 11, color: tokens["muted-foreground"] }}>+142 today</Text>
69
+ </View>
70
+ <View style={{ height: 40, width: 40, alignItems: "center", justifyContent: "center", borderRadius: 8, backgroundColor: alpha(palette["purple-500"], 0.1), color: palette["purple-600"] }}>
71
+ <Text style={{ fontSize: 14, lineHeight: 20, fontWeight: "600", color: palette["purple-600"] }}>O</Text>
72
+ </View>
73
+ </View>
74
+ </Card>
75
+ ```
76
+
77
+ ### Icon tone - destructive
78
+
79
+ ```tsx
80
+ <Card style={{ width: 280, padding: 20 }}>
81
+ <View style={{ flexDirection: "row", alignItems: "flex-start", justifyContent: "space-between" }}>
82
+ <View>
83
+ <Text style={{ fontSize: 12, lineHeight: 16, fontWeight: "500", textTransform: "uppercase", letterSpacing: 0.4, color: tokens["muted-foreground"] }}>Active identities</Text>
84
+ <Text style={{ marginTop: 4, fontSize: 24, lineHeight: 32, fontWeight: "700", color: tokens["card-foreground"] }}>12,348</Text>
85
+ <Text style={{ marginTop: 2, fontSize: 11, color: tokens["muted-foreground"] }}>+142 today</Text>
86
+ </View>
87
+ <View style={{ height: 40, width: 40, alignItems: "center", justifyContent: "center", borderRadius: 8, backgroundColor: alpha(tokens.destructive, 0.1), color: tokens.destructive }}>
88
+ <Text style={{ fontSize: 14, lineHeight: 20, fontWeight: "600", color: tokens.destructive }}>!</Text>
89
+ </View>
90
+ </View>
91
+ </Card>
92
+ ```
93
+
94
+ ### Icon tone - amber
95
+
96
+ ```tsx
97
+ <Card style={{ width: 280, padding: 20 }}>
98
+ <View style={{ flexDirection: "row", alignItems: "flex-start", justifyContent: "space-between" }}>
99
+ <View>
100
+ <Text style={{ fontSize: 12, lineHeight: 16, fontWeight: "500", textTransform: "uppercase", letterSpacing: 0.4, color: tokens["muted-foreground"] }}>Active identities</Text>
101
+ <Text style={{ marginTop: 4, fontSize: 24, lineHeight: 32, fontWeight: "700", color: tokens["card-foreground"] }}>12,348</Text>
102
+ <Text style={{ marginTop: 2, fontSize: 11, color: tokens["muted-foreground"] }}>+142 today</Text>
103
+ </View>
104
+ <View style={{ height: 40, width: 40, alignItems: "center", justifyContent: "center", borderRadius: 8, backgroundColor: alpha(palette["amber-500"], 0.1), color: palette["amber-600"] }}>
105
+ <Text style={{ fontSize: 14, lineHeight: 20, fontWeight: "600", color: palette["amber-600"] }}>T</Text>
106
+ </View>
107
+ </View>
108
+ </Card>
109
+ ```
110
+
111
+ ## Do & Don't
112
+
113
+ ### stat
114
+
115
+ **Do** — One big number, a short label, a small delta. The metric is scannable in a glance.
116
+
117
+ ```tsx
118
+ <Card padded style={{ maxWidth: 280 }}>
119
+ <View style={{ flexDirection: "row", alignItems: "flex-start", justifyContent: "space-between" }}>
120
+ <View>
121
+ <Text style={{ fontSize: 12, lineHeight: 16, fontWeight: "500", textTransform: "uppercase", letterSpacing: 0.4, color: tokens["muted-foreground"] }}>Active identities</Text>
122
+ <Text style={{ marginTop: 4, fontSize: 24, lineHeight: 32, fontWeight: "700", color: tokens["card-foreground"] }}>12,348</Text>
123
+ <Text style={{ marginTop: 2, fontSize: 11, color: tokens["muted-foreground"] }}>+142 today</Text>
124
+ </View>
125
+ <View style={{ height: 40, width: 40, alignItems: "center", justifyContent: "center", borderRadius: 8, backgroundColor: alpha(palette["blue-500"], 0.1) }}>
126
+ <Text style={{ fontSize: 14, lineHeight: 20, fontWeight: "600", color: palette["blue-600"] }}>U</Text>
127
+ </View>
128
+ </View>
129
+ </Card>
130
+ ```
131
+
132
+ **Don't** — Prose where the number should be: the eye has nothing big to land on, so the card stops being a stat.
133
+
134
+ ```tsx
135
+ <Card padded style={{ maxWidth: 280 }}>
136
+ <Text style={{ fontSize: 12, lineHeight: 16, fontWeight: "500", textTransform: "uppercase", letterSpacing: 0.4, color: tokens["muted-foreground"] }}>This month</Text>
137
+ <Text style={{ marginTop: 4, fontSize: 14, lineHeight: 20, fontWeight: "500", color: tokens["card-foreground"] }}>We onboarded 12,348 active identities, up 142 today, with churn holding steady.</Text>
138
+ </Card>
139
+ ```
140
+
141
+ ### section
142
+
143
+ **Do** — Keep the divider between header and body; it anchors the title.
144
+
145
+ ```tsx
146
+ <Card style={{ maxWidth: 360 }}>
147
+ <CardHeader>
148
+ <CardTitle>Recent activity</CardTitle>
149
+ </CardHeader>
150
+ <CardSeparator />
151
+ <CardContent>
152
+ <Text style={{ fontSize: 14, lineHeight: 20, color: tokens["card-foreground"] }}>Two events today.</Text>
153
+ </CardContent>
154
+ </Card>
155
+ ```
156
+
157
+ **Don't** — Without the divider the header floats and stops reading as a header.
158
+
159
+ ```tsx
160
+ <Card style={{ maxWidth: 360 }}>
161
+ <CardHeader>
162
+ <CardTitle>Recent activity</CardTitle>
163
+ </CardHeader>
164
+ <CardContent>
165
+ <Text style={{ fontSize: 14, lineHeight: 20, color: tokens["card-foreground"] }}>Two events today.</Text>
166
+ </CardContent>
167
+ </Card>
168
+ ```
169
+
170
+ ### generic
171
+
172
+ **Do** — Use the surface once and layout the content with plain spacing inside it.
173
+
174
+ ```tsx
175
+ <Card padded style={{ maxWidth: 360 }}>
176
+ <Text style={{ marginBottom: 4, fontSize: 15, fontWeight: "600", color: tokens["card-foreground"] }}>Anything goes here</Text>
177
+ <Text style={{ fontSize: 14, lineHeight: 20, color: tokens["muted-foreground"] }}>The card surface gives you the border, radius, and shadow. You bring the content.</Text>
178
+ </Card>
179
+ ```
180
+
181
+ **Don't** — Nesting one card surface inside another stacks border on border and shadow on shadow; the inner block looks dropped in.
182
+
183
+ ```tsx
184
+ <Card padded style={{ maxWidth: 360 }}>
185
+ <Card padded>
186
+ <Text style={{ marginBottom: 4, fontSize: 15, fontWeight: "600", color: tokens["card-foreground"] }}>Nested surface</Text>
187
+ <Text style={{ fontSize: 14, lineHeight: 20, color: tokens["muted-foreground"] }}>A card inside a card doubles the border and shadow.</Text>
188
+ </Card>
189
+ </Card>
190
+ ```