feat: collapse/expand all — toggle icon in ArrayEditor + pricing sections

- ArrayEditor shows ChevronsUpDown toggle when collapsible with 2+ items
- Toggle appears even without label prop (fixes pricing missing icon)
- Pricing: centralized section state with toggle-all button for all 3 sections
This commit is contained in:
2026-03-26 11:31:31 +03:00
parent 4c8c6eb0d2
commit 09b2f40090
2 changed files with 74 additions and 33 deletions

View File

@@ -2,7 +2,7 @@
import { useState, useRef, useCallback, useEffect } from "react"; import { useState, useRef, useCallback, useEffect } from "react";
import { createPortal } from "react-dom"; import { createPortal } from "react-dom";
import { Plus, Trash2, GripVertical, ChevronDown } from "lucide-react"; import { Plus, Trash2, GripVertical, ChevronDown, ChevronsUpDown } from "lucide-react";
interface ArrayEditorProps<T> { interface ArrayEditorProps<T> {
items: T[]; items: T[];
@@ -293,8 +293,23 @@ export function ArrayEditor<T>({
return ( return (
<div> <div>
{label && ( {(label || (collapsible && items.length > 1)) && (
<h3 className="text-sm font-medium text-neutral-300 mb-3">{label}</h3> <div className="flex items-center justify-between mb-3">
{label ? <h3 className="text-sm font-medium text-neutral-300">{label}</h3> : <div />}
{collapsible && items.length > 1 && (() => {
const allCollapsed = collapsed.size >= items.length;
return (
<button
type="button"
onClick={() => allCollapsed ? setCollapsed(new Set()) : setCollapsed(new Set(items.map((_, i) => i)))}
className="rounded p-1 text-neutral-500 hover:text-white transition-colors"
title={allCollapsed ? "Развернуть все" : "Свернуть все"}
>
<ChevronsUpDown size={16} className={`transition-transform duration-200 ${allCollapsed ? "" : "rotate-90"}`} />
</button>
);
})()}
</div>
)} )}
{addPosition === "top" && ( {addPosition === "top" && (

View File

@@ -1,7 +1,7 @@
"use client"; "use client";
import { useState } from "react"; import { useState } from "react";
import { ChevronDown } from "lucide-react"; import { ChevronDown, ChevronsUpDown } from "lucide-react";
import { SectionEditor } from "../_components/SectionEditor"; import { SectionEditor } from "../_components/SectionEditor";
import { InputField, SelectField } from "../_components/FormField"; import { InputField, SelectField } from "../_components/FormField";
import { ArrayEditor } from "../_components/ArrayEditor"; import { ArrayEditor } from "../_components/ArrayEditor";
@@ -52,21 +52,21 @@ function PriceField({ label, value, onChange }: { label: string; value: string;
function CollapsibleSection({ function CollapsibleSection({
title, title,
count, count,
defaultOpen = true, isOpen,
onToggle,
children, children,
}: { }: {
title: string; title: string;
count?: number; count?: number;
defaultOpen?: boolean; isOpen: boolean;
onToggle: () => void;
children: React.ReactNode; children: React.ReactNode;
}) { }) {
const [open, setOpen] = useState(defaultOpen);
return ( return (
<div className="rounded-xl border border-white/10 bg-neutral-900/30 overflow-hidden"> <div className="rounded-xl border border-white/10 bg-neutral-900/30 overflow-hidden">
<button <button
type="button" type="button"
onClick={() => setOpen(!open)} onClick={onToggle}
className="flex items-center justify-between w-full px-5 py-3.5 text-left cursor-pointer group hover:bg-white/[0.02] transition-colors" className="flex items-center justify-between w-full px-5 py-3.5 text-left cursor-pointer group hover:bg-white/[0.02] transition-colors"
> >
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
@@ -77,12 +77,12 @@ function CollapsibleSection({
</div> </div>
<ChevronDown <ChevronDown
size={16} size={16}
className={`text-neutral-500 transition-transform duration-200 ${open ? "rotate-180" : ""}`} className={`text-neutral-500 transition-transform duration-200 ${isOpen ? "rotate-180" : ""}`}
/> />
</button> </button>
<div <div
className="grid transition-[grid-template-rows] duration-300 ease-out" className="grid transition-[grid-template-rows] duration-300 ease-out"
style={{ gridTemplateRows: open ? "1fr" : "0fr" }} style={{ gridTemplateRows: isOpen ? "1fr" : "0fr" }}
> >
<div className="overflow-hidden"> <div className="overflow-hidden">
<div className="px-5 pb-5 space-y-4"> <div className="px-5 pb-5 space-y-4">
@@ -94,12 +94,23 @@ function CollapsibleSection({
); );
} }
export default function PricingEditorPage() { function PricingContent({ data, update }: { data: PricingData; update: (d: PricingData) => void }) {
const [sections, setSections] = useState({ subscriptions: true, rental: true, rules: false });
const allOpen = sections.subscriptions && sections.rental && sections.rules;
function toggleAll() {
const target = !allOpen;
setSections({ subscriptions: target, rental: target, rules: target });
}
function toggleSection(key: keyof typeof sections) {
setSections(prev => ({ ...prev, [key]: !prev[key] }));
}
return ( return (
<SectionEditor<PricingData> sectionKey="pricing" title="Цены">
{(data, update) => (
<div className="space-y-4"> <div className="space-y-4">
<div className="grid gap-3 sm:grid-cols-2"> <div className="flex items-center justify-between">
<div className="grid gap-3 sm:grid-cols-2 flex-1">
<InputField <InputField
label="Заголовок секции" label="Заголовок секции"
value={data.title} value={data.title}
@@ -111,9 +122,18 @@ export default function PricingEditorPage() {
onChange={(v) => update({ ...data, subtitle: v })} onChange={(v) => update({ ...data, subtitle: v })}
/> />
</div> </div>
<button
type="button"
onClick={toggleAll}
className="rounded p-1 text-neutral-500 hover:text-white transition-colors ml-3 mt-4"
title={allOpen ? "Свернуть все секции" : "Развернуть все секции"}
>
<ChevronsUpDown size={16} className={`transition-transform duration-200 ${allOpen ? "rotate-90" : ""}`} />
</button>
</div>
{/* Абонементы */} {/* Абонементы */}
<CollapsibleSection title="Абонементы" count={data.items.length}> <CollapsibleSection title="Абонементы" count={data.items.length} isOpen={sections.subscriptions} onToggle={() => toggleSection("subscriptions")}>
{(() => { {(() => {
const itemOptions = data.items const itemOptions = data.items
.map((it, idx) => ({ value: String(idx), label: it.name })) .map((it, idx) => ({ value: String(idx), label: it.name }))
@@ -196,7 +216,7 @@ export default function PricingEditorPage() {
</CollapsibleSection> </CollapsibleSection>
{/* Аренда */} {/* Аренда */}
<CollapsibleSection title="Аренда" count={data.rentalItems.length}> <CollapsibleSection title="Аренда" count={data.rentalItems.length} isOpen={sections.rental} onToggle={() => toggleSection("rental")}>
<InputField <InputField
label="Заголовок" label="Заголовок"
value={data.rentalTitle} value={data.rentalTitle}
@@ -236,7 +256,7 @@ export default function PricingEditorPage() {
</CollapsibleSection> </CollapsibleSection>
{/* Правила */} {/* Правила */}
<CollapsibleSection title="Правила" count={data.rules.length} defaultOpen={false}> <CollapsibleSection title="Правила" count={data.rules.length} isOpen={sections.rules} onToggle={() => toggleSection("rules")}>
<ArrayEditor <ArrayEditor
items={data.rules} items={data.rules}
onChange={(rules) => update({ ...data, rules })} onChange={(rules) => update({ ...data, rules })}
@@ -248,7 +268,13 @@ export default function PricingEditorPage() {
/> />
</CollapsibleSection> </CollapsibleSection>
</div> </div>
)} );
}
export default function PricingEditorPage() {
return (
<SectionEditor<PricingData> sectionKey="pricing" title="Цены">
{(data, update) => <PricingContent data={data} update={update} />}
</SectionEditor> </SectionEditor>
); );
} }