@edux-design/buttons 0.0.2 → 0.0.4

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.
@@ -0,0 +1,67 @@
1
+ import React, { useState } from "react";
2
+ import { ButtonGroup, ButtonGroupItem } from "./../index";
3
+ import { Warning } from "@edux-design/icons";
4
+
5
+ export default {
6
+ title: "Components/ButtonGroup",
7
+ component: ButtonGroup,
8
+ parameters: {
9
+ layout: "centered",
10
+ },
11
+ argTypes: {
12
+ size: {
13
+ control: { type: "select" },
14
+ options: ["small", "medium", "large"],
15
+ },
16
+ isStretched: { control: "boolean" },
17
+ },
18
+ };
19
+
20
+ const Template = (args) => {
21
+ const [value, setValue] = useState(args.value ?? args.defaultValue ?? "one");
22
+ return (
23
+ <ButtonGroup
24
+ {...args}
25
+ value={value}
26
+ onChange={(nextValue) => {
27
+ setValue(nextValue);
28
+ args.onChange?.(nextValue);
29
+ }}
30
+ >
31
+ <ButtonGroupItem value="one">Label (11)</ButtonGroupItem>
32
+ <ButtonGroupItem value="two">Label (11)</ButtonGroupItem>
33
+ <ButtonGroupItem
34
+ value="alert"
35
+ endIcon={<Warning className="h-4 w-4 text-warning-600" />}
36
+ >
37
+ Label
38
+ </ButtonGroupItem>
39
+ </ButtonGroup>
40
+ );
41
+ };
42
+
43
+ export const Playground = Template.bind({});
44
+ Playground.args = { defaultValue: "one" };
45
+
46
+ export const WithIconCounts = () => {
47
+ const [value, setValue] = useState("inbox");
48
+
49
+ return (
50
+ <div className="w-[360px]">
51
+ <ButtonGroup
52
+ isStretched
53
+ value={value}
54
+ onChange={(next) => setValue(next)}
55
+ >
56
+ <ButtonGroupItem value="inbox">Inbox (11)</ButtonGroupItem>
57
+ <ButtonGroupItem value="drafts">Drafts (11)</ButtonGroupItem>
58
+ <ButtonGroupItem
59
+ value="alerts"
60
+ endAdornment={<Warning className="h-3.5 w-3.5 text-warning-600" />}
61
+ >
62
+ Alerts
63
+ </ButtonGroupItem>
64
+ </ButtonGroup>
65
+ </div>
66
+ );
67
+ };
@@ -1,6 +1,40 @@
1
1
  import React, { forwardRef } from "react";
2
2
  import { cx } from "@edux-design/utils";
3
3
 
