feat(shared): lateral project-stage strip in every tool's header
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>
This commit is contained in:
parent
cc515b0f56
commit
7ced0395b6
13 changed files with 508 additions and 1 deletions
|
|
@ -21,6 +21,7 @@ trap cleanup EXIT
|
|||
concat_files \
|
||||
"../shared/base.css" \
|
||||
"../shared/toast.css" \
|
||||
"../shared/nav.css" \
|
||||
"css/base.css" \
|
||||
"css/layout.css" \
|
||||
"css/components.css" \
|
||||
|
|
@ -40,6 +41,7 @@ concat_files \
|
|||
"../shared/hash.js" \
|
||||
"../shared/theme.js" \
|
||||
"../shared/toast.js" \
|
||||
"../shared/nav.js" \
|
||||
"../shared/preview-lib.js" \
|
||||
"js/init.js" \
|
||||
"js/parser.js" \
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ trap cleanup EXIT
|
|||
concat_files \
|
||||
"../shared/base.css" \
|
||||
"../shared/toast.css" \
|
||||
"../shared/nav.css" \
|
||||
"css/base.css" \
|
||||
"css/tree.css" \
|
||||
> "$css_temp"
|
||||
|
|
@ -36,6 +37,7 @@ concat_files \
|
|||
"../shared/zddc-filter.js" \
|
||||
"../shared/theme.js" \
|
||||
"../shared/toast.js" \
|
||||
"../shared/nav.js" \
|
||||
"../shared/help.js" \
|
||||
"../shared/preview-lib.js" \
|
||||
"js/init.js" \
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ trap cleanup EXIT
|
|||
concat_files \
|
||||
"../shared/base.css" \
|
||||
"../shared/toast.css" \
|
||||
"../shared/nav.css" \
|
||||
"css/base.css" \
|
||||
"css/layout.css" \
|
||||
"css/spreadsheet.css" \
|
||||
|
|
@ -39,6 +40,7 @@ concat_files \
|
|||
"../shared/zddc-source.js" \
|
||||
"../shared/theme.js" \
|
||||
"../shared/toast.js" \
|
||||
"../shared/nav.js" \
|
||||
"../shared/preview-lib.js" \
|
||||
"js/app.js" \
|
||||
"js/utils.js" \
|
||||
|
|
|
|||
|
|
@ -20,12 +20,14 @@ trap cleanup EXIT
|
|||
concat_files \
|
||||
"../shared/base.css" \
|
||||
"../shared/toast.css" \
|
||||
"../shared/nav.css" \
|
||||
"css/form.css" \
|
||||
> "$css_temp"
|
||||
|
||||
concat_files \
|
||||
"../shared/theme.js" \
|
||||
"../shared/toast.js" \
|
||||
"../shared/nav.js" \
|
||||
"../shared/help.js" \
|
||||
"js/app.js" \
|
||||
"js/context.js" \
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ trap cleanup EXIT
|
|||
concat_files \
|
||||
"../shared/base.css" \
|
||||
"../shared/toast.css" \
|
||||
"../shared/nav.css" \
|
||||
"css/landing.css" \
|
||||
> "$css_temp"
|
||||
|
||||
|
|
@ -28,6 +29,7 @@ concat_files \
|
|||
"../shared/zddc-filter.js" \
|
||||
"../shared/theme.js" \
|
||||
"../shared/toast.js" \
|
||||
"../shared/nav.js" \
|
||||
"../shared/help.js" \
|
||||
"js/landing.js" \
|
||||
> "$js_raw"
|
||||
|
|
|
|||
|
|
@ -31,6 +31,7 @@ concat_files \
|
|||
"css/tailwind-utils.css" \
|
||||
"../shared/base.css" \
|
||||
"../shared/toast.css" \
|
||||
"../shared/nav.css" \
|
||||
"css/base.css" \
|
||||
"css/editor.css" \
|
||||
"css/toc.css" \
|
||||
|
|
@ -43,6 +44,7 @@ concat_files \
|
|||
"../shared/zddc-source.js" \
|
||||
"../shared/theme.js" \
|
||||
"../shared/toast.js" \
|
||||
"../shared/nav.js" \
|
||||
"../shared/preview-lib.js" \
|
||||
"js/app.js" \
|
||||
"js/utils.js" \
|
||||
|
|
|
|||
|
|
@ -63,6 +63,10 @@ export default defineConfig({
|
|||
name: 'toast',
|
||||
testMatch: 'toast.spec.js',
|
||||
},
|
||||
{
|
||||
name: 'nav',
|
||||
testMatch: 'nav.spec.js',
|
||||
},
|
||||
{
|
||||
name: 'zddc',
|
||||
testMatch: 'zddc.spec.js',
|
||||
|
|
|
|||
56
shared/nav.css
Normal file
56
shared/nav.css
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
/* shared/nav.css — lateral project-stage strip paired with shared/nav.js.
|
||||
Sits as a sibling immediately under .app-header (mounted by JS).
|
||||
Rendered only in online mode when a project segment is in the URL. */
|
||||
|
||||
.zddc-stage-strip {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
padding: 0.3rem 1rem;
|
||||
background: var(--bg);
|
||||
border-bottom: 1px solid var(--border);
|
||||
font-size: 0.8rem;
|
||||
line-height: 1.3;
|
||||
flex-shrink: 0;
|
||||
overflow-x: auto;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.zddc-stage-strip__project {
|
||||
color: var(--text);
|
||||
font-weight: 600;
|
||||
margin-right: 0.15rem;
|
||||
}
|
||||
|
||||
.zddc-stage-strip__divider,
|
||||
.zddc-stage-strip__sep {
|
||||
color: var(--text-muted);
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.zddc-stage-strip__divider {
|
||||
margin-right: 0.35rem;
|
||||
}
|
||||
|
||||
.zddc-stage {
|
||||
color: var(--text-muted);
|
||||
text-decoration: none;
|
||||
padding: 0.1rem 0.25rem;
|
||||
border-radius: var(--radius);
|
||||
transition: color 0.15s, background 0.15s;
|
||||
}
|
||||
|
||||
.zddc-stage:hover {
|
||||
color: var(--text);
|
||||
background: var(--bg-secondary);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.zddc-stage--active {
|
||||
color: var(--primary);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.zddc-stage--active:hover {
|
||||
color: var(--primary);
|
||||
}
|
||||
141
shared/nav.js
Normal file
141
shared/nav.js
Normal file
|
|
@ -0,0 +1,141 @@
|
|||
// 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();
|
||||
}
|
||||
})();
|
||||
|
|
@ -20,6 +20,7 @@ trap cleanup EXIT
|
|||
concat_files \
|
||||
"../shared/base.css" \
|
||||
"../shared/toast.css" \
|
||||
"../shared/nav.css" \
|
||||
"css/table.css" \
|
||||
"../form/css/form.css" \
|
||||
> "$css_temp"
|
||||
|
|
@ -35,6 +36,7 @@ concat_files \
|
|||
"../shared/zddc-source.js" \
|
||||
"../shared/theme.js" \
|
||||
"../shared/toast.js" \
|
||||
"../shared/nav.js" \
|
||||
"../shared/help.js" \
|
||||
"js/mode.js" \
|
||||
"js/app.js" \
|
||||
|
|
|
|||
91
tests/nav.spec.js
Normal file
91
tests/nav.spec.js
Normal file
|
|
@ -0,0 +1,91 @@
|
|||
// Tests for shared/nav.js — the lateral project-stage strip.
|
||||
//
|
||||
// The strip's render decision depends on location.protocol and
|
||||
// location.pathname. file:// won't render at all (online-only). To
|
||||
// exercise online behavior we spin up a tiny in-process HTTP server
|
||||
// for this spec so the page can be served from http://127.0.0.1:<port>
|
||||
// at arbitrary paths.
|
||||
|
||||
import { test, expect } from '@playwright/test';
|
||||
import * as http from 'http';
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
|
||||
const HTML_PATH = path.resolve('classifier/dist/classifier.html');
|
||||
|
||||
let server;
|
||||
let baseUrl;
|
||||
|
||||
test.beforeAll(async () => {
|
||||
const html = fs.readFileSync(HTML_PATH, 'utf8');
|
||||
server = http.createServer((req, res) => {
|
||||
// Serve the same classifier HTML at every path. The strip's
|
||||
// detection logic uses location.pathname; the bytes don't have
|
||||
// to vary.
|
||||
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
|
||||
res.end(html);
|
||||
});
|
||||
await new Promise(resolve => server.listen(0, '127.0.0.1', resolve));
|
||||
const port = server.address().port;
|
||||
baseUrl = `http://127.0.0.1:${port}`;
|
||||
});
|
||||
|
||||
test.afterAll(async () => {
|
||||
if (server) await new Promise(resolve => server.close(resolve));
|
||||
});
|
||||
|
||||
test.describe('shared/nav.js stage strip', () => {
|
||||
|
||||
test('does NOT render at the deployment root', async ({ page }) => {
|
||||
await page.goto(`${baseUrl}/index.html`, { waitUntil: 'load' });
|
||||
await page.waitForSelector('.app-header', { timeout: 5000 });
|
||||
await expect(page.locator('.zddc-stage-strip')).toHaveCount(0);
|
||||
});
|
||||
|
||||
test('renders for <project>/archive.html with archive active', async ({ page }) => {
|
||||
await page.goto(`${baseUrl}/projA/archive.html`, { waitUntil: 'load' });
|
||||
const strip = page.locator('.zddc-stage-strip');
|
||||
await expect(strip).toHaveCount(1);
|
||||
await expect(strip.locator('.zddc-stage-strip__project')).toHaveText('projA');
|
||||
|
||||
const stages = await strip.locator('.zddc-stage').allTextContents();
|
||||
expect(stages).toEqual(['Archive', 'Working', 'Staging', 'Reviewing']);
|
||||
|
||||
const active = strip.locator('.zddc-stage--active');
|
||||
await expect(active).toHaveCount(1);
|
||||
await expect(active).toHaveText('Archive');
|
||||
await expect(active).toHaveAttribute('aria-current', 'page');
|
||||
});
|
||||
|
||||
test('renders for <project>/working/foo/mdedit.html with working active', async ({ page }) => {
|
||||
await page.goto(`${baseUrl}/projA/working/casey/mdedit.html`, { waitUntil: 'load' });
|
||||
const active = page.locator('.zddc-stage-strip .zddc-stage--active');
|
||||
await expect(active).toHaveText('Working');
|
||||
});
|
||||
|
||||
test('stage links point to the canonical <project>/<stage>/ URLs', async ({ page }) => {
|
||||
await page.goto(`${baseUrl}/projA/staging/`, { waitUntil: 'load' });
|
||||
await page.waitForSelector('.zddc-stage-strip');
|
||||
|
||||
const links = await page.evaluate(() => {
|
||||
const xs = document.querySelectorAll('.zddc-stage-strip .zddc-stage');
|
||||
return Array.from(xs).map(a => ({ text: a.textContent, href: a.getAttribute('href') }));
|
||||
});
|
||||
expect(links).toEqual([
|
||||
{ text: 'Archive', href: '/projA/archive.html' },
|
||||
{ text: 'Working', href: '/projA/working/' },
|
||||
{ text: 'Staging', href: '/projA/staging/' },
|
||||
{ text: 'Reviewing', href: '/projA/reviewing/' },
|
||||
]);
|
||||
});
|
||||
|
||||
test('mounts immediately under the app-header', async ({ page }) => {
|
||||
await page.goto(`${baseUrl}/projA/archive.html`, { waitUntil: 'load' });
|
||||
const next = await page.evaluate(() => {
|
||||
const h = document.querySelector('.app-header');
|
||||
return h && h.nextElementSibling && h.nextElementSibling.className;
|
||||
});
|
||||
expect(next).toContain('zddc-stage-strip');
|
||||
});
|
||||
|
||||
});
|
||||
|
|
@ -24,6 +24,7 @@ trap cleanup EXIT
|
|||
concat_files \
|
||||
"../shared/base.css" \
|
||||
"../shared/toast.css" \
|
||||
"../shared/nav.css" \
|
||||
"css/base.css" \
|
||||
"css/layout.css" \
|
||||
"css/forms.css" \
|
||||
|
|
@ -49,6 +50,7 @@ concat_files \
|
|||
"../shared/zddc-source.js" \
|
||||
"../shared/theme.js" \
|
||||
"../shared/toast.js" \
|
||||
"../shared/nav.js" \
|
||||
"../shared/preview-lib.js" \
|
||||
"js/app.js" \
|
||||
"js/reactive.js" \
|
||||
|
|
|
|||
|
|
@ -571,6 +571,63 @@ body.help-open .app-header {
|
|||
to { transform: translateX(100%); opacity: 0; }
|
||||
}
|
||||
|
||||
/* shared/nav.css — lateral project-stage strip paired with shared/nav.js.
|
||||
Sits as a sibling immediately under .app-header (mounted by JS).
|
||||
Rendered only in online mode when a project segment is in the URL. */
|
||||
|
||||
.zddc-stage-strip {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
padding: 0.3rem 1rem;
|
||||
background: var(--bg);
|
||||
border-bottom: 1px solid var(--border);
|
||||
font-size: 0.8rem;
|
||||
line-height: 1.3;
|
||||
flex-shrink: 0;
|
||||
overflow-x: auto;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.zddc-stage-strip__project {
|
||||
color: var(--text);
|
||||
font-weight: 600;
|
||||
margin-right: 0.15rem;
|
||||
}
|
||||
|
||||
.zddc-stage-strip__divider,
|
||||
.zddc-stage-strip__sep {
|
||||
color: var(--text-muted);
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.zddc-stage-strip__divider {
|
||||
margin-right: 0.35rem;
|
||||
}
|
||||
|
||||
.zddc-stage {
|
||||
color: var(--text-muted);
|
||||
text-decoration: none;
|
||||
padding: 0.1rem 0.25rem;
|
||||
border-radius: var(--radius);
|
||||
transition: color 0.15s, background 0.15s;
|
||||
}
|
||||
|
||||
.zddc-stage:hover {
|
||||
color: var(--text);
|
||||
background: var(--bg-secondary);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.zddc-stage--active {
|
||||
color: var(--primary);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.zddc-stage--active:hover {
|
||||
color: var(--primary);
|
||||
}
|
||||
|
||||
/* tables/ — directory-of-YAML table view. Reuses tokens from shared/base.css. */
|
||||
|
||||
.table-main {
|
||||
|
|
@ -1013,7 +1070,7 @@ body.help-open .app-header {
|
|||
</svg>
|
||||
<div class="header-title-group">
|
||||
<span class="app-header__title" id="table-title">ZDDC Table</span>
|
||||
<span class="build-timestamp"><span style="color:red;font-weight:bold">v0.0.17-alpha · 2026-05-10 00:02:54 · 538167b-dirty</span></span>
|
||||
<span class="build-timestamp"><span style="color:red;font-weight:bold">v0.0.17-alpha · 2026-05-10 00:48:10 · cc515b0-dirty</span></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="header-right">
|
||||
|
|
@ -2075,6 +2132,148 @@ body.help-open .app-header {
|
|||
window.zddc.toast = toast;
|
||||
})();
|
||||
|
||||
// shared/nav.js — lateral navigation strip across the four canonical
|
||||
// project stages (archive · working · staging · reviewing). Renders
|
||||
// only when:
|
||||
// 1. location.protocol is http: or https: (online — file:// has no
|
||||
// project structure to navigate within), AND
|
||||
// 2. a project segment can be detected from location.pathname (the
|
||||
// first path segment, when it isn't a tool HTML file).
|
||||
//
|
||||
// The strip is inserted as a sibling of <header class="app-header">
|
||||
// on DOMContentLoaded — no template changes required. Each tool just
|
||||
// needs ../shared/nav.{js,css} in its build.sh.
|
||||
//
|
||||
// Stage URLs follow the canonical workflow folders documented at
|
||||
// zddc.varasys.io/reference.html#transmittal-workflow:
|
||||
// archive → <project>/archive.html (archive tool, project-root mode)
|
||||
// working → <project>/working/ (directory listing → mdedit auto-serves)
|
||||
// staging → <project>/staging/ (directory listing → transmittal auto-serves)
|
||||
// reviewing → <project>/reviewing/ (directory listing)
|
||||
//
|
||||
// If a deployment doesn't have one of these folders the link will 404 —
|
||||
// the strip is convention-driven, not probed. Operators on non-standard
|
||||
// layouts can override by setting window.zddc.nav.disabled = true before
|
||||
// DOMContentLoaded.
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
if (!window.zddc) window.zddc = {};
|
||||
if (window.zddc.nav) return; // already loaded
|
||||
|
||||
var STAGES = [
|
||||
{ key: 'archive', label: 'Archive', target: 'archive.html' },
|
||||
{ key: 'working', label: 'Working', target: 'working/' },
|
||||
{ key: 'staging', label: 'Staging', target: 'staging/' },
|
||||
{ key: 'reviewing', label: 'Reviewing', target: 'reviewing/' },
|
||||
];
|
||||
|
||||
function projectSegment(pathname) {
|
||||
var parts = pathname.split('/').filter(Boolean);
|
||||
if (parts.length === 0) return null;
|
||||
var first = parts[0];
|
||||
// At deployment root (e.g. /archive.html?projects=A,B or
|
||||
// /index.html) the first segment is a tool HTML — no single
|
||||
// project to scope the strip to.
|
||||
if (first.indexOf('.') !== -1) return null;
|
||||
return first;
|
||||
}
|
||||
|
||||
function currentStage(pathname) {
|
||||
var parts = pathname.split('/').filter(Boolean);
|
||||
if (parts.length < 2) return null;
|
||||
var second = parts[1];
|
||||
// <project>/working/... | staging/... | reviewing/... | archive/...
|
||||
for (var i = 0; i < STAGES.length; i++) {
|
||||
if (second === STAGES[i].key) return STAGES[i].key;
|
||||
}
|
||||
// <project>/archive.html → still the archive stage
|
||||
if (second === 'archive.html') return 'archive';
|
||||
return null;
|
||||
}
|
||||
|
||||
function shouldRender() {
|
||||
if (typeof location === 'undefined') return false;
|
||||
if (location.protocol !== 'http:' && location.protocol !== 'https:') return false;
|
||||
if (window.zddc.nav && window.zddc.nav.disabled) return false;
|
||||
return projectSegment(location.pathname) !== null;
|
||||
}
|
||||
|
||||
function buildStrip(project, active) {
|
||||
var nav = document.createElement('nav');
|
||||
nav.className = 'zddc-stage-strip';
|
||||
nav.setAttribute('aria-label', 'Project stage');
|
||||
|
||||
var label = document.createElement('span');
|
||||
label.className = 'zddc-stage-strip__project';
|
||||
label.textContent = project;
|
||||
nav.appendChild(label);
|
||||
|
||||
var sep0 = document.createElement('span');
|
||||
sep0.className = 'zddc-stage-strip__divider';
|
||||
sep0.setAttribute('aria-hidden', 'true');
|
||||
sep0.textContent = '/';
|
||||
nav.appendChild(sep0);
|
||||
|
||||
for (var i = 0; i < STAGES.length; i++) {
|
||||
var s = STAGES[i];
|
||||
var a = document.createElement('a');
|
||||
a.className = 'zddc-stage';
|
||||
a.href = '/' + encodeURIComponent(project) + '/' + s.target;
|
||||
a.textContent = s.label;
|
||||
if (s.key === active) {
|
||||
a.classList.add('zddc-stage--active');
|
||||
a.setAttribute('aria-current', 'page');
|
||||
}
|
||||
nav.appendChild(a);
|
||||
|
||||
if (i < STAGES.length - 1) {
|
||||
var sep = document.createElement('span');
|
||||
sep.className = 'zddc-stage-strip__sep';
|
||||
sep.setAttribute('aria-hidden', 'true');
|
||||
sep.textContent = '·';
|
||||
nav.appendChild(sep);
|
||||
}
|
||||
}
|
||||
|
||||
return nav;
|
||||
}
|
||||
|
||||
function mount() {
|
||||
if (!shouldRender()) return;
|
||||
var header = document.querySelector('.app-header');
|
||||
if (!header) return;
|
||||
// Don't double-mount if a tool's main.js calls us a second time.
|
||||
if (header.nextElementSibling &&
|
||||
header.nextElementSibling.classList &&
|
||||
header.nextElementSibling.classList.contains('zddc-stage-strip')) {
|
||||
return;
|
||||
}
|
||||
var project = projectSegment(location.pathname);
|
||||
var active = currentStage(location.pathname);
|
||||
var strip = buildStrip(project, active);
|
||||
header.parentNode.insertBefore(strip, header.nextSibling);
|
||||
}
|
||||
|
||||
// Expose for tests + opt-out.
|
||||
window.zddc.nav = {
|
||||
mount: mount,
|
||||
// Internals visible for unit tests; do not call from tools.
|
||||
_projectSegment: projectSegment,
|
||||
_currentStage: currentStage,
|
||||
_stages: STAGES,
|
||||
// Set to true before DOMContentLoaded to suppress mounting on
|
||||
// deployments where the canonical folder layout doesn't apply.
|
||||
disabled: false,
|
||||
};
|
||||
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', mount, { once: true });
|
||||
} else {
|
||||
mount();
|
||||
}
|
||||
})();
|
||||
|
||||
/**
|
||||
* ZDDC shared help panel — open/close logic.
|
||||
* Works with all four tools regardless of their module pattern.
|
||||
|
|
|
|||
Loading…
Reference in a new issue