| author | |
| committer | |
| log | bb5e824c9fcbd76c93ef15b304f89f8b6999f32c |
| tree | 672331fb95c59876b4f890982b4f41f4d397c0b4 |
| parent | 0f602b3372b300ae94084bd3fe7dbaadaa58ba3a |
| signature | Signed by PGP key B5690EEEBB952194 |
11 files changed, 133 insertions(+), 24 deletions(-)
apps/web/src/components/GitActionsControl.tsx+4-4| ... | ... | @@ -1878,9 +1878,9 @@ export default function GitActionsControl({ |
| 1878 | 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 | 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 | 1886 | </span> |
| ... | ... | @@ -1891,11 +1891,11 @@ export default function GitActionsControl({ |
| 1891 | 1891 | </div> |
| 1892 | 1892 | </ScrollArea> |
| 1893 | 1893 | <div className="flex justify-end font-mono"> |
| 1894 | <span className="text-success"> | |
| 1894 | <span className="text-diff-addition"> | |
| 1895 | 1895 | +{selectedFiles.reduce((sum, f) => sum + f.insertions, 0)} |
| 1896 | 1896 | </span> |
| 1897 | 1897 | <span className="text-muted-foreground"> / </span> |
| 1898 | <span className="text-destructive"> | |
| 1898 | <span className="text-diff-deletion"> | |
| 1899 | 1899 | -{selectedFiles.reduce((sum, f) => sum + f.deletions, 0)} |
| 1900 | 1900 | </span> |
| 1901 | 1901 | </div> |
apps/web/src/components/Sidebar.tsx+2-2| ... | ... | @@ -1932,8 +1932,8 @@ const SidebarThreadRow = memo(function SidebarThreadRow(props: { |
| 1932 | 1932 | ) : null} |
| 1933 | 1933 | {diff ? ( |
| 1934 | 1934 | <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> | |
| 1937 | 1937 | </span> |
| 1938 | 1938 | ) : null} |
| 1939 | 1939 | <span |
apps/web/src/components/chat/DiffStatLabel.tsx+2-2| ... | ... | @@ -40,10 +40,10 @@ export const DiffStatLabel = memo(function DiffStatLabel(props: { |
| 40 | 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 | 44 | +{formatCompactDiffCount(additions)} |
| 45 | 45 | </span> |
| 46 | <span aria-hidden="true" className="font-mono text-destructive"> | |
| 46 | <span aria-hidden="true" className="font-mono text-diff-deletion"> | |
| 47 | 47 | -{formatCompactDiffCount(deletions)} |
| 48 | 48 | </span> |
| 49 | 49 | </span> |
apps/web/src/components/pullRequest/pullRequestPresentation.tsx+2-4| ... | ... | @@ -422,10 +422,8 @@ export function PullRequestDiffStat({ |
| 422 | 422 | } |
| 423 | 423 | return ( |
| 424 | 424 | <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> | |
| 429 | 427 | </span> |
| 430 | 428 | ); |
| 431 | 429 | } |
apps/web/src/components/settings/SettingsPanels.tsx+51| ... | ... | @@ -506,6 +506,9 @@ export function useSettingsRestore(onRestored?: () => void) { |
| 506 | 506 | ? ["Contrast"] |
| 507 | 507 | : []), |
| 508 | 508 | ...(settings.glassOpacity !== DEFAULT_UNIFIED_SETTINGS.glassOpacity ? ["Glass opacity"] : []), |
| 509 | ...(settings.diffColorScheme !== DEFAULT_UNIFIED_SETTINGS.diffColorScheme | |
| 510 | ? ["Diff colors"] | |
| 511 | : []), | |
| 509 | 512 | ...(settings.panelAnimationDurationMs !== DEFAULT_UNIFIED_SETTINGS.panelAnimationDurationMs |
| 510 | 513 | ? ["Panel animations"] |
| 511 | 514 | : []), |
| ... | ... | @@ -597,6 +600,7 @@ export function useSettingsRestore(onRestored?: () => void) { |
| 597 | 600 | settings.browserLinkTarget, |
| 598 | 601 | settings.browserAutoShowFloatingPreview, |
| 599 | 602 | settings.appearanceContrast, |
| 603 | settings.diffColorScheme, | |
| 600 | 604 | settings.enableAgentBrowserAccess, |
| 601 | 605 | settings.confirmQuit, |
| 602 | 606 | settings.confirmThreadArchive, |
| ... | ... | @@ -701,6 +705,7 @@ export function useSettingsRestore(onRestored?: () => void) { |
| 701 | 705 | } |
| 702 | 706 | updateSettings({ |
| 703 | 707 | appearanceContrast: DEFAULT_UNIFIED_SETTINGS.appearanceContrast, |
| 708 | diffColorScheme: DEFAULT_UNIFIED_SETTINGS.diffColorScheme, | |
| 704 | 709 | timestampFormat: DEFAULT_UNIFIED_SETTINGS.timestampFormat, |
| 705 | 710 | wordWrap: DEFAULT_UNIFIED_SETTINGS.wordWrap, |
| 706 | 711 | diffIgnoreWhitespace: DEFAULT_UNIFIED_SETTINGS.diffIgnoreWhitespace, |
| ... | ... | @@ -1240,6 +1245,52 @@ export function AppearanceSettingsPanel() { |
| 1240 | 1245 | } |
| 1241 | 1246 | /> |
| 1242 | 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 | 1294 | </SettingsSection> |
| 1244 | 1295 | |
| 1245 | 1296 | <SettingsSection id="motion" title="Motion"> |
apps/web/src/components/settings/settingsSearch.ts+6| ... | ... | @@ -106,6 +106,12 @@ export const SETTINGS_SEARCH_ITEMS = [ |
| 106 | 106 | to: "/settings/appearance", |
| 107 | 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 | 116 | id: "panel-animations", |
| 111 | 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 | 162 | --color-update-surface: var(--update-surface); |
| 163 | 163 | --color-success-foreground: var(--success-foreground); |
| 164 | 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 | 169 | --color-info-foreground: var(--info-foreground); |
| 166 | 170 | --color-info: var(--info); |
| 167 | 171 | --color-destructive-foreground: var(--destructive-foreground); |
| ... | ... | @@ -1955,6 +1959,28 @@ code { |
| 1955 | 1959 | } |
| 1956 | 1960 | } |
| 1957 | 1961 | |
| 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 | 1984 | .diff-render-surface diffs-container { |
| 1959 | 1985 | border: 0; |
| 1960 | 1986 | border-radius: 0; |
apps/web/src/lib/diffRendering.ts+12-12| ... | ... | @@ -282,37 +282,37 @@ export const DIFF_SURFACE_THEME_UNSAFE_CSS = ` |
| 282 | 282 | --diffs-bg-buffer-override: color-mix(in srgb, var(--code-background) 90%, var(--code-foreground)); |
| 283 | 283 | |
| 284 | 284 | --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)) | |
| 287 | 287 | ); |
| 288 | 288 | --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)) | |
| 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 | 293 | --diffs-bg-addition-emphasis-override: color-mix( |
| 294 | 294 | in srgb, |
| 295 | 295 | var(--code-background) 80%, |
| 296 | var(--success) | |
| 296 | var(--diff-addition) | |
| 297 | 297 | ); |
| 298 | 298 | |
| 299 | 299 | --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)) | |
| 302 | 302 | ); |
| 303 | 303 | --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)) | |
| 306 | 306 | ); |
| 307 | 307 | --diffs-bg-deletion-hover-override: color-mix( |
| 308 | 308 | in srgb, |
| 309 | 309 | var(--code-background) 85%, |
| 310 | var(--destructive) | |
| 310 | var(--diff-deletion) | |
| 311 | 311 | ); |
| 312 | 312 | --diffs-bg-deletion-emphasis-override: color-mix( |
| 313 | 313 | in srgb, |
| 314 | 314 | var(--code-background) 80%, |
| 315 | var(--destructive) | |
| 315 | var(--diff-deletion) | |
| 316 | 316 | ); |
| 317 | 317 | |
| 318 | 318 | background-color: var(--diffs-bg) !important; |
apps/web/src/routes/__root.tsx+5| ... | ... | @@ -226,6 +226,11 @@ function EnvironmentThemeSync() { |
| 226 | 226 | |
| 227 | 227 | function ContrastAppearanceSync() { |
| 228 | 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]); | |
| 229 | 234 | |
| 230 | 235 | useEffect(() => { |
| 231 | 236 | applyAppearanceContrast(document.documentElement, appearanceContrast); |
packages/contracts/src/settings.test.ts+17| ... | ... | @@ -135,6 +135,23 @@ describe("ClaudeSettings auto-compaction", () => { |
| 135 | 135 | }); |
| 136 | 136 | }); |
| 137 | 137 | |
| 138 | describe("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 | ||
| 138 | 155 | describe("ClientSettings load balancing", () => { |
| 139 | 156 | it("requires opt-in when settings are new or omit load balancing", () => { |
| 140 | 157 | expect(decodeClientSettings({}).loadBalancingEnabled).toBe(false); |
packages/contracts/src/settings.ts+6| ... | ... | @@ -271,7 +271,12 @@ export const LoadBalancingWeights = Schema.Record( |
| 271 | 271 | Schema.Int.check(Schema.isBetween({ minimum: 0, maximum: 100 })), |
| 272 | 272 | ); |
| 273 | 273 | |
| 274 | export const DiffColorScheme = Schema.Literals(["red-green", "blue-orange"]); | |
| 275 | ||
| 274 | 276 | export const ClientSettingsSchema = Schema.Struct({ |
| 277 | diffColorScheme: DiffColorScheme.pipe( | |
| 278 | Schema.withDecodingDefault(Effect.succeed("red-green" as const)), | |
| 279 | ), | |
| 275 | 280 | loadBalancingEnabled: Schema.Boolean.pipe(Schema.withDecodingDefault(Effect.succeed(false))), |
| 276 | 281 | loadBalancingWeights: LoadBalancingWeights.pipe(Schema.withDecodingDefault(Effect.succeed({}))), |
| 277 | 282 | appearanceContrast: AppearanceContrast.pipe( |
| ... | ... | @@ -1295,6 +1300,7 @@ export const ServerSettingsPatch = Schema.Struct({ |
| 1295 | 1300 | export type ServerSettingsPatch = typeof ServerSettingsPatch.Type; |
| 1296 | 1301 | |
| 1297 | 1302 | export const ClientSettingsPatch = Schema.Struct({ |
| 1303 | diffColorScheme: Schema.optionalKey(DiffColorScheme), | |
| 1298 | 1304 | loadBalancingEnabled: Schema.optionalKey(Schema.Boolean), |
| 1299 | 1305 | loadBalancingWeights: Schema.optionalKey(LoadBalancingWeights), |
| 1300 | 1306 | appearanceContrast: Schema.optionalKey(AppearanceContrast), |