Intelligence
Artifacts
Browse the repository, read documents, and manage the governance folders. Source, runtime, and infrastructure are read-only.
Repository
assignment-dispatch-button.tsxassignment-status-badge.tsxassignments-panel.tsxcreate-mission-form.tsxgovernance-panels.tsxmission-dispatch.tsxmission-edit.tsxmission-state-actions.tsxmission-status-badge.tsxobjective-edit.tsxobjective-status-badge.tsxobjective-status-select.tsxobjectives-panel.tsxreport-list.tsxwork-orders-panel.tsx
README.md
CONSTITUTION_COMPLIANCE_AUDIT_V1.mdREADME.md
repositories/aaf-holdings/hq01/components/artifacts/file-tree.tsx
4.7 KB
"use client";
import { useState } from "react";
import Link from "next/link";
import {
ChevronRight,
Folder,
FolderOpen,
FileText,
FileCode,
File as FileIcon,
} from "lucide-react";
import { cn } from "@/lib/utils";
import type { ArtifactNode } from "@/lib/content/types";
function fileIcon(ext?: string) {
if (ext === ".md" || ext === ".mdx" || ext === ".txt") return FileText;
if (
ext === ".ts" || ext === ".tsx" || ext === ".js" || ext === ".jsx" ||
ext === ".json" || ext === ".yaml" || ext === ".yml" || ext === ".css"
) {
return FileCode;
}
return FileIcon;
}
function pathIsAncestor(dir: string, file: string | null): boolean {
if (!file) return false;
return file === dir || file.startsWith(dir + "/");
}
/** Total number of nodes beneath a folder (files + sub-folders). */
function countDescendants(node: ArtifactNode): number {
if (!node.children) return 0;
let n = node.children.length;
for (const child of node.children) n += countDescendants(child);
return n;
}
/**
* Folders whose entire subtree is at or below this many nodes open by default,
* so curated document folders (uploads, os/*, constitutions, …) reveal their
* files immediately. Large trees like the repository source stay collapsed.
*/
const AUTO_OPEN_MAX = 40;
function TreeNode({
node,
selected,
selectedDir,
depth,
}: {
node: ArtifactNode;
selected: string | null;
selectedDir: string | null;
depth: number;
}) {
const [open, setOpen] = useState(
depth < 1 ||
pathIsAncestor(node.path, selected) ||
pathIsAncestor(node.path, selectedDir) ||
(node.type === "dir" && countDescendants(node) <= AUTO_OPEN_MAX),
);
if (node.type === "dir") {
const Icon = open ? FolderOpen : Folder;
const isSelectedDir = selectedDir === node.path;
return (
<div>
{/* Chevron toggles expansion; the name selects the folder for management. */}
<div
className={cn(
"flex items-center rounded-md pr-2 transition-colors hover:bg-secondary",
isSelectedDir && "bg-accent/10",
)}
style={{ paddingLeft: `${depth * 12 + 4}px` }}
>
<button
type="button"
onClick={() => setOpen((o) => !o)}
className="flex shrink-0 items-center p-1"
aria-label={open ? "Collapse folder" : "Expand folder"}
>
<ChevronRight
className={cn(
"h-3.5 w-3.5 text-muted-foreground transition-transform",
open && "rotate-90",
)}
/>
</button>
<Link
href={`/artifacts?dir=${encodeURIComponent(node.path)}`}
scroll={false}
className={cn(
"flex min-w-0 flex-1 items-center gap-1.5 py-1.5 text-[13px]",
isSelectedDir ? "font-medium text-accent" : "text-foreground/80",
)}
>
<Icon
className={cn(
"h-4 w-4 shrink-0",
isSelectedDir ? "text-accent" : "text-muted-foreground",
)}
/>
<span className="truncate font-medium">{node.name}</span>
</Link>
</div>
{open && node.children && (
<div>
{node.children.map((child) => (
<TreeNode
key={child.path}
node={child}
selected={selected}
selectedDir={selectedDir}
depth={depth + 1}
/>
))}
</div>
)}
</div>
);
}
const Icon = fileIcon(node.ext);
const isSelected = selected === node.path;
return (
<Link
href={`/artifacts?file=${encodeURIComponent(node.path)}`}
scroll={false}
className={cn(
"flex items-center gap-1.5 rounded-md py-1.5 pr-2 text-[13px] transition-colors",
isSelected
? "bg-accent/10 font-medium text-accent"
: "text-muted-foreground hover:bg-secondary hover:text-foreground",
)}
style={{ paddingLeft: `${depth * 12 + 22}px` }}
>
<Icon
className={cn(
"h-4 w-4 shrink-0",
isSelected ? "text-accent" : "text-muted-foreground",
)}
/>
<span className="truncate">{node.name}</span>
</Link>
);
}
export function FileTree({
tree,
selected,
selectedDir = null,
}: {
tree: ArtifactNode[];
selected: string | null;
selectedDir?: string | null;
}) {
return (
<div className="py-1">
{tree.map((node) => (
<TreeNode
key={node.path}
node={node}
selected={selected}
selectedDir={selectedDir}
depth={0}
/>
))}
</div>
);
}
root · /srv/aaf