@olympusoss/canvas 4.0.0 → 5.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (297) hide show
  1. package/README.md +108 -0
  2. package/package.json +14 -3
  3. package/src/atoms/avatar/avatar.md +185 -0
  4. package/src/atoms/avatar/avatar.styles.ts +48 -0
  5. package/src/atoms/avatar/avatar.tsx +99 -0
  6. package/src/atoms/badge/badge.md +237 -0
  7. package/src/atoms/badge/badge.styles.ts +79 -0
  8. package/src/atoms/badge/badge.tsx +86 -0
  9. package/src/atoms/breadcrumb/breadcrumb.md +233 -0
  10. package/src/atoms/breadcrumb/breadcrumb.styles.ts +40 -0
  11. package/src/atoms/breadcrumb/breadcrumb.tsx +130 -0
  12. package/src/atoms/button/button.android.tsx +6 -0
  13. package/src/atoms/button/button.ios.tsx +6 -0
  14. package/src/atoms/button/button.md +184 -0
  15. package/src/atoms/button/button.shared.tsx +79 -0
  16. package/src/atoms/button/button.styles.ts +152 -0
  17. package/src/atoms/button/button.tsx +6 -0
  18. package/src/atoms/button-group/button-group.android.tsx +6 -0
  19. package/src/atoms/button-group/button-group.ios.tsx +6 -0
  20. package/src/atoms/button-group/button-group.md +120 -0
  21. package/src/atoms/button-group/button-group.shared.tsx +398 -0
  22. package/src/atoms/button-group/button-group.styles.ts +483 -0
  23. package/src/atoms/button-group/button-group.tsx +6 -0
  24. package/src/atoms/checkbox/checkbox.android.tsx +6 -0
  25. package/src/atoms/checkbox/checkbox.ios.tsx +6 -0
  26. package/src/atoms/checkbox/checkbox.md +150 -0
  27. package/src/atoms/checkbox/checkbox.shared.tsx +103 -0
  28. package/src/atoms/checkbox/checkbox.styles.ts +106 -0
  29. package/src/atoms/checkbox/checkbox.tsx +6 -0
  30. package/src/atoms/combobox/combobox.android.tsx +6 -0
  31. package/src/atoms/combobox/combobox.ios.tsx +6 -0
  32. package/src/atoms/combobox/combobox.md +213 -0
  33. package/src/atoms/combobox/combobox.shared.tsx +160 -0
  34. package/src/atoms/combobox/combobox.styles.ts +270 -0
  35. package/src/atoms/combobox/combobox.tsx +6 -0
  36. package/src/atoms/divider/divider.md +140 -0
  37. package/src/atoms/divider/divider.styles.ts +35 -0
  38. package/src/atoms/divider/divider.tsx +67 -0
  39. package/src/atoms/dropdown/dropdown.android.tsx +6 -0
  40. package/src/atoms/dropdown/dropdown.ios.tsx +6 -0
  41. package/src/atoms/dropdown/dropdown.md +221 -0
  42. package/src/atoms/dropdown/dropdown.shared.tsx +190 -0
  43. package/src/atoms/dropdown/dropdown.styles.ts +233 -0
  44. package/src/atoms/dropdown/dropdown.tsx +6 -0
  45. package/src/atoms/icon/icon.md +131 -0
  46. package/src/atoms/icon/icon.styles.ts +30 -0
  47. package/src/atoms/icon/icon.tsx +328 -0
  48. package/src/atoms/index.ts +24 -0
  49. package/src/atoms/input/input.android.tsx +6 -0
  50. package/src/atoms/input/input.ios.tsx +6 -0
  51. package/src/atoms/input/input.md +118 -0
  52. package/src/atoms/input/input.shared.tsx +203 -0
  53. package/src/atoms/input/input.styles.ts +286 -0
  54. package/src/atoms/input/input.tsx +6 -0
  55. package/src/atoms/kbd/kbd.md +91 -0
  56. package/src/atoms/kbd/kbd.styles.ts +33 -0
  57. package/src/atoms/kbd/kbd.tsx +27 -0
  58. package/src/atoms/listbox/listbox.md +177 -0
  59. package/src/atoms/listbox/listbox.styles.ts +60 -0
  60. package/src/atoms/listbox/listbox.tsx +113 -0
  61. package/src/atoms/pagination/pagination.android.tsx +6 -0
  62. package/src/atoms/pagination/pagination.ios.tsx +6 -0
  63. package/src/atoms/pagination/pagination.md +133 -0
  64. package/src/atoms/pagination/pagination.shared.tsx +289 -0
  65. package/src/atoms/pagination/pagination.styles.ts +245 -0
  66. package/src/atoms/pagination/pagination.tsx +6 -0
  67. package/src/atoms/popover/popover.android.tsx +8 -0
  68. package/src/atoms/popover/popover.ios.tsx +6 -0
  69. package/src/atoms/popover/popover.md +87 -0
  70. package/src/atoms/popover/popover.shared.tsx +124 -0
  71. package/src/atoms/popover/popover.styles.ts +144 -0
  72. package/src/atoms/popover/popover.tsx +6 -0
  73. package/src/atoms/radio/radio.android.tsx +6 -0
  74. package/src/atoms/radio/radio.ios.tsx +6 -0
  75. package/src/atoms/radio/radio.md +173 -0
  76. package/src/atoms/radio/radio.shared.tsx +98 -0
  77. package/src/atoms/radio/radio.styles.ts +109 -0
  78. package/src/atoms/radio/radio.tsx +6 -0
  79. package/src/atoms/select/select.android.tsx +6 -0
  80. package/src/atoms/select/select.ios.tsx +6 -0
  81. package/src/atoms/select/select.md +156 -0
  82. package/src/atoms/select/select.shared.tsx +143 -0
  83. package/src/atoms/select/select.styles.ts +310 -0
  84. package/src/atoms/select/select.tsx +6 -0
  85. package/src/atoms/skeleton/skeleton.md +135 -0
  86. package/src/atoms/skeleton/skeleton.styles.ts +117 -0
  87. package/src/atoms/skeleton/skeleton.tsx +145 -0
  88. package/src/atoms/spinner/spinner.android.tsx +7 -0
  89. package/src/atoms/spinner/spinner.ios.tsx +7 -0
  90. package/src/atoms/spinner/spinner.md +94 -0
  91. package/src/atoms/spinner/spinner.shared.tsx +92 -0
  92. package/src/atoms/spinner/spinner.styles.tsx +115 -0
  93. package/src/atoms/spinner/spinner.tsx +7 -0
  94. package/src/atoms/switch/switch.android.tsx +6 -0
  95. package/src/atoms/switch/switch.ios.tsx +6 -0
  96. package/src/atoms/switch/switch.md +91 -0
  97. package/src/atoms/switch/switch.shared.tsx +97 -0
  98. package/src/atoms/switch/switch.styles.ts +79 -0
  99. package/src/atoms/switch/switch.tsx +6 -0
  100. package/src/atoms/textarea/textarea.android.tsx +6 -0
  101. package/src/atoms/textarea/textarea.ios.tsx +6 -0
  102. package/src/atoms/textarea/textarea.md +140 -0
  103. package/src/atoms/textarea/textarea.shared.tsx +74 -0
  104. package/src/atoms/textarea/textarea.styles.ts +116 -0
  105. package/src/atoms/textarea/textarea.tsx +6 -0
  106. package/src/atoms/tooltip/tooltip.android.tsx +6 -0
  107. package/src/atoms/tooltip/tooltip.ios.tsx +7 -0
  108. package/src/atoms/tooltip/tooltip.md +122 -0
  109. package/src/atoms/tooltip/tooltip.shared.tsx +113 -0
  110. package/src/atoms/tooltip/tooltip.styles.ts +113 -0
  111. package/src/atoms/tooltip/tooltip.tsx +6 -0
  112. package/src/atoms/typography/typography.md +330 -0
  113. package/src/atoms/typography/typography.styles.ts +95 -0
  114. package/src/atoms/typography/typography.tsx +76 -0
  115. package/src/index.ts +12 -2
  116. package/src/molecules/action-panels/action-panels.md +133 -0
  117. package/src/molecules/action-panels/action-panels.styles.ts +39 -0
  118. package/src/molecules/action-panels/action-panels.tsx +113 -0
  119. package/src/molecules/alert/alert.md +119 -0
  120. package/src/molecules/alert/alert.styles.ts +88 -0
  121. package/src/molecules/alert/alert.tsx +74 -0
  122. package/src/molecules/alert-dialog/alert-dialog.android.tsx +6 -0
  123. package/src/molecules/alert-dialog/alert-dialog.ios.tsx +6 -0
  124. package/src/molecules/alert-dialog/alert-dialog.md +177 -0
  125. package/src/molecules/alert-dialog/alert-dialog.shared.tsx +187 -0
  126. package/src/molecules/alert-dialog/alert-dialog.styles.ts +248 -0
  127. package/src/molecules/alert-dialog/alert-dialog.tsx +6 -0
  128. package/src/molecules/card/card.md +190 -0
  129. package/src/molecules/card/card.styles.ts +67 -0
  130. package/src/molecules/card/card.tsx +176 -0
  131. package/src/molecules/code-block/code-block.md +159 -0
  132. package/src/molecules/code-block/code-block.styles.ts +167 -0
  133. package/src/molecules/code-block/code-block.tsx +176 -0
  134. package/src/molecules/description-lists/description-lists.md +129 -0
  135. package/src/molecules/description-lists/description-lists.styles.ts +102 -0
  136. package/src/molecules/description-lists/description-lists.tsx +133 -0
  137. package/src/molecules/empty-state/empty-state.md +218 -0
  138. package/src/molecules/empty-state/empty-state.styles.ts +63 -0
  139. package/src/molecules/empty-state/empty-state.tsx +77 -0
  140. package/src/molecules/feeds/feeds.md +102 -0
  141. package/src/molecules/feeds/feeds.styles.ts +120 -0
  142. package/src/molecules/feeds/feeds.tsx +167 -0
  143. package/src/molecules/field/field.md +117 -0
  144. package/src/molecules/field/field.styles.ts +85 -0
  145. package/src/molecules/field/field.tsx +175 -0
  146. package/src/molecules/fieldset/fieldset.md +141 -0
  147. package/src/molecules/fieldset/fieldset.styles.ts +79 -0
  148. package/src/molecules/fieldset/fieldset.tsx +182 -0
  149. package/src/molecules/form/form.md +137 -0
  150. package/src/molecules/form/form.styles.ts +39 -0
  151. package/src/molecules/form/form.tsx +246 -0
  152. package/src/molecules/grid-lists/grid-lists.md +114 -0
  153. package/src/molecules/grid-lists/grid-lists.styles.ts +79 -0
  154. package/src/molecules/grid-lists/grid-lists.tsx +157 -0
  155. package/src/molecules/index.ts +16 -0
  156. package/src/molecules/media-objects/media-objects.md +87 -0
  157. package/src/molecules/media-objects/media-objects.styles.ts +94 -0
  158. package/src/molecules/media-objects/media-objects.tsx +128 -0
  159. package/src/molecules/stacked-lists/stacked-lists.md +116 -0
  160. package/src/molecules/stacked-lists/stacked-lists.styles.ts +111 -0
  161. package/src/molecules/stacked-lists/stacked-lists.tsx +195 -0
  162. package/src/molecules/stats/stats.md +166 -0
  163. package/src/molecules/stats/stats.styles.ts +91 -0
  164. package/src/molecules/stats/stats.tsx +88 -0
  165. package/src/organisms/calendar/calendar.android.tsx +6 -0
  166. package/src/organisms/calendar/calendar.ios.tsx +6 -0
  167. package/src/organisms/calendar/calendar.md +114 -0
  168. package/src/organisms/calendar/calendar.shared.tsx +146 -0
  169. package/src/organisms/calendar/calendar.styles.ts +315 -0
  170. package/src/organisms/calendar/calendar.tsx +6 -0
  171. package/src/organisms/charts/charts.md +326 -0
  172. package/src/organisms/charts/charts.styles.ts +135 -0
  173. package/src/organisms/charts/charts.tsx +124 -0
  174. package/src/organisms/command/command.md +117 -0
  175. package/src/organisms/command/command.styles.ts +179 -0
  176. package/src/organisms/command/command.tsx +164 -0
  177. package/src/organisms/data-table/data-table.md +182 -0
  178. package/src/organisms/data-table/data-table.styles.ts +103 -0
  179. package/src/organisms/data-table/data-table.tsx +105 -0
  180. package/src/organisms/dialog/dialog.android.tsx +6 -0
  181. package/src/organisms/dialog/dialog.ios.tsx +6 -0
  182. package/src/organisms/dialog/dialog.md +271 -0
  183. package/src/organisms/dialog/dialog.shared.tsx +230 -0
  184. package/src/organisms/dialog/dialog.styles.ts +272 -0
  185. package/src/organisms/dialog/dialog.tsx +6 -0
  186. package/src/organisms/filter-panel/filter-panel.md +116 -0
  187. package/src/organisms/filter-panel/filter-panel.styles.ts +83 -0
  188. package/src/organisms/filter-panel/filter-panel.tsx +91 -0
  189. package/src/organisms/index.ts +13 -0
  190. package/src/organisms/navbars/navbars.android.tsx +6 -0
  191. package/src/organisms/navbars/navbars.ios.tsx +6 -0
  192. package/src/organisms/navbars/navbars.md +144 -0
  193. package/src/organisms/navbars/navbars.shared.tsx +137 -0
  194. package/src/organisms/navbars/navbars.styles.ts +251 -0
  195. package/src/organisms/navbars/navbars.tsx +6 -0
  196. package/src/organisms/overlays/overlays.android.tsx +6 -0
  197. package/src/organisms/overlays/overlays.ios.tsx +6 -0
  198. package/src/organisms/overlays/overlays.md +123 -0
  199. package/src/organisms/overlays/overlays.shared.tsx +175 -0
  200. package/src/organisms/overlays/overlays.styles.ts +309 -0
  201. package/src/organisms/overlays/overlays.tsx +6 -0
  202. package/src/organisms/row-menu/row-menu.android.tsx +6 -0
  203. package/src/organisms/row-menu/row-menu.ios.tsx +6 -0
  204. package/src/organisms/row-menu/row-menu.md +102 -0
  205. package/src/organisms/row-menu/row-menu.shared.tsx +105 -0
  206. package/src/organisms/row-menu/row-menu.styles.ts +262 -0
  207. package/src/organisms/row-menu/row-menu.tsx +6 -0
  208. package/src/organisms/sidebar/sidebar.android.tsx +6 -0
  209. package/src/organisms/sidebar/sidebar.ios.tsx +6 -0
  210. package/src/organisms/sidebar/sidebar.md +188 -0
  211. package/src/organisms/sidebar/sidebar.shared.tsx +167 -0
  212. package/src/organisms/sidebar/sidebar.styles.ts +262 -0
  213. package/src/organisms/sidebar/sidebar.tsx +6 -0
  214. package/src/organisms/stepper/stepper.android.tsx +6 -0
  215. package/src/organisms/stepper/stepper.ios.tsx +6 -0
  216. package/src/organisms/stepper/stepper.md +150 -0
  217. package/src/organisms/stepper/stepper.shared.tsx +158 -0
  218. package/src/organisms/stepper/stepper.styles.ts +280 -0
  219. package/src/organisms/stepper/stepper.tsx +6 -0
  220. package/src/organisms/tabs/tabs.android.tsx +6 -0
  221. package/src/organisms/tabs/tabs.ios.tsx +6 -0
  222. package/src/organisms/tabs/tabs.md +127 -0
  223. package/src/organisms/tabs/tabs.shared.tsx +281 -0
  224. package/src/organisms/tabs/tabs.styles.ts +398 -0
  225. package/src/organisms/tabs/tabs.tsx +6 -0
  226. package/src/style/color.ts +17 -0
  227. package/src/style/index.ts +14 -0
  228. package/src/style/primitives.ts +26 -0
  229. package/src/style/responsive.ts +45 -0
  230. package/src/style/shadow.ts +21 -0
  231. package/src/style/theme.tsx +56 -0
  232. package/src/style/tokens.ts +487 -0
  233. package/src/theme.ts +21 -0
  234. package/styles/canvas.css +128 -67
  235. package/tsconfig.json +4 -2
  236. package/src/cn.ts +0 -3
  237. package/styles/base.css +0 -17
  238. package/styles/components/alert.css +0 -66
  239. package/styles/components/app-shell.css +0 -46
  240. package/styles/components/avatar.css +0 -15
  241. package/styles/components/badge.css +0 -83
  242. package/styles/components/breadcrumb.css +0 -35
  243. package/styles/components/button-group.css +0 -23
  244. package/styles/components/button.css +0 -107
  245. package/styles/components/calendar.css +0 -73
  246. package/styles/components/card.css +0 -58
  247. package/styles/components/checkbox.css +0 -55
  248. package/styles/components/code-block.css +0 -18
  249. package/styles/components/combobox.css +0 -75
  250. package/styles/components/command.css +0 -94
  251. package/styles/components/data-table.css +0 -142
  252. package/styles/components/dialog.css +0 -72
  253. package/styles/components/dropdown.css +0 -54
  254. package/styles/components/empty-state.css +0 -17
  255. package/styles/components/field.css +0 -27
  256. package/styles/components/filter-panel.css +0 -58
  257. package/styles/components/form.css +0 -27
  258. package/styles/components/icon.css +0 -8
  259. package/styles/components/input-group.css +0 -45
  260. package/styles/components/input.css +0 -56
  261. package/styles/components/kbd.css +0 -15
  262. package/styles/components/page-header.css +0 -52
  263. package/styles/components/pagination.css +0 -48
  264. package/styles/components/popover.css +0 -14
  265. package/styles/components/radio.css +0 -28
  266. package/styles/components/row-menu.css +0 -69
  267. package/styles/components/section-card.css +0 -49
  268. package/styles/components/select.css +0 -57
  269. package/styles/components/separator.css +0 -32
  270. package/styles/components/sheet.css +0 -70
  271. package/styles/components/sidebar.css +0 -146
  272. package/styles/components/skeleton.css +0 -32
  273. package/styles/components/spinner.css +0 -26
  274. package/styles/components/stat-card.css +0 -71
  275. package/styles/components/stepper.css +0 -63
  276. package/styles/components/switch.css +0 -45
  277. package/styles/components/tabs.css +0 -40
  278. package/styles/components/textarea.css +0 -31
  279. package/styles/components/toast.css +0 -95
  280. package/styles/components/tooltip.css +0 -53
  281. package/styles/components/topbar.css +0 -24
  282. package/styles/components/typography.css +0 -105
  283. package/styles/patterns/backdrops.css +0 -35
  284. package/styles/patterns/density.css +0 -66
  285. package/styles/patterns/focus.css +0 -38
  286. package/styles/patterns/glass.css +0 -85
  287. package/styles/patterns/high-contrast.css +0 -70
  288. package/styles/patterns/reduced-motion.css +0 -12
  289. package/styles/patterns/scrollbar.css +0 -10
  290. package/styles/reset.css +0 -89
  291. package/styles/tokens/colors.css +0 -106
  292. package/styles/tokens/motion.css +0 -33
  293. package/styles/tokens/radius.css +0 -10
  294. package/styles/tokens/shadows.css +0 -35
  295. package/styles/tokens/spacing.css +0 -19
  296. package/styles/tokens/typography.css +0 -6
  297. package/styles/tokens/z-index.css +0 -12
