Generated by Spark: implement program

This commit is contained in:
2026-01-17 14:32:11 +00:00
committed by GitHub
parent 0764c9585a
commit 857c900714
11 changed files with 764 additions and 31 deletions

131
PRD.md Normal file
View File

@@ -0,0 +1,131 @@
# 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.
**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
**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.
## Essential Features
### Create Snippet
- **Functionality**: Users can create a new code snippet with title, description, language selection, and code content
- **Purpose**: Core value proposition - storing reusable code for later retrieval
- **Trigger**: Click "New Snippet" button or keyboard shortcut
- **Progression**: Click New Snippet → Fill in title field → Select language from dropdown → Paste/type code → Add optional description → Click Save
- **Success criteria**: Snippet appears in the list immediately, persists across page refreshes, and is searchable
### View & Organize Snippets
- **Functionality**: Display all snippets in a filterable list with preview cards showing title, language, and truncated code
- **Purpose**: Quick scanning and navigation through saved snippets
- **Trigger**: Default view on app load
- **Progression**: View list → Scan titles and languages → Click card to expand/view full details
- **Success criteria**: All snippets visible, sorted by recent first, with clear visual hierarchy
### 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 or remove them entirely
- **Purpose**: Keep snippet library current and relevant
- **Trigger**: Click edit icon on snippet card or click delete with confirmation
- **Progression**: Click Edit → Modify fields in modal → Save changes → See updated snippet in list
- **Success criteria**: Changes persist, 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
## 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)
## 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.
## 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 ✓
## 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)
## 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.
## 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
- ScrollArea for long code blocks within cards
- Toast (Sonner) for copy confirmations and success messages
- AlertDialog for delete confirmations
- **Customizations**:
- Custom syntax language badges with predefined color mapping (JavaScript=yellow, Python=blue, 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
- **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
- Inputs: Default with muted border, focus with accent ring and border color shift, error with red ring
- **Icon Selection**:
- Plus (create new snippet)
- Copy (clipboard action)
- Pencil (edit action)
- Trash (delete action)
- MagnifyingGlass (search)
- Code (app logo/branding)
- Check (confirmation feedback)
- **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
- **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

View File

@@ -4,9 +4,10 @@
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title></title>
<title>SnippetVault - Code Snippet Manager</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="/src/main.css" rel="stylesheet" />
</head>

View File

@@ -1,3 +1,4 @@
{
"templateVersion": 1
}
"templateVersion": 1,
"dbType": "kv"
}

View File

@@ -1,5 +1,236 @@
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 { EmptyState } from '@/components/EmptyState'
import { Snippet, LANGUAGES } from '@/lib/types'
function App() {
return <div></div>
const [snippets, setSnippets] = useKV<Snippet[]>('snippets', [])
const [dialogOpen, setDialogOpen] = useState(false)
const [editingSnippet, setEditingSnippet] = useState<Snippet | null>(null)
const [deleteId, setDeleteId] = useState<string | null>(null)
const [searchQuery, setSearchQuery] = useState('')
const [filterLanguage, setFilterLanguage] = useState<string>('all')
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 handleEdit = (snippet: Snippet) => {
setEditingSnippet(snippet)
setDialogOpen(true)
}
const handleDelete = (id: string) => {
setDeleteId(id)
}
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)
}
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>
<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"
>
Clear filters
</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}
/>
))}
</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}
/>
<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>
</div>
)
}
export default App

View File

@@ -0,0 +1,24 @@
import { Code } from '@phosphor-icons/react'
import { Button } from '@/components/ui/button'
interface EmptyStateProps {
onCreateClick: () => void
}
export function EmptyState({ onCreateClick }: EmptyStateProps) {
return (
<div className="flex flex-col items-center justify-center py-20 px-4 text-center">
<div className="rounded-full bg-accent/10 p-6 mb-6">
<Code className="h-16 w-16 text-accent" weight="duotone" />
</div>
<h3 className="text-2xl font-semibold mb-2">No snippets yet</h3>
<p className="text-muted-foreground mb-8 max-w-sm">
Start building your code snippet library. Save reusable code for quick access anytime.
</p>
<Button onClick={onCreateClick} size="lg" className="gap-2">
<Code className="h-5 w-5" weight="bold" />
Create Your First Snippet
</Button>
</div>
)
}

View File

@@ -0,0 +1,120 @@
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 } from '@phosphor-icons/react'
import { Snippet, LANGUAGE_COLORS } from '@/lib/types'
import { cn } from '@/lib/utils'
import { ScrollArea } from '@/components/ui/scroll-area'
interface SnippetCardProps {
snippet: Snippet
onEdit: (snippet: Snippet) => void
onDelete: (id: string) => void
onCopy: (code: string) => void
}
export function SnippetCard({ snippet, onEdit, onDelete, onCopy }: SnippetCardProps) {
const [isExpanded, setIsExpanded] = useState(false)
const [isCopied, setIsCopied] = useState(false)
const handleCopy = () => {
onCopy(snippet.code)
setIsCopied(true)
setTimeout(() => setIsCopied(false), 2000)
}
const truncatedCode = snippet.code.length > 200
? snippet.code.slice(0, 200) + '...'
: snippet.code
return (
<Card
className={cn(
"group relative overflow-hidden transition-all duration-200",
"hover:shadow-lg hover:shadow-accent/10 hover:border-accent/30",
"cursor-pointer"
)}
onClick={() => setIsExpanded(!isExpanded)}
>
<div className="p-5 space-y-3">
<div className="flex items-start justify-between gap-3">
<div className="flex-1 min-w-0">
<h3 className="font-semibold text-lg text-foreground truncate mb-1">
{snippet.title}
</h3>
{snippet.description && (
<p className="text-sm text-muted-foreground line-clamp-2">
{snippet.description}
</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']
)}
>
{snippet.language}
</Badge>
</div>
<div className="relative">
{isExpanded ? (
<ScrollArea className="h-64 w-full rounded-md border border-border bg-secondary/30 p-3">
<pre className="text-sm text-foreground/90">
<code className="font-mono">{snippet.code}</code>
</pre>
</ScrollArea>
) : (
<div className="relative rounded-md border border-border bg-secondary/30 p-3 overflow-hidden">
<pre className="text-sm text-foreground/90 line-clamp-4">
<code className="font-mono">{truncatedCode}</code>
</pre>
{snippet.code.length > 200 && (
<div className="absolute bottom-0 left-0 right-0 h-12 bg-gradient-to-t from-secondary/30 to-transparent" />
)}
</div>
)}
</div>
<div className="flex items-center justify-between pt-2" onClick={(e) => e.stopPropagation()}>
<span className="text-xs text-muted-foreground">
{new Date(snippet.updatedAt).toLocaleDateString()}
</span>
<div className="flex gap-1">
<Button
variant="ghost"
size="sm"
onClick={handleCopy}
className="h-8 w-8 p-0 hover:bg-accent/20 hover:text-accent"
>
{isCopied ? (
<Check className="h-4 w-4 text-accent" weight="bold" />
) : (
<Copy className="h-4 w-4" />
)}
</Button>
<Button
variant="ghost"
size="sm"
onClick={() => onEdit(snippet)}
className="h-8 w-8 p-0 hover:bg-primary/20 hover:text-primary"
>
<Pencil className="h-4 w-4" />
</Button>
<Button
variant="ghost"
size="sm"
onClick={() => onDelete(snippet.id)}
className="h-8 w-8 p-0 hover:bg-destructive/20 hover:text-destructive"
>
<Trash className="h-4 w-4" />
</Button>
</div>
</div>
</div>
</Card>
)
}

View File

