Skip to content

feat(FR-3605): convert theme.json to the v2 appearance document and retire the theme-shim - #9361

Open
ironAiken2 wants to merge 18 commits into
mainfrom
feat/FR-3605-theme-v2-document
Open

feat(FR-3605): convert theme.json to the v2 appearance document and retire the theme-shim#9361
ironAiken2 wants to merge 18 commits into
mainfrom
feat/FR-3605-theme-v2-document

Conversation

@ironAiken2

@ironAiken2 ironAiken2 commented Sep 1, 2026

Copy link
Copy Markdown
Contributor

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:

  1. The v2 {theme, branding} appearance document in Astryx vocabulary replaces the antd-shaped theme.json, so when FR-1964 serves it from app config no antd-shaped document is ever stored and no data migration is needed later.
  2. The theme-shim is retired. Every theme.useToken() consumer (189 files across react/ and backend.ai-ui) reads Astryx tokens through useTheme().token('<css-var>'); the antd-vocabulary ThemeShimProvider and 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.jsonloadCustomThemeConfiguseCustomThemeConfigAstryxBrandThemeresolveRoleThemebuildBackendAITheme → Astryx defineTheme<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 Astryx var() references), and the prebuilt artifact is built from resources/theme.json itself. Nothing gates the first paint on the document: the first visible screen is already held by useSuspendedBackendaiClient (config + client init), which theme.json beats, 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_REV 22 → 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 the color-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, …): a useTheme().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:

Token Source
--color-info, --color-link brand seeds Astryx has no token family for (+ options.link, hash input)
--header-bg family headerBg, 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 the accent at 85% (light) / 65% (dark) over the surface — the ramp step progress fills consume
--color-text-*, --color-fill-*, --color-bg-container-disabled, --preset-* antd's neutral alpha ramp and the preset steps, verbatim

Dark values render as declared. The antd darkAlgorithm re-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 are color-mix() references now, and the one remaining consumer of presetPalettes (UsageBucketChartContent's series colors) carries its 13 hexes inline. --color-accent-muted is pinned as well (accent at 20%/25% alpha, like the status muted steps): Astryx's useTheme() re-applies the base theme's input tokens over generated ones, so an unpinned muted resolved to neutralTheme'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.marginMDtoken('--spacing-5'), token.colorBgContainertoken('--color-background-surface'), brand names → --color-accent/--color-info/…, token.Layout?.headerBg--header-bg (headerHeight was never provided, so its ?? 60 fallbacks were constants — inlined). Value semantics changed from antd numbers to CSS strings, so the non-style positions were fixed by hand: negative margins via calc(), numeric props via parseFloat(), BAIFlex gap sites pass the rung name. useTheme is a real hook where useToken read 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

ThemeShimProvider leaves the decorators. The FR-3819 "Theme" toolbar presets keep their Astryx layer (neutralTheme vs the brand theme) — with every component on useTheme() that layer is the whole switch. .storybook/theme.json is a copy of the v2 document and astryxBrandTheme.ts builds from it through the shared BUI helpers, so the custom tokens reach stories too.

⚠ Breaking for operator theme.json

  • The loader reads v2 only — a v1 document logs a loud console error and falls back to Astryx's neutral theme (no Backend.AI colors: the code holds no brand values of its own); so does any document without "schemaVersion": 2 or a fetch/parse failure. A theme.families block without default is accepted with a warning. Operators with a custom theme.json must migrate (release notes + migration guide required before release; field map below).
  • Dark-side values render as declared. Under antd the dark seed was an input to darkAlgorithm (#DC6B03 rendered 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).
  • Roles stay derived in code (brand←accent, admin←info, secondary←success; reverse is a <Theme mode> inversion, not a role). The document stores brand seeds only.
  • Dropped v1 keys with no consumer anywhere (verified): colorText, colorBorder, colorBgContainer, colorBgLayout, colorFillSecondary, screen* breakpoints, components.Tag/Divider/Table, siderBg/lightSiderBg, and the antd component-token passthrough beyond Layout.headerBg.
  • custom_primary_color / ThemeAccentColorPicker removed (2026-08-19 decision); the shim's usePrimaryColors is gone with the shim.

Branding editor

  • Color pickers write whole seed tuples; the untouched scheme keeps the recipe's rendered default instead of inheriting the color just picked. Logo items write branding.logo.*; the font item writes theme.fontFamily. The dead Text Color item is removed.
  • The JSON modal validates against the single theme.schema.json in Monaco and at the write boundary (pickValidAppearanceConfig). The schema rejects unknown keys at every level (additionalProperties: false) and non-6-digit-hex seeds; headerBg alone takes any CSS color, since it is applied verbatim (the shipped glass family uses rgba(…)), so a draft cannot bypass the loud v1 rejection when the schema fetch fails. antdThemeConfig.schema.json and its generator script are deleted; a new themeJson.test.ts (ajv) keeps theme.json, the schema and the loader agreeing.
  • A stale pre-v2 draft is reseeded from the shipped document (never from the applied one — in preview mode that is the draft itself).
  • Apply-to-domain is not in this PR — the editor still previews only; saving to app config arrives at the top of the stack (feat(FR-3834): move theme family to userConfig and save Branding edits to app config #9362).

v1 → v2 field map (for the migration guide)

v1 (antd-shaped) v2
light.token.colorPrimary / dark.token.colorPrimary theme.families.<f>.seeds.accent [light, dark]
*.token.colorLink theme.families.<f>.seeds.link
*.token.colorInfo theme.families.<f>.seeds.info
*.token.colorError / colorSuccess / colorWarning theme.families.<f>.seeds.error / success / warning
*.components.Layout.headerBg theme.families.<f>.headerBg
top-level light/dark (default family) explicit theme.families.default entry
families.<f>.label branding.familyLabels.<f>
fontFamily theme.fontFamily
sider.theme theme.siderMode
logo, branding.companyName/brandName branding.logo, branding.companyName/brandName
colorText, colorBorder, colorBg*, screen*, Tag/Divider/Table, siderBg dropped (no consumer)

Verification

  • bash scripts/verify.sh=== ALL PASS === (Relay, Lint, Format, TypeScript, Vite warmup, StyleX, Astryx theme build — prebuilt artifact bai-r25-default-brand-h1ux0ib0 current (THEME_NAME_REV 23), z-index mirrors, Terminology; the one terminology finding is the pre-existing warn-only ChangeGroupColor)
  • 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), rewritten customThemeConfig / backendAiTheme suites.
  • build-storybook → completed (decorators + brand theme on the shared BUI helpers, v2 theme.json copy)
  • Live probe (dev server, light + dark, headless Chromium): the app boots on the prebuilt theme (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.
  • Mechanics worth knowing when reviewing: mapping.ts (deleted) was the codemod's lookup table; every token('<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

  • 1. Start the dev server with a Claude /color and confirm the WebUI header band takes that color (VITE_THEME_HEADER_COLOR--header-bg).
  • 2. Remove 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.
  • 3. Change default.seeds.accent in resources/theme.json, hard-reload, and confirm the first painted screen already carries the new color — Astryx's default theme never shows before theme.json loads.
  • 4. Switch to dark mode and confirm the accent is the document's dark value #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

@github-actions github-actions Bot added area:ux UI / UX issue. area:i18n Localization size:XL 500~ LoC labels Sep 1, 2026
@github-actions

github-actions Bot commented Sep 1, 2026

Copy link
Copy Markdown
Contributor

Coverage Report for root-coverage

Status Category Percentage Covered / Total
🔵 Lines 18.79% 28 / 149
🔵 Statements 20% 32 / 160
🔵 Functions 37.5% 6 / 16
🔵 Branches 26.47% 18 / 68
File CoverageNo changed files found.
Generated in workflow #127 for commit 2c9922f by the Vitest Coverage Report Action

@github-actions

github-actions Bot commented Sep 1, 2026

Copy link
Copy Markdown
Contributor

⚠️ Coverage report too large to inline, see the workflow summary.

@ironAiken2

ironAiken2 commented Sep 1, 2026

Copy link
Copy Markdown
Contributor Author

@ironAiken2
ironAiken2 force-pushed the feat/FR-3605-theme-v2-document branch from 2c3f3e6 to 28df890 Compare September 2, 2026 07:39
@ironAiken2 ironAiken2 changed the title feat(FR-3605): convert theme.json to the v2 {theme, branding} appearance document feat(FR-3605): convert theme.json to the v2 appearance document and retire the theme-shim Sep 2, 2026
@ironAiken2
ironAiken2 force-pushed the feat/FR-3605-theme-v2-document branch from 4314cde to 7f69b7b Compare September 2, 2026 10:18
@github-actions

github-actions Bot commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

Coverage Report for backend-ai-ui-coverage (./packages/backend.ai-ui)

Status Category Percentage Covered / Total
🔵 Lines 44.01% 4214 / 9573
🔵 Statements 38.73% 4990 / 12882
🔵 Functions 40.57% 811 / 1999
🔵 Branches 29.54% 3649 / 12349
File Coverage
File Stmts Branches Functions Lines Uncovered Lines
Changed Files
packages/backend.ai-ui/src/components/BAIBoardItemErrorBoundary.tsx 20% 0% 0% 20% 22-46
packages/backend.ai-ui/src/components/BAIBoardItemTitle.tsx 60% 0% 0% 60% 24-64
packages/backend.ai-ui/src/components/BAIBulkErrorModal.tsx 72.72% 51.42% 100% 94.44% 66, 81, 83, 96, 98, 107, 111, 112, 113
packages/backend.ai-ui/src/components/BAICountdownBorder.tsx 2.08% 0% 0% 3.84% 54-91
packages/backend.ai-ui/src/components/BAIFlex.tsx 88% 77.77% 100% 91.66% 48, 74-75
packages/backend.ai-ui/src/components/BAIListAlert.tsx 80% 58.82% 100% 87.5% 30, 56, 58
packages/backend.ai-ui/src/components/BAINotificationItem.tsx 25% 0% 0% 25% 37-39, 50-109
packages/backend.ai-ui/src/components/BAIProgressWithLabel.tsx 25% 0% 0% 25% 46-97
packages/backend.ai-ui/src/components/BAIQuestionIconWithTooltip.tsx 33.33% 0% 0% 33.33% 71-84
packages/backend.ai-ui/src/components/BAIRowWrapWithDividers.tsx 2% 0% 0% 2.12% 35-131
packages/backend.ai-ui/src/components/BAISelectionLabel.tsx 5.26% 0% 0% 12.5% 37-48
packages/backend.ai-ui/src/components/BAITextHighlighter.tsx 11.11% 0% 0% 12.5% 16-40
packages/backend.ai-ui/src/components/BAIVFolderDeleteButton.tsx 4.54% 0% 0% 7.69% 44-64
packages/backend.ai-ui/src/components/BAIVFolderDeleteButtonV2.tsx 6.25% 0% 0% 11.11% 37-57
packages/backend.ai-ui/src/components/ResourceStatistics.tsx 16.66% 0% 0% 16.66% 38-46, 50-53, 63-140
packages/backend.ai-ui/src/components/Table/BAINameActionCell.tsx 57.22% 47.05% 43.75% 59.7% 158-163, 279, 295, 300-308, 319-322, 374-405, 423-436, 439-445, 451-452, 570, 222-395, 523-529
packages/backend.ai-ui/src/components/Table/BAITable.tsx 73.99% 72.72% 63.79% 77.29% 545-546, 552-564, 576, 581-582, 603-604, 610-615, 623-625, 703-707, 732, 755, 757, 766-769, 834, 947, 958, 980, 983-994, 998-1008, 1032, 1037, 1044-1049, 1046, 1065, 1076-1077, 1098-1118, 1138-1141, 1446-1452, 1363-1369, 1389-1436, 1410-1447
packages/backend.ai-ui/src/components/Table/BAITableColumnCSVExportModal.tsx 1.19% 0% 0% 1.98% 56-58, 62-253
packages/backend.ai-ui/src/components/Table/BAITableSettingModal.tsx 82.22% 73.98% 52.94% 87.77% 83, 90, 95, 96, 97, 114, 116, 151, 152-153, 156-162, 164, 182-183, 194, 202, 213, 225, 253, 259-268, 120-264
packages/backend.ai-ui/src/components/baiClient/FileExplorer/BAIFileExplorer.tsx 2.08% 0% 0% 2.15% 113-489
packages/backend.ai-ui/src/components/baiClient/FileExplorer/EditableFileName.tsx 1.36% 0% 0% 1.72% 71-117
packages/backend.ai-ui/src/components/baiClient/FileExplorer/ExplorerActionControls.tsx 1.66% 0% 0% 1.75% 77-350
packages/backend.ai-ui/src/components/fragments/BAIAgentTable.tsx 4.44% 0% 5% 6.84% 62, 67-72, 93-104, 255-503, 506-516, 537-542, 606-794
packages/backend.ai-ui/src/components/fragments/BAIAllowedVfolderHostsWithPermission.tsx 4.76% 0% 0% 4.76% 48-198
packages/backend.ai-ui/src/components/fragments/BAIArtifactRevisionDeleteButton.tsx 14.28% 0% 0% 14.28% 20-54
packages/backend.ai-ui/src/components/fragments/BAIArtifactRevisionDownloadButton.tsx 14.28% 0% 0% 14.28% 20-51
packages/backend.ai-ui/src/components/fragments/BAIDeleteArtifactRevisionsModal.tsx 3.84% 0% 0% 3.84% 53-169
packages/backend.ai-ui/src/components/fragments/BAIImportArtifactModal.tsx 3.44% 0% 0% 3.44% 64-201
packages/backend.ai-ui/src/components/fragments/BAIRouteNodes.tsx 11.11% 0% 8.33% 15.9% 47, 86-251
packages/backend.ai-ui/src/components/fragments/BAIVFolderMountConfigInput.tsx 6.54% 0% 0% 8.97% 74-77, 82-85, 114-148, 156-157, 193-446
packages/backend.ai-ui/src/form-engine/FormItemVisual.tsx 83.76% 74.39% 100% 85.33% 188-201, 207, 208, 404, 470, 495, 508, 212-329, 212-370
packages/backend.ai-ui/src/form-engine/feedbackIcons.tsx 42.85% 0% 0% 50% 67-77
packages/backend.ai-ui/src/helper/index.ts 46.79% 40.8% 42.42% 51.07% 33-34, 42-44, 53-61, 123, 254-256, 270, 295-310, 314-316, 329, 333, 367-373, 427-441, 446-447, 458-463, 468-474, 484-491, 502-523
packages/backend.ai-ui/src/hooks/index.ts 10.52% 0% 0% 18.18% 29-36, 47-54, 58, 63, 66-105
packages/backend.ai-ui/src/styles/zIndexLadder.ts 100% 100% 100% 100%
packages/backend.ai-ui/src/theme/baiCustomTokens.ts 57.14% 0% 0% 80% 35-37, 46-48, 77-107
packages/backend.ai-ui/src/theme/index.ts 100% 100% 100% 100%
Generated in workflow #305 for commit 2c9922f by the Vitest Coverage Report Action

@ironAiken2
ironAiken2 force-pushed the feat/FR-3605-theme-v2-document branch from 7f69b7b to 0daba8c Compare September 3, 2026 03:16
@github-actions

github-actions Bot commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

PR Analysis Report

📚 Storybook Preview

View Storybook for this PR

GitHub Pages may take up to a minute to hydrate after the deploy.

🧩 Changed Components

Component Changed Stories Open
BAIAgentTable source 7 Storybook ↗
BAIArtifactRevisionDeleteButton source 5 Storybook ↗
BAIArtifactRevisionDownloadButton source 5 Storybook ↗
BAIBoardItemTitle source 8 Storybook ↗
BAIBulkErrorModal source 3 Storybook ↗
BAIDeleteArtifactRevisionsModal source 4 Storybook ↗
BAIFlex source 8 Storybook ↗
BAIImportArtifactModal source 4 Storybook ↗
BAIListAlert source 5 Storybook ↗
BAINameActionCell source 14 Storybook ↗
BAINotificationItem source 3 Storybook ↗
BAIProgressWithLabel source 7 Storybook ↗
BAIQuestionIconWithTooltip source 4 Storybook ↗
BAIRowWrapWithDividers source 9 Storybook ↗
BAISelectionLabel source 3 Storybook ↗
BAITable source 10 Storybook ↗
BAITextHighlighter source 6 Storybook ↗
BAIVFolderDeleteButton source 5 Storybook ↗
BAIVFolderMountConfigInput source 6 Storybook ↗
ResourceStatistics source 7 Storybook ↗
10 changed component(s) with no story to open
  • BAIAllowedVfolderHostsWithPermissionpackages/backend.ai-ui/src/components/fragments/BAIAllowedVfolderHostsWithPermission.tsx
  • BAIBoardItemErrorBoundarypackages/backend.ai-ui/src/components/BAIBoardItemErrorBoundary.tsx
  • BAICountdownBorderpackages/backend.ai-ui/src/components/BAICountdownBorder.tsx
  • BAIFileExplorerpackages/backend.ai-ui/src/components/baiClient/FileExplorer/BAIFileExplorer.tsx
  • BAIRouteNodespackages/backend.ai-ui/src/components/fragments/BAIRouteNodes.tsx
  • BAITableColumnCSVExportModalpackages/backend.ai-ui/src/components/Table/BAITableColumnCSVExportModal.tsx
  • BAITableSettingModalpackages/backend.ai-ui/src/components/Table/BAITableSettingModal.tsx
  • BAIVFolderDeleteButtonV2packages/backend.ai-ui/src/components/BAIVFolderDeleteButtonV2.tsx
  • EditableFileNamepackages/backend.ai-ui/src/components/baiClient/FileExplorer/EditableFileName.tsx
  • ExplorerActionControlspackages/backend.ai-ui/src/components/baiClient/FileExplorer/ExplorerActionControls.tsx
26 other changed file(s) in packages/backend.ai-ui/
  • packages/backend.ai-ui/.storybook/astryxBrandTheme.ts
  • packages/backend.ai-ui/.storybook/decorators.tsx
  • packages/backend.ai-ui/.storybook/theme.json
  • packages/backend.ai-ui/.storybook/themeConfig.ts
  • packages/backend.ai-ui/src/form-engine/FormItemVisual.css
  • packages/backend.ai-ui/src/form-engine/FormItemVisual.tsx
  • packages/backend.ai-ui/src/form-engine/feedbackIcons.tsx
  • packages/backend.ai-ui/src/helper/index.ts
  • packages/backend.ai-ui/src/hooks/index.ts
  • packages/backend.ai-ui/src/hooks/useBAIBreakpoint.test.tsx
  • …and 16 more

📦 Bundle Size

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).


Generated by the PR Preview workflow · Storybook · CI run

github-actions Bot added a commit that referenced this pull request Sep 3, 2026
github-actions Bot added a commit that referenced this pull request Sep 3, 2026
github-actions Bot added a commit that referenced this pull request Sep 3, 2026
@ironAiken2
ironAiken2 marked this pull request as ready for review September 3, 2026 06:35

@yomybaby yomybaby left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

github-actions Bot added a commit that referenced this pull request Sep 3, 2026
@ironAiken2
ironAiken2 marked this pull request as draft September 3, 2026 07:22
github-actions Bot added a commit that referenced this pull request Sep 3, 2026
ironAiken2 and others added 17 commits September 3, 2026 11:59
…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
… 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
@ironAiken2
ironAiken2 force-pushed the feat/FR-3605-theme-v2-document branch from a8d5f52 to 7736662 Compare September 3, 2026 12:05
agatha197

This comment was marked as resolved.

@agatha197

Copy link
Copy Markdown
Contributor

@agatha197 agatha197 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.

I just change Light Mode Logo but both About Backend.AI Modal Logo (dark mode), Login Logo (dark mode) are changed too.

@agatha197 agatha197 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.

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'),

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.

-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')]}

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.

-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'))}

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.

--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;

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.

--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'),

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.

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();

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.

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.`);

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.

"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(

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.

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';

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.

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" />

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.

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.

github-actions Bot added a commit that referenced this pull request Sep 4, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area:i18n Localization area:ux UI / UX issue. astryx size:XL 500~ LoC theme

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Convert theme.json to the v2 {theme, branding} appearance document (Astryx vocabulary)

3 participants