From 242d25d55acb2611ff531eb799023f30a675850e Mon Sep 17 00:00:00 2001 From: ZDDC Date: Mon, 8 Jun 2026 08:10:42 -0500 Subject: [PATCH] chore(embedded): cut v0.0.27-beta --- zddc/internal/apps/embedded/archive.html | 2 +- zddc/internal/apps/embedded/browse.html | 190 +++++++++++++++---- zddc/internal/apps/embedded/classifier.html | 2 +- zddc/internal/apps/embedded/index.html | 2 +- zddc/internal/apps/embedded/transmittal.html | 2 +- zddc/internal/apps/embedded/versions.txt | 14 +- zddc/internal/handler/tables.html | 2 +- 7 files changed, 167 insertions(+), 47 deletions(-) diff --git a/zddc/internal/apps/embedded/archive.html b/zddc/internal/apps/embedded/archive.html index 2f5a2ff..f202881 100644 --- a/zddc/internal/apps/embedded/archive.html +++ b/zddc/internal/apps/embedded/archive.html @@ -2665,7 +2665,7 @@ td[data-field="trackingNumber"] {
ZDDC Archive - v0.0.27-beta · 2026-06-08 11:50:20 · 0d7feb3 + v0.0.27-beta · 2026-06-08 13:10:35 · 48b8199
diff --git a/zddc/internal/apps/embedded/browse.html b/zddc/internal/apps/embedded/browse.html index dd6b08a..3c5cce3 100644 --- a/zddc/internal/apps/embedded/browse.html +++ b/zddc/internal/apps/embedded/browse.html @@ -2639,7 +2639,7 @@ body {
ZDDC Browse - v0.0.27-beta · 2026-06-08 11:50:21 · 0d7feb3 + v0.0.27-beta · 2026-06-08 13:10:36 · 48b8199
@@ -10102,6 +10102,35 @@ var e=function(t,n){return e=Object.setPrototypeOf||{__proto__:[]}instanceof Arr return '---\n' + fm + '\n---\n' + (body || ''); } + // ── ZDDC identity (filename is the single source of truth) ────────────── + // The four identity fields live in the filename. They're mirrored into the + // front matter so the converter (which reads FM) sees them, but the + // filename always wins: on open we sync FM ← filename, and editing one in + // the FM is treated as a cue to RENAME the file (see renderIdentityCue), + // never a silent value change. Maps the FM key ↔ the parseFilename field. + var IDENTITY_FIELDS = [ + { fm: 'title', fn: 'title', label: 'title' }, + { fm: 'tracking_number', fn: 'trackingNumber', label: 'tracking number' }, + { fm: 'revision', fn: 'revision', label: 'revision' }, + { fm: 'status', fn: 'status', label: 'status' } + ]; + + // parseFilename → {title, tracking_number, revision, status} (non-empty + // fields only), or null when the name isn't a conventional ZDDC filename + // (no canonical identity — the editor stays fully usable on arbitrary + // directories, where FM is the sole source). + function filenameIdentity(filename) { + var z = window.zddc; + var fn = (z && z.parseFilename) ? z.parseFilename(filename) : null; + if (!fn || !fn.trackingNumber) return null; + var out = {}; + IDENTITY_FIELDS.forEach(function (f) { + var v = fn[f.fn]; + if (v != null && String(v).trim() !== '') out[f.fm] = v; + }); + return out; + } + // ── 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 @@ -10344,16 +10373,18 @@ var e=function(t,n){return e=Object.setPrototypeOf||{__proto__:[]}instanceof Arr fmTextarea.placeholder = ''; applyFrontMatterPlaceholder(fmTextarea); fmBody.appendChild(fmTextarea); - // Non-blocking warning shown when front matter disagrees with the - // canonical filename on an identity field (tracking_number / revision / - // status / title). The filename always wins in the rendered doc; this - // just tells the author their front-matter value is being ignored. + // Rename cue: shown when the author edits an identity field + // (tracking_number / revision / status / title) away from the + // filename. The filename owns identity, so the cue offers an explicit + // "Rename file & reopen" button rather than silently keeping or + // discarding the value. Populated by renderIdentityCue(). var fmWarn = document.createElement('div'); fmWarn.className = 'md-fm__warn'; fmWarn.hidden = true; fmWarn.style.cssText = 'color:#92400e;background:#fffbeb;border:1px solid ' - + '#fcd34d;border-radius:4px;padding:4px 8px;margin:0 0 4px;font-size:' - + '0.78rem;line-height:1.4;'; + + '#fcd34d;border-radius:4px;padding:6px 8px;margin:0 0 4px;font-size:' + + '0.78rem;line-height:1.5;display:flex;flex-wrap:wrap;align-items:' + + 'center;gap:6px;'; fmSection.appendChild(fmHeader); fmSection.appendChild(fmWarn); fmSection.appendChild(fmBody); @@ -10498,10 +10529,21 @@ var e=function(t,n){return e=Object.setPrototypeOf||{__proto__:[]}instanceof Arr // bytes so that round-tripping a clean file shows "not dirty" // even if we tweak whitespace in the YAML lines. var initialParsed = parseFrontMatter(text); - fmTextarea.value = stringifyFrontMatter(initialParsed.data); var bodyText = initialParsed.body; - - var initialHash = await hashContent(assembleContent(fmTextarea.value, bodyText)); + // On open, mirror the filename-derived identity into the front matter + // (the filename is the single source of truth; this keeps the values + // baked in for the converter). No-op for non-ZDDC filenames. The dirty + // baseline stays the ON-DISK state, so a correction opens the buffer + // dirty and a save persists it. + var onDiskFM = stringifyFrontMatter(initialParsed.data); + var fid = filenameIdentity(node.name); + if (fid) { + for (var ik in fid) { + if (Object.prototype.hasOwnProperty.call(fid, ik)) initialParsed.data[ik] = fid[ik]; + } + } + fmTextarea.value = stringifyFrontMatter(initialParsed.data); + var initialHash = await hashContent(assembleContent(onDiskFM, bodyText)); var writableMode = canSave(node); // autofocus:false keeps the keyboard caret in the tree pane — // arrow-key nav can continue through markdown files without @@ -10680,49 +10722,127 @@ var e=function(t,n){return e=Object.setPrototypeOf||{__proto__:[]}instanceof Arr }, 250); editor.on('change', onChange); - // Identity fields are sourced from the canonical ZDDC filename; setting - // a different value in front matter is ignored at render (the filename - // wins). Surface a mismatch so the author isn't silently overridden. - // Maps the front-matter key to the parseFilename field. - var IDENTITY_FIELDS = [ - { fm: 'title', fn: 'title', label: 'title' }, - { fm: 'tracking_number', fn: 'trackingNumber', label: 'tracking number' }, - { fm: 'revision', fn: 'revision', label: 'revision' }, - { fm: 'status', fn: 'status', label: 'status' } - ]; - function checkFilenameMismatch() { + // Build the filename the current FM identity edits imply: the parsed + // filename parts, overlaid with any non-empty identity values the + // author typed in the FM. "" when the name isn't ZDDC-conventional or + // the result wouldn't be a valid ZDDC filename. + function renamedFilenameFromEdits(data) { var z = window.zddc; var fn = (z && z.parseFilename) ? z.parseFilename(node.name) : null; - // Only meaningful for a conventional ZDDC filename (it always has a - // tracking number). Non-conventional files have no canonical - // identity, so front matter is free — no warning. - if (!fn || !fn.trackingNumber) { fmWarn.hidden = true; return; } + if (!fn || !fn.valid || !z.formatFilename) return ''; + var parts = { + trackingNumber: fn.trackingNumber, + revision: fn.revision, + status: fn.status, + title: fn.title, + extension: fn.extension + }; + IDENTITY_FIELDS.forEach(function (f) { + if (!(f.fm in data)) return; + var v = String(data[f.fm] == null ? '' : data[f.fm]).trim(); + if (v !== '') parts[f.fn] = v; + }); + try { return z.formatFilename(parts); } catch (_e) { return ''; } + } + + // The filename owns identity, so a manual edit to an identity field is + // a cue to RENAME the file, not a value to keep. When the FM identity + // differs from the filename, surface that + an explicit "Rename file & + // reopen" button. No-op for non-ZDDC names (no canonical identity). + function renderIdentityCue() { + while (fmWarn.firstChild) fmWarn.removeChild(fmWarn.firstChild); + var fid = filenameIdentity(node.name); + if (!fid || !canSave(node)) { fmWarn.hidden = true; return; } var data = parseFrontMatter('---\n' + fmTextarea.value + '\n---\n').data || {}; - var clashes = []; + var edits = []; IDENTITY_FIELDS.forEach(function (f) { if (!(f.fm in data)) return; var got = String(data[f.fm] == null ? '' : data[f.fm]).trim(); - var want = String(fn[f.fn] == null ? '' : fn[f.fn]).trim(); - if (got !== '' && want !== '' && got !== want) { - clashes.push(f.label + ' “' + got + '” ≠ filename “' + want + '”'); - } + var want = String(fid[f.fm] == null ? '' : fid[f.fm]).trim(); + if (got !== '' && got !== want) edits.push(f.label + ' → “' + got + '”'); }); - if (!clashes.length) { fmWarn.hidden = true; fmWarn.textContent = ''; return; } - fmWarn.textContent = '⚠ Front matter disagrees with the filename (the ' - + 'filename wins): ' + clashes.join('; ') + '.'; + if (!edits.length) { fmWarn.hidden = true; return; } + var msg = document.createElement('span'); + msg.textContent = '✎ Identity comes from the filename. You changed ' + + edits.join(', ') + '. '; + fmWarn.appendChild(msg); + var newName = renamedFilenameFromEdits(data); + if (newName && newName !== node.name) { + var btn = document.createElement('button'); + btn.type = 'button'; + btn.className = 'btn btn-sm md-fm__rename'; + btn.textContent = 'Rename file & reopen'; + btn.title = 'Save, rename to “' + newName + '”, and reopen it for editing.'; + btn.addEventListener('click', function () { renameToMatch(newName); }); + fmWarn.appendChild(btn); + } fmWarn.hidden = false; } + // Rename action: persist the current buffer (so body edits aren't + // lost), rename the file to match the edited identity, then reopen the + // new name fresh. Server mode reopens via the ?file deep-link (reuses + // the tested open-by-path walker); FS-Access mode reuses the moved + // handle in place. + async function renameToMatch(newName) { + var up = window.app.modules.upload; + if (!up || !up.renameNode || !newName) return; + // 1. Save first so body/FM edits survive the rename. A failed save + // (conflict, ACL) leaves the buffer dirty — abort the rename. + if (instance.dirty) { + await save(); + if (currentInstance !== instance || instance.dirty) return; + } + // 2. Rename on disk. + try { + statusEl.textContent = 'Renaming…'; + await up.renameNode(node, newName); + } catch (e) { + statusEl.textContent = ''; + if (window.zddc && window.zddc.toast) { + window.zddc.toast('Rename failed: ' + (e.message || e), 'error'); + } + return; + } + // Drop the dirty guard so the reopen/navigation isn't blocked. + markDirty(false); instance.dirty = false; + // 3. Reopen the renamed file. + if (window.app.state.source === 'server') { + var tree = window.app.modules.tree; + var scope = (window.app.state.currentPath || '/').replace(/\/$/, '') + '/'; + var oldPath = tree ? tree.pathFor(node) : ''; + var dir = oldPath.slice(0, oldPath.lastIndexOf('/') + 1); + var newPath = dir + newName; + var rel = newPath.indexOf(scope) === 0 ? newPath.slice(scope.length) : newName; + var params = new URLSearchParams(); + params.set('file', rel); + if (window.app.state.showHidden) params.set('hidden', '1'); + window.location.assign(scope + '?' + params.toString()); + return; + } + // FS-Access: the handle was renamed in place. Update the node, + // refresh the tree, reopen the editor on it. + node.name = newName; + var ev = window.app.modules.events; + if (ev && ev.refreshListing) { try { await ev.refreshListing(); } catch (_e) { /* swallow */ } } + var prev = window.app.modules.preview; + if (prev && prev.showFilePreview) prev.showFilePreview(node); + } + var onFmChange = debounce(async function () { if (currentInstance !== instance) return; var body = editor.getMarkdown(); var h = await hashContent(assembleContent(fmTextarea.value, body)); if (currentInstance !== instance) return; markDirty(h !== instance.hash); - checkFilenameMismatch(); + renderIdentityCue(); }, 250); fmTextarea.addEventListener('input', onFmChange); - checkFilenameMismatch(); // initial state on load + renderIdentityCue(); // initial state on load (clean after sync-on-open) + + // If sync-on-open corrected the front matter, open the buffer dirty so + // a save bakes the filename-derived identity in. + if (writableMode && fmTextarea.value !== onDiskFM) markDirty(true); // ── Save ─────────────────────────────────────────────────────────── // Mark a successful write: adopt the new server ETag (so the next diff --git a/zddc/internal/apps/embedded/classifier.html b/zddc/internal/apps/embedded/classifier.html index b7e32f5..5e7663d 100644 --- a/zddc/internal/apps/embedded/classifier.html +++ b/zddc/internal/apps/embedded/classifier.html @@ -1876,7 +1876,7 @@ body.is-elevated::after {
ZDDC Classifier - v0.0.27-beta · 2026-06-08 11:50:20 · 0d7feb3 + v0.0.27-beta · 2026-06-08 13:10:35 · 48b8199
diff --git a/zddc/internal/apps/embedded/index.html b/zddc/internal/apps/embedded/index.html index c1c65f8..2ff893a 100644 --- a/zddc/internal/apps/embedded/index.html +++ b/zddc/internal/apps/embedded/index.html @@ -1619,7 +1619,7 @@ body {
ZDDC - v0.0.27-beta · 2026-06-08 11:50:20 · 0d7feb3 + v0.0.27-beta · 2026-06-08 13:10:35 · 48b8199
diff --git a/zddc/internal/apps/embedded/transmittal.html b/zddc/internal/apps/embedded/transmittal.html index ce6236b..2d94619 100644 --- a/zddc/internal/apps/embedded/transmittal.html +++ b/zddc/internal/apps/embedded/transmittal.html @@ -2718,7 +2718,7 @@ dialog.modal--narrow {
ZDDC Transmittal - v0.0.27-beta · 2026-06-08 11:50:20 · 0d7feb3 + v0.0.27-beta · 2026-06-08 13:10:35 · 48b8199
JavaScript not available