zebflow 0.10.3

Reactive web automation platform — pipelines, templates, and real-time collaboration in a single binary.
Documentation
import { cx, useState } from "zeb/react";
import Badge from "@/components/ui/badge";
import Button from "@/components/ui/button";
import Card from "@/components/ui/card";
import CardContent from "@/components/ui/card-content";
import CardDescription from "@/components/ui/card-description";
import Input from "@/components/ui/input";
import Separator from "@/components/ui/separator";

/** Every pipeline node the running platform knows about. */
export default function NodeRegistryPanel({ groups, count }) {
  const [searchQuery, setSearchQuery] = useState("");
  const [activeTab, setActiveTab] = useState("installed");

  const nodeGroups = Array.isArray(groups) ? groups : [];
  const query = searchQuery.toLowerCase().trim();

  const filteredGroups = query
    ? nodeGroups
        .map((group) => ({
          ...group,
          nodes: (Array.isArray(group?.nodes) ? group.nodes : []).filter((node) =>
            `${node?.title ?? ""} ${node?.kind ?? ""} ${node?.description ?? ""}`
              .toLowerCase()
              .includes(query)
          ),
        }))
        .filter((group) => group.nodes.length > 0)
    : nodeGroups;

  const visibleCount = filteredGroups.reduce(
    (sum, g) => sum + (g.nodes?.length ?? 0),
    0
  );

  return (
    <div className="border border-border rounded-lg bg-card overflow-hidden">
      {/* Toolbar */}
      <div className="flex flex-row items-center gap-[0.55rem] px-3 pt-[0.7rem] pb-[0.6rem]">
        <Input
          placeholder="Search nodes by name or kind..."
          value={searchQuery}
          onInput={(e) => setSearchQuery(e.currentTarget.value)}
        />
        <Button variant="outline" size="sm" label="+ Install" disabled />
      </div>

      {/* Tabs */}
      <div className="flex gap-1 px-3 pb-2">
        {(["installed", "discover", "updates"] as const).map((tab) => (
          <Button
            key={tab}
            variant="ghost"
            size="sm"
            className={cx(tab === activeTab && "bg-primary/10 text-primary border-primary/40")}
            label={tab === "installed" ? `Installed · ${count}` : tab === "discover" ? "Discover" : "Updates"}
            onClick={() => setActiveTab(tab)}
          />
        ))}
      </div>

      <Separator />

      {/* Installed panel */}
      {activeTab === "installed" ? (
        <div>
          <p className="px-3 py-[0.4rem] text-[0.72rem] text-muted-foreground border-b border-border">
            {visibleCount === count
              ? `${count} nodes · ${count} built-in`
              : `${visibleCount} of ${count} nodes · ${count} built-in`}
          </p>
          <div className="flex flex-col gap-[0.35rem] px-3 py-[0.6rem]">
            {filteredGroups.length === 0 ? (
              <p className="p-8 text-center text-[0.8rem] text-muted-foreground">No nodes found.</p>
            ) : (
              filteredGroups.map((group, gi) => (
                <div key={`grp-${gi}`}>
                  <div className="flex items-center gap-2 mb-[0.35rem] mt-2 first:mt-0">
                    {group?.prefix ? (
                      <span className="text-[0.65rem] font-mono text-muted-foreground tracking-[0.05em] whitespace-nowrap shrink-0">{group.prefix}</span>
                    ) : null}
                    <div className="flex-1 h-px bg-border" />
                  </div>
                  {(Array.isArray(group?.nodes) ? group.nodes : []).map((node, ni) => (
                    <div
                      key={`${node?.kind ?? "node"}-${ni}`}
                      className="flex items-stretch border border-border rounded-lg bg-muted overflow-hidden transition-colors duration-[120ms]"
                    >
                      <div className="w-[3px] shrink-0 bg-border" />
                      <div className="flex-1 min-w-0 px-3 py-[0.6rem] flex items-start justify-between gap-3">
                        <div className="flex-1 min-w-0">
                          <div className="text-[0.83rem] font-bold text-foreground leading-tight">{node?.title}</div>
                          <div className="text-[0.66rem] font-mono text-muted-foreground mt-[0.15rem] tracking-[0.03em]">{node?.kind}</div>
                          <div className="text-[0.75rem] leading-[1.4] text-muted-foreground mt-[0.3rem]">{node?.description}</div>
                        </div>
                        <div className="flex items-center flex-wrap gap-[0.3rem] shrink-0 pt-[0.1rem]">
                          {node?.ai_registered ? (
                            <Badge label="agent tool" variant="outline" className="text-[0.65rem] text-gray-200 border-white/25 bg-transparent" />
                          ) : null}
                          <span className="inline-flex items-center px-2 py-[0.25rem] rounded-full border border-[rgba(74,222,128,0.3)] text-[#4ade80] text-[0.65rem] font-mono uppercase tracking-widest">● installed</span>
                        </div>
                      </div>
                    </div>
                  ))}
                </div>
              ))
            )}
          </div>
        </div>
      ) : null}

      {/* Discover panel */}
      {activeTab === "discover" ? (
        <div className="p-4 px-3">
          <Card>
            <CardContent>
              <CardDescription label="Community registry — coming soon." />
              <CardDescription label="Install custom nodes from a Git URL using + Install above." />
            </CardContent>
          </Card>
        </div>
      ) : null}

      {/* Updates panel */}
      {activeTab === "updates" ? (
        <div className="p-4 px-3">
          <Card>
            <CardContent>
              <CardDescription label={`All ${count} built-in nodes are current.`} />
            </CardContent>
          </Card>
        </div>
      ) : null}
    </div>
  );
}