diff --git a/zddc/internal/apps/embedded/archive.html b/zddc/internal/apps/embedded/archive.html index 7353922..8f708fa 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:57:44 · e2c2d15 + v0.0.27-beta · 2026-06-10 19:57:21 · 5f1df08
diff --git a/zddc/internal/apps/embedded/browse.html b/zddc/internal/apps/embedded/browse.html index ff36075..26930d8 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:57:44 · e2c2d15 + v0.0.27-beta · 2026-06-10 19:57:21 · 5f1df08
diff --git a/zddc/internal/apps/embedded/classifier.html b/zddc/internal/apps/embedded/classifier.html index 3336cc8..1d54d93 100644 --- a/zddc/internal/apps/embedded/classifier.html +++ b/zddc/internal/apps/embedded/classifier.html @@ -1672,6 +1672,7 @@ body.is-elevated::after { .cl-dot--done { background: var(--success); border-color: var(--success); } .cl-dot--excluded { background: var(--text-muted); border-color: var(--text-muted); opacity: 0.6; } .file-item.excluded .file-name { text-decoration: line-through; color: var(--text-muted); } +.folder-item.excluded .folder-name { text-decoration: line-through; color: var(--text-muted); } /* placed-file row in the target pane is clickable (reveal in source) */ .tfile { cursor: pointer; } @@ -2239,7 +2240,7 @@ input.tfile__name:focus { border-color: var(--primary); background: var(--bg); o
ZDDC Classifier - v0.0.27-beta · 2026-06-10 18:57:44 · e2c2d15 + v0.0.27-beta · 2026-06-10 19:57:21 · 5f1df08
@@ -7326,6 +7327,15 @@ X.B(E,Y);return E}return J}()) }); notify(); } + // Forget any assignment for these source keys (e.g. when a .zip flips + // between single-file and folder mode and the old keys cease to exist). + function dropAssignments(keys) { + var changed = false; + (keys || []).forEach(function (k) { + if (state.assignments[k]) { delete state.assignments[k]; changed = true; } + }); + if (changed) notify(); + } function setTitleOverride(key, title) { var a = assignmentFor(key); a.titleOverride = title && title.trim() ? title.trim() : null; @@ -7655,12 +7665,18 @@ X.B(E,Y);return E}return J}()) function parseFolderLevels(name) { var s = String(name == null ? '' : name).trim(); if (!s) return []; - var segs = s.split('-'); - var last = segs.pop(); - var u = last.indexOf('_'); - if (u >= 0) { segs.push(last.slice(0, u)); segs.push(last.slice(u + 1)); } - else { segs.push(last); } - return segs.map(function (x) { return x.trim(); }).filter(Boolean); + var u = s.indexOf('_'); // the "_" separates the tracking number from the leaf + if (u < 0) { + // No "_" → a pure tracking-number path: nest by "-". + return s.split('-').map(function (x) { return x.trim(); }).filter(Boolean); + } + // Tracking number (before "_") nests by "-"; everything AFTER the "_" is + // ONE leaf, kept whole — the revision may itself contain hyphens, e.g. a + // date revision "2025-11-17 (IFI)". + var segs = s.slice(0, u).split('-').map(function (x) { return x.trim(); }).filter(Boolean); + var leaf = s.slice(u + 1).trim(); + if (leaf) segs.push(leaf); + return segs; } // Children array for a tracking node (or the roots for null), or null. function trackingChildren(parentId) { @@ -7747,7 +7763,7 @@ X.B(E,Y);return E}return J}()) srcKeyForFile: srcKeyForFile, defaultTitle: defaultTitle, // assignments assignmentFor: assignmentFor, getAssignment: getAssignment, - place: place, setExcluded: setExcluded, + place: place, setExcluded: setExcluded, dropAssignments: dropAssignments, setTitleOverride: setTitleOverride, // trees addTrackingNode: addTrackingNode, addParty: addParty, @@ -8557,18 +8573,9 @@ X.B(E,Y);return E}return J}()) fo.folderPath = node.path; files.push(fo); if (scanStats) scanStats.files++; - if (fo.extension === 'zip' && typeof JSZip !== 'undefined') { - // Don't read the archive during the listing — make an - // expandable, lazy zip node scanned on open (scanZipNode). - const zipName = zddc.joinExtension(fo.originalFilename, fo.extension); - const zipPath = node.path + '/' + zipName; - const zh = { name: zipName, kind: 'directory', isZipRoot: true, zipPath: zipPath }; - const zipNode = makeNode(zh, zipPath, node); - zipNode._zipFileObj = fo; - zipNode.scanState = 'zip-pending'; - childDirs.push(zipNode); - if (scanStats) scanStats.folders++; - } + // A .zip is a single file by default (one classifiable unit). + // The user can later "Expand as folder" (expandZipAsFolder) to + // pull its members into the fileset. } else if (entry.kind === 'directory') { const childPath = node.path + '/' + entry.name; childDirs.push(makeNode(entry, childPath, node)); @@ -9103,6 +9110,76 @@ X.B(E,Y);return E}return J}()) if (!entry) throw new Error('zip member not found: ' + fileObj.zipEntryPath); return await entry.async('blob'); } + + // ── per-zip mode toggle (single file ⇄ expandable folder) ─────────────── + function findNodeByPath(path) { + var hit = null; + (function walk(ns) { (ns || []).forEach(function (n) { if (hit) return; if (n.path === path) hit = n; else walk(n.children); }); })(window.app.folderTree || []); + return hit; + } + // Recompute subtree totals after a structural change (expand/collapse a zip). + function recomputeTotals() { + (function walk(ns) { + (ns || []).forEach(function (n) { + walk(n.children || []); + var rf = (n.files || []).length, rd = (n.children || []).length; + (n.children || []).forEach(function (c) { rf += c.runFiles || 0; rd += c.runDirs || 0; }); + n.runFiles = rf; n.runDirs = rd; + n.fileCount = (n.files || []).length; n.subdirCount = (n.children || []).length; + }); + })(window.app.folderTree || []); + } + // Turn a .zip FILE into an expandable archive folder in place: scan its + // members into the fileset and drop the now-meaningless single-file + // assignment. Members come from the live handle, or (snapshot-restored) are + // re-read from the workspace root via scanZipNode's fallback. + async function expandZipAsFolder(file) { + var parent = findNodeByPath(file.folderPath); + if (!parent) return null; + var zipName = zddc.joinExtension(file.originalFilename, file.extension); + var zipPath = parent.path + '/' + zipName; + var zipNode = makeNode({ name: zipName, kind: 'directory', isZipRoot: true, zipPath: zipPath }, zipPath, parent); + zipNode._zipFileObj = file.handle ? file : null; // null → scanZipNode resolves from root + zipNode.scanState = 'zip-pending'; + zipNode.expanded = true; + parent.files = (parent.files || []).filter(function (f) { return f !== file; }); + parent.children = parent.children || []; + parent.children.push(zipNode); + var c = window.app.modules.classify; + if (c && c.dropAssignments) c.dropAssignments([c.srcKeyForFile(file)]); + await scanZipNode(zipNode); + recomputeTotals(); + return zipNode; + } + // Collapse an expanded archive folder back to a single .zip file, dropping + // every member's assignment. + function collapseZipToFile(zipNode) { + if (!zipNode || !zipNode.isZipRoot) return null; + var parent = zipNode.parent || findNodeByPath(zipNode.path.slice(0, zipNode.path.lastIndexOf('/'))); + if (!parent) return null; + var c = window.app.modules.classify; + if (c && c.dropAssignments) { + var keys = []; + (function walk(n) { (n.files || []).forEach(function (f) { keys.push(c.srcKeyForFile(f)); }); (n.children || []).forEach(walk); })(zipNode); + c.dropAssignments(keys); + } + var split = zddc.splitExtension(zipNode.name); + var file = { + handle: (zipNode._zipFileObj && zipNode._zipFileObj.handle) || null, + folderHandle: (zipNode._zipFileObj && zipNode._zipFileObj.folderHandle) || null, + originalFilename: split.name, extension: split.extension, + size: null, lastModified: null, + trackingNumber: '', revision: '', status: '', title: '', + isDirty: false, error: false, errorMessage: '', validation: null, sha256: null, + folderPath: parent.path, + }; + parent.children = (parent.children || []).filter(function (n) { return n !== zipNode; }); + parent.files = parent.files || []; + parent.files.push(file); + zipCache.delete(zipNode.zipPath); + recomputeTotals(); + return file; + } async function resolveDirHandle(rootHandle, relPath) { var cur = rootHandle; var parts = (relPath || '').split('/').filter(Boolean); @@ -9172,6 +9249,8 @@ X.B(E,Y);return E}return J}()) resolveDirHandle, ensureZipLoaded, extractZipMember, + expandZipAsFolder, + collapseZipToFile, resumeScan }; })(); @@ -9486,10 +9565,12 @@ X.B(E,Y);return E}return J}()) } item.appendChild(icon); - // Classify mode: an aggregate state dot for the folder's subtree. + // Classify mode: an aggregate state dot for the folder's subtree, and a + // struck-through name when the WHOLE subtree is excluded (mirrors files). if (classifyOn()) { const agg = aggregateState(subtreeFiles(folder)); if (agg) item.appendChild(stateDot(agg)); + if (agg === 'excluded') item.classList.add('excluded'); } // Folder name @@ -10078,7 +10159,31 @@ X.B(E,Y);return E}return J}()) } } - // ── context menu (exclude / include / clear) ─────────────────────────── + // ── per-zip mode toggle (single file ⇄ expandable folder) ─────────────── + function persistTreeChange() { + var ws = window.app.modules.workspace; + if (ws && ws.onRescanned) ws.onRescanned(); + } + async function expandZip(file) { + if (!file.handle && !window.app.rootHandle) { + if (window.zddc) window.zddc.toast('Connect the source directory first to expand this archive.', 'warning'); + return; + } + try { + var node = await window.app.modules.scanner.expandZipAsFolder(file); + if (node) { render(); persistTreeChange(); } + } catch (e) { + if (window.zddc) window.zddc.toast('Couldn’t expand the archive — ' + (e.message || e), 'error'); + } + } + function collapseZip(zipNode) { + if (!zipNode) return; + window.app.modules.scanner.collapseZipToFile(zipNode); + render(); + persistTreeChange(); + } + + // ── context menu (exclude / include / clear / zip mode) ───────────────── var menuEl = null; function hideMenu() { if (menuEl) { menuEl.remove(); menuEl = null; } } function showMenu(x, y, items) { @@ -10117,15 +10222,26 @@ X.B(E,Y);return E}return J}()) if (a.trackingNodeId) items.push({ label: 'Clear tracking', fn: function () { c.place([key], null, 'tracking'); } }); if (a.transmittalNodeId) items.push({ label: 'Clear transmittal', fn: function () { c.place([key], null, 'transmittal'); } }); } + var file = findFileByKey(key); + if (file && file.isVirtual) { + items.push({ label: 'Collapse archive to single file', fn: function () { collapseZip(findFolderByPath(file.zipPath)); } }); + } else if (file && file.extension === 'zip') { + items.push({ label: 'Expand as folder', fn: function () { expandZip(file); } }); + } } else { var folder = findFolderByPath(folderEl.dataset.path); + if (folder && folder.isZipRoot) { + items.push({ label: 'Collapse to single file', fn: function () { collapseZip(folder); } }); + } var keys = keysFor(subtreeFiles(folder || { files: [], children: [] })); - if (!keys.length) return; - var allExcl = keys.every(function (k) { var a = c.getAssignment(k); return a && a.excluded; }); - items.push({ - label: (allExcl ? 'Include' : 'Exclude') + ' folder (' + keys.length + ' file' + (keys.length === 1 ? '' : 's') + ')', - fn: function () { c.setExcluded(keys, !allExcl); }, - }); + if (keys.length) { + var allExcl = keys.every(function (k) { var a = c.getAssignment(k); return a && a.excluded; }); + items.push({ + label: (allExcl ? 'Include' : 'Exclude') + ' folder (' + keys.length + ' file' + (keys.length === 1 ? '' : 's') + ')', + fn: function () { c.setExcluded(keys, !allExcl); }, + }); + } + if (!items.length) return; } showMenu(e.clientX, e.clientY, items); } diff --git a/zddc/internal/apps/embedded/index.html b/zddc/internal/apps/embedded/index.html index 72f3c91..6ded5c0 100644 --- a/zddc/internal/apps/embedded/index.html +++ b/zddc/internal/apps/embedded/index.html @@ -1656,6 +1656,113 @@ body { font-style: italic; } +/* ── New-project dialog ──────────────────────────────────────────────────── */ +.np-modal { + position: fixed; + inset: 0; + z-index: 1000; + display: flex; + align-items: flex-start; + justify-content: center; + overflow-y: auto; + padding: 3rem 1rem; +} +.np-modal__backdrop { + position: fixed; + inset: 0; + background: rgba(0, 0, 0, 0.45); +} +.np-modal__dialog { + position: relative; + background: var(--bg); + color: var(--text); + border: 1px solid var(--border); + border-radius: var(--radius); + box-shadow: 0 10px 40px rgba(0, 0, 0, 0.3); + width: 100%; + max-width: 560px; + padding: 1.25rem 1.5rem 1.5rem; + margin: auto; +} +.np-modal__head { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: 0.25rem; +} +.np-modal__head h2 { margin: 0; font-size: 1.25rem; } +.np-modal__close { + background: none; + border: none; + font-size: 1.5rem; + line-height: 1; + color: var(--text-muted); + cursor: pointer; + padding: 0.1rem 0.4rem; +} +.np-modal__close:hover { color: var(--text); } +.np-help { color: var(--text-muted); font-size: 0.85rem; margin: 0 0 1rem; } +.np-field { display: block; margin-bottom: 0.75rem; font-size: 0.85rem; color: var(--text-secondary, var(--text-muted)); } +.np-field input { + display: block; + width: 100%; + margin-top: 0.25rem; + padding: 0.4rem 0.55rem; + border: 1px solid var(--border); + border-radius: var(--radius); + background: var(--bg-secondary, var(--bg)); + color: var(--text); + font-size: 0.9rem; + box-sizing: border-box; +} +.np-err { display: block; color: var(--danger); font-size: 0.8rem; margin-top: 0.25rem; } +.np-grouphdr { font-size: 0.95em; margin: 1rem 0 0.3rem; font-weight: 600; } +.np-sub { font-weight: 400; color: var(--text-muted); font-size: 0.8rem; } +.np-list { display: flex; flex-direction: column; gap: 0.35rem; margin-bottom: 0.4rem; } +.np-row { display: flex; gap: 0.4rem; align-items: center; } +.np-row__input { flex: 1; } +.np-row__verbs { flex: 0 0 8rem; } +.np-row input { + padding: 0.35rem 0.5rem; + border: 1px solid var(--border); + border-radius: var(--radius); + background: var(--bg-secondary, var(--bg)); + color: var(--text); + font-size: 0.88rem; + box-sizing: border-box; +} +.np-del { + flex: 0 0 auto; + background: none; + border: 1px solid var(--border); + border-radius: var(--radius); + color: var(--text-muted); + cursor: pointer; + width: 1.9rem; + height: 1.9rem; + font-size: 1.1rem; + line-height: 1; +} +.np-del:hover { color: var(--danger); border-color: var(--danger); } +.np-add { + background: none; + border: 1px dashed var(--border); + border-radius: var(--radius); + color: var(--primary); + cursor: pointer; + font-size: 0.82rem; + padding: 0.3rem 0.6rem; +} +.np-add:hover { border-color: var(--primary); } +.np-actions { + display: flex; + justify-content: flex-end; + gap: 0.5rem; + margin-top: 1.25rem; + border-top: 1px solid var(--border); + padding-top: 1rem; +} + @@ -1671,7 +1778,7 @@ body {
ZDDC - v0.0.27-beta · 2026-06-10 18:57:44 · e2c2d15 + v0.0.27-beta · 2026-06-10 19:57:21 · 5f1df08
@@ -1734,6 +1841,10 @@ body {

Projects

+
+ + +
@@ -1743,6 +1854,53 @@ body {
+ + +