Adapt remaining app to dark theme - #10284
Conversation
Sweep the remaining hardcoded light-only colors across the app and replace them with theme tokens or dark: variants. Introduce surface, danger and danger-surface tokens, and rename the sheet-* frame tokens to modal-* now that modals share the same frame treatment. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
There was a problem hiding this comment.
All reported issues were addressed across 151 files
Shadow auto-approve: would not auto-approve because issues were found.
Re-trigger cubic
There was a problem hiding this comment.
1 issue found across 3 files (changes from recent commits).
Confidence score: 4/5
- In
frontend/app/src/shared/components/editor/csv-table.tsx, reverting the empty-state text to hardcodedtext-neutral-400creates a concrete light-theme contrast regression (below AA), so users may struggle to read guidance in the editor’s empty state—switch back to the theme token (text-foreground-muted) or another AA-compliant token to de-risk accessibility.
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="frontend/app/src/shared/components/editor/csv-table.tsx">
<violation number="1" location="frontend/app/src/shared/components/editor/csv-table.tsx:35">
P3: The empty-state text was reverted from the theme token `text-foreground-muted` to a hardcoded `text-neutral-400`. In light theme this drops contrast from stone-600 (~7:1) to neutral-400 (~2.5:1, below the AA threshold for 14px text), and it abandons the theme-aware tokenization used by the rest of this PR. Use `text-foreground-muted` so the text follows the active theme and preserves the light-theme look the PR aims to keep unchanged.</violation>
</file>
Shadow auto-approve: would not auto-approve because issues were found.
Re-trigger cubic
|
|
||
| if (!rows[0]) { | ||
| return <div className="text-foreground-muted text-sm">No data available</div>; | ||
| return <div className="text-neutral-400 text-sm">No data available</div>; |
There was a problem hiding this comment.
P3: The empty-state text was reverted from the theme token text-foreground-muted to a hardcoded text-neutral-400. In light theme this drops contrast from stone-600 (~7:1) to neutral-400 (~2.5:1, below the AA threshold for 14px text), and it abandons the theme-aware tokenization used by the rest of this PR. Use text-foreground-muted so the text follows the active theme and preserves the light-theme look the PR aims to keep unchanged.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At frontend/app/src/shared/components/editor/csv-table.tsx, line 35:
<comment>The empty-state text was reverted from the theme token `text-foreground-muted` to a hardcoded `text-neutral-400`. In light theme this drops contrast from stone-600 (~7:1) to neutral-400 (~2.5:1, below the AA threshold for 14px text), and it abandons the theme-aware tokenization used by the rest of this PR. Use `text-foreground-muted` so the text follows the active theme and preserves the light-theme look the PR aims to keep unchanged.</comment>
<file context>
@@ -26,21 +26,21 @@ const parseCSV = (csv: string): string[][] => {
if (!rows[0]) {
- return <div className="text-foreground-muted text-sm">No data available</div>;
+ return <div className="text-neutral-400 text-sm">No data available</div>;
}
</file context>
| return <div className="text-neutral-400 text-sm">No data available</div>; | |
| return <div className="text-foreground-muted text-sm">No data available</div>; |
Covers the seven follow-ups recorded on handover from the dark-theme series: theme preference, non-production dark default, GraphiQL and Mermaid theme binding, token migration for legacy pages, data viewer tone, and schema visualizer dark support. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Resolves the two load-bearing design questions: the non-production signal is PEP 440 pre-release status on the running version, published as a resolved value on the unauthenticated config payload; and the first paint is owned by an inline classification script reading a localStorage mirror rather than by React. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Dual-lens review found three blocking items, all applied: - the planned file layout would have forced a shared/ -> entities/ import, which the frontend layer rules prohibit; the context now lives in shared/ and is filled from the entity, mirroring the existing DatePreferencesProvider - no contrast requirement existed for a feature entirely about color - the pre-paint script had no failure handling despite blocking render Left open for the requester: nothing defines when dark exits alpha, or where the defects the dogfooding surfaces are collected. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
59 tasks across 10 phases, grouped by user story so each ships independently. Records the two Ask First approvals gating phase 3 and the sequencing constraints that would otherwise be discovered late: removing the custom-variant last, memoising the mermaid plugin array, and baselining the light theme before any token swap. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
All seven handover items traced to requirements; verdict minor drift. Corrected one fidelity loss: the dark option's label had been generalised to 'pre-release' when the handover named 'alpha' specifically. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Production now defaults to following the user's browser appearance rather than to light, so the deployment default is three-valued. That removed a limitation instead of adding one: with system as both the cold-start fallback and the production default, a first-ever visit is correct from the first frame rather than flashing. Also: cross-tab sync and semantic-colour palettes move out of scope, system-appearance reaction stays in, the e2e suites stay in, and the branch stacks on bab-dark-theme-app with the PR targeting it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Withdraws the intermediate revision that defaulted production to the system appearance. It would have put dark-OS production users into the alpha palette without choosing it, which is what the alpha label exists to prevent. The spec now states one governing rule: dark is never reached by inference. That decides both defaults and the pre-paint fallback. Non-production still forces dark ignoring the OS, so an engineer on a light machine still dogfoods it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Grilling the product core replaced the version-derived deployment default with a plain experimental flag, following the convention both existing experimental settings already use: off by default, enabled in development/docker-compose.yml. Rejected the PEP 440 derivation because pre-release is a property of a version, not a deployment, so it also caught customer betas. Following the existing convention targets the intended deployments and removes a resolver module, the version parsing, a config field, and a dependency. Also: flag off hides the theme field entirely (a light-only picker would leak dark through match-system), stored preferences survive a flag flip, and the organisation-wide default is deferred to GA. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ct path T031 decided rather than left open: the flag goes in development/docker-compose.yml only, not the root compose file, so a deployment brought up from the root cannot enable dark via the host env var while it is still alpha. Dogfooding defects are reported over Slack, which closes the last open question and makes 'no new defects were found' checkable rather than an absence of evidence. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Stage-2 theme resolution as a pure function: a stored choice plus the browser's appearance resolves to a concrete palette. The browser is consulted only for an explicit SYSTEM choice, so an absent, unreadable or unrecognised choice falls back to light rather than inferring dark from a setting the user never pointed at this application. The React context const was planned for this phase but moved to land with its provider: knip fails on any export without a consumer, so a context with neither a producer nor a reader cannot ship green alone. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The GraphQL sandbox was pinned with forcedTheme="light" and rendered as a bright panel inside the dark shell; Mermaid was pinned to its default palette. Both now follow the active theme and react to a change without a reload. Adds a useResolvedTheme hook that reads the document class rather than a preference. That class is what the CSS already keys off, so a consumer cannot disagree with what the page has painted, and whatever ends up deciding the theme only has to set the class. Adds --content/--content-muted for raised content surfaces and migrates 18 files off hardcoded per-theme variants. Each mapping is byte-identical in both themes: the token resolves to rgb(255,255,255) in light, exactly the bg-white it replaced, and stone-900 in dark. Passes GraphiQL the resolved palette rather than "system" so the sandbox cannot run its own prefers-color-scheme check and drift from the app. Memoises the Mermaid plugin array on the theme, since a fresh array each render would re-run the rehype pipeline continuously. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The remaining dark-mode debt was not the files carrying hardcoded dark: variants -- those render correctly, they are only unmaintainable. It was the files carrying none: 38 unconditional bg-white and 21 bg-gray-* literals that paint bright regardless of theme. The artifacts tab was the clearest case, rendering every row as a white bar on black. Counting dark: occurrences would never have found these, since the defect is the absence of the variant rather than its presence. Adds --content-strong to the content family and migrates all three levels. Every mapping is byte-identical in light -- the token resolves to rgb(255,255,255) where it replaced bg-white -- so light cannot shift. Drops dark: overrides that now duplicate the token's own dark value. Warms the data viewer from neutral to stone, matching the theme's warm greys instead of a colder family. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The criterion counted dark: occurrences, which flagged files that work and missed every file that is actually broken. Restated as the property that matters: no component paints a fixed light surface regardless of theme. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The previous attempt set mermaidConfig.theme and did nothing. Verified in the browser: with the app dark, diagrams still rendered mermaid's default palette (nodeFill #ECECFF, #333 labels). Cause is in mermaid-isomorphic. Its node build forwards mermaidConfig into the page and calls mermaid.initialize; its browser build, which is what the bundler resolves, reads only containerStyle and prefix and calls mermaid.render with mermaid's global defaults. mermaidConfig has therefore never had any effect here -- the pre-existing securityLevel setting was equally inert, though mermaid already defaults to strict so nothing was exposed. mermaid is a phantom dependency, so calling initialize directly would mean promoting it to a direct dependency. Mermaid honours a per-diagram init directive without any global configuration, so the theme travels through the diagram source instead: no new dependency, and a diagram that configures itself keeps its own choice. Only the copy handed to the renderer is rewritten, never the stored document. Verified in the browser: dark gives nodeFill #1f2020 with #cccccc labels, toggling to light returns #ECECFF with #333, without a reload. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The forbidden list banned hex literals but said nothing about bg-white or bg-gray-*, which is the class of defect that left surfaces bright in dark mode. Records why searching for dark: does not find them: the defect is the absence of a variant, not the presence of a wrong one. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Scrollbars, native form controls and their popups, autofill highlights and the canvas behind overscroll are painted by the browser, not by CSS, and stay light in dark mode unless color-scheme says otherwise. Declare it alongside each palette so the two can never drift apart. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The token already resolves to this value in dark, so the override only duplicated it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
There was a problem hiding this comment.
1 issue found across 9 files (changes from recent commits).
Confidence score: 4/5
- In
frontend/app/src/shared/components/ui/command.tsx, applyingtext-subtletoCommandListchanges all non-selected item labels in light theme as well as dark theme, which may reduce contrast or unintentionally alter combobox/search UI; scope the styling to dark mode or the intended items.
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="frontend/app/src/shared/components/ui/command.tsx">
<violation number="1" location="frontend/app/src/shared/components/ui/command.tsx:46">
P2: Adding `text-subtle` to `CommandList` sets the text color for every non-selected item in the list, not just in dark mode. In light theme `--subtle` is `--color-stone-600`, so combobox/search/command item labels change from the previous inherited foreground (`stone-800`) to the muted `stone-600` tone. That alters light-theme visuals, contradicting the PR note that light theme is unchanged except danger colors, and it demotes primary labels (e.g. SearchAnywhereItem result titles, node labels) to a secondary contrast level throughout these popovers.
Since the pattern elsewhere in this component family is that secondary metadata is colored explicitly (e.g. ComboboxItem already applies `text-subtle-muted` to its schema-label span), primary labels should stay at foreground. Drop `text-subtle` (let items inherit) or use `text-foreground` if a guaranteed base color is wanted.</violation>
</file>
Shadow auto-approve: would not auto-approve because issues were found.
Tip: Review your code locally with the cubic CLI to iterate faster.
Re-trigger cubic
| ref={ref} | ||
| className={classNames( | ||
| "max-h-[280px] grow overflow-y-auto overflow-x-hidden rounded-md p-2", | ||
| "max-h-70 grow overflow-y-auto overflow-x-hidden rounded-md p-2 text-subtle", |
There was a problem hiding this comment.
P2: Adding text-subtle to CommandList sets the text color for every non-selected item in the list, not just in dark mode. In light theme --subtle is --color-stone-600, so combobox/search/command item labels change from the previous inherited foreground (stone-800) to the muted stone-600 tone. That alters light-theme visuals, contradicting the PR note that light theme is unchanged except danger colors, and it demotes primary labels (e.g. SearchAnywhereItem result titles, node labels) to a secondary contrast level throughout these popovers.
Since the pattern elsewhere in this component family is that secondary metadata is colored explicitly (e.g. ComboboxItem already applies text-subtle-muted to its schema-label span), primary labels should stay at foreground. Drop text-subtle (let items inherit) or use text-foreground if a guaranteed base color is wanted.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At frontend/app/src/shared/components/ui/command.tsx, line 46:
<comment>Adding `text-subtle` to `CommandList` sets the text color for every non-selected item in the list, not just in dark mode. In light theme `--subtle` is `--color-stone-600`, so combobox/search/command item labels change from the previous inherited foreground (`stone-800`) to the muted `stone-600` tone. That alters light-theme visuals, contradicting the PR note that light theme is unchanged except danger colors, and it demotes primary labels (e.g. SearchAnywhereItem result titles, node labels) to a secondary contrast level throughout these popovers.
Since the pattern elsewhere in this component family is that secondary metadata is colored explicitly (e.g. ComboboxItem already applies `text-subtle-muted` to its schema-label span), primary labels should stay at foreground. Drop `text-subtle` (let items inherit) or use `text-foreground` if a guaranteed base color is wanted.</comment>
<file context>
@@ -43,7 +43,7 @@ export function CommandList({ className, ref, ...props }: CommandListProps) {
ref={ref}
className={classNames(
- "max-h-[280px] grow overflow-y-auto overflow-x-hidden rounded-md p-2",
+ "max-h-70 grow overflow-y-auto overflow-x-hidden rounded-md p-2 text-subtle",
className
)}
</file context>
REVERT BEFORE THIS PR LEAVES DRAFT. `git revert` this commit; it is deliberately isolated so that is a single operation. The `cd/preview` label publishes an image and stops — the ephemeral deployment that consumes it lives outside this repository, so the env var is not reachable from here. Flipping the setting's default is the one lever that is, and it covers both shapes the deployer might take: it changes the generated root and development compose defaults to `:-true` for a deployer that reads them, and the setting's own default for one that does not. Every other guard still holds. The flag exists, an operator setting it to false still wins, and the frontend paints light the moment it does — none of the gating logic is touched. Regenerated alongside it, since all three record the default: `schema/openapi.json`, `docs/docs/reference/configuration.mdx`, `docker-compose.yml`. Also excludes the configuration reference from `end-of-file-fixer`. That generator emits no trailing newline while the hook appends one, so every regeneration had to be committed with the hook skipped or CI's `docs.validate` would fail on a newline. The two schema files were already excluded for exactly this reason; this is the third of the same kind. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Also worth recording for whoever runs it next: `/pre-ci` tells you to run its seven Phase 3 checks in parallel, and two of them fail spuriously when you do. `backend.validate-generated` and `docs.validate` resolve relative paths, so they break when a sibling shell has moved the working directory, and `docs.validate` can die between writing the CLI reference and stripping its developer sections — leaving un-stripped content in a committed generated file. Run them sequentially and all four generated-artifact validators pass. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Merged rather than rebased: this branch is published and carries review threads, which a force-push of 36 rewritten commits would strand. Four conflicts, all where both branches fixed the same hardcoded-light bug differently. Three were `<Popover className="bg-white">`. Base drops the override; this branch had swapped it for `bg-content`. Took base's, because it is the better fix: `Popover` already paints `bg-popover` in its own base styles, and that token is deliberately translucent (`stone-100/70%` light, `stone-900/70%` dark) to pair with the component's `backdrop-blur-lg`. Overriding it with the opaque `bg-content` would have defeated the blur. The fourth was the icon chip in the object-template form: `bg-indigo-100` became `bg-indigo-100/50` on base and `bg-active-surface` here. Took this branch's, because a fixed-palette surface class does not adapt to dark and is now forbidden by `dev/guidelines/frontend/styling.md`. `--active-surface` resolves to `indigo-100` in light, so the light appearance is unchanged from before either edit; base's `/50` softening is not carried over. Flagging that `--active-surface` is documented for the element the user is acting on, and this chip is decorative — it is the only indigo-tint surface token we have, so it is the closest fit rather than an exact one. Betterer's baseline moved by one content hash for the merged file — same issues, same positions, 186 either way. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…anged `frontend-validate-openapi-types` runs `pnpm codegen:openapi` and fails on any uncommitted diff. The temporary dark-theme default reached `schema/openapi.json` and therefore the generated types' `@default` docblock, which was not regenerated with it. There are four generated artifacts recording this default, not the three the revert note claimed. The frontend one hangs off the OpenAPI schema rather than off `config.py`, and `/pre-ci` regenerates the GraphQL types but not the OpenAPI ones, so nothing local catches it. Note corrected in place. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
There was a problem hiding this comment.
0 issues found across 5 files (changes from recent commits).
Confidence score: 5/5
- Automated review surfaced no issues in the provided summaries.
- No files require special attention.
Shadow auto-approve: would not auto-approve. Auto-approval blocked by 3 unresolved issues from previous reviews.
Re-trigger cubic
Base's "color cleanup" reworked three surfaces this branch had already tokenized. Took base's version in all three: each is the more thorough fix, and the mechanical `bg-gray-*` → `bg-content-strong` swaps here were only ever the minimum that made them theme-aware. - Token display: base replaces the ad-hoc classes with the shared `inputStyle`, so the box matches surrounding inputs, and adds `size="md"` to the copy button. Keeping this branch's version would have silently dropped both. - Drop target: base tokenizes the border as well (`border-ring bg-highlight`), where this branch had left `border-custom-blue-500` fixed. - Disabled list: base drops the background entirely for `opacity-60`, which is theme-agnostic by construction rather than by token lookup. Verified base's versions resolve before taking them: `--color-ring` and the `--highlight` gradient both exist, `inputStyle` is imported, and `CopyToClipboard` accepts `size`. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
react-datepicker ships a light-only stylesheet — hardcoded #fff/#f0f0f0 chrome, #216ba5 selection, and no custom properties to hook into — so the calendar popup stayed a white island in dark mode. It slipped past the bg-white/bg-gray sweep because the colours live in a vendored stylesheet, not in our class names. An override sheet re-points every colour it paints at the theme tokens, copying the stock selectors so specificity matches without !important. Layout untouched. Both consumers import it beside the stock sheet. The selection pairs --active with --content — each theme's active indigo against that theme's own content surface — so light selection moves from react-datepicker's foreign blue to the app's indigo, white-on-indigo-700 in light and stone-900-on-indigo-400 in dark, the pairing the contrast audit already measured. Verified live with computed colours in both themes: popup, header, day grid, outside/disabled days, time list, and the selected day + time chip all resolve to tokens. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Same class of miss as the date picker: react-toastify's default "light" theme paints a hardcoded white card, invisible to the bg-white sweep because the colour lives in a vendored stylesheet. Unlike the date picker it themes through its own custom properties, so re-pointing the light theme's two surface variables at our tokens is the whole fix — the tokens flip under `.dark`, and the toast follows with no `theme` prop and no JSX. The close button is the one colour outside its variable system (hardcoded near-black), overridden alongside. Semantic icon colours (success/error/info/warning) are deliberately untouched — semantic palettes are tracked separately. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…hite The prefix header fades its overflowing fields out at the right edge by painting a white gradient over them — a white smear in dark mode. No token can replace the white: the panel behind is a per-theme gradient, not a flat colour. A CSS mask fades the content itself to transparent instead, so whatever the panel paints shows through exactly, in any theme, with the overlay element gone entirely. Verified in both themes on the live page. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A pass over every gradient stop and fixed-palette background the earlier sweeps missed, verified live rather than by grep alone: - Diff-summary skeleton pulsed light gray in dark; the shared Skeleton already shimmers with a translucent tint that works in both themes, so it now just uses it. - The disabled list input had a second disabled site the base branch's cleanup did not reach; same fix as the first (opacity, no fixed background). - The IPAM available-prefix separator dots move to the border token, whose light value is the identical gray they hardcoded. - The resolved-thread card dropped its `to-gray-200` — probing the exact shipped classes showed it has been dead in both themes: the Card paints `background-image: var(--card)` directly, which Tailwind's gradient-stop utilities never touch. The "Resolved" checkbox already carries the state, so the class is removed rather than replaced with an equally dead token version. Restoring a working resolved tint is a design decision left for its own change. Reviewed and deliberately left: the colour-picker trigger's palette gradient (an affordance, not a surface), the always-dark tooltip and data-viewer families and everything rendered inside them, the diff expander strips (the diff palette is tracked separately), and the timeframe pill's fixed dark chip (pre-existing design, legible in both). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
GraphiQL's dark theme hangs off two HSL triplets with hue 219, a blue-navy that clashed with the warm near-black shell around it. Its every surface is derived from those two variables, so re-pointing base and neutral at stone-900 and stone-200 retunes the whole sandbox — the container now computes to rgb(28,25,23), byte-identical to the app's dark content surface. The triplets are duplicated from the theme as bare HSL components because GraphiQL composes them itself via hsl(var(...)) and cannot read oklch tokens. The selector list mirrors GraphiQL's own dark block so its portalled dialogs and tooltips retune with the main container. Light is untouched: the override only matches body.graphiql-dark, and GraphiQL's light base is already the same white as the app's content token. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* test: assert the current tree row by its new selected class 94cde56 replaced the hardcoded bg-neutral-100 highlight on the current tree row with semantic tokens for dark theme, but the E2E assertions still pinned the old literal class. The row is highlighted correctly; only the assertions were stale. That one change is why four jobs went red: the same assertion appears eight times across both suites, sharded into E2E-testing-playwright plus the foundation, sites_a and sites_b pytest shards. Assert bg-selected alone rather than the full token set, so a later shadow or hover tweak does not break these again. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix: stop leaking react-aria render-prop keys as class names composeAriaClassName invokes its tailwind callback with react-aria's render-props object, but TreeItem and ListBox both named that parameter resolvedClassName and forwarded it into cn(). cn is clsx-compatible, so an object argument emits its truthy keys as class names -- every tree row and list box carried a literal "hasAction hasChildItems level selectionMode selectionBehavior state id defaultClassName". Neither component uses any render prop, so pass the styles as a plain string. composeAriaClassName already merges the consumer's className after them, which is why the leak was cosmetic rather than a styling bug. Predates the dark-theme work; introduced in 33da87f. Correct callers destructure what they need, e.g. ({ isPressed }) in button.tsx. Add a regression test asserting no render-prop key reaches the row's class list, and that a consumer class still survives. Verified red before the fix, green after. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * test: cover the class-name leak for ListBox too, not just TreeItem Both components had the same defect and the same fix, but only TreeItem had a regression test. Add the ListBox equivalent, and move the file to shared/components/aria/ since it now covers className composition for two unrelated components rather than anything tree-specific. ListBox leaked a different set of keys (layout, orientation, state, defaultClassName) because react-aria's render props differ per component, so each list is asserted separately. Both leak tests were verified red against the unfixed components. The four pass-through tests pass in both states, which is the point: the fix removes the leaked keys and changes nothing else. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
The pre-paint script can only replay a theme this browser has resolved once before, so a first-time visitor arrives with no class at all. Applying the resolved theme from a passive effect let that first commit paint in the wrong palette and then snap to the right one. A layout effect runs before the browser paints the commit, which closes the window entirely. The end-to-end theme assertions were sampling the class once, immediately after a visibility wait, and could observe the same window. They retry now — except the two that run at domcontentloaded, which must keep sampling once: retrying there would also accept a class arriving later from React, which is exactly the regression they exist to catch. Two spec corrections found while checking the above: The token-discipline command could never fail. Git's ERE does not implement \b on every platform, and where it does not, the whole pattern matches nothing -- so "expect no output" passed while the check was seeing no files at all. Where \b does work, it reported five standing false positives, because the legitimate dark:bg-white/N overlays match a bare literal just as well as a fixed palette does. Switched to -P with a lookbehind that excludes the variant. T041 claimed a plugin-array stability test that does not exist, and cannot usefully exist: the React Compiler memoises that array whether or not the useMemo is hand-written, so an assertion on it passes identically with the memo deleted. Recorded the property as held by compilation rather than shipping a test that cannot fail. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The alpha tag rode on the switch itself, so it also appeared on the item offering the way back to light. Light is not pre-release, and tagging the exit route reads as a warning against leaving. It now tags what the item switches *to*, which is the step that actually warrants the caution. The menu item also passed no textValue. MenuItem falls back to the children only when they are a plain string, and these are an icon, a label and the tag, so the fallback landed undefined and react-aria lost the typeahead label. Passing the label restores it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Reverts the temporary flip (313b133, de36a8a) that defaulted the flag on so the ephemeral preview environment would serve dark without a deployment-side env var. The theme is still alpha, so shipping it on by default is not the posture we want once this leaves draft. Regenerated from the setting rather than reverting the diffs, because the same commit also carried a permanent fix -- the end-of-file-fixer exclusion that stops pre-commit fighting the configuration-reference generator -- which must survive. All four artifacts that record the default are back in step: the root compose file, the OpenAPI schema, the configuration reference, and the frontend REST types. development/docker-compose.yml keeps its `true`. That was always the intended long-term default there, and it is what gives an engineer running the dev stack the theme without extra configuration. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The visualizer deliberately does no theme detection of its own, so it renders whatever the embedder resolves. Passing the resolved theme is the whole integration. Pins the submodule at the squash-merge commit on main rather than at the branch commit this branch had been carrying. The two are not the same object: the upstream PR was squash-merged, so neither the old pin nor the branch tip is an ancestor of main, and the old pin would break every checkout once the branch is deleted. It was also three commits behind, missing the upstream review fixes. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
fix(frontend): theme the surfaces that stayed light in dark mode
Two conflicts, both in the test setup. vitest.config.ts: #10264 landed on develop and converted the optimizeDeps entries owned by workspace packages to Vite's nested "<owner> > <dep>" form, which replaced the bare html-to-image with "infrahub-schema-visualizer > html-to-image". Took develop's restructuring and dropped our now-duplicate bare entry. Kept mermaid and rehype-mermaid bare, since both are direct dependencies of frontend/app and resolve from there. pnpm-lock.yaml: regenerated from the merged manifests rather than resolved by hand, then verified with --frozen-lockfile so it matches what CI will install.
| # Four generated artifacts record this default: the root compose file, the OpenAPI schema, the | ||
| # configuration reference, and the frontend REST types. The frontend one is regenerated from the | ||
| # OpenAPI schema rather than from this file, so it is the easy one to miss when this changes. |
| # Four generated artifacts record this default: the root compose file, the OpenAPI schema, the | ||
| # configuration reference, and the frontend REST types. The frontend one is regenerated from the | ||
| # OpenAPI schema rather than from this file, so it is the easy one to miss when this changes. | ||
| dark_theme: bool = Field( | ||
| default=False, | ||
| description="Offer the dark theme in the web interface. Alpha: some surfaces still render incorrectly.", | ||
| ) |
There was a problem hiding this comment.
Should this actually be a user preference instead of a global switch?
Dark theme
Infrahub gets a dark theme. Earlier PRs in this series gave the design system its dark palette; this one carries it through the rest of the application and adds the machinery a person needs to actually choose it.
It ships behind an experimental flag, off by default, while the theme is alpha.
Approach
Colour has one source of truth. Components never ask which theme is active. They use semantic tokens —
--content,--foreground,--border,--surface,--dangerand friends — whose values are redefined under a singledarkclass on the document element. That is why supporting a second theme is mostly redefining tokens rather than editing components, and why a component with a fixed colour is a defect rather than a style choice.The document element is the state. Anything that needs to know the current theme reads it back off the document rather than from storage or config, so no component can disagree with what the user is actually looking at.
Content that carries its own colours is driven explicitly. Mermaid diagrams, GraphiQL, the schema visualizer, the date picker and the toasts each bake or vendor their own palette and cannot be reached by our tokens. Each one is handed the resolved theme, or has its vendored stylesheet re-pointed at our tokens.
Nothing flashes. The last resolved theme is replayed before the first frame, and the theme is applied before paint rather than after, so a reload and a first visit both land on the right palette directly instead of painting light and snapping.
The choice is a deployment decision. With the flag off there is no theme setting and no way to reach dark; the stored choice is kept rather than deleted, so enabling the flag later restores whatever the user had picked. The menu entry tags dark as alpha.
Rollout
CSS and styling only, plus one additive field on the unauthenticated config endpoint. No schema or API changes, nothing to migrate, and light is visually unchanged apart from a shared danger colour. Safe to deploy: with the flag at its default, the application renders exactly as it does today.
Screenshots
Captured on a live stack at 1440×900 — same pages, same data, only the theme differing. These cover this series as a whole, including the follow-up work merged in from #10295.
Home
Object list
Proposed change with a Mermaid diagram — the diagram itself follows the theme:
GraphQL sandbox — follows the app theme instead of being pinned light:
Login — themed before a session exists:
Checklist
+dark-theme.added.mddev/knowledge/frontend/theming.md