mirror of
https://github.com/badlogic/pi-mono.git
synced 2026-11-05 23:37:25 +00:00
fix(coding-agent): keep pastel palettes pastel in the system theme (#10293)
Palette colors now keep the bell-curve falloff and are also capped at the palette color's OKLCH chroma times that falloff. Lightness and hue are unchanged, so contrast is unchanged. closes #10255
This commit is contained in:
parent
c662ec7e37
commit
409e808f58
2 changed files with 44 additions and 5 deletions
|
|
@ -5,7 +5,8 @@
|
|||
* on the background and on the panels it is drawn on. Hue and saturation come from the terminal's palette
|
||||
* color for the family's ANSI slot, or from the family's own hue when the terminal reports no palette.
|
||||
* Lightness comes from the rules alone. Colors are built in OKHSL, whose saturation is relative to the
|
||||
* sRGB gamut, and fade toward gray near black and white.
|
||||
* sRGB gamut, and fade toward gray near black and white. A palette color never gains OKLCH chroma when it
|
||||
* moves to another lightness, so pastel palettes stay pastel.
|
||||
*
|
||||
* A contrast level is a target-lightness curve: the OKLab lightness a token needs, given the lightness of
|
||||
* the surface below it. The curves were fitted to the reference theme design from the "Pi themes: system
|
||||
|
|
@ -21,9 +22,11 @@
|
|||
import {
|
||||
colorToOkhsl,
|
||||
colorToOklch,
|
||||
colorToRgb,
|
||||
type OkhslChannels,
|
||||
okhslColor,
|
||||
oklabToOkhslLightness,
|
||||
oklchColor,
|
||||
type RgbColor,
|
||||
rgbColor,
|
||||
} from "@earendil-works/pi-tui";
|
||||
|
|
@ -463,7 +466,7 @@ export function generateSystemThemeColors(input: SystemThemeInput): SystemThemeC
|
|||
const saturation = clamp(input.saturation ?? 1, 0, 1);
|
||||
const { background, foreground } = input;
|
||||
if (!background) return indexedColors(saturation, input.appearanceHint);
|
||||
const palette = input.palette?.length === 16 ? input.palette.map(okhslOf) : undefined;
|
||||
const palette = input.palette?.length === 16 ? input.palette.map(sourceOf) : undefined;
|
||||
|
||||
const appearance = terminalAppearance(background, foreground);
|
||||
const lighter = appearance === "dark";
|
||||
|
|
@ -574,7 +577,7 @@ export function generateSystemThemeColors(input: SystemThemeInput): SystemThemeC
|
|||
result[token] = "";
|
||||
continue;
|
||||
}
|
||||
text = anchored(okhslOf(foreground), FAMILIES.neutral, oklabToOkhslLightness(needed), saturation);
|
||||
text = anchored(sourceOf(foreground), FAMILIES.neutral, oklabToOkhslLightness(needed), saturation);
|
||||
}
|
||||
}
|
||||
// Body text keeps at least 4.5:1 on the surfaces it is drawn on, even on relaxed mid-gray backgrounds.
|
||||
|
|
@ -587,14 +590,30 @@ function okhslOf({ r, g, b }: RgbColor): OkhslChannels {
|
|||
return colorToOkhsl(rgbColor(r, g, b));
|
||||
}
|
||||
|
||||
/** A terminal color's OKHSL channels and its OKLCH chroma. */
|
||||
interface SourceColor extends OkhslChannels {
|
||||
chroma: number;
|
||||
}
|
||||
|
||||
function sourceOf(color: RgbColor): SourceColor {
|
||||
return { ...okhslOf(color), chroma: colorToOklch(rgbColor(color.r, color.g, color.b)).c };
|
||||
}
|
||||
|
||||
/**
|
||||
* A source color's hue at another OKHSL lightness. Its saturation applies at its own lightness and falls off
|
||||
* toward black and white along the family's saturation curve, never rising above it.
|
||||
*
|
||||
* OKHSL saturation is relative to the most chroma sRGB allows at a lightness, so the same saturation can mean
|
||||
* more chroma elsewhere: Catppuccin Frappe's pink #f4b8e4 (chroma 0.089) would become #eb76d1 (0.180) at the
|
||||
* lightness the accent needs. Chroma is therefore also capped at the source's, with the same falloff.
|
||||
*/
|
||||
function anchored(source: OkhslChannels, family: Family, lightness: number, saturation: number): RgbColor {
|
||||
function anchored(source: SourceColor, family: Family, lightness: number, saturation: number): RgbColor {
|
||||
const anchor = saturationCurve(family, source.l);
|
||||
const falloff = anchor > 0 ? Math.min(1, saturationCurve(family, lightness) / anchor) : 1;
|
||||
return okhslColor(source.h, source.s * falloff * saturation, lightness);
|
||||
const color = okhslColor(source.h, source.s * falloff * saturation, lightness);
|
||||
const cap = source.chroma * falloff * saturation;
|
||||
const { l, c } = colorToOklch(color);
|
||||
return c <= cap ? color : colorToRgb(oklchColor(l, cap, source.h));
|
||||
}
|
||||
|
||||
/** Move a text color toward white or black until it reaches the WCAG minimum on every surface. */
|
||||
|
|
|
|||
|
|
@ -83,6 +83,26 @@ describe("generateSystemThemeColors", () => {
|
|||
expect(Math.abs(hue(resolved(DRACULA, "error")) - hue(DRACULA.palette![1]))).toBeLessThan(8);
|
||||
});
|
||||
|
||||
// https://github.com/earendil-works/pi/issues/10255
|
||||
it("keeps pastel palette colors pastel at other lightnesses", () => {
|
||||
const frappe: SystemThemeInput = {
|
||||
background: rgb("#303446"),
|
||||
foreground: rgb("#c6d0f5"),
|
||||
palette: ["#51576d", "#e78284", "#a6d189", "#e5c890", "#8caaee", "#f4b8e4", "#81c8be", "#b5bfe2"]
|
||||
.concat(["#626880", "#e67172", "#8ec772", "#d9ba73", "#7b9ef0", "#f2a4db", "#5abfb5", "#a5adce"])
|
||||
.map(rgb),
|
||||
};
|
||||
const chroma = ({ r, g, b }: RgbColor) => colorToOklch(rgbColor(r, g, b)).c;
|
||||
const pink = frappe.palette![5];
|
||||
const accent = resolved(frappe, "accent");
|
||||
// The accent is darker than the pink, but must not gain chroma (it was 2x before the cap).
|
||||
expect(lightness(accent)).toBeLessThan(lightness(pink) - 0.05);
|
||||
expect(chroma(accent)).toBeLessThanOrEqual(chroma(pink) * 1.03);
|
||||
for (const panel of ["userMessageBg", "customMessageBg"] as const) {
|
||||
expect(chroma(resolved(frappe, panel)), panel).toBeLessThanOrEqual(0.1);
|
||||
}
|
||||
});
|
||||
|
||||
it("renders grayscale at zero saturation", () => {
|
||||
const { colors } = generateSystemThemeColors({ ...DRACULA, saturation: 0 });
|
||||
expect(colorToOklch(parseColor(colors.error as string)).c).toBeLessThan(0.005);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue