You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Three Material surface roles are not backed by real surface tokens — they borrow
unrelated tokens whose values are wrong for the role:
ColorScheme role
Currently sourced from
Should be
surfaceContainer
colors.inputBackground (a text-field fill)
a real mid container rung
surfaceDim
colors.accent (a misnamed neutral)
the dimmest surface
surfaceBright
colors.background (the base surface)
the brightest surface
This is the same defect class as #418: a role painted from a token that does not
carry the right value for that role. It leaves the surface tonal ladder
non-monotonic and inverts surfaceDim / surfaceBright in dark mode.
Surfaced while fixing #418 (PR #422). This is the surfaces-are-mis-valued half
of the problem; #421 is the surfaces-aren't-settable half — both fell out of #418.
Sorting the dark-mode greys by lightness (M3 dark: higher elevation = lighter; surfaceDim should be darkest, surfaceBright brightest):
Role
Dark
Correct?
surfaceContainerLowest
0x0E
✓
surfaceBright (←background)
0x11
✗ should be the brightest (~0x36)
surface
0x11
✓
surfaceContainerLow
0x1A
✓
surfaceContainerHigh
0x2A
✓
surfaceDim (←accent)
0x2A
✗ should be the dimmest (~0x0C)
surfaceContainer (←inputBackground)
0x33
✗ should sit ~0x22, between Low and High
surfaceContainerHighest
0x33
✓
surfaceDim is the worst: in dark it is brighter than surface, so the
"dimmest" surface is one of the brightest — fully inverted. accent is itself
a misnamed neutral (dark 0x2A2A2A, light 0xE9EBEF), and its only
production consumer is this line — accent / onAccent are otherwise dead.
surfaceBright is correct in light (white) but in dark collapses onto surface (0x11), so there is effectively no bright surface in dark mode.
surfaceContainer is the actively-used one (card, chip, selected list
tile, collapsed expansion tile, popup menu). Mislayered in both palettes.
The whole surface family is withheld from BrandColorScheme (_lowerColors
is base.copyWith(<non-surface fields>), so surfaces stay at the base palette).
Two consequences: (a) correcting these token values fixes the defaults for every consumer — default and branded — with no migration; (b) the corrected
tokens stay fork-unsettable by design, so a fork with a tinted brand
background (e.g. #1D1F23) still gets stock neutrals that don't match. Making
surfaces track a tinted background needs the colors seam tracked in BrandTheme extensibility: a seam for full color control, and the spacing question #421. This
fix and that seam are complementary — the fix gives correct neutral defaults; the seam lets a fork override them. Neither alone closes the
tinted-background case.
Add three real named tokens to SoliplexColors (light + dark), completing the
M3 surface set (background + surfaceContainerLowest/Low/High/Highest
already exist): surfaceContainer, surfaceDim, surfaceBright. Values
chosen for correct M3 ordering. This is an intentional visual change and a
design call — needs sign-off on the exact values. Rough targets:
surfaceContainer: ~0x22 dark / ~0xEE light (between Low and High)
surfaceDim: darkest surface in dark (~0x0C) / dimmest in light (below Highest)
surfaceBright: brightest in dark (~0x36) / white in light
Map them straight through in buildSoliplexThemeData
(surfaceContainer/Dim/Bright: colors.<same>).
Repoint the structural fills currently reading colors.inputBackground at colors.surfaceContainer: card, chip, listTile.selectedTileColor, expansionTile.collapsedBackgroundColor, and the popup menu (it merged on colors.inputBackground).
Leave inputDecoration.fillColor on colors.inputBackground — a genuine
input token, correctly named for its sole remaining use.
Retire accent / onAccent (dead once surfaceDim has a real token). Removing a public token is a breaking change to the SoliplexColors
contract + mirror — decide whether to remove or leave unused.
This backs the whole surface family with correctly-valued, correctly-named
tokens. It is the complete fix.
Lighter interim alternative
Add only surfaceContainer, set to today's inputBackground values; leave surfaceDim / surfaceBright as-is. Decouples the input fill with no golden
churn, but leaves the ladder non-monotonic and surfaceDim / surfaceBright
inverted in dark. Only worth it to defer the golden/design loop; it does not fix
the root defect.
Required by project rules
Adding tokens needs explicit approval before code is written.
Tokens must be mirrored into design_system/tokens.{dart,css,jsx} in the same
change, and design_system/README.md updated. design_system/CLAUDE.md and scripts/content.js also describe the current double-use of inputBackground
and the accent token, so those notes need correcting.
Component goldens are a Linux baseline — regenerate on CI, not locally (the
complete fix shifts card/chip/tile fills).
Test impact
Full SoliplexColors(...) constructions gain the new fields (2 palettes, 2
mirror copies, theme_test.dart, colors_test.dart).
Assertion updates: theme_test.dart:145 (cs.surfaceContainer), :151
(surfaceDim), :152 (surfaceBright), :204 (popup); brand_lowering_test
gains/loses the accent lines depending on the retire decision.
Not required
Renaming inputBackground. After this fix it is used only for the text-input
fill, so its name matches its role. Renaming it would be a cosmetic,
contract-breaking change to do deliberately or not at all.
Summary
Three Material surface roles are not backed by real surface tokens — they borrow
unrelated tokens whose values are wrong for the role:
surfaceContainercolors.inputBackground(a text-field fill)surfaceDimcolors.accent(a misnamed neutral)surfaceBrightcolors.background(the base surface)This is the same defect class as #418: a role painted from a token that does not
carry the right value for that role. It leaves the surface tonal ladder
non-monotonic and inverts
surfaceDim/surfaceBrightin dark mode.Surfaced while fixing #418 (PR #422). This is the surfaces-are-mis-valued half
of the problem; #421 is the surfaces-aren't-settable half — both fell out of
#418.
Evidence
packages/soliplex_design/lib/src/theme/theme.dart:72,75,76Sorting the dark-mode greys by lightness (M3 dark: higher elevation = lighter;
surfaceDimshould be darkest,surfaceBrightbrightest):0x0E0x110x36)0x110x1A0x2A0x2A0x0C)0x330x22, between Low and High0x33surfaceDimis the worst: in dark it is brighter thansurface, so the"dimmest" surface is one of the brightest — fully inverted.
accentis itselfa misnamed neutral (dark
0x2A2A2A, light0xE9EBEF), and its onlyproduction consumer is this line —
accent/onAccentare otherwise dead.surfaceBrightis correct in light (white) but in dark collapses ontosurface(0x11), so there is effectively no bright surface in dark mode.surfaceContaineris the actively-used one (card, chip, selected listtile, collapsed expansion tile, popup menu). Mislayered in both palettes.
inverseSurface/onInverseSurface— fix(soliplex_design): paint component surfaces with surface roles, not onPrimary #422 alreadyrepointed these correctly (
foreground/background, a guaranteed contrastpair), so they're excluded here. This follow-up is only the three roles above.
Impact
surfaceContaineris visible today and couples every structural surface to thetext-input fill.
surfaceDim/surfaceBrightare currently latent — no widget in thedesign package or app modules reads them directly; only Material's internal
elevation machinery can. But it is the exact Dark mode: popup menus, mobile app bar, and expanded ExpansionTiles render white for brands with a colored dark-mode
primary(onPrimaryused as a surface color) #418 trap: a saturated-brand fork,or the first component to paint with these roles, gets the wrong tone.
BrandColorScheme(_lowerColorsis
base.copyWith(<non-surface fields>), so surfaces stay at the base palette).Two consequences: (a) correcting these token values fixes the defaults for
every consumer — default and branded — with no migration; (b) the corrected
tokens stay fork-unsettable by design, so a fork with a tinted brand
background (e.g.
#1D1F23) still gets stock neutrals that don't match. Makingsurfaces track a tinted background needs the colors seam tracked in BrandTheme extensibility: a seam for full color control, and the spacing question #421. This
fix and that seam are complementary — the fix gives correct neutral
defaults; the seam lets a fork override them. Neither alone closes the
tinted-background case.
surfaceContainer(now the stock#333neutral after fix(soliplex_design): paint component surfaces with surface roles, not onPrimary #422) is the root cause of the reporter's residual "surfaces
look wrong on my tinted background" complaint. Fully resolving their case
needs this fix plus the BrandTheme extensibility: a seam for full color control, and the spacing question #421 seam.
Proposed fix (complete)
SoliplexColors(light + dark), completing theM3 surface set (
background+surfaceContainerLowest/Low/High/Highestalready exist):
surfaceContainer,surfaceDim,surfaceBright. Valueschosen for correct M3 ordering. This is an intentional visual change and a
design call — needs sign-off on the exact values. Rough targets:
surfaceContainer: ~0x22dark / ~0xEElight (between Low and High)surfaceDim: darkest surface in dark (~0x0C) / dimmest in light (below Highest)surfaceBright: brightest in dark (~0x36) / white in lightbuildSoliplexThemeData(
surfaceContainer/Dim/Bright: colors.<same>).colors.inputBackgroundatcolors.surfaceContainer: card, chip,listTile.selectedTileColor,expansionTile.collapsedBackgroundColor, and the popup menu (it merged oncolors.inputBackground).inputDecoration.fillColoroncolors.inputBackground— a genuineinput token, correctly named for its sole remaining use.
accent/onAccent(dead oncesurfaceDimhas a real token).Removing a public token is a breaking change to the
SoliplexColorscontract + mirror — decide whether to remove or leave unused.
This backs the whole surface family with correctly-valued, correctly-named
tokens. It is the complete fix.
Lighter interim alternative
Add only
surfaceContainer, set to today'sinputBackgroundvalues; leavesurfaceDim/surfaceBrightas-is. Decouples the input fill with no goldenchurn, but leaves the ladder non-monotonic and
surfaceDim/surfaceBrightinverted in dark. Only worth it to defer the golden/design loop; it does not fix
the root defect.
Required by project rules
design_system/tokens.{dart,css,jsx}in the samechange, and
design_system/README.mdupdated.design_system/CLAUDE.mdandscripts/content.jsalso describe the current double-use ofinputBackgroundand the
accenttoken, so those notes need correcting.complete fix shifts card/chip/tile fills).
Test impact
SoliplexColors(...)constructions gain the new fields (2 palettes, 2mirror copies,
theme_test.dart,colors_test.dart).theme_test.dart:145(cs.surfaceContainer),:151(
surfaceDim),:152(surfaceBright),:204(popup);brand_lowering_testgains/loses the
accentlines depending on the retire decision.Not required
inputBackground. After this fix it is used only for the text-inputfill, so its name matches its role. Renaming it would be a cosmetic,
contract-breaking change to do deliberately or not at all.
Refs #418, #421, #422.