-
+
Files
@@ -2757,6 +2868,212 @@ body.help-open .app-header {
}
}());
+// 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
*
@@ -4441,9 +4758,9 @@ async function reloadFileFromDisk(filePath) {
}
}, 100);
- if (editorInstance.tocContainer && window.updateToc) {
+ if (editorInstance.tocContainer) {
try {
- window.updateToc(parsed.content, editorInstance.tocContainer, editorInstance.editor, tocMaxDepth);
+ updateToc(parsed.content, editorInstance.tocContainer, editorInstance.editor, tocMaxDepth);
} catch (error) {
console.error('Error updating TOC during reload:', error);
}
@@ -5837,9 +6154,9 @@ function initializeEditor(content, isMarkdown = true, filePath = '', fileName =
tocDepthSelector.addEventListener('change', function () {
const depth = parseInt(this.value);
- if (window.updateToc && editorInstance) {
+ if (editorInstance) {
const currentContent = editorInstance.getMarkdown();
- window.updateToc(currentContent, tocContainer, editorInstance, depth);
+ updateToc(currentContent, tocContainer, editorInstance, depth);
}
});
}
@@ -5886,16 +6203,16 @@ function initializeEditor(content, isMarkdown = true, filePath = '', fileName =
}
// Generate initial TOC
- if (isMarkdown && window.updateToc && tocContainer) {
+ if (isMarkdown && tocContainer) {
try {
- window.updateToc(markdownBody, tocContainer, editorInstance, tocMaxDepth);
+ updateToc(markdownBody, tocContainer, editorInstance, tocMaxDepth);
} catch (error) {
console.error('Error generating TOC:', error);
}
const debouncedUpdateToc = debounce(() => {
const currentContent = editorInstance.getMarkdown();
- window.updateToc(currentContent, tocContainer, editorInstance, tocMaxDepth);
+ updateToc(currentContent, tocContainer, editorInstance, tocMaxDepth);
}, 300);
editorInstance.on('change', () => {
@@ -6290,10 +6607,8 @@ function setActiveTocItem(tocContainer, headerText) {
}
}
-// Export globally
-window.updateToc = updateToc;
-window.clearActiveTocItem = clearActiveTocItem;
-window.setActiveTocItem = setActiveTocItem;
+// Reachable at top-level scope to other concatenated mdedit JS files via the
+// build's flat-IIFE-less module pattern; no window.* exports needed.
/**
* Pane resizing functionality
@@ -6506,7 +6821,7 @@ function setupTocDepthSelector() {
const content = instance.editor.getMarkdown();
try {
- window.updateToc(content, instance.tocContainer, instance.editor, tocMaxDepth);
+ updateToc(content, instance.tocContainer, instance.editor, tocMaxDepth);
} catch (error) {
console.error('Error updating TOC depth:', error);
}
diff --git a/zddc/internal/apps/embedded/transmittal.html b/zddc/internal/apps/embedded/transmittal.html
index 95588c2..90a1eed 100644
--- a/zddc/internal/apps/embedded/transmittal.html
+++ b/zddc/internal/apps/embedded/transmittal.html
@@ -339,6 +339,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 ─────────────────────────────────────────── */
@@ -529,6 +534,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);
+}
+
/* Placeholder for contenteditable elements */
[data-placeholder]:empty::before {
content: attr(data-placeholder);
@@ -638,51 +741,15 @@ body.help-open .app-header {
border-bottom-color: #86efac;
}
- /* Owner/Project names area and inline bg-white / bg-gray-50 utility classes */
- @media (prefers-color-scheme: dark) {
- :root:not([data-theme="light"]) .header-names {
- background-color: var(--bg-secondary) !important;
- border-color: var(--border) !important;
- }
- :root:not([data-theme="light"]) .text-gray-700 { color: var(--text-muted) !important; }
- :root:not([data-theme="light"]) .bg-white { background-color: var(--bg) !important; }
- :root:not([data-theme="light"]) .bg-gray-50 { background-color: var(--bg-secondary) !important; }
- :root:not([data-theme="light"]) .bg-gray-100 { background-color: var(--bg-secondary) !important; }
- :root:not([data-theme="light"]) .border-gray-100,
- :root:not([data-theme="light"]) .border-gray-200,
- :root:not([data-theme="light"]) .border-gray-300 { border-color: var(--border) !important; }
- :root:not([data-theme="light"]) .text-gray-900 { color: var(--text) !important; }
- }
- [data-theme="dark"] .header-names {
- background-color: var(--bg-secondary) !important;
- border-color: var(--border) !important;
- }
- [data-theme="dark"] .text-gray-700 { color: var(--text-muted) !important; }
- [data-theme="dark"] .bg-white { background-color: var(--bg) !important; }
- [data-theme="dark"] .bg-gray-50 { background-color: var(--bg-secondary) !important; }
- [data-theme="dark"] .bg-gray-100 { background-color: var(--bg-secondary) !important; }
- [data-theme="dark"] .border-gray-100,
- [data-theme="dark"] .border-gray-200,
- [data-theme="dark"] .border-gray-300 { border-color: var(--border) !important; }
- [data-theme="dark"] .text-gray-900 { color: var(--text) !important; }
-
- /* Filter inputs in table column headers */
- @media (prefers-color-scheme: dark) {
- :root:not([data-theme="light"]) .table-filter-input {
- background-color: var(--bg);
- color: var(--text);
- border-color: var(--border);
- }
- :root:not([data-theme="light"]) .table-header__caption { color: var(--text-muted); }
- :root:not([data-theme="light"]) .focus\:bg-white:focus { background-color: var(--bg) !important; }
- }
- [data-theme="dark"] .table-filter-input {
- background-color: var(--bg);
- color: var(--text);
- border-color: var(--border);
- }
- [data-theme="dark"] .table-header__caption { color: var(--text-muted); }
- [data-theme="dark"] .focus\:bg-white:focus { background-color: var(--bg) !important; }
+ /* Note: dark-mode overrides for .bg-white / .bg-gray-* / .text-gray-*
+ / .border-gray-* and .header-names used to live here as a 17-line
+ block of !important rules to fight hardcoded colors in
+ transmittal/css/utilities.css. The utility classes were tokenized
+ (var(--bg), var(--bg-secondary), var(--text), var(--text-muted),
+ var(--border)) so the cascade now does the right thing in both
+ themes without per-class overrides. .table-filter-input is unused
+ (no element references it; .column-filter from shared is used
+ instead) and was likewise dropped. */
}
/* Logo row: flex layout — logo | title | logo */
@@ -1895,11 +1962,15 @@ dialog.modal--narrow {
.text-2xl { font-size: 1.5rem; line-height: 2rem; }
.text-\[12px\] { font-size: 12px; line-height: 1.4; }
.text-\[10px\] { font-size: 10px; line-height: 1.3; }
-.text-gray-900 { color: #111827; }
-.text-gray-700 { color: #374151; }
-.text-gray-600 { color: #4b5563; }
-.text-gray-500 { color: #6b7280; }
-.text-gray-400 { color: #9ca3af; }
+/* Gray-scale text classes are theme-encoding — they map to shared
+ tokens so dark mode swaps automatically without per-class overrides.
+ The named-color text classes (.text-blue-600/-green-600/-red-600)
+ carry semantic meaning (link / success / danger) and stay hardcoded. */
+.text-gray-900 { color: var(--text); }
+.text-gray-700 { color: var(--text-muted); }
+.text-gray-600 { color: var(--text-muted); }
+.text-gray-500 { color: var(--text-muted); }
+.text-gray-400 { color: var(--text-muted); }
.text-blue-600 { color: #2563eb; }
.text-green-600 { color: #16a34a; }
.text-red-600 { color: #dc2626; }
@@ -1908,20 +1979,20 @@ dialog.modal--narrow {
.leading-6 { line-height: 1.5rem; }
.leading-snug { line-height: 1.375rem; }
-/* Backgrounds */
-.bg-white { background-color: #ffffff; }
+/* Backgrounds — gray-scale classes map to shared tokens. */
+.bg-white { background-color: var(--bg); }
.bg-transparent { background-color: transparent; }
-.bg-gray-50 { background-color: #f9fafb; }
-.bg-gray-100 { background-color: #f3f4f6; }
+.bg-gray-50 { background-color: var(--bg-secondary); }
+.bg-gray-100 { background-color: var(--bg-secondary); }
-/* Borders */
-.border { border: 1px solid #d1d5db; }
+/* Borders — gray-scale border classes map to the shared token. */
+.border { border: 1px solid var(--border); }
.border-0 { border: 0; }
-.border-b { border-bottom: 1px solid #d1d5db; }
-.border-t { border-top: 1px solid #d1d5db; }
-.border-gray-300 { border-color: #d1d5db; }
-.border-gray-200 { border-color: #e5e7eb; }
-.border-gray-100 { border-color: #f3f4f6; }
+.border-b { border-bottom: 1px solid var(--border); }
+.border-t { border-top: 1px solid var(--border); }
+.border-gray-300 { border-color: var(--border); }
+.border-gray-200 { border-color: var(--border); }
+.border-gray-100 { border-color: var(--border); }
.rounded-none { border-radius: 0; }
.rounded-sm { border-radius: 0.125rem; }
.rounded { border-radius: 0.25rem; }
@@ -2008,14 +2079,14 @@ dialog.modal--narrow {
.border-red-500 { border-color: #ef4444 !important; }
/* Hover & focus states */
-.hover\:bg-gray-50:hover { background-color: #f9fafb; }
-.hover\:bg-gray-100:hover { background-color: #f3f4f6; }
+.hover\:bg-gray-50:hover { background-color: var(--bg-hover); }
+.hover\:bg-gray-100:hover { background-color: var(--bg-hover); }
.hover\:underline:hover { text-decoration: underline; }
.focus\:outline-none:focus { outline: none; }
.focus\:border-blue-400:focus { border-color: #60a5fa; }
.focus\:ring-1:focus { box-shadow: 0 0 0 1px rgba(59, 130, 246, 0.35); }
.focus\:ring-blue-400:focus { box-shadow: 0 0 0 2px rgba(96, 165, 250, 0.45); }
-.focus\:bg-white:focus { background-color: #ffffff; }
+.focus\:bg-white:focus { background-color: var(--bg); }
.disabled\:pointer-events-none:disabled { pointer-events: none; }
/* Table helpers */
@@ -2192,7 +2263,7 @@ dialog.modal--narrow {
ZDDC Transmittal
- v0.0.17-beta · 2026-05-08 · falcon-alder-ginger
+ v0.0.17-beta · 2026-05-10 · alder-cherry-reef
JavaScript not available