Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 7 additions & 3 deletions apps/web/src/appearanceContrast.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,17 +17,21 @@ describe("applyAppearanceContrast", () => {
applyAppearanceContrast(root, 135);

expect(setProperty).toHaveBeenCalledWith("--appearance-contrast-base", "100%");
expect(setProperty).toHaveBeenCalledWith("--appearance-contrast-boost", "35%");
expect(setProperty).toHaveBeenCalledWith("--appearance-contrast-boost", "21%");
expect(setProperty).toHaveBeenCalledWith("--appearance-contrast-border-boost", "8.75%");
});

it("supports the maximum contrast boost", () => {
// The foreground mix stops short of the target on purpose: a full 100% makes
// every foreground role resolve to the target itself, so normal, muted and
// placeholder text become the same colour at the slider's own maximum.
it("keeps the maximum foreground boost short of a full mix", () => {
const { root, setProperty } = makeRoot();

applyAppearanceContrast(root, 200);

expect(setProperty).toHaveBeenCalledWith("--appearance-contrast-base", "100%");
expect(setProperty).toHaveBeenCalledWith("--appearance-contrast-boost", "100%");
expect(setProperty).toHaveBeenCalledWith("--appearance-contrast-boost", "60%");
expect(setProperty).not.toHaveBeenCalledWith("--appearance-contrast-boost", "100%");
expect(setProperty).toHaveBeenCalledWith("--appearance-contrast-border-boost", "25%");
});

Expand Down
25 changes: 20 additions & 5 deletions apps/web/src/appearanceContrast.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,25 @@
import type { AppearanceContrast } from "@t3tools/contracts/settings";

/**
* The boost mixes every foreground role toward one shared target, so a full
* 100% resolves them all to exactly that target and the normal/muted/placeholder
* hierarchy disappears — an empty composer becomes indistinguishable from a
* filled one. Measured in a browser against the real tokens: at contrast 200 an
* unscaled boost puts `--contrast-foreground`, `--contrast-muted-foreground`,
* `--contrast-placeholder` and `--contrast-icon-muted` all at `oklab(0 0 0)`.
*
* Capping the mix keeps the top of the range clearly darker than the default
* while holding the roles apart. Lightness separation between normal and muted
* text: 0.278 at the default, 0.111 here, 0 unscaled.
*
* Borders are excluded — they carry a single role, so converging on the target
* costs no hierarchy, and their own quarter-weight already bounds them.
*/
const FOREGROUND_BOOST_RATIO = 0.6;

export function applyAppearanceContrast(root: HTMLElement, contrast: AppearanceContrast): void {
const overshoot = Math.max(contrast - 100, 0);
root.style.setProperty("--appearance-contrast-base", `${Math.min(contrast, 100)}%`);
root.style.setProperty("--appearance-contrast-boost", `${Math.max(contrast - 100, 0)}%`);
root.style.setProperty(
"--appearance-contrast-border-boost",
`${Math.max(contrast - 100, 0) / 4}%`,
);
root.style.setProperty("--appearance-contrast-boost", `${overshoot * FOREGROUND_BOOST_RATIO}%`);
root.style.setProperty("--appearance-contrast-border-boost", `${overshoot / 4}%`);
}
Loading