zebflow 0.7.9

Reactive web automation platform — pipelines, templates, and real-time collaboration in a single binary.
Documentation
import { useState, useMemo, useEffect, useRef, usePageState, useNavigate, Link } from "zeb";

// ─── Inline sub-components ────────────────────────────────────────────────

function NavBar({ active }: any) {
  return (
    <nav class="bg-zinc-900 border-b border-zinc-800 px-6 py-3 flex items-center justify-between sticky top-0 z-10">
      <span class="text-zinc-100 font-bold text-sm tracking-tight">RWE Demo</span>
      <div class="flex gap-5 text-sm">
        <Link href="/" class={active === "/" ? "text-emerald-400 font-medium" : "text-zinc-400 hover:text-zinc-100"}>Home</Link>
        <Link href="/blog" class={active === "/blog" ? "text-emerald-400 font-medium" : "text-zinc-400 hover:text-zinc-100"}>Blog</Link>
        <Link href="/todo" class={active === "/todo" ? "text-emerald-400 font-medium" : "text-zinc-400 hover:text-zinc-100"}>Todo</Link>
      </div>
    </nav>
  );
}

function TodoItem({ id, text, completed, onToggle, onDelete }: any) {
  return (
    <li class="flex items-center gap-3 px-4 py-3 border-b border-zinc-800 last:border-0 group hover:bg-zinc-900/60">
      <button
        type="button"
        onClick={() => onToggle(id)}
        class={
          completed
            ? "w-5 h-5 rounded-full border border-emerald-500 bg-emerald-500 flex-shrink-0"
            : "w-5 h-5 rounded-full border border-zinc-600 bg-transparent flex-shrink-0 hover:border-emerald-400"
        }
      />
      <span
        class={
          completed
            ? "flex-grow text-sm text-zinc-500 line-through"
            : "flex-grow text-sm text-zinc-100"
        }
      >
        {text}
      </span>
      <button
        type="button"
        onClick={() => onDelete(id)}
        class="text-xs text-zinc-600 group-hover:text-zinc-400 hover:text-red-400 px-2 py-1 rounded border border-transparent hover:border-red-800 transition-colors"
      >
        ✕
      </button>
    </li>
  );
}

// ─── Page ─────────────────────────────────────────────────────────────────

export const page = {
  head: { title: "Todo — RWE Demo" },
  html: { lang: "en" },
};

