release: v0.20.0
This commit is contained in:
@@ -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>
|
||||
)}
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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}>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 };
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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 },
|
||||
],
|
||||
});
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user