@fanvue/ui 2.7.0 → 2.9.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/Alert/Alert.cjs +1 -1
- package/dist/cjs/components/Alert/Alert.cjs.map +1 -1
- package/dist/cjs/components/BottomNavigation/BottomNavigation.cjs +67 -4
- package/dist/cjs/components/BottomNavigation/BottomNavigation.cjs.map +1 -1
- package/dist/cjs/components/BottomNavigation/BottomNavigationAction.cjs +42 -1
- package/dist/cjs/components/BottomNavigation/BottomNavigationAction.cjs.map +1 -1
- package/dist/cjs/components/ChatInput/ChatInput.cjs +1 -1
- package/dist/cjs/components/ChatInput/ChatInput.cjs.map +1 -1
- package/dist/cjs/components/Tabs/TabsList.cjs +16 -2
- package/dist/cjs/components/Tabs/TabsList.cjs.map +1 -1
- package/dist/components/Alert/Alert.mjs +1 -1
- package/dist/components/Alert/Alert.mjs.map +1 -1
- package/dist/components/BottomNavigation/BottomNavigation.mjs +68 -5
- package/dist/components/BottomNavigation/BottomNavigation.mjs.map +1 -1
- package/dist/components/BottomNavigation/BottomNavigationAction.mjs +42 -1
- package/dist/components/BottomNavigation/BottomNavigationAction.mjs.map +1 -1
- package/dist/components/ChatInput/ChatInput.mjs +1 -1
- package/dist/components/ChatInput/ChatInput.mjs.map +1 -1
- package/dist/components/Tabs/TabsList.mjs +16 -2
- package/dist/components/Tabs/TabsList.mjs.map +1 -1
- package/dist/index.d.ts +19 -0
- package/dist/styles/base.css +17 -0
- package/dist/styles/theme.css +3 -3
- package/package.json +2 -2
|
@@ -73,7 +73,7 @@ const Alert = React__namespace.forwardRef(
|
|
|
73
73
|
),
|
|
74
74
|
...props,
|
|
75
75
|
children: [
|
|
76
|
-
resolvedIcon && /* @__PURE__ */ jsxRuntime.jsx("span", { className: "flex shrink-0 items-start", "aria-hidden": "true", children: resolvedIcon }),
|
|
76
|
+
resolvedIcon && /* @__PURE__ */ jsxRuntime.jsx("span", { className: "flex shrink-0 items-start h-full", "aria-hidden": "true", children: resolvedIcon }),
|
|
77
77
|
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex min-w-0 flex-col gap-2", children: [
|
|
78
78
|
title && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "typography-semibold-body-md text-content-primary", children: title }),
|
|
79
79
|
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "typography-regular-body-md text-content-primary", children })
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Alert.cjs","sources":["../../../../src/components/Alert/Alert.tsx"],"sourcesContent":["import * as React from \"react\";\nimport { cn } from \"../../utils/cn\";\nimport { Button } from \"../Button/Button\";\nimport { CheckCircleIcon } from \"../Icons/CheckCircleIcon\";\nimport { CrossIcon } from \"../Icons/CrossIcon\";\nimport { ErrorCircleIcon } from \"../Icons/ErrorCircleIcon\";\nimport { InfoCircleIcon } from \"../Icons/InfoCircleIcon\";\nimport { WarningTriangleIcon } from \"../Icons/WarningTriangleIcon\";\n\n/** Visual style variant of the alert. */\nexport type AlertVariant = \"info\" | \"success\" | \"warning\" | \"error\";\n\nconst DEFAULT_ICONS: Record<AlertVariant, React.ReactNode> = {\n info: <InfoCircleIcon />,\n success: <CheckCircleIcon />,\n warning: <WarningTriangleIcon />,\n error: <ErrorCircleIcon />,\n};\n\nexport interface AlertProps extends React.HTMLAttributes<HTMLDivElement> {\n /** Visual style variant of the alert. @default \"info\" */\n variant?: AlertVariant;\n /** Optional title text displayed in bold above the description. */\n title?: string;\n /** Custom icon override. Pass `null` to hide the icon entirely. Each variant shows a default icon when left `undefined`. */\n icon?: React.ReactNode | null;\n /** Whether to show the close button. @default false */\n closable?: boolean;\n /** Callback fired when the close button is clicked. */\n onClose?: () => void;\n /** Accessible label for the close button. @default \"Close alert\" */\n closeLabel?: string;\n}\n\nconst CLOSE_BUTTON_CLASSES: Record<AlertVariant, string> = {\n info: \"hover:bg-info-content/10 active:bg-info-content/20 text-info-content motion-safe:transition-colors motion-safe:duration-150\",\n success:\n \"hover:bg-success-content/10 active:bg-success-content/20 text-success-content motion-safe:transition-colors motion-safe:duration-150\",\n warning:\n \"hover:bg-warning-content/10 active:bg-warning-content/20 text-warning-content motion-safe:transition-colors motion-safe:duration-150\",\n error:\n \"hover:bg-error-content/10 active:bg-error-content/20 text-error-content motion-safe:transition-colors motion-safe:duration-150\",\n};\n\n/**\n * Displays a contextual feedback message to the user.\n *\n * Supports `info`, `success`, `warning`, and `error` variants with a default\n * icon per variant, optional title, description, and dismiss button.\n *\n * Each variant renders a default icon automatically. Pass a custom `icon` to\n * override, or `icon={null}` to hide the icon entirely.\n *\n * @example\n * ```tsx\n * <Alert variant=\"success\" title=\"Saved\" closable onClose={handleClose}>\n * Your changes have been saved.\n * </Alert>\n * ```\n */\nexport const Alert = React.forwardRef<HTMLDivElement, AlertProps>(\n (\n {\n className,\n variant = \"info\",\n title,\n icon,\n closable = false,\n onClose,\n closeLabel = \"Close alert\",\n children,\n ...props\n },\n ref,\n ) => {\n const resolvedIcon = icon === null ? null : (icon ?? DEFAULT_ICONS[variant]);\n\n return (\n <div\n ref={ref}\n role=\"alert\"\n data-testid=\"alert\"\n className={cn(\n \"grid gap-x-3 rounded-xs p-4 text-sm leading-[18px]\",\n resolvedIcon && closable && \"grid-cols-[auto_1fr_auto]\",\n resolvedIcon && !closable && \"grid-cols-[auto_1fr]\",\n !resolvedIcon && closable && \"grid-cols-[1fr_auto]\",\n !resolvedIcon && !closable && \"grid-cols-[1fr]\",\n title && children ? \"items-start\" : \"items-center\",\n variant === \"info\" && \"bg-info-surface text-info-content\",\n variant === \"success\" && \"bg-success-surface text-success-content\",\n variant === \"warning\" && \"bg-warning-surface text-warning-content\",\n variant === \"error\" && \"bg-error-surface text-error-content\",\n className,\n )}\n {...props}\n >\n {resolvedIcon && (\n <span className=\"flex shrink-0 items-start\" aria-hidden=\"true\">\n {resolvedIcon}\n </span>\n )}\n\n <div className=\"flex min-w-0 flex-col gap-2\">\n {title && <div className=\"typography-semibold-body-md text-content-primary\">{title}</div>}\n <div className=\"typography-regular-body-md text-content-primary\">{children}</div>\n </div>\n\n {closable && (\n <Button\n variant=\"tertiary\"\n size=\"24\"\n onClick={onClose}\n className={cn(\"self-start px-0\", CLOSE_BUTTON_CLASSES[variant])}\n aria-label={closeLabel}\n >\n <CrossIcon />\n </Button>\n )}\n </div>\n );\n },\n);\n\nAlert.displayName = \"Alert\";\n"],"names":["InfoCircleIcon","CheckCircleIcon","WarningTriangleIcon","ErrorCircleIcon","React","jsxs","cn","jsx","Button","CrossIcon"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAYA,MAAM,gBAAuD;AAAA,EAC3D,qCAAOA,eAAAA,gBAAA,EAAe;AAAA,EACtB,wCAAUC,gBAAAA,iBAAA,EAAgB;AAAA,EAC1B,wCAAUC,oBAAAA,qBAAA,EAAoB;AAAA,EAC9B,sCAAQC,gBAAAA,iBAAA,CAAA,CAAgB;AAC1B;AAiBA,MAAM,uBAAqD;AAAA,EACzD,MAAM;AAAA,EACN,SACE;AAAA,EACF,SACE;AAAA,EACF,OACE;AACJ;AAkBO,MAAM,QAAQC,iBAAM;AAAA,EACzB,CACE;AAAA,IACE;AAAA,IACA,UAAU;AAAA,IACV;AAAA,IACA;AAAA,IACA,WAAW;AAAA,IACX;AAAA,IACA,aAAa;AAAA,IACb;AAAA,IACA,GAAG;AAAA,EAAA,GAEL,QACG;AACH,UAAM,eAAe,SAAS,OAAO,OAAQ,QAAQ,cAAc,OAAO;AAE1E,WACEC,2BAAAA;AAAAA,MAAC;AAAA,MAAA;AAAA,QACC;AAAA,QACA,MAAK;AAAA,QACL,eAAY;AAAA,QACZ,WAAWC,GAAAA;AAAAA,UACT;AAAA,UACA,gBAAgB,YAAY;AAAA,UAC5B,gBAAgB,CAAC,YAAY;AAAA,UAC7B,CAAC,gBAAgB,YAAY;AAAA,UAC7B,CAAC,gBAAgB,CAAC,YAAY;AAAA,UAC9B,SAAS,WAAW,gBAAgB;AAAA,UACpC,YAAY,UAAU;AAAA,UACtB,YAAY,aAAa;AAAA,UACzB,YAAY,aAAa;AAAA,UACzB,YAAY,WAAW;AAAA,UACvB;AAAA,QAAA;AAAA,QAED,GAAG;AAAA,QAEH,UAAA;AAAA,UAAA,+CACE,QAAA,EAAK,WAAU,
|
|
1
|
+
{"version":3,"file":"Alert.cjs","sources":["../../../../src/components/Alert/Alert.tsx"],"sourcesContent":["import * as React from \"react\";\nimport { cn } from \"../../utils/cn\";\nimport { Button } from \"../Button/Button\";\nimport { CheckCircleIcon } from \"../Icons/CheckCircleIcon\";\nimport { CrossIcon } from \"../Icons/CrossIcon\";\nimport { ErrorCircleIcon } from \"../Icons/ErrorCircleIcon\";\nimport { InfoCircleIcon } from \"../Icons/InfoCircleIcon\";\nimport { WarningTriangleIcon } from \"../Icons/WarningTriangleIcon\";\n\n/** Visual style variant of the alert. */\nexport type AlertVariant = \"info\" | \"success\" | \"warning\" | \"error\";\n\nconst DEFAULT_ICONS: Record<AlertVariant, React.ReactNode> = {\n info: <InfoCircleIcon />,\n success: <CheckCircleIcon />,\n warning: <WarningTriangleIcon />,\n error: <ErrorCircleIcon />,\n};\n\nexport interface AlertProps extends React.HTMLAttributes<HTMLDivElement> {\n /** Visual style variant of the alert. @default \"info\" */\n variant?: AlertVariant;\n /** Optional title text displayed in bold above the description. */\n title?: string;\n /** Custom icon override. Pass `null` to hide the icon entirely. Each variant shows a default icon when left `undefined`. */\n icon?: React.ReactNode | null;\n /** Whether to show the close button. @default false */\n closable?: boolean;\n /** Callback fired when the close button is clicked. */\n onClose?: () => void;\n /** Accessible label for the close button. @default \"Close alert\" */\n closeLabel?: string;\n}\n\nconst CLOSE_BUTTON_CLASSES: Record<AlertVariant, string> = {\n info: \"hover:bg-info-content/10 active:bg-info-content/20 text-info-content motion-safe:transition-colors motion-safe:duration-150\",\n success:\n \"hover:bg-success-content/10 active:bg-success-content/20 text-success-content motion-safe:transition-colors motion-safe:duration-150\",\n warning:\n \"hover:bg-warning-content/10 active:bg-warning-content/20 text-warning-content motion-safe:transition-colors motion-safe:duration-150\",\n error:\n \"hover:bg-error-content/10 active:bg-error-content/20 text-error-content motion-safe:transition-colors motion-safe:duration-150\",\n};\n\n/**\n * Displays a contextual feedback message to the user.\n *\n * Supports `info`, `success`, `warning`, and `error` variants with a default\n * icon per variant, optional title, description, and dismiss button.\n *\n * Each variant renders a default icon automatically. Pass a custom `icon` to\n * override, or `icon={null}` to hide the icon entirely.\n *\n * @example\n * ```tsx\n * <Alert variant=\"success\" title=\"Saved\" closable onClose={handleClose}>\n * Your changes have been saved.\n * </Alert>\n * ```\n */\nexport const Alert = React.forwardRef<HTMLDivElement, AlertProps>(\n (\n {\n className,\n variant = \"info\",\n title,\n icon,\n closable = false,\n onClose,\n closeLabel = \"Close alert\",\n children,\n ...props\n },\n ref,\n ) => {\n const resolvedIcon = icon === null ? null : (icon ?? DEFAULT_ICONS[variant]);\n\n return (\n <div\n ref={ref}\n role=\"alert\"\n data-testid=\"alert\"\n className={cn(\n \"grid gap-x-3 rounded-xs p-4 text-sm leading-[18px]\",\n resolvedIcon && closable && \"grid-cols-[auto_1fr_auto]\",\n resolvedIcon && !closable && \"grid-cols-[auto_1fr]\",\n !resolvedIcon && closable && \"grid-cols-[1fr_auto]\",\n !resolvedIcon && !closable && \"grid-cols-[1fr]\",\n title && children ? \"items-start\" : \"items-center\",\n variant === \"info\" && \"bg-info-surface text-info-content\",\n variant === \"success\" && \"bg-success-surface text-success-content\",\n variant === \"warning\" && \"bg-warning-surface text-warning-content\",\n variant === \"error\" && \"bg-error-surface text-error-content\",\n className,\n )}\n {...props}\n >\n {resolvedIcon && (\n <span className=\"flex shrink-0 items-start h-full\" aria-hidden=\"true\">\n {resolvedIcon}\n </span>\n )}\n\n <div className=\"flex min-w-0 flex-col gap-2\">\n {title && <div className=\"typography-semibold-body-md text-content-primary\">{title}</div>}\n <div className=\"typography-regular-body-md text-content-primary\">{children}</div>\n </div>\n\n {closable && (\n <Button\n variant=\"tertiary\"\n size=\"24\"\n onClick={onClose}\n className={cn(\"self-start px-0\", CLOSE_BUTTON_CLASSES[variant])}\n aria-label={closeLabel}\n >\n <CrossIcon />\n </Button>\n )}\n </div>\n );\n },\n);\n\nAlert.displayName = \"Alert\";\n"],"names":["InfoCircleIcon","CheckCircleIcon","WarningTriangleIcon","ErrorCircleIcon","React","jsxs","cn","jsx","Button","CrossIcon"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAYA,MAAM,gBAAuD;AAAA,EAC3D,qCAAOA,eAAAA,gBAAA,EAAe;AAAA,EACtB,wCAAUC,gBAAAA,iBAAA,EAAgB;AAAA,EAC1B,wCAAUC,oBAAAA,qBAAA,EAAoB;AAAA,EAC9B,sCAAQC,gBAAAA,iBAAA,CAAA,CAAgB;AAC1B;AAiBA,MAAM,uBAAqD;AAAA,EACzD,MAAM;AAAA,EACN,SACE;AAAA,EACF,SACE;AAAA,EACF,OACE;AACJ;AAkBO,MAAM,QAAQC,iBAAM;AAAA,EACzB,CACE;AAAA,IACE;AAAA,IACA,UAAU;AAAA,IACV;AAAA,IACA;AAAA,IACA,WAAW;AAAA,IACX;AAAA,IACA,aAAa;AAAA,IACb;AAAA,IACA,GAAG;AAAA,EAAA,GAEL,QACG;AACH,UAAM,eAAe,SAAS,OAAO,OAAQ,QAAQ,cAAc,OAAO;AAE1E,WACEC,2BAAAA;AAAAA,MAAC;AAAA,MAAA;AAAA,QACC;AAAA,QACA,MAAK;AAAA,QACL,eAAY;AAAA,QACZ,WAAWC,GAAAA;AAAAA,UACT;AAAA,UACA,gBAAgB,YAAY;AAAA,UAC5B,gBAAgB,CAAC,YAAY;AAAA,UAC7B,CAAC,gBAAgB,YAAY;AAAA,UAC7B,CAAC,gBAAgB,CAAC,YAAY;AAAA,UAC9B,SAAS,WAAW,gBAAgB;AAAA,UACpC,YAAY,UAAU;AAAA,UACtB,YAAY,aAAa;AAAA,UACzB,YAAY,aAAa;AAAA,UACzB,YAAY,WAAW;AAAA,UACvB;AAAA,QAAA;AAAA,QAED,GAAG;AAAA,QAEH,UAAA;AAAA,UAAA,+CACE,QAAA,EAAK,WAAU,oCAAmC,eAAY,QAC5D,UAAA,cACH;AAAA,UAGFD,2BAAAA,KAAC,OAAA,EAAI,WAAU,+BACZ,UAAA;AAAA,YAAA,SAASE,2BAAAA,IAAC,OAAA,EAAI,WAAU,oDAAoD,UAAA,OAAM;AAAA,YACnFA,2BAAAA,IAAC,OAAA,EAAI,WAAU,mDAAmD,SAAA,CAAS;AAAA,UAAA,GAC7E;AAAA,UAEC,YACCA,2BAAAA;AAAAA,YAACC,OAAAA;AAAAA,YAAA;AAAA,cACC,SAAQ;AAAA,cACR,MAAK;AAAA,cACL,SAAS;AAAA,cACT,WAAWF,GAAAA,GAAG,mBAAmB,qBAAqB,OAAO,CAAC;AAAA,cAC9D,cAAY;AAAA,cAEZ,yCAACG,UAAAA,WAAA,CAAA,CAAU;AAAA,YAAA;AAAA,UAAA;AAAA,QACb;AAAA,MAAA;AAAA,IAAA;AAAA,EAIR;AACF;AAEA,MAAM,cAAc;;"}
|
|
@@ -21,16 +21,79 @@ function _interopNamespaceDefault(e) {
|
|
|
21
21
|
return Object.freeze(n);
|
|
22
22
|
}
|
|
23
23
|
const React__namespace = /* @__PURE__ */ _interopNamespaceDefault(React);
|
|
24
|
-
const BottomNavigationContext = React__namespace.createContext({
|
|
24
|
+
const BottomNavigationContext = React__namespace.createContext({
|
|
25
|
+
hasInformationArchitectureNav: false
|
|
26
|
+
});
|
|
25
27
|
function useBottomNavigationContext() {
|
|
26
28
|
return React__namespace.useContext(BottomNavigationContext);
|
|
27
29
|
}
|
|
30
|
+
function resolveChildren(children, value) {
|
|
31
|
+
const items = React__namespace.Children.toArray(children);
|
|
32
|
+
for (let i = 0; i < items.length; i++) {
|
|
33
|
+
const child = items[i];
|
|
34
|
+
if (React__namespace.isValidElement(child) && child.props.value === value) {
|
|
35
|
+
return { count: items.length, activeIndex: i };
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return { count: items.length, activeIndex: void 0 };
|
|
39
|
+
}
|
|
28
40
|
const BottomNavigation = React__namespace.forwardRef(
|
|
29
|
-
({
|
|
41
|
+
({
|
|
42
|
+
className,
|
|
43
|
+
children,
|
|
44
|
+
value,
|
|
45
|
+
onValueChange,
|
|
46
|
+
hasInformationArchitectureNav = false,
|
|
47
|
+
hideOnDesktop = false,
|
|
48
|
+
...props
|
|
49
|
+
}, ref) => {
|
|
30
50
|
const contextValue = React__namespace.useMemo(
|
|
31
|
-
() => ({ value, onValueChange }),
|
|
32
|
-
[value, onValueChange]
|
|
51
|
+
() => ({ value, onValueChange, hasInformationArchitectureNav }),
|
|
52
|
+
[value, onValueChange, hasInformationArchitectureNav]
|
|
33
53
|
);
|
|
54
|
+
if (hasInformationArchitectureNav) {
|
|
55
|
+
const { count: itemCount, activeIndex } = resolveChildren(children, value);
|
|
56
|
+
const { style, ...restProps } = props;
|
|
57
|
+
return /* @__PURE__ */ jsxRuntime.jsx(BottomNavigationContext.Provider, { value: contextValue, children: /* @__PURE__ */ jsxRuntime.jsxs(
|
|
58
|
+
"nav",
|
|
59
|
+
{
|
|
60
|
+
ref,
|
|
61
|
+
...restProps,
|
|
62
|
+
className: cn.cn(
|
|
63
|
+
"fixed inset-x-0 bottom-0 flex h-[calc(env(safe-area-inset-bottom,0px)+80px)] items-stretch justify-around pb-[env(safe-area-inset-bottom,0px)]",
|
|
64
|
+
"border-neutral-alphas-200 border-t bg-bg-primary",
|
|
65
|
+
hideOnDesktop && "md:hidden",
|
|
66
|
+
className
|
|
67
|
+
),
|
|
68
|
+
style: { zIndex: "var(--fanvue-ui-portal-z-index, 50)", ...style },
|
|
69
|
+
children: [
|
|
70
|
+
activeIndex != null && /* @__PURE__ */ jsxRuntime.jsx(
|
|
71
|
+
"div",
|
|
72
|
+
{
|
|
73
|
+
"aria-hidden": "true",
|
|
74
|
+
"data-part": "indicator",
|
|
75
|
+
className: "pointer-events-none absolute inset-x-0 -top-px h-0",
|
|
76
|
+
children: /* @__PURE__ */ jsxRuntime.jsxs(
|
|
77
|
+
"div",
|
|
78
|
+
{
|
|
79
|
+
className: "absolute top-0 flex -translate-x-1/2 flex-col items-center motion-safe:transition-[left] motion-safe:duration-300 motion-safe:ease-in-out",
|
|
80
|
+
style: {
|
|
81
|
+
left: `calc((${activeIndex} + 0.5) * (100% / ${itemCount}))`,
|
|
82
|
+
width: `calc(100% / ${itemCount})`
|
|
83
|
+
},
|
|
84
|
+
children: [
|
|
85
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "h-px w-full bg-linear-to-r from-transparent via-(--color-special-bottom-nav-highlight) to-transparent" }),
|
|
86
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "h-20 w-full overflow-hidden", children: /* @__PURE__ */ jsxRuntime.jsx("div", { className: "mx-auto aspect-square w-[70%] max-w-[70px] -translate-y-1/2 rounded-full bg-(--color-special-bottom-nav-highlight) opacity-30 blur-[20px] dark:opacity-15" }) })
|
|
87
|
+
]
|
|
88
|
+
}
|
|
89
|
+
)
|
|
90
|
+
}
|
|
91
|
+
),
|
|
92
|
+
children
|
|
93
|
+
]
|
|
94
|
+
}
|
|
95
|
+
) });
|
|
96
|
+
}
|
|
34
97
|
return /* @__PURE__ */ jsxRuntime.jsx(BottomNavigationContext.Provider, { value: contextValue, children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
35
98
|
"nav",
|
|
36
99
|
{
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"BottomNavigation.cjs","sources":["../../../../src/components/BottomNavigation/BottomNavigation.tsx"],"sourcesContent":["import * as React from \"react\";\nimport { cn } from \"../../utils/cn\";\n\nexport interface BottomNavigationProps extends React.HTMLAttributes<HTMLElement> {\n /** The currently selected action value. */\n value?: string;\n /** Called when the selected action changes. */\n onValueChange?: (value: string) => void;\n /** When `true`, the navigation bar is hidden on viewports wider than `md` (768 px). @default false */\n hideOnDesktop?: boolean;\n}\n\ninterface BottomNavigationContextValue {\n value?: string;\n onValueChange?: (value: string) => void;\n}\n\nconst BottomNavigationContext = React.createContext<BottomNavigationContextValue>({});\n\nexport function useBottomNavigationContext(): BottomNavigationContextValue {\n return React.useContext(BottomNavigationContext);\n}\n\nexport const BottomNavigation = React.forwardRef<HTMLElement, BottomNavigationProps>(\n ({
|
|
1
|
+
{"version":3,"file":"BottomNavigation.cjs","sources":["../../../../src/components/BottomNavigation/BottomNavigation.tsx"],"sourcesContent":["import * as React from \"react\";\nimport { cn } from \"../../utils/cn\";\n\nexport interface BottomNavigationProps extends React.HTMLAttributes<HTMLElement> {\n /** The currently selected action value. */\n value?: string;\n /** Called when the selected action changes. */\n onValueChange?: (value: string) => void;\n /** When `true`, the navigation bar is hidden on viewports wider than `md` (768 px). @default false */\n hideOnDesktop?: boolean;\n /** When `true`, renders the information-architecture style with visible labels and a sliding active indicator. @default false */\n hasInformationArchitectureNav?: boolean;\n}\n\ninterface BottomNavigationContextValue {\n value?: string;\n onValueChange?: (value: string) => void;\n hasInformationArchitectureNav: boolean;\n}\n\nconst BottomNavigationContext = React.createContext<BottomNavigationContextValue>({\n hasInformationArchitectureNav: false,\n});\n\nexport function useBottomNavigationContext(): BottomNavigationContextValue {\n return React.useContext(BottomNavigationContext);\n}\n\nfunction resolveChildren(children: React.ReactNode, value: string | undefined) {\n const items = React.Children.toArray(children);\n for (let i = 0; i < items.length; i++) {\n const child = items[i];\n if (React.isValidElement<{ value?: string }>(child) && child.props.value === value) {\n return { count: items.length, activeIndex: i };\n }\n }\n return { count: items.length, activeIndex: undefined };\n}\n\nexport const BottomNavigation = React.forwardRef<HTMLElement, BottomNavigationProps>(\n (\n {\n className,\n children,\n value,\n onValueChange,\n hasInformationArchitectureNav = false,\n hideOnDesktop = false,\n ...props\n },\n ref,\n ) => {\n const contextValue = React.useMemo<BottomNavigationContextValue>(\n () => ({ value, onValueChange, hasInformationArchitectureNav }),\n [value, onValueChange, hasInformationArchitectureNav],\n );\n\n if (hasInformationArchitectureNav) {\n const { count: itemCount, activeIndex } = resolveChildren(children, value);\n const { style, ...restProps } = props;\n\n return (\n <BottomNavigationContext.Provider value={contextValue}>\n <nav\n ref={ref}\n {...restProps}\n className={cn(\n \"fixed inset-x-0 bottom-0 flex h-[calc(env(safe-area-inset-bottom,0px)+80px)] items-stretch justify-around pb-[env(safe-area-inset-bottom,0px)]\",\n \"border-neutral-alphas-200 border-t bg-bg-primary\",\n hideOnDesktop && \"md:hidden\",\n className,\n )}\n style={\n { zIndex: \"var(--fanvue-ui-portal-z-index, 50)\", ...style } as React.CSSProperties\n }\n >\n {activeIndex != null && (\n <div\n aria-hidden=\"true\"\n data-part=\"indicator\"\n className=\"pointer-events-none absolute inset-x-0 -top-px h-0\"\n >\n <div\n className=\"absolute top-0 flex -translate-x-1/2 flex-col items-center motion-safe:transition-[left] motion-safe:duration-300 motion-safe:ease-in-out\"\n style={{\n left: `calc((${activeIndex} + 0.5) * (100% / ${itemCount}))`,\n width: `calc(100% / ${itemCount})`,\n }}\n >\n <div className=\"h-px w-full bg-linear-to-r from-transparent via-(--color-special-bottom-nav-highlight) to-transparent\" />\n <div className=\"h-20 w-full overflow-hidden\">\n <div className=\"mx-auto aspect-square w-[70%] max-w-[70px] -translate-y-1/2 rounded-full bg-(--color-special-bottom-nav-highlight) opacity-30 blur-[20px] dark:opacity-15\" />\n </div>\n </div>\n </div>\n )}\n {children}\n </nav>\n </BottomNavigationContext.Provider>\n );\n }\n\n return (\n <BottomNavigationContext.Provider value={contextValue}>\n <nav\n ref={ref}\n className={cn(\n \"fixed inset-x-0 bottom-0\",\n \"flex h-[calc(env(safe-area-inset-bottom,0px)+68px)] items-center justify-around\",\n \"border-neutral-alphas-200 border-t bg-bg-primary\",\n \"pb-[env(safe-area-inset-bottom,0px)]\",\n hideOnDesktop && \"md:hidden\",\n className,\n )}\n style={{ zIndex: \"var(--fanvue-ui-portal-z-index, 50)\", ...props.style }}\n {...props}\n >\n {children}\n </nav>\n </BottomNavigationContext.Provider>\n );\n },\n);\n\nBottomNavigation.displayName = \"BottomNavigation\";\n"],"names":["React","jsx","jsxs","cn"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;AAoBA,MAAM,0BAA0BA,iBAAM,cAA4C;AAAA,EAChF,+BAA+B;AACjC,CAAC;AAEM,SAAS,6BAA2D;AACzE,SAAOA,iBAAM,WAAW,uBAAuB;AACjD;AAEA,SAAS,gBAAgB,UAA2B,OAA2B;AAC7E,QAAM,QAAQA,iBAAM,SAAS,QAAQ,QAAQ;AAC7C,WAAS,IAAI,GAAG,IAAI,MAAM,QAAQ,KAAK;AACrC,UAAM,QAAQ,MAAM,CAAC;AACrB,QAAIA,iBAAM,eAAmC,KAAK,KAAK,MAAM,MAAM,UAAU,OAAO;AAClF,aAAO,EAAE,OAAO,MAAM,QAAQ,aAAa,EAAA;AAAA,IAC7C;AAAA,EACF;AACA,SAAO,EAAE,OAAO,MAAM,QAAQ,aAAa,OAAA;AAC7C;AAEO,MAAM,mBAAmBA,iBAAM;AAAA,EACpC,CACE;AAAA,IACE;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA,gCAAgC;AAAA,IAChC,gBAAgB;AAAA,IAChB,GAAG;AAAA,EAAA,GAEL,QACG;AACH,UAAM,eAAeA,iBAAM;AAAA,MACzB,OAAO,EAAE,OAAO,eAAe;MAC/B,CAAC,OAAO,eAAe,6BAA6B;AAAA,IAAA;AAGtD,QAAI,+BAA+B;AACjC,YAAM,EAAE,OAAO,WAAW,gBAAgB,gBAAgB,UAAU,KAAK;AACzE,YAAM,EAAE,OAAO,GAAG,UAAA,IAAc;AAEhC,aACEC,2BAAAA,IAAC,wBAAwB,UAAxB,EAAiC,OAAO,cACvC,UAAAC,2BAAAA;AAAAA,QAAC;AAAA,QAAA;AAAA,UACC;AAAA,UACC,GAAG;AAAA,UACJ,WAAWC,GAAAA;AAAAA,YACT;AAAA,YACA;AAAA,YACA,iBAAiB;AAAA,YACjB;AAAA,UAAA;AAAA,UAEF,OACE,EAAE,QAAQ,uCAAuC,GAAG,MAAA;AAAA,UAGrD,UAAA;AAAA,YAAA,eAAe,QACdF,2BAAAA;AAAAA,cAAC;AAAA,cAAA;AAAA,gBACC,eAAY;AAAA,gBACZ,aAAU;AAAA,gBACV,WAAU;AAAA,gBAEV,UAAAC,2BAAAA;AAAAA,kBAAC;AAAA,kBAAA;AAAA,oBACC,WAAU;AAAA,oBACV,OAAO;AAAA,sBACL,MAAM,SAAS,WAAW,qBAAqB,SAAS;AAAA,sBACxD,OAAO,eAAe,SAAS;AAAA,oBAAA;AAAA,oBAGjC,UAAA;AAAA,sBAAAD,2BAAAA,IAAC,OAAA,EAAI,WAAU,wGAAA,CAAwG;AAAA,sBACvHA,2BAAAA,IAAC,SAAI,WAAU,+BACb,yCAAC,OAAA,EAAI,WAAU,6JAA4J,EAAA,CAC7K;AAAA,oBAAA;AAAA,kBAAA;AAAA,gBAAA;AAAA,cACF;AAAA,YAAA;AAAA,YAGH;AAAA,UAAA;AAAA,QAAA;AAAA,MAAA,GAEL;AAAA,IAEJ;AAEA,WACEA,2BAAAA,IAAC,wBAAwB,UAAxB,EAAiC,OAAO,cACvC,UAAAA,2BAAAA;AAAAA,MAAC;AAAA,MAAA;AAAA,QACC;AAAA,QACA,WAAWE,GAAAA;AAAAA,UACT;AAAA,UACA;AAAA,UACA;AAAA,UACA;AAAA,UACA,iBAAiB;AAAA,UACjB;AAAA,QAAA;AAAA,QAEF,OAAO,EAAE,QAAQ,uCAAuC,GAAG,MAAM,MAAA;AAAA,QAChE,GAAG;AAAA,QAEH;AAAA,MAAA;AAAA,IAAA,GAEL;AAAA,EAEJ;AACF;AAEA,iBAAiB,cAAc;;;"}
|
|
@@ -24,13 +24,54 @@ function _interopNamespaceDefault(e) {
|
|
|
24
24
|
}
|
|
25
25
|
const React__namespace = /* @__PURE__ */ _interopNamespaceDefault(React);
|
|
26
26
|
const BottomNavigationAction = React__namespace.forwardRef(({ className, value, icon, label, badge, onClick, asChild = false, children, ...props }, ref) => {
|
|
27
|
-
const {
|
|
27
|
+
const {
|
|
28
|
+
value: selectedValue,
|
|
29
|
+
onValueChange,
|
|
30
|
+
hasInformationArchitectureNav
|
|
31
|
+
} = BottomNavigation.useBottomNavigationContext();
|
|
28
32
|
const isActive = selectedValue === value;
|
|
29
33
|
const handleClick = (e) => {
|
|
30
34
|
onValueChange?.(value);
|
|
31
35
|
onClick?.(e);
|
|
32
36
|
};
|
|
33
37
|
const Comp = asChild ? reactSlot.Slot : "button";
|
|
38
|
+
if (hasInformationArchitectureNav) {
|
|
39
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(
|
|
40
|
+
Comp,
|
|
41
|
+
{
|
|
42
|
+
ref,
|
|
43
|
+
...!asChild && { type: "button" },
|
|
44
|
+
"aria-current": isActive ? "page" : void 0,
|
|
45
|
+
"data-state": isActive ? "active" : "inactive",
|
|
46
|
+
onClick: handleClick,
|
|
47
|
+
...props,
|
|
48
|
+
className: cn.cn(
|
|
49
|
+
"relative flex min-w-0 flex-1 cursor-pointer flex-col items-center justify-center gap-1 px-2",
|
|
50
|
+
isActive ? "text-icons-primary" : "text-icons-tertiary",
|
|
51
|
+
"motion-safe:transition-colors motion-safe:duration-150 motion-safe:ease-in-out",
|
|
52
|
+
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-interaction-focus focus-visible:ring-offset-2 focus-visible:ring-offset-bg-primary",
|
|
53
|
+
className
|
|
54
|
+
),
|
|
55
|
+
children: [
|
|
56
|
+
asChild && /* @__PURE__ */ jsxRuntime.jsx(reactSlot.Slottable, { children }),
|
|
57
|
+
/* @__PURE__ */ jsxRuntime.jsxs("span", { className: "relative inline-flex", children: [
|
|
58
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "flex items-center justify-center [&>svg]:size-6", "aria-hidden": "true", children: icon }),
|
|
59
|
+
badge && /* @__PURE__ */ jsxRuntime.jsx("span", { className: "absolute -end-1 -top-2.5", children: badge })
|
|
60
|
+
] }),
|
|
61
|
+
label && /* @__PURE__ */ jsxRuntime.jsx(
|
|
62
|
+
"span",
|
|
63
|
+
{
|
|
64
|
+
className: cn.cn(
|
|
65
|
+
"typography-medium-caption-xs truncate text-center motion-safe:transition-colors motion-safe:duration-150 motion-safe:ease-in-out",
|
|
66
|
+
isActive ? "text-content-primary" : "text-content-tertiary"
|
|
67
|
+
),
|
|
68
|
+
children: label
|
|
69
|
+
}
|
|
70
|
+
)
|
|
71
|
+
]
|
|
72
|
+
}
|
|
73
|
+
);
|
|
74
|
+
}
|
|
34
75
|
return /* @__PURE__ */ jsxRuntime.jsxs(
|
|
35
76
|
Comp,
|
|
36
77
|
{
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"BottomNavigationAction.cjs","sources":["../../../../src/components/BottomNavigation/BottomNavigationAction.tsx"],"sourcesContent":["import { Slot, Slottable } from \"@radix-ui/react-slot\";\nimport * as React from \"react\";\nimport { cn } from \"../../utils/cn\";\nimport { useBottomNavigationContext } from \"./BottomNavigation\";\n\nexport interface BottomNavigationActionProps\n extends Omit<React.ButtonHTMLAttributes<HTMLButtonElement>, \"value\"> {\n /** Unique value that identifies this action. */\n value: string;\n /** Icon element displayed in the action. */\n icon: React.ReactElement;\n /** Accessible label applied as `aria-label`. */\n label?: string;\n /** Optional badge element (e.g. {@link Count}) rendered at the top-end corner of the icon. */\n badge?: React.ReactNode;\n /** Merge props onto a child element instead of rendering a `<button>`. @default false */\n asChild?: boolean;\n}\n\nexport const BottomNavigationAction = React.forwardRef<\n HTMLButtonElement,\n BottomNavigationActionProps\n>(({ className, value, icon, label, badge, onClick, asChild = false, children, ...props }, ref) => {\n const {
|
|
1
|
+
{"version":3,"file":"BottomNavigationAction.cjs","sources":["../../../../src/components/BottomNavigation/BottomNavigationAction.tsx"],"sourcesContent":["import { Slot, Slottable } from \"@radix-ui/react-slot\";\nimport * as React from \"react\";\nimport { cn } from \"../../utils/cn\";\nimport { useBottomNavigationContext } from \"./BottomNavigation\";\n\nexport interface BottomNavigationActionProps\n extends Omit<React.ButtonHTMLAttributes<HTMLButtonElement>, \"value\"> {\n /** Unique value that identifies this action. */\n value: string;\n /** Icon element displayed in the action. */\n icon: React.ReactElement;\n /** Accessible label applied as `aria-label`. */\n label?: string;\n /** Optional badge element (e.g. {@link Count}) rendered at the top-end corner of the icon. */\n badge?: React.ReactNode;\n /** Merge props onto a child element instead of rendering a `<button>`. @default false */\n asChild?: boolean;\n}\n\nexport const BottomNavigationAction = React.forwardRef<\n HTMLButtonElement,\n BottomNavigationActionProps\n>(({ className, value, icon, label, badge, onClick, asChild = false, children, ...props }, ref) => {\n const {\n value: selectedValue,\n onValueChange,\n hasInformationArchitectureNav,\n } = useBottomNavigationContext();\n\n const isActive = selectedValue === value;\n\n const handleClick = (e: React.MouseEvent<HTMLButtonElement>) => {\n onValueChange?.(value);\n onClick?.(e);\n };\n\n const Comp = asChild ? Slot : \"button\";\n\n if (hasInformationArchitectureNav) {\n return (\n <Comp\n ref={ref}\n {...(!asChild && { type: \"button\" as const })}\n aria-current={isActive ? (\"page\" as const) : undefined}\n data-state={isActive ? \"active\" : \"inactive\"}\n onClick={handleClick}\n {...props}\n className={cn(\n \"relative flex min-w-0 flex-1 cursor-pointer flex-col items-center justify-center gap-1 px-2\",\n isActive ? \"text-icons-primary\" : \"text-icons-tertiary\",\n \"motion-safe:transition-colors motion-safe:duration-150 motion-safe:ease-in-out\",\n \"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-interaction-focus focus-visible:ring-offset-2 focus-visible:ring-offset-bg-primary\",\n className,\n )}\n >\n {asChild && <Slottable>{children}</Slottable>}\n <span className=\"relative inline-flex\">\n <span className=\"flex items-center justify-center [&>svg]:size-6\" aria-hidden=\"true\">\n {icon}\n </span>\n {badge && <span className=\"absolute -end-1 -top-2.5\">{badge}</span>}\n </span>\n {label && (\n <span\n className={cn(\n \"typography-medium-caption-xs truncate text-center motion-safe:transition-colors motion-safe:duration-150 motion-safe:ease-in-out\",\n isActive ? \"text-content-primary\" : \"text-content-tertiary\",\n )}\n >\n {label}\n </span>\n )}\n </Comp>\n );\n }\n\n return (\n <Comp\n ref={ref}\n {...(!asChild && { type: \"button\" as const })}\n aria-current={isActive ? \"page\" : undefined}\n aria-label={label}\n data-state={isActive ? \"active\" : \"inactive\"}\n className={cn(\n \"relative flex min-w-0 flex-1 cursor-pointer flex-col items-center justify-center gap-0.5 overflow-hidden px-2 py-2\",\n \"text-content-primary\",\n \"motion-safe:transition-colors motion-safe:duration-150 motion-safe:ease-in-out\",\n \"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-interaction-focus focus-visible:ring-offset-2 focus-visible:ring-offset-bg-primary\",\n className,\n )}\n onClick={handleClick}\n {...props}\n >\n {asChild && <Slottable>{children}</Slottable>}\n <span className=\"relative inline-flex\">\n <span className=\"flex items-center justify-center [&>svg]:size-7\" aria-hidden=\"true\">\n {icon}\n </span>\n {badge && <span className=\"absolute -end-1 -top-2.5\">{badge}</span>}\n </span>\n </Comp>\n );\n});\n\nBottomNavigationAction.displayName = \"BottomNavigationAction\";\n"],"names":["React","useBottomNavigationContext","Slot","jsxs","cn","jsx","Slottable"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;AAmBO,MAAM,yBAAyBA,iBAAM,WAG1C,CAAC,EAAE,WAAW,OAAO,MAAM,OAAO,OAAO,SAAS,UAAU,OAAO,UAAU,GAAG,MAAA,GAAS,QAAQ;AACjG,QAAM;AAAA,IACJ,OAAO;AAAA,IACP;AAAA,IACA;AAAA,EAAA,IACEC,4CAAA;AAEJ,QAAM,WAAW,kBAAkB;AAEnC,QAAM,cAAc,CAAC,MAA2C;AAC9D,oBAAgB,KAAK;AACrB,cAAU,CAAC;AAAA,EACb;AAEA,QAAM,OAAO,UAAUC,UAAAA,OAAO;AAE9B,MAAI,+BAA+B;AACjC,WACEC,2BAAAA;AAAAA,MAAC;AAAA,MAAA;AAAA,QACC;AAAA,QACC,GAAI,CAAC,WAAW,EAAE,MAAM,SAAA;AAAA,QACzB,gBAAc,WAAY,SAAmB;AAAA,QAC7C,cAAY,WAAW,WAAW;AAAA,QAClC,SAAS;AAAA,QACR,GAAG;AAAA,QACJ,WAAWC,GAAAA;AAAAA,UACT;AAAA,UACA,WAAW,uBAAuB;AAAA,UAClC;AAAA,UACA;AAAA,UACA;AAAA,QAAA;AAAA,QAGD,UAAA;AAAA,UAAA,WAAWC,2BAAAA,IAACC,uBAAW,SAAA,CAAS;AAAA,UACjCH,2BAAAA,KAAC,QAAA,EAAK,WAAU,wBACd,UAAA;AAAA,YAAAE,+BAAC,QAAA,EAAK,WAAU,mDAAkD,eAAY,QAC3E,UAAA,MACH;AAAA,YACC,SAASA,2BAAAA,IAAC,QAAA,EAAK,WAAU,4BAA4B,UAAA,MAAA,CAAM;AAAA,UAAA,GAC9D;AAAA,UACC,SACCA,2BAAAA;AAAAA,YAAC;AAAA,YAAA;AAAA,cACC,WAAWD,GAAAA;AAAAA,gBACT;AAAA,gBACA,WAAW,yBAAyB;AAAA,cAAA;AAAA,cAGrC,UAAA;AAAA,YAAA;AAAA,UAAA;AAAA,QACH;AAAA,MAAA;AAAA,IAAA;AAAA,EAIR;AAEA,SACED,2BAAAA;AAAAA,IAAC;AAAA,IAAA;AAAA,MACC;AAAA,MACC,GAAI,CAAC,WAAW,EAAE,MAAM,SAAA;AAAA,MACzB,gBAAc,WAAW,SAAS;AAAA,MAClC,cAAY;AAAA,MACZ,cAAY,WAAW,WAAW;AAAA,MAClC,WAAWC,GAAAA;AAAAA,QACT;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,MAAA;AAAA,MAEF,SAAS;AAAA,MACR,GAAG;AAAA,MAEH,UAAA;AAAA,QAAA,WAAWC,2BAAAA,IAACC,uBAAW,SAAA,CAAS;AAAA,QACjCH,2BAAAA,KAAC,QAAA,EAAK,WAAU,wBACd,UAAA;AAAA,UAAAE,+BAAC,QAAA,EAAK,WAAU,mDAAkD,eAAY,QAC3E,UAAA,MACH;AAAA,UACC,SAASA,2BAAAA,IAAC,QAAA,EAAK,WAAU,4BAA4B,UAAA,MAAA,CAAM;AAAA,QAAA,EAAA,CAC9D;AAAA,MAAA;AAAA,IAAA;AAAA,EAAA;AAGN,CAAC;AAED,uBAAuB,cAAc;;"}
|
|
@@ -226,7 +226,7 @@ function InlineSelect({ options, value, onChange, disabled, selectedOption }) {
|
|
|
226
226
|
onClick: () => setOpen((prev) => !prev),
|
|
227
227
|
className: cn.cn(
|
|
228
228
|
"typography-semibold-body-sm text-content-primary",
|
|
229
|
-
"flex items-center gap-1 rounded-
|
|
229
|
+
"flex items-center gap-1 rounded-md px-2 py-2",
|
|
230
230
|
"hover:bg-neutral-alphas-50 focus-visible:shadow-focus-ring focus-visible:outline-none",
|
|
231
231
|
"disabled:cursor-not-allowed disabled:opacity-50",
|
|
232
232
|
"motion-safe:transition-colors"
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ChatInput.cjs","sources":["../../../../src/components/ChatInput/ChatInput.tsx"],"sourcesContent":["import * as React from \"react\";\nimport { cn } from \"../../utils/cn\";\nimport { IconButton } from \"../IconButton/IconButton\";\nimport { AddIcon } from \"../Icons/AddIcon\";\nimport { ArrowUpIcon } from \"../Icons/ArrowUpIcon\";\nimport { ChevronDownIcon } from \"../Icons/ChevronDownIcon\";\n\n/** A single option for the inline model/dropdown selector. */\nexport interface ChatInputSelectOption {\n /** Unique value for this option. */\n value: string;\n /** Display label. */\n label: string;\n /** Optional icon rendered to the left of the label. */\n icon?: React.ReactNode;\n}\n\n/**\n * Props for {@link ChatInput}. Standard textarea HTML attributes are forwarded to the inner\n * `<textarea>` except `className` (applied to the outer container), `rows` (use `minRows`), and\n * `onSubmit` (replaced by the chat message submit callback).\n */\nexport interface ChatInputProps\n extends Omit<\n React.TextareaHTMLAttributes<HTMLTextAreaElement>,\n \"className\" | \"rows\" | \"onSubmit\"\n > {\n /** Minimum number of visible rows. @default 1 */\n minRows?: number;\n /** Maximum number of visible rows before scrolling. @default 6 */\n maxRows?: number;\n /** Whether a submission is in progress (disables submit, shows visual feedback). @default false */\n loading?: boolean;\n /**\n * Callback fired when the user submits (clicks the send button or presses Enter without Shift).\n * Receives the current trimmed text value.\n */\n onSubmit?: (value: string) => void;\n /**\n * When `true`, renders an \"attach file\" button in the bottom-left toolbar.\n * @default false\n */\n showFileButton?: boolean;\n /** Callback fired when the attach-file button is clicked. Only relevant when `showFileButton` is `true`. */\n onFileClick?: () => void;\n /** Accessible label for the attach-file button. @default \"Attach file\" */\n fileButtonAriaLabel?: string;\n /** Accessible label for the submit button. @default \"Send message\" */\n submitAriaLabel?: string;\n /** Icon element for the submit button. @default `<ArrowUpIcon />` */\n submitIcon?: React.ReactNode;\n /**\n * Optional content rendered in the bottom-right toolbar, to the left of the submit button.\n * When provided, takes precedence over the built-in `selectOptions` dropdown.\n */\n toolbarRight?: React.ReactNode;\n /**\n * Options for the built-in inline dropdown selector (e.g. model picker).\n * Ignored when `toolbarRight` is provided.\n */\n selectOptions?: ChatInputSelectOption[];\n /** Currently selected value for the built-in dropdown. Should match one of `selectOptions[].value`. */\n selectValue?: string;\n /** Callback fired when the user picks a different dropdown option. */\n onSelectChange?: (value: string) => void;\n /** Additional className applied to the outermost container. */\n className?: string;\n}\n\nconst LINE_HEIGHT = 18;\nconst TEXTAREA_PY = 12;\n\nfunction calculateHeight(rows: number): number {\n return LINE_HEIGHT * rows + TEXTAREA_PY * 2;\n}\n\n/**\n * A chat-style multi-line input with an integrated toolbar containing an\n * optional file-attach button, optional right-side controls (e.g. a model\n * selector), and a submit button — all inside a single bordered container.\n *\n * Designed to behave like modern AI chat inputs: auto-grows with content,\n * submits on Enter (Shift+Enter for newlines), and keeps controls inline.\n *\n * @example\n * ```tsx\n * <ChatInput\n * placeholder=\"Type a message...\"\n * onSubmit={(text) => send(text)}\n * />\n * ```\n *\n * @example\n * ```tsx\n * <ChatInput\n * placeholder=\"Ask the agent...\"\n * showFileButton\n * onFileClick={() => openFilePicker()}\n * selectOptions={[\n * { value: \"fanvue-ai\", label: \"Fanvue AI\", icon: <AIIcon className=\"size-4\" /> },\n * { value: \"example\", label: \"Example\", icon: <BulbIcon className=\"size-4\" /> },\n * ]}\n * selectValue=\"fanvue-ai\"\n * onSelectChange={(v) => setModel(v)}\n * onSubmit={(text) => send(text)}\n * />\n * ```\n */\nexport const ChatInput = React.forwardRef<HTMLTextAreaElement, ChatInputProps>(\n (\n {\n className,\n minRows = 1,\n maxRows = 6,\n disabled = false,\n loading = false,\n value,\n defaultValue,\n placeholder,\n maxLength,\n \"aria-label\": ariaLabel,\n onChange,\n onKeyDown,\n onSubmit,\n showFileButton = false,\n onFileClick,\n fileButtonAriaLabel = \"Attach file\",\n submitAriaLabel = \"Send message\",\n submitIcon,\n toolbarRight,\n selectOptions,\n selectValue,\n onSelectChange,\n style,\n ...textareaProps\n },\n ref,\n ) => {\n const internalRef = React.useRef<HTMLTextAreaElement>(null);\n const [internalValue, setInternalValue] = React.useState(defaultValue ?? \"\");\n const resolvedValue = value !== undefined ? value : internalValue;\n const isControlled = value !== undefined;\n\n const mergedRef = React.useCallback(\n (node: HTMLTextAreaElement | null) => {\n (internalRef as React.MutableRefObject<HTMLTextAreaElement | null>).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 [ref],\n );\n\n const adjustHeight = React.useCallback(() => {\n const textarea = internalRef.current;\n if (!textarea) return;\n\n const minHeight = calculateHeight(minRows);\n const maxHeight = calculateHeight(maxRows);\n\n textarea.style.height = `${minHeight}px`;\n const desired = Math.min(Math.max(textarea.scrollHeight, minHeight), maxHeight);\n textarea.style.height = `${desired}px`;\n }, [minRows, maxRows]);\n\n React.useEffect(() => {\n adjustHeight();\n }, [resolvedValue, adjustHeight]);\n\n const handleChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {\n if (!isControlled) {\n setInternalValue(e.target.value);\n }\n onChange?.(e);\n };\n\n const canSubmit = !!String(resolvedValue).trim() && !disabled && !loading;\n\n const handleSubmit = () => {\n const text = String(resolvedValue).trim();\n if (!text || !canSubmit) return;\n onSubmit?.(text);\n if (!isControlled) {\n setInternalValue(\"\");\n }\n };\n\n const handleKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {\n if (e.key === \"Enter\" && !e.shiftKey) {\n e.preventDefault();\n handleSubmit();\n }\n onKeyDown?.(e);\n };\n\n const minHeight = calculateHeight(minRows);\n const maxHeight = calculateHeight(maxRows);\n\n const selectedOption =\n selectOptions?.find((o) => o.value === selectValue) ?? selectOptions?.[0];\n const resolvedToolbarRight =\n toolbarRight ??\n (selectOptions && selectOptions.length > 0 ? (\n <InlineSelect\n options={selectOptions}\n value={selectValue}\n onChange={onSelectChange}\n disabled={disabled}\n selectedOption={selectedOption}\n />\n ) : null);\n\n return (\n <div\n className={cn(\n \"relative flex flex-col gap-6 rounded-lg border border-border-primary bg-surface-primary\",\n \"has-focus-visible:border-neutral-alphas-400 has-focus-visible:outline-none\",\n \"motion-safe:transition-colors\",\n disabled && \"opacity-50\",\n className,\n )}\n >\n <textarea\n {...textareaProps}\n ref={mergedRef}\n value={isControlled ? value : internalValue}\n placeholder={placeholder}\n maxLength={maxLength}\n disabled={disabled}\n aria-label={ariaLabel ?? placeholder}\n onChange={handleChange}\n onKeyDown={handleKeyDown}\n rows={minRows}\n className={cn(\n \"w-full resize-none bg-transparent px-4 pt-4\",\n \"typography-regular-body-md text-content-primary\",\n \"placeholder:text-content-tertiary\",\n \"focus:outline-none disabled:cursor-not-allowed\",\n \"overflow-y-auto\",\n )}\n style={{\n minHeight: `${minHeight}px`,\n maxHeight: `${maxHeight}px`,\n ...style,\n }}\n />\n\n <div className=\"flex items-center justify-between gap-2 px-4 pb-4\">\n <div className=\"flex items-center gap-1\">\n {showFileButton && (\n <IconButton\n variant=\"tertiary\"\n size=\"32\"\n icon={<AddIcon />}\n aria-label={fileButtonAriaLabel}\n onClick={onFileClick}\n disabled={disabled}\n className=\"sm:border sm:border-border-primary max-sm:-ml-2\"\n />\n )}\n </div>\n\n <div className=\"flex items-center gap-1\">\n {resolvedToolbarRight}\n <IconButton\n variant=\"primary\"\n size=\"32\"\n icon={submitIcon ?? <ArrowUpIcon />}\n aria-label={submitAriaLabel}\n onClick={handleSubmit}\n disabled={!canSubmit}\n className=\"disabled:bg-surface-secondary disabled:opacity-100 disabled:text-icons-primary\"\n />\n </div>\n </div>\n </div>\n );\n },\n);\n\nChatInput.displayName = \"ChatInput\";\n\ninterface InlineSelectProps {\n options: ChatInputSelectOption[];\n value?: string;\n onChange?: (value: string) => void;\n disabled?: boolean;\n selectedOption?: ChatInputSelectOption;\n}\n\nfunction InlineSelect({ options, value, onChange, disabled, selectedOption }: InlineSelectProps) {\n const [open, setOpen] = React.useState(false);\n const containerRef = React.useRef<HTMLDivElement>(null);\n\n React.useEffect(() => {\n if (!open) return;\n const handleClick = (e: MouseEvent) => {\n if (containerRef.current && !containerRef.current.contains(e.target as Node)) {\n setOpen(false);\n }\n };\n document.addEventListener(\"mousedown\", handleClick);\n return () => document.removeEventListener(\"mousedown\", handleClick);\n }, [open]);\n\n React.useEffect(() => {\n if (!open) return;\n const handleKey = (e: KeyboardEvent) => {\n if (e.key === \"Escape\") setOpen(false);\n };\n document.addEventListener(\"keydown\", handleKey);\n return () => document.removeEventListener(\"keydown\", handleKey);\n }, [open]);\n\n return (\n <div ref={containerRef} className=\"relative\">\n <button\n type=\"button\"\n role=\"combobox\"\n aria-expanded={open}\n aria-haspopup=\"listbox\"\n aria-label=\"Select model\"\n disabled={disabled}\n onClick={() => setOpen((prev) => !prev)}\n className={cn(\n \"typography-semibold-body-sm text-content-primary\",\n \"flex items-center gap-1 rounded-sm px-2 py-1\",\n \"hover:bg-neutral-alphas-50 focus-visible:shadow-focus-ring focus-visible:outline-none\",\n \"disabled:cursor-not-allowed disabled:opacity-50\",\n \"motion-safe:transition-colors\",\n )}\n >\n {selectedOption?.icon && (\n <span className=\"flex shrink-0 items-center [&>svg]:size-4\">{selectedOption.icon}</span>\n )}\n {selectedOption?.label ?? options[0]?.label ?? \"Select\"}\n <ChevronDownIcon\n className={cn(\"size-4 motion-safe:transition-transform\", open && \"rotate-180\")}\n />\n </button>\n\n {open && (\n <div\n role=\"listbox\"\n className={cn(\n \"absolute right-0 bottom-full z-10 mb-1 min-w-[140px]\",\n \"overflow-hidden rounded-xs border border-border-primary bg-surface-primary p-1 shadow-lg\",\n )}\n >\n {options.map((option) => (\n <div\n key={option.value}\n role=\"option\"\n tabIndex={0}\n aria-selected={option.value === value}\n className={cn(\n \"typography-regular-body-md flex cursor-pointer items-center gap-2 rounded-xs px-3 py-1.5\",\n \"text-content-primary hover:bg-neutral-alphas-50\",\n \"focus-visible:shadow-focus-ring focus-visible:outline-none\",\n option.value === value && \"bg-neutral-alphas-50\",\n )}\n onClick={() => {\n onChange?.(option.value);\n setOpen(false);\n }}\n onKeyDown={(e) => {\n if (e.key === \"Enter\" || e.key === \" \") {\n e.preventDefault();\n onChange?.(option.value);\n setOpen(false);\n }\n }}\n >\n {option.icon && (\n <span className=\"flex shrink-0 items-center [&>svg]:size-4\">{option.icon}</span>\n )}\n {option.label}\n </div>\n ))}\n </div>\n )}\n </div>\n );\n}\n"],"names":["React","minHeight","maxHeight","jsx","jsxs","cn","IconButton","AddIcon","ArrowUpIcon","ChevronDownIcon"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"ChatInput.cjs","sources":["../../../../src/components/ChatInput/ChatInput.tsx"],"sourcesContent":["import * as React from \"react\";\nimport { cn } from \"../../utils/cn\";\nimport { IconButton } from \"../IconButton/IconButton\";\nimport { AddIcon } from \"../Icons/AddIcon\";\nimport { ArrowUpIcon } from \"../Icons/ArrowUpIcon\";\nimport { ChevronDownIcon } from \"../Icons/ChevronDownIcon\";\n\n/** A single option for the inline model/dropdown selector. */\nexport interface ChatInputSelectOption {\n /** Unique value for this option. */\n value: string;\n /** Display label. */\n label: string;\n /** Optional icon rendered to the left of the label. */\n icon?: React.ReactNode;\n}\n\n/**\n * Props for {@link ChatInput}. Standard textarea HTML attributes are forwarded to the inner\n * `<textarea>` except `className` (applied to the outer container), `rows` (use `minRows`), and\n * `onSubmit` (replaced by the chat message submit callback).\n */\nexport interface ChatInputProps\n extends Omit<\n React.TextareaHTMLAttributes<HTMLTextAreaElement>,\n \"className\" | \"rows\" | \"onSubmit\"\n > {\n /** Minimum number of visible rows. @default 1 */\n minRows?: number;\n /** Maximum number of visible rows before scrolling. @default 6 */\n maxRows?: number;\n /** Whether a submission is in progress (disables submit, shows visual feedback). @default false */\n loading?: boolean;\n /**\n * Callback fired when the user submits (clicks the send button or presses Enter without Shift).\n * Receives the current trimmed text value.\n */\n onSubmit?: (value: string) => void;\n /**\n * When `true`, renders an \"attach file\" button in the bottom-left toolbar.\n * @default false\n */\n showFileButton?: boolean;\n /** Callback fired when the attach-file button is clicked. Only relevant when `showFileButton` is `true`. */\n onFileClick?: () => void;\n /** Accessible label for the attach-file button. @default \"Attach file\" */\n fileButtonAriaLabel?: string;\n /** Accessible label for the submit button. @default \"Send message\" */\n submitAriaLabel?: string;\n /** Icon element for the submit button. @default `<ArrowUpIcon />` */\n submitIcon?: React.ReactNode;\n /**\n * Optional content rendered in the bottom-right toolbar, to the left of the submit button.\n * When provided, takes precedence over the built-in `selectOptions` dropdown.\n */\n toolbarRight?: React.ReactNode;\n /**\n * Options for the built-in inline dropdown selector (e.g. model picker).\n * Ignored when `toolbarRight` is provided.\n */\n selectOptions?: ChatInputSelectOption[];\n /** Currently selected value for the built-in dropdown. Should match one of `selectOptions[].value`. */\n selectValue?: string;\n /** Callback fired when the user picks a different dropdown option. */\n onSelectChange?: (value: string) => void;\n /** Additional className applied to the outermost container. */\n className?: string;\n}\n\nconst LINE_HEIGHT = 18;\nconst TEXTAREA_PY = 12;\n\nfunction calculateHeight(rows: number): number {\n return LINE_HEIGHT * rows + TEXTAREA_PY * 2;\n}\n\n/**\n * A chat-style multi-line input with an integrated toolbar containing an\n * optional file-attach button, optional right-side controls (e.g. a model\n * selector), and a submit button — all inside a single bordered container.\n *\n * Designed to behave like modern AI chat inputs: auto-grows with content,\n * submits on Enter (Shift+Enter for newlines), and keeps controls inline.\n *\n * @example\n * ```tsx\n * <ChatInput\n * placeholder=\"Type a message...\"\n * onSubmit={(text) => send(text)}\n * />\n * ```\n *\n * @example\n * ```tsx\n * <ChatInput\n * placeholder=\"Ask the agent...\"\n * showFileButton\n * onFileClick={() => openFilePicker()}\n * selectOptions={[\n * { value: \"fanvue-ai\", label: \"Fanvue AI\", icon: <AIIcon className=\"size-4\" /> },\n * { value: \"example\", label: \"Example\", icon: <BulbIcon className=\"size-4\" /> },\n * ]}\n * selectValue=\"fanvue-ai\"\n * onSelectChange={(v) => setModel(v)}\n * onSubmit={(text) => send(text)}\n * />\n * ```\n */\nexport const ChatInput = React.forwardRef<HTMLTextAreaElement, ChatInputProps>(\n (\n {\n className,\n minRows = 1,\n maxRows = 6,\n disabled = false,\n loading = false,\n value,\n defaultValue,\n placeholder,\n maxLength,\n \"aria-label\": ariaLabel,\n onChange,\n onKeyDown,\n onSubmit,\n showFileButton = false,\n onFileClick,\n fileButtonAriaLabel = \"Attach file\",\n submitAriaLabel = \"Send message\",\n submitIcon,\n toolbarRight,\n selectOptions,\n selectValue,\n onSelectChange,\n style,\n ...textareaProps\n },\n ref,\n ) => {\n const internalRef = React.useRef<HTMLTextAreaElement>(null);\n const [internalValue, setInternalValue] = React.useState(defaultValue ?? \"\");\n const resolvedValue = value !== undefined ? value : internalValue;\n const isControlled = value !== undefined;\n\n const mergedRef = React.useCallback(\n (node: HTMLTextAreaElement | null) => {\n (internalRef as React.MutableRefObject<HTMLTextAreaElement | null>).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 [ref],\n );\n\n const adjustHeight = React.useCallback(() => {\n const textarea = internalRef.current;\n if (!textarea) return;\n\n const minHeight = calculateHeight(minRows);\n const maxHeight = calculateHeight(maxRows);\n\n textarea.style.height = `${minHeight}px`;\n const desired = Math.min(Math.max(textarea.scrollHeight, minHeight), maxHeight);\n textarea.style.height = `${desired}px`;\n }, [minRows, maxRows]);\n\n React.useEffect(() => {\n adjustHeight();\n }, [resolvedValue, adjustHeight]);\n\n const handleChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {\n if (!isControlled) {\n setInternalValue(e.target.value);\n }\n onChange?.(e);\n };\n\n const canSubmit = !!String(resolvedValue).trim() && !disabled && !loading;\n\n const handleSubmit = () => {\n const text = String(resolvedValue).trim();\n if (!text || !canSubmit) return;\n onSubmit?.(text);\n if (!isControlled) {\n setInternalValue(\"\");\n }\n };\n\n const handleKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {\n if (e.key === \"Enter\" && !e.shiftKey) {\n e.preventDefault();\n handleSubmit();\n }\n onKeyDown?.(e);\n };\n\n const minHeight = calculateHeight(minRows);\n const maxHeight = calculateHeight(maxRows);\n\n const selectedOption =\n selectOptions?.find((o) => o.value === selectValue) ?? selectOptions?.[0];\n const resolvedToolbarRight =\n toolbarRight ??\n (selectOptions && selectOptions.length > 0 ? (\n <InlineSelect\n options={selectOptions}\n value={selectValue}\n onChange={onSelectChange}\n disabled={disabled}\n selectedOption={selectedOption}\n />\n ) : null);\n\n return (\n <div\n className={cn(\n \"relative flex flex-col gap-6 rounded-lg border border-border-primary bg-surface-primary\",\n \"has-focus-visible:border-neutral-alphas-400 has-focus-visible:outline-none\",\n \"motion-safe:transition-colors\",\n disabled && \"opacity-50\",\n className,\n )}\n >\n <textarea\n {...textareaProps}\n ref={mergedRef}\n value={isControlled ? value : internalValue}\n placeholder={placeholder}\n maxLength={maxLength}\n disabled={disabled}\n aria-label={ariaLabel ?? placeholder}\n onChange={handleChange}\n onKeyDown={handleKeyDown}\n rows={minRows}\n className={cn(\n \"w-full resize-none bg-transparent px-4 pt-4\",\n \"typography-regular-body-md text-content-primary\",\n \"placeholder:text-content-tertiary\",\n \"focus:outline-none disabled:cursor-not-allowed\",\n \"overflow-y-auto\",\n )}\n style={{\n minHeight: `${minHeight}px`,\n maxHeight: `${maxHeight}px`,\n ...style,\n }}\n />\n\n <div className=\"flex items-center justify-between gap-2 px-4 pb-4\">\n <div className=\"flex items-center gap-1\">\n {showFileButton && (\n <IconButton\n variant=\"tertiary\"\n size=\"32\"\n icon={<AddIcon />}\n aria-label={fileButtonAriaLabel}\n onClick={onFileClick}\n disabled={disabled}\n className=\"sm:border sm:border-border-primary max-sm:-ml-2\"\n />\n )}\n </div>\n\n <div className=\"flex items-center gap-1\">\n {resolvedToolbarRight}\n <IconButton\n variant=\"primary\"\n size=\"32\"\n icon={submitIcon ?? <ArrowUpIcon />}\n aria-label={submitAriaLabel}\n onClick={handleSubmit}\n disabled={!canSubmit}\n className=\"disabled:bg-surface-secondary disabled:opacity-100 disabled:text-icons-primary\"\n />\n </div>\n </div>\n </div>\n );\n },\n);\n\nChatInput.displayName = \"ChatInput\";\n\ninterface InlineSelectProps {\n options: ChatInputSelectOption[];\n value?: string;\n onChange?: (value: string) => void;\n disabled?: boolean;\n selectedOption?: ChatInputSelectOption;\n}\n\nfunction InlineSelect({ options, value, onChange, disabled, selectedOption }: InlineSelectProps) {\n const [open, setOpen] = React.useState(false);\n const containerRef = React.useRef<HTMLDivElement>(null);\n\n React.useEffect(() => {\n if (!open) return;\n const handleClick = (e: MouseEvent) => {\n if (containerRef.current && !containerRef.current.contains(e.target as Node)) {\n setOpen(false);\n }\n };\n document.addEventListener(\"mousedown\", handleClick);\n return () => document.removeEventListener(\"mousedown\", handleClick);\n }, [open]);\n\n React.useEffect(() => {\n if (!open) return;\n const handleKey = (e: KeyboardEvent) => {\n if (e.key === \"Escape\") setOpen(false);\n };\n document.addEventListener(\"keydown\", handleKey);\n return () => document.removeEventListener(\"keydown\", handleKey);\n }, [open]);\n\n return (\n <div ref={containerRef} className=\"relative\">\n <button\n type=\"button\"\n role=\"combobox\"\n aria-expanded={open}\n aria-haspopup=\"listbox\"\n aria-label=\"Select model\"\n disabled={disabled}\n onClick={() => setOpen((prev) => !prev)}\n className={cn(\n \"typography-semibold-body-sm text-content-primary\",\n \"flex items-center gap-1 rounded-md px-2 py-2\",\n \"hover:bg-neutral-alphas-50 focus-visible:shadow-focus-ring focus-visible:outline-none\",\n \"disabled:cursor-not-allowed disabled:opacity-50\",\n \"motion-safe:transition-colors\",\n )}\n >\n {selectedOption?.icon && (\n <span className=\"flex shrink-0 items-center [&>svg]:size-4\">{selectedOption.icon}</span>\n )}\n {selectedOption?.label ?? options[0]?.label ?? \"Select\"}\n <ChevronDownIcon\n className={cn(\"size-4 motion-safe:transition-transform\", open && \"rotate-180\")}\n />\n </button>\n\n {open && (\n <div\n role=\"listbox\"\n className={cn(\n \"absolute right-0 bottom-full z-10 mb-1 min-w-[140px]\",\n \"overflow-hidden rounded-xs border border-border-primary bg-surface-primary p-1 shadow-lg\",\n )}\n >\n {options.map((option) => (\n <div\n key={option.value}\n role=\"option\"\n tabIndex={0}\n aria-selected={option.value === value}\n className={cn(\n \"typography-regular-body-md flex cursor-pointer items-center gap-2 rounded-xs px-3 py-1.5\",\n \"text-content-primary hover:bg-neutral-alphas-50\",\n \"focus-visible:shadow-focus-ring focus-visible:outline-none\",\n option.value === value && \"bg-neutral-alphas-50\",\n )}\n onClick={() => {\n onChange?.(option.value);\n setOpen(false);\n }}\n onKeyDown={(e) => {\n if (e.key === \"Enter\" || e.key === \" \") {\n e.preventDefault();\n onChange?.(option.value);\n setOpen(false);\n }\n }}\n >\n {option.icon && (\n <span className=\"flex shrink-0 items-center [&>svg]:size-4\">{option.icon}</span>\n )}\n {option.label}\n </div>\n ))}\n </div>\n )}\n </div>\n );\n}\n"],"names":["React","minHeight","maxHeight","jsx","jsxs","cn","IconButton","AddIcon","ArrowUpIcon","ChevronDownIcon"],"mappings":"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"}
|
|
@@ -23,7 +23,21 @@ function _interopNamespaceDefault(e) {
|
|
|
23
23
|
}
|
|
24
24
|
const TabsPrimitive__namespace = /* @__PURE__ */ _interopNamespaceDefault(TabsPrimitive);
|
|
25
25
|
const React__namespace = /* @__PURE__ */ _interopNamespaceDefault(React);
|
|
26
|
-
const
|
|
26
|
+
const alignLeftClasses = {
|
|
27
|
+
always: "[&>[role=tab]]:flex-initial",
|
|
28
|
+
sm: "[&>[role=tab]]:sm:flex-initial",
|
|
29
|
+
md: "[&>[role=tab]]:md:flex-initial",
|
|
30
|
+
lg: "[&>[role=tab]]:lg:flex-initial",
|
|
31
|
+
xl: "[&>[role=tab]]:xl:flex-initial"
|
|
32
|
+
};
|
|
33
|
+
function getLayoutClass(fullWidth, alignLeft) {
|
|
34
|
+
if (!fullWidth) return "inline-flex";
|
|
35
|
+
const base = "flex w-full [&>[role=tab]]:flex-1";
|
|
36
|
+
if (alignLeft === true) return `${base} ${alignLeftClasses.always}`;
|
|
37
|
+
if (typeof alignLeft === "string") return `${base} ${alignLeftClasses[alignLeft]}`;
|
|
38
|
+
return base;
|
|
39
|
+
}
|
|
40
|
+
const TabsList = React__namespace.forwardRef(({ className, children, fullWidth = true, alignLeft, ...props }, ref) => {
|
|
27
41
|
const innerRef = React__namespace.useRef(null);
|
|
28
42
|
const indicatorRef = React__namespace.useRef(null);
|
|
29
43
|
React__namespace.useImperativeHandle(ref, () => innerRef.current);
|
|
@@ -78,7 +92,7 @@ const TabsList = React__namespace.forwardRef(({ className, children, fullWidth =
|
|
|
78
92
|
ref: innerRef,
|
|
79
93
|
className: cn.cn(
|
|
80
94
|
"relative",
|
|
81
|
-
fullWidth
|
|
95
|
+
getLayoutClass(fullWidth, alignLeft),
|
|
82
96
|
"data-[orientation=horizontal]:items-center data-[orientation=horizontal]:shadow-[inset_0_-1px_0_0_var(--color-neutral-alphas-200)]",
|
|
83
97
|
"data-[orientation=vertical]:flex-col data-[orientation=vertical]:shadow-[inset_-1px_0_0_0_var(--color-neutral-alphas-200)]",
|
|
84
98
|
className
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"TabsList.cjs","sources":["../../../../src/components/Tabs/TabsList.tsx"],"sourcesContent":["import * as TabsPrimitive from \"@radix-ui/react-tabs\";\nimport * as React from \"react\";\nimport { cn } from \"../../utils/cn\";\n\n/** Props for the {@link TabsList} component. */\nexport type TabsListProps = React.ComponentPropsWithoutRef<typeof TabsPrimitive.List> & {\n /** When `true` (the default), the tab list spans the full width of its container and each tab grows equally. Set to `false` for inline sizing. */\n fullWidth?: boolean;\n};\n\n/** Container for {@link TabsTrigger} elements. Renders a sliding active-tab indicator that animates between tabs. */\nexport const TabsList = React.forwardRef<\n React.ComponentRef<typeof TabsPrimitive.List>,\n TabsListProps\n>(({ className, children, fullWidth = true, ...props }, ref) => {\n const innerRef = React.useRef<HTMLDivElement>(null);\n const indicatorRef = React.useRef<HTMLSpanElement>(null);\n\n React.useImperativeHandle(ref, () => innerRef.current as HTMLDivElement);\n\n const updateIndicator = React.useCallback(() => {\n const list = innerRef.current;\n const indicator = indicatorRef.current;\n if (!list || !indicator) return;\n\n const activeTab = list.querySelector<HTMLElement>('[data-state=\"active\"]');\n if (!activeTab) {\n indicator.style.opacity = \"0\";\n return;\n }\n\n const isVertical = list.dataset.orientation === \"vertical\";\n\n indicator.style.opacity = \"1\";\n\n if (isVertical) {\n indicator.style.inset = `0 0 auto auto`;\n indicator.style.width = \"4px\";\n indicator.style.height = `${activeTab.offsetHeight}px`;\n indicator.style.transform = `translateY(${activeTab.offsetTop}px)`;\n } else {\n indicator.style.inset = `auto auto 0 0`;\n indicator.style.height = \"4px\";\n indicator.style.width = `${activeTab.offsetWidth}px`;\n indicator.style.transform = `translateX(${activeTab.offsetLeft}px)`;\n }\n }, []);\n\n React.useLayoutEffect(() => {\n const list = innerRef.current;\n const indicator = indicatorRef.current;\n if (!list || !indicator) return;\n\n indicator.style.transitionDuration = \"0s\";\n updateIndicator();\n indicator.getBoundingClientRect();\n indicator.style.transitionDuration = \"\";\n\n const mutationObserver = new MutationObserver(updateIndicator);\n mutationObserver.observe(list, {\n attributes: true,\n attributeFilter: [\"data-state\"],\n childList: true,\n subtree: true,\n });\n\n const resizeObserver = new ResizeObserver(updateIndicator);\n resizeObserver.observe(list);\n\n return () => {\n mutationObserver.disconnect();\n resizeObserver.disconnect();\n };\n }, [updateIndicator]);\n\n return (\n <TabsPrimitive.List\n ref={innerRef}\n className={cn(\n \"relative\",\n fullWidth
|
|
1
|
+
{"version":3,"file":"TabsList.cjs","sources":["../../../../src/components/Tabs/TabsList.tsx"],"sourcesContent":["import * as TabsPrimitive from \"@radix-ui/react-tabs\";\nimport * as React from \"react\";\nimport { cn } from \"../../utils/cn\";\n\n/** Breakpoint values for responsive props. */\ntype Breakpoint = \"sm\" | \"md\" | \"lg\" | \"xl\";\n\nconst alignLeftClasses: Record<Breakpoint | \"always\", string> = {\n always: \"[&>[role=tab]]:flex-initial\",\n sm: \"[&>[role=tab]]:sm:flex-initial\",\n md: \"[&>[role=tab]]:md:flex-initial\",\n lg: \"[&>[role=tab]]:lg:flex-initial\",\n xl: \"[&>[role=tab]]:xl:flex-initial\",\n};\n\nfunction getLayoutClass(fullWidth: boolean, alignLeft?: boolean | Breakpoint): string {\n if (!fullWidth) return \"inline-flex\";\n\n const base = \"flex w-full [&>[role=tab]]:flex-1\";\n if (alignLeft === true) return `${base} ${alignLeftClasses.always}`;\n if (typeof alignLeft === \"string\") return `${base} ${alignLeftClasses[alignLeft]}`;\n return base;\n}\n\n/** Props for the {@link TabsList} component. */\nexport type TabsListProps = React.ComponentPropsWithoutRef<typeof TabsPrimitive.List> & {\n /** When `true` (the default), the tab list spans the full width of its container and each tab grows equally. Set to `false` for inline sizing. */\n fullWidth?: boolean;\n /**\n * Controls tab alignment within a full-width container.\n * - `false` (default): tabs spread evenly\n * - `true`: tabs left-aligned, sized to content\n * - `\"md\"` (breakpoint): spread on mobile, left-aligned at breakpoint and up\n */\n alignLeft?: boolean | Breakpoint;\n};\n\n/** Container for {@link TabsTrigger} elements. Renders a sliding active-tab indicator that animates between tabs. */\nexport const TabsList = React.forwardRef<\n React.ComponentRef<typeof TabsPrimitive.List>,\n TabsListProps\n>(({ className, children, fullWidth = true, alignLeft, ...props }, ref) => {\n const innerRef = React.useRef<HTMLDivElement>(null);\n const indicatorRef = React.useRef<HTMLSpanElement>(null);\n\n React.useImperativeHandle(ref, () => innerRef.current as HTMLDivElement);\n\n const updateIndicator = React.useCallback(() => {\n const list = innerRef.current;\n const indicator = indicatorRef.current;\n if (!list || !indicator) return;\n\n const activeTab = list.querySelector<HTMLElement>('[data-state=\"active\"]');\n if (!activeTab) {\n indicator.style.opacity = \"0\";\n return;\n }\n\n const isVertical = list.dataset.orientation === \"vertical\";\n\n indicator.style.opacity = \"1\";\n\n if (isVertical) {\n indicator.style.inset = `0 0 auto auto`;\n indicator.style.width = \"4px\";\n indicator.style.height = `${activeTab.offsetHeight}px`;\n indicator.style.transform = `translateY(${activeTab.offsetTop}px)`;\n } else {\n indicator.style.inset = `auto auto 0 0`;\n indicator.style.height = \"4px\";\n indicator.style.width = `${activeTab.offsetWidth}px`;\n indicator.style.transform = `translateX(${activeTab.offsetLeft}px)`;\n }\n }, []);\n\n React.useLayoutEffect(() => {\n const list = innerRef.current;\n const indicator = indicatorRef.current;\n if (!list || !indicator) return;\n\n indicator.style.transitionDuration = \"0s\";\n updateIndicator();\n indicator.getBoundingClientRect();\n indicator.style.transitionDuration = \"\";\n\n const mutationObserver = new MutationObserver(updateIndicator);\n mutationObserver.observe(list, {\n attributes: true,\n attributeFilter: [\"data-state\"],\n childList: true,\n subtree: true,\n });\n\n const resizeObserver = new ResizeObserver(updateIndicator);\n resizeObserver.observe(list);\n\n return () => {\n mutationObserver.disconnect();\n resizeObserver.disconnect();\n };\n }, [updateIndicator]);\n\n return (\n <TabsPrimitive.List\n ref={innerRef}\n className={cn(\n \"relative\",\n getLayoutClass(fullWidth, alignLeft),\n \"data-[orientation=horizontal]:items-center data-[orientation=horizontal]:shadow-[inset_0_-1px_0_0_var(--color-neutral-alphas-200)]\",\n \"data-[orientation=vertical]:flex-col data-[orientation=vertical]:shadow-[inset_-1px_0_0_0_var(--color-neutral-alphas-200)]\",\n className,\n )}\n {...props}\n >\n {children}\n <span\n ref={indicatorRef}\n aria-hidden\n className=\"pointer-events-none absolute rounded-full bg-brand-primary-default motion-safe:transition-[transform,width,height] motion-safe:duration-200 motion-safe:ease-in-out\"\n style={{ opacity: 0 }}\n />\n </TabsPrimitive.List>\n );\n});\n\nTabsList.displayName = \"TabsList\";\n"],"names":["React","jsxs","TabsPrimitive","cn","jsx"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;AAOA,MAAM,mBAA0D;AAAA,EAC9D,QAAQ;AAAA,EACR,IAAI;AAAA,EACJ,IAAI;AAAA,EACJ,IAAI;AAAA,EACJ,IAAI;AACN;AAEA,SAAS,eAAe,WAAoB,WAA0C;AACpF,MAAI,CAAC,UAAW,QAAO;AAEvB,QAAM,OAAO;AACb,MAAI,cAAc,KAAM,QAAO,GAAG,IAAI,IAAI,iBAAiB,MAAM;AACjE,MAAI,OAAO,cAAc,SAAU,QAAO,GAAG,IAAI,IAAI,iBAAiB,SAAS,CAAC;AAChF,SAAO;AACT;AAgBO,MAAM,WAAWA,iBAAM,WAG5B,CAAC,EAAE,WAAW,UAAU,YAAY,MAAM,WAAW,GAAG,MAAA,GAAS,QAAQ;AACzE,QAAM,WAAWA,iBAAM,OAAuB,IAAI;AAClD,QAAM,eAAeA,iBAAM,OAAwB,IAAI;AAEvDA,mBAAM,oBAAoB,KAAK,MAAM,SAAS,OAAyB;AAEvE,QAAM,kBAAkBA,iBAAM,YAAY,MAAM;AAC9C,UAAM,OAAO,SAAS;AACtB,UAAM,YAAY,aAAa;AAC/B,QAAI,CAAC,QAAQ,CAAC,UAAW;AAEzB,UAAM,YAAY,KAAK,cAA2B,uBAAuB;AACzE,QAAI,CAAC,WAAW;AACd,gBAAU,MAAM,UAAU;AAC1B;AAAA,IACF;AAEA,UAAM,aAAa,KAAK,QAAQ,gBAAgB;AAEhD,cAAU,MAAM,UAAU;AAE1B,QAAI,YAAY;AACd,gBAAU,MAAM,QAAQ;AACxB,gBAAU,MAAM,QAAQ;AACxB,gBAAU,MAAM,SAAS,GAAG,UAAU,YAAY;AAClD,gBAAU,MAAM,YAAY,cAAc,UAAU,SAAS;AAAA,IAC/D,OAAO;AACL,gBAAU,MAAM,QAAQ;AACxB,gBAAU,MAAM,SAAS;AACzB,gBAAU,MAAM,QAAQ,GAAG,UAAU,WAAW;AAChD,gBAAU,MAAM,YAAY,cAAc,UAAU,UAAU;AAAA,IAChE;AAAA,EACF,GAAG,CAAA,CAAE;AAELA,mBAAM,gBAAgB,MAAM;AAC1B,UAAM,OAAO,SAAS;AACtB,UAAM,YAAY,aAAa;AAC/B,QAAI,CAAC,QAAQ,CAAC,UAAW;AAEzB,cAAU,MAAM,qBAAqB;AACrC,oBAAA;AACA,cAAU,sBAAA;AACV,cAAU,MAAM,qBAAqB;AAErC,UAAM,mBAAmB,IAAI,iBAAiB,eAAe;AAC7D,qBAAiB,QAAQ,MAAM;AAAA,MAC7B,YAAY;AAAA,MACZ,iBAAiB,CAAC,YAAY;AAAA,MAC9B,WAAW;AAAA,MACX,SAAS;AAAA,IAAA,CACV;AAED,UAAM,iBAAiB,IAAI,eAAe,eAAe;AACzD,mBAAe,QAAQ,IAAI;AAE3B,WAAO,MAAM;AACX,uBAAiB,WAAA;AACjB,qBAAe,WAAA;AAAA,IACjB;AAAA,EACF,GAAG,CAAC,eAAe,CAAC;AAEpB,SACEC,2BAAAA;AAAAA,IAACC,yBAAc;AAAA,IAAd;AAAA,MACC,KAAK;AAAA,MACL,WAAWC,GAAAA;AAAAA,QACT;AAAA,QACA,eAAe,WAAW,SAAS;AAAA,QACnC;AAAA,QACA;AAAA,QACA;AAAA,MAAA;AAAA,MAED,GAAG;AAAA,MAEH,UAAA;AAAA,QAAA;AAAA,QACDC,2BAAAA;AAAAA,UAAC;AAAA,UAAA;AAAA,YACC,KAAK;AAAA,YACL,eAAW;AAAA,YACX,WAAU;AAAA,YACV,OAAO,EAAE,SAAS,EAAA;AAAA,UAAE;AAAA,QAAA;AAAA,MACtB;AAAA,IAAA;AAAA,EAAA;AAGN,CAAC;AAED,SAAS,cAAc;;"}
|
|
@@ -54,7 +54,7 @@ const Alert = React.forwardRef(
|
|
|
54
54
|
),
|
|
55
55
|
...props,
|
|
56
56
|
children: [
|
|
57
|
-
resolvedIcon && /* @__PURE__ */ jsx("span", { className: "flex shrink-0 items-start", "aria-hidden": "true", children: resolvedIcon }),
|
|
57
|
+
resolvedIcon && /* @__PURE__ */ jsx("span", { className: "flex shrink-0 items-start h-full", "aria-hidden": "true", children: resolvedIcon }),
|
|
58
58
|
/* @__PURE__ */ jsxs("div", { className: "flex min-w-0 flex-col gap-2", children: [
|
|
59
59
|
title && /* @__PURE__ */ jsx("div", { className: "typography-semibold-body-md text-content-primary", children: title }),
|
|
60
60
|
/* @__PURE__ */ jsx("div", { className: "typography-regular-body-md text-content-primary", children })
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Alert.mjs","sources":["../../../src/components/Alert/Alert.tsx"],"sourcesContent":["import * as React from \"react\";\nimport { cn } from \"../../utils/cn\";\nimport { Button } from \"../Button/Button\";\nimport { CheckCircleIcon } from \"../Icons/CheckCircleIcon\";\nimport { CrossIcon } from \"../Icons/CrossIcon\";\nimport { ErrorCircleIcon } from \"../Icons/ErrorCircleIcon\";\nimport { InfoCircleIcon } from \"../Icons/InfoCircleIcon\";\nimport { WarningTriangleIcon } from \"../Icons/WarningTriangleIcon\";\n\n/** Visual style variant of the alert. */\nexport type AlertVariant = \"info\" | \"success\" | \"warning\" | \"error\";\n\nconst DEFAULT_ICONS: Record<AlertVariant, React.ReactNode> = {\n info: <InfoCircleIcon />,\n success: <CheckCircleIcon />,\n warning: <WarningTriangleIcon />,\n error: <ErrorCircleIcon />,\n};\n\nexport interface AlertProps extends React.HTMLAttributes<HTMLDivElement> {\n /** Visual style variant of the alert. @default \"info\" */\n variant?: AlertVariant;\n /** Optional title text displayed in bold above the description. */\n title?: string;\n /** Custom icon override. Pass `null` to hide the icon entirely. Each variant shows a default icon when left `undefined`. */\n icon?: React.ReactNode | null;\n /** Whether to show the close button. @default false */\n closable?: boolean;\n /** Callback fired when the close button is clicked. */\n onClose?: () => void;\n /** Accessible label for the close button. @default \"Close alert\" */\n closeLabel?: string;\n}\n\nconst CLOSE_BUTTON_CLASSES: Record<AlertVariant, string> = {\n info: \"hover:bg-info-content/10 active:bg-info-content/20 text-info-content motion-safe:transition-colors motion-safe:duration-150\",\n success:\n \"hover:bg-success-content/10 active:bg-success-content/20 text-success-content motion-safe:transition-colors motion-safe:duration-150\",\n warning:\n \"hover:bg-warning-content/10 active:bg-warning-content/20 text-warning-content motion-safe:transition-colors motion-safe:duration-150\",\n error:\n \"hover:bg-error-content/10 active:bg-error-content/20 text-error-content motion-safe:transition-colors motion-safe:duration-150\",\n};\n\n/**\n * Displays a contextual feedback message to the user.\n *\n * Supports `info`, `success`, `warning`, and `error` variants with a default\n * icon per variant, optional title, description, and dismiss button.\n *\n * Each variant renders a default icon automatically. Pass a custom `icon` to\n * override, or `icon={null}` to hide the icon entirely.\n *\n * @example\n * ```tsx\n * <Alert variant=\"success\" title=\"Saved\" closable onClose={handleClose}>\n * Your changes have been saved.\n * </Alert>\n * ```\n */\nexport const Alert = React.forwardRef<HTMLDivElement, AlertProps>(\n (\n {\n className,\n variant = \"info\",\n title,\n icon,\n closable = false,\n onClose,\n closeLabel = \"Close alert\",\n children,\n ...props\n },\n ref,\n ) => {\n const resolvedIcon = icon === null ? null : (icon ?? DEFAULT_ICONS[variant]);\n\n return (\n <div\n ref={ref}\n role=\"alert\"\n data-testid=\"alert\"\n className={cn(\n \"grid gap-x-3 rounded-xs p-4 text-sm leading-[18px]\",\n resolvedIcon && closable && \"grid-cols-[auto_1fr_auto]\",\n resolvedIcon && !closable && \"grid-cols-[auto_1fr]\",\n !resolvedIcon && closable && \"grid-cols-[1fr_auto]\",\n !resolvedIcon && !closable && \"grid-cols-[1fr]\",\n title && children ? \"items-start\" : \"items-center\",\n variant === \"info\" && \"bg-info-surface text-info-content\",\n variant === \"success\" && \"bg-success-surface text-success-content\",\n variant === \"warning\" && \"bg-warning-surface text-warning-content\",\n variant === \"error\" && \"bg-error-surface text-error-content\",\n className,\n )}\n {...props}\n >\n {resolvedIcon && (\n <span className=\"flex shrink-0 items-start\" aria-hidden=\"true\">\n {resolvedIcon}\n </span>\n )}\n\n <div className=\"flex min-w-0 flex-col gap-2\">\n {title && <div className=\"typography-semibold-body-md text-content-primary\">{title}</div>}\n <div className=\"typography-regular-body-md text-content-primary\">{children}</div>\n </div>\n\n {closable && (\n <Button\n variant=\"tertiary\"\n size=\"24\"\n onClick={onClose}\n className={cn(\"self-start px-0\", CLOSE_BUTTON_CLASSES[variant])}\n aria-label={closeLabel}\n >\n <CrossIcon />\n </Button>\n )}\n </div>\n );\n },\n);\n\nAlert.displayName = \"Alert\";\n"],"names":[],"mappings":";;;;;;;;;;AAYA,MAAM,gBAAuD;AAAA,EAC3D,0BAAO,gBAAA,EAAe;AAAA,EACtB,6BAAU,iBAAA,EAAgB;AAAA,EAC1B,6BAAU,qBAAA,EAAoB;AAAA,EAC9B,2BAAQ,iBAAA,CAAA,CAAgB;AAC1B;AAiBA,MAAM,uBAAqD;AAAA,EACzD,MAAM;AAAA,EACN,SACE;AAAA,EACF,SACE;AAAA,EACF,OACE;AACJ;AAkBO,MAAM,QAAQ,MAAM;AAAA,EACzB,CACE;AAAA,IACE;AAAA,IACA,UAAU;AAAA,IACV;AAAA,IACA;AAAA,IACA,WAAW;AAAA,IACX;AAAA,IACA,aAAa;AAAA,IACb;AAAA,IACA,GAAG;AAAA,EAAA,GAEL,QACG;AACH,UAAM,eAAe,SAAS,OAAO,OAAQ,QAAQ,cAAc,OAAO;AAE1E,WACE;AAAA,MAAC;AAAA,MAAA;AAAA,QACC;AAAA,QACA,MAAK;AAAA,QACL,eAAY;AAAA,QACZ,WAAW;AAAA,UACT;AAAA,UACA,gBAAgB,YAAY;AAAA,UAC5B,gBAAgB,CAAC,YAAY;AAAA,UAC7B,CAAC,gBAAgB,YAAY;AAAA,UAC7B,CAAC,gBAAgB,CAAC,YAAY;AAAA,UAC9B,SAAS,WAAW,gBAAgB;AAAA,UACpC,YAAY,UAAU;AAAA,UACtB,YAAY,aAAa;AAAA,UACzB,YAAY,aAAa;AAAA,UACzB,YAAY,WAAW;AAAA,UACvB;AAAA,QAAA;AAAA,QAED,GAAG;AAAA,QAEH,UAAA;AAAA,UAAA,oCACE,QAAA,EAAK,WAAU,
|
|
1
|
+
{"version":3,"file":"Alert.mjs","sources":["../../../src/components/Alert/Alert.tsx"],"sourcesContent":["import * as React from \"react\";\nimport { cn } from \"../../utils/cn\";\nimport { Button } from \"../Button/Button\";\nimport { CheckCircleIcon } from \"../Icons/CheckCircleIcon\";\nimport { CrossIcon } from \"../Icons/CrossIcon\";\nimport { ErrorCircleIcon } from \"../Icons/ErrorCircleIcon\";\nimport { InfoCircleIcon } from \"../Icons/InfoCircleIcon\";\nimport { WarningTriangleIcon } from \"../Icons/WarningTriangleIcon\";\n\n/** Visual style variant of the alert. */\nexport type AlertVariant = \"info\" | \"success\" | \"warning\" | \"error\";\n\nconst DEFAULT_ICONS: Record<AlertVariant, React.ReactNode> = {\n info: <InfoCircleIcon />,\n success: <CheckCircleIcon />,\n warning: <WarningTriangleIcon />,\n error: <ErrorCircleIcon />,\n};\n\nexport interface AlertProps extends React.HTMLAttributes<HTMLDivElement> {\n /** Visual style variant of the alert. @default \"info\" */\n variant?: AlertVariant;\n /** Optional title text displayed in bold above the description. */\n title?: string;\n /** Custom icon override. Pass `null` to hide the icon entirely. Each variant shows a default icon when left `undefined`. */\n icon?: React.ReactNode | null;\n /** Whether to show the close button. @default false */\n closable?: boolean;\n /** Callback fired when the close button is clicked. */\n onClose?: () => void;\n /** Accessible label for the close button. @default \"Close alert\" */\n closeLabel?: string;\n}\n\nconst CLOSE_BUTTON_CLASSES: Record<AlertVariant, string> = {\n info: \"hover:bg-info-content/10 active:bg-info-content/20 text-info-content motion-safe:transition-colors motion-safe:duration-150\",\n success:\n \"hover:bg-success-content/10 active:bg-success-content/20 text-success-content motion-safe:transition-colors motion-safe:duration-150\",\n warning:\n \"hover:bg-warning-content/10 active:bg-warning-content/20 text-warning-content motion-safe:transition-colors motion-safe:duration-150\",\n error:\n \"hover:bg-error-content/10 active:bg-error-content/20 text-error-content motion-safe:transition-colors motion-safe:duration-150\",\n};\n\n/**\n * Displays a contextual feedback message to the user.\n *\n * Supports `info`, `success`, `warning`, and `error` variants with a default\n * icon per variant, optional title, description, and dismiss button.\n *\n * Each variant renders a default icon automatically. Pass a custom `icon` to\n * override, or `icon={null}` to hide the icon entirely.\n *\n * @example\n * ```tsx\n * <Alert variant=\"success\" title=\"Saved\" closable onClose={handleClose}>\n * Your changes have been saved.\n * </Alert>\n * ```\n */\nexport const Alert = React.forwardRef<HTMLDivElement, AlertProps>(\n (\n {\n className,\n variant = \"info\",\n title,\n icon,\n closable = false,\n onClose,\n closeLabel = \"Close alert\",\n children,\n ...props\n },\n ref,\n ) => {\n const resolvedIcon = icon === null ? null : (icon ?? DEFAULT_ICONS[variant]);\n\n return (\n <div\n ref={ref}\n role=\"alert\"\n data-testid=\"alert\"\n className={cn(\n \"grid gap-x-3 rounded-xs p-4 text-sm leading-[18px]\",\n resolvedIcon && closable && \"grid-cols-[auto_1fr_auto]\",\n resolvedIcon && !closable && \"grid-cols-[auto_1fr]\",\n !resolvedIcon && closable && \"grid-cols-[1fr_auto]\",\n !resolvedIcon && !closable && \"grid-cols-[1fr]\",\n title && children ? \"items-start\" : \"items-center\",\n variant === \"info\" && \"bg-info-surface text-info-content\",\n variant === \"success\" && \"bg-success-surface text-success-content\",\n variant === \"warning\" && \"bg-warning-surface text-warning-content\",\n variant === \"error\" && \"bg-error-surface text-error-content\",\n className,\n )}\n {...props}\n >\n {resolvedIcon && (\n <span className=\"flex shrink-0 items-start h-full\" aria-hidden=\"true\">\n {resolvedIcon}\n </span>\n )}\n\n <div className=\"flex min-w-0 flex-col gap-2\">\n {title && <div className=\"typography-semibold-body-md text-content-primary\">{title}</div>}\n <div className=\"typography-regular-body-md text-content-primary\">{children}</div>\n </div>\n\n {closable && (\n <Button\n variant=\"tertiary\"\n size=\"24\"\n onClick={onClose}\n className={cn(\"self-start px-0\", CLOSE_BUTTON_CLASSES[variant])}\n aria-label={closeLabel}\n >\n <CrossIcon />\n </Button>\n )}\n </div>\n );\n },\n);\n\nAlert.displayName = \"Alert\";\n"],"names":[],"mappings":";;;;;;;;;;AAYA,MAAM,gBAAuD;AAAA,EAC3D,0BAAO,gBAAA,EAAe;AAAA,EACtB,6BAAU,iBAAA,EAAgB;AAAA,EAC1B,6BAAU,qBAAA,EAAoB;AAAA,EAC9B,2BAAQ,iBAAA,CAAA,CAAgB;AAC1B;AAiBA,MAAM,uBAAqD;AAAA,EACzD,MAAM;AAAA,EACN,SACE;AAAA,EACF,SACE;AAAA,EACF,OACE;AACJ;AAkBO,MAAM,QAAQ,MAAM;AAAA,EACzB,CACE;AAAA,IACE;AAAA,IACA,UAAU;AAAA,IACV;AAAA,IACA;AAAA,IACA,WAAW;AAAA,IACX;AAAA,IACA,aAAa;AAAA,IACb;AAAA,IACA,GAAG;AAAA,EAAA,GAEL,QACG;AACH,UAAM,eAAe,SAAS,OAAO,OAAQ,QAAQ,cAAc,OAAO;AAE1E,WACE;AAAA,MAAC;AAAA,MAAA;AAAA,QACC;AAAA,QACA,MAAK;AAAA,QACL,eAAY;AAAA,QACZ,WAAW;AAAA,UACT;AAAA,UACA,gBAAgB,YAAY;AAAA,UAC5B,gBAAgB,CAAC,YAAY;AAAA,UAC7B,CAAC,gBAAgB,YAAY;AAAA,UAC7B,CAAC,gBAAgB,CAAC,YAAY;AAAA,UAC9B,SAAS,WAAW,gBAAgB;AAAA,UACpC,YAAY,UAAU;AAAA,UACtB,YAAY,aAAa;AAAA,UACzB,YAAY,aAAa;AAAA,UACzB,YAAY,WAAW;AAAA,UACvB;AAAA,QAAA;AAAA,QAED,GAAG;AAAA,QAEH,UAAA;AAAA,UAAA,oCACE,QAAA,EAAK,WAAU,oCAAmC,eAAY,QAC5D,UAAA,cACH;AAAA,UAGF,qBAAC,OAAA,EAAI,WAAU,+BACZ,UAAA;AAAA,YAAA,SAAS,oBAAC,OAAA,EAAI,WAAU,oDAAoD,UAAA,OAAM;AAAA,YACnF,oBAAC,OAAA,EAAI,WAAU,mDAAmD,SAAA,CAAS;AAAA,UAAA,GAC7E;AAAA,UAEC,YACC;AAAA,YAAC;AAAA,YAAA;AAAA,cACC,SAAQ;AAAA,cACR,MAAK;AAAA,cACL,SAAS;AAAA,cACT,WAAW,GAAG,mBAAmB,qBAAqB,OAAO,CAAC;AAAA,cAC9D,cAAY;AAAA,cAEZ,8BAAC,WAAA,CAAA,CAAU;AAAA,YAAA;AAAA,UAAA;AAAA,QACb;AAAA,MAAA;AAAA,IAAA;AAAA,EAIR;AACF;AAEA,MAAM,cAAc;"}
|
|
@@ -1,17 +1,80 @@
|
|
|
1
1
|
"use client";
|
|
2
|
-
import { jsx } from "react/jsx-runtime";
|
|
2
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
3
3
|
import * as React from "react";
|
|
4
4
|
import { cn } from "../../utils/cn.mjs";
|
|
5
|
-
const BottomNavigationContext = React.createContext({
|
|
5
|
+
const BottomNavigationContext = React.createContext({
|
|
6
|
+
hasInformationArchitectureNav: false
|
|
7
|
+
});
|
|
6
8
|
function useBottomNavigationContext() {
|
|
7
9
|
return React.useContext(BottomNavigationContext);
|
|
8
10
|
}
|
|
11
|
+
function resolveChildren(children, value) {
|
|
12
|
+
const items = React.Children.toArray(children);
|
|
13
|
+
for (let i = 0; i < items.length; i++) {
|
|
14
|
+
const child = items[i];
|
|
15
|
+
if (React.isValidElement(child) && child.props.value === value) {
|
|
16
|
+
return { count: items.length, activeIndex: i };
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
return { count: items.length, activeIndex: void 0 };
|
|
20
|
+
}
|
|
9
21
|
const BottomNavigation = React.forwardRef(
|
|
10
|
-
({
|
|
22
|
+
({
|
|
23
|
+
className,
|
|
24
|
+
children,
|
|
25
|
+
value,
|
|
26
|
+
onValueChange,
|
|
27
|
+
hasInformationArchitectureNav = false,
|
|
28
|
+
hideOnDesktop = false,
|
|
29
|
+
...props
|
|
30
|
+
}, ref) => {
|
|
11
31
|
const contextValue = React.useMemo(
|
|
12
|
-
() => ({ value, onValueChange }),
|
|
13
|
-
[value, onValueChange]
|
|
32
|
+
() => ({ value, onValueChange, hasInformationArchitectureNav }),
|
|
33
|
+
[value, onValueChange, hasInformationArchitectureNav]
|
|
14
34
|
);
|
|
35
|
+
if (hasInformationArchitectureNav) {
|
|
36
|
+
const { count: itemCount, activeIndex } = resolveChildren(children, value);
|
|
37
|
+
const { style, ...restProps } = props;
|
|
38
|
+
return /* @__PURE__ */ jsx(BottomNavigationContext.Provider, { value: contextValue, children: /* @__PURE__ */ jsxs(
|
|
39
|
+
"nav",
|
|
40
|
+
{
|
|
41
|
+
ref,
|
|
42
|
+
...restProps,
|
|
43
|
+
className: cn(
|
|
44
|
+
"fixed inset-x-0 bottom-0 flex h-[calc(env(safe-area-inset-bottom,0px)+80px)] items-stretch justify-around pb-[env(safe-area-inset-bottom,0px)]",
|
|
45
|
+
"border-neutral-alphas-200 border-t bg-bg-primary",
|
|
46
|
+
hideOnDesktop && "md:hidden",
|
|
47
|
+
className
|
|
48
|
+
),
|
|
49
|
+
style: { zIndex: "var(--fanvue-ui-portal-z-index, 50)", ...style },
|
|
50
|
+
children: [
|
|
51
|
+
activeIndex != null && /* @__PURE__ */ jsx(
|
|
52
|
+
"div",
|
|
53
|
+
{
|
|
54
|
+
"aria-hidden": "true",
|
|
55
|
+
"data-part": "indicator",
|
|
56
|
+
className: "pointer-events-none absolute inset-x-0 -top-px h-0",
|
|
57
|
+
children: /* @__PURE__ */ jsxs(
|
|
58
|
+
"div",
|
|
59
|
+
{
|
|
60
|
+
className: "absolute top-0 flex -translate-x-1/2 flex-col items-center motion-safe:transition-[left] motion-safe:duration-300 motion-safe:ease-in-out",
|
|
61
|
+
style: {
|
|
62
|
+
left: `calc((${activeIndex} + 0.5) * (100% / ${itemCount}))`,
|
|
63
|
+
width: `calc(100% / ${itemCount})`
|
|
64
|
+
},
|
|
65
|
+
children: [
|
|
66
|
+
/* @__PURE__ */ jsx("div", { className: "h-px w-full bg-linear-to-r from-transparent via-(--color-special-bottom-nav-highlight) to-transparent" }),
|
|
67
|
+
/* @__PURE__ */ jsx("div", { className: "h-20 w-full overflow-hidden", children: /* @__PURE__ */ jsx("div", { className: "mx-auto aspect-square w-[70%] max-w-[70px] -translate-y-1/2 rounded-full bg-(--color-special-bottom-nav-highlight) opacity-30 blur-[20px] dark:opacity-15" }) })
|
|
68
|
+
]
|
|
69
|
+
}
|
|
70
|
+
)
|
|
71
|
+
}
|
|
72
|
+
),
|
|
73
|
+
children
|
|
74
|
+
]
|
|
75
|
+
}
|
|
76
|
+
) });
|
|
77
|
+
}
|
|
15
78
|
return /* @__PURE__ */ jsx(BottomNavigationContext.Provider, { value: contextValue, children: /* @__PURE__ */ jsx(
|
|
16
79
|
"nav",
|
|
17
80
|
{
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"BottomNavigation.mjs","sources":["../../../src/components/BottomNavigation/BottomNavigation.tsx"],"sourcesContent":["import * as React from \"react\";\nimport { cn } from \"../../utils/cn\";\n\nexport interface BottomNavigationProps extends React.HTMLAttributes<HTMLElement> {\n /** The currently selected action value. */\n value?: string;\n /** Called when the selected action changes. */\n onValueChange?: (value: string) => void;\n /** When `true`, the navigation bar is hidden on viewports wider than `md` (768 px). @default false */\n hideOnDesktop?: boolean;\n}\n\ninterface BottomNavigationContextValue {\n value?: string;\n onValueChange?: (value: string) => void;\n}\n\nconst BottomNavigationContext = React.createContext<BottomNavigationContextValue>({});\n\nexport function useBottomNavigationContext(): BottomNavigationContextValue {\n return React.useContext(BottomNavigationContext);\n}\n\nexport const BottomNavigation = React.forwardRef<HTMLElement, BottomNavigationProps>(\n ({
|
|
1
|
+
{"version":3,"file":"BottomNavigation.mjs","sources":["../../../src/components/BottomNavigation/BottomNavigation.tsx"],"sourcesContent":["import * as React from \"react\";\nimport { cn } from \"../../utils/cn\";\n\nexport interface BottomNavigationProps extends React.HTMLAttributes<HTMLElement> {\n /** The currently selected action value. */\n value?: string;\n /** Called when the selected action changes. */\n onValueChange?: (value: string) => void;\n /** When `true`, the navigation bar is hidden on viewports wider than `md` (768 px). @default false */\n hideOnDesktop?: boolean;\n /** When `true`, renders the information-architecture style with visible labels and a sliding active indicator. @default false */\n hasInformationArchitectureNav?: boolean;\n}\n\ninterface BottomNavigationContextValue {\n value?: string;\n onValueChange?: (value: string) => void;\n hasInformationArchitectureNav: boolean;\n}\n\nconst BottomNavigationContext = React.createContext<BottomNavigationContextValue>({\n hasInformationArchitectureNav: false,\n});\n\nexport function useBottomNavigationContext(): BottomNavigationContextValue {\n return React.useContext(BottomNavigationContext);\n}\n\nfunction resolveChildren(children: React.ReactNode, value: string | undefined) {\n const items = React.Children.toArray(children);\n for (let i = 0; i < items.length; i++) {\n const child = items[i];\n if (React.isValidElement<{ value?: string }>(child) && child.props.value === value) {\n return { count: items.length, activeIndex: i };\n }\n }\n return { count: items.length, activeIndex: undefined };\n}\n\nexport const BottomNavigation = React.forwardRef<HTMLElement, BottomNavigationProps>(\n (\n {\n className,\n children,\n value,\n onValueChange,\n hasInformationArchitectureNav = false,\n hideOnDesktop = false,\n ...props\n },\n ref,\n ) => {\n const contextValue = React.useMemo<BottomNavigationContextValue>(\n () => ({ value, onValueChange, hasInformationArchitectureNav }),\n [value, onValueChange, hasInformationArchitectureNav],\n );\n\n if (hasInformationArchitectureNav) {\n const { count: itemCount, activeIndex } = resolveChildren(children, value);\n const { style, ...restProps } = props;\n\n return (\n <BottomNavigationContext.Provider value={contextValue}>\n <nav\n ref={ref}\n {...restProps}\n className={cn(\n \"fixed inset-x-0 bottom-0 flex h-[calc(env(safe-area-inset-bottom,0px)+80px)] items-stretch justify-around pb-[env(safe-area-inset-bottom,0px)]\",\n \"border-neutral-alphas-200 border-t bg-bg-primary\",\n hideOnDesktop && \"md:hidden\",\n className,\n )}\n style={\n { zIndex: \"var(--fanvue-ui-portal-z-index, 50)\", ...style } as React.CSSProperties\n }\n >\n {activeIndex != null && (\n <div\n aria-hidden=\"true\"\n data-part=\"indicator\"\n className=\"pointer-events-none absolute inset-x-0 -top-px h-0\"\n >\n <div\n className=\"absolute top-0 flex -translate-x-1/2 flex-col items-center motion-safe:transition-[left] motion-safe:duration-300 motion-safe:ease-in-out\"\n style={{\n left: `calc((${activeIndex} + 0.5) * (100% / ${itemCount}))`,\n width: `calc(100% / ${itemCount})`,\n }}\n >\n <div className=\"h-px w-full bg-linear-to-r from-transparent via-(--color-special-bottom-nav-highlight) to-transparent\" />\n <div className=\"h-20 w-full overflow-hidden\">\n <div className=\"mx-auto aspect-square w-[70%] max-w-[70px] -translate-y-1/2 rounded-full bg-(--color-special-bottom-nav-highlight) opacity-30 blur-[20px] dark:opacity-15\" />\n </div>\n </div>\n </div>\n )}\n {children}\n </nav>\n </BottomNavigationContext.Provider>\n );\n }\n\n return (\n <BottomNavigationContext.Provider value={contextValue}>\n <nav\n ref={ref}\n className={cn(\n \"fixed inset-x-0 bottom-0\",\n \"flex h-[calc(env(safe-area-inset-bottom,0px)+68px)] items-center justify-around\",\n \"border-neutral-alphas-200 border-t bg-bg-primary\",\n \"pb-[env(safe-area-inset-bottom,0px)]\",\n hideOnDesktop && \"md:hidden\",\n className,\n )}\n style={{ zIndex: \"var(--fanvue-ui-portal-z-index, 50)\", ...props.style }}\n {...props}\n >\n {children}\n </nav>\n </BottomNavigationContext.Provider>\n );\n },\n);\n\nBottomNavigation.displayName = \"BottomNavigation\";\n"],"names":[],"mappings":";;;;AAoBA,MAAM,0BAA0B,MAAM,cAA4C;AAAA,EAChF,+BAA+B;AACjC,CAAC;AAEM,SAAS,6BAA2D;AACzE,SAAO,MAAM,WAAW,uBAAuB;AACjD;AAEA,SAAS,gBAAgB,UAA2B,OAA2B;AAC7E,QAAM,QAAQ,MAAM,SAAS,QAAQ,QAAQ;AAC7C,WAAS,IAAI,GAAG,IAAI,MAAM,QAAQ,KAAK;AACrC,UAAM,QAAQ,MAAM,CAAC;AACrB,QAAI,MAAM,eAAmC,KAAK,KAAK,MAAM,MAAM,UAAU,OAAO;AAClF,aAAO,EAAE,OAAO,MAAM,QAAQ,aAAa,EAAA;AAAA,IAC7C;AAAA,EACF;AACA,SAAO,EAAE,OAAO,MAAM,QAAQ,aAAa,OAAA;AAC7C;AAEO,MAAM,mBAAmB,MAAM;AAAA,EACpC,CACE;AAAA,IACE;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA,gCAAgC;AAAA,IAChC,gBAAgB;AAAA,IAChB,GAAG;AAAA,EAAA,GAEL,QACG;AACH,UAAM,eAAe,MAAM;AAAA,MACzB,OAAO,EAAE,OAAO,eAAe;MAC/B,CAAC,OAAO,eAAe,6BAA6B;AAAA,IAAA;AAGtD,QAAI,+BAA+B;AACjC,YAAM,EAAE,OAAO,WAAW,gBAAgB,gBAAgB,UAAU,KAAK;AACzE,YAAM,EAAE,OAAO,GAAG,UAAA,IAAc;AAEhC,aACE,oBAAC,wBAAwB,UAAxB,EAAiC,OAAO,cACvC,UAAA;AAAA,QAAC;AAAA,QAAA;AAAA,UACC;AAAA,UACC,GAAG;AAAA,UACJ,WAAW;AAAA,YACT;AAAA,YACA;AAAA,YACA,iBAAiB;AAAA,YACjB;AAAA,UAAA;AAAA,UAEF,OACE,EAAE,QAAQ,uCAAuC,GAAG,MAAA;AAAA,UAGrD,UAAA;AAAA,YAAA,eAAe,QACd;AAAA,cAAC;AAAA,cAAA;AAAA,gBACC,eAAY;AAAA,gBACZ,aAAU;AAAA,gBACV,WAAU;AAAA,gBAEV,UAAA;AAAA,kBAAC;AAAA,kBAAA;AAAA,oBACC,WAAU;AAAA,oBACV,OAAO;AAAA,sBACL,MAAM,SAAS,WAAW,qBAAqB,SAAS;AAAA,sBACxD,OAAO,eAAe,SAAS;AAAA,oBAAA;AAAA,oBAGjC,UAAA;AAAA,sBAAA,oBAAC,OAAA,EAAI,WAAU,wGAAA,CAAwG;AAAA,sBACvH,oBAAC,SAAI,WAAU,+BACb,8BAAC,OAAA,EAAI,WAAU,6JAA4J,EAAA,CAC7K;AAAA,oBAAA;AAAA,kBAAA;AAAA,gBAAA;AAAA,cACF;AAAA,YAAA;AAAA,YAGH;AAAA,UAAA;AAAA,QAAA;AAAA,MAAA,GAEL;AAAA,IAEJ;AAEA,WACE,oBAAC,wBAAwB,UAAxB,EAAiC,OAAO,cACvC,UAAA;AAAA,MAAC;AAAA,MAAA;AAAA,QACC;AAAA,QACA,WAAW;AAAA,UACT;AAAA,UACA;AAAA,UACA;AAAA,UACA;AAAA,UACA,iBAAiB;AAAA,UACjB;AAAA,QAAA;AAAA,QAEF,OAAO,EAAE,QAAQ,uCAAuC,GAAG,MAAM,MAAA;AAAA,QAChE,GAAG;AAAA,QAEH;AAAA,MAAA;AAAA,IAAA,GAEL;AAAA,EAEJ;AACF;AAEA,iBAAiB,cAAc;"}
|
|
@@ -5,13 +5,54 @@ import * as React from "react";
|
|
|
5
5
|
import { cn } from "../../utils/cn.mjs";
|
|
6
6
|
import { useBottomNavigationContext } from "./BottomNavigation.mjs";
|
|
7
7
|
const BottomNavigationAction = React.forwardRef(({ className, value, icon, label, badge, onClick, asChild = false, children, ...props }, ref) => {
|
|
8
|
-
const {
|
|
8
|
+
const {
|
|
9
|
+
value: selectedValue,
|
|
10
|
+
onValueChange,
|
|
11
|
+
hasInformationArchitectureNav
|
|
12
|
+
} = useBottomNavigationContext();
|
|
9
13
|
const isActive = selectedValue === value;
|
|
10
14
|
const handleClick = (e) => {
|
|
11
15
|
onValueChange?.(value);
|
|
12
16
|
onClick?.(e);
|
|
13
17
|
};
|
|
14
18
|
const Comp = asChild ? Slot : "button";
|
|
19
|
+
if (hasInformationArchitectureNav) {
|
|
20
|
+
return /* @__PURE__ */ jsxs(
|
|
21
|
+
Comp,
|
|
22
|
+
{
|
|
23
|
+
ref,
|
|
24
|
+
...!asChild && { type: "button" },
|
|
25
|
+
"aria-current": isActive ? "page" : void 0,
|
|
26
|
+
"data-state": isActive ? "active" : "inactive",
|
|
27
|
+
onClick: handleClick,
|
|
28
|
+
...props,
|
|
29
|
+
className: cn(
|
|
30
|
+
"relative flex min-w-0 flex-1 cursor-pointer flex-col items-center justify-center gap-1 px-2",
|
|
31
|
+
isActive ? "text-icons-primary" : "text-icons-tertiary",
|
|
32
|
+
"motion-safe:transition-colors motion-safe:duration-150 motion-safe:ease-in-out",
|
|
33
|
+
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-interaction-focus focus-visible:ring-offset-2 focus-visible:ring-offset-bg-primary",
|
|
34
|
+
className
|
|
35
|
+
),
|
|
36
|
+
children: [
|
|
37
|
+
asChild && /* @__PURE__ */ jsx(Slottable, { children }),
|
|
38
|
+
/* @__PURE__ */ jsxs("span", { className: "relative inline-flex", children: [
|
|
39
|
+
/* @__PURE__ */ jsx("span", { className: "flex items-center justify-center [&>svg]:size-6", "aria-hidden": "true", children: icon }),
|
|
40
|
+
badge && /* @__PURE__ */ jsx("span", { className: "absolute -end-1 -top-2.5", children: badge })
|
|
41
|
+
] }),
|
|
42
|
+
label && /* @__PURE__ */ jsx(
|
|
43
|
+
"span",
|
|
44
|
+
{
|
|
45
|
+
className: cn(
|
|
46
|
+
"typography-medium-caption-xs truncate text-center motion-safe:transition-colors motion-safe:duration-150 motion-safe:ease-in-out",
|
|
47
|
+
isActive ? "text-content-primary" : "text-content-tertiary"
|
|
48
|
+
),
|
|
49
|
+
children: label
|
|
50
|
+
}
|
|
51
|
+
)
|
|
52
|
+
]
|
|
53
|
+
}
|
|
54
|
+
);
|
|
55
|
+
}
|
|
15
56
|
return /* @__PURE__ */ jsxs(
|
|
16
57
|
Comp,
|
|
17
58
|
{
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"BottomNavigationAction.mjs","sources":["../../../src/components/BottomNavigation/BottomNavigationAction.tsx"],"sourcesContent":["import { Slot, Slottable } from \"@radix-ui/react-slot\";\nimport * as React from \"react\";\nimport { cn } from \"../../utils/cn\";\nimport { useBottomNavigationContext } from \"./BottomNavigation\";\n\nexport interface BottomNavigationActionProps\n extends Omit<React.ButtonHTMLAttributes<HTMLButtonElement>, \"value\"> {\n /** Unique value that identifies this action. */\n value: string;\n /** Icon element displayed in the action. */\n icon: React.ReactElement;\n /** Accessible label applied as `aria-label`. */\n label?: string;\n /** Optional badge element (e.g. {@link Count}) rendered at the top-end corner of the icon. */\n badge?: React.ReactNode;\n /** Merge props onto a child element instead of rendering a `<button>`. @default false */\n asChild?: boolean;\n}\n\nexport const BottomNavigationAction = React.forwardRef<\n HTMLButtonElement,\n BottomNavigationActionProps\n>(({ className, value, icon, label, badge, onClick, asChild = false, children, ...props }, ref) => {\n const {
|
|
1
|
+
{"version":3,"file":"BottomNavigationAction.mjs","sources":["../../../src/components/BottomNavigation/BottomNavigationAction.tsx"],"sourcesContent":["import { Slot, Slottable } from \"@radix-ui/react-slot\";\nimport * as React from \"react\";\nimport { cn } from \"../../utils/cn\";\nimport { useBottomNavigationContext } from \"./BottomNavigation\";\n\nexport interface BottomNavigationActionProps\n extends Omit<React.ButtonHTMLAttributes<HTMLButtonElement>, \"value\"> {\n /** Unique value that identifies this action. */\n value: string;\n /** Icon element displayed in the action. */\n icon: React.ReactElement;\n /** Accessible label applied as `aria-label`. */\n label?: string;\n /** Optional badge element (e.g. {@link Count}) rendered at the top-end corner of the icon. */\n badge?: React.ReactNode;\n /** Merge props onto a child element instead of rendering a `<button>`. @default false */\n asChild?: boolean;\n}\n\nexport const BottomNavigationAction = React.forwardRef<\n HTMLButtonElement,\n BottomNavigationActionProps\n>(({ className, value, icon, label, badge, onClick, asChild = false, children, ...props }, ref) => {\n const {\n value: selectedValue,\n onValueChange,\n hasInformationArchitectureNav,\n } = useBottomNavigationContext();\n\n const isActive = selectedValue === value;\n\n const handleClick = (e: React.MouseEvent<HTMLButtonElement>) => {\n onValueChange?.(value);\n onClick?.(e);\n };\n\n const Comp = asChild ? Slot : \"button\";\n\n if (hasInformationArchitectureNav) {\n return (\n <Comp\n ref={ref}\n {...(!asChild && { type: \"button\" as const })}\n aria-current={isActive ? (\"page\" as const) : undefined}\n data-state={isActive ? \"active\" : \"inactive\"}\n onClick={handleClick}\n {...props}\n className={cn(\n \"relative flex min-w-0 flex-1 cursor-pointer flex-col items-center justify-center gap-1 px-2\",\n isActive ? \"text-icons-primary\" : \"text-icons-tertiary\",\n \"motion-safe:transition-colors motion-safe:duration-150 motion-safe:ease-in-out\",\n \"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-interaction-focus focus-visible:ring-offset-2 focus-visible:ring-offset-bg-primary\",\n className,\n )}\n >\n {asChild && <Slottable>{children}</Slottable>}\n <span className=\"relative inline-flex\">\n <span className=\"flex items-center justify-center [&>svg]:size-6\" aria-hidden=\"true\">\n {icon}\n </span>\n {badge && <span className=\"absolute -end-1 -top-2.5\">{badge}</span>}\n </span>\n {label && (\n <span\n className={cn(\n \"typography-medium-caption-xs truncate text-center motion-safe:transition-colors motion-safe:duration-150 motion-safe:ease-in-out\",\n isActive ? \"text-content-primary\" : \"text-content-tertiary\",\n )}\n >\n {label}\n </span>\n )}\n </Comp>\n );\n }\n\n return (\n <Comp\n ref={ref}\n {...(!asChild && { type: \"button\" as const })}\n aria-current={isActive ? \"page\" : undefined}\n aria-label={label}\n data-state={isActive ? \"active\" : \"inactive\"}\n className={cn(\n \"relative flex min-w-0 flex-1 cursor-pointer flex-col items-center justify-center gap-0.5 overflow-hidden px-2 py-2\",\n \"text-content-primary\",\n \"motion-safe:transition-colors motion-safe:duration-150 motion-safe:ease-in-out\",\n \"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-interaction-focus focus-visible:ring-offset-2 focus-visible:ring-offset-bg-primary\",\n className,\n )}\n onClick={handleClick}\n {...props}\n >\n {asChild && <Slottable>{children}</Slottable>}\n <span className=\"relative inline-flex\">\n <span className=\"flex items-center justify-center [&>svg]:size-7\" aria-hidden=\"true\">\n {icon}\n </span>\n {badge && <span className=\"absolute -end-1 -top-2.5\">{badge}</span>}\n </span>\n </Comp>\n );\n});\n\nBottomNavigationAction.displayName = \"BottomNavigationAction\";\n"],"names":[],"mappings":";;;;;;AAmBO,MAAM,yBAAyB,MAAM,WAG1C,CAAC,EAAE,WAAW,OAAO,MAAM,OAAO,OAAO,SAAS,UAAU,OAAO,UAAU,GAAG,MAAA,GAAS,QAAQ;AACjG,QAAM;AAAA,IACJ,OAAO;AAAA,IACP;AAAA,IACA;AAAA,EAAA,IACE,2BAAA;AAEJ,QAAM,WAAW,kBAAkB;AAEnC,QAAM,cAAc,CAAC,MAA2C;AAC9D,oBAAgB,KAAK;AACrB,cAAU,CAAC;AAAA,EACb;AAEA,QAAM,OAAO,UAAU,OAAO;AAE9B,MAAI,+BAA+B;AACjC,WACE;AAAA,MAAC;AAAA,MAAA;AAAA,QACC;AAAA,QACC,GAAI,CAAC,WAAW,EAAE,MAAM,SAAA;AAAA,QACzB,gBAAc,WAAY,SAAmB;AAAA,QAC7C,cAAY,WAAW,WAAW;AAAA,QAClC,SAAS;AAAA,QACR,GAAG;AAAA,QACJ,WAAW;AAAA,UACT;AAAA,UACA,WAAW,uBAAuB;AAAA,UAClC;AAAA,UACA;AAAA,UACA;AAAA,QAAA;AAAA,QAGD,UAAA;AAAA,UAAA,WAAW,oBAAC,aAAW,SAAA,CAAS;AAAA,UACjC,qBAAC,QAAA,EAAK,WAAU,wBACd,UAAA;AAAA,YAAA,oBAAC,QAAA,EAAK,WAAU,mDAAkD,eAAY,QAC3E,UAAA,MACH;AAAA,YACC,SAAS,oBAAC,QAAA,EAAK,WAAU,4BAA4B,UAAA,MAAA,CAAM;AAAA,UAAA,GAC9D;AAAA,UACC,SACC;AAAA,YAAC;AAAA,YAAA;AAAA,cACC,WAAW;AAAA,gBACT;AAAA,gBACA,WAAW,yBAAyB;AAAA,cAAA;AAAA,cAGrC,UAAA;AAAA,YAAA;AAAA,UAAA;AAAA,QACH;AAAA,MAAA;AAAA,IAAA;AAAA,EAIR;AAEA,SACE;AAAA,IAAC;AAAA,IAAA;AAAA,MACC;AAAA,MACC,GAAI,CAAC,WAAW,EAAE,MAAM,SAAA;AAAA,MACzB,gBAAc,WAAW,SAAS;AAAA,MAClC,cAAY;AAAA,MACZ,cAAY,WAAW,WAAW;AAAA,MAClC,WAAW;AAAA,QACT;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,MAAA;AAAA,MAEF,SAAS;AAAA,MACR,GAAG;AAAA,MAEH,UAAA;AAAA,QAAA,WAAW,oBAAC,aAAW,SAAA,CAAS;AAAA,QACjC,qBAAC,QAAA,EAAK,WAAU,wBACd,UAAA;AAAA,UAAA,oBAAC,QAAA,EAAK,WAAU,mDAAkD,eAAY,QAC3E,UAAA,MACH;AAAA,UACC,SAAS,oBAAC,QAAA,EAAK,WAAU,4BAA4B,UAAA,MAAA,CAAM;AAAA,QAAA,EAAA,CAC9D;AAAA,MAAA;AAAA,IAAA;AAAA,EAAA;AAGN,CAAC;AAED,uBAAuB,cAAc;"}
|
|
@@ -207,7 +207,7 @@ function InlineSelect({ options, value, onChange, disabled, selectedOption }) {
|
|
|
207
207
|
onClick: () => setOpen((prev) => !prev),
|
|
208
208
|
className: cn(
|
|
209
209
|
"typography-semibold-body-sm text-content-primary",
|
|
210
|
-
"flex items-center gap-1 rounded-
|
|
210
|
+
"flex items-center gap-1 rounded-md px-2 py-2",
|
|
211
211
|
"hover:bg-neutral-alphas-50 focus-visible:shadow-focus-ring focus-visible:outline-none",
|
|
212
212
|
"disabled:cursor-not-allowed disabled:opacity-50",
|
|
213
213
|
"motion-safe:transition-colors"
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ChatInput.mjs","sources":["../../../src/components/ChatInput/ChatInput.tsx"],"sourcesContent":["import * as React from \"react\";\nimport { cn } from \"../../utils/cn\";\nimport { IconButton } from \"../IconButton/IconButton\";\nimport { AddIcon } from \"../Icons/AddIcon\";\nimport { ArrowUpIcon } from \"../Icons/ArrowUpIcon\";\nimport { ChevronDownIcon } from \"../Icons/ChevronDownIcon\";\n\n/** A single option for the inline model/dropdown selector. */\nexport interface ChatInputSelectOption {\n /** Unique value for this option. */\n value: string;\n /** Display label. */\n label: string;\n /** Optional icon rendered to the left of the label. */\n icon?: React.ReactNode;\n}\n\n/**\n * Props for {@link ChatInput}. Standard textarea HTML attributes are forwarded to the inner\n * `<textarea>` except `className` (applied to the outer container), `rows` (use `minRows`), and\n * `onSubmit` (replaced by the chat message submit callback).\n */\nexport interface ChatInputProps\n extends Omit<\n React.TextareaHTMLAttributes<HTMLTextAreaElement>,\n \"className\" | \"rows\" | \"onSubmit\"\n > {\n /** Minimum number of visible rows. @default 1 */\n minRows?: number;\n /** Maximum number of visible rows before scrolling. @default 6 */\n maxRows?: number;\n /** Whether a submission is in progress (disables submit, shows visual feedback). @default false */\n loading?: boolean;\n /**\n * Callback fired when the user submits (clicks the send button or presses Enter without Shift).\n * Receives the current trimmed text value.\n */\n onSubmit?: (value: string) => void;\n /**\n * When `true`, renders an \"attach file\" button in the bottom-left toolbar.\n * @default false\n */\n showFileButton?: boolean;\n /** Callback fired when the attach-file button is clicked. Only relevant when `showFileButton` is `true`. */\n onFileClick?: () => void;\n /** Accessible label for the attach-file button. @default \"Attach file\" */\n fileButtonAriaLabel?: string;\n /** Accessible label for the submit button. @default \"Send message\" */\n submitAriaLabel?: string;\n /** Icon element for the submit button. @default `<ArrowUpIcon />` */\n submitIcon?: React.ReactNode;\n /**\n * Optional content rendered in the bottom-right toolbar, to the left of the submit button.\n * When provided, takes precedence over the built-in `selectOptions` dropdown.\n */\n toolbarRight?: React.ReactNode;\n /**\n * Options for the built-in inline dropdown selector (e.g. model picker).\n * Ignored when `toolbarRight` is provided.\n */\n selectOptions?: ChatInputSelectOption[];\n /** Currently selected value for the built-in dropdown. Should match one of `selectOptions[].value`. */\n selectValue?: string;\n /** Callback fired when the user picks a different dropdown option. */\n onSelectChange?: (value: string) => void;\n /** Additional className applied to the outermost container. */\n className?: string;\n}\n\nconst LINE_HEIGHT = 18;\nconst TEXTAREA_PY = 12;\n\nfunction calculateHeight(rows: number): number {\n return LINE_HEIGHT * rows + TEXTAREA_PY * 2;\n}\n\n/**\n * A chat-style multi-line input with an integrated toolbar containing an\n * optional file-attach button, optional right-side controls (e.g. a model\n * selector), and a submit button — all inside a single bordered container.\n *\n * Designed to behave like modern AI chat inputs: auto-grows with content,\n * submits on Enter (Shift+Enter for newlines), and keeps controls inline.\n *\n * @example\n * ```tsx\n * <ChatInput\n * placeholder=\"Type a message...\"\n * onSubmit={(text) => send(text)}\n * />\n * ```\n *\n * @example\n * ```tsx\n * <ChatInput\n * placeholder=\"Ask the agent...\"\n * showFileButton\n * onFileClick={() => openFilePicker()}\n * selectOptions={[\n * { value: \"fanvue-ai\", label: \"Fanvue AI\", icon: <AIIcon className=\"size-4\" /> },\n * { value: \"example\", label: \"Example\", icon: <BulbIcon className=\"size-4\" /> },\n * ]}\n * selectValue=\"fanvue-ai\"\n * onSelectChange={(v) => setModel(v)}\n * onSubmit={(text) => send(text)}\n * />\n * ```\n */\nexport const ChatInput = React.forwardRef<HTMLTextAreaElement, ChatInputProps>(\n (\n {\n className,\n minRows = 1,\n maxRows = 6,\n disabled = false,\n loading = false,\n value,\n defaultValue,\n placeholder,\n maxLength,\n \"aria-label\": ariaLabel,\n onChange,\n onKeyDown,\n onSubmit,\n showFileButton = false,\n onFileClick,\n fileButtonAriaLabel = \"Attach file\",\n submitAriaLabel = \"Send message\",\n submitIcon,\n toolbarRight,\n selectOptions,\n selectValue,\n onSelectChange,\n style,\n ...textareaProps\n },\n ref,\n ) => {\n const internalRef = React.useRef<HTMLTextAreaElement>(null);\n const [internalValue, setInternalValue] = React.useState(defaultValue ?? \"\");\n const resolvedValue = value !== undefined ? value : internalValue;\n const isControlled = value !== undefined;\n\n const mergedRef = React.useCallback(\n (node: HTMLTextAreaElement | null) => {\n (internalRef as React.MutableRefObject<HTMLTextAreaElement | null>).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 [ref],\n );\n\n const adjustHeight = React.useCallback(() => {\n const textarea = internalRef.current;\n if (!textarea) return;\n\n const minHeight = calculateHeight(minRows);\n const maxHeight = calculateHeight(maxRows);\n\n textarea.style.height = `${minHeight}px`;\n const desired = Math.min(Math.max(textarea.scrollHeight, minHeight), maxHeight);\n textarea.style.height = `${desired}px`;\n }, [minRows, maxRows]);\n\n React.useEffect(() => {\n adjustHeight();\n }, [resolvedValue, adjustHeight]);\n\n const handleChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {\n if (!isControlled) {\n setInternalValue(e.target.value);\n }\n onChange?.(e);\n };\n\n const canSubmit = !!String(resolvedValue).trim() && !disabled && !loading;\n\n const handleSubmit = () => {\n const text = String(resolvedValue).trim();\n if (!text || !canSubmit) return;\n onSubmit?.(text);\n if (!isControlled) {\n setInternalValue(\"\");\n }\n };\n\n const handleKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {\n if (e.key === \"Enter\" && !e.shiftKey) {\n e.preventDefault();\n handleSubmit();\n }\n onKeyDown?.(e);\n };\n\n const minHeight = calculateHeight(minRows);\n const maxHeight = calculateHeight(maxRows);\n\n const selectedOption =\n selectOptions?.find((o) => o.value === selectValue) ?? selectOptions?.[0];\n const resolvedToolbarRight =\n toolbarRight ??\n (selectOptions && selectOptions.length > 0 ? (\n <InlineSelect\n options={selectOptions}\n value={selectValue}\n onChange={onSelectChange}\n disabled={disabled}\n selectedOption={selectedOption}\n />\n ) : null);\n\n return (\n <div\n className={cn(\n \"relative flex flex-col gap-6 rounded-lg border border-border-primary bg-surface-primary\",\n \"has-focus-visible:border-neutral-alphas-400 has-focus-visible:outline-none\",\n \"motion-safe:transition-colors\",\n disabled && \"opacity-50\",\n className,\n )}\n >\n <textarea\n {...textareaProps}\n ref={mergedRef}\n value={isControlled ? value : internalValue}\n placeholder={placeholder}\n maxLength={maxLength}\n disabled={disabled}\n aria-label={ariaLabel ?? placeholder}\n onChange={handleChange}\n onKeyDown={handleKeyDown}\n rows={minRows}\n className={cn(\n \"w-full resize-none bg-transparent px-4 pt-4\",\n \"typography-regular-body-md text-content-primary\",\n \"placeholder:text-content-tertiary\",\n \"focus:outline-none disabled:cursor-not-allowed\",\n \"overflow-y-auto\",\n )}\n style={{\n minHeight: `${minHeight}px`,\n maxHeight: `${maxHeight}px`,\n ...style,\n }}\n />\n\n <div className=\"flex items-center justify-between gap-2 px-4 pb-4\">\n <div className=\"flex items-center gap-1\">\n {showFileButton && (\n <IconButton\n variant=\"tertiary\"\n size=\"32\"\n icon={<AddIcon />}\n aria-label={fileButtonAriaLabel}\n onClick={onFileClick}\n disabled={disabled}\n className=\"sm:border sm:border-border-primary max-sm:-ml-2\"\n />\n )}\n </div>\n\n <div className=\"flex items-center gap-1\">\n {resolvedToolbarRight}\n <IconButton\n variant=\"primary\"\n size=\"32\"\n icon={submitIcon ?? <ArrowUpIcon />}\n aria-label={submitAriaLabel}\n onClick={handleSubmit}\n disabled={!canSubmit}\n className=\"disabled:bg-surface-secondary disabled:opacity-100 disabled:text-icons-primary\"\n />\n </div>\n </div>\n </div>\n );\n },\n);\n\nChatInput.displayName = \"ChatInput\";\n\ninterface InlineSelectProps {\n options: ChatInputSelectOption[];\n value?: string;\n onChange?: (value: string) => void;\n disabled?: boolean;\n selectedOption?: ChatInputSelectOption;\n}\n\nfunction InlineSelect({ options, value, onChange, disabled, selectedOption }: InlineSelectProps) {\n const [open, setOpen] = React.useState(false);\n const containerRef = React.useRef<HTMLDivElement>(null);\n\n React.useEffect(() => {\n if (!open) return;\n const handleClick = (e: MouseEvent) => {\n if (containerRef.current && !containerRef.current.contains(e.target as Node)) {\n setOpen(false);\n }\n };\n document.addEventListener(\"mousedown\", handleClick);\n return () => document.removeEventListener(\"mousedown\", handleClick);\n }, [open]);\n\n React.useEffect(() => {\n if (!open) return;\n const handleKey = (e: KeyboardEvent) => {\n if (e.key === \"Escape\") setOpen(false);\n };\n document.addEventListener(\"keydown\", handleKey);\n return () => document.removeEventListener(\"keydown\", handleKey);\n }, [open]);\n\n return (\n <div ref={containerRef} className=\"relative\">\n <button\n type=\"button\"\n role=\"combobox\"\n aria-expanded={open}\n aria-haspopup=\"listbox\"\n aria-label=\"Select model\"\n disabled={disabled}\n onClick={() => setOpen((prev) => !prev)}\n className={cn(\n \"typography-semibold-body-sm text-content-primary\",\n \"flex items-center gap-1 rounded-sm px-2 py-1\",\n \"hover:bg-neutral-alphas-50 focus-visible:shadow-focus-ring focus-visible:outline-none\",\n \"disabled:cursor-not-allowed disabled:opacity-50\",\n \"motion-safe:transition-colors\",\n )}\n >\n {selectedOption?.icon && (\n <span className=\"flex shrink-0 items-center [&>svg]:size-4\">{selectedOption.icon}</span>\n )}\n {selectedOption?.label ?? options[0]?.label ?? \"Select\"}\n <ChevronDownIcon\n className={cn(\"size-4 motion-safe:transition-transform\", open && \"rotate-180\")}\n />\n </button>\n\n {open && (\n <div\n role=\"listbox\"\n className={cn(\n \"absolute right-0 bottom-full z-10 mb-1 min-w-[140px]\",\n \"overflow-hidden rounded-xs border border-border-primary bg-surface-primary p-1 shadow-lg\",\n )}\n >\n {options.map((option) => (\n <div\n key={option.value}\n role=\"option\"\n tabIndex={0}\n aria-selected={option.value === value}\n className={cn(\n \"typography-regular-body-md flex cursor-pointer items-center gap-2 rounded-xs px-3 py-1.5\",\n \"text-content-primary hover:bg-neutral-alphas-50\",\n \"focus-visible:shadow-focus-ring focus-visible:outline-none\",\n option.value === value && \"bg-neutral-alphas-50\",\n )}\n onClick={() => {\n onChange?.(option.value);\n setOpen(false);\n }}\n onKeyDown={(e) => {\n if (e.key === \"Enter\" || e.key === \" \") {\n e.preventDefault();\n onChange?.(option.value);\n setOpen(false);\n }\n }}\n >\n {option.icon && (\n <span className=\"flex shrink-0 items-center [&>svg]:size-4\">{option.icon}</span>\n )}\n {option.label}\n </div>\n ))}\n </div>\n )}\n </div>\n );\n}\n"],"names":["minHeight","maxHeight"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"ChatInput.mjs","sources":["../../../src/components/ChatInput/ChatInput.tsx"],"sourcesContent":["import * as React from \"react\";\nimport { cn } from \"../../utils/cn\";\nimport { IconButton } from \"../IconButton/IconButton\";\nimport { AddIcon } from \"../Icons/AddIcon\";\nimport { ArrowUpIcon } from \"../Icons/ArrowUpIcon\";\nimport { ChevronDownIcon } from \"../Icons/ChevronDownIcon\";\n\n/** A single option for the inline model/dropdown selector. */\nexport interface ChatInputSelectOption {\n /** Unique value for this option. */\n value: string;\n /** Display label. */\n label: string;\n /** Optional icon rendered to the left of the label. */\n icon?: React.ReactNode;\n}\n\n/**\n * Props for {@link ChatInput}. Standard textarea HTML attributes are forwarded to the inner\n * `<textarea>` except `className` (applied to the outer container), `rows` (use `minRows`), and\n * `onSubmit` (replaced by the chat message submit callback).\n */\nexport interface ChatInputProps\n extends Omit<\n React.TextareaHTMLAttributes<HTMLTextAreaElement>,\n \"className\" | \"rows\" | \"onSubmit\"\n > {\n /** Minimum number of visible rows. @default 1 */\n minRows?: number;\n /** Maximum number of visible rows before scrolling. @default 6 */\n maxRows?: number;\n /** Whether a submission is in progress (disables submit, shows visual feedback). @default false */\n loading?: boolean;\n /**\n * Callback fired when the user submits (clicks the send button or presses Enter without Shift).\n * Receives the current trimmed text value.\n */\n onSubmit?: (value: string) => void;\n /**\n * When `true`, renders an \"attach file\" button in the bottom-left toolbar.\n * @default false\n */\n showFileButton?: boolean;\n /** Callback fired when the attach-file button is clicked. Only relevant when `showFileButton` is `true`. */\n onFileClick?: () => void;\n /** Accessible label for the attach-file button. @default \"Attach file\" */\n fileButtonAriaLabel?: string;\n /** Accessible label for the submit button. @default \"Send message\" */\n submitAriaLabel?: string;\n /** Icon element for the submit button. @default `<ArrowUpIcon />` */\n submitIcon?: React.ReactNode;\n /**\n * Optional content rendered in the bottom-right toolbar, to the left of the submit button.\n * When provided, takes precedence over the built-in `selectOptions` dropdown.\n */\n toolbarRight?: React.ReactNode;\n /**\n * Options for the built-in inline dropdown selector (e.g. model picker).\n * Ignored when `toolbarRight` is provided.\n */\n selectOptions?: ChatInputSelectOption[];\n /** Currently selected value for the built-in dropdown. Should match one of `selectOptions[].value`. */\n selectValue?: string;\n /** Callback fired when the user picks a different dropdown option. */\n onSelectChange?: (value: string) => void;\n /** Additional className applied to the outermost container. */\n className?: string;\n}\n\nconst LINE_HEIGHT = 18;\nconst TEXTAREA_PY = 12;\n\nfunction calculateHeight(rows: number): number {\n return LINE_HEIGHT * rows + TEXTAREA_PY * 2;\n}\n\n/**\n * A chat-style multi-line input with an integrated toolbar containing an\n * optional file-attach button, optional right-side controls (e.g. a model\n * selector), and a submit button — all inside a single bordered container.\n *\n * Designed to behave like modern AI chat inputs: auto-grows with content,\n * submits on Enter (Shift+Enter for newlines), and keeps controls inline.\n *\n * @example\n * ```tsx\n * <ChatInput\n * placeholder=\"Type a message...\"\n * onSubmit={(text) => send(text)}\n * />\n * ```\n *\n * @example\n * ```tsx\n * <ChatInput\n * placeholder=\"Ask the agent...\"\n * showFileButton\n * onFileClick={() => openFilePicker()}\n * selectOptions={[\n * { value: \"fanvue-ai\", label: \"Fanvue AI\", icon: <AIIcon className=\"size-4\" /> },\n * { value: \"example\", label: \"Example\", icon: <BulbIcon className=\"size-4\" /> },\n * ]}\n * selectValue=\"fanvue-ai\"\n * onSelectChange={(v) => setModel(v)}\n * onSubmit={(text) => send(text)}\n * />\n * ```\n */\nexport const ChatInput = React.forwardRef<HTMLTextAreaElement, ChatInputProps>(\n (\n {\n className,\n minRows = 1,\n maxRows = 6,\n disabled = false,\n loading = false,\n value,\n defaultValue,\n placeholder,\n maxLength,\n \"aria-label\": ariaLabel,\n onChange,\n onKeyDown,\n onSubmit,\n showFileButton = false,\n onFileClick,\n fileButtonAriaLabel = \"Attach file\",\n submitAriaLabel = \"Send message\",\n submitIcon,\n toolbarRight,\n selectOptions,\n selectValue,\n onSelectChange,\n style,\n ...textareaProps\n },\n ref,\n ) => {\n const internalRef = React.useRef<HTMLTextAreaElement>(null);\n const [internalValue, setInternalValue] = React.useState(defaultValue ?? \"\");\n const resolvedValue = value !== undefined ? value : internalValue;\n const isControlled = value !== undefined;\n\n const mergedRef = React.useCallback(\n (node: HTMLTextAreaElement | null) => {\n (internalRef as React.MutableRefObject<HTMLTextAreaElement | null>).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 [ref],\n );\n\n const adjustHeight = React.useCallback(() => {\n const textarea = internalRef.current;\n if (!textarea) return;\n\n const minHeight = calculateHeight(minRows);\n const maxHeight = calculateHeight(maxRows);\n\n textarea.style.height = `${minHeight}px`;\n const desired = Math.min(Math.max(textarea.scrollHeight, minHeight), maxHeight);\n textarea.style.height = `${desired}px`;\n }, [minRows, maxRows]);\n\n React.useEffect(() => {\n adjustHeight();\n }, [resolvedValue, adjustHeight]);\n\n const handleChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {\n if (!isControlled) {\n setInternalValue(e.target.value);\n }\n onChange?.(e);\n };\n\n const canSubmit = !!String(resolvedValue).trim() && !disabled && !loading;\n\n const handleSubmit = () => {\n const text = String(resolvedValue).trim();\n if (!text || !canSubmit) return;\n onSubmit?.(text);\n if (!isControlled) {\n setInternalValue(\"\");\n }\n };\n\n const handleKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {\n if (e.key === \"Enter\" && !e.shiftKey) {\n e.preventDefault();\n handleSubmit();\n }\n onKeyDown?.(e);\n };\n\n const minHeight = calculateHeight(minRows);\n const maxHeight = calculateHeight(maxRows);\n\n const selectedOption =\n selectOptions?.find((o) => o.value === selectValue) ?? selectOptions?.[0];\n const resolvedToolbarRight =\n toolbarRight ??\n (selectOptions && selectOptions.length > 0 ? (\n <InlineSelect\n options={selectOptions}\n value={selectValue}\n onChange={onSelectChange}\n disabled={disabled}\n selectedOption={selectedOption}\n />\n ) : null);\n\n return (\n <div\n className={cn(\n \"relative flex flex-col gap-6 rounded-lg border border-border-primary bg-surface-primary\",\n \"has-focus-visible:border-neutral-alphas-400 has-focus-visible:outline-none\",\n \"motion-safe:transition-colors\",\n disabled && \"opacity-50\",\n className,\n )}\n >\n <textarea\n {...textareaProps}\n ref={mergedRef}\n value={isControlled ? value : internalValue}\n placeholder={placeholder}\n maxLength={maxLength}\n disabled={disabled}\n aria-label={ariaLabel ?? placeholder}\n onChange={handleChange}\n onKeyDown={handleKeyDown}\n rows={minRows}\n className={cn(\n \"w-full resize-none bg-transparent px-4 pt-4\",\n \"typography-regular-body-md text-content-primary\",\n \"placeholder:text-content-tertiary\",\n \"focus:outline-none disabled:cursor-not-allowed\",\n \"overflow-y-auto\",\n )}\n style={{\n minHeight: `${minHeight}px`,\n maxHeight: `${maxHeight}px`,\n ...style,\n }}\n />\n\n <div className=\"flex items-center justify-between gap-2 px-4 pb-4\">\n <div className=\"flex items-center gap-1\">\n {showFileButton && (\n <IconButton\n variant=\"tertiary\"\n size=\"32\"\n icon={<AddIcon />}\n aria-label={fileButtonAriaLabel}\n onClick={onFileClick}\n disabled={disabled}\n className=\"sm:border sm:border-border-primary max-sm:-ml-2\"\n />\n )}\n </div>\n\n <div className=\"flex items-center gap-1\">\n {resolvedToolbarRight}\n <IconButton\n variant=\"primary\"\n size=\"32\"\n icon={submitIcon ?? <ArrowUpIcon />}\n aria-label={submitAriaLabel}\n onClick={handleSubmit}\n disabled={!canSubmit}\n className=\"disabled:bg-surface-secondary disabled:opacity-100 disabled:text-icons-primary\"\n />\n </div>\n </div>\n </div>\n );\n },\n);\n\nChatInput.displayName = \"ChatInput\";\n\ninterface InlineSelectProps {\n options: ChatInputSelectOption[];\n value?: string;\n onChange?: (value: string) => void;\n disabled?: boolean;\n selectedOption?: ChatInputSelectOption;\n}\n\nfunction InlineSelect({ options, value, onChange, disabled, selectedOption }: InlineSelectProps) {\n const [open, setOpen] = React.useState(false);\n const containerRef = React.useRef<HTMLDivElement>(null);\n\n React.useEffect(() => {\n if (!open) return;\n const handleClick = (e: MouseEvent) => {\n if (containerRef.current && !containerRef.current.contains(e.target as Node)) {\n setOpen(false);\n }\n };\n document.addEventListener(\"mousedown\", handleClick);\n return () => document.removeEventListener(\"mousedown\", handleClick);\n }, [open]);\n\n React.useEffect(() => {\n if (!open) return;\n const handleKey = (e: KeyboardEvent) => {\n if (e.key === \"Escape\") setOpen(false);\n };\n document.addEventListener(\"keydown\", handleKey);\n return () => document.removeEventListener(\"keydown\", handleKey);\n }, [open]);\n\n return (\n <div ref={containerRef} className=\"relative\">\n <button\n type=\"button\"\n role=\"combobox\"\n aria-expanded={open}\n aria-haspopup=\"listbox\"\n aria-label=\"Select model\"\n disabled={disabled}\n onClick={() => setOpen((prev) => !prev)}\n className={cn(\n \"typography-semibold-body-sm text-content-primary\",\n \"flex items-center gap-1 rounded-md px-2 py-2\",\n \"hover:bg-neutral-alphas-50 focus-visible:shadow-focus-ring focus-visible:outline-none\",\n \"disabled:cursor-not-allowed disabled:opacity-50\",\n \"motion-safe:transition-colors\",\n )}\n >\n {selectedOption?.icon && (\n <span className=\"flex shrink-0 items-center [&>svg]:size-4\">{selectedOption.icon}</span>\n )}\n {selectedOption?.label ?? options[0]?.label ?? \"Select\"}\n <ChevronDownIcon\n className={cn(\"size-4 motion-safe:transition-transform\", open && \"rotate-180\")}\n />\n </button>\n\n {open && (\n <div\n role=\"listbox\"\n className={cn(\n \"absolute right-0 bottom-full z-10 mb-1 min-w-[140px]\",\n \"overflow-hidden rounded-xs border border-border-primary bg-surface-primary p-1 shadow-lg\",\n )}\n >\n {options.map((option) => (\n <div\n key={option.value}\n role=\"option\"\n tabIndex={0}\n aria-selected={option.value === value}\n className={cn(\n \"typography-regular-body-md flex cursor-pointer items-center gap-2 rounded-xs px-3 py-1.5\",\n \"text-content-primary hover:bg-neutral-alphas-50\",\n \"focus-visible:shadow-focus-ring focus-visible:outline-none\",\n option.value === value && \"bg-neutral-alphas-50\",\n )}\n onClick={() => {\n onChange?.(option.value);\n setOpen(false);\n }}\n onKeyDown={(e) => {\n if (e.key === \"Enter\" || e.key === \" \") {\n e.preventDefault();\n onChange?.(option.value);\n setOpen(false);\n }\n }}\n >\n {option.icon && (\n <span className=\"flex shrink-0 items-center [&>svg]:size-4\">{option.icon}</span>\n )}\n {option.label}\n </div>\n ))}\n </div>\n )}\n </div>\n );\n}\n"],"names":["minHeight","maxHeight"],"mappings":"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"}
|
|
@@ -3,7 +3,21 @@ import { jsxs, jsx } from "react/jsx-runtime";
|
|
|
3
3
|
import * as TabsPrimitive from "@radix-ui/react-tabs";
|
|
4
4
|
import * as React from "react";
|
|
5
5
|
import { cn } from "../../utils/cn.mjs";
|
|
6
|
-
const
|
|
6
|
+
const alignLeftClasses = {
|
|
7
|
+
always: "[&>[role=tab]]:flex-initial",
|
|
8
|
+
sm: "[&>[role=tab]]:sm:flex-initial",
|
|
9
|
+
md: "[&>[role=tab]]:md:flex-initial",
|
|
10
|
+
lg: "[&>[role=tab]]:lg:flex-initial",
|
|
11
|
+
xl: "[&>[role=tab]]:xl:flex-initial"
|
|
12
|
+
};
|
|
13
|
+
function getLayoutClass(fullWidth, alignLeft) {
|
|
14
|
+
if (!fullWidth) return "inline-flex";
|
|
15
|
+
const base = "flex w-full [&>[role=tab]]:flex-1";
|
|
16
|
+
if (alignLeft === true) return `${base} ${alignLeftClasses.always}`;
|
|
17
|
+
if (typeof alignLeft === "string") return `${base} ${alignLeftClasses[alignLeft]}`;
|
|
18
|
+
return base;
|
|
19
|
+
}
|
|
20
|
+
const TabsList = React.forwardRef(({ className, children, fullWidth = true, alignLeft, ...props }, ref) => {
|
|
7
21
|
const innerRef = React.useRef(null);
|
|
8
22
|
const indicatorRef = React.useRef(null);
|
|
9
23
|
React.useImperativeHandle(ref, () => innerRef.current);
|
|
@@ -58,7 +72,7 @@ const TabsList = React.forwardRef(({ className, children, fullWidth = true, ...p
|
|
|
58
72
|
ref: innerRef,
|
|
59
73
|
className: cn(
|
|
60
74
|
"relative",
|
|
61
|
-
fullWidth
|
|
75
|
+
getLayoutClass(fullWidth, alignLeft),
|
|
62
76
|
"data-[orientation=horizontal]:items-center data-[orientation=horizontal]:shadow-[inset_0_-1px_0_0_var(--color-neutral-alphas-200)]",
|
|
63
77
|
"data-[orientation=vertical]:flex-col data-[orientation=vertical]:shadow-[inset_-1px_0_0_0_var(--color-neutral-alphas-200)]",
|
|
64
78
|
className
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"TabsList.mjs","sources":["../../../src/components/Tabs/TabsList.tsx"],"sourcesContent":["import * as TabsPrimitive from \"@radix-ui/react-tabs\";\nimport * as React from \"react\";\nimport { cn } from \"../../utils/cn\";\n\n/** Props for the {@link TabsList} component. */\nexport type TabsListProps = React.ComponentPropsWithoutRef<typeof TabsPrimitive.List> & {\n /** When `true` (the default), the tab list spans the full width of its container and each tab grows equally. Set to `false` for inline sizing. */\n fullWidth?: boolean;\n};\n\n/** Container for {@link TabsTrigger} elements. Renders a sliding active-tab indicator that animates between tabs. */\nexport const TabsList = React.forwardRef<\n React.ComponentRef<typeof TabsPrimitive.List>,\n TabsListProps\n>(({ className, children, fullWidth = true, ...props }, ref) => {\n const innerRef = React.useRef<HTMLDivElement>(null);\n const indicatorRef = React.useRef<HTMLSpanElement>(null);\n\n React.useImperativeHandle(ref, () => innerRef.current as HTMLDivElement);\n\n const updateIndicator = React.useCallback(() => {\n const list = innerRef.current;\n const indicator = indicatorRef.current;\n if (!list || !indicator) return;\n\n const activeTab = list.querySelector<HTMLElement>('[data-state=\"active\"]');\n if (!activeTab) {\n indicator.style.opacity = \"0\";\n return;\n }\n\n const isVertical = list.dataset.orientation === \"vertical\";\n\n indicator.style.opacity = \"1\";\n\n if (isVertical) {\n indicator.style.inset = `0 0 auto auto`;\n indicator.style.width = \"4px\";\n indicator.style.height = `${activeTab.offsetHeight}px`;\n indicator.style.transform = `translateY(${activeTab.offsetTop}px)`;\n } else {\n indicator.style.inset = `auto auto 0 0`;\n indicator.style.height = \"4px\";\n indicator.style.width = `${activeTab.offsetWidth}px`;\n indicator.style.transform = `translateX(${activeTab.offsetLeft}px)`;\n }\n }, []);\n\n React.useLayoutEffect(() => {\n const list = innerRef.current;\n const indicator = indicatorRef.current;\n if (!list || !indicator) return;\n\n indicator.style.transitionDuration = \"0s\";\n updateIndicator();\n indicator.getBoundingClientRect();\n indicator.style.transitionDuration = \"\";\n\n const mutationObserver = new MutationObserver(updateIndicator);\n mutationObserver.observe(list, {\n attributes: true,\n attributeFilter: [\"data-state\"],\n childList: true,\n subtree: true,\n });\n\n const resizeObserver = new ResizeObserver(updateIndicator);\n resizeObserver.observe(list);\n\n return () => {\n mutationObserver.disconnect();\n resizeObserver.disconnect();\n };\n }, [updateIndicator]);\n\n return (\n <TabsPrimitive.List\n ref={innerRef}\n className={cn(\n \"relative\",\n fullWidth
|
|
1
|
+
{"version":3,"file":"TabsList.mjs","sources":["../../../src/components/Tabs/TabsList.tsx"],"sourcesContent":["import * as TabsPrimitive from \"@radix-ui/react-tabs\";\nimport * as React from \"react\";\nimport { cn } from \"../../utils/cn\";\n\n/** Breakpoint values for responsive props. */\ntype Breakpoint = \"sm\" | \"md\" | \"lg\" | \"xl\";\n\nconst alignLeftClasses: Record<Breakpoint | \"always\", string> = {\n always: \"[&>[role=tab]]:flex-initial\",\n sm: \"[&>[role=tab]]:sm:flex-initial\",\n md: \"[&>[role=tab]]:md:flex-initial\",\n lg: \"[&>[role=tab]]:lg:flex-initial\",\n xl: \"[&>[role=tab]]:xl:flex-initial\",\n};\n\nfunction getLayoutClass(fullWidth: boolean, alignLeft?: boolean | Breakpoint): string {\n if (!fullWidth) return \"inline-flex\";\n\n const base = \"flex w-full [&>[role=tab]]:flex-1\";\n if (alignLeft === true) return `${base} ${alignLeftClasses.always}`;\n if (typeof alignLeft === \"string\") return `${base} ${alignLeftClasses[alignLeft]}`;\n return base;\n}\n\n/** Props for the {@link TabsList} component. */\nexport type TabsListProps = React.ComponentPropsWithoutRef<typeof TabsPrimitive.List> & {\n /** When `true` (the default), the tab list spans the full width of its container and each tab grows equally. Set to `false` for inline sizing. */\n fullWidth?: boolean;\n /**\n * Controls tab alignment within a full-width container.\n * - `false` (default): tabs spread evenly\n * - `true`: tabs left-aligned, sized to content\n * - `\"md\"` (breakpoint): spread on mobile, left-aligned at breakpoint and up\n */\n alignLeft?: boolean | Breakpoint;\n};\n\n/** Container for {@link TabsTrigger} elements. Renders a sliding active-tab indicator that animates between tabs. */\nexport const TabsList = React.forwardRef<\n React.ComponentRef<typeof TabsPrimitive.List>,\n TabsListProps\n>(({ className, children, fullWidth = true, alignLeft, ...props }, ref) => {\n const innerRef = React.useRef<HTMLDivElement>(null);\n const indicatorRef = React.useRef<HTMLSpanElement>(null);\n\n React.useImperativeHandle(ref, () => innerRef.current as HTMLDivElement);\n\n const updateIndicator = React.useCallback(() => {\n const list = innerRef.current;\n const indicator = indicatorRef.current;\n if (!list || !indicator) return;\n\n const activeTab = list.querySelector<HTMLElement>('[data-state=\"active\"]');\n if (!activeTab) {\n indicator.style.opacity = \"0\";\n return;\n }\n\n const isVertical = list.dataset.orientation === \"vertical\";\n\n indicator.style.opacity = \"1\";\n\n if (isVertical) {\n indicator.style.inset = `0 0 auto auto`;\n indicator.style.width = \"4px\";\n indicator.style.height = `${activeTab.offsetHeight}px`;\n indicator.style.transform = `translateY(${activeTab.offsetTop}px)`;\n } else {\n indicator.style.inset = `auto auto 0 0`;\n indicator.style.height = \"4px\";\n indicator.style.width = `${activeTab.offsetWidth}px`;\n indicator.style.transform = `translateX(${activeTab.offsetLeft}px)`;\n }\n }, []);\n\n React.useLayoutEffect(() => {\n const list = innerRef.current;\n const indicator = indicatorRef.current;\n if (!list || !indicator) return;\n\n indicator.style.transitionDuration = \"0s\";\n updateIndicator();\n indicator.getBoundingClientRect();\n indicator.style.transitionDuration = \"\";\n\n const mutationObserver = new MutationObserver(updateIndicator);\n mutationObserver.observe(list, {\n attributes: true,\n attributeFilter: [\"data-state\"],\n childList: true,\n subtree: true,\n });\n\n const resizeObserver = new ResizeObserver(updateIndicator);\n resizeObserver.observe(list);\n\n return () => {\n mutationObserver.disconnect();\n resizeObserver.disconnect();\n };\n }, [updateIndicator]);\n\n return (\n <TabsPrimitive.List\n ref={innerRef}\n className={cn(\n \"relative\",\n getLayoutClass(fullWidth, alignLeft),\n \"data-[orientation=horizontal]:items-center data-[orientation=horizontal]:shadow-[inset_0_-1px_0_0_var(--color-neutral-alphas-200)]\",\n \"data-[orientation=vertical]:flex-col data-[orientation=vertical]:shadow-[inset_-1px_0_0_0_var(--color-neutral-alphas-200)]\",\n className,\n )}\n {...props}\n >\n {children}\n <span\n ref={indicatorRef}\n aria-hidden\n className=\"pointer-events-none absolute rounded-full bg-brand-primary-default motion-safe:transition-[transform,width,height] motion-safe:duration-200 motion-safe:ease-in-out\"\n style={{ opacity: 0 }}\n />\n </TabsPrimitive.List>\n );\n});\n\nTabsList.displayName = \"TabsList\";\n"],"names":[],"mappings":";;;;;AAOA,MAAM,mBAA0D;AAAA,EAC9D,QAAQ;AAAA,EACR,IAAI;AAAA,EACJ,IAAI;AAAA,EACJ,IAAI;AAAA,EACJ,IAAI;AACN;AAEA,SAAS,eAAe,WAAoB,WAA0C;AACpF,MAAI,CAAC,UAAW,QAAO;AAEvB,QAAM,OAAO;AACb,MAAI,cAAc,KAAM,QAAO,GAAG,IAAI,IAAI,iBAAiB,MAAM;AACjE,MAAI,OAAO,cAAc,SAAU,QAAO,GAAG,IAAI,IAAI,iBAAiB,SAAS,CAAC;AAChF,SAAO;AACT;AAgBO,MAAM,WAAW,MAAM,WAG5B,CAAC,EAAE,WAAW,UAAU,YAAY,MAAM,WAAW,GAAG,MAAA,GAAS,QAAQ;AACzE,QAAM,WAAW,MAAM,OAAuB,IAAI;AAClD,QAAM,eAAe,MAAM,OAAwB,IAAI;AAEvD,QAAM,oBAAoB,KAAK,MAAM,SAAS,OAAyB;AAEvE,QAAM,kBAAkB,MAAM,YAAY,MAAM;AAC9C,UAAM,OAAO,SAAS;AACtB,UAAM,YAAY,aAAa;AAC/B,QAAI,CAAC,QAAQ,CAAC,UAAW;AAEzB,UAAM,YAAY,KAAK,cAA2B,uBAAuB;AACzE,QAAI,CAAC,WAAW;AACd,gBAAU,MAAM,UAAU;AAC1B;AAAA,IACF;AAEA,UAAM,aAAa,KAAK,QAAQ,gBAAgB;AAEhD,cAAU,MAAM,UAAU;AAE1B,QAAI,YAAY;AACd,gBAAU,MAAM,QAAQ;AACxB,gBAAU,MAAM,QAAQ;AACxB,gBAAU,MAAM,SAAS,GAAG,UAAU,YAAY;AAClD,gBAAU,MAAM,YAAY,cAAc,UAAU,SAAS;AAAA,IAC/D,OAAO;AACL,gBAAU,MAAM,QAAQ;AACxB,gBAAU,MAAM,SAAS;AACzB,gBAAU,MAAM,QAAQ,GAAG,UAAU,WAAW;AAChD,gBAAU,MAAM,YAAY,cAAc,UAAU,UAAU;AAAA,IAChE;AAAA,EACF,GAAG,CAAA,CAAE;AAEL,QAAM,gBAAgB,MAAM;AAC1B,UAAM,OAAO,SAAS;AACtB,UAAM,YAAY,aAAa;AAC/B,QAAI,CAAC,QAAQ,CAAC,UAAW;AAEzB,cAAU,MAAM,qBAAqB;AACrC,oBAAA;AACA,cAAU,sBAAA;AACV,cAAU,MAAM,qBAAqB;AAErC,UAAM,mBAAmB,IAAI,iBAAiB,eAAe;AAC7D,qBAAiB,QAAQ,MAAM;AAAA,MAC7B,YAAY;AAAA,MACZ,iBAAiB,CAAC,YAAY;AAAA,MAC9B,WAAW;AAAA,MACX,SAAS;AAAA,IAAA,CACV;AAED,UAAM,iBAAiB,IAAI,eAAe,eAAe;AACzD,mBAAe,QAAQ,IAAI;AAE3B,WAAO,MAAM;AACX,uBAAiB,WAAA;AACjB,qBAAe,WAAA;AAAA,IACjB;AAAA,EACF,GAAG,CAAC,eAAe,CAAC;AAEpB,SACE;AAAA,IAAC,cAAc;AAAA,IAAd;AAAA,MACC,KAAK;AAAA,MACL,WAAW;AAAA,QACT;AAAA,QACA,eAAe,WAAW,SAAS;AAAA,QACnC;AAAA,QACA;AAAA,QACA;AAAA,MAAA;AAAA,MAED,GAAG;AAAA,MAEH,UAAA;AAAA,QAAA;AAAA,QACD;AAAA,UAAC;AAAA,UAAA;AAAA,YACC,KAAK;AAAA,YACL,eAAW;AAAA,YACX,WAAU;AAAA,YACV,OAAO,EAAE,SAAS,EAAA;AAAA,UAAE;AAAA,QAAA;AAAA,MACtB;AAAA,IAAA;AAAA,EAAA;AAGN,CAAC;AAED,SAAS,cAAc;"}
|
package/dist/index.d.ts
CHANGED
|
@@ -452,6 +452,8 @@ export declare interface BottomNavigationProps extends React_2.HTMLAttributes<HT
|
|
|
452
452
|
onValueChange?: (value: string) => void;
|
|
453
453
|
/** When `true`, the navigation bar is hidden on viewports wider than `md` (768 px). @default false */
|
|
454
454
|
hideOnDesktop?: boolean;
|
|
455
|
+
/** When `true`, renders the information-architecture style with visible labels and a sliding active indicator. @default false */
|
|
456
|
+
hasInformationArchitectureNav?: boolean;
|
|
455
457
|
}
|
|
456
458
|
|
|
457
459
|
/**
|
|
@@ -522,6 +524,9 @@ export declare const BreadcrumbSeparator: React_2.ForwardRefExoticComponent<Brea
|
|
|
522
524
|
export declare interface BreadcrumbSeparatorProps extends React_2.ComponentPropsWithoutRef<"li"> {
|
|
523
525
|
}
|
|
524
526
|
|
|
527
|
+
/** Breakpoint values for responsive props. */
|
|
528
|
+
declare type Breakpoint = "sm" | "md" | "lg" | "xl";
|
|
529
|
+
|
|
525
530
|
export declare const BulbIcon: React_2.ForwardRefExoticComponent<React_2.SVGAttributes<SVGSVGElement> & {
|
|
526
531
|
className?: string;
|
|
527
532
|
} & React_2.RefAttributes<SVGSVGElement>>;
|
|
@@ -2691,12 +2696,26 @@ export declare type TabsContentProps = React_2.ComponentPropsWithoutRef<typeof T
|
|
|
2691
2696
|
export declare const TabsList: React_2.ForwardRefExoticComponent<Omit<TabsPrimitive.TabsListProps & React_2.RefAttributes<HTMLDivElement>, "ref"> & {
|
|
2692
2697
|
/** When `true` (the default), the tab list spans the full width of its container and each tab grows equally. Set to `false` for inline sizing. */
|
|
2693
2698
|
fullWidth?: boolean;
|
|
2699
|
+
/**
|
|
2700
|
+
* Controls tab alignment within a full-width container.
|
|
2701
|
+
* - `false` (default): tabs spread evenly
|
|
2702
|
+
* - `true`: tabs left-aligned, sized to content
|
|
2703
|
+
* - `"md"` (breakpoint): spread on mobile, left-aligned at breakpoint and up
|
|
2704
|
+
*/
|
|
2705
|
+
alignLeft?: boolean | Breakpoint;
|
|
2694
2706
|
} & React_2.RefAttributes<HTMLDivElement>>;
|
|
2695
2707
|
|
|
2696
2708
|
/** Props for the {@link TabsList} component. */
|
|
2697
2709
|
export declare type TabsListProps = React_2.ComponentPropsWithoutRef<typeof TabsPrimitive.List> & {
|
|
2698
2710
|
/** When `true` (the default), the tab list spans the full width of its container and each tab grows equally. Set to `false` for inline sizing. */
|
|
2699
2711
|
fullWidth?: boolean;
|
|
2712
|
+
/**
|
|
2713
|
+
* Controls tab alignment within a full-width container.
|
|
2714
|
+
* - `false` (default): tabs spread evenly
|
|
2715
|
+
* - `true`: tabs left-aligned, sized to content
|
|
2716
|
+
* - `"md"` (breakpoint): spread on mobile, left-aligned at breakpoint and up
|
|
2717
|
+
*/
|
|
2718
|
+
alignLeft?: boolean | Breakpoint;
|
|
2700
2719
|
};
|
|
2701
2720
|
|
|
2702
2721
|
/** Props for the {@link Tabs} root component. Extends Radix `Tabs.Root` props. */
|
package/dist/styles/base.css
CHANGED
|
@@ -119,3 +119,20 @@
|
|
|
119
119
|
@utility animate-accordion-collapse {
|
|
120
120
|
animation: accordion-collapse 200ms ease-out;
|
|
121
121
|
}
|
|
122
|
+
|
|
123
|
+
/* Static tokens not in Figma — manually maintained for now */
|
|
124
|
+
@utility typography-medium-caption-xs {
|
|
125
|
+
font-size: 0.625rem;
|
|
126
|
+
font-family: Inter, sans-serif;
|
|
127
|
+
font-weight: 500;
|
|
128
|
+
letter-spacing: 0;
|
|
129
|
+
line-height: 0.75rem;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
:root {
|
|
133
|
+
--color-special-bottom-nav-highlight: var(--primitives-color-green-500);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
.dark {
|
|
137
|
+
--color-special-bottom-nav-highlight: var(--primitives-color-gray-white);
|
|
138
|
+
}
|
package/dist/styles/theme.css
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
/*
|
|
1
|
+
/* AUTO-GENERATED — do not edit. Run `node src/styles/buildStyles.js` to regenerate. */
|
|
2
|
+
|
|
3
|
+
@import "./base.css";
|
|
2
4
|
|
|
3
5
|
@variant dark (&:where(.dark, .dark *));
|
|
4
6
|
@custom-variant infloww (&:is([data-infloww] *));
|
|
@@ -11,8 +13,6 @@
|
|
|
11
13
|
--breakpoint-lg: 1280px;
|
|
12
14
|
}
|
|
13
15
|
|
|
14
|
-
@import "./base.css";
|
|
15
|
-
|
|
16
16
|
@theme {
|
|
17
17
|
--shadow-sm: 0px 1px 3px -1px #0000000d, 0px 1px 4px 0px #0000000f;
|
|
18
18
|
--shadow-md: 0px 2px 4px -1px #0000000f, 0px 3px 10px -1px #00000014;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fanvue/ui",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.9.0",
|
|
4
4
|
"description": "React component library built with Tailwind CSS for Fanvue ecosystem",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"registry": "https://registry.npmjs.org",
|
|
@@ -168,7 +168,7 @@
|
|
|
168
168
|
"size-limit": [
|
|
169
169
|
{
|
|
170
170
|
"path": "dist/index.mjs",
|
|
171
|
-
"limit": "
|
|
171
|
+
"limit": "60 KB",
|
|
172
172
|
"ignore": [
|
|
173
173
|
"@radix-ui/*",
|
|
174
174
|
"clsx",
|