The shared header strip pointed Working/Staging/Reviewing at the slash form (working/, etc.), which now serves browse per the slash/no-slash convention established earlier. The user expected those links to open the stage's tool (mdedit for working, transmittal for staging, etc.) — which is what the no-slash form serves. Also drops the .html suffix from the archive target: <project>/archive (no slash) → archive tool, same as the other stages. The currentStage recognizer still accepts /archive.html as a fallback for any direct URLs that survive in bookmarks. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
146 lines
5.8 KiB
JavaScript
146 lines
5.8 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 slash/no-slash convention: no slash opens the
|
|
// stage's default tool; slash opens browse. The stage strip points
|
|
// users at the working tool for each stage:
|
|
// archive → <project>/archive (archive tool)
|
|
// working → <project>/working (mdedit rooted at working/)
|
|
// staging → <project>/staging (transmittal tool)
|
|
// reviewing → <project>/reviewing (mdedit at the virtual aggregator)
|
|
//
|
|
// If a deployment doesn't have one of these folders the server's
|
|
// canonical-folder fallback still lands the user on a usable empty
|
|
// view — 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' },
|
|
{ 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.previousElementSibling &&
|
|
header.previousElementSibling.classList &&
|
|
header.previousElementSibling.classList.contains('zddc-stage-strip')) {
|
|
return;
|
|
}
|
|
var project = projectSegment(location.pathname);
|
|
var active = currentStage(location.pathname);
|
|
var strip = buildStrip(project, active);
|
|
// Mount ABOVE the header — the strip is project-level chrome
|
|
// (where in the project), the header is tool-level chrome (which
|
|
// tool, theme, help). Reading order matches outer-to-inner scope.
|
|
header.parentNode.insertBefore(strip, header);
|
|
}
|
|
|
|
// 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();
|
|
}
|
|
})();
|