@tldiagram/core-ui 1.94.5 → 1.95.1
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/components/MiniZoomOnboarding.d.ts +2 -1
- package/dist/components/ZUI/ZUICanvas.d.ts +6 -0
- package/dist/components/ZUI/index.d.ts +1 -1
- package/dist/index.js +6896 -6779
- package/dist/pages/InfiniteZoom.d.ts +2 -0
- package/package.json +1 -1
- package/src/components/MiniZoomOnboarding.tsx +29 -22
- package/src/components/ZUI/ZUICanvas.tsx +151 -7
- package/src/components/ZUI/index.ts +1 -1
- package/src/pages/InfiniteZoom.tsx +58 -9
|
@@ -1,9 +1,11 @@
|
|
|
1
|
+
import { type ZUICameraFrame } from '../components/ZUI';
|
|
1
2
|
interface Props {
|
|
2
3
|
sharedToken?: string;
|
|
3
4
|
shareSlot?: React.ReactNode;
|
|
4
5
|
}
|
|
5
6
|
export interface InfiniteZoomHandle {
|
|
6
7
|
focusDiagram(viewId: number): boolean;
|
|
8
|
+
setCameraFrame(frame: ZUICameraFrame): boolean;
|
|
7
9
|
}
|
|
8
10
|
declare const InfiniteZoom: import("react").ForwardRefExoticComponent<Props & import("react").RefAttributes<InfiniteZoomHandle>>;
|
|
9
11
|
export default InfiniteZoom;
|
package/package.json
CHANGED
|
@@ -1,18 +1,13 @@
|
|
|
1
|
-
import { Box, HStack, Text } from '@chakra-ui/react'
|
|
2
|
-
import { keyframes } from '@emotion/react'
|
|
1
|
+
import { Box, CloseButton, HStack, Text } from '@chakra-ui/react'
|
|
3
2
|
import { ZoomInIcon } from './Icons'
|
|
4
3
|
|
|
5
4
|
interface Props {
|
|
6
5
|
isVisible: boolean
|
|
6
|
+
onClose?: () => void
|
|
7
7
|
}
|
|
8
8
|
|
|
9
|
-
const pulseGlow = keyframes`
|
|
10
|
-
0% { box-shadow: 0 0 0 0 rgba(var(--accent-rgb), 0.4); }
|
|
11
|
-
70% { box-shadow: 0 0 0 20px rgba(var(--accent-rgb), 0); }
|
|
12
|
-
100% { box-shadow: 0 0 0 0 rgba(var(--accent-rgb), 0); }
|
|
13
|
-
`
|
|
14
9
|
|
|
15
|
-
export default function MiniZoomOnboarding({ isVisible }: Props) {
|
|
10
|
+
export default function MiniZoomOnboarding({ isVisible, onClose }: Props) {
|
|
16
11
|
return (
|
|
17
12
|
<Box
|
|
18
13
|
position="absolute"
|
|
@@ -22,14 +17,14 @@ export default function MiniZoomOnboarding({ isVisible }: Props) {
|
|
|
22
17
|
zIndex={100}
|
|
23
18
|
opacity={isVisible ? 1 : 0}
|
|
24
19
|
transition="all 0.8s cubic-bezier(0.16, 1, 0.3, 1)"
|
|
25
|
-
pointerEvents=
|
|
20
|
+
pointerEvents={isVisible ? 'auto' : 'none'}
|
|
26
21
|
>
|
|
27
22
|
<Box
|
|
28
23
|
className="glass"
|
|
29
24
|
px={6}
|
|
25
|
+
pr={onClose ? 11 : 6}
|
|
30
26
|
py={4}
|
|
31
27
|
borderRadius="12px"
|
|
32
|
-
animation={isVisible ? `${pulseGlow} 3s infinite` : 'none'}
|
|
33
28
|
position="relative"
|
|
34
29
|
overflow="hidden"
|
|
35
30
|
border="1.5px solid rgba(var(--accent-rgb), 0.3)"
|
|
@@ -44,27 +39,39 @@ export default function MiniZoomOnboarding({ isVisible }: Props) {
|
|
|
44
39
|
bg="var(--accent)"
|
|
45
40
|
opacity={0.8}
|
|
46
41
|
/>
|
|
47
|
-
|
|
42
|
+
{onClose && (
|
|
43
|
+
<CloseButton
|
|
44
|
+
aria-label="Dismiss zoom hint"
|
|
45
|
+
position="absolute"
|
|
46
|
+
top={2}
|
|
47
|
+
right={2}
|
|
48
|
+
size="sm"
|
|
49
|
+
color="whiteAlpha.700"
|
|
50
|
+
_hover={{ color: 'white', bg: 'whiteAlpha.200' }}
|
|
51
|
+
onClick={onClose}
|
|
52
|
+
/>
|
|
53
|
+
)}
|
|
54
|
+
|
|
48
55
|
<HStack spacing={5} pl={3}>
|
|
49
56
|
<Box color="var(--accent)">
|
|
50
57
|
<ZoomInIcon size={24} />
|
|
51
58
|
</Box>
|
|
52
59
|
<Box>
|
|
53
|
-
<Text
|
|
54
|
-
fontSize="10px"
|
|
55
|
-
color="var(--accent)"
|
|
56
|
-
fontWeight="900"
|
|
57
|
-
letterSpacing="0.15em"
|
|
58
|
-
textTransform="uppercase"
|
|
60
|
+
<Text
|
|
61
|
+
fontSize="10px"
|
|
62
|
+
color="var(--accent)"
|
|
63
|
+
fontWeight="900"
|
|
64
|
+
letterSpacing="0.15em"
|
|
65
|
+
textTransform="uppercase"
|
|
59
66
|
mb={0.5}
|
|
60
67
|
opacity={0.9}
|
|
61
68
|
>
|
|
62
|
-
|
|
69
|
+
Hint:
|
|
63
70
|
</Text>
|
|
64
|
-
<Text
|
|
65
|
-
fontSize="15px"
|
|
66
|
-
color="white"
|
|
67
|
-
fontWeight="600"
|
|
71
|
+
<Text
|
|
72
|
+
fontSize="15px"
|
|
73
|
+
color="white"
|
|
74
|
+
fontWeight="600"
|
|
68
75
|
whiteSpace="nowrap"
|
|
69
76
|
letterSpacing="-0.01em"
|
|
70
77
|
>
|
|
@@ -38,6 +38,12 @@ import { buildVisibleProxyConnectors, collectVisibleNodeAnchors, drawVisibleProx
|
|
|
38
38
|
export interface ZUICanvasHandle {
|
|
39
39
|
fitView(): void
|
|
40
40
|
focusDiagram(viewId: number): boolean
|
|
41
|
+
setCameraFrame(frame: ZUICameraFrame): boolean
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export interface ZUICameraFrame {
|
|
45
|
+
profile: 'detail-to-overview'
|
|
46
|
+
progress: number
|
|
41
47
|
}
|
|
42
48
|
|
|
43
49
|
interface Props {
|
|
@@ -45,6 +51,7 @@ interface Props {
|
|
|
45
51
|
onReady?: () => void
|
|
46
52
|
onZoom?: () => void
|
|
47
53
|
onPan?: () => void
|
|
54
|
+
initialCameraFrame?: ZUICameraFrame
|
|
48
55
|
highlightedTags?: string[]
|
|
49
56
|
highlightColor?: string
|
|
50
57
|
hiddenTags?: string[]
|
|
@@ -238,7 +245,82 @@ function easeOutQuart(t: number): number {
|
|
|
238
245
|
return 1 - Math.pow(1 - t, 4)
|
|
239
246
|
}
|
|
240
247
|
|
|
241
|
-
|
|
248
|
+
function clamp01(value: number): number {
|
|
249
|
+
return Math.max(0, Math.min(1, value))
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function fitWorldRect(
|
|
253
|
+
rect: { x: number; y: number; w: number; h: number },
|
|
254
|
+
canvasW: number,
|
|
255
|
+
canvasH: number,
|
|
256
|
+
maxZoom: number,
|
|
257
|
+
padding: number,
|
|
258
|
+
): ZUIViewState | null {
|
|
259
|
+
const bboxW = Math.max(1, rect.w)
|
|
260
|
+
const bboxH = Math.max(1, rect.h)
|
|
261
|
+
const zoom = Math.min(
|
|
262
|
+
(canvasW * (1 - padding * 2)) / bboxW,
|
|
263
|
+
(canvasH * (1 - padding * 2)) / bboxH,
|
|
264
|
+
maxZoom,
|
|
265
|
+
)
|
|
266
|
+
if (!Number.isFinite(zoom) || zoom <= 0) return null
|
|
267
|
+
|
|
268
|
+
return {
|
|
269
|
+
x: (canvasW - bboxW * zoom) / 2 - rect.x * zoom,
|
|
270
|
+
y: (canvasH - bboxH * zoom) / 2 - rect.y * zoom,
|
|
271
|
+
zoom,
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
function findFirstExpandableNode(groups: DiagramGroupLayout[]): PathItem | null {
|
|
276
|
+
for (const group of groups) {
|
|
277
|
+
const found = findFirstExpandableNodeInTree(group.nodes, 0, 0, 1, 0, 0)
|
|
278
|
+
if (found) return found
|
|
279
|
+
}
|
|
280
|
+
return null
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
function findFirstExpandableNodeInTree(
|
|
284
|
+
nodes: DiagramGroupLayout['nodes'],
|
|
285
|
+
parentAbsX: number,
|
|
286
|
+
parentAbsY: number,
|
|
287
|
+
parentAbsScale: number,
|
|
288
|
+
parentChildOffsetX: number,
|
|
289
|
+
parentChildOffsetY: number,
|
|
290
|
+
): PathItem | null {
|
|
291
|
+
for (const node of nodes) {
|
|
292
|
+
const absX = parentAbsX + (node.worldX - parentChildOffsetX) * parentAbsScale
|
|
293
|
+
const absY = parentAbsY + (node.worldY - parentChildOffsetY) * parentAbsScale
|
|
294
|
+
const absW = node.worldW * parentAbsScale
|
|
295
|
+
const absH = node.worldH * parentAbsScale
|
|
296
|
+
|
|
297
|
+
if (node.children.length > 0) {
|
|
298
|
+
return {
|
|
299
|
+
id: node.id,
|
|
300
|
+
label: node.linkedDiagramLabel || node.label,
|
|
301
|
+
type: 'node',
|
|
302
|
+
isCircular: node.isCircular,
|
|
303
|
+
absX,
|
|
304
|
+
absY,
|
|
305
|
+
absW,
|
|
306
|
+
absH,
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
const found = findFirstExpandableNodeInTree(
|
|
311
|
+
node.children,
|
|
312
|
+
absX,
|
|
313
|
+
absY,
|
|
314
|
+
parentAbsScale * node.childScale,
|
|
315
|
+
node.childOffsetX,
|
|
316
|
+
node.childOffsetY,
|
|
317
|
+
)
|
|
318
|
+
if (found) return found
|
|
319
|
+
}
|
|
320
|
+
return null
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
export const ZUICanvas = forwardRef<ZUICanvasHandle, Props>(function ZUICanvas({ data, onReady, onZoom, onPan, initialCameraFrame, highlightedTags, highlightColor, hiddenTags, crossBranchSettings, hoverLocked = false }, ref) {
|
|
242
324
|
const canvasRef = useRef<HTMLCanvasElement>(null)
|
|
243
325
|
const containerRef = useRef<HTMLDivElement>(null)
|
|
244
326
|
const cameraTransitionRef = useRef<number | null>(null)
|
|
@@ -445,6 +527,68 @@ export const ZUICanvas = forwardRef<ZUICanvasHandle, Props>(function ZUICanvas({
|
|
|
445
527
|
return true
|
|
446
528
|
}, [isMobileLayout, layout.groups, maxZoom, setHoveredItem, setViewState, viewStateRef])
|
|
447
529
|
|
|
530
|
+
const setCameraFrame = useCallback((frame: ZUICameraFrame) => {
|
|
531
|
+
if (frame.profile !== 'detail-to-overview') return false
|
|
532
|
+
|
|
533
|
+
const el = containerRef.current
|
|
534
|
+
if (!el) return false
|
|
535
|
+
|
|
536
|
+
const canvasW = el.offsetWidth
|
|
537
|
+
const canvasH = el.offsetHeight
|
|
538
|
+
if (canvasW === 0 || canvasH === 0) return false
|
|
539
|
+
|
|
540
|
+
const detailTarget = findFirstExpandableNode(layout.groups)
|
|
541
|
+
const overviewTarget = layout.groups[0]
|
|
542
|
+
if (!detailTarget || !overviewTarget) return false
|
|
543
|
+
|
|
544
|
+
const detail = fitWorldRect(
|
|
545
|
+
{
|
|
546
|
+
x: detailTarget.absX,
|
|
547
|
+
y: detailTarget.absY,
|
|
548
|
+
w: detailTarget.absW,
|
|
549
|
+
h: detailTarget.absH,
|
|
550
|
+
},
|
|
551
|
+
canvasW,
|
|
552
|
+
canvasH,
|
|
553
|
+
maxZoom,
|
|
554
|
+
0.28,
|
|
555
|
+
)
|
|
556
|
+
|
|
557
|
+
const overview = fitWorldRect(
|
|
558
|
+
{
|
|
559
|
+
x: overviewTarget.worldX,
|
|
560
|
+
y: overviewTarget.worldY,
|
|
561
|
+
w: overviewTarget.worldW,
|
|
562
|
+
h: overviewTarget.worldH,
|
|
563
|
+
},
|
|
564
|
+
canvasW,
|
|
565
|
+
canvasH,
|
|
566
|
+
maxZoom,
|
|
567
|
+
0.18,
|
|
568
|
+
)
|
|
569
|
+
|
|
570
|
+
if (!detail || !overview) return false
|
|
571
|
+
|
|
572
|
+
if (cameraTransitionRef.current !== null) {
|
|
573
|
+
cancelAnimationFrame(cameraTransitionRef.current)
|
|
574
|
+
cameraTransitionRef.current = null
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
setHoveredItem(null, true)
|
|
578
|
+
const t = easeOutQuart(clamp01(frame.progress))
|
|
579
|
+
setViewState({
|
|
580
|
+
x: detail.x + (overview.x - detail.x) * t,
|
|
581
|
+
y: detail.y + (overview.y - detail.y) * t,
|
|
582
|
+
zoom: detail.zoom + (overview.zoom - detail.zoom) * t,
|
|
583
|
+
})
|
|
584
|
+
return true
|
|
585
|
+
}, [layout.groups, maxZoom, setHoveredItem, setViewState])
|
|
586
|
+
|
|
587
|
+
const fitInitialView = useCallback((w: number, h: number) => {
|
|
588
|
+
if (initialCameraFrame && setCameraFrame(initialCameraFrame)) return
|
|
589
|
+
fitView(w, h, layout.bbox)
|
|
590
|
+
}, [fitView, initialCameraFrame, layout.bbox, setCameraFrame])
|
|
591
|
+
|
|
448
592
|
useEffect(() => {
|
|
449
593
|
return () => {
|
|
450
594
|
if (cameraTransitionRef.current !== null) {
|
|
@@ -462,14 +606,13 @@ export const ZUICanvas = forwardRef<ZUICanvasHandle, Props>(function ZUICanvas({
|
|
|
462
606
|
// Only set as initialized if we have valid dimensions
|
|
463
607
|
if (w > 0 && h > 0) {
|
|
464
608
|
setContainerSize({ w, h })
|
|
465
|
-
|
|
609
|
+
fitInitialView(w, h)
|
|
466
610
|
if (!initialized) {
|
|
467
611
|
setInitialized(true)
|
|
468
612
|
onReady?.()
|
|
469
613
|
}
|
|
470
614
|
}
|
|
471
|
-
|
|
472
|
-
}, [layout, initialized, onReady])
|
|
615
|
+
}, [initialized, onReady, fitInitialView])
|
|
473
616
|
|
|
474
617
|
// ── Expose fitView to parent ─────────────────────────────────────
|
|
475
618
|
useImperativeHandle(
|
|
@@ -482,8 +625,9 @@ export const ZUICanvas = forwardRef<ZUICanvasHandle, Props>(function ZUICanvas({
|
|
|
482
625
|
fitView(el.offsetWidth, el.offsetHeight, layout.bbox)
|
|
483
626
|
},
|
|
484
627
|
focusDiagram,
|
|
628
|
+
setCameraFrame,
|
|
485
629
|
}),
|
|
486
|
-
[fitView, focusDiagram, layout.bbox, setHoveredItem],
|
|
630
|
+
[fitView, focusDiagram, layout.bbox, setCameraFrame, setHoveredItem],
|
|
487
631
|
)
|
|
488
632
|
|
|
489
633
|
// ── RAF render loop ──────────────────────────────────────────────
|
|
@@ -531,7 +675,7 @@ export const ZUICanvas = forwardRef<ZUICanvasHandle, Props>(function ZUICanvas({
|
|
|
531
675
|
|
|
532
676
|
// Trigger initialization if it hasn't happened yet
|
|
533
677
|
if (!initialized && w > 0 && h > 0) {
|
|
534
|
-
|
|
678
|
+
fitInitialView(w, h)
|
|
535
679
|
setInitialized(true)
|
|
536
680
|
onReady?.()
|
|
537
681
|
}
|
|
@@ -541,7 +685,7 @@ export const ZUICanvas = forwardRef<ZUICanvasHandle, Props>(function ZUICanvas({
|
|
|
541
685
|
ro.observe(container)
|
|
542
686
|
resize()
|
|
543
687
|
return () => ro.disconnect()
|
|
544
|
-
}, [initialized,
|
|
688
|
+
}, [initialized, fitInitialView, onReady])
|
|
545
689
|
|
|
546
690
|
useEffect(() => {
|
|
547
691
|
if (!initialized) return // Don't start loop until initialized
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// src/pages/InfiniteZoom.tsx Explore page holds the ZUI feature
|
|
2
2
|
import { forwardRef, useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState } from 'react'
|
|
3
|
-
import { useNavigate, useParams } from 'react-router-dom'
|
|
3
|
+
import { useLocation, useNavigate, useParams } from 'react-router-dom'
|
|
4
4
|
import {
|
|
5
5
|
Box,
|
|
6
6
|
Button,
|
|
@@ -24,7 +24,7 @@ import { FitViewIcon as FitViewSvg, TagsIcon, EyeIcon, EyeOffIcon, FocusIcon as
|
|
|
24
24
|
import ExploreOnboarding from '../components/ExploreOnboarding'
|
|
25
25
|
import ExplorePageOnboarding from '../components/ExplorePageOnboarding'
|
|
26
26
|
import MiniZoomOnboarding from '../components/MiniZoomOnboarding'
|
|
27
|
-
import { ZUICanvas, type ZUICanvasHandle } from '../components/ZUI'
|
|
27
|
+
import { ZUICanvas, type ZUICameraFrame, type ZUICanvasHandle } from '../components/ZUI'
|
|
28
28
|
import { useCrossBranchContextSettings } from '../crossBranch/settings'
|
|
29
29
|
import { primeWorkspaceGraphSnapshot } from '../crossBranch/store'
|
|
30
30
|
|
|
@@ -36,6 +36,7 @@ interface Props {
|
|
|
36
36
|
|
|
37
37
|
export interface InfiniteZoomHandle {
|
|
38
38
|
focusDiagram(viewId: number): boolean
|
|
39
|
+
setCameraFrame(frame: ZUICameraFrame): boolean
|
|
39
40
|
}
|
|
40
41
|
|
|
41
42
|
const MINI_ONBOARDING_KEY = 'shared_zoom_onboarding_dismissed'
|
|
@@ -43,11 +44,13 @@ const MINI_ONBOARDING_KEY = 'shared_zoom_onboarding_dismissed'
|
|
|
43
44
|
// ── Inner component ────────────────────────────────────────────────
|
|
44
45
|
function InfiniteZoomInner({ sharedToken, shareSlot }: Props, ref?: React.Ref<InfiniteZoomHandle>) {
|
|
45
46
|
const navigate = useNavigate()
|
|
47
|
+
const location = useLocation()
|
|
46
48
|
|
|
47
49
|
const [data, setData] = useState<ExploreData | null>(null)
|
|
48
50
|
const [loading, setLoading] = useState(true)
|
|
49
51
|
const [canvasReady, setCanvasReady] = useState(false)
|
|
50
52
|
const [showMiniOnboarding, setShowMiniOnboarding] = useState(false)
|
|
53
|
+
const [miniOnboardingInteractionSeen, setMiniOnboardingInteractionSeen] = useState(false)
|
|
51
54
|
const [tagColors] = useState<Record<string, import('../types').Tag>>({})
|
|
52
55
|
const [layers, setLayers] = useState<ViewLayer[]>([])
|
|
53
56
|
const [highlightedTags, setHighlightedTags] = useState<string[]>([])
|
|
@@ -58,10 +61,22 @@ function InfiniteZoomInner({ sharedToken, shareSlot }: Props, ref?: React.Ref<In
|
|
|
58
61
|
const crossBranchSurface = sharedToken ? 'zui-shared' : 'zui'
|
|
59
62
|
const { settings: crossBranchSettings, setEnabled: setCrossBranchEnabled } = useCrossBranchContextSettings(crossBranchSurface)
|
|
60
63
|
|
|
64
|
+
const cameraProfile = useMemo(() => new URLSearchParams(location.search).get('profile'), [location.search])
|
|
65
|
+
const isDetailToOverviewProfile = sharedToken && cameraProfile === 'detail-to-overview'
|
|
66
|
+
|
|
67
|
+
const initialCameraFrame = useMemo<ZUICameraFrame | undefined>(() => {
|
|
68
|
+
return isDetailToOverviewProfile
|
|
69
|
+
? { profile: 'detail-to-overview', progress: 0 }
|
|
70
|
+
: undefined
|
|
71
|
+
}, [isDetailToOverviewProfile])
|
|
72
|
+
|
|
61
73
|
useImperativeHandle(ref, () => ({
|
|
62
74
|
focusDiagram(viewId: number) {
|
|
63
75
|
return zuiRef.current?.focusDiagram(viewId) ?? false
|
|
64
76
|
},
|
|
77
|
+
setCameraFrame(frame: ZUICameraFrame) {
|
|
78
|
+
return zuiRef.current?.setCameraFrame(frame) ?? false
|
|
79
|
+
},
|
|
65
80
|
}), [])
|
|
66
81
|
|
|
67
82
|
// ── No data or No content ────────────────────────────────────────
|
|
@@ -120,17 +135,32 @@ function InfiniteZoomInner({ sharedToken, shareSlot }: Props, ref?: React.Ref<In
|
|
|
120
135
|
}, [])
|
|
121
136
|
|
|
122
137
|
useEffect(() => {
|
|
138
|
+
if (isDetailToOverviewProfile) return
|
|
123
139
|
if (sharedToken && canvasReady && !localStorage.getItem(MINI_ONBOARDING_KEY)) {
|
|
124
140
|
setShowMiniOnboarding(true)
|
|
125
141
|
}
|
|
126
|
-
}, [sharedToken, canvasReady])
|
|
142
|
+
}, [sharedToken, canvasReady, isDetailToOverviewProfile])
|
|
127
143
|
|
|
128
|
-
const
|
|
144
|
+
const dismissMiniOnboarding = useCallback(() => {
|
|
129
145
|
if (showMiniOnboarding) {
|
|
130
146
|
setShowMiniOnboarding(false)
|
|
131
|
-
|
|
147
|
+
if (!isDetailToOverviewProfile) {
|
|
148
|
+
localStorage.setItem(MINI_ONBOARDING_KEY, 'true')
|
|
149
|
+
}
|
|
132
150
|
}
|
|
133
|
-
}, [showMiniOnboarding])
|
|
151
|
+
}, [isDetailToOverviewProfile, showMiniOnboarding])
|
|
152
|
+
|
|
153
|
+
const showMiniOnboardingAfterCanvasInteraction = useCallback(() => {
|
|
154
|
+
if (!isDetailToOverviewProfile || miniOnboardingInteractionSeen) return
|
|
155
|
+
setMiniOnboardingInteractionSeen(true)
|
|
156
|
+
setShowMiniOnboarding(true)
|
|
157
|
+
}, [isDetailToOverviewProfile, miniOnboardingInteractionSeen])
|
|
158
|
+
|
|
159
|
+
const handleCanvasZoom = useCallback(() => {
|
|
160
|
+
setMiniOnboardingInteractionSeen(true)
|
|
161
|
+
dismissMiniOnboarding()
|
|
162
|
+
}, [dismissMiniOnboarding])
|
|
163
|
+
|
|
134
164
|
useEffect(() => {
|
|
135
165
|
const loader = sharedToken ? api.explore.loadShared(sharedToken) : api.explore.load()
|
|
136
166
|
loader.then((d) => {
|
|
@@ -173,6 +203,24 @@ function InfiniteZoomInner({ sharedToken, shareSlot }: Props, ref?: React.Ref<In
|
|
|
173
203
|
setCanvasReady(true)
|
|
174
204
|
}, [])
|
|
175
205
|
|
|
206
|
+
useEffect(() => {
|
|
207
|
+
if (!sharedToken) return
|
|
208
|
+
|
|
209
|
+
const handleMessage = (event: MessageEvent) => {
|
|
210
|
+
const data = event.data as { type?: unknown; progress?: unknown; profile?: unknown } | null
|
|
211
|
+
if (!data || data.type !== 'tldiagram-zui-camera') return
|
|
212
|
+
if (data.profile !== 'detail-to-overview') return
|
|
213
|
+
|
|
214
|
+
const progress = Number(data.progress)
|
|
215
|
+
if (!Number.isFinite(progress)) return
|
|
216
|
+
|
|
217
|
+
zuiRef.current?.setCameraFrame({ profile: 'detail-to-overview', progress })
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
window.addEventListener('message', handleMessage)
|
|
221
|
+
return () => window.removeEventListener('message', handleMessage)
|
|
222
|
+
}, [sharedToken])
|
|
223
|
+
|
|
176
224
|
if (!loading && (!data || (data.tree ?? []).length === 0 || !hasPlacements)) {
|
|
177
225
|
const noDiagrams = !data || (data.tree ?? []).length === 0
|
|
178
226
|
return (
|
|
@@ -221,8 +269,9 @@ function InfiniteZoomInner({ sharedToken, shareSlot }: Props, ref?: React.Ref<In
|
|
|
221
269
|
ref={zuiRef}
|
|
222
270
|
data={data}
|
|
223
271
|
onReady={handleCanvasReady}
|
|
224
|
-
onZoom={
|
|
225
|
-
onPan={
|
|
272
|
+
onZoom={handleCanvasZoom}
|
|
273
|
+
onPan={showMiniOnboardingAfterCanvasInteraction}
|
|
274
|
+
initialCameraFrame={initialCameraFrame}
|
|
226
275
|
highlightedTags={highlightedTags}
|
|
227
276
|
highlightColor={highlightColor}
|
|
228
277
|
hiddenTags={hiddenTags}
|
|
@@ -232,7 +281,7 @@ function InfiniteZoomInner({ sharedToken, shareSlot }: Props, ref?: React.Ref<In
|
|
|
232
281
|
|
|
233
282
|
{/* Onboarding overlay */}
|
|
234
283
|
{data && !sharedToken && <ExploreOnboarding hasLinkedNodes={!!(data.navigations?.length > 0)} />}
|
|
235
|
-
<MiniZoomOnboarding isVisible={showMiniOnboarding} />
|
|
284
|
+
<MiniZoomOnboarding isVisible={showMiniOnboarding} onClose={dismissMiniOnboarding} />
|
|
236
285
|
|
|
237
286
|
{/* Bottom toolbar */}
|
|
238
287
|
<Box
|