From 30f35ae07f1e22f697887d393eb4db4ddb87e46b Mon Sep 17 00:00:00 2001 From: johndoe6345789 Date: Sat, 27 Dec 2025 18:12:29 +0000 Subject: [PATCH] refactor: split command dialog components --- .../ui/organisms/dialogs/Command.tsx | 351 +----------------- .../dialogs/command/CommandDialogShell.tsx | 72 ++++ .../organisms/dialogs/command/CommandList.tsx | 138 +++++++ .../dialogs/command/command.types.ts | 68 ++++ .../ui/organisms/dialogs/command/index.ts | 59 +++ .../dialogs/command/useCommandShortcuts.ts | 19 + 6 files changed, 357 insertions(+), 350 deletions(-) create mode 100644 frontends/nextjs/src/components/ui/organisms/dialogs/command/CommandDialogShell.tsx create mode 100644 frontends/nextjs/src/components/ui/organisms/dialogs/command/CommandList.tsx create mode 100644 frontends/nextjs/src/components/ui/organisms/dialogs/command/command.types.ts create mode 100644 frontends/nextjs/src/components/ui/organisms/dialogs/command/index.ts create mode 100644 frontends/nextjs/src/components/ui/organisms/dialogs/command/useCommandShortcuts.ts diff --git a/frontends/nextjs/src/components/ui/organisms/dialogs/Command.tsx b/frontends/nextjs/src/components/ui/organisms/dialogs/Command.tsx index b33121a3a..7005d4e05 100644 --- a/frontends/nextjs/src/components/ui/organisms/dialogs/Command.tsx +++ b/frontends/nextjs/src/components/ui/organisms/dialogs/Command.tsx @@ -1,352 +1,3 @@ -// TODO: Split this file (351 LOC) into smaller organisms (<150 LOC each) 'use client' -import { forwardRef, ReactNode, useEffect } from 'react' -import { - Box, - Dialog, - DialogContent, - InputBase, - List, - ListItem, - ListItemButton, - ListItemIcon, - ListItemText, - Divider, - Typography, - Paper, -} from '@mui/material' -import SearchIcon from '@mui/icons-material/Search' - -// Custom Kbd component since MUI doesn't export one -const Kbd = ({ children, ...props }: { children: ReactNode; [key: string]: unknown }) => ( - - {children} - -) - -// Types -interface CommandItem { - id: string - label: string - description?: string - icon?: ReactNode - shortcut?: string[] - onSelect?: () => void - disabled?: boolean - keywords?: string[] -} - -interface CommandGroup { - heading?: string - items: CommandItem[] -} - -// Command Dialog -interface CommandProps { - open: boolean - onClose: () => void - children: ReactNode -} - -const Command = forwardRef( - ({ open, onClose, children, ...props }, ref) => { - return ( - - {children} - - ) - } -) -Command.displayName = 'Command' - -// CommandInput -interface CommandInputProps { - placeholder?: string - value?: string - onChange?: (value: string) => void - autoFocus?: boolean -} - -const CommandInput = forwardRef( - ({ placeholder = 'Type a command or search...', value, onChange, autoFocus = true, ...props }, ref) => { - return ( - - - onChange?.(e.target.value)} - autoFocus={autoFocus} - fullWidth - sx={{ - fontSize: '0.875rem', - '& input': { - p: 0, - }, - }} - {...props} - /> - - ) - } -) -CommandInput.displayName = 'CommandInput' - -// CommandList -interface CommandListProps { - children: ReactNode -} - -const CommandList = forwardRef( - ({ children, ...props }, ref) => { - return ( - - {children} - - ) - } -) -CommandList.displayName = 'CommandList' - -// CommandEmpty -interface CommandEmptyProps { - children?: ReactNode -} - -const CommandEmpty = forwardRef( - ({ children = 'No results found.', ...props }, ref) => { - return ( - - {children} - - ) - } -) -CommandEmpty.displayName = 'CommandEmpty' - -// CommandGroup -interface CommandGroupProps { - heading?: string - children: ReactNode -} - -const CommandGroup = forwardRef( - ({ heading, children, ...props }, ref) => { - return ( - - {heading && ( - - {heading} - - )} - - {children} - - - ) - } -) -CommandGroup.displayName = 'CommandGroup' - -// CommandItem -interface CommandItemProps { - children?: ReactNode - icon?: ReactNode - shortcut?: string[] - onSelect?: () => void - disabled?: boolean - selected?: boolean -} - -const CommandItem = forwardRef( - ({ children, icon, shortcut, onSelect, disabled = false, selected = false, ...props }, ref) => { - return ( - - - {icon && ( - - {icon} - - )} - - {shortcut && shortcut.length > 0 && ( - - {shortcut.map((key, index) => ( - - {key} - - ))} - - )} - - - ) - } -) -CommandItem.displayName = 'CommandItem' - -// CommandSeparator -const CommandSeparator = forwardRef>( - (props, ref) => { - return - } -) -CommandSeparator.displayName = 'CommandSeparator' - -// CommandShortcut -interface CommandShortcutProps { - children: ReactNode -} - -const CommandShortcut = forwardRef( - ({ children, ...props }, ref) => { - return ( - - {children} - - ) - } -) -CommandShortcut.displayName = 'CommandShortcut' - -// Hook for command dialog keyboard shortcuts -function useCommandShortcut(key: string, callback: () => void) { - useEffect(() => { - const handleKeyDown = (event: globalThis.KeyboardEvent) => { - if ((event.metaKey || event.ctrlKey) && event.key === key) { - event.preventDefault() - callback() - } - } - - document.addEventListener('keydown', handleKeyDown) - return () => document.removeEventListener('keydown', handleKeyDown) - }, [key, callback]) -} - -export { - Command, - CommandInput, - CommandList, - CommandEmpty, - CommandGroup, - CommandItem, - CommandSeparator, - CommandShortcut, - useCommandShortcut, -} -export type { CommandItem as CommandItemType, CommandGroup as CommandGroupType } +export * from './command' diff --git a/frontends/nextjs/src/components/ui/organisms/dialogs/command/CommandDialogShell.tsx b/frontends/nextjs/src/components/ui/organisms/dialogs/command/CommandDialogShell.tsx new file mode 100644 index 000000000..ed6b09e05 --- /dev/null +++ b/frontends/nextjs/src/components/ui/organisms/dialogs/command/CommandDialogShell.tsx @@ -0,0 +1,72 @@ +'use client' + +import { forwardRef } from 'react' +import { Box, Dialog, InputBase } from '@mui/material' +import SearchIcon from '@mui/icons-material/Search' + +import type { CommandDialogProps, CommandInputProps } from './command.types' + +const CommandDialogRoot = forwardRef( + ({ open, onClose, children, ...props }, ref) => { + return ( + + {children} + + ) + } +) +CommandDialogRoot.displayName = 'CommandDialogRoot' + +const CommandInput = forwardRef( + ({ placeholder = 'Type a command or search...', value, onChange, autoFocus = true, ...props }, ref) => { + return ( + + + onChange?.(e.target.value)} + autoFocus={autoFocus} + fullWidth + sx={{ + fontSize: '0.875rem', + '& input': { + p: 0, + }, + }} + {...props} + /> + + ) + } +) +CommandInput.displayName = 'CommandInput' + +export { CommandDialogRoot, CommandInput } diff --git a/frontends/nextjs/src/components/ui/organisms/dialogs/command/CommandList.tsx b/frontends/nextjs/src/components/ui/organisms/dialogs/command/CommandList.tsx new file mode 100644 index 000000000..596fe2b66 --- /dev/null +++ b/frontends/nextjs/src/components/ui/organisms/dialogs/command/CommandList.tsx @@ -0,0 +1,138 @@ +'use client' + +import { forwardRef } from 'react' +import { + Box, + Divider, + List, + ListItem, + ListItemButton, + ListItemIcon, + ListItemText, + Typography, +} from '@mui/material' + +import type { + CommandEmptyProps, + CommandGroupProps, + CommandItemProps, + CommandListProps, + CommandShortcutProps, +} from './command.types' + +const CommandList = forwardRef(({ children, ...props }, ref) => { + return ( + + {children} + + ) +}) +CommandList.displayName = 'CommandList' + +const CommandEmpty = forwardRef( + ({ children = 'No results found.', ...props }, ref) => { + return ( + + {children} + + ) + } +) +CommandEmpty.displayName = 'CommandEmpty' + +const CommandGroup = forwardRef(({ heading, children, ...props }, ref) => { + return ( + + {heading && ( + + {heading} + + )} + + {children} + + + ) +}) +CommandGroup.displayName = 'CommandGroup' + +const CommandItem = forwardRef( + ({ children, icon, shortcut, onSelect, disabled = false, selected = false, ...props }, ref) => { + return ( + + + {icon && ( + + {icon} + + )} + + {shortcut && shortcut.length > 0 && ( + + {shortcut.map((key, index) => ( + + {key} + + ))} + + )} + + + ) + } +) +CommandItem.displayName = 'CommandItem' + +const CommandSeparator = forwardRef>((props, ref) => { + return +}) +CommandSeparator.displayName = 'CommandSeparator' + +const CommandShortcut = forwardRef(({ children, ...props }, ref) => { + return ( + + {children} + + ) +}) +CommandShortcut.displayName = 'CommandShortcut' + +export { + CommandEmpty, + CommandGroup, + CommandItem, + CommandList, + CommandSeparator, + CommandShortcut, +} diff --git a/frontends/nextjs/src/components/ui/organisms/dialogs/command/command.types.ts b/frontends/nextjs/src/components/ui/organisms/dialogs/command/command.types.ts new file mode 100644 index 000000000..e23e6da79 --- /dev/null +++ b/frontends/nextjs/src/components/ui/organisms/dialogs/command/command.types.ts @@ -0,0 +1,68 @@ +import { ReactNode } from 'react' + +interface CommandItem { + id: string + label: string + description?: string + icon?: ReactNode + shortcut?: string[] + onSelect?: () => void + disabled?: boolean + keywords?: string[] +} + +interface CommandGroup { + heading?: string + items: CommandItem[] +} + +interface CommandDialogProps { + open: boolean + onClose: () => void + children: ReactNode +} + +interface CommandInputProps { + placeholder?: string + value?: string + onChange?: (value: string) => void + autoFocus?: boolean +} + +interface CommandListProps { + children: ReactNode +} + +interface CommandEmptyProps { + children?: ReactNode +} + +interface CommandGroupProps { + heading?: string + children: ReactNode +} + +interface CommandItemProps { + children?: ReactNode + icon?: ReactNode + shortcut?: string[] + onSelect?: () => void + disabled?: boolean + selected?: boolean +} + +interface CommandShortcutProps { + children: ReactNode +} + +export type { + CommandDialogProps, + CommandEmptyProps, + CommandGroup, + CommandGroupProps, + CommandInputProps, + CommandItem, + CommandItemProps, + CommandListProps, + CommandShortcutProps, +} diff --git a/frontends/nextjs/src/components/ui/organisms/dialogs/command/index.ts b/frontends/nextjs/src/components/ui/organisms/dialogs/command/index.ts new file mode 100644 index 000000000..961f7c9a2 --- /dev/null +++ b/frontends/nextjs/src/components/ui/organisms/dialogs/command/index.ts @@ -0,0 +1,59 @@ +"use client" + +import { CommandDialogRoot, CommandInput } from './CommandDialogShell' +import { + CommandEmpty, + CommandGroup, + CommandItem, + CommandList, + CommandSeparator, + CommandShortcut, +} from './CommandList' +import { useCommandShortcut } from './useCommandShortcuts' + +import type { + CommandDialogProps, + CommandEmptyProps, + CommandGroup as CommandGroupType, + CommandGroupProps, + CommandInputProps, + CommandItem as CommandItemType, + CommandItemProps, + CommandListProps, + CommandShortcutProps, +} from './command.types' + +const CommandDialog = Object.assign(CommandDialogRoot, { + Input: CommandInput, + List: CommandList, + Empty: CommandEmpty, + Group: CommandGroup, + Item: CommandItem, + Separator: CommandSeparator, + Shortcut: CommandShortcut, +}) + +export { + CommandDialog, + CommandDialogRoot, + CommandEmpty, + CommandGroup, + CommandInput, + CommandItem, + CommandList, + CommandSeparator, + CommandShortcut, + useCommandShortcut, +} + +export type { + CommandDialogProps, + CommandEmptyProps, + CommandGroupProps, + CommandGroupType, + CommandInputProps, + CommandItemProps, + CommandItemType, + CommandListProps, + CommandShortcutProps, +} diff --git a/frontends/nextjs/src/components/ui/organisms/dialogs/command/useCommandShortcuts.ts b/frontends/nextjs/src/components/ui/organisms/dialogs/command/useCommandShortcuts.ts new file mode 100644 index 000000000..15aa98cb4 --- /dev/null +++ b/frontends/nextjs/src/components/ui/organisms/dialogs/command/useCommandShortcuts.ts @@ -0,0 +1,19 @@ +'use client' + +import { useEffect } from 'react' + +const useCommandShortcut = (key: string, callback: () => void) => { + useEffect(() => { + const handleKeyDown = (event: globalThis.KeyboardEvent) => { + if ((event.metaKey || event.ctrlKey) && event.key === key) { + event.preventDefault() + callback() + } + } + + document.addEventListener('keydown', handleKeyDown) + return () => document.removeEventListener('keydown', handleKeyDown) + }, [key, callback]) +} + +export { useCommandShortcut }