zebflow 0.11.14

Reactive web automation platform — pipelines, templates, and real-time collaboration in a single binary.
Documentation
import { cx, createContext, useContext, useRef } from "zeb/react";

/**
 * Resizable — shadcn/ui's resizable panels, on Zebflow's engine.
 *
 * Upstream wraps `react-resizable-panels`; there is no such runtime library
 * here, so sizing is written by hand. `ResizablePanelGroup` hands its
 * `direction` down through a `ResizableContext` — matching how upstream's
 * `PanelGroup` shares it with `PanelResizeHandle` — so `ResizableHandle`
 * picks it up automatically; `direction` on the handle itself remains an
 * optional override for a caller that wants to force one. Sizing is real: on
 * pointer-down the handle reads its DOM `previousElementSibling`/
 * `nextElementSibling` (the two adjacent `ResizablePanel`s) and, on every
 * `pointermove`, writes `flexBasis` percentages on both directly as inline
 * styles — an imperative drag that bypasses re-renders, same as the
 * interactive part of the real thing, and SSR-safe since it only touches
 * `window`/DOM from inside the handler.
 */

const HANDLE_BAR = {
  horizontal: "h-full w-px cursor-col-resize",
  vertical: "w-full h-px cursor-row-resize",
};

const ResizableContext = createContext({ direction: "horizontal" });

export function ResizablePanelGroup({ direction = "horizontal", className, children, ...props }) {
  return (
    <ResizableContext.Provider value={{ direction }}>
      <div
        data-slot="resizable-panel-group"
        data-direction={direction}
        className={cx("flex h-full w-full", direction === "vertical" ? "flex-col" : "flex-row", className)}
        {...props}
      >
        {children}
      </div>
    </ResizableContext.Provider>
  );
}

export function ResizablePanel({ defaultSize = 50, minSize = 10, className, style, children, ...props }) {
  return (
    <div
      data-slot="resizable-panel"
      data-min-size={minSize}
      className={cx("overflow-auto", className)}
      style={{ flexBasis: `${defaultSize}%`, flexGrow: 0, flexShrink: 0, minWidth: 0, minHeight: 0, ...style }}
      {...props}
    >
      {children}
    </div>
  );
}

export function ResizableHandle({ direction: directionProp, withHandle, className, ...props }) {
  const ctx = useContext(ResizableContext);
  const direction = directionProp ?? ctx.direction;
  const ref = useRef(null);
  const onPointerDown = (e) => {
    const handle = ref.current;
    const before = handle?.previousElementSibling;
    const after = handle?.nextElementSibling;
    if (!handle || !before || !after) return;
    const vertical = direction === "vertical";
    const dim = (el) => (vertical ? el.getBoundingClientRect().height : el.getBoundingClientRect().width);
    const total = dim(handle.parentElement);
    const beforeStart = (dim(before) / total) * 100;
    const afterStart = (dim(after) / total) * 100;
    const originPos = vertical ? e.clientY : e.clientX;
    const minBefore = Number(before.getAttribute("data-min-size") ?? 10);
    const minAfter = Number(after.getAttribute("data-min-size") ?? 10);
    handle.setPointerCapture(e.pointerId);
    const onMove = (ev) => {
      const deltaPct = ((vertical ? ev.clientY : ev.clientX) - originPos) / total * 100;
      let nextBefore = beforeStart + deltaPct;
      let nextAfter = afterStart - deltaPct;
      if (nextBefore < minBefore) { nextAfter -= minBefore - nextBefore; nextBefore = minBefore; }
      if (nextAfter < minAfter) { nextBefore -= minAfter - nextAfter; nextAfter = minAfter; }
      before.style.flexBasis = `${nextBefore}%`;
      after.style.flexBasis = `${nextAfter}%`;
    };
    const onUp = () => {
      window.removeEventListener("pointermove", onMove);
      window.removeEventListener("pointerup", onUp);
    };
    window.addEventListener("pointermove", onMove);
    window.addEventListener("pointerup", onUp);
  };

  return (
    <div
      ref={ref}
      data-slot="resizable-handle"
      data-direction={direction}
      role="separator"
      aria-orientation={direction === "vertical" ? "horizontal" : "vertical"}
      tabIndex={0}
      onPointerDown={onPointerDown}
      className={cx(
        "relative flex shrink-0 items-center justify-center bg-border focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring",
        direction === "vertical" ? HANDLE_BAR.vertical : HANDLE_BAR.horizontal,
        className
      )}
      {...props}
    >
      {withHandle ? (
        <div className={cx("z-10 flex h-4 w-3 items-center justify-center rounded-xs border border-border bg-border", direction === "vertical" ? "rotate-90" : "")}>
          <svg viewBox="0 0 10 16" className="size-2.5 text-muted-foreground" fill="currentColor">
            <circle cx="3" cy="2" r="1.1" />
            <circle cx="3" cy="8" r="1.1" />
            <circle cx="3" cy="14" r="1.1" />
            <circle cx="7" cy="2" r="1.1" />
            <circle cx="7" cy="8" r="1.1" />
            <circle cx="7" cy="14" r="1.1" />
          </svg>
        </div>
      ) : null}
    </div>
  );
}

export default ResizablePanelGroup;