diff --git a/src/components/editor/ProjectSettingsModal.tsx b/src/components/editor/ProjectSettingsModal.tsx index e8b5360..4add2f7 100644 --- a/src/components/editor/ProjectSettingsModal.tsx +++ b/src/components/editor/ProjectSettingsModal.tsx @@ -373,49 +373,345 @@ function CharacterForm({ formData, formError, onChange, onSave, onCancel, saveLa ) } -// Variables Tab (placeholder for US-057) +// Variables Tab type VariablesTabProps = { variables: Variable[] onChange: (variables: Variable[]) => void getUsageCount: (variableId: string) => number } -function VariablesTab({ variables }: VariablesTabProps) { +type VariableType = 'numeric' | 'string' | 'boolean' + +type VariableFormData = { + name: string + type: VariableType + initialValue: string + description: string +} + +const defaultInitialValues: Record = { + numeric: '0', + string: '', + boolean: 'false', +} + +function parseInitialValue(type: VariableType, raw: string): number | string | boolean { + switch (type) { + case 'numeric': + return Number(raw) || 0 + case 'boolean': + return raw === 'true' + case 'string': + return raw + } +} + +function VariablesTab({ variables, onChange, getUsageCount }: VariablesTabProps) { + const [isAdding, setIsAdding] = useState(false) + const [editingId, setEditingId] = useState(null) + const [formData, setFormData] = useState({ name: '', type: 'numeric', initialValue: '0', description: '' }) + const [formError, setFormError] = useState(null) + const [deleteConfirm, setDeleteConfirm] = useState(null) + + const resetForm = () => { + setFormData({ name: '', type: 'numeric', initialValue: '0', description: '' }) + setFormError(null) + } + + const validateName = (name: string, excludeId?: string): boolean => { + if (!name.trim()) { + setFormError('Name is required') + return false + } + const duplicate = variables.find( + (v) => v.name.toLowerCase() === name.trim().toLowerCase() && v.id !== excludeId + ) + if (duplicate) { + setFormError('A variable with this name already exists') + return false + } + setFormError(null) + return true + } + + const handleAdd = () => { + if (!validateName(formData.name)) return + const newVariable: Variable = { + id: nanoid(), + name: formData.name.trim(), + type: formData.type, + initialValue: parseInitialValue(formData.type, formData.initialValue), + description: formData.description.trim() || undefined, + } + onChange([...variables, newVariable]) + setIsAdding(false) + resetForm() + } + + const handleEdit = (variable: Variable) => { + setEditingId(variable.id) + setFormData({ + name: variable.name, + type: variable.type, + initialValue: String(variable.initialValue), + description: variable.description || '', + }) + setFormError(null) + setIsAdding(false) + } + + const handleSaveEdit = () => { + if (!editingId) return + if (!validateName(formData.name, editingId)) return + onChange( + variables.map((v) => + v.id === editingId + ? { + ...v, + name: formData.name.trim(), + type: formData.type, + initialValue: parseInitialValue(formData.type, formData.initialValue), + description: formData.description.trim() || undefined, + } + : v + ) + ) + setEditingId(null) + resetForm() + } + + const handleDelete = (id: string) => { + const usageCount = getUsageCount(id) + if (usageCount > 0 && deleteConfirm !== id) { + setDeleteConfirm(id) + return + } + onChange(variables.filter((v) => v.id !== id)) + setDeleteConfirm(null) + } + + const handleCancelForm = () => { + setIsAdding(false) + setEditingId(null) + resetForm() + } + return (
-

- Define variables that can be referenced in variable nodes and edge conditions. -

- {variables.length === 0 ? ( -

- No variables defined yet. Variable management will be available in a future update. +

+

+ Define variables that can be referenced in variable nodes and edge conditions.

- ) : ( -
- {variables.map((variable) => ( -
-
- - {variable.type} - -
- - {variable.name} + {!isAdding && !editingId && ( + + )} +
+ + {/* Variable List */} +
+ {variables.map((variable) => ( +
+ {editingId === variable.id ? ( + + ) : ( +
+
+ + {variable.type} - {variable.description && ( -

- {variable.description} -

- )} +
+ + {variable.name} + + {variable.description && ( +

+ {variable.description} +

+ )} +
+
+
+ + Initial: {String(variable.initialValue)} + +
+ {deleteConfirm === variable.id && ( + + Used in {getUsageCount(variable.id)} node(s). Delete anyway? + + )} + + +
- - Initial: {String(variable.initialValue)} - -
- ))} + )} +
+ ))} + + {variables.length === 0 && !isAdding && ( +

+ No variables defined yet. Click "Add Variable" to create one. +

+ )} +
+ + {/* Add Form */} + {isAdding && ( +
+
)}
) } + +// Variable Form +type VariableFormProps = { + formData: VariableFormData + formError: string | null + onChange: (data: VariableFormData) => void + onSave: () => void + onCancel: () => void + saveLabel: string +} + +function VariableForm({ formData, formError, onChange, onSave, onCancel, saveLabel }: VariableFormProps) { + const handleTypeChange = (newType: VariableType) => { + onChange({ ...formData, type: newType, initialValue: defaultInitialValues[newType] }) + } + + return ( +
+
+
+
+ + onChange({ ...formData, name: e.target.value })} + placeholder="Variable name" + autoFocus + className="block w-full rounded-md border border-zinc-300 bg-white px-3 py-1.5 text-sm text-zinc-900 placeholder-zinc-400 focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-500 dark:border-zinc-600 dark:bg-zinc-800 dark:text-zinc-50 dark:placeholder-zinc-500" + /> +
+
+ + +
+
+
+ + {formData.type === 'boolean' ? ( + + ) : formData.type === 'numeric' ? ( + onChange({ ...formData, initialValue: e.target.value })} + className="block w-full rounded-md border border-zinc-300 bg-white px-3 py-1.5 text-sm text-zinc-900 placeholder-zinc-400 focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-500 dark:border-zinc-600 dark:bg-zinc-800 dark:text-zinc-50 dark:placeholder-zinc-500" + /> + ) : ( + onChange({ ...formData, initialValue: e.target.value })} + placeholder="Initial value" + className="block w-full rounded-md border border-zinc-300 bg-white px-3 py-1.5 text-sm text-zinc-900 placeholder-zinc-400 focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-500 dark:border-zinc-600 dark:bg-zinc-800 dark:text-zinc-50 dark:placeholder-zinc-500" + /> + )} +
+
+ + onChange({ ...formData, description: e.target.value })} + placeholder="Optional description" + className="block w-full rounded-md border border-zinc-300 bg-white px-3 py-1.5 text-sm text-zinc-900 placeholder-zinc-400 focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-500 dark:border-zinc-600 dark:bg-zinc-800 dark:text-zinc-50 dark:placeholder-zinc-500" + /> +
+ {formError && ( +

{formError}

+ )} +
+ + +
+
+
+ ) +}