@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.
- package/README.md +75 -65
- package/package.json +11 -5
- package/src/atoms/avatar/avatar.md +185 -0
- package/src/atoms/avatar/avatar.styles.ts +48 -0
- package/src/atoms/avatar/avatar.tsx +99 -0
- package/src/atoms/badge/badge.md +237 -0
- package/src/atoms/badge/badge.styles.ts +79 -0
- package/src/atoms/badge/badge.tsx +86 -0
- package/src/atoms/breadcrumb/breadcrumb.md +233 -0
- package/src/atoms/breadcrumb/breadcrumb.styles.ts +40 -0
- package/src/atoms/breadcrumb/breadcrumb.tsx +130 -0
- package/src/atoms/button/button.android.tsx +6 -0
- package/src/atoms/button/button.ios.tsx +6 -0
- package/src/atoms/button/button.md +184 -0
- package/src/atoms/button/button.shared.tsx +79 -0
- package/src/atoms/button/button.styles.ts +152 -0
- package/src/atoms/button/button.tsx +6 -0
- package/src/atoms/button-group/button-group.android.tsx +6 -0
- package/src/atoms/button-group/button-group.ios.tsx +6 -0
- package/src/atoms/button-group/button-group.md +120 -0
- package/src/atoms/button-group/button-group.shared.tsx +398 -0
- package/src/atoms/button-group/button-group.styles.ts +483 -0
- package/src/atoms/button-group/button-group.tsx +6 -0
- package/src/atoms/checkbox/checkbox.android.tsx +6 -0
- package/src/atoms/checkbox/checkbox.ios.tsx +6 -0
- package/src/atoms/checkbox/checkbox.md +150 -0
- package/src/atoms/checkbox/checkbox.shared.tsx +103 -0
- package/src/atoms/checkbox/checkbox.styles.ts +106 -0
- package/src/atoms/checkbox/checkbox.tsx +6 -0
- package/src/atoms/combobox/combobox.android.tsx +6 -0
- package/src/atoms/combobox/combobox.ios.tsx +6 -0
- package/src/atoms/combobox/combobox.md +213 -0
- package/src/atoms/combobox/combobox.shared.tsx +160 -0
- package/src/atoms/combobox/combobox.styles.ts +270 -0
- package/src/atoms/combobox/combobox.tsx +6 -0
- package/src/atoms/divider/divider.md +140 -0
- package/src/atoms/divider/divider.styles.ts +35 -0
- package/src/atoms/divider/divider.tsx +67 -0
- package/src/atoms/dropdown/dropdown.android.tsx +6 -0
- package/src/atoms/dropdown/dropdown.ios.tsx +6 -0
- package/src/atoms/dropdown/dropdown.md +221 -0
- package/src/atoms/dropdown/dropdown.shared.tsx +190 -0
- package/src/atoms/dropdown/dropdown.styles.ts +233 -0
- package/src/atoms/dropdown/dropdown.tsx +6 -0
- package/src/atoms/icon/icon.md +131 -0
- package/src/atoms/icon/icon.styles.ts +30 -0
- package/src/atoms/icon/icon.tsx +328 -0
- package/src/atoms/index.ts +24 -0
- package/src/atoms/input/input.android.tsx +6 -0
- package/src/atoms/input/input.ios.tsx +6 -0
- package/src/atoms/input/input.md +118 -0
- package/src/atoms/input/input.shared.tsx +203 -0
- package/src/atoms/input/input.styles.ts +286 -0
- package/src/atoms/input/input.tsx +6 -0
- package/src/atoms/kbd/kbd.md +91 -0
- package/src/atoms/kbd/kbd.styles.ts +33 -0
- package/src/atoms/kbd/kbd.tsx +27 -0
- package/src/atoms/listbox/listbox.md +177 -0
- package/src/atoms/listbox/listbox.styles.ts +60 -0
- package/src/atoms/listbox/listbox.tsx +113 -0
- package/src/atoms/pagination/pagination.android.tsx +6 -0
- package/src/atoms/pagination/pagination.ios.tsx +6 -0
- package/src/atoms/pagination/pagination.md +133 -0
- package/src/atoms/pagination/pagination.shared.tsx +289 -0
- package/src/atoms/pagination/pagination.styles.ts +245 -0
- package/src/atoms/pagination/pagination.tsx +6 -0
- package/src/atoms/popover/popover.android.tsx +8 -0
- package/src/atoms/popover/popover.ios.tsx +6 -0
- package/src/atoms/popover/popover.md +87 -0
- package/src/atoms/popover/popover.shared.tsx +124 -0
- package/src/atoms/popover/popover.styles.ts +144 -0
- package/src/atoms/popover/popover.tsx +6 -0
- package/src/atoms/radio/radio.android.tsx +6 -0
- package/src/atoms/radio/radio.ios.tsx +6 -0
- package/src/atoms/radio/radio.md +173 -0
- package/src/atoms/radio/radio.shared.tsx +98 -0
- package/src/atoms/radio/radio.styles.ts +109 -0
- package/src/atoms/radio/radio.tsx +6 -0
- package/src/atoms/select/select.android.tsx +6 -0
- package/src/atoms/select/select.ios.tsx +6 -0
- package/src/atoms/select/select.md +156 -0
- package/src/atoms/select/select.shared.tsx +143 -0
- package/src/atoms/select/select.styles.ts +310 -0
- package/src/atoms/select/select.tsx +6 -0
- package/src/atoms/skeleton/skeleton.md +135 -0
- package/src/atoms/skeleton/skeleton.styles.ts +117 -0
- package/src/atoms/skeleton/skeleton.tsx +145 -0
- package/src/atoms/spinner/spinner.android.tsx +7 -0
- package/src/atoms/spinner/spinner.ios.tsx +7 -0
- package/src/atoms/spinner/spinner.md +94 -0
- package/src/atoms/spinner/spinner.shared.tsx +92 -0
- package/src/atoms/spinner/spinner.styles.tsx +115 -0
- package/src/atoms/spinner/spinner.tsx +7 -0
- package/src/atoms/switch/switch.android.tsx +6 -0
- package/src/atoms/switch/switch.ios.tsx +6 -0
- package/src/atoms/switch/switch.md +91 -0
- package/src/atoms/switch/switch.shared.tsx +97 -0
- package/src/atoms/switch/switch.styles.ts +79 -0
- package/src/atoms/switch/switch.tsx +6 -0
- package/src/atoms/textarea/textarea.android.tsx +6 -0
- package/src/atoms/textarea/textarea.ios.tsx +6 -0
- package/src/atoms/textarea/textarea.md +140 -0
- package/src/atoms/textarea/textarea.shared.tsx +74 -0
- package/src/atoms/textarea/textarea.styles.ts +116 -0
- package/src/atoms/textarea/textarea.tsx +6 -0
- package/src/atoms/tooltip/tooltip.android.tsx +6 -0
- package/src/atoms/tooltip/tooltip.ios.tsx +7 -0
- package/src/atoms/tooltip/tooltip.md +122 -0
- package/src/atoms/tooltip/tooltip.shared.tsx +113 -0
- package/src/atoms/tooltip/tooltip.styles.ts +113 -0
- package/src/atoms/tooltip/tooltip.tsx +6 -0
- package/src/atoms/typography/typography.md +330 -0
- package/src/atoms/typography/typography.styles.ts +95 -0
- package/src/atoms/typography/typography.tsx +76 -0
- package/src/index.ts +12 -2
- package/src/molecules/action-panels/action-panels.md +133 -0
- package/src/molecules/action-panels/action-panels.styles.ts +39 -0
- package/src/molecules/action-panels/action-panels.tsx +113 -0
- package/src/molecules/alert/alert.md +119 -0
- package/src/molecules/alert/alert.styles.ts +88 -0
- package/src/molecules/alert/alert.tsx +74 -0
- package/src/molecules/alert-dialog/alert-dialog.android.tsx +6 -0
- package/src/molecules/alert-dialog/alert-dialog.ios.tsx +6 -0
- package/src/molecules/alert-dialog/alert-dialog.md +177 -0
- package/src/molecules/alert-dialog/alert-dialog.shared.tsx +187 -0
- package/src/molecules/alert-dialog/alert-dialog.styles.ts +248 -0
- package/src/molecules/alert-dialog/alert-dialog.tsx +6 -0
- package/src/molecules/card/card.md +190 -0
- package/src/molecules/card/card.styles.ts +67 -0
- package/src/molecules/card/card.tsx +176 -0
- package/src/molecules/code-block/code-block.md +159 -0
- package/src/molecules/code-block/code-block.styles.ts +167 -0
- package/src/molecules/code-block/code-block.tsx +176 -0
- package/src/molecules/description-lists/description-lists.md +129 -0
- package/src/molecules/description-lists/description-lists.styles.ts +102 -0
- package/src/molecules/description-lists/description-lists.tsx +133 -0
- package/src/molecules/empty-state/empty-state.md +218 -0
- package/src/molecules/empty-state/empty-state.styles.ts +63 -0
- package/src/molecules/empty-state/empty-state.tsx +77 -0
- package/src/molecules/feeds/feeds.md +102 -0
- package/src/molecules/feeds/feeds.styles.ts +120 -0
- package/src/molecules/feeds/feeds.tsx +167 -0
- package/src/molecules/field/field.md +117 -0
- package/src/molecules/field/field.styles.ts +85 -0
- package/src/molecules/field/field.tsx +175 -0
- package/src/molecules/fieldset/fieldset.md +141 -0
- package/src/molecules/fieldset/fieldset.styles.ts +79 -0
- package/src/molecules/fieldset/fieldset.tsx +182 -0
- package/src/molecules/form/form.md +137 -0
- package/src/molecules/form/form.styles.ts +39 -0
- package/src/molecules/form/form.tsx +246 -0
- package/src/molecules/grid-lists/grid-lists.md +114 -0
- package/src/molecules/grid-lists/grid-lists.styles.ts +79 -0
- package/src/molecules/grid-lists/grid-lists.tsx +157 -0
- package/src/molecules/index.ts +16 -0
- package/src/molecules/media-objects/media-objects.md +87 -0
- package/src/molecules/media-objects/media-objects.styles.ts +94 -0
- package/src/molecules/media-objects/media-objects.tsx +128 -0
- package/src/molecules/stacked-lists/stacked-lists.md +116 -0
- package/src/molecules/stacked-lists/stacked-lists.styles.ts +111 -0
- package/src/molecules/stacked-lists/stacked-lists.tsx +195 -0
- package/src/molecules/stats/stats.md +166 -0
- package/src/molecules/stats/stats.styles.ts +91 -0
- package/src/molecules/stats/stats.tsx +88 -0
- package/src/organisms/calendar/calendar.android.tsx +6 -0
- package/src/organisms/calendar/calendar.ios.tsx +6 -0
- package/src/organisms/calendar/calendar.md +114 -0
- package/src/organisms/calendar/calendar.shared.tsx +146 -0
- package/src/organisms/calendar/calendar.styles.ts +315 -0
- package/src/organisms/calendar/calendar.tsx +6 -0
- package/src/organisms/charts/charts.md +326 -0
- package/src/organisms/charts/charts.styles.ts +135 -0
- package/src/organisms/charts/charts.tsx +124 -0
- package/src/organisms/command/command.md +117 -0
- package/src/organisms/command/command.styles.ts +179 -0
- package/src/organisms/command/command.tsx +164 -0
- package/src/organisms/data-table/data-table.md +182 -0
- package/src/organisms/data-table/data-table.styles.ts +103 -0
- package/src/organisms/data-table/data-table.tsx +105 -0
- package/src/organisms/dialog/dialog.android.tsx +6 -0
- package/src/organisms/dialog/dialog.ios.tsx +6 -0
- package/src/organisms/dialog/dialog.md +271 -0
- package/src/organisms/dialog/dialog.shared.tsx +230 -0
- package/src/organisms/dialog/dialog.styles.ts +272 -0
- package/src/organisms/dialog/dialog.tsx +6 -0
- package/src/organisms/filter-panel/filter-panel.md +116 -0
- package/src/organisms/filter-panel/filter-panel.styles.ts +83 -0
- package/src/organisms/filter-panel/filter-panel.tsx +91 -0
- package/src/organisms/index.ts +13 -0
- package/src/organisms/navbars/navbars.android.tsx +6 -0
- package/src/organisms/navbars/navbars.ios.tsx +6 -0
- package/src/organisms/navbars/navbars.md +144 -0
- package/src/organisms/navbars/navbars.shared.tsx +137 -0
- package/src/organisms/navbars/navbars.styles.ts +251 -0
- package/src/organisms/navbars/navbars.tsx +6 -0
- package/src/organisms/overlays/overlays.android.tsx +6 -0
- package/src/organisms/overlays/overlays.ios.tsx +6 -0
- package/src/organisms/overlays/overlays.md +123 -0
- package/src/organisms/overlays/overlays.shared.tsx +175 -0
- package/src/organisms/overlays/overlays.styles.ts +309 -0
- package/src/organisms/overlays/overlays.tsx +6 -0
- package/src/organisms/row-menu/row-menu.android.tsx +6 -0
- package/src/organisms/row-menu/row-menu.ios.tsx +6 -0
- package/src/organisms/row-menu/row-menu.md +102 -0
- package/src/organisms/row-menu/row-menu.shared.tsx +105 -0
- package/src/organisms/row-menu/row-menu.styles.ts +262 -0
- package/src/organisms/row-menu/row-menu.tsx +6 -0
- package/src/organisms/sidebar/sidebar.android.tsx +6 -0
- package/src/organisms/sidebar/sidebar.ios.tsx +6 -0
- package/src/organisms/sidebar/sidebar.md +188 -0
- package/src/organisms/sidebar/sidebar.shared.tsx +167 -0
- package/src/organisms/sidebar/sidebar.styles.ts +262 -0
- package/src/organisms/sidebar/sidebar.tsx +6 -0
- package/src/organisms/stepper/stepper.android.tsx +6 -0
- package/src/organisms/stepper/stepper.ios.tsx +6 -0
- package/src/organisms/stepper/stepper.md +150 -0
- package/src/organisms/stepper/stepper.shared.tsx +158 -0
- package/src/organisms/stepper/stepper.styles.ts +280 -0
- package/src/organisms/stepper/stepper.tsx +6 -0
- package/src/organisms/tabs/tabs.android.tsx +6 -0
- package/src/organisms/tabs/tabs.ios.tsx +6 -0
- package/src/organisms/tabs/tabs.md +127 -0
- package/src/organisms/tabs/tabs.shared.tsx +281 -0
- package/src/organisms/tabs/tabs.styles.ts +398 -0
- package/src/organisms/tabs/tabs.tsx +6 -0
- package/src/style/color.ts +17 -0
- package/src/style/index.ts +14 -0
- package/src/style/primitives.ts +26 -0
- package/src/style/responsive.ts +45 -0
- package/src/style/shadow.ts +21 -0
- package/src/style/theme.tsx +56 -0
- package/src/style/tokens.ts +487 -0
- package/styles/canvas.css +127 -74
- package/tsconfig.json +4 -2
- package/src/cn.ts +0 -3
- package/styles/atoms/avatar.css +0 -22
- package/styles/atoms/badge.css +0 -83
- package/styles/atoms/breadcrumb.css +0 -35
- package/styles/atoms/button-group.css +0 -23
- package/styles/atoms/button.css +0 -107
- package/styles/atoms/checkbox.css +0 -55
- package/styles/atoms/combobox.css +0 -76
- package/styles/atoms/dropdown.css +0 -54
- package/styles/atoms/icon.css +0 -8
- package/styles/atoms/input-group.css +0 -45
- package/styles/atoms/input.css +0 -56
- package/styles/atoms/kbd.css +0 -15
- package/styles/atoms/pagination.css +0 -48
- package/styles/atoms/popover.css +0 -14
- package/styles/atoms/radio.css +0 -28
- package/styles/atoms/select.css +0 -57
- package/styles/atoms/separator.css +0 -32
- package/styles/atoms/skeleton.css +0 -32
- package/styles/atoms/spinner.css +0 -26
- package/styles/atoms/switch.css +0 -45
- package/styles/atoms/textarea.css +0 -31
- package/styles/atoms/tooltip.css +0 -53
- package/styles/atoms/typography.css +0 -105
- package/styles/base.css +0 -17
- package/styles/molecules/alert.css +0 -66
- package/styles/molecules/card.css +0 -58
- package/styles/molecules/code-block.css +0 -18
- package/styles/molecules/empty-state.css +0 -17
- package/styles/molecules/field.css +0 -27
- package/styles/molecules/form.css +0 -27
- package/styles/molecules/page-header.css +0 -52
- package/styles/molecules/section-card.css +0 -49
- package/styles/molecules/stat-card.css +0 -71
- package/styles/molecules/toast.css +0 -95
- package/styles/organisms/app-shell.css +0 -46
- package/styles/organisms/calendar.css +0 -73
- package/styles/organisms/command.css +0 -95
- package/styles/organisms/data-table.css +0 -142
- package/styles/organisms/dialog.css +0 -72
- package/styles/organisms/filter-panel.css +0 -58
- package/styles/organisms/row-menu.css +0 -69
- package/styles/organisms/sheet.css +0 -70
- package/styles/organisms/sidebar.css +0 -146
- package/styles/organisms/stepper.css +0 -63
- package/styles/organisms/tabs.css +0 -40
- package/styles/organisms/topbar.css +0 -24
- package/styles/patterns/backdrops.css +0 -35
- package/styles/patterns/density.css +0 -66
- package/styles/patterns/focus.css +0 -22
- package/styles/patterns/glass.css +0 -85
- package/styles/patterns/high-contrast.css +0 -70
- package/styles/patterns/reduced-motion.css +0 -12
- package/styles/patterns/scrollbar.css +0 -10
- package/styles/reset.css +0 -89
- package/styles/tokens/colors.css +0 -108
- package/styles/tokens/motion.css +0 -33
- package/styles/tokens/radius.css +0 -10
- package/styles/tokens/shadows.css +0 -35
- package/styles/tokens/spacing.css +0 -19
- package/styles/tokens/typography.css +0 -6
- package/styles/tokens/z-index.css +0 -12
- package/styles/utilities/display.css +0 -66
- package/styles/utilities/flexbox.css +0 -240
- package/styles/utilities/gap.css +0 -288
- package/styles/utilities/grid.css +0 -138
- package/styles/utilities/position.css +0 -78
- 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";
|