feat(documents): multi-select bulk actions on the vault timeline

Selection grows file-manager gestures — shift-click ranges (additive,
anchored on the last plain toggle) and press-and-hold on touch — and a
floating toolbar appears with the selected count and the bulk verbs:
change type, link condition, delete, clear. All verbs drive the bulk
endpoint in <=100-id chunks with merged per-id results; partial failures
surface as one counted toast, and a bulk delete gets one aggregate undo
that bulk-restores the succeeded rows (both sides no-op-safe by
contract). Escape clears the selection unless a dialog owns the key.

Refs: document-vault plan W3-T2
This commit is contained in:
Marc-André Bombeck committed 2026-07-07 13:21:16 +02:00
1 parent a8a733fc4b
commit aef2003d4c
13 files changed
+586 -45

No files matched your search

+34
View File
@@ -189,6 +189,40 @@ export function formatBytes(bytes: number, locale: string): string {
}).format(value)} ${units[unit]}`;
}
/**
* Shift-click range selection over the timeline's visual order. Selects
* every id between the last plainly-toggled anchor and the target
* (inclusive) — ADDITIVE, matching file-manager convention (a range gesture
* never deselects). Falls back to a plain toggle when the anchor is gone
* (filtered away, deleted) or was never set.
*/
export function expandRangeSelection(
orderedIds: readonly string[],
selected: ReadonlySet<string>,
anchorId: string | null,
targetId: string,
): Set<string> {
const next = new Set(selected);
const anchorIndex = anchorId === null ? -1 : orderedIds.indexOf(anchorId);
const targetIndex = orderedIds.indexOf(targetId);
if (anchorIndex === -1 || targetIndex === -1) {
if (next.has(targetId)) {
next.delete(targetId);
} else {
next.add(targetId);
}
return next;
}
const [from, to] =
anchorIndex <= targetIndex
? [anchorIndex, targetIndex]
: [targetIndex, anchorIndex];
for (let i = from; i <= to; i += 1) {
next.add(orderedIds[i]);
}
return next;
}
// ─── Upload response contract (§3.2) ───────────────────────────────────────
/**