Skip to content

Commit a0b2e5b

Browse files
committed
Add element tree panel and arrow key movement for selected elements
1 parent ea03c3c commit a0b2e5b

4 files changed

Lines changed: 130 additions & 3 deletions

File tree

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
import { useEditorStore } from "../store/editor-store.ts";
2+
import { Type, Image, QrCode, Barcode, Square, Minus, ChevronUp, ChevronDown, Trash2 } from "lucide-react";
3+
import type { EditorElement } from "../store/types.ts";
4+
5+
const typeIcons: Record<EditorElement["type"], typeof Type> = {
6+
text: Type,
7+
image: Image,
8+
qrcode: QrCode,
9+
barcode: Barcode,
10+
rect: Square,
11+
line: Minus,
12+
};
13+
14+
function getElementLabel(el: EditorElement): string {
15+
switch (el.type) {
16+
case "text": {
17+
const text = (el.props as { text: string }).text;
18+
return text.length > 20 ? text.slice(0, 20) + "…" : text || "Empty text";
19+
}
20+
case "image":
21+
return "Image";
22+
case "qrcode":
23+
return "QR Code";
24+
case "barcode":
25+
return "Barcode";
26+
case "rect":
27+
return "Rectangle";
28+
case "line":
29+
return "Line";
30+
}
31+
}
32+
33+
export function ElementTree() {
34+
const elements = useEditorStore((s) => s.elements);
35+
const selectedId = useEditorStore((s) => s.selectedId);
36+
const setSelectedId = useEditorStore((s) => s.setSelectedId);
37+
const moveElement = useEditorStore((s) => s.moveElement);
38+
const removeElement = useEditorStore((s) => s.removeElement);
39+
40+
if (elements.length === 0) {
41+
return (
42+
<div className="px-3 py-2">
43+
<h3 className="text-xs font-semibold uppercase text-gray-400 dark:text-gray-500 mb-2">Elements</h3>
44+
<p className="text-xs text-gray-400 dark:text-gray-500">No elements yet</p>
45+
</div>
46+
);
47+
}
48+
49+
// Show in reverse order (top of z-stack first)
50+
const reversed = [...elements].reverse();
51+
52+
return (
53+
<div className="px-3 py-2">
54+
<h3 className="text-xs font-semibold uppercase text-gray-400 dark:text-gray-500 mb-2">Elements</h3>
55+
<div className="flex flex-col gap-0.5">
56+
{reversed.map((el) => {
57+
const Icon = typeIcons[el.type];
58+
const isSelected = el.id === selectedId;
59+
return (
60+
<div
61+
key={el.id}
62+
className={`flex items-center gap-1.5 px-2 py-1 rounded cursor-pointer text-xs group transition-colors ${
63+
isSelected
64+
? "bg-blue-100 dark:bg-blue-900/40 text-blue-700 dark:text-blue-300"
65+
: "hover:bg-gray-100 dark:hover:bg-gray-700/50 text-gray-600 dark:text-gray-400"
66+
}`}
67+
onClick={() => setSelectedId(isSelected ? null : el.id)}
68+
>
69+
<Icon size={13} className="shrink-0" />
70+
<span className="flex-1 truncate">{getElementLabel(el)}</span>
71+
<div className="flex items-center gap-0 opacity-0 group-hover:opacity-100 transition-opacity">
72+
<button
73+
className="p-0.5 rounded hover:bg-gray-200 dark:hover:bg-gray-600 cursor-pointer"
74+
onClick={(e) => { e.stopPropagation(); moveElement(el.id, "up"); }}
75+
title="Move up"
76+
>
77+
<ChevronUp size={12} />
78+
</button>
79+
<button
80+
className="p-0.5 rounded hover:bg-gray-200 dark:hover:bg-gray-600 cursor-pointer"
81+
onClick={(e) => { e.stopPropagation(); moveElement(el.id, "down"); }}
82+
title="Move down"
83+
>
84+
<ChevronDown size={12} />
85+
</button>
86+
<button
87+
className="p-0.5 rounded hover:bg-gray-200 dark:hover:bg-gray-600 text-red-400 hover:text-red-500 cursor-pointer"
88+
onClick={(e) => { e.stopPropagation(); removeElement(el.id); }}
89+
title="Delete"
90+
>
91+
<Trash2 size={12} />
92+
</button>
93+
</div>
94+
</div>
95+
);
96+
})}
97+
</div>
98+
</div>
99+
);
100+
}

