Files
roboco/docs/frontend/components/collapsible-section.md
eefaca1d3b [3dfc43a1] Task detail overhaul: markdown, navigation, collapsible sections, timestamps (#410)
* [35a27c3d] UX/UI: design task-detail overhaul (#404)

* [39ea1900] docs(ux_ui): add content-readability spec for markdown, collapsible sections, timestamps (#388)

Co-authored-by: UX/UI Developer 1 <ux-dev-1@roboco.tech>

* [71f9aec6] docs(ux_ui): add task navigation/structure design spec (#400)

Adds docs/ux_ui/design/task-navigation-structure.md covering the
breadcrumb trail, prev/next sibling navigation, and a distinct visual
treatment for the read-only constraints section, grounded in the real
task-detail components and existing amber/Lock read-only tokens.

Co-authored-by: UX/UI Developer 2 <ux-dev-2@roboco.tech>

---------

Co-authored-by: UX/UI Developer 1 <ux-dev-1@roboco.tech>
Co-authored-by: UX/UI Developer 2 <ux-dev-2@roboco.tech>

* [9baa1c34] Frontend: implement task-detail overhaul (#408)

* [13b6c723] Task detail: inline timestamps + breadcrumb + prev/next navigation (#390)

* [13b6c723] feat(panel): add inline absolute timestamps, task breadcrumb, and prev/next list nav to task detail

Adds a shared formatAbsoluteTimestamp helper used inline (with tooltip)
next to relative time on progress updates and checkpoints in
tab-progress.tsx, progress-timeline.tsx, and checkpoint-card.tsx.
Adds TaskBreadcrumb (renders only when task.parent_task_id is set) and
TaskListNav, which reads a new taskListNav context in the
scroll-restoration zustand store — populated by the Tasks list page from
TaskTable's live filtered/sorted order — to move to the adjacent task.
When no list context exists for the session or the current task isn't
part of the captured order, both nav buttons render disabled with an
explanatory tooltip (the documented fallback).

* [13b6c723] docs(guide): task detail navigation, timestamps, breadcrumb, and prev/next behavior

---------

Co-authored-by: Frontend Developer 2 <fe-dev-2@roboco.tech>
Co-authored-by: Frontend Documenter <fe-doc@roboco.tech>

* [40acdd31] Task detail: collapsible markdown sections + distinct Constraints styling (#407)

* [40acdd31] feat(panel): collapsible task-detail sections + distinct Constraints styling

Wrap the Description, per-field Notes, and Plan cards in a new
CollapsibleSection (Radix Collapsible + tw-animate-css fade/slide, so
collapse/expand only animates opacity/transform) so a long task no longer
forces continuous scrolling. Restyle the read-only Constraints card with an
amber accent border, background tint, and ShieldAlert icon so it reads as
distinct from authored content. Existing edit/preview toggles are
force-open while active and otherwise unchanged. Adds a global
prefers-reduced-motion override in globals.css.

* [40acdd31] docs(panel): CollapsibleSection component API and usage guide

Documents the new CollapsibleSection wrapper component used for independent collapse/expand of task-detail sections (Description, Constraints, Notes, Plan). Covers component API, controlled vs. uncontrolled state patterns, animation behavior (fade+slide, transform/opacity only), prefers-reduced-motion handling, and usage examples across task-description.tsx / tab-notes.tsx / tab-plan.tsx.

---------

Co-authored-by: Frontend Developer 1 <fe-dev-1@roboco.tech>
Co-authored-by: Frontend Documenter <fe-doc@roboco.tech>

* [73f8311f] fix(task-table): remove exhaustive-deps suppression on visible-order effect (#409)

Co-authored-by: Frontend Developer 1 <fe-dev-1@roboco.tech>

---------

Co-authored-by: Frontend Developer 2 <fe-dev-2@roboco.tech>
Co-authored-by: Frontend Documenter <fe-doc@roboco.tech>
Co-authored-by: Frontend Developer 1 <fe-dev-1@roboco.tech>

* [eb417ef1] Fix: apply auto-collapse thresholds to Progress and Acceptance Criteria surfaces (#429)

* [4e855d24] Apply content-readability-spec collapse thresholds to Progress and Acceptance Criteria surfaces (#416)

* [4e855d24] feat(task-detail): auto-collapse long progress/checkpoint/AC content per readability spec

* [4e855d24] refactor(task-detail): remove inline JSX section-marker comments per no-inline-comments convention

* [4e855d24] docs(task-detail): document content-readability-spec collapse thresholds for CollapsibleSection

---------

Co-authored-by: Frontend Developer 1 <fe-dev-1@roboco.tech>
Co-authored-by: Frontend Developer 2 <fe-dev-2@roboco.tech>
Co-authored-by: Frontend Documenter <fe-doc@roboco.tech>

* [3c90ef34] Wire content-readability thresholds into CollapsibleSection, tab-progress, acceptance-criteria (#430)

* [3c90ef34] test(task-detail): add AC4 combined readability test — 30+ progress entries + long acceptance-criteria list

* [3c90ef34] docs: enhance content-readability thresholds documentation and code comments

- Enhance panel/src/lib/content-readability.ts with usage examples and clarified intent
- Enhance CollapsibleSection with auto-collapse logic explanation and precedence rules
- Enhance TabProgress's RECENT_OPEN_COUNT logic with dual-threshold explanation
- Add comprehensive architecture guide: panel/docs/CONTENT_READABILITY_THRESHOLDS.md covering thresholds, components, testing, and implementation notes

The readability feature prevents long-history tasks (30+ updates, 20+ criteria) from rendering fully expanded, keeping pages navigable. Tests confirm 32 progress updates default to 2 open, and long criteria lists collapse while short ones stay expanded.

---------

Co-authored-by: Frontend Developer 2 <fe-dev-2@roboco.tech>
Co-authored-by: Frontend Documenter <fe-doc@roboco.tech>

---------

Co-authored-by: Frontend Developer 1 <fe-dev-1@roboco.tech>
Co-authored-by: Frontend Developer 2 <fe-dev-2@roboco.tech>
Co-authored-by: Frontend Documenter <fe-doc@roboco.tech>

* [fc04d84a] Round-3 revision: fix 4 named gaps on task-detail overhaul, one dev leaf per fix (#455)

* [cac9b603] fix(panel): fall back to task.created_at for missing written_at stamp in tab-notes.tsx (#446)

Co-authored-by: Frontend Developer 2 <fe-dev-2@roboco.tech>

* [31dd4f99] Remove ArrowLeft back button from task-header.tsx (#441)

* [31dd4f99] Remove ArrowLeft back button and Link wrapper from task-header.tsx, drop now-unused imports

* [31dd4f99] docs(task-navigation): mark spec as implemented, clarify ArrowLeft button removal

Update task-navigation-structure.md to reflect v0.21.0+ implementation:
- Status changed from "proposed" to "implemented"
- Clarified that ArrowLeft back button was removed from task-header.tsx
- Noted that breadcrumb and prev/next navigation now provide all navigation
- Constraints section styling with amber tint and ShieldAlert icon is complete
- Referenced related guide documentation for task-detail-navigation features

---------

Co-authored-by: Frontend Developer 2 <fe-dev-2@roboco.tech>
Co-authored-by: Frontend Documenter <fe-doc@roboco.tech>

* [75fd7444] Wire content prop into EditableNoteCard's CollapsibleSection (#449)

* [75fd7444] feat(panel): wire content prop into EditableNoteCard's CollapsibleSection

Pass the note field's current value into CollapsibleSection's content
prop and derive EditableNoteCard's initial sectionOpen state from
exceedsReadabilityThreshold, so long notes default collapsed with an
expand affordance while short notes render fully expanded.

* [75fd7444] docs(panel): document EditableNoteCard's content-driven collapse pattern in collapsible-section.md

Updated docs/frontend/components/collapsible-section.md to reflect how EditableNoteCard in tab-notes.tsx uses both controlled mode (force-open while editing) and content-driven initialization (seed sectionOpen from content length). Added a new "Combined: controlled + content-driven initialization" example showing this pattern for future developers extending editable-content sections.

Pattern: long notes default collapsed with expand affordance, short notes default expanded, edit forms always visible during editing.

---------

Co-authored-by: Frontend Developer 1 <fe-dev-1@roboco.tech>
Co-authored-by: Frontend Documenter <fe-doc@roboco.tech>

* [18ada610] docs(ux-ui): reconcile prev/next nav design spec with shipped list-order behavior (#453)

Co-authored-by: Frontend Developer 2 <fe-dev-2@roboco.tech>

---------

Co-authored-by: Frontend Developer 2 <fe-dev-2@roboco.tech>
Co-authored-by: Frontend Documenter <fe-doc@roboco.tech>
Co-authored-by: Frontend Developer 1 <fe-dev-1@roboco.tech>

* [3dfc43a1] round-3 fixes: reconcile nav spec, Alt+Arrow shortcuts, CHANGELOG

The breadcrumb section of task-navigation-structure.md now describes the
shipped single-ancestor design (and drops the stale DropdownMenu claims);
Alt+ArrowLeft/Right on TaskListNav mirror the visible prev/next buttons,
suppressed while an editable element has focus, with tests; the
user-facing CHANGELOG entry lands under Unreleased. Also reflows the
round-1 content-readability-spec so the prose gate is green branch-wide.

* [3dfc43a1] blank line between Unreleased and 0.22.0 sections

---------

Co-authored-by: UX/UI Developer 1 <ux-dev-1@roboco.tech>
Co-authored-by: UX/UI Developer 2 <ux-dev-2@roboco.tech>
Co-authored-by: Frontend Developer 2 <fe-dev-2@roboco.tech>
Co-authored-by: Frontend Documenter <fe-doc@roboco.tech>
Co-authored-by: Frontend Developer 1 <fe-dev-1@roboco.tech>
Co-authored-by: Renn F <rennf93@users.noreply.github.com>
2026-07-11 07:41:15 +02:00

15 KiB

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

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:

"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:

"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 (
    <CollapsibleSection
      title={
        <>
          <FileText className="h-5 w-5" />
          Section Title
        </>
      }
      actions={
        <Button size="sm" variant="ghost" onClick={() => setIsEditing(true)}>
          <Edit3 className="h-4 w-4 mr-1" />
          Edit
        </Button>
      }
      content={sectionText}  // Optional: drive defaultOpen based on content length
      open={isEditing || sectionOpen}
      onOpenChange={setSectionOpen}
    >
      <p>{sectionText}</p>
    </CollapsibleSection>
  );
}

Using content-driven defaultOpen

To automatically collapse long sections without explicit defaultOpen:

const listText = items.map(item => item.title).join("\n");

<CollapsibleSection
  title="Long List"
  content={listText}  // Checked against ~10 lines / ~640 chars thresholds
>
  <ul>
    {items.map(item => (
      <li key={item.id}>{item.title}</li>
    ))}
  </ul>
</CollapsibleSection>

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):

<CollapsibleSection title="Notes" defaultOpen={true}>
  <p>Your notes content.</p>
</CollapsibleSection>

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):

const [sectionOpen, setSectionOpen] = useState(true);
const [isEditing, setIsEditing] = useState(false);

<CollapsibleSection
  title="Notes"
  open={isEditing || sectionOpen}
  onOpenChange={setSectionOpen}
>
  {isEditing ? <textarea /> : <p>Rendered content.</p>}
</CollapsibleSection>

When isEditing is true, the section is forced open even if the user clicked to close it. This prevents an edit form from being hidden mid-interaction.

Combined: controlled mode + content-driven initialization

For components that edit long content (like task notes), seed the initial collapsed/expanded state from content length, but use controlled mode to force-open during edit:

const currentValue = task.dev_notes;
const [isEditing, setIsEditing] = useState(false);
const [sectionOpen, setSectionOpen] = useState(() =>
  !exceedsReadabilityThreshold(currentValue ?? ""),
);

<CollapsibleSection
  title="Developer Notes"
  content={currentValue ?? undefined}  // Drives content-readability check
  open={isEditing || sectionOpen}       // Controlled: force-open while editing
  onOpenChange={setSectionOpen}
>
  {isEditing ? <textarea value={currentValue} /> : <p>{currentValue}</p>}
</CollapsibleSection>

This pattern (used in tab-notes.tsx's EditableNoteCard) ensures:

  • Long notes default collapsed with an expand affordance
  • Short notes default expanded (fully visible)
  • Edit forms are always visible when editing, even if the user had collapsed the section
  • User's collapse/expand choice persists across edit cycles (via sectionOpen state)

Used in

The component is now applied across task-detail pages:

Page / Component Sections wrapped Notes
task-description.tsx Description, Constraints Constraints section styled with amber border/background + ShieldAlert icon for visual distinction from authored content.
tab-notes.tsx Each editable note field (dev_notes, qa_notes, doc_notes, etc.) EditableNoteCard seeds initial sectionOpen from content length via exceedsReadabilityThreshold, passes content={currentValue} to CollapsibleSection, and forces open while editing via controlled open={isEditing || sectionOpen}. Long notes default collapsed with expand affordance; short notes default expanded.
tab-plan.tsx Approach, Sub-Tasks, Technical Considerations, Risks, Open Questions Each sub-section independently collapsible.
acceptance-criteria.tsx Full acceptance criteria list Wrapped in CollapsibleSection with content={criteriaText}, so a long AC list defaults collapsed per content-readability spec. Forced open while adding/editing via controlled open prop.
tab-progress.tsx Individual progress updates and checkpoints (via internal Radix Collapsible wrapper) Each entry wrapped in a collapsible section; only the 2 most recent entries default open (gated by content length as well). Older entries default collapsed even if short, keeping task detail navigable without endless scrolling.

Design decisions

  • Content-driven defaultOpen (new): instead of always defaulting open (which forced tasks with long histories to be fully expanded), the component now checks content length against readability thresholds (~10 lines / ~640 characters) when defaultOpen is not explicitly set. Long content defaults collapsed, keeping task-detail pages navigable. The explicit defaultOpen prop always takes precedence, so existing callers (task-description, tab-notes, tab-plan) that pass open={...} are unaffected — the content-length check only applies to uncontrolled sections. This is the "content-readability spec" driving progress and AC collapse in tab-progress and acceptance-criteria.

  • Fade + slide animation only: opacity and transform are GPU-accelerated and don't trigger layout reflow. Height/width animations are avoided because they force the browser to recalculate layout mid-animation, causing jank on slower devices and making the motion distracting.

  • Controlled + uncontrolled modes: uncontrolled is the default for simple read-only sections (no extra state management needed), while controlled mode (via open prop) lets parent components force a section open during edit (the common case for tab-notes EditableNoteCard).

  • Always-visible actions: the actions slot (buttons, toggles) is never collapsed, so users can always edit, delete, or perform actions on a section without expanding it first.

  • ChevronDown icon rotates, not replaces: using a rotating icon is more intuitive and uses less real estate than swapping between two different icons (Chevron-Down vs. Chevron-Up).

  • Global prefers-reduced-motion override: instead of checking prefers-reduced-motion in JavaScript (which is error-prone and scattered across components), a single global CSS rule ensures that all animations and transitions respect the user's OS setting. No component logic needed.

  • CardTitle inside the trigger: the title is inside a styled <button> (the CollapsibleTrigger) so it's keyboard-accessible and screenreader-labeled. The button is full-width (flex-1) and text-left, so users can click anywhere on the title to toggle.

Testing

The test suite covers:

  • Rendering a section with title, children, and optional actions.
  • Toggling the section open/closed on click.
  • Checking aria-expanded attribute on the trigger button.
  • Verifying that content is hidden when closed (not just visually; CollapsibleContent removes it from the DOM).
  • Controlled vs. uncontrolled state management.
  • Animation classes applied to CollapsibleContent based on open state.
  • ChevronDown icon rotation (CSS class toggling).
  • Always-visible actions slot (buttons not collapsed away).

See panel/src/components/tasks/task-detail/__tests__/task-description.test.tsx for integration tests covering:

  • Independent collapse of Description and Constraints sections.
  • Controlled open state while editing.
  • Edit/preview toggle working alongside collapse behavior.
  • Task detail: collapsible markdown sections + distinct Constraints styling — this commit. Introduces CollapsibleSection and applies it to task-description.tsx, tab-notes.tsx, tab-plan.tsx. Constraints section gets distinct amber styling.

Migration / rollout

For developers adding a new collapsible section to task-detail or other pages:

  1. Import CollapsibleSection from ./collapsible-section (adjust path as needed).
  2. Wrap the section content, provide a title (can include icons, badges).
  3. Optionally provide actions (buttons, toggles that should stay visible).
  4. For sections with potentially long content (lists, histories): pass a plain-text content prop so the section automatically collapses if the content is long. This defers to the content-readability spec thresholds (~10 lines / ~640 chars).
  5. For editable content, use the controlled pattern: open={isEditing || sectionOpen} to force-open while editing. The controlled open prop takes precedence over content-driven collapse.
  6. No extra state management is needed for read-only sections; the component handles it internally.

Example (uncontrolled with content-driven collapse):

const listText = items.map(item => item.name).join("\n");

<CollapsibleSection
  title="My List"
  content={listText}  // Drives defaultOpen based on length
>
  <ul>
    {items.map(item => (
      <li key={item.id}>{item.name}</li>
    ))}
  </ul>
</CollapsibleSection>

Example (controlled, forcing open during edit):

const [sectionOpen, setSectionOpen] = useState(true);
const [isEditing, setIsEditing] = useState(false);

<CollapsibleSection
  title="My Section"
  open={isEditing || sectionOpen}  // Controlled: explicit `open` wins over content
  onOpenChange={setSectionOpen}
>
  {isEditing ? <textarea /> : <p>Content here.</p>}
</CollapsibleSection>

Accessibility

  • aria-expanded attribute on the trigger button communicates the open/closed state to screenreaders.
  • Keyboard support: the trigger is a <button>, so it's focusable with Tab and activatable with Enter/Space.
  • aria-hidden="true" on the ChevronDown icon: the icon is decorative; screenreaders skip it.
  • prefers-reduced-motion support: animations are disabled globally for users with motion sensitivity, but the section still opens/closes (the content is not hidden, just instant).