@fanvue/ui 3.1.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":"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"}
@@ -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;;"}
@@ -49,48 +49,64 @@ const SIZE_CLASSES = {
49
49
  md: "sm:max-w-[440px]",
50
50
  lg: "sm:max-w-[600px]"
51
51
  };
52
- const DialogContent = React__namespace.forwardRef(({ className, children, size = "md", overlay = true, style, onOpenAutoFocus, ...props }, ref) => /* @__PURE__ */ jsxRuntime.jsxs(DialogPrimitive__namespace.Portal, { children: [
53
- overlay && /* @__PURE__ */ jsxRuntime.jsx(DialogOverlay, {}),
54
- /* @__PURE__ */ jsxRuntime.jsx(
55
- DialogPrimitive__namespace.Content,
56
- {
57
- ref,
58
- style: { zIndex: "var(--fanvue-ui-portal-z-index, 50)", ...style },
59
- onOpenAutoFocus: (e) => {
60
- if (onOpenAutoFocus) {
61
- onOpenAutoFocus(e);
62
- return;
52
+ const DialogContent = React__namespace.forwardRef(
53
+ ({
54
+ className,
55
+ children,
56
+ size = "md",
57
+ overlay = true,
58
+ portal = true,
59
+ showMobileHandle = true,
60
+ style,
61
+ onOpenAutoFocus,
62
+ ...props
63
+ }, ref) => {
64
+ const content = /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
65
+ overlay && /* @__PURE__ */ jsxRuntime.jsx(DialogOverlay, {}),
66
+ /* @__PURE__ */ jsxRuntime.jsxs(
67
+ DialogPrimitive__namespace.Content,
68
+ {
69
+ ref,
70
+ style: { zIndex: "var(--fanvue-ui-portal-z-index, 50)", ...style },
71
+ onOpenAutoFocus: (e) => {
72
+ if (onOpenAutoFocus) {
73
+ onOpenAutoFocus(e);
74
+ return;
75
+ }
76
+ e.preventDefault();
77
+ e.currentTarget.focus();
78
+ },
79
+ className: cn.cn(
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",
82
+ "data-[state=open]:fade-in-0 data-[state=open]:animate-in",
83
+ "data-[state=closed]:fade-out-0 data-[state=closed]:animate-out",
84
+ "data-[state=open]:slide-in-from-bottom-full",
85
+ "data-[state=closed]:slide-out-to-bottom-full",
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",
87
+ "sm:data-[state=open]:slide-in-from-bottom-0 sm:data-[state=open]:zoom-in-95",
88
+ "sm:data-[state=closed]:slide-out-to-bottom-0 sm:data-[state=closed]:zoom-out-95",
89
+ "duration-200",
90
+ SIZE_CLASSES[size],
91
+ className
92
+ ),
93
+ ...props,
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
+ ]
63
104
  }
64
- e.preventDefault();
65
- e.currentTarget.focus();
66
- },
67
- className: cn.cn(
68
- // Base
69
- "fixed flex flex-col overflow-hidden bg-background-primary shadow-lg focus:outline-none dark:bg-surface-primary",
70
- // Mobile: bottom sheet
71
- "inset-x-0 bottom-0 max-h-[85vh] w-full rounded-t-lg",
72
- // Animation (shared)
73
- "data-[state=open]:fade-in-0 data-[state=open]:animate-in",
74
- "data-[state=closed]:fade-out-0 data-[state=closed]:animate-out",
75
- // Mobile: slide up from bottom
76
- "data-[state=open]:slide-in-from-bottom-full",
77
- "data-[state=closed]:slide-out-to-bottom-full",
78
- // Desktop: centered dialog
79
- "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",
80
- // Desktop: scale in/out (cancel mobile slide)
81
- "sm:data-[state=open]:slide-in-from-bottom-0 sm:data-[state=open]:zoom-in-95",
82
- "sm:data-[state=closed]:slide-out-to-bottom-0 sm:data-[state=closed]:zoom-out-95",
83
- // Duration
84
- "duration-200",
85
- // Size
86
- SIZE_CLASSES[size],
87
- className
88
- ),
89
- ...props,
90
- children
91
- }
92
- )
93
- ] }));
105
+ )
106
+ ] });
107
+ return portal ? /* @__PURE__ */ jsxRuntime.jsx(DialogPrimitive__namespace.Portal, { children: content }) : content;
108
+ }
109
+ );
94
110
  DialogContent.displayName = "DialogContent";
95
111
  const DialogHeader = React__namespace.forwardRef(
96
112
  ({
@@ -108,22 +124,30 @@ const DialogHeader = React__namespace.forwardRef(
108
124
  "div",
109
125
  {
110
126
  ref,
111
- 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),
112
128
  ...props,
113
129
  children: [
114
130
  shouldShowBack && /* @__PURE__ */ jsxRuntime.jsx(
115
131
  IconButton.IconButton,
116
132
  {
117
- variant: "tertiary",
133
+ variant: "secondary",
118
134
  size: "32",
119
- icon: /* @__PURE__ */ jsxRuntime.jsx(ArrowLeftIcon.ArrowLeftIcon, {}),
135
+ icon: /* @__PURE__ */ jsxRuntime.jsx(ArrowLeftIcon.ArrowLeftIcon, { size: 16 }),
120
136
  onClick: onBack,
121
137
  disabled: !onBack,
122
138
  "aria-label": backLabel
123
139
  }
124
140
  ),
125
141
  /* @__PURE__ */ jsxRuntime.jsx("div", { className: "min-w-0 flex-1", children }),
126
- 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
+ ) })
127
151
  ]
128
152
  }
129
153
  );
@@ -134,7 +158,7 @@ const DialogTitle = React__namespace.forwardRef(({ className, ...props }, ref) =
134
158
  DialogPrimitive__namespace.Title,
135
159
  {
136
160
  ref,
137
- className: cn.cn("typography-header-heading-xs truncate text-content-primary", className),
161
+ className: cn.cn("typography-header-heading-xs text-content-primary", className),
138
162
  ...props
139
163
  }
140
164
  ));
@@ -149,7 +173,7 @@ const DialogDescription = React__namespace.forwardRef(({ className, ...props },
149
173
  ));
150
174
  DialogDescription.displayName = "DialogDescription";
151
175
  const DialogBody = React__namespace.forwardRef(
152
- ({ 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 })
153
177
  );
154
178
  DialogBody.displayName = "DialogBody";
