import { type ReactNode, useCallback, useEffect, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' import { Button } from '../../../shared/ui/Button' import { PRESETS } from '../presets.generated' import type { GradientSpec, ThemeColors, ThemeEntry } from '../types' import { EditorPreview } from './EditorPreview' import { ColorField } from './fields/ColorField' import { COLOR_SLOTS, GRADIENT_SLOTS, fromProfileJson, toProfileJson, toShareHash } from './model/codec' import { GradientField } from './fields/GradientField' import { useThemeHistory } from './model/useThemeHistory' /** Pack the draft name into a slug id, mirroring the codec's `slug`. */ const nameToId = (name: string) => name.trim().toLowerCase().replace(/[^a-z0-9]+/g, '_').replace(/^_|_$/g, '') || 'custom' /** `intro` (page title + lead) sits on top of the controls column so the whole editor fits one screen. */ export function ThemeEditor({ initial, intro }: { initial: ThemeEntry; intro?: ReactNode }) { const { t } = useTranslation('themes') const { draft, set, reset, undo, redo, canUndo, canRedo } = useThemeHistory(initial) const [notice, setNotice] = useState(null) const fileRef = useRef(null) const timer = useRef>(undefined) const flash = useCallback((text: string) => { setNotice(text) clearTimeout(timer.current) timer.current = setTimeout(() => setNotice(null), 2000) }, []) useEffect(() => () => clearTimeout(timer.current), []) useEffect(() => { const onKey = (e: KeyboardEvent) => { if (!(e.ctrlKey || e.metaKey) || e.key.toLowerCase() !== 'z') return e.preventDefault() if (e.shiftKey) redo() else undo() } window.addEventListener('keydown', onKey) return () => window.removeEventListener('keydown', onKey) }, [undo, redo]) const setColor = (slot: keyof ThemeColors, hex: string) => set({ ...draft, theme: { ...draft.theme, [slot]: hex } }) const setGradient = (key: keyof ThemeEntry, patch: Partial) => set({ ...draft, [key]: { ...(draft[key] as GradientSpec), ...patch } }) const copy = async () => { await navigator.clipboard?.writeText(toProfileJson(draft)) flash(t('editor.copied')) } const download = () => { const url = URL.createObjectURL(new Blob([toProfileJson(draft)], { type: 'application/json' })) const a = document.createElement('a') a.href = url a.download = `${draft.id}.json` a.click() URL.revokeObjectURL(url) } const share = async () => { await navigator.clipboard?.writeText(`${location.origin}${location.pathname}#t=${toShareHash(draft)}`) flash(t('editor.shared')) } const pickPreset = (id: string) => { const preset = PRESETS.find((p) => p.id === id) if (preset) reset(preset) } const onFile = (file?: File) => { if (!file) return file.text().then((text) => { const result = fromProfileJson(text) if ('error' in result) flash(t('editor.importError')) else reset(result) }) } return (
{intro &&
{intro}
}
{notice && {notice}}

{t('editor.presets')}

{PRESETS.map((p) => ( ))}

{t('editor.colors')}

{COLOR_SLOTS.map((slot) => ( setColor(slot, hex)} /> ))}

{t('editor.gradients')}

{GRADIENT_SLOTS.map((g) => ( setGradient(g.key, patch)} /> ))}
onFile(e.target.files?.[0])} />
) }