@bsol-oss/react-datatable5 13.0.1-beta.33 → 13.0.1-beta.34

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -1057,6 +1057,10 @@ interface TimeViewportGridProps {
1057
1057
  viewportStart?: TimeInput;
1058
1058
  viewportEnd?: TimeInput;
1059
1059
  tickCount?: number;
1060
+ tickStrategy?: 'count' | 'timeUnit';
1061
+ tickUnit?: 'minute' | 'hour' | 'day';
1062
+ tickStep?: number;
1063
+ format?: string;
1060
1064
  minorDivisions?: number;
1061
1065
  majorLineColor?: string;
1062
1066
  minorLineColor?: string;
@@ -1090,6 +1094,12 @@ interface TimeViewportBlocksProps {
1090
1094
  trackKey?: string | number;
1091
1095
  }) => ReactNode;
1092
1096
  onBlockClick?: (block: TimeViewportBlockItem) => void;
1097
+ /** Enable virtual scrolling for large track lists. */
1098
+ virtualize?: boolean;
1099
+ /** Fixed pixel height of the scroll container when virtualize is true. Defaults to 400. */
1100
+ virtualHeight?: number;
1101
+ /** Number of off-screen rows to render above/below the visible area. Defaults to 5. */
1102
+ overscan?: number;
1093
1103
  }
