Skip to content

Commit 3a2ceb2

Browse files
Reimplement button API to simplify usage (#102)
* Reimplement button API to simplify usage - introduce new types for nav-button * Improve documentation * dd styles to support all size variants of the nav button * introduce new doc pages for nav button types * Split Button and Nav Button to separate sections * Improve buttons * Improvements after review * style markdown table * fix color * update npm package
1 parent d4b7175 commit 3a2ceb2

File tree

76 files changed

+1508
-568
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

76 files changed

+1508
-568
lines changed

packages/ui/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "@synergycodes/axiom",
33
"type": "module",
4-
"version": "1.0.0-beta.9",
4+
"version": "1.0.0-beta.10",
55
"description": "A React library for creating node-based UIs and diagram-driven applications. Perfect for React Flow users, providing ready-to-use node templates and components that work seamlessly with React Flow's ecosystem.",
66
"keywords": [
77
"react",

packages/ui/src/components/accordion/accordion.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,9 @@ export const Accordion = forwardRef<HTMLDivElement, AccordionProps>(
7878
aria-expanded={isOpen}
7979
>
8080
<span className="ax-public-h10">{label}</span>
81-
<NavButton className={styles['header-button']} icon={<CaretUp />} />
81+
<NavButton className={styles['header-button']}>
82+
<CaretUp />
83+
</NavButton>
8284
</div>
8385
<div
8486
className={clsx(styles['inner-container'], {

packages/ui/src/components/button/base-button/base-button.tsx

Lines changed: 4 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,8 @@
11
import clsx from 'clsx';
22
import buttonStyles from './base-button.module.css';
3-
import gapStyles from './gap.module.css';
43

54
import { Button } from '@mui/base/Button';
6-
import { BaseButtonProps, CommonButtonProps } from '../types';
5+
import { BaseButtonProps } from '../types';
76
import { forwardRef } from 'react';
87
import { prepareForSlot } from '@mui/base';
98
import { Tooltip } from '@ui/components/tooltip/tooltip';
@@ -12,10 +11,10 @@ type Props = {
1211
/** Class name meant to be used by parent components using <BaseButton /> directly */
1312
styles: string;
1413
children: React.ReactNode;
15-
} & CommonButtonProps;
14+
} & BaseButtonProps;
1615

1716
export const BaseButton = prepareForSlot(
18-
forwardRef<HTMLButtonElement, BaseButtonProps<Props>>(
17+
forwardRef<HTMLButtonElement, Props>(
1918
(
2019
{
2120
children,
@@ -24,20 +23,14 @@ export const BaseButton = prepareForSlot(
2423
tooltip,
2524
disabled,
2625
tooltipType = 'default',
27-
size = 'medium',
2826
...props
2927
},
3028
ref,
3129
) => {
3230
const button = (
3331
<Button
3432
ref={ref}
35-
className={clsx(
36-
buttonStyles['button'],
37-
gapStyles[size],
38-
styles,
39-
className,
40-
)}
33+
className={clsx(buttonStyles['button'], styles, className)}
4134
disabled={disabled}
4235
{...props}
4336
>
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
# Button Components Redesign: Structural Discrimination & API Simplification
2+
3+
Proposed by: **Piotr Błaszczyk**
4+
5+
Date: **12.06.2025**
6+
7+
## Context
8+
9+
The original implementation of button components involved creating four distinct components (`LabelButton`, `NavButton`, `IconButton`, and `IconLabelButton`) to cover different usage scenarios. While this provided a high degree of specificity and control, it introduced complexity in the API.
10+
11+
Each button type had its own props, styling logic, and render behavior. While maintainable in the short term, it made the overall button API harder to learn and less adaptable to changes.
12+
13+
With the evolution of our design system, the need emerged for a more **unified**, **simpler**, and **smarter** API that maintains strong **type safety**, and full **customizability**.
14+
15+
## Decision
16+
17+
To simplify the API while increasing flexibility and consistency, we refactored the button components using **structural discrimination** and **TypeScript overloads**. This led to the introduction of only two components:
18+
19+
### `Button`
20+
21+
### `NavButton`
22+
23+
Both components now support three rendering modes, automatically selected based on the `children` structure:
24+
25+
- **LabelButton** → Rendered when `children` is a plain `string`
26+
- **IconButton** → Rendered when `children` is a single `ReactElement` (icon)
27+
- **IconLabelButton** → Rendered when `children` includes both `string` and one or two icons (before/after)
28+
29+
### Key Improvements
30+
31+
1. **Structural Discrimination**
32+
33+
- The component infers the correct variant purely from the structure of `children`, not from a manual `type` prop.
34+
- This significantly simplifies usage while maintaining strict prop boundaries.
35+
36+
2. **Overload-based Props**
37+
38+
- TypeScript function overloads are used to expose only valid prop combinations for each type.
39+
- Invalid combinations (e.g., passing label-specific props to an icon-only button) are caught at compile time.
40+
41+
3. **API Parity**
42+
43+
- `NavButton` now supports the same rendering flexibility as `Button`, adapting automatically to icon-only, label-only, or mixed children.
44+
45+
4. **Facade Design**
46+
47+
- Under the hood, the same internal components (`LabelButton`, `IconButton`, `IconLabelButton`) are still used.
48+
- A lightweight **facade layer** determines the correct button types based on structural cues and delegates rendering to the appropriate internal component.
49+
- This separation allows the internal implementation to stay clean and type-safe, while simplifying the public API.
50+
51+
## Consequences
52+
53+
- **Simplified Usage**: Consumers use one consistent Button API without needing to remember different component names for different content types.
54+
- **Strong Type Safety**: Incorrect prop usage is prevented by TypeScript at compile time.
55+
- **Alignment with Design System**: The new components fully match the latest design system expectations while reducing entry friction for developers.
56+
- **White-label Ready**: Continued use of CSS Modules ensures easy theming and customization per client.
57+
- **Ease of Migration**: Existing buttons can be migrated progressively by converting them to use the new `Button` or `NavButton` with the appropriate children.
58+
- **Future-Proofing**: The facade model allows additional internal types to be added or optimized without breaking the public API.
59+
60+
## Status
61+
62+
Accepted
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import { isValidElement, PropsWithChildren } from 'react';
2+
3+
export function hasIconChildrenOnly<T>(
4+
props: PropsWithChildren,
5+
): props is PropsWithChildren<T> {
6+
return isValidElement(props.children) || typeof props.children === 'function';
7+
}
8+
9+
export function hasChildrenWithStringAndIcons<T>(
10+
props: PropsWithChildren,
11+
): props is PropsWithChildren<T> {
12+
return (
13+
Array.isArray(props.children) &&
14+
props.children.length >= 2 &&
15+
(typeof props.children[0] === 'string' ||
16+
isValidElement(props.children[0])) &&
17+
(typeof props.children[1] === 'string' || isValidElement(props.children[1]))
18+
);
19+
}
20+
21+
export function hasStringChildrenOnly<T>(
22+
props: PropsWithChildren,
23+
): props is PropsWithChildren<T> {
24+
return typeof (props as PropsWithChildren<T>).children === 'string';
25+
}

packages/ui/src/components/button/icon-button/icon-button.tsx

Lines changed: 0 additions & 50 deletions
This file was deleted.

packages/ui/src/components/button/icon-label-button/icon-label-button.tsx

Lines changed: 0 additions & 55 deletions
This file was deleted.

packages/ui/src/components/button/label-button/label-button.tsx

Lines changed: 0 additions & 54 deletions
This file was deleted.

0 commit comments

Comments
 (0)