Ilya Ashikhmin 6585cfb32c feat(ui): app shell — palette, property panel, toolbar, settings, canvas, store, persistence
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 23:15:38 +02:00

83 lines
2.7 KiB
TypeScript

/**
* Left panel: node types grouped by category. Items are dragged onto the
* canvas (HTML5 drag & drop carrying the type id) or double-clicked to drop
* at the viewport center.
*/
import { useMemo, useState } from 'react';
import { nodeTypesByCategory, type NodeTypeDef } from '../model/nodeTypes';
import { svgDataUri } from '../editor/shapes';
import { getColorScheme } from '../editor/colorSchemes';
export const PALETTE_MIME = 'application/x-pipeline-node-type';
interface PaletteProps {
schemeId: string;
onQuickAdd(typeId: string): void;
}
function PaletteItem({ type, schemeId, onQuickAdd }: {
type: NodeTypeDef;
schemeId: string;
onQuickAdd(typeId: string): void;
}) {
const scheme = getColorScheme(schemeId);
return (
<div
className="pf-palette-item"
draggable
title={`${type.label} — drag onto canvas`}
data-testid={`palette-item-${type.id}`}
onDragStart={(e) => {
e.dataTransfer.setData(PALETTE_MIME, type.id);
e.dataTransfer.effectAllowed = 'copy';
}}
onDoubleClick={() => onQuickAdd(type.id)}
>
<img src={svgDataUri(type, scheme.nodeStroke)} alt={type.label} width={34} height={34} />
<span>{type.label}</span>
</div>
);
}
export function Palette({ schemeId, onQuickAdd }: PaletteProps) {
const [filter, setFilter] = useState('');
const [collapsed, setCollapsed] = useState<Record<string, boolean>>({});
const groups = useMemo(() => nodeTypesByCategory(), []);
const query = filter.trim().toLowerCase();
return (
<aside className="pf-palette" data-testid="palette">
<input
className="pf-palette-search"
placeholder="Search nodes…"
value={filter}
onChange={(e) => setFilter(e.target.value)}
/>
{groups.map(({ category, types }) => {
const visible = query
? types.filter((t) => t.label.toLowerCase().includes(query))
: types;
if (visible.length === 0) return null;
const isCollapsed = collapsed[category.id] && !query;
return (
<section key={category.id} className="pf-palette-group">
<header
onClick={() => setCollapsed((c) => ({ ...c, [category.id]: !c[category.id] }))}
>
<span className="pf-caret">{isCollapsed ? '▸' : '▾'}</span> {category.label}
</header>
{!isCollapsed && (
<div className="pf-palette-items">
{visible.map((type) => (
<PaletteItem key={type.id} type={type} schemeId={schemeId} onQuickAdd={onQuickAdd} />
))}
</div>
)}
</section>
);
})}
</aside>
);
}