@versini/ui-menu 5.3.4 โ†’ 6.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 CHANGED
@@ -3,9 +3,9 @@
3
3
  [![npm version](https://img.shields.io/npm/v/@versini/ui-menu?style=flat-square)](https://www.npmjs.com/package/@versini/ui-menu)
4
4
  ![npm package minimized gzipped size](<https://img.shields.io/bundlejs/size/%40versini%2Fui-menu?style=flat-square&label=size%20(gzip)>)
5
5
 
6
- > Accessible and flexible React menu components built with TypeScript and TailwindCSS.
6
+ > Accessible and lightweight React menu components built with TypeScript and TailwindCSS โ€” no external UI library required.
7
7
 
8
- The Menu package provides dropdown menus and navigation components with full keyboard navigation, focus management, theming for triggers, and composable items / separators.
8
+ The Menu package provides dropdown menus with full keyboard navigation, focus management, theming for triggers, and composable items / separators. It offers the same capabilities as `@versini/ui-dropdown` but with a smaller footprint by replacing Radix UI with a custom implementation using the native Popover API.
9
9
 
10
10
  ## Table of Contents
11
11
 
@@ -17,13 +17,14 @@ The Menu package provides dropdown menus and navigation components with full key
17
17
 
18
18
  ## Features
19
19
 
20
- - **๐Ÿ“‹ Composable**: `Menu`, `MenuItem`, `MenuSeparator`, `MenuGroupLabel`
21
- - **๐Ÿ”„ Nested Sub-menus**: Support for multi-level menu hierarchies with automatic positioning
22
- - **โ™ฟ Accessible**: Built with Floating UI & ARIA roles for robust a11y
23
- - **โŒจ๏ธ Keyboard Support**: Arrow navigation, typeahead matching, ESC / click outside close
24
- - **๐ŸŽจ Theme & Focus Modes**: Trigger inherits color + separate focus styling
25
- - **๐Ÿงญ Smart Positioning**: Auto flip / shift to remain within viewport, responsive spacing
26
- - **๐Ÿงช Type Safe**: Strongly typed props & label-based typeahead
20
+ - **Composable**: `Menu`, `MenuItem`, `MenuSeparator`, `MenuGroupLabel`, `MenuSub`
21
+ - **Nested Sub-menus**: Support for multi-level menu hierarchies with automatic positioning
22
+ - **Accessible**: Built with ARIA roles & WAI-ARIA menu patterns for robust a11y
23
+ - **Keyboard Support**: Arrow navigation, ESC / click outside to close
24
+ - **Theme & Focus Modes**: Trigger inherits color + separate focus styling
25
+ - **Smart Positioning**: Auto flip / shift to remain within viewport
26
+ - **Type Safe**: Strongly typed props with TypeScript
27
+ - **Lightweight**: No Radix UI dependency โ€” uses the native Popover API
27
28
 
28
29
  ## Installation
29
30
 
@@ -51,9 +52,18 @@ function App() {
51
52
  </ButtonIcon>
52
53
  }
53
54
  >
54
- <MenuItem label="Profile" onClick={() => console.log("Profile")} />
55
- <MenuItem label="Settings" onClick={() => console.log("Settings")} />
56
- <MenuItem label="Logout" onClick={() => console.log("Logout")} />
55
+ <MenuItem
56
+ label="Profile"
57
+ onSelect={() => console.info("Profile")}
58
+ />
59
+ <MenuItem
60
+ label="Settings"
61
+ onSelect={() => console.info("Settings")}
62
+ />
63
+ <MenuItem
64
+ label="Logout"
65
+ onSelect={() => console.info("Logout")}
66
+ />
57
67
  </Menu>
58
68
  );
59
69
  }
@@ -64,7 +74,11 @@ function App() {
64
74
  ### Menu with Icons & Selection
65
75
 
66
76
  ```tsx
67
- import { Menu, MenuItem } from "@versini/ui-menu";
77
+ import {
78
+ Menu,
79
+ MenuItem,
80
+ MenuSeparator
81
+ } from "@versini/ui-menu";
68
82
  import { ButtonIcon } from "@versini/ui-button";
69
83
  import {
70
84
  IconMenu,
@@ -83,23 +97,23 @@ function AccountMenu() {
83
97
  <IconMenu />
84
98
  </ButtonIcon>
85
99
  }
86
- onOpenChange={(o) => console.log("open?", o)}
100
+ onOpenChange={(o) => console.info("open?", o)}
87
101
  >
88
102
  <MenuItem
89
103
  label="Profile"
90
104
  icon={<IconUser />}
91
- onClick={() => setLast("profile")}
105
+ onSelect={() => setLast("profile")}
92
106
  />
93
107
  <MenuItem
94
108
  label="Settings"
95
109
  icon={<IconSettings />}
96
- onClick={() => setLast("settings")}
110
+ onSelect={() => setLast("settings")}
97
111
  />
98
112
  <MenuSeparator />
99
113
  <MenuItem
100
114
  label="Logout"
101
115
  icon={<IconLogout />}
102
- onClick={() => setLast("logout")}
116
+ onSelect={() => setLast("logout")}
103
117
  />
104
118
  </Menu>
105
119
  );
@@ -127,10 +141,15 @@ function AccountMenu() {
127
141
 
128
142
  ### Nested Sub-menus
129
143
 
130
- Create hierarchical menus by nesting `Menu` components. Simply use a `Menu` with a `label` prop but no `trigger` to create a sub-menu:
144
+ Create hierarchical menus using `MenuSub`:
131
145
 
132
146
  ```tsx
133
- import { Menu, MenuItem, MenuGroupLabel } from "@versini/ui-menu";
147
+ import {
148
+ Menu,
149
+ MenuItem,
150
+ MenuSub,
151
+ MenuGroupLabel
152
+ } from "@versini/ui-menu";
134
153
  import { ButtonIcon } from "@versini/ui-button";
135
154
  import { IconSettings, IconOpenAI, IconAnthropic } from "@versini/ui-icons";
136
155
 
@@ -148,22 +167,24 @@ function SettingsMenu() {
148
167
  <MenuItem label="Profile" />
149
168
  <MenuItem label="Preferences" />
150
169
 
151
- {/* Nested sub-menu */}
152
- <Menu label="AI Settings">
153
- <MenuGroupLabel>Engines</MenuGroupLabel>
170
+ {/* Nested sub-menu with icon */}
171
+ <MenuSub label="AI Settings" icon={<IconSettings />}>
172
+ <MenuGroupLabel icon={<IconSettings />}>
173
+ Engines
174
+ </MenuGroupLabel>
154
175
  <MenuItem
155
176
  label="OpenAI"
156
177
  icon={<IconOpenAI />}
157
178
  selected={engine === "openai"}
158
- onClick={() => setEngine("openai")}
179
+ onSelect={() => setEngine("openai")}
159
180
  />
160
181
  <MenuItem
161
182
  label="Anthropic"
162
183
  icon={<IconAnthropic />}
163
184
  selected={engine === "anthropic"}
164
- onClick={() => setEngine("anthropic")}
185
+ onSelect={() => setEngine("anthropic")}
165
186
  />
166
- </Menu>
187
+ </MenuSub>
167
188
 
168
189
  <MenuItem label="About" />
169
190
  </Menu>
@@ -174,7 +195,7 @@ function SettingsMenu() {
174
195
  **Features of nested sub-menus:**
175
196
 
176
197
  - Automatically positioned to the right (or left if no space)
177
- - Visual chevron indicator (`โ†’`) shows expandable items
198
+ - Visual chevron indicator shows expandable items
178
199
  - Hover or click to open sub-menus
179
200
  - Smart positioning adjusts for viewport constraints
180
201
  - Keyboard navigation works across all levels
@@ -184,38 +205,50 @@ function SettingsMenu() {
184
205
 
185
206
  ### Menu Props
186
207
 
187
- | Prop | Type | Default | Description |
188
- | ------------------ | ----------------------------------------------- | ---------------- | ---------------------------------------------------------------------------------------------- |
189
- | `trigger` | `React.ReactNode` | - | Element used to open the menu (Button / ButtonIcon / custom). Optional for nested sub-menus. |
190
- | `children` | `React.ReactNode` | - | One or more `MenuItem` / `MenuSeparator` / `Menu` (for sub-menus) / custom nodes. |
191
- | `label` | `string` | `"Open menu"` | Accessible label for the trigger. When used without `trigger`, creates a nested sub-menu item. |
192
- | `defaultPlacement` | `Placement` (Floating UI) | `"bottom-start"` | Initial preferred placement (only applies to root menu). |
193
- | `mode` | `"dark" \| "light" \| "system" \| "alt-system"` | `"system"` | Color mode of trigger (when using UI buttons). |
194
- | `focusMode` | `"dark" \| "light" \| "system" \| "alt-system"` | `"system"` | Focus ring thematic mode (when using UI buttons). |
195
- | `onOpenChange` | `(open: boolean) => void` | - | Called when menu opens or closes. |
196
-
197
- **Creating nested sub-menus:**
198
-
199
- - Use `Menu` with `label` but without `trigger` to create a sub-menu item
200
- - Sub-menus automatically show a chevron (`โ†’`) indicator
201
- - Positioning is automatically handled (right-start, flips to left if needed)
202
- - Hover or click to open nested menus
208
+ | Prop | Type | Default | Description |
209
+ | ------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------- | ---------------- | ---------------------------------------------- |
210
+ | `trigger` | `React.ReactElement` | - | Element used to open the menu (Button, etc.). |
211
+ | `children` | `React.ReactNode` | - | MenuItem, MenuSeparator, etc. |
212
+ | `label` | `string` | `"Open menu"` | Accessible label for the trigger. |
213
+ | `defaultPlacement` | `"bottom"` \| `"bottom-start"` \| `"bottom-end"` \| `"top"` \| `"top-start"` \| `"top-end"` \| `"left"` \| `"left-start"` \| `"right"` \| etc. | `"bottom-start"` | Initial preferred placement. |
214
+ | `mode` | `"dark"` \| `"light"` \| `"system"` \| `"alt-system"` | `"system"` | Color mode of trigger (when using UI buttons). |
215
+ | `focusMode` | `"dark"` \| `"light"` \| `"system"` \| `"alt-system"` | `"system"` | Focus ring thematic mode. |
216
+ | `onOpenChange` | `(open: boolean) => void` | - | Called when menu opens or closes. |
217
+ | `sideOffset` | `number` | `10` | Offset distance from the trigger element. |
203
218
 
204
219
  ### MenuItem Props
205
220
 
206
- | Prop | Type | Default | Description |
207
- | ------------- | ----------------- | ------- | ---------------------------------------------------- |
208
- | `label` | `string` | - | Text label of the item (used for typeahead). |
209
- | `disabled` | `boolean` | `false` | Disable item interaction. |
210
- | `icon` | `React.ReactNode` | - | Icon element displayed before label. |
211
- | `raw` | `boolean` | `false` | Render custom content (bypasses built-in button). |
212
- | `ignoreClick` | `boolean` | `false` | Prevent auto menu close on click. |
213
- | `selected` | `boolean` | `false` | Visually indicate selected state (shows check icon). |
214
-
215
- ### MenuSeparator
216
-
217
- Simple visual separator between items.
218
-
219
- ### MenuGroupLabel
220
-
221
- Optional label element to group logical sets; pass standard `div` attributes.
221
+ | Prop | Type | Default | Description |
222
+ | -------------- | ------------------------ | ----------- | --------------------------------------------- |
223
+ | `label` | `string` | - | The label to display for the menu item. |
224
+ | `disabled` | `boolean` | `false` | Whether the menu item is disabled. |
225
+ | `icon` | `React.ReactNode` | - | Icon to display on the left of the label. |
226
+ | `raw` | `boolean` | `false` | Disable internal styling for custom content. |
227
+ | `ignoreClick` | `boolean` | `false` | Prevent menu from closing when item selected. |
228
+ | `selected` | `boolean` | `undefined` | Show selected/unselected indicator. |
229
+ | `onSelect` | `(event: Event) => void` | - | Callback fired when the item is selected. |
230
+ | `onClick` | `(event) => void` | - | Optional click handler. |
231
+ | `onFocus` | `(event) => void` | - | Optional focus handler. |
232
+ | `onMouseEnter` | `(event) => void` | - | Optional mouse enter handler. |
233
+
234
+ ### MenuSub Props
235
+
236
+ | Prop | Type | Default | Description |
237
+ | ------------ | ----------------- | ------- | ----------------------------------------- |
238
+ | `label` | `string` | - | The label for the sub-menu trigger. |
239
+ | `icon` | `React.ReactNode` | - | Icon to display on the left of the label. |
240
+ | `children` | `React.ReactNode` | - | Items to render inside sub-menu. |
241
+ | `disabled` | `boolean` | `false` | Whether the sub-menu is disabled. |
242
+ | `sideOffset` | `number` | `14` | Offset from sub-menu trigger. |
243
+
244
+ ### MenuSeparator Props
245
+
246
+ Standard `React.HTMLAttributes<HTMLDivElement>` - use `className` for custom styling.
247
+
248
+ ### MenuGroupLabel Props
249
+
250
+ | Prop | Type | Default | Description |
251
+ | ----------- | ----------------- | ------- | ----------------------------------------- |
252
+ | `icon` | `React.ReactNode` | - | Icon to display on the left of the label. |
253
+ | `children` | `React.ReactNode` | - | The label content. |
254
+ | `className` | `string` | - | Custom CSS class for styling. |
package/dist/index.d.ts CHANGED
@@ -1,13 +1,26 @@
1
1
  import { JSX } from 'react/jsx-runtime';
2
- import type { Placement } from '@floating-ui/react';
3
- import { default as React_2 } from 'react';
4
- import * as React_3 from 'react';
5
2
 
6
- export declare const Menu: React_2.ForwardRefExoticComponent<Omit<MenuProps & React_2.HTMLProps<HTMLButtonElement>, "ref"> & React_2.RefAttributes<HTMLButtonElement>>;
3
+ export declare const Menu: {
4
+ ({ trigger, children, label, defaultPlacement, onOpenChange, mode, focusMode, sideOffset, }: MenuProps): JSX.Element;
5
+ displayName: string;
6
+ };
7
+
8
+ export declare const MenuGroupLabel: {
9
+ ({ className, icon, children, ...props }: MenuGroupLabelProps): JSX.Element;
10
+ displayName: string;
11
+ };
7
12
 
8
- export declare const MenuGroupLabel: ({ className, ...props }: React_3.HTMLAttributes<HTMLDivElement>) => JSX.Element;
13
+ declare type MenuGroupLabelProps = {
14
+ /**
15
+ * A React component of type Icon to be placed on the left of the label.
16
+ */
17
+ icon?: React.ReactNode;
18
+ } & React.HTMLAttributes<HTMLDivElement>;
9
19
 
10
- export declare const MenuItem: React_3.ForwardRefExoticComponent<MenuItemProps & React_3.ButtonHTMLAttributes<HTMLButtonElement> & React_3.RefAttributes<HTMLButtonElement>>;
20
+ export declare const MenuItem: {
21
+ ({ label, disabled, icon, raw, children, ignoreClick, selected, onSelect, onClick, onFocus, onMouseEnter, ...props }: MenuItemProps): JSX.Element;
22
+ displayName: string;
23
+ };
11
24
 
12
25
  declare type MenuItemProps = {
13
26
  /**
@@ -22,12 +35,16 @@ declare type MenuItemProps = {
22
35
  /**
23
36
  * A React component of type Icon to be placed on the left of the label.
24
37
  */
25
- icon?: React_2.ReactNode;
38
+ icon?: React.ReactNode;
26
39
  /**
27
40
  * Disable internal menu item behavior (click, focus, etc.).
28
41
  * @default false
29
42
  */
30
43
  raw?: boolean;
44
+ /**
45
+ * Children to render when using raw mode.
46
+ */
47
+ children?: React.ReactNode;
31
48
  /**
32
49
  * Whether or not the menu should close when the menu item is selected.
33
50
  * @default false
@@ -35,26 +52,41 @@ declare type MenuItemProps = {
35
52
  ignoreClick?: boolean;
36
53
  /**
37
54
  * Whether or not the menu item is selected.
38
- * @default false
55
+ * @default undefined
39
56
  */
40
57
  selected?: boolean;
58
+ /**
59
+ * Callback fired when the menu item is selected.
60
+ */
61
+ onSelect?: (event: Event) => void;
62
+ /**
63
+ * Optional click handler.
64
+ */
65
+ onClick?: (event: React.MouseEvent<HTMLDivElement>) => void;
66
+ /**
67
+ * Optional focus handler.
68
+ */
69
+ onFocus?: (event: React.FocusEvent<HTMLDivElement>) => void;
70
+ /**
71
+ * Optional mouse enter handler.
72
+ */
73
+ onMouseEnter?: (event: React.MouseEvent<HTMLDivElement>) => void;
41
74
  };
42
75
 
43
76
  declare type MenuProps = {
44
77
  /**
45
78
  * The component to use to open the menu, e.g. a ButtonIcon, a Button, etc.
46
- * Required for root menus, omit for nested sub-menus (use label instead).
47
79
  */
48
- trigger?: React_2.ReactNode;
80
+ trigger?: React.ReactElement;
49
81
  /**
50
- * The children to render.
82
+ * The children to render (MenuItem, MenuSeparator, etc.).
51
83
  */
52
- children?: React_2.ReactNode;
84
+ children?: React.ReactNode;
53
85
  /**
54
86
  * The default location of the popup.
55
87
  * @default "bottom-start"
56
88
  */
57
- defaultPlacement?: Placement;
89
+ defaultPlacement?: "bottom" | "bottom-start" | "bottom-end" | "top" | "top-start" | "top-end" | "left" | "left-start" | "left-end" | "right" | "right-start" | "right-end";
58
90
  /**
59
91
  * The type of focus for the Button. This will change the color
60
92
  * of the focus ring around the Button.
@@ -65,8 +97,7 @@ declare type MenuProps = {
65
97
  */
66
98
  mode?: "dark" | "light" | "system" | "alt-system";
67
99
  /**
68
- * The label to use for the menu button (root menu) or the sub-menu trigger text (nested menu).
69
- * When used without a trigger, this creates a nested sub-menu.
100
+ * The label to use for the menu button.
70
101
  */
71
102
  label?: string;
72
103
  /**
@@ -74,10 +105,48 @@ declare type MenuProps = {
74
105
  * @param open whether or not the menu is open
75
106
  */
76
107
  onOpenChange?: (open: boolean) => void;
108
+ /**
109
+ * The offset distance from the trigger element.
110
+ * @default 10
111
+ */
112
+ sideOffset?: number;
113
+ };
114
+
115
+ export declare const MenuSeparator: {
116
+ ({ className, ...props }: MenuSeparatorProps): JSX.Element;
117
+ displayName: string;
77
118
  };
78
119
 
79
- export declare const MenuSeparator: ({ className, ...props }: MenuSeparatorProps) => JSX.Element;
120
+ declare type MenuSeparatorProps = React.HTMLAttributes<HTMLDivElement>;
121
+
122
+ export declare const MenuSub: {
123
+ ({ label, icon, children, disabled, sideOffset, }: MenuSubProps): JSX.Element;
124
+ displayName: string;
125
+ };
80
126
 
81
- declare type MenuSeparatorProps = React_2.HTMLAttributes<HTMLDivElement>;
127
+ declare type MenuSubProps = {
128
+ /**
129
+ * The label for the sub-menu trigger.
130
+ */
131
+ label: string;
132
+ /**
133
+ * A React component of type Icon to be placed on the left of the label.
134
+ */
135
+ icon?: React.ReactNode;
136
+ /**
137
+ * The children to render inside the sub-menu.
138
+ */
139
+ children?: React.ReactNode;
140
+ /**
141
+ * Whether the sub-menu trigger is disabled.
142
+ * @default false
143
+ */
144
+ disabled?: boolean;
145
+ /**
146
+ * The offset distance from the sub-menu trigger.
147
+ * @default 14
148
+ */
149
+ sideOffset?: number;
150
+ };
82
151
 
83
152
  export { }