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);
|
const selectedElement = snap.elements.find(el => el.id === selectedElementId);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-72 bg-neutral-800 border-l border-neutral-700 overflow-y-auto">
|
<div className="w-80 bg-[#09090b] border-l border-white/5 overflow-y-auto h-full">
|
||||||
<div className="p-4">
|
<div className="p-6">
|
||||||
{selectedElement ? (
|
{selectedElement ? (
|
||||||
<>
|
<div className="space-y-6">
|
||||||
{/* Element header */}
|
{/* Header with Title and Element Actions */}
|
||||||
<div className="flex items-center justify-between mb-4">
|
<div className="flex items-center justify-between">
|
||||||
<h3 className="text-white font-medium capitalize">
|
<h3 className="text-white font-semibold text-sm uppercase tracking-wider">
|
||||||
{selectedElement.type} Element
|
{selectedElement.type}
|
||||||
</h3>
|
</h3>
|
||||||
<div className="flex gap-1">
|
<div className="flex items-center 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>
|
|
||||||
<button
|
<button
|
||||||
onClick={() => duplicateElement(selectedElement.id)}
|
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)"
|
title="Duplicate (⌘D)"
|
||||||
>
|
>
|
||||||
<svg className="w-4 h-4" 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="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" />
|
<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>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
|
<div className="w-px h-3 bg-white/10 mx-1" />
|
||||||
<button
|
<button
|
||||||
onClick={() => deleteElement(selectedElement.id)}
|
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 (⌫)"
|
title="Delete (⌫)"
|
||||||
>
|
>
|
||||||
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
@@ -68,7 +51,32 @@ const Inspector: React.FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
</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 */}
|
{/* Element-specific inspector */}
|
||||||
|
<div className="inspector-content">
|
||||||
{selectedElement.type === 'code' && (
|
{selectedElement.type === 'code' && (
|
||||||
<CodeInspector element={selectedElement as CodeElement} />
|
<CodeInspector element={selectedElement as CodeElement} />
|
||||||
)}
|
)}
|
||||||
@@ -78,16 +86,16 @@ const Inspector: React.FC = () => {
|
|||||||
{selectedElement.type === 'arrow' && (
|
{selectedElement.type === 'arrow' && (
|
||||||
<ArrowInspector element={selectedElement as ArrowElement} />
|
<ArrowInspector element={selectedElement as ArrowElement} />
|
||||||
)}
|
)}
|
||||||
</>
|
</div>
|
||||||
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<div className="space-y-6">
|
||||||
<h3 className="text-white font-medium mb-4">Canvas Settings</h3>
|
<h3 className="text-white font-semibold text-sm uppercase tracking-wider">Canvas Settings</h3>
|
||||||
<BackgroundPanel />
|
<BackgroundPanel />
|
||||||
</>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Inspector;
|
export default Inspector;
|
||||||
|
|||||||
+61
-31
@@ -5,24 +5,57 @@ const Toolbar: React.FC = () => {
|
|||||||
const { tool, setTool, showGrid, setShowGrid, zoom, setZoom } = useCanvasStore();
|
const { tool, setTool, showGrid, setShowGrid, zoom, setZoom } = useCanvasStore();
|
||||||
|
|
||||||
const tools = [
|
const tools = [
|
||||||
{ id: 'select', icon: '↖', label: 'Select (V)' },
|
{
|
||||||
{ id: 'code', icon: '{ }', label: 'Code Block (C)' },
|
id: 'select',
|
||||||
{ id: 'text', icon: 'T', label: 'Text (T)' },
|
label: 'Select (V)',
|
||||||
{ id: 'arrow', icon: '→', 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="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;
|
] as const;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-14 bg-neutral-800 border-r border-neutral-700 flex flex-col items-center py-4 gap-2">
|
<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 */}
|
|
||||||
<div className="flex flex-col gap-1">
|
{/* Tools Group */}
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
{tools.map(({ id, icon, label }) => (
|
{tools.map(({ id, icon, label }) => (
|
||||||
<button
|
<button
|
||||||
key={id}
|
key={id}
|
||||||
onClick={() => setTool(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
|
tool === id
|
||||||
? 'bg-blue-600 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:bg-neutral-700 hover:text-white'
|
: 'text-neutral-400 hover:text-white hover:bg-white/5 active:scale-95'
|
||||||
}`}
|
}`}
|
||||||
title={label}
|
title={label}
|
||||||
>
|
>
|
||||||
@@ -31,15 +64,15 @@ const Toolbar: React.FC = () => {
|
|||||||
))}
|
))}
|
||||||
</div>
|
</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
|
<button
|
||||||
onClick={() => setShowGrid(!showGrid)}
|
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
|
showGrid
|
||||||
? 'bg-neutral-600 text-white'
|
? 'bg-white/10 text-white ring-1 ring-white/20'
|
||||||
: 'text-neutral-400 hover:bg-neutral-700 hover:text-white'
|
: 'text-neutral-400 hover:text-white hover:bg-white/5 active:scale-95'
|
||||||
}`}
|
}`}
|
||||||
title="Toggle Grid (⌘;)"
|
title="Toggle Grid (⌘;)"
|
||||||
>
|
>
|
||||||
@@ -48,30 +81,27 @@ const Toolbar: React.FC = () => {
|
|||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
{/* Spacer */}
|
{/* Zoom Controls */}
|
||||||
<div className="flex-1" />
|
<div className="flex items-center gap-1 bg-white/5 rounded-xl p-1 ml-1">
|
||||||
|
|
||||||
{/* Zoom controls */}
|
|
||||||
<div className="flex flex-col gap-1">
|
|
||||||
<button
|
<button
|
||||||
onClick={() => setZoom(zoom + 0.1)}
|
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"
|
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 (⌘+)"
|
title="Zoom Out (⌘-)"
|
||||||
>
|
>
|
||||||
<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 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0zM10 7v6m3-3H7" />
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M20 12H4" />
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</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)}%
|
{Math.round(zoom * 100)}%
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
onClick={() => setZoom(zoom - 0.1)}
|
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"
|
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 (⌘-)"
|
title="Zoom In (⌘+)"
|
||||||
>
|
>
|
||||||
<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 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0zM13 10H7" />
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 4v16m8-8H4" />
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
+69
-60
@@ -96,13 +96,24 @@ const TopBar: React.FC<TopBarProps> = ({ stageRef }) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="h-14 bg-neutral-800 border-b border-neutral-700 flex items-center justify-between px-4">
|
<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 */}
|
{/* Left section: Logo & Actions */}
|
||||||
<div className="flex items-center gap-4">
|
<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
|
<button
|
||||||
onClick={handleNewSnap}
|
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)"
|
title="New (⌘N)"
|
||||||
>
|
>
|
||||||
<svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
<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>
|
||||||
<button
|
<button
|
||||||
onClick={handleImportJSON}
|
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"
|
title="Open"
|
||||||
>
|
>
|
||||||
<svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
<svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
@@ -119,46 +130,24 @@ const TopBar: React.FC<TopBarProps> = ({ stageRef }) => {
|
|||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="h-6 w-px bg-neutral-600" />
|
|
||||||
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<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"
|
|
||||||
title="Undo (⌘Z)"
|
|
||||||
>
|
|
||||||
<svg className="w-5 h-5" 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"
|
|
||||||
title="Redo (⇧⌘Z)"
|
|
||||||
>
|
|
||||||
<svg className="w-5 h-5" 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>
|
</div>
|
||||||
|
|
||||||
{/* Center section */}
|
{/* Center section: Title & Tools */}
|
||||||
<div className="flex items-center gap-4">
|
<div className="absolute left-1/2 -translate-x-1/2 flex items-center">
|
||||||
|
<div className="flex flex-col items-center">
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
value={snap.meta.title}
|
value={snap.meta.title}
|
||||||
onChange={(e) => updateMeta({ title: e.target.value })}
|
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"
|
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
|
<select
|
||||||
value={snap.meta.aspect}
|
value={snap.meta.aspect}
|
||||||
onChange={handleAspectChange}
|
onChange={handleAspectChange}
|
||||||
className="bg-neutral-700 text-white px-3 py-1.5 rounded text-sm"
|
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) => (
|
{ASPECT_RATIOS.map((ratio) => (
|
||||||
<option key={ratio.name} value={ratio.name}>
|
<option key={ratio.name} value={ratio.name}>
|
||||||
@@ -167,47 +156,67 @@ const TopBar: React.FC<TopBarProps> = ({ stageRef }) => {
|
|||||||
))}
|
))}
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Right section */}
|
{/* Right section: History & Export */}
|
||||||
<div className="flex items-center gap-2">
|
<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
|
<button
|
||||||
onClick={handleExportJSON}
|
onClick={undo}
|
||||||
className="px-3 py-1.5 text-sm bg-neutral-700 hover:bg-neutral-600 rounded text-white transition-colors"
|
disabled={history.past.length === 0}
|
||||||
|
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)"
|
||||||
>
|
>
|
||||||
Save JSON
|
<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>
|
||||||
|
<button
|
||||||
|
onClick={redo}
|
||||||
|
disabled={history.future.length === 0}
|
||||||
|
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-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 className="h-6 w-px bg-white/10" />
|
||||||
|
|
||||||
<div className="relative group">
|
<div className="relative group">
|
||||||
<button
|
<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>
|
||||||
</button>
|
<svg className="w-4 h-4 text-neutral-600" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
<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">
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
|
||||||
<button
|
</svg>
|
||||||
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>
|
</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
|
<button
|
||||||
onClick={() => handleExportImage('png', 2)}
|
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>
|
||||||
<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
|
<button
|
||||||
onClick={() => handleExportImage('jpeg', 2)}
|
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>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -26,9 +26,10 @@ const Arrow: React.FC<ArrowProps> = ({ element, isSelected, onSelect, onChange }
|
|||||||
onChange({ points: newPoints });
|
onChange({ points: newPoints });
|
||||||
};
|
};
|
||||||
|
|
||||||
// Arrow head pointer
|
// Modern arrow head calculations
|
||||||
const pointerLength = props.head === 'none' ? 0 : props.thickness * 4;
|
// Make the head slightly sleeker
|
||||||
const pointerWidth = props.head === 'none' ? 0 : props.thickness * 3;
|
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 (
|
return (
|
||||||
<Group>
|
<Group>
|
||||||
@@ -40,9 +41,14 @@ const Arrow: React.FC<ArrowProps> = ({ element, isSelected, onSelect, onChange }
|
|||||||
fill={props.head === 'filled' ? props.color : 'transparent'}
|
fill={props.head === 'filled' ? props.color : 'transparent'}
|
||||||
pointerLength={pointerLength}
|
pointerLength={pointerLength}
|
||||||
pointerWidth={pointerWidth}
|
pointerWidth={pointerWidth}
|
||||||
tension={props.style === 'curved' ? 0.5 : 0}
|
tension={props.style === 'curved' ? 0.4 : 0}
|
||||||
lineCap="round"
|
lineCap="round"
|
||||||
lineJoin="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}
|
onClick={onSelect}
|
||||||
onTap={onSelect}
|
onTap={onSelect}
|
||||||
hitStrokeWidth={20}
|
hitStrokeWidth={20}
|
||||||
@@ -54,13 +60,26 @@ const Arrow: React.FC<ArrowProps> = ({ element, isSelected, onSelect, onChange }
|
|||||||
key={index}
|
key={index}
|
||||||
x={point.x}
|
x={point.x}
|
||||||
y={point.y}
|
y={point.y}
|
||||||
radius={8}
|
radius={5}
|
||||||
fill="#3b82f6"
|
fill="#ffffff"
|
||||||
stroke="#ffffff"
|
stroke="#3b82f6"
|
||||||
strokeWidth={2}
|
strokeWidth={2}
|
||||||
|
shadowColor="rgba(0,0,0,0.15)"
|
||||||
|
shadowBlur={4}
|
||||||
|
shadowOffset={{ x: 0, y: 1 }}
|
||||||
draggable={!element.locked}
|
draggable={!element.locked}
|
||||||
onDragMove={(e) => handlePointDrag(index, e)}
|
onDragMove={(e) => handlePointDrag(index, e)}
|
||||||
onDragEnd={(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>
|
</Group>
|
||||||
|
|||||||
@@ -19,27 +19,27 @@ const BackgroundPanel: React.FC = () => {
|
|||||||
const { background } = snap;
|
const { background } = snap;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-4">
|
<div className="space-y-6">
|
||||||
{/* Background type */}
|
{/* Background type */}
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm text-neutral-400 mb-2">Type</label>
|
<label className="block text-xs font-medium text-neutral-500 uppercase tracking-wider mb-3">Type</label>
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2 p-1 bg-white/5 rounded-lg border border-white/5">
|
||||||
<button
|
<button
|
||||||
onClick={() => setBackground({ type: 'solid' })}
|
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'
|
background.type === 'solid'
|
||||||
? 'bg-blue-600 text-white'
|
? 'bg-neutral-700 text-white shadow-sm'
|
||||||
: 'bg-neutral-700 text-neutral-300 hover:bg-neutral-600'
|
: 'text-neutral-400 hover:text-white hover:bg-white/5'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
Solid
|
Solid
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={() => setBackground({ type: 'gradient' })}
|
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'
|
background.type === 'gradient'
|
||||||
? 'bg-blue-600 text-white'
|
? 'bg-neutral-700 text-white shadow-sm'
|
||||||
: 'bg-neutral-700 text-neutral-300 hover:bg-neutral-600'
|
: 'text-neutral-400 hover:text-white hover:bg-white/5'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
Gradient
|
Gradient
|
||||||
@@ -49,26 +49,28 @@ const BackgroundPanel: React.FC = () => {
|
|||||||
|
|
||||||
{background.type === 'solid' ? (
|
{background.type === 'solid' ? (
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm text-neutral-400 mb-2">Color</label>
|
<label className="block text-xs font-medium text-neutral-500 uppercase tracking-wider mb-2">Color</label>
|
||||||
<div className="flex gap-2 items-center">
|
<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
|
<input
|
||||||
type="color"
|
type="color"
|
||||||
value={background.solid.color}
|
value={background.solid.color}
|
||||||
onChange={(e) => setBackground({ solid: { color: e.target.value } })}
|
onChange={(e) => setBackground({ solid: { color: e.target.value } })}
|
||||||
className="w-10 h-10 rounded cursor-pointer bg-transparent"
|
className="absolute inset-[-4px] w-[200%] h-[200%] cursor-pointer p-0 m-0 border-none"
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
value={background.solid.color}
|
value={background.solid.color}
|
||||||
onChange={(e) => setBackground({ solid: { color: e.target.value } })}
|
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>
|
</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">
|
<div className="grid grid-cols-5 gap-2">
|
||||||
{GRADIENT_PRESETS.map((preset, i) => (
|
{GRADIENT_PRESETS.map((preset, i) => (
|
||||||
<button
|
<button
|
||||||
@@ -76,7 +78,7 @@ const BackgroundPanel: React.FC = () => {
|
|||||||
onClick={() => setBackground({
|
onClick={() => setBackground({
|
||||||
gradient: { ...background.gradient, from: preset.from, to: preset.to }
|
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={{
|
style={{
|
||||||
background: `linear-gradient(135deg, ${preset.from}, ${preset.to})`
|
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 className="grid grid-cols-2 gap-3">
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm text-neutral-400 mb-2">From</label>
|
<label className="block text-xs font-medium text-neutral-500 uppercase tracking-wider mb-2">From</label>
|
||||||
<div className="flex gap-2 items-center">
|
<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
|
<input
|
||||||
type="color"
|
type="color"
|
||||||
value={background.gradient.from}
|
value={background.gradient.from}
|
||||||
onChange={(e) => setBackground({
|
onChange={(e) => setBackground({
|
||||||
gradient: { ...background.gradient, from: e.target.value }
|
gradient: { ...background.gradient, from: e.target.value }
|
||||||
})}
|
})}
|
||||||
className="w-8 h-8 rounded cursor-pointer bg-transparent"
|
className="absolute inset-[-4px] w-[200%] h-[200%] cursor-pointer"
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
value={background.gradient.from}
|
value={background.gradient.from}
|
||||||
onChange={(e) => setBackground({
|
onChange={(e) => setBackground({
|
||||||
gradient: { ...background.gradient, from: e.target.value }
|
gradient: { ...background.gradient, from: e.target.value }
|
||||||
})}
|
})}
|
||||||
className="flex-1 bg-neutral-700 text-white px-2 py-1.5 rounded text-xs"
|
className="w-full bg-transparent text-white text-xs focus:outline-none font-mono"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm text-neutral-400 mb-2">To</label>
|
<label className="block text-xs font-medium text-neutral-500 uppercase tracking-wider mb-2">To</label>
|
||||||
<div className="flex gap-2 items-center">
|
<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
|
<input
|
||||||
type="color"
|
type="color"
|
||||||
value={background.gradient.to}
|
value={background.gradient.to}
|
||||||
onChange={(e) => setBackground({
|
onChange={(e) => setBackground({
|
||||||
gradient: { ...background.gradient, to: e.target.value }
|
gradient: { ...background.gradient, from: e.target.value }
|
||||||
})}
|
})}
|
||||||
className="w-8 h-8 rounded cursor-pointer bg-transparent"
|
className="absolute inset-[-4px] w-[200%] h-[200%] cursor-pointer"
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
value={background.gradient.to}
|
value={background.gradient.to}
|
||||||
onChange={(e) => setBackground({
|
onChange={(e) => setBackground({
|
||||||
gradient: { ...background.gradient, to: e.target.value }
|
gradient: { ...background.gradient, from: e.target.value }
|
||||||
})}
|
})}
|
||||||
className="flex-1 bg-neutral-700 text-white px-2 py-1.5 rounded text-xs"
|
className="w-full bg-transparent text-white text-xs focus:outline-none font-mono"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm text-neutral-400 mb-2">
|
<label className="block text-xs font-medium text-neutral-500 uppercase tracking-wider mb-2">Angle: {background.gradient.angle}°</label>
|
||||||
Angle: {background.gradient.angle}°
|
|
||||||
</label>
|
|
||||||
<input
|
<input
|
||||||
type="range"
|
type="range"
|
||||||
min="0"
|
min="0"
|
||||||
@@ -143,7 +147,7 @@ const BackgroundPanel: React.FC = () => {
|
|||||||
onChange={(e) => setBackground({
|
onChange={(e) => setBackground({
|
||||||
gradient: { ...background.gradient, angle: parseInt(e.target.value) }
|
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>
|
</div>
|
||||||
</>
|
</>
|
||||||
|
|||||||
Reference in New Issue
Block a user