feat(frontend): add "add to pool" button on the file viewer
deploy / deploy (push) Successful in 58s
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:
@@ -4,6 +4,7 @@
|
|||||||
import { api, ApiError } from '$lib/api/client';
|
import { api, ApiError } from '$lib/api/client';
|
||||||
import { authStore } from '$lib/stores/auth';
|
import { authStore } from '$lib/stores/auth';
|
||||||
import TagPicker from '$lib/components/file/TagPicker.svelte';
|
import TagPicker from '$lib/components/file/TagPicker.svelte';
|
||||||
|
import PoolPicker from '$lib/components/file/PoolPicker.svelte';
|
||||||
import type { File, Tag } from '$lib/api/types';
|
import type { File, Tag } from '$lib/api/types';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
@@ -26,6 +27,7 @@
|
|||||||
let loading = $state(true);
|
let loading = $state(true);
|
||||||
let saving = $state(false);
|
let saving = $state(false);
|
||||||
let error = $state('');
|
let error = $state('');
|
||||||
|
let poolPickerOpen = $state(false);
|
||||||
|
|
||||||
// Tags are loaded lazily — the Tags section sits below a full-viewport
|
// 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
|
// preview, so fetching them on open just hammers the DB for data the user
|
||||||
@@ -196,6 +198,17 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function handleKeydown(e: KeyboardEvent) {
|
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.target instanceof HTMLInputElement || e.target instanceof HTMLTextAreaElement) return;
|
||||||
if (e.ctrlKey || e.metaKey || e.altKey) 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
|
// Letter keys are matched by physical position (e.code) so j/k/e work on any
|
||||||
@@ -265,6 +278,32 @@
|
|||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
<span class="filename">{file?.original_name ?? ''}</span>
|
<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>
|
</div>
|
||||||
|
|
||||||
<!-- Preview -->
|
<!-- Preview -->
|
||||||
@@ -409,6 +448,10 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{#if poolPickerOpen && file}
|
||||||
|
<PoolPicker fileIds={[file.id!]} onClose={() => (poolPickerOpen = false)} />
|
||||||
|
{/if}
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.viewer-page {
|
.viewer-page {
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -457,6 +500,25 @@
|
|||||||
white-space: nowrap;
|
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 ---- */
|
||||||
.preview-wrap {
|
.preview-wrap {
|
||||||
position: relative;
|
position: relative;
|
||||||
|
|||||||
@@ -0,0 +1,251 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { api } from '$lib/api/client';
|
||||||
|
import type { Pool, PoolOffsetPage } from '$lib/api/types';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
/** Files to add to the chosen pool. */
|
||||||
|
fileIds: string[];
|
||||||
|
/** Called after a successful add (before close) — e.g. to clear a selection. */
|
||||||
|
onAdded?: (poolId: string) => void;
|
||||||
|
/** Close the picker without adding. */
|
||||||
|
onClose: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
let { fileIds, onAdded, onClose }: Props = $props();
|
||||||
|
|
||||||
|
let pools = $state<Pool[]>([]);
|
||||||
|
let loading = $state(true);
|
||||||
|
let loadError = $state('');
|
||||||
|
let addError = $state('');
|
||||||
|
let search = $state('');
|
||||||
|
let busy = $state(false);
|
||||||
|
|
||||||
|
$effect(() => {
|
||||||
|
void load();
|
||||||
|
});
|
||||||
|
|
||||||
|
async function load() {
|
||||||
|
loading = true;
|
||||||
|
loadError = '';
|
||||||
|
try {
|
||||||
|
const res = await api.get<PoolOffsetPage>('/pools?limit=200&sort=name&order=asc');
|
||||||
|
pools = res.items ?? [];
|
||||||
|
} catch {
|
||||||
|
loadError = 'Failed to load pools';
|
||||||
|
} finally {
|
||||||
|
loading = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let filtered = $derived(
|
||||||
|
search.trim()
|
||||||
|
? pools.filter((p) => p.name?.toLowerCase().includes(search.toLowerCase()))
|
||||||
|
: pools
|
||||||
|
);
|
||||||
|
|
||||||
|
async function add(poolId: string) {
|
||||||
|
if (busy) return;
|
||||||
|
busy = true;
|
||||||
|
addError = '';
|
||||||
|
try {
|
||||||
|
await api.post(`/pools/${poolId}/files`, { file_ids: fileIds });
|
||||||
|
onAdded?.(poolId);
|
||||||
|
onClose();
|
||||||
|
} catch {
|
||||||
|
addError = 'Failed to add to pool';
|
||||||
|
busy = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let count = $derived(fileIds.length);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
||||||
|
<div class="picker-backdrop" role="presentation" onclick={onClose}></div>
|
||||||
|
<div class="picker-sheet" class:busy role="dialog" aria-label="Add to pool">
|
||||||
|
<div class="picker-header">
|
||||||
|
<span class="picker-title">Add {count} file{count !== 1 ? 's' : ''} to pool</span>
|
||||||
|
<button class="picker-close" onclick={onClose} aria-label="Close">
|
||||||
|
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" aria-hidden="true">
|
||||||
|
<path
|
||||||
|
d="M3 3l10 10M13 3L3 13"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="1.8"
|
||||||
|
stroke-linecap="round"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="picker-search-wrap">
|
||||||
|
<input
|
||||||
|
class="picker-search"
|
||||||
|
type="search"
|
||||||
|
placeholder="Search pools…"
|
||||||
|
bind:value={search}
|
||||||
|
autocomplete="off"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{#if loading}
|
||||||
|
<p class="picker-empty">Loading…</p>
|
||||||
|
{:else if loadError}
|
||||||
|
<p class="picker-error">{loadError}</p>
|
||||||
|
{:else}
|
||||||
|
{#if addError}
|
||||||
|
<p class="picker-error">{addError}</p>
|
||||||
|
{/if}
|
||||||
|
{#if filtered.length === 0}
|
||||||
|
<p class="picker-empty">No pools found.</p>
|
||||||
|
{:else}
|
||||||
|
<ul class="picker-list">
|
||||||
|
{#each filtered as pool (pool.id)}
|
||||||
|
<li>
|
||||||
|
<button class="picker-item" onclick={() => pool.id && add(pool.id)}>
|
||||||
|
<span class="picker-item-name">{pool.name}</span>
|
||||||
|
<span class="picker-item-count">{pool.file_count ?? 0} files</span>
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
{/each}
|
||||||
|
</ul>
|
||||||
|
{/if}
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.picker-backdrop {
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
z-index: 110;
|
||||||
|
background: rgba(0, 0, 0, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.picker-sheet {
|
||||||
|
position: fixed;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
z-index: 111;
|
||||||
|
background-color: var(--color-bg-secondary);
|
||||||
|
border-radius: 14px 14px 0 0;
|
||||||
|
padding-bottom: env(safe-area-inset-bottom, 0px);
|
||||||
|
max-height: 70dvh;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
animation: slide-up 0.18s ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.picker-sheet.busy {
|
||||||
|
opacity: 0.6;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes slide-up {
|
||||||
|
from {
|
||||||
|
transform: translateY(20px);
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
transform: translateY(0);
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.picker-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 14px 16px 10px;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.picker-title {
|
||||||
|
flex: 1;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.picker-close {
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
|
color: var(--color-text-muted);
|
||||||
|
padding: 4px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.picker-close:hover {
|
||||||
|
color: var(--color-text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.picker-search-wrap {
|
||||||
|
padding: 0 14px 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.picker-search {
|
||||||
|
width: 100%;
|
||||||
|
box-sizing: border-box;
|
||||||
|
height: 34px;
|
||||||
|
padding: 0 10px;
|
||||||
|
border-radius: 8px;
|
||||||
|
border: 1px solid color-mix(in srgb, var(--color-accent) 30%, transparent);
|
||||||
|
background-color: var(--color-bg-elevated);
|
||||||
|
color: var(--color-text-primary);
|
||||||
|
font-size: 0.9rem;
|
||||||
|
font-family: inherit;
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.picker-search:focus {
|
||||||
|
border-color: var(--color-accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.picker-list {
|
||||||
|
list-style: none;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0 8px 12px;
|
||||||
|
overflow-y: auto;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.picker-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
width: 100%;
|
||||||
|
text-align: left;
|
||||||
|
padding: 11px 10px;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
|
font-family: inherit;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.picker-item:hover {
|
||||||
|
background-color: color-mix(in srgb, var(--color-accent) 12%, transparent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.picker-item-name {
|
||||||
|
flex: 1;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
color: var(--color-text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.picker-item-count {
|
||||||
|
font-size: 0.8rem;
|
||||||
|
color: var(--color-text-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.picker-empty,
|
||||||
|
.picker-error {
|
||||||
|
text-align: center;
|
||||||
|
padding: 20px;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
color: var(--color-text-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.picker-error {
|
||||||
|
color: var(--color-danger);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -15,9 +15,10 @@
|
|||||||
import { selectionStore, selectionActive } from '$lib/stores/selection';
|
import { selectionStore, selectionActive } from '$lib/stores/selection';
|
||||||
import ConfirmDialog from '$lib/components/common/ConfirmDialog.svelte';
|
import ConfirmDialog from '$lib/components/common/ConfirmDialog.svelte';
|
||||||
import BulkTagEditor from '$lib/components/file/BulkTagEditor.svelte';
|
import BulkTagEditor from '$lib/components/file/BulkTagEditor.svelte';
|
||||||
|
import PoolPicker from '$lib/components/file/PoolPicker.svelte';
|
||||||
import { tick, flushSync } from 'svelte';
|
import { tick, flushSync } from 'svelte';
|
||||||
import { parseDslFilter } from '$lib/utils/dsl';
|
import { parseDslFilter } from '$lib/utils/dsl';
|
||||||
import type { File, FileCursorPage, Pool, PoolOffsetPage } from '$lib/api/types';
|
import type { File, FileCursorPage } from '$lib/api/types';
|
||||||
import { appSettings } from '$lib/stores/appSettings';
|
import { appSettings } from '$lib/stores/appSettings';
|
||||||
|
|
||||||
// What the section cache stores for the Files grid. `resetKey` guards against
|
// What the section cache stores for the Files grid. `resetKey` guards against
|
||||||
@@ -207,43 +208,13 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ---- Add to pool picker ----
|
// ---- Add to pool picker ----
|
||||||
|
// The picker itself (load, search, add) lives in PoolPicker; here we just
|
||||||
|
// gate it open and clear the selection once files land in a pool.
|
||||||
let poolPickerOpen = $state(false);
|
let poolPickerOpen = $state(false);
|
||||||
let pools = $state<Pool[]>([]);
|
|
||||||
let poolsLoading = $state(false);
|
|
||||||
let poolPickerSearch = $state('');
|
|
||||||
let poolPickerError = $state('');
|
|
||||||
|
|
||||||
async function openPoolPicker() {
|
function openPoolPicker() {
|
||||||
poolPickerOpen = true;
|
poolPickerOpen = true;
|
||||||
poolPickerError = '';
|
|
||||||
poolsLoading = true;
|
|
||||||
poolPickerSearch = '';
|
|
||||||
try {
|
|
||||||
const res = await api.get<PoolOffsetPage>('/pools?limit=200&sort=name&order=asc');
|
|
||||||
pools = res.items ?? [];
|
|
||||||
} catch {
|
|
||||||
poolPickerError = 'Failed to load pools';
|
|
||||||
} finally {
|
|
||||||
poolsLoading = false;
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
async function addToPool(poolId: string) {
|
|
||||||
const ids = [...$selectionStore.ids];
|
|
||||||
poolPickerOpen = false;
|
|
||||||
selectionStore.exit();
|
|
||||||
try {
|
|
||||||
await api.post(`/pools/${poolId}/files`, { file_ids: ids });
|
|
||||||
} catch {
|
|
||||||
// silently ignore
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let filteredPools = $derived(
|
|
||||||
poolPickerSearch.trim()
|
|
||||||
? pools.filter((p) => p.name?.toLowerCase().includes(poolPickerSearch.toLowerCase()))
|
|
||||||
: pools
|
|
||||||
);
|
|
||||||
|
|
||||||
function handleUploaded(file: File) {
|
function handleUploaded(file: File) {
|
||||||
files = [file, ...files];
|
files = [file, ...files];
|
||||||
@@ -904,52 +875,11 @@
|
|||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if poolPickerOpen}
|
{#if poolPickerOpen}
|
||||||
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
<PoolPicker
|
||||||
<div class="picker-backdrop" role="presentation" onclick={() => (poolPickerOpen = false)}></div>
|
fileIds={[...$selectionStore.ids]}
|
||||||
<div class="picker-sheet" role="dialog" aria-label="Add to pool">
|
onAdded={() => selectionStore.exit()}
|
||||||
<div class="picker-header">
|
onClose={() => (poolPickerOpen = false)}
|
||||||
<span class="picker-title"
|
|
||||||
>Add {$selectionStore.ids.size} file{$selectionStore.ids.size !== 1 ? 's' : ''} to pool</span
|
|
||||||
>
|
|
||||||
<button class="picker-close" onclick={() => (poolPickerOpen = false)} aria-label="Close">
|
|
||||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" aria-hidden="true">
|
|
||||||
<path
|
|
||||||
d="M3 3l10 10M13 3L3 13"
|
|
||||||
stroke="currentColor"
|
|
||||||
stroke-width="1.8"
|
|
||||||
stroke-linecap="round"
|
|
||||||
/>
|
/>
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div class="picker-search-wrap">
|
|
||||||
<input
|
|
||||||
class="picker-search"
|
|
||||||
type="search"
|
|
||||||
placeholder="Search pools…"
|
|
||||||
bind:value={poolPickerSearch}
|
|
||||||
autocomplete="off"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
{#if poolPickerError}
|
|
||||||
<p class="picker-error">{poolPickerError}</p>
|
|
||||||
{:else if poolsLoading}
|
|
||||||
<p class="picker-empty">Loading…</p>
|
|
||||||
{:else if filteredPools.length === 0}
|
|
||||||
<p class="picker-empty">No pools found.</p>
|
|
||||||
{:else}
|
|
||||||
<ul class="picker-list">
|
|
||||||
{#each filteredPools as pool (pool.id)}
|
|
||||||
<li>
|
|
||||||
<button class="picker-item" onclick={() => pool.id && addToPool(pool.id)}>
|
|
||||||
<span class="picker-item-name">{pool.name}</span>
|
|
||||||
<span class="picker-item-count">{pool.file_count ?? 0} files</span>
|
|
||||||
</button>
|
|
||||||
</li>
|
|
||||||
{/each}
|
|
||||||
</ul>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if confirmDeleteFiles}
|
{#if confirmDeleteFiles}
|
||||||
@@ -1035,7 +965,7 @@
|
|||||||
flex: 1;
|
flex: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ---- Pool picker ---- */
|
/* ---- Bottom-sheet shell (shared by the tag editor sheet) ---- */
|
||||||
.picker-backdrop {
|
.picker-backdrop {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
inset: 0;
|
inset: 0;
|
||||||
@@ -1095,75 +1025,4 @@
|
|||||||
.picker-close:hover {
|
.picker-close:hover {
|
||||||
color: var(--color-text-primary);
|
color: var(--color-text-primary);
|
||||||
}
|
}
|
||||||
|
|
||||||
.picker-search-wrap {
|
|
||||||
padding: 0 14px 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.picker-search {
|
|
||||||
width: 100%;
|
|
||||||
box-sizing: border-box;
|
|
||||||
height: 34px;
|
|
||||||
padding: 0 10px;
|
|
||||||
border-radius: 8px;
|
|
||||||
border: 1px solid color-mix(in srgb, var(--color-accent) 30%, transparent);
|
|
||||||
background-color: var(--color-bg-elevated);
|
|
||||||
color: var(--color-text-primary);
|
|
||||||
font-size: 0.9rem;
|
|
||||||
font-family: inherit;
|
|
||||||
outline: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.picker-search:focus {
|
|
||||||
border-color: var(--color-accent);
|
|
||||||
}
|
|
||||||
|
|
||||||
.picker-list {
|
|
||||||
list-style: none;
|
|
||||||
margin: 0;
|
|
||||||
padding: 0 8px 12px;
|
|
||||||
overflow-y: auto;
|
|
||||||
flex: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.picker-item {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
width: 100%;
|
|
||||||
text-align: left;
|
|
||||||
padding: 11px 10px;
|
|
||||||
border-radius: 8px;
|
|
||||||
background: none;
|
|
||||||
border: none;
|
|
||||||
cursor: pointer;
|
|
||||||
font-family: inherit;
|
|
||||||
gap: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.picker-item:hover {
|
|
||||||
background-color: color-mix(in srgb, var(--color-accent) 12%, transparent);
|
|
||||||
}
|
|
||||||
|
|
||||||
.picker-item-name {
|
|
||||||
flex: 1;
|
|
||||||
font-size: 0.95rem;
|
|
||||||
color: var(--color-text-primary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.picker-item-count {
|
|
||||||
font-size: 0.8rem;
|
|
||||||
color: var(--color-text-muted);
|
|
||||||
}
|
|
||||||
|
|
||||||
.picker-empty,
|
|
||||||
.picker-error {
|
|
||||||
text-align: center;
|
|
||||||
padding: 20px;
|
|
||||||
font-size: 0.9rem;
|
|
||||||
color: var(--color-text-muted);
|
|
||||||
}
|
|
||||||
|
|
||||||
.picker-error {
|
|
||||||
color: var(--color-danger);
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
Reference in New Issue
Block a user