@versini/ui-dropdown 1.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/LICENSE +21 -0
- package/README.md +245 -0
- package/dist/DropdownMenu/DropdownMenu.d.ts +17 -0
- package/dist/DropdownMenu/DropdownMenu.js +183 -0
- package/dist/DropdownMenu/DropdownMenuItem.d.ts +5 -0
- package/dist/DropdownMenu/DropdownMenuItem.js +92 -0
- package/dist/DropdownMenu/DropdownMenuTypes.d.ts +118 -0
- package/dist/DropdownMenu/DropdownMenuTypes.js +21 -0
- package/dist/DropdownMenu/utilities.d.ts +1 -0
- package/dist/DropdownMenu/utilities.js +36 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +24 -0
- package/package.json +54 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) Arno Versini
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
# @versini/ui-dropdown
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/@versini/ui-dropdown)
|
|
4
|
+
>)
|
|
5
|
+
|
|
6
|
+
> Accessible and flexible React dropdown menu components built with TypeScript, TailwindCSS, and Radix UI primitives.
|
|
7
|
+
|
|
8
|
+
The DropdownMenu package provides dropdown menus with full keyboard navigation, focus management, theming for triggers, and composable items / separators.
|
|
9
|
+
|
|
10
|
+
## Table of Contents
|
|
11
|
+
|
|
12
|
+
- [Features](#features)
|
|
13
|
+
- [Installation](#installation)
|
|
14
|
+
- [Usage](#usage)
|
|
15
|
+
- [Examples](#examples)
|
|
16
|
+
- [API](#api)
|
|
17
|
+
|
|
18
|
+
## Features
|
|
19
|
+
|
|
20
|
+
- **📋 Composable**: `DropdownMenu`, `DropdownMenuItem`, `DropdownMenuSeparator`, `DropdownMenuGroupLabel`, `DropdownMenuSub`
|
|
21
|
+
- **🔄 Nested Sub-menus**: Support for multi-level menu hierarchies with automatic positioning
|
|
22
|
+
- **♿ Accessible**: Built with Radix UI primitives & 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
|
|
26
|
+
- **🧪 Type Safe**: Strongly typed props with TypeScript
|
|
27
|
+
|
|
28
|
+
## Installation
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
npm install @versini/ui-dropdown
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
> **Note**: This component requires TailwindCSS and the `@versini/ui-styles` plugin for proper styling. See the [installation documentation](https://versini-org.github.io/ui-components/?path=/docs/getting-started-installation--docs) for complete setup instructions.
|
|
35
|
+
|
|
36
|
+
## Usage
|
|
37
|
+
|
|
38
|
+
### Basic Dropdown Menu
|
|
39
|
+
|
|
40
|
+
```tsx
|
|
41
|
+
import { DropdownMenu, DropdownMenuItem } from "@versini/ui-dropdown";
|
|
42
|
+
import { ButtonIcon } from "@versini/ui-button";
|
|
43
|
+
import { IconMenu } from "@versini/ui-icons";
|
|
44
|
+
|
|
45
|
+
function App() {
|
|
46
|
+
return (
|
|
47
|
+
<DropdownMenu
|
|
48
|
+
trigger={
|
|
49
|
+
<ButtonIcon label="Menu">
|
|
50
|
+
<IconMenu />
|
|
51
|
+
</ButtonIcon>
|
|
52
|
+
}
|
|
53
|
+
>
|
|
54
|
+
<DropdownMenuItem
|
|
55
|
+
label="Profile"
|
|
56
|
+
onSelect={() => console.info("Profile")}
|
|
57
|
+
/>
|
|
58
|
+
<DropdownMenuItem
|
|
59
|
+
label="Settings"
|
|
60
|
+
onSelect={() => console.info("Settings")}
|
|
61
|
+
/>
|
|
62
|
+
<DropdownMenuItem
|
|
63
|
+
label="Logout"
|
|
64
|
+
onSelect={() => console.info("Logout")}
|
|
65
|
+
/>
|
|
66
|
+
</DropdownMenu>
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
## Examples
|
|
72
|
+
|
|
73
|
+
### Menu with Icons & Selection
|
|
74
|
+
|
|
75
|
+
```tsx
|
|
76
|
+
import {
|
|
77
|
+
DropdownMenu,
|
|
78
|
+
DropdownMenuItem,
|
|
79
|
+
DropdownMenuSeparator
|
|
80
|
+
} from "@versini/ui-dropdown";
|
|
81
|
+
import { ButtonIcon } from "@versini/ui-button";
|
|
82
|
+
import {
|
|
83
|
+
IconMenu,
|
|
84
|
+
IconUser,
|
|
85
|
+
IconSettings,
|
|
86
|
+
IconLogout
|
|
87
|
+
} from "@versini/ui-icons";
|
|
88
|
+
|
|
89
|
+
function AccountMenu() {
|
|
90
|
+
const [last, setLast] = useState("");
|
|
91
|
+
return (
|
|
92
|
+
<DropdownMenu
|
|
93
|
+
label="Account options"
|
|
94
|
+
trigger={
|
|
95
|
+
<ButtonIcon label="Account">
|
|
96
|
+
<IconMenu />
|
|
97
|
+
</ButtonIcon>
|
|
98
|
+
}
|
|
99
|
+
onOpenChange={(o) => console.info("open?", o)}
|
|
100
|
+
>
|
|
101
|
+
<DropdownMenuItem
|
|
102
|
+
label="Profile"
|
|
103
|
+
icon={<IconUser />}
|
|
104
|
+
onSelect={() => setLast("profile")}
|
|
105
|
+
/>
|
|
106
|
+
<DropdownMenuItem
|
|
107
|
+
label="Settings"
|
|
108
|
+
icon={<IconSettings />}
|
|
109
|
+
onSelect={() => setLast("settings")}
|
|
110
|
+
/>
|
|
111
|
+
<DropdownMenuSeparator />
|
|
112
|
+
<DropdownMenuItem
|
|
113
|
+
label="Logout"
|
|
114
|
+
icon={<IconLogout />}
|
|
115
|
+
onSelect={() => setLast("logout")}
|
|
116
|
+
/>
|
|
117
|
+
</DropdownMenu>
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
### Raw Custom Item
|
|
123
|
+
|
|
124
|
+
```tsx
|
|
125
|
+
<DropdownMenu
|
|
126
|
+
trigger={
|
|
127
|
+
<ButtonIcon label="More">
|
|
128
|
+
<IconMenu />
|
|
129
|
+
</ButtonIcon>
|
|
130
|
+
}
|
|
131
|
+
>
|
|
132
|
+
<DropdownMenuItem raw ignoreClick>
|
|
133
|
+
<div className="p-2 text-xs uppercase tracking-wide text-copy-medium">
|
|
134
|
+
Custom Header
|
|
135
|
+
</div>
|
|
136
|
+
</DropdownMenuItem>
|
|
137
|
+
<DropdownMenuItem label="Action" />
|
|
138
|
+
</DropdownMenu>
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
### Nested Sub-menus
|
|
142
|
+
|
|
143
|
+
Create hierarchical menus using `DropdownMenuSub`:
|
|
144
|
+
|
|
145
|
+
```tsx
|
|
146
|
+
import {
|
|
147
|
+
DropdownMenu,
|
|
148
|
+
DropdownMenuItem,
|
|
149
|
+
DropdownMenuSub,
|
|
150
|
+
DropdownMenuGroupLabel
|
|
151
|
+
} from "@versini/ui-dropdown";
|
|
152
|
+
import { ButtonIcon } from "@versini/ui-button";
|
|
153
|
+
import { IconSettings, IconOpenAI, IconAnthropic } from "@versini/ui-icons";
|
|
154
|
+
|
|
155
|
+
function SettingsMenu() {
|
|
156
|
+
const [engine, setEngine] = useState("openai");
|
|
157
|
+
|
|
158
|
+
return (
|
|
159
|
+
<DropdownMenu
|
|
160
|
+
trigger={
|
|
161
|
+
<ButtonIcon label="Settings">
|
|
162
|
+
<IconSettings />
|
|
163
|
+
</ButtonIcon>
|
|
164
|
+
}
|
|
165
|
+
>
|
|
166
|
+
<DropdownMenuItem label="Profile" />
|
|
167
|
+
<DropdownMenuItem label="Preferences" />
|
|
168
|
+
|
|
169
|
+
{/* Nested sub-menu */}
|
|
170
|
+
<DropdownMenuSub label="AI Settings">
|
|
171
|
+
<DropdownMenuGroupLabel>Engines</DropdownMenuGroupLabel>
|
|
172
|
+
<DropdownMenuItem
|
|
173
|
+
label="OpenAI"
|
|
174
|
+
icon={<IconOpenAI />}
|
|
175
|
+
selected={engine === "openai"}
|
|
176
|
+
onSelect={() => setEngine("openai")}
|
|
177
|
+
/>
|
|
178
|
+
<DropdownMenuItem
|
|
179
|
+
label="Anthropic"
|
|
180
|
+
icon={<IconAnthropic />}
|
|
181
|
+
selected={engine === "anthropic"}
|
|
182
|
+
onSelect={() => setEngine("anthropic")}
|
|
183
|
+
/>
|
|
184
|
+
</DropdownMenuSub>
|
|
185
|
+
|
|
186
|
+
<DropdownMenuItem label="About" />
|
|
187
|
+
</DropdownMenu>
|
|
188
|
+
);
|
|
189
|
+
}
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
**Features of nested sub-menus:**
|
|
193
|
+
|
|
194
|
+
- Automatically positioned to the right (or left if no space)
|
|
195
|
+
- Visual chevron indicator (`→`) shows expandable items
|
|
196
|
+
- Hover or click to open sub-menus
|
|
197
|
+
- Smart positioning adjusts for viewport constraints
|
|
198
|
+
- Keyboard navigation works across all levels
|
|
199
|
+
- Sibling sub-menus auto-close when opening another
|
|
200
|
+
|
|
201
|
+
## API
|
|
202
|
+
|
|
203
|
+
### DropdownMenu Props
|
|
204
|
+
|
|
205
|
+
| Prop | Type | Default | Description |
|
|
206
|
+
| ------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------- | ---------------- | ---------------------------------------------------- |
|
|
207
|
+
| `trigger` | `React.ReactNode` | - | Element used to open the menu (Button / ButtonIcon). |
|
|
208
|
+
| `children` | `React.ReactNode` | - | DropdownMenuItem, DropdownMenuSeparator, etc. |
|
|
209
|
+
| `label` | `string` | `"Open menu"` | Accessible label for the trigger. |
|
|
210
|
+
| `defaultPlacement` | `"bottom"` \| `"bottom-start"` \| `"bottom-end"` \| `"top"` \| `"top-start"` \| `"top-end"` \| `"left"` \| `"left-start"` \| `"right"` \| etc. | `"bottom-start"` | Initial preferred placement. |
|
|
211
|
+
| `mode` | `"dark"` \| `"light"` \| `"system"` \| `"alt-system"` | `"system"` | Color mode of trigger (when using UI buttons). |
|
|
212
|
+
| `focusMode` | `"dark"` \| `"light"` \| `"system"` \| `"alt-system"` | `"system"` | Focus ring thematic mode (when using UI buttons). |
|
|
213
|
+
| `onOpenChange` | `(open: boolean) => void` | - | Called when menu opens or closes. |
|
|
214
|
+
| `sideOffset` | `number` | `10` | Offset distance from the trigger element. |
|
|
215
|
+
| `modal` | `boolean` | `true` | Whether the dropdown is modal. |
|
|
216
|
+
|
|
217
|
+
### DropdownMenuItem Props
|
|
218
|
+
|
|
219
|
+
| Prop | Type | Default | Description |
|
|
220
|
+
| ------------- | ------------------------ | ----------- | --------------------------------------------- |
|
|
221
|
+
| `label` | `string` | - | The label to display for the menu item. |
|
|
222
|
+
| `disabled` | `boolean` | `false` | Whether the menu item is disabled. |
|
|
223
|
+
| `icon` | `React.ReactNode` | - | Icon to display on the left of the label. |
|
|
224
|
+
| `raw` | `boolean` | `false` | Disable internal styling for custom content. |
|
|
225
|
+
| `ignoreClick` | `boolean` | `false` | Prevent menu from closing when item selected. |
|
|
226
|
+
| `selected` | `boolean` | `undefined` | Show selected/unselected indicator. |
|
|
227
|
+
| `onSelect` | `(event: Event) => void` | - | Callback fired when the item is selected. |
|
|
228
|
+
|
|
229
|
+
### DropdownMenuSub Props
|
|
230
|
+
|
|
231
|
+
| Prop | Type | Default | Description |
|
|
232
|
+
| ------------- | ----------------- | ------- | ----------------------------------- |
|
|
233
|
+
| `label` | `string` | - | The label for the sub-menu trigger. |
|
|
234
|
+
| `children` | `React.ReactNode` | - | Items to render inside sub-menu. |
|
|
235
|
+
| `disabled` | `boolean` | `false` | Whether the sub-menu is disabled. |
|
|
236
|
+
| `sideOffset` | `number` | `2` | Offset from sub-menu trigger. |
|
|
237
|
+
| `alignOffset` | `number` | `-4` | Alignment offset for sub-menu. |
|
|
238
|
+
|
|
239
|
+
### DropdownMenuSeparator Props
|
|
240
|
+
|
|
241
|
+
Standard `React.HTMLAttributes<HTMLDivElement>` - use `className` for custom styling.
|
|
242
|
+
|
|
243
|
+
### DropdownMenuGroupLabel Props
|
|
244
|
+
|
|
245
|
+
Standard `React.HTMLAttributes<HTMLDivElement>` - use `className` for custom styling.
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { DropdownMenuGroupLabelProps, DropdownMenuProps, DropdownMenuSeparatorProps, DropdownMenuSubProps } from "./DropdownMenuTypes";
|
|
2
|
+
export declare const DropdownMenu: {
|
|
3
|
+
({ trigger, children, label, defaultPlacement, onOpenChange, mode, focusMode, sideOffset, modal, }: DropdownMenuProps): import("react/jsx-runtime").JSX.Element;
|
|
4
|
+
displayName: string;
|
|
5
|
+
};
|
|
6
|
+
export declare const DropdownMenuSub: {
|
|
7
|
+
({ label, children, disabled, sideOffset, alignOffset, }: DropdownMenuSubProps): import("react/jsx-runtime").JSX.Element;
|
|
8
|
+
displayName: string;
|
|
9
|
+
};
|
|
10
|
+
export declare const DropdownMenuSeparator: {
|
|
11
|
+
({ className, ...props }: DropdownMenuSeparatorProps): import("react/jsx-runtime").JSX.Element;
|
|
12
|
+
displayName: string;
|
|
13
|
+
};
|
|
14
|
+
export declare const DropdownMenuGroupLabel: {
|
|
15
|
+
({ className, ...props }: DropdownMenuGroupLabelProps): import("react/jsx-runtime").JSX.Element;
|
|
16
|
+
displayName: string;
|
|
17
|
+
};
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
/*!
|
|
2
|
+
@versini/ui-dropdown v1.1.0
|
|
3
|
+
© 2025 gizmette.com
|
|
4
|
+
*/
|
|
5
|
+
try {
|
|
6
|
+
if (!window.__VERSINI_UI_DROPDOWN__) {
|
|
7
|
+
window.__VERSINI_UI_DROPDOWN__ = {
|
|
8
|
+
version: "1.1.0",
|
|
9
|
+
buildTime: "12/15/2025 01:21 PM EST",
|
|
10
|
+
homepage: "https://www.npmjs.com/package/@versini/ui-dropdown",
|
|
11
|
+
license: "MIT",
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
} catch (error) {
|
|
15
|
+
// nothing to declare officer
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
19
|
+
import { Content, Label, Portal, Root, Separator, Sub, SubContent, SubTrigger, Trigger } from "@radix-ui/react-dropdown-menu";
|
|
20
|
+
import { IconNext } from "@versini/ui-icons";
|
|
21
|
+
import clsx from "clsx";
|
|
22
|
+
import { cloneElement, useState } from "react";
|
|
23
|
+
import { getDisplayName } from "./utilities.js";
|
|
24
|
+
|
|
25
|
+
;// CONCATENATED MODULE: external "react/jsx-runtime"
|
|
26
|
+
|
|
27
|
+
;// CONCATENATED MODULE: external "@radix-ui/react-dropdown-menu"
|
|
28
|
+
|
|
29
|
+
;// CONCATENATED MODULE: external "@versini/ui-icons"
|
|
30
|
+
|
|
31
|
+
;// CONCATENATED MODULE: external "clsx"
|
|
32
|
+
|
|
33
|
+
;// CONCATENATED MODULE: external "react"
|
|
34
|
+
|
|
35
|
+
;// CONCATENATED MODULE: external "./utilities.js"
|
|
36
|
+
|
|
37
|
+
;// CONCATENATED MODULE: ./src/components/DropdownMenu/DropdownMenu.tsx
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
const CONTENT_CLASS = "rounded-md bg-surface-light shadow-sm shadow-border-dark outline-hidden p-3 sm:p-2";
|
|
45
|
+
const SUB_TRIGGER_CLASS = clsx("flex items-center flex-row justify-between", "w-full", "m-0 first:mt-0 mt-2 sm:mt-1 px-2 py-1", "rounded-md border border-transparent", "text-left text-base", "outline-hidden focus:border focus:border-border-medium focus:bg-surface-lighter focus:underline", "disabled:cursor-not-allowed disabled:text-copy-medium", "data-[highlighted]:bg-surface-lighter data-[highlighted]:border-border-medium data-[highlighted]:underline", "data-[state=open]:bg-surface-lighter");
|
|
46
|
+
/**
|
|
47
|
+
* Convert Radix placement format to our simplified format.
|
|
48
|
+
*/ const getRadixSide = (placement)=>{
|
|
49
|
+
/* v8 ignore next 3 */ if (!placement) {
|
|
50
|
+
return "bottom";
|
|
51
|
+
}
|
|
52
|
+
if (placement.startsWith("top")) {
|
|
53
|
+
return "top";
|
|
54
|
+
}
|
|
55
|
+
if (placement.startsWith("left")) {
|
|
56
|
+
return "left";
|
|
57
|
+
}
|
|
58
|
+
if (placement.startsWith("right")) {
|
|
59
|
+
return "right";
|
|
60
|
+
}
|
|
61
|
+
return "bottom";
|
|
62
|
+
};
|
|
63
|
+
const getRadixAlign = (placement)=>{
|
|
64
|
+
/* v8 ignore next 3 */ if (!placement) {
|
|
65
|
+
return "start";
|
|
66
|
+
}
|
|
67
|
+
if (placement.endsWith("-start")) {
|
|
68
|
+
return "start";
|
|
69
|
+
}
|
|
70
|
+
if (placement.endsWith("-end")) {
|
|
71
|
+
return "end";
|
|
72
|
+
}
|
|
73
|
+
return "center";
|
|
74
|
+
};
|
|
75
|
+
const DropdownMenu = ({ trigger, children, label = "Open menu", defaultPlacement = "bottom-start", onOpenChange, mode = "system", focusMode = "system", sideOffset = 10, modal = true })=>{
|
|
76
|
+
const [isOpen, setIsOpen] = useState(false);
|
|
77
|
+
const noInternalClick = getDisplayName(trigger) === "Button" || getDisplayName(trigger) === "ButtonIcon";
|
|
78
|
+
const uiButtonsExtraProps = noInternalClick ? {
|
|
79
|
+
noInternalClick,
|
|
80
|
+
focusMode,
|
|
81
|
+
mode
|
|
82
|
+
} : {};
|
|
83
|
+
/* v8 ignore next 6 - trigger is required in practice */ const triggerElement = trigger ? /*#__PURE__*/ cloneElement(trigger, {
|
|
84
|
+
...uiButtonsExtraProps,
|
|
85
|
+
"aria-label": label
|
|
86
|
+
}) : null;
|
|
87
|
+
const handleOpenChange = (open)=>{
|
|
88
|
+
setIsOpen(open);
|
|
89
|
+
onOpenChange?.(open);
|
|
90
|
+
};
|
|
91
|
+
/**
|
|
92
|
+
* Handle pointer down to ensure the event propagates to parent elements.
|
|
93
|
+
* This is crucial for compatibility with Tooltip components that need
|
|
94
|
+
* to detect clicks on their trigger to disable tooltip display.
|
|
95
|
+
* We use onPointerDown because it fires before Radix's internal handlers
|
|
96
|
+
* and allows proper event bubbling.
|
|
97
|
+
*/ const handlePointerDown = (e)=>{
|
|
98
|
+
/**
|
|
99
|
+
* Dispatch a click event to ensure parent components (like Tooltip)
|
|
100
|
+
* can detect the interaction and respond appropriately.
|
|
101
|
+
*/ const clickEvent = new MouseEvent("click", {
|
|
102
|
+
bubbles: true,
|
|
103
|
+
cancelable: true,
|
|
104
|
+
view: window
|
|
105
|
+
});
|
|
106
|
+
e.currentTarget.dispatchEvent(clickEvent);
|
|
107
|
+
};
|
|
108
|
+
return /*#__PURE__*/ jsxs(Root, {
|
|
109
|
+
onOpenChange: handleOpenChange,
|
|
110
|
+
modal: modal,
|
|
111
|
+
children: [
|
|
112
|
+
/*#__PURE__*/ jsx(Trigger, {
|
|
113
|
+
asChild: true,
|
|
114
|
+
"data-state": isOpen ? "open" : "closed",
|
|
115
|
+
onPointerDown: handlePointerDown,
|
|
116
|
+
children: triggerElement
|
|
117
|
+
}),
|
|
118
|
+
/*#__PURE__*/ jsx(Portal, {
|
|
119
|
+
children: /*#__PURE__*/ jsx(Content, {
|
|
120
|
+
className: CONTENT_CLASS,
|
|
121
|
+
sideOffset: sideOffset,
|
|
122
|
+
side: getRadixSide(defaultPlacement),
|
|
123
|
+
align: getRadixAlign(defaultPlacement),
|
|
124
|
+
onCloseAutoFocus: (e)=>{
|
|
125
|
+
/**
|
|
126
|
+
* Prevent focus from returning to the trigger when menu closes.
|
|
127
|
+
* This helps avoid tooltip re-triggering immediately after menu closes.
|
|
128
|
+
*/ e.preventDefault();
|
|
129
|
+
},
|
|
130
|
+
children: children
|
|
131
|
+
})
|
|
132
|
+
})
|
|
133
|
+
]
|
|
134
|
+
});
|
|
135
|
+
};
|
|
136
|
+
DropdownMenu.displayName = "DropdownMenu";
|
|
137
|
+
const DropdownMenuSub = ({ label, children, disabled = false, sideOffset = 2, alignOffset = -4 })=>{
|
|
138
|
+
return /*#__PURE__*/ jsxs(Sub, {
|
|
139
|
+
children: [
|
|
140
|
+
/*#__PURE__*/ jsxs(SubTrigger, {
|
|
141
|
+
className: SUB_TRIGGER_CLASS,
|
|
142
|
+
disabled: disabled,
|
|
143
|
+
children: [
|
|
144
|
+
/*#__PURE__*/ jsx("span", {
|
|
145
|
+
children: label
|
|
146
|
+
}),
|
|
147
|
+
/*#__PURE__*/ jsx(IconNext, {
|
|
148
|
+
className: "ml-2",
|
|
149
|
+
size: "size-3",
|
|
150
|
+
monotone: true
|
|
151
|
+
})
|
|
152
|
+
]
|
|
153
|
+
}),
|
|
154
|
+
/*#__PURE__*/ jsx(Portal, {
|
|
155
|
+
children: /*#__PURE__*/ jsx(SubContent, {
|
|
156
|
+
className: CONTENT_CLASS,
|
|
157
|
+
sideOffset: sideOffset,
|
|
158
|
+
alignOffset: alignOffset,
|
|
159
|
+
children: children
|
|
160
|
+
})
|
|
161
|
+
})
|
|
162
|
+
]
|
|
163
|
+
});
|
|
164
|
+
};
|
|
165
|
+
DropdownMenuSub.displayName = "DropdownMenuSub";
|
|
166
|
+
const DropdownMenuSeparator = ({ className, ...props })=>{
|
|
167
|
+
const separatorClass = clsx(className, "my-1 border-t border-border-medium");
|
|
168
|
+
return /*#__PURE__*/ jsx(Separator, {
|
|
169
|
+
className: separatorClass,
|
|
170
|
+
...props
|
|
171
|
+
});
|
|
172
|
+
};
|
|
173
|
+
DropdownMenuSeparator.displayName = "DropdownMenuSeparator";
|
|
174
|
+
const DropdownMenuGroupLabel = ({ className, ...props })=>{
|
|
175
|
+
const groupLabelClass = clsx(className, "pt-1 mb-2", "text-sm text-copy-dark font-bold", "border-b border-border-medium");
|
|
176
|
+
return /*#__PURE__*/ jsx(Label, {
|
|
177
|
+
className: groupLabelClass,
|
|
178
|
+
...props
|
|
179
|
+
});
|
|
180
|
+
};
|
|
181
|
+
DropdownMenuGroupLabel.displayName = "DropdownMenuGroupLabel";
|
|
182
|
+
|
|
183
|
+
export { DropdownMenu, DropdownMenuGroupLabel, DropdownMenuSeparator, DropdownMenuSub };
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { DropdownMenuItemProps } from "./DropdownMenuTypes";
|
|
2
|
+
export declare const DropdownMenuItem: {
|
|
3
|
+
({ label, disabled, icon, raw, children, ignoreClick, selected, onSelect, onClick, onFocus, ...props }: DropdownMenuItemProps): import("react/jsx-runtime").JSX.Element;
|
|
4
|
+
displayName: string;
|
|
5
|
+
};
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/*!
|
|
2
|
+
@versini/ui-dropdown v1.1.0
|
|
3
|
+
© 2025 gizmette.com
|
|
4
|
+
*/
|
|
5
|
+
try {
|
|
6
|
+
if (!window.__VERSINI_UI_DROPDOWN__) {
|
|
7
|
+
window.__VERSINI_UI_DROPDOWN__ = {
|
|
8
|
+
version: "1.1.0",
|
|
9
|
+
buildTime: "12/15/2025 01:21 PM EST",
|
|
10
|
+
homepage: "https://www.npmjs.com/package/@versini/ui-dropdown",
|
|
11
|
+
license: "MIT",
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
} catch (error) {
|
|
15
|
+
// nothing to declare officer
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
19
|
+
import { Item } from "@radix-ui/react-dropdown-menu";
|
|
20
|
+
import { IconSelected, IconUnSelected } from "@versini/ui-icons";
|
|
21
|
+
import clsx from "clsx";
|
|
22
|
+
|
|
23
|
+
;// CONCATENATED MODULE: external "react/jsx-runtime"
|
|
24
|
+
|
|
25
|
+
;// CONCATENATED MODULE: external "@radix-ui/react-dropdown-menu"
|
|
26
|
+
|
|
27
|
+
;// CONCATENATED MODULE: external "@versini/ui-icons"
|
|
28
|
+
|
|
29
|
+
;// CONCATENATED MODULE: external "clsx"
|
|
30
|
+
|
|
31
|
+
;// CONCATENATED MODULE: ./src/components/DropdownMenu/DropdownMenuItem.tsx
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
const ITEM_CLASS = clsx("flex flex-row items-center", "w-full", "m-0 first:mt-0 mt-2 sm:mt-1 px-2 py-1", "rounded-md border border-transparent", "text-left text-base", "outline-hidden focus:border focus:border-border-medium focus:bg-surface-lighter focus:underline", "disabled:cursor-not-allowed disabled:text-copy-medium", "data-[highlighted]:bg-surface-lighter data-[highlighted]:border-border-medium data-[highlighted]:underline", "data-[disabled]:cursor-not-allowed data-[disabled]:text-copy-medium");
|
|
37
|
+
const DropdownMenuItem = ({ label, disabled, icon, raw = false, children, ignoreClick = false, selected, onSelect, onClick, onFocus, ...props })=>{
|
|
38
|
+
let buttonSpanClass = "";
|
|
39
|
+
if (raw && children) {
|
|
40
|
+
return /*#__PURE__*/ jsx(Item, {
|
|
41
|
+
className: "outline-hidden",
|
|
42
|
+
onSelect: (event)=>{
|
|
43
|
+
if (ignoreClick) {
|
|
44
|
+
event.preventDefault();
|
|
45
|
+
}
|
|
46
|
+
onSelect?.(event);
|
|
47
|
+
/* v8 ignore next 1 - optional onClick may not be provided */ onClick?.(event);
|
|
48
|
+
},
|
|
49
|
+
...props,
|
|
50
|
+
children: children
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
if (icon) {
|
|
54
|
+
buttonSpanClass = "pl-2";
|
|
55
|
+
}
|
|
56
|
+
const itemClass = clsx(ITEM_CLASS, {
|
|
57
|
+
"bg-none": !disabled && !selected
|
|
58
|
+
});
|
|
59
|
+
const handleSelect = (event)=>{
|
|
60
|
+
if (ignoreClick) {
|
|
61
|
+
event.preventDefault();
|
|
62
|
+
}
|
|
63
|
+
onSelect?.(event);
|
|
64
|
+
// Also call onClick for compatibility with common patterns
|
|
65
|
+
/* v8 ignore next 1 - optional onClick may not be provided */ onClick?.(event);
|
|
66
|
+
};
|
|
67
|
+
return /*#__PURE__*/ jsxs(Item, {
|
|
68
|
+
className: itemClass,
|
|
69
|
+
disabled: disabled,
|
|
70
|
+
onSelect: handleSelect,
|
|
71
|
+
onFocus: onFocus,
|
|
72
|
+
...props,
|
|
73
|
+
children: [
|
|
74
|
+
selected === true && /*#__PURE__*/ jsx(IconSelected, {
|
|
75
|
+
className: "text-copy-success mr-2",
|
|
76
|
+
size: "size-4"
|
|
77
|
+
}),
|
|
78
|
+
selected === false && /*#__PURE__*/ jsx(IconUnSelected, {
|
|
79
|
+
className: "text-copy-medium mr-2",
|
|
80
|
+
size: "size-4"
|
|
81
|
+
}),
|
|
82
|
+
icon,
|
|
83
|
+
label && /*#__PURE__*/ jsx("span", {
|
|
84
|
+
className: buttonSpanClass,
|
|
85
|
+
children: label
|
|
86
|
+
})
|
|
87
|
+
]
|
|
88
|
+
});
|
|
89
|
+
};
|
|
90
|
+
DropdownMenuItem.displayName = "DropdownMenuItem";
|
|
91
|
+
|
|
92
|
+
export { DropdownMenuItem };
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
export type DropdownMenuProps = {
|
|
2
|
+
/**
|
|
3
|
+
* The component to use to open the dropdown menu, e.g. a ButtonIcon, a Button, etc.
|
|
4
|
+
* Required for root menus, omit for nested sub-menus (use label instead).
|
|
5
|
+
*/
|
|
6
|
+
trigger?: React.ReactNode;
|
|
7
|
+
/**
|
|
8
|
+
* The children to render (DropdownMenuItem, DropdownMenuSeparator, etc.).
|
|
9
|
+
*/
|
|
10
|
+
children?: React.ReactNode;
|
|
11
|
+
/**
|
|
12
|
+
* The default location of the popup.
|
|
13
|
+
* @default "bottom-start"
|
|
14
|
+
*/
|
|
15
|
+
defaultPlacement?: "bottom" | "bottom-start" | "bottom-end" | "top" | "top-start" | "top-end" | "left" | "left-start" | "left-end" | "right" | "right-start" | "right-end";
|
|
16
|
+
/**
|
|
17
|
+
* The type of focus for the Button. This will change the color
|
|
18
|
+
* of the focus ring around the Button.
|
|
19
|
+
*/
|
|
20
|
+
focusMode?: "dark" | "light" | "system" | "alt-system";
|
|
21
|
+
/**
|
|
22
|
+
* The type of Button trigger. This will change the color of the Button.
|
|
23
|
+
*/
|
|
24
|
+
mode?: "dark" | "light" | "system" | "alt-system";
|
|
25
|
+
/**
|
|
26
|
+
* The label to use for the menu button (root menu) or the sub-menu trigger text (nested menu).
|
|
27
|
+
* When used without a trigger, this creates a nested sub-menu.
|
|
28
|
+
*/
|
|
29
|
+
label?: string;
|
|
30
|
+
/**
|
|
31
|
+
* Callback fired when the component is opened or closed.
|
|
32
|
+
* @param open whether or not the menu is open
|
|
33
|
+
*/
|
|
34
|
+
onOpenChange?: (open: boolean) => void;
|
|
35
|
+
/**
|
|
36
|
+
* The offset distance from the trigger element.
|
|
37
|
+
* @default 10
|
|
38
|
+
*/
|
|
39
|
+
sideOffset?: number;
|
|
40
|
+
/**
|
|
41
|
+
* Whether the dropdown menu is modal (locks interaction outside).
|
|
42
|
+
* @default true
|
|
43
|
+
*/
|
|
44
|
+
modal?: boolean;
|
|
45
|
+
};
|
|
46
|
+
export type DropdownMenuItemProps = {
|
|
47
|
+
/**
|
|
48
|
+
* The label to use for the menu item.
|
|
49
|
+
*/
|
|
50
|
+
label?: string;
|
|
51
|
+
/**
|
|
52
|
+
* Whether or not the menu item is disabled.
|
|
53
|
+
* @default false
|
|
54
|
+
*/
|
|
55
|
+
disabled?: boolean;
|
|
56
|
+
/**
|
|
57
|
+
* A React component of type Icon to be placed on the left of the label.
|
|
58
|
+
*/
|
|
59
|
+
icon?: React.ReactNode;
|
|
60
|
+
/**
|
|
61
|
+
* Disable internal menu item behavior (click, focus, etc.).
|
|
62
|
+
* @default false
|
|
63
|
+
*/
|
|
64
|
+
raw?: boolean;
|
|
65
|
+
/**
|
|
66
|
+
* Children to render when using raw mode.
|
|
67
|
+
*/
|
|
68
|
+
children?: React.ReactNode;
|
|
69
|
+
/**
|
|
70
|
+
* Whether or not the menu should close when the menu item is selected.
|
|
71
|
+
* @default false
|
|
72
|
+
*/
|
|
73
|
+
ignoreClick?: boolean;
|
|
74
|
+
/**
|
|
75
|
+
* Whether or not the menu item is selected.
|
|
76
|
+
* @default undefined
|
|
77
|
+
*/
|
|
78
|
+
selected?: boolean;
|
|
79
|
+
/**
|
|
80
|
+
* Callback fired when the menu item is selected.
|
|
81
|
+
*/
|
|
82
|
+
onSelect?: (event: Event) => void;
|
|
83
|
+
/**
|
|
84
|
+
* Optional click handler.
|
|
85
|
+
*/
|
|
86
|
+
onClick?: (event: React.MouseEvent<HTMLDivElement>) => void;
|
|
87
|
+
/**
|
|
88
|
+
* Optional focus handler.
|
|
89
|
+
*/
|
|
90
|
+
onFocus?: (event: React.FocusEvent<HTMLDivElement>) => void;
|
|
91
|
+
};
|
|
92
|
+
export type DropdownMenuSeparatorProps = React.HTMLAttributes<HTMLDivElement>;
|
|
93
|
+
export type DropdownMenuGroupLabelProps = React.HTMLAttributes<HTMLDivElement>;
|
|
94
|
+
export type DropdownMenuSubProps = {
|
|
95
|
+
/**
|
|
96
|
+
* The label for the sub-menu trigger.
|
|
97
|
+
*/
|
|
98
|
+
label: string;
|
|
99
|
+
/**
|
|
100
|
+
* The children to render inside the sub-menu.
|
|
101
|
+
*/
|
|
102
|
+
children?: React.ReactNode;
|
|
103
|
+
/**
|
|
104
|
+
* Whether the sub-menu trigger is disabled.
|
|
105
|
+
* @default false
|
|
106
|
+
*/
|
|
107
|
+
disabled?: boolean;
|
|
108
|
+
/**
|
|
109
|
+
* The offset distance from the sub-menu trigger.
|
|
110
|
+
* @default 2
|
|
111
|
+
*/
|
|
112
|
+
sideOffset?: number;
|
|
113
|
+
/**
|
|
114
|
+
* The alignment offset for the sub-menu.
|
|
115
|
+
* @default -4
|
|
116
|
+
*/
|
|
117
|
+
alignOffset?: number;
|
|
118
|
+
};
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/*!
|
|
2
|
+
@versini/ui-dropdown v1.1.0
|
|
3
|
+
© 2025 gizmette.com
|
|
4
|
+
*/
|
|
5
|
+
try {
|
|
6
|
+
if (!window.__VERSINI_UI_DROPDOWN__) {
|
|
7
|
+
window.__VERSINI_UI_DROPDOWN__ = {
|
|
8
|
+
version: "1.1.0",
|
|
9
|
+
buildTime: "12/15/2025 01:21 PM EST",
|
|
10
|
+
homepage: "https://www.npmjs.com/package/@versini/ui-dropdown",
|
|
11
|
+
license: "MIT",
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
} catch (error) {
|
|
15
|
+
// nothing to declare officer
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
;// CONCATENATED MODULE: ./src/components/DropdownMenu/DropdownMenuTypes.ts
|
|
20
|
+
|
|
21
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const getDisplayName: (element: unknown) => string;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/*!
|
|
2
|
+
@versini/ui-dropdown v1.1.0
|
|
3
|
+
© 2025 gizmette.com
|
|
4
|
+
*/
|
|
5
|
+
try {
|
|
6
|
+
if (!window.__VERSINI_UI_DROPDOWN__) {
|
|
7
|
+
window.__VERSINI_UI_DROPDOWN__ = {
|
|
8
|
+
version: "1.1.0",
|
|
9
|
+
buildTime: "12/15/2025 01:21 PM EST",
|
|
10
|
+
homepage: "https://www.npmjs.com/package/@versini/ui-dropdown",
|
|
11
|
+
license: "MIT",
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
} catch (error) {
|
|
15
|
+
// nothing to declare officer
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
;// CONCATENATED MODULE: ./src/components/DropdownMenu/utilities.ts
|
|
20
|
+
const getDisplayName = (element)=>{
|
|
21
|
+
if (typeof element === "string") {
|
|
22
|
+
return element;
|
|
23
|
+
}
|
|
24
|
+
if (typeof element === "function") {
|
|
25
|
+
return element.displayName || element.name || "Component";
|
|
26
|
+
}
|
|
27
|
+
if (typeof element === "object" && element !== null && "type" in element) {
|
|
28
|
+
const type = element.type;
|
|
29
|
+
if (typeof type === "function" || typeof type === "object") {
|
|
30
|
+
/* v8 ignore next 4 */ return type.displayName || type.name || "Component";
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return "Element";
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
export { getDisplayName };
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/*!
|
|
2
|
+
@versini/ui-dropdown v1.1.0
|
|
3
|
+
© 2025 gizmette.com
|
|
4
|
+
*/
|
|
5
|
+
try {
|
|
6
|
+
if (!window.__VERSINI_UI_DROPDOWN__) {
|
|
7
|
+
window.__VERSINI_UI_DROPDOWN__ = {
|
|
8
|
+
version: "1.1.0",
|
|
9
|
+
buildTime: "12/15/2025 01:21 PM EST",
|
|
10
|
+
homepage: "https://www.npmjs.com/package/@versini/ui-dropdown",
|
|
11
|
+
license: "MIT",
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
} catch (error) {
|
|
15
|
+
// nothing to declare officer
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export * from "./DropdownMenu/DropdownMenu.js";
|
|
19
|
+
export * from "./DropdownMenu/DropdownMenuItem.js";
|
|
20
|
+
|
|
21
|
+
;// CONCATENATED MODULE: ./src/components/index.ts
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
|
package/package.json
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@versini/ui-dropdown",
|
|
3
|
+
"version": "1.1.0",
|
|
4
|
+
"license": "MIT",
|
|
5
|
+
"author": "Arno Versini",
|
|
6
|
+
"publishConfig": {
|
|
7
|
+
"access": "public"
|
|
8
|
+
},
|
|
9
|
+
"homepage": "https://www.npmjs.com/package/@versini/ui-dropdown",
|
|
10
|
+
"repository": {
|
|
11
|
+
"type": "git",
|
|
12
|
+
"url": "git@github.com:aversini/ui-components.git"
|
|
13
|
+
},
|
|
14
|
+
"type": "module",
|
|
15
|
+
"main": "dist/index.js",
|
|
16
|
+
"types": "dist/index.d.ts",
|
|
17
|
+
"files": [
|
|
18
|
+
"dist",
|
|
19
|
+
"README.md"
|
|
20
|
+
],
|
|
21
|
+
"scripts": {
|
|
22
|
+
"build:check": "tsc",
|
|
23
|
+
"build:js": "rslib build",
|
|
24
|
+
"build:types": "echo 'Types now built with rslib'",
|
|
25
|
+
"build": "npm-run-all --serial clean build:check build:js",
|
|
26
|
+
"clean": "rimraf dist tmp",
|
|
27
|
+
"dev:js": "rslib build --watch",
|
|
28
|
+
"dev:types": "echo 'Types now watched with rslib'",
|
|
29
|
+
"dev": "rslib build --watch",
|
|
30
|
+
"lint": "biome lint src",
|
|
31
|
+
"lint:fix": "biome check src --write --no-errors-on-unmatched",
|
|
32
|
+
"prettier": "biome check --write --no-errors-on-unmatched",
|
|
33
|
+
"start": "static-server dist --port 5173",
|
|
34
|
+
"test:coverage:ui": "vitest --coverage --ui",
|
|
35
|
+
"test:coverage": "vitest run --coverage",
|
|
36
|
+
"test:watch": "vitest",
|
|
37
|
+
"test": "vitest run"
|
|
38
|
+
},
|
|
39
|
+
"devDependencies": {
|
|
40
|
+
"@testing-library/jest-dom": "6.9.1",
|
|
41
|
+
"@versini/ui-types": "8.0.0"
|
|
42
|
+
},
|
|
43
|
+
"dependencies": {
|
|
44
|
+
"@radix-ui/react-dropdown-menu": "2.1.16",
|
|
45
|
+
"@tailwindcss/typography": "0.5.19",
|
|
46
|
+
"@versini/ui-icons": "4.15.1",
|
|
47
|
+
"clsx": "2.1.1",
|
|
48
|
+
"tailwindcss": "4.1.18"
|
|
49
|
+
},
|
|
50
|
+
"sideEffects": [
|
|
51
|
+
"**/*.css"
|
|
52
|
+
],
|
|
53
|
+
"gitHead": "28e13adabce18578034a9ca5553d7cfb9853c214"
|
|
54
|
+
}
|