zebflow 0.11.14

Reactive web automation platform — pipelines, templates, and real-time collaboration in a single binary.
Documentation
import { cx } from "zeb/react";
import Checkbox from "@/components/ui/checkbox";

function gitStatusChar(code) {
  if (code === "??") return "U";
  const trimmed = String(code || "").replace(/\s/g, "");
  return trimmed[0] || "M";
}

function buildGitFileTree(files) {
  const byPath = new Map();
  const roots = [];

  files.forEach((f) => {
    const parts = String(f.rel_path || "").split("/").filter(Boolean);
    for (let i = 1; i < parts.length; i++) {
      const dirPath = parts.slice(0, i).join("/");
      if (!byPath.has(dirPath)) {
        byPath.set(dirPath, { id: dirPath, name: parts[i - 1], isDir: true, children: [] });
      }
    }
    byPath.set(f.rel_path, {
      id: f.rel_path,
      name: parts[parts.length - 1] || f.rel_path,
      isDir: false,
      file: f,
      children: [],
    });
  });

  byPath.forEach((node, path) => {
    const lastSlash = path.lastIndexOf("/");
    if (lastSlash > 0) {
      const parentPath = path.slice(0, lastSlash);
      if (byPath.has(parentPath)) {
        byPath.get(parentPath).children.push(node);
        return;
      }
    }
    roots.push(node);
  });

  return roots;
}

function sortGitNodes(nodes) {
  return [...nodes]
    .sort((a, b) => {
      if (a.isDir !== b.isDir) return a.isDir ? -1 : 1;
      return a.name.localeCompare(b.name);
    })
    .map((n) => ({ ...n, children: sortGitNodes(n.children) }));
}

function GitTreeNodes({ nodes, setFiles }) {
  return (
    <>
      {nodes.map((node) =>
        node.isDir ? (
          <li key={node.id} className="project-tree-branch">
            <details className="project-tree-details" open>
              <summary className="project-tree-summary git-tree-dir">
                <span className="project-tree-caret">
                  <svg viewBox="0 0 24 24" fill="none" width="12" height="12" aria-hidden="true">
                    <path d="M7 10l5 5 5-5" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
                  </svg>
                </span>
                <span className="project-tree-segment">{node.name}</span>
              </summary>
              <div className="project-tree-body">
                <ul className="project-tree-list">
                  <GitTreeNodes nodes={sortGitNodes(node.children)} setFiles={setFiles} />
                </ul>
              </div>
            </details>
          </li>
        ) : (
          <li key={node.id} className="project-tree-leaf">
            <div className="project-tree-leaf-link git-tree-file">
              <Checkbox
                checked={node.file.checked}
                onChange={(e) =>
                  setFiles((prev) =>
                    prev.map((x) =>
                      x.rel_path === node.file.rel_path ? { ...x, checked: e.target.checked } : x
                    )
                  )
                }
                className="git-tree-check"
              />
              <span className="git-tree-file-name" title={node.file.rel_path}>{node.name}</span>
              <code className={cx(
                "git-tree-code",
                gitStatusChar(node.file.code) === "A" && "is-added",
                gitStatusChar(node.file.code) === "D" && "is-deleted",
                gitStatusChar(node.file.code) === "M" && "is-modified",
                gitStatusChar(node.file.code) === "U" && "is-untracked",
              )}>
                {gitStatusChar(node.file.code)}
              </code>
            </div>
          </li>
        )
      )}
    </>
  );
}

export function GitFileTree({ files, setFiles }) {
  const roots = sortGitNodes(buildGitFileTree(files));
  if (roots.length === 0) return null;
  return (
    <ul className="project-tree-root git-tree-root">
      <GitTreeNodes nodes={roots} setFiles={setFiles} />
    </ul>
  );
}