feat(ItemAction): default to the current type instead of inheriting the row's - #1326
feat(ItemAction): default to the current type instead of inheriting the row's#1326tenphi wants to merge 7 commits into
current type instead of inheriting the row's#1326Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
🦋 Changeset detectedLatest commit: 2bc4a71 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 |
🧪 Storybook is successfully deployed!
|
… the row's `ItemAction` / `ItemBadge` now default to `type="current"` and no longer mirror the host row's `type` from context. `current` derives every color from the inherited `currentcolor`, so one type covers every host type x theme the context mapping used to enumerate — and because `currentcolor` is inherited rather than resolved once, an action follows its row through hover, selected and disabled instead of holding a fixed palette. The mapping in `ItemActionProvider` that folded the row types onto `clear` is gone. `ItemActionContext` stays: it still carries `disableActionsFocus`, `isDisabled`, the `theme`, and `type` for its presence alone, which drives the `context` mod collapsing an action's side margins. The provider signature is unchanged, so no call site moved. An explicit `theme` opts back into `clear`, since `current` is theme-agnostic and the theme would have nothing to color. `theme="default"` is excluded so passing a prop's own default value cannot change what renders. - Scheme-aware alpha ramp for `current`: `#current` alphas do not adapt the way brand tokens do, so each step carries a light, `@dark` and `theme=special` value. Steps live in custom properties because three ramps in one state-map would collide under `mergeEntriesByValue` and break negation. - `ItemAction` regains a focus ring. `CURRENT_ITEM_STYLES` leaves focus to the owning collection, which is wrong for a focusable action. - `ItemButton` paints its actions' color. It renders them as a sibling of the button, so `currentcolor` reached them from the page: `danger` rows handed actions neutral text and `special` rows handed them dark text on a dark base. - `Item`'s inline variants object becomes the exported `ITEM_VARIANTS`, shared with the color projection so the two cannot drift. Every clear and trigger button in the field components drops its explicit `type` / validation `theme` and relies on the new default, so each takes the color of the field it sits in and follows a custom theme. `Picker` / `FilterPicker` are unchanged — their trigger text already carries validation state. ComboBox, SearchComboBox and SearchInput move to the input's own `#danger-accent-text` when invalid, and the ComboBox trigger stops sitting neutral beside red text. `Select`'s clear button no longer reddens when invalid: its trigger keeps neutral label text, so the button now matches its own field. That `Select` vs `Picker` divergence is a real inconsistency, and belongs in `Select` rather than being masked here. Also exclude `.claude/worktrees` from vitest collection and git. Vitest globs with `dot: true`, so other branches' checkouts were collected alongside the real suite: specs ran twice, path arguments matched in both trees, and failures were reported from code not on this branch. The registry generator behind `pnpm audit-defaults` resolves its output against `process.cwd()`, so a worktree's copy silently overwrote this tree's `defaults.generated.ts`. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`ActionsWrapper` mirrored `Item`'s variant normalisation by hand and missed one of the three cases: the `special` theme has no `outline-2` variant, and `Item` folds that pair onto `outline`. Without it, `<ItemButton theme="special" type="outline-2">` resolved to a nonexistent `special.outline-2` resting-color variant, so the wrapper got no color and its sibling actions fell back to the page's `currentcolor` — dark neutral text on a dark purple surface, which is the exact failure the wrapper exists to prevent. Verified with `pnpm probe:browser`: `special` + `outline-2` returned `rgb(68, 69, 77)` and now returns `rgba(255, 255, 255, 0.8)`, matching `special` + `outline`. Both call sites now go through a shared `resolveItemVariant()` in `item-themes.ts` instead of restating the conditional, since hand-duplicating it is what allowed them to drift in the first place. `header` and `current` still resolve to `default.*` — theme-agnostic by design, and the wrapper agrees with the row because both ask the same function. `special.card` remains absent from the variants union; the resolver returns the same key `Item` already returned there, so that pre-existing gap is unchanged. Reported by Cursor Bugbot on #1326. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
538a944 to
6c194d9
Compare
📦 NPM canary releaseDeployed canary version 0.0.0-canary-2ac9da8. |
🏋️ Size limit report
Compared against main at f3d9a1f — run 32052281350, 2026-08-17T17:52:39Z.Click here if you want to find out what is changed in this build |
Removing the validation `theme` from the `ItemActionProvider` in `Select`, `Picker` and `FilterPicker` also removed the only channel telling those clear buttons which SURFACE they sit on. `CURRENT_ITEM_STYLES` steps up to its stronger alpha ramp on `theme=special`, so with `data-theme="default"` a clear button on a `special` trigger kept the light-scheme alphas (4%/6%) against the dark purple base, where they are close to invisible. Text color was unaffected — it inherits through `currentcolor` — so only the interaction fill was wrong. Probed on a `theme="special"` field: the trigger rendered `data-theme="special"` while the clear button rendered `data-theme="default"` in all three components. Each now forwards the field's own theme, and the buttons render `data-theme="special"` with `data-type="current"` — the `[data-theme="special"]` ramp rule (15%) applies and the scheme branches negate against it as intended. The theme travels through context rather than as a prop on purpose: an explicit `theme` prop is what opts an action out of `current` back to `clear`, and these buttons need to stay `current` so their label keeps inheriting the field's color. That is why `ItemActionContext` still carries `theme` at all. Field clear/trigger colors across neutral and invalid states are byte-identical to the previous verified state; only the special-surface ramp changed. Reported by Cursor Bugbot on #1326. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ints
`Picker` and `FilterPicker` paint their trigger with
`getValidationTheme(theme, {isInvalid, isValid})`, but the `ItemActionProvider`
beside it forwarded the RAW `theme`. Validation overrides the theme, so with
`theme="special"` on an invalid field the trigger renders `danger` — a light
surface — while the clear action still received `data-theme="special"` and picked
up the strong dark-purple alpha ramp, making hover/press read far too heavy.
Probed before: trigger `data-theme=danger` vs clear `data-theme=special`. Both now
agree across all six theme/validation combinations, with `data-type=current`
preserved:
Picker valid trigger=special clear=special
Picker isInvalid trigger=danger clear=danger
FilterPicker valid trigger=special clear=special
FilterPicker isInvalid trigger=danger clear=danger
Select valid trigger=special clear=special
Select isInvalid trigger=special clear=special
`Select` keeps forwarding the raw `theme` on purpose: its trigger is painted with
the raw theme too and surfaces validation through the wrapper rather than by
switching theme. The rule is that the forwarded value must always match whatever
the trigger paints with, which is now stated at both call sites so neither drifts
toward the other.
Field clear/trigger colors across neutral and invalid states are byte-identical to
the previous verified state; only the special-surface ramp selection changed.
Reported by Cursor Bugbot on #1326.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A selected `ItemAction` / `ItemBadge` looked unselected. Every other type marks `isSelected` with a brand hue — an accent-tinted fill under an accent label — and `current` has exactly one inherited color, so it cannot. It inherited the neutral types' `.09` step, which reads as a slightly dirty background rather than an "on" state, and its label colour does not change at all. Measured against the old default (`clear`) on a default row: clear selected fill rgba(109,113,176,.09) label rgb(100,103,162) (brand) current selected fill 9% of the inherited colour, label unchanged Selection now jumps clear of the interaction steps rather than continuing them (`.18` in light, up from `.09`); hover and press are untouched so a row full of actions does not look busy. Selection is a persistent state, so it earns the jump. The dark surfaces cannot spend as freely, and this is measured rather than guessed. In light the chip is a pale tint under an opaque dark label and contrast barely moves (`.30` still measures 5.66:1). On a dark surface the construction inverts — a light chip climbing toward an equally light label — and both the dark scheme and the special theme cross the AA floor (4.5:1) for their label at exactly `.24`. Every dark step is written under that ceiling, so `selected` there is a smaller jump, and the special hover/press steps came down to make room for it: light .04 .06 | .18 .24 .30 (5.66:1 at the top) dark .07 .11 | .16 .19 .22 (4.65:1 at the top) special .08 .12 | .17 .21 .24 (4.53:1 at the top) `SPECIAL_CLEAR_STYLES` escaped the same ceiling by inverting selected to a white pill with dark text, which a single inherited colour cannot do. Known limitation, pre-existing and left alone: on a `primary` row the white label measures 4.55:1 against the brand fill before any overlay, so every step of any ramp sits below AA there — the previous `.09` already did (3.84:1). Fixing that needs the row's own type as a styling signal, which is a separate change. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
An action inside a disabled row washed out. `current` paints from the color it
inherits, and a disabled host has already faded that color to
`#disabled-surface-text` — so `CURRENT_ITEM_STYLES` fading again by `.4`
multiplied the two.
Measured inside `<Item isDisabled>`:
row rgb(178, 181, 205)
action, before that colour at a further .4 (~rgb(224,225,228) on white)
action, after rgb(178, 181, 205) — matches the row
action, old `clear` type rgb(178, 181, 205) — what it used to be
`ItemAction` now sets an `inherit-disabled` mod when its disabled state came from
the surrounding `ItemActionProvider` rather than its own `isDisabled` prop, and the
fade is keyed on `disabled & !inherit-disabled`. An action disabled on its own in
an enabled row still fades to `.4`, keeping its own affordance. Nothing else sets
the mod, so `Item` continues to fade itself exactly as before (verified: a
`type="current"` disabled Item still renders at alpha .4).
Known limitation this exposes, NOT fixed here: `<ItemAction isDisabled={false}>`
inside a disabled row — the documented way to keep one action live — now inherits
the row's muted colour and so reads as disabled even though it is interactive.
`current` has only the inherited colour to work with and that colour is muted at
the source, so distinguishing it needs a signal beyond `currentcolor`.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 2bc4a71. Configure here.
| // `inherit-disabled` when its disabled state came from the surrounding | ||
| // `ItemActionProvider` rather than its own prop; nothing else sets the mod, so | ||
| // `Item` keeps fading itself as before. | ||
| 'disabled & !inherit-disabled': '#current.4', |
There was a problem hiding this comment.
Disabled actions stay full opacity
Medium Severity
Skipping the #current.4 fade when disabled is inherited assumes the host has already faded currentcolor. That holds for Item, where actions sit inside the row, but not for ItemButton, whose ActionsWrapper only paints the resting color via ITEM_RESTING_COLOR_VARIANTS. Inherited-disabled actions there keep full-strength labels while non-interactive.
Additional Locations (1)
Reviewed by Cursor Bugbot for commit 2bc4a71. Configure here.


ItemAction/ItemBadgenow default totype="current"and no longer mirror the host row'stypefrom context.currentderives every color from the inheritedcurrentcolor, so one type covers every host type × theme that the context mapping used to enumerate — and becausecurrentcoloris inherited rather than resolved once, an action also follows its row through hover, selected and disabled instead of holding a fixed palette. The mapping inItemActionProviderthat foldeditem/outline/outline-2/header/cardontoclearis gone.Started from the question "can we drop
ItemActionContextentirely?" — the answer is no, but the type-inheritance half of it can go.What stays, and why
ItemActionContextstill carriesdisableActionsFocus,isDisabled, thetheme, andtype— the last only for its presence, which drives thecontextmod that collapses an action's side margins. The provider's signature is unchanged, so no call site moved.themeis still consumed for exactly one reason:currentneeds to know when it sits on the special theme's dark surface, where the alpha ramp has to step harder to stay visible.An explicit
themeopts an action out ofcurrentand back toclear, sincecurrentis theme-agnostic by construction and the theme would otherwise have nothing to color.theme="default"is excluded from that fallback — passing a prop's own default value must not change what renders, which is the invariantno-redundant-default-proplints for. The first cut got this wrong andprobe.test.tsxcase E caught it.Supporting changes
#currentalphas don't adapt to the color scheme — a 4% tint of a dark label on a light surface reads far stronger than 4% of a light label on a dark one. Each step now carries a light value, an@darkvalue, and atheme=specialvalue (special is static across light/dark/HC by design, so one ramp). Each step lives in its own custom property rather than inline infill: three ramps in one state-map would put ~18 alpha values where Tasty'smergeEntriesByValuepass coalesces equal value strings into one OR-entry at the group's max priority and breaks negation against lower-priority rules — the failure modeSPECIAL_OUTLINE_STYLESdocuments.ItemActionregains a focus ring.CURRENT_ITEM_STYLESfollows the*_ITEM_STYLESconvention of leaving focus to the collection that owns the row, which is wrong for a focusable action. The ring moves intoItemAction's base styles, where every variant's ownoutlinestill overrides it, so onlycurrentis affected.ItemButtonpaints its actions' color. It renders actions as a sibling of the button rather than inside it — deliberately, so they aren't nested in a<button>— socurrentcolorreached them from the page: adangerrow handed its actions neutral text, and aspecialrow handed them the page's dark text to tint on a dark purple surface.ActionsWrappernow carries the row's resting color, derived from the variant map rather than restating the palette.Item's inlinetheme.type→ styles object is now the exportedITEM_VARIANTS, shared with the color projection above so the two can't drift.Field components
Every clear and trigger button in the field components drops its explicit
type/ validationthemeand relies on the new default, so each takes the color of the field it sits in and follows a custom theme instead of being pinned todefault.clear. Where that changes rendering, it changes it toward matching the field's own text:Picker,FilterPickerclearrgb(157,88,85)ComboBox,SearchComboBox,SearchInputclearrgb(157,88,85)→rgb(130,72,69)ComboBoxtriggerrgb(68,69,77)→rgb(130,72,69)PasswordInputtoggleSelectclearrgb(157,88,85)→rgb(68,69,77)That last row is the one deliberate regression, and it surfaces a real inconsistency rather than being caused by this change:
Select's trigger keeps neutral label text when invalid, whilePickerandFilterPicker— alsoItem-based — tint theirs. So atmainan invalidSelectshowed a neutral label beside a red clear button. The button now matches its own field, and the red border still signals invalidity. FixingSelect's non-tinting invalid label belongs inSelect, and is worth a follow-up — masking it with a themed clear button is what we just removed.ColorInput's pipette andDatePicker's calendar button had no explicit type and were already picking upcurrent.Tooling fix
.claude/worktreesis now excluded from vitest collection and from git. Vitest globs withdot: true, so other branches' checkouts were collected alongside the real suite: every spec ran twice, a CLI path argument matched as a substring in both trees, and failures were reported from code not on this branch. Worse for anything that writes — the registry generator behindpnpm audit-defaultsresolves its output againstprocess.cwd(), so a worktree's copy silently overwrote this tree'sdefaults.generated.tswith its own branch's registry, wiping the wholealiasesblock.oxlint src,prettier "src/**"andtsconfigare all rooted atsrcand were never affected.Notes for the reviewer
ItemAction/ItemBadgetypereturns to a plain'current'default in the lint registry. Theskip: 'context'it was given in feat(eslint-plugin): lint ui-kit with its own no-redundant-default-prop rule #1319 existed because the prop resolved throughItemActionProvider; it no longer does.isDisabledis still context-resolved and still skipped.#current.28rather than inverting to a white pill — the inversion is aSPECIAL_CLEAR_STYLESsignature that a single inherited ramp can't express..04/.06/.09/.12/.15, dark.07/.1/.14/.18/.22, special.15/.22/.28/.34/.4) are the main design tunables here if any step reads too strong or too weak.CURRENT_BUTTON_STYLES(Button'scurrenttype) has the same dark-scheme weakness but was left alone as out of scope.Verification
pnpm vitest run— 1870 passed, 1 skipped, 91 filespnpm lintclean,pnpm buildclean, no newtscerrors (18, matching themainbaseline)pnpm probe:browser --computedand diffed againstmain, not reasoned about; the ComboBox trigger was the only unintended line that moved, and it was kept on purpose.🤖 Generated with Claude Code
Note
Medium Risk
Wide visual surface across list actions and many field affordances, with intentional invalid-state color shifts; core styling is centralized but regressions would show as contrast or theme mismatches on special/dark surfaces.
Overview
ItemAction/ItemBadgedefault totype="current"instead of mirroring the host row'stypefromItemActionProvider. Colors come from inheritedcurrentcolor, so actions track hover, selected, and disabled with the row; explicit non-defaultthemestill falls back toclear. Provider no longer maps row types ontoclear.Theme/styling plumbing:
CURRENT_ITEM_STYLESgains a light /@dark/theme=specialalpha ramp via custom properties, stronger selected chips, focus ring onItemAction, andinherit-disabledso host-disabled rows are not double-faded.ITEM_VARIANTS,resolveItemVariant, andITEM_RESTING_COLOR_VARIANTScentralize variants;ItemButton'sActionsWrappersets resting row color for sibling actions.Field components drop pinned
type/getValidationThemeon clears and related controls so icons inherit the field text (notably invalid ComboBox trigger/clear align with input text; Select clear is neutral when invalid because the trigger label stays neutral).Tooling: ESLint defaults treat
typeas'current'; Vitest and.gitignoreexclude.claude/worktrees.Reviewed by Cursor Bugbot for commit 2bc4a71. Bugbot is set up for automated code reviews on this repo. Configure here.