Generated by Spark: Fav Icon designer tool

This commit is contained in:
2026-01-16 02:45:22 +00:00
committed by GitHub
parent 0a8cacd92e
commit 13c8154122
3 changed files with 799 additions and 1 deletions

View File

@@ -6,7 +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 { Code, Database, Tree, PaintBrush, Download, Sparkle, Flask, BookOpen, Play, Wrench, Gear, Cube, FileText, ChartBar, Keyboard, FlowArrow, Faders, DeviceMobile } from '@phosphor-icons/react'
import { Code, Database, Tree, PaintBrush, Download, Sparkle, Flask, BookOpen, Play, Wrench, Gear, Cube, FileText, ChartBar, Keyboard, FlowArrow, Faders, DeviceMobile, Image } 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'
@@ -32,6 +32,7 @@ import { PWAInstallPrompt } from '@/components/PWAInstallPrompt'
import { PWAUpdatePrompt } from '@/components/PWAUpdatePrompt'
import { PWAStatusBar } from '@/components/PWAStatusBar'
import { PWASettings } from '@/components/PWASettings'
import { FaviconDesigner } from '@/components/FaviconDesigner'
import { useKeyboardShortcuts } from '@/hooks/use-keyboard-shortcuts'
import { generateNextJSProject, generatePrismaSchema, generateMUITheme, generatePlaywrightTests, generateStorybookStories, generateUnitTests, generateFlaskApp } from '@/lib/generators'
import { AIService } from '@/lib/ai-service'
@@ -99,6 +100,7 @@ const DEFAULT_FEATURE_TOGGLES: FeatureToggles = {
errorRepair: true,
documentation: true,
sassStyles: true,
faviconDesigner: true,
}
const DEFAULT_THEME: ThemeConfig = {
@@ -278,6 +280,12 @@ function App() {
description: 'Go to Styling',
action: () => setActiveTab('styling'),
}] : []),
...(safeFeatureToggles.faviconDesigner ? [{
key: '9',
ctrl: true,
description: 'Go to Favicon Designer',
action: () => setActiveTab('favicon'),
}] : []),
{
key: 'e',
ctrl: true,
@@ -673,6 +681,12 @@ Navigate to the backend directory and follow the setup instructions.
Sass Styles
</TabsTrigger>
)}
{safeFeatureToggles.faviconDesigner && (
<TabsTrigger value="favicon" className="gap-2">
<Image size={18} />
Favicon Designer
</TabsTrigger>
)}
</TabsList>
</div>
@@ -828,6 +842,12 @@ Navigate to the backend directory and follow the setup instructions.
<SassStylesShowcase />
</TabsContent>
)}
{safeFeatureToggles.faviconDesigner && (
<TabsContent value="favicon" className="h-full m-0">
<FaviconDesigner />
</TabsContent>
)}
</div>
</Tabs>

View File

