Skip to content

fix(web): finish contrast role audit

MacroscopeApp / Macroscope - UI Consistency failed Aug 22, 2026 in 7m 29s

UI Consistency: 1 issue found

apps/web/src/index.css — Toolbar chrome roles split into adjusted and unadjusted colors.

--toolbar-foreground, --toolbar-border, and --toolbar-control-foreground were switched to the new --contrast-* reads at L1394-1397, but those tokens are only ever read from html[data-theme-id] … selectors (chat header text L1748, toolbar control icon/border/text L1754-1757, panel and titlebar toggles L1769-1770, header separator L1783), and the themed block redeclares all three from raw --app-theme-* values at L1556-1559. Result: the adjusted values never reach a consumer in the default palette, and with any theme active the chat header, toolbar control icons/borders, and separators keep stock contrast while the sidebar, markdown chrome, and all Tailwind utilities follow the setting.

Suggested fix: keep --toolbar-* raw in both palettes and derive --contrast-toolbar-* in the contrast block (against --toolbar-background / --toolbar-control), then point the themed consumers at the adjusted names.

Details

Note

Your check run agent prompt is: .macroscope/check-run-agents/ui-consistency.md
More information about how Check Run Agents work can be found in our Help Center.

Verified as correct in this revision

  • Contrast is now expressed as ordinary custom properties (--contrast-*) declared on :root and redeclared on [data-app-sidebar], so the Tailwind bridge, global CSS, and the imperative style strings in contextMenuFallback.ts resolve the same adjusted role. No filter is applied to html, body, or the app root.
  • Alpha and surface ownership are preserved: --contrast-border / --contrast-input / --contrast-sidebar-border soften toward transparent and strengthen with a small semantic-foreground mix, and card, popover, accent, secondary, message, and sidebar foregrounds are each mixed against their own surface.
  • Role-read audit across apps/web/src: no remaining literal var(--foreground) / var(--border) / var(--muted-foreground) chrome reads outside the contrast definitions. The two splits flagged in earlier runs (t3-chat inline-code --message-foreground at L1826 and the theme inspector hover label --popover-foreground at L1727) now read the adjusted roles. --code-foreground / --terminal-foreground remain intentionally unadjusted, consistent with keeping code, terminals, and previews out of scope.
  • Theme scoping checked: html[data-theme-id]:not([data-theme-id=""]) and html[data-theme-id] [data-app-sidebar] override the base roles on elements where --contrast-* is also declared, so per-element substitution picks up themed and sidebar palettes correctly. No TSX or preview component overrides base role variables locally, so theme previews are unaffected.
  • Settings UI: the new row reuses SettingsRow, SettingResetButton, and the shared settings-slider treatment, is registered in SETTINGS_SEARCH_ITEMS, and the reset/restore paths include appearanceContrast. The earlier data-appearance-contrast no-op DOM write is gone; applyAppearanceContrast now writes only the three consumed custom properties.

Not re-reported

The shared-SettingsSlider-primitive suggestion (third call site duplicating the ratio math, slider CSS variables, and clamping onChange) was raised on an earlier revision of this PR and the code is unchanged, so it is not repeated here.


🔍 Agent activity — 22 tool calls

💭 I want to check whether the PR's move to --contrast-* custom properties actually resolved the two issues flagged in the earlier review at 4b52a03—the unadjusted contrast-message-foreground in t3-chat inline code and the inspector popover-foreground—now that the head is at 24ad866. I'll look through the current index.css for any remaining raw role reads that weren't converted.

1. git_tools — var(--(foreground|border|input|muted-foreground|popover-foreground|card-foreground|accent-foreground|secondary-foreground|message-foreground|sidebar-foreground|sidebar-muted-foreground|sidebar-border|placeholder|secondary-label|icon-muted))

2. git_tools — 4b52a03..24ad866

💭 Index.css now consistently uses contrast tokens, so the message-foreground and popover-foreground issues seem fixed—I want to double-check those two lines. I should also search apps/web/src outside of index.css for other places that reference raw color variables like var(--foreground) directly in strings or tsx literals.

3. git_tools — var(--(foreground|border|input|muted-foreground|popover-foreground|card-foreground|accent-foreground|secondary-foreground|message-foreground|sidebar-foreground|sidebar-muted-foreground|sidebar-border))

4. git_tools — contrast

