Skip to content

Adapt remaining app to dark theme - #10284

Open
bilalabbad wants to merge 57 commits into
developfrom
bab-dark-theme-app
Open

Adapt remaining app to dark theme#10284
bilalabbad wants to merge 57 commits into
developfrom
bab-dark-theme-app

Conversation

@bilalabbad

@bilalabbad bilalabbad commented Aug 17, 2026

Copy link
Copy Markdown
Contributor

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, --danger and friends — whose values are redefined under a single dark class 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

light dark
home light home dark

Object list

light dark
devices light devices dark

Proposed change with a Mermaid diagram — the diagram itself follows the theme:

light dark
proposed change with mermaid, light proposed change with mermaid, dark

GraphQL sandbox — follows the app theme instead of being pinned light:

light dark
graphql sandbox light graphql sandbox dark

Login — themed before a session exists:

light dark
login light login dark

Checklist

  • Tests added/updated — theme provider, gating rule, resolved-theme hook, account-menu switch, and visual tests asserting the colours Mermaid actually bakes in
  • Changelog entry — +dark-theme.added.md
  • External docs updated — FAQ entry; the configuration reference carries the setting through generation
  • Internal .md docs updated — dev/knowledge/frontend/theming.md
  • I have reviewed AI generated content

Review in cubic

bilalabbad and others added 3 commits August 17, 2026 02:12
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>
@bilalabbad
bilalabbad requested a review from a team as a code owner August 17, 2026 00:20
@bilalabbad bilalabbad added group/frontend Issue related to the frontend (React) type/housekeeping Maintenance task ci/skip-changelog Don't include this PR in the changelog labels Aug 17, 2026
@bilalabbad bilalabbad changed the title refactor(frontend): adapt remaining app surfaces to dark theme Adapt remaining app to dark theme Aug 17, 2026
@bilalabbad bilalabbad self-assigned this Aug 17, 2026

@cubic-dev-ai cubic-dev-ai Bot left a comment

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.

All reported issues were addressed across 151 files

Shadow auto-approve: would not auto-approve because issues were found.

Re-trigger cubic

Comment thread frontend/packages/ui/src/components/button/button.tsx
Comment thread frontend/app/src/shared/components/editor/csv-table.tsx Outdated
Comment thread frontend/app/src/entities/tasks/ui/task-display.tsx Outdated
Comment thread frontend/app/src/shared/components/ui/pagination.tsx
Comment thread frontend/app/src/shared/components/ui/pagination.tsx
Comment thread frontend/app/src/shared/components/editor/json/json-editor.tsx
Comment thread frontend/app/src/entities/path-traversal/ui/infra-node.tsx
Comment thread frontend/app/src/entities/proposed-changes/ui/tabs/proposed-change-tab.tsx Outdated
Comment thread frontend/app/src/shared/components/display/pie-chart.tsx Outdated

@cubic-dev-ai cubic-dev-ai Bot left a comment

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.

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 hardcoded text-neutral-400 creates 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>;

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.

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>
Suggested change
return <div className="text-neutral-400 text-sm">No data available</div>;
return <div className="text-foreground-muted text-sm">No data available</div>;

saltas888 and others added 15 commits August 17, 2026 14:31
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>
saltas888 and others added 2 commits August 17, 2026 18:09
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>

@cubic-dev-ai cubic-dev-ai Bot left a comment

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.

1 issue found across 9 files (changes from recent commits).

Confidence score: 4/5

  • In frontend/app/src/shared/components/ui/command.tsx, applying text-subtle to CommandList changes 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",

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.

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>

saltas888 and others added 5 commits August 18, 2026 16:07
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>

@cubic-dev-ai cubic-dev-ai Bot left a comment

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.

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

saltas888 and others added 12 commits August 18, 2026 16:57
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
@saltas888
saltas888 requested review from a team as code owners August 19, 2026 15:28
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.
@github-actions github-actions Bot added type/documentation Improvements or additions to documentation group/backend Issue related to the backend (API Server, Git Agent) type/spec A specification for an upcoming change to the project labels Aug 19, 2026
@codspeed-hq

codspeed-hq Bot commented Aug 19, 2026

Copy link
Copy Markdown

Merging this PR will not alter performance

✅ 13 untouched benchmarks


Comparing bab-dark-theme-app (b9bc6b4) with develop (e8b22b7)

Open in CodSpeed

Comment on lines +993 to +995
# 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.

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 looks unnecessary

Comment on lines +993 to +999
# 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.",
)

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.

Should this actually be a user preference instead of a global switch?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ci/skip-changelog Don't include this PR in the changelog group/backend Issue related to the backend (API Server, Git Agent) group/frontend Issue related to the frontend (React) type/documentation Improvements or additions to documentation type/housekeeping Maintenance task type/spec A specification for an upcoming change to the project

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants