"use client" import { useState, useEffect, useRef, useCallback } from "react" import { Plus, X, ChevronDown, ChevronUp, GripVertical, Copy, FileInput, } from "lucide-react" import { motion, AnimatePresence, Reorder, useDragControls } from "motion/react" export interface SettingItem { title: string value: string | number | boolean } export interface SettingCategory { category: string settings: SettingItem[] } const DEFAULT_CATEGORIES: SettingCategory[] = [ { category: "Graphics", settings: [ { title: "Texture Quality", value: "High" }, { title: "Shadow Quality", value: "Medium" }, { title: "Anti-Aliasing", value: "TAA" }, { title: "Effects Quality", value: "High" }, ], }, { category: "Display", settings: [ { title: "Resolution", value: "1280x800" }, { title: "Refresh Rate", value: 60 }, { title: "VSync", value: false }, { title: "Fullscreen", value: true }, ], }, { category: "Audio", settings: [ { title: "Master Volume", value: 80 }, { title: "SFX Volume", value: 70 }, ], }, ] const typeOptions = [ { value: "text", label: "Abc" }, { value: "number", label: "123" }, { value: "boolean", label: "\u2713/\u2717" }, ] as const interface SettingsEditorProps { value: SettingCategory[] onChange: (categories: SettingCategory[]) => void className?: string } function getTypeMeta(value: string | number | boolean) { if (typeof value === "boolean") { return { label: "\u2713/\u2717", className: "bg-emerald-500/10 text-emerald-400 border-emerald-500/20", title: "Toggle", next: "text" as const, } } if (typeof value === "number") { return { label: "123", className: "bg-amber-500/10 text-amber-400 border-amber-500/20", title: "Number", next: "boolean" as const, } } return { label: "Abc", className: "bg-sky-500/10 text-sky-400 border-sky-500/20", title: "Text", next: "number" as const, } } function convertValue( current: string | number | boolean, targetType: "text" | "number" | "boolean", ): string | number | boolean { if (targetType === "boolean") { if (typeof current === "boolean") return current if (typeof current === "number") return current !== 0 return current.toLowerCase() === "true" } if (targetType === "number") { if (typeof current === "number") return current if (typeof current === "boolean") return current ? 1 : 0 const parsed = parseInt(current, 10) return isNaN(parsed) ? 0 : parsed } // text if (typeof current === "string") return current return String(current) } function SettingRow({ setting, category, onUpdate, onRemove, onDuplicate, onTypeSwitch, }: { setting: SettingItem category: string onUpdate: ( cat: string, title: string, val: string | number | boolean, ) => void onRemove: (cat: string, title: string) => void onDuplicate: (cat: string, title: string) => void onTypeSwitch: (cat: string, title: string) => void }) { const dragControls = useDragControls() const typeMeta = getTypeMeta(setting.value) return ( {/* Drag Handle */}
{ e.preventDefault(); dragControls.start(e) }} >
{/* Type Badge — clickable to cycle type */} {/* Label */} {setting.title} {/* Value Control */}
{typeof setting.value === "boolean" ? ( ) : typeof setting.value === "number" ? ( { const cleaned = e.target.value.replace( /[^0-9]/g, "", ) const updated = cleaned === "" ? 0 : Number(cleaned) onUpdate(category, setting.title, updated) }} className="w-full px-3 py-1.5 rounded-md border border-border bg-text/5 text-text text-sm placeholder:text-text/40 outline-none focus:border-primary focus:ring-2 focus:ring-primary/50 transition-colors text-right" /> ) : ( { onUpdate(category, setting.title, e.target.value) }} className="w-full px-3 py-1.5 rounded-md border border-border bg-text/5 text-text text-sm placeholder:text-text/40 outline-none focus:border-primary focus:ring-2 focus:ring-primary/50 transition-colors" /> )}
{/* Actions: Duplicate + Remove */}
) } export function SettingsEditor({ value, onChange, className = "", }: SettingsEditorProps) { const [collapsedCategories, setCollapsedCategories] = useState>( new Set(), ) const [newCategoryName, setNewCategoryName] = useState("") const [newSettingNames, setNewSettingNames] = useState< Record >({}) const [newSettingTypes, setNewSettingTypes] = useState< Record >({}) const addSettingInputRefs = useRef>( {}, ) const [pasteOpenFor, setPasteOpenFor] = useState(null) const [pasteText, setPasteText] = useState("") const setAddSettingInputRef = useCallback( (category: string) => (el: HTMLInputElement | null) => { addSettingInputRefs.current[category] = el }, [], ) const toggleCategory = (category: string) => { setCollapsedCategories((prev) => { const next = new Set(prev) if (next.has(category)) { next.delete(category) } else { next.add(category) } return next }) } const collapseAll = () => { setCollapsedCategories(new Set(value.map((c) => c.category))) } const expandAll = () => { setCollapsedCategories(new Set()) } const allCollapsed = value.length > 0 && collapsedCategories.size === value.length const allExpanded = collapsedCategories.size === 0 const addCategory = () => { const name = newCategoryName.trim() if (!name) return if (value.some((c) => c.category === name)) return onChange([...value, { category: name, settings: [] }]) setNewCategoryName("") } const removeCategory = (category: string) => { onChange(value.filter((c) => c.category !== category)) setCollapsedCategories((prev) => { const next = new Set(prev) next.delete(category) return next }) } const addSetting = (category: string) => { const name = (newSettingNames[category] || "").trim() if (!name) return const type = newSettingTypes[category] || "text" const defaultValue = type === "boolean" ? false : type === "number" ? 0 : "" onChange( value.map((c) => { if (c.category !== category) return c if (c.settings.some((s) => s.title === name)) return c return { ...c, settings: [ ...c.settings, { title: name, value: defaultValue }, ], } }), ) setNewSettingNames((prev) => ({ ...prev, [category]: "" })) // Refocus after React re-renders setTimeout(() => addSettingInputRefs.current[category]?.focus(), 0) } const removeSetting = (category: string, settingTitle: string) => { onChange( value.map((c) => { if (c.category !== category) return c return { ...c, settings: c.settings.filter( (s) => s.title !== settingTitle, ), } }), ) } const duplicateSetting = (category: string, settingTitle: string) => { onChange( value.map((c) => { if (c.category !== category) return c const idx = c.settings.findIndex((s) => s.title === settingTitle) if (idx === -1) return c const original = c.settings[idx] // Find a unique title let copyTitle = `${original.title} (copy)` let n = 2 while (c.settings.some((s) => s.title === copyTitle)) { copyTitle = `${original.title} (copy ${n})` n++ } const next = [...c.settings] next.splice(idx + 1, 0, { title: copyTitle, value: original.value, }) return { ...c, settings: next } }), ) } const cycleSettingType = (category: string, settingTitle: string) => { onChange( value.map((c) => { if (c.category !== category) return c return { ...c, settings: c.settings.map((s) => { if (s.title !== settingTitle) return s const meta = getTypeMeta(s.value) return { ...s, value: convertValue(s.value, meta.next) } }), } }), ) } const updateSetting = ( category: string, settingTitle: string, newValue: string | number | boolean, ) => { onChange( value.map((c) => { if (c.category !== category) return c return { ...c, settings: c.settings.map((s) => s.title === settingTitle ? { ...s, value: newValue } : s, ), } }), ) } const handleReorderSettings = ( category: string, newSettings: SettingItem[], ) => { onChange( value.map((c) => { if (c.category !== category) return c return { ...c, settings: newSettings } }), ) } const parsePaste = (category: string) => { const lines = pasteText.split("\n") const newSettings: SettingItem[] = [] for (const line of lines) { const trimmed = line.trim() if (!trimmed) continue // Match: Key = Value, Key: Value, Key - Value const match = trimmed.match(/^(.+?)\s*[:=\-]\s*(.+)$/) if (!match) continue const [, rawKey, rawVal] = match const key = rawKey.trim() const val = rawVal.trim() if (!key || !val) continue // Detect type const lower = val.toLowerCase() let parsedValue: string | number | boolean = val if ( lower === "true" || lower === "false" || lower === "on" || lower === "off" || lower === "yes" || lower === "no" ) { parsedValue = lower === "true" || lower === "on" || lower === "yes" } else if (/^-?\d+$/.test(val)) { parsedValue = Number(val) } newSettings.push({ title: key, value: parsedValue }) } if (newSettings.length === 0) return onChange( value.map((c) => { if (c.category !== category) return c const existingTitles = new Set(c.settings.map((s) => s.title)) const uniqueNew = newSettings.filter( (s) => !existingTitles.has(s.title), ) return { ...c, settings: [...c.settings, ...uniqueNew], } }), ) setPasteText("") setPasteOpenFor(null) } const loadDefaults = () => { onChange(DEFAULT_CATEGORIES) setCollapsedCategories(new Set()) } const isEmpty = value.length === 0 const [showCustomInput, setShowCustomInput] = useState(false) const customInputRef = useRef(null) useEffect(() => { if (showCustomInput && customInputRef.current) { customInputRef.current.focus() } }, [showCustomInput]) // Keyboard reorder: Alt + ArrowUp/ArrowDown const valueRef = useRef(value) const onChangeRef = useRef(onChange) useEffect(() => { valueRef.current = value onChangeRef.current = onChange }) useEffect(() => { const handler = (e: KeyboardEvent) => { if (!e.altKey) return if (e.key !== "ArrowUp" && e.key !== "ArrowDown") return const active = document.activeElement if (!active) return const row = active.closest("[data-setting-row]") if (!row) return const category = row.getAttribute("data-category") const title = row.getAttribute("data-title") if (!category || !title) return e.preventDefault() const currentValue = valueRef.current const direction = e.key === "ArrowUp" ? -1 : 1 const nextValue = currentValue.map((c) => { if (c.category !== category) return c const idx = c.settings.findIndex((s) => s.title === title) if (idx === -1) return c const newIdx = idx + direction if (newIdx < 0 || newIdx >= c.settings.length) return c const nextSettings = [...c.settings] ;[nextSettings[idx], nextSettings[newIdx]] = [ nextSettings[newIdx], nextSettings[idx], ] return { ...c, settings: nextSettings } }) onChangeRef.current(nextValue) // Restore focus after React re-renders requestAnimationFrame(() => { const newRow = document.querySelector( `[data-setting-row][data-category="${category}"][data-title="${title}"]`, ) const input = newRow?.querySelector( "input, button", ) as HTMLElement | null input?.focus() }) } window.addEventListener("keydown", handler) return () => window.removeEventListener("keydown", handler) }, []) return (
{isEmpty && !showCustomInput && (

No settings configured

)} {isEmpty && showCustomInput && (
setNewCategoryName(e.target.value)} onKeyDown={(e) => { if (e.key === "Enter") { e.preventDefault() if (newCategoryName.trim()) { addCategory() setShowCustomInput(false) } } }} placeholder="e.g. Graphics, Audio, Display..." className="flex-1 px-4 py-2.5 rounded-lg border border-border bg-text/5 text-text text-sm placeholder:text-text/40 outline-none focus:border-primary focus:ring-2 focus:ring-primary/50 transition-colors" />
)} {/* Global toolbar */} {value.length > 0 && (
)} {value.map((cat) => { const isCollapsed = collapsedCategories.has( cat.category, ) return ( toggleCategory(cat.category)} onRemove={() => removeCategory(cat.category)} newSettingNames={newSettingNames} newSettingTypes={newSettingTypes} onNewSettingNameChange={(name) => setNewSettingNames((prev) => ({ ...prev, [cat.category]: name, })) } onNewSettingTypeChange={(type) => setNewSettingTypes((prev) => ({ ...prev, [cat.category]: type, })) } onAddSetting={() => addSetting(cat.category)} setAddSettingInputRef={setAddSettingInputRef} onUpdateSetting={updateSetting} onRemoveSetting={removeSetting} onDuplicateSetting={duplicateSetting} onTypeSwitch={cycleSettingType} onReorderSettings={(newSettings) => handleReorderSettings( cat.category, newSettings, ) } pasteOpen={pasteOpenFor === cat.category} pasteText={pasteText} onPasteTextChange={setPasteText} onTogglePaste={() => setPasteOpenFor((prev) => prev === cat.category ? null : cat.category, ) } onParsePaste={() => parsePaste(cat.category)} /> ) })} {!isEmpty && (
setNewCategoryName(e.target.value)} onKeyDown={(e) => { if (e.key === "Enter") { e.preventDefault() addCategory() } }} placeholder="New category name..." className="flex-1 px-4 py-2.5 rounded-lg border border-border bg-text/5 text-text text-sm placeholder:text-text/40 outline-none focus:border-primary focus:ring-2 focus:ring-primary/50 transition-colors" />
)}
) } function CategoryCard({ cat, isCollapsed, onToggle, onRemove, newSettingNames, newSettingTypes, onNewSettingNameChange, onNewSettingTypeChange, onAddSetting, setAddSettingInputRef, onUpdateSetting, onRemoveSetting, onDuplicateSetting, onTypeSwitch, onReorderSettings, pasteOpen, pasteText, onPasteTextChange, onTogglePaste, onParsePaste, }: { cat: SettingCategory isCollapsed: boolean onToggle: () => void onRemove: () => void newSettingNames: Record newSettingTypes: Record onNewSettingNameChange: (name: string) => void onNewSettingTypeChange: (type: string) => void onAddSetting: () => void setAddSettingInputRef: ( category: string, ) => (el: HTMLInputElement | null) => void onUpdateSetting: ( cat: string, title: string, val: string | number | boolean, ) => void onRemoveSetting: (cat: string, title: string) => void onDuplicateSetting: (cat: string, title: string) => void onTypeSwitch: (cat: string, title: string) => void onReorderSettings: (newSettings: SettingItem[]) => void pasteOpen: boolean pasteText: string onPasteTextChange: (text: string) => void onTogglePaste: () => void onParsePaste: () => void }) { const dragControls = useDragControls() return ( {/* Category Header */}
{/* Category Drag Handle */}
{ e.preventDefault(); dragControls.start(e) }} >
{/* Collapse Toggle */} {/* Title */} {/* Remove Category */}
{!isCollapsed && (
{/* Add setting row at the top */}
{typeOptions.map((opt) => ( ))}
onNewSettingNameChange(e.target.value) } onKeyDown={(e) => { if (e.key === "Enter") { e.preventDefault() onAddSetting() } }} placeholder="Add setting..." className="flex-1 min-w-0 px-3 py-1.5 rounded-md border border-border bg-text/5 text-text text-sm placeholder:text-text/40 outline-none focus:border-primary focus:ring-2 focus:ring-primary/50 transition-colors" />
{/* Bulk paste textarea */} {pasteOpen && (