web: fold the whole roll from the head of the tree
A roll read to the bottom of its dates is a long column of indented rows, and
the only way to shut it was one row at a time — the click that folds a row also
opens it, which is the right trade for reading and a poor one for putting away.
The column's own name is now the whole tree's control: a right click there opens
a menu whose one item is COLLAPSE ALL / THU GỌN TẤT CẢ, and it folds every row
that has something under it at once — the root's own row included, so the tree
comes back to one line per picked folder and the subfolder two deep is away with
the rest. The menu is the folder menu's shape at a third subject rather than a
second menu: one `root` flag on the state that already knows where a right click
landed, and the same Escape, same click-away, same clamp to the window edge.
The frames do not move with the rows: folding is a way of looking at the tree,
and the strip already reads the open folder, which the fold leaves alone. The
name keeps reading as the folder it belongs to — the folder's label, with the
hint appended to its title — and the item is disabled rather than hidden when
nothing in the column has children, so a fresh folder does not open a menu with
a dead line in it.
Verified:
library-check.mjs — 34 steps, all passed, 2 new: a right click on the root
name offers exactly [lib-menu-collapse] while the name still reads "Roll A",
and one click takes the fully open roll (4 rows: the picked folder, its
subfolder, the subfolder under that, and one holding no frame) to a single
row. The fold is measured against the ruler of opening the root back up:
the 3 rows that return are CheckRoll, CheckRoll/2026 and CheckRoll/Empty —
CheckRoll/2026/04 is still shut, which is what says the deep row went with
the fold and not just the root. The strip held its 2 tiles throughout, and
the folder the visit was on (lib-node-CheckRoll/2026) is what the reload
below still reopens on.
scan-nav-check.mjs — all steps passed; roll-walk-check.mjs — passed.
frontend tsc --noEmit clean. Live 8090, served asset index-DZFlTBDG.js
matching dist/: /, /library and /app all 200 and 0 console errors.
ponytail: only folding, no unfold-all to match — the click on a row already
opens it and the root's own row is one click away; add the pair when the column
routinely holds many picked folders and opening them one by one stops being
cheap.
This commit is contained in:
@@ -373,6 +373,53 @@ step('a right click on the empty column offers a folder', blank.join(',') === 'l
|
||||
await page.keyboard.press('Escape');
|
||||
step('escape closes the folder menu too', (await page.$('[data-key="lib-menu"]')) === null);
|
||||
|
||||
// The head of the tree is the whole tree's control: a right click on the column's
|
||||
// own name folds every row that has something under it, so a roll read to the
|
||||
// bottom of its dates comes back to its own line. The frames in the strip are the
|
||||
// strip's business and do not move with it. The subfolder opened above carries a
|
||||
// fold of its own, so it is opened first: the claim is what ONE click does to
|
||||
// every row at once, not to the one that was already shut.
|
||||
await page.click('[data-key="lib-node-CheckRoll/2026"]');
|
||||
await page.waitForFunction(() => document.querySelectorAll('[data-key^="lib-node-"]').length === 4, null, {
|
||||
timeout: 10_000,
|
||||
polling: 50,
|
||||
});
|
||||
const deepRows = await page.$$eval('[data-key^="lib-node-"]', (els) => els.length);
|
||||
const deepTiles = await page.$$eval(thumbs, (els) => els.length);
|
||||
await page.click('[data-key="lib-root-name"]', { button: 'right' });
|
||||
await page.waitForSelector('[data-key="lib-menu"]', { timeout: 10_000 });
|
||||
const head = await page.$$eval('[data-key="lib-menu"] button', (els) => els.map((b) => b.dataset.key));
|
||||
step('a right click on the root name offers to fold the tree', head.join(',') === 'lib-menu-collapse', JSON.stringify(head));
|
||||
step(
|
||||
'the column name still reads as the folder it belongs to',
|
||||
(await page.$eval('[data-key="lib-root-name"]', (e) => e.textContent)) === 'Roll A',
|
||||
await page.getAttribute('[data-key="lib-root-name"]', 'title')
|
||||
);
|
||||
await page.click('[data-key="lib-menu-collapse"]');
|
||||
await page.waitForFunction(() => document.querySelectorAll('[data-key^="lib-node-"]').length === 1, null, {
|
||||
timeout: 10_000,
|
||||
polling: 50,
|
||||
});
|
||||
const flatTiles = await page.$$eval(thumbs, (els) => els.length);
|
||||
const shutRoot = await page.getAttribute('[data-key="lib-node-CheckRoll"]', 'aria-expanded');
|
||||
// Opening the root back up is the ruler for the rest: only the rows the fold shut
|
||||
// are still away, and the subfolder two deep is one of them.
|
||||
await page.click('[data-key="lib-node-CheckRoll"]');
|
||||
await page.waitForFunction(() => document.querySelectorAll('[data-key^="lib-node-"]').length === 3, null, {
|
||||
timeout: 10_000,
|
||||
polling: 50,
|
||||
});
|
||||
const revealed = await page.$$eval('[data-key^="lib-node-"]', (els) => els.map((e) => e.dataset.key));
|
||||
step(
|
||||
'and one click folds every row that has something under it',
|
||||
deepRows === 4 && shutRoot === 'false' && flatTiles === deepTiles && revealed.join(',') === 'lib-node-CheckRoll,lib-node-CheckRoll/2026,lib-node-CheckRoll/Empty',
|
||||
`${deepRows} rows → 1, ${revealed.length} back with the root opened, ${flatTiles} tiles kept`,
|
||||
);
|
||||
// The open folder is the visit's, not the fold's, so it is put back where the
|
||||
// sections below left it.
|
||||
await page.click('[data-key="lib-node-CheckRoll/2026"]');
|
||||
await page.waitForFunction((sel) => document.querySelectorAll(sel).length === 2, thumbs, { timeout: 30_000 });
|
||||
|
||||
// The divider drags the column, and what the visit leaves behind — the width the
|
||||
// column was left at and the folder that was open — is what the next visit opens
|
||||
// with. The right click on the empty column answered with a folder, not with a
|
||||
|
||||
Reference in New Issue
Block a user