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;
|
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 {
|
.preset-footer-actions {
|
||||||
padding: 0.5rem 0.75rem;
|
padding: 0.5rem 0.75rem;
|
||||||
border-top: 1px solid var(--border);
|
border-top: 1px solid var(--border);
|
||||||
|
|
@ -2461,7 +2470,7 @@ 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-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>
|
||||||
<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">⟳</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);
|
})(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.
|
* ZDDC shared theme toggle — light / dark / auto.
|
||||||
* Persists choice to localStorage under 'zddc-theme'.
|
* 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
|
// shared/nav.js — lateral navigation strip across the project's
|
||||||
// project stages (archive · working · staging · reviewing). Renders
|
// cascade-declared stages. Mounted as a sibling of <header class="app-
|
||||||
// only when:
|
// header"> on DOMContentLoaded, hydrated from the project root's
|
||||||
// 1. location.protocol is http: or https: (online — file:// has no
|
// directory listing.
|
||||||
// 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">
|
// Stage discovery is cascade-driven (Phase 4c): fetch the project
|
||||||
// on DOMContentLoaded — no template changes required. Each tool just
|
// root's JSON listing, filter to entries with `declared: true`
|
||||||
// needs ../shared/nav.{js,css} in its build.sh.
|
// (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
|
// When the fetch fails (offline / no-server / file://), the strip
|
||||||
// zddc.varasys.io/reference.html#transmittal-workflow:
|
// falls back to the hardcoded four-stage list so existing
|
||||||
// archive → <project>/archive.html (archive tool, project-root mode)
|
// deployments don't lose chrome. Hardcoded labels in this file are
|
||||||
// working → <project>/working/ (directory listing → mdedit auto-serves)
|
// the LAST resort — the cascade is the source of truth in normal
|
||||||
// staging → <project>/staging/ (directory listing → transmittal auto-serves)
|
// operation.
|
||||||
// reviewing → <project>/reviewing/ (directory listing)
|
|
||||||
//
|
//
|
||||||
// If a deployment doesn't have one of these folders the link will 404 —
|
// Stage URLs follow the slash/no-slash convention: no slash opens
|
||||||
// the strip is convention-driven, not probed. Operators on non-standard
|
// the stage's default tool. Operators on non-standard layouts can
|
||||||
// layouts can override by setting window.zddc.nav.disabled = true before
|
// override by setting window.zddc.nav.disabled = true before
|
||||||
// DOMContentLoaded.
|
// DOMContentLoaded.
|
||||||
(function () {
|
(function () {
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
@ -4667,33 +4946,37 @@ X.B(E,Y);return E}return J}())
|
||||||
if (!window.zddc) window.zddc = {};
|
if (!window.zddc) window.zddc = {};
|
||||||
if (window.zddc.nav) return; // already loaded
|
if (window.zddc.nav) return; // already loaded
|
||||||
|
|
||||||
var STAGES = [
|
// Hardcoded fallback for offline / file:// / fetch-error contexts.
|
||||||
{ key: 'archive', label: 'Archive', target: 'archive.html' },
|
// Server-driven discovery (FETCH_STAGES below) is the normal path.
|
||||||
{ key: 'working', label: 'Working', target: 'working/' },
|
var FALLBACK_STAGES = [
|
||||||
{ key: 'staging', label: 'Staging', target: 'staging/' },
|
{ name: 'archive', label: 'Archive' },
|
||||||
{ key: 'reviewing', label: 'Reviewing', target: 'reviewing/' },
|
{ 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) {
|
function projectSegment(pathname) {
|
||||||
var parts = pathname.split('/').filter(Boolean);
|
var parts = pathname.split('/').filter(Boolean);
|
||||||
if (parts.length === 0) return null;
|
if (parts.length === 0) return null;
|
||||||
var first = parts[0];
|
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;
|
if (first.indexOf('.') !== -1) return null;
|
||||||
return first;
|
return first;
|
||||||
}
|
}
|
||||||
|
|
||||||
function currentStage(pathname) {
|
function currentStage(pathname, stages) {
|
||||||
var parts = pathname.split('/').filter(Boolean);
|
var parts = pathname.split('/').filter(Boolean);
|
||||||
if (parts.length < 2) return null;
|
if (parts.length < 2) return null;
|
||||||
var second = parts[1];
|
var second = parts[1];
|
||||||
// <project>/working/... | staging/... | reviewing/... | archive/...
|
for (var i = 0; i < stages.length; i++) {
|
||||||
for (var i = 0; i < STAGES.length; i++) {
|
if (second.toLowerCase() === stages[i].name.toLowerCase()) {
|
||||||
if (second === STAGES[i].key) return STAGES[i].key;
|
return stages[i].name;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
// <project>/archive.html → still the archive stage
|
|
||||||
if (second === 'archive.html') return 'archive';
|
if (second === 'archive.html') return 'archive';
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
@ -4705,7 +4988,53 @@ X.B(E,Y);return E}return J}())
|
||||||
return projectSegment(location.pathname) !== null;
|
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');
|
var nav = document.createElement('nav');
|
||||||
nav.className = 'zddc-stage-strip';
|
nav.className = 'zddc-stage-strip';
|
||||||
nav.setAttribute('aria-label', 'Project stage');
|
nav.setAttribute('aria-label', 'Project stage');
|
||||||
|
|
@ -4721,19 +5050,19 @@ X.B(E,Y);return E}return J}())
|
||||||
sep0.textContent = '/';
|
sep0.textContent = '/';
|
||||||
nav.appendChild(sep0);
|
nav.appendChild(sep0);
|
||||||
|
|
||||||
for (var i = 0; i < STAGES.length; i++) {
|
for (var i = 0; i < stages.length; i++) {
|
||||||
var s = STAGES[i];
|
var s = stages[i];
|
||||||
var a = document.createElement('a');
|
var a = document.createElement('a');
|
||||||
a.className = 'zddc-stage';
|
a.className = 'zddc-stage';
|
||||||
a.href = '/' + encodeURIComponent(project) + '/' + s.target;
|
a.href = '/' + encodeURIComponent(project) + '/' + s.name;
|
||||||
a.textContent = s.label;
|
a.textContent = s.label;
|
||||||
if (s.key === active) {
|
if (s.name === active) {
|
||||||
a.classList.add('zddc-stage--active');
|
a.classList.add('zddc-stage--active');
|
||||||
a.setAttribute('aria-current', 'page');
|
a.setAttribute('aria-current', 'page');
|
||||||
}
|
}
|
||||||
nav.appendChild(a);
|
nav.appendChild(a);
|
||||||
|
|
||||||
if (i < STAGES.length - 1) {
|
if (i < stages.length - 1) {
|
||||||
var sep = document.createElement('span');
|
var sep = document.createElement('span');
|
||||||
sep.className = 'zddc-stage-strip__sep';
|
sep.className = 'zddc-stage-strip__sep';
|
||||||
sep.setAttribute('aria-hidden', 'true');
|
sep.setAttribute('aria-hidden', 'true');
|
||||||
|
|
@ -4745,34 +5074,44 @@ X.B(E,Y);return E}return J}())
|
||||||
return nav;
|
return nav;
|
||||||
}
|
}
|
||||||
|
|
||||||
function mount() {
|
function mountWith(project, stages) {
|
||||||
if (!shouldRender()) return;
|
|
||||||
var header = document.querySelector('.app-header');
|
var header = document.querySelector('.app-header');
|
||||||
if (!header) return;
|
if (!header) return;
|
||||||
// Don't double-mount if a tool's main.js calls us a second time.
|
|
||||||
if (header.previousElementSibling &&
|
if (header.previousElementSibling &&
|
||||||
header.previousElementSibling.classList &&
|
header.previousElementSibling.classList &&
|
||||||
header.previousElementSibling.classList.contains('zddc-stage-strip')) {
|
header.previousElementSibling.classList.contains('zddc-stage-strip')) {
|
||||||
return;
|
return; // already mounted
|
||||||
}
|
}
|
||||||
var project = projectSegment(location.pathname);
|
var active = currentStage(location.pathname, stages);
|
||||||
var active = currentStage(location.pathname);
|
var strip = buildStrip(project, active, stages);
|
||||||
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.
|
|
||||||
header.parentNode.insertBefore(strip, header);
|
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 = {
|
window.zddc.nav = {
|
||||||
mount: mount,
|
mount: mount,
|
||||||
// Internals visible for unit tests; do not call from tools.
|
|
||||||
_projectSegment: projectSegment,
|
_projectSegment: projectSegment,
|
||||||
_currentStage: currentStage,
|
_currentStage: currentStage,
|
||||||
_stages: STAGES,
|
_fallbackStages: FALLBACK_STAGES,
|
||||||
// Set to true before DOMContentLoaded to suppress mounting on
|
|
||||||
// deployments where the canonical folder layout doesn't apply.
|
|
||||||
disabled: false,
|
disabled: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -5469,6 +5808,15 @@ X.B(E,Y);return E}return J}())
|
||||||
return !!(parsed && parsed.valid);
|
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) {
|
function groupFilesByTrackingNumber(files) {
|
||||||
const groups = {};
|
const groups = {};
|
||||||
files.forEach(file => {
|
files.forEach(file => {
|
||||||
|
|
@ -5520,6 +5868,7 @@ X.B(E,Y);return E}return J}())
|
||||||
|
|
||||||
window.app.modules.parser = {
|
window.app.modules.parser = {
|
||||||
isTransmittalFolder,
|
isTransmittalFolder,
|
||||||
|
isTransmittalFolderZip,
|
||||||
groupFilesByTrackingNumber,
|
groupFilesByTrackingNumber,
|
||||||
sortGroupedFiles,
|
sortGroupedFiles,
|
||||||
};
|
};
|
||||||
|
|
@ -5712,6 +6061,29 @@ X.B(E,Y);return E}return J}())
|
||||||
console.warn('Could not process directory ' + entry.name + ':', err);
|
console.warn('Could not process directory ' + entry.name + ':', err);
|
||||||
}
|
}
|
||||||
} else if (entry.kind === 'file') {
|
} 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.
|
// File directly in a grouping folder — assign to the Outstanding virtual transmittal.
|
||||||
// actualPath records the real containing folder for grouping-folder-scoped filtering.
|
// actualPath records the real containing folder for grouping-folder-scoped filtering.
|
||||||
try {
|
try {
|
||||||
|
|
@ -6007,6 +6379,27 @@ X.B(E,Y);return E}return J}())
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// It's a file
|
// 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) {
|
if (transmittalPath === null) {
|
||||||
// File directly in a grouping folder — assign to Outstanding virtual transmittal.
|
// File directly in a grouping folder — assign to Outstanding virtual transmittal.
|
||||||
// actualPath records the real containing folder for grouping-folder-scoped filtering.
|
// 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 selected = new Set(window.app.visibleProjects || []);
|
||||||
var known = getKnownProjects().slice().sort();
|
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 projectsHtml = known.map(name => {
|
||||||
var checked = selected.has(name) ? ' checked' : '';
|
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">'
|
return '<div class="preset-project-item">'
|
||||||
+ '<label class="preset-project-label">'
|
+ '<label class="preset-project-label">'
|
||||||
+ '<input type="checkbox" class="preset-checkbox" data-name="' + n + '"' + checked + '>'
|
+ '<input type="checkbox" class="preset-checkbox" data-name="' + nAttr + '"' + checked + '>'
|
||||||
+ ' ' + n
|
+ ' ' + nLabel + hint
|
||||||
+ '</label>'
|
+ '</label>'
|
||||||
+ '</div>';
|
+ '</div>';
|
||||||
}).join('');
|
}).join('');
|
||||||
|
|
@ -9490,6 +9894,9 @@ window.app.modules.filtering = {
|
||||||
// Fetch the server's ACL-filtered project list so we can drop any
|
// 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
|
// listed names the user doesn't actually have access to (and so
|
||||||
// the empty-projects= "include everything" mode has a list to use).
|
// 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;
|
var serverNames = null;
|
||||||
try {
|
try {
|
||||||
var resp = await fetch(baseUrl, { headers: { 'Accept': 'application/json' } });
|
var resp = await fetch(baseUrl, { headers: { 'Accept': 'application/json' } });
|
||||||
|
|
@ -9498,6 +9905,13 @@ window.app.modules.filtering = {
|
||||||
if (Array.isArray(serverProjects) && serverProjects.length > 0
|
if (Array.isArray(serverProjects) && serverProjects.length > 0
|
||||||
&& serverProjects[0] && typeof serverProjects[0].name === 'string') {
|
&& serverProjects[0] && typeof serverProjects[0].name === 'string') {
|
||||||
serverNames = new Set(serverProjects.map(function(p) { return p.name; }));
|
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) {
|
} catch (e) {
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load diff
|
|
@ -1681,7 +1681,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-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>
|
||||||
<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>
|
||||||
|
|
@ -4131,28 +4131,28 @@ X.B(E,Y);return E}return J}())
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
|
|
||||||
// shared/nav.js — lateral navigation strip across the four canonical
|
// shared/nav.js — lateral navigation strip across the project's
|
||||||
// project stages (archive · working · staging · reviewing). Renders
|
// cascade-declared stages. Mounted as a sibling of <header class="app-
|
||||||
// only when:
|
// header"> on DOMContentLoaded, hydrated from the project root's
|
||||||
// 1. location.protocol is http: or https: (online — file:// has no
|
// directory listing.
|
||||||
// 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">
|
// Stage discovery is cascade-driven (Phase 4c): fetch the project
|
||||||
// on DOMContentLoaded — no template changes required. Each tool just
|
// root's JSON listing, filter to entries with `declared: true`
|
||||||
// needs ../shared/nav.{js,css} in its build.sh.
|
// (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
|
// When the fetch fails (offline / no-server / file://), the strip
|
||||||
// zddc.varasys.io/reference.html#transmittal-workflow:
|
// falls back to the hardcoded four-stage list so existing
|
||||||
// archive → <project>/archive.html (archive tool, project-root mode)
|
// deployments don't lose chrome. Hardcoded labels in this file are
|
||||||
// working → <project>/working/ (directory listing → mdedit auto-serves)
|
// the LAST resort — the cascade is the source of truth in normal
|
||||||
// staging → <project>/staging/ (directory listing → transmittal auto-serves)
|
// operation.
|
||||||
// reviewing → <project>/reviewing/ (directory listing)
|
|
||||||
//
|
//
|
||||||
// If a deployment doesn't have one of these folders the link will 404 —
|
// Stage URLs follow the slash/no-slash convention: no slash opens
|
||||||
// the strip is convention-driven, not probed. Operators on non-standard
|
// the stage's default tool. Operators on non-standard layouts can
|
||||||
// layouts can override by setting window.zddc.nav.disabled = true before
|
// override by setting window.zddc.nav.disabled = true before
|
||||||
// DOMContentLoaded.
|
// DOMContentLoaded.
|
||||||
(function () {
|
(function () {
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
@ -4160,33 +4160,37 @@ X.B(E,Y);return E}return J}())
|
||||||
if (!window.zddc) window.zddc = {};
|
if (!window.zddc) window.zddc = {};
|
||||||
if (window.zddc.nav) return; // already loaded
|
if (window.zddc.nav) return; // already loaded
|
||||||
|
|
||||||
var STAGES = [
|
// Hardcoded fallback for offline / file:// / fetch-error contexts.
|
||||||
{ key: 'archive', label: 'Archive', target: 'archive.html' },
|
// Server-driven discovery (FETCH_STAGES below) is the normal path.
|
||||||
{ key: 'working', label: 'Working', target: 'working/' },
|
var FALLBACK_STAGES = [
|
||||||
{ key: 'staging', label: 'Staging', target: 'staging/' },
|
{ name: 'archive', label: 'Archive' },
|
||||||
{ key: 'reviewing', label: 'Reviewing', target: 'reviewing/' },
|
{ 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) {
|
function projectSegment(pathname) {
|
||||||
var parts = pathname.split('/').filter(Boolean);
|
var parts = pathname.split('/').filter(Boolean);
|
||||||
if (parts.length === 0) return null;
|
if (parts.length === 0) return null;
|
||||||
var first = parts[0];
|
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;
|
if (first.indexOf('.') !== -1) return null;
|
||||||
return first;
|
return first;
|
||||||
}
|
}
|
||||||
|
|
||||||
function currentStage(pathname) {
|
function currentStage(pathname, stages) {
|
||||||
var parts = pathname.split('/').filter(Boolean);
|
var parts = pathname.split('/').filter(Boolean);
|
||||||
if (parts.length < 2) return null;
|
if (parts.length < 2) return null;
|
||||||
var second = parts[1];
|
var second = parts[1];
|
||||||
// <project>/working/... | staging/... | reviewing/... | archive/...
|
for (var i = 0; i < stages.length; i++) {
|
||||||
for (var i = 0; i < STAGES.length; i++) {
|
if (second.toLowerCase() === stages[i].name.toLowerCase()) {
|
||||||
if (second === STAGES[i].key) return STAGES[i].key;
|
return stages[i].name;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
// <project>/archive.html → still the archive stage
|
|
||||||
if (second === 'archive.html') return 'archive';
|
if (second === 'archive.html') return 'archive';
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
@ -4198,7 +4202,53 @@ X.B(E,Y);return E}return J}())
|
||||||
return projectSegment(location.pathname) !== null;
|
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');
|
var nav = document.createElement('nav');
|
||||||
nav.className = 'zddc-stage-strip';
|
nav.className = 'zddc-stage-strip';
|
||||||
nav.setAttribute('aria-label', 'Project stage');
|
nav.setAttribute('aria-label', 'Project stage');
|
||||||
|
|
@ -4214,19 +4264,19 @@ X.B(E,Y);return E}return J}())
|
||||||
sep0.textContent = '/';
|
sep0.textContent = '/';
|
||||||
nav.appendChild(sep0);
|
nav.appendChild(sep0);
|
||||||
|
|
||||||
for (var i = 0; i < STAGES.length; i++) {
|
for (var i = 0; i < stages.length; i++) {
|
||||||
var s = STAGES[i];
|
var s = stages[i];
|
||||||
var a = document.createElement('a');
|
var a = document.createElement('a');
|
||||||
a.className = 'zddc-stage';
|
a.className = 'zddc-stage';
|
||||||
a.href = '/' + encodeURIComponent(project) + '/' + s.target;
|
a.href = '/' + encodeURIComponent(project) + '/' + s.name;
|
||||||
a.textContent = s.label;
|
a.textContent = s.label;
|
||||||
if (s.key === active) {
|
if (s.name === active) {
|
||||||
a.classList.add('zddc-stage--active');
|
a.classList.add('zddc-stage--active');
|
||||||
a.setAttribute('aria-current', 'page');
|
a.setAttribute('aria-current', 'page');
|
||||||
}
|
}
|
||||||
nav.appendChild(a);
|
nav.appendChild(a);
|
||||||
|
|
||||||
if (i < STAGES.length - 1) {
|
if (i < stages.length - 1) {
|
||||||
var sep = document.createElement('span');
|
var sep = document.createElement('span');
|
||||||
sep.className = 'zddc-stage-strip__sep';
|
sep.className = 'zddc-stage-strip__sep';
|
||||||
sep.setAttribute('aria-hidden', 'true');
|
sep.setAttribute('aria-hidden', 'true');
|
||||||
|
|
@ -4238,34 +4288,44 @@ X.B(E,Y);return E}return J}())
|
||||||
return nav;
|
return nav;
|
||||||
}
|
}
|
||||||
|
|
||||||
function mount() {
|
function mountWith(project, stages) {
|
||||||
if (!shouldRender()) return;
|
|
||||||
var header = document.querySelector('.app-header');
|
var header = document.querySelector('.app-header');
|
||||||
if (!header) return;
|
if (!header) return;
|
||||||
// Don't double-mount if a tool's main.js calls us a second time.
|
|
||||||
if (header.previousElementSibling &&
|
if (header.previousElementSibling &&
|
||||||
header.previousElementSibling.classList &&
|
header.previousElementSibling.classList &&
|
||||||
header.previousElementSibling.classList.contains('zddc-stage-strip')) {
|
header.previousElementSibling.classList.contains('zddc-stage-strip')) {
|
||||||
return;
|
return; // already mounted
|
||||||
}
|
}
|
||||||
var project = projectSegment(location.pathname);
|
var active = currentStage(location.pathname, stages);
|
||||||
var active = currentStage(location.pathname);
|
var strip = buildStrip(project, active, stages);
|
||||||
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.
|
|
||||||
header.parentNode.insertBefore(strip, header);
|
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 = {
|
window.zddc.nav = {
|
||||||
mount: mount,
|
mount: mount,
|
||||||
// Internals visible for unit tests; do not call from tools.
|
|
||||||
_projectSegment: projectSegment,
|
_projectSegment: projectSegment,
|
||||||
_currentStage: currentStage,
|
_currentStage: currentStage,
|
||||||
_stages: STAGES,
|
_fallbackStages: FALLBACK_STAGES,
|
||||||
// Set to true before DOMContentLoaded to suppress mounting on
|
|
||||||
// deployments where the canonical folder layout doesn't apply.
|
|
||||||
disabled: false,
|
disabled: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -903,51 +903,6 @@ body {
|
||||||
padding: 0 16px 64px;
|
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 */
|
/* Welcome / hero */
|
||||||
.landing-hero {
|
.landing-hero {
|
||||||
margin: 0 0 24px;
|
margin: 0 0 24px;
|
||||||
|
|
@ -1469,7 +1424,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-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>
|
</div>
|
||||||
<div class="header-right">
|
<div class="header-right">
|
||||||
|
|
@ -1481,48 +1436,6 @@ body {
|
||||||
<main id="landingMain" class="landing-main">
|
<main id="landingMain" class="landing-main">
|
||||||
<!-- Picker mode (deployment root /). Project picker + groups. -->
|
<!-- Picker mode (deployment root /). Project picker + groups. -->
|
||||||
<div id="pickerView">
|
<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 -->
|
<!-- Welcome / hero -->
|
||||||
<section class="landing-hero">
|
<section class="landing-hero">
|
||||||
<h1>Welcome to the ZDDC Archive</h1>
|
<h1>Welcome to the ZDDC Archive</h1>
|
||||||
|
|
@ -2384,28 +2297,28 @@ body {
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
|
|
||||||
// shared/nav.js — lateral navigation strip across the four canonical
|
// shared/nav.js — lateral navigation strip across the project's
|
||||||
// project stages (archive · working · staging · reviewing). Renders
|
// cascade-declared stages. Mounted as a sibling of <header class="app-
|
||||||
// only when:
|
// header"> on DOMContentLoaded, hydrated from the project root's
|
||||||
// 1. location.protocol is http: or https: (online — file:// has no
|
// directory listing.
|
||||||
// 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">
|
// Stage discovery is cascade-driven (Phase 4c): fetch the project
|
||||||
// on DOMContentLoaded — no template changes required. Each tool just
|
// root's JSON listing, filter to entries with `declared: true`
|
||||||
// needs ../shared/nav.{js,css} in its build.sh.
|
// (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
|
// When the fetch fails (offline / no-server / file://), the strip
|
||||||
// zddc.varasys.io/reference.html#transmittal-workflow:
|
// falls back to the hardcoded four-stage list so existing
|
||||||
// archive → <project>/archive.html (archive tool, project-root mode)
|
// deployments don't lose chrome. Hardcoded labels in this file are
|
||||||
// working → <project>/working/ (directory listing → mdedit auto-serves)
|
// the LAST resort — the cascade is the source of truth in normal
|
||||||
// staging → <project>/staging/ (directory listing → transmittal auto-serves)
|
// operation.
|
||||||
// reviewing → <project>/reviewing/ (directory listing)
|
|
||||||
//
|
//
|
||||||
// If a deployment doesn't have one of these folders the link will 404 —
|
// Stage URLs follow the slash/no-slash convention: no slash opens
|
||||||
// the strip is convention-driven, not probed. Operators on non-standard
|
// the stage's default tool. Operators on non-standard layouts can
|
||||||
// layouts can override by setting window.zddc.nav.disabled = true before
|
// override by setting window.zddc.nav.disabled = true before
|
||||||
// DOMContentLoaded.
|
// DOMContentLoaded.
|
||||||
(function () {
|
(function () {
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
@ -2413,33 +2326,37 @@ body {
|
||||||
if (!window.zddc) window.zddc = {};
|
if (!window.zddc) window.zddc = {};
|
||||||
if (window.zddc.nav) return; // already loaded
|
if (window.zddc.nav) return; // already loaded
|
||||||
|
|
||||||
var STAGES = [
|
// Hardcoded fallback for offline / file:// / fetch-error contexts.
|
||||||
{ key: 'archive', label: 'Archive', target: 'archive.html' },
|
// Server-driven discovery (FETCH_STAGES below) is the normal path.
|
||||||
{ key: 'working', label: 'Working', target: 'working/' },
|
var FALLBACK_STAGES = [
|
||||||
{ key: 'staging', label: 'Staging', target: 'staging/' },
|
{ name: 'archive', label: 'Archive' },
|
||||||
{ key: 'reviewing', label: 'Reviewing', target: 'reviewing/' },
|
{ 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) {
|
function projectSegment(pathname) {
|
||||||
var parts = pathname.split('/').filter(Boolean);
|
var parts = pathname.split('/').filter(Boolean);
|
||||||
if (parts.length === 0) return null;
|
if (parts.length === 0) return null;
|
||||||
var first = parts[0];
|
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;
|
if (first.indexOf('.') !== -1) return null;
|
||||||
return first;
|
return first;
|
||||||
}
|
}
|
||||||
|
|
||||||
function currentStage(pathname) {
|
function currentStage(pathname, stages) {
|
||||||
var parts = pathname.split('/').filter(Boolean);
|
var parts = pathname.split('/').filter(Boolean);
|
||||||
if (parts.length < 2) return null;
|
if (parts.length < 2) return null;
|
||||||
var second = parts[1];
|
var second = parts[1];
|
||||||
// <project>/working/... | staging/... | reviewing/... | archive/...
|
for (var i = 0; i < stages.length; i++) {
|
||||||
for (var i = 0; i < STAGES.length; i++) {
|
if (second.toLowerCase() === stages[i].name.toLowerCase()) {
|
||||||
if (second === STAGES[i].key) return STAGES[i].key;
|
return stages[i].name;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
// <project>/archive.html → still the archive stage
|
|
||||||
if (second === 'archive.html') return 'archive';
|
if (second === 'archive.html') return 'archive';
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
@ -2451,7 +2368,53 @@ body {
|
||||||
return projectSegment(location.pathname) !== null;
|
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');
|
var nav = document.createElement('nav');
|
||||||
nav.className = 'zddc-stage-strip';
|
nav.className = 'zddc-stage-strip';
|
||||||
nav.setAttribute('aria-label', 'Project stage');
|
nav.setAttribute('aria-label', 'Project stage');
|
||||||
|
|
@ -2467,19 +2430,19 @@ body {
|
||||||
sep0.textContent = '/';
|
sep0.textContent = '/';
|
||||||
nav.appendChild(sep0);
|
nav.appendChild(sep0);
|
||||||
|
|
||||||
for (var i = 0; i < STAGES.length; i++) {
|
for (var i = 0; i < stages.length; i++) {
|
||||||
var s = STAGES[i];
|
var s = stages[i];
|
||||||
var a = document.createElement('a');
|
var a = document.createElement('a');
|
||||||
a.className = 'zddc-stage';
|
a.className = 'zddc-stage';
|
||||||
a.href = '/' + encodeURIComponent(project) + '/' + s.target;
|
a.href = '/' + encodeURIComponent(project) + '/' + s.name;
|
||||||
a.textContent = s.label;
|
a.textContent = s.label;
|
||||||
if (s.key === active) {
|
if (s.name === active) {
|
||||||
a.classList.add('zddc-stage--active');
|
a.classList.add('zddc-stage--active');
|
||||||
a.setAttribute('aria-current', 'page');
|
a.setAttribute('aria-current', 'page');
|
||||||
}
|
}
|
||||||
nav.appendChild(a);
|
nav.appendChild(a);
|
||||||
|
|
||||||
if (i < STAGES.length - 1) {
|
if (i < stages.length - 1) {
|
||||||
var sep = document.createElement('span');
|
var sep = document.createElement('span');
|
||||||
sep.className = 'zddc-stage-strip__sep';
|
sep.className = 'zddc-stage-strip__sep';
|
||||||
sep.setAttribute('aria-hidden', 'true');
|
sep.setAttribute('aria-hidden', 'true');
|
||||||
|
|
@ -2491,34 +2454,44 @@ body {
|
||||||
return nav;
|
return nav;
|
||||||
}
|
}
|
||||||
|
|
||||||
function mount() {
|
function mountWith(project, stages) {
|
||||||
if (!shouldRender()) return;
|
|
||||||
var header = document.querySelector('.app-header');
|
var header = document.querySelector('.app-header');
|
||||||
if (!header) return;
|
if (!header) return;
|
||||||
// Don't double-mount if a tool's main.js calls us a second time.
|
|
||||||
if (header.previousElementSibling &&
|
if (header.previousElementSibling &&
|
||||||
header.previousElementSibling.classList &&
|
header.previousElementSibling.classList &&
|
||||||
header.previousElementSibling.classList.contains('zddc-stage-strip')) {
|
header.previousElementSibling.classList.contains('zddc-stage-strip')) {
|
||||||
return;
|
return; // already mounted
|
||||||
}
|
}
|
||||||
var project = projectSegment(location.pathname);
|
var active = currentStage(location.pathname, stages);
|
||||||
var active = currentStage(location.pathname);
|
var strip = buildStrip(project, active, stages);
|
||||||
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.
|
|
||||||
header.parentNode.insertBefore(strip, header);
|
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 = {
|
window.zddc.nav = {
|
||||||
mount: mount,
|
mount: mount,
|
||||||
// Internals visible for unit tests; do not call from tools.
|
|
||||||
_projectSegment: projectSegment,
|
_projectSegment: projectSegment,
|
||||||
_currentStage: currentStage,
|
_currentStage: currentStage,
|
||||||
_stages: STAGES,
|
_fallbackStages: FALLBACK_STAGES,
|
||||||
// Set to true before DOMContentLoaded to suppress mounting on
|
|
||||||
// deployments where the canonical folder layout doesn't apply.
|
|
||||||
disabled: false,
|
disabled: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -3365,7 +3338,11 @@ body {
|
||||||
function openSelectedMdl() {
|
function openSelectedMdl() {
|
||||||
var party = mdlSelect.value;
|
var party = mdlSelect.value;
|
||||||
if (!party) return;
|
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);
|
window.location.assign(url);
|
||||||
}
|
}
|
||||||
mdlOpenBtn.addEventListener('click', openSelectedMdl);
|
mdlOpenBtn.addEventListener('click', openSelectedMdl);
|
||||||
|
|
|
||||||
|
|
@ -2155,7 +2155,7 @@ 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-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>
|
||||||
<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">⟳</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
|
// shared/nav.js — lateral navigation strip across the project's
|
||||||
// project stages (archive · working · staging · reviewing). Renders
|
// cascade-declared stages. Mounted as a sibling of <header class="app-
|
||||||
// only when:
|
// header"> on DOMContentLoaded, hydrated from the project root's
|
||||||
// 1. location.protocol is http: or https: (online — file:// has no
|
// directory listing.
|
||||||
// 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">
|
// Stage discovery is cascade-driven (Phase 4c): fetch the project
|
||||||
// on DOMContentLoaded — no template changes required. Each tool just
|
// root's JSON listing, filter to entries with `declared: true`
|
||||||
// needs ../shared/nav.{js,css} in its build.sh.
|
// (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
|
// When the fetch fails (offline / no-server / file://), the strip
|
||||||
// zddc.varasys.io/reference.html#transmittal-workflow:
|
// falls back to the hardcoded four-stage list so existing
|
||||||
// archive → <project>/archive.html (archive tool, project-root mode)
|
// deployments don't lose chrome. Hardcoded labels in this file are
|
||||||
// working → <project>/working/ (directory listing → mdedit auto-serves)
|
// the LAST resort — the cascade is the source of truth in normal
|
||||||
// staging → <project>/staging/ (directory listing → transmittal auto-serves)
|
// operation.
|
||||||
// reviewing → <project>/reviewing/ (directory listing)
|
|
||||||
//
|
//
|
||||||
// If a deployment doesn't have one of these folders the link will 404 —
|
// Stage URLs follow the slash/no-slash convention: no slash opens
|
||||||
// the strip is convention-driven, not probed. Operators on non-standard
|
// the stage's default tool. Operators on non-standard layouts can
|
||||||
// layouts can override by setting window.zddc.nav.disabled = true before
|
// override by setting window.zddc.nav.disabled = true before
|
||||||
// DOMContentLoaded.
|
// DOMContentLoaded.
|
||||||
(function () {
|
(function () {
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
@ -4457,33 +4457,37 @@ X.B(E,Y);return E}return J}())
|
||||||
if (!window.zddc) window.zddc = {};
|
if (!window.zddc) window.zddc = {};
|
||||||
if (window.zddc.nav) return; // already loaded
|
if (window.zddc.nav) return; // already loaded
|
||||||
|
|
||||||
var STAGES = [
|
// Hardcoded fallback for offline / file:// / fetch-error contexts.
|
||||||
{ key: 'archive', label: 'Archive', target: 'archive.html' },
|
// Server-driven discovery (FETCH_STAGES below) is the normal path.
|
||||||
{ key: 'working', label: 'Working', target: 'working/' },
|
var FALLBACK_STAGES = [
|
||||||
{ key: 'staging', label: 'Staging', target: 'staging/' },
|
{ name: 'archive', label: 'Archive' },
|
||||||
{ key: 'reviewing', label: 'Reviewing', target: 'reviewing/' },
|
{ 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) {
|
function projectSegment(pathname) {
|
||||||
var parts = pathname.split('/').filter(Boolean);
|
var parts = pathname.split('/').filter(Boolean);
|
||||||
if (parts.length === 0) return null;
|
if (parts.length === 0) return null;
|
||||||
var first = parts[0];
|
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;
|
if (first.indexOf('.') !== -1) return null;
|
||||||
return first;
|
return first;
|
||||||
}
|
}
|
||||||
|
|
||||||
function currentStage(pathname) {
|
function currentStage(pathname, stages) {
|
||||||
var parts = pathname.split('/').filter(Boolean);
|
var parts = pathname.split('/').filter(Boolean);
|
||||||
if (parts.length < 2) return null;
|
if (parts.length < 2) return null;
|
||||||
var second = parts[1];
|
var second = parts[1];
|
||||||
// <project>/working/... | staging/... | reviewing/... | archive/...
|
for (var i = 0; i < stages.length; i++) {
|
||||||
for (var i = 0; i < STAGES.length; i++) {
|
if (second.toLowerCase() === stages[i].name.toLowerCase()) {
|
||||||
if (second === STAGES[i].key) return STAGES[i].key;
|
return stages[i].name;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
// <project>/archive.html → still the archive stage
|
|
||||||
if (second === 'archive.html') return 'archive';
|
if (second === 'archive.html') return 'archive';
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
@ -4495,7 +4499,53 @@ X.B(E,Y);return E}return J}())
|
||||||
return projectSegment(location.pathname) !== null;
|
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');
|
var nav = document.createElement('nav');
|
||||||
nav.className = 'zddc-stage-strip';
|
nav.className = 'zddc-stage-strip';
|
||||||
nav.setAttribute('aria-label', 'Project stage');
|
nav.setAttribute('aria-label', 'Project stage');
|
||||||
|
|
@ -4511,19 +4561,19 @@ X.B(E,Y);return E}return J}())
|
||||||
sep0.textContent = '/';
|
sep0.textContent = '/';
|
||||||
nav.appendChild(sep0);
|
nav.appendChild(sep0);
|
||||||
|
|
||||||
for (var i = 0; i < STAGES.length; i++) {
|
for (var i = 0; i < stages.length; i++) {
|
||||||
var s = STAGES[i];
|
var s = stages[i];
|
||||||
var a = document.createElement('a');
|
var a = document.createElement('a');
|
||||||
a.className = 'zddc-stage';
|
a.className = 'zddc-stage';
|
||||||
a.href = '/' + encodeURIComponent(project) + '/' + s.target;
|
a.href = '/' + encodeURIComponent(project) + '/' + s.name;
|
||||||
a.textContent = s.label;
|
a.textContent = s.label;
|
||||||
if (s.key === active) {
|
if (s.name === active) {
|
||||||
a.classList.add('zddc-stage--active');
|
a.classList.add('zddc-stage--active');
|
||||||
a.setAttribute('aria-current', 'page');
|
a.setAttribute('aria-current', 'page');
|
||||||
}
|
}
|
||||||
nav.appendChild(a);
|
nav.appendChild(a);
|
||||||
|
|
||||||
if (i < STAGES.length - 1) {
|
if (i < stages.length - 1) {
|
||||||
var sep = document.createElement('span');
|
var sep = document.createElement('span');
|
||||||
sep.className = 'zddc-stage-strip__sep';
|
sep.className = 'zddc-stage-strip__sep';
|
||||||
sep.setAttribute('aria-hidden', 'true');
|
sep.setAttribute('aria-hidden', 'true');
|
||||||
|
|
@ -4535,34 +4585,44 @@ X.B(E,Y);return E}return J}())
|
||||||
return nav;
|
return nav;
|
||||||
}
|
}
|
||||||
|
|
||||||
function mount() {
|
function mountWith(project, stages) {
|
||||||
if (!shouldRender()) return;
|
|
||||||
var header = document.querySelector('.app-header');
|
var header = document.querySelector('.app-header');
|
||||||
if (!header) return;
|
if (!header) return;
|
||||||
// Don't double-mount if a tool's main.js calls us a second time.
|
|
||||||
if (header.previousElementSibling &&
|
if (header.previousElementSibling &&
|
||||||
header.previousElementSibling.classList &&
|
header.previousElementSibling.classList &&
|
||||||
header.previousElementSibling.classList.contains('zddc-stage-strip')) {
|
header.previousElementSibling.classList.contains('zddc-stage-strip')) {
|
||||||
return;
|
return; // already mounted
|
||||||
}
|
}
|
||||||
var project = projectSegment(location.pathname);
|
var active = currentStage(location.pathname, stages);
|
||||||
var active = currentStage(location.pathname);
|
var strip = buildStrip(project, active, stages);
|
||||||
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.
|
|
||||||
header.parentNode.insertBefore(strip, header);
|
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 = {
|
window.zddc.nav = {
|
||||||
mount: mount,
|
mount: mount,
|
||||||
// Internals visible for unit tests; do not call from tools.
|
|
||||||
_projectSegment: projectSegment,
|
_projectSegment: projectSegment,
|
||||||
_currentStage: currentStage,
|
_currentStage: currentStage,
|
||||||
_stages: STAGES,
|
_fallbackStages: FALLBACK_STAGES,
|
||||||
// Set to true before DOMContentLoaded to suppress mounting on
|
|
||||||
// deployments where the canonical folder layout doesn't apply.
|
|
||||||
disabled: false,
|
disabled: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2523,7 +2523,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-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>
|
||||||
<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;
|
||||||
|
|
@ -5187,28 +5187,28 @@ X.B(E,Y);return E}return J}())
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
|
|
||||||
// shared/nav.js — lateral navigation strip across the four canonical
|
// shared/nav.js — lateral navigation strip across the project's
|
||||||
// project stages (archive · working · staging · reviewing). Renders
|
// cascade-declared stages. Mounted as a sibling of <header class="app-
|
||||||
// only when:
|
// header"> on DOMContentLoaded, hydrated from the project root's
|
||||||
// 1. location.protocol is http: or https: (online — file:// has no
|
// directory listing.
|
||||||
// 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">
|
// Stage discovery is cascade-driven (Phase 4c): fetch the project
|
||||||
// on DOMContentLoaded — no template changes required. Each tool just
|
// root's JSON listing, filter to entries with `declared: true`
|
||||||
// needs ../shared/nav.{js,css} in its build.sh.
|
// (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
|
// When the fetch fails (offline / no-server / file://), the strip
|
||||||
// zddc.varasys.io/reference.html#transmittal-workflow:
|
// falls back to the hardcoded four-stage list so existing
|
||||||
// archive → <project>/archive.html (archive tool, project-root mode)
|
// deployments don't lose chrome. Hardcoded labels in this file are
|
||||||
// working → <project>/working/ (directory listing → mdedit auto-serves)
|
// the LAST resort — the cascade is the source of truth in normal
|
||||||
// staging → <project>/staging/ (directory listing → transmittal auto-serves)
|
// operation.
|
||||||
// reviewing → <project>/reviewing/ (directory listing)
|
|
||||||
//
|
//
|
||||||
// If a deployment doesn't have one of these folders the link will 404 —
|
// Stage URLs follow the slash/no-slash convention: no slash opens
|
||||||
// the strip is convention-driven, not probed. Operators on non-standard
|
// the stage's default tool. Operators on non-standard layouts can
|
||||||
// layouts can override by setting window.zddc.nav.disabled = true before
|
// override by setting window.zddc.nav.disabled = true before
|
||||||
// DOMContentLoaded.
|
// DOMContentLoaded.
|
||||||
(function () {
|
(function () {
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
@ -5216,33 +5216,37 @@ X.B(E,Y);return E}return J}())
|
||||||
if (!window.zddc) window.zddc = {};
|
if (!window.zddc) window.zddc = {};
|
||||||
if (window.zddc.nav) return; // already loaded
|
if (window.zddc.nav) return; // already loaded
|
||||||
|
|
||||||
var STAGES = [
|
// Hardcoded fallback for offline / file:// / fetch-error contexts.
|
||||||
{ key: 'archive', label: 'Archive', target: 'archive.html' },
|
// Server-driven discovery (FETCH_STAGES below) is the normal path.
|
||||||
{ key: 'working', label: 'Working', target: 'working/' },
|
var FALLBACK_STAGES = [
|
||||||
{ key: 'staging', label: 'Staging', target: 'staging/' },
|
{ name: 'archive', label: 'Archive' },
|
||||||
{ key: 'reviewing', label: 'Reviewing', target: 'reviewing/' },
|
{ 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) {
|
function projectSegment(pathname) {
|
||||||
var parts = pathname.split('/').filter(Boolean);
|
var parts = pathname.split('/').filter(Boolean);
|
||||||
if (parts.length === 0) return null;
|
if (parts.length === 0) return null;
|
||||||
var first = parts[0];
|
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;
|
if (first.indexOf('.') !== -1) return null;
|
||||||
return first;
|
return first;
|
||||||
}
|
}
|
||||||
|
|
||||||
function currentStage(pathname) {
|
function currentStage(pathname, stages) {
|
||||||
var parts = pathname.split('/').filter(Boolean);
|
var parts = pathname.split('/').filter(Boolean);
|
||||||
if (parts.length < 2) return null;
|
if (parts.length < 2) return null;
|
||||||
var second = parts[1];
|
var second = parts[1];
|
||||||
// <project>/working/... | staging/... | reviewing/... | archive/...
|
for (var i = 0; i < stages.length; i++) {
|
||||||
for (var i = 0; i < STAGES.length; i++) {
|
if (second.toLowerCase() === stages[i].name.toLowerCase()) {
|
||||||
if (second === STAGES[i].key) return STAGES[i].key;
|
return stages[i].name;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
// <project>/archive.html → still the archive stage
|
|
||||||
if (second === 'archive.html') return 'archive';
|
if (second === 'archive.html') return 'archive';
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
@ -5254,7 +5258,53 @@ X.B(E,Y);return E}return J}())
|
||||||
return projectSegment(location.pathname) !== null;
|
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');
|
var nav = document.createElement('nav');
|
||||||
nav.className = 'zddc-stage-strip';
|
nav.className = 'zddc-stage-strip';
|
||||||
nav.setAttribute('aria-label', 'Project stage');
|
nav.setAttribute('aria-label', 'Project stage');
|
||||||
|
|
@ -5270,19 +5320,19 @@ X.B(E,Y);return E}return J}())
|
||||||
sep0.textContent = '/';
|
sep0.textContent = '/';
|
||||||
nav.appendChild(sep0);
|
nav.appendChild(sep0);
|
||||||
|
|
||||||
for (var i = 0; i < STAGES.length; i++) {
|
for (var i = 0; i < stages.length; i++) {
|
||||||
var s = STAGES[i];
|
var s = stages[i];
|
||||||
var a = document.createElement('a');
|
var a = document.createElement('a');
|
||||||
a.className = 'zddc-stage';
|
a.className = 'zddc-stage';
|
||||||
a.href = '/' + encodeURIComponent(project) + '/' + s.target;
|
a.href = '/' + encodeURIComponent(project) + '/' + s.name;
|
||||||
a.textContent = s.label;
|
a.textContent = s.label;
|
||||||
if (s.key === active) {
|
if (s.name === active) {
|
||||||
a.classList.add('zddc-stage--active');
|
a.classList.add('zddc-stage--active');
|
||||||
a.setAttribute('aria-current', 'page');
|
a.setAttribute('aria-current', 'page');
|
||||||
}
|
}
|
||||||
nav.appendChild(a);
|
nav.appendChild(a);
|
||||||
|
|
||||||
if (i < STAGES.length - 1) {
|
if (i < stages.length - 1) {
|
||||||
var sep = document.createElement('span');
|
var sep = document.createElement('span');
|
||||||
sep.className = 'zddc-stage-strip__sep';
|
sep.className = 'zddc-stage-strip__sep';
|
||||||
sep.setAttribute('aria-hidden', 'true');
|
sep.setAttribute('aria-hidden', 'true');
|
||||||
|
|
@ -5294,34 +5344,44 @@ X.B(E,Y);return E}return J}())
|
||||||
return nav;
|
return nav;
|
||||||
}
|
}
|
||||||
|
|
||||||
function mount() {
|
function mountWith(project, stages) {
|
||||||
if (!shouldRender()) return;
|
|
||||||
var header = document.querySelector('.app-header');
|
var header = document.querySelector('.app-header');
|
||||||
if (!header) return;
|
if (!header) return;
|
||||||
// Don't double-mount if a tool's main.js calls us a second time.
|
|
||||||
if (header.previousElementSibling &&
|
if (header.previousElementSibling &&
|
||||||
header.previousElementSibling.classList &&
|
header.previousElementSibling.classList &&
|
||||||
header.previousElementSibling.classList.contains('zddc-stage-strip')) {
|
header.previousElementSibling.classList.contains('zddc-stage-strip')) {
|
||||||
return;
|
return; // already mounted
|
||||||
}
|
}
|
||||||
var project = projectSegment(location.pathname);
|
var active = currentStage(location.pathname, stages);
|
||||||
var active = currentStage(location.pathname);
|
var strip = buildStrip(project, active, stages);
|
||||||
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.
|
|
||||||
header.parentNode.insertBefore(strip, header);
|
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 = {
|
window.zddc.nav = {
|
||||||
mount: mount,
|
mount: mount,
|
||||||
// Internals visible for unit tests; do not call from tools.
|
|
||||||
_projectSegment: projectSegment,
|
_projectSegment: projectSegment,
|
||||||
_currentStage: currentStage,
|
_currentStage: currentStage,
|
||||||
_stages: STAGES,
|
_fallbackStages: FALLBACK_STAGES,
|
||||||
// Set to true before DOMContentLoaded to suppress mounting on
|
|
||||||
// deployments where the canonical folder layout doesn't apply.
|
|
||||||
disabled: false,
|
disabled: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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-11 · lens-mesa-chalk
|
archive=v0.0.17-beta · 2026-05-12 · candle-mast-pearl
|
||||||
transmittal=v0.0.17-beta · 2026-05-11 · lens-mesa-chalk
|
transmittal=v0.0.17-beta · 2026-05-12 · candle-mast-pearl
|
||||||
classifier=v0.0.17-beta · 2026-05-11 · lens-mesa-chalk
|
classifier=v0.0.17-beta · 2026-05-12 · candle-mast-pearl
|
||||||
mdedit=v0.0.17-beta · 2026-05-11 · lens-mesa-chalk
|
mdedit=v0.0.17-beta · 2026-05-12 · candle-mast-pearl
|
||||||
landing=v0.0.17-beta · 2026-05-11 · lens-mesa-chalk
|
landing=v0.0.17-beta · 2026-05-12 · candle-mast-pearl
|
||||||
form=v0.0.17-beta · 2026-05-11 · lens-mesa-chalk
|
form=v0.0.17-beta · 2026-05-12 · candle-mast-pearl
|
||||||
tables=v0.0.17-beta · 2026-05-11 · lens-mesa-chalk
|
tables=v0.0.17-beta · 2026-05-12 · candle-mast-pearl
|
||||||
browse=v0.0.17-beta · 2026-05-11 · lens-mesa-chalk
|
browse=v0.0.17-beta · 2026-05-12 · candle-mast-pearl
|
||||||
|
|
|
||||||
|
|
@ -1300,7 +1300,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-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>
|
</div>
|
||||||
<div class="header-right">
|
<div class="header-right">
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue