Skip to content

feat(ItemAction): default to the current type instead of inheriting the row's - #1326

Open
tenphi wants to merge 7 commits into
mainfrom
feat/item-action-current-default
Open

feat(ItemAction): default to the current type instead of inheriting the row's#1326
tenphi wants to merge 7 commits into
mainfrom
feat/item-action-current-default

Conversation

@tenphi

@tenphi tenphi commented Aug 17, 2026

Copy link
Copy Markdown
Member

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 × theme that the context mapping used to enumerate — and because currentcolor is inherited rather than resolved once, an action also follows its row through hover, selected and disabled instead of holding a fixed palette. The mapping in ItemActionProvider that folded item / outline / outline-2 / header / card onto clear is gone.

Started from the question "can we drop ItemActionContext entirely?" — the answer is no, but the type-inheritance half of it can go.

What stays, and why

ItemActionContext still carries disableActionsFocus, isDisabled, the theme, and type — the last only for its presence, which drives the context mod that collapses an action's side margins. The provider's signature is unchanged, so no call site moved.

theme is still consumed for exactly one reason: current needs 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 theme opts an action out of current and back to clear, since current is 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 invariant no-redundant-default-prop lints for. The first cut got this wrong and probe.test.tsx case E caught it.

Supporting changes

  • A scheme-aware alpha ramp. Unlike the brand tokens, #current alphas 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 @dark value, and a theme=special value (special is static across light/dark/HC by design, so one ramp). Each step lives in its own custom property rather than inline in fill: three ramps in one state-map would put ~18 alpha values where Tasty's mergeEntriesByValue pass coalesces equal value strings into one OR-entry at the group's max priority and breaks negation against lower-priority rules — the failure mode SPECIAL_OUTLINE_STYLES documents.
  • ItemAction regains a focus ring. CURRENT_ITEM_STYLES follows the *_ITEM_STYLES convention of leaving focus to the collection that owns the row, which is wrong for a focusable action. The ring moves into ItemAction's base styles, where every variant's own outline still overrides it, so only current is affected.
  • ItemButton paints 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> — so currentcolor reached them from the page: a danger row handed its actions neutral text, and a special row handed them the page's dark text to tint on a dark purple surface. ActionsWrapper now carries the row's resting color, derived from the variant map rather than restating the palette.
  • One variants map. Item's inline theme.type → styles object is now the exported ITEM_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 / validation theme and 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 to default.clear. Where that changes rendering, it changes it toward matching the field's own text:

Button Invalid-state color Change
Picker, FilterPicker clear rgb(157,88,85) none — trigger text already carries validation
ComboBox, SearchComboBox, SearchInput clear rgb(157,88,85)rgb(130,72,69) now matches the input text exactly
ComboBox trigger rgb(68,69,77)rgb(130,72,69) was neutral beside red input text
PasswordInput toggle neutral → tinted now follows the field
Select clear rgb(157,88,85)rgb(68,69,77) no longer reddens when invalid

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, while Picker and FilterPicker — also Item-based — tint theirs. So at main an invalid Select showed a neutral label beside a red clear button. The button now matches its own field, and the red border still signals invalidity. Fixing Select's non-tinting invalid label belongs in Select, and is worth a follow-up — masking it with a themed clear button is what we just removed.

ColorInput's pipette and DatePicker's calendar button had no explicit type and were already picking up current.

Tooling fix

.claude/worktrees is now excluded from vitest collection and from git. Vitest globs with dot: 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 behind pnpm audit-defaults resolves its output against process.cwd(), so a worktree's copy silently overwrote this tree's defaults.generated.ts with its own branch's registry, wiping the whole aliases block. oxlint src, prettier "src/**" and tsconfig are all rooted at src and were never affected.

Notes for the reviewer

  • ItemAction / ItemBadge type returns to a plain 'current' default in the lint registry. The skip: '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 through ItemActionProvider; it no longer does. isDisabled is still context-resolved and still skipped.
  • A selected action inside a special row now paints #current.28 rather than inverting to a white pill — the inversion is a SPECIAL_CLEAR_STYLES signature that a single inherited ramp can't express.
  • The ramp alphas (light .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's current type) has the same dark-scheme weakness but was left alone as out of scope.

Verification

  • pnpm vitest run — 1870 passed, 1 skipped, 91 files
  • pnpm lint clean, pnpm build clean, no new tsc errors (18, matching the main baseline)
  • Every inherited color above was measured with pnpm probe:browser --computed and diffed against main, 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 / ItemBadge default to type="current" instead of mirroring the host row's type from ItemActionProvider. Colors come from inherited currentcolor, so actions track hover, selected, and disabled with the row; explicit non-default theme still falls back to clear. Provider no longer maps row types onto clear.

Theme/styling plumbing: CURRENT_ITEM_STYLES gains a light / @dark / theme=special alpha ramp via custom properties, stronger selected chips, focus ring on ItemAction, and inherit-disabled so host-disabled rows are not double-faded. ITEM_VARIANTS, resolveItemVariant, and ITEM_RESTING_COLOR_VARIANTS centralize variants; ItemButton's ActionsWrapper sets resting row color for sibling actions.

Field components drop pinned type / getValidationTheme on 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 type as 'current'; Vitest and .gitignore exclude .claude/worktrees.

Reviewed by Cursor Bugbot for commit 2bc4a71. Bugbot is set up for automated code reviews on this repo. Configure here.

@vercel

vercel Bot commented Aug 17, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
cube-ui-kit Ready Ready Preview Aug 17, 2026 8:35pm

Request Review

@changeset-bot

changeset-bot Bot commented Aug 17, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 2bc4a71

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
@cube-dev/ui-kit Minor

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

@github-actions

github-actions Bot commented Aug 17, 2026

Copy link
Copy Markdown
Contributor

🧪 Storybook is successfully deployed!

Comment thread src/components/actions/ItemButton/ItemButton.tsx Outdated
tenphi and others added 2 commits August 17, 2026 19:37
… 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>
Comment thread src/components/fields/Picker/Picker.tsx Outdated
@github-actions

github-actions Bot commented Aug 17, 2026

Copy link
Copy Markdown
Contributor

📦 NPM canary release

Deployed canary version 0.0.0-canary-2ac9da8.

@github-actions

github-actions Bot commented Aug 17, 2026

Copy link
Copy Markdown
Contributor

🏋️ Size limit report

Name Size Passed?
All 487.75 KB (+0.07% 🔺) Yes 🎉
Tree shaking (just a Button) 119.78 KB (+0.16% 🔺) Yes 🎉

Compared against main at f3d9a1frun 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>
Comment thread src/components/fields/Picker/Picker.tsx Outdated
…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>

@cursor cursor Bot 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.

Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.

Fix All in Cursor

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

Comment thread src/data/item-themes.ts
// `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',

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.

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)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 2bc4a71. Configure here.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant