web: drop the heading over the folder column

The column carried the name of the folder its tree belongs to, painted
above the rows. The rows already say it — the head of the tree is a row
like any other — so the heading only said it twice, and because it was
not a row it stayed on screen when the tree was folded away: the one
label left with nothing under it. The right click that raised it now
finds the row itself, which is where a reader aims anyway.

The label, its menu hook, its translation and its rule go; the tree, the
row menus and fold-all keep working as before.

Co-authored-by: PenguinHarness <noreply@penguin.local>
This commit is contained in:
2026-09-28 21:27:55 +07:00
parent 5e28b42067
commit 067b32131e
5 changed files with 20 additions and 65 deletions
+10 -19
View File
@@ -359,10 +359,12 @@ step(
(await page.$('[data-key="lib-node-CheckRoll/2026"]')) !== null,
await page.$eval('[data-key="lib-node-CheckRoll"] .adm-album-name', (e) => e.textContent)
);
// The folder's name is the row's, said once: a heading over the column would only
// repeat it, and that heading would outlive the rows when the tree is folded.
step(
'the column is titled with the folder the tree belongs to',
(await page.$eval('[data-key="lib-root-name"]', (e) => e.textContent)) === 'Roll A',
await page.$eval('[data-key="lib-root-name"]', (e) => e.textContent)
'the folder is named on its row and nowhere over the column',
(await page.$$('[data-key="lib-root-name"], .lib-col-title')).length === 0 &&
(await page.$eval('[data-key="lib-node-CheckRoll"] .adm-album-name', (e) => e.textContent)) === 'Roll A'
);
// The empty part of the column is where one more folder comes from.
@@ -373,10 +375,10 @@ 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, or on the row of a folder picked at the top level, folds the folders
// nested below the top level. The top-level row itself stays open — one click on
// its name already shuts it, so the menu item would only repeat that click — and
// The head of the tree is the whole tree's control: a right click on the row of a
// folder picked at the top level folds the folders nested below the top level. The
// top-level row itself stays open — one click on its name already shuts it, so the
// menu item would only repeat that click — and
// the frames in the strip are the strip's business and do not move with the rows.
// 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 one already shut.
@@ -387,8 +389,7 @@ 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.
// The row is where a reader right-clicks, so fold-the-tree leads that row's menu.
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));
@@ -397,16 +398,6 @@ step(
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 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',
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 === 3, null, {
timeout: 10_000,
+10 -30
View File
@@ -107,9 +107,8 @@ 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 head of a tree — the column's
// own name, or the row of a folder picked at the top level — which carries
// folding the folders nested below the top level.
// one more folder; `root` means it landed on a row at the top level — the head
// of a tree — which also carries 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.
@@ -371,10 +370,6 @@ export function Library() {
[nodes, folded]
);
// The folder the open node belongs to: its name titles the column, and its
// handle is what the menu acts on.
const root = folders.find((f) => f.name === rootName) ?? null;
// A remembered subfolder that no longer holds a frame falls back to the folder
// it sits in, so the screen never opens on a node that is not drawn.
useEffect(() => {
@@ -519,21 +514,6 @@ export function Library() {
setMenu({ folder: null, x: Math.min(e.clientX, window.innerWidth - 190), y: Math.min(e.clientY, window.innerHeight - 96) });
}}
>
{/* 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 the folders nested below the top level. */}
<b
className="lib-col-title"
data-key="lib-root-name"
title={`${root?.label || root?.name} — ${t('lib.rootHint')}`}
onContextMenu={(e) => {
e.preventDefault();
setMenu({ folder: null, root: true, x: Math.min(e.clientX, window.innerWidth - 190), y: Math.min(e.clientY, window.innerHeight - 96) });
}}
>
{root?.label || root?.name}
</b>
{rows.map((n) => {
const folder = folders.find((f) => f.name === n.key.split('/')[0]) ?? null;
// A roll being read says so on its own row: while a scan runs
@@ -571,9 +551,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
// 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.
// what sits under it, 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();
@@ -729,10 +709,10 @@ export function Library() {
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: 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. */}
{/* Fold-the-tree leads the menu of a row at the top level: the folders
nested below it 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"
@@ -799,7 +779,7 @@ export function Library() {
{t('lib.reconnect')}
</button>
)
) : menu.root ? null : (
) : (
<button
type="button"
className="lib-menu-item"
-1
View File
@@ -237,7 +237,6 @@ export const en: Dict = {
'lib.reconnect': 'GRANT ACCESS AGAIN',
'lib.nodeHint': 'Right-click to rename, rescan or remove the folder.',
'lib.collapseAll': 'COLLAPSE ALL',
'lib.rootHint': 'Right-click to fold every subfolder.',
'lib.colHint': 'Right-click the empty part of this column to add a folder.',
'lib.colWidth': 'Drag to set the width of the folder column (arrow keys when focused)',
'lib.viewPreview': 'SINGLE',
-1
View File
@@ -250,7 +250,6 @@ export const vi = {
'lib.reconnect': 'CẤP LẠI QUYỀN',
'lib.nodeHint': 'Chuột phải để đổi tên, quét lại hay bỏ thư mục.',
'lib.collapseAll': 'THU GỌN TẤT CẢ',
'lib.rootHint': 'Chuột phải để thu gọn mọi thư mục con.',
'lib.colHint': 'Chuột phải vào vùng trống của cột này để thêm thư mục.',
'lib.colWidth': 'Kéo để chỉnh độ rộng cột thư mục (mũi tên trái/phải khi đang chọn)',
'lib.viewPreview': 'MỘT ẢNH',
-14
View File
@@ -1019,20 +1019,6 @@ input[type="range"] { width: 100%; accent-color: var(--accent); }
}
.lib-grip { cursor: col-resize; border-radius: 999px; touch-action: none; }
.lib-grip:hover, .lib-grip:focus-visible { background: var(--accent-soft); outline: none; }
/* The folder the tree belongs to, named over it: a roll's own name is the one
thing a renamed folder has to say here. */
.lib-col-title {
margin: 0 0 6px;
padding: 0 2px;
font-size: 11.5px;
font-weight: 600;
letter-spacing: 0.06em;
text-transform: uppercase;
color: var(--text-dim);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.lib-root .adm-album { padding: 6px 8px; gap: 6px; justify-content: flex-start; }
.lib-root .adm-album-name { flex: 1 1 auto; min-width: 0; }
/* The caret column: pointing right where a row's children are folded away, down