@tldiagram/core-ui 1.95.0 → 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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tldiagram/core-ui",
3
- "version": "1.95.0",
3
+ "version": "1.95.1",
4
4
  "type": "module",
5
5
  "repository": {
6
6
  "type": "git",
@@ -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="none"
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
- Pro Tip
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
  >
@@ -51,6 +51,7 @@ interface Props {
51
51
  onReady?: () => void
52
52
  onZoom?: () => void
53
53
  onPan?: () => void
54
+ initialCameraFrame?: ZUICameraFrame
54
55
  highlightedTags?: string[]
55
56
  highlightColor?: string
56
57
  hiddenTags?: string[]
@@ -319,7 +320,7 @@ function findFirstExpandableNodeInTree(
319
320
  return null
320
321
  }
321
322
 
322
- export const ZUICanvas = forwardRef<ZUICanvasHandle, Props>(function ZUICanvas({ data, onReady, onZoom, onPan, highlightedTags, highlightColor, hiddenTags, crossBranchSettings, hoverLocked = false }, ref) {
323
+ export const ZUICanvas = forwardRef<ZUICanvasHandle, Props>(function ZUICanvas({ data, onReady, onZoom, onPan, initialCameraFrame, highlightedTags, highlightColor, hiddenTags, crossBranchSettings, hoverLocked = false }, ref) {
323
324
  const canvasRef = useRef<HTMLCanvasElement>(null)
324
325
  const containerRef = useRef<HTMLDivElement>(null)
325
326
  const cameraTransitionRef = useRef<number | null>(null)
@@ -583,6 +584,11 @@ export const ZUICanvas = forwardRef<ZUICanvasHandle, Props>(function ZUICanvas({
583
584
  return true
584
585
  }, [layout.groups, maxZoom, setHoveredItem, setViewState])
585
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
+
586
592
  useEffect(() => {
587
593
  return () => {
588
594
  if (cameraTransitionRef.current !== null) {
@@ -600,14 +606,13 @@ export const ZUICanvas = forwardRef<ZUICanvasHandle, Props>(function ZUICanvas({
600
606
  // Only set as initialized if we have valid dimensions
601
607
  if (w > 0 && h > 0) {
602
608
  setContainerSize({ w, h })
603
- fitView(w, h, layout.bbox)
609
+ fitInitialView(w, h)
604
610
  if (!initialized) {
605
611
  setInitialized(true)
606
612
  onReady?.()
607
613
  }
608
614
  }
609
- // eslint-disable-next-line react-hooks/exhaustive-deps
610
- }, [layout, initialized, onReady])
615
+ }, [initialized, onReady, fitInitialView])
611
616
 
612
617
  // ── Expose fitView to parent ─────────────────────────────────────
613
618
  useImperativeHandle(
@@ -670,7 +675,7 @@ export const ZUICanvas = forwardRef<ZUICanvasHandle, Props>(function ZUICanvas({
670
675
 
671
676
  // Trigger initialization if it hasn't happened yet
672
677
  if (!initialized && w > 0 && h > 0) {
673
- fitView(w, h, layout.bbox)
678
+ fitInitialView(w, h)
674
679
  setInitialized(true)
675
680
  onReady?.()
676
681
  }
@@ -680,7 +685,7 @@ export const ZUICanvas = forwardRef<ZUICanvasHandle, Props>(function ZUICanvas({
680
685
  ro.observe(container)
681
686
  resize()
682
687
  return () => ro.disconnect()
683
- }, [initialized, layout, fitView, onReady])
688
+ }, [initialized, fitInitialView, onReady])
684
689
 
685
690
  useEffect(() => {
686
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,
@@ -44,11 +44,13 @@ const MINI_ONBOARDING_KEY = 'shared_zoom_onboarding_dismissed'
44
44
  // ── Inner component ────────────────────────────────────────────────
45
45
  function InfiniteZoomInner({ sharedToken, shareSlot }: Props, ref?: React.Ref<InfiniteZoomHandle>) {
46
46
  const navigate = useNavigate()
47
+ const location = useLocation()
47
48
 
48
49
  const [data, setData] = useState<ExploreData | null>(null)
49
50
  const [loading, setLoading] = useState(true)
50
51
  const [canvasReady, setCanvasReady] = useState(false)
51
52
  const [showMiniOnboarding, setShowMiniOnboarding] = useState(false)
53
+ const [miniOnboardingInteractionSeen, setMiniOnboardingInteractionSeen] = useState(false)
52
54
  const [tagColors] = useState<Record<string, import('../types').Tag>>({})
53
55
  const [layers, setLayers] = useState<ViewLayer[]>([])
54
56
  const [highlightedTags, setHighlightedTags] = useState<string[]>([])
@@ -59,6 +61,15 @@ function InfiniteZoomInner({ sharedToken, shareSlot }: Props, ref?: React.Ref<In
59
61
  const crossBranchSurface = sharedToken ? 'zui-shared' : 'zui'
60
62
  const { settings: crossBranchSettings, setEnabled: setCrossBranchEnabled } = useCrossBranchContextSettings(crossBranchSurface)
61
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
+
62
73
  useImperativeHandle(ref, () => ({
63
74
  focusDiagram(viewId: number) {
64
75
  return zuiRef.current?.focusDiagram(viewId) ?? false
@@ -124,17 +135,32 @@ function InfiniteZoomInner({ sharedToken, shareSlot }: Props, ref?: React.Ref<In
124
135
  }, [])
125
136
 
126
137
  useEffect(() => {
138
+ if (isDetailToOverviewProfile) return
127
139
  if (sharedToken && canvasReady && !localStorage.getItem(MINI_ONBOARDING_KEY)) {
128
140
  setShowMiniOnboarding(true)
129
141
  }
130
- }, [sharedToken, canvasReady])
142
+ }, [sharedToken, canvasReady, isDetailToOverviewProfile])
131
143
 
132
- const handleInteraction = useCallback(() => {
144
+ const dismissMiniOnboarding = useCallback(() => {
133
145
  if (showMiniOnboarding) {
134
146
  setShowMiniOnboarding(false)
135
- localStorage.setItem(MINI_ONBOARDING_KEY, 'true')
147
+ if (!isDetailToOverviewProfile) {
148
+ localStorage.setItem(MINI_ONBOARDING_KEY, 'true')
149
+ }
136
150
  }
137
- }, [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
+
138
164
  useEffect(() => {
139
165
  const loader = sharedToken ? api.explore.loadShared(sharedToken) : api.explore.load()
140
166
  loader.then((d) => {
@@ -243,8 +269,9 @@ function InfiniteZoomInner({ sharedToken, shareSlot }: Props, ref?: React.Ref<In
243
269
  ref={zuiRef}
244
270
  data={data}
245
271
  onReady={handleCanvasReady}
246
- onZoom={handleInteraction}
247
- onPan={handleInteraction}
272
+ onZoom={handleCanvasZoom}
273
+ onPan={showMiniOnboardingAfterCanvasInteraction}
274
+ initialCameraFrame={initialCameraFrame}
248
275
  highlightedTags={highlightedTags}
249
276
  highlightColor={highlightColor}
250
277
  hiddenTags={hiddenTags}
@@ -254,7 +281,7 @@ function InfiniteZoomInner({ sharedToken, shareSlot }: Props, ref?: React.Ref<In
254
281
 
255
282
  {/* Onboarding overlay */}
256
283
  {data && !sharedToken && <ExploreOnboarding hasLinkedNodes={!!(data.navigations?.length > 0)} />}
257
- <MiniZoomOnboarding isVisible={showMiniOnboarding} />
284
+ <MiniZoomOnboarding isVisible={showMiniOnboarding} onClose={dismissMiniOnboarding} />
258
285
 
259
286
  {/* Bottom toolbar */}
260
287
  <Box