@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,135 @@
1
+ # Skeletons
2
+
3
+ Placeholders for loading content.
4
+
5
+ ## Usage
6
+
7
+ ```tsx
8
+ <Skeleton text animate style={{ width: "60%" }} />
9
+ ```
10
+
11
+ ## Variants
12
+
13
+ ### Shape - avatar
14
+
15
+ ```tsx
16
+ <Skeleton avatar animate />
17
+ ```
18
+
19
+ ### Shape - button
20
+
21
+ ```tsx
22
+ <Skeleton button animate style={{ width: "60%" }} />
23
+ ```
24
+
25
+ ### Shape - card
26
+
27
+ ```tsx
28
+ <Skeleton card animate />
29
+ ```
30
+
31
+ ### Shape - list
32
+
33
+ ```tsx
34
+ <Skeleton list animate />
35
+ ```
36
+
37
+ ### Shape - table
38
+
39
+ ```tsx
40
+ <Skeleton table animate />
41
+ ```
42
+
43
+ ## Do & Don't
44
+
45
+ ### text
46
+
47
+ **Do** — Vary the line widths and shorten the last line so it reads like real wrapped text.
48
+
49
+ ```tsx
50
+ <View style={{ width: 320, flexDirection: "column", gap: 6 }}>
51
+ <Skeleton text animate style={{ width: "100%" }} />
52
+ <Skeleton text animate style={{ width: "95%" }} />
53
+ <Skeleton text animate style={{ width: "60%" }} />
54
+ </View>
55
+ ```
56
+
57
+ **Don't** — Three full-width lines read as a solid block, not as a paragraph of prose.
58
+
59
+ ```tsx
60
+ <View style={{ width: 320, flexDirection: "column", gap: 6 }}>
61
+ <Skeleton text animate style={{ width: "100%" }} />
62
+ <Skeleton text animate style={{ width: "100%" }} />
63
+ <Skeleton text animate style={{ width: "100%" }} />
64
+ </View>
65
+ ```
66
+
67
+ ### avatar
68
+
69
+ **Do** — Match the avatar's circle exactly so the photo drops in with no shift.
70
+
71
+ ```tsx
72
+ <Skeleton avatar animate />
73
+ ```
74
+
75
+ **Don't** — A square placeholder for a round avatar snaps shape the instant the image loads.
76
+
77
+ ```tsx
78
+ <View style={{ backgroundColor: tokens.muted, borderRadius: 6, width: 40, height: 40 }} />
79
+ ```
80
+
81
+ ### button
82
+
83
+ **Do** — Size the placeholder to the button's real height and width (h-9, content-fit).
84
+
85
+ ```tsx
86
+ <Skeleton button animate />
87
+ ```
88
+
89
+ **Don't** — An oversized bar overstates a button and the layout jumps when the real control mounts.
90
+
91
+ ```tsx
92
+ <View style={{ backgroundColor: tokens.muted, width: 320, height: 72, borderRadius: 6 }} />
93
+ ```
94
+
95
+ ### card
96
+
97
+ **Do** — Mirror the real layout (avatar circle, text lines) so the swap is seamless.
98
+
99
+ ```tsx
100
+ <Skeleton card animate />
101
+ ```
102
+
103
+ **Don't** — A generic block that ignores the content's shape causes a jarring shift when it loads.
104
+
105
+ ```tsx
106
+ <View style={{ backgroundColor: tokens.muted, borderRadius: 6, width: 320, height: 88 }} />
107
+ ```
108
+
109
+ ### list
110
+
111
+ **Do** — Repeat a per-row placeholder so the avatar-and-text rhythm matches the loaded list.
112
+
113
+ ```tsx
114
+ <Skeleton list animate />
115
+ ```
116
+
117
+ **Don't** — One tall block hides the row rhythm, so the list reflows when each item appears.
118
+
119
+ ```tsx
120
+ <View style={{ backgroundColor: tokens.muted, width: 400, height: 120, borderRadius: 6 }} />
121
+ ```
122
+
123
+ ### table
124
+
125
+ **Do** — Lay placeholders out on the real column grid so each cell stays put when it fills in.
126
+
127
+ ```tsx
128
+ <Skeleton table animate />
129
+ ```
130
+
131
+ **Don't** — A single rectangle gives no column structure; cells shift sideways once data lands.
132
+
133
+ ```tsx
134
+ <View style={{ backgroundColor: tokens.muted, width: 400, height: 120, borderRadius: 6 }} />
135
+ ```
@@ -0,0 +1,117 @@
1
+ import { type ViewStyle } from "react-native";
2
+ import { type ColorTokens } from "../../style/index.js";
3
+
4
+ // Co-located Skeleton styles. Every placeholder shares one muted fill (a color,
5
+ // so it reads the active token); the rest is layout-only (line heights, the
6
+ // avatar/button footprints, and the composite card/list/table scaffolds). The
7
+ // component resolves its shape and size axes and composes these fragments.
8
+
9
+ export type Shape = "text" | "avatar" | "button" | "card" | "list" | "table";
10
+
11
+ // The muted fill every placeholder shares (was `bg-muted`).
12
+ export function fill(tokens: ColorTokens): ViewStyle {
13
+ return { backgroundColor: tokens.muted };
14
+ }
15
+
16
+ // --- line (the building block for text and composite shapes) ----------------
17
+
18
+ // A single muted line base: full width, the default line height (`h-3.5`), the
19
+ // small radius (`rounded`). Was `h-3.5 rounded w-full`.
20
+ export const lineBase: ViewStyle = { height: 14, borderRadius: 4, width: "100%" };
21
+
22
+ // --- text shape: line height per size --------------------------------------
23
+
24
+ // Line height per size; the default line reads like a single row of text.
25
+ // `h-4` -> 16, `h-3` -> 12, `h-3.5` -> 14.
26
+ export function lineHeight(p: { small?: boolean; large?: boolean }): ViewStyle {
27
+ if (p.large) return { height: 16 };
28
+ if (p.small) return { height: 12 };
29
+ return { height: 14 };
30
+ }
31
+
32
+ // The text-line radius (`rounded`) plus its full-width default (`w-full`), which
33
+ // the caller's `style` escape hatch can override (e.g. width: "60%").
34
+ export const textBase: ViewStyle = { borderRadius: 4, width: "100%" };
35
+
36
+ // --- avatar shape: diameter per size ----------------------------------------
37
+
38
+ // Avatar diameter per size + the full radius (`rounded-full`).
39
+ // `w-12 h-12` -> 48, `w-8 h-8` -> 32, `w-10 h-10` -> 40.
40
+ export function avatarSize(p: { small?: boolean; large?: boolean }): ViewStyle {
41
+ const d = p.large ? 48 : p.small ? 32 : 40;
42
+ return { width: d, height: d, borderRadius: 9999 };
43
+ }
44
+
45
+ // --- button shape: footprint per size ---------------------------------------
46
+
47
+ // Button placeholder footprint per size + the medium radius (`rounded-md`);
48
+ // mirrors the real control's height. `h-12 w-32` -> {48,128},
49
+ // `h-8 w-20` -> {32,80}, `h-9 w-28` -> {36,112}.
50
+ export function buttonSize(p: { small?: boolean; large?: boolean }): ViewStyle {
51
+ if (p.large) return { height: 48, width: 128, borderRadius: 6 };
52
+ if (p.small) return { height: 32, width: 80, borderRadius: 6 };
53
+ return { height: 36, width: 112, borderRadius: 6 };
54
+ }
55
+
56
+ // --- card shape -------------------------------------------------------------
57
+
58
+ // The card surface: rounded-lg border on the card fill, capped width, padded.
59
+ // `rounded-lg border border-border bg-card max-w-[320px] p-4`.
60
+ export function cardSurface(tokens: ColorTokens): ViewStyle {
61
+ return {
62
+ borderRadius: 8,
63
+ borderWidth: 1,
64
+ borderColor: tokens.border,
65
+ backgroundColor: tokens.card,
66
+ maxWidth: 320,
67
+ padding: 16,
68
+ };
69
+ }
70
+
71
+ // The card's identity row (avatar + two lines). `flex-row items-center gap-3 mb-4`.
72
+ export const cardRow: ViewStyle = { flexDirection: "row", alignItems: "center", gap: 12, marginBottom: 16 };
73
+
74
+ // The card avatar pulse. `shrink-0 rounded-full w-10 h-10`.
75
+ export const cardAvatar: ViewStyle = { flexShrink: 0, borderRadius: 9999, width: 40, height: 40 };
76
+
77
+ // The flex column holding the avatar's two lines. `flex-1`.
78
+ export const flexFill: ViewStyle = { flexGrow: 1, flexShrink: 1, flexBasis: "0%" };
79
+
80
+ // Line widths inside the card (`w-[70%]` / `w-[40%]` / `w-[80%]`).
81
+ export const cardLine70: ViewStyle = { width: "70%" };
82
+ export const cardLine40: ViewStyle = { width: "40%", marginTop: 6 };
83
+ export const cardLine80: ViewStyle = { width: "80%", marginTop: 6 };
84
+
85
+ // --- list shape -------------------------------------------------------------
86
+
87
+ // The list container. `flex-col gap-4 max-w-[400px]`.
88
+ export const listContainer: ViewStyle = { flexDirection: "column", gap: 16, maxWidth: 400 };
89
+
90
+ // A list row. `flex-row items-center gap-3`.
91
+ export const listRow: ViewStyle = { flexDirection: "row", alignItems: "center", gap: 12 };
92
+
93
+ // The list row's avatar pulse. `rounded-full w-8 h-8`.
94
+ export const listAvatar: ViewStyle = { borderRadius: 9999, width: 32, height: 32 };
95
+
96
+ // The list row's primary line spacing. `mb-1.5`.
97
+ export const listLineGap: ViewStyle = { marginBottom: 6 };
98
+
99
+ // The list row's trailing meta line. `w-10`.
100
+ export const w10: ViewStyle = { width: 40 };
101
+
102
+ // --- table shape ------------------------------------------------------------
103
+
104
+ // The table container. `max-w-[560px]`.
105
+ export const tableContainer: ViewStyle = { maxWidth: 560 };
106
+
107
+ // A table row. `flex-row items-center gap-3 py-3`.
108
+ export const tableRow: ViewStyle = { flexDirection: "row", alignItems: "center", gap: 12, paddingVertical: 12 };
109
+
110
+ // The hairline divider between table rows (omitted on the last row).
111
+ // `border-b border-border`.
112
+ export function tableDivider(tokens: ColorTokens): ViewStyle {
113
+ return { borderBottomWidth: 1, borderColor: tokens.border };
114
+ }
115
+
116
+ // The table row's trailing cell line. `w-20`.
117
+ export const w20: ViewStyle = { width: 80 };
@@ -0,0 +1,145 @@
1
+ import { useEffect, useRef } from "react";
2
+ import { Animated } from "react-native";
3
+ import { View, useTheme, type StyleProp, type ViewStyle } from "../../style/index.js";
4
+ import * as s from "./skeleton.styles.js";
5
+
6
+ // Skeleton: muted placeholder blocks shown while content loads. A single shape
7
+ // (text line, avatar, button, or a composite card/list/table scaffold) built
8
+ // from one muted fill, optionally pulsing. The size axis scales the line height
9
+ // and the avatar/button footprint.
10
+ //
11
+ // Boolean-prop API: one boolean per option, grouped by axis, first-match
12
+ // precedence within an axis (mirrors Button's intentOf).
13
+
14
+ export interface SkeletonProps {
15
+ // Shape (pick one; default is a single text line).
16
+ text?: boolean;
17
+ avatar?: boolean;
18
+ button?: boolean;
19
+ card?: boolean;
20
+ list?: boolean;
21
+ table?: boolean;
22
+ // Size (pick one). Scales the line height and the avatar/button footprint.
23
+ small?: boolean;
24
+ large?: boolean;
25
+ /** Subtle opacity pulse while content loads. */
26
+ animate?: boolean;
27
+ /** Escape hatch for layout/positioning composition (mainly sizing, e.g. width). */
28
+ style?: StyleProp<ViewStyle>;
29
+ }
30
+
31
+ type Shape = s.Shape;
32
+
33
+ // Shape precedence when more than one is passed: first match wins.
34
+ function shapeOf(p: SkeletonProps): Shape {
35
+ if (p.text) return "text";
36
+ if (p.avatar) return "avatar";
37
+ if (p.button) return "button";
38
+ if (p.card) return "card";
39
+ if (p.list) return "list";
40
+ if (p.table) return "table";
41
+ return "text";
42
+ }
43
+
44
+ /** A pulsing or static muted block. The resolved width/height/fill go on the
45
+ * Animated.View itself so percentage widths resolve against the real parent
46
+ * (a nested View would collapse `w-[60%]` against an auto-width wrapper). */
47
+ function Pulse({ animate, style }: { animate?: boolean; style: StyleProp<ViewStyle> }) {
48
+ const opacity = useRef(new Animated.Value(1)).current;
49
+
50
+ useEffect(() => {
51
+ if (!animate) {
52
+ opacity.setValue(1);
53
+ return;
54
+ }
55
+ const loop = Animated.loop(
56
+ Animated.sequence([
57
+ Animated.timing(opacity, { toValue: 0.5, duration: 600, useNativeDriver: false }),
58
+ Animated.timing(opacity, { toValue: 1, duration: 600, useNativeDriver: false }),
59
+ ]),
60
+ );
61
+ loop.start();
62
+ return () => loop.stop();
63
+ }, [animate, opacity]);
64
+
65
+ return <Animated.View style={[style, { opacity: animate ? opacity : 1 }]} />;
66
+ }
67
+
68
+ // A single muted line; the building block for text and the composite shapes.
69
+ // The muted fill + the line base (`h-3.5 rounded w-full`), then any width/margin
70
+ // overrides the caller layers on.
71
+ function Line({ animate, style }: { animate?: boolean; style?: StyleProp<ViewStyle> }) {
72
+ const { tokens } = useTheme();
73
+ return <Pulse animate={animate} style={[s.fill(tokens), s.lineBase, style]} />;
74
+ }
75
+
76
+ export function Skeleton(props: SkeletonProps) {
77
+ const { animate, style } = props;
78
+ const { tokens } = useTheme();
79
+ const shape = shapeOf(props);
80
+
81
+ if (shape === "avatar") {
82
+ return <Pulse animate={animate} style={[s.fill(tokens), s.avatarSize(props), style]} />;
83
+ }
84
+
85
+ if (shape === "button") {
86
+ return <Pulse animate={animate} style={[s.fill(tokens), s.buttonSize(props), style]} />;
87
+ }
88
+
89
+ if (shape === "card") {
90
+ return (
91
+ <View style={[s.cardSurface(tokens), style]}>
92
+ <View style={s.cardRow}>
93
+ <Pulse animate={animate} style={[s.fill(tokens), s.cardAvatar]} />
94
+ <View style={s.flexFill}>
95
+ <Line animate={animate} style={s.cardLine70} />
96
+ <Line animate={animate} style={s.cardLine40} />
97
+ </View>
98
+ </View>
99
+ <Line animate={animate} />
100
+ <Line animate={animate} style={s.cardLine80} />
101
+ </View>
102
+ );
103
+ }
104
+
105
+ if (shape === "list") {
106
+ const Row = ({ a, b }: { a: StyleProp<ViewStyle>; b: StyleProp<ViewStyle> }) => (
107
+ <View style={s.listRow}>
108
+ <Pulse animate={animate} style={[s.fill(tokens), s.listAvatar]} />
109
+ <View style={s.flexFill}>
110
+ <Line animate={animate} style={[s.listLineGap, a]} />
111
+ <Line animate={animate} style={b} />
112
+ </View>
113
+ <Line animate={animate} style={s.w10} />
114
+ </View>
115
+ );
116
+ return (
117
+ <View style={[s.listContainer, style]}>
118
+ <Row a={{ width: "70%" }} b={{ width: "50%" }} />
119
+ <Row a={{ width: "55%" }} b={{ width: "35%" }} />
120
+ </View>
121
+ );
122
+ }
123
+
124
+ if (shape === "table") {
125
+ const Row = ({ a, b, last }: { a: StyleProp<ViewStyle>; b: StyleProp<ViewStyle>; last?: boolean }) => (
126
+ <View style={[s.tableRow, !last ? s.tableDivider(tokens) : null]}>
127
+ <Line animate={animate} style={s.w10} />
128
+ <Line animate={animate} style={[s.flexFill, a]} />
129
+ <Line animate={animate} style={[s.flexFill, b]} />
130
+ <Line animate={animate} style={s.w20} />
131
+ </View>
132
+ );
133
+ return (
134
+ <View style={[s.tableContainer, style]}>
135
+ <Row a={{ width: "70%" }} b={{ width: "50%" }} />
136
+ <Row a={{ width: "80%" }} b={{ width: "60%" }} />
137
+ <Row a={{ width: "65%" }} b={{ width: "45%" }} last />
138
+ </View>
139
+ );
140
+ }
141
+
142
+ // Default: a single text line, full width by default; style carries the width
143
+ // override (e.g. width: "60%") and any other layout.
144
+ return <Pulse animate={animate} style={[s.fill(tokens), s.lineHeight(props), s.textBase, style]} />;
145
+ }
@@ -0,0 +1,7 @@
1
+ import { createSpinner } from "./spinner.shared.js";
2
+ import { androidSkin } from "./spinner.styles.js";
3
+
4
+ // Material 3 Spinner: the indeterminate CircularProgressIndicator, a single
5
+ // sweeping arc. Metro resolves this file on Android; the docs import it for preview.
6
+ export const Spinner = createSpinner(androidSkin);
7
+ export type { SpinnerProps } from "./spinner.shared.js";
@@ -0,0 +1,7 @@
1
+ import { createSpinner } from "./spinner.shared.js";
2
+ import { iosSkin } from "./spinner.styles.js";
3
+
4
+ // iOS (HIG) Spinner: the UIActivityIndicatorView ring of fading spokes. Metro
5
+ // resolves this file on iOS; the docs import it for preview.
6
+ export const Spinner = createSpinner(iosSkin);
7
+ export type { SpinnerProps } from "./spinner.shared.js";
@@ -0,0 +1,94 @@
1
+ # Spinner
2
+
3
+ Animated loading spinner in three sizes.
4
+
5
+ ## Usage
6
+
7
+ ```tsx
8
+ <Spinner />
9
+ ```
10
+
11
+ ## Variants
12
+
13
+ ### Size - sm
14
+
15
+ ```tsx
16
+ <Spinner small />
17
+ ```
18
+
19
+ ### Size - lg
20
+
21
+ ```tsx
22
+ <Spinner large />
23
+ ```
24
+
25
+ ## Do & Don't
26
+
27
+ **Do** — Pair longer waits with a short label so the spinner has context.
28
+
29
+ ```tsx
30
+ <View style={{ flexDirection: "row", alignItems: "center", gap: 8 }}>
31
+ <Spinner small />
32
+ <Text style={{ fontSize: 14, lineHeight: 20, color: tokens["muted-foreground"] }}>Loading…</Text>
33
+ </View>
34
+ ```
35
+
36
+ **Don't** — A bare spinner with no label leaves users guessing what is happening and for how long.
37
+
38
+ ```tsx
39
+ <Spinner />
40
+ ```
41
+
42
+ ### sm
43
+
44
+ **Do** — Use the small size inline: inside a button or beside a line of text where its scale matches the type.
45
+
46
+ ```tsx
47
+ <Button loading disabled>Saving…</Button>
48
+ ```
49
+
50
+ **Don't** — The 4×4 spinner is too small to anchor a full panel; alone in open space it reads as a stray dot.
51
+
52
+ ```tsx
53
+ <View style={{ height: 128, alignItems: "center", justifyContent: "center", borderRadius: 8, borderWidth: 1, borderStyle: "dashed", borderColor: tokens.border }}>
54
+ <Spinner small />
55
+ </View>
56
+ ```
57
+
58
+ ### default
59
+
60
+ **Do** — Keep the default square and centered with a label for small content panels and cards.
61
+
62
+ ```tsx
63
+ <View style={{ flexDirection: "column", alignItems: "center", gap: 8, borderRadius: 8, borderWidth: 1, borderColor: tokens.border, padding: 24 }}>
64
+ <Spinner />
65
+ <Text style={{ fontSize: 14, lineHeight: 20, color: tokens["muted-foreground"] }}>Loading…</Text>
66
+ </View>
67
+ ```
68
+
69
+ **Don't** — Don't stretch it with conflicting w/h utilities; a spinner must stay a perfect circle to spin cleanly.
70
+
71
+ ```tsx
72
+ <View style={{ borderRadius: 6, backgroundColor: tokens.muted, padding: 12 }}>
73
+ <View style={{ width: 48, height: 20, borderRadius: 9999, borderWidth: 2, borderColor: tokens.muted, borderTopColor: tokens.foreground }} />
74
+ </View>
75
+ ```
76
+
77
+ ### lg
78
+
79
+ **Do** — Reserve the large size for section- or page-level loading, centered in the empty content area.
80
+
81
+ ```tsx
82
+ <View style={{ height: 160, flexDirection: "column", alignItems: "center", justifyContent: "center", gap: 12, borderRadius: 8, borderWidth: 1, borderColor: tokens.border }}>
83
+ <Spinner large />
84
+ <Text style={{ fontSize: 14, lineHeight: 20, color: tokens["muted-foreground"] }}>Loading dashboard…</Text>
85
+ </View>
86
+ ```
87
+
88
+ **Don't** — The 8×8 spinner overflows a small control; cramming the large size into a button breaks its height.
89
+
90
+ ```tsx
91
+ <Pressable style={{ flexDirection: "row", alignItems: "center", justifyContent: "center", height: 32, borderRadius: 6, paddingHorizontal: 12, backgroundColor: tokens.primary, opacity: 0.5 }}>
92
+ <Spinner large />
93
+ </Pressable>
94
+ ```
@@ -0,0 +1,92 @@
1
+ import { type ReactElement, useEffect, useRef } from "react";
2
+ import { Animated, Easing } from "react-native";
3
+ import { useTheme, type ColorTokens } from "../../style/index.js";
4
+ import { type Tone, TONE_TOKEN } from "./spinner.styles.js";
5
+
6
+ // Shared Spinner shell. Uses React Native's primitives DIRECTLY (no engine
7
+ // className layer) and reads the active brand tokens via useTheme, so the arc/
8
+ // spoke color follows light/dark and the glass surface. The shared structure
9
+ // (size/tone precedence, the continuous-rotation animation, accessibility) lives
10
+ // here once; a platform file supplies only its rendered shape (a SpinnerSkin) and
11
+ // calls createSpinner. The web skin keeps the current ActivityIndicator look; the
12
+ // iOS skin draws the ring of fading spokes (UIActivityIndicatorView); the Android
13
+ // skin draws the single sweeping arc (M3 CircularProgressIndicator).
14
+
15
+ export interface SpinnerProps {
16
+ // Size (pick one; default sits between small and large).
17
+ small?: boolean;
18
+ large?: boolean;
19
+ // Tone (pick one; default is the foreground arc color).
20
+ primary?: boolean;
21
+ muted?: boolean;
22
+ foreground?: boolean;
23
+ /** Accessible description of what is loading. */
24
+ accessibilityLabel?: string;
25
+ }
26
+
27
+ // Tone precedence when more than one is passed: first match wins.
28
+ function toneOf(p: SpinnerProps): Tone {
29
+ if (p.primary) return "primary";
30
+ if (p.muted) return "muted";
31
+ if (p.foreground) return "foreground";
32
+ return "foreground";
33
+ }
34
+
35
+ // Three distinct diameters (px) so each size axis value renders a different
36
+ // spinner. Precedence within the size axis: large > small > default (first
37
+ // match wins). Kept identical to the original component.
38
+ function sizeOf(p: SpinnerProps): number {
39
+ if (p.large) return 32;
40
+ if (p.small) return 16;
41
+ return 20;
42
+ }
43
+
44
+ // What a platform skin owns: how it draws the spinner for a given diameter and
45
+ // color. `rotate` is the shared Animated spin value (0..1, mapped to 0..360deg by
46
+ // the skin if it spins the whole shape). The skin returns a ready-to-mount node.
47
+ export interface SpinnerSkin {
48
+ render: (args: {
49
+ size: number;
50
+ color: string;
51
+ rotate: Animated.Value;
52
+ tokens: ColorTokens;
53
+ }) => ReactElement;
54
+ }
55
+
56
+ /** Build a Spinner component from a platform skin. */
57
+ export function createSpinner(skin: SpinnerSkin) {
58
+ return function Spinner(props: SpinnerProps) {
59
+ const { accessibilityLabel } = props;
60
+ const { tokens } = useTheme();
61
+ const tone = toneOf(props);
62
+ const size = sizeOf(props);
63
+ const color = tokens[TONE_TOKEN[tone]];
64
+
65
+ // One continuous rotation per ~900ms, looping forever. The skins that spin a
66
+ // drawn shape (iOS spokes, Android arc) interpolate this 0..1 value to
67
+ // 0..360deg; the web ActivityIndicator animates itself and ignores it.
68
+ const rotate = useRef(new Animated.Value(0)).current;
69
+ useEffect(() => {
70
+ const loop = Animated.loop(
71
+ Animated.timing(rotate, {
72
+ toValue: 1,
73
+ duration: 900,
74
+ easing: Easing.linear,
75
+ useNativeDriver: true,
76
+ }),
77
+ );
78
+ loop.start();
79
+ return () => loop.stop();
80
+ }, [rotate]);
81
+
82
+ return (
83
+ <Animated.View
84
+ accessibilityRole="progressbar"
85
+ accessibilityLabel={accessibilityLabel ?? "Loading"}
86
+ style={{ width: size, height: size, alignItems: "center", justifyContent: "center" }}
87
+ >
88
+ {skin.render({ size, color, rotate, tokens })}
89
+ </Animated.View>
90
+ );
91
+ };
92
+ }