feat(frontend): add "add to pool" button on the file viewer
deploy / deploy (push) Successful in 58s

Extract the bottom-sheet pool picker (load, search, add) into a
reusable PoolPicker component and use it both for the grid's bulk
selection and from a new button in the file viewer's top bar, which adds
the single open file to a chosen pool. While the picker is open the
viewer hands it the keyboard so Escape closes the sheet (even from its
search) instead of the viewer.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-11 21:58:01 +03:00
parent fedfa8df3a
commit 38572b1c80
3 changed files with 324 additions and 152 deletions
@@ -4,6 +4,7 @@
import { api, ApiError } from '$lib/api/client';
import { authStore } from '$lib/stores/auth';
import TagPicker from '$lib/components/file/TagPicker.svelte';
import PoolPicker from '$lib/components/file/PoolPicker.svelte';
import type { File, Tag } from '$lib/api/types';
interface Props {
@@ -26,6 +27,7 @@
let loading = $state(true);
let saving = $state(false);
let error = $state('');
let poolPickerOpen = $state(false);
// Tags are loaded lazily — the Tags section sits below a full-viewport
// preview, so fetching them on open just hammers the DB for data the user
@@ -196,6 +198,17 @@
}
function handleKeydown(e: KeyboardEvent) {
// While the pool picker is open it owns the keyboard: Escape closes it
// (even from its search field), and every other key is swallowed so the
// viewer's shortcuts don't fire behind the modal. Typing still works —
// non-Escape keys aren't prevented, only ignored here.
if (poolPickerOpen) {
if (e.key === 'Escape') {
e.preventDefault();
poolPickerOpen = false;
}
return;
}
if (e.target instanceof HTMLInputElement || e.target instanceof HTMLTextAreaElement) return;
if (e.ctrlKey || e.metaKey || e.altKey) return;
// Letter keys are matched by physical position (e.code) so j/k/e work on any
@@ -265,6 +278,32 @@
</svg>
</button>
<span class="filename">{file?.original_name ?? ''}</span>
{#if file}
<button
class="pool-btn"
onclick={() => (poolPickerOpen = true)}
aria-label="Add to pool"
title="Add to pool"
>
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" aria-hidden="true">
<rect
x="3"
y="5"
width="14"
height="11"
rx="2"
stroke="currentColor"
stroke-width="1.6"
/>
<path
d="M10 8.5v4M8 10.5h4"
stroke="currentColor"
stroke-width="1.6"
stroke-linecap="round"
/>
</svg>
</button>
{/if}
</div>
<!-- Preview -->
@@ -409,6 +448,10 @@
</div>
</div>
{#if poolPickerOpen && file}
<PoolPicker fileIds={[file.id!]} onClose={() => (poolPickerOpen = false)} />
{/if}
<style>
.viewer-page {
display: flex;
@@ -457,6 +500,25 @@
white-space: nowrap;
}
.pool-btn {
margin-left: auto;
display: flex;
align-items: center;
justify-content: center;
width: 36px;
height: 36px;
border-radius: 8px;
border: none;
background: none;
color: var(--color-text-primary);
cursor: pointer;
flex-shrink: 0;
}
.pool-btn:hover {
background-color: color-mix(in srgb, var(--color-accent) 15%, transparent);
}
/* ---- Preview ---- */
.preview-wrap {
position: relative;