fix(ui): theme the onboarding wizard decorative panel instead of hardcoding dark (#11379)

The onboarding wizard's decorative right-hand panel, which holds the ASCII
paperclip illustration, hardcoded a near-black surface. `ThemeContext`
supports light and dark and follows `prefers-color-scheme`, so in light mode
a new customer met the product as a pale form beside a solid black rectangle
— on the one screen meant to introduce it. The glyphs inside already used
`text-muted-foreground`, so the panel was the only part ignoring the theme.

It now uses the paired `bg-muted` surface, which is defined in both themes
(`oklch(0.97 0 0)` light, `oklch(0.269 0 0)` dark), so the illustration reads
as ink on a surface either way and follows any future theme without another
fix.

The guard for it asserts the complete set of `bg-` classes on the panel is
`["bg-muted"]`, anchored to the `<AsciiArtAnimation />` wrapper rather than
scanning the file. Forbidding specific spellings is what failed here
originally: the first version checked `bg-[#rrggbb]` and silently stopped
guarding anything once master migrated the class to `bg-(--hex-1d1d1d)`.
Naming what is allowed cannot decay that way, and it catches named colours
like `bg-black` that no spelling list covered.

Lands the work from #8982 by @stubbi, whose two commits are included
unchanged with their authorship. The rebase and the guard are mine.

Tested: ui typecheck clean; both theme cases fail against four spellings of
the regression — `bg-(--hex-1d1d1d)`, `bg-[#1d1d1d]`, `bg-black`,
`bg-zinc-900` — where the original caught only one and my first widening
caught two. Full ui suite 3958 pass, with one timezone-dependent
IssueProperties failure present on master. All CI gates green; Greptile 5/5.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Tonio 2026-08-14 12:06:20 -07:00 committed by GitHub
parent a53cc8819b
commit 35a9b98733
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 70 additions and 1 deletions

View File

@ -2155,7 +2155,7 @@ function OnboardingWizardInner({
name + mission steps) */}
<div
className={cn(
"hidden md:block overflow-hidden bg-(--hex-1d1d1d) transition-(--tp-width-opacity) duration-500 ease-in-out",
"hidden md:block overflow-hidden bg-muted text-muted-foreground transition-(--tp-width-opacity) duration-500 ease-in-out",
step === 1 || step === 2 ? "w-1/2 opacity-100" : "w-0 opacity-0"
)}
>

View File

@ -0,0 +1,69 @@
// @vitest-environment node
import { readFileSync } from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";
import { describe, expect, it } from "vitest";
// The onboarding wizard's decorative right-hand panel (which renders the
// ASCII paperclip illustration) must follow the active shadcn theme instead of
// hardcoding a dark surface. Otherwise a light/cream deployer theme (set via
// the PAPERCLIP_DEFAULT_THEME bootstrap) renders a jarring cream form next to a
// solid dark panel. The illustration glyphs already use `text-muted-foreground`,
// so the panel must sit on the paired `bg-muted` surface to read as an
// intentional ink-on-surface texture in every theme.
//
// Asserting against the source keeps this guard cheap: the panel className is a
// static string literal, and the full wizard dialog is too heavy to mount here.
const here = path.dirname(fileURLToPath(import.meta.url));
describe("OnboardingWizard decorative panel theming", () => {
const source = readFileSync(
path.join(here, "OnboardingWizard.tsx"),
"utf8"
);
/**
* The className expression on the wrapper around <AsciiArtAnimation />.
* Anchoring here keeps both guards on the decorative panel itself - the same
* tokens appear elsewhere in the wizard. `[^<>]` stops the match spanning
* into other JSX elements.
*/
function panelClassNames(): string | null {
return (
source.match(/className=\{cn\(([^<>]*)\)\}\s*>\s*<AsciiArtAnimation\s*\/>/)?.[1] ??
null
);
}
it("gives the decorative panel no background but the muted token", () => {
// Scoped to the panel, not the file. A file-wide scan would fail this
// case for a hardcoded colour anywhere else in the wizard, reporting a
// panel regression that had not happened.
//
// Asserted as the complete set of `bg-` classes rather than a list of
// spellings to forbid. The previous version scanned for `bg-[#rrggbb]`
// alone, and passed unchanged once master migrated this class to
// `bg-(--hex-1d1d1d)` - so it guarded nothing at all for a while. A named
// colour like `bg-black` or `bg-zinc-900` would have slipped through the
// same way. Naming what is allowed cannot rot like that.
const panel = panelClassNames();
expect(panel).not.toBeNull();
const backgrounds = panel!.match(/\bbg-[^\s"'`,]+/g) ?? [];
expect(backgrounds).toEqual(["bg-muted"]);
});
it("themes the decorative panel with shadcn surface tokens", () => {
// Anchor to the wrapper around <AsciiArtAnimation /> so the guard checks
// the decorative panel itself (the same tokens appear elsewhere in the
// wizard), then assert each token independently so a class reorder or a
// utility inserted between them cannot false-fail the test. `[^<>]`
// keeps the match from spanning across other JSX elements.
const panel = source.match(
/className=\{cn\(([^<>]*)\)\}\s*>\s*<AsciiArtAnimation\s*\/>/
);
expect(panel).not.toBeNull();
expect(panel?.[1]).toMatch(/\bbg-muted\b(?!-)/);
expect(panel?.[1]).toMatch(/\btext-muted-foreground\b/);
});
});