zebflow 0.9.1

Reactive web automation platform — pipelines, templates, and real-time collaboration in a single binary.
Documentation
import { cx } from "zeb/react";
import ContextMenu from "@/components/ui/context-menu";
import FileKindIcon from "@/components/ui/file-kind-icon";

/** An open or closed folder. Files get the studio's own technology marks. */
export function EntryIcon({ kind, name, open }) {
  if (kind !== "folder" && kind !== "root") {
    return (
      <span className="flex h-4 w-4 shrink-0 items-center justify-center text-muted-foreground">
        <FileKindIcon name={name} />
      </span>
    );
  }
  return (
    <svg
      viewBox="0 0 24 24"
      fill="none"
      stroke="currentColor"
      strokeWidth="1.6"
      strokeLinejoin="round"
      className="h-4 w-4 shrink-0"
    >
      {open ? (
        <path d="M4 20a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h3.9a2 2 0 0 1 1.7.9l.8 1.2a2 2 0 0 0 1.7.9H20a2 2 0 0 1 2 2v1H6.5L4 20Z" />
      ) : (
        <path d="M20 20a2 2 0 0 0 2-2V8a2 2 0 0 0-2-2h-7.9a2 2 0 0 1-1.69-.9L9.6 3.9A2 2 0 0 0 7.93 3H4a2 2 0 0 0-2 2v13a2 2 0 0 0 2 2Z" />
      )}
    </svg>
  );
}

/** A caret that points down once the folder is open. */
function Caret({ open, hidden }) {
  return (
    <svg
      viewBox="0 0 12 12"
      fill="none"
      stroke="currentColor"
      strokeWidth="1.5"
      className={cx(
        "h-3 w-3 shrink-0 transition-transform",
        open && "rotate-90",
        hidden && "invisible",
      )}
    >
      <path d="M4.5 2.5 8 6l-3.5 3.5" />
    </svg>
  );
}

/**
 * One line of the tree: the root, a folder, or a file.
 *
 * Every row is the same shape and carries the same `⋯`, which is the whole
 * point — "what can I do here" has one answer and one place to find it,
 * including at the repository root, which is a row like any other.
 */
export default function RepoTreeRow({ item, depth, open, active, status, menu, onActivate }) {
  const expandable = item.kind === "folder" || item.kind === "root";

  return (
    <ContextMenu
      items={menu}
      trigger="⋯"
      className={cx(
        "flex w-full items-center gap-1.5 py-[3px] pr-1 text-[12px] transition-colors",
        active
          ? "bg-accent text-foreground"
          : "text-muted-foreground hover:bg-accent/60 hover:text-foreground",
      )}
    >
      <button
          style={{ paddingLeft: `${depth * 12 + 8}px` }}
          type="button"
          title={item.rel_path || item.name}
          data-repo-tree-row={item.rel_path}
          data-repo-tree-kind={item.kind}
          data-selected={active ? "true" : undefined}
          className="flex min-w-0 flex-1 items-center gap-1.5 text-left"
          onClick={() => onActivate(item)}
        >
          <Caret open={open} hidden={!expandable} />
          <EntryIcon kind={item.kind} name={item.name} open={open} />
          <span className={cx("truncate", item.kind === "root" && "font-medium text-foreground")}>
            {item.name}
          </span>
          {status ? (
            <span className="shrink-0 font-mono text-[10px] text-success" title={`git: ${status}`}>
              {status}
            </span>
          ) : null}
      </button>
    </ContextMenu>
  );
}