Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
1916cc2
feat(theme): move `current` from the type axis to the theme axis
claude Aug 19, 2026
12c88e2
docs(Button): drop the redundant Pressed&Hovered state from the state…
claude Aug 19, 2026
e209376
fix(themes): stop the disabled selected chip outweighing its enabled …
claude Aug 19, 2026
eeb6683
refactor: drop the type="current" deprecation shim
claude Aug 19, 2026
1690170
Merge remote-tracking branch 'origin/main' into claude/button-item-cu…
claude Aug 19, 2026
c854fbb
fix(themes): fade only the label on a disabled selected chip
claude Aug 19, 2026
c6a3435
fix(themes): keep the status themes' own clear chip when disabled
claude Aug 19, 2026
bb27112
fix(themes): mute the special theme's disabled labels
claude Aug 19, 2026
7348e01
fix(themes): give current.clear a disabled selected chip
claude Aug 19, 2026
8e5f0a7
fix(Banner): stop clearing the border off its actions
claude Aug 19, 2026
545ad32
feat(themes): invert current.primary
claude Aug 19, 2026
0f6e603
fix(themes): recolor every icon slot on current.primary, stop double-…
claude Aug 19, 2026
3b44314
fix(themes): label current.primary with #surface so it inverts in dar…
claude Aug 19, 2026
b116368
fix(themes): give current.primary a visible rim
claude Aug 19, 2026
26d45a5
feat(themes): add an `invert` type on every theme
claude Aug 19, 2026
7689b36
Merge remote-tracking branch 'origin/main' into claude/button-item-cu…
claude Aug 19, 2026
9125615
fix(themes): fade a disabled `current` label exactly once, pin the in…
claude Aug 19, 2026
e59cc2b
fix(themes): make `current.invert` invert `primary` instead of aliasi…
claude Aug 19, 2026
f4c5722
fix(item): give `current.invert` the inheriting HotKeys hint
claude Aug 19, 2026
f37a0d0
feat(banner): make `Banner.Action` a filled `invert` chip on the bann…
claude Aug 19, 2026
e31092d
feat(themes): let a container offer `current` a color via `--current-…
claude Aug 20, 2026
20ae376
fix(banner): offer a muted `--current-accent` when the banner is disa…
claude Aug 20, 2026
60b8975
fix(themes): let a container redirect `current.primary`'s label too
claude Aug 20, 2026
4f0fa82
fix(themes): split the `current` label hook in two, and four review f…
claude Aug 20, 2026
c35db73
Merge origin/main into claude/button-item-current-theme-js20o6
claude Aug 20, 2026
c49a0a6
fix(themes): fade `current.primary`'s label on a bare `disabled`
claude Aug 20, 2026
1f7f7fb
fix(themes): make `current.invert` one mirrored pair with `primary`
claude Aug 20, 2026
6beda2e
refactor(themes): drop `invert`, label `current.primary` with `#curre…
tenphi Aug 20, 2026
545af49
fix(themes): derive the `current` dark ramp, drop the special one
tenphi Aug 20, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions .changeset/current-single-fade.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
'@cube-dev/ui-kit': patch
---

Fix the `current` theme fading a disabled label twice.

