Persist catalog layout and add inspector toolbar

This commit is contained in:
DCCONSTRUCTIONS
2026-07-10 23:28:10 +03:00
parent 8ca12f2676
commit 406e692a5e
14 changed files with 715 additions and 268 deletions
+108
View File
@@ -0,0 +1,108 @@
import { useState, type CSSProperties } from "react";
import { cn } from "./cn.js";
import { Icon, type IconName } from "./Icon.js";
export type ToolbarPlacement = "left" | "right" | "bottom";
export interface ToolbarItem<T extends string = string> {
id: T;
label: string;
icon: IconName;
active?: boolean;
disabled?: boolean;
onSelect: (id: T) => void;
}
export interface ToolbarProps<T extends string = string> {
items: Array<ToolbarItem<T>>;
placement?: ToolbarPlacement;
background?: string;
border?: string;
outline?: string;
accent?: string;
minSize?: number;
maxSize?: number;
lensCount?: number;
autoHide?: boolean;
label?: string;
className?: string;
}
export function Toolbar<T extends string = string>({
items,
placement = "left",
background = "#111115",
border = "#111117",
outline = "#1c1c1c",
accent = "#ff2f92",
minSize = 25,
maxSize = 87,
lensCount = 5,
autoHide = false,
label = "Панель инструментов",
className,
}: ToolbarProps<T>) {
const [hoveredIndex, setHoveredIndex] = useState<number | null>(null);
const [visible, setVisible] = useState(!autoHide);
const vertical = placement !== "bottom";
const safeMin = Math.max(18, Math.min(42, minSize));
const safeMax = Math.max(safeMin, Math.min(88, maxSize));
const safeLens = Math.max(1, Math.min(13, lensCount % 2 === 0 ? lensCount + 1 : lensCount));
const radius = Math.max(9, Math.round(safeMin * 0.42));
const style = {
"--nodedc-toolbar-bg": background,
"--nodedc-toolbar-border": border,
"--nodedc-toolbar-outline": outline,
"--nodedc-toolbar-accent": accent,
"--nodedc-toolbar-min-size": `${safeMin}px`,
"--nodedc-toolbar-max-size": `${safeMax}px`,
"--nodedc-toolbar-radius": `${radius}px`,
} as CSSProperties;
return (
<div
className={cn("nodedc-toolbar-wrap", className)}
data-placement={placement}
data-visible={visible ? "true" : undefined}
data-autohide={autoHide ? "true" : undefined}
style={style}
onPointerEnter={() => setVisible(true)}
onPointerLeave={() => {
setHoveredIndex(null);
if (autoHide) setVisible(false);
}}
>
{autoHide ? <div className="nodedc-toolbar__hotzone" aria-hidden="true" onPointerEnter={() => setVisible(true)} /> : null}
<div className="nodedc-toolbar" role="toolbar" aria-label={label}>
{items.map((item, index) => {
const distance = hoveredIndex === null ? Number.POSITIVE_INFINITY : Math.abs(index - hoveredIndex);
const lensRadius = Math.floor(safeLens / 2);
const influence = distance > lensRadius ? 0 : 1 - distance / Math.max(1, lensRadius + 1);
const size = safeMin + (safeMax - safeMin) * influence;
return (
<button
key={item.id}
type="button"
className="nodedc-toolbar__button"
data-active={item.active ? "true" : undefined}
aria-label={item.label}
aria-pressed={item.active}
disabled={item.disabled}
style={{
"--nodedc-toolbar-item-size": `${Math.round(size)}px`,
"--nodedc-toolbar-item-shift": `${Math.round((size - safeMin) / (vertical ? 2 : 2))}px`,
} as CSSProperties}
onPointerEnter={() => setHoveredIndex(index)}
onFocus={() => setHoveredIndex(index)}
onBlur={() => setHoveredIndex(null)}
onClick={() => item.onSelect(item.id)}
>
<Icon name={item.icon} size={Math.max(12, Math.min(18, Math.round(safeMin * 0.62)))} />
</button>
);
})}
</div>
</div>
);
}