Generated by Spark: Have option to have a split screen snippet. Code + Preview. This could allow rendering a React app.

This commit is contained in:
2026-01-17 14:43:49 +00:00
committed by GitHub
parent f14e777cd8
commit b409f168d1
7 changed files with 230 additions and 33 deletions

42
PRD.md
View File

@@ -13,18 +13,25 @@ This is a CRUD application with search, filtering, and organization features but
## Essential Features
### Create Snippet
- **Functionality**: Users can create a new code snippet with title, description, language selection, and code content using Monaco Editor for enhanced code editing
- **Purpose**: Core value proposition - storing reusable code for later retrieval with professional IDE-like experience
- **Functionality**: Users can create a new code snippet with title, description, language selection, code content using Monaco Editor, and optional preview mode for React components
- **Purpose**: Core value proposition - storing reusable code for later retrieval with professional IDE-like experience and live preview capability for React snippets
- **Trigger**: Click "New Snippet" button or keyboard shortcut
- **Progression**: Click New Snippet → Fill in title field → Select language from dropdown → Write/paste code in Monaco Editor with syntax highlighting → Add optional description → Click Save
- **Success criteria**: Snippet appears in the list immediately, persists across page refreshes, Monaco Editor loads lazily without blocking UI, and code is searchable
- **Progression**: Click New Snippet → Fill in title field → Select language from dropdown (including JSX/TSX options) → Toggle preview mode checkbox if creating React component → Write/paste code in Monaco Editor with syntax highlighting → Add optional description → Click Save
- **Success criteria**: Snippet appears in the list immediately with preview badge if enabled, persists across page refreshes, Monaco Editor loads lazily without blocking UI, and code is searchable
### View & Organize Snippets
- **Functionality**: Display all snippets in a filterable list with preview cards showing title, language, and truncated code; click to open full-screen Monaco viewer
- **Purpose**: Quick scanning and navigation through saved snippets with professional code viewing
- **Functionality**: Display all snippets in a filterable list with preview cards showing title, language, preview badge, and truncated code; click to open full-screen Monaco viewer with optional split-screen preview
- **Purpose**: Quick scanning and navigation through saved snippets with professional code viewing and live React component preview
- **Trigger**: Default view on app load, click card to view full code
- **Progression**: View list → Scan titles and languages → Click card to open full-screen Monaco viewer with syntax highlighting → Copy or edit from viewer
- **Success criteria**: All snippets visible, sorted by recent first, viewer opens instantly with lazy-loaded Monaco Editor
- **Progression**: View list → Scan titles, languages, and preview badges → Click card to open full-screen viewer with syntax highlighting → Toggle split-screen preview for React components → Copy or edit from viewer
- **Success criteria**: All snippets visible, sorted by recent first, viewer opens instantly with lazy-loaded Monaco Editor, preview renders React components in real-time
### Split-Screen Preview
- **Functionality**: For React-compatible snippets (JSX/TSX/JavaScript/TypeScript), render live preview alongside code editor in split-screen layout
- **Purpose**: Enable developers to see React components rendered in real-time, test UI snippets instantly, and save complete working component examples
- **Trigger**: Enable preview mode checkbox when creating/editing snippet, toggle preview button in viewer
- **Progression**: Create snippet with JSX/TSX → Check "Enable preview" → Save → Open viewer → See code on left, live preview on right → Toggle preview on/off as needed
- **Success criteria**: React code compiles and renders safely, errors display helpful messages, preview updates reflect code changes, supports React hooks and JSX syntax
### Search & Filter
- **Functionality**: Real-time search across snippet titles, descriptions, and code content; filter by programming language
@@ -54,6 +61,8 @@ This is a CRUD application with search, filtering, and organization features but
- **Invalid Input**: Require title and code content minimum, show inline validation errors
- **Search No Results**: Display "No snippets found" message with suggestion to adjust filters
- **Network/Storage Errors**: Graceful error messages with retry options (though KV storage is local)
- **Preview Rendering Errors**: Display detailed error messages when React code fails to compile or render, show warnings for non-React language previews
- **Preview Not Available**: Show informative message for snippets without preview enabled or non-React languages
## Design Direction
The design should evoke a premium developer tool - clean, focused, and sophisticated with subtle tech-forward aesthetics. Think VS Code meets Notion: professional minimalism with purposeful color accents and smooth micro-interactions that make frequent use satisfying.
@@ -91,20 +100,25 @@ Animations should feel responsive and technical - quick, purposeful movements th
- Button for all actions (primary solid style for Save/Create, ghost style for secondary actions)
- Select for language dropdown with custom styling to match theme
- Textarea for code input with monospace font
- Badge for language tags with color coding
- Badge for language tags with color coding, preview indicator badge with split-screen icon
- ScrollArea for long code blocks within cards
- Toast (Sonner) for copy confirmations and success messages
- AlertDialog for delete confirmations
- Checkbox for enabling split-screen preview mode
- Alert for displaying preview rendering errors
- **Customizations**:
- Custom syntax language badges with predefined color mapping (JavaScript=yellow, Python=blue, etc.)
- Custom syntax language badges with predefined color mapping (JavaScript=yellow, Python=blue, JSX/TSX=cyan/sky, etc.)
- Floating action button for "New Snippet" with plus icon, fixed bottom-right on mobile
- Custom empty state component with illustration or icon and encouraging copy
- Split-screen preview renderer with safe React code execution
- Preview badge with split-screen icon to indicate preview-enabled snippets
- **States**:
- Buttons: Default with subtle gradient, hover with brightness increase and shadow, active with slight scale-down (0.98), disabled with 50% opacity
- Cards: Default flat, hover with shadow-lg and border glow, selected/expanded with accent border
- Buttons: Default with subtle gradient, hover with brightness increase and shadow, active with slight scale-down (0.98), disabled with 50% opacity, toggle state for preview on/off
- Cards: Default flat, hover with shadow-lg and border glow, selected/expanded with accent border, preview badge visible when enabled
- Inputs: Default with muted border, focus with accent ring and border color shift, error with red ring
- Preview pane: Loading state, error state with helpful message, rendered state with scrolling
- **Icon Selection**:
- Plus (create new snippet)
@@ -114,6 +128,8 @@ Animations should feel responsive and technical - quick, purposeful movements th
- MagnifyingGlass (search)
- Code (app logo/branding)
- Check (confirmation feedback)
- SplitVertical (preview mode indicator and toggle)
- WarningCircle (preview errors)
- **Spacing**:
- Container padding: p-6 (desktop) / p-4 (mobile)
@@ -121,6 +137,7 @@ Animations should feel responsive and technical - quick, purposeful movements th
- Form fields: space-y-4 for vertical stacking
- Section margins: mb-8 for major sections
- Inline elements: gap-2 for icon+text combinations
- Split-screen: Equal 50/50 width distribution with border separator
- **Mobile**:
- Single column card layout with full-width cards
@@ -129,3 +146,4 @@ Animations should feel responsive and technical - quick, purposeful movements th
- Floating action button (FAB) for create action instead of header button
- Touch-friendly button sizes (min 44px tap targets)
- Bottom sheet for language filter on mobile vs. dropdown on desktop
- Preview stacks vertically below code editor on small screens

