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;
|
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. */
|
/* Toast CSS lives in classifier/css/base.css — only that tool uses toasts. */
|
||||||
|
|
||||||
/* ── Theme and help icon buttons ─────────────────────────────────────────── */
|
/* ── Theme and help icon buttons ─────────────────────────────────────────── */
|
||||||
|
|
@ -525,6 +530,104 @@ body.help-open .app-header {
|
||||||
color: var(--text-muted);
|
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
|
/* Archive-specific base overrides
|
||||||
Reset, tokens, and font are provided by shared/base.css */
|
Reset, tokens, and font are provided by shared/base.css */
|
||||||
|
|
||||||
|
|
@ -1771,6 +1874,17 @@ input[type="checkbox"] {
|
||||||
cursor: default;
|
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 {
|
.sortable .th-content {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
@ -2131,10 +2245,10 @@ td[data-field="trackingNumber"] {
|
||||||
</svg>
|
</svg>
|
||||||
<div class="header-title-group">
|
<div class="header-title-group">
|
||||||
<span class="app-header__title">ZDDC Archive</span>
|
<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>
|
</div>
|
||||||
<button id="addDirectoryBtn" class="btn btn-primary">Add Local Directory</button>
|
<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>
|
||||||
<div class="header-right">
|
<div class="header-right">
|
||||||
<button id="theme-btn" class="btn btn-secondary" title="Theme: auto (follows OS)" aria-label="Theme: auto (follows OS)">◐</button>
|
<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>
|
<div class="resize-handle"></div>
|
||||||
</th>
|
</th>
|
||||||
<th class="resizable" data-field="revisions">
|
<th class="resizable" data-field="revisions">
|
||||||
<div class="th-content" style="justify-content: flex-start;">
|
<div class="th-content th-content--start">
|
||||||
<input type="checkbox"
|
<input type="checkbox"
|
||||||
id="selectAllVisibleCheckbox"
|
id="selectAllVisibleCheckbox"
|
||||||
title="Select/deselect all visible files"
|
class="select-all-checkbox"
|
||||||
style="margin-right: 0.5rem;">
|
title="Select/deselect all visible files">
|
||||||
<span>Revisions</span>
|
<span>Revisions</span>
|
||||||
</div>
|
</div>
|
||||||
<input type="text"
|
<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
|
* 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
|
// Multi-select handling for folder lists
|
||||||
function setupFolderMultiSelect() {
|
function setupFolderMultiSelect() {
|
||||||
let lastSelectedGroupingIndex = -1;
|
let lastSelectedGroupingIndex = -1;
|
||||||
|
|
|
||||||
|
|
@ -335,6 +335,11 @@ a:hover {
|
||||||
font-size: 1rem;
|
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. */
|
/* Toast CSS lives in classifier/css/base.css — only that tool uses toasts. */
|
||||||
|
|
||||||
/* ── Theme and help icon buttons ─────────────────────────────────────────── */
|
/* ── Theme and help icon buttons ─────────────────────────────────────────── */
|
||||||
|
|
@ -525,6 +530,104 @@ body.help-open .app-header {
|
||||||
color: var(--text-muted);
|
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 */
|
/* browse-specific layout on top of shared/base.css */
|
||||||
|
|
||||||
html, body {
|
html, body {
|
||||||
|
|
@ -896,10 +999,10 @@ body {
|
||||||
</svg>
|
</svg>
|
||||||
<div class="header-title-group">
|
<div class="header-title-group">
|
||||||
<span class="app-header__title">ZDDC Browse</span>
|
<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>
|
</div>
|
||||||
<button id="addDirectoryBtn" class="btn btn-primary">Add Local Directory</button>
|
<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>
|
||||||
<div class="header-right">
|
<div class="header-right">
|
||||||
<button id="theme-btn" class="btn btn-secondary" title="Theme: auto (follows OS)" aria-label="Theme: auto (follows OS)">◐</button>
|
<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.
|
* ZDDC shared help panel — open/close logic.
|
||||||
* Works with all four tools regardless of their module pattern.
|
* Works with all four tools regardless of their module pattern.
|
||||||
|
|
|
||||||
|
|
@ -335,6 +335,11 @@ a:hover {
|
||||||
font-size: 1rem;
|
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. */
|
/* Toast CSS lives in classifier/css/base.css — only that tool uses toasts. */
|
||||||
|
|
||||||
/* ── Theme and help icon buttons ─────────────────────────────────────────── */
|
/* ── Theme and help icon buttons ─────────────────────────────────────────── */
|
||||||
|
|
@ -525,6 +530,104 @@ body.help-open .app-header {
|
||||||
color: var(--text-muted);
|
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
|
/* Classifier-specific base overrides
|
||||||
Reset, tokens, buttons, and font are provided by shared/base.css */
|
Reset, tokens, buttons, and font are provided by shared/base.css */
|
||||||
|
|
||||||
|
|
@ -555,41 +658,8 @@ body.help-open .app-header {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ── Toast notifications (classifier-only) ───────────────────────────────── */
|
/* Toast notifications come from shared/toast.css (.zddc-toast); the
|
||||||
/* shared/base.css intentionally omits toast CSS; only classifier uses toasts. */
|
classifier-local .toast block was promoted there. */
|
||||||
.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; }
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Classifier layout — tokens from shared/base.css */
|
/* Classifier layout — tokens from shared/base.css */
|
||||||
|
|
||||||
|
|
@ -1394,7 +1464,7 @@ body.help-open .app-header {
|
||||||
</svg>
|
</svg>
|
||||||
<div class="header-title-group">
|
<div class="header-title-group">
|
||||||
<span class="app-header__title">ZDDC Classifier</span>
|
<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>
|
</div>
|
||||||
<button id="addDirectoryBtn" class="btn btn-primary">Add Local Directory</button>
|
<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>
|
<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
|
* ZDDC — shared preview helpers
|
||||||
*
|
*
|
||||||
|
|
@ -7863,26 +8139,19 @@ https://github.com/nodeca/pako/blob/main/LICENSE
|
||||||
'use strict';
|
'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') {
|
function showToast(message, type = 'info') {
|
||||||
// Remove existing toast
|
if (window.zddc && typeof window.zddc.toast === 'function') {
|
||||||
const existing = document.querySelector('.toast');
|
window.zddc.toast(message, type);
|
||||||
if (existing) {
|
} else {
|
||||||
existing.remove();
|
// 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;
|
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. */
|
/* Toast CSS lives in classifier/css/base.css — only that tool uses toasts. */
|
||||||
|
|
||||||
/* ── Theme and help icon buttons ─────────────────────────────────────────── */
|
/* ── Theme and help icon buttons ─────────────────────────────────────────── */
|
||||||
|
|
@ -525,6 +530,104 @@ body.help-open .app-header {
|
||||||
color: var(--text-muted);
|
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 */
|
/* Landing page layout */
|
||||||
body {
|
body {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
|
|
@ -885,7 +988,7 @@ body {
|
||||||
</svg>
|
</svg>
|
||||||
<div class="header-title-group">
|
<div class="header-title-group">
|
||||||
<span class="app-header__title">ZDDC</span>
|
<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>
|
</div>
|
||||||
<div class="header-right">
|
<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.
|
* ZDDC shared help panel — open/close logic.
|
||||||
* Works with all four tools regardless of their module pattern.
|
* Works with all four tools regardless of their module pattern.
|
||||||
|
|
@ -2107,8 +2416,22 @@ body {
|
||||||
function openArchiveWith(names) {
|
function openArchiveWith(names) {
|
||||||
if (!names || names.length === 0) return;
|
if (!names || names.length === 0) return;
|
||||||
var base = location.pathname.replace(/\/[^\/]*$/, '/');
|
var base = location.pathname.replace(/\/[^\/]*$/, '/');
|
||||||
var params = ['projects=' + names.map(encodeURIComponent).join(',')];
|
|
||||||
var v = new URLSearchParams(location.search).get('v');
|
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));
|
if (v) params.push('v=' + encodeURIComponent(v));
|
||||||
navigate(base + 'archive.html?' + params.join('&'));
|
navigate(base + 'archive.html?' + params.join('&'));
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -555,6 +555,11 @@ a:hover {
|
||||||
font-size: 1rem;
|
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. */
|
/* Toast CSS lives in classifier/css/base.css — only that tool uses toasts. */
|
||||||
|
|
||||||
/* ── Theme and help icon buttons ─────────────────────────────────────────── */
|
/* ── Theme and help icon buttons ─────────────────────────────────────────── */
|
||||||
|
|
@ -745,6 +750,104 @@ body.help-open .app-header {
|
||||||
color: var(--text-muted);
|
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 */
|
/* mdedit component styles — reset and tokens from shared/base.css */
|
||||||
|
|
||||||
/* Pane resizer */
|
/* Pane resizer */
|
||||||
|
|
@ -1151,6 +1254,14 @@ body.help-open .app-header {
|
||||||
gap: 0.5rem;
|
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 */
|
/* Toast UI Editor styles */
|
||||||
#markdown-editor {
|
#markdown-editor {
|
||||||
display: block !important;
|
display: block !important;
|
||||||
|
|
@ -1792,10 +1903,10 @@ body.help-open .app-header {
|
||||||
</svg>
|
</svg>
|
||||||
<div class="header-title-group">
|
<div class="header-title-group">
|
||||||
<span class="app-header__title">ZDDC Markdown</span>
|
<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>
|
</div>
|
||||||
<button id="addDirectoryBtn" class="btn btn-primary" title="Add a local directory">Add Local Directory</button>
|
<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>
|
||||||
<div class="header-right">
|
<div class="header-right">
|
||||||
<button id="theme-btn" class="btn btn-secondary" title="Theme: auto (follows OS)" aria-label="Theme: auto (follows OS)">◐</button>
|
<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">
|
<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="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="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">
|
<div class="flex justify-between items-center w-full">
|
||||||
<span>Files</span>
|
<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
|
* ZDDC — shared preview helpers
|
||||||
*
|
*
|
||||||
|
|
@ -4441,9 +4758,9 @@ async function reloadFileFromDisk(filePath) {
|
||||||
}
|
}
|
||||||
}, 100);
|
}, 100);
|
||||||
|
|
||||||
if (editorInstance.tocContainer && window.updateToc) {
|
if (editorInstance.tocContainer) {
|
||||||
try {
|
try {
|
||||||
window.updateToc(parsed.content, editorInstance.tocContainer, editorInstance.editor, tocMaxDepth);
|
updateToc(parsed.content, editorInstance.tocContainer, editorInstance.editor, tocMaxDepth);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error updating TOC during reload:', error);
|
console.error('Error updating TOC during reload:', error);
|
||||||
}
|
}
|
||||||
|
|
@ -5837,9 +6154,9 @@ function initializeEditor(content, isMarkdown = true, filePath = '', fileName =
|
||||||
|
|
||||||
tocDepthSelector.addEventListener('change', function () {
|
tocDepthSelector.addEventListener('change', function () {
|
||||||
const depth = parseInt(this.value);
|
const depth = parseInt(this.value);
|
||||||
if (window.updateToc && editorInstance) {
|
if (editorInstance) {
|
||||||
const currentContent = editorInstance.getMarkdown();
|
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
|
// Generate initial TOC
|
||||||
if (isMarkdown && window.updateToc && tocContainer) {
|
if (isMarkdown && tocContainer) {
|
||||||
try {
|
try {
|
||||||
window.updateToc(markdownBody, tocContainer, editorInstance, tocMaxDepth);
|
updateToc(markdownBody, tocContainer, editorInstance, tocMaxDepth);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error generating TOC:', error);
|
console.error('Error generating TOC:', error);
|
||||||
}
|
}
|
||||||
|
|
||||||
const debouncedUpdateToc = debounce(() => {
|
const debouncedUpdateToc = debounce(() => {
|
||||||
const currentContent = editorInstance.getMarkdown();
|
const currentContent = editorInstance.getMarkdown();
|
||||||
window.updateToc(currentContent, tocContainer, editorInstance, tocMaxDepth);
|
updateToc(currentContent, tocContainer, editorInstance, tocMaxDepth);
|
||||||
}, 300);
|
}, 300);
|
||||||
|
|
||||||
editorInstance.on('change', () => {
|
editorInstance.on('change', () => {
|
||||||
|
|
@ -6290,10 +6607,8 @@ function setActiveTocItem(tocContainer, headerText) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Export globally
|
// Reachable at top-level scope to other concatenated mdedit JS files via the
|
||||||
window.updateToc = updateToc;
|
// build's flat-IIFE-less module pattern; no window.* exports needed.
|
||||||
window.clearActiveTocItem = clearActiveTocItem;
|
|
||||||
window.setActiveTocItem = setActiveTocItem;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Pane resizing functionality
|
* Pane resizing functionality
|
||||||
|
|
@ -6506,7 +6821,7 @@ function setupTocDepthSelector() {
|
||||||
const content = instance.editor.getMarkdown();
|
const content = instance.editor.getMarkdown();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
window.updateToc(content, instance.tocContainer, instance.editor, tocMaxDepth);
|
updateToc(content, instance.tocContainer, instance.editor, tocMaxDepth);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error updating TOC depth:', error);
|
console.error('Error updating TOC depth:', error);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -339,6 +339,11 @@ a:hover {
|
||||||
font-size: 1rem;
|
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. */
|
/* Toast CSS lives in classifier/css/base.css — only that tool uses toasts. */
|
||||||
|
|
||||||
/* ── Theme and help icon buttons ─────────────────────────────────────────── */
|
/* ── Theme and help icon buttons ─────────────────────────────────────────── */
|
||||||
|
|
@ -529,6 +534,104 @@ body.help-open .app-header {
|
||||||
color: var(--text-muted);
|
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 */
|
/* Placeholder for contenteditable elements */
|
||||||
[data-placeholder]:empty::before {
|
[data-placeholder]:empty::before {
|
||||||
content: attr(data-placeholder);
|
content: attr(data-placeholder);
|
||||||
|
|
@ -638,51 +741,15 @@ body.help-open .app-header {
|
||||||
border-bottom-color: #86efac;
|
border-bottom-color: #86efac;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Owner/Project names area and inline bg-white / bg-gray-50 utility classes */
|
/* Note: dark-mode overrides for .bg-white / .bg-gray-* / .text-gray-*
|
||||||
@media (prefers-color-scheme: dark) {
|
/ .border-gray-* and .header-names used to live here as a 17-line
|
||||||
:root:not([data-theme="light"]) .header-names {
|
block of !important rules to fight hardcoded colors in
|
||||||
background-color: var(--bg-secondary) !important;
|
transmittal/css/utilities.css. The utility classes were tokenized
|
||||||
border-color: var(--border) !important;
|
(var(--bg), var(--bg-secondary), var(--text), var(--text-muted),
|
||||||
}
|
var(--border)) so the cascade now does the right thing in both
|
||||||
:root:not([data-theme="light"]) .text-gray-700 { color: var(--text-muted) !important; }
|
themes without per-class overrides. .table-filter-input is unused
|
||||||
:root:not([data-theme="light"]) .bg-white { background-color: var(--bg) !important; }
|
(no element references it; .column-filter from shared is used
|
||||||
:root:not([data-theme="light"]) .bg-gray-50 { background-color: var(--bg-secondary) !important; }
|
instead) and was likewise dropped. */
|
||||||
: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; }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Logo row: flex layout — logo | title | logo */
|
/* Logo row: flex layout — logo | title | logo */
|
||||||
|
|
@ -1895,11 +1962,15 @@ dialog.modal--narrow {
|
||||||
.text-2xl { font-size: 1.5rem; line-height: 2rem; }
|
.text-2xl { font-size: 1.5rem; line-height: 2rem; }
|
||||||
.text-\[12px\] { font-size: 12px; line-height: 1.4; }
|
.text-\[12px\] { font-size: 12px; line-height: 1.4; }
|
||||||
.text-\[10px\] { font-size: 10px; line-height: 1.3; }
|
.text-\[10px\] { font-size: 10px; line-height: 1.3; }
|
||||||
.text-gray-900 { color: #111827; }
|
/* Gray-scale text classes are theme-encoding — they map to shared
|
||||||
.text-gray-700 { color: #374151; }
|
tokens so dark mode swaps automatically without per-class overrides.
|
||||||
.text-gray-600 { color: #4b5563; }
|
The named-color text classes (.text-blue-600/-green-600/-red-600)
|
||||||
.text-gray-500 { color: #6b7280; }
|
carry semantic meaning (link / success / danger) and stay hardcoded. */
|
||||||
.text-gray-400 { color: #9ca3af; }
|
.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-blue-600 { color: #2563eb; }
|
||||||
.text-green-600 { color: #16a34a; }
|
.text-green-600 { color: #16a34a; }
|
||||||
.text-red-600 { color: #dc2626; }
|
.text-red-600 { color: #dc2626; }
|
||||||
|
|
@ -1908,20 +1979,20 @@ dialog.modal--narrow {
|
||||||
.leading-6 { line-height: 1.5rem; }
|
.leading-6 { line-height: 1.5rem; }
|
||||||
.leading-snug { line-height: 1.375rem; }
|
.leading-snug { line-height: 1.375rem; }
|
||||||
|
|
||||||
/* Backgrounds */
|
/* Backgrounds — gray-scale classes map to shared tokens. */
|
||||||
.bg-white { background-color: #ffffff; }
|
.bg-white { background-color: var(--bg); }
|
||||||
.bg-transparent { background-color: transparent; }
|
.bg-transparent { background-color: transparent; }
|
||||||
.bg-gray-50 { background-color: #f9fafb; }
|
.bg-gray-50 { background-color: var(--bg-secondary); }
|
||||||
.bg-gray-100 { background-color: #f3f4f6; }
|
.bg-gray-100 { background-color: var(--bg-secondary); }
|
||||||
|
|
||||||
/* Borders */
|
/* Borders — gray-scale border classes map to the shared token. */
|
||||||
.border { border: 1px solid #d1d5db; }
|
.border { border: 1px solid var(--border); }
|
||||||
.border-0 { border: 0; }
|
.border-0 { border: 0; }
|
||||||
.border-b { border-bottom: 1px solid #d1d5db; }
|
.border-b { border-bottom: 1px solid var(--border); }
|
||||||
.border-t { border-top: 1px solid #d1d5db; }
|
.border-t { border-top: 1px solid var(--border); }
|
||||||
.border-gray-300 { border-color: #d1d5db; }
|
.border-gray-300 { border-color: var(--border); }
|
||||||
.border-gray-200 { border-color: #e5e7eb; }
|
.border-gray-200 { border-color: var(--border); }
|
||||||
.border-gray-100 { border-color: #f3f4f6; }
|
.border-gray-100 { border-color: var(--border); }
|
||||||
.rounded-none { border-radius: 0; }
|
.rounded-none { border-radius: 0; }
|
||||||
.rounded-sm { border-radius: 0.125rem; }
|
.rounded-sm { border-radius: 0.125rem; }
|
||||||
.rounded { border-radius: 0.25rem; }
|
.rounded { border-radius: 0.25rem; }
|
||||||
|
|
@ -2008,14 +2079,14 @@ dialog.modal--narrow {
|
||||||
.border-red-500 { border-color: #ef4444 !important; }
|
.border-red-500 { border-color: #ef4444 !important; }
|
||||||
|
|
||||||
/* Hover & focus states */
|
/* Hover & focus states */
|
||||||
.hover\:bg-gray-50:hover { background-color: #f9fafb; }
|
.hover\:bg-gray-50:hover { background-color: var(--bg-hover); }
|
||||||
.hover\:bg-gray-100:hover { background-color: #f3f4f6; }
|
.hover\:bg-gray-100:hover { background-color: var(--bg-hover); }
|
||||||
.hover\:underline:hover { text-decoration: underline; }
|
.hover\:underline:hover { text-decoration: underline; }
|
||||||
.focus\:outline-none:focus { outline: none; }
|
.focus\:outline-none:focus { outline: none; }
|
||||||
.focus\:border-blue-400:focus { border-color: #60a5fa; }
|
.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-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\: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; }
|
.disabled\:pointer-events-none:disabled { pointer-events: none; }
|
||||||
|
|
||||||
/* Table helpers */
|
/* Table helpers */
|
||||||
|
|
@ -2192,7 +2263,7 @@ dialog.modal--narrow {
|
||||||
</svg>
|
</svg>
|
||||||
<div class="header-title-group">
|
<div class="header-title-group">
|
||||||
<span class="app-header__title">ZDDC Transmittal</span>
|
<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>
|
</div>
|
||||||
<span id="no-js-notice" class="text-gray-400 text-xs italic">JavaScript not available</span>
|
<span id="no-js-notice" class="text-gray-400 text-xs italic">JavaScript not available</span>
|
||||||
<!-- Publish split-button (Transmittal-specific primary action;
|
<!-- 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
|
* ZDDC — shared preview helpers
|
||||||
*
|
*
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,9 @@
|
||||||
# Generated by build.sh — do not edit. One <app>=<build label> per line.
|
# Generated by build.sh — do not edit. One <app>=<build label> per line.
|
||||||
archive=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-08 · falcon-alder-ginger
|
transmittal=v0.0.17-beta · 2026-05-10 · alder-cherry-reef
|
||||||
classifier=v0.0.17-beta · 2026-05-08 · falcon-alder-ginger
|
classifier=v0.0.17-beta · 2026-05-10 · alder-cherry-reef
|
||||||
mdedit=v0.0.17-beta · 2026-05-08 · falcon-alder-ginger
|
mdedit=v0.0.17-beta · 2026-05-10 · alder-cherry-reef
|
||||||
landing=v0.0.17-beta · 2026-05-08 · falcon-alder-ginger
|
landing=v0.0.17-beta · 2026-05-10 · alder-cherry-reef
|
||||||
form=v0.0.17-beta · 2026-05-08 · falcon-alder-ginger
|
form=v0.0.17-beta · 2026-05-10 · alder-cherry-reef
|
||||||
tables=v0.0.17-beta · 2026-05-08 · falcon-alder-ginger
|
tables=v0.0.17-beta · 2026-05-10 · alder-cherry-reef
|
||||||
browse=v0.0.17-beta · 2026-05-08 · falcon-alder-ginger
|
browse=v0.0.17-beta · 2026-05-10 · alder-cherry-reef
|
||||||
|
|
|
||||||
|
|
@ -1070,7 +1070,7 @@ body.help-open .app-header {
|
||||||
</svg>
|
</svg>
|
||||||
<div class="header-title-group">
|
<div class="header-title-group">
|
||||||
<span class="app-header__title" id="table-title">ZDDC Table</span>
|
<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>
|
</div>
|
||||||
<div class="header-right">
|
<div class="header-right">
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue