121 lines
4.7 KiB
TypeScript
121 lines
4.7 KiB
TypeScript
import { useState } from 'react';
|
|
import { useTranslation } from 'react-i18next';
|
|
import { MovementForm } from './MovementForm';
|
|
|
|
export interface MovementAccordionProps {
|
|
movements: any[];
|
|
onChange: (index: number, field: string, value: any) => void;
|
|
onAdd: () => void;
|
|
onRemove: (index: number) => void;
|
|
onMove: (index: number, direction: 'up' | 'down') => void;
|
|
disabled?: boolean;
|
|
}
|
|
|
|
export function MovementAccordion({ movements, onChange, onAdd, onRemove, onMove, disabled = false }: MovementAccordionProps) {
|
|
const { t } = useTranslation('settings');
|
|
const [expandedIndex, setExpandedIndex] = useState<number | null>(null);
|
|
const movementNames = movements.map((m) => m.name ?? '');
|
|
|
|
const toggle = (i: number) => {
|
|
setExpandedIndex((prev) => (prev === i ? null : i));
|
|
};
|
|
|
|
return (
|
|
<div>
|
|
<h3 className="text-sm font-semibold text-slate-800 mb-2">Movements</h3>
|
|
<div className="space-y-2">
|
|
{movements.map((movement, i) => {
|
|
const isExpanded = expandedIndex === i;
|
|
const toolCount = (movement.allowed_tools ?? []).length;
|
|
const ruleCount = (movement.rules ?? []).length;
|
|
|
|
return (
|
|
<div key={i} className="bg-canvas border border-slate-200 rounded-lg">
|
|
{/* Collapsed header */}
|
|
<div
|
|
className="flex items-center gap-2 p-3 cursor-pointer select-none"
|
|
onClick={() => toggle(i)}
|
|
>
|
|
<span className="text-xs text-slate-400 mr-1">{isExpanded ? '\u25BC' : '\u25B6'}</span>
|
|
<span className="text-sm font-medium text-slate-800">{movement.name || '(unnamed)'}</span>
|
|
{movement.persona && (
|
|
<span className="bg-blue-100 dark:bg-blue-500/15 text-blue-700 dark:text-blue-300 text-xs px-2 py-0.5 rounded">
|
|
{movement.persona}
|
|
</span>
|
|
)}
|
|
<span className={`text-xs px-2 py-0.5 rounded ${movement.edit ? 'bg-green-100 dark:bg-green-500/15 text-green-700 dark:text-green-300' : 'bg-purple-100 dark:bg-purple-500/15 text-purple-700 dark:text-purple-300'}`}>
|
|
edit: {movement.edit ? 'on' : 'off'}
|
|
</span>
|
|
<span className="text-xs text-slate-400">{toolCount} tools</span>
|
|
<span className="text-xs text-slate-400">{ruleCount} rules</span>
|
|
|
|
{/* Spacer */}
|
|
<div className="flex-1" />
|
|
|
|
{/* Controls — hidden in read-only mode */}
|
|
{!disabled && (
|
|
<div className="flex items-center gap-1" onClick={(e) => e.stopPropagation()}>
|
|
<button
|
|
type="button"
|
|
onClick={() => onMove(i, 'up')}
|
|
disabled={i === 0}
|
|
className="text-slate-400 hover:text-slate-600 disabled:opacity-30 text-sm px-1"
|
|
title="Move up"
|
|
>
|
|
▲
|
|
</button>
|
|
<button
|
|
type="button"
|
|
onClick={() => onMove(i, 'down')}
|
|
disabled={i === movements.length - 1}
|
|
className="text-slate-400 hover:text-slate-600 disabled:opacity-30 text-sm px-1"
|
|
title="Move down"
|
|
>
|
|
▼
|
|
</button>
|
|
<button
|
|
type="button"
|
|
onClick={() => {
|
|
if (confirm(t('movementAccordion.confirmDelete', { name: movement.name }))) {
|
|
onRemove(i);
|
|
if (expandedIndex === i) setExpandedIndex(null);
|
|
}
|
|
}}
|
|
className="text-slate-400 hover:text-red-500 text-sm px-1"
|
|
title="Delete"
|
|
>
|
|
×
|
|
</button>
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{/* Expanded form */}
|
|
{isExpanded && (
|
|
<div className="px-3 pb-3 border-t border-slate-100 pt-3">
|
|
<MovementForm
|
|
movement={movement}
|
|
movementNames={movementNames}
|
|
onChange={(field, value) => onChange(i, field, value)}
|
|
disabled={disabled}
|
|
/>
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
|
|
{!disabled && (
|
|
<button
|
|
type="button"
|
|
onClick={onAdd}
|
|
className="mt-3 text-sm text-blue-600 hover:text-blue-700 dark:hover:text-blue-300"
|
|
>
|
|
+ Add Movement
|
|
</button>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|