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 %>
-