Merge pull request #836 from basecamp/lightbox-improvements

Lightbox improvements
This commit is contained in:
Andy Smith
2025-08-05 09:59:37 -05:00
committed by GitHub
10 changed files with 120 additions and 61 deletions
+3
View File
@@ -53,6 +53,9 @@
--focus-ring-offset: 1px;
--focus-ring-size: 2px;
/* Dialogs */
--dialog-duration: 150ms;
@media (max-width: 799px) {
--tray-size: var(--footer-height);
}
+10
View File
@@ -39,6 +39,16 @@
75% { transform: translateX(1rem); }
}
@keyframes slide-up {
from { transform: translateY(2rem); }
to { transform: translateY(0); }
}
@keyframes slide-down {
from { transform: translateY(0); }
to { transform: translateY(2rem); }
}
@keyframes success {
0% { background-color: var(--color-border-darker); scale: 0.8; }
33% { background-color: var(--color-border-darker); scale: 1; }
+4 -12
View File
@@ -1,26 +1,18 @@
@layer components {
:is(.dialog) {
--backdrop-speed: 150ms;
--speed: 150ms;
border: 0;
opacity: 0;
transform: scale(0.2);
transform-origin: top center;
transition:
display var(--speed) allow-discrete,
opacity var(--speed),
overlay var(--speed) allow-discrete,
transform var(--speed);
transition: var(--dialog-duration) allow-discrete;
transition-property: display, opacity, overlay, transform;
&::backdrop {
background-color: var(--color-black);
opacity: 0;
transform: scale(1);
transition:
display var(--backdrop-speed) allow-discrete,
opacity var(--backdrop-speed),
overlay var(--backdrop-speed) allow-discrete;
transition: var(--dialog-duration) allow-discrete;
transition-property: display, opacity, overlay;
}
&[open] {
+56 -29
View File
@@ -1,8 +1,9 @@
@layer components {
.lightbox {
--backdrop-speed: 150ms;
--dialog-duration: 350ms;
--lightbox-padding: 3vmin;
background-color: oklch(var(--lch-canvas) / 0.66);
background-color: transparent;
block-size: 100dvh;
border: 0;
inline-size: 100dvw;
@@ -11,49 +12,75 @@
max-height: unset;
max-width: unset;
overflow: hidden;
padding: var(--block-space-half) var(--inline-space);
padding: var(--lightbox-padding);
text-align: center;
&::backdrop {
-webkit-backdrop-filter: blur(66px);
backdrop-filter: blur(66px);
background-color: var(--color-ink);
-webkit-backdrop-filter: blur(16px);
backdrop-filter: blur(16px);
background-color: oklch(var(--lch-black) / 50%);
}
/* Closed state */
&,
&::backdrop {
opacity: 0;
transition:
display var(--backdrop-speed) allow-discrete,
opacity var(--backdrop-speed),
overlay var(--backdrop-speed) allow-discrete;
transition: var(--dialog-duration) allow-discrete;
transition-property: display, opacity, overlay;
}
&[open] {
display: grid;
/* Open state */
&[open],
&[open]::backdrop {
opacity: 1;
place-items: center;
&::backdrop {
opacity: 0.75;
}
}
@starting-style {
&[open] {
@starting-style {
opacity: 0;
}
&[open]::backdrop {
opacity: 0;
.lightbox__figure {
animation: slide-up var(--dialog-duration);
}
}
}
.lightbox__btn {
align-self: start;
grid-area: 1/1;
justify-self: end;
.lightbox__actions {
display: flex;
gap: 1ch;
inset: var(--lightbox-padding) var(--lightbox-padding) auto auto;
position: absolute;
}
.lightbox__figure {
align-self: stretch;
animation-fill-mode: forwards;
animation: slide-down var(--dialog-duration);
display: flex;
flex-direction: column;
gap: var(--lightbox-padding);
margin: 0 auto;
max-block-size: 100%;
img {
object-fit: contain;
}
}
.lightbox__caption {
color: var(--color-ink-inverted);
&:empty {
display: none;
}
}
.lightbox__image {
grid-area: 1/1;
max-inline-size: calc(100dvw - (var(--inline-space) * 2));
max-block-size: calc(100dvh - (var(--block-space) * 2));
flex: 1;
min-block-size: 0;
}
/* Prevent body from scrolling when lightbox is open */
html:has(.lightbox[open]) {
overflow: clip;
}
}
+4 -12
View File
@@ -153,26 +153,18 @@
}
.popup--animated {
--backdrop-speed: 150ms;
--speed: 150ms;
opacity: 0;
transform: scale(0.2) translateX(-50%);
transform-origin: top left;
transition:
display var(--speed) allow-discrete,
opacity var(--speed),
overlay var(--speed) allow-discrete,
transform var(--speed);
transition: var(--dialog-duration) allow-discrete;
transition-property: display, opacity, overlay, transform;
&::backdrop {
background-color: var(--color-always-black);
opacity: 0;
transform: scale(1);
transition:
display var(--backdrop-speed) allow-discrete,
opacity var(--backdrop-speed),
overlay var(--backdrop-speed) allow-discrete;
transition: var(--dialog-duration) allow-discrete;
transition-property: display, opacity, overlay;
}
&[open] {
@@ -1,18 +1,42 @@
import { Controller } from "@hotwired/stimulus"
export default class extends Controller {
static targets = [ "image", "dialog", "zoomedImage" ]
static targets = [ "caption", "image", "dialog", "zoomedImage" ]
connect() {
this.dialogTarget.addEventListener('transitionend', this.handleTransitionEnd.bind(this))
}
disconnect() {
this.dialogTarget.removeEventListener('transitionend', this.handleTransitionEnd.bind(this))
}
open(event) {
this.dialogTarget.showModal()
this.#set(event.target.closest("a"))
}
// Wait for the transition to finish before resetting the image
handleTransitionEnd(event) {
if (event.target === this.dialogTarget && !this.dialogTarget.open) {
this.reset()
}
}
reset() {
this.zoomedImageTarget.src = ""
this.captionTarget.innerText = ""
this.dispatch('closed')
}
#set(target) {
this.zoomedImageTarget.src = target.href
const imageSrc = target.href
const caption = target.dataset.lightboxCaptionValue
this.zoomedImageTarget.src = imageSrc
if (caption) {
this.captionTarget.innerText = caption
}
}
}
@@ -4,4 +4,3 @@
<span class="for-screen-reader">Zoom background image</span>
<% end %>
<% end %>
+9
View File
@@ -49,3 +49,12 @@
<%= render "cards/messages", card: @card %>
<% end %>
</div>
<%= render "layouts/lightbox" do %>
<% if @card.image.attached? %>
<%= button_to card_image_path(@card), method: :delete, class: "btn" do %>
<%= icon_tag "trash" %>
<span class="for-screen-reader">Remove background image</span>
<% end %>
<% end %>
<% end %>
+8 -3
View File
@@ -1,8 +1,13 @@
<%= tag.dialog class:"lightbox", aria: { label: "Image Viewer (Press escape to close)" },
data: { controller: "dialog", dialog_target: "dialog", lightbox_target: "dialog", action: "close->lightbox#reset" } do %>
<img src="" class="lightbox__image" data-lightbox-target="zoomedImage" />
data: { controller: "dialog", dialog_target: "dialog", dialog_modal_value: "true", lightbox_target: "dialog" } do %>
<div class="lightbox__btn">
<figure class="lightbox__figure">
<img src="" class="lightbox__image" data-lightbox-target="zoomedImage" />
<figcaption class="lightbox__caption" data-lightbox-target="caption"></figcaption>
</figure>
<div class="lightbox__actions">
<%= yield %>
<button class="btn fill-white" title="Close (esc)" data-action="dialog#close keydown.esc->dialog#close:stop" data-controller="hotkey">
<%= icon_tag "remove" %>
<span class="for-screen-reader">Close image viewer (esc)</span>
-2
View File
@@ -31,7 +31,5 @@
</div>
<% end %>
</footer>
<%= render "layouts/lightbox" %>
</body>
</html>