feat: add Text node type to canvas

New canvas object Text with edit/create modal: text content (single
or multi-line), font (Inter, JetBrains Mono, Serif, System Sans),
size, color, border (style + width + color), background. Double-click
on node opens edit modal. Sidebar gets "Add Text" entry below "Add Zone".

Refs #124
This commit is contained in:
Pouzor
2026-05-10 15:58:35 +02:00
parent e672cb086a
commit 27fd78ece6
10 changed files with 654 additions and 4 deletions
@@ -27,12 +27,71 @@ describe('canvasStore', () => {
selectedNodeId: null,
selectedNodeIds: [],
editingGroupRectId: null,
editingTextId: null,
past: [],
future: [],
clipboard: [],
})
})
it('setEditingTextId sets and clears editing text id', () => {
const { setEditingTextId } = useCanvasStore.getState()
setEditingTextId('t1')
expect(useCanvasStore.getState().editingTextId).toBe('t1')
setEditingTextId(null)
expect(useCanvasStore.getState().editingTextId).toBeNull()
})
it('addNode supports text type with text_content and style', () => {
const { addNode } = useCanvasStore.getState()
const textNode: Node<NodeData> = {
id: 't1',
type: 'text',
position: { x: 50, y: 50 },
data: {
label: '',
type: 'text',
status: 'unknown',
services: [],
text_content: 'Hello world',
custom_colors: {
text_color: '#ffffff',
text_size: 24,
font: 'mono',
border_style: 'dashed',
border_width: 2,
border: '#00d4ff',
background: '#00000000',
},
},
width: 200,
height: 60,
}
addNode(textNode)
const stored = useCanvasStore.getState().nodes.find((n) => n.id === 't1')
expect(stored?.type).toBe('text')
expect(stored?.data.text_content).toBe('Hello world')
expect(stored?.data.custom_colors?.text_size).toBe(24)
expect(stored?.data.custom_colors?.border_style).toBe('dashed')
})
it('updateNode updates text_content and custom_colors on a text node', () => {
const { addNode, updateNode } = useCanvasStore.getState()
addNode({ ...makeNode('t1', { type: 'text', text_content: 'old' }), type: 'text' })
updateNode('t1', { text_content: 'new', custom_colors: { text_color: '#ff0000', text_size: 32 } })
const stored = useCanvasStore.getState().nodes.find((n) => n.id === 't1')
expect(stored?.data.text_content).toBe('new')
expect(stored?.data.custom_colors?.text_color).toBe('#ff0000')
expect(stored?.data.custom_colors?.text_size).toBe(32)
})
it('deleteNode removes a text node', () => {
const { addNode, deleteNode } = useCanvasStore.getState()
addNode({ ...makeNode('t1', { type: 'text' }), type: 'text' })
deleteNode('t1')
expect(useCanvasStore.getState().nodes).toHaveLength(0)
})
it('starts empty', () => {
const { nodes, edges, hasUnsavedChanges } = useCanvasStore.getState()
expect(nodes).toHaveLength(0)
@@ -750,6 +809,7 @@ describe('canvasStore — custom style apply', () => {
selectedNodeId: null,
selectedNodeIds: [],
editingGroupRectId: null,
editingTextId: null,
past: [],
future: [],
clipboard: [],
+5
View File
@@ -49,6 +49,8 @@ interface CanvasState {
setNodeZIndex: (id: string, zIndex: number) => void
editingGroupRectId: string | null
setEditingGroupRectId: (id: string | null) => void
editingTextId: string | null
setEditingTextId: (id: string | null) => void
createGroup: (nodeIds: string[], name: string) => void
ungroup: (groupId: string) => void
markSaved: () => void
@@ -71,6 +73,7 @@ export const useCanvasStore = create<CanvasState>((set) => ({
selectedNodeId: null,
selectedNodeIds: [],
editingGroupRectId: null,
editingTextId: null,
hideIp: false,
scanEventTs: 0,
fitViewPending: false,
@@ -350,6 +353,8 @@ export const useCanvasStore = create<CanvasState>((set) => ({
setEditingGroupRectId: (id) => set({ editingGroupRectId: id }),
setEditingTextId: (id) => set({ editingTextId: id }),
createGroup: (nodeIds, name) =>
set((state) => {
const PADDING_H = 24