feat: FAQ collapsible + drag UX — gold styling, compact cards, drop highlight

This commit is contained in:
2026-03-26 00:55:39 +03:00
parent 95c33391e5
commit 30398d2aeb
2 changed files with 42 additions and 24 deletions

View File

@@ -37,6 +37,7 @@ export function ArrayEditor<T>({
const itemRefs = useRef<(HTMLDivElement | null)[]>([]); const itemRefs = useRef<(HTMLDivElement | null)[]>([]);
const [mounted, setMounted] = useState(false); const [mounted, setMounted] = useState(false);
const [newItemIndex, setNewItemIndex] = useState<number | null>(null); const [newItemIndex, setNewItemIndex] = useState<number | null>(null);
const [droppedIndex, setDroppedIndex] = useState<number | null>(null);
const [collapsed, setCollapsed] = useState<Set<number>>(() => collapsible ? new Set(items.map((_, i) => i)) : new Set()); const [collapsed, setCollapsed] = useState<Set<number>>(() => collapsible ? new Set(items.map((_, i) => i)) : new Set());
function toggleCollapse(index: number) { function toggleCollapse(index: number) {
@@ -132,6 +133,8 @@ export function ArrayEditor<T>({
const [moved] = updated.splice(capturedDrag, 1); const [moved] = updated.splice(capturedDrag, 1);
updated.splice(targetIndex, 0, moved); updated.splice(targetIndex, 0, moved);
onChange(updated); onChange(updated);
setDroppedIndex(targetIndex);
setTimeout(() => setDroppedIndex(null), 1500);
} }
} }
}); });
@@ -157,7 +160,7 @@ export function ArrayEditor<T>({
key={i} key={i}
ref={(el) => { itemRefs.current[i] = el; }} ref={(el) => { itemRefs.current[i] = el; }}
className={`rounded-lg border bg-neutral-900/50 mb-3 hover:border-white/25 hover:bg-neutral-800/50 focus-within:border-gold/50 focus-within:bg-neutral-800 transition-all ${ className={`rounded-lg border bg-neutral-900/50 mb-3 hover:border-white/25 hover:bg-neutral-800/50 focus-within:border-gold/50 focus-within:bg-neutral-800 transition-all ${
newItemIndex === i ? "border-gold/40 ring-1 ring-gold/20" : "border-white/10" newItemIndex === i || droppedIndex === i ? "border-gold/40 ring-1 ring-gold/20" : "border-white/10"
} ${isHidden ? "hidden" : ""}`} } ${isHidden ? "hidden" : ""}`}
> >
<div className={`flex items-center justify-between gap-2 p-4 ${isCollapsed ? "" : "pb-0 mb-3"}`}> <div className={`flex items-center justify-between gap-2 p-4 ${isCollapsed ? "" : "pb-0 mb-3"}`}>
@@ -226,35 +229,48 @@ export function ArrayEditor<T>({
elements.push( elements.push(
<div <div
key="placeholder" key="placeholder"
className="rounded-lg border-2 border-dashed border-rose-500/50 bg-rose-500/5 mb-3" className="rounded-lg border-2 border-dashed border-gold/40 bg-gold/5 mb-3"
style={{ height: dragSize.h }} style={{ height: collapsible ? 48 : dragSize.h }}
/> />
); );
} }
const item = items[i]; const item = items[i];
const dragTitle = getItemTitle?.(item, i) || `#${i + 1}`;
elements.push( elements.push(
<div <div
key={i} key={i}
ref={(el) => { itemRefs.current[i] = el; }} ref={(el) => { itemRefs.current[i] = el; }}
className="rounded-lg border border-white/10 bg-neutral-900/50 p-4 mb-3 hover:border-white/25 hover:bg-neutral-800/50 focus-within:border-gold/50 focus-within:bg-neutral-800 transition-colors" className="rounded-lg border border-white/10 bg-neutral-900/50 mb-3 transition-colors"
> >
<div className="flex items-start justify-between gap-2 mb-3"> {collapsible ? (
<div <div className="flex items-center gap-2 p-4">
className="cursor-grab active:cursor-grabbing rounded p-1 text-neutral-500 hover:text-white transition-colors select-none" <GripVertical size={16} className="text-neutral-500 shrink-0" />
onMouseDown={(e) => handleGripMouseDown(e, i)} <span className="text-sm font-medium text-neutral-300 truncate">{dragTitle}</span>
> {getItemBadge?.(item, i)}
<GripVertical size={16} />
</div> </div>
<button ) : (
type="button" <>
onClick={() => removeItem(i)} <div className="flex items-start justify-between gap-2 p-4 pb-0 mb-3">
className="rounded p-1 text-neutral-500 hover:text-red-400 transition-colors" <div
> className="cursor-grab active:cursor-grabbing rounded p-1 text-neutral-500 hover:text-white transition-colors select-none"
<Trash2 size={16} /> onMouseDown={(e) => handleGripMouseDown(e, i)}
</button> >
</div> <GripVertical size={16} />
{renderItem(item, i, (updated) => updateItem(i, updated))} </div>
<button
type="button"
onClick={() => removeItem(i)}
className="rounded p-1 text-neutral-500 hover:text-red-400 transition-colors"
>
<Trash2 size={16} />
</button>
</div>
<div className="px-4 pb-4">
{renderItem(item, i, (updated) => updateItem(i, updated))}
</div>
</>
)}
</div> </div>
); );
visualIndex++; visualIndex++;
@@ -264,8 +280,8 @@ export function ArrayEditor<T>({
elements.push( elements.push(
<div <div
key="placeholder" key="placeholder"
className="rounded-lg border-2 border-dashed border-rose-500/50 bg-rose-500/5 mb-3" className="rounded-lg border-2 border-dashed border-gold/40 bg-gold/5 mb-3"
style={{ height: dragSize.h }} style={{ height: collapsible ? 48 : dragSize.h }}
/> />
); );
} }
@@ -304,9 +320,9 @@ export function ArrayEditor<T>({
height: dragSize.h, height: dragSize.h,
}} }}
> >
<div className="h-full rounded-lg border-2 border-rose-500 bg-neutral-900/95 shadow-2xl shadow-rose-500/20 flex items-center gap-3 px-4"> <div className="h-full rounded-lg border-2 border-gold/60 bg-neutral-900/95 shadow-2xl shadow-gold/20 flex items-center gap-3 px-4">
<GripVertical size={16} className="text-rose-400 shrink-0" /> <GripVertical size={16} className="text-gold shrink-0" />
<span className="text-sm text-neutral-300">Перемещение элемента...</span> <span className="text-sm text-neutral-300">{collapsible && dragIndex !== null ? (getItemTitle?.(items[dragIndex], dragIndex) || "Перемещение...") : "Перемещение элемента..."}</span>
</div> </div>
</div>, </div>,
document.body document.body

View File

@@ -23,6 +23,8 @@ export default function FAQEditorPage() {
label="Вопросы и ответы" label="Вопросы и ответы"
items={data.items} items={data.items}
onChange={(items) => update({ ...data, items })} onChange={(items) => update({ ...data, items })}
collapsible
getItemTitle={(item) => item.question || "Без вопроса"}
renderItem={(item, _i, updateItem) => ( renderItem={(item, _i, updateItem) => (
<div className="space-y-3"> <div className="space-y-3">
<InputField <InputField