release: v0.13.0 (#535)

Twenty-nine commits since v0.12.0. See CHANGELOG.md for the full list
and the upgrade notes.

Ten additions — the SEO package (site URL, robots.txt, sitemap.xml,
JSON-LD), slideshow mode, opt-in original downloads, copy-link,
previous/next album navigation, alt text from the Immich description,
per-album location precision, the update notice, a Help tab, and a
subpage cover grid separate from its photo grid. Two fixes: CJK album
slugs, and the lightbox control bar.

Photo of the Day (#476) was built on `dev` and reverted as not planned;
it never appears in a release.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

https://claude.ai/code/session_01DmsHVRL9yQvckUB1ZpVWre

---------

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>
This commit is contained in:
Ralksta
2026-08-25 10:08:12 +02:00
committed by GitHub
parent 10ec68f493
commit a813433891
73 changed files with 4697 additions and 453 deletions

View File

@@ -65,6 +65,23 @@
transform: scale(1);
}
/*
* The photograph enters with a fade and a slight scale. Under a slideshow
* that repeats every few seconds, which is exactly the motion this preference
* is about — so it simply appears instead. It still fades in from nothing, so
* a half-loaded image is never shown.
*/
@media (prefers-reduced-motion: reduce) {
.image {
transform: none;
transition: none;
}
.imageLoaded {
transform: none;
}
}
.close {
position: fixed;
top: 20px;
@@ -127,11 +144,51 @@
height: 20px;
}
.counter {
/* ── Bottom bar ───────────────────────────────────── */
/*
* One row rather than five separately anchored controls.
*
* Each button used to carry its own fixed offset, which collided in two ways:
* the info button widens when its label becomes "Hide info" and grew into the
* favourite button beside it, and the left-hand controls ran into the centred
* counter once the viewport was narrow enough. A flex row cannot collide with
* itself, and a control that is conditionally absent now leaves no hole.
*/
.bottomBar {
position: fixed;
bottom: 24px;
left: 50%;
transform: translateX(-50%);
left: 0;
right: 0;
bottom: 0;
z-index: 1001;
display: flex;
align-items: center;
gap: 12px;
padding: 16px 24px;
/* The bar spans the viewport, so it must not swallow clicks on the photo. */
pointer-events: none;
}
.bottomBarGroup {
display: flex;
align-items: center;
gap: 8px;
/* Equal flex on both sides is what keeps the counter centred. */
flex: 1 1 0;
min-width: 0;
pointer-events: auto;
}
.bottomBarLeft {
justify-content: flex-start;
}
.bottomBarRight {
justify-content: flex-end;
}
.counter {
flex: 0 0 auto;
font-size: 0.85rem;
color: var(--text-muted);
font-weight: 300;
@@ -140,9 +197,6 @@
/* ── EXIF Info Panel ──────────────────────────────── */
.infoToggle {
position: fixed;
bottom: 24px;
right: 24px;
/* Icon and label sit on one line — the buttons carried an emoji before, which
needed no alignment of its own. */
display: inline-flex;
@@ -169,9 +223,71 @@
color: var(--text-primary);
}
/* Proofing favourite button — sits left of the info toggle. */
.favToggle {
right: 6.5rem;
/* Anchors are not buttons; strip the underline so it matches its neighbours. */
.downloadToggle {
text-decoration: none;
}
/* Shown only when the clipboard API is unavailable. */
.copyManual {
position: fixed;
bottom: 76px;
left: 24px;
z-index: 1001;
display: flex;
flex-direction: column;
gap: 6px;
max-width: min(28rem, calc(100vw - 48px));
padding: 12px;
background: rgba(255, 255, 255, 0.08);
backdrop-filter: blur(8px);
-webkit-backdrop-filter: blur(8px);
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: var(--radius-md);
}
.copyManualLabel {
font-family: var(--font-sans);
font-size: 0.75rem;
color: var(--text-muted);
}
.copyManualInput {
width: 100%;
padding: 6px 8px;
background: rgba(0, 0, 0, 0.35);
border: 1px solid rgba(255, 255, 255, 0.15);
border-radius: var(--radius-sm);
color: var(--text-primary);
font-family: var(--font-sans);
font-size: 0.8rem;
}
@media (max-width: 600px) {
/*
* Four controls, a counter and the close button do not fit on one line at
* phone width. Wrapping is the honest answer: the bottom edge already
* belongs to the EXIF strip in some presets, so there is nowhere to move to.
*/
.bottomBar {
padding: 12px 16px;
gap: 8px;
flex-wrap: wrap;
}
.bottomBarGroup {
flex: 0 1 auto;
}
/* Must not grow — a stretched counter pushes the row into the close button. */
.counter {
flex: 0 0 auto;
}
.copyManual {
left: 16px;
bottom: 66px;
}
}
.exifPanel {
@@ -187,7 +303,7 @@
--lb-panel-muted: var(--text-muted);
--lb-panel-border: var(--border-subtle);
position: fixed;
bottom: 60px;
bottom: 76px;
right: 24px;
z-index: 1001;
background: rgba(20, 20, 20, 0.92);
@@ -277,7 +393,7 @@
--lb-panel-muted: var(--text-muted);
--lb-panel-border: var(--border-subtle);
position: fixed;
bottom: 62px;
bottom: 76px;
left: 24px;
z-index: 1001;
animation: exif-slide-up var(--transition-normal) ease forwards;
@@ -463,11 +579,22 @@
}
/* Counter → top left, monospace */
:global([data-preset='studio-modern']) .counter {
top: 20px;
/*
* This preset puts the viewer's chrome along the top edge. That is now said
* once, on the bar, instead of by each control: when every button carried its
* own offset, a button that changed width walked into its neighbour.
*/
:global([data-preset='studio-modern']) .bottomBar {
top: 0;
bottom: auto;
left: 32px;
transform: none;
/* The close button owns the top-right corner, so the bar stops short of it.
That is the 76px the individual controls used to reserve by hand. */
padding: 20px 76px 20px 32px;
}
/* Counter first, so it keeps the left edge it had in this preset. */
:global([data-preset='studio-modern']) .counter {
order: -1;
font-family: var(--font-caption);
font-size: 0.6875rem;
letter-spacing: 0.15em;
@@ -520,9 +647,6 @@
/* Info + favourite toggles move up into the topbar */
:global([data-preset='studio-modern']) .infoToggle {
top: 20px;
bottom: auto;
right: 76px;
background: none;
backdrop-filter: none;
-webkit-backdrop-filter: none;
@@ -542,10 +666,6 @@
color: var(--accent);
}
:global([data-preset='studio-modern']) .favToggle {
right: 176px;
}
/* The EXIF strip owns the bottom edge in this preset, so the panel opens from
the top instead. */
:global([data-preset='studio-modern']) .shortcutsPanel {
@@ -638,12 +758,9 @@
}
@media (max-width: 640px) {
/* No room next to the info toggle — park it in the opposite corner. */
:global([data-preset='studio-modern']) .favToggle {
top: auto;
bottom: 76px;
right: auto;
left: 16px;
:global([data-preset='studio-modern']) .bottomBar {
/* Same reserved corner as on desktop: the close button sits there. */
padding: 16px 76px 16px 16px;
}
:global([data-preset='studio-modern']) .navPrev {