Skip to content

Commit a344664

Browse files
samejrclaude
andcommitted
feat(webapp): let the Black theme take the contrast slider below its base
Black pins every surface flat on #000, so the grid lines are the only thing giving a page structure - and at the base palette they carry more weight than that theme wants. The slider had nowhere to go: the ramps run from the base upward, so 0 was the faintest setting available. The slider now floors at -50 on Black, and only on Black. The stored value is split in two on the way out: `--theme-contrast` keeps the strengthening half exactly as it was, and a new `--theme-fade` carries the negative half, which Black's own grid ramp uses to fade the rules back toward the page. At 0 the fade term mixes 0%, so this is the shared dark ramp untouched - nothing moves for anyone already on a value of 0 or above. The Default tick lands a third of the way along on Black rather than hard left, which is the visible sign that the extra travel is there. One consequence worth knowing: contrast is a single preference shared by all four themes, so a negative value set on Black means nothing on the other three - they have no fade term to read it. The slider clamps its handle to the floor of whichever theme is showing rather than pinning it off the end of the track. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent 7df6f71 commit a344664

4 files changed

Lines changed: 51 additions & 4 deletions

File tree

apps/webapp/app/root.tsx

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -226,7 +226,15 @@ export default function App() {
226226
// Underlines links carrying the inline-text-link marker class
227227
data-underline-links={underlineLinks ? "true" : "false"}
228228
// Contrast overlay input for the System themes; Classic never reads it
229-
style={{ "--theme-contrast": themeContrast / 100 } as CSSProperties}
229+
style={
230+
{
231+
// Split at 0 so the existing ramps never see a negative percentage:
232+
// `--theme-contrast` strengthens as before, `--theme-fade` carries
233+
// the Black-only half below the base.
234+
"--theme-contrast": Math.max(0, themeContrast) / 100,
235+
"--theme-fade": Math.max(0, -themeContrast) / 100,
236+
} as CSSProperties
237+
}
230238
>
231239
<head>
232240
<script

apps/webapp/app/routes/account._index/route.tsx

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@ import {
7979
normalizeThemeContrast,
8080
normalizeUnderlineLinks,
8181
normalizeThemePreference,
82+
MIN_THEME_CONTRAST_BLACK,
8283
SystemDarkTheme,
8384
SystemLightTheme,
8485
type ThemePreference,
@@ -874,6 +875,12 @@ export default function Page() {
874875
);
875876
const systemThemes = { light: systemLightTheme, dark: systemDarkTheme };
876877

878+
/* Which of the four is on screen, resolving `system` through the OS
879+
appearance. Only Black gets the extra travel below the base, so the slider
880+
needs to know rather than reading the raw preference. */
881+
const activeTheme = theme === "system" ? systemThemes[appearance] : theme;
882+
const minContrast = activeTheme === "black" ? MIN_THEME_CONTRAST_BLACK : MIN_CONTRAST;
883+
877884
const saveSystemTheme = (end: "light" | "dark", value: string) => {
878885
const fetcher = end === "light" ? systemLightFetcher : systemDarkFetcher;
879886
// Re-resolve straight away: on `system` this changes which theme is showing
@@ -1093,7 +1100,7 @@ export default function Page() {
10931100
variant="settings"
10941101
className="w-44"
10951102
aria-label="Contrast"
1096-
min={MIN_CONTRAST}
1103+
min={minContrast}
10971104
max={100}
10981105
step={1}
10991106
marks={[
@@ -1109,7 +1116,7 @@ export default function Page() {
11091116
valueTooltip={(value) =>
11101117
value === DEFAULT_CONTRAST_MARK ? "Default" : `${value}%`
11111118
}
1112-
value={[contrastPreview]}
1119+
value={[Math.max(minContrast, contrastPreview)]}
11131120
onValueChange={(values) => previewContrast(values[0] ?? 0)}
11141121
onValueCommit={(values) => saveContrast(values[0] ?? 0)}
11151122
/>

apps/webapp/app/tailwind.css

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1216,6 +1216,32 @@
12161216
--color-background-deep: #000000;
12171217
--color-background-dimmed: #000000;
12181218
--color-background-bright: #000000;
1219+
/*
1220+
Grid lines get the extra travel below the base that the other themes don't
1221+
have. Everything here sits flat on #000, so the rules are the only thing
1222+
giving the page structure - and at the base palette they can be heavier than
1223+
this theme wants. Above 0 this is the shared dark ramp exactly; below it the
1224+
line fades back toward the page.
1225+
*/
1226+
--color-grid-dimmed: color-mix(
1227+
in srgb,
1228+
color-mix(
1229+
in srgb,
1230+
var(--color-charcoal-750),
1231+
var(--color-charcoal-650) calc(var(--theme-contrast, 0) * 100%)
1232+
),
1233+
#000000 calc(var(--theme-fade, 0) * 100%)
1234+
);
1235+
--color-grid-bright: color-mix(
1236+
in srgb,
1237+
color-mix(
1238+
in srgb,
1239+
var(--color-charcoal-700),
1240+
var(--color-charcoal-600) calc(var(--theme-contrast, 0) * 100%)
1241+
),
1242+
#000000 calc(var(--theme-fade, 0) * 100%)
1243+
);
1244+
12191245
/* Interaction feedback still needs somewhere to go */
12201246
--color-background-hover: #171717;
12211247
--color-background-raised: #1f1f1f;

apps/webapp/app/utils/themePreference.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,8 +53,14 @@ export function normalizeUnderlineLinks(value: unknown): boolean {
5353

5454
/** Interface contrast for the System themes, 0 to 100. Missing or invalid
5555
* values fall back to the default bump. */
56+
/** Floor of the contrast range on the Black theme, where the slider can also run
57+
* *below* the base palette to fade the grid lines back toward the page. Every
58+
* other theme floors at 0; a negative value simply has no ramp to act on there,
59+
* so it reads as 0. */
60+
export const MIN_THEME_CONTRAST_BLACK = -50;
61+
5662
export function normalizeThemeContrast(value: unknown): number {
5763
const num = typeof value === "string" ? Number(value) : value;
5864
if (typeof num !== "number" || !Number.isFinite(num)) return DEFAULT_THEME_CONTRAST;
59-
return Math.min(100, Math.max(0, Math.round(num)));
65+
return Math.min(100, Math.max(MIN_THEME_CONTRAST_BLACK, Math.round(num)));
6066
}

0 commit comments

Comments
 (0)