Skip to content

fix(web): delete vestigial AppRootProvider and fix theme selector mismatch#31324

Merged
ashleeradka merged 2 commits into
mainfrom
devin/1779302850-lum-1711-remove-app-root-provider
May 20, 2026
Merged

fix(web): delete vestigial AppRootProvider and fix theme selector mismatch#31324
ashleeradka merged 2 commits into
mainfrom
devin/1779302850-lum-1711-remove-app-root-provider

Conversation

@ashleeradka
Copy link
Copy Markdown
Contributor

@ashleeradka ashleeradka commented May 20, 2026

Prompt / plan

Inventory and fix vestigial platform-era patterns in the web app. LUM-1711 identified AppRootProvider as never mounted; investigation revealed a second bug — theme selectors were misaligned between runtime and design library.

Closes LUM-1711

What

Deletes the vestigial AppRootProvider / useAppRootContainer() pattern and fixes the theme selector mismatch between applyThemePreference() and tokens.css.

Why

Two bugs, both caused by blindly porting platform repo patterns into the new Vite SPA without verifying they still apply:

Bug 1: 6 portal-based components silently rendering nothing

AppRootProvider provides a scoped DOM element for createPortal(). It was ported from the platform repo but never mounted in the route tree. All 6 consumers called useAppRootContainer() → got null → hit if (!portalTarget) return null guards → silently rendered nothing:

  • TrustRulesModal
  • TrustRuleFormModal
  • AttachmentPreviewModal
  • AvatarManagementModal
  • CommandPalette (desktop overlay)
  • ContextWindowIndicator (tooltip)

The provider existed so portals would land inside a .app-root div where theme CSS variables resolve. In the new repo, all theme tokens are on :root / [data-theme] — there is no .app-root CSS scoping. Portals to document.body inherit all tokens via normal CSS inheritance. The provider is unnecessary.

Bug 2: Dark/velvet mode completely broken

applyThemePreference() was copied verbatim from the platform repo using classList.toggle("dark"). But tokens.css in @vellum/design-library expects [data-theme="dark"] attribute selectors, and the Tailwind v4 dark variant is wired to the same attribute:

@custom-variant dark (&:where([data-theme=dark], [data-theme=dark] *));

No code anywhere set the data-theme attribute → all dark/velvet CSS custom properties never activated → all 304 dark: Tailwind utility classes were non-functional.

Changes

Portal fix (commit 1):

  • Delete apps/web/src/components/app-root-context.tsx
  • Update 6 consumers: remove useAppRootContainer(), portal to document.body, remove dead !portalTarget null guards

Theme fix (commit 2):

  • applyThemePreference(): set data-theme attribute on <html> instead of toggling CSS classes — aligns with tokens.css [data-theme="dark"] / [data-theme="velvet"] selectors
  • Two document viewer files: check dataset.theme instead of classList.contains("dark")
  • Two OAuth pages: update :root.dark CSS selector to :root[data-theme="dark"]

Safety

  • All theme token inheritance verified: :root and [data-theme] selectors in tokens.css apply to <html>, so document.body and all descendants inherit them
  • font-sans is set on <body> via root-layout.tsx — portaled content inherits it
  • No .app-root CSS rules exist anywhere in the codebase
  • data-theme is the canonical selector strategy per tokens.css, @custom-variant dark, and the design library's Storybook config

Root cause analysis

  1. How did this happen? Both patterns were ported from the platform repo during the web app migration without verifying they were correct for the new stack. The platform repo uses appTheme.css with class-based dark mode (.dark); the new repo uses tokens.css with attribute-based dark mode ([data-theme]). The provider was ported but never wired into the route tree.
  2. What led to it? Treating migration as copy-paste rather than evaluating each pattern against the new repo's architecture. The platform repo's appTheme.css class-based selectors and the design library's tokens.css attribute-based selectors are two different systems that were never reconciled.
  3. Warning signs? tokens.css documents data-theme as the theming mechanism. applyThemePreference() doesn't set it. The mismatch is visible by reading both files.
  4. Prevention: Ported code must be verified against the new repo's conventions — CONVENTIONS.md, STYLE_GUIDE.md, tokens.css, and AGENTS.md are the source of truth, not the platform repo.

Test plan

  • CI (lint, typecheck, build)
  • End-to-end verification pending: modals/popovers render, dark mode tokens activate

References

Link to Devin session: https://app.devin.ai/sessions/0a7470e554b2479db3dd1f2bb8f5a53d
Requested by: @ashleeradka


Open in Devin Review

…to portal to document.body

LUM-1711

AppRootProvider was carried from the platform repo but serves no purpose in
the new stack — all theme CSS variables are defined on :root via tokens.css
and applied to document.documentElement by applyThemePreference(). Zero CSS
rules target .app-root. Portals to document.body inherit all theme tokens.

The provider was never mounted, so 6 consumers that depended on
useAppRootContainer() were silently broken (returning null, causing modals
and popovers to not render).

- Delete app-root-context.tsx (AppRootProvider + useAppRootContainer hook)
- Update 6 consumers to portal to document.body instead
- Remove dead if-null-return-null guards that prevented rendering

Co-Authored-By: ashlee@vellum.ai <ashlee@vellum.ai>
@devin-ai-integration
Copy link
Copy Markdown
Contributor

🤖 Devin AI Engineer

I'll be helping with this pull request! Here's what you should know:

✅ I will automatically:

  • Address comments on this PR. Add '(aside)' to your comment to have me ignore it.
  • Look at CI failures and help fix them

Note: I can only respond to comments from users who have write access to this repository.

⚙️ Control Options:

  • Disable automatic comment and CI monitoring

@linear
Copy link
Copy Markdown

linear Bot commented May 20, 2026

LUM-1711

Copy link
Copy Markdown
Contributor

@devin-ai-integration devin-ai-integration Bot left a comment

Choose a reason for hiding this comment

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

Devin Review found 1 potential issue.

View 2 additional findings in Devin Review.

Open in Devin Review

/>
</div>,
portalTarget,
document.body,
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.

🚩 Future dark/velvet theme support requires data-theme on , not a scoped element

The CSS in tokens.css defines dark and velvet themes via [data-theme="dark"] and [data-theme="velvet"] selectors. Currently no data-theme attribute is set anywhere in the app (index.html has a bare <html lang="en">). When theme switching is implemented, the data-theme attribute must be placed on <html> (or at minimum an ancestor of <body>) for portaled content on document.body to inherit the correct theme tokens. If it were placed on an inner wrapper element (as .app-root would have been), portals to document.body would fall back to light-mode :root values. This is not a current bug but a constraint worth documenting.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

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 a pre-existing architectural detail, not introduced by this PR.

The constraint the bot identifies is already satisfied by the current setup: applyThemePreference() applies theme classes to document.documentElement (the <html> element), and document.body is a child of <html>, so portals to document.body correctly inherit all theme tokens via CSS inheritance regardless of whether theming uses data-theme attributes or CSS classes.

Worth noting: there's a pre-existing gap between tokens.css (which uses [data-theme="dark"] selectors) and applyThemePreference() (which toggles CSS classes like .dark), but that's unrelated to this PR's portal target change.

vex-assistant-bot[bot]
vex-assistant-bot Bot previously approved these changes May 20, 2026
Copy link
Copy Markdown
Contributor

@vex-assistant-bot vex-assistant-bot Bot left a comment

Choose a reason for hiding this comment

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

APPROVE

What this does: Deletes AppRootProvider/useAppRootContainer (ported from the platform repo but never wired up here), removes the dead null guards that were silently preventing 6 components from rendering, and migrates all portal consumers to document.body.

This is more of a bug fix than a refactor — command-palette, trust-rules-modal, trust-rule-form-modal, avatar-management-modal, AttachmentPreviewModal, and context-window-indicator tooltip were all rendering null 100% of the time because AppRootProvider was never mounted in the route tree.


Deletion safety verified:

  • app-root-context.tsx gone at HEAD ✅
  • tokens.css has zero .app-root occurrences — all CSS custom properties (--surface-lift, --border-base, etc.) are defined on :root, so portals to document.body inherit them via normal CSS inheritance ✅
  • AppRootProvider was never mounted in main.tsx, root-layout.tsx, or the provider stack — the null guards were therefore always firing ✅
  • No remaining imports of app-root-context anywhere in apps/web/src/

The old ContextWindowIndicator comment ("portal into .app-root so appTheme.css tokens resolve") was describing the platform repo's scoping model, not this one. :root scoping in the Vite SPA means the comment was cargo-culted and the AppRootProvider was never necessary.


Devin finding:

Devin flagged that dark/velvet theme support requires data-theme on <html> for portals to document.body to inherit the correct tokens. Self-resolved: applyThemePreference() targets document.documentElement (the <html> element), so document.body is a child and inherits theme tokens regardless. ✅

Devin also noted a pre-existing gap: tokens.css uses [data-theme="dark"] selectors while applyThemePreference() toggles CSS class names like .dark. These two systems are misaligned — one of them doesn't apply. Worth a follow-up audit/ticket to reconcile which selector strategy is canonical and align the two. Not introduced by this PR.


Pattern check:

All 6 consumers now follow the same pattern (createPortal(..., document.body)) — consistent and idiomatic. The if (!open) guards remain where needed to prevent unnecessary portal creation, but the !portalTarget guard is correctly gone. ✅

Reviewed at 67a60c92.

applyThemePreference() was ported from the platform repo using
classList.toggle('dark'), but tokens.css expects [data-theme='dark']
attribute selectors. This meant dark/velvet mode tokens and all 304
dark: Tailwind utilities never activated.

- Set data-theme attribute on <html> instead of toggling CSS classes
- Update document viewer isDark checks to use dataset.theme
- Update OAuth page CSS from :root.dark to :root[data-theme='dark']

Co-Authored-By: ashlee@vellum.ai <ashlee@vellum.ai>
@devin-ai-integration devin-ai-integration Bot changed the title refactor(web): delete vestigial AppRootProvider and update portal consumers fix(web): delete vestigial AppRootProvider and fix theme selector mismatch May 20, 2026
@ashleeradka ashleeradka merged commit 91596f3 into main May 20, 2026
7 checks passed
@ashleeradka ashleeradka deleted the devin/1779302850-lum-1711-remove-app-root-provider branch May 20, 2026 19:40
@devin-ai-integration
Copy link
Copy Markdown
Contributor

Test Results — LUM-1711

Session: https://app.devin.ai/sessions/0a7470e554b2479db3dd1f2bb8f5a53d
Tested on: main branch (post-merge), Vite dev server on localhost:3333

Summary

Ran Vite dev server locally. Tested theme selector fix by toggling data-theme attribute and calling applyThemePreference() on the login page. All theme tests passed. Portal fix (modals/command palette) requires auth/backend — verified by code review only.

Results

Theme selector fix (3/3 passed)
  • It should activate dark mode via data-theme attribute — passed. data-theme="dark" on <html> changed --surface-base from #F6F5F4#17191C, --content-default#F6F5F4. Full visual change confirmed.
  • It should toggle themes via applyThemePreference() — passed. applyThemePreference("dark") sets data-theme="dark" attribute (NOT .dark class). Round-trip light→dark→light works correctly.
  • Old classList.add("dark") pattern confirmed broken — passed. Adding .dark class has zero effect on CSS variables — --surface-base stays #F6F5F4 (light). Proves the old code was completely non-functional.
Portal fix (untested — requires auth)

6 consumers (TrustRulesModal, TrustRuleFormModal, AttachmentPreviewModal, AvatarManagementModal, CommandPalette, ContextWindowIndicator) changed from if (!portalTarget) return null → always render with container={document.body}. Structural correctness verified by code review. End-to-end verification requires being logged into the app with a running backend.

Console Evidence

After applyThemePreference("dark"):
  dataTheme: dark | hasDarkClass: false | surfaceBase: #17191C

After applyThemePreference("light"):
  dataTheme: light | surfaceBase: #F6F5F4

Old pattern classList.add("dark"):
  dataTheme: null | hasDarkClass: true | surfaceBase: #F6F5F4 ← still light!

ashleeradka added a commit that referenced this pull request May 20, 2026
…rollbars (#31387)

The initial port of platform appTheme.css + globals.css to
`packages/design-library/src/tokens.css` only bridged
`--color-background`, `--color-foreground`, `--font-sans`, and the
radius scale — but ~500 utility uses across the web app and design
library reference moss/forest/danger/amber/stone Tailwind color scales
and a handful of layout/animation classes that weren't carried over.
After #31324 fixed the `dark:` selector wiring, those utilities
activated but resolved to undefined variables, so half the palette
silently no-op'd.

Tier 1 — restore missing tokens in `tokens.css`:
- Color scales (moss/stone/forest/emerald/danger/amber) with Figma hex
  values inside `@theme inline` so Tailwind generates utilities
- Font tokens (`--font-mono`, `--font-serif`, `--font-display`, `--font-body`)
- Shadow scale (`--shadow-sm/md/lg/glow/accent-glow`)
- `--app-spacing-xxs..xxxl`
- `--chat-max-width: 800px`
- Missing velvet tokens (`--system-info-*`, `--feed-*`)
- Explicit dark/velvet `--ring` so it tracks per-mode positive accent
  even if `data-theme` ever moves off the root

Tier 2 — restore missing app-level classes in `apps/web/src/index.css`:
- `.busy-indicator` + `busy-pulse` keyframe
- `.onboarding-avatar-pulse/awake/failed` + `morphPulse` keyframe
- `.app-shell` / `.app-shell-main` layout guards
- Themed scrollbars rekeyed to `data-theme` (was `.dark .app-root`)
- `prefers-reduced-motion` overrides for busy + typing indicators

Misc:
- Fix `--app-spacing-2xl` → `--app-spacing-xxl` typo in home-page.tsx
- Fix stale "defined in appTheme.css" comment in busy-indicator.tsx
- Drop "Keep in sync with platform appTheme.css" header — direction
  reversed post-cutover

Architectural follow-up (NOT in this PR): a chunk of the restored
color-scale utilities could collapse to semantic surface tokens
(`bg-[var(--surface-lift)]` instead of `bg-stone-100 dark:bg-moss-700`).
That's a ~500-site refactor and a separate decision; tracking
separately.

Co-authored-by: Claude <noreply@anthropic.com>
ashleeradka added a commit that referenced this pull request May 20, 2026
…1389)

PR #31324 deleted AppRootProvider after confirming zero CSS rules
target `.app-root` in this repo — theme tokens live on `:root` and
apply via `data-theme` on `<html>`. Two `className="app-root"` strings
survived that cleanup and are dead (target no CSS rule). The
portal-container docstring example also still referenced the old
wrapper pattern; it now reflects current conventions where overlays
fall back to `document.body` directly and the provider is reserved
for nested overlay scoping (dialogs, shadow DOM).

Closes LUM-1769

Co-authored-by: Claude <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant