Skip to content

feat(web): give each project its own colour - #5085

Open
Brechard wants to merge 3 commits into
pingdotgg:mainfrom
Brechard:feat/project-colors
Open

feat(web): give each project its own colour#5085
Brechard wants to merge 3 commits into
pingdotgg:mainfrom
Brechard:feat/project-colors

Conversation

@Brechard

@Brechard Brechard commented Jul 31, 2026

Copy link
Copy Markdown

Closes #1376

What Changed

Each project gets an accent colour, shown on its name in Sidebar V2 rows and pickable from the existing project settings dialog: ten swatches, any hue behind Custom, Auto to drop back to the derived default.

Why

Projects are told apart in the sidebar by favicon and name. Neither scales — icons don't resolve for every repo (#4304, #1020), and a list of similarly-named projects reads as one wall of text. #1376 asks for this directly, comparing it to colouring terminal tabs, and a second person there offered to build it.

How this differs from #2381

#2381 built this feature and was closed as stale. The design here is deliberately smaller, so this is not that PR rebased:

#2381 this
Opt-in New sidebarProjectColors setting to turn it on None — colours are just on
Default 10-entry palette with adjacent-collision avoidance, recomputed as projects change Derived from the project key, computed on read
Stored sidebarProjectColorOverrides + the enable flag sidebarProjectColorOverrides only
Custom colours Palette only Any hue
Surface New colour dot in the sidebar group header + swatch popover Existing project settings dialog

The collision-avoidance is the part I most wanted to avoid inheriting: it makes a project's colour depend on which other projects exist, so adding one can restyle its neighbours, and the assignment has to be recomputed and kept somewhere. Deriving each colour from its own key independently gives up guaranteed adjacent-distinctness and gets back statelessness — no write on project create, no repair path, no reshuffling. With ten hues a couple of neighbours will occasionally match; that's what the picker is for.

Dropping the on/off setting is the other deliberate difference. A colour that has to be enabled is a colour most people never see, and the derived default means there is no unconfigured state worth hiding.

Design notes

A hue, not a colour. sidebarProjectColorOverrides stores an OKLCH hue angle (0–359), not hex. Lightness and chroma are fixed per theme at the render site (oklch(0.52 0.12 h) light, oklch(0.78 0.11 h) dark), which buys two things a hex triple can't: every project reads at the same visual weight, so no project's label shouts over another's or over the thread title; and one stored value stays legible in both themes, so there's no second value to keep in sync and no way to pick "dark red on a dark sidebar". The hue travels to CSS as a custom property so the theme pair can live in the class list.

Keyed by workspace path. Overrides use derivePhysicalProjectKey — the same identity sidebarProjectGroupingOverrides already uses. Renaming a project keeps its colour, and the same checkout added on two machines lands on the same colour.

Additive schema. A new optional record with a decoding default of {}. Existing settings blobs decode unchanged; no migration.

Absent means auto. Clearing an override deletes the key rather than writing a sentinel, so "never touched" and "reset to auto" are one state that can't drift apart.

Verification

  • tsc --noEmit clean; vp lint and vp format --check clean.
  • apps/web projectColor.test.ts — 13 new tests: palette size/uniqueness, minimum hue separation between neighbouring swatches, default stability and spread across sequential keys, override precedence, custom hues outside the palette, and hue 0 surviving as a real override rather than being swallowed as falsy.
  • packages/contracts settings.test.ts — 31 pass unchanged.

Scope

Sidebar V2 rows and the project settings dialog only. Not touched: Sidebar V1, mobile, the project filter menu, and the favicon fallback glyph — each is a reasonable next surface, but they're separable and this is easier to judge on its own. Happy to extend or trim to taste.


Note

Low Risk
UI and additive client-settings only; no auth or server paths. Wrong override keys would only mis-colour labels.

Overview
Adds per-project accent colours in Sidebar V2: project names in thread rows tint via OKLCH (--project-hue), and the project settings dialog includes a Colour row with ten swatches, a Custom hue strip (pointer preview on drag, commit on release), and Auto to clear overrides.

Overrides live in new client setting sidebarProjectColorOverrides, keyed by the same physical project key as grouping overrides. With no override, hue is derived on read from an FNV-1a hash of that key into a ten-entry palette—no enable flag and no collision-avoidance across neighbours.

Contracts add ProjectColorHue (0–359) and the optional override record with default {}. Tests cover palette/resolveProjectHue and HueStrip pointer behaviour.

Reviewed by Cursor Bugbot for commit 422d3b5. Bugbot is set up for automated code reviews on this repo. Configure here.

Note

