@fanvue/ui 3.12.1 → 3.14.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/Accordion/AccordionTrigger.cjs +11 -2
- package/dist/cjs/components/Accordion/AccordionTrigger.cjs.map +1 -1
- package/dist/cjs/components/AudioPlayer/AudioPlayer.cjs +357 -0
- package/dist/cjs/components/AudioPlayer/AudioPlayer.cjs.map +1 -0
- package/dist/cjs/components/Avatar/Avatar.cjs +2 -2
- package/dist/cjs/components/Avatar/Avatar.cjs.map +1 -1
- package/dist/cjs/components/Dialog/Dialog.cjs +2 -1
- package/dist/cjs/components/Dialog/Dialog.cjs.map +1 -1
- package/dist/cjs/components/DropdownMenu/DropdownMenu.cjs +25 -2
- package/dist/cjs/components/DropdownMenu/DropdownMenu.cjs.map +1 -1
- package/dist/cjs/components/FloatingActionButton/FloatingActionButton.cjs +59 -0
- package/dist/cjs/components/FloatingActionButton/FloatingActionButton.cjs.map +1 -0
- package/dist/cjs/components/Icons/GifIcon.cjs +12 -36
- package/dist/cjs/components/Icons/GifIcon.cjs.map +1 -1
- package/dist/cjs/components/Icons/GridViewIcon.cjs +50 -0
- package/dist/cjs/components/Icons/GridViewIcon.cjs.map +1 -0
- package/dist/cjs/components/Icons/ListViewIcon.cjs +50 -0
- package/dist/cjs/components/Icons/ListViewIcon.cjs.map +1 -0
- package/dist/cjs/components/ProgressBar/ProgressBar.cjs +30 -5
- package/dist/cjs/components/ProgressBar/ProgressBar.cjs.map +1 -1
- package/dist/cjs/components/ProgressBar/ProgressBarItem.cjs +49 -0
- package/dist/cjs/components/ProgressBar/ProgressBarItem.cjs.map +1 -0
- package/dist/cjs/components/ProgressBar/ProgressBarSteps.cjs +66 -0
- package/dist/cjs/components/ProgressBar/ProgressBarSteps.cjs.map +1 -0
- package/dist/cjs/components/SegmentedControl/SegmentedControl.cjs +42 -11
- package/dist/cjs/components/SegmentedControl/SegmentedControl.cjs.map +1 -1
- package/dist/cjs/components/TextArea/TextArea.cjs +4 -4
- package/dist/cjs/components/TextArea/TextArea.cjs.map +1 -1
- package/dist/cjs/components/TextField/TextField.cjs +105 -62
- package/dist/cjs/components/TextField/TextField.cjs.map +1 -1
- package/dist/cjs/index.cjs +12 -0
- package/dist/cjs/index.cjs.map +1 -1
- package/dist/components/Accordion/AccordionTrigger.mjs +11 -2
- package/dist/components/Accordion/AccordionTrigger.mjs.map +1 -1
- package/dist/components/AudioPlayer/AudioPlayer.mjs +340 -0
- package/dist/components/AudioPlayer/AudioPlayer.mjs.map +1 -0
- package/dist/components/Avatar/Avatar.mjs +2 -2
- package/dist/components/Avatar/Avatar.mjs.map +1 -1
- package/dist/components/Dialog/Dialog.mjs +2 -1
- package/dist/components/Dialog/Dialog.mjs.map +1 -1
- package/dist/components/DropdownMenu/DropdownMenu.mjs +25 -2
- package/dist/components/DropdownMenu/DropdownMenu.mjs.map +1 -1
- package/dist/components/FloatingActionButton/FloatingActionButton.mjs +42 -0
- package/dist/components/FloatingActionButton/FloatingActionButton.mjs.map +1 -0
- package/dist/components/Icons/GifIcon.mjs +12 -36
- package/dist/components/Icons/GifIcon.mjs.map +1 -1
- package/dist/components/Icons/GridViewIcon.mjs +33 -0
- package/dist/components/Icons/GridViewIcon.mjs.map +1 -0
- package/dist/components/Icons/ListViewIcon.mjs +33 -0
- package/dist/components/Icons/ListViewIcon.mjs.map +1 -0
- package/dist/components/ProgressBar/ProgressBar.mjs +30 -5
- package/dist/components/ProgressBar/ProgressBar.mjs.map +1 -1
- package/dist/components/ProgressBar/ProgressBarItem.mjs +32 -0
- package/dist/components/ProgressBar/ProgressBarItem.mjs.map +1 -0
- package/dist/components/ProgressBar/ProgressBarSteps.mjs +49 -0
- package/dist/components/ProgressBar/ProgressBarSteps.mjs.map +1 -0
- package/dist/components/SegmentedControl/SegmentedControl.mjs +42 -11
- package/dist/components/SegmentedControl/SegmentedControl.mjs.map +1 -1
- package/dist/components/TextArea/TextArea.mjs +4 -4
- package/dist/components/TextArea/TextArea.mjs.map +1 -1
- package/dist/components/TextField/TextField.mjs +105 -62
- package/dist/components/TextField/TextField.mjs.map +1 -1
- package/dist/index.d.ts +237 -10
- package/dist/index.mjs +12 -0
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ProgressBar.mjs","sources":["../../../src/components/ProgressBar/ProgressBar.tsx"],"sourcesContent":["import * as React from \"react\";\nimport { cn } from \"@/utils/cn\";\n\n/** Track height — `\"default\"` (12px) or `\"small\"` (6px). */\nexport type ProgressBarSize = \"default\" | \"small\";\n
|
|
1
|
+
{"version":3,"file":"ProgressBar.mjs","sources":["../../../src/components/ProgressBar/ProgressBar.tsx"],"sourcesContent":["import * as React from \"react\";\nimport { cn } from \"@/utils/cn\";\n\n/** Track height — `\"default\"` (12px) or `\"small\"` (6px). */\nexport type ProgressBarSize = \"default\" | \"small\";\n/**\n * Colour mode.\n * - `\"brand\"` — V2 brand (green) fill on a tinted track.\n * - `\"mono\"` — V2 monochrome fill on a tinted track (theme-aware).\n * - `\"default\"` — legacy value-coded red/yellow/green.\n * - `\"generic\"` — legacy solid brand green on a neutral track.\n * - `\"neutral\"` — legacy theme-aware inverse colour on a neutral track.\n */\nexport type ProgressBarVariant = \"brand\" | \"mono\" | \"default\" | \"generic\" | \"neutral\";\n\nexport interface ProgressBarProps extends Omit<React.HTMLAttributes<HTMLDivElement>, \"title\"> {\n /** Current progress value, clamped to 0–100. */\n value: number;\n /** Track height — `\"default\"` (12px) or `\"small\"` (6px). @default \"default\" */\n size?: ProgressBarSize;\n /** Colour mode. Prefer `\"brand\"` or `\"mono\"` (V2); `\"default\"`/`\"generic\"`/`\"neutral\"` are legacy. @default \"default\" */\n variant?: ProgressBarVariant;\n /** Title content shown at the top-left of the bar. */\n title?: React.ReactNode;\n /** Whether to display the completion percentage above the track. @default false */\n showCompletion?: boolean;\n /** Steps label shown at the top-right (e.g. `\"2/8 steps\"`). */\n stepsLabel?: React.ReactNode;\n /** Helper content at the bottom-left of the bar. */\n helperLeft?: React.ReactNode;\n /** Helper content at the bottom-right of the bar. */\n helperRight?: React.ReactNode;\n /** Icon shown at the bottom-left before the helper text. */\n leftIcon?: React.ReactNode;\n /** Accessible label for the `progressbar` role. @default \"Progress\" */\n ariaLabel?: string;\n /** Human-readable text alternative for the current value (e.g. \"Step 3 of 5\"). */\n ariaValueText?: string;\n}\n\nconst TRACK_HEIGHT: Record<ProgressBarSize, string> = {\n default: \"h-3\",\n small: \"h-1.5\",\n};\n\nconst GAP: Record<ProgressBarSize, string> = {\n default: \"gap-3\",\n small: \"gap-1\",\n};\n\nfunction getDefaultBarColor(value: number): string {\n if (value >= 100) return \"bg-success-content\";\n if (value >= 40) return \"bg-warning-content\";\n return \"bg-error-content\";\n}\n\nfunction getDefaultTextColor(value: number): string {\n if (value >= 100) return \"text-success-content\";\n if (value >= 40) return \"text-warning-content\";\n return \"text-error-content\";\n}\n\nfunction resolveColors(\n variant: ProgressBarVariant,\n value: number,\n): { barColor: string; textColor: string; trackColor: string } {\n const neutralTrack = \"bg-neutral-alphas-50\";\n if (variant === \"brand\")\n return {\n barColor: \"bg-progress-bar-brand-active\",\n textColor: \"text-brand-primary-default\",\n trackColor: \"bg-progress-bar-brand-inactive\",\n };\n if (variant === \"mono\")\n return {\n barColor: \"bg-progress-bar-mono-active\",\n textColor: \"text-content-primary\",\n trackColor: \"bg-progress-bar-mono-inactive\",\n };\n if (variant === \"neutral\")\n return {\n barColor: \"bg-content-tertiary\",\n textColor: \"text-content-tertiary\",\n trackColor: neutralTrack,\n };\n if (variant === \"generic\")\n return {\n barColor: \"bg-brand-primary-default\",\n textColor: \"text-brand-primary-default\",\n trackColor: neutralTrack,\n };\n return {\n barColor: getDefaultBarColor(value),\n textColor: getDefaultTextColor(value),\n trackColor: neutralTrack,\n };\n}\n\n/**\n * A horizontal progress indicator with optional title, completion percentage,\n * step count, and helper text. The bar colour reflects progress when using the\n * `\"default\"` variant.\n *\n * @example\n * ```tsx\n * <ProgressBar value={65} title=\"Upload\" showCompletion />\n * ```\n */\nexport const ProgressBar = React.forwardRef<HTMLDivElement, ProgressBarProps>(\n (\n {\n value,\n size = \"default\",\n variant = \"default\",\n title,\n showCompletion = false,\n stepsLabel,\n helperLeft,\n helperRight,\n leftIcon,\n ariaLabel,\n ariaValueText,\n className,\n ...props\n },\n ref,\n ) => {\n const clampedValue = Math.min(100, Math.max(0, value));\n const isSmall = size === \"small\";\n const { barColor, textColor, trackColor } = resolveColors(variant, clampedValue);\n\n const showHeader = title != null || showCompletion || stepsLabel != null;\n const showFooter = leftIcon != null || helperLeft != null || helperRight != null;\n\n return (\n <div ref={ref} className={cn(\"flex w-full flex-col\", GAP[size], className)} {...props}>\n {showHeader && (\n <div className=\"flex w-full items-end justify-between\">\n {title != null && (\n <p className=\"typography-description-12px-semibold text-content-primary\">{title}</p>\n )}\n {showCompletion && (\n <span\n className={cn(\n textColor,\n isSmall ? \"typography-header-heading-sm\" : \"typography-header-heading-xl\",\n )}\n >\n {Math.round(clampedValue)}%\n </span>\n )}\n {stepsLabel != null && (\n <span className=\"typography-description-12px-regular text-content-primary\">\n {stepsLabel}\n </span>\n )}\n </div>\n )}\n\n <div\n role=\"progressbar\"\n aria-label={ariaLabel ?? \"Progress\"}\n aria-valuenow={clampedValue}\n aria-valuemin={0}\n aria-valuemax={100}\n aria-valuetext={ariaValueText}\n className={cn(\"relative w-full rounded-full\", trackColor, TRACK_HEIGHT[size])}\n >\n <div\n className={cn(\n \"absolute inset-y-0 left-0 rounded-full transition-[width] duration-300 ease-in-out\",\n barColor,\n )}\n style={{ width: `${clampedValue}%` }}\n />\n </div>\n\n {showFooter && (\n <div className=\"flex w-full items-center justify-between\">\n <div className=\"flex items-center gap-1\">\n {leftIcon != null && (\n <span className=\"flex size-5 items-center justify-center\" aria-hidden=\"true\">\n {leftIcon}\n </span>\n )}\n {helperLeft != null && (\n <span className=\"typography-description-12px-regular text-content-primary\">\n {helperLeft}\n </span>\n )}\n </div>\n {helperRight != null && (\n <span className=\"typography-description-12px-regular text-content-primary\">\n {helperRight}\n </span>\n )}\n </div>\n )}\n </div>\n );\n },\n);\n\nProgressBar.displayName = \"ProgressBar\";\n"],"names":[],"mappings":";;;;AAwCA,MAAM,eAAgD;AAAA,EACpD,SAAS;AAAA,EACT,OAAO;AACT;AAEA,MAAM,MAAuC;AAAA,EAC3C,SAAS;AAAA,EACT,OAAO;AACT;AAEA,SAAS,mBAAmB,OAAuB;AACjD,MAAI,SAAS,IAAK,QAAO;AACzB,MAAI,SAAS,GAAI,QAAO;AACxB,SAAO;AACT;AAEA,SAAS,oBAAoB,OAAuB;AAClD,MAAI,SAAS,IAAK,QAAO;AACzB,MAAI,SAAS,GAAI,QAAO;AACxB,SAAO;AACT;AAEA,SAAS,cACP,SACA,OAC6D;AAC7D,QAAM,eAAe;AACrB,MAAI,YAAY;AACd,WAAO;AAAA,MACL,UAAU;AAAA,MACV,WAAW;AAAA,MACX,YAAY;AAAA,IAAA;AAEhB,MAAI,YAAY;AACd,WAAO;AAAA,MACL,UAAU;AAAA,MACV,WAAW;AAAA,MACX,YAAY;AAAA,IAAA;AAEhB,MAAI,YAAY;AACd,WAAO;AAAA,MACL,UAAU;AAAA,MACV,WAAW;AAAA,MACX,YAAY;AAAA,IAAA;AAEhB,MAAI,YAAY;AACd,WAAO;AAAA,MACL,UAAU;AAAA,MACV,WAAW;AAAA,MACX,YAAY;AAAA,IAAA;AAEhB,SAAO;AAAA,IACL,UAAU,mBAAmB,KAAK;AAAA,IAClC,WAAW,oBAAoB,KAAK;AAAA,IACpC,YAAY;AAAA,EAAA;AAEhB;AAYO,MAAM,cAAc,MAAM;AAAA,EAC/B,CACE;AAAA,IACE;AAAA,IACA,OAAO;AAAA,IACP,UAAU;AAAA,IACV;AAAA,IACA,iBAAiB;AAAA,IACjB;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA,GAAG;AAAA,EAAA,GAEL,QACG;AACH,UAAM,eAAe,KAAK,IAAI,KAAK,KAAK,IAAI,GAAG,KAAK,CAAC;AACrD,UAAM,UAAU,SAAS;AACzB,UAAM,EAAE,UAAU,WAAW,eAAe,cAAc,SAAS,YAAY;AAE/E,UAAM,aAAa,SAAS,QAAQ,kBAAkB,cAAc;AACpE,UAAM,aAAa,YAAY,QAAQ,cAAc,QAAQ,eAAe;AAE5E,WACE,qBAAC,OAAA,EAAI,KAAU,WAAW,GAAG,wBAAwB,IAAI,IAAI,GAAG,SAAS,GAAI,GAAG,OAC7E,UAAA;AAAA,MAAA,cACC,qBAAC,OAAA,EAAI,WAAU,yCACZ,UAAA;AAAA,QAAA,SAAS,QACR,oBAAC,KAAA,EAAE,WAAU,6DAA6D,UAAA,OAAM;AAAA,QAEjF,kBACC;AAAA,UAAC;AAAA,UAAA;AAAA,YACC,WAAW;AAAA,cACT;AAAA,cACA,UAAU,iCAAiC;AAAA,YAAA;AAAA,YAG5C,UAAA;AAAA,cAAA,KAAK,MAAM,YAAY;AAAA,cAAE;AAAA,YAAA;AAAA,UAAA;AAAA,QAAA;AAAA,QAG7B,cAAc,QACb,oBAAC,QAAA,EAAK,WAAU,4DACb,UAAA,WAAA,CACH;AAAA,MAAA,GAEJ;AAAA,MAGF;AAAA,QAAC;AAAA,QAAA;AAAA,UACC,MAAK;AAAA,UACL,cAAY,aAAa;AAAA,UACzB,iBAAe;AAAA,UACf,iBAAe;AAAA,UACf,iBAAe;AAAA,UACf,kBAAgB;AAAA,UAChB,WAAW,GAAG,gCAAgC,YAAY,aAAa,IAAI,CAAC;AAAA,UAE5E,UAAA;AAAA,YAAC;AAAA,YAAA;AAAA,cACC,WAAW;AAAA,gBACT;AAAA,gBACA;AAAA,cAAA;AAAA,cAEF,OAAO,EAAE,OAAO,GAAG,YAAY,IAAA;AAAA,YAAI;AAAA,UAAA;AAAA,QACrC;AAAA,MAAA;AAAA,MAGD,cACC,qBAAC,OAAA,EAAI,WAAU,4CACb,UAAA;AAAA,QAAA,qBAAC,OAAA,EAAI,WAAU,2BACZ,UAAA;AAAA,UAAA,YAAY,QACX,oBAAC,QAAA,EAAK,WAAU,2CAA0C,eAAY,QACnE,UAAA,SAAA,CACH;AAAA,UAED,cAAc,QACb,oBAAC,QAAA,EAAK,WAAU,4DACb,UAAA,WAAA,CACH;AAAA,QAAA,GAEJ;AAAA,QACC,eAAe,QACd,oBAAC,QAAA,EAAK,WAAU,4DACb,UAAA,YAAA,CACH;AAAA,MAAA,EAAA,CAEJ;AAAA,IAAA,GAEJ;AAAA,EAEJ;AACF;AAEA,YAAY,cAAc;"}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx } from "react/jsx-runtime";
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
import { cn } from "../../utils/cn.mjs";
|
|
5
|
+
const ACTIVE_COLOR = {
|
|
6
|
+
brand: "bg-progress-bar-brand-active",
|
|
7
|
+
mono: "bg-progress-bar-mono-active"
|
|
8
|
+
};
|
|
9
|
+
const INACTIVE_COLOR = {
|
|
10
|
+
brand: "bg-progress-bar-brand-inactive",
|
|
11
|
+
mono: "bg-progress-bar-mono-inactive"
|
|
12
|
+
};
|
|
13
|
+
const ProgressBarItem = React.forwardRef(
|
|
14
|
+
({ active = false, variant = "brand", size = "default", className, ...props }, ref) => /* @__PURE__ */ jsx(
|
|
15
|
+
"div",
|
|
16
|
+
{
|
|
17
|
+
ref,
|
|
18
|
+
className: cn(
|
|
19
|
+
"w-full rounded-full",
|
|
20
|
+
size === "small" ? "h-1" : "h-2",
|
|
21
|
+
active ? ACTIVE_COLOR[variant] : INACTIVE_COLOR[variant],
|
|
22
|
+
className
|
|
23
|
+
),
|
|
24
|
+
...props
|
|
25
|
+
}
|
|
26
|
+
)
|
|
27
|
+
);
|
|
28
|
+
ProgressBarItem.displayName = "ProgressBarItem";
|
|
29
|
+
export {
|
|
30
|
+
ProgressBarItem
|
|
31
|
+
};
|
|
32
|
+
//# sourceMappingURL=ProgressBarItem.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ProgressBarItem.mjs","sources":["../../../src/components/ProgressBar/ProgressBarItem.tsx"],"sourcesContent":["import * as React from \"react\";\nimport { cn } from \"@/utils/cn\";\n\n/** Colour mode for a progress bar item — brand green or theme-aware monochrome. */\nexport type ProgressBarItemVariant = \"brand\" | \"mono\";\n/** Pill thickness — `\"default\"` (8px) or `\"small\"` (4px). */\nexport type ProgressBarItemSize = \"default\" | \"small\";\n\nexport interface ProgressBarItemProps extends React.HTMLAttributes<HTMLDivElement> {\n /** Whether this item is filled (completed). @default false */\n active?: boolean;\n /** Colour mode. @default \"brand\" */\n variant?: ProgressBarItemVariant;\n /** Pill thickness — `\"default\"` (8px) or `\"small\"` (4px). @default \"default\" */\n size?: ProgressBarItemSize;\n}\n\nconst ACTIVE_COLOR: Record<ProgressBarItemVariant, string> = {\n brand: \"bg-progress-bar-brand-active\",\n mono: \"bg-progress-bar-mono-active\",\n};\n\nconst INACTIVE_COLOR: Record<ProgressBarItemVariant, string> = {\n brand: \"bg-progress-bar-brand-inactive\",\n mono: \"bg-progress-bar-mono-inactive\",\n};\n\n/**\n * A single rounded segment used to compose a stepped progress indicator. Render\n * a row of these (see {@link ProgressBarSteps}) to represent discrete steps.\n *\n * @example\n * ```tsx\n * <ProgressBarItem active variant=\"brand\" />\n * ```\n */\nexport const ProgressBarItem = React.forwardRef<HTMLDivElement, ProgressBarItemProps>(\n ({ active = false, variant = \"brand\", size = \"default\", className, ...props }, ref) => (\n <div\n ref={ref}\n className={cn(\n \"w-full rounded-full\",\n size === \"small\" ? \"h-1\" : \"h-2\",\n active ? ACTIVE_COLOR[variant] : INACTIVE_COLOR[variant],\n className,\n )}\n {...props}\n />\n ),\n);\n\nProgressBarItem.displayName = \"ProgressBarItem\";\n"],"names":[],"mappings":";;;;AAiBA,MAAM,eAAuD;AAAA,EAC3D,OAAO;AAAA,EACP,MAAM;AACR;AAEA,MAAM,iBAAyD;AAAA,EAC7D,OAAO;AAAA,EACP,MAAM;AACR;AAWO,MAAM,kBAAkB,MAAM;AAAA,EACnC,CAAC,EAAE,SAAS,OAAO,UAAU,SAAS,OAAO,WAAW,WAAW,GAAG,MAAA,GAAS,QAC7E;AAAA,IAAC;AAAA,IAAA;AAAA,MACC;AAAA,MACA,WAAW;AAAA,QACT;AAAA,QACA,SAAS,UAAU,QAAQ;AAAA,QAC3B,SAAS,aAAa,OAAO,IAAI,eAAe,OAAO;AAAA,QACvD;AAAA,MAAA;AAAA,MAED,GAAG;AAAA,IAAA;AAAA,EAAA;AAGV;AAEA,gBAAgB,cAAc;"}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx } from "react/jsx-runtime";
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
import { cn } from "../../utils/cn.mjs";
|
|
5
|
+
import { ProgressBarItem } from "./ProgressBarItem.mjs";
|
|
6
|
+
const ProgressBarSteps = React.forwardRef(
|
|
7
|
+
({
|
|
8
|
+
steps,
|
|
9
|
+
value,
|
|
10
|
+
variant = "brand",
|
|
11
|
+
size = "default",
|
|
12
|
+
ariaLabel,
|
|
13
|
+
ariaValueText,
|
|
14
|
+
className,
|
|
15
|
+
...props
|
|
16
|
+
}, ref) => {
|
|
17
|
+
const total = Math.max(1, Math.floor(steps));
|
|
18
|
+
const completed = Math.min(total, Math.max(0, Math.floor(value)));
|
|
19
|
+
return /* @__PURE__ */ jsx(
|
|
20
|
+
"div",
|
|
21
|
+
{
|
|
22
|
+
ref,
|
|
23
|
+
role: "progressbar",
|
|
24
|
+
"aria-label": ariaLabel ?? "Progress",
|
|
25
|
+
"aria-valuemin": 0,
|
|
26
|
+
"aria-valuemax": total,
|
|
27
|
+
"aria-valuenow": completed,
|
|
28
|
+
"aria-valuetext": ariaValueText,
|
|
29
|
+
className: cn("flex w-full items-center gap-1", className),
|
|
30
|
+
...props,
|
|
31
|
+
children: Array.from({ length: total }, (_, index) => /* @__PURE__ */ jsx(
|
|
32
|
+
ProgressBarItem,
|
|
33
|
+
{
|
|
34
|
+
active: index < completed,
|
|
35
|
+
variant,
|
|
36
|
+
size,
|
|
37
|
+
className: "flex-1"
|
|
38
|
+
},
|
|
39
|
+
index
|
|
40
|
+
))
|
|
41
|
+
}
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
);
|
|
45
|
+
ProgressBarSteps.displayName = "ProgressBarSteps";
|
|
46
|
+
export {
|
|
47
|
+
ProgressBarSteps
|
|
48
|
+
};
|
|
49
|
+
//# sourceMappingURL=ProgressBarSteps.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ProgressBarSteps.mjs","sources":["../../../src/components/ProgressBar/ProgressBarSteps.tsx"],"sourcesContent":["import * as React from \"react\";\nimport { cn } from \"@/utils/cn\";\nimport {\n ProgressBarItem,\n type ProgressBarItemSize,\n type ProgressBarItemVariant,\n} from \"./ProgressBarItem\";\n\nexport interface ProgressBarStepsProps extends React.HTMLAttributes<HTMLDivElement> {\n /** Total number of steps (segments). Values below 1 are treated as 1. */\n steps: number;\n /** Number of completed steps, clamped to `0`–`steps`. */\n value: number;\n /** Colour mode. @default \"brand\" */\n variant?: ProgressBarItemVariant;\n /** Pill thickness — `\"default\"` (8px) or `\"small\"` (4px). @default \"default\" */\n size?: ProgressBarItemSize;\n /** Accessible label for the `progressbar` role. @default \"Progress\" */\n ariaLabel?: string;\n /** Human-readable text alternative for the current value (e.g. \"Step 2 of 4\"). */\n ariaValueText?: string;\n}\n\n/**\n * A segmented (stepped) progress indicator built from {@link ProgressBarItem}\n * pills. Fills the first `value` of `steps` segments.\n *\n * @example\n * ```tsx\n * <ProgressBarSteps steps={4} value={2} variant=\"brand\" />\n * ```\n */\nexport const ProgressBarSteps = React.forwardRef<HTMLDivElement, ProgressBarStepsProps>(\n (\n {\n steps,\n value,\n variant = \"brand\",\n size = \"default\",\n ariaLabel,\n ariaValueText,\n className,\n ...props\n },\n ref,\n ) => {\n const total = Math.max(1, Math.floor(steps));\n const completed = Math.min(total, Math.max(0, Math.floor(value)));\n\n return (\n <div\n ref={ref}\n role=\"progressbar\"\n aria-label={ariaLabel ?? \"Progress\"}\n aria-valuemin={0}\n aria-valuemax={total}\n aria-valuenow={completed}\n aria-valuetext={ariaValueText}\n className={cn(\"flex w-full items-center gap-1\", className)}\n {...props}\n >\n {Array.from({ length: total }, (_, index) => (\n <ProgressBarItem\n key={index}\n active={index < completed}\n variant={variant}\n size={size}\n className=\"flex-1\"\n />\n ))}\n </div>\n );\n },\n);\n\nProgressBarSteps.displayName = \"ProgressBarSteps\";\n"],"names":[],"mappings":";;;;;AAgCO,MAAM,mBAAmB,MAAM;AAAA,EACpC,CACE;AAAA,IACE;AAAA,IACA;AAAA,IACA,UAAU;AAAA,IACV,OAAO;AAAA,IACP;AAAA,IACA;AAAA,IACA;AAAA,IACA,GAAG;AAAA,EAAA,GAEL,QACG;AACH,UAAM,QAAQ,KAAK,IAAI,GAAG,KAAK,MAAM,KAAK,CAAC;AAC3C,UAAM,YAAY,KAAK,IAAI,OAAO,KAAK,IAAI,GAAG,KAAK,MAAM,KAAK,CAAC,CAAC;AAEhE,WACE;AAAA,MAAC;AAAA,MAAA;AAAA,QACC;AAAA,QACA,MAAK;AAAA,QACL,cAAY,aAAa;AAAA,QACzB,iBAAe;AAAA,QACf,iBAAe;AAAA,QACf,iBAAe;AAAA,QACf,kBAAgB;AAAA,QAChB,WAAW,GAAG,kCAAkC,SAAS;AAAA,QACxD,GAAG;AAAA,QAEH,UAAA,MAAM,KAAK,EAAE,QAAQ,SAAS,CAAC,GAAG,UACjC;AAAA,UAAC;AAAA,UAAA;AAAA,YAEC,QAAQ,QAAQ;AAAA,YAChB;AAAA,YACA;AAAA,YACA,WAAU;AAAA,UAAA;AAAA,UAJL;AAAA,QAAA,CAMR;AAAA,MAAA;AAAA,IAAA;AAAA,EAGP;AACF;AAEA,iBAAiB,cAAc;"}
|
|
@@ -16,11 +16,47 @@ function warnMissingAccessibleName(ariaLabel, ariaLabelledBy) {
|
|
|
16
16
|
}
|
|
17
17
|
}
|
|
18
18
|
}
|
|
19
|
+
function warnMissingOptionAccessibleName(options) {
|
|
20
|
+
if (process.env.NODE_ENV !== "production") {
|
|
21
|
+
for (const option of options) {
|
|
22
|
+
if (option.icon && !option.label?.trim()) {
|
|
23
|
+
console.warn(
|
|
24
|
+
`SegmentedControl: icon-only segment "${option.value}" is missing a non-empty \`label\` to use as its accessible name.`
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
function getSegmentClassName({
|
|
31
|
+
appearance,
|
|
32
|
+
size,
|
|
33
|
+
variant,
|
|
34
|
+
isSelected,
|
|
35
|
+
disabled
|
|
36
|
+
}) {
|
|
37
|
+
return cn(
|
|
38
|
+
"relative inline-flex min-w-0 cursor-pointer items-center justify-center rounded-full",
|
|
39
|
+
"motion-safe:transition-colors motion-safe:duration-150 motion-safe:ease-in-out",
|
|
40
|
+
"focus-visible:shadow-focus-ring focus-visible:outline-none",
|
|
41
|
+
variant === "fill" ? "flex-1" : "shrink-0",
|
|
42
|
+
appearance === "plain" ? cn(
|
|
43
|
+
// Padding + negative margin enlarge the hit target without changing the
|
|
44
|
+
// visual footprint, which must stay glyph-only to match the design.
|
|
45
|
+
"-m-1 p-1",
|
|
46
|
+
isSelected ? "text-icons-primary" : "text-icons-tertiary hover:text-icons-primary"
|
|
47
|
+
) : cn(
|
|
48
|
+
sizeClasses[size],
|
|
49
|
+
isSelected ? "bg-buttons-primary-default text-content-primary-inverted shadow-sm" : "text-content-primary hover:bg-buttons-switch-hover"
|
|
50
|
+
),
|
|
51
|
+
disabled && "pointer-events-none"
|
|
52
|
+
);
|
|
53
|
+
}
|
|
19
54
|
const SegmentedControl = React.forwardRef(
|
|
20
55
|
({
|
|
21
56
|
className,
|
|
22
57
|
size = "32",
|
|
23
58
|
variant = "hug",
|
|
59
|
+
appearance = "pill",
|
|
24
60
|
options,
|
|
25
61
|
value: controlledValue,
|
|
26
62
|
defaultValue,
|
|
@@ -29,6 +65,7 @@ const SegmentedControl = React.forwardRef(
|
|
|
29
65
|
...props
|
|
30
66
|
}, ref) => {
|
|
31
67
|
warnMissingAccessibleName(props["aria-label"], props["aria-labelledby"]);
|
|
68
|
+
warnMissingOptionAccessibleName(options);
|
|
32
69
|
const [internalValue, setInternalValue] = React.useState(defaultValue ?? options[0]?.value);
|
|
33
70
|
const isControlled = controlledValue !== void 0;
|
|
34
71
|
const currentValue = isControlled ? controlledValue : internalValue;
|
|
@@ -55,8 +92,9 @@ const SegmentedControl = React.forwardRef(
|
|
|
55
92
|
ref,
|
|
56
93
|
role: "radiogroup",
|
|
57
94
|
className: cn(
|
|
58
|
-
"relative items-center rounded-full
|
|
95
|
+
"relative items-center rounded-full",
|
|
59
96
|
variant === "fill" ? "flex w-full" : "inline-flex",
|
|
97
|
+
appearance === "plain" ? "gap-2" : "bg-surface-tertiary p-1",
|
|
60
98
|
disabled && "cursor-not-allowed opacity-50",
|
|
61
99
|
className
|
|
62
100
|
),
|
|
@@ -76,18 +114,11 @@ const SegmentedControl = React.forwardRef(
|
|
|
76
114
|
"aria-checked": isSelected,
|
|
77
115
|
tabIndex: isSelected || !anySelected && index === 0 ? 0 : -1,
|
|
78
116
|
disabled,
|
|
117
|
+
"aria-label": option.icon ? option.label : void 0,
|
|
79
118
|
onClick: () => handleSelect(option.value),
|
|
80
119
|
onKeyDown: (e) => handleKeyDown(e, index),
|
|
81
|
-
className:
|
|
82
|
-
|
|
83
|
-
"motion-safe:transition-colors motion-safe:duration-150 motion-safe:ease-in-out",
|
|
84
|
-
"focus-visible:shadow-focus-ring focus-visible:outline-none",
|
|
85
|
-
variant === "fill" ? "flex-1" : "shrink-0",
|
|
86
|
-
sizeClasses[size],
|
|
87
|
-
isSelected ? "bg-buttons-primary-default text-content-primary-inverted shadow-sm" : "text-content-primary hover:bg-buttons-switch-hover",
|
|
88
|
-
disabled && "pointer-events-none"
|
|
89
|
-
),
|
|
90
|
-
children: /* @__PURE__ */ jsx("span", { className: "min-w-0 truncate", children: option.label })
|
|
120
|
+
className: getSegmentClassName({ appearance, size, variant, isSelected, disabled }),
|
|
121
|
+
children: option.icon ? /* @__PURE__ */ jsx("span", { className: "flex shrink-0 items-center justify-center", "aria-hidden": "true", children: option.icon }) : /* @__PURE__ */ jsx("span", { className: "min-w-0 truncate", children: option.label })
|
|
91
122
|
},
|
|
92
123
|
option.value
|
|
93
124
|
)
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"SegmentedControl.mjs","sources":["../../../src/components/SegmentedControl/SegmentedControl.tsx"],"sourcesContent":["import * as React from \"react\";\nimport { cn } from \"../../utils/cn\";\n\n/** Height of the segmented control in pixels. */\nexport type SegmentedControlSize = \"32\" | \"40\" | \"48\";\n\n/**\n * Segment layout.\n * - `\"hug\"`: each segment is sized to its content.\n * - `\"fill\"`: the control spans the full width of its container and each segment grows equally.\n */\nexport type SegmentedControlVariant = \"hug\" | \"fill\";\n\n/** Describes one selectable segment. */\nexport interface SegmentedControlOption {\n /** Display label for the segment. */\n label: string;\n /** Value identifier returned via `onChange`. */\n value: string;\n}\n\nexport interface SegmentedControlProps\n extends Omit<React.HTMLAttributes<HTMLDivElement>, \"onChange\"> {\n /** Height of the control in pixels. @default \"32\" */\n size?: SegmentedControlSize;\n /** Segment layout. @default \"hug\" */\n variant?: SegmentedControlVariant;\n /** The selectable segments. Designed for two or three mutually exclusive options. */\n options: SegmentedControlOption[];\n /** Currently selected value (controlled). */\n value?: string;\n /** Initially selected value (uncontrolled). Defaults to the first option. */\n defaultValue?: string;\n /** Callback fired when the selected value changes. */\n onChange?: (value: string) => void;\n /** Whether the control is disabled. @default false */\n disabled?: boolean;\n}\n\n/** Padding and typography per size. Combined with the container's `p-1` these hit the 32/40/48px heights. */\nconst sizeClasses: Record<SegmentedControlSize, string> = {\n \"32\": \"px-2 py-1 typography-description-12px-semibold\",\n \"40\": \"px-3 py-1.75 typography-body-small-14px-semibold\",\n \"48\": \"px-4 py-2 typography-body-default-16px-semibold\",\n};\n\nfunction warnMissingAccessibleName(ariaLabel?: string, ariaLabelledBy?: string) {\n if (process.env.NODE_ENV !== \"production\") {\n if (!ariaLabel && !ariaLabelledBy) {\n console.warn(\n \"SegmentedControl: no accessible name provided. Pass an `aria-label` or `aria-labelledby` prop.\",\n );\n }\n }\n}\n\n/**\n * A compact selector for choosing between two or three mutually exclusive\n * options where the choice affects the content immediately below it. Use\n * instead of tabs when the options are more like settings or filters than\n * navigation, such as toggling a list/grid view or a monthly/annual price.\n *\n * Rendered as a `radiogroup` with roving-tabindex keyboard navigation. Supports\n * both controlled and uncontrolled usage.\n *\n * @example\n * ```tsx\n * <SegmentedControl\n * options={[\n * { label: \"Net\", value: \"net\" },\n * { label: \"Gross\", value: \"gross\" },\n * ]}\n * value={amount}\n * onChange={setAmount}\n * aria-label=\"Amount type\"\n * />\n * ```\n */\nexport const SegmentedControl = React.forwardRef<HTMLDivElement, SegmentedControlProps>(\n (\n {\n className,\n size = \"32\",\n variant = \"hug\",\n options,\n value: controlledValue,\n defaultValue,\n onChange,\n disabled = false,\n ...props\n },\n ref,\n ) => {\n warnMissingAccessibleName(props[\"aria-label\"], props[\"aria-labelledby\"]);\n\n // Tracks selection for uncontrolled usage; ignored when `value` prop is provided\n const [internalValue, setInternalValue] = React.useState(defaultValue ?? options[0]?.value);\n const isControlled = controlledValue !== undefined;\n const currentValue = isControlled ? controlledValue : internalValue;\n const anySelected = options.some((o) => o.value === currentValue);\n const buttonRefs = React.useRef<(HTMLButtonElement | null)[]>([]);\n\n const handleSelect = (optionValue: string) => {\n if (disabled || optionValue === currentValue) return;\n if (!isControlled) {\n setInternalValue(optionValue);\n }\n onChange?.(optionValue);\n };\n\n const handleKeyDown = (e: React.KeyboardEvent, index: number) => {\n const nextIndex =\n e.key === \"ArrowRight\" || e.key === \"ArrowDown\"\n ? (index + 1) % options.length\n : e.key === \"ArrowLeft\" || e.key === \"ArrowUp\"\n ? (index - 1 + options.length) % options.length\n : null;\n if (nextIndex === null) return;\n e.preventDefault();\n const nextOption = options[nextIndex] as SegmentedControlOption;\n handleSelect(nextOption.value);\n buttonRefs.current[nextIndex]?.focus();\n };\n\n return (\n <div\n ref={ref}\n role=\"radiogroup\"\n className={cn(\n \"relative items-center rounded-full bg-surface-tertiary p-1\",\n variant === \"fill\" ? \"flex w-full\" : \"inline-flex\",\n disabled && \"cursor-not-allowed opacity-50\",\n className,\n )}\n {...props}\n >\n {options.map((option, index) => {\n const isSelected = currentValue === option.value;\n return (\n // biome-ignore lint/a11y/useSemanticElements: native radio inputs only allow Tab-focus on the checked item; buttons with roving tabindex give full keyboard navigation\n <button\n key={option.value}\n ref={(el) => {\n buttonRefs.current[index] = el;\n }}\n type=\"button\"\n role=\"radio\"\n aria-checked={isSelected}\n tabIndex={isSelected || (!anySelected && index === 0) ? 0 : -1}\n disabled={disabled}\n onClick={() => handleSelect(option.value)}\n onKeyDown={(e) => handleKeyDown(e, index)}\n className={cn(\n \"relative inline-flex min-w-0 cursor-pointer items-center justify-center rounded-full\",\n \"motion-safe:transition-colors motion-safe:duration-150 motion-safe:ease-in-out\",\n \"focus-visible:shadow-focus-ring focus-visible:outline-none\",\n variant === \"fill\" ? \"flex-1\" : \"shrink-0\",\n sizeClasses[size],\n isSelected\n ? \"bg-buttons-primary-default text-content-primary-inverted shadow-sm\"\n : \"text-content-primary hover:bg-buttons-switch-hover\",\n disabled && \"pointer-events-none\",\n )}\n >\n <span className=\"min-w-0 truncate\">{option.label}</span>\n </button>\n );\n })}\n </div>\n );\n },\n);\n\nSegmentedControl.displayName = \"SegmentedControl\";\n"],"names":[],"mappings":";;;;AAwCA,MAAM,cAAoD;AAAA,EACxD,MAAM;AAAA,EACN,MAAM;AAAA,EACN,MAAM;AACR;AAEA,SAAS,0BAA0B,WAAoB,gBAAyB;AAC9E,MAAI,QAAQ,IAAI,aAAa,cAAc;AACzC,QAAI,CAAC,aAAa,CAAC,gBAAgB;AACjC,cAAQ;AAAA,QACN;AAAA,MAAA;AAAA,IAEJ;AAAA,EACF;AACF;AAwBO,MAAM,mBAAmB,MAAM;AAAA,EACpC,CACE;AAAA,IACE;AAAA,IACA,OAAO;AAAA,IACP,UAAU;AAAA,IACV;AAAA,IACA,OAAO;AAAA,IACP;AAAA,IACA;AAAA,IACA,WAAW;AAAA,IACX,GAAG;AAAA,EAAA,GAEL,QACG;AACH,8BAA0B,MAAM,YAAY,GAAG,MAAM,iBAAiB,CAAC;AAGvE,UAAM,CAAC,eAAe,gBAAgB,IAAI,MAAM,SAAS,gBAAgB,QAAQ,CAAC,GAAG,KAAK;AAC1F,UAAM,eAAe,oBAAoB;AACzC,UAAM,eAAe,eAAe,kBAAkB;AACtD,UAAM,cAAc,QAAQ,KAAK,CAAC,MAAM,EAAE,UAAU,YAAY;AAChE,UAAM,aAAa,MAAM,OAAqC,EAAE;AAEhE,UAAM,eAAe,CAAC,gBAAwB;AAC5C,UAAI,YAAY,gBAAgB,aAAc;AAC9C,UAAI,CAAC,cAAc;AACjB,yBAAiB,WAAW;AAAA,MAC9B;AACA,iBAAW,WAAW;AAAA,IACxB;AAEA,UAAM,gBAAgB,CAAC,GAAwB,UAAkB;AAC/D,YAAM,YACJ,EAAE,QAAQ,gBAAgB,EAAE,QAAQ,eAC/B,QAAQ,KAAK,QAAQ,SACtB,EAAE,QAAQ,eAAe,EAAE,QAAQ,aAChC,QAAQ,IAAI,QAAQ,UAAU,QAAQ,SACvC;AACR,UAAI,cAAc,KAAM;AACxB,QAAE,eAAA;AACF,YAAM,aAAa,QAAQ,SAAS;AACpC,mBAAa,WAAW,KAAK;AAC7B,iBAAW,QAAQ,SAAS,GAAG,MAAA;AAAA,IACjC;AAEA,WACE;AAAA,MAAC;AAAA,MAAA;AAAA,QACC;AAAA,QACA,MAAK;AAAA,QACL,WAAW;AAAA,UACT;AAAA,UACA,YAAY,SAAS,gBAAgB;AAAA,UACrC,YAAY;AAAA,UACZ;AAAA,QAAA;AAAA,QAED,GAAG;AAAA,QAEH,UAAA,QAAQ,IAAI,CAAC,QAAQ,UAAU;AAC9B,gBAAM,aAAa,iBAAiB,OAAO;AAC3C;AAAA;AAAA,YAEE;AAAA,cAAC;AAAA,cAAA;AAAA,gBAEC,KAAK,CAAC,OAAO;AACX,6BAAW,QAAQ,KAAK,IAAI;AAAA,gBAC9B;AAAA,gBACA,MAAK;AAAA,gBACL,MAAK;AAAA,gBACL,gBAAc;AAAA,gBACd,UAAU,cAAe,CAAC,eAAe,UAAU,IAAK,IAAI;AAAA,gBAC5D;AAAA,gBACA,SAAS,MAAM,aAAa,OAAO,KAAK;AAAA,gBACxC,WAAW,CAAC,MAAM,cAAc,GAAG,KAAK;AAAA,gBACxC,WAAW;AAAA,kBACT;AAAA,kBACA;AAAA,kBACA;AAAA,kBACA,YAAY,SAAS,WAAW;AAAA,kBAChC,YAAY,IAAI;AAAA,kBAChB,aACI,uEACA;AAAA,kBACJ,YAAY;AAAA,gBAAA;AAAA,gBAGd,UAAA,oBAAC,QAAA,EAAK,WAAU,oBAAoB,iBAAO,MAAA,CAAM;AAAA,cAAA;AAAA,cAvB5C,OAAO;AAAA,YAAA;AAAA;AAAA,QA0BlB,CAAC;AAAA,MAAA;AAAA,IAAA;AAAA,EAGP;AACF;AAEA,iBAAiB,cAAc;"}
|
|
1
|
+
{"version":3,"file":"SegmentedControl.mjs","sources":["../../../src/components/SegmentedControl/SegmentedControl.tsx"],"sourcesContent":["import * as React from \"react\";\nimport { cn } from \"../../utils/cn\";\n\n/** Height of the segmented control in pixels. */\nexport type SegmentedControlSize = \"32\" | \"40\" | \"48\";\n\n/**\n * Segment layout.\n * - `\"hug\"`: each segment is sized to its content.\n * - `\"fill\"`: the control spans the full width of its container and each segment grows equally.\n */\nexport type SegmentedControlVariant = \"hug\" | \"fill\";\n\n/**\n * Visual style of the control.\n * - `\"pill\"`: the container has a muted background and the selected segment shows a filled pill (default).\n * - `\"plain\"`: no container or selected-pill background; segments are bare content and selection is\n * communicated by color alone. Designed for icon-only toggles (e.g. a list/grid view switch).\n */\nexport type SegmentedControlAppearance = \"pill\" | \"plain\";\n\n/** Describes one selectable segment. */\nexport interface SegmentedControlOption {\n /**\n * Display label for the segment. When `icon` is provided, the segment renders icon-only and\n * this value is used as its accessible name (applied as `aria-label`) instead of visible text.\n */\n label: string;\n /** Value identifier returned via `onChange`. */\n value: string;\n /**\n * Icon to render instead of the visible label. When set, the segment renders icon-only and\n * `label` becomes required as its accessible name — it is applied as `aria-label` and no\n * visible text is rendered.\n */\n icon?: React.ReactNode;\n}\n\nexport interface SegmentedControlProps\n extends Omit<React.HTMLAttributes<HTMLDivElement>, \"onChange\"> {\n /** Height of the control in pixels. @default \"32\" */\n size?: SegmentedControlSize;\n /** Segment layout. @default \"hug\" */\n variant?: SegmentedControlVariant;\n /** Visual style of the control. @default \"pill\" */\n appearance?: SegmentedControlAppearance;\n /** The selectable segments. Designed for two or three mutually exclusive options. */\n options: SegmentedControlOption[];\n /** Currently selected value (controlled). */\n value?: string;\n /** Initially selected value (uncontrolled). Defaults to the first option. */\n defaultValue?: string;\n /** Callback fired when the selected value changes. */\n onChange?: (value: string) => void;\n /** Whether the control is disabled. @default false */\n disabled?: boolean;\n}\n\n/** Padding and typography per size. Combined with the container's `p-1` these hit the 32/40/48px heights. */\nconst sizeClasses: Record<SegmentedControlSize, string> = {\n \"32\": \"px-2 py-1 typography-description-12px-semibold\",\n \"40\": \"px-3 py-1.75 typography-body-small-14px-semibold\",\n \"48\": \"px-4 py-2 typography-body-default-16px-semibold\",\n};\n\nfunction warnMissingAccessibleName(ariaLabel?: string, ariaLabelledBy?: string) {\n if (process.env.NODE_ENV !== \"production\") {\n if (!ariaLabel && !ariaLabelledBy) {\n console.warn(\n \"SegmentedControl: no accessible name provided. Pass an `aria-label` or `aria-labelledby` prop.\",\n );\n }\n }\n}\n\nfunction warnMissingOptionAccessibleName(options: SegmentedControlOption[]) {\n if (process.env.NODE_ENV !== \"production\") {\n for (const option of options) {\n if (option.icon && !option.label?.trim()) {\n console.warn(\n `SegmentedControl: icon-only segment \"${option.value}\" is missing a non-empty \\`label\\` to use as its accessible name.`,\n );\n }\n }\n }\n}\n\nfunction getSegmentClassName({\n appearance,\n size,\n variant,\n isSelected,\n disabled,\n}: {\n appearance: SegmentedControlAppearance;\n size: SegmentedControlSize;\n variant: SegmentedControlVariant;\n isSelected: boolean;\n disabled: boolean;\n}) {\n return cn(\n \"relative inline-flex min-w-0 cursor-pointer items-center justify-center rounded-full\",\n \"motion-safe:transition-colors motion-safe:duration-150 motion-safe:ease-in-out\",\n \"focus-visible:shadow-focus-ring focus-visible:outline-none\",\n variant === \"fill\" ? \"flex-1\" : \"shrink-0\",\n appearance === \"plain\"\n ? cn(\n // Padding + negative margin enlarge the hit target without changing the\n // visual footprint, which must stay glyph-only to match the design.\n \"-m-1 p-1\",\n isSelected ? \"text-icons-primary\" : \"text-icons-tertiary hover:text-icons-primary\",\n )\n : cn(\n sizeClasses[size],\n isSelected\n ? \"bg-buttons-primary-default text-content-primary-inverted shadow-sm\"\n : \"text-content-primary hover:bg-buttons-switch-hover\",\n ),\n disabled && \"pointer-events-none\",\n );\n}\n\n/**\n * A compact selector for choosing between two or three mutually exclusive\n * options where the choice affects the content immediately below it. Use\n * instead of tabs when the options are more like settings or filters than\n * navigation, such as toggling a list/grid view or a monthly/annual price.\n *\n * Rendered as a `radiogroup` with roving-tabindex keyboard navigation. Supports\n * both controlled and uncontrolled usage.\n *\n * @example\n * ```tsx\n * <SegmentedControl\n * options={[\n * { label: \"Net\", value: \"net\" },\n * { label: \"Gross\", value: \"gross\" },\n * ]}\n * value={amount}\n * onChange={setAmount}\n * aria-label=\"Amount type\"\n * />\n * ```\n *\n * @example Icon-only segments (e.g. a list/grid view toggle)\n * ```tsx\n * <SegmentedControl\n * appearance=\"plain\"\n * options={[\n * { label: \"List view\", value: \"list\", icon: <ListViewIcon size={16} /> },\n * { label: \"Grid view\", value: \"grid\", icon: <GridViewIcon size={16} /> },\n * ]}\n * value={view}\n * onChange={setView}\n * aria-label=\"View\"\n * />\n * ```\n */\nexport const SegmentedControl = React.forwardRef<HTMLDivElement, SegmentedControlProps>(\n (\n {\n className,\n size = \"32\",\n variant = \"hug\",\n appearance = \"pill\",\n options,\n value: controlledValue,\n defaultValue,\n onChange,\n disabled = false,\n ...props\n },\n ref,\n ) => {\n warnMissingAccessibleName(props[\"aria-label\"], props[\"aria-labelledby\"]);\n warnMissingOptionAccessibleName(options);\n\n // Tracks selection for uncontrolled usage; ignored when `value` prop is provided\n const [internalValue, setInternalValue] = React.useState(defaultValue ?? options[0]?.value);\n const isControlled = controlledValue !== undefined;\n const currentValue = isControlled ? controlledValue : internalValue;\n const anySelected = options.some((o) => o.value === currentValue);\n const buttonRefs = React.useRef<(HTMLButtonElement | null)[]>([]);\n\n const handleSelect = (optionValue: string) => {\n if (disabled || optionValue === currentValue) return;\n if (!isControlled) {\n setInternalValue(optionValue);\n }\n onChange?.(optionValue);\n };\n\n const handleKeyDown = (e: React.KeyboardEvent, index: number) => {\n const nextIndex =\n e.key === \"ArrowRight\" || e.key === \"ArrowDown\"\n ? (index + 1) % options.length\n : e.key === \"ArrowLeft\" || e.key === \"ArrowUp\"\n ? (index - 1 + options.length) % options.length\n : null;\n if (nextIndex === null) return;\n e.preventDefault();\n const nextOption = options[nextIndex] as SegmentedControlOption;\n handleSelect(nextOption.value);\n buttonRefs.current[nextIndex]?.focus();\n };\n\n return (\n <div\n ref={ref}\n role=\"radiogroup\"\n className={cn(\n \"relative items-center rounded-full\",\n variant === \"fill\" ? \"flex w-full\" : \"inline-flex\",\n appearance === \"plain\" ? \"gap-2\" : \"bg-surface-tertiary p-1\",\n disabled && \"cursor-not-allowed opacity-50\",\n className,\n )}\n {...props}\n >\n {options.map((option, index) => {\n const isSelected = currentValue === option.value;\n return (\n // biome-ignore lint/a11y/useSemanticElements: native radio inputs only allow Tab-focus on the checked item; buttons with roving tabindex give full keyboard navigation\n <button\n key={option.value}\n ref={(el) => {\n buttonRefs.current[index] = el;\n }}\n type=\"button\"\n role=\"radio\"\n aria-checked={isSelected}\n tabIndex={isSelected || (!anySelected && index === 0) ? 0 : -1}\n disabled={disabled}\n aria-label={option.icon ? option.label : undefined}\n onClick={() => handleSelect(option.value)}\n onKeyDown={(e) => handleKeyDown(e, index)}\n className={getSegmentClassName({ appearance, size, variant, isSelected, disabled })}\n >\n {option.icon ? (\n <span className=\"flex shrink-0 items-center justify-center\" aria-hidden=\"true\">\n {option.icon}\n </span>\n ) : (\n <span className=\"min-w-0 truncate\">{option.label}</span>\n )}\n </button>\n );\n })}\n </div>\n );\n },\n);\n\nSegmentedControl.displayName = \"SegmentedControl\";\n"],"names":[],"mappings":"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"}
|
|
@@ -32,8 +32,8 @@ const CLEAR_BUTTON_RIGHT = {
|
|
|
32
32
|
};
|
|
33
33
|
function getContainerClassName(size, error, disabled) {
|
|
34
34
|
return cn(
|
|
35
|
-
"relative rounded-sm border bg-
|
|
36
|
-
error ? "border-error-content" : "border-
|
|
35
|
+
"relative rounded-sm border bg-inputs-inputs-primary has-focus-visible:shadow-focus-ring has-focus-visible:outline-none motion-safe:transition-colors",
|
|
36
|
+
error ? "border-error-content" : "border-border-primary",
|
|
37
37
|
!disabled && !error && "hover:border-neutral-alphas-400",
|
|
38
38
|
CONTAINER_MIN_HEIGHT[size],
|
|
39
39
|
disabled && "opacity-50"
|
|
@@ -41,7 +41,7 @@ function getContainerClassName(size, error, disabled) {
|
|
|
41
41
|
}
|
|
42
42
|
function getTextareaClassName(size, hasClearButton, hasMinRows, resizable) {
|
|
43
43
|
return cn(
|
|
44
|
-
"h-full w-full bg-transparent text-content-primary no-underline placeholder:text-content-
|
|
44
|
+
"h-full w-full bg-transparent text-content-primary no-underline placeholder:text-content-tertiary focus:outline-none disabled:cursor-not-allowed",
|
|
45
45
|
resizable ? "resize-y" : "resize-none",
|
|
46
46
|
!hasMinRows && "min-h-[80px]",
|
|
47
47
|
TEXTAREA_SIZE_CLASSES[size],
|
|
@@ -160,7 +160,7 @@ const TextArea = React.forwardRef(
|
|
|
160
160
|
"label",
|
|
161
161
|
{
|
|
162
162
|
htmlFor: inputId,
|
|
163
|
-
className: "typography-description-12px-semibold
|
|
163
|
+
className: "typography-description-12px-semibold pb-2 text-content-primary",
|
|
164
164
|
children: label
|
|
165
165
|
}
|
|
166
166
|
),
|
|
@@ -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 /** 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-default-16px-regular autofill-body-lg\",\n \"40\": \"py-2 typography-body-default-16px-regular autofill-body-lg\",\n \"32\": \"py-2 typography-body-small-14px-regular autofill-body-md\",\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-sm border bg-neutral-alphas-50 has-focus-visible:shadow-focus-ring has-focus-visible:outline-none motion-safe:transition-colors\",\n error ? \"border-error-content\" : \"border-transparent\",\n !disabled && !error && \"hover:border-neutral-alphas-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 \"h-full w-full bg-transparent text-content-primary no-underline placeholder:text-content-secondary 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-description-12px-regular px-2 pt-1 pb-0.5\",\n error ? \"text-error-content\" : \"text-content-secondary\",\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 = false,\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-description-12px-semibold px-1 pt-1 pb-2 text-content-primary\"\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-content\" />\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-default-16px-regular autofill-body-lg\",\n \"40\": \"py-2 typography-body-default-16px-regular autofill-body-lg\",\n \"32\": \"py-2 typography-body-small-14px-regular autofill-body-md\",\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-sm border bg-inputs-inputs-primary has-focus-visible:shadow-focus-ring has-focus-visible:outline-none motion-safe:transition-colors\",\n error ? \"border-error-content\" : \"border-border-primary\",\n !disabled && !error && \"hover:border-neutral-alphas-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 \"h-full w-full bg-transparent text-content-primary no-underline placeholder:text-content-tertiary 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-description-12px-regular px-2 pt-1 pb-0.5\",\n error ? \"text-error-content\" : \"text-content-secondary\",\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 = false,\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-description-12px-semibold pb-2 text-content-primary\"\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-content\" />\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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"}
|
|
@@ -9,43 +9,71 @@ const CONTAINER_HEIGHT = {
|
|
|
9
9
|
"40": "h-10",
|
|
10
10
|
"32": "h-8"
|
|
11
11
|
};
|
|
12
|
-
const
|
|
13
|
-
"48": "py-3 typography-body-default-16px-regular autofill-body-lg",
|
|
14
|
-
"40": "py-2 typography-body-default-16px-regular autofill-body-lg",
|
|
15
|
-
"32": "py-2 typography-body-small-14px-regular autofill-body-md"
|
|
16
|
-
};
|
|
17
|
-
const INPUT_PL = {
|
|
18
|
-
"48": { default: "pl-4", withIcon: "pl-10" },
|
|
19
|
-
"40": { default: "pl-4", withIcon: "pl-10" },
|
|
20
|
-
"32": { default: "pl-3", withIcon: "pl-9" }
|
|
21
|
-
};
|
|
22
|
-
const INPUT_PR = {
|
|
23
|
-
"48": { default: "pr-4", withIcon: "pr-10" },
|
|
24
|
-
"40": { default: "pr-4", withIcon: "pr-10" },
|
|
25
|
-
"32": { default: "pr-3", withIcon: "pr-9" }
|
|
26
|
-
};
|
|
27
|
-
const ICON_INSET = {
|
|
12
|
+
const CONTAINER_PADDING_X = {
|
|
28
13
|
"48": "px-4",
|
|
29
14
|
"40": "px-4",
|
|
30
15
|
"32": "px-3"
|
|
31
16
|
};
|
|
17
|
+
const CONTAINER_GAP = {
|
|
18
|
+
"48": "gap-2.5",
|
|
19
|
+
"40": "gap-2.5",
|
|
20
|
+
"32": "gap-2"
|
|
21
|
+
};
|
|
22
|
+
const INPUT_TYPOGRAPHY = {
|
|
23
|
+
"48": "typography-body-default-16px-regular autofill-body-lg",
|
|
24
|
+
"40": "typography-body-default-16px-regular autofill-body-lg",
|
|
25
|
+
"32": "typography-body-small-14px-regular autofill-body-md"
|
|
26
|
+
};
|
|
27
|
+
const SIDE_LABEL_TYPOGRAPHY = {
|
|
28
|
+
"48": "typography-body-default-16px-regular",
|
|
29
|
+
"40": "typography-body-default-16px-regular",
|
|
30
|
+
"32": "typography-body-small-14px-regular"
|
|
31
|
+
};
|
|
32
32
|
function getContainerClassName(size, error, disabled) {
|
|
33
33
|
return cn(
|
|
34
|
-
"relative overflow-hidden rounded-sm border bg-inputs-inputs-primary has-focus-visible:shadow-focus-ring has-focus-visible:outline-none motion-safe:transition-colors",
|
|
34
|
+
"relative flex items-center overflow-hidden rounded-sm border bg-inputs-inputs-primary has-focus-visible:shadow-focus-ring has-focus-visible:outline-none motion-safe:transition-colors",
|
|
35
|
+
CONTAINER_PADDING_X[size],
|
|
36
|
+
CONTAINER_GAP[size],
|
|
35
37
|
error ? "border-error-content" : "border-border-primary",
|
|
36
38
|
!disabled && !error && "hover:border-neutral-alphas-400",
|
|
37
39
|
CONTAINER_HEIGHT[size],
|
|
38
40
|
disabled && "opacity-50"
|
|
39
41
|
);
|
|
40
42
|
}
|
|
41
|
-
function
|
|
42
|
-
return
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
43
|
+
function LeadingIcon({ children }) {
|
|
44
|
+
if (!children) return null;
|
|
45
|
+
return /* @__PURE__ */ jsx("span", { className: "pointer-events-none flex size-4 shrink-0 items-center justify-center text-content-secondary", children });
|
|
46
|
+
}
|
|
47
|
+
function SideLabel({
|
|
48
|
+
id,
|
|
49
|
+
size,
|
|
50
|
+
align,
|
|
51
|
+
children
|
|
52
|
+
}) {
|
|
53
|
+
if (!children) return null;
|
|
54
|
+
return /* @__PURE__ */ jsx(
|
|
55
|
+
"span",
|
|
56
|
+
{
|
|
57
|
+
id,
|
|
58
|
+
className: cn(
|
|
59
|
+
"shrink-0 select-none whitespace-nowrap text-content-tertiary",
|
|
60
|
+
align === "right" && "text-right",
|
|
61
|
+
SIDE_LABEL_TYPOGRAPHY[size]
|
|
62
|
+
),
|
|
63
|
+
children
|
|
64
|
+
}
|
|
47
65
|
);
|
|
48
66
|
}
|
|
67
|
+
function TrailingAdornment({
|
|
68
|
+
rightIcon,
|
|
69
|
+
validated
|
|
70
|
+
}) {
|
|
71
|
+
if (!rightIcon && !validated) return null;
|
|
72
|
+
return /* @__PURE__ */ jsxs("span", { className: "flex shrink-0 items-center gap-2 text-content-secondary", children: [
|
|
73
|
+
rightIcon && /* @__PURE__ */ jsx("span", { "data-tf-interactive": "", className: "flex size-4 shrink-0 items-center justify-center", children: rightIcon }),
|
|
74
|
+
validated && /* @__PURE__ */ jsx("span", { className: "pointer-events-none flex size-4 shrink-0 items-center justify-center", children: /* @__PURE__ */ jsx(CheckOutlineIcon, { className: "text-success-content" }) })
|
|
75
|
+
] });
|
|
76
|
+
}
|
|
49
77
|
function TextFieldHelperText({
|
|
50
78
|
id,
|
|
51
79
|
error,
|
|
@@ -82,6 +110,8 @@ const TextField = React.forwardRef(
|
|
|
82
110
|
validated = false,
|
|
83
111
|
leftIcon,
|
|
84
112
|
rightIcon,
|
|
113
|
+
leftLabel,
|
|
114
|
+
rightLabel,
|
|
85
115
|
className,
|
|
86
116
|
id,
|
|
87
117
|
disabled,
|
|
@@ -91,7 +121,31 @@ const TextField = React.forwardRef(
|
|
|
91
121
|
const generatedId = React.useId();
|
|
92
122
|
const inputId = id || generatedId;
|
|
93
123
|
const helperTextId = `${inputId}-helper`;
|
|
124
|
+
const leftLabelId = `${inputId}-left-label`;
|
|
125
|
+
const rightLabelId = `${inputId}-right-label`;
|
|
94
126
|
const bottomText = error && errorMessage ? errorMessage : helperText;
|
|
127
|
+
const describedBy = [
|
|
128
|
+
leftLabel != null ? leftLabelId : null,
|
|
129
|
+
rightLabel != null ? rightLabelId : null,
|
|
130
|
+
bottomText ? helperTextId : null
|
|
131
|
+
].filter(Boolean).join(" ") || void 0;
|
|
132
|
+
const innerRef = React.useRef(null);
|
|
133
|
+
const setRefs = React.useCallback(
|
|
134
|
+
(node) => {
|
|
135
|
+
innerRef.current = node;
|
|
136
|
+
if (typeof ref === "function") ref(node);
|
|
137
|
+
else if (ref) ref.current = node;
|
|
138
|
+
},
|
|
139
|
+
[ref]
|
|
140
|
+
);
|
|
141
|
+
const handleContainerMouseDown = (event) => {
|
|
142
|
+
if (disabled) return;
|
|
143
|
+
const target = event.target;
|
|
144
|
+
if (target === innerRef.current) return;
|
|
145
|
+
if (target.closest("[data-tf-interactive]")) return;
|
|
146
|
+
event.preventDefault();
|
|
147
|
+
innerRef.current?.focus();
|
|
148
|
+
};
|
|
95
149
|
warnMissingAccessibleName(label, props["aria-label"], props["aria-labelledby"]);
|
|
96
150
|
return /* @__PURE__ */ jsxs(
|
|
97
151
|
"div",
|
|
@@ -108,46 +162,35 @@ const TextField = React.forwardRef(
|
|
|
108
162
|
children: label
|
|
109
163
|
}
|
|
110
164
|
),
|
|
111
|
-
/* @__PURE__ */ jsxs(
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
children: /* @__PURE__ */ jsx("div", { className: "flex size-4 shrink-0 items-center justify-center", children: leftIcon })
|
|
135
|
-
}
|
|
136
|
-
),
|
|
137
|
-
(rightIcon || validated) && /* @__PURE__ */ jsxs(
|
|
138
|
-
"div",
|
|
139
|
-
{
|
|
140
|
-
className: cn(
|
|
141
|
-
"absolute inset-y-0 right-0 flex items-center gap-2 text-content-secondary",
|
|
142
|
-
ICON_INSET[size]
|
|
165
|
+
/* @__PURE__ */ jsxs(
|
|
166
|
+
"div",
|
|
167
|
+
{
|
|
168
|
+
className: getContainerClassName(size, error, disabled),
|
|
169
|
+
onMouseDown: handleContainerMouseDown,
|
|
170
|
+
children: [
|
|
171
|
+
/* @__PURE__ */ jsx(LeadingIcon, { children: leftIcon }),
|
|
172
|
+
/* @__PURE__ */ jsx(SideLabel, { id: leftLabelId, size, align: "left", children: leftLabel }),
|
|
173
|
+
/* @__PURE__ */ jsx(
|
|
174
|
+
"input",
|
|
175
|
+
{
|
|
176
|
+
ref: setRefs,
|
|
177
|
+
id: inputId,
|
|
178
|
+
disabled,
|
|
179
|
+
"aria-describedby": describedBy,
|
|
180
|
+
"aria-invalid": error || void 0,
|
|
181
|
+
className: cn(
|
|
182
|
+
"h-full min-w-0 flex-1 bg-transparent text-content-primary no-underline placeholder:text-content-tertiary focus:outline-none disabled:cursor-not-allowed",
|
|
183
|
+
INPUT_TYPOGRAPHY[size],
|
|
184
|
+
"[&[type='search']::-webkit-search-cancel-button]:hidden [&[type='search']::-webkit-search-cancel-button]:appearance-none"
|
|
185
|
+
),
|
|
186
|
+
...props
|
|
187
|
+
}
|
|
143
188
|
),
|
|
144
|
-
children:
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
)
|
|
150
|
-
] }),
|
|
189
|
+
/* @__PURE__ */ jsx(SideLabel, { id: rightLabelId, size, align: "right", children: rightLabel }),
|
|
190
|
+
/* @__PURE__ */ jsx(TrailingAdornment, { rightIcon, validated })
|
|
191
|
+
]
|
|
192
|
+
}
|
|
193
|
+
),
|
|
151
194
|
bottomText && /* @__PURE__ */ jsx(TextFieldHelperText, { id: helperTextId, error, children: bottomText })
|
|
152
195
|
]
|
|
153
196
|
}
|