Skip to content

fix(ui): align picker list-header surface and disable stuck gradient in dark mode#888

Merged
Astro-Han merged 3 commits into
devfrom
claude/picker-header-dark-bg
May 24, 2026
Merged

fix(ui): align picker list-header surface and disable stuck gradient in dark mode#888
Astro-Han merged 3 commits into
devfrom
claude/picker-header-dark-bg

Conversation

@Astro-Han

@Astro-Han Astro-Han commented May 24, 2026

Copy link
Copy Markdown
Owner

Summary

Fix two visible bugs in the model picker (and any <List> rendered inside a [data-picker-content] popover):

  1. Dark mode: the grouped header (e.g. "OpenCode Zen") reads as a darker band than the rest of the popover body.
  2. First item gets clipped: a ~16px stripe of header colour overlaps the first model row (e.g. "Big Pickle") once the list is scrolled.

Why

picker.css already tried to disable List's sticky header behaviour, but its override [data-picker-content] [data-slot="list-header"] (specificity 0,2,0) lost to list.css's default chain [data-component="list"] [data-slot="list-scroll"] [data-slot="list-group"] [data-slot="list-header"] (specificity 0,4,0). The override never applied: the header stayed position: sticky and the default 16px ::after linear-gradient(--surface-base → transparent) painted on top of the first item once GroupHeader flipped data-stuck="true" on scroll.

In dark mode the bug is doubly visible because List's default header background is --surface-base (#1a1917) while picker content uses --surface-raised (#2d2a27), so the sticky band reads as a darker stripe overlapping the first model row. In light mode both tokens are #ffffff, which is why bug 1 only manifests in dark.

Fix in packages/ui/src/components/picker.css:

  • Match list.css's 0,4,0 selector chain so picker.css (imported after list.css) wins on equal specificity.
  • Set background: var(--surface-raised) so the header sits flush with the picker body.
  • Hide the ::after stuck gradient inside pickers — the header is no longer sticky and the gradient has nothing to fade into.

command-palette.css already uses the same scoped pattern; this aligns the generic picker contract with it.

Related Issue

None — surfaced by visual inspection.

Human Review Status

Pending

Review Focus

  • packages/ui/src/components/picker.css — confirm the selector chain matches list.css's default rule exactly so the override outranks it on cascade order. Anything shorter silently loses.
  • Confirm the override is correctly scoped under [data-picker-content] and does not change <List> behaviour outside pickers (sidebar session list, dialogs, etc.).
  • New snap target packages/app/e2e/snap/model-picker-header.snap.ts captures the contract in light and dark.

Risk Notes

Scope is CSS only, gated on [data-picker-content]. No JS or other consumers touched. Did not change list.css's default header background — List is a generic primitive and binding its default to --surface-raised would silently flip every non-picker consumer (sidebar, dialogs). The picker-scoped override is the smallest correct fix.

How To Verify

bun run snap model-picker-header → 1 passed (8.5s); grid shows header at picker surface in dark and no overlap on first item
bun run lint → clean
bun x tsc --noEmit (packages/app) → clean
bun run dev:desktop → opened the real Electron build in dark mode, opened the model picker, scrolled through OpenCode Zen / OpenCode Go groups; author confirmed by eye that the group header rows match the popover surface and no longer clip the first model row

Screenshots or Recordings

image

Checklist

  • Type labelbug
  • Routing labelsapp, ui
  • Priority labelP2
  • Human Review Status above is set to Pending, Approved by @<reviewer>, or Not required: <reason> (default is Pending; "not required" is restricted to bot-authored low-risk PRs).
  • I linked the related issue, or stated in Summary why there is no issue.
  • I described the review focus and any meaningful risks.
  • I replaced the example block in How To Verify with the real verification steps and the key result for each.
  • I did not introduce unrelated refactors, dependencies, generated files, or file changes beyond the stated scope.
  • (conditional) I manually checked visible UI or copy changes when needed, with screenshots or recordings. Leave unticked only if no visible UI or copy changed.
  • (conditional) I considered macOS and Windows impact for platform, packaging, updater, signing, paths, shell, or permissions changes. Leave unticked only if no platform/packaging surface was touched.
  • (conditional) I called out docs, release notes, dependencies, permissions, credentials, deletion behavior, generated content, or local file changes when relevant. Leave unticked only if none of those surfaces was touched.
  • I reviewed the final diff for unrelated changes and suspicious dependency changes.
  • I am targeting dev, and my PR title and commit messages use Conventional Commits in English.

The picker.css override of List's grouped header used
`[data-picker-content] [data-slot="list-header"]` (specificity 0,2,0),
which loses to list.css's default rule on the longer
`[data-component="list"] [data-slot="list-scroll"] [data-slot="list-group"]
[data-slot="list-header"]` chain (specificity 0,4,0). The header therefore
stayed `position: sticky` inside pickers despite the override, and the
default 16px `::after` linear-gradient (--surface-base → transparent)
kept painting on top of the first item once GroupHeader flipped
`data-stuck="true"` on scroll.

In dark mode the visual gap is wider because the header background uses
--surface-base (#1a1917) while the picker body uses --surface-raised
(#2d2a27), so the sticky band reads as a darker stripe overlapping the
first model row.

Match the 0,4,0 chain in picker.css so the override actually applies,
swap the header background to --surface-raised so it sits flush with the
picker surface, and hide the stuck gradient inside pickers where the
header is no longer sticky. command-palette.css already uses the same
scoped pattern; this brings the generic picker contract in line.

Add e2e/snap/model-picker-header.snap.ts as the regression check —
captures the open model picker in light and dark so any future drop in
selector strength or surface-token drift shows up in the grid.
@coderabbitai

coderabbitai Bot commented May 24, 2026

Copy link
Copy Markdown
Contributor

Warning

Review limit reached

@Astro-Han, we couldn't start this review because you've used your available PR reviews for now.

Your plan includes 1 review of capacity. Refill in 5 minutes and 26 seconds.

Your organization has run out of usage credits. Purchase more in the billing tab.

⌛ How to resolve this issue?

After more review capacity refills, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than trial, open-source, and free plans. In all cases, review capacity refills continuously over time.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 02a553f9-ca8b-495a-a412-f5d2743015ef

📥 Commits

Reviewing files that changed from the base of the PR and between 3500fa5 and d0e947d.

📒 Files selected for processing (2)
  • packages/app/e2e/snap/model-picker-header.snap.ts
  • packages/ui/src/components/picker.css
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch claude/picker-header-dark-bg

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions github-actions Bot added app Application behavior and product flows ui Design system and user interface P2 Medium priority labels May 24, 2026

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Suggested priority: P2 (includes non-doc, non-test paths outside the low-risk bucket).

P1/P0 are reserved for maintainer confirmation. Please relabel manually if this is a release blocker, security issue, data-loss risk, or updater/runtime failure.

@Astro-Han Astro-Han added the bug Something isn't working label May 24, 2026

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Code Review

This pull request introduces a new visual regression test for the model picker header and updates the picker's CSS to ensure correct background colors and non-sticky behavior in dark mode. Feedback includes a suggestion to use CSS nesting for the updated styles to improve maintainability and a recommendation to avoid using .first() in E2E locators to ensure test stability.

Comment thread packages/ui/src/components/picker.css
Comment thread packages/app/e2e/snap/model-picker-header.snap.ts
Comment thread packages/app/e2e/snap/model-picker-header.snap.ts
@github-actions

github-actions Bot commented May 24, 2026

Copy link
Copy Markdown

Perf delta summary

Comparator: pass

Profile / Scenario interaction median interaction worst long task max tbt frame gap p95 frame gap max jank count cls status
default / homepage-cold 24 -> 24 (0) 24 -> 32 (+8) 56 -> 0 (-56) 6 -> 0 (-6) 16.8 -> 16.8 (0) 83.3 -> 66.7 (-16.6) 1 -> 2 (+1) 0 -> 0 (0) pass
default / long-session-input-lag 40 -> 40 (0) 40 -> 40 (0) 0 -> 0 (0) 0 -> 0 (0) 16.7 -> 16.8 (+0.1) 16.7 -> 16.8 (+0.1) 0 -> 0 (0) 0 -> 0 (0) pass
default / session-streaming-long 40 -> 40 (0) 56 -> 40 (-16) 0 -> 0 (0) 0 -> 0 (0) 66.7 -> 16.7 (-50) 66.7 -> 16.7 (-50) 1 -> 0 (-1) 0 -> 0 (0) pass
default / tool-call-expand 40 -> 24 (-16) 40 -> 40 (0) 0 -> 0 (0) 0 -> 0 (0) 16.7 -> 16.7 (0) 16.7 -> 16.7 (0) 0 -> 0 (0) 0 -> 0 (0) pass
default / tool-default-open-heavy-bash 0 -> 16 (+16) 0 -> 16 (+16) 0 -> 0 (0) 0 -> 0 (0) 33.3 -> 33.3 (0) 66.6 -> 66.7 (+0.1) 1 -> 1 (0) 0.004 -> 0.004 (0) pass
default / terminal-side-panel-open 32 -> 32 (0) 40 -> 48 (+8) 0 -> 0 (0) 0 -> 0 (0) 16.8 -> 16.7 (-0.1) 16.8 -> 16.7 (-0.1) 0 -> 0 (0) 0 -> 0 (0) pass
default / session-scroll-reading 0 -> 0 (0) 24 -> 0 (-24) 0 -> 0 (0) 0 -> 0 (0) 16.8 -> 16.7 (-0.1) 16.8 -> 16.7 (-0.1) 0 -> 0 (0) 0 -> 0 (0) pass

Astro-Han added 2 commits May 25, 2026 03:26
Address Gemini review feedback: nest the ::after { display: none } under
the header rule instead of repeating the four-deep selector chain. Same
compiled CSS, less duplication, matches list.css's nesting style.
Address review P2: the snap-only check screenshots the picker at rest,
but the underlying `data-stuck="true"` + ::after gradient bug only paints
once the list is scrolled. The fixture renders too few models to overflow
the picker's max-h-[400px] body, so a scroll-driven snap would not
reliably trigger stuck.

Read the computed style directly instead: header.position must be
`static`, header.backgroundColor must match the resolved
--surface-raised token, and ::after.display must be `none`. Any future
selector demotion below list.css's 0,4,0 chain, or a token swap, fails
the test even though the screenshot would still look right at rest.
@Astro-Han

Copy link
Copy Markdown
Owner Author

Thanks for the review. Both addressed:

P2 — snap coverage gap on stuck state: Verified the concern is real — list.tsx:230 only flips data-stuck="true" after scrollTop > 0, and the fixture renders too few models to overflow the picker's max-h-[400px] body, so a scroll-driven snap would not reliably reach the regression path.

Stronger fix in d0e947d: read the computed styles directly instead of relying on the screenshot. assertHeaderInvariants now asserts in both light and dark that

  • header.position === "static" — guards against the picker.css override dropping below list.css's 0,4,0 specificity chain and the header going sticky again.
  • header.backgroundColor === resolved(--surface-raised) — guards against the dark-mode header-band regression (bug 1).
  • ::after.display === "none" — guards against the 16px stuck-gradient repainting on top of the first item (bug 2's actual paint mechanism), independent of whether the list is currently scrolled.

This is tighter than scrolling: a regression that re-introduces the bug fails the test even on an unscrolled, single-group picker, where a scroll-driven snap might not have been able to trigger it at all.

P3 — CSS selector repetition: Already landed in 86dee217c3 (one commit before this review) — ::after is now nested under the header rule with &::after. The reviewed diff was b474a2c, before that commit.

@Astro-Han Astro-Han merged commit e8fd23d into dev May 24, 2026
27 checks passed
@Astro-Han Astro-Han deleted the claude/picker-header-dark-bg branch May 24, 2026 19:50
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

app Application behavior and product flows bug Something isn't working P2 Medium priority ui Design system and user interface

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant