From 2efbbf9bd29902ea15e072395802bfeee1762cc3 Mon Sep 17 00:00:00 2001 From: Agent Zero Date: Fri, 28 Aug 2026 23:20:11 +0200 Subject: [PATCH] task(F4): library providers - builtin catalogs (event/architecture/landscape 15 blocks each), user-library and global-library via LibraryProviderExtension, panel UI with existing Drop-Channel --- .../src/components/LibraryProviderPanel.tsx | 84 +++ frontend/src/components/RightSidebar.tsx | 23 + .../library/catalogs/architecture.json | 587 +++++++++++++++++ .../builtin/library/catalogs/event.json | 619 ++++++++++++++++++ .../builtin/library/catalogs/landscape.json | 406 ++++++++++++ frontend/src/plugins/builtin/library/index.ts | 24 + .../src/plugins/builtin/library/providers.ts | 161 +++++ frontend/src/plugins/index.ts | 4 + frontend/src/plugins/types.ts | 4 +- frontend/src/styles.css | 39 ++ frontend/tests/libraryProviders.test.tsx | 143 ++++ 11 files changed, 2093 insertions(+), 1 deletion(-) create mode 100644 frontend/src/components/LibraryProviderPanel.tsx create mode 100644 frontend/src/plugins/builtin/library/catalogs/architecture.json create mode 100644 frontend/src/plugins/builtin/library/catalogs/event.json create mode 100644 frontend/src/plugins/builtin/library/catalogs/landscape.json create mode 100644 frontend/src/plugins/builtin/library/index.ts create mode 100644 frontend/src/plugins/builtin/library/providers.ts create mode 100644 frontend/tests/libraryProviders.test.tsx diff --git a/frontend/src/components/LibraryProviderPanel.tsx b/frontend/src/components/LibraryProviderPanel.tsx new file mode 100644 index 0000000..65e0085 --- /dev/null +++ b/frontend/src/components/LibraryProviderPanel.tsx @@ -0,0 +1,84 @@ +/** + * Task F4 – UI für LibraryProviderExtensions. + * + * Lädt alle übergebenen Provider (builtin/user/global) und rendert je Ordner + * eine Sektion mit Thumbnail-Blöcken. Jeder Block ist als DragSource an den + * bestehenden 'text/global-block-data'-Kanal angebunden (DragDrop → Canvas). + */ +import React, { useEffect, useState } from 'react'; +import type { LibraryProviderExtension, LibBlock } from '../plugins/types'; + +interface Props { + providers: LibraryProviderExtension[]; +} + +interface Section { key: string; label: string; blocks: LibBlock[]; } + +const LibraryProviderPanel: React.FC = ({ providers }) => { + const [sections, setSections] = useState([]); + + useEffect(() => { + let cancelled = false; + (async () => { + const out: Section[] = []; + for (const provider of providers) { + try { + const folders = await provider.listFolders(); + for (const folder of folders) { + try { + const blocks = await provider.listBlocks(folder.id); + if (blocks.length > 0) { + out.push({ key: `${provider.id}/${folder.id}`, label: folder.label, blocks }); + } + } catch { + // Ordner bei Fehler überspringen + } + } + } catch { + // Provider bei Fehler überspringen + } + } + if (!cancelled) setSections(out); + })(); + return () => { + cancelled = true; + }; + }, [providers]); + + const handleDragStart = (e: React.DragEvent, block: LibBlock) => { + const payload = typeof block.payload === 'string' ? block.payload : JSON.stringify(block.payload ?? []); + e.dataTransfer.setData('text/global-block-data', payload); + e.dataTransfer.effectAllowed = 'copy'; + }; + + if (sections.length === 0) return null; + + return ( +
+ {sections.map((section) => ( +
+
{section.label}
+
+ {section.blocks.map((block) => ( + + ))} +
+
+ ))} +
+ ); +}; + +export default LibraryProviderPanel; diff --git a/frontend/src/components/RightSidebar.tsx b/frontend/src/components/RightSidebar.tsx index b435255..3d71060 100644 --- a/frontend/src/components/RightSidebar.tsx +++ b/frontend/src/components/RightSidebar.tsx @@ -1,9 +1,14 @@ import React, { useCallback } from 'react'; import type { RightSidebarProps, RightPanel } from '../types/ui.types'; +import type { LibraryProviderExtension } from '../plugins/types'; import PropertiesPanel from './PropertiesPanel'; import LayerPanel from './LayerPanel'; import BlockLibrary from './BlockLibrary'; import BlockLibraryTree from './BlockLibraryTree'; +import LibraryProviderPanel from './LibraryProviderPanel'; +import { pluginRegistry, builtinCatalogProvider } from '../plugins'; +import { makeUserLibraryProvider, makeGlobalLibraryProvider } from '../plugins/builtin/library/providers'; +import { useMemo } from 'react'; import KICopilot from './KICopilot'; const MIN_RIGHTBAR_WIDTH = 200; @@ -39,6 +44,23 @@ const RightSidebar: React.FC = ({ rightbarWidth = 300, onWidthChange, }) => { + const libraryProviders = useMemo(() => { + const list: LibraryProviderExtension[] = []; + const seen = new Set(); + const push = (p: LibraryProviderExtension) => { + if (p && !seen.has(p.id)) { + seen.add(p.id); + list.push(p); + } + }; + for (const p of pluginRegistry.getLibraryProviders()) push(p); + push(builtinCatalogProvider); + if (blocks) push(makeUserLibraryProvider(blocks)); + if (token) push(makeGlobalLibraryProvider(token)); + return list; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [blocks, token]); + const handleResizeMouseDown = useCallback((e: React.MouseEvent) => { e.preventDefault(); e.stopPropagation(); @@ -138,6 +160,7 @@ const RightSidebar: React.FC = ({ }} /> )} + )} diff --git a/frontend/src/plugins/builtin/library/catalogs/architecture.json b/frontend/src/plugins/builtin/library/catalogs/architecture.json new file mode 100644 index 0000000..e9fa882 --- /dev/null +++ b/frontend/src/plugins/builtin/library/catalogs/architecture.json @@ -0,0 +1,587 @@ +{ + "folderId": "cat-arch", + "label": "Architektur-Katalog", + "blocks": [ + { + "id": "arch-1", + "name": "Tür 900", + "elements": [ + { + "id": "a1a", + "type": "line", + "layerId": "layer-0", + "x": 0, + "y": 0, + "width": 0, + "height": 0, + "properties": { + "x1": 0, + "y1": 0, + "x2": 900, + "y2": 0 + } + }, + { + "id": "a1b", + "type": "arc", + "layerId": "layer-0", + "x": 0, + "y": 0, + "width": 1800, + "height": 1800, + "properties": { + "radius": 900, + "startAngle": 0, + "endAngle": 90 + } + } + ] + }, + { + "id": "arch-2", + "name": "Doppel-Tür 1800", + "elements": [ + { + "id": "a2a", + "type": "line", + "layerId": "layer-0", + "x": 0, + "y": 0, + "width": 0, + "height": 0, + "properties": { + "x1": -900, + "y1": 0, + "x2": -450, + "y2": 0 + } + }, + { + "id": "a2b", + "type": "arc", + "layerId": "layer-0", + "x": -900, + "y": 0, + "width": 900, + "height": 900, + "properties": { + "radius": 450, + "startAngle": 0, + "endAngle": 90 + } + }, + { + "id": "a2c", + "type": "line", + "layerId": "layer-0", + "x": 0, + "y": 0, + "width": 0, + "height": 0, + "properties": { + "x1": 450, + "y1": 0, + "x2": 900, + "y2": 0 + } + }, + { + "id": "a2d", + "type": "arc", + "layerId": "layer-0", + "x": 900, + "y": 0, + "width": 900, + "height": 900, + "properties": { + "radius": 450, + "startAngle": 90, + "endAngle": 180 + } + } + ] + }, + { + "id": "arch-3", + "name": "Tür 1200", + "elements": [ + { + "id": "a3a", + "type": "line", + "layerId": "layer-0", + "x": 0, + "y": 0, + "width": 0, + "height": 0, + "properties": { + "x1": 0, + "y1": 0, + "x2": 1200, + "y2": 0 + } + }, + { + "id": "a3b", + "type": "arc", + "layerId": "layer-0", + "x": 0, + "y": 0, + "width": 2400, + "height": 2400, + "properties": { + "radius": 1200, + "startAngle": 0, + "endAngle": 90 + } + } + ] + }, + { + "id": "arch-4", + "name": "Fenster 1200", + "elements": [ + { + "id": "a4a", + "type": "rect", + "layerId": "layer-0", + "x": 0, + "y": -30, + "width": 1200, + "height": 20, + "properties": {} + }, + { + "id": "a4b", + "type": "rect", + "layerId": "layer-0", + "x": 0, + "y": 30, + "width": 1200, + "height": 20, + "properties": {} + } + ] + }, + { + "id": "arch-5", + "name": "Fenster 2400", + "elements": [ + { + "id": "a5a", + "type": "rect", + "layerId": "layer-0", + "x": 0, + "y": -30, + "width": 2400, + "height": 20, + "properties": {} + }, + { + "id": "a5b", + "type": "rect", + "layerId": "layer-0", + "x": 0, + "y": 30, + "width": 2400, + "height": 20, + "properties": {} + } + ] + }, + { + "id": "arch-6", + "name": "Wandstück 2m", + "elements": [ + { + "id": "a6a", + "type": "rect", + "layerId": "layer-0", + "x": 0, + "y": 0, + "width": 2000, + "height": 240, + "properties": { + "fill": "#8a8f98" + } + } + ] + }, + { + "id": "arch-7", + "name": "Wandstück vertikal 2m", + "elements": [ + { + "id": "a7a", + "type": "rect", + "layerId": "layer-0", + "x": 0, + "y": 0, + "width": 240, + "height": 2000, + "properties": { + "fill": "#8a8f98" + } + } + ] + }, + { + "id": "arch-8", + "name": "Treppe (8 Stufen)", + "elements": [ + { + "id": "a8s0", + "type": "line", + "layerId": "layer-0", + "x": 0, + "y": 0, + "width": 0, + "height": 0, + "properties": { + "x1": -350, + "y1": 0, + "x2": -350, + "y2": 1200 + } + }, + { + "id": "a8s1", + "type": "line", + "layerId": "layer-0", + "x": 0, + "y": 0, + "width": 0, + "height": 0, + "properties": { + "x1": -250, + "y1": 0, + "x2": -250, + "y2": 1200 + } + }, + { + "id": "a8s2", + "type": "line", + "layerId": "layer-0", + "x": 0, + "y": 0, + "width": 0, + "height": 0, + "properties": { + "x1": -150, + "y1": 0, + "x2": -150, + "y2": 1200 + } + }, + { + "id": "a8s3", + "type": "line", + "layerId": "layer-0", + "x": 0, + "y": 0, + "width": 0, + "height": 0, + "properties": { + "x1": -50, + "y1": 0, + "x2": -50, + "y2": 1200 + } + }, + { + "id": "a8s4", + "type": "line", + "layerId": "layer-0", + "x": 0, + "y": 0, + "width": 0, + "height": 0, + "properties": { + "x1": 50, + "y1": 0, + "x2": 50, + "y2": 1200 + } + }, + { + "id": "a8s5", + "type": "line", + "layerId": "layer-0", + "x": 0, + "y": 0, + "width": 0, + "height": 0, + "properties": { + "x1": 150, + "y1": 0, + "x2": 150, + "y2": 1200 + } + }, + { + "id": "a8s6", + "type": "line", + "layerId": "layer-0", + "x": 0, + "y": 0, + "width": 0, + "height": 0, + "properties": { + "x1": 250, + "y1": 0, + "x2": 250, + "y2": 1200 + } + }, + { + "id": "a8s7", + "type": "line", + "layerId": "layer-0", + "x": 0, + "y": 0, + "width": 0, + "height": 0, + "properties": { + "x1": 350, + "y1": 0, + "x2": 350, + "y2": 1200 + } + }, + { + "id": "a8la", + "type": "line", + "layerId": "layer-0", + "x": 0, + "y": 0, + "width": 0, + "height": 0, + "properties": { + "x1": -450, + "y1": 0, + "x2": -450, + "y2": 1200 + } + }, + { + "id": "a8lb", + "type": "line", + "layerId": "layer-0", + "x": 0, + "y": 0, + "width": 0, + "height": 0, + "properties": { + "x1": 450, + "y1": 0, + "x2": 450, + "y2": 1200 + } + } + ] + }, + { + "id": "arch-9", + "name": "Säule Ø50", + "elements": [ + { + "id": "a9a", + "type": "circle", + "layerId": "layer-0", + "x": 0, + "y": 0, + "width": 500, + "height": 500, + "properties": { + "radius": 250, + "fill": "#9aa0a6" + } + } + ] + }, + { + "id": "arch-10", + "name": "Schreibtisch", + "elements": [ + { + "id": "a10a", + "type": "rect", + "layerId": "layer-0", + "x": 0, + "y": 0, + "width": 1800, + "height": 800, + "properties": { + "fill": "#6b7280" + } + } + ] + }, + { + "id": "arch-11", + "name": "Bürostuhl", + "elements": [ + { + "id": "a11a", + "type": "chair", + "layerId": "layer-0", + "x": 0, + "y": 0, + "width": 60, + "height": 60, + "properties": { + "fill": "#5a7d9a", + "outlineColor": "#2a5a99" + } + } + ] + }, + { + "id": "arch-12", + "name": "Sofa 3-Sitzer", + "elements": [ + { + "id": "a12a", + "type": "rect", + "layerId": "layer-0", + "x": 0, + "y": 0, + "width": 2200, + "height": 900, + "properties": { + "fill": "#5a7d9a" + } + } + ] + }, + { + "id": "arch-13", + "name": "Einzelbett", + "elements": [ + { + "id": "a13a", + "type": "rect", + "layerId": "layer-0", + "x": 0, + "y": 0, + "width": 900, + "height": 2000, + "properties": { + "fill": "#8fa3bf" + } + }, + { + "id": "a13b", + "type": "rect", + "layerId": "layer-0", + "x": 0, + "y": -750, + "width": 700, + "height": 300, + "properties": { + "fill": "#d5dbdb" + } + } + ] + }, + { + "id": "arch-14", + "name": "Waschbecken", + "elements": [ + { + "id": "a14a", + "type": "rect", + "layerId": "layer-0", + "x": 0, + "y": 0, + "width": 600, + "height": 450, + "properties": {} + }, + { + "id": "a14b", + "type": "circle", + "layerId": "layer-0", + "x": 0, + "y": 0, + "width": 340, + "height": 340, + "properties": { + "radius": 170, + "fill": "#7f9db9" + } + } + ] + }, + { + "id": "arch-15", + "name": "Küchenzeile 2.4m", + "elements": [ + { + "id": "a15a", + "type": "rect", + "layerId": "layer-0", + "x": 0, + "y": 0, + "width": 2400, + "height": 600, + "properties": { + "fill": "#3c4043" + } + }, + { + "id": "a15p0", + "type": "circle", + "layerId": "layer-0", + "x": -1800, + "y": 0, + "width": 180, + "height": 180, + "properties": { + "radius": 90, + "fill": "#202124" + } + }, + { + "id": "a15p1", + "type": "circle", + "layerId": "layer-0", + "x": -1320, + "y": 0, + "width": 180, + "height": 180, + "properties": { + "radius": 90, + "fill": "#202124" + } + }, + { + "id": "a15p2", + "type": "circle", + "layerId": "layer-0", + "x": -840, + "y": 0, + "width": 180, + "height": 180, + "properties": { + "radius": 90, + "fill": "#202124" + } + }, + { + "id": "a15p3", + "type": "circle", + "layerId": "layer-0", + "x": -360, + "y": 0, + "width": 180, + "height": 180, + "properties": { + "radius": 90, + "fill": "#202124" + } + } + ] + } + ] +} \ No newline at end of file diff --git a/frontend/src/plugins/builtin/library/catalogs/event.json b/frontend/src/plugins/builtin/library/catalogs/event.json new file mode 100644 index 0000000..acd583e --- /dev/null +++ b/frontend/src/plugins/builtin/library/catalogs/event.json @@ -0,0 +1,619 @@ +{ + "folderId": "cat-event", + "label": "Event-Katalog", + "blocks": [ + { + "id": "evt-1", + "name": "Stuhl", + "elements": [ + { + "id": "e1", + "type": "chair", + "layerId": "layer-0", + "x": 0, + "y": 0, + "width": 45, + "height": 45, + "properties": { + "fill": "#4a90d9", + "outlineColor": "#2a5a99" + } + } + ] + }, + { + "id": "evt-2", + "name": "Tisch rund 180cm", + "elements": [ + { + "id": "e2a", + "type": "table", + "layerId": "layer-0", + "x": 0, + "y": 0, + "width": 1800, + "height": 1800, + "properties": { + "fill": "#8b7355" + } + } + ] + }, + { + "id": "evt-3", + "name": "Tisch rund 120cm", + "elements": [ + { + "id": "e3a", + "type": "table", + "layerId": "layer-0", + "x": 0, + "y": 0, + "width": 1200, + "height": 1200, + "properties": { + "fill": "#8b7355" + } + } + ] + }, + { + "id": "evt-4", + "name": "Stehtisch 80cm", + "elements": [ + { + "id": "e4a", + "type": "table", + "layerId": "layer-0", + "x": 0, + "y": 0, + "width": 800, + "height": 800, + "properties": { + "fill": "#a08f6a" + } + } + ] + }, + { + "id": "evt-5", + "name": "Bühne 3x2m", + "elements": [ + { + "id": "e5a", + "type": "stage", + "layerId": "layer-0", + "x": 0, + "y": 0, + "width": 3000, + "height": 2000, + "properties": {} + } + ] + }, + { + "id": "evt-6", + "name": "Bühne 4x3m", + "elements": [ + { + "id": "e6a", + "type": "stage", + "layerId": "layer-0", + "x": 0, + "y": 0, + "width": 4000, + "height": 3000, + "properties": {} + } + ] + }, + { + "id": "evt-7", + "name": "Podest 1x1m", + "elements": [ + { + "id": "e7a", + "type": "stage", + "layerId": "layer-0", + "x": 0, + "y": 0, + "width": 1000, + "height": 1000, + "properties": {} + } + ] + }, + { + "id": "evt-8", + "name": "Sitzreihe (10 Stühle)", + "elements": [ + { + "id": "e2", + "type": "chair", + "layerId": "layer-0", + "x": -405, + "y": 0, + "width": 45, + "height": 45, + "properties": { + "fill": "#4a90d9", + "outlineColor": "#2a5a99" + } + }, + { + "id": "e3", + "type": "chair", + "layerId": "layer-0", + "x": -315, + "y": 0, + "width": 45, + "height": 45, + "properties": { + "fill": "#4a90d9", + "outlineColor": "#2a5a99" + } + }, + { + "id": "e4", + "type": "chair", + "layerId": "layer-0", + "x": -225, + "y": 0, + "width": 45, + "height": 45, + "properties": { + "fill": "#4a90d9", + "outlineColor": "#2a5a99" + } + }, + { + "id": "e5", + "type": "chair", + "layerId": "layer-0", + "x": -135, + "y": 0, + "width": 45, + "height": 45, + "properties": { + "fill": "#4a90d9", + "outlineColor": "#2a5a99" + } + }, + { + "id": "e6", + "type": "chair", + "layerId": "layer-0", + "x": -45, + "y": 0, + "width": 45, + "height": 45, + "properties": { + "fill": "#4a90d9", + "outlineColor": "#2a5a99" + } + }, + { + "id": "e7", + "type": "chair", + "layerId": "layer-0", + "x": 45, + "y": 0, + "width": 45, + "height": 45, + "properties": { + "fill": "#4a90d9", + "outlineColor": "#2a5a99" + } + }, + { + "id": "e8", + "type": "chair", + "layerId": "layer-0", + "x": 135, + "y": 0, + "width": 45, + "height": 45, + "properties": { + "fill": "#4a90d9", + "outlineColor": "#2a5a99" + } + }, + { + "id": "e9", + "type": "chair", + "layerId": "layer-0", + "x": 225, + "y": 0, + "width": 45, + "height": 45, + "properties": { + "fill": "#4a90d9", + "outlineColor": "#2a5a99" + } + }, + { + "id": "e10", + "type": "chair", + "layerId": "layer-0", + "x": 315, + "y": 0, + "width": 45, + "height": 45, + "properties": { + "fill": "#4a90d9", + "outlineColor": "#2a5a99" + } + }, + { + "id": "e11", + "type": "chair", + "layerId": "layer-0", + "x": 405, + "y": 0, + "width": 45, + "height": 45, + "properties": { + "fill": "#4a90d9", + "outlineColor": "#2a5a99" + } + } + ] + }, + { + "id": "evt-9", + "name": "Bankett-Rundtisch (8 Stühle)", + "elements": [ + { + "id": "e9a", + "type": "circle", + "layerId": "layer-0", + "x": 0, + "y": 0, + "width": 1800, + "height": 1800, + "properties": { + "radius": 900, + "fill": "#8b7355" + } + }, + { + "id": "e12", + "type": "chair", + "layerId": "layer-0", + "x": 1200, + "y": 0, + "width": 45, + "height": 45, + "properties": { + "fill": "#4a90d9", + "outlineColor": "#2a5a99" + } + }, + { + "id": "e13", + "type": "chair", + "layerId": "layer-0", + "x": 848, + "y": 848, + "width": 45, + "height": 45, + "properties": { + "fill": "#4a90d9", + "outlineColor": "#2a5a99" + } + }, + { + "id": "e14", + "type": "chair", + "layerId": "layer-0", + "x": 0, + "y": 1200, + "width": 45, + "height": 45, + "properties": { + "fill": "#4a90d9", + "outlineColor": "#2a5a99" + } + }, + { + "id": "e15", + "type": "chair", + "layerId": "layer-0", + "x": -848, + "y": 848, + "width": 45, + "height": 45, + "properties": { + "fill": "#4a90d9", + "outlineColor": "#2a5a99" + } + }, + { + "id": "e16", + "type": "chair", + "layerId": "layer-0", + "x": -1200, + "y": 0, + "width": 45, + "height": 45, + "properties": { + "fill": "#4a90d9", + "outlineColor": "#2a5a99" + } + }, + { + "id": "e17", + "type": "chair", + "layerId": "layer-0", + "x": -848, + "y": -848, + "width": 45, + "height": 45, + "properties": { + "fill": "#4a90d9", + "outlineColor": "#2a5a99" + } + }, + { + "id": "e18", + "type": "chair", + "layerId": "layer-0", + "x": 0, + "y": -1200, + "width": 45, + "height": 45, + "properties": { + "fill": "#4a90d9", + "outlineColor": "#2a5a99" + } + }, + { + "id": "e19", + "type": "chair", + "layerId": "layer-0", + "x": 848, + "y": -848, + "width": 45, + "height": 45, + "properties": { + "fill": "#4a90d9", + "outlineColor": "#2a5a99" + } + } + ] + }, + { + "id": "evt-10", + "name": "Tischgruppe (4 Stühle)", + "elements": [ + { + "id": "e10a", + "type": "rect", + "layerId": "layer-0", + "x": 0, + "y": 0, + "width": 1600, + "height": 800, + "properties": { + "fill": "#8b7355" + } + }, + { + "id": "e20", + "type": "chair", + "layerId": "layer-0", + "x": -400, + "y": -580, + "width": 45, + "height": 45, + "properties": { + "fill": "#4a90d9", + "outlineColor": "#2a5a99" + } + }, + { + "id": "e21", + "type": "chair", + "layerId": "layer-0", + "x": 400, + "y": -580, + "width": 45, + "height": 45, + "properties": { + "fill": "#4a90d9", + "outlineColor": "#2a5a99" + } + }, + { + "id": "e22", + "type": "chair", + "layerId": "layer-0", + "x": -400, + "y": 580, + "width": 45, + "height": 45, + "properties": { + "fill": "#4a90d9", + "outlineColor": "#2a5a99" + } + }, + { + "id": "e23", + "type": "chair", + "layerId": "layer-0", + "x": 400, + "y": 580, + "width": 45, + "height": 45, + "properties": { + "fill": "#4a90d9", + "outlineColor": "#2a5a99" + } + } + ] + }, + { + "id": "evt-11", + "name": "Flipchart", + "elements": [ + { + "id": "e11a", + "type": "rect", + "layerId": "layer-0", + "x": 0, + "y": 0, + "width": 700, + "height": 500, + "properties": { + "fill": "#546b7a" + } + }, + { + "id": "e11b", + "type": "line", + "layerId": "layer-0", + "x": 0, + "y": 0, + "width": 0, + "height": 0, + "properties": { + "x1": 0, + "y1": 250, + "x2": 0, + "y2": 500 + } + } + ] + }, + { + "id": "evt-12", + "name": "Pinnwand 120cm", + "elements": [ + { + "id": "e12a", + "type": "rect", + "layerId": "layer-0", + "x": 0, + "y": 0, + "width": 1200, + "height": 150, + "properties": { + "fill": "#7a6550" + } + } + ] + }, + { + "id": "evt-13", + "name": "Kaffeetafel 2m", + "elements": [ + { + "id": "e13a", + "type": "rect", + "layerId": "layer-0", + "x": 0, + "y": 0, + "width": 2000, + "height": 600, + "properties": { + "fill": "#6b5340" + } + } + ] + }, + { + "id": "evt-14", + "name": "Garderobe 1m", + "elements": [ + { + "id": "e14a", + "type": "rect", + "layerId": "layer-0", + "x": 0, + "y": 0, + "width": 1000, + "height": 300, + "properties": {} + }, + { + "id": "e14h0", + "type": "circle", + "layerId": "layer-0", + "x": -300, + "y": 0, + "width": 180, + "height": 180, + "properties": { + "radius": 90 + } + }, + { + "id": "e14h1", + "type": "circle", + "layerId": "layer-0", + "x": -150, + "y": 0, + "width": 180, + "height": 180, + "properties": { + "radius": 90 + } + }, + { + "id": "e14h2", + "type": "circle", + "layerId": "layer-0", + "x": 0, + "y": 0, + "width": 180, + "height": 180, + "properties": { + "radius": 90 + } + }, + { + "id": "e14h3", + "type": "circle", + "layerId": "layer-0", + "x": 150, + "y": 0, + "width": 180, + "height": 180, + "properties": { + "radius": 90 + } + }, + { + "id": "e14h4", + "type": "circle", + "layerId": "layer-0", + "x": 300, + "y": 0, + "width": 180, + "height": 180, + "properties": { + "radius": 90 + } + } + ] + }, + { + "id": "evt-15", + "name": "Teppichläufer 2m", + "elements": [ + { + "id": "e15a", + "type": "rect", + "layerId": "layer-0", + "x": 0, + "y": 0, + "width": 2000, + "height": 200, + "properties": { + "fill": "#7a5230" + } + } + ] + } + ] +} \ No newline at end of file diff --git a/frontend/src/plugins/builtin/library/catalogs/landscape.json b/frontend/src/plugins/builtin/library/catalogs/landscape.json new file mode 100644 index 0000000..30af658 --- /dev/null +++ b/frontend/src/plugins/builtin/library/catalogs/landscape.json @@ -0,0 +1,406 @@ +{ + "folderId": "cat-land", + "label": "Landschafts-Katalog", + "blocks": [ + { + "id": "land-1", + "name": "Baum (groß)", + "elements": [ + { + "id": "l1a", + "type": "circle", + "layerId": "layer-0", + "x": 0, + "y": -200, + "width": 2200, + "height": 2200, + "properties": { + "radius": 1100, + "fill": "#6aa84f" + } + }, + { + "id": "l1b", + "type": "rect", + "layerId": "layer-0", + "x": 0, + "y": 600, + "width": 200, + "height": 800, + "properties": { + "fill": "#8b5a2b" + } + } + ] + }, + { + "id": "land-2", + "name": "Baum (mittel)", + "elements": [ + { + "id": "l2a", + "type": "circle", + "layerId": "layer-0", + "x": 0, + "y": -150, + "width": 1500, + "height": 1500, + "properties": { + "radius": 750, + "fill": "#6aa84f" + } + }, + { + "id": "l2b", + "type": "rect", + "layerId": "layer-0", + "x": 0, + "y": 450, + "width": 150, + "height": 600, + "properties": { + "fill": "#8b5a2b" + } + } + ] + }, + { + "id": "land-3", + "name": "Strauch", + "elements": [ + { + "id": "l3a", + "type": "circle", + "layerId": "layer-0", + "x": 0, + "y": 0, + "width": 1000, + "height": 1000, + "properties": { + "radius": 500, + "fill": "#85b75f" + } + } + ] + }, + { + "id": "land-4", + "name": "Hecke horizontal 4m", + "elements": [ + { + "id": "l4a", + "type": "rect", + "layerId": "layer-0", + "x": 0, + "y": 0, + "width": 4000, + "height": 500, + "properties": { + "fill": "#4c7a3f" + } + } + ] + }, + { + "id": "land-5", + "name": "Hecke vertikal 4m", + "elements": [ + { + "id": "l5a", + "type": "rect", + "layerId": "layer-0", + "x": 0, + "y": 0, + "width": 500, + "height": 4000, + "properties": { + "fill": "#4c7a3f" + } + } + ] + }, + { + "id": "land-6", + "name": "Blumenbeet", + "elements": [ + { + "id": "l6a", + "type": "circle", + "layerId": "layer-0", + "x": 0, + "y": 0, + "width": 1600, + "height": 1600, + "properties": { + "radius": 800, + "fill": "#c76fb6" + } + }, + { + "id": "l6b", + "type": "circle", + "layerId": "layer-0", + "x": 0, + "y": 0, + "width": 600, + "height": 600, + "properties": { + "radius": 300, + "fill": "#e6a23c" + } + } + ] + }, + { + "id": "land-7", + "name": "Teich", + "elements": [ + { + "id": "l7a", + "type": "circle", + "layerId": "layer-0", + "x": 0, + "y": 0, + "width": 3000, + "height": 3000, + "properties": { + "radius": 1500, + "fill": "#4a90d9" + } + }, + { + "id": "l7b", + "type": "circle", + "layerId": "layer-0", + "x": 0, + "y": 100, + "width": 2700, + "height": 2700, + "properties": { + "radius": 1350, + "fill": "#5fa8e0" + } + } + ] + }, + { + "id": "land-8", + "name": "Rasenfläche 5x3m", + "elements": [ + { + "id": "l8a", + "type": "rect", + "layerId": "layer-0", + "x": 0, + "y": 0, + "width": 5000, + "height": 3000, + "properties": { + "fill": "#7cb342" + } + } + ] + }, + { + "id": "land-9", + "name": "Bodenplatte", + "elements": [ + { + "id": "l9a", + "type": "rect", + "layerId": "layer-0", + "x": 0, + "y": 0, + "width": 1200, + "height": 1200, + "properties": { + "fill": "#b8a888" + } + } + ] + }, + { + "id": "land-10", + "name": "Weg (Gerade)", + "elements": [ + { + "id": "l10a", + "type": "rect", + "layerId": "layer-0", + "x": 0, + "y": 0, + "width": 600, + "height": 4000, + "properties": { + "fill": "#b8a888" + } + } + ] + }, + { + "id": "land-11", + "name": "Sonnenliege", + "elements": [ + { + "id": "l11a", + "type": "rect", + "layerId": "layer-0", + "x": 0, + "y": 0, + "width": 600, + "height": 2000, + "properties": { + "fill": "#d2b48c" + } + } + ] + }, + { + "id": "land-12", + "name": "Feuerschale", + "elements": [ + { + "id": "l12a", + "type": "circle", + "layerId": "layer-0", + "x": 0, + "y": 0, + "width": 1200, + "height": 1200, + "properties": { + "radius": 600, + "fill": "#5a5a5a" + } + }, + { + "id": "l12b", + "type": "circle", + "layerId": "layer-0", + "x": 0, + "y": 0, + "width": 600, + "height": 600, + "properties": { + "radius": 300, + "fill": "#cc4125" + } + } + ] + }, + { + "id": "land-13", + "name": "Zaun 3m", + "elements": [ + { + "id": "l13a", + "type": "line", + "layerId": "layer-0", + "x": 0, + "y": 0, + "width": 0, + "height": 0, + "properties": { + "x1": -1500, + "y1": 0, + "x2": -1500, + "y2": 900 + } + }, + { + "id": "l13b", + "type": "line", + "layerId": "layer-0", + "x": 0, + "y": 0, + "width": 0, + "height": 0, + "properties": { + "x1": 1500, + "y1": 0, + "x2": 1500, + "y2": 900 + } + }, + { + "id": "l13c", + "type": "line", + "layerId": "layer-0", + "x": 0, + "y": 0, + "width": 0, + "height": 0, + "properties": { + "x1": -1500, + "y1": 450, + "x2": 1500, + "y2": 450 + } + } + ] + }, + { + "id": "land-14", + "name": "Laterne", + "elements": [ + { + "id": "l14a", + "type": "circle", + "layerId": "layer-0", + "x": 0, + "y": -1000, + "width": 400, + "height": 400, + "properties": { + "radius": 200, + "fill": "#f5d76e" + } + }, + { + "id": "l14b", + "type": "line", + "layerId": "layer-0", + "x": 0, + "y": 0, + "width": 0, + "height": 0, + "properties": { + "x1": 0, + "y1": -800, + "x2": 0, + "y2": 1000 + } + }, + { + "id": "l14c", + "type": "rect", + "layerId": "layer-0", + "x": 0, + "y": 1100, + "width": 300, + "height": 200, + "properties": { + "fill": "#5a5a5a" + } + } + ] + }, + { + "id": "land-15", + "name": "Gartenbank", + "elements": [ + { + "id": "l15a", + "type": "rect", + "layerId": "layer-0", + "x": 0, + "y": 0, + "width": 1500, + "height": 400, + "properties": { + "fill": "#8b5a2b" + } + } + ] + } + ] +} \ No newline at end of file diff --git a/frontend/src/plugins/builtin/library/index.ts b/frontend/src/plugins/builtin/library/index.ts new file mode 100644 index 0000000..db5e39f --- /dev/null +++ b/frontend/src/plugins/builtin/library/index.ts @@ -0,0 +1,24 @@ +/** + * Task F4 – Builtin-Bibliotheks-Plugin. + * + * Registriert den builtin-Katalog-Provider über die Plugin-API v2. Die Provider + * für user-library (zeichnungsscoped) und global-library (API/Token) sind als + * Fabriken (makeUserLibraryProvider / makeGlobalLibraryProvider) umgesetzt und + * werden vom UI je Token/Zeichnung instanziert. + */ +import type { PluginV2 } from '../../types'; +import { builtinCatalogProvider } from './providers'; + +export const builtinLibraryPlugin: PluginV2 = { + manifest: { + id: 'builtin-library', + name: 'Block-Bibliothek', + version: '1.0.0', + author: 'WebCAD Project', + description: 'Bibliothek: builtin-Kataloge (Event/Architektur/Landschaft) als LibraryProvider (Task F4).', + category: 'library', + enabledByDefault: true, + permissions: ['storage'], + }, + libraryProviders: [builtinCatalogProvider], +}; diff --git a/frontend/src/plugins/builtin/library/providers.ts b/frontend/src/plugins/builtin/library/providers.ts new file mode 100644 index 0000000..1185074 --- /dev/null +++ b/frontend/src/plugins/builtin/library/providers.ts @@ -0,0 +1,161 @@ +/** + * Task F4 – Bibliotheksquellen als LibraryProviderExtension. + * + * Drei Quellen: + * - builtinCatalogProvider: statische JSON-Startersets (event/architecture/landscape) + * - makeUserLibraryProvider: zeichnungsscoped Blockdefinitionen (Drawing-Blöcke) + * - makeGlobalLibraryProvider: persistente globale Blockbibliothek (global-blocks API) + * + * Payload-Konvention: String-Payload = JSON-Array von CADElementen — identisch zum + * 'text/global-block-data' Drop-Kanal von BlockLibraryTree (F2/F3), damit der + * Canvas-Drop-Pfad (CanvasArea) ohne Änderung funktioniert. + */ +import catalogEvent from './catalogs/event.json'; +import catalogArch from './catalogs/architecture.json'; +import catalogLand from './catalogs/landscape.json'; +import { generateBlockSvg } from '../../../utils/blockThumbnail'; +import type { CADElement } from '../../../types/cad.types'; +import type { LibraryProviderExtension, LibBlock, LibFolder } from '../../types'; +import { getGlobalFolders, getGlobalBlocks, type GlobalBlock } from '../../../services/api'; + +interface CatalogBlock { id: string; name: string; elements: CADElement[]; } +interface CatalogFile { folderId: string; label: string; blocks: CatalogBlock[]; } + +const CATALOGS: CatalogFile[] = [catalogEvent, catalogArch, catalogLand].map( + (c) => c as unknown as CatalogFile, +); + +const thumbCache = new Map(); + +function toLibBlock(folderId: string, block: CatalogBlock): LibBlock { + let thumbnail = thumbCache.get(block.id); + if (!thumbnail) { + thumbnail = generateBlockSvg(block.elements); + thumbCache.set(block.id, thumbnail); + } + return { + id: block.id, + folderId, + name: block.name, + thumbnail, + payload: JSON.stringify(block.elements), + }; +} + +/** Statischer builtin-Katalog: Event / Architektur / Landschaft Startersets. */ +export const builtinCatalogProvider: LibraryProviderExtension = { + id: 'builtin-catalog', + label: 'Builtin-Kataloge', + async listFolders(): Promise { + return CATALOGS.map((c) => ({ id: c.folderId, label: c.label, parentId: null })); + }, + async listBlocks(folderId: string): Promise { + const cat = CATALOGS.find((c) => c.folderId === folderId); + return cat ? cat.blocks.map((b) => toLibBlock(folderId, b)) : []; + }, + async getBlock(id: string): Promise { + for (const cat of CATALOGS) { + const found = cat.blocks.find((b) => b.id === id); + if (found) return toLibBlock(cat.folderId, found); + } + throw new Error(`Block not found: ${id}`); + }, +}; + +export interface UserBlockLike { + id: string; + name: string; + elements?: unknown; + elements_json?: string; + thumbnail?: string | null; + drawing_id?: string; + description?: string | null; + category?: string; +} + +function extractElements(block: UserBlockLike): CADElement[] { + try { + if (Array.isArray(block.elements)) return block.elements as CADElement[]; + if (typeof block.elements_json === 'string') { + const parsed: unknown = JSON.parse(block.elements_json); + if (Array.isArray(parsed)) return parsed as CADElement[]; + const wrapped = parsed as { elements?: unknown } | null; + if (wrapped && Array.isArray(wrapped.elements)) return wrapped.elements as CADElement[]; + } + } catch { + // invalider Inhalt → leerer Block wird beim Auflisten gefiltert + } + return []; +} + +/** Zeichnungsscoped Blöcke (Drawing-Blöcke) als Bibliothek anbinden. */ +export function makeUserLibraryProvider(userBlocks: readonly UserBlockLike[]): LibraryProviderExtension { + const folderId = 'user-blocks'; + const toBlock = (block: UserBlockLike): LibBlock => { + const elements = extractElements(block); + return { + id: `user:${block.id}`, + folderId, + name: block.name, + thumbnail: elements.length ? generateBlockSvg(elements) : undefined, + payload: elements.length ? JSON.stringify(elements) : undefined, + }; + }; + return { + id: 'user-library', + label: 'Benutzer-Bibliothek', + async listFolders(): Promise { + return [{ id: folderId, label: 'Zeichnungs-Blöcke', parentId: null }]; + }, + async listBlocks(id: string): Promise { + return id === folderId + ? userBlocks.filter((b) => extractElements(b).length > 0).map(toBlock) + : []; + }, + async getBlock(id: string): Promise { + const found = userBlocks.find((b) => `user:${b.id}` === id && extractElements(b).length > 0); + if (found) return toBlock(found); + throw new Error(`Block not found: ${id}`); + }, + }; +} + +/** Persistente globale Blockbibliothek über die global-blocks API anbinden. */ +export function makeGlobalLibraryProvider(token: string): LibraryProviderExtension { + const toBlock = (block: GlobalBlock): LibBlock => ({ + id: `global:${block.id}`, + folderId: block.folder_id ?? 'global-root', + name: block.name, + thumbnail: block.svg_data ?? undefined, + payload: block.block_data, + }); + const loadAll = async (): Promise => { + try { + return await getGlobalBlocks(token); + } catch { + return []; + } + }; + return { + id: 'global-library', + label: 'Globale Bibliothek', + async listFolders(): Promise { + try { + const folders = await getGlobalFolders(token); + return folders.map((f) => ({ id: f.id, label: f.name, parentId: f.parent_id })); + } catch { + return []; + } + }, + async listBlocks(folderId: string): Promise { + const blocks = await loadAll(); + return blocks.filter((b) => (b.folder_id ?? 'global-root') === folderId).map(toBlock); + }, + async getBlock(id: string): Promise { + const blocks = await loadAll(); + const found = blocks.find((b) => `global:${b.id}` === id); + if (found) return toBlock(found); + throw new Error(`Block not found: ${id}`); + }, + }; +} diff --git a/frontend/src/plugins/index.ts b/frontend/src/plugins/index.ts index 97923e0..e37386d 100644 --- a/frontend/src/plugins/index.ts +++ b/frontend/src/plugins/index.ts @@ -32,6 +32,8 @@ export { coreModifyPlugin } from './builtin/core-modify'; export { coreAnnotatePlugin } from './builtin/core-annotate'; export { coreMeasurePlugin } from './builtin/core-measure'; export { eventToolsV2Plugin } from './builtin/event-tools'; +export { builtinLibraryPlugin } from './builtin/library'; +export { builtinCatalogProvider, makeUserLibraryProvider, makeGlobalLibraryProvider } from './builtin/library/providers'; import { pluginRegistry } from './PluginRegistry'; import { eventToolsPlugin } from './builtin/eventTools'; @@ -40,6 +42,7 @@ import { coreModifyPlugin } from './builtin/core-modify'; import { coreAnnotatePlugin } from './builtin/core-annotate'; import { coreMeasurePlugin } from './builtin/core-measure'; import { eventToolsV2Plugin } from './builtin/event-tools'; +import { builtinLibraryPlugin } from './builtin/library'; /** Register all built-in plugins */ export function registerBuiltinPlugins() { @@ -50,4 +53,5 @@ export function registerBuiltinPlugins() { pluginRegistry.registerV2(coreAnnotatePlugin); pluginRegistry.registerV2(coreMeasurePlugin); pluginRegistry.registerV2(eventToolsV2Plugin); + pluginRegistry.registerV2(builtinLibraryPlugin); } diff --git a/frontend/src/plugins/types.ts b/frontend/src/plugins/types.ts index f6e7139..8deb69c 100644 --- a/frontend/src/plugins/types.ts +++ b/frontend/src/plugins/types.ts @@ -208,7 +208,9 @@ export interface LibBlock { folderId: string; name: string; tags?: string[]; - thumbnail?: string; // dataURL + thumbnail?: string; // svg/dataURL + /** Payload für Canvas-Drop: i.d.R. JSON-String eines CADElement-Arrays (Task F4). */ + payload?: unknown; } /** Liefert Kataloge — Kern der Plugin-erweiterbaren Library (Phase F). */ diff --git a/frontend/src/styles.css b/frontend/src/styles.css index 52a896c..01d457c 100644 --- a/frontend/src/styles.css +++ b/frontend/src/styles.css @@ -2796,3 +2796,42 @@ body.drawer-open { overflow: hidden; } background: var(--color-accent); opacity: 0.7; } + + +/* ─── Task F4 – LibraryProviderPanel ─── */ +.lib-provider-panel { + display: flex; + flex-direction: column; + gap: 8px; + padding: 8px 0 4px; + border-top: 1px solid rgba(255, 255, 255, 0.08); +} +.lprov-section { display: flex; flex-direction: column; gap: 4px; } +.lprov-title { + font-size: 11px; + font-weight: 600; + color: #9aa0a6; + text-transform: uppercase; + letter-spacing: 0.04em; + padding: 2px 8px; +} +.lprov-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(56px, 1fr)); + gap: 6px; + padding: 0 8px; +} +.lprov-block { + display: flex; + align-items: center; + justify-content: center; + padding: 6px 4px; + background: transparent; + border: 1px solid transparent; + border-radius: 6px; + cursor: grab; + color: inherit; + min-height: 44px; +} +.lprov-block:hover { background: rgba(255, 255, 255, 0.06); border-color: rgba(255, 255, 255, 0.12); } +.lprov-block .tree-thumb svg { width: 44px; height: 36px; display: block; } diff --git a/frontend/tests/libraryProviders.test.tsx b/frontend/tests/libraryProviders.test.tsx new file mode 100644 index 0000000..b800fc7 --- /dev/null +++ b/frontend/tests/libraryProviders.test.tsx @@ -0,0 +1,143 @@ +/** + * Task F4 – LibraryProviderExtension Tests. + * + * Testet die drei Bibliotheksquellen: + * - builtin-catalog (statische JSON-Startersets event/architecture/landscape) + * - user-library (zeichnungsscoped Blöcke via /api/drawings/:id/blocks) + * - global-library (globale Blöcke via /api/global-blocks) + */ +import { describe, it, expect, vi, afterEach } from 'vitest'; +import { render, screen, waitFor, fireEvent } from '@testing-library/react'; + +import { builtinCatalogProvider } from '../src/plugins/builtin/library/providers'; +import { makeUserLibraryProvider, makeGlobalLibraryProvider } from '../src/plugins/builtin/library/providers'; +import { builtinLibraryPlugin } from '../src/plugins/builtin/library'; +import { pluginRegistry } from '../src/plugins/PluginRegistry'; +import LibraryProviderPanel from '../src/components/LibraryProviderPanel'; + +describe('F4: builtin catalog provider', () => { + it('listet genau die 3 Katalog-Ordner', async () => { + const folders = await builtinCatalogProvider.listFolders(); + expect(folders.map(f => f.id).sort()).toEqual(['cat-arch', 'cat-event', 'cat-land']); + expect(folders.every(f => f.parentId === null)).toBe(true); + }); + + it('Startersets: je 15-30 Blöcke mit gültiger Payload und SVG-Thumbnail', async () => { + const folders = await builtinCatalogProvider.listFolders(); + for (const folder of folders) { + const blocks = await builtinCatalogProvider.listBlocks(folder.id); + expect(blocks.length).toBeGreaterThanOrEqual(15); + expect(blocks.length).toBeLessThanOrEqual(30); + expect(blocks.every(b => b.folderId === folder.id)).toBe(true); + for (const block of blocks) { + expect(block.thumbnail).toContain(' { + const eventBlocks = await builtinCatalogProvider.listBlocks('cat-event'); + expect(eventBlocks.length).toBeGreaterThanOrEqual(15); + const first = eventBlocks[0]; + const fetched = await builtinCatalogProvider.getBlock(first.id); + expect(fetched.id).toBe(first.id); + expect(fetched.payload).toBe(first.payload); + }); +}); + +describe('F4: Registry-Anbindung', () => { + afterEach(() => { + pluginRegistry.unregisterV2(builtinLibraryPlugin.manifest.id); + }); + + it('Plugin registriert Provider, enableV2 macht ihn sichtbar, disableV2 entfernt ihn', () => { + pluginRegistry.registerV2(builtinLibraryPlugin); + pluginRegistry.enableV2(builtinLibraryPlugin.manifest.id); + const providers = pluginRegistry.getLibraryProviders(); + expect(providers.some(p => p.id === 'builtin-catalog')).toBe(true); + + pluginRegistry.disableV2(builtinLibraryPlugin.manifest.id); + expect(pluginRegistry.getLibraryProviders().some(p => p.id === 'builtin-catalog')).toBe(false); + }); +}); + +describe('F4: user-library Provider', () => { + it('mappt zeichnungsscoped Blöcke auf LibBlock mit Thumbnail+Payload', async () => { + const provider = makeUserLibraryProvider([ + { + id: 'b1', drawing_id: 'd1', name: 'Bühne A', description: null, category: 'stage', + elements_json: JSON.stringify([ + { id: 'e1', type: 'rect', layerId: 'layer-0', x: 0, y: 0, width: 100, height: 50, properties: { fill: '#3a3a4a' } }, + ]), + thumbnail: null, + }, + ]); + const folders = await provider.listFolders(); + expect(folders.length).toBe(1); + const blocks = await provider.listBlocks(folders[0].id); + expect(blocks).toHaveLength(1); + expect(blocks[0].name).toBe('Bühne A'); + expect(blocks[0].thumbnail).toContain(' { + afterEach(() => { + vi.unstubAllGlobals(); + }); + + it('lädt Ordner und Blöcke der globalen Bibliothek über die API', async () => { + const mockFetch = vi.fn() + .mockResolvedValueOnce({ ok: true, json: async () => [ + { id: 'gf1', name: 'Event', parent_id: null, created_at: '2026-01-01' }, + ] }) + .mockResolvedValueOnce({ ok: true, json: async () => [ + { id: 'gb1', folder_id: 'gf1', name: 'Stuhl-Reihe', block_data: '[]', svg_data: null, is_favorite: false, created_at: '2026-01-01', updated_at: '2026-01-01' }, + ] }); + vi.stubGlobal('fetch', mockFetch); + + const provider = makeGlobalLibraryProvider('tok'); + const folders = await provider.listFolders(); + expect(folders).toHaveLength(1); + expect(folders[0].label).toBe('Event'); + const blocks = await provider.listBlocks('gf1'); + expect(blocks).toHaveLength(1); + expect(blocks[0].name).toBe('Stuhl-Reihe'); + }); +}); + +describe('F4: LibraryProviderPanel UI', () => { + it('rendert Provider-Sektion mit Katalog-Ordnern und Blöcken', async () => { + render(); + await waitFor(() => { + expect(screen.getAllByText(/Event-Katalog/).length).toBeGreaterThan(0); + }); + // Jede Sektion listet Blöcke + expect(screen.getAllByRole('button', { name: /Stuhl/ }).length).toBeGreaterThan(0); + }); + + it('DragStart legt die Element-Array-Payload in den Datentransfer', async () => { + const setData = vi.fn(); + render(); + const blockBtn = await screen.findByRole('button', { name: /Stuhl/ }); + fireEvent.dragStart(blockBtn, { dataTransfer: { setData } }); + expect(setData).toHaveBeenCalledWith('text/global-block-data', expect.any(String)); + const payload = JSON.parse(setData.mock.calls[0][1] as string); + expect(Array.isArray(payload)).toBe(true); + + expect(payload[0].id).toBeTypeOf('string'); + }); +});