diff --git a/components/wizard/settings-editor.tsx b/components/wizard/settings-editor.tsx new file mode 100644 index 0000000..aa6e0ed --- /dev/null +++ b/components/wizard/settings-editor.tsx @@ -0,0 +1,335 @@ +"use client" + +import { useState } from "react" +import { Plus, X, ChevronDown, ChevronUp, ToggleLeft, ToggleRight } from "lucide-react" +import { motion, AnimatePresence } 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 }, + ], + }, +] + +interface SettingsEditorProps { + value: SettingCategory[] + onChange: (categories: SettingCategory[]) => void + className?: string +} + +export function SettingsEditor({ + value, + onChange, + className = "", +}: SettingsEditorProps) { + const [collapsedCategories, setCollapsedCategories] = useState>( + new Set() + ) + const [newCategoryName, setNewCategoryName] = useState("") + const [newSettingNames, setNewSettingNames] = useState>({}) + + 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 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 + + 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: "" }], + } + }) + ) + setNewSettingNames((prev) => ({ ...prev, [category]: "" })) + } + + 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 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 loadDefaults = () => { + onChange(DEFAULT_CATEGORIES) + setCollapsedCategories(new Set()) + } + + const isEmpty = value.length === 0 + + return ( +
+ {isEmpty && ( +
+

No settings configured

+ +
+ )} + + + {value.map((cat) => { + const isCollapsed = collapsedCategories.has(cat.category) + return ( + +
+ + +
+ + + {!isCollapsed && ( + +
+ + {cat.settings.map((setting) => ( + + + {setting.title} + + + {typeof setting.value === "boolean" ? ( + + ) : ( + { + const raw = e.target.value + const updated = + typeof setting.value === "number" + ? raw === "" + ? 0 + : Number(raw) + : raw + updateSetting( + cat.category, + setting.title, + updated + ) + }} + 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" + /> + )} + + + + ))} + + +
+ + setNewSettingNames((prev) => ({ + ...prev, + [cat.category]: e.target.value, + })) + } + onKeyDown={(e) => { + if (e.key === "Enter") { + e.preventDefault() + addSetting(cat.category) + } + }} + placeholder="Add setting..." + className="flex-1 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" + /> + +
+
+
+ )} +
+
+ ) + })} +
+ + {!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" + /> + +
+ )} +
+ ) +}