packages/web/src/editor/label-editor.tsx

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import type Konva from "konva";
33
import { Canvas } from "./canvas.tsx";
44
import { Toolbar } from "./toolbar/toolbar.tsx";
55
import { PropertiesPanel } from "./properties/properties-panel.tsx";
6+
import { ElementTree } from "./element-tree.tsx";
67
import { PrinterPanel } from "../printer/printer-panel.tsx";
78
import { PrintSettingsPanel } from "../printer/print-settings-panel.tsx";
89
import { PrintButton } from "../printer/print-button.tsx";
@@ -30,7 +31,11 @@ export function LabelEditor() {
3031
<TemplateManager />
3132
</div>
3233
<Canvas ref={stageRef} />
33-
<PropertiesPanel />
34+
<div className="w-56 bg-white dark:bg-gray-800 border-l border-gray-200 dark:border-gray-700 flex flex-col overflow-y-auto">
35+
<ElementTree />
36+
<div className="border-t border-gray-200 dark:border-gray-700" />
37+
<PropertiesPanel />
38+
</div>
3439
</div>
3540
</div>
3641
);

packages/web/src/editor/properties/properties-panel.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ export function PropertiesPanel() {
2020

2121
if (!element) {
2222
return (
23-
<div className="w-56 bg-white dark:bg-gray-800 border-l border-gray-200 dark:border-gray-700 p-4">
23+
<div className="p-4">
2424
<p className="text-sm text-gray-400 dark:text-gray-500">Select an element to edit its properties</p>
2525
</div>
2626
);
@@ -29,7 +29,7 @@ export function PropertiesPanel() {
2929
const iconBtn = "p-1 rounded hover:bg-gray-200 dark:hover:bg-gray-700 text-gray-500 dark:text-gray-400 cursor-pointer";
3030

3131
return (
32-
<div className="w-56 bg-white dark:bg-gray-800 border-l border-gray-200 dark:border-gray-700 p-4 overflow-y-auto">
32+
<div className="p-4">
3333
<div className="flex items-center justify-between mb-3">
3434
<h3 className="text-sm font-semibold capitalize">{element.type}</h3>
3535
<div className="flex items-center gap-0.5">

packages/web/src/hooks/use-keyboard.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,28 @@ export function useKeyboard() {
3434
if (e.key === "Escape") {
3535
store.setSelectedId(null);
3636
}
37+
38+
if (store.selectedId && ["ArrowUp", "ArrowDown", "ArrowLeft", "ArrowRight"].includes(e.key)) {
39+
e.preventDefault();
40+
const step = e.shiftKey ? 10 : 1;
41+
const el = store.elements.find((el) => el.id === store.selectedId);
42+
if (!el) return;
43+
store.pushHistory();
44+
switch (e.key) {
45+
case "ArrowUp":
46+
store.updateElement(el.id, { y: el.y - step });
47+
break;
48+
case "ArrowDown":
49+
store.updateElement(el.id, { y: el.y + step });
50+
break;
51+
case "ArrowLeft":
52+
store.updateElement(el.id, { x: el.x - step });
53+
break;
54+
case "ArrowRight":
55+
store.updateElement(el.id, { x: el.x + step });
56+
break;
57+
}
58+
}
3759
};
3860

3961
window.addEventListener("keydown", handler);

0 commit comments

Comments
 (0)