web: fold a folder's rows without folding its frames
This commit is contained in:
@@ -229,6 +229,28 @@ step(
|
||||
JSON.stringify(nodes)
|
||||
);
|
||||
|
||||
// A row with children folds its subtree on the same click that opens it, and the
|
||||
// frames it holds stay in the strip — the tree narrows, the pictures do not.
|
||||
await page.click('[data-key="lib-node-CheckRoll"]');
|
||||
await page.waitForFunction(() => document.querySelectorAll('[data-key^="lib-node-"]').length === 1, null, {
|
||||
timeout: 10_000,
|
||||
polling: 50,
|
||||
});
|
||||
const foldedThumbs = await page.$$eval(thumbs, (els) => els.length);
|
||||
const foldedMeta = await page.locator('[data-key="lib-stage-meta"]').innerText();
|
||||
const shut = await page.getAttribute('[data-key="lib-node-CheckRoll"]', 'aria-expanded');
|
||||
step(
|
||||
'a folder folds its subtree and keeps its frames in the strip',
|
||||
foldedThumbs === 3 && shut === 'false' && /P1010256/.test(foldedMeta),
|
||||
`${foldedThumbs} tiles, aria-expanded=${shut}, stage ${foldedMeta.split('\n')[0]}`
|
||||
);
|
||||
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,
|
||||
});
|
||||
step('the same click draws it open again', true, '3 rows');
|
||||
|
||||
// Opening the subfolder redraws the strip and the frame above it.
|
||||
await page.click('[data-key="lib-node-CheckRoll/2026"]');
|
||||
await page.waitForFunction((sel) => document.querySelectorAll(sel).length === 2, thumbs, { timeout: 30_000 });
|
||||
|
||||
@@ -26,9 +26,8 @@ import {
|
||||
// thumbnails, and the RAW itself is read only when a frame is opened in the
|
||||
// studio (`/app?lib=<id>`).
|
||||
//
|
||||
// ponytail: the tree does not collapse, and there is no search, rating or flag.
|
||||
// A roll of date folders is three levels deep at most; add a collapsing node or
|
||||
// a filter once the column stops being scrollable in practice.
|
||||
// ponytail: no search, rating or flag. A roll of date folders is three levels
|
||||
// deep at most; add a filter once the column stops being scrollable in practice.
|
||||
interface Node {
|
||||
// The tree's own key: a folder name, or `folder/sub/dir`.
|
||||
key: string;
|
||||
@@ -73,6 +72,9 @@ export function Library() {
|
||||
// views (the frame, or every thumbnail of the open node) is on. The open node
|
||||
// is picked up where the last visit left it.
|
||||
const [node, setNode] = useState<string | null>(() => remembered(NODE_KEY));
|
||||
// Rows whose children are folded away. A collapse is a way of looking at the
|
||||
// tree, not a property of the roll, so it is not remembered between visits.
|
||||
const [folded, setFolded] = useState<Set<string>>(() => new Set());
|
||||
const [up, setUp] = useState<string | null>(null);
|
||||
const [view, setView] = useState<'preview' | 'grid'>('preview');
|
||||
const [colWidth, setColWidth] = useState(() => {
|
||||
@@ -285,6 +287,24 @@ export function Library() {
|
||||
return out;
|
||||
}, [folders, photos]);
|
||||
|
||||
// A row with anything under it is the one that folds: its key is the prefix of
|
||||
// its children's, in the path order the rows come in.
|
||||
const parents = useMemo(
|
||||
() => new Set(nodes.filter((n) => n.depth > 0).map((n) => n.key.slice(0, n.key.lastIndexOf('/')))),
|
||||
[nodes]
|
||||
);
|
||||
|
||||
// What the column draws: a folded row takes its whole subtree with it.
|
||||
const rows = useMemo(
|
||||
() =>
|
||||
nodes.filter((n) => {
|
||||
const parts = n.key.split('/');
|
||||
for (let i = 1; i < parts.length; i++) if (folded.has(parts.slice(0, i).join('/'))) return false;
|
||||
return true;
|
||||
}),
|
||||
[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;
|
||||
@@ -423,11 +443,12 @@ export function Library() {
|
||||
<b className="lib-col-title" data-key="lib-root-name" title={root?.label || root?.name}>
|
||||
{root?.label || root?.name}
|
||||
</b>
|
||||
{nodes.map((n) => {
|
||||
{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
|
||||
// the toolbar's progress line is nowhere near the eye.
|
||||
const scanning = !!folder && progress?.folder === folder.name;
|
||||
const under = parents.has(n.key);
|
||||
return (
|
||||
<button
|
||||
key={n.key}
|
||||
@@ -436,9 +457,22 @@ export function Library() {
|
||||
data-key={`lib-node-${n.key}`}
|
||||
aria-current={node === n.key}
|
||||
aria-busy={scanning}
|
||||
aria-expanded={under ? !folded.has(n.key) : undefined}
|
||||
title={t('lib.nodeHint')}
|
||||
style={{ paddingLeft: 9 + n.depth * 12 }}
|
||||
onClick={() => setNode(n.key)}
|
||||
// A row with children opens on the folder and folds it in
|
||||
// the same click: the frames below stay in the strip, only
|
||||
// the rows go.
|
||||
onClick={() => {
|
||||
setNode(n.key);
|
||||
if (!under) return;
|
||||
setFolded((prev) => {
|
||||
const next = new Set(prev);
|
||||
if (next.has(n.key)) next.delete(n.key);
|
||||
else next.add(n.key);
|
||||
return next;
|
||||
});
|
||||
}}
|
||||
// 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
|
||||
@@ -454,6 +488,10 @@ export function Library() {
|
||||
});
|
||||
}}
|
||||
>
|
||||
<span
|
||||
className={`lib-caret${under ? (folded.has(n.key) ? '' : ' open') : ' leaf'}`}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<span className="adm-album-name">{n.label}</span>
|
||||
{scanning ? <span className="lib-spin" aria-hidden="true" /> : null}
|
||||
<span className="adm-album-count">{n.count}</span>
|
||||
|
||||
@@ -1033,7 +1033,22 @@ input[type="range"] { width: 100%; accent-color: var(--accent); }
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.lib-root .adm-album { padding: 6px 8px; gap: 6px; }
|
||||
.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
|
||||
where they are drawn, and empty on a leaf so names at one depth line up. */
|
||||
.lib-caret {
|
||||
flex: 0 0 auto;
|
||||
width: 0;
|
||||
height: 0;
|
||||
border-left: 4.5px solid currentColor;
|
||||
border-top: 3.5px solid transparent;
|
||||
border-bottom: 3.5px solid transparent;
|
||||
opacity: 0.5;
|
||||
transition: transform 0.15s ease;
|
||||
}
|
||||
.lib-caret.open { transform: rotate(90deg); }
|
||||
.lib-caret.leaf { border-left-color: transparent; }
|
||||
/* A roll being read turns a ring on its own row. */
|
||||
.lib-spin {
|
||||
flex: 0 0 auto;
|
||||
|
||||
Reference in New Issue
Block a user