feat: enhance UI components with improved styling and interactions

This commit is contained in:
2026-01-07 16:26:16 +02:00
parent bb5a9e0715
commit 866929c358
5 changed files with 274 additions and 204 deletions
+50 -42
View File
@@ -19,46 +19,29 @@ const Inspector: React.FC = () => {
const selectedElement = snap.elements.find(el => el.id === selectedElementId);
return (
<div className="w-72 bg-neutral-800 border-l border-neutral-700 overflow-y-auto">
<div className="p-4">
<div className="w-80 bg-[#09090b] border-l border-white/5 overflow-y-auto h-full">
<div className="p-6">
{selectedElement ? (
<>
{/* Element header */}
<div className="flex items-center justify-between mb-4">
<h3 className="text-white font-medium capitalize">
{selectedElement.type} Element
<div className="space-y-6">
{/* Header with Title and Element Actions */}
<div className="flex items-center justify-between">
<h3 className="text-white font-semibold text-sm uppercase tracking-wider">
{selectedElement.type}
</h3>
<div className="flex gap-1">
<button
onClick={() => moveElementDown(selectedElement.id)}
className="p-1.5 hover:bg-neutral-700 rounded text-neutral-400 hover:text-white"
title="Move Back"
>
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 14l-7 7m0 0l-7-7m7 7V3" />
</svg>
</button>
<button
onClick={() => moveElementUp(selectedElement.id)}
className="p-1.5 hover:bg-neutral-700 rounded text-neutral-400 hover:text-white"
title="Move Front"
>
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 10l7-7m0 0l7 7m-7-7v18" />
</svg>
</button>
<div className="flex items-center gap-1">
<button
onClick={() => duplicateElement(selectedElement.id)}
className="p-1.5 hover:bg-neutral-700 rounded text-neutral-400 hover:text-white"
className="p-1.5 hover:bg-white/10 rounded-md text-neutral-400 hover:text-white transition-colors"
title="Duplicate (⌘D)"
>
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z" />
</svg>
</button>
<div className="w-px h-3 bg-white/10 mx-1" />
<button
onClick={() => deleteElement(selectedElement.id)}
className="p-1.5 hover:bg-red-600 rounded text-neutral-400 hover:text-white"
className="p-1.5 hover:bg-red-500/10 hover:text-red-500 rounded-md text-neutral-400 transition-colors"
title="Delete (⌫)"
>
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
@@ -68,26 +51,51 @@ const Inspector: React.FC = () => {
</div>
</div>
{/* Layer Controls */}
<div className="grid grid-cols-2 gap-2">
<button
onClick={() => moveElementDown(selectedElement.id)}
className="flex items-center justify-center gap-2 px-3 py-2 bg-white/5 hover:bg-white/10 rounded-lg text-xs font-medium text-neutral-400 hover:text-white transition-colors border border-white/5"
>
<svg className="w-3 h-3" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 14l-7 7m0 0l-7-7m7 7V3" />
</svg>
Send Backward
</button>
<button
onClick={() => moveElementUp(selectedElement.id)}
className="flex items-center justify-center gap-2 px-3 py-2 bg-white/5 hover:bg-white/10 rounded-lg text-xs font-medium text-neutral-400 hover:text-white transition-colors border border-white/5"
>
<svg className="w-3 h-3" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 10l7-7m0 0l7 7m-7-7v18" />
</svg>
Bring Forward
</button>
</div>
<div className="h-px bg-white/5 w-full -mx-2" />
{/* Element-specific inspector */}
{selectedElement.type === 'code' && (
<CodeInspector element={selectedElement as CodeElement} />
)}
{selectedElement.type === 'text' && (
<TextInspector element={selectedElement as TextElement} />
)}
{selectedElement.type === 'arrow' && (
<ArrowInspector element={selectedElement as ArrowElement} />
)}
</>
<div className="inspector-content">
{selectedElement.type === 'code' && (
<CodeInspector element={selectedElement as CodeElement} />
)}
{selectedElement.type === 'text' && (
<TextInspector element={selectedElement as TextElement} />
)}
{selectedElement.type === 'arrow' && (
<ArrowInspector element={selectedElement as ArrowElement} />
)}
</div>
</div>
) : (
<>
<h3 className="text-white font-medium mb-4">Canvas Settings</h3>
<div className="space-y-6">
<h3 className="text-white font-semibold text-sm uppercase tracking-wider">Canvas Settings</h3>
<BackgroundPanel />
</>
</div>
)}
</div>
</div>
);
};
export default Inspector;
+61 -31
View File
@@ -5,24 +5,57 @@ const Toolbar: React.FC = () => {
const { tool, setTool, showGrid, setShowGrid, zoom, setZoom } = useCanvasStore();
const tools = [
{ id: 'select', icon: '↖', label: 'Select (V)' },
{ id: 'code', icon: '{ }', label: 'Code Block (C)' },
{ id: 'text', icon: 'T', label: 'Text (T)' },
{ id: 'arrow', icon: '→', label: 'Arrow (A)' },
{
id: 'select',
label: 'Select (V)',
icon: (
<svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 15l-2 5L9 9l11 4-5 2zm0 0l5 5M7.188 2.239l.777 2.897M5.136 7.965l-2.898-.777M13.95 4.05l-2.122 2.122m-5.657 5.656l-2.12 2.122" />
</svg>
)
},
{
id: 'code',
label: 'Code Block (C)',
icon: (
<svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M10 20l4-16m4 4l4 4-4 4M6 16l-4-4 4-4" />
</svg>
)
},
{
id: 'text',
label: 'Text (T)',
icon: (
<svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 6h16M4 12h16M4 18h7" />
</svg>
)
},
{
id: 'arrow',
label: 'Arrow (A)',
icon: (
<svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M17 8l4 4m0 0l-4 4m4-4H3" />
</svg>
)
},
] as const;
return (
<div className="w-14 bg-neutral-800 border-r border-neutral-700 flex flex-col items-center py-4 gap-2">
{/* Tools */}
<div className="flex flex-col gap-1">
<div className="absolute bottom-6 left-1/2 -translate-x-1/2 flex items-center gap-2 p-2 rounded-2xl bg-neutral-900/90 backdrop-blur-xl border border-white/10 shadow-2xl shadow-black/50 z-50">
{/* Tools Group */}
<div className="flex items-center gap-1">
{tools.map(({ id, icon, label }) => (
<button
key={id}
onClick={() => setTool(id)}
className={`w-10 h-10 rounded flex items-center justify-center text-lg transition-colors ${
className={`w-10 h-10 rounded-xl flex items-center justify-center transition-all duration-200 ${
tool === id
? 'bg-blue-600 text-white'
: 'text-neutral-400 hover:bg-neutral-700 hover:text-white'
? 'bg-blue-600 text-white shadow-lg shadow-blue-500/25 scale-100 ring-1 ring-blue-500/50'
: 'text-neutral-400 hover:text-white hover:bg-white/5 active:scale-95'
}`}
title={label}
>
@@ -31,15 +64,15 @@ const Toolbar: React.FC = () => {
))}
</div>
<div className="h-px w-8 bg-neutral-600 my-2" />
<div className="w-px h-6 bg-white/10 mx-2" />
{/* Grid toggle */}
{/* Grid Toggle */}
<button
onClick={() => setShowGrid(!showGrid)}
className={`w-10 h-10 rounded flex items-center justify-center transition-colors ${
className={`w-10 h-10 rounded-xl flex items-center justify-center transition-all duration-200 ${
showGrid
? 'bg-neutral-600 text-white'
: 'text-neutral-400 hover:bg-neutral-700 hover:text-white'
? 'bg-white/10 text-white ring-1 ring-white/20'
: 'text-neutral-400 hover:text-white hover:bg-white/5 active:scale-95'
}`}
title="Toggle Grid (⌘;)"
>
@@ -48,30 +81,27 @@ const Toolbar: React.FC = () => {
</svg>
</button>
{/* Spacer */}
<div className="flex-1" />
{/* Zoom controls */}
<div className="flex flex-col gap-1">
{/* Zoom Controls */}
<div className="flex items-center gap-1 bg-white/5 rounded-xl p-1 ml-1">
<button
onClick={() => setZoom(zoom + 0.1)}
className="w-10 h-10 rounded flex items-center justify-center text-neutral-400 hover:bg-neutral-700 hover:text-white transition-colors"
title="Zoom In (⌘+)"
onClick={() => setZoom(zoom - 0.1)}
className="w-8 h-8 rounded-lg flex items-center justify-center text-neutral-400 hover:text-white hover:bg-white/10 transition-colors"
title="Zoom Out (⌘-)"
>
<svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0zM10 7v6m3-3H7" />
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M20 12H4" />
</svg>
</button>
<div className="text-xs text-neutral-400 text-center py-1">
<div className="w-12 text-xs font-medium text-neutral-300 text-center select-none tabular-nums">
{Math.round(zoom * 100)}%
</div>
<button
onClick={() => setZoom(zoom - 0.1)}
className="w-10 h-10 rounded flex items-center justify-center text-neutral-400 hover:bg-neutral-700 hover:text-white transition-colors"
title="Zoom Out (⌘-)"
onClick={() => setZoom(zoom + 0.1)}
className="w-8 h-8 rounded-lg flex items-center justify-center text-neutral-400 hover:text-white hover:bg-white/10 transition-colors"
title="Zoom In (⌘+)"
>
<svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0zM13 10H7" />
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 4v16m8-8H4" />
</svg>
</button>
</div>
+73 -64
View File
@@ -96,13 +96,24 @@ const TopBar: React.FC<TopBarProps> = ({ stageRef }) => {
};
return (
<div className="h-14 bg-neutral-800 border-b border-neutral-700 flex items-center justify-between px-4">
{/* Left section */}
<div className="h-16 bg-[#09090b]/80 backdrop-blur-md border-b border-white/5 flex items-center justify-between px-6 z-40 fixed top-0 w-full">
{/* Left section: Logo & Actions */}
<div className="flex items-center gap-4">
<div className="flex items-center gap-2">
<div className="flex items-center gap-2 pr-4 border-r border-white/5">
<div className="w-8 h-8 rounded-xl bg-gradient-to-br from-blue-600 to-blue-700 shadow-lg shadow-blue-900/20 flex items-center justify-center">
<svg className="w-5 h-5 text-white" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2.5} d="M13 10V3L4 14h7v7l9-11h-7z" />
</svg>
</div>
<span className="font-bold text-lg tracking-tight bg-clip-text text-transparent bg-gradient-to-r from-white to-white/60">
YvCode
</span>
</div>
<div className="flex items-center gap-1">
<button
onClick={handleNewSnap}
className="p-2 hover:bg-neutral-700 rounded text-neutral-300 hover:text-white transition-colors"
className="p-2 rounded-lg text-neutral-400 hover:text-white hover:bg-white/5 transition-all active:scale-95"
title="New (⌘N)"
>
<svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
@@ -111,7 +122,7 @@ const TopBar: React.FC<TopBarProps> = ({ stageRef }) => {
</button>
<button
onClick={handleImportJSON}
className="p-2 hover:bg-neutral-700 rounded text-neutral-300 hover:text-white transition-colors"
className="p-2 rounded-lg text-neutral-400 hover:text-white hover:bg-white/5 transition-all active:scale-95"
title="Open"
>
<svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
@@ -119,95 +130,93 @@ const TopBar: React.FC<TopBarProps> = ({ stageRef }) => {
</svg>
</button>
</div>
<div className="h-6 w-px bg-neutral-600" />
<div className="flex items-center gap-2">
</div>
{/* Center section: Title & Tools */}
<div className="absolute left-1/2 -translate-x-1/2 flex items-center">
<div className="flex flex-col items-center">
<input
type="text"
value={snap.meta.title}
onChange={(e) => updateMeta({ title: e.target.value })}
className="bg-transparent text-sm font-medium text-center text-neutral-200 focus:text-white px-2 py-1 outline-none rounded hover:bg-white/5 focus:bg-white/10 transition-colors placeholder-neutral-600 w-48"
placeholder="Untitled Project"
/>
<div className="flex items-center gap-2 mt-0.5">
<span className="w-1.5 h-1.5 rounded-full bg-neutral-600" />
<select
value={snap.meta.aspect}
onChange={handleAspectChange}
className="bg-transparent text-[10px] items-center uppercase tracking-wider font-semibold text-neutral-500 hover:text-neutral-300 outline-none cursor-pointer appearance-none text-center"
>
{ASPECT_RATIOS.map((ratio) => (
<option key={ratio.name} value={ratio.name}>
{ratio.name}
</option>
))}
</select>
</div>
</div>
</div>
{/* Right section: History & Export */}
<div className="flex items-center gap-3">
<div className="flex items-center gap-0.5 p-1 bg-white/5 rounded-lg border border-white/5">
<button
onClick={undo}
disabled={history.past.length === 0}
className="p-2 hover:bg-neutral-700 rounded text-neutral-300 hover:text-white transition-colors disabled:opacity-30"
className="p-1.5 rounded text-neutral-400 hover:text-white hover:bg-white/10 transition-colors disabled:opacity-30 disabled:hover:bg-transparent"
title="Undo (⌘Z)"
>
<svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M3 10h10a8 8 0 018 8v2M3 10l6 6m-6-6l6-6" />
</svg>
</button>
<button
onClick={redo}
disabled={history.future.length === 0}
className="p-2 hover:bg-neutral-700 rounded text-neutral-300 hover:text-white transition-colors disabled:opacity-30"
className="p-1.5 rounded text-neutral-400 hover:text-white hover:bg-white/10 transition-colors disabled:opacity-30 disabled:hover:bg-transparent"
title="Redo (⇧⌘Z)"
>
<svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M21 10h-10a8 8 0 00-8 8v2M21 10l-6 6m6-6l-6-6" />
</svg>
</button>
</div>
</div>
{/* Center section */}
<div className="flex items-center gap-4">
<input
type="text"
value={snap.meta.title}
onChange={(e) => updateMeta({ title: e.target.value })}
className="bg-transparent text-white text-center px-2 py-1 border-b border-transparent hover:border-neutral-600 focus:border-blue-500 outline-none"
/>
<select
value={snap.meta.aspect}
onChange={handleAspectChange}
className="bg-neutral-700 text-white px-3 py-1.5 rounded text-sm"
>
{ASPECT_RATIOS.map((ratio) => (
<option key={ratio.name} value={ratio.name}>
{ratio.name}
</option>
))}
</select>
</div>
<div className="h-6 w-px bg-white/10" />
{/* Right section */}
<div className="flex items-center gap-2">
<button
onClick={handleExportJSON}
className="px-3 py-1.5 text-sm bg-neutral-700 hover:bg-neutral-600 rounded text-white transition-colors"
>
Save JSON
</button>
<div className="relative group">
<button
className="px-4 py-1.5 text-sm bg-blue-600 hover:bg-blue-500 rounded text-white font-medium transition-colors"
className="flex items-center gap-2 px-4 py-2 bg-white text-black hover:bg-blue-50 text-sm font-semibold rounded-lg shadow-lg shadow-white/5 transition-all active:scale-95"
>
Export
<span>Export</span>
<svg className="w-4 h-4 text-neutral-600" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
</svg>
</button>
<div className="absolute right-0 top-full mt-1 bg-neutral-800 border border-neutral-700 rounded shadow-xl opacity-0 invisible group-hover:opacity-100 group-hover:visible transition-all z-50">
<button
onClick={() => handleExportImage('png', 1)}
className="block w-full px-4 py-2 text-sm text-left text-white hover:bg-neutral-700"
>
PNG (1x)
</button>
<div className="absolute right-0 top-full mt-2 w-48 bg-[#09090b] border border-white/10 rounded-xl shadow-2xl p-1 opacity-0 invisible group-hover:opacity-100 group-hover:visible transition-all z-50 transform origin-top-right">
<div className="px-3 py-2 text-xs font-semibold text-neutral-500 uppercase tracking-wider">Format</div>
<button
onClick={() => handleExportImage('png', 2)}
className="block w-full px-4 py-2 text-sm text-left text-white hover:bg-neutral-700"
className="w-full text-left px-3 py-2 text-sm text-neutral-300 hover:text-white hover:bg-white/10 rounded-lg transition-colors flex justify-between group/item"
>
PNG (2x)
<span>PNG Image</span>
<span className="bg-white/10 px-1.5 py-0.5 rounded text-[10px] text-neutral-400 group-hover/item:text-white">2x</span>
</button>
<button
onClick={() => handleExportImage('png', 3)}
className="block w-full px-4 py-2 text-sm text-left text-white hover:bg-neutral-700"
>
PNG (3x)
</button>
<div className="border-t border-neutral-700" />
<button
onClick={() => handleExportImage('jpeg', 2)}
className="block w-full px-4 py-2 text-sm text-left text-white hover:bg-neutral-700"
className="w-full text-left px-3 py-2 text-sm text-neutral-300 hover:text-white hover:bg-white/10 rounded-lg transition-colors"
>
JPEG (2x)
JPEG Image
</button>
<div className="h-px bg-white/5 my-1" />
<button
onClick={handleExportJSON}
className="w-full text-left px-3 py-2 text-sm text-neutral-300 hover:text-white hover:bg-white/10 rounded-lg transition-colors"
>
Save Project JSON
</button>
</div>
</div>
+26 -7
View File
@@ -26,9 +26,10 @@ const Arrow: React.FC<ArrowProps> = ({ element, isSelected, onSelect, onChange }
onChange({ points: newPoints });
};
// Arrow head pointer
const pointerLength = props.head === 'none' ? 0 : props.thickness * 4;
const pointerWidth = props.head === 'none' ? 0 : props.thickness * 3;
// Modern arrow head calculations
// Make the head slightly sleeker
const pointerLength = props.head === 'none' ? 0 : Math.max(props.thickness * 3, 12);
const pointerWidth = props.head === 'none' ? 0 : Math.max(props.thickness * 2.5, 12);
return (
<Group>
@@ -40,9 +41,14 @@ const Arrow: React.FC<ArrowProps> = ({ element, isSelected, onSelect, onChange }
fill={props.head === 'filled' ? props.color : 'transparent'}
pointerLength={pointerLength}
pointerWidth={pointerWidth}
tension={props.style === 'curved' ? 0.5 : 0}
tension={props.style === 'curved' ? 0.4 : 0}
lineCap="round"
lineJoin="round"
// Add subtle glow/shadow for modern feel
shadowColor={props.color}
shadowBlur={8}
shadowOpacity={0.2}
shadowOffset={{ x: 0, y: 0 }}
onClick={onSelect}
onTap={onSelect}
hitStrokeWidth={20}
@@ -54,13 +60,26 @@ const Arrow: React.FC<ArrowProps> = ({ element, isSelected, onSelect, onChange }
key={index}
x={point.x}
y={point.y}
radius={8}
fill="#3b82f6"
stroke="#ffffff"
radius={5}
fill="#ffffff"
stroke="#3b82f6"
strokeWidth={2}
shadowColor="rgba(0,0,0,0.15)"
shadowBlur={4}
shadowOffset={{ x: 0, y: 1 }}
draggable={!element.locked}
onDragMove={(e) => handlePointDrag(index, e)}
onDragEnd={(e) => handlePointDrag(index, e)}
onMouseEnter={(e) => {
const container = e.target.getStage()?.container();
if (container) container.style.cursor = 'grab';
e.target.scale({ x: 1.5, y: 1.5 });
}}
onMouseLeave={(e) => {
const container = e.target.getStage()?.container();
if (container) container.style.cursor = 'default';
e.target.scale({ x: 1, y: 1 });
}}
/>
))}
</Group>
+64 -60
View File
@@ -19,27 +19,27 @@ const BackgroundPanel: React.FC = () => {
const { background } = snap;
return (
<div className="space-y-4">
<div className="space-y-6">
{/* Background type */}
<div>
<label className="block text-sm text-neutral-400 mb-2">Type</label>
<div className="flex gap-2">
<label className="block text-xs font-medium text-neutral-500 uppercase tracking-wider mb-3">Type</label>
<div className="flex gap-2 p-1 bg-white/5 rounded-lg border border-white/5">
<button
onClick={() => setBackground({ type: 'solid' })}
className={`flex-1 py-2 rounded text-sm ${
className={`flex-1 py-1.5 rounded-md text-xs font-medium transition-all ${
background.type === 'solid'
? 'bg-blue-600 text-white'
: 'bg-neutral-700 text-neutral-300 hover:bg-neutral-600'
? 'bg-neutral-700 text-white shadow-sm'
: 'text-neutral-400 hover:text-white hover:bg-white/5'
}`}
>
Solid
</button>
<button
onClick={() => setBackground({ type: 'gradient' })}
className={`flex-1 py-2 rounded text-sm ${
className={`flex-1 py-1.5 rounded-md text-xs font-medium transition-all ${
background.type === 'gradient'
? 'bg-blue-600 text-white'
: 'bg-neutral-700 text-neutral-300 hover:bg-neutral-600'
? 'bg-neutral-700 text-white shadow-sm'
: 'text-neutral-400 hover:text-white hover:bg-white/5'
}`}
>
Gradient
@@ -49,26 +49,28 @@ const BackgroundPanel: React.FC = () => {
{background.type === 'solid' ? (
<div>
<label className="block text-sm text-neutral-400 mb-2">Color</label>
<div className="flex gap-2 items-center">
<input
type="color"
value={background.solid.color}
onChange={(e) => setBackground({ solid: { color: e.target.value } })}
className="w-10 h-10 rounded cursor-pointer bg-transparent"
/>
<label className="block text-xs font-medium text-neutral-500 uppercase tracking-wider mb-2">Color</label>
<div className="flex gap-2 items-center p-2 bg-white/5 rounded-lg border border-white/5">
<div className="w-8 h-8 rounded overflow-hidden relative border border-white/10 shrink-0">
<input
type="color"
value={background.solid.color}
onChange={(e) => setBackground({ solid: { color: e.target.value } })}
className="absolute inset-[-4px] w-[200%] h-[200%] cursor-pointer p-0 m-0 border-none"
/>
</div>
<input
type="text"
value={background.solid.color}
onChange={(e) => setBackground({ solid: { color: e.target.value } })}
className="flex-1 bg-neutral-700 text-white px-3 py-2 rounded text-sm"
className="flex-1 bg-transparent text-white text-sm focus:outline-none font-mono"
/>
</div>
</div>
) : (
<>
<div>
<label className="block text-sm text-neutral-400 mb-2">Presets</label>
<label className="block text-xs font-medium text-neutral-500 uppercase tracking-wider mb-3">Presets</label>
<div className="grid grid-cols-5 gap-2">
{GRADIENT_PRESETS.map((preset, i) => (
<button
@@ -76,7 +78,7 @@ const BackgroundPanel: React.FC = () => {
onClick={() => setBackground({
gradient: { ...background.gradient, from: preset.from, to: preset.to }
})}
className="w-10 h-10 rounded border border-neutral-600 hover:border-blue-500 transition-colors"
className="w-full aspect-square rounded-lg border border-white/10 hover:border-white/40 transition-all hover:scale-105 shadow-sm"
style={{
background: `linear-gradient(135deg, ${preset.from}, ${preset.to})`
}}
@@ -88,53 +90,55 @@ const BackgroundPanel: React.FC = () => {
<div className="grid grid-cols-2 gap-3">
<div>
<label className="block text-sm text-neutral-400 mb-2">From</label>
<div className="flex gap-2 items-center">
<input
type="color"
value={background.gradient.from}
onChange={(e) => setBackground({
gradient: { ...background.gradient, from: e.target.value }
})}
className="w-8 h-8 rounded cursor-pointer bg-transparent"
/>
<input
type="text"
value={background.gradient.from}
onChange={(e) => setBackground({
gradient: { ...background.gradient, from: e.target.value }
})}
className="flex-1 bg-neutral-700 text-white px-2 py-1.5 rounded text-xs"
/>
<label className="block text-xs font-medium text-neutral-500 uppercase tracking-wider mb-2">From</label>
<div className="flex gap-2 items-center p-2 bg-white/5 rounded-lg border border-white/5">
<div className="w-6 h-6 rounded overflow-hidden relative border border-white/10 shrink-0">
<input
type="color"
value={background.gradient.from}
onChange={(e) => setBackground({
gradient: { ...background.gradient, from: e.target.value }
})}
className="absolute inset-[-4px] w-[200%] h-[200%] cursor-pointer"
/>
</div>
<input
type="text"
value={background.gradient.from}
onChange={(e) => setBackground({
gradient: { ...background.gradient, from: e.target.value }
})}
className="w-full bg-transparent text-white text-xs focus:outline-none font-mono"
/>
</div>
</div>
<div>
<label className="block text-sm text-neutral-400 mb-2">To</label>
<div className="flex gap-2 items-center">
<input
type="color"
value={background.gradient.to}
onChange={(e) => setBackground({
gradient: { ...background.gradient, to: e.target.value }
})}
className="w-8 h-8 rounded cursor-pointer bg-transparent"
/>
<input
type="text"
value={background.gradient.to}
onChange={(e) => setBackground({
gradient: { ...background.gradient, to: e.target.value }
})}
className="flex-1 bg-neutral-700 text-white px-2 py-1.5 rounded text-xs"
/>
<label className="block text-xs font-medium text-neutral-500 uppercase tracking-wider mb-2">To</label>
<div className="flex gap-2 items-center p-2 bg-white/5 rounded-lg border border-white/5">
<div className="w-6 h-6 rounded overflow-hidden relative border border-white/10 shrink-0">
<input
type="color"
value={background.gradient.to}
onChange={(e) => setBackground({
gradient: { ...background.gradient, from: e.target.value }
})}
className="absolute inset-[-4px] w-[200%] h-[200%] cursor-pointer"
/>
</div>
<input
type="text"
value={background.gradient.to}
onChange={(e) => setBackground({
gradient: { ...background.gradient, from: e.target.value }
})}
className="w-full bg-transparent text-white text-xs focus:outline-none font-mono"
/>
</div>
</div>
</div>
<div>
<label className="block text-sm text-neutral-400 mb-2">
Angle: {background.gradient.angle}°
</label>
<label className="block text-xs font-medium text-neutral-500 uppercase tracking-wider mb-2">Angle: {background.gradient.angle}°</label>
<input
type="range"
min="0"
@@ -143,7 +147,7 @@ const BackgroundPanel: React.FC = () => {
onChange={(e) => setBackground({
gradient: { ...background.gradient, angle: parseInt(e.target.value) }
})}
className="w-full"
className="w-full h-1.5 bg-white/10 rounded-lg appearance-none cursor-pointer accent-blue-600"
/>
</div>
</>