experimental: portfolio features (justified layout, unlisted pages, captions, per-album grid, focal point, nav links, cover hero) (#431)

## Was ist das?

Sieben neue Portfolio-Features, alle als **experimental** gekennzeichnet
— in YAML-Kommentaren, Admin-Panel-Labels und Commit-Scopes, damit sie
einzeln nachvollziehbar und revertierbar bleiben.

## Features (je ein Commit)

| Commit | Feature |
| --- | --- |
| `justified rows gallery layout` | Neues Grid-Layout `justified`:
zeilenbasiert, gleiche Höhen, volle Breite — Flexbox ohne JS-Messung,
letzte Zeile bleibt natürlich klein |
| `hide subpages from navigation` | `hidden: true`: Seite aus
Nav/Homepage raus, per Direktlink erreichbar — im Gegensatz zu `enabled:
false` (404) |
| `lightbox captions` | Immich-Asset-Description als Bildunterschrift im
Info-Panel; EXIF-Route liefert bei `exifOnHover: false` jetzt nur noch
die Caption statt 403 |
| `per-album grid overrides` | `grid:` pro Album-Eintrag, Präzedenz
global < Subpage < Album |
| `per-album cover focal point` | `coverPosition:` → `object-position`
auf Cover-Karten, strikt validiert |
| `external links in header navigation` | `navLinks:` in settings.yaml,
nur http(s), sanitisiert |
| `cover splash hero style` | `heroStyle: cover`: Fullscreen-Splash mit
Enter-Link ins Portfolio |

Alle Optionen sind im Admin-Panel editierbar (PageBuilder /
SettingsEditor).

## Tests

- 356 Unit-Tests grün (13 neue: deriveGallery hidden/grid/coverPosition,
sanitizeNavLinks, EXIF-Route-Captions, justified-Layout-Validierung)
- `npx tsc --noEmit`: 0 Fehler
- `npm run build`: sauber
- Justified-Zeilenmathematik und Cover-Splash per Browser-Harness
visuell/numerisch verifiziert (volle Zeilen enden exakt an der
Containerkante, letzte Zeile bleibt natürlich)

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: ralksta <ralksta@users.noreply.github.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: lancetm714 <108419425+lancetm714@users.noreply.github.com>
This commit is contained in:
Ralksta
2026-08-14 17:56:22 +02:00
committed by GitHub
parent 2fe1157a5d
commit 84c7eeef40
24 changed files with 916 additions and 84 deletions

View File

@@ -2220,7 +2220,9 @@ h3 .svg-icon {
.album-drawer {
width: 100%;
max-width: 620px;
/* Narrower than the subpage sheet (1100px) so the nested editor still reads
as a layer on top of it, but wide enough for the same two-column body. */
max-width: 960px;
max-height: 90vh;
background: var(--admin-bg-raised);
border: 1px solid var(--admin-border);
@@ -2255,8 +2257,9 @@ h3 .svg-icon {
gap: 0.5rem;
}
/* Single-column body — and the drawer's only scroll container. Nesting a second
scroller here is what used to clip the sort listbox popup. */
/* The drawer's only scroll container. Nesting a second scroller here is what
used to clip the sort listbox popup — the two columns inside must stay
overflow-free and scroll together. */
.album-drawer-body {
flex: 1;
min-height: 0;
@@ -2278,6 +2281,20 @@ h3 .svg-icon {
}
}
/* Full-screen on phones, mirroring the subpage sheet. */
@media (max-width: 560px) {
.album-drawer-overlay {
padding: 0;
}
.album-drawer {
max-width: none;
max-height: none;
height: 100dvh;
border-radius: 0;
border: none;
}
}
.album-drawer-body::-webkit-scrollbar {
width: 6px;
}
@@ -3470,7 +3487,9 @@ h3 .svg-icon {
border-radius: 4px;
}
/* ── Subpage Slide-Over Drawer Modal ──────────────────────────── */
/* ── Subpage Editor Sheet (centered two-column modal) ─────────── */
/* z-index 999 is load-bearing: the album detail modal (1000) and the
portalled Listbox popup (1100) must stack above this sheet. */
.subpage-drawer-backdrop {
position: fixed;
inset: 0;
@@ -3478,28 +3497,37 @@ h3 .svg-icon {
backdrop-filter: blur(6px);
z-index: 999;
display: flex;
justify-content: flex-end;
align-items: center;
justify-content: center;
padding: 1.5rem;
animation: fadeIn 0.2s ease-out;
}
.subpage-drawer-container {
width: 100%;
max-width: 620px;
height: 100vh;
max-width: 1100px;
max-height: 90vh;
background: var(--admin-bg-card, #181818);
border-left: 1px solid var(--admin-border, #333);
border: 1px solid var(--admin-border, #333);
border-radius: 16px;
overflow: hidden;
display: flex;
flex-direction: column;
box-shadow: -10px 0 30px rgba(0, 0, 0, 0.5);
animation: slideInRight 0.25s cubic-bezier(0.16, 1, 0.3, 1);
box-shadow: 0 20px 50px rgba(0, 0, 0, 0.4);
/* No fill-mode: after the entry animation the computed transform must
return to none, so the container never becomes a containing block for
fixed-position descendants (Listbox popup measurement). */
animation: sheetIn 0.25s cubic-bezier(0.16, 1, 0.3, 1);
}
@keyframes slideInRight {
@keyframes sheetIn {
from {
transform: translateX(100%);
opacity: 0;
transform: scale(0.96) translateY(10px);
}
to {
transform: translateX(0);
opacity: 1;
transform: scale(1) translateY(0);
}
}
@@ -3538,6 +3566,8 @@ h3 .svg-icon {
overflow-y: auto;
overscroll-behavior: contain;
flex: 1;
/* Required so the flex child scrolls under max-height instead of growing */
min-height: 0;
display: flex;
flex-direction: column;
gap: 1.25rem;
@@ -3552,6 +3582,71 @@ h3 .svg-icon {
justify-content: space-between;
}
/* ── Shared two-column layout for admin editor sheets ─────────── */
/* Used by the subpage editor and the album detail editor so both read as the
same surface. Columns always live inside their sheet's single body scroller
— never give a column its own overflow, a nested scroller clips the
portalled Listbox popup (see the .album-drawer-body note). */
.admin-sheet-columns {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 2rem;
align-items: start;
}
/* The subpage editor is asymmetric on purpose: the left column is a settings
form with a natural maximum, the right one holds the album list and wants
every pixel it can get. */
.admin-sheet-columns--settings-aside {
grid-template-columns: minmax(380px, 460px) 1fr;
}
.admin-sheet-col {
display: flex;
flex-direction: column;
gap: 1.25rem;
min-width: 0;
}
.admin-sheet-col--divided {
border-left: 1px solid var(--admin-border);
padding-left: 2rem;
}
@media (max-width: 1024px) {
.admin-sheet-columns,
.admin-sheet-columns--settings-aside {
grid-template-columns: 1fr;
gap: 1.25rem;
}
.admin-sheet-col--divided {
border-left: none;
padding-left: 0;
}
}
@media (max-width: 768px) {
.subpage-drawer-backdrop {
padding: 0;
}
.subpage-drawer-container {
max-width: none;
max-height: none;
height: 100dvh;
border-radius: 0;
border: none;
}
}
@media (prefers-reduced-motion: reduce) {
.subpage-drawer-container,
.subpage-drawer-backdrop {
animation: none;
}
}
/* Breadcrumb + title stack on the left of the drawer header. */
.subpage-drawer-title-group {
display: flex;