Generated by Spark: Add split screen code example with live preview

This commit is contained in:
2026-01-17 16:55:51 +00:00
committed by GitHub
parent a4ab731735
commit 3ddfea6a81
3 changed files with 153 additions and 13 deletions

30
PRD.md
View File

@@ -26,11 +26,18 @@ A code snippet management application with an integrated component library showc
- Success criteria: Snippet appears in grid immediately with proper syntax highlighting
**Snippet Viewing**
- Functionality: Full-screen viewer with syntax highlighting and copy functionality
- Purpose: Easy reading and copying of saved snippets
- Functionality: Full-screen viewer with syntax highlighting, split-screen live preview for React code, and copy functionality
- Purpose: Easy reading, testing, and copying of saved snippets
- Trigger: Click on any snippet card
- Progression: User clicks snippet → Full viewer opens → User reads code → Copies if needed → Closes viewer
- Success criteria: Code displays with proper formatting, copy works reliably
- Progression: User clicks snippet → Full viewer opens → User reads code → Toggles preview if available → Copies if needed → Closes viewer
- Success criteria: Code displays with proper formatting, live preview renders React components accurately, copy works reliably
**Split-Screen Code Editor with Live Preview**
- Functionality: Interactive code editor with live React component preview, resizable panels, and view mode switching (code-only, split, preview-only)
- Purpose: Enable real-time testing and visualization of React code while editing
- Trigger: Enable "Enable split-screen preview" checkbox when creating/editing JSX/TSX/JavaScript/TypeScript snippets
- Progression: User enables preview → Split editor appears → User types code → Preview updates in real-time → User adjusts panel sizes or switches view modes → Saves snippet
- Success criteria: Preview updates within 100ms of code changes, no lag during typing, error messages display clearly with AI help option
**Snippet Organization**
- Functionality: Real-time search across title, description, language, and code content
@@ -90,21 +97,27 @@ Animations should feel **smooth and purposeful**, enhancing navigation and feedb
- **Components**:
- Router (react-router-dom) for page navigation and routing
- Custom Navigation component with hamburger menu using Framer Motion for slide-in animations
- ResizablePanel (shadcn) for split-screen editor with adjustable panel sizes
- Button (shadcn) for all interactive actions with hover effects
- Card (shadcn) for snippet cards with hover elevation
- Dialog (shadcn) for snippet creation and viewing
- Input (shadcn) for search and form fields
- Select (shadcn) for language picker dropdown
- Alert Dialog (shadcn) for delete confirmations
- Checkbox (shadcn) for preview toggle
- Monaco Editor (@monaco-editor/react) for code editing with syntax highlighting
- **Customizations**:
- Custom Navigation drawer slides from left with backdrop overlay
- Custom SplitScreenEditor with three view modes (code-only, split-screen, preview-only)
- Active navigation items show bold text and filled background
- Page transitions use fade and slide animations
- Buttons: Rest → Hover (brightness increase) → Active (scale 0.98) → Disabled (opacity 50%)
- ResizableHandle shows interactive draggable separator with visual feedback
- **States**:
- Navigation items: Default → Hover (background muted) → Active (background primary, bold text)
- Buttons: Rest → Hover (brightness increase) → Active (scale down) → Disabled (opacity 50%)
- Cards: Rest → Hover (slight elevation and border glow)
- Split Editor view toggle: Default → Hover (subtle highlight) → Active (filled background)
- **Icon Selection**:
- House (regular/bold) for home navigation
- Atom (regular/bold) for atoms page
@@ -114,17 +127,24 @@ Animations should feel **smooth and purposeful**, enhancing navigation and feedb
- X (bold) for close menu
- Trash (regular) for delete
- MagnifyingGlass (regular) for search
- Code (regular) for code-only view
- Eye (regular) for preview-only view
- SplitHorizontal (regular) for split-screen view
- SplitVertical (regular) for toggle preview in viewer
- Sparkle (fill) for AI error helper
- **Spacing**:
- Navigation menu: p-4 for nav container, space-y-2 for items
- Navigation items: px-4 py-3 (touch-friendly 44px+ targets)
- Page container: px-6 py-8
- Section margins: mb-8 for headers
- Grid: gap-6 (24px) for snippet cards
- Split editor controls: gap-2 for view mode buttons
- **Mobile**:
- Navigation menu expands to full-width overlay on mobile
- Touch-optimized button sizes (min 44px tap targets)
- Responsive grid adapts from multi-column to single column
- Stack navigation items vertically with full-width buttons
- Stack navigation items vertically with full-width buttons
- Split-screen editor stacks vertically on small screens or switches to single view mode

