@olympusoss/canvas 3.2.1 → 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 (302) hide show
  1. package/README.md +75 -65
  2. package/package.json +11 -5
  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/styles/canvas.css +127 -74
  234. package/tsconfig.json +4 -2
  235. package/src/cn.ts +0 -3
  236. package/styles/atoms/avatar.css +0 -22
  237. package/styles/atoms/badge.css +0 -83
  238. package/styles/atoms/breadcrumb.css +0 -35
  239. package/styles/atoms/button-group.css +0 -23
  240. package/styles/atoms/button.css +0 -107
  241. package/styles/atoms/checkbox.css +0 -55
  242. package/styles/atoms/combobox.css +0 -76
  243. package/styles/atoms/dropdown.css +0 -54
  244. package/styles/atoms/icon.css +0 -8
  245. package/styles/atoms/input-group.css +0 -45
  246. package/styles/atoms/input.css +0 -56
  247. package/styles/atoms/kbd.css +0 -15
  248. package/styles/atoms/pagination.css +0 -48
  249. package/styles/atoms/popover.css +0 -14
  250. package/styles/atoms/radio.css +0 -28
  251. package/styles/atoms/select.css +0 -57
  252. package/styles/atoms/separator.css +0 -32
  253. package/styles/atoms/skeleton.css +0 -32
  254. package/styles/atoms/spinner.css +0 -26
  255. package/styles/atoms/switch.css +0 -45
  256. package/styles/atoms/textarea.css +0 -31
  257. package/styles/atoms/tooltip.css +0 -53
  258. package/styles/atoms/typography.css +0 -105
  259. package/styles/base.css +0 -17
  260. package/styles/molecules/alert.css +0 -66
  261. package/styles/molecules/card.css +0 -58
  262. package/styles/molecules/code-block.css +0 -18
  263. package/styles/molecules/empty-state.css +0 -17
  264. package/styles/molecules/field.css +0 -27
  265. package/styles/molecules/form.css +0 -27
  266. package/styles/molecules/page-header.css +0 -52
  267. package/styles/molecules/section-card.css +0 -49
  268. package/styles/molecules/stat-card.css +0 -71
  269. package/styles/molecules/toast.css +0 -95
  270. package/styles/organisms/app-shell.css +0 -46
  271. package/styles/organisms/calendar.css +0 -73
  272. package/styles/organisms/command.css +0 -95
  273. package/styles/organisms/data-table.css +0 -142
  274. package/styles/organisms/dialog.css +0 -72
  275. package/styles/organisms/filter-panel.css +0 -58
  276. package/styles/organisms/row-menu.css +0 -69
  277. package/styles/organisms/sheet.css +0 -70
  278. package/styles/organisms/sidebar.css +0 -146
  279. package/styles/organisms/stepper.css +0 -63
  280. package/styles/organisms/tabs.css +0 -40
  281. package/styles/organisms/topbar.css +0 -24
  282. package/styles/patterns/backdrops.css +0 -35
  283. package/styles/patterns/density.css +0 -66
  284. package/styles/patterns/focus.css +0 -22
  285. package/styles/patterns/glass.css +0 -85
  286. package/styles/patterns/high-contrast.css +0 -70
  287. package/styles/patterns/reduced-motion.css +0 -12
  288. package/styles/patterns/scrollbar.css +0 -10
  289. package/styles/reset.css +0 -89
  290. package/styles/tokens/colors.css +0 -108
  291. package/styles/tokens/motion.css +0 -33
  292. package/styles/tokens/radius.css +0 -10
  293. package/styles/tokens/shadows.css +0 -35
  294. package/styles/tokens/spacing.css +0 -19
  295. package/styles/tokens/typography.css +0 -6
  296. package/styles/tokens/z-index.css +0 -12
  297. package/styles/utilities/display.css +0 -66
  298. package/styles/utilities/flexbox.css +0 -240
  299. package/styles/utilities/gap.css +0 -288
  300. package/styles/utilities/grid.css +0 -138
  301. package/styles/utilities/position.css +0 -78
  302. package/styles/utilities/sizing.css +0 -138
@@ -0,0 +1,6 @@
1
+ import { createRadio } from "./radio.shared.js";
2
+ import { webSkin } from "./radio.styles.js";
3
+
4
+ // Web Radio (the base; Metro falls back to it on native, web bundlers resolve it).
5
+ export const Radio = createRadio(webSkin);
6
+ export type { RadioProps } from "./radio.shared.js";
@@ -0,0 +1,6 @@
1
+ import { createSelect } from "./select.shared.js";
2
+ import { androidSkin } from "./select.styles.js";
3
+
4
+ // Material 3 (exposed dropdown) Select. Metro resolves this file on Android; the docs import it for preview.
5
+ export const Select = createSelect(androidSkin);
6
+ export type { SelectProps } from "./select.shared.js";
@@ -0,0 +1,6 @@
1
+ import { createSelect } from "./select.shared.js";
2
+ import { iosSkin } from "./select.styles.js";
3
+
4
+ // iOS (HIG pop-up button) Select. Metro resolves this file on iOS; the docs import it for preview.
5
+ export const Select = createSelect(iosSkin);
6
+ export type { SelectProps } from "./select.shared.js";
@@ -0,0 +1,156 @@
1
+ # Selects
2
+
3
+ Native select restyled to match Canvas inputs.
4
+
5
+ ## Usage
6
+
7
+ ```tsx
8
+ <Select
9
+ label="Country"
10
+ value="United States"
11
+ options={["United States", "Canada", "Mexico", "United Kingdom"]}
12
+ placeholder="Select a country"
13
+ style={{ maxWidth: 280 }}
14
+ />
15
+ ```
16
+
17
+ ## Variants
18
+
19
+ ### Size - sm
20
+
21
+ ```tsx
22
+ <Select
23
+ small
24
+ label="Country"
25
+ value="United States"
26
+ options={["United States", "Canada", "Mexico", "United Kingdom"]}
27
+ placeholder="Select a country"
28
+ style={{ maxWidth: 280 }}
29
+ />
30
+ ```
31
+
32
+ ### Size - lg
33
+
34
+ ```tsx
35
+ <Select
36
+ large
37
+ label="Country"
38
+ value="United States"
39
+ options={["United States", "Canada", "Mexico", "United Kingdom"]}
40
+ placeholder="Select a country"
41
+ style={{ maxWidth: 280 }}
42
+ />
43
+ ```
44
+
45
+ ### With leading icon
46
+
47
+ ```tsx
48
+ <Select
49
+ label="Country"
50
+ icon
51
+ value="United States"
52
+ options={["United States", "Canada", "Mexico", "United Kingdom"]}
53
+ placeholder="Select a country"
54
+ style={{ maxWidth: 280 }}
55
+ />
56
+ ```
57
+
58
+ ### Disabled
59
+
60
+ ```tsx
61
+ <Select
62
+ disabled
63
+ label="Country"
64
+ value="United States"
65
+ options={["United States", "Canada", "Mexico", "United Kingdom"]}
66
+ placeholder="Select a country"
67
+ style={{ maxWidth: 280 }}
68
+ />
69
+ ```
70
+
71
+ ## Do & Don't
72
+
73
+ **Do** — Mark the placeholder disabled and selected so it prompts without being a valid choice.
74
+
75
+ ```tsx
76
+ <View style={{ minHeight: 220 }}>
77
+ <Select open label="Country" placeholder="Choose a country…" options={["United States", "Canada", "Mexico"]} style={{ maxWidth: 280 }} />
78
+ </View>
79
+ ```
80
+
81
+ **Don't** — A placeholder as a normal option can be submitted as a real value.
82
+
83
+ ```tsx
84
+ <View style={{ minHeight: 260 }}>
85
+ <Select open label="Country" value="Choose a country…" options={["Choose a country…", "United States", "Canada", "Mexico"]} style={{ maxWidth: 280 }} />
86
+ </View>
87
+ ```
88
+
89
+ ### sm
90
+
91
+ **Do** — Keep the small select inline with a short label so it stays compact inside toolbars and table footers.
92
+
93
+ ```tsx
94
+ <View style={{ flexDirection: "row", alignItems: "center", gap: 8 }}>
95
+ <Text style={{ fontSize: 12, lineHeight: 16, color: tokens["muted-foreground"] }}>Rows</Text>
96
+ <Select small value="10" options={["10", "25", "50"]} style={{ width: "auto" }} />
97
+ </View>
98
+ ```
99
+
100
+ **Don't** — A stacked block label towers over the small control and breaks the dense row it belongs in.
101
+
102
+ ```tsx
103
+ <Select small label="Rows per page" value="10" options={["10", "25", "50"]} style={{ maxWidth: 200 }} />
104
+ ```
105
+
106
+ ### default
107
+
108
+ **Do** — Match the default select to sibling inputs at the same height so the form row lines up.
109
+
110
+ ```tsx
111
+ <View style={{ flexDirection: "row", alignItems: "flex-end", gap: 12, maxWidth: 420 }}>
112
+ <View style={{ flexGrow: 1, flexShrink: 1, flexBasis: "0%" }}>
113
+ <Text style={{ marginBottom: 6, fontSize: 14, lineHeight: 20, fontWeight: "500", color: tokens.foreground }}>City</Text>
114
+ <Input value="Austin" />
115
+ </View>
116
+ <View style={{ flexGrow: 1, flexShrink: 1, flexBasis: "0%" }}>
117
+ <Text style={{ marginBottom: 6, fontSize: 14, lineHeight: 20, fontWeight: "500", color: tokens.foreground }}>State</Text>
118
+ <Select value="Texas" options={["Texas", "Oregon"]} />
119
+ </View>
120
+ </View>
121
+ ```
122
+
123
+ **Don't** — A default select next to a taller lg input leaves the row baselines misaligned.
124
+
125
+ ```tsx
126
+ <View style={{ flexDirection: "row", alignItems: "flex-end", gap: 12, maxWidth: 420 }}>
127
+ <View style={{ flexGrow: 1, flexShrink: 1, flexBasis: "0%" }}>
128
+ <Text style={{ marginBottom: 6, fontSize: 14, lineHeight: 20, fontWeight: "500", color: tokens.foreground }}>City</Text>
129
+ <Input large value="Austin" />
130
+ </View>
131
+ <View style={{ flexGrow: 1, flexShrink: 1, flexBasis: "0%" }}>
132
+ <Text style={{ marginBottom: 6, fontSize: 14, lineHeight: 20, fontWeight: "500", color: tokens.foreground }}>State</Text>
133
+ <Select value="Texas" options={["Texas", "Oregon"]} />
134
+ </View>
135
+ </View>
136
+ ```
137
+
138
+ ### lg
139
+
140
+ **Do** — Scale the text up with the height so the large select reads as a deliberate, touch-friendly target.
141
+
142
+ ```tsx
143
+ <Select large label="Plan" value="Starter" options={["Starter", "Pro", "Enterprise"]} style={{ maxWidth: 320 }} />
144
+ ```
145
+
146
+ **Don't** — Tiny option text inside a tall control wastes the height and looks like an accidental mismatch.
147
+
148
+ ```tsx
149
+ <View style={{ maxWidth: 320 }}>
150
+ <Text style={{ marginBottom: 6, fontSize: 14, lineHeight: 20, fontWeight: "500", color: tokens.foreground }}>Plan</Text>
151
+ <Pressable style={{ height: 40, flexDirection: "row", alignItems: "center", justifyContent: "space-between", borderRadius: 6, borderWidth: 1, borderColor: tokens.input, backgroundColor: tokens.background, paddingHorizontal: 12 }} accessibilityRole="button">
152
+ <Text style={{ fontSize: 12, lineHeight: 16, color: tokens.foreground }}>Starter</Text>
153
+ <Text style={{ fontSize: 12, lineHeight: 16, color: tokens["muted-foreground"] }}>▾</Text>
154
+ </Pressable>
155
+ </View>
156
+ ```
@@ -0,0 +1,143 @@
1
+ import { useState } from "react";
2
+ import { View, Pressable, Text, useTheme, type StyleProp, type ViewStyle } from "../../style/index.js";
3
+ import { Icon } from "../icon/icon.js";
4
+ import { root, rootLifted, type SelectSkin, type Size } from "./select.styles.js";
5
+
6
+ // Shared Select shell. The structure (the stacked label + the trigger row with
7
+ // its optional leading icon, value/placeholder and trailing chevron, plus the
8
+ // inline open option list with its selectable rows), the public boolean-prop
9
+ // API, the size precedence, the controlled/uncontrolled open state, the
10
+ // select/close handlers, the disabled handling, and accessibility all live here
11
+ // once. A platform file supplies only its skin (trigger shape/fill/border, the
12
+ // chevron glyph, the menu surface, the row tint, where the selection indicator
13
+ // renders, and the press feedback) and calls createSelect.
14
+
15
+ export interface SelectProps {
16
+ /** The currently selected option label. Empty shows the placeholder. */
17
+ value?: string;
18
+ /** The list of selectable option labels. */
19
+ options?: string[];
20
+ /** Optional stacked field label rendered above the trigger. */
21
+ label?: string;
22
+ /** Renders a leading globe glyph inside the trigger, indented so the value clears it. */
23
+ icon?: boolean;
24
+ /** Prompt shown in the trigger when no value is selected. */
25
+ placeholder?: string;
26
+ /**
27
+ * Whether the option list is open. Defaults to true so the open state is
28
+ * visible inline (the docs render it this way; there is no portal/Modal).
29
+ */
30
+ open?: boolean;
31
+ /** Fired when the open state changes (trigger press, select). */
32
+ onOpenChange?: (open: boolean) => void;
33
+ /** Dims the control and blocks interaction. */
34
+ disabled?: boolean;
35
+ /** Called with the chosen option label when a row is pressed. */
36
+ onSelect?: (option: string) => void;
37
+ // Size (pick one; default is the medium field, matching Input's h-9).
38
+ small?: boolean;
39
+ large?: boolean;
40
+ /** Escape hatch for layout/positioning composition (mainly width). */
41
+ style?: StyleProp<ViewStyle>;
42
+ }
43
+
44
+ // Size precedence when more than one is passed: first match wins.
45
+ function sizeOf(p: SelectProps): Size {
46
+ if (p.small) return "small";
47
+ if (p.large) return "large";
48
+ return "default";
49
+ }
50
+
51
+ /** Build a Select component from a platform skin. */
52
+ export function createSelect(skin: SelectSkin) {
53
+ return function Select(props: SelectProps) {
54
+ const {
55
+ value,
56
+ options = [],
57
+ label,
58
+ icon,
59
+ placeholder = "Select an option",
60
+ open: openProp,
61
+ onOpenChange,
62
+ disabled,
63
+ onSelect,
64
+ style,
65
+ } = props;
66
+ const size = sizeOf(props);
67
+ const { tokens } = useTheme();
68
+ // Uncontrolled by default: the trigger opens/closes the list, a select closes
69
+ // it; a controlled `open` prop overrides this.
70
+ const [internalOpen, setInternalOpen] = useState(false);
71
+ const open = openProp ?? internalOpen;
72
+ const setOpen = (next: boolean) => {
73
+ if (openProp === undefined) setInternalOpen(next);
74
+ onOpenChange?.(next);
75
+ };
76
+
77
+ const hasValue = value != null && value !== "";
78
+ const ripple = skin.ripple ? skin.ripple(tokens) : undefined;
79
+
80
+ return (
81
+ <View style={[root, open ? rootLifted : null, style]}>
82
+ {label != null && label !== "" ? (
83
+ <Text style={skin.label(tokens, size)}>{label}</Text>
84
+ ) : null}
85
+ <Pressable
86
+ style={({ pressed }) => [
87
+ skin.trigger(tokens, size, open),
88
+ disabled ? { opacity: skin.disabledOpacity } : null,
89
+ skin.pressedOpacity != null && pressed ? { opacity: skin.pressedOpacity } : null,
90
+ ]}
91
+ disabled={disabled}
92
+ onPress={() => setOpen(!open)}
93
+ android_ripple={ripple}
94
+ accessibilityRole="button"
95
+ >
96
+ <View style={skin.triggerValue}>
97
+ {icon ? <Icon globe muted size={14} /> : null}
98
+ <Text style={skin.valueText(tokens, size, hasValue)}>{hasValue ? value : placeholder}</Text>
99
+ </View>
100
+ <Text style={skin.chevron(tokens, size, open)}>{skin.chevronGlyph}</Text>
101
+ </Pressable>
102
+
103
+ {open ? (
104
+ <View style={skin.panel(tokens)}>
105
+ {options.map((option, i) => {
106
+ const selected = option === value;
107
+ return (
108
+ <Pressable
109
+ key={option}
110
+ style={({ pressed }) => [
111
+ skin.optionRow(tokens, selected),
112
+ // iOS draws a hairline group separator between rows (not above the
113
+ // first); a skin that omits rowSeparator keeps every row borderless.
114
+ i > 0 && skin.rowSeparator ? skin.rowSeparator(tokens) : null,
115
+ // Web/iOS tint the row on press here; Android uses the ripple instead.
116
+ skin.ripple == null && pressed ? skin.optionPressed(tokens) : null,
117
+ ]}
118
+ onPress={() => { onSelect?.(option); setOpen(false); }}
119
+ android_ripple={ripple}
120
+ accessibilityRole="button"
121
+ >
122
+ {skin.selectedSide === "leading" ? (
123
+ <Text style={[skin.indicator(tokens, size), { width: 14 }]}>
124
+ {selected ? "✓" : " "}
125
+ </Text>
126
+ ) : null}
127
+ <Text style={[skin.optionText(tokens, size), { flexShrink: 1 }]}>
128
+ {option}
129
+ </Text>
130
+ {skin.selectedSide === "trailing" ? (
131
+ <Text style={skin.indicator(tokens, size)}>
132
+ {selected ? "✓" : ""}
133
+ </Text>
134
+ ) : null}
135
+ </Pressable>
136
+ );
137
+ })}
138
+ </View>
139
+ ) : null}
140
+ </View>
141
+ );
142
+ };
143
+ }
@@ -0,0 +1,310 @@
1
+ import { StyleSheet, type ViewStyle, type TextStyle } from "react-native";
2
+ import { type ColorTokens, shadow, alpha } from "../../style/index.js";
3
+
4
+ // Co-located Select skins, one per platform, all driven by the brand tokens
5
+ // (passed in from useTheme so they follow light/dark and the glass surface, since
6
+ // tokens.popover is swapped translucent at the theming level). The BRAND survives
7
+ // on every platform (the open/focus accent and the selected-row indicator are the
8
+ // indigo `primary`, never a platform default); only the native SHAPE, sizing,
9
+ // fill, border/underline treatment, and press feedback change per OS:
10
+ // iOS 26 (Liquid Glass pop-up button): a PLAIN hairline-outlined trigger
11
+ // (~12 radius, 1px `border`, `background` fill, NO heavy gray capsule), ~44pt
12
+ // tall, with a trailing chevron-up-down glyph in `primary`; press = opacity
13
+ // dim (~0.8). The menu is the very rounded Liquid Glass popover (26 radius,
14
+ // `popover`, soft shadow, ~17pt rows ~42pt tall, hairline group separators);
15
+ // the selected row shows a LEADING brand checkmark.
16
+ // Android (Material 3 exposed dropdown): a filled trigger (subtle `muted`
17
+ // fill, TOP corners ~4 radius, flat bottom) with a bottom active-indicator
18
+ // underline — 1dp `input` at rest -> 2dp `primary` when open — and a trailing
19
+ // chevron-down; press = android_ripple. The menu is an elevated surface
20
+ // (4 radius, `popover`, soft shadow); pressed rows tint with the ripple
21
+ // (alpha(primary, 0.12) state layer) and the selected row is tinted.
22
+ // Web: the established Canvas look (the current select, lifted verbatim) — a
23
+ // full 1px `input` border, 6 radius, `background` fill, 32/36/40 tall, a
24
+ // trailing ▾ chevron in `muted-foreground`; the menu is a bordered popover
25
+ // (6 radius, `border`, shadow-lg) and the selected row carries the `accent`
26
+ // fill with a LEADING ✓ in the gutter.
27
+
28
+ export type Size = "small" | "default" | "large";
29
+
30
+ // Type scale per size, shared by the label, the trigger value, and the option
31
+ // rows (text-xs / text-sm / text-base). Brand type, not a platform face.
32
+ const TEXT_SIZE: Record<Size, TextStyle> = {
33
+ small: { fontSize: 12, lineHeight: 16 },
34
+ default: { fontSize: 14, lineHeight: 20 },
35
+ large: { fontSize: 16, lineHeight: 24 },
36
+ };
37
+ function textType(size: Size): TextStyle {
38
+ return TEXT_SIZE[size];
39
+ }
40
+
41
+ // The contract a platform skin fulfills. The shell resolves size + the open and
42
+ // hasValue/selected states and passes them in; the skin maps them to RN style
43
+ // objects. `selectedSide` tells the shell where to render the selection
44
+ // indicator (a leading gutter glyph on web, a trailing brand check on iOS), and
45
+ // `selectedGlyph` is the character it draws there.
46
+ export interface SelectSkin {
47
+ /** Type scale per size; label, trigger value, and rows share it so they line up. */
48
+ text: (size: Size) => TextStyle;
49
+ /** The stacked field label above the trigger. */
50
+ label: (t: ColorTokens, size: Size) => TextStyle;
51
+ /** The trigger surface: shape, fill, border/underline; `open` lights the active state. */
52
+ trigger: (t: ColorTokens, size: Size, open: boolean) => ViewStyle;
53
+ /** The leading cluster inside the trigger (optional icon + value/placeholder). */
54
+ triggerValue: ViewStyle;
55
+ /** The trigger value text: foreground when a value is selected, muted otherwise. */
56
+ valueText: (t: ColorTokens, size: Size, hasValue: boolean) => TextStyle;
57
+ /** The trailing chevron glyph. Different character per platform; `open` lets
58
+ * Android tint it with the brand `primary` when the menu is expanded. */
59
+ chevron: (t: ColorTokens, size: Size, open: boolean) => TextStyle;
60
+ /** The chevron character (▾ on web, ⌄ on Android, chevron-up-down on iOS). */
61
+ chevronGlyph: string;
62
+ /** The open option list surface. */
63
+ panel: (t: ColorTokens) => ViewStyle;
64
+ /** An option row. `selected` carries the active tint. */
65
+ optionRow: (t: ColorTokens, selected: boolean) => ViewStyle;
66
+ /**
67
+ * Optional hairline group separator applied to every row after the first, so
68
+ * the menu reads as iOS's separated item groups. Skins that omit it (web,
69
+ * Android) render borderless rows exactly as before.
70
+ */
71
+ rowSeparator?: (t: ColorTokens) => ViewStyle;
72
+ /** The fill applied on press (web/iOS dim via this; Android uses a ripple). */
73
+ optionPressed: (t: ColorTokens) => ViewStyle;
74
+ /** Option row text (label + the indicator glyph). */
75
+ optionText: (t: ColorTokens, size: Size) => TextStyle;
76
+ /** The selected-row indicator glyph (✓) styled in the platform's accent. */
77
+ indicator: (t: ColorTokens, size: Size) => TextStyle;
78
+ /** Which side the selection indicator renders on. */
79
+ selectedSide: "leading" | "trailing";
80
+ /** Opacity applied to the trigger when disabled. */
81
+ disabledOpacity: number;
82
+ /** iOS/web dim the trigger + rows on press; Android uses a ripple instead (null). */
83
+ pressedOpacity: number | null;
84
+ /** Android ripple over the trigger and the rows; null on iOS/web. */
85
+ ripple: ((t: ColorTokens) => { color: string; borderless: boolean }) | null;
86
+ }
87
+
88
+ // The control owns the full width of its slot; `relative` makes it the
89
+ // positioning context for the floating option list. The escape-hatch `style`
90
+ // (mainly width) is applied after this by the shell.
91
+ export const root: ViewStyle = { position: "relative", width: "100%" };
92
+
93
+ // When the list is open, the root is lifted into its own stacking context above
94
+ // sibling content. react-native-web gives every positioned View an implicit
95
+ // stacking context, so the panel's own `zIndex` is scoped INSIDE the `relative`
96
+ // root and cannot rise above a later sibling. Raising the root's zIndex while
97
+ // open lifts the whole control — trigger and panel together — above everything
98
+ // painted after it.
99
+ export const rootLifted: ViewStyle = { zIndex: 50 };
100
+
101
+ // --- shared layout fragments (identical across platforms) -------------------
102
+
103
+ const TRIGGER_ROW: ViewStyle = {
104
+ flexDirection: "row",
105
+ alignItems: "center",
106
+ justifyContent: "space-between",
107
+ };
108
+
109
+ // Every skin's option list floats below the trigger (the root is `relative`) so
110
+ // it overlays the content beneath instead of reflowing the page, mirroring
111
+ // Combobox. The per-skin `marginTop` adds the gap; `maxHeight`/fill/shape stay
112
+ // per platform.
113
+ const PANEL_ANCHOR: ViewStyle = { position: "absolute", top: "100%", left: 0, right: 0, zIndex: 50 };
114
+
115
+ // ---------- Web: the established Canvas look (lifted verbatim) ----------
116
+ // Trigger height per size; mirrors the Input control's footprint (h-8/h-9/h-10).
117
+ const WEB_TRIGGER_BOX: Record<Size, number> = { small: 32, default: 36, large: 40 };
118
+ export const webSkin: SelectSkin = {
119
+ text: textType,
120
+ label: (t, size) => ({ marginBottom: 6, fontWeight: "500", color: t.foreground, ...TEXT_SIZE[size] }),
121
+ trigger: (t, size) => ({
122
+ ...TRIGGER_ROW,
123
+ borderRadius: 6,
124
+ borderWidth: 1,
125
+ borderColor: t.input,
126
+ backgroundColor: t.background,
127
+ paddingHorizontal: 12,
128
+ height: WEB_TRIGGER_BOX[size],
129
+ }),
130
+ triggerValue: { flexDirection: "row", alignItems: "center", gap: 8 },
131
+ valueText: (t, size, hasValue) => ({ color: hasValue ? t.foreground : t["muted-foreground"], ...TEXT_SIZE[size] }),
132
+ chevron: (t, size) => ({ color: t["muted-foreground"], ...TEXT_SIZE[size] }),
133
+ chevronGlyph: "▾",
134
+ panel: (t) => ({
135
+ ...PANEL_ANCHOR,
136
+ marginTop: 4,
137
+ maxHeight: 240,
138
+ borderRadius: 6,
139
+ borderWidth: 1,
140
+ borderColor: t.border,
141
+ backgroundColor: t.popover,
142
+ padding: 4,
143
+ ...shadow("lg"),
144
+ }),
145
+ optionRow: (t, selected) => ({
146
+ flexDirection: "row",
147
+ alignItems: "center",
148
+ gap: 8,
149
+ borderRadius: 2,
150
+ paddingHorizontal: 8,
151
+ paddingVertical: 6,
152
+ ...(selected ? { backgroundColor: t.accent } : null),
153
+ }),
154
+ optionPressed: (t) => ({ backgroundColor: t.accent }),
155
+ optionText: (t, size) => ({ color: t["popover-foreground"], ...TEXT_SIZE[size] }),
156
+ indicator: (t, size) => ({ color: t["popover-foreground"], ...TEXT_SIZE[size] }),
157
+ selectedSide: "leading",
158
+ disabledOpacity: 0.5,
159
+ pressedOpacity: 0.9,
160
+ ripple: null,
161
+ };
162
+
163
+ // ---------- iOS 26 (Liquid Glass) pop-up button + menu ----------
164
+ // Apple's iOS 26 pop-up button is a PLAIN, lightly outlined row (not a heavy
165
+ // filled gray capsule): the value text followed by a trailing chevron-up-down
166
+ // disclosure tinted with the brand `primary`, over the `background` fill with a
167
+ // single hairline `border`, ~44pt tall and only modestly rounded (~12pt). The
168
+ // MENU it opens is the Liquid Glass surface from Apple's kit: a VERY rounded
169
+ // popover (26pt continuous corners), `popover` fill, a soft drop shadow, ~17pt
170
+ // rows that are ~42pt tall (the kit's iPhone "Menu Item, Title" is 198x42), with
171
+ // a hairline group separator between rows. The SELECTED row is marked by a
172
+ // LEADING brand checkmark (the kit's "Menu Item - Selectable" puts the check on
173
+ // the leading edge), in `primary`.
174
+ const IOS_TRIGGER_RADIUS = 12;
175
+ const IOS_MENU_RADIUS = 26;
176
+ const IOS_TRIGGER_BOX: Record<Size, number> = { small: 36, default: 44, large: 50 };
177
+ const IOS_TEXT: Record<Size, TextStyle> = {
178
+ small: { fontSize: 13, lineHeight: 18 },
179
+ default: { fontSize: 15, lineHeight: 20 },
180
+ large: { fontSize: 17, lineHeight: 22 },
181
+ };
182
+ // Menu rows hold the iOS body size (17pt) regardless of the trigger's size axis,
183
+ // matching the kit's fixed menu type.
184
+ const IOS_ROW_TEXT: TextStyle = { fontSize: 17, lineHeight: 22 };
185
+ export const iosSkin: SelectSkin = {
186
+ text: (size) => IOS_TEXT[size],
187
+ label: (t, size) => ({ marginBottom: 6, fontWeight: "600", color: t.foreground, ...IOS_TEXT[size] }),
188
+ // A plain pop-up button: hairline-outlined row over `background`, NOT a filled
189
+ // capsule, so the value + primary chevron read as the iOS 26 pop-up control.
190
+ trigger: (t, size) => ({
191
+ ...TRIGGER_ROW,
192
+ borderRadius: IOS_TRIGGER_RADIUS,
193
+ borderWidth: 1,
194
+ borderColor: t.border,
195
+ backgroundColor: t.background,
196
+ paddingHorizontal: 14,
197
+ height: IOS_TRIGGER_BOX[size],
198
+ }),
199
+ triggerValue: { flexDirection: "row", alignItems: "center", gap: 8 },
200
+ valueText: (t, size, hasValue) => ({ color: hasValue ? t.foreground : t["muted-foreground"], ...IOS_TEXT[size] }),
201
+ // The trailing disclosure is the brand indigo, the iOS pop-up button tint.
202
+ // "⇅" reads as the chevron-up-down pop-up disclosure inline.
203
+ chevron: (t, size) => ({ color: t.primary, fontWeight: "600", ...IOS_TEXT[size] }),
204
+ chevronGlyph: "⇅",
205
+ // The Liquid Glass menu: very rounded (26pt), `popover`, soft shadow. No
206
+ // `overflow: hidden` (it would clip the shadow on iOS, matching how the web and
207
+ // Android panels here keep their drop shadow); the inset hairline separators and
208
+ // the subtle neutral press tint stay clear of the rounded corners.
209
+ panel: (t) => ({
210
+ ...PANEL_ANCHOR,
211
+ marginTop: 8,
212
+ maxHeight: 320,
213
+ borderRadius: IOS_MENU_RADIUS,
214
+ backgroundColor: t.popover,
215
+ paddingVertical: 4,
216
+ ...shadow("lg"),
217
+ }),
218
+ // No row tint at rest on iOS; the selection is shown by the leading check and
219
+ // rows are separated by hairlines (see rowSeparator). ~42pt tall per the kit.
220
+ optionRow: (_t, _selected) => ({
221
+ flexDirection: "row",
222
+ alignItems: "center",
223
+ gap: 10,
224
+ paddingHorizontal: 16,
225
+ paddingVertical: 11,
226
+ minHeight: 42,
227
+ }),
228
+ // Hairline group separator between rows, in `border` (the iOS opaque-separator
229
+ // read), inset to clear the leading text gutter as the kit shows.
230
+ rowSeparator: (t) => ({ borderTopWidth: StyleSheet.hairlineWidth, borderTopColor: t.border }),
231
+ optionPressed: (t) => ({ backgroundColor: t.secondary }),
232
+ optionText: (t, _size) => ({ color: t["popover-foreground"], ...IOS_ROW_TEXT }),
233
+ // The selected-row checkmark is the brand indigo, LEADING-aligned (iOS 26
234
+ // selectable menu marks the leading edge).
235
+ indicator: (t, _size) => ({ color: t.primary, fontWeight: "600", ...IOS_ROW_TEXT }),
236
+ selectedSide: "leading",
237
+ disabledOpacity: 0.5,
238
+ pressedOpacity: 0.8,
239
+ ripple: null,
240
+ };
241
+
242
+ // ---------- Android (Material 3 exposed dropdown): filled field, top radius, active indicator ----------
243
+ // M3 exposed dropdown menu: a filled trigger (subtle `muted` fill ~
244
+ // surface-container-highest), the TOP corners rounded ~4dp and a flat bottom,
245
+ // with a bottom active-indicator underline — 1dp `input` at rest, 2dp `primary`
246
+ // (brand) when open — and a trailing dropdown arrow (chevron-down). The menu is
247
+ // an elevated surface (4dp, `popover`, soft shadow); pressed rows tint with the
248
+ // ripple (alpha(primary, 0.12) state layer) and the selected row is tinted.
249
+ const ANDROID_TOP_RADIUS = 4;
250
+ const ANDROID_TRIGGER_BOX: Record<Size, number> = { small: 48, default: 56, large: 60 };
251
+ const ANDROID_TEXT: Record<Size, TextStyle> = {
252
+ small: { fontSize: 14, lineHeight: 20 },
253
+ default: { fontSize: 16, lineHeight: 24 },
254
+ large: { fontSize: 18, lineHeight: 26 },
255
+ };
256
+ // M3 supporting-text label scale, a notch below the field type.
257
+ const ANDROID_LABEL: Record<Size, TextStyle> = {
258
+ small: { fontSize: 12, lineHeight: 16 },
259
+ default: { fontSize: 12, lineHeight: 16 },
260
+ large: { fontSize: 14, lineHeight: 20 },
261
+ };
262
+ export const androidSkin: SelectSkin = {
263
+ text: (size) => ANDROID_TEXT[size],
264
+ label: (t, size) => ({ marginBottom: 6, fontWeight: "500", color: t.foreground, ...ANDROID_LABEL[size] }),
265
+ trigger: (t, size, open) => ({
266
+ ...TRIGGER_ROW,
267
+ borderTopLeftRadius: ANDROID_TOP_RADIUS,
268
+ borderTopRightRadius: ANDROID_TOP_RADIUS,
269
+ borderBottomLeftRadius: 0,
270
+ borderBottomRightRadius: 0,
271
+ borderBottomWidth: open ? 2 : 1,
272
+ // Rest baseline reads clearly (on-surface-variant ~ muted-foreground) so the M3
273
+ // filled trigger is distinct from the iOS lineless capsule.
274
+ borderBottomColor: open ? t.primary : t["muted-foreground"],
275
+ backgroundColor: t.muted,
276
+ paddingHorizontal: 16,
277
+ height: ANDROID_TRIGGER_BOX[size],
278
+ }),
279
+ triggerValue: { flexDirection: "row", alignItems: "center", gap: 8 },
280
+ valueText: (t, size, hasValue) => ({ color: hasValue ? t.foreground : t["muted-foreground"], ...ANDROID_TEXT[size] }),
281
+ // The trailing dropdown arrow tints with the brand `primary` when open, muted at rest.
282
+ chevron: (t, size, open) => ({ color: open ? t.primary : t["muted-foreground"], ...ANDROID_TEXT[size] }),
283
+ chevronGlyph: "⌄",
284
+ panel: (t) => ({
285
+ ...PANEL_ANCHOR,
286
+ marginTop: 2,
287
+ maxHeight: 280,
288
+ borderRadius: 4,
289
+ backgroundColor: t.popover,
290
+ paddingVertical: 8,
291
+ ...shadow("md"),
292
+ }),
293
+ optionRow: (t, selected) => ({
294
+ flexDirection: "row",
295
+ alignItems: "center",
296
+ gap: 12,
297
+ paddingHorizontal: 16,
298
+ paddingVertical: 12,
299
+ minHeight: 48,
300
+ ...(selected ? { backgroundColor: alpha(t.primary, 0.12) } : null),
301
+ }),
302
+ // The M3 pressed state layer: the brand primary at ~12% alpha (the ripple tint).
303
+ optionPressed: (t) => ({ backgroundColor: alpha(t.primary, 0.12) }),
304
+ optionText: (t, _size) => ({ color: t["popover-foreground"], ...ANDROID_TEXT["small"] }),
305
+ indicator: (t, _size) => ({ color: t.primary, fontWeight: "700", ...ANDROID_TEXT["small"] }),
306
+ selectedSide: "leading",
307
+ disabledOpacity: 0.38, // M3 disabled opacity
308
+ pressedOpacity: null, // Android uses a ripple instead
309
+ ripple: (t) => ({ color: alpha(t.primary, 0.12), borderless: false }),
310
+ };
@@ -0,0 +1,6 @@
1
+ import { createSelect } from "./select.shared.js";
2
+ import { webSkin } from "./select.styles.js";
3
+
4
+ // Web Select (the base; Metro falls back to it on native, web bundlers resolve it).
5
+ export const Select = createSelect(webSkin);
6
+ export type { SelectProps } from "./select.shared.js";