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:
ZDDC 2026-05-09 19:50:30 -05:00
parent cc515b0f56
commit 7ced0395b6
13 changed files with 508 additions and 1 deletions

View file

@ -21,6 +21,7 @@ trap cleanup EXIT
concat_files \ concat_files \
"../shared/base.css" \ "../shared/base.css" \
"../shared/toast.css" \ "../shared/toast.css" \
"../shared/nav.css" \
"css/base.css" \ "css/base.css" \
"css/layout.css" \ "css/layout.css" \
"css/components.css" \ "css/components.css" \
@ -40,6 +41,7 @@ concat_files \
"../shared/hash.js" \ "../shared/hash.js" \
"../shared/theme.js" \ "../shared/theme.js" \
"../shared/toast.js" \ "../shared/toast.js" \
"../shared/nav.js" \
"../shared/preview-lib.js" \ "../shared/preview-lib.js" \
"js/init.js" \ "js/init.js" \
"js/parser.js" \ "js/parser.js" \

View file

@ -21,6 +21,7 @@ trap cleanup EXIT
concat_files \ concat_files \
"../shared/base.css" \ "../shared/base.css" \
"../shared/toast.css" \ "../shared/toast.css" \
"../shared/nav.css" \
"css/base.css" \ "css/base.css" \
"css/tree.css" \ "css/tree.css" \
> "$css_temp" > "$css_temp"
@ -36,6 +37,7 @@ concat_files \
"../shared/zddc-filter.js" \ "../shared/zddc-filter.js" \
"../shared/theme.js" \ "../shared/theme.js" \
"../shared/toast.js" \ "../shared/toast.js" \
"../shared/nav.js" \
"../shared/help.js" \ "../shared/help.js" \
"../shared/preview-lib.js" \ "../shared/preview-lib.js" \
"js/init.js" \ "js/init.js" \

View file

@ -21,6 +21,7 @@ trap cleanup EXIT
concat_files \ concat_files \
"../shared/base.css" \ "../shared/base.css" \
"../shared/toast.css" \ "../shared/toast.css" \
"../shared/nav.css" \
"css/base.css" \ "css/base.css" \
"css/layout.css" \ "css/layout.css" \
"css/spreadsheet.css" \ "css/spreadsheet.css" \
@ -39,6 +40,7 @@ concat_files \
"../shared/zddc-source.js" \ "../shared/zddc-source.js" \
"../shared/theme.js" \ "../shared/theme.js" \
"../shared/toast.js" \ "../shared/toast.js" \
"../shared/nav.js" \
"../shared/preview-lib.js" \ "../shared/preview-lib.js" \
"js/app.js" \ "js/app.js" \
"js/utils.js" \ "js/utils.js" \

View file

@ -20,12 +20,14 @@ trap cleanup EXIT
concat_files \ concat_files \
"../shared/base.css" \ "../shared/base.css" \
"../shared/toast.css" \ "../shared/toast.css" \
"../shared/nav.css" \
"css/form.css" \ "css/form.css" \
> "$css_temp" > "$css_temp"
concat_files \ concat_files \
"../shared/theme.js" \ "../shared/theme.js" \
"../shared/toast.js" \ "../shared/toast.js" \
"../shared/nav.js" \
"../shared/help.js" \ "../shared/help.js" \
"js/app.js" \ "js/app.js" \
"js/context.js" \ "js/context.js" \

View file

@ -20,6 +20,7 @@ trap cleanup EXIT
concat_files \ concat_files \
"../shared/base.css" \ "../shared/base.css" \
"../shared/toast.css" \ "../shared/toast.css" \
"../shared/nav.css" \
"css/landing.css" \ "css/landing.css" \
> "$css_temp" > "$css_temp"
@ -28,6 +29,7 @@ concat_files \
"../shared/zddc-filter.js" \ "../shared/zddc-filter.js" \
"../shared/theme.js" \ "../shared/theme.js" \
"../shared/toast.js" \ "../shared/toast.js" \
"../shared/nav.js" \
"../shared/help.js" \ "../shared/help.js" \
"js/landing.js" \ "js/landing.js" \
> "$js_raw" > "$js_raw"

