From a4204a98ba013f3ed2fb9b5df68502b64a951726 Mon Sep 17 00:00:00 2001 From: johndoe6345789 Date: Fri, 16 Jan 2026 03:28:35 +0000 Subject: [PATCH] Generated by Spark: Project has alot of features, a search box would be nice. --- PRD.md | 9 +- src/App.tsx | 42 ++- src/components/GlobalSearch.tsx | 508 ++++++++++++++++++++++++++++++++ 3 files changed, 556 insertions(+), 3 deletions(-) create mode 100644 src/components/GlobalSearch.tsx diff --git a/PRD.md b/PRD.md index 4841e91..eb16c22 100644 --- a/PRD.md +++ b/PRD.md @@ -1,6 +1,6 @@ # Planning Guide -A visual low-code platform for generating Next.js applications with Material UI styling, integrated Monaco code editor, Prisma schema designer, and persistent project management. +A visual low-code platform for generating Next.js applications with Material UI styling, integrated Monaco code editor, Prisma schema designer, and persistent project management with comprehensive global search. **Experience Qualities**: 1. **Empowering** - Users feel in control with both visual and code-level editing capabilities @@ -12,6 +12,13 @@ This is a full-featured low-code IDE with multiple integrated tools (code editor ## Essential Features +### Global Search (Ctrl+K) +- **Functionality**: Comprehensive fuzzy search across all application features, files, models, components, workflows, lambdas, tests, and navigation +- **Purpose**: Quickly find and navigate to any part of the application without clicking through multiple tabs +- **Trigger**: Clicking the Search button in header or pressing Ctrl+K (Cmd+K on Mac) +- **Progression**: Press Ctrl+K → Type search query → See filtered results grouped by category → Select result → Navigate to that item/tab +- **Success criteria**: Search is fast and responsive; results are relevantly ranked; all major entities are searchable; keyboard navigation works; shows helpful metadata (file paths, descriptions, counts) + ### Project Save/Load Management - **Functionality**: Complete project persistence system using Spark KV database with save, load, duplicate, export, import, and delete operations - **Purpose**: Allow users to work on multiple projects over time without losing progress, share projects via JSON export, and maintain a library of saved work diff --git a/src/App.tsx b/src/App.tsx index f9e22be..c726d1a 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -6,7 +6,7 @@ import { Button } from '@/components/ui/button' import { Badge } from '@/components/ui/badge' import { Card } from '@/components/ui/card' import { ResizableHandle, ResizablePanel, ResizablePanelGroup } from '@/components/ui/resizable' -import { Code, Database, Tree, PaintBrush, Download, Sparkle, Flask, BookOpen, Play, Wrench, Gear, Cube, FileText, ChartBar, Keyboard, FlowArrow, Faders, DeviceMobile, Image } from '@phosphor-icons/react' +import { Code, Database, Tree, PaintBrush, Download, Sparkle, Flask, BookOpen, Play, Wrench, Gear, Cube, FileText, ChartBar, Keyboard, FlowArrow, Faders, DeviceMobile, Image, MagnifyingGlass } from '@phosphor-icons/react' import { ProjectFile, PrismaModel, ComponentNode, ComponentTree, ThemeConfig, PlaywrightTest, StorybookStory, UnitTest, FlaskConfig, NextJsConfig, NpmSettings, Workflow, Lambda, FeatureToggles, Project } from '@/types/project' import { CodeEditor } from '@/components/CodeEditor' import { ModelDesigner } from '@/components/ModelDesigner' @@ -33,6 +33,7 @@ import { PWAUpdatePrompt } from '@/components/PWAUpdatePrompt' import { PWAStatusBar } from '@/components/PWAStatusBar' import { PWASettings } from '@/components/PWASettings' import { FaviconDesigner } from '@/components/FaviconDesigner' +import { GlobalSearch } from '@/components/GlobalSearch' import { useKeyboardShortcuts } from '@/hooks/use-keyboard-shortcuts' import { generateNextJSProject, generatePrismaSchema, generateMUITheme, generatePlaywrightTests, generateStorybookStories, generateUnitTests, generateFlaskApp } from '@/lib/generators' import { AIService } from '@/lib/ai-service' @@ -192,6 +193,7 @@ function App() { const [activeTab, setActiveTab] = useState('dashboard') const [exportDialogOpen, setExportDialogOpen] = useState(false) const [shortcutsDialogOpen, setShortcutsDialogOpen] = useState(false) + const [searchDialogOpen, setSearchDialogOpen] = useState(false) const [generatedCode, setGeneratedCode] = useState>({}) const safeFiles = files || [] @@ -286,6 +288,12 @@ function App() { description: 'Go to Favicon Designer', action: () => setActiveTab('favicon'), }] : []), + { + key: 'k', + ctrl: true, + description: 'Search everything', + action: () => setSearchDialogOpen(true), + }, { key: 'e', ctrl: true, @@ -293,8 +301,9 @@ function App() { action: () => handleExportProject(), }, { - key: 'k', + key: 'g', ctrl: true, + shift: true, description: 'AI Generate', action: () => handleGenerateWithAI(), }, @@ -531,6 +540,17 @@ Navigate to the backend directory and follow the setup instructions.
+ + { + setActiveTab(tab) + }} + onFileSelect={setActiveFileId} + /> +
) diff --git a/src/components/GlobalSearch.tsx b/src/components/GlobalSearch.tsx new file mode 100644 index 0000000..91dcc04 --- /dev/null +++ b/src/components/GlobalSearch.tsx @@ -0,0 +1,508 @@ +import { useState, useMemo, useEffect } from 'react' +import { + CommandDialog, + CommandEmpty, + CommandGroup, + CommandInput, + CommandItem, + CommandList, + CommandSeparator, +} from '@/components/ui/command' +import { + Code, + Database, + Tree, + PaintBrush, + Flask, + Play, + BookOpen, + Cube, + FlowArrow, + Wrench, + FileText, + Gear, + DeviceMobile, + Faders, + ChartBar, + Image, + File, + Folder, + MagnifyingGlass, +} from '@phosphor-icons/react' +import { ProjectFile, PrismaModel, ComponentNode, ComponentTree, Workflow, Lambda, PlaywrightTest, StorybookStory, UnitTest } from '@/types/project' +import { Badge } from '@/components/ui/badge' + +interface SearchResult { + id: string + title: string + subtitle?: string + category: string + icon: React.ReactNode + action: () => void + tags?: string[] +} + +interface GlobalSearchProps { + open: boolean + onOpenChange: (open: boolean) => void + files: ProjectFile[] + models: PrismaModel[] + components: ComponentNode[] + componentTrees: ComponentTree[] + workflows: Workflow[] + lambdas: Lambda[] + playwrightTests: PlaywrightTest[] + storybookStories: StorybookStory[] + unitTests: UnitTest[] + onNavigate: (tab: string, itemId?: string) => void + onFileSelect: (fileId: string) => void +} + +export function GlobalSearch({ + open, + onOpenChange, + files, + models, + components, + componentTrees, + workflows, + lambdas, + playwrightTests, + storybookStories, + unitTests, + onNavigate, + onFileSelect, +}: GlobalSearchProps) { + const [searchQuery, setSearchQuery] = useState('') + + useEffect(() => { + if (!open) { + setSearchQuery('') + } + }, [open]) + + const allResults = useMemo(() => { + const results: SearchResult[] = [] + + results.push({ + id: 'nav-dashboard', + title: 'Dashboard', + subtitle: 'View project overview and statistics', + category: 'Navigation', + icon: , + action: () => onNavigate('dashboard'), + tags: ['home', 'overview', 'stats', 'metrics'], + }) + + results.push({ + id: 'nav-code', + title: 'Code Editor', + subtitle: 'Edit project files with Monaco', + category: 'Navigation', + icon: , + action: () => onNavigate('code'), + tags: ['editor', 'monaco', 'typescript', 'javascript'], + }) + + results.push({ + id: 'nav-models', + title: 'Models', + subtitle: 'Design Prisma database models', + category: 'Navigation', + icon: , + action: () => onNavigate('models'), + tags: ['prisma', 'database', 'schema', 'orm'], + }) + + results.push({ + id: 'nav-components', + title: 'Components', + subtitle: 'Build React components', + category: 'Navigation', + icon: , + action: () => onNavigate('components'), + tags: ['react', 'mui', 'ui', 'design'], + }) + + results.push({ + id: 'nav-component-trees', + title: 'Component Trees', + subtitle: 'Manage component hierarchies', + category: 'Navigation', + icon: , + action: () => onNavigate('component-trees'), + tags: ['hierarchy', 'structure', 'layout'], + }) + + results.push({ + id: 'nav-workflows', + title: 'Workflows', + subtitle: 'Design n8n-style workflows', + category: 'Navigation', + icon: , + action: () => onNavigate('workflows'), + tags: ['automation', 'n8n', 'flow', 'pipeline'], + }) + + results.push({ + id: 'nav-lambdas', + title: 'Lambdas', + subtitle: 'Create serverless functions', + category: 'Navigation', + icon: , + action: () => onNavigate('lambdas'), + tags: ['serverless', 'functions', 'api'], + }) + + results.push({ + id: 'nav-styling', + title: 'Styling', + subtitle: 'Design themes and colors', + category: 'Navigation', + icon: , + action: () => onNavigate('styling'), + tags: ['theme', 'colors', 'css', 'design'], + }) + + results.push({ + id: 'nav-flask', + title: 'Flask API', + subtitle: 'Configure Flask backend', + category: 'Navigation', + icon: , + action: () => onNavigate('flask'), + tags: ['python', 'backend', 'api', 'rest'], + }) + + results.push({ + id: 'nav-playwright', + title: 'Playwright Tests', + subtitle: 'E2E testing configuration', + category: 'Navigation', + icon: , + action: () => onNavigate('playwright'), + tags: ['testing', 'e2e', 'automation'], + }) + + results.push({ + id: 'nav-storybook', + title: 'Storybook', + subtitle: 'Component documentation', + category: 'Navigation', + icon: , + action: () => onNavigate('storybook'), + tags: ['documentation', 'components', 'stories'], + }) + + results.push({ + id: 'nav-unit-tests', + title: 'Unit Tests', + subtitle: 'Configure unit testing', + category: 'Navigation', + icon: , + action: () => onNavigate('unit-tests'), + tags: ['testing', 'jest', 'vitest'], + }) + + results.push({ + id: 'nav-errors', + title: 'Error Repair', + subtitle: 'Auto-detect and fix errors', + category: 'Navigation', + icon: , + action: () => onNavigate('errors'), + tags: ['debugging', 'errors', 'fixes'], + }) + + results.push({ + id: 'nav-docs', + title: 'Documentation', + subtitle: 'View project documentation', + category: 'Navigation', + icon: , + action: () => onNavigate('docs'), + tags: ['readme', 'guide', 'help'], + }) + + results.push({ + id: 'nav-sass', + title: 'Sass Styles', + subtitle: 'Custom Sass styling', + category: 'Navigation', + icon: , + action: () => onNavigate('sass'), + tags: ['sass', 'scss', 'styles', 'css'], + }) + + results.push({ + id: 'nav-favicon', + title: 'Favicon Designer', + subtitle: 'Design app icons', + category: 'Navigation', + icon: , + action: () => onNavigate('favicon'), + tags: ['icon', 'logo', 'design'], + }) + + results.push({ + id: 'nav-settings', + title: 'Settings', + subtitle: 'Configure Next.js and npm', + category: 'Navigation', + icon: , + action: () => onNavigate('settings'), + tags: ['config', 'nextjs', 'npm', 'packages'], + }) + + results.push({ + id: 'nav-pwa', + title: 'PWA Settings', + subtitle: 'Progressive Web App config', + category: 'Navigation', + icon: , + action: () => onNavigate('pwa'), + tags: ['mobile', 'install', 'offline'], + }) + + results.push({ + id: 'nav-features', + title: 'Feature Toggles', + subtitle: 'Enable or disable features', + category: 'Navigation', + icon: , + action: () => onNavigate('features'), + tags: ['settings', 'toggles', 'enable'], + }) + + files.forEach((file) => { + results.push({ + id: `file-${file.id}`, + title: file.name, + subtitle: file.path, + category: 'Files', + icon: , + action: () => { + onNavigate('code') + onFileSelect(file.id) + }, + tags: [file.language, file.path, 'code', 'file'], + }) + }) + + models.forEach((model) => { + results.push({ + id: `model-${model.id}`, + title: model.name, + subtitle: `${model.fields.length} fields`, + category: 'Models', + icon: , + action: () => onNavigate('models', model.id), + tags: ['prisma', 'database', 'schema', model.name.toLowerCase()], + }) + }) + + components.forEach((component) => { + results.push({ + id: `component-${component.id}`, + title: component.name, + subtitle: component.type, + category: 'Components', + icon: , + action: () => onNavigate('components', component.id), + tags: ['react', 'component', component.type.toLowerCase(), component.name.toLowerCase()], + }) + }) + + componentTrees.forEach((tree) => { + results.push({ + id: `tree-${tree.id}`, + title: tree.name, + subtitle: tree.description || `${tree.rootNodes.length} root nodes`, + category: 'Component Trees', + icon: , + action: () => onNavigate('component-trees', tree.id), + tags: ['hierarchy', 'structure', tree.name.toLowerCase()], + }) + }) + + workflows.forEach((workflow) => { + results.push({ + id: `workflow-${workflow.id}`, + title: workflow.name, + subtitle: workflow.description || `${workflow.nodes.length} nodes`, + category: 'Workflows', + icon: , + action: () => onNavigate('workflows', workflow.id), + tags: ['automation', 'flow', workflow.name.toLowerCase()], + }) + }) + + lambdas.forEach((lambda) => { + results.push({ + id: `lambda-${lambda.id}`, + title: lambda.name, + subtitle: lambda.description || lambda.runtime, + category: 'Lambdas', + icon: , + action: () => onNavigate('lambdas', lambda.id), + tags: ['serverless', 'function', lambda.runtime, lambda.name.toLowerCase()], + }) + }) + + playwrightTests.forEach((test) => { + results.push({ + id: `playwright-${test.id}`, + title: test.name, + subtitle: test.description, + category: 'Playwright Tests', + icon: , + action: () => onNavigate('playwright', test.id), + tags: ['testing', 'e2e', test.name.toLowerCase()], + }) + }) + + storybookStories.forEach((story) => { + results.push({ + id: `storybook-${story.id}`, + title: story.storyName, + subtitle: story.componentName, + category: 'Storybook Stories', + icon: , + action: () => onNavigate('storybook', story.id), + tags: ['documentation', 'story', story.componentName.toLowerCase()], + }) + }) + + unitTests.forEach((test) => { + results.push({ + id: `unit-test-${test.id}`, + title: test.name, + subtitle: test.description, + category: 'Unit Tests', + icon: , + action: () => onNavigate('unit-tests', test.id), + tags: ['testing', 'unit', test.name.toLowerCase()], + }) + }) + + return results + }, [ + files, + models, + components, + componentTrees, + workflows, + lambdas, + playwrightTests, + storybookStories, + unitTests, + onNavigate, + onFileSelect, + ]) + + const filteredResults = useMemo(() => { + if (!searchQuery.trim()) { + return allResults.slice(0, 50) + } + + const query = searchQuery.toLowerCase().trim() + const queryWords = query.split(/\s+/) + + return allResults + .map((result) => { + let score = 0 + const titleLower = result.title.toLowerCase() + const subtitleLower = result.subtitle?.toLowerCase() || '' + const categoryLower = result.category.toLowerCase() + const tagsLower = result.tags?.map(t => t.toLowerCase()) || [] + + if (titleLower === query) score += 100 + else if (titleLower.startsWith(query)) score += 50 + else if (titleLower.includes(query)) score += 30 + + if (subtitleLower.includes(query)) score += 20 + if (categoryLower.includes(query)) score += 15 + + tagsLower.forEach(tag => { + if (tag === query) score += 40 + else if (tag.includes(query)) score += 10 + }) + + queryWords.forEach(word => { + if (titleLower.includes(word)) score += 5 + if (subtitleLower.includes(word)) score += 3 + if (tagsLower.some(tag => tag.includes(word))) score += 2 + }) + + return { result, score } + }) + .filter(({ score }) => score > 0) + .sort((a, b) => b.score - a.score) + .slice(0, 50) + .map(({ result }) => result) + }, [allResults, searchQuery]) + + const groupedResults = useMemo(() => { + const groups: Record = {} + filteredResults.forEach((result) => { + if (!groups[result.category]) { + groups[result.category] = [] + } + groups[result.category].push(result) + }) + return groups + }, [filteredResults]) + + const handleSelect = (result: SearchResult) => { + result.action() + onOpenChange(false) + } + + return ( + + + + +
+ +

No results found

+
+
+ {Object.entries(groupedResults).map(([category, results], index) => ( +
+ {index > 0 && } + + {results.map((result) => ( + handleSelect(result)} + className="flex items-center gap-3 px-4 py-3 cursor-pointer" + > +
+ {result.icon} +
+
+
{result.title}
+ {result.subtitle && ( +
+ {result.subtitle} +
+ )} +
+ + {category} + +
+ ))} +
+
+ ))} +
+
+ ) +}