4
+ /**
5
+ * Props for the Button component.
6
+ *
7
+ * Extends all native HTML `<button>` attributes, while adding
8
+ * design-system variants and state controls.
9
+ *
10
+ * @typedef {React.ButtonHTMLAttributes<HTMLButtonElement> & {
11
+ * children?: React.ReactNode,
12
+ * size?: "small"|"medium"|"large",
13
+ * isSecondary?: boolean,
14
+ * isDanger?: boolean,
15
+ * isBare?: boolean,
16
+ * isStretched?: boolean,
17
+ * disabled?: boolean,
18
+ * className?: string
19
+ * }} ButtonProps
20
+ */
21
+
22
+ /**
23
+ * Button component.
24
+ *
25
+ * Provides design-system styled button with support for
26
+ * multiple visual variants, sizing, and states.
27
+ *
28
+ * @component
29
+ * @type {React.ForwardRefExoticComponent<ButtonProps & React.RefAttributes<HTMLButtonElement>>}
30
+ *
31
+ * @example
32
+ * ```jsx
33
+ * <Button size="small">Save</Button>
34
+ * <Button isSecondary>Cancel</Button>
35
+ * <Button isDanger disabled>Delete</Button>
36
+ * ```
37
+ */
4
38
  export const Button = forwardRef(
5
39
  (
6
40
  {
@@ -22,8 +56,7 @@ export const Button = forwardRef(
22
56
  if (isDanger) variant = "danger";
23
57
  if (disabled) variant = "disabled";
24
58
 
25
- // handle conflicting props - the last bool wins. i.e. The priority we are accepting is isbare, isSecondary, isDanger, disabled
26
-
59
+ // handle conflicting props - the last bool wins
27
60
  if (process.env.NODE_ENV !== "production") {
28
61
  const count = [isSecondary, isDanger, isBare].filter(Boolean).length;
29
62
  if (count > 1) {
@@ -39,7 +72,6 @@ export const Button = forwardRef(
39
72
  const fonts =
40
73
  "text-bold tracking-normal hover:underline hover:underline-offset-(--underline-offset-btn) active:underline active:underline-offset-(--underline-offset-btn)";
41
74
 
42
- //FIX-ME - Remove hardcoded values
43
75
  const sizes = {
44
76
  small: "h-[36px] py-8 px-16 rounded-lg text-sm",
45
77
  medium: "h-[44px] py-8 px-16 rounded-lg text-base",
@@ -0,0 +1,387 @@
1
+ import React, {
2
+ forwardRef,
3
+ useCallback,
4
+ useEffect,
5
+ useLayoutEffect,
6
+ useMemo,
7
+ useRef,
8
+ useState,
9
+ } from "react";
10
+ import { cx, filterChildrenByDisplayName } from "@edux-design/utils";
11
+
12
+ const SIZE_STYLES = {
13
+ small: {
14
+ button: "text-xs px-12 py-6 rounded-md ",
15
+ },
16
+ medium: {
17
+ button: "text-sm px-12 py-6 rounded-md ",
18
+ },
19
+ large: {
20
+ button: "text-base px-12 py-6 rounded-md",
21
+ },
22
+ };
23
+
24
+ const isBrowser = typeof window !== "undefined";
25
+ const useIsomorphicLayoutEffect = isBrowser ? useLayoutEffect : useEffect;
26
+
27
+ /**
28
+ * @typedef {Object} ButtonGroupOption
29
+ * @property {React.ReactNode} [label] - Main label for the option.
30
+ * @property {React.ReactNode} [startAdornment] - Optional leading element (icon, badge, etc.).
31
+ * @property {React.ReactNode} [endIcon] - Optional trailing element.
32
+ * @property {string|number} [value] - Unique value used for selection.
33
+ * @property {boolean} [disabled] - Whether the option is disabled.
34
+ */
35
+
36
+ /**
37
+ * Segmented button group with animated active state.
38
+ *
39
+ * @typedef {React.HTMLAttributes<HTMLDivElement> & {
40
+ * value?: string|number,
41
+ * defaultValue?: string|number,
42
+ * onChange?: (value: string|number, option: ButtonGroupOption) => void,
43
+ * size?: "small"|"medium"|"large",
44
+ * isStretched?: boolean,
45
+ * }} ButtonGroupProps
46
+ */
47
+ export const ButtonGroup = forwardRef(function ButtonGroup(
48
+ {
49
+ value,
50
+ defaultValue,
51
+ onChange,
52
+ size = "medium",
53
+ isStretched = false,
54
+ className,
55
+ children,
56
+ ...rest
57
+ },
58
+ forwardedRef
59
+ ) {
60
+ const normalizedOptions = useMemo(() => {
61
+ const childItems = filterChildrenByDisplayName(children, "ButtonGroupItem");
62
+ return childItems.map((child, index) => {
63
+ const props = child.props || {};
64
+ const hasValue = props.value !== undefined;
65
+ const safeValue = hasValue && props.value !== null ? props.value : index;
66
+ return {
67
+ ...props,
68
+ label: props.label ?? props.children ?? `Option ${index + 1}`,
69
+ value: safeValue,
70
+ };
71
+ });
72
+ }, [children]);
73
+
74
+ const sizeStyles = SIZE_STYLES[size] ?? SIZE_STYLES.medium;
75
+
76
+ const initialValue =
77
+ value ??
78
+ defaultValue ??
79
+ (normalizedOptions.length ? normalizedOptions[0].value : undefined);
80
+
81
+ const [internalValue, setInternalValue] = useState(initialValue);
82
+ const isControlled = value !== undefined;
83
+ const selectedValue = isControlled ? value : internalValue;
84
+
85
+ useEffect(() => {
86
+ if (!isControlled && defaultValue !== undefined) {
87
+ setInternalValue(defaultValue);
88
+ }
89
+ }, [defaultValue, isControlled]);
90
+
91
+ useEffect(() => {
92
+ if (isControlled) return;
93
+ if (
94
+ normalizedOptions.length &&
95
+ !normalizedOptions.some((option) => option.value === internalValue)
96
+ ) {
97
+ setInternalValue(normalizedOptions[0].value);
98
+ }
99
+ }, [internalValue, isControlled, normalizedOptions]);
100
+
101
+ const buttonRefs = useRef(new Map());
102
+ const containerRef = useRef(null);
103
+ const indicatorRef = useRef({ width: 0, height: 0, x: 0, y: 0 });
104
+ const [indicator, setIndicator] = useState({
105
+ width: 0,
106
+ height: 0,
107
+ x: 0,
108
+ y: 0,
109
+ });
110
+
111
+ const setContainerRef = useCallback(
112
+ (node) => {
113
+ containerRef.current = node;
114
+ if (typeof forwardedRef === "function") {
115
+ forwardedRef(node);
116
+ } else if (forwardedRef) {
117
+ forwardedRef.current = node;
118
+ }
119
+ },
120
+ [forwardedRef]
121
+ );
122
+
123
+ const registerButtonRef = useCallback((optionValue) => {
124
+ return (node) => {
125
+ if (!buttonRefs.current) return;
126
+ if (node) {
127
+ buttonRefs.current.set(optionValue, node);
128
+ } else {
129
+ buttonRefs.current.delete(optionValue);
130
+ }
131
+ };
132
+ }, []);
133
+
134
+ const updateIndicator = useCallback(() => {
135
+ const container = containerRef.current;
136
+ const target = buttonRefs.current.get(selectedValue);
137
+
138
+ if (!container || !target) {
139
+ if (indicatorRef.current.width !== 0) {
140
+ indicatorRef.current = { width: 0, height: 0, x: 0, y: 0 };
141
+ setIndicator(indicatorRef.current);
142
+ }
143
+ return;
144
+ }
145
+
146
+ const containerRect = container.getBoundingClientRect();
147
+ const targetRect = target.getBoundingClientRect();
148
+
149
+ const next = {
150
+ width: targetRect.width,
151
+ height: targetRect.height,
152
+ x: targetRect.left - containerRect.left,
153
+ y: targetRect.top - containerRect.top,
154
+ };
155
+
156
+ const prev = indicatorRef.current;
157
+ if (
158
+ prev.width !== next.width ||
159
+ prev.height !== next.height ||
160
+ prev.x !== next.x ||
161
+ prev.y !== next.y
162
+ ) {
163
+ indicatorRef.current = next;
164
+ setIndicator(next);
165
+ }
166
+ }, [selectedValue]);
167
+
168
+ useIsomorphicLayoutEffect(() => {
169
+ updateIndicator();
170
+ }, [updateIndicator, normalizedOptions.length, size]);
171
+
172
+ useEffect(() => {
173
+ if (!isBrowser) return;
174
+ const handleResize = () => updateIndicator();
175
+ window.addEventListener("resize", handleResize);
176
+ return () => window.removeEventListener("resize", handleResize);
177
+ }, [updateIndicator]);
178
+
179
+ useEffect(() => {
180
+ if (!isBrowser) return;
181
+ const node = containerRef.current;
182
+ if (!node || typeof ResizeObserver === "undefined") return;
183
+
184
+ const observer = new ResizeObserver(() => {
185
+ updateIndicator();
186
+ });
187
+
188
+ observer.observe(node);
189
+ return () => observer.disconnect();
190
+ }, [updateIndicator]);
191
+
192
+ const focusOption = useCallback((option) => {
193
+ const node = buttonRefs.current.get(option.value);
194
+ if (node) {
195
+ node.focus();
196
+ }
197
+ }, []);
198
+
199
+ const handleSelect = useCallback(
200
+ (option) => {
201
+ if (option.disabled || option.value === selectedValue) {
202
+ return;
203
+ }
204
+
205
+ if (!isControlled) {
206
+ setInternalValue(option.value);
207
+ }
208
+
209
+ if (typeof onChange === "function") {
210
+ onChange(option.value, option);
211
+ }
212
+ },
213
+ [isControlled, onChange, selectedValue]
214
+ );
215
+
216
+ const getNextEnabledIndex = useCallback(
217
+ (startIndex, direction) => {
218
+ if (!normalizedOptions.length) return -1;
219
+ let nextIndex = startIndex;
220
+
221
+ for (let i = 0; i < normalizedOptions.length; i += 1) {
222
+ nextIndex =
223
+ (nextIndex + direction + normalizedOptions.length) %
224
+ normalizedOptions.length;
225
+
226
+ if (!normalizedOptions[nextIndex].disabled) {
227
+ return nextIndex;
228
+ }
229
+ }
230
+
231
+ return startIndex;
232
+ },
233
+ [normalizedOptions]
234
+ );
235
+
236
+ const findFirstEnabledIndex = useCallback(() => {
237
+ return normalizedOptions.findIndex((option) => !option.disabled);
238
+ }, [normalizedOptions]);
239
+
240
+ const findLastEnabledIndex = useCallback(() => {
241
+ for (let i = normalizedOptions.length - 1; i >= 0; i -= 1) {
242
+ if (!normalizedOptions[i].disabled) {
243
+ return i;
244
+ }
245
+ }
246
+ return -1;
247
+ }, [normalizedOptions]);
248
+
249
+ const indicatorVisible = indicator.width > 0 && indicator.height > 0;
250
+
251
+ const rootClasses = cx(
252
+ "inline-flex h-[44px] rounded-lg border-2 border-border-subtle bg-bg-base/90 p-2 shadow-inner",
253
+ isStretched ? "w-full" : "w-auto",
254
+ className
255
+ );
256
+
257
+ const trackClasses = cx(
258
+ "relative isolate flex w-full items-center gap-8",
259
+ !isStretched && "w-auto"
260
+ );
261
+
262
+ const buttonBaseClasses = cx(
263
+ "relative z-10 h-[36px] inline-flex items-center justify-center gap-2 whitespace-nowrap font-semibold tracking-tight transition-colors duration-200 ease-out focus-visible:outline-none focus-visible:shadow-focus motion-reduce:duration-0",
264
+ sizeStyles.button,
265
+ isStretched ? "flex-1" : "w-auto"
266
+ );
267
+
268
+ return (
269
+ <div
270
+ {...rest}
271
+ ref={setContainerRef}
272
+ role="radiogroup"
273
+ className={rootClasses}
274
+ >
275
+ <div className={trackClasses}>
276
+ <span
277
+ aria-hidden="true"
278
+ className={cx(
279
+ "pointer-events-none h-[44px] absolute rounded-md border-2 border-border-primary-emphasis bg-bg-base shadow-[0_8px_20px_rgb(22_92_131/0.18)] transition-[transform,width,height] duration-300 ease-out motion-reduce:duration-0",
280
+ !indicatorVisible && "opacity-0"
281
+ )}
282
+ style={{
283
+ width: indicator.width || undefined,
284
+ height: indicator.height || undefined,
285
+ transform: `translate(${indicator.x - 4}px`,
286
+ }}
287
+ />
288
+
289
+ {normalizedOptions.map((option, index) => {
290
+ const isActive = option.value === selectedValue;
291
+ const handleKeyDown = (event) => {
292
+ if (option.disabled) return;
293
+
294
+ if (event.key === "ArrowRight" || event.key === "ArrowDown") {
295
+ event.preventDefault();
296
+ const nextIndex = getNextEnabledIndex(index, 1);
297
+ const nextOption = normalizedOptions[nextIndex];
298
+ if (nextOption && nextOption !== option) {
299
+ focusOption(nextOption);
300
+ handleSelect(nextOption);
301
+ }
302
+ } else if (event.key === "ArrowLeft" || event.key === "ArrowUp") {
303
+ event.preventDefault();
304
+ const prevIndex = getNextEnabledIndex(index, -1);
305
+ const prevOption = normalizedOptions[prevIndex];
306
+ if (prevOption && prevOption !== option) {
307
+ focusOption(prevOption);
308
+ handleSelect(prevOption);
309
+ }
310
+ } else if (event.key === "Home") {
311
+ event.preventDefault();
312
+ const firstIndex = findFirstEnabledIndex();
313
+ const firstOption =
314
+ firstIndex >= 0 ? normalizedOptions[firstIndex] : undefined;
315
+ if (firstOption && firstOption !== option) {
316
+ focusOption(firstOption);
317
+ handleSelect(firstOption);
318
+ }
319
+ } else if (event.key === "End") {
320
+ event.preventDefault();
321
+ const lastIndex = findLastEnabledIndex();
322
+ const lastOption =
323
+ lastIndex >= 0 ? normalizedOptions[lastIndex] : undefined;
324
+ if (lastOption && lastOption !== option) {
325
+ focusOption(lastOption);
326
+ handleSelect(lastOption);
327
+ }
328
+ }
329
+ };
330
+
331
+ const buttonClasses = cx(
332
+ buttonBaseClasses,
333
+ option.disabled
334
+ ? "cursor-not-allowed text-fg-inactive opacity-60"
335
+ : "cursor-pointer text-fg-subtle hover:text-fg-base",
336
+ isActive && "text-fg-primary-emphasis",
337
+ !isActive && "hover:bg-bg-minimal-5 active:bg-bg-inactive"
338
+ );
339
+
340
+ const hasStartAdornment =
341
+ option.startAdornment !== undefined &&
342
+ option.startAdornment !== null;
343
+ const hasendIcon =
344
+ option.endIcon !== undefined && option.endIcon !== null;
345
+
346
+ return (
347
+ <button
348
+ key={String(option.value)}
349
+ ref={registerButtonRef(option.value)}
350
+ type="button"
351
+ role="radio"
352
+ aria-checked={isActive}
353
+ // tabIndex={isActive ? 0 : -1}
354
+ disabled={option.disabled}
355
+ className={buttonClasses}
356
+ onClick={() => handleSelect(option)}
357
+ onKeyDown={handleKeyDown}
358
+ data-state={isActive ? "active" : "inactive"}
359
+ >
360
+ {hasStartAdornment && (
361
+ <span
362
+ aria-hidden="true"
363
+ className="flex items-center text-current"
364
+ >
365
+ {option.startAdornment}
366
+ </span>
367
+ )}
368
+
369
+ <span className="flex items-center gap-1 leading-none text-current">
370
+ {option.label ?? `Option ${index + 1}`}
371
+ </span>
372
+
373
+ {hasendIcon && (
374
+ <span
375
+ aria-hidden="true"
376
+ className="flex items-center text-current"
377
+ >
378
+ {option.endIcon}
379
+ </span>
380
+ )}
381
+ </button>
382
+ );
383
+ })}
384
+ </div>
385
+ </div>
386
+ );
387
+ });
@@ -0,0 +1,69 @@
1
+ import React from "react";
2
+ import { fireEvent, render, screen } from "@testing-library/react";
3
+ import { ButtonGroup } from "./ButtonGroup";
4
+ import { ButtonGroupItem } from "./ButtonGroupItem";
5
+
6
+ describe("ButtonGroup", () => {
7
+ it("renders provided options and honors defaultValue", () => {
8
+ render(
9
+ <ButtonGroup defaultValue="second">
10
+ <ButtonGroupItem value="first">Label A (11)</ButtonGroupItem>
11
+ <ButtonGroupItem value="second">Label B (11)</ButtonGroupItem>
12
+ <ButtonGroupItem value="alert" endAdornment={<span>!</span>}>
13
+ Label C
14
+ </ButtonGroupItem>
15
+ </ButtonGroup>
16
+ );
17
+
18
+ expect(screen.getAllByRole("radio")).toHaveLength(3);
19
+
20
+ expect(
21
+ screen.getByRole("radio", { name: "Label B (11)" })
22
+ ).toHaveAttribute("aria-checked", "true");
23
+
24
+ expect(
25
+ screen.getByRole("radio", { name: "Label A (11)" })
26
+ ).toHaveAttribute("aria-checked", "false");
27
+
28
+ expect(screen.getByText("!")).toBeInTheDocument();
29
+ });
30
+
31
+ it("moves selection when clicking on an option", () => {
32
+ const handleChange = vi.fn();
33
+ render(
34
+ <ButtonGroup defaultValue="first" onChange={handleChange}>
35
+ <ButtonGroupItem value="first">Label A (11)</ButtonGroupItem>
36
+ <ButtonGroupItem value="second">Label B (11)</ButtonGroupItem>
37
+ <ButtonGroupItem value="alert">Label C</ButtonGroupItem>
38
+ </ButtonGroup>
39
+ );
40
+
41
+ const second = screen.getByRole("radio", { name: "Label B (11)" });
42
+
43
+ fireEvent.click(second);
44
+
45
+ expect(handleChange).toHaveBeenCalledWith(
46
+ "second",
47
+ expect.objectContaining({ value: "second" })
48
+ );
49
+ expect(second).toHaveAttribute("aria-checked", "true");
50
+ });
51
+
52
+ it("ignores clicks for disabled options", () => {
53
+ const handleChange = vi.fn();
54
+ render(
55
+ <ButtonGroup defaultValue="first" onChange={handleChange}>
56
+ <ButtonGroupItem value="first">First</ButtonGroupItem>
57
+ <ButtonGroupItem value="second" disabled>
58
+ Second
59
+ </ButtonGroupItem>
60
+ </ButtonGroup>
61
+ );
62
+
63
+ const disabled = screen.getByRole("radio", { name: "Second" });
64
+ fireEvent.click(disabled);
65
+
66
+ expect(handleChange).not.toHaveBeenCalled();
67
+ expect(disabled).toHaveAttribute("aria-checked", "false");
68
+ });
69
+ });
@@ -0,0 +1,8 @@
1
+ import React from "react";
2
+
3
+ export function ButtonGroupItem(_props) {
4
+ return null;
5
+ }
6
+
7
+ ButtonGroupItem.displayName = "ButtonGroupItem";
8
+
@@ -0,0 +1,104 @@
1
+ import React, { forwardRef } from "react";
2
+ import { cx } from "@edux-design/utils";
3
+
4
+ /**
5
+ * Props for the IconButton component.
6
+ *
7
+ * Extends all native HTML `<button>` attributes and accepts an icon prop.
8
+ * Designed for circular icon-only buttons that follow the same
9
+ * design-system patterns as the regular Button.
10
+ *
11
+ * @typedef {React.ButtonHTMLAttributes<HTMLButtonElement> & {
12
+ * icon: React.ReactNode,
13
+ * size?: "small"|"medium"|"large",
14
+ * isSecondary?: boolean,
15
+ * isDanger?: boolean,
16
+ * isBare?: boolean,
17
+ * disabled?: boolean,
18
+ * className?: string
19
+ * }} IconButtonProps
20
+ */
21
+
22
+ /**
23
+ * IconButton component.
24
+ *
25
+ * Provides a circular button used for icon-only actions, matching
26
+ * the design-system variants and states of the main Button component.
27
+ *
28
+ * @component
29
+ * @type {React.ForwardRefExoticComponent<IconButtonProps & React.RefAttributes<HTMLButtonElement>>}
30
+ *
31
+ * @example
32
+ * ```jsx
33
+ * <IconButton icon={<XIcon />} aria-label="Close" />
34
+ * <IconButton icon={<EditIcon />} isSecondary />
35
+ * <IconButton icon={<TrashIcon />} isDanger disabled />
36
+ * ```
37
+ */
38
+ export const IconButton = forwardRef(
39
+ (
40
+ {
41
+ icon,
42
+ size = "medium",
43
+ isSecondary,
44
+ isDanger,
45
+ isBare,
46
+ disabled,
47
+ className,
48
+ ...props
49
+ },
50
+ ref
51
+ ) => {
52
+ let variant = "primary";
53
+ if (isBare) variant = "bare";
54
+ if (isSecondary) variant = "secondary";
55
+ if (isDanger) variant = "danger";
56
+ if (disabled) variant = "disabled";
57
+
58
+ if (process.env.NODE_ENV !== "production") {
59
+ const count = [isSecondary, isDanger, isBare].filter(Boolean).length;
60
+ if (count > 1) {
61
+ console.warn(
62
+ "[IconButton]: Multiple variant props passed. Using the highest priority only."
63
+ );
64
+ }
65
+ }
66
+
67
+ const defaults =
68
+ "flex justify-center items-center focus:shadow-focus focus-visible:shadow-focus outline-none transition-all duration-300 ease-in-out rounded-full";
69
+
70
+ const sizes = {
71
+ small: "h-[36px] w-[36px] text-sm",
72
+ medium: "h-[44px] w-[44px] text-base",
73
+ large: "h-[56px] w-[56px] text-lg",
74
+ };
75
+
76
+ const variants = {
77
+ primary:
78
+ "border-2 border-border-primary-emphasis bg-bg-primary-base text-white hover:bg-border-focus hover:text-fg-base hover:border-border-emphasis hover:shadow-md active:bg-border-focus active:bg-bg-focus-overlay active:text-fg-base",
79
+ secondary:
80
+ "border-2 border-border-primary-base bg-bg-base text-fg-primary-emphasis hover:shadow-md hover:bg-bg-primary-subtle active:bg-bg-primary-subtle-90",
81
+ bare: "bg-bg-base text-fg-primary-emphasis hover:bg-bg-minimal active:bg-bg-minimal-5",
82
+ danger:
83
+ "bg-bg-danger-base border-2 border-border-danger-emphasis text-white hover:bg-bg-danger-base-5 active:bg-bg-danger-base-10",
84
+ disabled:
85
+ "border-2 border-border-subtle bg-bg-inactive text-fg-inactive hover:no-underline cursor-not-allowed",
86
+ };
87
+
88
+ const STYLES = cx(defaults, sizes[size], variants[variant], className);
89
+
90
+ return (
91
+ <button
92
+ role="button"
93
+ type="button"
94
+ aria-disabled={disabled}
95
+ disabled={disabled}
96
+ className={STYLES}
97
+ ref={ref}
98
+ {...props}
99
+ >
100
+ {icon}
101
+ </button>
102
+ );
103
+ }
104
+ );