@traky-app/ui 1.6.7 → 1.6.9
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 +1 -1
- package/RELEASE.md +14 -2
- package/package.json +2 -2
- package/registry/traky/json/file-manager.json +25 -0
- package/registry/traky/json/globe.json +1 -1
- package/registry/traky/json/lib-file-items.json +14 -0
- package/registry/traky/json/lib-file-manager-utils.json +14 -0
- package/registry/traky/json/world-globe.json +20 -0
- package/registry/traky/lib/file-items.ts +99 -0
- package/registry/traky/lib/file-manager-utils.ts +116 -0
- package/registry/traky/registry.json +40 -0
- package/registry/traky/ui/file-manager.tsx +646 -0
- package/registry/traky/ui/globe.tsx +71 -32
- package/registry/traky/ui/world-globe.tsx +292 -0
- package/src/lib/file-items.ts +99 -0
- package/src/lib/file-manager-utils.test.tsx +71 -0
- package/src/lib/file-manager-utils.ts +116 -0
- package/src/ui/file-manager.tsx +646 -0
- package/src/ui/globe.tsx +71 -32
- package/src/ui/world-globe.tsx +292 -0
package/src/ui/globe.tsx
CHANGED
|
@@ -28,7 +28,6 @@ function lerpAngle(a: number, b: number, t: number): number {
|
|
|
28
28
|
const GLOBE_CONFIG: COBEOptions = {
|
|
29
29
|
width: 800,
|
|
30
30
|
height: 800,
|
|
31
|
-
onRender: () => {},
|
|
32
31
|
devicePixelRatio: 2,
|
|
33
32
|
phi: 0,
|
|
34
33
|
theta: 0.3,
|
|
@@ -60,6 +59,8 @@ export function Globe({
|
|
|
60
59
|
interactive = true,
|
|
61
60
|
panBetweenMarkers = false,
|
|
62
61
|
cycleMarkers = false,
|
|
62
|
+
centerMarkerLatitude = false,
|
|
63
|
+
playing = true,
|
|
63
64
|
markerHoldMs = MARKER_HOLD_MS,
|
|
64
65
|
markerTransitionMs = MARKER_TRANSITION_MS,
|
|
65
66
|
}: {
|
|
@@ -71,10 +72,16 @@ export function Globe({
|
|
|
71
72
|
panBetweenMarkers?: boolean
|
|
72
73
|
/** Centra cada marker uno por uno con pausa y transición suave. */
|
|
73
74
|
cycleMarkers?: boolean
|
|
75
|
+
/** Inclina el globo para dejar la latitud de cada marker en el centro. */
|
|
76
|
+
centerMarkerLatitude?: boolean
|
|
77
|
+
/** Si es false, el globo se queda en el fotograma actual y no avanza el tour. */
|
|
78
|
+
playing?: boolean
|
|
74
79
|
markerHoldMs?: number
|
|
75
80
|
markerTransitionMs?: number
|
|
76
81
|
}) {
|
|
77
82
|
const canvasRef = useRef<HTMLCanvasElement>(null)
|
|
83
|
+
const playingRef = useRef(playing)
|
|
84
|
+
playingRef.current = playing
|
|
78
85
|
const phiRef = useRef(config.phi ?? 0)
|
|
79
86
|
const widthRef = useRef(0)
|
|
80
87
|
const pointerInteracting = useRef<number | null>(null)
|
|
@@ -84,12 +91,15 @@ export function Globe({
|
|
|
84
91
|
|
|
85
92
|
const fixedTheta = config.theta ?? 0.22
|
|
86
93
|
|
|
87
|
-
const markerViews = useMemo((): Array<{ phi: number }> | null => {
|
|
94
|
+
const markerViews = useMemo((): Array<{ phi: number; theta: number }> | null => {
|
|
88
95
|
if (!shouldCycleMarkers || !config.markers?.length) return null
|
|
89
96
|
return config.markers.map((marker) => ({
|
|
90
97
|
phi: Math.PI - ((marker.location[1] * Math.PI) / 180 - Math.PI / 2),
|
|
98
|
+
theta: centerMarkerLatitude
|
|
99
|
+
? (marker.location[0] * Math.PI) / 180
|
|
100
|
+
: fixedTheta,
|
|
91
101
|
}))
|
|
92
|
-
}, [shouldCycleMarkers, config.markers])
|
|
102
|
+
}, [shouldCycleMarkers, centerMarkerLatitude, fixedTheta, config.markers])
|
|
93
103
|
|
|
94
104
|
const markerPhiRange = useMemo((): [number, number] | null => {
|
|
95
105
|
if (!panBetweenMarkers || cycleMarkers || !config.markers?.length) return null
|
|
@@ -127,6 +137,7 @@ export function Globe({
|
|
|
127
137
|
const canvas = canvasRef.current
|
|
128
138
|
if (!canvas) return
|
|
129
139
|
|
|
140
|
+
const host = canvas.parentElement
|
|
130
141
|
const onResize = () => {
|
|
131
142
|
widthRef.current = canvas.offsetWidth
|
|
132
143
|
}
|
|
@@ -139,49 +150,76 @@ export function Globe({
|
|
|
139
150
|
|
|
140
151
|
const globe = createGlobe(canvas, {
|
|
141
152
|
...config,
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
+
devicePixelRatio: config.devicePixelRatio ?? 2,
|
|
154
|
+
width: widthRef.current,
|
|
155
|
+
height: widthRef.current,
|
|
156
|
+
})
|
|
157
|
+
|
|
158
|
+
let frame = 0
|
|
159
|
+
let elapsedMs = 0
|
|
160
|
+
let lastNow = 0
|
|
161
|
+
const render = () => {
|
|
162
|
+
const now = Date.now()
|
|
163
|
+
if (playingRef.current) {
|
|
164
|
+
if (lastNow !== 0) elapsedMs += now - lastNow
|
|
165
|
+
lastNow = now
|
|
166
|
+
} else {
|
|
167
|
+
lastNow = 0
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const cssSize = widthRef.current
|
|
171
|
+
let phi = interactive ? phiRef.current + rs.get() : (config.phi ?? 0)
|
|
172
|
+
let theta = config.theta ?? 0.3
|
|
153
173
|
|
|
174
|
+
if (shouldCycleMarkers && markerViews && markerViews.length > 0) {
|
|
175
|
+
const segment = markerHoldMs + markerTransitionMs
|
|
176
|
+
const elapsed = elapsedMs
|
|
177
|
+
const index = Math.floor(elapsed / segment) % markerViews.length
|
|
178
|
+
const local = elapsed % segment
|
|
179
|
+
const from = markerViews[index % markerViews.length]
|
|
180
|
+
const to = markerViews[(index + 1) % markerViews.length]
|
|
181
|
+
if (from && to) {
|
|
154
182
|
if (local < markerHoldMs) {
|
|
155
|
-
|
|
183
|
+
phi = from.phi
|
|
184
|
+
theta = from.theta
|
|
156
185
|
} else {
|
|
157
186
|
const t = easeInOutCubic((local - markerHoldMs) / markerTransitionMs)
|
|
158
|
-
|
|
187
|
+
phi = lerpAngle(from.phi, to.phi, t)
|
|
188
|
+
theta = lerp(from.theta, to.theta, t)
|
|
159
189
|
}
|
|
160
|
-
state.theta = fixedTheta
|
|
161
|
-
} else if (panBetweenMarkers && markerPhiRange) {
|
|
162
|
-
const [phiMin, phiMax] = markerPhiRange
|
|
163
|
-
const t = Date.now() / 5200
|
|
164
|
-
state.phi = phiMin + ((Math.sin(t) + 1) / 2) * (phiMax - phiMin)
|
|
165
|
-
} else if (autoRotate && !pointerInteracting.current) {
|
|
166
|
-
phiRef.current += 0.005
|
|
167
|
-
state.phi = interactive ? phiRef.current + rs.get() : (config.phi ?? 0)
|
|
168
|
-
} else {
|
|
169
|
-
state.phi = interactive ? phiRef.current + rs.get() : (config.phi ?? 0)
|
|
170
190
|
}
|
|
191
|
+
} else if (panBetweenMarkers && markerPhiRange) {
|
|
192
|
+
const [phiMin, phiMax] = markerPhiRange
|
|
193
|
+
const t = Date.now() / 5200
|
|
194
|
+
phi = phiMin + ((Math.sin(t) + 1) / 2) * (phiMax - phiMin)
|
|
195
|
+
} else if (autoRotate && playingRef.current && !pointerInteracting.current) {
|
|
196
|
+
phiRef.current += 0.005
|
|
197
|
+
phi = interactive ? phiRef.current + rs.get() : (config.phi ?? 0)
|
|
198
|
+
}
|
|
171
199
|
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
200
|
+
globe.update({
|
|
201
|
+
phi,
|
|
202
|
+
theta,
|
|
203
|
+
scale: config.scale ?? 1,
|
|
204
|
+
width: cssSize,
|
|
205
|
+
height: cssSize,
|
|
206
|
+
})
|
|
207
|
+
frame = requestAnimationFrame(render)
|
|
208
|
+
}
|
|
209
|
+
frame = requestAnimationFrame(render)
|
|
178
210
|
|
|
179
211
|
setTimeout(() => {
|
|
180
212
|
canvas.style.opacity = "1"
|
|
181
213
|
}, 0)
|
|
182
214
|
|
|
183
215
|
return () => {
|
|
216
|
+
cancelAnimationFrame(frame)
|
|
184
217
|
globe.destroy()
|
|
218
|
+
const wrapper = canvas.parentElement
|
|
219
|
+
if (wrapper && host && wrapper !== host) {
|
|
220
|
+
host.insertBefore(canvas, wrapper)
|
|
221
|
+
wrapper.remove()
|
|
222
|
+
}
|
|
185
223
|
window.removeEventListener("resize", onResize)
|
|
186
224
|
resizeObserver.disconnect()
|
|
187
225
|
}
|
|
@@ -192,6 +230,7 @@ export function Globe({
|
|
|
192
230
|
interactive,
|
|
193
231
|
panBetweenMarkers,
|
|
194
232
|
cycleMarkers,
|
|
233
|
+
centerMarkerLatitude,
|
|
195
234
|
shouldCycleMarkers,
|
|
196
235
|
markerViews,
|
|
197
236
|
markerPhiRange,
|
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import { useEffect, useMemo, useRef, useState, type CSSProperties } from "react"
|
|
4
|
+
import type { COBEOptions } from "cobe"
|
|
5
|
+
|
|
6
|
+
import { useTheme } from "../hooks/use-document-theme"
|
|
7
|
+
import { cn } from "../lib/utils"
|
|
8
|
+
import { Globe } from "./globe"
|
|
9
|
+
|
|
10
|
+
/** Tour: Norteamérica → Suramérica → Europa → África → Asia → Oceanía. */
|
|
11
|
+
const MARKERS = [
|
|
12
|
+
{ id: "cascade", location: [49.2827, -123.1207] as [number, number], label: "Cascade LLC" },
|
|
13
|
+
{ id: "logistic", location: [41.8781, -87.6298] as [number, number], label: "Logistic LLC" },
|
|
14
|
+
{ id: "alianza", location: [19.4326, -99.1332] as [number, number], label: "Alianza SA" },
|
|
15
|
+
{ id: "bolivar", location: [4.711, -74.0721] as [number, number], label: "Bolívar SAS" },
|
|
16
|
+
{ id: "minerva", location: [-23.5505, -46.6333] as [number, number], label: "Minerva SA" },
|
|
17
|
+
{ id: "cordillera", location: [-33.4489, -70.6693] as [number, number], label: "Cordillera SA" },
|
|
18
|
+
{ id: "iberica", location: [40.4168, -3.7038] as [number, number], label: "Ibérica SA" },
|
|
19
|
+
{ id: "avrora", location: [55.7558, 37.6173] as [number, number], label: "Avrora LLC" },
|
|
20
|
+
{ id: "meridian", location: [6.5244, 3.3792] as [number, number], label: "Meridian Ltd" },
|
|
21
|
+
{ id: "savanna", location: [-26.2041, 28.0473] as [number, number], label: "Savanna SA" },
|
|
22
|
+
{ id: "deccan", location: [19.076, 72.8777] as [number, number], label: "Deccan Ltd" },
|
|
23
|
+
{ id: "straits", location: [1.3521, 103.8198] as [number, number], label: "Straits Ltd" },
|
|
24
|
+
{ id: "huayun", location: [31.2304, 121.4737] as [number, number], label: "华运 Ltd" },
|
|
25
|
+
{ id: "harbour", location: [-33.8688, 151.2093] as [number, number], label: "Harbour Ltd" },
|
|
26
|
+
]
|
|
27
|
+
|
|
28
|
+
const ARCS = [
|
|
29
|
+
{
|
|
30
|
+
id: "alianza-logistic",
|
|
31
|
+
from: [19.4326, -99.1332] as [number, number],
|
|
32
|
+
to: [41.8781, -87.6298] as [number, number],
|
|
33
|
+
unit: "AWB",
|
|
34
|
+
fromValue: 120,
|
|
35
|
+
toValue: 240,
|
|
36
|
+
duration: 28000,
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
id: "iberica-avrora",
|
|
40
|
+
from: [40.4168, -3.7038] as [number, number],
|
|
41
|
+
to: [55.7558, 37.6173] as [number, number],
|
|
42
|
+
unit: "BL",
|
|
43
|
+
fromValue: 100,
|
|
44
|
+
toValue: 220,
|
|
45
|
+
duration: 32000,
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
id: "meridian-savanna",
|
|
49
|
+
from: [6.5244, 3.3792] as [number, number],
|
|
50
|
+
to: [-26.2041, 28.0473] as [number, number],
|
|
51
|
+
unit: "WR",
|
|
52
|
+
fromValue: 160,
|
|
53
|
+
toValue: 280,
|
|
54
|
+
duration: 36000,
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
id: "avrora-huayun",
|
|
58
|
+
from: [55.7558, 37.6173] as [number, number],
|
|
59
|
+
to: [31.2304, 121.4737] as [number, number],
|
|
60
|
+
unit: "MAWB",
|
|
61
|
+
fromValue: 140,
|
|
62
|
+
toValue: 260,
|
|
63
|
+
duration: 30000,
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
id: "harbour-huayun",
|
|
67
|
+
from: [-33.8688, 151.2093] as [number, number],
|
|
68
|
+
to: [31.2304, 121.4737] as [number, number],
|
|
69
|
+
unit: "AWB",
|
|
70
|
+
fromValue: 110,
|
|
71
|
+
toValue: 230,
|
|
72
|
+
duration: 34000,
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
id: "avrora-cascade",
|
|
76
|
+
from: [55.7558, 37.6173] as [number, number],
|
|
77
|
+
to: [49.2827, -123.1207] as [number, number],
|
|
78
|
+
unit: "BL",
|
|
79
|
+
fromValue: 130,
|
|
80
|
+
toValue: 250,
|
|
81
|
+
duration: 31000,
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
id: "bolivar-minerva",
|
|
85
|
+
from: [4.711, -74.0721] as [number, number],
|
|
86
|
+
to: [-23.5505, -46.6333] as [number, number],
|
|
87
|
+
unit: "WR",
|
|
88
|
+
fromValue: 150,
|
|
89
|
+
toValue: 270,
|
|
90
|
+
duration: 33000,
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
id: "deccan-straits",
|
|
94
|
+
from: [19.076, 72.8777] as [number, number],
|
|
95
|
+
to: [1.3521, 103.8198] as [number, number],
|
|
96
|
+
unit: "AWB",
|
|
97
|
+
fromValue: 170,
|
|
98
|
+
toValue: 260,
|
|
99
|
+
duration: 29000,
|
|
100
|
+
},
|
|
101
|
+
]
|
|
102
|
+
|
|
103
|
+
/** Azul oscuro de marca #000b27. Misma línea en tema claro y oscuro. */
|
|
104
|
+
const ARC_COLOR: [number, number, number] = [0, 0.043, 0.153]
|
|
105
|
+
|
|
106
|
+
const WORLD_GLOBE_CONFIG: COBEOptions = {
|
|
107
|
+
width: 800,
|
|
108
|
+
height: 800,
|
|
109
|
+
devicePixelRatio: 2,
|
|
110
|
+
phi: 0,
|
|
111
|
+
theta: 0.2,
|
|
112
|
+
dark: 0,
|
|
113
|
+
diffuse: 1.5,
|
|
114
|
+
mapSamples: 16000,
|
|
115
|
+
mapBrightness: 10,
|
|
116
|
+
baseColor: [1, 1, 1],
|
|
117
|
+
markerColor: [0.647, 0.867, 0.392],
|
|
118
|
+
glowColor: [0.94, 0.93, 0.91],
|
|
119
|
+
markerElevation: 0.01,
|
|
120
|
+
opacity: 1,
|
|
121
|
+
markers: MARKERS.map((marker) => ({
|
|
122
|
+
location: marker.location,
|
|
123
|
+
size: 0.025,
|
|
124
|
+
id: marker.id,
|
|
125
|
+
})),
|
|
126
|
+
arcs: ARCS.map((arc) => ({
|
|
127
|
+
from: arc.from,
|
|
128
|
+
to: arc.to,
|
|
129
|
+
id: arc.id,
|
|
130
|
+
})),
|
|
131
|
+
arcColor: ARC_COLOR,
|
|
132
|
+
arcWidth: 0.5,
|
|
133
|
+
arcHeight: 0.25,
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const GLOW_LIGHT: [number, number, number] = [0.97, 0.97, 0.98]
|
|
137
|
+
const GLOW_DARK: [number, number, number] = [0.07, 0.075, 0.08]
|
|
138
|
+
|
|
139
|
+
function anchoredStyle(anchor: string, visible: string): CSSProperties {
|
|
140
|
+
return {
|
|
141
|
+
positionAnchor: anchor,
|
|
142
|
+
bottom: "anchor(top)",
|
|
143
|
+
left: "anchor(center)",
|
|
144
|
+
translate: "-50% 0",
|
|
145
|
+
marginBottom: 8,
|
|
146
|
+
opacity: `var(${visible}, 0)`,
|
|
147
|
+
filter: `blur(var(${visible}, 10px))`,
|
|
148
|
+
transition: "opacity 0.8s, filter 0.8s",
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function glowForTheme(theme: string): [number, number, number] {
|
|
153
|
+
if (theme === "dark") return GLOW_DARK
|
|
154
|
+
if (theme === "light") return GLOW_LIGHT
|
|
155
|
+
if (typeof document !== "undefined" && document.documentElement.classList.contains("dark")) {
|
|
156
|
+
return GLOW_DARK
|
|
157
|
+
}
|
|
158
|
+
return GLOW_LIGHT
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/** Globo listo para la página web. No edita empresas ni envíos. */
|
|
162
|
+
export function WorldGlobe({ className }: { className?: string }) {
|
|
163
|
+
const rootRef = useRef<HTMLDivElement>(null)
|
|
164
|
+
const playingRef = useRef(false)
|
|
165
|
+
const [playing, setPlaying] = useState(false)
|
|
166
|
+
const { theme } = useTheme()
|
|
167
|
+
const glowColor = glowForTheme(theme)
|
|
168
|
+
const config = useMemo(
|
|
169
|
+
() => ({ ...WORLD_GLOBE_CONFIG, glowColor }),
|
|
170
|
+
[glowColor],
|
|
171
|
+
)
|
|
172
|
+
const [lanes, setLanes] = useState(() =>
|
|
173
|
+
ARCS.map((arc) => ({
|
|
174
|
+
id: arc.id,
|
|
175
|
+
unit: arc.unit,
|
|
176
|
+
value: arc.fromValue,
|
|
177
|
+
})),
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
useEffect(() => {
|
|
181
|
+
const node = rootRef.current
|
|
182
|
+
if (!node) return
|
|
183
|
+
|
|
184
|
+
let onScreen = false
|
|
185
|
+
const sync = () => {
|
|
186
|
+
const next = onScreen && document.visibilityState === "visible"
|
|
187
|
+
playingRef.current = next
|
|
188
|
+
setPlaying(next)
|
|
189
|
+
}
|
|
190
|
+
const observer = new IntersectionObserver(
|
|
191
|
+
([entry]) => {
|
|
192
|
+
onScreen = entry?.isIntersecting ?? false
|
|
193
|
+
sync()
|
|
194
|
+
},
|
|
195
|
+
{ threshold: 0.35 },
|
|
196
|
+
)
|
|
197
|
+
observer.observe(node)
|
|
198
|
+
document.addEventListener("visibilitychange", sync)
|
|
199
|
+
return () => {
|
|
200
|
+
observer.disconnect()
|
|
201
|
+
document.removeEventListener("visibilitychange", sync)
|
|
202
|
+
}
|
|
203
|
+
}, [])
|
|
204
|
+
|
|
205
|
+
useEffect(() => {
|
|
206
|
+
let frame = 0
|
|
207
|
+
let elapsedMs = 0
|
|
208
|
+
let lastNow = 0
|
|
209
|
+
const shown = ARCS.map((arc) => arc.fromValue)
|
|
210
|
+
|
|
211
|
+
const tick = (now: number) => {
|
|
212
|
+
if (playingRef.current) {
|
|
213
|
+
if (lastNow !== 0) elapsedMs += now - lastNow
|
|
214
|
+
lastNow = now
|
|
215
|
+
} else {
|
|
216
|
+
lastNow = 0
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
let changed = false
|
|
220
|
+
const next = ARCS.map((arc, index) => {
|
|
221
|
+
const elapsed = elapsedMs / arc.duration
|
|
222
|
+
const ping = elapsed % 2
|
|
223
|
+
const t = ping < 1 ? ping : 2 - ping
|
|
224
|
+
const eased = t < 0.5 ? 2 * t * t : 1 - (-2 * t + 2) ** 2 / 2
|
|
225
|
+
const value = Math.round(arc.fromValue + (arc.toValue - arc.fromValue) * eased)
|
|
226
|
+
if (value !== shown[index]) {
|
|
227
|
+
shown[index] = value
|
|
228
|
+
changed = true
|
|
229
|
+
}
|
|
230
|
+
return value
|
|
231
|
+
})
|
|
232
|
+
if (changed) {
|
|
233
|
+
setLanes((current) =>
|
|
234
|
+
current.map((item, index) => ({ ...item, value: next[index] ?? item.value })),
|
|
235
|
+
)
|
|
236
|
+
}
|
|
237
|
+
frame = requestAnimationFrame(tick)
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
frame = requestAnimationFrame(tick)
|
|
241
|
+
return () => cancelAnimationFrame(frame)
|
|
242
|
+
}, [])
|
|
243
|
+
|
|
244
|
+
return (
|
|
245
|
+
<div
|
|
246
|
+
ref={rootRef}
|
|
247
|
+
className={cn(
|
|
248
|
+
"relative mx-auto aspect-square w-full max-w-lg bg-background",
|
|
249
|
+
className,
|
|
250
|
+
)}
|
|
251
|
+
>
|
|
252
|
+
<Globe
|
|
253
|
+
config={config}
|
|
254
|
+
autoRotate={false}
|
|
255
|
+
interactive={false}
|
|
256
|
+
cycleMarkers
|
|
257
|
+
centerMarkerLatitude
|
|
258
|
+
playing={playing}
|
|
259
|
+
markerHoldMs={2400}
|
|
260
|
+
markerTransitionMs={1800}
|
|
261
|
+
className="max-w-full"
|
|
262
|
+
/>
|
|
263
|
+
{MARKERS.map((marker) => (
|
|
264
|
+
<div
|
|
265
|
+
key={marker.id}
|
|
266
|
+
className="pointer-events-none absolute z-10 w-max max-w-36 rounded-sm bg-primary px-1.5 py-px font-mono text-[10px] tracking-normal text-[#000b27]"
|
|
267
|
+
style={anchoredStyle(`--cobe-${marker.id}`, `--cobe-visible-${marker.id}`)}
|
|
268
|
+
>
|
|
269
|
+
<span className="block truncate">{marker.label}</span>
|
|
270
|
+
<span className="absolute top-full left-1/2 -translate-x-1/2 border-[5px] border-transparent border-t-primary" />
|
|
271
|
+
</div>
|
|
272
|
+
))}
|
|
273
|
+
{lanes.map((item) => (
|
|
274
|
+
<div
|
|
275
|
+
key={item.id}
|
|
276
|
+
className="pointer-events-none absolute z-10 rounded bg-black px-2 py-[3px] font-mono text-[0.5rem] whitespace-nowrap text-white"
|
|
277
|
+
style={{
|
|
278
|
+
positionAnchor: `--cobe-arc-${item.id}`,
|
|
279
|
+
bottom: "anchor(top)",
|
|
280
|
+
left: "anchor(center)",
|
|
281
|
+
translate: "-50% 0",
|
|
282
|
+
opacity: `var(--cobe-visible-arc-${item.id}, 0)`,
|
|
283
|
+
filter: `blur(calc((1 - var(--cobe-visible-arc-${item.id}, 0)) * 8px))`,
|
|
284
|
+
transition: "opacity 0.3s, filter 0.3s",
|
|
285
|
+
}}
|
|
286
|
+
>
|
|
287
|
+
{item.value} {item.unit}/s
|
|
288
|
+
</div>
|
|
289
|
+
))}
|
|
290
|
+
</div>
|
|
291
|
+
)
|
|
292
|
+
}
|