Compare commits

..

No commits in common. "8368cc81b811df1fb16a9084008a9fe086be6529" and "589d804716a8bb8a5961b2a5b8f7c6620bc135ed" have entirely different histories.

12 changed files with 25 additions and 234 deletions

View file

@ -287,12 +287,7 @@
}
/* ── Welcome screen (intro + tutorial) ─────────────────────────────────── */
/* Scroll when the viewport is short. The inner card uses auto margins instead
of the base .empty-state's align-items:center so it centers when it fits but
collapses to the top when taller than the viewport otherwise centering
clips the top of the card and it can't be scrolled into view. */
.empty-state--overlay { overflow-y: auto; }
.empty-state--overlay > .empty-state__inner { margin: auto; }
.welcome { max-width: 900px; padding: 1.5rem 0.5rem 2.5rem; }
.welcome__title { font-size: 2.6rem; line-height: 1.1; margin: 0 0 0.6rem; }
.welcome__lede {
@ -327,8 +322,7 @@
/* ── Workspaces (welcome manager) ──────────────────────────────────────── */
.workspaces { text-align: left; margin: 1.5rem 0 0.5rem; }
.ws-head { display: flex; align-items: center; justify-content: space-between; gap: 1rem; flex-wrap: wrap; }
.ws-head__actions { display: flex; gap: 0.5rem; }
.ws-head { display: flex; align-items: center; justify-content: space-between; gap: 1rem; }
.ws-head h2 { margin: 0; font-size: 1.4rem; }
.ws-list { display: flex; flex-direction: column; gap: 0.4rem; }
.ws-empty { color: var(--text-muted); font-size: 0.85rem; padding: 0.75rem; border: 1px dashed var(--border); border-radius: var(--radius); }

View file

@ -259,7 +259,7 @@
var url = URL.createObjectURL(blob);
var a = document.createElement('a');
a.href = url;
a.download = String(name).replace(/[^\w.-]+/g, '_') + '.zddc-classification.json';
a.download = String(name).replace(/[^\w.-]+/g, '_') + '.json';
document.body.appendChild(a); a.click(); a.remove();
URL.revokeObjectURL(url);
}

View file

