fix(classifier): fit-to-content catalog columns + sorted Folder Tree

Two UI fixes:

- "By existing" catalog columns were far too wide. The seltable forced the
  table to width:100% (auto-layout then stretches columns) and — in the
  classifier's copy — the per-column filter <input>s had no styling, so each
  fell back to its ~170px intrinsic width and dictated the column width. Set
  the table to width:auto (cells are already nowrap → fit header/longest cell)
  and style .seltable__colfilter to fill its column (min-width:2rem,
  box-sizing:border-box) so the inputs never widen a column. Applied to both
  the classifier copy and shared/seltable.css (same fix for the tables tool's
  "Add from archive" table).
- The left Folder Tree rendered folders and files in raw scan order. Sort both
  at render — case-insensitive, natural (so "Rev 2" precedes "Rev 10") — via a
  non-mutating slice().sort() at each render point in tree.js.

Tests: a new spec asserts the natural/case-insensitive tree order; 62 classify
+ classifier green (108 across classify/classifier/tables/tables-mdl).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
ZDDC 2026-06-12 10:00:23 -05:00
parent 8e10e5e5e6
commit cfdf0f6db9
4 changed files with 54 additions and 6 deletions

View file

@ -613,12 +613,21 @@ input.tfile__name:focus { border-color: var(--primary); background: var(--bg); o
}
.seltable__count { color: var(--text-muted); font-size: 0.78rem; white-space: nowrap; }
.seltable__scroll { flex: 1; min-height: 0; overflow: auto; }
.seltable__table { border-collapse: separate; border-spacing: 0; width: 100%; font-size: 0.82rem; }
/* width:auto + nowrap cells → each column shrinks to fit its header/longest cell. */
.seltable__table { border-collapse: separate; border-spacing: 0; width: auto; font-size: 0.82rem; }
.seltable__table th, .seltable__table td { border-bottom: 1px solid var(--border); padding: 0.25rem 0.5rem; text-align: left; white-space: nowrap; }
.seltable__table thead th {
position: sticky; top: 0; z-index: 2; background: var(--bg-secondary, var(--bg));
color: var(--text-muted); font-size: 0.68rem; font-weight: 700; letter-spacing: 0.04em; text-transform: uppercase;
}
/* Per-column filter inputs: fill the column (min-width:0-ish) so they never
force a column wider than its header/cells. */
.seltable__table thead tr.seltable__filters th { padding: 0.08rem 0.3rem; }
.seltable__colfilter {
width: 100%; min-width: 2rem; box-sizing: border-box;
padding: 0.1rem 0.3rem; border: 1px solid var(--border); border-radius: var(--radius);
background: var(--bg); color: var(--text); font-size: 0.72rem; font-weight: 400; letter-spacing: 0; text-transform: none;
}
.seltable__row { cursor: pointer; user-select: none; }
.seltable__row:hover { background: var(--bg-hover); }
.seltable__row.is-selected { background: var(--primary-light, rgba(37,99,235,0.12)); }

View file

