fix: move lightbox to baseof as persistent component, fix HTMX history conflicts
- Lightbox lives outside #main-content so HTMX never destroys it - Gallery dispatches window events to open/close lightbox - Add hx-history-elt to #main-content so only that element is snapshotted - Remove view-transition-name to avoid duplicate conflict on history restore - Close lightbox on navigation via lightbox:close event Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
+8
-3
@@ -44,6 +44,11 @@ function initLazyVideos(root) {
|
|||||||
|
|
||||||
initLazyVideos();
|
initLazyVideos();
|
||||||
|
|
||||||
|
// ── Close lightbox on navigation ──────────────────────────────────
|
||||||
|
document.body.addEventListener("htmx:beforeSwap", () => {
|
||||||
|
window.dispatchEvent(new CustomEvent("lightbox:close"));
|
||||||
|
});
|
||||||
|
|
||||||
// ── After HTMX partial swap ────────────────────────────────────────
|
// ── After HTMX partial swap ────────────────────────────────────────
|
||||||
document.body.addEventListener("htmx:afterSwap", (e) => {
|
document.body.addEventListener("htmx:afterSwap", (e) => {
|
||||||
initLazyVideos(e.target);
|
initLazyVideos(e.target);
|
||||||
@@ -53,10 +58,10 @@ document.body.addEventListener("htmx:afterSwap", (e) => {
|
|||||||
|
|
||||||
// ── After HTMX history restore (back / forward navigation) ────────
|
// ── After HTMX history restore (back / forward navigation) ────────
|
||||||
document.body.addEventListener("htmx:historyRestore", () => {
|
document.body.addEventListener("htmx:historyRestore", () => {
|
||||||
// Sync nav active state to the restored URL
|
window.dispatchEvent(new CustomEvent("lightbox:close"));
|
||||||
|
|
||||||
if (window.Alpine) Alpine.store("nav").path = window.location.pathname;
|
if (window.Alpine) Alpine.store("nav").path = window.location.pathname;
|
||||||
|
|
||||||
// Complete the progress bar (htmx:afterSettle never fires on history restore)
|
|
||||||
const bar = document.getElementById("progress-bar");
|
const bar = document.getElementById("progress-bar");
|
||||||
if (bar) {
|
if (bar) {
|
||||||
bar.style.width = "100%";
|
bar.style.width = "100%";
|
||||||
@@ -69,11 +74,11 @@ document.body.addEventListener("htmx:historyRestore", () => {
|
|||||||
const main = document.getElementById("main-content");
|
const main = document.getElementById("main-content");
|
||||||
if (!main) return;
|
if (!main) return;
|
||||||
if (window.Alpine) Alpine.initTree(main);
|
if (window.Alpine) Alpine.initTree(main);
|
||||||
// Reload and replay autoplay videos (autoplay attr is ignored on restore)
|
|
||||||
main.querySelectorAll("video[autoplay]").forEach((v) => {
|
main.querySelectorAll("video[autoplay]").forEach((v) => {
|
||||||
v.load();
|
v.load();
|
||||||
v.play().catch(() => {});
|
v.play().catch(() => {});
|
||||||
});
|
});
|
||||||
window.scrollTo({ top: 0, behavior: "instant" });
|
window.scrollTo({ top: 0, behavior: "instant" });
|
||||||
|
window.dispatchEvent(new Event("scroll"));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -19,12 +19,101 @@
|
|||||||
|
|
||||||
{{- block "page-background" . -}}{{- end -}}
|
{{- block "page-background" . -}}{{- end -}}
|
||||||
|
|
||||||
<main id="main-content" class="flex-1" style="view-transition-name: main-content">
|
<main id="main-content" class="flex-1" hx-history-elt>
|
||||||
{{- block "main" . }}{{- end }}
|
{{- block "main" . }}{{- end }}
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
{{- partial "footer.html" . -}}
|
{{- partial "footer.html" . -}}
|
||||||
|
|
||||||
|
<!-- ── Global lightbox — lives outside #main-content so HTMX never touches it -->
|
||||||
|
<div
|
||||||
|
x-data="{
|
||||||
|
open: false,
|
||||||
|
idx: 0,
|
||||||
|
fill: false,
|
||||||
|
items: [],
|
||||||
|
close() { this.open = false; this.fill = false },
|
||||||
|
prev() { this.idx = (this.idx - 1 + this.items.length) % this.items.length },
|
||||||
|
next() { this.idx = (this.idx + 1) % this.items.length }
|
||||||
|
}"
|
||||||
|
@lightbox:open.window="items = $event.detail.items; idx = $event.detail.idx; open = true"
|
||||||
|
@lightbox:close.window="close()"
|
||||||
|
@keydown.escape.window="open && close()"
|
||||||
|
@keydown.arrow-left.window="open && prev()"
|
||||||
|
@keydown.arrow-right.window="open && next()"
|
||||||
|
@keydown.f.window="open && (fill = !fill)"
|
||||||
|
x-show="open"
|
||||||
|
x-cloak
|
||||||
|
x-transition:enter="transition duration-200 ease-out"
|
||||||
|
x-transition:enter-start="opacity-0"
|
||||||
|
x-transition:enter-end="opacity-100"
|
||||||
|
x-transition:leave="transition duration-150 ease-in"
|
||||||
|
x-transition:leave-start="opacity-100"
|
||||||
|
x-transition:leave-end="opacity-0"
|
||||||
|
class="fixed inset-0 z-[200] flex items-center justify-center bg-void/95 backdrop-blur-sm"
|
||||||
|
@click.self="close()"
|
||||||
|
role="dialog"
|
||||||
|
aria-modal="true"
|
||||||
|
aria-label="Media lightbox"
|
||||||
|
>
|
||||||
|
<!-- Top chrome bar -->
|
||||||
|
<div class="absolute top-0 inset-x-0 z-10 flex items-center justify-between px-2 h-14"
|
||||||
|
style="background: linear-gradient(to bottom, rgba(5,5,16,0.85), transparent)">
|
||||||
|
<div class="absolute top-0 inset-x-0 h-px gradient-line"></div>
|
||||||
|
<div class="w-24"></div>
|
||||||
|
<div class="label text-fog tabular-nums"
|
||||||
|
x-text="items.length ? `${String(idx + 1).padStart(2,'0')} / ${String(items.length).padStart(2,'0')}` : ''"></div>
|
||||||
|
<div class="flex items-center w-24 justify-end gap-1">
|
||||||
|
<button @click="fill = !fill"
|
||||||
|
class="w-10 h-10 flex items-center justify-center label transition-colors"
|
||||||
|
:class="fill ? 'text-heat hover:text-chalk' : 'text-fog hover:text-heat'"
|
||||||
|
:aria-label="fill ? 'Switch to fit mode' : 'Switch to fill mode'"
|
||||||
|
x-text="fill ? 'FIT' : 'FILL'"></button>
|
||||||
|
<button @click="close()"
|
||||||
|
class="w-10 h-10 flex items-center justify-center label text-fog hover:text-chalk transition-colors"
|
||||||
|
aria-label="Close">✕</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- Prev -->
|
||||||
|
<button @click="prev()" x-show="items.length > 1"
|
||||||
|
class="absolute left-2 top-1/2 -translate-y-1/2 z-10 w-10 h-10 md:w-12 md:h-12 flex items-center justify-center text-fog hover:text-heat transition-colors text-xl"
|
||||||
|
aria-label="Previous">{{- partial "icon.html" "arrow-left" -}}</button>
|
||||||
|
<!-- Media -->
|
||||||
|
<div class="absolute inset-0 flex items-center justify-center transition-[padding] duration-300"
|
||||||
|
:class="fill ? 'p-0' : 'pt-14 pb-14 px-14 md:px-20'">
|
||||||
|
<video
|
||||||
|
x-show="items[idx] && items[idx].video"
|
||||||
|
:src="items[idx] && items[idx].video ? items[idx].video : ''"
|
||||||
|
:poster="items[idx] ? items[idx].img : ''"
|
||||||
|
x-effect="if (open && items[idx] && items[idx].video) { $el.load(); $el.play() }"
|
||||||
|
:class="fill ? 'w-full h-full object-cover' : 'max-w-full max-h-full object-contain'"
|
||||||
|
class="transition-all duration-300"
|
||||||
|
controls loop muted playsinline></video>
|
||||||
|
<img
|
||||||
|
x-show="items[idx] && !items[idx].video"
|
||||||
|
:src="items[idx] && !items[idx].video ? items[idx].img : ''"
|
||||||
|
alt=""
|
||||||
|
:class="fill ? 'w-full h-full object-cover' : 'max-w-full max-h-full object-contain'"
|
||||||
|
class="transition-all duration-300">
|
||||||
|
</div>
|
||||||
|
<!-- Next -->
|
||||||
|
<button @click="next()" x-show="items.length > 1"
|
||||||
|
class="absolute right-2 top-1/2 -translate-y-1/2 z-10 w-10 h-10 md:w-12 md:h-12 flex items-center justify-center text-fog hover:text-heat transition-colors text-xl"
|
||||||
|
aria-label="Next">{{- partial "icon.html" "arrow-right" -}}</button>
|
||||||
|
<!-- Dots -->
|
||||||
|
<div x-show="items.length > 1"
|
||||||
|
class="absolute bottom-0 inset-x-0 z-10 flex items-center justify-center h-14 gap-2"
|
||||||
|
style="background: linear-gradient(to top, rgba(5,5,16,0.85), transparent)">
|
||||||
|
<div class="absolute bottom-0 inset-x-0 h-px gradient-line"></div>
|
||||||
|
<template x-for="(item, i) in items" :key="i">
|
||||||
|
<button @click="idx = i"
|
||||||
|
class="h-1.5 rounded-full transition-all duration-300"
|
||||||
|
:class="i === idx ? 'w-5 bg-heat' : 'w-1.5 bg-fog hover:bg-chalk'"
|
||||||
|
:aria-label="`Go to item ${i + 1}`"></button>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- HTMX (page transitions & progressive enhancement) -->
|
<!-- HTMX (page transitions & progressive enhancement) -->
|
||||||
<script src="https://unpkg.com/htmx.org@2.0.4/dist/htmx.min.js"></script>
|
<script src="https://unpkg.com/htmx.org@2.0.4/dist/htmx.min.js"></script>
|
||||||
<!-- Alpine.js store initialisation (must run before alpine:init) -->
|
<!-- Alpine.js store initialisation (must run before alpine:init) -->
|
||||||
|
|||||||
+3
-118
@@ -112,22 +112,7 @@
|
|||||||
{{- $lbItems = $lbItems | append $entry -}}
|
{{- $lbItems = $lbItems | append $entry -}}
|
||||||
{{- end -}}
|
{{- end -}}
|
||||||
|
|
||||||
<div
|
<div x-data="{ items: {{ $lbItems | jsonify }} }">
|
||||||
x-data="{
|
|
||||||
open: false,
|
|
||||||
idx: 0,
|
|
||||||
fill: false,
|
|
||||||
items: {{ $lbItems | jsonify }},
|
|
||||||
show(i) { this.idx = i; this.open = true },
|
|
||||||
close() { this.open = false; this.fill = false },
|
|
||||||
prev() { this.idx = (this.idx - 1 + this.items.length) % this.items.length },
|
|
||||||
next() { this.idx = (this.idx + 1) % this.items.length }
|
|
||||||
}"
|
|
||||||
@keydown.escape.window="open && close()"
|
|
||||||
@keydown.arrow-left.window="open && prev()"
|
|
||||||
@keydown.arrow-right.window="open && next()"
|
|
||||||
@keydown.f.window="open && (fill = !fill)"
|
|
||||||
>
|
|
||||||
|
|
||||||
<div class="flex items-center gap-5 mb-10">
|
<div class="flex items-center gap-5 mb-10">
|
||||||
<span class="badge badge-gradient">Gallery</span>
|
<span class="badge badge-gradient">Gallery</span>
|
||||||
@@ -135,12 +120,12 @@
|
|||||||
<div class="flex-1 gradient-line ml-2"></div>
|
<div class="flex-1 gradient-line ml-2"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Numbered png+mp4 pairs from page bundle -->
|
|
||||||
<div class="columns-1 sm:columns-2 lg:columns-3 gap-5 space-y-5">
|
<div class="columns-1 sm:columns-2 lg:columns-3 gap-5 space-y-5">
|
||||||
{{- range $i, $img := $bundleImages -}}
|
{{- range $i, $img := $bundleImages -}}
|
||||||
{{- $stem := $img.Name | strings.TrimSuffix ".png" -}}
|
{{- $stem := $img.Name | strings.TrimSuffix ".png" -}}
|
||||||
{{- $video := $.Resources.GetMatch (printf "%s.mp4" $stem) -}}
|
{{- $video := $.Resources.GetMatch (printf "%s.mp4" $stem) -}}
|
||||||
<figure class="break-inside-avoid group card-comic cursor-zoom-in" @click="show({{ $i }})">
|
<figure class="break-inside-avoid group card-comic cursor-zoom-in"
|
||||||
|
@click="$dispatch('lightbox:open', { items, idx: {{ $i }} })">
|
||||||
<div class="card-media min-h-48">
|
<div class="card-media min-h-48">
|
||||||
{{- if $video -}}
|
{{- if $video -}}
|
||||||
<video class="w-full pointer-events-none" src="{{ $video.RelPermalink }}" poster="{{ $img.RelPermalink }}" autoplay loop muted playsinline></video>
|
<video class="w-full pointer-events-none" src="{{ $video.RelPermalink }}" poster="{{ $img.RelPermalink }}" autoplay loop muted playsinline></video>
|
||||||
@@ -152,106 +137,6 @@
|
|||||||
{{- end -}}
|
{{- end -}}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Lightbox — teleported to <body> to escape stacking context -->
|
|
||||||
<template x-teleport="body">
|
|
||||||
<div
|
|
||||||
x-show="open"
|
|
||||||
x-cloak
|
|
||||||
x-transition:enter="transition duration-200 ease-out"
|
|
||||||
x-transition:enter-start="opacity-0"
|
|
||||||
x-transition:enter-end="opacity-100"
|
|
||||||
x-transition:leave="transition duration-150 ease-in"
|
|
||||||
x-transition:leave-start="opacity-100"
|
|
||||||
x-transition:leave-end="opacity-0"
|
|
||||||
class="fixed inset-0 z-50 flex items-center justify-center bg-void/95 backdrop-blur-sm"
|
|
||||||
@click.self="close()"
|
|
||||||
role="dialog"
|
|
||||||
aria-modal="true"
|
|
||||||
aria-label="Media lightbox"
|
|
||||||
>
|
|
||||||
<!-- ── Top chrome bar ── -->
|
|
||||||
<div class="absolute top-0 inset-x-0 z-10 flex items-center justify-between px-2 h-14"
|
|
||||||
style="background: linear-gradient(to bottom, rgba(5,5,16,0.85), transparent)">
|
|
||||||
<!-- gradient accent -->
|
|
||||||
<div class="absolute top-0 inset-x-0 h-px gradient-line"></div>
|
|
||||||
<!-- left spacer (mirrors right buttons width) -->
|
|
||||||
<div class="w-24"></div>
|
|
||||||
<!-- counter centred -->
|
|
||||||
<div class="label text-fog tabular-nums"
|
|
||||||
x-text="`${String(idx + 1).padStart(2,'0')} / ${String(items.length).padStart(2,'0')}`"></div>
|
|
||||||
<!-- right actions: fill toggle + close -->
|
|
||||||
<div class="flex items-center w-24 justify-end gap-1">
|
|
||||||
<button
|
|
||||||
@click="fill = !fill"
|
|
||||||
class="w-10 h-10 flex items-center justify-center label transition-colors"
|
|
||||||
:class="fill ? 'text-heat hover:text-chalk' : 'text-fog hover:text-heat'"
|
|
||||||
:aria-label="fill ? 'Switch to fit mode' : 'Switch to fill mode'"
|
|
||||||
x-text="fill ? 'FIT' : 'FILL'"
|
|
||||||
></button>
|
|
||||||
<button
|
|
||||||
@click="close()"
|
|
||||||
class="w-10 h-10 flex items-center justify-center label text-fog hover:text-chalk transition-colors"
|
|
||||||
aria-label="Close"
|
|
||||||
>✕</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- ── Prev ── -->
|
|
||||||
<button
|
|
||||||
@click="prev()"
|
|
||||||
x-show="items.length > 1"
|
|
||||||
class="absolute left-2 top-1/2 -translate-y-1/2 z-10 w-10 h-10 md:w-12 md:h-12 flex items-center justify-center text-fog hover:text-heat transition-colors text-xl"
|
|
||||||
aria-label="Previous"
|
|
||||||
>{{ partial "icon.html" "arrow-left" }}</button>
|
|
||||||
|
|
||||||
<!-- ── Media ── -->
|
|
||||||
<div
|
|
||||||
class="absolute inset-0 flex items-center justify-center transition-[padding] duration-300"
|
|
||||||
:class="fill ? 'p-0' : 'pt-14 pb-14 px-14 md:px-20'"
|
|
||||||
>
|
|
||||||
<video
|
|
||||||
x-show="items[idx] && items[idx].video"
|
|
||||||
:src="items[idx] && items[idx].video ? items[idx].video : ''"
|
|
||||||
:poster="items[idx] ? items[idx].img : ''"
|
|
||||||
x-effect="if (open && items[idx] && items[idx].video) { $el.load(); $el.play() }"
|
|
||||||
:class="fill ? 'w-full h-full object-cover' : 'max-w-full max-h-full object-contain'"
|
|
||||||
class="transition-all duration-300"
|
|
||||||
controls loop muted playsinline
|
|
||||||
></video>
|
|
||||||
<img
|
|
||||||
x-show="items[idx] && !items[idx].video"
|
|
||||||
:src="items[idx] && !items[idx].video ? items[idx].img : ''"
|
|
||||||
alt=""
|
|
||||||
:class="fill ? 'w-full h-full object-cover' : 'max-w-full max-h-full object-contain'"
|
|
||||||
class="transition-all duration-300"
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- ── Next ── -->
|
|
||||||
<button
|
|
||||||
@click="next()"
|
|
||||||
x-show="items.length > 1"
|
|
||||||
class="absolute right-2 top-1/2 -translate-y-1/2 z-10 w-10 h-10 md:w-12 md:h-12 flex items-center justify-center text-fog hover:text-heat transition-colors text-xl"
|
|
||||||
aria-label="Next"
|
|
||||||
>{{ partial "icon.html" "arrow-right" }}</button>
|
|
||||||
|
|
||||||
<!-- ── Bottom chrome bar (dots) ── -->
|
|
||||||
<div x-show="items.length > 1"
|
|
||||||
class="absolute bottom-0 inset-x-0 z-10 flex items-center justify-center h-14 gap-2"
|
|
||||||
style="background: linear-gradient(to top, rgba(5,5,16,0.85), transparent)">
|
|
||||||
<div class="absolute bottom-0 inset-x-0 h-px gradient-line"></div>
|
|
||||||
<template x-for="(item, i) in items" :key="i">
|
|
||||||
<button
|
|
||||||
@click="idx = i"
|
|
||||||
class="h-1.5 rounded-full transition-all duration-300"
|
|
||||||
:class="i === idx ? 'w-5 bg-heat' : 'w-1.5 bg-fog hover:bg-chalk'"
|
|
||||||
:aria-label="`Go to item ${i + 1}`"
|
|
||||||
></button>
|
|
||||||
</template>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{{- else -}}
|
{{- else -}}
|
||||||
|
|||||||
Reference in New Issue
Block a user