@ -35,8 +35,6 @@
newBtn: document.getElementById('newWorkspaceBtn'),
wsBtn: document.getElementById('workspacesBtn'),
connectBtn: document.getElementById('connectDirBtn'),
importBtn: document.getElementById('importWorkspaceBtn'),
importInput: document.getElementById('importWorkspaceInput'),
};
if (!P() || !P().available) {
// No IndexedDB → hide the workspace UI; legacy rename path still works.
@ -48,11 +46,6 @@
if (els.wsBtn) els.wsBtn.addEventListener('click', showWelcome);
if (els.connectBtn) els.connectBtn.addEventListener('click', function () { tryReconnect(false); });
if (els.list) els.list.addEventListener('click', onListClick);
if (els.importBtn) els.importBtn.addEventListener('click', function () { els.importInput.click(); });
if (els.importInput) els.importInput.addEventListener('change', function () {
if (this.files && this.files[0]) importWorkspace(this.files[0]);
this.value = '';
});
// Autosave the active workspace whenever the map changes.
C().on(scheduleAutosave);
@ -108,15 +101,10 @@
var actions = document.createElement('div');
actions.className = 'ws-row__actions';
var titles = {
export: 'Export this whole workspace (scanned snapshot + classifications) to transfer to another browser or back up',
delete: 'Delete this workspace — your source files are untouched',
};
[['open', 'Open'], ['rename', 'Rename'], ['export', 'Export'], ['delete', 'Delete']].forEach(function (a) {
[['open', 'Open'], ['rename', 'Rename'], ['delete', 'Delete']].forEach(function (a) {
var b = document.createElement('button');
b.className = 'btn btn-sm ' + (a[0] === 'open' ? 'btn-primary' : 'btn-secondary');
b.dataset.act = a[0]; b.textContent = a[1];
if (titles[a[0]]) b.title = titles[a[0]];
actions.appendChild(b);
});
row.appendChild(main); row.appendChild(actions);
@ -130,74 +118,9 @@
if (!id) return;
if (btn.dataset.act === 'open') openWorkspace(id);
else if (btn.dataset.act === 'rename') renameWorkspace(id);
else if (btn.dataset.act === 'export') exportWorkspace(id);
else if (btn.dataset.act === 'delete') deleteWorkspace(id);
}
// ── transfer (export / import a whole workspace as JSON) ─────────────────
// The scan is the expensive part (minutes on cloud mounts), so a workspace
// is portable: its snapshot + classify map travel as one JSON. The source
// directory HANDLE can't cross browsers, so an imported workspace has none —
// "Connect directory" re-attaches the folder once, without re-scanning.
async function exportWorkspace(id) {
var rows = await P().listWorkspaces();
var meta = rows.filter(function (r) { return r.id === id; })[0];
var rec = await P().getWorkspace(id);
if (!meta || !rec) { window.zddc.toast('Could not load that workspace to export.', 'error'); return; }
var payload = {
zddcWorkspace: 1,
exportedAt: new Date().toISOString(),
meta: { name: meta.name, rootName: meta.rootName, createdAt: meta.createdAt, updatedAt: meta.updatedAt, summary: meta.summary },
tree: rec.tree || [], // the scanned snapshot (no re-scan on the other side)
classify: rec.classify || {}, // assignments + target trees
};
var blob = new Blob([JSON.stringify(payload)], { type: 'application/json' });
var url = URL.createObjectURL(blob);
var a = document.createElement('a');
a.href = url;
a.download = String(meta.name || 'workspace').replace(/[^\w.-]+/g, '_') + '.zddc-workspace.json';
document.body.appendChild(a); a.click(); a.remove();
URL.revokeObjectURL(url);
window.zddc.toast('Exported “' + meta.name + '”. Import it in another browser to skip the re-scan.', 'success');
}
function importWorkspace(file) {
return new Promise(function (resolve) {
var reader = new FileReader();
reader.onload = function () {
var obj;
try { obj = JSON.parse(reader.result); }
catch (e) { window.zddc.toast('Import failed — not valid JSON.', 'error'); resolve(null); return; }
if (!obj || !obj.zddcWorkspace) {
window.zddc.toast(obj && obj.zddcClassifierFiles
? 'Thats a classification dataset, not a workspace — open a workspace and use Import in the toolbar.'
: 'Import failed — not a ZDDC workspace export.', 'error');
resolve(null); return;
}
var m = obj.meta || {};
var id = uid();
var meta = {
id: id,
name: m.name || 'Imported workspace',
rootName: m.rootName || '',
createdAt: m.createdAt || now(),
updatedAt: now(),
summary: m.summary || { files: 0, done: 0, excluded: 0 },
};
// No rootHandle — it can't be serialized across browsers; the
// user re-attaches via "Connect directory" after opening.
P().putWorkspace(meta, { id: id, rootHandle: null, tree: obj.tree || [], classify: obj.classify || {} })
.then(function () {
renderList();
window.zddc.toast('Imported “' + meta.name + '”. Open it, then “Connect directory” to re-attach the source folder for preview/copy.', 'success', { durationMs: 8000 });
resolve(id);
});
};
reader.onerror = function () { window.zddc.toast('Import failed — could not read the file.', 'error'); resolve(null); };
reader.readAsText(file);
});
}
// ── summary ───────────────────────────────────────────────────────────
function allFiles() {
var out = [];
@ -374,11 +297,8 @@
showWelcome: showWelcome,
newWorkspace: newWorkspace,
openWorkspace: openWorkspace,
exportWorkspace: exportWorkspace,
importWorkspace: importWorkspace,
onRescanned: onRescanned,
renderList: renderList,
activeId: function () { return activeId; },
activeName: function () { return activeMeta ? activeMeta.name : null; },
};
})();

View file

