web: fold all keeps the top-level row open

COLLAPSE ALL folded the top-level row as well, and that row is already its own
fold: one click on its name shuts it and takes the whole subtree with it. Folding
it from the menu hid every row of the tree — the item repeated a click the reader
already had, while the one thing a click there cannot reach, the folders nested
below, was the part that got lost with it.

So the item folds the folders nested below the top level: every row with a
subfolder under it that is not itself a folder picked at the top level. The
top-level row stays drawn and stays open, its own subfolders are drawn folded,
and no top-level row ever leaves the column. `nested` is computed off `parents`
once, and the item is disabled on a folder with nothing nested under it.

Verified:
  library-check.mjs — 35 steps, all passed. The fold step now reads the rows the
    click leaves behind instead of a row count: the fully open roll (4 rows)
    becomes exactly CheckRoll aria-expanded=true, CheckRoll/2026
    aria-expanded=false, CheckRoll/Empty (a leaf, so no aria-expanded) —
    CheckRoll/2026/04 is away, the top-level row is not — with the strip's 2
    tiles unmoved and the row menu still leading with lib-menu-collapse.
  frontend tsc --noEmit clean. Live 8090 on index-DAIpyDNV.js matching dist/:
  /, /library and /app 200 with 0 console errors.

ponytail: with several folders picked, one COLLAPSE ALL folds the nested rows of
all of them, not only the row that was clicked — no top-level row is hidden
either way; scope it to the clicked folder's prefix when a column routinely
holds many picked folders.
This commit is contained in:
2026-09-28 21:23:56 +07:00
parent 4a740b1e28
commit 5e28b42067
2 changed files with 31 additions and 33 deletions
+15 -10
View File
@@ -109,7 +109,7 @@ export function Library() {
// right click landed on the empty part of the column, which is the way in for
// one more folder; `root` means it landed on the head of a tree — the column's
// own name, or the row of a folder picked at the top level — which carries
// folding every row that has something under it.
// folding the folders nested below the top level.
const [menu, setMenu] = useState<{ folder: LibraryFolder | null; root?: boolean; x: number; y: number } | null>(null);
// Where the drag of the column divider started, and how wide the column was
// then — the pointer only ever reports a position, not a distance moved.
@@ -355,6 +355,11 @@ export function Library() {
[nodes]
);
// The folders nested below a top-level row — what COLLAPSE ALL has to fold. A
// top-level row is not one of them: it is its own fold, since one click on its
// name shuts it, so the menu item would only repeat the row's own click.
const nested = useMemo(() => new Set([...parents].filter((key) => key.includes('/'))), [parents]);
// What the column draws: a folded row takes its whole subtree with it.
const rows = useMemo(
() =>
@@ -517,7 +522,7 @@ export function Library() {
{/* The column is titled with the folder the tree belongs to, so
a renamed roll is named where its frames are named. The name
is the whole tree's handle as well: one right click here
folds every row that has something under it. */}
folds the folders nested below the top level. */}
<b
className="lib-col-title"
data-key="lib-root-name"
@@ -566,9 +571,9 @@ export function Library() {
// act on the folder that was picked, so a right click on a
// subfolder row brings up its roll's menu. A row at the top
// level is a whole tree's head, so it also carries folding
// the tree — the item the column's own name has, where a
// reader actually right-clicks. Shift+F10 raises the same
// event, which is the keyboard's way in.
// what sits under it — the item the column's own name has,
// where a reader actually right-clicks. Shift+F10 raises
// the same event, which is the keyboard's way in.
onContextMenu={(e) => {
if (!folder) return;
e.preventDefault();
@@ -725,18 +730,18 @@ export function Library() {
{menu ? (
<div className="lib-menu" data-key="lib-menu" style={{ left: menu.x, top: menu.y }} onContextMenu={(e) => e.preventDefault()}>
{/* Fold-the-tree leads the menu of a top-level row, and stands alone on
the column's own name: every row that has something under it goes,
so a long roll comes back to one line per folder and the click that
folds a row still opens it again one at a time. */}
the column's own name: the folders nested below the top level go,
one line per folder, and no top-level row ever leaves the column —
the row itself is one click away and its click already shuts it. */}
{menu.root ? (
<button
type="button"
className="lib-menu-item"
data-key="lib-menu-collapse"
disabled={parents.size === 0}
disabled={nested.size === 0}
onClick={() => {
setMenu(null);
setFolded(new Set(parents));
setFolded(new Set(nested));
}}
>
{t('lib.collapseAll')}