dashboard: theme all remaining dialogs (replace native confirm/alert/prompt)

Follow-up to the themed-modal component: route every remaining native
browser dialog through modal.js so nothing falls back to the OS chrome.

- modal.js: add themedPrompt (input dialog) + themedToast (non-blocking
  transient notification, info/error/ok) alongside openDialog/themedConfirm.
- Migrate call sites: bindAsyncForms confirm/prompt/alerts (common.js),
  the answer-validation alert (call.js), the M0V3 reparent confirm + action
  toasts (tabs.js), and the schedule form/cancel/fire confirms + validation
  and error alerts (schedules.js).
- UX: blocking modal for confirms/prompts; non-blocking toast for transient
  errors + validation. Destructive confirms keep the danger styling.
- CSS for the toast stack + prompt input.

common.js <-> modal.js is a safe deferred import cycle (usage is call-time
only); esbuild bundles it clean.
This commit is contained in:
iris 2026-06-19 02:03:46 +02:00 committed by mara
commit 7c2de690ec
6 changed files with 141 additions and 32 deletions

View file

@ -4,6 +4,10 @@
// infrastructure for the side panel.
import { linkify as termLinkify } from '@hive/shared/terminal.js';
// Themed dialog/toast helpers (modal.js imports `el` back from here — a safe
// deferred cycle: neither side uses the other at module-init time, only inside
// runtime handlers).
import { themedConfirm, themedPrompt, themedToast } from './modal.js';
// ─── helpers ────────────────────────────────────────────────────────────
export const $ = (id) => document.getElementById(id);
@ -58,9 +62,9 @@ export function bindAsyncForms(onSuccess) {
const f = e.target;
if (!(f instanceof HTMLFormElement) || !f.hasAttribute('data-async')) return;
e.preventDefault();
if (f.dataset.confirm && !confirm(f.dataset.confirm)) return;
if (f.dataset.confirm && !(await themedConfirm({ message: f.dataset.confirm }))) return;
if (f.dataset.prompt) {
const ans = prompt(f.dataset.prompt, '');
const ans = await themedPrompt({ message: f.dataset.prompt });
if (ans === null) return; // operator hit Cancel
// Drop into a hidden input named after `data-prompt-field` (or
// 'note' by default) so the value rides along on the POST.
@ -88,7 +92,7 @@ export function bindAsyncForms(onSuccess) {
|| (resp.status >= 200 && resp.status < 400);
if (!ok) {
const text = await resp.text().catch(() => '');
alert('action failed: ' + resp.status + (text ? '\n\n' + text : ''));
themedToast('action failed: ' + resp.status + (text ? '\n\n' + text : ''), { type: 'error' });
if (btn) { btn.disabled = false; btn.innerHTML = original; }
return;
}
@ -101,7 +105,7 @@ export function bindAsyncForms(onSuccess) {
onSuccess();
}
} catch (err) {
alert('action failed: ' + err);
themedToast('action failed: ' + err, { type: 'error' });
if (btn) { btn.disabled = false; btn.innerHTML = original; }
}
});