@ -162,8 +162,8 @@
<div class="pane-header-right">
<span id="classifyStats" class="file-stats"></span>
<span class="header-divider">|</span>
<button id="exportDatasetBtn" class="btn btn-secondary btn-sm" title="Download the classifications as a filename-per-file JSON to edit (e.g. with an AI), then re-import here. NOT a workspace — no scanned tree.">Export for editing</button>
<button id="importDatasetBtn" class="btn btn-secondary btn-sm" title="Load an edited classification JSON back in — replaces the current classifications. (To move a whole scanned workspace between browsers, use “Import workspace” on the welcome screen.)">Import edits</button>
<button id="exportDatasetBtn" class="btn btn-secondary btn-sm" title="Export the classification dataset (trees + assignments) as JSON">Export</button>
<button id="importDatasetBtn" class="btn btn-secondary btn-sm" title="Import a classification dataset JSON (replaces the current one)">Import</button>
<input type="file" id="importDatasetInput" accept="application/json,.json" hidden>
<button id="resetDatasetBtn" class="btn btn-sm btn-danger" title="Discard all classifications and start over from the raw scanned input (does not touch your files)">Reset</button>
<button id="copyOutputBtn" class="btn btn-primary btn-sm" disabled title="Copy mapped files to an output directory (source untouched)">Copy…</button>
@ -208,12 +208,8 @@
<section id="workspacesSection" class="workspaces">
<div class="ws-head">
<h2>Your workspaces</h2>
<div class="ws-head__actions">
<button id="importWorkspaceBtn" class="btn btn-secondary" title="Import a whole workspace (.zddc-workspace.json) exported from another browser — restores the scanned snapshot so you don't re-scan">⭱ Import workspace</button>
<button id="newWorkspaceBtn" class="btn btn-primary">+ New workspace</button>
</div>
<input type="file" id="importWorkspaceInput" accept="application/json,.json" hidden>
</div>
<div id="workspaceList" class="ws-list"><!-- rendered --></div>
</section>

View file

@ -883,32 +883,3 @@ test('copy: a zip member is extracted from its archive and written out', async (
expect(res.wrote).toBe(true);
expect(res.content).toBe('ZIPBYTES');
});
test('workspace: import recreates a transferable record (snapshot + map, no handle)', async ({ page }) => {
const r = await page.evaluate(async () => {
const ws = window.app.modules.workspace, P = window.app.modules.persist;
const json = JSON.stringify({
zddcWorkspace: 1,
meta: { name: 'Transferred', rootName: 'BigProject', summary: { files: 3, done: 1, excluded: 0 } },
tree: [{ n: 'BigProject', p: 'BigProject', f: [{ o: 'a', e: 'pdf', p: 'BigProject' }] }],
classify: {
assignments: { 'a.pdf': { trackingNodeId: null, transmittalNodeId: null, excluded: true, titleOverride: null } },
trackingTree: [], transmittalTree: [],
},
});
const file = new File([json], 'x.zddc-workspace.json', { type: 'application/json' });
const id = await ws.importWorkspace(file);
const rec = id && await P.getWorkspace(id);
const rows = await P.listWorkspaces();
return {
listed: rows.some((x) => x.id === id && x.name === 'Transferred' && x.rootName === 'BigProject'),
hasTree: !!(rec && rec.tree && rec.tree.length),
excluded: !!(rec && rec.classify && rec.classify.assignments['a.pdf'] && rec.classify.assignments['a.pdf'].excluded),
noHandle: rec ? (rec.rootHandle == null) : false,
};
});
expect(r.listed).toBe(true); // appears in the welcome list
expect(r.hasTree).toBe(true); // the expensive scan came across
expect(r.excluded).toBe(true); // classifications came across
expect(r.noHandle).toBe(true); // source handle intentionally absent (re-attach on this browser)
});

View file

@ -2717,7 +2717,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.27-beta · 2026-06-10 18:57:44 · e2c2d15</span></span>
<span class="build-timestamp"><span style="color:red;font-weight:bold">v0.0.27-beta · 2026-06-10 18:32:03 · 203674e</span></span>
</div>
<button id="addDirectoryBtn" class="btn btn-primary">Use Local Directory</button>
<button id="refreshHeaderBtn" class="btn btn-secondary hidden" title="Refresh Data"></button>

View file

