zebflow 0.11.14

Reactive web automation platform — pipelines, templates, and real-time collaboration in a single binary.
Documentation
import { defineExtension, routeSearch } from "zeb/ui/editor-extension";

/**
 * Embed extension — a card for a record that lives elsewhere, stored as its
 * key plus a snapshot, so the page renders without fetching anything.
 *
 *   embedExtension({ name: "product", label: "Product", route: "/api/products/search" })
 *
 * `/product` opens a picker fed by the route (`GET route?q=…` → a JSON array
 * of `{ id, label, href, snapshot }`); the chosen item is stored as
 * `{ key: id, snapshot: { title, description, image, href, meta } }`. A
 * pipeline that owns the records may rewrite the snapshot of every node with
 * that `type` and `key` when the record changes. Without a route the card is
 * filled in by hand in its panel.
 */

export function embedExtension({ name = "embed", label = "Embed", hint = "A card for a linked record", route, search } = {}) {
  return defineExtension({
    name,
    node: { group: "block", atom: true, draggable: true, attrs: { key: { default: "" }, snapshot: { default: {} } } },
    render: (node, r) => {
      const s = node.attrs.snapshot || {};
      const body = r.h(
        "span",
        { class: "min-w-0 flex-1" },
        r.h("strong", { class: "block truncate font-semibold" }, s.title || node.attrs.key || "Untitled"),
        s.description ? r.h("span", { class: "mt-0.5 block text-sm text-muted-foreground" }, s.description) : null,
        s.meta ? r.h("small", { class: "mt-1 block text-xs text-muted-foreground" }, s.meta) : null,
      );
      const image = s.image ? r.h("img", { class: "h-20 w-28 shrink-0 rounded-md object-cover", src: s.image, alt: "" }) : null;
      const attrs = { class: "my-4 flex gap-4 rounded-lg border border-border bg-card p-3 text-card-foreground no-underline", "data-embed": name, "data-key": node.attrs.key };
      return s.href ? r.h("a", { ...attrs, href: s.href }, image, body) : r.h("div", attrs, image, body);
    },
    text: (node) => (node.attrs.snapshot && node.attrs.snapshot.title) || node.attrs.key,
    picker: route || search ? { search: search || routeSearch(route), toAttrs: (item) => ({ key: String(item.id), snapshot: { title: item.label, href: item.href || "", ...(item.snapshot || {}) } }) } : null,
    insert: [{ id: name, label, hint, keys: `embed card ${name}`, run: (api) => (route || search ? api.pick() : api.insert(name)) }],
    fields: [
      { name: "key", label: "Key" },
      { name: "snapshot.title", label: "Title" },
      { name: "snapshot.description", label: "Description" },
      { name: "snapshot.href", label: "Link", type: "url" },
      { name: "snapshot.image", label: "Image", type: "url" },
      { name: "snapshot.meta", label: "Meta" },
    ],
  });
}

export default embedExtension;