chore(embedded): cut v0.0.25-beta
This commit is contained in:
parent
9972e6773a
commit
e58e66a49c
7 changed files with 675 additions and 13 deletions
|
|
@ -2582,7 +2582,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">v0.0.24</span>
|
<span class="build-timestamp"><span style="color:red;font-weight:bold">v0.0.25-beta · 2026-05-28 19:19:54 · 9972e67</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>
|
||||||
|
|
|
||||||
|
|
@ -2329,6 +2329,138 @@ body {
|
||||||
max-width: 32rem;
|
max-width: 32rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* history.css — markdown edit-history modal (browse/js/history.js). */
|
||||||
|
|
||||||
|
.md-history-box {
|
||||||
|
background: var(--bg, #fff);
|
||||||
|
color: var(--fg, #111);
|
||||||
|
padding: 1.1rem 1.35rem;
|
||||||
|
border-radius: 6px;
|
||||||
|
min-width: 30rem;
|
||||||
|
max-width: 56rem;
|
||||||
|
width: 80vw;
|
||||||
|
max-height: 85vh;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.25);
|
||||||
|
font-family: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
.md-history-title {
|
||||||
|
margin: 0 0 0.5rem 0;
|
||||||
|
font-size: 1.1rem;
|
||||||
|
overflow-wrap: anywhere;
|
||||||
|
}
|
||||||
|
|
||||||
|
.md-history-body {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
min-height: 0; /* allow inner scroll regions to shrink */
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.md-history-hint {
|
||||||
|
margin: 0 0 0.6rem 0;
|
||||||
|
font-size: 0.82rem;
|
||||||
|
color: var(--muted, #666);
|
||||||
|
}
|
||||||
|
|
||||||
|
.md-history-empty {
|
||||||
|
margin: 1rem 0;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
color: var(--muted, #666);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── version list ── */
|
||||||
|
.md-history-list {
|
||||||
|
overflow-y: auto;
|
||||||
|
border: 1px solid var(--border, #ddd);
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.md-history-row {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: auto 1fr auto;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.6rem;
|
||||||
|
padding: 0.4rem 0.6rem;
|
||||||
|
border-bottom: 1px solid var(--border, #eee);
|
||||||
|
font-size: 0.88rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.md-history-row:last-child { border-bottom: none; }
|
||||||
|
.md-history-row.is-current { background: var(--accent-bg, rgba(60, 130, 246, 0.08)); }
|
||||||
|
|
||||||
|
.md-history-meta {
|
||||||
|
display: flex;
|
||||||
|
align-items: baseline;
|
||||||
|
gap: 0.75rem;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.md-history-time { font-variant-numeric: tabular-nums; }
|
||||||
|
.md-history-by { color: var(--muted, #555); overflow-wrap: anywhere; }
|
||||||
|
.md-history-size { color: var(--muted, #888); font-size: 0.8rem; }
|
||||||
|
|
||||||
|
.md-history-badge {
|
||||||
|
font-size: 0.7rem;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.04em;
|
||||||
|
padding: 0.05rem 0.4rem;
|
||||||
|
border-radius: 10px;
|
||||||
|
background: var(--accent, #3c82f6);
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.md-history-actions { display: flex; gap: 0.35rem; }
|
||||||
|
|
||||||
|
/* ── single-version view ── */
|
||||||
|
.md-history-pre {
|
||||||
|
flex: 1 1 auto;
|
||||||
|
overflow: auto;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0.6rem 0.8rem;
|
||||||
|
border: 1px solid var(--border, #ddd);
|
||||||
|
border-radius: 4px;
|
||||||
|
background: var(--code-bg, #f7f7f8);
|
||||||
|
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
||||||
|
font-size: 0.82rem;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
overflow-wrap: anywhere;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── diff view ── */
|
||||||
|
.md-diff {
|
||||||
|
flex: 1 1 auto;
|
||||||
|
overflow: auto;
|
||||||
|
border: 1px solid var(--border, #ddd);
|
||||||
|
border-radius: 4px;
|
||||||
|
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
||||||
|
font-size: 0.82rem;
|
||||||
|
line-height: 1.45;
|
||||||
|
}
|
||||||
|
|
||||||
|
.md-diff-line { display: flex; gap: 0.5rem; padding: 0 0.5rem; white-space: pre-wrap; overflow-wrap: anywhere; }
|
||||||
|
.md-diff-gutter { flex: 0 0 1ch; text-align: center; color: var(--muted, #999); user-select: none; }
|
||||||
|
.md-diff-text { flex: 1 1 auto; }
|
||||||
|
|
||||||
|
.md-diff-add { background: rgba(46, 160, 67, 0.16); }
|
||||||
|
.md-diff-add .md-diff-gutter { color: #2ea043; }
|
||||||
|
.md-diff-del { background: rgba(248, 81, 73, 0.16); }
|
||||||
|
.md-diff-del .md-diff-gutter { color: #f85149; }
|
||||||
|
.md-diff-eq { color: var(--muted, #777); }
|
||||||
|
|
||||||
|
.md-diff-old { color: #f85149; }
|
||||||
|
.md-diff-new { color: #2ea043; }
|
||||||
|
|
||||||
|
/* ── footer ── */
|
||||||
|
.md-history-footer {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
gap: 0.5rem;
|
||||||
|
margin-top: 0.85rem;
|
||||||
|
}
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|
@ -2344,7 +2476,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">v0.0.24</span>
|
<span class="build-timestamp"><span style="color:red;font-weight:bold">v0.0.25-beta · 2026-05-28 19:19:54 · 9972e67</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>
|
||||||
|
|
@ -4227,6 +4359,115 @@ var e=function(t,n){return e=Object.setPrototypeOf||{__proto__:[]}instanceof Arr
|
||||||
window.zddc.filter = { parse: parse, matches: matches };
|
window.zddc.filter = { parse: parse, matches: matches };
|
||||||
})();
|
})();
|
||||||
|
|
||||||
|
/*
|
||||||
|
* shared/diff.js — a small, dependency-free text diff.
|
||||||
|
*
|
||||||
|
* Attaches to window.zddc.diff. Must load AFTER shared/zddc.js (which
|
||||||
|
* creates the window.zddc object). Used by the browse tool's markdown
|
||||||
|
* version-history viewer to show what changed between any two saved
|
||||||
|
* versions; kept in shared/ so other tools can reuse it.
|
||||||
|
*
|
||||||
|
* API:
|
||||||
|
* window.zddc.diff.lines(oldStr, newStr)
|
||||||
|
* → [{ type: 'eq'|'del'|'add', text }] line-level diff (LCS)
|
||||||
|
* window.zddc.diff.words(oldStr, newStr)
|
||||||
|
* → [{ type: 'eq'|'del'|'add', text }] token-level diff for one
|
||||||
|
* changed line (whitespace-preserving), for intra-line highlights
|
||||||
|
* window.zddc.diff.stats(ops) → { added, removed }
|
||||||
|
*
|
||||||
|
* The line diff trims the common prefix/suffix before running the O(n*m)
|
||||||
|
* LCS dynamic program, so a small edit in a large file stays cheap. A
|
||||||
|
* safety cap falls back to "replace whole block" when the changed middle
|
||||||
|
* is pathologically large, so the UI never freezes.
|
||||||
|
*/
|
||||||
|
(function () {
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
var LCS_CELL_CAP = 4000000; // ~4M cells (n*m) before the fallback
|
||||||
|
|
||||||
|
function splitLines(s) {
|
||||||
|
return String(s == null ? '' : s).replace(/\r\n/g, '\n').split('\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
// LCS diff of two arrays of strings → ordered [{type, text}] ops.
|
||||||
|
function lcsDiff(a, b) {
|
||||||
|
var n = a.length, m = b.length;
|
||||||
|
if (n === 0 && m === 0) return [];
|
||||||
|
if (n === 0) return b.map(function (t) { return { type: 'add', text: t }; });
|
||||||
|
if (m === 0) return a.map(function (t) { return { type: 'del', text: t }; });
|
||||||
|
|
||||||
|
if (n * m > LCS_CELL_CAP) {
|
||||||
|
// Too large to diff finely without risking a UI stall: treat
|
||||||
|
// the whole block as a wholesale replacement.
|
||||||
|
var out = a.map(function (t) { return { type: 'del', text: t }; });
|
||||||
|
return out.concat(b.map(function (t) { return { type: 'add', text: t }; }));
|
||||||
|
}
|
||||||
|
|
||||||
|
// dp[i][j] = LCS length of a[i:] and b[j:].
|
||||||
|
var dp = new Array(n + 1);
|
||||||
|
for (var i = 0; i <= n; i++) dp[i] = new Array(m + 1).fill(0);
|
||||||
|
for (var ii = n - 1; ii >= 0; ii--) {
|
||||||
|
for (var jj = m - 1; jj >= 0; jj--) {
|
||||||
|
if (a[ii] === b[jj]) dp[ii][jj] = dp[ii + 1][jj + 1] + 1;
|
||||||
|
else dp[ii][jj] = Math.max(dp[ii + 1][jj], dp[ii][jj + 1]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var ops = [], i = 0, j = 0;
|
||||||
|
while (i < n && j < m) {
|
||||||
|
if (a[i] === b[j]) { ops.push({ type: 'eq', text: a[i] }); i++; j++; }
|
||||||
|
else if (dp[i + 1][j] >= dp[i][j + 1]) { ops.push({ type: 'del', text: a[i] }); i++; }
|
||||||
|
else { ops.push({ type: 'add', text: b[j] }); j++; }
|
||||||
|
}
|
||||||
|
while (i < n) ops.push({ type: 'del', text: a[i++] });
|
||||||
|
while (j < m) ops.push({ type: 'add', text: b[j++] });
|
||||||
|
return ops;
|
||||||
|
}
|
||||||
|
|
||||||
|
function diffLines(oldStr, newStr) {
|
||||||
|
var a = splitLines(oldStr), b = splitLines(newStr);
|
||||||
|
var ops = [];
|
||||||
|
|
||||||
|
// Common prefix.
|
||||||
|
var start = 0;
|
||||||
|
while (start < a.length && start < b.length && a[start] === b[start]) start++;
|
||||||
|
// Common suffix (not overlapping the prefix).
|
||||||
|
var endA = a.length, endB = b.length;
|
||||||
|
while (endA > start && endB > start && a[endA - 1] === b[endB - 1]) { endA--; endB--; }
|
||||||
|
|
||||||
|
for (var p = 0; p < start; p++) ops.push({ type: 'eq', text: a[p] });
|
||||||
|
var mid = lcsDiff(a.slice(start, endA), b.slice(start, endB));
|
||||||
|
for (var k = 0; k < mid.length; k++) ops.push(mid[k]);
|
||||||
|
for (var s = endA; s < a.length; s++) ops.push({ type: 'eq', text: a[s] });
|
||||||
|
return ops;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Whitespace-preserving tokenization: words and the runs of
|
||||||
|
// whitespace between them are separate tokens, so a re-diff lines up
|
||||||
|
// on word boundaries while keeping the original spacing renderable.
|
||||||
|
function tokenize(s) {
|
||||||
|
return String(s == null ? '' : s).split(/(\s+)/).filter(function (x) { return x !== ''; });
|
||||||
|
}
|
||||||
|
|
||||||
|
function diffWords(oldStr, newStr) {
|
||||||
|
return lcsDiff(tokenize(oldStr), tokenize(newStr));
|
||||||
|
}
|
||||||
|
|
||||||
|
function stats(ops) {
|
||||||
|
var added = 0, removed = 0;
|
||||||
|
for (var i = 0; i < ops.length; i++) {
|
||||||
|
if (ops[i].type === 'add') added++;
|
||||||
|
else if (ops[i].type === 'del') removed++;
|
||||||
|
}
|
||||||
|
return { added: added, removed: removed };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!window.zddc) {
|
||||||
|
throw new Error('shared/diff.js: window.zddc must be loaded first');
|
||||||
|
}
|
||||||
|
window.zddc.diff = { lines: diffLines, words: diffWords, stats: stats };
|
||||||
|
})();
|
||||||
|
|
||||||
// shared/zip-source.js — present the contents of a .zip as a tree of
|
// shared/zip-source.js — present the contents of a .zip as a tree of
|
||||||
// File System Access API handles, so tools written against
|
// File System Access API handles, so tools written against
|
||||||
// FileSystemDirectoryHandle / FileSystemFileHandle (archive's scanner,
|
// FileSystemDirectoryHandle / FileSystemFileHandle (archive's scanner,
|
||||||
|
|
@ -6790,6 +7031,12 @@ var e=function(t,n){return e=Object.setPrototypeOf||{__proto__:[]}instanceof Arr
|
||||||
// whatever the server enforces on the
|
// whatever the server enforces on the
|
||||||
// actual PUT/DELETE still apply.
|
// actual PUT/DELETE still apply.
|
||||||
verbs: typeof e.verbs === 'string' ? e.verbs : undefined,
|
verbs: typeof e.verbs === 'string' ? e.verbs : undefined,
|
||||||
|
// Server-computed: true when this file lives in a history:true
|
||||||
|
// cascade subtree, so every save is versioned and
|
||||||
|
// GET <url>?history lists prior versions. Drives the "History…"
|
||||||
|
// context-menu affordance (server mode only — offline has no
|
||||||
|
// authenticated identity to attribute saves to).
|
||||||
|
history: !!e.history,
|
||||||
// FS-API specific (null in server mode):
|
// FS-API specific (null in server mode):
|
||||||
handle: null
|
handle: null
|
||||||
};
|
};
|
||||||
|
|
@ -11375,6 +11622,403 @@ var e=function(t,n){return e=Object.setPrototypeOf||{__proto__:[]}instanceof Arr
|
||||||
};
|
};
|
||||||
})();
|
})();
|
||||||
|
|
||||||
|
// history.js — markdown edit-history viewer for the browse tool.
|
||||||
|
//
|
||||||
|
// Surfaced by events.js as a "History…" right-click item on files in a
|
||||||
|
// history:true cascade subtree (working/). Server mode only — the audit
|
||||||
|
// trail (who saved when) is stamped server-side, so there's no offline
|
||||||
|
// equivalent.
|
||||||
|
//
|
||||||
|
// Talks to the zddc-server history endpoints on the file's own URL:
|
||||||
|
// GET <url>?history=1 → JSON [{ts, by, sha, prev, bytes, current}]
|
||||||
|
// GET <url>?history=<sha> → that version's raw bytes
|
||||||
|
// Restore re-PUTs a chosen version's bytes to <url>, which the server
|
||||||
|
// records as a new version (forward-only; never destructive).
|
||||||
|
//
|
||||||
|
// Diffs are computed client-side via window.zddc.diff (shared/diff.js).
|
||||||
|
|
||||||
|
(function () {
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
function escapeHtml(s) {
|
||||||
|
return String(s == null ? '' : s)
|
||||||
|
.replace(/&/g, '&').replace(/</g, '<')
|
||||||
|
.replace(/>/g, '>').replace(/"/g, '"');
|
||||||
|
}
|
||||||
|
|
||||||
|
function toast(msg, kind) {
|
||||||
|
if (window.zddc && typeof window.zddc.toast === 'function') {
|
||||||
|
window.zddc.toast(msg, kind || 'info');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Append ?history=<v> (or &history=) to a file URL.
|
||||||
|
function histURL(baseURL, v) {
|
||||||
|
var sep = baseURL.indexOf('?') === -1 ? '?' : '&';
|
||||||
|
return baseURL + sep + 'history=' + encodeURIComponent(v);
|
||||||
|
}
|
||||||
|
|
||||||
|
function fmtTime(ts) {
|
||||||
|
var d = new Date(ts);
|
||||||
|
if (isNaN(d.getTime())) return ts || '';
|
||||||
|
return d.toLocaleString();
|
||||||
|
}
|
||||||
|
|
||||||
|
function fmtBytes(n) {
|
||||||
|
if (n == null) return '';
|
||||||
|
if (n < 1024) return n + ' B';
|
||||||
|
if (n < 1024 * 1024) return (n / 1024).toFixed(1) + ' KB';
|
||||||
|
return (n / (1024 * 1024)).toFixed(1) + ' MB';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Can the principal write (restore) to this file? Mirrors the
|
||||||
|
// events.js Rename/Delete gating: verbs===undefined means a non-zddc
|
||||||
|
// backend (no cascade signal) → allow the attempt; otherwise check w.
|
||||||
|
function canRestore(node) {
|
||||||
|
if (!node || !node.url) return false;
|
||||||
|
if (!window.zddc || !window.zddc.cap) return true;
|
||||||
|
if (typeof node.verbs !== 'string') return true;
|
||||||
|
return window.zddc.cap.has(node, 'w');
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetchList(node) {
|
||||||
|
var resp = await fetch(histURL(node.url, '1'), {
|
||||||
|
headers: { 'Accept': 'application/json' },
|
||||||
|
credentials: 'same-origin'
|
||||||
|
});
|
||||||
|
if (!resp.ok) throw new Error('HTTP ' + resp.status);
|
||||||
|
var data = await resp.json();
|
||||||
|
return Array.isArray(data) ? data : [];
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetchVersion(node, sha) {
|
||||||
|
var resp = await fetch(histURL(node.url, sha), { credentials: 'same-origin' });
|
||||||
|
if (!resp.ok) throw new Error('HTTP ' + resp.status);
|
||||||
|
return await resp.text();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Modal shell ──────────────────────────────────────────────────────
|
||||||
|
// One overlay; its body is swapped between the list, a diff, and a
|
||||||
|
// single-version view. Returns { overlay, body, setTitle, close }.
|
||||||
|
function makeModal(titleText) {
|
||||||
|
var overlay = document.createElement('div');
|
||||||
|
overlay.className = 'modal-overlay md-history-overlay';
|
||||||
|
overlay.style.cssText = 'position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,0.4);display:flex;align-items:center;justify-content:center;z-index:9000;';
|
||||||
|
|
||||||
|
var box = document.createElement('div');
|
||||||
|
box.className = 'md-history-box';
|
||||||
|
|
||||||
|
var title = document.createElement('h2');
|
||||||
|
title.className = 'md-history-title';
|
||||||
|
title.textContent = titleText;
|
||||||
|
|
||||||
|
var body = document.createElement('div');
|
||||||
|
body.className = 'md-history-body';
|
||||||
|
|
||||||
|
box.appendChild(title);
|
||||||
|
box.appendChild(body);
|
||||||
|
overlay.appendChild(box);
|
||||||
|
document.body.appendChild(overlay);
|
||||||
|
|
||||||
|
function close() {
|
||||||
|
if (overlay.parentNode) overlay.parentNode.removeChild(overlay);
|
||||||
|
document.removeEventListener('keydown', onKey);
|
||||||
|
}
|
||||||
|
function onKey(e) { if (e.key === 'Escape') close(); }
|
||||||
|
document.addEventListener('keydown', onKey);
|
||||||
|
overlay.addEventListener('mousedown', function (e) {
|
||||||
|
if (e.target === overlay) close();
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
overlay: overlay,
|
||||||
|
body: body,
|
||||||
|
setTitle: function (t) { title.textContent = t; },
|
||||||
|
close: close
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function footerBar() {
|
||||||
|
var f = document.createElement('div');
|
||||||
|
f.className = 'md-history-footer';
|
||||||
|
return f;
|
||||||
|
}
|
||||||
|
|
||||||
|
function button(label, opts) {
|
||||||
|
opts = opts || {};
|
||||||
|
var b = document.createElement('button');
|
||||||
|
b.type = 'button';
|
||||||
|
b.textContent = label;
|
||||||
|
if (opts.primary) b.className = 'btn-primary';
|
||||||
|
if (opts.disabled) b.disabled = true;
|
||||||
|
if (opts.onClick) b.addEventListener('click', opts.onClick);
|
||||||
|
return b;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── List view ──────────────────────────────────────────────────────
|
||||||
|
function renderList(modal, node, entries) {
|
||||||
|
modal.setTitle('History — ' + node.name);
|
||||||
|
var body = modal.body;
|
||||||
|
body.innerHTML = '';
|
||||||
|
|
||||||
|
if (!entries.length) {
|
||||||
|
var empty = document.createElement('p');
|
||||||
|
empty.className = 'md-history-empty';
|
||||||
|
empty.textContent = 'No saved versions yet. Each save of this file is recorded here.';
|
||||||
|
body.appendChild(empty);
|
||||||
|
var f0 = footerBar();
|
||||||
|
f0.appendChild(button('Close', { onClick: modal.close }));
|
||||||
|
body.appendChild(f0);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var hint = document.createElement('p');
|
||||||
|
hint.className = 'md-history-hint';
|
||||||
|
hint.textContent = 'Newest first. Select two versions to diff.';
|
||||||
|
body.appendChild(hint);
|
||||||
|
|
||||||
|
var list = document.createElement('div');
|
||||||
|
list.className = 'md-history-list';
|
||||||
|
var selected = []; // shas, in click order (max 2)
|
||||||
|
|
||||||
|
var diffBtn;
|
||||||
|
function syncDiffBtn() {
|
||||||
|
if (diffBtn) diffBtn.disabled = selected.length !== 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
entries.forEach(function (ent) {
|
||||||
|
var row = document.createElement('div');
|
||||||
|
row.className = 'md-history-row' + (ent.current ? ' is-current' : '');
|
||||||
|
|
||||||
|
var cb = document.createElement('input');
|
||||||
|
cb.type = 'checkbox';
|
||||||
|
cb.className = 'md-history-pick';
|
||||||
|
cb.addEventListener('change', function () {
|
||||||
|
if (cb.checked) {
|
||||||
|
selected.push(ent.sha);
|
||||||
|
// Keep at most two: drop the oldest selection.
|
||||||
|
if (selected.length > 2) {
|
||||||
|
var dropped = selected.shift();
|
||||||
|
var others = list.querySelectorAll('.md-history-pick');
|
||||||
|
others.forEach(function (o, i) {
|
||||||
|
if (o !== cb && entries[i] && entries[i].sha === dropped) o.checked = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
selected = selected.filter(function (s) { return s !== ent.sha; });
|
||||||
|
}
|
||||||
|
syncDiffBtn();
|
||||||
|
});
|
||||||
|
|
||||||
|
var meta = document.createElement('div');
|
||||||
|
meta.className = 'md-history-meta';
|
||||||
|
meta.innerHTML =
|
||||||
|
'<span class="md-history-time">' + escapeHtml(fmtTime(ent.ts)) + '</span>' +
|
||||||
|
'<span class="md-history-by">' + escapeHtml(ent.by || '—') + '</span>' +
|
||||||
|
'<span class="md-history-size">' + escapeHtml(fmtBytes(ent.bytes)) + '</span>' +
|
||||||
|
(ent.current ? '<span class="md-history-badge">current</span>' : '');
|
||||||
|
|
||||||
|
var actions = document.createElement('div');
|
||||||
|
actions.className = 'md-history-actions';
|
||||||
|
actions.appendChild(button('View', {
|
||||||
|
onClick: function () { renderView(modal, node, ent, entries); }
|
||||||
|
}));
|
||||||
|
if (!ent.current && canRestore(node)) {
|
||||||
|
actions.appendChild(button('Restore', {
|
||||||
|
onClick: function () { restore(modal, node, ent); }
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
row.appendChild(cb);
|
||||||
|
row.appendChild(meta);
|
||||||
|
row.appendChild(actions);
|
||||||
|
list.appendChild(row);
|
||||||
|
});
|
||||||
|
|
||||||
|
body.appendChild(list);
|
||||||
|
|
||||||
|
var f = footerBar();
|
||||||
|
diffBtn = button('Diff selected', {
|
||||||
|
primary: true, disabled: true,
|
||||||
|
onClick: function () {
|
||||||
|
if (selected.length !== 2) return;
|
||||||
|
// Order oldest→newest by the entries' position (newest
|
||||||
|
// first in the list), so the diff reads old → new.
|
||||||
|
var picks = entries.filter(function (e) { return selected.indexOf(e.sha) !== -1; });
|
||||||
|
picks.sort(function (a, b) { return (a.ts < b.ts ? -1 : 1); });
|
||||||
|
renderDiff(modal, node, picks[0], picks[1], entries);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
f.appendChild(diffBtn);
|
||||||
|
f.appendChild(button('Close', { onClick: modal.close }));
|
||||||
|
body.appendChild(f);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Single-version view ──────────────────────────────────────────────
|
||||||
|
async function renderView(modal, node, ent, entries) {
|
||||||
|
modal.setTitle('Version — ' + fmtTime(ent.ts));
|
||||||
|
var body = modal.body;
|
||||||
|
body.innerHTML = '<p class="md-history-hint">Loading…</p>';
|
||||||
|
var text;
|
||||||
|
try {
|
||||||
|
text = await fetchVersion(node, ent.sha);
|
||||||
|
} catch (e) {
|
||||||
|
body.innerHTML = '';
|
||||||
|
var err = document.createElement('p');
|
||||||
|
err.className = 'md-history-empty';
|
||||||
|
err.textContent = 'Could not load this version: ' + (e.message || e);
|
||||||
|
body.appendChild(err);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
body.innerHTML = '';
|
||||||
|
var meta = document.createElement('p');
|
||||||
|
meta.className = 'md-history-hint';
|
||||||
|
meta.textContent = (ent.by || '—') + ' · ' + fmtTime(ent.ts);
|
||||||
|
body.appendChild(meta);
|
||||||
|
|
||||||
|
var pre = document.createElement('pre');
|
||||||
|
pre.className = 'md-history-pre';
|
||||||
|
pre.textContent = text;
|
||||||
|
body.appendChild(pre);
|
||||||
|
|
||||||
|
var f = footerBar();
|
||||||
|
f.appendChild(button('Back', { onClick: function () { renderList(modal, node, entries); } }));
|
||||||
|
if (!ent.current && canRestore(node)) {
|
||||||
|
f.appendChild(button('Restore this version', {
|
||||||
|
primary: true, onClick: function () { restore(modal, node, ent); }
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
body.appendChild(f);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Diff view ─────────────────────────────────────────────────────────
|
||||||
|
async function renderDiff(modal, node, oldEnt, newEnt, entries) {
|
||||||
|
modal.setTitle('Diff');
|
||||||
|
var body = modal.body;
|
||||||
|
body.innerHTML = '<p class="md-history-hint">Loading…</p>';
|
||||||
|
var oldText, newText;
|
||||||
|
try {
|
||||||
|
oldText = await fetchVersion(node, oldEnt.sha);
|
||||||
|
newText = await fetchVersion(node, newEnt.sha);
|
||||||
|
} catch (e) {
|
||||||
|
body.innerHTML = '';
|
||||||
|
var err = document.createElement('p');
|
||||||
|
err.className = 'md-history-empty';
|
||||||
|
err.textContent = 'Could not load versions: ' + (e.message || e);
|
||||||
|
body.appendChild(err);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
body.innerHTML = '';
|
||||||
|
|
||||||
|
var hdr = document.createElement('p');
|
||||||
|
hdr.className = 'md-history-hint';
|
||||||
|
hdr.innerHTML =
|
||||||
|
'<span class="md-diff-old">' + escapeHtml(fmtTime(oldEnt.ts)) + ' · ' + escapeHtml(oldEnt.by || '—') + '</span>' +
|
||||||
|
' → ' +
|
||||||
|
'<span class="md-diff-new">' + escapeHtml(fmtTime(newEnt.ts)) + ' · ' + escapeHtml(newEnt.by || '—') + '</span>';
|
||||||
|
body.appendChild(hdr);
|
||||||
|
|
||||||
|
var ops = (window.zddc && window.zddc.diff)
|
||||||
|
? window.zddc.diff.lines(oldText, newText)
|
||||||
|
: null;
|
||||||
|
|
||||||
|
var pane = document.createElement('div');
|
||||||
|
pane.className = 'md-diff';
|
||||||
|
if (!ops) {
|
||||||
|
pane.textContent = 'Diff unavailable (diff module not loaded).';
|
||||||
|
} else {
|
||||||
|
var unchanged = true;
|
||||||
|
ops.forEach(function (op) {
|
||||||
|
if (op.type !== 'eq') unchanged = false;
|
||||||
|
var line = document.createElement('div');
|
||||||
|
line.className = 'md-diff-line md-diff-' + op.type;
|
||||||
|
var gutter = op.type === 'add' ? '+' : (op.type === 'del' ? '-' : ' ');
|
||||||
|
var g = document.createElement('span');
|
||||||
|
g.className = 'md-diff-gutter';
|
||||||
|
g.textContent = gutter;
|
||||||
|
var t = document.createElement('span');
|
||||||
|
t.className = 'md-diff-text';
|
||||||
|
t.textContent = op.text;
|
||||||
|
line.appendChild(g);
|
||||||
|
line.appendChild(t);
|
||||||
|
pane.appendChild(line);
|
||||||
|
});
|
||||||
|
if (unchanged) {
|
||||||
|
var same = document.createElement('div');
|
||||||
|
same.className = 'md-diff-line md-diff-eq';
|
||||||
|
same.textContent = '(no differences)';
|
||||||
|
pane.appendChild(same);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
body.appendChild(pane);
|
||||||
|
|
||||||
|
if (window.zddc && window.zddc.diff && ops) {
|
||||||
|
var s = window.zddc.diff.stats(ops);
|
||||||
|
var statline = document.createElement('p');
|
||||||
|
statline.className = 'md-history-hint';
|
||||||
|
statline.textContent = '+' + s.added + ' / −' + s.removed;
|
||||||
|
body.appendChild(statline);
|
||||||
|
}
|
||||||
|
|
||||||
|
var f = footerBar();
|
||||||
|
f.appendChild(button('Back', { onClick: function () { renderList(modal, node, entries); } }));
|
||||||
|
body.appendChild(f);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Restore ───────────────────────────────────────────────────────────
|
||||||
|
async function restore(modal, node, ent) {
|
||||||
|
if (!confirm('Restore the version from ' + fmtTime(ent.ts) + '?\nThis is saved as a new version — nothing is lost.')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
var text = await fetchVersion(node, ent.sha);
|
||||||
|
var resp = await fetch(node.url, {
|
||||||
|
method: 'PUT',
|
||||||
|
credentials: 'same-origin',
|
||||||
|
headers: { 'Content-Type': 'text/markdown' },
|
||||||
|
body: text
|
||||||
|
});
|
||||||
|
if (!resp.ok) throw new Error('HTTP ' + resp.status);
|
||||||
|
toast('Restored version from ' + fmtTime(ent.ts), 'success');
|
||||||
|
// Reflect the new head: refetch the list.
|
||||||
|
var entries = await fetchList(node);
|
||||||
|
renderList(modal, node, entries);
|
||||||
|
// If the file is open in the preview pane, reload it.
|
||||||
|
var preview = window.app && window.app.modules && window.app.modules.preview;
|
||||||
|
if (preview && typeof preview.showFilePreview === 'function') {
|
||||||
|
try { preview.showFilePreview(node); } catch (_e) { /* best effort */ }
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
toast('Restore failed: ' + (e.message || e), 'error');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Entry point ─────────────────────────────────────────────────────
|
||||||
|
async function open(node) {
|
||||||
|
if (!node || !node.url) {
|
||||||
|
toast('History is only available in server mode.', 'error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var modal = makeModal('History — ' + node.name);
|
||||||
|
modal.body.innerHTML = '<p class="md-history-hint">Loading…</p>';
|
||||||
|
try {
|
||||||
|
var entries = await fetchList(node);
|
||||||
|
renderList(modal, node, entries);
|
||||||
|
} catch (e) {
|
||||||
|
modal.body.innerHTML = '';
|
||||||
|
var err = document.createElement('p');
|
||||||
|
err.className = 'md-history-empty';
|
||||||
|
err.textContent = 'Could not load history: ' + (e.message || e);
|
||||||
|
modal.body.appendChild(err);
|
||||||
|
var f = footerBar();
|
||||||
|
f.appendChild(button('Close', { onClick: modal.close }));
|
||||||
|
modal.body.appendChild(f);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
window.app.modules.history = { open: open };
|
||||||
|
})();
|
||||||
|
|
||||||
// create-transmittal.js — folder-creation plumbing for outgoing
|
// create-transmittal.js — folder-creation plumbing for outgoing
|
||||||
// transmittals.
|
// transmittals.
|
||||||
//
|
//
|
||||||
|
|
@ -12585,6 +13229,24 @@ var e=function(t,n){return e=Object.setPrototypeOf||{__proto__:[]}instanceof Arr
|
||||||
if (s) s.invokeUnstage(c.node);
|
if (s) s.invokeUnstage(c.node);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
// ── Version history (history:true subtree, real files only) ──
|
||||||
|
// Server-mode only: the audit trail (who saved when) is
|
||||||
|
// server-stamped, so there's no offline equivalent. node.history
|
||||||
|
// is set by the listing when this file sits in a history-enabled
|
||||||
|
// cascade subtree (working/).
|
||||||
|
{
|
||||||
|
label: 'History…',
|
||||||
|
icon: '🕘',
|
||||||
|
visible: function (c) {
|
||||||
|
if (!serverMode) return false;
|
||||||
|
if (c.node.isDir || c.node.isZip || c.node.virtual) return false;
|
||||||
|
return !!c.node.history;
|
||||||
|
},
|
||||||
|
action: function (c) {
|
||||||
|
var h = window.app.modules.history;
|
||||||
|
if (h) h.open(c.node);
|
||||||
|
}
|
||||||
|
},
|
||||||
{ separator: true },
|
{ separator: true },
|
||||||
|
|
||||||
// ── View ──
|
// ── View ──
|
||||||
|
|
|
||||||
|
|
@ -1793,7 +1793,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">v0.0.24</span>
|
<span class="build-timestamp"><span style="color:red;font-weight:bold">v0.0.25-beta · 2026-05-28 19:19:54 · 9972e67</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>
|
||||||
|
|
|
||||||
|
|
@ -1536,7 +1536,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">v0.0.24</span>
|
<span class="build-timestamp"><span style="color:red;font-weight:bold">v0.0.25-beta · 2026-05-28 19:19:54 · 9972e67</span></span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="header-right">
|
<div class="header-right">
|
||||||
|
|
|
||||||
|
|
@ -2635,7 +2635,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">v0.0.24</span>
|
<span class="build-timestamp"><span style="color:red;font-weight:bold">v0.0.25-beta · 2026-05-28 19:19:53 · 9972e67</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;
|
||||||
|
|
|
||||||
|
|
@ -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.24
|
archive=v0.0.25-beta · 2026-05-28 19:19:54 · 9972e67
|
||||||
transmittal=v0.0.24
|
transmittal=v0.0.25-beta · 2026-05-28 19:19:53 · 9972e67
|
||||||
classifier=v0.0.24
|
classifier=v0.0.25-beta · 2026-05-28 19:19:54 · 9972e67
|
||||||
landing=v0.0.24
|
landing=v0.0.25-beta · 2026-05-28 19:19:54 · 9972e67
|
||||||
form=v0.0.24
|
form=v0.0.25-beta · 2026-05-28 19:19:54 · 9972e67
|
||||||
tables=v0.0.24
|
tables=v0.0.25-beta · 2026-05-28 19:19:54 · 9972e67
|
||||||
browse=v0.0.24
|
browse=v0.0.25-beta · 2026-05-28 19:19:54 · 9972e67
|
||||||
|
|
|
||||||
|
|
@ -1534,7 +1534,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">v0.0.24</span>
|
<span class="build-timestamp"><span style="color:red;font-weight:bold">v0.0.25-beta · 2026-05-28 19:19:54 · 9972e67</span></span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="header-right">
|
<div class="header-right">
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue