Skip to content

Commit bc5849d

Browse files
committed
feat: enhance GalleryLightbox with improved thumbnail display and navigation functionality
1 parent e461970 commit bc5849d

File tree

2 files changed

+194
-60
lines changed

2 files changed

+194
-60
lines changed

src/components/GalleryLightbox.astro

Lines changed: 108 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -18,17 +18,35 @@ const galleryId = `glb-${Math.random().toString(36).slice(2, 10)}`;
1818
const dialogId = `dlg-${galleryId}`;
1919
---
2020

21-
<div class="gallery" style={`--cols:${cols};`} data-gallery-id={galleryId}>
22-
{images.map((img, i) => (
23-
<button
24-
class="thumb"
25-
type="button"
26-
data-index={i}
27-
aria-label={`Open image ${i + 1} of ${images.length}`}
28-
>
29-
<img src={img.src} alt={img.alt ?? ""} loading="lazy" />
30-
</button>
31-
))}
21+
<div class="gallery" style={`--cols:${cols};`} data-gallery-id={galleryId} data-total={images.length}>
22+
<div class="gallery-grid">
23+
{images.map((img, i) => (
24+
<button
25+
class="thumb"
26+
type="button"
27+
data-index={i}
28+
aria-label={`Open image ${i + 1} of ${images.length}`}
29+
>
30+
<img
31+
class="thumb-bg"
32+
src={img.src}
33+
alt=""
34+
aria-hidden="true"
35+
loading="lazy"
36+
/>
37+
<img
38+
class="thumb-img"
39+
src={img.src}
40+
alt={img.alt ?? ""}
41+
loading="lazy"
42+
/>
43+
</button>
44+
))}
45+
</div>
46+
47+
<div class="gallery-actions">
48+
<button class="gallery-toggle" type="button" aria-expanded="false"></button>
49+
</div>
3250

3351
<dialog class="lightbox" id={dialogId} aria-label="Image viewer">
3452
<button class="close" type="button" aria-label="Close">×</button>
@@ -65,9 +83,79 @@ const dialogId = `dlg-${galleryId}`;
6583

