Skip to content

editor: light chrome that follows the host's theme - #1041

Merged
Aymericr merged 8 commits into
mainfrom
feedback/interface-theme
Oct 13, 2026
Merged

Aymericr merged 8 commits into
mainfrom
feedback/interface-theme

Conversation

@Aymericr

@Aymericr Aymericr commented Oct 12, 2026 •

Copy link
Copy Markdown
Contributor

What

The editor no longer forces its chrome dark. It sets no theme of its own and follows the host's .dark class, so a host can offer a light editor.

  • No forced dark. The document.body.classList.add('dark') mount effect is gone, and so is dark on the v1, v2 and mobile layout roots, the app sidebar, the mobile panel sheet and the riser diagram panel.
  • Light counterparts, dark unchanged. About 75 editor and nodes files: every dark-only chrome literal gets a light base, and the original value stays under dark:. For example, bg-[#2C2C2E] becomes bg-black/4 dark:bg-[#2C2C2E], white tints become black tints, and pale inks become -700/-800 in light. The riser diagram's waste and vent colours are CSS variables with a darker light value.
  • Caller overrides still win.
    • ActionButton (controls) runs its fill through custom properties, so a caller's bg-* or hover:bg-* (red delete hovers, bg-primary buttons) replaces it in both themes.
    • Ghost-variant dock buttons leave the dark hover to the variant, as before.
    • Where a caller resets a state under disabled:hover:, the reset is repeated under dark:disabled:hover:, because dark:hover:* ties with it and is emitted later.
  • Rail selection. The selected and hover states use bg-sidebar-accent. It equals accent in dark and shows on the light sidebar.
  • What stays dark in either theme:
    • the Preview stage, the walkthrough HUD and the viewer scene header;
    • the controls bar and the stage switcher;
    • the controls bar's dropdown menus and tooltips, which portal to <body> (the action-menu ActionButton takes a tooltipClassName for this);
    • the snapshot viewfinder, including its level picker;
    • the scene-graph viewer dialog, which draws its own dark viewer;
    • the Point and ask layer over the canvas, which draws its own dark colours.
  • Interface theme row. InterfaceThemeRow ({ value, onChange }) is the System / Light / Dark "Interface theme" row, exported with InterfaceTheme like SettingsSwitchRow. A host that owns the setting passes it through SettingsPanel's preferences slot; a host that passes nothing gets no row. SegmentedControl buttons report aria-pressed.
  • Bundle 2's new controls. The Point and ask button, the Follow Pascal toggle and its "Resume following" pill, and the window inspector's Wrap / Unwrap corner buttons get light values, with their dark values kept under dark:. Follow Pascal also sits in the Preview controls bar, so it takes a tooltipClassName and the bar passes dark.
  • "Scene theme". The viewer's Display submenu now reads "Scene theme", so it isn't confused with the interface theme.
  • The open-source app keeps its look. SceneLoader is the one page that mounts the editor. Its root carries dark from the first paint, and it adds dark to <body> while mounted for the menus the editor portals there, as the editor used to do itself. The home, login, settings and setup pages keep their own theme.
  • Test hygiene. lib/sfx-player.test.ts sets the real audio store's state instead of mocking ../store/use-audio for the whole process.
  • Wiki: wiki/architecture/materials-and-themes.md.

Why

Owner feedback of 2026-10-12, row D2 (verbatim):

D2 · "introduce light mode for the editor (default system sync, allow override reuse theme config from the main app nav pages)"

This is slice E-theme of the feedback plan: "The host owns the theme: no forced dark, light counterparts for every dark-only literal (dark stays pixel-identical), an "Interface theme" row in Settings". It follows the plan's decision "Light mode: Studio stays dark; presentation HUDs stay dark; the 3D scene keeps its own theme."

How to test

  1. Open a project in a host that sets no .dark class: the chrome is light. That covers the navbar slot, rail, panels, inspectors, action dock and its popovers, level selector, view bar, split view, helper HUD and dialogs. With dark on <html> or <body>, it renders as before.
  2. In dark, check these computed values:
    • an inspector ActionButton is rgb(44, 44, 46) and hovers to rgb(62, 62, 62);
    • its Delete variant hovers red (red-500/20);
    • the slab and ceiling bg-primary buttons stay primary;
    • a disabled segment has no hover tint;
    • a block's disabled slot delete stays muted on hover;
    • the dock's camera buttons hover to accent/50, as before.
  3. In light, select an elevator. Turn on Service and Disabled for a level and look at the current Destination tile: every label reads. The storey heights in the level selector read too.
  4. In light, take a snapshot (⌘K → "Take Snapshot") on a building with two levels: "Levels in the shot" is dark, like the rest of the viewfinder. In Preview, the controls' tooltips look the same in both themes.
  5. Pass <InterfaceThemeRow value=… onChange=… /> through SettingsPanel's preferences: Preferences shows the row with the current value pressed, after the construction animation. Without it there is no row.
  6. In light, with a host that answers Point and ask: the Point and ask button (C) and Follow Pascal show a lavender or violet tint with a deep ink when on, and a readable ink on hover when off. In dark they look as on main.
  7. Open-source app: open /project/<id>. The editor and its menus are dark; / and /settings are not.

Checks run on this head (beef29f, on editor main 35a4905), from the hosted worktree root (the editor is its submodule):

  • bunx turbo run check-types --filter='./editor/packages/*': 9/9 passed.
  • Biome with the editor's config on the 47 changed files: clean.
  • bun test editor/packages/nodes/src: 4797 pass, 0 fail.
  • bun test editor/packages/editor/src: 2432 pass, 19 fail. All 19 are lib/history.test.ts subprocess checks: run from the host root, their stderr carries three's THREE_CJS_DEPRECATED warning. This branch touches no history code.
  • bun test editor/packages/editor/src/components/ui/sidebar/panels/settings-panel: 7 pass.

Visual QA. I ran the hosted app with this branch in light and dark, at 1440 px and 390 px. The first pass covered every surface in step 1, plus the profile menu, command palette, Studio and Preview. The review round (below) found more editor defects, now fixed and re-checked in the running app:

  • elevator access and destination states;
  • tile surfaces and storey heights;
  • the snapshot viewfinder's level picker;
  • Preview tooltips;
  • the "Scene theme" label.

The riser diagram can't be opened from the hosted UI, so its colour variables were checked by computed style: light rgb(3, 105, 161) and rgb(147, 51, 234); dark #0ea5e9 and #a855f7 as before. Dark crops of the sidebar, level selector, navbar and dock are pixel-identical to the screenshots taken before the fixes.

After the rebase on Bundle 2 I re-checked the new controls in the running hosted app, in both themes: the dock with Point and ask on and Follow Pascal on, off and hovered, and Settings → Preferences with the construction animation, the theme row and the host's rows. Computed values: Point and ask on is rgb(93, 73, 223) on #a99bff at 20% in light and rgb(201, 193, 255) in dark, as on main; hovered it takes the ghost variant's accent fill and ink in both themes, as on main.

Screenshots / screen recording

Local QA screenshots for both themes are listed in pascalorg/private-editor#901.

Review

Reviewed adversarially by Claude and GPT-6 Astra.

Fixed:

  • Elevator selected states kept their dark-only -100 inks and were invisible in light. Each now has a -700 or -800 light ink.
  • The block slot's disabled delete turned red on hover in dark. It now has a dark:disabled:hover: reset.
  • Warning and accent inks (solar panel, cabinet, item, floor plan notice, measurement, panel extensions, property line) are -700 in light. At -600 they fell below 4.5:1.
  • The snapshot viewfinder carries dark, so the level picker matches it. The picker's light variant is dropped.
  • Viewer tooltips in Preview get dark through tooltipClassName.
  • The riser diagram's waste and vent colours have darker light values.
  • Tiles (build tools, item categories, tool options) get a light surface.
  • Storey heights read in light.
  • "Scene theme". The viewer's Display submenu is renamed.
  • The SFX player test no longer leaks its audio-store mock.
  • The endpoint drag pill, found while checking the elevator, reads in light.
  • No light flash in the open-source app (Cursor Bugbot): the editor page's root carries dark, so its first paint is dark.

Not changed:

  • The rail's selected fill: it keeps bg-sidebar-accent. The active tab already changes its icon from grey to full colour and adds a shadow, so selection does not depend on the tint.

Deployment notes

  • Merge this first. pascalorg/private-editor#901 is re-bumped to this PR's merge commit when it lands.
  • Hosted pin. Hosted main now pins editor main (35a4905), so Add "Mirror" option for stairs (Cut Back Stairs) #901 pins this branch's head (beef29f) directly. The old feedback/interface-theme-pin branch is no longer used.
  • Hosts. A host that renders the editor and wants it dark must put dark on an ancestor of both the editor and its portals (<body> or <html>). A host that sets nothing now gets light chrome.
  • Env vars: none. Migrations: none.

Reconciled with Bundle 2

Rebased on editor main 35a4905: #1024 ("Bundle 2 editing, interior captures and native materials") and #1042 (room labels), which merge without conflicts beyond the one below.

  • Dropped: the interfaceTheme prop on SettingsPanel. Bundle 2 added a host slot for Preferences (preferences, with an exported SettingsSwitchRow), so the theme row became an exported InterfaceThemeRow that hosts pass through that slot. That leaves one extension point. The Preferences description stays Bundle 2's, since the open-source app shows no theme row.
  • Kept: everything else. Bundle 2 ships no light chrome, so no forced dark, the light counterparts, the ActionButton custom properties, the dark scopes and the open-source app's dark editor page are all still this PR's.
  • Added: a light pass over the chrome Bundle 2 brought in (Point and ask, Follow Pascal and its pill, the window corner buttons). The dark-only literal sweep over editor main found nothing else: the Point and ask layer is a self-contained dark overlay, like the HUDs, and the other hits are 3D materials and fixtures.
  • Test fixes from the merge: the theme row test now reads the row's own pressed value, because Bundle 2's construction animation control is a SegmentedControl that also reports aria-pressed. Bundle 2's new muted-startup SFX test sets the real audio store's state, like the rest of that file.

Verified in the hosted app after merge

I checked this on private-editor main c5741f613, which pins c69d1d3, in light and dark. Screenshots are in /tmp/feedback-work/theme/reconciled/:

  • Point and ask, Follow Pascal (off, on and hovered) and the dark Preview bar (06-* to 09-*).
  • The window inspector's Wrap / Unwrap buttons (10-*) and the multi-selection Merge windows block (11-*).
  • The Settings → Preferences row order (03-*).
  • The inspector's dark values, which are unchanged (21-*).

One defect, in the command palette rather than in this PR's code: a search left groups in the order they were registered, so theme listed the theme commands fifth. The fix is #1052.

Supersedes

Nothing.

Checklist

  • I've tested this locally with bun dev (not run for the open-source app; QA ran in the hosted app)
  • My code follows the existing code style (Biome with the editor's config on the changed files)
  • I've updated relevant documentation (if applicable)
  • This PR targets the main branch

🤖 Generated with Claude Code


Note

Cursor Bugbot is generating a summary for commit b691909. Configure here.

@cursor cursor Bot left a comment

Copy link
Copy Markdown

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 2 potential issues.

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 167d936. Configure here.

Comment thread apps/editor/components/scene-loader.tsx
Comment thread packages/nodes/src/elevator/panel.tsx
@Aymericr
Aymericr force-pushed the feedback/interface-theme branch 3 times, most recently from 0d3e9af to b691909 Compare October 13, 2026 15:56
Aymericr and others added 8 commits October 13, 2026 11:59
The editor forced its chrome dark: a mount effect put `dark` on <body> and
the v1, v2, mobile, sidebar, mobile-sheet and riser roots carried it too, so
a host's light theme never reached the editor or its portals. The editor now
sets no theme; it resolves against the host's `.dark` ancestor.

Every dark-only chrome literal gets a light base with the original kept under
`dark:` (bg-[#2C2C2E] -> bg-black/4 dark:bg-[#2C2C2E], white tints -> black
tints, 300/400 inks -> 600/700), so dark renders as before. Where a class is
merged with a caller's override, the pair is arranged so the override still
wins: ActionButton's fill runs through custom properties, ghost-button callers
leave the dark hover to the variant, and the mobile delete hover and disabled
segments carry their own dark twin. The rail's selection uses sidebar-accent,
which equals accent in dark and reads on the light sidebar.

Presentation HUDs keep `dark` (Preview stage, walkthrough, viewer controls
and now their dropdown menus), as do the scene-graph viewer and the self-dark
chips. The open-source app puts `dark` on <html>.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
InterfaceThemeRow is the System / Light / Dark setting as a Preferences row,
exported like SettingsSwitchRow. The editor still applies no theme: the host
owns the value and the class, and passes the row through SettingsPanel's
`preferences` slot. A host that passes nothing (the open-source app) gets no
row.

SegmentedControl buttons report aria-pressed, which the row's group label
and the test read. The sfx-player test's use-audio mock is now callable like
the real hook: bun keeps a module mock for the rest of the process, and the
Settings panel rendered after it calls useAudio().

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`dark` on the app's <html> also turned its home, login, settings and setup
pages dark, which were light before. SceneLoader, the one page that mounts
the editor, now adds `dark` to <body> while mounted, as the editor itself
used to, so the editor and the menus it portals there stay dark and the
other pages keep their look.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Elevator access and destination: selected Service, Disabled and the
  current stop get dark inks in light (they kept their -100 dark inks).
- Warning and accent inks in light are -700, which reach 4.5:1.
- Build, category and tool-option tiles get a light surface that reads.
- The storey height is legible in light; the riser diagram's waste and
  vent colours are darker in light; the endpoint drag pill reads on light.
- A block slot's disabled delete keeps its muted hover in dark:
  `dark:hover:*` ties with `disabled:hover:*` and is emitted later.

Dark keeps its values.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The snapshot overlay carries its own `dark` scope, so its level picker
matches the viewfinder in either theme (the picker's light variant goes).
The viewer controls pass `dark` to their tooltips, which portal to <body>.
The viewer's Display submenu reads "Scene theme", apart from the
interface theme.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
It mocked `../store/use-audio` for the whole process, so a later file that
rendered the settings panel got the fake store. The test now sets the real
store's state instead.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`dark` on <body> comes from an effect, after the first paint, so the editor
flashed light. The page root now carries it too; <body> keeps it for the
menus and dialogs portalled there.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… in light

Bundle 2's new dock controls were drawn for dark chrome only. Point and ask's
lavender ink, Follow Pascal's violet-300 ink and white hover, the "Resume
following" pill's violet-200 text, and the window inspector's Wrap / Unwrap
corner hovers now have light values, with the old ones kept under dark:.
The active toggles reset their dark hover text, as Select and Delete do, so
dark renders as before.

Follow Pascal also sits in the Preview controls bar, which stays dark, so it
takes a tooltipClassName and the bar passes `dark` like its other buttons.
The Point and ask layer is a self-contained overlay and stays dark in both
themes; the wiki lists it with the other overlays.

Co-Authored-By: Claude Opus 5.5 <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