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,214 @@
|
||||
import React, { useRef, useState, useEffect, useCallback } from 'react';
|
||||
import { Stage, Layer, Rect, Line } from 'react-konva';
|
||||
import type Konva from 'konva';
|
||||
import { useCanvasStore, createCodeElement, createTextElement, createArrowElement } from '../store/canvasStore';
|
||||
import CodeBlock from './elements/CodeBlock';
|
||||
import TextBlock from './elements/TextBlock';
|
||||
import Arrow from './elements/Arrow';
|
||||
import type { CodeElement, TextElement, ArrowElement } from '../types';
|
||||
|
||||
interface CanvasProps {
|
||||
stageRef: React.RefObject<Konva.Stage | null>;
|
||||
}
|
||||
|
||||
const Canvas: React.FC<CanvasProps> = ({ stageRef }) => {
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const [dimensions, setDimensions] = useState({ width: window.innerWidth, height: window.innerHeight - 120 });
|
||||
const {
|
||||
snap,
|
||||
zoom,
|
||||
showGrid,
|
||||
tool,
|
||||
selectedElementId,
|
||||
selectElement,
|
||||
addElement,
|
||||
updateElement,
|
||||
} = useCanvasStore();
|
||||
|
||||
const { width, height } = snap.meta;
|
||||
const { background } = snap;
|
||||
|
||||
// Handle resize
|
||||
useEffect(() => {
|
||||
const updateDimensions = () => {
|
||||
if (containerRef.current) {
|
||||
setDimensions({
|
||||
width: containerRef.current.offsetWidth,
|
||||
height: containerRef.current.offsetHeight,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
updateDimensions();
|
||||
window.addEventListener('resize', updateDimensions);
|
||||
return () => window.removeEventListener('resize', updateDimensions);
|
||||
}, []);
|
||||
|
||||
// Calculate stage position to center the canvas
|
||||
const getStagePosition = useCallback(() => {
|
||||
const scaledWidth = width * zoom;
|
||||
const scaledHeight = height * zoom;
|
||||
return {
|
||||
x: Math.max(20, (dimensions.width - scaledWidth) / 2),
|
||||
y: Math.max(20, (dimensions.height - scaledHeight) / 2),
|
||||
};
|
||||
}, [width, height, zoom, dimensions]);
|
||||
|
||||
const handleStageClick = (e: Konva.KonvaEventObject<MouseEvent>) => {
|
||||
const clickedOnEmpty = e.target === e.target.getStage() || e.target.name() === 'background';
|
||||
|
||||
if (clickedOnEmpty) {
|
||||
const stage = e.target.getStage();
|
||||
if (!stage) return;
|
||||
|
||||
const pos = stage.getPointerPosition();
|
||||
if (!pos) return;
|
||||
|
||||
// Convert screen position to canvas position
|
||||
const stagePos = getStagePosition();
|
||||
const canvasX = (pos.x - stagePos.x) / zoom;
|
||||
const canvasY = (pos.y - stagePos.y) / zoom;
|
||||
|
||||
if (tool === 'code') {
|
||||
addElement(createCodeElement(canvasX - 300, canvasY - 150));
|
||||
} else if (tool === 'text') {
|
||||
addElement(createTextElement(canvasX, canvasY));
|
||||
} else if (tool === 'arrow') {
|
||||
addElement(createArrowElement(canvasX, canvasY));
|
||||
} else {
|
||||
selectElement(null);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Background gradient
|
||||
const renderBackground = () => {
|
||||
if (background.type === 'gradient') {
|
||||
return (
|
||||
<Rect
|
||||
name="background"
|
||||
x={0}
|
||||
y={0}
|
||||
width={width}
|
||||
height={height}
|
||||
fillLinearGradientStartPoint={{ x: 0, y: 0 }}
|
||||
fillLinearGradientEndPoint={{
|
||||
x: width * Math.cos((background.gradient.angle * Math.PI) / 180),
|
||||
y: height * Math.sin((background.gradient.angle * Math.PI) / 180)
|
||||
}}
|
||||
fillLinearGradientColorStops={[0, background.gradient.from, 1, background.gradient.to]}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<Rect
|
||||
name="background"
|
||||
x={0}
|
||||
y={0}
|
||||
width={width}
|
||||
height={height}
|
||||
fill={background.solid.color}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
// Grid overlay
|
||||
const renderGrid = () => {
|
||||
if (!showGrid) return null;
|
||||
const gridSize = 50;
|
||||
const lines = [];
|
||||
|
||||
// Vertical lines
|
||||
for (let i = 0; i <= width; i += gridSize) {
|
||||
lines.push(
|
||||
<Line
|
||||
key={`v-${i}`}
|
||||
points={[i, 0, i, height]}
|
||||
stroke="rgba(255,255,255,0.1)"
|
||||
strokeWidth={1}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
// Horizontal lines
|
||||
for (let i = 0; i <= height; i += gridSize) {
|
||||
lines.push(
|
||||
<Line
|
||||
key={`h-${i}`}
|
||||
points={[0, i, width, i]}
|
||||
stroke="rgba(255,255,255,0.1)"
|
||||
strokeWidth={1}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return <>{lines}</>;
|
||||
};
|
||||
|
||||
const stagePosition = getStagePosition();
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={containerRef}
|
||||
className="flex-1 bg-neutral-900 overflow-hidden relative"
|
||||
style={{ cursor: tool !== 'select' ? 'crosshair' : 'default' }}
|
||||
>
|
||||
<Stage
|
||||
ref={stageRef}
|
||||
width={dimensions.width}
|
||||
height={dimensions.height}
|
||||
onClick={handleStageClick}
|
||||
x={stagePosition.x}
|
||||
y={stagePosition.y}
|
||||
scaleX={zoom}
|
||||
scaleY={zoom}
|
||||
>
|
||||
<Layer>
|
||||
{renderBackground()}
|
||||
{renderGrid()}
|
||||
|
||||
{snap.elements.map((element) => {
|
||||
if (!element.visible) return null;
|
||||
|
||||
switch (element.type) {
|
||||
case 'code':
|
||||
return (
|
||||
<CodeBlock
|
||||
key={element.id}
|
||||
element={element as CodeElement}
|
||||
isSelected={selectedElementId === element.id}
|
||||
onSelect={() => selectElement(element.id)}
|
||||
onChange={(updates: Partial<CodeElement>) => updateElement(element.id, updates)}
|
||||
/>
|
||||
);
|
||||
case 'text':
|
||||
return (
|
||||
<TextBlock
|
||||
key={element.id}
|
||||
element={element as TextElement}
|
||||
isSelected={selectedElementId === element.id}
|
||||
onSelect={() => selectElement(element.id)}
|
||||
onChange={(updates: Partial<TextElement>) => updateElement(element.id, updates)}
|
||||
/>
|
||||
);
|
||||
case 'arrow':
|
||||
return (
|
||||
<Arrow
|
||||
key={element.id}
|
||||
element={element as ArrowElement}
|
||||
isSelected={selectedElementId === element.id}
|
||||
onSelect={() => selectElement(element.id)}
|
||||
onChange={(updates: Partial<ArrowElement>) => updateElement(element.id, updates)}
|
||||
/>
|
||||
);
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
})}
|
||||
</Layer>
|
||||
</Stage>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Canvas;
|
||||
Reference in New Issue
Block a user