@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
package/README.md ADDED
@@ -0,0 +1,108 @@
1
+ # @olympusoss/canvas
2
+
3
+ Universal React Native UI kit. Canvas runs natively on iOS and Android, and on
4
+ the web through React Native Web, from a single component API. Components are
5
+ styled with semantic boolean props and authored desktop-first, so they adapt
6
+ cleanly from large desktop down to phone.
7
+
8
+ ## Install
9
+
10
+ ```bash
11
+ npm install @olympusoss/canvas
12
+ ```
13
+
14
+ Canvas relies on three peer dependencies that you install alongside it:
15
+
16
+ ```bash
17
+ npm install react react-native react-native-svg
18
+ ```
19
+
20
+ For web rendering, add `react-native-web` to your app and alias `react-native`
21
+ to `react-native-web` in your bundler, the same way any React Native Web project
22
+ does.
23
+
24
+ ## Quick Start
25
+
26
+ Wrap your app in the `ThemeProvider`, then compose components imported
27
+ from `@olympusoss/canvas`. The provider supplies the active color scheme and
28
+ token map; omit `scheme` to follow the OS appearance, or force it with
29
+ `scheme="light"` / `scheme="dark"`.
30
+
31
+ ```jsx
32
+ import { ThemeProvider, Card, CardHeader, CardTitle, CardContent, Button } from "@olympusoss/canvas";
33
+
34
+ export default function App() {
35
+ return (
36
+ <ThemeProvider>
37
+ <Card padded>
38
+ <CardHeader>
39
+ <CardTitle>Welcome to Canvas</CardTitle>
40
+ </CardHeader>
41
+ <CardContent>
42
+ <Button primary large onPress={() => console.log("saved")}>
43
+ Save
44
+ </Button>
45
+ </CardContent>
46
+ </Card>
47
+ </ThemeProvider>
48
+ );
49
+ }
50
+ ```
51
+
52
+ The same component tree renders natively on iOS and Android and, through React
53
+ Native Web, in the browser. There is no separate web component set to learn.
54
+
55
+ ## Semantic boolean props
56
+
57
+ Styling is done with flat boolean props. Each style choice is its own prop,
58
+ named for the meaning it carries, and passing the prop turns it on. The prop
59
+ name is the value, so the call site reads like natural language ("a primary,
60
+ large button").
61
+
62
+ ```jsx
63
+ <Button primary large>Save</Button>
64
+ <Button destructive>Delete</Button>
65
+ <Button ghost small>Cancel</Button>
66
+ <Card glass>...</Card>
67
+ ```
68
+
69
+ Props are grouped into orthogonal axes (intent, size, surface, density, and
70
+ stacking state/layout flags). Props on different axes combine freely; props
71
+ within one axis are mutually exclusive, so you pass at most one and the
72
+ component resolves any conflict by a fixed precedence.
73
+
74
+ ```jsx
75
+ // Four props from four axes, all applied together.
76
+ <Button primary large loading block>Save</Button>
77
+ ```
78
+
79
+ String-valued enum props such as `variant="primary"`, `size="lg"`, or
80
+ `tone="destructive"` are not part of the API and are not accepted. The boolean
81
+ form is the only styling surface.
82
+
83
+ ## Theming
84
+
85
+ `ThemeProvider` reads the OS color scheme by default and exposes the resolved
86
+ tokens to every Canvas component through `useTheme`. Force a scheme when you
87
+ need to:
88
+
89
+ ```jsx
90
+ <ThemeProvider scheme="dark">
91
+ <App />
92
+ </ThemeProvider>
93
+ ```
94
+
95
+ ## What's Included
96
+
97
+ - A full component kit: buttons, inputs, cards, tables, tabs, dialogs,
98
+ dropdowns, calendars, charts, sidebars, and more, all exported from
99
+ `@olympusoss/canvas`.
100
+ - The style foundation: design tokens, the theme runtime (`ThemeProvider`,
101
+ `useTheme`), the `useResponsive` / `shadow` / `alpha` helpers, and the raw React
102
+ Native `View` / `Text` / `Pressable` / `Image` / `TextInput` / `ScrollView` primitives.
103
+ - Light and dark color schemes resolved through theme tokens.
104
+ - Desktop-first responsiveness built into every component.
105
+
106
+ ## License
107
+
108
+ MIT
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "@olympusoss/canvas",
3
- "version": "4.0.0",
3
+ "version": "5.0.0",
4
4
  "type": "module",
5
- "description": "CSS-first design system for the Olympus platform",
5
+ "description": "Universal React Native UI kit styled with Tailwind",
6
6
  "main": "./src/index.ts",
7
7
  "types": "./src/index.ts",
8
8
  "exports": {
@@ -29,15 +29,26 @@
29
29
  "screenshots": "bun scripts/capture-screenshots.ts",
30
30
  "changeset": "changeset",
31
31
  "version-packages": "changeset version",
32
- "release": "changeset publish"
32
+ "release": "changeset publish",
33
+ "docs:dev": "cd docs && npx vite",
34
+ "docs:build": "cd docs && npx vite build"
33
35
  },
34
36
  "publishConfig": {
35
37
  "registry": "https://registry.npmjs.org",
36
38
  "access": "public"
37
39
  },
40
+ "peerDependencies": {
41
+ "react": ">=18",
42
+ "react-native": ">=0.74",
43
+ "react-native-svg": ">=13"
44
+ },
38
45
  "devDependencies": {
39
46
  "@changesets/cli": "^2.31.0",
40
47
  "@playwright/test": "^1.60.0",
48
+ "@types/react": "^19.2.16",
49
+ "react": "^19.2.7",
50
+ "react-native": "^0.85.3",
51
+ "react-native-svg": "^15",
41
52
  "typescript": "^5.8.3"
42
53
  }
43
54
  }
@@ -0,0 +1,185 @@
1
+ # Avatars
2
+
3
+ A photo when the account has one, falling back to two initials on a brand gradient (seeded admin accounts). Sizes scale font proportionally (40% of diameter).
4
+
5
+ ## Usage
6
+
7
+ ```tsx
8
+ <Avatar name="AO" />
9
+ ```
10
+
11
+ ## Variants
12
+
13
+ ### Variant - stacked
14
+
15
+ ```tsx
16
+ <View style={{ flexDirection: "row", alignItems: "center" }}>
17
+ <Avatar ring src="/rachel-chen.jpg" name="RC" />
18
+ <Avatar ring src="/liang-bao.jpg" name="LB" style={{ marginLeft: -12 }} />
19
+ <Avatar ring src="/marcus-allen.jpg" name="LB" style={{ marginLeft: -12 }} />
20
+ <Avatar ring src="/kira-tanaka.jpg" name="KT" style={{ marginLeft: -12 }} />
21
+ </View>
22
+ ```
23
+
24
+ ### Variant - topbar
25
+
26
+ ```tsx
27
+ <Dropdown items={[
28
+ { label: "Your profile", icon: "👤" },
29
+ { label: "Settings", icon: "⚙" },
30
+ { label: "Sign out", icon: "↩", separatorBefore: true }
31
+ ]}>
32
+ <View style={{ flexDirection: "row", alignItems: "center", gap: 8, borderRadius: 9999, borderWidth: 1, borderColor: tokens.border, backgroundColor: tokens.card, paddingVertical: 4, paddingLeft: 4, paddingRight: 10 }}>
33
+ <Avatar small src="/marcus-allen.jpg" name="MA" />
34
+ <Text style={{ fontSize: 14, lineHeight: 20, fontWeight: "500", color: tokens.foreground }}>admin@example.com</Text>
35
+ <Icon chevronDown muted size={12} />
36
+ </View>
37
+ </Dropdown>
38
+ ```
39
+
40
+ ### Variant - identity
41
+
42
+ ```tsx
43
+ <View style={{ flexDirection: "row", alignItems: "center", gap: 16 }}>
44
+ <Avatar src="/rachel-chen.jpg" name="RC" />
45
+ <View>
46
+ <Text style={{ fontSize: 16, lineHeight: 24, fontWeight: "600", color: tokens.foreground }}>Rachel Chen</Text>
47
+ <Text style={{ fontSize: 14, lineHeight: 20, color: tokens["muted-foreground"] }}>rachel.chen@example.com</Text>
48
+ </View>
49
+ </View>
50
+ ```
51
+
52
+ ### Variant - menu
53
+
54
+ ```tsx
55
+ <View style={{ flexDirection: "row", alignItems: "center", gap: 12, borderBottomWidth: 1, borderColor: tokens.border, paddingBottom: 12 }}>
56
+ <Avatar src="/ada-lovelace.jpg" name="AL" />
57
+ <View>
58
+ <Text style={{ fontSize: 14, lineHeight: 20, fontWeight: "600", color: tokens.foreground }}>Ada Lovelace</Text>
59
+ <Text style={{ fontSize: 12, lineHeight: 16, color: tokens["muted-foreground"] }}>admin@example.com</Text>
60
+ </View>
61
+ </View>
62
+ ```
63
+
64
+ ### Ring outline
65
+
66
+ ```tsx
67
+ <Avatar ring name="AO" />
68
+ ```
69
+
70
+ ## Do & Don't
71
+
72
+ ### Single
73
+
74
+ **Do** — One or two initials, sized about 40% of the diameter.
75
+
76
+ ```tsx
77
+ <Avatar name="AO" />
78
+ ```
79
+
80
+ **Don't** — Cramming in a full set of initials shrinks the type and crowds the circle.
81
+
82
+ ```tsx
83
+ <View style={{ flexShrink: 0, alignItems: "center", justifyContent: "center", overflow: "hidden", backgroundColor: tokens.muted, width: 40, height: 40, borderRadius: 9999 }}>
84
+ <Text style={{ fontWeight: "500", color: tokens["muted-foreground"], fontSize: 12 }}>ABCD</Text>
85
+ </View>
86
+ ```
87
+
88
+ ### Stacked
89
+
90
+ **Do** — Cap the stack and summarize the rest with a +N count.
91
+
92
+ ```tsx
93
+ <View style={{ flexDirection: "row", alignItems: "center" }}>
94
+ <Avatar small ring name="AO" />
95
+ <Avatar small ring name="RC" style={{ marginLeft: -10 }} />
96
+ <Avatar small ring name="LB" style={{ marginLeft: -10 }} />
97
+ <Avatar small ring name="KT" style={{ marginLeft: -10 }} />
98
+ <Text style={{ marginLeft: 6, fontSize: 12, lineHeight: 16, color: tokens["muted-foreground"] }}>+12</Text>
99
+ </View>
100
+ ```
101
+
102
+ **Don't** — An unbounded stack runs off the row and stops being scannable.
103
+
104
+ ```tsx
105
+ <View style={{ flexDirection: "row", alignItems: "center" }}>
106
+ <Avatar small ring name="AO" />
107
+ <Avatar small ring name="RC" style={{ marginLeft: -10 }} />
108
+ <Avatar small ring name="LB" style={{ marginLeft: -10 }} />
109
+ <Avatar small ring name="KT" style={{ marginLeft: -10 }} />
110
+ <Avatar small ring name="JD" style={{ marginLeft: -10 }} />
111
+ <Avatar small ring name="MA" style={{ marginLeft: -10 }} />
112
+ <Avatar small ring name="AL" style={{ marginLeft: -10 }} />
113
+ <Avatar small ring name="SK" style={{ marginLeft: -10 }} />
114
+ </View>
115
+ ```
116
+
117
+ ### Topbar account menu
118
+
119
+ **Do** — Pair it with the account name and a chevron so it reads as a trigger.
120
+
121
+ ```tsx
122
+ <View style={{ flexDirection: "row", alignItems: "center", gap: 8, borderRadius: 9999, borderWidth: 1, borderColor: tokens.border, backgroundColor: tokens.card, paddingVertical: 4, paddingLeft: 4, paddingRight: 10 }}>
123
+ <Avatar small src="/marcus-allen.jpg" name="MA" />
124
+ <Text style={{ fontSize: 14, lineHeight: 20, fontWeight: "500", color: tokens.foreground }}>admin@example.com</Text>
125
+ <Icon chevronDown muted size={12} />
126
+ </View>
127
+ ```
128
+
129
+ **Don't** — A lone avatar gives no hint that it opens the account menu.
130
+
131
+ ```tsx
132
+ <Avatar small src="/marcus-allen.jpg" name="MA" />
133
+ ```
134
+
135
+ ### Identity
136
+
137
+ **Do** — Name primary; email muted and secondary.
138
+
139
+ ```tsx
140
+ <View style={{ flexDirection: "row", alignItems: "center", gap: 16 }}>
141
+ <Avatar src="/rachel-chen.jpg" name="RC" />
142
+ <View>
143
+ <Text style={{ fontSize: 16, lineHeight: 24, fontWeight: "600", color: tokens.foreground }}>Rachel Chen</Text>
144
+ <Text style={{ fontSize: 14, lineHeight: 20, color: tokens["muted-foreground"] }}>rachel.chen@example.com</Text>
145
+ </View>
146
+ </View>
147
+ ```
148
+
149
+ **Don't** — Equal weight on the name and email flattens the hierarchy.
150
+
151
+ ```tsx
152
+ <View style={{ flexDirection: "row", alignItems: "center", gap: 16 }}>
153
+ <Avatar src="/rachel-chen.jpg" name="RC" />
154
+ <View>
155
+ <Text style={{ fontSize: 14, lineHeight: 20, color: tokens.foreground }}>Rachel Chen</Text>
156
+ <Text style={{ fontSize: 14, lineHeight: 20, color: tokens.foreground }}>rachel.chen@example.com</Text>
157
+ </View>
158
+ </View>
159
+ ```
160
+
161
+ ### Menu header
162
+
163
+ **Do** — Keep one consistent circular avatar shape across contexts.
164
+
165
+ ```tsx
166
+ <View style={{ flexDirection: "row", alignItems: "center", gap: 12 }}>
167
+ <Avatar src="/ada-lovelace.jpg" name="AL" />
168
+ <View>
169
+ <Text style={{ fontSize: 14, lineHeight: 20, fontWeight: "600", color: tokens.foreground }}>Ada Lovelace</Text>
170
+ <Text style={{ fontSize: 12, lineHeight: 16, color: tokens["muted-foreground"] }}>admin@example.com</Text>
171
+ </View>
172
+ </View>
173
+ ```
174
+
175
+ **Don't** — Squaring the avatar here clashes with the circular avatars everywhere else.
176
+
177
+ ```tsx
178
+ <View style={{ flexDirection: "row", alignItems: "center", gap: 12 }}>
179
+ <Avatar rounded src="/ada-lovelace.jpg" name="AL" />
180
+ <View>
181
+ <Text style={{ fontSize: 14, lineHeight: 20, fontWeight: "600", color: tokens.foreground }}>Ada Lovelace</Text>
182
+ <Text style={{ fontSize: 12, lineHeight: 16, color: tokens["muted-foreground"] }}>admin@example.com</Text>
183
+ </View>
184
+ </View>
185
+ ```
@@ -0,0 +1,48 @@
1
+ import { type ViewStyle, type TextStyle, type ImageStyle } from "react-native";
2
+ import { type ColorTokens } from "../../style/index.js";
3
+
4
+ // Co-located Avatar styles. Diameter per size, corner radius per shape, and the
5
+ // muted fallback surface, all built from the active tokens.
6
+
7
+ export type Size = "small" | "default" | "large";
8
+ export type Shape = "circle" | "rounded";
9
+
10
+ // Diameter per size: small is the inline topbar/stack size, default the 40px row
11
+ // avatar, large the identity-header size.
12
+ const BOX: Record<Size, number> = { small: 28, default: 40, large: 48 };
13
+
14
+ // Circle by default; the rounded square uses the card/menu radius.
15
+ const RADIUS: Record<Shape, number> = { circle: 9999, rounded: 6 };
16
+
17
+ // Initials type per size, ~40% of the diameter.
18
+ const LABEL_SIZE: Record<Size, { fontSize: number; lineHeight: number }> = {
19
+ small: { fontSize: 12, lineHeight: 16 },
20
+ default: { fontSize: 16, lineHeight: 24 },
21
+ large: { fontSize: 18, lineHeight: 28 },
22
+ };
23
+
24
+ export function container(tokens: ColorTokens, size: Size, shape: Shape, ring: boolean): ViewStyle {
25
+ return {
26
+ flexShrink: 0,
27
+ alignItems: "center",
28
+ justifyContent: "center",
29
+ overflow: "hidden",
30
+ backgroundColor: tokens.muted,
31
+ width: BOX[size],
32
+ height: BOX[size],
33
+ borderRadius: RADIUS[shape],
34
+ // No ring-* equivalent in RN; a 2px background-colored border is the stand-in
35
+ // for ring-2 ring-background, the separator outline used when avatars overlap.
36
+ ...(ring ? { borderWidth: 2, borderColor: tokens.background } : null),
37
+ };
38
+ }
39
+
40
+ // The photo fills the container exactly; the parent's overflow-hidden clips it
41
+ // to the circle (RN clips children to a parent's borderRadius).
42
+ export function image(shape: Shape): ImageStyle {
43
+ return { width: "100%", height: "100%", borderRadius: RADIUS[shape] };
44
+ }
45
+
46
+ export function label(tokens: ColorTokens, size: Size): TextStyle {
47
+ return { fontWeight: "500", color: tokens["muted-foreground"], ...LABEL_SIZE[size] };
48
+ }
@@ -0,0 +1,99 @@
1
+ import { type ReactNode } from "react";
2
+ import { View, Image, Pressable, Text, useTheme, type StyleProp, type ViewStyle } from "../../style/index.js";
3
+ import * as s from "./avatar.styles.js";
4
+ import { type Size, type Shape } from "./avatar.styles.js";
5
+
6
+ // The avatar shows an account's photo when it has one, falling back to one or
7
+ // two initials on a muted surface. It is a circle by default (the consistent
8
+ // shape across topbars, identity rows, and menus), optionally a rounded square.
9
+ //
10
+ // Boolean-prop API: one boolean per option, grouped by axis, first-match
11
+ // precedence within an axis (mirrors Button's intentOf). Size picks the
12
+ // diameter and proportional type (~40% of the diameter); shape switches the
13
+ // corner radius; `ring` draws the separator outline used when avatars overlap
14
+ // in a stack.
15
+
16
+ export interface AvatarProps {
17
+ /** Photo URL. When set, the image fills the circle and the fallback is hidden. */
18
+ src?: string;
19
+ /** Alias for `src`, for callers that think in terms of a native image uri. */
20
+ uri?: string;
21
+ /** Initials fallback shown when there is no photo (e.g. "AO"). */
22
+ name?: string;
23
+ /** Same as `name`; the rendered initials when no photo is supplied. */
24
+ children?: ReactNode;
25
+ // Size (pick one; default is the 40px row avatar).
26
+ small?: boolean;
27
+ large?: boolean;
28
+ // Shape (pick one; default is a circle).
29
+ circle?: boolean;
30
+ rounded?: boolean;
31
+ /** Separator outline, for avatars that overlap in a stack. */
32
+ ring?: boolean;
33
+ /** When set, the avatar becomes pressable (e.g. a topbar account trigger). */
34
+ onPress?: () => void;
35
+ /** Escape hatch for layout/positioning composition (e.g. negative margin to overlap in a stack). */
36
+ style?: StyleProp<ViewStyle>;
37
+ }
38
+
39
+ // Size precedence when more than one is passed: first match wins.
40
+ function sizeOf(p: AvatarProps): Size {
41
+ if (p.small) return "small";
42
+ if (p.large) return "large";
43
+ return "default";
44
+ }
45
+
46
+ // Shape precedence when more than one is passed: first match wins.
47
+ function shapeOf(p: AvatarProps): Shape {
48
+ if (p.circle) return "circle";
49
+ if (p.rounded) return "rounded";
50
+ return "circle";
51
+ }
52
+
53
+ // Reduce a name or label to one or two initials ("Rachel Chen" -> "RC", "AO" ->
54
+ // "AO"), so callers can pass either a full name or ready-made initials.
55
+ function initialsFrom(text: string): string {
56
+ const parts = text.trim().split(/\s+/).filter(Boolean);
57
+ if (parts.length === 0) return "";
58
+ if (parts.length === 1) return parts[0].slice(0, 2).toUpperCase();
59
+ return (parts[0][0] + parts[parts.length - 1][0]).toUpperCase();
60
+ }
61
+
62
+ export function Avatar(props: AvatarProps) {
63
+ const { src, uri, name, children, ring, onPress, style } = props;
64
+ const { tokens } = useTheme();
65
+ const size = sizeOf(props);
66
+ const shape = shapeOf(props);
67
+ const photo = src ?? uri;
68
+
69
+ const container: StyleProp<ViewStyle> = [s.container(tokens, size, shape, !!ring), style];
70
+
71
+ let inner: ReactNode;
72
+ if (photo) {
73
+ inner = (
74
+ <Image
75
+ style={s.image(shape)}
76
+ source={{ uri: photo }}
77
+ accessibilityLabel={name}
78
+ resizeMode="cover"
79
+ />
80
+ );
81
+ } else {
82
+ const source = name ?? (typeof children === "string" ? children : "");
83
+ const initials = source ? initialsFrom(source) : "";
84
+ inner = initials ? <Text style={s.label(tokens, size)}>{initials}</Text> : null;
85
+ }
86
+
87
+ if (onPress) {
88
+ return (
89
+ <Pressable
90
+ style={({ pressed }) => [container, pressed ? { opacity: 0.9 } : null]}
91
+ onPress={onPress}
92
+ accessibilityRole="button"
93
+ >
94
+ {inner}
95
+ </Pressable>
96
+ );
97
+ }
98
+ return <View style={container}>{inner}</View>;
99
+ }