feat(ImageLightbox): morph to dialog via View Transitions, fix sizing and stacking

- Use the shared Modal for dialog mechanics; ImageLightbox keeps only the
  view-transition coordination (just-in-time view-transition-name handoff
  between thumb and dialog frame, ESC routed through onCancel).
- Switch to <Image> for both thumb and dialog image; thumb is priority/sizes-
  driven for LCP, dialog image is lazy-loaded (deferred until open).
- New brutal-outline utility for the dialog frame: outline paints after
  children so subpixel image bleed can't cover it; dialog gets overflow-
  visible so the outline isn't clipped.
- lightbox-image utility caps image dimensions to viewport minus close-button
  and footer headroom, with box-sizing: content-box.
- Lightbox dialog gets view-transition-name lightbox-dialog (z=20) and the
  frame gets lightbox-frame (z=30) so both stack cleanly above the page
  during the open/close transition. Footer drops its named VT group since
  section-body no longer slides over it.
- Cream-tinted backdrop replaces the blur (Firefox-friendly), color-mix
  with var(--cream) for the token reference.
- scrollbar-gutter: stable on html so locking body scroll doesn't shift
  the layout.
This commit is contained in:
Ilia Mashkov
2026-05-23 09:54:20 +03:00
parent ecbb76312b
commit cd59766f92
3 changed files with 215 additions and 48 deletions
+50 -8
View File
@@ -154,6 +154,9 @@
html {
font-size: var(--font-size);
scroll-behavior: smooth;
/* Reserve scrollbar gutter so locking body scroll (e.g. when a modal
* opens) doesn't widen the viewport and shift fixed elements. */
scrollbar-gutter: stable;
}
body {
@@ -272,6 +275,12 @@
@utility brutal-border-right {
border-right: var(--border-width) solid var(--blue);
}
/* Border drawn as an outline — painted after children, so an image's
* subpixel paint bleed can't cover it. Doesn't take layout space; the
* ancestor must not have overflow:hidden or the outline gets clipped. */
@utility brutal-outline {
outline: var(--border-width) solid var(--blue);
}
/* Apply Fraunces variable axes to non-heading elements using the heading font */
.font-wonk {
font-variation-settings:
@@ -413,9 +422,13 @@
}
}
/* Keep footer above sliding section-body during view transitions */
/* Page-load entry animation for the footer. Previously also carried
* `view-transition-name: site-footer` to layer above section-body's slide
* group, but the section-body group now has `animation: none` (purely
* horizontal slide of OLD/NEW snapshots), so the footer can live in the root
* snapshot during transitions — which also lets the lightbox dialog group
* stack cleanly above it. */
.footer-vt {
view-transition-name: site-footer;
animation: footer-enter var(--duration-slow) var(--ease-spring) both;
}
@@ -430,12 +443,41 @@
}
}
::view-transition-group(site-footer) {
z-index: 10;
/* Lightbox dialog backdrop — flat cream wash. No filter, no gradient.
* Cheapest possible; Firefox-friendly during view transitions. */
dialog.lightbox::backdrop {
background-color: color-mix(in srgb, var(--cream) 80%, transparent);
}
/* Lightbox dialog backdrop */
dialog.lightbox::backdrop {
background-color: rgba(4, 28, 243, 0.25);
backdrop-filter: blur(4px);
/* Give the lightbox dialog its own view-transition group so it can stack
* above the footer's named group (z=10) during the open/close transition.
* Closed dialogs have `display: none` (UA) and don't get snapshotted, so
* sharing the name across all `.lightbox` dialogs at rest is harmless —
* only the open one participates in any given transition. */
dialog.lightbox {
view-transition-name: lightbox-dialog;
}
::view-transition-group(lightbox-dialog) {
z-index: 20;
}
/* The image wrapper (and the thumb during the OLD snapshot of an open
* transition) shares this static name — imperatively assigned only during
* the morph, so there's never more than one element holding it at a time.
* z=30 keeps it above the dialog (z=20) and the footer (z=10) during VT. */
::view-transition-group(lightbox-frame) {
z-index: 30;
}
/* Lightbox image sizing — leaves vertical headroom for the fixed close button
* (sits at top-3, ~2.5rem tall). 8rem total = ~4rem top/bottom after centering,
* so the button never overlaps the image.
* box-sizing: content-box so max-w/max-h apply to the image pixels and the
* 3px brutal-border sits outside them — avoids subpixel clipping of the
* border by the dialog's overflow:hidden when both have border-box. */
@utility lightbox-image {
box-sizing: content-box;
max-width: calc(100vw - 2rem);
max-height: calc(100vh - 8rem);
}