feat(site): add legacy auto sync helper and tritan cleanup (#25180)

## Summary

- Add `legacyAutoToSync` to map legacy auto-family theme preferences to
light and dark sync pairs.
- Expand colorblind theme tests for legacy auto-family handling.
- Move tritan success and git-added surfaces onto the sky-blue palette
in theme roles and CSS variables.

## Dependencies

- None. This can merge independently before the dropdown work.

## Validation

- `pnpm -C site exec vitest run --project=unit
src/theme/colorblind.test.ts`
- `pnpm -C site lint:types`
- Pre-commit hook passed on the branch commit.
This commit is contained in:
Jaayden Halko
2026-05-14 14:43:13 +01:00
committed by GitHub
parent 25a803221e
commit acf57b3b35
5 changed files with 130 additions and 41 deletions
+26 -3
View File
@@ -199,9 +199,15 @@
success+error axis onto sky-blue (success) + vermilion/orange
(destructive). Warning shifts to fuchsia so it does not collide
with destructive states on the orange axis.
- dark-tritan / light-tritan: keep the red/green success+error
axis intact and move warning from amber to fuchsia because
amber and sky-blue blur together under tritanopia.
- dark-tritan / light-tritan: keep destructive states on the
red/orange axis and shift `success` (and the green-tinted
diff/git surfaces) onto sky-blue. This matches GitHub
Primer's tritanopia preset, where diff additions render in
blue rather than green so the success+destructive pair stays
consistent with the protan-deuter palette and with other
tritan-aware tools. Warning still moves from amber to
fuchsia because amber and sky-blue blur together under
tritanopia.
*/
.light-protan-deuter {
--content-success: 199 89% 48%;
@@ -252,24 +258,41 @@
--surface-git-deleted: 17 75% 15%;
}
.light-tritan {
--content-success: 199 89% 48%;
--content-warning: 322 81% 43%;
--surface-green: 201 94% 86%;
--surface-orange: 289 100% 98%;
--border-success: 199 89% 48%;
--border-green: 201 94% 86%;
--border-warning: 322 81% 60%;
--highlight-green: 201 94% 36%;
--highlight-orange: 322 81% 43%;
--highlight-magenta: 322, 81%, 43%;
--syntax-boolean: 322 81% 43%;
--syntax-number: 199 89% 38%;
--git-added: 199 89% 48%;
--git-modified: 322 81% 43%;
--git-added-bright: 199 89% 48%;
--surface-git-added: 204 94% 94%;
}
.dark-tritan {
--content-success: 199 82% 67%;
--content-warning: 322 81% 67%;
--surface-green: 201 80% 14%;
--surface-orange: 322 70% 15%;
--surface-magenta: 322 70% 15%;
--border-success: 199 82% 67%;
--border-green: 201 94% 86%;
--border-magenta: 322 81% 72%;
--border-warning: 322 81% 67%;
--highlight-green: 201 94% 86%;
--highlight-orange: 322 81% 67%;
--highlight-magenta: 322 81% 72%;
--syntax-boolean: 322 81% 67%;
--git-added: 199 82% 67%;
--git-modified: 322 81% 72%;
--git-added-bright: 199 89% 48%;
--surface-git-added: 201 80% 14%;
}
}
+47 -1
View File
@@ -2,6 +2,7 @@ import themes, {
baseModeFor,
CONCRETE_THEMES,
isConcreteThemeName,
legacyAutoToSync,
resolveThemeName,
} from ".";
@@ -71,8 +72,10 @@ describe("isConcreteThemeName", () => {
}
});
it("rejects the auto preference (embeds require a concrete theme)", () => {
it("rejects legacy auto-family preferences", () => {
expect(isConcreteThemeName("auto")).toBe(false);
expect(isConcreteThemeName("auto-protan-deuter")).toBe(false);
expect(isConcreteThemeName("auto-tritan")).toBe(false);
});
it("rejects non-string and empty values", () => {
@@ -84,6 +87,34 @@ describe("isConcreteThemeName", () => {
});
});
describe("legacyAutoToSync", () => {
it("maps each legacy auto value to its sync pair", () => {
expect(legacyAutoToSync("auto")).toEqual({
mode: "sync",
light: "light",
dark: "dark",
});
expect(legacyAutoToSync("auto-protan-deuter")).toEqual({
mode: "sync",
light: "light-protan-deuter",
dark: "dark-protan-deuter",
});
expect(legacyAutoToSync("auto-tritan")).toEqual({
mode: "sync",
light: "light-tritan",
dark: "dark-tritan",
});
});
it("returns null for concrete theme names and unrelated values", () => {
expect(legacyAutoToSync("dark")).toBeNull();
expect(legacyAutoToSync("dark-tritan")).toBeNull();
expect(legacyAutoToSync("")).toBeNull();
expect(legacyAutoToSync(undefined)).toBeNull();
expect(legacyAutoToSync("garbage")).toBeNull();
});
});
describe("baseModeFor", () => {
it("maps every concrete theme to its base mode", () => {
for (const name of CONCRETE_THEMES) {
@@ -120,4 +151,19 @@ describe("colorblind role palettes", () => {
themes.dark.roles.danger,
);
});
it("shifts tritan success off the base green role onto sky-blue", () => {
expect(themes["light-tritan"].roles.success).not.toEqual(
themes.light.roles.success,
);
expect(themes["dark-tritan"].roles.success).not.toEqual(
themes.dark.roles.success,
);
expect(themes["light-tritan"].roles.success).toEqual(
themes["light-protan-deuter"].roles.success,
);
expect(themes["dark-tritan"].roles.success).toEqual(
themes["dark-protan-deuter"].roles.success,
);
});
});
+14 -19
View File
@@ -1,11 +1,6 @@
import type { Roles } from "../roles";
import colors from "../tailwindColors";
// Tritanopia reduces blue/yellow discrimination, so the standard amber
// warning can blur into the sky-blue active/notice accents. Under
// tritanopia, red vs green remains intact, so we keep `success` on green,
// `error` on red, and `danger` on the base orange. Only `warning` shifts
// to a magenta/pink that stays distinct from blue and red states.
const roles: Roles = {
danger: {
background: colors.orange[950],
@@ -80,31 +75,31 @@ const roles: Roles = {
},
},
success: {
background: colors.green[950],
outline: colors.green[500],
text: colors.green[50],
background: colors.sky[950],
outline: colors.sky[500],
text: colors.sky[50],
fill: {
solid: colors.green[600],
outline: colors.green[600],
solid: colors.sky[600],
outline: colors.sky[600],
text: colors.white,
},
disabled: {
background: colors.green[950],
outline: colors.green[800],
text: colors.green[200],
background: colors.sky[950],
outline: colors.sky[800],
text: colors.sky[200],
fill: {
solid: colors.green[800],
outline: colors.green[800],
solid: colors.sky[800],
outline: colors.sky[800],
text: colors.white,
},
},
hover: {
background: colors.green[900],
outline: colors.green[500],
background: colors.sky[900],
outline: colors.sky[500],
text: colors.white,
fill: {
solid: colors.green[500],
outline: colors.green[500],
solid: colors.sky[500],
outline: colors.sky[500],
text: colors.white,
},
},
+29
View File
@@ -53,6 +53,35 @@ export const isConcreteThemeName = (
return typeof value === "string" && concreteThemeSet.has(value);
};
type LegacyAutoSync = {
mode: "sync";
light: ConcreteThemeName;
dark: ConcreteThemeName;
};
const LEGACY_AUTO_SYNC: Record<string, LegacyAutoSync> = {
auto: { mode: "sync", light: "light", dark: "dark" },
"auto-protan-deuter": {
mode: "sync",
light: "light-protan-deuter",
dark: "dark-protan-deuter",
},
"auto-tritan": {
mode: "sync",
light: "light-tritan",
dark: "dark-tritan",
},
};
export const legacyAutoToSync = (
preference: string | undefined,
): LegacyAutoSync | null => {
if (!preference) {
return null;
}
return LEGACY_AUTO_SYNC[preference] ?? null;
};
export const resolveThemeName = (
preference: string | undefined,
osScheme: "dark" | "light",
+14 -18
View File
@@ -1,10 +1,6 @@
import type { Roles } from "../roles";
import colors from "../tailwindColors";
// Tritanopia reduces blue/yellow discrimination. Red vs green remains
// intact, so we keep `success` on green, `error` on red, and `danger`
// on the base orange. Only `warning` shifts to a magenta/fuchsia that
// stays distinct from the blue accents and red destructive states.
const roles: Roles = {
danger: {
background: colors.orange[50],
@@ -79,31 +75,31 @@ const roles: Roles = {
},
},
success: {
background: colors.green[50],
outline: colors.green[500],
text: colors.green[950],
background: colors.sky[100],
outline: colors.sky[500],
text: colors.sky[950],
fill: {
solid: colors.green[600],
outline: colors.green[600],
solid: colors.sky[600],
outline: colors.sky[600],
text: colors.white,
},
disabled: {
background: colors.green[50],
outline: colors.green[800],
text: colors.green[800],
background: colors.sky[50],
outline: colors.sky[800],
text: colors.sky[800],
fill: {
solid: colors.green[800],
outline: colors.green[800],
solid: colors.sky[800],
outline: colors.sky[800],
text: colors.white,
},
},
hover: {
background: colors.green[100],
outline: colors.green[500],
background: colors.sky[200],
outline: colors.sky[500],
text: colors.black,
fill: {
solid: colors.green[500],
outline: colors.green[500],
solid: colors.sky[500],
outline: colors.sky[500],
text: colors.white,
},
},