Skip to content

release: v0.22.0 - #894

Merged
ralksta merged 289 commits into
mainfrom
dev
Nov 4, 2026
Merged

ralksta merged 289 commits into
mainfrom
dev

Conversation

@ralksta

@ralksta ralksta commented Nov 4, 2026

Copy link
Copy Markdown
Owner

Release v0.22.0. See the [0.22.0] section of CHANGELOG.md: unlisted albums with custom links (#878), the ghcr.io image for amd64 and arm64 (#879), one vocabulary across the admin panel (#873), Next.js 16.4.3 (#891), zod/Prettier (#893), studio-modern.css cleanup (#881).

ralksta and others added 30 commits August 25, 2026 23:13
Closes #551.

## The bug

`findUnwritable()` asks whether *this process* can write;
`checkWritable()` phrased the answer as "Saving from the admin panel
will fail, and so will backups." In the panel that is exactly right — it
runs inside the app, as the app's user. In a CLI it is a different
question, and run from the host against a container that writes as uid
1001, it produced this on a deployment with nothing wrong with it:

```
ERRORS (1)
  x 1 content path is not writable
    Saving from the admin panel will fail, and so will backups. Check the
    ownership of the mounted volume: content/.backups
```

The worst part is the advice: chowning the mounted volume from the host
is the one change that would actually break the container's access.

## After

```
NOTES (1)
  i 1 content path is not writable by uid 1000 — but it is owned by uid 1001
    Checked as the user running this command, not as the user the app runs as.
    The paths belong to uid 1001, which can write them; the Docker image runs
    the app as uid 1001. This only matters if the app itself runs as uid 1000.
    To check as the app: docker compose exec folio npm run doctor. Paths:
    content/.backups

0 errors, 0 warnings, 0 checks passed, 1 note.
```

`findUnwritable()` returns each path's owner, and the level follows what
the answer is worth:

| Situation | Level |
| --- | --- |
| everything writable | `ok`, a normal passed check |
| unwritable, CLI runs as the owner | `error`, wording unchanged — this
one really is about the app |
| unwritable, owned by someone else | note: both uids named, `docker
compose exec` suggested |
| no `getuid()` (Windows) | note: claims nothing, and never prints
`undefined` |

The `id` stays `content-writable`, so the panel and the CLI still name
the same check.

## One design change from #550

Notes move from an id set to a `note` flag on the finding.
`content-writable` is a note in one case and a real error in the other,
and an id cannot express that — the id set would also have swallowed a
healthy `content/ is writable` into NOTES. The flag sits on a CLI-local
`CliFinding = DoctorFinding & { note?: boolean }` rather than on
`DoctorFinding`, which is shared with the panel where all of these
checks are real. A test pins that the flag, not the id, is what decides.

## Verification

- `npx tsc --noEmit` — 0 errors
- `npm run test:unit` — 863 passed, incl. 9 new (owner recorded by
`findUnwritable`, each of the four `reportWritable` outcomes, mixed
ownership, the stable id, and the flag-not-id rule)
- `npm run build` — passes
- `npm run lint` — unchanged from the baseline on `dev`
- Ran end to end: the error branch against a staged read-only directory
I own (`EXIT=2`, wording unchanged), the note branch rendered through
`formatReport`, and the healthy path against a real `content/`

`docs/deployment.md` now describes both reported-not-judged findings
together.

Co-authored-by: ralksta <ralksta@users.noreply.github.com>
The `[Unreleased]` section already carried the four bug fixes and the
smoke test. This adds the two entries that were missing and tidies the
structure, so the release commit only has to rename the heading.

## Added

- **`npm run doctor`**
([#521](#521)) — what it
checks, that it runs inside the shipped image, the exit-code mapping,
and the two findings that are reported rather than judged.
- The admin smoke test entry was already there.

## Changed

- **The form-field layer**
([#533](#533)) — framed as
what the panel costs to change rather than as a file listing, with the
two things deliberately left undone named so nobody has to re-derive
them from the issue.

## Structure

The section had grown **two `### Fixed` headings**. Now one each of
Added / Changed / Fixed, in Keep a Changelog's order. No existing entry
was reworded — the diff moves them, and a before/after comparison of the
entry list confirms only the two new ones were added and none were lost.

## What is deliberately absent

- **[#542](#542
(THEME_INFO used before initialization) — a regression of the field
layer, fixed before either shipped. In v0.13.0 the declaration sat at
line 108 and its use at 1034; `a3c6611` moved the use to 108 and left
the declaration at 370. No released version has this bug, so it is not
release notes. It is mentioned where it belongs: as the reason the admin
smoke test exists.
- **[#551](#551
(writability reported for the wrong user) — a fix to the doctor CLI that
this same release introduces. The corrected behaviour is described as
how the doctor works, not as a fix to something users had.

Verified with `npx prettier --check`.

Co-authored-by: ralksta <ralksta@users.noreply.github.com>
Closes the Unreleased section.

Thirteen commits since v0.13.0: the config doctor moved to the terminal,
the admin panel got a form-field layer under an unchanged surface, and a
Playwright spec now loads all fourteen admin routes in a real browser.

The documentation screenshots are still the v0.12.0 set, for the same
reason as last time — the panel renders client-side against a live
Immich instance, so `docs/screenshots/admin-*.png` cannot be recaptured
from a bare checkout. They have now been stale for two releases.
CodeQL flagged the admin smoke-test job added in #545 for running with
the default GITHUB_TOKEN permissions. The older job had the same gap and
was never flagged — an alert is raised for changed code, and that job
predates the scan.

Neither job writes anything back: they check out the code, build it and
report through the run. One top-level `permissions: contents: read`
covers both, so a third job cannot inherit a writable token by being
added without one.
main is a chain of one squash commit per release, each tree-identical to the dev commit it was made from. Because a squash leaves no parent link, the merge base of dev and main stays at the previous release, so every release PR re-presents the whole of the last release as a conflict: CHANGELOG.md, package.json, package-lock.json, admin.css and PageBuilder.tsx all changed on both sides.

This merge takes dev's tree unchanged, which loses nothing: main's tree already equals that of an ancestor of dev, and the diff from main to dev is byte-identical to the diff from that ancestor. What it does is move the merge base to main, so this release PR and the ones after it apply cleanly.
…ched (#558)

## Was das behebt

`/api/image/:token?size=original` hat die unveränderte Originaldatei aus
Immich durchgereicht. Das Asset-Token autorisiert aber **das Bild, nicht
die Größe** — ein aus dem öffentlichen HTML gelesenes Thumbnail-Token
plus `?size=original` lieferte damit das Original samt GPS-Koordinaten
an jeden nicht angemeldeten Besucher.

An der Live-Galerie nachgemessen:

| Fassung | Größe | EXIF |
|---|---|---|
| preview | 176 KB | kein EXIF-Block |
| original | 16 MB | GPSInfo-Tag + Kameramodell |

Das ging an zwei bewussten Schutzmaßnahmen vorbei: `app/api/exif/[id]`
gibt dem Browser nur `city`/`country`, und `/api/map` quantisiert
Koordinaten serverseitig.

## Warum genullt und nicht herausgeschnitten

Die Bilddaten bleiben bitgenau erhalten — **Neu-Encodieren kostet
Qualität und kam deshalb nicht infrage**. Nur die Metadaten-Bytes werden
mit Nullen überschrieben. Das hält außerdem die Dateilänge konstant,
womit `Content-Length` gültig bleibt und — bei AVIF entscheidend — die
absoluten Offsets in der `iloc`-Box weiter stimmen. Ein Item zu
entfernen hieße, sämtliche Offsets im Container neu zu berechnen.

Ein genulltes APPn-Segment ist für Decoder schlicht ein unbekanntes
Anwendungssegment und wird übersprungen.

## Umfang

**Entfernt:** JPEG APP1 (EXIF/XMP), APP13 (IPTC), COM · `Exif`- und
`mime`-Items in ISOBMFF-Containern (AVIF/HEIC/HEIF), gefunden über
`iinf`/`iloc`.

**Bewusst behalten:** JPEG APP0 (JFIF) und APP2 (ICC). Das Farbprofil zu
verwerfen würde die Farben sichtbar verschieben — das wäre eine
Darstellungs-, keine Datenschutzänderung.

**Nicht angefasst:** die Download-Route pro Album. Die ist sauber
abgesichert (Allowlist, `download: true`, Passwort-Gates,
Asset-Zugehörigkeit) und eine bewusste Opt-in-Funktion.

Ein Format, das wir nicht sicher parsen können, fällt jetzt auf die
Vorschau zurück, statt durchgereicht zu werden — genau dieses
stillschweigende Durchreichen war die Ursache.

## Tests

8 neue Unit-Tests in `lib/__tests__/metadataStrip.test.ts` gegen
synthetisch gebaute JPEG- und AVIF-Strukturen: GPS verschwindet, ICC und
JFIF bleiben, Bilddaten und Dateilänge sind Byte für Byte unverändert,
kaputte Container liefern `null`. Gesamte Suite: 871 Tests grün, `tsc`
und `eslint` sauber, `next build` läuft durch.

## ⚠️ Beim Ausrollen zu beachten

Diese URLs werden als `immutable` mit einem Jahr Cache ausgeliefert.
Bereits ausgelieferte Originale bleiben in Browser-Caches also erhalten,
auch nach dem Deploy. **`IMAGE_CACHE_VERSION` muss hochgezählt werden**,
weil das die URLs selbst ändert; das eingebaute `STRIP_VERSION` deckt
nur Caches ab, die revalidieren.

Ob ein bereits abgeflossenes Original bei Dritten liegt, lässt sich
naturgemäß nicht rückgängig machen.

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

https://claude.ai/code/session_01BkUJtcTjmKHPoUwxArAdGb

Co-authored-by: Ralf <rohlenbo79@gmail.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
assetAspectRatio() used the stored sensor dimensions as-is. Cameras that record a portrait as landscape pixels plus orientation 6 or 8 got a landscape tile for every portrait, which flattened masonry into a uniform grid. Orientations 5-8 now swap width and height.

Closes #565
…sory (#571)

- Raise --text-muted to 4.5:1 in every theme; move --text-secondary along in minimal and monograph.
- Give the admin panel a visible focus ring (WCAG 1.4.11) and remove a permanent outline: none.
- Replace nine undefined tokens that made the proofing modal and journal cards unreadable in the light theme.
- Add useModalDialog: Escape, focus in, focus trap and focus return for four dialogs.
- Give eight admin select elements an accessible name.
- Bump next and eslint-config-next to 16.3.4 (GHSA-p293-qw3h-jr36) and js-yaml to 4.3.2.
## Why

#572 by @ImScheinox fixed the masonry EXIF-orientation bug (#565) on 11
September. It was overlooked, and #576 fixed the same bug again on 19
September. The fix should land under the person who found it first.

## What

This PR reverts 9a87895 (#576). On its own it reintroduces the bug, so
**merge #572 immediately afterwards**. #572 conflicts with `dev` today
(both touch `lib/urls.ts` and `lib/immich.ts`). Once this revert is in,
it merges cleanly.

## The two fixes compared

Both swap width and height for orientations 5–8 and are tested against
the same Sony 7008×4672 frame. The only difference: #572 parses the flag
with `Number()`, so it also works if Immich ever sends the orientation
as a number rather than a string. #576 matches strings only.

## Verified

On `dev` + this revert + #572 merged locally:

- `npx vitest run lib/__tests__/urls.test.ts`: 29/29 pass
- `npx tsc --noEmit`: 0 errors

Refs #565, #572

Co-authored-by: ralksta <ralksta@users.noreply.github.com>
## Summary

`assetAspectRatio()` computes the ratio from `exifImageWidth /
exifImageHeight` without consulting the EXIF `orientation` flag. Most
cameras read the sensor out in landscape however the body is held and
record the quarter turn as a flag rather than rewriting the pixels, so a
portrait frame arrives as e.g. `7008x4672` with `orientation: "6"`.

The browser rotates the image correctly on display, but the layout has
already reserved a landscape tile. In `layout: masonry` with
`aspectRatio: auto`, every photo from such a camera is treated as
landscape and the grid loses its staggered look entirely.

First five assets of an affected album, via `/search/metadata` with
`withExif: true` — four landscape frames and one portrait frame,
identical stored dimensions, differing only in the flag:

```json
[{"f":"DSC00308.JPG","w":7008,"h":4672,"o":"1"},
 {"f":"DSC00307.JPG","w":7008,"h":4672,"o":"1"},
 {"f":"DSC00302.JPG","w":7008,"h":4672,"o":"1"},
 {"f":"DSC00295.JPG","w":7008,"h":4672,"o":"1"},
 {"f":"DSC01158.JPG","w":7008,"h":4672,"o":"6"}]
```

Immich's own web UI reports `DSC01158` as 4672 × 7008 because it applies
the flag. The API does not.

**Change:**

- `lib/urls.ts` — `assetAspectRatio()` swaps width and height for
orientations 5–8. Values 1–4 are upright or mirrored in place and leave
the ratio untouched. The flag arrives as a string, so it is parsed with
`Number()`; anything unparseable falls through as upright, which is the
previous behaviour.
- `lib/immich.ts` — `orientation` added to `ImmichExifInfo` as an
optional field. Immich only fills it when the file carries the tag, and
making it required would break every existing EXIF fixture.
- `lib/__tests__/urls.test.ts` — cases for all eight orientations plus
null and absent.

`assetAspectRatio()` is the single place dimensions become a ratio, so
this covers masonry, justified and editorial-flow at once. The uniform
grid is unaffected, as it does not use the natural ratio.

Verified against a live album of Sony ILCE-7M5 JPEGs mixing both
orientations: portrait frames now render as taller tiles.

## Type of change

- [x] 🐛 Bug fix
- [ ] ✨ New feature
- [ ] 🎨 Style / visual
- [ ] ♻️ Refactor
- [ ] 🔒 Security fix
- [ ] 📄 Documentation
- [ ] 🛠️ Build / CI

## Checklist

- [x] Targets the `dev` branch (see
[CONTRIBUTING.md](../CONTRIBUTING.md#branches))
- [x] `npm run build` passes locally
- [x] `npx tsc --noEmit` passes (0 type errors)
- [x] `npx vitest run` passes (all tests green)
- [x] No secrets / API keys in diff
- [ ] Documentation updated if needed (`docs/`, `README.md`)
- [x] Screenshots attached if there are visual changes

## Screenshots
<img width="1216" height="735" alt="image"
src="https://github.com/user-attachments/assets/6f823ef7-1d70-48ee-98c6-a5139a694b73"
/>

<img width="1172" height="837" alt="image"
src="https://github.com/user-attachments/assets/76eb2f32-50d8-48ba-af4c-d99076612b21"
/>
## Summary

Enables Markdown links in journal/essay **quote attributions**, per
#559.

The quote body and photo captions already go through
`renderInlineMarkdown`, so `[text](url)` becomes an anchor there — but
the attribution was the one inline field rendered as plain text, so a
link in `Quote text -- [Source](https://…)` showed as literal markdown.

## What changed

`app/[...path]/EssayView.tsx` — render the attribution the same way as
the quote body:

```tsx
<span className="essay-quote__author"
      dangerouslySetInnerHTML={{ __html: `— ${renderInlineMarkdown(block.author)}` }} />
```

The attribution keeps its Markdown in the parsed block and is rendered
at display time, which means it works:

- in the published journal page and photo essays (they share
`EssayView`),
- in the Journal Studio live preview (which renders the same component),
- from both the raw-Markdown file *and* the block editor — because the
field stays Markdown, typing `[link](…)` in the author input round-trips
untouched and renders live.

## Security

No new surface: it reuses `renderInlineMarkdown`, which escapes
everything first and then emits only bold/italic/anchors, with the URL
scheme allowlisted (`http/https/mailto/tel`/relative/anchor).
`javascript:`/`data:` URLs render as plain text, exactly as captions
behave today.

## Tests

`lib/__tests__/essay.test.ts` — three cases:

- a link in the attribution is preserved for rendering,
- it survives a serialize → parse round-trip,
- an unsafe scheme (`javascript:`) is not turned into an anchor.

## Notes

- **Photo captions already support links** — they've always run through
`renderInlineMarkdown` (this also answers the question raised in #559).
- Out of scope, flagged for later: links in paragraphs/captions still
get *dropped* on a save through the block editor (the serializer undoes
`<strong>`/`<em>` but strips `<a>`). The attribution is unaffected
because it stays Markdown. Fixing that would be a small follow-up in
`serializeJournalMarkdown`.
…560)

## Problem

With page transitions enabled (the default — `settings.yaml` →
`transitions`), the client proofing UI never floats. The "❤️ N Selected"
bar and its **Share & Export** modal are `position: fixed`, but they
rendered at the bottom of the page content instead of over the viewport
— scroll up and they scroll away with the page.

## Root cause

`app/globals.css` fades the page in with a transform:

```css
[data-transitions='true'] .main {
  animation: page-fade-in 0.5s ease-out forwards;
}

@Keyframes page-fade-in {
  from { opacity: 0; transform: translateY(8px); }
  to   { opacity: 1; transform: translateY(0); }
}
```

Per the CSS spec, an element with a `transform` establishes a
**containing block** for its `position: fixed` descendants. Two things
compound this:

1. `animation-fill-mode: forwards` keeps the `to` keyframe applied
**forever** after the 0.5s animation finishes, so `.main` never loses
its transform.
2. Even the *identity* `translateY(0)` counts — any value other than
`none` creates the containing block.

The result: the proofing bar (`.proofing-sticky-bar`) and the modal
overlay (`.proofing-modal-overlay`) resolve their `fixed` coordinates
against `.main` instead of the viewport, so they sit at the bottom of
the page content.

This also silently breaks any other `position: fixed` element rendered
inside `<main>` — present or future.

## Fix

Drop the transform and keep only the opacity fade:

```css
@Keyframes page-fade-in {
  from { opacity: 0; }
  to   { opacity: 1; }
}
```

`opacity` is safe: it creates a stacking context but **not** a
containing block, so `position: fixed` keeps working. The visual change
is the loss of the 8px slide; the fade — which is the dominant effect —
is unchanged.

## Scope

- `app/globals.css` — one keyframe, plus a comment explaining *why* the
transform must not come back.

## Verification

- `npm run build` ✅
- `npx tsc --noEmit` ✅
- `npx vitest run` ✅ (no test exercises CSS animation; this is a
rendering fix)

## Notes for review

- This is deliberately a standalone fix, separate from the download
feature it was discovered alongside: the proofing bar/modal predate the
download work and were already broken.
- The fix is behavioural (removes an 8px slide) but not a breaking
change — no config, markup, or API is touched.
…562)

## Problem

The client-proofing selection UI — the floating "❤️ N Selected" bar and
the **Share & Export** modal — ignores the page's colour mode. In light
mode the text turns dark but the bar/modal stay dark, so the text is
unreadable.

## Root cause

The bar and modal were styled with two CSS custom properties that no
theme defines:

- `var(--bg-surface, #1e1e1e)` — `--bg-surface` does not exist in
`app/tokens.css`, so it **always** falls back to the hardcoded dark
`#1e1e1e`.
- `var(--border-color, rgba(255,255,255,0.15))` — `--border-color` also
does not exist, so the border stays a white-alpha value that only reads
correctly on dark.
- The secondary buttons used hardcoded `rgba(255,255,255,…)`
backgrounds/borders.

`--text-primary` *is* a real, themed token, which is why the text
switched to dark while the surfaces stayed dark — the mismatch made it
unreadable.

## Fix

Swap the undefined tokens for the ones the rest of the site already uses
(all defined for both dark and light in every preset):

| Before | After |
| --- | --- |
| `var(--bg-surface, #1e1e1e)` | `var(--bg-card)` |
| `var(--border-color, …)` | `var(--border-subtle)` |
| `rgba(255,255,255,0.1/0.15)` button surfaces/borders |
`var(--bg-card-hover)` / `var(--border-subtle)` |

Also, the "❤️ N Selected" filter chip now uses `var(--text-primary)`
when inactive instead of hardcoded white, so it reads on a light
surface.

## Scope

- `components/ProofingModal.tsx`
- `app/[...path]/PhotoGrid.tsx`
- `app/[...path]/EssayView.tsx` (essays render a duplicate of the
selection bar)

## Verification

- `npx tsc --noEmit` ✅
- `npm run build` ✅
- `npx vitest run` ✅

No config, markup, or API changes — this is purely swapping colour
tokens.

Co-authored-by: ralksta <ralksta@users.noreply.github.com>
Bumps
[actions/upload-artifact](https://github.com/actions/upload-artifact)
from 4 to 7.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/actions/upload-artifact/releases">actions/upload-artifact's
releases</a>.</em></p>
<blockquote>
<h2>v7.0.0</h2>
<h2>v7 What's new</h2>
<h3>Direct Uploads</h3>
<p>Adds support for uploading single files directly (unzipped). Callers
can set the new <code>archive</code> parameter to <code>false</code> to
skip zipping the file during upload. Right now, we only support single
files. The action will fail if the glob passed resolves to multiple
files. The <code>name</code> parameter is also ignored with this
setting. Instead, the name of the artifact will be the name of the
uploaded file.</p>
<h3>ESM</h3>
<p>To support new versions of the <code>@actions/*</code> packages,
we've upgraded the package to ESM.</p>
<h2>What's Changed</h2>
<ul>
<li>Add proxy integration test by <a
href="https://github.com/Link"><code>@​Link</code></a>- in <a
href="https://redirect.github.com/actions/upload-artifact/pull/754">actions/upload-artifact#754</a></li>
<li>Upgrade the module to ESM and bump dependencies by <a
href="https://github.com/danwkennedy"><code>@​danwkennedy</code></a> in
<a
href="https://redirect.github.com/actions/upload-artifact/pull/762">actions/upload-artifact#762</a></li>
<li>Support direct file uploads by <a
href="https://github.com/danwkennedy"><code>@​danwkennedy</code></a> in
<a
href="https://redirect.github.com/actions/upload-artifact/pull/764">actions/upload-artifact#764</a></li>
</ul>
<h2>New Contributors</h2>
<ul>
<li><a href="https://github.com/Link"><code>@​Link</code></a>- made
their first contribution in <a
href="https://redirect.github.com/actions/upload-artifact/pull/754">actions/upload-artifact#754</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/actions/upload-artifact/compare/v6...v7.0.0">https://github.com/actions/upload-artifact/compare/v6...v7.0.0</a></p>
<h2>v6.0.0</h2>
<h2>v6 - What's new</h2>
<blockquote>
<p>[!IMPORTANT]
actions/upload-artifact@v6 now runs on Node.js 24 (<code>runs.using:
node24</code>) and requires a minimum Actions Runner version of 2.327.1.
If you are using self-hosted runners, ensure they are updated before
upgrading.</p>
</blockquote>
<h3>Node.js 24</h3>
<p>This release updates the runtime to Node.js 24. v5 had preliminary
support for Node.js 24, however this action was by default still running
on Node.js 20. Now this action by default will run on Node.js 24.</p>
<h2>What's Changed</h2>
<ul>
<li>Upload Artifact Node 24 support by <a
href="https://github.com/salmanmkc"><code>@​salmanmkc</code></a> in <a
href="https://redirect.github.com/actions/upload-artifact/pull/719">actions/upload-artifact#719</a></li>
<li>fix: update <code>@​actions/artifact</code> for Node.js 24 punycode
deprecation by <a
href="https://github.com/salmanmkc"><code>@​salmanmkc</code></a> in <a
href="https://redirect.github.com/actions/upload-artifact/pull/744">actions/upload-artifact#744</a></li>
<li>prepare release v6.0.0 for Node.js 24 support by <a
href="https://github.com/salmanmkc"><code>@​salmanmkc</code></a> in <a
href="https://redirect.github.com/actions/upload-artifact/pull/745">actions/upload-artifact#745</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/actions/upload-artifact/compare/v5.0.0...v6.0.0">https://github.com/actions/upload-artifact/compare/v5.0.0...v6.0.0</a></p>
<h2>v5.0.0</h2>
<h2>What's Changed</h2>
<p><strong>BREAKING CHANGE:</strong> this update supports Node
<code>v24.x</code>. This is not a breaking change per-se but we're
treating it as such.</p>
<ul>
<li>Update README.md by <a
href="https://github.com/GhadimiR"><code>@​GhadimiR</code></a> in <a
href="https://redirect.github.com/actions/upload-artifact/pull/681">actions/upload-artifact#681</a></li>
<li>Update README.md by <a
href="https://github.com/nebuk89"><code>@​nebuk89</code></a> in <a
href="https://redirect.github.com/actions/upload-artifact/pull/712">actions/upload-artifact#712</a></li>
<li>Readme: spell out the first use of GHES by <a
href="https://github.com/danwkennedy"><code>@​danwkennedy</code></a> in
<a
href="https://redirect.github.com/actions/upload-artifact/pull/727">actions/upload-artifact#727</a></li>
<li>Update GHES guidance to include reference to Node 20 version by <a
href="https://github.com/patrikpolyak"><code>@​patrikpolyak</code></a>
in <a
href="https://redirect.github.com/actions/upload-artifact/pull/725">actions/upload-artifact#725</a></li>
<li>Bump <code>@actions/artifact</code> to <code>v4.0.0</code></li>
<li>Prepare <code>v5.0.0</code> by <a
href="https://github.com/danwkennedy"><code>@​danwkennedy</code></a> in
<a
href="https://redirect.github.com/actions/upload-artifact/pull/734">actions/upload-artifact#734</a></li>
</ul>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/actions/upload-artifact/commit/043fb46d1a93c77aae656e7c1c64a875d1fc6a0a"><code>043fb46</code></a>
Merge pull request <a
href="https://redirect.github.com/actions/upload-artifact/issues/797">#797</a>
from actions/yacaovsnc/update-dependency</li>
<li><a
href="https://github.com/actions/upload-artifact/commit/634250c1388765ea7ed0f053e636f1f399000b94"><code>634250c</code></a>
Include changes in typespec/ts-http-runtime 0.3.5</li>
<li><a
href="https://github.com/actions/upload-artifact/commit/e454baaac2be505c9450e11b8f3215c6fc023ce8"><code>e454baa</code></a>
Readme: bump all the example versions to v7 (<a
href="https://redirect.github.com/actions/upload-artifact/issues/796">#796</a>)</li>
<li><a
href="https://github.com/actions/upload-artifact/commit/74fad66b98a6d799dc004d3353ccd0e6f6b2530e"><code>74fad66</code></a>
Update the readme with direct upload details (<a
href="https://redirect.github.com/actions/upload-artifact/issues/795">#795</a>)</li>
<li><a
href="https://github.com/actions/upload-artifact/commit/bbbca2ddaa5d8feaa63e36b76fdaad77386f024f"><code>bbbca2d</code></a>
Support direct file uploads (<a
href="https://redirect.github.com/actions/upload-artifact/issues/764">#764</a>)</li>
<li><a
href="https://github.com/actions/upload-artifact/commit/589182c5a4cec8920b8c1bce3e2fab1c97a02296"><code>589182c</code></a>
Upgrade the module to ESM and bump dependencies (<a
href="https://redirect.github.com/actions/upload-artifact/issues/762">#762</a>)</li>
<li><a
href="https://github.com/actions/upload-artifact/commit/47309c993abb98030a35d55ef7ff34b7fa1074b5"><code>47309c9</code></a>
Merge pull request <a
href="https://redirect.github.com/actions/upload-artifact/issues/754">#754</a>
from actions/Link-/add-proxy-integration-tests</li>
<li><a
href="https://github.com/actions/upload-artifact/commit/02a8460834e70dab0ce194c64360c59dc1475ef0"><code>02a8460</code></a>
Add proxy integration test</li>
<li><a
href="https://github.com/actions/upload-artifact/commit/b7c566a772e6b6bfb58ed0dc250532a479d7789f"><code>b7c566a</code></a>
Merge pull request <a
href="https://redirect.github.com/actions/upload-artifact/issues/745">#745</a>
from actions/upload-artifact-v6-release</li>
<li><a
href="https://github.com/actions/upload-artifact/commit/e516bc8500aaf3d07d591fcd4ae6ab5f9c391d5b"><code>e516bc8</code></a>
docs: correct description of Node.js 24 support in README</li>
<li>Additional commits viewable in <a
href="https://github.com/actions/upload-artifact/compare/v4...v7">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=actions/upload-artifact&package-manager=github_actions&previous-version=4&new-version=7)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)


</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
…566)

Bumps the nextjs group with 2 updates in the / directory:
[next](https://github.com/vercel/next.js) and
[eslint-config-next](https://github.com/vercel/next.js/tree/HEAD/packages/eslint-config-next).

Updates `next` from 16.3.4 to 16.3.5
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/vercel/next.js/releases">next's
releases</a>.</em></p>
<blockquote>
<h2>v16.3.5</h2>
<p>The following bug fixes have been backported. It does not include all
pending features/changes on canary.</p>
<ul>
<li>next/image: Skip 0-byte entries when initializing disk LRU cache (<a
href="https://redirect.github.com/vercel/next.js/issues/98185">#98185</a>)</li>
<li>next/image: Reject empty images when reading/writing to the disk
cache (<a
href="https://redirect.github.com/vercel/next.js/issues/98186">#98186</a>)</li>
<li>Emit whole-app server NFTs when <code>output: 'standalone'</code> is
used with an adapter (<a
href="https://redirect.github.com/vercel/next.js/issues/98167">#98167</a>)</li>
<li>Add CSP nonce to script tags of loading and template files (<a
href="https://redirect.github.com/vercel/next.js/issues/98403">#98403</a>)</li>
<li>Fix <code>use cache</code> prerender signal retention (<a
href="https://redirect.github.com/vercel/next.js/issues/98448">#98448</a>)</li>
</ul>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/vercel/next.js/commit/ca2c75eb7f8d9dd012a8bb83c06132149fe221f9"><code>ca2c75e</code></a>
v16.3.5</li>
<li><a
href="https://github.com/vercel/next.js/commit/14fb290de65077e9f1e22ef56d8de6cc1e63d436"><code>14fb290</code></a>
[backport] Fix use cache prerender signal retention (<a
href="https://redirect.github.com/vercel/next.js/issues/98448">#98448</a>)</li>
<li><a
href="https://github.com/vercel/next.js/commit/2b1f28dbe1de344807ec0946a85171bc890a6047"><code>2b1f28d</code></a>
[16.3.x] Add CSP nonce to script tags of loading and template files (<a
href="https://redirect.github.com/vercel/next.js/issues/98403">#98403</a>)</li>
<li><a
href="https://github.com/vercel/next.js/commit/4b56cee3f01d3b249edcd798b51895d5126a4170"><code>4b56cee</code></a>
[16.3.x] Backport docs fixes (<a
href="https://redirect.github.com/vercel/next.js/issues/98317">#98317</a>)</li>
<li><a
href="https://github.com/vercel/next.js/commit/5568a02a7d47f9cb088e58350f2c2e68d9e93a00"><code>5568a02</code></a>
[backport] docs: local development: Rewrite docker section, add Windows
Dev D...</li>
<li><a
href="https://github.com/vercel/next.js/commit/93249ab2144132abfd4a8d611dad5b5978107ee2"><code>93249ab</code></a>
[16.3.X] Emit whole-app server NFTs when <code>output:
'standalone'</code> is used with ...</li>
<li><a
href="https://github.com/vercel/next.js/commit/6549fd7c4e15a8883b0ad1c2ef67dec287a44f12"><code>6549fd7</code></a>
[16.3.x] next/image: reject empty image on read/write to disk cache (<a
href="https://redirect.github.com/vercel/next.js/issues/98186">#98186</a>)</li>
<li><a
href="https://github.com/vercel/next.js/commit/d9eac96e7526ff0b9cb51db9801f06e741fe1960"><code>d9eac96</code></a>
[16.3.x] next/image: skip 0-byte entries when initializing disk LRU
cache (<a
href="https://redirect.github.com/vercel/next.js/issues/9">#9</a>...</li>
<li><a
href="https://github.com/vercel/next.js/commit/84b35feccb2b53a563e41ad2dfe7a5fe08c58d3f"><code>84b35fe</code></a>
[test] Fix 16.3 deploy test assertions (<a
href="https://redirect.github.com/vercel/next.js/issues/98133">#98133</a>)</li>
<li><a
href="https://github.com/vercel/next.js/commit/14f9c1ac4e084a44633c354476ddeaf70969cd90"><code>14f9c1a</code></a>
[16.3.x][ci] Run flake detection and new deploy tests when merged and on
back...</li>
<li>See full diff in <a
href="https://github.com/vercel/next.js/compare/v16.3.4...v16.3.5">compare
view</a></li>
</ul>
</details>
<br />

Updates `eslint-config-next` from 16.3.4 to 16.3.5
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/vercel/next.js/releases">eslint-config-next's
releases</a>.</em></p>
<blockquote>
<h2>v16.3.5</h2>
<p>The following bug fixes have been backported. It does not include all
pending features/changes on canary.</p>
<ul>
<li>next/image: Skip 0-byte entries when initializing disk LRU cache (<a
href="https://github.com/vercel/next.js/tree/HEAD/packages/eslint-config-next/issues/98185">#98185</a>)</li>
<li>next/image: Reject empty images when reading/writing to the disk
cache (<a
href="https://github.com/vercel/next.js/tree/HEAD/packages/eslint-config-next/issues/98186">#98186</a>)</li>
<li>Emit whole-app server NFTs when <code>output: 'standalone'</code> is
used with an adapter (<a
href="https://github.com/vercel/next.js/tree/HEAD/packages/eslint-config-next/issues/98167">#98167</a>)</li>
<li>Add CSP nonce to script tags of loading and template files (<a
href="https://github.com/vercel/next.js/tree/HEAD/packages/eslint-config-next/issues/98403">#98403</a>)</li>
<li>Fix <code>use cache</code> prerender signal retention (<a
href="https://github.com/vercel/next.js/tree/HEAD/packages/eslint-config-next/issues/98448">#98448</a>)</li>
</ul>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/vercel/next.js/commit/ca2c75eb7f8d9dd012a8bb83c06132149fe221f9"><code>ca2c75e</code></a>
v16.3.5</li>
<li>See full diff in <a
href="https://github.com/vercel/next.js/commits/v16.3.5/packages/eslint-config-next">compare
view</a></li>
</ul>
</details>
<br />

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Bumps [@playwright/test](https://github.com/microsoft/playwright) from
1.62.1 to 1.63.0.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/microsoft/playwright/releases">@​playwright/test's
releases</a>.</em></p>
<blockquote>
<h2>v1.63.0</h2>
<h2>🔒 Test locks</h2>
<p>Tests that access a shared resource — an external service, a global
account setting — can now declare a named <code>lock</code>.
Tests that share a lock name never run concurrently, across files,
workers and <a
href="https://playwright.dev/docs/test-projects">projects</a>, while
everything else keeps running in parallel:</p>
<pre lang="js"><code>test('update user settings', { lock:
'user-settings' }, async ({ page }) =&gt; {
  // never runs at the same time as other tests holding 'user-settings'
});
</code></pre>
<p>A test can hold multiple locks, and <a
href="https://playwright.dev/docs/api/class-test#test-describe">test.describe()</a>
accepts a <code>lock</code> for the whole group.
Learn more about <a
href="https://playwright.dev/docs/test-parallel#test-locks">test
locks</a>.</p>
<h2>🪟 Locate across frames</h2>
<p><a
href="https://playwright.dev/docs/api/class-page#page-frame-locator">page.frameLocator()</a>
and <a
href="https://playwright.dev/docs/api/class-frame#frame-frame-locator">frame.frameLocator()</a>
called without a selector search in any frame of the
subtree, so you no longer need to locate the iframe first:</p>
<pre lang="js"><code>// Finds the button in any frame on the page.
await page.frameLocator().getByRole('button').click();
</code></pre>
<p>The rest of the locator resolves inside a single frame, just like a
regular locator, and an error is thrown when it
matches elements in several frames.</p>
<h2>👁️ Visible-only locators</h2>
<p>New <a
href="https://playwright.dev/docs/api/class-locator#locator-visible">locator.visible()</a>
returns a locator that matches only visible elements. It is the
recommended
replacement for the <code>:visible</code> CSS pseudo-class:</p>
<pre lang="js"><code>await page.locator('button').visible().click();
</code></pre>
<h2>🧾 Step params and subtitles</h2>
<p>Steps now carry structured data for reporters. Playwright API steps
report the target locator and call arguments,
and <a
href="https://playwright.dev/docs/api/class-test#test-step">test.step()</a>
accepts <code>subtitle</code> and <code>params</code> options for your
own steps:</p>
<pre lang="js"><code>await test.step('Login', async () =&gt; {
  // ...
}, { subtitle: 'as admin', params: { user: 'admin' } });
</code></pre>
<p>Reporters receive them via <a
href="https://playwright.dev/docs/api/class-teststep#test-step-subtitle">testStep.subtitle</a>
and <a
href="https://playwright.dev/docs/api/class-teststep#test-step-params">testStep.params</a>.
For Playwright API</p>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/microsoft/playwright/commit/1b025d7e20a026371cd5f98ba0cdce48892737c8"><code>1b025d7</code></a>
chore: mark v1.63.0 (<a
href="https://redirect.github.com/microsoft/playwright/issues/42569">#42569</a>)</li>
<li><a
href="https://github.com/microsoft/playwright/commit/0b9956d2247ce88399c3c46a22c1cc0526acb340"><code>0b9956d</code></a>
cherry-pick(<a
href="https://redirect.github.com/microsoft/playwright/issues/42568">#42568</a>):
docs(test): mark test.step subtitle option as since v1.63</li>
<li><a
href="https://github.com/microsoft/playwright/commit/13dbf102b88305ed96b7791a7843246cd75d4dae"><code>13dbf10</code></a>
cherry-pick(<a
href="https://redirect.github.com/microsoft/playwright/issues/42552">#42552</a>):
docs: release notes for v1.63</li>
<li><a
href="https://github.com/microsoft/playwright/commit/e93b64ed6f9cf6ac2cc8cb13ad01514b406a7142"><code>e93b64e</code></a>
cherry-pick(<a
href="https://redirect.github.com/microsoft/playwright/issues/42566">#42566</a>):
feat(test): add subtitle option to test.step (<a
href="https://redirect.github.com/microsoft/playwright/issues/42567">#42567</a>)</li>
<li><a
href="https://github.com/microsoft/playwright/commit/2b7a5f2ea1cea04b549c5378498c1b16b6bc6a6f"><code>2b7a5f2</code></a>
test: response.body() for content-encoding:identity (<a
href="https://redirect.github.com/microsoft/playwright/issues/42537">#42537</a>)</li>
<li><a
href="https://github.com/microsoft/playwright/commit/648a67c7c1261eefe4113cba2d586417d5e3f2f2"><code>648a67c</code></a>
fix(mcp): create parent directories for explicitly named files (<a
href="https://redirect.github.com/microsoft/playwright/issues/42540">#42540</a>)</li>
<li><a
href="https://github.com/microsoft/playwright/commit/7894f5653e7f253c17aa1638c19f63e0d54d5e0e"><code>7894f56</code></a>
docs(mcp): clarify how tool file names are resolved (<a
href="https://redirect.github.com/microsoft/playwright/issues/42538">#42538</a>)</li>
<li><a
href="https://github.com/microsoft/playwright/commit/52900a1b99ca51011526afcaf8981970cc1a521a"><code>52900a1</code></a>
devops: restore npm publishing from GitHub Actions (<a
href="https://redirect.github.com/microsoft/playwright/issues/42550">#42550</a>)</li>
<li><a
href="https://github.com/microsoft/playwright/commit/8c47f596edebd2460895a5953f2ea67f8b884001"><code>8c47f59</code></a>
docs(csharp): fix nonexistent method names in guide examples (<a
href="https://redirect.github.com/microsoft/playwright/issues/42507">#42507</a>)</li>
<li><a
href="https://github.com/microsoft/playwright/commit/bd6e552a63f8cf9b0da0216f392b8e63a08bf1d2"><code>bd6e552</code></a>
chore(video): emit frames with real timestamps, drop frame number
quantizatio...</li>
<li>Additional commits viewable in <a
href="https://github.com/microsoft/playwright/compare/v1.62.1...v1.63.0">compare
view</a></li>
</ul>
</details>
<br />

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
…574)

Bumps the react group with 4 updates in the / directory:
[react](https://github.com/react/react/tree/HEAD/packages/react),
[@types/react](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/react),
[react-dom](https://github.com/react/react/tree/HEAD/packages/react-dom)
and
[@types/react-dom](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/react-dom).

Updates `react` from 19.2.8 to 19.3.0
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/react/react/releases">react's
releases</a>.</em></p>
<blockquote>
<h2>19.3.0 (September 9, 2026)</h2>
<p>Below is a list of all new features, APIs, and bug fixes.</p>
<p>Read the <a href="https://react.dev/blog/2026/09/09/react-19-3">React
19.3 release post</a> for more information.</p>
<h2>New React Features</h2>
<ul>
<li><code>&lt;ViewTransition /&gt;</code>: Adds <code>&lt;ViewTransition
/&gt;</code> and <code>addTransitionType</code> APIs to power View
Transition animations in React (<a
href="https://github.com/sebmarkbage"><code>@​sebmarkbage</code></a>, <a
href="https://github.com/jackpope"><code>@​jackpope</code></a>, <a
href="https://github.com/gaearon"><code>@​gaearon</code></a>: <a
href="https://redirect.github.com/facebook/react/pull/31975">#31975</a>,
<a
href="https://redirect.github.com/facebook/react/pull/31987">#31987</a>,
<a
href="https://redirect.github.com/facebook/react/pull/31996">#31996</a>,
<a
href="https://redirect.github.com/facebook/react/pull/31999">#31999</a>,
<a
href="https://redirect.github.com/facebook/react/pull/32001">#32001</a>,
<a
href="https://redirect.github.com/facebook/react/pull/32002">#32002</a>,
<a
href="https://redirect.github.com/facebook/react/pull/32028">#32028</a>,
<a
href="https://redirect.github.com/facebook/react/pull/32029">#32029</a>,
<a
href="https://redirect.github.com/facebook/react/pull/32031">#32031</a>,
<a
href="https://redirect.github.com/facebook/react/pull/32034">#32034</a>,
<a
href="https://redirect.github.com/facebook/react/pull/32038">#32038</a>,
<a
href="https://redirect.github.com/facebook/react/pull/32041">#32041</a>,
<a
href="https://redirect.github.com/facebook/react/pull/32050">#32050</a>,
<a
href="https://redirect.github.com/facebook/react/pull/32090">#32090</a>,
<a
href="https://redirect.github.com/facebook/react/pull/32105">#32105</a>,
<a
href="https://redirect.github.com/facebook/react/pull/32254">#32254</a>,
<a
href="https://redirect.github.com/facebook/react/pull/32379">#32379</a>,
<a
href="https://redirect.github.com/facebook/react/pull/32422">#32422</a>,
<a
href="https://redirect.github.com/facebook/react/pull/32462">#32462</a>,
<a
href="https://redirect.github.com/facebook/react/pull/32540">#32540</a>,
<a
href="https://redirect.github.com/facebook/react/pull/32545">#32545</a>,
<a
href="https://redirect.github.com/facebook/react/pull/32585">#32585</a>,
<a
href="https://redirect.github.com/facebook/react/pull/32599">#32599</a>,
<a
href="https://redirect.github.com/facebook/react/pull/32611">#32611</a>,
<a
href="https://redirect.github.com/facebook/react/pull/32612">#32612</a>,
<a
href="https://redirect.github.com/facebook/react/pull/32617">#32617</a>,
<a
href="https://redirect.github.com/facebook/react/pull/32651">#32651</a>,
<a
href="https://redirect.github.com/facebook/react/pull/32653">#32653</a>,
<a
href="https://redirect.github.com/facebook/react/pull/32656">#32656</a>,
<a
href="https://redirect.github.com/facebook/react/pull/32664">#32664</a>,
<a
href="https://redirect.github.com/facebook/react/pull/32699">#32699</a>,
<a
href="https://redirect.github.com/facebook/react/pull/32723">#32723</a>,
<a
href="https://redirect.github.com/facebook/react/pull/32734">#32734</a>,
<a
href="https://redirect.github.com/facebook/react/pull/32751">#32751</a>,
<a
href="https://redirect.github.com/facebook/react/pull/32752">#32752</a>,
<a
href="https://redirect.github.com/facebook/react/pull/32760">#32760</a>,
<a
href="https://redirect.github.com/facebook/react/pull/32761">#32761</a>,
<a
href="https://redirect.github.com/facebook/react/pull/32764">#32764</a>,
<a
href="https://redirect.github.com/facebook/react/pull/32772">#32772</a>,
<a
href="https://redirect.github.com/facebook/react/pull/32790">#32790</a>,
<a
href="https://redirect.github.com/facebook/react/pull/32819">#32819</a>,
<a
href="https://redirect.github.com/facebook/react/pull/32820">#32820</a>,
<a
href="https://redirect.github.com/facebook/react/pull/32822">#32822</a>,
<a
href="https://redirect.github.com/facebook/react/pull/32833">#32833</a>,
<a
href="https://redirect.github.com/facebook/react/pull/32849">#32849</a>,
<a
href="https://redirect.github.com/facebook/react/pull/33094">#33094</a>,
<a
href="https://redirect.github.com/facebook/react/pull/33191">#33191</a>,
<a
href="https://redirect.github.com/facebook/react/pull/33200">#33200</a>,
<a
href="https://redirect.github.com/facebook/react/pull/33206">#33206</a>,
<a
href="https://redirect.github.com/facebook/react/pull/33293">#33293</a>,
<a
href="https://redirect.github.com/facebook/react/pull/33330">#33330</a>,
<a
href="https://redirect.github.com/facebook/react/pull/33331">#33331</a>,
<a
href="https://redirect.github.com/facebook/react/pull/33332">#33332</a>,
<a
href="https://redirect.github.com/facebook/react/pull/33357">#33357</a>,
<a
href="https://redirect.github.com/facebook/react/pull/33362">#33362</a>,
<a
href="https://redirect.github.com/facebook/react/pull/33433">#33433</a>,
<a
href="https://redirect.github.com/facebook/react/pull/33576">#33576</a>,
<a
href="https://redirect.github.com/facebook/react/pull/34374">#34374</a>,
<a
href="https://redirect.github.com/facebook/react/pull/34450">#34450</a>,
<a
href="https://redirect.github.com/facebook/react/pull/34481">#34481</a>,
<a
href="https://redirect.github.com/facebook/react/pull/34500">#34500</a>,
<a
href="https://redirect.github.com/facebook/react/pull/34502">#34502</a>,
<a
href="https://redirect.github.com/facebook/react/pull/34510">#34510</a>,
<a
href="https://redirect.github.com/facebook/react/pull/34511">#34511</a>,
<a
href="https://redirect.github.com/facebook/react/pull/34539">#34539</a>,
<a
href="https://redirect.github.com/facebook/react/pull/35567">#35567</a>,
<a
href="https://redirect.github.com/facebook/react/pull/35564">#35564</a>,
<a
href="https://redirect.github.com/facebook/react/pull/35485">#35485</a>,
<a
href="https://redirect.github.com/facebook/react/pull/35380">#35380</a>,
<a
href="https://redirect.github.com/facebook/react/pull/35063">#35063</a>,
<a
href="https://redirect.github.com/facebook/react/pull/35060">#35060</a>,
<a
href="https://redirect.github.com/facebook/react/pull/34676">#34676</a>,
<a
href="https://redirect.github.com/facebook/react/pull/36917">#36917</a>,
<a
href="https://redirect.github.com/facebook/react/pull/35337">#35337</a>,
<a
href="https://redirect.github.com/facebook/react/pull/35520">#35520</a>)</li>
<li>Fragment Refs: Add Refs to <code>&lt;Fragment /&gt;</code> to
support composable platform behavior (<a
href="https://github.com/jackpope"><code>@​jackpope</code></a>, <a
href="https://github.com/sebmarkbage"><code>@​sebmarkbage</code></a>, <a
href="https://github.com/eps1lon"><code>@​eps1lon</code></a>, <a
href="https://github.com/Dhakshin2007"><code>@​Dhakshin2007</code></a>,
<a href="https://github.com/chirokas"><code>@​chirokas</code></a>, <a
href="https://github.com/teamleaderleo"><code>@​teamleaderleo</code></a>,
<a
href="https://github.com/fallintoplace"><code>@​fallintoplace</code></a>:
<a
href="https://redirect.github.com/facebook/react/pull/32465">#32465</a>,
<a
href="https://redirect.github.com/facebook/react/pull/32613">#32613</a>,
<a
href="https://redirect.github.com/facebook/react/pull/32619">#32619</a>,
<a
href="https://redirect.github.com/facebook/react/pull/32654">#32654</a>,
<a
href="https://redirect.github.com/facebook/react/pull/32660">#32660</a>,
<a
href="https://redirect.github.com/facebook/react/pull/32682">#32682</a>,
<a
href="https://redirect.github.com/facebook/react/pull/32722">#32722</a>,
<a
href="https://redirect.github.com/facebook/react/pull/32813">#32813</a>,
<a
href="https://redirect.github.com/facebook/react/pull/32814">#32814</a>,
<a
href="https://redirect.github.com/facebook/react/pull/33056">#33056</a>,
<a
href="https://redirect.github.com/facebook/react/pull/33058">#33058</a>,
<a
href="https://redirect.github.com/facebook/react/pull/33093">#33093</a>,
<a
href="https://redirect.github.com/facebook/react/pull/34069">#34069</a>,
<a
href="https://redirect.github.com/facebook/react/pull/34103">#34103</a>,
<a
href="https://redirect.github.com/facebook/react/pull/34544">#34544</a>,
<a
href="https://redirect.github.com/facebook/react/pull/34545">#34545</a>,
<a
href="https://redirect.github.com/facebook/react/pull/37062">#37062</a>,
<a
href="https://redirect.github.com/facebook/react/pull/37061">#37061</a>,
<a
href="https://redirect.github.com/facebook/react/pull/37060">#37060</a>,
<a
href="https://redirect.github.com/facebook/react/pull/36047">#36047</a>,
<a
href="https://redirect.github.com/facebook/react/pull/36010">#36010</a>,
<a
href="https://redirect.github.com/facebook/react/pull/35642">#35642</a>,
<a
href="https://redirect.github.com/facebook/react/pull/35641">#35641</a>,
<a
href="https://redirect.github.com/facebook/react/pull/35637">#35637</a>,
<a
href="https://redirect.github.com/facebook/react/pull/35630">#35630</a>,
<a
href="https://redirect.github.com/facebook/react/pull/34935">#34935</a>,
<a
href="https://redirect.github.com/facebook/react/pull/37457">#37457</a>,
<a
href="https://redirect.github.com/facebook/react/pull/37408">#37408</a>,
<a
href="https://redirect.github.com/facebook/react/pull/37326">#37326</a>,
<a
href="https://redirect.github.com/facebook/react/pull/37251">#37251</a>,
<a
href="https://redirect.github.com/facebook/react/pull/37171">#37171</a>,
<a
href="https://redirect.github.com/facebook/react/pull/37169">#37169</a>,
<a
href="https://redirect.github.com/facebook/react/pull/37168">#37168</a>,
<a
href="https://redirect.github.com/facebook/react/pull/37167">#37167</a>,
<a
href="https://redirect.github.com/facebook/react/pull/37166">#37166</a>,
<a
href="https://redirect.github.com/facebook/react/pull/37165">#37165</a>,
<a
href="https://redirect.github.com/facebook/react/pull/37164">#37164</a>,
<a
href="https://redirect.github.com/facebook/react/pull/37163">#37163</a>,
<a
href="https://redirect.github.com/facebook/react/pull/37162">#37162</a>,
<a
href="https://redirect.github.com/facebook/react/pull/37161">#37161</a>,
<a
href="https://redirect.github.com/facebook/react/pull/37160">#37160</a>,
<a
href="https://redirect.github.com/facebook/react/pull/37125">#37125</a>,
<a
href="https://redirect.github.com/facebook/react/pull/37063">#37063</a>)</li>
</ul>
<h2>New React DOM Features</h2>
<ul>
<li><code>browser()</code>: a new <code>react-dom</code> API that
returns a usable which errors during server rendering and resolves in
the browser. <code>use(browser())</code> inside a
<code>&lt;Suspense&gt;</code> boundary marks a subtree as browser-only
without reporting a recoverable error (<a
href="https://github.com/gnoff"><code>@​gnoff</code></a>: <a
href="https://redirect.github.com/facebook/react/pull/37143">#37143</a>,
<a
href="https://redirect.github.com/facebook/react/pull/37241">#37241</a>)
<ul>
<li>Added an <code>onBrowserBailout</code> option to the
<code>react-dom/server</code> APIs to observe when a subtree defers to
the browser (<a href="https://github.com/gnoff"><code>@​gnoff</code></a>
<a
href="https://redirect.github.com/facebook/react/pull/37193">#37193</a>)</li>
</ul>
</li>
</ul>
<h2>Notable changes</h2>
<ul>
<li>Enable Trusted Types API integration (<a
href="https://github.com/rickhanlonii"><code>@​rickhanlonii</code></a>
<a
href="https://redirect.github.com/facebook/react/pull/35816">#35816</a>)</li>
<li>Transitions now render independently instead of being entangled into
a single render, so a slow transition no longer holds up unrelated ones
(<a href="https://github.com/acdlite"><code>@​acdlite</code></a> <a
href="https://redirect.github.com/facebook/react/pull/37290">#37290</a>)</li>
<li>Added a DEV-only warning when a component appears to have been
unblocked by calling <code>use()</code> conditionally (<a
href="https://github.com/hoxyq"><code>@​hoxyq</code></a>, <a
href="https://github.com/eps1lon"><code>@​eps1lon</code></a>: <a
href="https://redirect.github.com/facebook/react/pull/37104">#37104</a>,
<a
href="https://redirect.github.com/facebook/react/pull/37203">#37203</a>,
<a
href="https://redirect.github.com/facebook/react/pull/37491">#37491</a>)</li>
</ul>
<h2>All Changes</h2>
<h3>React</h3>
<ul>
<li>Fast Refresh Fixes
<ul>
<li>Fix Fast Refresh to find and remount edits to components wrapped
behind <code>lazy()</code> (<a
href="https://github.com/sophiebits"><code>@​sophiebits</code></a> <a
href="https://redirect.github.com/facebook/react/pull/36965">#36965</a>)</li>
<li>Fix Fast Refresh so edits to a <code>memo()</code> comparison
function take effect (<a
href="https://github.com/sophiebits"><code>@​sophiebits</code></a> <a
href="https://redirect.github.com/facebook/react/pull/36964">#36964</a>)</li>
<li>Fix Fast Refresh crash when an edit changes the kind of a
component's type (<a
href="https://github.com/sophiebits"><code>@​sophiebits</code></a> <a
href="https://redirect.github.com/facebook/react/pull/36963">#36963</a>)</li>
<li>Unify hot reload type resolution for Fast Refresh (<a
href="https://github.com/sophiebits"><code>@​sophiebits</code></a> <a
href="https://redirect.github.com/facebook/react/pull/36962">#36962</a>)</li>
<li>Fix Fast Refresh to remount correctly when an edit changes the
component kind (<a
href="https://github.com/sophiebits"><code>@​sophiebits</code></a> <a
href="https://redirect.github.com/facebook/react/pull/36950">#36950</a>)</li>
<li>Double invoke effects in StrictMode after Fast Refresh (<a
href="https://github.com/eps1lon"><code>@​eps1lon</code></a> <a
href="https://redirect.github.com/facebook/react/pull/35962">#35962</a>)</li>
</ul>
</li>
<li>Performance Track Fixes
<ul>
<li>Prevent crash when accessing <code>$$typeof</code> in Performance
Tracks (<a href="https://github.com/eps1lon"><code>@​eps1lon</code></a>
<a
href="https://redirect.github.com/facebook/react/pull/35679">#35679</a>)</li>
<li>Handle non-string function names in Performance Tracks (<a
href="https://github.com/eps1lon"><code>@​eps1lon</code></a> <a
href="https://redirect.github.com/facebook/react/pull/35659">#35659</a>)</li>
<li>Use minus (<code>-</code>) instead of en dash for removed props in
Performance Tracks (<a
href="https://github.com/eps1lon"><code>@​eps1lon</code></a> <a
href="https://redirect.github.com/facebook/react/pull/35649">#35649</a>)</li>
<li>Handle arrays with bigints in deep objects in Performance Tracks (<a
href="https://github.com/eps1lon"><code>@​eps1lon</code></a> <a
href="https://redirect.github.com/facebook/react/pull/35648">#35648</a>)</li>
<li>Don't enumerate typed array props in Performance Tracks in DEV (<a
href="https://github.com/UditDewan"><code>@​UditDewan</code></a> <a
href="https://redirect.github.com/facebook/react/pull/36913">#36913</a>)</li>
<li>Bail out of diffing wide objects and arrays in Performance Tracks
(<a href="https://github.com/eps1lon"><code>@​eps1lon</code></a> <a
href="https://redirect.github.com/facebook/react/pull/34742">#34742</a>)</li>
<li>Clear potentially large performance measures in DEV (<a
href="https://github.com/hoxyq"><code>@​hoxyq</code></a> <a
href="https://redirect.github.com/facebook/react/pull/34803">#34803</a>)</li>
<li>Fix missing else branch for renders with no props change in
Performance Tracks (<a
href="https://github.com/hoxyq"><code>@​hoxyq</code></a> <a
href="https://redirect.github.com/facebook/react/pull/34837">#34837</a>)</li>
</ul>
</li>
<li>Activity Fixes
<ul>
<li>Fix <code>useSyncExternalStore</code> missing store mutations that
happened while an Activity tree was hidden (<a
href="https://github.com/sophiebits"><code>@​sophiebits</code></a> <a
href="https://redirect.github.com/facebook/react/pull/36947">#36947</a>)</li>
<li>Hide portal contents when an Activity is hidden (<a
href="https://github.com/acdlite"><code>@​acdlite</code></a> <a
href="https://redirect.github.com/facebook/react/pull/35091">#35091</a>)</li>
<li>Prevent metadata hoisting in hidden <code>&lt;Activity&gt;</code>
trees (<a
href="https://github.com/ronnakamoto"><code>@​ronnakamoto</code></a> <a
href="https://redirect.github.com/facebook/react/pull/34983">#34983</a>)</li>
<li>Prevent errors thrown inside a hidden Activity from escaping to the
visible UI (<a
href="https://github.com/acdlite"><code>@​acdlite</code></a> <a
href="https://redirect.github.com/facebook/react/pull/35074">#35074</a>)</li>
<li>Don't unhide a node if a direct parent Offscreen is still hidden (<a
href="https://github.com/sebmarkbage"><code>@​sebmarkbage</code></a> <a
href="https://redirect.github.com/facebook/react/pull/34821">#34821</a>)</li>
<li>Don't show internal <code>&lt;Offscreen&gt;</code> component in
error messages (<a
href="https://github.com/rickhanlonii"><code>@​rickhanlonii</code></a>
<a
href="https://redirect.github.com/facebook/react/pull/35763">#35763</a>)</li>
</ul>
</li>
<li>Warn in DEV when a component appears to have been unblocked by a
conditional <code>use()</code> (<a
href="https://github.com/hoxyq"><code>@​hoxyq</code></a>, <a
href="https://github.com/eps1lon"><code>@​eps1lon</code></a>: <a
href="https://redirect.github.com/facebook/react/pull/37104">#37104</a>,
<a
href="https://redirect.github.com/facebook/react/pull/37203">#37203</a>,
<a
href="https://redirect.github.com/facebook/react/pull/37491">#37491</a>)</li>
<li>Render transitions independently instead of entangling them into a
single render (<a
href="https://github.com/acdlite"><code>@​acdlite</code></a> <a
href="https://redirect.github.com/facebook/react/pull/37290">#37290</a>)</li>
</ul>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/react/react/blob/main/CHANGELOG.md">react's
changelog</a>.</em></p>
<blockquote>
<h2>19.3.0 (September 9, 2026)</h2>
<h3>New React Features</h3>
<ul>
<li><code>&lt;ViewTransition /&gt;</code>: Adds <code>&lt;ViewTransition
/&gt;</code> and <code>addTransitionType</code> APIs to power View
Transition animations in React (<a
href="https://github.com/sebmarkbage"><code>@​sebmarkbage</code></a>, <a
href="https://github.com/jackpope"><code>@​jackpope</code></a>, <a
href="https://github.com/gaearon"><code>@​gaearon</code></a>: <a
href="https://redirect.github.com/facebook/react/pull/31975">#31975</a>,
<a
href="https://redirect.github.com/facebook/react/pull/31987">#31987</a>,
<a
href="https://redirect.github.com/facebook/react/pull/31996">#31996</a>,
<a
href="https://redirect.github.com/facebook/react/pull/31999">#31999</a>,
<a
href="https://redirect.github.com/facebook/react/pull/32001">#32001</a>,
<a
href="https://redirect.github.com/facebook/react/pull/32002">#32002</a>,
<a
href="https://redirect.github.com/facebook/react/pull/32028">#32028</a>,
<a
href="https://redirect.github.com/facebook/react/pull/32029">#32029</a>,
<a
href="https://redirect.github.com/facebook/react/pull/32031">#32031</a>,
<a
href="https://redirect.github.com/facebook/react/pull/32034">#32034</a>,
<a
href="https://redirect.github.com/facebook/react/pull/32038">#32038</a>,
<a
href="https://redirect.github.com/facebook/react/pull/32041">#32041</a>,
<a
href="https://redirect.github.com/facebook/react/pull/32050">#32050</a>,
<a
href="https://redirect.github.com/facebook/react/pull/32090">#32090</a>,
<a
href="https://redirect.github.com/facebook/react/pull/32105">#32105</a>,
<a
href="https://redirect.github.com/facebook/react/pull/32254">#32254</a>,
<a
href="https://redirect.github.com/facebook/react/pull/32379">#32379</a>,
<a
href="https://redirect.github.com/facebook/react/pull/32422">#32422</a>,
<a
href="https://redirect.github.com/facebook/react/pull/32462">#32462</a>,
<a
href="https://redirect.github.com/facebook/react/pull/32540">#32540</a>,
<a
href="https://redirect.github.com/facebook/react/pull/32545">#32545</a>,
<a
href="https://redirect.github.com/facebook/react/pull/32585">#32585</a>,
<a
href="https://redirect.github.com/facebook/react/pull/32599">#32599</a>,
<a
href="https://redirect.github.com/facebook/react/pull/32611">#32611</a>,
<a
href="https://redirect.github.com/facebook/react/pull/32612">#32612</a>,
<a
href="https://redirect.github.com/facebook/react/pull/32617">#32617</a>,
<a
href="https://redirect.github.com/facebook/react/pull/32651">#32651</a>,
<a
href="https://redirect.github.com/facebook/react/pull/32653">#32653</a>,
<a
href="https://redirect.github.com/facebook/react/pull/32656">#32656</a>,
<a
href="https://redirect.github.com/facebook/react/pull/32664">#32664</a>,
<a
href="https://redirect.github.com/facebook/react/pull/32699">#32699</a>,
<a
href="https://redirect.github.com/facebook/react/pull/32723">#32723</a>,
<a
href="https://redirect.github.com/facebook/react/pull/32734">#32734</a>,
<a
href="https://redirect.github.com/facebook/react/pull/32751">#32751</a>,
<a
href="https://redirect.github.com/facebook/react/pull/32752">#32752</a>,
<a
href="https://redirect.github.com/facebook/react/pull/32760">#32760</a>,
<a
href="https://redirect.github.com/facebook/react/pull/32761">#32761</a>,
<a
href="https://redirect.github.com/facebook/react/pull/32764">#32764</a>,
<a
href="https://redirect.github.com/facebook/react/pull/32772">#32772</a>,
<a
href="https://redirect.github.com/facebook/react/pull/32790">#32790</a>,
<a
href="https://redirect.github.com/facebook/react/pull/32819">#32819</a>,
<a
href="https://redirect.github.com/facebook/react/pull/32820">#32820</a>,
<a
href="https://redirect.github.com/facebook/react/pull/32822">#32822</a>,
<a
href="https://redirect.github.com/facebook/react/pull/32833">#32833</a>,
<a
href="https://redirect.github.com/facebook/react/pull/32849">#32849</a>,
<a
href="https://redirect.github.com/facebook/react/pull/33094">#33094</a>,
<a
href="https://redirect.github.com/facebook/react/pull/33191">#33191</a>,
<a
href="https://redirect.github.com/facebook/react/pull/33200">#33200</a>,
<a
href="https://redirect.github.com/facebook/react/pull/33206">#33206</a>,
<a
href="https://redirect.github.com/facebook/react/pull/33293">#33293</a>,
<a
href="https://redirect.github.com/facebook/react/pull/33330">#33330</a>,
<a
href="https://redirect.github.com/facebook/react/pull/33331">#33331</a>,
<a
href="https://redirect.github.com/facebook/react/pull/33332">#33332</a>,
<a
href="https://redirect.github.com/facebook/react/pull/33357">#33357</a>,
<a
href="https://redirect.github.com/facebook/react/pull/33362">#33362</a>,
<a
href="https://redirect.github.com/facebook/react/pull/33433">#33433</a>,
<a
href="https://redirect.github.com/facebook/react/pull/33576">#33576</a>,
<a
href="https://redirect.github.com/facebook/react/pull/34374">#34374</a>,
<a
href="https://redirect.github.com/facebook/react/pull/34450">#34450</a>,
<a
href="https://redirect.github.com/facebook/react/pull/34481">#34481</a>,
<a
href="https://redirect.github.com/facebook/react/pull/34500">#34500</a>,
<a
href="https://redirect.github.com/facebook/react/pull/34502">#34502</a>,
<a
href="https://redirect.github.com/facebook/react/pull/34510">#34510</a>,
<a
href="https://redirect.github.com/facebook/react/pull/34511">#34511</a>,
<a
href="https://redirect.github.com/facebook/react/pull/34539">#34539</a>,
<a
href="https://redirect.github.com/facebook/react/pull/35567">#35567</a>,
<a
href="https://redirect.github.com/facebook/react/pull/35564">#35564</a>,
<a
href="https://redirect.github.com/facebook/react/pull/35485">#35485</a>,
<a
href="https://redirect.github.com/facebook/react/pull/35380">#35380</a>,
<a
href="https://redirect.github.com/facebook/react/pull/35063">#35063</a>,
<a
href="https://redirect.github.com/facebook/react/pull/35060">#35060</a>,
<a
href="https://redirect.github.com/facebook/react/pull/34676">#34676</a>,
<a
href="https://redirect.github.com/facebook/react/pull/36917">#36917</a>,
<a
href="https://redirect.github.com/facebook/react/pull/35337">#35337</a>,
<a
href="https://redirect.github.com/facebook/react/pull/35520">#35520</a>)</li>
<li>Fragment Refs: Add Refs to <code>&lt;Fragment /&gt;</code> to
support composable platform behavior (<a
href="https://github.com/jackpope"><code>@​jackpope</code></a>, <a
href="https://github.com/sebmarkbage"><code>@​sebmarkbage</code></a>, <a
href="https://github.com/eps1lon"><code>@​eps1lon</code></a>, <a
href="https://github.com/Dhakshin2007"><code>@​Dhakshin2007</code></a>,
<a href="https://github.com/chirokas"><code>@​chirokas</code></a>, <a
href="https://github.com/teamleaderleo"><code>@​teamleaderleo</code></a>,
<a
href="https://github.com/fallintoplace"><code>@​fallintoplace</code></a>:
<a
href="https://redirect.github.com/facebook/react/pull/32465">#32465</a>,
<a
href="https://redirect.github.com/facebook/react/pull/32613">#32613</a>,
<a
href="https://redirect.github.com/facebook/react/pull/32619">#32619</a>,
<a
href="https://redirect.github.com/facebook/react/pull/32654">#32654</a>,
<a
href="https://redirect.github.com/facebook/react/pull/32660">#32660</a>,
<a
href="https://redirect.github.com/facebook/react/pull/32682">#32682</a>,
<a
href="https://redirect.github.com/facebook/react/pull/32722">#32722</a>,
<a
href="https://redirect.github.com/facebook/react/pull/32813">#32813</a>,
<a
href="https://redirect.github.com/facebook/react/pull/32814">#32814</a>,
<a
href="https://redirect.github.com/facebook/react/pull/33056">#33056</a>,
<a
href="https://redirect.github.com/facebook/react/pull/33058">#33058</a>,
<a
href="https://redirect.github.com/facebook/react/pull/33093">#33093</a>,
<a
href="https://redirect.github.com/facebook/react/pull/34069">#34069</a>,
<a
href="https://redirect.github.com/facebook/react/pull/34103">#34103</a>,
<a
href="https://redirect.github.com/facebook/react/pull/34544">#34544</a>,
<a
href="https://redirect.github.com/facebook/react/pull/34545">#34545</a>,
<a
href="https://redirect.github.com/facebook/react/pull/37062">#37062</a>,
<a
href="https://redirect.github.com/facebook/react/pull/37061">#37061</a>,
<a
href="https://redirect.github.com/facebook/react/pull/37060">#37060</a>,
<a
href="https://redirect.github.com/facebook/react/pull/36047">#36047</a>,
<a
href="https://redirect.github.com/facebook/react/pull/36010">#36010</a>,
<a
href="https://redirect.github.com/facebook/react/pull/35642">#35642</a>,
<a
href="https://redirect.github.com/facebook/react/pull/35641">#35641</a>,
<a
href="https://redirect.github.com/facebook/react/pull/35637">#35637</a>,
<a
href="https://redirect.github.com/facebook/react/pull/35630">#35630</a>,
<a
href="https://redirect.github.com/facebook/react/pull/34935">#34935</a>,
<a
href="https://redirect.github.com/facebook/react/pull/37457">#37457</a>,
<a
href="https://redirect.github.com/facebook/react/pull/37408">#37408</a>,
<a
href="https://redirect.github.com/facebook/react/pull/37326">#37326</a>,
<a
href="https://redirect.github.com/facebook/react/pull/37251">#37251</a>,
<a
href="https://redirect.github.com/facebook/react/pull/37171">#37171</a>,
<a
href="https://redirect.github.com/facebook/react/pull/37169">#37169</a>,
<a
href="https://redirect.github.com/facebook/react/pull/37168">#37168</a>,
<a
href="https://redirect.github.com/facebook/react/pull/37167">#37167</a>,
<a
href="https://redirect.github.com/facebook/react/pull/37166">#37166</a>,
<a
href="https://redirect.github.com/facebook/react/pull/37165">#37165</a>,
<a
href="https://redirect.github.com/facebook/react/pull/37164">#37164</a>,
<a
href="https://redirect.github.com/facebook/react/pull/37163">#37163</a>,
<a
href="https://redirect.github.com/facebook/react/pull/37162">#37162</a>,
<a
href="https://redirect.github.com/facebook/react/pull/37161">#37161</a>,
<a
href="https://redirect.github.com/facebook/react/pull/37160">#37160</a>,
<a
href="https://redirect.github.com/facebook/react/pull/37125">#37125</a>,
<a
href="https://redirect.github.com/facebook/react/pull/37063">#37063</a>)</li>
</ul>
<h3>New React DOM Features</h3>
<ul>
<li><code>browser()</code>: a new <code>react-dom</code> API that
returns a usable which errors during server rendering and resolves in
the browser. <code>use(browser())</code> inside a
<code>&lt;Suspense&gt;</code> boundary marks a subtree as browser-only
without reporting a recoverable error (<a
href="https://github.com/gnoff"><code>@​gnoff</code></a>: <a
href="https://redirect.github.com/facebook/react/pull/37143">#37143</a>,
<a
href="https://redirect.github.com/facebook/react/pull/37241">#37241</a>)
<ul>
<li>Added an <code>onBrowserBailout</code> option to the
<code>react-dom/server</code> APIs to observe when a subtree defers to
the browser (<a href="https://github.com/gnoff"><code>@​gnoff</code></a>
<a
href="https://redirect.github.com/facebook/react/pull/37193">#37193</a>)</li>
</ul>
</li>
</ul>
<h3>Notable changes</h3>
<ul>
<li>Enable Trusted Types API integration (<a
href="https://github.com/rickhanlonii"><code>@​rickhanlonii</code></a>
<a
href="https://redirect.github.com/facebook/react/pull/35816">#35816</a>)</li>
<li>Transitions now render independently instead of being entangled into
a single render, so a slow transition no longer holds up unrelated ones
(<a href="https://github.com/acdlite"><code>@​acdlite</code></a> <a
href="https://redirect.github.com/facebook/react/pull/37290">#37290</a>)</li>
<li>Added a DEV-only warning when a component appears to have been
unblocked by calling <code>use()</code> conditionally (<a
href="https://github.com/hoxyq"><code>@​hoxyq</code></a>, <a
href="https://github.com/eps1lon"><code>@​eps1lon</code></a>: <a
href="https://redirect.github.com/facebook/react/pull/37104">#37104</a>,
<a
href="https://redirect.github.com/facebook/react/pull/37203">#37203</a>,
<a
href="https://redirect.github.com/facebook/react/pull/37491">#37491</a>)</li>
</ul>
<h3>All Changes</h3>
<h4>React</h4>
<ul>
<li>Fast Refresh Fixes
<ul>
<li>Fix Fast Refresh to find and remount edits to components wrapped
behind <code>lazy()</code> (<a
href="https://github.com/sophiebits"><code>@​sophiebits</code></a> <a
href="https://redirect.github.com/facebook/react/pull/36965">#36965</a>)</li>
<li>Fix Fast Refresh so edits to a <code>memo()</code> comparison
function take effect (<a
href="https://github.com/sophiebits"><code>@​sophiebits</code></a> <a
href="https://redirect.github.com/facebook/react/pull/36964">#36964</a>)</li>
<li>Fix Fast Refresh crash when an edit changes the kind of a
component's type (<a
href="https://github.com/sophiebits"><code>@​sophiebits</code></a> <a
href="https://redirect.github.com/facebook/react/pull/36963">#36963</a>)</li>
<li>Unify hot reload type resolution for Fast Refresh (<a
href="https://github.com/sophiebits"><code>@​sophiebits</code></a> <a
href="https://redirect.github.com/facebook/react/pull/36962">#36962</a>)</li>
<li>Fix Fast Refresh to remount correctly when an edit changes the
component kind (<a
href="https://github.com/sophiebits"><code>@​sophiebits</code></a> <a
href="https://redirect.github.com/facebook/react/pull/36950">#36950</a>)</li>
<li>Double invoke effects in StrictMode after Fast Refresh (<a
href="https://github.com/eps1lon"><code>@​eps1lon</code></a> <a
href="https://redirect.github.com/facebook/react/pull/35962">#35962</a>)</li>
</ul>
</li>
<li>Performance Track Fixes
<ul>
<li>Prevent crash when accessing <code>$$typeof</code> in Performance
Tracks (<a href="https://github.com/eps1lon"><code>@​eps1lon</code></a>
<a
href="https://redirect.github.com/facebook/react/pull/35679">#35679</a>)</li>
<li>Handle non-string function names in Performance Tracks (<a
href="https://github.com/eps1lon"><code>@​eps1lon</code></a> <a
href="https://redirect.github.com/facebook/react/pull/35659">#35659</a>)</li>
<li>Use minus (<code>-</code>) instead of en dash for removed props in
Performance Tracks (<a
href="https://github.com/eps1lon"><code>@​eps1lon</code></a> <a
href="https://redirect.github.com/facebook/react/pull/35649">#35649</a>)</li>
<li>Handle arrays with bigints in deep objects in Performance Tracks (<a
href="https://github.com/eps1lon"><code>@​eps1lon</code></a> <a
href="https://redirect.github.com/facebook/react/pull/35648">#35648</a>)</li>
<li>Don't enumerate typed array props in Performance Tracks in DEV (<a
href="https://github.com/UditDewan"><code>@​UditDewan</code></a> <a
href="https://redirect.github.com/facebook/react/pull/36913">#36913</a>)</li>
<li>Bail out of diffing wide objects and arrays in Performance Tracks
(<a href="https://github.com/eps1lon"><code>@​eps1lon</code></a> <a
href="https://redirect.github.com/facebook/react/pull/34742">#34742</a>)</li>
<li>Clear potentially large performance measures in DEV (<a
href="https://github.com/hoxyq"><code>@​hoxyq</code></a> <a
href="https://redirect.github.com/facebook/react/pull/34803">#34803</a>)</li>
<li>Fix missing else branch for renders with no props change in
Performance Tracks (<a
href="https://github.com/hoxyq"><code>@​hoxyq</code></a> <a
href="https://redirect.github.com/facebook/react/pull/34837">#34837</a>)</li>
</ul>
</li>
<li>Activity Fixes
<ul>
<li>Fix <code>useSyncExternalStore</code> missing store mutations that
happened while an Activity tree was hidden (<a
href="https://github.com/sophiebits"><code>@​sophiebits</code></a> <a
href="https://redirect.github.com/facebook/react/pull/36947">#36947</a>)</li>
<li>Hide portal contents when an Activity is hidden (<a
href="https://github.com/acdlite"><code>@​acdlite</code></a> <a
href="https://redirect.github.com/facebook/react/pull/35091">#35091</a>)</li>
<li>Prevent metadata hoisting in hidden <code>&lt;Activity&gt;</code>
trees (<a
href="https://github.com/ronnakamoto"><code>@​ronnakamoto</code></a> <a
href="https://redirect.github.com/facebook/react/pull/34983">#34983</a>)</li>
<li>Prevent errors thrown inside a hidden Activity from escaping to the
visible UI (<a
href="https://github.com/acdlite"><code>@​acdlite</code></a> <a
href="https://redirect.github.com/facebook/react/pull/35074">#35074</a>)</li>
<li>Don't unhide a node if a direct parent Offscreen is still hidden (<a
href="https://github.com/sebmarkbage"><code>@​sebmarkbage</code></a> <a
href="https://redirect.github.com/facebook/react/pull/34821">#34821</a>)</li>
<li>Don't show internal <code>&lt;Offscreen&gt;</code> component in
error messages (<a
href="https://github.com/rickhanlonii"><code>@​rickhanlonii</code></a>
<a
href="https://redirect.github.com/facebook/react/pull/35763">#35763</a>)</li>
</ul>
</li>
<li>Warn in DEV when a component appears to have been unblocked by a
conditional <code>use()</code> (<a
href="https://github.com/hoxyq"><code>@​hoxyq</code></a>, <a
href="https://github.com/eps1lon"><code>@​eps1lon</code></a>: <a
href="https://redirect.github.com/facebook/react/pull/37104">#37104</a>,
<a
href="https://redirect.github.com/facebook/react/pull/37203">#37203</a>,
<a
href="https://redirect.github.com/facebook/react/pull/37491">#37491</a>)</li>
<li>Render transitions independently instead of entangling them into a
single render (<a
href="https://github.com/acdlite"><code>@​acdlite</code></a> <a
href="https://redirect.github.com/facebook/react/pull/37290">#37290</a>)</li>
<li>Fix hang when updating a dehydrated boundary inside a hidden tree
(<a href="https://github.com/gaearon"><code>@​gaearon</code></a> <a
href="https://redirect.github.com/facebook/react/pull/37135">#37135</a>)</li>
<li>Don't reacquire Host Singletons during dev effect validation (<a
href="https://github.com/gnoff"><code>@​gnoff</code></a> <a
href="https://redirect.github.com/facebook/react/pull/37113">#37113</a>)</li>
<li>Only remove properties from Host Singletons on release (<a
href="https://github.com/gnoff"><code>@​gnoff</code></a> <a
href="https://redirect.github.com/facebook/react/pull/37112">#37112</a>)</li>
</ul>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/react/react/commit/2dc7da790d6388b95b83198ca9b588b2ad5f5c0b"><code>2dc7da7</code></a>
[test] Bump Jest to 30.4 (<a
href="https://github.com/react/react/tree/HEAD/packages/react/issues/37382">#37382</a>)</li>
<li><a
href="https://github.com/react/react/commit/4f9389423b7319e1f7acc3d158c84a8365462748"><code>4f93894</code></a>
docs: remove stale parentType param from validateChildKeys JSDoc (<a
href="https://github.com/react/react/tree/HEAD/packages/react/issues/36928">#36928</a>)</li>
<li><a
href="https://github.com/react/react/commit/dbc37501ffeaf8fec45af5898caf1c3d64ad10bf"><code>dbc3750</code></a>
Update required references to GitHub repo (<a
href="https://github.com/react/react/tree/HEAD/packages/react/issues/36752">#36752</a>)</li>
<li><a
href="https://github.com/react/react/commit/900ae094d85b11c67d53dd14af50a2bda5db4495"><code>900ae09</code></a>
[flow] Bump flow to v0.317.0 (<a
href="https://github.com/react/react/tree/HEAD/packages/react/issues/36701">#36701</a>)</li>
<li><a
href="https://github.com/react/react/commit/fbb137059e4aacfaab1d36516e9b55050b4a0454"><code>fbb1370</code></a>
[flow] Bump flow to v0.307.1 (<a
href="https://github.com/react/react/tree/HEAD/packages/react/issues/36199">#36199</a>)</li>
<li><a
href="https://github.com/react/react/commit/56922cf751fab6c7ab4c12ddbbd15839959fa255"><code>56922cf</code></a>
[react-native-renderer] Delete Paper (legacy) renderer (<a
href="https://github.com/react/react/tree/HEAD/packages/react/issues/36285">#36285</a>)</li>
<li><a
href="https://github.com/react/react/commit/74568e8627aa43469b74f2972f427a209639d0b6"><code>74568e8</code></a>
[Flight] Transport <code>AggregateErrors.errors</code> (<a
href="https://github.com/react/react/tree/HEAD/packages/react/issues/36156">#36156</a>)</li>
<li><a
href="https://github.com/react/react/commit/e66ef6480ecd19c6885f2c06dec34fec1fdc0a98"><code>e66ef64</code></a>
[tests] remove withoutStack from assertConsole helpers (<a
href="https://github.com/react/react/tree/HEAD/packages/react/issues/35498">#35498</a>)</li>
<li><a
href="https://github.com/react/react/commit/db71391c5c70dc113560d1c23d0b6548604d827f"><code>db71391</code></a>
[Fiber] Instrument the lazy initializer thenable in all cases (<a
href="https://github.com/react/react/tree/HEAD/packages/react/issues/35521">#35521</a>)</li>
<li><a
href="https://github.com/react/react/commit/3e1abcc8d7083a13adf4774feb0d67ecbe4a2bc4"><code>3e1abcc</code></a>
[tests] Require exact error messages in assertConsole helpers (<a
href="https://github.com/react/react/tree/HEAD/packages/react/issues/35497">#35497</a>)</li>
<li>Additional commits viewable in <a
href="https://github.com/react/react/commits/v19.3.0/packages/react">compare
view</a></li>
</ul>
</details>
<br />

Updates `@types/react` from 19.2.18 to 19.3.0
<details>
<summary>Commits</summary>
<ul>
<li>See full diff in <a
href="https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/react">compare
view</a></li>
</ul>
</details>
<br />

Updates `react-dom` from 19.2.8 to 19.3.0
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/react/react/releases">react-dom's
releases</a>.</em></p>
<blockquote>
<h2>19.3.0 (September 9, 2026)</h2>
<p>Below is a list of all new features, APIs, and bug fixes.</p>
<p>Read the <a href="https://react.dev/blog/2026/09/09/react-19-3">React
19.3 release post</a> for more information.</p>
<h2>New React Features</h2>
<ul>
<li><code>&lt;ViewTransition /&gt;</code>: Adds <code>&lt;ViewTransition
/&gt;</code> and <code>addTransitionType</code> APIs to power View
Transition animations in React (<a
href="https://github.com/sebmarkbage"><code>@​sebmarkbage</code></a>, <a
href="https://github.com/jackpope"><code>@​jackpope</code></a>, <a
href="https://github.com/gaearon"><code>@​gaearon</code></a>: <a
href="https://redirect.github.com/facebook/react/pull/31975">#31975</a>,
<a
href="https://redirect.github.com/facebook/react/pull/31987">#31987</a>,
<a
href="https://redirect.github.com/facebook/react/pull/31996">#31996</a>,
<a
href="https://redirect.github.com/facebook/react/pull/31999">#31999</a>,
<a
href="https://redirect.github.com/facebook/react/pull/32001">#32001</a>,
<a
href="https://redirect.github.com/facebook/react/pull/32002">#32002</a>,
<a
href="https://redirect.github.com/facebook/react/pull/32028">#32028</a>,
<a
href="https://redirect.github.com/facebook/react/pull/32029">#32029</a>,
<a
href="https://redirect.github.com/facebook/react/pull/32031">#32031</a>,
<a
href="https://redirect.github.com/facebook/react/pull/32034">#32034</a>,
<a
href="https://redirect.github.com/facebook/react/pull/32038">#32038</a>,
<a
href="https://redirect.github.com/facebook/react/pull/32041">#32041</a>,
<a
href="https://redirect.github.com/facebook/react/pull/32050">#32050</a>,
<a
href="https://redirect.github.com/facebook/react/pull/32090">#32090</a>,
<a
href="https://redirect.github.com/facebook/react/pull/32105">#32105</a>,
<a
href="https://redirect.github.com/facebook/react/pull/32254">#32254</a>,
<a
href="https://redirect.github.com/facebook/react/pull/32379">#32379</a>,
<a
href="https://redirect.github.com/facebook/react/pull/32422">#32422</a>,
<a
href="https://redirect.github.com/facebook/react/pull/32462">#32462</a>,
<a
href="https://redirect.github.com/facebook/react/pull/32540">#32540</a>,
<a
href="https://redirect.github.com/facebook/react/pull/32545">#32545</a>,
<a
href="https://redirect.github.com/facebook/react/pull/32585">#32585</a>,
<a
href="https://redirect.github.com/facebook/react/pull/32599">#32599</a>,
<a
href="https://redirect.github.com/facebook/react/pull/32611">#32611</a>,
<a
href="https://redirect.github.com/facebook/react/pull/32612">#32612</a>,
<a
href="https://redirect.github.com/facebook/react/pull/32617">#32617</a>,
<a
href="https://redirect.github.com/facebook/react/pull/32651">#32651</a>,
<a
href="https://redirect.github.com/facebook/react/pull/32653">#32653</a>,
<a
href="https://redirect.github.com/facebook/react/pull/32656">#32656</a>,
<a
href="https://redirect.github.com/facebook/react/pull/32664">#32664</a>,
<a
href="https://redirect.github.com/facebook/react/pull/32699">#32699</a>,
<a
href="https://redirect.github.com/facebook/react/pull/32723">#32723</a>,
<a
href="https://redirect.github.com/facebook/react/pull/32734">#32734</a>,
<a
href="https://redirect.github.com/facebook/react/pull/32751">#32751</a>,
<a
href="https://redirect.github.com/facebook/react/pull/32752">#32752</a>,
<a
href="https://redirect.github.com/facebook/react/pull/32760">#32760</a>,
<a
href="https://redirect.github.com/facebook/react/pull/32761">#32761</a>,
<a
href="https://redirect.github.com/facebook/react/pull/32764">#32764</a>,
<a
href="https://redirect.github.com/facebook/react/pull/32772">#32772</a>,
<a
href="https://redirect.github.com/facebook/react/pull/32790">#32790</a>,
<a
href="https://redirect.github.com/facebook/react/pull/32819">#32819</a>,
<a
href="https://redirect.github.com/facebook/react/pull/32820">#32820</a>,
<a
href="https://redirect.github.com/facebook/react/pull/32822">#32822</a>,
<a
href="https://redirect.github.com/facebook/react/pull/32833">#32833</a>,
<a
href="https://redirect.github.com/facebook/react/pull/32849">#32849</a>,
<a
href="https://redirect.github.com/facebook/react/pull/33094">#33094</a>,
<a
href="https://redirect.github.com/facebook/react/pull/33191">#33191</a>,
<a
href="https://redirect.github.com/facebook/react/pull/33200">#33200</a>,
<a
href="https://redirect.github.com/facebook/react/pull/33206">#33206</a>,
<a
href="https://redirect.github.com/facebook/react/pull/33293">#33293</a>,
<a
href="https://redirect.github.com/facebook/react/pull/33330">#33330</a>,
<a
href="https://redirect.github.com/facebook/react/pull/33331">#33331</a>,
<a
href="https://redirect.github.com/facebook/react/pull/33332">#33332</a>,
<a
href="https://redirect.github.com/facebook/react/pull/33357">#33357</a>,
<a
href="https://redirect.github.com/facebook/react/pull/33362">#33362</a>,
<a
href="https://redirect.github.com/facebook/react/pull/33433">#33433</a>,
<a
href="https://redirect.github.com/facebook/react/pull/33576">#33576</a>,
<a
href="https://redirect.github.com/facebook/react/pull/34374">#34374</a>,
<a
href="https://redirect.github.com/facebook/react/pull/34450">#34450</a>,
<a
href="https://redirect.github.com/facebook/react/pull/34481">#34481</a>,
<a
href="https://redirect.github.com/facebook/react/pull/34500">#34500</a>,
<a
href="https://redirect.github.com/facebook/react/pull/34502">#34502</a>,
<a
href="https://redirect.github.com/facebook/react/pull/34510">#34510</a>,
<a
href="https://redirect.github.com/facebook/react/pull/34511">#34511</a>,
<a
href="https://redirect.github.com/facebook/react/pull/34539">#34539</a>,
<a
href="https://redirect.github.com/facebook/react/pull/35567">#35567</a>,
<a
href="https://redirect.github.com/facebook/react/pull/35564">#35564</a>,
<a
href="https://redirect.github.com/facebook/react/pull/35485">#35485</a>,
<a
href="https://redirect.github.com/facebook/react/pull/35380">#35380</a>,
<a
href="https://redirect.github.com/facebook/react/pull/35063">#35063</a>,
<a
href="https://redirect.github.com/facebook/react/pull/35060">#35060</a>,
<a
href="https://redirect.github.com/facebook/react/pull/34676">#34676</a>,
<a
href="https://redirect.github.com/facebook/react/pull/36917">#36917</a>,
<a
href="https://redirect.github.com/facebook/react/pull/35337">#35337</a>,
<a
href="https://redirect.github.com/facebook/react/pull/35520">#35520</a>)</li>
<li>Fragment Refs: Add Refs to <code>&lt;Fragment /&gt;</code> to
support composable platform behavior (<a
href="https://github.com/jackpope"><code>@​jackpope</code></a>, <a
href="https://github.com/sebmarkbage"><code>@​sebmarkbage</code></a>, <a
href="https://github.com/eps1lon"><code>@​eps1lon</code></a>, <a
href="https://github.com/Dhakshin2007"><code>@​Dhakshin2007</code></a>,
<a href="https://github.com/chirokas"><code>@​chirokas</code></a>, <a
href="https://github.com/teamleaderleo"><code>@​teamleaderleo</code></a>,
<a
href="https://github.com/fallintoplace"><code>@​fallintoplace</code></a>:
<a
href="https://redirect.github.com/facebook/react/pull/32465">#32465</a>,
<a
href="https://redirect.github.com/facebook/react/pull/32613">#32613</a>,
<a
href="https://redirect.github.com/facebook/react/pull/32619">#32619</a>,
<a
href="https://redirect.github.com/facebook/react/pull/32654">#32654</a>,
<a
href="https://redirect.github.com/facebook/react/pull/32660">#32660</a>,
<a
href="https://redirect.github.com/facebook/react/pull/32682">#32682</a>,
<a
href="https://redirect.github.com/facebook/react/pull/32722">#32722</a>,
<a
href="https://redirect.github.com/facebook/react/pull/32813">#32813</a>,
<a
href="https://redirect.github.com/facebook/react/pull/32814">#32814</a>,
<a
href="https://redirect.github.com/facebook/react/pull/33056">#33056</a>,
<a
href="https://redirect.github.com/facebook/react/pull/33058">#33058</a>,
<a
href="https://redirect.github.com/facebook/react/pull/33093">#33093</a>,
<a
href="https://redirect.github.com/facebook/react/pull/34069">#34069</a>,
<a
href="https://redirect.github.com/facebook/react/pull/34103">#34103</a>,
<a
href="https://redirect.github.com/facebook/react/pull/34544">#34544</a>,
<a
href="https://redirect.github.com/facebook/react/pull/34545">#34545</a>,
<a
href="https://redirect.github.com/facebook/react/pull/37062">#37062</a>,
<a
href="https://redirect.github.com/facebook/react/pull/37061">#37061</a>,
<a
href="https://redirect.github.com/facebook/react/pull/37060">#37060</a>,
<a
href="https://redirect.github.com/facebook/react/pull/36047">#36047</a>,
<a
href="https://redirect.github.com/facebook/react/pull/36010">#36010</a>,
<a
href="https://redirect.github.com/facebook/react/pull/35642">#35642</a>,
<a
href="https://redirect.github.com/facebook/react/pull/35641">#35641</a>,
<a
href="https://redirect.github.com/facebook/react/pull/35637">#35637</a>,
<a
href="https://redirect.github.com/facebook/react/pull/35630">#35630</a>,
<a
href="https://redirect.github.com/facebook/react/pull/34935">#34935</a>,
<a
href="https://redirect.github.com/facebook/react/pull/37457">#37457</a>,
<a
href="https://redirect.github.com/facebook/react/pull/37408">#37408</a>,
<a
href="https://redirect.github.com/facebook/react/pull/37326">#37326</a>,
<a
href="https://redirect.github.com/facebook/react/pull/37251">#37251</a>,
<a
href="https://redirect.github.com/facebook/react/pull/37171">#37171</a>,
<a
href="https://redirect.github.com/facebook/react/pull/37169">#37169</a>,
<a
href="https://redirect.github.com/facebook/react/pull/37168">#37168</a>,
<a
href="https://redirect.github.com/facebook/react/pull/37167">#37167</a>,
<a
href="https://redirect.github.com/facebook/react/pull/37166">#37166</a>,
<a
href="https://redirect.github.com/facebook/react/pull/37165">#37165</a>,
<a
href="https://redirect.github.com/facebook/react/pull/37164">#37164</a>,
<a
href="https://redirect.github.com/facebook/react/pull/37163">#37163</a>,
<a
href="https://redirect.github.com/facebook/react/pull/37162">#37162</a>,
<a
href="https://redirect.github.com/facebook/react/pull/37161">#37161</a>,
<a
href="https://redirect.github.com/facebook/react/pull/37160">#37160</a>,
<a
href="https://redirect.github.com/facebook/react/pull/37125">#37125</a>,
<a
href="https://redirect.github.com/facebook/react/pull/37063">#37063</a>)</li>
</ul>
<h2>New React DOM Features</h2>
<ul>
<li><code>browser()</code>: a new <code>react-dom</code> API that
returns a usable which errors during server rendering and resolves in
the browser. <code>use(browser())</code> inside a
<code>&lt;Suspense&gt;</code> boundary marks a subtree as browser-only
without reporting a recoverable error (<a
href="https://github.com/gnoff"><code>@​gnoff</code></a>: <a
href="https://redirect.github.com/facebook/react/pull/37143">#37143</a>,
<a
href="https://redirect.github.com/facebook/react/pull/37241">#37241</a>)
<ul>
<li>Added an <code>onBrowserBailout</code> option to the
<code>react-dom/server</code> APIs to observe when a subtree defers to
the browser (<a href="https://github.com/gnoff"><code>@​gnoff</code></a>
<a
href="https://redirect.github.com/facebook/react/pull/37193">#37193</a>)</li>
</ul>
</li>
</ul>
<h2>Notable changes</h2>
<ul>
<li>Enable Trusted Types API integration (<a
href="https://github.com/rickhanlonii"><code>@​rickhanlonii</code></a>
<a
href="https://redirect.github.com/facebook/react/pull/35816">#35816</a>)</li>
<li>Transitions now render independently instead of being entangled into
a single render, so a slow transition no longer holds up unrelated ones
(<a href="https://github.com/acdlite"><code>@​acdlite</code></a> <a
href="https://redirect.github.com/facebook/react/pull/37290">#37290</a>)</li>
<li>Added a DEV-only warning when a component appears to have been
unblocked by calling <code>use()</code> conditionally (<a
href="https://github.com/hoxyq"><code>@​hoxyq</code></a>, <a
href="https://github.com/eps1lon"><code>@​eps1lon</code></a>: <a
href="https://redirect.github.com/facebook/react/pull/37104">#37104</a>,
<a
href="https://redirect.github.com/facebook/react/pull/37203">#37203</a>,
<a
href="https://redirect.github.com/facebook/react/pull/37491">#37491</a>)</li>
</ul>
<h2>All Changes</h2>
<h3>React</h3>
<ul>
<li>Fast Refresh Fixes
<ul>
<li>Fix Fast Refresh to find and remount edits to components wrapped
behind <code>lazy()</code> (<a
href="https://github.com/sophiebits"><code>@​sophiebits</code></a> <a
href="https://redirect.github.com/facebook/react/pull/36965">#36965</a>)</li>
<li>Fix Fast Refresh so edits to a <code>memo()</code> comparison
function take effect (<a
href="https://github.com/sophiebits"><code>@​sophiebits</code></a> <a
href="https://redirect.github.com/facebook/react/pull/36964">#36964</a>)</li>
<li>Fix Fast Refresh crash when an edit changes the kind of a
component's type (<a
href="https://github.com/sophiebits"><code>@​sophiebits</code></a> <a
href="https://redirect.github.com/facebook/react/pull/36963">#36963</a>)</li>
<li>Unify hot reload type resolution for Fast Refresh (<a
href="https://github.com/sophiebits"><code>@​sophiebits</code></a> <a
href="https://redirect.github.com/facebook/react/pull/36962">#36962</a>)</li>
<li>Fix Fast Refresh to remount correctly when an edit changes the
component kind (<a
href="https://github.com/sophiebits"><code>@​sophiebits</code></a> <a
href="https://redirect.github.com/facebook/react/pull/36950">#36950</a>)</li>
<li>Double invoke effects in StrictMode after Fast Refresh (<a
href="https://github.com/eps1lon"><code>@​eps1lon</code></a> <a
href="https://redirect.github.com/facebook/react/pull/35962">#35962</a>)</li>
</ul>
</li>
<li>Performance Track Fixes
<ul>
<li>Prevent crash when accessing <code>$$typeof</code> in Performance
Tracks (<a href="https://github.com/eps1lon"><code>@​eps1lon</code></a>
<a
href="https://redirect.github.com/facebook/react/pull/35679">#35679</a>)</li>
<li>Handle non-string function names in Performance Tracks (<a
href="https://github.com/eps1lon"><code>@​eps1lon</code></a> <a
href="https://redirect.github.com/facebook/react/pull/35659">#35659</a>)</li>
<li>Use minus (<code>-</code>) instead of en dash for removed props in
Performance Tracks (<a
href="https://github.com/eps1lon"><code>@​eps1lon</code></a> <a
href="https://redirect.github.com/facebook/react/pull/35649">#35649</a>)</li>
<li>Handle arrays with bigints in deep objects in Performance Tracks (<a
href="https://github.com/eps1lon"><code>@​eps1lon</code></a> <a
href="https://redirect.github.com/facebook/react/pull/35648">#35648</a>)</li>
<li>Don't enumerate typed array props in Performance Tracks in DEV (<a
href="https://github.com/UditDewan"><code>@​UditDewan</code></a> <a
href="https://redirect.github.com/facebook/react/pull/36913">#36913</a>)</li>
<li>Bail out of diffing wide objects and arrays in Performance Tracks
(<a href="https://github.com/eps1lon"><code>@​eps1lon</code></a> <a
href="https://redirect.github.com/facebook/react/pull/34742">#34742</a>)</li>
<li>Clear potentially large performance measures in DEV (<a
href="https://github.com/hoxyq"><code>@​hoxyq</code></a> <a
href="https://redirect.github.com/facebook/react/pull/34803">#34803</a>)</li>
<li>Fix missing else branch for renders with no props change in
Performance Tracks (<a
href="https://github.com/hoxyq"><code>@​hoxyq</code></a> <a
href="https://redirect.github.com/facebook/react/pull/34837">#34837</a>)</li>
</ul>
</li>
<li>Activity Fixes
<ul>
<li>Fix <code>useSyncExternalStore</code> missing store mutations that
happened while an Activity tree was hidden (<a
href="https://github.com/sophiebits"><code>@​sophiebits</code></a> <a
href="https://redirect.github.com/facebook/react/pull/36947">#36947</a>)</li>
<li>Hide portal contents when an Activity is hidden (<a
href="https://github.com/acdlite"><code>@​acdlite</code></a> <a
href="https://redirect.github.com/facebook/react/pull/35091">#35091</a>)</li>
<li>Prevent metadata hoisting in hidden <code>&lt;Activity&gt;</code>
trees (<a
href="https://github.com/ronnakamoto"><code>@​ronnakamoto</code></a> <a
href="https://redirect.github.com/facebook/react/pull/34983">#34983</a>)</li>
<li>Prevent errors thrown inside a hidden Activity from escaping to the
visible UI (<a
href="https://github.com/acdlite"><code>@​acdlite</code></a> <a
href="https://redirect.github.com/facebook/react/pull/35074">#35074</a>)</li>
<li>Don't unhide a node if a direct parent Offscreen is still hidden (<a
href="https://github.com/sebmarkbage"><code>@​sebmarkbage</code></a> <a
href="https://redirect.github.com/facebook/react/pull/34821">#34821</a>)</li>
<li>Don't show internal <code>&lt;Offscreen&gt;</code> component in
error messages (<a
href="https://github.com/rickhanlonii"><code>@​rickhanlonii</code></a>
<a
href="https://redirect.github.com/facebook/react/pull/35763">#35763</a>)</li>
</ul>
</li>
<li>Warn in DEV when a component appears to have been unblocked by a
conditional <code>use()</code> (<a
href="https://github.com/hoxyq"><code>@​hoxyq</code></a>, <a
href="https://github.com/eps1lon"><code>@​eps1lon</code></a>: <a
href="https://redirect.github.com/facebook/react/pull/37104">#37104</a>,
<a
href="https://redirect.github.com/facebook/react/pull/37203">#37203</a>,
<a
href="https://redirect.github.com/facebook/react/pull/37491">#37491</a>)</li>
<li>Render transitions independently instead of entangling them into a
single render (<a
href="https://github.com/acdlite"><code>@​acdlite</code></a> <a
href="https://redirect.github.com/facebook/react/pull/37290">#37290</a>)</li>
</ul>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/react/react/blob/main/CHANGELOG.md">react-dom's
changelog</a>.</em></p>
<blockquote>
<h2>19.3.0 (September 9, 2026)</h2>
<h3>New React Features</h3>
<ul>
<li><code>&lt;ViewTransition /&gt;</code>: Adds <code>&lt;ViewTransition
/&gt;</code> and <code>addTransitionType</code> APIs to power View
Transition animations in React (<a
href="https://github.com/sebmarkbage"><code>@​sebmarkbage</code></a>, <a
href="https://github.com/jackpope"><code>@​jackpope</code></a>, <a
href="https://github.com/gaearon"><code>@​gaearon</code></a>: <a
href="https://redirect.github.com/facebook/react/pull/31975">#31975</a>,
<a
href="https://redirect.github.com/facebook/react/pull/31987">#31987</a>,
<a
href="https://redirect.github.com/facebook/react/pull/31996">#31996</a>,
<a
href="https://redirect.github.com/facebook/react/pull/31999">#31999</a>,
<a
href="https://redirect.github.com/facebook/react/pull/32001">#32001</a>,
<a
href="https://redirect.github.com/facebook/react/pull/32002">#32002</a>,
<a
href="https://redirect.github.com/facebook/react/pull/32028">#32028</a>,
<a
href="https://redirect.github.com/facebook/react/pull/32029">#32029</a>,
<a
href="https://redirect.github.com/facebook/react/pull/32031">#32031</a>,
<a
href="https://redirect.github.com/facebook/react/pull/32034">#32034</a>,
<a
href="https://redirect.github.com/facebook/react/pull/32038">#32038</a>,
<a
href="https://redirect.github.com/facebook/react/pull/32041">#32041</a>,
<a
href="https://redirect.github.com/facebook/react/pull/32050">#32050</a>,
<a
href="https://redirect.github.com/facebook/react/pull/32090">#32090</a>,
<a
href="https://redirect.github.com/facebook/react/pull/32105">#32105</a>,
<a
href="https://redirect.github.com/facebook/react/pull/32254">#32254</a>,
<a
href="https://redirect.github.com/facebook/react/pull/32379">#32379</a>,
<a
href="https://redirect.github.com/facebook/react/pull/32422">#32422</a>,
<a
href="https://redirect.github.com/facebook/react/pull/32462">#32462</a>,
<a
href="https://redirect.github.com/facebook/react/pull/32540">#32540</a>,
<a
href="https://redirect.github.com/facebook/react/pull/32545">#32545</a>,
<a
href="https://redirect.github.com/facebook/react/pull/32585">#32585</a>,
<a
href="https://redirect.github.com/facebook/react/pull/32599">#32599</a>,
<a
href="https://redirect.github.com/facebook/react/pull/32611">#32611</a>,
<a
href="https://redirect.github.com/facebook/react/pull/32612">#32612</a>,
<a
href="https://redirect.github.com/facebook/react/pull/32617">#32617</a>,
<a
href="https://redirect.github.com/facebook/react/pull/32651">#32651</a>,
<a
href="https://redirect.github.com/facebook/react/pull/32653">#32653</a>,
<a
href="https://redirect.github.com/facebook/react/pull/32656">#32656</a>,
<a
href="https://redirect.github.com/facebook/react/pull/32664">#32664</a>,
<a
href="https://redirect.github.com/facebook/react/pull/32699">#32699</a>,
<a
href="https://redirect.github.com/facebook/react/pull/32723">#32723</a>,
<a
href="https://redirect.github.com/facebook/react/pull/32734">#32734</a>,
<a
href="https://redirect.github.com/facebook/react/pull/32751">#32751</a>,
<a
href="https://redirect.github.com/facebook/react/pull/32752">#32752</a>,
<a
href="https://redirect.github.com/facebook/react/pull/32760">#32760</a>,
<a
href="https://redirect.github.com/facebook/react/pull/32761">#32761</a>,
<a
href="https://redirect.github.com/facebook/react/pull/32764">#32764</a>,
<a
href="https://redirect.github.com/facebook/react/pull/32772">#32772</a>,
<a
href="https://redirect.github.com/facebook/react/pull/32790">#32790</a>,
<a
href="https://redirect.github.com/facebook/react/pull/32819">#32819</a>,
<a
href="https://redirect.github.com/facebook/react/pull/32820">#32820</a>,
<a
href="https://redirect.github.com/facebook/react/pull/32822">#32822</a>,
<a
href="https://redirect.github.com/facebook/react/pull/32833">#32833</a>,
<a
href="https://redirect.github.com/facebook/react/pull/32849">#32849</a>,
<a
href="https://redirect.github.com/facebook/react/pull/33094">#33094</a>,
<a
href="https://redirect.github.com/facebook/react/pull/33191">#33191</a>,
<a
href="https://redirect.github.com/facebook/react/pull/33200">#33200</a>,
<a
href="https://redirect.github.com/facebook/react/pull/33206">#33206</a>,
<a
href="https://redirect.github.com/facebook/react/pull/33293">#33293</a>,
<a
href="https://redirect.github.com/facebook/react/pull/33330">#33330</a>,
<a
href="https://redirect.github.com/facebook/react/pull/33331">#33331</a>,
<a
href="https://redirect.github.com/facebook/react/pull/33332">#33332</a>,
<a
href="https://redirect.github.com/facebook/react/pull/33357">#33357</a>,
<a
href="https://redirect.github.com/facebook/react/pull/33362">#33362</a>,
<a
href="https://redirect.github.com/facebook/react/pull/33433">#33433</a>,
<a
href="https://redirect.github.com/facebook/react/pull/33576">#33576</a>,
<a
href="https://redirect.github.com/facebook/react/pull/34374">#34374</a>,
<a
href="https://redirect.github.com/facebook/react/pull/34450">#34450</a>,
<a
href="https://redirect.github.com/facebook/react/pull/34481">#34481</a>,
<a
href="https://redirect.github.com/facebook/react/pull/34500">#34500</a>,
<a
href="https://redirect.github.com/facebook/react/pull/34502">#34502</a>,
<a
href="https://redirect.github.com/facebook/react/pull/34510">#34510</a>,
<a
href="https://redirect.github.com/facebook/react/pull/34511">#34511</a>,
<a
href="https://redirect.github.com/facebook/react/pull/34539">#34539</a>,
<a
href="https://redirect.github.com/facebook/react/pull/35567">#35567</a>,
<a
href="https://redirect.github.com/facebook/react/pull/35564">#35564</a>,
<a
href="https://redirect.github.com/facebook/react/pull/35485">#35485</a>,
<a
href="https://redirect.github.com/facebook/react/pull/35380">#35380</a>,
<a
href="https://redirect.github.com/facebook/react/pull/35063">#35063</a>,
<a
href="https://redirect.github.com/facebook/react/pull/35060">#35060</a>,
<a
href="https://redirect.github.com/facebook/react/pull/34676">#34676</a>,
<a
href="https://redirect.github.com/facebook/react/pull/36917">#36917</a>,
<a
href="https://redirect.github.com/facebook/react/pull/35337">#35337</a>,
<a
href="https://redirect.github.com/facebook/react/pull/35520">#35520</a>)</li>
<li>Fragment Refs: Add Refs to <code>&lt;Fragment /&gt;</code> to
support composable platform behavior (<a
href="https://github.com/jackpope"><code>@​jackpope</code></a>, <a
href="https://github.com/sebmarkbage"><code>@​sebmarkbage</code></a>, <a
href="https://github.com/eps1lon"><code>@​eps1lon</code></a>, <a
href="https://github.com/Dhakshin2007"><code>@​Dhakshin2007</code></a>,
<a href="https://github.com/chirokas"><code>@​chirokas</code></a>, <a
href="https://github.com/teamleaderleo"><code>@​teamleaderleo</code></a>,
<a
href="https://github.com/fallintoplace"><code>@​fallintoplace</code></a>:
<a
href="https://redirect.github.com/facebook/react/pull/32465">#32465</a>,
<a
href="https://redirect.github.com/facebook/react/pull/32613">#32613</a>,
<a
href="https://redirect.github.com/facebook/react/pull/32619">#32619</a>,
<a
href="https://redirect.github.com/facebook/react/pull/32654">#32654</a>,
<a
href="https://redirect.github.com/facebook/react/pull/32660">#32660</a>,
<a
href="https://redirect.github.com/facebook/react/pull/32682">#32682</a>,
<a
href="https://redirect.github.com/facebook/react/pull/32722">#32722</a>,
<a
href="https://redirect.github.com/facebook/react/pull/32813">#32813</a>,
<a
href="https://redirect.github.com/facebook/react/pull/32814">#32814</a>,
<a
href="https://redirect.github.com/facebook/react/pull/33056">#33056</a>,
<a
href="https://redirect.github.com/facebook/react/pull/33058">#33058</a>,
<a
href="https://redirect.github.com/facebook/react/pull/33093">#33093</a>,
<a
href="https://redirect.github.com/facebook/react/pull/34069">#34069</a>,
<a
href="https://redirect.github.com/facebook/react/pull/34103">#34103</a>,
<a
href="https://redirect.github.com/facebook/react/pull/34544">#34544</a>,
<a
href="https://redirect.github.com/facebook/react/pull/34545">#34545</a>,
<a
href="https://redirect.github.com/facebook/react/pull/37062">#37062</a>,
<a
href="https://redirect.github.com/fac…
## Problem

In light mode with `studio-modern`, album titles in a subpage's cover
grid are nearly unreadable. For example, "An der Ostsee" on
`/deutschland` shows as dark text on a dark grey-green bar.

## Cause

`SubpageGridView` sets the cover's dominant colour (the loading
placeholder) as an inline `background-color` on the whole card.
`studio-modern` adds a caption bar below the image and gives the card
`background: var(--bg-card)`, but the inline style wins. So the caption
bar takes the photo's colour. The caption title uses `--text-primary`,
which is dark in light mode, so it ends up dark on dark. In dark mode it
happens to stay readable, because most dominant colours are dark.

This dates back to March and has nothing to do with the recent contrast
work.

## Fix

The inline colour moves from the card to `.subpage-grid__item-media`,
the image box, where it serves its purpose as a placeholder behind the
loading cover.

Every other preset fills the card with the media box (`height: 100%`)
and hides the caption, so nothing changes visually there.

## Checked

- `npx tsc --noEmit`: 0 errors
- ESLint and Prettier: clean on the changed file

Co-authored-by: ralksta <ralksta@users.noreply.github.com>
…#580)

## Why

Follow-up to #578. I ran an axe `color-contrast` audit in a real browser
across all seven presets in light mode, on `/`, a subpage, an album,
`/about`, `/map` and `/impressum`. It found **129 failures**, all
traceable to four spots. In three of them, an `opacity` sits on top of a
colour token that #571 had already tuned to exactly 4.5:1.

| Where | Presets | Before |
|---|---|---|
| Footer "Impressum" link: `opacity: 0.5` on `--text-muted` | all except
studio-modern | ~2:1 |
| `/impressum` subtitle, section titles, source note: `opacity` 0.6/0.8
| all | 2.7–3.2:1 |
| Editorial hero subtitle: `opacity: 0.7` | editorial | 2.95:1 |
| Nav links: `--text-muted` `#64748b` on the tinted header | minimal,
monograph | 4.39:1 |

## What

- Opacity dimming replaced with the colour tokens (`--text-muted`,
`--text-secondary`).
- The footer link's hover now changes colour rather than opacity.
- Light `--text-muted` for minimal and monograph goes from `#64748b` to
`#627083`: 4.65:1 on the header, 5.04:1 on white.

## Verified

The same audit with these rules applied:

- **Light mode:** 129 → **0** failures.
- **Dark mode:** 56 → 12. The remaining 12 are the configured accent
`#e60012` on studio-modern's small index numbers (3.7–3.9:1). That's a
design choice about the accent, so it's left alone here.

Prettier is clean. CSS only, no TypeScript touched.

## Also: `npm run audit:contrast`

The audit is now in the repo as `scripts/contrast-audit.mjs`. It
discovers the pages from the running site, checks all presets in light
and dark mode, blocks analytics requests and exits 1 on any failure:

```
npm run audit:contrast -- http://localhost:7211
```

`axe-core` was only a transitive dependency (via
`eslint-plugin-jsx-a11y`), so it is now a direct devDependency.
`pnpm-lock.yaml` gains only the 3-line importer entry, and `pnpm install
--frozen-lockfile` accepts it.

---------

Co-authored-by: ralksta <ralksta@users.noreply.github.com>
## What

- **Diagnostics moves from a modal to `/admin/diagnostics`.** Findings
that want a look come first. The passed checks follow, grouped into
connection, security and content. Overview tiles show Immich, config,
backups and version. Backups and cache clearing sit on the page instead
of in two further places. **Copy report** now includes the version.
- **New alt-text report.** Folio's alt text is the Immich description,
and it is used only while *Photo Description* (the `caption` EXIF group)
is on. A photo without a description rendered with an empty `alt`, and
nothing said so. `GET /api/admin/alt-text` lists the published photos
without one, by album, with thumbnails linking to the photo in Immich.
If captions are switched off, the page says that instead and links to
the setting.
- **Every finding that wants a look links to its fix** instead of
showing a severity pill that looked like a button. Where the fix lives
in the admin (passwords, pages), the button goes there. For an
environment variable or the volume, it links to the matching docs
section. The dot carries the severity, and a screen-reader label repeats
it.
- **A finding about a single album opens the page it is published on.**
It goes to `/admin/pages?album=<id>`, which opens that subpage's sheet
and briefly marks the album's tile. A standalone album has no page, so
its own drawer opens. The doctor findings `albums-shared` and
`album-ids` now carry `albumIds` for this. The parameter is removed once
read.
- **The alt-text photo list starts collapsed.** With hundreds of gaps,
it pushed every other check below the fold.
- The status-badge dropdown and the Help page link to the page.
`DoctorModal` is removed.
- After each run, the page hands the doctor level to the badge through a
window event, so the badge updates without asking Immich again.
- **New `--admin-warning` token.** `studio-modern` recolours
`--admin-accent` red, which made every warning read as an error.
`.status-val.update` already referenced the token without it being
defined.

## Decisions worth a second look

- **Alt text is kept out of the status badge.** Most libraries have
photos without a description, and a badge that never turns green stops
being read. It is counted in the page's headline.
- **Immich links use Immich's own External domain** (`GET
/server/config`) when it is set. That is the address Immich uses for its
share links, so a browser can open it. Otherwise they fall back to the
API URL minus `/api`, which is often an internal Docker name, and the
page says where to set the domain.
- **The report goes through `immich.getAlbum()`**, so it shares the
album cache with the public site. An album that fails to load is
counted, not fatal.

## Testing

- `npx tsc --noEmit`, `npm run lint` (no new warnings), `npm run
test:unit` (894 passed), `npm run format:check` and `npm run build` are
all clean.
- New `lib/__tests__/alt-text.test.ts` and
`app/admin/__tests__/findAlbumAddress.test.ts`. `admin-guards.test.ts`
has a row for the new route.
- Checked against a live Immich: 15 albums, 584 published photos, 1 with
a description. Screenshots were taken in dark, light and mobile widths.
With an unreachable Immich URL, all albums are reported as unreadable
and the page still renders.

---------

Co-authored-by: ralksta <ralksta@users.noreply.github.com>
#581)

## Problem

Deleting a journal entry was a bare `fs.unlink`
(`lib/admin/journal-service.ts`), while saving rotated a backup. The one
irreversible action in the journal was the one with no way back.

On top of that, the Backup Manager only listed `gallery.yaml` and
`settings.yaml`. Saves did write journal and `about.md` backups, but
nothing in the panel could restore them, even though
`docs/admin-panel.md` said it could.

## Changes

- **Delete:** `deleteJournalEntry` now writes
`<slug>.md.<ts>.deleted.bak` before unlinking. If that copy can't be
written, the delete is refused.
- **Restore:** new `restoreJournalBackup` / `listJournalBackups`. A
restore snapshots the current version first (`.pre-restore.bak`), and a
deleted entry comes back under its old slug. The name is validated by an
anchored regex over the `isValidSlug` alphabet, plus a `basename` check
and `containedPath`.
- **Rotation:** `.deleted.bak` and `.pre-restore.bak` are exempt from
the 10-backup limit, the same way `yaml-service` treats its pre-restore
files. Re-using a slug can therefore never push out the deleted copy.
- **about.md:** `restoreBackup` accepts `about.md` backups. The about
route already writes them into `content/.backups/`.
- **API:** `GET /api/admin/backups` also returns `about` and `journal`.
`POST` takes an optional `target`, and `'journal'` routes to the journal
service, because `<slug>.md.<ts>.bak` can't be told apart from an
`about.md` backup by name alone.
- **Backup Manager:** four tabs (Gallery / Settings / About / Journal).
Journal rows show the slug and a "Deleted entry" badge.
- **Journal studio:** the delete prompt says a copy is kept and where to
restore it.
- **Docs:** `docs/admin-panel.md` updated.

The subpage `confirm()` in the page builder is left alone on purpose.
That removal only changes local state and can be undone until Save.

## Testing

- New `lib/__tests__/journal-backup.test.ts`, run against a real temp
directory: delete keeps a copy; a failed backup blocks the delete; a
deleted entry is restored; restore snapshots the current version;
traversal names are rejected; rotation keeps the deleted snapshot and
doesn't touch slugs that share a prefix.
- `backup-restore.test.ts`: `about.md` is restored onto
`content/about.md`; `about.yaml.*` and journal names are rejected.
- `npx tsc --noEmit` clean, `npm run test:unit` 901/901, `npm run build`
passes, eslint 0 errors.
- Not clicked through in a browser.

Co-authored-by: ralksta <ralksta@users.noreply.github.com>
…#583)

## Problem

The Live Preview tab in the subpage drawer drew its own banner and tiles
in admin styling: a dashed box as a hero, a fixed `auto-fill` grid, and
white cards with the name underneath. It knew nothing of the theme
preset, the cover grid columns (`coverGrid` / global `grid.columns`),
the caption bar, or the "03 — Collection" kicker. It showed a page the
site never renders, which makes it worthless as a check before saving.

## Changes

- **Real markup:** `SubpagePreview` now emits the markup and classes of
`SubpageGridView`: `subpage-container`, `subpage-header`, the TOC,
sections, and `subpage-grid__item` with its overlay and caption. The
admin `<html>` already carries `data-preset` and the theme variables, so
the preset CSS styles the preview exactly like the public page. There is
no second stylesheet to keep in sync.
- **Same grid and strings:** the cover grid is sized by the same
`buildCoverGridVars()` from the subpage's `coverGrid` and the global
column count. That count, and `lang` for the visitor dictionary, are
read from `/api/admin/settings` each time the preview opens.
- **Scaled canvas:** the page is laid out at 1280px and scaled into the
drawer with `zoom`, so a three-column grid still reads as one. The media
queries see a desktop viewport too. The canvas is `inert`: a picture of
the page, not a second set of links.
- **Kicker:** it counts enabled subpages only, as the page does.
- **Class collision:** the page builder's own subpage list used the
public `.subpage-grid` class and overrode its columns in
`page-builder.css`. That override would have leaked into the preview,
and the public grid rules were already leaking into the admin list. The
list class is now `.subpage-tiles`.

Unsaved drawer edits (title, subtitle, sections, albums, cover grid)
show up live. A preset changed in Settings shows up once it is saved,
since the preset comes from `<html data-preset>`. The note above the
preview says so.

## Testing

- Playwright against a dev server with a copy of `content/`, preset
`studio-modern`: the preview shows the kicker "03 — Sammlung" (`lang:
de`, a disabled subpage correctly skipped), the uppercase title, the
meta line, three 381px columns and the numbered caption bar. The page
builder's subpage list looks unchanged after the class rename.
- Immich was not reachable from that server, so covers and photo counts
were empty. Not checked with live thumbnails.
- `npx tsc --noEmit` clean, `npm run test:unit` 884/884, `npm run build`
passes, eslint 0 errors.

Co-authored-by: ralksta <ralksta@users.noreply.github.com>
…ll (#582)

## Problem

Each settings section is its own route (`/admin/settings/[section]`),
and the section page rendered the editor, so the editor remounted on
every switch. **Changing the site title under General and then opening
another section threw the edit away.** There was no warning: the save
bar just disappeared, and settings.yaml was read again. I reproduced
this in the browser on `dev` (edit the title → About → General: the
field is back to its old value).

There were two more problems:
- **Two save bars.** "Save Settings" and "Save About" were never visible
together, so an About edit could not be seen from the other sections,
and the reverse.
- **No leave-page warning for About.** `useUnsavedGuard` only watched
`settings.yaml`, so closing the tab after editing About lost the edit
without asking.

## Changes

- **Editor in the layout:** it moves into the new
`app/admin/settings/layout.tsx`. A layout stays mounted across its child
routes, so edits survive a section switch. The section is read from the
URL with `useParams()`, and the two pages now render `null`.
- **One save bar:** a single "Save Changes" bar on every section, active
while either `settings.yaml` or `about.md` has unsaved changes. It saves
both. ⌘S/Ctrl+S and the leave-page warning cover both files too. If
either save fails, the error takes priority in the bar.
- **About loads once:** `about.md` is loaded the first time its section
opens, and not again. Otherwise coming back to About would overwrite
unsaved About edits with the file on disk.

## Testing

- Playwright against a dev server with a copy of `content/`: edit the
title → About (bar stays) → edit the name → General (title kept) → About
(name kept) → ⌘S. Both `settings.yaml` and `about.md` were written, each
with a backup.
- `npx tsc --noEmit` clean, `npm run test:unit` 884/884, `npm run build`
passes, eslint 0 errors.

Came out of a UX review of the admin panel. The panel is used from one
screen at a time, so none of this is visible until an edit goes missing.

Co-authored-by: ralksta <ralksta@users.noreply.github.com>
Writes the `[Unreleased]` section for v0.15.0, covering the 18 commits
on `dev` since v0.14.0. The release commit renames the heading to
`[0.15.0] — <date>`, as it did for v0.14.0 (#553).

The three open refactors in the milestone (#554–#556) moved to
**v0.16.0**: all three are pure refactors with nothing user-visible,
kept out of release prep on purpose.

## What is in it

- **Security:** EXIF/GPS stripped from full-size originals (#558);
Next.js 16.3.1 → 16.3.5 for GHSA-p293-qw3h-jr36, plus the js-yaml
advisory (#571, #566).
- **Added:** `/admin/diagnostics` with the alt-text report (#579);
Markdown links in quote attributions (#563); journal and About restore
in the Backup Manager, deleted entries included (#581).
- **Fixed:** settings edits lost on a section switch (#582); Live
Preview renders the real page (#583); EXIF orientation (#565/#572); WCAG
contrast and keyboard access (#571, #580); light-mode surfaces (#562);
proofing bar with transitions on (#560); studio-modern caption bar
(#578).
- **Upgrade notes:** bump `IMAGE_CACHE_VERSION` on deploy. Without that
bump, browsers that already cached an original keep its GPS metadata for
up to a year, which is the point #558 made about its own rollout.

External contributors are credited inline: @lancetm714 (#560, #562,
#563) and @ImScheinox (#572, which is also why #576 was reverted in
#577).

## Checked

- Every entry against its PR description and the code on `dev`. The
version numbers come from the `package.json` diff `v0.14.0..origin/dev`.
- The explicit CI token (`3ef9184`) is **not** listed. It already
shipped in the v0.14.0 tag.
- `npx prettier --check CHANGELOG.md` is clean.


## README

The Contributors section gains **@ImScheinox** (first contribution,
#572) and extends **@lancetm714**'s entry with the v0.15.0 work (#560,
#562, #563). The changelog already credits both inline; the README is
where the lasting thanks lives.

---------

Co-authored-by: ralksta <ralksta@users.noreply.github.com>
main is a chain of one squash commit per release, each tree-identical to the dev commit it was made from. Because a squash leaves no parent link, the merge base of dev and main stays at the previous release, so every release PR re-presents the whole of the last release as a conflict.

This merge takes dev's tree unchanged, which loses nothing: main (v0.14.0, e99d3f2) is tree-identical to 3ef9184, an ancestor of dev. What it does is move the merge base to main, so the v0.15.0 release PR applies cleanly.
CodeQL blocks the v0.15.0 release PR (#585) with two **high** alerts,
*Uncontrolled data used in path expression*, both on
`lib/admin/journal-service.ts:49`. That is `snapshotEntry`, added in
#581, which built the backup path with `path.join`. The two sources are
the slug from `DELETE /api/admin/journal/[slug]` and `backupFilename`
from `POST /api/admin/backups`.

## Exploitable?

No. The name is always `<slug>.md`, and the slug has already passed
`isValidSlug` (delete, save) or the anchored `JOURNAL_BACKUP_NAME` regex
plus `isValidSlug` (restore). Neither admits a dot or a separator. But
that validation lives in the callers and in another module, and CodeQL
does not follow it there. That is exactly why this file has
`containedPath()`, as its own comment explains.

## Fix

The backup path is built through `containedPath(BACKUP_DIR, …)` like
every other path in the file. A path that would escape `.backups/`
throws, and a delete is refused rather than carried out unprotected,
because `deleteJournalEntry` snapshots before it unlinks.

## Checked

`npx tsc --noEmit` is clean, `npm run test:unit` passes 918/918
(`journal-backup.test.ts` included), and eslint reports 0 errors.
Whether the alerts clear shows up in this PR's own CodeQL run.

Co-authored-by: ralksta <ralksta@users.noreply.github.com>
main (v0.15.0, c3f92ca) is tree-identical to 1779140, the dev commit it was squashed from. This merge takes dev's tree unchanged and moves the merge base to main, so the v0.16.0 release PR applies cleanly.
Closes #612.

## The decision

`jsdom` and `@testing-library/react` come in as **dev** dependencies.
The seven runtime dependencies from #480 are untouched — test tooling
never reaches a user's browser or server, so that rule was never the
thing standing in the way.

## What is set up

- **Node stays the default environment.** Most of what is worth testing
here is plain logic and needs no DOM, and the existing 918 tests should
not get slower.
- **A component test opts in per file** with `// @vitest-environment
jsdom` as its first line. Explicit, visible in the file itself, and
stable across vitest versions.
- `include` now covers `.tsx` and adds `components/__tests__/`.
- `CONTRIBUTING.md` documents the convention — including that
**extracting the logic is still preferred over rendering it**. A
component test is for behaviour that *is* the rendering: a state that
hides a form, a control that must stay disabled, a shortcut that must
still fire.

## Why SaveBar as the example

It is small enough to read in one go, and it earns the coverage on its
own:

`SaveBar` decides whether an outcome reads as success or failure by
testing whether the message happens to **start with the word "Error"**.
The suite pins that as it behaves today — including the case where a
genuine failure ("Could not reach the server") is styled as a success.
That is exactly the argument #600 makes for a typed state, and it is now
a verifiable one rather than an assertion in an issue.

Seven tests: nothing rendered when there is nothing to act on, the
unsaved badge, the button disabled unless dirty, disabled again while
saving, `onSave` firing, the prefix-based message styling, and the
preview link appearing only where it belongs.

## Verification

- full suite: **925 tests in 69 files, all passing** (918 before, plus
these 7)
- `npx tsc --noEmit` clean, `npx eslint app components lib` clean
- `npm run build` succeeds

## Two notes

**This unblocks the refactors.** #608, #610, #611, #554 and #555 can now
leave behind more tested surface than they found, instead of
redistributing an untested one. The #619 error paths — empty form,
blocked save, `Cmd+S` still intercepted — become testable once that PR
lands.

**`pnpm-lock.yaml` is dead weight.** CI runs `npm ci` and the Dockerfile
copies `package-lock.json` only, so nothing reads the pnpm lockfile —
but it still conflicts on every dependency change (it is one of the
three conflicts in #561 right now). I left it alone rather than deleting
it unasked; worth a one-line decision.

---------

Co-authored-by: ralksta <ralksta@users.noreply.github.com>
…615)

Closes #606.

## What

The enabled + authenticated check was copy-pasted into **24 handlers
across 16 route files**. All of them are now defined inside a single
`withAdmin()` wrapper in `lib/admin/withAdmin.ts`.

Net effect: **−164 lines**, and the two refusals keep their distinct
meanings exactly as the hand-written copies had them — `403` when the
admin panel is switched off for the deployment, `401` when it is on and
the caller is not signed in.

`/api/admin/auth` is deliberately left alone: it is the login endpoint
and must stay reachable without a session.

## Why it is worth doing

The repetition was not the problem. What it made possible was: a new
admin route protected by nobody is a diff that looks fine, and nothing
fails when the guard is forgotten.

Two structural tests now close that off:

- no handler may be exported outside the wrapper (`export async function
GET` and friends are rejected)
- no route may reach for `isAdminEnabled` / `isAdminAuthenticated`
directly

The existing table-driven suite is untouched and still proves that every
route answers 403 and 401 to an unauthenticated caller — so the
behaviour is pinned from both sides: what the routes do, and how they do
it.

## Verification

- `npx tsc --noEmit` clean
- `npx eslint app/api/admin lib/admin` clean
- full suite: **920 tests in 68 files, all passing** — including
`admin-guards.test.ts`, which is what makes this refactor safe: the
403/401 behaviour of all 24 handlers was already covered before the
change.
- I checked the new structural test actually fails on a bare `export
async function GET` rather than passing vacuously.

## Review notes

- The transformation was mechanical (`export async function X(args) {` +
guard block → `export const X = withAdmin(async (args) => {`), then
prettier. `thumbnail/[id]/route.ts` has a large diff purely from
re-indentation.
- No behaviour change is intended anywhere. If you want the 403/401
distinction collapsed into one code, say so — I kept it because the
existing tests assert both.

Co-authored-by: ralksta <ralksta@users.noreply.github.com>
ralksta and others added 29 commits October 4, 2026 10:21
…841)

README: 419 → 97 lines. gallery-config.md: 1099 → 360 lines. Nothing
deleted; content moved into guides.

**README split**
- `docs/features.md`: full feature list, screenshots, security table
- `docs/immich-setup.md` (new): Immich version, API key permissions,
full-size images, album allowlist. Single source for the permission
table
- `docs/deployment.md`: first-run setup and environment variables
- `docs/admin-panel.md`: admin screenshots
- `CONTRIBUTORS.md`: credits

**gallery-config.md split** (headings unchanged, so anchors keep their
names)
- `gallery-config.md`: gallery.yaml structure (hero, albums, subpages,
per-album options, photo order, content pages, UUIDs)
- `site-settings.md`: grid, site behaviour, navigation, watermark, SEO,
footer, about
- `gallery-features.md`: passwords, proofing, proofing links, photo
zoom, originals download
- `security.md`: rate limiting, trusted proxies, image token revocation

**Archive**: brainstorms, deep dive, ideas, journal design notes, old
plans moved to `docs/archive/` (`git mv`). `docs/admin-ux-concept.md`
stays because code comments reference it.

**Links**: every link to a moved section is rewritten (README, guides,
CHANGELOG, `.env.local.example`, the doctor's links in
`DiagnosticsView.tsx`). Link check over all Markdown files (paths and
anchors), before and after: no new broken link; the three remaining hits
are placeholders in an archived note.

Verified: prettier, `tsc --noEmit` clean, 2707 unit tests pass.

Not in this PR: a test that keeps the permission table in sync with the
code.

---------

Co-authored-by: ralksta <ralksta@users.noreply.github.com>
…ht backgrounds (#842)

- Add top and bottom gradient scrims to Lightbox overlay for reliable legibility
- Add frosted backdrop, border, and drop-shadow to close button and counter
- Increase --lightbox-control-bg opacity to 0.65 to guarantee WCAG AA 4.5:1 on pure white
- Adjust resting control opacity to 0.85 (and 1.0 when zoom is engaged)
- Enhance studio-modern preset controls with dark blur backdrop and box-shadow
- Add regression test asserting controls clear 4.5:1 against #ffffff
#604) (#855)

First part of #604. The admin panel now has a language of its own:
`adminLang` in `settings.yaml`, which falls back to the site's `lang`,
so an English panel can run over a German site and the other way round.
It can be picked under Settings → General → Admin panel language, and
the panel switches as soon as the settings are saved.

This PR translates the frame and the two screens the issue names. That
covers sign-in, the sidebar and its status dropdown, the overview, the
save bar, every settings section, and diagnostics including the doctor
findings. It also covers the shared pieces those screens use: the draft
notice, the "changed elsewhere" dialog, the confirm dialog defaults, the
load/retry state and the On/Off words of toggle switches. The page
builder, the journal studio, messages, proofing, analytics and help are
still English and will follow in separate PRs.

How it works:

- The admin dictionaries live in `lib/i18n/admin/<locale>/<area>.ts`,
separate from the visitor dictionaries, so visitors never download them.
They are split by area so the follow-up PRs each add a file instead of
editing a shared one. English is the reference type, as on the visitor
side.
- `app/admin/layout.tsx` resolves the locale on every request and mounts
`AdminI18nProvider`. Components call `useAdminDictionary()`. A string
that wraps markup uses `{name}` placeholders, filled in by `rich()`, so
a translation can put the `<code>` where its grammar needs it.
- Doctor checks take the dictionary as a trailing parameter that
defaults to English, so `npm run doctor` and the existing tests don't
change. The route passes the panel's language.
- `lib/__tests__/admin-i18n.test.ts` checks that every locale covers
every English key, keeps the same placeholders, and leaves nothing
untranslated except the listed shared words. It also checks plural
forms, including French putting 0 in the singular.

Translations: German, French, Spanish, Italian and Dutch, using the same
forms of address as the visitor dictionaries.

Still English: messages returned by the API routes (`data.error`,
`data.message`), the theme preset names and taglines (shared with the
install wizard), collision reasons from `lib/pages`, and Immich's own
menu paths quoted in the doctor findings.

Part of #604.

---------

Co-authored-by: ralksta <ralksta@users.noreply.github.com>
#604) (#856)

Second part of #604, following #855. It translates the screens that
don't touch the page builder or the journal studio: Messages, Client
proofing, Analytics, Help and the backup dialog, plus two shared pieces
those screens use: the load errors from `useAdminFetch` and the close
button on notifications.

- Two new areas, `visitors` (messages, proofing, analytics) and `system`
(help, backups), each a file per locale, so nothing in the #855
dictionaries changes beyond the two shared strings.
- The Help page now shows the lightbox shortcut names from the visitor
dictionary in the panel's language instead of always English, and the
notes explaining when a key is absent come from the admin dictionary. A
test keeps the English notes identical to the catalogue in
`lib/lightboxShortcuts.ts`, so the two cannot drift.
- One English text changed on purpose: Help said to open Diagnostics
"from the tab in the top bar", which hasn't existed since the sidebar
replaced it. It now says the sidebar.
- `adminGet()` takes the dictionary as an optional parameter (English by
default) and `useAdminFetch` passes the panel's, so the existing tests
on its error strings are unchanged.

Translated into German, French, Spanish, Italian and Dutch, with the
terms the first part already uses. Still English: the page builder, the
journal studio and the album/photo pickers they share. Those wait until
#852 has settled, since it reworks `AssetPicker`.

Checked: all unit tests pass, `tsc` is clean, `npm run build` passes,
and the five screens render in German against a real Immich with no
console errors.


Part of #604.

---------

Co-authored-by: ralksta <ralksta@users.noreply.github.com>
OpenStreetMap answers tile requests that carry no `Referer` with an
"Access blocked" tile (still HTTP 200, so it doesn't show up as an error
in the network tab). Folio sends `Referrer-Policy:
strict-origin-when-cross-origin`, which includes the origin, but a proxy
in front of the site can replace that header with a stricter one. The
map then shows nothing but blocked tiles, which is what #851 reports.

Leaflet's `referrerPolicy` option puts the policy on each tile `<img>`,
and an element's own policy takes precedence over the page header. Tile
requests now send the origin no matter what a proxy does to the header.
A browser extension that strips Referers altogether still breaks OSM,
but there's nothing Folio can do about that.

I reproduced it with curl: the same tile is the normal map with a
Referer and the "Access blocked" image without one.

Closes #851

Co-authored-by: ralksta <ralksta@users.noreply.github.com>
…hild (#859)

Both layouts targeted the tile as if it were the grid cell. The grid's
children are the `FadeIn` wrappers, and the tile sits inside one.

- **editorial-flow:** `.photo-grid__item:nth-child(3n + 1)` matched
every tile, since each is the first child of its wrapper, so every photo
spanned the full width and the single–pair–pair rhythm never showed. The
rule now counts the grid's own children (`.photo-grid--editorial-flow >
:nth-child(3n + 1)`), and the 21:9 shape stays on the tile inside.
- **showcase:** `grid-column: 1 / -1` sat on the tile, so the lead photo
stayed one column wide. The wrapper of the first photo now gets
`photo-grid__cell--featured` and spans the row. The lead tile also no
longer takes the photo's own aspect ratio inline, which used to override
the stylesheet's 16:9. A portrait lead photo at full width would
otherwise be several screens tall.

Checked against the real gallery with both layouts under studio-modern
(no frame), kunsthalle (passepartout) and cyanotype (shadow), plus
editorial-flow at phone width. Measured cell widths, editorial-flow at
1400px: 1304 / 646 / 646 / 1304 / … (kunsthalle: 1160 / 574 / 574 /
1160); showcase: lead 1304, then 427 per column. No console errors.

New tests in `app/[...path]/__tests__/grid-layout-cells.test.tsx` check
the featured cell class, the lead tile's missing inline ratio, and that
`globals.css` no longer counts siblings on the tile. All four fail on
dev and pass here.

Closes #825

Co-authored-by: ralksta <ralksta@users.noreply.github.com>
…to clips (#860)

Downloads and ZIPs removed the location from JPEG and HEIF/AVIF and
passed everything else through unchanged. So a video's recorded location
went out as stored, and so did the clip that Samsung and Pixel motion
photos append after their JPEG, even though the JPEG part was cleaned.

**What changes**

`lib/mp4Location.ts` walks the MP4/QuickTime box tree while the file
streams and only descends into containers that can hold metadata
(`moov`, `trak`, `mdia`, `minf`, `udta`, `meta`, `ilst`):

- QuickTime `©xyz` and 3GPP `loci` are zeroed and renamed to `free`,
which every reader skips. A `loci` that is only zeroed still reads as
0/0.
- Apple's QuickTime metadata (`keys` + `ilst`): the values of
`…location.ISO6709`, `.name`, `.body` and friends are zeroed. Other keys
like make and model stay. If an `ilst` comes before its `keys`, every
indexed item is zeroed, since nothing says which one is the location.
- The iTunes-style `©xyz` item in `udta/meta/ilst`.
- XMP in `udta/XMP_` and in the XMP `uuid` box goes through the same
`scrubXmpLocation()` as a photo's. C2PA manifests are zeroed.

Nothing moves. The file keeps its length and every `stco`/`co64` offset
still points at its sample. `mdat` is never held, so it doesn't matter
whether `moov` comes before or after it. A 45 MB QuickTime file went
through in ~120 ms with flat memory, and exactly 51 bytes changed.

For motion photos, the clip is found by its `ftyp` after the JPEG's EOI
and handed to the same walker. From there on the trailer's SOI search
stops, so bytes inside the video that look like a JPEG head aren't
"scrubbed" as one. That's the concern from the #828 review, and there's
a test where that would have damaged the clip. Anything after the clip
(Samsung's SEF directory) passes through unchanged.

The doctor now counts MP4 and MOV as cleaned, with its texts updated in
all six admin languages. The docs say what's still left: GPS **tracks**
recorded as timed metadata (GoPro GPMF, Google CAMM, some dashcams).
Those samples sit in `mdat` and would need the sample tables, which can
arrive after `mdat` has already passed.

**Not in this PR:** `/api/video` still streams Immich's playback file to
visitors. When Immich didn't transcode a video, that's the original with
its location atom, regardless of the download setting. Range requests
make that a separate piece of work.

**Checked**

- Unit tests with synthetic boxes at chunk sizes 1, 3, 7, 64 and 1 MiB
(header, `keys` and `ftyp` split across chunks), and a mutation check
(disabling the zeroing or the clip hand-off makes them fail).
- Real files from ffmpeg: MP4 (`loci`), MOV (`©xyz`), MOV with Apple
`mdta` keys, fast-start, and a JPEG + `MotionPhoto_Data` + MP4 motion
photo. After scrubbing, `ffprobe` shows no location tag, the size is
unchanged and every file decodes without errors.
- All unit tests pass, `tsc` is clean, `npm run build` passes.

Closes #829

Co-authored-by: ralksta <ralksta@users.noreply.github.com>
Refs #862

## What changes

Video playback through `/api/video` now goes out without the recorded
location, matching downloads since #860.

A player asks for ranges and seeks, so the one-pass download scrubber
can't run as-is. `lib/videoPlayback.ts` does the work once per video
instead:

- `planPlayback()` reads the top-level box headers with small ranged
requests, fetches the boxes that can carry metadata (`moov`, plus a
top-level `udta`, `meta` or `uuid`), runs `lib/mp4Location.ts` over a
copy and records which bytes it changed. `mdat` is skipped by its header
and never read.
- `patchStream()` overwrites those bytes in whatever range is being
served.
- Plans are cached per asset (500 entries, a few patches each) and tied
to the file's length, so a file Immich replaces (a finished transcode)
gets a new plan. Concurrent requests share one planning run, and
failures aren't cached.

Nothing changes length, so `Content-Length` and `Content-Range` stay as
Immich sent them. A file that isn't MP4/QuickTime (WebM, AVI) passes
through, as it does for downloads. An MP4 whose metadata can't be
located isn't played (404, `no-store`), and Immich dropping out while
planning answers 503.

Video URLs get `?r=2`. They're served `immutable` for a year, so without
a new URL browsers and CDNs would keep the copies they already have.

## Checked

- Equivalence: for ~40 random ranges plus the typical start, seek and
tail requests, at chunk sizes 1, 17 and 4096, the patched range is
byte-for-byte the download scrubber's output for the same part of the
file. Tested with moov at the end and with fast start.
- Real file from ffmpeg (2.4 MB MOV with `©xyz` and Apple location
keys), served the way a browser asks for it: planning took 5 reads
totalling 2.4 KB, and 6 patches changed 51 bytes. The result is
identical to the download scrubber's, `ffprobe` shows no location and
the file decodes cleanly.
- Route tests: whole file, a range covering `moov`, a range of samples
(unchanged), WebM passthrough, refusal without `moov`, and 503 when
Immich drops out mid-planning.
- All unit tests pass, `tsc` is clean, `npm run build` passes.

Co-authored-by: ralksta <ralksta@users.noreply.github.com>
Bundles the five open Dependabot updates so they land together instead
of rebasing each other one by one:

| Package | From | To | Dependabot PR |
|---|---|---|---|
| `next`, `eslint-config-next` | 16.3.6 | 16.4.2 | #843 |
| `react`, `react-dom` | 19.3.0 | 19.3.1 | #848 |
| `@playwright/test` | 1.63.0 | 1.64.1 | #850 |
| `@vitest/coverage-v8` (+ `vitest` floor) | 4.1.11 | 4.1.12 | #849 |
| `axe-core` | 4.13.0 | 4.14.0 | #845 |

Both lockfiles are regenerated (`npm install`, `pnpm install
--lockfile-only`).

What Next 16.4 needed:

- **#843 failed CI** on `lib/__tests__/image-url-per-tier.test.ts`:
`getImgProps` now takes a prepared image config (sorted sizes,
`allSizes`, `output`). The test builds it with `prepareImageConfig()`,
the same function `next/image` uses.
- **`eslint-config-next` 16.4** turns `react-hooks/set-state-in-effect`
(14 findings) and `react-hooks/refs` (4) from warnings into errors. CI
doesn't run ESLint, but `npm run lint` would go red. Both stay at `warn`
for now. Fixing them touches effects in the lightbox and the admin
editors (and `AssetPicker`, which #852 reworks), so that's #864.
- ESLint now ignores `.claude/**`. Local worktrees are full checkouts
and made `npm run lint` in the main checkout take minutes.

Also: `vitest`'s floor goes to `^4.1.12`, the peer that
`@vitest/coverage-v8` 4.1.12 requires. `pnpm-lock.yaml` had kept 4.0.18.

Checked: `tsc` clean, all 2,827 unit tests pass, `npm run lint` has 0
errors, `npm run build` passes on Next 16.4.2. A dev server against the
real gallery answers 200 for home, a subpage, an album, journal, about,
map and `/admin` with nothing in the server log. I didn't check
hydration in a browser; CI's admin smoke test does.

Co-authored-by: ralksta <ralksta@users.noreply.github.com>
## Summary

This PR fixes the missing album source in journal block creation. The
picker now lists every Immich album (including albums not shared in
Immich), and choosing an album lets authors select only the photos they
want from **+ Photo**, **+ 2-Photo Pair**, or **+ Photo Grid**. These
actions open the picker immediately and create a block only after
selection; they no longer insert an empty block. A pair requires exactly
two photos and a grid at least three. Escape closes only the topmost
album picker when it is open over the asset picker.

The creation-studio flow and **+ Photos** (one block per selected photo)
remain available. CARTO and installation changes are excluded from this
PR and are tracked separately in #858 and #857.

## Type of change

- [ ] 🐛 Bug fix
- [x] ✨ New feature
- [x] 🎨 Style / visual
- [ ] ♻️ Refactor
- [ ] 🔒 Security fix
- [x] 📝 Documentation
- [ ] 🛠️ Build / CI

## Checklist

- [x] Targets the `dev` branch (see
[CONTRIBUTING.md](../CONTRIBUTING.md#branches))
- [x] `npm run build` passes locally
- [x] `npx tsc --noEmit` passes (0 type errors)
- [ ] `npx vitest run` passes (all tests green)
- [x] No secrets / API keys in diff
- [x] Documentation updated if needed (`docs/`, `README.md`)
- [x] Screenshots attached if there are visual changes

## Validation

- TypeScript check, production build, and Prettier checks passed.
- Focused journal/picker tests passed: 8 tests across three files,
covering all-album visibility, block-specific selection counts, and
Escape behavior for stacked dialogs.
- Full Vitest run: 2,721/2,722 passed. One existing timing-threshold
test in `lib/__tests__/locationScrub.test.ts` exceeded 100 ms under
full-suite load (123.8 ms), then passed in isolation (39/39).

## Screenshots (if applicable)

<img width="640" height="639" alt="Capture d’écran du 2026-10-20
03-29-06"
src="https://github.com/user-attachments/assets/f8768c2f-87c6-4edb-809c-1ad5f74f7843"
/>
<img width="338" height="213" alt="Capture d’écran du 2026-10-20
03-28-52"
src="https://github.com/user-attachments/assets/693f202c-3d1b-401e-ad87-e995fdde88b9"
/>
<img width="506" height="388" alt="Capture d’écran du 2026-10-20
03-28-43"
src="https://github.com/user-attachments/assets/82e21c0b-bb15-43f7-b871-54c2ec2d2cf1"
/>

---------

Co-authored-by: sgsmcfe <344374139+sgsmcfe@users.noreply.github.com>
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
## Summary

When the setup wizard reused the server-configured API key independently
of the URL, submitting a different Immich URL with an empty key could
send that configured key to the other host. The wizard now reuses the
configured key only when the URL is omitted or normalizes to the
configured server; changing servers requires an explicitly supplied key.

Environment-only API keys are no longer copied into
`content/install.json`. A key already persisted there remains available
when setup reuses the configured connection, and the wizard explains
when that reuse is safe.

## Type of change

- [x] 🐛 Bug fix
- [ ] ✨ New feature
- [ ] 🎨 Style / visual
- [ ] ♻️ Refactor
- [ ] 🔒 Security fix
- [x] 📝 Documentation
- [ ] 🛠️ Build / CI

## Checklist

- [x] Targets the `dev` branch (see
[CONTRIBUTING.md](../CONTRIBUTING.md#branches))
- [x] `npm run build` passes locally
- [x] `npx tsc --noEmit` passes (0 type errors)
- [ ] `npx vitest run` passes (all tests green)
- [x] No secrets / API keys in diff
- [x] Documentation updated if needed (`docs/`, `README.md`)
- [ ] Screenshots attached if there are visual changes

## Validation

- TypeScript check and production build passed.
- Install-focused tests passed: 41 tests across
`lib/__tests__/install.test.ts` and
`app/install/__tests__/InstallWizard.test.tsx`.
- The full Vitest suite was not entirely green in the previous run
because an unrelated timing-threshold test failed under full-suite load;
it passed in isolation.
- Prettier check passed for changed files.

## Screenshots (if applicable)

N/A — no visual change beyond setup guidance text.

---------

Co-authored-by: sgsmcfe <344374139+sgsmcfe@users.noreply.github.com>
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Co-authored-by: ralksta <ralksta@users.noreply.github.com>
…604) (#867)

Third and last part of #604, following #855 and #856. It translates the
page builder, the journal studio and the pickers they share, so the
whole admin panel now follows `adminLang` in all six languages.

- **Three new areas:** `pages`, `journal` and `pickers`, with one file
per locale under `lib/i18n/admin/<locale>/` and about 560 keys. English
stays byte for byte as before, and the existing tests pass unchanged
apart from one `.map(createBlock)` call that now passes the block type
explicitly.
- **`pages`** covers `PageBuilder`, everything in `page-builder/`,
`EssayBlockEditor` and the route header (`PageBuilderHeader`, since the
route is a server component). The seed names for a new subpage and
section, the sort labels and the starter essay follow the panel
language. They're passed into `galleryOps`/`sortOptions` the same way
`systemHealth` takes its words: a trailing parameter that defaults to
English.
- **`journal`** covers `JournalStudio`, `JournalEditor`, `BlockFields`,
the story and page settings, the preview frame, template names and
descriptions, and the starting text of new heading, paragraph and quote
blocks.
- **`pickers`** covers `AlbumPicker`, `AssetPicker`, `AssetOrderEditor`,
`SortableBlocks`, `BlockBadge`, and the drag & drop announcements for
screen readers (`dndAnnouncements(label, t)`, now read in the panel
language).
- **Slug-collision reasons:** `describeCollision()` (`lib/pages.ts`)
takes its words as a trailing parameter too. `NewPageDialog`,
`PagePanel` and the doctor's page check pass `pages.collision` in the
panel language; the API keeps the English default, and a test holds that
default equal to the English dictionary.
- **Parity test fix:** `invoke()` in `admin-i18n.test.ts` now tries a
count, lists and strings as arguments. Before, it crashed on functions
whose first argument is a list or a label (`legacyRefs`, the dnd
sentences), and that would have hidden untranslated keys.

**Still English, on purpose**

- Messages the API returns (`data.error`, `data.message`,
`data.warning`).
- The text a journal template inserts into a new entry. It's the
author's content, and the template names around it are translated.
- Theme preset names and taglines, which are shared with the install
wizard.

**Checked**

- All unit tests pass, including the parity checks for every locale; the
identical-to-English allowances are the words that really are the same
(Album, Layout, URL, Text, Live, …). `tsc` is clean, `npm run lint` has
0 errors and `npm run build` passes.
- In a browser against the real gallery, with `lang: de`: page builder
with an open subpage drawer, journal studio, an entry in the editor, and
the photo picker, all German and without console errors. With
`adminLang: fr` the same screens are French. Two longer labels used to
break badly: "Nouvelle sous-page" wrapped inside its button, and "Albums
de cette page" was squeezed to one word per line next to its button.
Both now wrap as a whole (`flex-wrap`, `nowrap` labels). I checked
French at 1400, 1100 and 390 px, plus Italian and English at 1400.
- Translations were made by language-specific agents told to reuse each
locale's existing admin terminology. I read the German myself; the other
four have only been checked by the parity test.

Closes #604

---------

Co-authored-by: ralksta <ralksta@users.noreply.github.com>
…821) (#869)

Closes #821. This finishes what #707 started: every admin screen in the
issue's list now loads through `useAdminFetch()` or `adminGet()`. That
means one wording for failures, which includes the server's reason, plus
the session-expired event on a 401 and a retry wherever one makes sense.

## The two silent failures (user-visible)

- **Subpage Live Preview:** if `/api/admin/settings` failed, the preview
fell back to three columns and English without telling anyone. It now
says so above the preview ("uses the default grid and English until the
site settings load", followed by the reason) and offers Retry. The
preview still renders underneath.
- **Journal editor, album picker:** a failed album list opened the
picker on an empty list, which reads as "no albums in Immich". The
editor now reports the failure and keeps the picker closed, and the next
click tries again. This is how JournalStudio already behaved.

## Found along the way

- **Album blocks stuck on "loading…":** the effect that fetches an album
block's photos cancelled itself on every edit. If the author typed
before the answer came back, the answer was dropped, and because the
album was already marked as requested it was never asked for again. The
cancel is gone; the answer is keyed by album, so it can't be stale. A
failed load now shows its reason on the block instead of "loading…"
forever. There's a test that fails without the fix.
- **Page builder:** a failed album list or page list is still
survivable, but it is now reported instead of leaving an empty picker.
- **Privacy editor and journal entry:** both used to show a dead-end
error and now have a Retry button. Saving over an unread file stays
blocked (#595).

## How each screen moved

| Screen | Now | Kept from #707 |
| --- | --- | --- |
| SubpagePreview | `useAdminFetch` | refetched on every open |
| ProofingView (list, album choice, a card's picks) | `useAdminFetch`;
the hook takes an optional `revision` so a card refetches when the
client changes or submits the selection | the `api()` wrapper, for
mutations only |
| AssetOrderEditor | `useAdminFetch`; the pinned order is derived from
the stored list and the live album until the author changes it (no
effect copying it into state) | save blocked until loaded |
| JournalEditor (entry, album list, album photos) | `adminGet` | drafts,
save blocking, stale answers dropped |
| AssetPicker | `adminGet` | generation guard, paging, search debounce |
| SettingsEditor, AboutSection, PrivacyEditor | `adminGet` | drafts,
on-demand About, #595 save blocking |
| PageBuilder | `adminGet`, three in parallel | gallery required,
albums/pages survivable |
| PagePanel | `adminGet`, with a load key instead of resetting state
synchronously in the effect | 404 still says "no file for this page" |
| AdminDashboard probes | `adminGet` | a failed probe reads as unknown,
never broken; `DOCTOR_LEVEL_EVENT` |
| DiagnosticsView | `adminGet` | parallel requests; alt text still
arrives on its own |

Smaller API changes:
- `AdminResult` failures now carry `status` (null when no answer
arrived), so PagePanel can word the 404 itself.
- `diagnostics.errors.failed` takes the reason.
- I removed the strings nothing reads anymore:
`sessionExpired`/`serverAnswered` and the screen-specific `loadFailed`
fallbacks.
- The new strings are in all six admin locales.

Still on raw `fetch` on purpose:
- the AdminShell session check, where a 401 is the expected answer;
- the Lightroom export, which is plain text;
- JournalStudio's album list, which already reports its failure;
- all mutations.

## Checked

- `npm run test:unit`: 228 files, 2871 passed. New tests cover every
converted screen: `load-errors-shown`, `ProofingView-load`,
`AssetPicker-load-errors`, `AssetOrderEditor-load`,
`editor-load-errors`, `status-load-errors`, plus a revision case in
`useAdminFetch.test`. I ran each new test file against the `dev` version
of its components, and every test fails there except the PagePanel
404/switch case, which is a regression guard.
- `tsc` is clean, `npm run build` passes, and ESLint has 0 errors with
warnings down from 246 to 239 (no file gained one).
- In the browser against a copy of the real config (German panel):
- The page builder, a subpage's Live Preview, Diagnostics, Proofing,
Settings, About, Legal/privacy and a content page all load without
errors.
- A forced 500 on `/api/admin/settings` shows the preview notice with
the reason, and Retry clears it.

---------

Co-authored-by: ralksta <ralksta@users.noreply.github.com>
The public stylesheet gets the treatment `admin.css` got in #533/#549.
`app/globals.css` (2,338 lines) keeps its theme, tokens and Leaflet
imports, and below them becomes the load order for fourteen files in
`app/styles/`, each named for what it styles:

`a11y`, `header`, `main`, `hero`, `subpage`, `album-grid`, `album`,
`states`, `controls`, `footer`, `badges-empty`, `theme-conditionals`,
`aesthetics`, `responsive`

Cuts are only at existing section banners, contiguous ranges only,
nothing reordered. `app/layout.tsx` doesn't change. A comment in
`globals.css` says the order is load-bearing (the presets layer on top
by specificity, and the responsive rules have to come last).

**The two checks from the issue**

1. **Byte for byte:** the fourteen parts, concatenated in import order,
are exactly lines 17–2338 of the old file. The split script asserted
this before writing anything.
2. **Rule by rule on the built CSS:** I built `dev` and this branch and
compared the emitted CSS.
   - 13 of the 14 CSS files are byte-identical.
- The global bundle has the same 911 rules in the same order, plus 4
that the minifier used to drop:
`.header__nav-link:hover,.header__nav-link.active`, `.album-card:hover`,
`.album-header` and `.footer__link:hover`. Each of these is followed
later, in the same context, by a rule with the same selector that sets
all of its properties again, so they're dead and the computed styles are
unchanged. It's the same effect #549 found once in `admin.css`: the
minifier can't merge duplicates across an import boundary.
- No theme preset depends on a rule's position inside `globals.css`. The
presets are all imported before it, so they only win by specificity, and
that relationship is the same as before.

**Tests**

Four tests read `globals.css` as text. They now use `readGlobalsCss()`
(`lib/__tests__/fixtures/globalsCss.ts`), which inlines the parts, and
the accent-text check also walks `app/styles/`. All unit tests pass,
`tsc` is clean and `npm run build` passes.

Closes #556

Co-authored-by: ralksta <ralksta@users.noreply.github.com>
…again (#866)

`eslint-config-next` 16.4 reports two React Compiler rules as errors,
`react-hooks/set-state-in-effect` and `react-hooks/refs`. #865 kept both
at `warn`. This resolves the 18 findings and removes the overrides, so
new ones fail `npm run lint` again.

**Changed, so the rule has nothing to report**

- **Lightbox.** The client-only mount guard and the fullscreen check now
come from `useSyncExternalStore` instead of an effect that sets state.
The load state and the "Copied" of the previous photo are reset while
rendering ("adjusting state when a prop changes"). A side effect: the
next photo is never drawn for a frame with the previous one's state.
- **LeafletMap.** The latest-value refs (labels, markers, callbacks) are
updated in an effect declared before the map's own, instead of being
assigned during render.
- **AdminDashboard** closes its mobile menu on navigation while
rendering.
- **BackupManagerModal** clears its last confirmation and messages on
reopening while rendering. The list reload stays in its effect.

**Kept, with the reason next to it**

- **ProofingContext** reads the favourites from `localStorage` and
`?proof=` after mount, on purpose: both only exist on the client, and
reading them during render would cause a hydration mismatch.
- **Loads started on mount**: diagnostics, privacy policy, about page,
the sidebar status, the asset picker and the content-page panel. Each
keeps its effect with a pointer to #821, which moves them to
`useAdminFetch` (that hook already sets state only in callbacks).
Converting them here would mean rewriting six load paths, including the
behaviours #707 asked to keep.

**Checked**

- New tests: the Lightbox resets load state and "Copied" on the next
photo, and offers the fullscreen key only where
`document.fullscreenEnabled` is true. The admin menu closes on a route
change, and the backup dialog reopens without the old confirmation. The
Lightbox tests fail with the reset removed.
- In a browser against the real gallery: the next photo starts as not
loaded, `c` shows "Kopiert" and the next photo shows "Link" again, the
fullscreen key is listed, `/map` renders tiles and markers, and there
are no console errors.
- All unit tests pass, `tsc` is clean, `npm run lint` has 0 errors with
both rules at their default severity.
- A probe merge with the open admin-translation branch (`AssetPicker`,
`PagePanel`) merges cleanly and lints clean.

Closes #864

Co-authored-by: ralksta <ralksta@users.noreply.github.com>
Changelog section for v0.21.0 and the README "Latest" line. Scope: the
16 commits on dev since v0.20.1; #853/#858 moved to v0.22.0.

Co-authored-by: ralksta <ralksta@users.noreply.github.com>
)

CodeQL flags the new thumbnail `src` in the journal creation modal
(#852) as js/xss-through-dom on the v0.21.0 release PR (#871, alert
118). The id is now passed through `encodeURIComponent`, so it can only
ever be one path segment.

Co-authored-by: ralksta <ralksta@users.noreply.github.com>
Closes #877.

Every release is already pushed to `ghcr.io/ralksta/immich-folio`
(public, `latest` / `x.y` / `x.y.z` / `sha-…`), but nothing in the docs
said so: the README and `docs/deployment.md` only described building
from a checkout.

## Docs

- **README → Docker** leads with a checkout-free Compose file using the
image, and points at `/install` plus the log line with the setup token.
- **`docs/deployment.md`**: Compose and standalone recipes use the
image; new tag table, update command (`docker compose pull`), and
building from source as its own subsection. `.env.local` is now optional
in the Compose recipe (`required: false`, as in the repo's own
`docker-compose.yml`), since the wizard covers a fresh install.
- **Doctor commands** said `docker compose exec folio …`. That service
doesn't exist (it's `lightbox`), and `exec` runs as root, so the
`content/` writability check passed whatever the ownership was. They now
read `docker compose exec -u nextjs lightbox npm run doctor`.
- `docs/admin-panel.md`: the Docker example's `image: immich-folio` →
the ghcr image.

## arm64

The image was amd64-only: the workflow set up QEMU but never passed
`platforms`. It now builds both architectures natively, using the
[multi-runner pattern from the Docker
docs](https://docs.docker.com/build/ci/github-actions/multi-platform/#distribute-build-across-multiple-runners):

- `build` matrix: `linux/amd64` on `ubuntu-24.04`, `linux/arm64` on
`ubuntu-24.04-arm`, each pushed by digest with its own GHA cache scope.
- `merge` creates the manifest list under the release tags, then runs
the Trivy scan and SARIF upload as before.
- I dropped `type=ref,event=branch`: a manual run from a branch
published a tag named after the branch, which is how the stale `main`
tag (v0.9 era) is still on the registry. A manual run now adds only its
`sha-` tag.

The lockfile already has the arm64-musl builds of `@next/swc` and
`sharp`, so the Dockerfile is unchanged.

## Verification

- Pulled the current `latest` anonymously and ran it with an empty
`content/`: health answers `setup`, `/` shows the setup screen, and the
token is in the log. No checkout needed.
- `npm run doctor` as `-u nextjs` in the image works (as root it reports
a writable `content/` regardless).
- actionlint and Prettier are clean.
- **Workflow run on this branch** ([run
41201750104](https://github.com/ralksta/immich-folio/actions/runs/41201750104)):
both build jobs and the merge passed (arm64 in ~3 min, amd64 ~4 min).
`sha-e801777…` on ghcr lists `linux/amd64` and `linux/arm64`. Pulled on
amd64, it starts into the setup screen. `latest` is untouched.

---------

Co-authored-by: ralksta <ralksta@users.noreply.github.com>
## Summary

Adds link-only standalone albums, and the admin controls needed to share
them safely.

**Unlisted albums**
- A top-level `albums:` entry can set `hidden: true`, keeping its own
`/<slug>` route and photos while staying out of the header menu,
homepage links, sitemap, `/map` and search indexing (`noindex`). JSON-LD
is omitted on the page.
- Photos still load for anyone with the link, and the direct route
returns `200`.
- Albums inside a subpage are unchanged: hide the subpage instead.
- This is unlisting, not access control. A password can be combined with
it.

**Custom link**
- The album drawer now shows the album's address with a copy button.
- The slug can be set explicitly or generated with **Random link** for
an unguessable URL.
- Custom slugs are validated against built-in routes and subpages,
checked for collisions, and used consistently by routing, nav, sitemap,
map and the doctor.
- Changing a slug changes the address; old links stop working, with no
redirect.

**Admin recovery**
- The builder now catches a subpage with no albums before sending,
naming the page instead of surfacing a server 400.
- The floating save bar has **Discard changes**, so a stale or broken
draft can always be reset to the saved version.

No new routes or dependencies. `hidden` and `slug` are preserved by the
page builder and only read on standalone album entries.

## Type of change

- [x] New feature
- [x] Bug fix (admin draft/save recovery)

## Checklist

- [x] Targets the `dev` branch
- [x] `npm run build` passes locally
- [x] `npx tsc --noEmit` passes (0 type errors)
- [x] `npx vitest run` passes in CI
- On a Windows checkout, only the pre-existing
file-permission/POSIX-path assertions fail (`chmod 0600`, POSIX paths,
`rename` semantics).
- [x] No secrets / API keys in diff
- [x] Documentation updated (`docs/gallery-config.md`,
`docs/admin-panel.md`, `docs/features.md`,
`content/gallery.yaml.example`)

## Testing

- `npx tsc --noEmit` — clean.
- `npm run build` — passes.
- 220 targeted tests pass, covering config derivation, routing/slug
lookup, sitemap/feed visibility, map listings, metadata, admin
round-tripping and i18n parity.
- End-to-end against the mock Immich server:
- toggled **Unlisted** in the admin and saved; the PUT returned `200`
and wrote `hidden: true`;
- the album disappeared from the homepage nav and the admin card showed
the unlisted badge;
  - the direct URL still returned `200` with `noindex` and no JSON-LD;
  - toggling it back republished the home link;
- set a custom slug, saved, and confirmed the new URL served `200` while
the old one returned `404`;
  - generated a random link and copied it from the drawer;
- removing a subpage's last album was stopped before the request with a
message naming the page, and **Discard changes** restored the saved
state.
#873)

Closes #691.

The same concept carried several names across the panel, the visitor
strings and the docs, so an owner had to learn that *Page*, *Subpage*
and *Collection* are one thing while "Photo Essay" meant two different
features.

## What changed

| Concept | Before | Now |
| --- | --- | --- |
| A named group of albums | Subpage / Collection | **Page** (`subpages`
in YAML) |
| A `content/pages/*.md` entry | Page | **Content Page** |
| A dated post under `/journal` | Story, entry, article | **Journal
entry** |
| `grid.layout: essay` | "Photo Essay Mode", "Storytelling & Photo Essay
Builder" | **Story Layout** |
| An album's `heroImage` | "Custom Hero Image" | **Cover Photo** (Hero
stays the homepage hero) |

- **One layout table.** Settings › Grid, an album's override and a
page's layout each carried their own copy of the labels; they had
drifted (the page sheet lacked `justified`, described `uniform`
differently). They now read `common.layouts` through
`lib/admin/layouts.ts`.
- **One name for one action.** Diagnostics' "Clear cache" and the
header's "Reload" hit the same endpoint; both say **Reload** now, and
the tooltip/cache detail explain that it also re-reads the config.
- **English visitor copy.** Sentence case throughout, no emoji or arrows
inside strings — the heart, envelope and arrow are markup now, so a
screen reader no longer announces "red heart" and a translation can
place them. `lib/__tests__/i18n.test.ts` gains a check for both rules.
- **Docs.** A vocabulary table in `docs/admin-panel.md` with the
unchanged YAML keys beside each name; the journal guide stops calling
entries "photo essays"; the gallery docs use the US spelling "favorites"
like the panel and Immich.

## Backward compatibility

Nothing a running deployment stores changes:

- **YAML keys unchanged** — `subpages`, `albums`, `sections`,
`heroImage`, `grid.layout: essay`, `proofing` keep their names; only
labels move.
- **URLs, slugs and cookie names unchanged.**
- **Content files unchanged** — no migration, no rewrite on the next
admin save.
- **Visitor copy for a page keeps saying "Collection"** in the kicker
and the `/map` meta line. That is deliberately site copy, not the
panel's vocabulary, and changing it would touch every visitor locale;
the issue's table is scoped to owner-facing names. Say the word if you
want "Page" there too — it is two strings per locale.
- The API's default collision text (`COLLISION_TEXT_EN`) says "is
already a page", matching the panel.

## Already fixed before this PR

Five bullets on the issue were stale: the password gate already has a
per-type subtitle (`siteSubtitle`/`pageSubtitle`/`journalSubtitle`), the
legal page already cites the DDG, the footer says "Sign out" (not
"Logout"), the theme metadata already lives once in `PRESET_REGISTRY`,
and proofing already speaks of a "Selection". The `albumEntries.ts`
comment claiming `download` has no UI is corrected.

## Verification

- `npx tsc --noEmit` — clean apart from the repo's existing
stale-`.next` and `image-url-per-tier` errors (the latter from this
machine's outdated `node_modules`).
- `npm run test:unit` — 2893 passed with the pinned dependencies (230
files).
- `npm run build` — passes (exit 0) with the pinned dependencies.
- `npx eslint` / `prettier --check` on the touched files — clean.
- Not live-tested against a real Immich instance; every changed string
is display-only apart from the two layout `<select>`s, which now read
the shared table.

---------

Co-authored-by: ralksta <ralksta@users.noreply.github.com>
… (#881)

Moves the map, Leaflet popup/marker and admin panel rules out of
`app/themes/studio-modern.css` into the stylesheets that own those
surfaces:

- `/map` header and container overrides (`[data-preset='studio-modern']
.map-page__*`, `.map-container`) → `app/map/map.css`
- Shared Leaflet marker and popup overrides
(`[data-preset='studio-modern'] .map-marker`, `.leaflet-popup-*`,
`.map-popup__*`) → `app/leaflet.css`
- Admin token overrides (`[data-preset='studio-modern'] .admin-layout`
dark and light) → `app/admin/styles/tokens.css`, folding in the
contrast-corrected `--admin-text-muted` and `--admin-on-accent` values
from #795

### Tests
- `lib/__tests__/preset-registry.test.ts` now asserts that no preset
stylesheet in `app/themes/*.css` styles `.admin-layout`, `.map-page`,
`.map-container`, `.map-marker`, `.map-popup` or `.leaflet-*`.
- `lib/__tests__/accent-text.test.ts` includes `app/map/map.css` in the
raw-accent text check.

Closes #613

Co-authored-by: ralksta <ralksta@users.noreply.github.com>
Follow-up to #873: the panel renamed **Story Settings** to **Entry
Settings**, but `docs/journal.md` still used the old name in the text
and in the screenshot's alt text. The screenshot's dialog title is the
old one too; it'll be updated with the next screenshot refresh.

Co-authored-by: ralksta <ralksta@users.noreply.github.com>
Bumps the nextjs group with 2 updates:
[next](https://github.com/vercel/next.js) and
[eslint-config-next](https://github.com/vercel/next.js/tree/HEAD/packages/eslint-config-next).

Updates `next` from 16.4.2 to 16.4.3
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/vercel/next.js/releases">next's
releases</a>.</em></p>
<blockquote>
<h2>v16.4.3</h2>
<blockquote>
<p>[!NOTE]
This release is backporting bug fixes. It does <strong>not</strong>
include all pending features/changes on canary.</p>
</blockquote>
<h3>Core Changes</h3>
<ul>
<li>[lts-active] Fix extensionless Google font URLs in both bundlers (<a
href="https://redirect.github.com/vercel/next.js/issues/100277">#100277</a>)</li>
<li>[lts-active] Don't load the app-page runtime in proxy.ts (<a
href="https://redirect.github.com/vercel/next.js/issues/100423">#100423</a>)</li>
<li>[lts-active] Revert &quot;Turbopack: loader change invalidation (<a
href="https://redirect.github.com/vercel/next.js/issues/95732">#95732</a>)&quot;
(<a
href="https://redirect.github.com/vercel/next.js/issues/100438">#100438</a>)</li>
<li>[lts-active][cd] Gate release bot credentials behind GitHub
environments (<a
href="https://redirect.github.com/vercel/next.js/issues/100123">#100123</a>)</li>
<li>[lts-active] test: Pin react-virtualized (<a
href="https://redirect.github.com/vercel/next.js/issues/100545">#100545</a>)</li>
<li>[lts-active][cd] Move release un-draft to a follow-up job in
bot-autonomous (<a
href="https://redirect.github.com/vercel/next.js/issues/100572">#100572</a>)</li>
<li>[lts-active] Keep OpenTelemetry request traces together (<a
href="https://redirect.github.com/vercel/next.js/issues/100349">#100349</a>,
<a
href="https://redirect.github.com/vercel/next.js/issues/100294">#100294</a>,
<a
href="https://redirect.github.com/vercel/next.js/issues/100324">#100324</a>,
<a
href="https://redirect.github.com/vercel/next.js/issues/100350">#100350</a>,
<a
href="https://redirect.github.com/vercel/next.js/issues/100552">#100552</a>,
<a
href="https://redirect.github.com/vercel/next.js/issues/100583">#100583</a>)
(<a
href="https://redirect.github.com/vercel/next.js/issues/100542">#100542</a>)</li>
<li>[lts-active] Avoid loading standalone React in server bootstrap (<a
href="https://redirect.github.com/vercel/next.js/issues/100610">#100610</a>)</li>
</ul>
<h3>Credits</h3>
<p>Huge thanks to <a
href="https://github.com/mischnic"><code>@​mischnic</code></a>, <a
href="https://github.com/lukesandberg"><code>@​lukesandberg</code></a>,
<a href="https://github.com/acdlite"><code>@​acdlite</code></a>, <a
href="https://github.com/eps1lon"><code>@​eps1lon</code></a>, and <a
href="https://github.com/sokra"><code>@​sokra</code></a> for
helping!</p>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/vercel/next.js/commit/29bbeab09981148ade186e8599ecdede15abc281"><code>29bbeab</code></a>
v16.4.3</li>
<li><a
href="https://github.com/vercel/next.js/commit/5117201d795c7a4d185bad044cef545be0b4e504"><code>5117201</code></a>
[lts-active] Avoid loading standalone React in server bootstrap (<a
href="https://redirect.github.com/vercel/next.js/issues/100610">#100610</a>)</li>
<li><a
href="https://github.com/vercel/next.js/commit/37d603bf4d91cd4ecf97b9090b1dde61d84a2296"><code>37d603b</code></a>
[lts-active] Keep OpenTelemetry request traces together (<a
href="https://redirect.github.com/vercel/next.js/issues/100349">#100349</a>,
<a
href="https://redirect.github.com/vercel/next.js/issues/100294">#100294</a>,
<a
href="https://redirect.github.com/vercel/next.js/issues/1">#1</a>...</li>
<li><a
href="https://github.com/vercel/next.js/commit/cf9d27d96c7d9efa5b7ba0a5a30d2817b01364e7"><code>cf9d27d</code></a>
[lts-active][cd] Move release un-draft to a follow-up job in
bot-autonomous (...</li>
<li><a
href="https://github.com/vercel/next.js/commit/3f6a632d413e57004718297543677caea7219ba4"><code>3f6a632</code></a>
[lts-active] test: Pin react-virtualized (<a
href="https://redirect.github.com/vercel/next.js/issues/100545">#100545</a>)</li>
<li><a
href="https://github.com/vercel/next.js/commit/f715bba5e2d7ce4f76cb41d2ed06ee44ac71dc9a"><code>f715bba</code></a>
[lts-active][cd] Gate release bot credentials behind GitHub environments
(<a
href="https://redirect.github.com/vercel/next.js/issues/10">#10</a>...</li>
<li><a
href="https://github.com/vercel/next.js/commit/c864c78fb14b79f47c152e17d7fce64f3b88f1db"><code>c864c78</code></a>
[lts-active] Revert &quot;Turbopack: loader change invalidation (<a
href="https://redirect.github.com/vercel/next.js/issues/95732">#95732</a>)&quot;
(<a
href="https://redirect.github.com/vercel/next.js/issues/100438">#100438</a>)</li>
<li><a
href="https://github.com/vercel/next.js/commit/f3ff943085e07643be8ab18435611d6c259e8cb2"><code>f3ff943</code></a>
[lts-active] Don't load the app-page runtime in proxy.ts (<a
href="https://redirect.github.com/vercel/next.js/issues/100423">#100423</a>)</li>
<li><a
href="https://github.com/vercel/next.js/commit/f697cf5db456a846ea2f83ef16a85d34afbda3d5"><code>f697cf5</code></a>
[lts-active] Fix extensionless Google font URLs in both bundlers (<a
href="https://redirect.github.com/vercel/next.js/issues/100277">#100277</a>)</li>
<li>See full diff in <a
href="https://github.com/vercel/next.js/compare/v16.4.2...v16.4.3">compare
view</a></li>
</ul>
</details>
<br />

Updates `eslint-config-next` from 16.4.2 to 16.4.3
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/vercel/next.js/releases">eslint-config-next's
releases</a>.</em></p>
<blockquote>
<h2>v16.4.3</h2>
<blockquote>
<p>[!NOTE]
This release is backporting bug fixes. It does <strong>not</strong>
include all pending features/changes on canary.</p>
</blockquote>
<h3>Core Changes</h3>
<ul>
<li>[lts-active] Fix extensionless Google font URLs in both bundlers (<a
href="https://github.com/vercel/next.js/tree/HEAD/packages/eslint-config-next/issues/100277">#100277</a>)</li>
<li>[lts-active] Don't load the app-page runtime in proxy.ts (<a
href="https://github.com/vercel/next.js/tree/HEAD/packages/eslint-config-next/issues/100423">#100423</a>)</li>
<li>[lts-active] Revert &quot;Turbopack: loader change invalidation (<a
href="https://github.com/vercel/next.js/tree/HEAD/packages/eslint-config-next/issues/95732">#95732</a>)&quot;
(<a
href="https://github.com/vercel/next.js/tree/HEAD/packages/eslint-config-next/issues/100438">#100438</a>)</li>
<li>[lts-active][cd] Gate release bot credentials behind GitHub
environments (<a
href="https://github.com/vercel/next.js/tree/HEAD/packages/eslint-config-next/issues/100123">#100123</a>)</li>
<li>[lts-active] test: Pin react-virtualized (<a
href="https://github.com/vercel/next.js/tree/HEAD/packages/eslint-config-next/issues/100545">#100545</a>)</li>
<li>[lts-active][cd] Move release un-draft to a follow-up job in
bot-autonomous (<a
href="https://github.com/vercel/next.js/tree/HEAD/packages/eslint-config-next/issues/100572">#100572</a>)</li>
<li>[lts-active] Keep OpenTelemetry request traces together (<a
href="https://github.com/vercel/next.js/tree/HEAD/packages/eslint-config-next/issues/100349">#100349</a>,
<a
href="https://github.com/vercel/next.js/tree/HEAD/packages/eslint-config-next/issues/100294">#100294</a>,
<a
href="https://github.com/vercel/next.js/tree/HEAD/packages/eslint-config-next/issues/100324">#100324</a>,
<a
href="https://github.com/vercel/next.js/tree/HEAD/packages/eslint-config-next/issues/100350">#100350</a>,
<a
href="https://github.com/vercel/next.js/tree/HEAD/packages/eslint-config-next/issues/100552">#100552</a>,
<a
href="https://github.com/vercel/next.js/tree/HEAD/packages/eslint-config-next/issues/100583">#100583</a>)
(<a
href="https://github.com/vercel/next.js/tree/HEAD/packages/eslint-config-next/issues/100542">#100542</a>)</li>
<li>[lts-active] Avoid loading standalone React in server bootstrap (<a
href="https://github.com/vercel/next.js/tree/HEAD/packages/eslint-config-next/issues/100610">#100610</a>)</li>
</ul>
<h3>Credits</h3>
<p>Huge thanks to <a
href="https://github.com/mischnic"><code>@​mischnic</code></a>, <a
href="https://github.com/lukesandberg"><code>@​lukesandberg</code></a>,
<a href="https://github.com/acdlite"><code>@​acdlite</code></a>, <a
href="https://github.com/eps1lon"><code>@​eps1lon</code></a>, and <a
href="https://github.com/sokra"><code>@​sokra</code></a> for
helping!</p>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/vercel/next.js/commit/29bbeab09981148ade186e8599ecdede15abc281"><code>29bbeab</code></a>
v16.4.3</li>
<li>See full diff in <a
href="https://github.com/vercel/next.js/commits/v16.4.3/packages/eslint-config-next">compare
view</a></li>
</ul>
</details>
<br />


Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore <dependency name> major version` will close this
group update PR and stop Dependabot creating any more for the specific
dependency's major version (unless you unignore this specific
dependency's major version or upgrade to it yourself)
- `@dependabot ignore <dependency name> minor version` will close this
group update PR and stop Dependabot creating any more for the specific
dependency's minor version (unless you unignore this specific
dependency's minor version or upgrade to it yourself)
- `@dependabot ignore <dependency name>` will close this group update PR
and stop Dependabot creating any more for the specific dependency
(unless you unignore this specific dependency or upgrade to it yourself)
- `@dependabot unignore <dependency name>` will remove all of the ignore
conditions of the specified dependency
- `@dependabot unignore <dependency name> <ignore condition>` will
remove the ignore condition of the specified dependency and ignore
conditions


</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
…#893)

Supersedes #874, #875 and #876, which conflicted with the lockfile after
#891 and did not rebase on request. Same versions, one lockfile update.
tsc 0, lint 0 errors, 2905 unit tests pass; Prettier 3.9.10 flags no
file that 3.9.9 did not.

Co-authored-by: ralksta <ralksta@users.noreply.github.com>
Changelog section for v0.22.0 (#878, #879, #873, #891, #874–#876, #881),
the README's Latest line, and @lancetm714's v0.22.0 line in
CONTRIBUTORS.md.

Co-authored-by: ralksta <ralksta@users.noreply.github.com>
@ralksta
ralksta merged commit 3671cfe into main Nov 4, 2026
7 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants