@trackunit/react-components 1.9.8 → 1.9.9

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.esm.js CHANGED
@@ -1,5 +1,5 @@
1
- import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
2
- import { useRef, useMemo, useEffect, useState, useCallback, createElement, useReducer, forwardRef, Fragment as Fragment$1, memo, createContext, useContext, isValidElement, cloneElement, Children } from 'react';
1
+ import { jsx, jsxs, Fragment as Fragment$1 } from 'react/jsx-runtime';
2
+ import { useRef, useMemo, useEffect, useState, useCallback, createElement, useReducer, forwardRef, Fragment, memo, Children, isValidElement, cloneElement, createContext, useContext } from 'react';
3
3
  import { objectKeys, uuidv4, objectEntries, objectValues, nonNullable } from '@trackunit/shared-utils';
4
4
  import { intentPalette, generalPalette, criticalityPalette, activityPalette, utilizationPalette, sitesPalette, rentalStatusPalette, themeScreenSizeAsNumber, color } from '@trackunit/ui-design-tokens';
5
5
  import { iconNames } from '@trackunit/ui-icons';
@@ -15,7 +15,7 @@ import { Link, useBlocker } from '@tanstack/react-router';
15
15
  import { useFloating, autoUpdate, offset, flip, shift, size, useClick, useDismiss, useHover as useHover$1, useRole, useInteractions, FloatingPortal, useMergeRefs, FloatingFocusManager, arrow, useTransitionStatus, FloatingArrow } from '@floating-ui/react';
16
16
  import { omit } from 'es-toolkit';
17
17
  import { twMerge } from 'tailwind-merge';
18
- import { noPagination, useInfiniteScroll } from '@trackunit/react-table-pagination';
18
+ import { useInfiniteScroll, noPagination } from '@trackunit/react-table-pagination';
19
19
  import { HelmetProvider, Helmet } from 'react-helmet-async';
20
20
  import { Trigger, Content, List as List$1, Root } from '@radix-ui/react-tabs';
21
21
 
@@ -1311,7 +1311,7 @@ const useElevatedState = (initialState, customState) => {
1311
1311
  * Custom hook to get the geometry of an element.
1312
1312
  * Size and position of the element relative to the viewport.
1313
1313
  */
1314
- const useGeometry = (ref) => {
1314
+ const useGeometry = (ref, { skip = false } = {}) => {
1315
1315
  const [geometry, setGeometry] = useState({
1316
1316
  width: 0,
1317
1317
  height: 0,
@@ -1324,7 +1324,7 @@ const useGeometry = (ref) => {
1324
1324
  });
1325
1325
  const resizeObserver = useRef(null);
1326
1326
  useEffect(() => {
1327
- if (!ref.current) {
1327
+ if (skip || !ref.current) {
1328
1328
  return;
1329
1329
  }
1330
1330
  const observe = () => {
@@ -1355,7 +1355,7 @@ const useGeometry = (ref) => {
1355
1355
  resizeObserver.current.disconnect();
1356
1356
  }
1357
1357
  };
1358
- }, [ref]);
1358
+ }, [ref, skip]);
1359
1359
  return geometry;
1360
1360
  };
1361
1361
 
@@ -1484,17 +1484,21 @@ const useModifierKey = ({ exclude = [] } = {}) => {
1484
1484
  /**
1485
1485
  * Custom hook to handle window resize events and provide the current window size.
1486
1486
  *
1487
+ * @param {UseResizeOptions} options - Options for the hook.
1487
1488
  * @returns {WindowSize} An object containing the current window height and width.
1488
1489
  */
1489
- const useResize = () => {
1490
+ const useResize = ({ skip = false } = {}) => {
1490
1491
  const [size, setSize] = useState(getWindowSize());
1491
1492
  useEffect(() => {
1493
+ if (skip) {
1494
+ return;
1495
+ }
1492
1496
  const handleResize = () => {
1493
1497
  setSize(getWindowSize());
1494
1498
  };
1495
1499
  window.addEventListener("resize", handleResize, false);
1496
1500
  return () => window.removeEventListener("resize", handleResize);
1497
- }, [setSize]);
1501
+ }, [setSize, skip]);
1498
1502
  return size;
1499
1503
  };
1500
1504
  /**
@@ -1519,36 +1523,73 @@ const getWindowSize = () => {
1519
1523
 
1520
1524
  const SCROLL_DEBOUNCE_TIME = 50;
1521
1525
  /**
1522
- * Hook for getting detecting scroll values.
1526
+ * Hook for detecting scroll values in horizontal or vertical direction.
1523
1527
  *
1524
1528
  * @param {useRef} elementRef - Ref hook holding the element that needs to be observed during scrolling
1525
- * @returns {object} An object containing if the element is scrollable, is at the top, is at the bottom, and its current scroll position.
1529
+ * @param {object} options - Options object containing direction and onScrollStateChange callback
1530
+ * @returns {object} An object containing if the element is scrollable, is at the beginning, is at the end, and its current scroll position.
1526
1531
  */
1527
- const useScrollDetection = (elementRef) => {
1532
+ const useScrollDetection = (elementRef, options) => {
1533
+ const { direction = "vertical", onScrollStateChange } = options ?? {};
1528
1534
  const [isScrollable, setIsScrollable] = useState(false);
1529
- const [isAtTop, setIsAtTop] = useState(true);
1530
- const [isAtBottom, setIsAtBottom] = useState(false);
1531
- const [scrollPosition, setScrollPosition] = useState({ top: 0, bottom: 0 });
1535
+ const [isAtBeginning, setIsAtBeginning] = useState(true);
1536
+ const [isAtEnd, setIsAtEnd] = useState(false);
1537
+ const [scrollPosition, setScrollPosition] = useState({ start: 0, end: 0 });
1532
1538
  const observerRef = useRef(null);
1539
+ const isFirstRender = useIsFirstRender();
1533
1540
  const checkScrollable = useCallback(() => {
1534
1541
  const element = elementRef?.current;
1535
1542
  if (!element) {
1536
1543
  return;
1537
1544
  }
1538
- const hasOverflow = element.scrollHeight > element.clientHeight;
1539
- setIsScrollable(hasOverflow);
1540
- }, [elementRef]);
1545
+ const hasOverflow = direction === "horizontal"
1546
+ ? element.scrollWidth > element.clientWidth
1547
+ : element.scrollHeight > element.clientHeight;
1548
+ setIsScrollable(prev => {
1549
+ if (prev !== hasOverflow) {
1550
+ // State will be updated, so we'll notify in the next effect
1551
+ return hasOverflow;
1552
+ }
1553
+ return prev;
1554
+ });
1555
+ }, [elementRef, direction]);
1541
1556
  const checkScrollPosition = useCallback(() => {
1542
1557
  const element = elementRef?.current;
1543
1558
  if (!element) {
1544
1559
  return;
1545
1560
  }
1546
- const { scrollTop, scrollHeight, clientHeight } = element;
1547
- setIsAtTop(scrollTop === 0);
1548
- setIsAtBottom(Math.abs(scrollHeight - scrollTop - clientHeight) <= 1);
1549
- setScrollPosition(prev => ({ ...prev, top: scrollTop, bottom: clientHeight - scrollTop }));
1550
- }, [elementRef]);
1561
+ if (direction === "horizontal") {
1562
+ const { scrollLeft, scrollWidth, clientWidth } = element;
1563
+ const newIsAtBeginning = scrollLeft === 0;
1564
+ const newIsAtEnd = Math.abs(scrollWidth - scrollLeft - clientWidth) <= 1;
1565
+ const newScrollPosition = { start: scrollLeft, end: clientWidth - scrollLeft };
1566
+ setIsAtBeginning(newIsAtBeginning);
1567
+ setIsAtEnd(newIsAtEnd);
1568
+ setScrollPosition(newScrollPosition);
1569
+ }
1570
+ else {
1571
+ const { scrollTop, scrollHeight, clientHeight } = element;
1572
+ const newIsAtBeginning = scrollTop === 0;
1573
+ const newIsAtEnd = Math.abs(scrollHeight - scrollTop - clientHeight) <= 1;
1574
+ const newScrollPosition = { start: scrollTop, end: clientHeight - scrollTop };
1575
+ setIsAtBeginning(newIsAtBeginning);
1576
+ setIsAtEnd(newIsAtEnd);
1577
+ setScrollPosition(newScrollPosition);
1578
+ }
1579
+ }, [elementRef, direction]);
1551
1580
  const debouncedCheckScrollPosition = useDebounceCallback(checkScrollPosition, SCROLL_DEBOUNCE_TIME);
1581
+ // Notify about state changes whenever any state value changes
1582
+ useEffect(() => {
1583
+ if (isFirstRender) {
1584
+ return;
1585
+ }
1586
+ onScrollStateChange?.({
1587
+ isScrollable,
1588
+ isAtBeginning,
1589
+ isAtEnd,
1590
+ scrollPosition,
1591
+ });
1592
+ }, [isScrollable, isAtBeginning, isAtEnd, scrollPosition, onScrollStateChange, isFirstRender]);
1552
1593
  useEffect(() => {
1553
1594
  const element = elementRef?.current;
1554
1595
  if (!element) {
@@ -1570,7 +1611,7 @@ const useScrollDetection = (elementRef) => {
1570
1611
  element.removeEventListener("scroll", debouncedCheckScrollPosition);
1571
1612
  };
1572
1613
  }, [elementRef, checkScrollable, checkScrollPosition, debouncedCheckScrollPosition]);
1573
- return { isScrollable, isAtTop, isAtBottom, scrollPosition };
1614
+ return useMemo(() => ({ isScrollable, isAtBeginning, isAtEnd, scrollPosition }), [isScrollable, isAtBeginning, isAtEnd, scrollPosition]);
1574
1615
  };
1575
1616
 
1576
1617
  /**
@@ -1586,6 +1627,36 @@ const useSelfUpdatingRef = (initialState) => {
1586
1627
  return stateRef;
1587
1628
  };
1588
1629
 
1630
+ /**
1631
+ * The useStable hook ensures that a value is computed only once and remains stable across renders.
1632
+ * This is useful for expensive computations or when you need a value to never change after initial creation.
1633
+ * Perfect for generating stable random values, creating stable object references, or any one-time computation.
1634
+ *
1635
+ * @example
1636
+ * // Stable random ID that never changes
1637
+ * const stableId = useStable(() => Math.random().toString(36));
1638
+ * @example
1639
+ * // Stable configuration object
1640
+ * const config = useStable(() => ({
1641
+ * apiKey: generateApiKey(),
1642
+ * timestamp: Date.now()
1643
+ * }));
1644
+ * @example
1645
+ * // Stable random widths for skeleton loading
1646
+ * const lineWidths = useStable(() => ({
1647
+ * title: Math.floor(Math.random() * 100) + 120,
1648
+ * description: Math.floor(Math.random() * 80) + 80
1649
+ * }));
1650
+ */
1651
+ const useStable = (factory) => {
1652
+ const ref = useRef(null);
1653
+ // Compute value only once and store it
1654
+ if (ref.current === null) {
1655
+ ref.current = { value: factory() };
1656
+ }
1657
+ return ref.current.value;
1658
+ };
1659
+
1589
1660
  /**
1590
1661
  * A custom React hook that provides real-time information about the current viewport size.
1591
1662
  * ! Consider using `useContainerBreakpoints` instead, and only use this when you need to actually react to the viewport size, not the container size.
@@ -1755,11 +1826,9 @@ const useBreadcrumbItemsToRender = (breadcrumbItems) => {
1755
1826
  const breadCrumbItemsToJSX = breadcrumbItems.map((item, index, array) => {
1756
1827
  const isLast = index === array.length - 1;
1757
1828
  if (!isLast) {
1758
- return (jsxs(Fragment, { children: [jsx(Button, { asChild: true, size: "small", variant: "ghost-neutral", children: jsx(Link, { to: item.to, children: item.label }) }), jsx(Icon, { className: "text-secondary-300", name: "Slash", size: "small" })] }));
1759
- }
1760
- else {
1761
- return (jsx(Text, { className: "text-nowrap", size: "small", children: item.label }));
1829
+ return (jsxs("div", { children: [jsx(Button, { asChild: true, size: "small", variant: "ghost-neutral", children: jsx(Link, { to: item.to, children: item.label }) }), jsx(Icon, { className: "text-secondary-300", name: "Slash", size: "small" })] }, index));
1762
1830
  }
1831
+ return (jsx(Text, { className: "text-nowrap", size: "small", children: item.label }, index));
1763
1832
  });
1764
1833
  return breadCrumbItemsToJSX;
1765
1834
  };
@@ -2180,45 +2249,66 @@ const cvaDetailsListItem = cvaMerge(["last:truncate"]);
2180
2249
  * @returns {ReactElement} The details list element.
2181
2250
  */
2182
2251
  const DetailsList = ({ details, className, density = "default" }) => {
2183
- return (jsx("div", { className: cvaDetailsList({ className, density }), children: details.map((value, index, array) => (jsxs(Fragment$1, { children: [jsx("span", { className: cvaDetailsListItem({ className }), children: value }), index < array.length - 1 && (jsx("div", { className: "mx-0.5 flex items-center", children: jsx(Icon, { className: "w-4 text-neutral-300", color: "neutral", name: "Slash", size: "small" }) }))] }, index))) }));
2252
+ return (jsx("div", { className: cvaDetailsList({ className, density }), children: details.map((value, index, array) => (jsxs(Fragment, { children: [jsx("span", { className: cvaDetailsListItem({ className }), children: value }), index < array.length - 1 && (jsx("div", { className: "mx-0.5 flex items-center", children: jsx(Icon, { className: "w-4 text-neutral-300", color: "neutral", name: "Slash", size: "small" }) }))] }, index))) }));
2184
2253
  };
2185
2254
 
2255
+ /**
2256
+ * Generates a random width percentage string for skeleton loading components.
2257
+ *
2258
+ * @param {object} params - The parameter object
2259
+ * @param {number} params.min - Minimum percentage value (e.g., 30 for 30%)
2260
+ * @param {number} params.max - Maximum percentage value (e.g., 80 for 80%)
2261
+ * @returns {string} A percentage string (e.g., "65%")
2262
+ */
2263
+ const getResponsiveRandomWidthPercentage = ({ min, max }) => {
2264
+ const randomWidth = Math.floor(Math.random() * (max - min + 1)) + min;
2265
+ return `${randomWidth}%`;
2266
+ };
2267
+
2268
+ const cvaSkeletonContainer = cvaMerge(["flex", "flex-col"]);
2186
2269
  const cvaSkeletonLine = cvaMerge([
2187
- "rounded-md",
2188
- "h-3",
2189
- "opacity-20",
2190
- "animate-pulse",
2191
- "bg-black/50",
2192
- "bg-auto",
2270
+ "relative",
2271
+ "overflow-hidden",
2272
+ "rounded-lg",
2273
+ // Gradient background
2193
2274
  "bg-gradient-to-r",
2194
- "from-black/0",
2195
- "via-black/50",
2196
- "to-black/20",
2275
+ "from-gray-200/80",
2276
+ "via-gray-300/60",
2277
+ "to-gray-200/80",
2278
+ // Pulse animation
2279
+ "animate-pulse",
2280
+ // Shimmer overlay
2281
+ "before:absolute",
2282
+ "before:inset-0",
2283
+ "before:bg-gradient-to-r",
2284
+ "before:from-transparent",
2285
+ "before:via-white/50",
2286
+ "before:to-transparent",
2287
+ "before:opacity-0",
2288
+ "before:animate-pulse",
2289
+ // Smooth transitions for accessibility
2290
+ "transition-all",
2291
+ "duration-300",
2292
+ "ease-in-out",
2197
2293
  ]);
2198
2294
 
2199
2295
  /**
2200
2296
  * Display placeholder lines before the data gets loaded to reduce load-time frustration.
2201
2297
  */
2202
- const SkeletonLines = memo(({ margin = "10px 0", lines = 1, height, width = "100%", className, dataTestId, }) => {
2203
- const skeletonLines = [];
2204
- const getWidth = (index) => {
2205
- if (Array.isArray(width)) {
2206
- return width[index] ?? "100%";
2207
- }
2208
- return width;
2209
- };
2210
- const getHeight = (index) => {
2211
- if (Array.isArray(height)) {
2212
- return height[index] ?? "auto";
2213
- }
2214
- return height;
2215
- };
2216
- for (let i = 0; i < lines; i++) {
2217
- skeletonLines.push(jsx("div", { className: cvaSkeletonLine({ className }), "data-testid": dataTestId ? `${dataTestId}-${i}` : `skeleton-lines-${i}`, "data-type": "loading-skeleton-line", style: { height: getHeight(i), width: getWidth(i), margin: lines > 1 && i >= 1 ? margin : "" }, children: lines > 1 && jsx(Fragment, { children: "\u00A0" }) }, i));
2218
- }
2219
- return jsx(Fragment, { children: skeletonLines });
2298
+ const SkeletonLines = memo(({ lines = 1, height = "0.75rem", width = "100%", margin = 10, className, dataTestId }) => {
2299
+ const gapStyle = typeof margin === "number" ? `${margin}px` : margin;
2300
+ return (jsx("div", { "aria-label": `Loading ${lines} ${lines === 1 ? "item" : "items"}`, className: cvaSkeletonContainer({ className }), "data-testid": dataTestId, role: "status", style: { gap: gapStyle }, children: Array.from({ length: lines }, (_, index) => (jsx("div", { className: cvaSkeletonLine(), "data-testid": dataTestId ? `${dataTestId}-${index}` : `skeleton-lines-${index}`, "data-type": "loading-skeleton-line", style: {
2301
+ width: getDimension(width, index),
2302
+ height: getDimension(height, index),
2303
+ } }, index))) }));
2220
2304
  });
2221
- SkeletonLines.displayName = "SkeletonLines";
2305
+ const getDimension = (dimension, index) => {
2306
+ if (Array.isArray(dimension)) {
2307
+ const value = dimension[index] ?? dimension[0] ?? "100%";
2308
+ return typeof value === "number" ? `${value}px` : value;
2309
+ }
2310
+ return typeof dimension === "number" ? `${dimension}px` : dimension;
2311
+ };
2222
2312
 
2223
2313
  const cvaContainerStyles = cvaMerge([
2224
2314
  "flex",
@@ -2311,7 +2401,7 @@ const EmptyState = ({ description, altText, image = "SEARCH_DOCUMENT", customIma
2311
2401
  return SearchDocumentSVG;
2312
2402
  }
2313
2403
  }, [image]);
2314
- return (jsx("div", { className: cvaContainerStyles({ className }), "data-testid": dataTestId ?? "empty-state", children: loading ? (jsxs(Fragment, { children: [jsx(Spinner, { centering: "centered", dataTestId: "spinner" }), jsx(SkeletonLines, { dataTestId: "skeleton-lines", width: 50 })] })) : (jsxs(Fragment, { children: [customImageSrc !== null && customImageSrc !== undefined ? (typeof customImageSrc === "string" ? (jsx("img", { alt: altText, className: cvaImgStyles(), height: 200, src: customImageSrc, width: 200 })) : (customImageSrc)) : (typeof ImageSource !== "undefined" && (jsx(ImageSource, { "data-testid": "empty-state-image", height: 200, width: 200 }, image))), description !== undefined && description !== "" ? (jsx(Text, { align: "center", size: "large", children: description })) : null, jsxs("div", { className: "mt-4 grid gap-3", children: [jsxs("div", { className: "flex gap-3", children: [secondaryAction ? (jsx(Button, { dataTestId: "empty-state-secondary-button", disabled: secondaryAction.disabled, onClick: secondaryAction.onClick, variant: "secondary", children: secondaryAction.to ? (jsx(Link, { params: secondaryAction.to.parameters, to: secondaryAction.to.pathname, children: secondaryAction.title })) : (secondaryAction.title) })) : null, primaryAction ? (jsx(Button, { dataTestId: "empty-state-primary-button", disabled: primaryAction.disabled, onClick: primaryAction.onClick, children: primaryAction.to ? (jsx(Link, { params: primaryAction.to.parameters, to: primaryAction.to.pathname, children: primaryAction.title })) : (primaryAction.title) })) : null] }), additionalHelpAction?.to ? (jsx(Button, { asChild: true, dataTestId: "empty-state-additional-button", disabled: additionalHelpAction.disabled, onClick: additionalHelpAction.onClick, suffix: jsx(Icon, { name: "ArrowTopRightOnSquare", size: "small" }), variant: "ghost", children: jsx(Link, { params: additionalHelpAction.to.parameters, target: additionalHelpAction.to.target, to: additionalHelpAction.to.pathname, children: additionalHelpAction.title }) })) : null] })] })) }));
2404
+ return (jsx("div", { className: cvaContainerStyles({ className }), "data-testid": dataTestId ?? "empty-state", children: loading ? (jsxs(Fragment$1, { children: [jsx(Spinner, { centering: "centered", dataTestId: "spinner" }), jsx(SkeletonLines, { dataTestId: "skeleton-lines", width: 50 })] })) : (jsxs(Fragment$1, { children: [customImageSrc !== null && customImageSrc !== undefined ? (typeof customImageSrc === "string" ? (jsx("img", { alt: altText, className: cvaImgStyles(), height: 200, src: customImageSrc, width: 200 })) : (customImageSrc)) : (typeof ImageSource !== "undefined" && (jsx(ImageSource, { "data-testid": "empty-state-image", height: 200, width: 200 }, image))), description !== undefined && description !== "" ? (jsx(Text, { align: "center", size: "large", children: description })) : null, jsxs("div", { className: "mt-4 grid gap-3", children: [jsxs("div", { className: "flex gap-3", children: [secondaryAction ? (jsx(Button, { dataTestId: "empty-state-secondary-button", disabled: secondaryAction.disabled, onClick: secondaryAction.onClick, variant: "secondary", children: secondaryAction.to ? (jsx(Link, { params: secondaryAction.to.parameters, to: secondaryAction.to.pathname, children: secondaryAction.title })) : (secondaryAction.title) })) : null, primaryAction ? (jsx(Button, { dataTestId: "empty-state-primary-button", disabled: primaryAction.disabled, onClick: primaryAction.onClick, children: primaryAction.to ? (jsx(Link, { params: primaryAction.to.parameters, to: primaryAction.to.pathname, children: primaryAction.title })) : (primaryAction.title) })) : null] }), additionalHelpAction?.to ? (jsx(Button, { asChild: true, dataTestId: "empty-state-additional-button", disabled: additionalHelpAction.disabled, onClick: additionalHelpAction.onClick, suffix: jsx(Icon, { name: "ArrowTopRightOnSquare", size: "small" }), variant: "ghost", children: jsx(Link, { params: additionalHelpAction.to.parameters, target: additionalHelpAction.to.target, to: additionalHelpAction.to.pathname, children: additionalHelpAction.title }) })) : null] })] })) }));
2315
2405
  };
2316
2406
 
2317
2407
  const cvaEmptyValue = cvaMerge(["text-neutral-400"]);
@@ -2390,6 +2480,119 @@ const Highlight = ({ className, dataTestId, children, size = "small", color = "w
2390
2480
  };
2391
2481
  Highlight.displayName = "Highlight";
2392
2482
 
2483
+ const cvaZStackContainer = cvaMerge(["grid", "grid-cols-1", "grid-rows-1"]);
2484
+ const cvaZStackItem = cvaMerge(["col-start-1", "col-end-1", "row-start-1", "row-end-2"]);
2485
+
2486
+ /**
2487
+ * ZStack is a component that stacks its children on the z-axis.
2488
+ * Is a good alternative to "position: absolute" that avoids some of the unfortunate side effects of absolute positioning.
2489
+ *
2490
+ * @param { ZStackProps} props - The props for the ZStack component
2491
+ * @returns {Element} ZStack component
2492
+ */
2493
+ const ZStack = ({ children, className, dataTestId }) => {
2494
+ return (jsx("div", { className: cvaZStackContainer({ className }), "data-testid": dataTestId, children: Children.map(children, (child, index) => {
2495
+ if (!isValidElement(child)) {
2496
+ return child;
2497
+ }
2498
+ return cloneElement(child, {
2499
+ className: cvaZStackItem({ className: child.props.className }),
2500
+ key: index,
2501
+ });
2502
+ }) }));
2503
+ };
2504
+
2505
+ const cvaHorizontalOverflowScroller = cvaMerge([
2506
+ "flex",
2507
+ "flex-nowrap",
2508
+ "gap-1",
2509
+ "overflow-y-hidden",
2510
+ "overflow-x-scroll",
2511
+ "w-full",
2512
+ "no-scrollbar",
2513
+ ]);
2514
+ const cvaHorizontalOverflowScrollerAndIndicatorsContainer = cvaMerge(["group", "w-full", "overflow-clip"]);
2515
+
2516
+ const cvaOverflowIndicatorContainer = cvaMerge(["pointer-events-none", "h-full", "w-full", "isolate"]);
2517
+ const cvaOverflowIndicatorGradient = cvaMerge(["pointer-events-none", "h-full", "w-8"], {
2518
+ variants: {
2519
+ direction: {
2520
+ left: ["bg-gradient-to-r", "from-white", "to-transparent"],
2521
+ right: ["bg-gradient-to-l", "from-white", "to-transparent"],
2522
+ },
2523
+ },
2524
+ });
2525
+ const cvaJustificationContainer = cvaMerge(["flex", "w-full", "items-center", "pointer-events-none"], {
2526
+ variants: {
2527
+ direction: {
2528
+ left: ["justify-start"],
2529
+ right: ["justify-end"],
2530
+ },
2531
+ },
2532
+ });
2533
+ const cvaOverflowIndicatorButton = cvaMerge([
2534
+ "shadow-md",
2535
+ "opacity-0",
2536
+ "transition-opacity",
2537
+ "duration-200",
2538
+ "pointer-events-auto",
2539
+ "starting:opacity-0",
2540
+ "group-hover:opacity-100",
2541
+ ]);
2542
+
2543
+ /**
2544
+ * Overflow indicator component that shows visual cues when content extends beyond visible area
2545
+ * Shows a scroll button on hover to navigate in the specified direction
2546
+ *
2547
+ * @param {OverflowIndicatorProps} props - The props for the component
2548
+ * @returns {ReactElement} OverflowIndicator component
2549
+ */
2550
+ const OverflowIndicator = ({ className, dataTestId, direction, onClickScroll, }) => {
2551
+ const iconName = direction === "left" ? "ChevronLeft" : "ChevronRight";
2552
+ return (jsxs(ZStack, { className: cvaOverflowIndicatorContainer({ className }), dataTestId: dataTestId, children: [jsx("div", { className: cvaJustificationContainer({ direction }), children: jsx("div", { className: cvaOverflowIndicatorGradient({ direction }), "data-testid": dataTestId ? `${dataTestId}-gradient` : undefined }) }), jsx("div", { className: cvaJustificationContainer({ direction }), children: jsx(IconButton, { circular: true, className: cvaOverflowIndicatorButton(), "data-testid": dataTestId ? `${dataTestId}-button` : undefined, icon: jsx(Icon, { name: iconName, size: "small" }), onClick: onClickScroll, size: "small", variant: "secondary" }) })] }));
2553
+ };
2554
+
2555
+ /**
2556
+ * Container for displaying components in a horizontal layout with overflow detection
2557
+ *
2558
+ * @param {HorizontalOverflowScrollerProps} props - The props for the component
2559
+ * @returns {Element} HorizontalOverflowScroller component
2560
+ */
2561
+ const HorizontalOverflowScroller = ({ className, dataTestId, children, onScrollStateChange, }) => {
2562
+ const containerRef = useRef(null);
2563
+ const childrenArray = Children.toArray(children);
2564
+ const { width: containerWidth } = useGeometry(containerRef);
2565
+ const { isScrollable, isAtBeginning, isAtEnd } = useScrollDetection(containerRef, {
2566
+ direction: "horizontal",
2567
+ onScrollStateChange: onScrollStateChange
2568
+ ? (state) => onScrollStateChange({
2569
+ isScrollable: state.isScrollable,
2570
+ isAtBeginning: state.isAtBeginning,
2571
+ isAtEnd: state.isAtEnd,
2572
+ })
2573
+ : undefined,
2574
+ });
2575
+ const handleScrollLeft = () => {
2576
+ const element = containerRef.current;
2577
+ if (!element || !containerWidth)
2578
+ return;
2579
+ element.scrollBy({
2580
+ left: -containerWidth,
2581
+ behavior: "smooth",
2582
+ });
2583
+ };
2584
+ const handleScrollRight = () => {
2585
+ const element = containerRef.current;
2586
+ if (!element || !containerWidth)
2587
+ return;
2588
+ element.scrollBy({
2589
+ left: containerWidth,
2590
+ behavior: "smooth",
2591
+ });
2592
+ };
2593
+ return (jsxs(ZStack, { className: cvaHorizontalOverflowScrollerAndIndicatorsContainer({ className }), children: [jsx("div", { className: cvaHorizontalOverflowScroller({ className }), "data-testid": dataTestId, ref: containerRef, children: childrenArray.map((child, index) => (jsx(Fragment, { children: child }, index))) }), isScrollable && !isAtBeginning ? (jsx(OverflowIndicator, { dataTestId: `${dataTestId}-left-indicator`, direction: "left", onClickScroll: handleScrollLeft })) : null, isScrollable && !isAtEnd ? (jsx(OverflowIndicator, { dataTestId: `${dataTestId}-right-indicator`, direction: "right", onClickScroll: handleScrollRight })) : null] }));
2594
+ };
2595
+
2393
2596
  const PADDING = 12;
2394
2597
  /**
2395
2598
  * Converts a width size value into a CSS dimension value for max constraints
@@ -2774,7 +2977,7 @@ const FloatingArrowContainer = ({ arrowRef, mode = "dark", }) => {
2774
2977
  * @param {TooltipProps} props - The props for the Tooltip component
2775
2978
  * @returns {ReactElement} Tooltip component
2776
2979
  */
2777
- const Tooltip = ({ children, dataTestId, disabled = false, className, label, placement = "auto", mode = "dark", iconProps, id, }) => {
2980
+ const Tooltip = ({ children, dataTestId, disabled = false, className, label, placement = "auto", mode = "dark", iconProps, id, style, }) => {
2778
2981
  const [isOpen, setIsOpen] = useState(false);
2779
2982
  const arrowRef = useRef(null);
2780
2983
  const { refs, floatingStyles, context } = useFloating({
@@ -2807,7 +3010,7 @@ const Tooltip = ({ children, dataTestId, disabled = false, className, label, pla
2807
3010
  }
2808
3011
  setIsOpen(false);
2809
3012
  }, [disabled]);
2810
- return (jsxs(Popover, { activation: { hover: true }, className: cvaTooltipPopover(), dataTestId: dataTestId, id: id, placement: placement === "auto" ? "bottom" : placement, children: [jsx(PopoverTrigger, { className: cvaTooltipIcon({ color: mode, className }), "data-testid": dataTestId ? `${dataTestId}-trigger` : null, onMouseDown: closeTooltip, onMouseEnter: openTooltip, onMouseLeave: closeTooltip, ref: refs.setReference, children: children === undefined ? (jsx("div", { children: jsx(Icon, { dataTestId: dataTestId ? `${dataTestId}-icon` : undefined, name: "QuestionMarkCircle", size: "small", ...iconProps }) })) : (wrappedChildren) }), isMounted ? (jsx("div", { ref: refs.setFloating, style: floatingStyles, children: jsx(PopoverContent, { children: jsxs("div", { "aria-label": typeof label === "string" ? label : undefined, className: cvaTooltipPopoverContent({ color: mode }), "data-testid": `${dataTestId}-content`, children: [jsx(Text, { dataTestId: `${dataTestId}-text`, inverted: mode === "dark", size: "small", type: typeof label === "string" ? "p" : "span", children: label }), placement !== "auto" && jsx(FloatingArrowContainer, { arrowRef: arrowRef, mode: mode })] }) }) })) : null] }));
3013
+ return (jsxs(Popover, { activation: { hover: true }, className: cvaTooltipPopover(), dataTestId: dataTestId, id: id, placement: placement === "auto" ? "bottom" : placement, children: [jsx(PopoverTrigger, { className: cvaTooltipIcon({ color: mode, className }), "data-testid": dataTestId ? `${dataTestId}-trigger` : null, onMouseDown: closeTooltip, onMouseEnter: openTooltip, onMouseLeave: closeTooltip, ref: refs.setReference, style: style, children: children === undefined ? (jsx("div", { children: jsx(Icon, { dataTestId: dataTestId ? `${dataTestId}-icon` : undefined, name: "QuestionMarkCircle", size: "small", ...iconProps }) })) : (wrappedChildren) }), isMounted ? (jsx("div", { ref: refs.setFloating, style: floatingStyles, children: jsx(PopoverContent, { children: jsxs("div", { "aria-label": typeof label === "string" ? label : undefined, className: cvaTooltipPopoverContent({ color: mode }), "data-testid": `${dataTestId}-content`, children: [jsx(Text, { dataTestId: `${dataTestId}-text`, inverted: mode === "dark", size: "small", type: typeof label === "string" ? "p" : "span", children: label }), placement !== "auto" && jsx(FloatingArrowContainer, { arrowRef: arrowRef, mode: mode })] }) }) })) : null] }));
2811
3014
  };
2812
3015
 
2813
3016
  const cvaIndicator = cvaMerge(["flex", "items-center"]);
@@ -2944,12 +3147,12 @@ const Indicator = ({ dataTestId, icon, label, color = "unknown", withBackground
2944
3147
  return (jsx(Tooltip, { className: className, disabled: withLabel, label: label, placement: "bottom", children: jsxs("div", { "aria-label": label, className: cvaIndicator(), "data-testid": dataTestId, ...rest, children: [jsxs("div", { className: cvaIndicatorIconBackground({ color, background: withBackground ? "visible" : "hidden" }), "data-testid": dataTestId ? `${dataTestId}-background` : "indicator-background", children: [ping ? (jsx("div", { className: cvaIndicatorPing({ color }), "data-testid": dataTestId ? `${dataTestId}-ping` : "indicator-ping" })) : null, icon] }), label && withLabel ? (jsx("div", { className: cvaIndicatorLabel({ size, weight, background: withBackground ? "visible" : "hidden" }), "data-testid": dataTestId ? `${dataTestId}-label` : undefined, children: label })) : null] }) }));
2945
3148
  };
2946
3149
 
2947
- const cvaKPI = cvaMerge(["w-full", "px-4", "py-2", "flex", "flex-col"], {
3150
+ const cvaKPI = cvaMerge(["w-full", "flex", "flex-col"], {
2948
3151
  variants: {
2949
3152
  variant: {
2950
- small: ["px-3"],
3153
+ small: ["px-3", "py-2"],
2951
3154
  condensed: ["px-2", "py-0"],
2952
- default: [""],
3155
+ default: ["px-4", "py-2"],
2953
3156
  },
2954
3157
  },
2955
3158
  defaultVariants: {
@@ -3001,9 +3204,9 @@ const LoadingContent$1 = () => (jsx("div", { className: "flex h-11 flex-row item
3001
3204
  * @param {KPIProps} props - The props for the KPI component
3002
3205
  * @returns {ReactElement} KPI component
3003
3206
  */
3004
- const KPI = ({ title, value, loading = false, unit, className, dataTestId, tooltipLabel, variant = "default", trend, ...rest }) => {
3207
+ const KPI = ({ title, value, loading = false, unit, className, dataTestId, tooltipLabel, variant = "default", trend, style, ...rest }) => {
3005
3208
  const isSmallVariant = variant === "small";
3006
- return (jsx(Tooltip, { dataTestId: dataTestId ? `${dataTestId}-tooltip` : undefined, disabled: tooltipLabel === undefined || tooltipLabel === "", label: tooltipLabel, placement: "bottom", children: jsx("div", { className: cvaKPI({ variant, className }), "data-testid": dataTestId ? `${dataTestId}` : undefined, ...rest, children: loading ? (jsx(LoadingContent$1, {})) : (jsxs(Fragment, { children: [jsx("div", { className: cvaKPIHeader(), children: jsx(Text, { className: cvaKPITitleText(), dataTestId: dataTestId ? `${dataTestId}-title` : undefined, size: isSmallVariant ? "small" : "medium", subtle: true, weight: isSmallVariant ? "normal" : "bold", children: title }) }), jsx(Text, { className: cvaKPIvalueText({ variant }), dataTestId: dataTestId ? `${dataTestId}-value` : undefined, size: isSmallVariant ? "small" : "large", type: "div", weight: isSmallVariant ? "bold" : "thick", children: jsxs("div", { className: cvaKPIValueContainer({
3209
+ return (jsx(Tooltip, { dataTestId: dataTestId ? `${dataTestId}-tooltip` : undefined, disabled: tooltipLabel === undefined || tooltipLabel === "", label: tooltipLabel, placement: "bottom", style: style, children: jsx("div", { className: cvaKPI({ variant, className }), "data-testid": dataTestId ? `${dataTestId}` : undefined, ...rest, children: loading ? (jsx(LoadingContent$1, {})) : (jsxs(Fragment$1, { children: [jsx("div", { className: cvaKPIHeader(), children: jsx(Text, { className: cvaKPITitleText(), dataTestId: dataTestId ? `${dataTestId}-title` : undefined, size: isSmallVariant ? "small" : "medium", subtle: true, weight: isSmallVariant ? "normal" : "bold", children: title }) }), jsx(Text, { className: cvaKPIvalueText({ variant }), dataTestId: dataTestId ? `${dataTestId}-value` : undefined, size: isSmallVariant ? "small" : "large", type: "div", weight: isSmallVariant ? "bold" : "thick", children: jsxs("div", { className: cvaKPIValueContainer({
3007
3210
  isDefaultAndHasTrendValue: Boolean(trend !== undefined && trend.value !== undefined && !isSmallVariant),
3008
3211
  className,
3009
3212
  }), children: [jsxs("span", { className: cvaKPIvalueText({ variant }), children: [value, " ", unit] }), jsx(TrendIndicator, { isSmallVariant: isSmallVariant, trend: trend, unit: unit })] }) })] })) }) }));
@@ -3075,25 +3278,66 @@ const cvaCardBodyDensityContainer = cvaMerge(["grid", "grid-cols-[1fr_auto]"], {
3075
3278
  },
3076
3279
  });
3077
3280
 
3078
- const cvaListContainer = cvaMerge(["h-full"], {
3281
+ const cvaListItem$1 = cvaMerge(["py-3", "px-4", "min-h-14", "w-full", "flex", "justify-between", "items-center"]);
3282
+ const cvaMainInformationClass = cvaMerge(["grid", "items-center", "text-sm", "gap-2"], {
3283
+ variants: {
3284
+ hasThumbnail: {
3285
+ true: "grid-cols-min-fr",
3286
+ false: "grid-cols-1",
3287
+ },
3288
+ },
3289
+ });
3290
+ const cvaThumbnailContainer = cvaMerge([
3291
+ "flex",
3292
+ "h-8",
3293
+ "w-8",
3294
+ "items-center",
3295
+ "justify-center",
3296
+ "overflow-hidden",
3297
+ "rounded-md",
3298
+ ]);
3299
+
3300
+ const DEFAULT_SKELETON_LIST_ITEM_PROPS = {
3301
+ hasThumbnail: true,
3302
+ thumbnailShape: "circle",
3303
+ hasDescription: true,
3304
+ hasMeta: false,
3305
+ hasDetails: false,
3306
+ };
3307
+ /**
3308
+ * Skeleton loading indicator that mimics the ListItem component structure.
3309
+ * Uses the same layout, spacing, and visual hierarchy as ListItem.
3310
+ */
3311
+ const ListItemSkeleton = ({ hasThumbnail = DEFAULT_SKELETON_LIST_ITEM_PROPS.hasThumbnail, thumbnailShape = "circle", hasDescription = DEFAULT_SKELETON_LIST_ITEM_PROPS.hasDescription, hasMeta = DEFAULT_SKELETON_LIST_ITEM_PROPS.hasMeta, hasDetails = DEFAULT_SKELETON_LIST_ITEM_PROPS.hasDetails, }) => {
3312
+ // Generate stable random widths once and never change them
3313
+ const lineWidths = useStable(() => {
3314
+ return {
3315
+ title: getResponsiveRandomWidthPercentage({ min: 60, max: 85 }),
3316
+ description: getResponsiveRandomWidthPercentage({ min: 45, max: 70 }),
3317
+ meta: getResponsiveRandomWidthPercentage({ min: 30, max: 55 }),
3318
+ details: getResponsiveRandomWidthPercentage({ min: 25, max: 45 }),
3319
+ };
3320
+ });
3321
+ return (jsxs("div", { className: cvaListItem$1({ className: "w-full" }), children: [jsxs("div", { className: cvaMainInformationClass({ hasThumbnail, className: "w-full" }), children: [hasThumbnail ? (jsx("div", { className: cvaThumbnailContainer({ className: "bg-gray-200" }), children: jsx("div", { className: twMerge("bg-gray-300", thumbnailShape === "circle" ? "rounded-full" : "rounded"), style: { width: 20, height: 20 } }) })) : null, jsxs("div", { className: "grid-rows-min-fr grid w-full items-center gap-1 text-sm", children: [jsx(SkeletonLines, { height: "0.875em", lines: 1, width: lineWidths.title }), hasDescription ? jsx(SkeletonLines, { height: "0.75em", lines: 1, width: lineWidths.description }) : null, hasMeta ? jsx(SkeletonLines, { height: "0.75em", lines: 1, width: lineWidths.meta }) : null] })] }), hasDetails ? (jsx("div", { className: "pl-2 text-sm", children: jsx(SkeletonLines, { height: "0.875em", lines: 1, width: lineWidths.details }) })) : null] }));
3322
+ };
3323
+
3324
+ const cvaListContainer = cvaMerge(["overflow-auto", "h-full"], {
3079
3325
  variants: {
3080
- parentControlledScrollable: {
3081
- true: [""],
3082
- false: ["overflow-auto"],
3326
+ withTopSeparator: {
3327
+ true: ["border-t", "border-neutral-200", "transition-colors duration-200 ease-in"],
3328
+ false: ["border-t", "border-transparent", "transition-colors duration-200 ease-in"],
3083
3329
  },
3084
3330
  },
3085
3331
  defaultVariants: {
3086
- parentControlledScrollable: false,
3332
+ withTopSeparator: false,
3087
3333
  },
3088
3334
  });
3089
3335
  const cvaList = cvaMerge(["relative"]);
3090
- const cvaListItem$1 = cvaMerge(["absolute", "top-0", "left-0", "w-full"], {
3336
+ const cvaListItem = cvaMerge(["absolute", "top-0", "left-0", "w-full"], {
3091
3337
  variants: {
3092
3338
  separator: {
3093
- alternating: ["even:bg-slate-100"],
3094
- line: ["[&:not(:last-child)]:border-b", "border-gray-200"],
3339
+ line: ["[&:not(:last-child)]:border-b", "border-neutral-200"],
3095
3340
  none: "",
3096
- space: "[&:not(:last-child)]:pb-0.5",
3097
3341
  },
3098
3342
  },
3099
3343
  defaultVariants: {
@@ -3102,62 +3346,129 @@ const cvaListItem$1 = cvaMerge(["absolute", "top-0", "left-0", "w-full"], {
3102
3346
  });
3103
3347
 
3104
3348
  /**
3105
- * Render a performant virtualized list of items. Optionally with infinite scrolling.
3106
3349
  *
3107
- * @property {number} count - The total number of items in the list.
3108
- * @property {number} [rowHeight="40"] - The estimated height of each row in the list.
3109
- * @property {RelayPagination | undefined} pagination - Pagination configuration for the list.
3110
- * @property {separator} [separator="line"] - The separator style between items in the list.
3111
- * @property {(index: number) =>ReactElement} children - A function that takes an index and returns the JSX element to be rendered at said index.
3112
- * @property {loadingIndicator} [loadingIndicator="spinner"] - The type of loading indicator in the list.
3113
- * @property {skeletonLinesHeight} [skeletonLinesHeight="2rem"] - The height of the skeleton lines.
3114
3350
  */
3115
- const List = ({ count, rowHeight = 40, pagination, children, className, dataTestId, separator = "none", loadingIndicator = "spinner", skeletonLinesHeight = rowHeight + "px", onRowClick, scrollRef, }) => {
3116
- const containerRef = useRef(null);
3117
- const listRef = useRef(null);
3118
- const [scrollParent, setScrollParent] = useState(null);
3119
- const [parentControlledScrollable, setParentControlledScrollable] = useState(false);
3120
- useEffect(() => {
3121
- if (scrollRef?.current) {
3122
- setParentControlledScrollable(true);
3123
- setScrollParent(scrollRef.current);
3351
+ const ListLoadingIndicator = ({ type, hasThumbnail, thumbnailShape, hasDescription, component, hasMeta, hasDetails, }) => {
3352
+ switch (type) {
3353
+ case "none":
3354
+ return null;
3355
+ case "spinner":
3356
+ return jsx(Spinner, { centering: "horizontally", containerClassName: "p-4" });
3357
+ case "custom":
3358
+ return component;
3359
+ case "skeleton":
3360
+ return (jsx(ListItemSkeleton, { hasDescription: hasDescription, hasDetails: hasDetails, hasMeta: hasMeta, hasThumbnail: hasThumbnail, thumbnailShape: thumbnailShape }));
3361
+ default: {
3362
+ throw new Error(`${type} is not known`);
3124
3363
  }
3125
- else {
3126
- setParentControlledScrollable(false);
3127
- setScrollParent(containerRef.current);
3364
+ }
3365
+ };
3366
+
3367
+ const DEFAULT_ROW_HEIGHT = 61; // 61px is the height of the ListItem component as of 2025-09-26
3368
+ /**
3369
+ * A performant virtualized list component with infinite scrolling support.
3370
+ *
3371
+ * ⚠️ **Important**: Requires a container with defined height to work properly.
3372
+ *
3373
+ * Features:
3374
+ * - Virtualized rendering using TanStack Virtual for performance with large datasets
3375
+ * - Automatic infinite scroll loading when approaching the end of the list
3376
+ * - Optional header support (automatically managed, scrolls with content)
3377
+ * - Built-in pagination with relay-style cursor support
3378
+ * - Configurable loading indicators (skeleton, spinner, or custom)
3379
+ * - Scroll state detection and callbacks
3380
+ * - Variable-height item support via `estimateItemSize`
3381
+ *
3382
+ * The component automatically loads more data when:
3383
+ * - User scrolls to the last visible item
3384
+ * - Content height is insufficient to fill the container
3385
+ */
3386
+ const List = ({ count, pagination, children, className, dataTestId, separator = "line", loadingIndicator = { type: "skeleton", ...DEFAULT_SKELETON_LIST_ITEM_PROPS }, onRowClick, onScrollStateChange, topSeparatorOnScroll = false, estimateItemSize, header, getItem, }) => {
3387
+ const parentRef = useRef(null);
3388
+ // Calculate the actual count including header
3389
+ const actualCount = count + (header ? 1 : 0);
3390
+ // Helper function to get item for a given data index
3391
+ const getItemAtIndex = useCallback((dataIndex) => {
3392
+ return getItem(dataIndex);
3393
+ }, [getItem]);
3394
+ // Calculate how many loading rows we need
3395
+ const getLoadingRowsCount = useCallback(() => {
3396
+ const { type: loadingIndicatorType } = loadingIndicator;
3397
+ if (pagination?.isLoading === false)
3398
+ return 0;
3399
+ switch (loadingIndicatorType) {
3400
+ case "none":
3401
+ return 0;
3402
+ case "spinner":
3403
+ return 1;
3404
+ case "custom":
3405
+ case "skeleton": {
3406
+ const isInitialLoading = !pagination?.pageInfo;
3407
+ const initialCount = loadingIndicator.initialLoadingCount ?? 10;
3408
+ const scrollCount = loadingIndicator.scrollLoadingCount ?? 3;
3409
+ return isInitialLoading ? initialCount : scrollCount;
3410
+ }
3411
+ default: {
3412
+ throw new Error(`${loadingIndicatorType} is not known`);
3413
+ }
3128
3414
  }
3129
- }, [scrollRef]);
3130
- const infiniteScrollProps = useMemo(() => {
3131
- return {
3132
- pagination: pagination || noPagination,
3133
- containerRef: { current: scrollParent },
3134
- rowSize: pagination !== undefined &&
3135
- pagination.pageInfo !== undefined &&
3136
- pagination.pageInfo.hasNextPage === true &&
3137
- pagination.isLoading === true
3138
- ? count + 1
3139
- : count,
3140
- rowHeight,
3141
- };
3142
- }, [pagination, scrollParent, count, rowHeight]);
3143
- const { fetchMoreOnBottomReached, getVirtualItems, getTotalSize, measureElement } = useInfiniteScroll(infiniteScrollProps);
3144
- useEffect(() => {
3145
- if (scrollParent) {
3146
- const handleScroll = () => {
3147
- fetchMoreOnBottomReached(scrollParent);
3148
- };
3149
- scrollParent.addEventListener("scroll", handleScroll);
3150
- return () => {
3151
- scrollParent.removeEventListener("scroll", handleScroll);
3152
- };
3415
+ }, [loadingIndicator, pagination]);
3416
+ const estimateSize = useCallback((index) => {
3417
+ // Check if this is a loading row
3418
+ if (index >= actualCount) {
3419
+ const loaderIndex = index - actualCount;
3420
+ const shouldShowLoader = pagination?.isLoading === true && loaderIndex < getLoadingRowsCount();
3421
+ // Empty loader rows should be estimated at 0 height to prevent blank space
3422
+ return shouldShowLoader ? DEFAULT_ROW_HEIGHT : 0;
3153
3423
  }
3154
- return undefined;
3155
- }, [scrollParent, fetchMoreOnBottomReached]);
3156
- return (jsx("div", { className: cvaListContainer({ parentControlledScrollable, className }), "data-testid": dataTestId, ref: containerRef, children: jsx("ul", { className: cvaList(), ref: listRef, style: { height: `${getTotalSize()}px`, outline: "none" }, children: getVirtualItems().map(virtualRow => {
3157
- const isLoaderRow = virtualRow.index > count - 1;
3158
- return (jsx("li", { className: cvaListItem$1({ separator }), "data-index": virtualRow.index, onClick: onRowClick !== undefined ? () => onRowClick(virtualRow.index) : undefined, ref: measureElement, style: {
3159
- transform: `translateY(${virtualRow.start}px)`,
3160
- }, tabIndex: -1, children: isLoaderRow ? (pagination?.isLoading === true ? (jsxs(Fragment, { children: [loadingIndicator === "spinner" && jsx(Spinner, { centering: "horizontally", containerClassName: "p-4" }), loadingIndicator === "skeletonLines" && (jsx(SkeletonLines, { height: skeletonLinesHeight, lines: 3, width: "full" }))] })) : null) : (children(virtualRow.index)) }, virtualRow.key));
3424
+ // For data rows (including header), use custom estimator if provided
3425
+ return estimateItemSize ? estimateItemSize(index) : DEFAULT_ROW_HEIGHT;
3426
+ }, [estimateItemSize, actualCount, pagination?.isLoading, getLoadingRowsCount]);
3427
+ const { getVirtualItems, getTotalSize, measureElement, scrollOffset } = useInfiniteScroll({
3428
+ pagination: pagination || noPagination,
3429
+ containerRef: parentRef,
3430
+ count: actualCount + (pagination?.isLoading === true ? getLoadingRowsCount() : 0),
3431
+ estimateSize,
3432
+ onChange: virtualizer => onScrollStateChange?.(virtualizer.scrollOffset ?? 0, virtualizer.isScrolling),
3433
+ });
3434
+ const isAtTop = scrollOffset <= 0;
3435
+ return (jsx("div", { className: cvaListContainer({
3436
+ withTopSeparator: topSeparatorOnScroll && !isAtTop,
3437
+ className,
3438
+ }), "data-testid": dataTestId, ref: parentRef, children: jsx("ul", { className: cvaList(), style: { height: `${getTotalSize()}px` }, children: getVirtualItems().map(virtualRow => {
3439
+ const isLoaderRow = virtualRow.index >= actualCount;
3440
+ const isHeaderRow = Boolean(header) && virtualRow.index === 0;
3441
+ // Calculate data index: if header exists, subtract 1 from index for data items
3442
+ const dataIndex = Boolean(header) && !isHeaderRow ? virtualRow.index - 1 : virtualRow.index;
3443
+ // Calculate which loading indicator this is (for multiple loading indicators)
3444
+ const loaderIndex = isLoaderRow ? virtualRow.index - actualCount : 0;
3445
+ // Props required by the virtualizer for proper positioning and behavior
3446
+ const listItemProps = {
3447
+ className: cvaListItem({ separator }), // List styling (separators, spacing)
3448
+ "data-index": isLoaderRow || isHeaderRow ? virtualRow.index : dataIndex, // For accessibility and debugging
3449
+ onClick: onRowClick && !isLoaderRow && !isHeaderRow
3450
+ ? () => {
3451
+ const clickedItem = getItemAtIndex(dataIndex);
3452
+ onRowClick(clickedItem, dataIndex);
3453
+ }
3454
+ : undefined, // Row-level click handling (skip header)
3455
+ ref: measureElement, // Required for virtualizer to measure item dimensions
3456
+ style: {
3457
+ transform: `translateY(${virtualRow.start}px)`, // Critical: positions item in virtual scroll
3458
+ },
3459
+ tabIndex: -1, // Keyboard navigation support
3460
+ };
3461
+ // Handle loading rows
3462
+ if (isLoaderRow) {
3463
+ return (jsx("li", { ...listItemProps, children: pagination?.isLoading === true && loaderIndex < getLoadingRowsCount() ? (jsx(ListLoadingIndicator, { ...loadingIndicator })) : null }, virtualRow.index));
3464
+ }
3465
+ // Handle header row
3466
+ if (isHeaderRow && header) {
3467
+ return (jsx("li", { ...listItemProps, children: header }, "header"));
3468
+ }
3469
+ // For regular children, call the children function with virtualization props and item data
3470
+ const item = getItemAtIndex(dataIndex);
3471
+ return children(listItemProps, item, dataIndex);
3161
3472
  }) }) }));
3162
3473
  };
3163
3474
 
@@ -3222,35 +3533,16 @@ const cvaInteractableItem = cvaMerge("", {
3222
3533
  },
3223
3534
  });
3224
3535
 
3225
- const cvaListItem = cvaMerge(["py-3", "px-4", "min-h-14", "w-full", "flex", "justify-between", "items-center"]);
3226
- const cvaMainInformationClass = cvaMerge(["grid", "items-center", "text-sm", "gap-2"], {
3227
- variants: {
3228
- hasThumbnail: {
3229
- true: "grid-cols-min-fr",
3230
- false: "grid-cols-1",
3231
- },
3232
- },
3233
- });
3234
- const cvaThumbnailContainer = cvaMerge([
3235
- "flex",
3236
- "h-8",
3237
- "w-8",
3238
- "items-center",
3239
- "justify-center",
3240
- "overflow-hidden",
3241
- "rounded-md",
3242
- ]);
3243
-
3244
3536
  /**
3245
3537
  * The ListItem is designed to present a concise set of items for quick scanning and navigation. It supports multiple content types and actions, and its flexible layout allows for customization based on the type of data being shown - assets, events, users, etc.
3246
3538
  *
3247
3539
  * @param { ListItemProps} props - The props for the ListItem component
3248
3540
  * @returns {Element} ListItem component
3249
3541
  */
3250
- const ListItem = ({ className, dataTestId, onClick, details, title, description, meta, thumbnail, thumbnailColor = "info-600", thumbnailBackground = "info-100", }) => {
3251
- const baseClass = cvaListItem({ className });
3542
+ const ListItem = ({ className, dataTestId, onClick, details, title, description, meta, thumbnail, thumbnailColor = "info-600", thumbnailBackground = "info-100", ...rest }) => {
3543
+ const baseClass = cvaListItem$1({ className });
3252
3544
  const interactableItemClass = onClick ? twMerge(baseClass, cvaInteractableItem({ cursor: "pointer" })) : baseClass;
3253
- return (jsxs("div", { className: interactableItemClass, "data-testid": dataTestId, onClick: onClick, children: [jsxs("div", { className: cvaMainInformationClass({ hasThumbnail: !!thumbnail }), children: [thumbnail ? (jsx("div", { className: cvaThumbnailContainer({
3545
+ return (jsxs("li", { className: interactableItemClass, "data-testid": dataTestId, onClick: onClick, ...rest, children: [jsxs("div", { className: cvaMainInformationClass({ hasThumbnail: !!thumbnail }), children: [thumbnail ? (jsx("div", { className: cvaThumbnailContainer({
3254
3546
  className: `text-${thumbnailColor} bg-${thumbnailBackground}`,
3255
3547
  }), children: thumbnail })) : null, jsxs("div", { className: "grid-rows-min-fr grid items-center text-sm", children: [jsx("div", { className: "gap-responsive-space-sm flex w-full min-w-0 items-center text-sm", children: typeof title === "string" ? (jsx(Text, { className: "truncate", dataTestId: dataTestId ? `${dataTestId}-title` : undefined, weight: "bold", children: title })) : (cloneElement(title, {
3256
3548
  className: twMerge(title.props.className, "neutral-900 text-sm font-medium truncate"),
@@ -3258,7 +3550,7 @@ const ListItem = ({ className, dataTestId, onClick, details, title, description,
3258
3550
  })) }), description !== undefined && description !== "" ? (jsx("div", { className: "gap-responsive-space-sm flex w-full min-w-0 items-center", children: typeof description === "string" ? (jsx(Text, { className: "truncate text-xs text-neutral-500", dataTestId: dataTestId ? `${dataTestId}-description` : undefined, weight: "bold", children: description })) : (cloneElement(description, {
3259
3551
  className: twMerge(description.props.className, "text-neutral-500 text-xs font-medium truncate"),
3260
3552
  dataTestId: !description.props.dataTestId && dataTestId ? `${dataTestId}-description` : undefined,
3261
- })) })) : null, meta ? (jsx("div", { className: "gap-responsive-space-sm flex w-full min-w-0 items-center pt-0.5", children: jsx(Text, { className: "truncate text-xs text-neutral-400", dataTestId: dataTestId ? `${dataTestId}-meta` : undefined, weight: "bold", children: meta }) })) : null] })] }), jsxs("div", { className: "flex items-center gap-0.5 pl-2", children: [details, onClick ? jsx(Icon, { color: "neutral", name: "ChevronRight", size: "medium" }) : null] })] }));
3553
+ })) })) : null, meta ? (jsx("div", { className: "gap-responsive-space-sm flex w-full min-w-0 items-center pt-0.5", children: jsx(Text, { className: "truncate text-xs text-neutral-400", dataTestId: dataTestId ? `${dataTestId}-meta` : undefined, weight: "bold", children: meta }) })) : null] })] }), jsxs("div", { className: "flex items-center gap-0.5 text-nowrap pl-2", children: [details, onClick ? jsx(Icon, { color: "neutral", name: "ChevronRight", size: "medium" }) : null] })] }));
3262
3554
  };
3263
3555
 
3264
3556
  const cvaMenuList = cvaMerge([
@@ -3783,7 +4075,7 @@ const Pagination = ({ previousPage, nextPage, canPreviousPage = false, canNextPa
3783
4075
  if (loading) {
3784
4076
  return (jsx("div", { className: cvaPagination({ className }), children: jsx(SkeletonLines, { height: 16, width: 150 }) }));
3785
4077
  }
3786
- return (jsxs("div", { className: cvaPagination({ className }), "data-testid": dataTestId, children: [jsx(IconButton, { "data-testid": "prev-page", disabled: cursorBase ? !canPreviousPage || false : page !== undefined && page <= 0, icon: jsx(Icon, { name: "ChevronLeft", size: "small" }), onClick: () => handlePageChange("prev"), size: "small", variant: "ghost-neutral" }), !cursorBase && (jsxs(Fragment, { children: [jsx("div", { className: cvaPaginationText(), "data-testid": "current-page", children: currentPage }), jsx("div", { className: cvaPaginationText(), "data-testid": "page-count", children: pageCount !== null && pageCount !== undefined && getTranslatedCount ? getTranslatedCount(pageCount) : null })] })), jsx(IconButton, { "data-testid": "next-page", disabled: cursorBase
4078
+ return (jsxs("div", { className: cvaPagination({ className }), "data-testid": dataTestId, children: [jsx(IconButton, { "data-testid": "prev-page", disabled: cursorBase ? !canPreviousPage || false : page !== undefined && page <= 0, icon: jsx(Icon, { name: "ChevronLeft", size: "small" }), onClick: () => handlePageChange("prev"), size: "small", variant: "ghost-neutral" }), !cursorBase && (jsxs(Fragment$1, { children: [jsx("div", { className: cvaPaginationText(), "data-testid": "current-page", children: currentPage }), jsx("div", { className: cvaPaginationText(), "data-testid": "page-count", children: pageCount !== null && pageCount !== undefined && getTranslatedCount ? getTranslatedCount(pageCount) : null })] })), jsx(IconButton, { "data-testid": "next-page", disabled: cursorBase
3787
4079
  ? !canNextPage || false
3788
4080
  : page !== undefined && pageCount !== undefined && pageCount !== null && page >= pageCount - 1, icon: jsx(Icon, { name: "ChevronRight", size: "small" }), onClick: () => handlePageChange("next"), size: "small", variant: "ghost-neutral" })] }));
3789
4081
  };
@@ -4064,7 +4356,7 @@ const cvaTab = cvaMerge([
4064
4356
  * We add a custom implementation of the asChild prop to make it easy to make the child element look like other tabs.
4065
4357
  */
4066
4358
  const Tab = ({ value, isFullWidth = false, iconName = undefined, dataTestId, className, children, suffix, asChild = false, appendTabStylesToChildIfAsChild = true, ...rest }) => {
4067
- const renderContent = () => (jsxs(Fragment, { children: [iconName !== undefined ? jsx(Icon, { name: iconName, size: "small" }) : null, isValidElement(children) ? children.props.children : children, suffix] }));
4359
+ const renderContent = () => (jsxs(Fragment$1, { children: [iconName !== undefined ? jsx(Icon, { name: iconName, size: "small" }) : null, isValidElement(children) ? children.props.children : children, suffix] }));
4068
4360
  const commonProps = {
4069
4361
  className: appendTabStylesToChildIfAsChild ? cvaTab({ className, isFullWidth }) : className,
4070
4362
  ...rest,
@@ -4283,7 +4575,7 @@ const ToggleButton = ({ title, size, children, dataTestId, className, icon, icon
4283
4575
  large: "p-2 text-base",
4284
4576
  };
4285
4577
  const paddingClasses = isIconOnly ? iconOnlySizeClasses[size] : sizeClasses[size];
4286
- return (jsx("button", { className: twMerge("flex items-center justify-center gap-1 self-stretch", paddingClasses, className), "data-testid": dataTestId, title: isIconOnly ? title : undefined, type: "button", ...rest, children: isIconOnly ? (icon) : (jsxs(Fragment, { children: [iconPrefix, children] })) }));
4578
+ return (jsx("button", { className: twMerge("flex items-center justify-center gap-1 self-stretch", paddingClasses, className), "data-testid": dataTestId, title: isIconOnly ? title : undefined, type: "button", ...rest, children: isIconOnly ? (icon) : (jsxs(Fragment$1, { children: [iconPrefix, children] })) }));
4287
4579
  };
4288
4580
 
4289
4581
  const cvaValueBar = cvaMerge([
@@ -4399,28 +4691,6 @@ const ValueBar = ({ value, min = 0, max = 100, unit, size = "small", levelColors
4399
4691
  return (jsxs("span", { className: "relative flex items-center gap-2", "data-testid": dataTestId, children: [jsx("progress", { "aria-label": valueText, className: cvaValueBar({ className, size }), max: 100, style: { color: barFillColor }, value: score * 100 }), showValue && (size === "small" || size === "large") ? (jsx(Text, { className: cvaValueBarText({ size }), dataTestId: dataTestId ? `${dataTestId}-value` : undefined, children: jsx("span", { style: valueColor ? { color: valueColor } : undefined, children: valueText }) })) : null] }));
4400
4692
  };
4401
4693
 
4402
- const cvaZStackContainer = cvaMerge(["grid", "grid-cols-1", "grid-rows-1"]);
4403
- const cvaZStackItem = cvaMerge(["col-start-1", "col-end-1", "row-start-1", "row-end-2"]);
4404
-
4405
- /**
4406
- * ZStack is a component that stacks its children on the z-axis.
4407
- * Is a good alternative to "position: absolute" that avoids some of the unfortunate side effects of absolute positioning.
4408
- *
4409
- * @param { ZStackProps} props - The props for the ZStack component
4410
- * @returns {Element} ZStack component
4411
- */
4412
- const ZStack = ({ children, className, dataTestId }) => {
4413
- return (jsx("div", { className: cvaZStackContainer({ className }), "data-testid": dataTestId, children: Children.map(children, (child, index) => {
4414
- if (!isValidElement(child)) {
4415
- return child;
4416
- }
4417
- return cloneElement(child, {
4418
- className: cvaZStackItem({ className: child.props.className }),
4419
- key: index,
4420
- });
4421
- }) }));
4422
- };
4423
-
4424
4694
  const cvaClickable = cvaMerge([
4425
4695
  "shadow-lg",
4426
4696
  "rounded-lg",
@@ -4445,4 +4715,4 @@ const cvaClickable = cvaMerge([
4445
4715
  },
4446
4716
  });
4447
4717
 
4448
- export { ActionRenderer, Alert, Badge, Breadcrumb, BreadcrumbContainer, Button, Card, CardBody, CardFooter, CardHeader, Collapse, CompletionStatusIndicator, CopyableText, DetailsList, EmptyState, EmptyValue, ExternalLink, Heading, Highlight, Icon, IconButton, Indicator, KPI, KPICard, List, ListItem, 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, Tooltip, ValueBar, ZStack, cvaButton, cvaButtonPrefixSuffix, cvaButtonSpinner, cvaButtonSpinnerContainer, cvaClickable, cvaContainerStyles, cvaIconButton, cvaImgStyles, cvaIndicator, cvaIndicatorIcon, cvaIndicatorIconBackground, cvaIndicatorLabel, cvaIndicatorPing, cvaInteractableItem, cvaList, cvaListContainer, cvaListItem$1 as cvaListItem, cvaMenuItem, cvaMenuItemLabel, cvaMenuItemPrefix, cvaMenuItemStyle, cvaMenuItemSuffix, cvaPageHeader, cvaPageHeaderContainer, cvaPageHeaderHeading, cvaToggleGroup, cvaToggleGroupWithSlidingBackground, cvaToggleItem, cvaToggleItemContent, cvaToggleItemText, cvaZStackContainer, cvaZStackItem, docs, getDevicePixelRatio, getValueBarColorByValue, iconColorNames, iconPalette, useClickOutside, useContainerBreakpoints, useContinuousTimeout, useDebounce, useDevicePixelRatio, useElevatedReducer, useElevatedState, useGeometry, useHover, useIsFirstRender, useIsFullscreen, useIsTextTruncated, useModifierKey, useOverflowItems, usePopoverContext, usePrompt, useResize, useScrollDetection, useSelfUpdatingRef, useTimeout, useViewportBreakpoints, useWatch, useWindowActivity };
4718
+ export { ActionRenderer, Alert, Badge, Breadcrumb, BreadcrumbContainer, Button, Card, CardBody, CardFooter, CardHeader, Collapse, CompletionStatusIndicator, CopyableText, DetailsList, EmptyState, EmptyValue, ExternalLink, Heading, Highlight, HorizontalOverflowScroller, Icon, IconButton, Indicator, KPI, KPICard, List, ListItem, 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, Tooltip, ValueBar, ZStack, cvaButton, cvaButtonPrefixSuffix, cvaButtonSpinner, cvaButtonSpinnerContainer, cvaClickable, cvaContainerStyles, cvaIconButton, cvaImgStyles, cvaIndicator, cvaIndicatorIcon, cvaIndicatorIconBackground, cvaIndicatorLabel, cvaIndicatorPing, cvaInteractableItem, cvaList, cvaListContainer, cvaListItem, cvaMenuItem, cvaMenuItemLabel, cvaMenuItemPrefix, cvaMenuItemStyle, cvaMenuItemSuffix, cvaPageHeader, cvaPageHeaderContainer, cvaPageHeaderHeading, cvaToggleGroup, cvaToggleGroupWithSlidingBackground, cvaToggleItem, cvaToggleItemContent, cvaToggleItemText, cvaZStackContainer, cvaZStackItem, docs, getDevicePixelRatio, getResponsiveRandomWidthPercentage, getValueBarColorByValue, iconColorNames, iconPalette, useClickOutside, useContainerBreakpoints, useContinuousTimeout, useDebounce, useDevicePixelRatio, useElevatedReducer, useElevatedState, useGeometry, useHover, useIsFirstRender, useIsFullscreen, useIsTextTruncated, useModifierKey, useOverflowItems, usePopoverContext, usePrompt, useResize, useScrollDetection, useSelfUpdatingRef, useStable, useTimeout, useViewportBreakpoints, useWatch, useWindowActivity };