release: v0.12.0 (#520)
Forty-one commits since v0.11.0. See CHANGELOG.md for the full list and the upgrade notes.
This commit is contained in:
@@ -8,12 +8,22 @@ import SaveBar from './SaveBar';
|
||||
// Direct import from the theme module, not from '@/lib/config': the config
|
||||
// index pulls in `fs` and cannot be bundled into a client component.
|
||||
import { DEFAULT_PRESET } from '@/lib/config/theme';
|
||||
import { resolveExifDisplay, resolveWatermarkOpacity } from '@/lib/config/schema';
|
||||
import { SUPPORTED_LOCALES } from '@/lib/i18n';
|
||||
|
||||
interface Settings {
|
||||
title?: string;
|
||||
subtitle?: string;
|
||||
lang?: string;
|
||||
sitePassword?: string;
|
||||
mode?: 'light' | 'dark' | 'auto';
|
||||
exifOnHover?: boolean;
|
||||
exif?: {
|
||||
camera?: boolean;
|
||||
settings?: boolean;
|
||||
location?: boolean;
|
||||
caption?: boolean;
|
||||
};
|
||||
map?: boolean;
|
||||
transitions?: boolean;
|
||||
scrollToTop?: boolean;
|
||||
@@ -204,6 +214,91 @@ interface SettingsEditorProps {
|
||||
section?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* One labelled block of related switches.
|
||||
*
|
||||
* The features panel was a single flat grid of identical cards, so nothing said
|
||||
* which switches belonged together — and a switch that publishes private notes
|
||||
* looked exactly like one that controls a fade (#510).
|
||||
*/
|
||||
function FeatureGroup({
|
||||
icon,
|
||||
title,
|
||||
description,
|
||||
chip,
|
||||
children,
|
||||
}: {
|
||||
icon: React.ReactNode;
|
||||
title: string;
|
||||
description: string;
|
||||
/** Short marker for a group that needs its own weight, e.g. what visitors see. */
|
||||
chip?: string;
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<div className="settings-group">
|
||||
<div className="settings-group-head">
|
||||
<span className="settings-group-title">
|
||||
{icon}
|
||||
{title}
|
||||
{chip && <span className="settings-group-chip">{chip}</span>}
|
||||
</span>
|
||||
<span className="settings-group-desc">{description}</span>
|
||||
</div>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* A switch inside a group panel. Deliberately lighter than a toggle card: these
|
||||
* are settings *of* the panel they sit in, and rendering them as peers of the
|
||||
* top-level cards is what hid the structure in the first place.
|
||||
*/
|
||||
function SettingRow({
|
||||
title,
|
||||
description,
|
||||
checked,
|
||||
onToggle,
|
||||
disabled,
|
||||
hint,
|
||||
indented,
|
||||
}: {
|
||||
title: string;
|
||||
description: string;
|
||||
checked: boolean;
|
||||
onToggle: () => void;
|
||||
/** A row whose parent switch is off — shown, but inert and explained. */
|
||||
disabled?: boolean;
|
||||
hint?: string;
|
||||
indented?: boolean;
|
||||
}) {
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
className={`setting-row${indented ? ' setting-row--indented' : ''}${
|
||||
disabled ? ' setting-row--disabled' : ''
|
||||
}`}
|
||||
onClick={onToggle}
|
||||
disabled={disabled}
|
||||
aria-pressed={checked}
|
||||
>
|
||||
<span className="setting-row-info">
|
||||
<span className="setting-row-title">{title}</span>
|
||||
<span className="setting-row-desc">{description}</span>
|
||||
{hint && (
|
||||
<span className="setting-row-hint">
|
||||
<Icons.IconBan size={11} /> {hint}
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
<span className={`switch-toggle switch-toggle--sm ${checked && !disabled ? 'on' : ''}`}>
|
||||
<span className="switch-slider" />
|
||||
</span>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
export default function SettingsEditor({ section }: SettingsEditorProps) {
|
||||
const router = useRouter();
|
||||
// An unknown section in the URL falls back to General instead of an empty panel.
|
||||
@@ -212,6 +307,9 @@ export default function SettingsEditor({ section }: SettingsEditorProps) {
|
||||
: 'general';
|
||||
const [settings, setSettings] = useState<Settings>({});
|
||||
const [loading, setLoading] = useState(true);
|
||||
// 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 [saving, setSaving] = useState(false);
|
||||
const [dirty, setDirty] = useState(false);
|
||||
const [saveMessage, setSaveMessage] = useState('');
|
||||
@@ -330,6 +428,25 @@ export default function SettingsEditor({ section }: SettingsEditorProps) {
|
||||
}
|
||||
}
|
||||
|
||||
/** Write several paths in one state update — used by the metadata master switch. */
|
||||
function updateMany(entries: Record<string, unknown>) {
|
||||
setSettings((s) => {
|
||||
const copy = JSON.parse(JSON.stringify(s));
|
||||
for (const [path, value] of Object.entries(entries)) {
|
||||
const parts = path.split('.');
|
||||
let obj = copy;
|
||||
for (let i = 0; i < parts.length - 1; i++) {
|
||||
if (!obj[parts[i]]) obj[parts[i]] = {};
|
||||
obj = obj[parts[i]];
|
||||
}
|
||||
obj[parts[parts.length - 1]] = value;
|
||||
}
|
||||
return copy;
|
||||
});
|
||||
setDirty(true);
|
||||
setSaveMessage('');
|
||||
}
|
||||
|
||||
function update(path: string, value: unknown) {
|
||||
setSettings((s) => {
|
||||
const copy = JSON.parse(JSON.stringify(s));
|
||||
@@ -457,6 +574,34 @@ export default function SettingsEditor({ section }: SettingsEditorProps) {
|
||||
);
|
||||
}
|
||||
|
||||
// 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
|
||||
// `exifOnHover`.
|
||||
const exif = resolveExifDisplay(settings.exif, settings.exifOnHover);
|
||||
|
||||
// The master switch has no key of its own and needs none: "off" is all four
|
||||
// groups off, a state the site already acts on — with nothing left to show,
|
||||
// the lightbox withdraws its info button and the `i` key entirely (#506).
|
||||
const anyMetadata = exif.camera || exif.settings || exif.location || exif.caption;
|
||||
const metadataSummary =
|
||||
[
|
||||
exif.camera && 'Camera & lens',
|
||||
exif.settings && 'Exposure',
|
||||
exif.location && 'Location',
|
||||
exif.caption && 'Description',
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(' · ') || 'Nothing published — the lightbox hides its info panel';
|
||||
|
||||
/** Turning it back on selects everything; the details below narrow it again. */
|
||||
const toggleMetadata = () =>
|
||||
updateMany({
|
||||
'exif.camera': !anyMetadata,
|
||||
'exif.settings': !anyMetadata,
|
||||
'exif.location': !anyMetadata,
|
||||
'exif.caption': !anyMetadata,
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="settings-editor">
|
||||
{/* The About panel edits about.md, not settings.yaml, so it drives the bar itself. */}
|
||||
@@ -537,6 +682,11 @@ export default function SettingsEditor({ section }: SettingsEditorProps) {
|
||||
<option value="es">Español (ES)</option>
|
||||
<option value="ja">日本語 (JA)</option>
|
||||
</select>
|
||||
<p className="admin-field-hint">
|
||||
The visitor-facing interface is translated for {SUPPORTED_LOCALES.join(', ')}.
|
||||
Other languages still set <code><html lang></code> and date formatting, but
|
||||
show the English interface. The admin panel is always English.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="settings-section-divider" />
|
||||
@@ -550,136 +700,224 @@ export default function SettingsEditor({ section }: SettingsEditorProps) {
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="admin-toggle-cards-grid">
|
||||
<button
|
||||
type="button"
|
||||
className={`admin-toggle-card ${settings.exifOnHover !== false ? 'active' : ''}`}
|
||||
onClick={() => update('exifOnHover', settings.exifOnHover === false)}
|
||||
>
|
||||
<div className="toggle-card-info">
|
||||
<span className="toggle-card-title">
|
||||
<Icons.IconCamera size={16} /> EXIF Data on Hover
|
||||
</span>
|
||||
<span className="toggle-card-desc">
|
||||
Display camera gear, lens, aperture & shutter speed on hover
|
||||
</span>
|
||||
</div>
|
||||
<div className={`switch-toggle ${settings.exifOnHover !== false ? 'on' : ''}`}>
|
||||
<span className="switch-slider" />
|
||||
</div>
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
className={`admin-toggle-card ${settings.map === true ? 'active' : ''}`}
|
||||
onClick={() => update('map', !settings.map)}
|
||||
>
|
||||
<div className="toggle-card-info">
|
||||
<span className="toggle-card-title">
|
||||
<Icons.IconMap size={16} /> Interactive GPS Map
|
||||
</span>
|
||||
<span className="toggle-card-desc">
|
||||
Enable /map view showing photo locations on a world map
|
||||
</span>
|
||||
</div>
|
||||
<div className={`switch-toggle ${settings.map === true ? 'on' : ''}`}>
|
||||
<span className="switch-slider" />
|
||||
</div>
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
className={`admin-toggle-card ${settings.transitions !== false ? 'active' : ''}`}
|
||||
onClick={() => update('transitions', settings.transitions === false)}
|
||||
>
|
||||
<div className="toggle-card-info">
|
||||
<span className="toggle-card-title">
|
||||
<Icons.IconSparkles size={16} /> Smooth Page Transitions
|
||||
</span>
|
||||
<span className="toggle-card-desc">
|
||||
Enable subtle fade-in animations between page navigation
|
||||
</span>
|
||||
</div>
|
||||
<div className={`switch-toggle ${settings.transitions !== false ? 'on' : ''}`}>
|
||||
<span className="switch-slider" />
|
||||
</div>
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
className={`admin-toggle-card ${settings.scrollToTop !== false ? 'active' : ''}`}
|
||||
onClick={() => update('scrollToTop', settings.scrollToTop === false)}
|
||||
>
|
||||
<div className="toggle-card-info">
|
||||
<span className="toggle-card-title">
|
||||
<Icons.IconArrowUp size={16} /> Scroll-to-Top Button
|
||||
</span>
|
||||
<span className="toggle-card-desc">
|
||||
Show a floating arrow that returns visitors to the top of long pages
|
||||
</span>
|
||||
</div>
|
||||
<div className={`switch-toggle ${settings.scrollToTop !== false ? 'on' : ''}`}>
|
||||
<span className="switch-slider" />
|
||||
</div>
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
className={`admin-toggle-card ${settings.analytics !== false ? 'active' : ''}`}
|
||||
onClick={() => update('analytics', settings.analytics === false)}
|
||||
>
|
||||
<div className="toggle-card-info">
|
||||
<span className="toggle-card-title">
|
||||
<Icons.IconBarChart size={16} /> Analytics Tracking
|
||||
</span>
|
||||
<span className="toggle-card-desc">
|
||||
Collect anonymous privacy-friendly visit statistics
|
||||
</span>
|
||||
</div>
|
||||
<div className={`switch-toggle ${settings.analytics !== false ? 'on' : ''}`}>
|
||||
<span className="switch-slider" />
|
||||
</div>
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
className={`admin-toggle-card ${settings.proofing?.enabled !== false ? 'active' : ''}`}
|
||||
onClick={() => update('proofing.enabled', settings.proofing?.enabled === false)}
|
||||
>
|
||||
<div className="toggle-card-info">
|
||||
<span className="toggle-card-title">
|
||||
<Icons.IconHeart size={16} /> Client Proofing & Favorites
|
||||
</span>
|
||||
<span className="toggle-card-desc">
|
||||
Allow visitors & clients to heart, filter, and export favorite photo
|
||||
selections
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
className={`switch-toggle ${settings.proofing?.enabled !== false ? 'on' : ''}`}
|
||||
<FeatureGroup
|
||||
icon={<Icons.IconFrame size={13} />}
|
||||
title="Pages & motion"
|
||||
description="Chrome the visitor sees on every page."
|
||||
>
|
||||
<div className="admin-toggle-cards-grid">
|
||||
<button
|
||||
type="button"
|
||||
className={`admin-toggle-card ${settings.about?.enabled !== false ? 'active' : ''}`}
|
||||
onClick={() => update('about.enabled', settings.about?.enabled === false)}
|
||||
>
|
||||
<span className="switch-slider" />
|
||||
</div>
|
||||
</button>
|
||||
<div className="toggle-card-info">
|
||||
<span className="toggle-card-title">
|
||||
<Icons.IconCamera size={16} /> About Page
|
||||
</span>
|
||||
<span className="toggle-card-desc">
|
||||
Show a portrait, bio, and gear section on your portfolio
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
className={`switch-toggle ${settings.about?.enabled !== false ? 'on' : ''}`}
|
||||
>
|
||||
<span className="switch-slider" />
|
||||
</div>
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
className={`admin-toggle-card ${settings.about?.enabled !== false ? 'active' : ''}`}
|
||||
onClick={() => update('about.enabled', settings.about?.enabled === false)}
|
||||
>
|
||||
<div className="toggle-card-info">
|
||||
<span className="toggle-card-title">
|
||||
<Icons.IconCamera size={16} /> About Page
|
||||
<button
|
||||
type="button"
|
||||
className={`admin-toggle-card ${settings.transitions !== false ? 'active' : ''}`}
|
||||
onClick={() => update('transitions', settings.transitions === false)}
|
||||
>
|
||||
<div className="toggle-card-info">
|
||||
<span className="toggle-card-title">
|
||||
<Icons.IconSparkles size={16} /> Smooth Page Transitions
|
||||
</span>
|
||||
<span className="toggle-card-desc">
|
||||
Enable subtle fade-in animations between page navigation
|
||||
</span>
|
||||
</div>
|
||||
<div className={`switch-toggle ${settings.transitions !== false ? 'on' : ''}`}>
|
||||
<span className="switch-slider" />
|
||||
</div>
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
className={`admin-toggle-card ${settings.scrollToTop !== false ? 'active' : ''}`}
|
||||
onClick={() => update('scrollToTop', settings.scrollToTop === false)}
|
||||
>
|
||||
<div className="toggle-card-info">
|
||||
<span className="toggle-card-title">
|
||||
<Icons.IconArrowUp size={16} /> Scroll-to-Top Button
|
||||
</span>
|
||||
<span className="toggle-card-desc">
|
||||
Show a floating arrow that returns visitors to the top of long pages
|
||||
</span>
|
||||
</div>
|
||||
<div className={`switch-toggle ${settings.scrollToTop !== false ? 'on' : ''}`}>
|
||||
<span className="switch-slider" />
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</FeatureGroup>
|
||||
|
||||
<FeatureGroup
|
||||
icon={<Icons.IconShieldCheck size={13} />}
|
||||
title="What each photo reveals"
|
||||
description="Published to anyone who opens a photo."
|
||||
chip="visible to visitors"
|
||||
>
|
||||
<div className={`metadata-card ${anyMetadata ? 'active' : ''}`}>
|
||||
<button
|
||||
type="button"
|
||||
className="metadata-card-main"
|
||||
onClick={toggleMetadata}
|
||||
aria-pressed={anyMetadata}
|
||||
>
|
||||
<span className="toggle-card-info">
|
||||
<span className="toggle-card-title">
|
||||
<Icons.IconCamera size={16} /> Photo metadata
|
||||
</span>
|
||||
<span className="toggle-card-desc">{metadataSummary}</span>
|
||||
</span>
|
||||
<span className="toggle-card-desc">
|
||||
Show a portrait, bio, and gear section on your portfolio
|
||||
<span className={`switch-toggle ${anyMetadata ? 'on' : ''}`}>
|
||||
<span className="switch-slider" />
|
||||
</span>
|
||||
</div>
|
||||
<div className={`switch-toggle ${settings.about?.enabled !== false ? 'on' : ''}`}>
|
||||
<span className="switch-slider" />
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
className="metadata-details-btn"
|
||||
onClick={() => setMetadataOpen((open) => !open)}
|
||||
aria-expanded={metadataOpen}
|
||||
>
|
||||
Details
|
||||
<Icons.IconChevronDown
|
||||
size={13}
|
||||
className={`metadata-details-chevron${metadataOpen ? ' open' : ''}`}
|
||||
/>
|
||||
</button>
|
||||
|
||||
{metadataOpen && (
|
||||
<div className="metadata-details">
|
||||
<SettingRow
|
||||
title="Camera & Lens"
|
||||
description="Body, lens and focal length"
|
||||
checked={exif.camera}
|
||||
onToggle={() => update('exif.camera', !exif.camera)}
|
||||
/>
|
||||
<SettingRow
|
||||
title="Exposure Settings"
|
||||
description="Aperture, shutter speed and ISO"
|
||||
checked={exif.settings}
|
||||
onToggle={() => update('exif.settings', !exif.settings)}
|
||||
/>
|
||||
<SettingRow
|
||||
title="Location"
|
||||
description="City and country from the photo's GPS data"
|
||||
checked={exif.location}
|
||||
onToggle={() => update('exif.location', !exif.location)}
|
||||
/>
|
||||
<SettingRow
|
||||
title="Photo Description"
|
||||
description="The description written in Immich"
|
||||
checked={exif.caption}
|
||||
onToggle={() => update('exif.caption', !exif.caption)}
|
||||
/>
|
||||
|
||||
{/* Dependent, not a peer: this only controls the grid overlay, and
|
||||
the overlay carries camera and lens. With those off it has
|
||||
nothing to show, which used to leave it switched on and
|
||||
silently doing nothing (#510). */}
|
||||
<SettingRow
|
||||
indented
|
||||
title="Summary on grid hover"
|
||||
description="Camera and lens over the photo, not only in the lightbox"
|
||||
checked={settings.exifOnHover !== false}
|
||||
disabled={!exif.camera}
|
||||
hint={exif.camera ? undefined : 'Needs Camera & Lens'}
|
||||
onToggle={() => update('exifOnHover', settings.exifOnHover === false)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</FeatureGroup>
|
||||
|
||||
<FeatureGroup
|
||||
icon={<Icons.IconHeart size={13} />}
|
||||
title="What visitors can do"
|
||||
description="Optional pages and interactions."
|
||||
>
|
||||
<div className="admin-toggle-cards-grid">
|
||||
<button
|
||||
type="button"
|
||||
className={`admin-toggle-card ${settings.map === true ? 'active' : ''}`}
|
||||
onClick={() => update('map', !settings.map)}
|
||||
>
|
||||
<div className="toggle-card-info">
|
||||
<span className="toggle-card-title">
|
||||
<Icons.IconMap size={16} /> Interactive GPS Map
|
||||
</span>
|
||||
<span className="toggle-card-desc">
|
||||
Enable /map view showing photo locations on a world map
|
||||
</span>
|
||||
</div>
|
||||
<div className={`switch-toggle ${settings.map === true ? 'on' : ''}`}>
|
||||
<span className="switch-slider" />
|
||||
</div>
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
className={`admin-toggle-card ${settings.proofing?.enabled !== false ? 'active' : ''}`}
|
||||
onClick={() => update('proofing.enabled', settings.proofing?.enabled === false)}
|
||||
>
|
||||
<div className="toggle-card-info">
|
||||
<span className="toggle-card-title">
|
||||
<Icons.IconHeart size={16} /> Client Proofing & Favorites
|
||||
</span>
|
||||
<span className="toggle-card-desc">
|
||||
Allow visitors & clients to heart, filter, and export favorite photo
|
||||
selections
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
className={`switch-toggle ${settings.proofing?.enabled !== false ? 'on' : ''}`}
|
||||
>
|
||||
<span className="switch-slider" />
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</FeatureGroup>
|
||||
|
||||
<FeatureGroup
|
||||
icon={<Icons.IconBarChart size={13} />}
|
||||
title="Measurement"
|
||||
description="Cookieless and self-hosted — nothing leaves your server."
|
||||
>
|
||||
<div className="admin-toggle-cards-grid">
|
||||
<button
|
||||
type="button"
|
||||
className={`admin-toggle-card ${settings.analytics !== false ? 'active' : ''}`}
|
||||
onClick={() => update('analytics', settings.analytics === false)}
|
||||
>
|
||||
<div className="toggle-card-info">
|
||||
<span className="toggle-card-title">
|
||||
<Icons.IconBarChart size={16} /> Analytics Tracking
|
||||
</span>
|
||||
<span className="toggle-card-desc">
|
||||
Collect anonymous privacy-friendly visit statistics
|
||||
</span>
|
||||
</div>
|
||||
<div className={`switch-toggle ${settings.analytics !== false ? 'on' : ''}`}>
|
||||
<span className="switch-slider" />
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</FeatureGroup>
|
||||
|
||||
<div className="admin-field favicon-field">
|
||||
<label>Favicon</label>
|
||||
@@ -758,6 +996,23 @@ export default function SettingsEditor({ section }: SettingsEditorProps) {
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="admin-field">
|
||||
<label>Visitor Default Mode</label>
|
||||
<select
|
||||
value={settings.mode || 'dark'}
|
||||
onChange={(e) => update('mode', e.target.value)}
|
||||
>
|
||||
<option value="dark">Dark</option>
|
||||
<option value="light">Light</option>
|
||||
<option value="auto">Follow the visitor's system</option>
|
||||
</select>
|
||||
<p className="admin-field-hint">
|
||||
What a first-time visitor sees. The light/dark toggle in the site header still
|
||||
lets them choose, and their choice is remembered on their device. The preview
|
||||
below is your own, and does not change this.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="admin-field">
|
||||
<label>Preset</label>
|
||||
<div className="preset-card-grid">
|
||||
@@ -1256,7 +1511,11 @@ export default function SettingsEditor({ section }: SettingsEditorProps) {
|
||||
<h3>
|
||||
<Icons.IconColumns size={18} /> Spacing & Columns
|
||||
</h3>
|
||||
<p className="settings-section-sub">Adjust column counts and grid gap spacing.</p>
|
||||
<p className="settings-section-sub">
|
||||
Adjust column counts and grid gap spacing. The column count also tiles the album
|
||||
covers on a subpage; their spacing stays with the theme unless a page overrides
|
||||
it.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="admin-field-row">
|
||||
@@ -1618,6 +1877,35 @@ export default function SettingsEditor({ section }: SettingsEditorProps) {
|
||||
|
||||
{activeSection === 'security' && (
|
||||
<div className="settings-panel">
|
||||
<div className="settings-section-header">
|
||||
<h3>
|
||||
<Icons.IconLock size={18} /> Site Password
|
||||
</h3>
|
||||
<p className="settings-section-sub">
|
||||
Puts the whole public site behind one password — useful while a portfolio is still
|
||||
being built, or for a folio only ever shown to clients.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="admin-field">
|
||||
<label>Site Password</label>
|
||||
<input
|
||||
type="text"
|
||||
value={settings.sitePassword || ''}
|
||||
onChange={(e) => update('sitePassword', e.target.value)}
|
||||
placeholder="Leave empty for a public site"
|
||||
autoComplete="off"
|
||||
/>
|
||||
<p className="admin-field-hint">
|
||||
Stored in <code>settings.yaml</code>. Log in once and the server log prints a{' '}
|
||||
<code>scrypt:…</code> hash to paste back here instead of the plaintext. The{' '}
|
||||
<code>SITE_PASSWORD</code> environment variable overrides this field. The admin
|
||||
panel keeps its own password and is never behind this gate.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="settings-section-divider" />
|
||||
|
||||
<div className="settings-section-header">
|
||||
<h3>
|
||||
<Icons.IconShieldCheck size={18} /> Asset Protection & Watermark
|
||||
@@ -1730,15 +2018,20 @@ export default function SettingsEditor({ section }: SettingsEditorProps) {
|
||||
</select>
|
||||
</div>
|
||||
<div className="admin-field">
|
||||
{/* Read through the same normaliser the lightbox uses, so a
|
||||
config written as a percentage shows as "90%" here rather
|
||||
than "9000%" — and is written back as a fraction on save. */}
|
||||
<label>
|
||||
Opacity ({Math.round((settings.watermark?.opacity ?? 0.3) * 100)}%)
|
||||
Opacity (
|
||||
{Math.round(resolveWatermarkOpacity(settings.watermark?.opacity) * 100)}
|
||||
%)
|
||||
</label>
|
||||
<input
|
||||
type="range"
|
||||
min="0.1"
|
||||
max="0.8"
|
||||
max="1"
|
||||
step="0.05"
|
||||
value={settings.watermark?.opacity ?? 0.3}
|
||||
value={resolveWatermarkOpacity(settings.watermark?.opacity)}
|
||||
onChange={(e) => update('watermark.opacity', parseFloat(e.target.value))}
|
||||
/>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user