diff --git a/app/(home-pages)/home/LeafletList/LeafletListItem.tsx b/app/(home-pages)/home/LeafletList/LeafletListItem.tsx
index d647206e..af30a578 100644
--- a/app/(home-pages)/home/LeafletList/LeafletListItem.tsx
+++ b/app/(home-pages)/home/LeafletList/LeafletListItem.tsx
@@ -57,7 +57,7 @@ export const LeafletListItem = (props: {
>
{props.showPreview && }
{
// sort accents by contrast against the background
return (
- getColorDifference(
- colorToString(b, "rgb"),
- colorToString(showPageBackground ? bgPage : bgLeaflet, "rgb"),
- ) -
- getColorDifference(
- colorToString(a, "rgb"),
- colorToString(showPageBackground ? bgPage : bgLeaflet, "rgb"),
- )
+ compareColors(colorToString(b, "rgb"), bgRef).distance -
+ compareColors(colorToString(a, "rgb"), bgRef).distance
);
});
+
+ let bestVsText = compareColors(
+ colorToString(sortedAccents[0], "rgb"),
+ primaryStr,
+ );
+ let altVsBg = compareColors(colorToString(sortedAccents[1], "rgb"), bgRef);
+
if (
// if the contrast-y accent is too similar to text color
- getColorDifference(
- colorToString(sortedAccents[0], "rgb"),
- colorToString(primary, "rgb"),
- ) < 0.15 &&
+ // (close in distance AND not distinguishable by hue/chroma)
+ bestVsText.distance < 0.15 &&
+ bestVsText.chromaDiff < 0.05 &&
// and if the other accent is different enough from the background
- getColorDifference(
- colorToString(sortedAccents[1], "rgb"),
- colorToString(showPageBackground ? bgPage : bgLeaflet, "rgb"),
- ) > 0.31
+ altVsBg.distance > 0.31
) {
//then choose the less contrast-y accent
accentContrast = sortedAccents[1];
@@ -202,12 +202,18 @@ export const BaseThemeProvider = ({
accentContrast = sortedAccents[0];
}
- // Check if the final accent contrast color is very similar to the text color
+ // Check if the accent contrast color is visually similar to the text color.
+ // We check both overall OKLab distance AND chroma (hue/saturation) difference
+ // because dark colors are compressed in OKLab lightness — a dark blue accent
+ // vs black text can have a small OKLab distance yet be clearly distinguishable
+ // by hue. If the chroma difference is significant, the colors are visually
+ // distinct and don't need an underline to tell them apart.
+ let accentVsText = compareColors(
+ colorToString(accentContrast, "rgb"),
+ primaryStr,
+ );
let accentContrastSimilarToText =
- getColorDifference(
- colorToString(accentContrast, "rgb"),
- colorToString(primary, "rgb"),
- ) < 0.2;
+ accentVsText.distance < 0.45 && accentVsText.chromaDiff < 0.05;
// Get font configs for CSS variables.
// When using the default font (Quattro), use var(--font-quattro) which is
@@ -418,15 +424,10 @@ export function CardThemeProvider(props: {
let accentContrast =
bgPage && accent1 && accent2
? [accent1, accent2].sort((a, b) => {
+ let bgStr = colorToString(bgPage, "rgb");
return (
- getColorDifference(
- colorToString(b, "rgb"),
- colorToString(bgPage, "rgb"),
- ) -
- getColorDifference(
- colorToString(a, "rgb"),
- colorToString(bgPage, "rgb"),
- )
+ compareColors(colorToString(b, "rgb"), bgStr).distance -
+ compareColors(colorToString(a, "rgb"), bgStr).distance
);
})[0]
: null;
diff --git a/components/ThemeManager/themeUtils.ts b/components/ThemeManager/themeUtils.ts
index 50ef8866..baf1e8fc 100644
--- a/components/ThemeManager/themeUtils.ts
+++ b/components/ThemeManager/themeUtils.ts
@@ -1,4 +1,4 @@
-import { parse, ColorSpace, sRGB, distance, OKLab } from "colorjs.io/fn";
+import { parse, ColorSpace, sRGB, distance, OKLab, to } from "colorjs.io/fn";
// define the color defaults for everything
export const ThemeDefaults = {
@@ -16,13 +16,25 @@ export const ThemeDefaults = {
"theme/accent-contrast": "#57822B",
};
-// used to calculate the contrast between page and accent1, accent2, and determin which is higher contrast
-export function getColorDifference(color1: string, color2: string) {
+// Compares two RGB color strings in OKLab space and returns both the overall
+// perceptual distance and the chroma (hue/saturation) difference.
+//
+// Why both? Dark colors are compressed in OKLab lightness, so two colors can
+// have a small overall distance yet be clearly distinguishable by hue (e.g.
+// dark blue vs black). Checking chromaDiff lets callers tell apart "two
+// similar grays" from "a dark chromatic color next to a gray/black".
+export function compareColors(color1: string, color2: string) {
ColorSpace.register(sRGB);
ColorSpace.register(OKLab);
let parsedColor1 = parse(`rgb(${color1})`);
let parsedColor2 = parse(`rgb(${color2})`);
- return distance(parsedColor1, parsedColor2, "oklab");
+ let [, a1, b1] = to(parsedColor1, "oklab").coords;
+ let [, a2, b2] = to(parsedColor2, "oklab").coords;
+
+ return {
+ distance: distance(parsedColor1, parsedColor2, "oklab"),
+ chromaDiff: Math.sqrt((a1 - a2) ** 2 + (b1 - b2) ** 2),
+ };
}