@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 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
+ [![npm version](https://img.shields.io/npm/v/@versini/ui-dropdown?style=flat-square)](https://www.npmjs.com/package/@versini/ui-dropdown)
4
+ ![npm package minimized gzipped size](<https://img.shields.io/bundlejs/size/%40versini%2Fui-dropdown?style=flat-square&label=size%20(gzip)>)
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 };
@@ -0,0 +1,2 @@
1
+ export * from "./DropdownMenu/DropdownMenu";
2
+ export * from "./DropdownMenu/DropdownMenuItem";
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
+ }