mirror of
https://github.com/johndoe6345789/low-code-react-app-b.git
synced 2026-04-24 21:54:56 +00:00
Generated by Spark: Refactor into atomic component library
This commit is contained in:
124
src/App.tsx
124
src/App.tsx
@@ -6,8 +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 { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'
|
||||
import { Code, Database, Tree, PaintBrush, Download, Sparkle, Flask, BookOpen, Play, Wrench, Gear, Cube, FileText, ChartBar, Keyboard, FlowArrow, Faders, DeviceMobile, Image, MagnifyingGlass, CloudArrowUp } from '@phosphor-icons/react'
|
||||
import { Download } 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'
|
||||
@@ -28,7 +27,6 @@ import { SassStylesShowcase } from '@/components/SassStylesShowcase'
|
||||
import { ProjectDashboard } from '@/components/ProjectDashboard'
|
||||
import { KeyboardShortcutsDialog } from '@/components/KeyboardShortcutsDialog'
|
||||
import { FeatureToggleSettings } from '@/components/FeatureToggleSettings'
|
||||
import { ProjectManager } from '@/components/ProjectManager'
|
||||
import { PWAInstallPrompt } from '@/components/PWAInstallPrompt'
|
||||
import { PWAUpdatePrompt } from '@/components/PWAUpdatePrompt'
|
||||
import { PWAStatusBar } from '@/components/PWAStatusBar'
|
||||
@@ -36,9 +34,7 @@ import { PWASettings } from '@/components/PWASettings'
|
||||
import { FaviconDesigner } from '@/components/FaviconDesigner'
|
||||
import { FeatureIdeaCloud } from '@/components/FeatureIdeaCloud'
|
||||
import { GlobalSearch } from '@/components/GlobalSearch'
|
||||
import { NavigationMenu } from '@/components/NavigationMenu'
|
||||
import { PageHeader } from '@/components/PageHeader'
|
||||
import { SaveIndicator } from '@/components/SaveIndicator'
|
||||
import { AppHeader, PageHeader } from '@/components/organisms'
|
||||
import { useKeyboardShortcuts } from '@/hooks/use-keyboard-shortcuts'
|
||||
import { generateNextJSProject, generatePrismaSchema, generateMUITheme, generatePlaywrightTests, generateStorybookStories, generateUnitTests, generateFlaskApp } from '@/lib/generators'
|
||||
import { AIService } from '@/lib/ai-service'
|
||||
@@ -552,108 +548,20 @@ Navigate to the backend directory and follow the setup instructions.
|
||||
<PWAStatusBar />
|
||||
<PWAUpdatePrompt />
|
||||
|
||||
<header className="border-b border-border bg-card px-4 sm:px-6 py-3 sm:py-4">
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<div className="flex items-center gap-2 sm:gap-3 flex-1 min-w-0">
|
||||
<NavigationMenu
|
||||
activeTab={activeTab}
|
||||
onTabChange={setActiveTab}
|
||||
featureToggles={safeFeatureToggles}
|
||||
errorCount={autoDetectedErrors.length}
|
||||
/>
|
||||
<div className="w-8 h-8 sm:w-10 sm:h-10 rounded-lg bg-gradient-to-br from-primary to-accent flex items-center justify-center shrink-0">
|
||||
<Code size={20} weight="duotone" className="text-white sm:w-6 sm:h-6" />
|
||||
</div>
|
||||
<div className="flex flex-col min-w-[100px]">
|
||||
<h1 className="text-base sm:text-xl font-bold whitespace-nowrap">CodeForge</h1>
|
||||
<p className="text-xs text-muted-foreground hidden sm:block whitespace-nowrap">
|
||||
Low-Code Next.js App Builder
|
||||
</p>
|
||||
</div>
|
||||
<SaveIndicator lastSaved={lastSaved} />
|
||||
</div>
|
||||
<div className="flex gap-1 sm:gap-2 shrink-0">
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
onClick={() => setSearchDialogOpen(true)}
|
||||
className="shrink-0"
|
||||
>
|
||||
<MagnifyingGlass size={18} />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>Search (Ctrl+K)</TooltipContent>
|
||||
</Tooltip>
|
||||
<ProjectManager
|
||||
currentProject={getCurrentProject()}
|
||||
onProjectLoad={handleLoadProject}
|
||||
/>
|
||||
{safeFeatureToggles.errorRepair && autoDetectedErrors.length > 0 && (
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
onClick={() => setActiveTab('errors')}
|
||||
className="border-destructive text-destructive hover:bg-destructive hover:text-destructive-foreground shrink-0 relative"
|
||||
>
|
||||
<Wrench size={18} />
|
||||
<Badge
|
||||
variant="destructive"
|
||||
className="absolute -top-1 -right-1 h-5 w-5 p-0 flex items-center justify-center text-[10px]"
|
||||
>
|
||||
{autoDetectedErrors.length}
|
||||
</Badge>
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
{autoDetectedErrors.length} {autoDetectedErrors.length === 1 ? 'Error' : 'Errors'}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
)}
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={() => setShortcutsDialogOpen(true)}
|
||||
className="hidden sm:flex shrink-0"
|
||||
>
|
||||
<Keyboard size={18} />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>Keyboard Shortcuts (Ctrl+/)</TooltipContent>
|
||||
</Tooltip>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
onClick={handleGenerateWithAI}
|
||||
className="shrink-0"
|
||||
>
|
||||
<Sparkle size={18} weight="duotone" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>AI Generate (Ctrl+Shift+G)</TooltipContent>
|
||||
</Tooltip>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
size="icon"
|
||||
onClick={handleExportProject}
|
||||
className="shrink-0"
|
||||
>
|
||||
<Download size={18} />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>Export Project (Ctrl+E)</TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
<AppHeader
|
||||
activeTab={activeTab}
|
||||
onTabChange={setActiveTab}
|
||||
featureToggles={safeFeatureToggles}
|
||||
errorCount={autoDetectedErrors.length}
|
||||
lastSaved={lastSaved}
|
||||
currentProject={getCurrentProject()}
|
||||
onProjectLoad={handleLoadProject}
|
||||
onSearch={() => setSearchDialogOpen(true)}
|
||||
onShowShortcuts={() => setShortcutsDialogOpen(true)}
|
||||
onGenerateAI={handleGenerateWithAI}
|
||||
onExport={handleExportProject}
|
||||
onShowErrors={() => setActiveTab('errors')}
|
||||
/>
|
||||
|
||||
<Tabs value={activeTab} onValueChange={setActiveTab} className="flex-1 flex flex-col">
|
||||
<PageHeader activeTab={activeTab} />
|
||||
|
||||
9
src/components/atoms/AppLogo.tsx
Normal file
9
src/components/atoms/AppLogo.tsx
Normal file
@@ -0,0 +1,9 @@
|
||||
import { Code } from '@phosphor-icons/react'
|
||||
|
||||
export function AppLogo() {
|
||||
return (
|
||||
<div className="w-8 h-8 sm:w-10 sm:h-10 rounded-lg bg-gradient-to-br from-primary to-accent flex items-center justify-center shrink-0">
|
||||
<Code size={20} weight="duotone" className="text-white sm:w-6 sm:h-6" />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
17
src/components/atoms/EmptyStateIcon.tsx
Normal file
17
src/components/atoms/EmptyStateIcon.tsx
Normal file
@@ -0,0 +1,17 @@
|
||||
interface EmptyStateIconProps {
|
||||
icon: React.ReactNode
|
||||
variant?: 'default' | 'muted'
|
||||
}
|
||||
|
||||
export function EmptyStateIcon({ icon, variant = 'muted' }: EmptyStateIconProps) {
|
||||
const variantClasses = {
|
||||
default: 'from-primary/20 to-accent/20 text-primary',
|
||||
muted: 'from-muted to-muted/50 text-muted-foreground',
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={`w-16 h-16 rounded-full bg-gradient-to-br ${variantClasses[variant]} flex items-center justify-center`}>
|
||||
{icon}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
25
src/components/atoms/ErrorBadge.tsx
Normal file
25
src/components/atoms/ErrorBadge.tsx
Normal file
@@ -0,0 +1,25 @@
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
|
||||
interface ErrorBadgeProps {
|
||||
count: number
|
||||
variant?: 'default' | 'destructive'
|
||||
size?: 'sm' | 'md'
|
||||
}
|
||||
|
||||
export function ErrorBadge({ count, variant = 'destructive', size = 'md' }: ErrorBadgeProps) {
|
||||
if (count === 0) return null
|
||||
|
||||
const sizeClasses = {
|
||||
sm: 'h-5 w-5 text-[10px]',
|
||||
md: 'h-6 w-6 text-xs',
|
||||
}
|
||||
|
||||
return (
|
||||
<Badge
|
||||
variant={variant}
|
||||
className={`${sizeClasses[size]} p-0 flex items-center justify-center absolute -top-1 -right-1`}
|
||||
>
|
||||
{count}
|
||||
</Badge>
|
||||
)
|
||||
}
|
||||
32
src/components/atoms/IconWrapper.tsx
Normal file
32
src/components/atoms/IconWrapper.tsx
Normal file
@@ -0,0 +1,32 @@
|
||||
interface IconWrapperProps {
|
||||
icon: React.ReactNode
|
||||
size?: 'sm' | 'md' | 'lg'
|
||||
variant?: 'default' | 'muted' | 'primary' | 'destructive'
|
||||
className?: string
|
||||
}
|
||||
|
||||
export function IconWrapper({
|
||||
icon,
|
||||
size = 'md',
|
||||
variant = 'default',
|
||||
className = ''
|
||||
}: IconWrapperProps) {
|
||||
const sizeClasses = {
|
||||
sm: 'w-4 h-4',
|
||||
md: 'w-5 h-5',
|
||||
lg: 'w-6 h-6',
|
||||
}
|
||||
|
||||
const variantClasses = {
|
||||
default: 'text-foreground',
|
||||
muted: 'text-muted-foreground',
|
||||
primary: 'text-primary',
|
||||
destructive: 'text-destructive',
|
||||
}
|
||||
|
||||
return (
|
||||
<span className={`inline-flex items-center justify-center ${sizeClasses[size]} ${variantClasses[variant]} ${className}`}>
|
||||
{icon}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
20
src/components/atoms/LoadingSpinner.tsx
Normal file
20
src/components/atoms/LoadingSpinner.tsx
Normal file
@@ -0,0 +1,20 @@
|
||||
interface LoadingSpinnerProps {
|
||||
size?: 'sm' | 'md' | 'lg'
|
||||
className?: string
|
||||
}
|
||||
|
||||
export function LoadingSpinner({ size = 'md', className = '' }: LoadingSpinnerProps) {
|
||||
const sizeClasses = {
|
||||
sm: 'w-4 h-4 border-2',
|
||||
md: 'w-6 h-6 border-2',
|
||||
lg: 'w-8 h-8 border-3',
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`inline-block ${sizeClasses[size]} border-primary border-t-transparent rounded-full animate-spin ${className}`}
|
||||
role="status"
|
||||
aria-label="Loading"
|
||||
/>
|
||||
)
|
||||
}
|
||||
21
src/components/atoms/StatusIcon.tsx
Normal file
21
src/components/atoms/StatusIcon.tsx
Normal file
@@ -0,0 +1,21 @@
|
||||
import { CheckCircle, CloudCheck } from '@phosphor-icons/react'
|
||||
|
||||
interface StatusIconProps {
|
||||
type: 'saved' | 'synced'
|
||||
size?: number
|
||||
animate?: boolean
|
||||
}
|
||||
|
||||
export function StatusIcon({ type, size = 14, animate = false }: StatusIconProps) {
|
||||
if (type === 'saved') {
|
||||
return (
|
||||
<CheckCircle
|
||||
size={size}
|
||||
weight="fill"
|
||||
className={`text-accent ${animate ? 'animate-in zoom-in duration-200' : ''}`}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
return <CloudCheck size={size} weight="duotone" />
|
||||
}
|
||||
16
src/components/atoms/TabIcon.tsx
Normal file
16
src/components/atoms/TabIcon.tsx
Normal file
@@ -0,0 +1,16 @@
|
||||
interface TabIconProps {
|
||||
icon: React.ReactNode
|
||||
variant?: 'default' | 'gradient'
|
||||
}
|
||||
|
||||
export function TabIcon({ icon, variant = 'default' }: TabIconProps) {
|
||||
if (variant === 'gradient') {
|
||||
return (
|
||||
<div className="w-10 h-10 rounded-lg bg-gradient-to-br from-primary/20 to-accent/20 flex items-center justify-center text-primary shrink-0">
|
||||
{icon}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return <>{icon}</>
|
||||
}
|
||||
7
src/components/atoms/index.ts
Normal file
7
src/components/atoms/index.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
export { AppLogo } from './AppLogo'
|
||||
export { TabIcon } from './TabIcon'
|
||||
export { StatusIcon } from './StatusIcon'
|
||||
export { ErrorBadge } from './ErrorBadge'
|
||||
export { IconWrapper } from './IconWrapper'
|
||||
export { LoadingSpinner } from './LoadingSpinner'
|
||||
export { EmptyStateIcon } from './EmptyStateIcon'
|
||||
3
src/components/index.ts
Normal file
3
src/components/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export * from './atoms'
|
||||
export * from './molecules'
|
||||
export * from './organisms'
|
||||
23
src/components/molecules/AppBranding.tsx
Normal file
23
src/components/molecules/AppBranding.tsx
Normal file
@@ -0,0 +1,23 @@
|
||||
import { AppLogo } from '@/components/atoms'
|
||||
|
||||
interface AppBrandingProps {
|
||||
title?: string
|
||||
subtitle?: string
|
||||
}
|
||||
|
||||
export function AppBranding({
|
||||
title = 'CodeForge',
|
||||
subtitle = 'Low-Code Next.js App Builder'
|
||||
}: AppBrandingProps) {
|
||||
return (
|
||||
<div className="flex items-center gap-2 sm:gap-3 flex-1 min-w-0">
|
||||
<AppLogo />
|
||||
<div className="flex flex-col min-w-[100px]">
|
||||
<h1 className="text-base sm:text-xl font-bold whitespace-nowrap">{title}</h1>
|
||||
<p className="text-xs text-muted-foreground hidden sm:block whitespace-nowrap">
|
||||
{subtitle}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
23
src/components/molecules/EmptyState.tsx
Normal file
23
src/components/molecules/EmptyState.tsx
Normal file
@@ -0,0 +1,23 @@
|
||||
import { EmptyStateIcon } from '@/components/atoms'
|
||||
|
||||
interface EmptyStateProps {
|
||||
icon: React.ReactNode
|
||||
title: string
|
||||
description?: string
|
||||
action?: React.ReactNode
|
||||
}
|
||||
|
||||
export function EmptyState({ icon, title, description, action }: EmptyStateProps) {
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center gap-4 py-12 px-4 text-center">
|
||||
<EmptyStateIcon icon={icon} />
|
||||
<div className="space-y-2">
|
||||
<h3 className="text-lg font-semibold">{title}</h3>
|
||||
{description && (
|
||||
<p className="text-sm text-muted-foreground max-w-md">{description}</p>
|
||||
)}
|
||||
</div>
|
||||
{action && <div className="mt-2">{action}</div>}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
24
src/components/molecules/LabelWithBadge.tsx
Normal file
24
src/components/molecules/LabelWithBadge.tsx
Normal file
@@ -0,0 +1,24 @@
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
|
||||
interface LabelWithBadgeProps {
|
||||
label: string
|
||||
badge?: number | string
|
||||
badgeVariant?: 'default' | 'secondary' | 'destructive' | 'outline'
|
||||
}
|
||||
|
||||
export function LabelWithBadge({
|
||||
label,
|
||||
badge,
|
||||
badgeVariant = 'secondary'
|
||||
}: LabelWithBadgeProps) {
|
||||
return (
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-sm font-medium">{label}</span>
|
||||
{badge !== undefined && (
|
||||
<Badge variant={badgeVariant} className="text-xs">
|
||||
{badge}
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
15
src/components/molecules/LoadingState.tsx
Normal file
15
src/components/molecules/LoadingState.tsx
Normal file
@@ -0,0 +1,15 @@
|
||||
import { LoadingSpinner } from '@/components/atoms'
|
||||
|
||||
interface LoadingStateProps {
|
||||
message?: string
|
||||
size?: 'sm' | 'md' | 'lg'
|
||||
}
|
||||
|
||||
export function LoadingState({ message = 'Loading...', size = 'md' }: LoadingStateProps) {
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center gap-3 py-12">
|
||||
<LoadingSpinner size={size} />
|
||||
<p className="text-sm text-muted-foreground">{message}</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
30
src/components/molecules/NavigationGroupHeader.tsx
Normal file
30
src/components/molecules/NavigationGroupHeader.tsx
Normal file
@@ -0,0 +1,30 @@
|
||||
import { CaretDown } from '@phosphor-icons/react'
|
||||
import { CollapsibleTrigger } from '@/components/ui/collapsible'
|
||||
|
||||
interface NavigationGroupHeaderProps {
|
||||
label: string
|
||||
count: number
|
||||
isExpanded: boolean
|
||||
}
|
||||
|
||||
export function NavigationGroupHeader({
|
||||
label,
|
||||
count,
|
||||
isExpanded,
|
||||
}: NavigationGroupHeaderProps) {
|
||||
return (
|
||||
<CollapsibleTrigger className="w-full flex items-center gap-2 px-2 py-2 rounded-lg hover:bg-muted transition-colors group">
|
||||
<CaretDown
|
||||
size={16}
|
||||
weight="bold"
|
||||
className={`text-muted-foreground transition-transform ${
|
||||
isExpanded ? 'rotate-0' : '-rotate-90'
|
||||
}`}
|
||||
/>
|
||||
<h3 className="flex-1 text-left text-xs font-semibold text-muted-foreground uppercase tracking-wider">
|
||||
{label}
|
||||
</h3>
|
||||
<span className="text-xs text-muted-foreground">{count}</span>
|
||||
</CollapsibleTrigger>
|
||||
)
|
||||
}
|
||||
43
src/components/molecules/NavigationItem.tsx
Normal file
43
src/components/molecules/NavigationItem.tsx
Normal file
@@ -0,0 +1,43 @@
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
|
||||
interface NavigationItemProps {
|
||||
icon: React.ReactNode
|
||||
label: string
|
||||
isActive: boolean
|
||||
badge?: number
|
||||
onClick: () => void
|
||||
}
|
||||
|
||||
export function NavigationItem({
|
||||
icon,
|
||||
label,
|
||||
isActive,
|
||||
badge,
|
||||
onClick,
|
||||
}: NavigationItemProps) {
|
||||
return (
|
||||
<button
|
||||
onClick={onClick}
|
||||
className={`w-full flex items-center gap-3 px-3 py-2 rounded-lg transition-colors ${
|
||||
isActive
|
||||
? 'bg-primary text-primary-foreground'
|
||||
: 'hover:bg-muted text-foreground'
|
||||
}`}
|
||||
>
|
||||
<span className={isActive ? 'text-primary-foreground' : 'text-muted-foreground'}>
|
||||
{icon}
|
||||
</span>
|
||||
<span className="flex-1 text-left text-sm font-medium">
|
||||
{label}
|
||||
</span>
|
||||
{badge !== undefined && badge > 0 && (
|
||||
<Badge
|
||||
variant={isActive ? 'secondary' : 'destructive'}
|
||||
className="ml-auto"
|
||||
>
|
||||
{badge}
|
||||
</Badge>
|
||||
)}
|
||||
</button>
|
||||
)
|
||||
}
|
||||
23
src/components/molecules/PageHeaderContent.tsx
Normal file
23
src/components/molecules/PageHeaderContent.tsx
Normal file
@@ -0,0 +1,23 @@
|
||||
import { TabIcon } from '@/components/atoms'
|
||||
|
||||
interface PageHeaderContentProps {
|
||||
title: string
|
||||
icon: React.ReactNode
|
||||
description?: string
|
||||
}
|
||||
|
||||
export function PageHeaderContent({ title, icon, description }: PageHeaderContentProps) {
|
||||
return (
|
||||
<div className="flex items-center gap-3">
|
||||
<TabIcon icon={icon} variant="gradient" />
|
||||
<div className="min-w-0">
|
||||
<h2 className="text-lg sm:text-xl font-bold truncate">{title}</h2>
|
||||
{description && (
|
||||
<p className="text-xs sm:text-sm text-muted-foreground hidden sm:block">
|
||||
{description}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
36
src/components/molecules/SaveIndicator.tsx
Normal file
36
src/components/molecules/SaveIndicator.tsx
Normal file
@@ -0,0 +1,36 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import { formatDistanceToNow } from 'date-fns'
|
||||
import { StatusIcon } from '@/components/atoms'
|
||||
|
||||
interface SaveIndicatorProps {
|
||||
lastSaved: number | null
|
||||
}
|
||||
|
||||
export function SaveIndicator({ lastSaved }: SaveIndicatorProps) {
|
||||
const [timeAgo, setTimeAgo] = useState<string>('')
|
||||
|
||||
useEffect(() => {
|
||||
if (!lastSaved) return
|
||||
|
||||
const updateTimeAgo = () => {
|
||||
const distance = formatDistanceToNow(lastSaved, { addSuffix: true })
|
||||
setTimeAgo(distance)
|
||||
}
|
||||
|
||||
updateTimeAgo()
|
||||
const interval = setInterval(updateTimeAgo, 10000)
|
||||
|
||||
return () => clearInterval(interval)
|
||||
}, [lastSaved])
|
||||
|
||||
if (!lastSaved) return null
|
||||
|
||||
const isRecent = Date.now() - lastSaved < 3000
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-1.5 text-xs text-muted-foreground">
|
||||
<StatusIcon type={isRecent ? 'saved' : 'synced'} animate={isRecent} />
|
||||
<span className="hidden sm:inline">{isRecent ? 'Saved' : timeAgo}</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
33
src/components/molecules/StatCard.tsx
Normal file
33
src/components/molecules/StatCard.tsx
Normal file
@@ -0,0 +1,33 @@
|
||||
import { Card } from '@/components/ui/card'
|
||||
import { IconWrapper } from '@/components/atoms'
|
||||
|
||||
interface StatCardProps {
|
||||
icon: React.ReactNode
|
||||
label: string
|
||||
value: string | number
|
||||
variant?: 'default' | 'primary' | 'destructive'
|
||||
}
|
||||
|
||||
export function StatCard({ icon, label, value, variant = 'default' }: StatCardProps) {
|
||||
const variantClasses = {
|
||||
default: 'border-border',
|
||||
primary: 'border-primary/50 bg-primary/5',
|
||||
destructive: 'border-destructive/50 bg-destructive/5',
|
||||
}
|
||||
|
||||
return (
|
||||
<Card className={`p-4 ${variantClasses[variant]}`}>
|
||||
<div className="flex items-center gap-3">
|
||||
<IconWrapper
|
||||
icon={icon}
|
||||
size="lg"
|
||||
variant={variant === 'default' ? 'muted' : variant}
|
||||
/>
|
||||
<div className="flex-1">
|
||||
<p className="text-xs text-muted-foreground">{label}</p>
|
||||
<p className="text-2xl font-bold">{value}</p>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
37
src/components/molecules/ToolbarButton.tsx
Normal file
37
src/components/molecules/ToolbarButton.tsx
Normal file
@@ -0,0 +1,37 @@
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'
|
||||
|
||||
interface ToolbarButtonProps {
|
||||
icon: React.ReactNode
|
||||
label: string
|
||||
onClick: () => void
|
||||
variant?: 'default' | 'outline' | 'ghost' | 'destructive'
|
||||
disabled?: boolean
|
||||
className?: string
|
||||
}
|
||||
|
||||
export function ToolbarButton({
|
||||
icon,
|
||||
label,
|
||||
onClick,
|
||||
variant = 'outline',
|
||||
disabled = false,
|
||||
className = '',
|
||||
}: ToolbarButtonProps) {
|
||||
return (
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant={variant}
|
||||
size="icon"
|
||||
onClick={onClick}
|
||||
disabled={disabled}
|
||||
className={`shrink-0 ${className}`}
|
||||
>
|
||||
{icon}
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>{label}</TooltipContent>
|
||||
</Tooltip>
|
||||
)
|
||||
}
|
||||
10
src/components/molecules/index.ts
Normal file
10
src/components/molecules/index.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
export { SaveIndicator } from './SaveIndicator'
|
||||
export { AppBranding } from './AppBranding'
|
||||
export { PageHeaderContent } from './PageHeaderContent'
|
||||
export { ToolbarButton } from './ToolbarButton'
|
||||
export { NavigationItem } from './NavigationItem'
|
||||
export { NavigationGroupHeader } from './NavigationGroupHeader'
|
||||
export { EmptyState } from './EmptyState'
|
||||
export { LoadingState } from './LoadingState'
|
||||
export { StatCard } from './StatCard'
|
||||
export { LabelWithBadge } from './LabelWithBadge'
|
||||
67
src/components/organisms/AppHeader.tsx
Normal file
67
src/components/organisms/AppHeader.tsx
Normal file
@@ -0,0 +1,67 @@
|
||||
import { AppBranding, SaveIndicator } from '@/components/molecules'
|
||||
import { NavigationMenu } from '@/components/organisms/NavigationMenu'
|
||||
import { ToolbarActions } from '@/components/organisms/ToolbarActions'
|
||||
import { ProjectManager } from '@/components/ProjectManager'
|
||||
import { FeatureToggles, Project } from '@/types/project'
|
||||
|
||||
interface AppHeaderProps {
|
||||
activeTab: string
|
||||
onTabChange: (tab: string) => void
|
||||
featureToggles: FeatureToggles
|
||||
errorCount: number
|
||||
lastSaved: number | null
|
||||
currentProject: Project
|
||||
onProjectLoad: (project: Project) => void
|
||||
onSearch: () => void
|
||||
onShowShortcuts: () => void
|
||||
onGenerateAI: () => void
|
||||
onExport: () => void
|
||||
onShowErrors: () => void
|
||||
}
|
||||
|
||||
export function AppHeader({
|
||||
activeTab,
|
||||
onTabChange,
|
||||
featureToggles,
|
||||
errorCount,
|
||||
lastSaved,
|
||||
currentProject,
|
||||
onProjectLoad,
|
||||
onSearch,
|
||||
onShowShortcuts,
|
||||
onGenerateAI,
|
||||
onExport,
|
||||
onShowErrors,
|
||||
}: AppHeaderProps) {
|
||||
return (
|
||||
<header className="border-b border-border bg-card px-4 sm:px-6 py-3 sm:py-4">
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<div className="flex items-center gap-2 sm:gap-3 flex-1 min-w-0">
|
||||
<NavigationMenu
|
||||
activeTab={activeTab}
|
||||
onTabChange={onTabChange}
|
||||
featureToggles={featureToggles}
|
||||
errorCount={errorCount}
|
||||
/>
|
||||
<AppBranding />
|
||||
<SaveIndicator lastSaved={lastSaved} />
|
||||
</div>
|
||||
<div className="flex gap-1 sm:gap-2 shrink-0">
|
||||
<ProjectManager
|
||||
currentProject={currentProject}
|
||||
onProjectLoad={onProjectLoad}
|
||||
/>
|
||||
<ToolbarActions
|
||||
onSearch={onSearch}
|
||||
onShowShortcuts={onShowShortcuts}
|
||||
onGenerateAI={onGenerateAI}
|
||||
onExport={onExport}
|
||||
onShowErrors={onShowErrors}
|
||||
errorCount={errorCount}
|
||||
showErrorButton={featureToggles.errorRepair && errorCount > 0}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
)
|
||||
}
|
||||
149
src/components/organisms/NavigationMenu.tsx
Normal file
149
src/components/organisms/NavigationMenu.tsx
Normal file
@@ -0,0 +1,149 @@
|
||||
import { useState } from 'react'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Sheet, SheetContent, SheetHeader, SheetTitle, SheetTrigger } from '@/components/ui/sheet'
|
||||
import { ScrollArea } from '@/components/ui/scroll-area'
|
||||
import { Collapsible, CollapsibleContent } from '@/components/ui/collapsible'
|
||||
import { List, CaretDoubleDown, CaretDoubleUp } from '@phosphor-icons/react'
|
||||
import { NavigationItem, NavigationGroupHeader } from '@/components/molecules'
|
||||
import { navigationGroups, NavigationItemData } from '@/lib/navigation-config'
|
||||
import { FeatureToggles } from '@/types/project'
|
||||
|
||||
interface NavigationMenuProps {
|
||||
activeTab: string
|
||||
onTabChange: (tab: string) => void
|
||||
featureToggles: FeatureToggles
|
||||
errorCount?: number
|
||||
}
|
||||
|
||||
export function NavigationMenu({
|
||||
activeTab,
|
||||
onTabChange,
|
||||
featureToggles,
|
||||
errorCount = 0,
|
||||
}: NavigationMenuProps) {
|
||||
const [open, setOpen] = useState(false)
|
||||
const [expandedGroups, setExpandedGroups] = useState<Set<string>>(
|
||||
new Set(['overview', 'development', 'automation', 'design', 'backend', 'testing', 'tools'])
|
||||
)
|
||||
|
||||
const handleItemClick = (value: string) => {
|
||||
onTabChange(value)
|
||||
setOpen(false)
|
||||
}
|
||||
|
||||
const toggleGroup = (groupId: string) => {
|
||||
setExpandedGroups((prev) => {
|
||||
const newSet = new Set(prev)
|
||||
if (newSet.has(groupId)) {
|
||||
newSet.delete(groupId)
|
||||
} else {
|
||||
newSet.add(groupId)
|
||||
}
|
||||
return newSet
|
||||
})
|
||||
}
|
||||
|
||||
const isItemVisible = (item: NavigationItemData) => {
|
||||
if (!item.featureKey) return true
|
||||
return featureToggles[item.featureKey]
|
||||
}
|
||||
|
||||
const getVisibleItemsCount = (groupId: string) => {
|
||||
const group = navigationGroups.find((g) => g.id === groupId)
|
||||
if (!group) return 0
|
||||
return group.items.filter(isItemVisible).length
|
||||
}
|
||||
|
||||
const handleExpandAll = () => {
|
||||
const allGroupIds = navigationGroups
|
||||
.filter((group) => getVisibleItemsCount(group.id) > 0)
|
||||
.map((group) => group.id)
|
||||
setExpandedGroups(new Set(allGroupIds))
|
||||
}
|
||||
|
||||
const handleCollapseAll = () => {
|
||||
setExpandedGroups(new Set())
|
||||
}
|
||||
|
||||
const getItemBadge = (item: NavigationItemData) => {
|
||||
if (item.id === 'errors') return errorCount
|
||||
return item.badge
|
||||
}
|
||||
|
||||
return (
|
||||
<Sheet open={open} onOpenChange={setOpen}>
|
||||
<SheetTrigger asChild>
|
||||
<Button variant="outline" size="icon" className="shrink-0">
|
||||
<List size={20} weight="bold" />
|
||||
</Button>
|
||||
</SheetTrigger>
|
||||
<SheetContent side="left" className="w-80">
|
||||
<SheetHeader>
|
||||
<SheetTitle>Navigation</SheetTitle>
|
||||
</SheetHeader>
|
||||
<div className="flex gap-2 mt-4">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={handleExpandAll}
|
||||
className="flex-1"
|
||||
>
|
||||
<CaretDoubleDown size={16} className="mr-2" />
|
||||
Expand All
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={handleCollapseAll}
|
||||
className="flex-1"
|
||||
>
|
||||
<CaretDoubleUp size={16} className="mr-2" />
|
||||
Collapse All
|
||||
</Button>
|
||||
</div>
|
||||
<ScrollArea className="h-[calc(100vh-12rem)] mt-4">
|
||||
<div className="space-y-2">
|
||||
{navigationGroups.map((group) => {
|
||||
const visibleItemsCount = getVisibleItemsCount(group.id)
|
||||
if (visibleItemsCount === 0) return null
|
||||
|
||||
const isExpanded = expandedGroups.has(group.id)
|
||||
|
||||
return (
|
||||
<Collapsible
|
||||
key={group.id}
|
||||
open={isExpanded}
|
||||
onOpenChange={() => toggleGroup(group.id)}
|
||||
>
|
||||
<NavigationGroupHeader
|
||||
label={group.label}
|
||||
count={visibleItemsCount}
|
||||
isExpanded={isExpanded}
|
||||
/>
|
||||
<CollapsibleContent className="mt-1">
|
||||
<div className="space-y-1 pl-2">
|
||||
{group.items.map((item) => {
|
||||
if (!isItemVisible(item)) return null
|
||||
|
||||
return (
|
||||
<NavigationItem
|
||||
key={item.id}
|
||||
icon={item.icon}
|
||||
label={item.label}
|
||||
isActive={activeTab === item.value}
|
||||
badge={getItemBadge(item)}
|
||||
onClick={() => handleItemClick(item.value)}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</CollapsibleContent>
|
||||
</Collapsible>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</ScrollArea>
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
)
|
||||
}
|
||||
22
src/components/organisms/PageHeader.tsx
Normal file
22
src/components/organisms/PageHeader.tsx
Normal file
@@ -0,0 +1,22 @@
|
||||
import { PageHeaderContent } from '@/components/molecules'
|
||||
import { tabInfo } from '@/lib/navigation-config'
|
||||
|
||||
interface PageHeaderProps {
|
||||
activeTab: string
|
||||
}
|
||||
|
||||
export function PageHeader({ activeTab }: PageHeaderProps) {
|
||||
const info = tabInfo[activeTab]
|
||||
|
||||
if (!info) return null
|
||||
|
||||
return (
|
||||
<div className="border-b border-border bg-card px-4 sm:px-6 py-3 sm:py-4">
|
||||
<PageHeaderContent
|
||||
title={info.title}
|
||||
icon={info.icon}
|
||||
description={info.description}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
73
src/components/organisms/ToolbarActions.tsx
Normal file
73
src/components/organisms/ToolbarActions.tsx
Normal file
@@ -0,0 +1,73 @@
|
||||
import { ToolbarButton } from '@/components/molecules'
|
||||
import { ErrorBadge } from '@/components/atoms'
|
||||
import {
|
||||
MagnifyingGlass,
|
||||
Keyboard,
|
||||
Sparkle,
|
||||
Download,
|
||||
Wrench,
|
||||
} from '@phosphor-icons/react'
|
||||
|
||||
interface ToolbarActionsProps {
|
||||
onSearch: () => void
|
||||
onShowShortcuts: () => void
|
||||
onGenerateAI: () => void
|
||||
onExport: () => void
|
||||
onShowErrors?: () => void
|
||||
errorCount?: number
|
||||
showErrorButton?: boolean
|
||||
}
|
||||
|
||||
export function ToolbarActions({
|
||||
onSearch,
|
||||
onShowShortcuts,
|
||||
onGenerateAI,
|
||||
onExport,
|
||||
onShowErrors,
|
||||
errorCount = 0,
|
||||
showErrorButton = false,
|
||||
}: ToolbarActionsProps) {
|
||||
return (
|
||||
<div className="flex gap-1 sm:gap-2 shrink-0">
|
||||
<ToolbarButton
|
||||
icon={<MagnifyingGlass size={18} />}
|
||||
label="Search (Ctrl+K)"
|
||||
onClick={onSearch}
|
||||
/>
|
||||
|
||||
{showErrorButton && errorCount > 0 && onShowErrors && (
|
||||
<div className="relative">
|
||||
<ToolbarButton
|
||||
icon={<Wrench size={18} />}
|
||||
label={`${errorCount} ${errorCount === 1 ? 'Error' : 'Errors'}`}
|
||||
onClick={onShowErrors}
|
||||
variant="outline"
|
||||
className="border-destructive text-destructive hover:bg-destructive hover:text-destructive-foreground"
|
||||
/>
|
||||
<ErrorBadge count={errorCount} size="sm" />
|
||||
</div>
|
||||
)}
|
||||
|
||||
<ToolbarButton
|
||||
icon={<Keyboard size={18} />}
|
||||
label="Keyboard Shortcuts (Ctrl+/)"
|
||||
onClick={onShowShortcuts}
|
||||
variant="ghost"
|
||||
className="hidden sm:flex"
|
||||
/>
|
||||
|
||||
<ToolbarButton
|
||||
icon={<Sparkle size={18} weight="duotone" />}
|
||||
label="AI Generate (Ctrl+Shift+G)"
|
||||
onClick={onGenerateAI}
|
||||
/>
|
||||
|
||||
<ToolbarButton
|
||||
icon={<Download size={18} />}
|
||||
label="Export Project (Ctrl+E)"
|
||||
onClick={onExport}
|
||||
variant="default"
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
4
src/components/organisms/index.ts
Normal file
4
src/components/organisms/index.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export { NavigationMenu } from './NavigationMenu'
|
||||
export { PageHeader } from './PageHeader'
|
||||
export { ToolbarActions } from './ToolbarActions'
|
||||
export { AppHeader } from './AppHeader'
|
||||
325
src/lib/navigation-config.tsx
Normal file
325
src/lib/navigation-config.tsx
Normal file
@@ -0,0 +1,325 @@
|
||||
import {
|
||||
ChartBar,
|
||||
Code,
|
||||
Database,
|
||||
Tree,
|
||||
FlowArrow,
|
||||
PaintBrush,
|
||||
Flask,
|
||||
Play,
|
||||
BookOpen,
|
||||
Cube,
|
||||
Wrench,
|
||||
FileText,
|
||||
Gear,
|
||||
DeviceMobile,
|
||||
Image,
|
||||
Faders,
|
||||
Lightbulb,
|
||||
} from '@phosphor-icons/react'
|
||||
import { FeatureToggles } from '@/types/project'
|
||||
|
||||
export interface NavigationItemData {
|
||||
id: string
|
||||
label: string
|
||||
icon: React.ReactNode
|
||||
value: string
|
||||
badge?: number
|
||||
featureKey?: keyof FeatureToggles
|
||||
}
|
||||
|
||||
export interface NavigationGroup {
|
||||
id: string
|
||||
label: string
|
||||
items: NavigationItemData[]
|
||||
}
|
||||
|
||||
export interface TabInfo {
|
||||
title: string
|
||||
icon: React.ReactNode
|
||||
description?: string
|
||||
}
|
||||
|
||||
export const tabInfo: Record<string, TabInfo> = {
|
||||
dashboard: {
|
||||
title: 'Dashboard',
|
||||
icon: <ChartBar size={24} weight="duotone" />,
|
||||
description: 'Project overview and statistics',
|
||||
},
|
||||
code: {
|
||||
title: 'Code Editor',
|
||||
icon: <Code size={24} weight="duotone" />,
|
||||
description: 'Edit project files',
|
||||
},
|
||||
models: {
|
||||
title: 'Models',
|
||||
icon: <Database size={24} weight="duotone" />,
|
||||
description: 'Define Prisma data models',
|
||||
},
|
||||
components: {
|
||||
title: 'Components',
|
||||
icon: <Tree size={24} weight="duotone" />,
|
||||
description: 'Create React components',
|
||||
},
|
||||
'component-trees': {
|
||||
title: 'Component Trees',
|
||||
icon: <Tree size={24} weight="duotone" />,
|
||||
description: 'Manage component hierarchies',
|
||||
},
|
||||
workflows: {
|
||||
title: 'Workflows',
|
||||
icon: <FlowArrow size={24} weight="duotone" />,
|
||||
description: 'Design automation workflows',
|
||||
},
|
||||
lambdas: {
|
||||
title: 'Lambdas',
|
||||
icon: <Code size={24} weight="duotone" />,
|
||||
description: 'Serverless functions',
|
||||
},
|
||||
styling: {
|
||||
title: 'Styling',
|
||||
icon: <PaintBrush size={24} weight="duotone" />,
|
||||
description: 'Theme and design tokens',
|
||||
},
|
||||
sass: {
|
||||
title: 'Sass Styles',
|
||||
icon: <PaintBrush size={24} weight="duotone" />,
|
||||
description: 'Custom Sass stylesheets',
|
||||
},
|
||||
favicon: {
|
||||
title: 'Favicon Designer',
|
||||
icon: <Image size={24} weight="duotone" />,
|
||||
description: 'Design app icons',
|
||||
},
|
||||
flask: {
|
||||
title: 'Flask API',
|
||||
icon: <Flask size={24} weight="duotone" />,
|
||||
description: 'Backend API configuration',
|
||||
},
|
||||
playwright: {
|
||||
title: 'Playwright',
|
||||
icon: <Play size={24} weight="duotone" />,
|
||||
description: 'E2E test scenarios',
|
||||
},
|
||||
storybook: {
|
||||
title: 'Storybook',
|
||||
icon: <BookOpen size={24} weight="duotone" />,
|
||||
description: 'Component documentation',
|
||||
},
|
||||
'unit-tests': {
|
||||
title: 'Unit Tests',
|
||||
icon: <Cube size={24} weight="duotone" />,
|
||||
description: 'Unit test suites',
|
||||
},
|
||||
errors: {
|
||||
title: 'Error Repair',
|
||||
icon: <Wrench size={24} weight="duotone" />,
|
||||
description: 'Automated error detection and fixing',
|
||||
},
|
||||
docs: {
|
||||
title: 'Documentation',
|
||||
icon: <FileText size={24} weight="duotone" />,
|
||||
description: 'Project guides and references',
|
||||
},
|
||||
settings: {
|
||||
title: 'Settings',
|
||||
icon: <Gear size={24} weight="duotone" />,
|
||||
description: 'Project configuration',
|
||||
},
|
||||
pwa: {
|
||||
title: 'PWA',
|
||||
icon: <DeviceMobile size={24} weight="duotone" />,
|
||||
description: 'Progressive Web App settings',
|
||||
},
|
||||
features: {
|
||||
title: 'Features',
|
||||
icon: <Faders size={24} weight="duotone" />,
|
||||
description: 'Toggle feature modules',
|
||||
},
|
||||
ideas: {
|
||||
title: 'Feature Ideas',
|
||||
icon: <Lightbulb size={24} weight="duotone" />,
|
||||
description: 'Brainstorm and organize feature ideas',
|
||||
},
|
||||
}
|
||||
|
||||
export const navigationGroups: NavigationGroup[] = [
|
||||
{
|
||||
id: 'overview',
|
||||
label: 'Overview',
|
||||
items: [
|
||||
{
|
||||
id: 'dashboard',
|
||||
label: 'Dashboard',
|
||||
icon: <ChartBar size={18} />,
|
||||
value: 'dashboard',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'development',
|
||||
label: 'Development',
|
||||
items: [
|
||||
{
|
||||
id: 'code',
|
||||
label: 'Code Editor',
|
||||
icon: <Code size={18} />,
|
||||
value: 'code',
|
||||
featureKey: 'codeEditor',
|
||||
},
|
||||
{
|
||||
id: 'models',
|
||||
label: 'Models',
|
||||
icon: <Database size={18} />,
|
||||
value: 'models',
|
||||
featureKey: 'models',
|
||||
},
|
||||
{
|
||||
id: 'components',
|
||||
label: 'Components',
|
||||
icon: <Tree size={18} />,
|
||||
value: 'components',
|
||||
featureKey: 'components',
|
||||
},
|
||||
{
|
||||
id: 'component-trees',
|
||||
label: 'Component Trees',
|
||||
icon: <Tree size={18} />,
|
||||
value: 'component-trees',
|
||||
featureKey: 'componentTrees',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'automation',
|
||||
label: 'Automation',
|
||||
items: [
|
||||
{
|
||||
id: 'workflows',
|
||||
label: 'Workflows',
|
||||
icon: <FlowArrow size={18} />,
|
||||
value: 'workflows',
|
||||
featureKey: 'workflows',
|
||||
},
|
||||
{
|
||||
id: 'lambdas',
|
||||
label: 'Lambdas',
|
||||
icon: <Code size={18} />,
|
||||
value: 'lambdas',
|
||||
featureKey: 'lambdas',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'design',
|
||||
label: 'Design & Styling',
|
||||
items: [
|
||||
{
|
||||
id: 'styling',
|
||||
label: 'Styling',
|
||||
icon: <PaintBrush size={18} />,
|
||||
value: 'styling',
|
||||
featureKey: 'styling',
|
||||
},
|
||||
{
|
||||
id: 'sass',
|
||||
label: 'Sass Styles',
|
||||
icon: <PaintBrush size={18} />,
|
||||
value: 'sass',
|
||||
featureKey: 'sassStyles',
|
||||
},
|
||||
{
|
||||
id: 'favicon',
|
||||
label: 'Favicon Designer',
|
||||
icon: <Image size={18} />,
|
||||
value: 'favicon',
|
||||
featureKey: 'faviconDesigner',
|
||||
},
|
||||
{
|
||||
id: 'ideas',
|
||||
label: 'Feature Ideas',
|
||||
icon: <Lightbulb size={18} />,
|
||||
value: 'ideas',
|
||||
featureKey: 'ideaCloud',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'backend',
|
||||
label: 'Backend',
|
||||
items: [
|
||||
{
|
||||
id: 'flask',
|
||||
label: 'Flask API',
|
||||
icon: <Flask size={18} />,
|
||||
value: 'flask',
|
||||
featureKey: 'flaskApi',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'testing',
|
||||
label: 'Testing',
|
||||
items: [
|
||||
{
|
||||
id: 'playwright',
|
||||
label: 'Playwright',
|
||||
icon: <Play size={18} />,
|
||||
value: 'playwright',
|
||||
featureKey: 'playwright',
|
||||
},
|
||||
{
|
||||
id: 'storybook',
|
||||
label: 'Storybook',
|
||||
icon: <BookOpen size={18} />,
|
||||
value: 'storybook',
|
||||
featureKey: 'storybook',
|
||||
},
|
||||
{
|
||||
id: 'unit-tests',
|
||||
label: 'Unit Tests',
|
||||
icon: <Cube size={18} />,
|
||||
value: 'unit-tests',
|
||||
featureKey: 'unitTests',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'tools',
|
||||
label: 'Tools & Configuration',
|
||||
items: [
|
||||
{
|
||||
id: 'errors',
|
||||
label: 'Error Repair',
|
||||
icon: <Wrench size={18} />,
|
||||
value: 'errors',
|
||||
featureKey: 'errorRepair',
|
||||
},
|
||||
{
|
||||
id: 'docs',
|
||||
label: 'Documentation',
|
||||
icon: <FileText size={18} />,
|
||||
value: 'docs',
|
||||
featureKey: 'documentation',
|
||||
},
|
||||
{
|
||||
id: 'settings',
|
||||
label: 'Settings',
|
||||
icon: <Gear size={18} />,
|
||||
value: 'settings',
|
||||
},
|
||||
{
|
||||
id: 'pwa',
|
||||
label: 'PWA',
|
||||
icon: <DeviceMobile size={18} />,
|
||||
value: 'pwa',
|
||||
},
|
||||
{
|
||||
id: 'features',
|
||||
label: 'Features',
|
||||
icon: <Faders size={18} />,
|
||||
value: 'features',
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
Reference in New Issue
Block a user