Repository navigation
refactor(frontend): read chart colors from design tokens - #1360
Merged
Merged
Conversation
Chart.js paints on canvas, so the dashboard and savings-history charts hard-coded their hex colors and no stylesheet could restyle them: a theme or contrast fix could reach every surface except the charts. Add --cudly-chart-* tokens (series palette, cumulative line, legend swatches, axis text and gridlines) to the :root block in base.css, with the same values the charts used, and lib/chart-theme.ts to read them at render time (falling back to the same values when the stylesheet is absent, e.g. under jsdom). The dashboard per-service bars, legend and savings trend, and the savings-history chart now take every color from it, and Chart.js's global text/grid defaults follow the tokens too. The ladder chart's existing token reader moves into the same module. Also swap the stylesheet literals that already equal a token's value (#fff surfaces and button text, #f8f9fa, #2563eb) for the token. With no token values changed, every chart and surface renders pixel-identical. New tests keep the TypeScript fallbacks in sync with base.css, keep the chart modules free of color literals, and cap the remaining stylesheet color literals at today's 139 so the count can only go down. This is the theming groundwork from the issue. Migrating the remaining one-off literals and adding a prefers-color-scheme dark palette are left for follow-ups that need palette decisions. Refs LeanerCloud#699 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Contributor
Conflicts: - frontend/src/dashboard.ts: main (LeanerCloud#1335, Refs LeanerCloud#189) recoloured the per-service savings chart by layer (one hex per stacked layer in SAVINGS_LAYER_COLORS, Chart.js default legend) and removed the per-service palette, darkenHexColor and the custom legend; this branch had moved those per-service colours onto --cudly-chart-* tokens. Kept main's layer design and this branch's tokens: the three layer colours are now --cudly-chart-layer-current/-lowest/-upside tokens (main's exact hex values) read at render time by a new savingsLayerColors(), which replaces the SAVINGS_LAYER_COLORS constant. Dropped serviceBarColor, KNOWN_SERVICE_COLOR_INDEX, legendSwatchColors, darkenHexColor and the parseHexColor re-export, as main did. Import block: main's formatTrendTooltipTitle plus the chart-theme helpers still used (applyChartDefaults, chartColor, withAlpha). - frontend/src/styles/base.css: :root token block. Kept both main's motion tokens (LeanerCloud#725) and this branch's chart tokens; replaced the now-unused --cudly-chart-legend-dark/-mid/-border tokens with the three layer tokens. - frontend/src/lib/chart-theme.ts (follows from the above): fallbacks swap the legend tokens for the layer tokens; comments updated. - frontend/src/__tests__/chart-theme.test.ts, dashboard.test.ts (follow from the above): the themed-palette test now overrides a layer token via savingsLayerColors(); main's layer-colour tests call savingsLayerColors() instead of SAVINGS_LAYER_COLORS, assertions unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ing main Main's CSS tidy-ups since a14dc8f removed three more literals, so the ratchet drops from 139 to the current count. Refs LeanerCloud#699 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The Trends sub-tab chart that landed on main (LeanerCloud#1317) hard-coded its bar and gridline colors. They already equal --cudly-chart-2 and --cudly-chart-grid-faint, so it now reads those tokens (and the shared Chart.js text/grid defaults) like the other charts, rendering identically, and the no-color-literals test covers trends.ts. Refs LeanerCloud#699 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Collaborator
Author
|
Independent review (IAmJSD's agent, fresh context, given only the diff, the issue and the PR text): APPROVE Blocking:
|
Brings in main at b42245c (LeanerCloud#1369 and the UI PRs before it) so the chart-theme guards run against the current chart code. Conflicts and resolutions: - frontend/src/dashboard.ts imports: kept main's enabled-providers, kpi-tile, upcoming-scope, chart-utils (sparse trend, upsertChart) and chart-a11y imports plus this branch's lib/chart-theme import. - frontend/src/dashboard.ts renderSavingsByService: kept main's LeanerCloud#744 summary and "View as table" before the chart and LeanerCloud#747's upsertChart, with this branch's savingsLayerColors() and applyChartDefaults() read right before it. Main's LeanerCloud#744 SEGMENT_GAP spread stays on all three layers. - frontend/src/dashboard.ts loadSavingsTrendChart: main's LeanerCloud#747 upsertChart with this branch's --cudly-chart-1 trend color and applyChartDefaults(). - frontend/src/modules/savings-history.ts imports: kept both (main's upsertChart, shared formatters and chart-a11y; this branch's chart-theme). - frontend/src/modules/savings-history.ts renderSavingsChart: took main's LeanerCloud#749 single-series dataset (line for cumulative, bar per period), LeanerCloud#744 summary/table and LeanerCloud#747 upsertChart; the colors main added are read from the existing tokens with the same values: cumulative line --cudly-chart-cumulative (#4285f4, fill at 0.1 alpha), period bars --cudly-chart-2 (#34a853 border, fill at 0.7 alpha). applyChartDefaults() runs before upsertChart. - frontend/src/styles/modals.css .archera-offer-panel: took main's var(--cudly-r-lg) and var(--cudly-shadow-dialog); the background keeps this branch's var(--cudly-surface). Main's '#ffffff' segment gap in dashboard.ts still trips the "no color literals" guard here; the next commit moves it to a token. Refs LeanerCloud#699 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Main's LeanerCloud#744 gave the per-service savings chart a 2px '#ffffff' gap between stacked layers, a color literal in dashboard.ts that the chart-theme guard rejects and that a theme could not reach. The gap is meant to read as the card background, so it now uses the --cudly-surface token (#ffffff, so the chart renders the same) through a new surfaceColor() in lib/chart-theme.ts, read at render time like the other chart colors. The fallback for jsdom or a failed stylesheet load equals base.css, and a test keeps the two in sync. Refs LeanerCloud#699 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This PR lands after the other UI PRs, so the ratchet's baseline is the count on main at that point. Merged with main at b42245c, styles/*.css hold 113 color literals outside token definitions (components 43, modals 29, settings 14, responsive 13, tables 8, layout 4, forms 2), down from 136. The comment now says the baseline is the count when the ratchet landed. Refs LeanerCloud#699 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Collaborator
Author
|
Independent review (IAmJSD's agent, fresh context, given only the diff, the issue and the PR text): APPROVE Blocking:
|
Collaborator
Author
|
Local verification at
|
IAmJSD
marked this pull request as ready for review
October 25, 2026 00:13
Collaborator
Author
|
Marked ready and merged by IAmJSD's agent, at IAmJSD's request (IAmJSD reviewed the before/after screenshots; cristim is traveling), after the independent review and local verification above, all at |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Charts hard-code their hex colors in TypeScript and the stylesheets hold 121 raw hex/rgb colors (on main at b42245c) that bypass the
--cudly-*design tokens, so no theme (dark mode, a contrast fix) could reach the charts and nothing stopped new literals. This PR adds the theming path for charts (--cudly-chart-*tokens read through a newlib/chart-theme.ts), swaps CSS literals that already equal a token, and adds guard tests. Nothing renders differently by default.Refs #699
Partial: the remaining 113 one-off CSS literals and a
prefers-color-schemedark palette are left for follow-ups, because both need palette decisions.What changed
frontend/src/styles/base.css:--cudly-chart-*tokens in:root(10-color series palette, cumulative line, 3 per-service layer tokens--cudly-chart-layer-current/-lowest/-upside, axis text, 2 gridline tokens), each with the exact value the charts used before.frontend/src/lib/chart-theme.ts: reads the tokens at render time throughchartColor(),chartPalette(),withAlpha(),applyChartDefaults()(sets Chart.js's global text and grid colors) andsurfaceColor()(reads--cudly-surface).requireToken()moves here fromladder-timeline-chart.ts, andparseHexColorlives here too. Fallbacks repeat thebase.cssvalues for jsdom or a failed CSS load.frontend/src/dashboard.ts: the per-service savings chart reads its three layer colors through a newsavingsLayerColors()(replacing theSAVINGS_LAYER_COLORSconstant, with main's exact values as the token defaults), and the 2px gap between its stacked segments reads--cudly-surface(#ffffff) instead of'#ffffff'. The savings trend line reads--cudly-chart-1.dashboard.test.tscallssavingsLayerColors()where it used the constant.frontend/src/modules/savings-history.ts: the single-series chart reads its colors from the tokens: the cumulative line from--cudly-chart-cumulative(#4285f4, fill at 0.1 alpha), the per-period bars from--cudly-chart-2(#34a853border, fill at 0.7 alpha), the gridlines from--cudly-chart-grid-faint.frontend/src/trends.ts(Inventory > Trends bar chart) reads its series and gridline colors from the tokens.dashboard.ts,trends.ts,modules/savings-history.tsorladder-timeline-chart.tscontains a color literal any more.modals.css#fff->--cudly-surfaceor--cudly-primary-fg,components.css#f8f9fa->--cudly-surface-muted,charts.css#2563eb->--cudly-primary.frontend/src/__tests__/chart-theme.test.ts.3ffc3b7b(chart tokens and theme helpers), the merge with mainb8a54fdc,f4e84397(ceiling lowered to 136, since main removed 3 literals),21d231b6(Trends chart tokenized), the merge with main540b0169,5ab3ad41(segment gap reads--cudly-surface) and4677d7cb(ceiling set to 113, the count on this branch after merging main at b42245c).Why this approach
#1a73e8next to the#2563ebprimary, many grays). Mapping each one to a token, or choosing a dark palette, changes how the app looks, so it is left as a design decision.base.css.Screenshots
Screenshots were taken before this branch was merged with main; before = main at a14dc8f. After is this branch at that point. Both are headless Chromium at 1440x900 against the repo's mocked e2e API, with the same
--cudly-chart-*override (purple palette, red axis text) applied to both builds to show which colors the tokens control.Verification
Run on this branch's head
4677d7cb:chart-theme.test.ts(13 tests): fallbacks equalbase.cssand cover every--cudly-chart-*token; series and layer tokens are#rrggbb; a live token wins over the fallback;surfaceColor()follows--cudly-surfaceand its fallback equalsbase.css;requireTokenthrows;withAlphaandapplyChartDefaultswork;savingsLayerColors()follows an overridden layer token;dashboard.ts,modules/savings-history.ts,ladder-timeline-chart.tsandtrends.tshave no color literals; the CSS literal count is at most 113.dashboard.test.tscase: the stacked-segment gap follows a themed--cudly-surface. It fails with the'#ffffff'literal (expected#1f2937, received#ffffff) and passes with the token.e2e-local.sh): 264 passed.scripts/test-git-secrets-allowlist.sh: 137 PASS, 0 FAIL.frontend/src/dashboard.ts,frontend/src/modules/savings-history.tsandfrontend/src/styles/modals.cssresolved, see the merge commit540b0169.Notes for review
lib/chart-theme.tsstep), exact-match CSS literal swaps and guard tests.@media (prefers-color-scheme: dark)token override. It needs a dark palette and only makes sense after the literal migration, or the UI comes out half dark.docs.css(API docs page).--cudly-chart-1..10), not by service. Since main's fix(frontend): color the per-service savings chart by layer #1335 dropped the per-service palette, only slots 1 and 2 are drawn; slots 3-10 andchartPalette()are unused outside the tests and can be dropped if a spare series palette is not wanted.whiteare not counted).CSS_LITERAL_CEILINGinchart-theme.test.tsis set to 113, the count after this PR's 8 token swaps (main at b42245c has 121), i.e. main's count once this lands.Overlaps
modals.css) and feat(frontend): wire Azure RI exchange compatible-offerings + execute UI (refs #596) #81 (components.css).Note for the UI owner
This changes UI code that @cristim owns. IAmJSD reviewed the before/after screenshots and asked for it to be merged; any design point you disagree with can be a follow-up.
🤖 Generated with Claude Code