Refine gallery hover border flow

This commit is contained in:
FengLee
2026-05-13 20:44:30 +08:00
parent 489c4c377a
commit f06c475034
4 changed files with 55 additions and 72 deletions

View File

@@ -77,7 +77,7 @@ Use this guide when the user reports behavior. Start from the symptom row, inspe
| Gallery delete does not remove public item | `src/app/api/gallery/route.ts`, admin UI route using it | DELETE unpublishes by setting `is_public = false`, not hard delete. |
| Search/filter/sort wrong | `src/app/api/gallery/route.ts`, `src/app/gallery/page.tsx` | Query params `type`, `limit`, `offset`, `sort`, `q/search`; SQL where/order. |
| Gallery search box looks inconsistent with the rest of the UI | `src/app/gallery/page.tsx` | The search field is a custom glass panel with an inner focused input surface; avoid reverting it to a plain transparent input row. |
| Gallery hover makes images muddy, covers the image with prompt text, or action buttons disappear on dark/light images | `src/app/gallery/page.tsx`, `src/app/globals.css` | Gallery cards should not use a full-image dark hover overlay or center prompt text. Keep hover feedback on the card container with image-color glow/scale, and keep like/download buttons legible through sampled image brightness inversion. |
| Gallery hover makes images muddy, covers the image with prompt text, shows a rotating glow block, or action buttons disappear on dark/light images | `src/app/gallery/page.tsx`, `src/app/globals.css` | Gallery cards should not use a full-image dark hover overlay, center prompt text, or broad square glow under the card. Keep hover feedback on the card container with scale plus a thin image-color perimeter flow, and keep like/download buttons legible through sampled image brightness inversion. |
## Canvas

View File

@@ -102,7 +102,7 @@ Use this document to jump directly to code before broad searching.
| Feature | Files | Notes |
| --- | --- | --- |
| Public gallery page | `src/app/gallery/page.tsx`, `src/app/globals.css` | Lists public works, search/sort/filter, preview/download. The search box is custom styled in-page to match the glass UI; gallery cards use hover scale plus image-color glow instead of an image-covering dark overlay, and hover like/download buttons invert against sampled image brightness. Gallery detail image previews use `ImageMetadataBadge` for actual ratio/resolution. |
| Public gallery page | `src/app/gallery/page.tsx`, `src/app/globals.css` | Lists public works, search/sort/filter, preview/download. The search box is custom styled in-page to match the glass UI; gallery cards use hover scale plus a thin image-color perimeter flow instead of an image-covering dark overlay or broad glow block, and hover like/download buttons invert against sampled image brightness. Gallery detail image previews use `ImageMetadataBadge` for actual ratio/resolution. |
| Public gallery API | `src/app/api/gallery/route.ts` | GET public works, admin DELETE unpublishes. |
| Publish API | `src/app/api/gallery/publish/route.ts` | Copies media into gallery folders and inserts public work. |
| History persistence | `src/app/api/creation-history/route.ts`, `src/lib/creation-history-store.ts` | User-private completed works and published state. Single-record deletion is server-first when logged in; detail dialogs call the same store path and then refresh local history. |

View File