@@ -0,0 +1,315 @@
1
+ import { type ViewStyle, type TextStyle } from "react-native";
2
+ import { type ColorTokens, alpha } from "../../style/index.js";
3
+
4
+ // Co-located Calendar skins, one per platform. The shell resolves the density
5
+ // metrics (compact vs default cell sizing), the leading-blank padding, and the
6
+ // per-day selected/today/highlight state; the skin supplies only the native
7
+ // SHAPE, sizing, weekday-label style, day-cell fill, today treatment, and press
8
+ // feedback. The BRAND survives on every platform (the selected day fills with the
9
+ // indigo `primary` token, never a platform default), so each follows light/dark
10
+ // and reads as glass when the ThemeProvider's surface is "glass" (tokens.card is
11
+ // swapped translucent).
12
+ //
13
+ // iOS (HIG date picker): the SELECTED day is a filled `primary` circle
14
+ // (radius 9999) with `primary-foreground` text; TODAY is `primary`-colored
15
+ // text with no fill; weekday headers are ~13pt muted SF-style two-letter
16
+ // labels; the month label sits between two ghost chevrons. Press = opacity
17
+ // dim (~0.8).
18
+ // Android (M3 date picker): the SELECTED day is a filled `primary` circle;
19
+ // TODAY is an OUTLINED ring (1px `primary`) on a transparent fill; weekday
20
+ // headers are single-letter; day cells get a circular `android_ripple`
21
+ // (alpha(primary, 0.12)); slightly larger touch targets.
22
+ // Web: the established Canvas look (rounded-full primary fill for any
23
+ // highlighted day, Su/Mo two-letter weekday labels), lifted verbatim from the
24
+ // original file.
25
+
26
+ export type Density = "compact" | "default";
27
+
28
+ // Per-density box sizes, in px. The grid width is exactly seven cell widths so the
29
+ // flex-wrap row breaks after the seventh cell.
30
+ export interface CellMetrics {
31
+ /** A day cell (square). */
32
+ cell: ViewStyle;
33
+ /** The grid width = seven cell widths. */
34
+ gridWidth: number;
35
+ /** A weekday head cell. */
36
+ head: ViewStyle;
37
+ /** The day-label type scale. */
38
+ label: TextStyle;
39
+ }
40
+
41
+ // The per-day visual state the shell resolves and hands the skin.
42
+ export interface DayState {
43
+ /** The day equals `selected` (the primary highlight). */
44
+ selected: boolean;
45
+ /** The day equals `today` (the secondary highlight). */
46
+ today: boolean;
47
+ }
48
+
49
+ // The platform-varying surface. Everything color/shape-bearing the calendar needs
50
+ // lives here, built from the active tokens (so each follows light/dark/glass) and
51
+ // the active density metrics.
52
+ export interface CalendarSkin {
53
+ /** Per-density cell/grid/head/label metrics. */
54
+ metrics: Record<Density, CellMetrics>;
55
+
56
+ /** iOS/web dim the day cell on press; Android uses a ripple instead (null). */
57
+ pressedOpacity: number | null;
58
+ /** Android ripple over a pressed day cell; null on iOS/web. */
59
+ ripple: ((t: ColorTokens) => { color: string; borderless: boolean; radius?: number }) | null;
60
+
61
+ // --- container ---
62
+ /** The outer surface: border, radius, padding (layout-only). */
63
+ containerBase: ViewStyle;
64
+ /** The surface fill + border (tokens.card goes translucent under glass). */
65
+ containerSurface: (t: ColorTokens) => ViewStyle;
66
+
67
+ // --- header (month label + prev/next chevrons) ---
68
+ header: ViewStyle;
69
+ /** A ghost chevron button box. */
70
+ chevron: ViewStyle;
71
+ /** The chevron glyph. */
72
+ chevronText: (t: ColorTokens) => TextStyle;
73
+ /** The month/year label. */
74
+ monthLabel: (t: ColorTokens) => TextStyle;
75
+
76
+ // --- weekday header row ---
77
+ /** The weekday + day grid row (fixed width supplied per density). */
78
+ grid: ViewStyle;
79
+ /** A weekday head cell interior. */
80
+ headCell: ViewStyle;
81
+ /** The weekday label. */
82
+ weekdayLabel: (t: ColorTokens) => TextStyle;
83
+ /** The weekday header strings (single-letter on Android, two-letter elsewhere). */
84
+ weekdays: string[];
85
+
86
+ // --- day cell ---
87
+ /** A day cell interior (size from metrics; selected/today fill applied on top). */
88
+ dayCellBase: ViewStyle;
89
+ /** The day-cell fill/outline per state (selected circle, today ring, etc.). */
90
+ dayCellState: (t: ColorTokens, state: DayState) => ViewStyle;
91
+ /** The day-label color/weight per state. */
92
+ dayLabel: (t: ColorTokens, state: DayState) => TextStyle;
93
+ }
94
+
95
+ // --- shared weekday strings ------------------------------------------------
96
+
97
+ const WEEKDAYS_TWO = ["Su", "Mo", "Tu", "We", "Th", "Fr", "Sa"];
98
+ const WEEKDAYS_ONE = ["S", "M", "T", "W", "T", "F", "S"];
99
+
100
+ // =============================================================================
101
+ // Web: the established Canvas look (lifted verbatim from the original file).
102
+ // Any highlighted day (selected OR today) gets a `primary` rounded-full fill.
103
+ // =============================================================================
104
+
105
+ export const webSkin: CalendarSkin = {
106
+ metrics: {
107
+ compact: {
108
+ cell: { width: 32, height: 32 },
109
+ gridWidth: 224,
110
+ head: { width: 32, height: 28 },
111
+ label: { fontSize: 12, lineHeight: 16 },
112
+ },
113
+ default: {
114
+ cell: { width: 36, height: 36 },
115
+ gridWidth: 252,
116
+ head: { width: 36, height: 32 },
117
+ label: { fontSize: 14, lineHeight: 20 },
118
+ },
119
+ },
120
+
121
+ pressedOpacity: 0.9,
122
+ ripple: null,
123
+
124
+ // `self-start rounded-lg border p-3`.
125
+ containerBase: {
126
+ alignSelf: "flex-start",
127
+ borderRadius: 8,
128
+ borderWidth: 1,
129
+ padding: 12,
130
+ },
131
+ containerSurface: (t) => ({ borderColor: t.border, backgroundColor: t.card }),
132
+
133
+ // `mb-2 flex-row items-center justify-between`.
134
+ header: {
135
+ marginBottom: 8,
136
+ flexDirection: "row",
137
+ alignItems: "center",
138
+ justifyContent: "space-between",
139
+ },
140
+ // `h-7 w-7 items-center justify-center rounded-md bg-transparent`.
141
+ chevron: {
142
+ height: 28,
143
+ width: 28,
144
+ alignItems: "center",
145
+ justifyContent: "center",
146
+ borderRadius: 6,
147
+ backgroundColor: "transparent",
148
+ },
149
+ chevronText: (t) => ({ fontSize: 14, lineHeight: 20, color: t.foreground }),
150
+ monthLabel: (t) => ({ fontSize: 14, lineHeight: 20, fontWeight: "500", color: t.foreground }),
151
+
152
+ grid: { flexDirection: "row", flexWrap: "wrap" },
153
+ headCell: { alignItems: "center", justifyContent: "center" },
154
+ weekdayLabel: (t) => ({ fontSize: 12, lineHeight: 16, fontWeight: "500", color: t["muted-foreground"] }),
155
+ weekdays: WEEKDAYS_TWO,
156
+
157
+ // `items-center justify-center rounded-full`.
158
+ dayCellBase: { alignItems: "center", justifyContent: "center", borderRadius: 9999 },
159
+ // Any highlighted (today/selected) day fills `bg-primary`.
160
+ dayCellState: (t, st) => (st.selected || st.today ? { backgroundColor: t.primary } : {}),
161
+ // Highlighted -> `font-medium text-primary-foreground`, otherwise `text-foreground`.
162
+ dayLabel: (t, st) =>
163
+ st.selected || st.today
164
+ ? { fontWeight: "500", color: t["primary-foreground"] }
165
+ : { color: t.foreground },
166
+ };
167
+
168
+ // =============================================================================
169
+ // iOS (HIG date picker): the SELECTED day is a filled `primary` circle with
170
+ // `primary-foreground` text; TODAY is `primary`-colored text with no fill;
171
+ // weekday headers ~13pt muted; the month label sits between two ghost chevrons.
172
+ // Press = opacity dim.
173
+ // =============================================================================
174
+
175
+ export const iosSkin: CalendarSkin = {
176
+ // Slightly larger, airier cells in the HIG date-picker spirit.
177
+ metrics: {
178
+ compact: {
179
+ cell: { width: 34, height: 34 },
180
+ gridWidth: 238,
181
+ head: { width: 34, height: 28 },
182
+ label: { fontSize: 15, lineHeight: 20 },
183
+ },
184
+ default: {
185
+ cell: { width: 38, height: 38 },
186
+ gridWidth: 266,
187
+ head: { width: 38, height: 30 },
188
+ label: { fontSize: 17, lineHeight: 22 },
189
+ },
190
+ },
191
+
192
+ pressedOpacity: 0.8, // HIG: dim on press
193
+ ripple: null,
194
+
195
+ containerBase: {
196
+ alignSelf: "flex-start",
197
+ borderRadius: 12, // HIG larger corner radius
198
+ borderWidth: 1,
199
+ padding: 12,
200
+ },
201
+ containerSurface: (t) => ({ borderColor: t.border, backgroundColor: t.card }),
202
+
203
+ header: {
204
+ marginBottom: 8,
205
+ flexDirection: "row",
206
+ alignItems: "center",
207
+ justifyContent: "space-between",
208
+ },
209
+ chevron: {
210
+ height: 30,
211
+ width: 30,
212
+ alignItems: "center",
213
+ justifyContent: "center",
214
+ borderRadius: 9999,
215
+ backgroundColor: "transparent",
216
+ },
217
+ // HIG: the chevrons carry the brand indigo (system-accent style), heavier glyph.
218
+ chevronText: (t) => ({ fontSize: 22, lineHeight: 24, fontWeight: "500", color: t.primary }),
219
+ // HIG: a bold ~17pt month/year title.
220
+ monthLabel: (t) => ({ fontSize: 17, lineHeight: 22, fontWeight: "600", color: t.foreground }),
221
+
222
+ grid: { flexDirection: "row", flexWrap: "wrap" },
223
+ headCell: { alignItems: "center", justifyContent: "center" },
224
+ // ~13pt muted SF-style weekday header.
225
+ weekdayLabel: (t) => ({ fontSize: 13, lineHeight: 16, fontWeight: "600", color: t["muted-foreground"] }),
226
+ weekdays: WEEKDAYS_TWO,
227
+
228
+ dayCellBase: { alignItems: "center", justifyContent: "center", borderRadius: 9999 },
229
+ // Selected wins: filled `primary` circle. Today (when not selected): no fill
230
+ // (the day label carries the brand indigo instead).
231
+ dayCellState: (t, st) => (st.selected ? { backgroundColor: t.primary } : {}),
232
+ // Selected -> `primary-foreground`; today (unselected) -> `primary` colored
233
+ // text, semibold; otherwise plain foreground.
234
+ dayLabel: (t, st) => {
235
+ if (st.selected) return { fontWeight: "600", color: t["primary-foreground"] };
236
+ if (st.today) return { fontWeight: "600", color: t.primary };
237
+ return { color: t.foreground };
238
+ },
239
+ };
240
+
241
+ // =============================================================================
242
+ // Android (M3 date picker): the SELECTED day is a filled `primary` circle;
243
+ // TODAY is an OUTLINED ring (1px `primary`) on transparent; weekday headers are
244
+ // single-letter; day cells get a circular `android_ripple`; larger touch targets.
245
+ // =============================================================================
246
+
247
+ export const androidSkin: CalendarSkin = {
248
+ // M3 calendar cells are ~40dp/48dp targets; bump the grid accordingly.
249
+ metrics: {
250
+ compact: {
251
+ cell: { width: 36, height: 36 },
252
+ gridWidth: 252,
253
+ head: { width: 36, height: 28 },
254
+ label: { fontSize: 12, lineHeight: 16 },
255
+ },
256
+ default: {
257
+ cell: { width: 40, height: 40 },
258
+ gridWidth: 280,
259
+ head: { width: 40, height: 32 },
260
+ label: { fontSize: 14, lineHeight: 20 },
261
+ },
262
+ },
263
+
264
+ pressedOpacity: null, // Android uses a ripple instead
265
+ // Circular ripple roughly matching the cell radius (default 40/2 = 20dp).
266
+ ripple: (t) => ({ color: alpha(t.primary, 0.12), borderless: false, radius: 20 }),
267
+
268
+ containerBase: {
269
+ alignSelf: "flex-start",
270
+ borderRadius: 12, // M3 large corner
271
+ borderWidth: 1,
272
+ padding: 12,
273
+ },
274
+ containerSurface: (t) => ({ borderColor: t.border, backgroundColor: t.card }),
275
+
276
+ header: {
277
+ marginBottom: 8,
278
+ flexDirection: "row",
279
+ alignItems: "center",
280
+ justifyContent: "space-between",
281
+ },
282
+ chevron: {
283
+ height: 40,
284
+ width: 40,
285
+ alignItems: "center",
286
+ justifyContent: "center",
287
+ borderRadius: 9999,
288
+ backgroundColor: "transparent",
289
+ },
290
+ chevronText: (t) => ({ fontSize: 22, lineHeight: 24, color: t["muted-foreground"] }),
291
+ // M3 labelLarge-ish month label.
292
+ monthLabel: (t) => ({ fontSize: 14, lineHeight: 20, fontWeight: "500", color: t.foreground }),
293
+
294
+ grid: { flexDirection: "row", flexWrap: "wrap" },
295
+ headCell: { alignItems: "center", justifyContent: "center" },
296
+ // M3 single-letter weekday headers, muted.
297
+ weekdayLabel: (t) => ({ fontSize: 12, lineHeight: 16, fontWeight: "500", color: t["muted-foreground"] }),
298
+ weekdays: WEEKDAYS_ONE,
299
+
300
+ dayCellBase: { alignItems: "center", justifyContent: "center", borderRadius: 9999 },
301
+ // Selected -> filled `primary` circle. Today (unselected) -> a 1px `primary`
302
+ // ring on a transparent fill.
303
+ dayCellState: (t, st) => {
304
+ if (st.selected) return { backgroundColor: t.primary };
305
+ if (st.today) return { borderWidth: 1, borderColor: t.primary, backgroundColor: "transparent" };
306
+ return {};
307
+ },
308
+ // Selected -> `primary-foreground`; today (unselected) -> `primary` text;
309
+ // otherwise plain foreground.
310
+ dayLabel: (t, st) => {
311
+ if (st.selected) return { fontWeight: "500", color: t["primary-foreground"] };
312
+ if (st.today) return { fontWeight: "500", color: t.primary };
313
+ return { color: t.foreground };
314
+ },
315
+ };
@@ -0,0 +1,6 @@
1
+ import { createCalendar } from "./calendar.shared.js";
2
+ import { webSkin } from "./calendar.styles.js";
3
+
4
+ // Web Calendar (the base; Metro falls back to it on native, web bundlers resolve it).
5
+ export const Calendar = createCalendar(webSkin);
6
+ export type { CalendarProps } from "./calendar.shared.js";
@@ -0,0 +1,326 @@
1
+ # Charts
2
+
3
+ Sparklines, bars, gauges, heatmaps. All SVG, all token-themed. No charting library required.
4
+
5
+ ## Usage
6
+
7
+ ```tsx
8
+ <Chart
9
+ title="Signups"
10
+ data={[
11
+ { label: "Mon", value: 45 },
12
+ { label: "Tue", value: 60 },
13
+ { label: "Wed", value: 35 },
14
+ { label: "Thu", value: 70 },
15
+ { label: "Fri", value: 55 },
16
+ { label: "Sat", value: 80 },
17
+ { label: "Sun", value: 95 }
18
+ ]}
19
+ max={100}
20
+ />
21
+ ```
22
+
23
+ ## Variants
24
+
25
+ ### Chart type - sparkline
26
+
27
+ ```tsx
28
+ <Chart
29
+ title="Signups"
30
+ data={[
31
+ { label: "Mon", value: 45 },
32
+ { label: "Tue", value: 60 },
33
+ { label: "Wed", value: 35 },
34
+ { label: "Thu", value: 70 },
35
+ { label: "Fri", value: 55 },
36
+ { label: "Sat", value: 80 },
37
+ { label: "Sun", value: 95 }
38
+ ]}
39
+ max={100}
40
+ horizontal
41
+ />
42
+ ```
43
+
44
+ ### Chart type - stacked
45
+
46
+ ```tsx
47
+ <Chart
48
+ title="Signups"
49
+ data={[
50
+ { label: "Mon", value: 45 },
51
+ { label: "Tue", value: 60 },
52
+ { label: "Wed", value: 35 },
53
+ { label: "Thu", value: 70 },
54
+ { label: "Fri", value: 55 },
55
+ { label: "Sat", value: 80 },
56
+ { label: "Sun", value: 95 }
57
+ ]}
58
+ max={100}
59
+ success
60
+ />
61
+ ```
62
+
63
+ ### Chart type - gauge
64
+
65
+ ```tsx
66
+ <Chart
67
+ title="Signups"
68
+ data={[
69
+ { label: "Mon", value: 45 },
70
+ { label: "Tue", value: 60 },
71
+ { label: "Wed", value: 35 },
72
+ { label: "Thu", value: 70 },
73
+ { label: "Fri", value: 55 },
74
+ { label: "Sat", value: 80 },
75
+ { label: "Sun", value: 95 }
76
+ ]}
77
+ max={100}
78
+ success
79
+ horizontal
80
+ />
81
+ ```
82
+
83
+ ### Chart type - heatmap
84
+
85
+ ```tsx
86
+ <Chart
87
+ title="Signups"
88
+ data={[
89
+ { label: "Mon", value: 45 },
90
+ { label: "Tue", value: 60 },
91
+ { label: "Wed", value: 35 },
92
+ { label: "Thu", value: 70 },
93
+ { label: "Fri", value: 55 },
94
+ { label: "Sat", value: 80 },
95
+ { label: "Sun", value: 95 }
96
+ ]}
97
+ max={100}
98
+ destructive
99
+ />
100
+ ```
101
+
102
+ ## Do & Don't
103
+
104
+ ### Bar
105
+
106
+ **Do** — Keep a labelled axis row and a single bar tone so the buckets read at a glance.
107
+
108
+ ```tsx
109
+ <Chart title="Signups" max={100} style={{ maxWidth: 560 }} data={[
110
+ { label: "Mon", value: 45 },
111
+ { label: "Tue", value: 60 },
112
+ { label: "Wed", value: 35 },
113
+ { label: "Thu", value: 70 },
114
+ { label: "Fri", value: 55 },
115
+ { label: "Sat", value: 80 },
116
+ { label: "Sun", value: 95 }
117
+ ]} />
118
+ ```
119
+
120
+ **Don't** — Every bar the same full-strength fill and no labels: nothing is emphasized and the axis is unreadable.
121
+
122
+ ```tsx
123
+ <View style={{ borderRadius: 8, borderWidth: 1, borderColor: tokens.border, backgroundColor: tokens.card, padding: 20, maxWidth: 560 }}>
124
+ <View style={{ flexDirection: "row", alignItems: "flex-end", gap: 4, height: 120, width: 520 }}>
125
+ <View style={{ flexGrow: 1, flexShrink: 1, flexBasis: "0%", borderTopLeftRadius: 4, borderTopRightRadius: 4, backgroundColor: tokens.primary, height: 63 }} />
126
+ <View style={{ flexGrow: 1, flexShrink: 1, flexBasis: "0%", borderTopLeftRadius: 4, borderTopRightRadius: 4, backgroundColor: tokens.primary, height: 84 }} />
127
+ <View style={{ flexGrow: 1, flexShrink: 1, flexBasis: "0%", borderTopLeftRadius: 4, borderTopRightRadius: 4, backgroundColor: tokens.primary, height: 49 }} />
128
+ <View style={{ flexGrow: 1, flexShrink: 1, flexBasis: "0%", borderTopLeftRadius: 4, borderTopRightRadius: 4, backgroundColor: tokens.primary, height: 98 }} />
129
+ <View style={{ flexGrow: 1, flexShrink: 1, flexBasis: "0%", borderTopLeftRadius: 4, borderTopRightRadius: 4, backgroundColor: tokens.primary, height: 77 }} />
130
+ <View style={{ flexGrow: 1, flexShrink: 1, flexBasis: "0%", borderTopLeftRadius: 4, borderTopRightRadius: 4, backgroundColor: tokens.primary, height: 112 }} />
131
+ <View style={{ flexGrow: 1, flexShrink: 1, flexBasis: "0%", borderTopLeftRadius: 4, borderTopRightRadius: 4, backgroundColor: tokens.primary, height: 118 }} />
132
+ </View>
133
+ </View>
134
+ ```
135
+
136
+ ### Sparkline
137
+
138
+ **Do** — Pair the line with the current value and delta and an end dot so it anchors a stat.
139
+
140
+ ```tsx
141
+ <View style={{ borderRadius: 8, borderWidth: 1, borderColor: tokens.border, backgroundColor: tokens.card, padding: 20, maxWidth: 200 }}>
142
+ <Text style={{ fontSize: 12, lineHeight: 16, color: tokens["muted-foreground"] }}>Tokens issued</Text>
143
+ <View style={{ marginTop: 4, flexDirection: "row", alignItems: "baseline", justifyContent: "space-between" }}>
144
+ <Text style={{ fontSize: 24, lineHeight: 32, fontWeight: "600", color: tokens["card-foreground"] }}>4,847</Text>
145
+ <Text style={{ fontSize: 12, lineHeight: 16, color: tokens.primary }}>+12%</Text>
146
+ </View>
147
+ <View style={{ marginTop: 8, flexDirection: "row", alignItems: "flex-end", gap: 1, height: 34, width: 180 }}>
148
+ <View style={{ flexGrow: 1, flexShrink: 1, flexBasis: "0%", borderTopLeftRadius: 4, borderTopRightRadius: 4, backgroundColor: tokens.primary, height: 6 }} />
149
+ <View style={{ flexGrow: 1, flexShrink: 1, flexBasis: "0%", borderTopLeftRadius: 4, borderTopRightRadius: 4, backgroundColor: tokens.primary, height: 8 }} />
150
+ <View style={{ flexGrow: 1, flexShrink: 1, flexBasis: "0%", borderTopLeftRadius: 4, borderTopRightRadius: 4, backgroundColor: tokens.primary, height: 12 }} />
151
+ <View style={{ flexGrow: 1, flexShrink: 1, flexBasis: "0%", borderTopLeftRadius: 4, borderTopRightRadius: 4, backgroundColor: tokens.primary, height: 10 }} />
152
+ <View style={{ flexGrow: 1, flexShrink: 1, flexBasis: "0%", borderTopLeftRadius: 4, borderTopRightRadius: 4, backgroundColor: tokens.primary, height: 16 }} />
153
+ <View style={{ flexGrow: 1, flexShrink: 1, flexBasis: "0%", borderTopLeftRadius: 4, borderTopRightRadius: 4, backgroundColor: tokens.primary, height: 20 }} />
154
+ <View style={{ flexGrow: 1, flexShrink: 1, flexBasis: "0%", borderTopLeftRadius: 4, borderTopRightRadius: 4, backgroundColor: tokens.primary, height: 18 }} />
155
+ <View style={{ flexGrow: 1, flexShrink: 1, flexBasis: "0%", borderTopLeftRadius: 4, borderTopRightRadius: 4, backgroundColor: tokens.primary, height: 25 }} />
156
+ <View style={{ flexGrow: 1, flexShrink: 1, flexBasis: "0%", borderTopLeftRadius: 4, borderTopRightRadius: 4, backgroundColor: tokens.primary, height: 23 }} />
157
+ <View style={{ flexGrow: 1, flexShrink: 1, flexBasis: "0%", borderTopLeftRadius: 4, borderTopRightRadius: 4, backgroundColor: tokens.primary, height: 29 }} />
158
+ <View style={{ height: 8, width: 8, alignSelf: "flex-start", borderRadius: 9999, backgroundColor: tokens.primary }} />
159
+ </View>
160
+ </View>
161
+ ```
162
+
163
+ **Don't** — A bare line with no value or end dot reads as decoration: you cannot tell the current figure or where it ends.
164
+
165
+ ```tsx
166
+ <View style={{ borderRadius: 8, borderWidth: 1, borderColor: tokens.border, backgroundColor: tokens.card, padding: 20, maxWidth: 200 }}>
167
+ <View style={{ flexDirection: "row", alignItems: "flex-end", gap: 1, height: 34, width: 180 }}>
168
+ <View style={{ flexGrow: 1, flexShrink: 1, flexBasis: "0%", borderTopLeftRadius: 4, borderTopRightRadius: 4, backgroundColor: tokens.primary, height: 6 }} />
169
+ <View style={{ flexGrow: 1, flexShrink: 1, flexBasis: "0%", borderTopLeftRadius: 4, borderTopRightRadius: 4, backgroundColor: tokens.primary, height: 8 }} />
170
+ <View style={{ flexGrow: 1, flexShrink: 1, flexBasis: "0%", borderTopLeftRadius: 4, borderTopRightRadius: 4, backgroundColor: tokens.primary, height: 12 }} />
171
+ <View style={{ flexGrow: 1, flexShrink: 1, flexBasis: "0%", borderTopLeftRadius: 4, borderTopRightRadius: 4, backgroundColor: tokens.primary, height: 10 }} />
172
+ <View style={{ flexGrow: 1, flexShrink: 1, flexBasis: "0%", borderTopLeftRadius: 4, borderTopRightRadius: 4, backgroundColor: tokens.primary, height: 16 }} />
173
+ <View style={{ flexGrow: 1, flexShrink: 1, flexBasis: "0%", borderTopLeftRadius: 4, borderTopRightRadius: 4, backgroundColor: tokens.primary, height: 20 }} />
174
+ <View style={{ flexGrow: 1, flexShrink: 1, flexBasis: "0%", borderTopLeftRadius: 4, borderTopRightRadius: 4, backgroundColor: tokens.primary, height: 18 }} />
175
+ <View style={{ flexGrow: 1, flexShrink: 1, flexBasis: "0%", borderTopLeftRadius: 4, borderTopRightRadius: 4, backgroundColor: tokens.primary, height: 25 }} />
176
+ <View style={{ flexGrow: 1, flexShrink: 1, flexBasis: "0%", borderTopLeftRadius: 4, borderTopRightRadius: 4, backgroundColor: tokens.primary, height: 23 }} />
177
+ <View style={{ flexGrow: 1, flexShrink: 1, flexBasis: "0%", borderTopLeftRadius: 4, borderTopRightRadius: 4, backgroundColor: tokens.primary, height: 29 }} />
178
+ <View style={{ flexGrow: 1, flexShrink: 1, flexBasis: "0%", borderTopLeftRadius: 4, borderTopRightRadius: 4, backgroundColor: tokens.primary, height: 31 }} />
179
+ </View>
180
+ </View>
181
+ ```
182
+
183
+ ### Stacked bar
184
+
185
+ **Do** — Always ship a legend with a colored dot, label, and percentage per segment.
186
+
187
+ ```tsx
188
+ <View style={{ borderRadius: 8, borderWidth: 1, borderColor: tokens.border, backgroundColor: tokens.card, padding: 20, maxWidth: 560 }}>
189
+ <View style={{ marginBottom: 12, flexDirection: "row", overflow: "hidden", borderRadius: 9999, height: 10, width: 520 }}>
190
+ <View style={{ width: "42%", backgroundColor: "#6366f1" }} />
191
+ <View style={{ width: "28%", backgroundColor: "#14b8a6" }} />
192
+ <View style={{ width: "18%", backgroundColor: "#f59e0b" }} />
193
+ <View style={{ width: "12%", backgroundColor: "#f43f5e" }} />
194
+ </View>
195
+ <View style={{ flexDirection: "column", gap: 8 }}>
196
+ <View style={{ flexDirection: "row", alignItems: "center", gap: 10 }}>
197
+ <View style={{ borderRadius: 9999, height: 8, width: 8, backgroundColor: "#6366f1" }} />
198
+ <Text style={{ flexGrow: 1, flexShrink: 1, flexBasis: "0%", fontSize: 14, lineHeight: 20, color: tokens["card-foreground"] }}>Direct</Text>
199
+ <Text style={{ fontSize: 12, lineHeight: 16, color: tokens["muted-foreground"] }}>42%</Text>
200
+ </View>
201
+ <View style={{ flexDirection: "row", alignItems: "center", gap: 10 }}>
202
+ <View style={{ borderRadius: 9999, height: 8, width: 8, backgroundColor: "#14b8a6" }} />
203
+ <Text style={{ flexGrow: 1, flexShrink: 1, flexBasis: "0%", fontSize: 14, lineHeight: 20, color: tokens["card-foreground"] }}>Organic search</Text>
204
+ <Text style={{ fontSize: 12, lineHeight: 16, color: tokens["muted-foreground"] }}>28%</Text>
205
+ </View>
206
+ <View style={{ flexDirection: "row", alignItems: "center", gap: 10 }}>
207
+ <View style={{ borderRadius: 9999, height: 8, width: 8, backgroundColor: "#f59e0b" }} />
208
+ <Text style={{ flexGrow: 1, flexShrink: 1, flexBasis: "0%", fontSize: 14, lineHeight: 20, color: tokens["card-foreground"] }}>Social</Text>
209
+ <Text style={{ fontSize: 12, lineHeight: 16, color: tokens["muted-foreground"] }}>18%</Text>
210
+ </View>
211
+ <View style={{ flexDirection: "row", alignItems: "center", gap: 10 }}>
212
+ <View style={{ borderRadius: 9999, height: 8, width: 8, backgroundColor: "#f43f5e" }} />
213
+ <Text style={{ flexGrow: 1, flexShrink: 1, flexBasis: "0%", fontSize: 14, lineHeight: 20, color: tokens["card-foreground"] }}>Referral</Text>
214
+ <Text style={{ fontSize: 12, lineHeight: 16, color: tokens["muted-foreground"] }}>12%</Text>
215
+ </View>
216
+ </View>
217
+ </View>
218
+ ```
219
+
220
+ **Don't** — Colored segments with no legend force the reader to guess which channel each band represents.
221
+
222
+ ```tsx
223
+ <View style={{ borderRadius: 8, borderWidth: 1, borderColor: tokens.border, backgroundColor: tokens.card, padding: 20, maxWidth: 560 }}>
224
+ <View style={{ flexDirection: "row", overflow: "hidden", borderRadius: 9999, height: 10, width: 520 }}>
225
+ <View style={{ width: "42%", backgroundColor: "#6366f1" }} />
226
+ <View style={{ width: "28%", backgroundColor: "#14b8a6" }} />
227
+ <View style={{ width: "18%", backgroundColor: "#f59e0b" }} />
228
+ <View style={{ width: "12%", backgroundColor: "#f43f5e" }} />
229
+ </View>
230
+ </View>
231
+ ```
232
+
233
+ ### Gauge
234
+
235
+ **Do** — Put a muted track behind the fill and the numeric value plus label in the center.
236
+
237
+ ```tsx
238
+ <View style={{ borderRadius: 8, borderWidth: 1, borderColor: tokens.border, backgroundColor: tokens.card, padding: 20, maxWidth: 200, alignItems: "center" }}>
239
+ <View style={{ alignItems: "center", justifyContent: "center" }}>
240
+ <View style={{ borderRadius: 9999, borderWidth: 8, borderColor: tokens.muted, height: 120, width: 120 }} />
241
+ <View style={{ position: "absolute", alignItems: "center", justifyContent: "center" }}>
242
+ <Text style={{ fontSize: 24, lineHeight: 32, fontWeight: "600", color: tokens["card-foreground"] }}>72%</Text>
243
+ <Text style={{ fontSize: 12, lineHeight: 16, color: tokens["muted-foreground"] }}>Uptime</Text>
244
+ </View>
245
+ </View>
246
+ </View>
247
+ ```
248
+
249
+ **Don't** — An arc with no track and no number: there is no baseline to read the fill against and no exact value.
250
+
251
+ ```tsx
252
+ <View style={{ borderRadius: 8, borderWidth: 1, borderColor: tokens.border, backgroundColor: tokens.card, padding: 20, maxWidth: 200, alignItems: "center" }}>
253
+ <View style={{ borderRadius: 9999, borderWidth: 8, borderColor: tokens.primary, height: 120, width: 120 }} />
254
+ </View>
255
+ ```
256
+
257
+ ### Heatmap
258
+
259
+ **Do** — Pair the grid with a discrete less-to-more legend so the density scale is legible.
260
+
261
+ ```tsx
262
+ <View style={{ borderRadius: 8, borderWidth: 1, borderColor: tokens.border, backgroundColor: tokens.card, padding: 20, maxWidth: 260 }}>
263
+ <View style={{ flexDirection: "row", flexWrap: "wrap", gap: 4, maxWidth: 220 }}>
264
+ <View style={{ borderRadius: 2, height: 18, width: 18, backgroundColor: "rgba(99,102,241,0.15)" }} />
265
+ <View style={{ borderRadius: 2, height: 18, width: 18, backgroundColor: "rgba(99,102,241,0.4)" }} />
266
+ <View style={{ borderRadius: 2, height: 18, width: 18, backgroundColor: "rgba(99,102,241,0.7)" }} />
267
+ <View style={{ borderRadius: 2, height: 18, width: 18, backgroundColor: "rgba(99,102,241,1)" }} />
268
+ <View style={{ borderRadius: 2, height: 18, width: 18, backgroundColor: "rgba(99,102,241,0.55)" }} />
269
+ <View style={{ borderRadius: 2, height: 18, width: 18, backgroundColor: "rgba(99,102,241,0.25)" }} />
270
+ <View style={{ borderRadius: 2, height: 18, width: 18, backgroundColor: "rgba(99,102,241,0.85)" }} />
271
+ <View style={{ borderRadius: 2, height: 18, width: 18, backgroundColor: "rgba(99,102,241,0.35)" }} />
272
+ <View style={{ borderRadius: 2, height: 18, width: 18, backgroundColor: "rgba(99,102,241,0.6)" }} />
273
+ <View style={{ borderRadius: 2, height: 18, width: 18, backgroundColor: "rgba(99,102,241,0.9)" }} />
274
+ <View style={{ borderRadius: 2, height: 18, width: 18, backgroundColor: "rgba(99,102,241,0.2)" }} />
275
+ <View style={{ borderRadius: 2, height: 18, width: 18, backgroundColor: "rgba(99,102,241,0.5)" }} />
276
+ <View style={{ borderRadius: 2, height: 18, width: 18, backgroundColor: "rgba(99,102,241,0.75)" }} />
277
+ <View style={{ borderRadius: 2, height: 18, width: 18, backgroundColor: "rgba(99,102,241,0.3)" }} />
278
+ <View style={{ borderRadius: 2, height: 18, width: 18, backgroundColor: "rgba(99,102,241,0.95)" }} />
279
+ <View style={{ borderRadius: 2, height: 18, width: 18, backgroundColor: "rgba(99,102,241,0.45)" }} />
280
+ <View style={{ borderRadius: 2, height: 18, width: 18, backgroundColor: "rgba(99,102,241,0.65)" }} />
281
+ <View style={{ borderRadius: 2, height: 18, width: 18, backgroundColor: "rgba(99,102,241,0.1)" }} />
282
+ <View style={{ borderRadius: 2, height: 18, width: 18, backgroundColor: "rgba(99,102,241,0.8)" }} />
283
+ <View style={{ borderRadius: 2, height: 18, width: 18, backgroundColor: "rgba(99,102,241,0.4)" }} />
284
+ <View style={{ borderRadius: 2, height: 18, width: 18, backgroundColor: "rgba(99,102,241,0.7)" }} />
285
+ </View>
286
+ <View style={{ marginTop: 12, flexDirection: "row", alignItems: "center", gap: 8 }}>
287
+ <Text style={{ fontSize: 12, lineHeight: 16, color: tokens["muted-foreground"] }}>Less</Text>
288
+ <View style={{ borderRadius: 2, height: 12, width: 12, backgroundColor: "rgba(99,102,241,0.2)" }} />
289
+ <View style={{ borderRadius: 2, height: 12, width: 12, backgroundColor: "rgba(99,102,241,0.4)" }} />
290
+ <View style={{ borderRadius: 2, height: 12, width: 12, backgroundColor: "rgba(99,102,241,0.6)" }} />
291
+ <View style={{ borderRadius: 2, height: 12, width: 12, backgroundColor: "rgba(99,102,241,0.8)" }} />
292
+ <View style={{ borderRadius: 2, height: 12, width: 12, backgroundColor: "rgba(99,102,241,1)" }} />
293
+ <Text style={{ fontSize: 12, lineHeight: 16, color: tokens["muted-foreground"] }}>More</Text>
294
+ </View>
295
+ </View>
296
+ ```
297
+
298
+ **Don't** — A density grid with no legend leaves the alpha-to-value mapping a mystery.
299
+
300
+ ```tsx
301
+ <View style={{ borderRadius: 8, borderWidth: 1, borderColor: tokens.border, backgroundColor: tokens.card, padding: 20, maxWidth: 260 }}>
302
+ <View style={{ flexDirection: "row", flexWrap: "wrap", gap: 4, maxWidth: 220 }}>
303
+ <View style={{ borderRadius: 2, height: 18, width: 18, backgroundColor: "rgba(99,102,241,0.15)" }} />
304
+ <View style={{ borderRadius: 2, height: 18, width: 18, backgroundColor: "rgba(99,102,241,0.4)" }} />
305
+ <View style={{ borderRadius: 2, height: 18, width: 18, backgroundColor: "rgba(99,102,241,0.7)" }} />
306
+ <View style={{ borderRadius: 2, height: 18, width: 18, backgroundColor: "rgba(99,102,241,1)" }} />
307
+ <View style={{ borderRadius: 2, height: 18, width: 18, backgroundColor: "rgba(99,102,241,0.55)" }} />
308
+ <View style={{ borderRadius: 2, height: 18, width: 18, backgroundColor: "rgba(99,102,241,0.25)" }} />
309
+ <View style={{ borderRadius: 2, height: 18, width: 18, backgroundColor: "rgba(99,102,241,0.85)" }} />
310
+ <View style={{ borderRadius: 2, height: 18, width: 18, backgroundColor: "rgba(99,102,241,0.35)" }} />
311
+ <View style={{ borderRadius: 2, height: 18, width: 18, backgroundColor: "rgba(99,102,241,0.6)" }} />
312
+ <View style={{ borderRadius: 2, height: 18, width: 18, backgroundColor: "rgba(99,102,241,0.9)" }} />
313
+ <View style={{ borderRadius: 2, height: 18, width: 18, backgroundColor: "rgba(99,102,241,0.2)" }} />
314
+ <View style={{ borderRadius: 2, height: 18, width: 18, backgroundColor: "rgba(99,102,241,0.5)" }} />
315
+ <View style={{ borderRadius: 2, height: 18, width: 18, backgroundColor: "rgba(99,102,241,0.75)" }} />
316
+ <View style={{ borderRadius: 2, height: 18, width: 18, backgroundColor: "rgba(99,102,241,0.3)" }} />
317
+ <View style={{ borderRadius: 2, height: 18, width: 18, backgroundColor: "rgba(99,102,241,0.95)" }} />
318
+ <View style={{ borderRadius: 2, height: 18, width: 18, backgroundColor: "rgba(99,102,241,0.45)" }} />
319
+ <View style={{ borderRadius: 2, height: 18, width: 18, backgroundColor: "rgba(99,102,241,0.65)" }} />
320
+ <View style={{ borderRadius: 2, height: 18, width: 18, backgroundColor: "rgba(99,102,241,0.1)" }} />
321
+ <View style={{ borderRadius: 2, height: 18, width: 18, backgroundColor: "rgba(99,102,241,0.8)" }} />
322
+ <View style={{ borderRadius: 2, height: 18, width: 18, backgroundColor: "rgba(99,102,241,0.4)" }} />
323
+ <View style={{ borderRadius: 2, height: 18, width: 18, backgroundColor: "rgba(99,102,241,0.7)" }} />
324
+ </View>
325
+ </View>
326
+ ```