release: v0.20.0

This commit is contained in:
Ralksta
2026-10-02 18:14:23 +02:00
committed by GitHub
parent 81ce886b01
commit 5f831f719a
209 changed files with 13807 additions and 803 deletions

View File

@@ -38,7 +38,7 @@ export function SortableAlbumCard({
};
return (
<div ref={setNodeRef} style={style} {...attributes}>
<div ref={setNodeRef} style={style}>
<AlbumCard
album={album}
name={name}
@@ -46,7 +46,7 @@ export function SortableAlbumCard({
thumbnailId={thumbnailId}
onRemove={onRemove}
onEdit={onEdit}
dragListeners={listeners}
dragListeners={{ ...attributes, ...listeners }}
highlighted={highlighted}
/>
</div>
@@ -86,7 +86,12 @@ export default function AlbumCard({
>
<div className="album-tile-cover">
{dragListeners && (
<div className="album-tile-drag" {...dragListeners} title="Drag to reorder">
<div
className="album-tile-drag"
{...dragListeners}
aria-label={`Reorder ${name}`}
title="Drag to reorder"
>
<IconGripVertical size={18} className="svg-icon svg-drag" />
</div>
)}

View File

@@ -6,6 +6,7 @@ import { Listbox } from '../Listbox';
import { SORT_OPTIONS } from './sortOptions';
import type { AlbumEntry, HeroPickerTarget, OrderEditorTarget } from './types';
import PasswordField from '../fields/PasswordField';
import ZoomOverrideField from '../fields/ZoomOverrideField';
import { useModalDialog } from '@/hooks/useModalDialog';
interface AlbumDrawerProps {
@@ -153,6 +154,14 @@ export default function AlbumDrawer({
</div>
</button>
</div>
<ZoomOverrideField
id="album-zoom"
value={album.zoom}
onChange={(zoom) => onUpdate({ zoom })}
inheritLabel="Inherit from page / site settings"
hint="Lets visitors zoom into a photo in the viewer to check sharpness. JPEG and AVIF originals are shown without their GPS data; HEIC and RAW need Immich's full-size previews. PNG, WebP, GIF and photos edited in Immich are never zoomed. Applies where the album is listed here."
/>
</div>
<div className="admin-sheet-col admin-sheet-col--divided">

View File

@@ -35,8 +35,17 @@ export function SortableHeroTile({
};
return (
<div ref={setNodeRef} style={style} className="hero-tile" {...attributes}>
<div className="hero-tile-drag" {...listeners} title="Drag to reorder">
<div ref={setNodeRef} style={style} className="hero-tile">
{/* The handle, not the tile, is the sortable control: the tile holds a
remove button, and a button inside role="button" is nested
interactive content (QA A-20). */}
<div
className="hero-tile-drag"
{...attributes}
{...listeners}
aria-label={`Reorder hero photo ${index + 1}`}
title="Drag to reorder"
>
<IconGripVertical size={18} className="svg-icon svg-drag" />
</div>
<img src={`/api/admin/thumbnail/${id}`} alt="" loading="lazy" />
@@ -88,9 +97,14 @@ export function SortableSubpageTile({
style={style}
className={`subpage-tile ${isActive ? 'active' : ''}`}
onClick={onClick}
{...attributes}
>
<div className="subpage-tile-drag" {...listeners} title="Drag to reorder">
<div
className="subpage-tile-drag"
{...attributes}
{...listeners}
aria-label={`Reorder ${sp.name || 'page'}`}
title="Drag to reorder"
>
<IconGripVertical size={18} className="svg-icon svg-drag" />
</div>
@@ -185,9 +199,14 @@ export function SortableSubpageRow({
zIndex: isDragging ? 10 : undefined,
}}
className={`pb-row ${isActive ? 'active' : ''} ${sp.enabled === false ? 'is-off' : ''}`}
{...attributes}
>
<span className="pb-row-grip" {...listeners} title="Drag to reorder">
<span
className="pb-row-grip"
{...attributes}
{...listeners}
aria-label={`Reorder ${sp.name || 'page'}`}
title="Drag to reorder"
>
<IconGripVertical size={14} />
</span>
<button type="button" className="pb-row-main" onClick={onClick} aria-current={isActive}>
@@ -252,9 +271,14 @@ export function SortablePageRow({
zIndex: isDragging ? 10 : undefined,
}}
className={`pb-row ${isActive ? 'active' : ''} ${draft || missing ? 'is-off' : ''}`}
{...attributes}
>
<span className="pb-row-grip" {...listeners} title="Drag to reorder">
<span
className="pb-row-grip"
{...attributes}
{...listeners}
aria-label={`Reorder ${title}`}
title="Drag to reorder"
>
<IconGripVertical size={14} />
</span>
<button type="button" className="pb-row-main" onClick={onClick} aria-current={isActive}>

View File

@@ -23,6 +23,7 @@ import {
import AlbumCard, { SortableAlbumCard } from './AlbumCard';
import SubpagePreview from './SubpagePreview';
import PasswordField from '../fields/PasswordField';
import ZoomOverrideField from '../fields/ZoomOverrideField';
import {
normalizeSubpageGrid,
type ActiveEditAlbumAddress,
@@ -120,20 +121,20 @@ export default function SubpageDrawer({
<h3 className="subpage-drawer-title">{sp.title || sp.name || 'Untitled Page'}</h3>
</div>
<div className="subpage-drawer-header-actions">
<div className="segmented-control" style={{ padding: '2px' }}>
<div className="segmented-control segmented-control--sm" role="group" aria-label="View">
<button
type="button"
className={`segment-btn ${drawerMode === 'edit' ? 'active' : ''}`}
aria-pressed={drawerMode === 'edit'}
onClick={() => onDrawerModeChange('edit')}
style={{ padding: '0.2rem 0.6rem', fontSize: '0.75rem' }}
>
<IconPencil size={14} /> Edit
</button>
<button
type="button"
className={`segment-btn ${drawerMode === 'preview' ? 'active' : ''}`}
aria-pressed={drawerMode === 'preview'}
onClick={() => onDrawerModeChange('preview')}
style={{ padding: '0.2rem 0.6rem', fontSize: '0.75rem' }}
>
<IconSearch size={14} /> Live Preview
</button>
@@ -189,7 +190,9 @@ export default function SubpageDrawer({
onChange={(e) =>
updateSubpage(spIndex, { title: e.target.value || undefined })
}
placeholder="Display title (defaults to name)"
// Short enough for the half-width column at any size; the
// old "Display title (defaults to name)" was cut off (A-18).
placeholder="Same as page name"
/>
</div>
<div className="admin-field">
@@ -343,6 +346,19 @@ export default function SubpageDrawer({
</select>
</div>
{/* Zoom belongs to the album viewer; a photo essay has none. */}
{sp.grid?.layout !== 'essay' && sp.essayText == null && !sp.essayFile && (
<div style={{ marginTop: '1rem' }}>
<ZoomOverrideField
id="subpage-zoom"
value={sp.zoom}
onChange={(zoom) => updateSubpage(spIndex, { zoom })}
inheritLabel="Inherit from site settings"
hint="Applies to every album on this page that does not set its own. Settings › General has the site-wide switch."
/>
</div>
)}
{/* Album cover grid — hidden in essay mode, which
renders a story instead of a cover grid. The
album count is deliberately not checked: it

View File

@@ -30,7 +30,9 @@ export function hasAlbumOptions(entry: AlbumEntry): boolean {
entry.grid ||
entry.coverPosition ||
entry.location ||
entry.download,
entry.download ||
// `zoom: false` is an option too: it switches zoom off for this album.
entry.zoom !== undefined,
);
}
@@ -52,6 +54,7 @@ export function parseAlbumEntries(raw: RawAlbumEntry[] | undefined): AlbumEntry[
coverPosition: value.coverPosition,
location: value.location,
download: value.download,
zoom: typeof value.zoom === 'boolean' ? value.zoom : undefined,
};
});
}
@@ -82,6 +85,8 @@ export function serializeAlbumEntries(entries: AlbumEntry[]): RawAlbumEntry[] {
// Same story as location: set by hand in YAML since #475, with no UI, so
// the builder was turning original downloads back off on the next save.
if (entry.download) val.download = entry.download;
// Both values mean something: false switches zoom off for this album.
if (entry.zoom !== undefined) val.zoom = entry.zoom;
return { [entry.id]: val };
});
}

View File

@@ -115,7 +115,7 @@ export const addSubpage = (g: GalleryState): GalleryState => ({
...g,
subpages: [
...g.subpages,
{ name: `New Page ${g.subpages.length + 1}`, albums: [], sections: undefined },
{ name: `New subpage ${g.subpages.length + 1}`, albums: [], sections: undefined },
],
});

View File

@@ -62,6 +62,7 @@ export function parseGalleryYaml(raw: Record<string, unknown>): GalleryState {
enabled: sp.enabled !== false,
hidden: sp.hidden === true,
proofing: typeof sp.proofing === 'boolean' ? sp.proofing : undefined,
zoom: typeof sp.zoom === 'boolean' ? sp.zoom : undefined,
location: sp.location as string | undefined,
essayText: sp.essayText as string | undefined,
essayFile: sp.essayFile as string | undefined,
@@ -91,6 +92,7 @@ export function parseGalleryYaml(raw: Record<string, unknown>): GalleryState {
enabled: sp.enabled !== false,
hidden: sp.hidden === true,
proofing: typeof sp.proofing === 'boolean' ? sp.proofing : undefined,
zoom: typeof sp.zoom === 'boolean' ? sp.zoom : undefined,
location: sp.location as string | undefined,
essayText: sp.essayText as string | undefined,
essayFile: sp.essayFile as string | undefined,
@@ -127,6 +129,8 @@ export function serializeGallery(gallery: GalleryState): Record<string, unknown>
if (sp.hidden === true) entry.hidden = true;
// Both values mean something: false switches proofing off for this page.
if (sp.proofing !== undefined) entry.proofing = sp.proofing;
// Likewise for zoom (#467).
if (sp.zoom !== undefined) entry.zoom = sp.zoom;
if (sp.essayText) entry.essayText = sp.essayText;
if (sp.essayFile) entry.essayFile = sp.essayFile;
if (sp.grid) entry.grid = sp.grid;

View File

@@ -24,6 +24,11 @@ export interface AlbumEntry {
location?: string;
/** Whether visitors may download the originals from this album (#475). */
download?: boolean;
/**
* Lightbox zoom for this album: true/false override the page and the site,
* undefined inherits (#467).
*/
zoom?: boolean;
}
export interface Section {
@@ -43,6 +48,11 @@ export interface Subpage {
* direction. No control in the builder yet; carried so a save keeps it.
*/
proofing?: boolean;
/**
* Lightbox zoom for the albums on this page, over the site setting in either
* direction; undefined inherits (#467).
*/
zoom?: boolean;
/** EXPERIMENTAL: reachable by direct link, but not shown in navigation */
hidden?: boolean;
essayText?: string;