@tldiagram/core-ui 1.94.4 → 1.95.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/components/PanelUI.d.ts +3 -2
- package/dist/components/ZUI/ZUICanvas.d.ts +5 -0
- package/dist/components/ZUI/index.d.ts +1 -1
- package/dist/index.js +6989 -6768
- package/dist/pages/InfiniteZoom.d.ts +2 -0
- package/package.json +1 -1
- package/src/components/PanelUI.tsx +3 -2
- package/src/components/ViewExplorer/TagManager/index.tsx +94 -43
- package/src/components/ViewExplorer/ViewNavigator.tsx +122 -36
- package/src/components/ViewExplorer/index.tsx +9 -33
- package/src/components/ZUI/ZUICanvas.tsx +140 -1
- package/src/components/ZUI/index.ts +1 -1
- package/src/pages/InfiniteZoom.tsx +25 -3
|
@@ -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,7 +1,7 @@
|
|
|
1
1
|
import React from 'react'
|
|
2
|
-
import { Box } from '@chakra-ui/react'
|
|
2
|
+
import { Box, BoxProps } from '@chakra-ui/react'
|
|
3
3
|
|
|
4
|
-
export function KbdHint({ children }: { children: string }) {
|
|
4
|
+
export function KbdHint({ children, ...props }: { children: string } & BoxProps) {
|
|
5
5
|
return (
|
|
6
6
|
<Box
|
|
7
7
|
as="span"
|
|
@@ -17,6 +17,7 @@ export function KbdHint({ children }: { children: string }) {
|
|
|
17
17
|
color="whiteAlpha.900"
|
|
18
18
|
flexShrink={0}
|
|
19
19
|
ml={2}
|
|
20
|
+
{...props}
|
|
20
21
|
>
|
|
21
22
|
{children}
|
|
22
23
|
</Box>
|
|
@@ -60,6 +60,7 @@ export const TagManager: React.FC<Props> = ({
|
|
|
60
60
|
layerCounts,
|
|
61
61
|
}) => {
|
|
62
62
|
const [isCollapsed, setIsCollapsed] = useState(false)
|
|
63
|
+
const [isUnusedCollapsed, setIsUnusedCollapsed] = useState(true)
|
|
63
64
|
const [expandedLayerIds, setExpandedLayerIds] = useState<Set<number>>(new Set())
|
|
64
65
|
const [namingPopover, setNamingPopover] = useState<{
|
|
65
66
|
isOpen: boolean;
|
|
@@ -122,7 +123,7 @@ export const TagManager: React.FC<Props> = ({
|
|
|
122
123
|
const handleConfirmNaming = async (name: string) => {
|
|
123
124
|
const { tags } = namingPopover
|
|
124
125
|
if (!tags || tags.length === 0) return
|
|
125
|
-
|
|
126
|
+
|
|
126
127
|
const color = pickUnusedColor(Object.values(tagColors).map(t => t.color))
|
|
127
128
|
await onCreateLayer(name, tags, color)
|
|
128
129
|
setNamingPopover(prev => ({ ...prev, isOpen: false, targetTag: null, targetLayerId: null }))
|
|
@@ -300,28 +301,53 @@ export const TagManager: React.FC<Props> = ({
|
|
|
300
301
|
<Divider borderColor="whiteAlpha.100" />
|
|
301
302
|
)}
|
|
302
303
|
{unusedTags.length > 0 && (
|
|
303
|
-
<
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
304
|
+
<VStack align="stretch" spacing={2}>
|
|
305
|
+
<HStack
|
|
306
|
+
spacing={1.5}
|
|
307
|
+
cursor="pointer"
|
|
308
|
+
onClick={() => setIsUnusedCollapsed(!isUnusedCollapsed)}
|
|
309
|
+
role="button"
|
|
310
|
+
opacity={0.6}
|
|
311
|
+
_hover={{ opacity: 1 }}
|
|
312
|
+
transition="opacity 0.2s"
|
|
313
|
+
>
|
|
314
|
+
<Box
|
|
315
|
+
transform={isUnusedCollapsed ? 'rotate(-90deg)' : 'none'}
|
|
316
|
+
transition="transform 0.2s"
|
|
317
|
+
display="flex"
|
|
318
|
+
alignItems="center"
|
|
319
|
+
>
|
|
320
|
+
<ChevronDownIcon size={10} />
|
|
321
|
+
</Box>
|
|
322
|
+
<Text fontSize="10px" fontWeight="bold" color="gray.500" textTransform="uppercase" letterSpacing="0.05em">
|
|
323
|
+
Unused on view ({unusedTags.length})
|
|
324
|
+
</Text>
|
|
325
|
+
</HStack>
|
|
326
|
+
{!isUnusedCollapsed && (
|
|
327
|
+
<Wrap spacing={1} opacity={0.6}>
|
|
328
|
+
{unusedTags.map((tag) => (
|
|
329
|
+
<WrapItem key={tag}>
|
|
330
|
+
<TagItem
|
|
331
|
+
tag={tag}
|
|
332
|
+
color={tagColors[tag]?.color || '#A0AEC0'}
|
|
333
|
+
description={tagColors[tag]?.description || null}
|
|
334
|
+
isAssigned={(selectedElement.tags || []).includes(tag)}
|
|
335
|
+
tagCount={tagCounts[tag]}
|
|
336
|
+
onToggle={() => onToggleTagOnElement(tag)}
|
|
337
|
+
onHover={(active) => onHoverLayer(active ? [tag] : null, tagColors[tag]?.color)}
|
|
338
|
+
onDropTag={(dragged: string) => handleCreateGroup(tag, dragged, tag)}
|
|
339
|
+
onDropLayer={(draggedId: number) => handleCreateGroupFromLayer(tag, draggedId)}
|
|
340
|
+
namingPopover={namingPopover.targetTag === tag ? namingPopover : undefined}
|
|
341
|
+
onConfirmNaming={handleConfirmNaming}
|
|
342
|
+
onCloseNaming={() => setNamingPopover(prev => ({ ...prev, isOpen: false, targetTag: null }))}
|
|
343
|
+
onSetColor={(color) => onCreateTag(tag, color)}
|
|
344
|
+
onSetDescription={(desc) => onCreateTag(tag, tagColors[tag]?.color, desc)}
|
|
345
|
+
/>
|
|
346
|
+
</WrapItem>
|
|
347
|
+
))}
|
|
348
|
+
</Wrap>
|
|
349
|
+
)}
|
|
350
|
+
</VStack>
|
|
325
351
|
)}
|
|
326
352
|
</VStack>
|
|
327
353
|
</Box>
|
|
@@ -385,26 +411,51 @@ export const TagManager: React.FC<Props> = ({
|
|
|
385
411
|
<Divider borderColor="whiteAlpha.100" />
|
|
386
412
|
)}
|
|
387
413
|
{unusedTags.length > 0 && (
|
|
388
|
-
<
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
/>
|
|
405
|
-
</
|
|
406
|
-
|
|
407
|
-
|
|
414
|
+
<VStack align="stretch" spacing={2}>
|
|
415
|
+
<HStack
|
|
416
|
+
spacing={1.5}
|
|
417
|
+
cursor="pointer"
|
|
418
|
+
onClick={() => setIsUnusedCollapsed(!isUnusedCollapsed)}
|
|
419
|
+
role="button"
|
|
420
|
+
opacity={0.6}
|
|
421
|
+
_hover={{ opacity: 1 }}
|
|
422
|
+
transition="opacity 0.2s"
|
|
423
|
+
>
|
|
424
|
+
<Box
|
|
425
|
+
transform={isUnusedCollapsed ? 'rotate(-90deg)' : 'none'}
|
|
426
|
+
transition="transform 0.2s"
|
|
427
|
+
display="flex"
|
|
428
|
+
alignItems="center"
|
|
429
|
+
>
|
|
430
|
+
<ChevronDownIcon size={10} />
|
|
431
|
+
</Box>
|
|
432
|
+
<Text fontSize="10px" fontWeight="bold" color="gray.500" textTransform="uppercase" letterSpacing="0.05em">
|
|
433
|
+
Other tags ({unusedTags.length})
|
|
434
|
+
</Text>
|
|
435
|
+
</HStack>
|
|
436
|
+
{!isUnusedCollapsed && (
|
|
437
|
+
<Wrap spacing={2} opacity={0.6}>
|
|
438
|
+
{unusedTags.map((tag) => (
|
|
439
|
+
<WrapItem key={tag}>
|
|
440
|
+
<TagItem
|
|
441
|
+
tag={tag}
|
|
442
|
+
color={tagColors[tag]?.color || '#A0AEC0'}
|
|
443
|
+
description={tagColors[tag]?.description || null}
|
|
444
|
+
tagCount={tagCounts[tag]}
|
|
445
|
+
onHover={(active) => onHoverLayer(active ? [tag] : null, tagColors[tag]?.color)}
|
|
446
|
+
onDropTag={(dragged: string) => handleCreateGroup(tag, dragged, tag)}
|
|
447
|
+
onDropLayer={(draggedId: number) => handleCreateGroupFromLayer(tag, draggedId)}
|
|
448
|
+
namingPopover={namingPopover.targetTag === tag ? namingPopover : undefined}
|
|
449
|
+
onConfirmNaming={handleConfirmNaming}
|
|
450
|
+
onCloseNaming={() => setNamingPopover(prev => ({ ...prev, isOpen: false, targetTag: null }))}
|
|
451
|
+
onSetColor={(color) => onCreateTag(tag, color)}
|
|
452
|
+
onSetDescription={(desc) => onCreateTag(tag, tagColors[tag]?.color, desc)}
|
|
453
|
+
/>
|
|
454
|
+
</WrapItem>
|
|
455
|
+
))}
|
|
456
|
+
</Wrap>
|
|
457
|
+
)}
|
|
458
|
+
</VStack>
|
|
408
459
|
)}
|
|
409
460
|
</VStack>
|
|
410
461
|
</Box>
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import React from 'react'
|
|
2
|
-
import { Box, Tooltip, VStack, Text,
|
|
1
|
+
import React, { useState } from 'react'
|
|
2
|
+
import { Box, Tooltip, VStack, Text, HStack } from '@chakra-ui/react'
|
|
3
3
|
import { ZoomInIcon, ZoomOutIcon, ChevronDownIcon } from '../Icons'
|
|
4
4
|
import { KbdHint } from '../PanelUI'
|
|
5
5
|
import { NavItem } from './types'
|
|
@@ -20,6 +20,8 @@ export const ViewNavigator: React.FC<Props> = ({
|
|
|
20
20
|
onFilterToggle,
|
|
21
21
|
onHoverZoom,
|
|
22
22
|
}) => {
|
|
23
|
+
const [hoveredType, setHoveredType] = useState<'out' | 'in' | null>(null)
|
|
24
|
+
|
|
23
25
|
const renderNavButton = (type: 'out' | 'in', items: NavItem[]) => {
|
|
24
26
|
const isOut = type === 'out'
|
|
25
27
|
const label = isOut ? 'Zoom Out' : 'Zoom In'
|
|
@@ -28,6 +30,7 @@ export const ViewNavigator: React.FC<Props> = ({
|
|
|
28
30
|
const disabled = items.length === 0
|
|
29
31
|
const isActive = activeFilter === type
|
|
30
32
|
const accentColor = isOut ? PARENT_VIEW_COLOR : CHILD_VIEW_COLOR
|
|
33
|
+
const isHovered = hoveredType === type
|
|
31
34
|
|
|
32
35
|
const subtitle = disabled
|
|
33
36
|
? isOut
|
|
@@ -46,7 +49,7 @@ export const ViewNavigator: React.FC<Props> = ({
|
|
|
46
49
|
: 'No child views'
|
|
47
50
|
: `Navigate to ${isOut ? 'Parent' : 'Child'} View [${shortcut}]`
|
|
48
51
|
}
|
|
49
|
-
placement="
|
|
52
|
+
placement="top"
|
|
50
53
|
openDelay={400}
|
|
51
54
|
>
|
|
52
55
|
<Box
|
|
@@ -56,41 +59,125 @@ export const ViewNavigator: React.FC<Props> = ({
|
|
|
56
59
|
disabled={disabled}
|
|
57
60
|
onClick={() => onFilterToggle(type, items)}
|
|
58
61
|
onMouseEnter={() => {
|
|
62
|
+
setHoveredType(type)
|
|
59
63
|
if (disabled || items.length !== 1) return
|
|
60
64
|
onHoverZoom?.(items[0].elementId ?? null, type)
|
|
61
65
|
}}
|
|
62
66
|
onMouseLeave={() => {
|
|
67
|
+
setHoveredType(null)
|
|
63
68
|
if (disabled || items.length !== 1) return
|
|
64
69
|
onHoverZoom?.(null, null)
|
|
65
70
|
}}
|
|
66
71
|
opacity={disabled ? 0.4 : 1}
|
|
72
|
+
flex={hoveredType === null ? 1 : (isHovered ? 4 : 1)}
|
|
73
|
+
transition="all 0.4s cubic-bezier(0.4, 0, 0.2, 1)"
|
|
74
|
+
minW={0}
|
|
75
|
+
overflow="hidden"
|
|
76
|
+
position="relative"
|
|
77
|
+
px={isHovered || hoveredType === null ? 3 : 1}
|
|
78
|
+
sx={{
|
|
79
|
+
'.panel-action-icon-container': {
|
|
80
|
+
transition: 'all 0.3s ease',
|
|
81
|
+
width: '28px',
|
|
82
|
+
height: '28px',
|
|
83
|
+
marginRight: isOut ? '0.5rem' : 0,
|
|
84
|
+
marginLeft: isOut ? 0 : '0.5rem',
|
|
85
|
+
position: 'relative',
|
|
86
|
+
}
|
|
87
|
+
}}
|
|
67
88
|
>
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
89
|
+
{isOut ? (
|
|
90
|
+
<>
|
|
91
|
+
<Box className="panel-action-icon-container" color={disabled ? 'gray.600' : accentColor}>
|
|
92
|
+
<IconCmp />
|
|
93
|
+
{items.length > 1 && (
|
|
94
|
+
<Box
|
|
95
|
+
position="absolute"
|
|
96
|
+
bottom="-5px"
|
|
97
|
+
right="-5px"
|
|
98
|
+
color="white"
|
|
99
|
+
fontSize="12px"
|
|
100
|
+
fontWeight="bold"
|
|
101
|
+
minW="14px"
|
|
102
|
+
h="14px"
|
|
103
|
+
display="flex"
|
|
104
|
+
alignItems="center"
|
|
105
|
+
justifyContent="center"
|
|
106
|
+
zIndex={2}
|
|
107
|
+
>
|
|
108
|
+
{items.length}
|
|
109
|
+
</Box>
|
|
110
|
+
)}
|
|
111
|
+
</Box>
|
|
112
|
+
<VStack
|
|
113
|
+
align="start"
|
|
114
|
+
spacing={0}
|
|
115
|
+
flex={1}
|
|
116
|
+
minW={0}
|
|
117
|
+
opacity={hoveredType === 'in' ? 0 : 1}
|
|
118
|
+
transition="opacity 0.2s, transform 0.3s"
|
|
119
|
+
transform={hoveredType === 'in' ? 'translateX(-10px)' : 'none'}
|
|
120
|
+
>
|
|
121
|
+
<Text fontSize="xs" color={disabled ? 'gray.500' : 'white'} fontWeight="bold" isTruncated w="full" textAlign="left">
|
|
122
|
+
{label}
|
|
123
|
+
</Text>
|
|
124
|
+
{isHovered && (
|
|
125
|
+
<Text fontSize="10px" color={disabled ? 'gray.600' : isActive ? accentColor : 'gray.500'} isTruncated w="full" transition="color 0.15s" textAlign="left">
|
|
126
|
+
{subtitle}
|
|
127
|
+
</Text>
|
|
128
|
+
)}
|
|
129
|
+
</VStack>
|
|
130
|
+
<Box opacity={hoveredType === 'in' ? 0 : 1} transition="opacity 0.2s">
|
|
131
|
+
<KbdHint ml={1}>{shortcut}</KbdHint>
|
|
132
|
+
</Box>
|
|
133
|
+
</>
|
|
134
|
+
) : (
|
|
135
|
+
<>
|
|
136
|
+
<Box opacity={hoveredType === 'out' ? 0 : 1} transition="opacity 0.2s">
|
|
137
|
+
<KbdHint ml={0} mr={1}>{shortcut}</KbdHint>
|
|
138
|
+
</Box>
|
|
139
|
+
<VStack
|
|
140
|
+
align="end"
|
|
141
|
+
spacing={0}
|
|
142
|
+
flex={1}
|
|
143
|
+
minW={0}
|
|
144
|
+
opacity={hoveredType === 'out' ? 0 : 1}
|
|
145
|
+
transition="opacity 0.2s, transform 0.3s"
|
|
146
|
+
transform={hoveredType === 'out' ? 'translateX(10px)' : 'none'}
|
|
147
|
+
>
|
|
148
|
+
<Text fontSize="xs" color={disabled ? 'gray.500' : 'white'} fontWeight="bold" isTruncated w="full" textAlign="right">
|
|
149
|
+
{label}
|
|
150
|
+
</Text>
|
|
151
|
+
{isHovered && (
|
|
152
|
+
<Text fontSize="10px" color={disabled ? 'gray.600' : isActive ? accentColor : 'gray.500'} isTruncated w="full" transition="color 0.15s" textAlign="right">
|
|
153
|
+
{subtitle}
|
|
154
|
+
</Text>
|
|
155
|
+
)}
|
|
156
|
+
</VStack>
|
|
157
|
+
<Box className="panel-action-icon-container" color={disabled ? 'gray.600' : accentColor}>
|
|
158
|
+
<IconCmp />
|
|
159
|
+
{items.length > 1 && (
|
|
160
|
+
<Box
|
|
161
|
+
position="absolute"
|
|
162
|
+
bottom="-5px"
|
|
163
|
+
right="-5px"
|
|
164
|
+
color="white"
|
|
165
|
+
fontSize="12px"
|
|
166
|
+
fontWeight="bold"
|
|
167
|
+
minW="14px"
|
|
168
|
+
h="14px"
|
|
169
|
+
display="flex"
|
|
170
|
+
alignItems="center"
|
|
171
|
+
justifyContent="center"
|
|
172
|
+
zIndex={2}
|
|
173
|
+
>
|
|
174
|
+
{items.length}
|
|
175
|
+
</Box>
|
|
176
|
+
)}
|
|
177
|
+
</Box>
|
|
178
|
+
</>
|
|
179
|
+
)}
|
|
180
|
+
{items.length > 1 && isHovered && (
|
|
94
181
|
<Box
|
|
95
182
|
color="whiteAlpha.400"
|
|
96
183
|
_groupHover={{ color: 'white' }}
|
|
@@ -102,20 +189,19 @@ export const ViewNavigator: React.FC<Props> = ({
|
|
|
102
189
|
<ChevronDownIcon size={12} strokeWidth={3.5} />
|
|
103
190
|
</Box>
|
|
104
191
|
)}
|
|
105
|
-
{isActive && items.length <= 1 && (
|
|
192
|
+
{isActive && items.length <= 1 && isHovered && (
|
|
106
193
|
<Box w="5px" h="5px" rounded="full" bg={accentColor} flexShrink={0} mx={1} />
|
|
107
194
|
)}
|
|
108
|
-
<KbdHint>{shortcut}</KbdHint>
|
|
109
195
|
</Box>
|
|
110
196
|
</Tooltip>
|
|
111
197
|
)
|
|
112
198
|
}
|
|
113
199
|
|
|
114
200
|
return (
|
|
115
|
-
<
|
|
116
|
-
|
|
117
|
-
<
|
|
118
|
-
|
|
119
|
-
</
|
|
201
|
+
<HStack spacing={0} align="stretch" flexShrink={0} height="60px" borderColor="whiteAlpha.100">
|
|
202
|
+
{renderNavButton('out', parents)}
|
|
203
|
+
<Box w="1px" bg="whiteAlpha.100" />
|
|
204
|
+
{renderNavButton('in', children)}
|
|
205
|
+
</HStack>
|
|
120
206
|
)
|
|
121
207
|
}
|
|
@@ -142,23 +142,17 @@ function ViewExplorer({
|
|
|
142
142
|
const children = useMemo(() => {
|
|
143
143
|
if (viewId == null) return []
|
|
144
144
|
const diagMap = new Map<number, NavItem>()
|
|
145
|
-
|
|
146
|
-
if (n.parent_view_id === viewId)
|
|
147
|
-
diagMap.set(n.id, { id: n.id, name: n.name, subtitle: 'Child View' })
|
|
148
|
-
})
|
|
145
|
+
|
|
149
146
|
const objMap = new Map(viewElements.map((o) => [o.element_id, o.name]))
|
|
150
147
|
Object.entries(linksMap).forEach(([elementIdStr, links]) => {
|
|
151
148
|
const elementId = Number(elementIdStr)
|
|
152
|
-
if (!Number.isFinite(elementId)) return
|
|
153
|
-
|
|
149
|
+
if (!Number.isFinite(elementId) || !onCanvasIds.has(elementId)) return
|
|
150
|
+
|
|
154
151
|
const objName = objMap.get(elementId) || 'Element'
|
|
155
152
|
links.forEach((link) => {
|
|
156
153
|
const existing = diagMap.get(link.to_view_id)
|
|
157
154
|
if (existing) {
|
|
158
|
-
if (!existing.
|
|
159
|
-
if (existing.subtitle === 'Child View') {
|
|
160
|
-
existing.subtitle = `Child View (Via ${objName})`
|
|
161
|
-
} else if (!existing.subtitle?.includes(objName)) {
|
|
155
|
+
if (!existing.subtitle?.includes(objName)) {
|
|
162
156
|
existing.subtitle = `${existing.subtitle}, ${objName}`
|
|
163
157
|
}
|
|
164
158
|
} else {
|
|
@@ -166,13 +160,13 @@ function ViewExplorer({
|
|
|
166
160
|
id: link.to_view_id,
|
|
167
161
|
name: link.to_view_name,
|
|
168
162
|
subtitle: `Via ${objName}`,
|
|
169
|
-
elementId:
|
|
163
|
+
elementId: elementId,
|
|
170
164
|
})
|
|
171
165
|
}
|
|
172
166
|
})
|
|
173
167
|
})
|
|
174
168
|
return Array.from(diagMap.values())
|
|
175
|
-
}, [viewId,
|
|
169
|
+
}, [viewId, linksMap, viewElements, onCanvasIds])
|
|
176
170
|
|
|
177
171
|
const viewHoverMap = useMemo(() => {
|
|
178
172
|
const map = new Map<number, { elementId: number | undefined; type: 'in' | 'out' }>()
|
|
@@ -184,32 +178,14 @@ function ViewExplorer({
|
|
|
184
178
|
const filteredByMode = useMemo(() => {
|
|
185
179
|
if (activeFilter === 'out') {
|
|
186
180
|
const parentIds = new Set(parents.map((p) => p.id))
|
|
187
|
-
|
|
188
|
-
const findAncestors = (id: number) => {
|
|
189
|
-
const node = treeNodes.find(n => n.id === id)
|
|
190
|
-
if (node?.parent_view_id) {
|
|
191
|
-
ancestors.add(node.parent_view_id)
|
|
192
|
-
findAncestors(node.parent_view_id)
|
|
193
|
-
}
|
|
194
|
-
}
|
|
195
|
-
if (viewId) findAncestors(viewId)
|
|
196
|
-
return flat.filter((n) => parentIds.has(n.id) || ancestors.has(n.id) || n.id === viewId)
|
|
181
|
+
return flat.filter((n) => parentIds.has(n.id) || n.id === viewId)
|
|
197
182
|
}
|
|
198
183
|
if (activeFilter === 'in') {
|
|
199
184
|
const childIds = new Set(children.map((c) => c.id))
|
|
200
|
-
|
|
201
|
-
const traverse = (nodes: TreeNode[]) => {
|
|
202
|
-
nodes.forEach(n => {
|
|
203
|
-
subtree.add(n.id)
|
|
204
|
-
traverse(n.children)
|
|
205
|
-
})
|
|
206
|
-
}
|
|
207
|
-
const current = flat.find(n => n.id === viewId)
|
|
208
|
-
if (current) traverse(current.children)
|
|
209
|
-
return flat.filter((n) => childIds.has(n.id) || subtree.has(n.id) || n.id === viewId)
|
|
185
|
+
return flat.filter((n) => childIds.has(n.id) || n.id === viewId)
|
|
210
186
|
}
|
|
211
187
|
return flat
|
|
212
|
-
}, [flat, parents, children, activeFilter, viewId
|
|
188
|
+
}, [flat, parents, children, activeFilter, viewId])
|
|
213
189
|
|
|
214
190
|
const filtered = useMemo(() => {
|
|
215
191
|
if (!query.trim()) return filteredByMode
|
|
@@ -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 {
|
|
@@ -238,6 +244,81 @@ function easeOutQuart(t: number): number {
|
|
|
238
244
|
return 1 - Math.pow(1 - t, 4)
|
|
239
245
|
}
|
|
240
246
|
|
|
247
|
+
function clamp01(value: number): number {
|
|
248
|
+
return Math.max(0, Math.min(1, value))
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function fitWorldRect(
|
|
252
|
+
rect: { x: number; y: number; w: number; h: number },
|
|
253
|
+
canvasW: number,
|
|
254
|
+
canvasH: number,
|
|
255
|
+
maxZoom: number,
|
|
256
|
+
padding: number,
|
|
257
|
+
): ZUIViewState | null {
|
|
258
|
+
const bboxW = Math.max(1, rect.w)
|
|
259
|
+
const bboxH = Math.max(1, rect.h)
|
|
260
|
+
const zoom = Math.min(
|
|
261
|
+
(canvasW * (1 - padding * 2)) / bboxW,
|
|
262
|
+
(canvasH * (1 - padding * 2)) / bboxH,
|
|
263
|
+
maxZoom,
|
|
264
|
+
)
|
|
265
|
+
if (!Number.isFinite(zoom) || zoom <= 0) return null
|
|
266
|
+
|
|
267
|
+
return {
|
|
268
|
+
x: (canvasW - bboxW * zoom) / 2 - rect.x * zoom,
|
|
269
|
+
y: (canvasH - bboxH * zoom) / 2 - rect.y * zoom,
|
|
270
|
+
zoom,
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
function findFirstExpandableNode(groups: DiagramGroupLayout[]): PathItem | null {
|
|
275
|
+
for (const group of groups) {
|
|
276
|
+
const found = findFirstExpandableNodeInTree(group.nodes, 0, 0, 1, 0, 0)
|
|
277
|
+
if (found) return found
|
|
278
|
+
}
|
|
279
|
+
return null
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
function findFirstExpandableNodeInTree(
|
|
283
|
+
nodes: DiagramGroupLayout['nodes'],
|
|
284
|
+
parentAbsX: number,
|
|
285
|
+
parentAbsY: number,
|
|
286
|
+
parentAbsScale: number,
|
|
287
|
+
parentChildOffsetX: number,
|
|
288
|
+
parentChildOffsetY: number,
|
|
289
|
+
): PathItem | null {
|
|
290
|
+
for (const node of nodes) {
|
|
291
|
+
const absX = parentAbsX + (node.worldX - parentChildOffsetX) * parentAbsScale
|
|
292
|
+
const absY = parentAbsY + (node.worldY - parentChildOffsetY) * parentAbsScale
|
|
293
|
+
const absW = node.worldW * parentAbsScale
|
|
294
|
+
const absH = node.worldH * parentAbsScale
|
|
295
|
+
|
|
296
|
+
if (node.children.length > 0) {
|
|
297
|
+
return {
|
|
298
|
+
id: node.id,
|
|
299
|
+
label: node.linkedDiagramLabel || node.label,
|
|
300
|
+
type: 'node',
|
|
301
|
+
isCircular: node.isCircular,
|
|
302
|
+
absX,
|
|
303
|
+
absY,
|
|
304
|
+
absW,
|
|
305
|
+
absH,
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
const found = findFirstExpandableNodeInTree(
|
|
310
|
+
node.children,
|
|
311
|
+
absX,
|
|
312
|
+
absY,
|
|
313
|
+
parentAbsScale * node.childScale,
|
|
314
|
+
node.childOffsetX,
|
|
315
|
+
node.childOffsetY,
|
|
316
|
+
)
|
|
317
|
+
if (found) return found
|
|
318
|
+
}
|
|
319
|
+
return null
|
|
320
|
+
}
|
|
321
|
+
|
|
241
322
|
export const ZUICanvas = forwardRef<ZUICanvasHandle, Props>(function ZUICanvas({ data, onReady, onZoom, onPan, highlightedTags, highlightColor, hiddenTags, crossBranchSettings, hoverLocked = false }, ref) {
|
|
242
323
|
const canvasRef = useRef<HTMLCanvasElement>(null)
|
|
243
324
|
const containerRef = useRef<HTMLDivElement>(null)
|
|
@@ -445,6 +526,63 @@ export const ZUICanvas = forwardRef<ZUICanvasHandle, Props>(function ZUICanvas({
|
|
|
445
526
|
return true
|
|
446
527
|
}, [isMobileLayout, layout.groups, maxZoom, setHoveredItem, setViewState, viewStateRef])
|
|
447
528
|
|
|
529
|
+
const setCameraFrame = useCallback((frame: ZUICameraFrame) => {
|
|
530
|
+
if (frame.profile !== 'detail-to-overview') return false
|
|
531
|
+
|
|
532
|
+
const el = containerRef.current
|
|
533
|
+
if (!el) return false
|
|
534
|
+
|
|
535
|
+
const canvasW = el.offsetWidth
|
|
536
|
+
const canvasH = el.offsetHeight
|
|
537
|
+
if (canvasW === 0 || canvasH === 0) return false
|
|
538
|
+
|
|
539
|
+
const detailTarget = findFirstExpandableNode(layout.groups)
|
|
540
|
+
const overviewTarget = layout.groups[0]
|
|
541
|
+
if (!detailTarget || !overviewTarget) return false
|
|
542
|
+
|
|
543
|
+
const detail = fitWorldRect(
|
|
544
|
+
{
|
|
545
|
+
x: detailTarget.absX,
|
|
546
|
+
y: detailTarget.absY,
|
|
547
|
+
w: detailTarget.absW,
|
|
548
|
+
h: detailTarget.absH,
|
|
549
|
+
},
|
|
550
|
+
canvasW,
|
|
551
|
+
canvasH,
|
|
552
|
+
maxZoom,
|
|
553
|
+
0.28,
|
|
554
|
+
)
|
|
555
|
+
|
|
556
|
+
const overview = fitWorldRect(
|
|
557
|
+
{
|
|
558
|
+
x: overviewTarget.worldX,
|
|
559
|
+
y: overviewTarget.worldY,
|
|
560
|
+
w: overviewTarget.worldW,
|
|
561
|
+
h: overviewTarget.worldH,
|
|
562
|
+
},
|
|
563
|
+
canvasW,
|
|
564
|
+
canvasH,
|
|
565
|
+
maxZoom,
|
|
566
|
+
0.18,
|
|
567
|
+
)
|
|
568
|
+
|
|
569
|
+
if (!detail || !overview) return false
|
|
570
|
+
|
|
571
|
+
if (cameraTransitionRef.current !== null) {
|
|
572
|
+
cancelAnimationFrame(cameraTransitionRef.current)
|
|
573
|
+
cameraTransitionRef.current = null
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
setHoveredItem(null, true)
|
|
577
|
+
const t = easeOutQuart(clamp01(frame.progress))
|
|
578
|
+
setViewState({
|
|
579
|
+
x: detail.x + (overview.x - detail.x) * t,
|
|
580
|
+
y: detail.y + (overview.y - detail.y) * t,
|
|
581
|
+
zoom: detail.zoom + (overview.zoom - detail.zoom) * t,
|
|
582
|
+
})
|
|
583
|
+
return true
|
|
584
|
+
}, [layout.groups, maxZoom, setHoveredItem, setViewState])
|
|
585
|
+
|
|
448
586
|
useEffect(() => {
|
|
449
587
|
return () => {
|
|
450
588
|
if (cameraTransitionRef.current !== null) {
|
|
@@ -482,8 +620,9 @@ export const ZUICanvas = forwardRef<ZUICanvasHandle, Props>(function ZUICanvas({
|
|
|
482
620
|
fitView(el.offsetWidth, el.offsetHeight, layout.bbox)
|
|
483
621
|
},
|
|
484
622
|
focusDiagram,
|
|
623
|
+
setCameraFrame,
|
|
485
624
|
}),
|
|
486
|
-
[fitView, focusDiagram, layout.bbox, setHoveredItem],
|
|
625
|
+
[fitView, focusDiagram, layout.bbox, setCameraFrame, setHoveredItem],
|
|
487
626
|
)
|
|
488
627
|
|
|
489
628
|
// ── RAF render loop ──────────────────────────────────────────────
|