// ── 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);
}
// ──