View File

@@ -21,6 +21,7 @@ import {
} from '@/components/ui/select'
import { Snippet } from '@/lib/types'
import { MonacoEditor } from '@/components/MonacoEditor'
import { SplitScreenEditor } from '@/components/SplitScreenEditor'
import { strings, appConfig, LANGUAGES } from '@/lib/config'
interface SnippetDialogProps {
@@ -166,14 +167,25 @@ export function SnippetDialog({ open, onOpenChange, onSave, editingSnippet }: Sn
<Label htmlFor="code">
{strings.snippetDialog.fields.code.label}{strings.snippetDialog.fields.code.required ? ' *' : ''}
</Label>
<div className={`rounded-md border overflow-hidden ${errors.code ? 'border-destructive ring-2 ring-destructive/20' : 'border-border'}`}>
<MonacoEditor
value={code}
onChange={setCode}
language={language}
height="400px"
/>
</div>
{hasPreview && appConfig.previewEnabledLanguages.includes(language) ? (
<div className={errors.code ? 'ring-2 ring-destructive/20 rounded-md' : ''}>
<SplitScreenEditor
value={code}
onChange={setCode}
language={language}
height="500px"
/>
</div>
) : (
<div className={`rounded-md border overflow-hidden ${errors.code ? 'border-destructive ring-2 ring-destructive/20' : 'border-border'}`}>
<MonacoEditor
value={code}
onChange={setCode}
language={language}
height="400px"
/>
</div>
)}
{errors.code && (
<p className="text-sm text-destructive">{errors.code}</p>
)}

View File

@@ -0,0 +1,108 @@
import { useState } from 'react'
import { MonacoEditor } from '@/components/MonacoEditor'
import { ReactPreview } from '@/components/ReactPreview'
import { Button } from '@/components/ui/button'
import { ResizableHandle, ResizablePanel, ResizablePanelGroup } from '@/components/ui/resizable'
import { Code, Eye, SplitHorizontal } from '@phosphor-icons/react'
interface SplitScreenEditorProps {
value: string
onChange: (value: string) => void
language: string
height?: string
}
type ViewMode = 'split' | 'code' | 'preview'
export function SplitScreenEditor({
value,
onChange,
language,
height = '500px'
}: SplitScreenEditorProps) {
const [viewMode, setViewMode] = useState<ViewMode>('split')
const isPreviewSupported = ['JSX', 'TSX', 'JavaScript', 'TypeScript'].includes(language)
if (!isPreviewSupported) {
return (
<MonacoEditor
value={value}
onChange={onChange}
language={language}
height={height}
/>
)
}
return (
<div className="space-y-3">
<div className="flex items-center gap-2 justify-end">
<div className="flex items-center gap-1 p-1 bg-muted rounded-md">
<Button
variant={viewMode === 'code' ? 'secondary' : 'ghost'}
size="sm"
onClick={() => setViewMode('code')}
className="gap-2 h-8"
>
<Code className="h-4 w-4" />
<span className="hidden sm:inline">Code</span>
</Button>
<Button
variant={viewMode === 'split' ? 'secondary' : 'ghost'}
size="sm"
onClick={() => setViewMode('split')}
className="gap-2 h-8"
>
<SplitHorizontal className="h-4 w-4" />
<span className="hidden sm:inline">Split</span>
</Button>
<Button
variant={viewMode === 'preview' ? 'secondary' : 'ghost'}
size="sm"
onClick={() => setViewMode('preview')}
className="gap-2 h-8"
>
<Eye className="h-4 w-4" />
<span className="hidden sm:inline">Preview</span>
</Button>
</div>
</div>
<div
className="rounded-md border border-border overflow-hidden bg-card"
style={{ height }}
>
{viewMode === 'code' && (
<MonacoEditor
value={value}
onChange={onChange}
language={language}
height={height}
/>
)}
{viewMode === 'preview' && (
<ReactPreview code={value} language={language} />
)}
{viewMode === 'split' && (
<ResizablePanelGroup direction="horizontal" className="h-full">
<ResizablePanel defaultSize={50} minSize={30}>
<MonacoEditor
value={value}
onChange={onChange}
language={language}
height="100%"
/>
</ResizablePanel>
<ResizableHandle withHandle />
<ResizablePanel defaultSize={50} minSize={30}>
<ReactPreview code={value} language={language} />
</ResizablePanel>
</ResizablePanelGroup>
)}
</div>
</div>
)
}