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:
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user