ai generated solutions to our ai generated problems

This commit is contained in:
2026-06-22 22:25:34 +01:00
parent dd259081c1
commit 5288f8293e
+171 -210
View File
@@ -3271,25 +3271,79 @@ function SettingsPage({ theme, onThemeChange, customColor, onCustomColorChange,
setDraftColor('#e82517') setDraftColor('#e82517')
} }
const hasAnyCustomColor = customColor || Object.keys(customColors).length > 0
function resetAllColors() {
onCustomColorChange('')
setDraftColor('#e82517')
onCustomColorsChange({})
}
const allColorDefs = [
{
id: 'accent',
label: 'Accent',
desc: 'Links, highlights, interactive elements',
getValue: () => draftColor,
getDisplayVal: () => draftColor,
isCustom: () => !!customColor,
onChange: handleColorPickerChange,
onReset: resetToDefault,
},
...advancedColorDefs.map(({ field, label, desc, defaults }) => ({
id: field,
label,
desc,
getValue: () => customColors[field] || '',
getDisplayVal: () => customColors[field] || defaults[theme] || defaults.dark,
isCustom: () => !!customColors[field],
onChange: (val) => onCustomColorsChange({ ...customColors, [field]: val }),
onReset: () => {
const next = { ...customColors }
delete next[field]
onCustomColorsChange(next)
},
})),
]
return ( return (
<section className="mx-auto max-w-3xl pb-16 pt-24 sm:pt-28"> <section className="mx-auto max-w-2xl pb-16 pt-24 sm:pt-28">
<div className="border-b border-border pb-6"> <div className="border-b border-border pb-6">
<p className="text-sm font-semibold uppercase tracking-wide text-fury-cyan">Preferences</p> <h1 className="text-3xl font-bold">Settings</h1>
<h1 className="mt-2 text-4xl font-bold">Settings</h1> <p className="mt-2 text-sm text-text-soft">
<p className="mt-3 max-w-xl text-text-soft"> Your choices are saved locally and persist across visits.
Customise the look and feel of the site. Your choices are saved in cookies and local storage.
</p> </p>
</div> </div>
{/* Appearance */}
<div className="mt-8 space-y-6"> <div className="mt-8 space-y-6">
{/* Appearance */}
<div className="rounded-xl border border-border bg-fury-white p-6 shadow-sm"> <div className="rounded-xl border border-border bg-fury-white p-6 shadow-sm">
<h2 className="text-lg font-semibold">Appearance</h2> <h2 className="text-base font-semibold">Appearance</h2>
<p className="mt-1 text-sm text-text-soft">Choose a base theme and accent colour.</p>
{/* Theme presets */} {/* Light / Dark toggle */}
<div className="mt-4 flex items-center justify-between">
<span className="text-sm text-text-soft">Base mode</span>
<div className="flex rounded-full border border-border bg-surface p-0.5">
<button
type="button"
onClick={() => onThemeChange('light')}
className={`rounded-full px-4 py-1 text-sm font-semibold transition ${theme === 'light' ? 'bg-text text-bg' : 'text-text-soft hover:text-text'}`}
>
Light
</button>
<button
type="button"
onClick={() => onThemeChange('dark')}
className={`rounded-full px-4 py-1 text-sm font-semibold transition ${theme === 'dark' ? 'bg-text text-bg' : 'text-text-soft hover:text-text'}`}
>
Dark
</button>
</div>
</div>
{/* Presets */}
<div className="mt-5"> <div className="mt-5">
<p className="mb-3 text-xs font-semibold uppercase tracking-wide text-text-muted">Theme presets</p> <p className="mb-2.5 text-xs font-semibold uppercase tracking-wide text-text-muted">Presets</p>
<div className="flex flex-wrap gap-2"> <div className="flex flex-wrap gap-2">
{accentPresets.map((preset) => { {accentPresets.map((preset) => {
const isActive = activePresetId === preset.id const isActive = activePresetId === preset.id
@@ -3298,27 +3352,22 @@ function SettingsPage({ theme, onThemeChange, customColor, onCustomColorChange,
key={preset.id} key={preset.id}
type="button" type="button"
onClick={() => handlePreset(preset)} onClick={() => handlePreset(preset)}
className={`flex items-center gap-2 rounded-full border px-3 py-1.5 text-sm font-semibold transition ${isActive className={`flex items-center gap-1.5 rounded-full border px-3 py-1 text-xs font-semibold transition ${isActive
? 'border-fury-cyan bg-fury-cyan text-bg' ? 'border-fury-cyan bg-fury-cyan text-bg'
: 'border-border bg-surface text-text hover:border-ring hover:bg-surface-alt' : 'border-border bg-surface text-text hover:border-ring hover:bg-surface-alt'
}`} }`}
> >
{preset.color ? ( <span
<span className="h-2.5 w-2.5 rounded-full shrink-0"
className="h-3 w-3 rounded-full border border-white/20 shrink-0" style={{
style={{ background: preset.color }} background: preset.color
/> ? preset.color
) : ( : preset.id === 'default-light'
<span ? 'linear-gradient(135deg, #fefde7 50%, #e82517 50%)'
className="h-3 w-3 rounded-full shrink-0" : 'linear-gradient(135deg, #130d08 50%, #ff6a5f 50%)',
style={{ border: '1px solid rgba(128,128,128,0.3)',
background: preset.id === 'default-light' }}
? 'linear-gradient(135deg, #fefde7 50%, #e82517 50%)' />
: 'linear-gradient(135deg, #130d08 50%, #ff6a5f 50%)',
border: '1px solid rgba(255,255,255,0.2)',
}}
/>
)}
{preset.label} {preset.label}
</button> </button>
) )
@@ -3326,176 +3375,89 @@ function SettingsPage({ theme, onThemeChange, customColor, onCustomColorChange,
</div> </div>
</div> </div>
{/* Light / Dark toggle */} {/* Colour grid */}
<div className="mt-5 flex items-center gap-3"> <div className="mt-5">
<p className="text-sm font-semibold text-text-soft">Base mode:</p> <div className="mb-2.5 flex items-center justify-between">
<div className="flex rounded-full border border-border bg-surface p-0.5"> <p className="text-xs font-semibold uppercase tracking-wide text-text-muted">Colours</p>
<button {hasAnyCustomColor && (
type="button"
onClick={() => onThemeChange('light')}
className={`rounded-full px-4 py-1.5 text-sm font-semibold transition ${theme === 'light' ? 'bg-text text-bg' : 'text-text-soft hover:text-text'
}`}
>
Light
</button>
<button
type="button"
onClick={() => onThemeChange('dark')}
className={`rounded-full px-4 py-1.5 text-sm font-semibold transition ${theme === 'dark' ? 'bg-text text-bg' : 'text-text-soft hover:text-text'
}`}
>
Dark
</button>
</div>
</div>
{/* Custom colour picker */}
<div className="mt-6">
<p className="mb-3 text-xs font-semibold uppercase tracking-wide text-text-muted">Custom accent colour</p>
<div className="flex flex-wrap items-center gap-3">
{/* Colour wheel trigger */}
<label
className="relative h-11 w-11 cursor-pointer overflow-hidden rounded-full border-2 border-border shadow-md transition hover:border-ring hover:scale-105 active:scale-95"
style={{ background: draftColor }}
title="Pick a custom colour"
>
<input
type="color"
value={draftColor}
onChange={(e) => handleColorPickerChange(e.target.value)}
className="absolute inset-0 h-full w-full cursor-pointer opacity-0"
/>
</label>
{/* Hex input */}
<div className="flex items-center gap-1.5 rounded-md border border-border bg-surface px-3 py-2">
<span className="text-xs font-semibold text-text-muted">#</span>
<input
type="text"
value={draftColor.replace('#', '')}
maxLength={6}
onChange={(e) => handleColorInput('#' + e.target.value.replace(/[^0-9a-fA-F]/g, ''))}
className="w-20 bg-transparent text-sm font-mono font-semibold text-text outline-none"
placeholder="e82517"
spellCheck={false}
/>
</div>
{/* Live preview swatch */}
<div className="flex items-center gap-2">
<span
className="h-8 rounded-md px-3 py-1.5 text-xs font-semibold text-white shadow"
style={{ background: draftColor }}
>
Preview
</span>
</div>
{/* Reset button */}
{customColor ? (
<button <button
type="button" type="button"
onClick={resetToDefault} onClick={resetAllColors}
className="rounded-md border border-border px-3 py-1.5 text-xs font-semibold text-text-soft transition hover:bg-surface hover:text-text" className="text-xs text-text-muted transition hover:text-text"
> >
Reset to default Reset all
</button> </button>
) : null} )}
</div> </div>
<p className="mt-2 text-xs text-text-muted"> <div className="grid grid-cols-2 gap-2 sm:grid-cols-3 lg:grid-cols-4">
Changes all accent highlights, links, and interactive elements across the site. {allColorDefs.map(({ id, label, desc, getDisplayVal, isCustom, onChange, onReset }) => {
</p> const displayVal = getDisplayVal()
</div> const customised = isCustom()
</div> return (
<div
{/* Advanced colours */} key={id}
<div className="rounded-xl border border-border bg-fury-white p-6 shadow-sm"> className="group relative flex flex-col gap-2 rounded-lg border border-border bg-surface p-3"
<h2 className="text-lg font-semibold">Advanced colours</h2>
<p className="mt-1 text-sm text-text-soft">Override individual colours for the current theme. Each resets independently.</p>
<div className="mt-5 space-y-4">
{advancedColorDefs.map(({ field, label, desc, defaults }) => {
const defaultVal = defaults[theme] ?? defaults.dark
const currentVal = customColors[field] || ''
const displayVal = currentVal || defaultVal
return (
<div key={field} className="flex flex-wrap items-center gap-3">
<label
className="relative h-9 w-9 shrink-0 cursor-pointer overflow-hidden rounded-full border-2 border-border shadow-sm transition hover:border-ring hover:scale-105 active:scale-95"
style={{ background: displayVal }}
title={`Pick ${label}`}
> >
<input <div className="flex items-center justify-between">
type="color" <label
value={displayVal} className="relative h-7 w-7 cursor-pointer overflow-hidden rounded-full border-2 border-border shadow-sm transition hover:scale-105 active:scale-95"
onChange={(e) => onCustomColorsChange({ ...customColors, [field]: e.target.value })} style={{ background: displayVal }}
className="absolute inset-0 h-full w-full cursor-pointer opacity-0" title={`Pick ${label}`}
/> >
</label> <input
type="color"
<div className="flex min-w-0 flex-col"> value={displayVal}
<span className="text-sm font-semibold text-text leading-none">{label}</span> onChange={(e) => onChange(e.target.value)}
<span className="text-xs text-text-muted mt-0.5">{desc}</span> className="absolute inset-0 h-full w-full cursor-pointer opacity-0"
</div> />
</label>
<div className="ml-auto flex items-center gap-2"> {customised && (
<div className="flex items-center gap-1 rounded-md border border-border bg-surface px-2 py-1.5"> <button
<span className="text-xs font-semibold text-text-muted">#</span> type="button"
onClick={onReset}
className="text-xs text-text-muted opacity-0 transition group-hover:opacity-100 hover:text-text"
title="Reset to default"
>
</button>
)}
</div>
<div>
<p className="text-xs font-semibold text-text leading-none">
{label}
{customised && <span className="ml-1 text-fury-cyan"></span>}
</p>
<p className="mt-0.5 text-xs text-text-muted leading-tight">{desc}</p>
</div>
<div className="flex items-center gap-0.5 rounded border border-border bg-bg px-1.5 py-1">
<span className="text-xs text-text-muted">#</span>
<input <input
type="text" type="text"
value={displayVal.replace('#', '')} value={displayVal.replace('#', '')}
maxLength={6} maxLength={6}
onChange={(e) => { onChange={(e) => {
const v = '#' + e.target.value.replace(/[^0-9a-fA-F]/g, '') const v = '#' + e.target.value.replace(/[^0-9a-fA-F]/g, '')
if (/^#[0-9a-fA-F]{6}$/.test(v)) onCustomColorsChange({ ...customColors, [field]: v }) if (/^#[0-9a-fA-F]{6}$/.test(v)) onChange(v)
}} }}
className="w-16 bg-transparent text-xs font-mono font-semibold text-text outline-none" className="w-full bg-transparent text-xs font-mono text-text outline-none"
spellCheck={false} spellCheck={false}
/> />
</div> </div>
{currentVal ? (
<button
type="button"
onClick={() => {
const next = { ...customColors }
delete next[field]
onCustomColorsChange(next)
}}
className="rounded-md border border-border px-2 py-1.5 text-xs font-semibold text-text-muted transition hover:bg-surface hover:text-text"
title="Reset to theme default"
>
Reset
</button>
) : (
<span className="px-2 py-1.5 text-xs text-text-muted">Theme default</span>
)}
</div> </div>
</div> )
) })}
})}
</div>
{Object.keys(customColors).length > 0 ? (
<div className="mt-5 flex justify-end">
<button
type="button"
onClick={() => onCustomColorsChange({})}
className="rounded-md border border-border px-3 py-1.5 text-xs font-semibold text-text-soft transition hover:bg-surface hover:text-text"
>
Reset all to theme defaults
</button>
</div> </div>
) : null} </div>
</div> </div>
{/* Cookie & Analytics */} {/* Analytics */}
<div className="rounded-xl border border-border bg-fury-white p-6 shadow-sm"> <div className="rounded-xl border border-border bg-fury-white p-6 shadow-sm">
<h2 className="text-lg font-semibold">Cookie &amp; analytics settings</h2> <h2 className="text-base font-semibold">Analytics &amp; cookies</h2>
<p className="mt-1 text-sm leading-6 text-text-soft"> <p className="mt-1 text-sm text-text-soft">
We use a necessary cookie to remember these choices. You can also allow analytics for the public viewers page and choose which details are included. A necessary cookie stores these preferences. You can allow analytics for the viewers page and control what's included.
</p> </p>
<div className="mt-4 space-y-3"> <div className="mt-4 space-y-2">
<PreferenceToggle <PreferenceToggle
checked checked
description="Stores your theme and consent choice so preferences persist across visits." description="Stores your theme and consent choice so preferences persist across visits."
@@ -3508,41 +3470,40 @@ function SettingsPage({ theme, onThemeChange, customColor, onCustomColorChange,
label="Viewer analytics" label="Viewer analytics"
onChange={(value) => updateDraft('analytics', value)} onChange={(value) => updateDraft('analytics', value)}
/> />
<PreferenceToggle {draft.analytics && (
checked={draft.device} <div className="ml-7 space-y-2">
description="Includes browser, operating system, and broad device type." <PreferenceToggle
disabled={!draft.analytics} checked={draft.device}
label="Browser and device details" description="Browser, OS, and broad device type."
onChange={(value) => updateDraft('device', value)} label="Browser and device"
/> onChange={(value) => updateDraft('device', value)}
<PreferenceToggle />
checked={draft.display} <PreferenceToggle
description="Includes screen size, viewport size, and colour depth." checked={draft.display}
disabled={!draft.analytics} description="Screen size, viewport size, and colour depth."
label="Screen details" label="Screen details"
onChange={(value) => updateDraft('display', value)} onChange={(value) => updateDraft('display', value)}
/> />
<PreferenceToggle <PreferenceToggle
checked={draft.locale} checked={draft.locale}
description="Includes browser language and timezone." description="Browser language and timezone."
disabled={!draft.analytics} label="Language and timezone"
label="Language and timezone" onChange={(value) => updateDraft('locale', value)}
onChange={(value) => updateDraft('locale', value)} />
/> <PreferenceToggle
<PreferenceToggle checked={draft.referrer}
checked={draft.referrer} description="The page that linked you here, when the browser provides it."
description="Includes the page that linked you here when the browser provides it." label="Referrer"
disabled={!draft.analytics} onChange={(value) => updateDraft('referrer', value)}
label="Referrer" />
onChange={(value) => updateDraft('referrer', value)} <PreferenceToggle
/> checked={draft.diagnostics}
<PreferenceToggle description="Network quality, privacy signals, touch support, CPU/memory hints."
checked={draft.diagnostics} label="Technical diagnostics"
description="Includes network quality, privacy signals, touch support, CPU/memory hints, and other browser diagnostics." onChange={(value) => updateDraft('diagnostics', value)}
disabled={!draft.analytics} />
label="Technical diagnostics" </div>
onChange={(value) => updateDraft('diagnostics', value)} )}
/>
</div> </div>
<div className="mt-5 flex flex-col-reverse gap-2 sm:flex-row sm:justify-end"> <div className="mt-5 flex flex-col-reverse gap-2 sm:flex-row sm:justify-end">
@@ -3551,7 +3512,7 @@ function SettingsPage({ theme, onThemeChange, customColor, onCustomColorChange,
onClick={() => onAnalyticsChoose({ ...defaultAnalyticsPreferences, chosen: true })} onClick={() => onAnalyticsChoose({ ...defaultAnalyticsPreferences, chosen: true })}
className="rounded-md border border-border px-4 py-2 text-sm font-semibold text-text-soft transition hover:bg-surface hover:text-text" className="rounded-md border border-border px-4 py-2 text-sm font-semibold text-text-soft transition hover:bg-surface hover:text-text"
> >
Decline all analytics Decline all
</button> </button>
<button <button
type="button" type="button"