155
179
  const DialogFooter = React__namespace.forwardRef(
@@ -157,11 +181,7 @@ const DialogFooter = React__namespace.forwardRef(
157
181
  "div",
158
182
  {
159
183
  ref,
160
- className: cn.cn(
161
- "flex shrink-0 items-center gap-3 px-6 pt-3 pb-6",
162
- "[&>*]:min-w-0 [&>*]:flex-1",
163
- className
164
- ),
184
+ className: cn.cn("flex shrink-0 items-center gap-2", "[&>*]:min-w-0 [&>*]:flex-1", className),
165
185
  ...props
166
186
  }
167
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 inside a portal automatically by {@link DialogContent}.\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\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 rendered inside a portal. Includes the overlay by default.\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>(({ className, children, size = \"md\", overlay = true, style, onOpenAutoFocus, ...props }, ref) => (\n <DialogPrimitive.Portal>\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 // Base\n \"fixed flex flex-col overflow-hidden bg-background-primary shadow-lg focus:outline-none dark:bg-surface-primary\",\n // Mobile: bottom sheet\n \"inset-x-0 bottom-0 max-h-[85vh] w-full rounded-t-lg\",\n // Animation (shared)\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 // Mobile: slide up from bottom\n \"data-[state=open]:slide-in-from-bottom-full\",\n \"data-[state=closed]:slide-out-to-bottom-full\",\n // Desktop: centered dialog\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 // Desktop: scale in/out (cancel mobile slide)\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\n \"duration-200\",\n // Size\n SIZE_CLASSES[size],\n className,\n )}\n {...props}\n >\n {children}\n </DialogPrimitive.Content>\n </DialogPrimitive.Portal>\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","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;"}
@@ -1,5 +1,5 @@
1
1
  "use client";
2
- import { jsx, jsxs } from "react/jsx-runtime";
2
+ import { jsx, jsxs, Fragment } from "react/jsx-runtime";
3
3
  import * as DialogPrimitive from "@radix-ui/react-dialog";
4
4
  import * as React from "react";
5
5
  import { cn } from "../../utils/cn.mjs";
@@ -29,48 +29,64 @@ const SIZE_CLASSES = {
29
29
  md: "sm:max-w-[440px]",
30
30
  lg: "sm:max-w-[600px]"
31
31
  };
32
- const DialogContent = React.forwardRef(({ className, children, size = "md", overlay = true, style, onOpenAutoFocus, ...props }, ref) => /* @__PURE__ */ jsxs(DialogPrimitive.Portal, { children: [
33
- overlay && /* @__PURE__ */ jsx(DialogOverlay, {}),
34
- /* @__PURE__ */ jsx(
35
- DialogPrimitive.Content,
36
- {
37
- ref,
38
- style: { zIndex: "var(--fanvue-ui-portal-z-index, 50)", ...style },
39
- onOpenAutoFocus: (e) => {
40
- if (onOpenAutoFocus) {
41
- onOpenAutoFocus(e);
42
- return;
32
+ const DialogContent = React.forwardRef(
33
+ ({
34
+ className,
35
+ children,
36
+ size = "md",
37
+ overlay = true,
38
+ portal = true,
39
+ showMobileHandle = true,
40
+ style,
41
+ onOpenAutoFocus,
42
+ ...props
43
+ }, ref) => {
44
+ const content = /* @__PURE__ */ jsxs(Fragment, { children: [
45
+ overlay && /* @__PURE__ */ jsx(DialogOverlay, {}),
46
+ /* @__PURE__ */ jsxs(
47
+ DialogPrimitive.Content,
48
+ {
49
+ ref,
50
+ style: { zIndex: "var(--fanvue-ui-portal-z-index, 50)", ...style },
51
+ onOpenAutoFocus: (e) => {
52
+ if (onOpenAutoFocus) {
53
+ onOpenAutoFocus(e);
54
+ return;
55
+ }
56
+ e.preventDefault();
57
+ e.currentTarget.focus();
58
+ },
59
+ className: cn(
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",
62
+ "data-[state=open]:fade-in-0 data-[state=open]:animate-in",
63
+ "data-[state=closed]:fade-out-0 data-[state=closed]:animate-out",
64
+ "data-[state=open]:slide-in-from-bottom-full",
65
+ "data-[state=closed]:slide-out-to-bottom-full",
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",
67
+ "sm:data-[state=open]:slide-in-from-bottom-0 sm:data-[state=open]:zoom-in-95",
68
+ "sm:data-[state=closed]:slide-out-to-bottom-0 sm:data-[state=closed]:zoom-out-95",
69
+ "duration-200",
70
+ SIZE_CLASSES[size],
71
+ className
72
+ ),
73
+ ...props,
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
+ ]
43
84
  }
44
- e.preventDefault();
45
- e.currentTarget.focus();
46
- },
47
- className: cn(
48
- // Base
49
- "fixed flex flex-col overflow-hidden bg-background-primary shadow-lg focus:outline-none dark:bg-surface-primary",
50
- // Mobile: bottom sheet
51
- "inset-x-0 bottom-0 max-h-[85vh] w-full rounded-t-lg",
52
- // Animation (shared)
53
- "data-[state=open]:fade-in-0 data-[state=open]:animate-in",
54
- "data-[state=closed]:fade-out-0 data-[state=closed]:animate-out",
55
- // Mobile: slide up from bottom
56
- "data-[state=open]:slide-in-from-bottom-full",
57
- "data-[state=closed]:slide-out-to-bottom-full",
58
- // Desktop: centered dialog
59
- "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",
60
- // Desktop: scale in/out (cancel mobile slide)
61
- "sm:data-[state=open]:slide-in-from-bottom-0 sm:data-[state=open]:zoom-in-95",
62
- "sm:data-[state=closed]:slide-out-to-bottom-0 sm:data-[state=closed]:zoom-out-95",
63
- // Duration
64
- "duration-200",
65
- // Size
66
- SIZE_CLASSES[size],
67
- className
68
- ),
69
- ...props,
70
- children
71
- }
72
- )
73
- ] }));
85
+ )
86
+ ] });
87
+ return portal ? /* @__PURE__ */ jsx(DialogPrimitive.Portal, { children: content }) : content;
88
+ }
89
+ );
74
90
  DialogContent.displayName = "DialogContent";
75
91
  const DialogHeader = React.forwardRef(
76
92
  ({
@@ -88,22 +104,30 @@ const DialogHeader = React.forwardRef(
88
104
  "div",
89
105
  {
90
106
  ref,
91
- 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),
92
108
  ...props,
93
109
  children: [
94
110
  shouldShowBack && /* @__PURE__ */ jsx(
95
111
  IconButton,
96
112
  {
97
- variant: "tertiary",
113
+ variant: "secondary",
98
114
  size: "32",
99
- icon: /* @__PURE__ */ jsx(ArrowLeftIcon, {}),
115
+ icon: /* @__PURE__ */ jsx(ArrowLeftIcon, { size: 16 }),
100
116
  onClick: onBack,
101
117
  disabled: !onBack,
102
118
  "aria-label": backLabel
103
119
  }
104
120
  ),
105
121
  /* @__PURE__ */ jsx("div", { className: "min-w-0 flex-1", children }),
106
- 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
+ ) })
107
131
  ]
108
132
  }
109
133
  );
@@ -114,7 +138,7 @@ const DialogTitle = React.forwardRef(({ className, ...props }, ref) => /* @__PUR
114
138
  DialogPrimitive.Title,
115
139
  {
116
140
  ref,
117
- className: cn("typography-header-heading-xs truncate text-content-primary", className),
141
+ className: cn("typography-header-heading-xs text-content-primary", className),
118
142
  ...props
119
143
  }
120
144
  ));
@@ -129,7 +153,7 @@ const DialogDescription = React.forwardRef(({ className, ...props }, ref) => /*
129
153
  ));
130
154
  DialogDescription.displayName = "DialogDescription";
131
155
  const DialogBody = React.forwardRef(
132
- ({ 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 })
133
157
  );
134
158
  DialogBody.displayName = "DialogBody";
135
159
  const DialogFooter = React.forwardRef(
@@ -137,11 +161,7 @@ const DialogFooter = React.forwardRef(
137
161
  "div",
138
162
  {
139
163
  ref,
140
- className: cn(
141
- "flex shrink-0 items-center gap-3 px-6 pt-3 pb-6",
142
- "[&>*]:min-w-0 [&>*]:flex-1",
143
- className
144
- ),
164
+ className: cn("flex shrink-0 items-center gap-2", "[&>*]:min-w-0 [&>*]:flex-1", className),
145
165
  ...props
146
166
  }
147
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 inside a portal automatically by {@link DialogContent}.\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\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 rendered inside a portal. Includes the overlay by default.\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>(({ className, children, size = \"md\", overlay = true, style, onOpenAutoFocus, ...props }, ref) => (\n <DialogPrimitive.Portal>\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 // Base\n \"fixed flex flex-col overflow-hidden bg-background-primary shadow-lg focus:outline-none dark:bg-surface-primary\",\n // Mobile: bottom sheet\n \"inset-x-0 bottom-0 max-h-[85vh] w-full rounded-t-lg\",\n // Animation (shared)\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 // Mobile: slide up from bottom\n \"data-[state=open]:slide-in-from-bottom-full\",\n \"data-[state=closed]:slide-out-to-bottom-full\",\n // Desktop: centered dialog\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 // Desktop: scale in/out (cancel mobile slide)\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\n \"duration-200\",\n // Size\n SIZE_CLASSES[size],\n className,\n )}\n {...props}\n >\n {children}\n </DialogPrimitive.Content>\n </DialogPrimitive.Portal>\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
  */
@@ -1647,7 +1661,10 @@ export declare const DialogClose: React_2.ForwardRefExoticComponent<DialogPrimit
1647
1661
  export declare type DialogCloseProps = React_2.ComponentPropsWithoutRef<typeof DialogPrimitive.Close>;
1648
1662
 
1649
1663
  /**
1650
- * The dialog panel rendered inside a portal. Includes the overlay by default.
1664
+ * The dialog panel. Includes the overlay by default and portals to `document.body` by default.
1665
+ *
1666
+ * Set `portal={false}` to keep overlay and content in the DOM subtree of the parent `Dialog`.
1667
+ * `fixed` positioning still applies; ancestors with `transform` or `overflow` may affect layout.
1651
1668
  *
1652
1669
  * On mobile viewports (<640px), the dialog slides up from the bottom as a sheet
1653
1670
  * with top-only border radius. On larger viewports it renders centered with
@@ -1688,6 +1705,14 @@ export declare interface DialogContentProps extends React_2.ComponentPropsWithou
1688
1705
  size?: "sm" | "md" | "lg";
1689
1706
  /** When true, renders overlay automatically. @default true */
1690
1707
  overlay?: boolean;
1708
+ /**
1709
+ * When true, teleports overlay and panel to `document.body`.
1710
+ * When false, renders inline in the React tree (useful inside theme providers or scoped containers).
1711
+ * @default true
1712
+ */
1713
+ portal?: boolean;
1714
+ /** Show the v2 mobile sheet pull handle. @default true */
1715
+ showMobileHandle?: boolean;
1691
1716
  }
1692
1717
 
1693
1718
  /** Accessible description for the dialog. Rendered as secondary text. */
@@ -1737,7 +1762,7 @@ export declare interface DialogHeaderProps extends React_2.HTMLAttributes<HTMLDi
1737
1762
 
1738
1763
  /**
1739
1764
  * Semi-transparent backdrop rendered behind the dialog content.
1740
- * Rendered inside a portal automatically by {@link DialogContent}.
1765
+ * Rendered by {@link DialogContent}; portaled to `document.body` when {@link DialogContent} `portal` is true.
1741
1766
  */
1742
1767
  export declare const DialogOverlay: React_2.ForwardRefExoticComponent<DialogOverlayProps & React_2.RefAttributes<HTMLDivElement>>;
1743
1768
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fanvue/ui",
3
- "version": "3.1.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",