feat(web): add appearance contrast control - #7906
Conversation
Co-authored-by: maria <254055478+maria-rcks@users.noreply.github.com>
Thread transfer impact✅ Thread transfer remains within every enforced ceiling.
Baseline: Scenario and decoded snapshot size10 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.
Updated in place by a trusted workflow. PR artifacts are strictly validated and never executed. |
There was a problem hiding this comment.
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
| 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; |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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.
ApprovabilityVerdict: 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>
Co-authored-by: maria <254055478+maria-rcks@users.noreply.github.com>
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using high effort and found 1 potential issue.
❌ 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.
Co-authored-by: maria <254055478+maria-rcks@users.noreply.github.com>
Co-authored-by: maria <254055478+maria-rcks@users.noreply.github.com>
There was a problem hiding this comment.
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
Co-authored-by: maria <254055478+maria-rcks@users.noreply.github.com>
There was a problem hiding this comment.
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
Co-authored-by: maria <254055478+maria-rcks@users.noreply.github.com>
Co-authored-by: maria <254055478+maria-rcks@users.noreply.github.com>
There was a problem hiding this comment.
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
| color-mix(in srgb, var(--border) var(--appearance-contrast-base), transparent), | ||
| var(--foreground) var(--appearance-contrast-border-boost) | ||
| ); | ||
| --contrast-foreground: color-mix( |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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>

problem
theme contrast was fixed, so users could not make interface colors and borders softer or stronger.
change
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.tsvp run --filter @t3tools/contracts typecheckvp run --filter @t3tools/web typecheckvp run --filter @t3tools/desktop typecheckvp run --filter @t3tools/web buildbuilt 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-booston 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; nofilteron 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
appearanceContrastinteger setting (range 50–200, default 100) toClientSettingsSchemaandClientSettingsPatchin settings.ts, with validation that rejects out-of-range or non-integer values.applyAppearanceContrast(root, contrast)in appearanceContrast.ts, which sets--appearance-contrast-base,--appearance-contrast-boost, and--appearance-contrast-border-boostCSS variables on the target element.--contrast-foreground,--contrast-border,--contrast-input, etc.) in index.css usingcolor-mixfunctions that blend base roles with a target color according to the contrast variables.ContrastAppearanceSyncin __root.tsx to keepdocument.documentElementin sync with the setting.--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
readPreviewAnnotationThemenow reads the--contrast-*custom properties verbatim. Those properties are defined ascolor-mix(...)expressions containing references such asvar(--foreground)andvar(--appearance-contrast-base), andgetComputedStyle(...).getPropertyValue()does not resolve the contents of a custom property to a final color.PickPreloadlater installs this string as--t3-foregroundinside 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 ]