diff --git a/.changeset/item-selected-disabled.md b/.changeset/item-selected-disabled.md
new file mode 100644
index 000000000..02c829672
--- /dev/null
+++ b/.changeset/item-selected-disabled.md
@@ -0,0 +1,5 @@
+---
+'@cube-dev/ui-kit': patch
+---
+
+`Item` themes: a disabled item now keeps showing whether it is selected. The `disabled` entry in every `outline`, `outline-2` and `clear` variant used to override `selected` outright, so a disabled segmented control — `RadioGroup type="button"` most visibly — rendered every option identically with no sign of which one was active. Each of those variants gains a `selected & disabled` state that paints the brand-tinted `accent-disabled-surface` chip and its paired label instead of the neutral one, across all six themes (`special` and `current` stay in their own white-alpha / `currentcolor` registers). `type="item"` rows are unchanged — they already keep brand identity in their disabled label and pair selection with a checkmark.
diff --git a/.changeset/radio-button-item-props.md b/.changeset/radio-button-item-props.md
new file mode 100644
index 000000000..88e8c11b9
--- /dev/null
+++ b/.changeset/radio-button-item-props.md
@@ -0,0 +1,5 @@
+---
+'@cube-dev/ui-kit': minor
+---
+
+`Radio` / `Radio.Button`: a button- or tabs-type radio now accepts the full content API of the `Item` it renders, matching `ItemButton`. Newly forwarded: `descriptionPlacement`, `descriptionProps`, `keyboardShortcutProps`, `isLoading`, `loadingSlot`, `highlight`, `highlightCaseSensitive`, `highlightStyles`, `level` and `labelRef`. `description` now has a grid area to land in, so it renders in both `inline` and `block` placement, and container style props (`padding`, `gap`, `fill`, `preset`, …) apply to button-type radios instead of being dropped. A loading button radio is also disabled, so it no longer takes clicks or arrow-key selection — pass an explicit `isDisabled={false}` to opt out.
diff --git a/.storybook/preview-head.html b/.storybook/preview-head.html
index 84dcf77ba..7e1c36bc4 100644
--- a/.storybook/preview-head.html
+++ b/.storybook/preview-head.html
@@ -1,3 +1,62 @@
+
+
+
diff --git a/src/components/fields/RadioGroup/Radio.tsx b/src/components/fields/RadioGroup/Radio.tsx
index 981e93033..1d98532cb 100644
--- a/src/components/fields/RadioGroup/Radio.tsx
+++ b/src/components/fields/RadioGroup/Radio.tsx
@@ -1,9 +1,10 @@
import { useFocusableRef } from '@react-spectrum/utils';
import {
BaseProps,
+ CONTAINER_STYLES,
+ ContainerStyleProps,
filterBaseProps,
OUTER_STYLES,
- OuterStyleProps,
tasty,
} from '@tenphi/tasty';
import { forwardRef, useMemo, useRef } from 'react';
@@ -38,8 +39,20 @@ const RadioButtonElement = tasty(Item, {
preset: 't3m',
lineHeight: '1em',
flexGrow: 1,
- gridTemplate:
- '"icon prefix label suffix rightIcon actions" auto / max-content max-content max-content max-content max-content max-content',
+ // Same areas as `Item`, but every column is `max-content` — a radio button
+ // is sized by its content, not stretched like a list item. The
+ // `description` rows have to be repeated here because a plain string would
+ // replace `Item`'s whole state map and leave the description without a
+ // grid area to land in.
+ gridTemplate: {
+ '': '"icon prefix label suffix rightIcon actions" auto / max-content max-content max-content max-content max-content max-content',
+ 'description=inline':
+ '"icon prefix description suffix rightIcon actions" auto / max-content max-content max-content max-content max-content max-content',
+ 'description=inline & has-label':
+ '"icon prefix label suffix rightIcon actions" auto "icon prefix description suffix rightIcon actions" auto / max-content max-content max-content max-content max-content max-content',
+ 'description=block':
+ '"icon prefix label suffix rightIcon actions" auto "description description description description description description" auto / max-content max-content max-content max-content max-content max-content',
+ },
placeContent: 'center',
shadow: {
'': false,
@@ -161,11 +174,61 @@ const RadioLabelElement = tasty({
styles: INLINE_LABEL_STYLES,
});
+/**
+ * `Item` props a button-type radio hands straight to the `Item` it renders, so
+ * `Radio.Button` accepts the same content and presentation API as an
+ * `ItemButton`. Kept as a runtime list so a new `Item` prop only has to be
+ * added in one place.
+ *
+ * Deliberately absent:
+ * - `size`, `type`, `theme`, `isSelected`, `isDisabled`, `mods`, `styles` —
+ * owned by the radio (they come from the prop/context/validation resolution
+ * below).
+ * - `shape` — the radius is owned by the button-group layout below, which keeps
+ * only the outer-side radius on the first/last radio so the group reads as one
+ * continuous control. Forwarding `shape` would have no effect.
+ * - `htmlType`, `as`, `insideWrapper`, `showActions` — internal to `Item` /
+ * `ItemButton`.
+ * - `actions`, `autoHideActions`, `preserveActionsSpace`,
+ * `disableActionsFocus` — a button radio renders as a ``, and a click
+ * anywhere inside a label activates its control, so nested action buttons
+ * would select the radio (and nested interactive content is invalid inside a
+ * label to begin with).
+ */
+const ITEM_PROPS = [
+ 'icon',
+ 'rightIcon',
+ 'prefix',
+ 'suffix',
+ 'description',
+ 'descriptionPlacement',
+ 'descriptionProps',
+ 'tooltip',
+ 'hotkeys',
+ 'keyboardShortcutProps',
+ 'level',
+ 'isLoading',
+ 'loadingSlot',
+ 'highlight',
+ 'highlightCaseSensitive',
+ 'highlightStyles',
+ 'labelRef',
+] as const;
+
+type RadioItemProps = Pick;
+
export interface CubeRadioProps
extends BaseProps,
AriaRadioProps,
Omit,
- OuterStyleProps {
+ /**
+ * Container style props apply to button-type radios (they style the `Item`
+ * the radio renders). A classic radio only reads the outer subset —
+ * everything else belongs to its inner circle and label.
+ */
+ ContainerStyleProps,
+ /** All of these apply to button/tabs-type radios only. */
+ RadioItemProps {
'aria-label'?: string;
/* The visual type of the radio button */
type?: 'button' | 'radio';
@@ -173,20 +236,6 @@ export interface CubeRadioProps
value?: string;
/* Size of the button (for button type only) */
size?: Omit;
- /* Icon to display (for button type only) */
- icon?: CubeItemProps['icon'];
- /* Icon to display on the right (for button type only) */
- rightIcon?: CubeItemProps['rightIcon'];
- /* Prefix element (for button type only) */
- prefix?: CubeItemProps['prefix'];
- /* Suffix element (for button type only) */
- suffix?: CubeItemProps['suffix'];
- /* Description text (for button type only) */
- description?: CubeItemProps['description'];
- /* Tooltip configuration (for button type only) */
- tooltip?: CubeItemProps['tooltip'];
- /* Keyboard shortcut (for button type only) */
- hotkeys?: CubeItemProps['hotkeys'];
}
function Radio(props: CubeRadioProps, ref) {
@@ -205,13 +254,6 @@ function Radio(props: CubeRadioProps, ref) {
type,
buttonType,
size,
- icon,
- rightIcon,
- prefix,
- suffix,
- description,
- tooltip,
- hotkeys,
'aria-label': ariaLabel,
form,
...otherProps
@@ -219,7 +261,17 @@ function Radio(props: CubeRadioProps, ref) {
label = label || children;
- let styles = extractStyles(otherProps, OUTER_STYLES);
+ // Only the props the caller actually passed, so an unset one keeps `Item`'s
+ // own default instead of being overridden with `undefined`.
+ const itemProps: Record = {};
+
+ for (const itemPropName of ITEM_PROPS) {
+ if (itemPropName in props) {
+ itemProps[itemPropName] = (props as Record)[
+ itemPropName
+ ];
+ }
+ }
labelStyles = {
...INLINE_LABEL_STYLES,
@@ -243,6 +295,15 @@ function Radio(props: CubeRadioProps, ref) {
let effectiveType = type ?? contextType ?? 'radio';
let isButton = effectiveType === 'button' || effectiveType === 'tabs';
+ // A button radio *is* an `Item`, so it takes the same container style props an
+ // `ItemButton` does. A classic radio only exposes the outer subset: the rest
+ // would land on the wrapper that holds the circle and the label, where
+ // `padding` / `fill` / `preset` mean something entirely different.
+ let styles = extractStyles(
+ otherProps,
+ isButton ? CONTAINER_STYLES : OUTER_STYLES,
+ );
+
// Determine effective size with priority: prop > context > default
let effectiveSize: CubeItemProps['size'] = (size ??
contextSize ??
@@ -261,7 +322,10 @@ function Radio(props: CubeRadioProps, ref) {
// When buttonType is 'primary', non-selected radios use 'outline' with a
// visual-only `selected` mod (mods.selected=true) to render the brand-tinted
// outline+selected look — this is the only place mods.selected intentionally
- // decouples from the aria/isSelected state.
+ // decouples from the aria/isSelected state. Suppressed while disabled: the
+ // real selected radio is then painted with the brand-tinted disabled chip
+ // (`accent-disabled-surface`), and faking `selected` on its siblings would
+ // give them the outline variant of that same chip and erase the distinction.
let forceSelectedMod = false;
if (effectiveType === 'tabs') {
effectiveButtonType = 'clear';
@@ -280,7 +344,21 @@ function Radio(props: CubeRadioProps, ref) {
}
// Use context isDisabled if prop isDisabled is not explicitly set
- let effectiveIsDisabled = isDisabled ?? contextIsDisabled ?? false;
+ let baseIsDisabled = isDisabled ?? contextIsDisabled ?? false;
+
+ // Loading has to disable the radio here, not inside `Item`. `useRadio` and the
+ // `HiddenInput` covering the button own the actual selection, so leaving it to
+ // `Item` would render a spinner on an option that still takes clicks and arrow
+ // keys. And because this component always passes a resolved `isDisabled` down,
+ // an unresolved `isLoading` would additionally read to `Item` as an explicit
+ // `isDisabled={false}` and cancel its own loading-disables-the-item rule.
+ //
+ // Same precedence as `Item` / `ItemButton`: an explicit `isDisabled={false}`
+ // still wins over loading. Button-type only — a classic radio has no spinner,
+ // so disabling it on `isLoading` would leave no visible reason why.
+ let isLoadingButton = isButton && props.isLoading === true;
+ let effectiveIsDisabled =
+ baseIsDisabled === true || (isLoadingButton && isDisabled !== false);
let { isFocused, focusProps } = useFocus(
{ isDisabled: effectiveIsDisabled },
@@ -339,16 +417,13 @@ function Radio(props: CubeRadioProps, ref) {
{ includeValid: true },
)}
size={effectiveSize}
- icon={icon}
- rightIcon={rightIcon}
- prefix={prefix}
- suffix={suffix}
- description={description}
- tooltip={tooltip}
- hotkeys={hotkeys}
+ {...itemProps}
isSelected={isRadioSelected}
isDisabled={isRadioDisabled}
- mods={{ ...mods, ...(forceSelectedMod ? { selected: true } : {}) }}
+ mods={{
+ ...mods,
+ ...(forceSelectedMod && !isRadioDisabled ? { selected: true } : {}),
+ }}
styles={styles}
{...mergeProps(hoverProps, focusProps)}
>
diff --git a/src/components/fields/RadioGroup/RadioGroup.docs.mdx b/src/components/fields/RadioGroup/RadioGroup.docs.mdx
index 651c80983..5ee72f58c 100644
--- a/src/components/fields/RadioGroup/RadioGroup.docs.mdx
+++ b/src/components/fields/RadioGroup/RadioGroup.docs.mdx
@@ -35,6 +35,32 @@ A radio group allows users to select exactly one option from a set of mutually e
- **`onBlur`** `function` — Callback fired when the radio group loses focus
- **`onFocus`** `function` — Callback fired when the radio group receives focus
+### Radio Properties
+
+Each option is a `Radio` (also exported as `Radio.Button`).
+
+- **`value`** `string` — The value this option contributes to the group
+- **`children`** — The option label
+- **`isDisabled`** `boolean` — Whether this option is disabled. Falls back to the group's `isDisabled`
+- **`type`** `'radio' | 'button'` — Overrides the group's visual type for this option
+- **`size`**, **`buttonType`** — Override the group's `size` / `buttonType` for this option
+
+A `button`- or `tabs`-type radio renders an [Item](/docs/content-item--docs), so it takes the same content props an [ItemButton](/docs/actions-itembutton--docs) does. These are ignored by a classic radio:
+
+- **`icon`**, **`rightIcon`** — Icons before / after the label
+- **`prefix`**, **`suffix`** — Content before / after the label
+- **`description`**, **`descriptionPlacement`**, **`descriptionProps`** — Secondary line, placed `inline` (default) or `block`
+- **`tooltip`** — Tooltip content, or `true` to show one only when the label is truncated
+- **`hotkeys`**, **`keyboardShortcutProps`** — Keyboard shortcut that selects the option, rendered as a hint
+- **`isLoading`**, **`loadingSlot`** — Replaces a slot with a spinner and disables the option
+- **`highlight`**, **`highlightCaseSensitive`**, **`highlightStyles`** — Highlights a substring of the label
+- **`level`** — Heading level for the label when `buttonType` is `card` or `header`
+- **`labelRef`** — Ref to the label element
+
+Container [style properties](#style-properties) also apply to button-type radios (they style the `Item`). A classic radio only reads the outer subset — `margin`, `width`, `height`, `flex*`, `grid*`, `place*`, `position`, `inset`, `zIndex`, `border`, `radius`, `shadow`, `outline`.
+
+`shape` and `actions` are not forwarded: the corner radius is owned by the group layout, which keeps only the outer-side radius on the first and last option so the group reads as one continuous control, and a button radio renders as a ``, where a click on a nested action button would select the option.
+
### Base Properties
Supports [Base properties](/docs/getting-started-base-properties--docs)
@@ -166,6 +192,18 @@ When using `type="button"`, you can customize the button appearance:
```
+### Rich Button Content
+
+
+
+```jsx
+
+ } description="Frankfurt">EU
+ } description="Oregon" hotkeys="alt+2">US
+ } isLoading>AP
+
+```
+
### Custom Button Type
```jsx
diff --git a/src/components/fields/RadioGroup/RadioGroup.stories.tsx b/src/components/fields/RadioGroup/RadioGroup.stories.tsx
index e02f060d0..dc21f6c51 100644
--- a/src/components/fields/RadioGroup/RadioGroup.stories.tsx
+++ b/src/components/fields/RadioGroup/RadioGroup.stories.tsx
@@ -210,6 +210,66 @@ export const Stretched: StoryFn = (args) => (
);
+// A button radio renders an `Item`, so it takes the same content props an
+// `ItemButton` does.
+export const ButtonGroupRichContent: StoryFn = () => (
+
+
+ Icons, prefix and suffix
+
+ } suffix="9">
+ Yes
+
+ }>
+ No
+
+ } />
+
+
+
+ Descriptions
+
+
+ Yes
+
+
+ No
+
+
+
+
+ Yes
+
+
+ No
+
+
+
+
+ Hotkeys and loading
+
+
+ Yes
+
+
+ No
+
+
+
+
+ Container style props
+
+
+ Yes
+
+
+ No
+
+
+
+
+);
+
// Size demonstrations
export const ButtonGroupSizes: StoryFn = () => (
@@ -409,5 +469,73 @@ export const DisabledState: StoryFn = () => (
Maybe
+ {/* A disabled group still has to show which option is selected — every
+ button type carries that through its own muted register. */}
+
+ Button Types (Disabled, "Yes" selected)
+
+ Yes
+ No
+ Maybe
+
+
+
+ Yes
+ No
+ Maybe
+
+
+
+ Yes
+ No
+ Maybe
+
+
+ Yes
+ No
+ Maybe
+
+
+
+ Validation Themes (Disabled, "Yes" selected)
+
+ Yes
+ No
+
+
+ Yes
+ No
+
+
);
diff --git a/src/components/fields/RadioGroup/radio.test.tsx b/src/components/fields/RadioGroup/radio.test.tsx
index 702c1ca68..7e4f3a48b 100644
--- a/src/components/fields/RadioGroup/radio.test.tsx
+++ b/src/components/fields/RadioGroup/radio.test.tsx
@@ -40,6 +40,58 @@ describe(' and ', () => {
expect(formInstance.getFieldValue('test')).toBe('test');
});
+ it('should not select a loading button radio', async () => {
+ const { getAllByRole } = renderWithRoot(
+
+ test
+
+ test2
+
+ ,
+ );
+ const radio = getAllByRole('radio');
+
+ expect(radio[1]).toBeDisabled();
+
+ await act(async () => await userEvent.click(radio[1]));
+
+ expect(radio[1]).not.toBeChecked();
+ });
+
+ it('should keep a loading button radio selectable with an explicit isDisabled={false}', async () => {
+ const { getAllByRole } = renderWithRoot(
+
+ test
+
+ test2
+
+ ,
+ );
+ const radio = getAllByRole('radio');
+
+ expect(radio[1]).not.toBeDisabled();
+
+ await act(async () => await userEvent.click(radio[1]));
+
+ expect(radio[1]).toBeChecked();
+ });
+
+ it('should leave a classic radio selectable while loading', async () => {
+ const { getAllByRole } = renderWithRoot(
+
+ test
+
+ test2
+
+ ,
+ );
+ const radio = getAllByRole('radio');
+
+ await act(async () => await userEvent.click(radio[1]));
+
+ expect(radio[1]).toBeChecked();
+ });
+
it("Radio shouldn't work without ", () => {
const inst = vi.spyOn(console, 'error').mockImplementation(() => {});
diff --git a/src/data/item-themes.ts b/src/data/item-themes.ts
index 88ca941f3..516a60427 100644
--- a/src/data/item-themes.ts
+++ b/src/data/item-themes.ts
@@ -128,7 +128,9 @@ export const DEFAULT_OUTLINE_STYLES: Styles = {
// body-composited single-layer `#primary-accent-surface.X` would be, but
// visually almost identical and free of the overlay-snap flash on click).
// Disabled paints the neutral `#disabled-surface` chip on top of the same
- // base.
+ // base — or, when selected, the brand-tinted `accent-disabled-surface` one, so
+ // a disabled segmented control still shows which option is active (CUB-3912).
+ // Every `selected & disabled` entry in this file exists for that reason.
fill: {
'': '#surface-2 #surface-text.0',
hovered: '#surface-2 #surface-text.03',
@@ -137,6 +139,7 @@ export const DEFAULT_OUTLINE_STYLES: Styles = {
'selected & (hovered | focused)': '#surface-2 #primary-accent-surface.12',
'selected & pressed': '#surface-2 #primary-accent-surface.18',
disabled: '#surface-2 #disabled-surface',
+ 'selected & disabled': '#surface-2 #primary-accent-disabled-surface',
},
color: {
'': '#surface-text-soft',
@@ -145,6 +148,7 @@ export const DEFAULT_OUTLINE_STYLES: Styles = {
selected: '#primary-accent-text-soft',
'selected & hovered': '#primary-accent-text',
disabled: '#disabled-surface-text',
+ 'selected & disabled': '#primary-accent-disabled-surface-text',
},
} as const;
@@ -160,6 +164,7 @@ export const DEFAULT_OUTLINE_2_STYLES: Styles = {
'selected & (hovered | focused)': '#surface-3 #primary-accent-surface.12',
'selected & pressed': '#surface-3 #primary-accent-surface.15',
disabled: '#surface-3 #disabled-surface',
+ 'selected & disabled': '#surface-3 #primary-accent-disabled-surface',
},
} as const;
@@ -182,6 +187,7 @@ export const DEFAULT_CLEAR_STYLES: Styles = {
'selected & (hovered | focused)': '#primary-accent-surface.12',
'selected & pressed': '#primary-accent-surface.18',
disabled: 'transparent',
+ 'selected & disabled': '#primary-accent-disabled-surface',
},
// Selected label mirrors LINK: soft at rest, `#primary-accent-text` on
// hover. See DEFAULT_OUTLINE_STYLES.
@@ -192,6 +198,7 @@ export const DEFAULT_CLEAR_STYLES: Styles = {
selected: '#primary-accent-text-soft',
'selected & hovered': '#primary-accent-text',
disabled: '#disabled-surface-text',
+ 'selected & disabled': '#primary-accent-disabled-surface-text',
},
} as const;
@@ -281,11 +288,13 @@ export const DANGER_OUTLINE_STYLES: Styles = {
'selected & (hovered | focused)': '#surface-2 #danger-accent-surface.12',
'selected & pressed': '#surface-2 #danger-accent-surface.18',
disabled: '#surface-2 #disabled-surface',
+ 'selected & disabled': '#surface-2 #danger-accent-disabled-surface',
},
color: {
'': '#danger-accent-text-soft',
selected: '#danger-accent-text',
disabled: '#disabled-surface-text',
+ 'selected & disabled': '#danger-accent-disabled-surface-text',
},
} as const;
@@ -299,6 +308,7 @@ export const DANGER_OUTLINE_2_STYLES: Styles = {
'selected & (hovered | focused)': '#surface-3 #danger-accent-surface.12',
'selected & pressed': '#surface-3 #danger-accent-surface.18',
disabled: '#surface-3 #disabled-surface',
+ 'selected & disabled': '#surface-3 #danger-accent-disabled-surface',
},
} as const;
@@ -321,11 +331,13 @@ export const DANGER_CLEAR_STYLES: Styles = {
'selected & (hovered | focused)': '#danger-accent-text.12',
'selected & pressed': '#danger-accent-text.18',
disabled: 'transparent',
+ 'selected & disabled': '#danger-accent-disabled-surface',
},
color: {
'': '#danger-accent-text-soft',
selected: '#danger-accent-text',
disabled: '#disabled-surface-text',
+ 'selected & disabled': '#danger-accent-disabled-surface-text',
},
} as const;
@@ -407,11 +419,13 @@ export const SUCCESS_OUTLINE_STYLES: Styles = {
'selected & (hovered | focused)': '#surface-2 #success-accent-surface.12',
'selected & pressed': '#surface-2 #success-accent-surface.18',
disabled: '#surface-2 #disabled-surface',
+ 'selected & disabled': '#surface-2 #success-accent-disabled-surface',
},
color: {
'': '#success-accent-text-soft',
selected: '#success-accent-text',
disabled: '#disabled-surface-text',
+ 'selected & disabled': '#success-accent-disabled-surface-text',
},
} as const;
@@ -425,6 +439,7 @@ export const SUCCESS_OUTLINE_2_STYLES: Styles = {
'selected & (hovered | focused)': '#surface-3 #success-accent-surface.12',
'selected & pressed': '#surface-3 #success-accent-surface.18',
disabled: '#surface-3 #disabled-surface',
+ 'selected & disabled': '#surface-3 #success-accent-disabled-surface',
},
} as const;
@@ -445,11 +460,13 @@ export const SUCCESS_CLEAR_STYLES: Styles = {
'selected & (hovered | focused)': '#success-accent-text.12',
'selected & pressed': '#success-accent-text.18',
disabled: 'transparent',
+ 'selected & disabled': '#success-accent-disabled-surface',
},
color: {
'': '#success-accent-text-soft',
selected: '#success-accent-text',
disabled: '#disabled-surface-text',
+ 'selected & disabled': '#success-accent-disabled-surface-text',
},
} as const;
@@ -531,11 +548,13 @@ export const WARNING_OUTLINE_STYLES: Styles = {
'selected & (hovered | focused)': '#surface-2 #warning-accent-surface.12',
'selected & pressed': '#surface-2 #warning-accent-surface.18',
disabled: '#surface-2 #disabled-surface',
+ 'selected & disabled': '#surface-2 #warning-accent-disabled-surface',
},
color: {
'': '#warning-accent-text-soft',
selected: '#warning-accent-text',
disabled: '#disabled-surface-text',
+ 'selected & disabled': '#warning-accent-disabled-surface-text',
},
} as const;
@@ -549,6 +568,7 @@ export const WARNING_OUTLINE_2_STYLES: Styles = {
'selected & (hovered | focused)': '#surface-3 #warning-accent-surface.12',
'selected & pressed': '#surface-3 #warning-accent-surface.18',
disabled: '#surface-3 #disabled-surface',
+ 'selected & disabled': '#surface-3 #warning-accent-disabled-surface',
},
} as const;
@@ -569,11 +589,13 @@ export const WARNING_CLEAR_STYLES: Styles = {
'selected & (hovered | focused)': '#warning-accent-text.12',
'selected & pressed': '#warning-accent-text.18',
disabled: 'transparent',
+ 'selected & disabled': '#warning-accent-disabled-surface',
},
color: {
'': '#warning-accent-text-soft',
selected: '#warning-accent-text',
disabled: '#disabled-surface-text',
+ 'selected & disabled': '#warning-accent-disabled-surface-text',
},
} as const;
@@ -655,11 +677,13 @@ export const NOTE_OUTLINE_STYLES: Styles = {
'selected & (hovered | focused)': '#surface-2 #note-accent-surface.12',
'selected & pressed': '#surface-2 #note-accent-surface.18',
disabled: '#surface-2 #disabled-surface',
+ 'selected & disabled': '#surface-2 #note-accent-disabled-surface',
},
color: {
'': '#note-accent-text-soft',
selected: '#note-accent-text',
disabled: '#disabled-surface-text',
+ 'selected & disabled': '#note-accent-disabled-surface-text',
},
} as const;
@@ -673,6 +697,7 @@ export const NOTE_OUTLINE_2_STYLES: Styles = {
'selected & (hovered | focused)': '#surface-3 #note-accent-surface.12',
'selected & pressed': '#surface-3 #note-accent-surface.18',
disabled: '#surface-3 #disabled-surface',
+ 'selected & disabled': '#surface-3 #note-accent-disabled-surface',
},
} as const;
@@ -693,11 +718,13 @@ export const NOTE_CLEAR_STYLES: Styles = {
'selected & (hovered | focused)': '#note-accent-text.12',
'selected & pressed': '#note-accent-text.18',
disabled: 'transparent',
+ 'selected & disabled': '#note-accent-disabled-surface',
},
color: {
'': '#note-accent-text-soft',
selected: '#note-accent-text',
disabled: '#disabled-surface-text',
+ 'selected & disabled': '#note-accent-disabled-surface-text',
},
} as const;
@@ -811,6 +838,8 @@ export const SPECIAL_OUTLINE_STYLES: Styles = {
'selected & (hovered | focused)': '#special-surface #white.24',
'selected & pressed': '#special-surface #white.3',
disabled: '#special-surface #white.04',
+ // Stays in the white-alpha register: the base here is a fixed dark tone.
+ 'selected & disabled': '#special-surface #white.09',
},
// Mirrors the colored-theme soft→opaque pattern (`*-accent-text-soft` →
// `*-accent-text`) using white-alpha steps: default is slightly muted so
@@ -819,6 +848,7 @@ export const SPECIAL_OUTLINE_STYLES: Styles = {
'': '#white.8',
selected: '#white',
disabled: '#white.4',
+ 'selected & disabled': '#white.55',
},
} as const;
@@ -986,6 +1016,7 @@ export const CURRENT_BUTTON_STYLES: Styles = {
selected: '#current.3',
'selected & pressed': '#current.4',
disabled: '#current.12',
+ 'selected & disabled': '#current.35',
},
fill: {
'': '#current.03',
@@ -995,6 +1026,10 @@ export const CURRENT_BUTTON_STYLES: Styles = {
'selected & (hovered | focused)': '#current.16',
'selected & pressed': '#current.2',
disabled: '#current.06',
+ // Alpha step rather than a brand token — `current` inherits `currentcolor`.
+ // Authored high because the disabled label already dims `currentcolor` to
+ // `.4`, and these alphas resolve against it: `.18` renders as ~.07.
+ 'selected & disabled': '#current.18',
},
color: {
'': '#current',