View file

@ -31,6 +31,7 @@ concat_files \
"css/tailwind-utils.css" \ "css/tailwind-utils.css" \
"../shared/base.css" \ "../shared/base.css" \
"../shared/toast.css" \ "../shared/toast.css" \
"../shared/nav.css" \
"css/base.css" \ "css/base.css" \
"css/editor.css" \ "css/editor.css" \
"css/toc.css" \ "css/toc.css" \
@ -43,6 +44,7 @@ concat_files \
"../shared/zddc-source.js" \ "../shared/zddc-source.js" \
"../shared/theme.js" \ "../shared/theme.js" \
"../shared/toast.js" \ "../shared/toast.js" \
"../shared/nav.js" \
"../shared/preview-lib.js" \ "../shared/preview-lib.js" \
"js/app.js" \ "js/app.js" \
"js/utils.js" \ "js/utils.js" \

View file

@ -63,6 +63,10 @@ export default defineConfig({
name: 'toast', name: 'toast',
testMatch: 'toast.spec.js', testMatch: 'toast.spec.js',
}, },
{
name: 'nav',
testMatch: 'nav.spec.js',
},
{ {
name: 'zddc', name: 'zddc',
testMatch: 'zddc.spec.js', testMatch: 'zddc.spec.js',

56
shared/nav.css Normal file
View 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
View 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();
}
})();

View file

@ -20,6 +20,7 @@ trap cleanup EXIT
concat_files \ concat_files \
"../shared/base.css" \ "../shared/base.css" \
"../shared/toast.css" \ "../shared/toast.css" \
"../shared/nav.css" \
"css/table.css" \ "css/table.css" \
"../form/css/form.css" \ "../form/css/form.css" \
> "$css_temp" > "$css_temp"
@ -35,6 +36,7 @@ concat_files \
"../shared/zddc-source.js" \ "../shared/zddc-source.js" \
"../shared/theme.js" \ "../shared/theme.js" \
"../shared/toast.js" \ "../shared/toast.js" \
"../shared/nav.js" \
"../shared/help.js" \ "../shared/help.js" \
"js/mode.js" \ "js/mode.js" \
"js/app.js" \ "js/app.js" \

91
tests/nav.spec.js Normal file
View 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');
});
});

View file

@ -24,6 +24,7 @@ trap cleanup EXIT
concat_files \ concat_files \
"../shared/base.css" \ "../shared/base.css" \
"../shared/toast.css" \ "../shared/toast.css" \
"../shared/nav.css" \
"css/base.css" \ "css/base.css" \
"css/layout.css" \ "css/layout.css" \
"css/forms.css" \ "css/forms.css" \
@ -49,6 +50,7 @@ concat_files \
"../shared/zddc-source.js" \ "../shared/zddc-source.js" \
"../shared/theme.js" \ "../shared/theme.js" \
"../shared/toast.js" \ "../shared/toast.js" \
"../shared/nav.js" \
"../shared/preview-lib.js" \ "../shared/preview-lib.js" \
"js/app.js" \ "js/app.js" \
"js/reactive.js" \ "js/reactive.js" \

View file

@ -571,6 +571,63 @@ body.help-open .app-header {
to { transform: translateX(100%); opacity: 0; } 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. */ /* tables/ — directory-of-YAML table view. Reuses tokens from shared/base.css. */
.table-main { .table-main {
@ -1013,7 +1070,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-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> </div>
<div class="header-right"> <div class="header-right">
@ -2075,6 +2132,148 @@ body.help-open .app-header {
window.zddc.toast = toast; 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. * ZDDC shared help panel — open/close logic.
* Works with all four tools regardless of their module pattern. * Works with all four tools regardless of their module pattern.