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:
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
Reference in New Issue
Block a user