Generated by Spark: React Counter Component

JSX
A simple counter component with increment and decrement buttons

Last updated: 01/01/2024, 00:00:00

Hide Preview
Copy
Edit

12345678910111213141516171819202122232425
Unexpected token '<'

Ask AI for Help
This commit is contained in:
2026-01-17 15:02:23 +00:00
committed by GitHub
parent 1ba8130f27
commit 8017e2a37d
5 changed files with 213 additions and 368 deletions

200
PRD.md
View File

@@ -1,159 +1,99 @@
# Planning Guide
A developer-focused code snippet manager that allows users to save, organize, search, and quickly access reusable code snippets across multiple programming languages.
A simple, interactive counter application that allows users to increment and decrement a numerical value with clear visual feedback.
**Experience Qualities**:
1. **Efficient** - Users should be able to save and retrieve snippets in seconds with minimal friction
2. **Professional** - The interface should feel polished and trustworthy, suitable for daily developer workflow
3. **Intuitive** - Navigation and organization should be self-evident without requiring documentation
**Experience Qualities**:
1. **Playful** - The counter should feel fun and responsive, encouraging interaction through satisfying button clicks and smooth animations.
2. **Clear** - The current count value should be immediately obvious and easy to read at any time.
3. **Tactile** - Button interactions should provide strong visual feedback that makes the interface feel physical and responsive.
**Complexity Level**: Light Application (multiple features with basic state)
This is a CRUD application with search, filtering, and organization features but doesn't require complex routing or advanced state management beyond persisted storage.
**Complexity Level**: Micro Tool (single-purpose application) - This is a focused counter with a single clear purpose: track a numerical value through increment and decrement actions.
## Essential Features
### Create Snippet
- **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 (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
**Increment Counter**
- Functionality: Increases the counter value by 1
- Purpose: Allows users to count up for any tracking need
- Trigger: Click/tap the increment (+) button
- Progression: User clicks button → Value increases by 1 → New value displays with brief animation
- Success criteria: Counter value increases correctly and persists between sessions
### View & Organize Snippets
- **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, 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
**Decrement Counter**
- Functionality: Decreases the counter value by 1
- Purpose: Allows users to count down or correct mistakes
- Trigger: Click/tap the decrement (-) button
- Progression: User clicks button → Value decreases by 1 → New value displays with brief animation
- Success criteria: Counter value decreases correctly and persists between sessions
### 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
- **Purpose**: Quick retrieval when snippet library grows large
- **Trigger**: Type in search bar or select language filter
- **Progression**: Type search query → Results filter in real-time → Clear search to return to full list
- **Success criteria**: Results appear instantly (<100ms), search is case-insensitive, highlights matched terms
### Edit & Delete
- **Functionality**: Modify existing snippets using Monaco Editor or remove them entirely
- **Purpose**: Keep snippet library current and relevant with professional editing experience
- **Trigger**: Click edit icon on snippet card or from viewer, click delete with confirmation
- **Progression**: Click Edit → Monaco Editor opens with existing code → Modify fields with syntax highlighting → Save changes → See updated snippet in list
- **Success criteria**: Changes persist, Monaco Editor retains user edits, delete requires confirmation, no accidental data loss
### Copy to Clipboard
- **Functionality**: One-click copy of code content to clipboard
- **Purpose**: Primary use case - quickly use snippets in other projects
- **Trigger**: Click copy icon on snippet card
- **Progression**: Click copy button → Visual feedback (toast notification) → Paste code elsewhere
- **Success criteria**: Code copies exactly as stored, toast confirms action, works across browsers
### AI Error Helper
- **Functionality**: Intelligent error analysis button that appears when errors occur, uses AI to explain errors in plain language and suggest fixes
- **Purpose**: Help developers quickly understand and resolve errors without leaving the app
- **Trigger**: Automatically appears when runtime errors, preview rendering errors, or validation errors occur
- **Progression**: Error occurs → AI helper button appears with pulsing animation → Click button → AI analyzes error context → Display explanation and suggested fixes in dialog
- **Success criteria**: Button appears within 100ms of error, AI provides helpful context-aware explanations, suggestions are actionable and specific to the error type
**Reset Counter**
- Functionality: Returns the counter to zero
- Purpose: Quickly start fresh without multiple decrements
- Trigger: Click/tap the reset button
- Progression: User clicks reset → Counter returns to 0 → Visual confirmation
- Success criteria: Counter resets to zero instantly
## Edge Case Handling
- **Empty State**: Show welcoming illustration and "Create your first snippet" CTA when no snippets exist
- **Long Code Blocks**: Truncate preview with "Show more" expansion, scroll within card for full view
- **Duplicate Titles**: Allow duplicates but show language badge to differentiate
- **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, AI helper button available for error analysis
- **Preview Not Available**: Show informative message for snippets without preview enabled or non-React languages
- **AI Error Analysis Failure**: If AI service is unavailable, show graceful fallback message with standard error details
- **Negative Numbers**: Allow negative values - no lower limit restriction
- **Large Numbers**: Display properly formatted large numbers (with commas for readability)
- **Rapid Clicking**: Handle multiple rapid button presses smoothly without lag
- **Initial Load**: Start at 0 if no saved value exists
## 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.
The design should evoke a sense of **precision, control, and satisfaction** - like using a premium mechanical device. Think digital tally counter meets modern minimalism, with bold typography that makes the number feel important and tactile buttons that beg to be pressed.
## Color Selection
A developer-focused dark-leaning palette with vibrant accent colors for actions and syntax language tags.
- **Primary Color**: Deep indigo `oklch(0.35 0.15 265)` - Communicates technical sophistication and trust, used for primary actions and headers
- **Secondary Colors**: Charcoal gray `oklch(0.25 0.01 265)` for cards/surfaces; Soft gray `oklch(0.65 0.01 265)` for secondary text
- **Accent Color**: Electric cyan `oklch(0.75 0.15 195)` - Attention-grabbing highlight for CTAs, copy actions, and success states
- **Foreground/Background Pairings**:
- Background (Off-black #0F0F14 / oklch(0.08 0.01 265)): Light gray text (oklch(0.95 0.01 265) #F0F0F2) - Ratio 11.2:1 ✓
- Card surface (oklch(0.15 0.01 265)): White text (oklch(0.98 0 0)) - Ratio 13.5:1 ✓
- Primary (Deep indigo oklch(0.35 0.15 265)): White text (oklch(0.98 0 0)) - Ratio 7.8:1 ✓
- Accent (Cyan oklch(0.75 0.15 195)): Deep gray text (oklch(0.15 0.01 265)) - Ratio 8.5:1 ✓
A bold, high-contrast scheme that feels modern and precise.
- **Primary Color**: Deep Electric Blue (oklch(0.45 0.20 240)) - Communicates precision and digital accuracy, used for primary action buttons
- **Secondary Colors**:
- Rich Navy (oklch(0.15 0.03 240)) for depth and cards
- Slate Gray (oklch(0.25 0.02 240)) for secondary elements
- **Accent Color**: Vibrant Cyan (oklch(0.75 0.15 200)) - Eye-catching highlight for the counter value itself and focus states
- **Foreground/Background Pairings**:
- Background (Dark Navy oklch(0.10 0.02 240)): Light Gray text (oklch(0.95 0.01 240)) - Ratio 15.2:1 ✓
- Primary (Electric Blue oklch(0.45 0.20 240)): White text (oklch(0.98 0 0)) - Ratio 5.8:1 ✓
- Accent (Vibrant Cyan oklch(0.75 0.15 200)): Dark Navy text (oklch(0.10 0.02 240)) - Ratio 12.1:1 ✓
## Font Selection
Typography should balance technical precision with readability - a clean geometric sans for UI elements and a monospace font for code display that developers recognize and trust.
- **Typographic Hierarchy**:
- H1 (App Title): Space Grotesk Bold/32px/tight letter spacing (-0.02em)
- H2 (Section Headers): Space Grotesk SemiBold/20px/normal spacing
- Body (UI Text): Space Grotesk Regular/15px/1.5 line height
- Code Display: JetBrains Mono Regular/14px/1.6 line height
- Labels & Captions: Space Grotesk Medium/13px/uppercase with increased tracking (0.05em)
Typography should feel technical yet approachable, with numeric characters that are highly legible and distinctive.
- **Typographic Hierarchy**:
- Counter Value: Space Grotesk Bold/96px/tight letter spacing - The hero element, large and commanding
- Button Labels: Space Grotesk Medium/16px/normal spacing - Clear and readable
- Secondary Text: Space Grotesk Regular/14px/relaxed spacing - Subtle guidance
## Animations
Animations should feel responsive and technical - quick, purposeful movements that provide feedback without delay. Focus on micro-interactions: button states that respond instantly, smooth card expansions when viewing details, and satisfying confirmation animations when copying code (scale pulse + checkmark). Keep transitions under 200ms for interactions, 300ms max for layout changes. Use ease-out curves for most interactions to feel snappy.
Animations should emphasize the counting action - the number should briefly scale and glow when changed, buttons should have satisfying press states with subtle scale transforms, and all transitions should use snappy easing (0.2s) to feel responsive. The reset action gets a more pronounced animation to signal the larger state change.
## Component Selection
- **Components**:
- Dialog for create/edit snippet forms with full-screen modal feel on mobile
- Card for snippet list items with hover elevation and click interaction
- Input for search bar with clear button and icon
- 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, 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
- Custom AI helper button with sparkle/magic wand icon for error analysis
- Button (shadcn) for all interactive controls, with `size="lg"` for main increment/decrement, and `variant="outline"` for reset
- Card (shadcn) as the main container for the counter interface
- Separator (shadcn) to divide counter from controls
- **Customizations**:
- 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
- Custom counter display component with animated number transitions using framer-motion
- Button hover/active states enhanced with scale transforms and glow effects
- Gradient background pattern using CSS for visual interest
- **States**:
- 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
- Buttons: default has subtle shadow, hover scales to 1.05 and brightens, active scales to 0.95, disabled is muted
- Counter display: pulses briefly on value change with scale animation
- **Icon Selection**:
- Plus (create new snippet)
- Copy (clipboard action)
- Pencil (edit action)
- Trash (delete action)
- MagnifyingGlass (search)
- Code (app logo/branding)
- Check (confirmation feedback)
- SplitVertical (preview mode indicator and toggle)
- WarningCircle (preview errors)
- Sparkles/MagicWand (AI helper for error analysis)
- Plus (bold weight) for increment
- Minus (bold weight) for decrement
- ArrowCounterClockwise (bold weight) for reset
- **Spacing**:
- Container padding: p-6 (desktop) / p-4 (mobile)
- Card gaps: gap-4 for grid layout
- 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
- Outer container: p-8
- Card padding: p-12
- Button gaps: gap-4
- Section spacing: space-y-8
- **Mobile**:
- Single column card layout with full-width cards
- Dialog becomes full-screen sheet on mobile
- Search bar remains sticky at top
- 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
- Stack buttons vertically on mobile with full-width layout
- Reduce counter font size to 64px on small screens
- Maintain tap-friendly 44px minimum touch targets
- Card padding reduces to p-6 on mobile

View File

@@ -4,10 +4,10 @@
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>SnippetVault - Code Snippet Manager</title>
<title>Counter</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
<link href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@400;500;600;700&display=swap" rel="stylesheet">
<link href="/src/main.css" rel="stylesheet" />
</head>

View File

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

View File

@@ -1,249 +1,96 @@
import { useState, useMemo } from 'react'
import { useKV } from '@github/spark/hooks'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select'
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
} from '@/components/ui/alert-dialog'
import { Code, Plus, MagnifyingGlass, Funnel } from '@phosphor-icons/react'
import { toast } from 'sonner'
import { SnippetCard } from '@/components/SnippetCard'
import { SnippetDialog } from '@/components/SnippetDialog'
import { SnippetViewer } from '@/components/SnippetViewer'
import { EmptyState } from '@/components/EmptyState'
import { Snippet, LANGUAGES } from '@/lib/types'
import { Card } from '@/components/ui/card'
import { Separator } from '@/components/ui/separator'
import { Plus, Minus, ArrowCounterClockwise } from '@phosphor-icons/react'
import { motion } from 'framer-motion'
function App() {
const [snippets, setSnippets] = useKV<Snippet[]>('snippets', [])
const [dialogOpen, setDialogOpen] = useState(false)
const [editingSnippet, setEditingSnippet] = useState<Snippet | null>(null)
const [viewingSnippet, setViewingSnippet] = useState<Snippet | null>(null)
const [deleteId, setDeleteId] = useState<string | null>(null)
const [searchQuery, setSearchQuery] = useState('')
const [filterLanguage, setFilterLanguage] = useState<string>('all')
const [count, setCount] = useKV<number>('counter-value', 0)
const filteredSnippets = useMemo(() => {
let filtered = snippets || []
if (searchQuery.trim()) {
const query = searchQuery.toLowerCase()
filtered = filtered.filter(
(snippet) =>
snippet.title.toLowerCase().includes(query) ||
snippet.description.toLowerCase().includes(query) ||
snippet.code.toLowerCase().includes(query) ||
snippet.language.toLowerCase().includes(query)
)
}
if (filterLanguage !== 'all') {
filtered = filtered.filter((snippet) => snippet.language === filterLanguage)
}
return filtered.sort((a, b) => b.updatedAt - a.updatedAt)
}, [snippets, searchQuery, filterLanguage])
const handleSave = (snippetData: Omit<Snippet, 'id' | 'createdAt' | 'updatedAt'>) => {
if (editingSnippet) {
setSnippets((current) =>
(current || []).map((s) =>
s.id === editingSnippet.id
? { ...s, ...snippetData, updatedAt: Date.now() }
: s
)
)
toast.success('Snippet updated successfully')
} else {
const newSnippet: Snippet = {
...snippetData,
id: Date.now().toString(),
createdAt: Date.now(),
updatedAt: Date.now(),
}
setSnippets((current) => [newSnippet, ...(current || [])])
toast.success('Snippet created successfully')
}
setEditingSnippet(null)
const increment = () => {
setCount((current) => (current ?? 0) + 1)
}
const handleEdit = (snippet: Snippet) => {
setEditingSnippet(snippet)
setDialogOpen(true)
const decrement = () => {
setCount((current) => (current ?? 0) - 1)
}
const handleDelete = (id: string) => {
setDeleteId(id)
const reset = () => {
setCount(0)
}
const confirmDelete = () => {
if (deleteId) {
setSnippets((current) => (current || []).filter((s) => s.id !== deleteId))
toast.success('Snippet deleted')
setDeleteId(null)
}
}
const handleCopy = (code: string) => {
navigator.clipboard.writeText(code)
toast.success('Code copied to clipboard')
}
const handleNewSnippet = () => {
setEditingSnippet(null)
setDialogOpen(true)
}
const handleView = (snippet: Snippet) => {
setViewingSnippet(snippet)
}
const formattedCount = (count ?? 0).toLocaleString()
return (
<div className="min-h-screen bg-background">
<div className="border-b border-border bg-card/50 backdrop-blur-sm sticky top-0 z-10">
<div className="container mx-auto px-4 py-4">
<div className="flex items-center justify-between mb-4">
<div className="flex items-center gap-3">
<div className="rounded-lg bg-accent/10 p-2">
<Code className="h-7 w-7 text-accent" weight="bold" />
</div>
<div>
<h1 className="text-2xl md:text-3xl font-bold tracking-tight">
SnippetVault
</h1>
<p className="text-sm text-muted-foreground hidden sm:block">
Your personal code snippet library
</p>
</div>
</div>
<Button onClick={handleNewSnippet} className="gap-2 hidden sm:flex">
<Plus className="h-5 w-5" weight="bold" />
New Snippet
</Button>
</div>
<div className="flex flex-col sm:flex-row gap-3">
<div className="relative flex-1">
<MagnifyingGlass className="absolute left-3 top-1/2 -translate-y-1/2 h-5 w-5 text-muted-foreground" />
<Input
placeholder="Search snippets..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="pl-10"
/>
</div>
<div className="flex gap-3">
<div className="relative flex-1 sm:flex-none sm:w-48">
<Funnel className="absolute left-3 top-1/2 -translate-y-1/2 h-5 w-5 text-muted-foreground pointer-events-none" />
<Select value={filterLanguage} onValueChange={setFilterLanguage}>
<SelectTrigger className="pl-10">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="all">All Languages</SelectItem>
{LANGUAGES.map((lang) => (
<SelectItem key={lang} value={lang}>
{lang}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
</div>
</div>
</div>
<div className="min-h-screen bg-background flex items-center justify-center p-4 sm:p-8 relative overflow-hidden">
<div className="absolute inset-0 opacity-30">
<div className="absolute inset-0" style={{
backgroundImage: `
repeating-linear-gradient(0deg, transparent, transparent 2px, oklch(0.20 0.04 240) 2px, oklch(0.20 0.04 240) 4px),
repeating-linear-gradient(90deg, transparent, transparent 2px, oklch(0.20 0.04 240) 2px, oklch(0.20 0.04 240) 4px)
`,
backgroundSize: '40px 40px'
}} />
</div>
<main className="container mx-auto px-4 py-8">
{!snippets || snippets.length === 0 ? (
<EmptyState onCreateClick={handleNewSnippet} />
) : filteredSnippets.length === 0 ? (
<div className="text-center py-20">
<p className="text-muted-foreground text-lg">
No snippets match your search.
</p>
<Button
variant="link"
onClick={() => {
setSearchQuery('')
setFilterLanguage('all')
}}
className="mt-4"
<Card className="w-full max-w-md p-6 sm:p-12 shadow-2xl relative z-10 border-2">
<div className="space-y-8">
<div className="text-center">
<h1 className="text-2xl sm:text-3xl font-bold text-foreground mb-2">Counter</h1>
<p className="text-sm text-muted-foreground">Track anything, one click at a time</p>
</div>
<Separator />
<div className="flex items-center justify-center py-8">
<motion.div
key={count}
initial={{ scale: 1.2, opacity: 0 }}
animate={{ scale: 1, opacity: 1 }}
transition={{ duration: 0.2, ease: 'easeOut' }}
className="relative"
>
Clear filters
<div className="text-6xl sm:text-8xl font-bold text-accent tracking-tight" style={{
textShadow: '0 0 40px oklch(0.75 0.15 200 / 0.5)'
}}>
{formattedCount}
</div>
</motion.div>
</div>
<Separator />
<div className="flex flex-col sm:flex-row gap-4">
<Button
onClick={decrement}
size="lg"
className="flex-1 h-14 text-lg font-medium transition-all hover:scale-105 active:scale-95"
>
<Minus className="mr-2" weight="bold" />
Decrement
</Button>
<Button
onClick={increment}
size="lg"
className="flex-1 h-14 text-lg font-medium transition-all hover:scale-105 active:scale-95"
>
<Plus className="mr-2" weight="bold" />
Increment
</Button>
</div>
) : (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{filteredSnippets.map((snippet) => (
<SnippetCard
key={snippet.id}
snippet={snippet}
onEdit={handleEdit}
onDelete={handleDelete}
onCopy={handleCopy}
onView={handleView}
/>
))}
</div>
)}
</main>
<Button
onClick={handleNewSnippet}
size="lg"
className="fixed bottom-6 right-6 h-14 w-14 rounded-full shadow-lg sm:hidden"
>
<Plus className="h-6 w-6" weight="bold" />
</Button>
<SnippetDialog
open={dialogOpen}
onOpenChange={(open) => {
setDialogOpen(open)
if (!open) setEditingSnippet(null)
}}
onSave={handleSave}
editingSnippet={editingSnippet}
/>
<SnippetViewer
snippet={viewingSnippet}
open={!!viewingSnippet}
onOpenChange={(open) => !open && setViewingSnippet(null)}
onEdit={handleEdit}
onCopy={handleCopy}
/>
<AlertDialog open={!!deleteId} onOpenChange={(open) => !open && setDeleteId(null)}>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Delete snippet?</AlertDialogTitle>
<AlertDialogDescription>
This action cannot be undone. This will permanently delete this code snippet.
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogAction onClick={confirmDelete} className="bg-destructive text-destructive-foreground hover:bg-destructive/90">
Delete
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
<Button
onClick={reset}
variant="outline"
size="lg"
className="w-full h-12 transition-all hover:scale-105 active:scale-95 hover:border-accent hover:text-accent"
>
<ArrowCounterClockwise className="mr-2" weight="bold" />
Reset
</Button>
</div>
</Card>
</div>
)
}

View File

@@ -1,7 +1,67 @@
* {
font-family: 'Space Grotesk', sans-serif;
@import 'tailwindcss';
@import "tw-animate-css";
@layer base {
* {
@apply border-border
}
}
code, pre, textarea.font-mono {
font-family: 'JetBrains Mono', monospace;
:root {
--background: oklch(0.10 0.02 240);
--foreground: oklch(0.95 0.01 240);
--card: oklch(0.15 0.03 240);
--card-foreground: oklch(0.95 0.01 240);
--popover: oklch(0.15 0.03 240);
--popover-foreground: oklch(0.95 0.01 240);
--primary: oklch(0.45 0.20 240);
--primary-foreground: oklch(0.98 0 0);
--secondary: oklch(0.25 0.02 240);
--secondary-foreground: oklch(0.95 0.01 240);
--muted: oklch(0.20 0.02 240);
--muted-foreground: oklch(0.65 0.01 240);
--accent: oklch(0.75 0.15 200);
--accent-foreground: oklch(0.10 0.02 240);
--destructive: oklch(0.577 0.245 27.325);
--destructive-foreground: oklch(0.98 0 0);
--border: oklch(0.25 0.02 240);
--input: oklch(0.25 0.02 240);
--ring: oklch(0.75 0.15 200);
--radius: 0.75rem;
}
@theme {
--color-background: var(--background);
--color-foreground: var(--foreground);
--color-card: var(--card);
--color-card-foreground: var(--card-foreground);
--color-popover: var(--popover);
--color-popover-foreground: var(--popover-foreground);
--color-primary: var(--primary);
--color-primary-foreground: var(--primary-foreground);
--color-secondary: var(--secondary);
--color-secondary-foreground: var(--secondary-foreground);
--color-muted: var(--muted);
--color-muted-foreground: var(--muted-foreground);
--color-accent: var(--accent);
--color-accent-foreground: var(--accent-foreground);
--color-destructive: var(--destructive);
--color-destructive-foreground: var(--destructive-foreground);
--color-border: var(--border);
--color-input: var(--input);
--color-ring: var(--ring);
--radius-sm: calc(var(--radius) * 0.5);
--radius-md: var(--radius);
--radius-lg: calc(var(--radius) * 1.5);
--radius-xl: calc(var(--radius) * 2);
--radius-2xl: calc(var(--radius) * 3);
--radius-full: 9999px;
}
* {
font-family: 'Space Grotesk', sans-serif;
}