From c00914b9c84b1d72ca55bcf7a86ed2f1d2aed4f5 Mon Sep 17 00:00:00 2001 From: Andy Smith Date: Wed, 30 Jul 2025 16:15:33 -0500 Subject: [PATCH 1/9] Hoist up dialog variable --- app/assets/stylesheets/_global.css | 3 +++ app/assets/stylesheets/dialog.css | 16 ++++------------ app/assets/stylesheets/lightbox.css | 6 ------ app/assets/stylesheets/popup.css | 16 ++++------------ 4 files changed, 11 insertions(+), 30 deletions(-) diff --git a/app/assets/stylesheets/_global.css b/app/assets/stylesheets/_global.css index 1b2b9ba36..63a99ab94 100644 --- a/app/assets/stylesheets/_global.css +++ b/app/assets/stylesheets/_global.css @@ -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); } diff --git a/app/assets/stylesheets/dialog.css b/app/assets/stylesheets/dialog.css index 40158b06a..5d6a7eb0e 100644 --- a/app/assets/stylesheets/dialog.css +++ b/app/assets/stylesheets/dialog.css @@ -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] { diff --git a/app/assets/stylesheets/lightbox.css b/app/assets/stylesheets/lightbox.css index a8e41db2b..5dc10d7dc 100644 --- a/app/assets/stylesheets/lightbox.css +++ b/app/assets/stylesheets/lightbox.css @@ -1,7 +1,5 @@ @layer components { .lightbox { - --backdrop-speed: 150ms; - background-color: oklch(var(--lch-canvas) / 0.66); block-size: 100dvh; border: 0; @@ -18,10 +16,6 @@ backdrop-filter: blur(66px); background-color: var(--color-ink); opacity: 0; - transition: - display var(--backdrop-speed) allow-discrete, - opacity var(--backdrop-speed), - overlay var(--backdrop-speed) allow-discrete; } &[open] { diff --git a/app/assets/stylesheets/popup.css b/app/assets/stylesheets/popup.css index 2fbb56659..c1bb3dc61 100644 --- a/app/assets/stylesheets/popup.css +++ b/app/assets/stylesheets/popup.css @@ -149,26 +149,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] { From 3f7f414c34d96fb0c22b1432ad2a801f6162cc3d Mon Sep 17 00:00:00 2001 From: Andy Smith Date: Wed, 30 Jul 2025 17:26:05 -0500 Subject: [PATCH 2/9] Animate the lightbox --- app/assets/stylesheets/lightbox.css | 36 +++++++++++-------- app/assets/stylesheets/popup.css | 2 +- .../controllers/lightbox_controller.js | 14 +++++++- 3 files changed, 36 insertions(+), 16 deletions(-) diff --git a/app/assets/stylesheets/lightbox.css b/app/assets/stylesheets/lightbox.css index 5dc10d7dc..ee4c1b2eb 100644 --- a/app/assets/stylesheets/lightbox.css +++ b/app/assets/stylesheets/lightbox.css @@ -1,8 +1,11 @@ @layer components { .lightbox { - background-color: oklch(var(--lch-canvas) / 0.66); + --dialog-duration: 250ms; + + background-color: transparent; block-size: 100dvh; border: 0; + display: grid; inline-size: 100dvw; inset: 0; margin: auto; @@ -10,29 +13,34 @@ max-width: unset; overflow: hidden; padding: var(--block-space-half) var(--inline-space); + place-items: 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-canvas) / 50%); + } + + /* Default transition state */ + &, &::backdrop, img { opacity: 0; + transition: var(--dialog-duration) allow-discrete; + transition-property: display, opacity, overlay, transform; + } + + img { + transform: translateY(10dvh); } &[open] { - display: grid; - opacity: 1; - place-items: center; - - &::backdrop { - opacity: 0.75; + &, &::backdrop, img { + opacity: 1; + transform: translateY(0); } } @starting-style { - &[open] { - opacity: 0; - } - + &[open], &[open]::backdrop { opacity: 0; } diff --git a/app/assets/stylesheets/popup.css b/app/assets/stylesheets/popup.css index c1bb3dc61..33072b65d 100644 --- a/app/assets/stylesheets/popup.css +++ b/app/assets/stylesheets/popup.css @@ -153,7 +153,7 @@ transform: scale(0.2) translateX(-50%); transform-origin: top left; transition: var(--dialog-duration) allow-discrete; - transition-property: display, opacity, overlay transform; + transition-property: display, opacity, overlay, transform; &::backdrop { background-color: var(--color-always-black); diff --git a/app/javascript/controllers/lightbox_controller.js b/app/javascript/controllers/lightbox_controller.js index c2a541a13..70cb61d38 100644 --- a/app/javascript/controllers/lightbox_controller.js +++ b/app/javascript/controllers/lightbox_controller.js @@ -3,13 +3,25 @@ import { Controller } from "@hotwired/stimulus" export default class extends Controller { static targets = [ "image", "dialog", "zoomedImage" ] + connect() { + this.dialogTarget.addEventListener('transitionend', this.onTransitionEnd.bind(this)) + } + + // Only remove the image src after the CSS transition has finished + onTransitionEnd(event) { + if (this._waitingForTransition && !this.dialogTarget.open) { + this._waitingForTransition = false + this.zoomedImageTarget.src = "" + } + } + open(event) { this.dialogTarget.showModal() this.#set(event.target.closest("a")) } reset() { - this.zoomedImageTarget.src = "" + this._waitingForTransition = true } #set(target) { From 58667e5b2d0a6ab708f009f1db8d88409dbccbc7 Mon Sep 17 00:00:00 2001 From: Andy Smith Date: Wed, 30 Jul 2025 17:27:10 -0500 Subject: [PATCH 3/9] Adjust timing --- app/assets/stylesheets/lightbox.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/assets/stylesheets/lightbox.css b/app/assets/stylesheets/lightbox.css index ee4c1b2eb..e60307489 100644 --- a/app/assets/stylesheets/lightbox.css +++ b/app/assets/stylesheets/lightbox.css @@ -1,6 +1,6 @@ @layer components { .lightbox { - --dialog-duration: 250ms; + --dialog-duration: 350ms; background-color: transparent; block-size: 100dvh; From 514dbc536b824476cc40ed558f20bbdb3846831b Mon Sep 17 00:00:00 2001 From: Andy Smith Date: Wed, 30 Jul 2025 17:30:56 -0500 Subject: [PATCH 4/9] Fix lightbox initial display --- app/assets/stylesheets/lightbox.css | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/assets/stylesheets/lightbox.css b/app/assets/stylesheets/lightbox.css index e60307489..e90bfd819 100644 --- a/app/assets/stylesheets/lightbox.css +++ b/app/assets/stylesheets/lightbox.css @@ -5,7 +5,6 @@ background-color: transparent; block-size: 100dvh; border: 0; - display: grid; inline-size: 100dvw; inset: 0; margin: auto; @@ -33,6 +32,8 @@ } &[open] { + display: grid; + &, &::backdrop, img { opacity: 1; transform: translateY(0); From 4c505aeb23cdb00ad6098387f0c6145151255978 Mon Sep 17 00:00:00 2001 From: Andy Smith Date: Thu, 31 Jul 2025 15:34:27 -0500 Subject: [PATCH 5/9] Get all transitions working properly --- app/assets/stylesheets/animation.css | 10 ++++ app/assets/stylesheets/lightbox.css | 51 ++++++++++--------- .../controllers/lightbox_controller.js | 20 +++++--- app/views/layouts/_lightbox.html.erb | 2 +- 4 files changed, 51 insertions(+), 32 deletions(-) diff --git a/app/assets/stylesheets/animation.css b/app/assets/stylesheets/animation.css index a6275ded4..5a90f679c 100644 --- a/app/assets/stylesheets/animation.css +++ b/app/assets/stylesheets/animation.css @@ -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; } diff --git a/app/assets/stylesheets/lightbox.css b/app/assets/stylesheets/lightbox.css index e90bfd819..a5e5cec2d 100644 --- a/app/assets/stylesheets/lightbox.css +++ b/app/assets/stylesheets/lightbox.css @@ -12,46 +12,46 @@ max-width: unset; overflow: hidden; padding: var(--block-space-half) var(--inline-space); - place-items: center; &::backdrop { -webkit-backdrop-filter: blur(16px); backdrop-filter: blur(16px); - background-color: oklch(var(--lch-canvas) / 50%); - } - - /* Default transition state */ - &, &::backdrop, img { - opacity: 0; - transition: var(--dialog-duration) allow-discrete; - transition-property: display, opacity, overlay, transform; + background-color: oklch(var(--lch-black) / 50%); } img { - transform: translateY(10dvh); + animation: slide-down var(--dialog-duration); + animation-fill-mode: forwards; + display: block; + margin: auto; } - &[open] { - display: grid; - - &, &::backdrop, img { - opacity: 1; - transform: translateY(0); - } + /* Closed state */ + &, + &::backdrop { + opacity: 0; + transition: var(--dialog-duration) allow-discrete; + transition-property: display, opacity, overlay; } - @starting-style { - &[open], - &[open]::backdrop { + /* Open state */ + &[open], + &[open]::backdrop { + opacity: 1; + + @starting-style { opacity: 0; } + + img { + animation: slide-up var(--dialog-duration); + } } } .lightbox__btn { - align-self: start; - grid-area: 1/1; - justify-self: end; + inset: var(--block-space-half) var(--inline-space) auto auto; + position: absolute; } .lightbox__image { @@ -59,4 +59,9 @@ max-inline-size: calc(100dvw - (var(--inline-space) * 2)); max-block-size: calc(100dvh - (var(--block-space) * 2)); } + + /* Prevent body from scrolling when lightbox is open */ + html:has(.lightbox[open]) { + overflow: clip; + } } diff --git a/app/javascript/controllers/lightbox_controller.js b/app/javascript/controllers/lightbox_controller.js index 70cb61d38..9032bcb02 100644 --- a/app/javascript/controllers/lightbox_controller.js +++ b/app/javascript/controllers/lightbox_controller.js @@ -4,15 +4,11 @@ export default class extends Controller { static targets = [ "image", "dialog", "zoomedImage" ] connect() { - this.dialogTarget.addEventListener('transitionend', this.onTransitionEnd.bind(this)) + this.dialogTarget.addEventListener('transitionend', this.handleTransitionEnd.bind(this)) } - // Only remove the image src after the CSS transition has finished - onTransitionEnd(event) { - if (this._waitingForTransition && !this.dialogTarget.open) { - this._waitingForTransition = false - this.zoomedImageTarget.src = "" - } + disconnect() { + this.dialogTarget.removeEventListener('transitionend', this.handleTransitionEnd.bind(this)) } open(event) { @@ -20,8 +16,16 @@ export default class extends Controller { 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._waitingForTransition = true + this.zoomedImageTarget.src = "" + this.dispatch('closed') } #set(target) { diff --git a/app/views/layouts/_lightbox.html.erb b/app/views/layouts/_lightbox.html.erb index ac30fc601..085b1c10e 100644 --- a/app/views/layouts/_lightbox.html.erb +++ b/app/views/layouts/_lightbox.html.erb @@ -1,5 +1,5 @@ <%= 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 %> + data: { controller: "dialog", dialog_target: "dialog", dialog_modal_value: "true", lightbox_target: "dialog" } do %> + +<%= render "layouts/lightbox" do %> + <% if @card.image.attached? %> + <%= button_to card_image_path(@card), method: :delete, class: "btn" do %> + <%= icon_tag "trash" %> + Remove background image + <% end %> + <% end %> +<% end %>