diff --git a/bun.lockb b/bun.lockb index 33f345b..d87a92e 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/package.json b/package.json index 0787322..5333a63 100644 --- a/package.json +++ b/package.json @@ -18,6 +18,7 @@ "@codemirror/lang-javascript": "^6.2.2", "@codemirror/lang-json": "^6.0.1", "@codemirror/theme-one-dark": "^6.1.2", + "@headlessui/react": "^2.2.0", "@monaco-editor/react": "^4.6.0", "@uiw/react-codemirror": "^4.23.6", "class-variance-authority": "^0.7.1", diff --git a/src/components/dropdown/Dropdown.tsx b/src/components/dropdown/Dropdown.tsx index e00a239..7278390 100644 --- a/src/components/dropdown/Dropdown.tsx +++ b/src/components/dropdown/Dropdown.tsx @@ -1,5 +1,5 @@ import * as React from "react"; -import { createPortal } from "react-dom"; +import { Menu } from "@headlessui/react"; import { cn } from "../../utils/cn"; import { ZINDEX } from "../../utils/z-index"; @@ -7,8 +7,6 @@ export interface DropdownProps { trigger: React.ReactNode; children: React.ReactNode; align?: "left" | "right"; - direction?: "down" | "up"; - variant?: "default" | "nav"; className?: string; } @@ -16,141 +14,87 @@ export const Dropdown: React.FC = ({ trigger, children, align = "left", - direction = "down", - variant = "default", className, }) => { - const [isOpen, setIsOpen] = React.useState(false); - const [position, setPosition] = React.useState<{ top?: number; bottom?: number; left: number; right: number }>({ left: 0, right: 0 }); - const dropdownRef = React.useRef(null); - const triggerRef = React.useRef(null); - - React.useEffect(() => { - const handleClickOutside = (event: MouseEvent) => { - if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) { - setIsOpen(false); - } - }; - - const handleEscape = (event: KeyboardEvent) => { - if (event.key === 'Escape') { - setIsOpen(false); - } - }; - - document.addEventListener("mousedown", handleClickOutside); - if (variant === "nav") { - document.addEventListener("keydown", handleEscape); - } - - return () => { - document.removeEventListener("mousedown", handleClickOutside); - if (variant === "nav") { - document.removeEventListener("keydown", handleEscape); - } - }; - }, [variant]); - - // Update position when scrolling or resizing - React.useEffect(() => { - if (!isOpen) return; - - const updatePosition = () => { - if (!triggerRef.current) return; - const rect = triggerRef.current.getBoundingClientRect(); - - setPosition({ - left: rect.left, - right: window.innerWidth - rect.right, - ...(direction === 'down' - ? { top: rect.bottom + 8 } - : { bottom: window.innerHeight - rect.top + 8 } - ), - }); - }; - - updatePosition(); - window.addEventListener('scroll', updatePosition, true); - window.addEventListener('resize', updatePosition); - - return () => { - window.removeEventListener('scroll', updatePosition, true); - window.removeEventListener('resize', updatePosition); - }; - }, [isOpen, direction]); - - const triggerClasses = cn({ - "cursor-pointer": true, - "flex items-center gap-1 text-sm font-medium text-gray-600 hover:text-gray-900 dark:text-gray-400 dark:hover:text-gray-200 transition-colors": - variant === "nav", - }); - - const menuClasses = cn( - "absolute rounded-lg shadow-lg min-w-[8rem] py-1", - variant === "default" && [ - "bg-white dark:bg-gray-900", - "border border-gray-200 dark:border-gray-800", - ], - variant === "nav" && [ - "bg-white dark:bg-gray-800", - "border border-gray-200 dark:border-gray-700", - "w-48 rounded-md", - ], - { - "left-0": align === "left", - "right-0": align === "right", - "top-full mt-2": direction === "down", - "bottom-full mb-2": direction === "up", - }, - className - ); - return ( -
-
setIsOpen(!isOpen)} - className={triggerClasses} - > + + {trigger} -
- {isOpen && createPortal( -
- {children} -
, - document.body - )} -
+ + + + {children} + + ); }; -export interface DropdownItemProps extends React.ButtonHTMLAttributes { - icon?: React.ReactNode; +interface DropdownItemProps { + children: React.ReactNode; + href?: string; + onClick?: () => void; + className?: string; } -export const DropdownItem = React.forwardRef( - ({ className, children, icon, ...props }, ref) => ( - - ) +export const DropdownItem = React.forwardRef( + ({ children, href, onClick, className }, ref) => { + const content = ( +
+ {children} +
+ ); + + return ( + + {({ active }) => + href ? ( + + {content} + + ) : ( + + ) + } + + ); + } ); + DropdownItem.displayName = "DropdownItem"; \ No newline at end of file diff --git a/src/components/navigation/NavMenu.tsx b/src/components/navigation/NavMenu.tsx index e01c292..179e1e4 100644 --- a/src/components/navigation/NavMenu.tsx +++ b/src/components/navigation/NavMenu.tsx @@ -3,31 +3,23 @@ import { cn } from "../../utils/cn"; import { ChevronDown } from "lucide-react"; import { Dropdown, DropdownProps } from "../dropdown/Dropdown"; -export interface NavMenuProps extends Omit { +export interface NavMenuProps extends Omit { trigger: React.ReactNode; } export const NavMenu: React.FC = ({ trigger, children, - direction = "down", ...props }) => { return ( +
{trigger} - - + +
} - variant="nav" - direction={direction} {...props} > {children} @@ -52,4 +44,5 @@ export const NavMenuItem = React.forwardRef< {children} )); + NavMenuItem.displayName = "NavMenuItem"; \ No newline at end of file diff --git a/src/docs/pages/components/DropdownPage.tsx b/src/docs/pages/components/DropdownPage.tsx index c9acd1d..8be3c87 100644 --- a/src/docs/pages/components/DropdownPage.tsx +++ b/src/docs/pages/components/DropdownPage.tsx @@ -20,15 +20,27 @@ export const DropdownPage = () => { description: "The alignment of the dropdown menu", }, { - name: "direction", - type: "'down' | 'up'", - defaultValue: "down", - description: "Direction for the menu to expand", + name: "className", + type: "string", + description: "Additional CSS classes for the dropdown menu", + }, + ]; + + const dropdownItemProps = [ + { + name: "href", + type: "string", + description: "Optional URL for link items", + }, + { + name: "onClick", + type: "() => void", + description: "Click handler for the item", }, { name: "className", type: "string", - description: "Additional CSS classes for the dropdown menu", + description: "Additional CSS classes", }, ]; @@ -36,23 +48,18 @@ export const DropdownPage = () => {
( - Open Menu} - align="left" - > + Open Menu}> Profile Settings Logout );`} > - Open Menu} - > + Open Menu}> Profile Settings Logout @@ -73,7 +80,7 @@ const MyComponent = () => ( title="Right Aligned" description="Dropdown menu aligned to the right" code={`Right Menu} + trigger={} align="right" > Option 1 @@ -82,7 +89,7 @@ const MyComponent = () => ( >
Right Menu} + trigger={} align="right" > Option 1 @@ -92,112 +99,89 @@ const MyComponent = () => ( - Custom Trigger - - } -> - Action + title="With Links" + description="Dropdown items as links" + code={`Navigation}> + + Google + + + Settings + + alert('Clicked!')}> + Action Button + `} > - - Custom Trigger - - } - > - Action + Navigation}> + + Google + + + Settings + + alert('Clicked!')}> + Action Button + Upward Menu} - direction="up" -> - Option 1 - Option 2 -`} - > -
- Upward Menu} - direction="up" - > - Option 1 - Option 2 - -
- -
- - - User Menu}> + title="With Icons" + description="Dropdown items with icons" + code={`User Menu}> - User 1 avatar - John Doe +
+ GitHub + GitHub +
- User 2 avatar - Jane Smith +
+ React + React +
`} - > - User Menu}> - - GitHub avatar - GitHub - - - React avatar - React - - -
+ > + User Menu}> + +
+ GitHub + GitHub +
+
+ +
+ React + React +
+
+
+
+
+
DropdownItem Properties
- +
diff --git a/src/docs/pages/components/NavMenuPage.tsx b/src/docs/pages/components/NavMenuPage.tsx index efc4026..4f0225d 100644 --- a/src/docs/pages/components/NavMenuPage.tsx +++ b/src/docs/pages/components/NavMenuPage.tsx @@ -15,16 +15,10 @@ export const NavMenuPage = () => { description: "Element that triggers the menu", }, { - name: "children", - type: "ReactNode", - required: true, - description: "Menu items", - }, - { - name: "direction", - type: "'down' | 'up'", - defaultValue: "down", - description: "Direction for the menu to expand", + name: "align", + type: "'left' | 'right'", + defaultValue: "left", + description: "The alignment of the menu", }, { name: "className", @@ -126,49 +120,6 @@ const MyComponent = () => ( - - - Option 1 - Option 2 - Option 3 - - - - Option 1 - Option 2 - Option 3 -`} - > -
-
- - Option 1 - Option 2 - Option 3 - -
- - Option 1 - Option 2 - Option 3 - -
-