@ -5,6 +5,17 @@
(function() {
'use strict';
// ── Sorting ────────────────────────────────────────────────────────────
// Render the tree in a stable, human order: case-insensitive, natural
// (so "Rev 2" sorts before "Rev 10"). Non-mutating — sort copies at render.
function cmpName(a, b) { return String(a).localeCompare(String(b), undefined, { numeric: true, sensitivity: 'base' }); }
function sortedFolders(list) { return (list || []).slice().sort(function (a, b) { return cmpName(a.name, b.name); }); }
function sortedFiles(list) {
return (list || []).slice().sort(function (a, b) {
return cmpName(window.zddc.joinExtension(a.originalFilename, a.extension), window.zddc.joinExtension(b.originalFilename, b.extension));
});
}
// ── Classify & Copy helpers ────────────────────────────────────────────
function classifyOn() {
var c = window.app.modules.classify;
@ -175,7 +186,7 @@
return;
}
window.app.folderTree.forEach(folder => {
sortedFolders(window.app.folderTree).forEach(folder => {
if (!folderShown(folder)) return;
const element = createFolderElement(folder);
container.appendChild(element);
@ -360,7 +371,7 @@
if ((folder.expanded || autoOpen(folder)) && folder.children && folder.children.length > 0) {
const childrenDiv = document.createElement('div');
childrenDiv.className = 'folder-children';
folder.children.forEach(child => {
sortedFolders(folder.children).forEach(child => {
if (!folderShown(child)) return;
const childElement = createFolderElement(child, level + 1);
childrenDiv.appendChild(childElement);
@ -373,7 +384,7 @@
if (classifyOn() && (folder.expanded || autoOpen(folder)) && folder.files && folder.files.length > 0) {
const filesDiv = document.createElement('div');
filesDiv.className = 'folder-children folder-files';
folder.files.forEach(function (file) {
sortedFiles(folder.files).forEach(function (file) {
if (!fileShown(file)) return;
filesDiv.appendChild(createFileElement(file, level + 1));
});

View file

@ -5,7 +5,8 @@
.seltable__bar { display: flex; align-items: center; gap: 0.5rem; padding: 0.4rem 0.5rem; border-bottom: 1px solid var(--border); flex: 0 0 auto; }
.seltable__count { color: var(--text-muted); font-size: 0.78rem; white-space: nowrap; }
.seltable__scroll { flex: 1; min-height: 0; overflow: auto; }
.seltable__table { border-collapse: separate; border-spacing: 0; width: 100%; font-size: 0.82rem; }
/* width:auto + nowrap cells → each column shrinks to fit its header/longest cell. */
.seltable__table { border-collapse: separate; border-spacing: 0; width: auto; font-size: 0.82rem; }
.seltable__table th, .seltable__table td { border-bottom: 1px solid var(--border); padding: 0.25rem 0.5rem; text-align: left; white-space: nowrap; }
.seltable__table thead th {
position: sticky; top: 0; z-index: 2; background: var(--bg-secondary, var(--bg));
@ -13,7 +14,7 @@
}
.seltable__table thead tr.seltable__filters th { top: 1.55rem; padding: 0.15rem 0.35rem; }
.seltable__colfilter {
width: 100%; min-width: 5rem; padding: 0.15rem 0.35rem;
width: 100%; min-width: 2rem; box-sizing: border-box; padding: 0.15rem 0.35rem;
border: 1px solid var(--border); border-radius: var(--radius);
background: var(--bg); color: var(--text); font-size: 0.74rem; font-weight: 400; text-transform: none; letter-spacing: 0;
}

View file

@ -247,6 +247,33 @@ test('source file rows render with a state dot in classify mode', async ({ page
await expect(page.locator('#folderTree .file-item .cl-dot--none')).toBeVisible();
});
test('Folder Tree renders folders and files in natural, case-insensitive order', async ({ page }) => {
await page.click('#modeClassifyBtn');
const order = await page.evaluate(() => {
window.app.folderTree = [{
name: 'Root', path: 'Root', expanded: true, scanState: 'done',
children: [
{ name: 'Beta', path: 'Root/Beta', scanState: 'done', children: [], files: [] },
{ name: 'alpha', path: 'Root/alpha', scanState: 'done', children: [], files: [] },
{ name: 'Rev 10', path: 'Root/Rev 10', scanState: 'done', children: [], files: [] },
{ name: 'Rev 2', path: 'Root/Rev 2', scanState: 'done', children: [], files: [] },
],
files: [
{ originalFilename: 'zeta', extension: 'pdf', folderPath: 'Root' },
{ originalFilename: 'Apple', extension: 'pdf', folderPath: 'Root' },
{ originalFilename: 'banana', extension: 'pdf', folderPath: 'Root' },
],
}];
window.app.modules.tree.render();
return {
folders: Array.from(document.querySelectorAll('#folderTree .folder-children .folder-name')).map(e => e.textContent.trim()),
files: Array.from(document.querySelectorAll('#folderTree .folder-files .file-name')).map(e => e.textContent.trim()),
};
});
expect(order.folders).toEqual(['alpha', 'Beta', 'Rev 2', 'Rev 10']); // case-insensitive + natural (2 < 10)
expect(order.files).toEqual(['Apple.pdf', 'banana.pdf', 'zeta.pdf']); // case-insensitive
});
test('classify: single-click a source file triggers preview', async ({ page }) => {
await page.click('#modeClassifyBtn');
const previewed = await page.evaluate(() => {