From 89d96b784f1b75bf988d90012f1724fc489aeb84 Mon Sep 17 00:00:00 2001 From: ZDDC Date: Sun, 10 May 2026 19:02:43 -0500 Subject: [PATCH] chore(embedded): cut v0.0.17-beta --- zddc/internal/apps/embedded/archive.html | 2 +- zddc/internal/apps/embedded/browse.html | 419 ++++++++++++++++--- zddc/internal/apps/embedded/classifier.html | 2 +- zddc/internal/apps/embedded/index.html | 2 +- zddc/internal/apps/embedded/mdedit.html | 2 +- zddc/internal/apps/embedded/transmittal.html | 2 +- zddc/internal/apps/embedded/versions.txt | 16 +- zddc/internal/handler/tables.html | 2 +- 8 files changed, 379 insertions(+), 68 deletions(-) diff --git a/zddc/internal/apps/embedded/archive.html b/zddc/internal/apps/embedded/archive.html index 468d8cc..5c9ec50 100644 --- a/zddc/internal/apps/embedded/archive.html +++ b/zddc/internal/apps/embedded/archive.html @@ -2316,7 +2316,7 @@ td[data-field="trackingNumber"] {
ZDDC Archive - v0.0.17-beta · 2026-05-10 · diamond-flame-kettle + v0.0.17-beta · 2026-05-11 · brass-dolphin-ivory
diff --git a/zddc/internal/apps/embedded/browse.html b/zddc/internal/apps/embedded/browse.html index 742ab91..f5f94e1 100644 --- a/zddc/internal/apps/embedded/browse.html +++ b/zddc/internal/apps/embedded/browse.html @@ -1137,6 +1137,140 @@ html, body { .status-bar.is-error { color: var(--danger); } .status-bar.is-info { color: var(--text); } +/* ── Markdown plugin (right-pane internals when a .md is selected) ──────── */ + +/* Editor toolbar (above the editor+TOC split): Save + dirty marker + + status + source hint. Sticks to the top of the pane body. */ +.md-toolbar { + display: flex; + align-items: center; + gap: 0.5rem; + padding: 0.35rem 0.75rem; + background: var(--bg-secondary); + border-bottom: 1px solid var(--border); + flex-shrink: 0; + font-size: 0.85rem; +} + +.md-toolbar__dirty { + color: var(--text-muted); + font-size: 0.85rem; + min-width: 6rem; +} + +.md-toolbar__status { + flex: 1; + text-align: right; + color: var(--text-muted); + font-size: 0.85rem; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.md-toolbar__source { + color: var(--text-muted); + font-size: 0.75rem; + font-style: italic; + margin-left: 0.5rem; + padding: 0.15rem 0.4rem; + border-radius: var(--radius); + background: var(--bg); + border: 1px solid var(--border); +} + +/* Editor + TOC two-pane split inside the preview body. */ +.md-split { + flex: 1; + display: flex; + flex-direction: row; + min-height: 0; + overflow: hidden; +} + +.md-editor-host { + flex: 1; + min-width: 0; + min-height: 0; + overflow: hidden; +} + +/* TOC pane sits on the right. Fixed width by default; the user can't + resize it (yet) — kept simple in v1. */ +.md-toc-pane { + width: 220px; + flex-shrink: 0; + border-left: 1px solid var(--border); + background: var(--bg); + display: flex; + flex-direction: column; + overflow: hidden; +} + +.md-toc-pane__header { + padding: 0.35rem 0.75rem; + background: var(--bg-secondary); + border-bottom: 1px solid var(--border); + font-size: 0.75rem; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.06em; + color: var(--text-muted); + flex-shrink: 0; +} + +.md-toc-pane__body { + flex: 1; + overflow-y: auto; + padding: 0.4rem 0; + font-size: 0.85rem; + line-height: 1.4; +} + +.toc-empty { + color: var(--text-muted); + font-style: italic; + padding: 0.5rem 0.75rem; + margin: 0; + font-size: 0.85rem; +} + +.toc-list { + list-style: none; + margin: 0; + padding: 0; +} + +.toc-item { + margin: 0; +} + +.toc-item a { + display: block; + padding: 0.2rem 0.75rem; + text-decoration: none; + color: var(--text); + border-left: 2px solid transparent; + transition: background 0.1s, border-color 0.1s; +} + +.toc-item a:hover { + background: var(--bg-hover); + border-left-color: var(--primary); +} + +.toc-item a:focus-visible { + outline: 2px solid var(--primary); + outline-offset: -2px; +} + +.toc-level-1 a { padding-left: 0.75rem; font-weight: 600; } +.toc-level-2 a { padding-left: 1.4rem; } +.toc-level-3 a { padding-left: 2.05rem; } +.toc-level-4 a { padding-left: 2.7rem; color: var(--text-muted); } +.toc-level-5 a { padding-left: 3.35rem; color: var(--text-muted); font-size: 0.8rem; } +.toc-level-6 a { padding-left: 4rem; color: var(--text-muted); font-size: 0.8rem; } + @@ -1152,7 +1286,7 @@ html, body {
ZDDC Browse - v0.0.17-beta · 2026-05-10 · diamond-flame-kettle + v0.0.17-beta · 2026-05-11 · brass-dolphin-ivory
@@ -5051,13 +5185,17 @@ 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. Save (Ctrl+S) writes back via PUT -// when the file came from a server URL; FS-API and zip-virtual files -// are read-only for now (toolbar shows a hint). +// 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 loaded from mdedit's bundled vendor file in the -// browse build (see browse/build.sh). window.toastui is available -// synchronously when this module runs. +// 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. (function () { 'use strict'; @@ -5068,11 +5206,10 @@ var e=function(t,n){return e=Object.setPrototypeOf||{__proto__:[]}instanceof Arr .replace(/>/g, '>').replace(/"/g, '"'); } - var currentInstance = null; // { editor, container, dirty, node, hash } + var currentInstance = null; // { editor, container, dirty, node, hash, tocEl } - // Compute SHA-256 hex of a string for a quick "is this content - // different from what was loaded?" check. Used to decide whether - // the save button should be active. Not used for integrity. + // 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); @@ -5092,6 +5229,152 @@ 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. + + function parseHeadings(content) { + var headings = []; + var lines = content.split('\n'); + for (var i = 0; i < lines.length; i++) { + var m = lines[i].match(/^(#{1,6})\s+(.+)$/); + if (!m) continue; + var text = m[2].trim() + .replace(/\\(.)/g, '$1') + .replace(/\*\*(.*?)\*\*/g, '$1') + .replace(/\*(.*?)\*/g, '$1') + .replace(/`(.*?)`/g, '$1') + .replace(/\[(.*?)\]\(.*?\)/g, '$1') + .replace(/~~(.*?)~~/g, '$1') + .trim(); + headings.push({ level: m[1].length, text: text, lineIndex: i }); + } + return headings; + } + + function scrollEditorToHeading(editor, heading) { + try { + var els = editor.getEditorElements(); + if (editor.isWysiwygMode && editor.isWysiwygMode()) { + var ww = els.wwEditor; + if (!ww) return; + 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; + flashHeading(hs[i]); + return; + } + } + } else { + var line = heading.lineIndex + 1; + try { editor.setSelection([line, 1], [line, 1]); } catch (_) { /* ignore */ } + 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; + } + } + } + } catch (e) { /* swallow; click was best-effort */ } + } + + 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); + } + + function renderToc(tocEl, content, editor) { + if (!tocEl) return; + var headings = parseHeadings(content); + if (!content.trim()) { + tocEl.innerHTML = '

Empty file.

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

No headings.

'; + return; + } + // Build a flat ordered list; CSS handles the visual indent. + var html = ''; + tocEl.innerHTML = html; + + // One delegated click handler. + tocEl.querySelectorAll('.toc-item').forEach(function (li) { + li.addEventListener('click', function (e) { + e.preventDefault(); + var idx = parseInt(li.dataset.line, 10); + var text = li.dataset.text; + scrollEditorToHeading(editor, { text: text, lineIndex: idx }); + }); + }); + } + + // Light debounce so TOC doesn't rebuild on every keystroke. + function debounce(fn, ms) { + var t; + return function () { + clearTimeout(t); + var args = arguments, self = this; + t = setTimeout(function () { fn.apply(self, args); }, ms); + }; + } + + // ── Save (server + FS-API) ────────────────────────────────────────────── + + async function saveContent(node, content) { + // FS-API mode: write via the local file handle. + if (node.handle && typeof node.handle.createWritable === 'function') { + var writable = await node.handle.createWritable(); + await writable.write(content); + await writable.close(); + return; + } + // Server mode: PUT the new bytes. + if (node.url && window.app.state.source === 'server') { + var resp = await fetch(node.url, { + method: 'PUT', + headers: { 'Content-Type': 'text/markdown; charset=utf-8' }, + body: content, + credentials: 'same-origin' + }); + if (!resp.ok) { + throw new Error('HTTP ' + resp.status); + } + return; + } + throw new Error('No write target for this file (read-only source).'); + } + + function canSave(node) { + if (node.zipParentId != null) return false; + if (node.handle && typeof node.handle.createWritable === 'function') return true; + if (node.url && window.app.state.source === 'server') return true; + return false; + } + + // ── Mount ─────────────────────────────────────────────────────────────── + async function render(node, container, ctx) { if (typeof window.toastui === 'undefined') { container.innerHTML = @@ -5100,10 +5383,9 @@ var e=function(t,n){return e=Object.setPrototypeOf||{__proto__:[]}instanceof Arr return; } - // Tear down any previous markdown instance (single-file model). dispose(); - // Read the file content. + // Read content. var text; try { var buf = await ctx.getArrayBuffer(node); @@ -5117,45 +5399,65 @@ var e=function(t,n){return e=Object.setPrototypeOf||{__proto__:[]}instanceof Arr } // Build the markdown plugin's DOM: - // ┌──────────────────────────────────┐ - // │ toolbar (Save, dirty marker) │ - // ├──────────────────────────────────┤ - // │ Toast UI editor │ - // └──────────────────────────────────┘ - // - // TOC pane is deferred — a near-term iteration can split this - // into editor | toc once the simpler form is exercised. + // ┌──────────────────────────────────────────────────┐ + // │ toolbar (Save, ● modified, status, source hint) │ + // ├──────────────────────────────────┬───────────────┤ + // │ editor (Toast UI) │ TOC pane │ + // └──────────────────────────────────┴───────────────┘ container.innerHTML = ''; container.style.display = 'flex'; container.style.flexDirection = 'column'; var toolbar = document.createElement('div'); toolbar.className = 'md-toolbar'; - toolbar.style.cssText = 'display:flex;align-items:center;gap:0.5rem;' - + 'padding:0.35rem 0.75rem;background:var(--bg-secondary);' - + 'border-bottom:1px solid var(--border);flex-shrink:0;'; var saveBtn = document.createElement('button'); saveBtn.className = 'btn btn-sm btn-primary'; saveBtn.type = 'button'; saveBtn.textContent = 'Save'; - saveBtn.disabled = true; // enabled when content changes + saveBtn.disabled = true; var dirty = document.createElement('span'); - dirty.style.cssText = 'color:var(--text-muted);font-size:0.85rem;'; - dirty.textContent = ''; + dirty.className = 'md-toolbar__dirty'; var status = document.createElement('span'); - status.style.cssText = 'flex:1;text-align:right;color:var(--text-muted);font-size:0.85rem;'; + status.className = 'md-toolbar__status'; + + var sourceHint = document.createElement('span'); + sourceHint.className = 'md-toolbar__source'; + if (node.zipParentId != null) { + sourceHint.textContent = 'read-only (inside zip)'; + } else if (node.handle) { + sourceHint.textContent = 'local'; + } else if (node.url) { + sourceHint.textContent = 'server'; + } toolbar.appendChild(saveBtn); toolbar.appendChild(dirty); toolbar.appendChild(status); + toolbar.appendChild(sourceHint); container.appendChild(toolbar); + var split = document.createElement('div'); + split.className = 'md-split'; + container.appendChild(split); + var editorHost = document.createElement('div'); - editorHost.style.cssText = 'flex:1;min-height:0;overflow:hidden;'; - container.appendChild(editorHost); + editorHost.className = 'md-editor-host'; + split.appendChild(editorHost); + + var tocPane = document.createElement('div'); + tocPane.className = 'md-toc-pane'; + var tocHeader = document.createElement('div'); + tocHeader.className = 'md-toc-pane__header'; + tocHeader.textContent = 'Outline'; + var tocBody = document.createElement('div'); + tocBody.className = 'md-toc-pane__body'; + tocBody.innerHTML = '

Loading…

'; + tocPane.appendChild(tocHeader); + tocPane.appendChild(tocBody); + split.appendChild(tocPane); var initialHash = await hashContent(text); var editor = new window.toastui.Editor({ @@ -5174,47 +5476,56 @@ var e=function(t,n){return e=Object.setPrototypeOf||{__proto__:[]}instanceof Arr ] }); - currentInstance = { editor: editor, container: container, dirty: false, node: node, hash: initialHash }; + currentInstance = { + editor: editor, + container: container, + dirty: false, + node: node, + hash: initialHash, + tocEl: tocBody + }; + + var writable = canSave(node); + if (!writable) { + saveBtn.disabled = true; + saveBtn.title = 'Save not available — read-only source.'; + } + + renderToc(tocBody, text, editor); function markDirty(isDirty) { currentInstance.dirty = isDirty; - saveBtn.disabled = !isDirty; + saveBtn.disabled = !isDirty || !writable; dirty.textContent = isDirty ? '● modified' : ''; } - editor.on('change', async function () { + var updateOnChange = debounce(async function () { var current = editor.getMarkdown(); var h = await hashContent(current); markDirty(h !== currentInstance.hash); - }); + renderToc(tocBody, current, editor); + }, 250); + + editor.on('change', updateOnChange); async function save() { - if (!currentInstance.dirty) return; + if (!currentInstance.dirty || !writable) return; var content = editor.getMarkdown(); - // Read-only sources: zip-virtual, FS-API without write - // permission. For now we only attempt PUT against server URLs; - // FS-API saves can be wired in a later iteration via the - // existing zddc-source polyfill. - if (!node.url || window.app.state.source !== 'server') { - status.textContent = 'Save not yet supported for this source.'; - return; - } try { status.textContent = 'Saving…'; - var resp = await fetch(node.url, { - method: 'PUT', - headers: { 'Content-Type': 'text/markdown; charset=utf-8' }, - body: content, - credentials: 'same-origin' - }); - if (!resp.ok) { - throw new Error('HTTP ' + resp.status); - } + await saveContent(node, content); currentInstance.hash = await hashContent(content); markDirty(false); - status.textContent = 'Saved ' + new Date().toLocaleTimeString(); + var now = new Date(); + status.textContent = 'Saved ' + now.toLocaleTimeString(); + if (window.zddc && window.zddc.toast) { + window.zddc.toast('Saved ' + node.name, 'success'); + } } catch (e) { status.textContent = 'Save failed: ' + (e.message || e); + if (window.zddc && window.zddc.toast) { + window.zddc.toast('Save failed: ' + (e.message || e), 'error'); + } } } @@ -5222,7 +5533,7 @@ var e=function(t,n){return e=Object.setPrototypeOf||{__proto__:[]}instanceof Arr // Ctrl+S / Cmd+S inside the editor → save. container.addEventListener('keydown', function (e) { - if ((e.ctrlKey || e.metaKey) && e.key === 's') { + if ((e.ctrlKey || e.metaKey) && (e.key === 's' || e.key === 'S')) { e.preventDefault(); save(); } diff --git a/zddc/internal/apps/embedded/classifier.html b/zddc/internal/apps/embedded/classifier.html index 88a6724..1761c48 100644 --- a/zddc/internal/apps/embedded/classifier.html +++ b/zddc/internal/apps/embedded/classifier.html @@ -1536,7 +1536,7 @@ body.help-open .app-header {
ZDDC Classifier - v0.0.17-beta · 2026-05-10 · diamond-flame-kettle + v0.0.17-beta · 2026-05-11 · brass-dolphin-ivory
diff --git a/zddc/internal/apps/embedded/index.html b/zddc/internal/apps/embedded/index.html index 262a765..d2a3978 100644 --- a/zddc/internal/apps/embedded/index.html +++ b/zddc/internal/apps/embedded/index.html @@ -1225,7 +1225,7 @@ body {
ZDDC - v0.0.17-beta · 2026-05-10 · diamond-flame-kettle + v0.0.17-beta · 2026-05-11 · brass-dolphin-ivory
diff --git a/zddc/internal/apps/embedded/mdedit.html b/zddc/internal/apps/embedded/mdedit.html index c9c2cef..db4c002 100644 --- a/zddc/internal/apps/embedded/mdedit.html +++ b/zddc/internal/apps/embedded/mdedit.html @@ -2010,7 +2010,7 @@ body.help-open .app-header {
ZDDC Markdown - v0.0.17-beta · 2026-05-10 · diamond-flame-kettle + v0.0.17-beta · 2026-05-11 · brass-dolphin-ivory
diff --git a/zddc/internal/apps/embedded/transmittal.html b/zddc/internal/apps/embedded/transmittal.html index ef2afe4..c91a1d4 100644 --- a/zddc/internal/apps/embedded/transmittal.html +++ b/zddc/internal/apps/embedded/transmittal.html @@ -2378,7 +2378,7 @@ dialog.modal--narrow {
ZDDC Transmittal - v0.0.17-beta · 2026-05-10 · diamond-flame-kettle + v0.0.17-beta · 2026-05-11 · brass-dolphin-ivory
JavaScript not available