From 585e84f2f40b66a4f95745d8d59e07f330f97503 Mon Sep 17 00:00:00 2001 From: ZDDC Date: Sat, 9 May 2026 20:13:21 -0500 Subject: [PATCH] chore(embedded): cut v0.0.17-beta MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Beta cut of the eight HTML tools into zddc/internal/apps/embedded/* and the unified form/tables bundle into zddc/internal/handler/tables.html. Each tool's on-page label changes from alpha → beta-stamped bytes; no source changes beyond the build label itself. The dev image (Dockerfile, devshell, ZDDC_REF=main) and the bitnest test container both pick this up automatically — bitnest's path-unit fired on the rebuild of zddc/dist/zddc-server-linux-amd64 and restarted the container with the new embedded apps: embedded_apps=archive=v0.0.17-beta browse=v0.0.17-beta classifier=v0.0.17-beta form=v0.0.17-beta landing=v0.0.17-beta mdedit=v0.0.17-beta tables=v0.0.17-beta transmittal=v0.0.17-beta Source-side commits since the previous beta: feat(landing): single-project click → /archive.html feat(shared): non-blocking toast helper feat(shared): lateral project-stage strip feat(form): standalone empty-state welcome fix(tables): keepalive on beforeunload save path refactor(mdedit): drop window.* TOC globals refactor(archive): remove dead debounce style(transmittal): tokenize utility classes, drop !important block style: replace inline styles with CSS test(shared): zddc-source.js + toast + nav specs test(browse): smoke spec docs: tool counts + state pattern + polyfill gaps Co-Authored-By: Claude Opus 4.7 (1M context) --- zddc/internal/apps/embedded/archive.html | 347 +++++++++++++++- zddc/internal/apps/embedded/browse.html | 313 +++++++++++++- zddc/internal/apps/embedded/classifier.html | 375 ++++++++++++++--- zddc/internal/apps/embedded/index.html | 327 ++++++++++++++- zddc/internal/apps/embedded/mdedit.html | 345 +++++++++++++++- zddc/internal/apps/embedded/transmittal.html | 407 ++++++++++++++++--- zddc/internal/apps/embedded/versions.txt | 16 +- zddc/internal/handler/tables.html | 2 +- 8 files changed, 1966 insertions(+), 166 deletions(-) diff --git a/zddc/internal/apps/embedded/archive.html b/zddc/internal/apps/embedded/archive.html index 9cbae50..e49e228 100644 --- a/zddc/internal/apps/embedded/archive.html +++ b/zddc/internal/apps/embedded/archive.html @@ -335,6 +335,11 @@ a:hover { font-size: 1rem; } +/* The refresh ⟳ glyph renders slightly smaller than ◐ / ? — bump to match. */ +#refreshHeaderBtn { + font-size: 1.1rem; +} + /* Toast CSS lives in classifier/css/base.css — only that tool uses toasts. */ /* ── Theme and help icon buttons ─────────────────────────────────────────── */ @@ -525,6 +530,104 @@ body.help-open .app-header { color: var(--text-muted); } +/* shared/toast.css — single-toast notification styles paired with + shared/toast.js. Uses BEM-ish .zddc-toast prefix to avoid collisions + with tool-local .toast classes; the old classifier rules can stay + alongside until this file is concatenated above them in the build. */ + +.zddc-toast { + position: fixed; + bottom: 2rem; + right: 2rem; + background: var(--bg); + color: var(--text); + padding: 0.875rem 1.25rem; + border-radius: var(--radius); + border: 1px solid var(--border); + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); + z-index: 9000; + max-width: 400px; + font-size: 0.875rem; + cursor: pointer; + animation: zddc-toast-in 0.3s ease-out; +} + +.zddc-toast--success { border-left: 4px solid var(--success); } +.zddc-toast--error { border-left: 4px solid var(--danger); } +.zddc-toast--info { border-left: 4px solid var(--info); } +.zddc-toast--warning { border-left: 4px solid var(--warning); } + +.zddc-toast--fade { + animation: zddc-toast-out 0.3s ease-out forwards; +} + +@keyframes zddc-toast-in { + from { transform: translateX(100%); opacity: 0; } + to { transform: translateX(0); opacity: 1; } +} + +@keyframes zddc-toast-out { + from { transform: translateX(0); opacity: 1; } + 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); +} + /* Archive-specific base overrides Reset, tokens, and font are provided by shared/base.css */ @@ -1771,6 +1874,17 @@ input[type="checkbox"] { cursor: default; } +/* Variant: content packs at the start instead of distributing across the + column header. Used by the Revisions column where a leading "select all" + checkbox sits beside the column title. */ +.th-content--start { + justify-content: flex-start; +} + +.th-content--start .select-all-checkbox { + margin-right: 0.5rem; +} + .sortable .th-content { cursor: pointer; } @@ -2131,10 +2245,10 @@ td[data-field="trackingNumber"] {
ZDDC Archive - v0.0.17-beta · 2026-05-08 · falcon-alder-ginger + v0.0.17-beta · 2026-05-10 · alder-cherry-reef
- +
@@ -2270,11 +2384,11 @@ td[data-field="trackingNumber"] {
-
- +
+ Revisions
+// 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 → /archive.html (archive tool, project-root mode) +// working → /working/ (directory listing → mdedit auto-serves) +// staging → /staging/ (directory listing → transmittal auto-serves) +// reviewing → /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]; + // /working/... | staging/... | reviewing/... | archive/... + for (var i = 0; i < STAGES.length; i++) { + if (second === STAGES[i].key) return STAGES[i].key; + } + // /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 preview helpers * @@ -7182,19 +7502,6 @@ window.app.modules.filtering = { } } - // Utility: Debounce function - function debounce(func, wait) { - let timeout; - return function executedFunction(...args) { - const later = () => { - clearTimeout(timeout); - func(...args); - }; - clearTimeout(timeout); - timeout = setTimeout(later, wait); - }; - } - // Multi-select handling for folder lists function setupFolderMultiSelect() { let lastSelectedGroupingIndex = -1; diff --git a/zddc/internal/apps/embedded/browse.html b/zddc/internal/apps/embedded/browse.html index 1083b02..fde5ec6 100644 --- a/zddc/internal/apps/embedded/browse.html +++ b/zddc/internal/apps/embedded/browse.html @@ -335,6 +335,11 @@ a:hover { font-size: 1rem; } +/* The refresh ⟳ glyph renders slightly smaller than ◐ / ? — bump to match. */ +#refreshHeaderBtn { + font-size: 1.1rem; +} + /* Toast CSS lives in classifier/css/base.css — only that tool uses toasts. */ /* ── Theme and help icon buttons ─────────────────────────────────────────── */ @@ -525,6 +530,104 @@ body.help-open .app-header { color: var(--text-muted); } +/* shared/toast.css — single-toast notification styles paired with + shared/toast.js. Uses BEM-ish .zddc-toast prefix to avoid collisions + with tool-local .toast classes; the old classifier rules can stay + alongside until this file is concatenated above them in the build. */ + +.zddc-toast { + position: fixed; + bottom: 2rem; + right: 2rem; + background: var(--bg); + color: var(--text); + padding: 0.875rem 1.25rem; + border-radius: var(--radius); + border: 1px solid var(--border); + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); + z-index: 9000; + max-width: 400px; + font-size: 0.875rem; + cursor: pointer; + animation: zddc-toast-in 0.3s ease-out; +} + +.zddc-toast--success { border-left: 4px solid var(--success); } +.zddc-toast--error { border-left: 4px solid var(--danger); } +.zddc-toast--info { border-left: 4px solid var(--info); } +.zddc-toast--warning { border-left: 4px solid var(--warning); } + +.zddc-toast--fade { + animation: zddc-toast-out 0.3s ease-out forwards; +} + +@keyframes zddc-toast-in { + from { transform: translateX(100%); opacity: 0; } + to { transform: translateX(0); opacity: 1; } +} + +@keyframes zddc-toast-out { + from { transform: translateX(0); opacity: 1; } + 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); +} + /* browse-specific layout on top of shared/base.css */ html, body { @@ -896,10 +999,10 @@ body {
ZDDC Browse - v0.0.17-beta · 2026-05-08 · falcon-alder-ginger + v0.0.17-beta · 2026-05-10 · alder-cherry-reef
- +
@@ -1703,6 +1806,212 @@ https://github.com/nodeca/pako/blob/main/LICENSE } }()); +// shared/toast.js — non-blocking notification helper available to every +// tool via window.zddc.toast(msg, level, opts). Originated as classifier's +// local showToast (classifier/js/excel.js); promoted here so tools that +// today use alert() or silent console.error can switch to a uniform +// non-blocking surface. +// +// Usage: +// window.zddc.toast('Saved.', 'success'); +// window.zddc.toast('Could not load: ' + err.message, 'error'); +// window.zddc.toast('Note', 'info', { durationMs: 3000 }); +// +// Levels: 'info' (default) | 'success' | 'warning' | 'error'. +// Each tool may also expose app.notify(msg, level) as a thin wrapper — +// see ARCHITECTURE.md for the convention. +(function () { + 'use strict'; + + if (!window.zddc) window.zddc = {}; + // Don't overwrite if a tool defined its own first. + if (typeof window.zddc.toast === 'function') return; + + var DEFAULT_DURATION_MS = 5000; + var FADE_MS = 300; + + function toast(message, level, opts) { + opts = opts || {}; + var lvl = (level === 'success' || level === 'error' || + level === 'warning') ? level : 'info'; + + // Single-toast policy: dismiss any existing toast immediately + // so the new one is always the most recent. Matches the + // classifier's prior behavior and avoids stack-of-toasts UX. + var existing = document.querySelector('.zddc-toast'); + if (existing) existing.remove(); + + var el = document.createElement('div'); + el.className = 'zddc-toast zddc-toast--' + lvl; + // ARIA: errors get assertive (interrupts SR queue), others polite. + el.setAttribute('role', lvl === 'error' ? 'alert' : 'status'); + el.setAttribute('aria-live', lvl === 'error' ? 'assertive' : 'polite'); + el.textContent = message == null ? '' : String(message); + document.body.appendChild(el); + + var dur = typeof opts.durationMs === 'number' ? + opts.durationMs : DEFAULT_DURATION_MS; + var timer = setTimeout(function () { + el.classList.add('zddc-toast--fade'); + setTimeout(function () { + if (el.parentNode) el.parentNode.removeChild(el); + }, FADE_MS); + }, dur); + + // Click-to-dismiss. Useful for sticky errors the user wants gone. + el.addEventListener('click', function () { + clearTimeout(timer); + if (el.parentNode) el.parentNode.removeChild(el); + }); + + return el; + } + + 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
+// 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 → /archive.html (archive tool, project-root mode) +// working → /working/ (directory listing → mdedit auto-serves) +// staging → /staging/ (directory listing → transmittal auto-serves) +// reviewing → /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]; + // /working/... | staging/... | reviewing/... | archive/... + for (var i = 0; i < STAGES.length; i++) { + if (second === STAGES[i].key) return STAGES[i].key; + } + // /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. diff --git a/zddc/internal/apps/embedded/classifier.html b/zddc/internal/apps/embedded/classifier.html index f23282b..05b1bd2 100644 --- a/zddc/internal/apps/embedded/classifier.html +++ b/zddc/internal/apps/embedded/classifier.html @@ -335,6 +335,11 @@ a:hover { font-size: 1rem; } +/* The refresh ⟳ glyph renders slightly smaller than ◐ / ? — bump to match. */ +#refreshHeaderBtn { + font-size: 1.1rem; +} + /* Toast CSS lives in classifier/css/base.css — only that tool uses toasts. */ /* ── Theme and help icon buttons ─────────────────────────────────────────── */ @@ -525,6 +530,104 @@ body.help-open .app-header { color: var(--text-muted); } +/* shared/toast.css — single-toast notification styles paired with + shared/toast.js. Uses BEM-ish .zddc-toast prefix to avoid collisions + with tool-local .toast classes; the old classifier rules can stay + alongside until this file is concatenated above them in the build. */ + +.zddc-toast { + position: fixed; + bottom: 2rem; + right: 2rem; + background: var(--bg); + color: var(--text); + padding: 0.875rem 1.25rem; + border-radius: var(--radius); + border: 1px solid var(--border); + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); + z-index: 9000; + max-width: 400px; + font-size: 0.875rem; + cursor: pointer; + animation: zddc-toast-in 0.3s ease-out; +} + +.zddc-toast--success { border-left: 4px solid var(--success); } +.zddc-toast--error { border-left: 4px solid var(--danger); } +.zddc-toast--info { border-left: 4px solid var(--info); } +.zddc-toast--warning { border-left: 4px solid var(--warning); } + +.zddc-toast--fade { + animation: zddc-toast-out 0.3s ease-out forwards; +} + +@keyframes zddc-toast-in { + from { transform: translateX(100%); opacity: 0; } + to { transform: translateX(0); opacity: 1; } +} + +@keyframes zddc-toast-out { + from { transform: translateX(0); opacity: 1; } + 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); +} + /* Classifier-specific base overrides Reset, tokens, buttons, and font are provided by shared/base.css */ @@ -555,41 +658,8 @@ body.help-open .app-header { cursor: pointer; } -/* ── Toast notifications (classifier-only) ───────────────────────────────── */ -/* shared/base.css intentionally omits toast CSS; only classifier uses toasts. */ -.toast { - position: fixed; - bottom: 2rem; - right: 2rem; - background: var(--bg); - color: var(--text); - padding: 0.875rem 1.25rem; - border-radius: var(--radius); - box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); - z-index: 9000; - max-width: 400px; - font-size: 0.875rem; - animation: zddc-toast-in 0.3s ease-out; -} - -.toast-success { border-left: 4px solid var(--success); } -.toast-error { border-left: 4px solid var(--danger); } -.toast-info { border-left: 4px solid var(--info); } -.toast-warning { border-left: 4px solid var(--warning); } - -.toast-fade { - animation: zddc-toast-out 0.3s ease-out forwards; -} - -@keyframes zddc-toast-in { - from { transform: translateX(100%); opacity: 0; } - to { transform: translateX(0); opacity: 1; } -} - -@keyframes zddc-toast-out { - from { transform: translateX(0); opacity: 1; } - to { transform: translateX(100%); opacity: 0; } -} +/* Toast notifications come from shared/toast.css (.zddc-toast); the + classifier-local .toast block was promoted there. */ /* Classifier layout — tokens from shared/base.css */ @@ -1394,7 +1464,7 @@ body.help-open .app-header {
ZDDC Classifier - v0.0.17-beta · 2026-05-08 · falcon-alder-ginger + v0.0.17-beta · 2026-05-10 · alder-cherry-reef
@@ -2559,6 +2629,212 @@ https://github.com/nodeca/pako/blob/main/LICENSE } }()); +// shared/toast.js — non-blocking notification helper available to every +// tool via window.zddc.toast(msg, level, opts). Originated as classifier's +// local showToast (classifier/js/excel.js); promoted here so tools that +// today use alert() or silent console.error can switch to a uniform +// non-blocking surface. +// +// Usage: +// window.zddc.toast('Saved.', 'success'); +// window.zddc.toast('Could not load: ' + err.message, 'error'); +// window.zddc.toast('Note', 'info', { durationMs: 3000 }); +// +// Levels: 'info' (default) | 'success' | 'warning' | 'error'. +// Each tool may also expose app.notify(msg, level) as a thin wrapper — +// see ARCHITECTURE.md for the convention. +(function () { + 'use strict'; + + if (!window.zddc) window.zddc = {}; + // Don't overwrite if a tool defined its own first. + if (typeof window.zddc.toast === 'function') return; + + var DEFAULT_DURATION_MS = 5000; + var FADE_MS = 300; + + function toast(message, level, opts) { + opts = opts || {}; + var lvl = (level === 'success' || level === 'error' || + level === 'warning') ? level : 'info'; + + // Single-toast policy: dismiss any existing toast immediately + // so the new one is always the most recent. Matches the + // classifier's prior behavior and avoids stack-of-toasts UX. + var existing = document.querySelector('.zddc-toast'); + if (existing) existing.remove(); + + var el = document.createElement('div'); + el.className = 'zddc-toast zddc-toast--' + lvl; + // ARIA: errors get assertive (interrupts SR queue), others polite. + el.setAttribute('role', lvl === 'error' ? 'alert' : 'status'); + el.setAttribute('aria-live', lvl === 'error' ? 'assertive' : 'polite'); + el.textContent = message == null ? '' : String(message); + document.body.appendChild(el); + + var dur = typeof opts.durationMs === 'number' ? + opts.durationMs : DEFAULT_DURATION_MS; + var timer = setTimeout(function () { + el.classList.add('zddc-toast--fade'); + setTimeout(function () { + if (el.parentNode) el.parentNode.removeChild(el); + }, FADE_MS); + }, dur); + + // Click-to-dismiss. Useful for sticky errors the user wants gone. + el.addEventListener('click', function () { + clearTimeout(timer); + if (el.parentNode) el.parentNode.removeChild(el); + }); + + return el; + } + + 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
+// 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 → /archive.html (archive tool, project-root mode) +// working → /working/ (directory listing → mdedit auto-serves) +// staging → /staging/ (directory listing → transmittal auto-serves) +// reviewing → /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]; + // /working/... | staging/... | reviewing/... | archive/... + for (var i = 0; i < STAGES.length; i++) { + if (second === STAGES[i].key) return STAGES[i].key; + } + // /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 preview helpers * @@ -7863,26 +8139,19 @@ https://github.com/nodeca/pako/blob/main/LICENSE 'use strict'; /** - * Show toast notification + * Thin wrapper over the shared toast helper. Keeps the + * window.app.modules.excel.showToast call sites in classifier + * unchanged while delegating to the canonical implementation in + * shared/toast.js (window.zddc.toast). */ function showToast(message, type = 'info') { - // Remove existing toast - const existing = document.querySelector('.toast'); - if (existing) { - existing.remove(); + if (window.zddc && typeof window.zddc.toast === 'function') { + window.zddc.toast(message, type); + } else { + // shared/toast.js missing from the build — log so the + // problem is visible without crashing the caller. + console.warn('[classifier] window.zddc.toast unavailable;', type, message); } - - // Create toast - const toast = document.createElement('div'); - toast.className = `toast toast-${type}`; - toast.textContent = message; - document.body.appendChild(toast); - - // Auto-remove after 5 seconds - setTimeout(() => { - toast.classList.add('toast-fade'); - setTimeout(() => toast.remove(), 300); - }, 5000); } /** diff --git a/zddc/internal/apps/embedded/index.html b/zddc/internal/apps/embedded/index.html index 3144dca..503457b 100644 --- a/zddc/internal/apps/embedded/index.html +++ b/zddc/internal/apps/embedded/index.html @@ -335,6 +335,11 @@ a:hover { font-size: 1rem; } +/* The refresh ⟳ glyph renders slightly smaller than ◐ / ? — bump to match. */ +#refreshHeaderBtn { + font-size: 1.1rem; +} + /* Toast CSS lives in classifier/css/base.css — only that tool uses toasts. */ /* ── Theme and help icon buttons ─────────────────────────────────────────── */ @@ -525,6 +530,104 @@ body.help-open .app-header { color: var(--text-muted); } +/* shared/toast.css — single-toast notification styles paired with + shared/toast.js. Uses BEM-ish .zddc-toast prefix to avoid collisions + with tool-local .toast classes; the old classifier rules can stay + alongside until this file is concatenated above them in the build. */ + +.zddc-toast { + position: fixed; + bottom: 2rem; + right: 2rem; + background: var(--bg); + color: var(--text); + padding: 0.875rem 1.25rem; + border-radius: var(--radius); + border: 1px solid var(--border); + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); + z-index: 9000; + max-width: 400px; + font-size: 0.875rem; + cursor: pointer; + animation: zddc-toast-in 0.3s ease-out; +} + +.zddc-toast--success { border-left: 4px solid var(--success); } +.zddc-toast--error { border-left: 4px solid var(--danger); } +.zddc-toast--info { border-left: 4px solid var(--info); } +.zddc-toast--warning { border-left: 4px solid var(--warning); } + +.zddc-toast--fade { + animation: zddc-toast-out 0.3s ease-out forwards; +} + +@keyframes zddc-toast-in { + from { transform: translateX(100%); opacity: 0; } + to { transform: translateX(0); opacity: 1; } +} + +@keyframes zddc-toast-out { + from { transform: translateX(0); opacity: 1; } + 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); +} + /* Landing page layout */ body { margin: 0; @@ -885,7 +988,7 @@ body {
ZDDC - v0.0.17-beta · 2026-05-08 · falcon-alder-ginger + v0.0.17-beta · 2026-05-10 · alder-cherry-reef
@@ -1629,6 +1732,212 @@ body { } }()); +// shared/toast.js — non-blocking notification helper available to every +// tool via window.zddc.toast(msg, level, opts). Originated as classifier's +// local showToast (classifier/js/excel.js); promoted here so tools that +// today use alert() or silent console.error can switch to a uniform +// non-blocking surface. +// +// Usage: +// window.zddc.toast('Saved.', 'success'); +// window.zddc.toast('Could not load: ' + err.message, 'error'); +// window.zddc.toast('Note', 'info', { durationMs: 3000 }); +// +// Levels: 'info' (default) | 'success' | 'warning' | 'error'. +// Each tool may also expose app.notify(msg, level) as a thin wrapper — +// see ARCHITECTURE.md for the convention. +(function () { + 'use strict'; + + if (!window.zddc) window.zddc = {}; + // Don't overwrite if a tool defined its own first. + if (typeof window.zddc.toast === 'function') return; + + var DEFAULT_DURATION_MS = 5000; + var FADE_MS = 300; + + function toast(message, level, opts) { + opts = opts || {}; + var lvl = (level === 'success' || level === 'error' || + level === 'warning') ? level : 'info'; + + // Single-toast policy: dismiss any existing toast immediately + // so the new one is always the most recent. Matches the + // classifier's prior behavior and avoids stack-of-toasts UX. + var existing = document.querySelector('.zddc-toast'); + if (existing) existing.remove(); + + var el = document.createElement('div'); + el.className = 'zddc-toast zddc-toast--' + lvl; + // ARIA: errors get assertive (interrupts SR queue), others polite. + el.setAttribute('role', lvl === 'error' ? 'alert' : 'status'); + el.setAttribute('aria-live', lvl === 'error' ? 'assertive' : 'polite'); + el.textContent = message == null ? '' : String(message); + document.body.appendChild(el); + + var dur = typeof opts.durationMs === 'number' ? + opts.durationMs : DEFAULT_DURATION_MS; + var timer = setTimeout(function () { + el.classList.add('zddc-toast--fade'); + setTimeout(function () { + if (el.parentNode) el.parentNode.removeChild(el); + }, FADE_MS); + }, dur); + + // Click-to-dismiss. Useful for sticky errors the user wants gone. + el.addEventListener('click', function () { + clearTimeout(timer); + if (el.parentNode) el.parentNode.removeChild(el); + }); + + return el; + } + + 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
+// 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 → /archive.html (archive tool, project-root mode) +// working → /working/ (directory listing → mdedit auto-serves) +// staging → /staging/ (directory listing → transmittal auto-serves) +// reviewing → /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]; + // /working/... | staging/... | reviewing/... | archive/... + for (var i = 0; i < STAGES.length; i++) { + if (second === STAGES[i].key) return STAGES[i].key; + } + // /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. @@ -2107,8 +2416,22 @@ body { function openArchiveWith(names) { if (!names || names.length === 0) return; var base = location.pathname.replace(/\/[^\/]*$/, '/'); - var params = ['projects=' + names.map(encodeURIComponent).join(',')]; var v = new URLSearchParams(location.search).get('v'); + + if (names.length === 1) { + // Single project → canonical project-subtree URL so the user + // can edit the address bar to swap archive.html for + // working/, staging/, reviewing/, etc. zddc-server's + // availability.go auto-serves the right tool at each. + // Multi-project (the `else` branch) keeps the ?projects= + // form because there's no single subtree root. + var url = base + encodeURIComponent(names[0]) + '/archive.html'; + if (v) url += '?v=' + encodeURIComponent(v); + navigate(url); + return; + } + + var params = ['projects=' + names.map(encodeURIComponent).join(',')]; if (v) params.push('v=' + encodeURIComponent(v)); navigate(base + 'archive.html?' + params.join('&')); } diff --git a/zddc/internal/apps/embedded/mdedit.html b/zddc/internal/apps/embedded/mdedit.html index 0621ed7..725231e 100644 --- a/zddc/internal/apps/embedded/mdedit.html +++ b/zddc/internal/apps/embedded/mdedit.html @@ -555,6 +555,11 @@ a:hover { font-size: 1rem; } +/* The refresh ⟳ glyph renders slightly smaller than ◐ / ? — bump to match. */ +#refreshHeaderBtn { + font-size: 1.1rem; +} + /* Toast CSS lives in classifier/css/base.css — only that tool uses toasts. */ /* ── Theme and help icon buttons ─────────────────────────────────────────── */ @@ -745,6 +750,104 @@ body.help-open .app-header { color: var(--text-muted); } +/* shared/toast.css — single-toast notification styles paired with + shared/toast.js. Uses BEM-ish .zddc-toast prefix to avoid collisions + with tool-local .toast classes; the old classifier rules can stay + alongside until this file is concatenated above them in the build. */ + +.zddc-toast { + position: fixed; + bottom: 2rem; + right: 2rem; + background: var(--bg); + color: var(--text); + padding: 0.875rem 1.25rem; + border-radius: var(--radius); + border: 1px solid var(--border); + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); + z-index: 9000; + max-width: 400px; + font-size: 0.875rem; + cursor: pointer; + animation: zddc-toast-in 0.3s ease-out; +} + +.zddc-toast--success { border-left: 4px solid var(--success); } +.zddc-toast--error { border-left: 4px solid var(--danger); } +.zddc-toast--info { border-left: 4px solid var(--info); } +.zddc-toast--warning { border-left: 4px solid var(--warning); } + +.zddc-toast--fade { + animation: zddc-toast-out 0.3s ease-out forwards; +} + +@keyframes zddc-toast-in { + from { transform: translateX(100%); opacity: 0; } + to { transform: translateX(0); opacity: 1; } +} + +@keyframes zddc-toast-out { + from { transform: translateX(0); opacity: 1; } + 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); +} + /* mdedit component styles — reset and tokens from shared/base.css */ /* Pane resizer */ @@ -1151,6 +1254,14 @@ body.help-open .app-header { gap: 0.5rem; } +/* File-nav pane: initial width + minimum size. Runtime resizer (resizer.js) + overrides via inline style.width when the user drags; the min-width here + is a defensive backstop. */ +#file-nav { + width: 450px; + min-width: 200px; +} + /* Toast UI Editor styles */ #markdown-editor { display: block !important; @@ -1792,10 +1903,10 @@ body.help-open .app-header {
ZDDC Markdown - v0.0.17-beta · 2026-05-08 · falcon-alder-ginger + v0.0.17-beta · 2026-05-10 · alder-cherry-reef
- +
@@ -1805,7 +1916,7 @@ body.help-open .app-header {
-