A web app to help you design things, local, offline, on device. In your browser.
TypeScript
53.1%
JSON
45.6%
CSS
0.9%
Markdown
0.3%
JavaScript
0.1%
'use client';
import React, { useRef, useState } from 'react';
import { Group, Text, Rect } from 'react-konva';
import type Konva from 'konva';
import type { Layer, TextLayerData } from '@/types';
import { useEditorStore } from '@/lib/store/editor-store';
interface TextLayerProps {
id: string;
layer: Layer;
isSelected: boolean;
onClick: (e: Konva.KonvaEventObject<MouseEvent | TouchEvent>) => void;
onTransformEnd: (node: Konva.Node) => void;
onDragStart?: () => void;
onDragMove?: (node: Konva.Node) => void;
onDragEnd: (node: Konva.Node) => void;
}
export function TextLayer({
id,
layer,
isSelected,
onClick,
onTransformEnd,
onDragStart,
onDragMove,
onDragEnd,
}: TextLayerProps) {
const groupRef = useRef<Konva.Group>(null);
const textRef = useRef<Konva.Text>(null);
const [isEditing, setIsEditing] = useState(false);
const { updateLayerData, saveToHistory } = useEditorStore();
const data = layer.data as TextLayerData;
// Handle double-click for text editing
const handleDblClick = () => {
if (layer.locked) return;
setIsEditing(true);
const textNode = textRef.current;
const stage = textNode?.getStage();
if (!textNode || !stage) return;
// Create editable textarea
const textPosition = textNode.absolutePosition();
const stageContainer = stage.container();
const containerRect = stageContainer.getBoundingClientRect();
const scale = stage.scaleX();
const textarea = document.createElement('textarea');
document.body.appendChild(textarea);
const originalText = data.text;
textarea.value = data.text;
textarea.style.position = 'absolute';
textarea.style.top = `${containerRect.top + textPosition.y * scale}px`;
textarea.style.left = `${containerRect.left + textPosition.x * scale}px`;
textarea.style.width = `${layer.size.width * scale}px`;
textarea.style.height = `${layer.size.height * scale}px`;
textarea.style.fontSize = `${data.fontSize * scale}px`;
textarea.style.fontFamily = data.fontFamily;
textarea.style.fontWeight = String(data.fontWeight);
textarea.style.color = data.color;
textarea.style.background = 'rgba(0, 0, 0, 0.8)';
textarea.style.border = '2px solid #4f46e5';
textarea.style.borderRadius = '4px';
textarea.style.outline = 'none';
textarea.style.resize = 'none';
textarea.style.lineHeight = String(data.lineHeight);
textarea.style.letterSpacing = `${data.letterSpacing * scale}px`;
textarea.style.textAlign = data.align;
textarea.style.zIndex = '1000';
textarea.style.padding = '4px';
textarea.style.overflow = 'hidden';
textarea.style.transformOrigin = 'top left';
textarea.focus();
textarea.select();
const saveAndClose = () => {
const newText = textarea.value;
if (newText !== originalText) {
updateLayerData(layer.id, { text: newText });
saveToHistory('Edited text');
}
textarea.remove();
setIsEditing(false);
};
const cancelAndClose = () => {
textarea.remove();
setIsEditing(false);
};
textarea.addEventListener('blur', saveAndClose);
textarea.addEventListener('keydown', (e) => {
if (e.key === 'Escape') {
e.preventDefault();
cancelAndClose();
}
// Enter without shift saves, Shift+Enter adds newline
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
saveAndClose();
}
});
};
// Build shadow config
const shadowConfig = data.shadow
? {
shadowColor: data.shadow.color,
shadowBlur: data.shadow.blur,
shadowOffsetX: data.shadow.offsetX,
shadowOffsetY: data.shadow.offsetY,
}
: {};
return (
<Group
ref={groupRef}
id={id}
x={layer.position.x}
y={layer.position.y}
rotation={layer.rotation}
opacity={layer.opacity}
draggable={!layer.locked}
onClick={onClick}
onTap={onClick}
onDblClick={handleDblClick}
onDblTap={handleDblClick}
onDragStart={onDragStart}
onDragMove={(e) => onDragMove?.(e.target)}
onDragEnd={(e) => onDragEnd(e.target)}
onTransformEnd={(e) => onTransformEnd(e.target)}
>
{/* Background rect for selection */}
{isSelected && !isEditing && (
<Rect
x={-4}
y={-4}
width={layer.size.width + 8}
height={layer.size.height + 8}
fill="transparent"
stroke="rgba(79, 70, 229, 0.3)"
strokeWidth={1}
dash={[4, 4]}
listening={false}
/>
)}
<Text
ref={textRef}
text={data.text}
width={layer.size.width}
height={layer.size.height}
fontFamily={data.fontFamily}
fontSize={data.fontSize}
fontStyle={data.fontWeight >= 600 ? 'bold' : 'normal'}
fill={data.gradient ? undefined : data.color}
fillLinearGradientStartPoint={
data.gradient?.type === 'linear' ? { x: 0, y: 0 } : undefined
}
fillLinearGradientEndPoint={
data.gradient?.type === 'linear'
? { x: layer.size.width, y: 0 }
: undefined
}
fillLinearGradientColorStops={
data.gradient?.type === 'linear'
? data.gradient.stops.flatMap((s) => [s.offset, s.color])
: undefined
}
align={data.align}
lineHeight={data.lineHeight}
letterSpacing={data.letterSpacing}
wrap="word"
ellipsis={true}
visible={!isEditing}
{...shadowConfig}
/>
</Group>
);
}
About
A web app to help you design things, local, offline, on device. In your browser.
0 stars
0 forks