Ports catalog.ts's tree renderer to components under src/components/sidebar/, following docs/SIDEBAR.md: two-line cards behind a 5px activity rail, adaptive meta line (duration, detail, program, diff stat), collapsible folders, j/k walk, inline rename and a right-click menu. The nav walk reads a pure model (model.ts) rather than the DOM, so the rule that an expanded folder header is skipped and a collapsed one is not is testable without a render. Rail motion needs @keyframes, so it stays in sidebar.css rather than the Tailwind config. Closes #244
52 lines
1.6 KiB
TypeScript
52 lines
1.6 KiB
TypeScript
// A collapsible directory in the sidebar tree. Only a *collapsed* header is a
|
|
// keyboard-walk stop (docs/SIDEBAR.md); an expanded one is decoration in front
|
|
// of the cards it holds, so it carries no highlight of its own.
|
|
|
|
import type { ReactNode } from "react";
|
|
|
|
import { splitFolderLabel } from "./model.js";
|
|
|
|
export interface FolderNodeProps {
|
|
name: string;
|
|
path: string;
|
|
depth: number;
|
|
collapsed: boolean;
|
|
highlighted: boolean;
|
|
onToggle(): void;
|
|
children: ReactNode;
|
|
}
|
|
|
|
export function FolderNode(props: FolderNodeProps) {
|
|
const { stem, last } = splitFolderLabel(props.name);
|
|
return (
|
|
<div className="tree-folder">
|
|
<button
|
|
type="button"
|
|
className={`tree-folder-header flex w-full items-center gap-sp-1 py-[2px] text-fs-11 text-muted ${
|
|
props.highlighted ? "ring-1 ring-accent-ring" : ""
|
|
}`}
|
|
style={{ paddingLeft: props.depth * 16 }}
|
|
aria-expanded={!props.collapsed}
|
|
data-nav-key={`folder:${props.path}`}
|
|
data-highlighted={props.highlighted ? "true" : undefined}
|
|
title={props.name}
|
|
onClick={props.onToggle}
|
|
>
|
|
{/* One glyph, rotated when collapsed, so the mark and the rotation
|
|
never disagree about which way the folder points. */}
|
|
<span
|
|
aria-hidden="true"
|
|
className={`inline-block transition-transform ${props.collapsed ? "-rotate-90" : ""}`}
|
|
>
|
|
▾
|
|
</span>
|
|
<span className="truncate">
|
|
{stem && <span className="text-faint">{stem}/</span>}
|
|
{last}
|
|
</span>
|
|
</button>
|
|
{!props.collapsed && props.children}
|
|
</div>
|
|
);
|
|
}
|