chore(embedded): cut v0.0.17-beta

This commit is contained in:
ZDDC 2026-05-10 19:02:43 -05:00
parent 0b382716e3
commit 89d96b784f
8 changed files with 379 additions and 68 deletions

View file

@ -2316,7 +2316,7 @@ td[data-field="trackingNumber"] {
</svg>
<div class="header-title-group">
<span class="app-header__title">ZDDC Archive</span>
<span class="build-timestamp"><span style="color:red;font-weight:bold">v0.0.17-beta · 2026-05-10 · diamond-flame-kettle</span></span>
<span class="build-timestamp"><span style="color:red;font-weight:bold">v0.0.17-beta · 2026-05-11 · brass-dolphin-ivory</span></span>
</div>
<button id="addDirectoryBtn" class="btn btn-primary">Add Local Directory</button>
<button id="refreshHeaderBtn" class="btn btn-secondary hidden" title="Refresh Data"></button>

View file

@ -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; }
</style>
</head>
<body>
@ -1152,7 +1286,7 @@ html, body {
</svg>
<div class="header-title-group">
<span class="app-header__title">ZDDC Browse</span>
<span class="build-timestamp"><span style="color:red;font-weight:bold">v0.0.17-beta · 2026-05-10 · diamond-flame-kettle</span></span>
<span class="build-timestamp"><span style="color:red;font-weight:bold">v0.0.17-beta · 2026-05-11 · brass-dolphin-ivory</span></span>
</div>
<button id="addDirectoryBtn" class="btn btn-primary">Add Local Directory</button>
<button id="refreshHeaderBtn" class="btn btn-secondary hidden" title="Refresh listing" aria-label="Refresh listing"></button>
@ -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, '&gt;').replace(/"/g, '&quot;');
}
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 = '<p class="toc-empty">Empty file.</p>';
return;
}
if (headings.length === 0) {
tocEl.innerHTML = '<p class="toc-empty">No headings.</p>';
return;
}
// Build a flat ordered list; CSS handles the visual indent.
var html = '<ul class="toc-list">';
for (var i = 0; i < headings.length; i++) {
var h = headings[i];
html += '<li class="toc-item toc-level-' + h.level + '" data-line="' + h.lineIndex + '" data-text="' + escapeHtml(h.text) + '">'
+ '<a href="#" tabindex="0">' + escapeHtml(h.text) + '</a></li>';
}
html += '</ul>';
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 = '<p class="toc-empty">Loading…</p>';
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();
}

View file

@ -1536,7 +1536,7 @@ body.help-open .app-header {
</svg>
<div class="header-title-group">
<span class="app-header__title">ZDDC Classifier</span>
<span class="build-timestamp"><span style="color:red;font-weight:bold">v0.0.17-beta · 2026-05-10 · diamond-flame-kettle</span></span>
<span class="build-timestamp"><span style="color:red;font-weight:bold">v0.0.17-beta · 2026-05-11 · brass-dolphin-ivory</span></span>
</div>
<button id="addDirectoryBtn" class="btn btn-primary">Add 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>

View file

@ -1225,7 +1225,7 @@ body {
</svg>
<div class="header-title-group">
<span class="app-header__title">ZDDC</span>
<span class="build-timestamp"><span style="color:red;font-weight:bold">v0.0.17-beta · 2026-05-10 · diamond-flame-kettle</span></span>
<span class="build-timestamp"><span style="color:red;font-weight:bold">v0.0.17-beta · 2026-05-11 · brass-dolphin-ivory</span></span>
</div>
</div>
<div class="header-right">

View file

@ -2010,7 +2010,7 @@ body.help-open .app-header {
</svg>
<div class="header-title-group">
<span class="app-header__title">ZDDC Markdown</span>
<span class="build-timestamp"><span style="color:red;font-weight:bold">v0.0.17-beta · 2026-05-10 · diamond-flame-kettle</span></span>
<span class="build-timestamp"><span style="color:red;font-weight:bold">v0.0.17-beta · 2026-05-11 · brass-dolphin-ivory</span></span>
</div>
<button id="addDirectoryBtn" class="btn btn-primary" title="Add a local directory">Add Local Directory</button>
<button id="refreshHeaderBtn" class="btn btn-secondary hidden" title="Refresh directory" aria-label="Refresh"></button>

View file

@ -2378,7 +2378,7 @@ dialog.modal--narrow {
</svg>
<div class="header-title-group">
<span class="app-header__title">ZDDC Transmittal</span>
<span class="build-timestamp"><span style="color:red;font-weight:bold">v0.0.17-beta · 2026-05-10 · diamond-flame-kettle</span></span>
<span class="build-timestamp"><span style="color:red;font-weight:bold">v0.0.17-beta · 2026-05-11 · brass-dolphin-ivory</span></span>
</div>
<span id="no-js-notice" class="text-gray-400 text-xs italic">JavaScript not available</span>
<!-- Publish split-button (Transmittal-specific primary action;

View file

@ -1,9 +1,9 @@
# Generated by build.sh — do not edit. One <app>=<build label> per line.
archive=v0.0.17-beta · 2026-05-10 · diamond-flame-kettle
transmittal=v0.0.17-beta · 2026-05-10 · diamond-flame-kettle
classifier=v0.0.17-beta · 2026-05-10 · diamond-flame-kettle
mdedit=v0.0.17-beta · 2026-05-10 · diamond-flame-kettle
landing=v0.0.17-beta · 2026-05-10 · diamond-flame-kettle
form=v0.0.17-beta · 2026-05-10 · diamond-flame-kettle
tables=v0.0.17-beta · 2026-05-10 · diamond-flame-kettle
browse=v0.0.17-beta · 2026-05-10 · diamond-flame-kettle
archive=v0.0.17-beta · 2026-05-11 · brass-dolphin-ivory
transmittal=v0.0.17-beta · 2026-05-11 · brass-dolphin-ivory
classifier=v0.0.17-beta · 2026-05-11 · brass-dolphin-ivory
mdedit=v0.0.17-beta · 2026-05-11 · brass-dolphin-ivory
landing=v0.0.17-beta · 2026-05-11 · brass-dolphin-ivory
form=v0.0.17-beta · 2026-05-11 · brass-dolphin-ivory
tables=v0.0.17-beta · 2026-05-11 · brass-dolphin-ivory
browse=v0.0.17-beta · 2026-05-11 · brass-dolphin-ivory

View file

@ -1155,7 +1155,7 @@ body.help-open .app-header {
</svg>
<div class="header-title-group">
<span class="app-header__title" id="table-title">ZDDC Table</span>
<span class="build-timestamp"><span style="color:red;font-weight:bold">v0.0.17-alpha · 2026-05-11 00:02:27 · d779814-dirty</span></span>
<span class="build-timestamp"><span style="color:red;font-weight:bold">v0.0.17-beta · 2026-05-11 · brass-dolphin-ivory</span></span>
</div>
</div>
<div class="header-right">