Refactor GalleryPanel and GalleryBrowser for improved layout and functionality
ci / build-backend (push) Successful in 2m54s
ci / build-frontend (push) Failing after 24s
ci / tests (push) Skipped
ci / sonar (push) Skipped

Updated the GalleryPanel component to ensure it occupies the full screen height, enhancing the user experience by preventing dual scrollbars. Modified the GalleryBrowser to support a new 'fill' prop, allowing it to adapt its layout based on the context, and adjusted the internal scrolling behavior accordingly. These changes improve the overall usability and visual consistency of the image gallery interface.
This commit is contained in:
Leonid Pershin
2026-07-27 11:48:19 +03:00
parent 61f934d19f
commit 860cde19a3
4 changed files with 35 additions and 10 deletions
@@ -1,14 +1,19 @@
import { useTranslation } from 'react-i18next'
import { GalleryBrowser } from './ImageGallery'
/** Отдельная страница «Галерея»: просмотр/загрузка/удаление всех изображений приложения. */
/**
* Отдельная страница «Галерея»: просмотр/загрузка/удаление всех изображений приложения.
*
* Страница занимает экран целиком и сама не прокручивается: списка на сотни картинок здесь нет
* пагинации, и две полосы прокрутки — внешняя и внутри сетки — мешали бы друг другу.
*/
export function GalleryPanel() {
const { t } = useTranslation()
return (
<div className="flex flex-col gap-4">
<div className="flex h-[calc(100vh-13rem)] min-h-96 flex-col gap-4">
<h2 className="crt-glow text-xl font-semibold">{t('admin.gallery.title')}</h2>
<div className="crt-panel rounded-md p-4">
<GalleryBrowser />
<div className="crt-panel flex min-h-0 flex-1 flex-col rounded-md p-4">
<GalleryBrowser fill />
</div>
</div>
)
@@ -8,6 +8,7 @@ import { useApiError } from '@/shared/lib/use-api-error'
import { Button } from '@/shared/ui/button'
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/shared/ui/dialog'
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/shared/ui/select'
import { cn } from '@/shared/lib/cn'
import { deleteImage, imageUrl, listImages, uploadImage } from './api'
const CATEGORIES: ImageCategory[] = ['Library', 'ShowPoster', 'EpisodeStill', 'BumperBackground']
@@ -25,10 +26,13 @@ export function GalleryBrowser({
category = 'Library',
onSelect,
onClose,
fill = false,
}: Readonly<{
category?: ImageCategory
onSelect?: (image: ImagePick) => void
onClose?: () => void
/** Занять экран целиком: на отдельной странице прокрутка должна быть одна — внутри сетки. */
fill?: boolean
}>) {
const { t } = useTranslation()
const queryClient = useQueryClient()
@@ -86,7 +90,7 @@ export function GalleryBrowser({
})
return (
<>
<div className={cn('flex flex-col', fill && 'min-h-0 flex-1')}>
<div className="flex flex-wrap gap-1.5">
{CATEGORIES.map((c) => (
<button
@@ -109,7 +113,7 @@ export function GalleryBrowser({
{onSelect ? t('admin.gallery.pickHint') : t('admin.gallery.browseHint')}
</span>
<Select value={order} onValueChange={(v) => setOrder(v as ImageOrder)}>
<SelectTrigger className="ml-auto h-8 w-40">
<SelectTrigger className="ml-auto h-8 w-48">
<SelectValue />
</SelectTrigger>
<SelectContent>
@@ -141,7 +145,14 @@ export function GalleryBrowser({
</Button>
</div>
<div className="mt-3 max-h-[55vh] overflow-y-auto">
<div
className={cn(
'mt-3 overflow-y-auto',
// В диалоге высота ограничена долей экрана, на своей странице — остатком до низа окна:
// иначе страница прокручивается вместе с сеткой и полос становится две.
fill ? 'min-h-0 flex-1' : 'max-h-[55vh]',
)}
>
{isLoading && (
<p className="py-6 text-center text-sm text-muted-foreground">{t('common.loading')}</p>
)}
@@ -183,7 +194,7 @@ export function GalleryBrowser({
</div>
)}
</div>
</>
</div>
)
}
@@ -44,6 +44,7 @@ export function ShowPicker({
const [query, setQuery] = useState('')
const [active, setActive] = useState(0)
const listRef = useRef<HTMLDivElement>(null)
const searchRef = useRef<HTMLInputElement>(null)
const trimmed = query.trim()
const matches = useMemo(() => {
@@ -133,9 +134,15 @@ export function ShowPicker({
<PopoverContent
className="w-[var(--radix-popover-trigger-width)] min-w-64 p-1"
onKeyDown={onKeyDown}
onOpenAutoFocus={(event) => {
// Фокус наводим сами: внутри диалога за него борются две фокус-ловушки, и по умолчанию
// он оседает на самом поповере — строка поиска остаётся мёртвой.
event.preventDefault()
requestAnimationFrame(() => searchRef.current?.focus())
}}
>
<Input
autoFocus
ref={searchRef}
className="h-8"
placeholder={t('admin.shows.pickerSearch')}
value={query}
+3 -1
View File
@@ -15,8 +15,10 @@ export const PopoverContent = forwardRef<
ref={ref}
align={align}
sideOffset={sideOffset}
// Фон непрозрачный, в отличие от обычной crt-панели: поповер висит над чужим текстом,
// и полупрозрачность превращала бы список в кашу из наложенных строк.
// z-50 — как у диалога: поповер живёт и внутри диалогов, и должен быть над ними.
className={cn('crt-panel z-50 rounded-sm outline-none', className)}
className={cn('crt-panel z-50 rounded-sm bg-muted outline-none', className)}
{...props}
/>
</PopoverPrimitive.Portal>