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:
Ralksta
2026-09-29 22:50:30 +02:00
committed by GitHub
parent 5f251923f0
commit ad8b09700b
297 changed files with 19398 additions and 1800 deletions

View File

@@ -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 {