// 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 (``, ``, // ``) 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`/`themedPrompt`/`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 — side-effect import import './hive-toast/hive-toast.js'; // registers — 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` = 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; }); } // themedPrompt({ title, message, label, placeholder, value, confirmLabel, cancelLabel }) // → Promise. Themed replacement for window.prompt(): a // resizable