Spaces:
Sleeping
Sleeping
File size: 3,563 Bytes
b64de39 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 | /**
* Button - Reusable button component with variant, size, and state support.
*
* Variants: primary | secondary | danger | ghost | outline
* Sizes: sm | md | lg
*
* Supports loading state with spinner, disabled state,
* full-width mode, and icon placement (left or right).
*/
import { forwardRef } from "react";
import PropTypes from "prop-types";
const VARIANT_CLASSES = {
primary:
"bg-primary-600 text-white hover:bg-primary-700 focus:ring-primary-500 active:bg-primary-800",
secondary:
"bg-secondary-500 text-white hover:bg-secondary-600 focus:ring-secondary-400 active:bg-secondary-700",
danger:
"bg-danger-600 text-white hover:bg-danger-700 focus:ring-danger-500 active:bg-danger-800",
ghost:
"bg-transparent text-neutral-700 hover:bg-neutral-100 focus:ring-neutral-400 active:bg-neutral-200",
outline:
"bg-transparent border border-primary-600 text-primary-600 hover:bg-primary-50 focus:ring-primary-500 active:bg-primary-100",
};
const SIZE_CLASSES = {
sm: "px-3 py-1.5 text-sm",
md: "px-4 py-2 text-sm",
lg: "px-6 py-3 text-base",
};
const Button = forwardRef(function Button(
{
children,
variant = "primary",
size = "md",
type = "button",
disabled = false,
loading = false,
fullWidth = false,
icon = null,
iconPosition = "left",
className = "",
onClick,
...rest
},
ref,
) {
const isDisabled = disabled || loading;
const baseClasses =
"inline-flex items-center justify-center gap-2 rounded-lg font-medium " +
"transition-colors duration-150 focus:outline-none focus:ring-2 focus:ring-offset-2 " +
"disabled:opacity-50 disabled:cursor-not-allowed";
const classes = [
baseClasses,
VARIANT_CLASSES[variant] || VARIANT_CLASSES.primary,
SIZE_CLASSES[size] || SIZE_CLASSES.md,
fullWidth ? "w-full" : "",
className,
]
.filter(Boolean)
.join(" ");
return (
<button
ref={ref}
type={type}
disabled={isDisabled}
className={classes}
onClick={onClick}
aria-busy={loading}
aria-disabled={isDisabled}
{...rest}
>
{loading && (
<svg
className="animate-spin h-4 w-4"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
aria-hidden="true"
>
<circle
className="opacity-25"
cx="12"
cy="12"
r="10"
stroke="currentColor"
strokeWidth="4"
/>
<path
className="opacity-75"
fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"
/>
</svg>
)}
{!loading && icon && iconPosition === "left" && (
<span className="inline-flex shrink-0" aria-hidden="true">
{icon}
</span>
)}
{children}
{!loading && icon && iconPosition === "right" && (
<span className="inline-flex shrink-0" aria-hidden="true">
{icon}
</span>
)}
</button>
);
});
Button.propTypes = {
children: PropTypes.node.isRequired,
variant: PropTypes.oneOf(["primary", "secondary", "danger", "ghost", "outline"]),
size: PropTypes.oneOf(["sm", "md", "lg"]),
type: PropTypes.oneOf(["button", "submit", "reset"]),
disabled: PropTypes.bool,
loading: PropTypes.bool,
fullWidth: PropTypes.bool,
icon: PropTypes.node,
iconPosition: PropTypes.oneOf(["left", "right"]),
className: PropTypes.string,
onClick: PropTypes.func,
};
export default Button;
|