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

This commit is contained in:
Agent Zero
2026-08-28 23:20:11 +02:00
parent ec7cb4043b
commit 2efbbf9bd2
11 changed files with 2093 additions and 1 deletions
@@ -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<Props> = ({ providers }) => {
const [sections, setSections] = useState<Section[]>([]);
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 (
<div className="lib-provider-panel">
{sections.map((section) => (
<div className="lprov-section" key={section.key}>
<div className="lprov-title">{section.label}</div>
<div className="lprov-grid">
{section.blocks.map((block) => (
<button
key={block.id}
type="button"
className="lprov-block"
aria-label={block.name}
title={block.name}
draggable
onDragStart={(e) => handleDragStart(e, block)}
>
{block.thumbnail
? <span className="tree-thumb" dangerouslySetInnerHTML={{ __html: block.thumbnail }} />
: <span className="lib-grid-placeholder">📦</span>}
</button>
))}
</div>
</div>
))}
</div>
);
};
export default LibraryProviderPanel;