@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,203 @@
1
+ import { useState } from "react";
2
+ import { type GestureResponderEvent, type TextStyle } from "react-native";
3
+ import { View, Pressable, Text, TextInput, useTheme, type ColorTokens, type StyleProp, type ViewStyle } from "../../style/index.js";
4
+ import { Icon } from "../icon/icon.js";
5
+ import { type InputSkin, type Size } from "./input.styles.js";
6
+
7
+ // Shared Input shell. The structure (bare field vs. grouped addon layout, the
8
+ // prefix/suffix boxes, overlaid icons, the optional action button), the
9
+ // public boolean-prop API, the size precedence, the border-color precedence
10
+ // (error > focus > default), accessibility, refs, and handlers all live here
11
+ // once. A platform file supplies only its skin (field shape, fill, border,
12
+ // height, the Android active-indicator underline, press feedback) and calls
13
+ // createInput.
14
+
15
+ // Glyphs an overlaid leading/trailing icon can name. Maps the scalar `icon`
16
+ // string to the Icon atom's flat boolean prop, so the playground stays
17
+ // serializable (a name string, not a React element).
18
+ const ICON_BOOL: Record<string, "search" | "mail" | "lock" | "user" | "key" | "globe"> = {
19
+ search: "search",
20
+ mail: "mail",
21
+ lock: "lock",
22
+ user: "user",
23
+ key: "key",
24
+ globe: "globe",
25
+ };
26
+
27
+ // react-native-web paints a default focus outline on the field; in the grouped
28
+ // (addon) layout that ring is clipped by the rounded, overflow-hidden container
29
+ // and reads as half-baked, so it is suppressed there and the group shows focus
30
+ // on its shared border instead. No-op on native, which has no CSS outline.
31
+ const FIELD_OUTLINE_RESET = { outlineStyle: "none", outlineWidth: 0 } as unknown as TextStyle;
32
+
33
+ export interface InputProps {
34
+ /** Current text value (controlled). */
35
+ value?: string;
36
+ /** Called with the new text on each keystroke. */
37
+ onChangeText?: (text: string) => void;
38
+ /** Placeholder shown while the field is empty. */
39
+ placeholder?: string;
40
+ // State (orthogonal). `error` (alias `invalid`) flags a validation problem.
41
+ error?: boolean;
42
+ invalid?: boolean;
43
+ disabled?: boolean;
44
+ /** Read-only: shows the value but blocks editing without the dimmed look. */
45
+ readOnly?: boolean;
46
+ // Size (pick one; default is the medium field).
47
+ small?: boolean;
48
+ large?: boolean;
49
+ /** Full-width field (the default); pass to be explicit. */
50
+ block?: boolean;
51
+ /** Multi-line text area instead of a single-line field. Ignored when addons
52
+ * (prefix/suffix/icons/action) are present, which are single-line only. */
53
+ multiline?: boolean;
54
+
55
+ // Addons. Passing any of these switches the field to the grouped layout: a
56
+ // single control where a leading prefix and/or trailing suffix share one outer
57
+ // border with the field (squared joined edges, 1px inner separators), so they
58
+ // read as one piece rather than detached. Addons are plain strings, so no icon
59
+ // library is required at this layer.
60
+ /** Leading addon content (e.g. "https://", "$", an icon glyph). */
61
+ prefix?: string;
62
+ /** Trailing addon content (e.g. "@canvas.dev", "USD", "Copy"). */
63
+ suffix?: string;
64
+ // Overlaid glyph mode. Unlike prefix/suffix (a bordered addon box), these
65
+ // float a real Lucide glyph INSIDE the field with no separator, and pad the
66
+ // text away from it (pl-9 / pr-9). `icon` names which glyph (see ICON_BOOL).
67
+ /** Render `icon` as a passive glyph inside the left of the field. */
68
+ leadingIcon?: boolean;
69
+ /** Render `icon` as a passive glyph inside the right of the field. */
70
+ trailingIcon?: boolean;
71
+ /** Glyph name for leadingIcon/trailingIcon (e.g. "search", "mail"). */
72
+ icon?: string;
73
+ /** Render the suffix as a pressable action button rather than a passive label. */
74
+ action?: boolean;
75
+ /** Called when the action suffix is pressed (action only). */
76
+ onActionPress?: (event: GestureResponderEvent) => void;
77
+
78
+ /** Escape hatch for layout/positioning composition (mainly width). */
79
+ style?: StyleProp<ViewStyle>;
80
+ }
81
+
82
+ // Size precedence when more than one is passed: first match wins.
83
+ function sizeOf(p: InputProps): Size {
84
+ if (p.large) return "large";
85
+ if (p.small) return "small";
86
+ return "base";
87
+ }
88
+
89
+ /** Build an Input component from a platform skin. */
90
+ export function createInput(skin: InputSkin) {
91
+ return function Input(props: InputProps) {
92
+ const {
93
+ value,
94
+ onChangeText,
95
+ placeholder,
96
+ disabled,
97
+ readOnly,
98
+ multiline,
99
+ prefix,
100
+ suffix,
101
+ leadingIcon,
102
+ trailingIcon,
103
+ icon,
104
+ action,
105
+ onActionPress,
106
+ style,
107
+ } = props;
108
+ const isError = !!(props.error || props.invalid);
109
+ const size = sizeOf(props);
110
+ const [focused, setFocused] = useState(false);
111
+ const { tokens } = useTheme();
112
+
113
+ // Border-color precedence: error > focus > default input border. Shared by
114
+ // both layouts; in the grouped layout it lives on the outer border so prefix
115
+ // + field + suffix light up together as one control. The skin decides how
116
+ // that color is used (a full border on iOS/web, the bottom active indicator
117
+ // on Android).
118
+ const borderColor: keyof ColorTokens = isError ? "destructive" : focused ? "ring" : "input";
119
+ const text = skin.text(tokens, size);
120
+ const iconName = icon != null ? ICON_BOOL[icon] : undefined;
121
+ const hasAddons = prefix != null || suffix != null || !!leadingIcon || !!trailingIcon || !!action;
122
+
123
+ const common = {
124
+ value,
125
+ onChangeText,
126
+ placeholder,
127
+ placeholderTextColor: tokens["muted-foreground"],
128
+ editable: !disabled && !readOnly,
129
+ selectionColor: tokens.primary, // brand cursor / selection on every platform
130
+ onFocus: () => setFocused(true),
131
+ onBlur: () => setFocused(false),
132
+ };
133
+
134
+ // Bare field (and the multiline text area): no addons.
135
+ if (!hasAddons) {
136
+ return (
137
+ <TextInput
138
+ style={[
139
+ skin.bareField(tokens, borderColor, focused, isError),
140
+ skin.bareBox(size, !!multiline),
141
+ text,
142
+ disabled ? { opacity: skin.disabledOpacity } : null,
143
+ style,
144
+ ]}
145
+ multiline={multiline}
146
+ textAlignVertical={multiline ? "top" : "center"}
147
+ {...common}
148
+ />
149
+ );
150
+ }
151
+
152
+ // Grouped field: prefix/suffix addons, overlaid icons, optional action button.
153
+ // The whole group shares one border, so it owns the focus state and the inner
154
+ // field's default outline is suppressed (see FIELD_OUTLINE_RESET).
155
+ const height = skin.groupedHeight(size);
156
+ return (
157
+ <View
158
+ style={[skin.groupContainer(tokens, borderColor, focused, isError), disabled ? { opacity: skin.disabledOpacity } : null, style]}
159
+ >
160
+ {prefix != null ? (
161
+ <View style={skin.addonBox(tokens, "left", height)}>
162
+ <Text style={[skin.addonText(tokens), text]}>{prefix}</Text>
163
+ </View>
164
+ ) : null}
165
+
166
+ {leadingIcon && iconName != null ? (
167
+ <View style={skin.iconOverlay("left")} pointerEvents="none">
168
+ <Icon {...{ [iconName]: true }} muted size={16} />
169
+ </View>
170
+ ) : null}
171
+
172
+ <TextInput style={[skin.groupField(tokens, !!leadingIcon, !!trailingIcon), text, FIELD_OUTLINE_RESET]} {...common} />
173
+
174
+ {trailingIcon && iconName != null ? (
175
+ <View style={skin.iconOverlay("right")} pointerEvents="none">
176
+ <Icon {...{ [iconName]: true }} muted size={16} />
177
+ </View>
178
+ ) : null}
179
+
180
+ {suffix != null ? (
181
+ action ? (
182
+ <Pressable
183
+ style={({ pressed }) => [
184
+ skin.addonBox(tokens, "right", height),
185
+ skin.pressedOpacity != null && pressed ? { opacity: skin.pressedOpacity } : null,
186
+ ]}
187
+ onPress={onActionPress}
188
+ disabled={disabled}
189
+ android_ripple={skin.ripple ? skin.ripple(tokens) : undefined}
190
+ accessibilityRole="button"
191
+ >
192
+ <Text style={[skin.actionText(tokens), text]}>{suffix}</Text>
193
+ </Pressable>
194
+ ) : (
195
+ <View style={skin.addonBox(tokens, "right", height)}>
196
+ <Text style={[skin.addonText(tokens), text]}>{suffix}</Text>
197
+ </View>
198
+ )
199
+ ) : null}
200
+ </View>
201
+ );
202
+ };
203
+ }
@@ -0,0 +1,286 @@
1
+ import { type ViewStyle, type TextStyle } from "react-native";
2
+ import { type ColorTokens } from "../../style/index.js";
3
+
4
+ // Co-located Input skins, one per platform. The BRAND survives on every platform
5
+ // (the cursor/selection is always the indigo `primary`, the focus accent is the
6
+ // `ring`, never a platform default), and only the native SHAPE, sizing, fill,
7
+ // border treatment, and press feedback change per OS:
8
+ // iOS (HIG, iOS 26+/Liquid Glass): a PLAIN text field — the value text sits on
9
+ // a TRANSPARENT surface with a single bottom HAIRLINE rule (1pt `border`
10
+ // separator) and NO fill, NO surrounding box, NO rounded capsule. Focus
11
+ // thickens/tints the hairline to the brand (`ring`), error to `destructive`;
12
+ // press (action suffix) = opacity dim (~0.8). The cursor/selection stays the
13
+ // indigo `primary`.
14
+ // Android (Material 3 filled): a subtle fill (`muted`), TOP corners ~4 radius
15
+ // and a flat bottom, a bottom active-indicator underline (1dp `border` at
16
+ // rest -> 2dp `ring` on focus, `destructive` on error), ~56dp tall; the
17
+ // action suffix uses android_ripple; disabled opacity 0.38.
18
+ // Web: the established Canvas look (the current input, lifted verbatim) — full
19
+ // 1px border (error > focus > input), 6 radius, background fill, 36/32/40
20
+ // tall, opacity 0.5 disabled, action press opacity 0.9.
21
+
22
+ export type Size = "small" | "base" | "large";
23
+
24
+ // The contract a platform skin fulfills. Both layouts (bare field, grouped addon
25
+ // row) and the size/state inputs the shell resolves are passed in; the skin maps
26
+ // them to RN style objects. `borderColor` is a token key (error > focus > input)
27
+ // the shell already resolved; the skin reads tokens[borderColor].
28
+ export interface InputSkin {
29
+ /** Type scale per size; the field and its addons share it so they line up. */
30
+ text: (t: ColorTokens, size: Size) => TextStyle;
31
+ /** Height (single line) or min-height (multiline) of the bare field. */
32
+ bareBox: (size: Size, multiline: boolean) => TextStyle;
33
+ /** Fixed row height for the grouped layout (addon boxes set it). */
34
+ groupedHeight: (size: Size) => number;
35
+ /** The bare field surface: shape, fill, border/underline for the active state. */
36
+ bareField: (t: ColorTokens, borderColor: keyof ColorTokens, focused: boolean, error: boolean) => TextStyle;
37
+ /** The grouped (addon) outer: the row that shares one border/underline. */
38
+ groupContainer: (t: ColorTokens, borderColor: keyof ColorTokens, focused: boolean, error: boolean) => ViewStyle;
39
+ /** The inner field inside the group (grows to fill; pads away from icons). */
40
+ groupField: (t: ColorTokens, leadingIcon: boolean, trailingIcon: boolean) => TextStyle;
41
+ /** A prefix/suffix addon box. */
42
+ addonBox: (t: ColorTokens, side: "left" | "right", height: number) => ViewStyle;
43
+ addonText: (t: ColorTokens) => TextStyle;
44
+ actionText: (t: ColorTokens) => TextStyle;
45
+ /** Overlaid icon position inside the field (left or right gutter). */
46
+ iconOverlay: (side: "left" | "right") => ViewStyle;
47
+ /** Opacity applied to the field when disabled. */
48
+ disabledOpacity: number;
49
+ /** iOS/web dim the action suffix on press; Android uses a ripple instead (null). */
50
+ pressedOpacity: number | null;
51
+ /** Android ripple over the action suffix; null on iOS/web. */
52
+ ripple: ((t: ColorTokens) => { color: string; borderless: boolean }) | null;
53
+ }
54
+
55
+ // --- shared type scale (identical across platforms; brand type, not a face) --
56
+ function webText(_t: ColorTokens, size: Size): TextStyle {
57
+ if (size === "large") return { fontSize: 16, lineHeight: 24 };
58
+ if (size === "small") return { fontSize: 12, lineHeight: 16 };
59
+ return { fontSize: 14, lineHeight: 20 };
60
+ }
61
+
62
+ // ---------- Web: the established Canvas look ----------
63
+ export const webSkin: InputSkin = {
64
+ text: webText,
65
+ bareBox: (size, multiline) => {
66
+ if (multiline) return { minHeight: size === "large" ? 96 : size === "small" ? 64 : 80 };
67
+ return { height: size === "large" ? 40 : size === "small" ? 32 : 36 };
68
+ },
69
+ groupedHeight: (size) => (size === "large" ? 40 : size === "small" ? 32 : 36),
70
+ bareField: (t, borderColor) => ({
71
+ width: "100%",
72
+ borderRadius: 6,
73
+ borderWidth: 1,
74
+ borderColor: t[borderColor],
75
+ backgroundColor: t.background,
76
+ paddingHorizontal: 12,
77
+ paddingVertical: 8,
78
+ color: t.foreground,
79
+ }),
80
+ groupContainer: (t, borderColor) => ({
81
+ flexDirection: "row",
82
+ alignItems: "stretch",
83
+ width: "100%",
84
+ borderWidth: 1,
85
+ borderColor: t[borderColor],
86
+ borderRadius: 6,
87
+ overflow: "hidden",
88
+ backgroundColor: t.background,
89
+ }),
90
+ groupField: (t, leadingIcon, trailingIcon) => ({
91
+ flexGrow: 1,
92
+ flexShrink: 1,
93
+ flexBasis: "0%",
94
+ height: "100%",
95
+ paddingHorizontal: 12,
96
+ paddingVertical: 8,
97
+ color: t.foreground,
98
+ ...(leadingIcon ? { paddingLeft: 36 } : null),
99
+ ...(trailingIcon ? { paddingRight: 36 } : null),
100
+ }),
101
+ addonBox: (t, side, height) => ({
102
+ justifyContent: "center",
103
+ backgroundColor: t.muted,
104
+ paddingHorizontal: 12,
105
+ borderColor: t.border,
106
+ ...(side === "left" ? { borderRightWidth: 1 } : { borderLeftWidth: 1 }),
107
+ height,
108
+ }),
109
+ addonText: (t) => ({ color: t["muted-foreground"] }),
110
+ actionText: (t) => ({ fontWeight: "500", color: t.foreground }),
111
+ iconOverlay: (side) => ({
112
+ position: "absolute",
113
+ top: 0,
114
+ bottom: 0,
115
+ zIndex: 10,
116
+ justifyContent: "center",
117
+ ...(side === "left" ? { left: 0, paddingLeft: 12 } : { right: 0, paddingRight: 12 }),
118
+ }),
119
+ disabledOpacity: 0.5,
120
+ pressedOpacity: 0.9,
121
+ ripple: null,
122
+ };
123
+
124
+ // ---------- iOS (HIG, iOS 26+/Liquid Glass): plain field, transparent, bottom hairline ----------
125
+ // Apple's plain text field on iOS 26+: the value text sits directly on a
126
+ // TRANSPARENT surface with a single bottom HAIRLINE rule (1pt separator) and no
127
+ // fill, no surrounding box, no rounded capsule. Canvas keeps the brand by
128
+ // thickening + tinting that hairline to the brand `ring` on focus (and
129
+ // `destructive` on error); at rest it is the faint `border` separator. The
130
+ // cursor/selection is always the indigo `primary` (set in the shell).
131
+ function iosHairline(t: ColorTokens, borderColor: keyof ColorTokens, focused: boolean, error: boolean): ViewStyle {
132
+ // Rest = the faint `border` separator; focus/error thicken to 2pt and tint to
133
+ // the brand color the shell resolved (ring on focus, destructive on error).
134
+ const active = focused || error;
135
+ return {
136
+ borderBottomWidth: active ? 2 : 1,
137
+ borderBottomColor: active ? t[borderColor] : t.border,
138
+ };
139
+ }
140
+ export const iosSkin: InputSkin = {
141
+ text: webText,
142
+ bareBox: (size, multiline) => {
143
+ if (multiline) return { minHeight: size === "large" ? 110 : size === "small" ? 76 : 92 };
144
+ return { height: size === "large" ? 50 : size === "small" ? 36 : 44 };
145
+ },
146
+ groupedHeight: (size) => (size === "large" ? 50 : size === "small" ? 36 : 44),
147
+ bareField: (t, borderColor, focused, error) => ({
148
+ width: "100%",
149
+ // Plain field: transparent surface, no box/radius, only a bottom hairline.
150
+ backgroundColor: "transparent",
151
+ ...iosHairline(t, borderColor, focused, error),
152
+ // No horizontal inset so the value text aligns flush with the hairline edge,
153
+ // as in the iOS 27 render.
154
+ paddingHorizontal: 0,
155
+ paddingVertical: 10,
156
+ color: t.foreground,
157
+ }),
158
+ groupContainer: (t, borderColor, focused, error) => ({
159
+ flexDirection: "row",
160
+ alignItems: "stretch",
161
+ width: "100%",
162
+ // The whole row shares the single bottom hairline; no fill, no box, no radius.
163
+ backgroundColor: "transparent",
164
+ ...iosHairline(t, borderColor, focused, error),
165
+ }),
166
+ groupField: (t, leadingIcon, trailingIcon) => ({
167
+ flexGrow: 1,
168
+ flexShrink: 1,
169
+ flexBasis: "0%",
170
+ height: "100%",
171
+ paddingHorizontal: 0,
172
+ paddingVertical: 10,
173
+ color: t.foreground,
174
+ ...(leadingIcon ? { paddingLeft: 28 } : null),
175
+ ...(trailingIcon ? { paddingRight: 28 } : null),
176
+ }),
177
+ // Addons are inline on the transparent field (no filled box, no separator) so
178
+ // the row reads as one plain line over the shared hairline.
179
+ addonBox: (_t, side, height) => ({
180
+ justifyContent: "center",
181
+ backgroundColor: "transparent",
182
+ ...(side === "left" ? { paddingRight: 8 } : { paddingLeft: 8 }),
183
+ height,
184
+ }),
185
+ addonText: (t) => ({ color: t["muted-foreground"] }),
186
+ actionText: (t) => ({ fontWeight: "600", color: t.primary }),
187
+ iconOverlay: (side) => ({
188
+ position: "absolute",
189
+ top: 0,
190
+ bottom: 0,
191
+ zIndex: 10,
192
+ justifyContent: "center",
193
+ ...(side === "left" ? { left: 0 } : { right: 0 }),
194
+ }),
195
+ disabledOpacity: 0.5,
196
+ pressedOpacity: 0.8,
197
+ ripple: null,
198
+ };
199
+
200
+ // ---------- Android (Material 3 filled): subtle fill, top radius, bottom active indicator ----------
201
+ // M3 filled text field: a ~56dp container with a subtle fill (`muted` ~
202
+ // surface-container-highest), the TOP corners rounded ~4dp and a flat bottom,
203
+ // and a bottom active-indicator underline — 1dp `border` at rest, 2dp `ring`
204
+ // (brand) on focus, `destructive` on error. The brand survives via the focused
205
+ // indicator color and the action suffix's primary label + ripple.
206
+ const ANDROID_TOP_RADIUS = 4;
207
+ function androidUnderline(t: ColorTokens, borderColor: keyof ColorTokens, focused: boolean, error: boolean): ViewStyle {
208
+ // M3 active indicator: a VISIBLE baseline at rest (on-surface-variant ~ the
209
+ // `muted-foreground` token), thickening to 2dp in the brand `ring` on focus /
210
+ // `destructive` on error. The rest color must read clearly so the filled field is
211
+ // distinct from the iOS lineless capsule (the regression this fixes).
212
+ const active = focused || error;
213
+ return {
214
+ borderBottomWidth: active ? 2 : 1,
215
+ borderBottomColor: active ? t[borderColor] : t["muted-foreground"],
216
+ };
217
+ }
218
+ export const androidSkin: InputSkin = {
219
+ // M3 body input is 16sp; nudge the base/large up, keep small readable.
220
+ text: (_t, size) => {
221
+ if (size === "large") return { fontSize: 18, lineHeight: 26 };
222
+ if (size === "small") return { fontSize: 14, lineHeight: 20 };
223
+ return { fontSize: 16, lineHeight: 24 };
224
+ },
225
+ bareBox: (size, multiline) => {
226
+ if (multiline) return { minHeight: size === "large" ? 120 : size === "small" ? 88 : 104 };
227
+ return { height: size === "large" ? 60 : size === "small" ? 48 : 56 };
228
+ },
229
+ groupedHeight: (size) => (size === "large" ? 60 : size === "small" ? 48 : 56),
230
+ bareField: (t, borderColor, focused, error) => ({
231
+ width: "100%",
232
+ borderTopLeftRadius: ANDROID_TOP_RADIUS,
233
+ borderTopRightRadius: ANDROID_TOP_RADIUS,
234
+ borderBottomLeftRadius: 0,
235
+ borderBottomRightRadius: 0,
236
+ ...androidUnderline(t, borderColor, focused, error),
237
+ backgroundColor: t.muted,
238
+ paddingHorizontal: 16,
239
+ paddingVertical: 8,
240
+ color: t.foreground,
241
+ }),
242
+ groupContainer: (t, borderColor, focused, error) => ({
243
+ flexDirection: "row",
244
+ alignItems: "stretch",
245
+ width: "100%",
246
+ borderTopLeftRadius: ANDROID_TOP_RADIUS,
247
+ borderTopRightRadius: ANDROID_TOP_RADIUS,
248
+ ...androidUnderline(t, borderColor, focused, error),
249
+ overflow: "hidden",
250
+ backgroundColor: t.muted,
251
+ }),
252
+ groupField: (t, leadingIcon, trailingIcon) => ({
253
+ flexGrow: 1,
254
+ flexShrink: 1,
255
+ flexBasis: "0%",
256
+ height: "100%",
257
+ paddingHorizontal: 16,
258
+ paddingVertical: 8,
259
+ color: t.foreground,
260
+ ...(leadingIcon ? { paddingLeft: 44 } : null),
261
+ ...(trailingIcon ? { paddingRight: 44 } : null),
262
+ }),
263
+ // The addon shares the field fill (M3 leading/trailing content sits inside the
264
+ // filled container) with a hairline separator.
265
+ addonBox: (t, side, height) => ({
266
+ justifyContent: "center",
267
+ backgroundColor: t.muted,
268
+ paddingHorizontal: 16,
269
+ borderColor: t.border,
270
+ ...(side === "left" ? { borderRightWidth: 1 } : { borderLeftWidth: 1 }),
271
+ height,
272
+ }),
273
+ addonText: (t) => ({ color: t["muted-foreground"] }),
274
+ actionText: (t) => ({ fontWeight: "500", color: t.primary, textTransform: "uppercase", letterSpacing: 0.5 }),
275
+ iconOverlay: (side) => ({
276
+ position: "absolute",
277
+ top: 0,
278
+ bottom: 0,
279
+ zIndex: 10,
280
+ justifyContent: "center",
281
+ ...(side === "left" ? { left: 0, paddingLeft: 16 } : { right: 0, paddingRight: 16 }),
282
+ }),
283
+ disabledOpacity: 0.38, // M3 disabled opacity
284
+ pressedOpacity: null, // Android uses a ripple instead
285
+ ripple: (t) => ({ color: t.primary, borderless: false }),
286
+ };
@@ -0,0 +1,6 @@
1
+ import { createInput } from "./input.shared.js";
2
+ import { webSkin } from "./input.styles.js";
3
+
4
+ // Web Input (the base; Metro falls back to it on native, web bundlers resolve it).
5
+ export const Input = createInput(webSkin);
6
+ export type { InputProps } from "./input.shared.js";
@@ -0,0 +1,91 @@
1
+ # Kbd
2
+
3
+ Keyboard shortcut indicator badge.
4
+
5
+ ## Usage
6
+
7
+ ```tsx
8
+ <View style={{ flexDirection: "row", alignItems: "center", gap: 4 }}>
9
+ <Kbd>⌘</Kbd>
10
+ <Text style={{ fontSize: 12, lineHeight: 16, color: tokens["muted-foreground"] }}>+</Text>
11
+ <Kbd>K</Kbd>
12
+ </View>
13
+ ```
14
+
15
+ ## Variants
16
+
17
+ ### Mode - single
18
+
19
+ ```tsx
20
+ <Kbd>⌘</Kbd>
21
+ ```
22
+
23
+ ### Mode - in a sentence
24
+
25
+ ```tsx
26
+ <View style={{ flexDirection: "row", flexWrap: "wrap", alignItems: "center", gap: 4 }}>
27
+ <Text style={{ fontSize: 14, lineHeight: 20, color: tokens.foreground }}>Press </Text>
28
+ <Kbd>⌘</Kbd>
29
+ <Kbd>K</Kbd>
30
+ <Text style={{ fontSize: 14, lineHeight: 20, color: tokens.foreground }}> to search.</Text>
31
+ </View>
32
+ ```
33
+
34
+ ## Do & Don't
35
+
36
+ ### Single
37
+
38
+ **Do** — Use the single mode for one real key; give each cap exactly one key.
39
+
40
+ ```tsx
41
+ <Kbd>Esc</Kbd>
42
+ ```
43
+
44
+ **Don't** — Packing a whole shortcut into one key cap reads as a single keystroke that does not exist.
45
+
46
+ ```tsx
47
+ <Kbd>⌘K</Kbd>
48
+ ```
49
+
50
+ ### Combo
51
+
52
+ **Do** — Separate each key with a + so the combo reads as keys pressed together.
53
+
54
+ ```tsx
55
+ <View style={{ flexDirection: "row", alignItems: "center", gap: 4 }}>
56
+ <Kbd>⌘</Kbd>
57
+ <Text style={{ fontSize: 12, lineHeight: 16, color: tokens["muted-foreground"] }}>+</Text>
58
+ <Kbd>⇧</Kbd>
59
+ <Text style={{ fontSize: 12, lineHeight: 16, color: tokens["muted-foreground"] }}>+</Text>
60
+ <Kbd>P</Kbd>
61
+ </View>
62
+ ```
63
+
64
+ **Don't** — Caps butted together with no separator blur into one token and hide that it is a chord.
65
+
66
+ ```tsx
67
+ <View style={{ flexDirection: "row", alignItems: "center" }}>
68
+ <Kbd>⌘</Kbd>
69
+ <Kbd>⇧</Kbd>
70
+ <Kbd>P</Kbd>
71
+ </View>
72
+ ```
73
+
74
+ ### In a sentence
75
+
76
+ **Do** — Wrap each key in a kbd so shortcuts read as physical keys.
77
+
78
+ ```tsx
79
+ <View style={{ flexDirection: "row", alignItems: "center", gap: 4 }}>
80
+ <Text style={{ fontSize: 14, lineHeight: 20 }}>Press</Text>
81
+ <Kbd>Ctrl</Kbd>
82
+ <Kbd>K</Kbd>
83
+ <Text style={{ fontSize: 14, lineHeight: 20 }}>to search.</Text>
84
+ </View>
85
+ ```
86
+
87
+ **Don't** — Plain-text shortcuts blend into the prose and are easy to miss.
88
+
89
+ ```tsx
90
+ <Text style={{ fontSize: 14, lineHeight: 20 }}>Press Ctrl+K to search.</Text>
91
+ ```
@@ -0,0 +1,33 @@
1
+ import { type ViewStyle, type TextStyle } from "react-native";
2
+ import { type ColorTokens } from "../../style/index.js";
3
+
4
+ // Co-located Kbd styles. The cap has one fixed look (no axes), so the layout-only
5
+ // box geometry is a static fragment and the colored surface (border + muted fill)
6
+ // plus the muted label color are functions of the active tokens (so the cap
7
+ // follows light/dark and reads as glass at the theming level).
8
+
9
+ // The key-cap box: a centered row, fixed cap height, a minimum width so a single
10
+ // glyph still reads as a key, the small radius, a hairline border, and snug
11
+ // horizontal padding.
12
+ export const capBox: ViewStyle = {
13
+ flexDirection: "row",
14
+ height: 20,
15
+ minWidth: 20,
16
+ alignItems: "center",
17
+ justifyContent: "center",
18
+ borderRadius: 4,
19
+ borderWidth: 1,
20
+ paddingHorizontal: 6,
21
+ };
22
+
23
+ // The cap surface: the muted fill and the hairline border color.
24
+ export function capSurface(tokens: ColorTokens): ViewStyle {
25
+ return { borderColor: tokens.border, backgroundColor: tokens.muted };
26
+ }
27
+
28
+ // The key label: small, medium-weight, muted text.
29
+ export const labelType: TextStyle = { fontSize: 12, lineHeight: 16, fontWeight: "500" };
30
+
31
+ export function labelColor(tokens: ColorTokens): TextStyle {
32
+ return { color: tokens["muted-foreground"] };
33
+ }
@@ -0,0 +1,27 @@
1
+ import { type ReactNode } from "react";
2
+ import { View, Text, useTheme, type StyleProp, type ViewStyle } from "../../style/index.js";
3
+ import * as s from "./kbd.styles.js";
4
+
5
+ // Kbd: a keyboard shortcut indicator. Renders a single key cap, a small
6
+ // bordered, slightly raised label with monospace-ish small text. The key label
7
+ // comes from children. One cap = one key; compose multiple Kbd caps with a
8
+ // separator for a chord (e.g. ⌘ + K), per the docs.
9
+ //
10
+ // The cap has one fixed look (no size or intent variants), so there are no
11
+ // boolean axes to map: the markup mirrors the docs' kbdCls exactly.
12
+
13
+ export interface KbdProps {
14
+ children?: ReactNode;
15
+ /** Escape hatch for layout/positioning composition (mainly width). */
16
+ style?: StyleProp<ViewStyle>;
17
+ }
18
+
19
+ export function Kbd({ children, style }: KbdProps) {
20
+ const { tokens } = useTheme();
21
+
22
+ return (
23
+ <View style={[s.capBox, s.capSurface(tokens), style]}>
24
+ {children != null ? <Text style={[s.labelType, s.labelColor(tokens)]}>{children}</Text> : null}
25
+ </View>
26
+ );
27
+ }