@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,166 @@
1
+ # Stats
2
+
3
+ Single value, grouped row, with sparkline, with comparison. Used for dashboards and overview pages.
4
+
5
+ ## Usage
6
+
7
+ ```tsx
8
+ <Stats
9
+ items={[
10
+ { label: "Active users", value: "71,897", delta: "+12.3% vs. last 30 days" }
11
+ ]}
12
+ />
13
+ ```
14
+
15
+ ## Variants
16
+
17
+ ### Variant - group
18
+
19
+ ```tsx
20
+ <Stats
21
+ items={[
22
+ { label: "Total users", value: "12,847", delta: "+12.5%" },
23
+ { label: "Active sessions", value: "1,024", delta: "+3.2%" },
24
+ { label: "Error rate", value: "0.12%", delta: "+0.03%", down: true }
25
+ ]}
26
+ />
27
+ ```
28
+
29
+ ### Variant - plain
30
+
31
+ ```tsx
32
+ <Stats
33
+ plain
34
+ title="Key metrics"
35
+ items={[
36
+ { label: "Revenue", value: "$48.2k" },
37
+ { label: "Orders", value: "842" },
38
+ { label: "Avg. value", value: "$57.24" },
39
+ { label: "Conversion", value: "3.6%" }
40
+ ]}
41
+ />
42
+ ```
43
+
44
+ ### Variant - sparkline
45
+
46
+ ```tsx
47
+ <Stats
48
+ items={[
49
+ { label: "Requests", value: "24.5k", delta: "+8.2%" },
50
+ { label: "Latency", value: "142ms", delta: "+12ms", down: true }
51
+ ]}
52
+ />
53
+ ```
54
+
55
+ ## Do & Don't
56
+
57
+ ### Single
58
+
59
+ **Do** — Name the comparison and the period so the delta is unambiguous.
60
+
61
+ ```tsx
62
+ <Stats style={{ maxWidth: 280 }} items={[
63
+ { label: "Active users", value: "71,897", delta: "+12.3% vs. last 30 days" }
64
+ ]} />
65
+ ```
66
+
67
+ **Don't** — A bare delta with no baseline leaves the reader asking: up against what, and over what window?
68
+
69
+ ```tsx
70
+ <Stats style={{ maxWidth: 280 }} items={[
71
+ { label: "Active users", value: "71,897", delta: "+12.3%" }
72
+ ]} />
73
+ ```
74
+
75
+ ### Group
76
+
77
+ **Do** — Use the auto-fit grid and round headline numbers so cards wrap and stay scannable.
78
+
79
+ ```tsx
80
+ <Stats items={[
81
+ { label: "Revenue", value: "$48.2k", delta: "+12.5%" },
82
+ { label: "Orders", value: "842", delta: "+3.2%" },
83
+ { label: "Conversion", value: "3.6%", delta: "+0.4%" }
84
+ ]} />
85
+ ```
86
+
87
+ **Don't** — A fixed flex row of full-precision numbers overflows on narrow viewports and crowds the cards.
88
+
89
+ ```tsx
90
+ <Stats items={[
91
+ { label: "Revenue", value: "$48,250.00", delta: "+12.5%" },
92
+ { label: "Orders", value: "842", delta: "+3.2%" },
93
+ { label: "Conversion", value: "3.6%", delta: "+0.4%" }
94
+ ]} />
95
+ ```
96
+
97
+ ### Plain (no border)
98
+
99
+ **Do** — On a parent surface drop the border and radius; let the number stacks stand on their own.
100
+
101
+ ```tsx
102
+ <Stats plain items={[
103
+ { label: "Revenue", value: "$48.2k" },
104
+ { label: "Orders", value: "842" }
105
+ ]} />
106
+ ```
107
+
108
+ **Don't** — Bordered cards inside a card surface double the chrome: a box drawn around boxes.
109
+
110
+ ```tsx
111
+ <View style={{ borderRadius: 8, borderWidth: 1, borderColor: tokens.border, backgroundColor: tokens.card, ...shadow("sm"), padding: 24 }}>
112
+ <Stats items={[
113
+ { label: "Revenue", value: "$48.2k" },
114
+ { label: "Orders", value: "842" }
115
+ ]} />
116
+ </View>
117
+ ```
118
+
119
+ ### With sparkline
120
+
121
+ **Do** — Pair the sparkline with an explicit delta so the headline reads without decoding the curve.
122
+
123
+ ```tsx
124
+ <View style={{ borderRadius: 8, borderWidth: 1, borderColor: tokens.border, backgroundColor: tokens.card, ...shadow("sm"), maxWidth: 220, padding: 20 }}>
125
+ <Text style={{ fontSize: 12, lineHeight: 16, color: tokens["muted-foreground"] }}>Requests</Text>
126
+ <View style={{ marginTop: 4, flexDirection: "row", alignItems: "baseline", justifyContent: "space-between" }}>
127
+ <Text style={{ fontSize: 24, lineHeight: 32, fontWeight: "600", letterSpacing: -0.4, color: tokens.foreground }}>24.5k</Text>
128
+ <Text style={{ fontFamily: "monospace", fontSize: 11, color: palette["emerald-600"] }}>+8.2%</Text>
129
+ </View>
130
+ <View style={{ marginTop: 12, flexDirection: "row", alignItems: "flex-end", gap: 2, height: 24 }}>
131
+ <View style={{ flexGrow: 1, flexShrink: 1, flexBasis: "0%", borderRadius: 2, backgroundColor: alpha(tokens.primary, 0.7), height: 4 }} />
132
+ <View style={{ flexGrow: 1, flexShrink: 1, flexBasis: "0%", borderRadius: 2, backgroundColor: alpha(tokens.primary, 0.7), height: 8 }} />
133
+ <View style={{ flexGrow: 1, flexShrink: 1, flexBasis: "0%", borderRadius: 2, backgroundColor: alpha(tokens.primary, 0.7), height: 6 }} />
134
+ <View style={{ flexGrow: 1, flexShrink: 1, flexBasis: "0%", borderRadius: 2, backgroundColor: alpha(tokens.primary, 0.7), height: 12 }} />
135
+ <View style={{ flexGrow: 1, flexShrink: 1, flexBasis: "0%", borderRadius: 2, backgroundColor: alpha(tokens.primary, 0.7), height: 10 }} />
136
+ <View style={{ flexGrow: 1, flexShrink: 1, flexBasis: "0%", borderRadius: 2, backgroundColor: alpha(tokens.primary, 0.7), height: 16 }} />
137
+ <View style={{ flexGrow: 1, flexShrink: 1, flexBasis: "0%", borderRadius: 2, backgroundColor: alpha(tokens.primary, 0.7), height: 14 }} />
138
+ <View style={{ flexGrow: 1, flexShrink: 1, flexBasis: "0%", borderRadius: 2, backgroundColor: alpha(tokens.primary, 0.7), height: 18 }} />
139
+ <View style={{ flexGrow: 1, flexShrink: 1, flexBasis: "0%", borderRadius: 2, backgroundColor: alpha(tokens.primary, 0.7), height: 16 }} />
140
+ <View style={{ flexGrow: 1, flexShrink: 1, flexBasis: "0%", borderRadius: 2, backgroundColor: alpha(tokens.primary, 0.7), height: 20 }} />
141
+ <View style={{ flexGrow: 1, flexShrink: 1, flexBasis: "0%", borderRadius: 2, backgroundColor: alpha(tokens.primary, 0.7), height: 24 }} />
142
+ </View>
143
+ </View>
144
+ ```
145
+
146
+ **Don't** — A trend line with no current delta makes you eyeball the slope to guess the direction.
147
+
148
+ ```tsx
149
+ <View style={{ borderRadius: 8, borderWidth: 1, borderColor: tokens.border, backgroundColor: tokens.card, ...shadow("sm"), maxWidth: 220, padding: 20 }}>
150
+ <Text style={{ fontSize: 12, lineHeight: 16, color: tokens["muted-foreground"] }}>Requests</Text>
151
+ <Text style={{ marginTop: 4, fontSize: 24, lineHeight: 32, fontWeight: "600", letterSpacing: -0.4, color: tokens.foreground }}>24.5k</Text>
152
+ <View style={{ marginTop: 12, flexDirection: "row", alignItems: "flex-end", gap: 2, height: 24 }}>
153
+ <View style={{ flexGrow: 1, flexShrink: 1, flexBasis: "0%", borderRadius: 2, backgroundColor: alpha(tokens.primary, 0.7), height: 4 }} />
154
+ <View style={{ flexGrow: 1, flexShrink: 1, flexBasis: "0%", borderRadius: 2, backgroundColor: alpha(tokens.primary, 0.7), height: 8 }} />
155
+ <View style={{ flexGrow: 1, flexShrink: 1, flexBasis: "0%", borderRadius: 2, backgroundColor: alpha(tokens.primary, 0.7), height: 6 }} />
156
+ <View style={{ flexGrow: 1, flexShrink: 1, flexBasis: "0%", borderRadius: 2, backgroundColor: alpha(tokens.primary, 0.7), height: 12 }} />
157
+ <View style={{ flexGrow: 1, flexShrink: 1, flexBasis: "0%", borderRadius: 2, backgroundColor: alpha(tokens.primary, 0.7), height: 10 }} />
158
+ <View style={{ flexGrow: 1, flexShrink: 1, flexBasis: "0%", borderRadius: 2, backgroundColor: alpha(tokens.primary, 0.7), height: 16 }} />
159
+ <View style={{ flexGrow: 1, flexShrink: 1, flexBasis: "0%", borderRadius: 2, backgroundColor: alpha(tokens.primary, 0.7), height: 14 }} />
160
+ <View style={{ flexGrow: 1, flexShrink: 1, flexBasis: "0%", borderRadius: 2, backgroundColor: alpha(tokens.primary, 0.7), height: 18 }} />
161
+ <View style={{ flexGrow: 1, flexShrink: 1, flexBasis: "0%", borderRadius: 2, backgroundColor: alpha(tokens.primary, 0.7), height: 16 }} />
162
+ <View style={{ flexGrow: 1, flexShrink: 1, flexBasis: "0%", borderRadius: 2, backgroundColor: alpha(tokens.primary, 0.7), height: 20 }} />
163
+ <View style={{ flexGrow: 1, flexShrink: 1, flexBasis: "0%", borderRadius: 2, backgroundColor: alpha(tokens.primary, 0.7), height: 24 }} />
164
+ </View>
165
+ </View>
166
+ ```
@@ -0,0 +1,91 @@
1
+ import { type ViewStyle, type TextStyle } from "react-native";
2
+ import { type ColorTokens, palette, shadow } from "../../style/index.js";
3
+
4
+ // Co-located Stats styles. Layout-only fragments are static objects; anything
5
+ // that reads a color is a function of the active tokens (so the surfaces follow
6
+ // light/dark and read as glass when the ThemeProvider's surface is "glass",
7
+ // since tokens.card is swapped translucent at the theming level). The delta tone
8
+ // uses the Tailwind palette (a 600 hue in light, a 400 hue in dark), branched on
9
+ // the active scheme.
10
+
11
+ export type Surface = "card" | "plain";
12
+
13
+ // The bordered card surface, mirroring the docs `cardCls` plus its padding
14
+ // (rounded-lg border border-border bg-card shadow-sm p-5).
15
+ export function cardSurface(tokens: ColorTokens): ViewStyle {
16
+ return {
17
+ borderRadius: 8,
18
+ borderWidth: 1,
19
+ borderColor: tokens.border,
20
+ backgroundColor: tokens.card,
21
+ padding: 20,
22
+ ...shadow("sm"),
23
+ };
24
+ }
25
+
26
+ // The parent surface the plain variant nests on
27
+ // (rounded-lg border border-border bg-card shadow-sm p-6).
28
+ export function plainContainer(tokens: ColorTokens): ViewStyle {
29
+ return {
30
+ borderRadius: 8,
31
+ borderWidth: 1,
32
+ borderColor: tokens.border,
33
+ backgroundColor: tokens.card,
34
+ padding: 24,
35
+ ...shadow("sm"),
36
+ };
37
+ }
38
+
39
+ // A wrapping row: items flow left to right and wrap to the next line when they
40
+ // run out of room (flex-row flex-wrap).
41
+ export const row: ViewStyle = { flexDirection: "row", flexWrap: "wrap" };
42
+
43
+ // Row gap by surface: plain stacks sit roomier (gap-6), cards pack tighter
44
+ // (gap-3.5).
45
+ export const rowGap: Record<Surface, ViewStyle> = {
46
+ card: { gap: 14 },
47
+ plain: { gap: 24 },
48
+ };
49
+
50
+ // Per-item minimum width and growth, by surface. Card items hold a wider floor
51
+ // and grow (flex-1 min-w-48); plain stacks pack tighter (min-w-28).
52
+ export const item: Record<Surface, ViewStyle> = {
53
+ card: { flexGrow: 1, flexShrink: 1, flexBasis: "0%", minWidth: 192 },
54
+ plain: { minWidth: 112 },
55
+ };
56
+
57
+ // Title type by surface (card: text-sm mb-3, plain: text-base mb-4).
58
+ export function title(tokens: ColorTokens, surface: Surface): TextStyle {
59
+ return surface === "card"
60
+ ? { fontSize: 14, lineHeight: 20, fontWeight: "600", color: tokens.foreground, marginBottom: 12 }
61
+ : { fontSize: 16, lineHeight: 24, fontWeight: "600", color: tokens.foreground, marginBottom: 16 };
62
+ }
63
+
64
+ // --- one metric: label, value, optional delta -------------------------------
65
+
66
+ // text-sm text-muted-foreground
67
+ export function labelText(tokens: ColorTokens): TextStyle {
68
+ return { fontSize: 14, lineHeight: 20, color: tokens["muted-foreground"] };
69
+ }
70
+
71
+ // mt-1 text-2xl font-semibold tracking-tight text-foreground
72
+ export function valueText(tokens: ColorTokens): TextStyle {
73
+ return {
74
+ marginTop: 4,
75
+ fontSize: 24,
76
+ lineHeight: 32,
77
+ fontWeight: "600",
78
+ letterSpacing: -0.4,
79
+ color: tokens.foreground,
80
+ };
81
+ }
82
+
83
+ // mt-1 text-xs font-medium, with the tone color applied on top.
84
+ export const deltaBase: TextStyle = { marginTop: 4, fontSize: 12, lineHeight: 16, fontWeight: "500" };
85
+
86
+ // Delta tone: a rise reads green (text-green-600 dark:text-green-400), a decline
87
+ // reads red (text-red-600 dark:text-red-400).
88
+ export function deltaTone(dark: boolean, down: boolean): TextStyle {
89
+ const hue = down ? "red" : "green";
90
+ return { color: dark ? palette[`${hue}-400`] : palette[`${hue}-600`] };
91
+ }
@@ -0,0 +1,88 @@
1
+ import { type ReactNode } from "react";
2
+ import { View, Text, useTheme, type StyleProp, type ViewStyle } from "../../style/index.js";
3
+ import * as s from "./stats.styles.js";
4
+ import { type Surface } from "./stats.styles.js";
5
+
6
+ // Stats: a row/grid of metric items, each a small label, a large headline value,
7
+ // and an optional delta (e.g. +12.5% in green, -3% in red). Used for dashboard
8
+ // and overview surfaces.
9
+ //
10
+ // Two surfaces, one axis (`plain` vs the default card):
11
+ //
12
+ // 1. Card (default): each metric sits in its own bordered, shadowed card. Good
13
+ // for a standalone metric or a free-standing group on a bare page.
14
+ // 2. Plain (`plain`): the metric stacks sit borderless on a shared parent
15
+ // surface, so you don't draw boxes inside a box. Pair with a parent card.
16
+ //
17
+ // Layout is responsive by default: items lay out in a wrapping row, each holding
18
+ // a sensible minimum width, so a group reflows from a single row on desktop down
19
+ // to a stack on a phone without any per-breakpoint props.
20
+ //
21
+ // Boolean-prop API: one boolean per option, first-match precedence within an
22
+ // axis (mirrors Button's intentOf). `items` carries the plain data; each item's
23
+ // `down` flag colors its delta red instead of the default green.
24
+
25
+ export interface StatItem {
26
+ /** Small caption above the value (e.g. "Total users"). */
27
+ label: string;
28
+ /** The headline figure, pre-formatted (e.g. "12,847", "$48.2k", "3.6%"). */
29
+ value: string;
30
+ /** Optional change indicator (e.g. "+12.5%"). Omit to hide. */
31
+ delta?: string;
32
+ /** Color the delta red (a decline) instead of the default green (a rise). */
33
+ down?: boolean;
34
+ }
35
+
36
+ export interface StatsProps {
37
+ /** The metrics to render, in order. */
38
+ items: StatItem[];
39
+ // Surface (pick one; default is bordered cards).
40
+ /** Borderless metric stacks on a shared surface, for nesting inside a card. */
41
+ plain?: boolean;
42
+ /** Optional heading shown above the metrics (mainly for the plain surface). */
43
+ title?: string;
44
+ /** Escape hatch for layout/positioning composition (mainly width). */
45
+ style?: StyleProp<ViewStyle>;
46
+ }
47
+
48
+ // Surface precedence when more than one is passed: first match wins.
49
+ function surfaceOf(p: StatsProps): Surface {
50
+ if (p.plain) return "plain";
51
+ return "card";
52
+ }
53
+
54
+ // One metric: label, value, optional delta.
55
+ function StatItemView({ item, surface }: { item: StatItem; surface: Surface }): ReactNode {
56
+ const { tokens, dark } = useTheme();
57
+ return (
58
+ <View style={[surface === "card" ? s.cardSurface(tokens) : null, s.item[surface]]}>
59
+ <Text style={s.labelText(tokens)}>{item.label}</Text>
60
+ <Text style={s.valueText(tokens)}>{item.value}</Text>
61
+ {item.delta != null && item.delta !== "" ? (
62
+ <Text style={[s.deltaBase, s.deltaTone(dark, !!item.down)]}>{item.delta}</Text>
63
+ ) : null}
64
+ </View>
65
+ );
66
+ }
67
+
68
+ export function Stats(props: StatsProps) {
69
+ const { items, title, style } = props;
70
+ const { tokens } = useTheme();
71
+ const surface = surfaceOf(props);
72
+
73
+ // The card surface lays cards directly in a wrapping, gapped row. The plain
74
+ // surface wraps the stacks in a shared parent card so the borderless metrics
75
+ // have something to sit on.
76
+ const isPlain = surface === "plain";
77
+
78
+ return (
79
+ <View style={[isPlain ? s.plainContainer(tokens) : null, style]}>
80
+ {title != null && title !== "" ? <Text style={s.title(tokens, surface)}>{title}</Text> : null}
81
+ <View style={[s.row, s.rowGap[surface]]}>
82
+ {items.map((item, i) => (
83
+ <StatItemView key={i} item={item} surface={surface} />
84
+ ))}
85
+ </View>
86
+ </View>
87
+ );
88
+ }
@@ -0,0 +1,6 @@
1
+ import { createCalendar } from "./calendar.shared.js";
2
+ import { androidSkin } from "./calendar.styles.js";
3
+
4
+ // Material 3 date picker Calendar. Metro resolves this file on Android; the docs import it for preview.
5
+ export const Calendar = createCalendar(androidSkin);
6
+ export type { CalendarProps } from "./calendar.shared.js";
@@ -0,0 +1,6 @@
1
+ import { createCalendar } from "./calendar.shared.js";
2
+ import { iosSkin } from "./calendar.styles.js";
3
+
4
+ // iOS (HIG date picker) Calendar. Metro resolves this file on iOS; the docs import it for preview.
5
+ export const Calendar = createCalendar(iosSkin);
6
+ export type { CalendarProps } from "./calendar.shared.js";
@@ -0,0 +1,114 @@
1
+ # Calendars
2
+
3
+ Date picker, event list. Production: wrap react-day-picker.
4
+
5
+ ## Usage
6
+
7
+ ```tsx
8
+ <Calendar
9
+ month="May 2026"
10
+ today={23}
11
+ selected={24}
12
+ daysInMonth={31}
13
+ startWeekday={4}
14
+ />
15
+ ```
16
+
17
+ ## Variants
18
+
19
+ ### Variant - events
20
+
21
+ ```tsx
22
+ <View style={{ flexDirection: "row", flexWrap: "wrap", alignItems: "flex-start", gap: 24 }}>
23
+ <Calendar month="May 2026" today={23} selected={24} daysInMonth={31} startWeekday={4} />
24
+ <Card style={{ minWidth: 240, flexGrow: 1, flexShrink: 1, flexBasis: "0%" }}>
25
+ <View style={{ borderBottomWidth: 1, borderColor: tokens.border, paddingHorizontal: 20, paddingVertical: 12 }}>
26
+ <Text style={{ fontSize: 14, lineHeight: 20, fontWeight: "600", color: tokens["card-foreground"] }}>May 24</Text>
27
+ </View>
28
+ <View style={{ flexDirection: "row", alignItems: "center", justifyContent: "space-between", paddingHorizontal: 16, paddingVertical: 10, borderBottomWidth: 1, borderColor: tokens.border }}>
29
+ <Text style={{ fontSize: 14, lineHeight: 20, fontWeight: "500", color: tokens.foreground }}>Sprint planning</Text>
30
+ <Text style={{ fontSize: 14, lineHeight: 20, color: tokens["muted-foreground"] }}>9:00 AM</Text>
31
+ </View>
32
+ <View style={{ flexDirection: "row", alignItems: "center", justifyContent: "space-between", paddingHorizontal: 16, paddingVertical: 10, borderBottomWidth: 1, borderColor: tokens.border }}>
33
+ <Text style={{ fontSize: 14, lineHeight: 20, fontWeight: "500", color: tokens.foreground }}>Design review</Text>
34
+ <Text style={{ fontSize: 14, lineHeight: 20, color: tokens["muted-foreground"] }}>11:30 AM</Text>
35
+ </View>
36
+ <View style={{ flexDirection: "row", alignItems: "center", justifyContent: "space-between", paddingHorizontal: 16, paddingVertical: 10 }}>
37
+ <Text style={{ fontSize: 14, lineHeight: 20, fontWeight: "500", color: tokens.foreground }}>1:1 with manager</Text>
38
+ <Text style={{ fontSize: 14, lineHeight: 20, color: tokens["muted-foreground"] }}>2:00 PM</Text>
39
+ </View>
40
+ </Card>
41
+ </View>
42
+ ```
43
+
44
+ ## Do & Don't
45
+
46
+ ### Single date
47
+
48
+ **Do** — Exactly one selected day (primary), with today marked separately in the accent tone.
49
+
50
+ ```tsx
51
+ <View style={{ width: "auto", borderRadius: 8, borderWidth: 1, borderColor: tokens.border, padding: 12 }}>
52
+ <View style={{ flexDirection: "row", gap: 2 }}>
53
+ <Pressable style={{ height: 36, width: 36, alignItems: "center", justifyContent: "center", borderRadius: 6 }}>
54
+ <Text style={{ fontSize: 14, lineHeight: 20, color: tokens.foreground }}>8</Text>
55
+ </Pressable>
56
+ <Pressable style={{ height: 36, width: 36, alignItems: "center", justifyContent: "center", borderRadius: 6, backgroundColor: tokens.accent }}>
57
+ <Text style={{ fontSize: 14, lineHeight: 20, fontWeight: "500", color: tokens["accent-foreground"] }}>23</Text>
58
+ </Pressable>
59
+ <Pressable style={{ height: 36, width: 36, alignItems: "center", justifyContent: "center", borderRadius: 6, backgroundColor: tokens.primary }}>
60
+ <Text style={{ fontSize: 14, lineHeight: 20, color: tokens["primary-foreground"] }}>24</Text>
61
+ </Pressable>
62
+ </View>
63
+ </View>
64
+ ```
65
+
66
+ **Don't** — Painting several days with the primary selected style makes a single-date picker look like a multi-select.
67
+
68
+ ```tsx
69
+ <View style={{ width: "auto", borderRadius: 8, borderWidth: 1, borderColor: tokens.border, padding: 12 }}>
70
+ <View style={{ flexDirection: "row", gap: 2 }}>
71
+ <Pressable style={{ height: 36, width: 36, alignItems: "center", justifyContent: "center", borderRadius: 6, backgroundColor: tokens.primary }}>
72
+ <Text style={{ fontSize: 14, lineHeight: 20, color: tokens["primary-foreground"] }}>8</Text>
73
+ </Pressable>
74
+ <Pressable style={{ height: 36, width: 36, alignItems: "center", justifyContent: "center", borderRadius: 6, backgroundColor: tokens.primary }}>
75
+ <Text style={{ fontSize: 14, lineHeight: 20, color: tokens["primary-foreground"] }}>14</Text>
76
+ </Pressable>
77
+ <Pressable style={{ height: 36, width: 36, alignItems: "center", justifyContent: "center", borderRadius: 6, backgroundColor: tokens.primary }}>
78
+ <Text style={{ fontSize: 14, lineHeight: 20, color: tokens["primary-foreground"] }}>23</Text>
79
+ </Pressable>
80
+ </View>
81
+ </View>
82
+ ```
83
+
84
+ ### With event list
85
+
86
+ **Do** — Keep the panel header and rows in sync with the selected day so the two views always agree.
87
+
88
+ ```tsx
89
+ <View style={{ flexDirection: "row", flexWrap: "wrap", alignItems: "flex-start", gap: 24 }}>
90
+ <Calendar month="May 2026" today={23} selected={24} daysInMonth={31} startWeekday={4} />
91
+ <Card style={{ minWidth: 240, flexGrow: 1, flexShrink: 1, flexBasis: "0%" }}>
92
+ <View style={{ borderBottomWidth: 1, borderColor: tokens.border, paddingHorizontal: 20, paddingVertical: 12 }}>
93
+ <Text style={{ fontSize: 14, lineHeight: 20, fontWeight: "600", color: tokens["card-foreground"] }}>May 24</Text>
94
+ </View>
95
+ <View style={{ flexDirection: "row", alignItems: "center", justifyContent: "space-between", paddingHorizontal: 16, paddingVertical: 10 }}>
96
+ <Text style={{ fontSize: 14, lineHeight: 20, fontWeight: "500", color: tokens.foreground }}>Sprint planning</Text>
97
+ <Text style={{ fontSize: 14, lineHeight: 20, color: tokens["muted-foreground"] }}>9:00 AM</Text>
98
+ </View>
99
+ </Card>
100
+ </View>
101
+ ```
102
+
103
+ **Don't** — Selecting May 24 but leaving the panel on a placeholder breaks the link between the grid and its day.
104
+
105
+ ```tsx
106
+ <View style={{ flexDirection: "row", flexWrap: "wrap", alignItems: "flex-start", gap: 24 }}>
107
+ <Calendar month="May 2026" today={23} selected={24} daysInMonth={31} startWeekday={4} />
108
+ <Card style={{ minWidth: 240, flexGrow: 1, flexShrink: 1, flexBasis: "0%" }}>
109
+ <View style={{ paddingHorizontal: 16, paddingVertical: 12 }}>
110
+ <Text style={{ fontSize: 14, lineHeight: 20, color: tokens["muted-foreground"] }}>Pick a date to see events.</Text>
111
+ </View>
112
+ </Card>
113
+ </View>
114
+ ```
@@ -0,0 +1,146 @@
1
+ import { type GestureResponderEvent } from "react-native";
2
+ import { View, Pressable, Text, useTheme, type StyleProp, type ViewStyle } from "../../style/index.js";
3
+ import { type CalendarSkin, type Density } from "./calendar.styles.js";
4
+
5
+ // Shared Calendar shell. The structure (header with prev/next chevrons + month
6
+ // label, a weekday label row, and a 6x7 grid of day cells), the density
7
+ // precedence, the leading-blank padding, the accessibility, and the press
8
+ // handlers live here once; a platform file supplies only its skin (the native
9
+ // container/header/cell shape, weekday-label style, selected/today day treatment,
10
+ // and press feedback) and calls createCalendar.
11
+ //
12
+ // A month calendar: a header (month/year label flanked by prev/next chevrons),
13
+ // a weekday label row, and a 6x7 grid of day cells. Today and the selected day
14
+ // are highlighted; leading blanks pad the first row to the correct weekday.
15
+ //
16
+ // There is no CSS grid, so the day grid is a `flex-row flex-wrap` of fixed-width
17
+ // cells. Seven cells per row times the cell width gives the grid a fixed width,
18
+ // set explicitly so wrapping lands exactly seven-per-row (width supplied by the
19
+ // skin's per-density metrics).
20
+
21
+ export interface CalendarProps {
22
+ /** Month + year label shown in the header, e.g. "June 2026". */
23
+ month?: string;
24
+ /** The day number currently selected (primary highlight). */
25
+ selected?: number;
26
+ /** The day number that is today (primary highlight when unselected). */
27
+ today?: number;
28
+ /** Number of days in the month. */
29
+ daysInMonth?: number;
30
+ /** Weekday (0=Sun .. 6=Sat) the 1st falls on; sets leading blank cells. */
31
+ startWeekday?: number;
32
+ /** Fired with the day number when a day cell is pressed. */
33
+ onSelect?: (day: number) => void;
34
+ /** Fired when the previous-month chevron is pressed. */
35
+ onPrev?: (event: GestureResponderEvent) => void;
36
+ /** Fired when the next-month chevron is pressed. */
37
+ onNext?: (event: GestureResponderEvent) => void;
38
+
39
+ // Density (pick one; default is the comfortable cell).
40
+ /** Tighter cells and smaller type, for dense surfaces. */
41
+ compact?: boolean;
42
+
43
+ /** Escape hatch for layout/positioning composition (width, margins). */
44
+ style?: StyleProp<ViewStyle>;
45
+ }
46
+
47
+ // Density precedence: `compact` wins, otherwise the default cell.
48
+ function densityOf(p: CalendarProps): Density {
49
+ if (p.compact) return "compact";
50
+ return "default";
51
+ }
52
+
53
+ /** Build a Calendar component from a platform skin. */
54
+ export function createCalendar(skin: CalendarSkin) {
55
+ return function Calendar(props: CalendarProps) {
56
+ const {
57
+ month = "June 2026",
58
+ selected,
59
+ today,
60
+ daysInMonth = 30,
61
+ startWeekday = 0,
62
+ onSelect,
63
+ onPrev,
64
+ onNext,
65
+ style,
66
+ } = props;
67
+
68
+ const { tokens } = useTheme();
69
+ const density = densityOf(props);
70
+ const m = skin.metrics[density];
71
+ const lead = ((startWeekday % 7) + 7) % 7;
72
+ const days = Array.from({ length: daysInMonth }, (_, i) => i + 1);
73
+ const ripple = skin.ripple ? skin.ripple(tokens) : undefined;
74
+
75
+ return (
76
+ <View style={[skin.containerBase, skin.containerSurface(tokens), style]}>
77
+ {/* Header: month label between two ghost chevron buttons. */}
78
+ <View style={skin.header}>
79
+ <Pressable
80
+ style={({ pressed }) => [
81
+ skin.chevron,
82
+ skin.pressedOpacity != null && pressed ? { opacity: skin.pressedOpacity } : null,
83
+ ]}
84
+ android_ripple={ripple ? { ...ripple, borderless: true } : undefined}
85
+ onPress={onPrev}
86
+ accessibilityRole="button"
87
+ accessibilityLabel="Previous month"
88
+ >
89
+ <Text style={skin.chevronText(tokens)}>{"‹"}</Text>
90
+ </Pressable>
91
+ <Text style={skin.monthLabel(tokens)}>{month}</Text>
92
+ <Pressable
93
+ style={({ pressed }) => [
94
+ skin.chevron,
95
+ skin.pressedOpacity != null && pressed ? { opacity: skin.pressedOpacity } : null,
96
+ ]}
97
+ android_ripple={ripple ? { ...ripple, borderless: true } : undefined}
98
+ onPress={onNext}
99
+ accessibilityRole="button"
100
+ accessibilityLabel="Next month"
101
+ >
102
+ <Text style={skin.chevronText(tokens)}>{"›"}</Text>
103
+ </Pressable>
104
+ </View>
105
+
106
+ {/* Weekday label row. */}
107
+ <View style={[skin.grid, { width: m.gridWidth }]}>
108
+ {skin.weekdays.map((wd, i) => (
109
+ <View key={`wd-${i}`} style={[skin.headCell, m.head]}>
110
+ <Text style={skin.weekdayLabel(tokens)}>{wd}</Text>
111
+ </View>
112
+ ))}
113
+ </View>
114
+
115
+ {/* Day grid: leading blanks, then one cell per day. */}
116
+ <View style={[skin.grid, { width: m.gridWidth }]}>
117
+ {Array.from({ length: lead }, (_, i) => (
118
+ <View key={`blank-${i}`} style={[skin.headCell, m.cell]} />
119
+ ))}
120
+ {days.map((day) => {
121
+ const isSelected = selected != null && day === selected;
122
+ const isToday = today != null && day === today;
123
+ const state = { selected: isSelected, today: isToday };
124
+ return (
125
+ <Pressable
126
+ key={day}
127
+ style={({ pressed }) => [
128
+ skin.dayCellBase,
129
+ m.cell,
130
+ skin.dayCellState(tokens, state),
131
+ skin.pressedOpacity != null && pressed ? { opacity: skin.pressedOpacity } : null,
132
+ ]}
133
+ android_ripple={ripple}
134
+ onPress={() => onSelect?.(day)}
135
+ accessibilityRole="button"
136
+ accessibilityState={{ selected: isSelected }}
137
+ >
138
+ <Text style={[m.label, skin.dayLabel(tokens, state)]}>{day}</Text>
139
+ </Pressable>
140
+ );
141
+ })}
142
+ </View>
143
+ </View>
144
+ );
145
+ };
146
+ }