@ -2824,7 +2824,7 @@ li.CodeMirror-hint-active {
</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.27-beta · 2026-06-10 18:57:44 · e2c2d15</span></span>
<span class="build-timestamp"><span style="color:red;font-weight:bold">v0.0.27-beta · 2026-06-10 18:32:03 · 203674e</span></span>
</div>
<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>

View file

@ -1457,12 +1457,7 @@ body.is-elevated::after {
}
/* ── Welcome screen (intro + tutorial) ─────────────────────────────────── */
/* Scroll when the viewport is short. The inner card uses auto margins instead
of the base .empty-state's align-items:center so it centers when it fits but
collapses to the top when taller than the viewport — otherwise centering
clips the top of the card and it can't be scrolled into view. */
.empty-state--overlay { overflow-y: auto; }
.empty-state--overlay > .empty-state__inner { margin: auto; }
.welcome { max-width: 900px; padding: 1.5rem 0.5rem 2.5rem; }
.welcome__title { font-size: 2.6rem; line-height: 1.1; margin: 0 0 0.6rem; }
.welcome__lede {
@ -1497,8 +1492,7 @@ body.is-elevated::after {
/* ── Workspaces (welcome manager) ──────────────────────────────────────── */
.workspaces { text-align: left; margin: 1.5rem 0 0.5rem; }
.ws-head { display: flex; align-items: center; justify-content: space-between; gap: 1rem; flex-wrap: wrap; }
.ws-head__actions { display: flex; gap: 0.5rem; }
.ws-head { display: flex; align-items: center; justify-content: space-between; gap: 1rem; }
.ws-head h2 { margin: 0; font-size: 1.4rem; }
.ws-list { display: flex; flex-direction: column; gap: 0.4rem; }
.ws-empty { color: var(--text-muted); font-size: 0.85rem; padding: 0.75rem; border: 1px dashed var(--border); border-radius: var(--radius); }
@ -2239,7 +2233,7 @@ input.tfile__name:focus { border-color: var(--primary); background: var(--bg); o
</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.27-beta · 2026-06-10 18:57:44 · e2c2d15</span></span>
<span class="build-timestamp"><span style="color:red;font-weight:bold">v0.0.27-beta · 2026-06-10 18:32:03 · 203674e</span></span>
</div>
<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>
@ -2375,8 +2369,8 @@ input.tfile__name:focus { border-color: var(--primary); background: var(--bg); o
<div class="pane-header-right">
<span id="classifyStats" class="file-stats"></span>
<span class="header-divider">|</span>
<button id="exportDatasetBtn" class="btn btn-secondary btn-sm" title="Download the classifications as a filename-per-file JSON to edit (e.g. with an AI), then re-import here. NOT a workspace — no scanned tree.">Export for editing</button>
<button id="importDatasetBtn" class="btn btn-secondary btn-sm" title="Load an edited classification JSON back in — replaces the current classifications. (To move a whole scanned workspace between browsers, use “Import workspace” on the welcome screen.)">Import edits</button>
<button id="exportDatasetBtn" class="btn btn-secondary btn-sm" title="Export the classification dataset (trees + assignments) as JSON">Export</button>
<button id="importDatasetBtn" class="btn btn-secondary btn-sm" title="Import a classification dataset JSON (replaces the current one)">Import</button>
<input type="file" id="importDatasetInput" accept="application/json,.json" hidden>
<button id="resetDatasetBtn" class="btn btn-sm btn-danger" title="Discard all classifications and start over from the raw scanned input (does not touch your files)">Reset</button>
<button id="copyOutputBtn" class="btn btn-primary btn-sm" disabled title="Copy mapped files to an output directory (source untouched)">Copy…</button>
@ -2421,12 +2415,8 @@ input.tfile__name:focus { border-color: var(--primary); background: var(--bg); o
<section id="workspacesSection" class="workspaces">
<div class="ws-head">
<h2>Your workspaces</h2>
<div class="ws-head__actions">
<button id="importWorkspaceBtn" class="btn btn-secondary" title="Import a whole workspace (.zddc-workspace.json) exported from another browser — restores the scanned snapshot so you don't re-scan">⭱ Import workspace</button>
<button id="newWorkspaceBtn" class="btn btn-primary">+ New workspace</button>
</div>
<input type="file" id="importWorkspaceInput" accept="application/json,.json" hidden>
</div>
<div id="workspaceList" class="ws-list"><!-- rendered --></div>
</section>
@ -5893,7 +5883,7 @@ X.B(E,Y);return E}return J}())
var url = URL.createObjectURL(blob);
var a = document.createElement('a');
a.href = url;
a.download = String(name).replace(/[^\w.-]+/g, '_') + '.zddc-classification.json';
a.download = String(name).replace(/[^\w.-]+/g, '_') + '.json';
document.body.appendChild(a); a.click(); a.remove();
URL.revokeObjectURL(url);
}
@ -7806,8 +7796,6 @@ X.B(E,Y);return E}return J}())
newBtn: document.getElementById('newWorkspaceBtn'),
wsBtn: document.getElementById('workspacesBtn'),
connectBtn: document.getElementById('connectDirBtn'),
importBtn: document.getElementById('importWorkspaceBtn'),
importInput: document.getElementById('importWorkspaceInput'),
};
if (!P() || !P().available) {
// No IndexedDB → hide the workspace UI; legacy rename path still works.
@ -7819,11 +7807,6 @@ X.B(E,Y);return E}return J}())
if (els.wsBtn) els.wsBtn.addEventListener('click', showWelcome);
if (els.connectBtn) els.connectBtn.addEventListener('click', function () { tryReconnect(false); });
if (els.list) els.list.addEventListener('click', onListClick);
if (els.importBtn) els.importBtn.addEventListener('click', function () { els.importInput.click(); });
if (els.importInput) els.importInput.addEventListener('change', function () {
if (this.files && this.files[0]) importWorkspace(this.files[0]);
this.value = '';
});
// Autosave the active workspace whenever the map changes.
C().on(scheduleAutosave);
@ -7879,15 +7862,10 @@ X.B(E,Y);return E}return J}())
var actions = document.createElement('div');
actions.className = 'ws-row__actions';
var titles = {
export: 'Export this whole workspace (scanned snapshot + classifications) to transfer to another browser or back up',
delete: 'Delete this workspace — your source files are untouched',
};
[['open', 'Open'], ['rename', 'Rename'], ['export', 'Export'], ['delete', 'Delete']].forEach(function (a) {
[['open', 'Open'], ['rename', 'Rename'], ['delete', 'Delete']].forEach(function (a) {
var b = document.createElement('button');
b.className = 'btn btn-sm ' + (a[0] === 'open' ? 'btn-primary' : 'btn-secondary');
b.dataset.act = a[0]; b.textContent = a[1];
if (titles[a[0]]) b.title = titles[a[0]];
actions.appendChild(b);
});
row.appendChild(main); row.appendChild(actions);
@ -7901,74 +7879,9 @@ X.B(E,Y);return E}return J}())
if (!id) return;
if (btn.dataset.act === 'open') openWorkspace(id);
else if (btn.dataset.act === 'rename') renameWorkspace(id);
else if (btn.dataset.act === 'export') exportWorkspace(id);
else if (btn.dataset.act === 'delete') deleteWorkspace(id);
}
// ── transfer (export / import a whole workspace as JSON) ─────────────────
// The scan is the expensive part (minutes on cloud mounts), so a workspace
// is portable: its snapshot + classify map travel as one JSON. The source
// directory HANDLE can't cross browsers, so an imported workspace has none —
// "Connect directory" re-attaches the folder once, without re-scanning.
async function exportWorkspace(id) {
var rows = await P().listWorkspaces();
var meta = rows.filter(function (r) { return r.id === id; })[0];
var rec = await P().getWorkspace(id);
if (!meta || !rec) { window.zddc.toast('Could not load that workspace to export.', 'error'); return; }
var payload = {
zddcWorkspace: 1,
exportedAt: new Date().toISOString(),
meta: { name: meta.name, rootName: meta.rootName, createdAt: meta.createdAt, updatedAt: meta.updatedAt, summary: meta.summary },
tree: rec.tree || [], // the scanned snapshot (no re-scan on the other side)
classify: rec.classify || {}, // assignments + target trees
};
var blob = new Blob([JSON.stringify(payload)], { type: 'application/json' });
var url = URL.createObjectURL(blob);
var a = document.createElement('a');
a.href = url;
a.download = String(meta.name || 'workspace').replace(/[^\w.-]+/g, '_') + '.zddc-workspace.json';
document.body.appendChild(a); a.click(); a.remove();
URL.revokeObjectURL(url);
window.zddc.toast('Exported “' + meta.name + '”. Import it in another browser to skip the re-scan.', 'success');
}
function importWorkspace(file) {
return new Promise(function (resolve) {
var reader = new FileReader();
reader.onload = function () {
var obj;
try { obj = JSON.parse(reader.result); }
catch (e) { window.zddc.toast('Import failed — not valid JSON.', 'error'); resolve(null); return; }
if (!obj || !obj.zddcWorkspace) {
window.zddc.toast(obj && obj.zddcClassifierFiles
? 'Thats a classification dataset, not a workspace — open a workspace and use Import in the toolbar.'
: 'Import failed — not a ZDDC workspace export.', 'error');
resolve(null); return;
}
var m = obj.meta || {};
var id = uid();
var meta = {
id: id,
name: m.name || 'Imported workspace',
rootName: m.rootName || '',
createdAt: m.createdAt || now(),
updatedAt: now(),
summary: m.summary || { files: 0, done: 0, excluded: 0 },
};
// No rootHandle — it can't be serialized across browsers; the
// user re-attaches via "Connect directory" after opening.
P().putWorkspace(meta, { id: id, rootHandle: null, tree: obj.tree || [], classify: obj.classify || {} })
.then(function () {
renderList();
window.zddc.toast('Imported “' + meta.name + '”. Open it, then “Connect directory” to re-attach the source folder for preview/copy.', 'success', { durationMs: 8000 });
resolve(id);
});
};
reader.onerror = function () { window.zddc.toast('Import failed — could not read the file.', 'error'); resolve(null); };
reader.readAsText(file);
});
}
// ── summary ───────────────────────────────────────────────────────────
function allFiles() {
var out = [];
@ -8145,12 +8058,9 @@ X.B(E,Y);return E}return J}())
showWelcome: showWelcome,
newWorkspace: newWorkspace,
openWorkspace: openWorkspace,
exportWorkspace: exportWorkspace,
importWorkspace: importWorkspace,
onRescanned: onRescanned,
renderList: renderList,
activeId: function () { return activeId; },
activeName: function () { return activeMeta ? activeMeta.name : null; },
};
})();