`current.item` and `current.clear` already suppressed their own `.4` label fade when the disabled state was inherited from a host that had faded `currentcolor` already, but `current.outline`, `current.outline-2`, `current.primary` and `current.link` stated it as a bare `disabled`. Two of those — `outline` and `primary` — are reachable `Item.Action` types, so an action inside a disabled row (including `Banner`'s outline actions) multiplied the two fades and rendered at `.16` of the row's color, washing out both the label and the alpha chip.

Every `current` flavour now gates the fade on `disabled & !inherit-disabled & !inside-wrapper`. The second mod closes the other half of the same hole: `ItemButton` renders its actions as siblings of the row inside a wrapper, and the wrapper reproduces the row's disabled color so those siblings inherit a faded `currentcolor`. It previously could not, because the gated key was skipped when deriving the wrapper's colors — so a disabled `ItemButton` on the `current` theme sat next to full-strength actions. The wrapper now reads the gated value, and the row suppresses its own fade under `inside-wrapper`, leaving exactly one `.4` on every path.
56 changes: 56 additions & 0 deletions .changeset/current-theme-axis.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
---
'@cube-dev/ui-kit': minor
---

`current` moves from the `type` axis to the `theme` axis on `Button`, `Item` (and `ItemButton`), `Item.Action` and `ItemBadge`. It was never a shape: it names where the colors come from — the inherited `currentcolor` rather than a brand ramp — which is the question `theme` answers. As a type it occupied the slot that decides emphasis, so picking `current` meant giving up the choice between a filled button, an outlined one and a bare label.

On the `theme` axis it composes instead, and every type now has a `current` flavour:

- `item` — the old `Item` shape: no border, nothing painted at rest, the fill stepping in on hover, pressed and selected.
- `clear` — the same ramp plus the focus ring a standalone control needs. The default for `Item.Action` and `ItemBadge`.
- `outline` — the old `Button` shape: a resting `#current.03` chip inside a `#current.08` border.
- `outline-2` — `outline` for a container that is already painting something. The brand themes swap an opaque base (`#surface-3` for `#surface-2`); `current` has no opaque base to swap, so the same intent is carried by roughly doubling the tint at every step.
- `primary` — the high-emphasis control, and the one flavour that fills opaquely, like every other theme's `primary`: the fill is the inherited color at full opacity and the label is punched out of it with the new `#current-fill` token, which defaults to `#surface` — the page background, which is always the opposite of the text painted on it and so follows the scheme for free. Hover and pressed lay a translucent `#black` over the same base, since an arbitrary color has no lighter or darker sibling to step to the way the brand ramps walk `accent-surface` to `-2` and `-3`. The rim comes from the same token at `.25` — every other `primary` rims its fill with `accent-surface-border`, cr 1.48 against it, and this measures 1.82 in light and 1.55 in dark. Disabled swaps the rim to `#surface-text.2`, which holds against a `.4` chip that a `#surface` rim would wash into. The label is painted with `-webkit-text-fill-color` rather than `color`: `#current` compiles to the literal `currentcolor`, which in `fill` resolves against the element's own `color`, so setting `color` to the label token would make the fill resolve to the label color and paint a white pill with a white label.

`#current-fill` is a real color token with a default, not a bare custom property, so it takes the alpha suffix (`#current-fill.5` is the disabled label) and a container overrides it with one declaration — `styles={{ '#current-fill': '#fixed-dark' }}` — moving the label, the icon slots and the rim together. It exists for the one container the `#surface` default is wrong for: a container whose own text color IS the page paints `#white`, which IS `#surface` in light mode, so an unaided label measures cr 1.00 against its own chip. Its own fill is the right value there, since it contrasts with its own text by construction. Ordinary containers set nothing.
- `link` — no chip at all. The brand themes intensify from `accent-text-soft` to `accent-text` on hover; here "soft" is the inherited color at `.8` and "strong" is it at full opacity.
- `card` — the static panel: a `#current.05` fill inside a `#current.2` border (`Item` only).

`current.outline` and `current.item` are byte-identical to the old `Button` and `Item` flavours, so nothing that used `type="current"` changes appearance.

The top step of each ramp stops at `#current.24` in light. The dark counterpart is not authored: each `@dark` step is solved so its OKHST tone delta from the surface matches the light step's, which lands the two schemes on the same chip-vs-page contrast (1.084 / 1.083 at hover, 1.959 / 1.961 at the top step). That works out *lower* than the light alpha throughout — `.031 / .046 / .13 / .175 / .221` against `.04 / .06 / .18 / .24 / .3` — because near the dark end of the scale a small sRGB move is a large perceptual one, so the same tint reads stronger on a dark surface than on a light page.

### Migration

**`type="current"` is removed with no runtime fallback.** It resolves to no variant and falls back to base styles, the same as any other unknown type — there is no mapping and no deprecation warning. The spelling shipped one release ago and has no consumers outside the kit, so this is a clean break rather than a deprecation:

| Old | New |
| ------------------------------ | ------------------------------------------------------- |
| `<Button type="current">` | `<Button theme="current">` (type defaults to `outline`) |
| `<Item type="current">` | `<Item theme="current">` (type defaults to `item`) |
| `<Item.Action type="current">` | drop it — `current` is already the default theme |
| `<ItemBadge type="current">` | drop it — `current` is already the default theme |

`Item` accepts `theme="current"` with every type except `header`, which stays theme-agnostic; the warning that fired for `type="current"` with any theme but `default` is gone, since there is no longer such a pair to reject.

### `Item.Action` / `ItemBadge` defaults

`type` now defaults to `clear` and `theme` to `current`, and neither is read from `ItemActionContext` any more — the two axes are independent, so a shape no longer implies a color source and vice versa. Both defaults are plain values the lint registry can prove, which the previous `theme` entry (`skip: 'context'`) was not.

This changes one case: an action that named a `type` but no `theme` used to inherit the host row's theme, and now takes the host's color through `currentcolor` instead. Inside a themed row the two are close by construction — a `danger` row paints `#danger-accent-text`, which is what the action then mixes from — but the chip is an alpha tint rather than the brand ramp. Pass `theme="default"` (or any other theme) to opt back into a fixed palette.

`Banner` is the one in-repo consumer that needed a matching edit. Its actions ask for `type="outline"` and then cleared the border, because back then `outline` meant `note.outline` and friends — whose border is the opaque `#note-border`, a pale line built for a `#surface-2` chip on a light page and plainly wrong on a saturated banner. The fill carried the chip on its own there. On the `current` theme the border is `#current.08` mixed from the banner's own white label, and the fill is a 3% tint that cannot carry a chip by itself, so clearing the border left the action invisible. The override is gone and the type renders as designed.

`ItemActionContext` no longer reaches the DOM at all beyond `isDisabled`. An interim version of this branch published the host theme as a `data-surface` attribute so the `current` ramp could pick per-surface alphas for the `special` theme's fixed dark-purple surface; that is gone, because only `ItemAction` and `ItemBadge` ever set the attribute, so `Button` and `Item` on the same surface silently fell back to the light ramp.

### Two nesting fixes

Both fall out of `current.primary` keeping `color` as the fill rather than the label:

- The `Actions` slot is recolored to the label, like the icon slots already were — a nested `Item.Action` defaults to `theme="current"` and mixes its own label from the `currentcolor` it inherits, so without this it took the chip color and vanished into it.
- `ItemButton`'s `ActionsWrapper` reproduces the label rather than the chip, for the same reason on the sibling path.

### Also

- `current` is registered in `TastyThemeNames`, so `theme="current"` autocompletes on every tasty component.
- New `CurrentStates` stories on `Button` and `Item` sweep every type and state on the theme, inside containers that paint their own text color; the context sweeps are renamed `CurrentTheme`.
12 changes: 6 additions & 6 deletions .changeset/item-action-current-default.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,17 @@
'@cube-dev/ui-kit': minor
---

`ItemAction` / `ItemBadge` now default to `type="current"` and stop mirroring the host row's `type` from context. `current` derives every color from the inherited `currentcolor`, so one type covers every host type × theme combination 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.
`ItemAction` / `ItemBadge` now default to the inherited-color `current` flavour and stop mirroring the host row's `type` from context. (A later change in this release moves `current` from the `type` axis to the `theme` axis, so the default is spelled `theme="current"` with `type="clear"` — see "`current` moves from the `type` axis to the `theme` axis". The behaviour described here is unchanged by that move.) `current` derives every color from the inherited `currentcolor`, so one type covers every host type × theme combination 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.

`ItemActionContext` stays. It 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.

Passing 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. The `theme="default"` case 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.
Passing an explicit `theme` opts an action out of the inherited color and into that palette.

Supporting changes:

- **Selection reads as a filled chip.** Every other type marks `isSelected` with a brand *hue* — an accent-tinted fill under an accent label — and `current` has one inherited color to work with, so it cannot. Alpha is the only channel left, and the neutral types' `.09` step read as a slightly dirty background rather than an "on" state, so a selected `ItemAction` / `ItemBadge` looked unselected. Selection now jumps clear of the interaction steps (`.18` in light) instead of continuing them, while hover and press stay subtle so a row full of actions is not busy. On dark surfaces the same construction inverts — a light chip climbing toward an equally light label — and both the dark scheme and the special theme hit the AA floor for their label at exactly `.24`, so their steps are written under that measured ceiling and `selected` is a smaller jump there.
- **A scheme-aware alpha ramp.** Unlike the brand tokens, `#current` alphas do not adapt to the color scheme — a 4% tint of a dark label on a light surface reads far stronger than a 4% tint of a light label on a dark one. Each step now carries a per-surface value: the base entry for light, `@dark` for the dark scheme, and `theme=special` as a single fixed ramp (special is static across light/dark/HC by design). Each step lives in its own custom property rather than inline in `fill`, because 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.
- **`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 to `ItemAction`'s base styles, where every variant's own `outline` still overrides it, so only `current` is affected.
- **Selection reads as a filled chip.** Every other type marks `isSelected` with a brand *hue* — an accent-tinted fill under an accent label — and `current` has one inherited color to work with, so it cannot. Alpha is the only channel left, and the neutral types' `.09` step read as a slightly dirty background rather than an "on" state, so a selected `ItemAction` / `ItemBadge` looked unselected. Selection now jumps clear of the interaction steps (`.18` in light) instead of continuing them, while hover and press stay subtle so a row full of actions is not busy. The dark scheme takes the same jump, scaled by the tone match described below.
- **A scheme-aware alpha ramp.** Unlike the brand tokens, `#current` alphas do not adapt to the color scheme: the same tint is not the same step in light and dark. Each step therefore carries a base entry for light and an `@dark` counterpart, the dark one *derived* — solved so its OKHST tone delta from the surface matches the light step's, which is also what puts the chip on the light step's contrast against the page. The direction is counter-intuitive: the dark alphas come out lower, because near the dark end of the scale a small sRGB move is a large perceptual one. Each step lives in its own custom property rather than inline in `fill`, because both ramps in one state-map would put twelve 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.
- **`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, so the ring came back on the action itself.
- **`ItemButton` paints its actions' color.** It renders actions as a sibling of the button rather than inside it — deliberately, so they are not nested in a `<button>` — so `currentcolor` reached them from the page instead of the row: 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 cannot drift.

Expand All @@ -25,4 +25,4 @@ Where that changes rendering, it changes it toward matching the field's own text
- `PasswordInput`'s toggle now tints with the field instead of always rendering neutral.
- `Select`'s clear button no longer turns red when the field is invalid. Its trigger keeps **neutral** label text in that state, so the button now matches its own field, and the red border still signals invalidity. `Picker` and `FilterPicker` are also `Item`-based but do tint their trigger text — that inconsistency lives in `Select` and is worth fixing there rather than being masked by a themed clear button.

`ItemAction` / `ItemBadge` `type` therefore returns to a plain `'current'` default in the lint registry. The `skip: 'context'` classification it was given existed because the prop resolved through `ItemActionProvider`; it no longer does. `isDisabled` is still context-resolved and still skipped.
`ItemAction` / `ItemBadge` `type` therefore returns to a plain default in the lint registry. The `skip: 'context'` classification it was given existed because the prop resolved through `ItemActionProvider`; it no longer does. `isDisabled` is still context-resolved and still skipped.
Loading
Loading