feat(frontend): implement file selection with long-press, shift+click, and touch drag

- selection.ts: store with select/deselect/toggle/enter/exit, derived count and active
- FileCard: long-press (400ms) enters selection mode, shows check overlay, blocks context menu
- Header: Select/Cancel button toggles selection mode
- SelectionBar: floating bar above navbar with count, Edit tags, Add to pool, Delete
- Shift+click range-selects between last and current index (desktop)
- Touch drag-to-select/deselect after long-press; non-passive touchmove blocks scroll only during drag

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-05 13:30:26 +03:00
parent 63ea1a4d6a
commit aebf7127af
5 changed files with 443 additions and 16 deletions
+65
View File
@@ -0,0 +1,65 @@
import { writable, derived } from 'svelte/store';
interface SelectionState {
active: boolean;
ids: Set<string>;
}
function createSelectionStore() {
const { subscribe, update, set } = writable<SelectionState>({
active: false,
ids: new Set(),
});
return {
subscribe,
enter() {
update((s) => ({ ...s, active: true }));
},
exit() {
set({ active: false, ids: new Set() });
},
toggle(id: string) {
update((s) => {
const ids = new Set(s.ids);
if (ids.has(id)) {
ids.delete(id);
} else {
ids.add(id);
}
// Exit selection mode automatically when last item is deselected
const active = ids.size > 0;
return { active, ids };
});
},
select(id: string) {
update((s) => {
const ids = new Set(s.ids);
ids.add(id);
return { active: true, ids };
});
},
deselect(id: string) {
update((s) => {
const ids = new Set(s.ids);
ids.delete(id);
const active = ids.size > 0;
return { active, ids };
});
},
clear() {
set({ active: false, ids: new Set() });
},
};
}
export const selectionStore = createSelectionStore();
export const selectionCount = derived(selectionStore, ($s) => $s.ids.size);
export const selectionActive = derived(selectionStore, ($s) => $s.active);