@bsol-oss/react-datatable5 13.0.1-beta.32 → 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.mjs CHANGED
@@ -3,7 +3,7 @@ import { Button as Button$1, AbsoluteCenter, Spinner, Span, IconButton, Portal,
3
3
  import { AiOutlineColumnWidth } from 'react-icons/ai';
4
4
  import * as React from 'react';
5
5
  import { createContext, useContext, useState, useMemo, useCallback, useEffect, useRef } from 'react';
6
- import { LuX, LuCheck, LuChevronRight, LuCopy, LuExternalLink, LuSearch, LuImage, LuFile } from 'react-icons/lu';
6
+ import { LuX, LuCheck, LuChevronRight, LuCopy, LuExternalLink, LuSearch, LuImage, LuFile, LuZoomOut, LuZoomIn } from 'react-icons/lu';
7
7
  import { MdOutlineSort, MdFilterAlt, MdSearch, MdOutlineChecklist, MdClear, MdFilterList, MdOutlineViewColumn, MdFilterListAlt, MdPushPin, MdCancel, MdDateRange } from 'react-icons/md';
8
8
  import { FaUpDown, FaGripLinesVertical } from 'react-icons/fa6';
9
9
  import { BiDownArrow, BiUpArrow, BiX, BiError } from 'react-icons/bi';
@@ -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({
@@ -7967,6 +7968,610 @@ const getMultiDates = ({ selected, selectedDate, selectedDates, selectable, }) =
7967
7968
  }
7968
7969
  };
7969
7970
 
7971
+ const VIEWPORT_TRANSITION_EASING = 'cubic-bezier(0.25, 0.46, 0.45, 0.94)';
7972
+ const VIEWPORT_TRANSITION_DURATION_MS = 220;
7973
+ const VIEWPORT_TRANSITION = `transform ${VIEWPORT_TRANSITION_DURATION_MS}ms ${VIEWPORT_TRANSITION_EASING}, opacity ${VIEWPORT_TRANSITION_DURATION_MS}ms ${VIEWPORT_TRANSITION_EASING}`;
7974
+ const TimeViewportContext = createContext(null);
7975
+ const useResolvedViewport = (viewportStart, viewportEnd) => {
7976
+ const context = useContext(TimeViewportContext);
7977
+ const resolvedStart = viewportStart ?? context?.viewportStart;
7978
+ const resolvedEnd = viewportEnd ?? context?.viewportEnd;
7979
+ if (resolvedStart === undefined || resolvedEnd === undefined)
7980
+ return null;
7981
+ return {
7982
+ viewportStart: resolvedStart,
7983
+ viewportEnd: resolvedEnd,
7984
+ };
7985
+ };
7986
+ function TimeViewportRoot({ viewportStart, viewportEnd, children, onViewportChange, enableDragPan = false, enableCtrlWheelZoom = false, wheelZoomFactor = 1.2, minDurationMs = 60 * 1000, maxDurationMs = 365 * 24 * 60 * 60 * 1000, }) {
7987
+ const containerRef = useRef(null);
7988
+ const [isDragging, setIsDragging] = useState(false);
7989
+ const dragRef = useRef(null);
7990
+ const parseViewport = useCallback(() => {
7991
+ const start = parseTimeInput(viewportStart);
7992
+ const end = parseTimeInput(viewportEnd);
7993
+ if (!start || !end || !end.isAfter(start))
7994
+ return null;
7995
+ return {
7996
+ startMs: start.valueOf(),
7997
+ endMs: end.valueOf(),
7998
+ };
7999
+ }, [viewportEnd, viewportStart]);
8000
+ const handlePointerDown = (e) => {
8001
+ if (!enableDragPan || !onViewportChange)
8002
+ return;
8003
+ if (e.button !== 0)
8004
+ return;
8005
+ const parsed = parseViewport();
8006
+ if (!parsed)
8007
+ return;
8008
+ dragRef.current = {
8009
+ pointerId: e.pointerId,
8010
+ startX: e.clientX,
8011
+ viewportStartMs: parsed.startMs,
8012
+ viewportEndMs: parsed.endMs,
8013
+ };
8014
+ setIsDragging(true);
8015
+ e.currentTarget.setPointerCapture(e.pointerId);
8016
+ };
8017
+ const handlePointerMove = (e) => {
8018
+ if (!enableDragPan || !onViewportChange)
8019
+ return;
8020
+ const dragging = dragRef.current;
8021
+ if (!dragging || dragging.pointerId !== e.pointerId)
8022
+ return;
8023
+ const width = containerRef.current?.clientWidth ?? 0;
8024
+ if (width <= 0)
8025
+ return;
8026
+ const deltaX = e.clientX - dragging.startX;
8027
+ const durationMs = dragging.viewportEndMs - dragging.viewportStartMs;
8028
+ const shiftMs = (-deltaX / width) * durationMs;
8029
+ onViewportChange({
8030
+ start: dayjs(dragging.viewportStartMs + shiftMs).toDate(),
8031
+ end: dayjs(dragging.viewportEndMs + shiftMs).toDate(),
8032
+ });
8033
+ };
8034
+ const stopDragging = (e) => {
8035
+ const dragging = dragRef.current;
8036
+ if (!dragging || dragging.pointerId !== e.pointerId)
8037
+ return;
8038
+ dragRef.current = null;
8039
+ setIsDragging(false);
8040
+ if (e.currentTarget.hasPointerCapture(e.pointerId)) {
8041
+ e.currentTarget.releasePointerCapture(e.pointerId);
8042
+ }
8043
+ };
8044
+ const handleWheel = (e) => {
8045
+ if (!e.ctrlKey)
8046
+ return;
8047
+ // Prevent browser-level Ctrl/Cmd + wheel page zoom while interacting
8048
+ // with the timeline surface.
8049
+ e.preventDefault();
8050
+ if (!enableCtrlWheelZoom || !onViewportChange)
8051
+ return;
8052
+ const parsed = parseViewport();
8053
+ if (!parsed)
8054
+ return;
8055
+ const width = containerRef.current?.clientWidth ?? 0;
8056
+ if (width <= 0)
8057
+ return;
8058
+ const safeFactor = wheelZoomFactor > 1 ? wheelZoomFactor : 1.2;
8059
+ const durationMs = parsed.endMs - parsed.startMs;
8060
+ // Exponential zoom curve: each wheel "step" compounds by safeFactor.
8061
+ // This keeps zooming smooth on trackpads and predictable on mouse wheels.
8062
+ const wheelStep = e.deltaY / 100;
8063
+ const zoomMultiplier = Math.pow(safeFactor, wheelStep);
8064
+ const nextDuration = clampNumber(durationMs * (Number.isFinite(zoomMultiplier) ? zoomMultiplier : 1), minDurationMs, maxDurationMs);
8065
+ const rect = containerRef.current?.getBoundingClientRect();
8066
+ const x = rect ? e.clientX - rect.left : width / 2;
8067
+ const ratio = clampNumber(x / width, 0, 1);
8068
+ const anchorMs = parsed.startMs + durationMs * ratio;
8069
+ const nextStartMs = anchorMs - nextDuration * ratio;
8070
+ const nextEndMs = anchorMs + nextDuration * (1 - ratio);
8071
+ onViewportChange({
8072
+ start: dayjs(nextStartMs).toDate(),
8073
+ end: dayjs(nextEndMs).toDate(),
8074
+ });
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 }) }));
8077
+ }
8078
+ function TimeViewportTrackRow({ trackKey, blocks, resolvedHeight, prefix, suffix, renderBlockNode, }) {
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));
8080
+ }
8081
+ const defaultLabels = {
8082
+ zoomIn: 'Zoom in',
8083
+ zoomOut: 'Zoom out',
8084
+ reset: 'Reset',
8085
+ visibleRange: 'Visible range',
8086
+ duration: 'Duration',
8087
+ daysShort: 'd',
8088
+ hoursShort: 'h',
8089
+ minutesShort: 'm',
8090
+ secondsShort: 's',
8091
+ invalidRange: 'Invalid range',
8092
+ dateTimeFormat: 'YYYY-MM-DD HH:mm:ss',
8093
+ };
8094
+ const DEFAULT_MIN_DURATION_MS = 60 * 1000;
8095
+ const DEFAULT_MAX_DURATION_MS = 365 * 24 * 60 * 60 * 1000;
8096
+ const DEFAULT_ZOOM_FACTOR = 2;
8097
+ const clampNumber = (value, min, max) => Math.min(Math.max(value, min), max);
8098
+ const toValidDayjs = (value, fallback) => {
8099
+ const parsed = dayjs(value);
8100
+ return parsed.isValid() ? parsed : fallback;
8101
+ };
8102
+ const parseTimeInput = (value) => {
8103
+ const parsed = dayjs(value);
8104
+ return parsed.isValid() ? parsed : null;
8105
+ };
8106
+ const flattenTrackBlocks = (blocks, inheritedTrack) => {
8107
+ const flattened = [];
8108
+ blocks.forEach((block) => {
8109
+ const resolvedTrack = block.track ?? inheritedTrack;
8110
+ if (block.children && block.children.length > 0) {
8111
+ flattened.push(...flattenTrackBlocks(block.children, resolvedTrack));
8112
+ }
8113
+ if (block.start !== undefined && block.end !== undefined) {
8114
+ flattened.push({
8115
+ ...block,
8116
+ track: resolvedTrack,
8117
+ });
8118
+ }
8119
+ });
8120
+ return flattened;
8121
+ };
8122
+ function useTimeViewport(viewportStart, viewportEnd, format) {
8123
+ const viewport = useResolvedViewport(viewportStart, viewportEnd);
8124
+ const parsedViewport = useMemo(() => {
8125
+ if (!viewport)
8126
+ return null;
8127
+ const start = parseTimeInput(viewport.viewportStart);
8128
+ const end = parseTimeInput(viewport.viewportEnd);
8129
+ if (!start || !end || !end.isAfter(start))
8130
+ return null;
8131
+ const viewportStartMs = start.valueOf();
8132
+ const viewportEndMs = end.valueOf();
8133
+ return {
8134
+ start,
8135
+ end,
8136
+ formatString: format ?? getDefaultHeaderFormat(start, end),
8137
+ viewportStartMs,
8138
+ viewportEndMs,
8139
+ viewportDurationMs: viewportEndMs - viewportStartMs,
8140
+ };
8141
+ }, [format, viewport]);
8142
+ const toTimeMs = useCallback((value) => {
8143
+ if (value === undefined)
8144
+ return null;
8145
+ const parsed = parseTimeInput(value);
8146
+ return parsed ? parsed.valueOf() : null;
8147
+ }, []);
8148
+ const getGeometry = useCallback((start, end) => {
8149
+ if (!parsedViewport || start === undefined || end === undefined) {
8150
+ return { valid: false, leftPercent: 0, widthPercent: 0 };
8151
+ }
8152
+ const blockStartMs = toTimeMs(start);
8153
+ const blockEndMs = toTimeMs(end);
8154
+ if (blockStartMs === null ||
8155
+ blockEndMs === null ||
8156
+ blockEndMs <= blockStartMs) {
8157
+ return { valid: false, leftPercent: 0, widthPercent: 0 };
8158
+ }
8159
+ const visibleStartMs = Math.max(blockStartMs, parsedViewport.viewportStartMs);
8160
+ const visibleEndMs = Math.min(blockEndMs, parsedViewport.viewportEndMs);
8161
+ if (visibleEndMs <= visibleStartMs) {
8162
+ return { valid: true, leftPercent: 0, widthPercent: 0 };
8163
+ }
8164
+ const leftMs = visibleStartMs - parsedViewport.viewportStartMs;
8165
+ const widthMs = visibleEndMs - visibleStartMs;
8166
+ return {
8167
+ valid: true,
8168
+ leftPercent: clampNumber((leftMs / parsedViewport.viewportDurationMs) * 100, 0, 100),
8169
+ widthPercent: clampNumber((widthMs / parsedViewport.viewportDurationMs) * 100, 0, 100),
8170
+ };
8171
+ }, [parsedViewport, toTimeMs]);
8172
+ const getTimestampPercent = useCallback((timestamp) => {
8173
+ if (!parsedViewport || timestamp === undefined) {
8174
+ return { valid: false, percent: 0, inView: false };
8175
+ }
8176
+ const timestampMs = toTimeMs(timestamp);
8177
+ if (timestampMs === null)
8178
+ return { valid: false, percent: 0, inView: false };
8179
+ const rawPercent = ((timestampMs - parsedViewport.viewportStartMs) /
8180
+ parsedViewport.viewportDurationMs) *
8181
+ 100;
8182
+ return {
8183
+ valid: true,
8184
+ percent: clampNumber(rawPercent, 0, 100),
8185
+ inView: rawPercent >= 0 && rawPercent <= 100,
8186
+ };
8187
+ }, [parsedViewport, toTimeMs]);
8188
+ const getTicksByCount = useCallback((tickCount = 7) => {
8189
+ if (!parsedViewport)
8190
+ return [];
8191
+ const safeTickCount = Math.max(2, tickCount);
8192
+ return Array.from({ length: safeTickCount }, (_, index) => {
8193
+ const ratio = index / (safeTickCount - 1);
8194
+ const tickTime = parsedViewport.start.add(parsedViewport.viewportDurationMs * ratio, 'millisecond');
8195
+ return {
8196
+ index,
8197
+ percent: ratio * 100,
8198
+ label: tickTime.format(parsedViewport.formatString),
8199
+ };
8200
+ });
8201
+ }, [parsedViewport]);
8202
+ const getTicksByTimeUnit = useCallback((tickUnit = 'hour', tickStep = 1) => {
8203
+ if (!parsedViewport)
8204
+ return [];
8205
+ const safeTickStep = Math.max(1, Math.floor(tickStep));
8206
+ const candidateTimes = [parsedViewport.start];
8207
+ let cursor = parsedViewport.start.startOf(tickUnit);
8208
+ while (cursor.isBefore(parsedViewport.start)) {
8209
+ cursor = cursor.add(safeTickStep, tickUnit);
8210
+ }
8211
+ while (cursor.isBefore(parsedViewport.end) ||
8212
+ cursor.isSame(parsedViewport.end)) {
8213
+ candidateTimes.push(cursor);
8214
+ cursor = cursor.add(safeTickStep, tickUnit);
8215
+ }
8216
+ candidateTimes.push(parsedViewport.end);
8217
+ const uniqueSortedTicks = Array.from(new Map(candidateTimes.map((time) => [time.valueOf(), time])).values()).sort((a, b) => a.valueOf() - b.valueOf());
8218
+ return uniqueSortedTicks.map((tickTime, index) => {
8219
+ const ratio = tickTime.diff(parsedViewport.start, 'millisecond') /
8220
+ parsedViewport.viewportDurationMs;
8221
+ return {
8222
+ index,
8223
+ percent: clampNumber(ratio * 100, 0, 100),
8224
+ label: tickTime.format(parsedViewport.formatString),
8225
+ };
8226
+ });
8227
+ }, [parsedViewport]);
8228
+ const getTicks = useCallback((options) => {
8229
+ const strategy = options?.tickStrategy ?? 'count';
8230
+ if (strategy === 'timeUnit') {
8231
+ return getTicksByTimeUnit(options?.tickUnit, options?.tickStep);
8232
+ }
8233
+ return getTicksByCount(options?.tickCount);
8234
+ }, [getTicksByCount, getTicksByTimeUnit]);
8235
+ return {
8236
+ isValidViewport: Boolean(parsedViewport),
8237
+ toTimeMs,
8238
+ getGeometry,
8239
+ getTimestampPercent,
8240
+ getTicksByCount,
8241
+ getTicksByTimeUnit,
8242
+ getTicks,
8243
+ };
8244
+ }
8245
+ function useTimeViewportBlockGeometry(viewportStart, viewportEnd) {
8246
+ const { isValidViewport, getGeometry, toTimeMs } = useTimeViewport(viewportStart, viewportEnd);
8247
+ return {
8248
+ hasValidViewport: isValidViewport,
8249
+ getGeometry,
8250
+ toTimeMs,
8251
+ };
8252
+ }
8253
+ const getDefaultHeaderFormat = (start, end) => {
8254
+ const durationHours = end.diff(start, 'hour', true);
8255
+ if (durationHours <= 24)
8256
+ return 'HH:mm';
8257
+ if (durationHours <= 24 * 7)
8258
+ return 'ddd HH:mm';
8259
+ return 'MMM D';
8260
+ };
8261
+ function useTimeViewportTicks({ viewportStart, viewportEnd, format, }) {
8262
+ const { isValidViewport, getTicksByCount, getTicksByTimeUnit, getTicks } = useTimeViewport(viewportStart, viewportEnd, format);
8263
+ return {
8264
+ isValidViewport,
8265
+ getTicksByCount,
8266
+ getTicksByTimeUnit,
8267
+ getTicks,
8268
+ };
8269
+ }
8270
+ const useTimeViewportHeader = useTimeViewportTicks;
8271
+ const formatDuration = (durationMs, labels) => {
8272
+ const totalSeconds = Math.max(0, Math.floor(durationMs / 1000));
8273
+ const days = Math.floor(totalSeconds / 86400);
8274
+ const hours = Math.floor((totalSeconds % 86400) / 3600);
8275
+ const minutes = Math.floor((totalSeconds % 3600) / 60);
8276
+ const seconds = totalSeconds % 60;
8277
+ const parts = [];
8278
+ if (days > 0)
8279
+ parts.push(`${days}${labels.daysShort}`);
8280
+ if (hours > 0)
8281
+ parts.push(`${hours}${labels.hoursShort}`);
8282
+ if (minutes > 0)
8283
+ parts.push(`${minutes}${labels.minutesShort}`);
8284
+ if (seconds > 0 || parts.length === 0)
8285
+ parts.push(`${seconds}${labels.secondsShort}`);
8286
+ return parts.join(' ');
8287
+ };
8288
+ /**
8289
+ * A resizable timeline block based on block time range and viewport time range.
8290
+ * Width and offset are automatically derived from datetime overlap.
8291
+ */
8292
+ function TimeViewportBlock({ start, end, viewportStart, viewportEnd, height = '28px', minWidthPx = 2, borderRadius = 'sm', colorPalette = 'blue', background, label, showLabel = true, hideWhenOutOfView = true, onClick, }) {
8293
+ const { getGeometry } = useTimeViewportBlockGeometry(viewportStart, viewportEnd);
8294
+ const geometry = useMemo(() => {
8295
+ return getGeometry(start, end);
8296
+ }, [end, getGeometry, start]);
8297
+ if (!geometry.valid)
8298
+ return null;
8299
+ if (hideWhenOutOfView && geometry.widthPercent <= 0)
8300
+ return null;
8301
+ return (jsx(Box, { position: "relative", width: "100%", height: height, children: 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: background ?? `${colorPalette}.500`, _dark: {
8302
+ bg: background ?? `${colorPalette}.900`,
8303
+ }, display: "flex", alignItems: "center", justifyContent: "center", px: 2, overflow: "hidden", pointerEvents: "auto", onClick: onClick, cursor: onClick ? 'pointer' : 'default', children: showLabel && label ? (jsx(Text, { fontSize: "xs", lineClamp: 1, color: "white", _dark: { color: 'gray.100' }, children: label })) : null }) }) }) }));
8304
+ }
8305
+ /**
8306
+ * Vertical marker line for a timestamp in the current viewport.
8307
+ */
8308
+ function TimeViewportMarkerLine({ timestamp, viewportStart, viewportEnd, height = '100%', colorPalette = 'red', color, lineWidthPx = 2, label, showLabel = true, hideWhenOutOfView = true, }) {
8309
+ const { getTimestampPercent } = useTimeViewport(viewportStart, viewportEnd);
8310
+ const marker = useMemo(() => {
8311
+ return getTimestampPercent(timestamp);
8312
+ }, [getTimestampPercent, timestamp]);
8313
+ if (!marker.valid)
8314
+ return null;
8315
+ if (hideWhenOutOfView && !marker.inView)
8316
+ return 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}.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] }) }));
8318
+ }
8319
+ /**
8320
+ * Header labels for timeline viewport time scale.
8321
+ */
8322
+ function TimeViewportHeader({ viewportStart, viewportEnd, tickCount = 7, tickStrategy = 'count', tickUnit = 'hour', tickStep = 1, format, height = '28px', color = 'gray.600', borderColor = 'gray.200', showBottomBorder = true, animationDurationMs = VIEWPORT_TRANSITION_DURATION_MS, animationEasing = VIEWPORT_TRANSITION_EASING, }) {
8323
+ const { isValidViewport, getTicks } = useTimeViewport(viewportStart, viewportEnd, format);
8324
+ const ticks = getTicks({ tickStrategy, tickCount, tickUnit, tickStep });
8325
+ const safeTickCount = ticks.length;
8326
+ const transitionValue = animationDurationMs > 0
8327
+ ? `transform ${animationDurationMs}ms ${animationEasing}, opacity ${animationDurationMs}ms ${animationEasing}`
8328
+ : undefined;
8329
+ if (!isValidViewport || safeTickCount < 2)
8330
+ return null;
8331
+ return (jsx(Box, { position: "relative", width: "100%", height: height, borderBottomWidth: showBottomBorder ? '1px' : '0px', borderColor: borderColor, _dark: { borderColor: 'gray.700' }, mb: 2, children: ticks.map((tick) => (jsx(Box, { position: "absolute", inset: 0, transform: `translateX(${tick.percent}%)`, transition: transitionValue, children: jsx(Text, { position: "absolute", insetInlineStart: 0, top: "50%", translate: "0 -50%", transform: tick.index === 0
8332
+ ? 'translateX(0%)'
8333
+ : tick.index === safeTickCount - 1
8334
+ ? 'translateX(-100%)'
8335
+ : 'translateX(-50%)', fontSize: "xs", color: color, _dark: { color: 'gray.300' }, whiteSpace: "nowrap", children: tick.label }) }, `tick-wrap-${tick.index}`))) }));
8336
+ }
8337
+ /**
8338
+ * Vertical grid lines for measuring block positions in the viewport.
8339
+ * Render inside a relative container that also contains blocks.
8340
+ */
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
+ return null;
8346
+ const safeMinorDivisions = Math.max(1, minorDivisions);
8347
+ const transitionValue = animationDurationMs > 0
8348
+ ? `transform ${animationDurationMs}ms ${animationEasing}, opacity ${animationDurationMs}ms ${animationEasing}`
8349
+ : undefined;
8350
+ const minorTicks = showMinorLines
8351
+ ? majorTicks.slice(0, -1).flatMap((tick, segmentIndex) => {
8352
+ const base = tick.percent;
8353
+ const next = majorTicks[segmentIndex + 1].percent;
8354
+ const segment = [];
8355
+ for (let step = 1; step < safeMinorDivisions; step += 1) {
8356
+ segment.push(base + ((next - base) * step) / safeMinorDivisions);
8357
+ }
8358
+ return segment;
8359
+ })
8360
+ : [];
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}`)))] }));
8362
+ }
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, }) {
8390
+ const { getGeometry, toTimeMs } = useTimeViewportBlockGeometry(viewportStart, viewportEnd);
8391
+ const resolvedHeight = typeof height === 'number' ? `${height}px` : height;
8392
+ const expandedBlocks = flattenTrackBlocks(blocks);
8393
+ const parsedBlocks = expandedBlocks
8394
+ .map((block, index) => {
8395
+ if (block.start === undefined || block.end === undefined) {
8396
+ return {
8397
+ block,
8398
+ index,
8399
+ geometry: { valid: false, leftPercent: 0, widthPercent: 0 },
8400
+ startMs: Number.NaN,
8401
+ endMs: Number.NaN,
8402
+ };
8403
+ }
8404
+ const geometry = getGeometry(block.start, block.end);
8405
+ const startMs = toTimeMs(block.start);
8406
+ const endMs = toTimeMs(block.end);
8407
+ return {
8408
+ block,
8409
+ index,
8410
+ geometry,
8411
+ startMs: startMs ?? Number.NaN,
8412
+ endMs: endMs ?? Number.NaN,
8413
+ };
8414
+ })
8415
+ .filter(({ geometry }) => geometry.valid)
8416
+ .filter(({ geometry }) => !hideWhenOutOfView || geometry.widthPercent > 0);
8417
+ const renderBlockNode = (blockItem, indexInLayer) => {
8418
+ const { block, geometry } = blockItem;
8419
+ const handleBlockClick = () => {
8420
+ block.onClick?.(block);
8421
+ onBlockClick?.(block);
8422
+ };
8423
+ const isBlockClickable = Boolean(block.onClick || onBlockClick);
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));
8429
+ };
8430
+ // ---------- Resolve tracks ----------
8431
+ const explicitTrackKeys = Array.from(new Set(expandedBlocks
8432
+ .map((item) => item.track)
8433
+ .filter((track) => track !== undefined)));
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 }));
8477
+ }
8478
+ return (jsx(VStack, { align: "stretch", gap: gap, children: resolvedTracks.map((track, trackIndex) => {
8479
+ const trackBlocks = track.blocks.map((item) => item.block);
8480
+ const prefix = renderTrackPrefix?.({
8481
+ trackIndex,
8482
+ trackBlocks,
8483
+ trackKey: track.trackKeyRaw,
8484
+ });
8485
+ const suffix = renderTrackSuffix?.({
8486
+ trackIndex,
8487
+ trackBlocks,
8488
+ trackKey: track.trackKeyRaw,
8489
+ });
8490
+ return (jsx(TimeViewportTrackRow, { trackKey: track.trackKey, blocks: track.blocks, resolvedHeight: resolvedHeight, prefix: prefix, suffix: suffix, renderBlockNode: renderBlockNode }));
8491
+ }) }));
8492
+ }
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, }) {
8494
+ const { labels: mergedLabels, canZoomIn, canZoomOut, visibleRangeText, durationText, zoomIn, zoomOut, reset, } = useTimeRangeZoom({
8495
+ range,
8496
+ onRangeChange,
8497
+ minDurationMs,
8498
+ maxDurationMs,
8499
+ zoomFactor,
8500
+ resetDurationMs,
8501
+ disabled,
8502
+ labels,
8503
+ });
8504
+ return (jsxs(VStack, { align: "stretch", gap: 2, p: 3, children: [jsxs(HStack, { justify: "space-between", gap: 2, children: [jsxs(HStack, { gap: 2, children: [jsx(IconButton, { "aria-label": mergedLabels.zoomOut, onClick: zoomOut, disabled: !canZoomOut, size: "sm", variant: "outline", children: jsx(LuZoomOut, {}) }), jsx(IconButton, { "aria-label": mergedLabels.zoomIn, onClick: zoomIn, disabled: !canZoomIn, size: "sm", variant: "outline", children: jsx(LuZoomIn, {}) })] }), showResetButton ? (jsx(Button$1, { size: "sm", variant: "ghost", onClick: reset, disabled: disabled, colorPalette: "blue", children: mergedLabels.reset })) : null] }), jsxs(Text, { fontSize: "sm", color: "gray.700", _dark: { color: 'gray.200' }, children: [mergedLabels.visibleRange, ": ", visibleRangeText] }), jsxs(Text, { fontSize: "xs", color: "gray.600", _dark: { color: 'gray.400' }, children: [mergedLabels.duration, ": ", durationText] })] }));
8505
+ }
8506
+ function useTimeRangeZoom({ range, onRangeChange, minDurationMs = DEFAULT_MIN_DURATION_MS, maxDurationMs = DEFAULT_MAX_DURATION_MS, zoomFactor = DEFAULT_ZOOM_FACTOR, resetDurationMs, disabled = false, labels, }) {
8507
+ const mergedLabels = useMemo(() => ({
8508
+ ...defaultLabels,
8509
+ ...(labels ?? {}),
8510
+ }), [labels]);
8511
+ const now = dayjs();
8512
+ const start = toValidDayjs(range.start, now.subtract(1, 'hour'));
8513
+ const end = toValidDayjs(range.end, now);
8514
+ const safeMinDurationMs = Math.max(1000, minDurationMs);
8515
+ const safeMaxDurationMs = Math.max(safeMinDurationMs, maxDurationMs);
8516
+ const safeZoomFactor = zoomFactor > 1 ? zoomFactor : DEFAULT_ZOOM_FACTOR;
8517
+ const durationMs = useMemo(() => {
8518
+ const diff = end.diff(start, 'millisecond');
8519
+ return clampNumber(diff > 0 ? diff : safeMinDurationMs, safeMinDurationMs, safeMaxDurationMs);
8520
+ }, [end, start, safeMaxDurationMs, safeMinDurationMs]);
8521
+ const initialDurationRef = useRef(clampNumber(resetDurationMs ?? durationMs, safeMinDurationMs, safeMaxDurationMs));
8522
+ const hasValidDisplayRange = end.isAfter(start);
8523
+ const applyDurationAroundCenter = useCallback((nextDurationMs) => {
8524
+ const centerMs = start.valueOf() + durationMs / 2;
8525
+ const half = nextDurationMs / 2;
8526
+ const nextStart = dayjs(centerMs - half).toDate();
8527
+ const nextEnd = dayjs(centerMs + half).toDate();
8528
+ onRangeChange({ start: nextStart, end: nextEnd });
8529
+ }, [durationMs, onRangeChange, start]);
8530
+ const zoomIn = useCallback(() => {
8531
+ const nextDuration = clampNumber(durationMs / safeZoomFactor, safeMinDurationMs, safeMaxDurationMs);
8532
+ applyDurationAroundCenter(nextDuration);
8533
+ }, [
8534
+ applyDurationAroundCenter,
8535
+ durationMs,
8536
+ safeMaxDurationMs,
8537
+ safeMinDurationMs,
8538
+ safeZoomFactor,
8539
+ ]);
8540
+ const zoomOut = useCallback(() => {
8541
+ const nextDuration = clampNumber(durationMs * safeZoomFactor, safeMinDurationMs, safeMaxDurationMs);
8542
+ applyDurationAroundCenter(nextDuration);
8543
+ }, [
8544
+ applyDurationAroundCenter,
8545
+ durationMs,
8546
+ safeMaxDurationMs,
8547
+ safeMinDurationMs,
8548
+ safeZoomFactor,
8549
+ ]);
8550
+ const reset = useCallback(() => {
8551
+ applyDurationAroundCenter(initialDurationRef.current);
8552
+ }, [applyDurationAroundCenter]);
8553
+ const canZoomIn = !disabled && durationMs > safeMinDurationMs;
8554
+ const canZoomOut = !disabled && durationMs < safeMaxDurationMs;
8555
+ const visibleRangeText = hasValidDisplayRange
8556
+ ? `${start.format(mergedLabels.dateTimeFormat)} - ${end.format(mergedLabels.dateTimeFormat)}`
8557
+ : mergedLabels.invalidRange;
8558
+ const durationText = formatDuration(durationMs, mergedLabels);
8559
+ return {
8560
+ labels: mergedLabels,
8561
+ start,
8562
+ end,
8563
+ durationMs,
8564
+ canZoomIn,
8565
+ canZoomOut,
8566
+ hasValidDisplayRange,
8567
+ visibleRangeText,
8568
+ durationText,
8569
+ zoomIn,
8570
+ zoomOut,
8571
+ reset,
8572
+ };
8573
+ }
8574
+
7970
8575
  const snakeToLabel = (str) => {
7971
8576
  return str
7972
8577
  .split("_") // Split by underscore
@@ -8870,4 +9475,4 @@ function DataTableServer({ columns, enableRowSelection = true, enableMultiRowSel
8870
9475
  }, children: jsx(DataTableServerContext.Provider, { value: { url: url ?? '', query }, children: children }) }));
8871
9476
  }
8872
9477
 
8873
- export { CalendarDisplay, CardHeader, DataDisplay, DataTable, DataTableServer, DatePickerContext, DatePickerInput, DefaultCardTitle, DefaultForm, DefaultTable, DefaultTableServer, DensityToggleButton, EditSortingButton, EmptyState, ErrorAlert, FilterDialog, FormBody, FormRoot, FormTitle, GlobalFilter, MediaLibraryBrowser, PageSizeControl, Pagination, RecordDisplay, ReloadButton, ResetFilteringButton, ResetSelectionButton, ResetSortingButton, RowCountText, SelectAllRowsToggle, Table, TableBody, TableCardContainer, TableCards, TableComponent, TableControls, TableDataDisplay, TableFilter, TableFilterTags, TableFooter, TableHeader, TableLoadingComponent, TableSelector, TableSorter, TableViewer, TextCell, ViewDialog, defaultRenderDisplay, getMultiDates, getRangeDates, useDataTable, useDataTableContext, useDataTableServer, useForm };
9478
+ export { CalendarDisplay, CardHeader, DataDisplay, DataTable, DataTableServer, DatePickerContext, DatePickerInput, DefaultCardTitle, DefaultForm, DefaultTable, DefaultTableServer, DensityToggleButton, EditSortingButton, EmptyState, ErrorAlert, FilterDialog, FormBody, FormRoot, FormTitle, GlobalFilter, MediaLibraryBrowser, PageSizeControl, Pagination, RecordDisplay, ReloadButton, ResetFilteringButton, ResetSelectionButton, ResetSortingButton, RowCountText, SelectAllRowsToggle, Table, TableBody, TableCardContainer, TableCards, TableComponent, TableControls, TableDataDisplay, TableFilter, TableFilterTags, TableFooter, TableHeader, TableLoadingComponent, TableSelector, TableSorter, TableViewer, TextCell, TimeRangeZoom, TimeViewportBlock, TimeViewportBlocks, TimeViewportGrid, TimeViewportHeader, TimeViewportMarkerLine, TimeViewportRoot, ViewDialog, defaultRenderDisplay, getMultiDates, getRangeDates, useDataTable, useDataTableContext, useDataTableServer, useForm, useTimeRangeZoom, useTimeViewport, useTimeViewportBlockGeometry, useTimeViewportHeader, useTimeViewportTicks };