@@ -0,0 +1,165 @@
import { useEffect, useState } from 'react'
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog'
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 {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select'
import { Snippet, LANGUAGES } from '@/lib/types'
interface SnippetDialogProps {
open: boolean
onOpenChange: (open: boolean) => void
onSave: (snippet: Omit<Snippet, 'id' | 'createdAt' | 'updatedAt'>) => void
editingSnippet?: Snippet | null
}
export function SnippetDialog({ open, onOpenChange, onSave, editingSnippet }: SnippetDialogProps) {
const [title, setTitle] = useState('')
const [description, setDescription] = useState('')
const [language, setLanguage] = useState('JavaScript')
const [code, setCode] = useState('')
const [errors, setErrors] = useState<{ title?: string; code?: string }>({})
useEffect(() => {
if (editingSnippet) {
setTitle(editingSnippet.title)
setDescription(editingSnippet.description)
setLanguage(editingSnippet.language)
setCode(editingSnippet.code)
} else {
setTitle('')
setDescription('')
setLanguage('JavaScript')
setCode('')
}
setErrors({})
}, [editingSnippet, open])
const handleSave = () => {
const newErrors: { title?: string; code?: string } = {}
if (!title.trim()) {
newErrors.title = 'Title is required'
}
if (!code.trim()) {
newErrors.code = 'Code is required'
}
if (Object.keys(newErrors).length > 0) {
setErrors(newErrors)
return
}
onSave({
title: title.trim(),
description: description.trim(),
language,
code: code.trim(),
})
setTitle('')
setDescription('')
setLanguage('JavaScript')
setCode('')
setErrors({})
onOpenChange(false)
}
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="sm:max-w-[600px] max-h-[90vh] overflow-y-auto">
<DialogHeader>
<DialogTitle className="text-2xl">
{editingSnippet ? 'Edit Snippet' : 'Create New Snippet'}
</DialogTitle>
<DialogDescription>
{editingSnippet
? 'Update your code snippet details below.'
: 'Save a reusable code snippet for quick access later.'}
</DialogDescription>
</DialogHeader>
<div className="space-y-4 py-4">
<div className="space-y-2">
<Label htmlFor="title">Title *</Label>
<Input
id="title"
placeholder="e.g., React useState Hook"
value={title}
onChange={(e) => setTitle(e.target.value)}
className={errors.title ? 'border-destructive ring-destructive' : ''}
/>
{errors.title && (
<p className="text-sm text-destructive">{errors.title}</p>
)}
</div>
<div className="space-y-2">
<Label htmlFor="language">Language</Label>
<Select value={language} onValueChange={setLanguage}>
<SelectTrigger id="language">
<SelectValue />
</SelectTrigger>
<SelectContent>
{LANGUAGES.map((lang) => (
<SelectItem key={lang} value={lang}>
{lang}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<Label htmlFor="description">Description</Label>
<Textarea
id="description"
placeholder="Optional description or notes..."
value={description}
onChange={(e) => setDescription(e.target.value)}
rows={2}
/>
</div>
<div className="space-y-2">
<Label htmlFor="code">Code *</Label>
<Textarea
id="code"
placeholder="Paste your code here..."
value={code}
onChange={(e) => setCode(e.target.value)}
rows={12}
className={`font-mono text-sm ${errors.code ? 'border-destructive ring-destructive' : ''}`}
/>
{errors.code && (
<p className="text-sm text-destructive">{errors.code}</p>
)}
</div>
</div>
<DialogFooter>
<Button variant="outline" onClick={() => onOpenChange(false)}>
Cancel
</Button>
<Button onClick={handleSave}>
{editingSnippet ? 'Update' : 'Create'} Snippet
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
)
}

View File

@@ -1 +1,7 @@
/* This is where custom CSS goes */
* {
font-family: 'Space Grotesk', sans-serif;
}
code, pre, textarea.font-mono {
font-family: 'JetBrains Mono', monospace;
}

51
src/lib/types.ts Normal file
View File

@@ -0,0 +1,51 @@
export interface Snippet {
id: string
title: string
description: string
language: string
code: string
createdAt: number
updatedAt: number
}
export const LANGUAGES = [
'JavaScript',
'TypeScript',
'Python',
'Java',
'C++',
'C#',
'Ruby',
'Go',
'Rust',
'PHP',
'Swift',
'Kotlin',
'HTML',
'CSS',
'SQL',
'Bash',
'Other'
] as const
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',
'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',
'C#': 'bg-purple-500/20 text-purple-300 border-purple-500/30',
'Ruby': 'bg-red-600/20 text-red-300 border-red-600/30',
'Go': 'bg-cyan-500/20 text-cyan-300 border-cyan-500/30',
'Rust': 'bg-orange-600/20 text-orange-300 border-orange-600/30',
'PHP': 'bg-indigo-500/20 text-indigo-300 border-indigo-500/30',
'Swift': 'bg-orange-500/20 text-orange-300 border-orange-500/30',
'Kotlin': 'bg-purple-600/20 text-purple-300 border-purple-600/30',
'HTML': 'bg-orange-400/20 text-orange-300 border-orange-400/30',
'CSS': 'bg-blue-600/20 text-blue-300 border-blue-600/30',
'SQL': 'bg-teal-500/20 text-teal-300 border-teal-500/30',
'Bash': 'bg-green-500/20 text-green-300 border-green-500/30',
'Other': 'bg-gray-500/20 text-gray-300 border-gray-500/30'
}

View File

@@ -32,38 +32,39 @@
---break---
*/
:root {
--radius: 0.625rem;
--background: oklch(1 0 0);
--foreground: oklch(0.145 0 0);
--card: oklch(1 0 0);
--card-foreground: oklch(0.145 0 0);
--popover: oklch(1 0 0);
--popover-foreground: oklch(0.145 0 0);
--primary: oklch(0.205 0 0);
--primary-foreground: oklch(0.985 0 0);
--secondary: oklch(0.97 0 0);
--secondary-foreground: oklch(0.205 0 0);
--muted: oklch(0.97 0 0);
--muted-foreground: oklch(0.556 0 0);
--accent: oklch(0.97 0 0);
--accent-foreground: oklch(0.205 0 0);
--radius: 0.5rem;
--background: oklch(0.08 0.01 265);
--foreground: oklch(0.95 0.01 265);
--card: oklch(0.15 0.01 265);
--card-foreground: oklch(0.98 0 0);
--popover: oklch(0.15 0.01 265);
--popover-foreground: oklch(0.98 0 0);
--primary: oklch(0.35 0.15 265);
--primary-foreground: oklch(0.98 0 0);
--secondary: oklch(0.25 0.01 265);
--secondary-foreground: oklch(0.95 0.01 265);
--muted: oklch(0.20 0.01 265);
--muted-foreground: oklch(0.65 0.01 265);
--accent: oklch(0.75 0.15 195);
--accent-foreground: oklch(0.15 0.01 265);
--destructive: oklch(0.577 0.245 27.325);
--border: oklch(0.922 0 0);
--input: oklch(0.922 0 0);
--ring: oklch(0.708 0 0);
--destructive-foreground: oklch(0.98 0 0);
--border: oklch(0.25 0.01 265);
--input: oklch(0.25 0.01 265);
--ring: oklch(0.75 0.15 195);
--chart-1: oklch(0.646 0.222 41.116);
--chart-2: oklch(0.6 0.118 184.704);
--chart-3: oklch(0.398 0.07 227.392);
--chart-4: oklch(0.828 0.189 84.429);
--chart-5: oklch(0.769 0.188 70.08);
--sidebar: oklch(0.985 0 0);
--sidebar-foreground: oklch(0.145 0 0);
--sidebar-primary: oklch(0.205 0 0);
--sidebar-primary-foreground: oklch(0.985 0 0);
--sidebar-accent: oklch(0.97 0 0);
--sidebar-accent-foreground: oklch(0.205 0 0);
--sidebar-border: oklch(0.922 0 0);
--sidebar-ring: oklch(0.708 0 0);
--sidebar: oklch(0.15 0.01 265);
--sidebar-foreground: oklch(0.98 0 0);
--sidebar-primary: oklch(0.35 0.15 265);
--sidebar-primary-foreground: oklch(0.98 0 0);
--sidebar-accent: oklch(0.25 0.01 265);
--sidebar-accent-foreground: oklch(0.95 0.01 265);
--sidebar-border: oklch(0.25 0.01 265);
--sidebar-ring: oklch(0.75 0.15 195);
}
/*

View File

@@ -1,6 +1,7 @@
import { createRoot } from 'react-dom/client'
import { ErrorBoundary } from "react-error-boundary";
import "@github/spark/spark"
import { Toaster } from '@/components/ui/sonner'
import App from './App.tsx'
import { ErrorFallback } from './ErrorFallback.tsx'
@@ -12,5 +13,6 @@ import "./index.css"
createRoot(document.getElementById('root')!).render(
<ErrorBoundary FallbackComponent={ErrorFallback}>
<App />
<Toaster />
</ErrorBoundary>
)