108 lines
3.1 KiB
React
108 lines
3.1 KiB
React
import React from "react";
|
|
|
|
const sizeMap = {
|
|
sm: { height: "var(--control-h-sm)", padding: "0 12px", font: "var(--text-xs)" },
|
|
md: { height: "var(--control-h)", padding: "0 14px", font: "var(--text-sm)" },
|
|
};
|
|
|
|
const variantStyle = {
|
|
primary: {
|
|
background: "var(--action-primary)",
|
|
color: "var(--text-on-accent)",
|
|
border: "1px solid transparent",
|
|
},
|
|
secondary: {
|
|
background: "var(--ctv-surface-2)",
|
|
color: "var(--text-primary)",
|
|
border: "1px solid var(--border-control)",
|
|
},
|
|
ghost: {
|
|
background: "transparent",
|
|
color: "var(--text-secondary)",
|
|
border: "1px solid transparent",
|
|
},
|
|
danger: {
|
|
background: "var(--ctv-error-soft)",
|
|
color: "var(--status-error)",
|
|
border: "1px solid rgba(229,72,77,0.35)",
|
|
},
|
|
};
|
|
|
|
const hoverBg = {
|
|
primary: "var(--action-primary-hover)",
|
|
secondary: "var(--ctv-surface-3)",
|
|
ghost: "var(--ctv-surface-2)",
|
|
danger: "rgba(229,72,77,0.22)",
|
|
};
|
|
|
|
/**
|
|
* ChicoryTV primary action button.
|
|
*/
|
|
export function Button({
|
|
children,
|
|
variant = "primary",
|
|
size = "md",
|
|
startIcon = null,
|
|
endIcon = null,
|
|
disabled = false,
|
|
loading = false,
|
|
fullWidth = false,
|
|
onClick,
|
|
type = "button",
|
|
style = {},
|
|
...rest
|
|
}) {
|
|
const [hover, setHover] = React.useState(false);
|
|
const [active, setActive] = React.useState(false);
|
|
const s = sizeMap[size] || sizeMap.md;
|
|
const v = variantStyle[variant] || variantStyle.primary;
|
|
const isDisabled = disabled || loading;
|
|
|
|
return (
|
|
<button
|
|
type={type}
|
|
disabled={isDisabled}
|
|
onClick={onClick}
|
|
onMouseEnter={() => setHover(true)}
|
|
onMouseLeave={() => { setHover(false); setActive(false); }}
|
|
onMouseDown={() => setActive(true)}
|
|
onMouseUp={() => setActive(false)}
|
|
style={{
|
|
display: "inline-flex",
|
|
alignItems: "center",
|
|
justifyContent: "center",
|
|
gap: "7px",
|
|
height: s.height,
|
|
padding: s.padding,
|
|
width: fullWidth ? "100%" : "auto",
|
|
font: `var(--weight-medium) ${s.font}/1 var(--font-sans)`,
|
|
letterSpacing: "0.01em",
|
|
borderRadius: "var(--radius-sm)",
|
|
cursor: isDisabled ? "not-allowed" : "pointer",
|
|
opacity: isDisabled ? 0.45 : 1,
|
|
whiteSpace: "nowrap",
|
|
transition: "background var(--dur-fast) var(--ease-standard), transform var(--dur-fast) var(--ease-standard)",
|
|
transform: active && !isDisabled ? "translateY(0.5px)" : "none",
|
|
...v,
|
|
background: !isDisabled && hover ? hoverBg[variant] : v.background,
|
|
...style,
|
|
}}
|
|
{...rest}
|
|
>
|
|
{loading ? <Spinner16 /> : startIcon}
|
|
{children}
|
|
{!loading && endIcon}
|
|
</button>
|
|
);
|
|
}
|
|
|
|
function Spinner16() {
|
|
return (
|
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" style={{ animation: "ctv-spin 0.7s linear infinite" }}>
|
|
<style>{`@keyframes ctv-spin{to{transform:rotate(360deg)}}`}</style>
|
|
<circle cx="12" cy="12" r="9" stroke="currentColor" strokeOpacity="0.25" strokeWidth="3" />
|
|
<path d="M21 12a9 9 0 0 0-9-9" stroke="currentColor" strokeWidth="3" strokeLinecap="round" />
|
|
</svg>
|
|
);
|
|
}
|