View File

@@ -1,4 +1,4 @@
{
"dbType": null
"dbType": null
{
"templateVersion": 0,
"dbType": null
}

View File

@@ -0,0 +1,107 @@
import { useEffect, useState, useRef } from 'react'
import * as React from 'react'
import * as ReactDOM from 'react-dom'
import { Alert, AlertDescription } from '@/components/ui/alert'
import { WarningCircle } from '@phosphor-icons/react'
interface ReactPreviewProps {
code: string
language: string
}
export function ReactPreview({ code, language }: ReactPreviewProps) {
const [error, setError] = useState<string | null>(null)
const [Component, setComponent] = useState<React.ComponentType | null>(null)
const mountRef = useRef<HTMLDivElement>(null)
useEffect(() => {
setError(null)
setComponent(null)
const isReactCode = ['JSX', 'TSX', 'JavaScript', 'TypeScript'].includes(language)
if (!isReactCode) {
return
}
try {
const transformedCode = code
.replace(/^import\s+.*from\s+['"]react['"];?\s*/gm, '')
.replace(/^import\s+.*from\s+['"].*['"];?\s*/gm, '')
.replace(/export\s+default\s+/g, '')
.replace(/export\s+/g, '')
const wrappedCode = `
(function() {
const React = arguments[0];
const useState = React.useState;
const useEffect = React.useEffect;
const useRef = React.useRef;
const useMemo = React.useMemo;
const useCallback = React.useCallback;
${transformedCode}
const lastStatement = (${transformedCode.trim().split('\n').pop()});
return lastStatement;
})
`
const componentFactory = eval(wrappedCode)
const CreatedComponent = componentFactory(React)
if (typeof CreatedComponent === 'function') {
setComponent(() => CreatedComponent)
} else if (React.isValidElement(CreatedComponent)) {
setComponent(() => () => CreatedComponent)
} else {
setError('Code must export a React component or JSX element')
}
} catch (err) {
setError(err instanceof Error ? err.message : 'Failed to render preview')
}
}, [code, language])
if (!['JSX', 'TSX', 'JavaScript', 'TypeScript'].includes(language)) {
return (
<div className="h-full flex items-center justify-center p-6 bg-muted/30">
<div className="text-center text-muted-foreground">
<WarningCircle className="h-12 w-12 mx-auto mb-3 opacity-50" />
<p className="text-sm">Preview not available for {language}</p>
<p className="text-xs mt-1">Use JSX, TSX, JavaScript, or TypeScript</p>
</div>
</div>
)
}
if (error) {
return (
<div className="h-full overflow-auto p-6 bg-destructive/5">
<Alert variant="destructive">
<WarningCircle className="h-4 w-4" />
<AlertDescription className="font-mono text-xs whitespace-pre-wrap">
{error}
</AlertDescription>
</Alert>
</div>
)
}
if (!Component) {
return (
<div className="h-full flex items-center justify-center p-6 bg-muted/30">
<div className="text-center text-muted-foreground">
<p className="text-sm">Loading preview...</p>
</div>
</div>
)
}
return (
<div className="h-full overflow-auto bg-background">
<div className="p-6" ref={mountRef}>
<Component />
</div>
</div>
)
}

View File

@@ -2,7 +2,7 @@ import { useState } from 'react'
import { Card } from '@/components/ui/card'
import { Button } from '@/components/ui/button'
import { Badge } from '@/components/ui/badge'
import { Copy, Pencil, Trash, Check, ArrowsOut } from '@phosphor-icons/react'
import { Copy, Pencil, Trash, Check, ArrowsOut, SplitVertical } from '@phosphor-icons/react'
import { Snippet, LANGUAGE_COLORS } from '@/lib/types'
import { cn } from '@/lib/utils'
@@ -46,15 +46,26 @@ export function SnippetCard({ snippet, onEdit, onDelete, onCopy, onView }: Snipp
</p>
)}
</div>
<Badge
variant="outline"
className={cn(
"shrink-0 border font-medium text-xs px-2 py-1",
LANGUAGE_COLORS[snippet.language] || LANGUAGE_COLORS['Other']
<div className="flex gap-2 shrink-0 items-start">
{snippet.hasPreview && (
<Badge
variant="outline"
className="border-accent/30 bg-accent/10 text-accent text-xs px-2 py-1 gap-1"
>
<SplitVertical className="h-3 w-3" weight="bold" />
Preview
</Badge>
)}
>
{snippet.language}
</Badge>
<Badge
variant="outline"
className={cn(
"border font-medium text-xs px-2 py-1",
LANGUAGE_COLORS[snippet.language] || LANGUAGE_COLORS['Other']
)}
>
{snippet.language}
</Badge>
</div>
</div>
<div

View File

@@ -11,6 +11,7 @@ import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
import { Textarea } from '@/components/ui/textarea'
import { Checkbox } from '@/components/ui/checkbox'
import {
Select,
SelectContent,
@@ -33,6 +34,7 @@ export function SnippetDialog({ open, onOpenChange, onSave, editingSnippet }: Sn
const [description, setDescription] = useState('')
const [language, setLanguage] = useState('JavaScript')
const [code, setCode] = useState('')
const [hasPreview, setHasPreview] = useState(false)
const [errors, setErrors] = useState<{ title?: string; code?: string }>({})
useEffect(() => {
@@ -41,11 +43,13 @@ export function SnippetDialog({ open, onOpenChange, onSave, editingSnippet }: Sn
setDescription(editingSnippet.description)
setLanguage(editingSnippet.language)
setCode(editingSnippet.code)
setHasPreview(editingSnippet.hasPreview || false)
} else {
setTitle('')
setDescription('')
setLanguage('JavaScript')
setCode('')
setHasPreview(false)
}
setErrors({})
}, [editingSnippet, open])
@@ -70,12 +74,14 @@ export function SnippetDialog({ open, onOpenChange, onSave, editingSnippet }: Sn
description: description.trim(),
language,
code: code.trim(),
hasPreview,
})
setTitle('')
setDescription('')
setLanguage('JavaScript')
setCode('')
setHasPreview(false)
setErrors({})
onOpenChange(false)
}
@@ -125,6 +131,22 @@ export function SnippetDialog({ open, onOpenChange, onSave, editingSnippet }: Sn
</Select>
</div>
{['JSX', 'TSX', 'JavaScript', 'TypeScript'].includes(language) && (
<div className="flex items-center space-x-2 py-2">
<Checkbox
id="hasPreview"
checked={hasPreview}
onCheckedChange={(checked) => setHasPreview(checked as boolean)}
/>
<Label
htmlFor="hasPreview"
className="text-sm font-normal cursor-pointer"
>
Enable split-screen preview for this snippet
</Label>
</div>
)}
<div className="space-y-2">
<Label htmlFor="description">Description</Label>
<Textarea

View File

@@ -6,9 +6,10 @@ import {
} from '@/components/ui/dialog'
import { Button } from '@/components/ui/button'
import { Badge } from '@/components/ui/badge'
import { Copy, Pencil, X, Check } from '@phosphor-icons/react'
import { Copy, Pencil, X, Check, SplitVertical } from '@phosphor-icons/react'
import { Snippet, LANGUAGE_COLORS } from '@/lib/types'
import { MonacoEditor } from '@/components/MonacoEditor'
import { ReactPreview } from '@/components/ReactPreview'
import { cn } from '@/lib/utils'
import { useState } from 'react'
@@ -22,6 +23,7 @@ interface SnippetViewerProps {
export function SnippetViewer({ snippet, open, onOpenChange, onEdit, onCopy }: SnippetViewerProps) {
const [isCopied, setIsCopied] = useState(false)
const [showPreview, setShowPreview] = useState(true)
if (!snippet) return null
@@ -35,6 +37,8 @@ export function SnippetViewer({ snippet, open, onOpenChange, onEdit, onCopy }: S
onOpenChange(false)
onEdit(snippet)
}
const canPreview = snippet.hasPreview && ['JSX', 'TSX', 'JavaScript', 'TypeScript'].includes(snippet.language)
return (
<Dialog open={open} onOpenChange={onOpenChange}>
@@ -66,6 +70,17 @@ export function SnippetViewer({ snippet, open, onOpenChange, onEdit, onCopy }: S
</p>
</div>
<div className="flex gap-2 shrink-0">
{canPreview && (
<Button
variant={showPreview ? "default" : "outline"}
size="sm"
onClick={() => setShowPreview(!showPreview)}
className="gap-2"
>
<SplitVertical className="h-4 w-4" />
{showPreview ? 'Hide' : 'Show'} Preview
</Button>
)}
<Button
variant="outline"
size="sm"
@@ -105,14 +120,33 @@ export function SnippetViewer({ snippet, open, onOpenChange, onEdit, onCopy }: S
</div>
</DialogHeader>
<div className="flex-1 overflow-hidden">
<MonacoEditor
value={snippet.code}
onChange={() => {}}
language={snippet.language}
height="100%"
readOnly={true}
/>
<div className="flex-1 overflow-hidden flex">
{canPreview && showPreview ? (
<>
<div className="flex-1 overflow-hidden border-r border-border">
<MonacoEditor
value={snippet.code}
onChange={() => {}}
language={snippet.language}
height="100%"
readOnly={true}
/>
</div>
<div className="flex-1 overflow-hidden">
<ReactPreview code={snippet.code} language={snippet.language} />
</div>
</>
) : (
<div className="flex-1 overflow-hidden">
<MonacoEditor
value={snippet.code}
onChange={() => {}}
language={snippet.language}
height="100%"
readOnly={true}
/>
</div>
)}
</div>
</DialogContent>
</Dialog>

View File

@@ -6,11 +6,14 @@ export interface Snippet {
code: string
createdAt: number
updatedAt: number
hasPreview?: boolean
}
export const LANGUAGES = [
'JavaScript',
'TypeScript',
'JSX',
'TSX',
'Python',
'Java',
'C++',
@@ -33,6 +36,8 @@ export type Language = typeof LANGUAGES[number]
export const LANGUAGE_COLORS: Record<string, string> = {
'JavaScript': 'bg-yellow-500/20 text-yellow-300 border-yellow-500/30',
'TypeScript': 'bg-blue-500/20 text-blue-300 border-blue-500/30',
'JSX': 'bg-cyan-500/20 text-cyan-300 border-cyan-500/30',
'TSX': 'bg-sky-500/20 text-sky-300 border-sky-500/30',
'Python': 'bg-blue-400/20 text-blue-200 border-blue-400/30',
'Java': 'bg-red-500/20 text-red-300 border-red-500/30',
'C++': 'bg-pink-500/20 text-pink-300 border-pink-500/30',