fix: make toolbar, sidebar, and lightbox responsive on small screens
This commit is contained in:
@@ -56,7 +56,7 @@ export function ColorFilter() {
|
||||
}, [open]);
|
||||
|
||||
return (
|
||||
<div ref={ref} className="ml-1 flex items-center border-l border-white/[0.06] pl-2">
|
||||
<div ref={ref} className="relative ml-1 flex shrink-0 items-center border-l border-white/[0.06] pl-2">
|
||||
{/* Trigger — a single palette icon; shows the active color as a dot when a
|
||||
filter is applied so the collapsed state still communicates it. */}
|
||||
<Tooltip label={isActive ? "Color filter active" : "Filter by color"} delay={400}>
|
||||
@@ -85,17 +85,17 @@ export function ColorFilter() {
|
||||
|
||||
<AnimatePresence initial={false}>
|
||||
{open ? (
|
||||
// Right-aligned popover so it never widens the toolbar row or gets
|
||||
// pushed off-screen on narrow windows. Swatches wrap into a compact
|
||||
// grid instead of a single long horizontal strip.
|
||||
<motion.div
|
||||
initial={{ width: 0, opacity: 0 }}
|
||||
animate={{ width: "auto", opacity: 1 }}
|
||||
exit={{ width: 0, opacity: 0 }}
|
||||
transition={{ duration: 0.18, ease: "easeOut" }}
|
||||
// Horizontal/vertical padding gives the active swatch's ring + scale
|
||||
// room inside the overflow-hidden clip box (needed for the width slide).
|
||||
// py-1 keeps the panel shorter than the always-present filter pills so
|
||||
// opening it never changes the row height (no 1px toolbar shift).
|
||||
className="flex items-center gap-1 overflow-hidden whitespace-nowrap px-1.5 py-1"
|
||||
initial={{ opacity: 0, y: -4, scale: 0.98 }}
|
||||
animate={{ opacity: 1, y: 0, scale: 1 }}
|
||||
exit={{ opacity: 0, y: -4, scale: 0.98 }}
|
||||
transition={{ duration: 0.14, ease: "easeOut" }}
|
||||
className="absolute right-0 top-full z-30 mt-2 w-max rounded-xl border border-white/10 bg-gray-950/98 p-2.5 shadow-2xl backdrop-blur light-theme:border-gray-700/50"
|
||||
>
|
||||
<div className="grid grid-cols-7 gap-1.5">
|
||||
{SWATCHES.map((swatch) => {
|
||||
const active = rgbEquals(colorFilter, swatch.rgb);
|
||||
return (
|
||||
@@ -103,7 +103,7 @@ export function ColorFilter() {
|
||||
key={swatch.name}
|
||||
title={swatch.name}
|
||||
aria-label={`Filter by ${swatch.name}`}
|
||||
className={`h-4 w-4 shrink-0 rounded-full border transition-transform ${
|
||||
className={`h-5 w-5 shrink-0 rounded-full border transition-transform ${
|
||||
active ? "scale-110 border-white/40 ring-2 ring-white/70" : "border-white/15 hover:scale-110"
|
||||
}`}
|
||||
style={{ backgroundColor: toHex(swatch.rgb) }}
|
||||
@@ -115,7 +115,7 @@ export function ColorFilter() {
|
||||
{/* Custom color picker — rainbow until a custom color is chosen. */}
|
||||
<label
|
||||
title="Custom color"
|
||||
className={`relative h-4 w-4 shrink-0 cursor-pointer overflow-hidden rounded-full border ${
|
||||
className={`relative h-5 w-5 shrink-0 cursor-pointer overflow-hidden rounded-full border ${
|
||||
isCustom ? "border-white/40 ring-2 ring-white/70" : "border-white/15 hover:scale-110"
|
||||
}`}
|
||||
style={
|
||||
@@ -131,24 +131,28 @@ export function ColorFilter() {
|
||||
onChange={(event) => setColorFilter(fromHex(event.target.value))}
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
{isActive || (colorBackfill && colorBackfill.total > 0) ? (
|
||||
<div className="mt-2 flex items-center justify-between gap-3 border-t border-white/[0.06] pt-2 light-theme:border-gray-700/40">
|
||||
{colorBackfill && colorBackfill.total > 0 ? (
|
||||
<span
|
||||
className="text-[10px] text-gray-600"
|
||||
title="Sampling colors from existing thumbnails — color search fills in as this runs"
|
||||
>
|
||||
sampling {colorBackfill.processed.toLocaleString()}/{colorBackfill.total.toLocaleString()}
|
||||
</span>
|
||||
) : <span />}
|
||||
{isActive ? (
|
||||
<button
|
||||
className="ml-0.5 shrink-0 rounded px-1 text-[11px] text-gray-500 transition-colors hover:text-gray-200"
|
||||
className="shrink-0 rounded px-1 text-[11px] text-gray-500 transition-colors hover:text-gray-200"
|
||||
onClick={() => setColorFilter(null)}
|
||||
title="Clear color filter"
|
||||
>
|
||||
Clear
|
||||
</button>
|
||||
) : null}
|
||||
|
||||
{colorBackfill && colorBackfill.total > 0 ? (
|
||||
<span
|
||||
className="ml-0.5 shrink-0 text-[10px] text-gray-600"
|
||||
title="Sampling colors from existing thumbnails — color search fills in as this runs"
|
||||
>
|
||||
sampling {colorBackfill.processed.toLocaleString()}/{colorBackfill.total.toLocaleString()}
|
||||
</span>
|
||||
</div>
|
||||
) : null}
|
||||
</motion.div>
|
||||
) : null}
|
||||
|
||||
@@ -517,7 +517,7 @@ export function Lightbox() {
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
|
||||
<div className="lightbox-panel flex w-72 shrink-0 flex-col border-l border-white/5 bg-gray-900/95">
|
||||
<div className="lightbox-panel flex w-64 shrink-0 flex-col border-l border-white/5 bg-gray-900/95 lg:w-72">
|
||||
<div className="flex shrink-0 items-center justify-between px-5 pt-5 pb-4">
|
||||
<div className="min-w-0">
|
||||
<p className="truncate text-sm font-medium text-white">{selectedImage.filename}</p>
|
||||
@@ -534,7 +534,7 @@ export function Lightbox() {
|
||||
</svg>
|
||||
</button>
|
||||
<button
|
||||
className={`rounded-full border px-3 py-1.5 text-xs ${
|
||||
className={`flex items-center gap-1.5 rounded-full border px-2 py-1.5 text-xs lg:px-3 ${
|
||||
canFindSimilar
|
||||
? "border-white/10 bg-white/5 text-gray-300 hover:text-white"
|
||||
: "border-white/5 bg-white/[0.03] text-gray-600 cursor-not-allowed"
|
||||
@@ -544,8 +544,15 @@ export function Lightbox() {
|
||||
void findSimilar(selectedImage.id, selectedImage.folder_id);
|
||||
}}
|
||||
disabled={!canFindSimilar}
|
||||
title={canFindSimilar ? "Find similar images" : "Embeddings not ready"}
|
||||
>
|
||||
{canFindSimilar ? "Similar" : "Embeddings not ready"}
|
||||
<svg className="h-4 w-4 shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.6}
|
||||
d="M13 3l1.55 4.65L19 9.2l-4.45 1.55L13 15.4l-1.55-4.65L7 9.2l4.45-1.55L13 3z" />
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.6}
|
||||
d="M5.5 14.5l.8 2.2 2.2.8-2.2.8-.8 2.2-.8-2.2L2.5 17.5l2.2-.8.8-2.2z" />
|
||||
</svg>
|
||||
<span className="hidden lg:inline">{canFindSimilar ? "Similar" : "Embeddings not ready"}</span>
|
||||
</button>
|
||||
</div>
|
||||
<button className="rounded p-1 text-gray-400 hover:text-white" onClick={closeImage}>
|
||||
@@ -949,7 +956,7 @@ export function Lightbox() {
|
||||
</div>
|
||||
|
||||
<button
|
||||
className="absolute right-80 top-1/2 z-10 -translate-y-1/2 rounded-full bg-white/10 p-3 text-white transition-colors hover:bg-white/20 disabled:opacity-20"
|
||||
className="absolute right-72 top-1/2 z-10 -translate-y-1/2 rounded-full bg-white/10 p-3 text-white transition-colors hover:bg-white/20 disabled:opacity-20 lg:right-80"
|
||||
disabled={currentIndex >= images.length - 1 || regionSelectMode}
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
|
||||
@@ -829,7 +829,7 @@ export function Sidebar() {
|
||||
};
|
||||
|
||||
return (
|
||||
<aside className="w-60 shrink-0 flex flex-col bg-gray-950 border-r border-white/[0.06]">
|
||||
<aside className="w-52 shrink-0 flex flex-col bg-gray-950 border-r border-white/[0.06] lg:w-60">
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between px-4 h-12 border-b border-white/[0.06] shrink-0">
|
||||
<span className="text-[13px] font-semibold text-white/80 tracking-wide">Phokus</span>
|
||||
|
||||
@@ -111,7 +111,7 @@ function FilterPill({
|
||||
: "bg-white/10 text-white light-theme:bg-gray-900 light-theme:text-white";
|
||||
return (
|
||||
<button
|
||||
className={`rounded-lg px-3 py-1.5 text-xs font-medium transition-all duration-150 ${
|
||||
className={`shrink-0 whitespace-nowrap rounded-lg px-3 py-1.5 text-xs font-medium transition-all duration-150 ${
|
||||
active
|
||||
? activeClass
|
||||
: "text-gray-500 hover:text-gray-200 hover:bg-white/5 light-theme:text-gray-600 light-theme:hover:bg-gray-900 light-theme:hover:text-white"
|
||||
@@ -268,10 +268,10 @@ export function Toolbar() {
|
||||
return (
|
||||
<div className="relative z-20 shrink-0 border-b border-white/[0.06] bg-gray-950/80 backdrop-blur-xl">
|
||||
{/* Primary row */}
|
||||
<div className="flex items-center gap-3 px-5 h-12">
|
||||
<div className="flex items-center gap-2 px-3 h-12 lg:gap-3 lg:px-5">
|
||||
{/* Title + count */}
|
||||
<div className="flex items-baseline gap-2.5 min-w-0 shrink-0">
|
||||
<h2 className="text-[15px] font-semibold text-white truncate max-w-48">{title}</h2>
|
||||
<h2 className="text-[15px] font-semibold text-white truncate max-w-32 lg:max-w-48">{title}</h2>
|
||||
<span className="text-xs text-gray-600 shrink-0">
|
||||
{loadedCount < totalImages
|
||||
? `${loadedCount.toLocaleString()} / ${totalImages.toLocaleString()}`
|
||||
@@ -322,7 +322,7 @@ export function Toolbar() {
|
||||
}}
|
||||
onFocus={() => setSearchPanelOpen(true)}
|
||||
placeholder="Search files, or use /s /t"
|
||||
className={`w-64 bg-transparent py-1.5 pr-9 text-sm text-white placeholder:text-gray-600 focus:outline-none transition-colors ${searchCommand !== null ? "pl-16" : "pl-8"}`}
|
||||
className={`w-40 bg-transparent py-1.5 pr-9 text-sm text-white placeholder:text-gray-600 focus:outline-none transition-colors lg:w-52 xl:w-64 ${searchCommand !== null ? "pl-16" : "pl-8"}`}
|
||||
/>
|
||||
{searchCommand !== null ? (
|
||||
<div className="absolute left-8 top-1/2 -translate-y-1/2">
|
||||
@@ -452,8 +452,10 @@ export function Toolbar() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Filter row */}
|
||||
{/* Filter row — pills scroll horizontally on narrow widths so they never
|
||||
squash or stack; the color filter stays pinned to the right. */}
|
||||
<div className="flex items-center gap-1 px-4 pb-1.5">
|
||||
<div className="flex min-w-0 flex-1 items-center gap-1 overflow-x-auto [scrollbar-width:none] [&::-webkit-scrollbar]:hidden">
|
||||
<FilterPill label="All" active={mediaFilter === "all" && !favoritesOnly && !failedEmbeddingsOnly && !failedTaggingOnly && minimumRating === 0 && colorFilter === null} onClick={() => { setMediaFilter("all"); setFavoritesOnly(false); setMinimumRating(0); setFailedEmbeddingsOnly(false); setFailedTaggingOnly(false); setColorFilter(null); }} />
|
||||
<FilterPill label="Images" active={mediaFilter === "image" && !favoritesOnly && !failedEmbeddingsOnly && !failedTaggingOnly} onClick={() => { setMediaFilter("image"); setFavoritesOnly(false); setFailedEmbeddingsOnly(false); setFailedTaggingOnly(false); }} />
|
||||
<FilterPill label="Videos" active={mediaFilter === "video" && !favoritesOnly && !failedEmbeddingsOnly && !failedTaggingOnly} onClick={() => { setMediaFilter("video"); setFavoritesOnly(false); setFailedEmbeddingsOnly(false); setFailedTaggingOnly(false); }} />
|
||||
@@ -481,8 +483,9 @@ export function Toolbar() {
|
||||
onClick={() => setFailedTaggingOnly(!failedTaggingOnly)}
|
||||
/>
|
||||
) : null}
|
||||
{isSimilarResults ? <span className="ml-2 shrink-0 whitespace-nowrap text-[11px] text-gray-500">Current similar scope: {similarScope === "current_album" ? "this album" : similarScope === "current_folder" ? "current folder" : "all media"}</span> : null}
|
||||
</div>
|
||||
<ColorFilter />
|
||||
{isSimilarResults ? <span className="ml-2 text-[11px] text-gray-500">Current similar scope: {similarScope === "current_album" ? "this album" : similarScope === "current_folder" ? "current folder" : "all media"}</span> : null}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user