feat(FR-3605): convert theme.json to the v2 appearance document and retire the theme-shim - #9361
feat(FR-3605): convert theme.json to the v2 appearance document and retire the theme-shim#9361ironAiken2 wants to merge 18 commits into
Conversation
Coverage Report for root-coverage
File CoverageNo changed files found. |
|
|
|
Dev server on box |
2c3f3e6 to
28df890
Compare
4314cde to
7f69b7b
Compare
7f69b7b to
0daba8c
Compare
PR Analysis Report📚 Storybook PreviewGitHub Pages may take up to a minute to hydrate after the deploy. 🧩 Changed Components
10 changed component(s) with no story to open
26 other changed file(s) in
|
| File | Size | Gzip |
|---|---|---|
backend.ai-ui.js |
2043.0 kB | 496.7 kB |
No base measurement — the merge-base build was skipped (dependency changes, or the base build failed).
yomybaby
left a comment
There was a problem hiding this comment.
Please check a field action. https://github.com/lablup/backend.ai-webui/actions/runs/33721727314/job/100549985759?pr=9361
…nce document Replace the antd-shaped v1 theme document with the v2 Astryx-vocabulary appearance document as both the shipped resources/theme.json format and the app's internal currency: - theme.families.<f>.seeds carries brand seeds (string = both schemes, [light, dark] tuple splits them) plus headerBg; branding carries logo, companyName, brandName and familyLabels. Roles stay derived in code (brand<-accent, admin<-info, secondary<-success). - The loader reads v2 only; a v1 document logs a loud console error and falls back to the built-in defaults (BREAKING for operator theme.json — migration guide in the PR description's field map). - computeThemeName hash inputs are unchanged, so the prebuilt brand theme artifact still matches by name and the Astryx theme-build gate passes without a rebuild. - The Branding editor is format-adapted in the same change (color pickers write whole seed tuples, ThemeJsonConfigModal validates the single theme.schema.json, stale pre-v2 localStorage drafts are reseeded) so the editor never produces a document the loader rejects. The dead Text Color item is dropped. - custom_primary_color / ThemeAccentColorPicker are removed here (2026-08-19 decision): the accent override mechanism was antd's palette algorithm over the v1 document and dies with it. usePrimaryColors stays — it reads shim tokens, not the setting. - antdThemeConfig.schema.json and its generator script are deleted. Co-Authored-By: Claude Fable 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01GkNUy6y6dgVTNzCpAv2wPk
…recipe (REV 23)
Move every theme-dependent value the theme-shim served into the Astryx
theme itself, as app-namespaced custom tokens, so the antd-vocabulary
provider can be retired and consumers read one source via useTheme():
- --bai-color-info / --bai-color-link (+ options.link, hash input): the
two brand seeds Astryx has no token family for.
- --bai-header-bg (+ options.headerBg, hash input): the header band pair,
applied verbatim per scheme like the shim's components passthrough,
with a default-family fallback the shim path never had.
- --bai-color-{error-bg,info-bg,warning-hover,success-border-hover,
primary-bg,error-border}: the consumed 'derive' verdicts, computed at
define time with the vendored palette() at the same keys the shim used
at runtime (darkKey honored).
- --bai-primary-1..10: usePrimaryColors' ramp — generate() over the
mode's palette key-6 map color, per scheme.
- BAI_SELF_COLOR_TOKENS (8): antd's neutral text/fill alpha ramp from
selfTokens, verbatim, now a hash input.
resolveDarkSeed now falls back to palette(seed,'dark')(6) for unknown
6-digit hex seeds instead of passing them through: the shim computed the
transform live, so passthrough made the Astryx and shim paths render two
different darks for any non-shipped seed (stained's #A78BFA vs #9179d8).
Parity wins; non-parsable strings still pass through.
THEME_NAME_REV 22 -> 23; prebuilt artifact regenerated
(bai-r23-default-brand-h7s55rh), old r22 artifacts deleted, built/index
repointed. BUI exports the vendored palette() for the recipe.
Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01HkJqxHeMfUumtg2dAdWko4
…d of the shim
Mechanical sweep of every `theme.useToken()` consumer (189 files across
react/ and backend.ai-ui) onto `useTheme().token('<css-var>')`, keyed by
the theme-shim's own mapping table — so each antd token name lands on the
Astryx variable the shim already resolved it to, or on the `--bai-*`
custom token that now carries the value the shim computed:
- astryx/aligned verdicts -> the mapped Astryx var (51 names)
- brand seeds -> --color-accent/error/success/warning, --bai-color-info/link
- derive verdicts -> --bai-color-*-bg/-hover/-border
- self alpha ramp -> --bai-color-text-*/--bai-color-fill-*
- `token.Layout?.headerBg` -> --bai-header-bg; `headerHeight` was never
provided by the shim (only headerBg was passed through), so its
`?? 60` fallbacks were constants — inlined as 60
- preset palette steps (purple5/green5/red5) -> --bai-preset-* static
tuples added to the recipe; bare `token.red` is the un-transformed
seed, inlined
- scalar self tokens (controlHeightSM, screenXS/SM, zIndexPopupBase,
lineHeight) -> literals at their 1-3 call sites
- `usePrimaryColors().primary5` (the only ramp step consumed: progress
fills in BAIProgress/BAIPanelItem) -> --bai-primary-5; the recipe's
ramp is trimmed to that one token and the hook + its test are deleted
Value semantics changed from antd numbers to CSS strings ("20px"), so the
non-style positions were fixed by hand: negative margins via calc(),
numeric props (recharts minTickGap, divider inset/width, icon size, the
launcher line-height product, BAINameActionCell's width budget) via
parseFloat(), and BAIFlex `gap` sites passed the rung name ("xs") instead
of a length. BAIFlex itself now maps rung -> --spacing-N and parses px.
`useTheme` is a real hook where the shim's `useToken` read a store, so
the calls inside ChatMessageContent's render callbacks and after the
TextHighlighters' early returns are hoisted to component scope
(rules-of-hooks); the callbacks list `token` in their deps.
No visual change intended: every replacement resolves to the value the
shim produced for the same document and mode.
Co-Authored-By: Claude Fable 5.1 <[email protected]>
Claude-Session: https://claude.ai/code/session_01HkJqxHeMfUumtg2dAdWko4
… token provider With every consumer on useTheme(), the antd-vocabulary provider has no reader left. Delete it and keep only what the Astryx recipe still needs: - packages/backend.ai-ui/src/theme/: the vendored @ant-design/colors port (generate/palette/preset tables), the measured parity tables (ANTD_ALIGN_TOKENS, ANTD_DARK_ALGORITHM_OUTPUT, ANTD_REVERSED_BAND_OVERLAYS) and the --bai-* custom token builder (buildBaiCustomTokens, resolveDarkSeed, BAI_SELF_COLOR_TOKENS) moved out of react/src so the app recipe and the Storybook brand theme share one implementation. The palette parity test moves with it; the shim-output suites go. - useBAIBreakpoint / BAI_BREAKPOINTS -> packages/backend.ai-ui/src/hooks (it never depended on the provider — a matchMedia store). - Deleted: ThemeShimProvider, buildTokens, the mapping/selfTokens/astryxVars layers, the frozen antd token fixture and type, react/src/theme-shim. - DefaultProviders: <AstryxBrandTheme> now wraps the app alone; the shim-seed resolution (pickSeed per family/mode) is gone with the second adapter, so the "two providers render two accents" class of bug cannot recur. - Storybook: decorators drop the shim wrapper and read body colors from useTheme(); .storybook/theme.json is now a copy of the v2 document and astryxBrandTheme.ts builds from it through the shared BUI helpers, so the --bai-* tokens reach stories too. - defaultDesignTokens: the "antd stock seeds through buildTokens" helper shrinks to the one value still read (the stock font-family fallback). - The 19 react importers of useBAIBreakpoint/BAI_BREAKPOINTS/presetPalettes import from 'backend.ai-ui'; stale shim pointers in comments trimmed. Co-Authored-By: Claude Fable 5.1 <[email protected]> Claude-Session: https://claude.ai/code/session_01HkJqxHeMfUumtg2dAdWko4
…ormed documents Review findings on the v2 document path, each a way a bad or missing document degraded silently or broke the page: - pickValidAppearanceConfig logs on EVERY rejection (non-object, missing or wrong-typed schemaVersion, v1 sniff) and warns when theme.families has no `default`; fetch failures, non-2xx responses and JSON parse errors are logged too, so an operator's migration mistake is diagnosable. - loadCustomThemeConfig dispatches `custom-theme-loaded` in .finally, so a throw after validation (non-string fontFamily, a malformed family entry) can no longer leave every listener waiting forever. - The dev VITE_THEME_HEADER_COLOR override is applied to a clone: the shipped document (getStaticAppearanceConfig) stays pristine and the applied one (getCustomTheme) carries the override — the two getters now differ by construction instead of aliasing each other. - pickSeed guards non-string tuple entries and falls back dark -> light, matching the recipe's seedPairFromValue. - useDefaultTheme seeds/reseeds the draft from the shipped document, never from rawThemeConfig — in preview mode that IS the draft, and reseeding a pre-v2 draft from itself looped without bound. - ThemeJsonConfigModal validates at the write boundary: a draft that fails pickValidAppearanceConfig is refused even when the Monaco markers were unavailable (schema fetch failure). - ThemeColorPicker fills the untouched scheme from the recipe's rendered default for that seed, not from the color just picked for the other one. - e2e modifyThemeJson authors the v2 document (type, fallback document, examples) and page-access-control clears branding.logo.href — the v1 `logo` merge the app no longer reads made those four specs CI-red. - New react/src/helper/themeJson.test.ts keeps resources/theme.json, theme.schema.json and the loader agreeing (ajv), replacing the drift guard the deleted schema generator provided. - Comments that described code that does not exist (login sync of userConfig.themeFamily, the removed accent picker, single-entry catalog) and the backend-sync spec's customPrimaryColor field are corrected. Co-Authored-By: Claude Fable 5.1 <[email protected]> Claude-Session: https://claude.ai/code/session_01HkJqxHeMfUumtg2dAdWko4
…ypes Co-Authored-By: Claude Fable 5.1 <[email protected]> Claude-Session: https://claude.ai/code/session_01HkJqxHeMfUumtg2dAdWko4
… values The recipe used to carry BAI_DEFAULT_SEEDS — a hand-maintained copy of the shipped theme.json — as the fallback for a missing document or an omitted seed. Two places to edit the same values, and "the default theme" meant either of them depending on where you looked. Now: - The recipe holds no brand values. A seed the document does not declare is not pinned: Astryx's own token applies, and the --bai-* vocabulary falls back to Astryx var() references (info -> --color-accent, link -> --color-text-accent, headerBg -> --color-background-surface, the derived bg/border/hover steps -> the matching Astryx family). No document at all builds the neutral theme plus the recipe's structural parity pins. - The prebuilt artifact entry (built/backendai-default.ts) reads resources/theme.json directly, so the shipped brand exists exactly once; the theme-build gate keeps the artifact in step with it. The default family now declares `warning` explicitly (antd's seed, unchanged value) instead of inheriting it from code. - <AstryxBrandTheme> holds the first paint until the appearance bootstrap settles (the splash stays up), so a slow theme.json cannot paint the neutral theme and then flip to the brand. Trees rendered without the bootstrap (tests) are not gated. - resolveRoleTheme's module-scope staleness warning goes: it compared the code defaults with the artifact, and the verify gate covers staleness. - ThemeColorPicker no longer reaches for code defaults; an undeclared scheme takes the picked color. - Storybook's brand theme reads its theme.json copy the same fallback-less way; this also fixes a leftover `light.colorPrimary` reference that bundled but would have thrown when the preview loaded. Co-Authored-By: Claude Fable 5.1 <[email protected]> Claude-Session: https://claude.ai/code/session_01HkJqxHeMfUumtg2dAdWko4
…an antd constant defaultDesignTokens.ts had shrunk to one value — antd's stock font stack — shown as the field's fallback. The app no longer falls back to that font (an undeclared fontFamily renders Astryx's own), so the constant was both a second place holding a brand default and a wrong one. The field now follows the color pickers' rule: draft, then the shipped theme.json, else empty. The helper is deleted. Co-Authored-By: Claude Fable 5.1 <[email protected]> Claude-Session: https://claude.ai/code/session_01HkJqxHeMfUumtg2dAdWko4
…eme> The gate held the first paint until theme.json settled so a slow document could not paint Astryx's neutral theme and then flip to the brand. In practice the first visible paint is already held by useSuspendedBackendaiClient (config.toml + client init), which theme.json beats by a wide margin, so the gate never changed what a user sees and only added a store flag, a useSyncExternalStore hook and a null-return for reviewers to reason about. The bootstrap still fires `custom-theme-loaded` once, success or failure, and the providers re-render on it as before. Co-Authored-By: Claude Fable 5.1 <[email protected]> Claude-Session: https://claude.ai/code/session_01HkJqxHeMfUumtg2dAdWko4
…ver header color `applyDevOverrides` suggested a general override layer; the only thing it ever applies is `VITE_THEME_HEADER_COLOR` onto every family's `headerBg` while the dev server runs. Rename it `applyDevServerHeaderColorOverride` and say so in the store field comment. Co-Authored-By: Claude Fable 5.1 <[email protected]> Claude-Session: https://claude.ai/code/session_01HkJqxHeMfUumtg2dAdWko4
…o `rawThemeConfig` The field is the fetched document as-is (the dev-server header color applied, or the Branding preview draft) — the same thing `rawConfig` named before the v2 conversion and what `useRawCustomThemeConfig` still calls it. `appearance` read like a resolved value; `rawThemeConfig` says it is the raw input that `resolveRoleTheme` and the branding consumers pick from. Co-Authored-By: Claude Fable 5.1 <[email protected]> Claude-Session: https://claude.ai/code/session_01HkJqxHeMfUumtg2dAdWko4
… commit
`useCustomThemeConfig` carried a `console.log('#', families)` from a local
inspection; the rename commit staged the whole file and took it along.
CI's `lint:ci` runs with `--max-warnings=0`, so the `no-console` warning
failed the react-lint job.
Co-Authored-By: Claude Fable 5.1 <[email protected]>
Claude-Session: https://claude.ai/code/session_01HkJqxHeMfUumtg2dAdWko4
…ntd dark re-mapping The recipe used to run every declared dark seed through antd's darkAlgorithm before pinning it (`#DC6B03` → `#be5e06`): a measured table for the shipped seeds, the vendored dark palette for any other hex. That kept the antd-era dark pixels, but it made the v2 document lie — the schema says a tuple splits the schemes, and an operator's dark value was silently replaced. With antd gone the parity target is gone too, so: - `toTuple` (react) and `toSeedTuple` (BUI) hand the declared pair through; `resolveDarkSeed` and `ANTD_DARK_ALGORITHM_OUTPUT` are deleted along with their tests. The shipped dark brand therefore renders the document's values (accent `#DC6B03`, error `#DC4446`, success `#03A487`, info `#009BDD`, warning `#FAAD14`); the PR body carries the old rendered values for an operator who wants them back. - `--color-accent-muted` is pinned (accent at 20%/25% alpha, like the status muted steps). Measured: Astryx's `resolveThemeTokens` re-applies the base theme's `__inputTokens` over generated tokens, so with `extends: neutralTheme` the generated muted resolved to neutral's grey in `useTheme()` while the CSS showed the brand tint — the only token where the two disagreed. - `buildBackendAiTheme` → `buildBackendAITheme` (and the options type). - `THEME_NAME_REV` 23 → 24; the prebuilt artifact is regenerated (`bai-r24-default-brand-h2hqtmn`) and the r23 files removed. Co-Authored-By: Claude Fable 5.1 <[email protected]> Claude-Session: https://claude.ai/code/session_01HkJqxHeMfUumtg2dAdWko4
The custom token layer (`--bai-color-info`, `--bai-header-bg`, …) made every `useTheme().token()` read a guessing game: is this one Astryx's or ours? The prefix bought nothing — none of the 21 names collides with an Astryx token today, and if Astryx adds one later our pinned value is meant to win anyway. So the theme tokens lose the prefix (`--color-info`, `--header-bg`, `--primary-5`, `--preset-*`, `--color-text-tertiary`, …): 143 reads across react and BUI, the builder, the Storybook brand theme, the tests and the docs. Component-local CSS custom properties (`--bai-gap`, `--bai-form-item-*`, `--bai-z-*`) are not theme tokens and keep the prefix. `THEME_NAME_REV` 24 → 25 (the token names are part of the CSS output); the prebuilt artifact is regenerated as `bai-r25-default-brand-h1ux0ib0` and the r24 files removed. Co-Authored-By: Claude Fable 5.1 <[email protected]> Claude-Session: https://claude.ai/code/session_01HkJqxHeMfUumtg2dAdWko4
…lor-text-tertiary The real-CSS declared-set test kept the three broken names of an old spike and asserted they are absent. Dropping the `--bai-` prefix made `--color-text-tertiary` a declared custom token of the brand theme, so that name leaves the absent list; a companion case asserts the theme now declares it (and `--color-info`) so the gate keeps seeing them. Co-Authored-By: Claude Fable 5.1 <[email protected]> Claude-Session: https://claude.ai/code/session_01HkJqxHeMfUumtg2dAdWko4
…e the antd palette generator The seven derived custom tokens (`--color-error-bg`, `--color-info-bg`, `--color-warning-hover`, `--color-success-border-hover`, `--color-primary-bg`, `--color-error-border`, `--primary-5`) were the last antd computation in the theme: each one ran the document's seed through the vendored `@ant-design/colors` generator to pick a palette step, in both schemes. They are now `color-mix()` references over the pinned base tokens and `--color-background-surface` — the way Astryx builds its own muted surfaces — at amounts that sit where antd's steps used to (bg ≈ step 1, border ≈ step 3, hover ≈ step 4, ramp-5 ≈ 85%/65%). A rebrand still reaches them, with no palette code; the exact tints move slightly. That leaves `antdColors.ts` with no theme consumer, so it goes, with its 840-line fixture and the palette test. The chart series colors in `UsageBucketChartContent` were the one other reader (`presetPalettes` step 3); the 13 hexes are inlined there. `THEME_NAME_REV` 25 → 26; the prebuilt artifact is regenerated as `bai-r26-default-brand-h1ux0ib0` and the r25 files removed. Co-Authored-By: Claude Fable 5.1 <[email protected]> Claude-Session: https://claude.ai/code/session_01HkJqxHeMfUumtg2dAdWko4
…seeds The v2 schema only required `schemaVersion` and `families.default`, so the Branding JSON modal — which validates through Monaco against this file — accepted a typo'd key (`seeds.acent`, `theme.fontFamly`, an unknown root key) and a malformed color (`#FF7A0`) without a marker; the loader then silently ignored them. Every object level now carries `additionalProperties: false`, and a seed value must be a 6-digit hex, which is what the HCT accent generator and the muted formula require anyway. `headerBg` is applied verbatim as a CSS background (the shipped `glass` family uses `rgba(…)`), so it gets its own `colorValue` type with no pattern. The shipped document and the Storybook copy validate unchanged. Co-Authored-By: Claude Fable 5.1 <[email protected]> Claude-Session: https://claude.ai/code/session_01HkJqxHeMfUumtg2dAdWko4
a8d5f52 to
7736662
Compare
|
Dev server on box |
agatha197
left a comment
There was a problem hiding this comment.
I just change Light Mode Logo but both About Backend.AI Modal Logo (dark mode), Login Logo (dark mode) are changed too.
agatha197
left a comment
There was a problem hiding this comment.
Read the loader, recipe and Branding editor directly and swept all 246 rewritten files: every token() name resolves, hook order is clean (TextHighlighter/ChatMessageContent even fix old violations) and the replacements match the retired mapping.ts. The inline threads are what should change before merge.
| style={{ | ||
| // Bottom gap comes from the component itself. | ||
| marginTop: -token.margin, | ||
| marginTop: -token('--spacing-4'), |
There was a problem hiding this comment.
-token('--spacing-4') is -'16px' → NaN; React drops the style with a warning, so the -16px pull-up is lost (tsc accepts unary minus on a string, so verify.sh cannot catch it). Use `-${token('--spacing-4')}` or calc(-1 * …). Same at line 958.
| // per-site colour decision BAIBadgeCount documents). | ||
| variant="error" | ||
| offset={[-token.sizeXS, -token.sizeXS]} | ||
| offset={[-token('--spacing-2'), -token('--spacing-2')]} |
There was a problem hiding this comment.
-token('--spacing-2') is NaN here too; BAIBadgeCount writes ${offset[0]}px (BAIBadgeCount.tsx:118), so the invitation pill gets NaNpx and loses its -8px offset. -parseFloat(token('--spacing-2')).
| <AuthorIcon | ||
| author={modelCard.metadata.author} | ||
| size={token.fontSizeSM} | ||
| size={parseFloat(token('--font-size-sm'))} |
There was a problem hiding this comment.
--font-size-sm is 0.75rem in the built theme (only --font-size-lg/3xl/4xl are pinned to px), so parseFloat yields 0.75 and AuthorIcon renders at 0.75px instead of 12. The same rem scale also reaches lucide size= verbatim at DeploymentRevisionHistoryTab.tsx:481/695 and DiagnosticResultList.tsx:81 — size="1em" like the sibling icons is the safe form there.
| // against the first line (not the whole wrapped block). | ||
| const lineHeightPx = token.fontSize * token.lineHeight; | ||
| const lineHeightPx = | ||
| parseFloat(token('--font-size-base')) * 1.5714285714285714; |
There was a problem hiding this comment.
--font-size-base is 0.875rem, so lineHeightPx becomes 1.375 instead of 22 and the icon box at line 1730 collapses. Either compute from a px value or hand the CSS string through (calc(${token('--font-size-base')} * 1.5714)).
| { | ||
| key: 'actions', | ||
| width: token.sizeXXL, | ||
| width: token('--spacing-12'), |
There was a problem hiding this comment.
width is now the string '48px'; BAITable only pins numeric widths (BAITable.tsx:776-779), so the action column flexes instead of staying 48px. parseFloat(token('--spacing-12')).
| }) => { | ||
| 'use memo'; | ||
| const { themeConfig, activeThemeFamily } = useCustomThemeConfig(); | ||
| const { rawThemeConfig, activeThemeFamily } = useCustomThemeConfig(); |
There was a problem hiding this comment.
With the settle gate gone this is the only reader of the document, and useRawCustomThemeConfig snapshots getCustomTheme() during render but subscribes to custom-theme-loaded in a passive effect — theme.json is in flight during the first (concurrent, yielding) render, so a document that lands between the two is missed for the session, and this provider sits above the Suspense boundary so it never remounts. Before this PR a miss was invisible (the code carried the brand seeds); now it is a neutral grey app. A useSyncExternalStore read of the store — what the removed gate used — closes the window.
| source: string, | ||
| ): BAIAppearanceConfig | undefined => { | ||
| if (!_.isPlainObject(input)) { | ||
| warn(`${source} is not a JSON object; using the built-in defaults.`); |
There was a problem hiding this comment.
"using the built-in defaults" (also line 154 "falls back to the built-in seeds", and the DEFAULT_THEME_FAMILY comment in useCustomThemeConfig.tsx:18): there are no built-in brand values any more — the operator gets Astryx's neutral theme with no Backend.AI colors. Say that, since checklist item 2 sends them to this message.
| setDefaultTheme(parsedValue); | ||
| // The Monaco markers are advisory (the schema fetch can fail); | ||
| // this is the gate every draft passes before it is stored. | ||
| const validated = pickValidAppearanceConfig( |
There was a problem hiding this comment.
pickValidAppearanceConfig checks only schemaVersion and the default family, not the schema, so the description's "validates against theme.schema.json at the write boundary" overstates it: unknown keys and non-hex seeds pass whenever the Monaco schema fetch fails. Either run ajv here (already a devDependency via themeJson.test.ts) or fix the description.
| `scripts/verify.sh` runs the CLI's `--check` for artifact staleness. | ||
| */ | ||
| export { baiR22DefaultBrandHg6tffsTheme as builtBackendAiBrandTheme } from './bai-r22-default-brand-hg6tffs'; | ||
| export { baiR26DefaultBrandH1ux0ib0Theme as builtBackendAiBrandTheme } from './bai-r26-default-brand-h1ux0ib0'; |
There was a problem hiding this comment.
The description's Verification block predates the verbatim-dark commit: it cites bai-r25 / REV 23 and probes --color-accent: light-dark(#FF7A00, #be5e06), while the artifact exported here is r26 and the dev server resolves light-dark(#FF7A00, #DC6B03). Please refresh it so checklist item 4 can be read against it.
| description: t('userSettings.theme.TextColorDesc'), | ||
| children: <ThemeColorPicker tokenName="token.colorText" />, | ||
| children: ( | ||
| <ThemeColorPicker seedPath="theme.families.default.seeds.success" /> |
There was a problem hiding this comment.
The Text Color picker is removed but the manual still lists it (packages/backend.ai-webui-docs/src/en/admin_menu/admin_menu.md:1977, ko :1658), and react/README.md:40 still sends operators to the antd theme editor for theme.json. Both need the v2 update alongside the migration guide this PR calls for.
…ding page (#9427) Co-authored-by: Claude Fable 5.1 <[email protected]>
Resolves #8919 (FR-3605)
Summary
Bottom PR of the FR-1964 app-config stack (restructured out of #8860 per the 2026-09-01 decision). Two things land together, because the second is what makes the first the app's only theme currency:
{theme, branding}appearance document in Astryx vocabulary replaces the antd-shapedtheme.json, so when FR-1964 serves it from app config no antd-shaped document is ever stored and no data migration is needed later.theme.useToken()consumer (189 files acrossreact/andbackend.ai-ui) reads Astryx tokens throughuseTheme().token('<css-var>'); the antd-vocabularyThemeShimProviderand its runtime palette machinery are deleted, and<AstryxBrandTheme>is the app's single token provider. What the shim used to compute for names Astryx has no token for now lives in the theme itself as custom custom tokens.{ "schemaVersion": 2, "theme": { // feeds the Astryx pipeline "fontFamily": "'Ubuntu', Roboto, sans-serif", "siderMode": "dark", // optional (was sider.theme) "families": { "default": { // explicit entry; top-level light/dark synthesis removed "seeds": { // string = both schemes, [light, dark] tuple splits them "accent": ["#FF7A00", "#DC6B03"], "link": ["#FF7A00", "#DC6B03"], "info": ["#028DF2", "#009BDD"], "error": ["#FF4D4F", "#DC4446"], "success": ["#00BD9B", "#03A487"], "warning": ["#FAAD14", "#FAAD14"] // declared explicitly now: nothing in code fills in a seed the document omits }, "headerBg": ["#FF9729", "#E88A28"] // was components.Layout.headerBg } } }, "branding": { // structural metadata no token can express "logo": { /* unchanged LogoConfig */ }, "companyName": "Lablup Inc.", "brandName": "Backend.AI", "familyLabels": { "stained": "Stained" } // was families[*].label } }How the document reaches the screen
theme.json→loadCustomThemeConfig→useCustomThemeConfig→AstryxBrandTheme→resolveRoleTheme→buildBackendAITheme→ AstryxdefineTheme→<Theme>. The seeds run through Astryx's generator (derived accent ramp) and the recipe pins the parity tokens on top. The document is the only source of brand values: the recipe carries no defaults, a seed the document omits is simply not pinned (Astryx's token applies; the custom vocabulary falls back to Astryxvar()references), and the prebuilt artifact is built fromresources/theme.jsonitself. Nothing gates the first paint on the document: the first visible screen is already held byuseSuspendedBackendaiClient(config + client init), whichtheme.jsonbeats, so the default theme is never on screen before the document applies (checklist 3). The shipped document still hits the precompiled CSS (bai-r25-…,THEME_NAME_REV22 → 24: 23 for the custom token layer, 24 for the verbatim dark values and the muted pin below, 25 for dropping the--bai-prefix from the custom tokens, 26 for thecolor-mix()tints).The custom token layer (
packages/backend.ai-ui/src/theme/baiCustomTokens.ts)Custom tokens carry no prefix (
--color-info,--header-bg,--primary-5, …): auseTheme().token()read looks the same whether the token is Astryx's or ours, and a name Astryx later adds is overridden by our value by design. Component-local CSS custom properties (--bai-gap,--bai-form-item-*,--bai-z-*) are not theme tokens and keep their prefix.Shared by the app recipe and the Storybook brand theme:
--color-info,--color-linkoptions.link, hash input)--header-bgheaderBg, verbatim per scheme, with a default-family fallback the shim path never had--color-{error-bg, info-bg, warning-hover, success-border-hover, primary-bg, error-border}color-mix()tints of the pinned base token over--color-background-surface(bg ≈ antd step 1, border ≈ step 3, hover ≈ step 4), so they follow a rebrand with no palette code--primary-5--color-text-*,--color-fill-*,--color-bg-container-disabled,--preset-*Dark values render as declared. The antd
darkAlgorithmre-mapping of the dark seed (ANTD_DARK_ALGORITHM_OUTPUT/resolveDarkSeed,#DC6B03→#be5e06) is gone with the shim: the[light, dark]tuple is pinned verbatim, so what an operator writes is what renders. The vendored antd palette generator (antdColors.ts, its fixture and test) is deleted with it: the derived bg/border/hover tints arecolor-mix()references now, and the one remaining consumer ofpresetPalettes(UsageBucketChartContent's series colors) carries its 13 hexes inline.--color-accent-mutedis pinned as well (accent at 20%/25% alpha, like the status muted steps): Astryx'suseTheme()re-applies the base theme's input tokens over generated ones, so an unpinned muted resolved toneutralTheme's grey in the hook while the CSS showed the brand tint.The sweep, mechanically
mapping.ts— the shim's own verdict table — was the lookup:token.marginMD→token('--spacing-5'),token.colorBgContainer→token('--color-background-surface'), brand names →--color-accent/--color-info/…,token.Layout?.headerBg→--header-bg(headerHeightwas never provided, so its?? 60fallbacks were constants — inlined). Value semantics changed from antd numbers to CSS strings, so the non-style positions were fixed by hand: negative margins viacalc(), numeric props viaparseFloat(),BAIFlex gapsites pass the rung name.useThemeis a real hook whereuseTokenread a store, so the calls inside ChatMessageContent's render callbacks and after the TextHighlighters' early returns are hoisted. No visual change intended — every replacement resolves to the value the shim produced for the same document and mode.Storybook
ThemeShimProviderleaves the decorators. The FR-3819 "Theme" toolbar presets keep their Astryx layer (neutralThemevs the brand theme) — with every component onuseTheme()that layer is the whole switch..storybook/theme.jsonis a copy of the v2 document andastryxBrandTheme.tsbuilds from it through the shared BUI helpers, so the custom tokens reach stories too.⚠ Breaking for operator
theme.json"schemaVersion": 2or a fetch/parse failure. Atheme.familiesblock withoutdefaultis accepted with a warning. Operators with a customtheme.jsonmust migrate (release notes + migration guide required before release; field map below).darkAlgorithm(#DC6B03rendered as#be5e06); v2 pins it verbatim, so the shipped dark brand shifts to the document's values (accent/link#DC6B03, error#DC4446, success#03A487, info#009BDD, warning#FAAD14). An operator who wants the previously rendered darks writes those instead (#be5e06,#be3d3f,#068e76,#0387bf,#d89614).reverseis a<Theme mode>inversion, not a role). The document stores brand seeds only.colorText,colorBorder,colorBgContainer,colorBgLayout,colorFillSecondary,screen*breakpoints,components.Tag/Divider/Table,siderBg/lightSiderBg, and the antd component-token passthrough beyondLayout.headerBg.custom_primary_color/ThemeAccentColorPickerremoved (2026-08-19 decision); the shim'susePrimaryColorsis gone with the shim.Branding editor
branding.logo.*; the font item writestheme.fontFamily. The dead Text Color item is removed.theme.schema.jsonin Monaco and at the write boundary (pickValidAppearanceConfig). The schema rejects unknown keys at every level (additionalProperties: false) and non-6-digit-hex seeds;headerBgalone takes any CSS color, since it is applied verbatim (the shippedglassfamily usesrgba(…)), so a draft cannot bypass the loud v1 rejection when the schema fetch fails.antdThemeConfig.schema.jsonand its generator script are deleted; a newthemeJson.test.ts(ajv) keepstheme.json, the schema and the loader agreeing.v1 → v2 field map (for the migration guide)
light.token.colorPrimary/dark.token.colorPrimarytheme.families.<f>.seeds.accent[light, dark]*.token.colorLinktheme.families.<f>.seeds.link*.token.colorInfotheme.families.<f>.seeds.info*.token.colorError/colorSuccess/colorWarningtheme.families.<f>.seeds.error/success/warning*.components.Layout.headerBgtheme.families.<f>.headerBglight/dark(default family)theme.families.defaultentryfamilies.<f>.labelbranding.familyLabels.<f>fontFamilytheme.fontFamilysider.themetheme.siderModelogo,branding.companyName/brandNamebranding.logo,branding.companyName/brandNamecolorText,colorBorder,colorBg*,screen*,Tag/Divider/Table,siderBgVerification
bash scripts/verify.sh→=== ALL PASS ===(Relay, Lint, Format, TypeScript, Vite warmup, StyleX, Astryx theme build — prebuilt artifactbai-r25-default-brand-h1ux0ib0current (THEME_NAME_REV 23), z-index mirrors, Terminology; the one terminology finding is the pre-existing warn-onlyChangeGroupColor)vitest run— react 109 files / 1714 tests, backend.ai-ui 74 files / 1955 tests (1 skipped), root 5 files / 111 tests. New:react/src/helper/themeJson.test.ts(ajv: theme.json ↔ schema ↔ loader),packages/backend.ai-ui/src/theme/antdColors.test.ts(vendored palette parity, moved), rewrittencustomThemeConfig/backendAiThemesuites.build-storybook→ completed (decorators + brand theme on the shared BUI helpers, v2theme.jsoncopy)data-astryx-theme="bai-r25-default-brand-h1ux0ib0") and resolves--color-accent: light-dark(#FF7A00, #be5e06),--color-info: light-dark(#028DF2, #0387bf),--header-bg: light-dark(#FF9729, #E88A28),--primary-5: light-dark(#ff9729, #cc7c27), the--color-text-*alpha ramp and--spacing-5: 20px; no page/console errors beyond the pre-existing Geist-font CSP notice.mapping.ts(deleted) was the codemod's lookup table; everytoken('<var>')in the sweep is the var the shim already resolved that antd name to. The token values changed from antd numbers to CSS strings — the hand-fixed sites (calc() margins, parseFloat() props, BAIFlex gap rungs, the hoisted hooks) are listed in the sweep commit.Reviewer checklist
/colorand confirm the WebUI header band takes that color (VITE_THEME_HEADER_COLOR→--header-bg).resources/theme.json(or serve a v1 / non-v2 document) and confirm the app renders on Astryx's default theme — no Backend.AI colors — with a[appearance] …console error naming the reason.default.seeds.accentinresources/theme.json, hard-reload, and confirm the first painted screen already carries the new color — Astryx's default theme never shows beforetheme.jsonloads.#DC6B03(not antd's#be5e06).Stack
Restructured from single-PR #8860: this PR (document + shim retirement) → #8860 (serve from app config + bootstrap) → #9362 (userConfig family + Branding Apply).
🤖 Generated with Claude Code
https://claude.ai/code/session_01HkJqxHeMfUumtg2dAdWko4