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

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;
@@ -1065,6 +1069,20 @@ interface TimeViewportGridProps {
1065
1069
  animationDurationMs?: number;
1066
1070
  animationEasing?: string;
1067
1071
  }
1072
+ interface TimeViewportBlockRenderArgs {
1073
+ block: TimeViewportBlockItem;
1074
+ geometry: {
1075
+ leftPercent: number;
1076
+ widthPercent: number;
1077
+ };
1078
+ index: number;
1079
+ }
1080
+ interface TimeViewportTrackRenderArgs {
1081
+ trackIndex: number;
1082
+ trackKey?: string | number;
1083
+ trackBlocks: TimeViewportBlockItem[];
1084
+ defaultContent: ReactNode;
1085
+ }
1068
1086
  interface TimeViewportBlocksProps {
1069
1087
  blocks: TimeViewportBlockItem[];
1070
1088
  viewportStart?: TimeInput;
@@ -1089,7 +1107,17 @@ interface TimeViewportBlocksProps {
1089
1107
  trackBlocks: TimeViewportBlockItem[];
1090
1108
  trackKey?: string | number;
1091
1109
  }) => ReactNode;
1110
+ /** Custom render function for block content. The returned node is placed inside a positioning wrapper that handles translateX and transitions. */
1111
+ renderBlock?: (args: TimeViewportBlockRenderArgs) => ReactNode;
1112
+ /** Custom render function for an entire track row. Receives the default rendered content so you can wrap or replace it. */
1113
+ renderTrack?: (args: TimeViewportTrackRenderArgs) => ReactNode;
1092
1114
  onBlockClick?: (block: TimeViewportBlockItem) => void;
1115
+ /** Enable virtual scrolling for large track lists. */
1116
+ virtualize?: boolean;
1117
+ /** Fixed pixel height of the scroll container when virtualize is true. Defaults to 400. */
1118
+ virtualHeight?: number;
1119
+ /** Number of off-screen rows to render above/below the visible area. Defaults to 5. */
1120
+ overscan?: number;
1093
1121
  }
1094
1122
  interface TimeViewportRootProps {
1095
1123
  viewportStart: TimeInput;
@@ -1172,8 +1200,8 @@ declare function TimeViewportHeader({ viewportStart, viewportEnd, tickCount, tic
1172
1200
  * Vertical grid lines for measuring block positions in the viewport.
1173
1201
  * Render inside a relative container that also contains blocks.
1174
1202
  */
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;
1203
+ declare function TimeViewportGrid({ viewportStart, viewportEnd, tickCount, tickStrategy, tickUnit, tickStep, format, minorDivisions, majorLineColor, minorLineColor, showMinorLines, zIndex, animationDurationMs, animationEasing, }: TimeViewportGridProps): react_jsx_runtime.JSX.Element | null;
1204
+ declare function TimeViewportBlocks({ blocks, viewportStart, viewportEnd, height, minWidthPx, borderRadius, defaultColorPalette, showLabel, hideWhenOutOfView, hideEmptyTracks, gap, allowOverlap, overlapOpacity, renderTrackPrefix, renderTrackSuffix, renderBlock, renderTrack, onBlockClick, virtualize, virtualHeight, overscan, }: TimeViewportBlocksProps): react_jsx_runtime.JSX.Element;
1177
1205
  declare function TimeRangeZoom({ range, onRangeChange, minDurationMs, maxDurationMs, zoomFactor, resetDurationMs, showResetButton, disabled, labels, }: TimeRangeZoomProps): react_jsx_runtime.JSX.Element;
1178
1206
  declare function useTimeRangeZoom({ range, onRangeChange, minDurationMs, maxDurationMs, zoomFactor, resetDurationMs, disabled, labels, }: TimeRangeZoomProps): UseTimeRangeZoomResult;
1179
1207
 
@@ -1450,4 +1478,4 @@ declare module '@tanstack/react-table' {
1450
1478
  }
1451
1479
  }
1452
1480
 
1453
- export { CalendarDisplay, type CalendarDisplayProps, type CalendarEvent, type CalendarProps, CardHeader, type CardHeaderProps, type CustomJSONSchema7, type CustomJSONSchema7Definition, type CustomQueryFn, type CustomQueryFnParams, type CustomQueryFnResponse, DataDisplay, type DataDisplayProps, type DataResponse, DataTable, type DataTableDefaultState, type DataTableProps, DataTableServer, type DataTableServerProps, DatePickerContext, DatePickerInput, type DatePickerInputProps, type DatePickerLabels, type DatePickerProps, type DateTimePickerLabels, DefaultCardTitle, DefaultForm, type DefaultFormProps, DefaultTable, type DefaultTableProps, DefaultTableServer, type DefaultTableServerProps, DensityToggleButton, type DensityToggleButtonProps, type EditFilterButtonProps, EditSortingButton, type EditSortingButtonProps, type EditViewButtonProps, EmptyState, type EmptyStateProps, type EnumPickerLabels, ErrorAlert, type ErrorAlertProps, type FilePickerLabels, type FilePickerMediaFile, type FilePickerProps, FilterDialog, FormBody, type FormButtonLabels, FormRoot, type FormRootProps, FormTitle, type GetDateColorProps, type GetMultiDatesProps, type GetRangeDatesProps, type GetStyleProps, type GetVariantProps, GlobalFilter, type IdPickerLabels, type LoadInitialValuesParams, type LoadInitialValuesResult, MediaLibraryBrowser, type MediaLibraryBrowserProps, PageSizeControl, type PageSizeControlProps, Pagination, type QueryParams, type RangeCalendarProps, type RangeDatePickerLabels, type RangeDatePickerProps, RecordDisplay, type RecordDisplayProps, ReloadButton, type ReloadButtonProps, ResetFilteringButton, ResetSelectionButton, ResetSortingButton, type Result, RowCountText, SelectAllRowsToggle, type SelectAllRowsToggleProps, Table, TableBody, type TableBodyProps, TableCardContainer, type TableCardContainerProps, TableCards, type TableCardsProps, TableComponent, TableControls, type TableControlsProps, TableDataDisplay, type TableDataDisplayProps, TableFilter, TableFilterTags, type TableFilterTagsProps, TableFooter, type TableFooterProps, TableHeader, type TableHeaderProps, type TableHeaderTexts, TableLoadingComponent, type TableLoadingComponentProps, type TableProps, type TableRendererProps, type TableRowSelectorProps, TableSelector, TableSorter, TableViewer, TextCell, type TextCellProps, type TimePickerLabels, TimeRangeZoom, type TimeRangeZoomLabels, type TimeRangeZoomProps, TimeViewportBlock, type TimeViewportBlockItem, type TimeViewportBlockProps, TimeViewportBlocks, type TimeViewportBlocksProps, TimeViewportGrid, type TimeViewportGridProps, TimeViewportHeader, type TimeViewportHeaderProps, type TimeViewportHeaderTick, TimeViewportMarkerLine, type TimeViewportMarkerLineProps, TimeViewportRoot, type TimeViewportRootProps, type UseDataTableProps, type UseDataTableReturn, type UseDataTableServerProps, type UseDataTableServerReturn, type UseFormProps, type UseTimeRangeZoomResult, type UseTimeViewportBlockGeometryResult, type UseTimeViewportDerivedResult, type UseTimeViewportTicksResult, type ValidationErrorType, ViewDialog, type ViewableTimeRange, defaultRenderDisplay, getMultiDates, getRangeDates, useDataTable, useDataTableContext, useDataTableServer, useForm, useTimeRangeZoom, useTimeViewport, useTimeViewportBlockGeometry, useTimeViewportHeader, useTimeViewportTicks };
1481
+ export { CalendarDisplay, type CalendarDisplayProps, type CalendarEvent, type CalendarProps, CardHeader, type CardHeaderProps, type CustomJSONSchema7, type CustomJSONSchema7Definition, type CustomQueryFn, type CustomQueryFnParams, type CustomQueryFnResponse, DataDisplay, type DataDisplayProps, type DataResponse, DataTable, type DataTableDefaultState, type DataTableProps, DataTableServer, type DataTableServerProps, DatePickerContext, DatePickerInput, type DatePickerInputProps, type DatePickerLabels, type DatePickerProps, type DateTimePickerLabels, DefaultCardTitle, DefaultForm, type DefaultFormProps, DefaultTable, type DefaultTableProps, DefaultTableServer, type DefaultTableServerProps, DensityToggleButton, type DensityToggleButtonProps, type EditFilterButtonProps, EditSortingButton, type EditSortingButtonProps, type EditViewButtonProps, EmptyState, type EmptyStateProps, type EnumPickerLabels, ErrorAlert, type ErrorAlertProps, type FilePickerLabels, type FilePickerMediaFile, type FilePickerProps, FilterDialog, FormBody, type FormButtonLabels, FormRoot, type FormRootProps, FormTitle, type GetDateColorProps, type GetMultiDatesProps, type GetRangeDatesProps, type GetStyleProps, type GetVariantProps, GlobalFilter, type IdPickerLabels, type LoadInitialValuesParams, type LoadInitialValuesResult, MediaLibraryBrowser, type MediaLibraryBrowserProps, PageSizeControl, type PageSizeControlProps, Pagination, type QueryParams, type RangeCalendarProps, type RangeDatePickerLabels, type RangeDatePickerProps, RecordDisplay, type RecordDisplayProps, ReloadButton, type ReloadButtonProps, ResetFilteringButton, ResetSelectionButton, ResetSortingButton, type Result, RowCountText, SelectAllRowsToggle, type SelectAllRowsToggleProps, Table, TableBody, type TableBodyProps, TableCardContainer, type TableCardContainerProps, TableCards, type TableCardsProps, TableComponent, TableControls, type TableControlsProps, TableDataDisplay, type TableDataDisplayProps, TableFilter, TableFilterTags, type TableFilterTagsProps, TableFooter, type TableFooterProps, TableHeader, type TableHeaderProps, type TableHeaderTexts, TableLoadingComponent, type TableLoadingComponentProps, type TableProps, type TableRendererProps, type TableRowSelectorProps, TableSelector, TableSorter, TableViewer, TextCell, type TextCellProps, type TimePickerLabels, TimeRangeZoom, type TimeRangeZoomLabels, type TimeRangeZoomProps, TimeViewportBlock, type TimeViewportBlockItem, type TimeViewportBlockProps, type TimeViewportBlockRenderArgs, TimeViewportBlocks, type TimeViewportBlocksProps, TimeViewportGrid, type TimeViewportGridProps, TimeViewportHeader, type TimeViewportHeaderProps, type TimeViewportHeaderTick, TimeViewportMarkerLine, type TimeViewportMarkerLineProps, TimeViewportRoot, type TimeViewportRootProps, type TimeViewportTrackRenderArgs, type UseDataTableProps, type UseDataTableReturn, type UseDataTableServerProps, type UseDataTableServerReturn, type UseFormProps, type UseTimeRangeZoomResult, type UseTimeViewportBlockGeometryResult, type UseTimeViewportDerivedResult, type UseTimeViewportTicksResult, type ValidationErrorType, ViewDialog, type ViewableTimeRange, defaultRenderDisplay, getMultiDates, getRangeDates, useDataTable, useDataTableContext, useDataTableServer, useForm, useTimeRangeZoom, useTimeViewport, useTimeViewportBlockGeometry, useTimeViewportHeader, useTimeViewportTicks };
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.Box, { width: "100%", overflowX: 'hidden', position: "relative", height: resolvedHeight, children: [jsxRuntime.jsx(react.Box, { position: "relative", width: "100%", height: "100%", children: blocks.map((item, index) => renderBlockNode(item, index)) }), prefix ? (jsxRuntime.jsx(react.Box, { position: "absolute", top: 0, insetInlineStart: 0, zIndex: 2, pointerEvents: "auto", children: prefix })) : null, suffix ? (jsxRuntime.jsx(react.Box, { position: "absolute", top: 0, insetInlineEnd: 0, zIndex: 2, pointerEvents: "auto", children: suffix })) : null] }, trackKey));
8099
8100
  }
8100
8101
  const defaultLabels = {
8101
8102
  zoomIn: 'Zoom in',
@@ -8333,7 +8334,7 @@ function TimeViewportMarkerLine({ timestamp, viewportStart, viewportEnd, height
8333
8334
  return null;
8334
8335
  if (hideWhenOutOfView && !marker.inView)
8335
8336
  return null;
8336
- return (jsxRuntime.jsx(react.Box, { position: "absolute", insetInlineStart: 0, insetInlineEnd: 0, top: 0, bottom: 0, pointerEvents: "none", zIndex: 100, height: height, children: jsxRuntime.jsxs(react.Box, { width: "100%", height: "100%", transform: `translateX(${marker.percent}%)`, transition: VIEWPORT_TRANSITION, transformOrigin: "left center", children: [jsxRuntime.jsx(react.Box, { width: `${lineWidthPx}px`, height: "100%", bg: color ?? `${colorPalette}.500`, _dark: { bg: color ?? `${colorPalette}.300` }, transform: "translateX(-50%)" }), showLabel && label ? (jsxRuntime.jsx(react.Text, { position: "absolute", insetInlineStart: 0, top: "100%", mt: 1, display: "inline-block", fontSize: "xs", whiteSpace: "nowrap", color: color ?? `${colorPalette}.700`, _dark: { color: color ?? `${colorPalette}.200` }, transform: "translateX(-50%)", children: label })) : null] }) }));
8337
+ return (jsxRuntime.jsx(react.Box, { position: "absolute", insetInlineStart: 0, insetInlineEnd: 0, top: 0, bottom: 0, pointerEvents: "none", zIndex: 100, height: height, children: jsxRuntime.jsxs(react.Box, { width: "100%", height: "100%", transform: `translateX(${marker.percent}%)`, transition: VIEWPORT_TRANSITION, transformOrigin: "left center", children: [jsxRuntime.jsx(react.Box, { width: `${lineWidthPx}px`, height: "100%", bg: color ?? `${colorPalette}.500`, _dark: { bg: color ?? `${colorPalette}.500` }, transform: "translateX(-50%)" }), showLabel && label ? (jsxRuntime.jsx(react.Text, { position: "absolute", insetInlineStart: 0, top: "100%", mt: 1, display: "inline-block", fontSize: "xs", whiteSpace: "nowrap", color: color ?? `${colorPalette}.700`, _dark: { color: color ?? `${colorPalette}.500` }, transform: "translateX(-50%)", children: label })) : null] }) }));
8337
8338
  }
8338
8339
  /**
8339
8340
  * Header labels for timeline viewport time scale.
@@ -8357,35 +8358,64 @@ 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, renderTrack, 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
+ const defaultContent = (jsxRuntime.jsx(TimeViewportTrackRow, { trackKey: track.trackKey, blocks: track.blocks, resolvedHeight: resolvedHeight, prefix: prefix, suffix: suffix, renderBlockNode: renderBlockNode }));
8407
+ const trackContent = renderTrack
8408
+ ? renderTrack({
8409
+ trackIndex: virtualRow.index,
8410
+ trackKey: track.trackKeyRaw,
8411
+ trackBlocks,
8412
+ defaultContent,
8413
+ })
8414
+ : defaultContent;
8415
+ return (jsxRuntime.jsx(react.Box, { position: "absolute", top: 0, left: 0, width: "100%", transform: `translateY(${virtualRow.start}px)`, children: trackContent }, track.trackKey));
8416
+ }) }) }));
8417
+ }
8418
+ 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, renderBlock, renderTrack, onBlockClick, virtualize = false, virtualHeight = 400, overscan = 5, }) {
8389
8419
  const { getGeometry, toTimeMs } = useTimeViewportBlockGeometry(viewportStart, viewportEnd);
8390
8420
  const resolvedHeight = typeof height === 'number' ? `${height}px` : height;
8391
8421
  const expandedBlocks = flattenTrackBlocks(blocks);
@@ -8420,74 +8450,81 @@ function TimeViewportBlocks({ blocks, viewportStart, viewportEnd, height = '28px
8420
8450
  onBlockClick?.(block);
8421
8451
  };
8422
8452
  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));
8453
+ const content = renderBlock ? (renderBlock({ block, geometry, index: indexInLayer })) : (jsxRuntime.jsx(react.Box, { width: `max(${geometry.widthPercent}%, ${minWidthPx}px)`, height: "100%", borderRadius: borderRadius, bg: block.background ?? `${block.colorPalette ?? defaultColorPalette}.500`, _dark: {
8454
+ bg: block.background ??
8455
+ `${block.colorPalette ?? defaultColorPalette}.900`,
8456
+ }, 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, children: block.label })) : null }));
8457
+ return (jsxRuntime.jsx(react.Box, { height: "100%", position: "absolute", inset: 0, pointerEvents: "none", transform: `translateX(${geometry.leftPercent}%)`, transition: VIEWPORT_TRANSITION, children: content }, block.id));
8428
8458
  };
8459
+ // ---------- Resolve tracks ----------
8429
8460
  const explicitTrackKeys = Array.from(new Set(expandedBlocks
8430
8461
  .map((item) => item.track)
8431
8462
  .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
- }) }));
8463
+ const resolvedTracks = React.useMemo(() => {
8464
+ if (explicitTrackKeys.length > 0) {
8465
+ return explicitTrackKeys
8466
+ .map((trackKey) => ({
8467
+ trackKey: `track-keyed-${String(trackKey)}`,
8468
+ trackKeyRaw: trackKey,
8469
+ blocks: parsedBlocks.filter((item) => item.block.track === trackKey),
8470
+ }))
8471
+ .filter((track) => !hideEmptyTracks || track.blocks.length > 0);
8472
+ }
8473
+ const autoPackedTracks = (() => {
8474
+ const sortedBlocks = [...parsedBlocks]
8475
+ .filter(({ startMs, endMs }) => Number.isFinite(startMs) && Number.isFinite(endMs))
8476
+ .sort((a, b) => {
8477
+ if (a.startMs === b.startMs)
8478
+ return a.endMs - b.endMs;
8479
+ return a.startMs - b.startMs;
8480
+ });
8481
+ const trackLastEndTimes = [];
8482
+ const tracks = [];
8483
+ sortedBlocks.forEach((item) => {
8484
+ const trackIndex = trackLastEndTimes.findIndex((endMs) => item.startMs >= endMs);
8485
+ if (trackIndex === -1) {
8486
+ trackLastEndTimes.push(item.endMs);
8487
+ tracks.push([item]);
8488
+ }
8489
+ else {
8490
+ trackLastEndTimes[trackIndex] = item.endMs;
8491
+ tracks[trackIndex].push(item);
8492
+ }
8493
+ });
8494
+ return tracks;
8495
+ })();
8496
+ const packed = allowOverlap ? [parsedBlocks] : autoPackedTracks;
8497
+ return packed.map((track, trackIndex) => ({
8498
+ trackKey: `track-row-${trackIndex}`,
8499
+ trackKeyRaw: undefined,
8500
+ blocks: track,
8501
+ }));
8502
+ }, [allowOverlap, explicitTrackKeys, hideEmptyTracks, parsedBlocks]);
8503
+ // ---------- Render ----------
8504
+ if (virtualize) {
8505
+ return (jsxRuntime.jsx(VirtualizedTrackList, { tracks: resolvedTracks, resolvedHeight: resolvedHeight, gap: gap, virtualHeight: virtualHeight, overscan: overscan, renderTrackPrefix: renderTrackPrefix, renderTrackSuffix: renderTrackSuffix, renderTrack: renderTrack, renderBlockNode: renderBlockNode }));
8453
8506
  }
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);
8507
+ return (jsxRuntime.jsx(react.VStack, { align: "stretch", gap: gap, children: resolvedTracks.map((track, trackIndex) => {
8508
+ const trackBlocks = track.blocks.map((item) => item.block);
8480
8509
  const prefix = renderTrackPrefix?.({
8481
8510
  trackIndex,
8482
8511
  trackBlocks,
8483
- trackKey: undefined,
8512
+ trackKey: track.trackKeyRaw,
8484
8513
  });
8485
8514
  const suffix = renderTrackSuffix?.({
8486
8515
  trackIndex,
8487
8516
  trackBlocks,
8488
- trackKey: undefined,
8517
+ trackKey: track.trackKeyRaw,
8489
8518
  });
8490
- return (jsxRuntime.jsx(TimeViewportTrackRow, { trackKey: `track-row-${trackIndex}`, blocks: track, resolvedHeight: resolvedHeight, prefix: prefix, suffix: suffix, renderBlockNode: renderBlockNode }));
8519
+ const defaultContent = (jsxRuntime.jsx(TimeViewportTrackRow, { trackKey: track.trackKey, blocks: track.blocks, resolvedHeight: resolvedHeight, prefix: prefix, suffix: suffix, renderBlockNode: renderBlockNode }));
8520
+ return renderTrack
8521
+ ? renderTrack({
8522
+ trackIndex,
8523
+ trackKey: track.trackKeyRaw,
8524
+ trackBlocks,
8525
+ defaultContent,
8526
+ })
8527
+ : defaultContent;
8491
8528
  }) }));
8492
8529
  }
8493
8530
  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(Box, { width: "100%", overflowX: 'hidden', position: "relative", height: resolvedHeight, children: [jsx(Box, { position: "relative", width: "100%", height: "100%", children: blocks.map((item, index) => renderBlockNode(item, index)) }), prefix ? (jsx(Box, { position: "absolute", top: 0, insetInlineStart: 0, zIndex: 2, pointerEvents: "auto", children: prefix })) : null, suffix ? (jsx(Box, { position: "absolute", top: 0, insetInlineEnd: 0, zIndex: 2, pointerEvents: "auto", children: suffix })) : null] }, trackKey));
8079
8080
  }
8080
8081
  const defaultLabels = {
8081
8082
  zoomIn: 'Zoom in',
@@ -8313,7 +8314,7 @@ function TimeViewportMarkerLine({ timestamp, viewportStart, viewportEnd, height
8313
8314
  return null;
8314
8315
  if (hideWhenOutOfView && !marker.inView)
8315
8316
  return null;
8316
- return (jsx(Box, { position: "absolute", insetInlineStart: 0, insetInlineEnd: 0, top: 0, bottom: 0, pointerEvents: "none", zIndex: 100, height: height, children: jsxs(Box, { width: "100%", height: "100%", transform: `translateX(${marker.percent}%)`, transition: VIEWPORT_TRANSITION, transformOrigin: "left center", children: [jsx(Box, { width: `${lineWidthPx}px`, height: "100%", bg: color ?? `${colorPalette}.500`, _dark: { bg: color ?? `${colorPalette}.300` }, transform: "translateX(-50%)" }), showLabel && label ? (jsx(Text, { position: "absolute", insetInlineStart: 0, top: "100%", mt: 1, display: "inline-block", fontSize: "xs", whiteSpace: "nowrap", color: color ?? `${colorPalette}.700`, _dark: { color: color ?? `${colorPalette}.200` }, transform: "translateX(-50%)", children: label })) : null] }) }));
8317
+ return (jsx(Box, { position: "absolute", insetInlineStart: 0, insetInlineEnd: 0, top: 0, bottom: 0, pointerEvents: "none", zIndex: 100, height: height, children: jsxs(Box, { width: "100%", height: "100%", transform: `translateX(${marker.percent}%)`, transition: VIEWPORT_TRANSITION, transformOrigin: "left center", children: [jsx(Box, { width: `${lineWidthPx}px`, height: "100%", bg: color ?? `${colorPalette}.500`, _dark: { bg: color ?? `${colorPalette}.500` }, transform: "translateX(-50%)" }), showLabel && label ? (jsx(Text, { position: "absolute", insetInlineStart: 0, top: "100%", mt: 1, display: "inline-block", fontSize: "xs", whiteSpace: "nowrap", color: color ?? `${colorPalette}.700`, _dark: { color: color ?? `${colorPalette}.500` }, transform: "translateX(-50%)", children: label })) : null] }) }));
8317
8318
  }
8318
8319
  /**
8319
8320
  * Header labels for timeline viewport time scale.
@@ -8337,35 +8338,64 @@ 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, renderTrack, 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
+ const defaultContent = (jsx(TimeViewportTrackRow, { trackKey: track.trackKey, blocks: track.blocks, resolvedHeight: resolvedHeight, prefix: prefix, suffix: suffix, renderBlockNode: renderBlockNode }));
8387
+ const trackContent = renderTrack
8388
+ ? renderTrack({
8389
+ trackIndex: virtualRow.index,
8390
+ trackKey: track.trackKeyRaw,
8391
+ trackBlocks,
8392
+ defaultContent,
8393
+ })
8394
+ : defaultContent;
8395
+ return (jsx(Box, { position: "absolute", top: 0, left: 0, width: "100%", transform: `translateY(${virtualRow.start}px)`, children: trackContent }, track.trackKey));
8396
+ }) }) }));
8397
+ }
8398
+ 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, renderBlock, renderTrack, onBlockClick, virtualize = false, virtualHeight = 400, overscan = 5, }) {
8369
8399
  const { getGeometry, toTimeMs } = useTimeViewportBlockGeometry(viewportStart, viewportEnd);
8370
8400
  const resolvedHeight = typeof height === 'number' ? `${height}px` : height;
8371
8401
  const expandedBlocks = flattenTrackBlocks(blocks);
@@ -8400,74 +8430,81 @@ function TimeViewportBlocks({ blocks, viewportStart, viewportEnd, height = '28px
8400
8430
  onBlockClick?.(block);
8401
8431
  };
8402
8432
  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));
8433
+ const content = renderBlock ? (renderBlock({ block, geometry, index: indexInLayer })) : (jsx(Box, { width: `max(${geometry.widthPercent}%, ${minWidthPx}px)`, height: "100%", borderRadius: borderRadius, bg: block.background ?? `${block.colorPalette ?? defaultColorPalette}.500`, _dark: {
8434
+ bg: block.background ??
8435
+ `${block.colorPalette ?? defaultColorPalette}.900`,
8436
+ }, 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, children: block.label })) : null }));
8437
+ return (jsx(Box, { height: "100%", position: "absolute", inset: 0, pointerEvents: "none", transform: `translateX(${geometry.leftPercent}%)`, transition: VIEWPORT_TRANSITION, children: content }, block.id));
8408
8438
  };
8439
+ // ---------- Resolve tracks ----------
8409
8440
  const explicitTrackKeys = Array.from(new Set(expandedBlocks
8410
8441
  .map((item) => item.track)
8411
8442
  .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
- }) }));
8443
+ const resolvedTracks = useMemo(() => {
8444
+ if (explicitTrackKeys.length > 0) {
8445
+ return explicitTrackKeys
8446
+ .map((trackKey) => ({
8447
+ trackKey: `track-keyed-${String(trackKey)}`,
8448
+ trackKeyRaw: trackKey,
8449
+ blocks: parsedBlocks.filter((item) => item.block.track === trackKey),
8450
+ }))
8451
+ .filter((track) => !hideEmptyTracks || track.blocks.length > 0);
8452
+ }
8453
+ const autoPackedTracks = (() => {
8454
+ const sortedBlocks = [...parsedBlocks]
8455
+ .filter(({ startMs, endMs }) => Number.isFinite(startMs) && Number.isFinite(endMs))
8456
+ .sort((a, b) => {
8457
+ if (a.startMs === b.startMs)
8458
+ return a.endMs - b.endMs;
8459
+ return a.startMs - b.startMs;
8460
+ });
8461
+ const trackLastEndTimes = [];
8462
+ const tracks = [];
8463
+ sortedBlocks.forEach((item) => {
8464
+ const trackIndex = trackLastEndTimes.findIndex((endMs) => item.startMs >= endMs);
8465
+ if (trackIndex === -1) {
8466
+ trackLastEndTimes.push(item.endMs);
8467
+ tracks.push([item]);
8468
+ }
8469
+ else {
8470
+ trackLastEndTimes[trackIndex] = item.endMs;
8471
+ tracks[trackIndex].push(item);
8472
+ }
8473
+ });
8474
+ return tracks;
8475
+ })();
8476
+ const packed = allowOverlap ? [parsedBlocks] : autoPackedTracks;
8477
+ return packed.map((track, trackIndex) => ({
8478
+ trackKey: `track-row-${trackIndex}`,
8479
+ trackKeyRaw: undefined,
8480
+ blocks: track,
8481
+ }));
8482
+ }, [allowOverlap, explicitTrackKeys, hideEmptyTracks, parsedBlocks]);
8483
+ // ---------- Render ----------
8484
+ if (virtualize) {
8485
+ return (jsx(VirtualizedTrackList, { tracks: resolvedTracks, resolvedHeight: resolvedHeight, gap: gap, virtualHeight: virtualHeight, overscan: overscan, renderTrackPrefix: renderTrackPrefix, renderTrackSuffix: renderTrackSuffix, renderTrack: renderTrack, renderBlockNode: renderBlockNode }));
8433
8486
  }
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);
8487
+ return (jsx(VStack, { align: "stretch", gap: gap, children: resolvedTracks.map((track, trackIndex) => {
8488
+ const trackBlocks = track.blocks.map((item) => item.block);
8460
8489
  const prefix = renderTrackPrefix?.({
8461
8490
  trackIndex,
8462
8491
  trackBlocks,
8463
- trackKey: undefined,
8492
+ trackKey: track.trackKeyRaw,
8464
8493
  });
8465
8494
  const suffix = renderTrackSuffix?.({
8466
8495
  trackIndex,
8467
8496
  trackBlocks,
8468
- trackKey: undefined,
8497
+ trackKey: track.trackKeyRaw,
8469
8498
  });
8470
- return (jsx(TimeViewportTrackRow, { trackKey: `track-row-${trackIndex}`, blocks: track, resolvedHeight: resolvedHeight, prefix: prefix, suffix: suffix, renderBlockNode: renderBlockNode }));
8499
+ const defaultContent = (jsx(TimeViewportTrackRow, { trackKey: track.trackKey, blocks: track.blocks, resolvedHeight: resolvedHeight, prefix: prefix, suffix: suffix, renderBlockNode: renderBlockNode }));
8500
+ return renderTrack
8501
+ ? renderTrack({
8502
+ trackIndex,
8503
+ trackKey: track.trackKeyRaw,
8504
+ trackBlocks,
8505
+ defaultContent,
8506
+ })
8507
+ : defaultContent;
8471
8508
  }) }));
8472
8509
  }
8473
8510
  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;
@@ -113,6 +117,20 @@ export interface TimeViewportGridProps {
113
117
  animationDurationMs?: number;
114
118
  animationEasing?: string;
115
119
  }
120
+ export interface TimeViewportBlockRenderArgs {
121
+ block: TimeViewportBlockItem;
122
+ geometry: {
123
+ leftPercent: number;
124
+ widthPercent: number;
125
+ };
126
+ index: number;
127
+ }
128
+ export interface TimeViewportTrackRenderArgs {
129
+ trackIndex: number;
130
+ trackKey?: string | number;
131
+ trackBlocks: TimeViewportBlockItem[];
132
+ defaultContent: ReactNode;
133
+ }
116
134
  export interface TimeViewportBlocksProps {
117
135
  blocks: TimeViewportBlockItem[];
118
136
  viewportStart?: TimeInput;
@@ -137,7 +155,17 @@ export interface TimeViewportBlocksProps {
137
155
  trackBlocks: TimeViewportBlockItem[];
138
156
  trackKey?: string | number;
139
157
  }) => ReactNode;
158
+ /** Custom render function for block content. The returned node is placed inside a positioning wrapper that handles translateX and transitions. */
159
+ renderBlock?: (args: TimeViewportBlockRenderArgs) => ReactNode;
160
+ /** Custom render function for an entire track row. Receives the default rendered content so you can wrap or replace it. */
161
+ renderTrack?: (args: TimeViewportTrackRenderArgs) => ReactNode;
140
162
  onBlockClick?: (block: TimeViewportBlockItem) => void;
163
+ /** Enable virtual scrolling for large track lists. */
164
+ virtualize?: boolean;
165
+ /** Fixed pixel height of the scroll container when virtualize is true. Defaults to 400. */
166
+ virtualHeight?: number;
167
+ /** Number of off-screen rows to render above/below the visible area. Defaults to 5. */
168
+ overscan?: number;
141
169
  }
142
170
  export interface TimeViewportRootProps {
143
171
  viewportStart: TimeInput;
@@ -220,8 +248,8 @@ export declare function TimeViewportHeader({ viewportStart, viewportEnd, tickCou
220
248
  * Vertical grid lines for measuring block positions in the viewport.
221
249
  * Render inside a relative container that also contains blocks.
222
250
  */
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;
251
+ 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;
252
+ export declare function TimeViewportBlocks({ blocks, viewportStart, viewportEnd, height, minWidthPx, borderRadius, defaultColorPalette, showLabel, hideWhenOutOfView, hideEmptyTracks, gap, allowOverlap, overlapOpacity, renderTrackPrefix, renderTrackSuffix, renderBlock, renderTrack, onBlockClick, virtualize, virtualHeight, overscan, }: TimeViewportBlocksProps): import("react/jsx-runtime").JSX.Element;
225
253
  export declare function TimeRangeZoom({ range, onRangeChange, minDurationMs, maxDurationMs, zoomFactor, resetDurationMs, showResetButton, disabled, labels, }: TimeRangeZoomProps): import("react/jsx-runtime").JSX.Element;
226
254
  export declare function useTimeRangeZoom({ range, onRangeChange, minDurationMs, maxDurationMs, zoomFactor, resetDurationMs, disabled, labels, }: TimeRangeZoomProps): UseTimeRangeZoomResult;
227
255
  export {};
@@ -6,4 +6,4 @@ export { TimeRangeZoom, TimeViewportRoot, TimeViewportBlock, TimeViewportBlocks,
6
6
  export { UniversalPicker } from './UniversalPicker';
7
7
  export { DatePickerInput } from './DatePicker';
8
8
  export type { DatePickerProps, CalendarProps, GetDateColorProps, GetVariantProps, } from './DatePicker';
9
- export type { ViewableTimeRange, TimeRangeZoomLabels, TimeRangeZoomProps, TimeViewportRootProps, TimeViewportBlockProps, TimeViewportBlockItem, TimeViewportBlocksProps, TimeViewportGridProps, TimeViewportHeaderProps, TimeViewportMarkerLineProps, } from './TimeRangeZoom';
9
+ export type { ViewableTimeRange, TimeRangeZoomLabels, TimeRangeZoomProps, TimeViewportRootProps, TimeViewportBlockProps, TimeViewportBlockItem, TimeViewportBlocksProps, TimeViewportBlockRenderArgs, TimeViewportTrackRenderArgs, TimeViewportGridProps, TimeViewportHeaderProps, TimeViewportMarkerLineProps, } from './TimeRangeZoom';
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.35",
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",