- Fix invalid nested button elements in furni grid cards that broke card borders, hover styles and selection rendering - Fix stale-closure bug where switching furni source fetched from the previously selected source - Add progress bar plus Cancel/Dismiss controls to batch imports - Make the detail drawer an overlay on small screens - Add clear-search and clear-selection actions in the toolbar - Use pixelated image rendering and keyboard focus rings on cards
104 lines
3.1 KiB
TypeScript
104 lines
3.1 KiB
TypeScript
"use client";
|
|
|
|
import { ChevronDown, ChevronRight, FolderTree } from "lucide-react";
|
|
import { useMemo, useState } from "react";
|
|
|
|
import { cn } from "@/lib/utils";
|
|
import type { TreeNode } from "@/types/catalog";
|
|
|
|
export function CatalogRail({
|
|
tree,
|
|
selectedId,
|
|
onSelect,
|
|
}: {
|
|
tree: TreeNode[];
|
|
selectedId: number | null;
|
|
onSelect: (id: number | null) => void;
|
|
}) {
|
|
const [collapsed, setCollapsed] = useState<Set<number>>(new Set());
|
|
|
|
const visible = useMemo(() => {
|
|
const hidden = new Set<number>();
|
|
for (const node of tree) {
|
|
if (hidden.has(node.parentId)) {
|
|
hidden.add(node.id);
|
|
continue;
|
|
}
|
|
if (collapsed.has(node.parentId)) {
|
|
hidden.add(node.id);
|
|
}
|
|
}
|
|
return tree.filter((n) => !hidden.has(n.id));
|
|
}, [tree, collapsed]);
|
|
|
|
const toggle = (id: number) => {
|
|
setCollapsed((prev) => {
|
|
const next = new Set(prev);
|
|
if (next.has(id)) next.delete(id);
|
|
else next.add(id);
|
|
return next;
|
|
});
|
|
};
|
|
|
|
return (
|
|
<aside className="flex h-full min-h-0 w-64 shrink-0 flex-col border-r border-[var(--admin-border)] bg-[var(--admin-surface)]">
|
|
<div className="flex items-center gap-2 border-b border-[var(--admin-border)] px-3 py-2.5">
|
|
<FolderTree size={14} className="text-[var(--admin-accent)]" />
|
|
<span className="text-xs font-bold uppercase tracking-wider text-[var(--admin-text-muted)]">
|
|
Catalog
|
|
</span>
|
|
</div>
|
|
<div className="min-h-0 flex-1 overflow-y-auto p-1.5">
|
|
{visible.length === 0 ? (
|
|
<p className="px-2 py-4 text-xs text-[var(--admin-text-muted)]">
|
|
No catalog pages found.
|
|
</p>
|
|
) : (
|
|
visible.map((node) => {
|
|
const isRoot = node.parentId <= 0;
|
|
const hasChildren = node.childCount > 0;
|
|
const isCollapsed = collapsed.has(node.id);
|
|
return (
|
|
<button
|
|
key={node.id}
|
|
type="button"
|
|
onClick={() => {
|
|
if (hasChildren) toggle(node.id);
|
|
onSelect(selectedId === node.id ? null : node.id);
|
|
}}
|
|
className={cn(
|
|
"group flex w-full cursor-pointer items-center gap-1.5 rounded-md px-1.5 py-1.5 text-left text-xs transition-colors outline-none focus-visible:ring-2 focus-visible:ring-[var(--admin-accent)]",
|
|
selectedId === node.id
|
|
? "bg-[var(--admin-accent)]/15 text-[var(--admin-text)]"
|
|
: "text-[var(--admin-text-muted)] hover:bg-[var(--admin-accent)]/10 hover:text-[var(--admin-text)]",
|
|
)}
|
|
style={{ paddingLeft: `${6 + node.depth * 12}px` }}
|
|
>
|
|
{hasChildren ? (
|
|
isCollapsed ? (
|
|
<ChevronRight size={12} className="shrink-0" />
|
|
) : (
|
|
<ChevronDown size={12} className="shrink-0" />
|
|
)
|
|
) : (
|
|
<span className="w-3 shrink-0" />
|
|
)}
|
|
<span className="min-w-0 flex-1 truncate">
|
|
{isRoot ? (
|
|
<span className="font-semibold">{node.caption}</span>
|
|
) : (
|
|
node.caption
|
|
)}
|
|
</span>
|
|
<span className="shrink-0 rounded-full bg-[var(--admin-accent)]/10 px-1.5 text-[0.6rem] font-semibold text-[var(--admin-accent)]">
|
|
{node.itemCount}
|
|
</span>
|
|
</button>
|
|
);
|
|
})
|
|
)}
|
|
</div>
|
|
</aside>
|
|
);
|
|
}
|