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

@@ -4,18 +4,28 @@ import { useState } from 'react';
import * as Icons from '../Icons';
import type { SaveStatus } from '../SaveBar';
import { resolveExifDisplay } from '@/lib/config/schema';
import { SUPPORTED_LOCALES } from '@/lib/i18n';
import { SUPPORTED_LOCALES, getDictionary, resolveLocale } from '@/lib/i18n';
import { FeatureGroup, FeatureRow, SettingRow } from './fields';
import type { SectionProps } from './types';
import { useConfirm } from '../ConfirmDialog';
export default function GeneralSection({ settings, update, updateMany }: SectionProps) {
export default function GeneralSection({
settings,
update,
updateMany,
siteTitleEnv,
}: SectionProps & { siteTitleEnv?: string | null }) {
const confirm = useConfirm();
// What the site shows while the field is empty, resolved as getConfig does:
// SITE_TITLE, else the site language's word for a gallery (QA A-15).
const fallbackTitle = siteTitleEnv || getDictionary(resolveLocale(settings.lang)).common.gallery;
// Collapsed by default: the four metadata switches are a detail of one
// decision, and showing them permanently is what made the section a wall (#510).
const [metadataOpen, setMetadataOpen] = useState(false);
const [faviconUploading, setFaviconUploading] = useState(false);
const [faviconStatus, setFaviconStatus] = useState<SaveStatus>(null);
/** Bumped after an upload or reset so the preview fetches the new icon. */
const [faviconVersion, setFaviconVersion] = useState(0);
// Resolved the same way the site resolves it, so the switches show what a
// visitor actually sees — including a config that only ever set the older
@@ -62,8 +72,14 @@ export default function GeneralSection({ settings, update, updateMany }: Section
id="general-site-title"
value={settings.title || ''}
onChange={(e) => update('title', e.target.value)}
placeholder="My Portfolio"
placeholder={fallbackTitle}
aria-describedby="general-site-title-hint"
/>
<p id="general-site-title-hint" className="admin-field-hint">
{settings.title
? 'Shown in the header, the browser tab and link previews.'
: `Empty, so the site shows “${fallbackTitle}”${siteTitleEnv ? ', from SITE_TITLE' : ''}.`}
</p>
</div>
<div className="admin-field">
<label htmlFor="general-subtitle">Subtitle</label>
@@ -136,6 +152,13 @@ export default function GeneralSection({ settings, update, updateMany }: Section
checked={settings.proofing?.enabled !== false}
onToggle={() => update('proofing.enabled', settings.proofing?.enabled === false)}
/>
<FeatureRow
icon={<Icons.IconSearch size={15} />}
title="Photo zoom"
description="Visitors zoom into a photo to full resolution (1:1) to check sharpness. Pages and albums can override this"
checked={settings.zoom === true}
onToggle={() => update('zoom', !settings.zoom)}
/>
</div>
{/* Shown even while proofing is off globally: a subpage can switch it
on for itself (`proofing: true`), so the address still has to be
@@ -249,12 +272,30 @@ export default function GeneralSection({ settings, update, updateMany }: Section
</FeatureGroup>
<div className="admin-field favicon-field">
<span className="admin-field-label">Favicon</span>
<span className="admin-field-label" id="favicon-label">
Favicon
</span>
<div className="favicon-row">
{/* eslint-disable-next-line @next/next/no-img-element -- the site's own icon route, a 32px preview */}
<img
className="favicon-preview"
src={`/api/favicon?v=${faviconVersion}`}
alt="Current favicon"
width={32}
height={32}
/>
{/* The native file input stays the control — focusable, keyboard
operable, announced as a file picker — but is visually hidden;
its label is drawn as the admin's secondary button, with the
focus ring passed through (QA A-18). */}
<input
id="favicon-file"
className="favicon-file-input"
type="file"
accept=".svg,.png,.ico,.jpg,.jpeg"
disabled={faviconUploading}
aria-labelledby="favicon-label favicon-file-button"
aria-describedby="favicon-hint"
onChange={async (e) => {
const file = e.target.files?.[0];
if (!file) return;
@@ -273,6 +314,7 @@ export default function GeneralSection({ settings, update, updateMany }: Section
? { kind: 'success', message: data.message }
: { kind: 'error', message: `Error: ${data.error}` },
);
if (res.ok) setFaviconVersion((v) => v + 1);
} catch {
setFaviconStatus({ kind: 'error', message: 'Error: Upload failed' });
} finally {
@@ -281,9 +323,16 @@ export default function GeneralSection({ settings, update, updateMany }: Section
}
}}
/>
<label
htmlFor="favicon-file"
id="favicon-file-button"
className="admin-btn admin-btn-sm admin-btn-secondary favicon-upload-btn"
>
<Icons.IconImage size={14} /> Upload icon
</label>
<button
type="button"
className="admin-btn"
className="admin-btn admin-btn-sm admin-btn-secondary"
disabled={faviconUploading}
onClick={async () => {
// Deletes the uploaded file at once, outside the staged form.
@@ -304,6 +353,7 @@ export default function GeneralSection({ settings, update, updateMany }: Section
? { kind: 'success', message: data.message }
: { kind: 'error', message: `Error: ${data.error}` },
);
if (res.ok) setFaviconVersion((v) => v + 1);
} catch {
setFaviconStatus({ kind: 'error', message: 'Error: Reset failed' });
} finally {
@@ -318,7 +368,7 @@ export default function GeneralSection({ settings, update, updateMany }: Section
{faviconStatus && (
<p className={`save-message ${faviconStatus.kind}`}>{faviconStatus.message}</p>
)}
<span className="admin-field-hint">
<span id="favicon-hint" className="admin-field-hint">
SVG, PNG, ICO, or JPEG — max 512 kB. Stored in the content volume. Reset restores the
bundled default.
</span>