1094
1104
  interface TimeViewportRootProps {
1095
1105
  viewportStart: TimeInput;
@@ -1172,8 +1182,8 @@ declare function TimeViewportHeader({ viewportStart, viewportEnd, tickCount, tic
1172
1182
  * Vertical grid lines for measuring block positions in the viewport.
1173
1183
  * Render inside a relative container that also contains blocks.
1174
1184
  */
1175
- declare function TimeViewportGrid({ viewportStart, viewportEnd, tickCount, minorDivisions, majorLineColor, minorLineColor, showMinorLines, zIndex, animationDurationMs, animationEasing, }: TimeViewportGridProps): react_jsx_runtime.JSX.Element | null;
1176
- declare function TimeViewportBlocks({ blocks, viewportStart, viewportEnd, height, minWidthPx, borderRadius, defaultColorPalette, showLabel, hideWhenOutOfView, hideEmptyTracks, gap, allowOverlap, overlapOpacity, renderTrackPrefix, renderTrackSuffix, onBlockClick, }: TimeViewportBlocksProps): react_jsx_runtime.JSX.Element;
1185
+ declare function TimeViewportGrid({ viewportStart, viewportEnd, tickCount, tickStrategy, tickUnit, tickStep, format, minorDivisions, majorLineColor, minorLineColor, showMinorLines, zIndex, animationDurationMs, animationEasing, }: TimeViewportGridProps): react_jsx_runtime.JSX.Element | null;
1186
+ declare function TimeViewportBlocks({ blocks, viewportStart, viewportEnd, height, minWidthPx, borderRadius, defaultColorPalette, showLabel, hideWhenOutOfView, hideEmptyTracks, gap, allowOverlap, overlapOpacity, renderTrackPrefix, renderTrackSuffix, onBlockClick, virtualize, virtualHeight, overscan, }: TimeViewportBlocksProps): react_jsx_runtime.JSX.Element;
1177
1187
  declare function TimeRangeZoom({ range, onRangeChange, minDurationMs, maxDurationMs, zoomFactor, resetDurationMs, showResetButton, disabled, labels, }: TimeRangeZoomProps): react_jsx_runtime.JSX.Element;
1178
1188
  declare function useTimeRangeZoom({ range, onRangeChange, minDurationMs, maxDurationMs, zoomFactor, resetDurationMs, disabled, labels, }: TimeRangeZoomProps): UseTimeRangeZoomResult;
1179
1189
 
package/dist/index.js CHANGED
@@ -32,6 +32,7 @@ var timezone = require('dayjs/plugin/timezone');
32
32
  var utc = require('dayjs/plugin/utc');
33
33
  var ti = require('react-icons/ti');
34
34
  var ajv = require('@hookform/resolvers/ajv');
35
+ var reactVirtual = require('@tanstack/react-virtual');
35
36
  var matchSorterUtils = require('@tanstack/match-sorter-utils');
36
37
 
37
38
  function _interopNamespaceDefault(e) {
@@ -8095,7 +8096,7 @@ function TimeViewportRoot({ viewportStart, viewportEnd, children, onViewportChan
8095
8096
  return (jsxRuntime.jsx(TimeViewportContext.Provider, { value: { viewportStart, viewportEnd }, children: jsxRuntime.jsx(react.Box, { ref: containerRef, position: "relative", width: "100%", cursor: enableDragPan ? (isDragging ? 'grabbing' : 'grab') : 'default', userSelect: enableDragPan ? 'none' : undefined, onPointerDown: handlePointerDown, onPointerMove: handlePointerMove, onPointerUp: stopDragging, onPointerCancel: stopDragging, onWheel: handleWheel, children: children }) }));
8096
8097
  }
8097
8098
  function TimeViewportTrackRow({ trackKey, blocks, resolvedHeight, prefix, suffix, renderBlockNode, }) {
8098
- return (jsxRuntime.jsxs(react.HStack, { align: "stretch", gap: 2, children: [prefix ? (jsxRuntime.jsx(react.Box, { minW: "fit-content", display: "flex", alignItems: "center", children: prefix })) : null, jsxRuntime.jsx(react.Box, { position: "relative", width: "100%", height: resolvedHeight, children: blocks.map((item, index) => renderBlockNode(item, index)) }), suffix ? (jsxRuntime.jsx(react.Box, { minW: "fit-content", display: "flex", alignItems: "center", children: suffix })) : null] }, trackKey));
8099
+ return (jsxRuntime.jsxs(react.HStack, { width: "100%", overflowX: 'hidden', align: "stretch", gap: 2, children: [prefix ? (jsxRuntime.jsx(react.Box, { minW: "fit-content", display: "flex", alignItems: "center", children: prefix })) : null, jsxRuntime.jsx(react.Box, { position: "relative", width: "100%", height: resolvedHeight, children: blocks.map((item, index) => renderBlockNode(item, index)) }), suffix ? (jsxRuntime.jsx(react.Box, { minW: "fit-content", display: "flex", alignItems: "center", children: suffix })) : null] }, trackKey));
8099
8100
  }
8100
8101
  const defaultLabels = {
8101
8102
  zoomIn: 'Zoom in',
@@ -8357,35 +8358,55 @@ function TimeViewportHeader({ viewportStart, viewportEnd, tickCount = 7, tickStr
8357
8358
  * Vertical grid lines for measuring block positions in the viewport.
8358
8359
  * Render inside a relative container that also contains blocks.
8359
8360
  */
8360
- function TimeViewportGrid({ viewportStart, viewportEnd, tickCount = 8, minorDivisions = 2, majorLineColor = 'gray.300', minorLineColor = 'gray.200', showMinorLines = true, zIndex = 0, animationDurationMs = VIEWPORT_TRANSITION_DURATION_MS, animationEasing = VIEWPORT_TRANSITION_EASING, }) {
8361
- const viewport = useResolvedViewport(viewportStart, viewportEnd);
8362
- const start = viewport ? parseTimeInput(viewport.viewportStart) : null;
8363
- const end = viewport ? parseTimeInput(viewport.viewportEnd) : null;
8364
- if (!start || !end || !end.isAfter(start))
8361
+ function TimeViewportGrid({ viewportStart, viewportEnd, tickCount = 8, tickStrategy = 'count', tickUnit = 'hour', tickStep = 1, format, minorDivisions = 2, majorLineColor = 'gray.300', minorLineColor = 'gray.200', showMinorLines = true, zIndex = 0, animationDurationMs = VIEWPORT_TRANSITION_DURATION_MS, animationEasing = VIEWPORT_TRANSITION_EASING, }) {
8362
+ const { isValidViewport, getTicks } = useTimeViewport(viewportStart, viewportEnd, format);
8363
+ const majorTicks = getTicks({ tickStrategy, tickCount, tickUnit, tickStep });
8364
+ if (!isValidViewport || majorTicks.length < 2)
8365
8365
  return null;
8366
- const safeTickCount = Math.max(2, tickCount);
8367
- const majorTicks = Array.from({ length: safeTickCount }, (_, index) => ({
8368
- index,
8369
- percent: (index / (safeTickCount - 1)) * 100,
8370
- }));
8371
8366
  const safeMinorDivisions = Math.max(1, minorDivisions);
8372
8367
  const transitionValue = animationDurationMs > 0
8373
8368
  ? `transform ${animationDurationMs}ms ${animationEasing}, opacity ${animationDurationMs}ms ${animationEasing}`
8374
8369
  : undefined;
8375
8370
  const minorTicks = showMinorLines
8376
- ? Array.from({ length: safeTickCount - 1 }, (_, segmentIndex) => {
8377
- const base = (segmentIndex / (safeTickCount - 1)) * 100;
8378
- const next = ((segmentIndex + 1) / (safeTickCount - 1)) * 100;
8371
+ ? majorTicks.slice(0, -1).flatMap((tick, segmentIndex) => {
8372
+ const base = tick.percent;
8373
+ const next = majorTicks[segmentIndex + 1].percent;
8379
8374
  const segment = [];
8380
8375
  for (let step = 1; step < safeMinorDivisions; step += 1) {
8381
8376
  segment.push(base + ((next - base) * step) / safeMinorDivisions);
8382
8377
  }
8383
8378
  return segment;
8384
- }).flat()
8379
+ })
8385
8380
  : [];
8386
8381
  return (jsxRuntime.jsxs(react.Box, { position: "absolute", inset: 0, pointerEvents: "none", zIndex: zIndex, children: [minorTicks.map((percent, index) => (jsxRuntime.jsx(react.Box, { position: "absolute", inset: 0, transform: `translateX(${percent}%)`, transition: transitionValue, children: jsxRuntime.jsx(react.Box, { position: "absolute", insetInlineStart: 0, top: 0, bottom: 0, width: "1px", bg: minorLineColor, _dark: { bg: 'gray.700' } }) }, `minor-grid-${index}`))), majorTicks.map((tick) => (jsxRuntime.jsx(react.Box, { position: "absolute", inset: 0, transform: `translateX(${tick.percent}%)`, transition: transitionValue, children: jsxRuntime.jsx(react.Box, { position: "absolute", insetInlineStart: 0, top: 0, bottom: 0, width: "1px", bg: majorLineColor, _dark: { bg: 'gray.600' } }) }, `major-grid-${tick.index}`)))] }));
8387
8382
  }
8388
- function TimeViewportBlocks({ blocks, viewportStart, viewportEnd, height = '28px', minWidthPx = 2, borderRadius = 'sm', defaultColorPalette = 'blue', showLabel = true, hideWhenOutOfView = true, hideEmptyTracks = true, gap = 2, allowOverlap = false, overlapOpacity = 0.9, renderTrackPrefix, renderTrackSuffix, onBlockClick, }) {
8383
+ function VirtualizedTrackList({ tracks, resolvedHeight, gap, virtualHeight, overscan, renderTrackPrefix, renderTrackSuffix, renderBlockNode, }) {
8384
+ const parentRef = React.useRef(null);
8385
+ const rowHeightPx = parseInt(resolvedHeight, 10) || 28;
8386
+ const gapPx = gap * 4; // Chakra spacing token to px (1 unit = 4px)
8387
+ const virtualizer = reactVirtual.useVirtualizer({
8388
+ count: tracks.length,
8389
+ getScrollElement: () => parentRef.current,
8390
+ estimateSize: () => rowHeightPx + gapPx,
8391
+ overscan,
8392
+ });
8393
+ return (jsxRuntime.jsx(react.Box, { ref: parentRef, overflowY: "auto", height: `${virtualHeight}px`, children: jsxRuntime.jsx(react.Box, { height: `${virtualizer.getTotalSize()}px`, position: "relative", children: virtualizer.getVirtualItems().map((virtualRow) => {
8394
+ const track = tracks[virtualRow.index];
8395
+ const trackBlocks = track.blocks.map((item) => item.block);
8396
+ const prefix = renderTrackPrefix?.({
8397
+ trackIndex: virtualRow.index,
8398
+ trackBlocks,
8399
+ trackKey: track.trackKeyRaw,
8400
+ });
8401
+ const suffix = renderTrackSuffix?.({
8402
+ trackIndex: virtualRow.index,
8403
+ trackBlocks,
8404
+ trackKey: track.trackKeyRaw,
8405
+ });
8406
+ return (jsxRuntime.jsx(react.Box, { position: "absolute", top: 0, left: 0, width: "100%", transform: `translateY(${virtualRow.start}px)`, children: jsxRuntime.jsx(TimeViewportTrackRow, { trackKey: track.trackKey, blocks: track.blocks, resolvedHeight: resolvedHeight, prefix: prefix, suffix: suffix, renderBlockNode: renderBlockNode }) }, track.trackKey));
8407
+ }) }) }));
8408
+ }
8409
+ function TimeViewportBlocks({ blocks, viewportStart, viewportEnd, height = '28px', minWidthPx = 2, borderRadius = 'sm', defaultColorPalette = 'blue', showLabel = true, hideWhenOutOfView = true, hideEmptyTracks = true, gap = 2, allowOverlap = false, overlapOpacity = 0.9, renderTrackPrefix, renderTrackSuffix, onBlockClick, virtualize = false, virtualHeight = 400, overscan = 5, }) {
8389
8410
  const { getGeometry, toTimeMs } = useTimeViewportBlockGeometry(viewportStart, viewportEnd);
8390
8411
  const resolvedHeight = typeof height === 'number' ? `${height}px` : height;
8391
8412
  const expandedBlocks = flattenTrackBlocks(blocks);
@@ -8420,74 +8441,73 @@ function TimeViewportBlocks({ blocks, viewportStart, viewportEnd, height = '28px
8420
8441
  onBlockClick?.(block);
8421
8442
  };
8422
8443
  const isBlockClickable = Boolean(block.onClick || onBlockClick);
8423
- return (jsxRuntime.jsx(react.Box, { position: "absolute", inset: 0, pointerEvents: "none", children: jsxRuntime.jsx(react.Box, { width: "100%", height: "100%", transform: `translateX(${geometry.leftPercent}%)`, transition: VIEWPORT_TRANSITION, children: jsxRuntime.jsx(react.Box, { width: `max(${geometry.widthPercent}%, ${minWidthPx}px)`, height: "100%", borderRadius: borderRadius, bg: block.background ??
8424
- `${block.colorPalette ?? defaultColorPalette}.500`, _dark: {
8425
- bg: block.background ??
8426
- `${block.colorPalette ?? defaultColorPalette}.900`,
8427
- }, display: "flex", alignItems: "center", justifyContent: "center", px: 2, overflow: "hidden", opacity: allowOverlap ? overlapOpacity : 1, zIndex: indexInLayer + 1, pointerEvents: "auto", onClick: isBlockClickable ? handleBlockClick : undefined, cursor: isBlockClickable ? 'pointer' : 'default', children: showLabel && block.label ? (jsxRuntime.jsx(react.Text, { fontSize: "xs", lineClamp: 1, color: "white", _dark: { color: 'gray.100' }, children: block.label })) : null }) }) }, block.id));
8444
+ return (jsxRuntime.jsx(react.Box, { height: "100%", position: "absolute", inset: 0, pointerEvents: "none", transform: `translateX(${geometry.leftPercent}%)`, transition: VIEWPORT_TRANSITION, children: jsxRuntime.jsx(react.Box, { width: `max(${geometry.widthPercent}%, ${minWidthPx}px)`, height: "100%", borderRadius: borderRadius, bg: block.background ??
8445
+ `${block.colorPalette ?? defaultColorPalette}.500`, _dark: {
8446
+ bg: block.background ??
8447
+ `${block.colorPalette ?? defaultColorPalette}.900`,
8448
+ }, display: "flex", alignItems: "center", justifyContent: "center", px: 2, overflow: "hidden", opacity: allowOverlap ? overlapOpacity : 1, zIndex: indexInLayer + 1, pointerEvents: "auto", onClick: isBlockClickable ? handleBlockClick : undefined, cursor: isBlockClickable ? 'pointer' : 'default', children: showLabel && block.label ? (jsxRuntime.jsx(react.Text, { fontSize: "xs", lineClamp: 1, color: "white", _dark: { color: 'gray.100' }, children: block.label })) : null }) }, block.id));
8428
8449
  };
8450
+ // ---------- Resolve tracks ----------
8429
8451
  const explicitTrackKeys = Array.from(new Set(expandedBlocks
8430
8452
  .map((item) => item.track)
8431
8453
  .filter((track) => track !== undefined)));
8432
- if (explicitTrackKeys.length > 0) {
8433
- const tracks = explicitTrackKeys
8434
- .map((trackKey) => ({
8435
- trackKey,
8436
- blocks: parsedBlocks.filter((item) => item.block.track === trackKey),
8437
- }))
8438
- .filter((track) => !hideEmptyTracks || track.blocks.length > 0);
8439
- return (jsxRuntime.jsx(react.VStack, { align: "stretch", gap: gap, children: tracks.map((track, trackIndex) => {
8440
- const trackBlocks = track.blocks.map((item) => item.block);
8441
- const prefix = renderTrackPrefix?.({
8442
- trackIndex,
8443
- trackBlocks,
8444
- trackKey: track.trackKey,
8445
- });
8446
- const suffix = renderTrackSuffix?.({
8447
- trackIndex,
8448
- trackBlocks,
8449
- trackKey: track.trackKey,
8450
- });
8451
- return (jsxRuntime.jsx(TimeViewportTrackRow, { trackKey: `track-keyed-${String(track.trackKey)}`, blocks: track.blocks, resolvedHeight: resolvedHeight, prefix: prefix, suffix: suffix, renderBlockNode: renderBlockNode }));
8452
- }) }));
8454
+ const resolvedTracks = React.useMemo(() => {
8455
+ if (explicitTrackKeys.length > 0) {
8456
+ return explicitTrackKeys
8457
+ .map((trackKey) => ({
8458
+ trackKey: `track-keyed-${String(trackKey)}`,
8459
+ trackKeyRaw: trackKey,
8460
+ blocks: parsedBlocks.filter((item) => item.block.track === trackKey),
8461
+ }))
8462
+ .filter((track) => !hideEmptyTracks || track.blocks.length > 0);
8463
+ }
8464
+ const autoPackedTracks = (() => {
8465
+ const sortedBlocks = [...parsedBlocks]
8466
+ .filter(({ startMs, endMs }) => Number.isFinite(startMs) && Number.isFinite(endMs))
8467
+ .sort((a, b) => {
8468
+ if (a.startMs === b.startMs)
8469
+ return a.endMs - b.endMs;
8470
+ return a.startMs - b.startMs;
8471
+ });
8472
+ const trackLastEndTimes = [];
8473
+ const tracks = [];
8474
+ sortedBlocks.forEach((item) => {
8475
+ const trackIndex = trackLastEndTimes.findIndex((endMs) => item.startMs >= endMs);
8476
+ if (trackIndex === -1) {
8477
+ trackLastEndTimes.push(item.endMs);
8478
+ tracks.push([item]);
8479
+ }
8480
+ else {
8481
+ trackLastEndTimes[trackIndex] = item.endMs;
8482
+ tracks[trackIndex].push(item);
8483
+ }
8484
+ });
8485
+ return tracks;
8486
+ })();
8487
+ const packed = allowOverlap ? [parsedBlocks] : autoPackedTracks;
8488
+ return packed.map((track, trackIndex) => ({
8489
+ trackKey: `track-row-${trackIndex}`,
8490
+ trackKeyRaw: undefined,
8491
+ blocks: track,
8492
+ }));
8493
+ }, [allowOverlap, explicitTrackKeys, hideEmptyTracks, parsedBlocks]);
8494
+ // ---------- Render ----------
8495
+ if (virtualize) {
8496
+ return (jsxRuntime.jsx(VirtualizedTrackList, { tracks: resolvedTracks, resolvedHeight: resolvedHeight, gap: gap, virtualHeight: virtualHeight, overscan: overscan, renderTrackPrefix: renderTrackPrefix, renderTrackSuffix: renderTrackSuffix, renderBlockNode: renderBlockNode }));
8453
8497
  }
8454
- const autoPackedTracks = (() => {
8455
- const sortedBlocks = [...parsedBlocks]
8456
- .filter(({ startMs, endMs }) => Number.isFinite(startMs) && Number.isFinite(endMs))
8457
- .sort((a, b) => {
8458
- if (a.startMs === b.startMs)
8459
- return a.endMs - b.endMs;
8460
- return a.startMs - b.startMs;
8461
- });
8462
- const trackLastEndTimes = [];
8463
- const tracks = [];
8464
- sortedBlocks.forEach((item) => {
8465
- const trackIndex = trackLastEndTimes.findIndex((endMs) => item.startMs >= endMs);
8466
- if (trackIndex === -1) {
8467
- trackLastEndTimes.push(item.endMs);
8468
- tracks.push([item]);
8469
- }
8470
- else {
8471
- trackLastEndTimes[trackIndex] = item.endMs;
8472
- tracks[trackIndex].push(item);
8473
- }
8474
- });
8475
- return tracks;
8476
- })();
8477
- const tracks = allowOverlap ? [parsedBlocks] : autoPackedTracks;
8478
- return (jsxRuntime.jsx(react.VStack, { align: "stretch", gap: gap, children: tracks.map((track, trackIndex) => {
8479
- const trackBlocks = track.map((item) => item.block);
8498
+ return (jsxRuntime.jsx(react.VStack, { align: "stretch", gap: gap, children: resolvedTracks.map((track, trackIndex) => {
8499
+ const trackBlocks = track.blocks.map((item) => item.block);
8480
8500
  const prefix = renderTrackPrefix?.({
8481
8501
  trackIndex,
8482
8502
  trackBlocks,
8483
- trackKey: undefined,
8503
+ trackKey: track.trackKeyRaw,
8484
8504
  });
8485
8505
  const suffix = renderTrackSuffix?.({
8486
8506
  trackIndex,
8487
8507
  trackBlocks,
8488
- trackKey: undefined,
8508
+ trackKey: track.trackKeyRaw,
8489
8509
  });
8490
- return (jsxRuntime.jsx(TimeViewportTrackRow, { trackKey: `track-row-${trackIndex}`, blocks: track, resolvedHeight: resolvedHeight, prefix: prefix, suffix: suffix, renderBlockNode: renderBlockNode }));
8510
+ return (jsxRuntime.jsx(TimeViewportTrackRow, { trackKey: track.trackKey, blocks: track.blocks, resolvedHeight: resolvedHeight, prefix: prefix, suffix: suffix, renderBlockNode: renderBlockNode }));
8491
8511
  }) }));
8492
8512
  }
8493
8513
  function TimeRangeZoom({ range, onRangeChange, minDurationMs = DEFAULT_MIN_DURATION_MS, maxDurationMs = DEFAULT_MAX_DURATION_MS, zoomFactor = DEFAULT_ZOOM_FACTOR, resetDurationMs, showResetButton = true, disabled = false, labels, }) {
package/dist/index.mjs CHANGED
@@ -31,6 +31,7 @@ import timezone from 'dayjs/plugin/timezone';
31
31
  import utc from 'dayjs/plugin/utc';
32
32
  import { TiDeleteOutline } from 'react-icons/ti';
33
33
  import { ajvResolver } from '@hookform/resolvers/ajv';
34
+ import { useVirtualizer } from '@tanstack/react-virtual';
34
35
  import { rankItem } from '@tanstack/match-sorter-utils';
35
36
 
36
37
  const DataTableContext = createContext({
@@ -8075,7 +8076,7 @@ function TimeViewportRoot({ viewportStart, viewportEnd, children, onViewportChan
8075
8076
  return (jsx(TimeViewportContext.Provider, { value: { viewportStart, viewportEnd }, children: jsx(Box, { ref: containerRef, position: "relative", width: "100%", cursor: enableDragPan ? (isDragging ? 'grabbing' : 'grab') : 'default', userSelect: enableDragPan ? 'none' : undefined, onPointerDown: handlePointerDown, onPointerMove: handlePointerMove, onPointerUp: stopDragging, onPointerCancel: stopDragging, onWheel: handleWheel, children: children }) }));
8076
8077
  }
8077
8078
  function TimeViewportTrackRow({ trackKey, blocks, resolvedHeight, prefix, suffix, renderBlockNode, }) {
8078
- return (jsxs(HStack, { align: "stretch", gap: 2, children: [prefix ? (jsx(Box, { minW: "fit-content", display: "flex", alignItems: "center", children: prefix })) : null, jsx(Box, { position: "relative", width: "100%", height: resolvedHeight, children: blocks.map((item, index) => renderBlockNode(item, index)) }), suffix ? (jsx(Box, { minW: "fit-content", display: "flex", alignItems: "center", children: suffix })) : null] }, trackKey));
8079
+ return (jsxs(HStack, { width: "100%", overflowX: 'hidden', align: "stretch", gap: 2, children: [prefix ? (jsx(Box, { minW: "fit-content", display: "flex", alignItems: "center", children: prefix })) : null, jsx(Box, { position: "relative", width: "100%", height: resolvedHeight, children: blocks.map((item, index) => renderBlockNode(item, index)) }), suffix ? (jsx(Box, { minW: "fit-content", display: "flex", alignItems: "center", children: suffix })) : null] }, trackKey));
8079
8080
  }
8080
8081
  const defaultLabels = {
8081
8082
  zoomIn: 'Zoom in',
@@ -8337,35 +8338,55 @@ function TimeViewportHeader({ viewportStart, viewportEnd, tickCount = 7, tickStr
8337
8338
  * Vertical grid lines for measuring block positions in the viewport.
8338
8339
  * Render inside a relative container that also contains blocks.
8339
8340
  */
8340
- function TimeViewportGrid({ viewportStart, viewportEnd, tickCount = 8, minorDivisions = 2, majorLineColor = 'gray.300', minorLineColor = 'gray.200', showMinorLines = true, zIndex = 0, animationDurationMs = VIEWPORT_TRANSITION_DURATION_MS, animationEasing = VIEWPORT_TRANSITION_EASING, }) {
8341
- const viewport = useResolvedViewport(viewportStart, viewportEnd);
8342
- const start = viewport ? parseTimeInput(viewport.viewportStart) : null;
8343
- const end = viewport ? parseTimeInput(viewport.viewportEnd) : null;
8344
- if (!start || !end || !end.isAfter(start))
8341
+ function TimeViewportGrid({ viewportStart, viewportEnd, tickCount = 8, tickStrategy = 'count', tickUnit = 'hour', tickStep = 1, format, minorDivisions = 2, majorLineColor = 'gray.300', minorLineColor = 'gray.200', showMinorLines = true, zIndex = 0, animationDurationMs = VIEWPORT_TRANSITION_DURATION_MS, animationEasing = VIEWPORT_TRANSITION_EASING, }) {
8342
+ const { isValidViewport, getTicks } = useTimeViewport(viewportStart, viewportEnd, format);
8343
+ const majorTicks = getTicks({ tickStrategy, tickCount, tickUnit, tickStep });
8344
+ if (!isValidViewport || majorTicks.length < 2)
8345
8345
  return null;
8346
- const safeTickCount = Math.max(2, tickCount);
8347
- const majorTicks = Array.from({ length: safeTickCount }, (_, index) => ({
8348
- index,
8349
- percent: (index / (safeTickCount - 1)) * 100,
8350
- }));
8351
8346
  const safeMinorDivisions = Math.max(1, minorDivisions);
8352
8347
  const transitionValue = animationDurationMs > 0
8353
8348
  ? `transform ${animationDurationMs}ms ${animationEasing}, opacity ${animationDurationMs}ms ${animationEasing}`
8354
8349
  : undefined;
8355
8350
  const minorTicks = showMinorLines
8356
- ? Array.from({ length: safeTickCount - 1 }, (_, segmentIndex) => {
8357
- const base = (segmentIndex / (safeTickCount - 1)) * 100;
8358
- const next = ((segmentIndex + 1) / (safeTickCount - 1)) * 100;
8351
+ ? majorTicks.slice(0, -1).flatMap((tick, segmentIndex) => {
8352
+ const base = tick.percent;
8353
+ const next = majorTicks[segmentIndex + 1].percent;
8359
8354
  const segment = [];
8360
8355
  for (let step = 1; step < safeMinorDivisions; step += 1) {
8361
8356
  segment.push(base + ((next - base) * step) / safeMinorDivisions);
8362
8357
  }
8363
8358
  return segment;
8364
- }).flat()
8359
+ })
8365
8360
  : [];
8366
8361
  return (jsxs(Box, { position: "absolute", inset: 0, pointerEvents: "none", zIndex: zIndex, children: [minorTicks.map((percent, index) => (jsx(Box, { position: "absolute", inset: 0, transform: `translateX(${percent}%)`, transition: transitionValue, children: jsx(Box, { position: "absolute", insetInlineStart: 0, top: 0, bottom: 0, width: "1px", bg: minorLineColor, _dark: { bg: 'gray.700' } }) }, `minor-grid-${index}`))), majorTicks.map((tick) => (jsx(Box, { position: "absolute", inset: 0, transform: `translateX(${tick.percent}%)`, transition: transitionValue, children: jsx(Box, { position: "absolute", insetInlineStart: 0, top: 0, bottom: 0, width: "1px", bg: majorLineColor, _dark: { bg: 'gray.600' } }) }, `major-grid-${tick.index}`)))] }));
8367
8362
  }
8368
- function TimeViewportBlocks({ blocks, viewportStart, viewportEnd, height = '28px', minWidthPx = 2, borderRadius = 'sm', defaultColorPalette = 'blue', showLabel = true, hideWhenOutOfView = true, hideEmptyTracks = true, gap = 2, allowOverlap = false, overlapOpacity = 0.9, renderTrackPrefix, renderTrackSuffix, onBlockClick, }) {
8363
+ function VirtualizedTrackList({ tracks, resolvedHeight, gap, virtualHeight, overscan, renderTrackPrefix, renderTrackSuffix, renderBlockNode, }) {
8364
+ const parentRef = useRef(null);
8365
+ const rowHeightPx = parseInt(resolvedHeight, 10) || 28;
8366
+ const gapPx = gap * 4; // Chakra spacing token to px (1 unit = 4px)
8367
+ const virtualizer = useVirtualizer({
8368
+ count: tracks.length,
8369
+ getScrollElement: () => parentRef.current,
8370
+ estimateSize: () => rowHeightPx + gapPx,
8371
+ overscan,
8372
+ });
8373
+ return (jsx(Box, { ref: parentRef, overflowY: "auto", height: `${virtualHeight}px`, children: jsx(Box, { height: `${virtualizer.getTotalSize()}px`, position: "relative", children: virtualizer.getVirtualItems().map((virtualRow) => {
8374
+ const track = tracks[virtualRow.index];
8375
+ const trackBlocks = track.blocks.map((item) => item.block);
8376
+ const prefix = renderTrackPrefix?.({
8377
+ trackIndex: virtualRow.index,
8378
+ trackBlocks,
8379
+ trackKey: track.trackKeyRaw,
8380
+ });
8381
+ const suffix = renderTrackSuffix?.({
8382
+ trackIndex: virtualRow.index,
8383
+ trackBlocks,
8384
+ trackKey: track.trackKeyRaw,
8385
+ });
8386
+ return (jsx(Box, { position: "absolute", top: 0, left: 0, width: "100%", transform: `translateY(${virtualRow.start}px)`, children: jsx(TimeViewportTrackRow, { trackKey: track.trackKey, blocks: track.blocks, resolvedHeight: resolvedHeight, prefix: prefix, suffix: suffix, renderBlockNode: renderBlockNode }) }, track.trackKey));
8387
+ }) }) }));
8388
+ }
8389
+ function TimeViewportBlocks({ blocks, viewportStart, viewportEnd, height = '28px', minWidthPx = 2, borderRadius = 'sm', defaultColorPalette = 'blue', showLabel = true, hideWhenOutOfView = true, hideEmptyTracks = true, gap = 2, allowOverlap = false, overlapOpacity = 0.9, renderTrackPrefix, renderTrackSuffix, onBlockClick, virtualize = false, virtualHeight = 400, overscan = 5, }) {
8369
8390
  const { getGeometry, toTimeMs } = useTimeViewportBlockGeometry(viewportStart, viewportEnd);
8370
8391
  const resolvedHeight = typeof height === 'number' ? `${height}px` : height;
8371
8392
  const expandedBlocks = flattenTrackBlocks(blocks);
@@ -8400,74 +8421,73 @@ function TimeViewportBlocks({ blocks, viewportStart, viewportEnd, height = '28px
8400
8421
  onBlockClick?.(block);
8401
8422
  };
8402
8423
  const isBlockClickable = Boolean(block.onClick || onBlockClick);
8403
- return (jsx(Box, { position: "absolute", inset: 0, pointerEvents: "none", children: jsx(Box, { width: "100%", height: "100%", transform: `translateX(${geometry.leftPercent}%)`, transition: VIEWPORT_TRANSITION, children: jsx(Box, { width: `max(${geometry.widthPercent}%, ${minWidthPx}px)`, height: "100%", borderRadius: borderRadius, bg: block.background ??
8404
- `${block.colorPalette ?? defaultColorPalette}.500`, _dark: {
8405
- bg: block.background ??
8406
- `${block.colorPalette ?? defaultColorPalette}.900`,
8407
- }, display: "flex", alignItems: "center", justifyContent: "center", px: 2, overflow: "hidden", opacity: allowOverlap ? overlapOpacity : 1, zIndex: indexInLayer + 1, pointerEvents: "auto", onClick: isBlockClickable ? handleBlockClick : undefined, cursor: isBlockClickable ? 'pointer' : 'default', children: showLabel && block.label ? (jsx(Text, { fontSize: "xs", lineClamp: 1, color: "white", _dark: { color: 'gray.100' }, children: block.label })) : null }) }) }, block.id));
8424
+ return (jsx(Box, { height: "100%", position: "absolute", inset: 0, pointerEvents: "none", transform: `translateX(${geometry.leftPercent}%)`, transition: VIEWPORT_TRANSITION, children: jsx(Box, { width: `max(${geometry.widthPercent}%, ${minWidthPx}px)`, height: "100%", borderRadius: borderRadius, bg: block.background ??
8425
+ `${block.colorPalette ?? defaultColorPalette}.500`, _dark: {
8426
+ bg: block.background ??
8427
+ `${block.colorPalette ?? defaultColorPalette}.900`,
8428
+ }, display: "flex", alignItems: "center", justifyContent: "center", px: 2, overflow: "hidden", opacity: allowOverlap ? overlapOpacity : 1, zIndex: indexInLayer + 1, pointerEvents: "auto", onClick: isBlockClickable ? handleBlockClick : undefined, cursor: isBlockClickable ? 'pointer' : 'default', children: showLabel && block.label ? (jsx(Text, { fontSize: "xs", lineClamp: 1, color: "white", _dark: { color: 'gray.100' }, children: block.label })) : null }) }, block.id));
8408
8429
  };
8430
+ // ---------- Resolve tracks ----------
8409
8431
  const explicitTrackKeys = Array.from(new Set(expandedBlocks
8410
8432
  .map((item) => item.track)
8411
8433
  .filter((track) => track !== undefined)));
8412
- if (explicitTrackKeys.length > 0) {
8413
- const tracks = explicitTrackKeys
8414
- .map((trackKey) => ({
8415
- trackKey,
8416
- blocks: parsedBlocks.filter((item) => item.block.track === trackKey),
8417
- }))
8418
- .filter((track) => !hideEmptyTracks || track.blocks.length > 0);
8419
- return (jsx(VStack, { align: "stretch", gap: gap, children: tracks.map((track, trackIndex) => {
8420
- const trackBlocks = track.blocks.map((item) => item.block);
8421
- const prefix = renderTrackPrefix?.({
8422
- trackIndex,
8423
- trackBlocks,
8424
- trackKey: track.trackKey,
8425
- });
8426
- const suffix = renderTrackSuffix?.({
8427
- trackIndex,
8428
- trackBlocks,
8429
- trackKey: track.trackKey,
8430
- });
8431
- return (jsx(TimeViewportTrackRow, { trackKey: `track-keyed-${String(track.trackKey)}`, blocks: track.blocks, resolvedHeight: resolvedHeight, prefix: prefix, suffix: suffix, renderBlockNode: renderBlockNode }));
8432
- }) }));
8434
+ const resolvedTracks = useMemo(() => {
8435
+ if (explicitTrackKeys.length > 0) {
8436
+ return explicitTrackKeys
8437
+ .map((trackKey) => ({
8438
+ trackKey: `track-keyed-${String(trackKey)}`,
8439
+ trackKeyRaw: trackKey,
8440
+ blocks: parsedBlocks.filter((item) => item.block.track === trackKey),
8441
+ }))
8442
+ .filter((track) => !hideEmptyTracks || track.blocks.length > 0);
8443
+ }
8444
+ const autoPackedTracks = (() => {
8445
+ const sortedBlocks = [...parsedBlocks]
8446
+ .filter(({ startMs, endMs }) => Number.isFinite(startMs) && Number.isFinite(endMs))
8447
+ .sort((a, b) => {
8448
+ if (a.startMs === b.startMs)
8449
+ return a.endMs - b.endMs;
8450
+ return a.startMs - b.startMs;
8451
+ });
8452
+ const trackLastEndTimes = [];
8453
+ const tracks = [];
8454
+ sortedBlocks.forEach((item) => {
8455
+ const trackIndex = trackLastEndTimes.findIndex((endMs) => item.startMs >= endMs);
8456
+ if (trackIndex === -1) {
8457
+ trackLastEndTimes.push(item.endMs);
8458
+ tracks.push([item]);
8459
+ }
8460
+ else {
8461
+ trackLastEndTimes[trackIndex] = item.endMs;
8462
+ tracks[trackIndex].push(item);
8463
+ }
8464
+ });
8465
+ return tracks;
8466
+ })();
8467
+ const packed = allowOverlap ? [parsedBlocks] : autoPackedTracks;
8468
+ return packed.map((track, trackIndex) => ({
8469
+ trackKey: `track-row-${trackIndex}`,
8470
+ trackKeyRaw: undefined,
8471
+ blocks: track,
8472
+ }));
8473
+ }, [allowOverlap, explicitTrackKeys, hideEmptyTracks, parsedBlocks]);
8474
+ // ---------- Render ----------
8475
+ if (virtualize) {
8476
+ return (jsx(VirtualizedTrackList, { tracks: resolvedTracks, resolvedHeight: resolvedHeight, gap: gap, virtualHeight: virtualHeight, overscan: overscan, renderTrackPrefix: renderTrackPrefix, renderTrackSuffix: renderTrackSuffix, renderBlockNode: renderBlockNode }));
8433
8477
  }
8434
- const autoPackedTracks = (() => {
8435
- const sortedBlocks = [...parsedBlocks]
8436
- .filter(({ startMs, endMs }) => Number.isFinite(startMs) && Number.isFinite(endMs))
8437
- .sort((a, b) => {
8438
- if (a.startMs === b.startMs)
8439
- return a.endMs - b.endMs;
8440
- return a.startMs - b.startMs;
8441
- });
8442
- const trackLastEndTimes = [];
8443
- const tracks = [];
8444
- sortedBlocks.forEach((item) => {
8445
- const trackIndex = trackLastEndTimes.findIndex((endMs) => item.startMs >= endMs);
8446
- if (trackIndex === -1) {
8447
- trackLastEndTimes.push(item.endMs);
8448
- tracks.push([item]);
8449
- }
8450
- else {
8451
- trackLastEndTimes[trackIndex] = item.endMs;
8452
- tracks[trackIndex].push(item);
8453
- }
8454
- });
8455
- return tracks;
8456
- })();
8457
- const tracks = allowOverlap ? [parsedBlocks] : autoPackedTracks;
8458
- return (jsx(VStack, { align: "stretch", gap: gap, children: tracks.map((track, trackIndex) => {
8459
- const trackBlocks = track.map((item) => item.block);
8478
+ return (jsx(VStack, { align: "stretch", gap: gap, children: resolvedTracks.map((track, trackIndex) => {
8479
+ const trackBlocks = track.blocks.map((item) => item.block);
8460
8480
  const prefix = renderTrackPrefix?.({
8461
8481
  trackIndex,
8462
8482
  trackBlocks,
8463
- trackKey: undefined,
8483
+ trackKey: track.trackKeyRaw,
8464
8484
  });
8465
8485
  const suffix = renderTrackSuffix?.({
8466
8486
  trackIndex,
8467
8487
  trackBlocks,
8468
- trackKey: undefined,
8488
+ trackKey: track.trackKeyRaw,
8469
8489
  });
8470
- return (jsx(TimeViewportTrackRow, { trackKey: `track-row-${trackIndex}`, blocks: track, resolvedHeight: resolvedHeight, prefix: prefix, suffix: suffix, renderBlockNode: renderBlockNode }));
8490
+ return (jsx(TimeViewportTrackRow, { trackKey: track.trackKey, blocks: track.blocks, resolvedHeight: resolvedHeight, prefix: prefix, suffix: suffix, renderBlockNode: renderBlockNode }));
8471
8491
  }) }));
8472
8492
  }
8473
8493
  function TimeRangeZoom({ range, onRangeChange, minDurationMs = DEFAULT_MIN_DURATION_MS, maxDurationMs = DEFAULT_MAX_DURATION_MS, zoomFactor = DEFAULT_ZOOM_FACTOR, resetDurationMs, showResetButton = true, disabled = false, labels, }) {
@@ -105,6 +105,10 @@ export interface TimeViewportGridProps {
105
105
  viewportStart?: TimeInput;
106
106
  viewportEnd?: TimeInput;
107
107
  tickCount?: number;
108
+ tickStrategy?: 'count' | 'timeUnit';
109
+ tickUnit?: 'minute' | 'hour' | 'day';
110
+ tickStep?: number;
111
+ format?: string;
108
112
  minorDivisions?: number;
109
113
  majorLineColor?: string;
110
114
  minorLineColor?: string;
@@ -138,6 +142,12 @@ export interface TimeViewportBlocksProps {
138
142
  trackKey?: string | number;
139
143
  }) => ReactNode;
140
144
  onBlockClick?: (block: TimeViewportBlockItem) => void;
145
+ /** Enable virtual scrolling for large track lists. */
146
+ virtualize?: boolean;
147
+ /** Fixed pixel height of the scroll container when virtualize is true. Defaults to 400. */
148
+ virtualHeight?: number;
149
+ /** Number of off-screen rows to render above/below the visible area. Defaults to 5. */
150
+ overscan?: number;
141
151
  }
142
152
  export interface TimeViewportRootProps {
143
153
  viewportStart: TimeInput;
@@ -220,8 +230,8 @@ export declare function TimeViewportHeader({ viewportStart, viewportEnd, tickCou
220
230
  * Vertical grid lines for measuring block positions in the viewport.
221
231
  * Render inside a relative container that also contains blocks.
222
232
  */
223
- export declare function TimeViewportGrid({ viewportStart, viewportEnd, tickCount, minorDivisions, majorLineColor, minorLineColor, showMinorLines, zIndex, animationDurationMs, animationEasing, }: TimeViewportGridProps): import("react/jsx-runtime").JSX.Element | null;
224
- export declare function TimeViewportBlocks({ blocks, viewportStart, viewportEnd, height, minWidthPx, borderRadius, defaultColorPalette, showLabel, hideWhenOutOfView, hideEmptyTracks, gap, allowOverlap, overlapOpacity, renderTrackPrefix, renderTrackSuffix, onBlockClick, }: TimeViewportBlocksProps): import("react/jsx-runtime").JSX.Element;
233
+ export declare function TimeViewportGrid({ viewportStart, viewportEnd, tickCount, tickStrategy, tickUnit, tickStep, format, minorDivisions, majorLineColor, minorLineColor, showMinorLines, zIndex, animationDurationMs, animationEasing, }: TimeViewportGridProps): import("react/jsx-runtime").JSX.Element | null;
234
+ export declare function TimeViewportBlocks({ blocks, viewportStart, viewportEnd, height, minWidthPx, borderRadius, defaultColorPalette, showLabel, hideWhenOutOfView, hideEmptyTracks, gap, allowOverlap, overlapOpacity, renderTrackPrefix, renderTrackSuffix, onBlockClick, virtualize, virtualHeight, overscan, }: TimeViewportBlocksProps): import("react/jsx-runtime").JSX.Element;
225
235
  export declare function TimeRangeZoom({ range, onRangeChange, minDurationMs, maxDurationMs, zoomFactor, resetDurationMs, showResetButton, disabled, labels, }: TimeRangeZoomProps): import("react/jsx-runtime").JSX.Element;
226
236
  export declare function useTimeRangeZoom({ range, onRangeChange, minDurationMs, maxDurationMs, zoomFactor, resetDurationMs, disabled, labels, }: TimeRangeZoomProps): UseTimeRangeZoomResult;
227
237
  export {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bsol-oss/react-datatable5",
3
- "version": "13.0.1-beta.33",
3
+ "version": "13.0.1-beta.34",
4
4
  "type": "module",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.mjs",
@@ -45,6 +45,7 @@
45
45
  "@tanstack/match-sorter-utils": "^8.15.1",
46
46
  "@tanstack/react-query": "^5.66.9",
47
47
  "@tanstack/react-table": "^8.21.2",
48
+ "@tanstack/react-virtual": "^3.13.0",
48
49
  "@uidotdev/usehooks": "^2.4.1",
49
50
  "ajv": "^8.12.0",
50
51
  "ajv-errors": "^3.0.0",
@@ -64,6 +65,7 @@
64
65
  "@storybook/addon-docs": "^10.0.7",
65
66
  "@storybook/addon-onboarding": "^10.0.7",
66
67
  "@storybook/react-vite": "^10.0.7",
68
+ "@tanstack/react-virtual": "^3.13.18",
67
69
  "@types/ajv-errors": "^2.0.0",
68
70
  "@types/json-schema": "^7.0.15",
69
71
  "@types/react": "19.0.2",