Intelligence

Artifacts

Browse the repository, read documents, and manage the governance folders. Source, runtime, and infrastructure are read-only.

Repository
.gitignoreDockerfilenext-env.d.tsnext.config.mjspackage-lock.jsonpackage.jsonpostcss.config.mjsREADME.mdtailwind.config.tstsconfig.jsontsconfig.tsbuildinfo
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