💭 With the prior fixes seemingly holding, I'm now scanning for new issues, specifically checking whether the contrast token definitions declared on :root and [data-app-sidebar] correctly re-resolve their referenced role tokens like foreground, card, and popover when themes apply higher-specificity overrides. Actually since custom property substitution happens where the property is declared, if a theme overrides --foreground on a descendant element rather than :root, the --contrast-foreground computed at :root would still be locked to root's value and wouldn't pick up the descendant override through simple inheritance. I need to check whether other scopes like .message, theme-id selectors, or dialog scopes redeclare --card, --popover, --message-surface, --accent, and related tokens, and confirm --message-surface is actually defined at :root for the contrast calculation to work. I should scan more of index.css to trace these declarations.

5. browse_code — apps/web/src/index.css

6. browse_code — apps/web/src/index.css

💭 I'm checking whether the theme block on the html element conflicts with the contrast token declarations at :root — since they target different properties, there's no conflict, but I need to check if other scopes like message context override those contrast roles too.

7. git_tools — ^\s*--(foreground|border|input|muted-foreground|card|card-foreground|popover|popover-foreground|accent|accent-foreground|secondary|secondary-foreground|message-surface|message-foreground|sidebar|sidebar-foreground|sidebar-border|background):

💭 I'm tracing the cascade order between the sidebar-specific selector at line 1837 and the redeclared contrast tokens at line 1875, since both target the same element and specificity will determine which custom properties actually win for [data-app-sidebar]. Since --foreground and --contrast-foreground are separate properties, there's no direct conflict — both apply and the contrast calculation correctly picks up the sidebar's themed foreground value. But I'm now checking whether --sidebar-icon-color at line 94, which depends on --contrast-sidebar-muted-foreground, properly inherits the sidebar-scoped override rather than falling back to root's value. That said, this matches pre-existing behavior since the same dependency chain existed before the diff, so it's not a regression. The bigger concern is theme preview components that locally override role variables like --foreground and --card on a preview container — if utility classes now resolve through --color-foreground to --contrast-foreground inherited from root instead of respecting those local overrides, theme previews could break, so I want to search for those preview components.

8. git_tools — --(foreground|card-foreground|popover-foreground|muted-foreground|border|input|accent-foreground|secondary-foreground|message-foreground|sidebar-foreground|sidebar-border|placeholder|icon-muted|secondary-label)"?]?\s*[:=]

💭 I don't see local role overrides in the TSX, but I should check if theme preview components set app-theme variables locally on a container, since that override happens at a different element than where --foreground is computed on html, so it likely wouldn't cascade down anyway. I want to look at the theme preview code to confirm.

9. git_tools — --app-theme-

💭 I checked for theme preview components applying colors locally via inline style variables but found none, and confirmed applyThemePalette just sets --app-theme-* vars on root correctly. Now I'm digging into how --contrast-* variables are declared to spot a potential correctness issue. I'm checking whether these declarations sit inside a Tailwind @layer, since unlayered rules would override layered ones regardless of order, and the :root block doesn't appear layered either. I'm also tracing how the dark variant and contrast-foreground boost interact with theme IDs — since a theme could theoretically apply while the .dark class is independently toggled, which might cause the black/white contrast target to pick the wrong direction. Beyond that, I've confirmed my grep for var(--foreground) correctly caught the FilePreviewPanel usage even inside Tailwind arbitrary-value syntax, and that alpha-modified utility classes like text-foreground/70 resolve through the adjusted --color-foreground token, so those are fine. I'm now noting that apps/desktop is out of scope since only apps/web/src was included in the search, and moving on to check the settings slider component next. The prior review at commit e08faf0 already flagged duplicated slider code in SettingsPanels around line 996, recommending a shared SettingsSlider primitive—since that code is unchanged, I won't repeat that comment. I'm now examining the contrast token logic: --contrast-input and --contrast-border soften toward transparent while boosting via a foreground color-mix, with the sidebar variant using --sidebar-foreground instead, and I'm double-checking how the color-mix percentages interact with the appearance-contrast-base variable. I want to verify the color-mix syntax works correctly when the boost percentage is 0%—confirming that omitting or zeroing the second color's weight correctly falls back to 100% of the first color, and thinking through how alpha values get scaled when the percentages don't sum to 100. Mixing a translucent border with opaque foreground at 8.75% raises the resulting alpha noticeably, which fits the intended modest boost effect. I'm now checking whether other bridge entries in index.css, like accent, card, popover, muted, secondary, and background, still correctly stay as raw surfaces untouched by this treatment, and starting to look at whether the message surface variable needs similar handling. Code and terminal foreground variables that reference the base foreground directly seem intentionally left unadjusted, consistent with the idea that the filter shouldn't affect code or terminal content. Now I want to scan other component CSS files across the web app to see if similar patterns need the same fix.

10. browse_code — apps/web/src/**/*.css

11. browse_code — apps/web/src/index.css

