@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,167 @@
1
+ import { type ReactNode } from "react";
2
+ import { View, Pressable, Text, useTheme, type StyleProp, type ViewStyle } from "../../style/index.js";
3
+ import { Avatar } from "../../atoms/avatar/avatar.js";
4
+ import * as s from "./feeds.styles.js";
5
+
6
+ // An activity feed is a vertical timeline of events. Each item is a row with a
7
+ // leading mark (a small dot/initials node or a person's avatar), a content
8
+ // column carrying the actor + action + target line, and a muted timestamp.
9
+ //
10
+ // Two lead variants:
11
+ //
12
+ // 1. The connector feed (default): each row leads with a small bordered node
13
+ // and a vertical connector line links one event to the next. The connector
14
+ // is dropped on the final item so the line terminates cleanly at the last
15
+ // event rather than dangling past it.
16
+ // 2. The avatar feed (`avatar`): each row leads with the actor's avatar and the
17
+ // rows are separated by hairline rules instead of a connector line.
18
+ //
19
+ // Boolean-prop API: one boolean per option, grouped by axis, first-match
20
+ // precedence within an axis (mirrors Button's intentOf). The lead axis picks
21
+ // between the connector node and the avatar; `compact` is an orthogonal density
22
+ // modifier that tightens the vertical rhythm.
23
+
24
+ /** One event in the feed. */
25
+ export interface FeedItem {
26
+ /** Actor who performed the action, rendered bold (e.g. "Rachel Chen"). */
27
+ actor?: string;
28
+ /** The action text, muted (e.g. "approved the request"). */
29
+ action: string;
30
+ /** Optional target of the action, muted and trailing the action. */
31
+ target?: string;
32
+ /** Relative timestamp, muted and small (e.g. "2 hours ago"). */
33
+ time: string;
34
+ /** Photo URL for the avatar lead; falls back to initials from the actor. */
35
+ avatar?: string;
36
+ }
37
+
38
+ export interface FeedProps {
39
+ /** Events to render, top to bottom. */
40
+ items?: FeedItem[];
41
+ // Lead axis (pick one; default is the connector node + vertical line).
42
+ connector?: boolean;
43
+ avatar?: boolean;
44
+ // Density modifier: tightens the row padding and connector spacing.
45
+ compact?: boolean;
46
+ /** When set, each event row is pressable, reporting the row index. */
47
+ onItemPress?: (index: number) => void;
48
+ /** Escape hatch for layout/positioning composition (mainly width). */
49
+ style?: StyleProp<ViewStyle>;
50
+ }
51
+
52
+ type Lead = "connector" | "avatar";
53
+
54
+ // Lead precedence when more than one is passed: first match wins.
55
+ function leadOf(p: FeedProps): Lead {
56
+ if (p.connector) return "connector";
57
+ if (p.avatar) return "avatar";
58
+ return "connector";
59
+ }
60
+
61
+ // Two initials from an actor name, used for the avatar/node fallback when no
62
+ // photo is supplied (e.g. "Rachel Chen" -> "RC").
63
+ function initialsFrom(name: string): string {
64
+ const parts = name.trim().split(/\s+/).filter(Boolean);
65
+ if (parts.length === 0) return "";
66
+ if (parts.length === 1) return parts[0].slice(0, 2).toUpperCase();
67
+ return (parts[0][0] + parts[parts.length - 1][0]).toUpperCase();
68
+ }
69
+
70
+ export function Feed(props: FeedProps) {
71
+ const { items = [], onItemPress, style } = props;
72
+ const { tokens } = useTheme();
73
+ const lead = leadOf(props);
74
+ const compact = !!props.compact;
75
+ const lastIndex = items.length - 1;
76
+
77
+ const renderContent = (item: FeedItem) => (
78
+ <View style={s.contentColumn}>
79
+ <Text style={s.lineText}>
80
+ {item.actor ? <Text style={s.actorLabel(tokens)}>{item.actor} </Text> : null}
81
+ <Text style={s.actionLabel(tokens)}>{item.action}</Text>
82
+ {item.target ? <Text style={s.actionLabel(tokens)}> {item.target}</Text> : null}
83
+ </Text>
84
+ <Text style={s.timeLabel(tokens)}>{item.time}</Text>
85
+ </View>
86
+ );
87
+
88
+ if (lead === "avatar") {
89
+ // Avatar lead: each row leads with the actor's avatar; rows are ruled by a
90
+ // hairline between items (the last row keeps no rule).
91
+ const rows = items.map((item, index) => {
92
+ const divider = index < lastIndex ? s.avatarDivider(tokens) : null;
93
+ const rowStyle: StyleProp<ViewStyle> = [s.avatarRow, s.avatarRowPad(compact), divider];
94
+ const inner: ReactNode = (
95
+ <>
96
+ <Avatar src={item.avatar} name={item.actor}>
97
+ {item.actor ? initialsFrom(item.actor) : ""}
98
+ </Avatar>
99
+ {renderContent(item)}
100
+ </>
101
+ );
102
+ if (onItemPress) {
103
+ return (
104
+ <Pressable
105
+ key={index}
106
+ accessibilityRole="button"
107
+ onPress={() => onItemPress(index)}
108
+ style={({ pressed }) => [rowStyle, pressed ? { backgroundColor: tokens.accent } : null]}
109
+ >
110
+ {inner}
111
+ </Pressable>
112
+ );
113
+ }
114
+ return (
115
+ <View key={index} style={rowStyle}>
116
+ {inner}
117
+ </View>
118
+ );
119
+ });
120
+ return <View style={[s.cardSurface(tokens), style]}>{rows}</View>;
121
+ }
122
+
123
+ // Connector lead: a bordered node per row with a vertical line linking each
124
+ // event to the next. The line is dropped on the final item.
125
+ const rows = items.map((item, index) => {
126
+ const isLast = index === lastIndex;
127
+ const rowStyle: StyleProp<ViewStyle> = [s.connectorRow, isLast ? null : s.connectorRowGap(compact)];
128
+ const inner: ReactNode = (
129
+ <>
130
+ {!isLast ? (
131
+ // Vertical connector: a 1px border-colored line running from just below
132
+ // the node down to the next row. Absolutely placed under the node's
133
+ // horizontal center (node is 28px wide -> center at 14px, minus the
134
+ // 0.5px line half-width lands at 13px).
135
+ <View style={s.connectorLine(tokens)} />
136
+ ) : null}
137
+ <View style={s.node(tokens)}>
138
+ {item.actor ? (
139
+ <Text style={s.nodeInitials(tokens)}>{initialsFrom(item.actor)}</Text>
140
+ ) : (
141
+ <View style={s.nodeDot(tokens)} />
142
+ )}
143
+ </View>
144
+ <View style={s.connectorContentColumn}>{renderContent(item)}</View>
145
+ </>
146
+ );
147
+ if (onItemPress) {
148
+ return (
149
+ <Pressable
150
+ key={index}
151
+ accessibilityRole="button"
152
+ onPress={() => onItemPress(index)}
153
+ style={({ pressed }) => [rowStyle, pressed ? { backgroundColor: tokens.accent } : null]}
154
+ >
155
+ {inner}
156
+ </Pressable>
157
+ );
158
+ }
159
+ return (
160
+ <View key={index} style={rowStyle}>
161
+ {inner}
162
+ </View>
163
+ );
164
+ });
165
+
166
+ return <View style={[s.cardSurface(tokens), s.connectorPad(compact), style]}>{rows}</View>;
167
+ }
@@ -0,0 +1,117 @@
1
+ # Field Display
2
+
3
+ Read-only key/value pairs. Used in detail views, modal previews, and audit screens. Optional mono mode for IDs, tokens, dates.
4
+
5
+ ## Usage
6
+
7
+ ```tsx
8
+ <Field
9
+ rows={[
10
+ { label: "User ID", value: "usr_abc123", mono: true },
11
+ { label: "Name", value: "Rachel Chen" },
12
+ { label: "Role", value: "Admin" },
13
+ { label: "Status", status: "Active" }
14
+ ]}
15
+ style={{ maxWidth: 400 }}
16
+ />
17
+ ```
18
+
19
+ ## Variants
20
+
21
+ ### Value mode - mono
22
+
23
+ ```tsx
24
+ <Field
25
+ rows={[
26
+ { label: "Client ID", value: "clt_8f2a9b4c7e1d", mono: true },
27
+ { label: "Created", value: "2026-05-24T14:32:00Z", mono: true },
28
+ { label: "Fingerprint", value: "sha256:xK9v...", mono: true }
29
+ ]}
30
+ style={{ maxWidth: 400 }}
31
+ />
32
+ ```
33
+
34
+ ### Value mode - composed
35
+
36
+ ```tsx
37
+ <Field
38
+ rows={[
39
+ { label: "Status", status: "Active" },
40
+ { label: "Plan", badge: "Pro" },
41
+ { label: "Token", value: "sk_live_a8f2...c9e1", mono: true, copyValue: "sk_live_a8f2c9e1" },
42
+ { label: "Members", avatars: [
43
+ { src: "/rachel-chen.jpg", name: "RC" },
44
+ { name: "AJ" }
45
+ ], overflow: 3 }
46
+ ]}
47
+ style={{ maxWidth: 400 }}
48
+ />
49
+ ```
50
+
51
+ ## Do & Don't
52
+
53
+ ### Basic
54
+
55
+ **Do** — Use the fixed 180px label column so every value aligns to one baseline.
56
+
57
+ ```tsx
58
+ <Field style={{ maxWidth: 400 }} rows={[
59
+ { label: "Name", value: "Rachel Chen" },
60
+ { label: "Role", value: "Admin" }
61
+ ]} />
62
+ ```
63
+
64
+ **Don't** — Inline label-colon-value with no shared column makes values ragged and impossible to scan down a list.
65
+
66
+ ```tsx
67
+ <View style={{ maxWidth: 400, flexDirection: "column", gap: 4 }}>
68
+ <Text style={{ fontSize: 14, lineHeight: 20 }}>
69
+ <Text style={{ fontWeight: "600" }}>Name:</Text>
70
+ Rachel Chen
71
+ </Text>
72
+ <Text style={{ fontSize: 14, lineHeight: 20 }}>
73
+ <Text style={{ fontWeight: "600" }}>Role:</Text>
74
+ Admin
75
+ </Text>
76
+ </View>
77
+ ```
78
+
79
+ ### Mono
80
+
81
+ **Do** — Wrap IDs, hashes, and timestamps in font-mono so every glyph is fixed-width and copy-able.
82
+
83
+ ```tsx
84
+ <Field style={{ maxWidth: 400 }} rows={[
85
+ { label: "Client ID", value: "clt_8f2a9b4c7e1d", mono: true },
86
+ { label: "Fingerprint", value: "sha256:xK9v...", mono: true }
87
+ ]} />
88
+ ```
89
+
90
+ **Don't** — Rendering IDs and hashes in proportional type makes look-alike characters (l/1, O/0) hard to compare.
91
+
92
+ ```tsx
93
+ <Field style={{ maxWidth: 400 }} rows={[
94
+ { label: "Client ID", value: "clt_8f2a9b4c7e1d" },
95
+ { label: "Fingerprint", value: "sha256:xK9v..." }
96
+ ]} />
97
+ ```
98
+
99
+ ### Composed
100
+
101
+ **Do** — Compose real nodes into the value slot: a status badge for state, a badge for the plan tier.
102
+
103
+ ```tsx
104
+ <Field style={{ maxWidth: 400 }} rows={[
105
+ { label: "Status", status: "Active" },
106
+ { label: "Plan", badge: "Pro" }
107
+ ]} />
108
+ ```
109
+
110
+ **Don't** — Flattening a live status or plan tier to plain text drops the color and shape that signal state at a glance.
111
+
112
+ ```tsx
113
+ <Field style={{ maxWidth: 400 }} rows={[
114
+ { label: "Status", value: "Active" },
115
+ { label: "Plan", value: "Pro" }
116
+ ]} />
117
+ ```
@@ -0,0 +1,85 @@
1
+ import { type ViewStyle, type TextStyle } from "react-native";
2
+ import { type ColorTokens } from "../../style/index.js";
3
+
4
+ // Co-located Field styles. Layout-only fragments are static objects; anything
5
+ // that reads a color is a function of the active tokens, so the label/value text
6
+ // and the avatar-overflow chip follow light/dark (and the glass surface).
7
+
8
+ // --- shared text ------------------------------------------------------------
9
+
10
+ // The engine has no font-family utility, so request RN's cross-platform
11
+ // monospace alias via inline style (the same technique Badge uses for `mono`).
12
+ export const monoStyle: TextStyle = { fontFamily: "monospace" };
13
+
14
+ // Left-column label of a display row: a fixed 180px muted term.
15
+ export function fieldLabel(tokens: ColorTokens): TextStyle {
16
+ return { width: 180, flexShrink: 0, fontSize: 14, lineHeight: 20, color: tokens["muted-foreground"] };
17
+ }
18
+
19
+ // The default value shape: medium-weight foreground text.
20
+ export function fieldValue(tokens: ColorTokens): TextStyle {
21
+ return { fontSize: 14, lineHeight: 20, fontWeight: "500", color: tokens.foreground };
22
+ }
23
+
24
+ // --- display mode (rows) ----------------------------------------------------
25
+
26
+ // Outer stack of label/value rows.
27
+ export const displayStack: ViewStyle = { flexDirection: "column", gap: 12 };
28
+
29
+ // One row: label column + value, baseline-aligned.
30
+ export const displayRow: ViewStyle = { flexDirection: "row", alignItems: "center", gap: 16 };
31
+
32
+ // The value column grows to fill the row beside the fixed label column.
33
+ export const valueFill: ViewStyle = { flexGrow: 1, flexShrink: 1, flexBasis: "0%" };
34
+
35
+ // An overlapping avatar stack row.
36
+ export const avatarRow: ViewStyle = { flexDirection: "row", alignItems: "center" };
37
+
38
+ // Each avatar after the first overlaps the previous one.
39
+ export const avatarOverlap: ViewStyle = { marginLeft: -8 };
40
+
41
+ // The trailing "+N" overflow chip after an avatar stack.
42
+ export function overflowChip(tokens: ColorTokens): ViewStyle {
43
+ return {
44
+ marginLeft: -8,
45
+ height: 28,
46
+ width: 28,
47
+ alignItems: "center",
48
+ justifyContent: "center",
49
+ borderRadius: 9999,
50
+ borderWidth: 2,
51
+ borderColor: tokens.background,
52
+ backgroundColor: tokens.muted,
53
+ };
54
+ }
55
+
56
+ export function overflowText(tokens: ColorTokens): TextStyle {
57
+ return { fontSize: 12, lineHeight: 16, fontWeight: "500", color: tokens["muted-foreground"] };
58
+ }
59
+
60
+ // A copyable value: the value text followed by a ghost Copy button.
61
+ export const copyRow: ViewStyle = { flexDirection: "row", alignItems: "center", gap: 8 };
62
+
63
+ // --- control mode -----------------------------------------------------------
64
+
65
+ // Outer stack of label / control / message.
66
+ export const controlStack: ViewStyle = { flexDirection: "column", gap: 6 };
67
+
68
+ // The dimmed look applied to the whole field when disabled.
69
+ export const dimmed: ViewStyle = { opacity: 0.5 };
70
+
71
+ // The control's label above the Input.
72
+ export function label(tokens: ColorTokens): TextStyle {
73
+ return { fontSize: 14, lineHeight: 20, fontWeight: "500", color: tokens.foreground };
74
+ }
75
+
76
+ // The destructive "*" appended to a required field's label.
77
+ export function requiredMark(tokens: ColorTokens): TextStyle {
78
+ return { color: tokens.destructive };
79
+ }
80
+
81
+ // The helper / error line below the control. Error text is destructive, the
82
+ // resting helper is muted.
83
+ export function message(tokens: ColorTokens, error: boolean): TextStyle {
84
+ return { fontSize: 12, lineHeight: 16, color: error ? tokens.destructive : tokens["muted-foreground"] };
85
+ }
@@ -0,0 +1,175 @@
1
+ import { View, Text, useTheme, type StyleProp, type ViewStyle } from "../../style/index.js";
2
+ import { Avatar } from "../../atoms/avatar/avatar.js";
3
+ import { Badge } from "../../atoms/badge/badge.js";
4
+ import { Button } from "../../atoms/button/button.js";
5
+ import { Input } from "../../atoms/input/input.js";
6
+ import * as s from "./field.styles.js";
7
+
8
+ /** One avatar in a Members stack: a photo (`src`) or initials (`name`). */
9
+ export interface FieldAvatar {
10
+ src?: string;
11
+ name?: string;
12
+ }
13
+
14
+ /**
15
+ * One row of the read-only Field display: a label paired with a value. The
16
+ * value slot is data-driven and composes real atoms; pass at most one value
17
+ * shape per row, resolved in this precedence: avatars > copyValue > status >
18
+ * badge > plain value.
19
+ */
20
+ export interface FieldRow {
21
+ /** Left-column label (the muted term). */
22
+ label: string;
23
+ /** Plain text value (the default value shape). */
24
+ value?: string;
25
+ /** Render `value`/`copyValue` in a fixed-width monospace face (IDs, tokens, hashes). */
26
+ mono?: boolean;
27
+ /** Render the value as a metadata Badge (secondary tone) carrying this text, e.g. a plan tier. */
28
+ badge?: string;
29
+ /** Render the value as a success status Badge carrying this text, e.g. "Active". */
30
+ status?: string;
31
+ /** Append a ghost "Copy" button after the value that copies this string. */
32
+ copyValue?: string;
33
+ /** Render the value as an overlapping avatar stack. */
34
+ avatars?: FieldAvatar[];
35
+ /** Trailing "+N" overflow chip after an avatar stack. */
36
+ overflow?: number;
37
+ }
38
+
39
+ export interface FieldProps {
40
+ /**
41
+ * Read-only key/value rows. When set, Field renders the field-display
42
+ * (label column + composed value) instead of the editable input control.
43
+ */
44
+ rows?: FieldRow[];
45
+ /** Label shown above the control. */
46
+ label?: string;
47
+ /** Helper text shown below the control in the resting state. */
48
+ helper?: string;
49
+ /** Error message shown below the control when `error` is set; replaces the helper. */
50
+ error?: string;
51
+ /** Placeholder forwarded to the wrapped Input. */
52
+ placeholder?: string;
53
+ /** Current text value (controlled), forwarded to the Input. */
54
+ value?: string;
55
+ /** Called with the new text on each keystroke, forwarded to the Input. */
56
+ onChangeText?: (text: string) => void;
57
+ // Boolean axes (orthogonal, stack freely).
58
+ /** Marks the field as required: appends a destructive "*" to the label. */
59
+ required?: boolean;
60
+ /** Disables the control and dims the whole field. */
61
+ disabled?: boolean;
62
+ /** Invalid state: shows the error message (red) and flags the Input. */
63
+ invalid?: boolean;
64
+ /** Escape hatch for layout/positioning composition (mainly width). */
65
+ style?: StyleProp<ViewStyle>;
66
+ }
67
+
68
+ // Render a row's value slot from its data descriptor. Precedence: an avatar
69
+ // stack, then a copyable value, then a status badge, then a metadata badge,
70
+ // then plain (optionally monospace) text.
71
+ function FieldValue(row: FieldRow) {
72
+ const { tokens } = useTheme();
73
+
74
+ if (row.avatars && row.avatars.length > 0) {
75
+ return (
76
+ <View style={s.avatarRow}>
77
+ {row.avatars.map((a, i) => (
78
+ <View key={i} style={i > 0 ? s.avatarOverlap : undefined}>
79
+ <Avatar small ring src={a.src} name={a.name}>
80
+ {a.name}
81
+ </Avatar>
82
+ </View>
83
+ ))}
84
+ {typeof row.overflow === "number" && row.overflow > 0 ? (
85
+ <View style={s.overflowChip(tokens)}>
86
+ <Text style={s.overflowText(tokens)}>{`+${row.overflow}`}</Text>
87
+ </View>
88
+ ) : null}
89
+ </View>
90
+ );
91
+ }
92
+ if (row.copyValue != null) {
93
+ return (
94
+ <View style={s.copyRow}>
95
+ <Text style={[s.fieldValue(tokens), row.mono ? s.monoStyle : null]}>
96
+ {row.value ?? row.copyValue}
97
+ </Text>
98
+ <Button ghost small>
99
+ Copy
100
+ </Button>
101
+ </View>
102
+ );
103
+ }
104
+ if (row.status != null) {
105
+ return (
106
+ <Badge status success>
107
+ {row.status}
108
+ </Badge>
109
+ );
110
+ }
111
+ if (row.badge != null) {
112
+ return <Badge secondary>{row.badge}</Badge>;
113
+ }
114
+ return (
115
+ <Text style={[s.fieldValue(tokens), row.mono ? s.monoStyle : null]}>
116
+ {row.value}
117
+ </Text>
118
+ );
119
+ }
120
+
121
+ export function Field(props: FieldProps) {
122
+ const {
123
+ rows,
124
+ label,
125
+ helper,
126
+ error,
127
+ placeholder,
128
+ value,
129
+ onChangeText,
130
+ required,
131
+ disabled,
132
+ invalid,
133
+ style,
134
+ } = props;
135
+ const { tokens } = useTheme();
136
+
137
+ // Display mode: a read-only stack of label/value rows. Each row aligns its
138
+ // label to a fixed 180px column (flex, not grid) so every value lines up to
139
+ // one baseline.
140
+ if (rows) {
141
+ return (
142
+ <View style={[s.displayStack, disabled ? s.dimmed : null, style]}>
143
+ {rows.map((row, index) => (
144
+ <View key={`${row.label}-${index}`} style={s.displayRow}>
145
+ <Text style={s.fieldLabel(tokens)}>{row.label}</Text>
146
+ <View style={s.valueFill}>{FieldValue(row)}</View>
147
+ </View>
148
+ ))}
149
+ </View>
150
+ );
151
+ }
152
+
153
+ // Error takes precedence over the resting helper below the control.
154
+ const showError = !!invalid && !!error;
155
+ const messageText = showError ? error : helper;
156
+
157
+ return (
158
+ <View style={[s.controlStack, disabled ? s.dimmed : null, style]}>
159
+ {label != null ? (
160
+ <Text style={s.label(tokens)}>
161
+ {label}
162
+ {required ? <Text style={s.requiredMark(tokens)}> *</Text> : null}
163
+ </Text>
164
+ ) : null}
165
+ <Input
166
+ value={value}
167
+ onChangeText={onChangeText}
168
+ placeholder={placeholder}
169
+ disabled={disabled}
170
+ error={invalid}
171
+ />
172
+ {messageText != null ? <Text style={s.message(tokens, showError)}>{messageText}</Text> : null}
173
+ </View>
174
+ );
175
+ }
@@ -0,0 +1,141 @@
1
+ # Fieldsets
2
+
3
+ Group related form controls under a legend. Each field pairs a label, control, optional help text, and an inline error, so a set of inputs reads as one labeled unit.
4
+
5
+ ## Usage
6
+
7
+ ```tsx
8
+ <Fieldset
9
+ legend="Shipping details"
10
+ description="Where should we send your order?"
11
+ items={[
12
+ { label: "Full name", placeholder: "Ada Lovelace" },
13
+ { label: "Email", placeholder: "ada@example.com", value: "ada@", help: "We'll only use this for order updates." },
14
+ { label: "Country", placeholder: "United States" }
15
+ ]}
16
+ />
17
+ ```
18
+
19
+ ## Variants
20
+
21
+ ### Content - checkboxes
22
+
23
+ ```tsx
24
+ <Fieldset
25
+ legend="Email notifications"
26
+ description="Choose what we email you about."
27
+ checkboxes={[
28
+ { label: "Product updates", checked: true },
29
+ { label: "Security alerts", checked: true },
30
+ { label: "Weekly digest", checked: false }
31
+ ]}
32
+ />
33
+ ```
34
+
35
+ ### Validation error
36
+
37
+ ```tsx
38
+ <Fieldset
39
+ legend="Shipping details"
40
+ description="Where should we send your order?"
41
+ items={[
42
+ { label: "Full name", placeholder: "Ada Lovelace" },
43
+ { label: "Email", placeholder: "ada@example.com", value: "ada@", help: "We'll only use this for order updates.", error: "Enter a valid email address" },
44
+ { label: "Country", placeholder: "United States" }
45
+ ]}
46
+ />
47
+ ```
48
+
49
+ ### Disabled
50
+
51
+ ```tsx
52
+ <Fieldset
53
+ legend="Shipping details"
54
+ description="Where should we send your order?"
55
+ disabled
56
+ items={[
57
+ { label: "Full name", placeholder: "Ada Lovelace" },
58
+ { label: "Email", placeholder: "ada@example.com", value: "ada@", help: "We'll only use this for order updates." },
59
+ { label: "Country", placeholder: "United States" }
60
+ ]}
61
+ />
62
+ ```
63
+
64
+ ### Columns - 2
65
+
66
+ ```tsx
67
+ <Fieldset
68
+ legend="Shipping details"
69
+ description="Where should we send your order?"
70
+ twoColumn
71
+ items={[
72
+ { label: "Full name", placeholder: "Ada Lovelace" },
73
+ { label: "Email", placeholder: "ada@example.com", value: "ada@", help: "We'll only use this for order updates." },
74
+ { label: "Country", placeholder: "United States" }
75
+ ]}
76
+ />
77
+ ```
78
+
79
+ ## Do & Don't
80
+
81
+ ### Text fields
82
+
83
+ **Do** — Give every field a persistent label; use the placeholder only for an example value or format hint.
84
+
85
+ ```tsx
86
+ <Fieldset legend="Shipping details" items={[
87
+ { label: "Full name", placeholder: "Ada Lovelace" },
88
+ { label: "Email", placeholder: "ada@example.com", help: "We'll only use this for order updates." }
89
+ ]} />
90
+ ```
91
+
92
+ **Don't** — A placeholder is not a label: it vanishes on focus and is skipped by many screen readers, leaving the field unnamed.
93
+
94
+ ```tsx
95
+ <Fieldset legend="Shipping details" items={[
96
+ { label: "", placeholder: "Full name" },
97
+ { label: "", placeholder: "Email" }
98
+ ]} />
99
+ ```
100
+
101
+ ### Checkbox group
102
+
103
+ **Do** — A legend names the group so its checkboxes read as one labeled unit.
104
+
105
+ ```tsx
106
+ <Fieldset legend="Notify me by" checkboxes={[
107
+ { label: "Email" },
108
+ { label: "SMS" },
109
+ { label: "Push" }
110
+ ]} />
111
+ ```
112
+
113
+ **Don't** — Without a legend the relationship between the controls is implicit; screen readers announce them as unrelated.
114
+
115
+ ```tsx
116
+ <Fieldset checkboxes={[
117
+ { label: "Email" },
118
+ { label: "SMS" },
119
+ { label: "Push" }
120
+ ]} />
121
+ ```
122
+
123
+ ### Two-column
124
+
125
+ **Do** — Pair only naturally adjacent, short fields side by side; the grid stacks to one column on small screens.
126
+
127
+ ```tsx
128
+ <Fieldset legend="Card details" twoColumn items={[
129
+ { label: "Expiry", placeholder: "MM / YY" },
130
+ { label: "CVC", placeholder: "123" }
131
+ ]} />
132
+ ```
133
+
134
+ **Don't** — Splitting unrelated or full-width fields across two columns crams the form and breaks the reading order.
135
+
136
+ ```tsx
137
+ <Fieldset legend="Account" twoColumn items={[
138
+ { label: "Bio", value: "Engineering lead." },
139
+ { label: "Country", value: "United States" }
140
+ ]} />
141
+ ```