@boostdev/design-system-components 1.2.8 → 2.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +5 -3
- package/README.md +57 -38
- package/dist/client.cjs +52 -50
- package/dist/client.css +537 -524
- package/dist/client.d.cts +31 -1
- package/dist/client.d.ts +31 -1
- package/dist/client.js +52 -50
- package/dist/index.cjs +52 -50
- package/dist/index.css +537 -524
- package/dist/index.d.cts +31 -1
- package/dist/index.d.ts +31 -1
- package/dist/index.js +52 -50
- package/dist/native/index.cjs +5 -2
- package/dist/native/index.d.cts +3 -1
- package/dist/native/index.d.ts +3 -1
- package/dist/native/index.js +5 -2
- package/dist/web-components/{chunk-DZRSJGPB.js → chunk-3S42DZ7W.js} +1 -1
- package/dist/web-components/{chunk-3REOIRDW.js → chunk-N3TN6WCH.js} +26 -1
- package/dist/web-components/globals.js +1 -1
- package/dist/web-components/index.js +2 -2
- package/dist/web-components/interaction/bds-button.d.ts +7 -0
- package/dist/web-components/interaction/bds-button.js +1 -1
- package/package.json +3 -3
- package/src/components/interaction/Button/Button.mdx +84 -29
- package/src/components/interaction/Button/Button.module.css +25 -1
- package/src/components/interaction/Button/Button.native.mdx +33 -14
- package/src/components/interaction/Button/Button.native.spec.tsx +20 -0
- package/src/components/interaction/Button/Button.native.stories.tsx +110 -9
- package/src/components/interaction/Button/Button.native.tsx +13 -4
- package/src/components/interaction/Button/Button.spec.tsx +16 -0
- package/src/components/interaction/Button/Button.stories.tsx +134 -16
- package/src/components/interaction/Button/Button.tsx +32 -0
- package/src/components/interaction/Command/Command.module.css +1 -1
- package/src/components/interaction/Dialog/Dialog.module.css +1 -1
- package/src/components/interaction/Dialog/Dialog.native.mdx +2 -2
- package/src/components/interaction/Drawer/Drawer.module.css +1 -1
- package/src/components/interaction/Drawer/Drawer.native.mdx +2 -2
- package/src/components/interaction/DropdownMenu/DropdownMenu.module.css +1 -1
- package/src/components/interaction/DropdownMenu/DropdownMenu.native.mdx +2 -2
- package/src/components/interaction/Popover/Popover.module.css +1 -1
- package/src/components/interaction/Popover/Popover.native.mdx +2 -2
- package/src/components/interaction/Rating/Rating.module.css +1 -1
- package/src/components/interaction/Rating/Rating.native.mdx +1 -1
- package/src/components/interaction/Toast/Toast.module.css +1 -1
- package/src/components/interaction/Toast/Toast.native.mdx +3 -3
- package/src/components/interaction/form/Checkbox/Checkbox.module.css +1 -1
- package/src/components/interaction/form/Checkbox/Checkbox.native.mdx +1 -1
- package/src/components/interaction/form/CheckboxGroup/CheckboxGroup.module.css +1 -1
- package/src/components/interaction/form/CheckboxGroup/CheckboxGroup.native.mdx +1 -1
- package/src/components/interaction/form/Combobox/Combobox.module.css +1 -1
- package/src/components/interaction/form/Combobox/Combobox.native.mdx +1 -1
- package/src/components/interaction/form/FileInput/FileInput.module.css +1 -1
- package/src/components/interaction/form/FileInput/FileInput.native.mdx +1 -1
- package/src/components/interaction/form/FormInput/FormInput.module.css +1 -1
- package/src/components/interaction/form/FormInput/FormInput.native.mdx +1 -1
- package/src/components/interaction/form/NumberInput/NumberInput.module.css +1 -1
- package/src/components/interaction/form/NumberInput/NumberInput.native.mdx +1 -1
- package/src/components/interaction/form/Radio/Radio.module.css +1 -1
- package/src/components/interaction/form/RadioGroup/RadioGroup.module.css +1 -1
- package/src/components/interaction/form/RadioGroup/RadioGroup.native.mdx +1 -1
- package/src/components/interaction/form/SegmentedControl/SegmentedControl.module.css +1 -1
- package/src/components/interaction/form/SegmentedControl/SegmentedControl.native.mdx +1 -1
- package/src/components/interaction/form/Select/Select.module.css +1 -1
- package/src/components/interaction/form/Select/Select.native.mdx +1 -1
- package/src/components/interaction/form/Slider/Slider.module.css +1 -1
- package/src/components/interaction/form/Slider/Slider.native.mdx +1 -1
- package/src/components/interaction/form/Switch/Switch.module.css +1 -1
- package/src/components/interaction/form/Textarea/Textarea.module.css +1 -1
- package/src/components/interaction/form/Textarea/Textarea.native.mdx +1 -1
- package/src/components/interaction/form/atoms/InputContainer.module.css +1 -1
- package/src/components/interaction/form/atoms/Label.module.css +1 -1
- package/src/components/interaction/form/atoms/Message.module.css +1 -1
- package/src/components/layout/ButtonGroup/ButtonGroup.module.css +1 -1
- package/src/components/layout/Card/Card.module.css +1 -1
- package/src/components/layout/IconWrapper/IconWrapper.module.css +1 -1
- package/src/components/layout/IconWrapper/IconWrapper.stories.tsx +46 -14
- package/src/components/layout/SectionHeader/SectionHeader.module.css +1 -1
- package/src/components/ui/Accordion/Accordion.module.css +1 -1
- package/src/components/ui/Accordion/Accordion.native.mdx +1 -1
- package/src/components/ui/Accordion/Accordion.native.stories.tsx +1 -1
- package/src/components/ui/Alert/Alert.module.css +1 -1
- package/src/components/ui/Alert/Alert.native.mdx +1 -1
- package/src/components/ui/Avatar/Avatar.module.css +1 -1
- package/src/components/ui/Avatar/Avatar.native.mdx +1 -1
- package/src/components/ui/Badge/Badge.module.css +1 -1
- package/src/components/ui/Badge/Badge.native.mdx +1 -1
- package/src/components/ui/Breadcrumb/Breadcrumb.module.css +1 -1
- package/src/components/ui/Breadcrumb/Breadcrumb.native.mdx +1 -1
- package/src/components/ui/Calendar/Calendar.module.css +1 -1
- package/src/components/ui/Calendar/Calendar.native.mdx +1 -1
- package/src/components/ui/Carousel/Carousel.module.css +1 -1
- package/src/components/ui/Carousel/Carousel.native.mdx +1 -1
- package/src/components/ui/Collapsible/Collapsible.module.css +1 -1
- package/src/components/ui/Collapsible/Collapsible.native.mdx +1 -1
- package/src/components/ui/DescriptionList/DescriptionList.module.css +1 -1
- package/src/components/ui/DescriptionList/DescriptionList.native.mdx +1 -1
- package/src/components/ui/Link/Link.module.css +1 -1
- package/src/components/ui/Link/Link.native.mdx +1 -1
- package/src/components/ui/Loading/Loading.module.css +1 -1
- package/src/components/ui/NotificationBanner/NotificationBanner.module.css +1 -1
- package/src/components/ui/Pagination/Pagination.module.css +1 -1
- package/src/components/ui/Pagination/Pagination.native.mdx +1 -1
- package/src/components/ui/Progress/Progress.module.css +1 -1
- package/src/components/ui/ProgressCircle/ProgressCircle.module.css +1 -1
- package/src/components/ui/ProgressCircle/ProgressCircle.native.mdx +1 -1
- package/src/components/ui/Separator/Separator.module.css +1 -1
- package/src/components/ui/Skeleton/Skeleton.module.css +1 -1
- package/src/components/ui/SkipLink/SkipLink.module.css +1 -1
- package/src/components/ui/Table/Table.module.css +1 -1
- package/src/components/ui/Table/Table.native.mdx +1 -1
- package/src/components/ui/Tabs/Tabs.module.css +1 -1
- package/src/components/ui/Tabs/Tabs.native.mdx +1 -1
- package/src/components/ui/Tooltip/Tooltip.module.css +1 -1
- package/src/components/ui/Tooltip/Tooltip.native.mdx +1 -1
- package/src/components/ui/Typography/Typography.module.css +1 -1
- package/src/components/ui/Typography/Typography.native.mdx +1 -1
- package/src/css/bdc.css +1 -1
- package/src/css/index.css +1 -1
- package/src/stories/DesignSystem/Borders.mdx +3 -3
- package/src/stories/DesignSystem/Colors.mdx +1 -1
- package/src/stories/DesignSystem/DarkMode.mdx +3 -3
- package/src/stories/DesignSystem/Overview.mdx +16 -5
- package/src/stories/DesignSystem/Typography.mdx +1 -1
- package/src/stories/Introduction.mdx +4 -4
- package/src/stories/ReactNative.mdx +6 -6
- package/src/web-components/globals.ts +1 -1
- package/src/web-components/interaction/BdsAccordion.mdx +3 -3
- package/src/web-components/interaction/BdsAccordion.stories.tsx +1 -1
- package/src/web-components/interaction/BdsButton.mdx +48 -16
- package/src/web-components/interaction/BdsButton.stories.tsx +171 -19
- package/src/web-components/interaction/BdsCollapsible.mdx +2 -2
- package/src/web-components/interaction/BdsDialog.mdx +2 -2
- package/src/web-components/interaction/BdsDrawer.mdx +2 -2
- package/src/web-components/interaction/BdsDropdownMenu.mdx +2 -2
- package/src/web-components/interaction/BdsPopover.mdx +1 -1
- package/src/web-components/interaction/BdsSkipLink.mdx +2 -2
- package/src/web-components/interaction/BdsTabs.mdx +2 -2
- package/src/web-components/interaction/BdsToastProvider.mdx +3 -3
- package/src/web-components/interaction/BdsTooltip.mdx +2 -2
- package/src/web-components/interaction/bds-button.spec.ts +35 -0
- package/src/web-components/interaction/bds-button.ts +28 -1
- package/src/web-components/interaction/form/BdsCheckbox.mdx +1 -1
- package/src/web-components/interaction/form/BdsCheckboxGroup.mdx +1 -1
- package/src/web-components/interaction/form/BdsCombobox.mdx +1 -1
- package/src/web-components/interaction/form/BdsFileInput.mdx +1 -1
- package/src/web-components/interaction/form/BdsFormInput.mdx +1 -1
- package/src/web-components/interaction/form/BdsNumberInput.mdx +1 -1
- package/src/web-components/interaction/form/BdsRadio.mdx +1 -1
- package/src/web-components/interaction/form/BdsRadioGroup.mdx +1 -1
- package/src/web-components/interaction/form/BdsSegmentedControl.mdx +1 -1
- package/src/web-components/interaction/form/BdsSelect.mdx +1 -1
- package/src/web-components/interaction/form/BdsSlider.mdx +1 -1
- package/src/web-components/interaction/form/BdsSwitch.mdx +1 -1
- package/src/web-components/interaction/form/BdsTextarea.mdx +1 -1
- package/src/web-components/ui/BdsAlert.mdx +1 -1
- package/src/web-components/ui/BdsAvatar.mdx +1 -1
- package/src/web-components/ui/BdsBadge.mdx +1 -1
- package/src/web-components/ui/BdsBreadcrumb.mdx +1 -1
- package/src/web-components/ui/BdsButtonGroup.mdx +1 -1
- package/src/web-components/ui/BdsCalendar.mdx +1 -1
- package/src/web-components/ui/BdsCard.mdx +1 -1
- package/src/web-components/ui/BdsCarousel.mdx +1 -1
- package/src/web-components/ui/BdsDescriptionList.mdx +1 -1
- package/src/web-components/ui/BdsIconWrapper.mdx +1 -1
- package/src/web-components/ui/BdsLink.mdx +1 -1
- package/src/web-components/ui/BdsLoading.mdx +1 -1
- package/src/web-components/ui/BdsNotificationBanner.mdx +1 -1
- package/src/web-components/ui/BdsPagination.mdx +1 -1
- package/src/web-components/ui/BdsProgress.mdx +1 -1
- package/src/web-components/ui/BdsProgressCircle.mdx +1 -1
- package/src/web-components/ui/BdsRating.mdx +1 -1
- package/src/web-components/ui/BdsSectionHeader.mdx +1 -1
- package/src/web-components/ui/BdsSeparator.mdx +2 -2
- package/src/web-components/ui/BdsSkeleton.mdx +1 -1
- package/src/web-components/ui/BdsTable.mdx +1 -1
- package/src/web-components/ui/BdsTypography.mdx +1 -1
|
@@ -1,34 +1,113 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
|
+
import type { ReactNode } from 'react';
|
|
2
3
|
import type { Meta, StoryObj } from '@storybook/react';
|
|
3
4
|
import { ThemeProvider } from '../../../native/ThemeContext';
|
|
4
|
-
import { Button } from './Button.native';
|
|
5
|
+
import { Button, type ButtonProps } from './Button.native';
|
|
5
6
|
|
|
6
|
-
const
|
|
7
|
+
const CheckIcon = (
|
|
8
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="20" height="20" aria-hidden>
|
|
9
|
+
<path d="M12 0a12 12 0 1 0 12 12A12 12 0 0 0 12 0Zm6.93 8.2-6.85 9.29a1 1 0 0 1-1.43.19l-4.89-3.91a1 1 0 0 1-.15-1.41A1 1 0 0 1 7 12.21l4.08 3.26L17.32 7a1 1 0 0 1 1.39-.21 1 1 0 0 1 .22 1.41Z" fill="currentColor"/>
|
|
10
|
+
</svg>
|
|
11
|
+
);
|
|
12
|
+
|
|
13
|
+
const ArrowRight = <span aria-hidden>→</span>;
|
|
14
|
+
const ArrowLeft = <span aria-hidden>←</span>;
|
|
15
|
+
|
|
16
|
+
const iconMap = {
|
|
17
|
+
none: undefined,
|
|
18
|
+
check: CheckIcon,
|
|
19
|
+
arrowRight: ArrowRight,
|
|
20
|
+
arrowLeft: ArrowLeft,
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
type IconKey = keyof typeof iconMap;
|
|
24
|
+
|
|
25
|
+
type ButtonStoryArgs = Omit<ButtonProps, 'iconStart' | 'iconEnd' | 'children'> & {
|
|
26
|
+
iconStart?: IconKey;
|
|
27
|
+
iconEnd?: IconKey;
|
|
28
|
+
children?: ReactNode;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const meta: Meta<ButtonStoryArgs> = {
|
|
7
32
|
tags: ['!stable', 'alpha'],
|
|
8
33
|
title: 'React Native/Interaction/Button',
|
|
9
34
|
component: Button,
|
|
10
35
|
decorators: [(Story) => <ThemeProvider colorScheme="light"><Story /></ThemeProvider>],
|
|
11
36
|
parameters: { layout: 'centered' },
|
|
12
37
|
argTypes: {
|
|
13
|
-
variant: {
|
|
14
|
-
|
|
38
|
+
variant: {
|
|
39
|
+
control: 'select',
|
|
40
|
+
options: ['default', 'outline'],
|
|
41
|
+
description: 'Visual style. `default` is filled; `outline` is transparent with a border.',
|
|
42
|
+
table: { defaultValue: { summary: 'default' } },
|
|
43
|
+
},
|
|
44
|
+
size: {
|
|
45
|
+
control: 'select',
|
|
46
|
+
options: ['small', 'medium', 'large'],
|
|
47
|
+
description: 'Height + horizontal padding.',
|
|
48
|
+
table: { defaultValue: { summary: 'medium' } },
|
|
49
|
+
},
|
|
50
|
+
iconStart: {
|
|
51
|
+
control: 'select',
|
|
52
|
+
options: Object.keys(iconMap) as IconKey[],
|
|
53
|
+
mapping: iconMap,
|
|
54
|
+
description: 'Icon rendered before the label.',
|
|
55
|
+
},
|
|
56
|
+
iconEnd: {
|
|
57
|
+
control: 'select',
|
|
58
|
+
options: Object.keys(iconMap) as IconKey[],
|
|
59
|
+
mapping: iconMap,
|
|
60
|
+
description: 'Icon rendered after the label.',
|
|
61
|
+
},
|
|
62
|
+
isIconOnly: {
|
|
63
|
+
control: 'boolean',
|
|
64
|
+
description: 'Forces 1:1 aspect ratio with minimal xs padding. Composable with any `variant`. Requires `accessibilityLabel`.',
|
|
65
|
+
table: { defaultValue: { summary: 'false' } },
|
|
66
|
+
},
|
|
67
|
+
disabled: { control: 'boolean' },
|
|
15
68
|
onPress: { action: 'pressed' },
|
|
16
69
|
},
|
|
17
|
-
|
|
70
|
+
args: { variant: 'default', size: 'medium' },
|
|
71
|
+
};
|
|
18
72
|
|
|
19
73
|
export default meta;
|
|
20
|
-
type Story = StoryObj<
|
|
74
|
+
type Story = StoryObj<ButtonStoryArgs>;
|
|
21
75
|
|
|
22
|
-
export const
|
|
23
|
-
|
|
76
|
+
export const Playground: Story = {
|
|
77
|
+
args: { children: 'Button', iconStart: 'none', iconEnd: 'none' },
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
export const Default: Story = { args: { children: 'Default' } };
|
|
81
|
+
export const Outline: Story = { args: { children: 'Outline', variant: 'outline' } };
|
|
24
82
|
export const Small: Story = { args: { children: 'Small', size: 'small' } };
|
|
25
83
|
export const Medium: Story = { args: { children: 'Medium', size: 'medium' } };
|
|
26
84
|
export const Large: Story = { args: { children: 'Large', size: 'large' } };
|
|
27
85
|
export const Disabled: Story = { args: { children: 'Disabled', disabled: true } };
|
|
86
|
+
|
|
87
|
+
export const WithIconStart: Story = { args: { children: 'Confirm', iconStart: 'check' } };
|
|
88
|
+
export const WithIconEnd: Story = { args: { children: 'Continue', iconEnd: 'arrowRight' } };
|
|
89
|
+
|
|
90
|
+
export const IconOnly: Story = {
|
|
91
|
+
args: {
|
|
92
|
+
isIconOnly: true,
|
|
93
|
+
accessibilityLabel: 'Confirm',
|
|
94
|
+
children: CheckIcon,
|
|
95
|
+
},
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
export const IconOnlyOutline: Story = {
|
|
99
|
+
args: {
|
|
100
|
+
variant: 'outline',
|
|
101
|
+
isIconOnly: true,
|
|
102
|
+
accessibilityLabel: 'Confirm',
|
|
103
|
+
children: CheckIcon,
|
|
104
|
+
},
|
|
105
|
+
};
|
|
106
|
+
|
|
28
107
|
export const AllVariants: Story = {
|
|
29
108
|
render: () => (
|
|
30
109
|
<div style={{ display: 'flex', flexDirection: 'column', gap: '12px' }}>
|
|
31
|
-
{(['default', '
|
|
110
|
+
{(['default', 'outline'] as const).map(v => (
|
|
32
111
|
<div key={v} style={{ display: 'flex', gap: '8px', alignItems: 'center' }}>
|
|
33
112
|
{(['small', 'medium', 'large'] as const).map(s => (
|
|
34
113
|
<Button key={s} variant={v} size={s}>{v} {s}</Button>
|
|
@@ -38,3 +117,25 @@ export const AllVariants: Story = {
|
|
|
38
117
|
</div>
|
|
39
118
|
),
|
|
40
119
|
};
|
|
120
|
+
|
|
121
|
+
export const AllIconOnly: Story = {
|
|
122
|
+
render: () => (
|
|
123
|
+
<div style={{ display: 'flex', flexDirection: 'column', gap: '12px' }}>
|
|
124
|
+
{(['default', 'outline'] as const).map(v => (
|
|
125
|
+
<div key={v} style={{ display: 'flex', gap: '8px', alignItems: 'center' }}>
|
|
126
|
+
{(['small', 'medium', 'large'] as const).map(s => (
|
|
127
|
+
<Button
|
|
128
|
+
key={s}
|
|
129
|
+
variant={v}
|
|
130
|
+
size={s}
|
|
131
|
+
isIconOnly
|
|
132
|
+
accessibilityLabel={`${v} ${s}`}
|
|
133
|
+
>
|
|
134
|
+
{CheckIcon}
|
|
135
|
+
</Button>
|
|
136
|
+
))}
|
|
137
|
+
</div>
|
|
138
|
+
))}
|
|
139
|
+
</div>
|
|
140
|
+
),
|
|
141
|
+
};
|
|
@@ -13,6 +13,8 @@ export interface ButtonProps {
|
|
|
13
13
|
iconStart?: ReactNode;
|
|
14
14
|
iconEnd?: ReactNode;
|
|
15
15
|
disabled?: boolean;
|
|
16
|
+
/** Forces a 1:1 aspect ratio with minimal padding for icon-only buttons. Requires `accessibilityLabel`. */
|
|
17
|
+
isIconOnly?: boolean;
|
|
16
18
|
onPress?: () => void;
|
|
17
19
|
accessibilityLabel?: string;
|
|
18
20
|
style?: StyleProp<ViewStyle>;
|
|
@@ -46,6 +48,7 @@ export function Button({
|
|
|
46
48
|
iconStart,
|
|
47
49
|
iconEnd,
|
|
48
50
|
disabled = false,
|
|
51
|
+
isIconOnly = false,
|
|
49
52
|
onPress,
|
|
50
53
|
accessibilityLabel,
|
|
51
54
|
style,
|
|
@@ -62,9 +65,11 @@ export function Button({
|
|
|
62
65
|
const isOutline = variant === 'outline' || variant === 'ghost';
|
|
63
66
|
const containerStyle: ViewStyle = {
|
|
64
67
|
height,
|
|
65
|
-
paddingHorizontal: px,
|
|
68
|
+
paddingHorizontal: isIconOnly ? spacing.xs : px,
|
|
66
69
|
borderRadius: radius,
|
|
67
70
|
opacity: disabled ? 0.4 : 1,
|
|
71
|
+
// Icon-only: square with equal xs padding on both axes.
|
|
72
|
+
...(isIconOnly && { aspectRatio: 1, paddingVertical: spacing.xs }),
|
|
68
73
|
...(variant === 'default' && { backgroundColor: primaryColor }),
|
|
69
74
|
...(isOutline && {
|
|
70
75
|
backgroundColor: 'transparent',
|
|
@@ -86,9 +91,13 @@ export function Button({
|
|
|
86
91
|
>
|
|
87
92
|
{iconStart && <View importantForAccessibility="no-hide-descendants" accessibilityElementsHidden={true}>{iconStart}</View>}
|
|
88
93
|
{children && (
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
94
|
+
isIconOnly
|
|
95
|
+
? children
|
|
96
|
+
: (
|
|
97
|
+
<Text style={[styles.label, { fontSize, color: textColor }]}>
|
|
98
|
+
{children}
|
|
99
|
+
</Text>
|
|
100
|
+
)
|
|
92
101
|
)}
|
|
93
102
|
{iconEnd && <View importantForAccessibility="no-hide-descendants">{iconEnd}</View>}
|
|
94
103
|
</Pressable>
|
|
@@ -47,4 +47,20 @@ describe('Button', () => {
|
|
|
47
47
|
render(<Button iconEnd={<span data-testid="icon">★</span>}>Label</Button>);
|
|
48
48
|
expect(screen.getByTestId('icon')).toBeInTheDocument();
|
|
49
49
|
});
|
|
50
|
+
|
|
51
|
+
it('applies --iconOnly class when isIconOnly is true', () => {
|
|
52
|
+
render(
|
|
53
|
+
<Button isIconOnly aria-label="Close">
|
|
54
|
+
<span data-testid="icon">×</span>
|
|
55
|
+
</Button>
|
|
56
|
+
);
|
|
57
|
+
expect(screen.getByRole('button', { name: 'Close' })).toBeInTheDocument();
|
|
58
|
+
expect(screen.getByTestId('icon')).toBeInTheDocument();
|
|
59
|
+
expect(screen.getByRole('button').className).toContain('--iconOnly');
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('does not apply --iconOnly class by default', () => {
|
|
63
|
+
render(<Button>Click</Button>);
|
|
64
|
+
expect(screen.getByRole('button').className).not.toContain('--iconOnly');
|
|
65
|
+
});
|
|
50
66
|
});
|
|
@@ -1,42 +1,160 @@
|
|
|
1
1
|
import type { Meta, StoryObj } from '@storybook/react';
|
|
2
|
-
import { Button } from './Button';
|
|
2
|
+
import { Button, type ButtonProps } from './Button';
|
|
3
3
|
|
|
4
|
-
const
|
|
4
|
+
const CheckIcon = (
|
|
5
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="1em" height="1em" aria-hidden>
|
|
6
|
+
<path d="M12 0a12 12 0 1 0 12 12A12 12 0 0 0 12 0Zm6.93 8.2-6.85 9.29a1 1 0 0 1-1.43.19l-4.89-3.91a1 1 0 0 1-.15-1.41A1 1 0 0 1 7 12.21l4.08 3.26L17.32 7a1 1 0 0 1 1.39-.21 1 1 0 0 1 .22 1.41Z"/>
|
|
7
|
+
</svg>
|
|
8
|
+
);
|
|
9
|
+
|
|
10
|
+
const ArrowRight = <span aria-hidden>→</span>;
|
|
11
|
+
const ArrowLeft = <span aria-hidden>←</span>;
|
|
12
|
+
|
|
13
|
+
const iconMap = {
|
|
14
|
+
none: undefined,
|
|
15
|
+
check: CheckIcon,
|
|
16
|
+
arrowRight: ArrowRight,
|
|
17
|
+
arrowLeft: ArrowLeft,
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
type IconKey = keyof typeof iconMap;
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Story-local args: `iconStart`/`iconEnd` are narrowed to the icon keys
|
|
24
|
+
* (`'check' | 'arrowRight' | …`) so the Controls panel stays in sync with
|
|
25
|
+
* the rendered icon. Storybook's `mapping` resolves the key → ReactNode at
|
|
26
|
+
* render time, so the prop still receives a real ReactNode downstream.
|
|
27
|
+
*/
|
|
28
|
+
type ButtonStoryArgs = Omit<ButtonProps, 'iconStart' | 'iconEnd'> & {
|
|
29
|
+
iconStart?: IconKey;
|
|
30
|
+
iconEnd?: IconKey;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
const meta: Meta<ButtonStoryArgs> = {
|
|
5
34
|
title: 'React/Interaction/Button',
|
|
6
35
|
component: Button,
|
|
7
36
|
argTypes: {
|
|
8
|
-
variant: {
|
|
9
|
-
|
|
37
|
+
variant: {
|
|
38
|
+
control: 'select',
|
|
39
|
+
options: ['default', 'outline'],
|
|
40
|
+
description: 'Visual style. `default` is filled; `outline` is transparent with a border.',
|
|
41
|
+
table: { defaultValue: { summary: 'default' } },
|
|
42
|
+
},
|
|
43
|
+
size: {
|
|
44
|
+
control: 'select',
|
|
45
|
+
options: ['small', 'medium', 'large'],
|
|
46
|
+
description: 'Height + padding. `small` is 2.25em, `medium` is 3em, `large` bumps font-size to the h3 scale.',
|
|
47
|
+
table: { defaultValue: { summary: 'medium' } },
|
|
48
|
+
},
|
|
49
|
+
iconStart: {
|
|
50
|
+
control: 'select',
|
|
51
|
+
options: Object.keys(iconMap) as IconKey[],
|
|
52
|
+
mapping: iconMap,
|
|
53
|
+
description: 'Icon rendered before the label.',
|
|
54
|
+
},
|
|
55
|
+
iconEnd: {
|
|
56
|
+
control: 'select',
|
|
57
|
+
options: Object.keys(iconMap) as IconKey[],
|
|
58
|
+
mapping: iconMap,
|
|
59
|
+
description: 'Icon rendered after the label.',
|
|
60
|
+
},
|
|
61
|
+
isIconOnly: {
|
|
62
|
+
control: 'boolean',
|
|
63
|
+
description: 'Forces a 1:1 aspect ratio with minimal padding. Composable with any `variant`. Requires `aria-label`.',
|
|
64
|
+
table: { defaultValue: { summary: 'false' } },
|
|
65
|
+
},
|
|
66
|
+
hasPulse: {
|
|
67
|
+
control: 'boolean',
|
|
68
|
+
description: 'Adds a pulsing animation for CTA emphasis. Respects `prefers-reduced-motion`.',
|
|
69
|
+
table: { defaultValue: { summary: 'false' } },
|
|
70
|
+
},
|
|
71
|
+
disabled: {
|
|
72
|
+
control: 'boolean',
|
|
73
|
+
description: 'Blocks interaction, sets `aria-disabled`, and removes the element from the tab order.',
|
|
74
|
+
table: { defaultValue: { summary: 'false' } },
|
|
75
|
+
},
|
|
76
|
+
href: {
|
|
77
|
+
control: 'text',
|
|
78
|
+
description: 'When set, renders as `<a>` pointing to this URL instead of `<button>`.',
|
|
79
|
+
},
|
|
80
|
+
'aria-label': {
|
|
81
|
+
control: 'text',
|
|
82
|
+
description: 'Accessible label. Required for icon-only buttons.',
|
|
83
|
+
},
|
|
10
84
|
onClick: { action: 'clicked' },
|
|
11
85
|
},
|
|
12
|
-
|
|
86
|
+
args: { variant: 'default', size: 'medium' },
|
|
87
|
+
};
|
|
13
88
|
|
|
14
89
|
export default meta;
|
|
15
|
-
type Story = StoryObj<
|
|
90
|
+
type Story = StoryObj<ButtonStoryArgs>;
|
|
91
|
+
|
|
92
|
+
export const Playground: Story = {
|
|
93
|
+
args: {
|
|
94
|
+
children: 'Button',
|
|
95
|
+
iconStart: 'none',
|
|
96
|
+
iconEnd: 'none',
|
|
97
|
+
},
|
|
98
|
+
};
|
|
16
99
|
|
|
17
|
-
export const Default: Story = { args: { children: 'Default'
|
|
18
|
-
export const
|
|
100
|
+
export const Default: Story = { args: { children: 'Default' } };
|
|
101
|
+
export const Outline: Story = { args: { children: 'Outline', variant: 'outline' } };
|
|
19
102
|
export const Small: Story = { args: { children: 'Small', size: 'small' } };
|
|
20
103
|
export const Medium: Story = { args: { children: 'Medium', size: 'medium' } };
|
|
21
104
|
export const Large: Story = { args: { children: 'Large', size: 'large' } };
|
|
22
105
|
export const AsLink: Story = { args: { children: 'Link Button', href: 'https://example.com' } };
|
|
23
106
|
export const WithPulse: Story = { args: { children: 'Pulsing', hasPulse: true } };
|
|
24
107
|
export const Disabled: Story = { args: { children: 'Disabled', disabled: true } };
|
|
25
|
-
|
|
108
|
+
|
|
109
|
+
export const WithIconStart: Story = { args: { children: 'Confirm', iconStart: 'check' } };
|
|
110
|
+
export const WithIconEnd: Story = { args: { children: 'Continue', iconEnd: 'arrowRight' } };
|
|
111
|
+
export const WithBothIcons: Story = { args: { children: 'Both', iconStart: 'arrowLeft', iconEnd: 'arrowRight' } };
|
|
112
|
+
|
|
113
|
+
export const IconOnly: Story = {
|
|
26
114
|
args: {
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
115
|
+
isIconOnly: true,
|
|
116
|
+
'aria-label': 'Confirm',
|
|
117
|
+
children: CheckIcon,
|
|
30
118
|
},
|
|
31
119
|
};
|
|
120
|
+
|
|
121
|
+
export const IconOnlyOutline: Story = {
|
|
122
|
+
args: {
|
|
123
|
+
variant: 'outline',
|
|
124
|
+
isIconOnly: true,
|
|
125
|
+
'aria-label': 'Confirm',
|
|
126
|
+
children: CheckIcon,
|
|
127
|
+
},
|
|
128
|
+
};
|
|
129
|
+
|
|
32
130
|
export const AllVariants: Story = {
|
|
33
131
|
render: () => (
|
|
34
132
|
<div style={{ display: 'flex', gap: '12px', flexWrap: 'wrap', alignItems: 'center' }}>
|
|
35
|
-
{(['default', '
|
|
36
|
-
['small', 'medium', 'large'].map(s => (
|
|
37
|
-
<Button key={`${v}-${s}`} variant={v} size={s
|
|
133
|
+
{(['default', 'outline'] as const).map(v =>
|
|
134
|
+
(['small', 'medium', 'large'] as const).map(s => (
|
|
135
|
+
<Button key={`${v}-${s}`} variant={v} size={s}>{v} {s}</Button>
|
|
136
|
+
))
|
|
137
|
+
)}
|
|
138
|
+
</div>
|
|
139
|
+
),
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
export const AllIconOnly: Story = {
|
|
143
|
+
render: () => (
|
|
144
|
+
<div style={{ display: 'flex', gap: '12px', flexWrap: 'wrap', alignItems: 'center' }}>
|
|
145
|
+
{(['default', 'outline'] as const).map(v =>
|
|
146
|
+
(['small', 'medium', 'large'] as const).map(s => (
|
|
147
|
+
<Button
|
|
148
|
+
key={`${v}-${s}`}
|
|
149
|
+
variant={v}
|
|
150
|
+
size={s}
|
|
151
|
+
isIconOnly
|
|
152
|
+
aria-label={`${v} ${s}`}
|
|
153
|
+
>
|
|
154
|
+
{CheckIcon}
|
|
155
|
+
</Button>
|
|
38
156
|
))
|
|
39
|
-
)
|
|
157
|
+
)}
|
|
40
158
|
</div>
|
|
41
159
|
),
|
|
42
160
|
};
|
|
@@ -3,20 +3,50 @@ import css from './Button.module.css';
|
|
|
3
3
|
import { cn } from '@boostdev/design-system-foundation';
|
|
4
4
|
import type { WithClassName } from '../../../types';
|
|
5
5
|
|
|
6
|
+
/**
|
|
7
|
+
* Props for the Button component.
|
|
8
|
+
*
|
|
9
|
+
* Button spreads all unrecognised props onto the underlying element —
|
|
10
|
+
* `<button>` when `href` is absent, `<a>` when `href` is provided —
|
|
11
|
+
* so every standard HTML attribute for that element is accepted.
|
|
12
|
+
*
|
|
13
|
+
* Notably, this means `type`, `form`, `name`, `value`, `autoFocus`, all
|
|
14
|
+
* `aria-*`, all `data-*`, and every DOM event handler are valid props.
|
|
15
|
+
*/
|
|
6
16
|
export interface ButtonProps extends WithClassName, ButtonHTMLAttributes<HTMLButtonElement | HTMLAnchorElement> {
|
|
17
|
+
/** When provided, the button renders as an `<a>` element pointing to this URL. */
|
|
7
18
|
href?: string;
|
|
19
|
+
/** Visual style. `'default'` is filled; `'outline'` is transparent with a border. `'ghost'` is a deprecated alias for `'outline'`. */
|
|
8
20
|
variant?: 'default' | 'outline' | /** @deprecated Use "outline" instead */ 'ghost';
|
|
21
|
+
/**
|
|
22
|
+
* Button behaviour within a form. Defaults to `'button'` (no form submission).
|
|
23
|
+
* Set to `'submit'` to trigger the parent `<form>`'s submit handler on click or Enter;
|
|
24
|
+
* `'reset'` to reset the form. Ignored when rendering as `<a>` (i.e. when `href` is set).
|
|
25
|
+
*/
|
|
9
26
|
type?: 'button' | 'submit' | 'reset';
|
|
27
|
+
/** Control height and padding. Defaults to `'medium'`. */
|
|
10
28
|
size?: 'small' | 'medium' | 'large';
|
|
29
|
+
/** Icon rendered before the label. */
|
|
11
30
|
iconStart?: ReactNode;
|
|
31
|
+
/** Icon rendered after the label. */
|
|
12
32
|
iconEnd?: ReactNode;
|
|
33
|
+
/** Label content. Required unless `aria-label` is set (icon-only button). */
|
|
13
34
|
children?: ReactNode;
|
|
35
|
+
/** When true, blocks interaction, sets `aria-disabled`, and removes the element from the tab order. */
|
|
14
36
|
disabled?: boolean;
|
|
37
|
+
/** Adds a pulsing animation for call-to-action emphasis. Respects `prefers-reduced-motion`. */
|
|
15
38
|
hasPulse?: boolean;
|
|
39
|
+
/** When true, forces a 1:1 aspect ratio with equal `xs` padding on both axes. Pass the icon as `children`; composable with any `variant`. Requires `aria-label`. */
|
|
40
|
+
isIconOnly?: boolean;
|
|
41
|
+
/** Click handler. Typed as `HTMLElement` because the root may be `<button>` or `<a>`. */
|
|
16
42
|
onClick?: MouseEventHandler<HTMLElement>;
|
|
43
|
+
/** Anchor target (only applied when rendered as `<a>`). */
|
|
17
44
|
target?: string;
|
|
45
|
+
/** Anchor rel (only applied when rendered as `<a>`). */
|
|
18
46
|
rel?: string;
|
|
47
|
+
/** Accessible label. Required for icon-only buttons (no text children). */
|
|
19
48
|
'aria-label'?: string;
|
|
49
|
+
/** ID(s) of element(s) that describe the button (e.g. a tooltip or helper text). */
|
|
20
50
|
'aria-describedby'?: string;
|
|
21
51
|
}
|
|
22
52
|
|
|
@@ -29,6 +59,7 @@ export function Button({
|
|
|
29
59
|
iconEnd,
|
|
30
60
|
size = 'medium',
|
|
31
61
|
hasPulse = false,
|
|
62
|
+
isIconOnly = false,
|
|
32
63
|
href,
|
|
33
64
|
target,
|
|
34
65
|
rel,
|
|
@@ -41,6 +72,7 @@ export function Button({
|
|
|
41
72
|
css[`--${variant}`],
|
|
42
73
|
css[`--size_${size}`],
|
|
43
74
|
hasPulse && css['--hasPulse'],
|
|
75
|
+
isIconOnly && css['--iconOnly'],
|
|
44
76
|
className,
|
|
45
77
|
);
|
|
46
78
|
|
|
@@ -12,7 +12,7 @@ A modal dialog overlay that centers content on screen with a semi-transparent ba
|
|
|
12
12
|
## Usage
|
|
13
13
|
|
|
14
14
|
```tsx
|
|
15
|
-
import { Dialog } from '@boostdev/components';
|
|
15
|
+
import { Dialog } from '@boostdev/design-system-components';
|
|
16
16
|
|
|
17
17
|
const [open, setOpen] = useState(false);
|
|
18
18
|
|
|
@@ -25,7 +25,7 @@ const [open, setOpen] = useState(false);
|
|
|
25
25
|
Wrap your tree in `ThemeProvider` (once, at the app root):
|
|
26
26
|
|
|
27
27
|
```tsx
|
|
28
|
-
import { ThemeProvider } from '@boostdev/components/native/ThemeContext';
|
|
28
|
+
import { ThemeProvider } from '@boostdev/design-system-components/native/ThemeContext';
|
|
29
29
|
|
|
30
30
|
export default function App() {
|
|
31
31
|
return <ThemeProvider><YourApp /></ThemeProvider>;
|
|
@@ -12,7 +12,7 @@ A sliding panel that slides in from the bottom of the screen. Built on React Nat
|
|
|
12
12
|
## Usage
|
|
13
13
|
|
|
14
14
|
```tsx
|
|
15
|
-
import { Drawer } from '@boostdev/components';
|
|
15
|
+
import { Drawer } from '@boostdev/design-system-components';
|
|
16
16
|
|
|
17
17
|
const [open, setOpen] = useState(false);
|
|
18
18
|
|
|
@@ -25,7 +25,7 @@ const [open, setOpen] = useState(false);
|
|
|
25
25
|
Wrap your tree in `ThemeProvider` (once, at the app root):
|
|
26
26
|
|
|
27
27
|
```tsx
|
|
28
|
-
import { ThemeProvider } from '@boostdev/components/native/ThemeContext';
|
|
28
|
+
import { ThemeProvider } from '@boostdev/design-system-components/native/ThemeContext';
|
|
29
29
|
|
|
30
30
|
export default function App() {
|
|
31
31
|
return <ThemeProvider><YourApp /></ThemeProvider>;
|
|
@@ -12,7 +12,7 @@ A dropdown menu that opens as a bottom sheet modal on mobile. Accepts a trigger
|
|
|
12
12
|
## Usage
|
|
13
13
|
|
|
14
14
|
```tsx
|
|
15
|
-
import { DropdownMenu } from '@boostdev/components';
|
|
15
|
+
import { DropdownMenu } from '@boostdev/design-system-components';
|
|
16
16
|
|
|
17
17
|
<DropdownMenu
|
|
18
18
|
trigger={<Button>Actions</Button>}
|
|
@@ -27,7 +27,7 @@ import { DropdownMenu } from '@boostdev/components';
|
|
|
27
27
|
Wrap your tree in `ThemeProvider` (once, at the app root):
|
|
28
28
|
|
|
29
29
|
```tsx
|
|
30
|
-
import { ThemeProvider } from '@boostdev/components/native/ThemeContext';
|
|
30
|
+
import { ThemeProvider } from '@boostdev/design-system-components/native/ThemeContext';
|
|
31
31
|
|
|
32
32
|
export default function App() {
|
|
33
33
|
return <ThemeProvider><YourApp /></ThemeProvider>;
|
|
@@ -12,7 +12,7 @@ A popover overlay that displays content in a centered modal. The trigger element
|
|
|
12
12
|
## Usage
|
|
13
13
|
|
|
14
14
|
```tsx
|
|
15
|
-
import { Popover } from '@boostdev/components';
|
|
15
|
+
import { Popover } from '@boostdev/design-system-components';
|
|
16
16
|
|
|
17
17
|
<Popover
|
|
18
18
|
content={<Text>Additional information here.</Text>}
|
|
@@ -25,7 +25,7 @@ import { Popover } from '@boostdev/components';
|
|
|
25
25
|
Wrap your tree in `ThemeProvider` (once, at the app root):
|
|
26
26
|
|
|
27
27
|
```tsx
|
|
28
|
-
import { ThemeProvider } from '@boostdev/components/native/ThemeContext';
|
|
28
|
+
import { ThemeProvider } from '@boostdev/design-system-components/native/ThemeContext';
|
|
29
29
|
|
|
30
30
|
export default function App() {
|
|
31
31
|
return <ThemeProvider><YourApp /></ThemeProvider>;
|
|
@@ -12,7 +12,7 @@ React Native read-only star rating display using text characters. Shows filled a
|
|
|
12
12
|
## Usage
|
|
13
13
|
|
|
14
14
|
```tsx
|
|
15
|
-
import { Rating } from '@boostdev/components';
|
|
15
|
+
import { Rating } from '@boostdev/design-system-components';
|
|
16
16
|
|
|
17
17
|
<Rating value={4} />
|
|
18
18
|
<Rating value={7} max={10} />
|
|
@@ -14,8 +14,8 @@ A toast notification system using React context. Provides `ToastProvider` and `u
|
|
|
14
14
|
Wrap your app root in `ToastProvider`:
|
|
15
15
|
|
|
16
16
|
```tsx
|
|
17
|
-
import { ToastProvider } from '@boostdev/components';
|
|
18
|
-
import { ThemeProvider } from '@boostdev/components/native/ThemeContext';
|
|
17
|
+
import { ToastProvider } from '@boostdev/design-system-components';
|
|
18
|
+
import { ThemeProvider } from '@boostdev/design-system-components/native/ThemeContext';
|
|
19
19
|
|
|
20
20
|
export default function App() {
|
|
21
21
|
return (
|
|
@@ -31,7 +31,7 @@ export default function App() {
|
|
|
31
31
|
Then use `useToast` in any descendant:
|
|
32
32
|
|
|
33
33
|
```tsx
|
|
34
|
-
import { useToast } from '@boostdev/components';
|
|
34
|
+
import { useToast } from '@boostdev/design-system-components';
|
|
35
35
|
|
|
36
36
|
function SaveButton() {
|
|
37
37
|
const { showToast } = useToast();
|
|
@@ -12,7 +12,7 @@ React Native checkbox component with label, optional hint, and error state. Full
|
|
|
12
12
|
## Usage
|
|
13
13
|
|
|
14
14
|
```tsx
|
|
15
|
-
import { Checkbox } from '@boostdev/components';
|
|
15
|
+
import { Checkbox } from '@boostdev/design-system-components';
|
|
16
16
|
import { useState } from 'react';
|
|
17
17
|
|
|
18
18
|
function Example() {
|
|
@@ -12,7 +12,7 @@ Groups related checkboxes under a common legend with optional hint and error mes
|
|
|
12
12
|
## Usage
|
|
13
13
|
|
|
14
14
|
```tsx
|
|
15
|
-
import { CheckboxGroup, Checkbox } from '@boostdev/components';
|
|
15
|
+
import { CheckboxGroup, Checkbox } from '@boostdev/design-system-components';
|
|
16
16
|
import { useState } from 'react';
|
|
17
17
|
|
|
18
18
|
function Example() {
|