@milktop/react-ui 0.0.0-stage → 0.1.1
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 +173 -2
- package/dist/accordion/Accordion.d.ts +65 -0
- package/dist/accordion/Accordion.js +61 -0
- package/dist/accordion/accordion.css +17 -0
- package/dist/action-bar/ActionBar.d.ts +33 -0
- package/dist/action-bar/ActionBar.js +48 -0
- package/dist/action-bar/action-bar.css +22 -0
- package/dist/alert/Alert.d.ts +28 -0
- package/dist/alert/Alert.js +12 -0
- package/dist/alert/alert.css +26 -0
- package/dist/app-shell/AppShell.d.ts +38 -0
- package/dist/app-shell/AppShell.js +92 -0
- package/dist/app-shell/Topbar.d.ts +20 -0
- package/dist/app-shell/Topbar.js +13 -0
- package/dist/app-shell/app-shell.css +41 -0
- package/dist/app-shell/shell.d.ts +9 -0
- package/dist/app-shell/shell.js +3 -0
- package/dist/attachments/Attachments.d.ts +84 -0
- package/dist/attachments/Attachments.js +235 -0
- package/dist/attachments/attachments.css +86 -0
- package/dist/avatar/Avatar.d.ts +31 -0
- package/dist/avatar/Avatar.js +41 -0
- package/dist/avatar/AvatarGroup.d.ts +41 -0
- package/dist/avatar/AvatarGroup.js +17 -0
- package/dist/avatar/avatar.css +73 -0
- package/dist/badge/Badge.d.ts +26 -0
- package/dist/badge/Badge.js +10 -0
- package/dist/badge/badge.css +24 -0
- package/dist/banner/Banner.d.ts +33 -0
- package/dist/banner/Banner.js +14 -0
- package/dist/banner/banner.css +25 -0
- package/dist/breadcrumbs/Breadcrumbs.d.ts +39 -0
- package/dist/breadcrumbs/Breadcrumbs.js +28 -0
- package/dist/breadcrumbs/breadcrumbs.css +18 -0
- package/dist/button/Button.d.ts +40 -0
- package/dist/button/Button.js +19 -0
- package/dist/button/button.css +63 -0
- package/dist/card/Card.d.ts +355 -0
- package/dist/card/Card.js +56 -0
- package/dist/card/card.css +43 -0
- package/dist/chart/AreaChart.d.ts +19 -0
- package/dist/chart/AreaChart.js +61 -0
- package/dist/chart/BarChart.d.ts +3 -0
- package/dist/chart/BarChart.js +60 -0
- package/dist/chart/ChartFrame.d.ts +25 -0
- package/dist/chart/ChartFrame.js +22 -0
- package/dist/chart/DonutChart.d.ts +20 -0
- package/dist/chart/DonutChart.js +41 -0
- package/dist/chart/Sparkline.d.ts +26 -0
- package/dist/chart/Sparkline.js +36 -0
- package/dist/chart/chart.css +51 -0
- package/dist/chart/chart.d.ts +23 -0
- package/dist/chart/chart.js +19 -0
- package/dist/checkbox/Checkbox.d.ts +50 -0
- package/dist/checkbox/Checkbox.js +19 -0
- package/dist/checkbox/CheckboxGroup.d.ts +44 -0
- package/dist/checkbox/CheckboxGroup.js +80 -0
- package/dist/checkbox/checkbox.css +49 -0
- package/dist/collapsible/Collapsible.d.ts +41 -0
- package/dist/collapsible/Collapsible.js +15 -0
- package/dist/collapsible/collapsible.css +20 -0
- package/dist/color-scheme.d.ts +18 -0
- package/dist/color-scheme.js +84 -0
- package/dist/command/Command.d.ts +64 -0
- package/dist/command/Command.js +162 -0
- package/dist/command/command.css +47 -0
- package/dist/confirm/confirm.css +5 -0
- package/dist/confirm/confirm.d.ts +42 -0
- package/dist/confirm/confirm.js +115 -0
- package/dist/copy-button/CopyButton.d.ts +30 -0
- package/dist/copy-button/CopyButton.js +23 -0
- package/dist/copy-button/copy-button.css +12 -0
- package/dist/css.d.ts +2 -0
- package/dist/data-list/DataList.d.ts +55 -0
- package/dist/data-list/DataList.js +43 -0
- package/dist/data-list/data-list.css +19 -0
- package/dist/date-picker/DatePicker.d.ts +95 -0
- package/dist/date-picker/DatePicker.js +237 -0
- package/dist/date-picker/date-picker.css +53 -0
- package/dist/dialog/Dialog.d.ts +98 -0
- package/dist/dialog/Dialog.js +92 -0
- package/dist/dialog/dialog.css +31 -0
- package/dist/divider/Divider.d.ts +27 -0
- package/dist/divider/Divider.js +8 -0
- package/dist/divider/divider.css +12 -0
- package/dist/editable/Editable.d.ts +47 -0
- package/dist/editable/Editable.js +51 -0
- package/dist/editable/editable.css +25 -0
- package/dist/editor/Editor.d.ts +59 -0
- package/dist/editor/Editor.js +220 -0
- package/dist/editor/editor.css +42 -0
- package/dist/editor/image.d.ts +1 -0
- package/dist/editor/image.js +23 -0
- package/dist/empty-state/EmptyState.d.ts +22 -0
- package/dist/empty-state/EmptyState.js +9 -0
- package/dist/empty-state/empty-state.css +14 -0
- package/dist/event-calendar/EventCalendar.d.ts +213 -0
- package/dist/event-calendar/EventCalendar.js +686 -0
- package/dist/event-calendar/dates.d.ts +8 -0
- package/dist/event-calendar/dates.js +21 -0
- package/dist/event-calendar/event-calendar.css +157 -0
- package/dist/field/Field.d.ts +66 -0
- package/dist/field/Field.js +23 -0
- package/dist/field/field.css +26 -0
- package/dist/file-upload/FileUpload.d.ts +60 -0
- package/dist/file-upload/FileUpload.js +102 -0
- package/dist/file-upload/file-upload.css +32 -0
- package/dist/format/FormatNumber.d.ts +42 -0
- package/dist/format/FormatNumber.js +59 -0
- package/dist/format/LocaleProvider.d.ts +33 -0
- package/dist/format/LocaleProvider.js +48 -0
- package/dist/format/format.d.ts +37 -0
- package/dist/format/format.js +74 -0
- package/dist/format/messages.d.ts +211 -0
- package/dist/format/messages.js +202 -0
- package/dist/highlight/Highlight.d.ts +11 -0
- package/dist/highlight/Highlight.js +14 -0
- package/dist/highlight/highlight.css +17 -0
- package/dist/hover-card/HoverCard.d.ts +47 -0
- package/dist/hover-card/HoverCard.js +18 -0
- package/dist/hover-card/hover-card.css +19 -0
- package/dist/index.d.ts +84 -0
- package/dist/index.js +85 -0
- package/dist/input/Input.d.ts +41 -0
- package/dist/input/Input.js +48 -0
- package/dist/input/input.css +25 -0
- package/dist/kbd/Kbd.d.ts +19 -0
- package/dist/kbd/Kbd.js +20 -0
- package/dist/kbd/kbd.css +8 -0
- package/dist/menu/Menu.d.ts +223 -0
- package/dist/menu/Menu.js +275 -0
- package/dist/menu/menu.css +38 -0
- package/dist/native-select/NativeSelect.d.ts +36 -0
- package/dist/native-select/NativeSelect.js +51 -0
- package/dist/native-select/native-select.css +16 -0
- package/dist/number-input/NumberInput.d.ts +56 -0
- package/dist/number-input/NumberInput.js +67 -0
- package/dist/number-input/number-input.css +21 -0
- package/dist/page/Page.d.ts +30 -0
- package/dist/page/Page.js +9 -0
- package/dist/page/page.css +17 -0
- package/dist/pagination/Pagination.d.ts +31 -0
- package/dist/pagination/Pagination.js +58 -0
- package/dist/pagination/pagination.css +15 -0
- package/dist/password-input/PasswordInput.d.ts +47 -0
- package/dist/password-input/PasswordInput.js +35 -0
- package/dist/password-input/password-input.css +18 -0
- package/dist/pin-input/PinInput.d.ts +45 -0
- package/dist/pin-input/PinInput.js +22 -0
- package/dist/pin-input/pin-input.css +13 -0
- package/dist/popover/Popover.d.ts +65 -0
- package/dist/popover/Popover.js +26 -0
- package/dist/popover/popover.css +16 -0
- package/dist/progress/Progress.d.ts +56 -0
- package/dist/progress/Progress.js +28 -0
- package/dist/progress/progress.css +24 -0
- package/dist/prose/Prose.d.ts +23 -0
- package/dist/prose/Prose.js +10 -0
- package/dist/prose/prose.css +29 -0
- package/dist/radio-group/RadioGroup.d.ts +59 -0
- package/dist/radio-group/RadioGroup.js +37 -0
- package/dist/radio-group/radio-group.css +30 -0
- package/dist/rolling-number/RollingNumber.d.ts +27 -0
- package/dist/rolling-number/RollingNumber.js +18 -0
- package/dist/rolling-number/rolling-number.css +10 -0
- package/dist/segment-group/Segmented.d.ts +68 -0
- package/dist/segment-group/Segmented.js +79 -0
- package/dist/segment-group/segment-group.css +25 -0
- package/dist/select/ListSelect.d.ts +5 -0
- package/dist/select/ListSelect.js +82 -0
- package/dist/select/SearchSelect.d.ts +43 -0
- package/dist/select/SearchSelect.js +219 -0
- package/dist/select/Select.d.ts +147 -0
- package/dist/select/Select.js +32 -0
- package/dist/select/items.d.ts +17 -0
- package/dist/select/items.js +14 -0
- package/dist/select/search-select.css +45 -0
- package/dist/select/select.css +52 -0
- package/dist/sheet/Sheet.d.ts +88 -0
- package/dist/sheet/Sheet.js +66 -0
- package/dist/sheet/sheet.css +44 -0
- package/dist/sidebar/NavGroup.d.ts +29 -0
- package/dist/sidebar/NavGroup.js +78 -0
- package/dist/sidebar/NavItem.d.ts +33 -0
- package/dist/sidebar/NavItem.js +50 -0
- package/dist/sidebar/NavSection.d.ts +20 -0
- package/dist/sidebar/NavSection.js +44 -0
- package/dist/sidebar/Sidebar.d.ts +65 -0
- package/dist/sidebar/Sidebar.js +59 -0
- package/dist/sidebar/SidebarUser.d.ts +31 -0
- package/dist/sidebar/SidebarUser.js +14 -0
- package/dist/sidebar/context.d.ts +13 -0
- package/dist/sidebar/context.js +6 -0
- package/dist/sidebar/sidebar.css +109 -0
- package/dist/skeleton/Skeleton.d.ts +25 -0
- package/dist/skeleton/Skeleton.js +8 -0
- package/dist/skeleton/skeleton.css +13 -0
- package/dist/slider/Slider.d.ts +70 -0
- package/dist/slider/Slider.js +65 -0
- package/dist/slider/slider.css +17 -0
- package/dist/sortable-list/SortableList.d.ts +46 -0
- package/dist/sortable-list/SortableList.js +85 -0
- package/dist/sortable-list/sortable-list.css +25 -0
- package/dist/spinner/Spinner.d.ts +18 -0
- package/dist/spinner/Spinner.js +11 -0
- package/dist/spinner/spinner.css +9 -0
- package/dist/stat/Stat.d.ts +42 -0
- package/dist/stat/Stat.js +21 -0
- package/dist/stat/Stats.d.ts +23 -0
- package/dist/stat/Stats.js +8 -0
- package/dist/stat/stat.css +34 -0
- package/dist/swatch-picker/SwatchPicker.d.ts +45 -0
- package/dist/swatch-picker/SwatchPicker.js +54 -0
- package/dist/swatch-picker/swatch-picker.css +38 -0
- package/dist/switch/Switch.d.ts +39 -0
- package/dist/switch/Switch.js +17 -0
- package/dist/switch/switch.css +13 -0
- package/dist/table/Table.d.ts +90 -0
- package/dist/table/Table.js +145 -0
- package/dist/table/table.css +44 -0
- package/dist/tabs/Tabs.d.ts +87 -0
- package/dist/tabs/Tabs.js +90 -0
- package/dist/tabs/tabs.css +53 -0
- package/dist/tags-input/TagsInput.d.ts +69 -0
- package/dist/tags-input/TagsInput.js +95 -0
- package/dist/tags-input/tags-input.css +37 -0
- package/dist/textarea/Textarea.d.ts +24 -0
- package/dist/textarea/Textarea.js +51 -0
- package/dist/textarea/textarea.css +14 -0
- package/dist/theme-toggle/ThemeToggle.d.ts +49 -0
- package/dist/theme-toggle/ThemeToggle.js +71 -0
- package/dist/theme-toggle/theme-toggle.css +23 -0
- package/dist/theme.css +119 -0
- package/dist/time-input/TimeInput.d.ts +25 -0
- package/dist/time-input/TimeInput.js +20 -0
- package/dist/timeline/Timeline.d.ts +50 -0
- package/dist/timeline/Timeline.js +15 -0
- package/dist/timeline/timeline.css +29 -0
- package/dist/toast/Toaster.d.ts +36 -0
- package/dist/toast/Toaster.js +25 -0
- package/dist/toast/toast.css +33 -0
- package/dist/tooltip/Tooltip.d.ts +55 -0
- package/dist/tooltip/Tooltip.js +48 -0
- package/dist/tooltip/tooltip.css +18 -0
- package/dist/tree-view/TreeView.d.ts +86 -0
- package/dist/tree-view/TreeView.js +74 -0
- package/dist/tree-view/tree-view.css +29 -0
- package/dist/utils.d.ts +19 -0
- package/dist/utils.js +59 -0
- package/package.json +99 -4
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A dropdown menu of actions from a trigger.
|
|
3
|
+
*
|
|
4
|
+
* <Menu items={actions} onSelect={run} trigger={<Button iconEnd="lucide:chevron-down">Actions</Button>} />
|
|
5
|
+
*
|
|
6
|
+
* <Menu onSelect={run} trigger={<Button>File</Button>}>
|
|
7
|
+
* <Menu.Item value="new" icon="lucide:file-plus" shortcut="⌘N">New</Menu.Item>
|
|
8
|
+
* <Menu.Submenu label="Share" icon="lucide:share-2"><Menu.Item value="email">Email</Menu.Item></Menu.Submenu>
|
|
9
|
+
* <Menu.Separator />
|
|
10
|
+
* <Menu.Group label="Danger zone"><Menu.Item value="delete" danger>Delete</Menu.Item></Menu.Group>
|
|
11
|
+
* </Menu>
|
|
12
|
+
*
|
|
13
|
+
* Arrow keys move through the items (right and left open and close
|
|
14
|
+
* submenus), typing jumps to one, Enter selects, and Escape or a click outside
|
|
15
|
+
* closes it, returning focus to the trigger. Items come from `items`, from
|
|
16
|
+
* child components (after any `items`), or both.
|
|
17
|
+
*
|
|
18
|
+
* - `items`: strings or objects with `labelKey`/`valueKey`/`disabledKey` (the
|
|
19
|
+
* label stands in for a missing value), plus optional `icon`, `shortcut`,
|
|
20
|
+
* `danger` and `href` (a link); `{ separator: true }` draws a line,
|
|
21
|
+
* `{ group: 'Label' }` a group heading, and `{ label, icon, items: […] }` a
|
|
22
|
+
* submenu with its own items
|
|
23
|
+
* - Checkbox and radio entries: `{ type: 'checkbox', value, label, checked }`
|
|
24
|
+
* and `{ type: 'radio', name, value, label, checked }` (one checked per
|
|
25
|
+
* `name`). The menu keeps them checked and calls `onChange` with
|
|
26
|
+
* `{ type, name, value, checked }`
|
|
27
|
+
* - `onSelect`: called with the chosen action's value, from submenus too
|
|
28
|
+
* - `trigger`: the element that opens it (it gets the trigger's props, so a
|
|
29
|
+
* component must pass them on, as Button does)
|
|
30
|
+
* - `placement`: 'bottom-start' (default), 'bottom-end', …
|
|
31
|
+
* - `label`: a small label above the items
|
|
32
|
+
* - `keepOpen`: stay open after an item is chosen (to tick several); an
|
|
33
|
+
* item's own `keepOpen` overrides it
|
|
34
|
+
* - `context`: open on right-click (or long-press) of the trigger, at the
|
|
35
|
+
* pointer (see ContextMenu)
|
|
36
|
+
* - `open`, `defaultOpen`; `onOpenChange`
|
|
37
|
+
* - Child components: Menu.Item, Menu.Separator, Menu.Group, Menu.Submenu,
|
|
38
|
+
* Menu.Checkbox, and Menu.RadioGroup with Menu.Radio (also exported as
|
|
39
|
+
* MenuItem, MenuSeparator…)
|
|
40
|
+
*
|
|
41
|
+
* By hand, Ark's parts are here too: Menu.Root, Trigger, ContextTrigger,
|
|
42
|
+
* Positioner, Content, ItemText, ItemIndicator, CheckboxItem, RadioItem…
|
|
43
|
+
*/
|
|
44
|
+
import { type ComponentProps, type ReactElement, type ReactNode } from 'react';
|
|
45
|
+
import { Menu as Ark } from '@ark-ui/react/menu';
|
|
46
|
+
import 'iconify-icon';
|
|
47
|
+
import { type Classes } from '../utils.js';
|
|
48
|
+
import '../theme.css';
|
|
49
|
+
import './menu.css';
|
|
50
|
+
type Value = string | number;
|
|
51
|
+
export type MenuEntry = string | number | {
|
|
52
|
+
label?: ReactNode;
|
|
53
|
+
value?: Value;
|
|
54
|
+
icon?: string;
|
|
55
|
+
shortcut?: string;
|
|
56
|
+
danger?: boolean;
|
|
57
|
+
href?: string;
|
|
58
|
+
disabled?: boolean;
|
|
59
|
+
keepOpen?: boolean | null;
|
|
60
|
+
separator?: boolean;
|
|
61
|
+
group?: ReactNode;
|
|
62
|
+
items?: MenuEntry[];
|
|
63
|
+
type?: 'checkbox' | 'radio';
|
|
64
|
+
name?: string;
|
|
65
|
+
checked?: boolean;
|
|
66
|
+
[key: string]: unknown;
|
|
67
|
+
};
|
|
68
|
+
export type MenuOptionChange = {
|
|
69
|
+
type: 'checkbox' | 'radio';
|
|
70
|
+
name: string | null;
|
|
71
|
+
value: unknown;
|
|
72
|
+
checked: boolean;
|
|
73
|
+
};
|
|
74
|
+
type ItemExtras = {
|
|
75
|
+
icon?: string | null;
|
|
76
|
+
shortcut?: ReactNode;
|
|
77
|
+
danger?: boolean;
|
|
78
|
+
disabled?: boolean;
|
|
79
|
+
keepOpen?: boolean | null;
|
|
80
|
+
};
|
|
81
|
+
export type MenuItemProps = Omit<ComponentProps<'div'>, 'onSelect'> & ItemExtras & {
|
|
82
|
+
value?: Value;
|
|
83
|
+
href?: string | null;
|
|
84
|
+
onSelect?: () => void;
|
|
85
|
+
};
|
|
86
|
+
/**
|
|
87
|
+
* One action in a Menu or Menu.Submenu; its content is the label.
|
|
88
|
+
*
|
|
89
|
+
* <Menu.Item value="edit" icon="lucide:pencil" shortcut="⌘E">Edit</Menu.Item>
|
|
90
|
+
*
|
|
91
|
+
* - `value`: what the menu's `onSelect` gets (the label's text if unset)
|
|
92
|
+
* - `href`: makes it a link
|
|
93
|
+
* - `icon`, `shortcut`, `danger`, `disabled` and `keepOpen`: as on `items` entries
|
|
94
|
+
* - `onSelect`: runs when it's chosen, by pointer or keyboard
|
|
95
|
+
*/
|
|
96
|
+
declare function Item({ value, href, icon, shortcut, danger, disabled, keepOpen, onSelect, className, children, ...rest }: MenuItemProps): import("react").JSX.Element;
|
|
97
|
+
/** A line between items in a Menu or Menu.Submenu. */
|
|
98
|
+
declare function Separator(props: ComponentProps<'hr'>): import("react").JSX.Element;
|
|
99
|
+
/**
|
|
100
|
+
* A group of items in a Menu or Menu.Submenu, under a heading.
|
|
101
|
+
*
|
|
102
|
+
* <Menu.Group label="Student"><Menu.Item value="message">Send message</Menu.Item></Menu.Group>
|
|
103
|
+
*/
|
|
104
|
+
declare function Group({ label, children, ...rest }: ComponentProps<'div'> & {
|
|
105
|
+
label?: ReactNode;
|
|
106
|
+
}): import("react").JSX.Element;
|
|
107
|
+
export type MenuCheckboxProps = ItemExtras & {
|
|
108
|
+
checked?: boolean;
|
|
109
|
+
defaultChecked?: boolean;
|
|
110
|
+
onCheckedChange?: (checked: boolean) => void;
|
|
111
|
+
value?: Value;
|
|
112
|
+
children?: ReactNode;
|
|
113
|
+
};
|
|
114
|
+
/**
|
|
115
|
+
* A checkbox item in a Menu or Menu.Submenu; its content is the label.
|
|
116
|
+
*
|
|
117
|
+
* <Menu.Checkbox checked={showGrid} onCheckedChange={setShowGrid} keepOpen>Show grid</Menu.Checkbox>
|
|
118
|
+
*
|
|
119
|
+
* - `checked`, `defaultChecked`; `onCheckedChange`
|
|
120
|
+
* - `value`, `icon`, `shortcut`, `disabled` and `keepOpen`: as on Menu.Item
|
|
121
|
+
*/
|
|
122
|
+
declare function Checkbox({ checked, defaultChecked, onCheckedChange, value, icon, shortcut, disabled, keepOpen, children }: MenuCheckboxProps): import("react").JSX.Element;
|
|
123
|
+
/**
|
|
124
|
+
* Radio items in a Menu or Menu.Submenu, one of which is checked.
|
|
125
|
+
*
|
|
126
|
+
* <Menu.RadioGroup value={sort} onValueChange={setSort} label="Sort by">
|
|
127
|
+
* <Menu.Radio value="name">Name</Menu.Radio>
|
|
128
|
+
* <Menu.Radio value="date">Date</Menu.Radio>
|
|
129
|
+
* </Menu.RadioGroup>
|
|
130
|
+
*
|
|
131
|
+
* - `value`, `defaultValue`; `onValueChange`: called with the checked radio's value
|
|
132
|
+
* - `label`: an optional heading
|
|
133
|
+
*/
|
|
134
|
+
declare function RadioGroup({ value, defaultValue, onValueChange, label, children }: {
|
|
135
|
+
value?: Value | null;
|
|
136
|
+
defaultValue?: Value | null;
|
|
137
|
+
onValueChange?: (value: Value) => void;
|
|
138
|
+
label?: ReactNode;
|
|
139
|
+
children?: ReactNode;
|
|
140
|
+
}): import("react").JSX.Element;
|
|
141
|
+
/**
|
|
142
|
+
* One radio item in a Menu.RadioGroup; its content is the label.
|
|
143
|
+
*
|
|
144
|
+
* - `value`: what the group's value becomes
|
|
145
|
+
* - `icon`, `shortcut`, `disabled` and `keepOpen`: as on Menu.Item
|
|
146
|
+
*/
|
|
147
|
+
declare function Radio({ value, icon, shortcut, disabled, keepOpen, children }: ItemExtras & {
|
|
148
|
+
value: Value;
|
|
149
|
+
children?: ReactNode;
|
|
150
|
+
}): import("react").JSX.Element;
|
|
151
|
+
/**
|
|
152
|
+
* A submenu inside a Menu (or another Submenu): a row that opens a nested
|
|
153
|
+
* menu of its own items, from `items` or child components like Menu's.
|
|
154
|
+
*
|
|
155
|
+
* <Menu.Submenu label="Share" icon="lucide:share-2">
|
|
156
|
+
* <Menu.Item value="email">Email</Menu.Item>
|
|
157
|
+
* </Menu.Submenu>
|
|
158
|
+
*
|
|
159
|
+
* - `label`: the row's text; `icon`: an optional Iconify name before it
|
|
160
|
+
* - `items`: as on Menu; `keepOpen` for its items (it follows its menu)
|
|
161
|
+
*
|
|
162
|
+
* Its items' selections reach the outer Menu's `onSelect`.
|
|
163
|
+
*/
|
|
164
|
+
declare function Submenu({ label, icon, items, keepOpen, className, children }: {
|
|
165
|
+
label: ReactNode;
|
|
166
|
+
icon?: string | null;
|
|
167
|
+
items?: MenuEntry[];
|
|
168
|
+
keepOpen?: boolean | null;
|
|
169
|
+
className?: string;
|
|
170
|
+
children?: ReactNode;
|
|
171
|
+
}): import("react").JSX.Element;
|
|
172
|
+
export type MenuProps = Omit<Ark.ContentProps, 'onSelect' | 'onChange'> & {
|
|
173
|
+
items?: MenuEntry[];
|
|
174
|
+
labelKey?: string;
|
|
175
|
+
valueKey?: string;
|
|
176
|
+
disabledKey?: string;
|
|
177
|
+
trigger?: ReactElement | null;
|
|
178
|
+
placement?: NonNullable<Ark.RootProps['positioning']>['placement'];
|
|
179
|
+
label?: ReactNode;
|
|
180
|
+
keepOpen?: boolean;
|
|
181
|
+
context?: boolean;
|
|
182
|
+
open?: boolean;
|
|
183
|
+
defaultOpen?: boolean;
|
|
184
|
+
onOpenChange?: (open: boolean) => void;
|
|
185
|
+
onSelect?: (value: unknown) => void;
|
|
186
|
+
onChange?: (change: MenuOptionChange) => void;
|
|
187
|
+
classes?: Classes<'positioner' | 'content' | 'menuLabel'>;
|
|
188
|
+
};
|
|
189
|
+
declare function MenuShortcut({ items, labelKey, valueKey, disabledKey, trigger, placement, label, keepOpen, context, open, defaultOpen, onOpenChange, onSelect, onChange, className, classes, children, ...rest }: MenuProps): import("react").JSX.Element;
|
|
190
|
+
export declare const Menu: typeof MenuShortcut & {
|
|
191
|
+
Item: typeof Item;
|
|
192
|
+
Separator: typeof Separator;
|
|
193
|
+
Group: typeof Group;
|
|
194
|
+
Checkbox: typeof Checkbox;
|
|
195
|
+
RadioGroup: typeof RadioGroup;
|
|
196
|
+
Radio: typeof Radio;
|
|
197
|
+
Submenu: typeof Submenu;
|
|
198
|
+
Root: (props: Ark.RootProps) => import("react").JSX.Element;
|
|
199
|
+
Trigger: import("react").ForwardRefExoticComponent<Ark.TriggerProps & import("react").RefAttributes<HTMLButtonElement>>;
|
|
200
|
+
ContextTrigger: import("react").ForwardRefExoticComponent<Ark.ContextTriggerProps & import("react").RefAttributes<HTMLButtonElement>>;
|
|
201
|
+
Positioner: import("react").ForwardRefExoticComponent<Ark.PositionerProps & import("react").RefAttributes<HTMLDivElement>>;
|
|
202
|
+
Content: import("react").ForwardRefExoticComponent<Ark.ContentProps & import("react").RefAttributes<HTMLDivElement>>;
|
|
203
|
+
ItemText: import("react").ForwardRefExoticComponent<Ark.ItemTextProps & import("react").RefAttributes<HTMLDivElement>>;
|
|
204
|
+
ItemIndicator: import("react").ForwardRefExoticComponent<Ark.ItemIndicatorProps & import("react").RefAttributes<HTMLDivElement>>;
|
|
205
|
+
ItemGroup: import("react").ForwardRefExoticComponent<Ark.ItemGroupProps & import("react").RefAttributes<HTMLDivElement>>;
|
|
206
|
+
ItemGroupLabel: import("react").ForwardRefExoticComponent<Ark.ItemGroupLabelProps & import("react").RefAttributes<HTMLDivElement>>;
|
|
207
|
+
CheckboxItem: import("react").ForwardRefExoticComponent<Ark.CheckboxItemProps & import("react").RefAttributes<HTMLDivElement>>;
|
|
208
|
+
RadioItemGroup: import("react").ForwardRefExoticComponent<Ark.RadioItemGroupProps & import("react").RefAttributes<HTMLDivElement>>;
|
|
209
|
+
RadioItem: import("react").ForwardRefExoticComponent<Ark.RadioItemProps & import("react").RefAttributes<HTMLDivElement>>;
|
|
210
|
+
TriggerItem: import("react").ForwardRefExoticComponent<Ark.TriggerItemProps & import("react").RefAttributes<HTMLDivElement>>;
|
|
211
|
+
Arrow: import("react").ForwardRefExoticComponent<Ark.ArrowProps & import("react").RefAttributes<HTMLDivElement>>;
|
|
212
|
+
ArrowTip: import("react").ForwardRefExoticComponent<Ark.ArrowTipProps & import("react").RefAttributes<HTMLDivElement>>;
|
|
213
|
+
Context: (props: Ark.ContextProps) => ReactNode;
|
|
214
|
+
};
|
|
215
|
+
/**
|
|
216
|
+
* A Menu opened by right-clicking (or long-pressing on touch) an area, at the pointer.
|
|
217
|
+
*
|
|
218
|
+
* <ContextMenu items={actions} onSelect={run} trigger={<div>…the area…</div>} />
|
|
219
|
+
*
|
|
220
|
+
* Takes the same props, `items` and child components (submenus too) as Menu.
|
|
221
|
+
*/
|
|
222
|
+
export declare function ContextMenu(props: Omit<MenuProps, 'context'>): import("react").JSX.Element;
|
|
223
|
+
export { Item as MenuItem, Separator as MenuSeparator, Group as MenuGroup, Checkbox as MenuCheckbox, RadioGroup as MenuRadioGroup, Radio as MenuRadio, Submenu };
|
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* A dropdown menu of actions from a trigger.
|
|
4
|
+
*
|
|
5
|
+
* <Menu items={actions} onSelect={run} trigger={<Button iconEnd="lucide:chevron-down">Actions</Button>} />
|
|
6
|
+
*
|
|
7
|
+
* <Menu onSelect={run} trigger={<Button>File</Button>}>
|
|
8
|
+
* <Menu.Item value="new" icon="lucide:file-plus" shortcut="⌘N">New</Menu.Item>
|
|
9
|
+
* <Menu.Submenu label="Share" icon="lucide:share-2"><Menu.Item value="email">Email</Menu.Item></Menu.Submenu>
|
|
10
|
+
* <Menu.Separator />
|
|
11
|
+
* <Menu.Group label="Danger zone"><Menu.Item value="delete" danger>Delete</Menu.Item></Menu.Group>
|
|
12
|
+
* </Menu>
|
|
13
|
+
*
|
|
14
|
+
* Arrow keys move through the items (right and left open and close
|
|
15
|
+
* submenus), typing jumps to one, Enter selects, and Escape or a click outside
|
|
16
|
+
* closes it, returning focus to the trigger. Items come from `items`, from
|
|
17
|
+
* child components (after any `items`), or both.
|
|
18
|
+
*
|
|
19
|
+
* - `items`: strings or objects with `labelKey`/`valueKey`/`disabledKey` (the
|
|
20
|
+
* label stands in for a missing value), plus optional `icon`, `shortcut`,
|
|
21
|
+
* `danger` and `href` (a link); `{ separator: true }` draws a line,
|
|
22
|
+
* `{ group: 'Label' }` a group heading, and `{ label, icon, items: […] }` a
|
|
23
|
+
* submenu with its own items
|
|
24
|
+
* - Checkbox and radio entries: `{ type: 'checkbox', value, label, checked }`
|
|
25
|
+
* and `{ type: 'radio', name, value, label, checked }` (one checked per
|
|
26
|
+
* `name`). The menu keeps them checked and calls `onChange` with
|
|
27
|
+
* `{ type, name, value, checked }`
|
|
28
|
+
* - `onSelect`: called with the chosen action's value, from submenus too
|
|
29
|
+
* - `trigger`: the element that opens it (it gets the trigger's props, so a
|
|
30
|
+
* component must pass them on, as Button does)
|
|
31
|
+
* - `placement`: 'bottom-start' (default), 'bottom-end', …
|
|
32
|
+
* - `label`: a small label above the items
|
|
33
|
+
* - `keepOpen`: stay open after an item is chosen (to tick several); an
|
|
34
|
+
* item's own `keepOpen` overrides it
|
|
35
|
+
* - `context`: open on right-click (or long-press) of the trigger, at the
|
|
36
|
+
* pointer (see ContextMenu)
|
|
37
|
+
* - `open`, `defaultOpen`; `onOpenChange`
|
|
38
|
+
* - Child components: Menu.Item, Menu.Separator, Menu.Group, Menu.Submenu,
|
|
39
|
+
* Menu.Checkbox, and Menu.RadioGroup with Menu.Radio (also exported as
|
|
40
|
+
* MenuItem, MenuSeparator…)
|
|
41
|
+
*
|
|
42
|
+
* By hand, Ark's parts are here too: Menu.Root, Trigger, ContextTrigger,
|
|
43
|
+
* Positioner, Content, ItemText, ItemIndicator, CheckboxItem, RadioItem…
|
|
44
|
+
*/
|
|
45
|
+
import { createContext, useContext, useId, useRef, useState } from 'react';
|
|
46
|
+
import { Menu as Ark, useMenuContext } from '@ark-ui/react/menu';
|
|
47
|
+
import 'iconify-icon';
|
|
48
|
+
import { cx, partsOf, withParts } from '../utils.js';
|
|
49
|
+
import '../theme.css';
|
|
50
|
+
import './menu.css';
|
|
51
|
+
const MenuContext = createContext(null);
|
|
52
|
+
const useMenu = () => {
|
|
53
|
+
const menu = useContext(MenuContext);
|
|
54
|
+
if (!menu)
|
|
55
|
+
throw new Error('Menu parts go inside a Menu');
|
|
56
|
+
return menu;
|
|
57
|
+
};
|
|
58
|
+
// Zag's per-item closeOnSelect for a `keepOpen` (unset follows the menu); left out when unset.
|
|
59
|
+
const closeFor = (keepOpen) => (keepOpen == null ? {} : { closeOnSelect: !keepOpen });
|
|
60
|
+
// A label's text, when it's plain text, to stand in for a missing value.
|
|
61
|
+
const textOf = (node) => (typeof node === 'string' || typeof node === 'number' ? String(node) : '');
|
|
62
|
+
// An item's inside: icon, label and shortcut.
|
|
63
|
+
function ItemInside({ part, icon, shortcut, children }) {
|
|
64
|
+
return _jsxs(_Fragment, { children: [icon && _jsx("iconify-icon", { ...part('icon'), icon: icon, "aria-hidden": "true" }), _jsx(Ark.ItemText, { ...part('label'), children: children }), shortcut && _jsx("kbd", { ...part('shortcut'), children: shortcut })] });
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* One action in a Menu or Menu.Submenu; its content is the label.
|
|
68
|
+
*
|
|
69
|
+
* <Menu.Item value="edit" icon="lucide:pencil" shortcut="⌘E">Edit</Menu.Item>
|
|
70
|
+
*
|
|
71
|
+
* - `value`: what the menu's `onSelect` gets (the label's text if unset)
|
|
72
|
+
* - `href`: makes it a link
|
|
73
|
+
* - `icon`, `shortcut`, `danger`, `disabled` and `keepOpen`: as on `items` entries
|
|
74
|
+
* - `onSelect`: runs when it's chosen, by pointer or keyboard
|
|
75
|
+
*/
|
|
76
|
+
function Item({ value, href = null, icon = null, shortcut = null, danger = false, disabled = false, keepOpen = null, onSelect, className, children, ...rest }) {
|
|
77
|
+
const menu = useMenu();
|
|
78
|
+
const id = useId();
|
|
79
|
+
const itemValue = value ?? (textOf(children) || id);
|
|
80
|
+
const key = String(itemValue);
|
|
81
|
+
menu.remember(key, itemValue);
|
|
82
|
+
const inside = _jsx(ItemInside, { part: menu.part, icon: icon, shortcut: shortcut, children: children });
|
|
83
|
+
return (_jsx(Ark.Item, { ...rest, ...menu.part('item'), className: className, value: key, disabled: !!disabled, ...closeFor(keepOpen), "data-danger": danger || undefined, ...(onSelect && { onSelect }), asChild: !!href, children: href ? _jsx("a", { href: href, children: inside }) : inside }));
|
|
84
|
+
}
|
|
85
|
+
/** A line between items in a Menu or Menu.Submenu. */
|
|
86
|
+
function Separator(props) {
|
|
87
|
+
const menu = useMenu();
|
|
88
|
+
return _jsx(Ark.Separator, { ...props, ...menu.part('separator') });
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* A group of items in a Menu or Menu.Submenu, under a heading.
|
|
92
|
+
*
|
|
93
|
+
* <Menu.Group label="Student"><Menu.Item value="message">Send message</Menu.Item></Menu.Group>
|
|
94
|
+
*/
|
|
95
|
+
function Group({ label = null, children, ...rest }) {
|
|
96
|
+
const menu = useMenu();
|
|
97
|
+
return (_jsxs(Ark.ItemGroup, { ...rest, ...menu.part('item-group'), children: [label && _jsx(Ark.ItemGroupLabel, { ...menu.part('group-label'), children: label }), children] }));
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* A checkbox item in a Menu or Menu.Submenu; its content is the label.
|
|
101
|
+
*
|
|
102
|
+
* <Menu.Checkbox checked={showGrid} onCheckedChange={setShowGrid} keepOpen>Show grid</Menu.Checkbox>
|
|
103
|
+
*
|
|
104
|
+
* - `checked`, `defaultChecked`; `onCheckedChange`
|
|
105
|
+
* - `value`, `icon`, `shortcut`, `disabled` and `keepOpen`: as on Menu.Item
|
|
106
|
+
*/
|
|
107
|
+
function Checkbox({ checked, defaultChecked = false, onCheckedChange, value, icon = null, shortcut = null, disabled = false, keepOpen = null, children }) {
|
|
108
|
+
const menu = useMenu();
|
|
109
|
+
const id = useId();
|
|
110
|
+
const [own, setOwn] = useState(defaultChecked);
|
|
111
|
+
const on = checked ?? own;
|
|
112
|
+
return (_jsxs(Ark.CheckboxItem, { ...menu.part('item'), value: String(value ?? id), checked: on, disabled: !!disabled, ...closeFor(keepOpen), onCheckedChange: (next) => { setOwn(next); onCheckedChange?.(next); }, children: [_jsx("span", { ...menu.part('indicator'), children: _jsx(Ark.ItemIndicator, { ...menu.part('item-indicator'), "data-type": "checkbox", children: _jsx("iconify-icon", { icon: "lucide:check", "aria-hidden": "true" }) }) }), _jsx(ItemInside, { part: menu.part, icon: icon, shortcut: shortcut, children: children })] }));
|
|
113
|
+
}
|
|
114
|
+
// A radio group's id and value, for its radios.
|
|
115
|
+
const RadioContext = createContext(null);
|
|
116
|
+
/**
|
|
117
|
+
* Radio items in a Menu or Menu.Submenu, one of which is checked.
|
|
118
|
+
*
|
|
119
|
+
* <Menu.RadioGroup value={sort} onValueChange={setSort} label="Sort by">
|
|
120
|
+
* <Menu.Radio value="name">Name</Menu.Radio>
|
|
121
|
+
* <Menu.Radio value="date">Date</Menu.Radio>
|
|
122
|
+
* </Menu.RadioGroup>
|
|
123
|
+
*
|
|
124
|
+
* - `value`, `defaultValue`; `onValueChange`: called with the checked radio's value
|
|
125
|
+
* - `label`: an optional heading
|
|
126
|
+
*/
|
|
127
|
+
function RadioGroup({ value, defaultValue = null, onValueChange, label = null, children }) {
|
|
128
|
+
const menu = useMenu();
|
|
129
|
+
const id = useId();
|
|
130
|
+
const [own, setOwn] = useState(defaultValue);
|
|
131
|
+
const current = value !== undefined ? value : own;
|
|
132
|
+
// Radios register their own values, keyed by the group (two groups can share values).
|
|
133
|
+
const values = useRef(new Map());
|
|
134
|
+
return (_jsx(RadioContext.Provider, { value: { id, value: current }, children: _jsxs(Ark.RadioItemGroup, { ...menu.part('item-group'), value: current == null ? '' : `${id}/${current}`, onValueChange: (details) => {
|
|
135
|
+
const next = values.current.get(details.value);
|
|
136
|
+
if (next === undefined)
|
|
137
|
+
return;
|
|
138
|
+
setOwn(next);
|
|
139
|
+
onValueChange?.(next);
|
|
140
|
+
}, children: [label && _jsx(Ark.ItemGroupLabel, { ...menu.part('group-label'), children: label }), _jsx(RadioValues.Provider, { value: values.current, children: children })] }) }));
|
|
141
|
+
}
|
|
142
|
+
const RadioValues = createContext(null);
|
|
143
|
+
/**
|
|
144
|
+
* One radio item in a Menu.RadioGroup; its content is the label.
|
|
145
|
+
*
|
|
146
|
+
* - `value`: what the group's value becomes
|
|
147
|
+
* - `icon`, `shortcut`, `disabled` and `keepOpen`: as on Menu.Item
|
|
148
|
+
*/
|
|
149
|
+
function Radio({ value, icon = null, shortcut = null, disabled = false, keepOpen = null, children }) {
|
|
150
|
+
const menu = useMenu();
|
|
151
|
+
const group = useContext(RadioContext);
|
|
152
|
+
const values = useContext(RadioValues);
|
|
153
|
+
const key = `${group?.id}/${value}`;
|
|
154
|
+
values?.set(key, value);
|
|
155
|
+
return (_jsxs(Ark.RadioItem, { ...menu.part('item'), value: key, disabled: !!disabled, ...closeFor(keepOpen), children: [_jsx("span", { ...menu.part('indicator'), children: _jsx(Ark.ItemIndicator, { ...menu.part('item-indicator'), "data-type": "radio" }) }), _jsx(ItemInside, { part: menu.part, icon: icon, shortcut: shortcut, children: children })] }));
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* A submenu inside a Menu (or another Submenu): a row that opens a nested
|
|
159
|
+
* menu of its own items, from `items` or child components like Menu's.
|
|
160
|
+
*
|
|
161
|
+
* <Menu.Submenu label="Share" icon="lucide:share-2">
|
|
162
|
+
* <Menu.Item value="email">Email</Menu.Item>
|
|
163
|
+
* </Menu.Submenu>
|
|
164
|
+
*
|
|
165
|
+
* - `label`: the row's text; `icon`: an optional Iconify name before it
|
|
166
|
+
* - `items`: as on Menu; `keepOpen` for its items (it follows its menu)
|
|
167
|
+
*
|
|
168
|
+
* Its items' selections reach the outer Menu's `onSelect`.
|
|
169
|
+
*/
|
|
170
|
+
function Submenu({ label, icon = null, items = [], keepOpen = null, className, children }) {
|
|
171
|
+
const outer = useMenu();
|
|
172
|
+
const part = outer.part;
|
|
173
|
+
const stays = keepOpen ?? outer.keepOpen;
|
|
174
|
+
return (_jsxs(Ark.Root, { closeOnSelect: !stays, positioning: { placement: 'right-start', strategy: 'fixed', gutter: 2 }, navigate: (details) => details.node.click(), onSelect: (details) => outer.select(details.value), children: [_jsxs(Ark.TriggerItem, { ...part('item'), children: [icon && _jsx("iconify-icon", { ...part('icon'), icon: icon, "aria-hidden": "true" }), _jsx("span", { ...part('label'), children: label }), _jsx("iconify-icon", { ...part('chevron'), icon: "lucide:chevron-right", "aria-hidden": "true" })] }), _jsx(Ark.Positioner, { ...part('positioner'), children: _jsx(Ark.Content, { ...part('content'), className: className, children: _jsxs(MenuContext.Provider, { value: { ...outer, keepOpen: stays }, children: [_jsx(Entries, { items: items }), children] }) }) })] }));
|
|
175
|
+
}
|
|
176
|
+
// A checkbox or radio entry from `items`, checked by the root menu. Zag's
|
|
177
|
+
// option props straight onto it: Ark's RadioItem wants a RadioItemGroup round it.
|
|
178
|
+
function Option({ entry, optionKey, label, disabled, siblings }) {
|
|
179
|
+
const menu = useMenu();
|
|
180
|
+
const api = useMenuContext();
|
|
181
|
+
const props = {
|
|
182
|
+
type: entry.type, value: optionKey, checked: menu.isChecked(optionKey, !!entry.checked), disabled,
|
|
183
|
+
valueText: typeof label === 'string' ? label : optionKey, ...closeFor(entry.keepOpen),
|
|
184
|
+
onCheckedChange: (on) => menu.check(entry, optionKey, on, siblings),
|
|
185
|
+
};
|
|
186
|
+
return (_jsxs("div", { ...api.getOptionItemProps(props), ...menu.part('item'), "data-danger": entry.danger || undefined, children: [_jsx("span", { ...menu.part('indicator'), children: _jsx("span", { ...api.getItemIndicatorProps(props), ...menu.part('item-indicator'), "data-type": entry.type, children: entry.type === 'checkbox' && _jsx("iconify-icon", { icon: "lucide:check", "aria-hidden": "true" }) }) }), entry.icon && _jsx("iconify-icon", { ...menu.part('icon'), icon: entry.icon, "aria-hidden": "true" }), _jsx("span", { ...menu.part('label'), children: label }), entry.shortcut && _jsx("kbd", { ...menu.part('shortcut'), children: entry.shortcut })] }));
|
|
187
|
+
}
|
|
188
|
+
// A menu's `items` array, as the components above.
|
|
189
|
+
function Entries({ items }) {
|
|
190
|
+
const menu = useMenu();
|
|
191
|
+
const { labelKey, valueKey, disabledKey } = menu.keys;
|
|
192
|
+
const field = (entry, key) => entry[key];
|
|
193
|
+
return _jsx(_Fragment, { children: items.map((entry, i) => {
|
|
194
|
+
if (typeof entry !== 'object')
|
|
195
|
+
return _jsx(Item, { value: entry, children: entry }, i);
|
|
196
|
+
const label = field(entry, labelKey);
|
|
197
|
+
const value = (field(entry, valueKey) ?? label);
|
|
198
|
+
const disabled = !!field(entry, disabledKey);
|
|
199
|
+
if (entry.separator)
|
|
200
|
+
return _jsx(Separator, {}, i);
|
|
201
|
+
if (entry.group)
|
|
202
|
+
return _jsx("div", { ...menu.part('group-label'), children: entry.group }, i);
|
|
203
|
+
if (entry.items)
|
|
204
|
+
return _jsx(Submenu, { label: label, icon: entry.icon, items: entry.items }, i);
|
|
205
|
+
if (entry.type === 'checkbox' || entry.type === 'radio') {
|
|
206
|
+
// Radio keys include their `name`, so two groups can share values.
|
|
207
|
+
const key = entry.type === 'radio' ? `${entry.name ?? ''}/${value}` : String(value);
|
|
208
|
+
return _jsx(Option, { entry: entry, optionKey: key, label: label, disabled: disabled, siblings: items }, i);
|
|
209
|
+
}
|
|
210
|
+
return (_jsx(Item, { value: value, href: entry.href, icon: entry.icon, shortcut: entry.shortcut, danger: entry.danger, disabled: disabled, keepOpen: entry.keepOpen, children: label }, i));
|
|
211
|
+
}) });
|
|
212
|
+
}
|
|
213
|
+
function MenuShortcut({ items = [], labelKey = 'label', valueKey = 'value', disabledKey = 'disabled', trigger = null, placement = 'bottom-start', label = null, keepOpen = false, context = false, open, defaultOpen, onOpenChange, onSelect, onChange, className, classes, children, ...rest }) {
|
|
214
|
+
const part = partsOf('menu', classes);
|
|
215
|
+
const values = useRef(new Map());
|
|
216
|
+
// `items` checkbox and radio entries, starting from their own `checked`.
|
|
217
|
+
const [checks, setChecks] = useState({});
|
|
218
|
+
const shared = {
|
|
219
|
+
part, keepOpen, keys: { labelKey, valueKey, disabledKey },
|
|
220
|
+
remember: (key, value) => { values.current.set(key, value); },
|
|
221
|
+
select: (key) => { if (values.current.has(key))
|
|
222
|
+
onSelect?.(values.current.get(key)); },
|
|
223
|
+
isChecked: (key, initial) => checks[key] ?? initial,
|
|
224
|
+
check: (entry, key, on, siblings) => {
|
|
225
|
+
const next = { ...checks };
|
|
226
|
+
if (entry.type === 'radio') {
|
|
227
|
+
if (next[key] ?? entry.checked)
|
|
228
|
+
return;
|
|
229
|
+
for (const other of siblings) {
|
|
230
|
+
if (typeof other === 'object' && other.type === 'radio' && other.name === entry.name) {
|
|
231
|
+
next[`${other.name ?? ''}/${other[valueKey] ?? other[labelKey]}`] = false;
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
next[key] = on;
|
|
236
|
+
setChecks(next);
|
|
237
|
+
onChange?.({ type: entry.type, name: entry.name ?? null, value: entry[valueKey] ?? entry[labelKey], checked: on });
|
|
238
|
+
},
|
|
239
|
+
};
|
|
240
|
+
const Trigger = context ? Ark.ContextTrigger : Ark.Trigger;
|
|
241
|
+
return (_jsxs(Ark.Root, { closeOnSelect: !keepOpen, positioning: { placement, strategy: 'fixed', gutter: 4 }, ...(open !== undefined && { open }), ...(defaultOpen !== undefined && { defaultOpen }), ...(onOpenChange && { onOpenChange: (details) => onOpenChange(details.open) }),
|
|
242
|
+
// Enter on a link: Zag's own click doesn't bubble, so routers listening
|
|
243
|
+
// on the document would miss it.
|
|
244
|
+
navigate: (details) => details.node.click(), onSelect: (details) => shared.select(details.value), children: [trigger && _jsx(Trigger, { asChild: true, children: trigger }), _jsx(Ark.Positioner, { ...part('positioner'), children: _jsx(Ark.Content, { ...rest, ...part('content'), className: cx(className, classes?.content), children: _jsxs(MenuContext.Provider, { value: shared, children: [label && _jsx("div", { ...part('menu-label'), children: label }), _jsx(Entries, { items: items }), children] }) }) })] }));
|
|
245
|
+
}
|
|
246
|
+
export const Menu = withParts(MenuShortcut, {
|
|
247
|
+
Item, Separator, Group, Checkbox, RadioGroup, Radio, Submenu,
|
|
248
|
+
Root: Ark.Root,
|
|
249
|
+
Trigger: Ark.Trigger,
|
|
250
|
+
ContextTrigger: Ark.ContextTrigger,
|
|
251
|
+
Positioner: Ark.Positioner,
|
|
252
|
+
Content: Ark.Content,
|
|
253
|
+
ItemText: Ark.ItemText,
|
|
254
|
+
ItemIndicator: Ark.ItemIndicator,
|
|
255
|
+
ItemGroup: Ark.ItemGroup,
|
|
256
|
+
ItemGroupLabel: Ark.ItemGroupLabel,
|
|
257
|
+
CheckboxItem: Ark.CheckboxItem,
|
|
258
|
+
RadioItemGroup: Ark.RadioItemGroup,
|
|
259
|
+
RadioItem: Ark.RadioItem,
|
|
260
|
+
TriggerItem: Ark.TriggerItem,
|
|
261
|
+
Arrow: Ark.Arrow,
|
|
262
|
+
ArrowTip: Ark.ArrowTip,
|
|
263
|
+
Context: Ark.Context,
|
|
264
|
+
});
|
|
265
|
+
/**
|
|
266
|
+
* A Menu opened by right-clicking (or long-pressing on touch) an area, at the pointer.
|
|
267
|
+
*
|
|
268
|
+
* <ContextMenu items={actions} onSelect={run} trigger={<div>…the area…</div>} />
|
|
269
|
+
*
|
|
270
|
+
* Takes the same props, `items` and child components (submenus too) as Menu.
|
|
271
|
+
*/
|
|
272
|
+
export function ContextMenu(props) {
|
|
273
|
+
return _jsx(MenuShortcut, { ...props, context: true });
|
|
274
|
+
}
|
|
275
|
+
export { Item as MenuItem, Separator as MenuSeparator, Group as MenuGroup, Checkbox as MenuCheckbox, RadioGroup as MenuRadioGroup, Radio as MenuRadio, Submenu };
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/* Shared by Menu, its child components and submenus. Zag names most parts:
|
|
2
|
+
item, option-item, trigger-item, separator, item-group-label… */
|
|
3
|
+
@layer ui {
|
|
4
|
+
/* Zag copies the content's z-index onto its positioner. */
|
|
5
|
+
[data-scope=menu][data-part=content] {
|
|
6
|
+
z-index: 50; font-weight: 400; min-width: 12rem; padding: 0.5rem 0.25rem; box-sizing: border-box; text-align: left; outline: none;
|
|
7
|
+
background: var(--ui-surface); color: var(--ui-text); font-family: var(--ui-font); font-size: 14px;
|
|
8
|
+
border: 1px solid var(--ui-border); border-radius: calc(var(--ui-radius) + 2px); box-shadow: var(--ui-shadow);
|
|
9
|
+
transform-origin: var(--transform-origin);
|
|
10
|
+
&[data-state=open] { animation: ui-menu-in 120ms ease-out; }
|
|
11
|
+
&[data-state=closed] { animation: ui-menu-out 100ms ease-in forwards; }
|
|
12
|
+
}
|
|
13
|
+
[data-scope=menu]:is([data-part=item], [data-part=option-item], [data-part=trigger-item]) {
|
|
14
|
+
display: flex; align-items: center; gap: 0.5rem; padding: 0 0.5rem; height: 2rem; border-radius: 4px; font-size: 13px;
|
|
15
|
+
color: inherit; text-decoration: none; cursor: pointer; user-select: none; outline: none;
|
|
16
|
+
&[data-highlighted], &[data-state=open] { background: var(--ui-hover); }
|
|
17
|
+
&[data-disabled] { opacity: 0.4; cursor: not-allowed; }
|
|
18
|
+
&[data-danger] { color: var(--ui-danger); }
|
|
19
|
+
}
|
|
20
|
+
[data-scope=menu][data-part=icon] { display: block; font-size: 15px; color: var(--ui-muted); flex-shrink: 0;
|
|
21
|
+
[data-danger] > & { color: inherit; } }
|
|
22
|
+
[data-scope=menu][data-part=label] { flex: 1; }
|
|
23
|
+
[data-scope=menu][data-part=shortcut] { margin-left: 1rem; color: var(--ui-muted); font-size: 11px; font-family: inherit; }
|
|
24
|
+
[data-scope=menu][data-part=indicator] { display: grid; place-items: center; width: 15px; height: 15px; flex-shrink: 0; }
|
|
25
|
+
/* Checked marks in the accent: a tick, and a dot in a soft halo. Zag hides them when unchecked. */
|
|
26
|
+
[data-scope=menu][data-part=item-indicator] {
|
|
27
|
+
&[data-type=checkbox] { display: block; font-size: 14px; color: var(--ui-accent); }
|
|
28
|
+
&[data-type=radio] { display: block; width: 6px; height: 6px; border-radius: 9999px; background: var(--ui-accent); box-shadow: 0 0 0 3px var(--ui-accent-soft); } }
|
|
29
|
+
[data-scope=menu][data-part=chevron] { display: block; font-size: 14px; margin-left: auto; color: var(--ui-muted); }
|
|
30
|
+
/* A div or an <hr> (Ark's): no border of its own. */
|
|
31
|
+
[data-scope=menu][data-part=separator] { display: block; height: 1px; margin: 0.25rem -0.25rem; border: none; background: var(--ui-border); }
|
|
32
|
+
[data-scope=menu]:is([data-part=group-label], [data-part=item-group-label], [data-part=menu-label]) {
|
|
33
|
+
padding: 0.5rem 0.5rem 0.25rem; font-size: 12px; font-weight: 500; color: var(--ui-muted); }
|
|
34
|
+
/* The menu's own label reads a little stronger than its group headings. */
|
|
35
|
+
[data-scope=menu][data-part=menu-label] { padding-top: 0.375rem; color: var(--ui-text); font-weight: 600; }
|
|
36
|
+
@keyframes ui-menu-in { from { opacity: 0; transform: translateY(-4px) scale(0.98); } }
|
|
37
|
+
@keyframes ui-menu-out { to { opacity: 0; transform: scale(0.98); } }
|
|
38
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The browser's own select, styled like our inputs: the phone's own picker,
|
|
3
|
+
* long lists without search, and plain form posts.
|
|
4
|
+
*
|
|
5
|
+
* <NativeSelect label="Species" items={['Dog', 'Cat', 'Rabbit']} value={species} onValueChange={setSpecies} />
|
|
6
|
+
*
|
|
7
|
+
* Inside a Field it takes the field's label, hint and error.
|
|
8
|
+
*
|
|
9
|
+
* - `items`: strings, or objects (see `labelKey`, `valueKey`, `disabledKey`;
|
|
10
|
+
* a `group` puts them under that heading)
|
|
11
|
+
* - `value`, `defaultValue`: the item's own value; `onValueChange`: called with it
|
|
12
|
+
* - `placeholder`: an empty first option (it can be chosen to clear it)
|
|
13
|
+
* - `label`: a label above it (skipped inside a Field); `size`: 'sm', 'md' or 'lg'
|
|
14
|
+
* - `name`, `required`, `disabled`: as on a <select>
|
|
15
|
+
*/
|
|
16
|
+
import { type ReactNode, type SelectHTMLAttributes } from 'react';
|
|
17
|
+
import 'iconify-icon';
|
|
18
|
+
import { type Classes } from '../utils.js';
|
|
19
|
+
import '../theme.css';
|
|
20
|
+
import './native-select.css';
|
|
21
|
+
type Value<T, K extends string> = T extends Record<string, unknown> ? T[K] : T;
|
|
22
|
+
export type NativeSelectProps<T, K extends string = 'value'> = Omit<SelectHTMLAttributes<HTMLSelectElement>, 'value' | 'defaultValue' | 'size'> & {
|
|
23
|
+
items: readonly T[];
|
|
24
|
+
value?: NoInfer<Value<T, K>> | null;
|
|
25
|
+
defaultValue?: NoInfer<Value<T, K>> | null;
|
|
26
|
+
onValueChange?: (value: NoInfer<Value<T, K>> | null) => void;
|
|
27
|
+
placeholder?: string | null;
|
|
28
|
+
label?: ReactNode;
|
|
29
|
+
labelKey?: string;
|
|
30
|
+
valueKey?: K;
|
|
31
|
+
disabledKey?: string;
|
|
32
|
+
size?: 'sm' | 'md' | 'lg';
|
|
33
|
+
classes?: Classes<'root' | 'label' | 'control' | 'select' | 'indicator'>;
|
|
34
|
+
};
|
|
35
|
+
export declare function NativeSelect<T, K extends string = 'value'>({ items, value, defaultValue, onValueChange, placeholder, label, labelKey, valueKey, disabledKey, size, className, classes, ...rest }: NativeSelectProps<T, K>): import("react").JSX.Element;
|
|
36
|
+
export {};
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* The browser's own select, styled like our inputs: the phone's own picker,
|
|
4
|
+
* long lists without search, and plain form posts.
|
|
5
|
+
*
|
|
6
|
+
* <NativeSelect label="Species" items={['Dog', 'Cat', 'Rabbit']} value={species} onValueChange={setSpecies} />
|
|
7
|
+
*
|
|
8
|
+
* Inside a Field it takes the field's label, hint and error.
|
|
9
|
+
*
|
|
10
|
+
* - `items`: strings, or objects (see `labelKey`, `valueKey`, `disabledKey`;
|
|
11
|
+
* a `group` puts them under that heading)
|
|
12
|
+
* - `value`, `defaultValue`: the item's own value; `onValueChange`: called with it
|
|
13
|
+
* - `placeholder`: an empty first option (it can be chosen to clear it)
|
|
14
|
+
* - `label`: a label above it (skipped inside a Field); `size`: 'sm', 'md' or 'lg'
|
|
15
|
+
* - `name`, `required`, `disabled`: as on a <select>
|
|
16
|
+
*/
|
|
17
|
+
import { useId } from 'react';
|
|
18
|
+
import { Field, useFieldContext } from '@ark-ui/react/field';
|
|
19
|
+
import 'iconify-icon';
|
|
20
|
+
import { cx, partsOf, useControllable } from '../utils.js';
|
|
21
|
+
import '../theme.css';
|
|
22
|
+
import './native-select.css';
|
|
23
|
+
export function NativeSelect({ items, value, defaultValue, onValueChange, placeholder = null, label = null, labelKey = 'label', valueKey = 'value', disabledKey = 'disabled', size = 'md', className, classes, ...rest }) {
|
|
24
|
+
const field = useFieldContext();
|
|
25
|
+
const part = partsOf('native-select', classes);
|
|
26
|
+
const id = useId();
|
|
27
|
+
const [current, setCurrent] = useControllable(value, defaultValue ?? null, onValueChange);
|
|
28
|
+
const key = (item, k) => item[k];
|
|
29
|
+
const isObject = (item) => typeof item === 'object' && item !== null;
|
|
30
|
+
const labelOf = (item) => String(isObject(item) ? key(item, labelKey) : item);
|
|
31
|
+
const valueOf = (item) => (isObject(item) ? key(item, valueKey) : item);
|
|
32
|
+
const groupOf = (item) => (isObject(item) ? key(item, 'group') : undefined);
|
|
33
|
+
const option = (item) => (_jsx("option", { value: String(valueOf(item)), disabled: isObject(item) && !!key(item, disabledKey), children: labelOf(item) }, String(valueOf(item))));
|
|
34
|
+
// Grouped items under their headings, in order of first appearance.
|
|
35
|
+
const groups = [...new Set(items.map(groupOf))];
|
|
36
|
+
// <select> values are strings: map back to the item's own.
|
|
37
|
+
function change(raw) {
|
|
38
|
+
const item = items.find((i) => String(valueOf(i)) === raw);
|
|
39
|
+
setCurrent(item === undefined ? null : valueOf(item));
|
|
40
|
+
}
|
|
41
|
+
const props = {
|
|
42
|
+
...rest, ...part('select'), value: current == null ? '' : String(current),
|
|
43
|
+
onChange: (e) => { rest.onChange?.(e); change(e.target.value); },
|
|
44
|
+
};
|
|
45
|
+
const options = _jsxs(_Fragment, { children: [placeholder !== null && _jsx("option", { value: "", children: placeholder }), groups.length > 1 || groups[0]
|
|
46
|
+
? groups.map((group) => group
|
|
47
|
+
? _jsx("optgroup", { label: group, children: items.filter((i) => groupOf(i) === group).map(option) }, group)
|
|
48
|
+
: items.filter((i) => !groupOf(i)).map(option))
|
|
49
|
+
: items.map(option)] });
|
|
50
|
+
return (_jsxs("div", { ...part('root'), className: cx(className, classes?.root), "data-size": size, children: [label && !field && _jsx("label", { ...part('label'), htmlFor: id, children: label }), _jsxs("div", { ...part('control'), children: [field ? _jsx(Field.Select, { ...props, children: options }) : _jsx("select", { id: id, ...props, children: options }), _jsx("iconify-icon", { ...part('indicator'), icon: "lucide:chevron-down", "aria-hidden": "true" })] })] }));
|
|
51
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
@layer ui {
|
|
2
|
+
[data-scope=native-select][data-part=root] { display: block; min-width: 0; color: var(--ui-text); font-family: var(--ui-font); }
|
|
3
|
+
[data-scope=native-select][data-part=label] { display: block; font-size: 14px; font-weight: 500; margin-bottom: 0.375rem; }
|
|
4
|
+
[data-scope=native-select][data-part=control] { position: relative; display: flex; align-items: center; min-width: 0; }
|
|
5
|
+
[data-scope=native-select][data-part=select] { appearance: none; width: 100%; min-width: 0; box-sizing: border-box;
|
|
6
|
+
height: var(--ui-control-height); padding: 0 2rem 0 0.75rem; background: var(--ui-surface); border: 1px solid var(--ui-border);
|
|
7
|
+
border-radius: var(--ui-radius); color: var(--ui-text); font-family: var(--ui-font); font-size: 14px; cursor: pointer;
|
|
8
|
+
&:focus-visible { border-color: var(--ui-ring); outline: 2px solid var(--ui-ring-soft); }
|
|
9
|
+
&[aria-invalid=true], &[data-invalid] { border-color: var(--ui-danger); }
|
|
10
|
+
&:disabled { opacity: 0.5; cursor: not-allowed; }
|
|
11
|
+
/* The placeholder (the empty value) looks like one. */
|
|
12
|
+
&:has(option[value=""]:checked) { color: var(--ui-muted); }
|
|
13
|
+
[data-size=sm] > [data-part=control] > & { height: var(--ui-control-height-sm); padding-left: 0.625rem; font-size: 13px; }
|
|
14
|
+
[data-size=lg] > [data-part=control] > & { height: var(--ui-control-height-lg); font-size: 15px; } }
|
|
15
|
+
[data-scope=native-select][data-part=indicator] { position: absolute; right: 0.625rem; color: var(--ui-muted); font-size: 16px; pointer-events: none; }
|
|
16
|
+
}
|