💭 I'm confirming that the sidebar's contrast tokens correctly resolve against its own computed foreground since custom properties substitute per-element, so there's no specificity conflict with the theme-scoped selector. Now I'm checking how the dark mode scope applies within the sidebar itself. Since the sidebar dark variant is class-based rather than media-based, the target inheritance should stay consistent even with mismatched light/dark contexts. I'm satisfied that code-foreground stays unaffected by the contrast filter as intended, matching the design guidance for preview content. Now I want to double-check whether other components reference sidebar-scoped variables like muted-foreground or icon-muted that my earlier grep might have missed. The theme inspector might enumerate tokens like --foreground and report computed values, which could differ visibly once contrast is applied — worth a minor check. I should also verify other consumers expecting --color-foreground, though other packages reusing index.css are likely out of scope. Let me grep for any remaining role reads I haven't covered yet.

12. git_tools — var(--(placeholder|secondary-label|icon-muted|sidebar-muted-foreground|toolbar-foreground|toolbar-border))

13. git_tools — contrast-

💭 I'm checking the toolbar foreground token and confirming that CSS custom property resolution doesn't depend on declaration order, so the placement of the root block before the contrast declarations isn't an issue. I do notice the toolbar foreground gets overridden later by the themed block, so I need to trace that override further. Under html[data-theme-i], theme values override the toolbar tokens, so the chat header stays unadjusted while other themed chrome, like sidebar borders and markdown, does pick up contrast. That's a genuine split between adjusted and unadjusted chrome, but only affects users with a theme active rather than the default case. Before flagging this, I want to check the theme block more closely around lines 1552-1620 to see if other theme-scoped roles like the chat header or message surface/foreground also need contrast handling.

14. browse_code — apps/web/src/index.css

💭 I'm noticing that under a theme, the toolbar foreground and border variables use raw unadjusted theme values, while the default palette's versions get adjusted. Checking the consumer selectors, they're all scoped to html[data-theme-id], meaning they only kick in exactly when the unadjusted values are active. So practically, for any real theme, the chat header text/icons and toolbar borders end up ignoring the contrast setting, while sidebar, markdown, and other utilities still respect it — a genuine inconsistency worth flagging. The fix seems simple: apply the same mixing logic to the themed override so toolbar tokens stay consistent with the rest of the palette. I notice the same pattern repeats for --toolbar-control-foreground, which is adjusted by default but raw when a theme is applied — same underlying bug. I want to confirm the toolbar tokens aren't also referenced elsewhere, like in TSX files or through a Tailwind color bridge, since that would affect the scope of the fix.

15. git_tools — toolbar-(foreground|border|control-foreground)

💭 I've confirmed that the toolbar foreground/border/control-foreground tokens are only consumed by the themed html[data-theme-id] chat-header/titlebar rules, meaning the contrast mix added earlier never actually applies in themed mode since those values remain unadjusted. So the chat header, titlebar controls, and separators never respect the contrast setting when a theme is active. But I want to double-check whether that selector is truly unreachable — if data-theme-id gets set to an empty string for the default theme, the selector would still match and fall through to the default :root tokens. I need to check the theme-apply code to see whether it ever sets that attribute to an empty value or removes it entirely.

16. git_tools — data-theme-id|themeId

💭 I want to check themePalette.ts near line 1423 to see whether dataset.themeId gets cleared when there's no theme selected. If the default theme still sets a themeId like "default", then data-theme-id would always be present, meaning the toolbar's root contrast styling never applies and the header stays unadjusted.

17. browse_code — apps/web/src/themePalette.ts

💭 I should look for where dataset.themeId gets set or removed, likely in themeBoot or applyTheme.

18. git_tools — themeId(\s*=|])|delete .*themeId

💭 I'm confirming that without a palette selected, the data-theme-id attribute gets removed, so the toolbar consumer rules that depend on it never match. In the default case the toolbar tokens exist but sit unused since chat-header rules require that attribute, while with a theme active, those consumers pull from the raw app-theme variables for contrast. This creates a real split: the chat header and titlebar chrome stay at stock contrast while the sidebar, markdown, and utility text shift with the adjusted values. I checked whether this was flagged before—a prior review comment mentioned toolbar tokens keeping default contrast, but that was tied to older code before the contrast-token restructuring, so the current gap is a distinct, unaddressed issue worth calling out on the specific lines that were touched in this diff. I also want to check whether there are other findings worth surfacing: the sidebar icon color mixing looks like a pre-existing pattern rather than something new, the settings slider duplication was already flagged in an earlier commit, and the appearance-contrast function now looks cleaner since it writes only the three properties and dropped the old attribute. I'm also looking at how the border boost value is computed by dividing the contrast percentage by four, which produces non-integer results. I'm confirming the black/white keyword mix works fine in oklab, that the foreground softening mix correctly targets background rather than each surface's own color, and that glass/translucent surfaces like the popover aren't affected. Now I'm scanning for any roles missing from the bridge — checking whether things like message-action-foreground, primary-foreground, destructive-foreground, and the status foregrounds (error/warning/success/info) are left unadjusted, since those brand/status colors are likely intentional to leave alone. Now I'm double-checking the ChangedFilesTree dark mode override and its background mix percentage, and verifying the settings UI slider and glass-opacity row styling. I want to confirm the newly added SettingsRow actually includes a visible label like its sibling rows do, since it currently only shows a description and might be missing a title compared to the neighboring glass-opacity row.

19. browse_code — apps/web/src/components/settings/SettingsPanels.tsx

💭 I'm wondering where the label for the contrast setting comes from — likely searchableSetting pulls the title from SETTINGS_SEARCH_ITEMS, so I should check its implementation to confirm.

20. git_tools — export function searchableSetting

💭 The title comes from the catalog labeled "Contrast", with the id "setting-appearance-contrast" and slider input id "appearance-contrast", linking to the appearance settings anchor. I'm double-checking that the settings-slider CSS variables for progress tracking still have that duplication issue I noticed earlier. I'm also verifying the contrast math handles values above 100 correctly, capping the base at 100% while letting the boost value increase separately. The inline styles set on the document element take highest CSS precedence, so the sidebar's contrast variables inherit properly, and theme preview swatches appear to use raw color values rather than semantic roles, meaning the contrast setting won't distort how those previews render. Now I'm checking whether --color-foreground gets used elsewhere in the theme system in ways where the contrast boost could misapply, like solid-background buttons or avatars—concluding that shift is expected behavior for a contrast setting rather than a bug. I'm also tracing whether --contrast-foreground on the sidebar could create a circular dependency, since it references both --foreground and --background while also being consumed by other sidebar variables. I trace through --sidebar-icon-color, --contrast-sidebar-muted-foreground, and --contrast-sidebar-border to confirm none of these dependency chains loop back on themselves — each resolves cleanly down to base tokens like --muted-foreground and --border. I'm now checking whether any other contrast token reads from a value that itself depends on --sidebar-row-hover's contrast-foreground reference, to rule out a subtler cycle. The main confirmed issue remains the toolbar/chat-header split I found earlier — button.tsx defaults to --contrast-muted-foreground but the themed header overrides it with --toolbar-control-foreground, and the same pattern shows up in [data-toolbar-control]. I'm now looking into a second potential issue around text-secondary-label. Actually, I realize --border/--foreground do get adjusted since contrast reads from themed --border, so themes are mostly fine there — it's specifically the tokens reading --app-theme-* directly, like toolbar and composer outline, that skip the contrast layer. I'll narrow my comment to the toolbar token lines that were actually changed in the diff, keeping mention of the composer outline as secondary context. The real issue is that the changed lines only affect default-palette toolbar tokens, but those tokens are exclusively consumed in themed scopes where they get overwritten by raw --app-theme-* values anyway, making the fix inert. The correct approach is to keep the toolbar tokens raw, add adjusted variants inside the contrast/themed block, and update the consumer lines to read those adjusted values instead. Let me also verify the theme-id selector doesn't accidentally match when the attribute is absent, and check whether anything outside the web app -- like the desktop titlebar CSS -- also depends on these toolbar foreground/background variables.

21. browse_code — Search the whole repository (outside apps/web/src) for any CSS or TS/TSX that reads the custom properties --toolbar-foreground, --toolbar-border, or --toolbar-control-foreground (e.g. in apps/desktop, packages/*, e2e tests). List file paths and the exact lines. Also state whether any selector outside html[data-theme-id] ... in apps/web/src/index.css reads them.

💭 That's a solid, concrete finding. Now I'll place the inline comment on apps/web/src/index.css spanning lines 1394-1397, since that range falls within the changed diff hunk with line 1396 as unchanged context. I'm drafting the comment body explaining that --toolbar-foreground, --toolbar-border, and --toolbar-control-foreground only apply under the themed selector for chat header and titlebar controls, but get redeclared from raw values elsewhere, meaning the contrast adjustment never actually reaches those elements while other UI does follow it. I'll suggest deriving dedicated --contrast-toolbar-* variables in the contrast block so both palettes stay consistent, and skip providing a diff since the fix touches multiple hunks — though I'm considering whether a small illustrative snippet would help. I confirm the header buttons and icon colors are indeed unadjusted while message text follows the setting, so this is a real inconsistency worth flagging as a failing finding, and I'll go ahead and post the review.

22. modify_pr — 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.


Agent Credits: 548 credits