@fanvue/ui 3.2.0 → 3.3.0

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.
@@ -5,6 +5,7 @@ const jsxRuntime = require("react/jsx-runtime");
5
5
  const reactSlot = require("@radix-ui/react-slot");
6
6
  const React = require("react");
7
7
  const cn = require("../../utils/cn.cjs");
8
+ const AIIcon = require("../Icons/AIIcon.cjs");
8
9
  const SpinnerIcon = require("../Icons/SpinnerIcon.cjs");
9
10
  function _interopNamespaceDefault(e) {
10
11
  const n = Object.create(null, { [Symbol.toStringTag]: { value: "Module" } });
@@ -23,12 +24,24 @@ function _interopNamespaceDefault(e) {
23
24
  return Object.freeze(n);
24
25
  }
25
26
  const React__namespace = /* @__PURE__ */ _interopNamespaceDefault(React);
27
+ const NEGATIVE_AWARE_VARIANTS = /* @__PURE__ */ new Set([
28
+ "primary",
29
+ "secondary",
30
+ "tertiary",
31
+ "outline"
32
+ ]);
26
33
  const SIZE_CLASSES = {
27
- "48": "h-12 px-4 py-3 typography-body-default-16px-semibold",
34
+ "48": "h-12 px-6 py-3 typography-body-default-16px-semibold",
28
35
  "40": "h-10 px-4 py-2 typography-body-default-16px-semibold",
29
- "32": "h-8 px-3 py-2 typography-body-small-14px-semibold",
36
+ "32": "h-8 px-3 py-[7px] typography-body-small-14px-semibold",
30
37
  "24": "h-6 px-2 py-1 typography-body-small-14px-semibold"
31
38
  };
39
+ const ICON_SIDE_PADDING_CLASSES = {
40
+ "48": { left: "pl-5", right: "pr-5" },
41
+ "40": { left: "pl-3", right: "pr-3" },
42
+ "32": { left: "pl-2", right: "pr-2" },
43
+ "24": { left: "pl-1", right: "pr-1" }
44
+ };
32
45
  const ICON_SIZE_CLASS = {
33
46
  "48": "size-5",
34
47
  "40": "size-5",
@@ -41,17 +54,78 @@ const ICON_WRAPPER_CLASS = {
41
54
  "32": "[&>svg]:size-4",
42
55
  "24": "[&>svg]:size-3.5"
43
56
  };
57
+ const AI_GRADIENT = "[background-clip:padding-box,border-box] [background-image:linear-gradient(50deg,var(--color-buttons-ai-background-gradient-default-start)_11.87%,var(--color-buttons-ai-background-gradient-default-end)_112.39%),linear-gradient(50deg,var(--color-buttons-ai-stroke-start)_11.87%,var(--color-buttons-ai-stroke-end)_112.39%)] [background-origin:border-box] hover:[background-image:linear-gradient(50deg,var(--color-buttons-ai-background-gradient-hover-start)_11.87%,var(--color-buttons-ai-background-gradient-hover-end)_112.39%),linear-gradient(50deg,var(--color-buttons-ai-stroke-start)_11.87%,var(--color-buttons-ai-stroke-end)_112.39%)]";
58
+ const DISABLED_FILL = "bg-buttons-disabled-default text-content-disabled";
59
+ const DISABLED_FILL_NEGATIVE = "bg-buttons-disabled-negative text-content-disabled";
60
+ const DISABLED_TRANSPARENT = "bg-transparent text-content-disabled";
44
61
  const VARIANT_CLASSES = {
45
- primary: "bg-buttons-primary-default text-content-primary-inverted hover:bg-buttons-primary-hover hover:text-content-primary-inverted active:bg-buttons-primary-hover active:text-content-primary-inverted",
46
- secondary: "border-content-primary border bg-transparent text-content-primary hover:bg-brand-primary-muted active:bg-brand-primary-muted",
47
- tertiary: "bg-transparent text-content-primary hover:bg-brand-primary-muted active:bg-brand-primary-muted",
48
- link: "bg-transparent text-content-primary underline decoration-solid hover:bg-brand-primary-muted active:bg-brand-primary-muted",
49
- brand: "bg-buttons-brand-default text-content-always-black hover:bg-buttons-brand-hover hover:text-content-always-black active:bg-buttons-brand-hover active:text-content-always-black",
50
- destructive: "bg-error-content text-content-always-white hover:bg-brand-primary-muted hover:text-content-primary active:bg-brand-primary-muted active:text-content-primary",
51
- white: "bg-content-always-white text-content-always-black hover:bg-brand-primary-muted hover:text-content-primary active:bg-brand-primary-muted active:text-content-primary",
52
- tertiaryDestructive: "bg-transparent text-error-content hover:bg-error-surface active:bg-error-surface",
53
- text: "bg-transparent text-content-primary hover:underline active:underline"
62
+ primary: {
63
+ default: "bg-buttons-primary-default text-content-primary-inverted hover:bg-buttons-primary-hover hover:text-content-primary-inverted active:bg-buttons-primary-hover active:text-content-primary-inverted",
64
+ disabled: DISABLED_FILL,
65
+ negative: "bg-buttons-primary-negative-default text-content-primary hover:bg-buttons-primary-negative-hover active:bg-buttons-primary-negative-hover",
66
+ negativeDisabled: DISABLED_FILL_NEGATIVE
67
+ },
68
+ secondary: {
69
+ default: "bg-buttons-secondary-default text-content-primary hover:bg-buttons-secondary-hover active:bg-buttons-secondary-hover",
70
+ disabled: DISABLED_FILL,
71
+ negative: "bg-buttons-secondary-negative-default text-content-primary-inverted hover:bg-buttons-secondary-negative-hover active:bg-buttons-secondary-negative-hover",
72
+ negativeDisabled: DISABLED_FILL_NEGATIVE
73
+ },
74
+ tertiary: {
75
+ default: "bg-transparent text-content-primary hover:bg-buttons-tertiary-hover active:bg-buttons-tertiary-hover",
76
+ disabled: DISABLED_TRANSPARENT,
77
+ negative: "bg-transparent text-content-primary-inverted hover:bg-buttons-tertiary-negative-hover active:bg-buttons-tertiary-negative-hover",
78
+ negativeDisabled: DISABLED_TRANSPARENT
79
+ },
80
+ outline: {
81
+ default: "border border-buttons-outline-default bg-transparent text-content-primary hover:bg-buttons-outline-hover active:bg-buttons-outline-hover",
82
+ disabled: "border border-buttons-disabled-default bg-transparent text-content-disabled",
83
+ negative: "border border-buttons-outline-negative-default bg-transparent text-content-primary-inverted hover:bg-buttons-outline-negative-hover active:bg-buttons-outline-negative-hover",
84
+ negativeDisabled: "border border-buttons-disabled-negative bg-transparent text-content-disabled"
85
+ },
86
+ brand: {
87
+ default: "bg-buttons-brand-default text-content-always-black hover:bg-buttons-brand-hover hover:text-content-always-black active:bg-buttons-brand-hover active:text-content-always-black",
88
+ disabled: DISABLED_FILL
89
+ },
90
+ destructive: {
91
+ default: "bg-buttons-error-default text-content-always-white hover:bg-buttons-error-hover hover:text-content-always-white active:bg-buttons-error-hover active:text-content-always-white",
92
+ disabled: DISABLED_FILL
93
+ },
94
+ white: {
95
+ default: "bg-buttons-always-white-default text-content-always-black hover:bg-buttons-always-white-hover hover:text-content-always-black active:bg-buttons-always-white-hover active:text-content-always-black",
96
+ disabled: DISABLED_FILL
97
+ },
98
+ alwaysBlack: {
99
+ default: "bg-buttons-always-black-default text-content-always-white hover:bg-buttons-always-black-hover hover:text-content-always-white active:bg-buttons-always-black-hover active:text-content-always-white",
100
+ disabled: DISABLED_FILL
101
+ },
102
+ ai: {
103
+ default: `border border-transparent text-content-always-white shadow-ai-button-glow ${AI_GRADIENT}`,
104
+ disabled: DISABLED_FILL
105
+ },
106
+ link: {
107
+ default: "bg-transparent text-content-primary underline decoration-solid hover:bg-buttons-tertiary-hover active:bg-buttons-tertiary-hover",
108
+ disabled: "bg-transparent text-content-primary underline decoration-solid opacity-50"
109
+ },
110
+ tertiaryDestructive: {
111
+ default: "bg-transparent text-error-content hover:bg-error-surface active:bg-error-surface",
112
+ disabled: "bg-transparent text-error-content opacity-50"
113
+ },
114
+ text: {
115
+ default: "bg-transparent text-content-primary hover:underline active:underline",
116
+ disabled: "bg-transparent text-content-primary opacity-50"
117
+ }
54
118
  };
119
+ function getVariantClasses(variant, negative, disabled) {
120
+ const spec = VARIANT_CLASSES[variant];
121
+ const isNegative = NEGATIVE_AWARE_VARIANTS.has(variant) && negative;
122
+ if (disabled) return isNegative && spec.negativeDisabled || spec.disabled;
123
+ return isNegative && spec.negative || spec.default;
124
+ }
125
+ function getIconPaddingClasses(size, hasLeftIcon, hasRightIcon) {
126
+ const padding = ICON_SIDE_PADDING_CLASSES[size];
127
+ return cn.cn(hasLeftIcon && padding.left, hasRightIcon && padding.right);
128
+ }
55
129
  function getTextContent(node) {
56
130
  if (typeof node === "string") return node;
57
131
  if (typeof node === "number") return String(node);
@@ -124,6 +198,7 @@ const Button = React__namespace.forwardRef(
124
198
  className,
125
199
  variant = "primary",
126
200
  size = "40",
201
+ negative = false,
127
202
  leftIcon,
128
203
  rightIcon,
129
204
  loading = false,
@@ -136,20 +211,27 @@ const Button = React__namespace.forwardRef(
136
211
  ...props
137
212
  }, ref) => {
138
213
  const Comp = asChild ? reactSlot.Slot : "button";
139
- const isDisabled = disabled || loading;
140
- const iconSizeClass = ICON_WRAPPER_CLASS[size];
214
+ const isDisabled = Boolean(disabled);
215
+ const isInteractionDisabled = isDisabled || loading;
216
+ const iconSizeClass = variant === "ai" ? "[&>svg]:size-4" : ICON_WRAPPER_CLASS[size];
217
+ const effectiveLeftIcon = leftIcon ?? (variant === "ai" ? /* @__PURE__ */ jsxRuntime.jsx(AIIcon.AIIcon, { filled: true }) : void 0);
218
+ const iconPaddingClasses = getIconPaddingClasses(
219
+ size,
220
+ Boolean(effectiveLeftIcon),
221
+ Boolean(rightIcon)
222
+ );
141
223
  const buttonSpecificProps = !asChild ? {
142
224
  type: "button",
143
225
  "data-testid": "button",
144
- disabled: isDisabled
145
- } : isDisabled ? { "aria-disabled": true } : {};
226
+ disabled: isInteractionDisabled
227
+ } : isInteractionDisabled ? { "aria-disabled": true } : {};
146
228
  const loadingLabelProps = loading && asChild ? { "aria-label": getTextContent(children) } : {};
147
229
  const content = renderContent({
148
230
  loading,
149
231
  asChild,
150
232
  children,
151
233
  size,
152
- leftIcon,
234
+ leftIcon: effectiveLeftIcon,
153
235
  rightIcon,
154
236
  iconSizeClass,
155
237
  discount,
@@ -163,20 +245,14 @@ const Button = React__namespace.forwardRef(
163
245
  "aria-busy": loading,
164
246
  ...loadingLabelProps,
165
247
  className: cn.cn(
166
- // Base styles
167
248
  "inline-flex min-w-0 cursor-pointer items-center gap-2 whitespace-nowrap rounded-full transition-colors",
168
- // Focus ring
169
249
  "focus-visible:shadow-focus-ring focus-visible:outline-none",
170
- // Disabled state
171
- "disabled:pointer-events-none disabled:opacity-50",
172
- "aria-disabled:pointer-events-none aria-disabled:opacity-50",
250
+ isInteractionDisabled && "pointer-events-none cursor-not-allowed",
173
251
  `${price ? "justify-between" : "justify-center"}`,
174
252
  fullWidth && "w-full",
175
- // Size styles
176
253
  SIZE_CLASSES[size],
177
- // Variant styles
178
- VARIANT_CLASSES[variant],
179
- // Manual CSS overrides
254
+ iconPaddingClasses,
255
+ getVariantClasses(variant, negative, isDisabled),
180
256
  className
181
257
  ),
182
258
  ...props,
@@ -1 +1 @@
1
- {"version":3,"file":"Button.cjs","sources":["../../../../src/components/Button/Button.tsx"],"sourcesContent":["import { Slot } from \"@radix-ui/react-slot\";\nimport * as React from \"react\";\nimport { cn } from \"../../utils/cn\";\nimport { SpinnerIcon } from \"../Icons/SpinnerIcon\";\n\n/** Visual style variant of the button. */\nexport type ButtonVariant =\n | \"primary\"\n | \"secondary\"\n | \"tertiary\"\n | \"link\"\n | \"brand\"\n | \"destructive\"\n | \"white\"\n | \"tertiaryDestructive\"\n | \"text\";\n\n/** Button height in pixels. */\nexport type ButtonSize = \"48\" | \"40\" | \"32\" | \"24\";\n\nexport interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {\n /** Visual style variant of the button. @default \"primary\" */\n variant?: ButtonVariant;\n /** Height of the button in pixels. @default \"40\" */\n size?: ButtonSize;\n /** Icon element displayed before the label. */\n leftIcon?: React.ReactNode;\n /** Icon element displayed after the label. */\n rightIcon?: React.ReactNode;\n /** When `true`, replaces the label with a spinner and disables interaction. @default false */\n loading?: boolean;\n /** Merge props onto a child element instead of rendering a `<button>`. @default false */\n asChild?: boolean;\n /** Old price shown with a strikethrough before the current price. */\n discount?: string;\n /** Current price shown inside the button after the label and icons. */\n price?: string;\n /** When `true`, the button will take the full width of its container. @default false */\n fullWidth?: boolean;\n}\n\nconst SIZE_CLASSES: Record<ButtonSize, string> = {\n \"48\": \"h-12 px-4 py-3 typography-body-default-16px-semibold\",\n \"40\": \"h-10 px-4 py-2 typography-body-default-16px-semibold\",\n \"32\": \"h-8 px-3 py-2 typography-body-small-14px-semibold\",\n \"24\": \"h-6 px-2 py-1 typography-body-small-14px-semibold\",\n};\n\nconst ICON_SIZE_CLASS: Record<ButtonSize, string> = {\n \"48\": \"size-5\",\n \"40\": \"size-5\",\n \"32\": \"size-4\",\n \"24\": \"size-3.5\",\n};\n\n/** Targets only direct SVG children so non-icon content (e.g. Pill) can size naturally. */\nconst ICON_WRAPPER_CLASS: Record<ButtonSize, string> = {\n \"48\": \"[&>svg]:size-5\",\n \"40\": \"[&>svg]:size-5\",\n \"32\": \"[&>svg]:size-4\",\n \"24\": \"[&>svg]:size-3.5\",\n};\n\nconst VARIANT_CLASSES: Record<ButtonVariant, string> = {\n primary:\n \"bg-buttons-primary-default text-content-primary-inverted hover:bg-buttons-primary-hover hover:text-content-primary-inverted active:bg-buttons-primary-hover active:text-content-primary-inverted\",\n secondary:\n \"border-content-primary border bg-transparent text-content-primary hover:bg-brand-primary-muted active:bg-brand-primary-muted\",\n tertiary:\n \"bg-transparent text-content-primary hover:bg-brand-primary-muted active:bg-brand-primary-muted\",\n link: \"bg-transparent text-content-primary underline decoration-solid hover:bg-brand-primary-muted active:bg-brand-primary-muted\",\n brand:\n \"bg-buttons-brand-default text-content-always-black hover:bg-buttons-brand-hover hover:text-content-always-black active:bg-buttons-brand-hover active:text-content-always-black\",\n destructive:\n \"bg-error-content text-content-always-white hover:bg-brand-primary-muted hover:text-content-primary active:bg-brand-primary-muted active:text-content-primary\",\n white:\n \"bg-content-always-white text-content-always-black hover:bg-brand-primary-muted hover:text-content-primary active:bg-brand-primary-muted active:text-content-primary\",\n tertiaryDestructive:\n \"bg-transparent text-error-content hover:bg-error-surface active:bg-error-surface\",\n text: \"bg-transparent text-content-primary hover:underline active:underline\",\n};\n\n/** Recursively extract text content from React nodes for accessible labels */\nfunction getTextContent(node: React.ReactNode): string | undefined {\n if (typeof node === \"string\") return node;\n if (typeof node === \"number\") return String(node);\n if (React.isValidElement(node)) {\n return getTextContent((node.props as { children?: React.ReactNode }).children);\n }\n if (Array.isArray(node)) {\n const text = node.map(getTextContent).filter(Boolean).join(\"\");\n return text || undefined;\n }\n return undefined;\n}\n\nconst LoadingSpinner = ({ size }: { size: ButtonSize }) => {\n return (\n <span className=\"animate-spin\" aria-hidden=\"true\">\n <SpinnerIcon className={ICON_SIZE_CLASS[size]}>\n <title>Loading</title>\n </SpinnerIcon>\n </span>\n );\n};\n\nfunction renderContent({\n loading,\n asChild,\n children,\n size,\n leftIcon,\n rightIcon,\n iconSizeClass,\n discount,\n price,\n}: {\n loading: boolean;\n asChild: boolean;\n children: React.ReactNode;\n size: ButtonSize;\n leftIcon: React.ReactNode;\n rightIcon: React.ReactNode;\n iconSizeClass: string;\n discount?: string;\n price?: string;\n fullWidth?: boolean;\n}) {\n if (loading) {\n // When asChild, clone the child element with spinner content instead of\n // wrapping in sr-only span (which would nest interactive elements)\n if (asChild && React.isValidElement(children)) {\n return React.cloneElement(\n children as React.ReactElement<{ children?: React.ReactNode }>,\n undefined,\n <LoadingSpinner size={size} />,\n );\n }\n return (\n <>\n <LoadingSpinner size={size} />\n <span className=\"sr-only\">{children}</span>\n </>\n );\n }\n\n if (asChild) return children;\n\n return (\n <>\n {leftIcon && (\n <span\n className={cn(\"flex shrink-0 items-center justify-center\", iconSizeClass)}\n aria-hidden=\"true\"\n >\n {leftIcon}\n </span>\n )}\n {React.Children.map(children, (child) =>\n typeof child === \"string\" ? (\n child.trim() ? (\n <span className=\"min-w-0 truncate\">{child}</span>\n ) : null\n ) : typeof child === \"number\" ? (\n <span className=\"min-w-0 truncate\">{child}</span>\n ) : (\n child\n ),\n )}\n {rightIcon && (\n <span\n className={cn(\"flex shrink-0 items-center justify-center\", iconSizeClass)}\n aria-hidden=\"true\"\n >\n {rightIcon}\n </span>\n )}\n {(price || discount) && (\n <div>\n {discount && (\n <span className=\"typography-body-default-16px-regular line-through\" aria-hidden=\"true\">\n {discount}\n </span>\n )}\n {price && (\n <span className=\"ml-2\" aria-hidden=\"true\">\n {price}\n </span>\n )}\n </div>\n )}\n </>\n );\n}\n\n/**\n * A versatile button component with multiple visual variants, sizes, icon\n * slots, loading state, and optional pricing display.\n *\n * @example\n * ```tsx\n * <Button variant=\"brand\" size=\"40\" leftIcon={<StarIcon />}>\n * Subscribe\n * </Button>\n * ```\n */\nexport const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(\n (\n {\n className,\n variant = \"primary\",\n size = \"40\",\n leftIcon,\n rightIcon,\n loading = false,\n asChild = false,\n disabled,\n children,\n discount,\n price,\n fullWidth = false,\n ...props\n },\n ref,\n ) => {\n const Comp = asChild ? Slot : \"button\";\n const isDisabled = disabled || loading;\n const iconSizeClass = ICON_WRAPPER_CLASS[size];\n\n const buttonSpecificProps = !asChild\n ? {\n type: \"button\" as const,\n \"data-testid\": \"button\",\n disabled: isDisabled,\n }\n : isDisabled\n ? { \"aria-disabled\": true }\n : {};\n\n // When asChild + loading, extract text from children for aria-label since we\n // can't wrap element children in an sr-only span (creates invalid nested markup)\n const loadingLabelProps = loading && asChild ? { \"aria-label\": getTextContent(children) } : {};\n\n const content = renderContent({\n loading,\n asChild,\n children,\n size,\n leftIcon,\n rightIcon,\n iconSizeClass,\n discount,\n price,\n });\n\n return (\n <Comp\n ref={ref}\n {...buttonSpecificProps}\n aria-busy={loading}\n {...loadingLabelProps}\n className={cn(\n // Base styles\n \"inline-flex min-w-0 cursor-pointer items-center gap-2 whitespace-nowrap rounded-full transition-colors\",\n // Focus ring\n \"focus-visible:shadow-focus-ring focus-visible:outline-none\",\n // Disabled state\n \"disabled:pointer-events-none disabled:opacity-50\",\n \"aria-disabled:pointer-events-none aria-disabled:opacity-50\",\n `${price ? \"justify-between\" : \"justify-center\"}`,\n fullWidth && \"w-full\",\n // Size styles\n SIZE_CLASSES[size],\n // Variant styles\n VARIANT_CLASSES[variant],\n // Manual CSS overrides\n className,\n )}\n {...props}\n >\n {content}\n </Comp>\n );\n },\n);\n\nButton.displayName = \"Button\";\n"],"names":["React","jsx","SpinnerIcon","jsxs","Fragment","cn","Slot"],"mappings":"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"}
1
+ {"version":3,"file":"Button.cjs","sources":["../../../../src/components/Button/Button.tsx"],"sourcesContent":["import { Slot } from \"@radix-ui/react-slot\";\nimport * as React from \"react\";\nimport { cn } from \"../../utils/cn\";\nimport { AIIcon } from \"../Icons/AIIcon\";\nimport { SpinnerIcon } from \"../Icons/SpinnerIcon\";\n\n/** Visual style variant of the button. */\nexport type ButtonVariant =\n | \"primary\"\n | \"secondary\"\n | \"tertiary\"\n | \"outline\"\n | \"link\"\n | \"brand\"\n | \"destructive\"\n | \"white\"\n | \"alwaysBlack\"\n | \"ai\"\n | \"tertiaryDestructive\"\n | \"text\";\n\n/** Button height in pixels. */\nexport type ButtonSize = \"48\" | \"40\" | \"32\" | \"24\";\n\nexport interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {\n /** Visual style variant of the button. @default \"primary\" */\n variant?: ButtonVariant;\n /** Height of the button in pixels. @default \"40\" */\n size?: ButtonSize;\n /**\n * Forces the dark-surface treatment regardless of theme. Only honored on\n * `primary`, `secondary`, `tertiary`, and `outline` variants; ignored on\n * all others. Use when placing the button on a dark background in a light\n * theme (e.g. an inverted hero or modal).\n * @default false\n */\n negative?: boolean;\n /** Icon element displayed before the label. */\n leftIcon?: React.ReactNode;\n /** Icon element displayed after the label. */\n rightIcon?: React.ReactNode;\n /** When `true`, replaces the label with a spinner and disables interaction. @default false */\n loading?: boolean;\n /** Merge props onto a child element instead of rendering a `<button>`. @default false */\n asChild?: boolean;\n /** Old price shown with a strikethrough before the current price. */\n discount?: string;\n /** Current price shown inside the button after the label and icons. */\n price?: string;\n /** When `true`, the button will take the full width of its container. @default false */\n fullWidth?: boolean;\n}\n\nconst NEGATIVE_AWARE_VARIANTS = new Set<ButtonVariant>([\n \"primary\",\n \"secondary\",\n \"tertiary\",\n \"outline\",\n]);\n\nconst SIZE_CLASSES: Record<ButtonSize, string> = {\n \"48\": \"h-12 px-6 py-3 typography-body-default-16px-semibold\",\n \"40\": \"h-10 px-4 py-2 typography-body-default-16px-semibold\",\n \"32\": \"h-8 px-3 py-[7px] typography-body-small-14px-semibold\",\n \"24\": \"h-6 px-2 py-1 typography-body-small-14px-semibold\",\n};\n\nconst ICON_SIDE_PADDING_CLASSES: Record<ButtonSize, { left: string; right: string }> = {\n \"48\": { left: \"pl-5\", right: \"pr-5\" },\n \"40\": { left: \"pl-3\", right: \"pr-3\" },\n \"32\": { left: \"pl-2\", right: \"pr-2\" },\n \"24\": { left: \"pl-1\", right: \"pr-1\" },\n};\n\nconst ICON_SIZE_CLASS: Record<ButtonSize, string> = {\n \"48\": \"size-5\",\n \"40\": \"size-5\",\n \"32\": \"size-4\",\n \"24\": \"size-3.5\",\n};\n\n/** Targets only direct SVG children so non-icon content (e.g. Pill) can size naturally. */\nconst ICON_WRAPPER_CLASS: Record<ButtonSize, string> = {\n \"48\": \"[&>svg]:size-5\",\n \"40\": \"[&>svg]:size-5\",\n \"32\": \"[&>svg]:size-4\",\n \"24\": \"[&>svg]:size-3.5\",\n};\n\n/** AI variant uses a fixed-angle gradient defined in the Figma design tokens. */\nconst AI_GRADIENT =\n \"[background-clip:padding-box,border-box] [background-image:linear-gradient(50deg,var(--color-buttons-ai-background-gradient-default-start)_11.87%,var(--color-buttons-ai-background-gradient-default-end)_112.39%),linear-gradient(50deg,var(--color-buttons-ai-stroke-start)_11.87%,var(--color-buttons-ai-stroke-end)_112.39%)] [background-origin:border-box] hover:[background-image:linear-gradient(50deg,var(--color-buttons-ai-background-gradient-hover-start)_11.87%,var(--color-buttons-ai-background-gradient-hover-end)_112.39%),linear-gradient(50deg,var(--color-buttons-ai-stroke-start)_11.87%,var(--color-buttons-ai-stroke-end)_112.39%)]\";\n\nconst DISABLED_FILL = \"bg-buttons-disabled-default text-content-disabled\";\nconst DISABLED_FILL_NEGATIVE = \"bg-buttons-disabled-negative text-content-disabled\";\nconst DISABLED_TRANSPARENT = \"bg-transparent text-content-disabled\";\n\n/**\n * Class strings for each `ButtonVariant`, indexed by `negative` and `disabled`\n * state. `negative` and `negativeDisabled` are only honored when the variant\n * is in `NEGATIVE_AWARE_VARIANTS` — other variants fall back to the default /\n * disabled entries regardless of the `negative` prop.\n */\ntype VariantClasses = {\n default: string;\n disabled: string;\n negative?: string;\n negativeDisabled?: string;\n};\n\nconst VARIANT_CLASSES: Record<ButtonVariant, VariantClasses> = {\n primary: {\n default:\n \"bg-buttons-primary-default text-content-primary-inverted hover:bg-buttons-primary-hover hover:text-content-primary-inverted active:bg-buttons-primary-hover active:text-content-primary-inverted\",\n disabled: DISABLED_FILL,\n negative:\n \"bg-buttons-primary-negative-default text-content-primary hover:bg-buttons-primary-negative-hover active:bg-buttons-primary-negative-hover\",\n negativeDisabled: DISABLED_FILL_NEGATIVE,\n },\n secondary: {\n default:\n \"bg-buttons-secondary-default text-content-primary hover:bg-buttons-secondary-hover active:bg-buttons-secondary-hover\",\n disabled: DISABLED_FILL,\n negative:\n \"bg-buttons-secondary-negative-default text-content-primary-inverted hover:bg-buttons-secondary-negative-hover active:bg-buttons-secondary-negative-hover\",\n negativeDisabled: DISABLED_FILL_NEGATIVE,\n },\n tertiary: {\n default:\n \"bg-transparent text-content-primary hover:bg-buttons-tertiary-hover active:bg-buttons-tertiary-hover\",\n disabled: DISABLED_TRANSPARENT,\n negative:\n \"bg-transparent text-content-primary-inverted hover:bg-buttons-tertiary-negative-hover active:bg-buttons-tertiary-negative-hover\",\n negativeDisabled: DISABLED_TRANSPARENT,\n },\n outline: {\n default:\n \"border border-buttons-outline-default bg-transparent text-content-primary hover:bg-buttons-outline-hover active:bg-buttons-outline-hover\",\n disabled: \"border border-buttons-disabled-default bg-transparent text-content-disabled\",\n negative:\n \"border border-buttons-outline-negative-default bg-transparent text-content-primary-inverted hover:bg-buttons-outline-negative-hover active:bg-buttons-outline-negative-hover\",\n negativeDisabled:\n \"border border-buttons-disabled-negative bg-transparent text-content-disabled\",\n },\n brand: {\n default:\n \"bg-buttons-brand-default text-content-always-black hover:bg-buttons-brand-hover hover:text-content-always-black active:bg-buttons-brand-hover active:text-content-always-black\",\n disabled: DISABLED_FILL,\n },\n destructive: {\n default:\n \"bg-buttons-error-default text-content-always-white hover:bg-buttons-error-hover hover:text-content-always-white active:bg-buttons-error-hover active:text-content-always-white\",\n disabled: DISABLED_FILL,\n },\n white: {\n default:\n \"bg-buttons-always-white-default text-content-always-black hover:bg-buttons-always-white-hover hover:text-content-always-black active:bg-buttons-always-white-hover active:text-content-always-black\",\n disabled: DISABLED_FILL,\n },\n alwaysBlack: {\n default:\n \"bg-buttons-always-black-default text-content-always-white hover:bg-buttons-always-black-hover hover:text-content-always-white active:bg-buttons-always-black-hover active:text-content-always-white\",\n disabled: DISABLED_FILL,\n },\n ai: {\n default: `border border-transparent text-content-always-white shadow-ai-button-glow ${AI_GRADIENT}`,\n disabled: DISABLED_FILL,\n },\n link: {\n default:\n \"bg-transparent text-content-primary underline decoration-solid hover:bg-buttons-tertiary-hover active:bg-buttons-tertiary-hover\",\n disabled: \"bg-transparent text-content-primary underline decoration-solid opacity-50\",\n },\n tertiaryDestructive: {\n default: \"bg-transparent text-error-content hover:bg-error-surface active:bg-error-surface\",\n disabled: \"bg-transparent text-error-content opacity-50\",\n },\n text: {\n default: \"bg-transparent text-content-primary hover:underline active:underline\",\n disabled: \"bg-transparent text-content-primary opacity-50\",\n },\n};\n\nfunction getVariantClasses(variant: ButtonVariant, negative: boolean, disabled: boolean): string {\n const spec = VARIANT_CLASSES[variant];\n const isNegative = NEGATIVE_AWARE_VARIANTS.has(variant) && negative;\n if (disabled) return (isNegative && spec.negativeDisabled) || spec.disabled;\n return (isNegative && spec.negative) || spec.default;\n}\n\nfunction getIconPaddingClasses(\n size: ButtonSize,\n hasLeftIcon: boolean,\n hasRightIcon: boolean,\n): string | undefined {\n const padding = ICON_SIDE_PADDING_CLASSES[size];\n return cn(hasLeftIcon && padding.left, hasRightIcon && padding.right);\n}\n\n/** Recursively extract text content from React nodes for accessible labels */\nfunction getTextContent(node: React.ReactNode): string | undefined {\n if (typeof node === \"string\") return node;\n if (typeof node === \"number\") return String(node);\n if (React.isValidElement(node)) {\n return getTextContent((node.props as { children?: React.ReactNode }).children);\n }\n if (Array.isArray(node)) {\n const text = node.map(getTextContent).filter(Boolean).join(\"\");\n return text || undefined;\n }\n return undefined;\n}\n\nconst LoadingSpinner = ({ size }: { size: ButtonSize }) => {\n return (\n <span className=\"animate-spin\" aria-hidden=\"true\">\n <SpinnerIcon className={ICON_SIZE_CLASS[size]}>\n <title>Loading</title>\n </SpinnerIcon>\n </span>\n );\n};\n\nfunction renderContent({\n loading,\n asChild,\n children,\n size,\n leftIcon,\n rightIcon,\n iconSizeClass,\n discount,\n price,\n}: {\n loading: boolean;\n asChild: boolean;\n children: React.ReactNode;\n size: ButtonSize;\n leftIcon: React.ReactNode;\n rightIcon: React.ReactNode;\n iconSizeClass: string;\n discount?: string;\n price?: string;\n fullWidth?: boolean;\n}) {\n if (loading) {\n if (asChild && React.isValidElement(children)) {\n return React.cloneElement(\n children as React.ReactElement<{ children?: React.ReactNode }>,\n undefined,\n <LoadingSpinner size={size} />,\n );\n }\n return (\n <>\n <LoadingSpinner size={size} />\n <span className=\"sr-only\">{children}</span>\n </>\n );\n }\n\n if (asChild) return children;\n\n return (\n <>\n {leftIcon && (\n <span\n className={cn(\"flex shrink-0 items-center justify-center\", iconSizeClass)}\n aria-hidden=\"true\"\n >\n {leftIcon}\n </span>\n )}\n {React.Children.map(children, (child) =>\n typeof child === \"string\" ? (\n child.trim() ? (\n <span className=\"min-w-0 truncate\">{child}</span>\n ) : null\n ) : typeof child === \"number\" ? (\n <span className=\"min-w-0 truncate\">{child}</span>\n ) : (\n child\n ),\n )}\n {rightIcon && (\n <span\n className={cn(\"flex shrink-0 items-center justify-center\", iconSizeClass)}\n aria-hidden=\"true\"\n >\n {rightIcon}\n </span>\n )}\n {(price || discount) && (\n <div>\n {discount && (\n <span className=\"typography-body-default-16px-regular line-through\" aria-hidden=\"true\">\n {discount}\n </span>\n )}\n {price && (\n <span className=\"ml-2\" aria-hidden=\"true\">\n {price}\n </span>\n )}\n </div>\n )}\n </>\n );\n}\n\n/**\n * A versatile button component with multiple visual variants, sizes, icon\n * slots, loading state, and optional pricing display.\n *\n * Pass `negative` when rendering on a dark surface to opt into the inverted\n * treatment for `primary`, `secondary`, `tertiary`, and `outline` variants.\n *\n * The `ai` variant ships with the AI sparkle icon baked in (locked at 16px\n * regardless of button size); pass `leftIcon` to override the default sparkle.\n *\n * @example\n * ```tsx\n * <Button variant=\"primary\" size=\"40\" leftIcon={<StarIcon />}>\n * Continue\n * </Button>\n * ```\n */\nexport const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(\n (\n {\n className,\n variant = \"primary\",\n size = \"40\",\n negative = false,\n leftIcon,\n rightIcon,\n loading = false,\n asChild = false,\n disabled,\n children,\n discount,\n price,\n fullWidth = false,\n ...props\n },\n ref,\n ) => {\n const Comp = asChild ? Slot : \"button\";\n const isDisabled = Boolean(disabled);\n const isInteractionDisabled = isDisabled || loading;\n const iconSizeClass = variant === \"ai\" ? \"[&>svg]:size-4\" : ICON_WRAPPER_CLASS[size];\n const effectiveLeftIcon = leftIcon ?? (variant === \"ai\" ? <AIIcon filled /> : undefined);\n const iconPaddingClasses = getIconPaddingClasses(\n size,\n Boolean(effectiveLeftIcon),\n Boolean(rightIcon),\n );\n\n const buttonSpecificProps = !asChild\n ? {\n type: \"button\" as const,\n \"data-testid\": \"button\",\n disabled: isInteractionDisabled,\n }\n : isInteractionDisabled\n ? { \"aria-disabled\": true }\n : {};\n\n const loadingLabelProps = loading && asChild ? { \"aria-label\": getTextContent(children) } : {};\n\n const content = renderContent({\n loading,\n asChild,\n children,\n size,\n leftIcon: effectiveLeftIcon,\n rightIcon,\n iconSizeClass,\n discount,\n price,\n });\n\n return (\n <Comp\n ref={ref}\n {...buttonSpecificProps}\n aria-busy={loading}\n {...loadingLabelProps}\n className={cn(\n \"inline-flex min-w-0 cursor-pointer items-center gap-2 whitespace-nowrap rounded-full transition-colors\",\n \"focus-visible:shadow-focus-ring focus-visible:outline-none\",\n isInteractionDisabled && \"pointer-events-none cursor-not-allowed\",\n `${price ? \"justify-between\" : \"justify-center\"}`,\n fullWidth && \"w-full\",\n SIZE_CLASSES[size],\n iconPaddingClasses,\n getVariantClasses(variant, negative, isDisabled),\n className,\n )}\n {...props}\n >\n {content}\n </Comp>\n );\n },\n);\n\nButton.displayName = \"Button\";\n"],"names":["cn","React","jsx","SpinnerIcon","jsxs","Fragment","Slot","AIIcon"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;AAqDA,MAAM,8CAA8B,IAAmB;AAAA,EACrD;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,CAAC;AAED,MAAM,eAA2C;AAAA,EAC/C,MAAM;AAAA,EACN,MAAM;AAAA,EACN,MAAM;AAAA,EACN,MAAM;AACR;AAEA,MAAM,4BAAiF;AAAA,EACrF,MAAM,EAAE,MAAM,QAAQ,OAAO,OAAA;AAAA,EAC7B,MAAM,EAAE,MAAM,QAAQ,OAAO,OAAA;AAAA,EAC7B,MAAM,EAAE,MAAM,QAAQ,OAAO,OAAA;AAAA,EAC7B,MAAM,EAAE,MAAM,QAAQ,OAAO,OAAA;AAC/B;AAEA,MAAM,kBAA8C;AAAA,EAClD,MAAM;AAAA,EACN,MAAM;AAAA,EACN,MAAM;AAAA,EACN,MAAM;AACR;AAGA,MAAM,qBAAiD;AAAA,EACrD,MAAM;AAAA,EACN,MAAM;AAAA,EACN,MAAM;AAAA,EACN,MAAM;AACR;AAGA,MAAM,cACJ;AAEF,MAAM,gBAAgB;AACtB,MAAM,yBAAyB;AAC/B,MAAM,uBAAuB;AAe7B,MAAM,kBAAyD;AAAA,EAC7D,SAAS;AAAA,IACP,SACE;AAAA,IACF,UAAU;AAAA,IACV,UACE;AAAA,IACF,kBAAkB;AAAA,EAAA;AAAA,EAEpB,WAAW;AAAA,IACT,SACE;AAAA,IACF,UAAU;AAAA,IACV,UACE;AAAA,IACF,kBAAkB;AAAA,EAAA;AAAA,EAEpB,UAAU;AAAA,IACR,SACE;AAAA,IACF,UAAU;AAAA,IACV,UACE;AAAA,IACF,kBAAkB;AAAA,EAAA;AAAA,EAEpB,SAAS;AAAA,IACP,SACE;AAAA,IACF,UAAU;AAAA,IACV,UACE;AAAA,IACF,kBACE;AAAA,EAAA;AAAA,EAEJ,OAAO;AAAA,IACL,SACE;AAAA,IACF,UAAU;AAAA,EAAA;AAAA,EAEZ,aAAa;AAAA,IACX,SACE;AAAA,IACF,UAAU;AAAA,EAAA;AAAA,EAEZ,OAAO;AAAA,IACL,SACE;AAAA,IACF,UAAU;AAAA,EAAA;AAAA,EAEZ,aAAa;AAAA,IACX,SACE;AAAA,IACF,UAAU;AAAA,EAAA;AAAA,EAEZ,IAAI;AAAA,IACF,SAAS,6EAA6E,WAAW;AAAA,IACjG,UAAU;AAAA,EAAA;AAAA,EAEZ,MAAM;AAAA,IACJ,SACE;AAAA,IACF,UAAU;AAAA,EAAA;AAAA,EAEZ,qBAAqB;AAAA,IACnB,SAAS;AAAA,IACT,UAAU;AAAA,EAAA;AAAA,EAEZ,MAAM;AAAA,IACJ,SAAS;AAAA,IACT,UAAU;AAAA,EAAA;AAEd;AAEA,SAAS,kBAAkB,SAAwB,UAAmB,UAA2B;AAC/F,QAAM,OAAO,gBAAgB,OAAO;AACpC,QAAM,aAAa,wBAAwB,IAAI,OAAO,KAAK;AAC3D,MAAI,SAAU,QAAQ,cAAc,KAAK,oBAAqB,KAAK;AACnE,SAAQ,cAAc,KAAK,YAAa,KAAK;AAC/C;AAEA,SAAS,sBACP,MACA,aACA,cACoB;AACpB,QAAM,UAAU,0BAA0B,IAAI;AAC9C,SAAOA,GAAAA,GAAG,eAAe,QAAQ,MAAM,gBAAgB,QAAQ,KAAK;AACtE;AAGA,SAAS,eAAe,MAA2C;AACjE,MAAI,OAAO,SAAS,SAAU,QAAO;AACrC,MAAI,OAAO,SAAS,SAAU,QAAO,OAAO,IAAI;AAChD,MAAIC,iBAAM,eAAe,IAAI,GAAG;AAC9B,WAAO,eAAgB,KAAK,MAAyC,QAAQ;AAAA,EAC/E;AACA,MAAI,MAAM,QAAQ,IAAI,GAAG;AACvB,UAAM,OAAO,KAAK,IAAI,cAAc,EAAE,OAAO,OAAO,EAAE,KAAK,EAAE;AAC7D,WAAO,QAAQ;AAAA,EACjB;AACA,SAAO;AACT;AAEA,MAAM,iBAAiB,CAAC,EAAE,WAAiC;AACzD,wCACG,QAAA,EAAK,WAAU,gBAAe,eAAY,QACzC,UAAAC,2BAAAA,IAACC,yBAAA,EAAY,WAAW,gBAAgB,IAAI,GAC1C,UAAAD,+BAAC,SAAA,EAAM,UAAA,UAAA,CAAO,GAChB,GACF;AAEJ;AAEA,SAAS,cAAc;AAAA,EACrB;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,GAWG;AACD,MAAI,SAAS;AACX,QAAI,WAAWD,iBAAM,eAAe,QAAQ,GAAG;AAC7C,aAAOA,iBAAM;AAAA,QACX;AAAA,QACA;AAAA,QACAC,+BAAC,kBAAe,KAAA,CAAY;AAAA,MAAA;AAAA,IAEhC;AACA,WACEE,2BAAAA,KAAAC,qBAAA,EACE,UAAA;AAAA,MAAAH,+BAAC,kBAAe,MAAY;AAAA,MAC5BA,2BAAAA,IAAC,QAAA,EAAK,WAAU,WAAW,SAAA,CAAS;AAAA,IAAA,GACtC;AAAA,EAEJ;AAEA,MAAI,QAAS,QAAO;AAEpB,SACEE,2BAAAA,KAAAC,qBAAA,EACG,UAAA;AAAA,IAAA,YACCH,2BAAAA;AAAAA,MAAC;AAAA,MAAA;AAAA,QACC,WAAWF,GAAAA,GAAG,6CAA6C,aAAa;AAAA,QACxE,eAAY;AAAA,QAEX,UAAA;AAAA,MAAA;AAAA,IAAA;AAAA,IAGJC,iBAAM,SAAS;AAAA,MAAI;AAAA,MAAU,CAAC,UAC7B,OAAO,UAAU,WACf,MAAM,SACJC,2BAAAA,IAAC,QAAA,EAAK,WAAU,oBAAoB,UAAA,MAAA,CAAM,IACxC,OACF,OAAO,UAAU,0CAClB,QAAA,EAAK,WAAU,oBAAoB,UAAA,MAAA,CAAM,IAE1C;AAAA,IAAA;AAAA,IAGH,aACCA,2BAAAA;AAAAA,MAAC;AAAA,MAAA;AAAA,QACC,WAAWF,GAAAA,GAAG,6CAA6C,aAAa;AAAA,QACxE,eAAY;AAAA,QAEX,UAAA;AAAA,MAAA;AAAA,IAAA;AAAA,KAGH,SAAS,aACTI,2BAAAA,KAAC,OAAA,EACE,UAAA;AAAA,MAAA,2CACE,QAAA,EAAK,WAAU,qDAAoD,eAAY,QAC7E,UAAA,UACH;AAAA,MAED,SACCF,2BAAAA,IAAC,QAAA,EAAK,WAAU,QAAO,eAAY,QAChC,UAAA,MAAA,CACH;AAAA,IAAA,EAAA,CAEJ;AAAA,EAAA,GAEJ;AAEJ;AAmBO,MAAM,SAASD,iBAAM;AAAA,EAC1B,CACE;AAAA,IACE;AAAA,IACA,UAAU;AAAA,IACV,OAAO;AAAA,IACP,WAAW;AAAA,IACX;AAAA,IACA;AAAA,IACA,UAAU;AAAA,IACV,UAAU;AAAA,IACV;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA,YAAY;AAAA,IACZ,GAAG;AAAA,EAAA,GAEL,QACG;AACH,UAAM,OAAO,UAAUK,UAAAA,OAAO;AAC9B,UAAM,aAAa,QAAQ,QAAQ;AACnC,UAAM,wBAAwB,cAAc;AAC5C,UAAM,gBAAgB,YAAY,OAAO,mBAAmB,mBAAmB,IAAI;AACnF,UAAM,oBAAoB,aAAa,YAAY,sCAAQC,OAAAA,QAAA,EAAO,QAAM,MAAC,IAAK;AAC9E,UAAM,qBAAqB;AAAA,MACzB;AAAA,MACA,QAAQ,iBAAiB;AAAA,MACzB,QAAQ,SAAS;AAAA,IAAA;AAGnB,UAAM,sBAAsB,CAAC,UACzB;AAAA,MACE,MAAM;AAAA,MACN,eAAe;AAAA,MACf,UAAU;AAAA,IAAA,IAEZ,wBACE,EAAE,iBAAiB,KAAA,IACnB,CAAA;AAEN,UAAM,oBAAoB,WAAW,UAAU,EAAE,cAAc,eAAe,QAAQ,EAAA,IAAM,CAAA;AAE5F,UAAM,UAAU,cAAc;AAAA,MAC5B;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA,UAAU;AAAA,MACV;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,IAAA,CACD;AAED,WACEL,2BAAAA;AAAAA,MAAC;AAAA,MAAA;AAAA,QACC;AAAA,QACC,GAAG;AAAA,QACJ,aAAW;AAAA,QACV,GAAG;AAAA,QACJ,WAAWF,GAAAA;AAAAA,UACT;AAAA,UACA;AAAA,UACA,yBAAyB;AAAA,UACzB,GAAG,QAAQ,oBAAoB,gBAAgB;AAAA,UAC/C,aAAa;AAAA,UACb,aAAa,IAAI;AAAA,UACjB;AAAA,UACA,kBAAkB,SAAS,UAAU,UAAU;AAAA,UAC/C;AAAA,QAAA;AAAA,QAED,GAAG;AAAA,QAEH,UAAA;AAAA,MAAA;AAAA,IAAA;AAAA,EAGP;AACF;AAEA,OAAO,cAAc;;"}
@@ -1 +1 @@
1
- {"version":3,"file":"CreatorCover.cjs","sources":["../../../../src/components/CreatorCover/CreatorCover.tsx"],"sourcesContent":["import * as React from \"react\";\nimport { cn } from \"../../utils/cn\";\nimport { Pill } from \"../Pill/Pill\";\n\n/** Slot that accepts a string (rendered as default styling) or a node for full control. */\nexport type CreatorCoverSlot = string | React.ReactNode;\n\nfunction isNonEmptyString(value: unknown): value is string {\n return typeof value === \"string\" && value.length > 0;\n}\n\nexport interface CreatorCoverProps extends Omit<React.HTMLAttributes<HTMLElement>, \"title\"> {\n /** URL of the creator image displayed in the centre card. Also used as the blurred backdrop unless `backgroundSrc` is provided. */\n imageSrc: string;\n /** Alt text for the centre cover image. @default \"\" */\n imageAlt?: string;\n /** Override URL used for the blurred background image. @default `imageSrc` */\n backgroundSrc?: string;\n /** Creator's name, rendered as the heading. */\n name: string;\n /** Smaller subtitle below the name (e.g. \"GLOBAL POPSTAR\"). Rendered uppercase in the brand colour. */\n tagline?: string;\n /**\n * Status label rendered as a pill overlapping the bottom of the cover image (e.g. \"New Joiner\").\n * Strings render with default green pill styling; pass a node for custom markup.\n */\n tag?: CreatorCoverSlot;\n /**\n * Primary call to action displayed below the title.\n */\n action?: React.ReactNode;\n /** When `true`, fades the bottom of the component to transparent and increases bottom padding to 64px. @default false */\n fadeBottom?: boolean;\n}\n\n/**\n * A creator profile hero with a stylised blurred backdrop, central cover image,\n * status pill, name, tagline, and primary call to action.\n *\n * @example\n * ```tsx\n * <CreatorCover\n * imageSrc=\"/creator.jpg\"\n * imageAlt=\"Jane Doe\"\n * name=\"JANE DOE\"\n * tagline=\"GLOBAL POPSTAR\"\n * tag=\"New Joiner\"\n * action={<Button variant=\"primary\" size=\"48\" fullWidth>Join for free for 7 days</Button>}\n * />\n * ```\n */\nexport const CreatorCover = React.forwardRef<HTMLElement, CreatorCoverProps>(\n (\n { className, imageSrc, imageAlt = \"\", backgroundSrc, name, tagline, tag, action, ...props },\n ref,\n ) => {\n const headingId = React.useId();\n\n const renderedTag = isNonEmptyString(tag) ? <Pill variant=\"brand\">{tag}</Pill> : tag;\n\n return (\n <section\n ref={ref}\n aria-labelledby={headingId}\n data-testid=\"creator-cover\"\n className={cn(\n \"relative isolate w-full overflow-hidden bg-white dark:bg-background-primary\",\n className,\n )}\n {...props}\n >\n <div className=\"absolute inset-0 -z-10\">\n <img\n src={backgroundSrc ?? imageSrc}\n alt=\"\"\n loading=\"lazy\"\n className=\"size-full scale-110 object-cover blur-3xl\"\n />\n <div className=\"absolute inset-0 bg-linear-to-b from-white/30 to-white/15 dark:from-background-primary/30 dark:to-background-primary/15\" />\n <div className=\"absolute inset-x-0 bottom-0 h-1/3 bg-linear-to-b from-transparent to-white dark:to-background-primary\" />\n </div>\n <div className={cn(\"mx-auto flex max-w-90 flex-col items-center gap-4 px-4 pt-17 pb-16\")}>\n <div className=\"relative\">\n <img\n src={imageSrc}\n alt={imageAlt}\n loading=\"lazy\"\n className=\"block h-55 w-37.5 rounded-lg object-cover\"\n />\n {renderedTag ? (\n <div className=\"absolute bottom-0 left-1/2 -translate-x-1/2 translate-y-1/2\">\n {renderedTag}\n </div>\n ) : null}\n </div>\n <div className=\"flex flex-col items-center gap-1 pt-4 text-center\">\n <h2 id={headingId} className=\"typography-header-heading-md m-0 text-white\">\n {name}\n </h2>\n {tagline ? (\n <p className=\"typography-badge-badgecaps m-0 text-brand-primary-default uppercase\">\n {tagline}\n </p>\n ) : null}\n </div>\n {action ? <div className=\"w-full pt-2\">{action}</div> : null}\n </div>\n </section>\n );\n },\n);\n\nCreatorCover.displayName = \"CreatorCover\";\n"],"names":["React","Pill","jsxs","cn","jsx"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;AAOA,SAAS,iBAAiB,OAAiC;AACzD,SAAO,OAAO,UAAU,YAAY,MAAM,SAAS;AACrD;AA0CO,MAAM,eAAeA,iBAAM;AAAA,EAChC,CACE,EAAE,WAAW,UAAU,WAAW,IAAI,eAAe,MAAM,SAAS,KAAK,QAAQ,GAAG,MAAA,GACpF,QACG;AACH,UAAM,YAAYA,iBAAM,MAAA;AAExB,UAAM,cAAc,iBAAiB,GAAG,mCAAKC,KAAAA,MAAA,EAAK,SAAQ,SAAS,UAAA,IAAA,CAAI,IAAU;AAEjF,WACEC,2BAAAA;AAAAA,MAAC;AAAA,MAAA;AAAA,QACC;AAAA,QACA,mBAAiB;AAAA,QACjB,eAAY;AAAA,QACZ,WAAWC,GAAAA;AAAAA,UACT;AAAA,UACA;AAAA,QAAA;AAAA,QAED,GAAG;AAAA,QAEJ,UAAA;AAAA,UAAAD,2BAAAA,KAAC,OAAA,EAAI,WAAU,0BACb,UAAA;AAAA,YAAAE,2BAAAA;AAAAA,cAAC;AAAA,cAAA;AAAA,gBACC,KAAK,iBAAiB;AAAA,gBACtB,KAAI;AAAA,gBACJ,SAAQ;AAAA,gBACR,WAAU;AAAA,cAAA;AAAA,YAAA;AAAA,YAEZA,2BAAAA,IAAC,OAAA,EAAI,WAAU,0HAAA,CAA0H;AAAA,YACzIA,2BAAAA,IAAC,OAAA,EAAI,WAAU,wGAAA,CAAwG;AAAA,UAAA,GACzH;AAAA,UACAF,2BAAAA,KAAC,OAAA,EAAI,WAAWC,GAAAA,GAAG,oEAAoE,GACrF,UAAA;AAAA,YAAAD,2BAAAA,KAAC,OAAA,EAAI,WAAU,YACb,UAAA;AAAA,cAAAE,2BAAAA;AAAAA,gBAAC;AAAA,gBAAA;AAAA,kBACC,KAAK;AAAA,kBACL,KAAK;AAAA,kBACL,SAAQ;AAAA,kBACR,WAAU;AAAA,gBAAA;AAAA,cAAA;AAAA,cAEX,cACCA,2BAAAA,IAAC,OAAA,EAAI,WAAU,+DACZ,uBACH,IACE;AAAA,YAAA,GACN;AAAA,YACAF,2BAAAA,KAAC,OAAA,EAAI,WAAU,qDACb,UAAA;AAAA,cAAAE,+BAAC,MAAA,EAAG,IAAI,WAAW,WAAU,+CAC1B,UAAA,MACH;AAAA,cACC,UACCA,2BAAAA,IAAC,KAAA,EAAE,WAAU,uEACV,mBACH,IACE;AAAA,YAAA,GACN;AAAA,YACC,SAASA,2BAAAA,IAAC,OAAA,EAAI,WAAU,eAAe,kBAAO,IAAS;AAAA,UAAA,EAAA,CAC1D;AAAA,QAAA;AAAA,MAAA;AAAA,IAAA;AAAA,EAGN;AACF;AAEA,aAAa,cAAc;;"}
1
+ {"version":3,"file":"CreatorCover.cjs","sources":["../../../../src/components/CreatorCover/CreatorCover.tsx"],"sourcesContent":["import * as React from \"react\";\nimport { cn } from \"../../utils/cn\";\nimport { Pill } from \"../Pill/Pill\";\n\n/** Slot that accepts a string (rendered as default styling) or a node for full control. */\nexport type CreatorCoverSlot = string | React.ReactNode;\n\nfunction isNonEmptyString(value: unknown): value is string {\n return typeof value === \"string\" && value.length > 0;\n}\n\nexport interface CreatorCoverProps extends Omit<React.HTMLAttributes<HTMLElement>, \"title\"> {\n /** URL of the creator image displayed in the centre card. Also used as the blurred backdrop unless `backgroundSrc` is provided. */\n imageSrc: string;\n /** Alt text for the centre cover image. @default \"\" */\n imageAlt?: string;\n /** Override URL used for the blurred background image. @default `imageSrc` */\n backgroundSrc?: string;\n /** Creator's name, rendered as the heading. */\n name: string;\n /** Smaller subtitle below the name (e.g. \"GLOBAL POPSTAR\"). Rendered uppercase in the brand colour. */\n tagline?: string;\n /**\n * Status label rendered as a pill overlapping the bottom of the cover image (e.g. \"New Joiner\").\n * Strings render with default green pill styling; pass a node for custom markup.\n */\n tag?: CreatorCoverSlot;\n /**\n * Primary call to action displayed below the title.\n */\n action?: React.ReactNode;\n /** When `true`, fades the bottom of the component to transparent and increases bottom padding to 64px. @default false */\n fadeBottom?: boolean;\n}\n\n/**\n * A creator profile hero with a stylised blurred backdrop, central cover image,\n * status pill, name, tagline, and primary call to action.\n *\n * @example\n * ```tsx\n * <CreatorCover\n * imageSrc=\"/creator.jpg\"\n * imageAlt=\"Jane Doe\"\n * name=\"JANE DOE\"\n * tagline=\"GLOBAL POPSTAR\"\n * tag=\"New Joiner\"\n * action={<Button variant=\"brand\" size=\"48\" fullWidth>Join for free for 7 days</Button>}\n * />\n * ```\n */\nexport const CreatorCover = React.forwardRef<HTMLElement, CreatorCoverProps>(\n (\n { className, imageSrc, imageAlt = \"\", backgroundSrc, name, tagline, tag, action, ...props },\n ref,\n ) => {\n const headingId = React.useId();\n\n const renderedTag = isNonEmptyString(tag) ? <Pill variant=\"brand\">{tag}</Pill> : tag;\n\n return (\n <section\n ref={ref}\n aria-labelledby={headingId}\n data-testid=\"creator-cover\"\n className={cn(\n \"relative isolate w-full overflow-hidden bg-white dark:bg-background-primary\",\n className,\n )}\n {...props}\n >\n <div className=\"absolute inset-0 -z-10\">\n <img\n src={backgroundSrc ?? imageSrc}\n alt=\"\"\n loading=\"lazy\"\n className=\"size-full scale-110 object-cover blur-3xl\"\n />\n <div className=\"absolute inset-0 bg-linear-to-b from-white/30 to-white/15 dark:from-background-primary/30 dark:to-background-primary/15\" />\n <div className=\"absolute inset-x-0 bottom-0 h-1/3 bg-linear-to-b from-transparent to-white dark:to-background-primary\" />\n </div>\n <div className={cn(\"mx-auto flex max-w-90 flex-col items-center gap-4 px-4 pt-17 pb-16\")}>\n <div className=\"relative\">\n <img\n src={imageSrc}\n alt={imageAlt}\n loading=\"lazy\"\n className=\"block h-55 w-37.5 rounded-lg object-cover\"\n />\n {renderedTag ? (\n <div className=\"absolute bottom-0 left-1/2 -translate-x-1/2 translate-y-1/2\">\n {renderedTag}\n </div>\n ) : null}\n </div>\n <div className=\"flex flex-col items-center gap-1 pt-4 text-center\">\n <h2 id={headingId} className=\"typography-header-heading-md m-0 text-white\">\n {name}\n </h2>\n {tagline ? (\n <p className=\"typography-badge-badgecaps m-0 text-brand-primary-default uppercase\">\n {tagline}\n </p>\n ) : null}\n </div>\n {action ? <div className=\"w-full pt-2\">{action}</div> : null}\n </div>\n </section>\n );\n },\n);\n\nCreatorCover.displayName = \"CreatorCover\";\n"],"names":["React","Pill","jsxs","cn","jsx"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;AAOA,SAAS,iBAAiB,OAAiC;AACzD,SAAO,OAAO,UAAU,YAAY,MAAM,SAAS;AACrD;AA0CO,MAAM,eAAeA,iBAAM;AAAA,EAChC,CACE,EAAE,WAAW,UAAU,WAAW,IAAI,eAAe,MAAM,SAAS,KAAK,QAAQ,GAAG,MAAA,GACpF,QACG;AACH,UAAM,YAAYA,iBAAM,MAAA;AAExB,UAAM,cAAc,iBAAiB,GAAG,mCAAKC,KAAAA,MAAA,EAAK,SAAQ,SAAS,UAAA,IAAA,CAAI,IAAU;AAEjF,WACEC,2BAAAA;AAAAA,MAAC;AAAA,MAAA;AAAA,QACC;AAAA,QACA,mBAAiB;AAAA,QACjB,eAAY;AAAA,QACZ,WAAWC,GAAAA;AAAAA,UACT;AAAA,UACA;AAAA,QAAA;AAAA,QAED,GAAG;AAAA,QAEJ,UAAA;AAAA,UAAAD,2BAAAA,KAAC,OAAA,EAAI,WAAU,0BACb,UAAA;AAAA,YAAAE,2BAAAA;AAAAA,cAAC;AAAA,cAAA;AAAA,gBACC,KAAK,iBAAiB;AAAA,gBACtB,KAAI;AAAA,gBACJ,SAAQ;AAAA,gBACR,WAAU;AAAA,cAAA;AAAA,YAAA;AAAA,YAEZA,2BAAAA,IAAC,OAAA,EAAI,WAAU,0HAAA,CAA0H;AAAA,YACzIA,2BAAAA,IAAC,OAAA,EAAI,WAAU,wGAAA,CAAwG;AAAA,UAAA,GACzH;AAAA,UACAF,2BAAAA,KAAC,OAAA,EAAI,WAAWC,GAAAA,GAAG,oEAAoE,GACrF,UAAA;AAAA,YAAAD,2BAAAA,KAAC,OAAA,EAAI,WAAU,YACb,UAAA;AAAA,cAAAE,2BAAAA;AAAAA,gBAAC;AAAA,gBAAA;AAAA,kBACC,KAAK;AAAA,kBACL,KAAK;AAAA,kBACL,SAAQ;AAAA,kBACR,WAAU;AAAA,gBAAA;AAAA,cAAA;AAAA,cAEX,cACCA,2BAAAA,IAAC,OAAA,EAAI,WAAU,+DACZ,uBACH,IACE;AAAA,YAAA,GACN;AAAA,YACAF,2BAAAA,KAAC,OAAA,EAAI,WAAU,qDACb,UAAA;AAAA,cAAAE,+BAAC,MAAA,EAAG,IAAI,WAAW,WAAU,+CAC1B,UAAA,MACH;AAAA,cACC,UACCA,2BAAAA,IAAC,KAAA,EAAE,WAAU,uEACV,mBACH,IACE;AAAA,YAAA,GACN;AAAA,YACC,SAASA,2BAAAA,IAAC,OAAA,EAAI,WAAU,eAAe,kBAAO,IAAS;AAAA,UAAA,EAAA,CAC1D;AAAA,QAAA;AAAA,MAAA;AAAA,IAAA;AAAA,EAGN;AACF;AAEA,aAAa,cAAc;;"}
@@ -56,13 +56,14 @@ const DialogContent = React__namespace.forwardRef(
56
56
  size = "md",
57
57
  overlay = true,
58
58
  portal = true,
59
+ showMobileHandle = true,
59
60
  style,
60
61
  onOpenAutoFocus,
61
62
  ...props
62
63
  }, ref) => {
63
64
  const content = /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
64
65
  overlay && /* @__PURE__ */ jsxRuntime.jsx(DialogOverlay, {}),
65
- /* @__PURE__ */ jsxRuntime.jsx(
66
+ /* @__PURE__ */ jsxRuntime.jsxs(
66
67
  DialogPrimitive__namespace.Content,
67
68
  {
68
69
  ref,
@@ -76,13 +77,13 @@ const DialogContent = React__namespace.forwardRef(
76
77
  e.currentTarget.focus();
77
78
  },
78
79
  className: cn.cn(
79
- "fixed flex flex-col overflow-hidden bg-background-primary shadow-lg focus:outline-none dark:bg-surface-primary",
80
- "inset-x-0 bottom-0 max-h-[85vh] w-full rounded-t-lg",
80
+ "fixed flex flex-col overflow-hidden border border-modal-stroke bg-modal-background shadow-blur-menu backdrop-blur-[4px] focus:outline-none",
81
+ "inset-x-0 bottom-0 max-h-[85vh] w-full rounded-t-xl p-4 pt-3",
81
82
  "data-[state=open]:fade-in-0 data-[state=open]:animate-in",
82
83
  "data-[state=closed]:fade-out-0 data-[state=closed]:animate-out",
83
84
  "data-[state=open]:slide-in-from-bottom-full",
84
85
  "data-[state=closed]:slide-out-to-bottom-full",
85
- "sm:inset-auto sm:top-1/2 sm:left-1/2 sm:max-h-[85vh] sm:-translate-x-1/2 sm:-translate-y-1/2 sm:rounded-lg",
86
+ "sm:inset-auto sm:top-1/2 sm:left-1/2 sm:max-h-[85vh] sm:w-full sm:-translate-x-1/2 sm:-translate-y-1/2 sm:rounded-xl sm:p-6",
86
87
  "sm:data-[state=open]:slide-in-from-bottom-0 sm:data-[state=open]:zoom-in-95",
87
88
  "sm:data-[state=closed]:slide-out-to-bottom-0 sm:data-[state=closed]:zoom-out-95",
88
89
  "duration-200",
@@ -90,7 +91,16 @@ const DialogContent = React__namespace.forwardRef(
90
91
  className
91
92
  ),
92
93
  ...props,
93
- children
94
+ children: [
95
+ showMobileHandle && /* @__PURE__ */ jsxRuntime.jsx(
96
+ "div",
97
+ {
98
+ "aria-hidden": "true",
99
+ className: "mb-3 h-1 w-8 shrink-0 self-center rounded-full bg-icons-tertiary sm:hidden"
100
+ }
101
+ ),
102
+ children
103
+ ]
94
104
  }
95
105
  )
96
106
  ] });
@@ -114,22 +124,30 @@ const DialogHeader = React__namespace.forwardRef(
114
124
  "div",
115
125
  {
116
126
  ref,
117
- className: cn.cn("flex h-16 shrink-0 items-center gap-2 px-6 py-4", className),
127
+ className: cn.cn("flex shrink-0 items-center justify-end gap-4", className),
118
128
  ...props,
119
129
  children: [
120
130
  shouldShowBack && /* @__PURE__ */ jsxRuntime.jsx(
121
131
  IconButton.IconButton,
122
132
  {
123
- variant: "tertiary",
133
+ variant: "secondary",
124
134
  size: "32",
125
- icon: /* @__PURE__ */ jsxRuntime.jsx(ArrowLeftIcon.ArrowLeftIcon, {}),
135
+ icon: /* @__PURE__ */ jsxRuntime.jsx(ArrowLeftIcon.ArrowLeftIcon, { size: 16 }),
126
136
  onClick: onBack,
127
137
  disabled: !onBack,
128
138
  "aria-label": backLabel
129
139
  }
130
140
  ),
131
141
  /* @__PURE__ */ jsxRuntime.jsx("div", { className: "min-w-0 flex-1", children }),
132
- showClose && /* @__PURE__ */ jsxRuntime.jsx(DialogPrimitive__namespace.Close, { asChild: true, children: /* @__PURE__ */ jsxRuntime.jsx(IconButton.IconButton, { variant: "tertiary", size: "32", icon: /* @__PURE__ */ jsxRuntime.jsx(CloseIcon.CloseIcon, {}), "aria-label": closeLabel }) })
142
+ showClose && /* @__PURE__ */ jsxRuntime.jsx(DialogPrimitive__namespace.Close, { asChild: true, children: /* @__PURE__ */ jsxRuntime.jsx(
143
+ IconButton.IconButton,
144
+ {
145
+ variant: "secondary",
146
+ size: "32",
147
+ icon: /* @__PURE__ */ jsxRuntime.jsx(CloseIcon.CloseIcon, { size: 16 }),
148
+ "aria-label": closeLabel
149
+ }
150
+ ) })
133
151
  ]
134
152
  }
135
153
  );
@@ -140,7 +158,7 @@ const DialogTitle = React__namespace.forwardRef(({ className, ...props }, ref) =
140
158
  DialogPrimitive__namespace.Title,
141
159
  {
142
160
  ref,
143
- className: cn.cn("typography-header-heading-xs truncate text-content-primary", className),
161
+ className: cn.cn("typography-header-heading-xs text-content-primary", className),
144
162
  ...props
145
163
  }
146
164
  ));
@@ -155,7 +173,7 @@ const DialogDescription = React__namespace.forwardRef(({ className, ...props },
155
173
  ));
156
174
  DialogDescription.displayName = "DialogDescription";
157
175
  const DialogBody = React__namespace.forwardRef(
158
- ({ className, ...props }, ref) => /* @__PURE__ */ jsxRuntime.jsx("div", { ref, className: cn.cn("flex-1 overflow-y-auto px-6 py-4", className), ...props })
176
+ ({ className, ...props }, ref) => /* @__PURE__ */ jsxRuntime.jsx("div", { ref, className: cn.cn("flex-1 overflow-y-auto py-4 sm:py-6", className), ...props })
159
177
  );
160
178
  DialogBody.displayName = "DialogBody";
161
179
  const DialogFooter = React__namespace.forwardRef(
@@ -163,11 +181,7 @@ const DialogFooter = React__namespace.forwardRef(
163
181
  "div",
164
182
  {
165
183
  ref,
166
- className: cn.cn(
167
- "flex shrink-0 items-center gap-3 px-6 pt-3 pb-6",
168
- "[&>*]:min-w-0 [&>*]:flex-1",
169
- className
170
- ),
184
+ className: cn.cn("flex shrink-0 items-center gap-2", "[&>*]:min-w-0 [&>*]:flex-1", className),
171
185
  ...props
172
186
  }
173
187
  )
@@ -1 +1 @@
1
- {"version":3,"file":"Dialog.cjs","sources":["../../../../src/components/Dialog/Dialog.tsx"],"sourcesContent":["import * as DialogPrimitive from \"@radix-ui/react-dialog\";\nimport * as React from \"react\";\nimport { cn } from \"../../utils/cn\";\nimport { useSuppressClickAfterDrag } from \"../../utils/useSuppressClickAfterDrag\";\nimport { IconButton } from \"../IconButton/IconButton\";\nimport { ArrowLeftIcon } from \"../Icons/ArrowLeftIcon\";\nimport { CloseIcon } from \"../Icons/CloseIcon\";\n\n/** Props for the {@link Dialog} root component. */\nexport interface DialogProps extends React.ComponentPropsWithoutRef<typeof DialogPrimitive.Root> {\n /** Controlled open state. When provided, you must also supply `onOpenChange`. */\n open?: boolean;\n /** Called when the open state changes. Required when `open` is controlled. */\n onOpenChange?: (open: boolean) => void;\n /** The open state of the dialog when it is initially rendered (uncontrolled). */\n defaultOpen?: boolean;\n}\n\n/** Root component that manages open/close state for a dialog. */\nexport const Dialog = DialogPrimitive.Root;\n\n/** Props for the {@link DialogTrigger} component. */\nexport type DialogTriggerProps = React.ComponentPropsWithoutRef<typeof DialogPrimitive.Trigger>;\n\n/**\n * The element that opens the dialog when clicked.\n *\n * On touch / pen, a press-and-release that crosses a small movement threshold\n * is treated as a drag and the resulting synthetic click is suppressed —\n * defends against Android Chrome opening the dialog on a scroll-drag-end.\n */\nexport const DialogTrigger = React.forwardRef<\n React.ComponentRef<typeof DialogPrimitive.Trigger>,\n DialogTriggerProps\n>((props, ref) => <DialogPrimitive.Trigger ref={ref} {...useSuppressClickAfterDrag(props)} />);\nDialogTrigger.displayName = \"DialogTrigger\";\n\n/** Convenience alias for Radix `Dialog.Close`. Closes the dialog when clicked. */\nexport const DialogClose = DialogPrimitive.Close;\n\n/** Props for the {@link DialogClose} component. */\nexport type DialogCloseProps = React.ComponentPropsWithoutRef<typeof DialogPrimitive.Close>;\n\nexport interface DialogOverlayProps\n extends React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay> {}\n\n/**\n * Semi-transparent backdrop rendered behind the dialog content.\n * Rendered by {@link DialogContent}; portaled to `document.body` when {@link DialogContent} `portal` is true.\n */\nexport const DialogOverlay = React.forwardRef<\n React.ComponentRef<typeof DialogPrimitive.Overlay>,\n DialogOverlayProps\n>(({ className, style, ...props }, ref) => (\n <DialogPrimitive.Overlay\n ref={ref}\n className={cn(\n \"data-[state=open]:fade-in-0 data-[state=closed]:fade-out-0 fixed inset-0 bg-background-overlay-default data-[state=closed]:animate-out data-[state=open]:animate-in\",\n className,\n )}\n style={{ zIndex: \"var(--fanvue-ui-portal-z-index, 50)\", ...style }}\n {...props}\n />\n));\nDialogOverlay.displayName = \"DialogOverlay\";\n\nexport interface DialogContentProps\n extends React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content> {\n /**\n * Width preset for the dialog.\n * - `\"sm\"` — 400px max-width (confirmations, simple forms)\n * - `\"md\"` — 440px max-width (default, standard dialogs)\n * - `\"lg\"` — 600px max-width (complex content, tables)\n *\n * @default \"md\"\n */\n size?: \"sm\" | \"md\" | \"lg\";\n /** When true, renders overlay automatically. @default true */\n overlay?: boolean;\n /**\n * When true, teleports overlay and panel to `document.body`.\n * When false, renders inline in the React tree (useful inside theme providers or scoped containers).\n * @default true\n */\n portal?: boolean;\n}\n\nconst SIZE_CLASSES: Record<NonNullable<DialogContentProps[\"size\"]>, string> = {\n sm: \"sm:max-w-[400px]\",\n md: \"sm:max-w-[440px]\",\n lg: \"sm:max-w-[600px]\",\n};\n\n/**\n * The dialog panel. Includes the overlay by default and portals to `document.body` by default.\n *\n * Set `portal={false}` to keep overlay and content in the DOM subtree of the parent `Dialog`.\n * `fixed` positioning still applies; ancestors with `transform` or `overflow` may affect layout.\n *\n * On mobile viewports (<640px), the dialog slides up from the bottom as a sheet\n * with top-only border radius. On larger viewports it renders centered with\n * full border radius.\n *\n * @example\n * ```tsx\n * <Dialog>\n * <DialogTrigger asChild>\n * <Button>Open</Button>\n * </DialogTrigger>\n * <DialogContent>\n * <DialogHeader>\n * <DialogTitle>Title</DialogTitle>\n * </DialogHeader>\n * <DialogBody>Content here</DialogBody>\n * <DialogFooter>\n * <DialogClose asChild>\n * <Button variant=\"secondary\">Cancel</Button>\n * </DialogClose>\n * <Button>Accept</Button>\n * </DialogFooter>\n * </DialogContent>\n * </Dialog>\n * ```\n */\nexport const DialogContent = React.forwardRef<\n React.ComponentRef<typeof DialogPrimitive.Content>,\n DialogContentProps\n>(\n (\n {\n className,\n children,\n size = \"md\",\n overlay = true,\n portal = true,\n style,\n onOpenAutoFocus,\n ...props\n },\n ref,\n ) => {\n const content = (\n <>\n {overlay && <DialogOverlay />}\n <DialogPrimitive.Content\n ref={ref}\n style={{ zIndex: \"var(--fanvue-ui-portal-z-index, 50)\", ...style }}\n onOpenAutoFocus={(e) => {\n if (onOpenAutoFocus) {\n onOpenAutoFocus(e);\n return;\n }\n e.preventDefault();\n (e.currentTarget as HTMLElement).focus();\n }}\n className={cn(\n \"fixed flex flex-col overflow-hidden bg-background-primary shadow-lg focus:outline-none dark:bg-surface-primary\",\n \"inset-x-0 bottom-0 max-h-[85vh] w-full rounded-t-lg\",\n \"data-[state=open]:fade-in-0 data-[state=open]:animate-in\",\n \"data-[state=closed]:fade-out-0 data-[state=closed]:animate-out\",\n \"data-[state=open]:slide-in-from-bottom-full\",\n \"data-[state=closed]:slide-out-to-bottom-full\",\n \"sm:inset-auto sm:top-1/2 sm:left-1/2 sm:max-h-[85vh] sm:-translate-x-1/2 sm:-translate-y-1/2 sm:rounded-lg\",\n \"sm:data-[state=open]:slide-in-from-bottom-0 sm:data-[state=open]:zoom-in-95\",\n \"sm:data-[state=closed]:slide-out-to-bottom-0 sm:data-[state=closed]:zoom-out-95\",\n \"duration-200\",\n SIZE_CLASSES[size],\n className,\n )}\n {...props}\n >\n {children}\n </DialogPrimitive.Content>\n </>\n );\n\n return portal ? <DialogPrimitive.Portal>{content}</DialogPrimitive.Portal> : content;\n },\n);\nDialogContent.displayName = \"DialogContent\";\n\nexport interface DialogHeaderProps extends React.HTMLAttributes<HTMLDivElement> {\n /** Show the close (X) button in the header. @default true */\n showClose?: boolean;\n /** Show a back arrow button on the left side. Defaults to `true` when `onBack` is provided. */\n showBack?: boolean;\n /** Called when the back button is clicked. */\n onBack?: () => void;\n /** Accessible label for the back button. @default \"Go back\" */\n backLabel?: string;\n /** Accessible label for the close button. @default \"Close\" */\n closeLabel?: string;\n}\n\n/**\n * Header bar for the dialog. Renders the title with an optional back arrow\n * and close button.\n *\n * @example\n * ```tsx\n * <DialogHeader>\n * <DialogTitle>Settings</DialogTitle>\n * </DialogHeader>\n *\n * <DialogHeader showBack onBack={() => setStep(0)}>\n * <DialogTitle>Step 2</DialogTitle>\n * </DialogHeader>\n * ```\n */\nexport const DialogHeader = React.forwardRef<HTMLDivElement, DialogHeaderProps>(\n (\n {\n className,\n children,\n showClose = true,\n showBack,\n onBack,\n backLabel = \"Go back\",\n closeLabel = \"Close\",\n ...props\n },\n ref,\n ) => {\n const shouldShowBack = showBack ?? !!onBack;\n\n return (\n <div\n ref={ref}\n className={cn(\"flex h-16 shrink-0 items-center gap-2 px-6 py-4\", className)}\n {...props}\n >\n {shouldShowBack && (\n <IconButton\n variant=\"tertiary\"\n size=\"32\"\n icon={<ArrowLeftIcon />}\n onClick={onBack}\n disabled={!onBack}\n aria-label={backLabel}\n />\n )}\n <div className=\"min-w-0 flex-1\">{children}</div>\n {showClose && (\n <DialogPrimitive.Close asChild>\n <IconButton variant=\"tertiary\" size=\"32\" icon={<CloseIcon />} aria-label={closeLabel} />\n </DialogPrimitive.Close>\n )}\n </div>\n );\n },\n);\nDialogHeader.displayName = \"DialogHeader\";\n\n/** Props for the {@link DialogTitle} component. */\nexport type DialogTitleProps = React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title>;\n\n/**\n * Accessible title for the dialog. Must be rendered inside {@link DialogHeader}\n * or directly within {@link DialogContent}.\n */\nexport const DialogTitle = React.forwardRef<\n React.ComponentRef<typeof DialogPrimitive.Title>,\n DialogTitleProps\n>(({ className, ...props }, ref) => (\n <DialogPrimitive.Title\n ref={ref}\n className={cn(\"typography-header-heading-xs truncate text-content-primary\", className)}\n {...props}\n />\n));\nDialogTitle.displayName = \"DialogTitle\";\n\n/** Props for the {@link DialogDescription} component. */\nexport type DialogDescriptionProps = React.ComponentPropsWithoutRef<\n typeof DialogPrimitive.Description\n>;\n\n/** Accessible description for the dialog. Rendered as secondary text. */\nexport const DialogDescription = React.forwardRef<\n React.ComponentRef<typeof DialogPrimitive.Description>,\n DialogDescriptionProps\n>(({ className, ...props }, ref) => (\n <DialogPrimitive.Description\n ref={ref}\n className={cn(\"typography-body-default-16px-regular text-content-secondary\", className)}\n {...props}\n />\n));\nDialogDescription.displayName = \"DialogDescription\";\n\nexport interface DialogBodyProps extends React.HTMLAttributes<HTMLDivElement> {}\n\n/**\n * Scrollable content area (slot) between the header and footer.\n * Grows to fill available space and scrolls when content overflows.\n */\nexport const DialogBody = React.forwardRef<HTMLDivElement, DialogBodyProps>(\n ({ className, ...props }, ref) => (\n <div ref={ref} className={cn(\"flex-1 overflow-y-auto px-6 py-4\", className)} {...props} />\n ),\n);\nDialogBody.displayName = \"DialogBody\";\n\nexport interface DialogFooterProps extends React.HTMLAttributes<HTMLDivElement> {}\n\n/**\n * Footer bar for the dialog. Typically contains action buttons.\n * Children are laid out in a horizontal row with equal flex-basis.\n */\nexport const DialogFooter = React.forwardRef<HTMLDivElement, DialogFooterProps>(\n ({ className, ...props }, ref) => (\n <div\n ref={ref}\n className={cn(\n \"flex shrink-0 items-center gap-3 px-6 pt-3 pb-6\",\n \"[&>*]:min-w-0 [&>*]:flex-1\",\n className,\n )}\n {...props}\n />\n ),\n);\nDialogFooter.displayName = \"DialogFooter\";\n"],"names":["DialogPrimitive","React","jsx","useSuppressClickAfterDrag","cn","jsxs","Fragment","IconButton","ArrowLeftIcon","CloseIcon"],"mappings":"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"}
1
+ {"version":3,"file":"Dialog.cjs","sources":["../../../../src/components/Dialog/Dialog.tsx"],"sourcesContent":["import * as DialogPrimitive from \"@radix-ui/react-dialog\";\nimport * as React from \"react\";\nimport { cn } from \"../../utils/cn\";\nimport { useSuppressClickAfterDrag } from \"../../utils/useSuppressClickAfterDrag\";\nimport { IconButton } from \"../IconButton/IconButton\";\nimport { ArrowLeftIcon } from \"../Icons/ArrowLeftIcon\";\nimport { CloseIcon } from \"../Icons/CloseIcon\";\n\n/** Props for the {@link Dialog} root component. */\nexport interface DialogProps extends React.ComponentPropsWithoutRef<typeof DialogPrimitive.Root> {\n /** Controlled open state. When provided, you must also supply `onOpenChange`. */\n open?: boolean;\n /** Called when the open state changes. Required when `open` is controlled. */\n onOpenChange?: (open: boolean) => void;\n /** The open state of the dialog when it is initially rendered (uncontrolled). */\n defaultOpen?: boolean;\n}\n\n/** Root component that manages open/close state for a dialog. */\nexport const Dialog = DialogPrimitive.Root;\n\n/** Props for the {@link DialogTrigger} component. */\nexport type DialogTriggerProps = React.ComponentPropsWithoutRef<typeof DialogPrimitive.Trigger>;\n\n/**\n * The element that opens the dialog when clicked.\n *\n * On touch / pen, a press-and-release that crosses a small movement threshold\n * is treated as a drag and the resulting synthetic click is suppressed —\n * defends against Android Chrome opening the dialog on a scroll-drag-end.\n */\nexport const DialogTrigger = React.forwardRef<\n React.ComponentRef<typeof DialogPrimitive.Trigger>,\n DialogTriggerProps\n>((props, ref) => <DialogPrimitive.Trigger ref={ref} {...useSuppressClickAfterDrag(props)} />);\nDialogTrigger.displayName = \"DialogTrigger\";\n\n/** Convenience alias for Radix `Dialog.Close`. Closes the dialog when clicked. */\nexport const DialogClose = DialogPrimitive.Close;\n\n/** Props for the {@link DialogClose} component. */\nexport type DialogCloseProps = React.ComponentPropsWithoutRef<typeof DialogPrimitive.Close>;\n\nexport interface DialogOverlayProps\n extends React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay> {}\n\n/**\n * Semi-transparent backdrop rendered behind the dialog content.\n * Rendered by {@link DialogContent}; portaled to `document.body` when {@link DialogContent} `portal` is true.\n */\nexport const DialogOverlay = React.forwardRef<\n React.ComponentRef<typeof DialogPrimitive.Overlay>,\n DialogOverlayProps\n>(({ className, style, ...props }, ref) => (\n <DialogPrimitive.Overlay\n ref={ref}\n className={cn(\n \"data-[state=open]:fade-in-0 data-[state=closed]:fade-out-0 fixed inset-0 bg-background-overlay-default data-[state=closed]:animate-out data-[state=open]:animate-in\",\n className,\n )}\n style={{ zIndex: \"var(--fanvue-ui-portal-z-index, 50)\", ...style }}\n {...props}\n />\n));\nDialogOverlay.displayName = \"DialogOverlay\";\n\nexport interface DialogContentProps\n extends React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content> {\n /**\n * Width preset for the dialog.\n * - `\"sm\"` — 400px max-width (confirmations, simple forms)\n * - `\"md\"` — 440px max-width (default, standard dialogs)\n * - `\"lg\"` — 600px max-width (complex content, tables)\n *\n * @default \"md\"\n */\n size?: \"sm\" | \"md\" | \"lg\";\n /** When true, renders overlay automatically. @default true */\n overlay?: boolean;\n /**\n * When true, teleports overlay and panel to `document.body`.\n * When false, renders inline in the React tree (useful inside theme providers or scoped containers).\n * @default true\n */\n portal?: boolean;\n /** Show the v2 mobile sheet pull handle. @default true */\n showMobileHandle?: boolean;\n}\n\nconst SIZE_CLASSES: Record<NonNullable<DialogContentProps[\"size\"]>, string> = {\n sm: \"sm:max-w-[400px]\",\n md: \"sm:max-w-[440px]\",\n lg: \"sm:max-w-[600px]\",\n};\n\n/**\n * The dialog panel. Includes the overlay by default and portals to `document.body` by default.\n *\n * Set `portal={false}` to keep overlay and content in the DOM subtree of the parent `Dialog`.\n * `fixed` positioning still applies; ancestors with `transform` or `overflow` may affect layout.\n *\n * On mobile viewports (<640px), the dialog slides up from the bottom as a sheet\n * with top-only border radius. On larger viewports it renders centered with\n * full border radius.\n *\n * @example\n * ```tsx\n * <Dialog>\n * <DialogTrigger asChild>\n * <Button>Open</Button>\n * </DialogTrigger>\n * <DialogContent>\n * <DialogHeader>\n * <DialogTitle>Title</DialogTitle>\n * </DialogHeader>\n * <DialogBody>Content here</DialogBody>\n * <DialogFooter>\n * <DialogClose asChild>\n * <Button variant=\"secondary\">Cancel</Button>\n * </DialogClose>\n * <Button>Accept</Button>\n * </DialogFooter>\n * </DialogContent>\n * </Dialog>\n * ```\n */\nexport const DialogContent = React.forwardRef<\n React.ComponentRef<typeof DialogPrimitive.Content>,\n DialogContentProps\n>(\n (\n {\n className,\n children,\n size = \"md\",\n overlay = true,\n portal = true,\n showMobileHandle = true,\n style,\n onOpenAutoFocus,\n ...props\n },\n ref,\n ) => {\n const content = (\n <>\n {overlay && <DialogOverlay />}\n <DialogPrimitive.Content\n ref={ref}\n style={{ zIndex: \"var(--fanvue-ui-portal-z-index, 50)\", ...style }}\n onOpenAutoFocus={(e) => {\n if (onOpenAutoFocus) {\n onOpenAutoFocus(e);\n return;\n }\n e.preventDefault();\n (e.currentTarget as HTMLElement).focus();\n }}\n className={cn(\n \"fixed flex flex-col overflow-hidden border border-modal-stroke bg-modal-background shadow-blur-menu backdrop-blur-[4px] focus:outline-none\",\n \"inset-x-0 bottom-0 max-h-[85vh] w-full rounded-t-xl p-4 pt-3\",\n \"data-[state=open]:fade-in-0 data-[state=open]:animate-in\",\n \"data-[state=closed]:fade-out-0 data-[state=closed]:animate-out\",\n \"data-[state=open]:slide-in-from-bottom-full\",\n \"data-[state=closed]:slide-out-to-bottom-full\",\n \"sm:inset-auto sm:top-1/2 sm:left-1/2 sm:max-h-[85vh] sm:w-full sm:-translate-x-1/2 sm:-translate-y-1/2 sm:rounded-xl sm:p-6\",\n \"sm:data-[state=open]:slide-in-from-bottom-0 sm:data-[state=open]:zoom-in-95\",\n \"sm:data-[state=closed]:slide-out-to-bottom-0 sm:data-[state=closed]:zoom-out-95\",\n \"duration-200\",\n SIZE_CLASSES[size],\n className,\n )}\n {...props}\n >\n {showMobileHandle && (\n <div\n aria-hidden=\"true\"\n className=\"mb-3 h-1 w-8 shrink-0 self-center rounded-full bg-icons-tertiary sm:hidden\"\n />\n )}\n {children}\n </DialogPrimitive.Content>\n </>\n );\n\n return portal ? <DialogPrimitive.Portal>{content}</DialogPrimitive.Portal> : content;\n },\n);\nDialogContent.displayName = \"DialogContent\";\n\nexport interface DialogHeaderProps extends React.HTMLAttributes<HTMLDivElement> {\n /** Show the close (X) button in the header. @default true */\n showClose?: boolean;\n /** Show a back arrow button on the left side. Defaults to `true` when `onBack` is provided. */\n showBack?: boolean;\n /** Called when the back button is clicked. */\n onBack?: () => void;\n /** Accessible label for the back button. @default \"Go back\" */\n backLabel?: string;\n /** Accessible label for the close button. @default \"Close\" */\n closeLabel?: string;\n}\n\n/**\n * Header bar for the dialog. Renders the title with an optional back arrow\n * and close button.\n *\n * @example\n * ```tsx\n * <DialogHeader>\n * <DialogTitle>Settings</DialogTitle>\n * </DialogHeader>\n *\n * <DialogHeader showBack onBack={() => setStep(0)}>\n * <DialogTitle>Step 2</DialogTitle>\n * </DialogHeader>\n * ```\n */\nexport const DialogHeader = React.forwardRef<HTMLDivElement, DialogHeaderProps>(\n (\n {\n className,\n children,\n showClose = true,\n showBack,\n onBack,\n backLabel = \"Go back\",\n closeLabel = \"Close\",\n ...props\n },\n ref,\n ) => {\n const shouldShowBack = showBack ?? !!onBack;\n\n return (\n <div\n ref={ref}\n className={cn(\"flex shrink-0 items-center justify-end gap-4\", className)}\n {...props}\n >\n {shouldShowBack && (\n <IconButton\n variant=\"secondary\"\n size=\"32\"\n icon={<ArrowLeftIcon size={16} />}\n onClick={onBack}\n disabled={!onBack}\n aria-label={backLabel}\n />\n )}\n <div className=\"min-w-0 flex-1\">{children}</div>\n {showClose && (\n <DialogPrimitive.Close asChild>\n <IconButton\n variant=\"secondary\"\n size=\"32\"\n icon={<CloseIcon size={16} />}\n aria-label={closeLabel}\n />\n </DialogPrimitive.Close>\n )}\n </div>\n );\n },\n);\nDialogHeader.displayName = \"DialogHeader\";\n\n/** Props for the {@link DialogTitle} component. */\nexport type DialogTitleProps = React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title>;\n\n/**\n * Accessible title for the dialog. Must be rendered inside {@link DialogHeader}\n * or directly within {@link DialogContent}.\n */\nexport const DialogTitle = React.forwardRef<\n React.ComponentRef<typeof DialogPrimitive.Title>,\n DialogTitleProps\n>(({ className, ...props }, ref) => (\n <DialogPrimitive.Title\n ref={ref}\n className={cn(\"typography-header-heading-xs text-content-primary\", className)}\n {...props}\n />\n));\nDialogTitle.displayName = \"DialogTitle\";\n\n/** Props for the {@link DialogDescription} component. */\nexport type DialogDescriptionProps = React.ComponentPropsWithoutRef<\n typeof DialogPrimitive.Description\n>;\n\n/** Accessible description for the dialog. Rendered as secondary text. */\nexport const DialogDescription = React.forwardRef<\n React.ComponentRef<typeof DialogPrimitive.Description>,\n DialogDescriptionProps\n>(({ className, ...props }, ref) => (\n <DialogPrimitive.Description\n ref={ref}\n className={cn(\"typography-body-default-16px-regular text-content-secondary\", className)}\n {...props}\n />\n));\nDialogDescription.displayName = \"DialogDescription\";\n\nexport interface DialogBodyProps extends React.HTMLAttributes<HTMLDivElement> {}\n\n/**\n * Scrollable content area (slot) between the header and footer.\n * Grows to fill available space and scrolls when content overflows.\n */\nexport const DialogBody = React.forwardRef<HTMLDivElement, DialogBodyProps>(\n ({ className, ...props }, ref) => (\n <div ref={ref} className={cn(\"flex-1 overflow-y-auto py-4 sm:py-6\", className)} {...props} />\n ),\n);\nDialogBody.displayName = \"DialogBody\";\n\nexport interface DialogFooterProps extends React.HTMLAttributes<HTMLDivElement> {}\n\n/**\n * Footer bar for the dialog. Typically contains action buttons.\n * Children are laid out in a horizontal row with equal flex-basis.\n */\nexport const DialogFooter = React.forwardRef<HTMLDivElement, DialogFooterProps>(\n ({ className, ...props }, ref) => (\n <div\n ref={ref}\n className={cn(\"flex shrink-0 items-center gap-2\", \"[&>*]:min-w-0 [&>*]:flex-1\", className)}\n {...props}\n />\n ),\n);\nDialogFooter.displayName = \"DialogFooter\";\n"],"names":["DialogPrimitive","React","jsx","useSuppressClickAfterDrag","cn","jsxs","Fragment","IconButton","ArrowLeftIcon","CloseIcon"],"mappings":"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"}
@@ -3,13 +3,26 @@ import { jsx, jsxs, Fragment } from "react/jsx-runtime";
3
3
  import { Slot } from "@radix-ui/react-slot";
4
4
  import * as React from "react";
5
5
  import { cn } from "../../utils/cn.mjs";
6
+ import { AIIcon } from "../Icons/AIIcon.mjs";
6
7
  import { SpinnerIcon } from "../Icons/SpinnerIcon.mjs";
8
+ const NEGATIVE_AWARE_VARIANTS = /* @__PURE__ */ new Set([
9
+ "primary",
10
+ "secondary",
11
+ "tertiary",
12
+ "outline"
13
+ ]);
7
14
  const SIZE_CLASSES = {
8
- "48": "h-12 px-4 py-3 typography-body-default-16px-semibold",
15
+ "48": "h-12 px-6 py-3 typography-body-default-16px-semibold",
9
16
  "40": "h-10 px-4 py-2 typography-body-default-16px-semibold",
10
- "32": "h-8 px-3 py-2 typography-body-small-14px-semibold",
17
+ "32": "h-8 px-3 py-[7px] typography-body-small-14px-semibold",
11
18
  "24": "h-6 px-2 py-1 typography-body-small-14px-semibold"
12
19
  };
20
+ const ICON_SIDE_PADDING_CLASSES = {
21
+ "48": { left: "pl-5", right: "pr-5" },
22
+ "40": { left: "pl-3", right: "pr-3" },
23
+ "32": { left: "pl-2", right: "pr-2" },
24
+ "24": { left: "pl-1", right: "pr-1" }
25
+ };
13
26
  const ICON_SIZE_CLASS = {
14
27
  "48": "size-5",
15
28
  "40": "size-5",
@@ -22,17 +35,78 @@ const ICON_WRAPPER_CLASS = {
22
35
  "32": "[&>svg]:size-4",
23
36
  "24": "[&>svg]:size-3.5"
24
37
  };
38
+ const AI_GRADIENT = "[background-clip:padding-box,border-box] [background-image:linear-gradient(50deg,var(--color-buttons-ai-background-gradient-default-start)_11.87%,var(--color-buttons-ai-background-gradient-default-end)_112.39%),linear-gradient(50deg,var(--color-buttons-ai-stroke-start)_11.87%,var(--color-buttons-ai-stroke-end)_112.39%)] [background-origin:border-box] hover:[background-image:linear-gradient(50deg,var(--color-buttons-ai-background-gradient-hover-start)_11.87%,var(--color-buttons-ai-background-gradient-hover-end)_112.39%),linear-gradient(50deg,var(--color-buttons-ai-stroke-start)_11.87%,var(--color-buttons-ai-stroke-end)_112.39%)]";
39
+ const DISABLED_FILL = "bg-buttons-disabled-default text-content-disabled";
40
+ const DISABLED_FILL_NEGATIVE = "bg-buttons-disabled-negative text-content-disabled";
41
+ const DISABLED_TRANSPARENT = "bg-transparent text-content-disabled";
25
42
  const VARIANT_CLASSES = {
26
- primary: "bg-buttons-primary-default text-content-primary-inverted hover:bg-buttons-primary-hover hover:text-content-primary-inverted active:bg-buttons-primary-hover active:text-content-primary-inverted",
27
- secondary: "border-content-primary border bg-transparent text-content-primary hover:bg-brand-primary-muted active:bg-brand-primary-muted",
28
- tertiary: "bg-transparent text-content-primary hover:bg-brand-primary-muted active:bg-brand-primary-muted",
29
- link: "bg-transparent text-content-primary underline decoration-solid hover:bg-brand-primary-muted active:bg-brand-primary-muted",
30
- brand: "bg-buttons-brand-default text-content-always-black hover:bg-buttons-brand-hover hover:text-content-always-black active:bg-buttons-brand-hover active:text-content-always-black",
31
- destructive: "bg-error-content text-content-always-white hover:bg-brand-primary-muted hover:text-content-primary active:bg-brand-primary-muted active:text-content-primary",
32
- white: "bg-content-always-white text-content-always-black hover:bg-brand-primary-muted hover:text-content-primary active:bg-brand-primary-muted active:text-content-primary",
33
- tertiaryDestructive: "bg-transparent text-error-content hover:bg-error-surface active:bg-error-surface",
34
- text: "bg-transparent text-content-primary hover:underline active:underline"
43
+ primary: {
44
+ default: "bg-buttons-primary-default text-content-primary-inverted hover:bg-buttons-primary-hover hover:text-content-primary-inverted active:bg-buttons-primary-hover active:text-content-primary-inverted",
45
+ disabled: DISABLED_FILL,
46
+ negative: "bg-buttons-primary-negative-default text-content-primary hover:bg-buttons-primary-negative-hover active:bg-buttons-primary-negative-hover",
47
+ negativeDisabled: DISABLED_FILL_NEGATIVE
48
+ },
49
+ secondary: {
50
+ default: "bg-buttons-secondary-default text-content-primary hover:bg-buttons-secondary-hover active:bg-buttons-secondary-hover",
51
+ disabled: DISABLED_FILL,
52
+ negative: "bg-buttons-secondary-negative-default text-content-primary-inverted hover:bg-buttons-secondary-negative-hover active:bg-buttons-secondary-negative-hover",
53
+ negativeDisabled: DISABLED_FILL_NEGATIVE
54
+ },
55
+ tertiary: {
56
+ default: "bg-transparent text-content-primary hover:bg-buttons-tertiary-hover active:bg-buttons-tertiary-hover",
57
+ disabled: DISABLED_TRANSPARENT,
58
+ negative: "bg-transparent text-content-primary-inverted hover:bg-buttons-tertiary-negative-hover active:bg-buttons-tertiary-negative-hover",
59
+ negativeDisabled: DISABLED_TRANSPARENT
60
+ },
61
+ outline: {
62
+ default: "border border-buttons-outline-default bg-transparent text-content-primary hover:bg-buttons-outline-hover active:bg-buttons-outline-hover",
63
+ disabled: "border border-buttons-disabled-default bg-transparent text-content-disabled",
64
+ negative: "border border-buttons-outline-negative-default bg-transparent text-content-primary-inverted hover:bg-buttons-outline-negative-hover active:bg-buttons-outline-negative-hover",
65
+ negativeDisabled: "border border-buttons-disabled-negative bg-transparent text-content-disabled"
66
+ },
67
+ brand: {
68
+ default: "bg-buttons-brand-default text-content-always-black hover:bg-buttons-brand-hover hover:text-content-always-black active:bg-buttons-brand-hover active:text-content-always-black",
69
+ disabled: DISABLED_FILL
70
+ },
71
+ destructive: {
72
+ default: "bg-buttons-error-default text-content-always-white hover:bg-buttons-error-hover hover:text-content-always-white active:bg-buttons-error-hover active:text-content-always-white",
73
+ disabled: DISABLED_FILL
74
+ },
75
+ white: {
76
+ default: "bg-buttons-always-white-default text-content-always-black hover:bg-buttons-always-white-hover hover:text-content-always-black active:bg-buttons-always-white-hover active:text-content-always-black",
77
+ disabled: DISABLED_FILL
78
+ },
79
+ alwaysBlack: {
80
+ default: "bg-buttons-always-black-default text-content-always-white hover:bg-buttons-always-black-hover hover:text-content-always-white active:bg-buttons-always-black-hover active:text-content-always-white",
81
+ disabled: DISABLED_FILL
82
+ },
83
+ ai: {
84
+ default: `border border-transparent text-content-always-white shadow-ai-button-glow ${AI_GRADIENT}`,
85
+ disabled: DISABLED_FILL
86
+ },
87
+ link: {
88
+ default: "bg-transparent text-content-primary underline decoration-solid hover:bg-buttons-tertiary-hover active:bg-buttons-tertiary-hover",
89
+ disabled: "bg-transparent text-content-primary underline decoration-solid opacity-50"
90
+ },
91
+ tertiaryDestructive: {
92
+ default: "bg-transparent text-error-content hover:bg-error-surface active:bg-error-surface",
93
+ disabled: "bg-transparent text-error-content opacity-50"
94
+ },
95
+ text: {
96
+ default: "bg-transparent text-content-primary hover:underline active:underline",
97
+ disabled: "bg-transparent text-content-primary opacity-50"
98
+ }
35
99
  };
100
+ function getVariantClasses(variant, negative, disabled) {
101
+ const spec = VARIANT_CLASSES[variant];
102
+ const isNegative = NEGATIVE_AWARE_VARIANTS.has(variant) && negative;
103
+ if (disabled) return isNegative && spec.negativeDisabled || spec.disabled;
104
+ return isNegative && spec.negative || spec.default;
105
+ }
106
+ function getIconPaddingClasses(size, hasLeftIcon, hasRightIcon) {
107
+ const padding = ICON_SIDE_PADDING_CLASSES[size];
108
+ return cn(hasLeftIcon && padding.left, hasRightIcon && padding.right);
109
+ }
36
110
  function getTextContent(node) {
37
111
  if (typeof node === "string") return node;
38
112
  if (typeof node === "number") return String(node);
@@ -105,6 +179,7 @@ const Button = React.forwardRef(
105
179
  className,
106
180
  variant = "primary",
107
181
  size = "40",
182
+ negative = false,
108
183
  leftIcon,
109
184
  rightIcon,
110
185
  loading = false,
@@ -117,20 +192,27 @@ const Button = React.forwardRef(
117
192
  ...props
118
193
  }, ref) => {
119
194
  const Comp = asChild ? Slot : "button";
120
- const isDisabled = disabled || loading;
121
- const iconSizeClass = ICON_WRAPPER_CLASS[size];
195
+ const isDisabled = Boolean(disabled);
196
+ const isInteractionDisabled = isDisabled || loading;
197
+ const iconSizeClass = variant === "ai" ? "[&>svg]:size-4" : ICON_WRAPPER_CLASS[size];
198
+ const effectiveLeftIcon = leftIcon ?? (variant === "ai" ? /* @__PURE__ */ jsx(AIIcon, { filled: true }) : void 0);
199
+ const iconPaddingClasses = getIconPaddingClasses(
200
+ size,
201
+ Boolean(effectiveLeftIcon),
202
+ Boolean(rightIcon)
203
+ );
122
204
  const buttonSpecificProps = !asChild ? {
123
205
  type: "button",
124
206
  "data-testid": "button",
125
- disabled: isDisabled
126
- } : isDisabled ? { "aria-disabled": true } : {};
207
+ disabled: isInteractionDisabled
208
+ } : isInteractionDisabled ? { "aria-disabled": true } : {};
127
209
  const loadingLabelProps = loading && asChild ? { "aria-label": getTextContent(children) } : {};
128
210
  const content = renderContent({
129
211
  loading,
130
212
  asChild,
131
213
  children,
132
214
  size,
133
- leftIcon,
215
+ leftIcon: effectiveLeftIcon,
134
216
  rightIcon,
135
217
  iconSizeClass,
136
218
  discount,
@@ -144,20 +226,14 @@ const Button = React.forwardRef(
144
226
  "aria-busy": loading,
145
227
  ...loadingLabelProps,
146
228
  className: cn(
147
- // Base styles
148
229
  "inline-flex min-w-0 cursor-pointer items-center gap-2 whitespace-nowrap rounded-full transition-colors",
149
- // Focus ring
150
230
  "focus-visible:shadow-focus-ring focus-visible:outline-none",
151
- // Disabled state
152
- "disabled:pointer-events-none disabled:opacity-50",
153
- "aria-disabled:pointer-events-none aria-disabled:opacity-50",
231
+ isInteractionDisabled && "pointer-events-none cursor-not-allowed",
154
232
  `${price ? "justify-between" : "justify-center"}`,
155
233
  fullWidth && "w-full",
156
- // Size styles
157
234
  SIZE_CLASSES[size],
158
- // Variant styles
159
- VARIANT_CLASSES[variant],
160
- // Manual CSS overrides
235
+ iconPaddingClasses,
236
+ getVariantClasses(variant, negative, isDisabled),
161
237
  className
162
238
  ),
163
239
  ...props,
@@ -1 +1 @@
1
- {"version":3,"file":"Button.mjs","sources":["../../../src/components/Button/Button.tsx"],"sourcesContent":["import { Slot } from \"@radix-ui/react-slot\";\nimport * as React from \"react\";\nimport { cn } from \"../../utils/cn\";\nimport { SpinnerIcon } from \"../Icons/SpinnerIcon\";\n\n/** Visual style variant of the button. */\nexport type ButtonVariant =\n | \"primary\"\n | \"secondary\"\n | \"tertiary\"\n | \"link\"\n | \"brand\"\n | \"destructive\"\n | \"white\"\n | \"tertiaryDestructive\"\n | \"text\";\n\n/** Button height in pixels. */\nexport type ButtonSize = \"48\" | \"40\" | \"32\" | \"24\";\n\nexport interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {\n /** Visual style variant of the button. @default \"primary\" */\n variant?: ButtonVariant;\n /** Height of the button in pixels. @default \"40\" */\n size?: ButtonSize;\n /** Icon element displayed before the label. */\n leftIcon?: React.ReactNode;\n /** Icon element displayed after the label. */\n rightIcon?: React.ReactNode;\n /** When `true`, replaces the label with a spinner and disables interaction. @default false */\n loading?: boolean;\n /** Merge props onto a child element instead of rendering a `<button>`. @default false */\n asChild?: boolean;\n /** Old price shown with a strikethrough before the current price. */\n discount?: string;\n /** Current price shown inside the button after the label and icons. */\n price?: string;\n /** When `true`, the button will take the full width of its container. @default false */\n fullWidth?: boolean;\n}\n\nconst SIZE_CLASSES: Record<ButtonSize, string> = {\n \"48\": \"h-12 px-4 py-3 typography-body-default-16px-semibold\",\n \"40\": \"h-10 px-4 py-2 typography-body-default-16px-semibold\",\n \"32\": \"h-8 px-3 py-2 typography-body-small-14px-semibold\",\n \"24\": \"h-6 px-2 py-1 typography-body-small-14px-semibold\",\n};\n\nconst ICON_SIZE_CLASS: Record<ButtonSize, string> = {\n \"48\": \"size-5\",\n \"40\": \"size-5\",\n \"32\": \"size-4\",\n \"24\": \"size-3.5\",\n};\n\n/** Targets only direct SVG children so non-icon content (e.g. Pill) can size naturally. */\nconst ICON_WRAPPER_CLASS: Record<ButtonSize, string> = {\n \"48\": \"[&>svg]:size-5\",\n \"40\": \"[&>svg]:size-5\",\n \"32\": \"[&>svg]:size-4\",\n \"24\": \"[&>svg]:size-3.5\",\n};\n\nconst VARIANT_CLASSES: Record<ButtonVariant, string> = {\n primary:\n \"bg-buttons-primary-default text-content-primary-inverted hover:bg-buttons-primary-hover hover:text-content-primary-inverted active:bg-buttons-primary-hover active:text-content-primary-inverted\",\n secondary:\n \"border-content-primary border bg-transparent text-content-primary hover:bg-brand-primary-muted active:bg-brand-primary-muted\",\n tertiary:\n \"bg-transparent text-content-primary hover:bg-brand-primary-muted active:bg-brand-primary-muted\",\n link: \"bg-transparent text-content-primary underline decoration-solid hover:bg-brand-primary-muted active:bg-brand-primary-muted\",\n brand:\n \"bg-buttons-brand-default text-content-always-black hover:bg-buttons-brand-hover hover:text-content-always-black active:bg-buttons-brand-hover active:text-content-always-black\",\n destructive:\n \"bg-error-content text-content-always-white hover:bg-brand-primary-muted hover:text-content-primary active:bg-brand-primary-muted active:text-content-primary\",\n white:\n \"bg-content-always-white text-content-always-black hover:bg-brand-primary-muted hover:text-content-primary active:bg-brand-primary-muted active:text-content-primary\",\n tertiaryDestructive:\n \"bg-transparent text-error-content hover:bg-error-surface active:bg-error-surface\",\n text: \"bg-transparent text-content-primary hover:underline active:underline\",\n};\n\n/** Recursively extract text content from React nodes for accessible labels */\nfunction getTextContent(node: React.ReactNode): string | undefined {\n if (typeof node === \"string\") return node;\n if (typeof node === \"number\") return String(node);\n if (React.isValidElement(node)) {\n return getTextContent((node.props as { children?: React.ReactNode }).children);\n }\n if (Array.isArray(node)) {\n const text = node.map(getTextContent).filter(Boolean).join(\"\");\n return text || undefined;\n }\n return undefined;\n}\n\nconst LoadingSpinner = ({ size }: { size: ButtonSize }) => {\n return (\n <span className=\"animate-spin\" aria-hidden=\"true\">\n <SpinnerIcon className={ICON_SIZE_CLASS[size]}>\n <title>Loading</title>\n </SpinnerIcon>\n </span>\n );\n};\n\nfunction renderContent({\n loading,\n asChild,\n children,\n size,\n leftIcon,\n rightIcon,\n iconSizeClass,\n discount,\n price,\n}: {\n loading: boolean;\n asChild: boolean;\n children: React.ReactNode;\n size: ButtonSize;\n leftIcon: React.ReactNode;\n rightIcon: React.ReactNode;\n iconSizeClass: string;\n discount?: string;\n price?: string;\n fullWidth?: boolean;\n}) {\n if (loading) {\n // When asChild, clone the child element with spinner content instead of\n // wrapping in sr-only span (which would nest interactive elements)\n if (asChild && React.isValidElement(children)) {\n return React.cloneElement(\n children as React.ReactElement<{ children?: React.ReactNode }>,\n undefined,\n <LoadingSpinner size={size} />,\n );\n }\n return (\n <>\n <LoadingSpinner size={size} />\n <span className=\"sr-only\">{children}</span>\n </>\n );\n }\n\n if (asChild) return children;\n\n return (\n <>\n {leftIcon && (\n <span\n className={cn(\"flex shrink-0 items-center justify-center\", iconSizeClass)}\n aria-hidden=\"true\"\n >\n {leftIcon}\n </span>\n )}\n {React.Children.map(children, (child) =>\n typeof child === \"string\" ? (\n child.trim() ? (\n <span className=\"min-w-0 truncate\">{child}</span>\n ) : null\n ) : typeof child === \"number\" ? (\n <span className=\"min-w-0 truncate\">{child}</span>\n ) : (\n child\n ),\n )}\n {rightIcon && (\n <span\n className={cn(\"flex shrink-0 items-center justify-center\", iconSizeClass)}\n aria-hidden=\"true\"\n >\n {rightIcon}\n </span>\n )}\n {(price || discount) && (\n <div>\n {discount && (\n <span className=\"typography-body-default-16px-regular line-through\" aria-hidden=\"true\">\n {discount}\n </span>\n )}\n {price && (\n <span className=\"ml-2\" aria-hidden=\"true\">\n {price}\n </span>\n )}\n </div>\n )}\n </>\n );\n}\n\n/**\n * A versatile button component with multiple visual variants, sizes, icon\n * slots, loading state, and optional pricing display.\n *\n * @example\n * ```tsx\n * <Button variant=\"brand\" size=\"40\" leftIcon={<StarIcon />}>\n * Subscribe\n * </Button>\n * ```\n */\nexport const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(\n (\n {\n className,\n variant = \"primary\",\n size = \"40\",\n leftIcon,\n rightIcon,\n loading = false,\n asChild = false,\n disabled,\n children,\n discount,\n price,\n fullWidth = false,\n ...props\n },\n ref,\n ) => {\n const Comp = asChild ? Slot : \"button\";\n const isDisabled = disabled || loading;\n const iconSizeClass = ICON_WRAPPER_CLASS[size];\n\n const buttonSpecificProps = !asChild\n ? {\n type: \"button\" as const,\n \"data-testid\": \"button\",\n disabled: isDisabled,\n }\n : isDisabled\n ? { \"aria-disabled\": true }\n : {};\n\n // When asChild + loading, extract text from children for aria-label since we\n // can't wrap element children in an sr-only span (creates invalid nested markup)\n const loadingLabelProps = loading && asChild ? { \"aria-label\": getTextContent(children) } : {};\n\n const content = renderContent({\n loading,\n asChild,\n children,\n size,\n leftIcon,\n rightIcon,\n iconSizeClass,\n discount,\n price,\n });\n\n return (\n <Comp\n ref={ref}\n {...buttonSpecificProps}\n aria-busy={loading}\n {...loadingLabelProps}\n className={cn(\n // Base styles\n \"inline-flex min-w-0 cursor-pointer items-center gap-2 whitespace-nowrap rounded-full transition-colors\",\n // Focus ring\n \"focus-visible:shadow-focus-ring focus-visible:outline-none\",\n // Disabled state\n \"disabled:pointer-events-none disabled:opacity-50\",\n \"aria-disabled:pointer-events-none aria-disabled:opacity-50\",\n `${price ? \"justify-between\" : \"justify-center\"}`,\n fullWidth && \"w-full\",\n // Size styles\n SIZE_CLASSES[size],\n // Variant styles\n VARIANT_CLASSES[variant],\n // Manual CSS overrides\n className,\n )}\n {...props}\n >\n {content}\n </Comp>\n );\n },\n);\n\nButton.displayName = \"Button\";\n"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"Button.mjs","sources":["../../../src/components/Button/Button.tsx"],"sourcesContent":["import { Slot } from \"@radix-ui/react-slot\";\nimport * as React from \"react\";\nimport { cn } from \"../../utils/cn\";\nimport { AIIcon } from \"../Icons/AIIcon\";\nimport { SpinnerIcon } from \"../Icons/SpinnerIcon\";\n\n/** Visual style variant of the button. */\nexport type ButtonVariant =\n | \"primary\"\n | \"secondary\"\n | \"tertiary\"\n | \"outline\"\n | \"link\"\n | \"brand\"\n | \"destructive\"\n | \"white\"\n | \"alwaysBlack\"\n | \"ai\"\n | \"tertiaryDestructive\"\n | \"text\";\n\n/** Button height in pixels. */\nexport type ButtonSize = \"48\" | \"40\" | \"32\" | \"24\";\n\nexport interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {\n /** Visual style variant of the button. @default \"primary\" */\n variant?: ButtonVariant;\n /** Height of the button in pixels. @default \"40\" */\n size?: ButtonSize;\n /**\n * Forces the dark-surface treatment regardless of theme. Only honored on\n * `primary`, `secondary`, `tertiary`, and `outline` variants; ignored on\n * all others. Use when placing the button on a dark background in a light\n * theme (e.g. an inverted hero or modal).\n * @default false\n */\n negative?: boolean;\n /** Icon element displayed before the label. */\n leftIcon?: React.ReactNode;\n /** Icon element displayed after the label. */\n rightIcon?: React.ReactNode;\n /** When `true`, replaces the label with a spinner and disables interaction. @default false */\n loading?: boolean;\n /** Merge props onto a child element instead of rendering a `<button>`. @default false */\n asChild?: boolean;\n /** Old price shown with a strikethrough before the current price. */\n discount?: string;\n /** Current price shown inside the button after the label and icons. */\n price?: string;\n /** When `true`, the button will take the full width of its container. @default false */\n fullWidth?: boolean;\n}\n\nconst NEGATIVE_AWARE_VARIANTS = new Set<ButtonVariant>([\n \"primary\",\n \"secondary\",\n \"tertiary\",\n \"outline\",\n]);\n\nconst SIZE_CLASSES: Record<ButtonSize, string> = {\n \"48\": \"h-12 px-6 py-3 typography-body-default-16px-semibold\",\n \"40\": \"h-10 px-4 py-2 typography-body-default-16px-semibold\",\n \"32\": \"h-8 px-3 py-[7px] typography-body-small-14px-semibold\",\n \"24\": \"h-6 px-2 py-1 typography-body-small-14px-semibold\",\n};\n\nconst ICON_SIDE_PADDING_CLASSES: Record<ButtonSize, { left: string; right: string }> = {\n \"48\": { left: \"pl-5\", right: \"pr-5\" },\n \"40\": { left: \"pl-3\", right: \"pr-3\" },\n \"32\": { left: \"pl-2\", right: \"pr-2\" },\n \"24\": { left: \"pl-1\", right: \"pr-1\" },\n};\n\nconst ICON_SIZE_CLASS: Record<ButtonSize, string> = {\n \"48\": \"size-5\",\n \"40\": \"size-5\",\n \"32\": \"size-4\",\n \"24\": \"size-3.5\",\n};\n\n/** Targets only direct SVG children so non-icon content (e.g. Pill) can size naturally. */\nconst ICON_WRAPPER_CLASS: Record<ButtonSize, string> = {\n \"48\": \"[&>svg]:size-5\",\n \"40\": \"[&>svg]:size-5\",\n \"32\": \"[&>svg]:size-4\",\n \"24\": \"[&>svg]:size-3.5\",\n};\n\n/** AI variant uses a fixed-angle gradient defined in the Figma design tokens. */\nconst AI_GRADIENT =\n \"[background-clip:padding-box,border-box] [background-image:linear-gradient(50deg,var(--color-buttons-ai-background-gradient-default-start)_11.87%,var(--color-buttons-ai-background-gradient-default-end)_112.39%),linear-gradient(50deg,var(--color-buttons-ai-stroke-start)_11.87%,var(--color-buttons-ai-stroke-end)_112.39%)] [background-origin:border-box] hover:[background-image:linear-gradient(50deg,var(--color-buttons-ai-background-gradient-hover-start)_11.87%,var(--color-buttons-ai-background-gradient-hover-end)_112.39%),linear-gradient(50deg,var(--color-buttons-ai-stroke-start)_11.87%,var(--color-buttons-ai-stroke-end)_112.39%)]\";\n\nconst DISABLED_FILL = \"bg-buttons-disabled-default text-content-disabled\";\nconst DISABLED_FILL_NEGATIVE = \"bg-buttons-disabled-negative text-content-disabled\";\nconst DISABLED_TRANSPARENT = \"bg-transparent text-content-disabled\";\n\n/**\n * Class strings for each `ButtonVariant`, indexed by `negative` and `disabled`\n * state. `negative` and `negativeDisabled` are only honored when the variant\n * is in `NEGATIVE_AWARE_VARIANTS` — other variants fall back to the default /\n * disabled entries regardless of the `negative` prop.\n */\ntype VariantClasses = {\n default: string;\n disabled: string;\n negative?: string;\n negativeDisabled?: string;\n};\n\nconst VARIANT_CLASSES: Record<ButtonVariant, VariantClasses> = {\n primary: {\n default:\n \"bg-buttons-primary-default text-content-primary-inverted hover:bg-buttons-primary-hover hover:text-content-primary-inverted active:bg-buttons-primary-hover active:text-content-primary-inverted\",\n disabled: DISABLED_FILL,\n negative:\n \"bg-buttons-primary-negative-default text-content-primary hover:bg-buttons-primary-negative-hover active:bg-buttons-primary-negative-hover\",\n negativeDisabled: DISABLED_FILL_NEGATIVE,\n },\n secondary: {\n default:\n \"bg-buttons-secondary-default text-content-primary hover:bg-buttons-secondary-hover active:bg-buttons-secondary-hover\",\n disabled: DISABLED_FILL,\n negative:\n \"bg-buttons-secondary-negative-default text-content-primary-inverted hover:bg-buttons-secondary-negative-hover active:bg-buttons-secondary-negative-hover\",\n negativeDisabled: DISABLED_FILL_NEGATIVE,\n },\n tertiary: {\n default:\n \"bg-transparent text-content-primary hover:bg-buttons-tertiary-hover active:bg-buttons-tertiary-hover\",\n disabled: DISABLED_TRANSPARENT,\n negative:\n \"bg-transparent text-content-primary-inverted hover:bg-buttons-tertiary-negative-hover active:bg-buttons-tertiary-negative-hover\",\n negativeDisabled: DISABLED_TRANSPARENT,\n },\n outline: {\n default:\n \"border border-buttons-outline-default bg-transparent text-content-primary hover:bg-buttons-outline-hover active:bg-buttons-outline-hover\",\n disabled: \"border border-buttons-disabled-default bg-transparent text-content-disabled\",\n negative:\n \"border border-buttons-outline-negative-default bg-transparent text-content-primary-inverted hover:bg-buttons-outline-negative-hover active:bg-buttons-outline-negative-hover\",\n negativeDisabled:\n \"border border-buttons-disabled-negative bg-transparent text-content-disabled\",\n },\n brand: {\n default:\n \"bg-buttons-brand-default text-content-always-black hover:bg-buttons-brand-hover hover:text-content-always-black active:bg-buttons-brand-hover active:text-content-always-black\",\n disabled: DISABLED_FILL,\n },\n destructive: {\n default:\n \"bg-buttons-error-default text-content-always-white hover:bg-buttons-error-hover hover:text-content-always-white active:bg-buttons-error-hover active:text-content-always-white\",\n disabled: DISABLED_FILL,\n },\n white: {\n default:\n \"bg-buttons-always-white-default text-content-always-black hover:bg-buttons-always-white-hover hover:text-content-always-black active:bg-buttons-always-white-hover active:text-content-always-black\",\n disabled: DISABLED_FILL,\n },\n alwaysBlack: {\n default:\n \"bg-buttons-always-black-default text-content-always-white hover:bg-buttons-always-black-hover hover:text-content-always-white active:bg-buttons-always-black-hover active:text-content-always-white\",\n disabled: DISABLED_FILL,\n },\n ai: {\n default: `border border-transparent text-content-always-white shadow-ai-button-glow ${AI_GRADIENT}`,\n disabled: DISABLED_FILL,\n },\n link: {\n default:\n \"bg-transparent text-content-primary underline decoration-solid hover:bg-buttons-tertiary-hover active:bg-buttons-tertiary-hover\",\n disabled: \"bg-transparent text-content-primary underline decoration-solid opacity-50\",\n },\n tertiaryDestructive: {\n default: \"bg-transparent text-error-content hover:bg-error-surface active:bg-error-surface\",\n disabled: \"bg-transparent text-error-content opacity-50\",\n },\n text: {\n default: \"bg-transparent text-content-primary hover:underline active:underline\",\n disabled: \"bg-transparent text-content-primary opacity-50\",\n },\n};\n\nfunction getVariantClasses(variant: ButtonVariant, negative: boolean, disabled: boolean): string {\n const spec = VARIANT_CLASSES[variant];\n const isNegative = NEGATIVE_AWARE_VARIANTS.has(variant) && negative;\n if (disabled) return (isNegative && spec.negativeDisabled) || spec.disabled;\n return (isNegative && spec.negative) || spec.default;\n}\n\nfunction getIconPaddingClasses(\n size: ButtonSize,\n hasLeftIcon: boolean,\n hasRightIcon: boolean,\n): string | undefined {\n const padding = ICON_SIDE_PADDING_CLASSES[size];\n return cn(hasLeftIcon && padding.left, hasRightIcon && padding.right);\n}\n\n/** Recursively extract text content from React nodes for accessible labels */\nfunction getTextContent(node: React.ReactNode): string | undefined {\n if (typeof node === \"string\") return node;\n if (typeof node === \"number\") return String(node);\n if (React.isValidElement(node)) {\n return getTextContent((node.props as { children?: React.ReactNode }).children);\n }\n if (Array.isArray(node)) {\n const text = node.map(getTextContent).filter(Boolean).join(\"\");\n return text || undefined;\n }\n return undefined;\n}\n\nconst LoadingSpinner = ({ size }: { size: ButtonSize }) => {\n return (\n <span className=\"animate-spin\" aria-hidden=\"true\">\n <SpinnerIcon className={ICON_SIZE_CLASS[size]}>\n <title>Loading</title>\n </SpinnerIcon>\n </span>\n );\n};\n\nfunction renderContent({\n loading,\n asChild,\n children,\n size,\n leftIcon,\n rightIcon,\n iconSizeClass,\n discount,\n price,\n}: {\n loading: boolean;\n asChild: boolean;\n children: React.ReactNode;\n size: ButtonSize;\n leftIcon: React.ReactNode;\n rightIcon: React.ReactNode;\n iconSizeClass: string;\n discount?: string;\n price?: string;\n fullWidth?: boolean;\n}) {\n if (loading) {\n if (asChild && React.isValidElement(children)) {\n return React.cloneElement(\n children as React.ReactElement<{ children?: React.ReactNode }>,\n undefined,\n <LoadingSpinner size={size} />,\n );\n }\n return (\n <>\n <LoadingSpinner size={size} />\n <span className=\"sr-only\">{children}</span>\n </>\n );\n }\n\n if (asChild) return children;\n\n return (\n <>\n {leftIcon && (\n <span\n className={cn(\"flex shrink-0 items-center justify-center\", iconSizeClass)}\n aria-hidden=\"true\"\n >\n {leftIcon}\n </span>\n )}\n {React.Children.map(children, (child) =>\n typeof child === \"string\" ? (\n child.trim() ? (\n <span className=\"min-w-0 truncate\">{child}</span>\n ) : null\n ) : typeof child === \"number\" ? (\n <span className=\"min-w-0 truncate\">{child}</span>\n ) : (\n child\n ),\n )}\n {rightIcon && (\n <span\n className={cn(\"flex shrink-0 items-center justify-center\", iconSizeClass)}\n aria-hidden=\"true\"\n >\n {rightIcon}\n </span>\n )}\n {(price || discount) && (\n <div>\n {discount && (\n <span className=\"typography-body-default-16px-regular line-through\" aria-hidden=\"true\">\n {discount}\n </span>\n )}\n {price && (\n <span className=\"ml-2\" aria-hidden=\"true\">\n {price}\n </span>\n )}\n </div>\n )}\n </>\n );\n}\n\n/**\n * A versatile button component with multiple visual variants, sizes, icon\n * slots, loading state, and optional pricing display.\n *\n * Pass `negative` when rendering on a dark surface to opt into the inverted\n * treatment for `primary`, `secondary`, `tertiary`, and `outline` variants.\n *\n * The `ai` variant ships with the AI sparkle icon baked in (locked at 16px\n * regardless of button size); pass `leftIcon` to override the default sparkle.\n *\n * @example\n * ```tsx\n * <Button variant=\"primary\" size=\"40\" leftIcon={<StarIcon />}>\n * Continue\n * </Button>\n * ```\n */\nexport const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(\n (\n {\n className,\n variant = \"primary\",\n size = \"40\",\n negative = false,\n leftIcon,\n rightIcon,\n loading = false,\n asChild = false,\n disabled,\n children,\n discount,\n price,\n fullWidth = false,\n ...props\n },\n ref,\n ) => {\n const Comp = asChild ? Slot : \"button\";\n const isDisabled = Boolean(disabled);\n const isInteractionDisabled = isDisabled || loading;\n const iconSizeClass = variant === \"ai\" ? \"[&>svg]:size-4\" : ICON_WRAPPER_CLASS[size];\n const effectiveLeftIcon = leftIcon ?? (variant === \"ai\" ? <AIIcon filled /> : undefined);\n const iconPaddingClasses = getIconPaddingClasses(\n size,\n Boolean(effectiveLeftIcon),\n Boolean(rightIcon),\n );\n\n const buttonSpecificProps = !asChild\n ? {\n type: \"button\" as const,\n \"data-testid\": \"button\",\n disabled: isInteractionDisabled,\n }\n : isInteractionDisabled\n ? { \"aria-disabled\": true }\n : {};\n\n const loadingLabelProps = loading && asChild ? { \"aria-label\": getTextContent(children) } : {};\n\n const content = renderContent({\n loading,\n asChild,\n children,\n size,\n leftIcon: effectiveLeftIcon,\n rightIcon,\n iconSizeClass,\n discount,\n price,\n });\n\n return (\n <Comp\n ref={ref}\n {...buttonSpecificProps}\n aria-busy={loading}\n {...loadingLabelProps}\n className={cn(\n \"inline-flex min-w-0 cursor-pointer items-center gap-2 whitespace-nowrap rounded-full transition-colors\",\n \"focus-visible:shadow-focus-ring focus-visible:outline-none\",\n isInteractionDisabled && \"pointer-events-none cursor-not-allowed\",\n `${price ? \"justify-between\" : \"justify-center\"}`,\n fullWidth && \"w-full\",\n SIZE_CLASSES[size],\n iconPaddingClasses,\n getVariantClasses(variant, negative, isDisabled),\n className,\n )}\n {...props}\n >\n {content}\n </Comp>\n );\n },\n);\n\nButton.displayName = \"Button\";\n"],"names":[],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"CreatorCover.mjs","sources":["../../../src/components/CreatorCover/CreatorCover.tsx"],"sourcesContent":["import * as React from \"react\";\nimport { cn } from \"../../utils/cn\";\nimport { Pill } from \"../Pill/Pill\";\n\n/** Slot that accepts a string (rendered as default styling) or a node for full control. */\nexport type CreatorCoverSlot = string | React.ReactNode;\n\nfunction isNonEmptyString(value: unknown): value is string {\n return typeof value === \"string\" && value.length > 0;\n}\n\nexport interface CreatorCoverProps extends Omit<React.HTMLAttributes<HTMLElement>, \"title\"> {\n /** URL of the creator image displayed in the centre card. Also used as the blurred backdrop unless `backgroundSrc` is provided. */\n imageSrc: string;\n /** Alt text for the centre cover image. @default \"\" */\n imageAlt?: string;\n /** Override URL used for the blurred background image. @default `imageSrc` */\n backgroundSrc?: string;\n /** Creator's name, rendered as the heading. */\n name: string;\n /** Smaller subtitle below the name (e.g. \"GLOBAL POPSTAR\"). Rendered uppercase in the brand colour. */\n tagline?: string;\n /**\n * Status label rendered as a pill overlapping the bottom of the cover image (e.g. \"New Joiner\").\n * Strings render with default green pill styling; pass a node for custom markup.\n */\n tag?: CreatorCoverSlot;\n /**\n * Primary call to action displayed below the title.\n */\n action?: React.ReactNode;\n /** When `true`, fades the bottom of the component to transparent and increases bottom padding to 64px. @default false */\n fadeBottom?: boolean;\n}\n\n/**\n * A creator profile hero with a stylised blurred backdrop, central cover image,\n * status pill, name, tagline, and primary call to action.\n *\n * @example\n * ```tsx\n * <CreatorCover\n * imageSrc=\"/creator.jpg\"\n * imageAlt=\"Jane Doe\"\n * name=\"JANE DOE\"\n * tagline=\"GLOBAL POPSTAR\"\n * tag=\"New Joiner\"\n * action={<Button variant=\"primary\" size=\"48\" fullWidth>Join for free for 7 days</Button>}\n * />\n * ```\n */\nexport const CreatorCover = React.forwardRef<HTMLElement, CreatorCoverProps>(\n (\n { className, imageSrc, imageAlt = \"\", backgroundSrc, name, tagline, tag, action, ...props },\n ref,\n ) => {\n const headingId = React.useId();\n\n const renderedTag = isNonEmptyString(tag) ? <Pill variant=\"brand\">{tag}</Pill> : tag;\n\n return (\n <section\n ref={ref}\n aria-labelledby={headingId}\n data-testid=\"creator-cover\"\n className={cn(\n \"relative isolate w-full overflow-hidden bg-white dark:bg-background-primary\",\n className,\n )}\n {...props}\n >\n <div className=\"absolute inset-0 -z-10\">\n <img\n src={backgroundSrc ?? imageSrc}\n alt=\"\"\n loading=\"lazy\"\n className=\"size-full scale-110 object-cover blur-3xl\"\n />\n <div className=\"absolute inset-0 bg-linear-to-b from-white/30 to-white/15 dark:from-background-primary/30 dark:to-background-primary/15\" />\n <div className=\"absolute inset-x-0 bottom-0 h-1/3 bg-linear-to-b from-transparent to-white dark:to-background-primary\" />\n </div>\n <div className={cn(\"mx-auto flex max-w-90 flex-col items-center gap-4 px-4 pt-17 pb-16\")}>\n <div className=\"relative\">\n <img\n src={imageSrc}\n alt={imageAlt}\n loading=\"lazy\"\n className=\"block h-55 w-37.5 rounded-lg object-cover\"\n />\n {renderedTag ? (\n <div className=\"absolute bottom-0 left-1/2 -translate-x-1/2 translate-y-1/2\">\n {renderedTag}\n </div>\n ) : null}\n </div>\n <div className=\"flex flex-col items-center gap-1 pt-4 text-center\">\n <h2 id={headingId} className=\"typography-header-heading-md m-0 text-white\">\n {name}\n </h2>\n {tagline ? (\n <p className=\"typography-badge-badgecaps m-0 text-brand-primary-default uppercase\">\n {tagline}\n </p>\n ) : null}\n </div>\n {action ? <div className=\"w-full pt-2\">{action}</div> : null}\n </div>\n </section>\n );\n },\n);\n\nCreatorCover.displayName = \"CreatorCover\";\n"],"names":[],"mappings":";;;;;AAOA,SAAS,iBAAiB,OAAiC;AACzD,SAAO,OAAO,UAAU,YAAY,MAAM,SAAS;AACrD;AA0CO,MAAM,eAAe,MAAM;AAAA,EAChC,CACE,EAAE,WAAW,UAAU,WAAW,IAAI,eAAe,MAAM,SAAS,KAAK,QAAQ,GAAG,MAAA,GACpF,QACG;AACH,UAAM,YAAY,MAAM,MAAA;AAExB,UAAM,cAAc,iBAAiB,GAAG,wBAAK,MAAA,EAAK,SAAQ,SAAS,UAAA,IAAA,CAAI,IAAU;AAEjF,WACE;AAAA,MAAC;AAAA,MAAA;AAAA,QACC;AAAA,QACA,mBAAiB;AAAA,QACjB,eAAY;AAAA,QACZ,WAAW;AAAA,UACT;AAAA,UACA;AAAA,QAAA;AAAA,QAED,GAAG;AAAA,QAEJ,UAAA;AAAA,UAAA,qBAAC,OAAA,EAAI,WAAU,0BACb,UAAA;AAAA,YAAA;AAAA,cAAC;AAAA,cAAA;AAAA,gBACC,KAAK,iBAAiB;AAAA,gBACtB,KAAI;AAAA,gBACJ,SAAQ;AAAA,gBACR,WAAU;AAAA,cAAA;AAAA,YAAA;AAAA,YAEZ,oBAAC,OAAA,EAAI,WAAU,0HAAA,CAA0H;AAAA,YACzI,oBAAC,OAAA,EAAI,WAAU,wGAAA,CAAwG;AAAA,UAAA,GACzH;AAAA,UACA,qBAAC,OAAA,EAAI,WAAW,GAAG,oEAAoE,GACrF,UAAA;AAAA,YAAA,qBAAC,OAAA,EAAI,WAAU,YACb,UAAA;AAAA,cAAA;AAAA,gBAAC;AAAA,gBAAA;AAAA,kBACC,KAAK;AAAA,kBACL,KAAK;AAAA,kBACL,SAAQ;AAAA,kBACR,WAAU;AAAA,gBAAA;AAAA,cAAA;AAAA,cAEX,cACC,oBAAC,OAAA,EAAI,WAAU,+DACZ,uBACH,IACE;AAAA,YAAA,GACN;AAAA,YACA,qBAAC,OAAA,EAAI,WAAU,qDACb,UAAA;AAAA,cAAA,oBAAC,MAAA,EAAG,IAAI,WAAW,WAAU,+CAC1B,UAAA,MACH;AAAA,cACC,UACC,oBAAC,KAAA,EAAE,WAAU,uEACV,mBACH,IACE;AAAA,YAAA,GACN;AAAA,YACC,SAAS,oBAAC,OAAA,EAAI,WAAU,eAAe,kBAAO,IAAS;AAAA,UAAA,EAAA,CAC1D;AAAA,QAAA;AAAA,MAAA;AAAA,IAAA;AAAA,EAGN;AACF;AAEA,aAAa,cAAc;"}
1
+ {"version":3,"file":"CreatorCover.mjs","sources":["../../../src/components/CreatorCover/CreatorCover.tsx"],"sourcesContent":["import * as React from \"react\";\nimport { cn } from \"../../utils/cn\";\nimport { Pill } from \"../Pill/Pill\";\n\n/** Slot that accepts a string (rendered as default styling) or a node for full control. */\nexport type CreatorCoverSlot = string | React.ReactNode;\n\nfunction isNonEmptyString(value: unknown): value is string {\n return typeof value === \"string\" && value.length > 0;\n}\n\nexport interface CreatorCoverProps extends Omit<React.HTMLAttributes<HTMLElement>, \"title\"> {\n /** URL of the creator image displayed in the centre card. Also used as the blurred backdrop unless `backgroundSrc` is provided. */\n imageSrc: string;\n /** Alt text for the centre cover image. @default \"\" */\n imageAlt?: string;\n /** Override URL used for the blurred background image. @default `imageSrc` */\n backgroundSrc?: string;\n /** Creator's name, rendered as the heading. */\n name: string;\n /** Smaller subtitle below the name (e.g. \"GLOBAL POPSTAR\"). Rendered uppercase in the brand colour. */\n tagline?: string;\n /**\n * Status label rendered as a pill overlapping the bottom of the cover image (e.g. \"New Joiner\").\n * Strings render with default green pill styling; pass a node for custom markup.\n */\n tag?: CreatorCoverSlot;\n /**\n * Primary call to action displayed below the title.\n */\n action?: React.ReactNode;\n /** When `true`, fades the bottom of the component to transparent and increases bottom padding to 64px. @default false */\n fadeBottom?: boolean;\n}\n\n/**\n * A creator profile hero with a stylised blurred backdrop, central cover image,\n * status pill, name, tagline, and primary call to action.\n *\n * @example\n * ```tsx\n * <CreatorCover\n * imageSrc=\"/creator.jpg\"\n * imageAlt=\"Jane Doe\"\n * name=\"JANE DOE\"\n * tagline=\"GLOBAL POPSTAR\"\n * tag=\"New Joiner\"\n * action={<Button variant=\"brand\" size=\"48\" fullWidth>Join for free for 7 days</Button>}\n * />\n * ```\n */\nexport const CreatorCover = React.forwardRef<HTMLElement, CreatorCoverProps>(\n (\n { className, imageSrc, imageAlt = \"\", backgroundSrc, name, tagline, tag, action, ...props },\n ref,\n ) => {\n const headingId = React.useId();\n\n const renderedTag = isNonEmptyString(tag) ? <Pill variant=\"brand\">{tag}</Pill> : tag;\n\n return (\n <section\n ref={ref}\n aria-labelledby={headingId}\n data-testid=\"creator-cover\"\n className={cn(\n \"relative isolate w-full overflow-hidden bg-white dark:bg-background-primary\",\n className,\n )}\n {...props}\n >\n <div className=\"absolute inset-0 -z-10\">\n <img\n src={backgroundSrc ?? imageSrc}\n alt=\"\"\n loading=\"lazy\"\n className=\"size-full scale-110 object-cover blur-3xl\"\n />\n <div className=\"absolute inset-0 bg-linear-to-b from-white/30 to-white/15 dark:from-background-primary/30 dark:to-background-primary/15\" />\n <div className=\"absolute inset-x-0 bottom-0 h-1/3 bg-linear-to-b from-transparent to-white dark:to-background-primary\" />\n </div>\n <div className={cn(\"mx-auto flex max-w-90 flex-col items-center gap-4 px-4 pt-17 pb-16\")}>\n <div className=\"relative\">\n <img\n src={imageSrc}\n alt={imageAlt}\n loading=\"lazy\"\n className=\"block h-55 w-37.5 rounded-lg object-cover\"\n />\n {renderedTag ? (\n <div className=\"absolute bottom-0 left-1/2 -translate-x-1/2 translate-y-1/2\">\n {renderedTag}\n </div>\n ) : null}\n </div>\n <div className=\"flex flex-col items-center gap-1 pt-4 text-center\">\n <h2 id={headingId} className=\"typography-header-heading-md m-0 text-white\">\n {name}\n </h2>\n {tagline ? (\n <p className=\"typography-badge-badgecaps m-0 text-brand-primary-default uppercase\">\n {tagline}\n </p>\n ) : null}\n </div>\n {action ? <div className=\"w-full pt-2\">{action}</div> : null}\n </div>\n </section>\n );\n },\n);\n\nCreatorCover.displayName = \"CreatorCover\";\n"],"names":[],"mappings":";;;;;AAOA,SAAS,iBAAiB,OAAiC;AACzD,SAAO,OAAO,UAAU,YAAY,MAAM,SAAS;AACrD;AA0CO,MAAM,eAAe,MAAM;AAAA,EAChC,CACE,EAAE,WAAW,UAAU,WAAW,IAAI,eAAe,MAAM,SAAS,KAAK,QAAQ,GAAG,MAAA,GACpF,QACG;AACH,UAAM,YAAY,MAAM,MAAA;AAExB,UAAM,cAAc,iBAAiB,GAAG,wBAAK,MAAA,EAAK,SAAQ,SAAS,UAAA,IAAA,CAAI,IAAU;AAEjF,WACE;AAAA,MAAC;AAAA,MAAA;AAAA,QACC;AAAA,QACA,mBAAiB;AAAA,QACjB,eAAY;AAAA,QACZ,WAAW;AAAA,UACT;AAAA,UACA;AAAA,QAAA;AAAA,QAED,GAAG;AAAA,QAEJ,UAAA;AAAA,UAAA,qBAAC,OAAA,EAAI,WAAU,0BACb,UAAA;AAAA,YAAA;AAAA,cAAC;AAAA,cAAA;AAAA,gBACC,KAAK,iBAAiB;AAAA,gBACtB,KAAI;AAAA,gBACJ,SAAQ;AAAA,gBACR,WAAU;AAAA,cAAA;AAAA,YAAA;AAAA,YAEZ,oBAAC,OAAA,EAAI,WAAU,0HAAA,CAA0H;AAAA,YACzI,oBAAC,OAAA,EAAI,WAAU,wGAAA,CAAwG;AAAA,UAAA,GACzH;AAAA,UACA,qBAAC,OAAA,EAAI,WAAW,GAAG,oEAAoE,GACrF,UAAA;AAAA,YAAA,qBAAC,OAAA,EAAI,WAAU,YACb,UAAA;AAAA,cAAA;AAAA,gBAAC;AAAA,gBAAA;AAAA,kBACC,KAAK;AAAA,kBACL,KAAK;AAAA,kBACL,SAAQ;AAAA,kBACR,WAAU;AAAA,gBAAA;AAAA,cAAA;AAAA,cAEX,cACC,oBAAC,OAAA,EAAI,WAAU,+DACZ,uBACH,IACE;AAAA,YAAA,GACN;AAAA,YACA,qBAAC,OAAA,EAAI,WAAU,qDACb,UAAA;AAAA,cAAA,oBAAC,MAAA,EAAG,IAAI,WAAW,WAAU,+CAC1B,UAAA,MACH;AAAA,cACC,UACC,oBAAC,KAAA,EAAE,WAAU,uEACV,mBACH,IACE;AAAA,YAAA,GACN;AAAA,YACC,SAAS,oBAAC,OAAA,EAAI,WAAU,eAAe,kBAAO,IAAS;AAAA,UAAA,EAAA,CAC1D;AAAA,QAAA;AAAA,MAAA;AAAA,IAAA;AAAA,EAGN;AACF;AAEA,aAAa,cAAc;"}
@@ -36,13 +36,14 @@ const DialogContent = React.forwardRef(
36
36
  size = "md",
37
37
  overlay = true,
38
38
  portal = true,
39
+ showMobileHandle = true,
39
40
  style,
40
41
  onOpenAutoFocus,
41
42
  ...props
42
43
  }, ref) => {
43
44
  const content = /* @__PURE__ */ jsxs(Fragment, { children: [
44
45
  overlay && /* @__PURE__ */ jsx(DialogOverlay, {}),
45
- /* @__PURE__ */ jsx(
46
+ /* @__PURE__ */ jsxs(
46
47
  DialogPrimitive.Content,
47
48
  {
48
49
  ref,
@@ -56,13 +57,13 @@ const DialogContent = React.forwardRef(
56
57
  e.currentTarget.focus();
57
58
  },
58
59
  className: cn(
59
- "fixed flex flex-col overflow-hidden bg-background-primary shadow-lg focus:outline-none dark:bg-surface-primary",
60
- "inset-x-0 bottom-0 max-h-[85vh] w-full rounded-t-lg",
60
+ "fixed flex flex-col overflow-hidden border border-modal-stroke bg-modal-background shadow-blur-menu backdrop-blur-[4px] focus:outline-none",
61
+ "inset-x-0 bottom-0 max-h-[85vh] w-full rounded-t-xl p-4 pt-3",
61
62
  "data-[state=open]:fade-in-0 data-[state=open]:animate-in",
62
63
  "data-[state=closed]:fade-out-0 data-[state=closed]:animate-out",
63
64
  "data-[state=open]:slide-in-from-bottom-full",
64
65
  "data-[state=closed]:slide-out-to-bottom-full",
65
- "sm:inset-auto sm:top-1/2 sm:left-1/2 sm:max-h-[85vh] sm:-translate-x-1/2 sm:-translate-y-1/2 sm:rounded-lg",
66
+ "sm:inset-auto sm:top-1/2 sm:left-1/2 sm:max-h-[85vh] sm:w-full sm:-translate-x-1/2 sm:-translate-y-1/2 sm:rounded-xl sm:p-6",
66
67
  "sm:data-[state=open]:slide-in-from-bottom-0 sm:data-[state=open]:zoom-in-95",
67
68
  "sm:data-[state=closed]:slide-out-to-bottom-0 sm:data-[state=closed]:zoom-out-95",
68
69
  "duration-200",
@@ -70,7 +71,16 @@ const DialogContent = React.forwardRef(
70
71
  className
71
72
  ),
72
73
  ...props,
73
- children
74
+ children: [
75
+ showMobileHandle && /* @__PURE__ */ jsx(
76
+ "div",
77
+ {
78
+ "aria-hidden": "true",
79
+ className: "mb-3 h-1 w-8 shrink-0 self-center rounded-full bg-icons-tertiary sm:hidden"
80
+ }
81
+ ),
82
+ children
83
+ ]
74
84
  }
75
85
  )
76
86
  ] });
@@ -94,22 +104,30 @@ const DialogHeader = React.forwardRef(
94
104
  "div",
95
105
  {
96
106
  ref,
97
- className: cn("flex h-16 shrink-0 items-center gap-2 px-6 py-4", className),
107
+ className: cn("flex shrink-0 items-center justify-end gap-4", className),
98
108
  ...props,
99
109
  children: [
100
110
  shouldShowBack && /* @__PURE__ */ jsx(
101
111
  IconButton,
102
112
  {
103
- variant: "tertiary",
113
+ variant: "secondary",
104
114
  size: "32",
105
- icon: /* @__PURE__ */ jsx(ArrowLeftIcon, {}),
115
+ icon: /* @__PURE__ */ jsx(ArrowLeftIcon, { size: 16 }),
106
116
  onClick: onBack,
107
117
  disabled: !onBack,
108
118
  "aria-label": backLabel
109
119
  }
110
120
  ),
111
121
  /* @__PURE__ */ jsx("div", { className: "min-w-0 flex-1", children }),
112
- showClose && /* @__PURE__ */ jsx(DialogPrimitive.Close, { asChild: true, children: /* @__PURE__ */ jsx(IconButton, { variant: "tertiary", size: "32", icon: /* @__PURE__ */ jsx(CloseIcon, {}), "aria-label": closeLabel }) })
122
+ showClose && /* @__PURE__ */ jsx(DialogPrimitive.Close, { asChild: true, children: /* @__PURE__ */ jsx(
123
+ IconButton,
124
+ {
125
+ variant: "secondary",
126
+ size: "32",
127
+ icon: /* @__PURE__ */ jsx(CloseIcon, { size: 16 }),
128
+ "aria-label": closeLabel
129
+ }
130
+ ) })
113
131
  ]
114
132
  }
115
133
  );
@@ -120,7 +138,7 @@ const DialogTitle = React.forwardRef(({ className, ...props }, ref) => /* @__PUR
120
138
  DialogPrimitive.Title,
121
139
  {
122
140
  ref,
123
- className: cn("typography-header-heading-xs truncate text-content-primary", className),
141
+ className: cn("typography-header-heading-xs text-content-primary", className),
124
142
  ...props
125
143
  }
126
144
  ));
@@ -135,7 +153,7 @@ const DialogDescription = React.forwardRef(({ className, ...props }, ref) => /*
135
153
  ));
136
154
  DialogDescription.displayName = "DialogDescription";
137
155
  const DialogBody = React.forwardRef(
138
- ({ className, ...props }, ref) => /* @__PURE__ */ jsx("div", { ref, className: cn("flex-1 overflow-y-auto px-6 py-4", className), ...props })
156
+ ({ className, ...props }, ref) => /* @__PURE__ */ jsx("div", { ref, className: cn("flex-1 overflow-y-auto py-4 sm:py-6", className), ...props })
139
157
  );
140
158
  DialogBody.displayName = "DialogBody";
141
159
  const DialogFooter = React.forwardRef(
@@ -143,11 +161,7 @@ const DialogFooter = React.forwardRef(
143
161
  "div",
144
162
  {
145
163
  ref,
146
- className: cn(
147
- "flex shrink-0 items-center gap-3 px-6 pt-3 pb-6",
148
- "[&>*]:min-w-0 [&>*]:flex-1",
149
- className
150
- ),
164
+ className: cn("flex shrink-0 items-center gap-2", "[&>*]:min-w-0 [&>*]:flex-1", className),
151
165
  ...props
152
166
  }
153
167
  )
@@ -1 +1 @@
1
- {"version":3,"file":"Dialog.mjs","sources":["../../../src/components/Dialog/Dialog.tsx"],"sourcesContent":["import * as DialogPrimitive from \"@radix-ui/react-dialog\";\nimport * as React from \"react\";\nimport { cn } from \"../../utils/cn\";\nimport { useSuppressClickAfterDrag } from \"../../utils/useSuppressClickAfterDrag\";\nimport { IconButton } from \"../IconButton/IconButton\";\nimport { ArrowLeftIcon } from \"../Icons/ArrowLeftIcon\";\nimport { CloseIcon } from \"../Icons/CloseIcon\";\n\n/** Props for the {@link Dialog} root component. */\nexport interface DialogProps extends React.ComponentPropsWithoutRef<typeof DialogPrimitive.Root> {\n /** Controlled open state. When provided, you must also supply `onOpenChange`. */\n open?: boolean;\n /** Called when the open state changes. Required when `open` is controlled. */\n onOpenChange?: (open: boolean) => void;\n /** The open state of the dialog when it is initially rendered (uncontrolled). */\n defaultOpen?: boolean;\n}\n\n/** Root component that manages open/close state for a dialog. */\nexport const Dialog = DialogPrimitive.Root;\n\n/** Props for the {@link DialogTrigger} component. */\nexport type DialogTriggerProps = React.ComponentPropsWithoutRef<typeof DialogPrimitive.Trigger>;\n\n/**\n * The element that opens the dialog when clicked.\n *\n * On touch / pen, a press-and-release that crosses a small movement threshold\n * is treated as a drag and the resulting synthetic click is suppressed —\n * defends against Android Chrome opening the dialog on a scroll-drag-end.\n */\nexport const DialogTrigger = React.forwardRef<\n React.ComponentRef<typeof DialogPrimitive.Trigger>,\n DialogTriggerProps\n>((props, ref) => <DialogPrimitive.Trigger ref={ref} {...useSuppressClickAfterDrag(props)} />);\nDialogTrigger.displayName = \"DialogTrigger\";\n\n/** Convenience alias for Radix `Dialog.Close`. Closes the dialog when clicked. */\nexport const DialogClose = DialogPrimitive.Close;\n\n/** Props for the {@link DialogClose} component. */\nexport type DialogCloseProps = React.ComponentPropsWithoutRef<typeof DialogPrimitive.Close>;\n\nexport interface DialogOverlayProps\n extends React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay> {}\n\n/**\n * Semi-transparent backdrop rendered behind the dialog content.\n * Rendered by {@link DialogContent}; portaled to `document.body` when {@link DialogContent} `portal` is true.\n */\nexport const DialogOverlay = React.forwardRef<\n React.ComponentRef<typeof DialogPrimitive.Overlay>,\n DialogOverlayProps\n>(({ className, style, ...props }, ref) => (\n <DialogPrimitive.Overlay\n ref={ref}\n className={cn(\n \"data-[state=open]:fade-in-0 data-[state=closed]:fade-out-0 fixed inset-0 bg-background-overlay-default data-[state=closed]:animate-out data-[state=open]:animate-in\",\n className,\n )}\n style={{ zIndex: \"var(--fanvue-ui-portal-z-index, 50)\", ...style }}\n {...props}\n />\n));\nDialogOverlay.displayName = \"DialogOverlay\";\n\nexport interface DialogContentProps\n extends React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content> {\n /**\n * Width preset for the dialog.\n * - `\"sm\"` — 400px max-width (confirmations, simple forms)\n * - `\"md\"` — 440px max-width (default, standard dialogs)\n * - `\"lg\"` — 600px max-width (complex content, tables)\n *\n * @default \"md\"\n */\n size?: \"sm\" | \"md\" | \"lg\";\n /** When true, renders overlay automatically. @default true */\n overlay?: boolean;\n /**\n * When true, teleports overlay and panel to `document.body`.\n * When false, renders inline in the React tree (useful inside theme providers or scoped containers).\n * @default true\n */\n portal?: boolean;\n}\n\nconst SIZE_CLASSES: Record<NonNullable<DialogContentProps[\"size\"]>, string> = {\n sm: \"sm:max-w-[400px]\",\n md: \"sm:max-w-[440px]\",\n lg: \"sm:max-w-[600px]\",\n};\n\n/**\n * The dialog panel. Includes the overlay by default and portals to `document.body` by default.\n *\n * Set `portal={false}` to keep overlay and content in the DOM subtree of the parent `Dialog`.\n * `fixed` positioning still applies; ancestors with `transform` or `overflow` may affect layout.\n *\n * On mobile viewports (<640px), the dialog slides up from the bottom as a sheet\n * with top-only border radius. On larger viewports it renders centered with\n * full border radius.\n *\n * @example\n * ```tsx\n * <Dialog>\n * <DialogTrigger asChild>\n * <Button>Open</Button>\n * </DialogTrigger>\n * <DialogContent>\n * <DialogHeader>\n * <DialogTitle>Title</DialogTitle>\n * </DialogHeader>\n * <DialogBody>Content here</DialogBody>\n * <DialogFooter>\n * <DialogClose asChild>\n * <Button variant=\"secondary\">Cancel</Button>\n * </DialogClose>\n * <Button>Accept</Button>\n * </DialogFooter>\n * </DialogContent>\n * </Dialog>\n * ```\n */\nexport const DialogContent = React.forwardRef<\n React.ComponentRef<typeof DialogPrimitive.Content>,\n DialogContentProps\n>(\n (\n {\n className,\n children,\n size = \"md\",\n overlay = true,\n portal = true,\n style,\n onOpenAutoFocus,\n ...props\n },\n ref,\n ) => {\n const content = (\n <>\n {overlay && <DialogOverlay />}\n <DialogPrimitive.Content\n ref={ref}\n style={{ zIndex: \"var(--fanvue-ui-portal-z-index, 50)\", ...style }}\n onOpenAutoFocus={(e) => {\n if (onOpenAutoFocus) {\n onOpenAutoFocus(e);\n return;\n }\n e.preventDefault();\n (e.currentTarget as HTMLElement).focus();\n }}\n className={cn(\n \"fixed flex flex-col overflow-hidden bg-background-primary shadow-lg focus:outline-none dark:bg-surface-primary\",\n \"inset-x-0 bottom-0 max-h-[85vh] w-full rounded-t-lg\",\n \"data-[state=open]:fade-in-0 data-[state=open]:animate-in\",\n \"data-[state=closed]:fade-out-0 data-[state=closed]:animate-out\",\n \"data-[state=open]:slide-in-from-bottom-full\",\n \"data-[state=closed]:slide-out-to-bottom-full\",\n \"sm:inset-auto sm:top-1/2 sm:left-1/2 sm:max-h-[85vh] sm:-translate-x-1/2 sm:-translate-y-1/2 sm:rounded-lg\",\n \"sm:data-[state=open]:slide-in-from-bottom-0 sm:data-[state=open]:zoom-in-95\",\n \"sm:data-[state=closed]:slide-out-to-bottom-0 sm:data-[state=closed]:zoom-out-95\",\n \"duration-200\",\n SIZE_CLASSES[size],\n className,\n )}\n {...props}\n >\n {children}\n </DialogPrimitive.Content>\n </>\n );\n\n return portal ? <DialogPrimitive.Portal>{content}</DialogPrimitive.Portal> : content;\n },\n);\nDialogContent.displayName = \"DialogContent\";\n\nexport interface DialogHeaderProps extends React.HTMLAttributes<HTMLDivElement> {\n /** Show the close (X) button in the header. @default true */\n showClose?: boolean;\n /** Show a back arrow button on the left side. Defaults to `true` when `onBack` is provided. */\n showBack?: boolean;\n /** Called when the back button is clicked. */\n onBack?: () => void;\n /** Accessible label for the back button. @default \"Go back\" */\n backLabel?: string;\n /** Accessible label for the close button. @default \"Close\" */\n closeLabel?: string;\n}\n\n/**\n * Header bar for the dialog. Renders the title with an optional back arrow\n * and close button.\n *\n * @example\n * ```tsx\n * <DialogHeader>\n * <DialogTitle>Settings</DialogTitle>\n * </DialogHeader>\n *\n * <DialogHeader showBack onBack={() => setStep(0)}>\n * <DialogTitle>Step 2</DialogTitle>\n * </DialogHeader>\n * ```\n */\nexport const DialogHeader = React.forwardRef<HTMLDivElement, DialogHeaderProps>(\n (\n {\n className,\n children,\n showClose = true,\n showBack,\n onBack,\n backLabel = \"Go back\",\n closeLabel = \"Close\",\n ...props\n },\n ref,\n ) => {\n const shouldShowBack = showBack ?? !!onBack;\n\n return (\n <div\n ref={ref}\n className={cn(\"flex h-16 shrink-0 items-center gap-2 px-6 py-4\", className)}\n {...props}\n >\n {shouldShowBack && (\n <IconButton\n variant=\"tertiary\"\n size=\"32\"\n icon={<ArrowLeftIcon />}\n onClick={onBack}\n disabled={!onBack}\n aria-label={backLabel}\n />\n )}\n <div className=\"min-w-0 flex-1\">{children}</div>\n {showClose && (\n <DialogPrimitive.Close asChild>\n <IconButton variant=\"tertiary\" size=\"32\" icon={<CloseIcon />} aria-label={closeLabel} />\n </DialogPrimitive.Close>\n )}\n </div>\n );\n },\n);\nDialogHeader.displayName = \"DialogHeader\";\n\n/** Props for the {@link DialogTitle} component. */\nexport type DialogTitleProps = React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title>;\n\n/**\n * Accessible title for the dialog. Must be rendered inside {@link DialogHeader}\n * or directly within {@link DialogContent}.\n */\nexport const DialogTitle = React.forwardRef<\n React.ComponentRef<typeof DialogPrimitive.Title>,\n DialogTitleProps\n>(({ className, ...props }, ref) => (\n <DialogPrimitive.Title\n ref={ref}\n className={cn(\"typography-header-heading-xs truncate text-content-primary\", className)}\n {...props}\n />\n));\nDialogTitle.displayName = \"DialogTitle\";\n\n/** Props for the {@link DialogDescription} component. */\nexport type DialogDescriptionProps = React.ComponentPropsWithoutRef<\n typeof DialogPrimitive.Description\n>;\n\n/** Accessible description for the dialog. Rendered as secondary text. */\nexport const DialogDescription = React.forwardRef<\n React.ComponentRef<typeof DialogPrimitive.Description>,\n DialogDescriptionProps\n>(({ className, ...props }, ref) => (\n <DialogPrimitive.Description\n ref={ref}\n className={cn(\"typography-body-default-16px-regular text-content-secondary\", className)}\n {...props}\n />\n));\nDialogDescription.displayName = \"DialogDescription\";\n\nexport interface DialogBodyProps extends React.HTMLAttributes<HTMLDivElement> {}\n\n/**\n * Scrollable content area (slot) between the header and footer.\n * Grows to fill available space and scrolls when content overflows.\n */\nexport const DialogBody = React.forwardRef<HTMLDivElement, DialogBodyProps>(\n ({ className, ...props }, ref) => (\n <div ref={ref} className={cn(\"flex-1 overflow-y-auto px-6 py-4\", className)} {...props} />\n ),\n);\nDialogBody.displayName = \"DialogBody\";\n\nexport interface DialogFooterProps extends React.HTMLAttributes<HTMLDivElement> {}\n\n/**\n * Footer bar for the dialog. Typically contains action buttons.\n * Children are laid out in a horizontal row with equal flex-basis.\n */\nexport const DialogFooter = React.forwardRef<HTMLDivElement, DialogFooterProps>(\n ({ className, ...props }, ref) => (\n <div\n ref={ref}\n className={cn(\n \"flex shrink-0 items-center gap-3 px-6 pt-3 pb-6\",\n \"[&>*]:min-w-0 [&>*]:flex-1\",\n className,\n )}\n {...props}\n />\n ),\n);\nDialogFooter.displayName = \"DialogFooter\";\n"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"Dialog.mjs","sources":["../../../src/components/Dialog/Dialog.tsx"],"sourcesContent":["import * as DialogPrimitive from \"@radix-ui/react-dialog\";\nimport * as React from \"react\";\nimport { cn } from \"../../utils/cn\";\nimport { useSuppressClickAfterDrag } from \"../../utils/useSuppressClickAfterDrag\";\nimport { IconButton } from \"../IconButton/IconButton\";\nimport { ArrowLeftIcon } from \"../Icons/ArrowLeftIcon\";\nimport { CloseIcon } from \"../Icons/CloseIcon\";\n\n/** Props for the {@link Dialog} root component. */\nexport interface DialogProps extends React.ComponentPropsWithoutRef<typeof DialogPrimitive.Root> {\n /** Controlled open state. When provided, you must also supply `onOpenChange`. */\n open?: boolean;\n /** Called when the open state changes. Required when `open` is controlled. */\n onOpenChange?: (open: boolean) => void;\n /** The open state of the dialog when it is initially rendered (uncontrolled). */\n defaultOpen?: boolean;\n}\n\n/** Root component that manages open/close state for a dialog. */\nexport const Dialog = DialogPrimitive.Root;\n\n/** Props for the {@link DialogTrigger} component. */\nexport type DialogTriggerProps = React.ComponentPropsWithoutRef<typeof DialogPrimitive.Trigger>;\n\n/**\n * The element that opens the dialog when clicked.\n *\n * On touch / pen, a press-and-release that crosses a small movement threshold\n * is treated as a drag and the resulting synthetic click is suppressed —\n * defends against Android Chrome opening the dialog on a scroll-drag-end.\n */\nexport const DialogTrigger = React.forwardRef<\n React.ComponentRef<typeof DialogPrimitive.Trigger>,\n DialogTriggerProps\n>((props, ref) => <DialogPrimitive.Trigger ref={ref} {...useSuppressClickAfterDrag(props)} />);\nDialogTrigger.displayName = \"DialogTrigger\";\n\n/** Convenience alias for Radix `Dialog.Close`. Closes the dialog when clicked. */\nexport const DialogClose = DialogPrimitive.Close;\n\n/** Props for the {@link DialogClose} component. */\nexport type DialogCloseProps = React.ComponentPropsWithoutRef<typeof DialogPrimitive.Close>;\n\nexport interface DialogOverlayProps\n extends React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay> {}\n\n/**\n * Semi-transparent backdrop rendered behind the dialog content.\n * Rendered by {@link DialogContent}; portaled to `document.body` when {@link DialogContent} `portal` is true.\n */\nexport const DialogOverlay = React.forwardRef<\n React.ComponentRef<typeof DialogPrimitive.Overlay>,\n DialogOverlayProps\n>(({ className, style, ...props }, ref) => (\n <DialogPrimitive.Overlay\n ref={ref}\n className={cn(\n \"data-[state=open]:fade-in-0 data-[state=closed]:fade-out-0 fixed inset-0 bg-background-overlay-default data-[state=closed]:animate-out data-[state=open]:animate-in\",\n className,\n )}\n style={{ zIndex: \"var(--fanvue-ui-portal-z-index, 50)\", ...style }}\n {...props}\n />\n));\nDialogOverlay.displayName = \"DialogOverlay\";\n\nexport interface DialogContentProps\n extends React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content> {\n /**\n * Width preset for the dialog.\n * - `\"sm\"` — 400px max-width (confirmations, simple forms)\n * - `\"md\"` — 440px max-width (default, standard dialogs)\n * - `\"lg\"` — 600px max-width (complex content, tables)\n *\n * @default \"md\"\n */\n size?: \"sm\" | \"md\" | \"lg\";\n /** When true, renders overlay automatically. @default true */\n overlay?: boolean;\n /**\n * When true, teleports overlay and panel to `document.body`.\n * When false, renders inline in the React tree (useful inside theme providers or scoped containers).\n * @default true\n */\n portal?: boolean;\n /** Show the v2 mobile sheet pull handle. @default true */\n showMobileHandle?: boolean;\n}\n\nconst SIZE_CLASSES: Record<NonNullable<DialogContentProps[\"size\"]>, string> = {\n sm: \"sm:max-w-[400px]\",\n md: \"sm:max-w-[440px]\",\n lg: \"sm:max-w-[600px]\",\n};\n\n/**\n * The dialog panel. Includes the overlay by default and portals to `document.body` by default.\n *\n * Set `portal={false}` to keep overlay and content in the DOM subtree of the parent `Dialog`.\n * `fixed` positioning still applies; ancestors with `transform` or `overflow` may affect layout.\n *\n * On mobile viewports (<640px), the dialog slides up from the bottom as a sheet\n * with top-only border radius. On larger viewports it renders centered with\n * full border radius.\n *\n * @example\n * ```tsx\n * <Dialog>\n * <DialogTrigger asChild>\n * <Button>Open</Button>\n * </DialogTrigger>\n * <DialogContent>\n * <DialogHeader>\n * <DialogTitle>Title</DialogTitle>\n * </DialogHeader>\n * <DialogBody>Content here</DialogBody>\n * <DialogFooter>\n * <DialogClose asChild>\n * <Button variant=\"secondary\">Cancel</Button>\n * </DialogClose>\n * <Button>Accept</Button>\n * </DialogFooter>\n * </DialogContent>\n * </Dialog>\n * ```\n */\nexport const DialogContent = React.forwardRef<\n React.ComponentRef<typeof DialogPrimitive.Content>,\n DialogContentProps\n>(\n (\n {\n className,\n children,\n size = \"md\",\n overlay = true,\n portal = true,\n showMobileHandle = true,\n style,\n onOpenAutoFocus,\n ...props\n },\n ref,\n ) => {\n const content = (\n <>\n {overlay && <DialogOverlay />}\n <DialogPrimitive.Content\n ref={ref}\n style={{ zIndex: \"var(--fanvue-ui-portal-z-index, 50)\", ...style }}\n onOpenAutoFocus={(e) => {\n if (onOpenAutoFocus) {\n onOpenAutoFocus(e);\n return;\n }\n e.preventDefault();\n (e.currentTarget as HTMLElement).focus();\n }}\n className={cn(\n \"fixed flex flex-col overflow-hidden border border-modal-stroke bg-modal-background shadow-blur-menu backdrop-blur-[4px] focus:outline-none\",\n \"inset-x-0 bottom-0 max-h-[85vh] w-full rounded-t-xl p-4 pt-3\",\n \"data-[state=open]:fade-in-0 data-[state=open]:animate-in\",\n \"data-[state=closed]:fade-out-0 data-[state=closed]:animate-out\",\n \"data-[state=open]:slide-in-from-bottom-full\",\n \"data-[state=closed]:slide-out-to-bottom-full\",\n \"sm:inset-auto sm:top-1/2 sm:left-1/2 sm:max-h-[85vh] sm:w-full sm:-translate-x-1/2 sm:-translate-y-1/2 sm:rounded-xl sm:p-6\",\n \"sm:data-[state=open]:slide-in-from-bottom-0 sm:data-[state=open]:zoom-in-95\",\n \"sm:data-[state=closed]:slide-out-to-bottom-0 sm:data-[state=closed]:zoom-out-95\",\n \"duration-200\",\n SIZE_CLASSES[size],\n className,\n )}\n {...props}\n >\n {showMobileHandle && (\n <div\n aria-hidden=\"true\"\n className=\"mb-3 h-1 w-8 shrink-0 self-center rounded-full bg-icons-tertiary sm:hidden\"\n />\n )}\n {children}\n </DialogPrimitive.Content>\n </>\n );\n\n return portal ? <DialogPrimitive.Portal>{content}</DialogPrimitive.Portal> : content;\n },\n);\nDialogContent.displayName = \"DialogContent\";\n\nexport interface DialogHeaderProps extends React.HTMLAttributes<HTMLDivElement> {\n /** Show the close (X) button in the header. @default true */\n showClose?: boolean;\n /** Show a back arrow button on the left side. Defaults to `true` when `onBack` is provided. */\n showBack?: boolean;\n /** Called when the back button is clicked. */\n onBack?: () => void;\n /** Accessible label for the back button. @default \"Go back\" */\n backLabel?: string;\n /** Accessible label for the close button. @default \"Close\" */\n closeLabel?: string;\n}\n\n/**\n * Header bar for the dialog. Renders the title with an optional back arrow\n * and close button.\n *\n * @example\n * ```tsx\n * <DialogHeader>\n * <DialogTitle>Settings</DialogTitle>\n * </DialogHeader>\n *\n * <DialogHeader showBack onBack={() => setStep(0)}>\n * <DialogTitle>Step 2</DialogTitle>\n * </DialogHeader>\n * ```\n */\nexport const DialogHeader = React.forwardRef<HTMLDivElement, DialogHeaderProps>(\n (\n {\n className,\n children,\n showClose = true,\n showBack,\n onBack,\n backLabel = \"Go back\",\n closeLabel = \"Close\",\n ...props\n },\n ref,\n ) => {\n const shouldShowBack = showBack ?? !!onBack;\n\n return (\n <div\n ref={ref}\n className={cn(\"flex shrink-0 items-center justify-end gap-4\", className)}\n {...props}\n >\n {shouldShowBack && (\n <IconButton\n variant=\"secondary\"\n size=\"32\"\n icon={<ArrowLeftIcon size={16} />}\n onClick={onBack}\n disabled={!onBack}\n aria-label={backLabel}\n />\n )}\n <div className=\"min-w-0 flex-1\">{children}</div>\n {showClose && (\n <DialogPrimitive.Close asChild>\n <IconButton\n variant=\"secondary\"\n size=\"32\"\n icon={<CloseIcon size={16} />}\n aria-label={closeLabel}\n />\n </DialogPrimitive.Close>\n )}\n </div>\n );\n },\n);\nDialogHeader.displayName = \"DialogHeader\";\n\n/** Props for the {@link DialogTitle} component. */\nexport type DialogTitleProps = React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title>;\n\n/**\n * Accessible title for the dialog. Must be rendered inside {@link DialogHeader}\n * or directly within {@link DialogContent}.\n */\nexport const DialogTitle = React.forwardRef<\n React.ComponentRef<typeof DialogPrimitive.Title>,\n DialogTitleProps\n>(({ className, ...props }, ref) => (\n <DialogPrimitive.Title\n ref={ref}\n className={cn(\"typography-header-heading-xs text-content-primary\", className)}\n {...props}\n />\n));\nDialogTitle.displayName = \"DialogTitle\";\n\n/** Props for the {@link DialogDescription} component. */\nexport type DialogDescriptionProps = React.ComponentPropsWithoutRef<\n typeof DialogPrimitive.Description\n>;\n\n/** Accessible description for the dialog. Rendered as secondary text. */\nexport const DialogDescription = React.forwardRef<\n React.ComponentRef<typeof DialogPrimitive.Description>,\n DialogDescriptionProps\n>(({ className, ...props }, ref) => (\n <DialogPrimitive.Description\n ref={ref}\n className={cn(\"typography-body-default-16px-regular text-content-secondary\", className)}\n {...props}\n />\n));\nDialogDescription.displayName = \"DialogDescription\";\n\nexport interface DialogBodyProps extends React.HTMLAttributes<HTMLDivElement> {}\n\n/**\n * Scrollable content area (slot) between the header and footer.\n * Grows to fill available space and scrolls when content overflows.\n */\nexport const DialogBody = React.forwardRef<HTMLDivElement, DialogBodyProps>(\n ({ className, ...props }, ref) => (\n <div ref={ref} className={cn(\"flex-1 overflow-y-auto py-4 sm:py-6\", className)} {...props} />\n ),\n);\nDialogBody.displayName = \"DialogBody\";\n\nexport interface DialogFooterProps extends React.HTMLAttributes<HTMLDivElement> {}\n\n/**\n * Footer bar for the dialog. Typically contains action buttons.\n * Children are laid out in a horizontal row with equal flex-basis.\n */\nexport const DialogFooter = React.forwardRef<HTMLDivElement, DialogFooterProps>(\n ({ className, ...props }, ref) => (\n <div\n ref={ref}\n className={cn(\"flex shrink-0 items-center gap-2\", \"[&>*]:min-w-0 [&>*]:flex-1\", className)}\n {...props}\n />\n ),\n);\nDialogFooter.displayName = \"DialogFooter\";\n"],"names":[],"mappings":"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"}
package/dist/index.d.ts CHANGED
@@ -826,10 +826,16 @@ export declare type BulbIconProps = BaseIconProps;
826
826
  * A versatile button component with multiple visual variants, sizes, icon
827
827
  * slots, loading state, and optional pricing display.
828
828
  *
829
+ * Pass `negative` when rendering on a dark surface to opt into the inverted
830
+ * treatment for `primary`, `secondary`, `tertiary`, and `outline` variants.
831
+ *
832
+ * The `ai` variant ships with the AI sparkle icon baked in (locked at 16px
833
+ * regardless of button size); pass `leftIcon` to override the default sparkle.
834
+ *
829
835
  * @example
830
836
  * ```tsx
831
- * <Button variant="brand" size="40" leftIcon={<StarIcon />}>
832
- * Subscribe
837
+ * <Button variant="primary" size="40" leftIcon={<StarIcon />}>
838
+ * Continue
833
839
  * </Button>
834
840
  * ```
835
841
  */
@@ -840,6 +846,14 @@ export declare interface ButtonProps extends React_2.ButtonHTMLAttributes<HTMLBu
840
846
  variant?: ButtonVariant;
841
847
  /** Height of the button in pixels. @default "40" */
842
848
  size?: ButtonSize;
849
+ /**
850
+ * Forces the dark-surface treatment regardless of theme. Only honored on
851
+ * `primary`, `secondary`, `tertiary`, and `outline` variants; ignored on
852
+ * all others. Use when placing the button on a dark background in a light
853
+ * theme (e.g. an inverted hero or modal).
854
+ * @default false
855
+ */
856
+ negative?: boolean;
843
857
  /** Icon element displayed before the label. */
844
858
  leftIcon?: React_2.ReactNode;
845
859
  /** Icon element displayed after the label. */
@@ -860,7 +874,7 @@ export declare interface ButtonProps extends React_2.ButtonHTMLAttributes<HTMLBu
860
874
  export declare type ButtonSize = "48" | "40" | "32" | "24";
861
875
 
862
876
  /** Visual style variant of the button. */
863
- export declare type ButtonVariant = "primary" | "secondary" | "tertiary" | "link" | "brand" | "destructive" | "white" | "tertiaryDestructive" | "text";
877
+ export declare type ButtonVariant = "primary" | "secondary" | "tertiary" | "outline" | "link" | "brand" | "destructive" | "white" | "alwaysBlack" | "ai" | "tertiaryDestructive" | "text";
864
878
 
865
879
  /**
866
880
  * Calendar 2 icon. Renders at sizes 16, 24, or 32 px with outlined and filled variants.
@@ -1468,7 +1482,7 @@ export declare interface CreatorCardProps extends React_2.HTMLAttributes<HTMLDiv
1468
1482
  * name="JANE DOE"
1469
1483
  * tagline="GLOBAL POPSTAR"
1470
1484
  * tag="New Joiner"
1471
- * action={<Button variant="primary" size="48" fullWidth>Join for free for 7 days</Button>}
1485
+ * action={<Button variant="brand" size="48" fullWidth>Join for free for 7 days</Button>}
1472
1486
  * />
1473
1487
  * ```
1474
1488
  */
@@ -1697,6 +1711,8 @@ export declare interface DialogContentProps extends React_2.ComponentPropsWithou
1697
1711
  * @default true
1698
1712
  */
1699
1713
  portal?: boolean;
1714
+ /** Show the v2 mobile sheet pull handle. @default true */
1715
+ showMobileHandle?: boolean;
1700
1716
  }
1701
1717
 
1702
1718
  /** Accessible description for the dialog. Rendered as secondary text. */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fanvue/ui",
3
- "version": "3.2.0",
3
+ "version": "3.3.0",
4
4
  "description": "React component library built with Tailwind CSS for Fanvue ecosystem",
5
5
  "publishConfig": {
6
6
  "registry": "https://registry.npmjs.org",