Y3R C4LL is the operator inbox alone: no P3NDING APPR0VALS section, no approval cards or history, no approval_resolved subscription, no approval browser notifications, and the tab count and title prefix count unread operator messages only. The H0M3 tile text drops "approvals". The deny form was the only data-prompt form, so bindAsyncForms' prompt branch, themedPrompt and its dialog styles go too, along with the approval-only CSS (.approval*, .approvals-history, .btn-deny, the status/glyph colours and .history-note).
141 lines
5.3 KiB
JavaScript
141 lines
5.3 KiB
JavaScript
// modal.js — reusable themed modal/dialog helpers, shared by the
|
|
// dashboard and the per-agent UI. An in-theme replacement for the
|
|
// browser's native `confirm()` / `alert()` overlays so destructive
|
|
// actions and prompts match each page's chrome instead of a jarring OS
|
|
// dialog.
|
|
//
|
|
// The custom elements themselves (`<hive-dialog>`, `<hive-toast>`,
|
|
// `<hive-btn>`) each live in their own directory (one component = one
|
|
// dir: `hive-dialog/`, `hive-toast/`, `hive-btn/`) — real per-component
|
|
// style encapsulation, CSS in real `.css` files imported as raw text
|
|
// (see each component's own header comment for the design rationale).
|
|
// This file is the orchestration layer on top: `openDialog` is the
|
|
// general primitive (any title/message/content + a row of buttons);
|
|
// `themedConfirm`/`themedToast` are thin wrappers built on
|
|
// it. It's the single entry point external packages import
|
|
// (`@hive/shared/modal.js`) — the component internals aren't exported
|
|
// individually.
|
|
//
|
|
// Other `.btn` consumers across the app stay on the light-DOM `.btn`
|
|
// class for now — migrating them is a separate follow-up.
|
|
|
|
import { el } from "./dom.js";
|
|
import "./hive-dialog/hive-dialog.js"; // registers <hive-dialog> — side-effect import
|
|
import "./hive-toast/hive-toast.js"; // registers <hive-toast> — side-effect import
|
|
|
|
// openDialog({ title, message, content, buttons, danger, dismissable })
|
|
// → Promise resolving to the clicked button's `value`, or `null` when the
|
|
// dialog is dismissed (Escape, backdrop click, or a button whose value is
|
|
// null). `content` is an optional DOM node rendered between the message
|
|
// and the buttons (checkboxes, custom fields, …) — built by the caller
|
|
// with `el()` and appended into the dialog's shadow root once mounted,
|
|
// same as any other node (a JS-created element isn't bound to a
|
|
// document/shadow-root until it's actually appended somewhere).
|
|
// `buttons` is `[{ label, value, danger?, class?, autofocus? }]`,
|
|
// rendered right-aligned. Initial focus: the `autofocus` button if any,
|
|
// else — for a `danger` dialog — the first non-destructive button (so a
|
|
// stray Enter can't fire the destructive path), else the last button.
|
|
export function openDialog(opts = {}) {
|
|
return new Promise((resolve) => {
|
|
const dlg = document.createElement("hive-dialog");
|
|
dlg._opts = opts;
|
|
dlg.addEventListener("hive-dialog-close", (e) => resolve(e.detail), {
|
|
once: true,
|
|
});
|
|
document.body.append(dlg);
|
|
});
|
|
}
|
|
|
|
// themedConfirm({ title, message, danger, confirmLabel, cancelLabel, checkboxes })
|
|
// → Promise<null | { [name]: bool }>. `null` = cancelled; otherwise an
|
|
// object of the checkbox states keyed by `name` (`{}` when there are none).
|
|
// Example:
|
|
// const r = await themedConfirm({ message: `stop ${n}?`, danger: true,
|
|
// confirmLabel: '■ stop', checkboxes: [{ name: 'graceful', label: '…' }] });
|
|
// if (!r) return; // cancelled
|
|
// doStop(r.graceful);
|
|
export function themedConfirm(opts = {}) {
|
|
const {
|
|
title = "",
|
|
message = "",
|
|
danger = false,
|
|
confirmLabel = "confirm",
|
|
cancelLabel = "cancel",
|
|
checkboxes = [],
|
|
} = opts;
|
|
|
|
const boxes = checkboxes.map((cb) => {
|
|
const input = el("input", {
|
|
type: "checkbox",
|
|
class: "check",
|
|
name: cb.name,
|
|
});
|
|
if (cb.checked) input.checked = true;
|
|
const row = el(
|
|
"label",
|
|
{ class: "checkrow" },
|
|
input,
|
|
el("span", {}, cb.label || cb.name),
|
|
);
|
|
return { input, row };
|
|
});
|
|
const content = boxes.length
|
|
? el("div", { class: "checks" }, ...boxes.map((b) => b.row))
|
|
: null;
|
|
|
|
return openDialog({
|
|
title,
|
|
message,
|
|
content,
|
|
danger,
|
|
buttons: [
|
|
{ label: cancelLabel, value: null, class: "cancel", autofocus: danger },
|
|
{
|
|
label: confirmLabel,
|
|
value: "confirm",
|
|
danger,
|
|
class: "confirm",
|
|
autofocus: !danger,
|
|
},
|
|
],
|
|
}).then((v) => {
|
|
if (v !== "confirm") return null;
|
|
const out = {};
|
|
for (let i = 0; i < boxes.length; i++)
|
|
out[checkboxes[i].name] = boxes[i].input.checked;
|
|
return out;
|
|
});
|
|
}
|
|
|
|
// themedToast(message, { type, duration }) — non-blocking transient
|
|
// notification; a lighter alternative to a modal for feedback that needs no
|
|
// decision (errors, validation, status). Stacks in a fixed top-right
|
|
// container, auto-dismisses after `duration` ms (errors linger longer), and
|
|
// can be clicked to dismiss early. `type` ∈ {'info','error','ok'}.
|
|
// The stack container is a plain positioning wrapper (not itself a
|
|
// component — no theming, no encapsulation need), so it's styled with a
|
|
// one-off inline style rather than a stylesheet.
|
|
export function themedToast(message, opts = {}) {
|
|
let container = document.getElementById("tc-toasts");
|
|
if (!container) {
|
|
container = document.createElement("div");
|
|
container.id = "tc-toasts";
|
|
Object.assign(container.style, {
|
|
position: "fixed",
|
|
top: "1em",
|
|
right: "1em",
|
|
zIndex: "1100",
|
|
display: "flex",
|
|
flexDirection: "column",
|
|
gap: "0.5em",
|
|
maxWidth: "min(28em, 92vw)",
|
|
pointerEvents: "none",
|
|
});
|
|
document.body.append(container);
|
|
}
|
|
const toast = document.createElement("hive-toast");
|
|
toast._message = message;
|
|
toast._opts = opts;
|
|
container.append(toast);
|
|
return toast;
|
|
}
|