@@ -0,0 +1,777 @@
import { useState, useRef, useEffect } from 'react'
import { useKV } from '@github/spark/hooks'
import { Button } from '@/components/ui/button'
import { Card } from '@/components/ui/card'
import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
import { Slider } from '@/components/ui/slider'
import { ScrollArea } from '@/components/ui/scroll-area'
import { Separator } from '@/components/ui/separator'
import { Badge } from '@/components/ui/badge'
import {
Plus,
Trash,
Download,
CircleNotch,
Square,
Triangle,
Star,
Heart,
Polygon,
TextT,
Image as ImageIcon,
ArrowCounterClockwise,
Copy,
FloppyDisk
} from '@phosphor-icons/react'
import { toast } from 'sonner'
interface FaviconElement {
id: string
type: 'circle' | 'square' | 'triangle' | 'star' | 'heart' | 'polygon' | 'text' | 'emoji'
x: number
y: number
width: number
height: number
color: string
rotation: number
text?: string
fontSize?: number
fontWeight?: string
emoji?: string
}
interface FaviconDesign {
id: string
name: string
size: number
backgroundColor: string
elements: FaviconElement[]
createdAt: number
updatedAt: number
}
const PRESET_SIZES = [16, 32, 48, 64, 128, 256, 512]
const ELEMENT_TYPES = [
{ value: 'circle', label: 'Circle', icon: CircleNotch },
{ value: 'square', label: 'Square', icon: Square },
{ value: 'triangle', label: 'Triangle', icon: Triangle },
{ value: 'star', label: 'Star', icon: Star },
{ value: 'heart', label: 'Heart', icon: Heart },
{ value: 'polygon', label: 'Polygon', icon: Polygon },
{ value: 'text', label: 'Text', icon: TextT },
{ value: 'emoji', label: 'Emoji', icon: ImageIcon },
]
const DEFAULT_DESIGN: FaviconDesign = {
id: 'default',
name: 'My Favicon',
size: 128,
backgroundColor: '#7c3aed',
elements: [
{
id: '1',
type: 'text',
x: 64,
y: 64,
width: 100,
height: 100,
color: '#ffffff',
rotation: 0,
text: 'CF',
fontSize: 48,
fontWeight: 'bold',
},
],
createdAt: Date.now(),
updatedAt: Date.now(),
}
export function FaviconDesigner() {
const [designs, setDesigns] = useKV<FaviconDesign[]>('favicon-designs', [DEFAULT_DESIGN])
const [activeDesignId, setActiveDesignId] = useState<string>(DEFAULT_DESIGN.id)
const [selectedElementId, setSelectedElementId] = useState<string | null>(null)
const canvasRef = useRef<HTMLCanvasElement>(null)
const safeDesigns = designs || [DEFAULT_DESIGN]
const activeDesign = safeDesigns.find((d) => d.id === activeDesignId) || DEFAULT_DESIGN
const selectedElement = activeDesign.elements.find((e) => e.id === selectedElementId)
useEffect(() => {
drawCanvas()
}, [activeDesign])
const drawCanvas = () => {
const canvas = canvasRef.current
if (!canvas) return
const ctx = canvas.getContext('2d')
if (!ctx) return
const size = activeDesign.size
canvas.width = size
canvas.height = size
ctx.fillStyle = activeDesign.backgroundColor
ctx.fillRect(0, 0, size, size)
activeDesign.elements.forEach((element) => {
ctx.save()
ctx.translate(element.x, element.y)
ctx.rotate((element.rotation * Math.PI) / 180)
ctx.fillStyle = element.color
switch (element.type) {
case 'circle':
ctx.beginPath()
ctx.arc(0, 0, element.width / 2, 0, Math.PI * 2)
ctx.fill()
break
case 'square':
ctx.fillRect(-element.width / 2, -element.height / 2, element.width, element.height)
break
case 'triangle':
ctx.beginPath()
ctx.moveTo(0, -element.height / 2)
ctx.lineTo(element.width / 2, element.height / 2)
ctx.lineTo(-element.width / 2, element.height / 2)
ctx.closePath()
ctx.fill()
break
case 'star':
drawStar(ctx, 0, 0, 5, element.width / 2, element.width / 4)
break
case 'heart':
drawHeart(ctx, 0, 0, element.width)
break
case 'polygon':
drawPolygon(ctx, 0, 0, 6, element.width / 2)
break
case 'text':
ctx.fillStyle = element.color
ctx.font = `${element.fontWeight || 'bold'} ${element.fontSize || 32}px sans-serif`
ctx.textAlign = 'center'
ctx.textBaseline = 'middle'
ctx.fillText(element.text || '', 0, 0)
break
case 'emoji':
ctx.font = `${element.fontSize || 32}px sans-serif`
ctx.textAlign = 'center'
ctx.textBaseline = 'middle'
ctx.fillText(element.emoji || '😀', 0, 0)
break
}
ctx.restore()
})
}
const drawStar = (ctx: CanvasRenderingContext2D, cx: number, cy: number, spikes: number, outerRadius: number, innerRadius: number) => {
let rot = (Math.PI / 2) * 3
let x = cx
let y = cy
const step = Math.PI / spikes
ctx.beginPath()
ctx.moveTo(cx, cy - outerRadius)
for (let i = 0; i < spikes; i++) {
x = cx + Math.cos(rot) * outerRadius
y = cy + Math.sin(rot) * outerRadius
ctx.lineTo(x, y)
rot += step
x = cx + Math.cos(rot) * innerRadius
y = cy + Math.sin(rot) * innerRadius
ctx.lineTo(x, y)
rot += step
}
ctx.lineTo(cx, cy - outerRadius)
ctx.closePath()
ctx.fill()
}
const drawHeart = (ctx: CanvasRenderingContext2D, x: number, y: number, size: number) => {
const topCurveHeight = size * 0.3
ctx.beginPath()
ctx.moveTo(x, y + topCurveHeight)
ctx.bezierCurveTo(x, y, x - size / 2, y - topCurveHeight, x - size / 2, y + topCurveHeight)
ctx.bezierCurveTo(x - size / 2, y + (size + topCurveHeight) / 2, x, y + (size + topCurveHeight) / 1.2, x, y + size)
ctx.bezierCurveTo(x, y + (size + topCurveHeight) / 1.2, x + size / 2, y + (size + topCurveHeight) / 2, x + size / 2, y + topCurveHeight)
ctx.bezierCurveTo(x + size / 2, y - topCurveHeight, x, y, x, y + topCurveHeight)
ctx.closePath()
ctx.fill()
}
const drawPolygon = (ctx: CanvasRenderingContext2D, x: number, y: number, sides: number, radius: number) => {
ctx.beginPath()
for (let i = 0; i < sides; i++) {
const angle = (i * 2 * Math.PI) / sides - Math.PI / 2
const px = x + radius * Math.cos(angle)
const py = y + radius * Math.sin(angle)
if (i === 0) ctx.moveTo(px, py)
else ctx.lineTo(px, py)
}
ctx.closePath()
ctx.fill()
}
const handleAddElement = (type: FaviconElement['type']) => {
const newElement: FaviconElement = {
id: `element-${Date.now()}`,
type,
x: activeDesign.size / 2,
y: activeDesign.size / 2,
width: type === 'text' || type === 'emoji' ? 100 : 40,
height: type === 'text' || type === 'emoji' ? 100 : 40,
color: '#ffffff',
rotation: 0,
...(type === 'text' && { text: 'A', fontSize: 32, fontWeight: 'bold' }),
...(type === 'emoji' && { emoji: '😀', fontSize: 40 }),
}
setDesigns((current) =>
(current || []).map((d) =>
d.id === activeDesignId
? { ...d, elements: [...d.elements, newElement], updatedAt: Date.now() }
: d
)
)
setSelectedElementId(newElement.id)
}
const handleUpdateElement = (updates: Partial<FaviconElement>) => {
if (!selectedElementId) return
setDesigns((current) =>
(current || []).map((d) =>
d.id === activeDesignId
? {
...d,
elements: d.elements.map((e) => (e.id === selectedElementId ? { ...e, ...updates } : e)),
updatedAt: Date.now(),
}
: d
)
)
}
const handleDeleteElement = (elementId: string) => {
setDesigns((current) =>
(current || []).map((d) =>
d.id === activeDesignId
? { ...d, elements: d.elements.filter((e) => e.id !== elementId), updatedAt: Date.now() }
: d
)
)
setSelectedElementId(null)
}
const handleUpdateDesign = (updates: Partial<FaviconDesign>) => {
setDesigns((current) =>
(current || []).map((d) => (d.id === activeDesignId ? { ...d, ...updates, updatedAt: Date.now() } : d))
)
}
const handleNewDesign = () => {
const newDesign: FaviconDesign = {
id: `design-${Date.now()}`,
name: `Favicon ${safeDesigns.length + 1}`,
size: 128,
backgroundColor: '#7c3aed',
elements: [],
createdAt: Date.now(),
updatedAt: Date.now(),
}
setDesigns((current) => [...(current || []), newDesign])
setActiveDesignId(newDesign.id)
setSelectedElementId(null)
}
const handleDuplicateDesign = () => {
const newDesign: FaviconDesign = {
...activeDesign,
id: `design-${Date.now()}`,
name: `${activeDesign.name} (Copy)`,
createdAt: Date.now(),
updatedAt: Date.now(),
}
setDesigns((current) => [...(current || []), newDesign])
setActiveDesignId(newDesign.id)
toast.success('Design duplicated')
}
const handleDeleteDesign = () => {
if (safeDesigns.length === 1) {
toast.error('Cannot delete the last design')
return
}
const filteredDesigns = safeDesigns.filter((d) => d.id !== activeDesignId)
setDesigns(filteredDesigns)
setActiveDesignId(filteredDesigns[0].id)
setSelectedElementId(null)
toast.success('Design deleted')
}
const handleExport = (format: 'png' | 'ico' | 'svg', size?: number) => {
const canvas = canvasRef.current
if (!canvas) return
if (format === 'png') {
const exportSize = size || activeDesign.size
const tempCanvas = document.createElement('canvas')
tempCanvas.width = exportSize
tempCanvas.height = exportSize
const ctx = tempCanvas.getContext('2d')
if (!ctx) return
ctx.drawImage(canvas, 0, 0, canvas.width, canvas.height, 0, 0, exportSize, exportSize)
tempCanvas.toBlob((blob) => {
if (!blob) return
const url = URL.createObjectURL(blob)
const a = document.createElement('a')
a.href = url
a.download = `${activeDesign.name}-${exportSize}x${exportSize}.png`
document.body.appendChild(a)
a.click()
document.body.removeChild(a)
URL.revokeObjectURL(url)
toast.success(`Exported as ${exportSize}x${exportSize} PNG`)
})
} else if (format === 'ico') {
canvas.toBlob((blob) => {
if (!blob) return
const url = URL.createObjectURL(blob)
const a = document.createElement('a')
a.href = url
a.download = `${activeDesign.name}.ico`
document.body.appendChild(a)
a.click()
document.body.removeChild(a)
URL.revokeObjectURL(url)
toast.success('Exported as ICO')
})
} else if (format === 'svg') {
const svg = generateSVG()
const blob = new Blob([svg], { type: 'image/svg+xml' })
const url = URL.createObjectURL(blob)
const a = document.createElement('a')
a.href = url
a.download = `${activeDesign.name}.svg`
document.body.appendChild(a)
a.click()
document.body.removeChild(a)
URL.revokeObjectURL(url)
toast.success('Exported as SVG')
}
}
const generateSVG = (): string => {
const size = activeDesign.size
let svg = `<svg xmlns="http://www.w3.org/2000/svg" width="${size}" height="${size}" viewBox="0 0 ${size} ${size}">`
svg += `<rect width="${size}" height="${size}" fill="${activeDesign.backgroundColor}"/>`
activeDesign.elements.forEach((element) => {
const transform = `translate(${element.x},${element.y}) rotate(${element.rotation})`
switch (element.type) {
case 'circle':
svg += `<circle cx="0" cy="0" r="${element.width / 2}" fill="${element.color}" transform="${transform}"/>`
break
case 'square':
svg += `<rect x="${-element.width / 2}" y="${-element.height / 2}" width="${element.width}" height="${element.height}" fill="${element.color}" transform="${transform}"/>`
break
case 'text':
svg += `<text x="0" y="0" fill="${element.color}" font-size="${element.fontSize}" font-weight="${element.fontWeight}" text-anchor="middle" dominant-baseline="middle" transform="${transform}">${element.text}</text>`
break
}
})
svg += '</svg>'
return svg
}
const handleExportAll = () => {
PRESET_SIZES.forEach((size) => {
setTimeout(() => handleExport('png', size), size * 10)
})
toast.success('Exporting all sizes...')
}
return (
<div className="h-full flex flex-col bg-background">
<div className="border-b border-border bg-card px-6 py-4">
<div className="flex items-center justify-between">
<div>
<h2 className="text-2xl font-bold">Favicon Designer</h2>
<p className="text-sm text-muted-foreground">
Create custom favicons for your web applications
</p>
</div>
<div className="flex gap-2">
<Button variant="outline" onClick={handleNewDesign}>
<Plus size={16} className="mr-2" />
New Design
</Button>
<Button variant="outline" onClick={handleDuplicateDesign}>
<Copy size={16} className="mr-2" />
Duplicate
</Button>
<Button variant="outline" onClick={handleDeleteDesign} disabled={safeDesigns.length === 1}>
<Trash size={16} className="mr-2" />
Delete
</Button>
</div>
</div>
</div>
<div className="flex-1 overflow-hidden">
<div className="h-full grid grid-cols-[1fr_400px]">
<div className="border-r border-border p-6 flex flex-col items-center justify-center bg-muted/20">
<Card className="p-8 mb-6">
<div className="flex flex-col items-center gap-4">
<div className="relative">
<canvas
ref={canvasRef}
className="border-2 border-border rounded-lg shadow-xl"
style={{
width: '400px',
height: '400px',
imageRendering: 'pixelated',
}}
/>
<Badge className="absolute -top-3 -right-3">
{activeDesign.size}x{activeDesign.size}
</Badge>
</div>
<div className="flex gap-2 flex-wrap justify-center">
{PRESET_SIZES.map((size) => (
<div
key={size}
className="flex flex-col items-center gap-1 p-2 rounded border border-border hover:bg-accent/50 cursor-pointer"
onClick={() => handleExport('png', size)}
title={`Export ${size}x${size}`}
>
<canvas
width={size}
height={size}
ref={(canvas) => {
if (!canvas) return
const ctx = canvas.getContext('2d')
if (!ctx || !canvasRef.current) return
ctx.drawImage(canvasRef.current, 0, 0, size, size)
}}
className="border border-border rounded"
style={{ width: `${size / 2}px`, height: `${size / 2}px` }}
/>
<span className="text-xs text-muted-foreground">{size}px</span>
</div>
))}
</div>
</div>
</Card>
<div className="flex gap-2">
<Button onClick={() => handleExport('png')}>
<Download size={16} className="mr-2" />
Export PNG
</Button>
<Button onClick={() => handleExport('svg')} variant="outline">
<Download size={16} className="mr-2" />
Export SVG
</Button>
<Button onClick={handleExportAll} variant="outline">
<Download size={16} className="mr-2" />
Export All Sizes
</Button>
</div>
</div>
<ScrollArea className="h-full">
<div className="p-6 space-y-6">
<div>
<Label>Design Name</Label>
<Input
value={activeDesign.name}
onChange={(e) => handleUpdateDesign({ name: e.target.value })}
placeholder="My Favicon"
/>
</div>
<div>
<Label>Select Design</Label>
<Select value={activeDesignId} onValueChange={setActiveDesignId}>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
{safeDesigns.map((design) => (
<SelectItem key={design.id} value={design.id}>
{design.name}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div>
<Label>Canvas Size</Label>
<Select
value={String(activeDesign.size)}
onValueChange={(value) => handleUpdateDesign({ size: Number(value) })}
>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
{PRESET_SIZES.map((size) => (
<SelectItem key={size} value={String(size)}>
{size}x{size}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div>
<Label>Background Color</Label>
<div className="flex gap-2">
<Input
type="color"
value={activeDesign.backgroundColor}
onChange={(e) => handleUpdateDesign({ backgroundColor: e.target.value })}
className="w-20 h-10"
/>
<Input
value={activeDesign.backgroundColor}
onChange={(e) => handleUpdateDesign({ backgroundColor: e.target.value })}
placeholder="#7c3aed"
/>
</div>
</div>
<Separator />
<div>
<Label className="mb-3 block">Add Elements</Label>
<div className="grid grid-cols-4 gap-2">
{ELEMENT_TYPES.map(({ value, label, icon: Icon }) => (
<Button
key={value}
variant="outline"
size="sm"
onClick={() => handleAddElement(value as FaviconElement['type'])}
className="flex flex-col gap-1 h-auto py-2"
>
<Icon size={20} />
<span className="text-xs">{label}</span>
</Button>
))}
</div>
</div>
<Separator />
<div>
<Label className="mb-3 block">
Elements ({activeDesign.elements.length})
</Label>
<ScrollArea className="h-40">
<div className="space-y-2">
{activeDesign.elements.map((element) => (
<div
key={element.id}
className={`flex items-center justify-between p-2 rounded border cursor-pointer ${
selectedElementId === element.id
? 'border-primary bg-primary/10'
: 'border-border hover:bg-accent/50'
}`}
onClick={() => setSelectedElementId(element.id)}
>
<div className="flex items-center gap-2">
{ELEMENT_TYPES.find((t) => t.value === element.type)?.icon && (
<span>
{(() => {
const Icon = ELEMENT_TYPES.find((t) => t.value === element.type)!.icon
return <Icon size={16} />
})()}
</span>
)}
<span className="text-sm capitalize">{element.type}</span>
{element.text && <span className="text-xs text-muted-foreground">"{element.text}"</span>}
{element.emoji && <span className="text-xs">{element.emoji}</span>}
</div>
<Button
size="sm"
variant="ghost"
onClick={(e) => {
e.stopPropagation()
handleDeleteElement(element.id)
}}
>
<Trash size={14} />
</Button>
</div>
))}
{activeDesign.elements.length === 0 && (
<p className="text-sm text-muted-foreground text-center py-4">
No elements yet. Add some!
</p>
)}
</div>
</ScrollArea>
</div>
{selectedElement && (
<>
<Separator />
<div className="space-y-4">
<Label className="text-base font-semibold">Edit Element</Label>
{(selectedElement.type === 'text' || selectedElement.type === 'emoji') && (
<>
{selectedElement.type === 'text' && (
<div>
<Label>Text</Label>
<Input
value={selectedElement.text || ''}
onChange={(e) => handleUpdateElement({ text: e.target.value })}
placeholder="Enter text"
/>
</div>
)}
{selectedElement.type === 'emoji' && (
<div>
<Label>Emoji</Label>
<Input
value={selectedElement.emoji || ''}
onChange={(e) => handleUpdateElement({ emoji: e.target.value })}
placeholder="😀"
/>
</div>
)}
<div>
<Label>Font Size: {selectedElement.fontSize}px</Label>
<Slider
value={[selectedElement.fontSize || 32]}
onValueChange={([value]) => handleUpdateElement({ fontSize: value })}
min={12}
max={200}
step={1}
/>
</div>
{selectedElement.type === 'text' && (
<div>
<Label>Font Weight</Label>
<Select
value={selectedElement.fontWeight || 'bold'}
onValueChange={(value) => handleUpdateElement({ fontWeight: value })}
>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="normal">Normal</SelectItem>
<SelectItem value="bold">Bold</SelectItem>
<SelectItem value="lighter">Light</SelectItem>
</SelectContent>
</Select>
</div>
)}
</>
)}
{selectedElement.type !== 'text' && selectedElement.type !== 'emoji' && (
<>
<div>
<Label>Width: {selectedElement.width}px</Label>
<Slider
value={[selectedElement.width]}
onValueChange={([value]) => handleUpdateElement({ width: value })}
min={10}
max={activeDesign.size}
step={1}
/>
</div>
<div>
<Label>Height: {selectedElement.height}px</Label>
<Slider
value={[selectedElement.height]}
onValueChange={([value]) => handleUpdateElement({ height: value })}
min={10}
max={activeDesign.size}
step={1}
/>
</div>
</>
)}
<div>
<Label>X Position: {selectedElement.x}px</Label>
<Slider
value={[selectedElement.x]}
onValueChange={([value]) => handleUpdateElement({ x: value })}
min={0}
max={activeDesign.size}
step={1}
/>
</div>
<div>
<Label>Y Position: {selectedElement.y}px</Label>
<Slider
value={[selectedElement.y]}
onValueChange={([value]) => handleUpdateElement({ y: value })}
min={0}
max={activeDesign.size}
step={1}
/>
</div>
<div>
<Label>Rotation: {selectedElement.rotation}°</Label>
<Slider
value={[selectedElement.rotation]}
onValueChange={([value]) => handleUpdateElement({ rotation: value })}
min={0}
max={360}
step={1}
/>
</div>
<div>
<Label>Color</Label>
<div className="flex gap-2">
<Input
type="color"
value={selectedElement.color}
onChange={(e) => handleUpdateElement({ color: e.target.value })}
className="w-20 h-10"
/>
<Input
value={selectedElement.color}
onChange={(e) => handleUpdateElement({ color: e.target.value })}
placeholder="#ffffff"
/>
</div>
</div>
</div>
</>
)}
</div>
</ScrollArea>
</div>
</div>
</div>
)
}

View File

@@ -274,6 +274,7 @@ export interface FeatureToggles {
errorRepair: boolean
documentation: boolean
sassStyles: boolean
faviconDesigner: boolean
}
export interface Project {