File size: 4,608 Bytes
d530f14
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
132
133
134
135
136
137
138
139
140
import { ButtonHTMLAttributes, forwardRef } from "react";

import { cn } from "@/utils/cn";

import "./button.css";

interface Props extends ButtonHTMLAttributes<HTMLButtonElement> {
  variant?: "primary" | "secondary" | "tertiary" | "playground" | "destructive";
  size?: "default" | "large";
  disabled?: boolean;
  loadingLabel?: string;
  isLoading?: boolean;
}

const Button = forwardRef<HTMLButtonElement, Props>(
  (
    {
      variant = "primary",
      size = "default",
      disabled,
      isLoading = false,
      loadingLabel = "Loading…",
      ...attrs
    },
    ref,
  ) => {
    const isNonInteractive = Boolean(disabled || isLoading);

    // Focus ring adapts to light/dark variants
    const focusRing =
      variant === "primary" || variant === "destructive"
        ? "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:ring-white"
        : "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:ring-black";

    return (
      <button
        {...attrs}
        ref={ref}
        type={attrs.type ?? "button"}
        aria-disabled={isNonInteractive || undefined}
        aria-busy={isLoading || undefined}
        aria-live={isLoading ? "polite" : undefined}
        data-state={
          isLoading ? "loading" : isNonInteractive ? "disabled" : "idle"
        }
        className={cn(
          attrs.className,
          "flex items-center justify-center button relative [&>*]:relative",
          "text-label-medium lg-max:[&_svg]:size-24",
          `button-${variant} group/button`,
          focusRing,

          // Shared non-interactive styles
          "disabled:cursor-not-allowed",
          isNonInteractive && "cursor-not-allowed",

          // Size
          size === "default" && "rounded-8 px-10 py-6 gap-4",
          size === "large" && "rounded-10 px-12 py-8 gap-6",

          // Variant + interactive nuances
          variant === "primary" && [
            "text-accent-white",
            // Hover/active only when interactive
            !isNonInteractive &&
              "hover:bg-[color:var(--heat-90)] active:[scale:0.995]",
            // Disabled: dim a bit, no hover, dim overlay bg layer if present
            "disabled:opacity-80",
            "disabled:[&_.button-background]:opacity-70",
          ],

          ["secondary", "tertiary", "playground"].includes(variant) && [
            "text-accent-black",
            !isNonInteractive && "active:[scale:0.99] active:bg-black-alpha-7",
          ],

          variant === "secondary" && [
            "bg-black-alpha-4",
            !isNonInteractive && "hover:bg-black-alpha-6",
            // Disabled: lighter fill + muted text, no hover
            "disabled:bg-black-alpha-3",
            "disabled:text-black-alpha-48",
            "disabled:hover:bg-black-alpha-3",
          ],

          variant === "tertiary" && [
            !isNonInteractive && "hover:bg-black-alpha-4",
            // Disabled: no hover background, text muted
            "disabled:text-black-alpha-48",
            "disabled:hover:bg-transparent",
          ],

          variant === "destructive" && [
            "bg-red-600 text-accent-white",
            !isNonInteractive && "hover:bg-red-700 active:scale-[0.98]",
            // Disabled: keep red but softer; soften text slightly
            "disabled:bg-red-600/70",
            "disabled:text-white-alpha-72",
            "disabled:hover:bg-red-600/70",
          ],

          variant === "playground" && [
            "inside-border before:border-black-alpha-4",
            isNonInteractive
              ? "before:opacity-0 bg-black-alpha-4 text-black-alpha-24"
              : "hover:bg-black-alpha-4 hover:before:opacity-0 active:before:opacity-0",
          ],
        )}
        disabled={isNonInteractive}
      >
        {variant === "primary" && (
          <div className="overlay button-background !absolute" />
        )}

        {/* loading state (spinner) */}
        {isLoading && (
          <div
            className={cn(
              "w-16 h-16 border-2 rounded-full animate-spin",
              variant === "primary" || variant === "destructive"
                ? "border-white/30 border-t-white"
                : "border-black/30 border-t-black",
            )}
            aria-hidden
          />
        )}

        {/* Screen reader-only loading label */}
        {isLoading && <span className="sr-only">{loadingLabel}</span>}

        {attrs.children}
      </button>
    );
  },
);

Button.displayName = "Button";

export default Button;