@bsol-oss/react-datatable5 13.0.1-beta.31 → 13.0.1-beta.33
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.js
CHANGED
|
@@ -7695,7 +7695,7 @@ function DateTimePicker$1({ value, onChange, format = 'date-time', showSeconds =
|
|
|
7695
7695
|
}
|
|
7696
7696
|
}
|
|
7697
7697
|
};
|
|
7698
|
-
return (jsxRuntime.jsxs(react.HStack, { justifyContent: 'start', alignItems: 'center', gap: 2, children: [jsxRuntime.jsx(react.InputGroup, { endElement: jsxRuntime.jsxs(react.Popover.Root, { open: calendarPopoverOpen, onOpenChange: (e) => setCalendarPopoverOpen(e.open), closeOnInteractOutside: false, autoFocus: false, children: [jsxRuntime.jsx(react.Popover.Trigger, { asChild: true, children: jsxRuntime.jsx(react.Button, { variant: "ghost", size: "xs", "aria-label": "Open calendar", onClick: () => setCalendarPopoverOpen(true), children: jsxRuntime.jsx(md.MdDateRange, {}) }) }), jsxRuntime.jsx(react.Portal, { disabled:
|
|
7698
|
+
return (jsxRuntime.jsxs(react.HStack, { justifyContent: 'start', alignItems: 'center', gap: 2, children: [jsxRuntime.jsx(react.InputGroup, { endElement: jsxRuntime.jsxs(react.Popover.Root, { open: calendarPopoverOpen, onOpenChange: (e) => setCalendarPopoverOpen(e.open), closeOnInteractOutside: false, autoFocus: false, children: [jsxRuntime.jsx(react.Popover.Trigger, { asChild: true, children: jsxRuntime.jsx(react.Button, { variant: "ghost", size: "xs", "aria-label": "Open calendar", onClick: () => setCalendarPopoverOpen(true), children: jsxRuntime.jsx(md.MdDateRange, {}) }) }), jsxRuntime.jsx(react.Portal, { disabled: portalled, children: jsxRuntime.jsx(react.Popover.Positioner, { children: jsxRuntime.jsx(react.Popover.Content, { width: "fit-content", zIndex: 1500, children: jsxRuntime.jsx(react.Popover.Body, { p: 4, children: jsxRuntime.jsx(DatePickerContext.Provider, { value: { labels: calendarLabels }, children: jsxRuntime.jsx(Calendar, { ...calendarProps, firstDayOfWeek: 0 }) }) }) }) }) })] }), children: jsxRuntime.jsx(react.Input, { value: dateInputValue, onChange: handleDateInputChange, onBlur: handleDateInputBlur, onKeyDown: handleDateInputKeyDown, placeholder: "YYYY-MM-DD", ref: dateInitialFocusEl }) }), jsxRuntime.jsxs(react.Popover.Root, { open: timePopoverOpen, onOpenChange: (e) => setTimePopoverOpen(e.open), initialFocusEl: () => timeInitialFocusEl.current, closeOnInteractOutside: true, autoFocus: false, children: [jsxRuntime.jsx(react.Popover.Trigger, { asChild: true, children: jsxRuntime.jsx(react.InputGroup, { startElement: jsxRuntime.jsx(bs.BsClock, {}), children: jsxRuntime.jsx(react.Input, { value: timeDisplayValue, onChange: handleTimeInputChange, onKeyDown: handleTimeInputKeyDown, placeholder: timePickerLabels?.placeholder ??
|
|
7699
7699
|
(is24Hour ? 'HH:mm' : 'hh:mm AM/PM'), ref: timeInitialFocusEl }) }) }), jsxRuntime.jsx(react.Portal, { disabled: portalled, children: jsxRuntime.jsx(react.Popover.Positioner, { children: jsxRuntime.jsx(react.Popover.Content, { overflowY: "auto", children: jsxRuntime.jsx(react.Popover.Body, { p: 0, minH: "200px", maxH: "70vh", width: "fit-content", children: collection.items.length === 0 ? (jsxRuntime.jsx(react.Text, { px: 3, py: 2, color: "gray.500", fontSize: "sm", children: timePickerLabels?.emptyMessage ?? 'No time found' })) : (collection.items.map((item) => {
|
|
7700
7700
|
const option = item;
|
|
7701
7701
|
const isSelected = option.value === currentTimeValue;
|
|
@@ -7987,6 +7987,591 @@ const getMultiDates = ({ selected, selectedDate, selectedDates, selectable, }) =
|
|
|
7987
7987
|
}
|
|
7988
7988
|
};
|
|
7989
7989
|
|
|
7990
|
+
const VIEWPORT_TRANSITION_EASING = 'cubic-bezier(0.25, 0.46, 0.45, 0.94)';
|
|
7991
|
+
const VIEWPORT_TRANSITION_DURATION_MS = 220;
|
|
7992
|
+
const VIEWPORT_TRANSITION = `transform ${VIEWPORT_TRANSITION_DURATION_MS}ms ${VIEWPORT_TRANSITION_EASING}, opacity ${VIEWPORT_TRANSITION_DURATION_MS}ms ${VIEWPORT_TRANSITION_EASING}`;
|
|
7993
|
+
const TimeViewportContext = React.createContext(null);
|
|
7994
|
+
const useResolvedViewport = (viewportStart, viewportEnd) => {
|
|
7995
|
+
const context = React.useContext(TimeViewportContext);
|
|
7996
|
+
const resolvedStart = viewportStart ?? context?.viewportStart;
|
|
7997
|
+
const resolvedEnd = viewportEnd ?? context?.viewportEnd;
|
|
7998
|
+
if (resolvedStart === undefined || resolvedEnd === undefined)
|
|
7999
|
+
return null;
|
|
8000
|
+
return {
|
|
8001
|
+
viewportStart: resolvedStart,
|
|
8002
|
+
viewportEnd: resolvedEnd,
|
|
8003
|
+
};
|
|
8004
|
+
};
|
|
8005
|
+
function TimeViewportRoot({ viewportStart, viewportEnd, children, onViewportChange, enableDragPan = false, enableCtrlWheelZoom = false, wheelZoomFactor = 1.2, minDurationMs = 60 * 1000, maxDurationMs = 365 * 24 * 60 * 60 * 1000, }) {
|
|
8006
|
+
const containerRef = React.useRef(null);
|
|
8007
|
+
const [isDragging, setIsDragging] = React.useState(false);
|
|
8008
|
+
const dragRef = React.useRef(null);
|
|
8009
|
+
const parseViewport = React.useCallback(() => {
|
|
8010
|
+
const start = parseTimeInput(viewportStart);
|
|
8011
|
+
const end = parseTimeInput(viewportEnd);
|
|
8012
|
+
if (!start || !end || !end.isAfter(start))
|
|
8013
|
+
return null;
|
|
8014
|
+
return {
|
|
8015
|
+
startMs: start.valueOf(),
|
|
8016
|
+
endMs: end.valueOf(),
|
|
8017
|
+
};
|
|
8018
|
+
}, [viewportEnd, viewportStart]);
|
|
8019
|
+
const handlePointerDown = (e) => {
|
|
8020
|
+
if (!enableDragPan || !onViewportChange)
|
|
8021
|
+
return;
|
|
8022
|
+
if (e.button !== 0)
|
|
8023
|
+
return;
|
|
8024
|
+
const parsed = parseViewport();
|
|
8025
|
+
if (!parsed)
|
|
8026
|
+
return;
|
|
8027
|
+
dragRef.current = {
|
|
8028
|
+
pointerId: e.pointerId,
|
|
8029
|
+
startX: e.clientX,
|
|
8030
|
+
viewportStartMs: parsed.startMs,
|
|
8031
|
+
viewportEndMs: parsed.endMs,
|
|
8032
|
+
};
|
|
8033
|
+
setIsDragging(true);
|
|
8034
|
+
e.currentTarget.setPointerCapture(e.pointerId);
|
|
8035
|
+
};
|
|
8036
|
+
const handlePointerMove = (e) => {
|
|
8037
|
+
if (!enableDragPan || !onViewportChange)
|
|
8038
|
+
return;
|
|
8039
|
+
const dragging = dragRef.current;
|
|
8040
|
+
if (!dragging || dragging.pointerId !== e.pointerId)
|
|
8041
|
+
return;
|
|
8042
|
+
const width = containerRef.current?.clientWidth ?? 0;
|
|
8043
|
+
if (width <= 0)
|
|
8044
|
+
return;
|
|
8045
|
+
const deltaX = e.clientX - dragging.startX;
|
|
8046
|
+
const durationMs = dragging.viewportEndMs - dragging.viewportStartMs;
|
|
8047
|
+
const shiftMs = (-deltaX / width) * durationMs;
|
|
8048
|
+
onViewportChange({
|
|
8049
|
+
start: dayjs(dragging.viewportStartMs + shiftMs).toDate(),
|
|
8050
|
+
end: dayjs(dragging.viewportEndMs + shiftMs).toDate(),
|
|
8051
|
+
});
|
|
8052
|
+
};
|
|
8053
|
+
const stopDragging = (e) => {
|
|
8054
|
+
const dragging = dragRef.current;
|
|
8055
|
+
if (!dragging || dragging.pointerId !== e.pointerId)
|
|
8056
|
+
return;
|
|
8057
|
+
dragRef.current = null;
|
|
8058
|
+
setIsDragging(false);
|
|
8059
|
+
if (e.currentTarget.hasPointerCapture(e.pointerId)) {
|
|
8060
|
+
e.currentTarget.releasePointerCapture(e.pointerId);
|
|
8061
|
+
}
|
|
8062
|
+
};
|
|
8063
|
+
const handleWheel = (e) => {
|
|
8064
|
+
if (!e.ctrlKey)
|
|
8065
|
+
return;
|
|
8066
|
+
// Prevent browser-level Ctrl/Cmd + wheel page zoom while interacting
|
|
8067
|
+
// with the timeline surface.
|
|
8068
|
+
e.preventDefault();
|
|
8069
|
+
if (!enableCtrlWheelZoom || !onViewportChange)
|
|
8070
|
+
return;
|
|
8071
|
+
const parsed = parseViewport();
|
|
8072
|
+
if (!parsed)
|
|
8073
|
+
return;
|
|
8074
|
+
const width = containerRef.current?.clientWidth ?? 0;
|
|
8075
|
+
if (width <= 0)
|
|
8076
|
+
return;
|
|
8077
|
+
const safeFactor = wheelZoomFactor > 1 ? wheelZoomFactor : 1.2;
|
|
8078
|
+
const durationMs = parsed.endMs - parsed.startMs;
|
|
8079
|
+
// Exponential zoom curve: each wheel "step" compounds by safeFactor.
|
|
8080
|
+
// This keeps zooming smooth on trackpads and predictable on mouse wheels.
|
|
8081
|
+
const wheelStep = e.deltaY / 100;
|
|
8082
|
+
const zoomMultiplier = Math.pow(safeFactor, wheelStep);
|
|
8083
|
+
const nextDuration = clampNumber(durationMs * (Number.isFinite(zoomMultiplier) ? zoomMultiplier : 1), minDurationMs, maxDurationMs);
|
|
8084
|
+
const rect = containerRef.current?.getBoundingClientRect();
|
|
8085
|
+
const x = rect ? e.clientX - rect.left : width / 2;
|
|
8086
|
+
const ratio = clampNumber(x / width, 0, 1);
|
|
8087
|
+
const anchorMs = parsed.startMs + durationMs * ratio;
|
|
8088
|
+
const nextStartMs = anchorMs - nextDuration * ratio;
|
|
8089
|
+
const nextEndMs = anchorMs + nextDuration * (1 - ratio);
|
|
8090
|
+
onViewportChange({
|
|
8091
|
+
start: dayjs(nextStartMs).toDate(),
|
|
8092
|
+
end: dayjs(nextEndMs).toDate(),
|
|
8093
|
+
});
|
|
8094
|
+
};
|
|
8095
|
+
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
|
+
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
|
+
}
|
|
8100
|
+
const defaultLabels = {
|
|
8101
|
+
zoomIn: 'Zoom in',
|
|
8102
|
+
zoomOut: 'Zoom out',
|
|
8103
|
+
reset: 'Reset',
|
|
8104
|
+
visibleRange: 'Visible range',
|
|
8105
|
+
duration: 'Duration',
|
|
8106
|
+
daysShort: 'd',
|
|
8107
|
+
hoursShort: 'h',
|
|
8108
|
+
minutesShort: 'm',
|
|
8109
|
+
secondsShort: 's',
|
|
8110
|
+
invalidRange: 'Invalid range',
|
|
8111
|
+
dateTimeFormat: 'YYYY-MM-DD HH:mm:ss',
|
|
8112
|
+
};
|
|
8113
|
+
const DEFAULT_MIN_DURATION_MS = 60 * 1000;
|
|
8114
|
+
const DEFAULT_MAX_DURATION_MS = 365 * 24 * 60 * 60 * 1000;
|
|
8115
|
+
const DEFAULT_ZOOM_FACTOR = 2;
|
|
8116
|
+
const clampNumber = (value, min, max) => Math.min(Math.max(value, min), max);
|
|
8117
|
+
const toValidDayjs = (value, fallback) => {
|
|
8118
|
+
const parsed = dayjs(value);
|
|
8119
|
+
return parsed.isValid() ? parsed : fallback;
|
|
8120
|
+
};
|
|
8121
|
+
const parseTimeInput = (value) => {
|
|
8122
|
+
const parsed = dayjs(value);
|
|
8123
|
+
return parsed.isValid() ? parsed : null;
|
|
8124
|
+
};
|
|
8125
|
+
const flattenTrackBlocks = (blocks, inheritedTrack) => {
|
|
8126
|
+
const flattened = [];
|
|
8127
|
+
blocks.forEach((block) => {
|
|
8128
|
+
const resolvedTrack = block.track ?? inheritedTrack;
|
|
8129
|
+
if (block.children && block.children.length > 0) {
|
|
8130
|
+
flattened.push(...flattenTrackBlocks(block.children, resolvedTrack));
|
|
8131
|
+
}
|
|
8132
|
+
if (block.start !== undefined && block.end !== undefined) {
|
|
8133
|
+
flattened.push({
|
|
8134
|
+
...block,
|
|
8135
|
+
track: resolvedTrack,
|
|
8136
|
+
});
|
|
8137
|
+
}
|
|
8138
|
+
});
|
|
8139
|
+
return flattened;
|
|
8140
|
+
};
|
|
8141
|
+
function useTimeViewport(viewportStart, viewportEnd, format) {
|
|
8142
|
+
const viewport = useResolvedViewport(viewportStart, viewportEnd);
|
|
8143
|
+
const parsedViewport = React.useMemo(() => {
|
|
8144
|
+
if (!viewport)
|
|
8145
|
+
return null;
|
|
8146
|
+
const start = parseTimeInput(viewport.viewportStart);
|
|
8147
|
+
const end = parseTimeInput(viewport.viewportEnd);
|
|
8148
|
+
if (!start || !end || !end.isAfter(start))
|
|
8149
|
+
return null;
|
|
8150
|
+
const viewportStartMs = start.valueOf();
|
|
8151
|
+
const viewportEndMs = end.valueOf();
|
|
8152
|
+
return {
|
|
8153
|
+
start,
|
|
8154
|
+
end,
|
|
8155
|
+
formatString: format ?? getDefaultHeaderFormat(start, end),
|
|
8156
|
+
viewportStartMs,
|
|
8157
|
+
viewportEndMs,
|
|
8158
|
+
viewportDurationMs: viewportEndMs - viewportStartMs,
|
|
8159
|
+
};
|
|
8160
|
+
}, [format, viewport]);
|
|
8161
|
+
const toTimeMs = React.useCallback((value) => {
|
|
8162
|
+
if (value === undefined)
|
|
8163
|
+
return null;
|
|
8164
|
+
const parsed = parseTimeInput(value);
|
|
8165
|
+
return parsed ? parsed.valueOf() : null;
|
|
8166
|
+
}, []);
|
|
8167
|
+
const getGeometry = React.useCallback((start, end) => {
|
|
8168
|
+
if (!parsedViewport || start === undefined || end === undefined) {
|
|
8169
|
+
return { valid: false, leftPercent: 0, widthPercent: 0 };
|
|
8170
|
+
}
|
|
8171
|
+
const blockStartMs = toTimeMs(start);
|
|
8172
|
+
const blockEndMs = toTimeMs(end);
|
|
8173
|
+
if (blockStartMs === null ||
|
|
8174
|
+
blockEndMs === null ||
|
|
8175
|
+
blockEndMs <= blockStartMs) {
|
|
8176
|
+
return { valid: false, leftPercent: 0, widthPercent: 0 };
|
|
8177
|
+
}
|
|
8178
|
+
const visibleStartMs = Math.max(blockStartMs, parsedViewport.viewportStartMs);
|
|
8179
|
+
const visibleEndMs = Math.min(blockEndMs, parsedViewport.viewportEndMs);
|
|
8180
|
+
if (visibleEndMs <= visibleStartMs) {
|
|
8181
|
+
return { valid: true, leftPercent: 0, widthPercent: 0 };
|
|
8182
|
+
}
|
|
8183
|
+
const leftMs = visibleStartMs - parsedViewport.viewportStartMs;
|
|
8184
|
+
const widthMs = visibleEndMs - visibleStartMs;
|
|
8185
|
+
return {
|
|
8186
|
+
valid: true,
|
|
8187
|
+
leftPercent: clampNumber((leftMs / parsedViewport.viewportDurationMs) * 100, 0, 100),
|
|
8188
|
+
widthPercent: clampNumber((widthMs / parsedViewport.viewportDurationMs) * 100, 0, 100),
|
|
8189
|
+
};
|
|
8190
|
+
}, [parsedViewport, toTimeMs]);
|
|
8191
|
+
const getTimestampPercent = React.useCallback((timestamp) => {
|
|
8192
|
+
if (!parsedViewport || timestamp === undefined) {
|
|
8193
|
+
return { valid: false, percent: 0, inView: false };
|
|
8194
|
+
}
|
|
8195
|
+
const timestampMs = toTimeMs(timestamp);
|
|
8196
|
+
if (timestampMs === null)
|
|
8197
|
+
return { valid: false, percent: 0, inView: false };
|
|
8198
|
+
const rawPercent = ((timestampMs - parsedViewport.viewportStartMs) /
|
|
8199
|
+
parsedViewport.viewportDurationMs) *
|
|
8200
|
+
100;
|
|
8201
|
+
return {
|
|
8202
|
+
valid: true,
|
|
8203
|
+
percent: clampNumber(rawPercent, 0, 100),
|
|
8204
|
+
inView: rawPercent >= 0 && rawPercent <= 100,
|
|
8205
|
+
};
|
|
8206
|
+
}, [parsedViewport, toTimeMs]);
|
|
8207
|
+
const getTicksByCount = React.useCallback((tickCount = 7) => {
|
|
8208
|
+
if (!parsedViewport)
|
|
8209
|
+
return [];
|
|
8210
|
+
const safeTickCount = Math.max(2, tickCount);
|
|
8211
|
+
return Array.from({ length: safeTickCount }, (_, index) => {
|
|
8212
|
+
const ratio = index / (safeTickCount - 1);
|
|
8213
|
+
const tickTime = parsedViewport.start.add(parsedViewport.viewportDurationMs * ratio, 'millisecond');
|
|
8214
|
+
return {
|
|
8215
|
+
index,
|
|
8216
|
+
percent: ratio * 100,
|
|
8217
|
+
label: tickTime.format(parsedViewport.formatString),
|
|
8218
|
+
};
|
|
8219
|
+
});
|
|
8220
|
+
}, [parsedViewport]);
|
|
8221
|
+
const getTicksByTimeUnit = React.useCallback((tickUnit = 'hour', tickStep = 1) => {
|
|
8222
|
+
if (!parsedViewport)
|
|
8223
|
+
return [];
|
|
8224
|
+
const safeTickStep = Math.max(1, Math.floor(tickStep));
|
|
8225
|
+
const candidateTimes = [parsedViewport.start];
|
|
8226
|
+
let cursor = parsedViewport.start.startOf(tickUnit);
|
|
8227
|
+
while (cursor.isBefore(parsedViewport.start)) {
|
|
8228
|
+
cursor = cursor.add(safeTickStep, tickUnit);
|
|
8229
|
+
}
|
|
8230
|
+
while (cursor.isBefore(parsedViewport.end) ||
|
|
8231
|
+
cursor.isSame(parsedViewport.end)) {
|
|
8232
|
+
candidateTimes.push(cursor);
|
|
8233
|
+
cursor = cursor.add(safeTickStep, tickUnit);
|
|
8234
|
+
}
|
|
8235
|
+
candidateTimes.push(parsedViewport.end);
|
|
8236
|
+
const uniqueSortedTicks = Array.from(new Map(candidateTimes.map((time) => [time.valueOf(), time])).values()).sort((a, b) => a.valueOf() - b.valueOf());
|
|
8237
|
+
return uniqueSortedTicks.map((tickTime, index) => {
|
|
8238
|
+
const ratio = tickTime.diff(parsedViewport.start, 'millisecond') /
|
|
8239
|
+
parsedViewport.viewportDurationMs;
|
|
8240
|
+
return {
|
|
8241
|
+
index,
|
|
8242
|
+
percent: clampNumber(ratio * 100, 0, 100),
|
|
8243
|
+
label: tickTime.format(parsedViewport.formatString),
|
|
8244
|
+
};
|
|
8245
|
+
});
|
|
8246
|
+
}, [parsedViewport]);
|
|
8247
|
+
const getTicks = React.useCallback((options) => {
|
|
8248
|
+
const strategy = options?.tickStrategy ?? 'count';
|
|
8249
|
+
if (strategy === 'timeUnit') {
|
|
8250
|
+
return getTicksByTimeUnit(options?.tickUnit, options?.tickStep);
|
|
8251
|
+
}
|
|
8252
|
+
return getTicksByCount(options?.tickCount);
|
|
8253
|
+
}, [getTicksByCount, getTicksByTimeUnit]);
|
|
8254
|
+
return {
|
|
8255
|
+
isValidViewport: Boolean(parsedViewport),
|
|
8256
|
+
toTimeMs,
|
|
8257
|
+
getGeometry,
|
|
8258
|
+
getTimestampPercent,
|
|
8259
|
+
getTicksByCount,
|
|
8260
|
+
getTicksByTimeUnit,
|
|
8261
|
+
getTicks,
|
|
8262
|
+
};
|
|
8263
|
+
}
|
|
8264
|
+
function useTimeViewportBlockGeometry(viewportStart, viewportEnd) {
|
|
8265
|
+
const { isValidViewport, getGeometry, toTimeMs } = useTimeViewport(viewportStart, viewportEnd);
|
|
8266
|
+
return {
|
|
8267
|
+
hasValidViewport: isValidViewport,
|
|
8268
|
+
getGeometry,
|
|
8269
|
+
toTimeMs,
|
|
8270
|
+
};
|
|
8271
|
+
}
|
|
8272
|
+
const getDefaultHeaderFormat = (start, end) => {
|
|
8273
|
+
const durationHours = end.diff(start, 'hour', true);
|
|
8274
|
+
if (durationHours <= 24)
|
|
8275
|
+
return 'HH:mm';
|
|
8276
|
+
if (durationHours <= 24 * 7)
|
|
8277
|
+
return 'ddd HH:mm';
|
|
8278
|
+
return 'MMM D';
|
|
8279
|
+
};
|
|
8280
|
+
function useTimeViewportTicks({ viewportStart, viewportEnd, format, }) {
|
|
8281
|
+
const { isValidViewport, getTicksByCount, getTicksByTimeUnit, getTicks } = useTimeViewport(viewportStart, viewportEnd, format);
|
|
8282
|
+
return {
|
|
8283
|
+
isValidViewport,
|
|
8284
|
+
getTicksByCount,
|
|
8285
|
+
getTicksByTimeUnit,
|
|
8286
|
+
getTicks,
|
|
8287
|
+
};
|
|
8288
|
+
}
|
|
8289
|
+
const useTimeViewportHeader = useTimeViewportTicks;
|
|
8290
|
+
const formatDuration = (durationMs, labels) => {
|
|
8291
|
+
const totalSeconds = Math.max(0, Math.floor(durationMs / 1000));
|
|
8292
|
+
const days = Math.floor(totalSeconds / 86400);
|
|
8293
|
+
const hours = Math.floor((totalSeconds % 86400) / 3600);
|
|
8294
|
+
const minutes = Math.floor((totalSeconds % 3600) / 60);
|
|
8295
|
+
const seconds = totalSeconds % 60;
|
|
8296
|
+
const parts = [];
|
|
8297
|
+
if (days > 0)
|
|
8298
|
+
parts.push(`${days}${labels.daysShort}`);
|
|
8299
|
+
if (hours > 0)
|
|
8300
|
+
parts.push(`${hours}${labels.hoursShort}`);
|
|
8301
|
+
if (minutes > 0)
|
|
8302
|
+
parts.push(`${minutes}${labels.minutesShort}`);
|
|
8303
|
+
if (seconds > 0 || parts.length === 0)
|
|
8304
|
+
parts.push(`${seconds}${labels.secondsShort}`);
|
|
8305
|
+
return parts.join(' ');
|
|
8306
|
+
};
|
|
8307
|
+
/**
|
|
8308
|
+
* A resizable timeline block based on block time range and viewport time range.
|
|
8309
|
+
* Width and offset are automatically derived from datetime overlap.
|
|
8310
|
+
*/
|
|
8311
|
+
function TimeViewportBlock({ start, end, viewportStart, viewportEnd, height = '28px', minWidthPx = 2, borderRadius = 'sm', colorPalette = 'blue', background, label, showLabel = true, hideWhenOutOfView = true, onClick, }) {
|
|
8312
|
+
const { getGeometry } = useTimeViewportBlockGeometry(viewportStart, viewportEnd);
|
|
8313
|
+
const geometry = React.useMemo(() => {
|
|
8314
|
+
return getGeometry(start, end);
|
|
8315
|
+
}, [end, getGeometry, start]);
|
|
8316
|
+
if (!geometry.valid)
|
|
8317
|
+
return null;
|
|
8318
|
+
if (hideWhenOutOfView && geometry.widthPercent <= 0)
|
|
8319
|
+
return null;
|
|
8320
|
+
return (jsxRuntime.jsx(react.Box, { position: "relative", width: "100%", height: height, children: 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: background ?? `${colorPalette}.500`, _dark: {
|
|
8321
|
+
bg: background ?? `${colorPalette}.900`,
|
|
8322
|
+
}, display: "flex", alignItems: "center", justifyContent: "center", px: 2, overflow: "hidden", pointerEvents: "auto", onClick: onClick, cursor: onClick ? 'pointer' : 'default', children: showLabel && label ? (jsxRuntime.jsx(react.Text, { fontSize: "xs", lineClamp: 1, color: "white", _dark: { color: 'gray.100' }, children: label })) : null }) }) }) }));
|
|
8323
|
+
}
|
|
8324
|
+
/**
|
|
8325
|
+
* Vertical marker line for a timestamp in the current viewport.
|
|
8326
|
+
*/
|
|
8327
|
+
function TimeViewportMarkerLine({ timestamp, viewportStart, viewportEnd, height = '100%', colorPalette = 'red', color, lineWidthPx = 2, label, showLabel = true, hideWhenOutOfView = true, }) {
|
|
8328
|
+
const { getTimestampPercent } = useTimeViewport(viewportStart, viewportEnd);
|
|
8329
|
+
const marker = React.useMemo(() => {
|
|
8330
|
+
return getTimestampPercent(timestamp);
|
|
8331
|
+
}, [getTimestampPercent, timestamp]);
|
|
8332
|
+
if (!marker.valid)
|
|
8333
|
+
return null;
|
|
8334
|
+
if (hideWhenOutOfView && !marker.inView)
|
|
8335
|
+
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
|
+
}
|
|
8338
|
+
/**
|
|
8339
|
+
* Header labels for timeline viewport time scale.
|
|
8340
|
+
*/
|
|
8341
|
+
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, }) {
|
|
8342
|
+
const { isValidViewport, getTicks } = useTimeViewport(viewportStart, viewportEnd, format);
|
|
8343
|
+
const ticks = getTicks({ tickStrategy, tickCount, tickUnit, tickStep });
|
|
8344
|
+
const safeTickCount = ticks.length;
|
|
8345
|
+
const transitionValue = animationDurationMs > 0
|
|
8346
|
+
? `transform ${animationDurationMs}ms ${animationEasing}, opacity ${animationDurationMs}ms ${animationEasing}`
|
|
8347
|
+
: undefined;
|
|
8348
|
+
if (!isValidViewport || safeTickCount < 2)
|
|
8349
|
+
return null;
|
|
8350
|
+
return (jsxRuntime.jsx(react.Box, { position: "relative", width: "100%", height: height, borderBottomWidth: showBottomBorder ? '1px' : '0px', borderColor: borderColor, _dark: { borderColor: 'gray.700' }, mb: 2, children: ticks.map((tick) => (jsxRuntime.jsx(react.Box, { position: "absolute", inset: 0, transform: `translateX(${tick.percent}%)`, transition: transitionValue, children: jsxRuntime.jsx(react.Text, { position: "absolute", insetInlineStart: 0, top: "50%", translate: "0 -50%", transform: tick.index === 0
|
|
8351
|
+
? 'translateX(0%)'
|
|
8352
|
+
: tick.index === safeTickCount - 1
|
|
8353
|
+
? 'translateX(-100%)'
|
|
8354
|
+
: 'translateX(-50%)', fontSize: "xs", color: color, _dark: { color: 'gray.300' }, whiteSpace: "nowrap", children: tick.label }) }, `tick-wrap-${tick.index}`))) }));
|
|
8355
|
+
}
|
|
8356
|
+
/**
|
|
8357
|
+
* Vertical grid lines for measuring block positions in the viewport.
|
|
8358
|
+
* Render inside a relative container that also contains blocks.
|
|
8359
|
+
*/
|
|
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))
|
|
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
|
+
const safeMinorDivisions = Math.max(1, minorDivisions);
|
|
8372
|
+
const transitionValue = animationDurationMs > 0
|
|
8373
|
+
? `transform ${animationDurationMs}ms ${animationEasing}, opacity ${animationDurationMs}ms ${animationEasing}`
|
|
8374
|
+
: undefined;
|
|
8375
|
+
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;
|
|
8379
|
+
const segment = [];
|
|
8380
|
+
for (let step = 1; step < safeMinorDivisions; step += 1) {
|
|
8381
|
+
segment.push(base + ((next - base) * step) / safeMinorDivisions);
|
|
8382
|
+
}
|
|
8383
|
+
return segment;
|
|
8384
|
+
}).flat()
|
|
8385
|
+
: [];
|
|
8386
|
+
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
|
+
}
|
|
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, }) {
|
|
8389
|
+
const { getGeometry, toTimeMs } = useTimeViewportBlockGeometry(viewportStart, viewportEnd);
|
|
8390
|
+
const resolvedHeight = typeof height === 'number' ? `${height}px` : height;
|
|
8391
|
+
const expandedBlocks = flattenTrackBlocks(blocks);
|
|
8392
|
+
const parsedBlocks = expandedBlocks
|
|
8393
|
+
.map((block, index) => {
|
|
8394
|
+
if (block.start === undefined || block.end === undefined) {
|
|
8395
|
+
return {
|
|
8396
|
+
block,
|
|
8397
|
+
index,
|
|
8398
|
+
geometry: { valid: false, leftPercent: 0, widthPercent: 0 },
|
|
8399
|
+
startMs: Number.NaN,
|
|
8400
|
+
endMs: Number.NaN,
|
|
8401
|
+
};
|
|
8402
|
+
}
|
|
8403
|
+
const geometry = getGeometry(block.start, block.end);
|
|
8404
|
+
const startMs = toTimeMs(block.start);
|
|
8405
|
+
const endMs = toTimeMs(block.end);
|
|
8406
|
+
return {
|
|
8407
|
+
block,
|
|
8408
|
+
index,
|
|
8409
|
+
geometry,
|
|
8410
|
+
startMs: startMs ?? Number.NaN,
|
|
8411
|
+
endMs: endMs ?? Number.NaN,
|
|
8412
|
+
};
|
|
8413
|
+
})
|
|
8414
|
+
.filter(({ geometry }) => geometry.valid)
|
|
8415
|
+
.filter(({ geometry }) => !hideWhenOutOfView || geometry.widthPercent > 0);
|
|
8416
|
+
const renderBlockNode = (blockItem, indexInLayer) => {
|
|
8417
|
+
const { block, geometry } = blockItem;
|
|
8418
|
+
const handleBlockClick = () => {
|
|
8419
|
+
block.onClick?.(block);
|
|
8420
|
+
onBlockClick?.(block);
|
|
8421
|
+
};
|
|
8422
|
+
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));
|
|
8428
|
+
};
|
|
8429
|
+
const explicitTrackKeys = Array.from(new Set(expandedBlocks
|
|
8430
|
+
.map((item) => item.track)
|
|
8431
|
+
.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
|
+
}) }));
|
|
8453
|
+
}
|
|
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);
|
|
8480
|
+
const prefix = renderTrackPrefix?.({
|
|
8481
|
+
trackIndex,
|
|
8482
|
+
trackBlocks,
|
|
8483
|
+
trackKey: undefined,
|
|
8484
|
+
});
|
|
8485
|
+
const suffix = renderTrackSuffix?.({
|
|
8486
|
+
trackIndex,
|
|
8487
|
+
trackBlocks,
|
|
8488
|
+
trackKey: undefined,
|
|
8489
|
+
});
|
|
8490
|
+
return (jsxRuntime.jsx(TimeViewportTrackRow, { trackKey: `track-row-${trackIndex}`, blocks: track, 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 (jsxRuntime.jsxs(react.VStack, { align: "stretch", gap: 2, p: 3, children: [jsxRuntime.jsxs(react.HStack, { justify: "space-between", gap: 2, children: [jsxRuntime.jsxs(react.HStack, { gap: 2, children: [jsxRuntime.jsx(react.IconButton, { "aria-label": mergedLabels.zoomOut, onClick: zoomOut, disabled: !canZoomOut, size: "sm", variant: "outline", children: jsxRuntime.jsx(lu.LuZoomOut, {}) }), jsxRuntime.jsx(react.IconButton, { "aria-label": mergedLabels.zoomIn, onClick: zoomIn, disabled: !canZoomIn, size: "sm", variant: "outline", children: jsxRuntime.jsx(lu.LuZoomIn, {}) })] }), showResetButton ? (jsxRuntime.jsx(react.Button, { size: "sm", variant: "ghost", onClick: reset, disabled: disabled, colorPalette: "blue", children: mergedLabels.reset })) : null] }), jsxRuntime.jsxs(react.Text, { fontSize: "sm", color: "gray.700", _dark: { color: 'gray.200' }, children: [mergedLabels.visibleRange, ": ", visibleRangeText] }), jsxRuntime.jsxs(react.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 = React.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 = React.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 = React.useRef(clampNumber(resetDurationMs ?? durationMs, safeMinDurationMs, safeMaxDurationMs));
|
|
8522
|
+
const hasValidDisplayRange = end.isAfter(start);
|
|
8523
|
+
const applyDurationAroundCenter = React.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 = React.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 = React.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 = React.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
|
+
|
|
7990
8575
|
const snakeToLabel = (str) => {
|
|
7991
8576
|
return str
|
|
7992
8577
|
.split("_") // Split by underscore
|
|
@@ -8936,6 +9521,13 @@ exports.TableSelector = TableSelector;
|
|
|
8936
9521
|
exports.TableSorter = TableSorter;
|
|
8937
9522
|
exports.TableViewer = TableViewer;
|
|
8938
9523
|
exports.TextCell = TextCell;
|
|
9524
|
+
exports.TimeRangeZoom = TimeRangeZoom;
|
|
9525
|
+
exports.TimeViewportBlock = TimeViewportBlock;
|
|
9526
|
+
exports.TimeViewportBlocks = TimeViewportBlocks;
|
|
9527
|
+
exports.TimeViewportGrid = TimeViewportGrid;
|
|
9528
|
+
exports.TimeViewportHeader = TimeViewportHeader;
|
|
9529
|
+
exports.TimeViewportMarkerLine = TimeViewportMarkerLine;
|
|
9530
|
+
exports.TimeViewportRoot = TimeViewportRoot;
|
|
8939
9531
|
exports.ViewDialog = ViewDialog;
|
|
8940
9532
|
exports.defaultRenderDisplay = defaultRenderDisplay;
|
|
8941
9533
|
exports.getMultiDates = getMultiDates;
|
|
@@ -8944,3 +9536,8 @@ exports.useDataTable = useDataTable;
|
|
|
8944
9536
|
exports.useDataTableContext = useDataTableContext;
|
|
8945
9537
|
exports.useDataTableServer = useDataTableServer;
|
|
8946
9538
|
exports.useForm = useForm;
|
|
9539
|
+
exports.useTimeRangeZoom = useTimeRangeZoom;
|
|
9540
|
+
exports.useTimeViewport = useTimeViewport;
|
|
9541
|
+
exports.useTimeViewportBlockGeometry = useTimeViewportBlockGeometry;
|
|
9542
|
+
exports.useTimeViewportHeader = useTimeViewportHeader;
|
|
9543
|
+
exports.useTimeViewportTicks = useTimeViewportTicks;
|