Compare commits

..

3 commits

Author SHA1 Message Date
8368cc81b8 chore(embedded): cut v0.0.27-beta
Some checks failed
Notify chart dev on beta cut / notify-chart-dev (push) Failing after 8s
2026-06-10 13:57:51 -05:00
e2c2d1571d ux(classifier): relabel the two JSON surfaces so their purpose is clear
Two exports for two different consumers were both just "Export". Name them:
- Classification (header, AI round-trip): "Export for editing" / "Import edits"
  — a filename-per-file JSON with no scanned tree, meant to be hand/AI-edited
  and re-imported. Download suffix is now .zddc-classification.json.
- Workspace (welcome screen, transfer/backup): "Import workspace" + a row
  "Export" tooltip spelling out it carries the snapshot + classifications for
  moving between browsers. Download suffix stays .zddc-workspace.json.
Each tooltip points at the other so they're not confused.

Also wire workspace.activeName (referenced by the dataset export but never
exported), so a classification file is named after its workspace.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-10 13:56:09 -05:00
533b830d2c feat(classifier): export/import a whole workspace; fix short-viewport welcome
The scan is the expensive part (minutes on cloud mounts), so a workspace is now
portable between browsers/machines without re-scanning:
- Per-workspace "Export" downloads the snapshot + classify map as one
  <name>.zddc-workspace.json (the source-directory handle is omitted — it can't
  be serialized across browsers).
- Landing-page "Import" recreates the workspace from that JSON; the user clicks
  "Connect directory" once on the new browser to re-attach the folder (no
  re-scan — the snapshot carries the 2-hour walk). A classification-dataset JSON
  is rejected with a pointer to the in-app Import.

Also fix the welcome screen clipping its top on short viewports: the base
.empty-state centers with align-items:center, which overflows symmetrically and
puts the card's top out of scroll reach. Center the inner card with auto margins
instead — they collapse when it's taller than the viewport, keeping the top
reachable.

Test: workspace import recreates a transferable record (snapshot + map, no handle); 46 green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-10 13:50:58 -05:00
12 changed files with 234 additions and 25 deletions

View file

@ -287,7 +287,12 @@
}
/* ── 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 {
@ -322,7 +327,8 @@
/* ── 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; }
.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 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, '_') + '.json';
a.download = String(name).replace(/[^\w.-]+/g, '_') + '.zddc-classification.json';
document.body.appendChild(a); a.click(); a.remove();
URL.revokeObjectURL(url);
}

View file

@ -35,6 +35,8 @@
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.
@ -46,6 +48,11 @@
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);
@ -101,10 +108,15 @@
var actions = document.createElement('div');
actions.className = 'ws-row__actions';
[['open', 'Open'], ['rename', 'Rename'], ['delete', 'Delete']].forEach(function (a) {
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) {
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);
@ -118,9 +130,74 @@
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 = [];
@ -297,8 +374,11 @@
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="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>
<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>
<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,7 +208,11 @@
<section id="workspacesSection" class="workspaces">
<div class="ws-head">
<h2>Your workspaces</h2>
<button id="newWorkspaceBtn" class="btn btn-primary">+ New workspace</button>
<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,3 +883,32 @@ 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:32:03 · 203674e</span></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>
</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:32:03 · 203674e</span></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>
</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,7 +1457,12 @@ 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 {
@ -1492,7 +1497,8 @@ 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; }
.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 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); }
@ -2233,7 +2239,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:32:03 · 203674e</span></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>
</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>
@ -2369,8 +2375,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="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>
<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>
<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>
@ -2415,7 +2421,11 @@ 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>
<button id="newWorkspaceBtn" class="btn btn-primary">+ New workspace</button>
<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>
@ -5883,7 +5893,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, '_') + '.json';
a.download = String(name).replace(/[^\w.-]+/g, '_') + '.zddc-classification.json';
document.body.appendChild(a); a.click(); a.remove();
URL.revokeObjectURL(url);
}
@ -7796,6 +7806,8 @@ 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.
@ -7807,6 +7819,11 @@ 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);
@ -7862,10 +7879,15 @@ X.B(E,Y);return E}return J}())
var actions = document.createElement('div');
actions.className = 'ws-row__actions';
[['open', 'Open'], ['rename', 'Rename'], ['delete', 'Delete']].forEach(function (a) {
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) {
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);
@ -7879,9 +7901,74 @@ 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 = [];
@ -8058,9 +8145,12 @@ 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:32:03 · 203674e</span></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>
</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:32:03 · 203674e</span></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>
</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: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
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

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:32:03 · 203674e</span></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>
</div>
</div>
<div class="header-right">