@oxyhq/bloom 0.9.1 → 0.11.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/lib/commonjs/admonition/index.js +2 -2
- package/lib/commonjs/admonition/index.js.map +1 -1
- package/lib/commonjs/alert-dialog/AlertDialog.js +5 -5
- package/lib/commonjs/alert-dialog/AlertDialog.js.map +1 -1
- package/lib/commonjs/button/Button.js +71 -12
- package/lib/commonjs/button/Button.js.map +1 -1
- package/lib/commonjs/button/Button.web.js +431 -0
- package/lib/commonjs/button/Button.web.js.map +1 -0
- package/lib/commonjs/button/index.js +25 -1
- package/lib/commonjs/button/index.js.map +1 -1
- package/lib/commonjs/button/index.web.js +67 -0
- package/lib/commonjs/button/index.web.js.map +1 -0
- package/lib/commonjs/index.js +70 -70
- package/lib/commonjs/index.js.map +1 -1
- package/lib/commonjs/index.web.js +84 -84
- package/lib/commonjs/index.web.js.map +1 -1
- package/lib/commonjs/menu/index.js +2 -2
- package/lib/commonjs/menu/index.js.map +1 -1
- package/lib/commonjs/search-input/index.js +2 -2
- package/lib/commonjs/search-input/index.js.map +1 -1
- package/lib/commonjs/select/index.js +4 -4
- package/lib/commonjs/select/index.js.map +1 -1
- package/lib/module/admonition/index.js +1 -1
- package/lib/module/admonition/index.js.map +1 -1
- package/lib/module/alert-dialog/AlertDialog.js +1 -1
- package/lib/module/alert-dialog/AlertDialog.js.map +1 -1
- package/lib/module/button/Button.js +70 -11
- package/lib/module/button/Button.js.map +1 -1
- package/lib/module/button/Button.web.js +426 -0
- package/lib/module/button/Button.web.js.map +1 -0
- package/lib/module/button/index.js +1 -1
- package/lib/module/button/index.js.map +1 -1
- package/lib/module/button/index.web.js +17 -0
- package/lib/module/button/index.web.js.map +1 -0
- package/lib/module/index.js +1 -1
- package/lib/module/index.js.map +1 -1
- package/lib/module/index.web.js +1 -1
- package/lib/module/index.web.js.map +1 -1
- package/lib/module/menu/index.js +1 -1
- package/lib/module/menu/index.js.map +1 -1
- package/lib/module/search-input/index.js +1 -1
- package/lib/module/search-input/index.js.map +1 -1
- package/lib/module/select/index.js +1 -1
- package/lib/module/select/index.js.map +1 -1
- package/lib/typescript/commonjs/button/Button.d.ts +3 -0
- package/lib/typescript/commonjs/button/Button.d.ts.map +1 -1
- package/lib/typescript/commonjs/button/Button.web.d.ts +14 -0
- package/lib/typescript/commonjs/button/Button.web.d.ts.map +1 -0
- package/lib/typescript/commonjs/button/index.d.ts +1 -1
- package/lib/typescript/commonjs/button/index.d.ts.map +1 -1
- package/lib/typescript/commonjs/button/index.web.d.ts +3 -0
- package/lib/typescript/commonjs/button/index.web.d.ts.map +1 -0
- package/lib/typescript/commonjs/button/types.d.ts +72 -2
- package/lib/typescript/commonjs/button/types.d.ts.map +1 -1
- package/lib/typescript/commonjs/index.web.d.ts +1 -1
- package/lib/typescript/commonjs/index.web.d.ts.map +1 -1
- package/lib/typescript/module/button/Button.d.ts +3 -0
- package/lib/typescript/module/button/Button.d.ts.map +1 -1
- package/lib/typescript/module/button/Button.web.d.ts +14 -0
- package/lib/typescript/module/button/Button.web.d.ts.map +1 -0
- package/lib/typescript/module/button/index.d.ts +1 -1
- package/lib/typescript/module/button/index.d.ts.map +1 -1
- package/lib/typescript/module/button/index.web.d.ts +3 -0
- package/lib/typescript/module/button/index.web.d.ts.map +1 -0
- package/lib/typescript/module/button/types.d.ts +72 -2
- package/lib/typescript/module/button/types.d.ts.map +1 -1
- package/lib/typescript/module/index.web.d.ts +1 -1
- package/lib/typescript/module/index.web.d.ts.map +1 -1
- package/package.json +8 -1
- package/src/__tests__/Button.web.test.tsx +201 -0
- package/src/button/Button.tsx +77 -12
- package/src/button/Button.web.tsx +472 -0
- package/src/button/index.ts +4 -0
- package/src/button/index.web.ts +27 -0
- package/src/button/types.ts +98 -2
- package/src/index.web.ts +1 -1
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @jest-environment jsdom
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import React from 'react';
|
|
6
|
+
import { act } from 'react';
|
|
7
|
+
import { createRoot, type Root } from 'react-dom/client';
|
|
8
|
+
import { getByRole, getByText, getByLabelText, fireEvent } from '@testing-library/dom';
|
|
9
|
+
import '@testing-library/jest-dom';
|
|
10
|
+
|
|
11
|
+
import { BloomThemeProvider } from '../theme/BloomThemeProvider';
|
|
12
|
+
import {
|
|
13
|
+
Button,
|
|
14
|
+
OutlineButton,
|
|
15
|
+
LinkButton,
|
|
16
|
+
DestructiveButton,
|
|
17
|
+
} from '../button/Button.web';
|
|
18
|
+
|
|
19
|
+
// react-dom 19 logs a guard unless this flag is set in test environments.
|
|
20
|
+
(globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
|
|
21
|
+
|
|
22
|
+
let container: HTMLDivElement;
|
|
23
|
+
let root: Root;
|
|
24
|
+
|
|
25
|
+
function mount(ui: React.ReactElement): HTMLElement {
|
|
26
|
+
act(() => {
|
|
27
|
+
root.render(
|
|
28
|
+
<BloomThemeProvider mode="light" colorPreset="teal">
|
|
29
|
+
{ui}
|
|
30
|
+
</BloomThemeProvider>,
|
|
31
|
+
);
|
|
32
|
+
});
|
|
33
|
+
return container;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
beforeEach(() => {
|
|
37
|
+
container = document.createElement('div');
|
|
38
|
+
document.body.appendChild(container);
|
|
39
|
+
root = createRoot(container);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
afterEach(() => {
|
|
43
|
+
act(() => root.unmount());
|
|
44
|
+
container.remove();
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
describe('Button.web', () => {
|
|
48
|
+
it('renders a real <button> element', () => {
|
|
49
|
+
const c = mount(<Button>Click me</Button>);
|
|
50
|
+
const btn = getByRole(c, 'button', { name: 'Click me' });
|
|
51
|
+
expect(btn.tagName).toBe('BUTTON');
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('defaults to type="button"', () => {
|
|
55
|
+
const c = mount(<Button>Default</Button>);
|
|
56
|
+
expect(getByRole(c, 'button')).toHaveAttribute('type', 'button');
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it('supports type="submit" for form participation', () => {
|
|
60
|
+
const onSubmit = jest.fn((e: Event) => e.preventDefault());
|
|
61
|
+
const c = mount(
|
|
62
|
+
<form onSubmit={onSubmit as unknown as React.FormEventHandler}>
|
|
63
|
+
<Button type="submit">Submit</Button>
|
|
64
|
+
</form>,
|
|
65
|
+
);
|
|
66
|
+
const btn = getByRole(c, 'button', { name: 'Submit' });
|
|
67
|
+
expect(btn).toHaveAttribute('type', 'submit');
|
|
68
|
+
act(() => {
|
|
69
|
+
fireEvent.click(btn);
|
|
70
|
+
});
|
|
71
|
+
expect(onSubmit).toHaveBeenCalledTimes(1);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it('fires both onClick and onPress on click', () => {
|
|
75
|
+
const onClick = jest.fn();
|
|
76
|
+
const onPress = jest.fn();
|
|
77
|
+
const c = mount(
|
|
78
|
+
<Button onClick={onClick} onPress={onPress}>
|
|
79
|
+
Go
|
|
80
|
+
</Button>,
|
|
81
|
+
);
|
|
82
|
+
act(() => {
|
|
83
|
+
fireEvent.click(getByRole(c, 'button'));
|
|
84
|
+
});
|
|
85
|
+
expect(onClick).toHaveBeenCalledTimes(1);
|
|
86
|
+
expect(onPress).toHaveBeenCalledTimes(1);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it('does not fire handlers when disabled', () => {
|
|
90
|
+
const onPress = jest.fn();
|
|
91
|
+
const c = mount(
|
|
92
|
+
<Button disabled onPress={onPress}>
|
|
93
|
+
Nope
|
|
94
|
+
</Button>,
|
|
95
|
+
);
|
|
96
|
+
const btn = getByRole(c, 'button');
|
|
97
|
+
expect(btn).toBeDisabled();
|
|
98
|
+
act(() => {
|
|
99
|
+
fireEvent.click(btn);
|
|
100
|
+
});
|
|
101
|
+
expect(onPress).not.toHaveBeenCalled();
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it('does not fire onPress when loading and marks aria-busy', () => {
|
|
105
|
+
const onPress = jest.fn();
|
|
106
|
+
const c = mount(
|
|
107
|
+
<Button loading onPress={onPress}>
|
|
108
|
+
Saving
|
|
109
|
+
</Button>,
|
|
110
|
+
);
|
|
111
|
+
const btn = getByRole(c, 'button');
|
|
112
|
+
expect(btn).toHaveAttribute('aria-busy', 'true');
|
|
113
|
+
act(() => {
|
|
114
|
+
fireEvent.click(btn);
|
|
115
|
+
});
|
|
116
|
+
expect(onPress).not.toHaveBeenCalled();
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it('keeps children mounted while loading so width is preserved', () => {
|
|
120
|
+
const c = mount(<Button loading>Submit</Button>);
|
|
121
|
+
expect(getByText(c, 'Submit')).toBeTruthy();
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it('passes className through so consumer layout classes win', () => {
|
|
125
|
+
const c = mount(<Button className="w-full custom">Wide</Button>);
|
|
126
|
+
const btn = getByRole(c, 'button');
|
|
127
|
+
expect(btn).toHaveClass('bloom-btn');
|
|
128
|
+
expect(btn).toHaveClass('w-full');
|
|
129
|
+
expect(btn).toHaveClass('custom');
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it('applies aria-label from accessibilityLabel', () => {
|
|
133
|
+
const c = mount(<Button accessibilityLabel="Save changes">Save</Button>);
|
|
134
|
+
expect(getByLabelText(c, 'Save changes')).toBeTruthy();
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it('exposes testID as data-testid', () => {
|
|
138
|
+
const c = mount(<Button testID="my-btn">Test</Button>);
|
|
139
|
+
expect(c.querySelector('[data-testid="my-btn"]')?.tagName).toBe('BUTTON');
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
describe('asChild', () => {
|
|
143
|
+
it('renders the child element (anchor) instead of a button', () => {
|
|
144
|
+
const c = mount(
|
|
145
|
+
<Button asChild>
|
|
146
|
+
<a href="/go">Go</a>
|
|
147
|
+
</Button>,
|
|
148
|
+
);
|
|
149
|
+
const link = getByRole(c, 'link', { name: 'Go' });
|
|
150
|
+
expect(link.tagName).toBe('A');
|
|
151
|
+
expect(link).toHaveAttribute('href', '/go');
|
|
152
|
+
expect(link).toHaveClass('bloom-btn');
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
it('merges onPress onto the child', () => {
|
|
156
|
+
const onPress = jest.fn();
|
|
157
|
+
const c = mount(
|
|
158
|
+
<Button asChild onPress={onPress}>
|
|
159
|
+
<a href="/go">Go</a>
|
|
160
|
+
</Button>,
|
|
161
|
+
);
|
|
162
|
+
act(() => {
|
|
163
|
+
fireEvent.click(getByRole(c, 'link'));
|
|
164
|
+
});
|
|
165
|
+
expect(onPress).toHaveBeenCalledTimes(1);
|
|
166
|
+
});
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
describe('web variants', () => {
|
|
170
|
+
it('OutlineButton renders a button', () => {
|
|
171
|
+
const c = mount(<OutlineButton>Outline</OutlineButton>);
|
|
172
|
+
expect(getByRole(c, 'button', { name: 'Outline' }).tagName).toBe('BUTTON');
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
it('LinkButton renders with the link modifier class', () => {
|
|
176
|
+
const c = mount(<LinkButton>Link</LinkButton>);
|
|
177
|
+
expect(getByRole(c, 'button', { name: 'Link' })).toHaveClass('bloom-btn--link');
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
it('DestructiveButton renders a button', () => {
|
|
181
|
+
const c = mount(<DestructiveButton>Delete</DestructiveButton>);
|
|
182
|
+
expect(getByRole(c, 'button', { name: 'Delete' }).tagName).toBe('BUTTON');
|
|
183
|
+
});
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
describe('size aliases', () => {
|
|
187
|
+
it('accepts shadcn-style sm size', () => {
|
|
188
|
+
const c = mount(<Button size="sm">Small</Button>);
|
|
189
|
+
expect(getByRole(c, 'button', { name: 'Small' }).tagName).toBe('BUTTON');
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
it('size="icon" selects the icon variant', () => {
|
|
193
|
+
const c = mount(
|
|
194
|
+
<Button size="icon" accessibilityLabel="icon">
|
|
195
|
+
x
|
|
196
|
+
</Button>,
|
|
197
|
+
);
|
|
198
|
+
expect(getByRole(c, 'button', { name: 'icon' }).tagName).toBe('BUTTON');
|
|
199
|
+
});
|
|
200
|
+
});
|
|
201
|
+
});
|
package/src/button/Button.tsx
CHANGED
|
@@ -14,10 +14,30 @@ import {
|
|
|
14
14
|
import { useTheme } from '../theme/use-theme';
|
|
15
15
|
import { usePressAnimation } from '../hooks/usePressAnimation';
|
|
16
16
|
import { useInteractionState } from '../hooks/useInteractionState';
|
|
17
|
-
import type { ButtonProps } from './types';
|
|
17
|
+
import type { ButtonProps, ButtonSize, ButtonVariant } from './types';
|
|
18
18
|
|
|
19
19
|
export type { ButtonProps, ButtonVariant, ButtonSize } from './types';
|
|
20
20
|
|
|
21
|
+
/**
|
|
22
|
+
* The shared {@link ButtonVariant}/{@link ButtonSize} types include web-only
|
|
23
|
+
* additions (shadcn variants + size aliases) so a single call site type-checks
|
|
24
|
+
* on both platforms. Native normalizes them to its own primitives:
|
|
25
|
+
* - `outline → secondary`, `link → text`, `destructive → primary` (tinted)
|
|
26
|
+
* - `sm|md|lg|icon` size aliases → `small|medium|large`
|
|
27
|
+
*/
|
|
28
|
+
type NativeVariant = 'primary' | 'secondary' | 'inverse' | 'icon' | 'ghost' | 'text';
|
|
29
|
+
type NativeSize = 'small' | 'medium' | 'large';
|
|
30
|
+
|
|
31
|
+
const SIZE_ALIAS: Record<ButtonSize, NativeSize> = {
|
|
32
|
+
small: 'small',
|
|
33
|
+
medium: 'medium',
|
|
34
|
+
large: 'large',
|
|
35
|
+
sm: 'small',
|
|
36
|
+
md: 'medium',
|
|
37
|
+
lg: 'large',
|
|
38
|
+
icon: 'medium',
|
|
39
|
+
};
|
|
40
|
+
|
|
21
41
|
const SIZE_CONFIG = {
|
|
22
42
|
small: {
|
|
23
43
|
paddingVertical: 6,
|
|
@@ -41,6 +61,11 @@ const SIZE_CONFIG = {
|
|
|
41
61
|
|
|
42
62
|
const ICON_HIT_SLOP = { top: 10, bottom: 10, left: 10, right: 10 } as const;
|
|
43
63
|
|
|
64
|
+
// Fully-rounded (pill/capsule) corner radius. Large enough to stay a perfect
|
|
65
|
+
// capsule at any button height. Shared by every text button variant so they all
|
|
66
|
+
// read as pills; the icon variant keeps its own large radius to stay a circle.
|
|
67
|
+
const PILL_RADIUS = 999;
|
|
68
|
+
|
|
44
69
|
const PRESS_SCALE = 0.97;
|
|
45
70
|
const SCALE_VARIANTS = new Set<string>(['primary', 'secondary', 'inverse']);
|
|
46
71
|
|
|
@@ -48,8 +73,8 @@ const ButtonComponent: React.FC<ButtonProps> = ({
|
|
|
48
73
|
onPress,
|
|
49
74
|
children,
|
|
50
75
|
disabled = false,
|
|
51
|
-
variant = 'primary',
|
|
52
|
-
size = 'medium',
|
|
76
|
+
variant: variantProp = 'primary',
|
|
77
|
+
size: sizeProp = 'medium',
|
|
53
78
|
style,
|
|
54
79
|
textStyle,
|
|
55
80
|
icon,
|
|
@@ -64,6 +89,23 @@ const ButtonComponent: React.FC<ButtonProps> = ({
|
|
|
64
89
|
className,
|
|
65
90
|
}) => {
|
|
66
91
|
const theme = useTheme();
|
|
92
|
+
// The shadcn `size="icon"` shorthand also selects the icon variant unless an
|
|
93
|
+
// explicit variant was given by the caller.
|
|
94
|
+
const variant: NativeVariant = useMemo(() => {
|
|
95
|
+
if (sizeProp === 'icon' && variantProp === 'primary') return 'icon';
|
|
96
|
+
switch (variantProp) {
|
|
97
|
+
case 'outline':
|
|
98
|
+
return 'secondary';
|
|
99
|
+
case 'link':
|
|
100
|
+
return 'text';
|
|
101
|
+
case 'destructive':
|
|
102
|
+
return 'primary';
|
|
103
|
+
default:
|
|
104
|
+
return variantProp;
|
|
105
|
+
}
|
|
106
|
+
}, [variantProp, sizeProp]);
|
|
107
|
+
const isDestructive = variantProp === 'destructive';
|
|
108
|
+
const size: NativeSize = SIZE_ALIAS[sizeProp];
|
|
67
109
|
const hasScaleFeedback = SCALE_VARIANTS.has(variant);
|
|
68
110
|
const isInteractionBlocked = disabled || loading;
|
|
69
111
|
const { scaleAnim, onPressIn: onScalePressIn, onPressOut: onScalePressOut } =
|
|
@@ -112,43 +154,48 @@ const ButtonComponent: React.FC<ButtonProps> = ({
|
|
|
112
154
|
|
|
113
155
|
switch (variant) {
|
|
114
156
|
case 'primary':
|
|
115
|
-
styles.backgroundColor =
|
|
116
|
-
|
|
157
|
+
styles.backgroundColor = isDestructive
|
|
158
|
+
? theme.colors.negative
|
|
159
|
+
: theme.colors.primary;
|
|
160
|
+
styles.borderRadius = PILL_RADIUS;
|
|
117
161
|
break;
|
|
118
162
|
case 'secondary':
|
|
119
163
|
styles.backgroundColor = 'transparent';
|
|
120
164
|
styles.borderWidth = 1;
|
|
121
165
|
styles.borderColor = theme.colors.border;
|
|
122
|
-
styles.borderRadius =
|
|
166
|
+
styles.borderRadius = PILL_RADIUS;
|
|
123
167
|
break;
|
|
124
168
|
case 'inverse':
|
|
125
169
|
styles.backgroundColor = '#FFFFFF';
|
|
126
|
-
styles.borderRadius =
|
|
170
|
+
styles.borderRadius = PILL_RADIUS;
|
|
127
171
|
break;
|
|
128
172
|
case 'icon':
|
|
129
|
-
styles.borderRadius =
|
|
173
|
+
styles.borderRadius = PILL_RADIUS;
|
|
130
174
|
styles.padding = 8;
|
|
131
175
|
styles.width = sizeConfig.minHeight;
|
|
132
176
|
styles.height = sizeConfig.minHeight;
|
|
133
177
|
break;
|
|
134
178
|
case 'ghost':
|
|
135
179
|
styles.backgroundColor = 'transparent';
|
|
136
|
-
styles.borderRadius =
|
|
180
|
+
styles.borderRadius = PILL_RADIUS;
|
|
137
181
|
break;
|
|
138
182
|
case 'text':
|
|
139
183
|
styles.backgroundColor = 'transparent';
|
|
184
|
+
styles.borderRadius = PILL_RADIUS;
|
|
140
185
|
styles.paddingVertical = 4;
|
|
141
186
|
styles.paddingHorizontal = 8;
|
|
142
187
|
break;
|
|
143
188
|
}
|
|
144
189
|
|
|
145
190
|
return styles;
|
|
146
|
-
}, [variant, size, theme]);
|
|
191
|
+
}, [variant, size, theme, isDestructive]);
|
|
147
192
|
|
|
148
193
|
const resolvedTextColor = useMemo((): string => {
|
|
149
194
|
switch (variant) {
|
|
150
195
|
case 'primary':
|
|
151
|
-
return
|
|
196
|
+
return isDestructive
|
|
197
|
+
? theme.colors.negativeForeground
|
|
198
|
+
: theme.colors.primaryForeground;
|
|
152
199
|
case 'secondary':
|
|
153
200
|
return theme.colors.text;
|
|
154
201
|
case 'inverse':
|
|
@@ -160,7 +207,7 @@ const ButtonComponent: React.FC<ButtonProps> = ({
|
|
|
160
207
|
default:
|
|
161
208
|
return theme.colors.text;
|
|
162
209
|
}
|
|
163
|
-
}, [variant, theme]);
|
|
210
|
+
}, [variant, theme, isDestructive]);
|
|
164
211
|
|
|
165
212
|
const computedTextStyle = useMemo((): TextStyle => {
|
|
166
213
|
const sizeConfig = SIZE_CONFIG[size];
|
|
@@ -275,3 +322,21 @@ export const TextButton = memo((props: Omit<ButtonProps, 'variant'>) => (
|
|
|
275
322
|
<Button {...props} variant="text" />
|
|
276
323
|
));
|
|
277
324
|
TextButton.displayName = 'TextButton';
|
|
325
|
+
|
|
326
|
+
// Web/shadcn-aligned variants. On native they normalize to existing primitives
|
|
327
|
+
// (`outline → secondary`, `link → text`, `destructive → primary` tinted with the
|
|
328
|
+
// negative token) inside `Button`, so these stay API-parallel with the web fork.
|
|
329
|
+
export const OutlineButton = memo((props: Omit<ButtonProps, 'variant'>) => (
|
|
330
|
+
<Button {...props} variant="outline" />
|
|
331
|
+
));
|
|
332
|
+
OutlineButton.displayName = 'OutlineButton';
|
|
333
|
+
|
|
334
|
+
export const LinkButton = memo((props: Omit<ButtonProps, 'variant'>) => (
|
|
335
|
+
<Button {...props} variant="link" />
|
|
336
|
+
));
|
|
337
|
+
LinkButton.displayName = 'LinkButton';
|
|
338
|
+
|
|
339
|
+
export const DestructiveButton = memo((props: Omit<ButtonProps, 'variant'>) => (
|
|
340
|
+
<Button {...props} variant="destructive" />
|
|
341
|
+
));
|
|
342
|
+
DestructiveButton.displayName = 'DestructiveButton';
|