diff --git a/app/assets/stylesheets/_global.css b/app/assets/stylesheets/_global.css index 583535f9d..cf8d1a387 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/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/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..4a9cb3d8b 100644 --- a/app/assets/stylesheets/lightbox.css +++ b/app/assets/stylesheets/lightbox.css @@ -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; } } diff --git a/app/assets/stylesheets/popup.css b/app/assets/stylesheets/popup.css index dea826395..ded257988 100644 --- a/app/assets/stylesheets/popup.css +++ b/app/assets/stylesheets/popup.css @@ -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] { diff --git a/app/javascript/controllers/lightbox_controller.js b/app/javascript/controllers/lightbox_controller.js index c2a541a13..b39566b44 100644 --- a/app/javascript/controllers/lightbox_controller.js +++ b/app/javascript/controllers/lightbox_controller.js @@ -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 + } } } diff --git a/app/views/cards/display/perma/_background.html.erb b/app/views/cards/display/perma/_background.html.erb index d8c3203a8..c591b6238 100644 --- a/app/views/cards/display/perma/_background.html.erb +++ b/app/views/cards/display/perma/_background.html.erb @@ -4,4 +4,3 @@ Zoom background image <% end %> <% end %> - \ No newline at end of file diff --git a/app/views/cards/show.html.erb b/app/views/cards/show.html.erb index a203b4bb2..f887a421b 100644 --- a/app/views/cards/show.html.erb +++ b/app/views/cards/show.html.erb @@ -49,3 +49,12 @@ <%= render "cards/messages", card: @card %> <% end %> + +<%= 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 %> diff --git a/app/views/layouts/_lightbox.html.erb b/app/views/layouts/_lightbox.html.erb index ac30fc601..2f2c34c86 100644 --- a/app/views/layouts/_lightbox.html.erb +++ b/app/views/layouts/_lightbox.html.erb @@ -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 %> - + data: { controller: "dialog", dialog_target: "dialog", dialog_modal_value: "true", lightbox_target: "dialog" } do %> -