feat: custom accent color picker in theme preferences

- Added 'custom' accent option with color picker + hex input
- Pipette icon on the custom swatch (switches to solid when active)
- Color picker appears inline when custom is selected
- Generates hover/light variants automatically from hex (darken/lighten)
- Dark mode accent-light uses rgba for translucency
- Persisted in localStorage, applied before React hydration (no flash)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-15 01:42:38 +01:00
parent 79651bc41d
commit 2eab705a8a
3 changed files with 132 additions and 15 deletions
@@ -1,4 +1,4 @@
import { Sun, Monitor, Moon } from 'lucide-react'
import { Sun, Monitor, Moon, Pipette } from 'lucide-react'
import { clsx } from 'clsx'
import { useThemeStore, ACCENT_PRESETS, type ThemeMode } from '../../store/theme'
@@ -9,7 +9,7 @@ const MODES: { key: ThemeMode; icon: typeof Sun; label: string }[] = [
]
export default function ThemePreferences() {
const { mode, accent, setMode, setAccent } = useThemeStore()
const { mode, accent, customHex, setMode, setAccent, setCustomHex } = useThemeStore()
return (
<div className="px-3 py-2 space-y-2">
@@ -39,7 +39,8 @@ export default function ThemePreferences() {
{/* Accent row */}
<div className="flex items-center gap-2">
<span className="text-xs text-content-muted w-12 shrink-0">Accent</span>
<div className="flex gap-2">
<div className="flex items-center gap-2">
{/* Preset colors */}
{ACCENT_PRESETS.map(({ key, label, hex }) => (
<button
key={key}
@@ -56,6 +57,59 @@ export default function ThemePreferences() {
}}
/>
))}
{/* Separator */}
<div className="w-px h-4 bg-border-default" />
{/* Custom color picker */}
<div className="relative">
<button
onClick={() => setAccent('custom')}
title="Custom color"
className={clsx(
'w-5 h-5 rounded-full transition-all flex items-center justify-center border',
accent === 'custom' ? 'scale-125' : 'hover:scale-110',
)}
style={{
backgroundColor: customHex,
borderColor: accent === 'custom' ? 'transparent' : 'var(--color-border)',
outline: accent === 'custom' ? `2px solid ${customHex}` : undefined,
outlineOffset: accent === 'custom' ? '2px' : undefined,
}}
>
{accent !== 'custom' && (
<Pipette size={10} className="text-white drop-shadow-sm" />
)}
</button>
</div>
{/* Color input (visible when custom is active) */}
{accent === 'custom' && (
<div className="flex items-center gap-1.5">
<input
type="color"
value={customHex}
onChange={(e) => setCustomHex(e.target.value)}
className="w-6 h-5 rounded border border-border-default cursor-pointer p-0"
title="Pick a custom accent color"
/>
<input
type="text"
value={customHex}
onChange={(e) => {
const v = e.target.value
if (/^#[0-9a-fA-F]{6}$/.test(v)) setCustomHex(v)
}}
onBlur={(e) => {
let v = e.target.value
if (!v.startsWith('#')) v = '#' + v
if (/^#[0-9a-fA-F]{6}$/.test(v)) setCustomHex(v)
}}
className="w-[4.5rem] px-1.5 py-0.5 text-[10px] font-mono border border-border-default rounded bg-surface text-content focus:outline-none focus:border-accent"
placeholder="#6366f1"
/>
</div>
)}
</div>
</div>
</div>