BL-024 - the last 21 native dialogs, onto the shared kit
S1 counted 79 native dialogs app-wide and its tasks removed 58; the audit found the rest on surfaces no S1 task named: admin.js (6), users.js (10), the launcher's inline script (5). All 21 now go through wp-dialog.js - the T7.9 kit extracted as a self-injecting shared component: markup and styles land on first use, styles are theme tokens only with its own wp-dlg-* class names (the consoles' existing .modal styles are untouched), 44px targets on coarse pointers, and the whole file is guarded so the creator's inline copy - which owns the same-id markup in its HTML - still wins on its own page. The kit's toast comes along (S10 role rules), since none of the three pages had one. Conversion follows the T7.9 precedent: confirms -> wpConfirmDialog with named ok-labels, the password prompt -> wpPromptDialog whose validate() finally enforces min-12 AT the input (it was label-text-only before, server-enforced), API failures with detail -> wpAlertDialog, small info/validation messages -> the announced toast. New probe console_dialogs_check (17): counts pinned at 0, kit guarded and loaded by all three pages, and the users console driven live with natives poisoned - reset a password end to end (short refused inline, good one accepted by the server and announced), cancel a delete and prove nothing died. Items: BL-024 (closed), S1 completed to zero app-wide, C1. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
172
html/wp-dialog.js
Normal file
172
html/wp-dialog.js
Normal file
@@ -0,0 +1,172 @@
|
||||
/* Dialog kit + toast, shared (BL-024, 2026-08-20).
|
||||
*
|
||||
* The T7.9 kit, extracted for the pages the S1 tasks never named: the launcher
|
||||
* (index.html), the admin console and the user console carried 21 native
|
||||
* dialogs between them. Same contract as the creator's copy:
|
||||
*
|
||||
* wpConfirmDialog({title, message, okLabel, cancelLabel}) -> Promise<bool>
|
||||
* wpPromptDialog({title, message, label, value, validate}) -> Promise<string|null>
|
||||
* wpAlertDialog({title, message, okLabel}) -> Promise (value not meaningful)
|
||||
* toast(msg, kind) kind 'alert' interrupts (role=alert); default role=status
|
||||
*
|
||||
* Self-contained on purpose: markup and styles are injected on first use, the
|
||||
* styles are theme tokens only (the token rule), and the class names are its
|
||||
* own (wp-dlg-*) so the consoles' existing .modal styles are never touched.
|
||||
* The creator keeps its inline copy - it owns the same-id markup in its HTML -
|
||||
* so everything here is guarded: if the page already has the kit, this file
|
||||
* defines nothing.
|
||||
*/
|
||||
(function (global) {
|
||||
'use strict';
|
||||
if (typeof global.wpConfirmDialog === 'function') return; // the creator's copy wins
|
||||
|
||||
var CSS =
|
||||
'#wp-dlg-overlay{position:fixed;inset:0;background:var(--wp-scrim-cool-strong);' +
|
||||
'display:none;align-items:center;justify-content:center;z-index:10500;padding:20px;}' +
|
||||
'#wp-dlg-overlay.open{display:flex;}' +
|
||||
'.wp-dlg{background:var(--cds-layer);color:var(--cds-text-primary);max-width:480px;width:100%;' +
|
||||
'border-radius:8px;box-shadow:0 12px 40px rgba(20,30,50,.3);overflow:hidden;' +
|
||||
'font-family:ui-sans-serif,system-ui,-apple-system,"Segoe UI",sans-serif;font-size:14px;}' +
|
||||
'.wp-dlg-head{display:flex;align-items:center;justify-content:space-between;padding:14px 18px;' +
|
||||
'border-bottom:1px solid var(--cds-border-subtle);font-weight:700;}' +
|
||||
'.wp-dlg-x{background:none;border:none;font-size:18px;line-height:1;cursor:pointer;' +
|
||||
'color:var(--cds-text-secondary);padding:4px 6px;}' +
|
||||
'.wp-dlg-x:focus-visible{outline:2px solid var(--cds-focus);outline-offset:1px;}' +
|
||||
'.wp-dlg-body{padding:16px 18px;}' +
|
||||
'#wp-dlg-msg{white-space:pre-wrap;line-height:1.5;}' +
|
||||
'#wp-dlg-input-wrap{margin-top:10px;}' +
|
||||
'#wp-dlg-input-wrap label{display:block;font-size:12px;margin-bottom:4px;color:var(--cds-text-secondary);}' +
|
||||
'#wp-dlg-input{width:100%;box-sizing:border-box;padding:8px 10px;font:inherit;' +
|
||||
'border:1px solid var(--cds-border-strong);border-radius:4px;background:var(--cds-field);}' +
|
||||
'#wp-dlg-input:focus{outline:2px solid var(--cds-focus);outline-offset:-1px;}' +
|
||||
'#wp-dlg-err{color:var(--cds-text-error);font-size:12px;font-weight:600;margin-top:4px;}' +
|
||||
'#wp-dlg-err:empty{display:none;}' +
|
||||
'.wp-dlg-foot{display:flex;justify-content:flex-end;gap:10px;padding:12px 18px;' +
|
||||
'border-top:1px solid var(--cds-border-subtle);}' +
|
||||
'.wp-dlg-btn{font:inherit;font-weight:600;padding:8px 16px;border-radius:6px;cursor:pointer;' +
|
||||
'border:1px solid var(--cds-border-strong);background:var(--cds-layer);color:var(--cds-text-primary);}' +
|
||||
'.wp-dlg-btn.primary{background:var(--cds-interactive-01);border-color:var(--cds-interactive-01);' +
|
||||
'color:var(--cds-text-on-color);}' +
|
||||
'.wp-dlg-btn:focus-visible{outline:2px solid var(--cds-focus);outline-offset:1px;}' +
|
||||
'@media(pointer:coarse){.wp-dlg-btn{min-height:44px;}.wp-dlg-x{min-width:44px;min-height:44px;}}' +
|
||||
'#toast{position:fixed;bottom:26px;left:50%;transform:translateX(-50%) translateY(20px);' +
|
||||
'background:var(--cds-background-inverse);color:var(--cds-text-inverse);padding:9px 16px;' +
|
||||
'border-radius:6px;font-size:13px;opacity:0;transition:opacity .18s,transform .18s;' +
|
||||
'pointer-events:none;z-index:10600;max-width:min(480px,calc(100vw - 32px));}' +
|
||||
'#toast.show{opacity:1;transform:translateX(-50%) translateY(0);}';
|
||||
|
||||
function ensure() {
|
||||
var ov = document.getElementById('wp-dlg-overlay');
|
||||
if (ov) return ov;
|
||||
var st = document.createElement('style');
|
||||
st.textContent = CSS;
|
||||
document.head.appendChild(st);
|
||||
ov = document.createElement('div');
|
||||
ov.id = 'wp-dlg-overlay';
|
||||
ov.setAttribute('role', 'dialog');
|
||||
ov.setAttribute('aria-modal', 'true');
|
||||
ov.setAttribute('aria-labelledby', 'wp-dlg-title');
|
||||
ov.innerHTML =
|
||||
'<div class="wp-dlg">' +
|
||||
'<div class="wp-dlg-head"><div id="wp-dlg-title"></div>' +
|
||||
'<button type="button" class="wp-dlg-x" id="wp-dlg-x" title="Cancel" aria-label="Cancel">✕</button></div>' +
|
||||
'<div class="wp-dlg-body">' +
|
||||
'<div id="wp-dlg-msg"></div>' +
|
||||
'<div id="wp-dlg-input-wrap">' +
|
||||
'<label id="wp-dlg-label" for="wp-dlg-input"></label>' +
|
||||
'<input type="text" id="wp-dlg-input">' +
|
||||
'<div id="wp-dlg-err" role="alert"></div>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'<div class="wp-dlg-foot">' +
|
||||
'<button type="button" class="wp-dlg-btn" id="wp-dlg-cancel">Cancel</button>' +
|
||||
'<button type="button" class="wp-dlg-btn primary" id="wp-dlg-ok">OK</button>' +
|
||||
'</div>' +
|
||||
'</div>';
|
||||
document.body.appendChild(ov);
|
||||
document.getElementById('wp-dlg-x').addEventListener('click', cancel);
|
||||
document.getElementById('wp-dlg-cancel').addEventListener('click', cancel);
|
||||
document.getElementById('wp-dlg-ok').addEventListener('click', ok);
|
||||
document.getElementById('wp-dlg-input').addEventListener('keydown', function (e) {
|
||||
if (e.key === 'Enter') ok();
|
||||
});
|
||||
document.addEventListener('keydown', function (e) {
|
||||
if (e.key === 'Escape' && ov.classList.contains('open')) cancel();
|
||||
});
|
||||
return ov;
|
||||
}
|
||||
|
||||
var resolveFn = null;
|
||||
|
||||
function open(opts) {
|
||||
return new Promise(function (res) {
|
||||
resolveFn = res;
|
||||
var ov = ensure();
|
||||
ov._opts = opts || {};
|
||||
document.getElementById('wp-dlg-title').textContent = opts.title || 'Confirm';
|
||||
document.getElementById('wp-dlg-msg').textContent = opts.message || '';
|
||||
document.getElementById('wp-dlg-input-wrap').style.display = opts.input ? '' : 'none';
|
||||
document.getElementById('wp-dlg-label').textContent = opts.label || '';
|
||||
var inp = document.getElementById('wp-dlg-input');
|
||||
inp.value = (opts.value != null ? String(opts.value) : '');
|
||||
document.getElementById('wp-dlg-err').textContent = '';
|
||||
document.getElementById('wp-dlg-ok').textContent = opts.okLabel || 'OK';
|
||||
var cb = document.getElementById('wp-dlg-cancel');
|
||||
cb.textContent = opts.cancelLabel || 'Cancel';
|
||||
cb.style.display = opts.okOnly ? 'none' : '';
|
||||
ov.classList.add('open');
|
||||
setTimeout(function () {
|
||||
(opts.input ? inp : document.getElementById('wp-dlg-ok')).focus();
|
||||
}, 0);
|
||||
});
|
||||
}
|
||||
|
||||
function close(val) {
|
||||
var ov = document.getElementById('wp-dlg-overlay');
|
||||
if (ov) ov.classList.remove('open');
|
||||
var r = resolveFn;
|
||||
resolveFn = null;
|
||||
if (r) r(val);
|
||||
}
|
||||
|
||||
function ok() {
|
||||
var ov = document.getElementById('wp-dlg-overlay');
|
||||
var opts = (ov && ov._opts) || {};
|
||||
if (opts.input) {
|
||||
var v = document.getElementById('wp-dlg-input').value;
|
||||
if (opts.validate) {
|
||||
var err = opts.validate(v);
|
||||
if (err) {
|
||||
document.getElementById('wp-dlg-err').textContent = err;
|
||||
document.getElementById('wp-dlg-input').focus();
|
||||
return;
|
||||
}
|
||||
}
|
||||
close(v);
|
||||
} else close(true);
|
||||
}
|
||||
|
||||
function cancel() {
|
||||
var ov = document.getElementById('wp-dlg-overlay');
|
||||
var opts = (ov && ov._opts) || {};
|
||||
close(opts.input ? null : false);
|
||||
}
|
||||
|
||||
global.wpConfirmDialog = function (opts) { return open(Object.assign({}, opts, { input: false })); };
|
||||
global.wpPromptDialog = function (opts) { return open(Object.assign({}, opts, { input: true })); };
|
||||
global.wpAlertDialog = function (opts) { return open(Object.assign({}, opts, { input: false, okOnly: true })); };
|
||||
|
||||
if (typeof global.toast !== 'function') {
|
||||
// S10's rule, same as the creator: role BEFORE text, 'alert' interrupts.
|
||||
global.toast = function (msg, kind) {
|
||||
ensure();
|
||||
var t = document.getElementById('toast');
|
||||
if (!t) { t = document.createElement('div'); t.id = 'toast'; document.body.appendChild(t); }
|
||||
t.setAttribute('role', kind === 'alert' ? 'alert' : 'status');
|
||||
t.textContent = msg;
|
||||
t.classList.add('show');
|
||||
clearTimeout(global.toast._t);
|
||||
global.toast._t = setTimeout(function () { t.classList.remove('show'); }, 2200);
|
||||
};
|
||||
}
|
||||
})(window);
|
||||
Reference in New Issue
Block a user