feat: enhance UI components with improved styling and interactions
This commit is contained in:
@@ -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
@@ -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
@@ -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,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>
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
|
||||
Reference in New Issue
Block a user