diff --git a/packages/sdk/src/features/json-form/controls/dynamic-conditions-control/dynamic-conditions-control.tsx b/packages/sdk/src/features/json-form/controls/dynamic-conditions-control/dynamic-conditions-control.tsx
index 99beb7c6f..4dcc0855c 100644
--- a/packages/sdk/src/features/json-form/controls/dynamic-conditions-control/dynamic-conditions-control.tsx
+++ b/packages/sdk/src/features/json-form/controls/dynamic-conditions-control/dynamic-conditions-control.tsx
@@ -44,9 +44,14 @@ function DynamicConditionsControl(props: DynamicConditionsControlProps) {
{t('title')}
-
-
-
+ }
+ />
diff --git a/packages/sdk/src/features/json-form/controls/dynamic-conditions-control/dynamic-conditions-form-field/conditions-form-field.tsx b/packages/sdk/src/features/json-form/controls/dynamic-conditions-control/dynamic-conditions-form-field/conditions-form-field.tsx
index b79c8153e..068936934 100644
--- a/packages/sdk/src/features/json-form/controls/dynamic-conditions-control/dynamic-conditions-form-field/conditions-form-field.tsx
+++ b/packages/sdk/src/features/json-form/controls/dynamic-conditions-control/dynamic-conditions-form-field/conditions-form-field.tsx
@@ -91,9 +91,12 @@ export function ConditionsFormField(props: ConditionsFormFieldProps) {
[styles['container-error']]: shouldShowValidation && (!condition.x || !condition.y),
})}
>
-
{}} tooltip={t('tooltips.menu')}>
-
-
+
{}}
+ tooltip={t('tooltips.menu')}
+ prefixIcon={}
+ />
-
-
-
+ }
+ />
>
);
diff --git a/packages/sdk/src/features/palette/components/header/palette-header.tsx b/packages/sdk/src/features/palette/components/header/palette-header.tsx
index a4ad59477..f6fc7ccae 100644
--- a/packages/sdk/src/features/palette/components/header/palette-header.tsx
+++ b/packages/sdk/src/features/palette/components/header/palette-header.tsx
@@ -17,12 +17,12 @@ export function PaletteHeader({ onClick, isSidebarExpanded }: PaletteHeaderProps
{t('palette.nodesLibrary')}
-
-
+ prefixIcon={}
+ />
);
}
diff --git a/packages/sdk/src/features/properties-bar/components/header/properties-bar-header.tsx b/packages/sdk/src/features/properties-bar/components/header/properties-bar-header.tsx
index 059c7a0e9..3fb0a2c23 100644
--- a/packages/sdk/src/features/properties-bar/components/header/properties-bar-header.tsx
+++ b/packages/sdk/src/features/properties-bar/components/header/properties-bar-header.tsx
@@ -27,21 +27,24 @@ export function PropertiesBarHeader({
return (
-
-
+ prefixIcon={
}
+ />
{header}
{name &&
{name}
}
{onDotsClick && (
-
-
-
+
}
+ />
)}
);
diff --git a/packages/sdk/src/features/variables/components/dynamic-typed-variable-or-input/dynamic-typed-variable-or-input.tsx b/packages/sdk/src/features/variables/components/dynamic-typed-variable-or-input/dynamic-typed-variable-or-input.tsx
index d079c1ccb..68a34809d 100644
--- a/packages/sdk/src/features/variables/components/dynamic-typed-variable-or-input/dynamic-typed-variable-or-input.tsx
+++ b/packages/sdk/src/features/variables/components/dynamic-typed-variable-or-input/dynamic-typed-variable-or-input.tsx
@@ -60,9 +60,13 @@ export function DynamicTypedVariableOrInput({
suggestionGroups={suggestionGroupsForType}
hasError={isError}
endAdornment={
-
-
-
+
}
+ />
}
/>
);
@@ -80,12 +84,12 @@ export function DynamicTypedVariableOrInput({
endAdornment={
suggestionGroupsForType.length > 0 ? (
-
-
+ prefixIcon={
}
+ />
) : undefined
}
/>
diff --git a/packages/sdk/src/features/variables/components/variable-text/variable-text.tsx b/packages/sdk/src/features/variables/components/variable-text/variable-text.tsx
index 8acc7b417..98f9df18f 100644
--- a/packages/sdk/src/features/variables/components/variable-text/variable-text.tsx
+++ b/packages/sdk/src/features/variables/components/variable-text/variable-text.tsx
@@ -149,13 +149,13 @@ function SuggestionsContainer({
{title}
{
event.stopPropagation();
handleClose();
}}
- >
-
-
+ prefixIcon={}
+ />
{cloneElement(ul, {}, grouped)}
diff --git a/packages/sdk/src/features/variables/modals/settings/settings-navigation.module.css b/packages/sdk/src/features/variables/modals/settings/settings-navigation.module.css
index dfd4f4f03..fb4342c84 100644
--- a/packages/sdk/src/features/variables/modals/settings/settings-navigation.module.css
+++ b/packages/sdk/src/features/variables/modals/settings/settings-navigation.module.css
@@ -36,7 +36,7 @@
&:hover,
&.button--active {
- background: var(--ax-public-button-nav-background-color);
+ background: var(--ax-public-nav-button-background-color-hover);
}
&.button--active {
diff --git a/packages/sdk/src/features/variables/modals/tab-global-variables/variable-preview/variable-preview.tsx b/packages/sdk/src/features/variables/modals/tab-global-variables/variable-preview/variable-preview.tsx
index a872a1f5c..b4c76fab5 100644
--- a/packages/sdk/src/features/variables/modals/tab-global-variables/variable-preview/variable-preview.tsx
+++ b/packages/sdk/src/features/variables/modals/tab-global-variables/variable-preview/variable-preview.tsx
@@ -29,14 +29,22 @@ export function VariablePreview({ id, onEdit, onRemove }: Props) {
{onGoBack && (
-
-
-
+ }
+ />
)}
{translateIfPossible(title) || title}
diff --git a/packages/ui/src/components/button/guards.spec.ts b/packages/ui/src/components/button/guards.spec.ts
deleted file mode 100644
index a9738caf1..000000000
--- a/packages/ui/src/components/button/guards.spec.ts
+++ /dev/null
@@ -1,85 +0,0 @@
-import { type ReactNode, createElement } from 'react';
-
-import { hasChildrenWithStringAndIcons, hasIconChildrenOnly, hasStringChildrenOnly } from './guards';
-
-const icon = createElement('span', null, 'icon');
-const otherIcon = createElement('svg');
-
-describe('hasIconChildrenOnly', () => {
- it('returns true for a single element child', () => {
- expect(hasIconChildrenOnly({ children: icon })).toBe(true);
- });
-
- it('returns true for a function child', () => {
- // A render-prop function is not a valid `ReactNode`, but the guard
- // accepts it at runtime via a `typeof === 'function'` check.
- const functionChild = (() => null) as unknown as ReactNode;
- expect(hasIconChildrenOnly({ children: functionChild })).toBe(true);
- });
-
- it('returns false for a string child', () => {
- expect(hasIconChildrenOnly({ children: 'label' })).toBe(false);
- });
-
- it('returns false for an array of children', () => {
- expect(hasIconChildrenOnly({ children: ['label', icon] })).toBe(false);
- });
-
- it('returns false for number/boolean/null children', () => {
- expect(hasIconChildrenOnly({ children: 42 })).toBe(false);
- expect(hasIconChildrenOnly({ children: true })).toBe(false);
- expect(hasIconChildrenOnly({ children: null })).toBe(false);
- });
-});
-
-describe('hasChildrenWithStringAndIcons', () => {
- it('returns true for [string, element]', () => {
- expect(hasChildrenWithStringAndIcons({ children: ['label', icon] })).toBe(true);
- });
-
- it('returns true for [element, string]', () => {
- expect(hasChildrenWithStringAndIcons({ children: [icon, 'label'] })).toBe(true);
- });
-
- it('returns true for two elements', () => {
- expect(hasChildrenWithStringAndIcons({ children: [icon, otherIcon] })).toBe(true);
- });
-
- it('returns false for an array shorter than 2', () => {
- expect(hasChildrenWithStringAndIcons({ children: [icon] })).toBe(false);
- });
-
- it('returns false for a single string child (not an array)', () => {
- expect(hasChildrenWithStringAndIcons({ children: 'label' })).toBe(false);
- });
-
- it('returns false for a single element child (not an array)', () => {
- expect(hasChildrenWithStringAndIcons({ children: icon })).toBe(false);
- });
-
- it('returns false when an entry is number/boolean/null', () => {
- expect(hasChildrenWithStringAndIcons({ children: [42, icon] })).toBe(false);
- expect(hasChildrenWithStringAndIcons({ children: [icon, null] })).toBe(false);
- expect(hasChildrenWithStringAndIcons({ children: [true, false] })).toBe(false);
- });
-});
-
-describe('hasStringChildrenOnly', () => {
- it('returns true for a string child', () => {
- expect(hasStringChildrenOnly({ children: 'label' })).toBe(true);
- });
-
- it('returns false for an element child', () => {
- expect(hasStringChildrenOnly({ children: icon })).toBe(false);
- });
-
- it('returns false for an array child', () => {
- expect(hasStringChildrenOnly({ children: ['label', icon] })).toBe(false);
- });
-
- it('returns false for number/boolean/null children', () => {
- expect(hasStringChildrenOnly({ children: 42 })).toBe(false);
- expect(hasStringChildrenOnly({ children: true })).toBe(false);
- expect(hasStringChildrenOnly({ children: null })).toBe(false);
- });
-});
diff --git a/packages/ui/src/components/button/guards.ts b/packages/ui/src/components/button/guards.ts
deleted file mode 100644
index 6ba289aa2..000000000
--- a/packages/ui/src/components/button/guards.ts
+++ /dev/null
@@ -1,18 +0,0 @@
-import { PropsWithChildren, isValidElement } from 'react';
-
-export function hasIconChildrenOnly(props: PropsWithChildren): props is PropsWithChildren {
- return isValidElement(props.children) || typeof props.children === 'function';
-}
-
-export function hasChildrenWithStringAndIcons(props: PropsWithChildren): props is PropsWithChildren {
- return (
- Array.isArray(props.children) &&
- props.children.length >= 2 &&
- (typeof props.children[0] === 'string' || isValidElement(props.children[0])) &&
- (typeof props.children[1] === 'string' || isValidElement(props.children[1]))
- );
-}
-
-export function hasStringChildrenOnly(props: PropsWithChildren): props is PropsWithChildren {
- return typeof (props as PropsWithChildren).children === 'string';
-}
diff --git a/packages/ui/src/components/button/index.ts b/packages/ui/src/components/button/index.ts
index bf89cdc25..dfa1db191 100644
--- a/packages/ui/src/components/button/index.ts
+++ b/packages/ui/src/components/button/index.ts
@@ -1,3 +1,4 @@
+export * from './menu-trigger-button';
export * from './nav-button/nav-button';
export * from './nav-button/types';
export * from './regular-button/button';
diff --git a/packages/ui/src/components/button/menu-trigger-button/index.ts b/packages/ui/src/components/button/menu-trigger-button/index.ts
new file mode 100644
index 000000000..a546b3652
--- /dev/null
+++ b/packages/ui/src/components/button/menu-trigger-button/index.ts
@@ -0,0 +1 @@
+export * from './menu-trigger-button';
diff --git a/packages/ui/src/components/button/menu-trigger-button/menu-trigger-button.tsx b/packages/ui/src/components/button/menu-trigger-button/menu-trigger-button.tsx
new file mode 100644
index 000000000..a5ce99843
--- /dev/null
+++ b/packages/ui/src/components/button/menu-trigger-button/menu-trigger-button.tsx
@@ -0,0 +1,15 @@
+import { forwardRef } from 'react';
+import type { ButtonHTMLAttributes, ReactNode } from 'react';
+
+import { NavButton } from '../nav-button/nav-button';
+
+export type MenuTriggerButtonProps = {
+ children: ReactNode;
+ isOpen?: boolean;
+} & ButtonHTMLAttributes;
+
+export const MenuTriggerButton = forwardRef(
+ ({ children, isOpen = false, ...props }, ref) => (
+
+ ),
+);
diff --git a/packages/ui/src/components/button/nav-button/nav-button.module.css b/packages/ui/src/components/button/nav-button/nav-button.module.css
new file mode 100644
index 000000000..a07fff7e4
--- /dev/null
+++ b/packages/ui/src/components/button/nav-button/nav-button.module.css
@@ -0,0 +1,202 @@
+:root {
+ --ax-public-nav-button-background-color-default: var(--wb-components-nav-button-bg-primary-default);
+ --ax-public-nav-button-background-color-hover: var(--wb-components-nav-button-bg-primary-hover);
+ --ax-public-nav-button-background-color-pressed: var(--wb-components-nav-button-bg-primary-pressed);
+ --ax-public-nav-button-background-color-active: var(--wb-components-nav-button-bg-primary-active);
+ --ax-public-nav-button-background-color-focus: var(--wb-components-nav-button-bg-primary-focus);
+ --ax-public-nav-button-background-color-disabled: var(--wb-components-nav-button-bg-primary-disabled);
+ --ax-public-nav-button-color-default: var(--wb-components-nav-button-icon-primary-default);
+ --ax-public-nav-button-color-hover: var(--wb-components-nav-button-icon-primary-hover);
+ --ax-public-nav-button-color-pressed: var(--wb-components-nav-button-icon-primary-pressed);
+ --ax-public-nav-button-color-active: var(--wb-components-nav-button-icon-primary-active);
+ --ax-public-nav-button-color-disabled: var(--wb-components-nav-button-icon-primary-disabled);
+ --ax-public-nav-button-height-xl: var(--wb-size-600);
+ --ax-public-nav-button-height-l: calc(var(--wb-size-500) + var(--wb-size-25));
+ --ax-public-nav-button-height-m: calc(var(--wb-size-400) + var(--wb-size-50));
+ --ax-public-nav-button-height-s: var(--wb-size-400);
+ --ax-public-nav-button-height-xs: calc(var(--wb-size-300) + var(--wb-size-50));
+ --ax-public-nav-button-height-xxs: var(--wb-size-300);
+ --ax-public-nav-button-height-xxxs: calc(var(--wb-size-200) + var(--wb-size-50));
+ --ax-public-nav-button-padding-xl: var(--wb-space-150);
+ --ax-public-nav-button-padding-l: var(--wb-space-137);
+ --ax-public-nav-button-padding-m: var(--wb-space-112);
+ --ax-public-nav-button-padding-s: var(--wb-space-87);
+ --ax-public-nav-button-padding-xs: var(--wb-space-75);
+ --ax-public-nav-button-padding-xxs: var(--wb-space-75);
+ --ax-public-nav-button-padding-xxxs: var(--wb-space-50);
+ --ax-public-nav-button-label-padding-inline-xl: var(--wb-space-150);
+ --ax-public-nav-button-label-padding-inline-l: var(--wb-space-150);
+ --ax-public-nav-button-label-padding-inline-m: var(--wb-space-112);
+ --ax-public-nav-button-label-padding-inline-s: var(--wb-space-87);
+ --ax-public-nav-button-label-padding-inline-xs: var(--wb-space-75);
+ --ax-public-nav-button-label-padding-inline-xxs: var(--wb-space-75);
+ --ax-public-nav-button-label-padding-inline-xxxs: var(--wb-space-50);
+ --ax-public-nav-button-gap-xl: var(--wb-space-100);
+ --ax-public-nav-button-gap-l: var(--wb-space-100);
+ --ax-public-nav-button-gap-m: var(--wb-space-100);
+ --ax-public-nav-button-gap-s: var(--wb-space-100);
+ --ax-public-nav-button-gap-xs: var(--wb-space-100);
+ --ax-public-nav-button-gap-xxs: var(--wb-space-75);
+ --ax-public-nav-button-gap-xxxs: var(--wb-space-75);
+ --ax-public-nav-button-border-radius-xl: var(--wb-radius-100);
+ --ax-public-nav-button-border-radius-l: var(--wb-radius-100);
+ --ax-public-nav-button-border-radius-m: var(--wb-radius-75);
+ --ax-public-nav-button-border-radius-s: var(--wb-radius-50);
+ --ax-public-nav-button-border-radius-xs: var(--wb-radius-50);
+ --ax-public-nav-button-border-radius-xxs: var(--wb-radius-50);
+ --ax-public-nav-button-border-radius-xxxs: var(--wb-radius-50);
+ --ax-public-nav-button-border-radius-round: var(--wb-radius-full);
+ --ax-public-nav-button-icon-size-xl: var(--wb-size-300);
+ --ax-public-nav-button-icon-size-l: calc(var(--wb-size-200) + var(--wb-size-50));
+ --ax-public-nav-button-icon-size-m: calc(var(--wb-size-200) + var(--wb-size-25));
+ --ax-public-nav-button-icon-size-s: calc(var(--wb-size-200) + var(--wb-size-25));
+ --ax-public-nav-button-icon-size-xs: var(--wb-size-200);
+ --ax-public-nav-button-icon-size-xxs: var(--wb-size-150);
+ --ax-public-nav-button-icon-size-xxxs: var(--wb-size-150);
+}
+
+@layer ui.component {
+ .nav-button {
+ box-sizing: border-box;
+ height: var(--nav-button-height);
+ padding-block: var(--nav-button-padding);
+ padding-inline: var(--nav-button-label-padding-inline);
+ gap: var(--nav-button-gap);
+ border-radius: var(--nav-button-border-radius);
+ background-color: var(--nav-button-background, var(--ax-public-nav-button-background-color-default));
+ color: var(--ax-public-nav-button-color-default);
+
+ &:not(:disabled):hover {
+ background-color: var(--ax-public-nav-button-background-color-hover);
+ color: var(--ax-public-nav-button-color-hover);
+ }
+
+ &.selected:not(:disabled) {
+ background-color: var(--ax-public-nav-button-background-color-pressed);
+ color: var(--ax-public-nav-button-color-pressed);
+ }
+
+ &:not(:disabled):focus-visible {
+ background-color: var(--ax-public-nav-button-background-color-focus);
+ color: var(--ax-public-nav-button-color-default);
+ }
+
+ &:not(:disabled):active {
+ background-color: var(--ax-public-nav-button-background-color-active);
+ color: var(--ax-public-nav-button-color-active);
+ }
+
+ &:disabled {
+ background-color: var(--ax-public-nav-button-background-color-disabled);
+ color: var(--ax-public-nav-button-color-disabled);
+ }
+ }
+
+ .icon-only {
+ width: var(--nav-button-height);
+ padding: var(--nav-button-padding);
+ }
+
+ .icon {
+ display: flex;
+ flex: none;
+ align-items: center;
+ justify-content: center;
+ width: var(--nav-button-icon-size);
+ height: var(--nav-button-icon-size);
+
+ svg {
+ width: 100%;
+ height: 100%;
+ }
+ }
+
+ .xl {
+ --nav-button-height: var(--ax-public-nav-button-height-xl);
+ --nav-button-padding: var(--ax-public-nav-button-padding-xl);
+ --nav-button-label-padding-inline: var(--ax-public-nav-button-label-padding-inline-xl);
+ --nav-button-gap: var(--ax-public-nav-button-gap-xl);
+ --nav-button-border-radius: var(--ax-public-nav-button-border-radius-xl);
+ --nav-button-icon-size: var(--ax-public-nav-button-icon-size-xl);
+
+ composes: wb-text-label-xl from global;
+ }
+
+ .l {
+ --nav-button-height: var(--ax-public-nav-button-height-l);
+ --nav-button-padding: var(--ax-public-nav-button-padding-l);
+ --nav-button-label-padding-inline: var(--ax-public-nav-button-label-padding-inline-l);
+ --nav-button-gap: var(--ax-public-nav-button-gap-l);
+ --nav-button-border-radius: var(--ax-public-nav-button-border-radius-l);
+ --nav-button-icon-size: var(--ax-public-nav-button-icon-size-l);
+
+ composes: wb-text-label-l from global;
+ }
+
+ .m {
+ --nav-button-height: var(--ax-public-nav-button-height-m);
+ --nav-button-padding: var(--ax-public-nav-button-padding-m);
+ --nav-button-label-padding-inline: var(--ax-public-nav-button-label-padding-inline-m);
+ --nav-button-gap: var(--ax-public-nav-button-gap-m);
+ --nav-button-border-radius: var(--ax-public-nav-button-border-radius-m);
+ --nav-button-icon-size: var(--ax-public-nav-button-icon-size-m);
+
+ composes: wb-text-label-m from global;
+ }
+
+ .s {
+ --nav-button-height: var(--ax-public-nav-button-height-s);
+ --nav-button-padding: var(--ax-public-nav-button-padding-s);
+ --nav-button-label-padding-inline: var(--ax-public-nav-button-label-padding-inline-s);
+ --nav-button-gap: var(--ax-public-nav-button-gap-s);
+ --nav-button-border-radius: var(--ax-public-nav-button-border-radius-s);
+ --nav-button-icon-size: var(--ax-public-nav-button-icon-size-s);
+
+ composes: wb-text-label-m from global;
+ }
+
+ .xs {
+ --nav-button-height: var(--ax-public-nav-button-height-xs);
+ --nav-button-padding: var(--ax-public-nav-button-padding-xs);
+ --nav-button-label-padding-inline: var(--ax-public-nav-button-label-padding-inline-xs);
+ --nav-button-gap: var(--ax-public-nav-button-gap-xs);
+ --nav-button-border-radius: var(--ax-public-nav-button-border-radius-xs);
+ --nav-button-icon-size: var(--ax-public-nav-button-icon-size-xs);
+
+ composes: wb-text-label-m from global;
+ }
+
+ .xxs {
+ --nav-button-height: var(--ax-public-nav-button-height-xxs);
+ --nav-button-padding: var(--ax-public-nav-button-padding-xxs);
+ --nav-button-label-padding-inline: var(--ax-public-nav-button-label-padding-inline-xxs);
+ --nav-button-gap: var(--ax-public-nav-button-gap-xxs);
+ --nav-button-border-radius: var(--ax-public-nav-button-border-radius-xxs);
+ --nav-button-icon-size: var(--ax-public-nav-button-icon-size-xxs);
+
+ composes: wb-text-label-m from global;
+ }
+
+ .xxxs {
+ --nav-button-height: var(--ax-public-nav-button-height-xxxs);
+ --nav-button-padding: var(--ax-public-nav-button-padding-xxxs);
+ --nav-button-label-padding-inline: var(--ax-public-nav-button-label-padding-inline-xxxs);
+ --nav-button-gap: var(--ax-public-nav-button-gap-xxxs);
+ --nav-button-border-radius: var(--ax-public-nav-button-border-radius-xxxs);
+ --nav-button-icon-size: var(--ax-public-nav-button-icon-size-xxxs);
+
+ composes: wb-text-label-m from global;
+ }
+
+ .round {
+ --nav-button-border-radius: var(--ax-public-nav-button-border-radius-round);
+ }
+
+ .square {
+ --nav-button-background: var(--ax-public-nav-button-background-color-default);
+ }
+
+ .plain {
+ --nav-button-background: var(--wb-colors-transparent);
+ }
+}
diff --git a/packages/ui/src/components/button/nav-button/nav-button.tsx b/packages/ui/src/components/button/nav-button/nav-button.tsx
index 33807559d..dcac59143 100644
--- a/packages/ui/src/components/button/nav-button/nav-button.tsx
+++ b/packages/ui/src/components/button/nav-button/nav-button.tsx
@@ -1,111 +1,32 @@
import clsx from 'clsx';
-import { ReactElement, forwardRef } from 'react';
+import { forwardRef } from 'react';
-import borderRadiusStyles from '../styles/border-radius.module.css';
-import navBorderRadiusStyles from './styles/nav-button-border-radius.module.css';
-import navButtonStyles from './styles/nav-button.module.css';
+import styles from './nav-button.module.css';
-import { hasChildrenWithStringAndIcons, hasIconChildrenOnly, hasStringChildrenOnly } from '../guards';
-import { NavIconButton, NavIconButtonProps } from './nav-icon-button/nav-icon-button';
-import { NavIconLabelButton, NavIconLabelButtonProps } from './nav-icon-label-button/nav-icon-label-button';
-import { NavLabelButton, NavLabelButtonProps } from './nav-label-button/nav-label-button';
+import { BaseButton } from '../base-button/base-button';
+import type { NavButtonProps } from './types';
-type WithRef = T & {
- ref?: React.Ref;
-};
-
-/**
- * NavButtonProps defines **discriminated overloads** for the Button component using
- * **structural discrimination** rather than a `type` field.
- *
- * The component dynamically determines which button variant to render based on the
- * **structure of the `children` prop**:
- *
- * - If `children` is a single `string`, it's treated as a **Label Button**.
- * - If `children` is a single icon (ReactElement), it's treated as an **Icon Button**.
- * - If `children` includes both a string and one or two icons (before/after),
- * it's treated as an **Icon Label Button**.
- *
- * Based on the inferred variant, **only props specific to that variant are allowed**.
- * This ensures that incorrect prop combinations (e.g., passing label-specific props
- * to an Icon Button) are caught at compile time.
- *
- * This is intentionally implemented with **overloads** instead of a union type,
- * which would incorrectly allow mixing props between types and compromise type safety.
- */
-type NavButtonProps = {
- (props: WithRef): ReactElement;
- (props: WithRef): ReactElement;
- (props: WithRef): ReactElement;
-};
-
-const NavButtonComponent = forwardRef<
- HTMLButtonElement,
- NavLabelButtonProps | NavIconButtonProps | NavIconLabelButtonProps
->(({ className, isSelected, size = 'medium', ...props }, ref) => {
- const buttonProps = {
+export const NavButton = forwardRef(
+ (
+ { children, className, isSelected = false, prefixIcon, size = 'm', suffixIcon, variant = 'square', ...props },
ref,
- ...props,
- className: clsx(
- borderRadiusStyles[size],
- navBorderRadiusStyles[size],
- navButtonStyles['nav-button'],
- { [navButtonStyles['selected']]: isSelected },
- className,
- ),
- size,
- };
-
- if (hasStringChildrenOnly(props)) {
- return {props.children};
- }
-
- if (hasIconChildrenOnly(props)) {
- return {props.children};
- }
-
- if (hasChildrenWithStringAndIcons(props)) {
- return {props.children};
- }
-
- return null;
-});
-
-/**
- * Button is a flexible, and type-safe component that automatically selects
- * the correct type (Label Button, Icon Button, or Icon Label Button) based on the
- * structure of its `children` prop.
- *
- * **Automatic Type Selection (Structural Discrimination)**
- * The component uses the shape of `children` to infer which button variant to render:
- * - **Label Button**: If `children` is a single `string`
- * - **Icon Button**: If `children` is a single React element (e.g., an icon)
- * - **Icon Label Button**: If `children` is a combination of string + icon(s)
- *
- * **Type Safety via Overloads**
- * Each variant supports its own unique set of props. Thanks to TypeScript overloads,
- * only the correct props for a given structure are allowed—invalid combinations
- * are caught at compile time.
- *
- * **How to Use**
- *
- * ```tsx
- * Submit // Label Button
- *
- *
- *
- * // Icon Button
- *
- *
- *
- * Confirm
- *
- * // Icon Label Button
- * ```
- *
- * This approach ensures:
- * - Simplified usage with fewer props
- * - No accidental mixing of incompatible props
- * - Autocomplete and type-checking experience
- */
-export const NavButton = NavButtonComponent as NavButtonProps;
+ ) => {
+ const isIconOnly = children == null;
+
+ return (
+
+ {prefixIcon != null && {prefixIcon}}
+ {children != null && {children}}
+ {suffixIcon != null && {suffixIcon}}
+
+ );
+ },
+);
diff --git a/packages/ui/src/components/button/nav-button/nav-icon-button/nav-button-icon-padding.module.css b/packages/ui/src/components/button/nav-button/nav-icon-button/nav-button-icon-padding.module.css
deleted file mode 100644
index 8d0430719..000000000
--- a/packages/ui/src/components/button/nav-button/nav-icon-button/nav-button-icon-padding.module.css
+++ /dev/null
@@ -1,14 +0,0 @@
-:root {
- --ax-public-button-icon-padding-xx-small: var(--wb-space-75);
- --ax-public-button-icon-padding-xxx-small: var(--wb-space-50);
-}
-
-@layer ui.component {
- .xx-small {
- padding: var(--ax-public-button-icon-padding-xx-small);
- }
-
- .xxx-small {
- padding: var(--ax-public-button-icon-padding-xxx-small);
- }
-}
diff --git a/packages/ui/src/components/button/nav-button/nav-icon-button/nav-icon-button.module.css b/packages/ui/src/components/button/nav-button/nav-icon-button/nav-icon-button.module.css
deleted file mode 100644
index 17df928e7..000000000
--- a/packages/ui/src/components/button/nav-button/nav-icon-button/nav-icon-button.module.css
+++ /dev/null
@@ -1,11 +0,0 @@
-:root {
- --ax-public-button-nav-color-hover: var(--wb-components-nav-button-icon-primary-hover);
-}
-
-@layer ui.component {
- button:hover {
- &.transparent {
- background: transparent;
- }
- }
-}
diff --git a/packages/ui/src/components/button/nav-button/nav-icon-button/nav-icon-button.tsx b/packages/ui/src/components/button/nav-button/nav-icon-button/nav-icon-button.tsx
deleted file mode 100644
index e0dffbca3..000000000
--- a/packages/ui/src/components/button/nav-button/nav-icon-button/nav-icon-button.tsx
+++ /dev/null
@@ -1,40 +0,0 @@
-import clsx from 'clsx';
-import { forwardRef } from 'react';
-
-import borderRadiusStyles from '../../styles/border-radius.module.css';
-import iconPaddingStyles from '../../styles/icon-padding.module.css';
-import iconSizeStyles from '../../styles/icon-size.module.css';
-import navButtonBorderRadiusStyles from '../styles/nav-button-border-radius.module.css';
-import navButtonIconSizeStyles from '../styles/nav-button-icon-size.module.css';
-import navButtonIconPaddingStyles from './nav-button-icon-padding.module.css';
-import navIconButtonStyles from './nav-icon-button.module.css';
-
-import { BaseButton } from '../../base-button/base-button';
-import { IconNode, Shape } from '../../types';
-import { NavBaseButtonProps } from '../types';
-
-export type NavIconButtonProps = {
- shape?: Shape;
- transparent?: boolean;
- children: IconNode;
-} & NavBaseButtonProps;
-
-export const NavIconButton = forwardRef(
- ({ size = 'medium', shape = 'default', children, transparent, ...props }, ref) => (
-
- {children}
-
- ),
-);
diff --git a/packages/ui/src/components/button/nav-button/nav-icon-label-button/nav-button-gap.module.css b/packages/ui/src/components/button/nav-button/nav-icon-label-button/nav-button-gap.module.css
deleted file mode 100644
index 2197614db..000000000
--- a/packages/ui/src/components/button/nav-button/nav-icon-label-button/nav-button-gap.module.css
+++ /dev/null
@@ -1,14 +0,0 @@
-:root {
- --ax-public-button-gap-xx-small: var(--wb-space-75) var(--wb-space-75);
- --ax-public-button-gap-xxx-small: var(--wb-space-50) var(--wb-space-75);
-}
-
-@layer ui.component {
- .xx-small {
- gap: var(--ax-public-button-gap-xx-small);
- }
-
- .xxx-small {
- gap: var(--ax-public-button-gap-xxx-small);
- }
-}
diff --git a/packages/ui/src/components/button/nav-button/nav-icon-label-button/nav-icon-label-button-padding.module.css b/packages/ui/src/components/button/nav-button/nav-icon-label-button/nav-icon-label-button-padding.module.css
deleted file mode 100644
index 26c50926d..000000000
--- a/packages/ui/src/components/button/nav-button/nav-icon-label-button/nav-icon-label-button-padding.module.css
+++ /dev/null
@@ -1,14 +0,0 @@
-:root {
- --ax-public-icon-label-button-padding-xx-small: var(--wb-space-75) var(--wb-space-75);
- --ax-public-icon-label-button-padding-xxx-small: var(--wb-space-50) var(--wb-space-75);
-}
-
-@layer ui.component {
- .xx-small {
- padding: var(--ax-public-icon-label-button-padding-xx-small);
- }
-
- .xxx-small {
- padding: var(--ax-public-icon-label-button-padding-xxx-small);
- }
-}
diff --git a/packages/ui/src/components/button/nav-button/nav-icon-label-button/nav-icon-label-button.tsx b/packages/ui/src/components/button/nav-button/nav-icon-label-button/nav-icon-label-button.tsx
deleted file mode 100644
index 7ae31c5d7..000000000
--- a/packages/ui/src/components/button/nav-button/nav-icon-label-button/nav-icon-label-button.tsx
+++ /dev/null
@@ -1,40 +0,0 @@
-import clsx from 'clsx';
-import { forwardRef } from 'react';
-
-import fontSizeStyles from '../../styles/font-size.module.css';
-import gapStyles from '../../styles/gap.module.css';
-import paddingStyles from '../../styles/icon-label-button-padding.module.css';
-import iconSizeStyles from '../../styles/icon-size.module.css';
-import navFontSizeStyles from '../styles/nav-button-font-size.module.css';
-import navButtonIconSizeStyles from '../styles/nav-button-icon-size.module.css';
-import navGapStyles from './nav-button-gap.module.css';
-import navPaddingStyles from './nav-icon-label-button-padding.module.css';
-
-import { BaseButton } from '../../base-button/base-button';
-import { IconNode } from '../../types';
-import { NavBaseButtonProps } from '../types';
-
-export type NavIconLabelButtonProps = {
- children: [IconNode, string] | [string, IconNode] | [IconNode, string, IconNode];
-} & NavBaseButtonProps;
-
-export const NavIconLabelButton = forwardRef(
- ({ size = 'medium', children, ...props }, ref) => (
-
- {children}
-
- ),
-);
diff --git a/packages/ui/src/components/button/nav-button/nav-label-button/nav-label-button-padding.module.css b/packages/ui/src/components/button/nav-button/nav-label-button/nav-label-button-padding.module.css
deleted file mode 100644
index 5d806dff3..000000000
--- a/packages/ui/src/components/button/nav-button/nav-label-button/nav-label-button-padding.module.css
+++ /dev/null
@@ -1,14 +0,0 @@
-:root {
- --ax-public-label-button-padding-xx-small: var(--wb-space-87) var(--wb-space-75);
- --ax-public-label-button-padding-xxx-small: var(--wb-space-62) var(--wb-space-75);
-}
-
-@layer ui.component {
- .xx-small {
- padding: var(--ax-public-label-button-padding-xx-small);
- }
-
- .xxx-small {
- padding: var(--ax-public-label-button-padding-xxx-small);
- }
-}
diff --git a/packages/ui/src/components/button/nav-button/nav-label-button/nav-label-button.tsx b/packages/ui/src/components/button/nav-button/nav-label-button/nav-label-button.tsx
deleted file mode 100644
index 2f837dab0..000000000
--- a/packages/ui/src/components/button/nav-button/nav-label-button/nav-label-button.tsx
+++ /dev/null
@@ -1,26 +0,0 @@
-import clsx from 'clsx';
-import { forwardRef } from 'react';
-
-import fontSizeStyles from '../../styles/font-size.module.css';
-import paddingStyles from '../../styles/label-button-padding.module.css';
-import navFontSizeStyles from '../styles/nav-button-font-size.module.css';
-import navPaddingStyles from './nav-label-button-padding.module.css';
-
-import { BaseButton } from '../../base-button/base-button';
-import { NavBaseButtonProps } from '../types';
-
-export type NavLabelButtonProps = {
- children: string;
-} & NavBaseButtonProps;
-
-export const NavLabelButton = forwardRef(
- ({ size = 'medium', children, ...props }, ref) => (
-
- {children}
-
- ),
-);
diff --git a/packages/ui/src/components/button/nav-button/styles/nav-button-border-radius.module.css b/packages/ui/src/components/button/nav-button/styles/nav-button-border-radius.module.css
deleted file mode 100644
index ce3fcb3c2..000000000
--- a/packages/ui/src/components/button/nav-button/styles/nav-button-border-radius.module.css
+++ /dev/null
@@ -1,16 +0,0 @@
-:root {
- --ax-public-button-border-radius-xx-small: var(--wb-radius-50);
- --ax-public-button-border-radius-xxx-small: var(--wb-radius-50);
-}
-
-@layer ui.component {
- :not(.circle) {
- &.xx-small {
- border-radius: var(--ax-public-button-border-radius-xx-small);
- }
-
- &.xxx-small {
- border-radius: var(--ax-public-button-border-radius-xxx-small);
- }
- }
-}
diff --git a/packages/ui/src/components/button/nav-button/styles/nav-button-font-size.module.css b/packages/ui/src/components/button/nav-button/styles/nav-button-font-size.module.css
deleted file mode 100644
index e1eec5551..000000000
--- a/packages/ui/src/components/button/nav-button/styles/nav-button-font-size.module.css
+++ /dev/null
@@ -1,6 +0,0 @@
-@layer ui.component {
- .xx-small,
- .xxx-small {
- composes: ax-public-button-extra-small from global;
- }
-}
diff --git a/packages/ui/src/components/button/nav-button/styles/nav-button-icon-size.module.css b/packages/ui/src/components/button/nav-button/styles/nav-button-icon-size.module.css
deleted file mode 100644
index 14f15bb3e..000000000
--- a/packages/ui/src/components/button/nav-button/styles/nav-button-icon-size.module.css
+++ /dev/null
@@ -1,20 +0,0 @@
-:root {
- --ax-public-icon-size-xx-small: 0.75rem /* missing token */;
- --ax-public-icon-size-xxx-small: 0.75rem /* missing token */;
-}
-
-@layer ui.component {
- .xx-small {
- svg {
- width: var(--ax-public-icon-size-xx-small);
- height: var(--ax-public-icon-size-xx-small);
- }
- }
-
- .xxx-small {
- svg {
- width: var(--ax-public-icon-size-xxx-small);
- height: var(--ax-public-icon-size-xxx-small);
- }
- }
-}
diff --git a/packages/ui/src/components/button/nav-button/styles/nav-button.module.css b/packages/ui/src/components/button/nav-button/styles/nav-button.module.css
deleted file mode 100644
index fcc35eef2..000000000
--- a/packages/ui/src/components/button/nav-button/styles/nav-button.module.css
+++ /dev/null
@@ -1,37 +0,0 @@
-:root {
- --ax-public-button-nav-background-color: var(--wb-components-nav-button-bg-primary-hover);
- --ax-public-button-nav-color: var(--wb-components-nav-button-icon-primary-default);
- --ax-public-button-nav-color-active: var(--wb-components-nav-button-icon-primary-pressed);
- --ax-public-button-nav-color-disabled: var(--wb-components-nav-button-icon-primary-disabled);
-}
-
-@layer ui.component {
- .nav-button {
- background-color: transparent;
- color: var(--ax-public-button-nav-color);
-
- &:hover {
- background-color: var(--ax-public-button-nav-background-color);
- }
-
- &:active {
- color: var(--ax-public-button-nav-color-active);
- background-color: var(--ax-public-button-nav-background-color);
- }
-
- &:focus-visible {
- background-color: var(--ax-public-button-nav-background-color);
- }
-
- &:disabled {
- color: var(--ax-public-button-nav-color-disabled);
- background-color: transparent;
- }
-
- &.selected {
- pointer-events: none;
- background-color: var(--wb-components-button-solid-primary-default);
- color: var(--wb-components-nav-button-icon-primary-active);
- }
- }
-}
diff --git a/packages/ui/src/components/button/nav-button/types.ts b/packages/ui/src/components/button/nav-button/types.ts
index 89ebd99bf..a6cba0976 100644
--- a/packages/ui/src/components/button/nav-button/types.ts
+++ b/packages/ui/src/components/button/nav-button/types.ts
@@ -1,16 +1,39 @@
-import { Size } from '@ui/shared/types/size';
+import type { ButtonHTMLAttributes, ReactNode } from 'react';
-import { BaseButtonProps } from '../types';
+import type { TooltipVariant } from '../../tooltip/types';
-export type NavBaseButtonProps = BaseButtonProps & {
- /**
- * Size variant of the nav button.
- * @default 'medium'
- */
- size?: Size;
+export const NAV_BUTTON_SIZES = ['xl', 'l', 'm', 's', 'xs', 'xxs', 'xxxs'] as const;
+
+export type NavButtonSize = (typeof NAV_BUTTON_SIZES)[number];
+
+export const NAV_BUTTON_VARIANTS = ['square', 'round', 'plain'] as const;
+
+export type NavButtonVariant = (typeof NAV_BUTTON_VARIANTS)[number];
+
+export type NavLabelButtonProps = {
+ /** @default 'm' */
+ size?: NavButtonSize;
+ /** @default 'square' */
+ variant?: NavButtonVariant;
+ isSelected?: boolean;
+ children: ReactNode;
+ prefixIcon?: ReactNode;
+ suffixIcon?: ReactNode;
+ tooltip?: string;
+ tooltipType?: TooltipVariant;
+} & Omit, 'children'>;
+
+export type NavIconButtonProps = {
+ /** @default 'm' */
+ size?: NavButtonSize;
+ /** @default 'square' */
+ variant?: NavButtonVariant;
isSelected?: boolean;
-};
+ prefixIcon: ReactNode;
+ children?: never;
+ suffixIcon?: never;
+ tooltip?: string;
+ tooltipType?: TooltipVariant;
+} & Omit, 'children'>;
-export type { NavIconButtonProps } from './nav-icon-button/nav-icon-button';
-export type { NavIconLabelButtonProps } from './nav-icon-label-button/nav-icon-label-button';
-export type { NavLabelButtonProps } from './nav-label-button/nav-label-button';
+export type NavButtonProps = NavLabelButtonProps | NavIconButtonProps;
diff --git a/packages/ui/src/components/button/styles/icon-label-button-padding.module.css b/packages/ui/src/components/button/styles/icon-label-button-padding.module.css
deleted file mode 100644
index 6db63d038..000000000
--- a/packages/ui/src/components/button/styles/icon-label-button-padding.module.css
+++ /dev/null
@@ -1,29 +0,0 @@
-:root {
- --ax-public-icon-label-button-padding-extra-large: var(--wb-space-150) var(--wb-space-150);
- --ax-public-icon-label-button-padding-large: var(--wb-space-137) var(--wb-space-150);
- --ax-public-icon-label-button-padding-medium: var(--wb-space-112) var(--wb-space-150);
- --ax-public-icon-label-button-padding-small: var(--wb-space-87) var(--wb-space-125);
- --ax-public-icon-label-button-padding-extra-small: var(--wb-space-75) var(--wb-space-100);
-}
-
-@layer ui.component {
- .extra-large {
- padding: var(--ax-public-icon-label-button-padding-extra-large);
- }
-
- .large {
- padding: var(--ax-public-icon-label-button-padding-large);
- }
-
- .medium {
- padding: var(--ax-public-icon-label-button-padding-medium);
- }
-
- .small {
- padding: var(--ax-public-icon-label-button-padding-small);
- }
-
- .extra-small {
- padding: var(--ax-public-icon-label-button-padding-extra-small);
- }
-}
diff --git a/packages/ui/src/components/button/styles/label-button-padding.module.css b/packages/ui/src/components/button/styles/label-button-padding.module.css
deleted file mode 100644
index 000c52197..000000000
--- a/packages/ui/src/components/button/styles/label-button-padding.module.css
+++ /dev/null
@@ -1,26 +0,0 @@
-:root {
- --ax-public-label-button-padding-extra-large: var(--wb-space-200) var(--wb-space-200);
- --ax-public-label-button-padding-large: var(--wb-space-200) var(--wb-space-200);
- --ax-public-label-button-padding-medium: var(--wb-space-150) var(--wb-space-150);
- --ax-public-label-button-padding-small: var(--wb-space-125) var(--wb-space-125);
- --ax-public-label-button-padding-extra-small: var(--wb-space-100) var(--wb-space-100);
-}
-
-@layer ui.component {
- .extra-large,
- .large {
- padding: var(--ax-public-label-button-padding-extra-large);
- }
-
- .medium {
- padding: var(--ax-public-label-button-padding-medium);
- }
-
- .small {
- padding: var(--ax-public-label-button-padding-small);
- }
-
- .extra-small {
- padding: var(--ax-public-label-button-padding-extra-small);
- }
-}
diff --git a/packages/ui/src/components/collapsible/collapsible.tsx b/packages/ui/src/components/collapsible/collapsible.tsx
index e2fbc59bd..c54ae2127 100644
--- a/packages/ui/src/components/collapsible/collapsible.tsx
+++ b/packages/ui/src/components/collapsible/collapsible.tsx
@@ -54,13 +54,13 @@ Collapsible.Button = function CollapsibleButton() {
return (
-
-
+ prefixIcon={}
+ />
);
};
diff --git a/packages/ui/src/components/modal/modal.tsx b/packages/ui/src/components/modal/modal.tsx
index 0d521d061..ba57d2dd2 100644
--- a/packages/ui/src/components/modal/modal.tsx
+++ b/packages/ui/src/components/modal/modal.tsx
@@ -99,11 +99,7 @@ export const Modal = forwardRef(
)}
- {onClose && (
-
-
-
- )}
+ {onClose && } />}
{children && {children}
}
diff --git a/packages/ui/src/components/segment-picker/item/segment-picker-item.tsx b/packages/ui/src/components/segment-picker/item/segment-picker-item.tsx
index 0cabf15d1..fa9ccd9a0 100644
--- a/packages/ui/src/components/segment-picker/item/segment-picker-item.tsx
+++ b/packages/ui/src/components/segment-picker/item/segment-picker-item.tsx
@@ -1,68 +1,62 @@
-import {
- hasChildrenWithStringAndIcons,
- hasIconChildrenOnly,
- hasStringChildrenOnly,
-} from '@ui/components/button/guards';
import { NavButton } from '@ui/components/button/nav-button/nav-button';
-import { NavIconButtonProps } from '@ui/components/button/nav-button/nav-icon-button/nav-icon-button';
-import { NavIconLabelButtonProps } from '@ui/components/button/nav-button/nav-icon-label-button/nav-icon-label-button';
-import { NavLabelButtonProps } from '@ui/components/button/nav-button/nav-label-button/nav-label-button';
+import type { NavButtonProps } from '@ui/components/button/nav-button/types';
import clsx from 'clsx';
-import { MouseEvent, useContext } from 'react';
+import { type MouseEvent, type ReactNode, isValidElement, useContext } from 'react';
import itemShapeStyles from './segment-picker-item-shape.module.css';
-import { BaseButtonProps } from '../../button/types';
+import type { BaseButtonProps } from '../../button/types';
import { SegmentPickerContext } from '../utils/context';
-export type SegmentPickerItemProps = BaseButtonProps & {
- value: string;
-} & (
- | Pick
- | Pick
- | Pick
- );
-
-/**
- * A single item in the SegmentPicker, rendered as a NavButton under the hood.
- *
- * Automatically receives size and shape from SegmentPicker context.
- * Must be used only within a SegmentPicker component.
- *
- * Determines which NavButton variant to render based on its children
- * (label only, icon only, or icon + label).
- */
-export function Item({ children, value, ...buttonProps }: SegmentPickerItemProps) {
- const context = useContext(SegmentPickerContext);
-
- if (!context) {
- console.error('SegmentPicker.Item must be used within a SegmentPicker');
- return null;
- }
+export type SegmentPickerItemProps = BaseButtonProps &
+ Pick & {
+ value: string;
+ };
- const { selectedValue, onSelect, shape, ...other } = context;
+type Slots = { prefixIcon?: ReactNode; label?: ReactNode; suffixIcon?: ReactNode };
- const props = {
- className: clsx(itemShapeStyles['item'], itemShapeStyles[shape ?? 'default']),
- isSelected: selectedValue === value,
- onClick: (event: MouseEvent) => onSelect(event, value),
- shape,
- children,
- ...other,
- ...buttonProps,
- };
+function toSlots(children: ReactNode): Slots {
+ if (Array.isArray(children)) {
+ const parts = children.filter((child) => child != null && child !== false);
+ const prefixIcon = isValidElement(parts[0]) ? parts[0] : undefined;
+ const last = parts.at(-1);
+ const suffixIcon = parts.length > 1 && isValidElement(last) ? last : undefined;
+ const label = parts.filter((child) => !isValidElement(child));
- if (hasStringChildrenOnly(props)) {
- return ;
+ return { prefixIcon, label: label.length > 0 ? label : undefined, suffixIcon };
}
- if (hasIconChildrenOnly(props)) {
- return ;
- }
+ return isValidElement(children) ? { prefixIcon: children } : { label: children };
+}
+
+export function Item({ children, className, prefixIcon, suffixIcon, value, ...buttonProps }: SegmentPickerItemProps) {
+ const context = useContext(SegmentPickerContext);
- if (hasChildrenWithStringAndIcons(props)) {
- return ;
+ if (!context) {
+ console.error('SegmentPicker.Item must be used within a SegmentPicker');
+ return null;
}
- return null;
+ const { selectedValue, onSelect, shape, size, navVariant } = context;
+ const slots = toSlots(children);
+ const isSelected = selectedValue === value;
+
+ return (
+ ) => {
+ if (isSelected) return;
+ onSelect(event, value);
+ }}
+ prefixIcon={prefixIcon ?? slots.prefixIcon}
+ size={size}
+ variant={navVariant}
+ suffixIcon={suffixIcon ?? slots.suffixIcon}
+ {...buttonProps}
+ >
+ {slots.label}
+
+ );
}
diff --git a/packages/ui/src/components/segment-picker/segment-picker.tsx b/packages/ui/src/components/segment-picker/segment-picker.tsx
index c1568da9e..4b736188b 100644
--- a/packages/ui/src/components/segment-picker/segment-picker.tsx
+++ b/packages/ui/src/components/segment-picker/segment-picker.tsx
@@ -1,27 +1,44 @@
-import { Shape } from '@ui/components/button/types';
-import { Size } from '@ui/shared/types/size';
+import type { NavButtonSize, NavButtonVariant } from '@ui/components/button/nav-button/types';
+import type { Shape } from '@ui/components/button/types';
+import type { Size } from '@ui/shared/types/size';
import clsx from 'clsx';
-import { ForwardRefExoticComponent, MouseEvent, ReactElement, forwardRef, useState } from 'react';
+import {
+ type ForwardRefExoticComponent,
+ type MouseEvent,
+ type ReactElement,
+ type RefAttributes,
+ forwardRef,
+ useState,
+} from 'react';
import borderRadiusStyles from './border-radius-size.module.css';
import styles from './segment-picker.module.css';
-import { Item, SegmentPickerItemProps } from './item/segment-picker-item';
+import { Item, type SegmentPickerItemProps } from './item/segment-picker-item';
import { SegmentPickerContext } from './utils/context';
import { getValidShape } from './utils/get-valid-shape';
+const NAV_BUTTON_SIZE_BY_SEGMENT_PICKER_SIZE: Record = {
+ 'extra-large': 'xl',
+ large: 'l',
+ medium: 'm',
+ small: 's',
+ 'extra-small': 'xs',
+ 'xx-small': 'xxs',
+ 'xxx-small': 'xxxs',
+};
+
+const NAV_BUTTON_VARIANT_BY_SEGMENT_PICKER_SHAPE: Record = {
+ default: 'square',
+ circle: 'round',
+};
+
export type SegmentPickerPropsBase = {
children: ReactElement[];
- /**
- * Size variant of the SegmentPicker and its items.
- * @default 'medium'
- */
+ /** @default 'medium' */
size?: Size;
/**
- * Controls the shape of the SegmentPicker and its items.
- * (default) - Items stretch to fill the container equally.
- * 'circle' - Items fit tightly around their content to maintain a circular shape.
- * Only supported when items contain icons only.
+ * Circle is supported only when every item contains an icon without a label.
* @default 'default'
*/
shape?: Shape;
@@ -30,22 +47,18 @@ export type SegmentPickerPropsBase = {
};
export type ControlledSegmentPickerProps = {
- /** The currently selected value (controlled mode). */
value: string;
- /** Must not be used in controlled mode. */
defaultValue?: never;
} & SegmentPickerPropsBase;
export type UncontrolledSegmentPickerProps = {
- /** The initial selected value (uncontrolled mode). */
defaultValue: string;
- /** Must not be used in uncontrolled mode. */
value?: never;
} & SegmentPickerPropsBase;
export type SegmentPickerProps = ControlledSegmentPickerProps | UncontrolledSegmentPickerProps;
-type SegmentPickerComponent = ForwardRefExoticComponent> & {
+type SegmentPickerComponent = ForwardRefExoticComponent> & {
Item: typeof Item;
};
@@ -54,7 +67,6 @@ export const SegmentPicker = forwardRef(
const validShape = getValidShape(shape, children);
const isControlled = value !== undefined;
const [internalValue, setInternalValue] = useState(defaultValue);
-
const selectedValue = isControlled ? value : internalValue;
const handleSelect = (event: MouseEvent, newValue: string) => {
@@ -69,8 +81,9 @@ export const SegmentPicker = forwardRef(
value={{
selectedValue,
onSelect: handleSelect,
- size,
+ size: NAV_BUTTON_SIZE_BY_SEGMENT_PICKER_SIZE[size],
shape: validShape,
+ navVariant: NAV_BUTTON_VARIANT_BY_SEGMENT_PICKER_SHAPE[validShape],
}}
>
diff --git a/packages/ui/src/components/segment-picker/utils/context.ts b/packages/ui/src/components/segment-picker/utils/context.ts
index 0a2a33938..a64f70dd7 100644
--- a/packages/ui/src/components/segment-picker/utils/context.ts
+++ b/packages/ui/src/components/segment-picker/utils/context.ts
@@ -1,12 +1,13 @@
-import { Shape } from '@ui/components/button/types';
-import { Size } from '@ui/shared/types/size';
-import { MouseEvent, createContext } from 'react';
+import type { NavButtonSize, NavButtonVariant } from '@ui/components/button/nav-button/types';
+import type { Shape } from '@ui/components/button/types';
+import { type MouseEvent, createContext } from 'react';
type SegmentPickerContextType = {
selectedValue: string | undefined;
onSelect: (event: MouseEvent, value: string) => void;
- size?: Size;
- shape?: Shape;
+ size: NavButtonSize;
+ shape: Shape;
+ navVariant: NavButtonVariant;
};
export const SegmentPickerContext = createContext(undefined);
diff --git a/packages/ui/src/components/segment-picker/utils/get-valid-shape.spec.ts b/packages/ui/src/components/segment-picker/utils/get-valid-shape.spec.ts
index 5ae8ea34b..5449ca285 100644
--- a/packages/ui/src/components/segment-picker/utils/get-valid-shape.spec.ts
+++ b/packages/ui/src/components/segment-picker/utils/get-valid-shape.spec.ts
@@ -8,16 +8,16 @@ type PickerItem = ReactElement;
const icon = createElement('svg');
-// `SegmentPickerItemProps` requires `children` in props, so the guards read
-// `item.props.children` — pass it as a prop rather than a createElement arg.
function iconItem(value: string): PickerItem {
- // eslint-disable-next-line react/no-children-prop
- return createElement(Item, { value, children: icon }) as PickerItem;
+ return createElement(Item, { value, prefixIcon: icon }) as PickerItem;
+}
+
+function legacyIconItem(value: string): PickerItem {
+ return createElement(Item, { value }, icon) as PickerItem;
}
function labelItem(value: string): PickerItem {
- // eslint-disable-next-line react/no-children-prop
- return createElement(Item, { value, children: 'label' }) as PickerItem;
+ return createElement(Item, { value }, 'label') as PickerItem;
}
describe('getValidShape', () => {
@@ -25,11 +25,15 @@ describe('getValidShape', () => {
expect(getValidShape('default', [labelItem('a')])).toBe('default');
});
- it("returns 'circle' when every item has icon-only children", () => {
+ it("returns 'circle' when every item has an explicit prefix icon", () => {
expect(getValidShape('circle', [iconItem('a'), iconItem('b')])).toBe('circle');
});
- it("falls back to 'default' and logs an error when an item has a string child", () => {
+ it("returns 'circle' for the existing icon-child API", () => {
+ expect(getValidShape('circle', [legacyIconItem('a'), legacyIconItem('b')])).toBe('circle');
+ });
+
+ it("falls back to 'default' and logs an error when an item has a label", () => {
const spy = vi.spyOn(console, 'error').mockImplementation(() => {});
expect(getValidShape('circle', [iconItem('a'), labelItem('b')])).toBe('default');
diff --git a/packages/ui/src/components/segment-picker/utils/get-valid-shape.ts b/packages/ui/src/components/segment-picker/utils/get-valid-shape.ts
index d70248787..f3d857df0 100644
--- a/packages/ui/src/components/segment-picker/utils/get-valid-shape.ts
+++ b/packages/ui/src/components/segment-picker/utils/get-valid-shape.ts
@@ -1,20 +1,22 @@
-import { Shape } from '@ui/components/button/types';
-import { ReactElement } from 'react';
+import type { Shape } from '@ui/components/button/types';
+import { type ReactElement, isValidElement } from 'react';
-import { hasIconChildrenOnly } from '../../button/guards';
-import { Item, SegmentPickerItemProps } from '../item/segment-picker-item';
+import { Item, type SegmentPickerItemProps } from '../item/segment-picker-item';
export function getValidShape(shape: Shape, items: ReactElement[]): Shape {
if (shape !== 'circle') {
return shape;
}
- const everyItemHasOnlyIcon = items.every((item) => hasIconChildrenOnly({ children: item.props.children }));
+ const everyItemHasOnlyIcon = items.every(({ props }) => {
+ const hasExplicitIcon = props.prefixIcon != null && props.children == null && props.suffixIcon == null;
+ const hasLegacyIconChild = props.prefixIcon == null && isValidElement(props.children) && props.suffixIcon == null;
+
+ return hasExplicitIcon || hasLegacyIconChild;
+ });
if (!everyItemHasOnlyIcon) {
- console.error(
- '[SegmentPicker] The "circle" shape can only be used when all SegmentPicker.Item components contain icon-only children.',
- );
+ console.error('[SegmentPicker] The "circle" shape can only be used when all items contain only a prefix icon.');
return 'default';
}
diff --git a/packages/ui/src/components/snackbar/components/action-buttons.tsx b/packages/ui/src/components/snackbar/components/action-buttons.tsx
index 32dc2f8fd..c526cd203 100644
--- a/packages/ui/src/components/snackbar/components/action-buttons.tsx
+++ b/packages/ui/src/components/snackbar/components/action-buttons.tsx
@@ -34,9 +34,7 @@ export function ActionButtons({ variant, buttonLabel, onButtonClick, close, onCl
)}
{close && onClose && (
-
-
-
+ } />
)}
);