authorgravatar for maria@kuuro.netmaria <maria@kuuro.net> 2026-09-10 02:18:01-03:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-09-10 02:18:01-03:00
logbb5e824c9fcbd76c93ef15b304f89f8b6999f32c
tree672331fb95c59876b4f890982b4f41f4d397c0b4
parent0f602b3372b300ae94084bd3fe7dbaadaa58ba3a
signature Signed by PGP key B5690EEEBB952194

feat: add blue and orange diff color palette (#10671)


11 files changed, 133 insertions(+), 24 deletions(-)

apps/web/src/components/GitActionsControl.tsx+4-4
......@@ -1878,9 +1878,9 @@ export default function GitActionsControl({
18781878 <span className="text-muted-foreground">Excluded</span>
18791879 ) : (
18801880 <>
1881 <span className="text-success">+{file.insertions}</span>
1881 <span className="text-diff-addition">+{file.insertions}</span>
18821882 <span className="text-muted-foreground"> / </span>
1883 <span className="text-destructive">-{file.deletions}</span>
1883 <span className="text-diff-deletion">-{file.deletions}</span>
18841884 </>
18851885 )}
18861886 </span>
......@@ -1891,11 +1891,11 @@ export default function GitActionsControl({
18911891 </div>
18921892 </ScrollArea>
18931893 <div className="flex justify-end font-mono">
1894 <span className="text-success">
1894 <span className="text-diff-addition">
18951895 +{selectedFiles.reduce((sum, f) => sum + f.insertions, 0)}
18961896 </span>
18971897 <span className="text-muted-foreground"> / </span>
1898 <span className="text-destructive">
1898 <span className="text-diff-deletion">
18991899 -{selectedFiles.reduce((sum, f) => sum + f.deletions, 0)}
19001900 </span>
19011901 </div>
apps/web/src/components/Sidebar.tsx+2-2
......@@ -1932,8 +1932,8 @@ const SidebarThreadRow = memo(function SidebarThreadRow(props: {
19321932 ) : null}
19331933 {diff ? (
19341934 <span className="shrink-0 font-mono">
1935 <span className="text-emerald-600 dark:text-emerald-400">+{diff.insertions}</span>{" "}
1936 <span className="text-red-600 dark:text-red-400">−{diff.deletions}</span>
1935 <span className="text-diff-addition-foreground">+{diff.insertions}</span>{" "}
1936 <span className="text-diff-deletion-foreground">−{diff.deletions}</span>
19371937 </span>
19381938 ) : null}
19391939 <span
apps/web/src/components/chat/DiffStatLabel.tsx+2-2
......@@ -40,10 +40,10 @@ export const DiffStatLabel = memo(function DiffStatLabel(props: {
4040 className,
4141 )}
4242 >
43 <span aria-hidden="true" className="font-mono text-success">
43 <span aria-hidden="true" className="font-mono text-diff-addition">
4444 +{formatCompactDiffCount(additions)}
4545 </span>
46 <span aria-hidden="true" className="font-mono text-destructive">
46 <span aria-hidden="true" className="font-mono text-diff-deletion">
4747 -{formatCompactDiffCount(deletions)}
4848 </span>
4949 </span>
apps/web/src/components/pullRequest/pullRequestPresentation.tsx+2-4
......@@ -422,10 +422,8 @@ export function PullRequestDiffStat({
422422 }
423423 return (
424424 <span className={cn("inline-flex items-baseline gap-1 tabular-nums", className)}>
425 <span className="text-emerald-600 dark:text-emerald-300/90">
426 +{additions.toLocaleString()}
427 </span>
428 <span className="text-destructive">-{deletions.toLocaleString()}</span>
425 <span className="text-diff-addition-foreground">+{additions.toLocaleString()}</span>
426 <span className="text-diff-deletion">-{deletions.toLocaleString()}</span>
429427 </span>
430428 );
431429}
apps/web/src/components/settings/SettingsPanels.tsx+51
......@@ -506,6 +506,9 @@ export function useSettingsRestore(onRestored?: () => void) {
506506 ? ["Contrast"]
507507 : []),
508508 ...(settings.glassOpacity !== DEFAULT_UNIFIED_SETTINGS.glassOpacity ? ["Glass opacity"] : []),
509 ...(settings.diffColorScheme !== DEFAULT_UNIFIED_SETTINGS.diffColorScheme
510 ? ["Diff colors"]
511 : []),
509512 ...(settings.panelAnimationDurationMs !== DEFAULT_UNIFIED_SETTINGS.panelAnimationDurationMs
510513 ? ["Panel animations"]
511514 : []),
......@@ -597,6 +600,7 @@ export function useSettingsRestore(onRestored?: () => void) {
597600 settings.browserLinkTarget,
598601 settings.browserAutoShowFloatingPreview,
599602 settings.appearanceContrast,
603 settings.diffColorScheme,
600604 settings.enableAgentBrowserAccess,
601605 settings.confirmQuit,
602606 settings.confirmThreadArchive,
......@@ -701,6 +705,7 @@ export function useSettingsRestore(onRestored?: () => void) {
701705 }
702706 updateSettings({
703707 appearanceContrast: DEFAULT_UNIFIED_SETTINGS.appearanceContrast,
708 diffColorScheme: DEFAULT_UNIFIED_SETTINGS.diffColorScheme,
704709 timestampFormat: DEFAULT_UNIFIED_SETTINGS.timestampFormat,
705710 wordWrap: DEFAULT_UNIFIED_SETTINGS.wordWrap,
706711 diffIgnoreWhitespace: DEFAULT_UNIFIED_SETTINGS.diffIgnoreWhitespace,
......@@ -1240,6 +1245,52 @@ export function AppearanceSettingsPanel() {
12401245 }
12411246 />
12421247 ) : null}
1248 <SettingsRow
1249 {...searchableSetting("diff-color-scheme")}
1250 description="Choose colors for additions and deletions, including change counts."
1251 resetAction={
1252 settings.diffColorScheme !== DEFAULT_UNIFIED_SETTINGS.diffColorScheme ? (
1253 <SettingResetButton
1254 label="diff colors"
1255 onClick={() =>
1256 updateSettings({ diffColorScheme: DEFAULT_UNIFIED_SETTINGS.diffColorScheme })
1257 }
1258 />
1259 ) : null
1260 }
1261 control={
1262 <div className="w-full sm:w-40">
1263 <Select
1264 value={settings.diffColorScheme}
1265 onValueChange={(value) => {
1266 if (value === "red-green" || value === "blue-orange")
1267 updateSettings({ diffColorScheme: value });
1268 }}
1269 >
1270 <SelectTrigger size="sm" className="w-full min-w-0" aria-label="Diff colors">
1271 <span
1272 aria-hidden="true"
1273 className={
1274 settings.diffColorScheme === "blue-orange"
1275 ? "flex shrink-0 flex-row-reverse gap-1"
1276 : "flex shrink-0 gap-1"
1277 }
1278 >
1279 <span className="size-2 rounded-full bg-[var(--diff-deletion)]" />
1280 <span className="size-2 rounded-full bg-[var(--diff-addition)]" />
1281 </span>
1282 <SelectValue>
1283 {settings.diffColorScheme === "blue-orange" ? "Blue & orange" : "Red & green"}
1284 </SelectValue>
1285 </SelectTrigger>
1286 <SelectPopup align="end" alignItemWithTrigger={false}>
1287 <SelectItem value="red-green">Red & green (default)</SelectItem>
1288 <SelectItem value="blue-orange">Blue & orange</SelectItem>
1289 </SelectPopup>
1290 </Select>
1291 </div>
1292 }
1293 />
12431294 </SettingsSection>
12441295
12451296 <SettingsSection id="motion" title="Motion">
apps/web/src/components/settings/settingsSearch.ts+6
......@@ -106,6 +106,12 @@ export const SETTINGS_SEARCH_ITEMS = [
106106 to: "/settings/appearance",
107107 searchTerms: ["transparent transparency solid menus dialogs composer"],
108108 },
109 {
110 id: "diff-color-scheme",
111 title: "Diff colors",
112 to: "/settings/appearance",
113 searchTerms: ["red green blue orange additions deletions changes counts palette colorblind"],
114 },
109115 {
110116 id: "panel-animations",
111117 title: "Panel animations",
apps/web/src/index.css+26
......@@ -162,6 +162,10 @@ html[data-mobile-composer-route-transition="true"]::view-transition-old(t3-mobil
162162 --color-update-surface: var(--update-surface);
163163 --color-success-foreground: var(--success-foreground);
164164 --color-success: var(--success);
165 --color-diff-addition: var(--diff-addition);
166 --color-diff-deletion: var(--diff-deletion);
167 --color-diff-addition-foreground: var(--diff-addition-foreground);
168 --color-diff-deletion-foreground: var(--diff-deletion-foreground);
165169 --color-info-foreground: var(--info-foreground);
166170 --color-info: var(--info);
167171 --color-destructive-foreground: var(--destructive-foreground);
......@@ -1955,6 +1959,28 @@ code {
19551959 }
19561960}
19571961
1962:root {
1963 --diff-addition: var(--success);
1964 --diff-deletion: var(--destructive);
1965 --diff-addition-foreground: light-dark(var(--color-emerald-600), var(--color-emerald-400));
1966 --diff-deletion-foreground: light-dark(var(--color-red-600), var(--color-red-400));
1967}
1968
1969:root[data-diff-color-scheme="blue-orange"] {
1970 --diff-addition: light-dark(var(--color-blue-600), var(--color-blue-400));
1971 --diff-deletion: light-dark(var(--color-orange-600), var(--color-orange-400));
1972 --diff-addition-foreground: var(--diff-addition);
1973 --diff-deletion-foreground: var(--diff-deletion);
1974 --diffs-addition-color-override: var(--diff-addition);
1975 --diffs-deletion-color-override: var(--diff-deletion);
1976 --trees-status-added-override: var(--diff-addition);
1977 --trees-status-deleted-override: var(--diff-deletion);
1978 --trees-status-untracked-override: var(--diff-addition);
1979 --trees-git-added-color-override: var(--diff-addition);
1980 --trees-git-deleted-color-override: var(--diff-deletion);
1981 --trees-git-untracked-color-override: var(--diff-addition);
1982}
1983
19581984.diff-render-surface diffs-container {
19591985 border: 0;
19601986 border-radius: 0;
apps/web/src/lib/diffRendering.ts+12-12
......@@ -282,37 +282,37 @@ export const DIFF_SURFACE_THEME_UNSAFE_CSS = `
282282 --diffs-bg-buffer-override: color-mix(in srgb, var(--code-background) 90%, var(--code-foreground));
283283
284284 --diffs-bg-addition-override: light-dark(
285 color-mix(in srgb, var(--code-background) 50%, var(--success)),
286 color-mix(in srgb, var(--code-background) 70%, var(--success))
285 color-mix(in srgb, var(--code-background) 50%, var(--diff-addition)),
286 color-mix(in srgb, var(--code-background) 70%, var(--diff-addition))
287287 );
288288 --diffs-bg-addition-number-override: light-dark(
289 color-mix(in srgb, var(--code-background) 35%, var(--success)),
290 color-mix(in srgb, var(--code-background) 60%, var(--success))
289 color-mix(in srgb, var(--code-background) 35%, var(--diff-addition)),
290 color-mix(in srgb, var(--code-background) 60%, var(--diff-addition))
291291 );
292 --diffs-bg-addition-hover-override: color-mix(in srgb, var(--code-background) 85%, var(--success));
292 --diffs-bg-addition-hover-override: color-mix(in srgb, var(--code-background) 85%, var(--diff-addition));
293293 --diffs-bg-addition-emphasis-override: color-mix(
294294 in srgb,
295295 var(--code-background) 80%,
296 var(--success)
296 var(--diff-addition)
297297 );
298298
299299 --diffs-bg-deletion-override: light-dark(
300 color-mix(in srgb, var(--code-background) 50%, var(--destructive)),
301 color-mix(in srgb, var(--code-background) 70%, var(--destructive))
300 color-mix(in srgb, var(--code-background) 50%, var(--diff-deletion)),
301 color-mix(in srgb, var(--code-background) 70%, var(--diff-deletion))
302302 );
303303 --diffs-bg-deletion-number-override: light-dark(
304 color-mix(in srgb, var(--code-background) 35%, var(--destructive)),
305 color-mix(in srgb, var(--code-background) 60%, var(--destructive))
304 color-mix(in srgb, var(--code-background) 35%, var(--diff-deletion)),
305 color-mix(in srgb, var(--code-background) 60%, var(--diff-deletion))
306306 );
307307 --diffs-bg-deletion-hover-override: color-mix(
308308 in srgb,
309309 var(--code-background) 85%,
310 var(--destructive)
310 var(--diff-deletion)
311311 );
312312 --diffs-bg-deletion-emphasis-override: color-mix(
313313 in srgb,
314314 var(--code-background) 80%,
315 var(--destructive)
315 var(--diff-deletion)
316316 );
317317
318318 background-color: var(--diffs-bg) !important;
apps/web/src/routes/__root.tsx+5
......@@ -226,6 +226,11 @@ function EnvironmentThemeSync() {
226226
227227function ContrastAppearanceSync() {
228228 const appearanceContrast = useClientSettings((settings) => settings.appearanceContrast);
229 const diffColorScheme = useClientSettings((settings) => settings.diffColorScheme);
230
231 useEffect(() => {
232 document.documentElement.dataset.diffColorScheme = diffColorScheme;
233 }, [diffColorScheme]);
229234
230235 useEffect(() => {
231236 applyAppearanceContrast(document.documentElement, appearanceContrast);
packages/contracts/src/settings.test.ts+17
......@@ -135,6 +135,23 @@ describe("ClaudeSettings auto-compaction", () => {
135135 });
136136});
137137
138describe("ClientSettings diff colors", () => {
139 it("keeps red and green for existing settings without a saved palette", () => {
140 expect(decodeClientSettings({}).diffColorScheme).toBe("red-green");
141 });
142
143 it.each(["red-green", "blue-orange"])("round-trips the %s palette", (diffColorScheme) => {
144 const settings = decodeClientSettings({ diffColorScheme });
145 expect(encodeClientSettings(settings).diffColorScheme).toBe(diffColorScheme);
146 expect(decodeClientSettingsPatch({ diffColorScheme }).diffColorScheme).toBe(diffColorScheme);
147 });
148
149 it("rejects unsupported palettes", () => {
150 expect(() => decodeClientSettings({ diffColorScheme: "purple-yellow" })).toThrow();
151 expect(() => decodeClientSettingsPatch({ diffColorScheme: "purple-yellow" })).toThrow();
152 });
153});
154
138155describe("ClientSettings load balancing", () => {
139156 it("requires opt-in when settings are new or omit load balancing", () => {
140157 expect(decodeClientSettings({}).loadBalancingEnabled).toBe(false);
packages/contracts/src/settings.ts+6
......@@ -271,7 +271,12 @@ export const LoadBalancingWeights = Schema.Record(
271271 Schema.Int.check(Schema.isBetween({ minimum: 0, maximum: 100 })),
272272);
273273
274export const DiffColorScheme = Schema.Literals(["red-green", "blue-orange"]);
275
274276export const ClientSettingsSchema = Schema.Struct({
277 diffColorScheme: DiffColorScheme.pipe(
278 Schema.withDecodingDefault(Effect.succeed("red-green" as const)),
279 ),
275280 loadBalancingEnabled: Schema.Boolean.pipe(Schema.withDecodingDefault(Effect.succeed(false))),
276281 loadBalancingWeights: LoadBalancingWeights.pipe(Schema.withDecodingDefault(Effect.succeed({}))),
277282 appearanceContrast: AppearanceContrast.pipe(
......@@ -1295,6 +1300,7 @@ export const ServerSettingsPatch = Schema.Struct({
12951300export type ServerSettingsPatch = typeof ServerSettingsPatch.Type;
12961301
12971302export const ClientSettingsPatch = Schema.Struct({
1303 diffColorScheme: Schema.optionalKey(DiffColorScheme),
12981304 loadBalancingEnabled: Schema.optionalKey(Schema.Boolean),
12991305 loadBalancingWeights: Schema.optionalKey(LoadBalancingWeights),
13001306 appearanceContrast: Schema.optionalKey(AppearanceContrast),