Skip to content

feat(web): restyle sidebar chrome and monochrome accents - #43

Merged
NoahHendrickson merged 2 commits into
customfrom
fork/sidebar-chrome-actions
Aug 2, 2026
Merged

feat(web): restyle sidebar chrome and monochrome accents#43
NoahHendrickson merged 2 commits into
customfrom
fork/sidebar-chrome-actions

Conversation

@NoahHendrickson

@NoahHendrickson NoahHendrickson commented Aug 2, 2026

Copy link
Copy Markdown
Owner

Summary

  • Restructures Sidebar V2 chrome into labeled Search, New thread, and Add project rows, with a static Projects label and filter menu for scope / group-by.
  • Active project scope lifts the funnel (foreground) and names the scope in aria-label / tooltip — filtered lists are never a silent mode.
  • Makes fork dark --primary / --ring white app-wide (not sidebar-only): focus rings, switches, primary buttons/badges, and related accents go monochrome. Light mode keeps upstream blue.
  • Aligns the shared workspace/sidebar topbar to 52px.

Test plan

  • Sidebar V2: Search / New thread / Add project stack with 4px gaps and 32px controls; New thread shows shortcut Kbd when available
  • Scope to one project: funnel lifts + tooltip/aria name the project; clear via All projects
  • Group-by switch still works from the filter menu; white on-state in dark
  • Spot-check dark primary Button, Switch, Checkbox, and a focused Input (white accent, not blue)
  • Confirm sidebar header and workspace header share one 52px seam
  • vp test run apps/web/src/__fork_guards__/forkSidebarChrome.test.ts apps/web/src/__fork_guards__/forkSurfacePalette.test.ts apps/web/src/__fork_guards__/forkWorkspaceHeader.test.ts apps/web/src/__fork_guards__/sidebarV2ProjectGrouping.test.ts apps/web/src/__fork_guards__/sidebarV2RowActionHitArea.test.ts apps/web/src/__fork_guards__/phosphorIcons.test.ts --dir apps/web

Model: Cursor Grok 4.5 · Cursor

Split Search, New thread, and Add project into labeled rows, put project
scope behind a filter menu, drop the dark blue primary/ring for white, and
align the shared topbar to 52px.

Co-authored-by: Cursor <cursoragent@cursor.com>
@cursor

cursor Bot commented Aug 2, 2026

Copy link
Copy Markdown

Bugbot is not enabled for your account, so this pull request was not reviewed.

Enable Bugbot in the Cursor dashboard to get automatic reviews on future PRs.

@github-actions github-actions Bot added vouch:trusted PR author is trusted by repo permissions or the VOUCHED list. size:L labels Aug 2, 2026

@cursor cursor 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.

Thermo-nuclear code quality

The call-site cleanup is real: Add project moves into the action stack, ProjectScopeRow drops scopedProjectGroup / onAddProject, and the filter affordance is clearer. Theme + topbar changes are small and fenced.

What's blocking is the shape of the new chrome module:

  1. Code judo missed in SidebarV2ChromeRowsNewThreadRow and AddProjectRow are copy-paste. Collapse to one private labeled-action helper; leave Search as the special case.
  2. Guards freeze the wrong API — three row function names are string-locked even though only ChromeActionRows + ProjectScopeRow are imported. Guard composition / labels / testids, not internal exports that should disappear.
  3. Projects label re-spells CHROME_CONTROL typography — pull the 14px type into one token so the static header can't drift from the action rows.

No 1k-line regression here (SidebarV2 was already huge; this PR shrinks the call site). Fix the duplication and the guard surface before this is maintainable enough to land.

Open in Web View Automation 

Sent by Cursor Automation: Thermo-nuclear PR review

Comment on lines +159 to +202
export function SidebarV2NewThreadRow(props: {
readonly newThreadShortcutLabel: string | null;
readonly newThreadDisabled: boolean;
readonly onNewThread: () => void;
}) {
return (
<div className={CONTROL_ROW}>
<SidebarMenuButton
size="sm"
type="button"
className={cn("min-w-0 flex-1", CHROME_CONTROL, CHROME_ROW_ICON_TINT)}
onClick={props.onNewThread}
disabled={props.newThreadDisabled}
aria-label={
props.newThreadShortcutLabel
? `New thread (${props.newThreadShortcutLabel})`
: "New thread"
}
data-testid="sidebar-v2-new-thread"
>
<PlusCircleIcon className="size-4 shrink-0 text-sidebar-muted-foreground/80" />
<span className="min-w-0 flex-1 truncate text-left">New thread</span>
</SidebarMenuButton>
</div>
);
}

export function SidebarV2AddProjectRow(props: { readonly onAddProject: () => void }) {
return (
<div className={CONTROL_ROW}>
<SidebarMenuButton
size="sm"
type="button"
className={cn("min-w-0 flex-1", CHROME_CONTROL, CHROME_ROW_ICON_TINT)}
onClick={props.onAddProject}
aria-label="Add project"
data-testid="sidebar-v2-add-project"
>
<FolderPlusIcon className="size-4 shrink-0 text-sidebar-muted-foreground/80" />
<span className="min-w-0 flex-1 truncate text-left">Add project</span>
</SidebarMenuButton>
</div>
);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

i think there's a code-judo move here that makes this much simpler.

SidebarV2NewThreadRow and SidebarV2AddProjectRow are the same CONTROL_ROW + SidebarMenuButton + icon + label block with different props. Search earns its own shape (CommandDialogTrigger + Kbd); these two do not.

Collapse them into one private labeled-action helper (icon, label, aria-label, testid, onClick, disabled?) and call it twice from SidebarV2ChromeActionRows. Keep Search special. Don't mint two near-identical exported components for a presentation-only fork file.

Comment on lines +160 to +162
expect(rows).toContain("function SidebarV2SearchRow");
expect(rows).toContain("function SidebarV2NewThreadRow");
expect(rows).toContain("function SidebarV2AddProjectRow");

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

this abstraction seems unnecessary — and the guard is cementing it.

Nothing outside this module imports SidebarV2SearchRow / NewThreadRow / AddProjectRow. The public seam is already SidebarV2ChromeActionRows + SidebarV2ProjectScopeRow. Locking the three internal function names as strings fights collapsing New thread / Add project into a helper.

Guard the composition instead: ActionRows call site, visible labels / testids, and the Projects filter affordance. Don't freeze an export surface nobody consumes.

// pt-2 is the 8px gap between the action block and this header.
<SidebarGroup className="px-3 pt-2 pb-2">
<div className={CONTROL_ROW}>
<span className="min-w-0 flex-1 truncate text-left text-[0.875rem] leading-4 font-normal text-sidebar-muted-foreground">

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

this looks like a bespoke spelling of something CHROME_CONTROL already owns.

text-[0.875rem] leading-4 font-normal text-sidebar-muted-foreground is re-declared on the static Projects label while action rows get the same type via CHROME_CONTROL. Extract a shared type token (or reuse the control's type classes without the interactive chrome) so a 14px retune can't drift the header off the action rows.

Copy link
Copy Markdown
Owner Author

Review

Static review only — this environment has no node_modules, so I could not execute the guard suite. Everything below is read from source, with file:line for each claim.

The 52px half of this PR is right, and I'll defend it below. The sidebar restructure is mostly good. Two things I'd hold the merge for: a filter that no longer shows it's on, and a global token change riding in a sidebar PR.


1. Blocking — an active project scope is now invisible

SidebarV2ChromeRows.tsx:223-226 replaces the scope button's label with a static Projects, and the manifest makes it explicit: "The filter glyph does not change when a project scope is active."

So once a user picks a project:

  • the thread list silently drops everything else,
  • the chrome says Projects, same as it always did,
  • the funnel glyph is unchanged,
  • the accessible name is a fixed "Filter threads by project" (:234), so screen readers lose the cue too — previously the trigger's text was the scope name,
  • and group-by is silently disabled as a side effect (SidebarV2.tsx:2942), with the reason only readable inside the menu the user has to open first.

The single surviving cue is No threads in {name} yet (SidebarV2.tsx:3238-3240) — which by construction only appears when the scoped project has zero threads. Scope to a project that has threads and there is no indicator anywhere in the app.

That's the hidden-mode bug CLAUDE.md's "a lying spinner, and a stale label" and "if you added a way in, add the way out and the way to see it" rules are aimed at. What softens it: scope is useState<string | null>(null) (SidebarV2.tsx:1548), not persisted, so a reload clears it and the trap is session-bounded. It's still wrong within the session.

Cheapest fixes, any one of which closes it:

  • funnel goes filled/text-sidebar-foreground when projectScopeKey !== null;
  • label reads the scoped project name and falls back to Projects;
  • at minimum, fold the scope into the trigger's aria-label so AT is not also blind.

I'd take the first — it's a glyph state, no layout change, and it's the thing the eye is already on.

2. High — a global palette change is shipped inside a sidebar-chrome PR

theme.custom.css:111-113 sets --primary, --primary-foreground, --ring for the whole fork dark theme. The PR title, summary, and test plan all read as sidebar scope. The actual blast radius:

--ring — every form control in the app. input.tsx:61, textarea.tsx:20, select.tsx:24, combobox.tsx:333, input-group.tsx:11 all use focus-visible:border-ring + ring-ring/24; KeybindingsSettings.tsx:164,840,879 too. Plus the fork's own --fork-composer-border-focus: var(--ring) (theme.custom.css:316) and the drag-over outline: 2px solid var(--ring) (:388). The test plan checks exactly one refs input.

--primarybutton.tsx:34 (default variant), badge.tsx:24, checkbox.tsx:18, radio-group.tsx:29, switch.tsx:11, menu.tsx:116, RelayClientInstallDialog.tsx:89 (progress), ComposerStashBadge.tsx:49, ModelPickerSidebar.tsx:29, PreviewChromeRow.tsx:313, AgentBrowserCursor.tsx:69, plus every selection state in GitActionsControl.tsx:570,582,673,759,834, ConnectOnboardingDialog.tsx:312,323, ProjectScriptsControl.tsx:522, ConnectionsSettings.tsx:1236,1259,2378,2390.

Two consequences I'd want screenshots for before this lands:

(a) text-primary collapses into text-foreground. Fork dark foreground is ~#f1f3f7 (#e9eaec under Cool Dark, theme.custom.css:661); --primary is now #ffffff. Those are ~1.05:1 apart — indistinguishable. Every place that uses text-primary to mean "active / needs your attention" stops meaning it: SidebarUpdatePill.tsx:156, SidebarProviderUpdatePill.tsx:16, ConnectionsSettings.tsx:1259 and :2390, AddProviderInstanceWizardSteps.tsx:53, PlanSidebar.tsx:46. Update pills in particular exist to be noticed. Selected-state fills go the same way — hue-coded becomes lightness-coded, and --accent: #262626 hover now sits close to the selected fills in GitActionsControl.

(b) button.tsx:34 default variant carries shadow-primary/24 and not-disabled:inset-shadow-[0_1px_white/16%]. On a white fill the inset highlight is invisible and the drop shadow becomes a white halo. Worth one screenshot of a primary Button, Badge, Checkbox, Switch and a focused Input in fork dark.

Per CLAUDE.md's one-concern rule this is arguably its own PR. If it stays here, the body and test plan need to say "the fork's dark accent goes monochrome app-wide," not "switches and focus underlines."

Also: light mode keeps upstream's blue, which the manifest already declares intentional (Light mode is untouched) — flagging only because toggling themes now changes accent hue, not just lightness. Confirm that's the stance.

3. Medium

  • The funnel lost its tooltip. Tooltip is dropped from the imports entirely, so the one entry point to both project scope and group-by is a bare 24px glyph with no hover affordance. The control it replaced had one. Its aria-label also under-describes what's behind it — the menu owns group-by too.
  • New thread's shortcut is no longer visible. Search renders a Kbd (:~148); New thread buries the shortcut in aria-label (:~172). Same three-row block, two different answers, and the Kbd slot is right there.
  • Disabled with no reason. newThreadDisabled renders a dead row with no explanation — in a file that goes out of its way to explain the group-by switch's disabled state via both title and aria-label (:246-253). Hold it to its own bar.
  • +68px of permanent chrome. Before: 16+28+4 + 8+28+16 = 100px. After: 16 + (32×3 + 4×2) + 8+32+8 = 168px. That's the thread list losing ~8% of its height on an 800px window, permanently, and it lands hardest in the mobile drawer. Defensible as a design call, but make it an explicit trade — a before/after at a short viewport would settle it.
  • Projects is a bare <span> (:223), not SidebarGroupLabel (sidebar.tsx:723) and not a heading. A row that used to be a named control is now unlabeled structure for AT.
  • Typography duplication. The label re-spells text-[0.875rem] leading-4 font-normal text-sidebar-muted-foreground inline instead of deriving from CHROME_CONTROL, and the 14px guard only inspects CHROME_CONTROL — so this one can silently drift to the panel's 13px remap. The fork-sidebar-type-size manifest entry lists Search / New thread / Add project as the opt-outs and omits Projects; it's now incomplete.

4. Low

  • Three new data-testids (sidebar-v2-new-thread, sidebar-v2-add-project, sidebar-v2-project-filter) are referenced by nothing in the repo. Use them in the guards or drop them.
  • SidebarV2SearchRow / NewThreadRow / AddProjectRow are exported but consumed only inside their own module; the guards match function X, so export buys nothing.
  • ListFilterIcon is inserted mid-Folder* cluster (lucide-phosphor.tsx:287) — it belongs with its own neighbors.
  • With zero projects the empty state renders a second "Add project" CTA (SidebarV2.tsx:3228-3237) directly under the new always-on row.
  • Untested behavior change worth pinning: Add project is now reachable at projectGroups.length === 0, because it moved out of the scope row that returns null there (:216). That's a real improvement and nothing guards it.

Verified — these hold up

Checked rather than assumed:

  • 52px is the correct direction. settings.tsx:95, DiffPanelShell.tsx:15 and toast.tsx:562 (--toast-header-offset:52px) were already 52 — under 56 the toast offset was quietly 4px wrong. This removes drift rather than creating it. macOS traffic lights (DesktopWindow.ts:191, y:18, ~12px tall) still clear, and center better at 52 than at 56. Windows WCO reads env(titlebar-area-height) against TITLEBAR_HEIGHT = 40 (:22), so the fallback edit is inert there — keeping the second guard assertion is still right.
  • The trailing axis survives the restructure. The Projects header's px-3 + flush 24px button lands on the same 24px axis the action rows used to define, and SIDEBAR_V2_TRAILING_OFFSET.chromeRow is "", so nothing double-counts.
  • No panel-level shadowing of the new tokens. --primary/--ring are declared only at index.css:876/887 and :919/930; nothing re-declares them on [data-sidebar-version="v2"], so :root[data-fork].dark wins. The fork's own "a property set only at :root never reaches an element that declares its own" trap does not fire here.
  • Cool Dark (theme.custom.css:658+) declares neither token, so it inherits white exactly as the manifest claims.
  • FunnelSimple exists in @phosphor-icons/react ^2.1.10, and the shim guard's "exports every lucide binding the app imports" is satisfied by the new ListFilterIcon export.

Summary: fix (1) before merge — a filter with no on-state is a bug, not a style. Decide on (2): either split it out or re-scope the PR description and test plan to match what it actually changes. The rest is polish.


Generated by Claude Code

Show when a project scope is active on the funnel, collapse New thread /
Add project into one labeled-action helper, share CHROME_TYPE with the
Projects label, and restore the filter tooltip.

Co-authored-by: Cursor <cursoragent@cursor.com>
@NoahHendrickson

Copy link
Copy Markdown
Owner Author

Addressed review feedback on cc315b210:

  1. Active scope visible — funnel lifts to foreground when scoped; aria-label + tooltip name the project.
  2. Monochrome primary/ring — kept here; PR body/test plan now state the app-wide dark blast radius (light stays blue).
  3. Thermo-nuclear / polishChromeLabeledAction helper, shared CHROME_TYPE, guards pin composition/testids (not internal exports), filter tooltip restored, New thread shows shortcut Kbd, disabled New thread explains why, Projects is role="heading".

@NoahHendrickson
NoahHendrickson merged commit d095fd8 into custom Aug 2, 2026
10 checks passed
@NoahHendrickson
NoahHendrickson deleted the fork/sidebar-chrome-actions branch August 2, 2026 13:25
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:L vouch:trusted PR author is trusted by repo permissions or the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant