import { isValidElement } from "react"; import { Button as ButtonPrimitive } from "@base-ui/react/button"; import { cva, type VariantProps } from "class-variance-authority"; import { cn } from "./cn"; /* * Every class below resolves through a token from `@sub-design/tokens`. The * component has no palette value, no radius literal, and no product name in it * — swapping `data-brand` is the only thing that changes how it looks. * * Colours use the `(color:--token)` form and widths the `(length:--token)` * form: without the data-type hint Tailwind cannot tell what a bare custom * property holds. */ const buttonVariants = cva( [ "inline-flex shrink-0 select-none items-center justify-center gap-2", "whitespace-nowrap border border-transparent", "text-(length:--font-size-control) leading-none font-(--button-font-weight)", "rounded-(--button-radius)", "transition-colors duration-(--motion-duration) ease-(--motion-ease)", // Focus is a semantic guarantee, not a brand decision — see tokens README. // `outline-solid` is load-bearing: `outline-none` zeroes outline-style, so // without restoring it the ring computes a correct width and colour and // still paints nothing. "outline-none", "focus-visible:outline-solid", "focus-visible:outline-(length:--focus-ring-width)", "focus-visible:outline-(color:--color-focus-ring)", "focus-visible:outline-offset-2", "disabled:pointer-events-none disabled:opacity-50", "data-disabled:pointer-events-none data-disabled:opacity-50", "[&_svg]:pointer-events-none [&_svg]:shrink-0", "[&_svg:not([class*='size-'])]:size-4", ], { variants: { variant: { primary: [ "bg-(color:--color-action-primary)", "text-(color:--color-action-primary-content)", "hover:bg-(color:--color-action-primary-hover)", ], neutral: [ "bg-(color:--color-action-neutral)", "text-(color:--color-action-neutral-content)", "hover:bg-(color:--color-action-neutral-hover)", ], outline: [ "border-(color:--color-border)", "text-(color:--color-content)", "hover:bg-(color:--color-action-neutral)", ], ghost: [ "text-(color:--color-content)", "hover:bg-(color:--button-ghost-hover-bg)", "hover:text-(color:--button-ghost-hover-content)", ], destructive: [ "bg-(color:--button-danger-bg)", "text-(color:--button-danger-content)", "hover:bg-(color:--button-danger-bg-hover)", ], link: [ // Accent, not action: a near-black GoodSync link would read as body // text. For NOKL the two tokens are the same green, so nothing moves. "text-(color:--color-brand-accent)", "underline-offset-4 hover:underline", ], }, size: { xs: "h-(--button-height-xs) gap-1.5 px-(--button-padding-x-xs) text-(length:--font-size-control-sm) [&_svg:not([class*='size-'])]:size-3.5", sm: "h-(--button-height-sm) px-(--button-padding-x-sm)", md: "h-(--button-height-md) px-(--button-padding-x-md)", lg: "h-(--button-height-lg) px-(--button-padding-x-lg)", "icon-xs": "size-(--button-height-xs) [&_svg:not([class*='size-'])]:size-3.5", "icon-sm": "size-(--button-height-sm)", "icon-md": "size-(--button-height-md)", "icon-lg": "size-(--button-height-lg)", }, }, defaultVariants: { variant: "primary", size: "md", }, }, ); type ButtonVariants = VariantProps; export interface ButtonProps extends Omit, ButtonVariants { className?: string; } function Button({ className, variant, size, render, nativeButton, focusableWhenDisabled, type, role, ref, ...props }: ButtonProps) { // A `render` target may be an anchor or any non-button element, where `type` // is invalid. Only default it when we know a real