feat(theme): move current to the theme axis, add an invert type - #1332
Conversation
`current` never named a shape — it named where the colors come from, which is the question `theme` answers. As a type it occupied the slot that decides emphasis, so picking it meant giving up the choice between a filled control, an outlined one and a bare label. On the `theme` axis it composes, and every type now has a `current` flavour: `item`, `clear`, `outline`, `outline-2`, `primary`, `link` and `card`. `current.outline` and `current.item` are byte-identical to the old `Button` and `Item` flavours. `primary` escalates the alpha ramp rather than inverting (one inherited color has nothing to punch a label out with), `outline-2` doubles the tint in place of the opaque base it cannot swap, and `link` goes soft at `#current.8`. `type="current"` still renders, mapped to the flavour it used to be, and warns once in development. `ItemAction` / `ItemBadge` now default to `type="clear"` + `theme="current"` and read neither from `ItemActionContext`; both are plain defaults the lint registry can prove. The host theme still reaches them as `data-surface`, which the `current` ramp reads to pick the alphas that work over the special theme's fixed dark-purple surface. Adds `CurrentStates` stories to `Button` and `Item` sweeping every type and state on the theme; the context sweeps become `CurrentTheme`. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BU1Vwm6cGgGRcMubszgbGD
🦋 Changeset detectedLatest commit: 545af49 The changes in this PR will be included in the next version bump. This PR includes changesets to release 1 package
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
📦 NPM canary releaseDeployed canary version 0.0.0-canary-8d79305. |
🧪 Storybook is successfully deployed!
|
🏋️ Size limit report
Compared against main at ffd1c7b — run 32357954115, 2026-08-20T10:14:26Z.To see which modules changed, download the size-limit-statoscope-report artifact from this run and open report.html. |
…s stories `pressed` already wins over `hovered` in every variant's state map, so the combined cell rendered identically to `Pressed` next to it — six columns of noise across seven theme stories. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BU1Vwm6cGgGRcMubszgbGD
…state `selected & disabled` on `outline`, `outline-2` and `clear` borrowed `accent-disabled-surface` — the mid-tone pill a PRIMARY button steps DOWN to, which on a non-solid type is a step UP — under a `tone: 'max'` label that resolved to literal white in light mode. A disabled segmented control drew more attention than a live one. Adds `accent-disabled-surface-soft` / `accent-disabled-surface-soft-text`: the neutral disabled geometry (the same `-3.5` chip and `-23` label deltas against `surface`, adaptive) carrying brand chroma instead of extra weight. Selection still reads, now as a tint rather than a heavier chip. No existing token changed value; the palette snapshot diff is additive. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BU1Vwm6cGgGRcMubszgbGD
`current` shipped on the `type` axis one release ago and has no consumers outside the kit, so the axis move is a clean break rather than a mapped and warned-about alias. The changeset carries the migration. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BU1Vwm6cGgGRcMubszgbGD
…rrent-theme-js20o6
The brand-tinted chip introduced in the previous commit fixed the weight inversion but read over-saturated beside the enabled selected chips it sits next to. The chip now stays exactly as it is when enabled and only the label fades: the chip is what says "this one is on", and a disabled control has no business saying that differently from a live one. Replaces the two `accent-disabled-surface-soft*` tokens with a single `accent-disabled-text` — the neutral `disabled-surface-text` geometry at brand chroma, ~2x the neutral and comfortably under `accent-text-soft`. The chip is `.08` rather than a literal reuse of `selected`'s `.09` so the two entries do not serialize identically; `mergeEntriesByValue` would otherwise merge them and negate `selected & (hovered | focused)`, the bug `SPECIAL_CLEAR_STYLES` documents. Verified with `pnpm probe`: all four selected states still emit distinct rules. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BU1Vwm6cGgGRcMubszgbGD
The four status themes tint their `clear` selected chip from `accent-text`, not `accent-surface` — `default.clear` is the odd one out — so the blanket edit in the previous commit shifted their hue and weight on `selected & disabled`, breaking the very invariant it introduced. Adds a unit test over `ITEM_VARIANTS` pinning both halves of that invariant across all fifteen brand-theme x non-solid-type variants: the disabled selected fill equals the enabled one modulo the `.09` -> `.08` step, and the label is its theme's `accent-disabled-text`. Verified the test fails on the bug it describes. Reported by Cursor Bugbot. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BU1Vwm6cGgGRcMubszgbGD
Every disabled label in the special theme sat far above the house figure for a dead control. Measured against the chip it sits on, `outline` disabled came out at cr 3.24 and `outline` selected + disabled at 4.21 — not only too legible, but inverted, since the selected one out-read the plain one. For scale, `disabled-surface-text` measures ~2.02 against `surface`, and this theme's own `primary` disabled pair measures 1.73. The labels are now solved for cr ~2.0 against whatever each sits on: `#white.23` for the plain disabled states across `outline`, `clear`, `item` and `link`, `#white.28` for `outline` selected + disabled on its chip. The two differ because they resolve against different chips, which lands them on the same contrast rather than the same opacity. `outline` selected + disabled also keeps the enabled selected chip, as the colored themes now do. `clear` — whose selected state inverts to a solid white pill — keeps that pill and fades its DARK label instead, to `#special-accent-text.45`. Generalises the `ITEM_VARIANTS` test to cover both constructions, and pins the `current` theme as the documented exception: its disabled alphas are pre-multiplied against an already-faded `currentcolor`, so they rise where every other theme's fall. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BU1Vwm6cGgGRcMubszgbGD
A disabled selected `clear` on the `current` theme fell through the item ramp to a bare `transparent` and rendered no chip at all, so the state was indistinguishable from an unselected one. It now carries `#current.18` — `CURRENT_OUTLINE_STYLES`' own disabled selected chip, so the two differ by exactly the border. Same split the colored themes already make between `*_ITEM_STYLES`, where a disabled row drops its chip, and `*_CLEAR_STYLES`, where it keeps one. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BU1Vwm6cGgGRcMubszgbGD
…bled Fallout from e31092d, and a good catch: `current.invert` gates its label fade on `!inherit-disabled`, on the grounds that something above already faded the color it paints from. That holds automatically while the color is INHERITED. An offered accent is not inherited — the element applies it itself — so nothing above touches it, and inside a disabled banner the action kept a full-strength `accent-text` label on a dead chip: measured cr 5.69 light / 6.13 dark, reading live next to a muted dismiss. Fixed on the offering side rather than the reading side, because that keeps one rule instead of two. The gate already means "someone above handled the fade"; the fix is to make that true for the accent path as well. So: a container that OFFERS a color owns that color in every state. `Banner` now pairs each `theme=X` accent with a `theme=X & disabled` counterpart at `.4`, which lands on 1.81 / 2.20 — the same ~2:1 band every other disabled label in `item-themes` is tuned to. The alternative, changing the reader to fade on a bare `disabled`, was rejected: with no accent offered it would fade a second time against a host that already faded, which is exactly the `.16` washout fixed in 9125615. Probed — tasty splits the two into mutually exclusive rules, so there is no cascade ambiguity: [data-theme="danger"]:not([data-disabled]) > [data-element="Actions"] { --current-accent: var(--danger-accent-text-color) } [data-disabled][data-theme="danger"] > [data-element="Actions"] { --current-accent: rgb(var(--danger-accent-text-color-rgb)/.4) } The accent map is exported as `BANNER_ACTION_ACCENT` and a new `Banner.test.ts` pins the live/muted pairing, so the contract holds by test rather than by discipline. The reader-side comment in `CURRENT_INVERT_STYLES` states it too, for whoever offers the next accent. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BU1Vwm6cGgGRcMubszgbGD
Reported from the `CurrentTheme` story: on the "Dark banner" container the
`primary` button renders as a blank white chip in light mode.
Same root cause as the Banner case, mirrored. `current.primary`'s pill IS
`currentcolor`, so its label has to contrast with an arbitrary color, and the
fixed `#surface` only manages that while the inherited color sits away from
the page. A container that INVERTS the surface breaks it: the dark banner
paints `#white`, so the pill is white and `#surface` is white too in light
mode — cr 1.00. In dark mode `#surface` is near-black and it happens to
work, which is why the report says "only in light schema".
No fixed token solves both directions: `#surface-text` fixes the inverted
container and collapses the ordinary one (measured 2.60 light / 1.91 dark on
a tinted container), which is the trade-off already documented on this
flavour.
So `primary` now reads the same `--current-accent` the invert flavour does.
The property already means "the color to write with when the inherited one
will not do", and this is the other flavour that cannot always write with
it. The fallback is the `#surface` it always used, so no container that
offers nothing moves at all.
A container that inverts the surface has the answer to hand: its own fill
contrasts with its own text by construction, and the pill IS that text. The
story's two inverted containers ("Dark banner", "Brand") now offer theirs;
the tinted ones leave it unset.
Probed the chain end to end — the container emits
`--current-accent: var(--fixed-dark-color)` and the button reads
`-webkit-text-fill-color: var(--current-accent, var(--surface-color))`, so
the label lands on the container's own dark fill against a white pill. The
four icon slots repeat it, since SVG stroked with `currentColor` never sees
`-webkit-text-fill-color`.
Tests: the reader test now pins BOTH filled flavours and nothing else, and a
new case pins that every read spells out its fallback — a bare
`var(--current-accent)` would resolve to nothing and drop the label wherever
no container offers one. Checked by introducing exactly that.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BU1Vwm6cGgGRcMubszgbGD
…ixes The headline: `current.primary` and `current.invert` cannot share one offered color. Reported from the story — on the dark banner in dark mode `invert` rendered a near-black label on a near-black chip. They sit on different chips. `primary`'s is `currentcolor`, whatever the container paints text with, often scheme-fixed. `invert`'s is `#surface`, which flips. On a container painting `#white` the first needs a dark label in BOTH schemes and the second needs one that flips with the page, so a single value always loses one of them: offered primary (chip #white) invert (chip #surface) container fill 16.31 / 16.31 16.31 / 1.00 <- reported #surface-text 16.31 / 1.12 16.31 / 14.51 So `primary` now reads `--current-label` and `invert` keeps `--current-accent`, each with its own fallback, and the story's two inverted containers offer both — their own fill for the first, `#surface-text` for the second. Tests pin that each flavour reads only its own property, that every read spells out its fallback, and that neither fades an offered color on a bare `disabled`. Also, from an external review — all four verified before acting: 1. HotKeys on `current.primary` was `#white` on a light fill. My comment claimed `inherit` would draw the hint's rim in the fill color, which is false: the hint renders inside `Suffix`, and this same file recolors that slot to the LABEL, so `currentcolor` there is the label. Measured cr 2.17 on a `#note` container in dark. The whole `current` theme now takes `inherit` — the neutral `#dark.65`-on-`#dark.04` chip the other flavours were getting all but vanishes on a dark overlay, which is where this theme is meant to live. 2. `CURRENT_PRIMARY_STYLES.fill` had no second layer at rest, so hover snapped instead of interpolating — the one filled flavour in the file missing the pin its own neighbours document. Now `#current #black.0`. 3. `current-theme-axis.md` still promised the `type="current"` shim that eeb6683 removed, telling consumers the old spelling was safe when it resolves to no variant. Rewritten to state the clean break. 4. `BANNER_ACTION_ACCENT` is now keyed by `Record<BannerTheme, true>`, so a fifth theme fails the build instead of silently falling through to the cr 1.00 the map exists to avoid. Not taken, with reasons: a `useWarn` for the removed `type="current"` was declined earlier in this branch (the changeset carries the migration); and the `hovered` vs `hovered | focused` split between the two `invert` families is per-family consistency — every `current` flavour steps on focus, every brand one does not. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BU1Vwm6cGgGRcMubszgbGD
One conflict, in `src/tokens/palette.ts`, and additive on both sides: this
branch adds the `accent-disabled-text` token immediately above the
loading-face block, while main rewrote that block's comment ("Isometric cube
faces", neutral chroma and WCAG-floor contrast). Kept both — our token, then
main's comment and its re-authored face values.
Palette snapshots and the full suite pass on the merge (2053).
The gate I put on this label in 4f0fa82 outlived its reason inside the same commit. It went on while `primary` read `--current-accent`, which `Banner` pre-mutes — so the reader had to skip its own fade or halve the value twice. Splitting the hook moved `primary` onto `--current-label`, which no container pre-mutes, and the gate then bought nothing while costing the fade in every nested disabled path: a crisp `#surface` label on a faded chip, still reading live. The rule underneath is simpler than "who owns the color", and it is the fallback that decides it: invert falls back to `currentcolor` — a disabled host already muted it, so the reader gates, and the offerer owns every state primary falls back to `#surface`, absolute and untouched by any host, so the reader always fades, and the offerer supplies live only So the asymmetry between the two properties is not a choice; it follows from which fallback each flavour has. Both sides are now stated where they are read, and pinned from both directions: one test asserts `invert` never fades the accent on a bare `disabled`, its converse asserts `primary` always does, on the label property and all four icon slots. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BU1Vwm6cGgGRcMubszgbGD
I had drifted `current.invert` into a second scheme-following construction with its own property, then spent two commits patching the seams between them. It is simpler than that, and always was: on this theme `invert` is `primary` with the two colors exchanged, and nothing else. primary fill #current label <swap> invert fill <swap> label #current `<swap>` is one property, `var(--current-accent, var(--surface-color))`, read by both. A container offers one value and both flavours move together, so the mirror cannot come apart — which is what the previous two-property split allowed, and why the dark banner ended up with a near-black label on a near-black chip. Unset, the fallback is the `#surface` both flavours used before the hook, so nothing outside such a container changes. The story's inverted containers offer their own fill: `primary` gets a dark label on its white chip and `invert` a white label on its dark chip, which is what was asked for. `#current`-derived fades keep their gate — a disabled host has already muted what they resolve against — while the swap color is faded by whoever reads it, since nothing above touches it. That asymmetry is now stated once rather than argued per property. Also folded in, both from review and both consequences of `current.primary` keeping `color` as its fill so `#current` resolves there: - the `Actions` slot is recolored to the label alongside the icon slots. A nested `Item.Action` defaults to `theme="current"` and mixes its label from the `currentcolor` it inherits, which was the chip — probed, it rendered invisible against it. - `ITEM_RESTING_COLOR_VARIANTS` overrides the same variant, so `ItemButton`'s wrapper hands sibling actions the label rather than the chip. Tests now pin the mirror directly — `primary` paints what `invert` writes and vice versa — which is a claim about the pair rather than about either object, and is the thing that kept breaking. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BU1Vwm6cGgGRcMubszgbGD
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.
There are 2 total unresolved issues (including 1 from previous review).
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Want higher recall? High effort reviews run extra passes and find more bugs. A team admin can switch effort levels in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 1f7f7fb. Configure here.
…nt-fill` Removes the `invert` type from `Button`, `Item`, `Item.Action` and `Item.Badge` — the `invertStyles()` factory and all eight `*_INVERT_STYLES`, the type and variant unions, the styles maps, and the stories, docs and changesets that described it. `Banner.Action` goes back to `current.outline` on `Item.Action`'s own theme default, so `BANNER_ACCENT_THEMES`, `BANNER_ACTION_ACCENT`, the `$current-accent` declaration and `Banner.test.ts` go with it. `current.primary` keeps the problem `--current-accent` was papering over: it fills with the color it INHERITS, so its label cannot come from that color too, and the page (`#surface`) is only the right answer while the inherited color sits away from the page. A container whose own text color IS the page — a `Banner` labels itself `#white`, and `#surface` is white in light mode — collapses label and chip to cr 1.00. The answer is now an explicit token rather than a private custom property. `#current-fill` is declared in a new `CONTEXT_TOKENS` block with a `#surface` default, so it takes the alpha suffix (`#current-fill.5` is the disabled label) and a container redirects the label, the icon slots and the chip's rim with one declaration. Two hand-written `color-mix()` strings and `CURRENT_SWAP_COLOR` are gone with it. Also fixes a stale paragraph in the CURRENT THEME header, which still claimed `current.primary` "escalates on alpha instead" of punching out a label — the construction it described predates the opaque fill. Size: the branch was 6 bytes over the `Button` budget (124.01 kB against 124 kB); dropping `invert` takes 0.34 kB off that entry and off `All`, landing at 123.67 kB / 504.04 kB. `.size-limit.cjs` records the measured numbers, and that `current` is the one feature which legitimately moves the Button budget: it sits on the theme axis of `Button` itself, so its flavours are in the variants map and there is nothing to tree-shake. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Addresses the review note that the `current` ramp's `surface=special` steps keyed off a `data-surface` attribute only `ItemAction` and `ItemBadge` set, so `Button` and `Item` on the same fixed dark-purple surface silently used the light ramp — and `theme` can no longer be `special` and `current` at once anyway. A per-surface axis three of five call sites miss is worse than none, so the five special steps go, and with them `data-surface` (introduced on this branch, read by nothing else) and the `contextTheme` reads that fed it. The dark ramp was the real concern, and it is now derived rather than authored. Each `@dark` step is solved so its OKHST tone delta from the surface matches the light step's, which — tone being contrast-shaped — also lands the chip on the light step's contrast against the page: step light ΔT dark ΔT cr(chip, page) hover .04 2.64 .031 2.62 1.084 / 1.083 press .06 3.99 .046 3.98 1.129 / 1.129 selected .18 12.57 .13 12.37 1.467 / 1.457 selected-hover .24 17.20 .175 17.16 1.689 / 1.686 selected-press .3 22.08 .221 22.13 1.959 / 1.961 The dark alphas come out LOWER, not higher: near the dark end of the scale a small sRGB move is a large perceptual one, so a light tint on a dark surface reads stronger than the same tint of a dark label on a light page. The previous hand-tuned values overshot most at the low end (hover measured 1.210 against light's 1.084) and already matched at the top step. Calibrated against `#surface` / `#surface-text` per scheme, which is the only tractable reference for a theme that paints an arbitrary inherited color over an arbitrary container. Also corrects the claim that the dark steps were capped by the AA floor for their label at `.24`. That figure belonged to the special surface, whose `#white.8` label measures 4.53 against a `.21` chip; on the plain dark page the label is opaque and the same steps measure 6.9-9.8, so there was no ceiling shaping them. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

Describe changes
Three changes, all on the
theme/typegrid.1.
currentmoves from thetypeaxis to thethemeaxiscurrentnever named a shape — it named where the colors come from (the inheritedcurrentcolorrather than a brand ramp), which is the questionthemeanswers. As atypeit occupied the slot that decides emphasis, so picking it meant giving up the choice between a filled control, an outlined one and a bare label.On the
themeaxis it composes, and every type now has acurrentflavour:currentflavouritemItemshape — no border, nothing painted at rest, fill stepping in on hover/pressed/selectedclearItem.Action/ItemBadgeoutlineButtonshape — a resting#current.03chip inside a#current.08borderoutline-2outlinefor a container that already paints something. No opaque base to swap, so the tint roughly doubles at every stepprimary#surfaceinvert#surfacefill with the inherited color written on itlink.8, "strong" is full opacitycard#current.05panel inside a#current.2border (Itemonly)current.outlineandcurrent.itemare byte-identical to the oldButtonandItemflavours, so nothing that usedtype="current"changes appearance. Every alpha ramp stops at#current.24— the measured AA floor for a full-strength label on a dark surface.Breaking:
type="current"is removed with no runtime fallback — it resolves to no variant, the same as any other unknown type. The spelling shipped one release ago and has no consumers outside the kit, so it is a clean break rather than a deprecation.Button type="current"Button theme="current"(type defaults tooutline)Item type="current"Item theme="current"(type defaults toitem)Item.Action type="current"/ItemBadge type="current"currentis already the default themeItemnow acceptstheme="current"with every type exceptheader; the warning that fired fortype="current"with any theme butdefaultis gone.currentis registered inTastyThemeNames, so it autocompletes on every tasty component.Item.Action/ItemBadgedefaults.typenow defaults toclearandthemetocurrent, and neither is read fromItemActionContext— the two axes are independent, so a shape no longer implies a color source. Both are plain defaults the lint registry can prove, which the previousthemeentry (skip: 'context') was not. One behaviour change: an action that named atypebut nothemeused to inherit the host row's theme and now takes the host's color instead; passtheme="default"to opt back into a fixed palette. The host theme still reaches the element asdata-surface, which thecurrentramp reads to pick the alphas that work over thespecialtheme's fixed dark-purple surface — the jobdata-themedid beforetheme="current"claimed that attribute.2. New
inverttype, on every themeThe filled type that follows the color scheme, where
primarydeliberately does not. The fill is the theme'saccent-text— the color normally painted on the page — and the label is#surface, the page itself, so the control lands on the opposite side of the page in either scheme: a dark chip with a light label in light mode, a light chip with a dark label in dark mode.primarypins a fixed#whitelabel on a brand surface and therefore reads with the same weight in both.accent-text(L 0.47)#surface(L 1.00)accent-text(L 0.76)#surface(L 0.24)The trap this pairing invites is labelling with
surface-text, which is painted on the page and so sits on the same side of the fill asaccent-textdoes — that measures 2.60 / 1.91 and was rejected on those numbers.Hover and pressed darken through a second fill layer rather than stepping to a darker sibling, because
accent-texthas none — its-softcounterpart is lighter — and a#blackoverlay darkens in both schemes, so the monotonic default → hover → pressed direction survives the scheme flip. Disabled hands over to the brand-tinted pairprimaryalready mutes to, so the two filled types stay calibrated in one place; the fill's base layer is pinned across all four states so the overlay interpolates instead of the base flashing through mid-transition.Two themes build it by swapping rather than from
accent-text.specialinverts against its own fixed dark surface instead of the page, giving a white pill with the theme's dark accent on it — the figurespecial.clearalready strikes when selected. Andcurrenthas noaccent-textat all: it has exactly one color, the one it inherits, soinvertthere isprimarywith its two colors exchanged.currentprimary#current#surfaceinvert#surface#currentThat is the same swap
specialmakes, so the pairing reads the same way on every theme even where the tokens differ. It also drops the machineryprimaryneeds there:#currentis both the fill and the valuecurrentcolorresolves against, so the label has to be painted with-webkit-text-fill-colorand every icon slot recolored by hand. With an absolute#surfacefill,coloris simply the label and icons inherit it. Hover and pressed tint toward the inherited color rather than laying#blackover it — that reads as a darkening on a white page and a lightening on a dark one, where a fixed#blackover a near-black#surfacewould barely move.Registered on
Button,Item,Item.ActionandItem.Badge. Around the edges: the split-button border collapse countsinvertamong the bordered types,ItemBadgedrops its rim as it does forprimary, and theHotKeyshint on aninvertrow takes theinheritflavour so it tracks the#surfacelabel rather thanprimary's fixed#white.The five brand variants come from an
invertStyles(accent)factory rather than hand-copied objects. The only thing that varies is the theme prefix, and hand-copying that is exactly how theselected & disabledfills below landed on the wrong token for four themes earlier in this branch.3.
selected & disabledno longer outweighs its enabled stateOn
outline,outline-2andclearacrossdefault/danger/success/warning/note, the state borrowedaccent-disabled-surface/accent-disabled-surface-text— the pair built for a PRIMARY button, whose enabled state is already an opaque brand fill under a#whitelabel, so a mid-tone chip is a step down there. On a non-solid type it is a step up: against a 9% brand tint under soft accent text, the chip read as a filled pill and itstone: 'max'label resolved to literal white in light mode. A disabled segmented control drew more attention than a live one.It now keeps the enabled
selectedchip at a slightly lower alpha and fades only the label, through a newaccent-disabled-texttoken. Selection survives as a brand tint on a chip of unchanged weight, which is what CUB-3912 asked for. No existing token changed value — the palette snapshot diff is purely additive.primarykeepsaccent-disabled-surface(correct for a solid fill);specialandcurrentkeep their own white-alpha andcurrentcolorregisters, retuned to roughly 2:1 for the muted state.Banneralso drops the border-clear override on its outline actions, so acurrentoutline stays visible on a saturated surface.Checklist
Closes: N/A
Other information
Stories. New
CurrentStatesonButtonandItemsweep every type and state on thecurrenttheme, inside containers that paint their own text color — the matrix the other themes get from*States/TypesAndThemes.primaryandinvertsit next to each other there, which is the clearest place to read the swap. The existing context sweeps are renamedCurrentTheme.invertalso joinsBUTTON_TYPESandItem'sTypesAndThemesgrid, and gets its own group in theItem.Action/ItemBadgeTypesstories; thoseTypesstories pintheme="default"so the shapes still show against a brand palette, and theThemesstories gain acurrentgroup. ThePressed&Hoveredcolumn is dropped from theButtonstates stories —pressedoutrankshoveredin every variant's state map, so it rendered identically to thePressedcell beside it.Single fade per subtree.
#currentis the color an element inherits, so a.4disabled fade applied twice down one chain multiplies to.16. Everycurrentflavour now gates its fade ondisabled & !inherit-disabled & !inside-wrapper: the first mod is set byItem.Actioninside a disabled row, the second byItemButtonon the row it renders insideActionsWrapper, and the wrapper reproduces the gated color so actions rendered as siblings of the row inherit a fadedcurrentcolortoo. Probed on all four paths — standalone control, action in a disabled row, and both halves of anItemButton— each lands on exactly one.4.Verification.
pnpm test(2042 passed),pnpm build,pnpm lintandpnpm audit-defaultsall clean;pnpm audit-docsreports only the pre-existingisChecked/scrollMargin/placebase-prop gaps.pnpm probeconfirmscurrent.outlineandcurrent.itememit the same CSS as before, that thesurface=specialbranch still wins over@dark, that the disabled-fade gate resolves as described above, and the measured tone/chroma/contrast figures quoted throughout.Regression tests.
src/data/item-themes.test.tspins two invariants that a blanket edit can silently break: theselected & disabledchip must equal its enabled counterpart modulo one alpha step across all fifteen brand-theme × non-solid-type variants (the tokens are not uniform —default.cleartints fromaccent-surfacewhile the four status themes tint fromaccent-text), and every interactivecurrentflavour must state the gated fade rather than a baredisabled. Both fail on the exact bugs they describe, checked by reintroducing them.Two test/registry follow-ons. The probe spike's "case E" (a default that is redundant bare but load-bearing under a provider) moved from
ItemActionthemetoisDisabled, which is now the only context-resolved prop there;docs/rules/eslint-plugin.mdand the fixtures' comments were updated to match. The unreleaseditem-action-current-defaultchangeset was reconciled so the two entries do not contradict each other in one release note.Note
Medium Risk
Touches core variant resolution and default props across widely used action/list components; visual changes are possible where row actions no longer inherit the host brand theme, though legacy
type="current"mappings aim to preserve prior appearance.Overview
currentis now atheme, not atype, onButton,Item/ItemButton,Item.Action, andItemBadge. Inherited-color styling composes with every shape (outline,primary,clear,link,outline-2,item,card) via newcurrent.*variants initem-themes.ts;current.outline/current.itemstay pixel-identical to the oldtype="current"button and item. Variants resolve as`${theme}.${type}`instead of foldingcurrentunderdefault.Item.Action/ItemBadgedefault totheme="current"andtype="clear"and no longer inherittype/themefromItemActionContext; host row theme is exposed asdata-surfaceso thecurrentalpha ramp can target thespecialsurface (surface=specialreplacestheme=specialon ramp tokens).Patch:
selected & disabledon non-solid brand types uses newaccent-disabled-surface-softtokens so muted selection is lighter than enabled, not heavier.Docs, Storybook (
CurrentStates, renamedCurrentTheme), palette snapshots, and the eslint defaults registry (plain defaults for action theme/type; probe case E moved toisDisabled) are updated. Changesets document migration fromtype="current"totheme="current".Reviewed by Cursor Bugbot for commit 1690170. Bugbot is set up for automated code reviews on this repo. Configure here.
Note
Medium Risk
Wide changes to variant keys, defaults, and visual tokens on core action/list components; row actions may look different when they no longer inherit the host theme, and consumers must migrate from
type="current"totheme="current".Overview
currentmoves fromtypetothemeonButton,Item/ItemButton,Item.Action, andItemBadge. Styling now resolves astheme.typewith a full set ofcurrent.*flavours initem-themes.ts(outline, primary, clear, link, outline-2, item, card); legacycurrent.outline/current.itemmatch the oldtype="current"look. Variants no longer foldcurrentunderdefault, andtheme="current"is registered for Tasty autocomplete.Item.Action/ItemBadgedefault totheme="current"andtype="clear"and no longer inherit type/theme from context; the host row theme is exposed asdata-surfaceso thecurrentramp can targetspecialsurfaces (surface=specialon ramp tokens). Actions that only settypenow tint fromcurrentcolorinstead of the host brand palette unless an explicit theme is passed.Patch: For brand
outline/outline-2/clear(plusspecialandcurrent.clear),selected & disabledkeeps the enabled selected chip and fades the label with newaccent-disabled-text;specialdisabled labels are retuned for ~2:1 contrast.Bannerdrops the border-clear override on outline actions socurrentoutlines stay visible on saturated surfaces.Docs, Storybook (
CurrentStates, renamedCurrentTheme), palette snapshots, unit tests, and the eslint defaults registry are updated accordingly.Reviewed by Cursor Bugbot for commit b116368. Bugbot is set up for automated code reviews on this repo. Configure here.
Note
Medium Risk
Broad changes to variant keys, default props, and tokens on widely used action/list components; row actions may render differently when they no longer inherit the host brand theme, and consumers must migrate from
type="current"totheme="current".Overview
currentis now atheme, not atype, onButton,Item/ItemButton,Item.Action, andItemBadge. Variants resolve astheme.typewith a fullcurrent.*set (outline, primary, clear, link, outline-2, item, card);current.outline/current.itemmatch the oldtype="current"look. Consumers must usetheme="current"instead oftype="current"(no runtime fallback for the old type).New
inverttype on every theme: scheme-following filled control (accent-textfill,#surfacelabel), withcurrent.invertandspecial.invertbuilt by swapping roles likecurrent.primary.Banner.Actionswitches totype="invert"on the banner’s theme so actions stay legible on white-on-saturated banners wherecurrentfilled types collapse to 1:1 contrast.Item.Action/ItemBadgedefault totheme="current"andtype="clear"and no longer inherit type/theme from context; host theme isdata-surfacefor thecurrentramp (surface=specialon ramp tokens).selected & disabledon non-solid brand types (andspecial/current.clear) keeps the enabled selected chip and fades the label viaaccent-disabled-text;currentflavours gate disabled label fade so it is not applied twice inside disabled rows / action wrappers.Docs, Storybook, palette snapshots,
item-themes.test.ts, and eslint defaults registry updates accompany the migration.Reviewed by Cursor Bugbot for commit f37a0d0. Bugbot is set up for automated code reviews on this repo. Configure here.