@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,128 @@
1
+ import { type ReactNode } from "react";
2
+ import { View, Image, Text, useTheme, type StyleProp, type ViewStyle } from "../../style/index.js";
3
+ import { Avatar } from "../../atoms/avatar/avatar.js";
4
+ import * as s from "./media-objects.styles.js";
5
+ import { type Align, type Direction } from "./media-objects.styles.js";
6
+
7
+ // A media object is a horizontal row: a leading media element (avatar, image, or
8
+ // icon glyph) sits beside a content column (a bold title, a muted description,
9
+ // and an optional longer supporting body), sometimes with a trailing action
10
+ // pinned to the right. It is the building block for list rows, notifications,
11
+ // and comment layouts.
12
+ //
13
+ // Boolean-prop API, grouped by axis with first-match precedence within an axis
14
+ // (mirrors Button's intentOf):
15
+ //
16
+ // - Alignment: `center` aligns the row's cross axis to the middle (the compact,
17
+ // single-line list/action row); the default top-aligns with items-start so the
18
+ // media anchors to the first line of a multi-line body (per the component's
19
+ // "Avatar" do/don't: items-start for multi-line, center for single-line rows).
20
+ // - Direction: `reversed` flips the media to the trailing edge; default leads
21
+ // with the media on the left.
22
+ // - Surface: `bordered` wraps the row in the card surface (border + padding) used
23
+ // when a media object stands alone as a card; omit for a bare row.
24
+ //
25
+ // State/layout booleans stack orthogonally: `truncate` clamps the title and
26
+ // description to one line each (the action pattern, so a long email never wraps
27
+ // and pushes the trailing action out of alignment).
28
+
29
+ export interface MediaObjectProps {
30
+ /** Primary line: the bold heading (e.g. a person's name). */
31
+ title?: string;
32
+ /** Secondary line: muted supporting text under the title (e.g. a role or email). */
33
+ description?: string;
34
+ /** Optional longer body paragraph rendered below the description. */
35
+ body?: ReactNode;
36
+ /** Trailing metadata text pinned to the right (e.g. "2h ago", "admin"). */
37
+ meta?: string;
38
+ /** Initials for the leading avatar (e.g. "RC"); rendered as <Avatar>{avatar}</Avatar>. */
39
+ avatar?: string;
40
+ /** Photo URL for the leading avatar; takes precedence over initials. */
41
+ src?: string;
42
+ /** A leading icon glyph rendered in a tinted square box (stands in for an SVG icon). */
43
+ icon?: ReactNode;
44
+ /** A trailing action node (e.g. a <Button>), pinned to the right edge. */
45
+ action?: ReactNode;
46
+ // Alignment (pick one; default top-aligns with items-start).
47
+ center?: boolean;
48
+ start?: boolean;
49
+ // Direction (pick one; default leads with the media on the left).
50
+ reversed?: boolean;
51
+ leading?: boolean;
52
+ // Surface.
53
+ bordered?: boolean;
54
+ // Layout.
55
+ truncate?: boolean;
56
+ /** Escape hatch for layout/positioning composition (width, margins). */
57
+ style?: StyleProp<ViewStyle>;
58
+ }
59
+
60
+ // Alignment precedence when more than one is passed: first match wins. Default is
61
+ // start (items-start) so the media anchors to the first line of a multi-line body.
62
+ function alignOf(p: MediaObjectProps): Align {
63
+ if (p.center) return "center";
64
+ if (p.start) return "start";
65
+ return "start";
66
+ }
67
+
68
+ // Direction precedence when more than one is passed: first match wins.
69
+ function directionOf(p: MediaObjectProps): Direction {
70
+ if (p.reversed) return "reversed";
71
+ if (p.leading) return "leading";
72
+ return "leading";
73
+ }
74
+
75
+ export function MediaObject(props: MediaObjectProps) {
76
+ const { title, description, body, meta, avatar, src, icon, action, truncate, style } = props;
77
+ const { tokens } = useTheme();
78
+ const align = alignOf(props);
79
+ const direction = directionOf(props);
80
+
81
+ const container: StyleProp<ViewStyle> = [
82
+ s.containerBase,
83
+ { flexDirection: s.DIRECTION_ROW[direction], alignItems: s.ALIGN_ITEMS[align] },
84
+ props.bordered ? s.borderedSurface(tokens) : null,
85
+ style,
86
+ ];
87
+
88
+ // Leading media: photo > initials avatar > icon box. Only one renders.
89
+ let media: ReactNode = null;
90
+ if (src) {
91
+ media = (
92
+ <View style={s.photoBox(tokens)}>
93
+ <Image style={s.photoImage} source={{ uri: src }} accessibilityLabel={title} resizeMode="cover" />
94
+ </View>
95
+ );
96
+ } else if (avatar) {
97
+ media = <Avatar name={avatar}>{avatar}</Avatar>;
98
+ } else if (icon != null) {
99
+ media = (
100
+ <View style={s.iconBox(tokens)}>
101
+ {typeof icon === "string" ? <Text style={s.iconGlyph(tokens)}>{icon}</Text> : icon}
102
+ </View>
103
+ );
104
+ }
105
+
106
+ // The engine has no truncate utility; RN clamps text via numberOfLines, which
107
+ // is the supported equivalent (single line with an ellipsis on overflow).
108
+ return (
109
+ <View style={container}>
110
+ {media}
111
+ <View style={s.content}>
112
+ {title != null ? (
113
+ <Text style={s.title(tokens)} numberOfLines={truncate ? 1 : undefined}>
114
+ {title}
115
+ </Text>
116
+ ) : null}
117
+ {description != null ? (
118
+ <Text style={s.description(tokens)} numberOfLines={truncate ? 1 : undefined}>
119
+ {description}
120
+ </Text>
121
+ ) : null}
122
+ {body != null ? <Text style={s.body(tokens)}>{body}</Text> : null}
123
+ </View>
124
+ {meta != null ? <Text style={s.meta(tokens)}>{meta}</Text> : null}
125
+ {action != null ? <View style={s.actionBox}>{action}</View> : null}
126
+ </View>
127
+ );
128
+ }
@@ -0,0 +1,116 @@
1
+ # Stacked Lists
2
+
3
+ Vertical lists with avatar, two-line items, and trailing metadata. Used for contacts, activity feeds, and data previews.
4
+
5
+ ## Usage
6
+
7
+ ```tsx
8
+ <StackedList
9
+ items={[
10
+ { name: "Rachel Chen", detail: "rachel.chen@example.com", meta: "admin" },
11
+ { name: "Ada Lovelace", detail: "ada@example.com", meta: "editor" },
12
+ { name: "Kevin Turner", detail: "kevin@example.com", meta: "viewer" }
13
+ ]}
14
+ />
15
+ ```
16
+
17
+ ## Variants
18
+
19
+ ### Variant - clickable
20
+
21
+ ```tsx
22
+ <StackedList
23
+ items={[
24
+ { name: "Rachel Chen", detail: "rachel.chen@example.com", meta: "2h ago" },
25
+ { name: "Ada Lovelace", detail: "ada@example.com", meta: "5h ago" },
26
+ { name: "Kevin Turner", detail: "kevin@example.com", meta: "1d ago" }
27
+ ]}
28
+ clickable
29
+ />
30
+ ```
31
+
32
+ ### Variant - card
33
+
34
+ ```tsx
35
+ <StackedList
36
+ items={[
37
+ { name: "Rachel Chen", detail: "Engineering Lead" },
38
+ { name: "Ada Lovelace", detail: "Staff Engineer" }
39
+ ]}
40
+ card
41
+ title="Team members"
42
+ addAction="Add"
43
+ rowMenu
44
+ />
45
+ ```
46
+
47
+ ## Do & Don't
48
+
49
+ ### Two-line with avatar
50
+
51
+ **Do** — Primary line bold; secondary line smaller and muted, truncated so long values never wrap.
52
+
53
+ ```tsx
54
+ <StackedList items={[
55
+ { name: "Rachel Chen", detail: "rachel.chen@example.com", initials: "RC" }
56
+ ]} />
57
+ ```
58
+
59
+ **Don't** — Equal weight on both lines flattens the hierarchy; the email competes with the name.
60
+
61
+ ```tsx
62
+ <View style={{ borderRadius: 8, borderWidth: 1, borderColor: tokens.border, backgroundColor: tokens.card, overflow: "hidden", ...shadow("sm"), width: "100%", maxWidth: 560 }}>
63
+ <View style={{ flexDirection: "row", alignItems: "center", gap: 12, paddingHorizontal: 20, paddingVertical: 12 }}>
64
+ <Avatar name="Rachel Chen">RC</Avatar>
65
+ <View style={{ minWidth: 0, flexGrow: 1, flexShrink: 1, flexBasis: "0%" }}>
66
+ <Text style={{ fontSize: 13.5, fontWeight: "600", color: tokens.foreground }}>Rachel Chen</Text>
67
+ <Text style={{ fontSize: 13.5, fontWeight: "600", color: tokens.foreground }}>rachel.chen@example.com</Text>
68
+ </View>
69
+ </View>
70
+ </View>
71
+ ```
72
+
73
+ ### Clickable
74
+
75
+ **Do** — Wrap the row in a link with a hover background and a trailing chevron to signal drilldown.
76
+
77
+ ```tsx
78
+ <StackedList clickable items={[
79
+ { name: "Ada Lovelace", detail: "ada@example.com", meta: "5h ago", initials: "AL" }
80
+ ]} />
81
+ ```
82
+
83
+ **Don't** — A drilldown row with no hover state and no chevron gives no hint it is interactive.
84
+
85
+ ```tsx
86
+ <StackedList items={[
87
+ { name: "Ada Lovelace", detail: "ada@example.com", meta: "5h ago", initials: "AL" }
88
+ ]} />
89
+ ```
90
+
91
+ ### Card surface group
92
+
93
+ **Do** — Separate the titled header with a rule and give each row a trailing action menu.
94
+
95
+ ```tsx
96
+ <StackedList card title="Team members" addAction="Add" rowMenu items={[
97
+ { name: "Rachel Chen", detail: "Engineering Lead", initials: "RC" }
98
+ ]} />
99
+ ```
100
+
101
+ **Don't** — A header with no rule blends into the rows, and dropping the per-row action removes the affordance.
102
+
103
+ ```tsx
104
+ <View style={{ borderRadius: 8, borderWidth: 1, borderColor: tokens.border, backgroundColor: tokens.card, overflow: "hidden", ...shadow("sm"), width: "100%", maxWidth: 560 }}>
105
+ <View style={{ flexDirection: "row", alignItems: "center", justifyContent: "space-between", paddingHorizontal: 20, paddingVertical: 12 }}>
106
+ <Text style={{ fontSize: 14, lineHeight: 20, fontWeight: "600", color: tokens.foreground }}>Team members</Text>
107
+ </View>
108
+ <View style={{ flexDirection: "row", alignItems: "center", gap: 12, paddingHorizontal: 20, paddingVertical: 12 }}>
109
+ <Avatar name="Rachel Chen">RC</Avatar>
110
+ <View style={{ minWidth: 0, flexGrow: 1, flexShrink: 1, flexBasis: "0%" }}>
111
+ <Text style={{ fontSize: 13.5, fontWeight: "600", color: tokens.foreground }}>Rachel Chen</Text>
112
+ <Text style={{ fontSize: 12, lineHeight: 16, color: tokens["muted-foreground"] }}>Engineering Lead</Text>
113
+ </View>
114
+ </View>
115
+ </View>
116
+ ```
@@ -0,0 +1,111 @@
1
+ import { type ViewStyle, type TextStyle } from "react-native";
2
+ import { type ColorTokens, shadow } from "../../style/index.js";
3
+
4
+ // Co-located StackedList styles. Layout-only fragments are static objects;
5
+ // anything that reads a color is a function of the active tokens (so the surface,
6
+ // dividers, and labels follow light/dark and read as glass when the
7
+ // ThemeProvider's surface is "glass", since tokens.card goes translucent there).
8
+
9
+ // --- outer frame ------------------------------------------------------------
10
+
11
+ // w-full max-w-[560px]: full width, capped at 560px.
12
+ export const outer: ViewStyle = { width: "100%", maxWidth: 560 };
13
+
14
+ // The card surface used by the `card` variant and as the optional frame when a
15
+ // title is supplied: rounded-lg border bg-card overflow-hidden shadow-sm.
16
+ export function cardSurface(tokens: ColorTokens): ViewStyle {
17
+ return {
18
+ borderRadius: 8,
19
+ borderWidth: 1,
20
+ borderColor: tokens.border,
21
+ backgroundColor: tokens.card,
22
+ overflow: "hidden",
23
+ ...shadow("sm"),
24
+ };
25
+ }
26
+
27
+ // --- rows -------------------------------------------------------------------
28
+
29
+ // flex-row items-center gap-3 px-5 py-3.
30
+ export const rowBase: ViewStyle = {
31
+ flexDirection: "row",
32
+ alignItems: "center",
33
+ gap: 12,
34
+ paddingHorizontal: 20,
35
+ paddingVertical: 12,
36
+ };
37
+
38
+ // border-b border-border: the hairline between ruled rows.
39
+ export function rowDivider(tokens: ColorTokens): ViewStyle {
40
+ return { borderBottomWidth: 1, borderColor: tokens.border };
41
+ }
42
+
43
+ // active:bg-accent press surface (clickable rows and the overflow menu button).
44
+ export function pressedSurface(tokens: ColorTokens): ViewStyle {
45
+ return { backgroundColor: tokens.accent };
46
+ }
47
+
48
+ // flex-1: the primary + secondary text column.
49
+ export const column: ViewStyle = { flexGrow: 1, flexShrink: 1, flexBasis: "0%" };
50
+
51
+ // --- text -------------------------------------------------------------------
52
+
53
+ // text-sm font-medium text-foreground: the primary (name) line.
54
+ export function nameLabel(tokens: ColorTokens): TextStyle {
55
+ return { fontSize: 14, lineHeight: 20, fontWeight: "500", color: tokens.foreground };
56
+ }
57
+
58
+ // text-xs text-muted-foreground: the secondary (detail) line, also the trailing
59
+ // meta text and the trailing chevron.
60
+ export function mutedLabel(tokens: ColorTokens): TextStyle {
61
+ return { fontSize: 12, lineHeight: 16, color: tokens["muted-foreground"] };
62
+ }
63
+
64
+ // --- header -----------------------------------------------------------------
65
+
66
+ // flex-row items-center justify-between border-b border-border px-5 py-3.
67
+ export function header(tokens: ColorTokens): ViewStyle {
68
+ return {
69
+ flexDirection: "row",
70
+ alignItems: "center",
71
+ justifyContent: "space-between",
72
+ borderBottomWidth: 1,
73
+ borderColor: tokens.border,
74
+ paddingHorizontal: 20,
75
+ paddingVertical: 12,
76
+ };
77
+ }
78
+
79
+ // text-sm font-semibold text-foreground: the header title.
80
+ export function headerTitle(tokens: ColorTokens): TextStyle {
81
+ return { fontSize: 14, lineHeight: 20, fontWeight: "600", color: tokens.foreground };
82
+ }
83
+
84
+ // --- add-action button ------------------------------------------------------
85
+
86
+ // flex-row items-center gap-1.5: the leading-plus + label row inside addAction.
87
+ export const addActionRow: ViewStyle = { flexDirection: "row", alignItems: "center", gap: 6 };
88
+
89
+ // text-xs font-medium text-foreground: the addAction button label.
90
+ export function addActionLabel(tokens: ColorTokens): TextStyle {
91
+ return { fontSize: 12, lineHeight: 16, fontWeight: "500", color: tokens.foreground };
92
+ }
93
+
94
+ // --- overflow ("...") menu --------------------------------------------------
95
+
96
+ // h-7 w-7 flex-row items-center justify-center gap-1 rounded-md bg-transparent.
97
+ export const menuButton: ViewStyle = {
98
+ height: 28,
99
+ width: 28,
100
+ flexDirection: "row",
101
+ alignItems: "center",
102
+ justifyContent: "center",
103
+ gap: 4,
104
+ borderRadius: 6,
105
+ backgroundColor: "transparent",
106
+ };
107
+
108
+ // h-1 w-1 rounded-full bg-foreground: one of the three overflow dots.
109
+ export function menuDot(tokens: ColorTokens): ViewStyle {
110
+ return { height: 4, width: 4, borderRadius: 9999, backgroundColor: tokens.foreground };
111
+ }
@@ -0,0 +1,195 @@
1
+ import { type ReactNode } from "react";
2
+ import { type GestureResponderEvent } from "react-native";
3
+ import { View, Pressable, Text, useTheme, type StyleProp, type ViewStyle } from "../../style/index.js";
4
+ import { Avatar } from "../../atoms/avatar/avatar.js";
5
+ import { Badge } from "../../atoms/badge/badge.js";
6
+ import { Button } from "../../atoms/button/button.js";
7
+ import { Icon } from "../../atoms/icon/icon.js";
8
+ import * as s from "./stacked-lists.styles.js";
9
+
10
+ // A stacked list is a vertical list of rows separated by hairlines, each row a
11
+ // leading avatar, a primary + secondary text column, and trailing meta/badge/
12
+ // action. It is the building block for contact lists, activity feeds, and data
13
+ // previews.
14
+ //
15
+ // Boolean-prop API: one boolean per option, grouped by axis, first-match
16
+ // precedence within an axis (mirrors Button's intentOf).
17
+ //
18
+ // - Variant axis (pick one; default is the plain two-line list): `clickable`
19
+ // makes each row a pressable drilldown with a hover/press surface and a
20
+ // trailing chevron; `card` wraps the list in a titled card surface with an
21
+ // optional header. `clickable` wins over `card` when both are passed.
22
+ // - Divider (orthogonal boolean, on by default): draws a hairline between rows.
23
+ // Pass `flush` to drop the dividers. The card variant always keeps its rows
24
+ // ruled regardless of `flush`, matching the documented card surface group.
25
+
26
+ /** One row in the list. */
27
+ export interface StackedListItem {
28
+ /** Primary line, bold (e.g. a person's name). */
29
+ name: string;
30
+ /** Secondary line, smaller and muted (e.g. an email or role). */
31
+ detail: string;
32
+ /** Trailing metadata text (e.g. "2h ago"). Ignored when `badge` is set. */
33
+ meta?: string;
34
+ /** Trailing badge label; rendered as a <Badge> instead of plain meta text. */
35
+ badge?: string;
36
+ /** Photo URL for the avatar; falls back to initials when absent. */
37
+ avatar?: string;
38
+ /** Initials shown when there is no photo; derived from `name` when omitted. */
39
+ initials?: string;
40
+ }
41
+
42
+ export interface StackedListProps {
43
+ /** Rows to render. */
44
+ items?: StackedListItem[];
45
+ /** Optional header title; shown above the rows, separated by a rule. */
46
+ title?: ReactNode;
47
+ /** Trailing header content (e.g. an action button); only shown with a title.
48
+ * Takes precedence over `addAction` when both are supplied. */
49
+ action?: ReactNode;
50
+ /** Convenience header action: renders a small outlined button with a leading
51
+ * plus icon and this label (e.g. "Add"). Only shown with a title and when
52
+ * `action` is not set. Serializable, so the playground can drive it. */
53
+ addAction?: string;
54
+ /** Appends a trailing ghost overflow ("...") action-menu button to every row.
55
+ * Press is reported through `onPressItemMenu`. */
56
+ rowMenu?: boolean;
57
+ /** Press handler for a row, by index. Only used in the `clickable` variant. */
58
+ onPressItem?: (index: number, event: GestureResponderEvent) => void;
59
+ /** Press handler for a row's overflow menu, by index. Used with `rowMenu`. */
60
+ onPressItemMenu?: (index: number, event: GestureResponderEvent) => void;
61
+ // Variant (pick one; default is the plain two-line list).
62
+ clickable?: boolean;
63
+ card?: boolean;
64
+ // Divider modifier: rows are ruled by default; `flush` removes the hairlines.
65
+ flush?: boolean;
66
+ /** Escape hatch for layout/positioning composition (mainly width). */
67
+ style?: StyleProp<ViewStyle>;
68
+ }
69
+
70
+ type Variant = "two-line" | "clickable" | "card";
71
+
72
+ // Variant precedence when more than one is passed: first match wins.
73
+ function variantOf(p: StackedListProps): Variant {
74
+ if (p.clickable) return "clickable";
75
+ if (p.card) return "card";
76
+ return "two-line";
77
+ }
78
+
79
+ // Two initials from a name, used when an item supplies no explicit initials and
80
+ // no photo (e.g. "Rachel Chen" -> "RC").
81
+ function initialsFrom(name: string): string {
82
+ const parts = name.trim().split(/\s+/).filter(Boolean);
83
+ if (parts.length === 0) return "";
84
+ if (parts.length === 1) return parts[0].slice(0, 2).toUpperCase();
85
+ return (parts[0][0] + parts[parts.length - 1][0]).toUpperCase();
86
+ }
87
+
88
+ export function StackedList(props: StackedListProps) {
89
+ const { items = [], title, action, addAction, rowMenu, onPressItem, onPressItemMenu, flush, style } = props;
90
+ const variant = variantOf(props);
91
+ const { tokens } = useTheme();
92
+
93
+ // The header action: an explicit ReactNode wins; otherwise a small outlined
94
+ // button with a leading plus icon when `addAction` supplies a label.
95
+ const headerAction =
96
+ action != null ? (
97
+ action
98
+ ) : addAction != null ? (
99
+ <Button outline small>
100
+ <View style={s.addActionRow}>
101
+ <Icon plus size={13} />
102
+ <Text style={s.addActionLabel(tokens)}>{addAction}</Text>
103
+ </View>
104
+ </Button>
105
+ ) : null;
106
+
107
+ // The card variant always rules its rows; the others rule unless `flush`.
108
+ const ruled = variant === "card" ? true : !flush;
109
+ const framed = variant === "card" || title != null;
110
+
111
+ const lastIndex = items.length - 1;
112
+
113
+ const renderColumn = (item: StackedListItem) => (
114
+ <View style={s.column}>
115
+ <Text style={s.nameLabel(tokens)} numberOfLines={1}>
116
+ {item.name}
117
+ </Text>
118
+ <Text style={s.mutedLabel(tokens)} numberOfLines={1}>
119
+ {item.detail}
120
+ </Text>
121
+ </View>
122
+ );
123
+
124
+ const renderTrailing = (item: StackedListItem) => {
125
+ if (item.badge != null) return <Badge secondary>{item.badge}</Badge>;
126
+ if (item.meta != null) return <Text style={s.mutedLabel(tokens)}>{item.meta}</Text>;
127
+ return null;
128
+ };
129
+
130
+ // A ghost overflow ("...") action-menu button drawn as three horizontal dots;
131
+ // the Icon set has no more-horizontal glyph, so the dots are primitives.
132
+ const renderMenu = (index: number) => (
133
+ <Pressable
134
+ style={({ pressed }) => [s.menuButton, pressed ? s.pressedSurface(tokens) : null]}
135
+ onPress={(event) => onPressItemMenu?.(index, event)}
136
+ accessibilityRole="button"
137
+ accessibilityLabel="Actions"
138
+ >
139
+ <View style={s.menuDot(tokens)} />
140
+ <View style={s.menuDot(tokens)} />
141
+ <View style={s.menuDot(tokens)} />
142
+ </Pressable>
143
+ );
144
+
145
+ const renderAvatar = (item: StackedListItem) => (
146
+ <Avatar src={item.avatar} name={item.name}>
147
+ {item.initials ?? initialsFrom(item.name)}
148
+ </Avatar>
149
+ );
150
+
151
+ const rows = items.map((item, index) => {
152
+ const divider = ruled && index < lastIndex ? s.rowDivider(tokens) : null;
153
+
154
+ if (variant === "clickable") {
155
+ return (
156
+ <Pressable
157
+ key={index}
158
+ style={({ pressed }) => [s.rowBase, pressed ? s.pressedSurface(tokens) : null, divider]}
159
+ onPress={(event) => onPressItem?.(index, event)}
160
+ accessibilityRole="button"
161
+ >
162
+ {renderAvatar(item)}
163
+ {renderColumn(item)}
164
+ {renderTrailing(item)}
165
+ {rowMenu ? renderMenu(index) : null}
166
+ <Text style={s.mutedLabel(tokens)}>{"›"}</Text>
167
+ </Pressable>
168
+ );
169
+ }
170
+
171
+ return (
172
+ <View key={index} style={[s.rowBase, divider]}>
173
+ {renderAvatar(item)}
174
+ {renderColumn(item)}
175
+ {renderTrailing(item)}
176
+ {rowMenu ? renderMenu(index) : null}
177
+ </View>
178
+ );
179
+ });
180
+
181
+ const header =
182
+ title != null ? (
183
+ <View style={s.header(tokens)}>
184
+ <Text style={s.headerTitle(tokens)}>{title}</Text>
185
+ {headerAction != null ? <View>{headerAction}</View> : null}
186
+ </View>
187
+ ) : null;
188
+
189
+ return (
190
+ <View style={[s.outer, framed ? s.cardSurface(tokens) : null, style]}>
191
+ {header}
192
+ {rows}
193
+ </View>
194
+ );
195
+ }