feat: add background panel for solid and gradient backgrounds

feat: implement code inspector with language detection and theme selection

feat: create text inspector for text properties and styling

style: add global styles and custom scrollbar for better UI

chore: initialize main entry point for the application

feat: set up Zustand store for canvas state management

feat: define types for canvas elements and background options

feat: implement code highlighting utility with language detection

chore: configure TypeScript settings for the project

chore: set up Vite configuration for React and Tailwind CSS
This commit is contained in:
2026-01-07 16:07:30 +02:00
commit bb5a9e0715
30 changed files with 6844 additions and 0 deletions
+131
View File
@@ -0,0 +1,131 @@
import React from 'react';
import { useCanvasStore } from '../../store/canvasStore';
import type { ArrowElement } from '../../types';
interface ArrowInspectorProps {
element: ArrowElement;
}
const ArrowInspector: React.FC<ArrowInspectorProps> = ({ element }) => {
const { updateElement } = useCanvasStore();
const update = (updates: Partial<ArrowElement>) => {
updateElement(element.id, updates);
};
const updateProps = (props: Partial<ArrowElement['props']>) => {
update({ props: { ...element.props, ...props } });
};
return (
<div className="space-y-4">
{/* Style */}
<div>
<label className="block text-sm text-neutral-400 mb-2">Style</label>
<div className="flex gap-2">
<button
onClick={() => updateProps({ style: 'straight' })}
className={`flex-1 py-2 rounded text-sm ${
element.props.style === 'straight'
? 'bg-blue-600 text-white'
: 'bg-neutral-700 text-neutral-300'
}`}
>
Straight
</button>
<button
onClick={() => updateProps({ style: 'curved' })}
className={`flex-1 py-2 rounded text-sm ${
element.props.style === 'curved'
? 'bg-blue-600 text-white'
: 'bg-neutral-700 text-neutral-300'
}`}
>
Curved
</button>
</div>
</div>
{/* Color */}
<div>
<label className="block text-sm text-neutral-400 mb-2">Color</label>
<div className="flex gap-2 items-center">
<input
type="color"
value={element.props.color}
onChange={(e) => updateProps({ color: e.target.value })}
className="w-10 h-10 rounded cursor-pointer bg-transparent"
/>
<input
type="text"
value={element.props.color}
onChange={(e) => updateProps({ color: e.target.value })}
className="flex-1 bg-neutral-700 text-white px-3 py-2 rounded text-sm"
/>
</div>
</div>
{/* Thickness */}
<div>
<label className="block text-sm text-neutral-400 mb-2">
Thickness: {element.props.thickness}px
</label>
<input
type="range"
value={element.props.thickness}
onChange={(e) => updateProps({ thickness: parseInt(e.target.value) })}
className="w-full"
min={1}
max={12}
/>
</div>
{/* Arrow head */}
<div>
<label className="block text-sm text-neutral-400 mb-2">Arrow Head</label>
<div className="flex gap-2">
<button
onClick={() => updateProps({ head: 'filled' })}
className={`flex-1 py-2 rounded text-sm ${
element.props.head === 'filled'
? 'bg-blue-600 text-white'
: 'bg-neutral-700 text-neutral-300'
}`}
>
Filled
</button>
<button
onClick={() => updateProps({ head: 'outline' })}
className={`flex-1 py-2 rounded text-sm ${
element.props.head === 'outline'
? 'bg-blue-600 text-white'
: 'bg-neutral-700 text-neutral-300'
}`}
>
Outline
</button>
<button
onClick={() => updateProps({ head: 'none' })}
className={`flex-1 py-2 rounded text-sm ${
element.props.head === 'none'
? 'bg-blue-600 text-white'
: 'bg-neutral-700 text-neutral-300'
}`}
>
None
</button>
</div>
</div>
{/* Points info */}
<div>
<label className="block text-sm text-neutral-400 mb-2">Points</label>
<p className="text-xs text-neutral-500">
Drag the blue handles on the canvas to adjust arrow points.
</p>
</div>
</div>
);
};
export default ArrowInspector;
@@ -0,0 +1,155 @@
import React from 'react';
import { useCanvasStore } from '../../store/canvasStore';
const GRADIENT_PRESETS = [
{ from: '#101022', to: '#1f1f3a', name: 'Midnight' },
{ from: '#0f172a', to: '#1e3a5f', name: 'Ocean' },
{ from: '#1a1a2e', to: '#16213e', name: 'Deep Blue' },
{ from: '#0f0f0f', to: '#232323', name: 'Charcoal' },
{ from: '#1a1a1a', to: '#2d2d2d', name: 'Dark' },
{ from: '#2d1b4e', to: '#1a1a2e', name: 'Purple' },
{ from: '#1e3c72', to: '#2a5298', name: 'Royal' },
{ from: '#134e5e', to: '#71b280', name: 'Teal' },
{ from: '#f5f5f5', to: '#e0e0e0', name: 'Light' },
{ from: '#ffffff', to: '#f0f0f0', name: 'White' },
];
const BackgroundPanel: React.FC = () => {
const { snap, setBackground } = useCanvasStore();
const { background } = snap;
return (
<div className="space-y-4">
{/* Background type */}
<div>
<label className="block text-sm text-neutral-400 mb-2">Type</label>
<div className="flex gap-2">
<button
onClick={() => setBackground({ type: 'solid' })}
className={`flex-1 py-2 rounded text-sm ${
background.type === 'solid'
? 'bg-blue-600 text-white'
: 'bg-neutral-700 text-neutral-300 hover:bg-neutral-600'
}`}
>
Solid
</button>
<button
onClick={() => setBackground({ type: 'gradient' })}
className={`flex-1 py-2 rounded text-sm ${
background.type === 'gradient'
? 'bg-blue-600 text-white'
: 'bg-neutral-700 text-neutral-300 hover:bg-neutral-600'
}`}
>
Gradient
</button>
</div>
</div>
{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"
/>
<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"
/>
</div>
</div>
) : (
<>
<div>
<label className="block text-sm text-neutral-400 mb-2">Presets</label>
<div className="grid grid-cols-5 gap-2">
{GRADIENT_PRESETS.map((preset, i) => (
<button
key={i}
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"
style={{
background: `linear-gradient(135deg, ${preset.from}, ${preset.to})`
}}
title={preset.name}
/>
))}
</div>
</div>
<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"
/>
</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"
/>
</div>
</div>
</div>
<div>
<label className="block text-sm text-neutral-400 mb-2">
Angle: {background.gradient.angle}°
</label>
<input
type="range"
min="0"
max="360"
value={background.gradient.angle}
onChange={(e) => setBackground({
gradient: { ...background.gradient, angle: parseInt(e.target.value) }
})}
className="w-full"
/>
</div>
</>
)}
</div>
);
};
export default BackgroundPanel;
+202
View File
@@ -0,0 +1,202 @@
import React from 'react';
import { useCanvasStore } from '../../store/canvasStore';
import type { CodeElement } from '../../types';
import { LANGUAGES, FONT_FAMILIES } from '../../types';
import { detectLanguage } from '../../utils/highlighter';
interface CodeInspectorProps {
element: CodeElement;
}
const CodeInspector: React.FC<CodeInspectorProps> = ({ element }) => {
const { updateElement, saveToHistory } = useCanvasStore();
const update = (updates: Partial<CodeElement>) => {
updateElement(element.id, updates);
};
const updateProps = (props: Partial<CodeElement['props']>) => {
update({ props: { ...element.props, ...props } });
};
const handleCodeChange = (code: string) => {
updateProps({ code });
};
const handleAutoDetect = () => {
const detected = detectLanguage(element.props.code);
updateProps({ language: detected });
};
return (
<div className="space-y-4">
{/* Code editor */}
<div>
<label className="block text-sm text-neutral-400 mb-2">Code</label>
<textarea
value={element.props.code}
onChange={(e) => handleCodeChange(e.target.value)}
onBlur={saveToHistory}
className="w-full h-32 bg-neutral-900 text-white text-sm font-mono p-3 rounded resize-y"
spellCheck={false}
/>
</div>
{/* Language */}
<div>
<div className="flex items-center justify-between mb-2">
<label className="text-sm text-neutral-400">Language</label>
<button
onClick={handleAutoDetect}
className="text-xs text-blue-400 hover:text-blue-300"
>
Auto-detect
</button>
</div>
<select
value={element.props.language}
onChange={(e) => updateProps({ language: e.target.value })}
className="w-full bg-neutral-700 text-white px-3 py-2 rounded text-sm"
>
{LANGUAGES.map((lang) => (
<option key={lang} value={lang}>
{lang}
</option>
))}
</select>
</div>
{/* Theme */}
<div>
<label className="block text-sm text-neutral-400 mb-2">Theme</label>
<div className="flex gap-2">
<button
onClick={() => updateProps({ theme: 'dark' })}
className={`flex-1 py-2 rounded text-sm ${
element.props.theme === 'dark'
? 'bg-blue-600 text-white'
: 'bg-neutral-700 text-neutral-300'
}`}
>
Dark
</button>
<button
onClick={() => updateProps({ theme: 'light' })}
className={`flex-1 py-2 rounded text-sm ${
element.props.theme === 'light'
? 'bg-blue-600 text-white'
: 'bg-neutral-700 text-neutral-300'
}`}
>
Light
</button>
</div>
</div>
{/* Font */}
<div>
<label className="block text-sm text-neutral-400 mb-2">Font</label>
<select
value={element.props.fontFamily}
onChange={(e) => updateProps({ fontFamily: e.target.value })}
className="w-full bg-neutral-700 text-white px-3 py-2 rounded text-sm"
>
{FONT_FAMILIES.code.map((font) => (
<option key={font} value={font}>
{font}
</option>
))}
</select>
</div>
{/* Font size & Line height */}
<div className="grid grid-cols-2 gap-3">
<div>
<label className="block text-sm text-neutral-400 mb-2">Size</label>
<input
type="number"
value={element.props.fontSize}
onChange={(e) => updateProps({ fontSize: parseInt(e.target.value) || 14 })}
className="w-full bg-neutral-700 text-white px-3 py-2 rounded text-sm"
min={10}
max={32}
/>
</div>
<div>
<label className="block text-sm text-neutral-400 mb-2">Line Height</label>
<input
type="number"
value={element.props.lineHeight}
onChange={(e) => updateProps({ lineHeight: parseFloat(e.target.value) || 1.5 })}
className="w-full bg-neutral-700 text-white px-3 py-2 rounded text-sm"
min={1}
max={3}
step={0.1}
/>
</div>
</div>
{/* Line numbers */}
<div className="flex items-center justify-between">
<label className="text-sm text-neutral-400">Line Numbers</label>
<button
onClick={() => updateProps({ lineNumbers: !element.props.lineNumbers })}
className={`w-12 h-6 rounded-full transition-colors ${
element.props.lineNumbers ? 'bg-blue-600' : 'bg-neutral-600'
}`}
>
<div
className={`w-5 h-5 bg-white rounded-full transition-transform ${
element.props.lineNumbers ? 'translate-x-6' : 'translate-x-0.5'
}`}
/>
</button>
</div>
{/* Padding & Corner radius */}
<div className="grid grid-cols-2 gap-3">
<div>
<label className="block text-sm text-neutral-400 mb-2">Padding</label>
<input
type="number"
value={element.props.padding}
onChange={(e) => updateProps({ padding: parseInt(e.target.value) || 0 })}
className="w-full bg-neutral-700 text-white px-3 py-2 rounded text-sm"
min={0}
max={64}
/>
</div>
<div>
<label className="block text-sm text-neutral-400 mb-2">Radius</label>
<input
type="number"
value={element.props.cornerRadius}
onChange={(e) => updateProps({ cornerRadius: parseInt(e.target.value) || 0 })}
className="w-full bg-neutral-700 text-white px-3 py-2 rounded text-sm"
min={0}
max={32}
/>
</div>
</div>
{/* Shadow */}
<div>
<label className="block text-sm text-neutral-400 mb-2">
Shadow Blur: {element.props.shadow.blur}
</label>
<input
type="range"
value={element.props.shadow.blur}
onChange={(e) => updateProps({
shadow: { ...element.props.shadow, blur: parseInt(e.target.value) }
})}
className="w-full"
min={0}
max={64}
/>
</div>
</div>
);
};
export default CodeInspector;
+198
View File
@@ -0,0 +1,198 @@
import React from 'react';
import { useCanvasStore } from '../../store/canvasStore';
import type { TextElement } from '../../types';
import { FONT_FAMILIES } from '../../types';
interface TextInspectorProps {
element: TextElement;
}
const TextInspector: React.FC<TextInspectorProps> = ({ element }) => {
const { updateElement, saveToHistory } = useCanvasStore();
const update = (updates: Partial<TextElement>) => {
updateElement(element.id, updates);
};
const updateProps = (props: Partial<TextElement['props']>) => {
update({ props: { ...element.props, ...props } });
};
return (
<div className="space-y-4">
{/* Text */}
<div>
<label className="block text-sm text-neutral-400 mb-2">Text</label>
<textarea
value={element.props.text}
onChange={(e) => updateProps({ text: e.target.value })}
onBlur={saveToHistory}
className="w-full h-24 bg-neutral-900 text-white text-sm p-3 rounded resize-y"
/>
</div>
{/* Font */}
<div>
<label className="block text-sm text-neutral-400 mb-2">Font</label>
<select
value={element.props.fontFamily}
onChange={(e) => updateProps({ fontFamily: e.target.value })}
className="w-full bg-neutral-700 text-white px-3 py-2 rounded text-sm"
>
{FONT_FAMILIES.text.map((font) => (
<option key={font} value={font}>
{font}
</option>
))}
</select>
</div>
{/* Font size */}
<div>
<label className="block text-sm text-neutral-400 mb-2">Size: {element.props.fontSize}</label>
<input
type="range"
value={element.props.fontSize}
onChange={(e) => updateProps({ fontSize: parseInt(e.target.value) })}
className="w-full"
min={12}
max={96}
/>
</div>
{/* Color */}
<div>
<label className="block text-sm text-neutral-400 mb-2">Color</label>
<div className="flex gap-2 items-center">
<input
type="color"
value={element.props.color}
onChange={(e) => updateProps({ color: e.target.value })}
className="w-10 h-10 rounded cursor-pointer bg-transparent"
/>
<input
type="text"
value={element.props.color}
onChange={(e) => updateProps({ color: e.target.value })}
className="flex-1 bg-neutral-700 text-white px-3 py-2 rounded text-sm"
/>
</div>
</div>
{/* Style buttons */}
<div>
<label className="block text-sm text-neutral-400 mb-2">Style</label>
<div className="flex gap-2">
<button
onClick={() => updateProps({ bold: !element.props.bold })}
className={`flex-1 py-2 rounded text-sm font-bold ${
element.props.bold
? 'bg-blue-600 text-white'
: 'bg-neutral-700 text-neutral-300'
}`}
>
B
</button>
<button
onClick={() => updateProps({ italic: !element.props.italic })}
className={`flex-1 py-2 rounded text-sm italic ${
element.props.italic
? 'bg-blue-600 text-white'
: 'bg-neutral-700 text-neutral-300'
}`}
>
I
</button>
<button
onClick={() => updateProps({ underline: !element.props.underline })}
className={`flex-1 py-2 rounded text-sm underline ${
element.props.underline
? 'bg-blue-600 text-white'
: 'bg-neutral-700 text-neutral-300'
}`}
>
U
</button>
</div>
</div>
{/* Alignment */}
<div>
<label className="block text-sm text-neutral-400 mb-2">Alignment</label>
<div className="flex gap-2">
<button
onClick={() => updateProps({ align: 'left' })}
className={`flex-1 py-2 rounded text-sm ${
element.props.align === 'left'
? 'bg-blue-600 text-white'
: 'bg-neutral-700 text-neutral-300'
}`}
>
Left
</button>
<button
onClick={() => updateProps({ align: 'center' })}
className={`flex-1 py-2 rounded text-sm ${
element.props.align === 'center'
? 'bg-blue-600 text-white'
: 'bg-neutral-700 text-neutral-300'
}`}
>
Center
</button>
<button
onClick={() => updateProps({ align: 'right' })}
className={`flex-1 py-2 rounded text-sm ${
element.props.align === 'right'
? 'bg-blue-600 text-white'
: 'bg-neutral-700 text-neutral-300'
}`}
>
Right
</button>
</div>
</div>
{/* Background */}
<div>
<div className="flex items-center justify-between mb-2">
<label className="text-sm text-neutral-400">Background</label>
<button
onClick={() => updateProps({
background: element.props.background
? null
: { color: 'rgba(0,0,0,0.5)' }
})}
className={`w-12 h-6 rounded-full transition-colors ${
element.props.background ? 'bg-blue-600' : 'bg-neutral-600'
}`}
>
<div
className={`w-5 h-5 bg-white rounded-full transition-transform ${
element.props.background ? 'translate-x-6' : 'translate-x-0.5'
}`}
/>
</button>
</div>
{element.props.background && (
<div className="flex gap-2 items-center">
<input
type="color"
value={element.props.background.color.substring(0, 7)}
onChange={(e) => updateProps({ background: { color: e.target.value } })}
className="w-10 h-10 rounded cursor-pointer bg-transparent"
/>
<input
type="text"
value={element.props.background.color}
onChange={(e) => updateProps({ background: { color: e.target.value } })}
className="flex-1 bg-neutral-700 text-white px-3 py-2 rounded text-sm"
/>
</div>
)}
</div>
</div>
);
};
export default TextInspector;