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 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,20 +229,29 @@ 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 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
|
<div
|
||||||
className="cursor-grab active:cursor-grabbing rounded p-1 text-neutral-500 hover:text-white transition-colors select-none"
|
className="cursor-grab active:cursor-grabbing rounded p-1 text-neutral-500 hover:text-white transition-colors select-none"
|
||||||
onMouseDown={(e) => handleGripMouseDown(e, i)}
|
onMouseDown={(e) => handleGripMouseDown(e, i)}
|
||||||
@@ -254,8 +266,12 @@ export function ArrayEditor<T>({
|
|||||||
<Trash2 size={16} />
|
<Trash2 size={16} />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="px-4 pb-4">
|
||||||
{renderItem(item, i, (updated) => updateItem(i, updated))}
|
{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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user