@fanvue/ui 1.10.2 → 1.11.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.
- package/dist/cjs/components/Avatar/Avatar.cjs +9 -9
- package/dist/cjs/components/Avatar/Avatar.cjs.map +1 -1
- package/dist/cjs/components/IconButton/IconButton.cjs +5 -5
- package/dist/cjs/components/IconButton/IconButton.cjs.map +1 -1
- package/dist/cjs/components/TextArea/TextArea.cjs +5 -3
- package/dist/cjs/components/TextArea/TextArea.cjs.map +1 -1
- package/dist/components/Avatar/Avatar.mjs +9 -9
- package/dist/components/Avatar/Avatar.mjs.map +1 -1
- package/dist/components/IconButton/IconButton.mjs +5 -5
- package/dist/components/IconButton/IconButton.mjs.map +1 -1
- package/dist/components/TextArea/TextArea.mjs +5 -3
- package/dist/components/TextArea/TextArea.mjs.map +1 -1
- package/dist/index.d.ts +2 -0
- package/package.json +1 -1
|
@@ -56,14 +56,14 @@ const AvatarRoot = React__namespace.forwardRef(
|
|
|
56
56
|
"data-testid": "avatar",
|
|
57
57
|
className: cn.cn(
|
|
58
58
|
"relative inline-flex shrink-0 items-center justify-center overflow-hidden rounded-full bg-neutral-100",
|
|
59
|
-
size === 16 && "
|
|
60
|
-
size === 24 && "
|
|
61
|
-
size === 32 && "
|
|
62
|
-
size === 40 && "
|
|
63
|
-
size === 48 && "
|
|
64
|
-
size === 64 && "
|
|
65
|
-
size === 88 && "
|
|
66
|
-
size === 148 && "
|
|
59
|
+
size === 16 && "size-4 text-[10px]",
|
|
60
|
+
size === 24 && "size-6 text-xs",
|
|
61
|
+
size === 32 && "size-8 text-xs",
|
|
62
|
+
size === 40 && "size-10 text-sm",
|
|
63
|
+
size === 48 && "size-12 text-base",
|
|
64
|
+
size === 64 && "size-16 text-xl",
|
|
65
|
+
size === 88 && "size-[88px] text-2xl",
|
|
66
|
+
size === 148 && "size-[148px] text-4xl",
|
|
67
67
|
className
|
|
68
68
|
),
|
|
69
69
|
...props,
|
|
@@ -114,7 +114,7 @@ const AvatarFallback = React__namespace.forwardRef(({ className, children, ...pr
|
|
|
114
114
|
{
|
|
115
115
|
ref,
|
|
116
116
|
className: cn.cn(
|
|
117
|
-
"flex size-full items-center justify-center bg-neutral-200 text-neutral-400 uppercase leading-none",
|
|
117
|
+
"flex size-full items-center justify-center bg-neutral-200 font-semibold text-neutral-400 uppercase leading-none",
|
|
118
118
|
className
|
|
119
119
|
),
|
|
120
120
|
delayMs: 0,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Avatar.cjs","sources":["../../../../src/components/Avatar/Avatar.tsx"],"sourcesContent":["import * as AvatarPrimitive from \"@radix-ui/react-avatar\";\nimport * as React from \"react\";\nimport { cn } from \"../../utils/cn\";\n\n/** Allowed pixel sizes for the avatar. */\nexport type AvatarSize = 16 | 24 | 32 | 40 | 48 | 64 | 88 | 148;\n\nconst AvatarContext = React.createContext<{ size: AvatarSize; NSFWShow: boolean }>({\n size: 40,\n NSFWShow: false,\n});\n\nconst STATUS_POSITIONS: Record<AvatarSize, { top: number; right: number }> = {\n 16: { top: -4, right: -4 },\n 24: { top: -3, right: -3 },\n 32: { top: -2, right: -2 },\n 40: { top: -1, right: -1 },\n 48: { top: 0, right: 0 },\n 64: { top: 2, right: 2 },\n 88: { top: 6, right: 6 },\n 148: { top: 15, right: 15 },\n};\n\n/** Shared avatar styling props. */\ninterface AvatarStyleProps {\n /** Pixel size of the avatar. @default 40 */\n size?: AvatarSize;\n /** Whether to show the online-status indicator dot. @default false */\n onlineIndicator?: boolean;\n /** Whether to show the platinum gradient border ring. @default false */\n platinumShow?: boolean;\n /** Whether to apply the NSFW blur filter over the image. @default false */\n NSFWShow?: boolean;\n}\n\n/** Props for the low-level {@link AvatarRoot} compound component. */\nexport interface AvatarRootProps\n extends React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Root>,\n AvatarStyleProps {}\n\n/**\n * Low-level avatar root for custom compositions. Provides size context to\n * child `AvatarImage` and `AvatarFallback` components.\n *\n * Prefer the higher-level {@link Avatar} component for most use cases.\n */\nconst AvatarRoot = React.forwardRef<\n React.ComponentRef<typeof AvatarPrimitive.Root>,\n AvatarRootProps\n>(\n (\n {\n className,\n size = 40,\n onlineIndicator = false,\n platinumShow = false,\n NSFWShow = false,\n children,\n ...props\n },\n ref,\n ) => {\n const statusPosition = STATUS_POSITIONS[size];\n\n return (\n <AvatarContext.Provider value={{ size, NSFWShow }}>\n <div className=\"relative inline-flex\">\n <AvatarPrimitive.Root\n ref={ref}\n data-testid=\"avatar\"\n className={cn(\n \"relative inline-flex shrink-0 items-center justify-center overflow-hidden rounded-full bg-neutral-100\",\n size === 16 && \"typography-caption-semibold size-4\",\n size === 24 && \"typography-caption-semibold size-6\",\n size === 32 && \"typography-body-2-semibold size-8\",\n size === 40 && \"typography-body-1-semibold size-10\",\n size === 48 && \"typography-heading-4 size-12\",\n size === 64 && \"typography-heading-3 size-16\",\n size === 88 && \"typography-heading-2 size-[88px]\",\n size === 148 && \"typography-heading-1 size-[148px]\",\n className,\n )}\n {...props}\n >\n {children}\n </AvatarPrimitive.Root>\n {platinumShow && (\n <div\n className=\"pointer-events-none absolute inset-0 rounded-full\"\n style={{\n background: `linear-gradient(143deg, #504F54 0%, #B1B1B1 20.3154%, #13181C 37.3727%, #C6C6C8 58.8154%, #FFFFFF 69.3154%, #0C0F14 81.3154%, #696A6E 100%)`,\n WebkitMask: \"radial-gradient(circle closest-side, transparent 96%, black 96%)\",\n mask: \"radial-gradient(circle closest-side, transparent 96%, black 96%)\",\n }}\n aria-hidden=\"true\"\n />\n )}\n {onlineIndicator && (\n <span\n className=\"absolute size-3 rounded-full border-2 border-background-150 bg-brand-green-500\"\n style={{\n top: `${statusPosition.top}px`,\n right: `${statusPosition.right}px`,\n }}\n aria-hidden=\"true\"\n />\n )}\n </div>\n </AvatarContext.Provider>\n );\n },\n);\n\nAvatarRoot.displayName = \"AvatarRoot\";\n\n/** Props for the {@link AvatarImage} compound component. */\nexport interface AvatarImageProps\n extends React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Image> {}\n\n/** Renders the avatar image. Automatically applies the NSFW blur when enabled on the parent `AvatarRoot`. */\nconst AvatarImage = React.forwardRef<\n React.ComponentRef<typeof AvatarPrimitive.Image>,\n AvatarImageProps\n>(({ className, ...props }, ref) => {\n const { NSFWShow } = React.useContext(AvatarContext);\n return (\n <AvatarPrimitive.Image\n ref={ref}\n className={cn(\"size-full bg-neutral-200 object-cover\", NSFWShow && \"blur-md\", className)}\n {...props}\n />\n );\n});\n\nAvatarImage.displayName = \"AvatarImage\";\n\n/** Props for the {@link AvatarFallback} compound component. */\nexport interface AvatarFallbackProps\n extends React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Fallback> {}\n\n/** Renders fallback content (e.g. initials or an icon) when the avatar image has not loaded. */\nconst AvatarFallback = React.forwardRef<\n React.ComponentRef<typeof AvatarPrimitive.Fallback>,\n AvatarFallbackProps\n>(({ className, children, ...props }, ref) => (\n <AvatarPrimitive.Fallback\n ref={ref}\n className={cn(\n \"flex size-full items-center justify-center bg-neutral-200 text-neutral-400 uppercase leading-none\",\n className,\n )}\n delayMs={0}\n {...props}\n >\n {children}\n </AvatarPrimitive.Fallback>\n));\n\nAvatarFallback.displayName = \"AvatarFallback\";\n\nexport interface AvatarProps\n extends React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Root>,\n AvatarStyleProps {\n /** URL of the avatar image. */\n src?: string;\n /** Alt text for the avatar image. @default \"Avatar\" */\n alt?: string;\n /** Fallback content rendered when the image has not loaded (e.g. initials or an icon). */\n fallback?: React.ReactNode;\n}\n\n/**\n * Displays a user avatar with optional online indicator, platinum border, and\n * NSFW blur. Pass `src` and `fallback` for the simple API, or compose your own\n * layout with `AvatarRoot`, `AvatarImage`, and `AvatarFallback` as children.\n *\n * @example\n * ```tsx\n * <Avatar src=\"/photo.jpg\" alt=\"Jane Doe\" fallback=\"JD\" size={48} />\n * ```\n */\nexport const Avatar = React.forwardRef<\n React.ComponentRef<typeof AvatarPrimitive.Root>,\n AvatarProps\n>(\n (\n {\n className,\n size = 40,\n src,\n alt,\n fallback,\n onlineIndicator = false,\n platinumShow = false,\n NSFWShow = false,\n children,\n ...props\n },\n ref,\n ) => {\n const rootProps = {\n ref,\n size,\n onlineIndicator,\n platinumShow,\n NSFWShow,\n className,\n ...props,\n };\n\n if (children) {\n return <AvatarRoot {...rootProps}>{children}</AvatarRoot>;\n }\n\n return (\n <AvatarRoot {...rootProps}>\n {src && <AvatarImage src={src} alt={alt ?? \"Avatar\"} />}\n <AvatarFallback>{fallback}</AvatarFallback>\n </AvatarRoot>\n );\n },\n);\n\nAvatar.displayName = \"Avatar\";\n\nexport { AvatarRoot, AvatarImage, AvatarFallback };\n"],"names":["React","jsx","jsxs","AvatarPrimitive","cn"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"Avatar.cjs","sources":["../../../../src/components/Avatar/Avatar.tsx"],"sourcesContent":["import * as AvatarPrimitive from \"@radix-ui/react-avatar\";\nimport * as React from \"react\";\nimport { cn } from \"../../utils/cn\";\n\n/** Allowed pixel sizes for the avatar. */\nexport type AvatarSize = 16 | 24 | 32 | 40 | 48 | 64 | 88 | 148;\n\nconst AvatarContext = React.createContext<{ size: AvatarSize; NSFWShow: boolean }>({\n size: 40,\n NSFWShow: false,\n});\n\nconst STATUS_POSITIONS: Record<AvatarSize, { top: number; right: number }> = {\n 16: { top: -4, right: -4 },\n 24: { top: -3, right: -3 },\n 32: { top: -2, right: -2 },\n 40: { top: -1, right: -1 },\n 48: { top: 0, right: 0 },\n 64: { top: 2, right: 2 },\n 88: { top: 6, right: 6 },\n 148: { top: 15, right: 15 },\n};\n\n/** Shared avatar styling props. */\ninterface AvatarStyleProps {\n /** Pixel size of the avatar. @default 40 */\n size?: AvatarSize;\n /** Whether to show the online-status indicator dot. @default false */\n onlineIndicator?: boolean;\n /** Whether to show the platinum gradient border ring. @default false */\n platinumShow?: boolean;\n /** Whether to apply the NSFW blur filter over the image. @default false */\n NSFWShow?: boolean;\n}\n\n/** Props for the low-level {@link AvatarRoot} compound component. */\nexport interface AvatarRootProps\n extends React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Root>,\n AvatarStyleProps {}\n\n/**\n * Low-level avatar root for custom compositions. Provides size context to\n * child `AvatarImage` and `AvatarFallback` components.\n *\n * Prefer the higher-level {@link Avatar} component for most use cases.\n */\nconst AvatarRoot = React.forwardRef<\n React.ComponentRef<typeof AvatarPrimitive.Root>,\n AvatarRootProps\n>(\n (\n {\n className,\n size = 40,\n onlineIndicator = false,\n platinumShow = false,\n NSFWShow = false,\n children,\n ...props\n },\n ref,\n ) => {\n const statusPosition = STATUS_POSITIONS[size];\n\n return (\n <AvatarContext.Provider value={{ size, NSFWShow }}>\n <div className=\"relative inline-flex\">\n <AvatarPrimitive.Root\n ref={ref}\n data-testid=\"avatar\"\n className={cn(\n \"relative inline-flex shrink-0 items-center justify-center overflow-hidden rounded-full bg-neutral-100\",\n size === 16 && \"size-4 text-[10px]\",\n size === 24 && \"size-6 text-xs\",\n size === 32 && \"size-8 text-xs\",\n size === 40 && \"size-10 text-sm\",\n size === 48 && \"size-12 text-base\",\n size === 64 && \"size-16 text-xl\",\n size === 88 && \"size-[88px] text-2xl\",\n size === 148 && \"size-[148px] text-4xl\",\n className,\n )}\n {...props}\n >\n {children}\n </AvatarPrimitive.Root>\n {platinumShow && (\n <div\n className=\"pointer-events-none absolute inset-0 rounded-full\"\n style={{\n background: `linear-gradient(143deg, #504F54 0%, #B1B1B1 20.3154%, #13181C 37.3727%, #C6C6C8 58.8154%, #FFFFFF 69.3154%, #0C0F14 81.3154%, #696A6E 100%)`,\n WebkitMask: \"radial-gradient(circle closest-side, transparent 96%, black 96%)\",\n mask: \"radial-gradient(circle closest-side, transparent 96%, black 96%)\",\n }}\n aria-hidden=\"true\"\n />\n )}\n {onlineIndicator && (\n <span\n className=\"absolute size-3 rounded-full border-2 border-background-150 bg-brand-green-500\"\n style={{\n top: `${statusPosition.top}px`,\n right: `${statusPosition.right}px`,\n }}\n aria-hidden=\"true\"\n />\n )}\n </div>\n </AvatarContext.Provider>\n );\n },\n);\n\nAvatarRoot.displayName = \"AvatarRoot\";\n\n/** Props for the {@link AvatarImage} compound component. */\nexport interface AvatarImageProps\n extends React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Image> {}\n\n/** Renders the avatar image. Automatically applies the NSFW blur when enabled on the parent `AvatarRoot`. */\nconst AvatarImage = React.forwardRef<\n React.ComponentRef<typeof AvatarPrimitive.Image>,\n AvatarImageProps\n>(({ className, ...props }, ref) => {\n const { NSFWShow } = React.useContext(AvatarContext);\n return (\n <AvatarPrimitive.Image\n ref={ref}\n className={cn(\"size-full bg-neutral-200 object-cover\", NSFWShow && \"blur-md\", className)}\n {...props}\n />\n );\n});\n\nAvatarImage.displayName = \"AvatarImage\";\n\n/** Props for the {@link AvatarFallback} compound component. */\nexport interface AvatarFallbackProps\n extends React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Fallback> {}\n\n/** Renders fallback content (e.g. initials or an icon) when the avatar image has not loaded. */\nconst AvatarFallback = React.forwardRef<\n React.ComponentRef<typeof AvatarPrimitive.Fallback>,\n AvatarFallbackProps\n>(({ className, children, ...props }, ref) => (\n <AvatarPrimitive.Fallback\n ref={ref}\n className={cn(\n \"flex size-full items-center justify-center bg-neutral-200 font-semibold text-neutral-400 uppercase leading-none\",\n className,\n )}\n delayMs={0}\n {...props}\n >\n {children}\n </AvatarPrimitive.Fallback>\n));\n\nAvatarFallback.displayName = \"AvatarFallback\";\n\nexport interface AvatarProps\n extends React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Root>,\n AvatarStyleProps {\n /** URL of the avatar image. */\n src?: string;\n /** Alt text for the avatar image. @default \"Avatar\" */\n alt?: string;\n /** Fallback content rendered when the image has not loaded (e.g. initials or an icon). */\n fallback?: React.ReactNode;\n}\n\n/**\n * Displays a user avatar with optional online indicator, platinum border, and\n * NSFW blur. Pass `src` and `fallback` for the simple API, or compose your own\n * layout with `AvatarRoot`, `AvatarImage`, and `AvatarFallback` as children.\n *\n * @example\n * ```tsx\n * <Avatar src=\"/photo.jpg\" alt=\"Jane Doe\" fallback=\"JD\" size={48} />\n * ```\n */\nexport const Avatar = React.forwardRef<\n React.ComponentRef<typeof AvatarPrimitive.Root>,\n AvatarProps\n>(\n (\n {\n className,\n size = 40,\n src,\n alt,\n fallback,\n onlineIndicator = false,\n platinumShow = false,\n NSFWShow = false,\n children,\n ...props\n },\n ref,\n ) => {\n const rootProps = {\n ref,\n size,\n onlineIndicator,\n platinumShow,\n NSFWShow,\n className,\n ...props,\n };\n\n if (children) {\n return <AvatarRoot {...rootProps}>{children}</AvatarRoot>;\n }\n\n return (\n <AvatarRoot {...rootProps}>\n {src && <AvatarImage src={src} alt={alt ?? \"Avatar\"} />}\n <AvatarFallback>{fallback}</AvatarFallback>\n </AvatarRoot>\n );\n },\n);\n\nAvatar.displayName = \"Avatar\";\n\nexport { AvatarRoot, AvatarImage, AvatarFallback };\n"],"names":["React","jsx","jsxs","AvatarPrimitive","cn"],"mappings":"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"}
|
|
@@ -42,11 +42,11 @@ const iconSizeVariants = {
|
|
|
42
42
|
72: "[&>svg]:size-8"
|
|
43
43
|
};
|
|
44
44
|
const sizeVariants = {
|
|
45
|
-
24: "p-1",
|
|
46
|
-
32: "p-1.5",
|
|
47
|
-
40: "p-[10px]",
|
|
48
|
-
52: "p-2",
|
|
49
|
-
72: "p-4"
|
|
45
|
+
24: "size-6 p-1",
|
|
46
|
+
32: "size-8 p-1.5",
|
|
47
|
+
40: "size-10 p-[10px]",
|
|
48
|
+
52: "size-[52px] p-2",
|
|
49
|
+
72: "size-[72px] p-4"
|
|
50
50
|
};
|
|
51
51
|
const countSizeMap = {
|
|
52
52
|
24: "16",
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"IconButton.cjs","sources":["../../../../src/components/IconButton/IconButton.tsx"],"sourcesContent":["import * as React from \"react\";\nimport { cn } from \"../../utils/cn\";\nimport { Count, type CountSize } from \"../Count/Count\";\n\nconst iconButtonVariants = {\n primary:\n \"bg-neutral-400 text-body-300 hover:bg-hover-100 not-disabled:active:bg-hover-100 disabled:opacity-50 focus-visible:shadow-focus-ring\",\n secondary:\n \"bg-neutral-100 text-neutral-400 hover:bg-neutral-200 not-disabled:active:bg-neutral-200 disabled:opacity-50 focus-visible:shadow-focus-ring\",\n tertiary:\n \"bg-transparent text-neutral-400 hover:bg-hover-300 not-disabled:active:bg-brand-green-50 disabled:opacity-50 focus-visible:shadow-focus-ring\",\n brand:\n \"bg-body-black-solid-constant text-brand-green-500 hover:bg-brand-green-500 hover:text-body-black-solid-constant not-disabled:active:bg-brand-green-500 not-disabled:active:text-body-black-solid-constant disabled:opacity-50 focus-visible:shadow-focus-ring\",\n contrast:\n \"bg-transparent text-body-white-solid-constant hover:bg-hover-300 not-disabled:active:bg-hover-300 disabled:opacity-50 focus-visible:shadow-focus-ring\",\n messaging:\n \"bg-body-black-solid-constant text-brand-green-500 hover:bg-brand-green-500 hover:text-body-black-solid-constant not-disabled:active:bg-brand-green-500 not-disabled:active:text-body-black-solid-constant disabled:opacity-50 focus-visible:shadow-focus-ring\",\n navTray:\n \"bg-transparent text-neutral-400 hover:bg-hover-300 not-disabled:active:bg-brand-green-50 disabled:opacity-50 focus-visible:shadow-focus-ring\",\n tertiaryDestructive:\n \"bg-transparent text-error-500 hover:bg-hover-300 not-disabled:active:bg-hover-300 disabled:opacity-50 focus-visible:shadow-focus-ring\",\n stop: \"bg-neutral-400 text-body-300 hover:bg-brand-green-500 hover:text-body-black-solid-constant not-disabled:active:bg-brand-green-500 not-disabled:active:text-body-black-solid-constant disabled:opacity-50 focus-visible:shadow-focus-ring\",\n microphone:\n \"bg-neutral-400 text-body-300 hover:bg-brand-green-500 hover:text-body-black-solid-constant not-disabled:active:bg-brand-green-500 not-disabled:active:text-body-black-solid-constant disabled:opacity-50 focus-visible:shadow-focus-ring\",\n};\n\nconst iconSizeVariants = {\n 24: \"[&>svg]:size-4\",\n 32: \"[&>svg]:size-5\",\n 40: \"[&>svg]:size-6\",\n 52: \"[&>svg]:size-7\",\n 72: \"[&>svg]:size-8\",\n} as const;\n\nconst sizeVariants = {\n 24: \"p-1\",\n 32: \"p-1.5\",\n 40: \"p-[10px]\",\n 52: \"p-2\",\n 72: \"p-4\",\n} as const;\n\nconst countSizeMap: Record<string, CountSize> = {\n 24: \"16\",\n 32: \"24\",\n 40: \"32\",\n 52: \"32\",\n 72: \"32\",\n};\n\n/** Visual style variant of the icon button. */\nexport type IconButtonVariant =\n | \"primary\"\n | \"secondary\"\n | \"tertiary\"\n | \"brand\"\n | \"contrast\"\n | \"messaging\"\n | \"navTray\"\n | \"tertiaryDestructive\"\n | \"stop\"\n | \"microphone\";\n\n/** Icon button size in pixels. */\nexport type IconButtonSize = \"24\" | \"32\" | \"40\" | \"52\" | \"72\";\n\nexport interface IconButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {\n /** Visual style variant of the icon button. @default \"primary\" */\n variant?: IconButtonVariant;\n /** Size of the button in pixels. @default \"40\" */\n size?: IconButtonSize;\n /** Icon element to render inside the button. */\n icon: React.ReactNode;\n /** When provided, displays a {@link Count} badge at the top-right corner (tertiary & navTray variants only). */\n counterValue?: number;\n}\n\n/**\n * A circular button containing only an icon. Use when an action can be\n * represented by an icon alone (e.g. close, send, mic). Pair with an\n * `aria-label` for accessibility.\n *\n * @example\n * ```tsx\n * <IconButton icon={<CloseIcon />} aria-label=\"Close\" variant=\"tertiary\" />\n * ```\n */\nexport const IconButton = React.forwardRef<HTMLButtonElement, IconButtonProps>(\n (\n { className, variant = \"primary\", size = \"40\", icon, counterValue, disabled = false, ...props },\n ref,\n ) => {\n if (process.env.NODE_ENV !== \"production\") {\n if (!props[\"aria-label\"] && !props[\"aria-labelledby\"] && !props.title) {\n console.warn(\n \"IconButton: No accessible name provided. Add an `aria-label`, `aria-labelledby`, or `title` prop so screen readers can announce this button.\",\n );\n }\n }\n\n return (\n <button\n ref={ref}\n type=\"button\"\n data-testid=\"icon-button\"\n disabled={disabled}\n className={cn(\n // Base styles\n \"relative inline-flex shrink-0 items-center justify-center focus-visible:outline-none\",\n \"cursor-pointer rounded-full transition-all duration-150 ease-in-out disabled:cursor-default\",\n // Size variants\n sizeVariants[size],\n // Variant styles\n iconButtonVariants[variant],\n // Manual CSS overrides\n className,\n )}\n {...props}\n >\n <span\n className={cn(\"flex shrink-0 items-center justify-center\", iconSizeVariants[size])}\n aria-hidden=\"true\"\n >\n {icon}\n </span>\n\n {counterValue !== undefined && (\n <Count\n value={counterValue}\n size={countSizeMap[size]}\n className=\"absolute -top-0.5 -right-0.5\"\n />\n )}\n </button>\n );\n },\n);\n\nIconButton.displayName = \"IconButton\";\n"],"names":["React","jsxs","cn","jsx","Count"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;AAIA,MAAM,qBAAqB;AAAA,EACzB,SACE;AAAA,EACF,WACE;AAAA,EACF,UACE;AAAA,EACF,OACE;AAAA,EACF,UACE;AAAA,EACF,WACE;AAAA,EACF,SACE;AAAA,EACF,qBACE;AAAA,EACF,MAAM;AAAA,EACN,YACE;AACJ;AAEA,MAAM,mBAAmB;AAAA,EACvB,IAAI;AAAA,EACJ,IAAI;AAAA,EACJ,IAAI;AAAA,EACJ,IAAI;AAAA,EACJ,IAAI;AACN;AAEA,MAAM,eAAe;AAAA,EACnB,IAAI;AAAA,EACJ,IAAI;AAAA,EACJ,IAAI;AAAA,EACJ,IAAI;AAAA,EACJ,IAAI;AACN;AAEA,MAAM,eAA0C;AAAA,EAC9C,IAAI;AAAA,EACJ,IAAI;AAAA,EACJ,IAAI;AAAA,EACJ,IAAI;AAAA,EACJ,IAAI;AACN;AAuCO,MAAM,aAAaA,iBAAM;AAAA,EAC9B,CACE,EAAE,WAAW,UAAU,WAAW,OAAO,MAAM,MAAM,cAAc,WAAW,OAAO,GAAG,MAAA,GACxF,QACG;AACH,QAAI,QAAQ,IAAI,aAAa,cAAc;AACzC,UAAI,CAAC,MAAM,YAAY,KAAK,CAAC,MAAM,iBAAiB,KAAK,CAAC,MAAM,OAAO;AACrE,gBAAQ;AAAA,UACN;AAAA,QAAA;AAAA,MAEJ;AAAA,IACF;AAEA,WACEC,2BAAAA;AAAAA,MAAC;AAAA,MAAA;AAAA,QACC;AAAA,QACA,MAAK;AAAA,QACL,eAAY;AAAA,QACZ;AAAA,QACA,WAAWC,GAAAA;AAAAA;AAAAA,UAET;AAAA,UACA;AAAA;AAAA,UAEA,aAAa,IAAI;AAAA;AAAA,UAEjB,mBAAmB,OAAO;AAAA;AAAA,UAE1B;AAAA,QAAA;AAAA,QAED,GAAG;AAAA,QAEJ,UAAA;AAAA,UAAAC,2BAAAA;AAAAA,YAAC;AAAA,YAAA;AAAA,cACC,WAAWD,GAAAA,GAAG,6CAA6C,iBAAiB,IAAI,CAAC;AAAA,cACjF,eAAY;AAAA,cAEX,UAAA;AAAA,YAAA;AAAA,UAAA;AAAA,UAGF,iBAAiB,UAChBC,2BAAAA;AAAAA,YAACC,MAAAA;AAAAA,YAAA;AAAA,cACC,OAAO;AAAA,cACP,MAAM,aAAa,IAAI;AAAA,cACvB,WAAU;AAAA,YAAA;AAAA,UAAA;AAAA,QACZ;AAAA,MAAA;AAAA,IAAA;AAAA,EAIR;AACF;AAEA,WAAW,cAAc;;"}
|
|
1
|
+
{"version":3,"file":"IconButton.cjs","sources":["../../../../src/components/IconButton/IconButton.tsx"],"sourcesContent":["import * as React from \"react\";\nimport { cn } from \"../../utils/cn\";\nimport { Count, type CountSize } from \"../Count/Count\";\n\nconst iconButtonVariants = {\n primary:\n \"bg-neutral-400 text-body-300 hover:bg-hover-100 not-disabled:active:bg-hover-100 disabled:opacity-50 focus-visible:shadow-focus-ring\",\n secondary:\n \"bg-neutral-100 text-neutral-400 hover:bg-neutral-200 not-disabled:active:bg-neutral-200 disabled:opacity-50 focus-visible:shadow-focus-ring\",\n tertiary:\n \"bg-transparent text-neutral-400 hover:bg-hover-300 not-disabled:active:bg-brand-green-50 disabled:opacity-50 focus-visible:shadow-focus-ring\",\n brand:\n \"bg-body-black-solid-constant text-brand-green-500 hover:bg-brand-green-500 hover:text-body-black-solid-constant not-disabled:active:bg-brand-green-500 not-disabled:active:text-body-black-solid-constant disabled:opacity-50 focus-visible:shadow-focus-ring\",\n contrast:\n \"bg-transparent text-body-white-solid-constant hover:bg-hover-300 not-disabled:active:bg-hover-300 disabled:opacity-50 focus-visible:shadow-focus-ring\",\n messaging:\n \"bg-body-black-solid-constant text-brand-green-500 hover:bg-brand-green-500 hover:text-body-black-solid-constant not-disabled:active:bg-brand-green-500 not-disabled:active:text-body-black-solid-constant disabled:opacity-50 focus-visible:shadow-focus-ring\",\n navTray:\n \"bg-transparent text-neutral-400 hover:bg-hover-300 not-disabled:active:bg-brand-green-50 disabled:opacity-50 focus-visible:shadow-focus-ring\",\n tertiaryDestructive:\n \"bg-transparent text-error-500 hover:bg-hover-300 not-disabled:active:bg-hover-300 disabled:opacity-50 focus-visible:shadow-focus-ring\",\n stop: \"bg-neutral-400 text-body-300 hover:bg-brand-green-500 hover:text-body-black-solid-constant not-disabled:active:bg-brand-green-500 not-disabled:active:text-body-black-solid-constant disabled:opacity-50 focus-visible:shadow-focus-ring\",\n microphone:\n \"bg-neutral-400 text-body-300 hover:bg-brand-green-500 hover:text-body-black-solid-constant not-disabled:active:bg-brand-green-500 not-disabled:active:text-body-black-solid-constant disabled:opacity-50 focus-visible:shadow-focus-ring\",\n};\n\nconst iconSizeVariants = {\n 24: \"[&>svg]:size-4\",\n 32: \"[&>svg]:size-5\",\n 40: \"[&>svg]:size-6\",\n 52: \"[&>svg]:size-7\",\n 72: \"[&>svg]:size-8\",\n} as const;\n\nconst sizeVariants = {\n 24: \"size-6 p-1\",\n 32: \"size-8 p-1.5\",\n 40: \"size-10 p-[10px]\",\n 52: \"size-[52px] p-2\",\n 72: \"size-[72px] p-4\",\n} as const;\n\nconst countSizeMap: Record<string, CountSize> = {\n 24: \"16\",\n 32: \"24\",\n 40: \"32\",\n 52: \"32\",\n 72: \"32\",\n};\n\n/** Visual style variant of the icon button. */\nexport type IconButtonVariant =\n | \"primary\"\n | \"secondary\"\n | \"tertiary\"\n | \"brand\"\n | \"contrast\"\n | \"messaging\"\n | \"navTray\"\n | \"tertiaryDestructive\"\n | \"stop\"\n | \"microphone\";\n\n/** Icon button size in pixels. */\nexport type IconButtonSize = \"24\" | \"32\" | \"40\" | \"52\" | \"72\";\n\nexport interface IconButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {\n /** Visual style variant of the icon button. @default \"primary\" */\n variant?: IconButtonVariant;\n /** Size of the button in pixels. @default \"40\" */\n size?: IconButtonSize;\n /** Icon element to render inside the button. */\n icon: React.ReactNode;\n /** When provided, displays a {@link Count} badge at the top-right corner (tertiary & navTray variants only). */\n counterValue?: number;\n}\n\n/**\n * A circular button containing only an icon. Use when an action can be\n * represented by an icon alone (e.g. close, send, mic). Pair with an\n * `aria-label` for accessibility.\n *\n * @example\n * ```tsx\n * <IconButton icon={<CloseIcon />} aria-label=\"Close\" variant=\"tertiary\" />\n * ```\n */\nexport const IconButton = React.forwardRef<HTMLButtonElement, IconButtonProps>(\n (\n { className, variant = \"primary\", size = \"40\", icon, counterValue, disabled = false, ...props },\n ref,\n ) => {\n if (process.env.NODE_ENV !== \"production\") {\n if (!props[\"aria-label\"] && !props[\"aria-labelledby\"] && !props.title) {\n console.warn(\n \"IconButton: No accessible name provided. Add an `aria-label`, `aria-labelledby`, or `title` prop so screen readers can announce this button.\",\n );\n }\n }\n\n return (\n <button\n ref={ref}\n type=\"button\"\n data-testid=\"icon-button\"\n disabled={disabled}\n className={cn(\n // Base styles\n \"relative inline-flex shrink-0 items-center justify-center focus-visible:outline-none\",\n \"cursor-pointer rounded-full transition-all duration-150 ease-in-out disabled:cursor-default\",\n // Size variants\n sizeVariants[size],\n // Variant styles\n iconButtonVariants[variant],\n // Manual CSS overrides\n className,\n )}\n {...props}\n >\n <span\n className={cn(\"flex shrink-0 items-center justify-center\", iconSizeVariants[size])}\n aria-hidden=\"true\"\n >\n {icon}\n </span>\n\n {counterValue !== undefined && (\n <Count\n value={counterValue}\n size={countSizeMap[size]}\n className=\"absolute -top-0.5 -right-0.5\"\n />\n )}\n </button>\n );\n },\n);\n\nIconButton.displayName = \"IconButton\";\n"],"names":["React","jsxs","cn","jsx","Count"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;AAIA,MAAM,qBAAqB;AAAA,EACzB,SACE;AAAA,EACF,WACE;AAAA,EACF,UACE;AAAA,EACF,OACE;AAAA,EACF,UACE;AAAA,EACF,WACE;AAAA,EACF,SACE;AAAA,EACF,qBACE;AAAA,EACF,MAAM;AAAA,EACN,YACE;AACJ;AAEA,MAAM,mBAAmB;AAAA,EACvB,IAAI;AAAA,EACJ,IAAI;AAAA,EACJ,IAAI;AAAA,EACJ,IAAI;AAAA,EACJ,IAAI;AACN;AAEA,MAAM,eAAe;AAAA,EACnB,IAAI;AAAA,EACJ,IAAI;AAAA,EACJ,IAAI;AAAA,EACJ,IAAI;AAAA,EACJ,IAAI;AACN;AAEA,MAAM,eAA0C;AAAA,EAC9C,IAAI;AAAA,EACJ,IAAI;AAAA,EACJ,IAAI;AAAA,EACJ,IAAI;AAAA,EACJ,IAAI;AACN;AAuCO,MAAM,aAAaA,iBAAM;AAAA,EAC9B,CACE,EAAE,WAAW,UAAU,WAAW,OAAO,MAAM,MAAM,cAAc,WAAW,OAAO,GAAG,MAAA,GACxF,QACG;AACH,QAAI,QAAQ,IAAI,aAAa,cAAc;AACzC,UAAI,CAAC,MAAM,YAAY,KAAK,CAAC,MAAM,iBAAiB,KAAK,CAAC,MAAM,OAAO;AACrE,gBAAQ;AAAA,UACN;AAAA,QAAA;AAAA,MAEJ;AAAA,IACF;AAEA,WACEC,2BAAAA;AAAAA,MAAC;AAAA,MAAA;AAAA,QACC;AAAA,QACA,MAAK;AAAA,QACL,eAAY;AAAA,QACZ;AAAA,QACA,WAAWC,GAAAA;AAAAA;AAAAA,UAET;AAAA,UACA;AAAA;AAAA,UAEA,aAAa,IAAI;AAAA;AAAA,UAEjB,mBAAmB,OAAO;AAAA;AAAA,UAE1B;AAAA,QAAA;AAAA,QAED,GAAG;AAAA,QAEJ,UAAA;AAAA,UAAAC,2BAAAA;AAAAA,YAAC;AAAA,YAAA;AAAA,cACC,WAAWD,GAAAA,GAAG,6CAA6C,iBAAiB,IAAI,CAAC;AAAA,cACjF,eAAY;AAAA,cAEX,UAAA;AAAA,YAAA;AAAA,UAAA;AAAA,UAGF,iBAAiB,UAChBC,2BAAAA;AAAAA,YAACC,MAAAA;AAAAA,YAAA;AAAA,cACC,OAAO;AAAA,cACP,MAAM,aAAa,IAAI;AAAA,cACvB,WAAU;AAAA,YAAA;AAAA,UAAA;AAAA,QACZ;AAAA,MAAA;AAAA,IAAA;AAAA,EAIR;AACF;AAEA,WAAW,cAAc;;"}
|
|
@@ -58,9 +58,10 @@ function getContainerClassName(size, error, disabled) {
|
|
|
58
58
|
disabled && "opacity-50"
|
|
59
59
|
);
|
|
60
60
|
}
|
|
61
|
-
function getTextareaClassName(size, hasClearButton, hasMinRows) {
|
|
61
|
+
function getTextareaClassName(size, hasClearButton, hasMinRows, resizable) {
|
|
62
62
|
return cn.cn(
|
|
63
|
-
"w-full
|
|
63
|
+
"w-full rounded-xl bg-transparent text-body-100 no-underline placeholder:text-body-200 placeholder:opacity-40 focus:outline-none disabled:cursor-not-allowed",
|
|
64
|
+
resizable ? "resize-y" : "resize-none",
|
|
64
65
|
!hasMinRows && "min-h-[80px]",
|
|
65
66
|
TEXTAREA_SIZE_CLASSES[size],
|
|
66
67
|
PADDING_HORIZONTAL[size],
|
|
@@ -144,6 +145,7 @@ const TextArea = React__namespace.forwardRef(
|
|
|
144
145
|
onChange,
|
|
145
146
|
minRows,
|
|
146
147
|
maxRows,
|
|
148
|
+
resizable = true,
|
|
147
149
|
...props
|
|
148
150
|
}, ref) => {
|
|
149
151
|
const generatedId = React__namespace.useId();
|
|
@@ -190,7 +192,7 @@ const TextArea = React__namespace.forwardRef(
|
|
|
190
192
|
disabled,
|
|
191
193
|
"aria-describedby": ariaDescribedBy,
|
|
192
194
|
"aria-invalid": error || void 0,
|
|
193
|
-
className: getTextareaClassName(size, showClear, !!minRows),
|
|
195
|
+
className: getTextareaClassName(size, showClear, !!minRows, resizable),
|
|
194
196
|
value: displayValue,
|
|
195
197
|
defaultValue: resolvedDefaultValue,
|
|
196
198
|
onChange: handleChange,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"TextArea.cjs","sources":["../../../../src/components/TextArea/TextArea.tsx"],"sourcesContent":["import * as React from \"react\";\nimport { cn } from \"../../utils/cn\";\nimport { IconButton } from \"../IconButton/IconButton\";\nimport { CheckOutlineIcon } from \"../Icons/CheckOutlineIcon\";\nimport { CloseIcon } from \"../Icons/CloseIcon\";\n\n/** Text area height in pixels. */\nexport type TextAreaSize = \"48\" | \"40\" | \"32\";\n\nexport interface TextAreaProps\n extends Omit<React.TextareaHTMLAttributes<HTMLTextAreaElement>, \"size\"> {\n /** Label text displayed above the textarea. Also used as the accessible name. */\n label?: string;\n /** Helper text displayed below the textarea. Replaced by `errorMessage` when `error` is `true`. */\n helperText?: string;\n /** Minimum height of the text area in pixels. @default \"48\" */\n size?: TextAreaSize;\n /** Whether the text area is in an error state. @default false */\n error?: boolean;\n /** Error message displayed below the textarea. Shown instead of `helperText` when `error` is `true`. */\n errorMessage?: string;\n /** Whether the text area is validated. @default false */\n validated?: boolean;\n /** Whether the text area stretches to fill its container width. @default false */\n fullWidth?: boolean;\n /** Whether to show a clear button when text is present. @default false */\n showClearButton?: boolean;\n /** Callback fired when the clear button is clicked. Note: `onChange` is also called with an empty value when clearing. */\n onClear?: () => void;\n /** Minimum number of rows (lines) for the textarea. */\n minRows?: number;\n /** Maximum number of rows (lines) for the textarea. */\n maxRows?: number;\n}\n\nconst CONTAINER_MIN_HEIGHT: Record<TextAreaSize, string> = {\n \"48\": \"min-h-12\",\n \"40\": \"min-h-10\",\n \"32\": \"min-h-8\",\n};\n\nconst TEXTAREA_SIZE_CLASSES: Record<TextAreaSize, string> = {\n \"48\": \"py-3 typography-body-1-regular\",\n \"40\": \"py-2 typography-body-1-regular\",\n \"32\": \"py-2 typography-body-2-regular\",\n};\n\nconst PADDING_HORIZONTAL: Record<TextAreaSize, string> = {\n \"48\": \"px-4\",\n \"40\": \"px-4\",\n \"32\": \"px-3\",\n};\n\nconst PADDING_RIGHT_WITH_CLEAR: Record<TextAreaSize, string> = {\n \"48\": \"pr-11\",\n \"40\": \"pr-11\",\n \"32\": \"pr-10\",\n};\n\nconst CLEAR_BUTTON_RIGHT: Record<TextAreaSize, string> = {\n \"48\": \"right-4 top-3\",\n \"40\": \"right-4 top-2\",\n \"32\": \"right-3 top-2\",\n};\n\nfunction getContainerClassName(size: TextAreaSize, error: boolean, disabled?: boolean) {\n return cn(\n \"relative rounded-xl border bg-neutral-100 has-focus-visible:outline-none motion-safe:transition-colors\",\n error ? \"border-error-500\" : \"border-transparent\",\n !disabled && !error && \"hover:border-neutral-400\",\n CONTAINER_MIN_HEIGHT[size],\n disabled && \"opacity-50\",\n );\n}\n\nfunction getTextareaClassName(size: TextAreaSize, hasClearButton: boolean, hasMinRows: boolean) {\n return cn(\n \"w-full resize-y rounded-xl bg-transparent text-body-100 no-underline placeholder:text-body-200 placeholder:opacity-40 focus:outline-none disabled:cursor-not-allowed\",\n !hasMinRows && \"min-h-[80px]\",\n TEXTAREA_SIZE_CLASSES[size],\n PADDING_HORIZONTAL[size],\n hasClearButton ? PADDING_RIGHT_WITH_CLEAR[size] : \"\",\n );\n}\n\nfunction TextAreaHelperText({\n id,\n error,\n children,\n}: {\n id: string;\n error: boolean;\n children: React.ReactNode;\n}) {\n return (\n <p\n id={id}\n className={cn(\n \"typography-caption-regular px-2 pt-1 pb-0.5\",\n error ? \"text-error-500\" : \"text-body-200\",\n )}\n >\n {children}\n </p>\n );\n}\n\nfunction warnMissingAccessibleName(label?: string, ariaLabel?: string, ariaLabelledBy?: string) {\n if (process.env.NODE_ENV !== \"production\") {\n if (!label && !ariaLabel && !ariaLabelledBy) {\n console.warn(\n \"TextArea: no accessible name provided. Pass a `label`, `aria-label`, or `aria-labelledby` prop.\",\n );\n }\n }\n}\n\nfunction calculateMaxHeight(size: TextAreaSize, maxRows?: number): string | undefined {\n if (!maxRows) return undefined;\n\n // Line height is 24px for body-1 (sizes 48 and 40) and 20px for body-2 (size 32)\n const lineHeight = size === \"32\" ? 20 : 24;\n // py-2 = 8px, py-3 = 12px\n const verticalPadding = size === \"32\" ? 8 : size === \"40\" ? 8 : 12;\n\n return `${lineHeight * maxRows + verticalPadding * 2}px`;\n}\n\nfunction useTextAreaValue(\n value: React.TextareaHTMLAttributes<HTMLTextAreaElement>[\"value\"],\n defaultValue: React.TextareaHTMLAttributes<HTMLTextAreaElement>[\"defaultValue\"],\n showClearButton: boolean,\n onChange?: React.ChangeEventHandler<HTMLTextAreaElement>,\n onClear?: () => void,\n textareaRef?: React.RefObject<HTMLTextAreaElement | null>,\n) {\n const [internalValue, setInternalValue] = React.useState(defaultValue ?? \"\");\n const resolvedValue = value !== undefined ? value : internalValue;\n\n const handleChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {\n setInternalValue(e.target.value);\n onChange?.(e);\n };\n\n const handleClear = () => {\n setInternalValue(\"\");\n\n if (onChange && textareaRef?.current) {\n const syntheticEvent = {\n target: { ...textareaRef.current, value: \"\" },\n currentTarget: { ...textareaRef.current, value: \"\" },\n } as React.ChangeEvent<HTMLTextAreaElement>;\n onChange(syntheticEvent);\n }\n\n onClear?.();\n };\n\n return {\n resolvedValue,\n displayValue: showClearButton ? resolvedValue : value,\n resolvedDefaultValue: showClearButton ? undefined : defaultValue,\n handleChange,\n handleClear,\n };\n}\n\n/**\n * A multi-line text input with optional label, helper/error text, and clear button.\n *\n * Provide at least one of `label`, `aria-label`, or `aria-labelledby` for\n * accessibility — a console warning is emitted in development if none are set.\n *\n * @example\n * ```tsx\n * <TextArea\n * label=\"Description\"\n * placeholder=\"Enter your description...\"\n * showClearButton\n * error={!!descError}\n * errorMessage={descError}\n * />\n * ```\n */\nexport const TextArea = React.forwardRef<HTMLTextAreaElement, TextAreaProps>(\n (\n {\n label,\n helperText,\n size = \"48\",\n error = false,\n errorMessage,\n validated = false,\n className,\n id,\n disabled,\n fullWidth = false,\n showClearButton = false,\n onClear,\n value,\n defaultValue,\n onChange,\n minRows,\n maxRows,\n ...props\n },\n ref,\n ) => {\n const generatedId = React.useId();\n const inputId = id || generatedId;\n const helperTextId = `${inputId}-helper`;\n const bottomText = error && errorMessage ? errorMessage : helperText;\n const maxHeight = calculateMaxHeight(size, maxRows);\n\n const internalRef = React.useRef<HTMLTextAreaElement>(null);\n\n const { resolvedValue, displayValue, resolvedDefaultValue, handleChange, handleClear } =\n useTextAreaValue(value, defaultValue, showClearButton, onChange, onClear, internalRef);\n\n const mergedRef = (node: HTMLTextAreaElement | null) => {\n internalRef.current = node;\n if (typeof ref === \"function\") {\n ref(node);\n } else if (ref) {\n (ref as React.MutableRefObject<HTMLTextAreaElement | null>).current = node;\n }\n };\n\n const showClear = showClearButton && resolvedValue !== \"\" && !disabled;\n const showValidated = validated && !showClear;\n const ariaDescribedBy = bottomText ? helperTextId : undefined;\n const textareaStyle = maxHeight ? { maxHeight } : undefined;\n\n warnMissingAccessibleName(label, props[\"aria-label\"], props[\"aria-labelledby\"]);\n\n return (\n <div\n className={cn(\"flex flex-col\", fullWidth && \"w-full\", className)}\n data-disabled={disabled ? \"\" : undefined}\n data-error={error ? \"\" : undefined}\n >\n {label && (\n <label\n htmlFor={inputId}\n className=\"typography-caption-semibold px-1 pt-1 pb-2 text-body-100\"\n >\n {label}\n </label>\n )}\n\n <div className={getContainerClassName(size, error, disabled)}>\n <textarea\n ref={mergedRef}\n id={inputId}\n disabled={disabled}\n aria-describedby={ariaDescribedBy}\n aria-invalid={error || undefined}\n className={getTextareaClassName(size, showClear, !!minRows)}\n value={displayValue}\n defaultValue={resolvedDefaultValue}\n onChange={handleChange}\n rows={minRows}\n style={textareaStyle}\n {...props}\n />\n\n {showClear && (\n <IconButton\n variant=\"tertiary\"\n size=\"24\"\n icon={<CloseIcon />}\n aria-label=\"Clear text\"\n onClick={handleClear}\n className={cn(\n \"absolute flex size-5 items-center justify-center self-end\",\n CLEAR_BUTTON_RIGHT[size],\n )}\n />\n )}\n {showValidated && (\n <div\n className={cn(\n \"pointer-events-none absolute flex size-5 items-center justify-center\",\n CLEAR_BUTTON_RIGHT[size],\n )}\n >\n <CheckOutlineIcon className=\"text-success-500\" />\n </div>\n )}\n </div>\n\n {bottomText && (\n <TextAreaHelperText id={helperTextId} error={error}>\n {bottomText}\n </TextAreaHelperText>\n )}\n </div>\n );\n },\n);\n\nTextArea.displayName = \"TextArea\";\n"],"names":["cn","jsx","React","jsxs","IconButton","CloseIcon","CheckOutlineIcon"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"TextArea.cjs","sources":["../../../../src/components/TextArea/TextArea.tsx"],"sourcesContent":["import * as React from \"react\";\nimport { cn } from \"../../utils/cn\";\nimport { IconButton } from \"../IconButton/IconButton\";\nimport { CheckOutlineIcon } from \"../Icons/CheckOutlineIcon\";\nimport { CloseIcon } from \"../Icons/CloseIcon\";\n\n/** Text area height in pixels. */\nexport type TextAreaSize = \"48\" | \"40\" | \"32\";\n\nexport interface TextAreaProps\n extends Omit<React.TextareaHTMLAttributes<HTMLTextAreaElement>, \"size\"> {\n /** Label text displayed above the textarea. Also used as the accessible name. */\n label?: string;\n /** Helper text displayed below the textarea. Replaced by `errorMessage` when `error` is `true`. */\n helperText?: string;\n /** Minimum height of the text area in pixels. @default \"48\" */\n size?: TextAreaSize;\n /** Whether the text area is in an error state. @default false */\n error?: boolean;\n /** Error message displayed below the textarea. Shown instead of `helperText` when `error` is `true`. */\n errorMessage?: string;\n /** Whether the text area is validated. @default false */\n validated?: boolean;\n /** Whether the text area stretches to fill its container width. @default false */\n fullWidth?: boolean;\n /** Whether to show a clear button when text is present. @default false */\n showClearButton?: boolean;\n /** Callback fired when the clear button is clicked. Note: `onChange` is also called with an empty value when clearing. */\n onClear?: () => void;\n /** Minimum number of rows (lines) for the textarea. */\n minRows?: number;\n /** Maximum number of rows (lines) for the textarea. */\n maxRows?: number;\n /** Whether the textarea can be resized by the user. @default true */\n resizable?: boolean;\n}\n\nconst CONTAINER_MIN_HEIGHT: Record<TextAreaSize, string> = {\n \"48\": \"min-h-12\",\n \"40\": \"min-h-10\",\n \"32\": \"min-h-8\",\n};\n\nconst TEXTAREA_SIZE_CLASSES: Record<TextAreaSize, string> = {\n \"48\": \"py-3 typography-body-1-regular\",\n \"40\": \"py-2 typography-body-1-regular\",\n \"32\": \"py-2 typography-body-2-regular\",\n};\n\nconst PADDING_HORIZONTAL: Record<TextAreaSize, string> = {\n \"48\": \"px-4\",\n \"40\": \"px-4\",\n \"32\": \"px-3\",\n};\n\nconst PADDING_RIGHT_WITH_CLEAR: Record<TextAreaSize, string> = {\n \"48\": \"pr-11\",\n \"40\": \"pr-11\",\n \"32\": \"pr-10\",\n};\n\nconst CLEAR_BUTTON_RIGHT: Record<TextAreaSize, string> = {\n \"48\": \"right-4 top-3\",\n \"40\": \"right-4 top-2\",\n \"32\": \"right-3 top-2\",\n};\n\nfunction getContainerClassName(size: TextAreaSize, error: boolean, disabled?: boolean) {\n return cn(\n \"relative rounded-xl border bg-neutral-100 has-focus-visible:outline-none motion-safe:transition-colors\",\n error ? \"border-error-500\" : \"border-transparent\",\n !disabled && !error && \"hover:border-neutral-400\",\n CONTAINER_MIN_HEIGHT[size],\n disabled && \"opacity-50\",\n );\n}\n\nfunction getTextareaClassName(\n size: TextAreaSize,\n hasClearButton: boolean,\n hasMinRows: boolean,\n resizable: boolean,\n) {\n return cn(\n \"w-full rounded-xl bg-transparent text-body-100 no-underline placeholder:text-body-200 placeholder:opacity-40 focus:outline-none disabled:cursor-not-allowed\",\n resizable ? \"resize-y\" : \"resize-none\",\n !hasMinRows && \"min-h-[80px]\",\n TEXTAREA_SIZE_CLASSES[size],\n PADDING_HORIZONTAL[size],\n hasClearButton ? PADDING_RIGHT_WITH_CLEAR[size] : \"\",\n );\n}\n\nfunction TextAreaHelperText({\n id,\n error,\n children,\n}: {\n id: string;\n error: boolean;\n children: React.ReactNode;\n}) {\n return (\n <p\n id={id}\n className={cn(\n \"typography-caption-regular px-2 pt-1 pb-0.5\",\n error ? \"text-error-500\" : \"text-body-200\",\n )}\n >\n {children}\n </p>\n );\n}\n\nfunction warnMissingAccessibleName(label?: string, ariaLabel?: string, ariaLabelledBy?: string) {\n if (process.env.NODE_ENV !== \"production\") {\n if (!label && !ariaLabel && !ariaLabelledBy) {\n console.warn(\n \"TextArea: no accessible name provided. Pass a `label`, `aria-label`, or `aria-labelledby` prop.\",\n );\n }\n }\n}\n\nfunction calculateMaxHeight(size: TextAreaSize, maxRows?: number): string | undefined {\n if (!maxRows) return undefined;\n\n // Line height is 24px for body-1 (sizes 48 and 40) and 20px for body-2 (size 32)\n const lineHeight = size === \"32\" ? 20 : 24;\n // py-2 = 8px, py-3 = 12px\n const verticalPadding = size === \"32\" ? 8 : size === \"40\" ? 8 : 12;\n\n return `${lineHeight * maxRows + verticalPadding * 2}px`;\n}\n\nfunction useTextAreaValue(\n value: React.TextareaHTMLAttributes<HTMLTextAreaElement>[\"value\"],\n defaultValue: React.TextareaHTMLAttributes<HTMLTextAreaElement>[\"defaultValue\"],\n showClearButton: boolean,\n onChange?: React.ChangeEventHandler<HTMLTextAreaElement>,\n onClear?: () => void,\n textareaRef?: React.RefObject<HTMLTextAreaElement | null>,\n) {\n const [internalValue, setInternalValue] = React.useState(defaultValue ?? \"\");\n const resolvedValue = value !== undefined ? value : internalValue;\n\n const handleChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {\n setInternalValue(e.target.value);\n onChange?.(e);\n };\n\n const handleClear = () => {\n setInternalValue(\"\");\n\n if (onChange && textareaRef?.current) {\n const syntheticEvent = {\n target: { ...textareaRef.current, value: \"\" },\n currentTarget: { ...textareaRef.current, value: \"\" },\n } as React.ChangeEvent<HTMLTextAreaElement>;\n onChange(syntheticEvent);\n }\n\n onClear?.();\n };\n\n return {\n resolvedValue,\n displayValue: showClearButton ? resolvedValue : value,\n resolvedDefaultValue: showClearButton ? undefined : defaultValue,\n handleChange,\n handleClear,\n };\n}\n\n/**\n * A multi-line text input with optional label, helper/error text, and clear button.\n *\n * Provide at least one of `label`, `aria-label`, or `aria-labelledby` for\n * accessibility — a console warning is emitted in development if none are set.\n *\n * @example\n * ```tsx\n * <TextArea\n * label=\"Description\"\n * placeholder=\"Enter your description...\"\n * showClearButton\n * error={!!descError}\n * errorMessage={descError}\n * />\n * ```\n */\nexport const TextArea = React.forwardRef<HTMLTextAreaElement, TextAreaProps>(\n (\n {\n label,\n helperText,\n size = \"48\",\n error = false,\n errorMessage,\n validated = false,\n className,\n id,\n disabled,\n fullWidth = false,\n showClearButton = false,\n onClear,\n value,\n defaultValue,\n onChange,\n minRows,\n maxRows,\n resizable = true,\n ...props\n },\n ref,\n ) => {\n const generatedId = React.useId();\n const inputId = id || generatedId;\n const helperTextId = `${inputId}-helper`;\n const bottomText = error && errorMessage ? errorMessage : helperText;\n const maxHeight = calculateMaxHeight(size, maxRows);\n\n const internalRef = React.useRef<HTMLTextAreaElement>(null);\n\n const { resolvedValue, displayValue, resolvedDefaultValue, handleChange, handleClear } =\n useTextAreaValue(value, defaultValue, showClearButton, onChange, onClear, internalRef);\n\n const mergedRef = (node: HTMLTextAreaElement | null) => {\n internalRef.current = node;\n if (typeof ref === \"function\") {\n ref(node);\n } else if (ref) {\n (ref as React.MutableRefObject<HTMLTextAreaElement | null>).current = node;\n }\n };\n\n const showClear = showClearButton && resolvedValue !== \"\" && !disabled;\n const showValidated = validated && !showClear;\n const ariaDescribedBy = bottomText ? helperTextId : undefined;\n const textareaStyle = maxHeight ? { maxHeight } : undefined;\n\n warnMissingAccessibleName(label, props[\"aria-label\"], props[\"aria-labelledby\"]);\n\n return (\n <div\n className={cn(\"flex flex-col\", fullWidth && \"w-full\", className)}\n data-disabled={disabled ? \"\" : undefined}\n data-error={error ? \"\" : undefined}\n >\n {label && (\n <label\n htmlFor={inputId}\n className=\"typography-caption-semibold px-1 pt-1 pb-2 text-body-100\"\n >\n {label}\n </label>\n )}\n\n <div className={getContainerClassName(size, error, disabled)}>\n <textarea\n ref={mergedRef}\n id={inputId}\n disabled={disabled}\n aria-describedby={ariaDescribedBy}\n aria-invalid={error || undefined}\n className={getTextareaClassName(size, showClear, !!minRows, resizable)}\n value={displayValue}\n defaultValue={resolvedDefaultValue}\n onChange={handleChange}\n rows={minRows}\n style={textareaStyle}\n {...props}\n />\n\n {showClear && (\n <IconButton\n variant=\"tertiary\"\n size=\"24\"\n icon={<CloseIcon />}\n aria-label=\"Clear text\"\n onClick={handleClear}\n className={cn(\n \"absolute flex size-5 items-center justify-center self-end\",\n CLEAR_BUTTON_RIGHT[size],\n )}\n />\n )}\n {showValidated && (\n <div\n className={cn(\n \"pointer-events-none absolute flex size-5 items-center justify-center\",\n CLEAR_BUTTON_RIGHT[size],\n )}\n >\n <CheckOutlineIcon className=\"text-success-500\" />\n </div>\n )}\n </div>\n\n {bottomText && (\n <TextAreaHelperText id={helperTextId} error={error}>\n {bottomText}\n </TextAreaHelperText>\n )}\n </div>\n );\n },\n);\n\nTextArea.displayName = \"TextArea\";\n"],"names":["cn","jsx","React","jsxs","IconButton","CloseIcon","CheckOutlineIcon"],"mappings":"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"}
|
|
@@ -36,14 +36,14 @@ const AvatarRoot = React.forwardRef(
|
|
|
36
36
|
"data-testid": "avatar",
|
|
37
37
|
className: cn(
|
|
38
38
|
"relative inline-flex shrink-0 items-center justify-center overflow-hidden rounded-full bg-neutral-100",
|
|
39
|
-
size === 16 && "
|
|
40
|
-
size === 24 && "
|
|
41
|
-
size === 32 && "
|
|
42
|
-
size === 40 && "
|
|
43
|
-
size === 48 && "
|
|
44
|
-
size === 64 && "
|
|
45
|
-
size === 88 && "
|
|
46
|
-
size === 148 && "
|
|
39
|
+
size === 16 && "size-4 text-[10px]",
|
|
40
|
+
size === 24 && "size-6 text-xs",
|
|
41
|
+
size === 32 && "size-8 text-xs",
|
|
42
|
+
size === 40 && "size-10 text-sm",
|
|
43
|
+
size === 48 && "size-12 text-base",
|
|
44
|
+
size === 64 && "size-16 text-xl",
|
|
45
|
+
size === 88 && "size-[88px] text-2xl",
|
|
46
|
+
size === 148 && "size-[148px] text-4xl",
|
|
47
47
|
className
|
|
48
48
|
),
|
|
49
49
|
...props,
|
|
@@ -94,7 +94,7 @@ const AvatarFallback = React.forwardRef(({ className, children, ...props }, ref)
|
|
|
94
94
|
{
|
|
95
95
|
ref,
|
|
96
96
|
className: cn(
|
|
97
|
-
"flex size-full items-center justify-center bg-neutral-200 text-neutral-400 uppercase leading-none",
|
|
97
|
+
"flex size-full items-center justify-center bg-neutral-200 font-semibold text-neutral-400 uppercase leading-none",
|
|
98
98
|
className
|
|
99
99
|
),
|
|
100
100
|
delayMs: 0,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Avatar.mjs","sources":["../../../src/components/Avatar/Avatar.tsx"],"sourcesContent":["import * as AvatarPrimitive from \"@radix-ui/react-avatar\";\nimport * as React from \"react\";\nimport { cn } from \"../../utils/cn\";\n\n/** Allowed pixel sizes for the avatar. */\nexport type AvatarSize = 16 | 24 | 32 | 40 | 48 | 64 | 88 | 148;\n\nconst AvatarContext = React.createContext<{ size: AvatarSize; NSFWShow: boolean }>({\n size: 40,\n NSFWShow: false,\n});\n\nconst STATUS_POSITIONS: Record<AvatarSize, { top: number; right: number }> = {\n 16: { top: -4, right: -4 },\n 24: { top: -3, right: -3 },\n 32: { top: -2, right: -2 },\n 40: { top: -1, right: -1 },\n 48: { top: 0, right: 0 },\n 64: { top: 2, right: 2 },\n 88: { top: 6, right: 6 },\n 148: { top: 15, right: 15 },\n};\n\n/** Shared avatar styling props. */\ninterface AvatarStyleProps {\n /** Pixel size of the avatar. @default 40 */\n size?: AvatarSize;\n /** Whether to show the online-status indicator dot. @default false */\n onlineIndicator?: boolean;\n /** Whether to show the platinum gradient border ring. @default false */\n platinumShow?: boolean;\n /** Whether to apply the NSFW blur filter over the image. @default false */\n NSFWShow?: boolean;\n}\n\n/** Props for the low-level {@link AvatarRoot} compound component. */\nexport interface AvatarRootProps\n extends React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Root>,\n AvatarStyleProps {}\n\n/**\n * Low-level avatar root for custom compositions. Provides size context to\n * child `AvatarImage` and `AvatarFallback` components.\n *\n * Prefer the higher-level {@link Avatar} component for most use cases.\n */\nconst AvatarRoot = React.forwardRef<\n React.ComponentRef<typeof AvatarPrimitive.Root>,\n AvatarRootProps\n>(\n (\n {\n className,\n size = 40,\n onlineIndicator = false,\n platinumShow = false,\n NSFWShow = false,\n children,\n ...props\n },\n ref,\n ) => {\n const statusPosition = STATUS_POSITIONS[size];\n\n return (\n <AvatarContext.Provider value={{ size, NSFWShow }}>\n <div className=\"relative inline-flex\">\n <AvatarPrimitive.Root\n ref={ref}\n data-testid=\"avatar\"\n className={cn(\n \"relative inline-flex shrink-0 items-center justify-center overflow-hidden rounded-full bg-neutral-100\",\n size === 16 && \"
|
|
1
|
+
{"version":3,"file":"Avatar.mjs","sources":["../../../src/components/Avatar/Avatar.tsx"],"sourcesContent":["import * as AvatarPrimitive from \"@radix-ui/react-avatar\";\nimport * as React from \"react\";\nimport { cn } from \"../../utils/cn\";\n\n/** Allowed pixel sizes for the avatar. */\nexport type AvatarSize = 16 | 24 | 32 | 40 | 48 | 64 | 88 | 148;\n\nconst AvatarContext = React.createContext<{ size: AvatarSize; NSFWShow: boolean }>({\n size: 40,\n NSFWShow: false,\n});\n\nconst STATUS_POSITIONS: Record<AvatarSize, { top: number; right: number }> = {\n 16: { top: -4, right: -4 },\n 24: { top: -3, right: -3 },\n 32: { top: -2, right: -2 },\n 40: { top: -1, right: -1 },\n 48: { top: 0, right: 0 },\n 64: { top: 2, right: 2 },\n 88: { top: 6, right: 6 },\n 148: { top: 15, right: 15 },\n};\n\n/** Shared avatar styling props. */\ninterface AvatarStyleProps {\n /** Pixel size of the avatar. @default 40 */\n size?: AvatarSize;\n /** Whether to show the online-status indicator dot. @default false */\n onlineIndicator?: boolean;\n /** Whether to show the platinum gradient border ring. @default false */\n platinumShow?: boolean;\n /** Whether to apply the NSFW blur filter over the image. @default false */\n NSFWShow?: boolean;\n}\n\n/** Props for the low-level {@link AvatarRoot} compound component. */\nexport interface AvatarRootProps\n extends React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Root>,\n AvatarStyleProps {}\n\n/**\n * Low-level avatar root for custom compositions. Provides size context to\n * child `AvatarImage` and `AvatarFallback` components.\n *\n * Prefer the higher-level {@link Avatar} component for most use cases.\n */\nconst AvatarRoot = React.forwardRef<\n React.ComponentRef<typeof AvatarPrimitive.Root>,\n AvatarRootProps\n>(\n (\n {\n className,\n size = 40,\n onlineIndicator = false,\n platinumShow = false,\n NSFWShow = false,\n children,\n ...props\n },\n ref,\n ) => {\n const statusPosition = STATUS_POSITIONS[size];\n\n return (\n <AvatarContext.Provider value={{ size, NSFWShow }}>\n <div className=\"relative inline-flex\">\n <AvatarPrimitive.Root\n ref={ref}\n data-testid=\"avatar\"\n className={cn(\n \"relative inline-flex shrink-0 items-center justify-center overflow-hidden rounded-full bg-neutral-100\",\n size === 16 && \"size-4 text-[10px]\",\n size === 24 && \"size-6 text-xs\",\n size === 32 && \"size-8 text-xs\",\n size === 40 && \"size-10 text-sm\",\n size === 48 && \"size-12 text-base\",\n size === 64 && \"size-16 text-xl\",\n size === 88 && \"size-[88px] text-2xl\",\n size === 148 && \"size-[148px] text-4xl\",\n className,\n )}\n {...props}\n >\n {children}\n </AvatarPrimitive.Root>\n {platinumShow && (\n <div\n className=\"pointer-events-none absolute inset-0 rounded-full\"\n style={{\n background: `linear-gradient(143deg, #504F54 0%, #B1B1B1 20.3154%, #13181C 37.3727%, #C6C6C8 58.8154%, #FFFFFF 69.3154%, #0C0F14 81.3154%, #696A6E 100%)`,\n WebkitMask: \"radial-gradient(circle closest-side, transparent 96%, black 96%)\",\n mask: \"radial-gradient(circle closest-side, transparent 96%, black 96%)\",\n }}\n aria-hidden=\"true\"\n />\n )}\n {onlineIndicator && (\n <span\n className=\"absolute size-3 rounded-full border-2 border-background-150 bg-brand-green-500\"\n style={{\n top: `${statusPosition.top}px`,\n right: `${statusPosition.right}px`,\n }}\n aria-hidden=\"true\"\n />\n )}\n </div>\n </AvatarContext.Provider>\n );\n },\n);\n\nAvatarRoot.displayName = \"AvatarRoot\";\n\n/** Props for the {@link AvatarImage} compound component. */\nexport interface AvatarImageProps\n extends React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Image> {}\n\n/** Renders the avatar image. Automatically applies the NSFW blur when enabled on the parent `AvatarRoot`. */\nconst AvatarImage = React.forwardRef<\n React.ComponentRef<typeof AvatarPrimitive.Image>,\n AvatarImageProps\n>(({ className, ...props }, ref) => {\n const { NSFWShow } = React.useContext(AvatarContext);\n return (\n <AvatarPrimitive.Image\n ref={ref}\n className={cn(\"size-full bg-neutral-200 object-cover\", NSFWShow && \"blur-md\", className)}\n {...props}\n />\n );\n});\n\nAvatarImage.displayName = \"AvatarImage\";\n\n/** Props for the {@link AvatarFallback} compound component. */\nexport interface AvatarFallbackProps\n extends React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Fallback> {}\n\n/** Renders fallback content (e.g. initials or an icon) when the avatar image has not loaded. */\nconst AvatarFallback = React.forwardRef<\n React.ComponentRef<typeof AvatarPrimitive.Fallback>,\n AvatarFallbackProps\n>(({ className, children, ...props }, ref) => (\n <AvatarPrimitive.Fallback\n ref={ref}\n className={cn(\n \"flex size-full items-center justify-center bg-neutral-200 font-semibold text-neutral-400 uppercase leading-none\",\n className,\n )}\n delayMs={0}\n {...props}\n >\n {children}\n </AvatarPrimitive.Fallback>\n));\n\nAvatarFallback.displayName = \"AvatarFallback\";\n\nexport interface AvatarProps\n extends React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Root>,\n AvatarStyleProps {\n /** URL of the avatar image. */\n src?: string;\n /** Alt text for the avatar image. @default \"Avatar\" */\n alt?: string;\n /** Fallback content rendered when the image has not loaded (e.g. initials or an icon). */\n fallback?: React.ReactNode;\n}\n\n/**\n * Displays a user avatar with optional online indicator, platinum border, and\n * NSFW blur. Pass `src` and `fallback` for the simple API, or compose your own\n * layout with `AvatarRoot`, `AvatarImage`, and `AvatarFallback` as children.\n *\n * @example\n * ```tsx\n * <Avatar src=\"/photo.jpg\" alt=\"Jane Doe\" fallback=\"JD\" size={48} />\n * ```\n */\nexport const Avatar = React.forwardRef<\n React.ComponentRef<typeof AvatarPrimitive.Root>,\n AvatarProps\n>(\n (\n {\n className,\n size = 40,\n src,\n alt,\n fallback,\n onlineIndicator = false,\n platinumShow = false,\n NSFWShow = false,\n children,\n ...props\n },\n ref,\n ) => {\n const rootProps = {\n ref,\n size,\n onlineIndicator,\n platinumShow,\n NSFWShow,\n className,\n ...props,\n };\n\n if (children) {\n return <AvatarRoot {...rootProps}>{children}</AvatarRoot>;\n }\n\n return (\n <AvatarRoot {...rootProps}>\n {src && <AvatarImage src={src} alt={alt ?? \"Avatar\"} />}\n <AvatarFallback>{fallback}</AvatarFallback>\n </AvatarRoot>\n );\n },\n);\n\nAvatar.displayName = \"Avatar\";\n\nexport { AvatarRoot, AvatarImage, AvatarFallback };\n"],"names":[],"mappings":"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"}
|
|
@@ -23,11 +23,11 @@ const iconSizeVariants = {
|
|
|
23
23
|
72: "[&>svg]:size-8"
|
|
24
24
|
};
|
|
25
25
|
const sizeVariants = {
|
|
26
|
-
24: "p-1",
|
|
27
|
-
32: "p-1.5",
|
|
28
|
-
40: "p-[10px]",
|
|
29
|
-
52: "p-2",
|
|
30
|
-
72: "p-4"
|
|
26
|
+
24: "size-6 p-1",
|
|
27
|
+
32: "size-8 p-1.5",
|
|
28
|
+
40: "size-10 p-[10px]",
|
|
29
|
+
52: "size-[52px] p-2",
|
|
30
|
+
72: "size-[72px] p-4"
|
|
31
31
|
};
|
|
32
32
|
const countSizeMap = {
|
|
33
33
|
24: "16",
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"IconButton.mjs","sources":["../../../src/components/IconButton/IconButton.tsx"],"sourcesContent":["import * as React from \"react\";\nimport { cn } from \"../../utils/cn\";\nimport { Count, type CountSize } from \"../Count/Count\";\n\nconst iconButtonVariants = {\n primary:\n \"bg-neutral-400 text-body-300 hover:bg-hover-100 not-disabled:active:bg-hover-100 disabled:opacity-50 focus-visible:shadow-focus-ring\",\n secondary:\n \"bg-neutral-100 text-neutral-400 hover:bg-neutral-200 not-disabled:active:bg-neutral-200 disabled:opacity-50 focus-visible:shadow-focus-ring\",\n tertiary:\n \"bg-transparent text-neutral-400 hover:bg-hover-300 not-disabled:active:bg-brand-green-50 disabled:opacity-50 focus-visible:shadow-focus-ring\",\n brand:\n \"bg-body-black-solid-constant text-brand-green-500 hover:bg-brand-green-500 hover:text-body-black-solid-constant not-disabled:active:bg-brand-green-500 not-disabled:active:text-body-black-solid-constant disabled:opacity-50 focus-visible:shadow-focus-ring\",\n contrast:\n \"bg-transparent text-body-white-solid-constant hover:bg-hover-300 not-disabled:active:bg-hover-300 disabled:opacity-50 focus-visible:shadow-focus-ring\",\n messaging:\n \"bg-body-black-solid-constant text-brand-green-500 hover:bg-brand-green-500 hover:text-body-black-solid-constant not-disabled:active:bg-brand-green-500 not-disabled:active:text-body-black-solid-constant disabled:opacity-50 focus-visible:shadow-focus-ring\",\n navTray:\n \"bg-transparent text-neutral-400 hover:bg-hover-300 not-disabled:active:bg-brand-green-50 disabled:opacity-50 focus-visible:shadow-focus-ring\",\n tertiaryDestructive:\n \"bg-transparent text-error-500 hover:bg-hover-300 not-disabled:active:bg-hover-300 disabled:opacity-50 focus-visible:shadow-focus-ring\",\n stop: \"bg-neutral-400 text-body-300 hover:bg-brand-green-500 hover:text-body-black-solid-constant not-disabled:active:bg-brand-green-500 not-disabled:active:text-body-black-solid-constant disabled:opacity-50 focus-visible:shadow-focus-ring\",\n microphone:\n \"bg-neutral-400 text-body-300 hover:bg-brand-green-500 hover:text-body-black-solid-constant not-disabled:active:bg-brand-green-500 not-disabled:active:text-body-black-solid-constant disabled:opacity-50 focus-visible:shadow-focus-ring\",\n};\n\nconst iconSizeVariants = {\n 24: \"[&>svg]:size-4\",\n 32: \"[&>svg]:size-5\",\n 40: \"[&>svg]:size-6\",\n 52: \"[&>svg]:size-7\",\n 72: \"[&>svg]:size-8\",\n} as const;\n\nconst sizeVariants = {\n 24: \"p-1\",\n 32: \"p-1.5\",\n 40: \"p-[10px]\",\n 52: \"p-2\",\n 72: \"p-4\",\n} as const;\n\nconst countSizeMap: Record<string, CountSize> = {\n 24: \"16\",\n 32: \"24\",\n 40: \"32\",\n 52: \"32\",\n 72: \"32\",\n};\n\n/** Visual style variant of the icon button. */\nexport type IconButtonVariant =\n | \"primary\"\n | \"secondary\"\n | \"tertiary\"\n | \"brand\"\n | \"contrast\"\n | \"messaging\"\n | \"navTray\"\n | \"tertiaryDestructive\"\n | \"stop\"\n | \"microphone\";\n\n/** Icon button size in pixels. */\nexport type IconButtonSize = \"24\" | \"32\" | \"40\" | \"52\" | \"72\";\n\nexport interface IconButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {\n /** Visual style variant of the icon button. @default \"primary\" */\n variant?: IconButtonVariant;\n /** Size of the button in pixels. @default \"40\" */\n size?: IconButtonSize;\n /** Icon element to render inside the button. */\n icon: React.ReactNode;\n /** When provided, displays a {@link Count} badge at the top-right corner (tertiary & navTray variants only). */\n counterValue?: number;\n}\n\n/**\n * A circular button containing only an icon. Use when an action can be\n * represented by an icon alone (e.g. close, send, mic). Pair with an\n * `aria-label` for accessibility.\n *\n * @example\n * ```tsx\n * <IconButton icon={<CloseIcon />} aria-label=\"Close\" variant=\"tertiary\" />\n * ```\n */\nexport const IconButton = React.forwardRef<HTMLButtonElement, IconButtonProps>(\n (\n { className, variant = \"primary\", size = \"40\", icon, counterValue, disabled = false, ...props },\n ref,\n ) => {\n if (process.env.NODE_ENV !== \"production\") {\n if (!props[\"aria-label\"] && !props[\"aria-labelledby\"] && !props.title) {\n console.warn(\n \"IconButton: No accessible name provided. Add an `aria-label`, `aria-labelledby`, or `title` prop so screen readers can announce this button.\",\n );\n }\n }\n\n return (\n <button\n ref={ref}\n type=\"button\"\n data-testid=\"icon-button\"\n disabled={disabled}\n className={cn(\n // Base styles\n \"relative inline-flex shrink-0 items-center justify-center focus-visible:outline-none\",\n \"cursor-pointer rounded-full transition-all duration-150 ease-in-out disabled:cursor-default\",\n // Size variants\n sizeVariants[size],\n // Variant styles\n iconButtonVariants[variant],\n // Manual CSS overrides\n className,\n )}\n {...props}\n >\n <span\n className={cn(\"flex shrink-0 items-center justify-center\", iconSizeVariants[size])}\n aria-hidden=\"true\"\n >\n {icon}\n </span>\n\n {counterValue !== undefined && (\n <Count\n value={counterValue}\n size={countSizeMap[size]}\n className=\"absolute -top-0.5 -right-0.5\"\n />\n )}\n </button>\n );\n },\n);\n\nIconButton.displayName = \"IconButton\";\n"],"names":[],"mappings":";;;;;AAIA,MAAM,qBAAqB;AAAA,EACzB,SACE;AAAA,EACF,WACE;AAAA,EACF,UACE;AAAA,EACF,OACE;AAAA,EACF,UACE;AAAA,EACF,WACE;AAAA,EACF,SACE;AAAA,EACF,qBACE;AAAA,EACF,MAAM;AAAA,EACN,YACE;AACJ;AAEA,MAAM,mBAAmB;AAAA,EACvB,IAAI;AAAA,EACJ,IAAI;AAAA,EACJ,IAAI;AAAA,EACJ,IAAI;AAAA,EACJ,IAAI;AACN;AAEA,MAAM,eAAe;AAAA,EACnB,IAAI;AAAA,EACJ,IAAI;AAAA,EACJ,IAAI;AAAA,EACJ,IAAI;AAAA,EACJ,IAAI;AACN;AAEA,MAAM,eAA0C;AAAA,EAC9C,IAAI;AAAA,EACJ,IAAI;AAAA,EACJ,IAAI;AAAA,EACJ,IAAI;AAAA,EACJ,IAAI;AACN;AAuCO,MAAM,aAAa,MAAM;AAAA,EAC9B,CACE,EAAE,WAAW,UAAU,WAAW,OAAO,MAAM,MAAM,cAAc,WAAW,OAAO,GAAG,MAAA,GACxF,QACG;AACH,QAAI,QAAQ,IAAI,aAAa,cAAc;AACzC,UAAI,CAAC,MAAM,YAAY,KAAK,CAAC,MAAM,iBAAiB,KAAK,CAAC,MAAM,OAAO;AACrE,gBAAQ;AAAA,UACN;AAAA,QAAA;AAAA,MAEJ;AAAA,IACF;AAEA,WACE;AAAA,MAAC;AAAA,MAAA;AAAA,QACC;AAAA,QACA,MAAK;AAAA,QACL,eAAY;AAAA,QACZ;AAAA,QACA,WAAW;AAAA;AAAA,UAET;AAAA,UACA;AAAA;AAAA,UAEA,aAAa,IAAI;AAAA;AAAA,UAEjB,mBAAmB,OAAO;AAAA;AAAA,UAE1B;AAAA,QAAA;AAAA,QAED,GAAG;AAAA,QAEJ,UAAA;AAAA,UAAA;AAAA,YAAC;AAAA,YAAA;AAAA,cACC,WAAW,GAAG,6CAA6C,iBAAiB,IAAI,CAAC;AAAA,cACjF,eAAY;AAAA,cAEX,UAAA;AAAA,YAAA;AAAA,UAAA;AAAA,UAGF,iBAAiB,UAChB;AAAA,YAAC;AAAA,YAAA;AAAA,cACC,OAAO;AAAA,cACP,MAAM,aAAa,IAAI;AAAA,cACvB,WAAU;AAAA,YAAA;AAAA,UAAA;AAAA,QACZ;AAAA,MAAA;AAAA,IAAA;AAAA,EAIR;AACF;AAEA,WAAW,cAAc;"}
|
|
1
|
+
{"version":3,"file":"IconButton.mjs","sources":["../../../src/components/IconButton/IconButton.tsx"],"sourcesContent":["import * as React from \"react\";\nimport { cn } from \"../../utils/cn\";\nimport { Count, type CountSize } from \"../Count/Count\";\n\nconst iconButtonVariants = {\n primary:\n \"bg-neutral-400 text-body-300 hover:bg-hover-100 not-disabled:active:bg-hover-100 disabled:opacity-50 focus-visible:shadow-focus-ring\",\n secondary:\n \"bg-neutral-100 text-neutral-400 hover:bg-neutral-200 not-disabled:active:bg-neutral-200 disabled:opacity-50 focus-visible:shadow-focus-ring\",\n tertiary:\n \"bg-transparent text-neutral-400 hover:bg-hover-300 not-disabled:active:bg-brand-green-50 disabled:opacity-50 focus-visible:shadow-focus-ring\",\n brand:\n \"bg-body-black-solid-constant text-brand-green-500 hover:bg-brand-green-500 hover:text-body-black-solid-constant not-disabled:active:bg-brand-green-500 not-disabled:active:text-body-black-solid-constant disabled:opacity-50 focus-visible:shadow-focus-ring\",\n contrast:\n \"bg-transparent text-body-white-solid-constant hover:bg-hover-300 not-disabled:active:bg-hover-300 disabled:opacity-50 focus-visible:shadow-focus-ring\",\n messaging:\n \"bg-body-black-solid-constant text-brand-green-500 hover:bg-brand-green-500 hover:text-body-black-solid-constant not-disabled:active:bg-brand-green-500 not-disabled:active:text-body-black-solid-constant disabled:opacity-50 focus-visible:shadow-focus-ring\",\n navTray:\n \"bg-transparent text-neutral-400 hover:bg-hover-300 not-disabled:active:bg-brand-green-50 disabled:opacity-50 focus-visible:shadow-focus-ring\",\n tertiaryDestructive:\n \"bg-transparent text-error-500 hover:bg-hover-300 not-disabled:active:bg-hover-300 disabled:opacity-50 focus-visible:shadow-focus-ring\",\n stop: \"bg-neutral-400 text-body-300 hover:bg-brand-green-500 hover:text-body-black-solid-constant not-disabled:active:bg-brand-green-500 not-disabled:active:text-body-black-solid-constant disabled:opacity-50 focus-visible:shadow-focus-ring\",\n microphone:\n \"bg-neutral-400 text-body-300 hover:bg-brand-green-500 hover:text-body-black-solid-constant not-disabled:active:bg-brand-green-500 not-disabled:active:text-body-black-solid-constant disabled:opacity-50 focus-visible:shadow-focus-ring\",\n};\n\nconst iconSizeVariants = {\n 24: \"[&>svg]:size-4\",\n 32: \"[&>svg]:size-5\",\n 40: \"[&>svg]:size-6\",\n 52: \"[&>svg]:size-7\",\n 72: \"[&>svg]:size-8\",\n} as const;\n\nconst sizeVariants = {\n 24: \"size-6 p-1\",\n 32: \"size-8 p-1.5\",\n 40: \"size-10 p-[10px]\",\n 52: \"size-[52px] p-2\",\n 72: \"size-[72px] p-4\",\n} as const;\n\nconst countSizeMap: Record<string, CountSize> = {\n 24: \"16\",\n 32: \"24\",\n 40: \"32\",\n 52: \"32\",\n 72: \"32\",\n};\n\n/** Visual style variant of the icon button. */\nexport type IconButtonVariant =\n | \"primary\"\n | \"secondary\"\n | \"tertiary\"\n | \"brand\"\n | \"contrast\"\n | \"messaging\"\n | \"navTray\"\n | \"tertiaryDestructive\"\n | \"stop\"\n | \"microphone\";\n\n/** Icon button size in pixels. */\nexport type IconButtonSize = \"24\" | \"32\" | \"40\" | \"52\" | \"72\";\n\nexport interface IconButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {\n /** Visual style variant of the icon button. @default \"primary\" */\n variant?: IconButtonVariant;\n /** Size of the button in pixels. @default \"40\" */\n size?: IconButtonSize;\n /** Icon element to render inside the button. */\n icon: React.ReactNode;\n /** When provided, displays a {@link Count} badge at the top-right corner (tertiary & navTray variants only). */\n counterValue?: number;\n}\n\n/**\n * A circular button containing only an icon. Use when an action can be\n * represented by an icon alone (e.g. close, send, mic). Pair with an\n * `aria-label` for accessibility.\n *\n * @example\n * ```tsx\n * <IconButton icon={<CloseIcon />} aria-label=\"Close\" variant=\"tertiary\" />\n * ```\n */\nexport const IconButton = React.forwardRef<HTMLButtonElement, IconButtonProps>(\n (\n { className, variant = \"primary\", size = \"40\", icon, counterValue, disabled = false, ...props },\n ref,\n ) => {\n if (process.env.NODE_ENV !== \"production\") {\n if (!props[\"aria-label\"] && !props[\"aria-labelledby\"] && !props.title) {\n console.warn(\n \"IconButton: No accessible name provided. Add an `aria-label`, `aria-labelledby`, or `title` prop so screen readers can announce this button.\",\n );\n }\n }\n\n return (\n <button\n ref={ref}\n type=\"button\"\n data-testid=\"icon-button\"\n disabled={disabled}\n className={cn(\n // Base styles\n \"relative inline-flex shrink-0 items-center justify-center focus-visible:outline-none\",\n \"cursor-pointer rounded-full transition-all duration-150 ease-in-out disabled:cursor-default\",\n // Size variants\n sizeVariants[size],\n // Variant styles\n iconButtonVariants[variant],\n // Manual CSS overrides\n className,\n )}\n {...props}\n >\n <span\n className={cn(\"flex shrink-0 items-center justify-center\", iconSizeVariants[size])}\n aria-hidden=\"true\"\n >\n {icon}\n </span>\n\n {counterValue !== undefined && (\n <Count\n value={counterValue}\n size={countSizeMap[size]}\n className=\"absolute -top-0.5 -right-0.5\"\n />\n )}\n </button>\n );\n },\n);\n\nIconButton.displayName = \"IconButton\";\n"],"names":[],"mappings":";;;;;AAIA,MAAM,qBAAqB;AAAA,EACzB,SACE;AAAA,EACF,WACE;AAAA,EACF,UACE;AAAA,EACF,OACE;AAAA,EACF,UACE;AAAA,EACF,WACE;AAAA,EACF,SACE;AAAA,EACF,qBACE;AAAA,EACF,MAAM;AAAA,EACN,YACE;AACJ;AAEA,MAAM,mBAAmB;AAAA,EACvB,IAAI;AAAA,EACJ,IAAI;AAAA,EACJ,IAAI;AAAA,EACJ,IAAI;AAAA,EACJ,IAAI;AACN;AAEA,MAAM,eAAe;AAAA,EACnB,IAAI;AAAA,EACJ,IAAI;AAAA,EACJ,IAAI;AAAA,EACJ,IAAI;AAAA,EACJ,IAAI;AACN;AAEA,MAAM,eAA0C;AAAA,EAC9C,IAAI;AAAA,EACJ,IAAI;AAAA,EACJ,IAAI;AAAA,EACJ,IAAI;AAAA,EACJ,IAAI;AACN;AAuCO,MAAM,aAAa,MAAM;AAAA,EAC9B,CACE,EAAE,WAAW,UAAU,WAAW,OAAO,MAAM,MAAM,cAAc,WAAW,OAAO,GAAG,MAAA,GACxF,QACG;AACH,QAAI,QAAQ,IAAI,aAAa,cAAc;AACzC,UAAI,CAAC,MAAM,YAAY,KAAK,CAAC,MAAM,iBAAiB,KAAK,CAAC,MAAM,OAAO;AACrE,gBAAQ;AAAA,UACN;AAAA,QAAA;AAAA,MAEJ;AAAA,IACF;AAEA,WACE;AAAA,MAAC;AAAA,MAAA;AAAA,QACC;AAAA,QACA,MAAK;AAAA,QACL,eAAY;AAAA,QACZ;AAAA,QACA,WAAW;AAAA;AAAA,UAET;AAAA,UACA;AAAA;AAAA,UAEA,aAAa,IAAI;AAAA;AAAA,UAEjB,mBAAmB,OAAO;AAAA;AAAA,UAE1B;AAAA,QAAA;AAAA,QAED,GAAG;AAAA,QAEJ,UAAA;AAAA,UAAA;AAAA,YAAC;AAAA,YAAA;AAAA,cACC,WAAW,GAAG,6CAA6C,iBAAiB,IAAI,CAAC;AAAA,cACjF,eAAY;AAAA,cAEX,UAAA;AAAA,YAAA;AAAA,UAAA;AAAA,UAGF,iBAAiB,UAChB;AAAA,YAAC;AAAA,YAAA;AAAA,cACC,OAAO;AAAA,cACP,MAAM,aAAa,IAAI;AAAA,cACvB,WAAU;AAAA,YAAA;AAAA,UAAA;AAAA,QACZ;AAAA,MAAA;AAAA,IAAA;AAAA,EAIR;AACF;AAEA,WAAW,cAAc;"}
|
|
@@ -39,9 +39,10 @@ function getContainerClassName(size, error, disabled) {
|
|
|
39
39
|
disabled && "opacity-50"
|
|
40
40
|
);
|
|
41
41
|
}
|
|
42
|
-
function getTextareaClassName(size, hasClearButton, hasMinRows) {
|
|
42
|
+
function getTextareaClassName(size, hasClearButton, hasMinRows, resizable) {
|
|
43
43
|
return cn(
|
|
44
|
-
"w-full
|
|
44
|
+
"w-full rounded-xl bg-transparent text-body-100 no-underline placeholder:text-body-200 placeholder:opacity-40 focus:outline-none disabled:cursor-not-allowed",
|
|
45
|
+
resizable ? "resize-y" : "resize-none",
|
|
45
46
|
!hasMinRows && "min-h-[80px]",
|
|
46
47
|
TEXTAREA_SIZE_CLASSES[size],
|
|
47
48
|
PADDING_HORIZONTAL[size],
|
|
@@ -125,6 +126,7 @@ const TextArea = React.forwardRef(
|
|
|
125
126
|
onChange,
|
|
126
127
|
minRows,
|
|
127
128
|
maxRows,
|
|
129
|
+
resizable = true,
|
|
128
130
|
...props
|
|
129
131
|
}, ref) => {
|
|
130
132
|
const generatedId = React.useId();
|
|
@@ -171,7 +173,7 @@ const TextArea = React.forwardRef(
|
|
|
171
173
|
disabled,
|
|
172
174
|
"aria-describedby": ariaDescribedBy,
|
|
173
175
|
"aria-invalid": error || void 0,
|
|
174
|
-
className: getTextareaClassName(size, showClear, !!minRows),
|
|
176
|
+
className: getTextareaClassName(size, showClear, !!minRows, resizable),
|
|
175
177
|
value: displayValue,
|
|
176
178
|
defaultValue: resolvedDefaultValue,
|
|
177
179
|
onChange: handleChange,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"TextArea.mjs","sources":["../../../src/components/TextArea/TextArea.tsx"],"sourcesContent":["import * as React from \"react\";\nimport { cn } from \"../../utils/cn\";\nimport { IconButton } from \"../IconButton/IconButton\";\nimport { CheckOutlineIcon } from \"../Icons/CheckOutlineIcon\";\nimport { CloseIcon } from \"../Icons/CloseIcon\";\n\n/** Text area height in pixels. */\nexport type TextAreaSize = \"48\" | \"40\" | \"32\";\n\nexport interface TextAreaProps\n extends Omit<React.TextareaHTMLAttributes<HTMLTextAreaElement>, \"size\"> {\n /** Label text displayed above the textarea. Also used as the accessible name. */\n label?: string;\n /** Helper text displayed below the textarea. Replaced by `errorMessage` when `error` is `true`. */\n helperText?: string;\n /** Minimum height of the text area in pixels. @default \"48\" */\n size?: TextAreaSize;\n /** Whether the text area is in an error state. @default false */\n error?: boolean;\n /** Error message displayed below the textarea. Shown instead of `helperText` when `error` is `true`. */\n errorMessage?: string;\n /** Whether the text area is validated. @default false */\n validated?: boolean;\n /** Whether the text area stretches to fill its container width. @default false */\n fullWidth?: boolean;\n /** Whether to show a clear button when text is present. @default false */\n showClearButton?: boolean;\n /** Callback fired when the clear button is clicked. Note: `onChange` is also called with an empty value when clearing. */\n onClear?: () => void;\n /** Minimum number of rows (lines) for the textarea. */\n minRows?: number;\n /** Maximum number of rows (lines) for the textarea. */\n maxRows?: number;\n}\n\nconst CONTAINER_MIN_HEIGHT: Record<TextAreaSize, string> = {\n \"48\": \"min-h-12\",\n \"40\": \"min-h-10\",\n \"32\": \"min-h-8\",\n};\n\nconst TEXTAREA_SIZE_CLASSES: Record<TextAreaSize, string> = {\n \"48\": \"py-3 typography-body-1-regular\",\n \"40\": \"py-2 typography-body-1-regular\",\n \"32\": \"py-2 typography-body-2-regular\",\n};\n\nconst PADDING_HORIZONTAL: Record<TextAreaSize, string> = {\n \"48\": \"px-4\",\n \"40\": \"px-4\",\n \"32\": \"px-3\",\n};\n\nconst PADDING_RIGHT_WITH_CLEAR: Record<TextAreaSize, string> = {\n \"48\": \"pr-11\",\n \"40\": \"pr-11\",\n \"32\": \"pr-10\",\n};\n\nconst CLEAR_BUTTON_RIGHT: Record<TextAreaSize, string> = {\n \"48\": \"right-4 top-3\",\n \"40\": \"right-4 top-2\",\n \"32\": \"right-3 top-2\",\n};\n\nfunction getContainerClassName(size: TextAreaSize, error: boolean, disabled?: boolean) {\n return cn(\n \"relative rounded-xl border bg-neutral-100 has-focus-visible:outline-none motion-safe:transition-colors\",\n error ? \"border-error-500\" : \"border-transparent\",\n !disabled && !error && \"hover:border-neutral-400\",\n CONTAINER_MIN_HEIGHT[size],\n disabled && \"opacity-50\",\n );\n}\n\nfunction getTextareaClassName(size: TextAreaSize, hasClearButton: boolean, hasMinRows: boolean) {\n return cn(\n \"w-full resize-y rounded-xl bg-transparent text-body-100 no-underline placeholder:text-body-200 placeholder:opacity-40 focus:outline-none disabled:cursor-not-allowed\",\n !hasMinRows && \"min-h-[80px]\",\n TEXTAREA_SIZE_CLASSES[size],\n PADDING_HORIZONTAL[size],\n hasClearButton ? PADDING_RIGHT_WITH_CLEAR[size] : \"\",\n );\n}\n\nfunction TextAreaHelperText({\n id,\n error,\n children,\n}: {\n id: string;\n error: boolean;\n children: React.ReactNode;\n}) {\n return (\n <p\n id={id}\n className={cn(\n \"typography-caption-regular px-2 pt-1 pb-0.5\",\n error ? \"text-error-500\" : \"text-body-200\",\n )}\n >\n {children}\n </p>\n );\n}\n\nfunction warnMissingAccessibleName(label?: string, ariaLabel?: string, ariaLabelledBy?: string) {\n if (process.env.NODE_ENV !== \"production\") {\n if (!label && !ariaLabel && !ariaLabelledBy) {\n console.warn(\n \"TextArea: no accessible name provided. Pass a `label`, `aria-label`, or `aria-labelledby` prop.\",\n );\n }\n }\n}\n\nfunction calculateMaxHeight(size: TextAreaSize, maxRows?: number): string | undefined {\n if (!maxRows) return undefined;\n\n // Line height is 24px for body-1 (sizes 48 and 40) and 20px for body-2 (size 32)\n const lineHeight = size === \"32\" ? 20 : 24;\n // py-2 = 8px, py-3 = 12px\n const verticalPadding = size === \"32\" ? 8 : size === \"40\" ? 8 : 12;\n\n return `${lineHeight * maxRows + verticalPadding * 2}px`;\n}\n\nfunction useTextAreaValue(\n value: React.TextareaHTMLAttributes<HTMLTextAreaElement>[\"value\"],\n defaultValue: React.TextareaHTMLAttributes<HTMLTextAreaElement>[\"defaultValue\"],\n showClearButton: boolean,\n onChange?: React.ChangeEventHandler<HTMLTextAreaElement>,\n onClear?: () => void,\n textareaRef?: React.RefObject<HTMLTextAreaElement | null>,\n) {\n const [internalValue, setInternalValue] = React.useState(defaultValue ?? \"\");\n const resolvedValue = value !== undefined ? value : internalValue;\n\n const handleChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {\n setInternalValue(e.target.value);\n onChange?.(e);\n };\n\n const handleClear = () => {\n setInternalValue(\"\");\n\n if (onChange && textareaRef?.current) {\n const syntheticEvent = {\n target: { ...textareaRef.current, value: \"\" },\n currentTarget: { ...textareaRef.current, value: \"\" },\n } as React.ChangeEvent<HTMLTextAreaElement>;\n onChange(syntheticEvent);\n }\n\n onClear?.();\n };\n\n return {\n resolvedValue,\n displayValue: showClearButton ? resolvedValue : value,\n resolvedDefaultValue: showClearButton ? undefined : defaultValue,\n handleChange,\n handleClear,\n };\n}\n\n/**\n * A multi-line text input with optional label, helper/error text, and clear button.\n *\n * Provide at least one of `label`, `aria-label`, or `aria-labelledby` for\n * accessibility — a console warning is emitted in development if none are set.\n *\n * @example\n * ```tsx\n * <TextArea\n * label=\"Description\"\n * placeholder=\"Enter your description...\"\n * showClearButton\n * error={!!descError}\n * errorMessage={descError}\n * />\n * ```\n */\nexport const TextArea = React.forwardRef<HTMLTextAreaElement, TextAreaProps>(\n (\n {\n label,\n helperText,\n size = \"48\",\n error = false,\n errorMessage,\n validated = false,\n className,\n id,\n disabled,\n fullWidth = false,\n showClearButton = false,\n onClear,\n value,\n defaultValue,\n onChange,\n minRows,\n maxRows,\n ...props\n },\n ref,\n ) => {\n const generatedId = React.useId();\n const inputId = id || generatedId;\n const helperTextId = `${inputId}-helper`;\n const bottomText = error && errorMessage ? errorMessage : helperText;\n const maxHeight = calculateMaxHeight(size, maxRows);\n\n const internalRef = React.useRef<HTMLTextAreaElement>(null);\n\n const { resolvedValue, displayValue, resolvedDefaultValue, handleChange, handleClear } =\n useTextAreaValue(value, defaultValue, showClearButton, onChange, onClear, internalRef);\n\n const mergedRef = (node: HTMLTextAreaElement | null) => {\n internalRef.current = node;\n if (typeof ref === \"function\") {\n ref(node);\n } else if (ref) {\n (ref as React.MutableRefObject<HTMLTextAreaElement | null>).current = node;\n }\n };\n\n const showClear = showClearButton && resolvedValue !== \"\" && !disabled;\n const showValidated = validated && !showClear;\n const ariaDescribedBy = bottomText ? helperTextId : undefined;\n const textareaStyle = maxHeight ? { maxHeight } : undefined;\n\n warnMissingAccessibleName(label, props[\"aria-label\"], props[\"aria-labelledby\"]);\n\n return (\n <div\n className={cn(\"flex flex-col\", fullWidth && \"w-full\", className)}\n data-disabled={disabled ? \"\" : undefined}\n data-error={error ? \"\" : undefined}\n >\n {label && (\n <label\n htmlFor={inputId}\n className=\"typography-caption-semibold px-1 pt-1 pb-2 text-body-100\"\n >\n {label}\n </label>\n )}\n\n <div className={getContainerClassName(size, error, disabled)}>\n <textarea\n ref={mergedRef}\n id={inputId}\n disabled={disabled}\n aria-describedby={ariaDescribedBy}\n aria-invalid={error || undefined}\n className={getTextareaClassName(size, showClear, !!minRows)}\n value={displayValue}\n defaultValue={resolvedDefaultValue}\n onChange={handleChange}\n rows={minRows}\n style={textareaStyle}\n {...props}\n />\n\n {showClear && (\n <IconButton\n variant=\"tertiary\"\n size=\"24\"\n icon={<CloseIcon />}\n aria-label=\"Clear text\"\n onClick={handleClear}\n className={cn(\n \"absolute flex size-5 items-center justify-center self-end\",\n CLEAR_BUTTON_RIGHT[size],\n )}\n />\n )}\n {showValidated && (\n <div\n className={cn(\n \"pointer-events-none absolute flex size-5 items-center justify-center\",\n CLEAR_BUTTON_RIGHT[size],\n )}\n >\n <CheckOutlineIcon className=\"text-success-500\" />\n </div>\n )}\n </div>\n\n {bottomText && (\n <TextAreaHelperText id={helperTextId} error={error}>\n {bottomText}\n </TextAreaHelperText>\n )}\n </div>\n );\n },\n);\n\nTextArea.displayName = \"TextArea\";\n"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"TextArea.mjs","sources":["../../../src/components/TextArea/TextArea.tsx"],"sourcesContent":["import * as React from \"react\";\nimport { cn } from \"../../utils/cn\";\nimport { IconButton } from \"../IconButton/IconButton\";\nimport { CheckOutlineIcon } from \"../Icons/CheckOutlineIcon\";\nimport { CloseIcon } from \"../Icons/CloseIcon\";\n\n/** Text area height in pixels. */\nexport type TextAreaSize = \"48\" | \"40\" | \"32\";\n\nexport interface TextAreaProps\n extends Omit<React.TextareaHTMLAttributes<HTMLTextAreaElement>, \"size\"> {\n /** Label text displayed above the textarea. Also used as the accessible name. */\n label?: string;\n /** Helper text displayed below the textarea. Replaced by `errorMessage` when `error` is `true`. */\n helperText?: string;\n /** Minimum height of the text area in pixels. @default \"48\" */\n size?: TextAreaSize;\n /** Whether the text area is in an error state. @default false */\n error?: boolean;\n /** Error message displayed below the textarea. Shown instead of `helperText` when `error` is `true`. */\n errorMessage?: string;\n /** Whether the text area is validated. @default false */\n validated?: boolean;\n /** Whether the text area stretches to fill its container width. @default false */\n fullWidth?: boolean;\n /** Whether to show a clear button when text is present. @default false */\n showClearButton?: boolean;\n /** Callback fired when the clear button is clicked. Note: `onChange` is also called with an empty value when clearing. */\n onClear?: () => void;\n /** Minimum number of rows (lines) for the textarea. */\n minRows?: number;\n /** Maximum number of rows (lines) for the textarea. */\n maxRows?: number;\n /** Whether the textarea can be resized by the user. @default true */\n resizable?: boolean;\n}\n\nconst CONTAINER_MIN_HEIGHT: Record<TextAreaSize, string> = {\n \"48\": \"min-h-12\",\n \"40\": \"min-h-10\",\n \"32\": \"min-h-8\",\n};\n\nconst TEXTAREA_SIZE_CLASSES: Record<TextAreaSize, string> = {\n \"48\": \"py-3 typography-body-1-regular\",\n \"40\": \"py-2 typography-body-1-regular\",\n \"32\": \"py-2 typography-body-2-regular\",\n};\n\nconst PADDING_HORIZONTAL: Record<TextAreaSize, string> = {\n \"48\": \"px-4\",\n \"40\": \"px-4\",\n \"32\": \"px-3\",\n};\n\nconst PADDING_RIGHT_WITH_CLEAR: Record<TextAreaSize, string> = {\n \"48\": \"pr-11\",\n \"40\": \"pr-11\",\n \"32\": \"pr-10\",\n};\n\nconst CLEAR_BUTTON_RIGHT: Record<TextAreaSize, string> = {\n \"48\": \"right-4 top-3\",\n \"40\": \"right-4 top-2\",\n \"32\": \"right-3 top-2\",\n};\n\nfunction getContainerClassName(size: TextAreaSize, error: boolean, disabled?: boolean) {\n return cn(\n \"relative rounded-xl border bg-neutral-100 has-focus-visible:outline-none motion-safe:transition-colors\",\n error ? \"border-error-500\" : \"border-transparent\",\n !disabled && !error && \"hover:border-neutral-400\",\n CONTAINER_MIN_HEIGHT[size],\n disabled && \"opacity-50\",\n );\n}\n\nfunction getTextareaClassName(\n size: TextAreaSize,\n hasClearButton: boolean,\n hasMinRows: boolean,\n resizable: boolean,\n) {\n return cn(\n \"w-full rounded-xl bg-transparent text-body-100 no-underline placeholder:text-body-200 placeholder:opacity-40 focus:outline-none disabled:cursor-not-allowed\",\n resizable ? \"resize-y\" : \"resize-none\",\n !hasMinRows && \"min-h-[80px]\",\n TEXTAREA_SIZE_CLASSES[size],\n PADDING_HORIZONTAL[size],\n hasClearButton ? PADDING_RIGHT_WITH_CLEAR[size] : \"\",\n );\n}\n\nfunction TextAreaHelperText({\n id,\n error,\n children,\n}: {\n id: string;\n error: boolean;\n children: React.ReactNode;\n}) {\n return (\n <p\n id={id}\n className={cn(\n \"typography-caption-regular px-2 pt-1 pb-0.5\",\n error ? \"text-error-500\" : \"text-body-200\",\n )}\n >\n {children}\n </p>\n );\n}\n\nfunction warnMissingAccessibleName(label?: string, ariaLabel?: string, ariaLabelledBy?: string) {\n if (process.env.NODE_ENV !== \"production\") {\n if (!label && !ariaLabel && !ariaLabelledBy) {\n console.warn(\n \"TextArea: no accessible name provided. Pass a `label`, `aria-label`, or `aria-labelledby` prop.\",\n );\n }\n }\n}\n\nfunction calculateMaxHeight(size: TextAreaSize, maxRows?: number): string | undefined {\n if (!maxRows) return undefined;\n\n // Line height is 24px for body-1 (sizes 48 and 40) and 20px for body-2 (size 32)\n const lineHeight = size === \"32\" ? 20 : 24;\n // py-2 = 8px, py-3 = 12px\n const verticalPadding = size === \"32\" ? 8 : size === \"40\" ? 8 : 12;\n\n return `${lineHeight * maxRows + verticalPadding * 2}px`;\n}\n\nfunction useTextAreaValue(\n value: React.TextareaHTMLAttributes<HTMLTextAreaElement>[\"value\"],\n defaultValue: React.TextareaHTMLAttributes<HTMLTextAreaElement>[\"defaultValue\"],\n showClearButton: boolean,\n onChange?: React.ChangeEventHandler<HTMLTextAreaElement>,\n onClear?: () => void,\n textareaRef?: React.RefObject<HTMLTextAreaElement | null>,\n) {\n const [internalValue, setInternalValue] = React.useState(defaultValue ?? \"\");\n const resolvedValue = value !== undefined ? value : internalValue;\n\n const handleChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {\n setInternalValue(e.target.value);\n onChange?.(e);\n };\n\n const handleClear = () => {\n setInternalValue(\"\");\n\n if (onChange && textareaRef?.current) {\n const syntheticEvent = {\n target: { ...textareaRef.current, value: \"\" },\n currentTarget: { ...textareaRef.current, value: \"\" },\n } as React.ChangeEvent<HTMLTextAreaElement>;\n onChange(syntheticEvent);\n }\n\n onClear?.();\n };\n\n return {\n resolvedValue,\n displayValue: showClearButton ? resolvedValue : value,\n resolvedDefaultValue: showClearButton ? undefined : defaultValue,\n handleChange,\n handleClear,\n };\n}\n\n/**\n * A multi-line text input with optional label, helper/error text, and clear button.\n *\n * Provide at least one of `label`, `aria-label`, or `aria-labelledby` for\n * accessibility — a console warning is emitted in development if none are set.\n *\n * @example\n * ```tsx\n * <TextArea\n * label=\"Description\"\n * placeholder=\"Enter your description...\"\n * showClearButton\n * error={!!descError}\n * errorMessage={descError}\n * />\n * ```\n */\nexport const TextArea = React.forwardRef<HTMLTextAreaElement, TextAreaProps>(\n (\n {\n label,\n helperText,\n size = \"48\",\n error = false,\n errorMessage,\n validated = false,\n className,\n id,\n disabled,\n fullWidth = false,\n showClearButton = false,\n onClear,\n value,\n defaultValue,\n onChange,\n minRows,\n maxRows,\n resizable = true,\n ...props\n },\n ref,\n ) => {\n const generatedId = React.useId();\n const inputId = id || generatedId;\n const helperTextId = `${inputId}-helper`;\n const bottomText = error && errorMessage ? errorMessage : helperText;\n const maxHeight = calculateMaxHeight(size, maxRows);\n\n const internalRef = React.useRef<HTMLTextAreaElement>(null);\n\n const { resolvedValue, displayValue, resolvedDefaultValue, handleChange, handleClear } =\n useTextAreaValue(value, defaultValue, showClearButton, onChange, onClear, internalRef);\n\n const mergedRef = (node: HTMLTextAreaElement | null) => {\n internalRef.current = node;\n if (typeof ref === \"function\") {\n ref(node);\n } else if (ref) {\n (ref as React.MutableRefObject<HTMLTextAreaElement | null>).current = node;\n }\n };\n\n const showClear = showClearButton && resolvedValue !== \"\" && !disabled;\n const showValidated = validated && !showClear;\n const ariaDescribedBy = bottomText ? helperTextId : undefined;\n const textareaStyle = maxHeight ? { maxHeight } : undefined;\n\n warnMissingAccessibleName(label, props[\"aria-label\"], props[\"aria-labelledby\"]);\n\n return (\n <div\n className={cn(\"flex flex-col\", fullWidth && \"w-full\", className)}\n data-disabled={disabled ? \"\" : undefined}\n data-error={error ? \"\" : undefined}\n >\n {label && (\n <label\n htmlFor={inputId}\n className=\"typography-caption-semibold px-1 pt-1 pb-2 text-body-100\"\n >\n {label}\n </label>\n )}\n\n <div className={getContainerClassName(size, error, disabled)}>\n <textarea\n ref={mergedRef}\n id={inputId}\n disabled={disabled}\n aria-describedby={ariaDescribedBy}\n aria-invalid={error || undefined}\n className={getTextareaClassName(size, showClear, !!minRows, resizable)}\n value={displayValue}\n defaultValue={resolvedDefaultValue}\n onChange={handleChange}\n rows={minRows}\n style={textareaStyle}\n {...props}\n />\n\n {showClear && (\n <IconButton\n variant=\"tertiary\"\n size=\"24\"\n icon={<CloseIcon />}\n aria-label=\"Clear text\"\n onClick={handleClear}\n className={cn(\n \"absolute flex size-5 items-center justify-center self-end\",\n CLEAR_BUTTON_RIGHT[size],\n )}\n />\n )}\n {showValidated && (\n <div\n className={cn(\n \"pointer-events-none absolute flex size-5 items-center justify-center\",\n CLEAR_BUTTON_RIGHT[size],\n )}\n >\n <CheckOutlineIcon className=\"text-success-500\" />\n </div>\n )}\n </div>\n\n {bottomText && (\n <TextAreaHelperText id={helperTextId} error={error}>\n {bottomText}\n </TextAreaHelperText>\n )}\n </div>\n );\n },\n);\n\nTextArea.displayName = \"TextArea\";\n"],"names":[],"mappings":"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"}
|
package/dist/index.d.ts
CHANGED
|
@@ -1152,6 +1152,8 @@ export declare interface TextAreaProps extends Omit<React_2.TextareaHTMLAttribut
|
|
|
1152
1152
|
minRows?: number;
|
|
1153
1153
|
/** Maximum number of rows (lines) for the textarea. */
|
|
1154
1154
|
maxRows?: number;
|
|
1155
|
+
/** Whether the textarea can be resized by the user. @default true */
|
|
1156
|
+
resizable?: boolean;
|
|
1155
1157
|
}
|
|
1156
1158
|
|
|
1157
1159
|
/** Text area height in pixels. */
|