View file

@ -1671,7 +1671,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.27-beta · 2026-06-10 18:57:44 · e2c2d15</span></span>
<span class="build-timestamp"><span style="color:red;font-weight:bold">v0.0.27-beta · 2026-06-10 18:32:03 · 203674e</span></span>
</div>
</div>
<div class="header-right">

View file

@ -2770,7 +2770,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.27-beta · 2026-06-10 18:57:43 · e2c2d15</span></span>
<span class="build-timestamp"><span style="color:red;font-weight:bold">v0.0.27-beta · 2026-06-10 18:32:03 · 203674e</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,8 +1,8 @@
# Generated by build.sh — do not edit. One <app>=<build label> per line.
archive=v0.0.27-beta · 2026-06-10 18:57:44 · e2c2d15
transmittal=v0.0.27-beta · 2026-06-10 18:57:43 · e2c2d15
classifier=v0.0.27-beta · 2026-06-10 18:57:44 · e2c2d15
landing=v0.0.27-beta · 2026-06-10 18:57:44 · e2c2d15
form=v0.0.27-beta · 2026-06-10 18:57:44 · e2c2d15
tables=v0.0.27-beta · 2026-06-10 18:57:44 · e2c2d15
browse=v0.0.27-beta · 2026-06-10 18:57:44 · e2c2d15
archive=v0.0.27-beta · 2026-06-10 18:32:03 · 203674e
transmittal=v0.0.27-beta · 2026-06-10 18:32:03 · 203674e
classifier=v0.0.27-beta · 2026-06-10 18:32:03 · 203674e
landing=v0.0.27-beta · 2026-06-10 18:32:03 · 203674e
form=v0.0.27-beta · 2026-06-10 18:32:03 · 203674e
tables=v0.0.27-beta · 2026-06-10 18:32:03 · 203674e
browse=v0.0.27-beta · 2026-06-10 18:32:03 · 203674e

View file

@ -1722,7 +1722,7 @@ body.is-elevated::after {
</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.27-beta · 2026-06-10 18:57:44 · e2c2d15</span></span>
<span class="build-timestamp"><span style="color:red;font-weight:bold">v0.0.27-beta · 2026-06-10 18:32:03 · 203674e</span></span>
</div>
</div>
<div class="header-right">