Adds a thin nav strip directly under the app-header showing the four canonical lifecycle stages from the transmittal-workflow spec: archive · working · staging · reviewing. Each is a link to that stage's directory under the current project. Current stage is highlighted (bold + primary color, aria-current="page"). Strip mounts as a sibling of .app-header on DOMContentLoaded — no template changes needed in any tool. Render rules (shared/nav.js shouldRender): - location.protocol must be http: or https: (file:// has no project structure to navigate within) - a project segment must be detectable as the first path segment (when it isn't a tool HTML file like /index.html or /archive.html?projects=A,B). Multi-project view at the deployment root therefore shows no strip. Stage URL targets: - Archive → <project>/archive.html (project-root archive view) - Working → <project>/working/ (directory listing — mdedit auto-served) - Staging → <project>/staging/ (directory listing — transmittal auto-served) - Reviewing → <project>/reviewing/ (directory listing) Convention-driven, not probed: if a deployment doesn't have one of these folders the link returns 404. Operators on non-standard layouts can opt out by setting window.zddc.nav.disabled = true before DOMContentLoaded. This pairs with the previous landing-tool change (single-project click → <project>/archive.html). Together they give the user both URL-bar manipulation AND visible navigation across the four canonical project stages. Five Playwright tests in tests/nav.spec.js exercise: - non-render at deployment root - render + active stage on <project>/archive.html - render + active stage deep inside <project>/working/foo/mdedit.html - canonical link targets - mount position is sibling of .app-header Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
141 lines
5.5 KiB
JavaScript
141 lines
5.5 KiB
JavaScript
// shared/nav.js — lateral navigation strip across the four canonical
|
|
// project stages (archive · working · staging · reviewing). Renders
|
|
// only when:
|
|
// 1. location.protocol is http: or https: (online — file:// has no
|
|
// project structure to navigate within), AND
|
|
// 2. a project segment can be detected from location.pathname (the
|
|
// first path segment, when it isn't a tool HTML file).
|
|
//
|
|
// The strip is inserted as a sibling of <header class="app-header">
|
|
// on DOMContentLoaded — no template changes required. Each tool just
|
|
// needs ../shared/nav.{js,css} in its build.sh.
|
|
//
|
|
// Stage URLs follow the canonical workflow folders documented at
|
|
// zddc.varasys.io/reference.html#transmittal-workflow:
|
|
// archive → <project>/archive.html (archive tool, project-root mode)
|
|
// working → <project>/working/ (directory listing → mdedit auto-serves)
|
|
// staging → <project>/staging/ (directory listing → transmittal auto-serves)
|
|
// reviewing → <project>/reviewing/ (directory listing)
|
|
//
|
|
// If a deployment doesn't have one of these folders the link will 404 —
|
|
// the strip is convention-driven, not probed. Operators on non-standard
|
|
// layouts can override by setting window.zddc.nav.disabled = true before
|
|
// DOMContentLoaded.
|
|
(function () {
|
|
'use strict';
|
|
|
|
if (!window.zddc) window.zddc = {};
|
|
if (window.zddc.nav) return; // already loaded
|
|
|
|
var STAGES = [
|
|
{ key: 'archive', label: 'Archive', target: 'archive.html' },
|
|
{ key: 'working', label: 'Working', target: 'working/' },
|
|
{ key: 'staging', label: 'Staging', target: 'staging/' },
|
|
{ key: 'reviewing', label: 'Reviewing', target: 'reviewing/' },
|
|
];
|
|
|
|
function projectSegment(pathname) {
|
|
var parts = pathname.split('/').filter(Boolean);
|
|
if (parts.length === 0) return null;
|
|
var first = parts[0];
|
|
// At deployment root (e.g. /archive.html?projects=A,B or
|
|
// /index.html) the first segment is a tool HTML — no single
|
|
// project to scope the strip to.
|
|
if (first.indexOf('.') !== -1) return null;
|
|
return first;
|
|
}
|
|
|
|
function currentStage(pathname) {
|
|
var parts = pathname.split('/').filter(Boolean);
|
|
if (parts.length < 2) return null;
|
|
var second = parts[1];
|
|
// <project>/working/... | staging/... | reviewing/... | archive/...
|
|
for (var i = 0; i < STAGES.length; i++) {
|
|
if (second === STAGES[i].key) return STAGES[i].key;
|
|
}
|
|
// <project>/archive.html → still the archive stage
|
|
if (second === 'archive.html') return 'archive';
|
|
return null;
|
|
}
|
|
|
|
function shouldRender() {
|
|
if (typeof location === 'undefined') return false;
|
|
if (location.protocol !== 'http:' && location.protocol !== 'https:') return false;
|
|
if (window.zddc.nav && window.zddc.nav.disabled) return false;
|
|
return projectSegment(location.pathname) !== null;
|
|
}
|
|
|
|
function buildStrip(project, active) {
|
|
var nav = document.createElement('nav');
|
|
nav.className = 'zddc-stage-strip';
|
|
nav.setAttribute('aria-label', 'Project stage');
|
|
|
|
var label = document.createElement('span');
|
|
label.className = 'zddc-stage-strip__project';
|
|
label.textContent = project;
|
|
nav.appendChild(label);
|
|
|
|
var sep0 = document.createElement('span');
|
|
sep0.className = 'zddc-stage-strip__divider';
|
|
sep0.setAttribute('aria-hidden', 'true');
|
|
sep0.textContent = '/';
|
|
nav.appendChild(sep0);
|
|
|
|
for (var i = 0; i < STAGES.length; i++) {
|
|
var s = STAGES[i];
|
|
var a = document.createElement('a');
|
|
a.className = 'zddc-stage';
|
|
a.href = '/' + encodeURIComponent(project) + '/' + s.target;
|
|
a.textContent = s.label;
|
|
if (s.key === active) {
|
|
a.classList.add('zddc-stage--active');
|
|
a.setAttribute('aria-current', 'page');
|
|
}
|
|
nav.appendChild(a);
|
|
|
|
if (i < STAGES.length - 1) {
|
|
var sep = document.createElement('span');
|
|
sep.className = 'zddc-stage-strip__sep';
|
|
sep.setAttribute('aria-hidden', 'true');
|
|
sep.textContent = '·';
|
|
nav.appendChild(sep);
|
|
}
|
|
}
|
|
|
|
return nav;
|
|
}
|
|
|
|
function mount() {
|
|
if (!shouldRender()) return;
|
|
var header = document.querySelector('.app-header');
|
|
if (!header) return;
|
|
// Don't double-mount if a tool's main.js calls us a second time.
|
|
if (header.nextElementSibling &&
|
|
header.nextElementSibling.classList &&
|
|
header.nextElementSibling.classList.contains('zddc-stage-strip')) {
|
|
return;
|
|
}
|
|
var project = projectSegment(location.pathname);
|
|
var active = currentStage(location.pathname);
|
|
var strip = buildStrip(project, active);
|
|
header.parentNode.insertBefore(strip, header.nextSibling);
|
|
}
|
|
|
|
// Expose for tests + opt-out.
|
|
window.zddc.nav = {
|
|
mount: mount,
|
|
// Internals visible for unit tests; do not call from tools.
|
|
_projectSegment: projectSegment,
|
|
_currentStage: currentStage,
|
|
_stages: STAGES,
|
|
// Set to true before DOMContentLoaded to suppress mounting on
|
|
// deployments where the canonical folder layout doesn't apply.
|
|
disabled: false,
|
|
};
|
|
|
|
if (document.readyState === 'loading') {
|
|
document.addEventListener('DOMContentLoaded', mount, { once: true });
|
|
} else {
|
|
mount();
|
|
}
|
|
})();
|