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
JavaScript
// 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
};