chore(embedded): cut v0.0.17-beta
This commit is contained in:
parent
141fef88fb
commit
ba7e7a3fdd
8 changed files with 2214 additions and 855 deletions
|
|
@ -2012,6 +2012,15 @@ input[type="checkbox"] {
|
|||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* Folder-name hint after the friendly title — shown only when the
|
||||
project's .zddc declares a different `title:`. Muted so the title
|
||||
reads first; the folder name is reference info. */
|
||||
.preset-project-folder {
|
||||
color: var(--text-muted);
|
||||
font-size: 0.78rem;
|
||||
font-family: var(--font-mono);
|
||||
}
|
||||
|
||||
.preset-footer-actions {
|
||||
padding: 0.5rem 0.75rem;
|
||||
border-top: 1px solid var(--border);
|
||||
|
|
@ -2461,7 +2470,7 @@ td[data-field="trackingNumber"] {
|
|||
</svg>
|
||||
<div class="header-title-group">
|
||||
<span class="app-header__title">ZDDC Archive</span>
|
||||
<span class="build-timestamp"><span style="color:red;font-weight:bold">v0.0.17-beta · 2026-05-11 · lens-mesa-chalk</span></span>
|
||||
<span class="build-timestamp"><span style="color:red;font-weight:bold">v0.0.17-beta · 2026-05-12 · candle-mast-pearl</span></span>
|
||||
</div>
|
||||
<button id="addDirectoryBtn" class="btn btn-primary">Add Local Directory</button>
|
||||
<button id="refreshHeaderBtn" class="btn btn-secondary hidden" title="Refresh Data">⟳</button>
|
||||
|
|
@ -4476,6 +4485,276 @@ X.B(E,Y);return E}return J}())
|
|||
};
|
||||
})(typeof window !== 'undefined' ? window : globalThis);
|
||||
|
||||
// shared/zip-source.js — present the contents of a .zip as a tree of
|
||||
// File System Access API handles, so tools written against
|
||||
// FileSystemDirectoryHandle / FileSystemFileHandle (archive's scanner,
|
||||
// browse's tree) can navigate into a zip with no special-casing.
|
||||
//
|
||||
// Mirrors shared/zddc-source.js's HttpDirectoryHandle / HttpFileHandle
|
||||
// pair, but read-only and backed by a JSZip instance instead of HTTP.
|
||||
// Online tools that talk to zddc-server should use the server's
|
||||
// "<…>.zip/" virtual-directory route instead (no whole-zip download);
|
||||
// this adapter is for the offline (file://) path where the zip bytes
|
||||
// are already in hand, and for zips nested inside other zips.
|
||||
//
|
||||
// Requires window.JSZip (vendored at shared/vendor/jszip.min.js and
|
||||
// concatenated by the tool's build.sh) — referenced lazily, only from
|
||||
// fromBlob / fromFileHandle, so this module is harmless to include in
|
||||
// a build that doesn't bundle JSZip (it just won't be usable there).
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
if (!window.zddc) window.zddc = {};
|
||||
|
||||
// Minimal extension → media-type map so getFile() returns a Blob
|
||||
// with a usable `type` (iframes/img tags need it to render inline).
|
||||
var MIME = {
|
||||
pdf: 'application/pdf',
|
||||
html: 'text/html', htm: 'text/html',
|
||||
txt: 'text/plain', md: 'text/markdown', csv: 'text/csv',
|
||||
json: 'application/json', xml: 'application/xml',
|
||||
yaml: 'application/yaml', yml: 'application/yaml',
|
||||
png: 'image/png', jpg: 'image/jpeg', jpeg: 'image/jpeg',
|
||||
gif: 'image/gif', webp: 'image/webp', svg: 'image/svg+xml',
|
||||
bmp: 'image/bmp', tif: 'image/tiff', tiff: 'image/tiff',
|
||||
zip: 'application/zip',
|
||||
doc: 'application/msword',
|
||||
docx: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
||||
xls: 'application/vnd.ms-excel',
|
||||
xlsx: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
|
||||
};
|
||||
function mimeFor(name) {
|
||||
var dot = name.lastIndexOf('.');
|
||||
if (dot < 0) return '';
|
||||
return MIME[name.slice(dot + 1).toLowerCase()] || '';
|
||||
}
|
||||
|
||||
function baseName(p) {
|
||||
var s = p.replace(/\/+$/, '');
|
||||
var i = s.lastIndexOf('/');
|
||||
return i >= 0 ? s.slice(i + 1) : s;
|
||||
}
|
||||
|
||||
// Reject zip entry names that aren't safe to surface: absolute
|
||||
// paths, backslash separators, or anything that escapes via "..".
|
||||
// Returns the cleaned forward-slash path (trailing "/" preserved
|
||||
// for directory entries) or null.
|
||||
function cleanEntryName(name) {
|
||||
if (!name || name.indexOf('\\') !== -1 || name[0] === '/') return null;
|
||||
var isDir = name.endsWith('/');
|
||||
var parts = [];
|
||||
var segs = name.split('/');
|
||||
for (var i = 0; i < segs.length; i++) {
|
||||
var s = segs[i];
|
||||
if (s === '' || s === '.') continue;
|
||||
if (s === '..') return null;
|
||||
parts.push(s);
|
||||
}
|
||||
if (parts.length === 0) return null;
|
||||
return parts.join('/') + (isDir ? '/' : '');
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------
|
||||
// ZipFileHandle — FileSystemFileHandle polyfill (read-only)
|
||||
// -----------------------------------------------------------------
|
||||
|
||||
function ZipFileHandle(jszip, fullPath, size, modTime) {
|
||||
this.kind = 'file';
|
||||
this.name = baseName(fullPath);
|
||||
this._zip = jszip;
|
||||
this._path = fullPath; // path within the zip (no trailing /)
|
||||
this._size = size || 0;
|
||||
this._modTime = modTime || null;
|
||||
}
|
||||
ZipFileHandle.prototype.getFile = async function () {
|
||||
var entry = this._zip.file(this._path);
|
||||
if (!entry) {
|
||||
var err = new Error('NotFoundError: ' + this._path);
|
||||
err.name = 'NotFoundError';
|
||||
throw err;
|
||||
}
|
||||
var buf = await entry.async('arraybuffer');
|
||||
return new File([buf], this.name, {
|
||||
type: mimeFor(this.name),
|
||||
lastModified: this._modTime ? this._modTime.getTime() : Date.now()
|
||||
});
|
||||
};
|
||||
ZipFileHandle.prototype.createWritable = async function () {
|
||||
var err = new Error('Zip archives are read-only');
|
||||
err.name = 'NoModificationAllowedError';
|
||||
throw err;
|
||||
};
|
||||
ZipFileHandle.prototype.queryPermission = async function () { return 'granted'; };
|
||||
ZipFileHandle.prototype.requestPermission = async function () { return 'granted'; };
|
||||
ZipFileHandle.prototype.isZipEntry = true;
|
||||
|
||||
// -----------------------------------------------------------------
|
||||
// ZipDirectoryHandle — FileSystemDirectoryHandle polyfill (read-only)
|
||||
// -----------------------------------------------------------------
|
||||
|
||||
// jszip: the JSZip instance. prefix: "" for the zip root, else
|
||||
// "<dir>/". name: the label for this level.
|
||||
function ZipDirectoryHandle(jszip, prefix, name) {
|
||||
this.kind = 'directory';
|
||||
this._zip = jszip;
|
||||
this._prefix = prefix || '';
|
||||
this.name = name != null ? name : (this._prefix ? baseName(this._prefix) : '');
|
||||
}
|
||||
|
||||
// Walk the flat entry list once, returning a Map of immediate child
|
||||
// name → { isDir, size, modTime, fullPath }. Synthesises directory
|
||||
// children that have no explicit "<dir>/" entry.
|
||||
ZipDirectoryHandle.prototype._children = function () {
|
||||
var prefix = this._prefix;
|
||||
var seen = new Map();
|
||||
var zip = this._zip;
|
||||
Object.keys(zip.files).forEach(function (rawName) {
|
||||
var clean = cleanEntryName(rawName);
|
||||
if (clean === null) return;
|
||||
var entryIsDir = clean.endsWith('/');
|
||||
var bare = entryIsDir ? clean.slice(0, -1) : clean;
|
||||
if (prefix) {
|
||||
if (bare === prefix.slice(0, -1)) return; // the prefix dir itself
|
||||
if (bare.indexOf(prefix) !== 0) return;
|
||||
}
|
||||
var rest = prefix ? bare.slice(prefix.length) : bare;
|
||||
if (rest === '') return;
|
||||
var slash = rest.indexOf('/');
|
||||
var seg = slash === -1 ? rest : rest.slice(0, slash);
|
||||
var nested = slash !== -1;
|
||||
var existing = seen.get(seg);
|
||||
if (nested) {
|
||||
if (!existing) {
|
||||
seen.set(seg, { isDir: true, size: 0, modTime: null, fullPath: prefix + seg });
|
||||
} else {
|
||||
existing.isDir = true;
|
||||
}
|
||||
} else if (entryIsDir) {
|
||||
var entry = zip.files[rawName];
|
||||
seen.set(seg, {
|
||||
isDir: true, size: 0,
|
||||
modTime: entry && entry.date instanceof Date ? entry.date : null,
|
||||
fullPath: prefix + seg
|
||||
});
|
||||
} else {
|
||||
if (!existing || existing.isDir !== false) {
|
||||
var fent = zip.files[rawName];
|
||||
seen.set(seg, {
|
||||
isDir: false,
|
||||
size: (fent && fent._data && fent._data.uncompressedSize) || 0,
|
||||
modTime: fent && fent.date instanceof Date ? fent.date : null,
|
||||
fullPath: prefix + seg
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
return seen;
|
||||
};
|
||||
|
||||
ZipDirectoryHandle.prototype._handleFor = function (seg, info) {
|
||||
if (info.isDir) {
|
||||
return new ZipDirectoryHandle(this._zip, this._prefix + seg + '/', seg);
|
||||
}
|
||||
return new ZipFileHandle(this._zip, info.fullPath, info.size, info.modTime);
|
||||
};
|
||||
|
||||
ZipDirectoryHandle.prototype.values = function () {
|
||||
var self = this;
|
||||
return (async function* () {
|
||||
var children = self._children();
|
||||
var names = Array.from(children.keys()).sort();
|
||||
for (var i = 0; i < names.length; i++) {
|
||||
yield self._handleFor(names[i], children.get(names[i]));
|
||||
}
|
||||
})();
|
||||
};
|
||||
ZipDirectoryHandle.prototype.entries = function () {
|
||||
var iter = this.values();
|
||||
return (async function* () {
|
||||
for (;;) {
|
||||
var step = await iter.next();
|
||||
if (step.done) return;
|
||||
yield [step.value.name, step.value];
|
||||
}
|
||||
})();
|
||||
};
|
||||
ZipDirectoryHandle.prototype.keys = function () {
|
||||
var iter = this.values();
|
||||
return (async function* () {
|
||||
for (;;) {
|
||||
var step = await iter.next();
|
||||
if (step.done) return;
|
||||
yield step.value.name;
|
||||
}
|
||||
})();
|
||||
};
|
||||
ZipDirectoryHandle.prototype.getDirectoryHandle = async function (name) {
|
||||
var children = this._children();
|
||||
var info = children.get(name);
|
||||
if (!info || !info.isDir) {
|
||||
var err = new Error('NotFoundError: ' + name);
|
||||
err.name = 'NotFoundError';
|
||||
throw err;
|
||||
}
|
||||
return this._handleFor(name, info);
|
||||
};
|
||||
ZipDirectoryHandle.prototype.getFileHandle = async function (name) {
|
||||
var children = this._children();
|
||||
var info = children.get(name);
|
||||
if (!info || info.isDir) {
|
||||
var err = new Error('NotFoundError: ' + name);
|
||||
err.name = 'NotFoundError';
|
||||
throw err;
|
||||
}
|
||||
return this._handleFor(name, info);
|
||||
};
|
||||
ZipDirectoryHandle.prototype.queryPermission = async function () { return 'granted'; };
|
||||
ZipDirectoryHandle.prototype.requestPermission = async function () { return 'granted'; };
|
||||
ZipDirectoryHandle.prototype.isZipEntry = true;
|
||||
|
||||
// -----------------------------------------------------------------
|
||||
// Constructors
|
||||
// -----------------------------------------------------------------
|
||||
|
||||
function requireJSZip() {
|
||||
if (!window.JSZip) {
|
||||
throw new Error('JSZip is not available — this build does not bundle it');
|
||||
}
|
||||
return window.JSZip;
|
||||
}
|
||||
|
||||
// Build a ZipDirectoryHandle rooted at the top level of `blob`
|
||||
// (an ArrayBuffer, Blob, Uint8Array, or anything JSZip.loadAsync
|
||||
// accepts). `name` labels the root level (default: empty).
|
||||
async function fromBlob(blob, name) {
|
||||
var JSZip = requireJSZip();
|
||||
var src = blob;
|
||||
if (blob && typeof blob.arrayBuffer === 'function') {
|
||||
src = await blob.arrayBuffer();
|
||||
}
|
||||
var zip = await JSZip.loadAsync(src);
|
||||
return new ZipDirectoryHandle(zip, '', name || '');
|
||||
}
|
||||
|
||||
// Build a ZipDirectoryHandle from a FileSystemFileHandle (or this
|
||||
// adapter's own ZipFileHandle — so a zip nested inside a zip works
|
||||
// by recursion). The handle's basename labels the root level.
|
||||
async function fromFileHandle(fileHandle) {
|
||||
var f = await fileHandle.getFile();
|
||||
return fromBlob(f, fileHandle.name || (f && f.name) || '');
|
||||
}
|
||||
|
||||
window.zddc.zip = {
|
||||
ZipDirectoryHandle: ZipDirectoryHandle,
|
||||
ZipFileHandle: ZipFileHandle,
|
||||
fromBlob: fromBlob,
|
||||
fromFileHandle: fromFileHandle,
|
||||
// True for handles produced by this adapter (vs. real FS Access
|
||||
// handles or the HTTP polyfill).
|
||||
isZipHandle: function (h) { return !!(h && h.isZipEntry === true); }
|
||||
};
|
||||
})();
|
||||
|
||||
/**
|
||||
* ZDDC shared theme toggle — light / dark / auto.
|
||||
* Persists choice to localStorage under 'zddc-theme'.
|
||||
|
|
@ -4638,28 +4917,28 @@ X.B(E,Y);return E}return J}())
|
|||
}
|
||||
})();
|
||||
|
||||
// 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).
|
||||
// shared/nav.js — lateral navigation strip across the project's
|
||||
// cascade-declared stages. Mounted as a sibling of <header class="app-
|
||||
// header"> on DOMContentLoaded, hydrated from the project root's
|
||||
// directory listing.
|
||||
//
|
||||
// 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 discovery is cascade-driven (Phase 4c): fetch the project
|
||||
// root's JSON listing, filter to entries with `declared: true`
|
||||
// (server stamps these from the .zddc cascade's paths: tree), and
|
||||
// render in canonical workflow order with display_name overrides
|
||||
// honored. An operator who edits the project's .zddc paths: to add
|
||||
// a new declared child sees it in the strip; one who removes a
|
||||
// canonical entry sees the strip drop it.
|
||||
//
|
||||
// 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)
|
||||
// When the fetch fails (offline / no-server / file://), the strip
|
||||
// falls back to the hardcoded four-stage list so existing
|
||||
// deployments don't lose chrome. Hardcoded labels in this file are
|
||||
// the LAST resort — the cascade is the source of truth in normal
|
||||
// operation.
|
||||
//
|
||||
// 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
|
||||
// Stage URLs follow the slash/no-slash convention: no slash opens
|
||||
// the stage's default tool. Operators on non-standard layouts can
|
||||
// override by setting window.zddc.nav.disabled = true before
|
||||
// DOMContentLoaded.
|
||||
(function () {
|
||||
'use strict';
|
||||
|
|
@ -4667,33 +4946,37 @@ X.B(E,Y);return E}return J}())
|
|||
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/' },
|
||||
// Hardcoded fallback for offline / file:// / fetch-error contexts.
|
||||
// Server-driven discovery (FETCH_STAGES below) is the normal path.
|
||||
var FALLBACK_STAGES = [
|
||||
{ name: 'archive', label: 'Archive' },
|
||||
{ name: 'working', label: 'Working' },
|
||||
{ name: 'staging', label: 'Staging' },
|
||||
{ name: 'reviewing', label: 'Reviewing' },
|
||||
];
|
||||
|
||||
// Canonical workflow order. Stages appearing in this list are
|
||||
// rendered in this order; any extras the cascade declares are
|
||||
// appended alphabetically.
|
||||
var WORKFLOW_ORDER = ['archive', 'working', 'staging', '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) {
|
||||
function currentStage(pathname, stages) {
|
||||
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;
|
||||
for (var i = 0; i < stages.length; i++) {
|
||||
if (second.toLowerCase() === stages[i].name.toLowerCase()) {
|
||||
return stages[i].name;
|
||||
}
|
||||
}
|
||||
// <project>/archive.html → still the archive stage
|
||||
if (second === 'archive.html') return 'archive';
|
||||
return null;
|
||||
}
|
||||
|
|
@ -4705,7 +4988,53 @@ X.B(E,Y);return E}return J}())
|
|||
return projectSegment(location.pathname) !== null;
|
||||
}
|
||||
|
||||
function buildStrip(project, active) {
|
||||
function titleCase(s) {
|
||||
if (!s) return s;
|
||||
return s.charAt(0).toUpperCase() + s.slice(1);
|
||||
}
|
||||
|
||||
function sortByWorkflow(stages) {
|
||||
return stages.slice().sort(function (a, b) {
|
||||
var ia = WORKFLOW_ORDER.indexOf(a.name.toLowerCase());
|
||||
var ib = WORKFLOW_ORDER.indexOf(b.name.toLowerCase());
|
||||
if (ia >= 0 && ib >= 0) return ia - ib;
|
||||
if (ia >= 0) return -1;
|
||||
if (ib >= 0) return 1;
|
||||
return a.name.localeCompare(b.name);
|
||||
});
|
||||
}
|
||||
|
||||
// Fetch the project root listing and extract declared stage
|
||||
// entries. Returns [] on any error so callers fall back to the
|
||||
// hardcoded list. Each stage entry is {name, label} — label
|
||||
// honors the cascade's display: override when present.
|
||||
async function fetchStagesFor(project) {
|
||||
try {
|
||||
var resp = await fetch('/' + encodeURIComponent(project) + '/', {
|
||||
headers: { 'Accept': 'application/json' },
|
||||
credentials: 'same-origin',
|
||||
});
|
||||
if (!resp.ok) return [];
|
||||
var data = await resp.json();
|
||||
if (!Array.isArray(data)) return [];
|
||||
var stages = [];
|
||||
for (var i = 0; i < data.length; i++) {
|
||||
var e = data[i];
|
||||
if (!e || !e.declared || !e.is_dir) continue;
|
||||
var bare = (e.name || '').replace(/\/$/, '');
|
||||
if (!bare) continue;
|
||||
stages.push({
|
||||
name: bare,
|
||||
label: e.display_name || titleCase(bare),
|
||||
});
|
||||
}
|
||||
return sortByWorkflow(stages);
|
||||
} catch (_e) {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
function buildStrip(project, active, stages) {
|
||||
var nav = document.createElement('nav');
|
||||
nav.className = 'zddc-stage-strip';
|
||||
nav.setAttribute('aria-label', 'Project stage');
|
||||
|
|
@ -4721,19 +5050,19 @@ X.B(E,Y);return E}return J}())
|
|||
sep0.textContent = '/';
|
||||
nav.appendChild(sep0);
|
||||
|
||||
for (var i = 0; i < STAGES.length; i++) {
|
||||
var s = STAGES[i];
|
||||
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.href = '/' + encodeURIComponent(project) + '/' + s.name;
|
||||
a.textContent = s.label;
|
||||
if (s.key === active) {
|
||||
if (s.name === active) {
|
||||
a.classList.add('zddc-stage--active');
|
||||
a.setAttribute('aria-current', 'page');
|
||||
}
|
||||
nav.appendChild(a);
|
||||
|
||||
if (i < STAGES.length - 1) {
|
||||
if (i < stages.length - 1) {
|
||||
var sep = document.createElement('span');
|
||||
sep.className = 'zddc-stage-strip__sep';
|
||||
sep.setAttribute('aria-hidden', 'true');
|
||||
|
|
@ -4745,34 +5074,44 @@ X.B(E,Y);return E}return J}())
|
|||
return nav;
|
||||
}
|
||||
|
||||
function mount() {
|
||||
if (!shouldRender()) return;
|
||||
function mountWith(project, stages) {
|
||||
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.previousElementSibling &&
|
||||
header.previousElementSibling.classList &&
|
||||
header.previousElementSibling.classList.contains('zddc-stage-strip')) {
|
||||
return;
|
||||
return; // already mounted
|
||||
}
|
||||
var project = projectSegment(location.pathname);
|
||||
var active = currentStage(location.pathname);
|
||||
var strip = buildStrip(project, active);
|
||||
// Mount ABOVE the header — the strip is project-level chrome
|
||||
// (where in the project), the header is tool-level chrome (which
|
||||
// tool, theme, help). Reading order matches outer-to-inner scope.
|
||||
var active = currentStage(location.pathname, stages);
|
||||
var strip = buildStrip(project, active, stages);
|
||||
header.parentNode.insertBefore(strip, header);
|
||||
}
|
||||
|
||||
// Expose for tests + opt-out.
|
||||
async function mount() {
|
||||
if (!shouldRender()) return;
|
||||
var project = projectSegment(location.pathname);
|
||||
if (!project) return;
|
||||
|
||||
// Render the hardcoded fallback immediately so the strip
|
||||
// appears with no flicker, then upgrade to cascade-resolved
|
||||
// stages once the fetch completes.
|
||||
mountWith(project, FALLBACK_STAGES);
|
||||
|
||||
var fetched = await fetchStagesFor(project);
|
||||
if (fetched.length === 0) return; // fetch failed → keep fallback
|
||||
|
||||
// Replace the strip with the cascade-driven one. Remove the
|
||||
// existing strip first so mountWith re-mounts cleanly.
|
||||
var existing = document.querySelector('.zddc-stage-strip');
|
||||
if (existing && existing.parentNode) existing.parentNode.removeChild(existing);
|
||||
mountWith(project, fetched);
|
||||
}
|
||||
|
||||
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.
|
||||
_fallbackStages: FALLBACK_STAGES,
|
||||
disabled: false,
|
||||
};
|
||||
|
||||
|
|
@ -5469,6 +5808,15 @@ X.B(E,Y);return E}return J}())
|
|||
return !!(parsed && parsed.valid);
|
||||
}
|
||||
|
||||
// A .zip whose name (minus the .zip extension) parses as a
|
||||
// transmittal-folder name is treated as that transmittal folder —
|
||||
// its members are scanned the same as an uncompressed folder's
|
||||
// files. (A plain `archive.zip` etc. is just a file.)
|
||||
function isTransmittalFolderZip(name) {
|
||||
var parts = zddc.splitExtension(name);
|
||||
return parts.extension === 'zip' && isTransmittalFolder(parts.name);
|
||||
}
|
||||
|
||||
function groupFilesByTrackingNumber(files) {
|
||||
const groups = {};
|
||||
files.forEach(file => {
|
||||
|
|
@ -5520,6 +5868,7 @@ X.B(E,Y);return E}return J}())
|
|||
|
||||
window.app.modules.parser = {
|
||||
isTransmittalFolder,
|
||||
isTransmittalFolderZip,
|
||||
groupFilesByTrackingNumber,
|
||||
sortGroupedFiles,
|
||||
};
|
||||
|
|
@ -5712,6 +6061,29 @@ X.B(E,Y);return E}return J}())
|
|||
console.warn('Could not process directory ' + entry.name + ':', err);
|
||||
}
|
||||
} else if (entry.kind === 'file') {
|
||||
// A zipped transmittal folder (e.g.
|
||||
// "2025-05-12_DOC-001 (IFI) - Title.zip") is treated as
|
||||
// that transmittal folder: open the zip in the browser
|
||||
// and scan its members like an uncompressed folder's
|
||||
// files. The .zip stays in the recorded path so it's
|
||||
// unambiguous; the displayed name drops it.
|
||||
if (window.app.modules.parser.isTransmittalFolderZip(entry.name)) {
|
||||
const base = zddc.splitExtension(entry.name).name;
|
||||
const zipPath = currentPath + '/' + entry.name;
|
||||
try {
|
||||
const zh = await window.zddc.zip.fromFileHandle(entry);
|
||||
callbacks.onTransmittalFolder({
|
||||
name: base,
|
||||
path: zipPath,
|
||||
displayPath: getDisplayPath(zipPath),
|
||||
handle: zh
|
||||
});
|
||||
await scanLocalTransmittalFolder(zh, zipPath, 0, zipPath, callbacks);
|
||||
} catch (zipErr) {
|
||||
console.warn('Could not open zip transmittal ' + entry.name + ':', zipErr);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
// File directly in a grouping folder — assign to the Outstanding virtual transmittal.
|
||||
// actualPath records the real containing folder for grouping-folder-scoped filtering.
|
||||
try {
|
||||
|
|
@ -6007,6 +6379,27 @@ X.B(E,Y);return E}return J}())
|
|||
}
|
||||
} else {
|
||||
// It's a file
|
||||
// A zipped transmittal folder at the grouping level:
|
||||
// zddc-server serves "<…>.zip/" as a virtual directory
|
||||
// of the zip's members, so recurse into it like an
|
||||
// uncompressed transmittal folder. Members come back
|
||||
// with URLs like "<…>.zip/<member>" that the server
|
||||
// extracts on demand — no whole-zip download.
|
||||
if (transmittalPath === null && window.app.modules.parser.isTransmittalFolderZip(rawName)) {
|
||||
const base = zddc.splitExtension(rawName).name;
|
||||
const zipDirUrl = itemUrl + '/'; // itemUrl is the .zip file URL (no trailing slash)
|
||||
callbacks.onTransmittalFolder({
|
||||
name: base,
|
||||
path: logicalPath,
|
||||
displayPath: getDisplayPath(logicalPath),
|
||||
handle: null,
|
||||
url: zipDirUrl
|
||||
});
|
||||
subdirPromises.push(
|
||||
scanHttpRecursive(zipDirUrl, rootUrl, depth + 1, logicalPath, callbacks)
|
||||
);
|
||||
continue;
|
||||
}
|
||||
if (transmittalPath === null) {
|
||||
// File directly in a grouping folder — assign to Outstanding virtual transmittal.
|
||||
// actualPath records the real containing folder for grouping-folder-scoped filtering.
|
||||
|
|
@ -8455,13 +8848,24 @@ window.app.modules.filtering = {
|
|||
var selected = new Set(window.app.visibleProjects || []);
|
||||
var known = getKnownProjects().slice().sort();
|
||||
|
||||
// Show the human-friendly title from each project's .zddc
|
||||
// when present (captured during auto-detect into
|
||||
// window.app.projectTitles), falling back to the folder name.
|
||||
// The data-name attribute always carries the canonical folder
|
||||
// name so URL state stays stable regardless of label.
|
||||
var titles = window.app.projectTitles || {};
|
||||
var projectsHtml = known.map(name => {
|
||||
var checked = selected.has(name) ? ' checked' : '';
|
||||
var n = escapeHtml(name);
|
||||
var label = titles[name] || name;
|
||||
var nAttr = escapeHtml(name);
|
||||
var nLabel = escapeHtml(label);
|
||||
var hint = (label !== name)
|
||||
? ' <span class="preset-project-folder">(' + escapeHtml(name) + ')</span>'
|
||||
: '';
|
||||
return '<div class="preset-project-item">'
|
||||
+ '<label class="preset-project-label">'
|
||||
+ '<input type="checkbox" class="preset-checkbox" data-name="' + n + '"' + checked + '>'
|
||||
+ ' ' + n
|
||||
+ '<input type="checkbox" class="preset-checkbox" data-name="' + nAttr + '"' + checked + '>'
|
||||
+ ' ' + nLabel + hint
|
||||
+ '</label>'
|
||||
+ '</div>';
|
||||
}).join('');
|
||||
|
|
@ -9490,6 +9894,9 @@ window.app.modules.filtering = {
|
|||
// Fetch the server's ACL-filtered project list so we can drop any
|
||||
// listed names the user doesn't actually have access to (and so
|
||||
// the empty-projects= "include everything" mode has a list to use).
|
||||
// ProjectInfo carries an optional `title` field sourced from each
|
||||
// project's .zddc — capture it so the dropdown can show the
|
||||
// human-friendly label instead of the folder name.
|
||||
var serverNames = null;
|
||||
try {
|
||||
var resp = await fetch(baseUrl, { headers: { 'Accept': 'application/json' } });
|
||||
|
|
@ -9498,6 +9905,13 @@ window.app.modules.filtering = {
|
|||
if (Array.isArray(serverProjects) && serverProjects.length > 0
|
||||
&& serverProjects[0] && typeof serverProjects[0].name === 'string') {
|
||||
serverNames = new Set(serverProjects.map(function(p) { return p.name; }));
|
||||
var titles = {};
|
||||
serverProjects.forEach(function (p) {
|
||||
if (p && typeof p.title === 'string' && p.title) {
|
||||
titles[p.name] = p.title;
|
||||
}
|
||||
});
|
||||
window.app.projectTitles = titles;
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -1681,7 +1681,7 @@ body.help-open .app-header {
|
|||
</svg>
|
||||
<div class="header-title-group">
|
||||
<span class="app-header__title">ZDDC Classifier</span>
|
||||
<span class="build-timestamp"><span style="color:red;font-weight:bold">v0.0.17-beta · 2026-05-11 · lens-mesa-chalk</span></span>
|
||||
<span class="build-timestamp"><span style="color:red;font-weight:bold">v0.0.17-beta · 2026-05-12 · candle-mast-pearl</span></span>
|
||||
</div>
|
||||
<button id="addDirectoryBtn" class="btn btn-primary">Add Local Directory</button>
|
||||
<button id="refreshHeaderBtn" class="btn btn-secondary hidden" title="Refresh and rescan directory" aria-label="Refresh" style="font-size:1.1rem;">⟳</button>
|
||||
|
|
@ -4131,28 +4131,28 @@ X.B(E,Y);return E}return J}())
|
|||
}
|
||||
})();
|
||||
|
||||
// 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).
|
||||
// shared/nav.js — lateral navigation strip across the project's
|
||||
// cascade-declared stages. Mounted as a sibling of <header class="app-
|
||||
// header"> on DOMContentLoaded, hydrated from the project root's
|
||||
// directory listing.
|
||||
//
|
||||
// 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 discovery is cascade-driven (Phase 4c): fetch the project
|
||||
// root's JSON listing, filter to entries with `declared: true`
|
||||
// (server stamps these from the .zddc cascade's paths: tree), and
|
||||
// render in canonical workflow order with display_name overrides
|
||||
// honored. An operator who edits the project's .zddc paths: to add
|
||||
// a new declared child sees it in the strip; one who removes a
|
||||
// canonical entry sees the strip drop it.
|
||||
//
|
||||
// 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)
|
||||
// When the fetch fails (offline / no-server / file://), the strip
|
||||
// falls back to the hardcoded four-stage list so existing
|
||||
// deployments don't lose chrome. Hardcoded labels in this file are
|
||||
// the LAST resort — the cascade is the source of truth in normal
|
||||
// operation.
|
||||
//
|
||||
// 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
|
||||
// Stage URLs follow the slash/no-slash convention: no slash opens
|
||||
// the stage's default tool. Operators on non-standard layouts can
|
||||
// override by setting window.zddc.nav.disabled = true before
|
||||
// DOMContentLoaded.
|
||||
(function () {
|
||||
'use strict';
|
||||
|
|
@ -4160,33 +4160,37 @@ X.B(E,Y);return E}return J}())
|
|||
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/' },
|
||||
// Hardcoded fallback for offline / file:// / fetch-error contexts.
|
||||
// Server-driven discovery (FETCH_STAGES below) is the normal path.
|
||||
var FALLBACK_STAGES = [
|
||||
{ name: 'archive', label: 'Archive' },
|
||||
{ name: 'working', label: 'Working' },
|
||||
{ name: 'staging', label: 'Staging' },
|
||||
{ name: 'reviewing', label: 'Reviewing' },
|
||||
];
|
||||
|
||||
// Canonical workflow order. Stages appearing in this list are
|
||||
// rendered in this order; any extras the cascade declares are
|
||||
// appended alphabetically.
|
||||
var WORKFLOW_ORDER = ['archive', 'working', 'staging', '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) {
|
||||
function currentStage(pathname, stages) {
|
||||
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;
|
||||
for (var i = 0; i < stages.length; i++) {
|
||||
if (second.toLowerCase() === stages[i].name.toLowerCase()) {
|
||||
return stages[i].name;
|
||||
}
|
||||
}
|
||||
// <project>/archive.html → still the archive stage
|
||||
if (second === 'archive.html') return 'archive';
|
||||
return null;
|
||||
}
|
||||
|
|
@ -4198,7 +4202,53 @@ X.B(E,Y);return E}return J}())
|
|||
return projectSegment(location.pathname) !== null;
|
||||
}
|
||||
|
||||
function buildStrip(project, active) {
|
||||
function titleCase(s) {
|
||||
if (!s) return s;
|
||||
return s.charAt(0).toUpperCase() + s.slice(1);
|
||||
}
|
||||
|
||||
function sortByWorkflow(stages) {
|
||||
return stages.slice().sort(function (a, b) {
|
||||
var ia = WORKFLOW_ORDER.indexOf(a.name.toLowerCase());
|
||||
var ib = WORKFLOW_ORDER.indexOf(b.name.toLowerCase());
|
||||
if (ia >= 0 && ib >= 0) return ia - ib;
|
||||
if (ia >= 0) return -1;
|
||||
if (ib >= 0) return 1;
|
||||
return a.name.localeCompare(b.name);
|
||||
});
|
||||
}
|
||||
|
||||
// Fetch the project root listing and extract declared stage
|
||||
// entries. Returns [] on any error so callers fall back to the
|
||||
// hardcoded list. Each stage entry is {name, label} — label
|
||||
// honors the cascade's display: override when present.
|
||||
async function fetchStagesFor(project) {
|
||||
try {
|
||||
var resp = await fetch('/' + encodeURIComponent(project) + '/', {
|
||||
headers: { 'Accept': 'application/json' },
|
||||
credentials: 'same-origin',
|
||||
});
|
||||
if (!resp.ok) return [];
|
||||
var data = await resp.json();
|
||||
if (!Array.isArray(data)) return [];
|
||||
var stages = [];
|
||||
for (var i = 0; i < data.length; i++) {
|
||||
var e = data[i];
|
||||
if (!e || !e.declared || !e.is_dir) continue;
|
||||
var bare = (e.name || '').replace(/\/$/, '');
|
||||
if (!bare) continue;
|
||||
stages.push({
|
||||
name: bare,
|
||||
label: e.display_name || titleCase(bare),
|
||||
});
|
||||
}
|
||||
return sortByWorkflow(stages);
|
||||
} catch (_e) {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
function buildStrip(project, active, stages) {
|
||||
var nav = document.createElement('nav');
|
||||
nav.className = 'zddc-stage-strip';
|
||||
nav.setAttribute('aria-label', 'Project stage');
|
||||
|
|
@ -4214,19 +4264,19 @@ X.B(E,Y);return E}return J}())
|
|||
sep0.textContent = '/';
|
||||
nav.appendChild(sep0);
|
||||
|
||||
for (var i = 0; i < STAGES.length; i++) {
|
||||
var s = STAGES[i];
|
||||
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.href = '/' + encodeURIComponent(project) + '/' + s.name;
|
||||
a.textContent = s.label;
|
||||
if (s.key === active) {
|
||||
if (s.name === active) {
|
||||
a.classList.add('zddc-stage--active');
|
||||
a.setAttribute('aria-current', 'page');
|
||||
}
|
||||
nav.appendChild(a);
|
||||
|
||||
if (i < STAGES.length - 1) {
|
||||
if (i < stages.length - 1) {
|
||||
var sep = document.createElement('span');
|
||||
sep.className = 'zddc-stage-strip__sep';
|
||||
sep.setAttribute('aria-hidden', 'true');
|
||||
|
|
@ -4238,34 +4288,44 @@ X.B(E,Y);return E}return J}())
|
|||
return nav;
|
||||
}
|
||||
|
||||
function mount() {
|
||||
if (!shouldRender()) return;
|
||||
function mountWith(project, stages) {
|
||||
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.previousElementSibling &&
|
||||
header.previousElementSibling.classList &&
|
||||
header.previousElementSibling.classList.contains('zddc-stage-strip')) {
|
||||
return;
|
||||
return; // already mounted
|
||||
}
|
||||
var project = projectSegment(location.pathname);
|
||||
var active = currentStage(location.pathname);
|
||||
var strip = buildStrip(project, active);
|
||||
// Mount ABOVE the header — the strip is project-level chrome
|
||||
// (where in the project), the header is tool-level chrome (which
|
||||
// tool, theme, help). Reading order matches outer-to-inner scope.
|
||||
var active = currentStage(location.pathname, stages);
|
||||
var strip = buildStrip(project, active, stages);
|
||||
header.parentNode.insertBefore(strip, header);
|
||||
}
|
||||
|
||||
// Expose for tests + opt-out.
|
||||
async function mount() {
|
||||
if (!shouldRender()) return;
|
||||
var project = projectSegment(location.pathname);
|
||||
if (!project) return;
|
||||
|
||||
// Render the hardcoded fallback immediately so the strip
|
||||
// appears with no flicker, then upgrade to cascade-resolved
|
||||
// stages once the fetch completes.
|
||||
mountWith(project, FALLBACK_STAGES);
|
||||
|
||||
var fetched = await fetchStagesFor(project);
|
||||
if (fetched.length === 0) return; // fetch failed → keep fallback
|
||||
|
||||
// Replace the strip with the cascade-driven one. Remove the
|
||||
// existing strip first so mountWith re-mounts cleanly.
|
||||
var existing = document.querySelector('.zddc-stage-strip');
|
||||
if (existing && existing.parentNode) existing.parentNode.removeChild(existing);
|
||||
mountWith(project, fetched);
|
||||
}
|
||||
|
||||
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.
|
||||
_fallbackStages: FALLBACK_STAGES,
|
||||
disabled: false,
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -903,51 +903,6 @@ body {
|
|||
padding: 0 16px 64px;
|
||||
}
|
||||
|
||||
/* Standalone-tool strip. Sits above the hero on the picker view. Each
|
||||
link is a small card with the tool name (display serif) and a short
|
||||
one-line hint (sans, muted). Hover lifts the card slightly to signal
|
||||
it's clickable; the strip wraps onto multiple lines on narrow widths
|
||||
rather than scrolling horizontally. */
|
||||
.tool-strip {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.5rem;
|
||||
margin: 0 0 24px;
|
||||
padding: 0 4px;
|
||||
}
|
||||
.tool-strip__link {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 2px;
|
||||
padding: 0.5rem 0.85rem;
|
||||
background: var(--bg);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius);
|
||||
color: var(--text);
|
||||
text-decoration: none;
|
||||
transition: transform 0.12s, border-color 0.12s, box-shadow 0.12s;
|
||||
min-width: 110px;
|
||||
}
|
||||
.tool-strip__link:hover {
|
||||
border-color: var(--primary);
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.06);
|
||||
text-decoration: none;
|
||||
}
|
||||
.tool-strip__name {
|
||||
font-family: var(--font-display);
|
||||
font-weight: 600;
|
||||
font-size: 1rem;
|
||||
line-height: 1.15;
|
||||
color: var(--text);
|
||||
}
|
||||
.tool-strip__hint {
|
||||
font-size: 0.78rem;
|
||||
color: var(--text-muted);
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
/* Welcome / hero */
|
||||
.landing-hero {
|
||||
margin: 0 0 24px;
|
||||
|
|
@ -1469,7 +1424,7 @@ body {
|
|||
</svg>
|
||||
<div class="header-title-group">
|
||||
<span class="app-header__title">ZDDC</span>
|
||||
<span class="build-timestamp"><span style="color:red;font-weight:bold">v0.0.17-beta · 2026-05-11 · lens-mesa-chalk</span></span>
|
||||
<span class="build-timestamp"><span style="color:red;font-weight:bold">v0.0.17-beta · 2026-05-12 · candle-mast-pearl</span></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="header-right">
|
||||
|
|
@ -1481,48 +1436,6 @@ body {
|
|||
<main id="landingMain" class="landing-main">
|
||||
<!-- Picker mode (deployment root /). Project picker + groups. -->
|
||||
<div id="pickerView">
|
||||
<!-- Standalone-tool strip. Each link opens the latest stable
|
||||
single-file build of one ZDDC tool from the canonical
|
||||
release host (zddc.varasys.io/releases/). Useful for
|
||||
"try this tool" / offline use without a project context. -->
|
||||
<nav class="tool-strip" aria-label="ZDDC tools">
|
||||
<a class="tool-strip__link" href="https://zddc.varasys.io/releases/archive_stable.html"
|
||||
title="Browse a project archive — filter by party, status, revision">
|
||||
<span class="tool-strip__name">Archive</span>
|
||||
<span class="tool-strip__hint">Browse & filter</span>
|
||||
</a>
|
||||
<a class="tool-strip__link" href="https://zddc.varasys.io/releases/transmittal_stable.html"
|
||||
title="Compose, validate, and publish transmittals">
|
||||
<span class="tool-strip__name">Transmittal</span>
|
||||
<span class="tool-strip__hint">Issue & receive</span>
|
||||
</a>
|
||||
<a class="tool-strip__link" href="https://zddc.varasys.io/releases/classifier_stable.html"
|
||||
title="Rename incoming files into ZDDC tracking numbers">
|
||||
<span class="tool-strip__name">Classifier</span>
|
||||
<span class="tool-strip__hint">Rename incoming</span>
|
||||
</a>
|
||||
<a class="tool-strip__link" href="https://zddc.varasys.io/releases/mdedit_stable.html"
|
||||
title="Markdown editor with multi-file FS-Access workflow">
|
||||
<span class="tool-strip__name">Markdown</span>
|
||||
<span class="tool-strip__hint">Edit prose</span>
|
||||
</a>
|
||||
<a class="tool-strip__link" href="https://zddc.varasys.io/releases/browse_stable.html"
|
||||
title="Unified file tree + per-file-type preview">
|
||||
<span class="tool-strip__name">Browse</span>
|
||||
<span class="tool-strip__hint">Files & preview</span>
|
||||
</a>
|
||||
<a class="tool-strip__link" href="https://zddc.varasys.io/releases/form_stable.html"
|
||||
title="Schema-driven form renderer for *.form.yaml files">
|
||||
<span class="tool-strip__name">Form</span>
|
||||
<span class="tool-strip__hint">Structured input</span>
|
||||
</a>
|
||||
<a class="tool-strip__link" href="https://zddc.varasys.io/releases/tables_stable.html"
|
||||
title="Aggregate a directory of YAML rows into a sortable table">
|
||||
<span class="tool-strip__name">Tables</span>
|
||||
<span class="tool-strip__hint">YAML rollup</span>
|
||||
</a>
|
||||
</nav>
|
||||
|
||||
<!-- Welcome / hero -->
|
||||
<section class="landing-hero">
|
||||
<h1>Welcome to the ZDDC Archive</h1>
|
||||
|
|
@ -2384,28 +2297,28 @@ body {
|
|||
}
|
||||
})();
|
||||
|
||||
// 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).
|
||||
// shared/nav.js — lateral navigation strip across the project's
|
||||
// cascade-declared stages. Mounted as a sibling of <header class="app-
|
||||
// header"> on DOMContentLoaded, hydrated from the project root's
|
||||
// directory listing.
|
||||
//
|
||||
// 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 discovery is cascade-driven (Phase 4c): fetch the project
|
||||
// root's JSON listing, filter to entries with `declared: true`
|
||||
// (server stamps these from the .zddc cascade's paths: tree), and
|
||||
// render in canonical workflow order with display_name overrides
|
||||
// honored. An operator who edits the project's .zddc paths: to add
|
||||
// a new declared child sees it in the strip; one who removes a
|
||||
// canonical entry sees the strip drop it.
|
||||
//
|
||||
// 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)
|
||||
// When the fetch fails (offline / no-server / file://), the strip
|
||||
// falls back to the hardcoded four-stage list so existing
|
||||
// deployments don't lose chrome. Hardcoded labels in this file are
|
||||
// the LAST resort — the cascade is the source of truth in normal
|
||||
// operation.
|
||||
//
|
||||
// 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
|
||||
// Stage URLs follow the slash/no-slash convention: no slash opens
|
||||
// the stage's default tool. Operators on non-standard layouts can
|
||||
// override by setting window.zddc.nav.disabled = true before
|
||||
// DOMContentLoaded.
|
||||
(function () {
|
||||
'use strict';
|
||||
|
|
@ -2413,33 +2326,37 @@ body {
|
|||
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/' },
|
||||
// Hardcoded fallback for offline / file:// / fetch-error contexts.
|
||||
// Server-driven discovery (FETCH_STAGES below) is the normal path.
|
||||
var FALLBACK_STAGES = [
|
||||
{ name: 'archive', label: 'Archive' },
|
||||
{ name: 'working', label: 'Working' },
|
||||
{ name: 'staging', label: 'Staging' },
|
||||
{ name: 'reviewing', label: 'Reviewing' },
|
||||
];
|
||||
|
||||
// Canonical workflow order. Stages appearing in this list are
|
||||
// rendered in this order; any extras the cascade declares are
|
||||
// appended alphabetically.
|
||||
var WORKFLOW_ORDER = ['archive', 'working', 'staging', '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) {
|
||||
function currentStage(pathname, stages) {
|
||||
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;
|
||||
for (var i = 0; i < stages.length; i++) {
|
||||
if (second.toLowerCase() === stages[i].name.toLowerCase()) {
|
||||
return stages[i].name;
|
||||
}
|
||||
}
|
||||
// <project>/archive.html → still the archive stage
|
||||
if (second === 'archive.html') return 'archive';
|
||||
return null;
|
||||
}
|
||||
|
|
@ -2451,7 +2368,53 @@ body {
|
|||
return projectSegment(location.pathname) !== null;
|
||||
}
|
||||
|
||||
function buildStrip(project, active) {
|
||||
function titleCase(s) {
|
||||
if (!s) return s;
|
||||
return s.charAt(0).toUpperCase() + s.slice(1);
|
||||
}
|
||||
|
||||
function sortByWorkflow(stages) {
|
||||
return stages.slice().sort(function (a, b) {
|
||||
var ia = WORKFLOW_ORDER.indexOf(a.name.toLowerCase());
|
||||
var ib = WORKFLOW_ORDER.indexOf(b.name.toLowerCase());
|
||||
if (ia >= 0 && ib >= 0) return ia - ib;
|
||||
if (ia >= 0) return -1;
|
||||
if (ib >= 0) return 1;
|
||||
return a.name.localeCompare(b.name);
|
||||
});
|
||||
}
|
||||
|
||||
// Fetch the project root listing and extract declared stage
|
||||
// entries. Returns [] on any error so callers fall back to the
|
||||
// hardcoded list. Each stage entry is {name, label} — label
|
||||
// honors the cascade's display: override when present.
|
||||
async function fetchStagesFor(project) {
|
||||
try {
|
||||
var resp = await fetch('/' + encodeURIComponent(project) + '/', {
|
||||
headers: { 'Accept': 'application/json' },
|
||||
credentials: 'same-origin',
|
||||
});
|
||||
if (!resp.ok) return [];
|
||||
var data = await resp.json();
|
||||
if (!Array.isArray(data)) return [];
|
||||
var stages = [];
|
||||
for (var i = 0; i < data.length; i++) {
|
||||
var e = data[i];
|
||||
if (!e || !e.declared || !e.is_dir) continue;
|
||||
var bare = (e.name || '').replace(/\/$/, '');
|
||||
if (!bare) continue;
|
||||
stages.push({
|
||||
name: bare,
|
||||
label: e.display_name || titleCase(bare),
|
||||
});
|
||||
}
|
||||
return sortByWorkflow(stages);
|
||||
} catch (_e) {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
function buildStrip(project, active, stages) {
|
||||
var nav = document.createElement('nav');
|
||||
nav.className = 'zddc-stage-strip';
|
||||
nav.setAttribute('aria-label', 'Project stage');
|
||||
|
|
@ -2467,19 +2430,19 @@ body {
|
|||
sep0.textContent = '/';
|
||||
nav.appendChild(sep0);
|
||||
|
||||
for (var i = 0; i < STAGES.length; i++) {
|
||||
var s = STAGES[i];
|
||||
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.href = '/' + encodeURIComponent(project) + '/' + s.name;
|
||||
a.textContent = s.label;
|
||||
if (s.key === active) {
|
||||
if (s.name === active) {
|
||||
a.classList.add('zddc-stage--active');
|
||||
a.setAttribute('aria-current', 'page');
|
||||
}
|
||||
nav.appendChild(a);
|
||||
|
||||
if (i < STAGES.length - 1) {
|
||||
if (i < stages.length - 1) {
|
||||
var sep = document.createElement('span');
|
||||
sep.className = 'zddc-stage-strip__sep';
|
||||
sep.setAttribute('aria-hidden', 'true');
|
||||
|
|
@ -2491,34 +2454,44 @@ body {
|
|||
return nav;
|
||||
}
|
||||
|
||||
function mount() {
|
||||
if (!shouldRender()) return;
|
||||
function mountWith(project, stages) {
|
||||
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.previousElementSibling &&
|
||||
header.previousElementSibling.classList &&
|
||||
header.previousElementSibling.classList.contains('zddc-stage-strip')) {
|
||||
return;
|
||||
return; // already mounted
|
||||
}
|
||||
var project = projectSegment(location.pathname);
|
||||
var active = currentStage(location.pathname);
|
||||
var strip = buildStrip(project, active);
|
||||
// Mount ABOVE the header — the strip is project-level chrome
|
||||
// (where in the project), the header is tool-level chrome (which
|
||||
// tool, theme, help). Reading order matches outer-to-inner scope.
|
||||
var active = currentStage(location.pathname, stages);
|
||||
var strip = buildStrip(project, active, stages);
|
||||
header.parentNode.insertBefore(strip, header);
|
||||
}
|
||||
|
||||
// Expose for tests + opt-out.
|
||||
async function mount() {
|
||||
if (!shouldRender()) return;
|
||||
var project = projectSegment(location.pathname);
|
||||
if (!project) return;
|
||||
|
||||
// Render the hardcoded fallback immediately so the strip
|
||||
// appears with no flicker, then upgrade to cascade-resolved
|
||||
// stages once the fetch completes.
|
||||
mountWith(project, FALLBACK_STAGES);
|
||||
|
||||
var fetched = await fetchStagesFor(project);
|
||||
if (fetched.length === 0) return; // fetch failed → keep fallback
|
||||
|
||||
// Replace the strip with the cascade-driven one. Remove the
|
||||
// existing strip first so mountWith re-mounts cleanly.
|
||||
var existing = document.querySelector('.zddc-stage-strip');
|
||||
if (existing && existing.parentNode) existing.parentNode.removeChild(existing);
|
||||
mountWith(project, fetched);
|
||||
}
|
||||
|
||||
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.
|
||||
_fallbackStages: FALLBACK_STAGES,
|
||||
disabled: false,
|
||||
};
|
||||
|
||||
|
|
@ -3365,7 +3338,11 @@ body {
|
|||
function openSelectedMdl() {
|
||||
var party = mdlSelect.value;
|
||||
if (!party) return;
|
||||
var url = '/' + p + '/archive/' + encodeURIComponent(party) + '/mdl/';
|
||||
// No trailing slash: per the convention, the no-slash form
|
||||
// serves the tables tool with the MDL view. The slash form
|
||||
// would serve browse, which is not what the user wants when
|
||||
// they click "Open MDL".
|
||||
var url = '/' + p + '/archive/' + encodeURIComponent(party) + '/mdl';
|
||||
window.location.assign(url);
|
||||
}
|
||||
mdlOpenBtn.addEventListener('click', openSelectedMdl);
|
||||
|
|
|
|||
|
|
@ -2155,7 +2155,7 @@ body.help-open .app-header {
|
|||
</svg>
|
||||
<div class="header-title-group">
|
||||
<span class="app-header__title">ZDDC Markdown</span>
|
||||
<span class="build-timestamp"><span style="color:red;font-weight:bold">v0.0.17-beta · 2026-05-11 · lens-mesa-chalk</span></span>
|
||||
<span class="build-timestamp"><span style="color:red;font-weight:bold">v0.0.17-beta · 2026-05-12 · candle-mast-pearl</span></span>
|
||||
</div>
|
||||
<button id="addDirectoryBtn" class="btn btn-primary" title="Add a local directory">Add Local Directory</button>
|
||||
<button id="refreshHeaderBtn" class="btn btn-secondary hidden" title="Refresh directory" aria-label="Refresh">⟳</button>
|
||||
|
|
@ -4428,28 +4428,28 @@ X.B(E,Y);return E}return J}())
|
|||
}
|
||||
})();
|
||||
|
||||
// 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).
|
||||
// shared/nav.js — lateral navigation strip across the project's
|
||||
// cascade-declared stages. Mounted as a sibling of <header class="app-
|
||||
// header"> on DOMContentLoaded, hydrated from the project root's
|
||||
// directory listing.
|
||||
//
|
||||
// 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 discovery is cascade-driven (Phase 4c): fetch the project
|
||||
// root's JSON listing, filter to entries with `declared: true`
|
||||
// (server stamps these from the .zddc cascade's paths: tree), and
|
||||
// render in canonical workflow order with display_name overrides
|
||||
// honored. An operator who edits the project's .zddc paths: to add
|
||||
// a new declared child sees it in the strip; one who removes a
|
||||
// canonical entry sees the strip drop it.
|
||||
//
|
||||
// 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)
|
||||
// When the fetch fails (offline / no-server / file://), the strip
|
||||
// falls back to the hardcoded four-stage list so existing
|
||||
// deployments don't lose chrome. Hardcoded labels in this file are
|
||||
// the LAST resort — the cascade is the source of truth in normal
|
||||
// operation.
|
||||
//
|
||||
// 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
|
||||
// Stage URLs follow the slash/no-slash convention: no slash opens
|
||||
// the stage's default tool. Operators on non-standard layouts can
|
||||
// override by setting window.zddc.nav.disabled = true before
|
||||
// DOMContentLoaded.
|
||||
(function () {
|
||||
'use strict';
|
||||
|
|
@ -4457,33 +4457,37 @@ X.B(E,Y);return E}return J}())
|
|||
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/' },
|
||||
// Hardcoded fallback for offline / file:// / fetch-error contexts.
|
||||
// Server-driven discovery (FETCH_STAGES below) is the normal path.
|
||||
var FALLBACK_STAGES = [
|
||||
{ name: 'archive', label: 'Archive' },
|
||||
{ name: 'working', label: 'Working' },
|
||||
{ name: 'staging', label: 'Staging' },
|
||||
{ name: 'reviewing', label: 'Reviewing' },
|
||||
];
|
||||
|
||||
// Canonical workflow order. Stages appearing in this list are
|
||||
// rendered in this order; any extras the cascade declares are
|
||||
// appended alphabetically.
|
||||
var WORKFLOW_ORDER = ['archive', 'working', 'staging', '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) {
|
||||
function currentStage(pathname, stages) {
|
||||
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;
|
||||
for (var i = 0; i < stages.length; i++) {
|
||||
if (second.toLowerCase() === stages[i].name.toLowerCase()) {
|
||||
return stages[i].name;
|
||||
}
|
||||
}
|
||||
// <project>/archive.html → still the archive stage
|
||||
if (second === 'archive.html') return 'archive';
|
||||
return null;
|
||||
}
|
||||
|
|
@ -4495,7 +4499,53 @@ X.B(E,Y);return E}return J}())
|
|||
return projectSegment(location.pathname) !== null;
|
||||
}
|
||||
|
||||
function buildStrip(project, active) {
|
||||
function titleCase(s) {
|
||||
if (!s) return s;
|
||||
return s.charAt(0).toUpperCase() + s.slice(1);
|
||||
}
|
||||
|
||||
function sortByWorkflow(stages) {
|
||||
return stages.slice().sort(function (a, b) {
|
||||
var ia = WORKFLOW_ORDER.indexOf(a.name.toLowerCase());
|
||||
var ib = WORKFLOW_ORDER.indexOf(b.name.toLowerCase());
|
||||
if (ia >= 0 && ib >= 0) return ia - ib;
|
||||
if (ia >= 0) return -1;
|
||||
if (ib >= 0) return 1;
|
||||
return a.name.localeCompare(b.name);
|
||||
});
|
||||
}
|
||||
|
||||
// Fetch the project root listing and extract declared stage
|
||||
// entries. Returns [] on any error so callers fall back to the
|
||||
// hardcoded list. Each stage entry is {name, label} — label
|
||||
// honors the cascade's display: override when present.
|
||||
async function fetchStagesFor(project) {
|
||||
try {
|
||||
var resp = await fetch('/' + encodeURIComponent(project) + '/', {
|
||||
headers: { 'Accept': 'application/json' },
|
||||
credentials: 'same-origin',
|
||||
});
|
||||
if (!resp.ok) return [];
|
||||
var data = await resp.json();
|
||||
if (!Array.isArray(data)) return [];
|
||||
var stages = [];
|
||||
for (var i = 0; i < data.length; i++) {
|
||||
var e = data[i];
|
||||
if (!e || !e.declared || !e.is_dir) continue;
|
||||
var bare = (e.name || '').replace(/\/$/, '');
|
||||
if (!bare) continue;
|
||||
stages.push({
|
||||
name: bare,
|
||||
label: e.display_name || titleCase(bare),
|
||||
});
|
||||
}
|
||||
return sortByWorkflow(stages);
|
||||
} catch (_e) {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
function buildStrip(project, active, stages) {
|
||||
var nav = document.createElement('nav');
|
||||
nav.className = 'zddc-stage-strip';
|
||||
nav.setAttribute('aria-label', 'Project stage');
|
||||
|
|
@ -4511,19 +4561,19 @@ X.B(E,Y);return E}return J}())
|
|||
sep0.textContent = '/';
|
||||
nav.appendChild(sep0);
|
||||
|
||||
for (var i = 0; i < STAGES.length; i++) {
|
||||
var s = STAGES[i];
|
||||
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.href = '/' + encodeURIComponent(project) + '/' + s.name;
|
||||
a.textContent = s.label;
|
||||
if (s.key === active) {
|
||||
if (s.name === active) {
|
||||
a.classList.add('zddc-stage--active');
|
||||
a.setAttribute('aria-current', 'page');
|
||||
}
|
||||
nav.appendChild(a);
|
||||
|
||||
if (i < STAGES.length - 1) {
|
||||
if (i < stages.length - 1) {
|
||||
var sep = document.createElement('span');
|
||||
sep.className = 'zddc-stage-strip__sep';
|
||||
sep.setAttribute('aria-hidden', 'true');
|
||||
|
|
@ -4535,34 +4585,44 @@ X.B(E,Y);return E}return J}())
|
|||
return nav;
|
||||
}
|
||||
|
||||
function mount() {
|
||||
if (!shouldRender()) return;
|
||||
function mountWith(project, stages) {
|
||||
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.previousElementSibling &&
|
||||
header.previousElementSibling.classList &&
|
||||
header.previousElementSibling.classList.contains('zddc-stage-strip')) {
|
||||
return;
|
||||
return; // already mounted
|
||||
}
|
||||
var project = projectSegment(location.pathname);
|
||||
var active = currentStage(location.pathname);
|
||||
var strip = buildStrip(project, active);
|
||||
// Mount ABOVE the header — the strip is project-level chrome
|
||||
// (where in the project), the header is tool-level chrome (which
|
||||
// tool, theme, help). Reading order matches outer-to-inner scope.
|
||||
var active = currentStage(location.pathname, stages);
|
||||
var strip = buildStrip(project, active, stages);
|
||||
header.parentNode.insertBefore(strip, header);
|
||||
}
|
||||
|
||||
// Expose for tests + opt-out.
|
||||
async function mount() {
|
||||
if (!shouldRender()) return;
|
||||
var project = projectSegment(location.pathname);
|
||||
if (!project) return;
|
||||
|
||||
// Render the hardcoded fallback immediately so the strip
|
||||
// appears with no flicker, then upgrade to cascade-resolved
|
||||
// stages once the fetch completes.
|
||||
mountWith(project, FALLBACK_STAGES);
|
||||
|
||||
var fetched = await fetchStagesFor(project);
|
||||
if (fetched.length === 0) return; // fetch failed → keep fallback
|
||||
|
||||
// Replace the strip with the cascade-driven one. Remove the
|
||||
// existing strip first so mountWith re-mounts cleanly.
|
||||
var existing = document.querySelector('.zddc-stage-strip');
|
||||
if (existing && existing.parentNode) existing.parentNode.removeChild(existing);
|
||||
mountWith(project, fetched);
|
||||
}
|
||||
|
||||
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.
|
||||
_fallbackStages: FALLBACK_STAGES,
|
||||
disabled: false,
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -2523,7 +2523,7 @@ dialog.modal--narrow {
|
|||
</svg>
|
||||
<div class="header-title-group">
|
||||
<span class="app-header__title">ZDDC Transmittal</span>
|
||||
<span class="build-timestamp"><span style="color:red;font-weight:bold">v0.0.17-beta · 2026-05-11 · lens-mesa-chalk</span></span>
|
||||
<span class="build-timestamp"><span style="color:red;font-weight:bold">v0.0.17-beta · 2026-05-12 · candle-mast-pearl</span></span>
|
||||
</div>
|
||||
<span id="no-js-notice" class="text-gray-400 text-xs italic">JavaScript not available</span>
|
||||
<!-- Publish split-button (Transmittal-specific primary action;
|
||||
|
|
@ -5187,28 +5187,28 @@ X.B(E,Y);return E}return J}())
|
|||
}
|
||||
})();
|
||||
|
||||
// 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).
|
||||
// shared/nav.js — lateral navigation strip across the project's
|
||||
// cascade-declared stages. Mounted as a sibling of <header class="app-
|
||||
// header"> on DOMContentLoaded, hydrated from the project root's
|
||||
// directory listing.
|
||||
//
|
||||
// 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 discovery is cascade-driven (Phase 4c): fetch the project
|
||||
// root's JSON listing, filter to entries with `declared: true`
|
||||
// (server stamps these from the .zddc cascade's paths: tree), and
|
||||
// render in canonical workflow order with display_name overrides
|
||||
// honored. An operator who edits the project's .zddc paths: to add
|
||||
// a new declared child sees it in the strip; one who removes a
|
||||
// canonical entry sees the strip drop it.
|
||||
//
|
||||
// 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)
|
||||
// When the fetch fails (offline / no-server / file://), the strip
|
||||
// falls back to the hardcoded four-stage list so existing
|
||||
// deployments don't lose chrome. Hardcoded labels in this file are
|
||||
// the LAST resort — the cascade is the source of truth in normal
|
||||
// operation.
|
||||
//
|
||||
// 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
|
||||
// Stage URLs follow the slash/no-slash convention: no slash opens
|
||||
// the stage's default tool. Operators on non-standard layouts can
|
||||
// override by setting window.zddc.nav.disabled = true before
|
||||
// DOMContentLoaded.
|
||||
(function () {
|
||||
'use strict';
|
||||
|
|
@ -5216,33 +5216,37 @@ X.B(E,Y);return E}return J}())
|
|||
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/' },
|
||||
// Hardcoded fallback for offline / file:// / fetch-error contexts.
|
||||
// Server-driven discovery (FETCH_STAGES below) is the normal path.
|
||||
var FALLBACK_STAGES = [
|
||||
{ name: 'archive', label: 'Archive' },
|
||||
{ name: 'working', label: 'Working' },
|
||||
{ name: 'staging', label: 'Staging' },
|
||||
{ name: 'reviewing', label: 'Reviewing' },
|
||||
];
|
||||
|
||||
// Canonical workflow order. Stages appearing in this list are
|
||||
// rendered in this order; any extras the cascade declares are
|
||||
// appended alphabetically.
|
||||
var WORKFLOW_ORDER = ['archive', 'working', 'staging', '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) {
|
||||
function currentStage(pathname, stages) {
|
||||
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;
|
||||
for (var i = 0; i < stages.length; i++) {
|
||||
if (second.toLowerCase() === stages[i].name.toLowerCase()) {
|
||||
return stages[i].name;
|
||||
}
|
||||
}
|
||||
// <project>/archive.html → still the archive stage
|
||||
if (second === 'archive.html') return 'archive';
|
||||
return null;
|
||||
}
|
||||
|
|
@ -5254,7 +5258,53 @@ X.B(E,Y);return E}return J}())
|
|||
return projectSegment(location.pathname) !== null;
|
||||
}
|
||||
|
||||
function buildStrip(project, active) {
|
||||
function titleCase(s) {
|
||||
if (!s) return s;
|
||||
return s.charAt(0).toUpperCase() + s.slice(1);
|
||||
}
|
||||
|
||||
function sortByWorkflow(stages) {
|
||||
return stages.slice().sort(function (a, b) {
|
||||
var ia = WORKFLOW_ORDER.indexOf(a.name.toLowerCase());
|
||||
var ib = WORKFLOW_ORDER.indexOf(b.name.toLowerCase());
|
||||
if (ia >= 0 && ib >= 0) return ia - ib;
|
||||
if (ia >= 0) return -1;
|
||||
if (ib >= 0) return 1;
|
||||
return a.name.localeCompare(b.name);
|
||||
});
|
||||
}
|
||||
|
||||
// Fetch the project root listing and extract declared stage
|
||||
// entries. Returns [] on any error so callers fall back to the
|
||||
// hardcoded list. Each stage entry is {name, label} — label
|
||||
// honors the cascade's display: override when present.
|
||||
async function fetchStagesFor(project) {
|
||||
try {
|
||||
var resp = await fetch('/' + encodeURIComponent(project) + '/', {
|
||||
headers: { 'Accept': 'application/json' },
|
||||
credentials: 'same-origin',
|
||||
});
|
||||
if (!resp.ok) return [];
|
||||
var data = await resp.json();
|
||||
if (!Array.isArray(data)) return [];
|
||||
var stages = [];
|
||||
for (var i = 0; i < data.length; i++) {
|
||||
var e = data[i];
|
||||
if (!e || !e.declared || !e.is_dir) continue;
|
||||
var bare = (e.name || '').replace(/\/$/, '');
|
||||
if (!bare) continue;
|
||||
stages.push({
|
||||
name: bare,
|
||||
label: e.display_name || titleCase(bare),
|
||||
});
|
||||
}
|
||||
return sortByWorkflow(stages);
|
||||
} catch (_e) {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
function buildStrip(project, active, stages) {
|
||||
var nav = document.createElement('nav');
|
||||
nav.className = 'zddc-stage-strip';
|
||||
nav.setAttribute('aria-label', 'Project stage');
|
||||
|
|
@ -5270,19 +5320,19 @@ X.B(E,Y);return E}return J}())
|
|||
sep0.textContent = '/';
|
||||
nav.appendChild(sep0);
|
||||
|
||||
for (var i = 0; i < STAGES.length; i++) {
|
||||
var s = STAGES[i];
|
||||
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.href = '/' + encodeURIComponent(project) + '/' + s.name;
|
||||
a.textContent = s.label;
|
||||
if (s.key === active) {
|
||||
if (s.name === active) {
|
||||
a.classList.add('zddc-stage--active');
|
||||
a.setAttribute('aria-current', 'page');
|
||||
}
|
||||
nav.appendChild(a);
|
||||
|
||||
if (i < STAGES.length - 1) {
|
||||
if (i < stages.length - 1) {
|
||||
var sep = document.createElement('span');
|
||||
sep.className = 'zddc-stage-strip__sep';
|
||||
sep.setAttribute('aria-hidden', 'true');
|
||||
|
|
@ -5294,34 +5344,44 @@ X.B(E,Y);return E}return J}())
|
|||
return nav;
|
||||
}
|
||||
|
||||
function mount() {
|
||||
if (!shouldRender()) return;
|
||||
function mountWith(project, stages) {
|
||||
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.previousElementSibling &&
|
||||
header.previousElementSibling.classList &&
|
||||
header.previousElementSibling.classList.contains('zddc-stage-strip')) {
|
||||
return;
|
||||
return; // already mounted
|
||||
}
|
||||
var project = projectSegment(location.pathname);
|
||||
var active = currentStage(location.pathname);
|
||||
var strip = buildStrip(project, active);
|
||||
// Mount ABOVE the header — the strip is project-level chrome
|
||||
// (where in the project), the header is tool-level chrome (which
|
||||
// tool, theme, help). Reading order matches outer-to-inner scope.
|
||||
var active = currentStage(location.pathname, stages);
|
||||
var strip = buildStrip(project, active, stages);
|
||||
header.parentNode.insertBefore(strip, header);
|
||||
}
|
||||
|
||||
// Expose for tests + opt-out.
|
||||
async function mount() {
|
||||
if (!shouldRender()) return;
|
||||
var project = projectSegment(location.pathname);
|
||||
if (!project) return;
|
||||
|
||||
// Render the hardcoded fallback immediately so the strip
|
||||
// appears with no flicker, then upgrade to cascade-resolved
|
||||
// stages once the fetch completes.
|
||||
mountWith(project, FALLBACK_STAGES);
|
||||
|
||||
var fetched = await fetchStagesFor(project);
|
||||
if (fetched.length === 0) return; // fetch failed → keep fallback
|
||||
|
||||
// Replace the strip with the cascade-driven one. Remove the
|
||||
// existing strip first so mountWith re-mounts cleanly.
|
||||
var existing = document.querySelector('.zddc-stage-strip');
|
||||
if (existing && existing.parentNode) existing.parentNode.removeChild(existing);
|
||||
mountWith(project, fetched);
|
||||
}
|
||||
|
||||
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.
|
||||
_fallbackStages: FALLBACK_STAGES,
|
||||
disabled: false,
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
# Generated by build.sh — do not edit. One <app>=<build label> per line.
|
||||
archive=v0.0.17-beta · 2026-05-11 · lens-mesa-chalk
|
||||
transmittal=v0.0.17-beta · 2026-05-11 · lens-mesa-chalk
|
||||
classifier=v0.0.17-beta · 2026-05-11 · lens-mesa-chalk
|
||||
mdedit=v0.0.17-beta · 2026-05-11 · lens-mesa-chalk
|
||||
landing=v0.0.17-beta · 2026-05-11 · lens-mesa-chalk
|
||||
form=v0.0.17-beta · 2026-05-11 · lens-mesa-chalk
|
||||
tables=v0.0.17-beta · 2026-05-11 · lens-mesa-chalk
|
||||
browse=v0.0.17-beta · 2026-05-11 · lens-mesa-chalk
|
||||
archive=v0.0.17-beta · 2026-05-12 · candle-mast-pearl
|
||||
transmittal=v0.0.17-beta · 2026-05-12 · candle-mast-pearl
|
||||
classifier=v0.0.17-beta · 2026-05-12 · candle-mast-pearl
|
||||
mdedit=v0.0.17-beta · 2026-05-12 · candle-mast-pearl
|
||||
landing=v0.0.17-beta · 2026-05-12 · candle-mast-pearl
|
||||
form=v0.0.17-beta · 2026-05-12 · candle-mast-pearl
|
||||
tables=v0.0.17-beta · 2026-05-12 · candle-mast-pearl
|
||||
browse=v0.0.17-beta · 2026-05-12 · candle-mast-pearl
|
||||
|
|
|
|||
|
|
@ -1300,7 +1300,7 @@ body.help-open .app-header {
|
|||
</svg>
|
||||
<div class="header-title-group">
|
||||
<span class="app-header__title" id="table-title">ZDDC Table</span>
|
||||
<span class="build-timestamp"><span style="color:red;font-weight:bold">v0.0.17-alpha · 2026-05-12 15:29:24 · 54dff4d-dirty</span></span>
|
||||
<span class="build-timestamp"><span style="color:red;font-weight:bold">v0.0.17-beta · 2026-05-12 · candle-mast-pearl</span></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="header-right">
|
||||
|
|
|
|||
Loading…
Reference in a new issue