Add per-project color overrides to the sidebar

  • Adds a ProjectColorPicker component with a palette of 10 hues and a custom hue strip (HueStrip) supporting drag, keyboard, and numeric input.
  • Introduces projectColor.ts with defaultProjectColor (FNV-1a hash → palette entry), resolveProjectHue (override or default), and CSS utility classes using OKLCH via a --project-hue custom property.
  • Extends ClientSettingsSchema and ClientSettingsPatch in settings.ts with sidebarProjectColorOverrides, a Record<string, ProjectColorHue> persisted in client settings.
  • Wires the picker into the sidebar project actions dialog, applies resolved hue to project title text, and supports an "Auto" reset that clears the override.
📊 Macroscope summarized 422d3b5. 1 file reviewed, 0 issues evaluated, 0 issues filtered, 0 comments posted

🗂️ Filtered Issues

No issues evaluated.

Projects are told apart in the sidebar by favicon and name alone. Neither
scales: icons do not resolve for every repo, and a list of similarly named
projects reads as one wall of text.

Each project now carries an accent colour, applied to its name on sidebar
rows and pickable in project settings — ten swatches, any hue behind
Custom, Auto to go back to the derived default.

The colour is a stored OKLCH hue angle rather than a finished colour, so
lightness and chroma can be pinned per theme at the render site: every
project reads at the same weight and one stored value stays legible in both
themes. The default is derived from the project's workspace key, so a
project has a stable colour before anyone configures anything, and the same
checkout lands on the same colour on every machine.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jul 31, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 03b4cb90-0547-4fdd-81f0-e2c0f237158c

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

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.

@github-actions github-actions Bot added vouch:unvouched PR author is not yet trusted in the VOUCHED list. size:L 100-499 changed lines (additions + deletions). labels Jul 31, 2026
Comment thread apps/web/src/components/ProjectColorPicker.tsx
@macroscopeapp

macroscopeapp Bot commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

Approvability

Verdict: Needs human review

1 blocking correctness issue found. This PR introduces a new feature (per-project color customization) with substantial new UI components and state management. Additionally, there are two unresolved medium-severity comments identifying potential bugs in the drag interaction logic that should be addressed.

You can customize Macroscope's approvability policy. Learn more.

# Conflicts:
#	apps/web/src/components/SidebarV2.tsx
Comment thread apps/web/src/components/ProjectColorPicker.tsx
Comment on lines +120 to +126
onPointerUp={(event) => {
if (event.currentTarget.hasPointerCapture(event.pointerId)) {
const nextHue = hueFromEvent(event);
if (nextHue !== null) onCommit(nextHue);
event.currentTarget.releasePointerCapture(event.pointerId);
}
}}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 Medium components/ProjectColorPicker.tsx:120

If the browser cancels an active pointer drag on the hue strip (e.g. app switch, hardware interruption), pointercancel fires but HueStrip has no handler for it, so onCommit is never called. The preview onChange calls during the drag already mutated ProjectColorPicker's draftHue, but props.hue never changes, so the useEffect that resyncs draftHue does not rerun. Closing and reopening the Custom popover then shows the uncommitted preview hue instead of the project's actual hue. Add an onPointerCancel handler that either commits the final preview via onCommit or restores draftHue to props.hue.

        onPointerUp={(event) => {
          if (event.currentTarget.hasPointerCapture(event.pointerId)) {
            const nextHue = hueFromEvent(event);
            if (nextHue !== null) onCommit(nextHue);
            event.currentTarget.releasePointerCapture(event.pointerId);
          }
        }}
+        onPointerCancel={(event) => {
+          if (event.currentTarget.hasPointerCapture(event.pointerId)) {
+            onChange(clampHue(props.hue));
+            event.currentTarget.releasePointerCapture(event.pointerId);
+          }
+        }}
🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/web/src/components/ProjectColorPicker.tsx around lines 120-126:

If the browser cancels an active pointer drag on the hue strip (e.g. app switch, hardware interruption), `pointercancel` fires but `HueStrip` has no handler for it, so `onCommit` is never called. The preview `onChange` calls during the drag already mutated `ProjectColorPicker`'s `draftHue`, but `props.hue` never changes, so the `useEffect` that resyncs `draftHue` does not rerun. Closing and reopening the Custom popover then shows the uncommitted preview hue instead of the project's actual hue. Add an `onPointerCancel` handler that either commits the final preview via `onCommit` or restores `draftHue` to `props.hue`.

@cursor cursor Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes using high effort and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Want fixes drafted automatically? Bugbot Autofix can create code changes for findings. A team admin can enable Autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 422d3b5. Configure here.

if (nextHue !== null) onCommit(nextHue);
event.currentTarget.releasePointerCapture(event.pointerId);
}
}}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Aborted drag leaves draft unsaved

Medium Severity

The HueStrip component lacks an onPointerCancel handler. If a drag is interrupted, the draftHue updates for preview but onCommit isn't called. This leaves draftHue with an unsaved value, which can reappear when the custom color picker is reopened, even though the project's color hasn't changed.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 422d3b5. Configure here.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:L 100-499 changed lines (additions + deletions). vouch:unvouched PR author is not yet trusted in the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Feature]: Set a color for Project

2 participants