chore(embedded): cut v0.0.17-beta
Beta cut of the eight HTML tools into zddc/internal/apps/embedded/*
and the unified form/tables bundle into zddc/internal/handler/tables.html.
Each tool's on-page label changes from alpha → beta-stamped bytes;
no source changes beyond the build label itself.
The dev image (Dockerfile, devshell, ZDDC_REF=main) and the bitnest
test container both pick this up automatically — bitnest's path-unit
fired on the rebuild of zddc/dist/zddc-server-linux-amd64 and
restarted the container with the new embedded apps:
embedded_apps=archive=v0.0.17-beta browse=v0.0.17-beta
classifier=v0.0.17-beta form=v0.0.17-beta
landing=v0.0.17-beta mdedit=v0.0.17-beta
tables=v0.0.17-beta transmittal=v0.0.17-beta
Source-side commits since the previous beta:
feat(landing): single-project click → <project>/archive.html
feat(shared): non-blocking toast helper
feat(shared): lateral project-stage strip
feat(form): standalone empty-state welcome
fix(tables): keepalive on beforeunload save path
refactor(mdedit): drop window.* TOC globals
refactor(archive): remove dead debounce
style(transmittal): tokenize utility classes, drop !important block
style: replace inline styles with CSS
test(shared): zddc-source.js + toast + nav specs
test(browse): smoke spec
docs: tool counts + state pattern + polyfill gaps
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
b10468d4e3
commit
585e84f2f4
8 changed files with 1966 additions and 166 deletions
|
|
@ -335,6 +335,11 @@ a:hover {
|
|||
font-size: 1rem;
|
||||
}
|
||||
|
||||
/* The refresh ⟳ glyph renders slightly smaller than ◐ / ? — bump to match. */
|
||||
#refreshHeaderBtn {
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
/* Toast CSS lives in classifier/css/base.css — only that tool uses toasts. */
|
||||
|
||||
/* ── Theme and help icon buttons ─────────────────────────────────────────── */
|
||||
|
|
@ -525,6 +530,104 @@ body.help-open .app-header {
|
|||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
/* shared/toast.css — single-toast notification styles paired with
|
||||
shared/toast.js. Uses BEM-ish .zddc-toast prefix to avoid collisions
|
||||
with tool-local .toast classes; the old classifier rules can stay
|
||||
alongside until this file is concatenated above them in the build. */
|
||||
|
||||
.zddc-toast {
|
||||
position: fixed;
|
||||
bottom: 2rem;
|
||||
right: 2rem;
|
||||
background: var(--bg);
|
||||
color: var(--text);
|
||||
padding: 0.875rem 1.25rem;
|
||||
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;
|
||||
font-size: 0.875rem;
|
||||
cursor: pointer;
|
||||
animation: zddc-toast-in 0.3s ease-out;
|
||||
}
|
||||
|
||||
.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); }
|
||||
.zddc-toast--warning { border-left: 4px solid var(--warning); }
|
||||
|
||||
.zddc-toast--fade {
|
||||
animation: zddc-toast-out 0.3s ease-out forwards;
|
||||
}
|
||||
|
||||
@keyframes zddc-toast-in {
|
||||
from { transform: translateX(100%); opacity: 0; }
|
||||
to { transform: translateX(0); opacity: 1; }
|
||||
}
|
||||
|
||||
@keyframes zddc-toast-out {
|
||||
from { transform: translateX(0); opacity: 1; }
|
||||
to { transform: translateX(100%); opacity: 0; }
|
||||
}
|
||||
|
||||
/* shared/nav.css — lateral project-stage strip paired with shared/nav.js.
|
||||
Sits as a sibling immediately under .app-header (mounted by JS).
|
||||
Rendered only in online mode when a project segment is in the URL. */
|
||||
|
||||
.zddc-stage-strip {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
padding: 0.3rem 1rem;
|
||||
background: var(--bg);
|
||||
border-bottom: 1px solid var(--border);
|
||||
font-size: 0.8rem;
|
||||
line-height: 1.3;
|
||||
flex-shrink: 0;
|
||||
overflow-x: auto;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.zddc-stage-strip__project {
|
||||
color: var(--text);
|
||||
font-weight: 600;
|
||||
margin-right: 0.15rem;
|
||||
}
|
||||
|
||||
.zddc-stage-strip__divider,
|
||||
.zddc-stage-strip__sep {
|
||||
color: var(--text-muted);
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.zddc-stage-strip__divider {
|
||||
margin-right: 0.35rem;
|
||||
}
|
||||
|
||||
.zddc-stage {
|
||||
color: var(--text-muted);
|
||||
text-decoration: none;
|
||||
padding: 0.1rem 0.25rem;
|
||||
border-radius: var(--radius);
|
||||
transition: color 0.15s, background 0.15s;
|
||||
}
|
||||
|
||||
.zddc-stage:hover {
|
||||
color: var(--text);
|
||||
background: var(--bg-secondary);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.zddc-stage--active {
|
||||
color: var(--primary);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.zddc-stage--active:hover {
|
||||
color: var(--primary);
|
||||
}
|
||||
|
||||
/* Archive-specific base overrides
|
||||
Reset, tokens, and font are provided by shared/base.css */
|
||||
|
||||
|
|
@ -1771,6 +1874,17 @@ input[type="checkbox"] {
|
|||
cursor: default;
|
||||
}
|
||||
|
||||
/* Variant: content packs at the start instead of distributing across the
|
||||
column header. Used by the Revisions column where a leading "select all"
|
||||
checkbox sits beside the column title. */
|
||||
.th-content--start {
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
.th-content--start .select-all-checkbox {
|
||||
margin-right: 0.5rem;
|
||||
}
|
||||
|
||||
.sortable .th-content {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
|
@ -2131,10 +2245,10 @@ 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.17-beta · 2026-05-08 · falcon-alder-ginger</span></span>
|
||||
<span class="build-timestamp"><span style="color:red;font-weight:bold">v0.0.17-beta · 2026-05-10 · alder-cherry-reef</span></span>
|
||||
</div>
|
||||
<button id="addDirectoryBtn" class="btn btn-primary">Add Local Directory</button>
|
||||
<button id="refreshHeaderBtn" class="btn btn-secondary hidden" title="Refresh Data" style="font-size:1.1rem;">⟳</button>
|
||||
<button id="refreshHeaderBtn" class="btn btn-secondary hidden" title="Refresh Data">⟳</button>
|
||||
</div>
|
||||
<div class="header-right">
|
||||
<button id="theme-btn" class="btn btn-secondary" title="Theme: auto (follows OS)" aria-label="Theme: auto (follows OS)">◐</button>
|
||||
|
|
@ -2270,11 +2384,11 @@ td[data-field="trackingNumber"] {
|
|||
<div class="resize-handle"></div>
|
||||
</th>
|
||||
<th class="resizable" data-field="revisions">
|
||||
<div class="th-content" style="justify-content: flex-start;">
|
||||
<input type="checkbox"
|
||||
id="selectAllVisibleCheckbox"
|
||||
title="Select/deselect all visible files"
|
||||
style="margin-right: 0.5rem;">
|
||||
<div class="th-content th-content--start">
|
||||
<input type="checkbox"
|
||||
id="selectAllVisibleCheckbox"
|
||||
class="select-all-checkbox"
|
||||
title="Select/deselect all visible files">
|
||||
<span>Revisions</span>
|
||||
</div>
|
||||
<input type="text"
|
||||
|
|
@ -3046,6 +3160,212 @@ https://github.com/nodeca/pako/blob/main/LICENSE
|
|||
}
|
||||
}());
|
||||
|
||||
// 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.
|
||||
//
|
||||
// Usage:
|
||||
// window.zddc.toast('Saved.', 'success');
|
||||
// window.zddc.toast('Could not load: ' + err.message, 'error');
|
||||
// window.zddc.toast('Note', 'info', { durationMs: 3000 });
|
||||
//
|
||||
// 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;
|
||||
|
||||
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 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);
|
||||
|
||||
// Click-to-dismiss. Useful for sticky errors the user wants gone.
|
||||
el.addEventListener('click', function () {
|
||||
clearTimeout(timer);
|
||||
if (el.parentNode) el.parentNode.removeChild(el);
|
||||
});
|
||||
|
||||
return el;
|
||||
}
|
||||
|
||||
window.zddc.toast = toast;
|
||||
})();
|
||||
|
||||
// shared/nav.js — lateral navigation strip across the four canonical
|
||||
// project stages (archive · working · staging · reviewing). Renders
|
||||
// only when:
|
||||
// 1. location.protocol is http: or https: (online — file:// has no
|
||||
// project structure to navigate within), AND
|
||||
// 2. a project segment can be detected from location.pathname (the
|
||||
// first path segment, when it isn't a tool HTML file).
|
||||
//
|
||||
// The strip is inserted as a sibling of <header class="app-header">
|
||||
// on DOMContentLoaded — no template changes required. Each tool just
|
||||
// needs ../shared/nav.{js,css} in its build.sh.
|
||||
//
|
||||
// Stage URLs follow the canonical workflow folders documented at
|
||||
// zddc.varasys.io/reference.html#transmittal-workflow:
|
||||
// archive → <project>/archive.html (archive tool, project-root mode)
|
||||
// working → <project>/working/ (directory listing → mdedit auto-serves)
|
||||
// staging → <project>/staging/ (directory listing → transmittal auto-serves)
|
||||
// reviewing → <project>/reviewing/ (directory listing)
|
||||
//
|
||||
// If a deployment doesn't have one of these folders the link will 404 —
|
||||
// the strip is convention-driven, not probed. Operators on non-standard
|
||||
// layouts can override by setting window.zddc.nav.disabled = true before
|
||||
// DOMContentLoaded.
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
if (!window.zddc) window.zddc = {};
|
||||
if (window.zddc.nav) return; // already loaded
|
||||
|
||||
var STAGES = [
|
||||
{ key: 'archive', label: 'Archive', target: 'archive.html' },
|
||||
{ key: 'working', label: 'Working', target: 'working/' },
|
||||
{ key: 'staging', label: 'Staging', target: 'staging/' },
|
||||
{ key: 'reviewing', label: 'Reviewing', target: 'reviewing/' },
|
||||
];
|
||||
|
||||
function projectSegment(pathname) {
|
||||
var parts = pathname.split('/').filter(Boolean);
|
||||
if (parts.length === 0) return null;
|
||||
var first = parts[0];
|
||||
// At deployment root (e.g. /archive.html?projects=A,B or
|
||||
// /index.html) the first segment is a tool HTML — no single
|
||||
// project to scope the strip to.
|
||||
if (first.indexOf('.') !== -1) return null;
|
||||
return first;
|
||||
}
|
||||
|
||||
function currentStage(pathname) {
|
||||
var parts = pathname.split('/').filter(Boolean);
|
||||
if (parts.length < 2) return null;
|
||||
var second = parts[1];
|
||||
// <project>/working/... | staging/... | reviewing/... | archive/...
|
||||
for (var i = 0; i < STAGES.length; i++) {
|
||||
if (second === STAGES[i].key) return STAGES[i].key;
|
||||
}
|
||||
// <project>/archive.html → still the archive stage
|
||||
if (second === 'archive.html') return 'archive';
|
||||
return null;
|
||||
}
|
||||
|
||||
function shouldRender() {
|
||||
if (typeof location === 'undefined') return false;
|
||||
if (location.protocol !== 'http:' && location.protocol !== 'https:') return false;
|
||||
if (window.zddc.nav && window.zddc.nav.disabled) return false;
|
||||
return projectSegment(location.pathname) !== null;
|
||||
}
|
||||
|
||||
function buildStrip(project, active) {
|
||||
var nav = document.createElement('nav');
|
||||
nav.className = 'zddc-stage-strip';
|
||||
nav.setAttribute('aria-label', 'Project stage');
|
||||
|
||||
var label = document.createElement('span');
|
||||
label.className = 'zddc-stage-strip__project';
|
||||
label.textContent = project;
|
||||
nav.appendChild(label);
|
||||
|
||||
var sep0 = document.createElement('span');
|
||||
sep0.className = 'zddc-stage-strip__divider';
|
||||
sep0.setAttribute('aria-hidden', 'true');
|
||||
sep0.textContent = '/';
|
||||
nav.appendChild(sep0);
|
||||
|
||||
for (var i = 0; i < STAGES.length; i++) {
|
||||
var s = STAGES[i];
|
||||
var a = document.createElement('a');
|
||||
a.className = 'zddc-stage';
|
||||
a.href = '/' + encodeURIComponent(project) + '/' + s.target;
|
||||
a.textContent = s.label;
|
||||
if (s.key === active) {
|
||||
a.classList.add('zddc-stage--active');
|
||||
a.setAttribute('aria-current', 'page');
|
||||
}
|
||||
nav.appendChild(a);
|
||||
|
||||
if (i < STAGES.length - 1) {
|
||||
var sep = document.createElement('span');
|
||||
sep.className = 'zddc-stage-strip__sep';
|
||||
sep.setAttribute('aria-hidden', 'true');
|
||||
sep.textContent = '·';
|
||||
nav.appendChild(sep);
|
||||
}
|
||||
}
|
||||
|
||||
return nav;
|
||||
}
|
||||
|
||||
function mount() {
|
||||
if (!shouldRender()) return;
|
||||
var header = document.querySelector('.app-header');
|
||||
if (!header) return;
|
||||
// Don't double-mount if a tool's main.js calls us a second time.
|
||||
if (header.nextElementSibling &&
|
||||
header.nextElementSibling.classList &&
|
||||
header.nextElementSibling.classList.contains('zddc-stage-strip')) {
|
||||
return;
|
||||
}
|
||||
var project = projectSegment(location.pathname);
|
||||
var active = currentStage(location.pathname);
|
||||
var strip = buildStrip(project, active);
|
||||
header.parentNode.insertBefore(strip, header.nextSibling);
|
||||
}
|
||||
|
||||
// Expose for tests + opt-out.
|
||||
window.zddc.nav = {
|
||||
mount: mount,
|
||||
// Internals visible for unit tests; do not call from tools.
|
||||
_projectSegment: projectSegment,
|
||||
_currentStage: currentStage,
|
||||
_stages: STAGES,
|
||||
// Set to true before DOMContentLoaded to suppress mounting on
|
||||
// deployments where the canonical folder layout doesn't apply.
|
||||
disabled: false,
|
||||
};
|
||||
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', mount, { once: true });
|
||||
} else {
|
||||
mount();
|
||||
}
|
||||
})();
|
||||
|
||||
/**
|
||||
* ZDDC — shared preview helpers
|
||||
*
|
||||
|
|
@ -7182,19 +7502,6 @@ window.app.modules.filtering = {
|
|||
}
|
||||
}
|
||||
|
||||
// Utility: Debounce function
|
||||
function debounce(func, wait) {
|
||||
let timeout;
|
||||
return function executedFunction(...args) {
|
||||
const later = () => {
|
||||
clearTimeout(timeout);
|
||||
func(...args);
|
||||
};
|
||||
clearTimeout(timeout);
|
||||
timeout = setTimeout(later, wait);
|
||||
};
|
||||
}
|
||||
|
||||
// Multi-select handling for folder lists
|
||||
function setupFolderMultiSelect() {
|
||||
let lastSelectedGroupingIndex = -1;
|
||||
|
|
|
|||
|
|
@ -335,6 +335,11 @@ a:hover {
|
|||
font-size: 1rem;
|
||||
}
|
||||
|
||||
/* The refresh ⟳ glyph renders slightly smaller than ◐ / ? — bump to match. */
|
||||
#refreshHeaderBtn {
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
/* Toast CSS lives in classifier/css/base.css — only that tool uses toasts. */
|
||||
|
||||
/* ── Theme and help icon buttons ─────────────────────────────────────────── */
|
||||
|
|
@ -525,6 +530,104 @@ body.help-open .app-header {
|
|||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
/* shared/toast.css — single-toast notification styles paired with
|
||||
shared/toast.js. Uses BEM-ish .zddc-toast prefix to avoid collisions
|
||||
with tool-local .toast classes; the old classifier rules can stay
|
||||
alongside until this file is concatenated above them in the build. */
|
||||
|
||||
.zddc-toast {
|
||||
position: fixed;
|
||||
bottom: 2rem;
|
||||
right: 2rem;
|
||||
background: var(--bg);
|
||||
color: var(--text);
|
||||
padding: 0.875rem 1.25rem;
|
||||
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;
|
||||
font-size: 0.875rem;
|
||||
cursor: pointer;
|
||||
animation: zddc-toast-in 0.3s ease-out;
|
||||
}
|
||||
|
||||
.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); }
|
||||
.zddc-toast--warning { border-left: 4px solid var(--warning); }
|
||||
|
||||
.zddc-toast--fade {
|
||||
animation: zddc-toast-out 0.3s ease-out forwards;
|
||||
}
|
||||
|
||||
@keyframes zddc-toast-in {
|
||||
from { transform: translateX(100%); opacity: 0; }
|
||||
to { transform: translateX(0); opacity: 1; }
|
||||
}
|
||||
|
||||
@keyframes zddc-toast-out {
|
||||
from { transform: translateX(0); opacity: 1; }
|
||||
to { transform: translateX(100%); opacity: 0; }
|
||||
}
|
||||
|
||||
/* shared/nav.css — lateral project-stage strip paired with shared/nav.js.
|
||||
Sits as a sibling immediately under .app-header (mounted by JS).
|
||||
Rendered only in online mode when a project segment is in the URL. */
|
||||
|
||||
.zddc-stage-strip {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
padding: 0.3rem 1rem;
|
||||
background: var(--bg);
|
||||
border-bottom: 1px solid var(--border);
|
||||
font-size: 0.8rem;
|
||||
line-height: 1.3;
|
||||
flex-shrink: 0;
|
||||
overflow-x: auto;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.zddc-stage-strip__project {
|
||||
color: var(--text);
|
||||
font-weight: 600;
|
||||
margin-right: 0.15rem;
|
||||
}
|
||||
|
||||
.zddc-stage-strip__divider,
|
||||
.zddc-stage-strip__sep {
|
||||
color: var(--text-muted);
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.zddc-stage-strip__divider {
|
||||
margin-right: 0.35rem;
|
||||
}
|
||||
|
||||
.zddc-stage {
|
||||
color: var(--text-muted);
|
||||
text-decoration: none;
|
||||
padding: 0.1rem 0.25rem;
|
||||
border-radius: var(--radius);
|
||||
transition: color 0.15s, background 0.15s;
|
||||
}
|
||||
|
||||
.zddc-stage:hover {
|
||||
color: var(--text);
|
||||
background: var(--bg-secondary);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.zddc-stage--active {
|
||||
color: var(--primary);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.zddc-stage--active:hover {
|
||||
color: var(--primary);
|
||||
}
|
||||
|
||||
/* browse-specific layout on top of shared/base.css */
|
||||
|
||||
html, body {
|
||||
|
|
@ -896,10 +999,10 @@ body {
|
|||
</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.17-beta · 2026-05-08 · falcon-alder-ginger</span></span>
|
||||
<span class="build-timestamp"><span style="color:red;font-weight:bold">v0.0.17-beta · 2026-05-10 · alder-cherry-reef</span></span>
|
||||
</div>
|
||||
<button id="addDirectoryBtn" class="btn btn-primary">Add Local Directory</button>
|
||||
<button id="refreshHeaderBtn" class="btn btn-secondary hidden" title="Refresh listing" aria-label="Refresh listing" style="font-size:1.1rem;">⟳</button>
|
||||
<button id="refreshHeaderBtn" class="btn btn-secondary hidden" title="Refresh listing" aria-label="Refresh listing">⟳</button>
|
||||
</div>
|
||||
<div class="header-right">
|
||||
<button id="theme-btn" class="btn btn-secondary" title="Theme: auto (follows OS)" aria-label="Theme: auto (follows OS)">◐</button>
|
||||
|
|
@ -1703,6 +1806,212 @@ https://github.com/nodeca/pako/blob/main/LICENSE
|
|||
}
|
||||
}());
|
||||
|
||||
// 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.
|
||||
//
|
||||
// Usage:
|
||||
// window.zddc.toast('Saved.', 'success');
|
||||
// window.zddc.toast('Could not load: ' + err.message, 'error');
|
||||
// window.zddc.toast('Note', 'info', { durationMs: 3000 });
|
||||
//
|
||||
// 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;
|
||||
|
||||
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 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);
|
||||
|
||||
// Click-to-dismiss. Useful for sticky errors the user wants gone.
|
||||
el.addEventListener('click', function () {
|
||||
clearTimeout(timer);
|
||||
if (el.parentNode) el.parentNode.removeChild(el);
|
||||
});
|
||||
|
||||
return el;
|
||||
}
|
||||
|
||||
window.zddc.toast = toast;
|
||||
})();
|
||||
|
||||
// shared/nav.js — lateral navigation strip across the four canonical
|
||||
// project stages (archive · working · staging · reviewing). Renders
|
||||
// only when:
|
||||
// 1. location.protocol is http: or https: (online — file:// has no
|
||||
// project structure to navigate within), AND
|
||||
// 2. a project segment can be detected from location.pathname (the
|
||||
// first path segment, when it isn't a tool HTML file).
|
||||
//
|
||||
// The strip is inserted as a sibling of <header class="app-header">
|
||||
// on DOMContentLoaded — no template changes required. Each tool just
|
||||
// needs ../shared/nav.{js,css} in its build.sh.
|
||||
//
|
||||
// Stage URLs follow the canonical workflow folders documented at
|
||||
// zddc.varasys.io/reference.html#transmittal-workflow:
|
||||
// archive → <project>/archive.html (archive tool, project-root mode)
|
||||
// working → <project>/working/ (directory listing → mdedit auto-serves)
|
||||
// staging → <project>/staging/ (directory listing → transmittal auto-serves)
|
||||
// reviewing → <project>/reviewing/ (directory listing)
|
||||
//
|
||||
// If a deployment doesn't have one of these folders the link will 404 —
|
||||
// the strip is convention-driven, not probed. Operators on non-standard
|
||||
// layouts can override by setting window.zddc.nav.disabled = true before
|
||||
// DOMContentLoaded.
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
if (!window.zddc) window.zddc = {};
|
||||
if (window.zddc.nav) return; // already loaded
|
||||
|
||||
var STAGES = [
|
||||
{ key: 'archive', label: 'Archive', target: 'archive.html' },
|
||||
{ key: 'working', label: 'Working', target: 'working/' },
|
||||
{ key: 'staging', label: 'Staging', target: 'staging/' },
|
||||
{ key: 'reviewing', label: 'Reviewing', target: 'reviewing/' },
|
||||
];
|
||||
|
||||
function projectSegment(pathname) {
|
||||
var parts = pathname.split('/').filter(Boolean);
|
||||
if (parts.length === 0) return null;
|
||||
var first = parts[0];
|
||||
// At deployment root (e.g. /archive.html?projects=A,B or
|
||||
// /index.html) the first segment is a tool HTML — no single
|
||||
// project to scope the strip to.
|
||||
if (first.indexOf('.') !== -1) return null;
|
||||
return first;
|
||||
}
|
||||
|
||||
function currentStage(pathname) {
|
||||
var parts = pathname.split('/').filter(Boolean);
|
||||
if (parts.length < 2) return null;
|
||||
var second = parts[1];
|
||||
// <project>/working/... | staging/... | reviewing/... | archive/...
|
||||
for (var i = 0; i < STAGES.length; i++) {
|
||||
if (second === STAGES[i].key) return STAGES[i].key;
|
||||
}
|
||||
// <project>/archive.html → still the archive stage
|
||||
if (second === 'archive.html') return 'archive';
|
||||
return null;
|
||||
}
|
||||
|
||||
function shouldRender() {
|
||||
if (typeof location === 'undefined') return false;
|
||||
if (location.protocol !== 'http:' && location.protocol !== 'https:') return false;
|
||||
if (window.zddc.nav && window.zddc.nav.disabled) return false;
|
||||
return projectSegment(location.pathname) !== null;
|
||||
}
|
||||
|
||||
function buildStrip(project, active) {
|
||||
var nav = document.createElement('nav');
|
||||
nav.className = 'zddc-stage-strip';
|
||||
nav.setAttribute('aria-label', 'Project stage');
|
||||
|
||||
var label = document.createElement('span');
|
||||
label.className = 'zddc-stage-strip__project';
|
||||
label.textContent = project;
|
||||
nav.appendChild(label);
|
||||
|
||||
var sep0 = document.createElement('span');
|
||||
sep0.className = 'zddc-stage-strip__divider';
|
||||
sep0.setAttribute('aria-hidden', 'true');
|
||||
sep0.textContent = '/';
|
||||
nav.appendChild(sep0);
|
||||
|
||||
for (var i = 0; i < STAGES.length; i++) {
|
||||
var s = STAGES[i];
|
||||
var a = document.createElement('a');
|
||||
a.className = 'zddc-stage';
|
||||
a.href = '/' + encodeURIComponent(project) + '/' + s.target;
|
||||
a.textContent = s.label;
|
||||
if (s.key === active) {
|
||||
a.classList.add('zddc-stage--active');
|
||||
a.setAttribute('aria-current', 'page');
|
||||
}
|
||||
nav.appendChild(a);
|
||||
|
||||
if (i < STAGES.length - 1) {
|
||||
var sep = document.createElement('span');
|
||||
sep.className = 'zddc-stage-strip__sep';
|
||||
sep.setAttribute('aria-hidden', 'true');
|
||||
sep.textContent = '·';
|
||||
nav.appendChild(sep);
|
||||
}
|
||||
}
|
||||
|
||||
return nav;
|
||||
}
|
||||
|
||||
function mount() {
|
||||
if (!shouldRender()) return;
|
||||
var header = document.querySelector('.app-header');
|
||||
if (!header) return;
|
||||
// Don't double-mount if a tool's main.js calls us a second time.
|
||||
if (header.nextElementSibling &&
|
||||
header.nextElementSibling.classList &&
|
||||
header.nextElementSibling.classList.contains('zddc-stage-strip')) {
|
||||
return;
|
||||
}
|
||||
var project = projectSegment(location.pathname);
|
||||
var active = currentStage(location.pathname);
|
||||
var strip = buildStrip(project, active);
|
||||
header.parentNode.insertBefore(strip, header.nextSibling);
|
||||
}
|
||||
|
||||
// Expose for tests + opt-out.
|
||||
window.zddc.nav = {
|
||||
mount: mount,
|
||||
// Internals visible for unit tests; do not call from tools.
|
||||
_projectSegment: projectSegment,
|
||||
_currentStage: currentStage,
|
||||
_stages: STAGES,
|
||||
// Set to true before DOMContentLoaded to suppress mounting on
|
||||
// deployments where the canonical folder layout doesn't apply.
|
||||
disabled: false,
|
||||
};
|
||||
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', mount, { once: true });
|
||||
} else {
|
||||
mount();
|
||||
}
|
||||
})();
|
||||
|
||||
/**
|
||||
* ZDDC shared help panel — open/close logic.
|
||||
* Works with all four tools regardless of their module pattern.
|
||||
|
|
|
|||
|
|
@ -335,6 +335,11 @@ a:hover {
|
|||
font-size: 1rem;
|
||||
}
|
||||
|
||||
/* The refresh ⟳ glyph renders slightly smaller than ◐ / ? — bump to match. */
|
||||
#refreshHeaderBtn {
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
/* Toast CSS lives in classifier/css/base.css — only that tool uses toasts. */
|
||||
|
||||
/* ── Theme and help icon buttons ─────────────────────────────────────────── */
|
||||
|
|
@ -525,6 +530,104 @@ body.help-open .app-header {
|
|||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
/* shared/toast.css — single-toast notification styles paired with
|
||||
shared/toast.js. Uses BEM-ish .zddc-toast prefix to avoid collisions
|
||||
with tool-local .toast classes; the old classifier rules can stay
|
||||
alongside until this file is concatenated above them in the build. */
|
||||
|
||||
.zddc-toast {
|
||||
position: fixed;
|
||||
bottom: 2rem;
|
||||
right: 2rem;
|
||||
background: var(--bg);
|
||||
color: var(--text);
|
||||
padding: 0.875rem 1.25rem;
|
||||
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;
|
||||
font-size: 0.875rem;
|
||||
cursor: pointer;
|
||||
animation: zddc-toast-in 0.3s ease-out;
|
||||
}
|
||||
|
||||
.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); }
|
||||
.zddc-toast--warning { border-left: 4px solid var(--warning); }
|
||||
|
||||
.zddc-toast--fade {
|
||||
animation: zddc-toast-out 0.3s ease-out forwards;
|
||||
}
|
||||
|
||||
@keyframes zddc-toast-in {
|
||||
from { transform: translateX(100%); opacity: 0; }
|
||||
to { transform: translateX(0); opacity: 1; }
|
||||
}
|
||||
|
||||
@keyframes zddc-toast-out {
|
||||
from { transform: translateX(0); opacity: 1; }
|
||||
to { transform: translateX(100%); opacity: 0; }
|
||||
}
|
||||
|
||||
/* shared/nav.css — lateral project-stage strip paired with shared/nav.js.
|
||||
Sits as a sibling immediately under .app-header (mounted by JS).
|
||||
Rendered only in online mode when a project segment is in the URL. */
|
||||
|
||||
.zddc-stage-strip {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
padding: 0.3rem 1rem;
|
||||
background: var(--bg);
|
||||
border-bottom: 1px solid var(--border);
|
||||
font-size: 0.8rem;
|
||||
line-height: 1.3;
|
||||
flex-shrink: 0;
|
||||
overflow-x: auto;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.zddc-stage-strip__project {
|
||||
color: var(--text);
|
||||
font-weight: 600;
|
||||
margin-right: 0.15rem;
|
||||
}
|
||||
|
||||
.zddc-stage-strip__divider,
|
||||
.zddc-stage-strip__sep {
|
||||
color: var(--text-muted);
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.zddc-stage-strip__divider {
|
||||
margin-right: 0.35rem;
|
||||
}
|
||||
|
||||
.zddc-stage {
|
||||
color: var(--text-muted);
|
||||
text-decoration: none;
|
||||
padding: 0.1rem 0.25rem;
|
||||
border-radius: var(--radius);
|
||||
transition: color 0.15s, background 0.15s;
|
||||
}
|
||||
|
||||
.zddc-stage:hover {
|
||||
color: var(--text);
|
||||
background: var(--bg-secondary);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.zddc-stage--active {
|
||||
color: var(--primary);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.zddc-stage--active:hover {
|
||||
color: var(--primary);
|
||||
}
|
||||
|
||||
/* Classifier-specific base overrides
|
||||
Reset, tokens, buttons, and font are provided by shared/base.css */
|
||||
|
||||
|
|
@ -555,41 +658,8 @@ body.help-open .app-header {
|
|||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* ── Toast notifications (classifier-only) ───────────────────────────────── */
|
||||
/* shared/base.css intentionally omits toast CSS; only classifier uses toasts. */
|
||||
.toast {
|
||||
position: fixed;
|
||||
bottom: 2rem;
|
||||
right: 2rem;
|
||||
background: var(--bg);
|
||||
color: var(--text);
|
||||
padding: 0.875rem 1.25rem;
|
||||
border-radius: var(--radius);
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||||
z-index: 9000;
|
||||
max-width: 400px;
|
||||
font-size: 0.875rem;
|
||||
animation: zddc-toast-in 0.3s ease-out;
|
||||
}
|
||||
|
||||
.toast-success { border-left: 4px solid var(--success); }
|
||||
.toast-error { border-left: 4px solid var(--danger); }
|
||||
.toast-info { border-left: 4px solid var(--info); }
|
||||
.toast-warning { border-left: 4px solid var(--warning); }
|
||||
|
||||
.toast-fade {
|
||||
animation: zddc-toast-out 0.3s ease-out forwards;
|
||||
}
|
||||
|
||||
@keyframes zddc-toast-in {
|
||||
from { transform: translateX(100%); opacity: 0; }
|
||||
to { transform: translateX(0); opacity: 1; }
|
||||
}
|
||||
|
||||
@keyframes zddc-toast-out {
|
||||
from { transform: translateX(0); opacity: 1; }
|
||||
to { transform: translateX(100%); opacity: 0; }
|
||||
}
|
||||
/* Toast notifications come from shared/toast.css (.zddc-toast); the
|
||||
classifier-local .toast block was promoted there. */
|
||||
|
||||
/* Classifier layout — tokens from shared/base.css */
|
||||
|
||||
|
|
@ -1394,7 +1464,7 @@ body.help-open .app-header {
|
|||
</svg>
|
||||
<div class="header-title-group">
|
||||
<span class="app-header__title">ZDDC Classifier</span>
|
||||
<span class="build-timestamp"><span style="color:red;font-weight:bold">v0.0.17-beta · 2026-05-08 · falcon-alder-ginger</span></span>
|
||||
<span class="build-timestamp"><span style="color:red;font-weight:bold">v0.0.17-beta · 2026-05-10 · alder-cherry-reef</span></span>
|
||||
</div>
|
||||
<button id="addDirectoryBtn" class="btn btn-primary">Add Local Directory</button>
|
||||
<button id="refreshHeaderBtn" class="btn btn-secondary hidden" title="Refresh and rescan directory" aria-label="Refresh" style="font-size:1.1rem;">⟳</button>
|
||||
|
|
@ -2559,6 +2629,212 @@ https://github.com/nodeca/pako/blob/main/LICENSE
|
|||
}
|
||||
}());
|
||||
|
||||
// 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.
|
||||
//
|
||||
// Usage:
|
||||
// window.zddc.toast('Saved.', 'success');
|
||||
// window.zddc.toast('Could not load: ' + err.message, 'error');
|
||||
// window.zddc.toast('Note', 'info', { durationMs: 3000 });
|
||||
//
|
||||
// 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;
|
||||
|
||||
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 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);
|
||||
|
||||
// Click-to-dismiss. Useful for sticky errors the user wants gone.
|
||||
el.addEventListener('click', function () {
|
||||
clearTimeout(timer);
|
||||
if (el.parentNode) el.parentNode.removeChild(el);
|
||||
});
|
||||
|
||||
return el;
|
||||
}
|
||||
|
||||
window.zddc.toast = toast;
|
||||
})();
|
||||
|
||||
// shared/nav.js — lateral navigation strip across the four canonical
|
||||
// project stages (archive · working · staging · reviewing). Renders
|
||||
// only when:
|
||||
// 1. location.protocol is http: or https: (online — file:// has no
|
||||
// project structure to navigate within), AND
|
||||
// 2. a project segment can be detected from location.pathname (the
|
||||
// first path segment, when it isn't a tool HTML file).
|
||||
//
|
||||
// The strip is inserted as a sibling of <header class="app-header">
|
||||
// on DOMContentLoaded — no template changes required. Each tool just
|
||||
// needs ../shared/nav.{js,css} in its build.sh.
|
||||
//
|
||||
// Stage URLs follow the canonical workflow folders documented at
|
||||
// zddc.varasys.io/reference.html#transmittal-workflow:
|
||||
// archive → <project>/archive.html (archive tool, project-root mode)
|
||||
// working → <project>/working/ (directory listing → mdedit auto-serves)
|
||||
// staging → <project>/staging/ (directory listing → transmittal auto-serves)
|
||||
// reviewing → <project>/reviewing/ (directory listing)
|
||||
//
|
||||
// If a deployment doesn't have one of these folders the link will 404 —
|
||||
// the strip is convention-driven, not probed. Operators on non-standard
|
||||
// layouts can override by setting window.zddc.nav.disabled = true before
|
||||
// DOMContentLoaded.
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
if (!window.zddc) window.zddc = {};
|
||||
if (window.zddc.nav) return; // already loaded
|
||||
|
||||
var STAGES = [
|
||||
{ key: 'archive', label: 'Archive', target: 'archive.html' },
|
||||
{ key: 'working', label: 'Working', target: 'working/' },
|
||||
{ key: 'staging', label: 'Staging', target: 'staging/' },
|
||||
{ key: 'reviewing', label: 'Reviewing', target: 'reviewing/' },
|
||||
];
|
||||
|
||||
function projectSegment(pathname) {
|
||||
var parts = pathname.split('/').filter(Boolean);
|
||||
if (parts.length === 0) return null;
|
||||
var first = parts[0];
|
||||
// At deployment root (e.g. /archive.html?projects=A,B or
|
||||
// /index.html) the first segment is a tool HTML — no single
|
||||
// project to scope the strip to.
|
||||
if (first.indexOf('.') !== -1) return null;
|
||||
return first;
|
||||
}
|
||||
|
||||
function currentStage(pathname) {
|
||||
var parts = pathname.split('/').filter(Boolean);
|
||||
if (parts.length < 2) return null;
|
||||
var second = parts[1];
|
||||
// <project>/working/... | staging/... | reviewing/... | archive/...
|
||||
for (var i = 0; i < STAGES.length; i++) {
|
||||
if (second === STAGES[i].key) return STAGES[i].key;
|
||||
}
|
||||
// <project>/archive.html → still the archive stage
|
||||
if (second === 'archive.html') return 'archive';
|
||||
return null;
|
||||
}
|
||||
|
||||
function shouldRender() {
|
||||
if (typeof location === 'undefined') return false;
|
||||
if (location.protocol !== 'http:' && location.protocol !== 'https:') return false;
|
||||
if (window.zddc.nav && window.zddc.nav.disabled) return false;
|
||||
return projectSegment(location.pathname) !== null;
|
||||
}
|
||||
|
||||
function buildStrip(project, active) {
|
||||
var nav = document.createElement('nav');
|
||||
nav.className = 'zddc-stage-strip';
|
||||
nav.setAttribute('aria-label', 'Project stage');
|
||||
|
||||
var label = document.createElement('span');
|
||||
label.className = 'zddc-stage-strip__project';
|
||||
label.textContent = project;
|
||||
nav.appendChild(label);
|
||||
|
||||
var sep0 = document.createElement('span');
|
||||
sep0.className = 'zddc-stage-strip__divider';
|
||||
sep0.setAttribute('aria-hidden', 'true');
|
||||
sep0.textContent = '/';
|
||||
nav.appendChild(sep0);
|
||||
|
||||
for (var i = 0; i < STAGES.length; i++) {
|
||||
var s = STAGES[i];
|
||||
var a = document.createElement('a');
|
||||
a.className = 'zddc-stage';
|
||||
a.href = '/' + encodeURIComponent(project) + '/' + s.target;
|
||||
a.textContent = s.label;
|
||||
if (s.key === active) {
|
||||
a.classList.add('zddc-stage--active');
|
||||
a.setAttribute('aria-current', 'page');
|
||||
}
|
||||
nav.appendChild(a);
|
||||
|
||||
if (i < STAGES.length - 1) {
|
||||
var sep = document.createElement('span');
|
||||
sep.className = 'zddc-stage-strip__sep';
|
||||
sep.setAttribute('aria-hidden', 'true');
|
||||
sep.textContent = '·';
|
||||
nav.appendChild(sep);
|
||||
}
|
||||
}
|
||||
|
||||
return nav;
|
||||
}
|
||||
|
||||
function mount() {
|
||||
if (!shouldRender()) return;
|
||||
var header = document.querySelector('.app-header');
|
||||
if (!header) return;
|
||||
// Don't double-mount if a tool's main.js calls us a second time.
|
||||
if (header.nextElementSibling &&
|
||||
header.nextElementSibling.classList &&
|
||||
header.nextElementSibling.classList.contains('zddc-stage-strip')) {
|
||||
return;
|
||||
}
|
||||
var project = projectSegment(location.pathname);
|
||||
var active = currentStage(location.pathname);
|
||||
var strip = buildStrip(project, active);
|
||||
header.parentNode.insertBefore(strip, header.nextSibling);
|
||||
}
|
||||
|
||||
// Expose for tests + opt-out.
|
||||
window.zddc.nav = {
|
||||
mount: mount,
|
||||
// Internals visible for unit tests; do not call from tools.
|
||||
_projectSegment: projectSegment,
|
||||
_currentStage: currentStage,
|
||||
_stages: STAGES,
|
||||
// Set to true before DOMContentLoaded to suppress mounting on
|
||||
// deployments where the canonical folder layout doesn't apply.
|
||||
disabled: false,
|
||||
};
|
||||
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', mount, { once: true });
|
||||
} else {
|
||||
mount();
|
||||
}
|
||||
})();
|
||||
|
||||
/**
|
||||
* ZDDC — shared preview helpers
|
||||
*
|
||||
|
|
@ -7863,26 +8139,19 @@ https://github.com/nodeca/pako/blob/main/LICENSE
|
|||
'use strict';
|
||||
|
||||
/**
|
||||
* Show toast notification
|
||||
* Thin wrapper over the shared toast helper. Keeps the
|
||||
* window.app.modules.excel.showToast call sites in classifier
|
||||
* unchanged while delegating to the canonical implementation in
|
||||
* shared/toast.js (window.zddc.toast).
|
||||
*/
|
||||
function showToast(message, type = 'info') {
|
||||
// Remove existing toast
|
||||
const existing = document.querySelector('.toast');
|
||||
if (existing) {
|
||||
existing.remove();
|
||||
if (window.zddc && typeof window.zddc.toast === 'function') {
|
||||
window.zddc.toast(message, type);
|
||||
} else {
|
||||
// shared/toast.js missing from the build — log so the
|
||||
// problem is visible without crashing the caller.
|
||||
console.warn('[classifier] window.zddc.toast unavailable;', type, message);
|
||||
}
|
||||
|
||||
// Create toast
|
||||
const toast = document.createElement('div');
|
||||
toast.className = `toast toast-${type}`;
|
||||
toast.textContent = message;
|
||||
document.body.appendChild(toast);
|
||||
|
||||
// Auto-remove after 5 seconds
|
||||
setTimeout(() => {
|
||||
toast.classList.add('toast-fade');
|
||||
setTimeout(() => toast.remove(), 300);
|
||||
}, 5000);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -335,6 +335,11 @@ a:hover {
|
|||
font-size: 1rem;
|
||||
}
|
||||
|
||||
/* The refresh ⟳ glyph renders slightly smaller than ◐ / ? — bump to match. */
|
||||
#refreshHeaderBtn {
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
/* Toast CSS lives in classifier/css/base.css — only that tool uses toasts. */
|
||||
|
||||
/* ── Theme and help icon buttons ─────────────────────────────────────────── */
|
||||
|
|
@ -525,6 +530,104 @@ body.help-open .app-header {
|
|||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
/* shared/toast.css — single-toast notification styles paired with
|
||||
shared/toast.js. Uses BEM-ish .zddc-toast prefix to avoid collisions
|
||||
with tool-local .toast classes; the old classifier rules can stay
|
||||
alongside until this file is concatenated above them in the build. */
|
||||
|
||||
.zddc-toast {
|
||||
position: fixed;
|
||||
bottom: 2rem;
|
||||
right: 2rem;
|
||||
background: var(--bg);
|
||||
color: var(--text);
|
||||
padding: 0.875rem 1.25rem;
|
||||
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;
|
||||
font-size: 0.875rem;
|
||||
cursor: pointer;
|
||||
animation: zddc-toast-in 0.3s ease-out;
|
||||
}
|
||||
|
||||
.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); }
|
||||
.zddc-toast--warning { border-left: 4px solid var(--warning); }
|
||||
|
||||
.zddc-toast--fade {
|
||||
animation: zddc-toast-out 0.3s ease-out forwards;
|
||||
}
|
||||
|
||||
@keyframes zddc-toast-in {
|
||||
from { transform: translateX(100%); opacity: 0; }
|
||||
to { transform: translateX(0); opacity: 1; }
|
||||
}
|
||||
|
||||
@keyframes zddc-toast-out {
|
||||
from { transform: translateX(0); opacity: 1; }
|
||||
to { transform: translateX(100%); opacity: 0; }
|
||||
}
|
||||
|
||||
/* shared/nav.css — lateral project-stage strip paired with shared/nav.js.
|
||||
Sits as a sibling immediately under .app-header (mounted by JS).
|
||||
Rendered only in online mode when a project segment is in the URL. */
|
||||
|
||||
.zddc-stage-strip {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
padding: 0.3rem 1rem;
|
||||
background: var(--bg);
|
||||
border-bottom: 1px solid var(--border);
|
||||
font-size: 0.8rem;
|
||||
line-height: 1.3;
|
||||
flex-shrink: 0;
|
||||
overflow-x: auto;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.zddc-stage-strip__project {
|
||||
color: var(--text);
|
||||
font-weight: 600;
|
||||
margin-right: 0.15rem;
|
||||
}
|
||||
|
||||
.zddc-stage-strip__divider,
|
||||
.zddc-stage-strip__sep {
|
||||
color: var(--text-muted);
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.zddc-stage-strip__divider {
|
||||
margin-right: 0.35rem;
|
||||
}
|
||||
|
||||
.zddc-stage {
|
||||
color: var(--text-muted);
|
||||
text-decoration: none;
|
||||
padding: 0.1rem 0.25rem;
|
||||
border-radius: var(--radius);
|
||||
transition: color 0.15s, background 0.15s;
|
||||
}
|
||||
|
||||
.zddc-stage:hover {
|
||||
color: var(--text);
|
||||
background: var(--bg-secondary);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.zddc-stage--active {
|
||||
color: var(--primary);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.zddc-stage--active:hover {
|
||||
color: var(--primary);
|
||||
}
|
||||
|
||||
/* Landing page layout */
|
||||
body {
|
||||
margin: 0;
|
||||
|
|
@ -885,7 +988,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.17-beta · 2026-05-08 · falcon-alder-ginger</span></span>
|
||||
<span class="build-timestamp"><span style="color:red;font-weight:bold">v0.0.17-beta · 2026-05-10 · alder-cherry-reef</span></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="header-right">
|
||||
|
|
@ -1629,6 +1732,212 @@ 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.
|
||||
//
|
||||
// Usage:
|
||||
// window.zddc.toast('Saved.', 'success');
|
||||
// window.zddc.toast('Could not load: ' + err.message, 'error');
|
||||
// window.zddc.toast('Note', 'info', { durationMs: 3000 });
|
||||
//
|
||||
// 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;
|
||||
|
||||
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 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);
|
||||
|
||||
// Click-to-dismiss. Useful for sticky errors the user wants gone.
|
||||
el.addEventListener('click', function () {
|
||||
clearTimeout(timer);
|
||||
if (el.parentNode) el.parentNode.removeChild(el);
|
||||
});
|
||||
|
||||
return el;
|
||||
}
|
||||
|
||||
window.zddc.toast = toast;
|
||||
})();
|
||||
|
||||
// shared/nav.js — lateral navigation strip across the four canonical
|
||||
// project stages (archive · working · staging · reviewing). Renders
|
||||
// only when:
|
||||
// 1. location.protocol is http: or https: (online — file:// has no
|
||||
// project structure to navigate within), AND
|
||||
// 2. a project segment can be detected from location.pathname (the
|
||||
// first path segment, when it isn't a tool HTML file).
|
||||
//
|
||||
// The strip is inserted as a sibling of <header class="app-header">
|
||||
// on DOMContentLoaded — no template changes required. Each tool just
|
||||
// needs ../shared/nav.{js,css} in its build.sh.
|
||||
//
|
||||
// Stage URLs follow the canonical workflow folders documented at
|
||||
// zddc.varasys.io/reference.html#transmittal-workflow:
|
||||
// archive → <project>/archive.html (archive tool, project-root mode)
|
||||
// working → <project>/working/ (directory listing → mdedit auto-serves)
|
||||
// staging → <project>/staging/ (directory listing → transmittal auto-serves)
|
||||
// reviewing → <project>/reviewing/ (directory listing)
|
||||
//
|
||||
// If a deployment doesn't have one of these folders the link will 404 —
|
||||
// the strip is convention-driven, not probed. Operators on non-standard
|
||||
// layouts can override by setting window.zddc.nav.disabled = true before
|
||||
// DOMContentLoaded.
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
if (!window.zddc) window.zddc = {};
|
||||
if (window.zddc.nav) return; // already loaded
|
||||
|
||||
var STAGES = [
|
||||
{ key: 'archive', label: 'Archive', target: 'archive.html' },
|
||||
{ key: 'working', label: 'Working', target: 'working/' },
|
||||
{ key: 'staging', label: 'Staging', target: 'staging/' },
|
||||
{ key: 'reviewing', label: 'Reviewing', target: 'reviewing/' },
|
||||
];
|
||||
|
||||
function projectSegment(pathname) {
|
||||
var parts = pathname.split('/').filter(Boolean);
|
||||
if (parts.length === 0) return null;
|
||||
var first = parts[0];
|
||||
// At deployment root (e.g. /archive.html?projects=A,B or
|
||||
// /index.html) the first segment is a tool HTML — no single
|
||||
// project to scope the strip to.
|
||||
if (first.indexOf('.') !== -1) return null;
|
||||
return first;
|
||||
}
|
||||
|
||||
function currentStage(pathname) {
|
||||
var parts = pathname.split('/').filter(Boolean);
|
||||
if (parts.length < 2) return null;
|
||||
var second = parts[1];
|
||||
// <project>/working/... | staging/... | reviewing/... | archive/...
|
||||
for (var i = 0; i < STAGES.length; i++) {
|
||||
if (second === STAGES[i].key) return STAGES[i].key;
|
||||
}
|
||||
// <project>/archive.html → still the archive stage
|
||||
if (second === 'archive.html') return 'archive';
|
||||
return null;
|
||||
}
|
||||
|
||||
function shouldRender() {
|
||||
if (typeof location === 'undefined') return false;
|
||||
if (location.protocol !== 'http:' && location.protocol !== 'https:') return false;
|
||||
if (window.zddc.nav && window.zddc.nav.disabled) return false;
|
||||
return projectSegment(location.pathname) !== null;
|
||||
}
|
||||
|
||||
function buildStrip(project, active) {
|
||||
var nav = document.createElement('nav');
|
||||
nav.className = 'zddc-stage-strip';
|
||||
nav.setAttribute('aria-label', 'Project stage');
|
||||
|
||||
var label = document.createElement('span');
|
||||
label.className = 'zddc-stage-strip__project';
|
||||
label.textContent = project;
|
||||
nav.appendChild(label);
|
||||
|
||||
var sep0 = document.createElement('span');
|
||||
sep0.className = 'zddc-stage-strip__divider';
|
||||
sep0.setAttribute('aria-hidden', 'true');
|
||||
sep0.textContent = '/';
|
||||
nav.appendChild(sep0);
|
||||
|
||||
for (var i = 0; i < STAGES.length; i++) {
|
||||
var s = STAGES[i];
|
||||
var a = document.createElement('a');
|
||||
a.className = 'zddc-stage';
|
||||
a.href = '/' + encodeURIComponent(project) + '/' + s.target;
|
||||
a.textContent = s.label;
|
||||
if (s.key === active) {
|
||||
a.classList.add('zddc-stage--active');
|
||||
a.setAttribute('aria-current', 'page');
|
||||
}
|
||||
nav.appendChild(a);
|
||||
|
||||
if (i < STAGES.length - 1) {
|
||||
var sep = document.createElement('span');
|
||||
sep.className = 'zddc-stage-strip__sep';
|
||||
sep.setAttribute('aria-hidden', 'true');
|
||||
sep.textContent = '·';
|
||||
nav.appendChild(sep);
|
||||
}
|
||||
}
|
||||
|
||||
return nav;
|
||||
}
|
||||
|
||||
function mount() {
|
||||
if (!shouldRender()) return;
|
||||
var header = document.querySelector('.app-header');
|
||||
if (!header) return;
|
||||
// Don't double-mount if a tool's main.js calls us a second time.
|
||||
if (header.nextElementSibling &&
|
||||
header.nextElementSibling.classList &&
|
||||
header.nextElementSibling.classList.contains('zddc-stage-strip')) {
|
||||
return;
|
||||
}
|
||||
var project = projectSegment(location.pathname);
|
||||
var active = currentStage(location.pathname);
|
||||
var strip = buildStrip(project, active);
|
||||
header.parentNode.insertBefore(strip, header.nextSibling);
|
||||
}
|
||||
|
||||
// Expose for tests + opt-out.
|
||||
window.zddc.nav = {
|
||||
mount: mount,
|
||||
// Internals visible for unit tests; do not call from tools.
|
||||
_projectSegment: projectSegment,
|
||||
_currentStage: currentStage,
|
||||
_stages: STAGES,
|
||||
// Set to true before DOMContentLoaded to suppress mounting on
|
||||
// deployments where the canonical folder layout doesn't apply.
|
||||
disabled: false,
|
||||
};
|
||||
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', mount, { once: true });
|
||||
} else {
|
||||
mount();
|
||||
}
|
||||
})();
|
||||
|
||||
/**
|
||||
* ZDDC shared help panel — open/close logic.
|
||||
* Works with all four tools regardless of their module pattern.
|
||||
|
|
@ -2107,8 +2416,22 @@ body {
|
|||
function openArchiveWith(names) {
|
||||
if (!names || names.length === 0) return;
|
||||
var base = location.pathname.replace(/\/[^\/]*$/, '/');
|
||||
var params = ['projects=' + names.map(encodeURIComponent).join(',')];
|
||||
var v = new URLSearchParams(location.search).get('v');
|
||||
|
||||
if (names.length === 1) {
|
||||
// Single project → canonical project-subtree URL so the user
|
||||
// can edit the address bar to swap archive.html for
|
||||
// working/, staging/, reviewing/, etc. zddc-server's
|
||||
// availability.go auto-serves the right tool at each.
|
||||
// Multi-project (the `else` branch) keeps the ?projects=
|
||||
// form because there's no single subtree root.
|
||||
var url = base + encodeURIComponent(names[0]) + '/archive.html';
|
||||
if (v) url += '?v=' + encodeURIComponent(v);
|
||||
navigate(url);
|
||||
return;
|
||||
}
|
||||
|
||||
var params = ['projects=' + names.map(encodeURIComponent).join(',')];
|
||||
if (v) params.push('v=' + encodeURIComponent(v));
|
||||
navigate(base + 'archive.html?' + params.join('&'));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -555,6 +555,11 @@ a:hover {
|
|||
font-size: 1rem;
|
||||
}
|
||||
|
||||
/* The refresh ⟳ glyph renders slightly smaller than ◐ / ? — bump to match. */
|
||||
#refreshHeaderBtn {
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
/* Toast CSS lives in classifier/css/base.css — only that tool uses toasts. */
|
||||
|
||||
/* ── Theme and help icon buttons ─────────────────────────────────────────── */
|
||||
|
|
@ -745,6 +750,104 @@ body.help-open .app-header {
|
|||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
/* shared/toast.css — single-toast notification styles paired with
|
||||
shared/toast.js. Uses BEM-ish .zddc-toast prefix to avoid collisions
|
||||
with tool-local .toast classes; the old classifier rules can stay
|
||||
alongside until this file is concatenated above them in the build. */
|
||||
|
||||
.zddc-toast {
|
||||
position: fixed;
|
||||
bottom: 2rem;
|
||||
right: 2rem;
|
||||
background: var(--bg);
|
||||
color: var(--text);
|
||||
padding: 0.875rem 1.25rem;
|
||||
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;
|
||||
font-size: 0.875rem;
|
||||
cursor: pointer;
|
||||
animation: zddc-toast-in 0.3s ease-out;
|
||||
}
|
||||
|
||||
.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); }
|
||||
.zddc-toast--warning { border-left: 4px solid var(--warning); }
|
||||
|
||||
.zddc-toast--fade {
|
||||
animation: zddc-toast-out 0.3s ease-out forwards;
|
||||
}
|
||||
|
||||
@keyframes zddc-toast-in {
|
||||
from { transform: translateX(100%); opacity: 0; }
|
||||
to { transform: translateX(0); opacity: 1; }
|
||||
}
|
||||
|
||||
@keyframes zddc-toast-out {
|
||||
from { transform: translateX(0); opacity: 1; }
|
||||
to { transform: translateX(100%); opacity: 0; }
|
||||
}
|
||||
|
||||
/* shared/nav.css — lateral project-stage strip paired with shared/nav.js.
|
||||
Sits as a sibling immediately under .app-header (mounted by JS).
|
||||
Rendered only in online mode when a project segment is in the URL. */
|
||||
|
||||
.zddc-stage-strip {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
padding: 0.3rem 1rem;
|
||||
background: var(--bg);
|
||||
border-bottom: 1px solid var(--border);
|
||||
font-size: 0.8rem;
|
||||
line-height: 1.3;
|
||||
flex-shrink: 0;
|
||||
overflow-x: auto;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.zddc-stage-strip__project {
|
||||
color: var(--text);
|
||||
font-weight: 600;
|
||||
margin-right: 0.15rem;
|
||||
}
|
||||
|
||||
.zddc-stage-strip__divider,
|
||||
.zddc-stage-strip__sep {
|
||||
color: var(--text-muted);
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.zddc-stage-strip__divider {
|
||||
margin-right: 0.35rem;
|
||||
}
|
||||
|
||||
.zddc-stage {
|
||||
color: var(--text-muted);
|
||||
text-decoration: none;
|
||||
padding: 0.1rem 0.25rem;
|
||||
border-radius: var(--radius);
|
||||
transition: color 0.15s, background 0.15s;
|
||||
}
|
||||
|
||||
.zddc-stage:hover {
|
||||
color: var(--text);
|
||||
background: var(--bg-secondary);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.zddc-stage--active {
|
||||
color: var(--primary);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.zddc-stage--active:hover {
|
||||
color: var(--primary);
|
||||
}
|
||||
|
||||
/* mdedit component styles — reset and tokens from shared/base.css */
|
||||
|
||||
/* Pane resizer */
|
||||
|
|
@ -1151,6 +1254,14 @@ body.help-open .app-header {
|
|||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
/* File-nav pane: initial width + minimum size. Runtime resizer (resizer.js)
|
||||
overrides via inline style.width when the user drags; the min-width here
|
||||
is a defensive backstop. */
|
||||
#file-nav {
|
||||
width: 450px;
|
||||
min-width: 200px;
|
||||
}
|
||||
|
||||
/* Toast UI Editor styles */
|
||||
#markdown-editor {
|
||||
display: block !important;
|
||||
|
|
@ -1792,10 +1903,10 @@ body.help-open .app-header {
|
|||
</svg>
|
||||
<div class="header-title-group">
|
||||
<span class="app-header__title">ZDDC Markdown</span>
|
||||
<span class="build-timestamp"><span style="color:red;font-weight:bold">v0.0.17-beta · 2026-05-08 · falcon-alder-ginger</span></span>
|
||||
<span class="build-timestamp"><span style="color:red;font-weight:bold">v0.0.17-beta · 2026-05-10 · alder-cherry-reef</span></span>
|
||||
</div>
|
||||
<button id="addDirectoryBtn" class="btn btn-primary" title="Add a local directory">Add Local Directory</button>
|
||||
<button id="refreshHeaderBtn" class="btn btn-secondary hidden" title="Refresh directory" aria-label="Refresh" style="font-size:1.1rem;">⟳</button>
|
||||
<button id="refreshHeaderBtn" class="btn btn-secondary hidden" title="Refresh directory" aria-label="Refresh">⟳</button>
|
||||
</div>
|
||||
<div class="header-right">
|
||||
<button id="theme-btn" class="btn btn-secondary" title="Theme: auto (follows OS)" aria-label="Theme: auto (follows OS)">◐</button>
|
||||
|
|
@ -1805,7 +1916,7 @@ body.help-open .app-header {
|
|||
|
||||
<main class="flex-1 overflow-hidden relative">
|
||||
<div class="resizable-pane horizontal flex flex-row relative w-full h-full overflow-hidden" id="root-pane" data-pane-type="root">
|
||||
<div class="pane nav-pane relative flex flex-col bg-white dark:bg-gray-900 overflow-hidden" id="file-nav" data-pane-type="file-nav" style="width: 450px; min-width: 200px;">
|
||||
<div class="pane nav-pane relative flex flex-col bg-white dark:bg-gray-900 overflow-hidden" id="file-nav" data-pane-type="file-nav">
|
||||
<div class="pane-header flex flex-col px-4 py-2 bg-gray-100 dark:bg-gray-800 text-gray-700 dark:text-gray-200 font-medium border-b border-gray-200 dark:border-gray-700 select-none">
|
||||
<div class="flex justify-between items-center w-full">
|
||||
<span>Files</span>
|
||||
|
|
@ -2757,6 +2868,212 @@ body.help-open .app-header {
|
|||
}
|
||||
}());
|
||||
|
||||
// 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.
|
||||
//
|
||||
// Usage:
|
||||
// window.zddc.toast('Saved.', 'success');
|
||||
// window.zddc.toast('Could not load: ' + err.message, 'error');
|
||||
// window.zddc.toast('Note', 'info', { durationMs: 3000 });
|
||||
//
|
||||
// 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;
|
||||
|
||||
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 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);
|
||||
|
||||
// Click-to-dismiss. Useful for sticky errors the user wants gone.
|
||||
el.addEventListener('click', function () {
|
||||
clearTimeout(timer);
|
||||
if (el.parentNode) el.parentNode.removeChild(el);
|
||||
});
|
||||
|
||||
return el;
|
||||
}
|
||||
|
||||
window.zddc.toast = toast;
|
||||
})();
|
||||
|
||||
// shared/nav.js — lateral navigation strip across the four canonical
|
||||
// project stages (archive · working · staging · reviewing). Renders
|
||||
// only when:
|
||||
// 1. location.protocol is http: or https: (online — file:// has no
|
||||
// project structure to navigate within), AND
|
||||
// 2. a project segment can be detected from location.pathname (the
|
||||
// first path segment, when it isn't a tool HTML file).
|
||||
//
|
||||
// The strip is inserted as a sibling of <header class="app-header">
|
||||
// on DOMContentLoaded — no template changes required. Each tool just
|
||||
// needs ../shared/nav.{js,css} in its build.sh.
|
||||
//
|
||||
// Stage URLs follow the canonical workflow folders documented at
|
||||
// zddc.varasys.io/reference.html#transmittal-workflow:
|
||||
// archive → <project>/archive.html (archive tool, project-root mode)
|
||||
// working → <project>/working/ (directory listing → mdedit auto-serves)
|
||||
// staging → <project>/staging/ (directory listing → transmittal auto-serves)
|
||||
// reviewing → <project>/reviewing/ (directory listing)
|
||||
//
|
||||
// If a deployment doesn't have one of these folders the link will 404 —
|
||||
// the strip is convention-driven, not probed. Operators on non-standard
|
||||
// layouts can override by setting window.zddc.nav.disabled = true before
|
||||
// DOMContentLoaded.
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
if (!window.zddc) window.zddc = {};
|
||||
if (window.zddc.nav) return; // already loaded
|
||||
|
||||
var STAGES = [
|
||||
{ key: 'archive', label: 'Archive', target: 'archive.html' },
|
||||
{ key: 'working', label: 'Working', target: 'working/' },
|
||||
{ key: 'staging', label: 'Staging', target: 'staging/' },
|
||||
{ key: 'reviewing', label: 'Reviewing', target: 'reviewing/' },
|
||||
];
|
||||
|
||||
function projectSegment(pathname) {
|
||||
var parts = pathname.split('/').filter(Boolean);
|
||||
if (parts.length === 0) return null;
|
||||
var first = parts[0];
|
||||
// At deployment root (e.g. /archive.html?projects=A,B or
|
||||
// /index.html) the first segment is a tool HTML — no single
|
||||
// project to scope the strip to.
|
||||
if (first.indexOf('.') !== -1) return null;
|
||||
return first;
|
||||
}
|
||||
|
||||
function currentStage(pathname) {
|
||||
var parts = pathname.split('/').filter(Boolean);
|
||||
if (parts.length < 2) return null;
|
||||
var second = parts[1];
|
||||
// <project>/working/... | staging/... | reviewing/... | archive/...
|
||||
for (var i = 0; i < STAGES.length; i++) {
|
||||
if (second === STAGES[i].key) return STAGES[i].key;
|
||||
}
|
||||
// <project>/archive.html → still the archive stage
|
||||
if (second === 'archive.html') return 'archive';
|
||||
return null;
|
||||
}
|
||||
|
||||
function shouldRender() {
|
||||
if (typeof location === 'undefined') return false;
|
||||
if (location.protocol !== 'http:' && location.protocol !== 'https:') return false;
|
||||
if (window.zddc.nav && window.zddc.nav.disabled) return false;
|
||||
return projectSegment(location.pathname) !== null;
|
||||
}
|
||||
|
||||
function buildStrip(project, active) {
|
||||
var nav = document.createElement('nav');
|
||||
nav.className = 'zddc-stage-strip';
|
||||
nav.setAttribute('aria-label', 'Project stage');
|
||||
|
||||
var label = document.createElement('span');
|
||||
label.className = 'zddc-stage-strip__project';
|
||||
label.textContent = project;
|
||||
nav.appendChild(label);
|
||||
|
||||
var sep0 = document.createElement('span');
|
||||
sep0.className = 'zddc-stage-strip__divider';
|
||||
sep0.setAttribute('aria-hidden', 'true');
|
||||
sep0.textContent = '/';
|
||||
nav.appendChild(sep0);
|
||||
|
||||
for (var i = 0; i < STAGES.length; i++) {
|
||||
var s = STAGES[i];
|
||||
var a = document.createElement('a');
|
||||
a.className = 'zddc-stage';
|
||||
a.href = '/' + encodeURIComponent(project) + '/' + s.target;
|
||||
a.textContent = s.label;
|
||||
if (s.key === active) {
|
||||
a.classList.add('zddc-stage--active');
|
||||
a.setAttribute('aria-current', 'page');
|
||||
}
|
||||
nav.appendChild(a);
|
||||
|
||||
if (i < STAGES.length - 1) {
|
||||
var sep = document.createElement('span');
|
||||
sep.className = 'zddc-stage-strip__sep';
|
||||
sep.setAttribute('aria-hidden', 'true');
|
||||
sep.textContent = '·';
|
||||
nav.appendChild(sep);
|
||||
}
|
||||
}
|
||||
|
||||
return nav;
|
||||
}
|
||||
|
||||
function mount() {
|
||||
if (!shouldRender()) return;
|
||||
var header = document.querySelector('.app-header');
|
||||
if (!header) return;
|
||||
// Don't double-mount if a tool's main.js calls us a second time.
|
||||
if (header.nextElementSibling &&
|
||||
header.nextElementSibling.classList &&
|
||||
header.nextElementSibling.classList.contains('zddc-stage-strip')) {
|
||||
return;
|
||||
}
|
||||
var project = projectSegment(location.pathname);
|
||||
var active = currentStage(location.pathname);
|
||||
var strip = buildStrip(project, active);
|
||||
header.parentNode.insertBefore(strip, header.nextSibling);
|
||||
}
|
||||
|
||||
// Expose for tests + opt-out.
|
||||
window.zddc.nav = {
|
||||
mount: mount,
|
||||
// Internals visible for unit tests; do not call from tools.
|
||||
_projectSegment: projectSegment,
|
||||
_currentStage: currentStage,
|
||||
_stages: STAGES,
|
||||
// Set to true before DOMContentLoaded to suppress mounting on
|
||||
// deployments where the canonical folder layout doesn't apply.
|
||||
disabled: false,
|
||||
};
|
||||
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', mount, { once: true });
|
||||
} else {
|
||||
mount();
|
||||
}
|
||||
})();
|
||||
|
||||
/**
|
||||
* ZDDC — shared preview helpers
|
||||
*
|
||||
|
|
@ -4441,9 +4758,9 @@ async function reloadFileFromDisk(filePath) {
|
|||
}
|
||||
}, 100);
|
||||
|
||||
if (editorInstance.tocContainer && window.updateToc) {
|
||||
if (editorInstance.tocContainer) {
|
||||
try {
|
||||
window.updateToc(parsed.content, editorInstance.tocContainer, editorInstance.editor, tocMaxDepth);
|
||||
updateToc(parsed.content, editorInstance.tocContainer, editorInstance.editor, tocMaxDepth);
|
||||
} catch (error) {
|
||||
console.error('Error updating TOC during reload:', error);
|
||||
}
|
||||
|
|
@ -5837,9 +6154,9 @@ function initializeEditor(content, isMarkdown = true, filePath = '', fileName =
|
|||
|
||||
tocDepthSelector.addEventListener('change', function () {
|
||||
const depth = parseInt(this.value);
|
||||
if (window.updateToc && editorInstance) {
|
||||
if (editorInstance) {
|
||||
const currentContent = editorInstance.getMarkdown();
|
||||
window.updateToc(currentContent, tocContainer, editorInstance, depth);
|
||||
updateToc(currentContent, tocContainer, editorInstance, depth);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
@ -5886,16 +6203,16 @@ function initializeEditor(content, isMarkdown = true, filePath = '', fileName =
|
|||
}
|
||||
|
||||
// Generate initial TOC
|
||||
if (isMarkdown && window.updateToc && tocContainer) {
|
||||
if (isMarkdown && tocContainer) {
|
||||
try {
|
||||
window.updateToc(markdownBody, tocContainer, editorInstance, tocMaxDepth);
|
||||
updateToc(markdownBody, tocContainer, editorInstance, tocMaxDepth);
|
||||
} catch (error) {
|
||||
console.error('Error generating TOC:', error);
|
||||
}
|
||||
|
||||
const debouncedUpdateToc = debounce(() => {
|
||||
const currentContent = editorInstance.getMarkdown();
|
||||
window.updateToc(currentContent, tocContainer, editorInstance, tocMaxDepth);
|
||||
updateToc(currentContent, tocContainer, editorInstance, tocMaxDepth);
|
||||
}, 300);
|
||||
|
||||
editorInstance.on('change', () => {
|
||||
|
|
@ -6290,10 +6607,8 @@ function setActiveTocItem(tocContainer, headerText) {
|
|||
}
|
||||
}
|
||||
|
||||
// Export globally
|
||||
window.updateToc = updateToc;
|
||||
window.clearActiveTocItem = clearActiveTocItem;
|
||||
window.setActiveTocItem = setActiveTocItem;
|
||||
// Reachable at top-level scope to other concatenated mdedit JS files via the
|
||||
// build's flat-IIFE-less module pattern; no window.* exports needed.
|
||||
|
||||
/**
|
||||
* Pane resizing functionality
|
||||
|
|
@ -6506,7 +6821,7 @@ function setupTocDepthSelector() {
|
|||
const content = instance.editor.getMarkdown();
|
||||
|
||||
try {
|
||||
window.updateToc(content, instance.tocContainer, instance.editor, tocMaxDepth);
|
||||
updateToc(content, instance.tocContainer, instance.editor, tocMaxDepth);
|
||||
} catch (error) {
|
||||
console.error('Error updating TOC depth:', error);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -339,6 +339,11 @@ a:hover {
|
|||
font-size: 1rem;
|
||||
}
|
||||
|
||||
/* The refresh ⟳ glyph renders slightly smaller than ◐ / ? — bump to match. */
|
||||
#refreshHeaderBtn {
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
/* Toast CSS lives in classifier/css/base.css — only that tool uses toasts. */
|
||||
|
||||
/* ── Theme and help icon buttons ─────────────────────────────────────────── */
|
||||
|
|
@ -529,6 +534,104 @@ body.help-open .app-header {
|
|||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
/* shared/toast.css — single-toast notification styles paired with
|
||||
shared/toast.js. Uses BEM-ish .zddc-toast prefix to avoid collisions
|
||||
with tool-local .toast classes; the old classifier rules can stay
|
||||
alongside until this file is concatenated above them in the build. */
|
||||
|
||||
.zddc-toast {
|
||||
position: fixed;
|
||||
bottom: 2rem;
|
||||
right: 2rem;
|
||||
background: var(--bg);
|
||||
color: var(--text);
|
||||
padding: 0.875rem 1.25rem;
|
||||
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;
|
||||
font-size: 0.875rem;
|
||||
cursor: pointer;
|
||||
animation: zddc-toast-in 0.3s ease-out;
|
||||
}
|
||||
|
||||
.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); }
|
||||
.zddc-toast--warning { border-left: 4px solid var(--warning); }
|
||||
|
||||
.zddc-toast--fade {
|
||||
animation: zddc-toast-out 0.3s ease-out forwards;
|
||||
}
|
||||
|
||||
@keyframes zddc-toast-in {
|
||||
from { transform: translateX(100%); opacity: 0; }
|
||||
to { transform: translateX(0); opacity: 1; }
|
||||
}
|
||||
|
||||
@keyframes zddc-toast-out {
|
||||
from { transform: translateX(0); opacity: 1; }
|
||||
to { transform: translateX(100%); opacity: 0; }
|
||||
}
|
||||
|
||||
/* shared/nav.css — lateral project-stage strip paired with shared/nav.js.
|
||||
Sits as a sibling immediately under .app-header (mounted by JS).
|
||||
Rendered only in online mode when a project segment is in the URL. */
|
||||
|
||||
.zddc-stage-strip {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
padding: 0.3rem 1rem;
|
||||
background: var(--bg);
|
||||
border-bottom: 1px solid var(--border);
|
||||
font-size: 0.8rem;
|
||||
line-height: 1.3;
|
||||
flex-shrink: 0;
|
||||
overflow-x: auto;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.zddc-stage-strip__project {
|
||||
color: var(--text);
|
||||
font-weight: 600;
|
||||
margin-right: 0.15rem;
|
||||
}
|
||||
|
||||
.zddc-stage-strip__divider,
|
||||
.zddc-stage-strip__sep {
|
||||
color: var(--text-muted);
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.zddc-stage-strip__divider {
|
||||
margin-right: 0.35rem;
|
||||
}
|
||||
|
||||
.zddc-stage {
|
||||
color: var(--text-muted);
|
||||
text-decoration: none;
|
||||
padding: 0.1rem 0.25rem;
|
||||
border-radius: var(--radius);
|
||||
transition: color 0.15s, background 0.15s;
|
||||
}
|
||||
|
||||
.zddc-stage:hover {
|
||||
color: var(--text);
|
||||
background: var(--bg-secondary);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.zddc-stage--active {
|
||||
color: var(--primary);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.zddc-stage--active:hover {
|
||||
color: var(--primary);
|
||||
}
|
||||
|
||||
/* Placeholder for contenteditable elements */
|
||||
[data-placeholder]:empty::before {
|
||||
content: attr(data-placeholder);
|
||||
|
|
@ -638,51 +741,15 @@ body.help-open .app-header {
|
|||
border-bottom-color: #86efac;
|
||||
}
|
||||
|
||||
/* Owner/Project names area and inline bg-white / bg-gray-50 utility classes */
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root:not([data-theme="light"]) .header-names {
|
||||
background-color: var(--bg-secondary) !important;
|
||||
border-color: var(--border) !important;
|
||||
}
|
||||
:root:not([data-theme="light"]) .text-gray-700 { color: var(--text-muted) !important; }
|
||||
:root:not([data-theme="light"]) .bg-white { background-color: var(--bg) !important; }
|
||||
:root:not([data-theme="light"]) .bg-gray-50 { background-color: var(--bg-secondary) !important; }
|
||||
:root:not([data-theme="light"]) .bg-gray-100 { background-color: var(--bg-secondary) !important; }
|
||||
:root:not([data-theme="light"]) .border-gray-100,
|
||||
:root:not([data-theme="light"]) .border-gray-200,
|
||||
:root:not([data-theme="light"]) .border-gray-300 { border-color: var(--border) !important; }
|
||||
:root:not([data-theme="light"]) .text-gray-900 { color: var(--text) !important; }
|
||||
}
|
||||
[data-theme="dark"] .header-names {
|
||||
background-color: var(--bg-secondary) !important;
|
||||
border-color: var(--border) !important;
|
||||
}
|
||||
[data-theme="dark"] .text-gray-700 { color: var(--text-muted) !important; }
|
||||
[data-theme="dark"] .bg-white { background-color: var(--bg) !important; }
|
||||
[data-theme="dark"] .bg-gray-50 { background-color: var(--bg-secondary) !important; }
|
||||
[data-theme="dark"] .bg-gray-100 { background-color: var(--bg-secondary) !important; }
|
||||
[data-theme="dark"] .border-gray-100,
|
||||
[data-theme="dark"] .border-gray-200,
|
||||
[data-theme="dark"] .border-gray-300 { border-color: var(--border) !important; }
|
||||
[data-theme="dark"] .text-gray-900 { color: var(--text) !important; }
|
||||
|
||||
/* Filter inputs in table column headers */
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root:not([data-theme="light"]) .table-filter-input {
|
||||
background-color: var(--bg);
|
||||
color: var(--text);
|
||||
border-color: var(--border);
|
||||
}
|
||||
:root:not([data-theme="light"]) .table-header__caption { color: var(--text-muted); }
|
||||
:root:not([data-theme="light"]) .focus\:bg-white:focus { background-color: var(--bg) !important; }
|
||||
}
|
||||
[data-theme="dark"] .table-filter-input {
|
||||
background-color: var(--bg);
|
||||
color: var(--text);
|
||||
border-color: var(--border);
|
||||
}
|
||||
[data-theme="dark"] .table-header__caption { color: var(--text-muted); }
|
||||
[data-theme="dark"] .focus\:bg-white:focus { background-color: var(--bg) !important; }
|
||||
/* Note: dark-mode overrides for .bg-white / .bg-gray-* / .text-gray-*
|
||||
/ .border-gray-* and .header-names used to live here as a 17-line
|
||||
block of !important rules to fight hardcoded colors in
|
||||
transmittal/css/utilities.css. The utility classes were tokenized
|
||||
(var(--bg), var(--bg-secondary), var(--text), var(--text-muted),
|
||||
var(--border)) so the cascade now does the right thing in both
|
||||
themes without per-class overrides. .table-filter-input is unused
|
||||
(no element references it; .column-filter from shared is used
|
||||
instead) and was likewise dropped. */
|
||||
}
|
||||
|
||||
/* Logo row: flex layout — logo | title | logo */
|
||||
|
|
@ -1895,11 +1962,15 @@ dialog.modal--narrow {
|
|||
.text-2xl { font-size: 1.5rem; line-height: 2rem; }
|
||||
.text-\[12px\] { font-size: 12px; line-height: 1.4; }
|
||||
.text-\[10px\] { font-size: 10px; line-height: 1.3; }
|
||||
.text-gray-900 { color: #111827; }
|
||||
.text-gray-700 { color: #374151; }
|
||||
.text-gray-600 { color: #4b5563; }
|
||||
.text-gray-500 { color: #6b7280; }
|
||||
.text-gray-400 { color: #9ca3af; }
|
||||
/* Gray-scale text classes are theme-encoding — they map to shared
|
||||
tokens so dark mode swaps automatically without per-class overrides.
|
||||
The named-color text classes (.text-blue-600/-green-600/-red-600)
|
||||
carry semantic meaning (link / success / danger) and stay hardcoded. */
|
||||
.text-gray-900 { color: var(--text); }
|
||||
.text-gray-700 { color: var(--text-muted); }
|
||||
.text-gray-600 { color: var(--text-muted); }
|
||||
.text-gray-500 { color: var(--text-muted); }
|
||||
.text-gray-400 { color: var(--text-muted); }
|
||||
.text-blue-600 { color: #2563eb; }
|
||||
.text-green-600 { color: #16a34a; }
|
||||
.text-red-600 { color: #dc2626; }
|
||||
|
|
@ -1908,20 +1979,20 @@ dialog.modal--narrow {
|
|||
.leading-6 { line-height: 1.5rem; }
|
||||
.leading-snug { line-height: 1.375rem; }
|
||||
|
||||
/* Backgrounds */
|
||||
.bg-white { background-color: #ffffff; }
|
||||
/* Backgrounds — gray-scale classes map to shared tokens. */
|
||||
.bg-white { background-color: var(--bg); }
|
||||
.bg-transparent { background-color: transparent; }
|
||||
.bg-gray-50 { background-color: #f9fafb; }
|
||||
.bg-gray-100 { background-color: #f3f4f6; }
|
||||
.bg-gray-50 { background-color: var(--bg-secondary); }
|
||||
.bg-gray-100 { background-color: var(--bg-secondary); }
|
||||
|
||||
/* Borders */
|
||||
.border { border: 1px solid #d1d5db; }
|
||||
/* Borders — gray-scale border classes map to the shared token. */
|
||||
.border { border: 1px solid var(--border); }
|
||||
.border-0 { border: 0; }
|
||||
.border-b { border-bottom: 1px solid #d1d5db; }
|
||||
.border-t { border-top: 1px solid #d1d5db; }
|
||||
.border-gray-300 { border-color: #d1d5db; }
|
||||
.border-gray-200 { border-color: #e5e7eb; }
|
||||
.border-gray-100 { border-color: #f3f4f6; }
|
||||
.border-b { border-bottom: 1px solid var(--border); }
|
||||
.border-t { border-top: 1px solid var(--border); }
|
||||
.border-gray-300 { border-color: var(--border); }
|
||||
.border-gray-200 { border-color: var(--border); }
|
||||
.border-gray-100 { border-color: var(--border); }
|
||||
.rounded-none { border-radius: 0; }
|
||||
.rounded-sm { border-radius: 0.125rem; }
|
||||
.rounded { border-radius: 0.25rem; }
|
||||
|
|
@ -2008,14 +2079,14 @@ dialog.modal--narrow {
|
|||
.border-red-500 { border-color: #ef4444 !important; }
|
||||
|
||||
/* Hover & focus states */
|
||||
.hover\:bg-gray-50:hover { background-color: #f9fafb; }
|
||||
.hover\:bg-gray-100:hover { background-color: #f3f4f6; }
|
||||
.hover\:bg-gray-50:hover { background-color: var(--bg-hover); }
|
||||
.hover\:bg-gray-100:hover { background-color: var(--bg-hover); }
|
||||
.hover\:underline:hover { text-decoration: underline; }
|
||||
.focus\:outline-none:focus { outline: none; }
|
||||
.focus\:border-blue-400:focus { border-color: #60a5fa; }
|
||||
.focus\:ring-1:focus { box-shadow: 0 0 0 1px rgba(59, 130, 246, 0.35); }
|
||||
.focus\:ring-blue-400:focus { box-shadow: 0 0 0 2px rgba(96, 165, 250, 0.45); }
|
||||
.focus\:bg-white:focus { background-color: #ffffff; }
|
||||
.focus\:bg-white:focus { background-color: var(--bg); }
|
||||
.disabled\:pointer-events-none:disabled { pointer-events: none; }
|
||||
|
||||
/* Table helpers */
|
||||
|
|
@ -2192,7 +2263,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.17-beta · 2026-05-08 · falcon-alder-ginger</span></span>
|
||||
<span class="build-timestamp"><span style="color:red;font-weight:bold">v0.0.17-beta · 2026-05-10 · alder-cherry-reef</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;
|
||||
|
|
@ -3577,6 +3648,212 @@ https://github.com/nodeca/pako/blob/main/LICENSE
|
|||
}
|
||||
}());
|
||||
|
||||
// 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.
|
||||
//
|
||||
// Usage:
|
||||
// window.zddc.toast('Saved.', 'success');
|
||||
// window.zddc.toast('Could not load: ' + err.message, 'error');
|
||||
// window.zddc.toast('Note', 'info', { durationMs: 3000 });
|
||||
//
|
||||
// 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;
|
||||
|
||||
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 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);
|
||||
|
||||
// Click-to-dismiss. Useful for sticky errors the user wants gone.
|
||||
el.addEventListener('click', function () {
|
||||
clearTimeout(timer);
|
||||
if (el.parentNode) el.parentNode.removeChild(el);
|
||||
});
|
||||
|
||||
return el;
|
||||
}
|
||||
|
||||
window.zddc.toast = toast;
|
||||
})();
|
||||
|
||||
// shared/nav.js — lateral navigation strip across the four canonical
|
||||
// project stages (archive · working · staging · reviewing). Renders
|
||||
// only when:
|
||||
// 1. location.protocol is http: or https: (online — file:// has no
|
||||
// project structure to navigate within), AND
|
||||
// 2. a project segment can be detected from location.pathname (the
|
||||
// first path segment, when it isn't a tool HTML file).
|
||||
//
|
||||
// The strip is inserted as a sibling of <header class="app-header">
|
||||
// on DOMContentLoaded — no template changes required. Each tool just
|
||||
// needs ../shared/nav.{js,css} in its build.sh.
|
||||
//
|
||||
// Stage URLs follow the canonical workflow folders documented at
|
||||
// zddc.varasys.io/reference.html#transmittal-workflow:
|
||||
// archive → <project>/archive.html (archive tool, project-root mode)
|
||||
// working → <project>/working/ (directory listing → mdedit auto-serves)
|
||||
// staging → <project>/staging/ (directory listing → transmittal auto-serves)
|
||||
// reviewing → <project>/reviewing/ (directory listing)
|
||||
//
|
||||
// If a deployment doesn't have one of these folders the link will 404 —
|
||||
// the strip is convention-driven, not probed. Operators on non-standard
|
||||
// layouts can override by setting window.zddc.nav.disabled = true before
|
||||
// DOMContentLoaded.
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
if (!window.zddc) window.zddc = {};
|
||||
if (window.zddc.nav) return; // already loaded
|
||||
|
||||
var STAGES = [
|
||||
{ key: 'archive', label: 'Archive', target: 'archive.html' },
|
||||
{ key: 'working', label: 'Working', target: 'working/' },
|
||||
{ key: 'staging', label: 'Staging', target: 'staging/' },
|
||||
{ key: 'reviewing', label: 'Reviewing', target: 'reviewing/' },
|
||||
];
|
||||
|
||||
function projectSegment(pathname) {
|
||||
var parts = pathname.split('/').filter(Boolean);
|
||||
if (parts.length === 0) return null;
|
||||
var first = parts[0];
|
||||
// At deployment root (e.g. /archive.html?projects=A,B or
|
||||
// /index.html) the first segment is a tool HTML — no single
|
||||
// project to scope the strip to.
|
||||
if (first.indexOf('.') !== -1) return null;
|
||||
return first;
|
||||
}
|
||||
|
||||
function currentStage(pathname) {
|
||||
var parts = pathname.split('/').filter(Boolean);
|
||||
if (parts.length < 2) return null;
|
||||
var second = parts[1];
|
||||
// <project>/working/... | staging/... | reviewing/... | archive/...
|
||||
for (var i = 0; i < STAGES.length; i++) {
|
||||
if (second === STAGES[i].key) return STAGES[i].key;
|
||||
}
|
||||
// <project>/archive.html → still the archive stage
|
||||
if (second === 'archive.html') return 'archive';
|
||||
return null;
|
||||
}
|
||||
|
||||
function shouldRender() {
|
||||
if (typeof location === 'undefined') return false;
|
||||
if (location.protocol !== 'http:' && location.protocol !== 'https:') return false;
|
||||
if (window.zddc.nav && window.zddc.nav.disabled) return false;
|
||||
return projectSegment(location.pathname) !== null;
|
||||
}
|
||||
|
||||
function buildStrip(project, active) {
|
||||
var nav = document.createElement('nav');
|
||||
nav.className = 'zddc-stage-strip';
|
||||
nav.setAttribute('aria-label', 'Project stage');
|
||||
|
||||
var label = document.createElement('span');
|
||||
label.className = 'zddc-stage-strip__project';
|
||||
label.textContent = project;
|
||||
nav.appendChild(label);
|
||||
|
||||
var sep0 = document.createElement('span');
|
||||
sep0.className = 'zddc-stage-strip__divider';
|
||||
sep0.setAttribute('aria-hidden', 'true');
|
||||
sep0.textContent = '/';
|
||||
nav.appendChild(sep0);
|
||||
|
||||
for (var i = 0; i < STAGES.length; i++) {
|
||||
var s = STAGES[i];
|
||||
var a = document.createElement('a');
|
||||
a.className = 'zddc-stage';
|
||||
a.href = '/' + encodeURIComponent(project) + '/' + s.target;
|
||||
a.textContent = s.label;
|
||||
if (s.key === active) {
|
||||
a.classList.add('zddc-stage--active');
|
||||
a.setAttribute('aria-current', 'page');
|
||||
}
|
||||
nav.appendChild(a);
|
||||
|
||||
if (i < STAGES.length - 1) {
|
||||
var sep = document.createElement('span');
|
||||
sep.className = 'zddc-stage-strip__sep';
|
||||
sep.setAttribute('aria-hidden', 'true');
|
||||
sep.textContent = '·';
|
||||
nav.appendChild(sep);
|
||||
}
|
||||
}
|
||||
|
||||
return nav;
|
||||
}
|
||||
|
||||
function mount() {
|
||||
if (!shouldRender()) return;
|
||||
var header = document.querySelector('.app-header');
|
||||
if (!header) return;
|
||||
// Don't double-mount if a tool's main.js calls us a second time.
|
||||
if (header.nextElementSibling &&
|
||||
header.nextElementSibling.classList &&
|
||||
header.nextElementSibling.classList.contains('zddc-stage-strip')) {
|
||||
return;
|
||||
}
|
||||
var project = projectSegment(location.pathname);
|
||||
var active = currentStage(location.pathname);
|
||||
var strip = buildStrip(project, active);
|
||||
header.parentNode.insertBefore(strip, header.nextSibling);
|
||||
}
|
||||
|
||||
// Expose for tests + opt-out.
|
||||
window.zddc.nav = {
|
||||
mount: mount,
|
||||
// Internals visible for unit tests; do not call from tools.
|
||||
_projectSegment: projectSegment,
|
||||
_currentStage: currentStage,
|
||||
_stages: STAGES,
|
||||
// Set to true before DOMContentLoaded to suppress mounting on
|
||||
// deployments where the canonical folder layout doesn't apply.
|
||||
disabled: false,
|
||||
};
|
||||
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', mount, { once: true });
|
||||
} else {
|
||||
mount();
|
||||
}
|
||||
})();
|
||||
|
||||
/**
|
||||
* ZDDC — shared preview helpers
|
||||
*
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
# Generated by build.sh — do not edit. One <app>=<build label> per line.
|
||||
archive=v0.0.17-beta · 2026-05-08 · falcon-alder-ginger
|
||||
transmittal=v0.0.17-beta · 2026-05-08 · falcon-alder-ginger
|
||||
classifier=v0.0.17-beta · 2026-05-08 · falcon-alder-ginger
|
||||
mdedit=v0.0.17-beta · 2026-05-08 · falcon-alder-ginger
|
||||
landing=v0.0.17-beta · 2026-05-08 · falcon-alder-ginger
|
||||
form=v0.0.17-beta · 2026-05-08 · falcon-alder-ginger
|
||||
tables=v0.0.17-beta · 2026-05-08 · falcon-alder-ginger
|
||||
browse=v0.0.17-beta · 2026-05-08 · falcon-alder-ginger
|
||||
archive=v0.0.17-beta · 2026-05-10 · alder-cherry-reef
|
||||
transmittal=v0.0.17-beta · 2026-05-10 · alder-cherry-reef
|
||||
classifier=v0.0.17-beta · 2026-05-10 · alder-cherry-reef
|
||||
mdedit=v0.0.17-beta · 2026-05-10 · alder-cherry-reef
|
||||
landing=v0.0.17-beta · 2026-05-10 · alder-cherry-reef
|
||||
form=v0.0.17-beta · 2026-05-10 · alder-cherry-reef
|
||||
tables=v0.0.17-beta · 2026-05-10 · alder-cherry-reef
|
||||
browse=v0.0.17-beta · 2026-05-10 · alder-cherry-reef
|
||||
|
|
|
|||
|
|
@ -1070,7 +1070,7 @@ body.help-open .app-header {
|
|||
</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.17-alpha · 2026-05-10 00:48:10 · cc515b0-dirty</span></span>
|
||||
<span class="build-timestamp"><span style="color:red;font-weight:bold">v0.0.17-beta · 2026-05-10 · alder-cherry-reef</span></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="header-right">
|
||||
|
|
|
|||
Loading…
Reference in a new issue