@versini/ui-menu 5.3.4 โ†’ 6.1.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 the now deprecated `@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,9 @@ 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 label="Profile" onSelect={() => console.info("Profile")} />
56
+ <MenuItem label="Settings" onSelect={() => console.info("Settings")} />
57
+ <MenuItem label="Logout" onSelect={() => console.info("Logout")} />
57
58
  </Menu>
58
59
  );
59
60
  }
@@ -64,7 +65,7 @@ function App() {
64
65
  ### Menu with Icons & Selection
65
66
 
66
67
  ```tsx
67
- import { Menu, MenuItem } from "@versini/ui-menu";
68
+ import { Menu, MenuItem, MenuSeparator } from "@versini/ui-menu";
68
69
  import { ButtonIcon } from "@versini/ui-button";
69
70
  import {
70
71
  IconMenu,
@@ -83,23 +84,23 @@ function AccountMenu() {
83
84
  <IconMenu />
84
85
  </ButtonIcon>
85
86
  }
86
- onOpenChange={(o) => console.log("open?", o)}
87
+ onOpenChange={(o) => console.info("open?", o)}
87
88
  >
88
89
  <MenuItem
89
90
  label="Profile"
90
91
  icon={<IconUser />}
91
- onClick={() => setLast("profile")}
92
+ onSelect={() => setLast("profile")}
92
93
  />
93
94
  <MenuItem
94
95
  label="Settings"
95
96
  icon={<IconSettings />}
96
- onClick={() => setLast("settings")}
97
+ onSelect={() => setLast("settings")}
97
98
  />
98
99
  <MenuSeparator />
99
100
  <MenuItem
100
101
  label="Logout"
101
102
  icon={<IconLogout />}
102
- onClick={() => setLast("logout")}
103
+ onSelect={() => setLast("logout")}
103
104
  />
104
105
  </Menu>
105
106
  );
