@vuer-ai/vuer-uikit 0.0.96 → 0.0.98
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/README.md +106 -7
- package/cli/dial-cli.js +49 -7
- package/dist/SyncScroll/SyncScroll.cjs +10 -10
- package/dist/SyncScroll/SyncScroll.mjs +3 -3
- package/dist/SyncScroll/index.cjs +10 -10
- package/dist/SyncScroll/index.mjs +3 -3
- package/dist/chunk-3HEZVWRW.mjs +62 -0
- package/dist/chunk-4KWGGESI.cjs +494 -0
- package/dist/{chunk-CCMKL2OA.cjs → chunk-7GWDO25E.cjs} +2 -2
- package/dist/chunk-A5LCX2UQ.cjs +208 -0
- package/dist/chunk-BEJIZ56L.mjs +300 -0
- package/dist/chunk-C7VGRU3O.mjs +283 -0
- package/dist/{chunk-LONOMMFA.cjs → chunk-LJMNHTTG.cjs} +21 -21
- package/dist/chunk-O66RESRR.cjs +285 -0
- package/dist/{chunk-RINTUFYQ.cjs → chunk-RMK6W774.cjs} +24 -19
- package/dist/{chunk-BFQ2WL5U.mjs → chunk-TTYSYGVE.mjs} +2 -2
- package/dist/chunk-VA3PEYFM.mjs +489 -0
- package/dist/chunk-VBBJSIY7.cjs +308 -0
- package/dist/{chunk-AIINOWEH.mjs → chunk-W4JCKCW7.mjs} +5 -5
- package/dist/chunk-WWGF6TBZ.mjs +206 -0
- package/dist/chunk-ZGN4UEJR.cjs +679 -0
- package/dist/chunk-ZQLRMOUW.mjs +661 -0
- package/dist/dial/DialPanel.cjs +24 -24
- package/dist/dial/DialPanel.mjs +23 -23
- package/dist/dial/DialProvider.cjs +3 -3
- package/dist/dial/DialProvider.d.cts +1 -0
- package/dist/dial/DialProvider.d.ts +1 -0
- package/dist/dial/DialProvider.example.cjs +72 -0
- package/dist/dial/DialProvider.example.d.cts +7 -0
- package/dist/dial/DialProvider.example.d.ts +7 -0
- package/dist/dial/DialProvider.example.mjs +68 -0
- package/dist/dial/DialProvider.mjs +1 -1
- package/dist/dial/index.cjs +42 -42
- package/dist/dial/index.mjs +23 -23
- package/dist/dial/wrapped-inputs/ControlledInputs.cjs +27 -27
- package/dist/dial/wrapped-inputs/ControlledInputs.mjs +23 -23
- package/dist/dial/wrapped-inputs/DialInputs.cjs +34 -34
- package/dist/dial/wrapped-inputs/DialInputs.mjs +23 -23
- package/dist/dial/wrapped-inputs/DialVectorInput.cjs +24 -24
- package/dist/dial/wrapped-inputs/DialVectorInput.mjs +23 -23
- package/dist/dial/wrapped-inputs/index.cjs +39 -39
- package/dist/dial/wrapped-inputs/index.mjs +23 -23
- package/dist/highlight-cursor/cursor-provider.cjs +3 -3
- package/dist/highlight-cursor/cursor-provider.mjs +2 -2
- package/dist/highlight-cursor/enhanced-components.cjs +10 -10
- package/dist/highlight-cursor/enhanced-components.mjs +5 -5
- package/dist/highlight-cursor/index.cjs +16 -16
- package/dist/highlight-cursor/index.mjs +6 -6
- package/dist/hooks/index.cjs +5 -5
- package/dist/hooks/index.mjs +1 -1
- package/dist/index.cjs +190 -190
- package/dist/index.mjs +23 -23
- package/dist/ui/UIKitBadge.cjs +6 -6
- package/dist/ui/UIKitBadge.mjs +2 -2
- package/dist/ui/avatar.cjs +1 -1
- package/dist/ui/avatar.mjs +1 -1
- package/dist/ui/badge.cjs +1 -1
- package/dist/ui/badge.d.cts +1 -1
- package/dist/ui/badge.d.ts +1 -1
- package/dist/ui/badge.mjs +1 -1
- package/dist/ui/button.cjs +1 -1
- package/dist/ui/button.mjs +1 -1
- package/dist/ui/card.cjs +1 -1
- package/dist/ui/card.mjs +1 -1
- package/dist/ui/checkbox.cjs +1 -1
- package/dist/ui/checkbox.mjs +1 -1
- package/dist/ui/collapsible.cjs +1 -1
- package/dist/ui/collapsible.mjs +1 -1
- package/dist/ui/drawer.cjs +1 -1
- package/dist/ui/drawer.mjs +1 -1
- package/dist/ui/dropdown.cjs +1 -1
- package/dist/ui/dropdown.mjs +1 -1
- package/dist/ui/index.cjs +107 -107
- package/dist/ui/index.mjs +17 -17
- package/dist/ui/inputs/color-input.cjs +1 -1
- package/dist/ui/inputs/color-input.mjs +1 -1
- package/dist/ui/inputs/index.cjs +11 -11
- package/dist/ui/inputs/index.mjs +3 -3
- package/dist/ui/inputs/input-numbers.cjs +1 -1
- package/dist/ui/inputs/input-numbers.mjs +1 -1
- package/dist/ui/inputs/input.cjs +1 -1
- package/dist/ui/inputs/input.d.cts +1 -1
- package/dist/ui/inputs/input.d.ts +1 -1
- package/dist/ui/inputs/input.mjs +1 -1
- package/dist/ui/inputs/number-inputs/CmInput.cjs +1 -1
- package/dist/ui/inputs/number-inputs/CmInput.mjs +1 -1
- package/dist/ui/inputs/number-inputs/DegInput.cjs +1 -1
- package/dist/ui/inputs/number-inputs/DegInput.mjs +1 -1
- package/dist/ui/inputs/number-inputs/EulerDegInput.cjs +1 -1
- package/dist/ui/inputs/number-inputs/EulerDegInput.mjs +1 -1
- package/dist/ui/inputs/number-inputs/EulerInput.cjs +1 -1
- package/dist/ui/inputs/number-inputs/EulerInput.mjs +1 -1
- package/dist/ui/inputs/number-inputs/EulerRadInput.cjs +1 -1
- package/dist/ui/inputs/number-inputs/EulerRadInput.mjs +1 -1
- package/dist/ui/inputs/number-inputs/InchInput.cjs +1 -1
- package/dist/ui/inputs/number-inputs/InchInput.mjs +1 -1
- package/dist/ui/inputs/number-inputs/IntInput.cjs +1 -1
- package/dist/ui/inputs/number-inputs/IntInput.mjs +1 -1
- package/dist/ui/inputs/number-inputs/KVectorInput.cjs +1 -1
- package/dist/ui/inputs/number-inputs/KVectorInput.mjs +1 -1
- package/dist/ui/inputs/number-inputs/QuaternionInput.cjs +1 -1
- package/dist/ui/inputs/number-inputs/QuaternionInput.mjs +1 -1
- package/dist/ui/inputs/number-inputs/RadInput.cjs +1 -1
- package/dist/ui/inputs/number-inputs/RadInput.mjs +1 -1
- package/dist/ui/inputs/number-inputs/TimeInput.cjs +1 -1
- package/dist/ui/inputs/number-inputs/TimeInput.mjs +1 -1
- package/dist/ui/inputs/number-inputs/Vec3Input.cjs +1 -1
- package/dist/ui/inputs/number-inputs/Vec3Input.mjs +1 -1
- package/dist/ui/inputs/number-inputs/VectorInput.cjs +1 -1
- package/dist/ui/inputs/number-inputs/VectorInput.mjs +1 -1
- package/dist/ui/inputs/number-inputs/index.cjs +11 -11
- package/dist/ui/inputs/number-inputs/index.mjs +3 -3
- package/dist/ui/inputs/presets-input.cjs +1 -1
- package/dist/ui/inputs/presets-input.mjs +1 -1
- package/dist/ui/label.cjs +1 -1
- package/dist/ui/label.mjs +1 -1
- package/dist/ui/layout.cjs +1 -1
- package/dist/ui/layout.mjs +1 -1
- package/dist/ui/layouts/dock-layout/DockLayoutView.cjs +1 -1
- package/dist/ui/layouts/dock-layout/DockLayoutView.mjs +1 -1
- package/dist/ui/layouts/dock-layout/LayoutSlots.cjs +1 -1
- package/dist/ui/layouts/dock-layout/LayoutSlots.mjs +1 -1
- package/dist/ui/layouts/dock-layout/index.cjs +1 -1
- package/dist/ui/layouts/dock-layout/index.mjs +1 -1
- package/dist/ui/layouts/index.cjs +2 -2
- package/dist/ui/layouts/index.mjs +2 -2
- package/dist/ui/layouts/liquid-layout/LayoutSlots.cjs +1 -1
- package/dist/ui/layouts/liquid-layout/LayoutSlots.mjs +1 -1
- package/dist/ui/layouts/liquid-layout/LiquidLayoutView.cjs +1 -1
- package/dist/ui/layouts/liquid-layout/LiquidLayoutView.mjs +1 -1
- package/dist/ui/layouts/liquid-layout/index.cjs +1 -1
- package/dist/ui/layouts/liquid-layout/index.mjs +1 -1
- package/dist/ui/modal.cjs +1 -1
- package/dist/ui/modal.mjs +1 -1
- package/dist/ui/navigation.cjs +1 -1
- package/dist/ui/navigation.mjs +1 -1
- package/dist/ui/pagination.cjs +1 -1
- package/dist/ui/pagination.mjs +1 -1
- package/dist/ui/panel.cjs +1 -1
- package/dist/ui/panel.mjs +1 -1
- package/dist/ui/popover.cjs +1 -1
- package/dist/ui/popover.mjs +1 -1
- package/dist/ui/radio-group.cjs +1 -1
- package/dist/ui/radio-group.mjs +1 -1
- package/dist/ui/resizable.cjs +1 -1
- package/dist/ui/resizable.mjs +1 -1
- package/dist/ui/select.cjs +1 -1
- package/dist/ui/select.d.cts +1 -1
- package/dist/ui/select.d.ts +1 -1
- package/dist/ui/select.mjs +1 -1
- package/dist/ui/separator.cjs +1 -1
- package/dist/ui/separator.mjs +1 -1
- package/dist/ui/sheet.cjs +1 -1
- package/dist/ui/sheet.mjs +1 -1
- package/dist/ui/sidebar.cjs +26 -26
- package/dist/ui/sidebar.mjs +2 -2
- package/dist/ui/simple-tree-view.cjs +1 -1
- package/dist/ui/simple-tree-view.mjs +1 -1
- package/dist/ui/skeleton.cjs +1 -1
- package/dist/ui/skeleton.mjs +1 -1
- package/dist/ui/slider.cjs +1 -1
- package/dist/ui/slider.mjs +1 -1
- package/dist/ui/switch.cjs +1 -1
- package/dist/ui/switch.mjs +1 -1
- package/dist/ui/table.cjs +1 -1
- package/dist/ui/table.mjs +1 -1
- package/dist/ui/tabs.cjs +1 -1
- package/dist/ui/tabs.mjs +1 -1
- package/dist/ui/textarea.cjs +1 -1
- package/dist/ui/textarea.d.cts +1 -1
- package/dist/ui/textarea.d.ts +1 -1
- package/dist/ui/textarea.mjs +1 -1
- package/dist/ui/theme/ThemeToggles.cjs +1 -1
- package/dist/ui/theme/ThemeToggles.mjs +1 -1
- package/dist/ui/theme/index.cjs +1 -1
- package/dist/ui/theme/index.mjs +1 -1
- package/dist/ui/toggle-buttons.cjs +1 -1
- package/dist/ui/toggle-buttons.mjs +1 -1
- package/dist/ui/toggle-group.cjs +1 -1
- package/dist/ui/toggle-group.mjs +1 -1
- package/dist/ui/toggle.cjs +1 -1
- package/dist/ui/toggle.mjs +1 -1
- package/dist/ui/toolbar.cjs +1 -1
- package/dist/ui/toolbar.mjs +1 -1
- package/dist/ui/tooltip.cjs +1 -1
- package/dist/ui/tooltip.mjs +1 -1
- package/dist/ui/tree-view/TreeSearchBar.cjs +1 -1
- package/dist/ui/tree-view/TreeSearchBar.mjs +1 -1
- package/dist/ui/tree-view/TreeView.cjs +1 -1
- package/dist/ui/tree-view/TreeView.mjs +1 -1
- package/dist/ui/tree-view/index.cjs +6 -6
- package/dist/ui/tree-view/index.mjs +2 -2
- package/dist/ui/tree-view-legacy.cjs +9 -9
- package/dist/ui/tree-view-legacy.mjs +5 -5
- package/dist/ui/waterfall/CursorOverlay.cjs +1 -1
- package/dist/ui/waterfall/CursorOverlay.mjs +1 -1
- package/dist/ui/waterfall/TimelineEvent.cjs +1 -1
- package/dist/ui/waterfall/TimelineEvent.mjs +1 -1
- package/dist/ui/waterfall/TimelineProcessBar.cjs +1 -1
- package/dist/ui/waterfall/TimelineProcessBar.mjs +1 -1
- package/dist/ui/waterfall/Wedges.cjs +1 -1
- package/dist/ui/waterfall/Wedges.mjs +1 -1
- package/dist/ui/waterfall/index.cjs +8 -8
- package/dist/ui/waterfall/index.mjs +7 -7
- package/package.json +28 -2
- package/src/SyncScroll/README.md +283 -0
- package/src/SyncScroll/SyncScroll.tsx +361 -0
- package/src/SyncScroll/index.ts +22 -0
- package/src/SyncScroll/useSyncScroll.tsx +226 -0
- package/src/cli/dial-cli.ts +1133 -0
- package/src/dial/DialPanel.tsx +208 -0
- package/src/dial/DialProvider.example.tsx +80 -0
- package/src/dial/DialProvider.tsx +138 -0
- package/src/dial/index.ts +26 -0
- package/src/dial/wrapped-inputs/ControlledInputs.tsx +176 -0
- package/src/dial/wrapped-inputs/DialInputs.tsx +401 -0
- package/src/dial/wrapped-inputs/DialVectorInput.tsx +125 -0
- package/src/dial/wrapped-inputs/index.ts +25 -0
- package/src/highlight-cursor/cursor-context.tsx +23 -0
- package/src/highlight-cursor/cursor-provider.tsx +264 -0
- package/src/highlight-cursor/enhanced-components.tsx +16 -0
- package/src/highlight-cursor/index.ts +21 -0
- package/src/highlight-cursor/tabs-cursor-context.tsx +121 -0
- package/src/highlight-cursor/types.ts +40 -0
- package/src/highlight-cursor/with-cursor.tsx +144 -0
- package/src/hooks/clientOnly.tsx +54 -0
- package/src/hooks/cn.ts +33 -0
- package/src/hooks/index.ts +9 -0
- package/src/hooks/useDocument.tsx +18 -0
- package/src/hooks/useDragSelect.ts +116 -0
- package/src/hooks/useIsMobile.ts +35 -0
- package/src/hooks/useLocalStorage.ts +122 -0
- package/src/hooks/useLocation.tsx +95 -0
- package/src/hooks/useQueryParams.tsx +31 -0
- package/src/hooks/useWindow.tsx +18 -0
- package/src/index.css +5 -0
- package/src/index.ts +5 -0
- package/src/styles/.editorconfig +0 -0
- package/src/styles/theme.css +152 -0
- package/src/styles/toast.css +67 -0
- package/src/styles/variables.css +229 -0
- package/src/ui/UIKitBadge.tsx +171 -0
- package/src/ui/avatar.tsx +221 -0
- package/src/ui/badge.tsx +74 -0
- package/src/ui/button.tsx +143 -0
- package/src/ui/card.tsx +146 -0
- package/src/ui/checkbox.tsx +78 -0
- package/src/ui/collapsible.tsx +43 -0
- package/src/ui/drag-selectable/DragSelectProvider.tsx +178 -0
- package/src/ui/drag-selectable/createSelectable.tsx +43 -0
- package/src/ui/drag-selectable/index.ts +2 -0
- package/src/ui/drawer.tsx +137 -0
- package/src/ui/dropdown.tsx +707 -0
- package/src/ui/icons/MouseCursorIcons.tsx +39 -0
- package/src/ui/icons/cursor.tsx +38 -0
- package/src/ui/icons/index.ts +2 -0
- package/src/ui/index.ts +45 -0
- package/src/ui/inputs/color-input.tsx +61 -0
- package/src/ui/inputs/index.tsx +5 -0
- package/src/ui/inputs/input-numbers.tsx +394 -0
- package/src/ui/inputs/input.tsx +155 -0
- package/src/ui/inputs/number-inputs/CmInput.tsx +26 -0
- package/src/ui/inputs/number-inputs/DegInput.tsx +26 -0
- package/src/ui/inputs/number-inputs/EulerDegInput.tsx +14 -0
- package/src/ui/inputs/number-inputs/EulerInput.tsx +30 -0
- package/src/ui/inputs/number-inputs/EulerRadInput.tsx +14 -0
- package/src/ui/inputs/number-inputs/InchInput.tsx +26 -0
- package/src/ui/inputs/number-inputs/IntInput.tsx +46 -0
- package/src/ui/inputs/number-inputs/KVectorInput.tsx +20 -0
- package/src/ui/inputs/number-inputs/QuaternionInput.tsx +27 -0
- package/src/ui/inputs/number-inputs/RadInput.tsx +26 -0
- package/src/ui/inputs/number-inputs/TimeInput.tsx +26 -0
- package/src/ui/inputs/number-inputs/Vec3Input.tsx +26 -0
- package/src/ui/inputs/number-inputs/VectorInput.tsx +38 -0
- package/src/ui/inputs/number-inputs/index.ts +38 -0
- package/src/ui/inputs/presets-input.tsx +59 -0
- package/src/ui/label.tsx +35 -0
- package/src/ui/layout.tsx +43 -0
- package/src/ui/layouts/dock-layout/DockLayoutView.tsx +128 -0
- package/src/ui/layouts/dock-layout/LayoutSlots.tsx +85 -0
- package/src/ui/layouts/dock-layout/index.tsx +2 -0
- package/src/ui/layouts/index.ts +2 -0
- package/src/ui/layouts/liquid-layout/LayoutSlots.tsx +82 -0
- package/src/ui/layouts/liquid-layout/LiquidLayoutView.tsx +76 -0
- package/src/ui/layouts/liquid-layout/index.ts +1 -0
- package/src/ui/modal.tsx +211 -0
- package/src/ui/navigation.tsx +86 -0
- package/src/ui/pagination.tsx +129 -0
- package/src/ui/panel.tsx +146 -0
- package/src/ui/popover.tsx +112 -0
- package/src/ui/radio-group.tsx +63 -0
- package/src/ui/resizable.tsx +52 -0
- package/src/ui/select.tsx +365 -0
- package/src/ui/separator.tsx +26 -0
- package/src/ui/sheet.tsx +257 -0
- package/src/ui/sidebar.tsx +695 -0
- package/src/ui/simple-tree-view.tsx +604 -0
- package/src/ui/skeleton.tsx +15 -0
- package/src/ui/slider.tsx +204 -0
- package/src/ui/switch.tsx +45 -0
- package/src/ui/table.tsx +99 -0
- package/src/ui/tabs.tsx +192 -0
- package/src/ui/textarea.tsx +55 -0
- package/src/ui/theme/ThemeProvider.tsx +319 -0
- package/src/ui/theme/ThemeToggles.tsx +84 -0
- package/src/ui/theme/index.ts +21 -0
- package/src/ui/theme/themeScript.tsx +179 -0
- package/src/ui/theme/types.ts +61 -0
- package/src/ui/toast.tsx +30 -0
- package/src/ui/toggle-buttons.tsx +290 -0
- package/src/ui/toggle-group.tsx +236 -0
- package/src/ui/toggle.tsx +94 -0
- package/src/ui/toolbar.tsx +54 -0
- package/src/ui/tooltip.tsx +171 -0
- package/src/ui/tree-view/TreeSearchBar.tsx +88 -0
- package/src/ui/tree-view/TreeView.tsx +232 -0
- package/src/ui/tree-view/hooks.tsx +289 -0
- package/src/ui/tree-view/index.ts +4 -0
- package/src/ui/tree-view/types.ts +23 -0
- package/src/ui/tree-view-legacy.tsx +644 -0
- package/src/ui/version-badge.tsx +0 -0
- package/src/ui/waterfall/CursorOverlay.tsx +96 -0
- package/src/ui/waterfall/NavigationControls.tsx +42 -0
- package/src/ui/waterfall/Tick.tsx +51 -0
- package/src/ui/waterfall/TimeRuleEventDot.tsx +19 -0
- package/src/ui/waterfall/TimelineEvent.tsx +60 -0
- package/src/ui/waterfall/TimelineProcessBar.tsx +207 -0
- package/src/ui/waterfall/Wedges.tsx +67 -0
- package/src/ui/waterfall/WheelZoomContext.tsx +128 -0
- package/src/ui/waterfall/hooks/useTimelineState.tsx +134 -0
- package/src/ui/waterfall/hooks/useViewport.tsx +293 -0
- package/src/ui/waterfall/index.tsx +326 -0
- package/src/ui/waterfall/types.ts +20 -0
- package/src/ui/waterfall/utils.ts +91 -0
- package/dist/chunk-W2YAQV5N.mjs +0 -57
- package/dist/{chunk-QLCEHV4Q.mjs → chunk-2OZK5DY5.mjs} +2 -2
- package/dist/{chunk-Z35DNFRZ.cjs → chunk-7IS37C3P.cjs} +2 -2
- package/dist/{chunk-4AOAH77D.mjs → chunk-A2PCEL5S.mjs} +2 -2
- package/dist/{chunk-K4VD5PPY.mjs → chunk-BIUDC66P.mjs} +1 -1
- package/dist/{chunk-RKJR6RZU.cjs → chunk-OYNLQTHW.cjs} +1 -1
- package/dist/{chunk-VE3XLQLO.cjs → chunk-QUFZWF4E.cjs} +2 -2
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { Magnet } from "lucide-react";
|
|
2
|
+
import React, { HTMLProps } from "react";
|
|
3
|
+
|
|
4
|
+
import { cn } from "../../hooks";
|
|
5
|
+
|
|
6
|
+
export interface TimelineCursorProps extends HTMLProps<HTMLDivElement> {
|
|
7
|
+
/** Position as percentage (0-100) */
|
|
8
|
+
left?: number;
|
|
9
|
+
/** Label text to display */
|
|
10
|
+
label?: string;
|
|
11
|
+
/** Cursor color */
|
|
12
|
+
color?: string;
|
|
13
|
+
/** Whether to show the readout tooltip */
|
|
14
|
+
showReadout?: boolean;
|
|
15
|
+
/** Whether to show magnet icon (only when showReadout is true) */
|
|
16
|
+
showMagnet?: boolean;
|
|
17
|
+
/** Readout variant - 'active' for interactive cursors, 'static' for fixed markers */
|
|
18
|
+
variant?: "active" | "static";
|
|
19
|
+
/** Additional CSS classes */
|
|
20
|
+
className?: string;
|
|
21
|
+
/** Z-index override */
|
|
22
|
+
zIndex?: number;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Simple timeline cursor component.
|
|
27
|
+
*
|
|
28
|
+
* @example
|
|
29
|
+
* // Simple cursor line
|
|
30
|
+
* <CursorOverlay left={50} color="blue" />
|
|
31
|
+
*
|
|
32
|
+
* // Cursor with readout
|
|
33
|
+
* <CursorOverlay left={50} label="2.5s" showReadout />
|
|
34
|
+
*/
|
|
35
|
+
export function CursorOverlay({
|
|
36
|
+
left = 0,
|
|
37
|
+
label,
|
|
38
|
+
color,
|
|
39
|
+
showReadout = false,
|
|
40
|
+
showMagnet = false,
|
|
41
|
+
variant = "active",
|
|
42
|
+
zIndex = 20,
|
|
43
|
+
}: TimelineCursorProps) {
|
|
44
|
+
const leftValue = `${left}%`;
|
|
45
|
+
|
|
46
|
+
// Default colors based on variant
|
|
47
|
+
const lineColor = color || (variant === "active" ? "rgb(239 68 68)" : "rgb(148 163 184)");
|
|
48
|
+
|
|
49
|
+
return (
|
|
50
|
+
<>
|
|
51
|
+
{/* Cursor Line */}
|
|
52
|
+
<div
|
|
53
|
+
className="pointer-events-none absolute top-0 z-75 h-full w-px"
|
|
54
|
+
style={{
|
|
55
|
+
left: leftValue,
|
|
56
|
+
backgroundColor: lineColor,
|
|
57
|
+
zIndex,
|
|
58
|
+
}}
|
|
59
|
+
/>
|
|
60
|
+
|
|
61
|
+
{/* Readout Tooltip */}
|
|
62
|
+
{showReadout && (
|
|
63
|
+
<div
|
|
64
|
+
className={cn(
|
|
65
|
+
"absolute top-1",
|
|
66
|
+
"flex items-center justify-center",
|
|
67
|
+
variant === "active" ? "pointer-events-none" : "pointer-events-auto",
|
|
68
|
+
"px-2 py-0.5",
|
|
69
|
+
"text-uk-sm z-80",
|
|
70
|
+
"border-line-primary/50",
|
|
71
|
+
variant === "active" && [
|
|
72
|
+
"bg-bg-primary rounded-uk-sm border",
|
|
73
|
+
"shadow-[0_4px_16px_0_var(--color-shadow-secondary)]",
|
|
74
|
+
],
|
|
75
|
+
variant === "static" && [
|
|
76
|
+
"bg-bg-secondary backdrop-blur-sm",
|
|
77
|
+
"rounded-uk-sm",
|
|
78
|
+
"text-text-secondary",
|
|
79
|
+
],
|
|
80
|
+
)}
|
|
81
|
+
style={{
|
|
82
|
+
left: leftValue,
|
|
83
|
+
transform: "translateX(-50%)",
|
|
84
|
+
minWidth: "11ch",
|
|
85
|
+
zIndex,
|
|
86
|
+
}}
|
|
87
|
+
>
|
|
88
|
+
{showMagnet && (
|
|
89
|
+
<Magnet className="text-text-secondary mr-1.5 size-3 transition-opacity" />
|
|
90
|
+
)}
|
|
91
|
+
<span className="font-mono tabular-nums">{label}</span>
|
|
92
|
+
</div>
|
|
93
|
+
)}
|
|
94
|
+
</>
|
|
95
|
+
);
|
|
96
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { ChevronLeft, ChevronRight } from "lucide-react";
|
|
2
|
+
import React from "react";
|
|
3
|
+
|
|
4
|
+
import { formatDuration } from "./utils";
|
|
5
|
+
|
|
6
|
+
// Component props interface
|
|
7
|
+
export interface NavigationControlsProps {
|
|
8
|
+
viewDuration: number;
|
|
9
|
+
handlePan: (direction: "left" | "right") => void;
|
|
10
|
+
handleZoomDragStart: (e: React.MouseEvent) => void;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function NavigationControls({
|
|
14
|
+
viewDuration,
|
|
15
|
+
handlePan,
|
|
16
|
+
handleZoomDragStart,
|
|
17
|
+
}: NavigationControlsProps) {
|
|
18
|
+
return (
|
|
19
|
+
<div className="sticky left-1/2 z-20 w-max">
|
|
20
|
+
<div className="bg-bg-primary/75 flex items-center gap-2 rounded-full p-1 text-sm shadow-[0_4px_16px_0_var(--color-shadow-secondary)] backdrop-blur-[2px]">
|
|
21
|
+
<button
|
|
22
|
+
onClick={() => handlePan("left")}
|
|
23
|
+
className="hover:bg-bg-secondary rounded-full p-1"
|
|
24
|
+
>
|
|
25
|
+
<ChevronLeft className="size-4" />
|
|
26
|
+
</button>
|
|
27
|
+
<span
|
|
28
|
+
className="text-uk-sm hover:bg-bg-secondary/50 w-24 cursor-col-resize rounded px-1 py-0.5 text-center font-mono transition-colors select-none"
|
|
29
|
+
onMouseDown={handleZoomDragStart}
|
|
30
|
+
>
|
|
31
|
+
{formatDuration(viewDuration)}
|
|
32
|
+
</span>
|
|
33
|
+
<button
|
|
34
|
+
onClick={() => handlePan("right")}
|
|
35
|
+
className="hover:bg-bg-secondary rounded-full p-1"
|
|
36
|
+
>
|
|
37
|
+
<ChevronRight className="size-4" />
|
|
38
|
+
</button>
|
|
39
|
+
</div>
|
|
40
|
+
</div>
|
|
41
|
+
);
|
|
42
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
|
|
3
|
+
export interface TimelineTickProps {
|
|
4
|
+
/** Time value for this tick */
|
|
5
|
+
time: number;
|
|
6
|
+
/** Label to display */
|
|
7
|
+
label: string;
|
|
8
|
+
/** Function to convert time to percentage */
|
|
9
|
+
timeToPercent: (time: number) => number;
|
|
10
|
+
/** Z-index for layering */
|
|
11
|
+
zIndex?: number;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Timeline tick mark with label.
|
|
16
|
+
* Shows time divisions on the timeline ruler.
|
|
17
|
+
*/
|
|
18
|
+
export function Tick({ time, label, timeToPercent, zIndex = 10 }: TimelineTickProps) {
|
|
19
|
+
const naturalCenterPercent = timeToPercent(time);
|
|
20
|
+
|
|
21
|
+
if (naturalCenterPercent < -20 || naturalCenterPercent > 120) {
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Keep label visible even when tick is near edges
|
|
26
|
+
const labelHalfWidthPercent = 3;
|
|
27
|
+
const clampedCenterPercent = Math.min(
|
|
28
|
+
100 - labelHalfWidthPercent,
|
|
29
|
+
Math.max(labelHalfWidthPercent, naturalCenterPercent),
|
|
30
|
+
);
|
|
31
|
+
|
|
32
|
+
return (
|
|
33
|
+
<>
|
|
34
|
+
{/* Tick line */}
|
|
35
|
+
<div
|
|
36
|
+
className="bg-line-primary absolute top-0 h-full w-px"
|
|
37
|
+
style={{ left: `${naturalCenterPercent}%` }}
|
|
38
|
+
/>
|
|
39
|
+
{/* Label */}
|
|
40
|
+
<div
|
|
41
|
+
className="bg-bg-primary text-text-secondary rounded-uk-xs text-uk-sm pointer-events-none absolute top-1/2 -translate-x-1/2 -translate-y-1/2 px-1 backdrop-blur-sm"
|
|
42
|
+
style={{
|
|
43
|
+
left: `${clampedCenterPercent}%`,
|
|
44
|
+
zIndex,
|
|
45
|
+
}}
|
|
46
|
+
>
|
|
47
|
+
{label}
|
|
48
|
+
</div>
|
|
49
|
+
</>
|
|
50
|
+
);
|
|
51
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export interface TimeRuleEventDotProps {
|
|
2
|
+
/** Position as percentage (0-100) */
|
|
3
|
+
percent: number;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Small dot indicator for key events on the timeline ruler.
|
|
8
|
+
* Shows where significant events occur for visual reference and snapping.
|
|
9
|
+
*/
|
|
10
|
+
export function TimeRuleEventDot({ percent }: TimeRuleEventDotProps) {
|
|
11
|
+
if (percent < 0 || percent > 100) return null;
|
|
12
|
+
|
|
13
|
+
return (
|
|
14
|
+
<div
|
|
15
|
+
className="bg-bg-tertiary absolute top-1/2 z-0 h-1 w-1 -translate-y-1/2 rounded-full"
|
|
16
|
+
style={{ left: `${percent}%` }}
|
|
17
|
+
/>
|
|
18
|
+
);
|
|
19
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
|
|
3
|
+
import { LogItemWithMeta } from "./types";
|
|
4
|
+
import { colorClasses } from "./utils";
|
|
5
|
+
import { cn } from "../../hooks";
|
|
6
|
+
|
|
7
|
+
export interface TimelineEventProps {
|
|
8
|
+
item: LogItemWithMeta;
|
|
9
|
+
isHovered: boolean;
|
|
10
|
+
onMouseEnter: () => void;
|
|
11
|
+
onMouseLeave: () => void;
|
|
12
|
+
onClick?: (time: number) => void;
|
|
13
|
+
timeToPercent: (time: number) => number;
|
|
14
|
+
index?: number;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Timeline event component for instant events.
|
|
19
|
+
* Renders events that occur at a specific point in time (no duration).
|
|
20
|
+
* Displays as a diamond shape on the timeline.
|
|
21
|
+
*/
|
|
22
|
+
export function TimelineEvent({
|
|
23
|
+
item,
|
|
24
|
+
isHovered,
|
|
25
|
+
onMouseEnter,
|
|
26
|
+
onMouseLeave,
|
|
27
|
+
onClick,
|
|
28
|
+
timeToPercent,
|
|
29
|
+
}: TimelineEventProps) {
|
|
30
|
+
// Only render if item has a time property (instant event)
|
|
31
|
+
if (item?.time === undefined || item?.time === null) return null;
|
|
32
|
+
|
|
33
|
+
return (
|
|
34
|
+
<div
|
|
35
|
+
className={cn("relative h-[32px]", isHovered && "bg-bg-secondary")}
|
|
36
|
+
onMouseEnter={onMouseEnter}
|
|
37
|
+
onMouseLeave={onMouseLeave}
|
|
38
|
+
>
|
|
39
|
+
{/* Diamond shape for instant event */}
|
|
40
|
+
<div
|
|
41
|
+
className={cn(
|
|
42
|
+
"absolute top-1/2 -translate-x-1/2 -translate-y-1/2",
|
|
43
|
+
"border-bg-primary h-3 w-3 rotate-45 border-2",
|
|
44
|
+
item.color && colorClasses[item.color],
|
|
45
|
+
onClick && "cursor-pointer transition-transform hover:scale-110",
|
|
46
|
+
)}
|
|
47
|
+
style={{
|
|
48
|
+
left: `${timeToPercent(item.time)}%`,
|
|
49
|
+
}}
|
|
50
|
+
onClick={(e) => {
|
|
51
|
+
if (onClick && item.time !== undefined) {
|
|
52
|
+
e.stopPropagation();
|
|
53
|
+
onClick(item.time);
|
|
54
|
+
}
|
|
55
|
+
}}
|
|
56
|
+
title={item.label}
|
|
57
|
+
/>
|
|
58
|
+
</div>
|
|
59
|
+
);
|
|
60
|
+
}
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
|
|
3
|
+
import { LogItemWithMeta } from "./types";
|
|
4
|
+
import { borderColorClasses, colorClasses, formatDuration } from "./utils";
|
|
5
|
+
import { cn } from "../../hooks";
|
|
6
|
+
|
|
7
|
+
export interface TimelineEventBarProps {
|
|
8
|
+
item: LogItemWithMeta;
|
|
9
|
+
isHovered: boolean;
|
|
10
|
+
onMouseEnter: () => void;
|
|
11
|
+
onMouseLeave: () => void;
|
|
12
|
+
onClick?: (time: number) => void;
|
|
13
|
+
viewStart: number;
|
|
14
|
+
viewWindow: number;
|
|
15
|
+
timeToPercent: (time: number) => number;
|
|
16
|
+
index?: number;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Individual timeline event bar component.
|
|
21
|
+
* Renders a single event with its launch wait line and execution bar.
|
|
22
|
+
*/
|
|
23
|
+
export function TimelineProcessBar({
|
|
24
|
+
item,
|
|
25
|
+
isHovered,
|
|
26
|
+
onMouseEnter,
|
|
27
|
+
onMouseLeave,
|
|
28
|
+
onClick,
|
|
29
|
+
viewStart,
|
|
30
|
+
viewWindow,
|
|
31
|
+
timeToPercent,
|
|
32
|
+
}: TimelineEventBarProps) {
|
|
33
|
+
const viewEnd = viewStart + viewWindow;
|
|
34
|
+
const isHaltedStep = item.isHaltedStep;
|
|
35
|
+
const barStart = item.startTime;
|
|
36
|
+
const barEnd =
|
|
37
|
+
item.startTime !== undefined && item.duration !== undefined
|
|
38
|
+
? item.startTime + item.duration
|
|
39
|
+
: undefined;
|
|
40
|
+
|
|
41
|
+
return (
|
|
42
|
+
<div
|
|
43
|
+
className={cn("relative h-[32px] w-full", isHovered && "bg-bg-secondary")}
|
|
44
|
+
onMouseEnter={onMouseEnter}
|
|
45
|
+
onMouseLeave={onMouseLeave}
|
|
46
|
+
>
|
|
47
|
+
{/* Launch Wait Line */}
|
|
48
|
+
{item.createTime !== undefined &&
|
|
49
|
+
item.startTime !== undefined &&
|
|
50
|
+
item.createTime < item.startTime &&
|
|
51
|
+
item.color && (
|
|
52
|
+
<div
|
|
53
|
+
className="absolute top-1/2 h-2 -translate-y-1/2"
|
|
54
|
+
style={{
|
|
55
|
+
left: `${timeToPercent(item.createTime)}%`,
|
|
56
|
+
width: `${((item.startTime - item.createTime) / viewWindow) * 100}%`,
|
|
57
|
+
}}
|
|
58
|
+
>
|
|
59
|
+
<div
|
|
60
|
+
className={cn(
|
|
61
|
+
"absolute top-1/2 left-0 h-2 w-px -translate-y-1/2",
|
|
62
|
+
colorClasses[item.color],
|
|
63
|
+
)}
|
|
64
|
+
/>
|
|
65
|
+
<div
|
|
66
|
+
className={cn(
|
|
67
|
+
"absolute top-1/2 w-full -translate-y-1/2 border-t",
|
|
68
|
+
borderColorClasses[item.color],
|
|
69
|
+
)}
|
|
70
|
+
/>
|
|
71
|
+
<div
|
|
72
|
+
className={cn(
|
|
73
|
+
"absolute top-1/2 right-0 h-2 w-px -translate-y-1/2",
|
|
74
|
+
colorClasses[item.color],
|
|
75
|
+
)}
|
|
76
|
+
/>
|
|
77
|
+
</div>
|
|
78
|
+
)}
|
|
79
|
+
|
|
80
|
+
{/* Execution Bar */}
|
|
81
|
+
{item.startTime !== undefined && item.duration !== undefined && !isHaltedStep && (
|
|
82
|
+
<div
|
|
83
|
+
className={cn(
|
|
84
|
+
"rounded-uk-xs absolute top-1/2 flex h-5 -translate-y-1/2 items-center justify-center overflow-hidden",
|
|
85
|
+
item.color && colorClasses[item.color],
|
|
86
|
+
item.hasStripes &&
|
|
87
|
+
"bg-[repeating-linear-gradient(-45deg,transparent,transparent_4px,rgba(0,0,0,0.1)_4px,rgba(0,0,0,0.1)_8px)]",
|
|
88
|
+
onClick && "cursor-pointer",
|
|
89
|
+
)}
|
|
90
|
+
style={{
|
|
91
|
+
left: `${timeToPercent(item.startTime)}%`,
|
|
92
|
+
width: `${(item.duration / viewWindow) * 100}%`,
|
|
93
|
+
}}
|
|
94
|
+
onClick={(e) => {
|
|
95
|
+
if (onClick && item.startTime !== undefined) {
|
|
96
|
+
e.stopPropagation();
|
|
97
|
+
// Calculate click position within the bar
|
|
98
|
+
const rect = e.currentTarget.getBoundingClientRect();
|
|
99
|
+
const clickX = e.clientX - rect.left;
|
|
100
|
+
const clickPercent = clickX / rect.width;
|
|
101
|
+
const clickTime = item.startTime + item.duration! * clickPercent;
|
|
102
|
+
onClick(clickTime);
|
|
103
|
+
}
|
|
104
|
+
}}
|
|
105
|
+
/>
|
|
106
|
+
)}
|
|
107
|
+
|
|
108
|
+
{/* Visible Label for Execution Bar */}
|
|
109
|
+
{item.startTime !== undefined &&
|
|
110
|
+
item.duration !== undefined &&
|
|
111
|
+
!isHaltedStep &&
|
|
112
|
+
(() => {
|
|
113
|
+
const visibleStart = Math.max(barStart!, viewStart);
|
|
114
|
+
const visibleEnd = Math.min(barEnd!, viewEnd);
|
|
115
|
+
|
|
116
|
+
if (visibleEnd <= visibleStart) return null;
|
|
117
|
+
|
|
118
|
+
const visibleDuration = visibleEnd - visibleStart;
|
|
119
|
+
const visibleWidthPercent = (visibleDuration / viewWindow) * 100;
|
|
120
|
+
|
|
121
|
+
if (visibleWidthPercent < 4) return null;
|
|
122
|
+
|
|
123
|
+
return (
|
|
124
|
+
<div
|
|
125
|
+
className="pointer-events-none absolute top-1/2 flex h-5 -translate-y-1/2 items-center justify-center"
|
|
126
|
+
style={{
|
|
127
|
+
left: `${timeToPercent(visibleStart)}%`,
|
|
128
|
+
width: `${visibleWidthPercent}%`,
|
|
129
|
+
}}
|
|
130
|
+
>
|
|
131
|
+
<span
|
|
132
|
+
className={cn(
|
|
133
|
+
"text-uk-sm font-medium whitespace-nowrap",
|
|
134
|
+
item.color === "gray-light" || item.color === "gray-medium"
|
|
135
|
+
? "text-slate-600 dark:text-slate-300"
|
|
136
|
+
: "text-text-withbg",
|
|
137
|
+
)}
|
|
138
|
+
>
|
|
139
|
+
{formatDuration(item.duration)}
|
|
140
|
+
</span>
|
|
141
|
+
</div>
|
|
142
|
+
);
|
|
143
|
+
})()}
|
|
144
|
+
|
|
145
|
+
{/* Start Circle */}
|
|
146
|
+
{item.startTime !== undefined &&
|
|
147
|
+
item.duration !== undefined &&
|
|
148
|
+
!isHaltedStep &&
|
|
149
|
+
item.color && (
|
|
150
|
+
<div
|
|
151
|
+
className={cn(
|
|
152
|
+
"bg-bg-primary absolute top-1/2 z-10 size-2 -translate-x-1/2 -translate-y-1/2 rounded-full border-2",
|
|
153
|
+
borderColorClasses[item.color],
|
|
154
|
+
)}
|
|
155
|
+
style={{
|
|
156
|
+
left: `${timeToPercent(item.startTime)}%`,
|
|
157
|
+
}}
|
|
158
|
+
/>
|
|
159
|
+
)}
|
|
160
|
+
|
|
161
|
+
{/* Special Halted Step Visualization */}
|
|
162
|
+
{isHaltedStep && item.startTime !== undefined && item.duration !== undefined && (
|
|
163
|
+
<div
|
|
164
|
+
className="absolute top-1/2 flex h-full -translate-y-1/2 items-center"
|
|
165
|
+
style={{
|
|
166
|
+
left: `${timeToPercent(item.startTime)}%`,
|
|
167
|
+
width: `${(item.duration / viewWindow) * 100}%`,
|
|
168
|
+
}}
|
|
169
|
+
>
|
|
170
|
+
<div className="relative flex h-full w-full items-center justify-center">
|
|
171
|
+
<div className="bg-line-secondary absolute top-1/2 left-0 h-2 w-px -translate-y-1/2" />
|
|
172
|
+
<div className="bg-line-secondary w-full border-t border-dashed" />
|
|
173
|
+
<div className="bg-line-secondary absolute top-1/2 right-0 h-2 w-px -translate-y-1/2" />
|
|
174
|
+
</div>
|
|
175
|
+
</div>
|
|
176
|
+
)}
|
|
177
|
+
|
|
178
|
+
{/* Halted Step Label */}
|
|
179
|
+
{isHaltedStep &&
|
|
180
|
+
item.startTime !== undefined &&
|
|
181
|
+
item.duration !== undefined &&
|
|
182
|
+
(() => {
|
|
183
|
+
const visibleStart = Math.max(barStart!, viewStart);
|
|
184
|
+
const visibleEnd = Math.min(barEnd!, viewEnd);
|
|
185
|
+
|
|
186
|
+
if (visibleEnd <= visibleStart) return null;
|
|
187
|
+
|
|
188
|
+
const visibleDuration = visibleEnd - visibleStart;
|
|
189
|
+
const visibleWidthPercent = (visibleDuration / viewWindow) * 100;
|
|
190
|
+
|
|
191
|
+
if (visibleWidthPercent < 8) return null;
|
|
192
|
+
|
|
193
|
+
return (
|
|
194
|
+
<div
|
|
195
|
+
className="pointer-events-none absolute top-1/2 flex -translate-y-1/2 items-center justify-center"
|
|
196
|
+
style={{
|
|
197
|
+
left: `${timeToPercent(visibleStart)}%`,
|
|
198
|
+
width: `${visibleWidthPercent}%`,
|
|
199
|
+
}}
|
|
200
|
+
>
|
|
201
|
+
<span className="text-text-secondary text-xs italic">Halted</span>
|
|
202
|
+
</div>
|
|
203
|
+
);
|
|
204
|
+
})()}
|
|
205
|
+
</div>
|
|
206
|
+
);
|
|
207
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
|
|
3
|
+
import { LogItemWithMeta } from "./types";
|
|
4
|
+
import { cn } from "../../hooks";
|
|
5
|
+
|
|
6
|
+
interface LeftWedgeProps {
|
|
7
|
+
item: LogItemWithMeta;
|
|
8
|
+
classes: Record<string, string>;
|
|
9
|
+
viewStart: number;
|
|
10
|
+
index: number;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
interface RightWedgeProps {
|
|
14
|
+
item: LogItemWithMeta;
|
|
15
|
+
classes: Record<string, string>;
|
|
16
|
+
viewEnd: number;
|
|
17
|
+
index: number;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function LeftWedge({ item, classes, viewStart }: LeftWedgeProps) {
|
|
21
|
+
const barEnd =
|
|
22
|
+
item.startTime !== undefined && item.duration !== undefined
|
|
23
|
+
? item.startTime + item.duration
|
|
24
|
+
: undefined;
|
|
25
|
+
const isOffscreenLeft =
|
|
26
|
+
(barEnd !== undefined && barEnd < viewStart) ||
|
|
27
|
+
(item.time !== undefined && item.time < viewStart);
|
|
28
|
+
|
|
29
|
+
return (
|
|
30
|
+
<div
|
|
31
|
+
key={`left-wedge=${item.id}`}
|
|
32
|
+
className={cn("relative flex h-[32px] w-0 flex-row justify-items-center")}
|
|
33
|
+
>
|
|
34
|
+
{isOffscreenLeft && item.color ? (
|
|
35
|
+
<div
|
|
36
|
+
className={cn(
|
|
37
|
+
"my-auto border-y-[6px] border-r-[5px] border-y-transparent",
|
|
38
|
+
classes[item.color],
|
|
39
|
+
)}
|
|
40
|
+
/>
|
|
41
|
+
) : null}
|
|
42
|
+
</div>
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function RightWedge({ item, classes, viewEnd }: RightWedgeProps) {
|
|
47
|
+
const barStart = item.startTime;
|
|
48
|
+
const isOffscreenRight =
|
|
49
|
+
(barStart !== undefined && barStart > viewEnd) ||
|
|
50
|
+
(item.time !== undefined && item.time > viewEnd);
|
|
51
|
+
|
|
52
|
+
return (
|
|
53
|
+
<div
|
|
54
|
+
key={`right-wedge=${item.id}`}
|
|
55
|
+
className={cn("relative flex h-[32px] w-0 flex-row justify-items-center")}
|
|
56
|
+
>
|
|
57
|
+
{isOffscreenRight && item.color ? (
|
|
58
|
+
<div
|
|
59
|
+
className={cn(
|
|
60
|
+
"my-auto border-y-[6px] border-l-[5px] border-y-transparent",
|
|
61
|
+
classes[item.color],
|
|
62
|
+
)}
|
|
63
|
+
/>
|
|
64
|
+
) : null}
|
|
65
|
+
</div>
|
|
66
|
+
);
|
|
67
|
+
}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import React, { useCallback, useEffect, useRef, type HTMLProps, RefObject } from "react";
|
|
2
|
+
|
|
3
|
+
export interface WheelZoomContextProps extends HTMLProps<HTMLDivElement> {
|
|
4
|
+
/** Current view start position */
|
|
5
|
+
viewStart: number;
|
|
6
|
+
/** Current view duration/viewWindow */
|
|
7
|
+
viewDuration: number;
|
|
8
|
+
/** Callback when view start changes */
|
|
9
|
+
onViewStartChange: (newStart: number) => void;
|
|
10
|
+
/** Callback when view duration/viewWindow changes */
|
|
11
|
+
onWindowChange: (newDuration: number) => void;
|
|
12
|
+
/** Minimum zoom viewWindow duration in seconds (default: 0.01) */
|
|
13
|
+
minWindow?: number;
|
|
14
|
+
/** Maximum zoom viewWindow duration in seconds (default: Infinity) */
|
|
15
|
+
maxWindow?: number;
|
|
16
|
+
/** Zoom factor for mouse wheel zoom (default: 1.1) */
|
|
17
|
+
zoomFactor?: number;
|
|
18
|
+
/** Enable wheel handling for pan and zoom (default: true) */
|
|
19
|
+
enabled?: boolean;
|
|
20
|
+
/** Children elements to wrap with wheel zoom functionality */
|
|
21
|
+
children: React.ReactNode;
|
|
22
|
+
/** Additional CSS classes for the wrapper */
|
|
23
|
+
className?: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Wrapper component that provides wheel zoom and pan functionality to its children.
|
|
28
|
+
* Uses both React synthetic events and native events for proper scroll prevention.
|
|
29
|
+
*
|
|
30
|
+
* - Normal wheel: Pan horizontally and vertically
|
|
31
|
+
* - Shift/Alt + wheel: Zoom in/out while maintaining cursor position
|
|
32
|
+
*
|
|
33
|
+
* @example
|
|
34
|
+
* ```tsx
|
|
35
|
+
* <WheelZoomContext
|
|
36
|
+
* viewStart={viewStart}
|
|
37
|
+
* viewWindow={viewWindow}
|
|
38
|
+
* onViewStartChange={setViewStart}
|
|
39
|
+
* onWindowChange={setViewDuration}
|
|
40
|
+
* minWindow={0.01}
|
|
41
|
+
* maxWindow={100}
|
|
42
|
+
* >
|
|
43
|
+
* <TimelineContent />
|
|
44
|
+
* </WheelZoomContext>
|
|
45
|
+
* ```
|
|
46
|
+
*/
|
|
47
|
+
export function WheelZoomContext({
|
|
48
|
+
ref,
|
|
49
|
+
className,
|
|
50
|
+
viewStart,
|
|
51
|
+
viewDuration,
|
|
52
|
+
onViewStartChange,
|
|
53
|
+
onWindowChange,
|
|
54
|
+
minWindow = 0.01,
|
|
55
|
+
maxWindow = Infinity,
|
|
56
|
+
zoomFactor = 1.1,
|
|
57
|
+
enabled = true,
|
|
58
|
+
children,
|
|
59
|
+
}: WheelZoomContextProps) {
|
|
60
|
+
const localRef = useRef<HTMLDivElement>(null);
|
|
61
|
+
const containerRef = (ref as RefObject<HTMLDivElement>) || localRef;
|
|
62
|
+
|
|
63
|
+
// Native event listener to prevent default scrolling
|
|
64
|
+
useEffect(() => {
|
|
65
|
+
const container = containerRef.current;
|
|
66
|
+
if (!container || !enabled) return;
|
|
67
|
+
|
|
68
|
+
const preventScroll = (e: WheelEvent) => {
|
|
69
|
+
e.preventDefault();
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
// Add passive: false to ensure preventDefault works
|
|
73
|
+
container.addEventListener("wheel", preventScroll, { passive: false });
|
|
74
|
+
|
|
75
|
+
return () => {
|
|
76
|
+
container.removeEventListener("wheel", preventScroll);
|
|
77
|
+
};
|
|
78
|
+
}, [enabled]);
|
|
79
|
+
|
|
80
|
+
const handleWheel = useCallback(
|
|
81
|
+
(e: React.WheelEvent<HTMLDivElement>) => {
|
|
82
|
+
if (!enabled) return;
|
|
83
|
+
|
|
84
|
+
const container = containerRef.current;
|
|
85
|
+
if (!container) return;
|
|
86
|
+
|
|
87
|
+
if (e.shiftKey || e.altKey) {
|
|
88
|
+
// Zoom: maintain cursor position
|
|
89
|
+
const rect = container.getBoundingClientRect();
|
|
90
|
+
const cursorX = e.clientX - rect.left;
|
|
91
|
+
const cursorRatio = cursorX / container.offsetWidth;
|
|
92
|
+
const timeAtCursor = viewStart + cursorRatio * viewDuration;
|
|
93
|
+
|
|
94
|
+
// Calculate new duration
|
|
95
|
+
const scaleFactor = e.deltaY < 0 ? 1 / zoomFactor : zoomFactor;
|
|
96
|
+
const newDuration = Math.max(minWindow, Math.min(maxWindow, viewDuration * scaleFactor));
|
|
97
|
+
|
|
98
|
+
if (newDuration === viewDuration) return;
|
|
99
|
+
|
|
100
|
+
// Adjust view start to keep cursor position stable
|
|
101
|
+
const newViewStart = timeAtCursor - cursorRatio * newDuration;
|
|
102
|
+
|
|
103
|
+
onWindowChange(newDuration);
|
|
104
|
+
onViewStartChange(newViewStart);
|
|
105
|
+
} else {
|
|
106
|
+
// Pan: convert wheel delta to time offset
|
|
107
|
+
const panAmount = ((e.deltaX + e.deltaY) / container.offsetWidth) * viewDuration;
|
|
108
|
+
onViewStartChange(viewStart + panAmount);
|
|
109
|
+
}
|
|
110
|
+
},
|
|
111
|
+
[
|
|
112
|
+
enabled,
|
|
113
|
+
viewStart,
|
|
114
|
+
viewDuration,
|
|
115
|
+
onViewStartChange,
|
|
116
|
+
onWindowChange,
|
|
117
|
+
minWindow,
|
|
118
|
+
maxWindow,
|
|
119
|
+
zoomFactor,
|
|
120
|
+
],
|
|
121
|
+
);
|
|
122
|
+
|
|
123
|
+
return (
|
|
124
|
+
<div ref={containerRef} className={className} onWheel={handleWheel}>
|
|
125
|
+
{children}
|
|
126
|
+
</div>
|
|
127
|
+
);
|
|
128
|
+
}
|