// ── Headless components (@zerotal/flow) ────────────────────────────────── // // Unstyled, fully-accessible interactive primitives — the Headless UI philosophy // adapted to Flow. They ship NO visual classes: state is exposed via `data-*` // attributes (`data-open`, `data-checked`, `data-active`) so you Tailwind-style // them yourself, e.g. `class="data-[open]:rotate-180"`. // // Behaviour is driven by Alpine (open/close, keyboard, focus) and ARIA roles are // wired in. Components that hold a value (Switch) bind to an @expose prop and sync // to the server via $flow.$set — so the value persists and submits like any field. // // // … // // Menu}>… import { jsx, _resolveReactiveName, _injectedBindKey } from "./jsx-runtime.ts"; import type { HtmlNode } from "./jsx-runtime.ts"; // Append extra classes without clobbering a structural base. function cx(base: string, extra?: string): string { return extra ? `${base} ${extra}`.trim() : base; } // ── ────────────────────────────────────────────────────────────────── // An accessible on/off toggle (role="switch"). Bind it to an @expose boolean: // clicking (or Space/Enter — native on a }>… arbitrary content … export interface PopoverProps { label?: unknown; trigger?: unknown; class?: string; buttonClass?: string; panelClass?: string; children?: unknown; [key: string]: unknown; } /** * A generic anchored panel: click to open, click-outside or Escape to close — the rigorous, * unstyled cousin of ``. `data-open` is exposed for styling. * @category Headless components */ export function Popover(props: PopoverProps): HtmlNode { const { label, trigger, class: cls, buttonClass, panelClass, children, ...rest } = props; const button = trigger ? jsx("span", { "x-on:click": "open = !open", ":aria-expanded": "open", ":data-open": "open ? '' : null", role: "button", tabindex: 0, class: cx("flow-popover-button", buttonClass), children: trigger, }) : jsx("button", { type: "button", "x-on:click": "open = !open", ":aria-expanded": "open", ":data-open": "open ? '' : null", class: cx("flow-popover-button", buttonClass), children: label ?? "Open", }); const panel = jsx("div", { "x-show": "open", "x-cloak": true, "x-transition": true, "x-on:click.outside": "open = false", "x-on:keydown.escape.window": "open = false", ":data-open": "open ? '' : null", class: cx("flow-popover-panel", panelClass), children, }); return jsx("div", { ...rest, "x-data": "{ open: false }", class: cx("flow-popover", cls), children: [button, panel], }); } // ── ────────────────────────────────────────────────────────────── // An accessible checkbox (role="checkbox") bound to an @expose boolean. Like // but checkbox-semantic. Style the checked state with `data-[checked]:…`. // // export interface CheckboxProps { bind: unknown; class?: string; children?: unknown; [key: string]: unknown; } /** * An accessible checkbox (`role="checkbox"`) bound to an `@expose` boolean — like * {@link Switch} but checkbox-semantic. Style the checked state with `data-[checked]:…`. * @category Headless components */ export function Checkbox(props: CheckboxProps): HtmlNode { const { bind, class: cls, children, ...rest } = props; const name = _injectedBindKey(props, "bind") ?? _resolveReactiveName(bind); const on = Boolean(bind); const out: Record = { ...rest, type: "button", role: "checkbox", tabindex: 0, "aria-checked": on ? "true" : "false", class: cx("flow-checkbox", cls), children, }; if (on) out["data-checked"] = ""; if (name) { out["flow:bind:attr"] = JSON.stringify({ "aria-checked": name, "data-checked": name }); out["x-on:click"] = `$flow.$set('${name}', !$flow.${name})`; } return jsx("button", out); } // ── bound to an @expose value (via flow:model). Reliable // and fully accessible for free — reach for only when you need custom // option markup. // // ` bound to an `@expose` value via `flow:model` — reliable and * fully accessible for free. Reach for {@link Listbox} only when you need custom option markup. * @category Headless components * * @example * ```tsx * // // // You can also compose