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:
@@ -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;
|
||||
@@ -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<RightSidebarProps> = ({
|
||||
rightbarWidth = 300,
|
||||
onWidthChange,
|
||||
}) => {
|
||||
const libraryProviders = useMemo(() => {
|
||||
const list: LibraryProviderExtension[] = [];
|
||||
const seen = new Set<string>();
|
||||
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<RightSidebarProps> = ({
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<LibraryProviderPanel providers={libraryProviders} />
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user