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,293 @@
|
||||
import { create } from 'zustand';
|
||||
import { immer } from 'zustand/middleware/immer';
|
||||
import { persist } from 'zustand/middleware';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import type {
|
||||
Snap,
|
||||
CanvasElement,
|
||||
CodeElement,
|
||||
TextElement,
|
||||
ArrowElement,
|
||||
Background,
|
||||
CanvasMeta,
|
||||
} from '../types';
|
||||
|
||||
interface HistoryState {
|
||||
past: Snap[];
|
||||
future: Snap[];
|
||||
}
|
||||
|
||||
interface CanvasState {
|
||||
snap: Snap;
|
||||
selectedElementId: string | null;
|
||||
zoom: number;
|
||||
showGrid: boolean;
|
||||
tool: 'select' | 'code' | 'text' | 'arrow';
|
||||
history: HistoryState;
|
||||
|
||||
// Actions
|
||||
setSnap: (snap: Snap) => void;
|
||||
updateMeta: (meta: Partial<CanvasMeta>) => void;
|
||||
setBackground: (background: Partial<Background>) => void;
|
||||
|
||||
addElement: (element: CanvasElement) => void;
|
||||
updateElement: (id: string, updates: Partial<CanvasElement>) => void;
|
||||
deleteElement: (id: string) => void;
|
||||
duplicateElement: (id: string) => void;
|
||||
|
||||
selectElement: (id: string | null) => void;
|
||||
setZoom: (zoom: number) => void;
|
||||
setShowGrid: (show: boolean) => void;
|
||||
setTool: (tool: 'select' | 'code' | 'text' | 'arrow') => void;
|
||||
|
||||
moveElementUp: (id: string) => void;
|
||||
moveElementDown: (id: string) => void;
|
||||
|
||||
undo: () => void;
|
||||
redo: () => void;
|
||||
saveToHistory: () => void;
|
||||
|
||||
newSnap: (meta: CanvasMeta) => void;
|
||||
exportSnap: () => string;
|
||||
importSnap: (json: string) => void;
|
||||
}
|
||||
|
||||
const defaultSnap: Snap = {
|
||||
version: '1.0.0',
|
||||
meta: {
|
||||
title: 'Untitled',
|
||||
aspect: '16:9',
|
||||
width: 1920,
|
||||
height: 1080,
|
||||
},
|
||||
background: {
|
||||
type: 'gradient',
|
||||
solid: { color: '#101022' },
|
||||
gradient: { from: '#101022', to: '#1f1f3a', angle: 135 },
|
||||
},
|
||||
elements: [],
|
||||
};
|
||||
|
||||
export const useCanvasStore = create<CanvasState>()(
|
||||
persist(
|
||||
immer((set, get) => ({
|
||||
snap: defaultSnap,
|
||||
selectedElementId: null,
|
||||
zoom: 0.5,
|
||||
showGrid: false,
|
||||
tool: 'select',
|
||||
history: { past: [], future: [] },
|
||||
|
||||
setSnap: (snap) => set((state) => {
|
||||
state.snap = snap;
|
||||
}),
|
||||
|
||||
updateMeta: (meta) => set((state) => {
|
||||
state.snap.meta = { ...state.snap.meta, ...meta };
|
||||
}),
|
||||
|
||||
setBackground: (background) => set((state) => {
|
||||
state.snap.background = { ...state.snap.background, ...background };
|
||||
}),
|
||||
|
||||
addElement: (element) => set((state) => {
|
||||
get().saveToHistory();
|
||||
state.snap.elements.push(element);
|
||||
state.selectedElementId = element.id;
|
||||
state.tool = 'select';
|
||||
}),
|
||||
|
||||
updateElement: (id, updates) => set((state) => {
|
||||
const index = state.snap.elements.findIndex((el) => el.id === id);
|
||||
if (index !== -1) {
|
||||
state.snap.elements[index] = { ...state.snap.elements[index], ...updates } as CanvasElement;
|
||||
}
|
||||
}),
|
||||
|
||||
deleteElement: (id) => set((state) => {
|
||||
get().saveToHistory();
|
||||
state.snap.elements = state.snap.elements.filter((el) => el.id !== id);
|
||||
if (state.selectedElementId === id) {
|
||||
state.selectedElementId = null;
|
||||
}
|
||||
}),
|
||||
|
||||
duplicateElement: (id) => set((state) => {
|
||||
const element = state.snap.elements.find((el) => el.id === id);
|
||||
if (element) {
|
||||
get().saveToHistory();
|
||||
const newElement = {
|
||||
...JSON.parse(JSON.stringify(element)),
|
||||
id: uuidv4(),
|
||||
x: element.x + 20,
|
||||
y: element.y + 20,
|
||||
};
|
||||
state.snap.elements.push(newElement);
|
||||
state.selectedElementId = newElement.id;
|
||||
}
|
||||
}),
|
||||
|
||||
selectElement: (id) => set((state) => {
|
||||
state.selectedElementId = id;
|
||||
}),
|
||||
|
||||
setZoom: (zoom) => set((state) => {
|
||||
state.zoom = Math.max(0.25, Math.min(4, zoom));
|
||||
}),
|
||||
|
||||
setShowGrid: (show) => set((state) => {
|
||||
state.showGrid = show;
|
||||
}),
|
||||
|
||||
setTool: (tool) => set((state) => {
|
||||
state.tool = tool;
|
||||
if (tool !== 'select') {
|
||||
state.selectedElementId = null;
|
||||
}
|
||||
}),
|
||||
|
||||
moveElementUp: (id) => set((state) => {
|
||||
const index = state.snap.elements.findIndex((el) => el.id === id);
|
||||
if (index < state.snap.elements.length - 1) {
|
||||
const temp = state.snap.elements[index];
|
||||
state.snap.elements[index] = state.snap.elements[index + 1];
|
||||
state.snap.elements[index + 1] = temp;
|
||||
}
|
||||
}),
|
||||
|
||||
moveElementDown: (id) => set((state) => {
|
||||
const index = state.snap.elements.findIndex((el) => el.id === id);
|
||||
if (index > 0) {
|
||||
const temp = state.snap.elements[index];
|
||||
state.snap.elements[index] = state.snap.elements[index - 1];
|
||||
state.snap.elements[index - 1] = temp;
|
||||
}
|
||||
}),
|
||||
|
||||
saveToHistory: () => set((state) => {
|
||||
state.history.past.push(JSON.parse(JSON.stringify(state.snap)));
|
||||
state.history.future = [];
|
||||
if (state.history.past.length > 50) {
|
||||
state.history.past.shift();
|
||||
}
|
||||
}),
|
||||
|
||||
undo: () => set((state) => {
|
||||
if (state.history.past.length > 0) {
|
||||
const previous = state.history.past.pop()!;
|
||||
state.history.future.push(JSON.parse(JSON.stringify(state.snap)));
|
||||
state.snap = previous;
|
||||
state.selectedElementId = null;
|
||||
}
|
||||
}),
|
||||
|
||||
redo: () => set((state) => {
|
||||
if (state.history.future.length > 0) {
|
||||
const next = state.history.future.pop()!;
|
||||
state.history.past.push(JSON.parse(JSON.stringify(state.snap)));
|
||||
state.snap = next;
|
||||
state.selectedElementId = null;
|
||||
}
|
||||
}),
|
||||
|
||||
newSnap: (meta) => set((state) => {
|
||||
state.snap = {
|
||||
...defaultSnap,
|
||||
meta: { ...defaultSnap.meta, ...meta },
|
||||
};
|
||||
state.selectedElementId = null;
|
||||
state.history = { past: [], future: [] };
|
||||
}),
|
||||
|
||||
exportSnap: () => {
|
||||
return JSON.stringify(get().snap, null, 2);
|
||||
},
|
||||
|
||||
importSnap: (json) => {
|
||||
try {
|
||||
const snap = JSON.parse(json) as Snap;
|
||||
set((state) => {
|
||||
state.snap = snap;
|
||||
state.selectedElementId = null;
|
||||
state.history = { past: [], future: [] };
|
||||
});
|
||||
} catch (e) {
|
||||
console.error('Failed to import snap:', e);
|
||||
}
|
||||
},
|
||||
})),
|
||||
{
|
||||
name: 'code-canvas-storage',
|
||||
partialize: (state) => ({ snap: state.snap }),
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
// Helper functions to create elements
|
||||
export const createCodeElement = (x: number, y: number): CodeElement => ({
|
||||
id: uuidv4(),
|
||||
type: 'code',
|
||||
x,
|
||||
y,
|
||||
width: 600,
|
||||
height: 300,
|
||||
rotation: 0,
|
||||
locked: false,
|
||||
visible: true,
|
||||
props: {
|
||||
code: '// Your code here\nconsole.log("Hello, World!");',
|
||||
language: 'javascript',
|
||||
theme: 'dark',
|
||||
fontFamily: 'JetBrains Mono',
|
||||
fontSize: 14,
|
||||
lineHeight: 1.5,
|
||||
lineNumbers: true,
|
||||
highlights: [],
|
||||
padding: 24,
|
||||
cornerRadius: 12,
|
||||
shadow: { blur: 24, spread: 0, color: 'rgba(0,0,0,0.4)' },
|
||||
},
|
||||
});
|
||||
|
||||
export const createTextElement = (x: number, y: number): TextElement => ({
|
||||
id: uuidv4(),
|
||||
type: 'text',
|
||||
x,
|
||||
y,
|
||||
rotation: 0,
|
||||
locked: false,
|
||||
visible: true,
|
||||
props: {
|
||||
text: 'Your text here',
|
||||
fontFamily: 'Inter',
|
||||
fontSize: 24,
|
||||
color: '#ffffff',
|
||||
bold: false,
|
||||
italic: false,
|
||||
underline: false,
|
||||
align: 'left',
|
||||
background: null,
|
||||
padding: 8,
|
||||
cornerRadius: 4,
|
||||
},
|
||||
});
|
||||
|
||||
export const createArrowElement = (x: number, y: number): ArrowElement => ({
|
||||
id: uuidv4(),
|
||||
type: 'arrow',
|
||||
x: 0,
|
||||
y: 0,
|
||||
rotation: 0,
|
||||
locked: false,
|
||||
visible: true,
|
||||
points: [
|
||||
{ x, y },
|
||||
{ x: x + 150, y: y + 80 },
|
||||
],
|
||||
props: {
|
||||
style: 'straight',
|
||||
color: '#60a5fa',
|
||||
thickness: 3,
|
||||
head: 'filled',
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user