@@ -127,10 +128,10 @@ function AccountMenu() {
127
128
 
128
129
  ### Nested Sub-menus
129
130
 
130
- Create hierarchical menus by nesting `Menu` components. Simply use a `Menu` with a `label` prop but no `trigger` to create a sub-menu:
131
+ Create hierarchical menus using `MenuSub`:
131
132
 
132
133
  ```tsx
133
- import { Menu, MenuItem, MenuGroupLabel } from "@versini/ui-menu";
134
+ import { Menu, MenuItem, MenuSub, MenuGroupLabel } from "@versini/ui-menu";
134
135
  import { ButtonIcon } from "@versini/ui-button";
135
136
  import { IconSettings, IconOpenAI, IconAnthropic } from "@versini/ui-icons";
136
137
 
@@ -148,22 +149,22 @@ function SettingsMenu() {
148
149
  <MenuItem label="Profile" />
149
150
  <MenuItem label="Preferences" />
150
151
 
151
- {/* Nested sub-menu */}
152
- <Menu label="AI Settings">
153
- <MenuGroupLabel>Engines</MenuGroupLabel>
152
+ {/* Nested sub-menu with icon */}
153
+ <MenuSub label="AI Settings" icon={<IconSettings />}>
154
+ <MenuGroupLabel icon={<IconSettings />}>Engines</MenuGroupLabel>
154
155
  <MenuItem
155
156
  label="OpenAI"
156
157
  icon={<IconOpenAI />}
157
158
  selected={engine === "openai"}
158
- onClick={() => setEngine("openai")}
159
+ onSelect={() => setEngine("openai")}
159
160
  />
160
161
  <MenuItem
161
162
  label="Anthropic"
162
163
  icon={<IconAnthropic />}
163
164
  selected={engine === "anthropic"}
164
- onClick={() => setEngine("anthropic")}
165
+ onSelect={() => setEngine("anthropic")}
165
166
  />
166
- </Menu>
167
+ </MenuSub>
167
168
 
168
169
  <MenuItem label="About" />
169
170
  </Menu>
@@ -174,7 +175,7 @@ function SettingsMenu() {
174
175
  **Features of nested sub-menus:**
175
176
 
176
177
  - Automatically positioned to the right (or left if no space)
177
- - Visual chevron indicator (`โ†’`) shows expandable items
178
+ - Visual chevron indicator shows expandable items
178
179
  - Hover or click to open sub-menus
179
180
  - Smart positioning adjusts for viewport constraints
180
181
  - Keyboard navigation works across all levels
@@ -184,38 +185,50 @@ function SettingsMenu() {
184
185
 
185
186
  ### Menu Props
186
187
 
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
188
+ | Prop | Type | Default | Description |
189
+ | ------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------- | ---------------- | ---------------------------------------------- |
190
+ | `trigger` | `React.ReactElement` | - | Element used to open the menu (Button, etc.). |
191
+ | `children` | `React.ReactNode` | - | MenuItem, MenuSeparator, etc. |
192
+ | `label` | `string` | `"Open menu"` | Accessible label for the trigger. |
193
+ | `defaultPlacement` | `"bottom"` \| `"bottom-start"` \| `"bottom-end"` \| `"top"` \| `"top-start"` \| `"top-end"` \| `"left"` \| `"left-start"` \| `"right"` \| etc. | `"bottom-start"` | Initial preferred placement. |
194
+ | `mode` | `"dark"` \| `"light"` \| `"system"` \| `"alt-system"` | `"system"` | Color mode of trigger (when using UI buttons). |
195
+ | `focusMode` | `"dark"` \| `"light"` \| `"system"` \| `"alt-system"` | `"system"` | Focus ring thematic mode. |
196
+ | `onOpenChange` | `(open: boolean) => void` | - | Called when menu opens or closes. |
197
+ | `sideOffset` | `number` | `10` | Offset distance from the trigger element. |
203
198
 
204
199
  ### MenuItem Props
205
200
 
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.
201
+ | Prop | Type | Default | Description |
202
+ | -------------- | ------------------------ | ----------- | --------------------------------------------- |
203
+ | `label` | `string` | - | The label to display for the menu item. |
204
+ | `disabled` | `boolean` | `false` | Whether the menu item is disabled. |
205
+ | `icon` | `React.ReactNode` | - | Icon to display on the left of the label. |
206
+ | `raw` | `boolean` | `false` | Disable internal styling for custom content. |
207
+ | `ignoreClick` | `boolean` | `false` | Prevent menu from closing when item selected. |
208
+ | `selected` | `boolean` | `undefined` | Show selected/unselected indicator. |
209
+ | `onSelect` | `(event: Event) => void` | - | Callback fired when the item is selected. |
210
+ | `onClick` | `(event) => void` | - | Optional click handler. |
211
+ | `onFocus` | `(event) => void` | - | Optional focus handler. |
212
+ | `onMouseEnter` | `(event) => void` | - | Optional mouse enter handler. |
213
+
214
+ ### MenuSub Props
215
+
216
+ | Prop | Type | Default | Description |
217
+ | ------------ | ----------------- | ------- | ----------------------------------------- |
218
+ | `label` | `string` | - | The label for the sub-menu trigger. |
219
+ | `icon` | `React.ReactNode` | - | Icon to display on the left of the label. |
220
+ | `children` | `React.ReactNode` | - | Items to render inside sub-menu. |
221
+ | `disabled` | `boolean` | `false` | Whether the sub-menu is disabled. |
222
+ | `sideOffset` | `number` | `14` | Offset from sub-menu trigger. |
223
+
224
+ ### MenuSeparator Props
225
+
226
+ Standard `React.HTMLAttributes<HTMLDivElement>` - use `className` for custom styling.
227
+
228
+ ### MenuGroupLabel Props
229
+
230
+ | Prop | Type | Default | Description |
231
+ | ----------- | ----------------- | ------- | ----------------------------------------- |
232
+ | `icon` | `React.ReactNode` | - | Icon to display on the left of the label. |
233
+ | `children` | `React.ReactNode` | - | The label content. |
234
+ | `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 { }