@dtour/viewer 0.1.0
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/Dtour.d.ts +46 -0
- package/dist/Dtour.d.ts.map +1 -0
- package/dist/DtourViewer.d.ts +24 -0
- package/dist/DtourViewer.d.ts.map +1 -0
- package/dist/components/AxisOverlay.d.ts +9 -0
- package/dist/components/AxisOverlay.d.ts.map +1 -0
- package/dist/components/CircularSlider.d.ts +16 -0
- package/dist/components/CircularSlider.d.ts.map +1 -0
- package/dist/components/ColorLegend.d.ts +2 -0
- package/dist/components/ColorLegend.d.ts.map +1 -0
- package/dist/components/DtourToolbar.d.ts +5 -0
- package/dist/components/DtourToolbar.d.ts.map +1 -0
- package/dist/components/Gallery.d.ts +12 -0
- package/dist/components/Gallery.d.ts.map +1 -0
- package/dist/components/LassoOverlay.d.ts +9 -0
- package/dist/components/LassoOverlay.d.ts.map +1 -0
- package/dist/components/Logo.d.ts +2 -0
- package/dist/components/Logo.d.ts.map +1 -0
- package/dist/components/ui/button.d.ts +12 -0
- package/dist/components/ui/button.d.ts.map +1 -0
- package/dist/components/ui/dropdown-menu.d.ts +10 -0
- package/dist/components/ui/dropdown-menu.d.ts.map +1 -0
- package/dist/components/ui/slider.d.ts +6 -0
- package/dist/components/ui/slider.d.ts.map +1 -0
- package/dist/components/ui/tooltip.d.ts +8 -0
- package/dist/components/ui/tooltip.d.ts.map +1 -0
- package/dist/hooks/useAnimatePosition.d.ts +13 -0
- package/dist/hooks/useAnimatePosition.d.ts.map +1 -0
- package/dist/hooks/useGrandTour.d.ts +14 -0
- package/dist/hooks/useGrandTour.d.ts.map +1 -0
- package/dist/hooks/useLongPressIndicator.d.ts +5 -0
- package/dist/hooks/useLongPressIndicator.d.ts.map +1 -0
- package/dist/hooks/useModeCycling.d.ts +12 -0
- package/dist/hooks/useModeCycling.d.ts.map +1 -0
- package/dist/hooks/usePlayback.d.ts +9 -0
- package/dist/hooks/usePlayback.d.ts.map +1 -0
- package/dist/hooks/useScatter.d.ts +10 -0
- package/dist/hooks/useScatter.d.ts.map +1 -0
- package/dist/hooks/useSystemTheme.d.ts +6 -0
- package/dist/hooks/useSystemTheme.d.ts.map +1 -0
- package/dist/index.d.ts +16 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/layout/gallery-positions.d.ts +38 -0
- package/dist/layout/gallery-positions.d.ts.map +1 -0
- package/dist/layout/selector-size.d.ts +15 -0
- package/dist/layout/selector-size.d.ts.map +1 -0
- package/dist/lib/color-utils.d.ts +7 -0
- package/dist/lib/color-utils.d.ts.map +1 -0
- package/dist/lib/gram-schmidt.d.ts +9 -0
- package/dist/lib/gram-schmidt.d.ts.map +1 -0
- package/dist/lib/utils.d.ts +3 -0
- package/dist/lib/utils.d.ts.map +1 -0
- package/dist/portal-container.d.ts +10 -0
- package/dist/portal-container.d.ts.map +1 -0
- package/dist/radial-chart/RadialChart.d.ts +13 -0
- package/dist/radial-chart/RadialChart.d.ts.map +1 -0
- package/dist/radial-chart/arc-path.d.ts +23 -0
- package/dist/radial-chart/arc-path.d.ts.map +1 -0
- package/dist/radial-chart/index.d.ts +5 -0
- package/dist/radial-chart/index.d.ts.map +1 -0
- package/dist/radial-chart/parse-metrics.d.ts +10 -0
- package/dist/radial-chart/parse-metrics.d.ts.map +1 -0
- package/dist/radial-chart/types.d.ts +23 -0
- package/dist/radial-chart/types.d.ts.map +1 -0
- package/dist/spec.d.ts +42 -0
- package/dist/spec.d.ts.map +1 -0
- package/dist/state/atoms.d.ts +150 -0
- package/dist/state/atoms.d.ts.map +1 -0
- package/dist/state/spec-sync.d.ts +5 -0
- package/dist/state/spec-sync.d.ts.map +1 -0
- package/dist/viewer.css +3 -0
- package/dist/viewer.js +14501 -0
- package/dist/views.d.ts +30 -0
- package/dist/views.d.ts.map +1 -0
- package/package.json +48 -0
- package/src/Dtour.tsx +300 -0
- package/src/DtourViewer.tsx +541 -0
- package/src/components/AxisOverlay.tsx +224 -0
- package/src/components/CircularSlider.tsx +202 -0
- package/src/components/ColorLegend.tsx +178 -0
- package/src/components/DtourToolbar.tsx +642 -0
- package/src/components/Gallery.tsx +166 -0
- package/src/components/LassoOverlay.tsx +240 -0
- package/src/components/Logo.tsx +37 -0
- package/src/components/ui/button.tsx +36 -0
- package/src/components/ui/dropdown-menu.tsx +92 -0
- package/src/components/ui/slider.tsx +89 -0
- package/src/components/ui/tooltip.tsx +45 -0
- package/src/hooks/useAnimatePosition.ts +102 -0
- package/src/hooks/useGrandTour.ts +176 -0
- package/src/hooks/useLongPressIndicator.ts +342 -0
- package/src/hooks/useModeCycling.ts +64 -0
- package/src/hooks/usePlayback.ts +54 -0
- package/src/hooks/useScatter.ts +162 -0
- package/src/hooks/useSystemTheme.ts +19 -0
- package/src/index.ts +55 -0
- package/src/layout/gallery-positions.ts +105 -0
- package/src/layout/selector-size.ts +135 -0
- package/src/lib/color-utils.ts +22 -0
- package/src/lib/gram-schmidt.ts +41 -0
- package/src/lib/utils.ts +4 -0
- package/src/portal-container.tsx +14 -0
- package/src/radial-chart/RadialChart.tsx +184 -0
- package/src/radial-chart/arc-path.ts +80 -0
- package/src/radial-chart/index.ts +4 -0
- package/src/radial-chart/parse-metrics.ts +99 -0
- package/src/radial-chart/types.ts +23 -0
- package/src/spec.ts +48 -0
- package/src/state/atoms.ts +169 -0
- package/src/state/spec-sync.ts +190 -0
- package/src/styles.css +44 -0
- package/src/views.ts +76 -0
- package/tsconfig.json +12 -0
- package/vite.config.ts +21 -0
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
import { useAtom, useAtomValue, useSetAtom } from 'jotai';
|
|
2
|
+
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
3
|
+
import { useAnimatePosition } from '../hooks/useAnimatePosition.ts';
|
|
4
|
+
import { computeGallerySizes } from '../layout/gallery-positions.ts';
|
|
5
|
+
import { cn } from '../lib/utils.ts';
|
|
6
|
+
import {
|
|
7
|
+
guidedSuspendedAtom,
|
|
8
|
+
previewCountAtom,
|
|
9
|
+
previewScaleAtom,
|
|
10
|
+
selectedKeyframeAtom,
|
|
11
|
+
tourPlayingAtom,
|
|
12
|
+
tourPositionAtom,
|
|
13
|
+
} from '../state/atoms.ts';
|
|
14
|
+
|
|
15
|
+
export type GalleryProps = {
|
|
16
|
+
/** Fixed pool of preview canvas elements (created at scatter init). */
|
|
17
|
+
previewCanvases: HTMLCanvasElement[];
|
|
18
|
+
/** Container width (px). */
|
|
19
|
+
containerWidth: number;
|
|
20
|
+
/** Container height (px). */
|
|
21
|
+
containerHeight: number;
|
|
22
|
+
/** Is toolbar visible? */
|
|
23
|
+
isToolbarVisible: boolean;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export const Gallery = ({
|
|
27
|
+
previewCanvases,
|
|
28
|
+
containerWidth,
|
|
29
|
+
containerHeight,
|
|
30
|
+
isToolbarVisible,
|
|
31
|
+
}: GalleryProps) => {
|
|
32
|
+
const previewCount = useAtomValue(previewCountAtom);
|
|
33
|
+
const previewScale = useAtomValue(previewScaleAtom);
|
|
34
|
+
const position = useAtomValue(tourPositionAtom);
|
|
35
|
+
const [selectedKeyframe, setSelectedKeyframe] = useAtom(selectedKeyframeAtom);
|
|
36
|
+
const setPlaying = useSetAtom(tourPlayingAtom);
|
|
37
|
+
const setGuidedSuspended = useSetAtom(guidedSuspendedAtom);
|
|
38
|
+
const { animateTo } = useAnimatePosition();
|
|
39
|
+
const wrapperRefs = useRef<(HTMLDivElement | null)[]>([]);
|
|
40
|
+
const [hoveredIndex, setHoveredIndex] = useState<number | null>(null);
|
|
41
|
+
|
|
42
|
+
// Grid area = container minus its CSS insets.
|
|
43
|
+
// When the toolbar is visible overlayOffsetY shifts the wrapper down by
|
|
44
|
+
// toolbarHeight/2 = 20px. Bump the top & bottom CSS insets by the same
|
|
45
|
+
// amount so the *visual* padding from the visible edges stays at 32px.
|
|
46
|
+
const verticalInset = isToolbarVisible ? 36 : 16; // 16 + toolbarHeight/2
|
|
47
|
+
const gridWidth = containerWidth - 32; // left-4 + right-4 = 32px
|
|
48
|
+
const gridHeight = containerHeight - 2 * verticalInset;
|
|
49
|
+
|
|
50
|
+
const { gridTemplateColumns, gridTemplateRows, sizes } = useMemo(
|
|
51
|
+
() => computeGallerySizes(gridWidth, gridHeight, previewCount, previewScale),
|
|
52
|
+
[gridWidth, gridHeight, previewCount, previewScale],
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
// Adopt each canvas into its wrapper div (once, on mount)
|
|
56
|
+
useEffect(() => {
|
|
57
|
+
for (let i = 0; i < previewCanvases.length; i++) {
|
|
58
|
+
const wrapper = wrapperRefs.current[i];
|
|
59
|
+
const canvas = previewCanvases[i];
|
|
60
|
+
if (wrapper && canvas && canvas.parentElement !== wrapper) {
|
|
61
|
+
wrapper.appendChild(canvas);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}, [previewCanvases]);
|
|
65
|
+
|
|
66
|
+
const currentKeyframe = Math.round(position * previewCount) % previewCount;
|
|
67
|
+
|
|
68
|
+
const getBorderColor = (i: number): string | undefined => {
|
|
69
|
+
const isActive = i === selectedKeyframe || i === currentKeyframe;
|
|
70
|
+
if (isActive || i === hoveredIndex) return 'var(--color-dtour-highlight)';
|
|
71
|
+
return undefined;
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
const getBorderWidth = (i: number): number | undefined => {
|
|
75
|
+
const isActive = i === selectedKeyframe || i === currentKeyframe;
|
|
76
|
+
if (isActive) return 2;
|
|
77
|
+
return undefined;
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
const getBoxShadow = (i: number): string => {
|
|
81
|
+
if (i === selectedKeyframe)
|
|
82
|
+
return '0 0 8px color-mix(in srgb, var(--color-dtour-highlight) 30%, transparent)';
|
|
83
|
+
if (i === currentKeyframe)
|
|
84
|
+
return '0 0 8px color-mix(in srgb, var(--color-dtour-highlight) 30%, transparent)';
|
|
85
|
+
if (i === hoveredIndex) return '0 0 6px rgba(255, 255, 255, 0.15)';
|
|
86
|
+
return 'none';
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
const handleClick = useCallback(
|
|
90
|
+
(i: number) => {
|
|
91
|
+
setGuidedSuspended(false);
|
|
92
|
+
setSelectedKeyframe(i);
|
|
93
|
+
setPlaying(false);
|
|
94
|
+
animateTo(i / previewCount);
|
|
95
|
+
},
|
|
96
|
+
[previewCount, setSelectedKeyframe, setPlaying, setGuidedSuspended, animateTo],
|
|
97
|
+
);
|
|
98
|
+
|
|
99
|
+
const k = previewCount / 4;
|
|
100
|
+
|
|
101
|
+
return (
|
|
102
|
+
<div
|
|
103
|
+
className="absolute left-4 right-4 grid gap-8 justify-between content-between pointer-events-none"
|
|
104
|
+
style={{ top: verticalInset, bottom: verticalInset, gridTemplateColumns, gridTemplateRows }}
|
|
105
|
+
>
|
|
106
|
+
{previewCanvases.map((_, i) => {
|
|
107
|
+
const visible = i < previewCount;
|
|
108
|
+
|
|
109
|
+
let col: number;
|
|
110
|
+
let row: number;
|
|
111
|
+
if (i < k) {
|
|
112
|
+
// top edge: left → right
|
|
113
|
+
row = 0;
|
|
114
|
+
col = i;
|
|
115
|
+
} else if (i < 2 * k) {
|
|
116
|
+
// right edge: top → bottom
|
|
117
|
+
row = i - k;
|
|
118
|
+
col = k;
|
|
119
|
+
} else if (i < 3 * k) {
|
|
120
|
+
// bottom edge: right → left
|
|
121
|
+
row = k;
|
|
122
|
+
col = 3 * k - i;
|
|
123
|
+
} else {
|
|
124
|
+
// left edge: bottom → top
|
|
125
|
+
row = 4 * k - i;
|
|
126
|
+
col = 0;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const verticalAlignment =
|
|
130
|
+
row === 0 ? 'items-start' : row < k ? 'items-center' : 'items-end';
|
|
131
|
+
const horizontalAlignment =
|
|
132
|
+
col === 0 ? 'justify-start' : col < k ? 'justify-center' : 'justify-end';
|
|
133
|
+
|
|
134
|
+
return (
|
|
135
|
+
<div
|
|
136
|
+
// biome-ignore lint/suspicious/noArrayIndexKey: fixed pool keyed by slot index
|
|
137
|
+
key={i}
|
|
138
|
+
className={cn('flex pointer-events-none', verticalAlignment, horizontalAlignment)}
|
|
139
|
+
style={{ gridColumn: col + 1, gridRow: row + 1 }}
|
|
140
|
+
>
|
|
141
|
+
<div
|
|
142
|
+
ref={(el) => {
|
|
143
|
+
wrapperRefs.current[i] = el;
|
|
144
|
+
}}
|
|
145
|
+
onClick={visible ? () => handleClick(i) : undefined}
|
|
146
|
+
onKeyDown={undefined}
|
|
147
|
+
onMouseEnter={visible ? () => setHoveredIndex(i) : undefined}
|
|
148
|
+
onMouseLeave={visible ? () => setHoveredIndex(null) : undefined}
|
|
149
|
+
className={cn(
|
|
150
|
+
'pointer-events-auto overflow-hidden border border-dtour-border rounded transition-[border-color,border-width,box-shadow] duration-200 ease-in-out z-20',
|
|
151
|
+
visible ? 'block cursor-pointer' : 'hidden',
|
|
152
|
+
)}
|
|
153
|
+
style={{
|
|
154
|
+
width: visible ? sizes[i] : 0,
|
|
155
|
+
height: visible ? sizes[i] : 0,
|
|
156
|
+
borderColor: getBorderColor(i),
|
|
157
|
+
borderWidth: getBorderWidth(i),
|
|
158
|
+
boxShadow: getBoxShadow(i),
|
|
159
|
+
}}
|
|
160
|
+
/>
|
|
161
|
+
</div>
|
|
162
|
+
);
|
|
163
|
+
})}
|
|
164
|
+
</div>
|
|
165
|
+
);
|
|
166
|
+
};
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
import type { ScatterInstance } from '@dtour/scatter';
|
|
2
|
+
import { useAtomValue, useSetAtom } from 'jotai';
|
|
3
|
+
import { useCallback, useEffect, useRef, useState } from 'react';
|
|
4
|
+
import { useLongPressIndicator } from '../hooks/useLongPressIndicator.ts';
|
|
5
|
+
import {
|
|
6
|
+
cameraPanXAtom,
|
|
7
|
+
cameraPanYAtom,
|
|
8
|
+
cameraZoomAtom,
|
|
9
|
+
guidedSuspendedAtom,
|
|
10
|
+
legendSelectionAtom,
|
|
11
|
+
viewModeAtom,
|
|
12
|
+
} from '../state/atoms.ts';
|
|
13
|
+
|
|
14
|
+
type LassoOverlayProps = {
|
|
15
|
+
scatter: ScatterInstance | null;
|
|
16
|
+
width: number;
|
|
17
|
+
height: number;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
const LONG_PRESS_MS = 750;
|
|
21
|
+
const MIN_MOVE_PX = 5;
|
|
22
|
+
const MIN_POINT_DISTANCE = 5;
|
|
23
|
+
const THROTTLE_MS = 10;
|
|
24
|
+
|
|
25
|
+
/** Convert CSS coords to NDC accounting for camera. */
|
|
26
|
+
const cssToNdc = (
|
|
27
|
+
x: number,
|
|
28
|
+
y: number,
|
|
29
|
+
width: number,
|
|
30
|
+
height: number,
|
|
31
|
+
panX: number,
|
|
32
|
+
panY: number,
|
|
33
|
+
zoom: number,
|
|
34
|
+
): [number, number] => {
|
|
35
|
+
const aspect = width / height || 1;
|
|
36
|
+
const ndcX = (((x / width) * 2 - 1) * aspect) / zoom - panX;
|
|
37
|
+
const ndcY = -((y / height) * 2 - 1) / zoom - panY;
|
|
38
|
+
return [ndcX, ndcY];
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
export const LassoOverlay = ({ scatter, width, height }: LassoOverlayProps) => {
|
|
42
|
+
const panX = useAtomValue(cameraPanXAtom);
|
|
43
|
+
const panY = useAtomValue(cameraPanYAtom);
|
|
44
|
+
const zoom = useAtomValue(cameraZoomAtom);
|
|
45
|
+
const viewMode = useAtomValue(viewModeAtom);
|
|
46
|
+
const setViewMode = useSetAtom(viewModeAtom);
|
|
47
|
+
const setGuidedSuspended = useSetAtom(guidedSuspendedAtom);
|
|
48
|
+
const setLegendSelection = useSetAtom(legendSelectionAtom);
|
|
49
|
+
|
|
50
|
+
const [lassoMode, setLassoMode] = useState(false);
|
|
51
|
+
const [path, setPath] = useState<[number, number][]>([]);
|
|
52
|
+
|
|
53
|
+
const longPressTimer = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
54
|
+
const startPos = useRef<[number, number] | null>(null);
|
|
55
|
+
const lastPointTime = useRef(0);
|
|
56
|
+
const overlayRef = useRef<HTMLDivElement>(null);
|
|
57
|
+
|
|
58
|
+
const { show: showIndicator, hide: hideIndicator } = useLongPressIndicator();
|
|
59
|
+
|
|
60
|
+
const clearLongPress = useCallback(() => {
|
|
61
|
+
if (longPressTimer.current) {
|
|
62
|
+
clearTimeout(longPressTimer.current);
|
|
63
|
+
longPressTimer.current = null;
|
|
64
|
+
hideIndicator(); // Only revert if cancelling (timer was still pending)
|
|
65
|
+
}
|
|
66
|
+
}, [hideIndicator]);
|
|
67
|
+
|
|
68
|
+
const handlePointerDown = useCallback(
|
|
69
|
+
(e: React.PointerEvent) => {
|
|
70
|
+
if (lassoMode || e.button !== 0) return;
|
|
71
|
+
startPos.current = [e.clientX, e.clientY];
|
|
72
|
+
|
|
73
|
+
showIndicator(e.clientX, e.clientY);
|
|
74
|
+
|
|
75
|
+
longPressTimer.current = setTimeout(() => {
|
|
76
|
+
setLassoMode(true);
|
|
77
|
+
setPath([]);
|
|
78
|
+
longPressTimer.current = null;
|
|
79
|
+
}, LONG_PRESS_MS);
|
|
80
|
+
},
|
|
81
|
+
[lassoMode, showIndicator],
|
|
82
|
+
);
|
|
83
|
+
|
|
84
|
+
const handlePointerMove = useCallback(
|
|
85
|
+
(e: React.PointerEvent) => {
|
|
86
|
+
// Cancel long press if moved too far
|
|
87
|
+
if (!lassoMode && startPos.current && longPressTimer.current) {
|
|
88
|
+
const dx = e.clientX - startPos.current[0];
|
|
89
|
+
const dy = e.clientY - startPos.current[1];
|
|
90
|
+
if (Math.sqrt(dx * dx + dy * dy) > MIN_MOVE_PX) {
|
|
91
|
+
clearLongPress();
|
|
92
|
+
}
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (!lassoMode) return;
|
|
97
|
+
|
|
98
|
+
const now = performance.now();
|
|
99
|
+
if (now - lastPointTime.current < THROTTLE_MS) return;
|
|
100
|
+
|
|
101
|
+
const rect = overlayRef.current?.getBoundingClientRect();
|
|
102
|
+
if (!rect) return;
|
|
103
|
+
|
|
104
|
+
const x = e.clientX - rect.left;
|
|
105
|
+
const y = e.clientY - rect.top;
|
|
106
|
+
|
|
107
|
+
setPath((prev) => {
|
|
108
|
+
if (prev.length > 0) {
|
|
109
|
+
const last = prev[prev.length - 1]!;
|
|
110
|
+
const dx = x - last[0];
|
|
111
|
+
const dy = y - last[1];
|
|
112
|
+
if (Math.sqrt(dx * dx + dy * dy) < MIN_POINT_DISTANCE) return prev;
|
|
113
|
+
}
|
|
114
|
+
return [...prev, [x, y]];
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
lastPointTime.current = now;
|
|
118
|
+
},
|
|
119
|
+
[lassoMode, clearLongPress],
|
|
120
|
+
);
|
|
121
|
+
|
|
122
|
+
const handlePointerLeave = useCallback(() => {
|
|
123
|
+
// Pointer left the canvas area (e.g. moved to sidebar / resize handle).
|
|
124
|
+
// Cancel the long-press timer so we don't accidentally enter lasso mode.
|
|
125
|
+
if (!lassoMode) {
|
|
126
|
+
clearLongPress();
|
|
127
|
+
startPos.current = null;
|
|
128
|
+
}
|
|
129
|
+
}, [lassoMode, clearLongPress]);
|
|
130
|
+
|
|
131
|
+
// Also cancel on window blur (e.g. user switches tabs/apps mid-press)
|
|
132
|
+
useEffect(() => {
|
|
133
|
+
const handleBlur = () => {
|
|
134
|
+
if (!lassoMode) {
|
|
135
|
+
clearLongPress();
|
|
136
|
+
startPos.current = null;
|
|
137
|
+
}
|
|
138
|
+
};
|
|
139
|
+
window.addEventListener('blur', handleBlur);
|
|
140
|
+
return () => window.removeEventListener('blur', handleBlur);
|
|
141
|
+
}, [lassoMode, clearLongPress]);
|
|
142
|
+
|
|
143
|
+
const handlePointerUp = useCallback(() => {
|
|
144
|
+
clearLongPress();
|
|
145
|
+
hideIndicator();
|
|
146
|
+
|
|
147
|
+
if (!lassoMode || path.length < 3 || !scatter) {
|
|
148
|
+
setLassoMode(false);
|
|
149
|
+
setPath([]);
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Convert CSS path to NDC polygon and send to GPU worker
|
|
154
|
+
const polygon = new Float32Array(path.length * 2);
|
|
155
|
+
for (let i = 0; i < path.length; i++) {
|
|
156
|
+
const [ndcX, ndcY] = cssToNdc(path[i]![0], path[i]![1], width, height, panX, panY, zoom);
|
|
157
|
+
polygon[i * 2] = ndcX;
|
|
158
|
+
polygon[i * 2 + 1] = ndcY;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
scatter.lassoSelect(polygon);
|
|
162
|
+
setLegendSelection(null);
|
|
163
|
+
|
|
164
|
+
setLassoMode(false);
|
|
165
|
+
setPath([]);
|
|
166
|
+
}, [
|
|
167
|
+
lassoMode,
|
|
168
|
+
path,
|
|
169
|
+
scatter,
|
|
170
|
+
width,
|
|
171
|
+
height,
|
|
172
|
+
panX,
|
|
173
|
+
panY,
|
|
174
|
+
zoom,
|
|
175
|
+
clearLongPress,
|
|
176
|
+
hideIndicator,
|
|
177
|
+
setLegendSelection,
|
|
178
|
+
]);
|
|
179
|
+
|
|
180
|
+
// Double-click or Escape clears selection
|
|
181
|
+
const handleDoubleClick = useCallback(() => {
|
|
182
|
+
scatter?.clearSelection();
|
|
183
|
+
setLegendSelection(null);
|
|
184
|
+
}, [scatter, setLegendSelection]);
|
|
185
|
+
|
|
186
|
+
useEffect(() => {
|
|
187
|
+
const handleKeyDown = (e: KeyboardEvent) => {
|
|
188
|
+
if (e.key === 'Escape') {
|
|
189
|
+
if (viewMode === 'grand') {
|
|
190
|
+
// In grand mode, Escape returns to guided mode
|
|
191
|
+
setGuidedSuspended(true);
|
|
192
|
+
setViewMode('guided');
|
|
193
|
+
} else {
|
|
194
|
+
scatter?.clearSelection();
|
|
195
|
+
}
|
|
196
|
+
setLegendSelection(null);
|
|
197
|
+
setLassoMode(false);
|
|
198
|
+
setPath([]);
|
|
199
|
+
clearLongPress();
|
|
200
|
+
}
|
|
201
|
+
};
|
|
202
|
+
window.addEventListener('keydown', handleKeyDown);
|
|
203
|
+
return () => window.removeEventListener('keydown', handleKeyDown);
|
|
204
|
+
}, [scatter, clearLongPress, viewMode, setViewMode, setGuidedSuspended, setLegendSelection]);
|
|
205
|
+
|
|
206
|
+
// Build path string for SVG polygon
|
|
207
|
+
const pathStr = path.map(([x, y]) => `${x},${y}`).join(' ');
|
|
208
|
+
|
|
209
|
+
return (
|
|
210
|
+
<div
|
|
211
|
+
ref={overlayRef}
|
|
212
|
+
className="absolute top-0 left-0 touch-none"
|
|
213
|
+
style={{ width, height, cursor: lassoMode ? 'crosshair' : undefined }}
|
|
214
|
+
onPointerDown={handlePointerDown}
|
|
215
|
+
onPointerMove={handlePointerMove}
|
|
216
|
+
onPointerUp={handlePointerUp}
|
|
217
|
+
onPointerLeave={handlePointerLeave}
|
|
218
|
+
onDoubleClick={handleDoubleClick}
|
|
219
|
+
>
|
|
220
|
+
{/* Lasso polygon */}
|
|
221
|
+
{lassoMode && path.length > 1 && (
|
|
222
|
+
<svg
|
|
223
|
+
width={width}
|
|
224
|
+
height={height}
|
|
225
|
+
role="img"
|
|
226
|
+
aria-label="Lasso selection path"
|
|
227
|
+
className="absolute top-0 left-0 pointer-events-none"
|
|
228
|
+
>
|
|
229
|
+
<polygon
|
|
230
|
+
points={pathStr}
|
|
231
|
+
fill="rgba(79, 143, 247, 0.1)"
|
|
232
|
+
stroke="#4f8ff7"
|
|
233
|
+
strokeWidth={1.5}
|
|
234
|
+
strokeDasharray="4 2"
|
|
235
|
+
/>
|
|
236
|
+
</svg>
|
|
237
|
+
)}
|
|
238
|
+
</div>
|
|
239
|
+
);
|
|
240
|
+
};
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
export const Logo = () => (
|
|
2
|
+
<svg
|
|
3
|
+
width="100%"
|
|
4
|
+
height="100%"
|
|
5
|
+
viewBox="0 0 31.2 9.1"
|
|
6
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
7
|
+
xmlSpace="preserve"
|
|
8
|
+
version="1.1"
|
|
9
|
+
>
|
|
10
|
+
<title>dtour</title>
|
|
11
|
+
<path
|
|
12
|
+
fill="currentColor"
|
|
13
|
+
fillRule="evenodd"
|
|
14
|
+
d="M 3.036 8.82 C 2.413 8.82 1.875 8.67 1.422 8.369 C 0.968 8.068 0.618 7.66 0.371 7.143 C 0.123 6.626 0 6.044 0 5.396 C 0 4.744 0.124 4.161 0.373 3.646 C 0.621 3.131 0.974 2.725 1.432 2.427 C 1.89 2.129 2.432 1.98 3.06 1.98 C 3.694 1.98 4.227 2.129 4.66 2.427 C 5.093 2.725 5.42 3.132 5.643 3.647 C 5.865 4.162 5.976 4.745 5.976 5.396 C 5.976 6.043 5.864 6.624 5.642 7.141 C 5.419 7.658 5.089 8.067 4.653 8.368 C 4.217 8.669 3.678 8.82 3.036 8.82 Z M 2.96 7.684 C 3.372 7.684 3.708 7.587 3.967 7.392 C 4.226 7.197 4.417 6.928 4.538 6.583 C 4.659 6.238 4.72 5.843 4.72 5.396 C 4.72 4.947 4.659 4.55 4.537 4.207 C 4.415 3.864 4.227 3.596 3.973 3.404 C 3.719 3.212 3.395 3.116 3 3.116 C 2.585 3.116 2.244 3.218 1.975 3.421 C 1.706 3.624 1.507 3.898 1.378 4.243 C 1.249 4.588 1.184 4.972 1.184 5.396 C 1.184 5.824 1.249 6.211 1.378 6.557 C 1.507 6.903 1.703 7.177 1.965 7.38 C 2.227 7.583 2.559 7.684 2.96 7.684 Z M 6.187 8.682 C 5.998 8.644 5.821 8.587 5.656 8.513 C 5.348 8.374 5.115 8.153 4.956 7.852 C 4.817 7.584 4.743 7.312 4.734 7.035 C 4.725 6.758 4.72 6.444 4.72 6.092 L 4.72 0 L 5.976 0 L 5.976 6.028 C 5.976 6.289 5.979 6.516 5.985 6.708 C 5.991 6.9 6.032 7.061 6.108 7.192 C 6.156 7.275 6.214 7.346 6.281 7.405 C 6.463 7.217 6.718 7.1 7 7.1 C 7.552 7.1 8 7.548 8 8.1 C 8 8.652 7.552 9.1 7 9.1 C 6.665 9.1 6.369 8.935 6.187 8.682 Z"
|
|
15
|
+
/>
|
|
16
|
+
<path
|
|
17
|
+
fill="currentColor"
|
|
18
|
+
d="M 6.187 8.682 C 5.998 8.644 5.821 8.587 5.656 8.513 C 5.348 8.374 5.115 8.153 4.956 7.852 C 4.817 7.584 4.743 7.312 4.734 7.035 C 4.725 6.758 4.72 6.444 4.72 6.092 L 4.72 0 L 5.976 0 L 5.976 6.028 C 5.976 6.289 5.979 6.516 5.985 6.708 C 5.991 6.9 6.032 7.061 6.108 7.192 C 6.156 7.275 6.214 7.346 6.281 7.405 C 6.463 7.217 6.718 7.1 7 7.1 C 7.552 7.1 8 7.548 8 8.1 C 8 8.652 7.552 9.1 7 9.1 C 6.665 9.1 6.369 8.935 6.187 8.682 Z"
|
|
19
|
+
/>
|
|
20
|
+
<path
|
|
21
|
+
fill="currentColor"
|
|
22
|
+
d="M 11.928 8.64 C 11.521 8.72 11.122 8.754 10.73 8.741 C 10.338 8.728 9.988 8.652 9.68 8.513 C 9.372 8.374 9.139 8.153 8.98 7.852 C 8.841 7.584 8.767 7.312 8.758 7.035 C 8.749 6.758 8.744 6.444 8.744 6.092 L 8.744 0.36 L 10 0.36 L 10 6.028 C 10 6.289 10.003 6.516 10.009 6.708 C 10.015 6.9 10.056 7.061 10.132 7.192 C 10.276 7.44 10.505 7.582 10.82 7.617 C 11.135 7.652 11.504 7.639 11.928 7.576 L 11.928 8.64 Z M 7.5 3.168 L 7.5 2.16 L 11.928 2.16 L 11.928 3.168 L 7.5 3.168 Z"
|
|
23
|
+
/>
|
|
24
|
+
<path
|
|
25
|
+
fill="currentColor"
|
|
26
|
+
d="M 15.8 8.82 C 15.152 8.82 14.591 8.674 14.116 8.382 C 13.641 8.09 13.275 7.687 13.016 7.172 C 12.757 6.657 12.628 6.065 12.628 5.396 C 12.628 4.719 12.76 4.123 13.024 3.61 C 13.288 3.097 13.658 2.697 14.133 2.41 C 14.608 2.123 15.164 1.98 15.8 1.98 C 16.449 1.98 17.012 2.126 17.488 2.417 C 17.964 2.708 18.332 3.111 18.591 3.624 C 18.85 4.137 18.98 4.728 18.98 5.396 C 18.98 6.072 18.849 6.667 18.588 7.181 C 18.327 7.695 17.958 8.097 17.481 8.386 C 17.004 8.675 16.444 8.82 15.8 8.82 Z M 15.8 7.636 C 16.421 7.636 16.885 7.428 17.19 7.011 C 17.495 6.594 17.648 6.056 17.648 5.396 C 17.648 4.717 17.494 4.176 17.185 3.771 C 16.876 3.366 16.415 3.164 15.8 3.164 C 15.38 3.164 15.034 3.259 14.763 3.448 C 14.492 3.637 14.29 3.899 14.158 4.234 C 14.026 4.569 13.96 4.956 13.96 5.396 C 13.96 6.076 14.116 6.619 14.427 7.026 C 14.738 7.433 15.196 7.636 15.8 7.636 Z"
|
|
27
|
+
/>
|
|
28
|
+
<path
|
|
29
|
+
fill="currentColor"
|
|
30
|
+
d="M 22.744 8.816 C 22.296 8.816 21.92 8.743 21.615 8.598 C 21.31 8.453 21.063 8.263 20.873 8.029 C 20.683 7.795 20.538 7.541 20.438 7.266 C 20.338 6.991 20.27 6.722 20.234 6.457 C 20.198 6.192 20.18 5.96 20.18 5.76 L 20.18 2.16 L 21.452 2.16 L 21.452 5.344 C 21.452 5.596 21.473 5.855 21.515 6.122 C 21.557 6.389 21.637 6.636 21.754 6.865 C 21.871 7.094 22.037 7.278 22.251 7.418 C 22.465 7.558 22.744 7.628 23.088 7.628 C 23.313 7.628 23.526 7.59 23.725 7.515 C 23.924 7.44 24.099 7.32 24.25 7.156 C 24.401 6.992 24.519 6.776 24.606 6.508 C 24.693 6.24 24.736 5.915 24.736 5.532 L 25.516 5.824 C 25.516 6.412 25.406 6.931 25.186 7.381 C 24.966 7.831 24.65 8.183 24.237 8.436 C 23.824 8.689 23.327 8.816 22.744 8.816 Z M 24.884 8.64 L 24.884 6.768 L 24.736 6.768 L 24.736 2.16 L 26 2.16 L 26 8.64 L 24.884 8.64 Z"
|
|
31
|
+
/>
|
|
32
|
+
<path
|
|
33
|
+
fill="currentColor"
|
|
34
|
+
d="M 27.68 8.64 L 27.68 2.16 L 28.796 2.16 L 28.796 3.732 L 28.64 3.528 C 28.719 3.32 28.822 3.129 28.95 2.956 C 29.078 2.783 29.227 2.64 29.396 2.528 C 29.559 2.407 29.742 2.313 29.945 2.247 C 30.148 2.181 30.356 2.141 30.568 2.127 C 30.78 2.113 30.983 2.124 31.176 2.16 L 31.176 3.336 C 30.967 3.279 30.733 3.262 30.474 3.287 C 30.215 3.312 29.979 3.395 29.764 3.536 C 29.56 3.665 29.398 3.825 29.279 4.014 C 29.16 4.203 29.074 4.413 29.022 4.643 C 28.97 4.873 28.944 5.115 28.944 5.368 L 28.944 8.64 L 27.68 8.64 Z"
|
|
35
|
+
/>
|
|
36
|
+
</svg>
|
|
37
|
+
);
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { Slot } from '@radix-ui/react-slot';
|
|
2
|
+
import { type VariantProps, cva } from 'class-variance-authority';
|
|
3
|
+
import type { ButtonHTMLAttributes } from 'react';
|
|
4
|
+
import { cn } from '../../lib/utils.ts';
|
|
5
|
+
|
|
6
|
+
const buttonVariants = cva(
|
|
7
|
+
'inline-flex items-center justify-center gap-1.5 rounded-md text-sm font-medium cursor-pointer transition-[color,background-color,border-color,transform] duration-150 ease-out active:scale-[0.97] focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-dtour-accent disabled:pointer-events-none disabled:opacity-50',
|
|
8
|
+
{
|
|
9
|
+
variants: {
|
|
10
|
+
variant: {
|
|
11
|
+
default: 'bg-dtour-accent text-white hover:bg-dtour-accent-hover',
|
|
12
|
+
ghost: 'text-dtour-text hover:bg-dtour-surface hover:text-dtour-highlight',
|
|
13
|
+
outline: 'border border-dtour-border bg-transparent text-dtour-text hover:bg-dtour-surface',
|
|
14
|
+
},
|
|
15
|
+
size: {
|
|
16
|
+
default: 'h-8 px-3',
|
|
17
|
+
sm: 'h-7 px-2 text-xs',
|
|
18
|
+
icon: 'h-8 w-8',
|
|
19
|
+
},
|
|
20
|
+
},
|
|
21
|
+
defaultVariants: {
|
|
22
|
+
variant: 'default',
|
|
23
|
+
size: 'default',
|
|
24
|
+
},
|
|
25
|
+
},
|
|
26
|
+
);
|
|
27
|
+
|
|
28
|
+
export type ButtonProps = ButtonHTMLAttributes<HTMLButtonElement> &
|
|
29
|
+
VariantProps<typeof buttonVariants> & {
|
|
30
|
+
asChild?: boolean;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
export const Button = ({ className, variant, size, asChild = false, ...props }: ButtonProps) => {
|
|
34
|
+
const Comp = asChild ? Slot : 'button';
|
|
35
|
+
return <Comp className={cn(buttonVariants({ variant, size, className }))} {...props} />;
|
|
36
|
+
};
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import * as DropdownMenuPrimitive from '@radix-ui/react-dropdown-menu';
|
|
2
|
+
import type { ComponentPropsWithoutRef } from 'react';
|
|
3
|
+
import { cn } from '../../lib/utils.ts';
|
|
4
|
+
import { usePortalContainer } from '../../portal-container.tsx';
|
|
5
|
+
|
|
6
|
+
export const DropdownMenu = DropdownMenuPrimitive.Root;
|
|
7
|
+
export const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger;
|
|
8
|
+
|
|
9
|
+
export const DropdownMenuContent = ({
|
|
10
|
+
className,
|
|
11
|
+
sideOffset = 4,
|
|
12
|
+
...props
|
|
13
|
+
}: ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Content>) => {
|
|
14
|
+
const container = usePortalContainer();
|
|
15
|
+
return (
|
|
16
|
+
<DropdownMenuPrimitive.Portal container={container}>
|
|
17
|
+
<DropdownMenuPrimitive.Content
|
|
18
|
+
sideOffset={sideOffset}
|
|
19
|
+
className={cn(
|
|
20
|
+
'z-50 min-w-[8rem] overflow-hidden rounded-md border border-dtour-border bg-dtour-surface p-1 text-dtour-text shadow-md',
|
|
21
|
+
'origin-(--radix-dropdown-menu-content-transform-origin) animate-ease-out data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95',
|
|
22
|
+
className,
|
|
23
|
+
)}
|
|
24
|
+
{...props}
|
|
25
|
+
/>
|
|
26
|
+
</DropdownMenuPrimitive.Portal>
|
|
27
|
+
);
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export const DropdownMenuItem = ({
|
|
31
|
+
className,
|
|
32
|
+
...props
|
|
33
|
+
}: ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Item>) => (
|
|
34
|
+
<DropdownMenuPrimitive.Item
|
|
35
|
+
className={cn(
|
|
36
|
+
'relative flex cursor-pointer select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none transition-colors focus:bg-dtour-border focus:text-dtour-highlight data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
|
|
37
|
+
className,
|
|
38
|
+
)}
|
|
39
|
+
{...props}
|
|
40
|
+
/>
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
export const DropdownMenuLabel = ({
|
|
44
|
+
className,
|
|
45
|
+
...props
|
|
46
|
+
}: ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Label>) => (
|
|
47
|
+
<DropdownMenuPrimitive.Label
|
|
48
|
+
className={cn('px-2 py-1.5 text-xs font-semibold text-dtour-text-muted', className)}
|
|
49
|
+
{...props}
|
|
50
|
+
/>
|
|
51
|
+
);
|
|
52
|
+
|
|
53
|
+
export const DropdownMenuCheckboxItem = ({
|
|
54
|
+
className,
|
|
55
|
+
children,
|
|
56
|
+
checked = false,
|
|
57
|
+
...props
|
|
58
|
+
}: ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.CheckboxItem>) => (
|
|
59
|
+
<DropdownMenuPrimitive.CheckboxItem
|
|
60
|
+
className={cn(
|
|
61
|
+
'relative flex cursor-pointer select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-dtour-border focus:text-dtour-highlight data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
|
|
62
|
+
className,
|
|
63
|
+
)}
|
|
64
|
+
checked={checked}
|
|
65
|
+
{...props}
|
|
66
|
+
>
|
|
67
|
+
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
|
68
|
+
<DropdownMenuPrimitive.ItemIndicator>
|
|
69
|
+
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" aria-hidden="true">
|
|
70
|
+
<path
|
|
71
|
+
d="M11.5 3.5L5.5 9.5L2.5 6.5"
|
|
72
|
+
stroke="currentColor"
|
|
73
|
+
strokeWidth="1.5"
|
|
74
|
+
strokeLinecap="round"
|
|
75
|
+
strokeLinejoin="round"
|
|
76
|
+
/>
|
|
77
|
+
</svg>
|
|
78
|
+
</DropdownMenuPrimitive.ItemIndicator>
|
|
79
|
+
</span>
|
|
80
|
+
{children}
|
|
81
|
+
</DropdownMenuPrimitive.CheckboxItem>
|
|
82
|
+
);
|
|
83
|
+
|
|
84
|
+
export const DropdownMenuSeparator = ({
|
|
85
|
+
className,
|
|
86
|
+
...props
|
|
87
|
+
}: ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Separator>) => (
|
|
88
|
+
<DropdownMenuPrimitive.Separator
|
|
89
|
+
className={cn('-mx-1 my-1 h-px bg-dtour-border', className)}
|
|
90
|
+
{...props}
|
|
91
|
+
/>
|
|
92
|
+
);
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import * as SliderPrimitive from '@radix-ui/react-slider';
|
|
2
|
+
import type { ComponentPropsWithoutRef, ElementRef } from 'react';
|
|
3
|
+
import { forwardRef } from 'react';
|
|
4
|
+
import { cn } from '../../lib/utils.ts';
|
|
5
|
+
|
|
6
|
+
const Slider = forwardRef<
|
|
7
|
+
ElementRef<typeof SliderPrimitive.Root>,
|
|
8
|
+
ComponentPropsWithoutRef<typeof SliderPrimitive.Root> & { ticks?: number }
|
|
9
|
+
>(({ className, orientation, ticks, value, ...props }, ref) => {
|
|
10
|
+
const current = value?.[0] ?? 0;
|
|
11
|
+
const min = props.min ?? 0;
|
|
12
|
+
const max = props.max ?? 100;
|
|
13
|
+
return (
|
|
14
|
+
<SliderPrimitive.Root
|
|
15
|
+
ref={ref}
|
|
16
|
+
orientation={orientation}
|
|
17
|
+
{...(value ? { value } : {})}
|
|
18
|
+
className={cn(
|
|
19
|
+
'relative flex touch-none select-none',
|
|
20
|
+
orientation === 'vertical' ? 'h-full flex-col items-center' : 'w-full items-center',
|
|
21
|
+
className,
|
|
22
|
+
)}
|
|
23
|
+
{...props}
|
|
24
|
+
>
|
|
25
|
+
{ticks != null && ticks > 1 && (
|
|
26
|
+
<div
|
|
27
|
+
className={cn(
|
|
28
|
+
'pointer-events-none absolute',
|
|
29
|
+
orientation === 'vertical'
|
|
30
|
+
? 'inset-y-0 left-1/2 flex -translate-x-1/2 flex-col justify-between'
|
|
31
|
+
: 'inset-x-0 top-1/2 flex -translate-y-1/2 justify-between',
|
|
32
|
+
)}
|
|
33
|
+
>
|
|
34
|
+
{Array.from({ length: ticks }, (_, i) => {
|
|
35
|
+
const tickValue =
|
|
36
|
+
orientation === 'vertical'
|
|
37
|
+
? max - (i * (max - min)) / (ticks - 1)
|
|
38
|
+
: min + (i * (max - min)) / (ticks - 1);
|
|
39
|
+
const filled = tickValue <= current;
|
|
40
|
+
return (
|
|
41
|
+
<span
|
|
42
|
+
// biome-ignore lint/suspicious/noArrayIndexKey: static tick marks
|
|
43
|
+
key={i}
|
|
44
|
+
className={cn(
|
|
45
|
+
'flex h-[9px] w-[9px] items-center justify-center rounded-full',
|
|
46
|
+
filled ? 'bg-dtour-accent' : 'bg-dtour-border',
|
|
47
|
+
)}
|
|
48
|
+
/>
|
|
49
|
+
);
|
|
50
|
+
})}
|
|
51
|
+
</div>
|
|
52
|
+
)}
|
|
53
|
+
<SliderPrimitive.Track
|
|
54
|
+
className={cn(
|
|
55
|
+
'relative grow overflow-hidden rounded-full bg-dtour-border',
|
|
56
|
+
orientation === 'vertical' ? 'w-1.5' : 'h-1.5 w-full',
|
|
57
|
+
)}
|
|
58
|
+
>
|
|
59
|
+
<SliderPrimitive.Range
|
|
60
|
+
className={cn(
|
|
61
|
+
'absolute rounded-full bg-dtour-accent',
|
|
62
|
+
orientation === 'vertical' ? 'w-full' : 'h-full',
|
|
63
|
+
)}
|
|
64
|
+
/>
|
|
65
|
+
</SliderPrimitive.Track>
|
|
66
|
+
{ticks != null && ticks > 1 && (
|
|
67
|
+
<div
|
|
68
|
+
className={cn(
|
|
69
|
+
'pointer-events-none absolute',
|
|
70
|
+
orientation === 'vertical'
|
|
71
|
+
? 'inset-y-0 left-1/2 flex -translate-x-1/2 flex-col justify-between'
|
|
72
|
+
: 'inset-x-0 top-1/2 flex -translate-y-1/2 justify-between',
|
|
73
|
+
)}
|
|
74
|
+
>
|
|
75
|
+
{Array.from({ length: ticks }, (_, i) => (
|
|
76
|
+
// biome-ignore lint/suspicious/noArrayIndexKey: <explanation>
|
|
77
|
+
<span key={i} className="h-[9px] w-[9px] flex items-center justify-center">
|
|
78
|
+
<span className="h-[3px] w-[3px] rounded-full bg-white/30" />
|
|
79
|
+
</span>
|
|
80
|
+
))}
|
|
81
|
+
</div>
|
|
82
|
+
)}
|
|
83
|
+
<SliderPrimitive.Thumb className="block h-4 w-4 rounded-full border-2 border-dtour-accent bg-white shadow transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-dtour-accent disabled:pointer-events-none disabled:opacity-50" />
|
|
84
|
+
</SliderPrimitive.Root>
|
|
85
|
+
);
|
|
86
|
+
});
|
|
87
|
+
Slider.displayName = SliderPrimitive.Root.displayName;
|
|
88
|
+
|
|
89
|
+
export { Slider };
|