|
| 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 | +} |
0 commit comments