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:
@@ -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',
|
||||
|
||||
@@ -107,8 +107,9 @@ export function Library() {
|
||||
const [note, setNote] = useState<string | null>(null);
|
||||
// The folder menu: what a right click opens, and where. No folder means the
|
||||
// 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 column's own name — the head
|
||||
// of the tree, whose one act is folding the whole roll.
|
||||
// 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.
|
||||
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.
|
||||
@@ -563,14 +564,18 @@ export function Library() {
|
||||
}}
|
||||
// A folder's own menu: rename, scan again, forget. All three
|
||||
// act on the folder that was picked, so a right click on a
|
||||
// subfolder row brings up its roll's menu. Shift+F10 raises
|
||||
// the same event, which is the keyboard's way in.
|
||||
// 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.
|
||||
onContextMenu={(e) => {
|
||||
if (!folder) return;
|
||||
e.preventDefault();
|
||||
const box = e.currentTarget.getBoundingClientRect();
|
||||
setMenu({
|
||||
folder,
|
||||
root: n.depth === 0,
|
||||
x: Math.min(e.clientX || box.left, window.innerWidth - 190),
|
||||
y: Math.min(e.clientY || box.top, window.innerHeight - 96),
|
||||
});
|
||||
@@ -715,14 +720,15 @@ export function Library() {
|
||||
|
||||
{/* The context menu: a folder gets what belongs to a folder rather than to a
|
||||
frame, the empty part of the column gets the folder that is missing, and
|
||||
the column's own name gets the whole tree folded. A folder whose
|
||||
the head of the tree gets the whole tree folded. A folder whose
|
||||
permission has lapsed gets the one thing that can be done about it. */}
|
||||
{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. */}
|
||||
{menu.root ? (
|
||||
// Every row that has something under it, the root's own included: 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.
|
||||
<button
|
||||
type="button"
|
||||
className="lib-menu-item"
|
||||
@@ -735,20 +741,9 @@ export function Library() {
|
||||
>
|
||||
{t('lib.collapseAll')}
|
||||
</button>
|
||||
) : !menu.folder ? (
|
||||
<button
|
||||
type="button"
|
||||
className="lib-menu-item"
|
||||
data-key="lib-menu-add"
|
||||
disabled={!!live}
|
||||
onClick={() => {
|
||||
setMenu(null);
|
||||
void add();
|
||||
}}
|
||||
>
|
||||
{t('lib.add')}
|
||||
</button>
|
||||
) : ready.has(menu.folder.name) ? (
|
||||
) : null}
|
||||
{menu.folder ? (
|
||||
ready.has(menu.folder.name) ? (
|
||||
<>
|
||||
<button
|
||||
type="button"
|
||||
@@ -798,6 +793,20 @@ export function Library() {
|
||||
>
|
||||
{t('lib.reconnect')}
|
||||
</button>
|
||||
)
|
||||
) : menu.root ? null : (
|
||||
<button
|
||||
type="button"
|
||||
className="lib-menu-item"
|
||||
data-key="lib-menu-add"
|
||||
disabled={!!live}
|
||||
onClick={() => {
|
||||
setMenu(null);
|
||||
void add();
|
||||
}}
|
||||
>
|
||||
{t('lib.add')}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
Reference in New Issue
Block a user