diff --git a/components/commandmenu/CommandMenu.stories.tsx b/components/commandmenu/CommandMenu.stories.tsx index 4a6d037..b1b9f70 100644 --- a/components/commandmenu/CommandMenu.stories.tsx +++ b/components/commandmenu/CommandMenu.stories.tsx @@ -1,14 +1,29 @@ -import React, {useRef, useState} from 'react'; +import React, {useState} from 'react'; import {Meta, StoryObj} from "@storybook/react"; -import {Input} from '../input/Input'; +import {CommandMenu, CommandMenuItemProps} from "@/components/commandmenu/CommandMenu"; import { - CommandMenu, - CommandMenuItem, - CommandMenuLabel, - CommandMenuSeperator -} from "@/components/commandmenu/CommandMenu"; -import {DialogRef} from "@/components/dialog/Dialog"; -import {Box, CalendarFold, CircleDashed, FilePenLine, GitBranch, LayoutDashboard, Mail} from "lucide-react"; + File, + Save, + Trash2, + Search, + Settings, + Upload, + Download, + Edit3, + Eye, + Copy, + Folder, + Plus, + Minus, + Bookmark, + RefreshCw, + CheckCircle, + XCircle, + Volume2, + PlayCircle, + PauseCircle, + GitBranch +} from "lucide-react"; const meta: Meta = { title: "Components/CommandMenu", @@ -30,18 +45,275 @@ export const Default = () => { const openDialog = () => setIsDialogOpen(true) const closeDialog = () => setIsDialogOpen(false) + const showItems: CommandMenuItemProps[] = [ + { + id: 1, + title: "Open File", + description: "Open an existing file from your system", + icon: , // Lucide icon for file + onClick: (e) => console.log("Open File clicked"), + shortcut: "Ctrl+O", + disabled: false, + selected: false, + keywords: ["open", "file", "load"] + }, + { + id: 2, + title: "Save File", + description: "Save the current file", + icon: , // Lucide icon for save + onClick: (e) => console.log("Save File clicked"), + keywords: ["save", "file", "store"], + disabled: false + }, + { + id: 3, + title: "Delete Item", + icon: , // Lucide icon for delete + onClick: (e) => console.log("Delete clicked"), + selected: true, + keywords: ["delete", "remove", "trash"], + disabled: false + }, + { + id: 4, + title: "Search", + icon: , // Lucide icon for search + keywords: ["search", "find", "lookup"], + disabled: false + }, + { + id: 5, + title: "Settings", + description: "Change system settings", + icon: , // Lucide icon for settings + onClick: (e) => console.log("Settings clicked"), + shortcut: "Ctrl+,", + disabled: false, + keywords: ["settings", "preferences", "options"] + } + ]; + const commandMenuItems: CommandMenuItemProps[] = [ + { + id: 1, + title: "Open File", + description: "Open an existing file from your system", + icon: , // Lucide icon for file + onClick: (e) => console.log("Open File clicked"), + shortcut: "Ctrl+O", + disabled: false, + selected: false, + keywords: ["open", "file", "load"] + }, + { + id: 2, + title: "Save File", + description: "Save the current file", + icon: , // Lucide icon for save + onClick: (e) => console.log("Save File clicked"), + keywords: ["save", "file", "store"], + disabled: false + }, + { + id: 3, + title: "Delete Item", + icon: , // Lucide icon for delete + onClick: (e) => console.log("Delete clicked"), + selected: true, + keywords: ["delete", "remove", "trash"], + disabled: false + }, + { + id: 4, + title: "Search", + icon: , // Lucide icon for search + keywords: ["search", "find", "lookup"], + disabled: false + }, + { + id: 5, + title: "Settings", + description: "Change system settings", + icon: , // Lucide icon for settings + onClick: (e) => console.log("Settings clicked"), + shortcut: "Ctrl+,", + disabled: false, + keywords: ["settings", "preferences", "options"] + }, + { + id: 6, + title: "Upload File", + description: "Upload a file to the server", + icon: , + onClick: (e) => console.log("Upload File clicked"), + keywords: ["upload", "file", "server"], + disabled: false + }, + { + id: 7, + title: "Download File", + description: "Download the selected file", + icon: , + onClick: (e) => console.log("Download clicked"), + shortcut: "Ctrl+D", + keywords: ["download", "file", "save"], + disabled: false + }, + { + id: 8, + title: "Edit Document", + icon: , + onClick: (e) => console.log("Edit clicked"), + keywords: ["edit", "document", "modify"], + disabled: false + }, + { + id: 9, + title: "View Details", + icon: , + keywords: ["view", "details", "inspect"], + disabled: false + }, + { + id: 10, + title: "Copy", + icon: , + onClick: (e) => console.log("Copy clicked"), + shortcut: "Ctrl+C", + keywords: ["copy", "duplicate"], + disabled: false + }, + { + id: 11, + title: "Paste", + icon: , + keywords: ["paste", "insert"], + disabled: false + }, + { + id: 12, + title: "Create New Folder", + description: "Create a new folder in the current directory", + icon: , + onClick: (e) => console.log("New Folder clicked"), + keywords: ["folder", "create", "new"], + disabled: false + }, + { + id: 13, + title: "Add Item", + icon: , + onClick: (e) => console.log("Add clicked"), + keywords: ["add", "create", "new"], + disabled: false + }, + { + id: 14, + title: "Remove Item", + icon: , + keywords: ["remove", "delete", "subtract"], + disabled: false + }, + { + id: 15, + title: "Bookmark Page", + icon: , + onClick: (e) => console.log("Bookmark clicked"), + keywords: ["bookmark", "save", "page"], + disabled: false + }, + { + id: 16, + title: "Refresh Page", + description: "Reload the current page", + icon: , + onClick: (e) => console.log("Refresh clicked"), + shortcut: "F5", + keywords: ["refresh", "reload", "update"], + disabled: false + }, + { + id: 17, + title: "Complete Task", + description: "Mark the task as complete", + icon: , + keywords: ["complete", "task", "finish"], + disabled: false + }, + { + id: 18, + title: "Cancel Task", + icon: , + onClick: (e) => console.log("Cancel clicked"), + keywords: ["cancel", "stop", "abort"], + disabled: false + }, + { + id: 19, + title: "Mute Sound", + icon: , + keywords: ["mute", "sound", "audio"], + disabled: false + }, + { + id: 20, + title: "Play Video", + description: "Start playing the video", + icon: , + onClick: (e) => console.log("Play clicked"), + keywords: ["play", "video", "start"], + disabled: false + }, + { + id: 21, + title: "Pause Video", + icon: , + keywords: ["pause", "video", "stop"], + disabled: false + }, + { + id: 22, + title: "Open Folder", + icon: , + description: "Open the selected folder", + keywords: ["open", "folder", "directory"], + disabled: false + }, + { + id: 23, + title: "Rename Item", + icon: , + onClick: (e) => console.log("Rename clicked"), + keywords: ["rename", "edit", "change"], + disabled: false + }, + { + id: 24, + title: "Search Documents", + icon: , + description: "Search for documents", + keywords: ["search", "documents", "find"], + disabled: false + }, + { + id: 25, + title: "Settings", + icon: , + onClick: (e) => console.log("Settings clicked"), + keywords: ["settings", "preferences", "options"], + disabled: false + } + ]; + return ( <> - - - } shortcut={"⌘ T"} onClick={() => console.log("test")}/> - } shortcut={"⌘ A"}/> - } shortcut={"⌘ I"}/> - - - } secondaryTitle={"Improve performance"}/> - } secondaryTitle={"Go to your dashboard"}/> - } secondaryTitle={"View your active projects"}/> + diff --git a/components/commandmenu/CommandMenu.tsx b/components/commandmenu/CommandMenu.tsx index 9328ec1..cdfd5b0 100644 --- a/components/commandmenu/CommandMenu.tsx +++ b/components/commandmenu/CommandMenu.tsx @@ -1,12 +1,4 @@ -import React, { - DialogHTMLAttributes, - forwardRef, - ReactNode, - useEffect, - useImperativeHandle, - useRef, - useState -} from "react"; +import React, {DialogHTMLAttributes, forwardRef, ReactNode, useEffect, useRef, useState} from "react"; import {cn} from "../../utils/cn"; import {DialogRef} from "@/components/dialog/Dialog"; import {Shortcut} from "@/components/shortcut/Shortcut"; @@ -14,95 +6,75 @@ import {Seperator} from "@/components/seperator/Seperator"; import {CornerDownLeft, MoveDown, MoveUp, Search} from "lucide-react"; import {Input} from "@/components/input/Input"; import {useHotkeys} from "react-hotkeys-hook"; -import {useOutsideClick} from "@/hooks/useOutsideClick"; +import {useOutsideClick} from "../../hooks/useOutsideClick"; import {AnimatePresence, motion} from "framer-motion"; interface CommandMenuProps extends DialogHTMLAttributes { - children: ReactNode; + items: CommandMenuItemProps[]; + showItems: CommandMenuItemProps[]; isOpen?: boolean; onClose?: () => void; animation: "slide" | "fade"; } interface CommandMenuItemProps { + id: number; title: string; - secondaryTitle?: string; + description?: string; icon?: ReactNode; - onClick?: (e: React.MouseEvent) => void; + onClick?: (e?: React.MouseEvent) => void; shortcut?: string; disabled?: boolean; selected?: boolean; + keywords: string[]; } -interface CommandMenuLabelProps { - label: string; -} +const CommandMenu = forwardRef(({ isOpen, onClose, items, showItems, animation = "fade", ...props }, ref) => { + const [searchTerm, setSearchTerm] = useState(""); + const [filteredItems, setFilteredItems] = useState(items); -const CommandMenuSeperator: React.FC = () => { - return ( -
- ); -} - -const CommandMenuLabel: React.FC = ({ label }) => { - return ( - {label} - ); -} - -const CommandMenuItem: React.FC = ({ title, secondaryTitle, icon, shortcut, onClick, selected, disabled = false }) => { - return ( -
-
- {icon} - {title} - {secondaryTitle && - {secondaryTitle} - } -
- {shortcut && - - } -
- ); -} - -const CommandMenu = forwardRef(({ isOpen, onClose, children, animation = "fade", ...props }, ref) => { const [selectedIndex, setSelectedIndex] = useState(-1); const [inputFocused, setInputFocused] = useState(true); const inputRef = useRef(null); const menuRef = useOutsideClick(() => handleClose()); - const getSelectableItems = () => { - return React.Children.toArray(children).filter( - (child) => React.isValidElement(child) && child.type === CommandMenuItem - ); - } - - const itemCount = getSelectableItems().length; - const handleClose = () => { setSelectedIndex(-1); setInputFocused(true); + setSearchTerm(''); + setFilteredItems(showItems); onClose?.(); } + const handleSearch = (e: React.ChangeEvent) => { + const value = e.target.value; + setSearchTerm(value); + setFilteredItems(items.filter(item => { + return item.keywords.some(keyword => keyword.toLowerCase().includes(value.toLowerCase())); + })); + + if (value.length === 0) { + setSelectedIndex(-1); + setFilteredItems(showItems); + } + } + + useEffect(() => { + if (isOpen) { + setFilteredItems(showItems); + } + }, []); + useHotkeys('esc', () => { handleClose(); }) useHotkeys('enter', (e) => { - if (selectedIndex >= 0 && selectedIndex < itemCount) { - const selectedItem = getSelectableItems()[selectedIndex] as React.ReactElement; + if (selectedIndex >= 0 && selectedIndex < filteredItems.length) { e.preventDefault(); e.stopPropagation(); - selectedItem.props.onClick?.(); + filteredItems[selectedIndex].onClick(); handleClose(); } }) @@ -110,14 +82,14 @@ const CommandMenu = forwardRef(({ isOpen, onClose, useHotkeys('up', () => { if (isOpen) { setInputFocused(false) - setSelectedIndex((prev) => (prev <= 0 ? itemCount - 1 : prev - 1)) + setSelectedIndex((prev) => (prev <= 0 ? filteredItems.length - 1 : prev - 1)) } }) useHotkeys('down', () => { if (isOpen) { setInputFocused(false) - setSelectedIndex((prev) => (prev >= itemCount - 1 ? 0 : prev + 1)) + setSelectedIndex((prev) => (prev >= filteredItems.length - 1 ? 0 : prev + 1)) } }) @@ -152,36 +124,49 @@ const CommandMenu = forwardRef(({ isOpen, onClose, - setInputFocused(true)} - onBlur={() => setInputFocused(false)} - ref={inputRef} +
-
- {(() => { - let itemIndex = -1; - return React.Children.map(children, (child) => { - if (React.isValidElement(child) && child.type === CommandMenuItem) { - itemIndex++; - return React.cloneElement(child, { - ...child.props, - selected: itemIndex === selectedIndex, - onClick: (e: React.MouseEvent) => { - child.props.onClick?.(e); - handleClose(); - } - }); +
+ {filteredItems.length === 0 && +
+ {"No results found"} +
+ } + {filteredItems.length > 0 && filteredItems.map((item) => ( +
+
+ {item.icon} + {item.title} + {item.description && + {item.description} + } +
+ {item.shortcut && + } - return child; - }); - })()} +
+ ))}
-
+
{"Close"} @@ -210,9 +195,4 @@ const CommandMenu = forwardRef(({ isOpen, onClose, ); }); -export { - CommandMenu, - CommandMenuItem, - CommandMenuLabel, - CommandMenuSeperator -}; \ No newline at end of file +export { CommandMenu, CommandMenuItemProps }; \ No newline at end of file diff --git a/components/tooltip/Tooltip.tsx b/components/tooltip/Tooltip.tsx index 9cb7342..9e18799 100644 --- a/components/tooltip/Tooltip.tsx +++ b/components/tooltip/Tooltip.tsx @@ -26,69 +26,72 @@ const Tooltip: React.FC if (!tooltipRef.current) return; const tooltipRect = tooltipRef.current.getBoundingClientRect(); + const scrollX = window.pageXOffset || document.documentElement.scrollLeft; + const scrollY = window.pageYOffset || document.documentElement.scrollTop; + let x: number; let y: number; switch (anchor) { //Top case "tl": - x = trigger.left; - y = trigger.top - offset - tooltipRect.height; + x = trigger.left + scrollX; + y = trigger.top + scrollY - offset - tooltipRect.height; break; case "tc": - x = trigger.left + (trigger.width / 2) - (tooltipRect.width / 2); - y = trigger.top - offset - tooltipRect.height; + x = trigger.left + scrollX + (trigger.width / 2) - (tooltipRect.width / 2); + y = trigger.top + scrollY - offset - tooltipRect.height; break; case "tr": - x = trigger.right - tooltipRect.width; - y = trigger.top - offset - tooltipRect.height; + x = trigger.right + scrollX - tooltipRect.width; + y = trigger.top + scrollY - offset - tooltipRect.height; break; // Bottom case "bl": - x = trigger.left; - y = trigger.bottom + offset; + x = trigger.left + scrollX; + y = trigger.bottom + scrollY + offset; break; case "bc": - x = trigger.left + (trigger.width / 2) - (tooltipRect.width / 2); - y = trigger.bottom + offset; + x = trigger.left + scrollX + (trigger.width / 2) - (tooltipRect.width / 2); + y = trigger.bottom + scrollY + offset; break; case "br": - x = trigger.right - tooltipRect.width; - y = trigger.bottom + offset; + x = trigger.right + scrollX - tooltipRect.width; + y = trigger.bottom + scrollY + offset; break; // Left case "lt": - x = trigger.left - offset - tooltipRect.width; - y = trigger.top; + x = trigger.left + scrollX - offset - tooltipRect.width; + y = trigger.top + scrollY; break; case "lc": - x = trigger.left - offset - tooltipRect.width; - y = trigger.top + (trigger.height / 2) - (tooltipRect.height / 2); + x = trigger.left + scrollX - offset - tooltipRect.width; + y = trigger.top + scrollY + (trigger.height / 2) - (tooltipRect.height / 2); break; case "lb": - x = trigger.left - offset - tooltipRect.width; - y = trigger.bottom - tooltipRect.height; + x = trigger.left + scrollX - offset - tooltipRect.width; + y = trigger.bottom + scrollY - tooltipRect.height; break; // Right case "rt": - x = trigger.right + offset; - y = trigger.top; + x = trigger.right + scrollX + offset; + y = trigger.top + scrollY; break; case "rc": - x = trigger.right + offset; - y = trigger.top + (trigger.height / 2) - (tooltipRect.height / 2); + x = trigger.right + scrollX + offset; + y = trigger.top + scrollY + (trigger.height / 2) - (tooltipRect.height / 2); break; case "rb": - x = trigger.right + offset; - y = trigger.bottom - tooltipRect.height; + x = trigger.right + scrollX + offset; + y = trigger.bottom + scrollY - tooltipRect.height; break; default: - x = trigger.left; - y = trigger.top; + x = trigger.left + scrollX; + y = trigger.top + scrollY; } setPosition({ x, y }); diff --git a/package.json b/package.json index d8a58f1..c0d7974 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@marraph/daisy", - "version": "1.0.139", + "version": "1.0.140", "description": "Daisy is a component library for the marraph organisation", "scripts": { "dev": "next dev",