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({...@@ -1878,9 +1878,9 @@ export default function GitActionsControl({
1878 <span className="text-muted-foreground">Excluded</span>1878 <span className="text-muted-foreground">Excluded</span>
1879 ) : (1879 ) : (
1880 <>1880 <>
1881 <span className="text-success">+{file.insertions}</span>1881 <span className="text-diff-addition">+{file.insertions}</span>
1882 <span className="text-muted-foreground"> / </span>1882 <span className="text-muted-foreground"> / </span>
1883 <span className="text-destructive">-{file.deletions}</span>1883 <span className="text-diff-deletion">-{file.deletions}</span>
1884 </>1884 </>
1885 )}1885 )}
1886 </span>1886 </span>
...@@ -1891,11 +1891,11 @@ export default function GitActionsControl({...@@ -1891,11 +1891,11 @@ export default function GitActionsControl({
1891 </div>1891 </div>
1892 </ScrollArea>1892 </ScrollArea>
1893 <div className="flex justify-end font-mono">1893 <div className="flex justify-end font-mono">
1894 <span className="text-success">1894 <span className="text-diff-addition">
1895 +{selectedFiles.reduce((sum, f) => sum + f.insertions, 0)}1895 +{selectedFiles.reduce((sum, f) => sum + f.insertions, 0)}
1896 </span>1896 </span>
1897 <span className="text-muted-foreground"> / </span>1897 <span className="text-muted-foreground"> / </span>
1898 <span className="text-destructive">1898 <span className="text-diff-deletion">
1899 -{selectedFiles.reduce((sum, f) => sum + f.deletions, 0)}1899 -{selectedFiles.reduce((sum, f) => sum + f.deletions, 0)}
1900 </span>1900 </span>
1901 </div>1901 </div>
apps/web/src/components/Sidebar.tsx+2-2
...@@ -1932,8 +1932,8 @@ const SidebarThreadRow = memo(function SidebarThreadRow(props: {...@@ -1932,8 +1932,8 @@ const SidebarThreadRow = memo(function SidebarThreadRow(props: {
1932 ) : null}1932 ) : null}
1933 {diff ? (1933 {diff ? (
1934 <span className="shrink-0 font-mono">1934 <span className="shrink-0 font-mono">
1935 <span className="text-emerald-600 dark:text-emerald-400">+{diff.insertions}</span>{" "}1935 <span className="text-diff-addition-foreground">+{diff.insertions}</span>{" "}
1936 <span className="text-red-600 dark:text-red-400">−{diff.deletions}</span>1936 <span className="text-diff-deletion-foreground">−{diff.deletions}</span>
1937 </span>1937 </span>
1938 ) : null}1938 ) : null}
1939 <span1939 <span
apps/web/src/components/chat/DiffStatLabel.tsx+2-2
...@@ -40,10 +40,10 @@ export const DiffStatLabel = memo(function DiffStatLabel(props: {...@@ -40,10 +40,10 @@ export const DiffStatLabel = memo(function DiffStatLabel(props: {
40 className,40 className,
41 )}41 )}
42 >42 >
43 <span aria-hidden="true" className="font-mono text-success">43 <span aria-hidden="true" className="font-mono text-diff-addition">
44 +{formatCompactDiffCount(additions)}44 +{formatCompactDiffCount(additions)}
45 </span>45 </span>
46 <span aria-hidden="true" className="font-mono text-destructive">46 <span aria-hidden="true" className="font-mono text-diff-deletion">
47 -{formatCompactDiffCount(deletions)}47 -{formatCompactDiffCount(deletions)}
48 </span>48 </span>
49 </span>49 </span>
apps/web/src/components/pullRequest/pullRequestPresentation.tsx+2-4
...@@ -422,10 +422,8 @@ export function PullRequestDiffStat({...@@ -422,10 +422,8 @@ export function PullRequestDiffStat({
422 }422 }
423 return (423 return (
424 <span className={cn("inline-flex items-baseline gap-1 tabular-nums", className)}>424 <span className={cn("inline-flex items-baseline gap-1 tabular-nums", className)}>
425 <span className="text-emerald-600 dark:text-emerald-300/90">425 <span className="text-diff-addition-foreground">+{additions.toLocaleString()}</span>
426 +{additions.toLocaleString()}426 <span className="text-diff-deletion">-{deletions.toLocaleString()}</span>
427 </span>
428 <span className="text-destructive">-{deletions.toLocaleString()}</span>
429 </span>427 </span>
430 );428 );
431}429}
apps/web/src/components/settings/SettingsPanels.tsx+51
...@@ -506,6 +506,9 @@ export function useSettingsRestore(onRestored?: () => void) {...@@ -506,6 +506,9 @@ export function useSettingsRestore(onRestored?: () => void) {
506 ? ["Contrast"]506 ? ["Contrast"]
507 : []),507 : []),
508 ...(settings.glassOpacity !== DEFAULT_UNIFIED_SETTINGS.glassOpacity ? ["Glass opacity"] : []),508 ...(settings.glassOpacity !== DEFAULT_UNIFIED_SETTINGS.glassOpacity ? ["Glass opacity"] : []),
509 ...(settings.diffColorScheme !== DEFAULT_UNIFIED_SETTINGS.diffColorScheme
510 ? ["Diff colors"]
511 : []),
509 ...(settings.panelAnimationDurationMs !== DEFAULT_UNIFIED_SETTINGS.panelAnimationDurationMs512 ...(settings.panelAnimationDurationMs !== DEFAULT_UNIFIED_SETTINGS.panelAnimationDurationMs
510 ? ["Panel animations"]513 ? ["Panel animations"]
511 : []),514 : []),
...@@ -597,6 +600,7 @@ export function useSettingsRestore(onRestored?: () => void) {...@@ -597,6 +600,7 @@ export function useSettingsRestore(onRestored?: () => void) {
597 settings.browserLinkTarget,600 settings.browserLinkTarget,
598 settings.browserAutoShowFloatingPreview,601 settings.browserAutoShowFloatingPreview,
599 settings.appearanceContrast,602 settings.appearanceContrast,
603 settings.diffColorScheme,
600 settings.enableAgentBrowserAccess,604 settings.enableAgentBrowserAccess,
601 settings.confirmQuit,605 settings.confirmQuit,
602 settings.confirmThreadArchive,606 settings.confirmThreadArchive,
...@@ -701,6 +705,7 @@ export function useSettingsRestore(onRestored?: () => void) {...@@ -701,6 +705,7 @@ export function useSettingsRestore(onRestored?: () => void) {
701 }705 }
702 updateSettings({706 updateSettings({
703 appearanceContrast: DEFAULT_UNIFIED_SETTINGS.appearanceContrast,707 appearanceContrast: DEFAULT_UNIFIED_SETTINGS.appearanceContrast,
708 diffColorScheme: DEFAULT_UNIFIED_SETTINGS.diffColorScheme,
704 timestampFormat: DEFAULT_UNIFIED_SETTINGS.timestampFormat,709 timestampFormat: DEFAULT_UNIFIED_SETTINGS.timestampFormat,
705 wordWrap: DEFAULT_UNIFIED_SETTINGS.wordWrap,710 wordWrap: DEFAULT_UNIFIED_SETTINGS.wordWrap,
706 diffIgnoreWhitespace: DEFAULT_UNIFIED_SETTINGS.diffIgnoreWhitespace,711 diffIgnoreWhitespace: DEFAULT_UNIFIED_SETTINGS.diffIgnoreWhitespace,
...@@ -1240,6 +1245,52 @@ export function AppearanceSettingsPanel() {...@@ -1240,6 +1245,52 @@ export function AppearanceSettingsPanel() {
1240 }1245 }
1241 />1246 />
1242 ) : null}1247 ) : 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 />
1243 </SettingsSection>1294 </SettingsSection>
12441295
1245 <SettingsSection id="motion" title="Motion">1296 <SettingsSection id="motion" title="Motion">
apps/web/src/components/settings/settingsSearch.ts+6
...@@ -106,6 +106,12 @@ export const SETTINGS_SEARCH_ITEMS = [...@@ -106,6 +106,12 @@ export const SETTINGS_SEARCH_ITEMS = [
106 to: "/settings/appearance",106 to: "/settings/appearance",
107 searchTerms: ["transparent transparency solid menus dialogs composer"],107 searchTerms: ["transparent transparency solid menus dialogs composer"],
108 },108 },
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 },
109 {115 {
110 id: "panel-animations",116 id: "panel-animations",
111 title: "Panel animations",117 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...@@ -162,6 +162,10 @@ html[data-mobile-composer-route-transition="true"]::view-transition-old(t3-mobil
162 --color-update-surface: var(--update-surface);162 --color-update-surface: var(--update-surface);
163 --color-success-foreground: var(--success-foreground);163 --color-success-foreground: var(--success-foreground);
164 --color-success: var(--success);164 --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);
165 --color-info-foreground: var(--info-foreground);169 --color-info-foreground: var(--info-foreground);
166 --color-info: var(--info);170 --color-info: var(--info);
167 --color-destructive-foreground: var(--destructive-foreground);171 --color-destructive-foreground: var(--destructive-foreground);
...@@ -1955,6 +1959,28 @@ code {...@@ -1955,6 +1959,28 @@ code {
1955 }1959 }
1956}1960}
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
1958.diff-render-surface diffs-container {1984.diff-render-surface diffs-container {
1959 border: 0;1985 border: 0;
1960 border-radius: 0;1986 border-radius: 0;
apps/web/src/lib/diffRendering.ts+12-12
...@@ -282,37 +282,37 @@ export const DIFF_SURFACE_THEME_UNSAFE_CSS = `...@@ -282,37 +282,37 @@ export const DIFF_SURFACE_THEME_UNSAFE_CSS = `
282 --diffs-bg-buffer-override: color-mix(in srgb, var(--code-background) 90%, var(--code-foreground));282 --diffs-bg-buffer-override: color-mix(in srgb, var(--code-background) 90%, var(--code-foreground));
283283
284 --diffs-bg-addition-override: light-dark(284 --diffs-bg-addition-override: light-dark(
285 color-mix(in srgb, var(--code-background) 50%, var(--success)),285 color-mix(in srgb, var(--code-background) 50%, var(--diff-addition)),
286 color-mix(in srgb, var(--code-background) 70%, var(--success))286 color-mix(in srgb, var(--code-background) 70%, var(--diff-addition))
287 );287 );
288 --diffs-bg-addition-number-override: light-dark(288 --diffs-bg-addition-number-override: light-dark(
289 color-mix(in srgb, var(--code-background) 35%, var(--success)),289 color-mix(in srgb, var(--code-background) 35%, var(--diff-addition)),
290 color-mix(in srgb, var(--code-background) 60%, var(--success))290 color-mix(in srgb, var(--code-background) 60%, var(--diff-addition))
291 );291 );
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));
293 --diffs-bg-addition-emphasis-override: color-mix(293 --diffs-bg-addition-emphasis-override: color-mix(
294 in srgb,294 in srgb,
295 var(--code-background) 80%,295 var(--code-background) 80%,
296 var(--success)296 var(--diff-addition)
297 );297 );
298298
299 --diffs-bg-deletion-override: light-dark(299 --diffs-bg-deletion-override: light-dark(
300 color-mix(in srgb, var(--code-background) 50%, var(--destructive)),300 color-mix(in srgb, var(--code-background) 50%, var(--diff-deletion)),
301 color-mix(in srgb, var(--code-background) 70%, var(--destructive))301 color-mix(in srgb, var(--code-background) 70%, var(--diff-deletion))
302 );302 );
303 --diffs-bg-deletion-number-override: light-dark(303 --diffs-bg-deletion-number-override: light-dark(
304 color-mix(in srgb, var(--code-background) 35%, var(--destructive)),304 color-mix(in srgb, var(--code-background) 35%, var(--diff-deletion)),
305 color-mix(in srgb, var(--code-background) 60%, var(--destructive))305 color-mix(in srgb, var(--code-background) 60%, var(--diff-deletion))
306 );306 );
307 --diffs-bg-deletion-hover-override: color-mix(307 --diffs-bg-deletion-hover-override: color-mix(
308 in srgb,308 in srgb,
309 var(--code-background) 85%,309 var(--code-background) 85%,
310 var(--destructive)310 var(--diff-deletion)
311 );311 );
312 --diffs-bg-deletion-emphasis-override: color-mix(312 --diffs-bg-deletion-emphasis-override: color-mix(
313 in srgb,313 in srgb,
314 var(--code-background) 80%,314 var(--code-background) 80%,
315 var(--destructive)315 var(--diff-deletion)
316 );316 );
317317
318 background-color: var(--diffs-bg) !important;318 background-color: var(--diffs-bg) !important;
apps/web/src/routes/__root.tsx+5
...@@ -226,6 +226,11 @@ function EnvironmentThemeSync() {...@@ -226,6 +226,11 @@ function EnvironmentThemeSync() {
226226
227function ContrastAppearanceSync() {227function ContrastAppearanceSync() {
228 const appearanceContrast = useClientSettings((settings) => settings.appearanceContrast);228 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
230 useEffect(() => {235 useEffect(() => {
231 applyAppearanceContrast(document.documentElement, appearanceContrast);236 applyAppearanceContrast(document.documentElement, appearanceContrast);
packages/contracts/src/settings.test.ts+17
...@@ -135,6 +135,23 @@ describe("ClaudeSettings auto-compaction", () => {...@@ -135,6 +135,23 @@ describe("ClaudeSettings auto-compaction", () => {
135 });135 });
136});136});
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
138describe("ClientSettings load balancing", () => {155describe("ClientSettings load balancing", () => {
139 it("requires opt-in when settings are new or omit load balancing", () => {156 it("requires opt-in when settings are new or omit load balancing", () => {
140 expect(decodeClientSettings({}).loadBalancingEnabled).toBe(false);157 expect(decodeClientSettings({}).loadBalancingEnabled).toBe(false);
packages/contracts/src/settings.ts+6
...@@ -271,7 +271,12 @@ export const LoadBalancingWeights = Schema.Record(...@@ -271,7 +271,12 @@ export const LoadBalancingWeights = Schema.Record(
271 Schema.Int.check(Schema.isBetween({ minimum: 0, maximum: 100 })),271 Schema.Int.check(Schema.isBetween({ minimum: 0, maximum: 100 })),
272);272);
273273
274export const DiffColorScheme = Schema.Literals(["red-green", "blue-orange"]);
275
274export const ClientSettingsSchema = Schema.Struct({276export const ClientSettingsSchema = Schema.Struct({
277 diffColorScheme: DiffColorScheme.pipe(
278 Schema.withDecodingDefault(Effect.succeed("red-green" as const)),
279 ),
275 loadBalancingEnabled: Schema.Boolean.pipe(Schema.withDecodingDefault(Effect.succeed(false))),280 loadBalancingEnabled: Schema.Boolean.pipe(Schema.withDecodingDefault(Effect.succeed(false))),
276 loadBalancingWeights: LoadBalancingWeights.pipe(Schema.withDecodingDefault(Effect.succeed({}))),281 loadBalancingWeights: LoadBalancingWeights.pipe(Schema.withDecodingDefault(Effect.succeed({}))),
277 appearanceContrast: AppearanceContrast.pipe(282 appearanceContrast: AppearanceContrast.pipe(
...@@ -1295,6 +1300,7 @@ export const ServerSettingsPatch = Schema.Struct({...@@ -1295,6 +1300,7 @@ export const ServerSettingsPatch = Schema.Struct({
1295export type ServerSettingsPatch = typeof ServerSettingsPatch.Type;1300export type ServerSettingsPatch = typeof ServerSettingsPatch.Type;
12961301
1297export const ClientSettingsPatch = Schema.Struct({1302export const ClientSettingsPatch = Schema.Struct({
1303 diffColorScheme: Schema.optionalKey(DiffColorScheme),
1298 loadBalancingEnabled: Schema.optionalKey(Schema.Boolean),1304 loadBalancingEnabled: Schema.optionalKey(Schema.Boolean),
1299 loadBalancingWeights: Schema.optionalKey(LoadBalancingWeights),1305 loadBalancingWeights: Schema.optionalKey(LoadBalancingWeights),
1300 appearanceContrast: Schema.optionalKey(AppearanceContrast),1306 appearanceContrast: Schema.optionalKey(AppearanceContrast),