@olympusoss/canvas 4.0.0 → 5.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +108 -0
- package/package.json +14 -3
- 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/src/theme.ts +21 -0
- package/styles/canvas.css +128 -67
- package/tsconfig.json +4 -2
- package/src/cn.ts +0 -3
- package/styles/base.css +0 -17
- package/styles/components/alert.css +0 -66
- package/styles/components/app-shell.css +0 -46
- package/styles/components/avatar.css +0 -15
- package/styles/components/badge.css +0 -83
- package/styles/components/breadcrumb.css +0 -35
- package/styles/components/button-group.css +0 -23
- package/styles/components/button.css +0 -107
- package/styles/components/calendar.css +0 -73
- package/styles/components/card.css +0 -58
- package/styles/components/checkbox.css +0 -55
- package/styles/components/code-block.css +0 -18
- package/styles/components/combobox.css +0 -75
- package/styles/components/command.css +0 -94
- package/styles/components/data-table.css +0 -142
- package/styles/components/dialog.css +0 -72
- package/styles/components/dropdown.css +0 -54
- package/styles/components/empty-state.css +0 -17
- package/styles/components/field.css +0 -27
- package/styles/components/filter-panel.css +0 -58
- package/styles/components/form.css +0 -27
- package/styles/components/icon.css +0 -8
- package/styles/components/input-group.css +0 -45
- package/styles/components/input.css +0 -56
- package/styles/components/kbd.css +0 -15
- package/styles/components/page-header.css +0 -52
- package/styles/components/pagination.css +0 -48
- package/styles/components/popover.css +0 -14
- package/styles/components/radio.css +0 -28
- package/styles/components/row-menu.css +0 -69
- package/styles/components/section-card.css +0 -49
- package/styles/components/select.css +0 -57
- package/styles/components/separator.css +0 -32
- package/styles/components/sheet.css +0 -70
- package/styles/components/sidebar.css +0 -146
- package/styles/components/skeleton.css +0 -32
- package/styles/components/spinner.css +0 -26
- package/styles/components/stat-card.css +0 -71
- package/styles/components/stepper.css +0 -63
- package/styles/components/switch.css +0 -45
- package/styles/components/tabs.css +0 -40
- package/styles/components/textarea.css +0 -31
- package/styles/components/toast.css +0 -95
- package/styles/components/tooltip.css +0 -53
- package/styles/components/topbar.css +0 -24
- package/styles/components/typography.css +0 -105
- package/styles/patterns/backdrops.css +0 -35
- package/styles/patterns/density.css +0 -66
- package/styles/patterns/focus.css +0 -38
- 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 -106
- 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
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
# Dropdowns
|
|
2
|
+
|
|
3
|
+
Floating menus triggered by a button: actions, options, navigation.
|
|
4
|
+
|
|
5
|
+
## Usage
|
|
6
|
+
|
|
7
|
+
```tsx
|
|
8
|
+
<Dropdown
|
|
9
|
+
trigger="Actions"
|
|
10
|
+
items={[
|
|
11
|
+
{ label: "Edit profile", icon: "✎" },
|
|
12
|
+
{ label: "Duplicate", icon: "⧉" },
|
|
13
|
+
{ label: "Settings", icon: "⚙" }
|
|
14
|
+
]}
|
|
15
|
+
/>
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## Variants
|
|
19
|
+
|
|
20
|
+
### Section label
|
|
21
|
+
|
|
22
|
+
```tsx
|
|
23
|
+
<Dropdown
|
|
24
|
+
trigger="Actions"
|
|
25
|
+
label="Actions"
|
|
26
|
+
items={[
|
|
27
|
+
{ label: "Edit profile", icon: "✎" },
|
|
28
|
+
{ label: "Duplicate", icon: "⧉" },
|
|
29
|
+
{ label: "Settings", icon: "⚙" }
|
|
30
|
+
]}
|
|
31
|
+
/>
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
### Keyboard shortcuts
|
|
35
|
+
|
|
36
|
+
```tsx
|
|
37
|
+
<Dropdown
|
|
38
|
+
trigger="Actions"
|
|
39
|
+
items={[
|
|
40
|
+
{ label: "Edit profile", icon: "✎", shortcut: "⌘E" },
|
|
41
|
+
{ label: "Duplicate", icon: "⧉", shortcut: "⌘D" },
|
|
42
|
+
{ label: "Settings", icon: "⚙", shortcut: "⌘," }
|
|
43
|
+
]}
|
|
44
|
+
/>
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
### Disabled item
|
|
48
|
+
|
|
49
|
+
```tsx
|
|
50
|
+
<Dropdown
|
|
51
|
+
trigger="Actions"
|
|
52
|
+
items={[
|
|
53
|
+
{ label: "Edit profile", icon: "✎" },
|
|
54
|
+
{ label: "Duplicate", icon: "⧉" },
|
|
55
|
+
{ label: "Settings", icon: "⚙" },
|
|
56
|
+
{ label: "Archive", icon: "📦", disabled: true }
|
|
57
|
+
]}
|
|
58
|
+
/>
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
### Destructive item
|
|
62
|
+
|
|
63
|
+
```tsx
|
|
64
|
+
<Dropdown
|
|
65
|
+
trigger="Actions"
|
|
66
|
+
items={[
|
|
67
|
+
{ label: "Edit profile", icon: "✎" },
|
|
68
|
+
{ label: "Duplicate", icon: "⧉" },
|
|
69
|
+
{ label: "Settings", icon: "⚙" },
|
|
70
|
+
{ label: "Delete…", icon: "🗑", destructive: true, separatorBefore: true }
|
|
71
|
+
]}
|
|
72
|
+
/>
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
## Do & Don't
|
|
76
|
+
|
|
77
|
+
### Trigger
|
|
78
|
+
|
|
79
|
+
**Do** — Click Actions to open; click outside to dismiss.
|
|
80
|
+
|
|
81
|
+
```tsx
|
|
82
|
+
<Dropdown trigger="Actions" items={[
|
|
83
|
+
{ label: "Edit profile" },
|
|
84
|
+
{ label: "Duplicate" },
|
|
85
|
+
{ label: "Settings" }
|
|
86
|
+
]} />
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
**Don't** — Always open: it clutters the page and there's no way to dismiss it.
|
|
90
|
+
|
|
91
|
+
```tsx
|
|
92
|
+
<Dropdown trigger="Actions" open items={[
|
|
93
|
+
{ label: "Edit profile" },
|
|
94
|
+
{ label: "Duplicate" },
|
|
95
|
+
{ label: "Settings" }
|
|
96
|
+
]} />
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
### Sectioning
|
|
100
|
+
|
|
101
|
+
**Do** — Click an item: group related actions under labels with a separator.
|
|
102
|
+
|
|
103
|
+
```tsx
|
|
104
|
+
<Dropdown trigger="Actions" label="Create" items={[
|
|
105
|
+
{ label: "New file" },
|
|
106
|
+
{ label: "New folder" },
|
|
107
|
+
{ label: "Upload" },
|
|
108
|
+
{ label: "Rename", separatorBefore: true },
|
|
109
|
+
{ label: "Move to…" },
|
|
110
|
+
{ label: "Download" }
|
|
111
|
+
]} />
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
**Don't** — Click an item: a long, flat menu of eight actions is hard to scan.
|
|
115
|
+
|
|
116
|
+
```tsx
|
|
117
|
+
<Dropdown trigger="Actions" items={[
|
|
118
|
+
{ label: "New file" },
|
|
119
|
+
{ label: "New folder" },
|
|
120
|
+
{ label: "Upload" },
|
|
121
|
+
{ label: "Rename" },
|
|
122
|
+
{ label: "Duplicate" },
|
|
123
|
+
{ label: "Move to…" },
|
|
124
|
+
{ label: "Download" },
|
|
125
|
+
{ label: "Delete" }
|
|
126
|
+
]} />
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
### Leading icons
|
|
130
|
+
|
|
131
|
+
**Do** — Click an item: give every row a leading icon so labels share one start column.
|
|
132
|
+
|
|
133
|
+
```tsx
|
|
134
|
+
<Dropdown trigger="Actions" items={[
|
|
135
|
+
{ label: "Edit", icon: "✎" },
|
|
136
|
+
{ label: "Duplicate", icon: "⧉" },
|
|
137
|
+
{ label: "Settings", icon: "⚙" }
|
|
138
|
+
]} />
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
**Don't** — Click an item: icons on some rows but not others leave labels misaligned and the column ragged.
|
|
142
|
+
|
|
143
|
+
```tsx
|
|
144
|
+
<Dropdown trigger="Actions" items={[
|
|
145
|
+
{ label: "Edit", icon: "✎" },
|
|
146
|
+
{ label: "Duplicate" },
|
|
147
|
+
{ label: "Settings", icon: "⚙" }
|
|
148
|
+
]} />
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
### Keyboard shortcuts
|
|
152
|
+
|
|
153
|
+
**Do** — Click an item: push shortcuts to a muted, right-aligned column so the eye can scan them.
|
|
154
|
+
|
|
155
|
+
```tsx
|
|
156
|
+
<Dropdown trigger="Actions" items={[
|
|
157
|
+
{ label: "Edit profile", shortcut: "⌘E" },
|
|
158
|
+
{ label: "Duplicate", shortcut: "⌘D" },
|
|
159
|
+
{ label: "Settings", shortcut: "⌘," }
|
|
160
|
+
]} />
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
**Don't** — Click an item: hints inline after the label crowd the text and never line up into a readable column.
|
|
164
|
+
|
|
165
|
+
```tsx
|
|
166
|
+
<Dropdown trigger="Actions" items={[
|
|
167
|
+
{ label: "Edit profile ⌘E" },
|
|
168
|
+
{ label: "Duplicate ⌘D" },
|
|
169
|
+
{ label: "Settings ⌘," }
|
|
170
|
+
]} />
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
### Disabled item
|
|
174
|
+
|
|
175
|
+
**Do** — Click Archive: nothing happens; a real disabled item doesn't respond.
|
|
176
|
+
|
|
177
|
+
```tsx
|
|
178
|
+
<Dropdown trigger="Actions" items={[
|
|
179
|
+
{ label: "Edit" },
|
|
180
|
+
{ label: "Archive", disabled: true },
|
|
181
|
+
{ label: "Duplicate" }
|
|
182
|
+
]} />
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
**Don't** — Click Archive: it looks disabled but still fires, a greyed item that works is a trap.
|
|
186
|
+
|
|
187
|
+
```tsx
|
|
188
|
+
<View style={{ alignSelf: "flex-start", borderRadius: 6, borderWidth: 1, borderColor: tokens.border, backgroundColor: tokens.popover, padding: 4, ...shadow("lg"), minWidth: 200 }}>
|
|
189
|
+
<Pressable style={({ pressed }) => [{ flexDirection: "row", alignItems: "center", gap: 8, borderRadius: 2, paddingHorizontal: 8, paddingVertical: 6 }, pressed ? { backgroundColor: tokens.accent } : null]}>
|
|
190
|
+
<Text style={{ fontSize: 14, lineHeight: 20, color: tokens["popover-foreground"] }}>Edit</Text>
|
|
191
|
+
</Pressable>
|
|
192
|
+
<Pressable style={({ pressed }) => [{ flexDirection: "row", alignItems: "center", gap: 8, borderRadius: 2, paddingHorizontal: 8, paddingVertical: 6, opacity: 0.5 }, pressed ? { backgroundColor: tokens.accent } : null]}>
|
|
193
|
+
<Text style={{ fontSize: 14, lineHeight: 20, color: tokens["popover-foreground"] }}>Archive</Text>
|
|
194
|
+
</Pressable>
|
|
195
|
+
<Pressable style={({ pressed }) => [{ flexDirection: "row", alignItems: "center", gap: 8, borderRadius: 2, paddingHorizontal: 8, paddingVertical: 6 }, pressed ? { backgroundColor: tokens.accent } : null]}>
|
|
196
|
+
<Text style={{ fontSize: 14, lineHeight: 20, color: tokens["popover-foreground"] }}>Duplicate</Text>
|
|
197
|
+
</Pressable>
|
|
198
|
+
</View>
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
### Destructive item
|
|
202
|
+
|
|
203
|
+
**Do** — Click an item: separate destructive actions with a divider, color them, and place them last.
|
|
204
|
+
|
|
205
|
+
```tsx
|
|
206
|
+
<Dropdown trigger="Actions" items={[
|
|
207
|
+
{ label: "Edit" },
|
|
208
|
+
{ label: "Duplicate" },
|
|
209
|
+
{ label: "Delete", destructive: true, separatorBefore: true }
|
|
210
|
+
]} />
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
**Don't** — Click an item: a destructive action wedged between routine ones invites a costly misclick.
|
|
214
|
+
|
|
215
|
+
```tsx
|
|
216
|
+
<Dropdown trigger="Actions" items={[
|
|
217
|
+
{ label: "Edit" },
|
|
218
|
+
{ label: "Delete" },
|
|
219
|
+
{ label: "Duplicate" }
|
|
220
|
+
]} />
|
|
221
|
+
```
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
import { useState, type ReactNode } from "react";
|
|
2
|
+
import { Platform } from "react-native";
|
|
3
|
+
import { View, Pressable, Text, useTheme, type StyleProp, type ViewStyle } from "../../style/index.js";
|
|
4
|
+
import { Button } from "../button/button.js";
|
|
5
|
+
import { wrapper, wrapperLifted, customTrigger, type DropdownSkin } from "./dropdown.styles.js";
|
|
6
|
+
|
|
7
|
+
// Shared Dropdown shell. The structure (the trigger plus a floating menu of
|
|
8
|
+
// action rows, each with an optional leading icon glyph, a label, and an
|
|
9
|
+
// optional trailing keyboard shortcut, with hairline separators between groups
|
|
10
|
+
// and red-tinted destructive rows), the public boolean-prop API, the
|
|
11
|
+
// controlled/uncontrolled open state, the trigger/select close handlers, the
|
|
12
|
+
// web-only dismiss backdrop, the disabled handling, and accessibility all live
|
|
13
|
+
// here once. A platform file supplies only its skin (the menu card shape/fill/
|
|
14
|
+
// border, the separators, the row sizing, and the press feedback) and calls
|
|
15
|
+
// createDropdown.
|
|
16
|
+
//
|
|
17
|
+
// The trigger defaults to an outline button labelled by `trigger`. Pass
|
|
18
|
+
// `children` to supply a CUSTOM trigger instead (e.g. an avatar account chip in
|
|
19
|
+
// a topbar): the children render in place of the button, inside a Pressable that
|
|
20
|
+
// toggles the menu. Either way the menu rows still come from `items`.
|
|
21
|
+
//
|
|
22
|
+
// Overlay note: the open menu renders as a floating card positioned absolutely
|
|
23
|
+
// below the trigger (the wrapper is `relative`), so it overflows its container
|
|
24
|
+
// (e.g. a docs card or the playground stage) instead of growing it, with no
|
|
25
|
+
// portal/Modal. On the web, an UNCONTROLLED menu also lays down a transparent
|
|
26
|
+
// full-viewport backdrop so a press anywhere off the menu dismisses it; a
|
|
27
|
+
// controlled `open` menu and native get no backdrop (native would use a Modal).
|
|
28
|
+
//
|
|
29
|
+
// There are no visual style axes on the menu itself, so there is no boolean-prop
|
|
30
|
+
// precedence to resolve; the per-item `destructive` flag is the only variant and
|
|
31
|
+
// it is scoped to its own row.
|
|
32
|
+
|
|
33
|
+
export interface DropdownItem {
|
|
34
|
+
label: string;
|
|
35
|
+
/** Optional leading glyph rendered before the label (a single character). */
|
|
36
|
+
icon?: string;
|
|
37
|
+
/** Optional trailing keyboard shortcut, right-aligned and muted. */
|
|
38
|
+
shortcut?: string;
|
|
39
|
+
/** Red-tinted row for destructive actions (e.g. Delete). */
|
|
40
|
+
destructive?: boolean;
|
|
41
|
+
/** Dimmed, non-interactive row: skips onSelect and renders at reduced opacity. */
|
|
42
|
+
disabled?: boolean;
|
|
43
|
+
/** Draw a hairline separator above this row to start a new group. */
|
|
44
|
+
separatorBefore?: boolean;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export interface DropdownProps {
|
|
48
|
+
/** Label for the default outline trigger button. Omit when supplying a custom
|
|
49
|
+
* trigger via `children`. */
|
|
50
|
+
trigger?: string;
|
|
51
|
+
/** A custom trigger rendered in place of the default outline button, e.g. an
|
|
52
|
+
* avatar account chip. It is wrapped in a Pressable that toggles the menu;
|
|
53
|
+
* the menu itself still comes from `items`. */
|
|
54
|
+
children?: ReactNode;
|
|
55
|
+
/** Optional muted section heading rendered above the rows (e.g. "Actions"). */
|
|
56
|
+
label?: string;
|
|
57
|
+
/** The menu rows, top to bottom. */
|
|
58
|
+
items: DropdownItem[];
|
|
59
|
+
/** Controlled open state. Omit for uncontrolled (the trigger opens/closes it). */
|
|
60
|
+
open?: boolean;
|
|
61
|
+
/** Fired when the open state changes (trigger press, select, etc.). */
|
|
62
|
+
onOpenChange?: (open: boolean) => void;
|
|
63
|
+
/** Fired with the selected item and its index when a row is pressed. */
|
|
64
|
+
onSelect?: (item: DropdownItem, index: number) => void;
|
|
65
|
+
/** Escape hatch for layout/positioning composition (mainly width). */
|
|
66
|
+
style?: StyleProp<ViewStyle>;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// The menu's width floor. A menu under a small trigger (e.g. an outline button)
|
|
70
|
+
// stays at least this wide; a wider trigger (an account chip) sets the width.
|
|
71
|
+
const MENU_MIN_WIDTH = 200;
|
|
72
|
+
|
|
73
|
+
// A transparent full-viewport layer behind the open menu (web only): a press off
|
|
74
|
+
// the menu dismisses it. `position: fixed` is not in RN's ViewStyle type but
|
|
75
|
+
// react-native-web honors it; cast through unknown. zIndex sits below the menu's
|
|
76
|
+
// z-50 so the menu and its items stay clickable above the backdrop.
|
|
77
|
+
const DISMISS_BACKDROP = {
|
|
78
|
+
position: "fixed",
|
|
79
|
+
top: 0,
|
|
80
|
+
right: 0,
|
|
81
|
+
bottom: 0,
|
|
82
|
+
left: 0,
|
|
83
|
+
zIndex: 40,
|
|
84
|
+
} as unknown as StyleProp<ViewStyle>;
|
|
85
|
+
|
|
86
|
+
// The menu card is positioned absolutely below the trigger on every platform; the
|
|
87
|
+
// skin owns the card's shape/fill/shadow, this owns the anchoring.
|
|
88
|
+
const MENU_ANCHOR: ViewStyle = { position: "absolute", top: "100%", left: 0, zIndex: 50, marginTop: 4 };
|
|
89
|
+
|
|
90
|
+
/** Build a Dropdown component from a platform skin. */
|
|
91
|
+
export function createDropdown(skin: DropdownSkin) {
|
|
92
|
+
return function Dropdown(props: DropdownProps) {
|
|
93
|
+
const { trigger, children, label, items, open: openProp, onOpenChange, onSelect, style } = props;
|
|
94
|
+
const { tokens, dark } = useTheme();
|
|
95
|
+
// Uncontrolled by default (Headless-UI style): the trigger opens/closes the
|
|
96
|
+
// menu and a select closes it; a controlled `open` prop overrides this.
|
|
97
|
+
const [internalOpen, setInternalOpen] = useState(false);
|
|
98
|
+
const open = openProp ?? internalOpen;
|
|
99
|
+
const setOpen = (next: boolean) => {
|
|
100
|
+
if (openProp === undefined) setInternalOpen(next);
|
|
101
|
+
onOpenChange?.(next);
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
// Match the menu width to the trigger (and let longer rows grow past it), so a
|
|
105
|
+
// wide trigger like a topbar account chip gets a menu of the same width.
|
|
106
|
+
// Measured via the wrapper's layout; the menu is absolute, so it never feeds
|
|
107
|
+
// back into this width.
|
|
108
|
+
const [triggerWidth, setTriggerWidth] = useState(0);
|
|
109
|
+
|
|
110
|
+
const ripple = skin.ripple ? skin.ripple(tokens) : undefined;
|
|
111
|
+
|
|
112
|
+
return (
|
|
113
|
+
// self-start keeps the trigger from stretching; relative anchors the menu.
|
|
114
|
+
<View
|
|
115
|
+
style={[wrapper, open ? wrapperLifted : null, style]}
|
|
116
|
+
onLayout={(e) => setTriggerWidth(e.nativeEvent.layout.width)}
|
|
117
|
+
>
|
|
118
|
+
{children != null ? (
|
|
119
|
+
<Pressable
|
|
120
|
+
style={customTrigger}
|
|
121
|
+
onPress={() => setOpen(!open)}
|
|
122
|
+
accessibilityRole="button"
|
|
123
|
+
accessibilityState={{ expanded: open }}
|
|
124
|
+
>
|
|
125
|
+
{children}
|
|
126
|
+
</Pressable>
|
|
127
|
+
) : (
|
|
128
|
+
<Button outline small onPress={() => setOpen(!open)}>
|
|
129
|
+
{trigger}
|
|
130
|
+
</Button>
|
|
131
|
+
)}
|
|
132
|
+
|
|
133
|
+
{open && openProp === undefined && Platform.OS === "web" ? (
|
|
134
|
+
<Pressable accessible={false} onPress={() => setOpen(false)} style={DISMISS_BACKDROP} />
|
|
135
|
+
) : null}
|
|
136
|
+
|
|
137
|
+
{open ? (
|
|
138
|
+
<View
|
|
139
|
+
style={[
|
|
140
|
+
MENU_ANCHOR,
|
|
141
|
+
skin.menuCard(tokens),
|
|
142
|
+
{ minWidth: Math.max(triggerWidth, MENU_MIN_WIDTH) },
|
|
143
|
+
]}
|
|
144
|
+
>
|
|
145
|
+
{label ? (
|
|
146
|
+
<Text style={skin.menuLabel(tokens)}>
|
|
147
|
+
{label}
|
|
148
|
+
</Text>
|
|
149
|
+
) : null}
|
|
150
|
+
{items.map((item, index) => (
|
|
151
|
+
<View key={`${item.label}-${index}`}>
|
|
152
|
+
{item.separatorBefore && skin.separator ? (
|
|
153
|
+
<View style={skin.separator(tokens)} />
|
|
154
|
+
) : null}
|
|
155
|
+
<Pressable
|
|
156
|
+
style={({ pressed }) => [
|
|
157
|
+
skin.itemRow,
|
|
158
|
+
// iOS/web tint the row on press here; Android uses the ripple instead.
|
|
159
|
+
skin.itemPressed != null && pressed ? skin.itemPressed(tokens) : null,
|
|
160
|
+
skin.pressedOpacity != null && pressed ? { opacity: skin.pressedOpacity } : null,
|
|
161
|
+
item.disabled ? { opacity: skin.disabledOpacity } : null,
|
|
162
|
+
]}
|
|
163
|
+
onPress={item.disabled ? undefined : () => { onSelect?.(item, index); setOpen(false); }}
|
|
164
|
+
disabled={item.disabled}
|
|
165
|
+
android_ripple={ripple}
|
|
166
|
+
accessibilityRole="menuitem"
|
|
167
|
+
accessibilityState={{ disabled: item.disabled }}
|
|
168
|
+
>
|
|
169
|
+
{item.icon ? (
|
|
170
|
+
<Text style={[skin.itemTextType, skin.itemTextColor(tokens, dark, !!item.destructive)]}>
|
|
171
|
+
{item.icon}
|
|
172
|
+
</Text>
|
|
173
|
+
) : null}
|
|
174
|
+
<Text style={[skin.itemTextType, skin.itemTextColor(tokens, dark, !!item.destructive)]}>
|
|
175
|
+
{item.label}
|
|
176
|
+
</Text>
|
|
177
|
+
{item.shortcut ? (
|
|
178
|
+
<Text style={skin.shortcut(tokens)}>
|
|
179
|
+
{item.shortcut}
|
|
180
|
+
</Text>
|
|
181
|
+
) : null}
|
|
182
|
+
</Pressable>
|
|
183
|
+
</View>
|
|
184
|
+
))}
|
|
185
|
+
</View>
|
|
186
|
+
) : null}
|
|
187
|
+
</View>
|
|
188
|
+
);
|
|
189
|
+
};
|
|
190
|
+
}
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
import { type ViewStyle, type TextStyle } from "react-native";
|
|
2
|
+
import { type ColorTokens, palette, shadow, alpha } from "../../style/index.js";
|
|
3
|
+
|
|
4
|
+
// Co-located Dropdown skins, one per platform, all driven by the brand tokens
|
|
5
|
+
// (passed in from useTheme so they follow light/dark and read as glass when
|
|
6
|
+
// tokens.popover is swapped translucent at the theming level). The BRAND
|
|
7
|
+
// survives on every platform (the indigo `primary` press tint on Android, the
|
|
8
|
+
// `destructive` red for destructive rows); only the native SHAPE, sizing, fill,
|
|
9
|
+
// separators, and press feedback change per OS:
|
|
10
|
+
// iOS (iOS 26 / Liquid Glass pull-down menu): a VERY rounded popover panel
|
|
11
|
+
// (~26 radius, `popover` fill, NO border, soft shadow), rows ~44pt tall with
|
|
12
|
+
// ~17pt labels, hairline `border` separators between groups, a destructive
|
|
13
|
+
// row in `destructive` red, an optional trailing SF-style icon; a pressed row
|
|
14
|
+
// tints with a subtle `secondary` highlight (no ripple) at pressedOpacity 0.8.
|
|
15
|
+
// Android (Material 3 menu): an ELEVATED surface (4 radius, `popover`, shadow
|
|
16
|
+
// md, paddingVertical 8), rows ~48dp tall with 14sp labels and a leading
|
|
17
|
+
// icon gutter, an android_ripple (alpha(primary, 0.12) state layer) on rows,
|
|
18
|
+
// and NO separators.
|
|
19
|
+
// Web: the established Canvas look (the current dropdown, lifted verbatim) — a
|
|
20
|
+
// bordered popover card (6 radius, `border`, `popover` fill, shadow-lg,
|
|
21
|
+
// padding 4), rows with rounded-sm px-2 py-1.5 layout, hairline `border`
|
|
22
|
+
// separators, an `accent` pressed/active fill, and `red-600/red-400`
|
|
23
|
+
// destructive rows.
|
|
24
|
+
|
|
25
|
+
// The contract a platform skin fulfills. The shell renders the wrapper, the
|
|
26
|
+
// trigger, the optional backdrop, the menu card, the optional section label, the
|
|
27
|
+
// per-group separators, and the item rows; the skin maps the active platform's
|
|
28
|
+
// shape/fill/sizing/feedback onto each piece.
|
|
29
|
+
export interface DropdownSkin {
|
|
30
|
+
/** The floating menu card: shape, fill, border (or lack of), shadow, padding.
|
|
31
|
+
* The shell supplies the measured minWidth inline. */
|
|
32
|
+
menuCard: (t: ColorTokens) => ViewStyle;
|
|
33
|
+
/** Muted section heading rendered above the rows. */
|
|
34
|
+
menuLabel: (t: ColorTokens) => TextStyle;
|
|
35
|
+
/** A hairline separator above a row that starts a new group. iOS/web draw one;
|
|
36
|
+
* Android (M3) returns null (no dividers). */
|
|
37
|
+
separator: ((t: ColorTokens) => ViewStyle) | null;
|
|
38
|
+
/** An item row layout (the flex-row + gap + radius + padding box). */
|
|
39
|
+
itemRow: ViewStyle;
|
|
40
|
+
/** The fill applied to a pressed row (iOS/web tint here; Android uses a
|
|
41
|
+
* ripple, so this is null). */
|
|
42
|
+
itemPressed: ((t: ColorTokens) => ViewStyle) | null;
|
|
43
|
+
/** Item icon + label type scale. */
|
|
44
|
+
itemTextType: TextStyle;
|
|
45
|
+
/** Item icon + label color; branches on `destructive`. */
|
|
46
|
+
itemTextColor: (t: ColorTokens, dark: boolean, destructive: boolean) => TextStyle;
|
|
47
|
+
/** Trailing keyboard shortcut, right-aligned and muted. */
|
|
48
|
+
shortcut: (t: ColorTokens) => TextStyle;
|
|
49
|
+
/** Opacity applied to a disabled row. */
|
|
50
|
+
disabledOpacity: number;
|
|
51
|
+
/** iOS/web dim a row on press via this; Android uses a ripple instead (null). */
|
|
52
|
+
pressedOpacity: number | null;
|
|
53
|
+
/** Android ripple over the rows; null on iOS/web. */
|
|
54
|
+
ripple: ((t: ColorTokens) => { color: string; borderless: boolean }) | null;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// --- wrapper + trigger (identical across platforms) -------------------------
|
|
58
|
+
|
|
59
|
+
// self-start keeps the trigger from stretching; relative anchors the menu.
|
|
60
|
+
export const wrapper: ViewStyle = { position: "relative", alignSelf: "flex-start" };
|
|
61
|
+
|
|
62
|
+
// When the menu is open, the wrapper is lifted into its own stacking context
|
|
63
|
+
// above sibling content. react-native-web gives every positioned View an
|
|
64
|
+
// implicit stacking context, so the menu's own `zIndex` is scoped INSIDE the
|
|
65
|
+
// `relative` wrapper and cannot rise above a later sibling. Raising the wrapper's
|
|
66
|
+
// zIndex while open lifts the whole control — trigger and menu together — above
|
|
67
|
+
// everything painted after it.
|
|
68
|
+
export const wrapperLifted: ViewStyle = { zIndex: 50 };
|
|
69
|
+
|
|
70
|
+
// The custom-trigger Pressable: keeps the chip from stretching.
|
|
71
|
+
export const customTrigger: ViewStyle = { alignSelf: "flex-start" };
|
|
72
|
+
|
|
73
|
+
// ---------- Web: the established Canvas look (lifted verbatim) ----------
|
|
74
|
+
// The current dropdown: a popover card (rounded-md border bg-popover p-1
|
|
75
|
+
// shadow-lg) positioned absolutely below the trigger; rows with the
|
|
76
|
+
// flex-row items-center gap-2 rounded-sm px-2 py-1.5 layout, hairline
|
|
77
|
+
// my-1 h-px bg-border separators, an active:bg-accent pressed fill, and
|
|
78
|
+
// text-red-600 dark:text-red-400 destructive rows.
|
|
79
|
+
export const webSkin: DropdownSkin = {
|
|
80
|
+
menuCard: (t) => ({
|
|
81
|
+
borderRadius: 6,
|
|
82
|
+
borderWidth: 1,
|
|
83
|
+
borderColor: t.border,
|
|
84
|
+
backgroundColor: t.popover,
|
|
85
|
+
padding: 4,
|
|
86
|
+
...shadow("lg"),
|
|
87
|
+
}),
|
|
88
|
+
menuLabel: (t) => ({
|
|
89
|
+
paddingHorizontal: 8,
|
|
90
|
+
paddingVertical: 6,
|
|
91
|
+
fontSize: 12,
|
|
92
|
+
lineHeight: 16,
|
|
93
|
+
fontWeight: "500",
|
|
94
|
+
color: t["muted-foreground"],
|
|
95
|
+
}),
|
|
96
|
+
separator: (t) => ({ marginTop: 4, marginBottom: 4, height: 1, backgroundColor: t.border }),
|
|
97
|
+
itemRow: {
|
|
98
|
+
flexDirection: "row",
|
|
99
|
+
alignItems: "center",
|
|
100
|
+
gap: 8,
|
|
101
|
+
borderRadius: 2,
|
|
102
|
+
paddingHorizontal: 8,
|
|
103
|
+
paddingVertical: 6,
|
|
104
|
+
},
|
|
105
|
+
itemPressed: (t) => ({ backgroundColor: t.accent }),
|
|
106
|
+
itemTextType: { fontSize: 14, lineHeight: 20 },
|
|
107
|
+
itemTextColor: (t, dark, destructive) => {
|
|
108
|
+
if (destructive) return { color: dark ? palette["red-400"] : palette["red-600"] };
|
|
109
|
+
return { color: t["popover-foreground"] };
|
|
110
|
+
},
|
|
111
|
+
shortcut: (t) => ({
|
|
112
|
+
marginLeft: "auto",
|
|
113
|
+
fontSize: 12,
|
|
114
|
+
lineHeight: 16,
|
|
115
|
+
letterSpacing: 1.6,
|
|
116
|
+
color: t["muted-foreground"],
|
|
117
|
+
}),
|
|
118
|
+
disabledOpacity: 0.5,
|
|
119
|
+
pressedOpacity: null, // web tints the row fill on press, no opacity dim
|
|
120
|
+
ripple: null,
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
// ---------- iOS 26 (Liquid Glass pull-down menu): VERY rounded popover, no border, hairline groups ----------
|
|
124
|
+
// Apple's iOS 26 pull-down/context menu: a very rounded popover panel (~26pt
|
|
125
|
+
// continuous radius, matching the Liquid Glass kit render in
|
|
126
|
+
// docs/public/refs/ios/dropdown/menu-iphone-light.png) over the `popover` fill
|
|
127
|
+
// with NO visible border and a soft shadow; rows ~44pt tall with ~17pt labels,
|
|
128
|
+
// full-bleed hairline `border` separators between groups, a destructive row in
|
|
129
|
+
// the `destructive` red (icon + label), section titles in `muted-foreground`,
|
|
130
|
+
// and an optional trailing SF-style icon (e.g. a submenu chevron). A pressed row
|
|
131
|
+
// tints with a subtle `secondary` highlight (no ripple) at pressedOpacity 0.8.
|
|
132
|
+
// iOS 26 menus are markedly rounder than legacy (~13pt) menus; the larger radius
|
|
133
|
+
// is the headline shape change.
|
|
134
|
+
const IOS_RADIUS = 26;
|
|
135
|
+
export const iosSkin: DropdownSkin = {
|
|
136
|
+
menuCard: (t) => ({
|
|
137
|
+
borderRadius: IOS_RADIUS,
|
|
138
|
+
backgroundColor: t.popover,
|
|
139
|
+
paddingVertical: 6,
|
|
140
|
+
// Clip the pressed-row highlight and full-bleed separators to the heavily
|
|
141
|
+
// rounded panel so they don't poke past the ~26pt corners. iOS still draws
|
|
142
|
+
// the soft shadow outside these bounds.
|
|
143
|
+
overflow: "hidden",
|
|
144
|
+
...shadow("lg"),
|
|
145
|
+
}),
|
|
146
|
+
menuLabel: (t) => ({
|
|
147
|
+
paddingHorizontal: 16,
|
|
148
|
+
paddingVertical: 6,
|
|
149
|
+
fontSize: 13,
|
|
150
|
+
lineHeight: 18,
|
|
151
|
+
fontWeight: "600",
|
|
152
|
+
color: t["muted-foreground"],
|
|
153
|
+
}),
|
|
154
|
+
// Hairline group separators run full-bleed across the panel (iOS 26 groups
|
|
155
|
+
// rows with a thin divider).
|
|
156
|
+
separator: (t) => ({ marginTop: 6, marginBottom: 6, height: 1, backgroundColor: t.border }),
|
|
157
|
+
itemRow: {
|
|
158
|
+
flexDirection: "row",
|
|
159
|
+
alignItems: "center",
|
|
160
|
+
gap: 12,
|
|
161
|
+
paddingHorizontal: 16,
|
|
162
|
+
paddingVertical: 11,
|
|
163
|
+
minHeight: 44,
|
|
164
|
+
},
|
|
165
|
+
// Subtle highlight under a pressed row (no ripple on iOS).
|
|
166
|
+
itemPressed: (t) => ({ backgroundColor: t.secondary }),
|
|
167
|
+
itemTextType: { fontSize: 17, lineHeight: 22 },
|
|
168
|
+
itemTextColor: (t, _dark, destructive) => {
|
|
169
|
+
if (destructive) return { color: t.destructive };
|
|
170
|
+
return { color: t["popover-foreground"] };
|
|
171
|
+
},
|
|
172
|
+
shortcut: (t) => ({
|
|
173
|
+
marginLeft: "auto",
|
|
174
|
+
fontSize: 15,
|
|
175
|
+
lineHeight: 20,
|
|
176
|
+
letterSpacing: 0.5,
|
|
177
|
+
color: t["muted-foreground"],
|
|
178
|
+
}),
|
|
179
|
+
disabledOpacity: 0.5,
|
|
180
|
+
pressedOpacity: 0.8,
|
|
181
|
+
ripple: null,
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
// ---------- Android (Material 3 menu): elevated surface, ripple rows, no dividers ----------
|
|
185
|
+
// M3 dropdown menu: an ELEVATED surface (4dp radius, `popover` fill, soft shadow,
|
|
186
|
+
// 8dp vertical padding); rows ~48dp tall with 14sp labels and a leading icon
|
|
187
|
+
// gutter, an android_ripple (alpha(primary, 0.12) state layer) on each row, and
|
|
188
|
+
// NO separators (M3 menus group with spacing, not dividers).
|
|
189
|
+
const ANDROID_RADIUS = 4;
|
|
190
|
+
export const androidSkin: DropdownSkin = {
|
|
191
|
+
menuCard: (t) => ({
|
|
192
|
+
borderRadius: ANDROID_RADIUS,
|
|
193
|
+
backgroundColor: t.popover,
|
|
194
|
+
paddingVertical: 8,
|
|
195
|
+
...shadow("md"),
|
|
196
|
+
}),
|
|
197
|
+
menuLabel: (t) => ({
|
|
198
|
+
paddingHorizontal: 16,
|
|
199
|
+
paddingVertical: 8,
|
|
200
|
+
fontSize: 12,
|
|
201
|
+
lineHeight: 16,
|
|
202
|
+
fontWeight: "500",
|
|
203
|
+
letterSpacing: 0.5,
|
|
204
|
+
color: t["muted-foreground"],
|
|
205
|
+
}),
|
|
206
|
+
// M3 menus use no dividers between items.
|
|
207
|
+
separator: null,
|
|
208
|
+
itemRow: {
|
|
209
|
+
flexDirection: "row",
|
|
210
|
+
alignItems: "center",
|
|
211
|
+
gap: 12,
|
|
212
|
+
paddingHorizontal: 16,
|
|
213
|
+
paddingVertical: 0,
|
|
214
|
+
minHeight: 48,
|
|
215
|
+
},
|
|
216
|
+
// Android uses the ripple, so no static pressed fill.
|
|
217
|
+
itemPressed: null,
|
|
218
|
+
itemTextType: { fontSize: 14, lineHeight: 20 },
|
|
219
|
+
itemTextColor: (t, _dark, destructive) => {
|
|
220
|
+
if (destructive) return { color: t.destructive };
|
|
221
|
+
return { color: t["popover-foreground"] };
|
|
222
|
+
},
|
|
223
|
+
shortcut: (t) => ({
|
|
224
|
+
marginLeft: "auto",
|
|
225
|
+
fontSize: 12,
|
|
226
|
+
lineHeight: 16,
|
|
227
|
+
letterSpacing: 0.5,
|
|
228
|
+
color: t["muted-foreground"],
|
|
229
|
+
}),
|
|
230
|
+
disabledOpacity: 0.38, // M3 disabled opacity
|
|
231
|
+
pressedOpacity: null, // Android uses a ripple instead
|
|
232
|
+
ripple: (t) => ({ color: alpha(t.primary, 0.12), borderless: false }),
|
|
233
|
+
};
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { createDropdown } from "./dropdown.shared.js";
|
|
2
|
+
import { webSkin } from "./dropdown.styles.js";
|
|
3
|
+
|
|
4
|
+
// Web Dropdown (the base; Metro falls back to it on native, web bundlers resolve it).
|
|
5
|
+
export const Dropdown = createDropdown(webSkin);
|
|
6
|
+
export type { DropdownProps, DropdownItem } from "./dropdown.shared.js";
|