diff --git a/zddc/internal/apps/embedded/archive.html b/zddc/internal/apps/embedded/archive.html index c11b4f2..7353922 100644 --- a/zddc/internal/apps/embedded/archive.html +++ b/zddc/internal/apps/embedded/archive.html @@ -2717,7 +2717,7 @@ td[data-field="trackingNumber"] {
ZDDC Archive - v0.0.27-beta · 2026-06-10 18:32:03 · 203674e + v0.0.27-beta · 2026-06-10 18:57:44 · e2c2d15
diff --git a/zddc/internal/apps/embedded/browse.html b/zddc/internal/apps/embedded/browse.html index a4d5837..ff36075 100644 --- a/zddc/internal/apps/embedded/browse.html +++ b/zddc/internal/apps/embedded/browse.html @@ -2824,7 +2824,7 @@ li.CodeMirror-hint-active {
ZDDC Browse - v0.0.27-beta · 2026-06-10 18:32:03 · 203674e + v0.0.27-beta · 2026-06-10 18:57:44 · e2c2d15
diff --git a/zddc/internal/apps/embedded/classifier.html b/zddc/internal/apps/embedded/classifier.html index c3dd32b..3336cc8 100644 --- a/zddc/internal/apps/embedded/classifier.html +++ b/zddc/internal/apps/embedded/classifier.html @@ -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
ZDDC Classifier - v0.0.27-beta · 2026-06-10 18:32:03 · 203674e + v0.0.27-beta · 2026-06-10 18:57:44 · e2c2d15
@@ -2369,8 +2375,8 @@ input.tfile__name:focus { border-color: var(--primary); background: var(--bg); o
| - - + + @@ -2415,7 +2421,11 @@ input.tfile__name:focus { border-color: var(--primary); background: var(--bg); o

Your workspaces

- +
+ + +
+
@@ -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 + ? 'That’s 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; }, }; })(); diff --git a/zddc/internal/apps/embedded/index.html b/zddc/internal/apps/embedded/index.html index 2156bc3..72f3c91 100644 --- a/zddc/internal/apps/embedded/index.html +++ b/zddc/internal/apps/embedded/index.html @@ -1671,7 +1671,7 @@ body {
ZDDC - v0.0.27-beta · 2026-06-10 18:32:03 · 203674e + v0.0.27-beta · 2026-06-10 18:57:44 · e2c2d15
diff --git a/zddc/internal/apps/embedded/transmittal.html b/zddc/internal/apps/embedded/transmittal.html index 5032969..5c269ca 100644 --- a/zddc/internal/apps/embedded/transmittal.html +++ b/zddc/internal/apps/embedded/transmittal.html @@ -2770,7 +2770,7 @@ dialog.modal--narrow {
ZDDC Transmittal - v0.0.27-beta · 2026-06-10 18:32:03 · 203674e + v0.0.27-beta · 2026-06-10 18:57:43 · e2c2d15
JavaScript not available