6684
<script is:inline define:vars={{ galleryId, dialogId, images }}>
6785
(function () {
68-
const gallery = document.querySelector(`[data-gallery-id="${galleryId}"]`);
69-
if (!gallery || !Array.isArray(images) || images.length === 0) return;
86+
const root = document.querySelector(`[data-gallery-id="${galleryId}"]`);
87+
if (!root || !Array.isArray(images) || images.length === 0) return;
88+
89+
const grid = root.querySelector(".gallery-grid");
90+
const toggle = root.querySelector(".gallery-toggle");
91+
const thumbs = Array.from(root.querySelectorAll(".thumb"));
92+
const total = thumbs.length;
93+
94+
// initial: desktop=6 (2 rows @ 3 cols), mobile=5 (single column)
95+
const mq = window.matchMedia("(max-width: 560px)");
96+
const initialCount = mq.matches ? 5 : 6;
97+
98+
// If not enough images, hide toggle completely
99+
if (total <= initialCount) {
100+
root.classList.remove("is-collapsed");
101+
root.classList.remove("has-fade");
102+
toggle?.remove();
103+
} else {
104+
// start collapsed
105+
root.classList.add("is-collapsed");
106+
root.classList.add("has-fade");
107+
}
108+
109+
// Remember the gallery top for "scroll back on collapse"
110+
const galleryTopY = () => root.getBoundingClientRect().top + window.scrollY;
111+
112+
function setThumbVisibility(expanded) {
113+
thumbs.forEach((btn, idx) => {
114+
const shouldShow = expanded || idx < initialCount;
115+
btn.toggleAttribute("hidden", !shouldShow);
116+
});
117+
}
118+
119+
function updateToggle(expanded) {
120+
if (!toggle) return;
121+
toggle.setAttribute("aria-expanded", expanded ? "true" : "false");
122+
123+
if (expanded) {
124+
toggle.textContent = "Zwiń";
125+
} else {
126+
const remaining = total - initialCount;
127+
toggle.textContent = `Pokaż pozostałe ${remaining}`;
128+
}
129+
}
70130

131+
// init state
132+
let expanded = false;
133+
setThumbVisibility(false);
134+
updateToggle(false);
135+
136+
// Toggle expand/collapse
137+
toggle?.addEventListener("click", () => {
138+
const y = galleryTopY();
139+
140+
expanded = !expanded;
141+
142+
if (expanded) {
143+
root.classList.remove("is-collapsed");
144+
root.classList.remove("has-fade");
145+
setThumbVisibility(true);
146+
updateToggle(true);
147+
} else {
148+
root.classList.add("is-collapsed");
149+
root.classList.add("has-fade");
150+
setThumbVisibility(false);
151+
updateToggle(false);
152+
153+
// Scroll back to gallery position so user doesn't "get lost"
154+
window.scrollTo({ top: y - 12, left: 0, behavior: "smooth" });
155+
}
156+
});
157+
158+
// Lightbox
71159
const dialog = document.getElementById(dialogId);
72160
if (!dialog) return;
73161

@@ -97,24 +185,15 @@ const dialogId = `dlg-${galleryId}`;
97185

98186
function openAt(i) {
99187
render(i);
100-
101-
try {
102-
dialog.showModal();
103-
} catch (_) {}
104-
105-
// Focus without scrolling (prevents jump in many browsers)
188+
try { dialog.showModal(); } catch (_) {}
106189
btnClose?.focus({ preventScroll: true });
107190
}
108191

109-
function prev() {
110-
render(index - 1);
111-
}
112-
function next() {
113-
render(index + 1);
114-
}
192+
function prev() { render(index - 1); }
193+
function next() { render(index + 1); }
115194

116-
// Thumb click → open
117-
gallery.querySelectorAll(".thumb").forEach((btn) => {
195+
// Thumb click → open (even hidden thumbs remain in DOM with hidden attr toggled)
196+
thumbs.forEach((btn) => {
118197
btn.addEventListener("click", () => {
119198
const i = Number(btn.dataset.index || 0);
120199
openAt(i);
@@ -125,20 +204,13 @@ const dialogId = `dlg-${galleryId}`;
125204
btnPrev?.addEventListener("click", prev);
126205
btnNext?.addEventListener("click", next);
127206

128-
// Keyboard navigation (only when THIS dialog is open)
129207
document.addEventListener("keydown", (e) => {
130208
if (!dialog.open) return;
131209

132-
if (e.key === "ArrowLeft") {
133-
e.preventDefault();
134-
prev();
135-
} else if (e.key === "ArrowRight") {
136-
e.preventDefault();
137-
next();
138-
}
210+
if (e.key === "ArrowLeft") { e.preventDefault(); prev(); }
211+
else if (e.key === "ArrowRight") { e.preventDefault(); next(); }
139212
});
140213

141-
// Click on backdrop area (dialog element itself) closes
142214
dialog.addEventListener("click", (e) => {
143215
if (e.target === dialog) dialog.close();
144216
});

src/styles/global.css

Lines changed: 86 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -704,15 +704,21 @@ h1{
704704
Scope: the grid is limited to .gallery[data-gallery-id] so it won't affect
705705
other .gallery usages. The modal uses dialog.lightbox. */
706706

707-
/* Grid */
708-
.gallery[data-gallery-id] {
707+
/* Root wrapper */
708+
.gallery[data-gallery-id]{
709+
display: block;
710+
}
711+
712+
/* Grid wrapper */
713+
.gallery[data-gallery-id] .gallery-grid{
709714
display: grid;
710715
grid-template-columns: repeat(var(--cols, 3), minmax(0, 1fr));
711716
gap: 14px;
712717
}
713718

714719
/* Thumbnails: constant height to avoid "tallest item stretches the row" */
715-
.gallery[data-gallery-id] .thumb {
720+
.gallery[data-gallery-id] .thumb{
721+
position: relative;
716722
padding: 0;
717723
border: 1px solid rgba(255,255,255,.12);
718724
border-radius: 14px;
@@ -722,15 +728,71 @@ h1{
722728
aspect-ratio: 16 / 10;
723729
}
724730

725-
.gallery[data-gallery-id] .thumb img {
731+
/* Blurred background layer */
732+
.gallery[data-gallery-id] .thumb .thumb-bg{
733+
position: absolute;
734+
inset: 0;
726735
width: 100%;
727736
height: 100%;
728-
display: block;
729737
object-fit: cover;
738+
filter: blur(14px);
739+
transform: scale(1.08);
740+
opacity: .55;
741+
pointer-events: none;
742+
}
743+
744+
/* Foreground image (always fully visible) */
745+
.gallery[data-gallery-id] .thumb .thumb-img{
746+
position: relative;
747+
width: 100%;
748+
height: 100%;
749+
object-fit: contain;
750+
display: block;
751+
}
752+
753+
/* Fade effect when collapsed */
754+
.gallery[data-gallery-id].has-fade .gallery-grid{
755+
position: relative;
756+
}
757+
758+
.gallery[data-gallery-id].has-fade .gallery-grid::after{
759+
content: "";
760+
position: absolute;
761+
left: 0;
762+
right: 0;
763+
bottom: 0;
764+
height: 72px;
765+
pointer-events: none;
766+
background: linear-gradient(to bottom, rgba(0,0,0,0), rgba(0,0,0,.85));
767+
}
768+
769+
/* Actions */
770+
.gallery[data-gallery-id] .gallery-actions{
771+
margin-top: 12px;
772+
display: flex;
773+
justify-content: center;
774+
}
775+
776+
.gallery[data-gallery-id] .gallery-toggle{
777+
border: 1px solid rgba(255,255,255,.14);
778+
background: rgba(255,255,255,.06);
779+
color: rgba(255,255,255,.92);
780+
border-radius: 999px;
781+
padding: 10px 14px;
782+
cursor: pointer;
783+
font: inherit;
784+
}
785+
786+
.gallery[data-gallery-id] .gallery-toggle:hover{
787+
background: rgba(255,255,255,.10);
788+
}
789+
790+
.gallery[data-gallery-id] .gallery-toggle:active{
791+
background: rgba(255,255,255,.14);
730792
}
731793

732794
/* Lightbox modal: centered in viewport (no page scroll needed) */
733-
dialog.lightbox {
795+
dialog.lightbox{
734796
position: fixed;
735797
top: 50%;
736798
left: 50%;
@@ -744,27 +806,27 @@ dialog.lightbox {
744806
max-height: 90vh;
745807
}
746808

747-
dialog.lightbox::backdrop {
809+
dialog.lightbox::backdrop{
748810
background: rgba(0,0,0,.75);
749811
}
750812

751813
/* Lightbox content */
752-
dialog.lightbox .lb-image {
814+
dialog.lightbox .lb-image{
753815
max-width: 90vw;
754816
max-height: 80vh;
755817
display: block;
756818
margin: 24px auto 12px;
757819
}
758820

759-
dialog.lightbox .caption {
821+
dialog.lightbox .caption{
760822
color: rgba(255,255,255,.75);
761823
text-align: center;
762824
padding: 0 20px 20px;
763825
font-size: 14px;
764826
}
765827

766828
/* Close + navigation buttons: reset to avoid any global button styles */
767-
dialog.lightbox .close {
829+
dialog.lightbox .close{
768830
all: unset;
769831
position: absolute;
770832
top: 12px;
@@ -777,7 +839,7 @@ dialog.lightbox .close {
777839
padding: 8px 10px;
778840
}
779841

780-
dialog.lightbox .nav {
842+
dialog.lightbox .nav{
781843
all: unset;
782844
position: absolute;
783845
top: 50%;
@@ -796,34 +858,34 @@ dialog.lightbox .nav {
796858
user-select: none;
797859
}
798860

799-
dialog.lightbox .nav:hover { background: rgba(255,255,255,.16); }
800-
dialog.lightbox .nav:active { background: rgba(255,255,255,.22); }
861+
dialog.lightbox .nav:hover{ background: rgba(255,255,255,.16); }
862+
dialog.lightbox .nav:active{ background: rgba(255,255,255,.22); }
801863

802-
dialog.lightbox .nav svg {
864+
dialog.lightbox .nav svg{
803865
width: 22px;
804866
height: 22px;
805867
display: block;
806868
}
807869

808-
dialog.lightbox .nav.prev { left: 14px; }
809-
dialog.lightbox .nav.next { right: 14px; }
870+
dialog.lightbox .nav.prev{ left: 14px; }
871+
dialog.lightbox .nav.next{ right: 14px; }
810872

811873
/* Responsive */
812-
@media (max-width: 900px) {
813-
.gallery[data-gallery-id] { grid-template-columns: repeat(2, minmax(0, 1fr)); }
874+
@media (max-width: 900px){
875+
.gallery[data-gallery-id] .gallery-grid{ grid-template-columns: repeat(2, minmax(0, 1fr)); }
814876
}
815877

816-
@media (max-width: 560px) {
817-
.gallery[data-gallery-id] { grid-template-columns: 1fr; }
878+
@media (max-width: 560px){
879+
.gallery[data-gallery-id] .gallery-grid{ grid-template-columns: 1fr; }
818880

819-
dialog.lightbox .nav {
881+
dialog.lightbox .nav{
820882
width: 40px;
821883
height: 40px;
822884
}
823-
dialog.lightbox .nav svg {
885+
dialog.lightbox .nav svg{
824886
width: 20px;
825887
height: 20px;
826888
}
827-
dialog.lightbox .nav.prev { left: 10px; }
828-
dialog.lightbox .nav.next { right: 10px; }
889+
dialog.lightbox .nav.prev{ left: 10px; }
890+
dialog.lightbox .nav.next{ right: 10px; }
829891
}

0 commit comments

Comments
 (0)