feat: lightbox fill/fit toggle mode

Add fill state (default false) to the Alpine lightbox data:
- FILL button (top-right, left of ✕): switches media to object-cover,
  filling the entire overlay edge-to-edge; button turns heat pink
- FIT restores object-contain with padding; button returns to fog
- Keyboard shortcut F toggles fill while lightbox is open
- close() resets fill to false for the next open
- transition-all duration-300 on container padding and media for
  a smooth morph between modes

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-11 18:33:45 +02:00
parent 1998820dec
commit 08ddfd54d7
+20 -4
View File
@@ -116,15 +116,17 @@
x-data="{ x-data="{
open: false, open: false,
idx: 0, idx: 0,
fill: false,
items: {{ $lbItems | jsonify }}, items: {{ $lbItems | jsonify }},
show(i) { this.idx = i; this.open = true }, show(i) { this.idx = i; this.open = true },
close() { this.open = false }, close() { this.open = false; this.fill = false },
prev() { this.idx = (this.idx - 1 + this.items.length) % this.items.length }, prev() { this.idx = (this.idx - 1 + this.items.length) % this.items.length },
next() { this.idx = (this.idx + 1) % this.items.length } next() { this.idx = (this.idx + 1) % this.items.length }
}" }"
@keydown.escape.window="open && close()" @keydown.escape.window="open && close()"
@keydown.arrow-left.window="open && prev()" @keydown.arrow-left.window="open && prev()"
@keydown.arrow-right.window="open && next()" @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">
@@ -174,6 +176,15 @@
<div class="absolute top-5 left-1/2 -translate-x-1/2 label text-fog tabular-nums" <div class="absolute top-5 left-1/2 -translate-x-1/2 label text-fog tabular-nums"
x-text="`${String(idx + 1).padStart(2,'0')} / ${String(items.length).padStart(2,'0')}`"></div> x-text="`${String(idx + 1).padStart(2,'0')} / ${String(items.length).padStart(2,'0')}`"></div>
<!-- Fill / Fit toggle -->
<button
@click="fill = !fill"
class="absolute top-3 right-16 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>
<!-- Close --> <!-- Close -->
<button <button
@click="close()" @click="close()"
@@ -190,14 +201,18 @@
>{{ partial "icon.html" "arrow-left" }}</button> >{{ partial "icon.html" "arrow-left" }}</button>
<!-- Media --> <!-- Media -->
<div class="w-full h-full flex items-center justify-center px-14 md:px-20 py-14"> <div
class="w-full h-full flex items-center justify-center transition-all duration-300"
:class="fill ? 'p-0' : 'px-14 md:px-20 py-14'"
>
<!-- Video --> <!-- Video -->
<video <video
x-show="items[idx] && items[idx].video" x-show="items[idx] && items[idx].video"
:src="items[idx] && items[idx].video ? items[idx].video : ''" :src="items[idx] && items[idx].video ? items[idx].video : ''"
:poster="items[idx] ? items[idx].img : ''" :poster="items[idx] ? items[idx].img : ''"
x-effect="if (open && items[idx] && items[idx].video) { $el.load(); $el.play() }" x-effect="if (open && items[idx] && items[idx].video) { $el.load(); $el.play() }"
class="max-w-full max-h-full object-contain" :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 controls loop muted playsinline
></video> ></video>
<!-- Image --> <!-- Image -->
@@ -205,7 +220,8 @@
x-show="items[idx] && !items[idx].video" x-show="items[idx] && !items[idx].video"
:src="items[idx] && !items[idx].video ? items[idx].img : ''" :src="items[idx] && !items[idx].video ? items[idx].img : ''"
alt="" alt=""
class="max-w-full max-h-full object-contain" :class="fill ? 'w-full h-full object-cover' : 'max-w-full max-h-full object-contain'"
class="transition-all duration-300"
> >
</div> </div>