# CollapsibleSection component A reusable Card wrapper that enables independent collapse/expand of section content, allowing users to navigate long pages (like task-detail) without forcing continuous scrolling. Collapse/expand animations use only opacity and transform (no height/width), respecting prefers-reduced-motion globally. ## Purpose When task-detail pages carry long descriptions, many notes, and detailed plans, users must scroll through all expanded content to reach later sections. `CollapsibleSection` wraps each logical section (Description, Constraints, Notes fields, Plan subsections) in a collapsible card so users can fold away irrelevant content and jump to what they need. The component supports both **controlled** (e.g., force-open while editing) and **uncontrolled** (stateless) modes. ## Files | File | Role | |------|------| | `panel/src/components/tasks/task-detail/collapsible-section.tsx` | Component definition, `CollapsibleSectionProps` interface, state management. | | `panel/src/components/tasks/task-detail/task-description.tsx` | Description and Constraints sections wrapped. Constraints styled with amber accent border/background + ShieldAlert icon for visual distinction. | | `panel/src/components/tasks/task-detail/tab-notes.tsx` | Each editable note field (Description, Notes, Plan) wrapped. Edit/preview toggle is force-open while editing. | | `panel/src/components/tasks/task-detail/tab-plan.tsx` | Approach, Sub-Tasks, Technical Considerations, Risks, and Open Questions sections wrapped. | | `panel/src/app/globals.css` | Global `prefers-reduced-motion: reduce` override that disables all animations/transitions for users with reduced-motion enabled. | ## API ### `CollapsibleSectionProps` ```typescript interface CollapsibleSectionProps { /** Card title content (icon + text + badges as needed) */ title: ReactNode; /** Right-aligned header controls (edit/preview toggles, buttons) — always visible */ actions?: ReactNode; /** Controlled open state (e.g. force-open while a section is mid-edit). Omit for uncontrolled. */ open?: boolean; /** * Whether the (uncontrolled) section starts expanded. Takes precedence * over `content`-derived collapsing. Omit to let `content` decide, or to * default open when neither is given (so nothing visible today disappears). */ defaultOpen?: boolean; /** * Plain-text representation of the section's body, used to derive * `defaultOpen` per the content-readability spec (~10 lines / ~640 chars) * when `defaultOpen` is not explicitly set. Ignored otherwise. */ content?: string; /** Callback when the user toggles the section open/closed. */ onOpenChange?: (open: boolean) => void; /** Tailwind class string applied to the outer Card element. */ className?: string; /** Tailwind class string applied to the CardHeader (title + actions row). */ headerClassName?: string; /** Content rendered inside CardContent when the section is open. */ children: ReactNode; } ``` ### Component behavior - **Uncontrolled mode** (omit `open` prop): component manages its own open state. `defaultOpen` determines initial state; if `defaultOpen` is omitted, the component uses `content`-derived collapsing (if `content` is provided), or defaults to `true` if neither is set. `onOpenChange` is called when the user clicks the toggle; internal state updates automatically. - **Controlled mode** (`open` prop set): `onOpenChange` is called on toggle, but internal state is not updated; parent must update the `open` prop. Useful to force a section open while a user is editing (e.g., `open={isEditing || sectionOpen}`). - **Content-driven defaultOpen** (new): when `content` is provided without an explicit `defaultOpen`, the component checks if the content exceeds the readability thresholds (~10 lines / ~640 characters, per `content-readability.ts`). If it does, the section defaults collapsed; otherwise, it defaults open. This keeps long lists/sections from forcing continuous scrolling. An explicit `defaultOpen` prop always takes precedence over this logic, maintaining backward compatibility with existing callers. - **Title and actions**: title is always visible in the header; actions (right side) are also always visible, never collapsed away. This allows edit/preview toggles, save/cancel buttons, etc. to remain accessible. - **ChevronDown icon**: rotates -90° when closed, 0° when open. Uses `transition-transform duration-200` so the rotation animates smoothly. ### Animation Collapse/expand uses fade + slide from Tailwind CSS's `tw-animate-css` utilities: ```tsx "duration-200 data-[state=closed]:animate-out data-[state=open]:animate-in", "data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0", "data-[state=closed]:slide-out-to-top-1 data-[state=open]:slide-in-from-top-1", ``` - **Duration**: 200ms - **Animation type**: fade (opacity) + slide (translateY), both controlled via transform/opacity CSS properties only — no height/width animation, so layout does not reflow mid-animation. - **Accessibility**: `prefers-reduced-motion: reduce` is handled globally in `panel/src/app/globals.css`, which sets `animation-duration` and `transition-duration` to 0.01ms for all elements when the user has enabled reduced motion in their OS settings. The section content still opens/closes; it just doesn't animate. ## How to use Wrap any section content that should be collapsible: ```tsx "use client"; import { useState } from "react"; import { CollapsibleSection } from "./collapsible-section"; import { FileText, Edit3 } from "lucide-react"; export function MySection() { const [sectionOpen, setSectionOpen] = useState(true); const [isEditing, setIsEditing] = useState(false); const sectionText = "Section content here."; // Plain-text representation return ( Section Title } actions={ } content={sectionText} // Optional: drive defaultOpen based on content length open={isEditing || sectionOpen} onOpenChange={setSectionOpen} >

{sectionText}

); } ``` ### Using content-driven defaultOpen To automatically collapse long sections without explicit `defaultOpen`: ```tsx const listText = items.map(item => item.title).join("\n"); ``` If `listText` exceeds the readability thresholds, the section defaults collapsed; otherwise, it defaults open. No explicit `defaultOpen` prop needed. ### Controlled vs. uncontrolled **Uncontrolled (simple case):** ```tsx

Your notes content.

``` The component manages open state internally. `onOpenChange` is optional; if provided, it's called for logging/debugging, but state still updates automatically. **Controlled (e.g., force-open while editing):** ```tsx const [sectionOpen, setSectionOpen] = useState(true); const [isEditing, setIsEditing] = useState(false); {isEditing ?