export default function TodoPage(input: any) {
  // Seed items from the server-rendered input
  const seedItems = (input.items || []).map((it: any, i: number) => ({
    id: i + 1,
    text: it.title ?? it.text ?? String(it),
    completed: false,
  }));

  // useState
  const [todos, setTodos] = useState<any[]>(seedItems);
  const [inputValue, setInputValue] = useState<string>("");
  const [filter, setFilter] = useState<string>("all");

  // useRef — focused on mount (browser only)
  const inputRef = useRef<any>(null);

  // usePageState — records the last added item, visible across all components
  const { lastAdded = "", setPageState } = usePageState({ lastAdded: "" });

  // useNavigate
  const navigate = useNavigate();

  // useMemo — derived counts
  const activeCount = useMemo(() => todos.filter((t: any) => !t.completed).length, [todos]);
  const doneCount = useMemo(() => todos.filter((t: any) => t.completed).length, [todos]);

  // useMemo — filtered list
  const filtered = useMemo(() => {
    if (filter === "active") return todos.filter((t: any) => !t.completed);
    if (filter === "done") return todos.filter((t: any) => t.completed);
    return todos;
  }, [todos, filter]);

  // useEffect — auto-focus input on mount
  useEffect(() => {
    if (inputRef.current) {
      inputRef.current.focus();
    }
  }, []);

  const addTodo = (e: any) => {
    e?.preventDefault?.();
    const text = String(inputValue || "").trim();
    if (!text) return;
    const id = Date.now();
    setTodos((prev: any[]) => [{ id, text, completed: false }, ...prev]);
    setInputValue("");
    setPageState({ lastAdded: text });
  };

  const toggle = (id: number) => {
    setTodos((prev: any[]) =>
      prev.map((t: any) => (t.id === id ? { ...t, completed: !t.completed } : t))
    );
  };

  const remove = (id: number) => {
    setTodos((prev: any[]) => prev.filter((t: any) => t.id !== id));
  };

  const clearDone = () => {
    setTodos((prev: any[]) => prev.filter((t: any) => !t.completed));
  };

  return (
    <div class="min-h-screen bg-zinc-950 text-zinc-100 flex flex-col">
      <NavBar active="/todo" />

      <main class="flex-1 max-w-2xl mx-auto w-full px-6 py-10">

        {/* Hero */}
        <div class="mb-8">
          <p class="text-xs font-mono text-emerald-400 uppercase tracking-widest mb-1">Todo</p>
          <h1 class="text-3xl font-bold text-zinc-100 mb-2">Task Manager</h1>
          <p class="text-zinc-400 text-sm">
            useState · useMemo · useEffect · useRef · usePageState · TodoItem component
          </p>
          {lastAdded !== "" && (
            <p class="mt-2 text-xs text-emerald-500 font-mono">
              Last added: "{lastAdded}"
            </p>
          )}
        </div>

        {/* Add form (useState for input, useEffect focuses on mount) */}
        <form onSubmit={addTodo} class="flex gap-2 mb-6">
          <input
            ref={inputRef}
            type="text"
            value={inputValue}
            onInput={(e: any) => setInputValue(e?.target?.value ?? "")}
            placeholder="What needs doing?"
            class="flex-1 bg-zinc-900 border border-zinc-700 rounded-lg px-4 py-2.5 text-sm text-zinc-100 placeholder-zinc-600 focus:border-emerald-500 focus:outline-none"
          />
          <button
            type="submit"
            class="px-5 py-2.5 bg-emerald-900 hover:bg-emerald-800 border border-emerald-700 rounded-lg text-sm font-medium text-emerald-100 disabled:opacity-40"
          >
            Add
          </button>
        </form>

        {/* Stats (useMemo) */}
        <div class="flex gap-5 mb-4 text-xs text-zinc-500">
          <span>Active: <strong class="text-zinc-300 font-mono">{activeCount}</strong></span>
          <span>Done: <strong class="text-zinc-300 font-mono">{doneCount}</strong></span>
          <span>Total: <strong class="text-zinc-300 font-mono">{todos.length}</strong></span>
        </div>

        {/* Filter tabs (useState) */}
        {todos.length > 0 && (
          <div class="flex gap-1 mb-4 bg-zinc-900 border border-zinc-800 rounded-lg p-1 w-fit">
            {["all", "active", "done"].map((f) => (
              <button
                key={f}
                onClick={() => setFilter(f)}
                class={
                  filter === f
                    ? "px-3 py-1.5 rounded-md bg-zinc-700 text-zinc-100 text-xs font-medium"
                    : "px-3 py-1.5 rounded-md text-zinc-500 text-xs hover:text-zinc-300"
                }
              >
                {f}
              </button>
            ))}
          </div>
        )}

        {/* Todo list — TodoItem sub-component */}
        <div class="bg-zinc-900 border border-zinc-800 rounded-xl overflow-hidden mb-3">
          {filtered.length === 0 ? (
            <div class="py-12 text-center text-zinc-500 text-sm">
              {todos.length === 0
                ? "No tasks yet — add one above."
                : "No tasks match this filter."}
            </div>
          ) : (
            <ul>
              {filtered.map((todo: any) => (
                <TodoItem
                  key={String(todo.id)}
                  id={todo.id}
                  text={todo.text}
                  completed={todo.completed}
                  onToggle={toggle}
                  onDelete={remove}
                />
              ))}
            </ul>
          )}
        </div>

        {/* Clear completed */}
        {doneCount > 0 && (
          <button
            onClick={clearDone}
            class="text-xs text-zinc-500 hover:text-red-400 transition-colors"
          >
            Clear {doneCount} completed task{doneCount !== 1 ? "s" : ""}
          </button>
        )}

        {/* useNavigate demo button */}
        <div class="mt-8 pt-6 border-t border-zinc-800">
          <p class="text-xs text-zinc-500 mb-3">Navigate with useNavigate:</p>
          <div class="flex gap-2 mb-5">
            <button
              onClick={() => navigate("/")}
              class="px-3 py-1.5 bg-zinc-800 border border-zinc-700 hover:bg-zinc-700 rounded text-xs text-zinc-300"
            >
              navigate("/")
            </button>
            <button
              onClick={() => navigate("/blog")}
              class="px-3 py-1.5 bg-zinc-800 border border-zinc-700 hover:bg-zinc-700 rounded text-xs text-zinc-300"
            >
              navigate("/blog")
            </button>
          </div>
          <div class="flex gap-4 text-sm">
            <Link href="/" class="text-zinc-400 hover:text-zinc-200 no-underline">← Home</Link>
            <Link href="/blog" class="text-zinc-400 hover:text-zinc-200 no-underline">Blog →</Link>
          </div>
        </div>

      </main>

      <footer class="bg-zinc-900 border-t border-zinc-800 px-6 py-4 mt-auto">
        <div class="max-w-2xl mx-auto flex items-center justify-between text-xs text-zinc-500">
          <span>RWE — Reactive Web Engine</span>
          <div class="flex gap-4">
            <Link href="/" class="hover:text-zinc-300 no-underline">Home</Link>
            <Link href="/blog" class="hover:text-zinc-300 no-underline">Blog</Link>
            <Link href="/todo" class="hover:text-zinc-300 no-underline">Todo</Link>
          </div>
        </div>
      </footer>
    </div>
  );
}