@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,116 @@
1
+ import { type TextStyle } from "react-native";
2
+ import { type ColorTokens, alpha } from "../../style/index.js";
3
+
4
+ // Co-located Textarea skins, one per platform. The field is a multiline
5
+ // TextInput, so every fragment is a TextStyle. The BRAND survives on every
6
+ // platform (the focus/active cue is always the indigo `ring`/`primary` token,
7
+ // the error cue the `destructive` token, never a platform default); only the
8
+ // native SHAPE, fill, border/underline, and focus feedback change per OS:
9
+ // iOS (HIG, iOS 27 / Liquid Glass): the PLAIN multiline text view, a fully
10
+ // transparent field with NO fill and NO box, carrying only a subtle bottom
11
+ // hairline rule (the `input` token at rest), so it reads like the iOS 27
12
+ // plain text field rather than a filled gray capsule. The hairline brightens
13
+ // to the brand `primary` on focus and `destructive` on error; the brand
14
+ // cursor/selection is the indigo `primary` (set on the shell, never a system
15
+ // blue).
16
+ // Android (Material 3 filled): a subtle fill with a flat bottom active
17
+ // indicator (underline). Top corners ~4, square bottom. The indicator is a
18
+ // 1px resting line that thickens to 2px indigo on focus (destructive on
19
+ // error).
20
+ // Web: the established Canvas look (lifted verbatim) — full-width, 6 radius,
21
+ // 1px border, on the background fill; border is error > focus(ring) > input.
22
+
23
+ export type Size = "small" | "base" | "large";
24
+
25
+ // State the shell resolves and hands to the skin's field builder.
26
+ export interface TextareaFieldState {
27
+ /** Error/validation state (error or invalid prop). */
28
+ error: boolean;
29
+ /** The field currently holds keyboard focus. */
30
+ focused: boolean;
31
+ }
32
+
33
+ // The only thing a platform skin owns: the field surface for a given state.
34
+ // `sizeText` and `minHeight` are shared (the brand type scale and the rows math
35
+ // are identical across platforms), so the shell composes them around the skin.
36
+ export interface TextareaSkin {
37
+ field: (tokens: ColorTokens, state: TextareaFieldState) => TextStyle;
38
+ }
39
+
40
+ // --- shared, platform-neutral fragments -------------------------------------
41
+
42
+ // Text scale per size; mirrors the height the larger control reads as. Default
43
+ // is the base text-sm field (no size prop). Shared across platforms.
44
+ export function sizeText(size: Size): TextStyle {
45
+ if (size === "large") return { fontSize: 16, lineHeight: 24 }; // text-base
46
+ if (size === "small") return { fontSize: 12, lineHeight: 16 }; // text-xs
47
+ return { fontSize: 14, lineHeight: 20 }; // text-sm
48
+ }
49
+
50
+ // Derived min height from the row count: each row ~22px plus the vertical
51
+ // padding. Falls back to the 80px floor when no rows are given. The field still
52
+ // grows with content past this floor. Shared across platforms.
53
+ export function minHeight(rows?: number): TextStyle {
54
+ return { minHeight: rows == null ? 80 : rows * 22 + 16 };
55
+ }
56
+
57
+ // ---------- Web: the established Canvas look (lifted verbatim) ----------
58
+ // Full width, bordered, padded, on the background fill, with the foreground
59
+ // text color. Border resolves error > focus(ring) > default input.
60
+ export const webSkin: TextareaSkin = {
61
+ field: (t, st) => ({
62
+ width: "100%",
63
+ borderRadius: 6,
64
+ borderWidth: 1,
65
+ borderColor: st.error ? t.destructive : st.focused ? t.ring : t.input,
66
+ backgroundColor: t.background,
67
+ paddingHorizontal: 12,
68
+ paddingVertical: 8,
69
+ color: t.foreground,
70
+ }),
71
+ };
72
+
73
+ // ---------- iOS (HIG, iOS 27 / Liquid Glass): plain transparent field, bottom hairline ----------
74
+ // The iOS 27 plain multiline text view: no fill, no box, a transparent field
75
+ // with a single subtle bottom hairline rule. The rule is the `input` token at
76
+ // rest (1pt), brightening to the brand `primary` on focus and `destructive` on
77
+ // error, mirroring the new plain single-line input. Horizontal padding drops to
78
+ // 0 so the text and the rule align to the field's edges like the reference.
79
+ export const iosSkin: TextareaSkin = {
80
+ field: (t, st) => ({
81
+ width: "100%",
82
+ backgroundColor: "transparent",
83
+ // The only chrome: a bottom hairline that carries focus/error.
84
+ borderBottomWidth: 1,
85
+ borderBottomColor: st.error ? t.destructive : st.focused ? t.primary : t.input,
86
+ paddingHorizontal: 0,
87
+ paddingTop: 8,
88
+ paddingBottom: 8,
89
+ color: t.foreground,
90
+ }),
91
+ };
92
+
93
+ // ---------- Android (Material 3 filled): subtle fill + active indicator ------
94
+ // A subtle fill (~8% of the muted-foreground hue over the surface) with rounded
95
+ // top corners (~4) and a square bottom, carrying a bottom active indicator
96
+ // (underline). The indicator is a 1px resting line (the input token) that
97
+ // thickens to 2px indigo `primary` on focus, or destructive on error.
98
+ export const androidSkin: TextareaSkin = {
99
+ field: (t, st) => ({
100
+ width: "100%",
101
+ borderTopLeftRadius: 4,
102
+ borderTopRightRadius: 4,
103
+ borderBottomLeftRadius: 0,
104
+ borderBottomRightRadius: 0,
105
+ backgroundColor: alpha(t["muted-foreground"], 0.08),
106
+ // The active indicator: only the bottom edge is drawn.
107
+ borderBottomWidth: st.focused || st.error ? 2 : 1,
108
+ // Rest baseline must read clearly (on-surface-variant ~ muted-foreground) so the
109
+ // M3 filled field is distinct from the iOS lineless capsule.
110
+ borderBottomColor: st.error ? t.destructive : st.focused ? t.primary : t["muted-foreground"],
111
+ paddingHorizontal: 12,
112
+ paddingTop: 10,
113
+ paddingBottom: 8,
114
+ color: t.foreground,
115
+ }),
116
+ };
@@ -0,0 +1,6 @@
1
+ import { createTextarea } from "./textarea.shared.js";
2
+ import { webSkin } from "./textarea.styles.js";
3
+
4
+ // Web Textarea (the base; Metro falls back to it on native, web bundlers resolve it).
5
+ export const Textarea = createTextarea(webSkin);
6
+ export type { TextareaProps } from "./textarea.shared.js";
@@ -0,0 +1,6 @@
1
+ import { createTooltip } from "./tooltip.shared.js";
2
+ import { androidSkin } from "./tooltip.styles.js";
3
+
4
+ // Material 3 (plain tooltip) Tooltip. Metro resolves this file on Android; the docs import it for preview.
5
+ export const Tooltip = createTooltip(androidSkin);
6
+ export type { TooltipProps } from "./tooltip.shared.js";
@@ -0,0 +1,7 @@
1
+ import { createTooltip } from "./tooltip.shared.js";
2
+ import { iosSkin } from "./tooltip.styles.js";
3
+
4
+ // iOS Tooltip (no native tooltip; a small rounded inverse label). Metro resolves
5
+ // this file on iOS; the docs import it for preview.
6
+ export const Tooltip = createTooltip(iosSkin);
7
+ export type { TooltipProps } from "./tooltip.shared.js";
@@ -0,0 +1,122 @@
1
+ # Tooltips
2
+
3
+ Small floating helper text on hover or focus.
4
+
5
+ ## Usage
6
+
7
+ ```tsx
8
+ <Tooltip label="Open settings" iconTrigger trigger="Hover me" open top />
9
+ ```
10
+
11
+ ## Variants
12
+
13
+ ### Side - right
14
+
15
+ ```tsx
16
+ <Tooltip label="Open settings" iconTrigger trigger="Hover me" open right />
17
+ ```
18
+
19
+ ### Side - bottom
20
+
21
+ ```tsx
22
+ <Tooltip label="Open settings" iconTrigger trigger="Hover me" open bottom />
23
+ ```
24
+
25
+ ### Side - left
26
+
27
+ ```tsx
28
+ <Tooltip label="Open settings" iconTrigger trigger="Hover me" open left />
29
+ ```
30
+
31
+ ### Trigger - button
32
+
33
+ ```tsx
34
+ <Tooltip label="Open settings" trigger="Hover me" open top />
35
+ ```
36
+
37
+ ### Trigger - text
38
+
39
+ ```tsx
40
+ <Tooltip label="Open settings" trigger="hover this text" open top />
41
+ ```
42
+
43
+ ### Reveal - on hover
44
+
45
+ ```tsx
46
+ <Tooltip label="Open settings" iconTrigger trigger="Hover me" top />
47
+ ```
48
+
49
+ ## Do & Don't
50
+
51
+ **Do** — Keep tooltips short and supplementary; put essential steps in visible copy.
52
+
53
+ ```tsx
54
+ <Tooltip iconTrigger bottom open label="Rotate key" />
55
+ ```
56
+
57
+ **Don't** — Long, essential instructions hidden in a tooltip are missed on touch and by screen readers.
58
+
59
+ ```tsx
60
+ <Tooltip iconTrigger bottom open label="To rotate this key you must first revoke the old one in Settings, then confirm via email within 24 hours." />
61
+ ```
62
+
63
+ ### top
64
+
65
+ **Do** — Leave headroom above (or flip to bottom) so a top-placed tooltip stays fully on screen.
66
+
67
+ ```tsx
68
+ <Tooltip trigger="Save" top open label="Saves your changes" />
69
+ ```
70
+
71
+ **Don't** — A top tooltip on a trigger near the top edge clips above the viewport and goes unread.
72
+
73
+ ```tsx
74
+ <Tooltip trigger="Save" top open label="Saves your changes" />
75
+ ```
76
+
77
+ ### right
78
+
79
+ **Do** — Keep room to the right, or flip the tooltip to the left when the trigger hugs the edge.
80
+
81
+ ```tsx
82
+ <Tooltip iconTrigger right open label="More info" />
83
+ ```
84
+
85
+ **Don't** — A right tooltip on a control flush against the right edge is cut off by the container.
86
+
87
+ ```tsx
88
+ <View style={{ alignItems: "flex-end" }}>
89
+ <Tooltip iconTrigger right open label="More info" />
90
+ </View>
91
+ ```
92
+
93
+ ### bottom
94
+
95
+ **Do** — Give a bottom tooltip clearance so it never overlaps the interactive content below.
96
+
97
+ ```tsx
98
+ <Tooltip trigger="Filters" bottom open label="Refine results" />
99
+ ```
100
+
101
+ **Don't** — A bottom tooltip sits right on top of the next row, masking the control beneath it.
102
+
103
+ ```tsx
104
+ <View style={{ alignItems: "flex-start", gap: 0 }}>
105
+ <Tooltip trigger="Filters" bottom open label="Refine results" />
106
+ <Button outline small style={{ marginTop: -12 }}>Clear all</Button>
107
+ </View>
108
+ ```
109
+
110
+ ### left
111
+
112
+ **Do** — Reserve space on the left, or flip to the right, so a left-placed tooltip is never cut off.
113
+
114
+ ```tsx
115
+ <Tooltip iconTrigger left open label="Need help?" />
116
+ ```
117
+
118
+ **Don't** — A left tooltip on a trigger at the left edge is clipped by the container's left boundary.
119
+
120
+ ```tsx
121
+ <Tooltip iconTrigger left open label="Need help?" />
122
+ ```
@@ -0,0 +1,113 @@
1
+ import { useState } from "react";
2
+ import { View, Pressable, Text, useTheme, type StyleProp, type ViewStyle } from "../../style/index.js";
3
+ import { Button } from "../button/button.js";
4
+ import { Icon } from "../icon/icon.js";
5
+ import {
6
+ wrapper,
7
+ bubbleGap,
8
+ iconTrigger,
9
+ type Placement,
10
+ type TooltipSkin,
11
+ } from "./tooltip.styles.js";
12
+
13
+ // Shared Tooltip shell. The structure (the trigger plus the open bubble placed
14
+ // next to it via flex order), the public boolean-prop API, the placement
15
+ // precedence, the controlled/uncontrolled open state, the toggle handler, and
16
+ // accessibility all live here once. A platform file supplies only its skin (the
17
+ // bubble surface shape/fill and the label type) and calls createTooltip.
18
+ //
19
+ // Tooltip: a small dark bubble of helper text shown beside a trigger on hover
20
+ // or focus. This RN port renders the open state inline (no portal/Modal): a
21
+ // trigger button with the bubble positioned next to it via flex order, so the
22
+ // docs playground can show it without an overlay layer.
23
+ //
24
+ // Placement is a boolean axis (top default, bottom, left, right); first match
25
+ // wins. top/bottom stack the bubble above/below the trigger in a column;
26
+ // left/right place it beside the trigger in a row.
27
+ export interface TooltipProps {
28
+ // The tip text shown in the bubble.
29
+ label?: string;
30
+ // The element the tip describes; rendered as an <Button outline small>.
31
+ trigger?: string;
32
+ // Render the trigger as a ghost icon button (a settings glyph) instead of the
33
+ // text Button. When set, `trigger` (the label string) is ignored.
34
+ iconTrigger?: boolean;
35
+ // Controlled visibility. Omit for uncontrolled (tap the trigger to toggle).
36
+ open?: boolean;
37
+ // Fired when the bubble is shown/hidden.
38
+ onOpenChange?: (open: boolean) => void;
39
+ // Placement (pick one; default is top).
40
+ top?: boolean;
41
+ bottom?: boolean;
42
+ left?: boolean;
43
+ right?: boolean;
44
+ /** Escape hatch for layout/positioning composition (margins, alignment). */
45
+ style?: StyleProp<ViewStyle>;
46
+ }
47
+
48
+ // Placement precedence when more than one is passed: first match wins.
49
+ function placementOf(p: TooltipProps): Placement {
50
+ if (p.top) return "top";
51
+ if (p.bottom) return "bottom";
52
+ if (p.left) return "left";
53
+ if (p.right) return "right";
54
+ return "top";
55
+ }
56
+
57
+ // Whether the bubble renders before the trigger in source order. top and left
58
+ // place the bubble first; bottom and right place it after.
59
+ const BUBBLE_FIRST: Record<Placement, boolean> = {
60
+ top: true,
61
+ bottom: false,
62
+ left: true,
63
+ right: false,
64
+ };
65
+
66
+ /** Build a Tooltip component from a platform skin. */
67
+ export function createTooltip(skin: TooltipSkin) {
68
+ return function Tooltip(props: TooltipProps) {
69
+ const { label, trigger, iconTrigger: isIconTrigger, onOpenChange, style } = props;
70
+ const placement = placementOf(props);
71
+ const { tokens } = useTheme();
72
+ // Uncontrolled by default: tapping the trigger toggles the bubble (a touch
73
+ // analogue of hover); a controlled `open` prop overrides this.
74
+ const [internalOpen, setInternalOpen] = useState(false);
75
+ const open = props.open ?? internalOpen;
76
+ const setOpen = (next: boolean) => {
77
+ if (props.open === undefined) setInternalOpen(next);
78
+ onOpenChange?.(next);
79
+ };
80
+
81
+ const tip = open ? (
82
+ <View style={[skin.bubble(tokens), bubbleGap[placement]]}>
83
+ <Text style={skin.label(tokens)}>{label}</Text>
84
+ </View>
85
+ ) : null;
86
+
87
+ // Icon trigger: a ghost icon button (40px square) holding the settings glyph,
88
+ // matching a ghost icon Button. The glyph renders directly inside the
89
+ // Pressable (not via Button's <Text> children, which can't host an SVG).
90
+ const triggerEl = isIconTrigger ? (
91
+ <Pressable
92
+ style={({ pressed }) => [iconTrigger, pressed ? { opacity: 0.9 } : null]}
93
+ onPress={() => setOpen(!open)}
94
+ accessibilityRole="button"
95
+ accessibilityLabel={label}
96
+ >
97
+ <Icon settings size={16} />
98
+ </Pressable>
99
+ ) : (
100
+ <Button outline small onPress={() => setOpen(!open)}>
101
+ {trigger}
102
+ </Button>
103
+ );
104
+
105
+ return (
106
+ <View style={[wrapper[placement], style]}>
107
+ {BUBBLE_FIRST[placement] ? tip : null}
108
+ {triggerEl}
109
+ {BUBBLE_FIRST[placement] ? null : tip}
110
+ </View>
111
+ );
112
+ };
113
+ }
@@ -0,0 +1,113 @@
1
+ import { type ViewStyle, type TextStyle } from "react-native";
2
+ import { type ColorTokens, shadow } from "../../style/index.js";
3
+
4
+ // Co-located Tooltip skins, one per platform, all driven by the brand tokens
5
+ // (passed in from useTheme so they follow light/dark and the glass surface). A
6
+ // tooltip carries no brand fill of its own; the convention on every platform is
7
+ // an INVERSE label (a dark bubble in a light theme, painted on the `foreground`
8
+ // token with `background` text) so the tip reads against the page. Only the
9
+ // native SHAPE, radius, type scale, padding, and elevation change per OS:
10
+ // iOS (no native tooltip): a small rounded label, radius ~6, an inverse fill
11
+ // (`foreground` bg with `background` text), caption ~12pt, compact padding,
12
+ // a soft lift. There is no system tooltip, so this is the platform-
13
+ // appropriate label convention, not an invented control.
14
+ // Android (Material 3 plain tooltip): a small rounded rect (radius 4), an
15
+ // inverse-surface fill (dark in a light theme: `foreground` bg, `background`
16
+ // text), body-small ~12sp, padding 8x4, flat (no elevation per M3 plain).
17
+ // Web: the established Canvas look (the current tooltip, lifted verbatim) — a
18
+ // 6-radius dark pill on `foreground`, padding 8x4, `background` text at 12/16
19
+ // medium, with a soft `md` shadow.
20
+
21
+ export type Placement = "top" | "bottom" | "left" | "right";
22
+
23
+ // The contract a platform skin fulfills. Layout (wrapper direction per placement,
24
+ // the gap between bubble and trigger, the bubble-first ordering) is shared and
25
+ // lives in the shell; the skin supplies only the bubble surface and the label
26
+ // type, both functions of the active tokens so the inverse fill follows
27
+ // light/dark automatically.
28
+ export interface TooltipSkin {
29
+ /** The bubble surface: shape, radius, inverse fill, padding, elevation. */
30
+ bubble: (t: ColorTokens) => ViewStyle;
31
+ /** The tip label: size/weight, painted in the inverse text token. */
32
+ label: (t: ColorTokens) => TextStyle;
33
+ }
34
+
35
+ // --- shared layout fragments (identical across platforms) -------------------
36
+
37
+ // Wrapper layout per placement: a column for top/bottom (bubble stacked above or
38
+ // below the trigger), a row for left/right (bubble beside the trigger). Centered
39
+ // on the cross axis and shrunk to its content (self-start).
40
+ export const wrapper: Record<Placement, ViewStyle> = {
41
+ top: { flexDirection: "column", alignItems: "center", alignSelf: "flex-start" },
42
+ bottom: { flexDirection: "column", alignItems: "center", alignSelf: "flex-start" },
43
+ left: { flexDirection: "row", alignItems: "center", alignSelf: "flex-start" },
44
+ right: { flexDirection: "row", alignItems: "center", alignSelf: "flex-start" },
45
+ };
46
+
47
+ // Gap between bubble and trigger (the old `m{b,t,r,l}-1.5` = 6), applied to the
48
+ // bubble on the trigger-facing side: top -> below, bottom -> above, etc.
49
+ export const bubbleGap: Record<Placement, ViewStyle> = {
50
+ top: { marginBottom: 6 },
51
+ bottom: { marginTop: 6 },
52
+ left: { marginRight: 6 },
53
+ right: { marginLeft: 6 },
54
+ };
55
+
56
+ // The icon trigger: a 40px square ghost button holding the settings glyph,
57
+ // matching a ghost icon Button. Press feedback is applied by the component's
58
+ // Pressable (opacity dim on iOS/web, ripple on Android).
59
+ export const iconTrigger: ViewStyle = {
60
+ height: 40,
61
+ width: 40,
62
+ alignItems: "center",
63
+ justifyContent: "center",
64
+ borderRadius: 6,
65
+ };
66
+
67
+ // ---------- Web: the established Canvas look (lifted verbatim) ----------
68
+ // A small dark pill on the `foreground` token (so it inverts against the page),
69
+ // 6 radius, 8x4 padding, a soft `md` lift; the label is 12/16 medium painted in
70
+ // `background` so it reads as light text on the dark bubble.
71
+ export const webSkin: TooltipSkin = {
72
+ bubble: (t) => ({
73
+ borderRadius: 6,
74
+ backgroundColor: t.foreground,
75
+ paddingHorizontal: 8,
76
+ paddingVertical: 4,
77
+ ...shadow("md"),
78
+ }),
79
+ label: (t) => ({ fontSize: 12, lineHeight: 16, fontWeight: "500", color: t.background }),
80
+ };
81
+
82
+ // ---------- iOS (no native tooltip): a small rounded inverse label ----------
83
+ // iOS ships no tooltip, so this is the platform-appropriate label convention: a
84
+ // small rounded rect (radius 6) over the inverse `foreground` fill with
85
+ // `background` text, a compact caption (12/16), tight 8x4 padding, and a soft
86
+ // `md` lift so it floats off the page.
87
+ export const iosSkin: TooltipSkin = {
88
+ bubble: (t) => ({
89
+ borderRadius: 6,
90
+ backgroundColor: t.foreground,
91
+ paddingHorizontal: 8,
92
+ paddingVertical: 4,
93
+ ...shadow("md"),
94
+ }),
95
+ label: (t) => ({ fontSize: 12, lineHeight: 16, fontWeight: "500", color: t.background }),
96
+ };
97
+
98
+ // ---------- Android (Material 3 plain tooltip): inverse-surface rounded rect ----------
99
+ // M3 plain tooltip: a small rounded rect with a tighter 4dp corner radius, an
100
+ // inverse-surface fill (dark in a light theme: `foreground` bg, `background`
101
+ // text), body-small ~12sp, padding 8x4, and FLAT (no elevation) per the M3 plain
102
+ // tooltip spec. A 24dp minimum height keeps single-line tips at the spec height.
103
+ export const androidSkin: TooltipSkin = {
104
+ bubble: (t) => ({
105
+ borderRadius: 4,
106
+ backgroundColor: t.foreground,
107
+ paddingHorizontal: 8,
108
+ paddingVertical: 4,
109
+ minHeight: 24,
110
+ justifyContent: "center",
111
+ }),
112
+ label: (t) => ({ fontSize: 12, lineHeight: 16, fontWeight: "400", color: t.background }),
113
+ };
@@ -0,0 +1,6 @@
1
+ import { createTooltip } from "./tooltip.shared.js";
2
+ import { webSkin } from "./tooltip.styles.js";
3
+
4
+ // Web Tooltip (the base; Metro falls back to it on native, web bundlers resolve it).
5
+ export const Tooltip = createTooltip(webSkin);
6
+ export type { TooltipProps } from "./tooltip.shared.js";