Skip to content

feat(site): add CollapsibleSection component and shared utilities#23990

Closed
DanielleMaywood wants to merge 1 commit into
mainfrom
danielle/collapsible-section
Closed

feat(site): add CollapsibleSection component and shared utilities#23990
DanielleMaywood wants to merge 1 commit into
mainfrom
danielle/collapsible-section

Conversation

@DanielleMaywood

@DanielleMaywood DanielleMaywood commented Apr 2, 2026

Copy link
Copy Markdown
Contributor

🤖 This PR was written by Coder Agent on behalf of Danielle Maywood 🤖

PR 1/4 in a stack to unify agent settings pages.

What

Adds a reusable CollapsibleSection component and applies it to the existing model form, replacing hand-rolled toggle logic.

Why

The agent settings pages are being restructured into collapsible bordered cards for visual consistency. Rather than each page rolling its own expand/collapse logic (as ModelForm.tsx already did with 3 separate useState + button + chevron blocks), this centralizes the pattern into a single component with proper accessibility (aria-expanded, aria-controls via useId(), keyboard handling for Enter/Space).

Changes

CollapsibleSection — two variants:

  • variant="card" (default): bordered card with rounded-lg border, h2 title, px-6 py-5 padding, hr divider. Used by settings pages (PRs 2–4).
  • variant="inline": lightweight border-t divider, h3 title, no outer padding. Designed for sub-sections inside forms where a full bordered card would be too heavy.

Both variants share toggle logic, aria-expanded/aria-controls, keyboard nav, action/badge slots with stopPropagation, and chevron animation.

ModelForm.tsx — replaces the 3 hand-rolled collapsible sections (Cost Tracking, Provider Configuration, Advanced) with <CollapsibleSection variant="inline">. Removes showAdvanced, showPricing, showProviderConfig state variables and the ChevronDownIcon/ChevronRightIcon imports they required.

AvatarData — adds min-w-0 to both inner flex containers. Without this, long usernames/titles cannot truncate because flexbox children default to min-width: auto, which prevents text from shrinking below its intrinsic width.

SectionHeader — adds min-w-0 flex-1 to the text container for the same truncation reason.

Stories — 6 stories with play functions covering card open/close, collapsed default, action slot isolation, keyboard toggle (Enter + Space), and the inline variant.

Stack

  1. ➡️ This PR — CollapsibleSection + shared fixes
  2. Behavior page CollapsibleSection cards (feat(site): wrap behavior page sections in CollapsibleSection cards #23991)
  3. Analytics DateRangePicker (feat(site): add DateRangePicker to analytics page #23992)
  4. Unified Spend page (feat: merge Limits + Usage into unified Spend page #24093)

@github-actions github-actions Bot added the community Pull Requests and issues created by the community. label Apr 2, 2026
@DanielleMaywood DanielleMaywood force-pushed the danielle/collapsible-section branch 2 times, most recently from b4985b7 to 4f8626a Compare April 7, 2026 12:58
@DanielleMaywood DanielleMaywood removed the community Pull Requests and issues created by the community. label Apr 7, 2026
@DanielleMaywood DanielleMaywood force-pushed the danielle/collapsible-section branch from 4f8626a to d26f486 Compare April 7, 2026 13:23
@DanielleMaywood DanielleMaywood force-pushed the danielle/collapsible-section branch 4 times, most recently from b6bee09 to 8f3b539 Compare April 7, 2026 13:56
@DanielleMaywood DanielleMaywood marked this pull request as ready for review April 7, 2026 14:07
@DanielleMaywood DanielleMaywood requested a review from mafredri April 7, 2026 14:07

@mafredri mafredri left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Solid extraction. The three hand-rolled toggle blocks in ModelForm are cleaner as a shared component, and building on Radix's Collapsible primitive is the right call for accessibility. The AvatarData and SectionHeader min-w-0 fixes are correct flexbox prerequisites.

Severity count: 1 P1, 1 P2, 3 P3, 2 Notes. One finding from all 7 reviewers.

The P1 is the heading level regression: the PR description promises the inline variant uses <h3>, but the code renders <h2> unconditionally. Every screen reader user who opens the model form gets a flat heading hierarchy instead of the parent/child nesting the old code had. This will compound across PRs 2-4 when card sections (h2) nest inline sub-sections (also h2). The other structural concern is the action slot living inside Radix's <button> trigger, which will become a problem when downstream PRs pass interactive controls into that slot.

Four {" "} text nodes were introduced as formatter artifacts. They're visually harmless (CSS collapses whitespace-only anonymous boxes in grid/flex contexts) but should be removed.

"Oh, this test suite looks polished! Six stories with play functions covering open, closed, keyboard, action isolation, inline variant. Real assertions on aria-expanded, content presence, toggle cycles." -- Bisky, before finding the heading level bug that no story checks for.

🤖 This review was automatically generated with Coder Agents.

>
<div className="min-w-0 flex-1">
<div className="flex items-center gap-2">
<h2 className={headingStyles({ variant })}>{title}</h2>

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 The inline variant renders <h2> but should render <h3>. The PR description explicitly states the inline variant uses "h3 title."

The old ModelForm used <h3> for Cost Tracking, Provider Configuration, and Advanced. The page already has <h2> for the parent section heading. After this change, subsections are also <h2>, flattening the heading hierarchy for screen readers (WCAG 1.3.1).

This is a class-of-bug: every future variant="inline" usage inherits the wrong heading level. PRs 2-4 in the stack will nest inline sections inside card sections, creating sibling <h2> elements where <h2> > <h3> is expected.

Fix: make the heading element variant-dependent (h2 for card, h3 for inline). The headingStyles CVA already differentiates visually; only the tag needs to change. (Knov P1, Bisky P2, Gon P2, Ging P2, Nami P2, Mafuuu P2, Meruem P2)

🤖

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 This comment was written by Coder Agent on behalf of Danielle Maywood 🤖

Fixed — card renders <h2>, inline renders <h3>. The headingStyles cva handles the visual differentiation, the tag handles the semantic level.

Comment thread site/src/pages/AgentsPage/components/CollapsibleSection.tsx Outdated
Comment thread site/src/pages/AgentsPage/components/ChatModelAdminPanel/ModelForm.tsx Outdated
Comment thread site/src/pages/AgentsPage/components/CollapsibleSection.stories.tsx Outdated
Comment thread site/src/pages/AgentsPage/components/CollapsibleSection.tsx Outdated
Comment thread site/src/pages/AgentsPage/components/CollapsibleSection.tsx
Comment thread site/src/pages/AgentsPage/components/CollapsibleSection.tsx Outdated
@DanielleMaywood DanielleMaywood force-pushed the danielle/collapsible-section branch 3 times, most recently from a28536d to fcfa599 Compare April 7, 2026 16:24
@DanielleMaywood DanielleMaywood force-pushed the danielle/collapsible-section branch from fcfa599 to de846d8 Compare April 7, 2026 17:54

@mafredri mafredri left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All 7 round 1 findings are resolved in code. The compound component refactor is a clean improvement over the original monolithic prop-based API. The as prop on CollapsibleSectionTitle is a better design than variant-dependent heading level: it lets the consumer match DOM nesting context rather than coupling heading semantics to visual variant.

Round 2 severity count: 1 P2, 2 P3. Clean design, minor gaps.

The P2 is the controlled mode API shipping without any story coverage. Every story uses uncontrolled mode (defaultOpen). The controlled path was added to resolve round 1 finding #5, but deleting lines 107-119 of CollapsibleSection.tsx leaves all five stories green. Passing open without onOpenChange silently freezes the section with no error or feedback.

One new stray {" "} was introduced at the same position class as the four removed in round 1. The as="h3" on the InlineVariant story exercises the heading prop but never asserts it.

Convention scan: em-dash (U+2014) found in CollapsibleSection.tsx:159 comment // Title — renders the heading element.... Project convention prohibits em-dashes. Replace with -- or rephrase.

"The tests that exist are genuine gems. They prove real behavior through the accessible API surface. Not a fake in the bunch. But let me look at what's not here." -- Bisky, before finding the missing controlled-mode story.

🤖 This review was automatically generated with Coder Agents.

render: () => (
<CollapsibleSection variant="inline" defaultOpen={false}>
<CollapsibleSectionHeader>
<CollapsibleSectionTitle as="h3">Cost Tracking</CollapsibleSectionTitle>

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 No story tests controlled mode (open/onOpenChange), the API surface added to resolve round 1 finding #comment-3045753898.

Every story uses uncontrolled mode (either defaultOpen or the implicit default). The controlled code path at CollapsibleSection.tsx lines 109-119 is untested. You could delete those lines and all five stories pass green.

PRs 2-4 in the stack will consume controlled mode (force-open on validation error, "expand all" toggle). A consumer passing open without onOpenChange gets a dead trigger that silently ignores clicks, and there's no story documenting or testing that edge.

Fix: add a Controlled story that manages open state externally, toggles via a parent button, and asserts the section responds. Also test the open-without-onOpenChange frozen state.

PS. The as="h3" on this story is exercised but never asserted. expect(canvas.getByRole("heading", { level: 3 })).toBeInTheDocument() would catch a regression of the round 1 heading-level fix. (Bisky P2, Nami P3, Mafuuu P3, Meruem P3, Razor P3, Ging P3)

🤖

</CollapsibleSection>
</div>
<div className="mt-auto py-6">
{" "}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3 New stray {" "} text node before <hr>. Same class of bug as resolved #comment-3045753889. The base code at this position had no whitespace node. The four original instances were removed, but this one was introduced. (Bisky P3, Gon P3, Nami P3, Mafuuu P3, Meruem P3, Razor P3, Ging P3)

🤖

@github-actions github-actions Bot added the stale This issue is like stale bread. label Apr 16, 2026
@github-actions github-actions Bot locked and limited conversation to collaborators Apr 17, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

stale This issue is like stale bread.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants