Skip to content

feat(web): add appearance contrast control - #7906

Open
t3-code[bot] wants to merge 9 commits into
mainfrom
feat/appearance-contrast
Open

feat(web): add appearance contrast control#7906
t3-code[bot] wants to merge 9 commits into
mainfrom
feat/appearance-contrast

Conversation

@t3-code

@t3-code t3-code Bot commented Aug 22, 2026

Copy link
Copy Markdown
Contributor

problem

theme contrast was fixed, so users could not make interface colors and borders softer or stronger.

change

  • add a persisted 50% to 200% contrast slider under settings > appearance
  • apply changes live across the full interface, with 100% preserving the theme exactly
  • derive contrast from semantic text and border tokens without filtering media, previews, terminals, or glass surfaces
  • expose reset and settings search support
  • validate stored contrast values through the client settings schema

demo

real app recording in light and dark, dragged from 100% to 50%, then 200%, then back to 100%:

https://t3bot-uploads-production.up.railway.app/files/iMVIoJi19ZmJe77eYjGEGrRa/t3code-appearance-contrast-demo-v6.mp4

verification

  • vp test run apps/web/src/appearanceContrast.test.ts apps/desktop/src/settings/DesktopClientSettings.test.ts packages/contracts/src/settings.test.ts apps/web/src/components/settings/settingsSearch.test.ts
  • vp run --filter @t3tools/contracts typecheck
  • vp run --filter @t3tools/web typecheck
  • vp run --filter @t3tools/desktop typecheck
  • vp run --filter @t3tools/web build
  • real app playback at 50%, 100%, and 200% in light and dark

built with gpt-5.6 via hermes agent.

request provenance


Note

Medium Risk
Touches the global color token bridge and many chrome consumers, so a missed role can split adjusted vs unadjusted colors. Schema and defaulting are straightforward, but visual regressions are possible across themes.

Overview
Adds a client setting for interface contrast (integers 50–200, default 100) with a live slider under Appearance, reset, and settings search.

Contrast is applied by writing --appearance-contrast-base/boost/border-boost on the document root. CSS then derives --contrast-* roles from semantic tokens (soften toward transparent, boost toward black/white) and the Tailwind color bridge, markdown, Clerk, menus, and preview annotation snapshots now read those roles instead of raw --foreground / --border. 100% leaves the theme unchanged; no filter on the app root.

Reviewed by Cursor Bugbot for commit 7bb0d15. Bugbot is set up for automated code reviews on this repo. Configure here.

Note

Add appearance contrast control with runtime CSS variable system

  • Adds an appearanceContrast integer setting (range 50–200, default 100) to ClientSettingsSchema and ClientSettingsPatch in settings.ts, with validation that rejects out-of-range or non-integer values.
  • Introduces applyAppearanceContrast(root, contrast) in appearanceContrast.ts, which sets --appearance-contrast-base, --appearance-contrast-boost, and --appearance-contrast-border-boost CSS variables on the target element.
  • Defines global contrast-computed tokens (--contrast-foreground, --contrast-border, --contrast-input, etc.) in index.css using color-mix functions that blend base roles with a target color according to the contrast variables.
  • Adds a Contrast slider to the Appearance settings panel in SettingsPanels.tsx and wires ContrastAppearanceSync in __root.tsx to keep document.documentElement in sync with the setting.
  • Updates many components (context menu, buttons, file browser, Clerk theme, markdown, annotations, etc.) to read contrast-adjusted tokens instead of base tokens.
  • Risk: any component or out-of-tree code that reads base role variables (--foreground, --border, --muted-foreground, --accent-foreground, --input) directly will no longer respond to contrast adjustments unless updated to the --contrast-* variants.
📊 Macroscope summarized 7bb0d15. 2 files reviewed, 1 issue evaluated, 1 issue filtered, 0 comments posted

🗂️ Filtered Issues

apps/web/src/browser/annotationTheme.ts — 0 comments posted, 1 evaluated, 1 filtered
  • line 13: readPreviewAnnotationTheme now reads the --contrast-* custom properties verbatim. Those properties are defined as color-mix(...) expressions containing references such as var(--foreground) and var(--appearance-contrast-base), and getComputedStyle(...).getPropertyValue() does not resolve the contents of a custom property to a final color. PickPreload later installs this string as --t3-foreground inside the unrelated preview page, where the referenced app theme variables are absent (or may have unrelated values), making the annotation overlay colors invalid or incorrect. This affects all six changed reads; they must be resolved to concrete colors before crossing into the preview document. [ Already posted ]

Co-authored-by: maria <254055478+maria-rcks@users.noreply.github.com>
@github-actions github-actions Bot added vouch:trusted PR author is trusted by repo permissions or the VOUCHED list. size:M 30-99 changed lines (additions + deletions). labels Aug 22, 2026
@github-actions

github-actions Bot commented Aug 22, 2026

Copy link
Copy Markdown
Contributor

Thread transfer impact

✅ Thread transfer remains within every enforced ceiling.

Provider Metric Main baseline This PR Impact PR ceiling
Codex Total thread wire 13.4 KiB 13.4 KiB −8 B (−0.1%) 15.1 KiB
Codex Thread snapshot wire 6.9 KiB 6.9 KiB −2 B (−0.0%) 7.3 KiB
Codex Live turn WebSocket wire 6.5 KiB 6.5 KiB −6 B (−0.1%) 7.8 KiB
Codex Live turn WebSocket decoded 55.0 KiB 55.0 KiB 0 B (0.0%) 66.4 KiB
Codex Live turn messages 16 16 0 (0.0%) 21
Claude Total thread wire 13.4 KiB 13.5 KiB +17 B (+0.1%) 15.1 KiB
Claude Thread snapshot wire 6.9 KiB 6.9 KiB +7 B (+0.1%) 7.3 KiB
Claude Live turn WebSocket wire 6.5 KiB 6.6 KiB +10 B (+0.1%) 7.8 KiB
Claude Live turn WebSocket decoded 55.8 KiB 55.8 KiB 0 B (0.0%) 66.4 KiB
Claude Live turn messages 16 16 0 (0.0%) 21

Baseline: 2c4158f · PR result: 7bb0d15 · Source CI: success

Scenario and decoded snapshot size

10 historical turns, 5 command tools per turn, 878.9 KiB retained MCP result per historical turn, and a 1.05 MiB retained result in the measured turn.

  • Codex decoded thread snapshot: 109.4 KiB
  • Claude decoded thread snapshot: 110.1 KiB

Updated in place by a trusted workflow. PR artifacts are strictly validated and never executed.

@macroscopeapp macroscopeapp Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed the appearance-contrast feature for UI consistency and ownership. One substantive concern with the page-wide filter on :root, plus one smaller duplication note in the settings panel. Details inline.

Posted via Macroscope — UI Consistency

Comment on lines +991 to +997
const appearanceContrastRatio =
(settings.appearanceContrast - MIN_APPEARANCE_CONTRAST) /
(MAX_APPEARANCE_CONTRAST - MIN_APPEARANCE_CONTRAST);
const appearanceContrastSliderStyle = {
"--settings-slider-progress": `${appearanceContrastRatio * 100}%`,
"--settings-slider-fill-offset": `${0.5 - appearanceContrastRatio}rem`,
} as CSSProperties;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is now the third call site (glass opacity, volume, contrast) duplicating the same slider treatment: the ratio math, the --settings-slider-progress / --settings-slider-fill-offset pair, the output badge classes, and the range-clamping onChange. The fill-offset geometry is durable and shared, so it would be better owned by a small SettingsSlider primitive (label, min, max, step, value, onValueChange) with the call site keeping only width and copy; otherwise the thumb geometry can drift between the three sliders as soon as one is tweaked. Not blocking on its own.

Posted via Macroscope — UI Consistency

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i checked the current tree and there are two settings-slider call sites: glass opacity and contrast. i kept the established local pattern rather than adding a shared api with only two owners. happy to extract it once a third live owner appears.

Comment thread apps/web/src/index.css Outdated
Comment thread apps/web/src/components/settings/SettingsPanels.tsx
@macroscopeapp

macroscopeapp Bot commented Aug 22, 2026

Copy link
Copy Markdown
Contributor

Approvability

Verdict: Skipped

Macroscope did not run approvability analysis for this PR. Macroscope could not determine whether this PR modifies its approvability configuration, so the PR was not approved automatically. A PR that may change the rules that govern approval is never approved automatically.

Co-authored-by: maria <254055478+maria-rcks@users.noreply.github.com>
@github-actions github-actions Bot added size:L 100-499 changed lines (additions + deletions). and removed size:M 30-99 changed lines (additions + deletions). labels Aug 22, 2026
Co-authored-by: maria <254055478+maria-rcks@users.noreply.github.com>

@macroscopeapp macroscopeapp Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Contrast is now derived from semantic tokens instead of a root filter, which resolves the earlier concern about glass backdrops, previews, and view-transition snapshots. Three issues remain in the new token math and its ownership.

Posted via Macroscope — UI Consistency

Comment thread apps/web/src/index.css Outdated
Comment thread apps/web/src/appearanceContrast.ts Outdated
Comment thread apps/web/src/index.css Outdated

@macroscopeapp macroscopeapp Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed the contrast setting against token ownership and theme behavior. The filter on the app root is gone — the semantic-token approach is the right shape — but the mix formula breaks translucent border/input tokens, and the overlay foreground tokens are left out of the setting. Details inline.

Posted via Macroscope — UI Consistency

Comment thread apps/web/src/index.css Outdated
Comment thread apps/web/src/appearanceContrast.ts Outdated
Comment thread apps/web/src/index.css Outdated

@cursor cursor Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes using high effort and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 5c8db7a. Configure here.

Comment thread apps/web/src/index.css
t3-code Bot and others added 2 commits August 22, 2026 15:33
Co-authored-by: maria <254055478+maria-rcks@users.noreply.github.com>
Co-authored-by: maria <254055478+maria-rcks@users.noreply.github.com>

@macroscopeapp macroscopeapp Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed the contrast token derivation, the Tailwind bridge, and the converted global/imperative role reads. The token ownership (ordinary --contrast-* custom properties, redeclared on [data-app-sidebar], alpha-preserving softening for --border/--input, per-surface foreground mixes) looks right. Two role reads were missed by the audit, so they stay at stock contrast while the chrome around them moves with the setting.

Posted via Macroscope — UI Consistency

Comment thread apps/web/src/index.css
Comment thread apps/web/src/index.css
Co-authored-by: maria <254055478+maria-rcks@users.noreply.github.com>

@macroscopeapp macroscopeapp Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One finding: the toolbar chrome tokens are the only role group that does not end up sharing the adjusted read, so themed documents keep the chat header and titlebar controls at stock contrast. Details inline.

Posted via Macroscope — UI Consistency

Comment thread apps/web/src/index.css Outdated
t3-code Bot and others added 2 commits August 22, 2026 16:15
Co-authored-by: maria <254055478+maria-rcks@users.noreply.github.com>
Co-authored-by: maria <254055478+maria-rcks@users.noreply.github.com>

@macroscopeapp macroscopeapp Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One remaining adjusted/unadjusted split: the desktop preview annotation overlay still snapshots the raw semantic roles, so that app chrome does not follow the new contrast setting. Everything else in the audit looks consistent — the Tailwind bridge, global chrome rules, markdown chrome, toolbar roles, and the imperative context-menu fallback all read the shared --contrast-* properties, and the border/input derivations now soften toward transparent so translucent hairlines keep their alpha.

Posted via Macroscope — UI Consistency

Comment thread apps/web/src/index.css
color-mix(in srgb, var(--border) var(--appearance-contrast-base), transparent),
var(--foreground) var(--appearance-contrast-border-boost)
);
--contrast-foreground: color-mix(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The desktop preview annotation overlay lands on the unadjusted side of this split. readPreviewAnnotationTheme() (apps/web/src/browser/annotationTheme.ts:13-23) snapshots --foreground, --popover-foreground, --muted-foreground, --accent-foreground, --border and --input off document.documentElement and ships them over IPC, where PickPreload maps them onto --t3-* for the annotation toolbar, editor popover, labels, and outlines — app chrome that otherwise mirrors the host theme role for role. Its MutationObserver on the style attribute does fire when applyAppearanceContrast writes, but the serialized payload is unchanged, so the overlay silently keeps stock contrast while the surrounding interface moves.

Suggest reading the adjusted roles for those six fields (--contrast-foreground, --contrast-popover-foreground, --contrast-muted-foreground, --contrast-accent-foreground, --contrast-border, --contrast-input) and leaving the surface roles as they are. The computed values come back as fully substituted color-mix() strings, and DesktopPreviewAnnotationThemeSchema accepts any string, so no contract change is needed.

Posted via Macroscope — UI Consistency

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixed in 7bb0d15. the preview annotation theme now snapshots the six adjusted chrome roles, and the macroscope agent now calls out renderer bridge snapshots in future audits.

Co-authored-by: maria <254055478+maria-rcks@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:L 100-499 changed lines (additions + deletions). vouch:trusted PR author is trusted by repo permissions or the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

0 participants