+ {/* Left Sidebar */}
+
+
+ {/* Center Canvas Area */}
+
+
+
+ Apparel Designer
+
+
+ T-shirt customization editor
+
+
- {/* Canvas container */}
-
+
+ {/* Layers panel below canvas */}
+
+
+
- {/* Debug info */}
-
-
Elements: {elements.length}
-
Selected: {selectedId || 'None'}
-
- Tip: Click to select, drag to move, use handles to resize. Press Delete to remove.
-
-
+ {/* Right Properties Panel */}
+
updateElement(selectedId, attrs)}
+ onDelete={deleteElement}
+ />
);
}
diff --git a/client/src/components/panels/LayersPanel.jsx b/client/src/components/panels/LayersPanel.jsx
new file mode 100644
index 0000000..bff5f82
--- /dev/null
+++ b/client/src/components/panels/LayersPanel.jsx
@@ -0,0 +1,131 @@
+export function LayersPanel({ elements, selectedId, onSelect, onDelete }) {
+ const getIcon = (element) => {
+ switch (element.type) {
+ case 'image':
+ return element.bgRemoved ? '๐ผ๏ธ' : '๐ท';
+ case 'text':
+ return '๐';
+ case 'sticker':
+ return '๐จ';
+ default:
+ return '๐';
+ }
+ };
+
+ const getName = (element) => {
+ switch (element.type) {
+ case 'image':
+ return element.bgRemoved ? 'Image (BG โ)' : 'Image';
+ case 'text':
+ return element.text?.substring(0, 20) || 'Text';
+ case 'sticker':
+ return 'Sticker';
+ default:
+ return 'Element';
+ }
+ };
+
+ if (elements.length === 0) {
+ return (
+
+ No elements yet. Add images, text, or stickers to your design.
+
+ );
+ }
+
+ return (
+
+
+ Layers ({elements.length})
+
+
+
+ {elements.map((element, index) => (
+
onSelect(element.id)}
+ style={{
+ display: 'flex',
+ alignItems: 'center',
+ gap: '0.5rem',
+ padding: '0.5rem 0.75rem',
+ background: selectedId === element.id ? 'var(--accent-bg)' : 'transparent',
+ border: `1px solid ${selectedId === element.id ? 'var(--accent)' : 'var(--border)'}`,
+ borderRadius: 'var(--radius-sm)',
+ cursor: 'pointer',
+ transition: 'all 0.15s ease',
+ }}
+ onMouseEnter={(e) => {
+ if (selectedId !== element.id) {
+ e.target.style.borderColor = 'var(--accent)';
+ }
+ }}
+ onMouseLeave={(e) => {
+ if (selectedId !== element.id) {
+ e.target.style.borderColor = 'var(--border)';
+ }
+ }}
+ >
+ {getIcon(element)}
+
+ {getName(element)}
+
+
+
+ ))}
+
+
+ );
+}
diff --git a/client/src/components/panels/PropertiesPanel.jsx b/client/src/components/panels/PropertiesPanel.jsx
new file mode 100644
index 0000000..316ffd6
--- /dev/null
+++ b/client/src/components/panels/PropertiesPanel.jsx
@@ -0,0 +1,281 @@
+export function PropertiesPanel({ element, onUpdate, onDelete }) {
+ if (!element) {
+ return (
+
+
+
+ Properties
+
+
+
+
+ Select an element to edit its properties
+
+
+ );
+ }
+
+ const handlePositionChange = (axis, value) => {
+ onUpdate({ [axis]: parseFloat(value) || 0 });
+ };
+
+ const handleSizeChange = (axis, value) => {
+ const numValue = parseFloat(value) || 20;
+ onUpdate({ [axis]: Math.max(20, numValue) });
+ };
+
+ const handleRotationChange = (value) => {
+ const numValue = parseFloat(value) || 0;
+ onUpdate({ rotation: Math.max(-180, Math.min(180, numValue)) });
+ };
+
+ return (
+
+
+
+ Properties
+
+
+
+
+ {/* Element type badge */}
+
+ {element.type}
+
+
+ {/* Position */}
+
+
+
+
+
+ handlePositionChange('x', e.target.value)}
+ style={{
+ width: '100%',
+ padding: '0.5rem',
+ border: `1px solid var(--border)`,
+ borderRadius: 'var(--radius-sm)',
+ fontSize: '13px',
+ }}
+ />
+
+
+
+ handlePositionChange('y', e.target.value)}
+ style={{
+ width: '100%',
+ padding: '0.5rem',
+ border: `1px solid var(--border)`,
+ borderRadius: 'var(--radius-sm)',
+ fontSize: '13px',
+ }}
+ />
+
+
+
+
+ {/* Size (for images and stickers) */}
+ {(element.type === 'image' || element.type === 'sticker') && (
+
+
+
+
+
+ handleSizeChange('width', e.target.value)}
+ style={{
+ width: '100%',
+ padding: '0.5rem',
+ border: `1px solid var(--border)`,
+ borderRadius: 'var(--radius-sm)',
+ fontSize: '13px',
+ }}
+ />
+
+
+
+ handleSizeChange('height', e.target.value)}
+ style={{
+ width: '100%',
+ padding: '0.5rem',
+ border: `1px solid var(--border)`,
+ borderRadius: 'var(--radius-sm)',
+ fontSize: '13px',
+ }}
+ />
+
+
+
+ )}
+
+ {/* Font size (for text) */}
+ {element.type === 'text' && (
+ <>
+
+
+ onUpdate({ fontSize: parseInt(e.target.value, 10) })}
+ style={{ width: '100%' }}
+ />
+
+
+
+
+ onUpdate({ fill: e.target.value })}
+ style={{
+ width: '100%',
+ height: '36px',
+ border: `1px solid var(--border)`,
+ borderRadius: 'var(--radius-sm)',
+ cursor: 'pointer',
+ padding: '2px',
+ }}
+ />
+
+ >
+ )}
+
+ {/* Rotation */}
+
+
+ handleRotationChange(e.target.value)}
+ style={{ width: '100%' }}
+ />
+
+
+ {/* Delete button */}
+
+
+
+ );
+}
diff --git a/client/src/components/panels/index.js b/client/src/components/panels/index.js
new file mode 100644
index 0000000..60db172
--- /dev/null
+++ b/client/src/components/panels/index.js
@@ -0,0 +1,2 @@
+export { LayersPanel } from './LayersPanel';
+export { PropertiesPanel } from './PropertiesPanel';
diff --git a/client/src/components/sidebar/Sidebar.jsx b/client/src/components/sidebar/Sidebar.jsx
new file mode 100644
index 0000000..dc2d69e
--- /dev/null
+++ b/client/src/components/sidebar/Sidebar.jsx
@@ -0,0 +1,73 @@
+import { useState } from 'react';
+import { UploadTab } from './UploadTab';
+import { StickersTab } from './StickersTab';
+import { TextTab } from './TextTab';
+import { TemplatesTab } from './TemplatesTab';
+
+const TABS = [
+ { id: 'upload', label: 'Upload', icon: '๐' },
+ { id: 'stickers', label: 'Stickers', icon: '๐จ' },
+ { id: 'text', label: 'Text', icon: '๐' },
+ { id: 'templates', label: 'Templates', icon: '๐' },
+];
+
+export function Sidebar({ onAddImage, onAddSticker, onAddText, onAddTemplate }) {
+ const [activeTab, setActiveTab] = useState('upload');
+
+ const renderTabContent = () => {
+ switch (activeTab) {
+ case 'upload':
+ return
;
+ case 'stickers':
+ return
;
+ case 'text':
+ return
;
+ case 'templates':
+ return
;
+ default:
+ return null;
+ }
+ };
+
+ return (
+
+ {/* Tab headers */}
+
+ {TABS.map((tab) => (
+
+ ))}
+
+
+ {/* Tab content */}
+
+ {renderTabContent()}
+
+
+ );
+}
diff --git a/client/src/components/sidebar/StickersTab.jsx b/client/src/components/sidebar/StickersTab.jsx
new file mode 100644
index 0000000..d87a6b4
--- /dev/null
+++ b/client/src/components/sidebar/StickersTab.jsx
@@ -0,0 +1,111 @@
+import { useState } from 'react';
+import { STICKERS, STICKER_CATEGORIES } from '../../constants/stickers';
+
+export function StickersTab({ onAddSticker }) {
+ const [activeCategory, setActiveCategory] = useState('all');
+
+ const categories = ['all', ...STICKER_CATEGORIES];
+
+ const filteredStickers = activeCategory === 'all'
+ ? STICKERS
+ : STICKERS.filter(s => s.category === activeCategory);
+
+ const handleAddSticker = (emoji) => {
+ // Create a canvas element with the emoji
+ const canvas = document.createElement('canvas');
+ const size = 100;
+ canvas.width = size;
+ canvas.height = size;
+ const ctx = canvas.getContext('2d');
+
+ ctx.font = `${size * 0.8}px Arial`;
+ ctx.textAlign = 'center';
+ ctx.textBaseline = 'middle';
+ ctx.fillText(emoji, size / 2, size / 2);
+
+ const dataUrl = canvas.toDataURL('image/png');
+
+ onAddSticker({
+ type: 'sticker',
+ x: 125,
+ y: 125,
+ width: 80,
+ height: 80,
+ rotation: 0,
+ src: dataUrl,
+ emoji,
+ });
+ };
+
+ return (
+
+
+ Stickers
+
+
+ {/* Category pills */}
+
+ {categories.map((cat) => (
+
+ ))}
+
+
+ {/* Sticker grid */}
+
+ {filteredStickers.map((sticker, index) => (
+
+ ))}
+
+
+ );
+}
diff --git a/client/src/components/sidebar/TemplatesTab.jsx b/client/src/components/sidebar/TemplatesTab.jsx
new file mode 100644
index 0000000..70d9500
--- /dev/null
+++ b/client/src/components/sidebar/TemplatesTab.jsx
@@ -0,0 +1,133 @@
+export function TemplatesTab({ onAddTemplate }) {
+ const templates = [
+ {
+ id: 'freeform',
+ name: 'Freeform',
+ description: 'No template - design freely',
+ thumbnail: '๐จ',
+ },
+ // Placeholder for future templates
+ {
+ id: 'classic-tee-front',
+ name: 'Classic Tee - Front',
+ description: 'Standard front chest print',
+ thumbnail: '๐',
+ disabled: true,
+ },
+ {
+ id: 'classic-tee-back',
+ name: 'Classic Tee - Back',
+ description: 'Full back print',
+ thumbnail: '๐',
+ disabled: true,
+ },
+ {
+ id: 'all-over',
+ name: 'All-Over Print',
+ description: 'Full front coverage',
+ thumbnail: '๐ฏ',
+ disabled: true,
+ },
+ ];
+
+ const handleSelectTemplate = (template) => {
+ if (template.disabled) {
+ alert('This template will be available in a future update');
+ return;
+ }
+ onAddTemplate(template.id);
+ };
+
+ return (
+
+
+ Templates
+
+
+
+ Choose a template to constrain your design to specific print zones. Templates will be available in a future update.
+
+
+
+ {templates.map((template) => (
+
+ ))}
+
+
+ );
+}
diff --git a/client/src/components/sidebar/TextTab.jsx b/client/src/components/sidebar/TextTab.jsx
new file mode 100644
index 0000000..b96baac
--- /dev/null
+++ b/client/src/components/sidebar/TextTab.jsx
@@ -0,0 +1,199 @@
+import { useState } from 'react';
+import { FONTS } from '../../constants/fonts';
+
+export function TextTab({ onAddText }) {
+ const [text, setText] = useState('Your text here');
+ const [fontFamily, setFontFamily] = useState('Roboto');
+ const [fontSize, setFontSize] = useState(48);
+ const [fill, setFill] = useState('#0f172a');
+
+ const handleAddText = () => {
+ onAddText({
+ type: 'text',
+ x: 150,
+ y: 150,
+ text,
+ fontFamily,
+ fontSize,
+ fill,
+ rotation: 0,
+ });
+ };
+
+ return (
+
+
+ Add Text
+
+
+ {/* Text input */}
+
+
+
+
+ {/* Font selector */}
+
+
+
+
+
+ {/* Font size */}
+
+
+ setFontSize(parseInt(e.target.value, 10))}
+ style={{ width: '100%' }}
+ />
+
+
+ {/* Color picker */}
+
+
+
+ setFill(e.target.value)}
+ style={{
+ width: '40px',
+ height: '40px',
+ border: `1px solid var(--border)`,
+ borderRadius: 'var(--radius-sm)',
+ cursor: 'pointer',
+ padding: '2px',
+ }}
+ />
+ setFill(e.target.value)}
+ style={{
+ flex: 1,
+ padding: '0.75rem',
+ border: `1px solid var(--border)`,
+ borderRadius: 'var(--radius-md)',
+ fontSize: '13px',
+ fontFamily: 'var(--font-mono)',
+ }}
+ />
+
+
+
+ {/* Preview */}
+
+
+ {/* Add Text button */}
+
+
+ );
+}
diff --git a/client/src/components/sidebar/UploadTab.jsx b/client/src/components/sidebar/UploadTab.jsx
new file mode 100644
index 0000000..1526708
--- /dev/null
+++ b/client/src/components/sidebar/UploadTab.jsx
@@ -0,0 +1,150 @@
+import { useRef, useState } from 'react';
+
+export function UploadTab({ onAddImage }) {
+ const fileInputRef = useRef(null);
+ const [isDragging, setIsDragging] = useState(false);
+ const [isUploading, setIsUploading] = useState(false);
+
+ const handleFiles = async (files) => {
+ const file = files[0];
+ if (!file) return;
+
+ // Validate file type
+ const validTypes = ['image/jpeg', 'image/png', 'image/webp'];
+ if (!validTypes.includes(file.type)) {
+ alert('Please upload a JPEG, PNG, or WebP image');
+ return;
+ }
+
+ // Validate file size (20MB)
+ if (file.size > 20 * 1024 * 1024) {
+ alert('File size must be under 20MB');
+ return;
+ }
+
+ setIsUploading(true);
+
+ try {
+ const formData = new FormData();
+ formData.append('image', file);
+
+ const response = await fetch('/api/upload', {
+ method: 'POST',
+ body: formData,
+ });
+
+ if (!response.ok) {
+ throw new Error('Upload failed');
+ }
+
+ const data = await response.json();
+
+ // Add the uploaded image to canvas (use preview for canvas)
+ onAddImage({
+ type: 'image',
+ x: 75,
+ y: 75,
+ width: 150,
+ height: 150,
+ rotation: 0,
+ src: data.preview.url,
+ originalUrl: data.original.url,
+ });
+ } catch (error) {
+ console.error('Upload error:', error);
+ alert('Failed to upload image. Please try again.');
+ } finally {
+ setIsUploading(false);
+ }
+ };
+
+ const handleDragOver = (e) => {
+ e.preventDefault();
+ setIsDragging(true);
+ };
+
+ const handleDragLeave = (e) => {
+ e.preventDefault();
+ setIsDragging(false);
+ };
+
+ const handleDrop = (e) => {
+ e.preventDefault();
+ setIsDragging(false);
+ handleFiles(e.dataTransfer.files);
+ };
+
+ const handleClick = () => {
+ fileInputRef.current?.click();
+ };
+
+ const handleFileChange = (e) => {
+ handleFiles(e.target.files);
+ };
+
+ return (
+
+
+ Upload Image
+
+
+
+
๐
+
+ Click to upload or drag and drop
+
+
+ JPEG, PNG, WebP (max 20MB)
+
+
+
+
+
+ {isUploading && (
+
+ Uploading...
+
+ )}
+
+
+ Tip: After uploading, you can remove the background from your image using the background removal tool.
+
+
+ );
+}
diff --git a/client/src/components/sidebar/index.js b/client/src/components/sidebar/index.js
new file mode 100644
index 0000000..d983c43
--- /dev/null
+++ b/client/src/components/sidebar/index.js
@@ -0,0 +1,5 @@
+export { Sidebar } from './Sidebar';
+export { UploadTab } from './UploadTab';
+export { StickersTab } from './StickersTab';
+export { TextTab } from './TextTab';
+export { TemplatesTab } from './TemplatesTab';
diff --git a/client/src/constants/fonts.js b/client/src/constants/fonts.js
new file mode 100644
index 0000000..5eb8d12
--- /dev/null
+++ b/client/src/constants/fonts.js
@@ -0,0 +1,22 @@
+export const FONTS = [
+ { name: 'Roboto', family: 'Roboto' },
+ { name: 'Open Sans', family: 'Open Sans' },
+ { name: 'Lato', family: 'Lato' },
+ { name: 'Montserrat', family: 'Montserrat' },
+ { name: 'Oswald', family: 'Oswald' },
+ { name: 'Raleway', family: 'Raleway' },
+ { name: 'Poppins', family: 'Poppins' },
+ { name: 'Roboto Condensed', family: 'Roboto Condensed' },
+ { name: 'Source Sans 3', family: 'Source Sans 3' },
+ { name: 'Roboto Slab', family: 'Roboto Slab' },
+ { name: 'Merriweather', family: 'Merriweather' },
+ { name: 'Ubuntu', family: 'Ubuntu' },
+ { name: 'Playfair Display', family: 'Playfair Display' },
+ { name: 'Nunito', family: 'Nunito' },
+ { name: 'Rubik', family: 'Rubik' },
+ { name: 'Work Sans', family: 'Work Sans' },
+ { name: 'Lora', family: 'Lora' },
+ { name: 'Fira Sans', family: 'Fira Sans' },
+ { name: 'Barlow', family: 'Barlow' },
+ { name: 'Bebas Neue', family: 'Bebas Neue' },
+];
diff --git a/client/src/constants/stickers.js b/client/src/constants/stickers.js
new file mode 100644
index 0000000..a0fda73
--- /dev/null
+++ b/client/src/constants/stickers.js
@@ -0,0 +1,159 @@
+export const STICKER_CATEGORIES = ['all', 'faces', 'animals', 'food', 'sports', 'nature', 'objects'];
+
+export const STICKERS = [
+ // Faces
+ { emoji: '๐', category: 'faces' },
+ { emoji: '๐', category: 'faces' },
+ { emoji: '๐', category: 'faces' },
+ { emoji: '๐คฃ', category: 'faces' },
+ { emoji: '๐', category: 'faces' },
+ { emoji: '๐', category: 'faces' },
+ { emoji: '๐
', category: 'faces' },
+ { emoji: '๐', category: 'faces' },
+ { emoji: '๐', category: 'faces' },
+ { emoji: '๐', category: 'faces' },
+ { emoji: '๐', category: 'faces' },
+ { emoji: '๐', category: 'faces' },
+ { emoji: '๐', category: 'faces' },
+ { emoji: '๐', category: 'faces' },
+ { emoji: '๐ฅฐ', category: 'faces' },
+ { emoji: '๐', category: 'faces' },
+ { emoji: '๐ค', category: 'faces' },
+ { emoji: '๐คจ', category: 'faces' },
+ { emoji: '๐ง', category: 'faces' },
+ { emoji: '๐ค', category: 'faces' },
+ { emoji: '๐', category: 'faces' },
+ { emoji: '๐ค ', category: 'faces' },
+ { emoji: '๐ฅณ', category: 'faces' },
+ { emoji: '๐คฉ', category: 'faces' },
+
+ // Animals
+ { emoji: '๐ถ', category: 'animals' },
+ { emoji: '๐ฑ', category: 'animals' },
+ { emoji: '๐ญ', category: 'animals' },
+ { emoji: '๐น', category: 'animals' },
+ { emoji: '๐ฐ', category: 'animals' },
+ { emoji: '๐ฆ', category: 'animals' },
+ { emoji: '๐ป', category: 'animals' },
+ { emoji: '๐ผ', category: 'animals' },
+ { emoji: '๐จ', category: 'animals' },
+ { emoji: '๐ฏ', category: 'animals' },
+ { emoji: '๐ฆ', category: 'animals' },
+ { emoji: '๐ฎ', category: 'animals' },
+ { emoji: '๐ท', category: 'animals' },
+ { emoji: '๐ธ', category: 'animals' },
+ { emoji: '๐ต', category: 'animals' },
+ { emoji: '๐', category: 'animals' },
+ { emoji: '๐ง', category: 'animals' },
+ { emoji: '๐ฆ', category: 'animals' },
+ { emoji: '๐ฆ', category: 'animals' },
+ { emoji: '๐', category: 'animals' },
+ { emoji: '๐ฆ', category: 'animals' },
+ { emoji: '๐', category: 'animals' },
+ { emoji: '๐', category: 'animals' },
+ { emoji: '๐ข', category: 'animals' },
+
+ // Food
+ { emoji: '๐', category: 'food' },
+ { emoji: '๐', category: 'food' },
+ { emoji: '๐', category: 'food' },
+ { emoji: '๐', category: 'food' },
+ { emoji: '๐', category: 'food' },
+ { emoji: '๐', category: 'food' },
+ { emoji: '๐', category: 'food' },
+ { emoji: '๐', category: 'food' },
+ { emoji: '๐', category: 'food' },
+ { emoji: '๐', category: 'food' },
+ { emoji: '๐', category: 'food' },
+ { emoji: '๐', category: 'food' },
+ { emoji: '๐ฅฅ', category: 'food' },
+ { emoji: '๐ฅ', category: 'food' },
+ { emoji: '๐
', category: 'food' },
+ { emoji: '๐ฅ', category: 'food' },
+ { emoji: '๐', category: 'food' },
+ { emoji: '๐ฅ', category: 'food' },
+ { emoji: '๐ฅ', category: 'food' },
+ { emoji: '๐ฝ', category: 'food' },
+ { emoji: '๐', category: 'food' },
+ { emoji: '๐', category: 'food' },
+ { emoji: '๐', category: 'food' },
+ { emoji: '๐ญ', category: 'food' },
+
+ // Sports
+ { emoji: 'โฝ', category: 'sports' },
+ { emoji: '๐', category: 'sports' },
+ { emoji: '๐', category: 'sports' },
+ { emoji: 'โพ', category: 'sports' },
+ { emoji: '๐ฅ', category: 'sports' },
+ { emoji: '๐พ', category: 'sports' },
+ { emoji: '๐', category: 'sports' },
+ { emoji: '๐', category: 'sports' },
+ { emoji: '๐ฑ', category: 'sports' },
+ { emoji: '๐', category: 'sports' },
+ { emoji: '๐ธ', category: 'sports' },
+ { emoji: '๐ฅ
', category: 'sports' },
+ { emoji: 'โณ', category: 'sports' },
+ { emoji: '๐ฅ', category: 'sports' },
+ { emoji: '๐ฅ', category: 'sports' },
+ { emoji: '๐ฏ', category: 'sports' },
+ { emoji: 'โน๏ธ', category: 'sports' },
+ { emoji: '๐ด', category: 'sports' },
+ { emoji: '๐', category: 'sports' },
+ { emoji: '๐ฅ', category: 'sports' },
+ { emoji: '๐ฅ', category: 'sports' },
+ { emoji: '๐ฅ', category: 'sports' },
+ { emoji: '๐
', category: 'sports' },
+ { emoji: '๐๏ธ', category: 'sports' },
+
+ // Nature
+ { emoji: '๐ธ', category: 'nature' },
+ { emoji: '๐', category: 'nature' },
+ { emoji: '๐น', category: 'nature' },
+ { emoji: '๐บ', category: 'nature' },
+ { emoji: '๐ป', category: 'nature' },
+ { emoji: '๐ผ', category: 'nature' },
+ { emoji: '๐ท', category: 'nature' },
+ { emoji: '๐ฑ', category: 'nature' },
+ { emoji: '๐ฒ', category: 'nature' },
+ { emoji: '๐ณ', category: 'nature' },
+ { emoji: '๐ด', category: 'nature' },
+ { emoji: '๐ต', category: 'nature' },
+ { emoji: '๐พ', category: 'nature' },
+ { emoji: '๐ฟ', category: 'nature' },
+ { emoji: 'โ๏ธ', category: 'nature' },
+ { emoji: '๐', category: 'nature' },
+ { emoji: '๐', category: 'nature' },
+ { emoji: '๐', category: 'nature' },
+ { emoji: '๐', category: 'nature' },
+ { emoji: '๐', category: 'nature' },
+ { emoji: 'โ๏ธ', category: 'nature' },
+ { emoji: '๐', category: 'nature' },
+ { emoji: 'โญ', category: 'nature' },
+ { emoji: '๐ฅ', category: 'nature' },
+
+ // Objects
+ { emoji: 'โค๏ธ', category: 'objects' },
+ { emoji: '๐', category: 'objects' },
+ { emoji: '๐', category: 'objects' },
+ { emoji: '๐', category: 'objects' },
+ { emoji: '๐', category: 'objects' },
+ { emoji: '๐งก', category: 'objects' },
+ { emoji: '๐', category: 'objects' },
+ { emoji: '๐ฏ', category: 'objects' },
+ { emoji: 'โจ', category: 'objects' },
+ { emoji: '๐', category: 'objects' },
+ { emoji: '๐ซ', category: 'objects' },
+ { emoji: '๐ต', category: 'objects' },
+ { emoji: '๐ถ', category: 'objects' },
+ { emoji: '๐ธ', category: 'objects' },
+ { emoji: '๐บ', category: 'objects' },
+ { emoji: '๐ท', category: 'objects' },
+ { emoji: '๐น', category: 'objects' },
+ { emoji: '๐', category: 'objects' },
+ { emoji: '๐', category: 'objects' },
+ { emoji: '๐', category: 'objects' },
+ { emoji: '๐', category: 'objects' },
+ { emoji: '๐', category: 'objects' },
+ { emoji: '๐', category: 'objects' },
+ { emoji: '๐ฎ', category: 'objects' },
+];
diff --git a/client/src/index.css b/client/src/index.css
index fb6de52..6f8251f 100644
--- a/client/src/index.css
+++ b/client/src/index.css
@@ -56,11 +56,54 @@ body {
#root {
min-height: 100vh;
+ display: flex;
+ flex-direction: column;
+}
+
+/* Three-column layout */
+.editor-layout {
+ display: flex;
+ flex: 1;
+ overflow: hidden;
+}
+
+.sidebar {
+ width: 320px;
+ background: var(--bg-secondary);
+ border-right: 1px solid var(--border);
+ display: flex;
+ flex-direction: column;
+ overflow: hidden;
+}
+
+.canvas-area {
+ flex: 1;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ background: var(--bg-tertiary);
+ overflow: auto;
+ padding: 2rem;
+}
+
+.properties-panel {
+ width: 280px;
+ background: var(--bg-secondary);
+ border-left: 1px solid var(--border);
+ display: flex;
+ flex-direction: column;
+ overflow: hidden;
}
button {
font-family: inherit;
cursor: pointer;
+ outline: none;
+}
+
+button:focus-visible {
+ box-shadow: 0 0 0 2px var(--bg-primary), 0 0 0 4px var(--accent);
}
input, textarea, select {