chore(embedded): cut v0.0.27-beta
Some checks failed
Notify chart dev on beta cut / notify-chart-dev (push) Failing after 9s

This commit is contained in:
ZDDC 2026-06-08 08:10:42 -05:00
parent 48b8199ff7
commit 242d25d55a
7 changed files with 167 additions and 47 deletions

View file

@ -2665,7 +2665,7 @@ td[data-field="trackingNumber"] {
</svg> </svg>
<div class="header-title-group"> <div class="header-title-group">
<span class="app-header__title">ZDDC Archive</span> <span class="app-header__title">ZDDC Archive</span>
<span class="build-timestamp"><span style="color:red;font-weight:bold">v0.0.27-beta · 2026-06-08 11:50:20 · 0d7feb3</span></span> <span class="build-timestamp"><span style="color:red;font-weight:bold">v0.0.27-beta · 2026-06-08 13:10:35 · 48b8199</span></span>
</div> </div>
<button id="addDirectoryBtn" class="btn btn-primary">Use Local Directory</button> <button id="addDirectoryBtn" class="btn btn-primary">Use Local Directory</button>
<button id="refreshHeaderBtn" class="btn btn-secondary hidden" title="Refresh Data"></button> <button id="refreshHeaderBtn" class="btn btn-secondary hidden" title="Refresh Data"></button>

View file

@ -2639,7 +2639,7 @@ body {
</svg> </svg>
<div class="header-title-group"> <div class="header-title-group">
<span class="app-header__title">ZDDC Browse</span> <span class="app-header__title">ZDDC Browse</span>
<span class="build-timestamp"><span style="color:red;font-weight:bold">v0.0.27-beta · 2026-06-08 11:50:21 · 0d7feb3</span></span> <span class="build-timestamp"><span style="color:red;font-weight:bold">v0.0.27-beta · 2026-06-08 13:10:36 · 48b8199</span></span>
</div> </div>
<button id="addDirectoryBtn" class="btn btn-primary">Use Local Directory</button> <button id="addDirectoryBtn" class="btn btn-primary">Use Local Directory</button>
<button id="refreshHeaderBtn" class="btn btn-secondary hidden" title="Refresh listing" aria-label="Refresh listing"></button> <button id="refreshHeaderBtn" class="btn btn-secondary hidden" title="Refresh listing" aria-label="Refresh listing"></button>
@ -10102,6 +10102,35 @@ var e=function(t,n){return e=Object.setPrototypeOf||{__proto__:[]}instanceof Arr
return '---\n' + fm + '\n---\n' + (body || ''); 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) ──────────────────────────────────────────── // ── TOC (table of contents) ────────────────────────────────────────────
// ATX headings only; the body markdown drives the outline. Clicking // ATX headings only; the body markdown drives the outline. Clicking
// a heading routes to whichever Toast UI pane is currently active // 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 = ''; fmTextarea.placeholder = '';
applyFrontMatterPlaceholder(fmTextarea); applyFrontMatterPlaceholder(fmTextarea);
fmBody.appendChild(fmTextarea); fmBody.appendChild(fmTextarea);
// Non-blocking warning shown when front matter disagrees with the // Rename cue: shown when the author edits an identity field
// canonical filename on an identity field (tracking_number / revision / // (tracking_number / revision / status / title) away from the
// status / title). The filename always wins in the rendered doc; this // filename. The filename owns identity, so the cue offers an explicit
// just tells the author their front-matter value is being ignored. // "Rename file & reopen" button rather than silently keeping or
// discarding the value. Populated by renderIdentityCue().
var fmWarn = document.createElement('div'); var fmWarn = document.createElement('div');
fmWarn.className = 'md-fm__warn'; fmWarn.className = 'md-fm__warn';
fmWarn.hidden = true; fmWarn.hidden = true;
fmWarn.style.cssText = 'color:#92400e;background:#fffbeb;border:1px solid ' fmWarn.style.cssText = 'color:#92400e;background:#fffbeb;border:1px solid '
+ '#fcd34d;border-radius:4px;padding:4px 8px;margin:0 0 4px;font-size:' + '#fcd34d;border-radius:4px;padding:6px 8px;margin:0 0 4px;font-size:'
+ '0.78rem;line-height:1.4;'; + '0.78rem;line-height:1.5;display:flex;flex-wrap:wrap;align-items:'
+ 'center;gap:6px;';
fmSection.appendChild(fmHeader); fmSection.appendChild(fmHeader);
fmSection.appendChild(fmWarn); fmSection.appendChild(fmWarn);
fmSection.appendChild(fmBody); 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" // bytes so that round-tripping a clean file shows "not dirty"
// even if we tweak whitespace in the YAML lines. // even if we tweak whitespace in the YAML lines.
var initialParsed = parseFrontMatter(text); var initialParsed = parseFrontMatter(text);
fmTextarea.value = stringifyFrontMatter(initialParsed.data);
var bodyText = initialParsed.body; var bodyText = initialParsed.body;
// On open, mirror the filename-derived identity into the front matter
var initialHash = await hashContent(assembleContent(fmTextarea.value, bodyText)); // (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); var writableMode = canSave(node);
// autofocus:false keeps the keyboard caret in the tree pane — // autofocus:false keeps the keyboard caret in the tree pane —
// arrow-key nav can continue through markdown files without // 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); }, 250);
editor.on('change', onChange); editor.on('change', onChange);
// Identity fields are sourced from the canonical ZDDC filename; setting // Build the filename the current FM identity edits imply: the parsed
// a different value in front matter is ignored at render (the filename // filename parts, overlaid with any non-empty identity values the
// wins). Surface a mismatch so the author isn't silently overridden. // author typed in the FM. "" when the name isn't ZDDC-conventional or
// Maps the front-matter key to the parseFilename field. // the result wouldn't be a valid ZDDC filename.
var IDENTITY_FIELDS = [ function renamedFilenameFromEdits(data) {
{ 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() {
var z = window.zddc; var z = window.zddc;
var fn = (z && z.parseFilename) ? z.parseFilename(node.name) : null; var fn = (z && z.parseFilename) ? z.parseFilename(node.name) : null;
// Only meaningful for a conventional ZDDC filename (it always has a if (!fn || !fn.valid || !z.formatFilename) return '';
// tracking number). Non-conventional files have no canonical var parts = {
// identity, so front matter is free — no warning. trackingNumber: fn.trackingNumber,
if (!fn || !fn.trackingNumber) { fmWarn.hidden = true; return; } 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 data = parseFrontMatter('---\n' + fmTextarea.value + '\n---\n').data || {};
var clashes = []; var edits = [];
IDENTITY_FIELDS.forEach(function (f) { IDENTITY_FIELDS.forEach(function (f) {
if (!(f.fm in data)) return; if (!(f.fm in data)) return;
var got = String(data[f.fm] == null ? '' : data[f.fm]).trim(); var got = String(data[f.fm] == null ? '' : data[f.fm]).trim();
var want = String(fn[f.fn] == null ? '' : fn[f.fn]).trim(); var want = String(fid[f.fm] == null ? '' : fid[f.fm]).trim();
if (got !== '' && want !== '' && got !== want) { if (got !== '' && got !== want) edits.push(f.label + ' → “' + got + '”');
clashes.push(f.label + ' “' + got + '” ≠ filename “' + want + '”');
}
}); });
if (!clashes.length) { fmWarn.hidden = true; fmWarn.textContent = ''; return; } if (!edits.length) { fmWarn.hidden = true; return; }
fmWarn.textContent = '⚠ Front matter disagrees with the filename (the ' var msg = document.createElement('span');
+ 'filename wins): ' + clashes.join('; ') + '.'; 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; 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 () { var onFmChange = debounce(async function () {
if (currentInstance !== instance) return; if (currentInstance !== instance) return;
var body = editor.getMarkdown(); var body = editor.getMarkdown();
var h = await hashContent(assembleContent(fmTextarea.value, body)); var h = await hashContent(assembleContent(fmTextarea.value, body));
if (currentInstance !== instance) return; if (currentInstance !== instance) return;
markDirty(h !== instance.hash); markDirty(h !== instance.hash);
checkFilenameMismatch(); renderIdentityCue();
}, 250); }, 250);
fmTextarea.addEventListener('input', onFmChange); 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 ─────────────────────────────────────────────────────────── // ── Save ───────────────────────────────────────────────────────────
// Mark a successful write: adopt the new server ETag (so the next // Mark a successful write: adopt the new server ETag (so the next

View file

@ -1876,7 +1876,7 @@ body.is-elevated::after {
</svg> </svg>
<div class="header-title-group"> <div class="header-title-group">
<span class="app-header__title">ZDDC Classifier</span> <span class="app-header__title">ZDDC Classifier</span>
<span class="build-timestamp"><span style="color:red;font-weight:bold">v0.0.27-beta · 2026-06-08 11:50:20 · 0d7feb3</span></span> <span class="build-timestamp"><span style="color:red;font-weight:bold">v0.0.27-beta · 2026-06-08 13:10:35 · 48b8199</span></span>
</div> </div>
<button id="addDirectoryBtn" class="btn btn-primary">Use Local Directory</button> <button id="addDirectoryBtn" class="btn btn-primary">Use Local Directory</button>
<button id="refreshHeaderBtn" class="btn btn-secondary hidden" title="Refresh and rescan directory" aria-label="Refresh" style="font-size:1.1rem;"></button> <button id="refreshHeaderBtn" class="btn btn-secondary hidden" title="Refresh and rescan directory" aria-label="Refresh" style="font-size:1.1rem;"></button>

View file

@ -1619,7 +1619,7 @@ body {
</svg> </svg>
<div class="header-title-group"> <div class="header-title-group">
<span class="app-header__title">ZDDC</span> <span class="app-header__title">ZDDC</span>
<span class="build-timestamp"><span style="color:red;font-weight:bold">v0.0.27-beta · 2026-06-08 11:50:20 · 0d7feb3</span></span> <span class="build-timestamp"><span style="color:red;font-weight:bold">v0.0.27-beta · 2026-06-08 13:10:35 · 48b8199</span></span>
</div> </div>
</div> </div>
<div class="header-right"> <div class="header-right">

View file

@ -2718,7 +2718,7 @@ dialog.modal--narrow {
</svg> </svg>
<div class="header-title-group"> <div class="header-title-group">
<span class="app-header__title">ZDDC Transmittal</span> <span class="app-header__title">ZDDC Transmittal</span>
<span class="build-timestamp"><span style="color:red;font-weight:bold">v0.0.27-beta · 2026-06-08 11:50:20 · 0d7feb3</span></span> <span class="build-timestamp"><span style="color:red;font-weight:bold">v0.0.27-beta · 2026-06-08 13:10:35 · 48b8199</span></span>
</div> </div>
<span id="no-js-notice" class="text-gray-400 text-xs italic">JavaScript not available</span> <span id="no-js-notice" class="text-gray-400 text-xs italic">JavaScript not available</span>
<!-- Publish split-button (Transmittal-specific primary action; <!-- Publish split-button (Transmittal-specific primary action;

View file

@ -1,8 +1,8 @@
# Generated by build.sh — do not edit. One <app>=<build label> per line. # Generated by build.sh — do not edit. One <app>=<build label> per line.
archive=v0.0.27-beta · 2026-06-08 11:50:20 · 0d7feb3 archive=v0.0.27-beta · 2026-06-08 13:10:35 · 48b8199
transmittal=v0.0.27-beta · 2026-06-08 11:50:20 · 0d7feb3 transmittal=v0.0.27-beta · 2026-06-08 13:10:35 · 48b8199
classifier=v0.0.27-beta · 2026-06-08 11:50:20 · 0d7feb3 classifier=v0.0.27-beta · 2026-06-08 13:10:35 · 48b8199
landing=v0.0.27-beta · 2026-06-08 11:50:20 · 0d7feb3 landing=v0.0.27-beta · 2026-06-08 13:10:35 · 48b8199
form=v0.0.27-beta · 2026-06-08 11:50:20 · 0d7feb3 form=v0.0.27-beta · 2026-06-08 13:10:35 · 48b8199
tables=v0.0.27-beta · 2026-06-08 11:50:21 · 0d7feb3 tables=v0.0.27-beta · 2026-06-08 13:10:35 · 48b8199
browse=v0.0.27-beta · 2026-06-08 11:50:21 · 0d7feb3 browse=v0.0.27-beta · 2026-06-08 13:10:36 · 48b8199

View file

@ -1648,7 +1648,7 @@ body.is-elevated::after {
</svg> </svg>
<div class="header-title-group"> <div class="header-title-group">
<span class="app-header__title" id="table-title">ZDDC Table</span> <span class="app-header__title" id="table-title">ZDDC Table</span>
<span class="build-timestamp"><span style="color:red;font-weight:bold">v0.0.27-beta · 2026-06-08 11:50:21 · 0d7feb3</span></span> <span class="build-timestamp"><span style="color:red;font-weight:bold">v0.0.27-beta · 2026-06-08 13:10:35 · 48b8199</span></span>
</div> </div>
</div> </div>
<div class="header-right"> <div class="header-right">