@@ -659,15 +659,6 @@ export default function GalleryPage() {
className="gallery-work-shell group"
style={getGalleryCardStyle(cardPalettes[work.id])}
>
{mediaPreviewUrl && (
<img
src={mediaPreviewUrl}
alt=""
aria-hidden="true"
className="gallery-work-image-glow"
loading="lazy"
/>
)}
<Card
className={`${galleryGlassCard} gallery-work-card w-full overflow-hidden cursor-pointer !rounded-2xl !py-0`}
onClick={() => setSelectedWork(work)}

View File

@@ -227,39 +227,66 @@
.gallery-work-shell::before {
content: "";
position: absolute;
inset: -10px;
z-index: -2;
border-radius: 1.35rem;
inset: -2px;
z-index: -1;
padding: 2px;
border-radius: 1.12rem;
background:
conic-gradient(
from 0deg,
var(--gallery-accent-1),
var(--gallery-accent-2),
var(--gallery-accent-3),
var(--gallery-accent-1)
transparent 0deg,
transparent 38deg,
var(--gallery-accent-1) 62deg,
var(--gallery-accent-2) 86deg,
var(--gallery-accent-3) 112deg,
transparent 142deg,
transparent 360deg
);
filter: blur(22px);
opacity: 0;
transform: scale(0.94) rotate(0deg);
transition:
opacity 320ms ease,
transform 320ms cubic-bezier(0.2, 0.8, 0.2, 1);
animation: gallery-glow-flow 5.5s linear infinite;
transition: opacity 260ms ease;
animation: gallery-border-flow 2.8s linear infinite;
animation-play-state: paused;
-webkit-mask:
linear-gradient(#000 0 0) content-box,
linear-gradient(#000 0 0);
-webkit-mask-composite: xor;
mask:
linear-gradient(#000 0 0) content-box,
linear-gradient(#000 0 0);
mask-composite: exclude;
}
.gallery-work-shell::after {
content: "";
position: absolute;
inset: -2px;
z-index: -1;
border-radius: 1.05rem;
inset: -5px;
z-index: -2;
padding: 5px;
border-radius: 1.28rem;
background:
linear-gradient(135deg, rgb(255 255 255 / 0.24), transparent 32% 68%, rgb(255 255 255 / 0.12)),
linear-gradient(120deg, var(--gallery-accent-1), transparent 38% 62%, var(--gallery-accent-2));
filter: blur(10px);
conic-gradient(
from 0deg,
transparent 0deg,
transparent 42deg,
var(--gallery-accent-1) 66deg,
var(--gallery-accent-2) 92deg,
var(--gallery-accent-3) 118deg,
transparent 152deg,
transparent 360deg
);
filter: blur(8px);
opacity: 0;
transition: opacity 320ms ease;
animation: gallery-border-flow 2.8s linear infinite;
animation-play-state: paused;
-webkit-mask:
linear-gradient(#000 0 0) content-box,
linear-gradient(#000 0 0);
-webkit-mask-composite: xor;
mask:
linear-gradient(#000 0 0) content-box,
linear-gradient(#000 0 0);
mask-composite: exclude;
}
.gallery-work-shell:hover {
@@ -269,13 +296,13 @@
}
.gallery-work-shell:hover::before {
opacity: 0.62;
transform: scale(1.02) rotate(360deg);
opacity: 0.98;
animation-play-state: running;
}
.gallery-work-shell:hover::after {
opacity: 0.5;
opacity: 0.42;
animation-play-state: running;
}
.gallery-work-card {
@@ -290,34 +317,10 @@
.gallery-work-shell:hover .gallery-work-card {
border-color: color-mix(in srgb, var(--gallery-accent-1) 48%, white 24%);
box-shadow:
0 18px 46px rgb(0 0 0 / 0.22),
0 16px 34px rgb(0 0 0 / 0.18),
inset 0 1px 0 rgb(255 255 255 / 0.12);
}
.gallery-work-image-glow {
position: absolute;
inset: -18px;
z-index: -1;
width: calc(100% + 36px);
height: calc(100% + 36px);
object-fit: cover;
border-radius: 1.6rem;
opacity: 0;
filter: blur(28px) saturate(1.38);
transform: scale(0.98);
transition:
opacity 320ms ease,
transform 520ms cubic-bezier(0.2, 0.8, 0.2, 1);
animation: gallery-image-aura 7s ease-in-out infinite;
animation-play-state: paused;
}
.gallery-work-shell:hover .gallery-work-image-glow {
opacity: 0.42;
transform: scale(1.08);
animation-play-state: running;
}
.gallery-work-action-button {
border-radius: 9999px !important;
border: 1px solid var(--gallery-action-border) !important;
@@ -343,22 +346,12 @@
color: currentColor;
}
@keyframes gallery-glow-flow {
@keyframes gallery-border-flow {
0% {
transform: scale(1.02) rotate(0deg);
transform: rotate(0deg);
}
100% {
transform: scale(1.02) rotate(360deg);
}
}
@keyframes gallery-image-aura {
0%,
100% {
transform: scale(1.06) translate3d(-1%, 0, 0);
}
50% {
transform: scale(1.1) translate3d(1%, -1%, 0);
transform: rotate(360deg);
}
}
@@ -366,7 +359,6 @@
.gallery-work-shell,
.gallery-work-shell::before,
.gallery-work-shell::after,
.gallery-work-image-glow,
.gallery-work-action-button {
animation: none !important;
transition-duration: 1ms !important;