UNPKG

trellis

Version:

Agentic State Engine — event-sourced causal graph with branching, decision traces, and realtime sync for AI-native applications

141 lines (139 loc) 3.93 kB
// src/dialog/core/index.ts var idCounter = 0; function nextId() { idCounter += 1; return `dialog-${idCounter}`; } var DEFAULT_BACKDROP = { modal: true, nonmodal: false, alert: true, confirm: true }; function deriveA11y(id, kind, spec) { const modal = kind === "modal" || kind === "alert" || kind === "confirm"; return { role: kind === "alert" || kind === "confirm" ? "alertdialog" : "dialog", ariaModal: modal, labelledBy: spec.title ? `${id}-title` : null, describedBy: spec.description ? `${id}-description` : null, focusTarget: spec.focusTarget ?? "auto", escToDismiss: spec.escToDismiss ?? true, backdrop: spec.backdrop ?? DEFAULT_BACKDROP[kind], dismissOnBackdrop: spec.dismissOnBackdrop ?? false }; } function deriveState(stack) { return { stack, top: stack.length > 0 ? stack[stack.length - 1] : null, count: stack.length, focusLocked: stack.some((d) => d.a11y.ariaModal && d.status === "open") }; } function createDialogCore(config = {}) { const defaultKind = config.defaultKind ?? "modal"; const resolvers = /* @__PURE__ */ new Map(); let stack = []; const subscribers = /* @__PURE__ */ new Set(); const notify = () => subscribers.forEach((fn) => fn()); function pushInstance(instance) { stack = [...stack, instance]; notify(); } function removeInstance(id) { stack = stack.filter((d) => d.id !== id); notify(); } function resolve(id, result) { const resolver = resolvers.get(id); if (!resolver) return; resolvers.delete(id); resolver(result); } const actions = { open: (spec) => { const id = nextId(); const kind = spec.kind ?? defaultKind; const instance = { id, kind, spec, status: "open", openedAt: stack.length + 1, a11y: deriveA11y(id, kind, spec) }; const promise = new Promise((resolvePromise) => { resolvers.set(id, resolvePromise); }); pushInstance(instance); return promise; }, close: (id, value) => { if (!stack.some((d) => d.id === id)) return; removeInstance(id); resolve(id, { status: "ok", value }); }, closeTop: (value) => { const top = stack[stack.length - 1]; if (!top) return; removeInstance(top.id); resolve(top.id, { status: "ok", value }); }, dismiss: (id) => { if (!stack.some((d) => d.id === id)) return; removeInstance(id); resolve(id, { status: "dismissed" }); }, closeWithButton: (id, buttonId) => { const instance = stack.find((d) => d.id === id); const button = instance?.spec.buttons?.find((b) => b.id === buttonId); if (!instance || !button || button.disabled) return; removeInstance(id); resolve(id, { status: "ok", value: buttonId }); }, replaceTop: (spec) => { const top = stack[stack.length - 1]; const id = top?.id ?? nextId(); const kind = spec.kind ?? top?.kind ?? defaultKind; const instance = { id, kind, spec, status: "open", openedAt: top?.openedAt ?? stack.length + 1, a11y: deriveA11y(id, kind, spec) }; stack = top ? [...stack.slice(0, -1), instance] : [instance]; notify(); return id; }, setClosing: (id) => { stack = stack.map((d) => d.id === id ? { ...d, status: "closing" } : d); notify(); }, finishClosing: (id) => { removeInstance(id); }, clear: () => { const ids = stack.map((d) => d.id); stack = []; for (const id of ids) resolve(id, { status: "dismissed" }); notify(); } }; const core = { get state() { return deriveState(stack); }, actions, subscribe: (listener) => { subscribers.add(listener); return () => subscribers.delete(listener); } }; return core; } export { createDialogCore };