feat: FAQ collapsible + drag UX — gold styling, compact cards, drop highlight
This commit is contained in:
@@ -37,6 +37,7 @@ export function ArrayEditor<T>({
|
||||
const itemRefs = useRef<(HTMLDivElement | null)[]>([]);
|
||||
const [mounted, setMounted] = useState(false);
|
||||
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());
|
||||
|
||||
function toggleCollapse(index: number) {
|
||||
@@ -132,6 +133,8 @@ export function ArrayEditor<T>({
|
||||
const [moved] = updated.splice(capturedDrag, 1);
|
||||
updated.splice(targetIndex, 0, moved);
|
||||
onChange(updated);
|
||||
setDroppedIndex(targetIndex);
|
||||
setTimeout(() => setDroppedIndex(null), 1500);
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -157,7 +160,7 @@ export function ArrayEditor<T>({
|
||||
key={i}
|
||||
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 ${
|
||||
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" : ""}`}
|
||||
>
|
||||
<div className={`flex items-center justify-between gap-2 p-4 ${isCollapsed ? "" : "pb-0 mb-3"}`}>
|
||||
@@ -226,20 +229,29 @@ export function ArrayEditor<T>({
|
||||
elements.push(
|
||||
<div
|
||||
key="placeholder"
|
||||
className="rounded-lg border-2 border-dashed border-rose-500/50 bg-rose-500/5 mb-3"
|
||||
style={{ height: dragSize.h }}
|
||||
className="rounded-lg border-2 border-dashed border-gold/40 bg-gold/5 mb-3"
|
||||
style={{ height: collapsible ? 48 : dragSize.h }}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const item = items[i];
|
||||
const dragTitle = getItemTitle?.(item, i) || `#${i + 1}`;
|
||||
elements.push(
|
||||
<div
|
||||
key={i}
|
||||
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 className="flex items-center gap-2 p-4">
|
||||
<GripVertical size={16} className="text-neutral-500 shrink-0" />
|
||||
<span className="text-sm font-medium text-neutral-300 truncate">{dragTitle}</span>
|
||||
{getItemBadge?.(item, i)}
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<div className="flex items-start justify-between gap-2 p-4 pb-0 mb-3">
|
||||
<div
|
||||
className="cursor-grab active:cursor-grabbing rounded p-1 text-neutral-500 hover:text-white transition-colors select-none"
|
||||
onMouseDown={(e) => handleGripMouseDown(e, i)}
|
||||
@@ -254,8 +266,12 @@ export function ArrayEditor<T>({
|
||||
<Trash2 size={16} />
|
||||
</button>
|
||||
</div>
|
||||
<div className="px-4 pb-4">
|
||||
{renderItem(item, i, (updated) => updateItem(i, updated))}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
visualIndex++;
|
||||
}
|
||||
@@ -264,8 +280,8 @@ export function ArrayEditor<T>({
|
||||
elements.push(
|
||||
<div
|
||||
key="placeholder"
|
||||
className="rounded-lg border-2 border-dashed border-rose-500/50 bg-rose-500/5 mb-3"
|
||||
style={{ height: dragSize.h }}
|
||||
className="rounded-lg border-2 border-dashed border-gold/40 bg-gold/5 mb-3"
|
||||
style={{ height: collapsible ? 48 : dragSize.h }}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -304,9 +320,9 @@ export function ArrayEditor<T>({
|
||||
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">
|
||||
<GripVertical size={16} className="text-rose-400 shrink-0" />
|
||||
<span className="text-sm text-neutral-300">Перемещение элемента...</span>
|
||||
<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-gold shrink-0" />
|
||||
<span className="text-sm text-neutral-300">{collapsible && dragIndex !== null ? (getItemTitle?.(items[dragIndex], dragIndex) || "Перемещение...") : "Перемещение элемента..."}</span>
|
||||
</div>
|
||||
</div>,
|
||||
document.body
|
||||
|
||||
@@ -23,6 +23,8 @@ export default function FAQEditorPage() {
|
||||
label="Вопросы и ответы"
|
||||
items={data.items}
|
||||
onChange={(items) => update({ ...data, items })}
|
||||
collapsible
|
||||
getItemTitle={(item) => item.question || "Без вопроса"}
|
||||
renderItem={(item, _i, updateItem) => (
|
||||
<div className="space-y-3">
|
||||
<InputField
|
||||
|
||||
Reference in New Issue
Block a user