diff --git a/src/App.tsx b/src/App.tsx index 555c5ea..ce56fb7 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -6,6 +6,8 @@ import { Toolbar } from './components/Toolbar'; import { FileTree, FileNode } from './components/FileTree'; import { FileTabs } from './components/FileTabs'; import { ConsolePanel } from './components/ConsolePanel'; +import { FileSearchModal } from './components/FileSearchModal'; +import { CommandPalette, createDefaultCommands } from './components/CommandPalette'; import { ResizablePanelGroup, ResizablePanel, ResizableHandle } from './components/ui/resizable'; import { useIsMobile } from './hooks/use-mobile'; import { useKeyboardShortcuts } from './hooks/use-keyboard-shortcuts'; @@ -30,6 +32,8 @@ function App() { const [showTemplates, setShowTemplates] = useState(false); const [showPreview, setShowPreview] = useState(false); const [showNewProject, setShowNewProject] = useState(false); + const [showFileSearch, setShowFileSearch] = useState(false); + const [showCommandPalette, setShowCommandPalette] = useState(false); const [code, setCode] = useState(''); const isMobile = useIsMobile(); @@ -61,8 +65,7 @@ function App() { meta: true, ctrl: true, handler: () => { - // TODO: Implement file search modal - toast.info('File search coming soon! (Cmd/Ctrl+P)'); + setShowFileSearch(true); captureEvent('keyboard_shortcut', { action: 'file_search' }); }, description: 'Quick file search', @@ -72,8 +75,7 @@ function App() { meta: true, ctrl: true, handler: () => { - // TODO: Implement command palette - toast.info('Command palette coming soon! (Cmd/Ctrl+K)'); + setShowCommandPalette(true); captureEvent('keyboard_shortcut', { action: 'command_palette' }); }, description: 'Command palette', @@ -435,6 +437,46 @@ end)`, + + {/* File Search Modal (Cmd/Ctrl+P) */} + setShowFileSearch(false)} + files={files || []} + onFileSelect={handleFileSelect} + /> + + {/* Command Palette (Cmd/Ctrl+K) */} + setShowCommandPalette(false)} + commands={createDefaultCommands({ + onNewProject: () => setShowNewProject(true), + onTemplates: () => setShowTemplates(true), + onPreview: () => setShowPreview(true), + onExport: async () => { + const blob = new Blob([currentCode], { type: 'text/plain' }); + const url = URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = 'script.lua'; + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); + URL.revokeObjectURL(url); + toast.success('Script exported!'); + }, + onCopy: async () => { + try { + await navigator.clipboard.writeText(currentCode); + toast.success('Code copied to clipboard!'); + } catch (error) { + toast.error('Failed to copy code'); + } + }, + })} + /> + {!user && ( + ))} + + )} + + +
+
+ + ↑↓ Navigate + + + Execute + + + Esc Close + +
+ {filteredCommands.length} commands +
+ + + ); +} + +// Predefined command templates +export const createDefaultCommands = (actions: { + onNewProject: () => void; + onTemplates: () => void; + onPreview: () => void; + onExport: () => void; + onCopy: () => void; +}): Command[] => [ + { + id: 'new-project', + label: 'New Project', + description: 'Create a new project from template', + icon: , + action: actions.onNewProject, + keywords: ['create', 'start', 'begin'], + }, + { + id: 'templates', + label: 'Browse Templates', + description: 'View and select code templates', + icon: , + action: actions.onTemplates, + keywords: ['snippets', 'examples', 'boilerplate'], + }, + { + id: 'preview', + label: 'Preview All Platforms', + description: 'Open multi-platform preview', + icon: , + action: actions.onPreview, + keywords: ['run', 'test', 'demo'], + }, + { + id: 'copy', + label: 'Copy Code', + description: 'Copy current code to clipboard', + icon: , + action: actions.onCopy, + keywords: ['clipboard', 'paste'], + }, + { + id: 'export', + label: 'Export Script', + description: 'Download code as .lua file', + icon: , + action: actions.onExport, + keywords: ['download', 'save', 'file'], + }, +]; diff --git a/src/components/FileSearchModal.tsx b/src/components/FileSearchModal.tsx new file mode 100644 index 0000000..299a649 --- /dev/null +++ b/src/components/FileSearchModal.tsx @@ -0,0 +1,155 @@ +import { useState, useEffect, useMemo } from 'react'; +import { Dialog, DialogContent } from './ui/dialog'; +import { Input } from './ui/input'; +import { ScrollArea } from './ui/scroll-area'; +import { FileNode } from './FileTree'; +import { MagnifyingGlass, File, Folder } from '@phosphor-icons/react'; + +interface FileSearchModalProps { + open: boolean; + onClose: () => void; + files: FileNode[]; + onFileSelect: (file: FileNode) => void; +} + +export function FileSearchModal({ open, onClose, files, onFileSelect }: FileSearchModalProps) { + const [search, setSearch] = useState(''); + const [selectedIndex, setSelectedIndex] = useState(0); + + // Flatten file tree to searchable list + const flattenFiles = (nodes: FileNode[], path = ''): Array<{ file: FileNode; path: string }> => { + let result: Array<{ file: FileNode; path: string }> = []; + + for (const node of nodes) { + const currentPath = path ? `${path}/${node.name}` : node.name; + + if (node.type === 'file') { + result.push({ file: node, path: currentPath }); + } + + if (node.children) { + result = [...result, ...flattenFiles(node.children, currentPath)]; + } + } + + return result; + }; + + const allFiles = useMemo(() => flattenFiles(files), [files]); + + // Filter files based on search + const filteredFiles = useMemo(() => { + if (!search) return allFiles; + + const searchLower = search.toLowerCase(); + return allFiles.filter(({ file, path }) => + path.toLowerCase().includes(searchLower) || + file.name.toLowerCase().includes(searchLower) + ); + }, [allFiles, search]); + + // Reset when opened/closed + useEffect(() => { + if (open) { + setSearch(''); + setSelectedIndex(0); + } + }, [open]); + + // Keyboard navigation + useEffect(() => { + if (!open) return; + + const handleKeyDown = (e: KeyboardEvent) => { + if (e.key === 'ArrowDown') { + e.preventDefault(); + setSelectedIndex((prev) => Math.min(prev + 1, filteredFiles.length - 1)); + } else if (e.key === 'ArrowUp') { + e.preventDefault(); + setSelectedIndex((prev) => Math.max(prev - 1, 0)); + } else if (e.key === 'Enter') { + e.preventDefault(); + if (filteredFiles[selectedIndex]) { + handleSelect(filteredFiles[selectedIndex].file); + } + } + }; + + window.addEventListener('keydown', handleKeyDown); + return () => window.removeEventListener('keydown', handleKeyDown); + }, [open, filteredFiles, selectedIndex]); + + const handleSelect = (file: FileNode) => { + onFileSelect(file); + onClose(); + }; + + return ( + + +
+ + { + setSearch(e.target.value); + setSelectedIndex(0); + }} + placeholder="Search files... (type to filter)" + className="border-0 focus-visible:ring-0 focus-visible:ring-offset-0 text-base" + autoFocus + /> +
+ + + {filteredFiles.length === 0 ? ( +
+ +

No files found

+ {search && ( +

Try a different search term

+ )} +
+ ) : ( +
+ {filteredFiles.map(({ file, path }, index) => ( + + ))} +
+ )} +
+ +
+
+ + ↑↓ Navigate + + + Select + + + Esc Close + +
+ {filteredFiles.length} files +
+
+
+ ); +}