src/settings/ThemeEditor.tsx

import { useEffect, useId, useLayoutEffect, useRef, useState } from "react";
import { Dialog } from "../ui/Dialog";
import { Segmented } from "../ui/Segmented";
import {
  generatePalette, maxThemeFileBytes, paletteFields, parseThemeFile, themeColorSchema,
  themeNameSchema, type CustomTheme, type Palette, type ThemePreview,
} from "../themes";

function ColorField({ field, value, onChange }: {
  field: (typeof paletteFields)[number];
  value: string;
  onChange: (value: string) => void;
}) {
  const id = useId();
  const [text, setText] = useState(value);
  useEffect(() => setText((current) => {
    const color = themeColorSchema.safeParse(current);
    return color.success && color.data === value ? current : value;
  }), [value]);
  const valid = themeColorSchema.safeParse(text).success;
  return (
    <div className="settings-row theme-color">
      <label htmlFor={id}>{field.label}</label>
      <div>
        <input type="color" aria-label={`${field.label} color`} value={value}
          onChange={(event) => { setText(event.target.value); onChange(event.target.value); }} />
        <input id={id} aria-label={`${field.label} hex`} value={text} required
          pattern="#[a-fA-F0-9]{3}([a-fA-F0-9]{3})?" maxLength={7}
          aria-invalid={!valid} spellCheck={false}
          onChange={(event) => {
            const next = event.target.value;
            setText(next);
            const color = themeColorSchema.safeParse(next);
            if (color.success) onChange(color.data);
          }} />
      </div>
    </div>
  );
}

export function ThemeEditor({ initial, scheme: initialScheme, editing, onSave, onPreview, onClose }: {
  initial: CustomTheme;
  scheme: ThemePreview["scheme"];
  editing: boolean;
  onSave: (theme: CustomTheme) => string | null;
  onPreview: (preview: ThemePreview | null) => void;
  onClose: () => void;
}) {
  const [theme, setTheme] = useState(initial);
  const [scheme, setScheme] = useState(initialScheme);
  const [advanced, setAdvanced] = useState(editing);
  const [error, setError] = useState("");
  const palette = theme.palettes[scheme];
  useLayoutEffect(() => { onPreview({ scheme, palette }); }, [scheme, palette, onPreview]);
  useLayoutEffect(() => () => onPreview(null), [onPreview]);
  const updateColor = (field: keyof Palette, color: string) => {
    const next = { ...palette, [field]: color };
    setTheme({ ...theme, palettes: { ...theme.palettes,
      [scheme]: !advanced && (field === "canvas" || field === "accent")
        ? generatePalette(next.canvas, next.accent, scheme) : next,
    } });
  };
  return (
    <Dialog title={editing ? "Edit theme" : "Create theme"} onClose={onClose}>
      <form className="theme-editor" onSubmit={(event) => {
        event.preventDefault();
        if (!event.currentTarget.checkValidity()) return;
        const name = themeNameSchema.safeParse(theme.name);
        if (!name.success) { setError("Enter a name of 1 to 48 characters."); return; }
        const failure = onSave({ ...theme, name: name.data });
        if (failure) setError(failure);
      }}>
        <label className="theme-name">Name
          <input value={theme.name} required maxLength={48}
            onChange={(event) => { setTheme({ ...theme, name: event.target.value }); setError(""); }} />
        </label>
        <div className="settings-row">
          <span id="palette-label">Edit palette</span>
          <Segmented labelledBy="palette-label" value={scheme}
            choices={[{ id: "dark", label: "Dark" }, { id: "light", label: "Light" }]}
            onChange={setScheme} />
        </div>
        <p className="note">Previewing across the app. Cancel restores your current theme.</p>
        <div className="settings-group theme-colors" key={scheme}>
          {paletteFields.filter((field) => advanced || field.id === "canvas" || field.id === "accent").map((field) => (
            <ColorField key={field.id} field={field} value={palette[field.id]}
              onChange={(color) => updateColor(field.id, color)} />
          ))}
        </div>
        <label className="settings-row">
          <span>Advanced colors</span>
          <input type="checkbox" role="switch" checked={advanced}
            onChange={(event) => setAdvanced(event.target.checked)} />
        </label>
        <p className="note">{advanced
          ? "Adjust each color independently. Both palettes are saved together."
          : "Background and accent generate matching colors for this palette. Use Advanced to fine-tune them."}</p>
        {error && <p className="theme-error" role="alert">{error}</p>}
        <div className="theme-actions">
          <button type="button" className="btn" onClick={onClose}>Cancel</button>
          <button type="submit" className="btn primary">Save theme</button>
        </div>
      </form>
    </Dialog>
  );
}

export function ImportTheme({ onImport, onClose }: {
  onImport: (theme: Omit<CustomTheme, "id">) => string | null;
  onClose: () => void;
}) {
  const [text, setText] = useState("");
  const [error, setError] = useState("");
  const [reading, setReading] = useState(false);
  const active = useRef(true);
  useEffect(() => { active.current = true; return () => { active.current = false; }; }, []);
  return (
    <Dialog title="Import theme" onClose={onClose}>
      <form className="theme-editor" onSubmit={(event) => {
        event.preventDefault();
        if (reading) return;
        try {
          const failure = onImport(parseThemeFile(text));
          if (failure) setError(failure);
        } catch (failure) {
          setError(failure instanceof Error ? failure.message : "This theme could not be imported.");
        }
      }}>
        <p className="dialog-copy">Choose an Emc² theme JSON file or paste its contents. Import creates a new theme.</p>
        <label className="theme-name">Theme file
          <input type="file" accept=".json,application/json" disabled={reading}
            onChange={async (event) => {
              const file = event.target.files?.[0];
              event.target.value = "";
              if (!file) return;
              setError("");
              if (file.size > maxThemeFileBytes) { setError("Theme files must be 64 KB or smaller."); return; }
              setReading(true);
              try {
                const contents = await file.text();
                if (active.current) setText(contents);
              } catch { if (active.current) setError("This file could not be read. Try pasting its JSON."); }
              finally { if (active.current) setReading(false); }
            }} />
        </label>
        <label className="theme-name">Theme JSON
          <textarea value={text} required maxLength={maxThemeFileBytes} rows={8} spellCheck={false} disabled={reading}
            onChange={(event) => { setText(event.target.value); setError(""); }} />
        </label>
        {error && <p className="theme-error" role="alert">{error}</p>}
        <div className="theme-actions">
          <button type="button" className="btn" onClick={onClose}>Cancel</button>
          <button type="submit" className="btn primary" disabled={reading || !text.trim()}>{reading ? "Reading file…" : "Import theme"}</button>
        </div>
      </form>
    </Dialog>
  );
}