@trackunit/react-components 1.1.2 → 1.1.7

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/index.cjs.js CHANGED
@@ -271,19 +271,41 @@ const PackageNameStoryComponent = ({ packageJSON }) => {
271
271
  /**
272
272
  * Hook to detect if text content is wrapping to multiple lines
273
273
  *
274
- * @param {RefObject<HTMLElement>} ref - The ref to the element to check for text wrapping
275
- * @returns {boolean} True if the text spans multiple lines
276
- */
277
- const useIsTextWrapping = (ref) => {
278
- const [isWrapping, setIsWrapping] = react.useState(false);
274
+ * @template TElement - The type of the HTML element being observed (e.g., HTMLDivElement).
275
+ * @returns {{
276
+ * ref: RefObject<TElement | null>;
277
+ * isTooltipVisible: boolean;
278
+ * }} An object with:
279
+ * - `ref`: a ref to attach to the element you want to observe for truncation.
280
+ * - `isTextWrapping`: a boolean indicating if the text is wrapping.
281
+ */
282
+ const useIsTextWrapping = () => {
283
+ const ref = react.useRef(null);
284
+ const [isTextWrapping, setIsTextWrapping] = react.useState(false);
285
+ const setTextWrappingState = react.useCallback(() => {
286
+ if (!ref.current) {
287
+ return;
288
+ }
289
+ const { clientHeight, scrollHeight } = ref.current;
290
+ setIsTextWrapping(clientHeight > scrollHeight / 2);
291
+ }, []);
279
292
  react.useEffect(() => {
280
293
  if (!ref.current) {
281
- setIsWrapping(false);
282
294
  return;
283
295
  }
284
- setIsWrapping(ref.current.clientHeight > ref.current.scrollHeight / 2);
285
- }, [ref.current?.clientHeight, ref.current?.scrollHeight, ref]);
286
- return isWrapping;
296
+ // Perform an immediate measurement on mount.
297
+ // In some environments (especially tests) no actual "resize" event is triggered,
298
+ // so this ensures we detect the correct wrapping state right away.
299
+ setTextWrappingState();
300
+ // Observe resizing to check if wrapping changes
301
+ const observer = new ResizeObserver(() => {
302
+ setTextWrappingState();
303
+ });
304
+ observer.observe(ref.current);
305
+ // Clean up on unmount
306
+ return () => observer.disconnect();
307
+ }, [setTextWrappingState]);
308
+ return { ref, isTextWrapping };
287
309
  };
288
310
 
289
311
  const cvaText = cssClassVarianceUtilities.cvaMerge(["text-black", "m-0", "relative", "text-sm", "font-normal"], {
@@ -756,15 +778,14 @@ const cvaAlertIconContainer = cssClassVarianceUtilities.cvaMerge(["self-start",
756
778
  */
757
779
  const Alert = ({ color = "info", title, className, children, primaryAction, secondaryAction, onClose, dataTestId, autoScroll, }) => {
758
780
  const ref = react.useRef(null);
759
- const titleRef = react.useRef(null);
760
- const isWrapping = useIsTextWrapping(titleRef);
781
+ const { isTextWrapping, ref: titleRef } = useIsTextWrapping();
761
782
  react.useEffect(() => {
762
783
  if (autoScroll) {
763
784
  // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
764
785
  ref.current?.scrollIntoView?.();
765
786
  }
766
787
  }, [ref, autoScroll]);
767
- return (jsxRuntime.jsxs("div", { className: cvaAlert({ color, className }), "data-testid": dataTestId, ref: ref, role: "alert", children: [jsxRuntime.jsxs("div", { className: cvaAlertContentContainer({ inline: !isWrapping && !children }), children: [jsxRuntime.jsx("div", { className: cvaAlertIconContainer(), children: jsxRuntime.jsx(Icon, { color: color, name: getIconName(color) }) }), jsxRuntime.jsxs("div", { className: cvaContent(), children: [title ? (jsxRuntime.jsx(Text, { dataTestId: `${dataTestId}-title`, ref: titleRef, weight: "bold", children: title })) : null, children ? (jsxRuntime.jsx(Text, { type: typeof children === "string" || typeof children === "number" ? "p" : "span", weight: !title ? "bold" : "normal", children: children })) : null] }), onClose ? (jsxRuntime.jsx("div", { className: cvaAlertCloseButtonContainer(), children: jsxRuntime.jsx(IconButton, { circular: true, icon: jsxRuntime.jsx(Icon, { name: "XMark", size: "small" }), onClick: onClose, size: "small", title: "Close Alert", variant: "ghost-neutral" }) })) : null] }), primaryAction || secondaryAction ? (jsxRuntime.jsxs("div", { className: cvaAlertActionsContainer(), children: [secondaryAction ? (jsxRuntime.jsx(Button, { loading: secondaryAction.loading, onClick: secondaryAction.onClick, size: "small", variant: "ghost-neutral", children: secondaryAction.label })) : null, primaryAction ? (jsxRuntime.jsx(Button, { loading: primaryAction.loading, onClick: primaryAction.onClick, size: "small", variant: color === "danger" ? "primary-danger" : "primary", children: primaryAction.label })) : null] })) : null] }));
788
+ return (jsxRuntime.jsxs("div", { className: cvaAlert({ color, className }), "data-testid": dataTestId, ref: ref, role: "alert", children: [jsxRuntime.jsxs("div", { className: cvaAlertContentContainer({ inline: !isTextWrapping && !children }), children: [jsxRuntime.jsx("div", { className: cvaAlertIconContainer(), children: jsxRuntime.jsx(Icon, { color: color, name: getIconName(color) }) }), jsxRuntime.jsxs("div", { className: cvaContent(), children: [title ? (jsxRuntime.jsx(Text, { dataTestId: `${dataTestId}-title`, ref: titleRef, weight: "bold", children: title })) : null, children ? (jsxRuntime.jsx(Text, { type: typeof children === "string" || typeof children === "number" ? "p" : "span", weight: !title ? "bold" : "normal", children: children })) : null] }), onClose ? (jsxRuntime.jsx("div", { className: cvaAlertCloseButtonContainer(), children: jsxRuntime.jsx(IconButton, { circular: true, icon: jsxRuntime.jsx(Icon, { name: "XMark", size: "small" }), onClick: onClose, size: "small", title: "Close Alert", variant: "ghost-neutral" }) })) : null] }), primaryAction || secondaryAction ? (jsxRuntime.jsxs("div", { className: cvaAlertActionsContainer(), children: [secondaryAction ? (jsxRuntime.jsx(Button, { loading: secondaryAction.loading, onClick: secondaryAction.onClick, size: "small", variant: "ghost-neutral", children: secondaryAction.label })) : null, primaryAction ? (jsxRuntime.jsx(Button, { loading: primaryAction.loading, onClick: primaryAction.onClick, size: "small", variant: color === "danger" ? "primary-danger" : "primary", children: primaryAction.label })) : null] })) : null] }));
768
789
  };
769
790
  const getIconName = (color) => {
770
791
  switch (color) {
@@ -1226,21 +1247,49 @@ const useIsFullscreen = () => {
1226
1247
  };
1227
1248
 
1228
1249
  /**
1229
- * Check if text is cut off.
1250
+ * A custom hook that determines if text within an element is truncated
1251
+ * (i.e., the text overflows the container).
1230
1252
  *
1231
- * @param {RefObject<HTMLElement>} ref The ref to the element to check.
1232
- * @returns {boolean} True if the text is cut off.
1253
+ * @template TElement - The type of the HTML element being observed (e.g., HTMLDivElement).
1254
+ * @param {string} [text] - (Optional) Text used to trigger a re-check of truncation,
1255
+ * especially if the text is dynamic (such as an input's value).
1256
+ * @returns {{
1257
+ * ref: RefObject<TElement | null>;
1258
+ * isTooltipVisible: boolean;
1259
+ * }} An object with:
1260
+ * - `ref`: a ref to attach to the element you want to observe for truncation.
1261
+ * - `isTextTruncated`: a boolean indicating if the text is truncated.
1233
1262
  */
1234
- const useIsTextCutOff = (ref) => {
1235
- const [isTextCutOff, setIsTextCutOff] = react.useState(false);
1263
+ const useIsTextTruncated = (text) => {
1264
+ const ref = react.useRef(null);
1265
+ const [isTextTruncated, setIsTextTruncated] = react.useState(false);
1266
+ const updateTextVisibility = react.useCallback(() => {
1267
+ if (!ref.current) {
1268
+ return;
1269
+ }
1270
+ const { scrollWidth, clientWidth } = ref.current;
1271
+ setIsTextTruncated(scrollWidth > clientWidth);
1272
+ }, []);
1236
1273
  react.useEffect(() => {
1237
1274
  if (!ref.current) {
1238
- setIsTextCutOff(false);
1239
1275
  return;
1240
1276
  }
1241
- setIsTextCutOff(ref.current.offsetWidth < ref.current.scrollWidth);
1242
- }, [ref.current?.offsetWidth, ref.current?.scrollWidth, ref]);
1243
- return isTextCutOff;
1277
+ // Observe resizing to check if truncation changes
1278
+ const observer = new ResizeObserver(() => {
1279
+ updateTextVisibility();
1280
+ });
1281
+ observer.observe(ref.current);
1282
+ // Clean up on unmount
1283
+ return () => observer.disconnect();
1284
+ }, [updateTextVisibility]);
1285
+ // Re-check whenever text changes
1286
+ react.useEffect(() => {
1287
+ if (!text) {
1288
+ return;
1289
+ }
1290
+ updateTextVisibility();
1291
+ }, [text, updateTextVisibility]);
1292
+ return { ref, isTextTruncated };
1244
1293
  };
1245
1294
 
1246
1295
  /**
@@ -1307,7 +1356,7 @@ const useScrollDetection = (elementRef) => {
1307
1356
  }
1308
1357
  const { scrollTop, scrollHeight, clientHeight } = element;
1309
1358
  setIsAtTop(scrollTop === 0);
1310
- setIsAtBottom(Math.abs(scrollHeight - scrollTop - clientHeight) < 1);
1359
+ setIsAtBottom(Math.abs(scrollHeight - scrollTop - clientHeight) <= 1);
1311
1360
  setScrollPosition(prev => ({ ...prev, top: scrollTop, bottom: clientHeight - scrollTop }));
1312
1361
  }, [elementRef]);
1313
1362
  const debouncedCheckScrollPosition = usehooksTs.useDebounceCallback(checkScrollPosition, SCROLL_DEBOUNCE_TIME);
@@ -2615,7 +2664,7 @@ const cvaKPICardValueContainer = cssClassVarianceUtilities.cvaMerge([], {
2615
2664
  },
2616
2665
  });
2617
2666
 
2618
- const LoadingContent = () => (jsxRuntime.jsx("div", { className: "flex flex-row items-center gap-3", "data-testid": "kpi-card-loading-content", children: jsxRuntime.jsx("div", { className: "w-full", children: jsxRuntime.jsx(SkeletonLines, { height: [18, 20], lines: 2, margin: "3px 0", width: [50, 100] }) }) }));
2667
+ const LoadingContent$1 = () => (jsxRuntime.jsx("div", { className: "flex flex-row items-center gap-3", "data-testid": "kpi-card-loading-content", children: jsxRuntime.jsx("div", { className: "w-full", children: jsxRuntime.jsx(SkeletonLines, { height: [18, 20], lines: 2, margin: "3px 0", width: [50, 100] }) }) }));
2619
2668
  /**
2620
2669
  * The KPICard component is used to display KPIs.
2621
2670
  *
@@ -2625,7 +2674,7 @@ const LoadingContent = () => (jsxRuntime.jsx("div", { className: "flex flex-row
2625
2674
  const KPICard = ({ asChild = false, title, value, loading, unit, className, dataTestId, tooltipLabel, isActive, variant = "default", trend, onClick, ...rest }) => {
2626
2675
  const Comp = asChild ? reactSlot.Slot : "div";
2627
2676
  const isSmallVariant = variant === "small";
2628
- return (jsxRuntime.jsxs(Comp, { className: cvaKPICardContainer({ className, isClickable: Boolean(asChild || onClick) }), "data-testid": `${dataTestId}-comp`, onClick: onClick, ...rest, children: [jsxRuntime.jsx(Tooltip, { className: "w-full", disabled: !tooltipLabel, label: tooltipLabel, placement: "bottom", children: jsxRuntime.jsx(Card, { className: cvaKPICard({ isClickable: Boolean((onClick || asChild) && !loading), isActive, variant }), children: loading ? (jsxRuntime.jsx(LoadingContent, {})) : (jsxRuntime.jsxs("div", { children: [jsxRuntime.jsx(Text, { dataTestId: `${dataTestId}-title`, size: isSmallVariant ? "small" : "medium", subtle: true, weight: isSmallVariant ? "normal" : "bold", children: title }), jsxRuntime.jsx(Text, { className: isSmallVariant ? "mt-0.5" : "", dataTestId: `${dataTestId}-value`, size: isSmallVariant ? "small" : "large", type: "div", weight: isSmallVariant ? "bold" : "thick", children: jsxRuntime.jsxs("div", { className: cvaKPICardValueContainer({
2677
+ return (jsxRuntime.jsxs(Comp, { className: cvaKPICardContainer({ className, isClickable: Boolean(asChild || onClick) }), "data-testid": `${dataTestId}-comp`, onClick: onClick, ...rest, children: [jsxRuntime.jsx(Tooltip, { className: "w-full", disabled: !tooltipLabel, label: tooltipLabel, placement: "bottom", children: jsxRuntime.jsx(Card, { className: cvaKPICard({ isClickable: Boolean((onClick || asChild) && !loading), isActive, variant }), children: loading ? (jsxRuntime.jsx(LoadingContent$1, {})) : (jsxRuntime.jsxs("div", { children: [jsxRuntime.jsx(Text, { dataTestId: `${dataTestId}-title`, size: isSmallVariant ? "small" : "medium", subtle: true, weight: isSmallVariant ? "normal" : "bold", children: title }), jsxRuntime.jsx(Text, { className: isSmallVariant ? "mt-0.5" : "", dataTestId: `${dataTestId}-value`, size: isSmallVariant ? "small" : "large", type: "div", weight: isSmallVariant ? "bold" : "thick", children: jsxRuntime.jsxs("div", { className: cvaKPICardValueContainer({
2629
2678
  isDefaultAndHasTrendValue: Boolean(trend?.value && !isSmallVariant),
2630
2679
  }), children: [jsxRuntime.jsxs("span", { children: [value, " ", unit] }), jsxRuntime.jsx(TrendIndicator, { isSmallVariant: isSmallVariant, trend: trend, unit: unit })] }) })] })) }) }), !loading && jsxRuntime.jsx(reactSlot.Slottable, { children: rest.children })] }));
2631
2680
  };
@@ -2908,7 +2957,7 @@ const cvaMoreMenu = cssClassVarianceUtilities.cvaMerge(["p-0"]);
2908
2957
  */
2909
2958
  const MoreMenu = ({ className, dataTestId, popoverProps, iconProps = {
2910
2959
  size: "medium",
2911
- className: "text-secondary-800",
2960
+ className: "text-secondary-400",
2912
2961
  }, iconButtonProps = {
2913
2962
  size: "medium",
2914
2963
  circular: false,
@@ -3027,6 +3076,63 @@ const PageContent = ({ className, children, dataTestId, layout }) => {
3027
3076
  return (jsxRuntime.jsx("div", { className: cvaPageContent({ className, layout }), "data-testid": dataTestId ? dataTestId : "page-content", children: children }));
3028
3077
  };
3029
3078
 
3079
+ const LoadingContent = () => (jsxRuntime.jsx("div", { className: "flex flex-row items-center gap-3", "data-testid": "kpi-card-loading-content", children: jsxRuntime.jsx("div", { className: "w-full", children: jsxRuntime.jsx(SkeletonLines, { height: [16, 25], lines: 2, margin: "3px 0 0 0", width: [75, 50] }) }) }));
3080
+ const PageHeaderKpiMetrics = ({ kpiMetrics }) => {
3081
+ return (jsxRuntime.jsx("div", { className: "hidden items-center gap-4 md:flex", children: kpiMetrics
3082
+ .filter(kpi => !kpi.hidden)
3083
+ .map((kpi, index) => {
3084
+ if (kpi.loading) {
3085
+ return jsxRuntime.jsx(LoadingContent, {}, `${kpi}-${index}`);
3086
+ }
3087
+ return (jsxRuntime.jsxs("div", { className: "flex flex-col text-nowrap text-left", children: [jsxRuntime.jsx("span", { className: "text-xs text-slate-500", children: kpi.header }), jsxRuntime.jsxs("div", { className: "flex flex-row items-center gap-1", children: [jsxRuntime.jsx("span", { className: "text-lg font-medium text-slate-900", children: kpi.value }), kpi.unit ? jsxRuntime.jsx("span", { className: "text-xs text-slate-900", children: kpi.unit }) : null] })] }, `${kpi}-${index}`));
3088
+ }) }));
3089
+ };
3090
+
3091
+ function ActionRenderer({ action, isMenuItem = false, externalOnClick }) {
3092
+ const { to, tooltipLabel, prefixIconName, disabled, actionText, actionCallback, dataTestId, target, variant } = action;
3093
+ // This component handles all the "wrapping" logic for Link/Tooltip
3094
+ // The "content" is either a Button or a MenuItem, depending on `isMenuItem`
3095
+ const content = isMenuItem ? (jsxRuntime.jsx(MenuItem, { dataTestId: dataTestId, disabled: disabled, label: actionText, onClick: e => {
3096
+ actionCallback?.(e);
3097
+ externalOnClick?.();
3098
+ }, prefix: prefixIconName ? jsxRuntime.jsx(Icon, { name: prefixIconName, size: "medium" }) : null, variant: variant === "secondary-danger" ? "danger" : "primary" })) : (jsxRuntime.jsx(Button, { dataTestId: dataTestId, disabled: disabled, onClick: e => {
3099
+ actionCallback?.(e);
3100
+ externalOnClick?.();
3101
+ }, prefix: prefixIconName ? jsxRuntime.jsx(Icon, { name: prefixIconName, size: "small" }) : undefined, size: "medium", variant: variant, children: actionText }));
3102
+ // Wrap `content` with Tooltip
3103
+ const wrappedWithTooltip = tooltipLabel ? (jsxRuntime.jsx(Tooltip, { className: "block", label: tooltipLabel, children: content })) : (content);
3104
+ // Finally, wrap with Link if `to` is provided
3105
+ return to ? (jsxRuntime.jsx(reactRouter.Link, { target: target, to: to, children: wrappedWithTooltip })) : (wrappedWithTooltip);
3106
+ }
3107
+ const PageHeaderSecondaryActions = ({ actions, hasPrimaryAction, }) => {
3108
+ const enabledActions = react.useMemo(() => actions.filter(action => !action.hidden), [actions]);
3109
+ // If we need to render a "More Menu" because we have too many actions:
3110
+ if (enabledActions.length > 2 || (hasPrimaryAction && enabledActions.length > 1)) {
3111
+ // Separate them into danger vs. other
3112
+ const [dangerActions, otherActions] = enabledActions.reduce(([danger, others], action) => {
3113
+ if (action.variant === "secondary-danger") {
3114
+ return [[...danger, action], others];
3115
+ }
3116
+ else {
3117
+ return [danger, [...others, action]];
3118
+ }
3119
+ }, [[], []]);
3120
+ return (jsxRuntime.jsx(MoreMenu, { dataTestId: "secondary-actions-more-menu", children: close => (jsxRuntime.jsxs(MenuList, { className: "min-w-[160px]", children: [otherActions.map((action, index) => (jsxRuntime.jsx(ActionRenderer, { action: action, externalOnClick: close, isMenuItem: true }, `${action.actionText}-${index}`))), dangerActions.length ? jsxRuntime.jsx(MenuDivider, {}) : null, dangerActions.map((action, index) => (jsxRuntime.jsx(ActionRenderer, { action: action, externalOnClick: close, isMenuItem: true }, `${action.actionText}-${index}`)))] })) }));
3121
+ }
3122
+ // Otherwise, render them inline as buttons
3123
+ return (jsxRuntime.jsx("div", { className: "flex flex-row items-center gap-2", children: enabledActions
3124
+ .toSorted((a, b) => {
3125
+ if (a.variant === "secondary" && b.variant === "secondary-danger") {
3126
+ return 1;
3127
+ }
3128
+ else if (a.variant === "secondary-danger" && b.variant === "secondary") {
3129
+ return -1;
3130
+ }
3131
+ return 0;
3132
+ })
3133
+ .map((action, index) => (jsxRuntime.jsx(ActionRenderer, { action: action }, `${action.actionText}-${index}`))) }));
3134
+ };
3135
+
3030
3136
  const cvaPageHeaderContainer = cssClassVarianceUtilities.cvaMerge(["bg-white", "tu-page-header"], {
3031
3137
  variants: {
3032
3138
  withBorder: {
@@ -3035,53 +3141,39 @@ const cvaPageHeaderContainer = cssClassVarianceUtilities.cvaMerge(["bg-white", "
3035
3141
  },
3036
3142
  },
3037
3143
  });
3038
- const cvaPageHeader = cssClassVarianceUtilities.cvaMerge([
3039
- "box-border",
3040
- "py-responsive-space",
3041
- "px-responsive-space-lg",
3042
- "flex",
3043
- "flex-wrap",
3044
- "items-center",
3045
- "gap-x-4",
3046
- "gap-y-1",
3047
- "justify-between",
3048
- ]);
3049
- const cvaPageHeaderHeading = cssClassVarianceUtilities.cvaMerge(["flex", "text-slate-900", "text-2xl", "font-bold", "min-w-0"]);
3050
- const cvaPageHeaderHeadingContainer = cssClassVarianceUtilities.cvaMerge(["flex", "gap-x-4", "flex-wrap", "items-center"]);
3051
- const cvaPageHeaderHeadingAccessoriesContainer = cssClassVarianceUtilities.cvaMerge(["flex", "flex-wrap", "items-center", "gap-x-4", "gap-y-2", "grow"], {
3052
- variants: {
3053
- actionPosition: {
3054
- right: ["justify-between"],
3055
- },
3056
- },
3057
- });
3058
- const cvaPageHeaderChildContainer = cssClassVarianceUtilities.cvaMerge(["flex", "flex-wrap", "gap-x-6", "items-center"]);
3059
- cssClassVarianceUtilities.cvaMerge(["flex", "h-fit", "items-center", "gap-1"]);
3060
- const cvaPageHeaderLink = cssClassVarianceUtilities.cvaMerge([
3061
- "text-primary-600",
3062
- "hover:text-primary-700",
3063
- "flex",
3064
- "items-center",
3065
- "gap-1",
3066
- "text-xs",
3067
- "underline",
3068
- ]);
3144
+ const cvaPageHeader = cssClassVarianceUtilities.cvaMerge(["box-border", "py-4", "px-6", "flex", "items-center", "gap-y-1"]);
3145
+ const cvaPageHeaderHeading = cssClassVarianceUtilities.cvaMerge(["text-slate-900", "text-2xl", "font-bold", "truncate"]);
3146
+
3147
+ const PageHeaderTitle = ({ title, dataTestId }) => {
3148
+ const { ref, isTextTruncated } = useIsTextTruncated();
3149
+ return (jsxRuntime.jsx("div", { className: "flex flex-row items-center", children: jsxRuntime.jsx(Tooltip, { className: "grid min-w-16", disabled: !isTextTruncated, label: title, placement: "top", children: jsxRuntime.jsx("h1", { className: cvaPageHeaderHeading(), "data-testid": dataTestId ? `${dataTestId}-heading` : undefined, ref: ref, children: title }) }) }));
3150
+ };
3069
3151
 
3070
3152
  /**
3071
- * Pageheader component is used to display a page header.
3153
+ * The PageHeader component is used to display the header of a page.
3072
3154
  *
3073
- * PageHeader can be used to highlight the page topic, display important information about the page, and carry the action items related to the current page.
3155
+ * It provides critical context by indicating the current location within the application or website.
3156
+ * PageHeader can be used to highlight the page topic, display helpful information about the page, and carry the action items related to the current page.
3157
+ * Tabs can be added to the PageHeader to allow users to navigate between different sections of the page.
3074
3158
  *
3075
3159
  * @param {PageHeaderProps} props - The props for the PageHeader component
3076
3160
  * @returns {JSX.Element} PageHeader component
3077
3161
  */
3078
- const PageHeader = ({ className, dataTestId, action, actionPosition, showLoading, description, link, title, tag, backTo, tagColor, children, tabsList, descriptionIcon, linkText = "Help Center Article", }) => {
3162
+ const PageHeader = ({ className, dataTestId, secondaryActions, showLoading, description, title, tagLabel, backTo, tagColor, tabsList, descriptionIcon, kpiMetrics, tagTooltipLabel, primaryAction, }) => {
3163
+ const tagRenderer = react.useMemo(() => {
3164
+ if (!tagLabel || showLoading) {
3165
+ return null;
3166
+ }
3167
+ // If the user passes a string, we render the string as a tag with props provided.
3168
+ return (jsxRuntime.jsx("div", { className: "ml-auto flex flex-row gap-2", children: jsxRuntime.jsx(Tooltip, { dataTestId: "page-header-tag-tooltip", disabled: !tagTooltipLabel, label: tagTooltipLabel, placement: "top", children: jsxRuntime.jsx(Tag, { color: tagColor, dataTestId: "page-header-tag", children: tagLabel }) }) }));
3169
+ }, [showLoading, tagColor, tagLabel, tagTooltipLabel]);
3079
3170
  return (jsxRuntime.jsxs("div", { className: cvaPageHeaderContainer({
3080
3171
  className,
3081
3172
  withBorder: !tabsList,
3082
- }), "data-testid": dataTestId, children: [jsxRuntime.jsxs("div", { className: cvaPageHeader(), children: [jsxRuntime.jsxs("div", { className: cvaPageHeaderHeadingAccessoriesContainer({
3083
- actionPosition: actionPosition ? actionPosition : null,
3084
- }), "data-testid": dataTestId ? `${dataTestId}-heading-container` : undefined, children: [jsxRuntime.jsxs("div", { className: cvaPageHeaderHeadingContainer(), children: [backTo ? (jsxRuntime.jsx(Button, { asChild: true, className: "bg-black/5 hover:bg-black/10", prefix: jsxRuntime.jsx(Icon, { name: "ArrowLeft", size: "small" }), size: "small", square: true, variant: "ghost-neutral", children: jsxRuntime.jsx(reactRouter.Link, { to: backTo }) })) : undefined, jsxRuntime.jsxs("div", { className: "flex flex-col", children: [jsxRuntime.jsxs("div", { className: "flex flex-row flex-wrap items-center gap-x-4 gap-y-2", children: [jsxRuntime.jsx("h1", { className: cvaPageHeaderHeading(), "data-testid": dataTestId ? `${dataTestId}-heading` : undefined, children: title }), description && !showLoading ? (jsxRuntime.jsx(Tooltip, { dataTestId: dataTestId ? `${dataTestId}-tooltip` : undefined, iconProps: { name: descriptionIcon ?? "QuestionMarkCircle" }, label: description, placement: "bottom" })) : undefined, tag && !showLoading ? (jsxRuntime.jsx(Tag, { color: tagColor, dataTestId: dataTestId ? `${dataTestId}-tag` : undefined, children: tag })) : undefined] }), link ? (jsxRuntime.jsxs(reactRouter.Link, { className: cvaPageHeaderLink(), "data-testid": "tooltip-link", target: "_blank", to: link, children: [linkText, jsxRuntime.jsx(Icon, { name: "ArrowTopRightOnSquare", size: "small" })] })) : undefined] }), showLoading ? (jsxRuntime.jsx(Spinner, { centering: "vertically", dataTestId: dataTestId ? `${dataTestId}-spinner` : undefined })) : null] }), jsxRuntime.jsx("div", { children: action })] }), children ? jsxRuntime.jsx("div", { className: cvaPageHeaderChildContainer(), children: children }) : null] }), tabsList] }));
3173
+ }), "data-testid": dataTestId, children: [jsxRuntime.jsxs("div", { className: cvaPageHeader(), children: [backTo ? (jsxRuntime.jsx(reactRouter.Link, { to: backTo, children: jsxRuntime.jsx(Button, { className: "mr-4 bg-black/5 hover:bg-black/10", prefix: jsxRuntime.jsx(Icon, { name: "ArrowLeft", size: "small" }), size: "small", square: true, variant: "ghost-neutral" }) })) : undefined, typeof title === "string" ? jsxRuntime.jsx(PageHeaderTitle, { dataTestId: dataTestId, title: title }) : title, tagRenderer || description ? (jsxRuntime.jsxs("div", { className: "mx-2 flex items-center gap-2", children: [description && !showLoading ? (jsxRuntime.jsx(Tooltip, { dataTestId: dataTestId ? `${dataTestId}-description-tooltip` : undefined, iconProps: {
3174
+ name: descriptionIcon ?? "QuestionMarkCircle",
3175
+ dataTestId: "page-header-description-icon",
3176
+ }, label: description, placement: "bottom" })) : undefined, tagRenderer] })) : null, jsxRuntime.jsxs("div", { className: "ml-auto flex gap-2", children: [kpiMetrics ? jsxRuntime.jsx(PageHeaderKpiMetrics, { kpiMetrics: kpiMetrics }) : null, Array.isArray(secondaryActions) ? (jsxRuntime.jsx(PageHeaderSecondaryActions, { actions: secondaryActions, hasPrimaryAction: !!primaryAction })) : secondaryActions ? (secondaryActions) : null, primaryAction && !primaryAction.hidden ? (jsxRuntime.jsx(Tooltip, { disabled: !primaryAction.tooltipLabel, label: primaryAction.tooltipLabel, children: jsxRuntime.jsx(Button, { dataTestId: primaryAction.dataTestId, disabled: primaryAction.disabled, loading: primaryAction.loading, onClick: () => primaryAction.actionCallback?.(), prefix: primaryAction.prefixIconName ? jsxRuntime.jsx(Icon, { name: primaryAction.prefixIconName, size: "small" }) : undefined, size: "medium", variant: primaryAction.variant, children: primaryAction.actionText }) })) : null] })] }), tabsList] }));
3085
3177
  };
3086
3178
 
3087
3179
  const cvaPagination = cssClassVarianceUtilities.cvaMerge(["flex", "items-center", "gap-1"]);
@@ -3165,7 +3257,6 @@ const Polygon = ({ points, size, color = "black", opaque = true, className, data
3165
3257
  const normalize = ({ value, min, max, size }) => ((value - min) / (max - min)) * size;
3166
3258
 
3167
3259
  function useConfirmExit(confirmExit, when = true) {
3168
- // eslint-disable-next-line sonarjs/deprecation
3169
3260
  reactRouter.useBlocker(confirmExit, when);
3170
3261
  }
3171
3262
  /**
@@ -3371,8 +3462,8 @@ const cvaTab = cssClassVarianceUtilities.cvaMerge([
3371
3462
  "justify-center",
3372
3463
  "gap-2",
3373
3464
  "text-sm",
3374
- "px-4",
3375
- "py-2.5",
3465
+ "px-6",
3466
+ "py-2",
3376
3467
  "cursor-pointer",
3377
3468
  "transition",
3378
3469
  "duration-200",
@@ -3477,7 +3568,6 @@ const ToggleGroup = ({ list, selected, setSelected, onChange, disabled = false,
3477
3568
  * Individual ToggleItem to create custom ToggleGroups
3478
3569
  */
3479
3570
  const ToggleItem = ({ title, onClick, disabled = false, isIconOnly = false, iconName, size, className, selected = false, text, tooltip, dataTestId, }) => {
3480
- // eslint-disable-next-line sonarjs/no-selector-parameter
3481
3571
  return isIconOnly ? (jsxRuntime.jsx(Tooltip, { label: tooltip?.content || title, placement: tooltip?.placement || "top", children: jsxRuntime.jsx(IconButton, { className: cvaToggleItem({ className, selected }), "data-testid": dataTestId, disabled: disabled, icon: iconName ? (jsxRuntime.jsx(Icon, { className: selected ? "text-slate-600" : "text-slate-400", name: iconName, size: "small" })) : null, onClick: onClick, size: size, title: iconName, variant: "secondary" }) })) : (jsxRuntime.jsx(Tooltip, { disabled: !text?.truncate && tooltip?.content === undefined, label: tooltip?.content || title, placement: tooltip?.placement || "top", children: jsxRuntime.jsx(Button, { className: cvaToggleItem({ className, selected }), "data-testid": dataTestId, disabled: disabled, onClick: onClick, prefix: iconName ? (jsxRuntime.jsx(Icon, { className: selected ? "text-slate-600" : "text-slate-400", name: iconName, size: "small" })) : null, size: size, variant: "secondary", children: jsxRuntime.jsx("span", { className: cvaToggleItemText({ disabledAndSelected: disabled && selected, maxWidth: text?.maxWidth }), children: title }) }) }));
3482
3572
  };
3483
3573
 
@@ -3716,6 +3806,7 @@ const cvaClickable = cssClassVarianceUtilities.cvaMerge([
3716
3806
  },
3717
3807
  });
3718
3808
 
3809
+ exports.ActionRenderer = ActionRenderer;
3719
3810
  exports.Alert = Alert;
3720
3811
  exports.Badge = Badge;
3721
3812
  exports.Breadcrumb = Breadcrumb;
@@ -3745,6 +3836,9 @@ exports.PackageNameStoryComponent = PackageNameStoryComponent;
3745
3836
  exports.Page = Page;
3746
3837
  exports.PageContent = PageContent;
3747
3838
  exports.PageHeader = PageHeader;
3839
+ exports.PageHeaderKpiMetrics = PageHeaderKpiMetrics;
3840
+ exports.PageHeaderSecondaryActions = PageHeaderSecondaryActions;
3841
+ exports.PageHeaderTitle = PageHeaderTitle;
3748
3842
  exports.Pagination = Pagination;
3749
3843
  exports.Polygon = Polygon;
3750
3844
  exports.Popover = Popover;
@@ -3789,6 +3883,9 @@ exports.cvaMenuItemLabel = cvaMenuItemLabel;
3789
3883
  exports.cvaMenuItemPrefix = cvaMenuItemPrefix;
3790
3884
  exports.cvaMenuItemStyle = cvaMenuItemStyle;
3791
3885
  exports.cvaMenuItemSuffix = cvaMenuItemSuffix;
3886
+ exports.cvaPageHeader = cvaPageHeader;
3887
+ exports.cvaPageHeaderContainer = cvaPageHeaderContainer;
3888
+ exports.cvaPageHeaderHeading = cvaPageHeaderHeading;
3792
3889
  exports.docs = docs;
3793
3890
  exports.getDevicePixelRatio = getDevicePixelRatio;
3794
3891
  exports.getValueBarColorByValue = getValueBarColorByValue;
@@ -3804,7 +3901,7 @@ exports.useGeometry = useGeometry;
3804
3901
  exports.useHover = useHover;
3805
3902
  exports.useIsFirstRender = useIsFirstRender;
3806
3903
  exports.useIsFullscreen = useIsFullscreen;
3807
- exports.useIsTextCutOff = useIsTextCutOff;
3904
+ exports.useIsTextTruncated = useIsTextTruncated;
3808
3905
  exports.useOverflowItems = useOverflowItems;
3809
3906
  exports.usePopoverContext = usePopoverContext;
3810
3907
  exports.usePrompt = usePrompt;
package/index.esm.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
2
- import { useRef, useMemo, useEffect, forwardRef, useState, createElement, useCallback, memo, createContext, useContext, isValidElement, cloneElement, Children } from 'react';
2
+ import { useRef, useMemo, useEffect, forwardRef, useState, useCallback, createElement, memo, createContext, useContext, isValidElement, cloneElement, Children } from 'react';
3
3
  import { objectKeys, uuidv4, objectEntries, objectValues } from '@trackunit/shared-utils';
4
4
  import { rentalStatusPalette, intentPalette, generalPalette, criticalityPalette, activityPalette, utilizationPalette, sitesPalette, themeScreenSizeAsNumber, color } from '@trackunit/ui-design-tokens';
5
5
  import { iconNames } from '@trackunit/ui-icons';
@@ -269,19 +269,41 @@ const PackageNameStoryComponent = ({ packageJSON }) => {
269
269
  /**
270
270
  * Hook to detect if text content is wrapping to multiple lines
271
271
  *
272
- * @param {RefObject<HTMLElement>} ref - The ref to the element to check for text wrapping
273
- * @returns {boolean} True if the text spans multiple lines
272
+ * @template TElement - The type of the HTML element being observed (e.g., HTMLDivElement).
273
+ * @returns {{
274
+ * ref: RefObject<TElement | null>;
275
+ * isTooltipVisible: boolean;
276
+ * }} An object with:
277
+ * - `ref`: a ref to attach to the element you want to observe for truncation.
278
+ * - `isTextWrapping`: a boolean indicating if the text is wrapping.
274
279
  */
275
- const useIsTextWrapping = (ref) => {
276
- const [isWrapping, setIsWrapping] = useState(false);
280
+ const useIsTextWrapping = () => {
281
+ const ref = useRef(null);
282
+ const [isTextWrapping, setIsTextWrapping] = useState(false);
283
+ const setTextWrappingState = useCallback(() => {
284
+ if (!ref.current) {
285
+ return;
286
+ }
287
+ const { clientHeight, scrollHeight } = ref.current;
288
+ setIsTextWrapping(clientHeight > scrollHeight / 2);
289
+ }, []);
277
290
  useEffect(() => {
278
291
  if (!ref.current) {
279
- setIsWrapping(false);
280
292
  return;
281
293
  }
282
- setIsWrapping(ref.current.clientHeight > ref.current.scrollHeight / 2);
283
- }, [ref.current?.clientHeight, ref.current?.scrollHeight, ref]);
284
- return isWrapping;
294
+ // Perform an immediate measurement on mount.
295
+ // In some environments (especially tests) no actual "resize" event is triggered,
296
+ // so this ensures we detect the correct wrapping state right away.
297
+ setTextWrappingState();
298
+ // Observe resizing to check if wrapping changes
299
+ const observer = new ResizeObserver(() => {
300
+ setTextWrappingState();
301
+ });
302
+ observer.observe(ref.current);
303
+ // Clean up on unmount
304
+ return () => observer.disconnect();
305
+ }, [setTextWrappingState]);
306
+ return { ref, isTextWrapping };
285
307
  };
286
308
 
287
309
  const cvaText = cvaMerge(["text-black", "m-0", "relative", "text-sm", "font-normal"], {
@@ -754,15 +776,14 @@ const cvaAlertIconContainer = cvaMerge(["self-start", "shrink-0", "grid", "w-min
754
776
  */
755
777
  const Alert = ({ color = "info", title, className, children, primaryAction, secondaryAction, onClose, dataTestId, autoScroll, }) => {
756
778
  const ref = useRef(null);
757
- const titleRef = useRef(null);
758
- const isWrapping = useIsTextWrapping(titleRef);
779
+ const { isTextWrapping, ref: titleRef } = useIsTextWrapping();
759
780
  useEffect(() => {
760
781
  if (autoScroll) {
761
782
  // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
762
783
  ref.current?.scrollIntoView?.();
763
784
  }
764
785
  }, [ref, autoScroll]);
765
- return (jsxs("div", { className: cvaAlert({ color, className }), "data-testid": dataTestId, ref: ref, role: "alert", children: [jsxs("div", { className: cvaAlertContentContainer({ inline: !isWrapping && !children }), children: [jsx("div", { className: cvaAlertIconContainer(), children: jsx(Icon, { color: color, name: getIconName(color) }) }), jsxs("div", { className: cvaContent(), children: [title ? (jsx(Text, { dataTestId: `${dataTestId}-title`, ref: titleRef, weight: "bold", children: title })) : null, children ? (jsx(Text, { type: typeof children === "string" || typeof children === "number" ? "p" : "span", weight: !title ? "bold" : "normal", children: children })) : null] }), onClose ? (jsx("div", { className: cvaAlertCloseButtonContainer(), children: jsx(IconButton, { circular: true, icon: jsx(Icon, { name: "XMark", size: "small" }), onClick: onClose, size: "small", title: "Close Alert", variant: "ghost-neutral" }) })) : null] }), primaryAction || secondaryAction ? (jsxs("div", { className: cvaAlertActionsContainer(), children: [secondaryAction ? (jsx(Button, { loading: secondaryAction.loading, onClick: secondaryAction.onClick, size: "small", variant: "ghost-neutral", children: secondaryAction.label })) : null, primaryAction ? (jsx(Button, { loading: primaryAction.loading, onClick: primaryAction.onClick, size: "small", variant: color === "danger" ? "primary-danger" : "primary", children: primaryAction.label })) : null] })) : null] }));
786
+ return (jsxs("div", { className: cvaAlert({ color, className }), "data-testid": dataTestId, ref: ref, role: "alert", children: [jsxs("div", { className: cvaAlertContentContainer({ inline: !isTextWrapping && !children }), children: [jsx("div", { className: cvaAlertIconContainer(), children: jsx(Icon, { color: color, name: getIconName(color) }) }), jsxs("div", { className: cvaContent(), children: [title ? (jsx(Text, { dataTestId: `${dataTestId}-title`, ref: titleRef, weight: "bold", children: title })) : null, children ? (jsx(Text, { type: typeof children === "string" || typeof children === "number" ? "p" : "span", weight: !title ? "bold" : "normal", children: children })) : null] }), onClose ? (jsx("div", { className: cvaAlertCloseButtonContainer(), children: jsx(IconButton, { circular: true, icon: jsx(Icon, { name: "XMark", size: "small" }), onClick: onClose, size: "small", title: "Close Alert", variant: "ghost-neutral" }) })) : null] }), primaryAction || secondaryAction ? (jsxs("div", { className: cvaAlertActionsContainer(), children: [secondaryAction ? (jsx(Button, { loading: secondaryAction.loading, onClick: secondaryAction.onClick, size: "small", variant: "ghost-neutral", children: secondaryAction.label })) : null, primaryAction ? (jsx(Button, { loading: primaryAction.loading, onClick: primaryAction.onClick, size: "small", variant: color === "danger" ? "primary-danger" : "primary", children: primaryAction.label })) : null] })) : null] }));
766
787
  };
767
788
  const getIconName = (color) => {
768
789
  switch (color) {
@@ -1224,21 +1245,49 @@ const useIsFullscreen = () => {
1224
1245
  };
1225
1246
 
1226
1247
  /**
1227
- * Check if text is cut off.
1248
+ * A custom hook that determines if text within an element is truncated
1249
+ * (i.e., the text overflows the container).
1228
1250
  *
1229
- * @param {RefObject<HTMLElement>} ref The ref to the element to check.
1230
- * @returns {boolean} True if the text is cut off.
1251
+ * @template TElement - The type of the HTML element being observed (e.g., HTMLDivElement).
1252
+ * @param {string} [text] - (Optional) Text used to trigger a re-check of truncation,
1253
+ * especially if the text is dynamic (such as an input's value).
1254
+ * @returns {{
1255
+ * ref: RefObject<TElement | null>;
1256
+ * isTooltipVisible: boolean;
1257
+ * }} An object with:
1258
+ * - `ref`: a ref to attach to the element you want to observe for truncation.
1259
+ * - `isTextTruncated`: a boolean indicating if the text is truncated.
1231
1260
  */
1232
- const useIsTextCutOff = (ref) => {
1233
- const [isTextCutOff, setIsTextCutOff] = useState(false);
1261
+ const useIsTextTruncated = (text) => {
1262
+ const ref = useRef(null);
1263
+ const [isTextTruncated, setIsTextTruncated] = useState(false);
1264
+ const updateTextVisibility = useCallback(() => {
1265
+ if (!ref.current) {
1266
+ return;
1267
+ }
1268
+ const { scrollWidth, clientWidth } = ref.current;
1269
+ setIsTextTruncated(scrollWidth > clientWidth);
1270
+ }, []);
1234
1271
  useEffect(() => {
1235
1272
  if (!ref.current) {
1236
- setIsTextCutOff(false);
1237
1273
  return;
1238
1274
  }
1239
- setIsTextCutOff(ref.current.offsetWidth < ref.current.scrollWidth);
1240
- }, [ref.current?.offsetWidth, ref.current?.scrollWidth, ref]);
1241
- return isTextCutOff;
1275
+ // Observe resizing to check if truncation changes
1276
+ const observer = new ResizeObserver(() => {
1277
+ updateTextVisibility();
1278
+ });
1279
+ observer.observe(ref.current);
1280
+ // Clean up on unmount
1281
+ return () => observer.disconnect();
1282
+ }, [updateTextVisibility]);
1283
+ // Re-check whenever text changes
1284
+ useEffect(() => {
1285
+ if (!text) {
1286
+ return;
1287
+ }
1288
+ updateTextVisibility();
1289
+ }, [text, updateTextVisibility]);
1290
+ return { ref, isTextTruncated };
1242
1291
  };
1243
1292
 
1244
1293
  /**
@@ -1305,7 +1354,7 @@ const useScrollDetection = (elementRef) => {
1305
1354
  }
1306
1355
  const { scrollTop, scrollHeight, clientHeight } = element;
1307
1356
  setIsAtTop(scrollTop === 0);
1308
- setIsAtBottom(Math.abs(scrollHeight - scrollTop - clientHeight) < 1);
1357
+ setIsAtBottom(Math.abs(scrollHeight - scrollTop - clientHeight) <= 1);
1309
1358
  setScrollPosition(prev => ({ ...prev, top: scrollTop, bottom: clientHeight - scrollTop }));
1310
1359
  }, [elementRef]);
1311
1360
  const debouncedCheckScrollPosition = useDebounceCallback(checkScrollPosition, SCROLL_DEBOUNCE_TIME);
@@ -2613,7 +2662,7 @@ const cvaKPICardValueContainer = cvaMerge([], {
2613
2662
  },
2614
2663
  });
2615
2664
 
2616
- const LoadingContent = () => (jsx("div", { className: "flex flex-row items-center gap-3", "data-testid": "kpi-card-loading-content", children: jsx("div", { className: "w-full", children: jsx(SkeletonLines, { height: [18, 20], lines: 2, margin: "3px 0", width: [50, 100] }) }) }));
2665
+ const LoadingContent$1 = () => (jsx("div", { className: "flex flex-row items-center gap-3", "data-testid": "kpi-card-loading-content", children: jsx("div", { className: "w-full", children: jsx(SkeletonLines, { height: [18, 20], lines: 2, margin: "3px 0", width: [50, 100] }) }) }));
2617
2666
  /**
2618
2667
  * The KPICard component is used to display KPIs.
2619
2668
  *
@@ -2623,7 +2672,7 @@ const LoadingContent = () => (jsx("div", { className: "flex flex-row items-cente
2623
2672
  const KPICard = ({ asChild = false, title, value, loading, unit, className, dataTestId, tooltipLabel, isActive, variant = "default", trend, onClick, ...rest }) => {
2624
2673
  const Comp = asChild ? Slot : "div";
2625
2674
  const isSmallVariant = variant === "small";
2626
- return (jsxs(Comp, { className: cvaKPICardContainer({ className, isClickable: Boolean(asChild || onClick) }), "data-testid": `${dataTestId}-comp`, onClick: onClick, ...rest, children: [jsx(Tooltip, { className: "w-full", disabled: !tooltipLabel, label: tooltipLabel, placement: "bottom", children: jsx(Card, { className: cvaKPICard({ isClickable: Boolean((onClick || asChild) && !loading), isActive, variant }), children: loading ? (jsx(LoadingContent, {})) : (jsxs("div", { children: [jsx(Text, { dataTestId: `${dataTestId}-title`, size: isSmallVariant ? "small" : "medium", subtle: true, weight: isSmallVariant ? "normal" : "bold", children: title }), jsx(Text, { className: isSmallVariant ? "mt-0.5" : "", dataTestId: `${dataTestId}-value`, size: isSmallVariant ? "small" : "large", type: "div", weight: isSmallVariant ? "bold" : "thick", children: jsxs("div", { className: cvaKPICardValueContainer({
2675
+ return (jsxs(Comp, { className: cvaKPICardContainer({ className, isClickable: Boolean(asChild || onClick) }), "data-testid": `${dataTestId}-comp`, onClick: onClick, ...rest, children: [jsx(Tooltip, { className: "w-full", disabled: !tooltipLabel, label: tooltipLabel, placement: "bottom", children: jsx(Card, { className: cvaKPICard({ isClickable: Boolean((onClick || asChild) && !loading), isActive, variant }), children: loading ? (jsx(LoadingContent$1, {})) : (jsxs("div", { children: [jsx(Text, { dataTestId: `${dataTestId}-title`, size: isSmallVariant ? "small" : "medium", subtle: true, weight: isSmallVariant ? "normal" : "bold", children: title }), jsx(Text, { className: isSmallVariant ? "mt-0.5" : "", dataTestId: `${dataTestId}-value`, size: isSmallVariant ? "small" : "large", type: "div", weight: isSmallVariant ? "bold" : "thick", children: jsxs("div", { className: cvaKPICardValueContainer({
2627
2676
  isDefaultAndHasTrendValue: Boolean(trend?.value && !isSmallVariant),
2628
2677
  }), children: [jsxs("span", { children: [value, " ", unit] }), jsx(TrendIndicator, { isSmallVariant: isSmallVariant, trend: trend, unit: unit })] }) })] })) }) }), !loading && jsx(Slottable, { children: rest.children })] }));
2629
2678
  };
@@ -2906,7 +2955,7 @@ const cvaMoreMenu = cvaMerge(["p-0"]);
2906
2955
  */
2907
2956
  const MoreMenu = ({ className, dataTestId, popoverProps, iconProps = {
2908
2957
  size: "medium",
2909
- className: "text-secondary-800",
2958
+ className: "text-secondary-400",
2910
2959
  }, iconButtonProps = {
2911
2960
  size: "medium",
2912
2961
  circular: false,
@@ -3025,6 +3074,63 @@ const PageContent = ({ className, children, dataTestId, layout }) => {
3025
3074
  return (jsx("div", { className: cvaPageContent({ className, layout }), "data-testid": dataTestId ? dataTestId : "page-content", children: children }));
3026
3075
  };
3027
3076
 
3077
+ const LoadingContent = () => (jsx("div", { className: "flex flex-row items-center gap-3", "data-testid": "kpi-card-loading-content", children: jsx("div", { className: "w-full", children: jsx(SkeletonLines, { height: [16, 25], lines: 2, margin: "3px 0 0 0", width: [75, 50] }) }) }));
3078
+ const PageHeaderKpiMetrics = ({ kpiMetrics }) => {
3079
+ return (jsx("div", { className: "hidden items-center gap-4 md:flex", children: kpiMetrics
3080
+ .filter(kpi => !kpi.hidden)
3081
+ .map((kpi, index) => {
3082
+ if (kpi.loading) {
3083
+ return jsx(LoadingContent, {}, `${kpi}-${index}`);
3084
+ }
3085
+ return (jsxs("div", { className: "flex flex-col text-nowrap text-left", children: [jsx("span", { className: "text-xs text-slate-500", children: kpi.header }), jsxs("div", { className: "flex flex-row items-center gap-1", children: [jsx("span", { className: "text-lg font-medium text-slate-900", children: kpi.value }), kpi.unit ? jsx("span", { className: "text-xs text-slate-900", children: kpi.unit }) : null] })] }, `${kpi}-${index}`));
3086
+ }) }));
3087
+ };
3088
+
3089
+ function ActionRenderer({ action, isMenuItem = false, externalOnClick }) {
3090
+ const { to, tooltipLabel, prefixIconName, disabled, actionText, actionCallback, dataTestId, target, variant } = action;
3091
+ // This component handles all the "wrapping" logic for Link/Tooltip
3092
+ // The "content" is either a Button or a MenuItem, depending on `isMenuItem`
3093
+ const content = isMenuItem ? (jsx(MenuItem, { dataTestId: dataTestId, disabled: disabled, label: actionText, onClick: e => {
3094
+ actionCallback?.(e);
3095
+ externalOnClick?.();
3096
+ }, prefix: prefixIconName ? jsx(Icon, { name: prefixIconName, size: "medium" }) : null, variant: variant === "secondary-danger" ? "danger" : "primary" })) : (jsx(Button, { dataTestId: dataTestId, disabled: disabled, onClick: e => {
3097
+ actionCallback?.(e);
3098
+ externalOnClick?.();
3099
+ }, prefix: prefixIconName ? jsx(Icon, { name: prefixIconName, size: "small" }) : undefined, size: "medium", variant: variant, children: actionText }));
3100
+ // Wrap `content` with Tooltip
3101
+ const wrappedWithTooltip = tooltipLabel ? (jsx(Tooltip, { className: "block", label: tooltipLabel, children: content })) : (content);
3102
+ // Finally, wrap with Link if `to` is provided
3103
+ return to ? (jsx(Link, { target: target, to: to, children: wrappedWithTooltip })) : (wrappedWithTooltip);
3104
+ }
3105
+ const PageHeaderSecondaryActions = ({ actions, hasPrimaryAction, }) => {
3106
+ const enabledActions = useMemo(() => actions.filter(action => !action.hidden), [actions]);
3107
+ // If we need to render a "More Menu" because we have too many actions:
3108
+ if (enabledActions.length > 2 || (hasPrimaryAction && enabledActions.length > 1)) {
3109
+ // Separate them into danger vs. other
3110
+ const [dangerActions, otherActions] = enabledActions.reduce(([danger, others], action) => {
3111
+ if (action.variant === "secondary-danger") {
3112
+ return [[...danger, action], others];
3113
+ }
3114
+ else {
3115
+ return [danger, [...others, action]];
3116
+ }
3117
+ }, [[], []]);
3118
+ return (jsx(MoreMenu, { dataTestId: "secondary-actions-more-menu", children: close => (jsxs(MenuList, { className: "min-w-[160px]", children: [otherActions.map((action, index) => (jsx(ActionRenderer, { action: action, externalOnClick: close, isMenuItem: true }, `${action.actionText}-${index}`))), dangerActions.length ? jsx(MenuDivider, {}) : null, dangerActions.map((action, index) => (jsx(ActionRenderer, { action: action, externalOnClick: close, isMenuItem: true }, `${action.actionText}-${index}`)))] })) }));
3119
+ }
3120
+ // Otherwise, render them inline as buttons
3121
+ return (jsx("div", { className: "flex flex-row items-center gap-2", children: enabledActions
3122
+ .toSorted((a, b) => {
3123
+ if (a.variant === "secondary" && b.variant === "secondary-danger") {
3124
+ return 1;
3125
+ }
3126
+ else if (a.variant === "secondary-danger" && b.variant === "secondary") {
3127
+ return -1;
3128
+ }
3129
+ return 0;
3130
+ })
3131
+ .map((action, index) => (jsx(ActionRenderer, { action: action }, `${action.actionText}-${index}`))) }));
3132
+ };
3133
+
3028
3134
  const cvaPageHeaderContainer = cvaMerge(["bg-white", "tu-page-header"], {
3029
3135
  variants: {
3030
3136
  withBorder: {
@@ -3033,53 +3139,39 @@ const cvaPageHeaderContainer = cvaMerge(["bg-white", "tu-page-header"], {
3033
3139
  },
3034
3140
  },
3035
3141
  });
3036
- const cvaPageHeader = cvaMerge([
3037
- "box-border",
3038
- "py-responsive-space",
3039
- "px-responsive-space-lg",
3040
- "flex",
3041
- "flex-wrap",
3042
- "items-center",
3043
- "gap-x-4",
3044
- "gap-y-1",
3045
- "justify-between",
3046
- ]);
3047
- const cvaPageHeaderHeading = cvaMerge(["flex", "text-slate-900", "text-2xl", "font-bold", "min-w-0"]);
3048
- const cvaPageHeaderHeadingContainer = cvaMerge(["flex", "gap-x-4", "flex-wrap", "items-center"]);
3049
- const cvaPageHeaderHeadingAccessoriesContainer = cvaMerge(["flex", "flex-wrap", "items-center", "gap-x-4", "gap-y-2", "grow"], {
3050
- variants: {
3051
- actionPosition: {
3052
- right: ["justify-between"],
3053
- },
3054
- },
3055
- });
3056
- const cvaPageHeaderChildContainer = cvaMerge(["flex", "flex-wrap", "gap-x-6", "items-center"]);
3057
- cvaMerge(["flex", "h-fit", "items-center", "gap-1"]);
3058
- const cvaPageHeaderLink = cvaMerge([
3059
- "text-primary-600",
3060
- "hover:text-primary-700",
3061
- "flex",
3062
- "items-center",
3063
- "gap-1",
3064
- "text-xs",
3065
- "underline",
3066
- ]);
3142
+ const cvaPageHeader = cvaMerge(["box-border", "py-4", "px-6", "flex", "items-center", "gap-y-1"]);
3143
+ const cvaPageHeaderHeading = cvaMerge(["text-slate-900", "text-2xl", "font-bold", "truncate"]);
3144
+
3145
+ const PageHeaderTitle = ({ title, dataTestId }) => {
3146
+ const { ref, isTextTruncated } = useIsTextTruncated();
3147
+ return (jsx("div", { className: "flex flex-row items-center", children: jsx(Tooltip, { className: "grid min-w-16", disabled: !isTextTruncated, label: title, placement: "top", children: jsx("h1", { className: cvaPageHeaderHeading(), "data-testid": dataTestId ? `${dataTestId}-heading` : undefined, ref: ref, children: title }) }) }));
3148
+ };
3067
3149
 
3068
3150
  /**
3069
- * Pageheader component is used to display a page header.
3151
+ * The PageHeader component is used to display the header of a page.
3070
3152
  *
3071
- * PageHeader can be used to highlight the page topic, display important information about the page, and carry the action items related to the current page.
3153
+ * It provides critical context by indicating the current location within the application or website.
3154
+ * PageHeader can be used to highlight the page topic, display helpful information about the page, and carry the action items related to the current page.
3155
+ * Tabs can be added to the PageHeader to allow users to navigate between different sections of the page.
3072
3156
  *
3073
3157
  * @param {PageHeaderProps} props - The props for the PageHeader component
3074
3158
  * @returns {JSX.Element} PageHeader component
3075
3159
  */
3076
- const PageHeader = ({ className, dataTestId, action, actionPosition, showLoading, description, link, title, tag, backTo, tagColor, children, tabsList, descriptionIcon, linkText = "Help Center Article", }) => {
3160
+ const PageHeader = ({ className, dataTestId, secondaryActions, showLoading, description, title, tagLabel, backTo, tagColor, tabsList, descriptionIcon, kpiMetrics, tagTooltipLabel, primaryAction, }) => {
3161
+ const tagRenderer = useMemo(() => {
3162
+ if (!tagLabel || showLoading) {
3163
+ return null;
3164
+ }
3165
+ // If the user passes a string, we render the string as a tag with props provided.
3166
+ return (jsx("div", { className: "ml-auto flex flex-row gap-2", children: jsx(Tooltip, { dataTestId: "page-header-tag-tooltip", disabled: !tagTooltipLabel, label: tagTooltipLabel, placement: "top", children: jsx(Tag, { color: tagColor, dataTestId: "page-header-tag", children: tagLabel }) }) }));
3167
+ }, [showLoading, tagColor, tagLabel, tagTooltipLabel]);
3077
3168
  return (jsxs("div", { className: cvaPageHeaderContainer({
3078
3169
  className,
3079
3170
  withBorder: !tabsList,
3080
- }), "data-testid": dataTestId, children: [jsxs("div", { className: cvaPageHeader(), children: [jsxs("div", { className: cvaPageHeaderHeadingAccessoriesContainer({
3081
- actionPosition: actionPosition ? actionPosition : null,
3082
- }), "data-testid": dataTestId ? `${dataTestId}-heading-container` : undefined, children: [jsxs("div", { className: cvaPageHeaderHeadingContainer(), children: [backTo ? (jsx(Button, { asChild: true, className: "bg-black/5 hover:bg-black/10", prefix: jsx(Icon, { name: "ArrowLeft", size: "small" }), size: "small", square: true, variant: "ghost-neutral", children: jsx(Link, { to: backTo }) })) : undefined, jsxs("div", { className: "flex flex-col", children: [jsxs("div", { className: "flex flex-row flex-wrap items-center gap-x-4 gap-y-2", children: [jsx("h1", { className: cvaPageHeaderHeading(), "data-testid": dataTestId ? `${dataTestId}-heading` : undefined, children: title }), description && !showLoading ? (jsx(Tooltip, { dataTestId: dataTestId ? `${dataTestId}-tooltip` : undefined, iconProps: { name: descriptionIcon ?? "QuestionMarkCircle" }, label: description, placement: "bottom" })) : undefined, tag && !showLoading ? (jsx(Tag, { color: tagColor, dataTestId: dataTestId ? `${dataTestId}-tag` : undefined, children: tag })) : undefined] }), link ? (jsxs(Link, { className: cvaPageHeaderLink(), "data-testid": "tooltip-link", target: "_blank", to: link, children: [linkText, jsx(Icon, { name: "ArrowTopRightOnSquare", size: "small" })] })) : undefined] }), showLoading ? (jsx(Spinner, { centering: "vertically", dataTestId: dataTestId ? `${dataTestId}-spinner` : undefined })) : null] }), jsx("div", { children: action })] }), children ? jsx("div", { className: cvaPageHeaderChildContainer(), children: children }) : null] }), tabsList] }));
3171
+ }), "data-testid": dataTestId, children: [jsxs("div", { className: cvaPageHeader(), children: [backTo ? (jsx(Link, { to: backTo, children: jsx(Button, { className: "mr-4 bg-black/5 hover:bg-black/10", prefix: jsx(Icon, { name: "ArrowLeft", size: "small" }), size: "small", square: true, variant: "ghost-neutral" }) })) : undefined, typeof title === "string" ? jsx(PageHeaderTitle, { dataTestId: dataTestId, title: title }) : title, tagRenderer || description ? (jsxs("div", { className: "mx-2 flex items-center gap-2", children: [description && !showLoading ? (jsx(Tooltip, { dataTestId: dataTestId ? `${dataTestId}-description-tooltip` : undefined, iconProps: {
3172
+ name: descriptionIcon ?? "QuestionMarkCircle",
3173
+ dataTestId: "page-header-description-icon",
3174
+ }, label: description, placement: "bottom" })) : undefined, tagRenderer] })) : null, jsxs("div", { className: "ml-auto flex gap-2", children: [kpiMetrics ? jsx(PageHeaderKpiMetrics, { kpiMetrics: kpiMetrics }) : null, Array.isArray(secondaryActions) ? (jsx(PageHeaderSecondaryActions, { actions: secondaryActions, hasPrimaryAction: !!primaryAction })) : secondaryActions ? (secondaryActions) : null, primaryAction && !primaryAction.hidden ? (jsx(Tooltip, { disabled: !primaryAction.tooltipLabel, label: primaryAction.tooltipLabel, children: jsx(Button, { dataTestId: primaryAction.dataTestId, disabled: primaryAction.disabled, loading: primaryAction.loading, onClick: () => primaryAction.actionCallback?.(), prefix: primaryAction.prefixIconName ? jsx(Icon, { name: primaryAction.prefixIconName, size: "small" }) : undefined, size: "medium", variant: primaryAction.variant, children: primaryAction.actionText }) })) : null] })] }), tabsList] }));
3083
3175
  };
3084
3176
 
3085
3177
  const cvaPagination = cvaMerge(["flex", "items-center", "gap-1"]);
@@ -3163,7 +3255,6 @@ const Polygon = ({ points, size, color = "black", opaque = true, className, data
3163
3255
  const normalize = ({ value, min, max, size }) => ((value - min) / (max - min)) * size;
3164
3256
 
3165
3257
  function useConfirmExit(confirmExit, when = true) {
3166
- // eslint-disable-next-line sonarjs/deprecation
3167
3258
  useBlocker(confirmExit, when);
3168
3259
  }
3169
3260
  /**
@@ -3369,8 +3460,8 @@ const cvaTab = cvaMerge([
3369
3460
  "justify-center",
3370
3461
  "gap-2",
3371
3462
  "text-sm",
3372
- "px-4",
3373
- "py-2.5",
3463
+ "px-6",
3464
+ "py-2",
3374
3465
  "cursor-pointer",
3375
3466
  "transition",
3376
3467
  "duration-200",
@@ -3475,7 +3566,6 @@ const ToggleGroup = ({ list, selected, setSelected, onChange, disabled = false,
3475
3566
  * Individual ToggleItem to create custom ToggleGroups
3476
3567
  */
3477
3568
  const ToggleItem = ({ title, onClick, disabled = false, isIconOnly = false, iconName, size, className, selected = false, text, tooltip, dataTestId, }) => {
3478
- // eslint-disable-next-line sonarjs/no-selector-parameter
3479
3569
  return isIconOnly ? (jsx(Tooltip, { label: tooltip?.content || title, placement: tooltip?.placement || "top", children: jsx(IconButton, { className: cvaToggleItem({ className, selected }), "data-testid": dataTestId, disabled: disabled, icon: iconName ? (jsx(Icon, { className: selected ? "text-slate-600" : "text-slate-400", name: iconName, size: "small" })) : null, onClick: onClick, size: size, title: iconName, variant: "secondary" }) })) : (jsx(Tooltip, { disabled: !text?.truncate && tooltip?.content === undefined, label: tooltip?.content || title, placement: tooltip?.placement || "top", children: jsx(Button, { className: cvaToggleItem({ className, selected }), "data-testid": dataTestId, disabled: disabled, onClick: onClick, prefix: iconName ? (jsx(Icon, { className: selected ? "text-slate-600" : "text-slate-400", name: iconName, size: "small" })) : null, size: size, variant: "secondary", children: jsx("span", { className: cvaToggleItemText({ disabledAndSelected: disabled && selected, maxWidth: text?.maxWidth }), children: title }) }) }));
3480
3570
  };
3481
3571
 
@@ -3714,4 +3804,4 @@ const cvaClickable = cvaMerge([
3714
3804
  },
3715
3805
  });
3716
3806
 
3717
- export { Alert, Badge, Breadcrumb, BreadcrumbContainer, Button, Card, CardBody, CardFooter, CardHeader, Collapse, CompletionStatusIndicator, CopyableText, EmptyState, EmptyValue, ExternalLink, Heading, Icon, IconButton, Indicator, KPICard, MenuDivider, MenuItem, MenuList, MoreMenu, Notice, PackageNameStoryComponent, Page, PageContent, PageHeader, Pagination, Polygon, Popover, PopoverContent, PopoverTitle, PopoverTrigger, Portal, Prompt, ROLE_CARD, SectionHeader, Sidebar, SkeletonLines, Spacer, Spinner, StarButton, Tab, TabContent, TabList, Tabs, Tag, Text, ToggleGroup, ToggleItem, Tooltip, ValueBar, VirtualizedList, WidgetBody, cvaButton, cvaButtonPrefixSuffix, cvaButtonSpinner, cvaButtonSpinnerContainer, cvaClickable, cvaIconButton, cvaIndicator, cvaIndicatorIcon, cvaIndicatorIconBackground, cvaIndicatorLabel, cvaIndicatorPing, cvaInteractableItem, cvaMenuItem, cvaMenuItemLabel, cvaMenuItemPrefix, cvaMenuItemStyle, cvaMenuItemSuffix, docs, getDevicePixelRatio, getValueBarColorByValue, iconColorNames, iconPalette, useClickOutside, useContainerBreakpoints, useContinuousTimeout, useDebounce, useDevicePixelRatio, useElevatedState, useGeometry, useHover, useIsFirstRender, useIsFullscreen, useIsTextCutOff, useOverflowItems, usePopoverContext, usePrompt, useResize, useScrollDetection, useSelfUpdatingRef, useTimeout, useViewportBreakpoints, useWindowActivity };
3807
+ export { ActionRenderer, Alert, Badge, Breadcrumb, BreadcrumbContainer, Button, Card, CardBody, CardFooter, CardHeader, Collapse, CompletionStatusIndicator, CopyableText, EmptyState, EmptyValue, ExternalLink, Heading, Icon, IconButton, Indicator, KPICard, MenuDivider, MenuItem, MenuList, MoreMenu, Notice, PackageNameStoryComponent, Page, PageContent, PageHeader, PageHeaderKpiMetrics, PageHeaderSecondaryActions, PageHeaderTitle, Pagination, Polygon, Popover, PopoverContent, PopoverTitle, PopoverTrigger, Portal, Prompt, ROLE_CARD, SectionHeader, Sidebar, SkeletonLines, Spacer, Spinner, StarButton, Tab, TabContent, TabList, Tabs, Tag, Text, ToggleGroup, ToggleItem, Tooltip, ValueBar, VirtualizedList, WidgetBody, cvaButton, cvaButtonPrefixSuffix, cvaButtonSpinner, cvaButtonSpinnerContainer, cvaClickable, cvaIconButton, cvaIndicator, cvaIndicatorIcon, cvaIndicatorIconBackground, cvaIndicatorLabel, cvaIndicatorPing, cvaInteractableItem, cvaMenuItem, cvaMenuItemLabel, cvaMenuItemPrefix, cvaMenuItemStyle, cvaMenuItemSuffix, cvaPageHeader, cvaPageHeaderContainer, cvaPageHeaderHeading, docs, getDevicePixelRatio, getValueBarColorByValue, iconColorNames, iconPalette, useClickOutside, useContainerBreakpoints, useContinuousTimeout, useDebounce, useDevicePixelRatio, useElevatedState, useGeometry, useHover, useIsFirstRender, useIsFullscreen, useIsTextTruncated, useOverflowItems, usePopoverContext, usePrompt, useResize, useScrollDetection, useSelfUpdatingRef, useTimeout, useViewportBreakpoints, useWindowActivity };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@trackunit/react-components",
3
- "version": "1.1.2",
3
+ "version": "1.1.7",
4
4
  "repository": "https://github.com/Trackunit/manager",
5
5
  "license": "SEE LICENSE IN LICENSE.txt",
6
6
  "engines": {
@@ -19,11 +19,11 @@
19
19
  "@tanstack/react-router": "1.47.1",
20
20
  "string-ts": "^2.0.0",
21
21
  "tailwind-merge": "^2.0.0",
22
- "@trackunit/ui-design-tokens": "^1.0.1",
23
- "@trackunit/css-class-variance-utilities": "^1.0.1",
24
- "@trackunit/shared-utils": "^1.0.3",
25
- "@trackunit/ui-icons": "^1.0.4",
26
- "@trackunit/react-table-pagination": "^1.0.1"
22
+ "@trackunit/ui-design-tokens": "1.0.5",
23
+ "@trackunit/css-class-variance-utilities": "1.0.4",
24
+ "@trackunit/shared-utils": "1.2.1",
25
+ "@trackunit/ui-icons": "1.0.7",
26
+ "@trackunit/react-table-pagination": "1.0.5"
27
27
  },
28
28
  "module": "./index.esm.js",
29
29
  "main": "./index.cjs.js",
@@ -1,69 +1,13 @@
1
- import { IconName } from "@trackunit/ui-icons";
2
- import { ReactElement, ReactNode } from "react";
3
- import { CommonProps } from "../../common/CommonProps";
4
- import { TabListProps } from "../Tabs/TabList";
5
- import { TagColors } from "../Tag";
6
- type ActionPosition = "right";
7
- export interface PageHeaderProps extends CommonProps {
8
- /**
9
- * Text to be displayed in the header.
10
- */
11
- title: string | ReactNode;
12
- /**
13
- * The description to be displayed inside the tip.
14
- */
15
- description?: string | ReactNode;
16
- /**
17
- * The link to which user is redirected to if user clicks on the tip.
18
- */
19
- link?: string;
20
- /**
21
- * Display text of the specified link
22
- */
23
- linkText?: string;
24
- /**
25
- * Additional actions to be rendered along with the header.
26
- */
27
- action?: ReactNode;
28
- /**
29
- * Enables the actions to be pushed to the right in the available space - the default is left side.
30
- */
31
- actionPosition?: ActionPosition;
32
- /**
33
- * A tag that is displayed on the side of the header.
34
- * A RectNode passed into the tag will be rendered as such.
35
- * A string passed into the tag will render it using the tag component.
36
- */
37
- tag?: string | ReactNode;
38
- /**
39
- * A theme color value can be used for tag component
40
- */
41
- tagColor?: TagColors;
42
- /**
43
- * Whether spinner should be shown while loading the data.
44
- */
45
- showLoading?: boolean;
46
- /**
47
- * Child nodes to be displayed inside the page header.
48
- */
49
- children?: ReactNode;
50
- /**
51
- * The link to the previous page which will be rendered inside an IconButton component to the left of the heading.
52
- */
53
- backTo?: string;
54
- /**
55
- * The icon to be used in the description.
56
- */
57
- descriptionIcon?: IconName;
58
- tabsList?: ReactElement<TabListProps>;
59
- }
1
+ import { ReactElement } from "react";
2
+ import { PageHeaderProps } from "./types";
60
3
  /**
61
- * Pageheader component is used to display a page header.
4
+ * The PageHeader component is used to display the header of a page.
62
5
  *
63
- * PageHeader can be used to highlight the page topic, display important information about the page, and carry the action items related to the current page.
6
+ * It provides critical context by indicating the current location within the application or website.
7
+ * PageHeader can be used to highlight the page topic, display helpful information about the page, and carry the action items related to the current page.
8
+ * Tabs can be added to the PageHeader to allow users to navigate between different sections of the page.
64
9
  *
65
10
  * @param {PageHeaderProps} props - The props for the PageHeader component
66
11
  * @returns {JSX.Element} PageHeader component
67
12
  */
68
- export declare const PageHeader: ({ className, dataTestId, action, actionPosition, showLoading, description, link, title, tag, backTo, tagColor, children, tabsList, descriptionIcon, linkText, }: PageHeaderProps) => ReactElement;
69
- export {};
13
+ export declare const PageHeader: ({ className, dataTestId, secondaryActions, showLoading, description, title, tagLabel, backTo, tagColor, tabsList, descriptionIcon, kpiMetrics, tagTooltipLabel, primaryAction, }: PageHeaderProps) => ReactElement;
@@ -3,10 +3,3 @@ export declare const cvaPageHeaderContainer: (props?: ({
3
3
  } & import("class-variance-authority/dist/types").ClassProp) | undefined) => string;
4
4
  export declare const cvaPageHeader: (props?: import("class-variance-authority/dist/types").ClassProp | undefined) => string;
5
5
  export declare const cvaPageHeaderHeading: (props?: import("class-variance-authority/dist/types").ClassProp | undefined) => string;
6
- export declare const cvaPageHeaderHeadingContainer: (props?: import("class-variance-authority/dist/types").ClassProp | undefined) => string;
7
- export declare const cvaPageHeaderHeadingAccessoriesContainer: (props?: ({
8
- actionPosition?: "right" | null | undefined;
9
- } & import("class-variance-authority/dist/types").ClassProp) | undefined) => string;
10
- export declare const cvaPageHeaderChildContainer: (props?: import("class-variance-authority/dist/types").ClassProp | undefined) => string;
11
- export declare const cvaPageHeaderDescriptionContainer: (props?: import("class-variance-authority/dist/types").ClassProp | undefined) => string;
12
- export declare const cvaPageHeaderLink: (props?: import("class-variance-authority/dist/types").ClassProp | undefined) => string;
@@ -0,0 +1,4 @@
1
+ import { PageHeaderKpiMetricsType } from "../types";
2
+ export declare const PageHeaderKpiMetrics: ({ kpiMetrics }: {
3
+ kpiMetrics: PageHeaderKpiMetricsType[];
4
+ }) => import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,20 @@
1
+ import { PageHeaderSecondaryActionType } from "../types";
2
+ type ActionRendererProps = {
3
+ action: PageHeaderSecondaryActionType;
4
+ /**
5
+ * Indicates if we should render a MenuItem or Button.
6
+ * If `isMenuItem` is true, we render `MenuItem`, otherwise we render `Button`.
7
+ */
8
+ isMenuItem?: boolean;
9
+ /**
10
+ * Because sometimes in a menu, you want to close the menu after clicking.
11
+ * If `externalOnClick` is provided, we’ll call it after the action callback.
12
+ */
13
+ externalOnClick?: () => void;
14
+ };
15
+ export declare function ActionRenderer({ action, isMenuItem, externalOnClick }: ActionRendererProps): import("react/jsx-runtime").JSX.Element;
16
+ export declare const PageHeaderSecondaryActions: ({ actions, hasPrimaryAction, }: {
17
+ actions: PageHeaderSecondaryActionType[];
18
+ hasPrimaryAction?: boolean;
19
+ }) => import("react/jsx-runtime").JSX.Element;
20
+ export {};
@@ -0,0 +1,4 @@
1
+ export declare const PageHeaderTitle: ({ title, dataTestId }: {
2
+ title: string;
3
+ dataTestId?: string;
4
+ }) => import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,3 @@
1
+ export * from "./PageHeaderKpiMetrics";
2
+ export * from "./PageHeaderSecondaryActions";
3
+ export * from "./PageHeaderTitle";
@@ -1 +1,4 @@
1
+ export * from "./components";
1
2
  export * from "./PageHeader";
3
+ export * from "./PageHeader.variants";
4
+ export * from "./types";
@@ -0,0 +1,83 @@
1
+ import { IconName } from "@trackunit/ui-icons";
2
+ import { ReactElement, ReactNode } from "react";
3
+ import { CommonProps } from "../../common";
4
+ import { ButtonVariant } from "../buttons";
5
+ import { TabListProps } from "../Tabs/TabList";
6
+ import { TagColors } from "../Tag";
7
+ export type PageHeaderKpiMetricsType = {
8
+ header: string;
9
+ value: string | number;
10
+ unit?: string;
11
+ loading?: boolean;
12
+ hidden?: boolean;
13
+ };
14
+ export type PageHeaderActionType = CommonProps & {
15
+ actionCallback?: (e?: React.MouseEvent) => void;
16
+ actionText: string;
17
+ prefixIconName?: IconName;
18
+ disabled?: boolean;
19
+ hidden?: boolean;
20
+ loading?: boolean;
21
+ tooltipLabel?: string;
22
+ };
23
+ export type PageHeaderSecondaryActionType = PageHeaderActionType & {
24
+ variant: Extract<ButtonVariant, "secondary" | "secondary-danger">;
25
+ to?: string;
26
+ target?: HTMLAnchorElement["target"];
27
+ };
28
+ export type PageHeaderPrimaryActionType = PageHeaderActionType & {
29
+ variant: Extract<ButtonVariant, "primary" | "primary-danger">;
30
+ };
31
+ interface BasePageHeaderProps extends CommonProps {
32
+ /**
33
+ * Text to be displayed in the header.
34
+ */
35
+ title: string | ReactNode;
36
+ /**
37
+ * The description to be displayed inside the tip.
38
+ */
39
+ description?: string | ReactNode;
40
+ /**
41
+ * The action to be displayed on the right side of the header.
42
+ */
43
+ primaryAction?: PageHeaderPrimaryActionType;
44
+ /**
45
+ * A tag that is displayed on the side of the header.
46
+ */
47
+ tagLabel?: string;
48
+ /**
49
+ * A theme color value can be used for tag component
50
+ */
51
+ tagColor?: TagColors;
52
+ /**
53
+ * The tooltip to be displayed in the tooltip for the tag.
54
+ */
55
+ tagTooltipLabel?: string;
56
+ /**
57
+ * Whether spinner should be shown while loading the data.
58
+ */
59
+ showLoading?: boolean;
60
+ /**
61
+ * The link to the previous page which will be rendered inside
62
+ * an IconButton component to the left of the heading.
63
+ */
64
+ backTo?: string;
65
+ /**
66
+ * The icon to be used in the description.
67
+ */
68
+ descriptionIcon?: IconName;
69
+ /**
70
+ * The list of tabs to be displayed below the header.
71
+ */
72
+ tabsList?: ReactElement<TabListProps>;
73
+ }
74
+ interface WithActions {
75
+ secondaryActions?: PageHeaderSecondaryActionType[] | ReactNode;
76
+ kpiMetrics?: never;
77
+ }
78
+ interface WithKpiMetrics {
79
+ secondaryActions?: never;
80
+ kpiMetrics?: PageHeaderKpiMetricsType[];
81
+ }
82
+ export type PageHeaderProps = BasePageHeaderProps & (WithActions | WithKpiMetrics);
83
+ export {};
@@ -8,7 +8,7 @@ export * from "./useGeometry";
8
8
  export * from "./useHover";
9
9
  export * from "./useIsFirstRender";
10
10
  export * from "./useIsFullScreen";
11
- export * from "./useIsTextCutOff";
11
+ export * from "./useIsTextTruncated";
12
12
  export * from "./useResize";
13
13
  export * from "./useScrollDetection";
14
14
  export * from "./useSelfUpdatingRef";
@@ -0,0 +1,19 @@
1
+ import { RefObject } from "react";
2
+ /**
3
+ * A custom hook that determines if text within an element is truncated
4
+ * (i.e., the text overflows the container).
5
+ *
6
+ * @template TElement - The type of the HTML element being observed (e.g., HTMLDivElement).
7
+ * @param {string} [text] - (Optional) Text used to trigger a re-check of truncation,
8
+ * especially if the text is dynamic (such as an input's value).
9
+ * @returns {{
10
+ * ref: RefObject<TElement | null>;
11
+ * isTooltipVisible: boolean;
12
+ * }} An object with:
13
+ * - `ref`: a ref to attach to the element you want to observe for truncation.
14
+ * - `isTextTruncated`: a boolean indicating if the text is truncated.
15
+ */
16
+ export declare const useIsTextTruncated: <TElement extends HTMLElement>(text?: string) => {
17
+ ref: RefObject<TElement>;
18
+ isTextTruncated: boolean;
19
+ };
@@ -2,7 +2,15 @@ import { RefObject } from "react";
2
2
  /**
3
3
  * Hook to detect if text content is wrapping to multiple lines
4
4
  *
5
- * @param {RefObject<HTMLElement>} ref - The ref to the element to check for text wrapping
6
- * @returns {boolean} True if the text spans multiple lines
5
+ * @template TElement - The type of the HTML element being observed (e.g., HTMLDivElement).
6
+ * @returns {{
7
+ * ref: RefObject<TElement | null>;
8
+ * isTooltipVisible: boolean;
9
+ * }} An object with:
10
+ * - `ref`: a ref to attach to the element you want to observe for truncation.
11
+ * - `isTextWrapping`: a boolean indicating if the text is wrapping.
7
12
  */
8
- export declare const useIsTextWrapping: (ref: RefObject<HTMLElement>) => boolean;
13
+ export declare const useIsTextWrapping: <TElement extends HTMLElement>() => {
14
+ ref: RefObject<TElement>;
15
+ isTextWrapping: boolean;
16
+ };
@@ -1,8 +0,0 @@
1
- import { RefObject } from "react";
2
- /**
3
- * Check if text is cut off.
4
- *
5
- * @param {RefObject<HTMLElement>} ref The ref to the element to check.
6
- * @returns {boolean} True if the text is cut off.
7
- */
8
- export declare const useIsTextCutOff: (ref: RefObject<HTMLElement>) => boolean;