@trackunit/react-components 1.1.2 → 1.1.5
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 +164 -67
- package/index.esm.js +157 -67
- package/package.json +6 -6
- package/src/components/PageHeader/PageHeader.d.ts +7 -63
- package/src/components/PageHeader/PageHeader.variants.d.ts +0 -7
- package/src/components/PageHeader/components/PageHeaderKpiMetrics.d.ts +4 -0
- package/src/components/PageHeader/components/PageHeaderSecondaryActions.d.ts +20 -0
- package/src/components/PageHeader/components/PageHeaderTitle.d.ts +4 -0
- package/src/components/PageHeader/components/index.d.ts +3 -0
- package/src/components/PageHeader/index.d.ts +3 -0
- package/src/components/PageHeader/types.d.ts +83 -0
- package/src/hooks/index.d.ts +1 -1
- package/src/hooks/useIsTextTruncated.d.ts +19 -0
- package/src/hooks/useIsTextWrapping.d.ts +11 -3
- package/src/hooks/useIsTextCutOff.d.ts +0 -8
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
|
-
* @
|
|
275
|
-
* @returns {
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
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
|
-
|
|
285
|
-
|
|
286
|
-
|
|
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 =
|
|
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: !
|
|
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
|
-
*
|
|
1250
|
+
* A custom hook that determines if text within an element is truncated
|
|
1251
|
+
* (i.e., the text overflows the container).
|
|
1230
1252
|
*
|
|
1231
|
-
* @
|
|
1232
|
-
* @
|
|
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
|
|
1235
|
-
const
|
|
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
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
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)
|
|
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-
|
|
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
|
-
|
|
3040
|
-
|
|
3041
|
-
|
|
3042
|
-
|
|
3043
|
-
"flex-
|
|
3044
|
-
|
|
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
|
-
*
|
|
3153
|
+
* The PageHeader component is used to display the header of a page.
|
|
3072
3154
|
*
|
|
3073
|
-
*
|
|
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,
|
|
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:
|
|
3083
|
-
|
|
3084
|
-
|
|
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-
|
|
3375
|
-
"py-2
|
|
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.
|
|
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,
|
|
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
|
-
* @
|
|
273
|
-
* @returns {
|
|
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 = (
|
|
276
|
-
const
|
|
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
|
-
|
|
283
|
-
|
|
284
|
-
|
|
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 =
|
|
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: !
|
|
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
|
-
*
|
|
1248
|
+
* A custom hook that determines if text within an element is truncated
|
|
1249
|
+
* (i.e., the text overflows the container).
|
|
1228
1250
|
*
|
|
1229
|
-
* @
|
|
1230
|
-
* @
|
|
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
|
|
1233
|
-
const
|
|
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
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
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)
|
|
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-
|
|
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
|
-
|
|
3038
|
-
|
|
3039
|
-
|
|
3040
|
-
|
|
3041
|
-
"flex-
|
|
3042
|
-
|
|
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
|
-
*
|
|
3151
|
+
* The PageHeader component is used to display the header of a page.
|
|
3070
3152
|
*
|
|
3071
|
-
*
|
|
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,
|
|
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:
|
|
3081
|
-
|
|
3082
|
-
|
|
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-
|
|
3373
|
-
"py-2
|
|
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,
|
|
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.
|
|
3
|
+
"version": "1.1.5",
|
|
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.
|
|
23
|
-
"@trackunit/css-class-variance-utilities": "^1.0.
|
|
24
|
-
"@trackunit/shared-utils": "^1.
|
|
25
|
-
"@trackunit/ui-icons": "^1.0.
|
|
26
|
-
"@trackunit/react-table-pagination": "^1.0.
|
|
22
|
+
"@trackunit/ui-design-tokens": "^1.0.3",
|
|
23
|
+
"@trackunit/css-class-variance-utilities": "^1.0.2",
|
|
24
|
+
"@trackunit/shared-utils": "^1.1.1",
|
|
25
|
+
"@trackunit/ui-icons": "^1.0.5",
|
|
26
|
+
"@trackunit/react-table-pagination": "^1.0.3"
|
|
27
27
|
},
|
|
28
28
|
"module": "./index.esm.js",
|
|
29
29
|
"main": "./index.cjs.js",
|
|
@@ -1,69 +1,13 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
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
|
-
*
|
|
4
|
+
* The PageHeader component is used to display the header of a page.
|
|
62
5
|
*
|
|
63
|
-
*
|
|
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,
|
|
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,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,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 {};
|
package/src/hooks/index.d.ts
CHANGED
|
@@ -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 "./
|
|
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
|
-
* @
|
|
6
|
-
* @returns {
|
|
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:
|
|
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;
|