web: fold the tree from the row a reader right-clicks

COLLAPSE ALL lived only on the column's own name — the label above the tree —
and that label scrolls with the list it heads: on a roll read deep the column is
scrolled past it, so the item looked like it only appeared once the top-level row
had been folded to bring the list back to the top. The row is where the pointer
already is, and it already carries the folder's menu.

The top-level folder row now leads its own menu with COLLAPSE ALL — rename,
rescan and remove follow it, and a subfolder row is unchanged, since folding a
whole tree is what a row that a tree hangs from can do and a row inside one
cannot. Both entry points run the same one: `root` on the menu state now means
"the head of a tree", the column's own name or the row of a folder picked at the
top level, and the menu draws the item first and then whatever the subject
itself has — the folder's items, or, on the empty part of the column, ADD
FOLDER. The empty part keeps ADD FOLDER alone: it is not the head of a tree.

Verified:
  library-check.mjs — 35 steps, all passed. The new step right-clicks
    lib-node-CheckRoll with the tree fully open (4 rows) and gets back exactly
    [lib-menu-collapse, lib-rename-CheckRoll, lib-rescan-CheckRoll,
    lib-drop-CheckRoll]; a right click on the column's name still answers with
    lib-menu-collapse alone; the subfolder menu above is still the three folder
    items with no fold in it; the empty column still offers lib-menu-add alone.
    The fold itself is unchanged and still measured by opening the root back up:
    4 rows → 1, and 3 back, with CheckRoll/2026/04 still away.
  frontend tsc --noEmit clean. Live 8090 on index-DEvUJDNY.js matching dist/:
  /, /library and /app 200 with 0 console errors.

ponytail: the column's name is still the second way to the same item and still
scrolls with the list — left alone because the row is now the one that matters;
make the name sticky when a roll routinely fills the column past one screen.
This commit is contained in:
2026-09-28 21:20:23 +07:00
parent c1ee2cf30a
commit 4a740b1e28
2 changed files with 43 additions and 23 deletions
+12 -1
View File
@@ -386,10 +386,21 @@ await page.waitForFunction(() => document.querySelectorAll('[data-key^="lib-node
});
const deepRows = await page.$$eval('[data-key^="lib-node-"]', (els) => els.length);
const deepTiles = await page.$$eval(thumbs, (els) => els.length);
// The row is where a reader right-clicks, so fold-the-tree leads that row's own
// menu rather than living only on the column's name above it.
await page.click('[data-key="lib-node-CheckRoll"]', { button: 'right' });
await page.waitForSelector('[data-key="lib-menu"]', { timeout: 10_000 });
const rowMenu = await page.$$eval('[data-key="lib-menu"] button', (els) => els.map((b) => b.dataset.key));
step(
'a right click on the top-level folder row offers to fold the tree',
rowMenu.join(',') === 'lib-menu-collapse,lib-rename-CheckRoll,lib-rescan-CheckRoll,lib-drop-CheckRoll',
JSON.stringify(rowMenu),
);
await page.keyboard.press('Escape');
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('a right click on the column name offers it alone', 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',