release: v0.19.0
Release v0.19.0. See the `## [0.19.0] — 2026-09-29` section of [CHANGELOG.md](https://github.com/ralksta/immich-folio/blob/dev/CHANGELOG.md) for the full list. Highlights: client proofing links (#683), custom content pages (#738), asset picker search (#740), concurrent-edit detection (#742), env-locked settings (#739), an accessibility baseline (#741), three security fixes with advisories (#737, #755, #759), and a pre-release quality pass (#780–#797). --------- Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: ralksta <ralksta@users.noreply.github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Claude Opus 5 <noreply@anthropic.com> Co-authored-by: Ralf <rohlenbo79@gmail.com> Co-authored-by: Paul Thies <110185070+ImScheinox@users.noreply.github.com> Co-authored-by: lancetm714 <108419425+lancetm714@users.noreply.github.com> Co-authored-by: Lance Martin <lancetaylor95@gmail.com>
This commit is contained in:
290
app/globals.css
290
app/globals.css
@@ -104,18 +104,31 @@
|
||||
border-bottom-color: var(--text-primary);
|
||||
}
|
||||
|
||||
/* The links (Home, subpages, standalone albums, About, Map) live inside
|
||||
MobileNav's wrapper, but `display: contents` erases the wrapper's own box
|
||||
above the mobile breakpoint — the links become direct flex items of
|
||||
`.header__nav`, so desktop keeps the exact layout it always had. */
|
||||
.header__nav-links {
|
||||
display: contents;
|
||||
}
|
||||
/* ── Header menu (MobileNav) ──────────────────────────
|
||||
The links (Home, subpages, standalone albums, About, Map) live inside
|
||||
MobileNav's wrapper. The menu — a button plus a dropdown panel — is the
|
||||
default here, and the inline row is the exception further down: above the
|
||||
phone breakpoint, and only while MobileNav has not marked the links
|
||||
`data-collapsed` because they do not fit (#590).
|
||||
|
||||
/* Hidden above the mobile breakpoint — MobileNav renders it unconditionally,
|
||||
but only the CSS decides when it is reachable, same as `.header__wordmark`. */
|
||||
The header used to rely on the breakpoint alone and scroll sideways with a
|
||||
hidden scrollbar between 641px and whatever width the links needed, which
|
||||
put About, Map and the theme toggle off-screen on every tablet. How many
|
||||
links fit depends on the site's own subpages, so the width is measured
|
||||
instead of guessed. */
|
||||
.header__menu-btn {
|
||||
display: none;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
/* 44px touch target (20px icon + 12px padding); the negative margins
|
||||
keep the header's height and spacing as before (#696). */
|
||||
margin: -4px -4px -4px auto;
|
||||
align-self: center;
|
||||
padding: 12px;
|
||||
background: none;
|
||||
border: none;
|
||||
color: var(--text-secondary);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.header__menu-btn svg {
|
||||
@@ -124,6 +137,68 @@
|
||||
display: block;
|
||||
}
|
||||
|
||||
.header__nav-links {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* `.header` is the nearest positioned ancestor (it's `position: fixed`),
|
||||
so `top: 100%` sits the panel right under it regardless of the actual
|
||||
header height, which differs by preset (plain padding vs. studio-modern's
|
||||
fixed `--header-height`). `left: 50%` + `width: 100vw` + negative margin
|
||||
breaks the panel out to the full viewport width regardless of `.header`'s
|
||||
own horizontal padding, which also differs by preset (`--sm-gutter` for
|
||||
studio-modern vs. a fixed 24px elsewhere). */
|
||||
.header__nav-links[data-open='true'] {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
left: 50%;
|
||||
width: 100vw;
|
||||
margin-left: -50vw;
|
||||
max-height: calc(100vh - 64px);
|
||||
overflow-y: auto;
|
||||
background: rgba(26, 26, 26, 0.98);
|
||||
border-bottom: 1px solid var(--border-subtle);
|
||||
z-index: 101;
|
||||
}
|
||||
|
||||
[data-theme='light'] .header__nav-links[data-open='true'] {
|
||||
background: rgba(245, 245, 240, 0.98);
|
||||
}
|
||||
|
||||
.header__nav-links[data-open='true'] .header__nav-link,
|
||||
.header__nav-links[data-open='true'] .header__nav-link--external {
|
||||
width: 100%;
|
||||
padding: 16px 24px;
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
/* Covers the full viewport, including the area behind the header — the
|
||||
header's own `z-index: 100` keeps it crisp above the dim, so this does
|
||||
not need to know the header's height either. */
|
||||
.header__nav-backdrop {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background: rgba(0, 0, 0, 0.4);
|
||||
z-index: 99;
|
||||
}
|
||||
|
||||
/* The inline row. `display: contents` erases the wrapper's own box, so the
|
||||
links become direct flex items of `.header__nav` and desktop keeps the
|
||||
exact layout it always had. The breakpoint is the CSS half — it holds
|
||||
before hydration, so a phone never paints the row first — and it has to
|
||||
match PHONE_QUERY in MobileNav. */
|
||||
@media (min-width: 641px) {
|
||||
.header__menu-btn:not([data-collapsed='true']) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.header__nav-links:not([data-collapsed='true']) {
|
||||
display: contents;
|
||||
}
|
||||
}
|
||||
|
||||
/* ── Main Content ─────────────────────────────────── */
|
||||
.main {
|
||||
max-width: var(--max-width);
|
||||
@@ -153,6 +228,27 @@
|
||||
}
|
||||
}
|
||||
|
||||
/* Reduced motion, site-wide — visitor pages, admin and /install alike (#696).
|
||||
*
|
||||
* Durations collapse to near zero instead of `none`, so every animation and
|
||||
* transition still lands on its end state (a drawer still opens, a toast still
|
||||
* shows) and animationend/transitionend still fire for code that waits on
|
||||
* them. Looping animations — the skeleton shimmer, spinners — run once and
|
||||
* stop; the element itself stays visible. !important is needed to win over
|
||||
* component styles and inline `style` animations (MapView's spinner). */
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
animation-duration: 0.01ms !important;
|
||||
animation-iteration-count: 1 !important;
|
||||
animation-delay: 0ms !important;
|
||||
transition-duration: 0.01ms !important;
|
||||
transition-delay: 0ms !important;
|
||||
scroll-behavior: auto !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* Homepage hero + About page use full bleed — override main padding */
|
||||
.main:has(.hero),
|
||||
.main:has(.about) {
|
||||
@@ -257,6 +353,39 @@
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
/* Pause/play for the crossfade (WCAG 2.2.2). Sits over the photo, so it
|
||||
takes the theme's background at partial opacity rather than a fixed
|
||||
black; the 44px box is the touch target. */
|
||||
.hero__carousel-toggle {
|
||||
position: absolute;
|
||||
right: 16px;
|
||||
bottom: 16px;
|
||||
z-index: 4;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
padding: 0;
|
||||
border: 1px solid var(--border-subtle);
|
||||
border-radius: 50%;
|
||||
background: color-mix(in srgb, var(--bg-primary) 70%, transparent);
|
||||
color: var(--text-primary);
|
||||
cursor: pointer;
|
||||
opacity: 0.75;
|
||||
transition: opacity var(--transition-fast);
|
||||
}
|
||||
|
||||
.hero__carousel-toggle:hover,
|
||||
.hero__carousel-toggle:focus-visible {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.hero__carousel-toggle svg {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
|
||||
/* Camera chip over the hero image — opt-in per preset. */
|
||||
.hero__exif-chip {
|
||||
display: none;
|
||||
@@ -372,7 +501,10 @@
|
||||
|
||||
.hero--fullbleed .hero__title {
|
||||
color: #fff;
|
||||
font-size: 4rem;
|
||||
/* Scales down on a phone instead of running off the edge; 4rem was cut off
|
||||
at 390px in minimal and noir, whose single long words cannot wrap. */
|
||||
font-size: clamp(2.25rem, 9vw, 4rem);
|
||||
overflow-wrap: break-word;
|
||||
text-shadow:
|
||||
0 2px 8px rgba(0, 0, 0, 0.7),
|
||||
0 8px 48px rgba(0, 0, 0, 0.6);
|
||||
@@ -1115,11 +1247,19 @@
|
||||
}
|
||||
}
|
||||
|
||||
.album-header__main {
|
||||
/* A flex child defaults to min-width: auto, so one long word in the title
|
||||
would widen the header past the viewport on phones. */
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.album-header__title {
|
||||
font-family: var(--font-serif);
|
||||
font-size: 2.5rem;
|
||||
font-weight: 400;
|
||||
letter-spacing: 0.02em;
|
||||
hyphens: auto;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.album-header__description {
|
||||
@@ -1131,6 +1271,31 @@
|
||||
max-width: 640px;
|
||||
}
|
||||
|
||||
/* Client proofing link (/proof/<token>): greeting and notes under the title. */
|
||||
.proof-session-intro {
|
||||
margin-top: 16px;
|
||||
max-width: 640px;
|
||||
}
|
||||
|
||||
.proof-session-intro__greeting {
|
||||
font-size: 1rem;
|
||||
font-weight: 500;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.proof-session-intro__text {
|
||||
margin-top: 6px;
|
||||
font-size: 0.95rem;
|
||||
line-height: 1.6;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.proof-session-intro__meta {
|
||||
margin-top: 6px;
|
||||
font-size: 0.85rem;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.album-header__meta {
|
||||
margin-top: 8px;
|
||||
font-size: 0.9rem;
|
||||
@@ -1372,6 +1537,24 @@
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
/* Hover-only overlays leave keyboard and touch visitors out (#696). A tile
|
||||
with keyboard focus shows its overlay like a hovered one, and on devices
|
||||
without hover — phones, and tablets wider than the 640px mobile rules — the
|
||||
overlays are simply always shown. */
|
||||
.photo-grid__item:is(:focus-visible, :has(:focus-visible)) .photo-grid__item-exif,
|
||||
.subpage-grid__item:is(:focus-visible, :has(:focus-visible)) .subpage-grid__item-overlay,
|
||||
.album-card:is(:focus-visible, :has(:focus-visible)) .album-card__overlay {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
@media (hover: none) {
|
||||
.photo-grid__item .photo-grid__item-exif,
|
||||
.subpage-grid__item .subpage-grid__item-overlay,
|
||||
.album-card .album-card__overlay {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.photo-grid__item-play {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
@@ -1452,8 +1635,9 @@
|
||||
bottom: 32px;
|
||||
right: 32px;
|
||||
z-index: 90;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
/* 44px: the minimum touch target, same as the proofing hearts (#696). */
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
border-radius: 50%;
|
||||
border: 1px solid var(--border-subtle);
|
||||
background: var(--bg-card);
|
||||
@@ -1511,14 +1695,17 @@
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 8px;
|
||||
/* 44px touch target (16px icon + 14px padding). The negative block
|
||||
margins keep the header from growing; no negative inline margin, which
|
||||
pushed the button past the viewport edge on phones (#696). */
|
||||
padding: 14px;
|
||||
margin: -6px 0 -6px auto;
|
||||
background: none;
|
||||
border: none;
|
||||
color: var(--text-secondary);
|
||||
cursor: pointer;
|
||||
border-radius: var(--radius-sm);
|
||||
transition: color var(--transition-fast);
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.theme-toggle:hover {
|
||||
@@ -1543,8 +1730,14 @@
|
||||
|
||||
.footer__left {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: 20px;
|
||||
gap: 12px 20px;
|
||||
/* Without wrapping, the name and three legal links in uppercase tracking
|
||||
ran to about 440px on a phone and widened the page itself: the layout
|
||||
viewport grew with it, and the fixed lightbox and menu panel grew along,
|
||||
cutting off the close button and the next arrow. */
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.footer__name {
|
||||
@@ -1935,69 +2128,10 @@
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
/* The header is about 37px tall here (the 44px menu button with its
|
||||
negative margins); 32px left the album back link partly under it. */
|
||||
.main {
|
||||
padding: 32px 20px 80px;
|
||||
}
|
||||
|
||||
/* A proper menu replaces the horizontal scroll below this width — see
|
||||
`.header__nav-links` / `.header__menu-btn` above and MobileNav (#590). */
|
||||
.header__menu-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-left: auto;
|
||||
padding: 8px;
|
||||
background: none;
|
||||
border: none;
|
||||
color: var(--text-secondary);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.header__nav-links {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* `.header` is the nearest positioned ancestor (it's `position: fixed`),
|
||||
so `top: 100%` sits the panel right under it regardless of the actual
|
||||
header height, which differs by preset (plain padding vs. studio-modern's
|
||||
fixed `--header-height`). `left: 50%` + `width: 100vw` + negative margin
|
||||
breaks the panel out to the full viewport width regardless of `.header`'s
|
||||
own horizontal padding, which also differs by preset (`--sm-gutter` for
|
||||
studio-modern vs. a fixed 24px elsewhere). */
|
||||
.header__nav-links[data-open='true'] {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
left: 50%;
|
||||
width: 100vw;
|
||||
margin-left: -50vw;
|
||||
max-height: calc(100vh - 64px);
|
||||
overflow-y: auto;
|
||||
background: rgba(26, 26, 26, 0.98);
|
||||
border-bottom: 1px solid var(--border-subtle);
|
||||
z-index: 101;
|
||||
}
|
||||
|
||||
[data-theme='light'] .header__nav-links[data-open='true'] {
|
||||
background: rgba(245, 245, 240, 0.98);
|
||||
}
|
||||
|
||||
.header__nav-links[data-open='true'] .header__nav-link,
|
||||
.header__nav-links[data-open='true'] .header__nav-link--external {
|
||||
width: 100%;
|
||||
padding: 16px 24px;
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
/* Covers the full viewport, including the area behind the header — the
|
||||
header's own `z-index: 100` keeps it crisp above the dim, so this does
|
||||
not need to know the header's height either. */
|
||||
.header__nav-backdrop {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background: rgba(0, 0, 0, 0.4);
|
||||
z-index: 99;
|
||||
padding: 56px 20px 80px;
|
||||
}
|
||||
|
||||
.header__nav-link {
|
||||
|
||||
Reference in New Issue
Block a user