@trackunit/react-components 1.9.7 → 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.cjs.js +438 -165
- package/index.esm.js +443 -173
- package/package.json +7 -7
- package/src/common/Styleable.d.ts +3 -0
- package/src/common/index.d.ts +1 -0
- package/src/components/HorizontalOverflowScroller/HorizontalOverflowScroller.d.ts +21 -0
- package/src/components/HorizontalOverflowScroller/HorizontalOverflowScroller.variants.d.ts +2 -0
- package/src/components/HorizontalOverflowScroller/OverflowIndicator.d.ts +22 -0
- package/src/components/HorizontalOverflowScroller/OverflowIndicator.variants.d.ts +8 -0
- package/src/components/KPI/KPI.d.ts +3 -3
- package/src/components/List/List.d.ts +109 -19
- package/src/components/List/List.variants.d.ts +2 -2
- package/src/components/List/ListLoadingIndicator.d.ts +71 -0
- package/src/components/ListItem/ListItem.d.ts +11 -3
- package/src/components/ListItem/ListItemSkeleton.d.ts +25 -0
- package/src/components/SkeletonLines/SkeletonLines.d.ts +9 -8
- package/src/components/SkeletonLines/SkeletonLines.variants.d.ts +1 -0
- package/src/components/SkeletonLines/index.d.ts +1 -0
- package/src/components/SkeletonLines/skeleton-utils.d.ts +12 -0
- package/src/components/Tooltip/Tooltip.d.ts +3 -3
- package/src/components/index.d.ts +1 -0
- package/src/hooks/index.d.ts +1 -0
- package/src/hooks/useGeometry.d.ts +5 -1
- package/src/hooks/useResize.d.ts +6 -1
- package/src/hooks/useScrollDetection.d.ts +19 -11
- package/src/hooks/useStable.d.ts +22 -0
package/index.cjs.js
CHANGED
|
@@ -1313,7 +1313,7 @@ const useElevatedState = (initialState, customState) => {
|
|
|
1313
1313
|
* Custom hook to get the geometry of an element.
|
|
1314
1314
|
* Size and position of the element relative to the viewport.
|
|
1315
1315
|
*/
|
|
1316
|
-
const useGeometry = (ref) => {
|
|
1316
|
+
const useGeometry = (ref, { skip = false } = {}) => {
|
|
1317
1317
|
const [geometry, setGeometry] = react.useState({
|
|
1318
1318
|
width: 0,
|
|
1319
1319
|
height: 0,
|
|
@@ -1326,7 +1326,7 @@ const useGeometry = (ref) => {
|
|
|
1326
1326
|
});
|
|
1327
1327
|
const resizeObserver = react.useRef(null);
|
|
1328
1328
|
react.useEffect(() => {
|
|
1329
|
-
if (!ref.current) {
|
|
1329
|
+
if (skip || !ref.current) {
|
|
1330
1330
|
return;
|
|
1331
1331
|
}
|
|
1332
1332
|
const observe = () => {
|
|
@@ -1357,7 +1357,7 @@ const useGeometry = (ref) => {
|
|
|
1357
1357
|
resizeObserver.current.disconnect();
|
|
1358
1358
|
}
|
|
1359
1359
|
};
|
|
1360
|
-
}, [ref]);
|
|
1360
|
+
}, [ref, skip]);
|
|
1361
1361
|
return geometry;
|
|
1362
1362
|
};
|
|
1363
1363
|
|
|
@@ -1486,17 +1486,21 @@ const useModifierKey = ({ exclude = [] } = {}) => {
|
|
|
1486
1486
|
/**
|
|
1487
1487
|
* Custom hook to handle window resize events and provide the current window size.
|
|
1488
1488
|
*
|
|
1489
|
+
* @param {UseResizeOptions} options - Options for the hook.
|
|
1489
1490
|
* @returns {WindowSize} An object containing the current window height and width.
|
|
1490
1491
|
*/
|
|
1491
|
-
const useResize = () => {
|
|
1492
|
+
const useResize = ({ skip = false } = {}) => {
|
|
1492
1493
|
const [size, setSize] = react.useState(getWindowSize());
|
|
1493
1494
|
react.useEffect(() => {
|
|
1495
|
+
if (skip) {
|
|
1496
|
+
return;
|
|
1497
|
+
}
|
|
1494
1498
|
const handleResize = () => {
|
|
1495
1499
|
setSize(getWindowSize());
|
|
1496
1500
|
};
|
|
1497
1501
|
window.addEventListener("resize", handleResize, false);
|
|
1498
1502
|
return () => window.removeEventListener("resize", handleResize);
|
|
1499
|
-
}, [setSize]);
|
|
1503
|
+
}, [setSize, skip]);
|
|
1500
1504
|
return size;
|
|
1501
1505
|
};
|
|
1502
1506
|
/**
|
|
@@ -1521,36 +1525,73 @@ const getWindowSize = () => {
|
|
|
1521
1525
|
|
|
1522
1526
|
const SCROLL_DEBOUNCE_TIME = 50;
|
|
1523
1527
|
/**
|
|
1524
|
-
* Hook for
|
|
1528
|
+
* Hook for detecting scroll values in horizontal or vertical direction.
|
|
1525
1529
|
*
|
|
1526
1530
|
* @param {useRef} elementRef - Ref hook holding the element that needs to be observed during scrolling
|
|
1527
|
-
* @
|
|
1531
|
+
* @param {object} options - Options object containing direction and onScrollStateChange callback
|
|
1532
|
+
* @returns {object} An object containing if the element is scrollable, is at the beginning, is at the end, and its current scroll position.
|
|
1528
1533
|
*/
|
|
1529
|
-
const useScrollDetection = (elementRef) => {
|
|
1534
|
+
const useScrollDetection = (elementRef, options) => {
|
|
1535
|
+
const { direction = "vertical", onScrollStateChange } = options ?? {};
|
|
1530
1536
|
const [isScrollable, setIsScrollable] = react.useState(false);
|
|
1531
|
-
const [
|
|
1532
|
-
const [
|
|
1533
|
-
const [scrollPosition, setScrollPosition] = react.useState({
|
|
1537
|
+
const [isAtBeginning, setIsAtBeginning] = react.useState(true);
|
|
1538
|
+
const [isAtEnd, setIsAtEnd] = react.useState(false);
|
|
1539
|
+
const [scrollPosition, setScrollPosition] = react.useState({ start: 0, end: 0 });
|
|
1534
1540
|
const observerRef = react.useRef(null);
|
|
1541
|
+
const isFirstRender = useIsFirstRender();
|
|
1535
1542
|
const checkScrollable = react.useCallback(() => {
|
|
1536
1543
|
const element = elementRef?.current;
|
|
1537
1544
|
if (!element) {
|
|
1538
1545
|
return;
|
|
1539
1546
|
}
|
|
1540
|
-
const hasOverflow =
|
|
1541
|
-
|
|
1542
|
-
|
|
1547
|
+
const hasOverflow = direction === "horizontal"
|
|
1548
|
+
? element.scrollWidth > element.clientWidth
|
|
1549
|
+
: element.scrollHeight > element.clientHeight;
|
|
1550
|
+
setIsScrollable(prev => {
|
|
1551
|
+
if (prev !== hasOverflow) {
|
|
1552
|
+
// State will be updated, so we'll notify in the next effect
|
|
1553
|
+
return hasOverflow;
|
|
1554
|
+
}
|
|
1555
|
+
return prev;
|
|
1556
|
+
});
|
|
1557
|
+
}, [elementRef, direction]);
|
|
1543
1558
|
const checkScrollPosition = react.useCallback(() => {
|
|
1544
1559
|
const element = elementRef?.current;
|
|
1545
1560
|
if (!element) {
|
|
1546
1561
|
return;
|
|
1547
1562
|
}
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1563
|
+
if (direction === "horizontal") {
|
|
1564
|
+
const { scrollLeft, scrollWidth, clientWidth } = element;
|
|
1565
|
+
const newIsAtBeginning = scrollLeft === 0;
|
|
1566
|
+
const newIsAtEnd = Math.abs(scrollWidth - scrollLeft - clientWidth) <= 1;
|
|
1567
|
+
const newScrollPosition = { start: scrollLeft, end: clientWidth - scrollLeft };
|
|
1568
|
+
setIsAtBeginning(newIsAtBeginning);
|
|
1569
|
+
setIsAtEnd(newIsAtEnd);
|
|
1570
|
+
setScrollPosition(newScrollPosition);
|
|
1571
|
+
}
|
|
1572
|
+
else {
|
|
1573
|
+
const { scrollTop, scrollHeight, clientHeight } = element;
|
|
1574
|
+
const newIsAtBeginning = scrollTop === 0;
|
|
1575
|
+
const newIsAtEnd = Math.abs(scrollHeight - scrollTop - clientHeight) <= 1;
|
|
1576
|
+
const newScrollPosition = { start: scrollTop, end: clientHeight - scrollTop };
|
|
1577
|
+
setIsAtBeginning(newIsAtBeginning);
|
|
1578
|
+
setIsAtEnd(newIsAtEnd);
|
|
1579
|
+
setScrollPosition(newScrollPosition);
|
|
1580
|
+
}
|
|
1581
|
+
}, [elementRef, direction]);
|
|
1553
1582
|
const debouncedCheckScrollPosition = usehooksTs.useDebounceCallback(checkScrollPosition, SCROLL_DEBOUNCE_TIME);
|
|
1583
|
+
// Notify about state changes whenever any state value changes
|
|
1584
|
+
react.useEffect(() => {
|
|
1585
|
+
if (isFirstRender) {
|
|
1586
|
+
return;
|
|
1587
|
+
}
|
|
1588
|
+
onScrollStateChange?.({
|
|
1589
|
+
isScrollable,
|
|
1590
|
+
isAtBeginning,
|
|
1591
|
+
isAtEnd,
|
|
1592
|
+
scrollPosition,
|
|
1593
|
+
});
|
|
1594
|
+
}, [isScrollable, isAtBeginning, isAtEnd, scrollPosition, onScrollStateChange, isFirstRender]);
|
|
1554
1595
|
react.useEffect(() => {
|
|
1555
1596
|
const element = elementRef?.current;
|
|
1556
1597
|
if (!element) {
|
|
@@ -1572,7 +1613,7 @@ const useScrollDetection = (elementRef) => {
|
|
|
1572
1613
|
element.removeEventListener("scroll", debouncedCheckScrollPosition);
|
|
1573
1614
|
};
|
|
1574
1615
|
}, [elementRef, checkScrollable, checkScrollPosition, debouncedCheckScrollPosition]);
|
|
1575
|
-
return { isScrollable,
|
|
1616
|
+
return react.useMemo(() => ({ isScrollable, isAtBeginning, isAtEnd, scrollPosition }), [isScrollable, isAtBeginning, isAtEnd, scrollPosition]);
|
|
1576
1617
|
};
|
|
1577
1618
|
|
|
1578
1619
|
/**
|
|
@@ -1588,6 +1629,36 @@ const useSelfUpdatingRef = (initialState) => {
|
|
|
1588
1629
|
return stateRef;
|
|
1589
1630
|
};
|
|
1590
1631
|
|
|
1632
|
+
/**
|
|
1633
|
+
* The useStable hook ensures that a value is computed only once and remains stable across renders.
|
|
1634
|
+
* This is useful for expensive computations or when you need a value to never change after initial creation.
|
|
1635
|
+
* Perfect for generating stable random values, creating stable object references, or any one-time computation.
|
|
1636
|
+
*
|
|
1637
|
+
* @example
|
|
1638
|
+
* // Stable random ID that never changes
|
|
1639
|
+
* const stableId = useStable(() => Math.random().toString(36));
|
|
1640
|
+
* @example
|
|
1641
|
+
* // Stable configuration object
|
|
1642
|
+
* const config = useStable(() => ({
|
|
1643
|
+
* apiKey: generateApiKey(),
|
|
1644
|
+
* timestamp: Date.now()
|
|
1645
|
+
* }));
|
|
1646
|
+
* @example
|
|
1647
|
+
* // Stable random widths for skeleton loading
|
|
1648
|
+
* const lineWidths = useStable(() => ({
|
|
1649
|
+
* title: Math.floor(Math.random() * 100) + 120,
|
|
1650
|
+
* description: Math.floor(Math.random() * 80) + 80
|
|
1651
|
+
* }));
|
|
1652
|
+
*/
|
|
1653
|
+
const useStable = (factory) => {
|
|
1654
|
+
const ref = react.useRef(null);
|
|
1655
|
+
// Compute value only once and store it
|
|
1656
|
+
if (ref.current === null) {
|
|
1657
|
+
ref.current = { value: factory() };
|
|
1658
|
+
}
|
|
1659
|
+
return ref.current.value;
|
|
1660
|
+
};
|
|
1661
|
+
|
|
1591
1662
|
/**
|
|
1592
1663
|
* A custom React hook that provides real-time information about the current viewport size.
|
|
1593
1664
|
* ! Consider using `useContainerBreakpoints` instead, and only use this when you need to actually react to the viewport size, not the container size.
|
|
@@ -1757,11 +1828,9 @@ const useBreadcrumbItemsToRender = (breadcrumbItems) => {
|
|
|
1757
1828
|
const breadCrumbItemsToJSX = breadcrumbItems.map((item, index, array) => {
|
|
1758
1829
|
const isLast = index === array.length - 1;
|
|
1759
1830
|
if (!isLast) {
|
|
1760
|
-
return (jsxRuntime.jsxs(
|
|
1761
|
-
}
|
|
1762
|
-
else {
|
|
1763
|
-
return (jsxRuntime.jsx(Text, { className: "text-nowrap", size: "small", children: item.label }));
|
|
1831
|
+
return (jsxRuntime.jsxs("div", { children: [jsxRuntime.jsx(Button, { asChild: true, size: "small", variant: "ghost-neutral", children: jsxRuntime.jsx(reactRouter.Link, { to: item.to, children: item.label }) }), jsxRuntime.jsx(Icon, { className: "text-secondary-300", name: "Slash", size: "small" })] }, index));
|
|
1764
1832
|
}
|
|
1833
|
+
return (jsxRuntime.jsx(Text, { className: "text-nowrap", size: "small", children: item.label }, index));
|
|
1765
1834
|
});
|
|
1766
1835
|
return breadCrumbItemsToJSX;
|
|
1767
1836
|
};
|
|
@@ -2185,42 +2254,63 @@ const DetailsList = ({ details, className, density = "default" }) => {
|
|
|
2185
2254
|
return (jsxRuntime.jsx("div", { className: cvaDetailsList({ className, density }), children: details.map((value, index, array) => (jsxRuntime.jsxs(react.Fragment, { children: [jsxRuntime.jsx("span", { className: cvaDetailsListItem({ className }), children: value }), index < array.length - 1 && (jsxRuntime.jsx("div", { className: "mx-0.5 flex items-center", children: jsxRuntime.jsx(Icon, { className: "w-4 text-neutral-300", color: "neutral", name: "Slash", size: "small" }) }))] }, index))) }));
|
|
2186
2255
|
};
|
|
2187
2256
|
|
|
2257
|
+
/**
|
|
2258
|
+
* Generates a random width percentage string for skeleton loading components.
|
|
2259
|
+
*
|
|
2260
|
+
* @param {object} params - The parameter object
|
|
2261
|
+
* @param {number} params.min - Minimum percentage value (e.g., 30 for 30%)
|
|
2262
|
+
* @param {number} params.max - Maximum percentage value (e.g., 80 for 80%)
|
|
2263
|
+
* @returns {string} A percentage string (e.g., "65%")
|
|
2264
|
+
*/
|
|
2265
|
+
const getResponsiveRandomWidthPercentage = ({ min, max }) => {
|
|
2266
|
+
const randomWidth = Math.floor(Math.random() * (max - min + 1)) + min;
|
|
2267
|
+
return `${randomWidth}%`;
|
|
2268
|
+
};
|
|
2269
|
+
|
|
2270
|
+
const cvaSkeletonContainer = cssClassVarianceUtilities.cvaMerge(["flex", "flex-col"]);
|
|
2188
2271
|
const cvaSkeletonLine = cssClassVarianceUtilities.cvaMerge([
|
|
2189
|
-
"
|
|
2190
|
-
"
|
|
2191
|
-
"
|
|
2192
|
-
|
|
2193
|
-
"bg-black/50",
|
|
2194
|
-
"bg-auto",
|
|
2272
|
+
"relative",
|
|
2273
|
+
"overflow-hidden",
|
|
2274
|
+
"rounded-lg",
|
|
2275
|
+
// Gradient background
|
|
2195
2276
|
"bg-gradient-to-r",
|
|
2196
|
-
"from-
|
|
2197
|
-
"via-
|
|
2198
|
-
"to-
|
|
2277
|
+
"from-gray-200/80",
|
|
2278
|
+
"via-gray-300/60",
|
|
2279
|
+
"to-gray-200/80",
|
|
2280
|
+
// Pulse animation
|
|
2281
|
+
"animate-pulse",
|
|
2282
|
+
// Shimmer overlay
|
|
2283
|
+
"before:absolute",
|
|
2284
|
+
"before:inset-0",
|
|
2285
|
+
"before:bg-gradient-to-r",
|
|
2286
|
+
"before:from-transparent",
|
|
2287
|
+
"before:via-white/50",
|
|
2288
|
+
"before:to-transparent",
|
|
2289
|
+
"before:opacity-0",
|
|
2290
|
+
"before:animate-pulse",
|
|
2291
|
+
// Smooth transitions for accessibility
|
|
2292
|
+
"transition-all",
|
|
2293
|
+
"duration-300",
|
|
2294
|
+
"ease-in-out",
|
|
2199
2295
|
]);
|
|
2200
2296
|
|
|
2201
2297
|
/**
|
|
2202
2298
|
* Display placeholder lines before the data gets loaded to reduce load-time frustration.
|
|
2203
2299
|
*/
|
|
2204
|
-
const SkeletonLines = react.memo(({
|
|
2205
|
-
const
|
|
2206
|
-
|
|
2207
|
-
|
|
2208
|
-
|
|
2209
|
-
|
|
2210
|
-
return width;
|
|
2211
|
-
};
|
|
2212
|
-
const getHeight = (index) => {
|
|
2213
|
-
if (Array.isArray(height)) {
|
|
2214
|
-
return height[index] ?? "auto";
|
|
2215
|
-
}
|
|
2216
|
-
return height;
|
|
2217
|
-
};
|
|
2218
|
-
for (let i = 0; i < lines; i++) {
|
|
2219
|
-
skeletonLines.push(jsxRuntime.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 && jsxRuntime.jsx(jsxRuntime.Fragment, { children: "\u00A0" }) }, i));
|
|
2220
|
-
}
|
|
2221
|
-
return jsxRuntime.jsx(jsxRuntime.Fragment, { children: skeletonLines });
|
|
2300
|
+
const SkeletonLines = react.memo(({ lines = 1, height = "0.75rem", width = "100%", margin = 10, className, dataTestId }) => {
|
|
2301
|
+
const gapStyle = typeof margin === "number" ? `${margin}px` : margin;
|
|
2302
|
+
return (jsxRuntime.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) => (jsxRuntime.jsx("div", { className: cvaSkeletonLine(), "data-testid": dataTestId ? `${dataTestId}-${index}` : `skeleton-lines-${index}`, "data-type": "loading-skeleton-line", style: {
|
|
2303
|
+
width: getDimension(width, index),
|
|
2304
|
+
height: getDimension(height, index),
|
|
2305
|
+
} }, index))) }));
|
|
2222
2306
|
});
|
|
2223
|
-
|
|
2307
|
+
const getDimension = (dimension, index) => {
|
|
2308
|
+
if (Array.isArray(dimension)) {
|
|
2309
|
+
const value = dimension[index] ?? dimension[0] ?? "100%";
|
|
2310
|
+
return typeof value === "number" ? `${value}px` : value;
|
|
2311
|
+
}
|
|
2312
|
+
return typeof dimension === "number" ? `${dimension}px` : dimension;
|
|
2313
|
+
};
|
|
2224
2314
|
|
|
2225
2315
|
const cvaContainerStyles = cssClassVarianceUtilities.cvaMerge([
|
|
2226
2316
|
"flex",
|
|
@@ -2392,6 +2482,119 @@ const Highlight = ({ className, dataTestId, children, size = "small", color = "w
|
|
|
2392
2482
|
};
|
|
2393
2483
|
Highlight.displayName = "Highlight";
|
|
2394
2484
|
|
|
2485
|
+
const cvaZStackContainer = cssClassVarianceUtilities.cvaMerge(["grid", "grid-cols-1", "grid-rows-1"]);
|
|
2486
|
+
const cvaZStackItem = cssClassVarianceUtilities.cvaMerge(["col-start-1", "col-end-1", "row-start-1", "row-end-2"]);
|
|
2487
|
+
|
|
2488
|
+
/**
|
|
2489
|
+
* ZStack is a component that stacks its children on the z-axis.
|
|
2490
|
+
* Is a good alternative to "position: absolute" that avoids some of the unfortunate side effects of absolute positioning.
|
|
2491
|
+
*
|
|
2492
|
+
* @param { ZStackProps} props - The props for the ZStack component
|
|
2493
|
+
* @returns {Element} ZStack component
|
|
2494
|
+
*/
|
|
2495
|
+
const ZStack = ({ children, className, dataTestId }) => {
|
|
2496
|
+
return (jsxRuntime.jsx("div", { className: cvaZStackContainer({ className }), "data-testid": dataTestId, children: react.Children.map(children, (child, index) => {
|
|
2497
|
+
if (!react.isValidElement(child)) {
|
|
2498
|
+
return child;
|
|
2499
|
+
}
|
|
2500
|
+
return react.cloneElement(child, {
|
|
2501
|
+
className: cvaZStackItem({ className: child.props.className }),
|
|
2502
|
+
key: index,
|
|
2503
|
+
});
|
|
2504
|
+
}) }));
|
|
2505
|
+
};
|
|
2506
|
+
|
|
2507
|
+
const cvaHorizontalOverflowScroller = cssClassVarianceUtilities.cvaMerge([
|
|
2508
|
+
"flex",
|
|
2509
|
+
"flex-nowrap",
|
|
2510
|
+
"gap-1",
|
|
2511
|
+
"overflow-y-hidden",
|
|
2512
|
+
"overflow-x-scroll",
|
|
2513
|
+
"w-full",
|
|
2514
|
+
"no-scrollbar",
|
|
2515
|
+
]);
|
|
2516
|
+
const cvaHorizontalOverflowScrollerAndIndicatorsContainer = cssClassVarianceUtilities.cvaMerge(["group", "w-full", "overflow-clip"]);
|
|
2517
|
+
|
|
2518
|
+
const cvaOverflowIndicatorContainer = cssClassVarianceUtilities.cvaMerge(["pointer-events-none", "h-full", "w-full", "isolate"]);
|
|
2519
|
+
const cvaOverflowIndicatorGradient = cssClassVarianceUtilities.cvaMerge(["pointer-events-none", "h-full", "w-8"], {
|
|
2520
|
+
variants: {
|
|
2521
|
+
direction: {
|
|
2522
|
+
left: ["bg-gradient-to-r", "from-white", "to-transparent"],
|
|
2523
|
+
right: ["bg-gradient-to-l", "from-white", "to-transparent"],
|
|
2524
|
+
},
|
|
2525
|
+
},
|
|
2526
|
+
});
|
|
2527
|
+
const cvaJustificationContainer = cssClassVarianceUtilities.cvaMerge(["flex", "w-full", "items-center", "pointer-events-none"], {
|
|
2528
|
+
variants: {
|
|
2529
|
+
direction: {
|
|
2530
|
+
left: ["justify-start"],
|
|
2531
|
+
right: ["justify-end"],
|
|
2532
|
+
},
|
|
2533
|
+
},
|
|
2534
|
+
});
|
|
2535
|
+
const cvaOverflowIndicatorButton = cssClassVarianceUtilities.cvaMerge([
|
|
2536
|
+
"shadow-md",
|
|
2537
|
+
"opacity-0",
|
|
2538
|
+
"transition-opacity",
|
|
2539
|
+
"duration-200",
|
|
2540
|
+
"pointer-events-auto",
|
|
2541
|
+
"starting:opacity-0",
|
|
2542
|
+
"group-hover:opacity-100",
|
|
2543
|
+
]);
|
|
2544
|
+
|
|
2545
|
+
/**
|
|
2546
|
+
* Overflow indicator component that shows visual cues when content extends beyond visible area
|
|
2547
|
+
* Shows a scroll button on hover to navigate in the specified direction
|
|
2548
|
+
*
|
|
2549
|
+
* @param {OverflowIndicatorProps} props - The props for the component
|
|
2550
|
+
* @returns {ReactElement} OverflowIndicator component
|
|
2551
|
+
*/
|
|
2552
|
+
const OverflowIndicator = ({ className, dataTestId, direction, onClickScroll, }) => {
|
|
2553
|
+
const iconName = direction === "left" ? "ChevronLeft" : "ChevronRight";
|
|
2554
|
+
return (jsxRuntime.jsxs(ZStack, { className: cvaOverflowIndicatorContainer({ className }), dataTestId: dataTestId, children: [jsxRuntime.jsx("div", { className: cvaJustificationContainer({ direction }), children: jsxRuntime.jsx("div", { className: cvaOverflowIndicatorGradient({ direction }), "data-testid": dataTestId ? `${dataTestId}-gradient` : undefined }) }), jsxRuntime.jsx("div", { className: cvaJustificationContainer({ direction }), children: jsxRuntime.jsx(IconButton, { circular: true, className: cvaOverflowIndicatorButton(), "data-testid": dataTestId ? `${dataTestId}-button` : undefined, icon: jsxRuntime.jsx(Icon, { name: iconName, size: "small" }), onClick: onClickScroll, size: "small", variant: "secondary" }) })] }));
|
|
2555
|
+
};
|
|
2556
|
+
|
|
2557
|
+
/**
|
|
2558
|
+
* Container for displaying components in a horizontal layout with overflow detection
|
|
2559
|
+
*
|
|
2560
|
+
* @param {HorizontalOverflowScrollerProps} props - The props for the component
|
|
2561
|
+
* @returns {Element} HorizontalOverflowScroller component
|
|
2562
|
+
*/
|
|
2563
|
+
const HorizontalOverflowScroller = ({ className, dataTestId, children, onScrollStateChange, }) => {
|
|
2564
|
+
const containerRef = react.useRef(null);
|
|
2565
|
+
const childrenArray = react.Children.toArray(children);
|
|
2566
|
+
const { width: containerWidth } = useGeometry(containerRef);
|
|
2567
|
+
const { isScrollable, isAtBeginning, isAtEnd } = useScrollDetection(containerRef, {
|
|
2568
|
+
direction: "horizontal",
|
|
2569
|
+
onScrollStateChange: onScrollStateChange
|
|
2570
|
+
? (state) => onScrollStateChange({
|
|
2571
|
+
isScrollable: state.isScrollable,
|
|
2572
|
+
isAtBeginning: state.isAtBeginning,
|
|
2573
|
+
isAtEnd: state.isAtEnd,
|
|
2574
|
+
})
|
|
2575
|
+
: undefined,
|
|
2576
|
+
});
|
|
2577
|
+
const handleScrollLeft = () => {
|
|
2578
|
+
const element = containerRef.current;
|
|
2579
|
+
if (!element || !containerWidth)
|
|
2580
|
+
return;
|
|
2581
|
+
element.scrollBy({
|
|
2582
|
+
left: -containerWidth,
|
|
2583
|
+
behavior: "smooth",
|
|
2584
|
+
});
|
|
2585
|
+
};
|
|
2586
|
+
const handleScrollRight = () => {
|
|
2587
|
+
const element = containerRef.current;
|
|
2588
|
+
if (!element || !containerWidth)
|
|
2589
|
+
return;
|
|
2590
|
+
element.scrollBy({
|
|
2591
|
+
left: containerWidth,
|
|
2592
|
+
behavior: "smooth",
|
|
2593
|
+
});
|
|
2594
|
+
};
|
|
2595
|
+
return (jsxRuntime.jsxs(ZStack, { className: cvaHorizontalOverflowScrollerAndIndicatorsContainer({ className }), children: [jsxRuntime.jsx("div", { className: cvaHorizontalOverflowScroller({ className }), "data-testid": dataTestId, ref: containerRef, children: childrenArray.map((child, index) => (jsxRuntime.jsx(react.Fragment, { children: child }, index))) }), isScrollable && !isAtBeginning ? (jsxRuntime.jsx(OverflowIndicator, { dataTestId: `${dataTestId}-left-indicator`, direction: "left", onClickScroll: handleScrollLeft })) : null, isScrollable && !isAtEnd ? (jsxRuntime.jsx(OverflowIndicator, { dataTestId: `${dataTestId}-right-indicator`, direction: "right", onClickScroll: handleScrollRight })) : null] }));
|
|
2596
|
+
};
|
|
2597
|
+
|
|
2395
2598
|
const PADDING = 12;
|
|
2396
2599
|
/**
|
|
2397
2600
|
* Converts a width size value into a CSS dimension value for max constraints
|
|
@@ -2776,7 +2979,7 @@ const FloatingArrowContainer = ({ arrowRef, mode = "dark", }) => {
|
|
|
2776
2979
|
* @param {TooltipProps} props - The props for the Tooltip component
|
|
2777
2980
|
* @returns {ReactElement} Tooltip component
|
|
2778
2981
|
*/
|
|
2779
|
-
const Tooltip = ({ children, dataTestId, disabled = false, className, label, placement = "auto", mode = "dark", iconProps, id, }) => {
|
|
2982
|
+
const Tooltip = ({ children, dataTestId, disabled = false, className, label, placement = "auto", mode = "dark", iconProps, id, style, }) => {
|
|
2780
2983
|
const [isOpen, setIsOpen] = react.useState(false);
|
|
2781
2984
|
const arrowRef = react.useRef(null);
|
|
2782
2985
|
const { refs, floatingStyles, context } = react$1.useFloating({
|
|
@@ -2809,7 +3012,7 @@ const Tooltip = ({ children, dataTestId, disabled = false, className, label, pla
|
|
|
2809
3012
|
}
|
|
2810
3013
|
setIsOpen(false);
|
|
2811
3014
|
}, [disabled]);
|
|
2812
|
-
return (jsxRuntime.jsxs(Popover, { activation: { hover: true }, className: cvaTooltipPopover(), dataTestId: dataTestId, id: id, placement: placement === "auto" ? "bottom" : placement, children: [jsxRuntime.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 ? (jsxRuntime.jsx("div", { children: jsxRuntime.jsx(Icon, { dataTestId: dataTestId ? `${dataTestId}-icon` : undefined, name: "QuestionMarkCircle", size: "small", ...iconProps }) })) : (wrappedChildren) }), isMounted ? (jsxRuntime.jsx("div", { ref: refs.setFloating, style: floatingStyles, children: jsxRuntime.jsx(PopoverContent, { children: jsxRuntime.jsxs("div", { "aria-label": typeof label === "string" ? label : undefined, className: cvaTooltipPopoverContent({ color: mode }), "data-testid": `${dataTestId}-content`, children: [jsxRuntime.jsx(Text, { dataTestId: `${dataTestId}-text`, inverted: mode === "dark", size: "small", type: typeof label === "string" ? "p" : "span", children: label }), placement !== "auto" && jsxRuntime.jsx(FloatingArrowContainer, { arrowRef: arrowRef, mode: mode })] }) }) })) : null] }));
|
|
3015
|
+
return (jsxRuntime.jsxs(Popover, { activation: { hover: true }, className: cvaTooltipPopover(), dataTestId: dataTestId, id: id, placement: placement === "auto" ? "bottom" : placement, children: [jsxRuntime.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 ? (jsxRuntime.jsx("div", { children: jsxRuntime.jsx(Icon, { dataTestId: dataTestId ? `${dataTestId}-icon` : undefined, name: "QuestionMarkCircle", size: "small", ...iconProps }) })) : (wrappedChildren) }), isMounted ? (jsxRuntime.jsx("div", { ref: refs.setFloating, style: floatingStyles, children: jsxRuntime.jsx(PopoverContent, { children: jsxRuntime.jsxs("div", { "aria-label": typeof label === "string" ? label : undefined, className: cvaTooltipPopoverContent({ color: mode }), "data-testid": `${dataTestId}-content`, children: [jsxRuntime.jsx(Text, { dataTestId: `${dataTestId}-text`, inverted: mode === "dark", size: "small", type: typeof label === "string" ? "p" : "span", children: label }), placement !== "auto" && jsxRuntime.jsx(FloatingArrowContainer, { arrowRef: arrowRef, mode: mode })] }) }) })) : null] }));
|
|
2813
3016
|
};
|
|
2814
3017
|
|
|
2815
3018
|
const cvaIndicator = cssClassVarianceUtilities.cvaMerge(["flex", "items-center"]);
|
|
@@ -2946,12 +3149,12 @@ const Indicator = ({ dataTestId, icon, label, color = "unknown", withBackground
|
|
|
2946
3149
|
return (jsxRuntime.jsx(Tooltip, { className: className, disabled: withLabel, label: label, placement: "bottom", children: jsxRuntime.jsxs("div", { "aria-label": label, className: cvaIndicator(), "data-testid": dataTestId, ...rest, children: [jsxRuntime.jsxs("div", { className: cvaIndicatorIconBackground({ color, background: withBackground ? "visible" : "hidden" }), "data-testid": dataTestId ? `${dataTestId}-background` : "indicator-background", children: [ping ? (jsxRuntime.jsx("div", { className: cvaIndicatorPing({ color }), "data-testid": dataTestId ? `${dataTestId}-ping` : "indicator-ping" })) : null, icon] }), label && withLabel ? (jsxRuntime.jsx("div", { className: cvaIndicatorLabel({ size, weight, background: withBackground ? "visible" : "hidden" }), "data-testid": dataTestId ? `${dataTestId}-label` : undefined, children: label })) : null] }) }));
|
|
2947
3150
|
};
|
|
2948
3151
|
|
|
2949
|
-
const cvaKPI = cssClassVarianceUtilities.cvaMerge(["w-full", "
|
|
3152
|
+
const cvaKPI = cssClassVarianceUtilities.cvaMerge(["w-full", "flex", "flex-col"], {
|
|
2950
3153
|
variants: {
|
|
2951
3154
|
variant: {
|
|
2952
|
-
small: ["px-3"],
|
|
3155
|
+
small: ["px-3", "py-2"],
|
|
2953
3156
|
condensed: ["px-2", "py-0"],
|
|
2954
|
-
default: [""],
|
|
3157
|
+
default: ["px-4", "py-2"],
|
|
2955
3158
|
},
|
|
2956
3159
|
},
|
|
2957
3160
|
defaultVariants: {
|
|
@@ -3003,9 +3206,9 @@ const LoadingContent$1 = () => (jsxRuntime.jsx("div", { className: "flex h-11 fl
|
|
|
3003
3206
|
* @param {KPIProps} props - The props for the KPI component
|
|
3004
3207
|
* @returns {ReactElement} KPI component
|
|
3005
3208
|
*/
|
|
3006
|
-
const KPI = ({ title, value, loading = false, unit, className, dataTestId, tooltipLabel, variant = "default", trend, ...rest }) => {
|
|
3209
|
+
const KPI = ({ title, value, loading = false, unit, className, dataTestId, tooltipLabel, variant = "default", trend, style, ...rest }) => {
|
|
3007
3210
|
const isSmallVariant = variant === "small";
|
|
3008
|
-
return (jsxRuntime.jsx(Tooltip, { dataTestId: dataTestId ? `${dataTestId}-tooltip` : undefined, disabled: tooltipLabel === undefined || tooltipLabel === "", label: tooltipLabel, placement: "bottom", children: jsxRuntime.jsx("div", { className: cvaKPI({ variant, className }), "data-testid": dataTestId ? `${dataTestId}` : undefined, ...rest, children: loading ? (jsxRuntime.jsx(LoadingContent$1, {})) : (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [jsxRuntime.jsx("div", { className: cvaKPIHeader(), children: jsxRuntime.jsx(Text, { className: cvaKPITitleText(), dataTestId: dataTestId ? `${dataTestId}-title` : undefined, size: isSmallVariant ? "small" : "medium", subtle: true, weight: isSmallVariant ? "normal" : "bold", children: title }) }), jsxRuntime.jsx(Text, { className: cvaKPIvalueText({ variant }), dataTestId: dataTestId ? `${dataTestId}-value` : undefined, size: isSmallVariant ? "small" : "large", type: "div", weight: isSmallVariant ? "bold" : "thick", children: jsxRuntime.jsxs("div", { className: cvaKPIValueContainer({
|
|
3211
|
+
return (jsxRuntime.jsx(Tooltip, { dataTestId: dataTestId ? `${dataTestId}-tooltip` : undefined, disabled: tooltipLabel === undefined || tooltipLabel === "", label: tooltipLabel, placement: "bottom", style: style, children: jsxRuntime.jsx("div", { className: cvaKPI({ variant, className }), "data-testid": dataTestId ? `${dataTestId}` : undefined, ...rest, children: loading ? (jsxRuntime.jsx(LoadingContent$1, {})) : (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [jsxRuntime.jsx("div", { className: cvaKPIHeader(), children: jsxRuntime.jsx(Text, { className: cvaKPITitleText(), dataTestId: dataTestId ? `${dataTestId}-title` : undefined, size: isSmallVariant ? "small" : "medium", subtle: true, weight: isSmallVariant ? "normal" : "bold", children: title }) }), jsxRuntime.jsx(Text, { className: cvaKPIvalueText({ variant }), dataTestId: dataTestId ? `${dataTestId}-value` : undefined, size: isSmallVariant ? "small" : "large", type: "div", weight: isSmallVariant ? "bold" : "thick", children: jsxRuntime.jsxs("div", { className: cvaKPIValueContainer({
|
|
3009
3212
|
isDefaultAndHasTrendValue: Boolean(trend !== undefined && trend.value !== undefined && !isSmallVariant),
|
|
3010
3213
|
className,
|
|
3011
3214
|
}), children: [jsxRuntime.jsxs("span", { className: cvaKPIvalueText({ variant }), children: [value, " ", unit] }), jsxRuntime.jsx(TrendIndicator, { isSmallVariant: isSmallVariant, trend: trend, unit: unit })] }) })] })) }) }));
|
|
@@ -3077,25 +3280,66 @@ const cvaCardBodyDensityContainer = cssClassVarianceUtilities.cvaMerge(["grid",
|
|
|
3077
3280
|
},
|
|
3078
3281
|
});
|
|
3079
3282
|
|
|
3080
|
-
const
|
|
3283
|
+
const cvaListItem$1 = cssClassVarianceUtilities.cvaMerge(["py-3", "px-4", "min-h-14", "w-full", "flex", "justify-between", "items-center"]);
|
|
3284
|
+
const cvaMainInformationClass = cssClassVarianceUtilities.cvaMerge(["grid", "items-center", "text-sm", "gap-2"], {
|
|
3285
|
+
variants: {
|
|
3286
|
+
hasThumbnail: {
|
|
3287
|
+
true: "grid-cols-min-fr",
|
|
3288
|
+
false: "grid-cols-1",
|
|
3289
|
+
},
|
|
3290
|
+
},
|
|
3291
|
+
});
|
|
3292
|
+
const cvaThumbnailContainer = cssClassVarianceUtilities.cvaMerge([
|
|
3293
|
+
"flex",
|
|
3294
|
+
"h-8",
|
|
3295
|
+
"w-8",
|
|
3296
|
+
"items-center",
|
|
3297
|
+
"justify-center",
|
|
3298
|
+
"overflow-hidden",
|
|
3299
|
+
"rounded-md",
|
|
3300
|
+
]);
|
|
3301
|
+
|
|
3302
|
+
const DEFAULT_SKELETON_LIST_ITEM_PROPS = {
|
|
3303
|
+
hasThumbnail: true,
|
|
3304
|
+
thumbnailShape: "circle",
|
|
3305
|
+
hasDescription: true,
|
|
3306
|
+
hasMeta: false,
|
|
3307
|
+
hasDetails: false,
|
|
3308
|
+
};
|
|
3309
|
+
/**
|
|
3310
|
+
* Skeleton loading indicator that mimics the ListItem component structure.
|
|
3311
|
+
* Uses the same layout, spacing, and visual hierarchy as ListItem.
|
|
3312
|
+
*/
|
|
3313
|
+
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, }) => {
|
|
3314
|
+
// Generate stable random widths once and never change them
|
|
3315
|
+
const lineWidths = useStable(() => {
|
|
3316
|
+
return {
|
|
3317
|
+
title: getResponsiveRandomWidthPercentage({ min: 60, max: 85 }),
|
|
3318
|
+
description: getResponsiveRandomWidthPercentage({ min: 45, max: 70 }),
|
|
3319
|
+
meta: getResponsiveRandomWidthPercentage({ min: 30, max: 55 }),
|
|
3320
|
+
details: getResponsiveRandomWidthPercentage({ min: 25, max: 45 }),
|
|
3321
|
+
};
|
|
3322
|
+
});
|
|
3323
|
+
return (jsxRuntime.jsxs("div", { className: cvaListItem$1({ className: "w-full" }), children: [jsxRuntime.jsxs("div", { className: cvaMainInformationClass({ hasThumbnail, className: "w-full" }), children: [hasThumbnail ? (jsxRuntime.jsx("div", { className: cvaThumbnailContainer({ className: "bg-gray-200" }), children: jsxRuntime.jsx("div", { className: tailwindMerge.twMerge("bg-gray-300", thumbnailShape === "circle" ? "rounded-full" : "rounded"), style: { width: 20, height: 20 } }) })) : null, jsxRuntime.jsxs("div", { className: "grid-rows-min-fr grid w-full items-center gap-1 text-sm", children: [jsxRuntime.jsx(SkeletonLines, { height: "0.875em", lines: 1, width: lineWidths.title }), hasDescription ? jsxRuntime.jsx(SkeletonLines, { height: "0.75em", lines: 1, width: lineWidths.description }) : null, hasMeta ? jsxRuntime.jsx(SkeletonLines, { height: "0.75em", lines: 1, width: lineWidths.meta }) : null] })] }), hasDetails ? (jsxRuntime.jsx("div", { className: "pl-2 text-sm", children: jsxRuntime.jsx(SkeletonLines, { height: "0.875em", lines: 1, width: lineWidths.details }) })) : null] }));
|
|
3324
|
+
};
|
|
3325
|
+
|
|
3326
|
+
const cvaListContainer = cssClassVarianceUtilities.cvaMerge(["overflow-auto", "h-full"], {
|
|
3081
3327
|
variants: {
|
|
3082
|
-
|
|
3083
|
-
true: [""],
|
|
3084
|
-
false: ["
|
|
3328
|
+
withTopSeparator: {
|
|
3329
|
+
true: ["border-t", "border-neutral-200", "transition-colors duration-200 ease-in"],
|
|
3330
|
+
false: ["border-t", "border-transparent", "transition-colors duration-200 ease-in"],
|
|
3085
3331
|
},
|
|
3086
3332
|
},
|
|
3087
3333
|
defaultVariants: {
|
|
3088
|
-
|
|
3334
|
+
withTopSeparator: false,
|
|
3089
3335
|
},
|
|
3090
3336
|
});
|
|
3091
3337
|
const cvaList = cssClassVarianceUtilities.cvaMerge(["relative"]);
|
|
3092
|
-
const cvaListItem
|
|
3338
|
+
const cvaListItem = cssClassVarianceUtilities.cvaMerge(["absolute", "top-0", "left-0", "w-full"], {
|
|
3093
3339
|
variants: {
|
|
3094
3340
|
separator: {
|
|
3095
|
-
|
|
3096
|
-
line: ["[&:not(:last-child)]:border-b", "border-gray-200"],
|
|
3341
|
+
line: ["[&:not(:last-child)]:border-b", "border-neutral-200"],
|
|
3097
3342
|
none: "",
|
|
3098
|
-
space: "[&:not(:last-child)]:pb-0.5",
|
|
3099
3343
|
},
|
|
3100
3344
|
},
|
|
3101
3345
|
defaultVariants: {
|
|
@@ -3104,62 +3348,129 @@ const cvaListItem$1 = cssClassVarianceUtilities.cvaMerge(["absolute", "top-0", "
|
|
|
3104
3348
|
});
|
|
3105
3349
|
|
|
3106
3350
|
/**
|
|
3107
|
-
* Render a performant virtualized list of items. Optionally with infinite scrolling.
|
|
3108
3351
|
*
|
|
3109
|
-
* @property {number} count - The total number of items in the list.
|
|
3110
|
-
* @property {number} [rowHeight="40"] - The estimated height of each row in the list.
|
|
3111
|
-
* @property {RelayPagination | undefined} pagination - Pagination configuration for the list.
|
|
3112
|
-
* @property {separator} [separator="line"] - The separator style between items in the list.
|
|
3113
|
-
* @property {(index: number) =>ReactElement} children - A function that takes an index and returns the JSX element to be rendered at said index.
|
|
3114
|
-
* @property {loadingIndicator} [loadingIndicator="spinner"] - The type of loading indicator in the list.
|
|
3115
|
-
* @property {skeletonLinesHeight} [skeletonLinesHeight="2rem"] - The height of the skeleton lines.
|
|
3116
3352
|
*/
|
|
3117
|
-
const
|
|
3118
|
-
|
|
3119
|
-
|
|
3120
|
-
|
|
3121
|
-
|
|
3122
|
-
|
|
3123
|
-
|
|
3124
|
-
|
|
3125
|
-
|
|
3353
|
+
const ListLoadingIndicator = ({ type, hasThumbnail, thumbnailShape, hasDescription, component, hasMeta, hasDetails, }) => {
|
|
3354
|
+
switch (type) {
|
|
3355
|
+
case "none":
|
|
3356
|
+
return null;
|
|
3357
|
+
case "spinner":
|
|
3358
|
+
return jsxRuntime.jsx(Spinner, { centering: "horizontally", containerClassName: "p-4" });
|
|
3359
|
+
case "custom":
|
|
3360
|
+
return component;
|
|
3361
|
+
case "skeleton":
|
|
3362
|
+
return (jsxRuntime.jsx(ListItemSkeleton, { hasDescription: hasDescription, hasDetails: hasDetails, hasMeta: hasMeta, hasThumbnail: hasThumbnail, thumbnailShape: thumbnailShape }));
|
|
3363
|
+
default: {
|
|
3364
|
+
throw new Error(`${type} is not known`);
|
|
3126
3365
|
}
|
|
3127
|
-
|
|
3128
|
-
|
|
3129
|
-
|
|
3366
|
+
}
|
|
3367
|
+
};
|
|
3368
|
+
|
|
3369
|
+
const DEFAULT_ROW_HEIGHT = 61; // 61px is the height of the ListItem component as of 2025-09-26
|
|
3370
|
+
/**
|
|
3371
|
+
* A performant virtualized list component with infinite scrolling support.
|
|
3372
|
+
*
|
|
3373
|
+
* ⚠️ **Important**: Requires a container with defined height to work properly.
|
|
3374
|
+
*
|
|
3375
|
+
* Features:
|
|
3376
|
+
* - Virtualized rendering using TanStack Virtual for performance with large datasets
|
|
3377
|
+
* - Automatic infinite scroll loading when approaching the end of the list
|
|
3378
|
+
* - Optional header support (automatically managed, scrolls with content)
|
|
3379
|
+
* - Built-in pagination with relay-style cursor support
|
|
3380
|
+
* - Configurable loading indicators (skeleton, spinner, or custom)
|
|
3381
|
+
* - Scroll state detection and callbacks
|
|
3382
|
+
* - Variable-height item support via `estimateItemSize`
|
|
3383
|
+
*
|
|
3384
|
+
* The component automatically loads more data when:
|
|
3385
|
+
* - User scrolls to the last visible item
|
|
3386
|
+
* - Content height is insufficient to fill the container
|
|
3387
|
+
*/
|
|
3388
|
+
const List = ({ count, pagination, children, className, dataTestId, separator = "line", loadingIndicator = { type: "skeleton", ...DEFAULT_SKELETON_LIST_ITEM_PROPS }, onRowClick, onScrollStateChange, topSeparatorOnScroll = false, estimateItemSize, header, getItem, }) => {
|
|
3389
|
+
const parentRef = react.useRef(null);
|
|
3390
|
+
// Calculate the actual count including header
|
|
3391
|
+
const actualCount = count + (header ? 1 : 0);
|
|
3392
|
+
// Helper function to get item for a given data index
|
|
3393
|
+
const getItemAtIndex = react.useCallback((dataIndex) => {
|
|
3394
|
+
return getItem(dataIndex);
|
|
3395
|
+
}, [getItem]);
|
|
3396
|
+
// Calculate how many loading rows we need
|
|
3397
|
+
const getLoadingRowsCount = react.useCallback(() => {
|
|
3398
|
+
const { type: loadingIndicatorType } = loadingIndicator;
|
|
3399
|
+
if (pagination?.isLoading === false)
|
|
3400
|
+
return 0;
|
|
3401
|
+
switch (loadingIndicatorType) {
|
|
3402
|
+
case "none":
|
|
3403
|
+
return 0;
|
|
3404
|
+
case "spinner":
|
|
3405
|
+
return 1;
|
|
3406
|
+
case "custom":
|
|
3407
|
+
case "skeleton": {
|
|
3408
|
+
const isInitialLoading = !pagination?.pageInfo;
|
|
3409
|
+
const initialCount = loadingIndicator.initialLoadingCount ?? 10;
|
|
3410
|
+
const scrollCount = loadingIndicator.scrollLoadingCount ?? 3;
|
|
3411
|
+
return isInitialLoading ? initialCount : scrollCount;
|
|
3412
|
+
}
|
|
3413
|
+
default: {
|
|
3414
|
+
throw new Error(`${loadingIndicatorType} is not known`);
|
|
3415
|
+
}
|
|
3130
3416
|
}
|
|
3131
|
-
}, [
|
|
3132
|
-
const
|
|
3133
|
-
|
|
3134
|
-
|
|
3135
|
-
|
|
3136
|
-
|
|
3137
|
-
|
|
3138
|
-
|
|
3139
|
-
pagination.isLoading === true
|
|
3140
|
-
? count + 1
|
|
3141
|
-
: count,
|
|
3142
|
-
rowHeight,
|
|
3143
|
-
};
|
|
3144
|
-
}, [pagination, scrollParent, count, rowHeight]);
|
|
3145
|
-
const { fetchMoreOnBottomReached, getVirtualItems, getTotalSize, measureElement } = reactTablePagination.useInfiniteScroll(infiniteScrollProps);
|
|
3146
|
-
react.useEffect(() => {
|
|
3147
|
-
if (scrollParent) {
|
|
3148
|
-
const handleScroll = () => {
|
|
3149
|
-
fetchMoreOnBottomReached(scrollParent);
|
|
3150
|
-
};
|
|
3151
|
-
scrollParent.addEventListener("scroll", handleScroll);
|
|
3152
|
-
return () => {
|
|
3153
|
-
scrollParent.removeEventListener("scroll", handleScroll);
|
|
3154
|
-
};
|
|
3417
|
+
}, [loadingIndicator, pagination]);
|
|
3418
|
+
const estimateSize = react.useCallback((index) => {
|
|
3419
|
+
// Check if this is a loading row
|
|
3420
|
+
if (index >= actualCount) {
|
|
3421
|
+
const loaderIndex = index - actualCount;
|
|
3422
|
+
const shouldShowLoader = pagination?.isLoading === true && loaderIndex < getLoadingRowsCount();
|
|
3423
|
+
// Empty loader rows should be estimated at 0 height to prevent blank space
|
|
3424
|
+
return shouldShowLoader ? DEFAULT_ROW_HEIGHT : 0;
|
|
3155
3425
|
}
|
|
3156
|
-
|
|
3157
|
-
|
|
3158
|
-
|
|
3159
|
-
|
|
3160
|
-
|
|
3161
|
-
|
|
3162
|
-
|
|
3426
|
+
// For data rows (including header), use custom estimator if provided
|
|
3427
|
+
return estimateItemSize ? estimateItemSize(index) : DEFAULT_ROW_HEIGHT;
|
|
3428
|
+
}, [estimateItemSize, actualCount, pagination?.isLoading, getLoadingRowsCount]);
|
|
3429
|
+
const { getVirtualItems, getTotalSize, measureElement, scrollOffset } = reactTablePagination.useInfiniteScroll({
|
|
3430
|
+
pagination: pagination || reactTablePagination.noPagination,
|
|
3431
|
+
containerRef: parentRef,
|
|
3432
|
+
count: actualCount + (pagination?.isLoading === true ? getLoadingRowsCount() : 0),
|
|
3433
|
+
estimateSize,
|
|
3434
|
+
onChange: virtualizer => onScrollStateChange?.(virtualizer.scrollOffset ?? 0, virtualizer.isScrolling),
|
|
3435
|
+
});
|
|
3436
|
+
const isAtTop = scrollOffset <= 0;
|
|
3437
|
+
return (jsxRuntime.jsx("div", { className: cvaListContainer({
|
|
3438
|
+
withTopSeparator: topSeparatorOnScroll && !isAtTop,
|
|
3439
|
+
className,
|
|
3440
|
+
}), "data-testid": dataTestId, ref: parentRef, children: jsxRuntime.jsx("ul", { className: cvaList(), style: { height: `${getTotalSize()}px` }, children: getVirtualItems().map(virtualRow => {
|
|
3441
|
+
const isLoaderRow = virtualRow.index >= actualCount;
|
|
3442
|
+
const isHeaderRow = Boolean(header) && virtualRow.index === 0;
|
|
3443
|
+
// Calculate data index: if header exists, subtract 1 from index for data items
|
|
3444
|
+
const dataIndex = Boolean(header) && !isHeaderRow ? virtualRow.index - 1 : virtualRow.index;
|
|
3445
|
+
// Calculate which loading indicator this is (for multiple loading indicators)
|
|
3446
|
+
const loaderIndex = isLoaderRow ? virtualRow.index - actualCount : 0;
|
|
3447
|
+
// Props required by the virtualizer for proper positioning and behavior
|
|
3448
|
+
const listItemProps = {
|
|
3449
|
+
className: cvaListItem({ separator }), // List styling (separators, spacing)
|
|
3450
|
+
"data-index": isLoaderRow || isHeaderRow ? virtualRow.index : dataIndex, // For accessibility and debugging
|
|
3451
|
+
onClick: onRowClick && !isLoaderRow && !isHeaderRow
|
|
3452
|
+
? () => {
|
|
3453
|
+
const clickedItem = getItemAtIndex(dataIndex);
|
|
3454
|
+
onRowClick(clickedItem, dataIndex);
|
|
3455
|
+
}
|
|
3456
|
+
: undefined, // Row-level click handling (skip header)
|
|
3457
|
+
ref: measureElement, // Required for virtualizer to measure item dimensions
|
|
3458
|
+
style: {
|
|
3459
|
+
transform: `translateY(${virtualRow.start}px)`, // Critical: positions item in virtual scroll
|
|
3460
|
+
},
|
|
3461
|
+
tabIndex: -1, // Keyboard navigation support
|
|
3462
|
+
};
|
|
3463
|
+
// Handle loading rows
|
|
3464
|
+
if (isLoaderRow) {
|
|
3465
|
+
return (jsxRuntime.jsx("li", { ...listItemProps, children: pagination?.isLoading === true && loaderIndex < getLoadingRowsCount() ? (jsxRuntime.jsx(ListLoadingIndicator, { ...loadingIndicator })) : null }, virtualRow.index));
|
|
3466
|
+
}
|
|
3467
|
+
// Handle header row
|
|
3468
|
+
if (isHeaderRow && header) {
|
|
3469
|
+
return (jsxRuntime.jsx("li", { ...listItemProps, children: header }, "header"));
|
|
3470
|
+
}
|
|
3471
|
+
// For regular children, call the children function with virtualization props and item data
|
|
3472
|
+
const item = getItemAtIndex(dataIndex);
|
|
3473
|
+
return children(listItemProps, item, dataIndex);
|
|
3163
3474
|
}) }) }));
|
|
3164
3475
|
};
|
|
3165
3476
|
|
|
@@ -3224,35 +3535,16 @@ const cvaInteractableItem = cssClassVarianceUtilities.cvaMerge("", {
|
|
|
3224
3535
|
},
|
|
3225
3536
|
});
|
|
3226
3537
|
|
|
3227
|
-
const cvaListItem = cssClassVarianceUtilities.cvaMerge(["py-3", "px-4", "min-h-14", "w-full", "flex", "justify-between", "items-center"]);
|
|
3228
|
-
const cvaMainInformationClass = cssClassVarianceUtilities.cvaMerge(["grid", "items-center", "text-sm", "gap-2"], {
|
|
3229
|
-
variants: {
|
|
3230
|
-
hasThumbnail: {
|
|
3231
|
-
true: "grid-cols-min-fr",
|
|
3232
|
-
false: "grid-cols-1",
|
|
3233
|
-
},
|
|
3234
|
-
},
|
|
3235
|
-
});
|
|
3236
|
-
const cvaThumbnailContainer = cssClassVarianceUtilities.cvaMerge([
|
|
3237
|
-
"flex",
|
|
3238
|
-
"h-8",
|
|
3239
|
-
"w-8",
|
|
3240
|
-
"items-center",
|
|
3241
|
-
"justify-center",
|
|
3242
|
-
"overflow-hidden",
|
|
3243
|
-
"rounded-md",
|
|
3244
|
-
]);
|
|
3245
|
-
|
|
3246
3538
|
/**
|
|
3247
3539
|
* 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.
|
|
3248
3540
|
*
|
|
3249
3541
|
* @param { ListItemProps} props - The props for the ListItem component
|
|
3250
3542
|
* @returns {Element} ListItem component
|
|
3251
3543
|
*/
|
|
3252
|
-
const ListItem = ({ className, dataTestId, onClick, details, title, description, meta, thumbnail, thumbnailColor = "info-600", thumbnailBackground = "info-100", }) => {
|
|
3253
|
-
const baseClass = cvaListItem({ className });
|
|
3544
|
+
const ListItem = ({ className, dataTestId, onClick, details, title, description, meta, thumbnail, thumbnailColor = "info-600", thumbnailBackground = "info-100", ...rest }) => {
|
|
3545
|
+
const baseClass = cvaListItem$1({ className });
|
|
3254
3546
|
const interactableItemClass = onClick ? tailwindMerge.twMerge(baseClass, cvaInteractableItem({ cursor: "pointer" })) : baseClass;
|
|
3255
|
-
return (jsxRuntime.jsxs("
|
|
3547
|
+
return (jsxRuntime.jsxs("li", { className: interactableItemClass, "data-testid": dataTestId, onClick: onClick, ...rest, children: [jsxRuntime.jsxs("div", { className: cvaMainInformationClass({ hasThumbnail: !!thumbnail }), children: [thumbnail ? (jsxRuntime.jsx("div", { className: cvaThumbnailContainer({
|
|
3256
3548
|
className: `text-${thumbnailColor} bg-${thumbnailBackground}`,
|
|
3257
3549
|
}), children: thumbnail })) : null, jsxRuntime.jsxs("div", { className: "grid-rows-min-fr grid items-center text-sm", children: [jsxRuntime.jsx("div", { className: "gap-responsive-space-sm flex w-full min-w-0 items-center text-sm", children: typeof title === "string" ? (jsxRuntime.jsx(Text, { className: "truncate", dataTestId: dataTestId ? `${dataTestId}-title` : undefined, weight: "bold", children: title })) : (react.cloneElement(title, {
|
|
3258
3550
|
className: tailwindMerge.twMerge(title.props.className, "neutral-900 text-sm font-medium truncate"),
|
|
@@ -3260,7 +3552,7 @@ const ListItem = ({ className, dataTestId, onClick, details, title, description,
|
|
|
3260
3552
|
})) }), description !== undefined && description !== "" ? (jsxRuntime.jsx("div", { className: "gap-responsive-space-sm flex w-full min-w-0 items-center", children: typeof description === "string" ? (jsxRuntime.jsx(Text, { className: "truncate text-xs text-neutral-500", dataTestId: dataTestId ? `${dataTestId}-description` : undefined, weight: "bold", children: description })) : (react.cloneElement(description, {
|
|
3261
3553
|
className: tailwindMerge.twMerge(description.props.className, "text-neutral-500 text-xs font-medium truncate"),
|
|
3262
3554
|
dataTestId: !description.props.dataTestId && dataTestId ? `${dataTestId}-description` : undefined,
|
|
3263
|
-
})) })) : null, meta ? (jsxRuntime.jsx("div", { className: "gap-responsive-space-sm flex w-full min-w-0 items-center pt-0.5", children: jsxRuntime.jsx(Text, { className: "truncate text-xs text-neutral-400", dataTestId: dataTestId ? `${dataTestId}-meta` : undefined, weight: "bold", children: meta }) })) : null] })] }), jsxRuntime.jsxs("div", { className: "flex items-center gap-0.5 pl-2", children: [details, onClick ? jsxRuntime.jsx(Icon, { color: "neutral", name: "ChevronRight", size: "medium" }) : null] })] }));
|
|
3555
|
+
})) })) : null, meta ? (jsxRuntime.jsx("div", { className: "gap-responsive-space-sm flex w-full min-w-0 items-center pt-0.5", children: jsxRuntime.jsx(Text, { className: "truncate text-xs text-neutral-400", dataTestId: dataTestId ? `${dataTestId}-meta` : undefined, weight: "bold", children: meta }) })) : null] })] }), jsxRuntime.jsxs("div", { className: "flex items-center gap-0.5 text-nowrap pl-2", children: [details, onClick ? jsxRuntime.jsx(Icon, { color: "neutral", name: "ChevronRight", size: "medium" }) : null] })] }));
|
|
3264
3556
|
};
|
|
3265
3557
|
|
|
3266
3558
|
const cvaMenuList = cssClassVarianceUtilities.cvaMerge([
|
|
@@ -4401,28 +4693,6 @@ const ValueBar = ({ value, min = 0, max = 100, unit, size = "small", levelColors
|
|
|
4401
4693
|
return (jsxRuntime.jsxs("span", { className: "relative flex items-center gap-2", "data-testid": dataTestId, children: [jsxRuntime.jsx("progress", { "aria-label": valueText, className: cvaValueBar({ className, size }), max: 100, style: { color: barFillColor }, value: score * 100 }), showValue && (size === "small" || size === "large") ? (jsxRuntime.jsx(Text, { className: cvaValueBarText({ size }), dataTestId: dataTestId ? `${dataTestId}-value` : undefined, children: jsxRuntime.jsx("span", { style: valueColor ? { color: valueColor } : undefined, children: valueText }) })) : null] }));
|
|
4402
4694
|
};
|
|
4403
4695
|
|
|
4404
|
-
const cvaZStackContainer = cssClassVarianceUtilities.cvaMerge(["grid", "grid-cols-1", "grid-rows-1"]);
|
|
4405
|
-
const cvaZStackItem = cssClassVarianceUtilities.cvaMerge(["col-start-1", "col-end-1", "row-start-1", "row-end-2"]);
|
|
4406
|
-
|
|
4407
|
-
/**
|
|
4408
|
-
* ZStack is a component that stacks its children on the z-axis.
|
|
4409
|
-
* Is a good alternative to "position: absolute" that avoids some of the unfortunate side effects of absolute positioning.
|
|
4410
|
-
*
|
|
4411
|
-
* @param { ZStackProps} props - The props for the ZStack component
|
|
4412
|
-
* @returns {Element} ZStack component
|
|
4413
|
-
*/
|
|
4414
|
-
const ZStack = ({ children, className, dataTestId }) => {
|
|
4415
|
-
return (jsxRuntime.jsx("div", { className: cvaZStackContainer({ className }), "data-testid": dataTestId, children: react.Children.map(children, (child, index) => {
|
|
4416
|
-
if (!react.isValidElement(child)) {
|
|
4417
|
-
return child;
|
|
4418
|
-
}
|
|
4419
|
-
return react.cloneElement(child, {
|
|
4420
|
-
className: cvaZStackItem({ className: child.props.className }),
|
|
4421
|
-
key: index,
|
|
4422
|
-
});
|
|
4423
|
-
}) }));
|
|
4424
|
-
};
|
|
4425
|
-
|
|
4426
4696
|
const cvaClickable = cssClassVarianceUtilities.cvaMerge([
|
|
4427
4697
|
"shadow-lg",
|
|
4428
4698
|
"rounded-lg",
|
|
@@ -4466,6 +4736,7 @@ exports.EmptyValue = EmptyValue;
|
|
|
4466
4736
|
exports.ExternalLink = ExternalLink;
|
|
4467
4737
|
exports.Heading = Heading;
|
|
4468
4738
|
exports.Highlight = Highlight;
|
|
4739
|
+
exports.HorizontalOverflowScroller = HorizontalOverflowScroller;
|
|
4469
4740
|
exports.Icon = Icon;
|
|
4470
4741
|
exports.IconButton = IconButton;
|
|
4471
4742
|
exports.Indicator = Indicator;
|
|
@@ -4526,7 +4797,7 @@ exports.cvaIndicatorPing = cvaIndicatorPing;
|
|
|
4526
4797
|
exports.cvaInteractableItem = cvaInteractableItem;
|
|
4527
4798
|
exports.cvaList = cvaList;
|
|
4528
4799
|
exports.cvaListContainer = cvaListContainer;
|
|
4529
|
-
exports.cvaListItem = cvaListItem
|
|
4800
|
+
exports.cvaListItem = cvaListItem;
|
|
4530
4801
|
exports.cvaMenuItem = cvaMenuItem;
|
|
4531
4802
|
exports.cvaMenuItemLabel = cvaMenuItemLabel;
|
|
4532
4803
|
exports.cvaMenuItemPrefix = cvaMenuItemPrefix;
|
|
@@ -4544,6 +4815,7 @@ exports.cvaZStackContainer = cvaZStackContainer;
|
|
|
4544
4815
|
exports.cvaZStackItem = cvaZStackItem;
|
|
4545
4816
|
exports.docs = docs;
|
|
4546
4817
|
exports.getDevicePixelRatio = getDevicePixelRatio;
|
|
4818
|
+
exports.getResponsiveRandomWidthPercentage = getResponsiveRandomWidthPercentage;
|
|
4547
4819
|
exports.getValueBarColorByValue = getValueBarColorByValue;
|
|
4548
4820
|
exports.iconColorNames = iconColorNames;
|
|
4549
4821
|
exports.iconPalette = iconPalette;
|
|
@@ -4566,6 +4838,7 @@ exports.usePrompt = usePrompt;
|
|
|
4566
4838
|
exports.useResize = useResize;
|
|
4567
4839
|
exports.useScrollDetection = useScrollDetection;
|
|
4568
4840
|
exports.useSelfUpdatingRef = useSelfUpdatingRef;
|
|
4841
|
+
exports.useStable = useStable;
|
|
4569
4842
|
exports.useTimeout = useTimeout;
|
|
4570
4843
|
exports.useViewportBreakpoints = useViewportBreakpoints;
|
|
4571
4844
|
exports.useWatch = useWatch;
|