diff --git a/zddc/internal/apps/embedded/archive.html b/zddc/internal/apps/embedded/archive.html index 98d61f2..95d9d15 100644 --- a/zddc/internal/apps/embedded/archive.html +++ b/zddc/internal/apps/embedded/archive.html @@ -6,6 +6,37 @@ ZDDC Archive @@ -1316,7 +1554,7 @@ html, body {
ZDDC Browse - v0.0.17-beta · 2026-05-11 · crescent-coast-beam + v0.0.17-beta · 2026-05-11 · lens-mesa-chalk
@@ -3335,6 +3573,19 @@ var e=function(t,n){return e=Object.setPrototypeOf||{__proto__:[]}instanceof Arr } window.zddc.toast = toast; + + // Route window.alert() calls into the toast helper. Every tool has + // accumulated some `alert(...)` sites for error reporting; rather + // than touch each one, intercept globally so they're non-blocking + // and ARIA-announced consistently. Native alert is preserved on + // window.alertNative for the rare case where a truly modal block + // is needed (e.g. before navigating away with unsaved changes). + if (typeof window.alert === 'function' && !window.alertNative) { + window.alertNative = window.alert.bind(window); + window.alert = function (msg) { + toast(String(msg == null ? '' : msg), 'error'); + }; + } })(); // shared/nav.js — lateral navigation strip across the four canonical @@ -4409,7 +4660,11 @@ var e=function(t,n){return e=Object.setPrototypeOf||{__proto__:[]}instanceof Arr isZip: isZip, zipFile: null, // cached JSZip instance zipPath: raw.zipPath || null, // path within zip (for virtual children) - zipParentId: raw.zipParentId || null // ancestor zip's node id (for nested entries) + zipParentId: raw.zipParentId || null, // ancestor zip's node id (for nested entries) + // True when this entry was synthesized client-side (e.g. + // canonical project folders that don't exist on disk yet). + // Rendered with a muted style + an "(empty)" hint. + virtual: !!raw.virtual }; state.nodes.set(id, node); return node; @@ -4533,17 +4788,23 @@ var e=function(t,n){return e=Object.setPrototypeOf||{__proto__:[]}instanceof Arr var chevronClass = 'tree-name__chevron' + (expandable ? '' : ' tree-name__chevron--leaf'); var selected = state.selectedId === node.id ? ' is-selected' : ''; + var virtualCls = node.virtual ? ' tree-row--virtual' : ''; + var virtualHint = node.virtual + ? '(empty)' + : ''; return '' - + '
' + '' + '' + iconChar + '' + '' + escapeHtml(node.name) + '' + + virtualHint + '
'; } @@ -5233,32 +5494,47 @@ var e=function(t,n){return e=Object.setPrototypeOf||{__proto__:[]}instanceof Arr })(); // preview-markdown.js — markdown plugin for the browse preview pane. -// Click a .md / .markdown file in the tree → instantiate Toast UI -// editor inside the right pane, alongside a TOC pane on the right. -// Save (Ctrl+S) writes back via: -// - PUT to the file's server URL when in server mode, or -// - FileSystemWritableFileStream when in FS-API mode (local folder -// picker). Both paths set dirty=false + a status timestamp on -// success. -// zip-virtual files are read-only — the save button stays disabled. // -// Toast UI Editor is bundled (shared/vendor/toastui-editor-all.min.js) -// and is available synchronously as window.toastui by the time this -// module runs. +// Layout (CSS Grid): +// ┌─────────────────────────────────────────────────────────────────┐ +// │ toolbar: Save | ● modified | status | source │ +// ├────────────────────────────────────────┬────────────────────────┤ +// │ │ Outline │ +// │ │ • Heading 1 │ +// │ Toast UI Editor │ • Subheading │ +// │ (md / wysiwyg / preview) │ • Heading 2 │ +// │ ├────────────────────────┤ +// │ │ Front matter │ +// │ │ title: Foo │ +// │ │ revision: A │ +// └────────────────────────────────────────┴────────────────────────┘ +// Grid keeps every cell's size definite, which is what Toast UI needs +// to compute its inner scroll regions correctly. The previous nested- +// flexbox layout produced indeterminate heights and a fragile TOC +// pane width — grid fixes both. +// +// Save (Ctrl+S) writes back via PUT (server mode) or +// FileSystemWritableFileStream (FS-API). Zip-virtual files are +// read-only — Save stays disabled. Toast UI is vendored +// (shared/vendor/toastui-editor-all.min.js); window.toastui is +// available synchronously before this module runs. (function () { 'use strict'; if (!window.app || !window.app.modules) return; + var TOC_MIN_WIDTH = 180; + var TOC_MAX_WIDTH = 480; + var TOC_DEFAULT_WIDTH = 260; + function escapeHtml(s) { return String(s).replace(/&/g, '&').replace(//g, '>').replace(/"/g, '"'); } - var currentInstance = null; // { editor, container, dirty, node, hash, tocEl } + var currentInstance = null; // { editor, container, dirty, node, hash, tocEl, fmEl } + var lastTocWidth = TOC_DEFAULT_WIDTH; // remember across mounts - // Compute SHA-256 hex of a string for a "is this content different - // from what we loaded?" check. Used to enable/disable Save. async function hashContent(text) { if (!window.crypto || !window.crypto.subtle) return null; var enc = new TextEncoder().encode(text); @@ -5278,20 +5554,80 @@ var e=function(t,n){return e=Object.setPrototypeOf||{__proto__:[]}instanceof Arr currentInstance = null; } - // ── TOC (table of contents) ───────────────────────────────────────────── - // - // Ported from mdedit/js/toc.js, condensed: parse markdown for ATX-style - // headings, build a flat hierarchical list, click jumps the editor to - // the heading's line. We track WYSIWYG vs markdown mode and route the - // scroll behaviour to whichever pane is visible. + // ── Front matter ──────────────────────────────────────────────────────── + // Lightweight YAML front-matter parser. Same envelope as mdedit's: + // `---\n…\n---\n`, key:value lines, simple `[a, b, c]` arrays. + + function parseFrontMatter(content) { + if (!content || content.indexOf('---\n') !== 0) { + return { data: {}, body: content || '' }; + } + var endIdx = content.indexOf('\n---\n', 4); + if (endIdx === -1) return { data: {}, body: content }; + var fmText = content.substring(4, endIdx); + var body = content.substring(endIdx + 5); + var data = {}; + var lines = fmText.split('\n'); + for (var i = 0; i < lines.length; i++) { + var line = lines[i].trim(); + if (!line || line.charAt(0) === '#') continue; + var colon = line.indexOf(':'); + if (colon <= 0) continue; + var key = line.substring(0, colon).trim(); + var val = line.substring(colon + 1).trim(); + val = val.replace(/^["']|["']$/g, ''); + if (val.startsWith('[') && val.endsWith(']')) { + val = val.slice(1, -1).split(',').map(function (s) { + return s.trim().replace(/^["']|["']$/g, ''); + }); + } + data[key] = val; + } + return { data: data, body: body }; + } + + function renderFrontMatter(fmEl, content) { + if (!fmEl) return; + var parsed = parseFrontMatter(content); + var keys = Object.keys(parsed.data); + if (keys.length === 0) { + fmEl.innerHTML = '

No front matter.

'; + return; + } + var html = '
'; + for (var i = 0; i < keys.length; i++) { + var k = keys[i]; + var v = parsed.data[k]; + var displayV = Array.isArray(v) + ? v.map(escapeHtml).join(', ') + : escapeHtml(String(v)); + html += '
' + escapeHtml(k) + '
' + displayV + '
'; + } + html += '
'; + fmEl.innerHTML = html; + } + + // ── TOC (table of contents) ──────────────────────────────────────────── + // ATX headings only; the body markdown drives the outline. Clicking + // a heading routes to whichever Toast UI pane is currently active + // (WYSIWYG or markdown preview). function parseHeadings(content) { var headings = []; - var lines = content.split('\n'); + // Strip front matter so headings inside the envelope (e.g. comments) + // don't appear in the outline. + var parsed = parseFrontMatter(content); + var body = parsed.body; + var lines = body.split('\n'); + var inFence = false; for (var i = 0; i < lines.length; i++) { - var m = lines[i].match(/^(#{1,6})\s+(.+)$/); + var line = lines[i]; + // Skip fenced code blocks — headings inside them aren't real. + if (/^\s*```/.test(line)) { inFence = !inFence; continue; } + if (inFence) continue; + var m = line.match(/^(#{1,6})\s+(.+?)\s*#*\s*$/); if (!m) continue; - var text = m[2].trim() + var text = m[2] .replace(/\\(.)/g, '$1') .replace(/\*\*(.*?)\*\*/g, '$1') .replace(/\*(.*?)\*/g, '$1') @@ -5313,8 +5649,11 @@ var e=function(t,n){return e=Object.setPrototypeOf||{__proto__:[]}instanceof Arr var hs = ww.querySelectorAll('h1, h2, h3, h4, h5, h6'); for (var i = 0; i < hs.length; i++) { if (hs[i].textContent.trim() === heading.text) { - var top = hs[i].getBoundingClientRect().top - ww.getBoundingClientRect().top; - ww.scrollTop = top - 10; + var scroller = findScrollParent(hs[i]) || ww; + scroller.scrollTo({ + top: hs[i].offsetTop - 12, + behavior: 'smooth' + }); flashHeading(hs[i]); return; } @@ -5322,56 +5661,72 @@ var e=function(t,n){return e=Object.setPrototypeOf||{__proto__:[]}instanceof Arr } else { var line = heading.lineIndex + 1; try { editor.setSelection([line, 1], [line, 1]); } catch (_) { /* ignore */ } + // Find the matching heading in the live markdown preview + // (right column of split view). If preview is collapsed + // (markdown-only) this is a no-op. var preview = els.mdPreview; - if (!preview) return; - var phs = preview.querySelectorAll('h1, h2, h3, h4, h5, h6'); - for (var j = 0; j < phs.length; j++) { - if (phs[j].textContent.trim() === heading.text) { - var ptop = phs[j].getBoundingClientRect().top - preview.getBoundingClientRect().top; - preview.scrollTop = ptop - 10; - flashHeading(phs[j]); - return; + if (preview) { + var phs = preview.querySelectorAll('h1, h2, h3, h4, h5, h6'); + for (var j = 0; j < phs.length; j++) { + if (phs[j].textContent.trim() === heading.text) { + var pscroller = findScrollParent(phs[j]) || preview; + pscroller.scrollTo({ + top: phs[j].offsetTop - 12, + behavior: 'smooth' + }); + flashHeading(phs[j]); + return; + } } } } - } catch (e) { /* swallow; click was best-effort */ } + } catch (_e) { /* swallow; click was best-effort */ } + } + + function findScrollParent(el) { + var cur = el.parentElement; + while (cur) { + var s = getComputedStyle(cur); + if (/(auto|scroll)/.test(s.overflowY)) return cur; + cur = cur.parentElement; + } + return null; } function flashHeading(el) { if (!el) return; - el.style.transition = 'background-color 0.3s ease'; - el.style.backgroundColor = 'var(--primary-light)'; - setTimeout(function () { - el.style.backgroundColor = ''; - setTimeout(function () { el.style.transition = ''; }, 300); - }, 1200); + el.classList.add('md-toc__flash'); + setTimeout(function () { el.classList.remove('md-toc__flash'); }, 900); } function renderToc(tocEl, content, editor) { if (!tocEl) return; + if (!content || !content.trim()) { + tocEl.innerHTML = '

Empty file.

'; + return; + } var headings = parseHeadings(content); - if (!content.trim()) { - tocEl.innerHTML = '

Empty file.

'; - return; - } if (headings.length === 0) { - tocEl.innerHTML = '

No headings.

'; + tocEl.innerHTML = '

No headings yet.

'; return; } - // Build a flat ordered list; CSS handles the visual indent. - var html = '