zebflow 0.10.2

Reactive web automation platform — pipelines, templates, and real-time collaboration in a single binary.
Documentation
import { cx, useState } from "zeb/react";
import { requestJson } from "@/components/lib/http";
import Button from "@/components/ui/button";
import Checkbox from "@/components/ui/checkbox";
import CommitDialog from "@/components/ui/commit-dialog";
import Input from "@/components/ui/input";
import Textarea from "@/components/ui/textarea";
import { settingsStatusToneClass } from "@/pages/project-studio/settings/components/settings-lib";
import SettingsSection from "@/pages/project-studio/settings/components/settings-section";

/** Render engine behaviour: caching, hydration, and the cache escape hatch. */
export default function RwePanel({ api, initialConfig, owner, project }) {
  const [allowList, setAllowList] = useState(
    (initialConfig?.allow_list ?? []).join("\n")
  );
  const [minify, setMinify] = useState(Boolean(initialConfig?.minify_html));
  const [strict, setStrict] = useState(initialConfig?.strict_mode !== false);
  const [deploymentAssetBase, setDeploymentAssetBase] = useState(
    String(initialConfig?.deployment_asset_base ?? "")
  );
  const [statusMsg, setStatusMsg] = useState("Ready.");
  const [statusTone, setStatusTone] = useState("info");
  const [saving, setSaving] = useState(false);
  const [clearing, setClearing] = useState(false);
  const [commitOpen, setCommitOpen] = useState(false);
  const [pendingData, setPendingData] = useState(null);

  function handleSubmit(e) {
    e.preventDefault();
    setPendingData({
      allow_list: allowList.split(/[\n,]/).map((s) => s.trim()).filter(Boolean),
      minify_html: minify,
      strict_mode: strict,
      deployment_asset_base: deploymentAssetBase.trim() || null,
    });
    setCommitOpen(true);
  }

  async function handleCommit(commitMessage) {
    setCommitOpen(false);
    setSaving(true);
    setStatusMsg("Saving...");
    setStatusTone("info");
    try {
      const resp = await requestJson(api, {
        method: "PUT",
        body: JSON.stringify({ commit_message: commitMessage, data: pendingData }),
      });
      if (resp?.committed) {
        setStatusMsg("Saved & committed.");
        setStatusTone("ok");
      } else if (resp?.git_error) {
        setStatusMsg(`Saved (git: ${resp.git_error})`);
        setStatusTone("info");
      } else {
        setStatusMsg("Saved.");
        setStatusTone("ok");
      }
    } catch (err) {
      setStatusMsg(`Failed: ${err?.message || String(err)}`);
      setStatusTone("error");
    } finally {
      setSaving(false);
      setPendingData(null);
    }
  }

  async function handleClearCache() {
    setClearing(true);
    try {
      const res = await fetch(`/api/projects/${owner}/${project}/rwe/cache/clear`, { method: "POST" });
      if (res.ok) {
        setStatusMsg("Template cache cleared.");
        setStatusTone("ok");
      } else {
        setStatusMsg("Failed to clear cache.");
        setStatusTone("error");
      }
    } finally {
      setClearing(false);
    }
  }

  return (
    <SettingsSection
      title="Reactive Web Engine"
      description={
        <>
          Project-level compile and render controls for all <code>n.web.render</code> nodes.
        </>
      }
      tag="RWE"
    >
      <CommitDialog
        open={commitOpen}
        section="rwe"
        defaultMessage="settings(rwe): update RWE config"
        onConfirm={handleCommit}
        onCancel={() => { setCommitOpen(false); setPendingData(null); }}
      />
      <form className="grid grid-cols-2 gap-[0.65rem]" onSubmit={handleSubmit}>
        <label className="pipeline-editor-field">
          <span>Script Allow List</span>
          <Textarea
            name="allow_list"
            rows={4}
            placeholder={"https://cdnjs.cloudflare.com/*\nhttps://cdn.jsdelivr.net/*"}
            value={allowList}
            onInput={(e) => setAllowList(e.currentTarget.value)}
          />
          <small className="pipeline-editor-field-help">
            One URL pattern per line (or comma-separated). Controls which external scripts and
            stylesheets <code>--load-scripts</code> may reference. Blessed <code>zeb/*</code>{" "}
            libraries are always allowed and do not appear here.
          </small>
        </label>

        <div className="flex flex-col gap-2 pt-1">
          <Checkbox
            name="minify_html"
            label="Minify HTML output"
            checked={minify}
            onChange={(e) => setMinify(e.target.checked)}
          />
          <Checkbox
            name="strict_mode"
            label="Strict compile-time checks"
            checked={strict}
            onChange={(e) => setStrict(e.target.checked)}
          />
        </div>

        <label className="pipeline-editor-field col-span-full">
          <span>Asset Base Path</span>
          <Input
            name="deployment_asset_base"
            placeholder={`/static/${owner ?? "owner"}/${project ?? "project"}`}
            value={deploymentAssetBase}
            onInput={(e) => setDeploymentAssetBase(e.currentTarget.value)}
          />
          <small className="pipeline-editor-field-help">
            Replaces the default <code>/static/{"{owner}/{project}"}</code> prefix in all rendered HTML — scripts, images, uploads, library chunks. E.g. set to <code>/my/custom/path</code> and <code>/static/{"{owner}/{project}"}/_rwe/…</code> becomes <code>/my/custom/path/_rwe/…</code>. Leave empty to keep the default.
          </small>
        </label>

        <div className="col-span-full flex items-center gap-[0.7rem]">
          <Button
            type="submit"
            variant="primary"
            size="sm"
            disabled={saving}
            label={saving ? "Saving..." : "Save RWE Config"}
          />
          <Button
            type="button"
            variant="outline"
            size="sm"
            disabled={clearing}
            onClick={handleClearCache}
            label={clearing ? "Clearing…" : "Clear Template Cache"}
          />
          <span className={cx("text-[0.72rem]", settingsStatusToneClass(statusTone))}>{statusMsg}</span>
        </div>
      </form>
    </SettingsSection>
  );
}