chore(embedded): cut v0.0.27-beta
Some checks failed
Notify chart dev on beta cut / notify-chart-dev (push) Failing after 9s

This commit is contained in:
ZDDC 2026-06-10 13:32:10 -05:00
parent 203674ee4c
commit 589d804716
7 changed files with 3973 additions and 390 deletions

View file

@ -814,23 +814,75 @@ body.help-open .app-header {
with tool-local .toast classes; the old classifier rules can stay
alongside until this file is concatenated above them in the build. */
.zddc-toast {
/* Toast STACK — bottom-right, newest at the bottom. The container is
click-through (pointer-events:none) so the gaps don't block the page; each
toast + button re-enables pointer events. */
.zddc-toasts {
position: fixed;
bottom: 2rem;
right: 2rem;
bottom: 1.5rem;
right: 1.5rem;
z-index: 9000;
display: flex;
flex-direction: column;
align-items: flex-end;
gap: 0.5rem;
max-height: calc(100vh - 3rem);
overflow-y: auto;
pointer-events: none;
}
/* "Clear all" — shown above the stack when 2+ toasts are present. */
.zddc-toasts__clear {
pointer-events: auto;
align-self: flex-end;
background: var(--bg);
color: var(--text);
padding: 0.875rem 1.25rem;
border: 1px solid var(--border);
border-radius: var(--radius);
padding: 0.2rem 0.6rem;
font-size: 0.78rem;
cursor: pointer;
}
.zddc-toasts__clear:hover { background: var(--bg-secondary, rgba(0, 0, 0, 0.05)); }
.zddc-toast {
position: relative;
pointer-events: auto;
background: var(--bg);
color: var(--text);
padding: 0.7rem 1.7rem 0.7rem 1rem; /* room for the × at top-right */
border-radius: var(--radius);
border: 1px solid var(--border);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
z-index: 9000;
max-width: 400px;
max-width: 420px;
font-size: 0.875rem;
cursor: pointer;
animation: zddc-toast-in 0.3s ease-out;
}
/* Message text — selectable + copyable; long/multi-line errors wrap. */
.zddc-toast__msg {
user-select: text;
-webkit-user-select: text;
cursor: text;
white-space: pre-wrap;
word-break: break-word;
}
/* Per-toast dismiss. */
.zddc-toast__close {
position: absolute;
top: 0.2rem;
right: 0.35rem;
border: none;
background: transparent;
color: var(--text-muted, #888);
font-size: 1.15rem;
line-height: 1;
cursor: pointer;
padding: 0 0.15rem;
}
.zddc-toast__close:hover { color: var(--text); }
.zddc-toast--success { border-left: 4px solid var(--success); }
.zddc-toast--error { border-left: 4px solid var(--danger); }
.zddc-toast--info { border-left: 4px solid var(--info); }
@ -2665,7 +2717,7 @@ td[data-field="trackingNumber"] {
</svg>
<div class="header-title-group">
<span class="app-header__title">ZDDC Archive</span>
<span class="build-timestamp"><span style="color:red;font-weight:bold">v0.0.27-beta · 2026-06-09 15:30:13 · 237c353</span></span>
<span class="build-timestamp"><span style="color:red;font-weight:bold">v0.0.27-beta · 2026-06-10 18:32:03 · 203674e</span></span>
</div>
<button id="addDirectoryBtn" class="btn btn-primary">Use Local Directory</button>
<button id="refreshHeaderBtn" class="btn btn-secondary hidden" title="Refresh Data"></button>
@ -5037,74 +5089,122 @@ X.B(E,Y);return E}return J}())
}());
// shared/toast.js — non-blocking notification helper available to every
// tool via window.zddc.toast(msg, level, opts). Originated as classifier's
// local showToast (classifier/js/excel.js); promoted here so tools that
// today use alert() or silent console.error can switch to a uniform
// non-blocking surface.
// tool via window.zddc.toast(msg, level, opts).
//
// Toasts collect in a bottom-right STACK. Error/warning toasts are STICKY —
// they stay until the user dismisses them (per-toast × or a "Clear all"
// button) so the message can be read, selected, and copied while
// troubleshooting. info/success toasts auto-dismiss. The message text is
// always selectable.
//
// Usage:
// window.zddc.toast('Saved.', 'success');
// window.zddc.toast('Could not load: ' + err.message, 'error');
// window.zddc.toast('Saved.', 'success'); // auto-dismiss
// window.zddc.toast('Could not load: ' + e.message, 'error'); // sticky
// window.zddc.toast('Note', 'info', { durationMs: 3000 });
// window.zddc.toast('Heads up', 'info', { durationMs: 0 }); // force sticky
//
// Levels: 'info' (default) | 'success' | 'warning' | 'error'.
// Each tool may also expose app.notify(msg, level) as a thin wrapper —
// see ARCHITECTURE.md for the convention.
(function () {
'use strict';
if (!window.zddc) window.zddc = {};
// Don't overwrite if a tool defined its own first.
if (typeof window.zddc.toast === 'function') return;
var DEFAULT_DURATION_MS = 5000;
var FADE_MS = 300;
// Levels that persist until the user dismisses them (troubleshooting).
var STICKY = { error: true, warning: true };
function container() {
var c = document.getElementById('zddc-toasts');
if (c) return c;
c = document.createElement('div');
c.id = 'zddc-toasts';
c.className = 'zddc-toasts';
document.body.appendChild(c);
return c;
}
// Show/hide a "Clear all" control when 2+ toasts are stacked.
function refreshClearAll(c) {
var bar = c.querySelector('.zddc-toasts__clear');
var count = c.querySelectorAll('.zddc-toast').length;
if (count >= 2) {
if (!bar) {
bar = document.createElement('button');
bar.type = 'button';
bar.className = 'zddc-toasts__clear';
bar.textContent = 'Clear all';
bar.addEventListener('click', function () {
var all = c.querySelectorAll('.zddc-toast');
for (var i = 0; i < all.length; i++) dismiss(all[i]);
});
c.insertBefore(bar, c.firstChild);
}
} else if (bar) {
bar.remove();
}
}
function dismiss(el) {
if (el._dismissed) return;
el._dismissed = true;
if (el._timer) clearTimeout(el._timer);
el.classList.add('zddc-toast--fade');
setTimeout(function () {
if (el.parentNode) el.parentNode.removeChild(el);
refreshClearAll(container());
}, FADE_MS);
}
function toast(message, level, opts) {
opts = opts || {};
var lvl = (level === 'success' || level === 'error' ||
level === 'warning') ? level : 'info';
// Single-toast policy: dismiss any existing toast immediately
// so the new one is always the most recent. Matches the
// classifier's prior behavior and avoids stack-of-toasts UX.
var existing = document.querySelector('.zddc-toast');
if (existing) existing.remove();
var c = container();
var el = document.createElement('div');
el.className = 'zddc-toast zddc-toast--' + lvl;
// ARIA: errors get assertive (interrupts SR queue), others polite.
el.setAttribute('role', lvl === 'error' ? 'alert' : 'status');
el.setAttribute('aria-live', lvl === 'error' ? 'assertive' : 'polite');
el.textContent = message == null ? '' : String(message);
document.body.appendChild(el);
var dur = typeof opts.durationMs === 'number' ?
opts.durationMs : DEFAULT_DURATION_MS;
var timer = setTimeout(function () {
el.classList.add('zddc-toast--fade');
setTimeout(function () {
if (el.parentNode) el.parentNode.removeChild(el);
}, FADE_MS);
}, dur);
// Selectable, copyable message text (its own element so clicking to
// select doesn't dismiss the toast — only the × does).
var msg = document.createElement('span');
msg.className = 'zddc-toast__msg';
msg.textContent = message == null ? '' : String(message);
el.appendChild(msg);
// Click-to-dismiss. Useful for sticky errors the user wants gone.
el.addEventListener('click', function () {
clearTimeout(timer);
if (el.parentNode) el.parentNode.removeChild(el);
});
var close = document.createElement('button');
close.type = 'button';
close.className = 'zddc-toast__close';
close.setAttribute('aria-label', 'Dismiss');
close.textContent = '×';
close.addEventListener('click', function () { dismiss(el); });
el.appendChild(close);
c.appendChild(el);
// Sticky (error/warning, or opts.durationMs === 0) persists; otherwise
// auto-dismiss after the (overridable) duration.
var sticky = opts.durationMs === 0 ||
(typeof opts.durationMs !== 'number' && STICKY[lvl]);
if (!sticky) {
var dur = typeof opts.durationMs === 'number'
? opts.durationMs : DEFAULT_DURATION_MS;
el._timer = setTimeout(function () { dismiss(el); }, dur);
}
refreshClearAll(c);
return el;
}
window.zddc.toast = toast;
// Route window.alert() calls into the toast helper. Every tool has
// accumulated some `alert(...)` sites for error reporting; rather
// than touch each one, intercept globally so they're non-blocking
// and ARIA-announced consistently. Native alert is preserved on
// window.alertNative for the rare case where a truly modal block
// is needed (e.g. before navigating away with unsaved changes).
// Route window.alert() into the toast helper (non-blocking, ARIA-announced,
// consistent). Native alert preserved on window.alertNative. alert() maps
// to an error toast (sticky) since that's its usual purpose.
if (typeof window.alert === 'function' && !window.alertNative) {
window.alertNative = window.alert.bind(window);
window.alert = function (msg) {

View file

@ -814,23 +814,75 @@ body.help-open .app-header {
with tool-local .toast classes; the old classifier rules can stay
alongside until this file is concatenated above them in the build. */
.zddc-toast {
/* Toast STACK — bottom-right, newest at the bottom. The container is
click-through (pointer-events:none) so the gaps don't block the page; each
toast + button re-enables pointer events. */
.zddc-toasts {
position: fixed;
bottom: 2rem;
right: 2rem;
bottom: 1.5rem;
right: 1.5rem;
z-index: 9000;
display: flex;
flex-direction: column;
align-items: flex-end;
gap: 0.5rem;
max-height: calc(100vh - 3rem);
overflow-y: auto;
pointer-events: none;
}
/* "Clear all" — shown above the stack when 2+ toasts are present. */
.zddc-toasts__clear {
pointer-events: auto;
align-self: flex-end;
background: var(--bg);
color: var(--text);
padding: 0.875rem 1.25rem;
border: 1px solid var(--border);
border-radius: var(--radius);
padding: 0.2rem 0.6rem;
font-size: 0.78rem;
cursor: pointer;
}
.zddc-toasts__clear:hover { background: var(--bg-secondary, rgba(0, 0, 0, 0.05)); }
.zddc-toast {
position: relative;
pointer-events: auto;
background: var(--bg);
color: var(--text);
padding: 0.7rem 1.7rem 0.7rem 1rem; /* room for the × at top-right */
border-radius: var(--radius);
border: 1px solid var(--border);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
z-index: 9000;
max-width: 400px;
max-width: 420px;
font-size: 0.875rem;
cursor: pointer;
animation: zddc-toast-in 0.3s ease-out;
}
/* Message text — selectable + copyable; long/multi-line errors wrap. */
.zddc-toast__msg {
user-select: text;
-webkit-user-select: text;
cursor: text;
white-space: pre-wrap;
word-break: break-word;
}
/* Per-toast dismiss. */
.zddc-toast__close {
position: absolute;
top: 0.2rem;
right: 0.35rem;
border: none;
background: transparent;
color: var(--text-muted, #888);
font-size: 1.15rem;
line-height: 1;
cursor: pointer;
padding: 0 0.15rem;
}
.zddc-toast__close:hover { color: var(--text); }
.zddc-toast--success { border-left: 4px solid var(--success); }
.zddc-toast--error { border-left: 4px solid var(--danger); }
.zddc-toast--info { border-left: 4px solid var(--info); }
@ -2772,7 +2824,7 @@ li.CodeMirror-hint-active {
</svg>
<div class="header-title-group">
<span class="app-header__title">ZDDC Browse</span>
<span class="build-timestamp"><span style="color:red;font-weight:bold">v0.0.27-beta · 2026-06-09 15:30:14 · 237c353</span></span>
<span class="build-timestamp"><span style="color:red;font-weight:bold">v0.0.27-beta · 2026-06-10 18:32:03 · 203674e</span></span>
</div>
<button id="addDirectoryBtn" class="btn btn-primary">Use Local Directory</button>
<button id="refreshHeaderBtn" class="btn btn-secondary hidden" title="Refresh listing" aria-label="Refresh listing"></button>
@ -5173,74 +5225,122 @@ var e=function(t,n){return e=Object.setPrototypeOf||{__proto__:[]}instanceof Arr
}());
// shared/toast.js — non-blocking notification helper available to every
// tool via window.zddc.toast(msg, level, opts). Originated as classifier's
// local showToast (classifier/js/excel.js); promoted here so tools that
// today use alert() or silent console.error can switch to a uniform
// non-blocking surface.
// tool via window.zddc.toast(msg, level, opts).
//
// Toasts collect in a bottom-right STACK. Error/warning toasts are STICKY —
// they stay until the user dismisses them (per-toast × or a "Clear all"
// button) so the message can be read, selected, and copied while
// troubleshooting. info/success toasts auto-dismiss. The message text is
// always selectable.
//
// Usage:
// window.zddc.toast('Saved.', 'success');
// window.zddc.toast('Could not load: ' + err.message, 'error');
// window.zddc.toast('Saved.', 'success'); // auto-dismiss
// window.zddc.toast('Could not load: ' + e.message, 'error'); // sticky
// window.zddc.toast('Note', 'info', { durationMs: 3000 });
// window.zddc.toast('Heads up', 'info', { durationMs: 0 }); // force sticky
//
// Levels: 'info' (default) | 'success' | 'warning' | 'error'.
// Each tool may also expose app.notify(msg, level) as a thin wrapper —
// see ARCHITECTURE.md for the convention.
(function () {
'use strict';
if (!window.zddc) window.zddc = {};
// Don't overwrite if a tool defined its own first.
if (typeof window.zddc.toast === 'function') return;
var DEFAULT_DURATION_MS = 5000;
var FADE_MS = 300;
// Levels that persist until the user dismisses them (troubleshooting).
var STICKY = { error: true, warning: true };
function container() {
var c = document.getElementById('zddc-toasts');
if (c) return c;
c = document.createElement('div');
c.id = 'zddc-toasts';
c.className = 'zddc-toasts';
document.body.appendChild(c);
return c;
}
// Show/hide a "Clear all" control when 2+ toasts are stacked.
function refreshClearAll(c) {
var bar = c.querySelector('.zddc-toasts__clear');
var count = c.querySelectorAll('.zddc-toast').length;
if (count >= 2) {
if (!bar) {
bar = document.createElement('button');
bar.type = 'button';
bar.className = 'zddc-toasts__clear';
bar.textContent = 'Clear all';
bar.addEventListener('click', function () {
var all = c.querySelectorAll('.zddc-toast');
for (var i = 0; i < all.length; i++) dismiss(all[i]);
});
c.insertBefore(bar, c.firstChild);
}
} else if (bar) {
bar.remove();
}
}
function dismiss(el) {
if (el._dismissed) return;
el._dismissed = true;
if (el._timer) clearTimeout(el._timer);
el.classList.add('zddc-toast--fade');
setTimeout(function () {
if (el.parentNode) el.parentNode.removeChild(el);
refreshClearAll(container());
}, FADE_MS);
}
function toast(message, level, opts) {
opts = opts || {};
var lvl = (level === 'success' || level === 'error' ||
level === 'warning') ? level : 'info';
// Single-toast policy: dismiss any existing toast immediately
// so the new one is always the most recent. Matches the
// classifier's prior behavior and avoids stack-of-toasts UX.
var existing = document.querySelector('.zddc-toast');
if (existing) existing.remove();
var c = container();
var el = document.createElement('div');
el.className = 'zddc-toast zddc-toast--' + lvl;
// ARIA: errors get assertive (interrupts SR queue), others polite.
el.setAttribute('role', lvl === 'error' ? 'alert' : 'status');
el.setAttribute('aria-live', lvl === 'error' ? 'assertive' : 'polite');
el.textContent = message == null ? '' : String(message);
document.body.appendChild(el);
var dur = typeof opts.durationMs === 'number' ?
opts.durationMs : DEFAULT_DURATION_MS;
var timer = setTimeout(function () {
el.classList.add('zddc-toast--fade');
setTimeout(function () {
if (el.parentNode) el.parentNode.removeChild(el);
}, FADE_MS);
}, dur);
// Selectable, copyable message text (its own element so clicking to
// select doesn't dismiss the toast — only the × does).
var msg = document.createElement('span');
msg.className = 'zddc-toast__msg';
msg.textContent = message == null ? '' : String(message);
el.appendChild(msg);
// Click-to-dismiss. Useful for sticky errors the user wants gone.
el.addEventListener('click', function () {
clearTimeout(timer);
if (el.parentNode) el.parentNode.removeChild(el);
});
var close = document.createElement('button');
close.type = 'button';
close.className = 'zddc-toast__close';
close.setAttribute('aria-label', 'Dismiss');
close.textContent = '×';
close.addEventListener('click', function () { dismiss(el); });
el.appendChild(close);
c.appendChild(el);
// Sticky (error/warning, or opts.durationMs === 0) persists; otherwise
// auto-dismiss after the (overridable) duration.
var sticky = opts.durationMs === 0 ||
(typeof opts.durationMs !== 'number' && STICKY[lvl]);
if (!sticky) {
var dur = typeof opts.durationMs === 'number'
? opts.durationMs : DEFAULT_DURATION_MS;
el._timer = setTimeout(function () { dismiss(el); }, dur);
}
refreshClearAll(c);
return el;
}
window.zddc.toast = toast;
// Route window.alert() calls into the toast helper. Every tool has
// accumulated some `alert(...)` sites for error reporting; rather
// than touch each one, intercept globally so they're non-blocking
// and ARIA-announced consistently. Native alert is preserved on
// window.alertNative for the rare case where a truly modal block
// is needed (e.g. before navigating away with unsaved changes).
// Route window.alert() into the toast helper (non-blocking, ARIA-announced,
// consistent). Native alert preserved on window.alertNative. alert() maps
// to an error toast (sticky) since that's its usual purpose.
if (typeof window.alert === 'function' && !window.alertNative) {
window.alertNative = window.alert.bind(window);
window.alert = function (msg) {

File diff suppressed because it is too large Load diff

View file

@ -814,23 +814,75 @@ body.help-open .app-header {
with tool-local .toast classes; the old classifier rules can stay
alongside until this file is concatenated above them in the build. */
.zddc-toast {
/* Toast STACK — bottom-right, newest at the bottom. The container is
click-through (pointer-events:none) so the gaps don't block the page; each
toast + button re-enables pointer events. */
.zddc-toasts {
position: fixed;
bottom: 2rem;
right: 2rem;
bottom: 1.5rem;
right: 1.5rem;
z-index: 9000;
display: flex;
flex-direction: column;
align-items: flex-end;
gap: 0.5rem;
max-height: calc(100vh - 3rem);
overflow-y: auto;
pointer-events: none;
}
/* "Clear all" — shown above the stack when 2+ toasts are present. */
.zddc-toasts__clear {
pointer-events: auto;
align-self: flex-end;
background: var(--bg);
color: var(--text);
padding: 0.875rem 1.25rem;
border: 1px solid var(--border);
border-radius: var(--radius);
padding: 0.2rem 0.6rem;
font-size: 0.78rem;
cursor: pointer;
}
.zddc-toasts__clear:hover { background: var(--bg-secondary, rgba(0, 0, 0, 0.05)); }
.zddc-toast {
position: relative;
pointer-events: auto;
background: var(--bg);
color: var(--text);
padding: 0.7rem 1.7rem 0.7rem 1rem; /* room for the × at top-right */
border-radius: var(--radius);
border: 1px solid var(--border);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
z-index: 9000;
max-width: 400px;
max-width: 420px;
font-size: 0.875rem;
cursor: pointer;
animation: zddc-toast-in 0.3s ease-out;
}
/* Message text — selectable + copyable; long/multi-line errors wrap. */
.zddc-toast__msg {
user-select: text;
-webkit-user-select: text;
cursor: text;
white-space: pre-wrap;
word-break: break-word;
}
/* Per-toast dismiss. */
.zddc-toast__close {
position: absolute;
top: 0.2rem;
right: 0.35rem;
border: none;
background: transparent;
color: var(--text-muted, #888);
font-size: 1.15rem;
line-height: 1;
cursor: pointer;
padding: 0 0.15rem;
}
.zddc-toast__close:hover { color: var(--text); }
.zddc-toast--success { border-left: 4px solid var(--success); }
.zddc-toast--error { border-left: 4px solid var(--danger); }
.zddc-toast--info { border-left: 4px solid var(--info); }
@ -1619,7 +1671,7 @@ body {
</svg>
<div class="header-title-group">
<span class="app-header__title">ZDDC</span>
<span class="build-timestamp"><span style="color:red;font-weight:bold">v0.0.27-beta · 2026-06-09 15:30:13 · 237c353</span></span>
<span class="build-timestamp"><span style="color:red;font-weight:bold">v0.0.27-beta · 2026-06-10 18:32:03 · 203674e</span></span>
</div>
</div>
<div class="header-right">
@ -2417,74 +2469,122 @@ body {
}());
// shared/toast.js — non-blocking notification helper available to every
// tool via window.zddc.toast(msg, level, opts). Originated as classifier's
// local showToast (classifier/js/excel.js); promoted here so tools that
// today use alert() or silent console.error can switch to a uniform
// non-blocking surface.
// tool via window.zddc.toast(msg, level, opts).
//
// Toasts collect in a bottom-right STACK. Error/warning toasts are STICKY —
// they stay until the user dismisses them (per-toast × or a "Clear all"
// button) so the message can be read, selected, and copied while
// troubleshooting. info/success toasts auto-dismiss. The message text is
// always selectable.
//
// Usage:
// window.zddc.toast('Saved.', 'success');
// window.zddc.toast('Could not load: ' + err.message, 'error');
// window.zddc.toast('Saved.', 'success'); // auto-dismiss
// window.zddc.toast('Could not load: ' + e.message, 'error'); // sticky
// window.zddc.toast('Note', 'info', { durationMs: 3000 });
// window.zddc.toast('Heads up', 'info', { durationMs: 0 }); // force sticky
//
// Levels: 'info' (default) | 'success' | 'warning' | 'error'.
// Each tool may also expose app.notify(msg, level) as a thin wrapper —
// see ARCHITECTURE.md for the convention.
(function () {
'use strict';
if (!window.zddc) window.zddc = {};
// Don't overwrite if a tool defined its own first.
if (typeof window.zddc.toast === 'function') return;
var DEFAULT_DURATION_MS = 5000;
var FADE_MS = 300;
// Levels that persist until the user dismisses them (troubleshooting).
var STICKY = { error: true, warning: true };
function container() {
var c = document.getElementById('zddc-toasts');
if (c) return c;
c = document.createElement('div');
c.id = 'zddc-toasts';
c.className = 'zddc-toasts';
document.body.appendChild(c);
return c;
}
// Show/hide a "Clear all" control when 2+ toasts are stacked.
function refreshClearAll(c) {
var bar = c.querySelector('.zddc-toasts__clear');
var count = c.querySelectorAll('.zddc-toast').length;
if (count >= 2) {
if (!bar) {
bar = document.createElement('button');
bar.type = 'button';
bar.className = 'zddc-toasts__clear';
bar.textContent = 'Clear all';
bar.addEventListener('click', function () {
var all = c.querySelectorAll('.zddc-toast');
for (var i = 0; i < all.length; i++) dismiss(all[i]);
});
c.insertBefore(bar, c.firstChild);
}
} else if (bar) {
bar.remove();
}
}
function dismiss(el) {
if (el._dismissed) return;
el._dismissed = true;
if (el._timer) clearTimeout(el._timer);
el.classList.add('zddc-toast--fade');
setTimeout(function () {
if (el.parentNode) el.parentNode.removeChild(el);
refreshClearAll(container());
}, FADE_MS);
}
function toast(message, level, opts) {
opts = opts || {};
var lvl = (level === 'success' || level === 'error' ||
level === 'warning') ? level : 'info';
// Single-toast policy: dismiss any existing toast immediately
// so the new one is always the most recent. Matches the
// classifier's prior behavior and avoids stack-of-toasts UX.
var existing = document.querySelector('.zddc-toast');
if (existing) existing.remove();
var c = container();
var el = document.createElement('div');
el.className = 'zddc-toast zddc-toast--' + lvl;
// ARIA: errors get assertive (interrupts SR queue), others polite.
el.setAttribute('role', lvl === 'error' ? 'alert' : 'status');
el.setAttribute('aria-live', lvl === 'error' ? 'assertive' : 'polite');
el.textContent = message == null ? '' : String(message);
document.body.appendChild(el);
var dur = typeof opts.durationMs === 'number' ?
opts.durationMs : DEFAULT_DURATION_MS;
var timer = setTimeout(function () {
el.classList.add('zddc-toast--fade');
setTimeout(function () {
if (el.parentNode) el.parentNode.removeChild(el);
}, FADE_MS);
}, dur);
// Selectable, copyable message text (its own element so clicking to
// select doesn't dismiss the toast — only the × does).
var msg = document.createElement('span');
msg.className = 'zddc-toast__msg';
msg.textContent = message == null ? '' : String(message);
el.appendChild(msg);
// Click-to-dismiss. Useful for sticky errors the user wants gone.
el.addEventListener('click', function () {
clearTimeout(timer);
if (el.parentNode) el.parentNode.removeChild(el);
});
var close = document.createElement('button');
close.type = 'button';
close.className = 'zddc-toast__close';
close.setAttribute('aria-label', 'Dismiss');
close.textContent = '×';
close.addEventListener('click', function () { dismiss(el); });
el.appendChild(close);
c.appendChild(el);
// Sticky (error/warning, or opts.durationMs === 0) persists; otherwise
// auto-dismiss after the (overridable) duration.
var sticky = opts.durationMs === 0 ||
(typeof opts.durationMs !== 'number' && STICKY[lvl]);
if (!sticky) {
var dur = typeof opts.durationMs === 'number'
? opts.durationMs : DEFAULT_DURATION_MS;
el._timer = setTimeout(function () { dismiss(el); }, dur);
}
refreshClearAll(c);
return el;
}
window.zddc.toast = toast;
// Route window.alert() calls into the toast helper. Every tool has
// accumulated some `alert(...)` sites for error reporting; rather
// than touch each one, intercept globally so they're non-blocking
// and ARIA-announced consistently. Native alert is preserved on
// window.alertNative for the rare case where a truly modal block
// is needed (e.g. before navigating away with unsaved changes).
// Route window.alert() into the toast helper (non-blocking, ARIA-announced,
// consistent). Native alert preserved on window.alertNative. alert() maps
// to an error toast (sticky) since that's its usual purpose.
if (typeof window.alert === 'function' && !window.alertNative) {
window.alertNative = window.alert.bind(window);
window.alert = function (msg) {

View file

@ -818,23 +818,75 @@ body.help-open .app-header {
with tool-local .toast classes; the old classifier rules can stay
alongside until this file is concatenated above them in the build. */
.zddc-toast {
/* Toast STACK — bottom-right, newest at the bottom. The container is
click-through (pointer-events:none) so the gaps don't block the page; each
toast + button re-enables pointer events. */
.zddc-toasts {
position: fixed;
bottom: 2rem;
right: 2rem;
bottom: 1.5rem;
right: 1.5rem;
z-index: 9000;
display: flex;
flex-direction: column;
align-items: flex-end;
gap: 0.5rem;
max-height: calc(100vh - 3rem);
overflow-y: auto;
pointer-events: none;
}
/* "Clear all" — shown above the stack when 2+ toasts are present. */
.zddc-toasts__clear {
pointer-events: auto;
align-self: flex-end;
background: var(--bg);
color: var(--text);
padding: 0.875rem 1.25rem;
border: 1px solid var(--border);
border-radius: var(--radius);
padding: 0.2rem 0.6rem;
font-size: 0.78rem;
cursor: pointer;
}
.zddc-toasts__clear:hover { background: var(--bg-secondary, rgba(0, 0, 0, 0.05)); }
.zddc-toast {
position: relative;
pointer-events: auto;
background: var(--bg);
color: var(--text);
padding: 0.7rem 1.7rem 0.7rem 1rem; /* room for the × at top-right */
border-radius: var(--radius);
border: 1px solid var(--border);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
z-index: 9000;
max-width: 400px;
max-width: 420px;
font-size: 0.875rem;
cursor: pointer;
animation: zddc-toast-in 0.3s ease-out;
}
/* Message text — selectable + copyable; long/multi-line errors wrap. */
.zddc-toast__msg {
user-select: text;
-webkit-user-select: text;
cursor: text;
white-space: pre-wrap;
word-break: break-word;
}
/* Per-toast dismiss. */
.zddc-toast__close {
position: absolute;
top: 0.2rem;
right: 0.35rem;
border: none;
background: transparent;
color: var(--text-muted, #888);
font-size: 1.15rem;
line-height: 1;
cursor: pointer;
padding: 0 0.15rem;
}
.zddc-toast__close:hover { color: var(--text); }
.zddc-toast--success { border-left: 4px solid var(--success); }
.zddc-toast--error { border-left: 4px solid var(--danger); }
.zddc-toast--info { border-left: 4px solid var(--info); }
@ -2718,7 +2770,7 @@ dialog.modal--narrow {
</svg>
<div class="header-title-group">
<span class="app-header__title">ZDDC Transmittal</span>
<span class="build-timestamp"><span style="color:red;font-weight:bold">v0.0.27-beta · 2026-06-09 15:30:13 · 237c353</span></span>
<span class="build-timestamp"><span style="color:red;font-weight:bold">v0.0.27-beta · 2026-06-10 18:32:03 · 203674e</span></span>
</div>
<span id="no-js-notice" class="text-gray-400 text-xs italic">JavaScript not available</span>
<!-- Publish split-button (Transmittal-specific primary action;
@ -5364,74 +5416,122 @@ X.B(E,Y);return E}return J}())
}());
// shared/toast.js — non-blocking notification helper available to every
// tool via window.zddc.toast(msg, level, opts). Originated as classifier's
// local showToast (classifier/js/excel.js); promoted here so tools that
// today use alert() or silent console.error can switch to a uniform
// non-blocking surface.
// tool via window.zddc.toast(msg, level, opts).
//
// Toasts collect in a bottom-right STACK. Error/warning toasts are STICKY —
// they stay until the user dismisses them (per-toast × or a "Clear all"
// button) so the message can be read, selected, and copied while
// troubleshooting. info/success toasts auto-dismiss. The message text is
// always selectable.
//
// Usage:
// window.zddc.toast('Saved.', 'success');
// window.zddc.toast('Could not load: ' + err.message, 'error');
// window.zddc.toast('Saved.', 'success'); // auto-dismiss
// window.zddc.toast('Could not load: ' + e.message, 'error'); // sticky
// window.zddc.toast('Note', 'info', { durationMs: 3000 });
// window.zddc.toast('Heads up', 'info', { durationMs: 0 }); // force sticky
//
// Levels: 'info' (default) | 'success' | 'warning' | 'error'.
// Each tool may also expose app.notify(msg, level) as a thin wrapper —
// see ARCHITECTURE.md for the convention.
(function () {
'use strict';
if (!window.zddc) window.zddc = {};
// Don't overwrite if a tool defined its own first.
if (typeof window.zddc.toast === 'function') return;
var DEFAULT_DURATION_MS = 5000;
var FADE_MS = 300;
// Levels that persist until the user dismisses them (troubleshooting).
var STICKY = { error: true, warning: true };
function container() {
var c = document.getElementById('zddc-toasts');
if (c) return c;
c = document.createElement('div');
c.id = 'zddc-toasts';
c.className = 'zddc-toasts';
document.body.appendChild(c);
return c;
}
// Show/hide a "Clear all" control when 2+ toasts are stacked.
function refreshClearAll(c) {
var bar = c.querySelector('.zddc-toasts__clear');
var count = c.querySelectorAll('.zddc-toast').length;
if (count >= 2) {
if (!bar) {
bar = document.createElement('button');
bar.type = 'button';
bar.className = 'zddc-toasts__clear';
bar.textContent = 'Clear all';
bar.addEventListener('click', function () {
var all = c.querySelectorAll('.zddc-toast');
for (var i = 0; i < all.length; i++) dismiss(all[i]);
});
c.insertBefore(bar, c.firstChild);
}
} else if (bar) {
bar.remove();
}
}
function dismiss(el) {
if (el._dismissed) return;
el._dismissed = true;
if (el._timer) clearTimeout(el._timer);
el.classList.add('zddc-toast--fade');
setTimeout(function () {
if (el.parentNode) el.parentNode.removeChild(el);
refreshClearAll(container());
}, FADE_MS);
}
function toast(message, level, opts) {
opts = opts || {};
var lvl = (level === 'success' || level === 'error' ||
level === 'warning') ? level : 'info';
// Single-toast policy: dismiss any existing toast immediately
// so the new one is always the most recent. Matches the
// classifier's prior behavior and avoids stack-of-toasts UX.
var existing = document.querySelector('.zddc-toast');
if (existing) existing.remove();
var c = container();
var el = document.createElement('div');
el.className = 'zddc-toast zddc-toast--' + lvl;
// ARIA: errors get assertive (interrupts SR queue), others polite.
el.setAttribute('role', lvl === 'error' ? 'alert' : 'status');
el.setAttribute('aria-live', lvl === 'error' ? 'assertive' : 'polite');
el.textContent = message == null ? '' : String(message);
document.body.appendChild(el);
var dur = typeof opts.durationMs === 'number' ?
opts.durationMs : DEFAULT_DURATION_MS;
var timer = setTimeout(function () {
el.classList.add('zddc-toast--fade');
setTimeout(function () {
if (el.parentNode) el.parentNode.removeChild(el);
}, FADE_MS);
}, dur);
// Selectable, copyable message text (its own element so clicking to
// select doesn't dismiss the toast — only the × does).
var msg = document.createElement('span');
msg.className = 'zddc-toast__msg';
msg.textContent = message == null ? '' : String(message);
el.appendChild(msg);
// Click-to-dismiss. Useful for sticky errors the user wants gone.
el.addEventListener('click', function () {
clearTimeout(timer);
if (el.parentNode) el.parentNode.removeChild(el);
});
var close = document.createElement('button');
close.type = 'button';
close.className = 'zddc-toast__close';
close.setAttribute('aria-label', 'Dismiss');
close.textContent = '×';
close.addEventListener('click', function () { dismiss(el); });
el.appendChild(close);
c.appendChild(el);
// Sticky (error/warning, or opts.durationMs === 0) persists; otherwise
// auto-dismiss after the (overridable) duration.
var sticky = opts.durationMs === 0 ||
(typeof opts.durationMs !== 'number' && STICKY[lvl]);
if (!sticky) {
var dur = typeof opts.durationMs === 'number'
? opts.durationMs : DEFAULT_DURATION_MS;
el._timer = setTimeout(function () { dismiss(el); }, dur);
}
refreshClearAll(c);
return el;
}
window.zddc.toast = toast;
// Route window.alert() calls into the toast helper. Every tool has
// accumulated some `alert(...)` sites for error reporting; rather
// than touch each one, intercept globally so they're non-blocking
// and ARIA-announced consistently. Native alert is preserved on
// window.alertNative for the rare case where a truly modal block
// is needed (e.g. before navigating away with unsaved changes).
// Route window.alert() into the toast helper (non-blocking, ARIA-announced,
// consistent). Native alert preserved on window.alertNative. alert() maps
// to an error toast (sticky) since that's its usual purpose.
if (typeof window.alert === 'function' && !window.alertNative) {
window.alertNative = window.alert.bind(window);
window.alert = function (msg) {

View file

@ -1,8 +1,8 @@
# Generated by build.sh — do not edit. One <app>=<build label> per line.
archive=v0.0.27-beta · 2026-06-09 15:30:13 · 237c353
transmittal=v0.0.27-beta · 2026-06-09 15:30:13 · 237c353
classifier=v0.0.27-beta · 2026-06-09 15:30:13 · 237c353
landing=v0.0.27-beta · 2026-06-09 15:30:13 · 237c353
form=v0.0.27-beta · 2026-06-09 15:30:13 · 237c353
tables=v0.0.27-beta · 2026-06-09 15:30:13 · 237c353
browse=v0.0.27-beta · 2026-06-09 15:30:14 · 237c353
archive=v0.0.27-beta · 2026-06-10 18:32:03 · 203674e
transmittal=v0.0.27-beta · 2026-06-10 18:32:03 · 203674e
classifier=v0.0.27-beta · 2026-06-10 18:32:03 · 203674e
landing=v0.0.27-beta · 2026-06-10 18:32:03 · 203674e
form=v0.0.27-beta · 2026-06-10 18:32:03 · 203674e
tables=v0.0.27-beta · 2026-06-10 18:32:03 · 203674e
browse=v0.0.27-beta · 2026-06-10 18:32:03 · 203674e

View file

@ -1722,7 +1722,7 @@ body.is-elevated::after {
</svg>
<div class="header-title-group">
<span class="app-header__title" id="table-title">ZDDC Table</span>
<span class="build-timestamp"><span style="color:red;font-weight:bold">v0.0.27-dev · 2026-06-10 14:42:21 · 8f839fc</span></span>
<span class="build-timestamp"><span style="color:red;font-weight:bold">v0.0.27-beta · 2026-06-10 18:32:03 · 203674e</span></span>
</div>
</div>
<div class="header-right">