@tldiagram/core-ui 1.95.1 → 2.0.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/api/client.d.ts +184 -3
- package/dist/components/ConnectorPanel.d.ts +5 -1
- package/dist/components/CrossBranchControls.d.ts +4 -3
- package/dist/components/ElementNode.d.ts +5 -0
- package/dist/components/ElementPanel.d.ts +6 -1
- package/dist/components/LayoutSection.d.ts +2 -1
- package/dist/components/MergeDialog.d.ts +16 -0
- package/dist/components/NodeContainer.d.ts +2 -0
- package/dist/components/ProxyConnectorPanel.d.ts +4 -1
- package/dist/components/ViewExplorer/index.d.ts +1 -1
- package/dist/components/ViewFloatingMenu-vscode.d.ts +5 -0
- package/dist/components/ViewFloatingMenu.d.ts +8 -1
- package/dist/components/ViewGridNode.d.ts +3 -0
- package/dist/components/ViewPanel.d.ts +2 -1
- package/dist/components/WorkspacePanel.d.ts +2 -0
- package/dist/components/ZUI/ZUICanvas.d.ts +4 -0
- package/dist/components/ZUI/focus.d.ts +32 -0
- package/dist/components/ZUI/focus.test.d.ts +1 -0
- package/dist/components/ZUI/layout.d.ts +2 -2
- package/dist/components/ZUI/proxy.d.ts +20 -4
- package/dist/components/ZUI/renderer.d.ts +35 -1
- package/dist/components/ZUI/types.d.ts +6 -0
- package/dist/components/ZUI/useZUIInteraction.d.ts +1 -0
- package/dist/context/WorkspaceVersionContext.d.ts +49 -0
- package/dist/crossBranch/resolve.d.ts +39 -2
- package/dist/crossBranch/resolve.test.d.ts +1 -0
- package/dist/crossBranch/settings.d.ts +6 -1
- package/dist/crossBranch/types.d.ts +8 -0
- package/dist/hooks/useElementSearch.d.ts +8 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +16529 -14030
- package/dist/pages/InfiniteZoom.d.ts +1 -0
- package/dist/pages/ViewEditor/hooks/useCanvasInteractions.d.ts +6 -1
- package/dist/pages/ViewEditor/hooks/useViewContextNeighbours.d.ts +2 -0
- package/dist/pages/ViewEditor/hooks/useViewData.d.ts +4 -2
- package/dist/pages/ViewEditor/hooks/useViewEditHistory.d.ts +13 -0
- package/dist/pages/viewsJumpSearch.d.ts +22 -0
- package/dist/pages/viewsJumpSearch.test.d.ts +1 -0
- package/dist/store/useStore.d.ts +3 -0
- package/dist/types/index.d.ts +9 -0
- package/dist/utils/elementIcon.d.ts +2 -0
- package/dist/utils/elementIcon.test.d.ts +1 -0
- package/dist/utils/sourceEditor.d.ts +7 -0
- package/dist/utils/watchDiffSummary.d.ts +34 -0
- package/package.json +2 -2
- package/src/App.tsx +12 -8
- package/src/api/client.ts +488 -26
- package/src/components/CodePreviewPanel.tsx +90 -16
- package/src/components/ConnectorPanel.tsx +34 -3
- package/src/components/ContextNeighborElement.tsx +2 -5
- package/src/components/CrossBranchControls.tsx +46 -17
- package/src/components/ElementNode.tsx +98 -47
- package/src/components/ElementPanel.tsx +62 -25
- package/src/components/InlineElementAdder.tsx +8 -3
- package/src/components/LayoutSection.tsx +4 -1
- package/src/components/MergeDialog.tsx +269 -0
- package/src/components/NodeContainer.tsx +55 -17
- package/src/components/ProxyConnectorPanel.tsx +58 -16
- package/src/components/ViewBezierConnector.tsx +116 -21
- package/src/components/ViewExplorer/index.tsx +1 -1
- package/src/components/ViewFloatingMenu-vscode.tsx +5 -0
- package/src/components/ViewFloatingMenu.tsx +110 -1
- package/src/components/ViewGridNode.tsx +59 -8
- package/src/components/ViewPanel.tsx +3 -2
- package/src/components/WorkspacePanel.tsx +938 -0
- package/src/components/ZUI/ZUICanvas.tsx +216 -122
- package/src/components/ZUI/focus.test.ts +534 -0
- package/src/components/ZUI/focus.ts +293 -0
- package/src/components/ZUI/layout.ts +7 -11
- package/src/components/ZUI/proxy.ts +470 -114
- package/src/components/ZUI/renderer.ts +510 -134
- package/src/components/ZUI/types.ts +6 -0
- package/src/components/ZUI/useZUIInteraction.ts +66 -29
- package/src/context/WorkspaceVersionContext.tsx +126 -0
- package/src/crossBranch/resolve.test.ts +342 -0
- package/src/crossBranch/resolve.ts +368 -68
- package/src/crossBranch/settings.ts +49 -3
- package/src/crossBranch/types.ts +9 -0
- package/src/hooks/useElementSearch.ts +45 -0
- package/src/index.css +11 -0
- package/src/index.ts +7 -0
- package/src/pages/AppearanceSettings.tsx +24 -1
- package/src/pages/Dependencies.tsx +231 -65
- package/src/pages/InfiniteZoom.tsx +41 -19
- package/src/pages/Settings.tsx +1 -1
- package/src/pages/ViewEditor/hooks/useCanvasInteractions.ts +103 -24
- package/src/pages/ViewEditor/hooks/useViewContextNeighbours.ts +102 -6
- package/src/pages/ViewEditor/hooks/useViewData.ts +42 -26
- package/src/pages/ViewEditor/hooks/useViewEditHistory.ts +62 -0
- package/src/pages/ViewEditor/index.tsx +549 -59
- package/src/pages/Views.tsx +112 -41
- package/src/pages/ViewsGrid.tsx +332 -113
- package/src/pages/viewsJumpSearch.test.ts +193 -0
- package/src/pages/viewsJumpSearch.ts +111 -0
- package/src/store/useStore.ts +58 -0
- package/src/types/index.ts +10 -0
- package/src/utils/elementIcon.test.ts +28 -0
- package/src/utils/elementIcon.ts +20 -0
- package/src/utils/sourceEditor.ts +46 -0
- package/src/utils/watchDiffSummary.ts +159 -0
|
@@ -1,15 +1,19 @@
|
|
|
1
|
-
import { Box, Button, HStack, Icon, Text, VStack, Divider, Flex } from '@chakra-ui/react'
|
|
1
|
+
import { Box, Button, HStack, Icon, Text, VStack, Divider, Flex, IconButton } from '@chakra-ui/react'
|
|
2
2
|
import { useNavigate } from 'react-router-dom'
|
|
3
3
|
import type { ProxyConnectorDetails } from '../crossBranch/types'
|
|
4
4
|
import SlidingPanel from './SlidingPanel'
|
|
5
5
|
import PanelHeader from './PanelHeader'
|
|
6
|
-
import { ChevronRightIcon, NavigationIcon } from './Icons'
|
|
6
|
+
import { ChevronRightIcon, NavigationIcon, TrashIcon, EditIcon } from './Icons'
|
|
7
|
+
import { useViewEditorContext } from '../pages/ViewEditor/context'
|
|
8
|
+
import type { Connector } from '../types'
|
|
7
9
|
|
|
8
10
|
interface Props {
|
|
9
11
|
isOpen: boolean
|
|
10
12
|
onClose: () => void
|
|
11
13
|
details: ProxyConnectorDetails | null
|
|
12
14
|
hasBackdrop?: boolean
|
|
15
|
+
onEdit?: (connector: Connector) => void
|
|
16
|
+
onDelete?: (connectorId: number, ownerViewId: number) => void
|
|
13
17
|
}
|
|
14
18
|
|
|
15
19
|
export default function ProxyConnectorPanel({
|
|
@@ -17,8 +21,11 @@ export default function ProxyConnectorPanel({
|
|
|
17
21
|
onClose,
|
|
18
22
|
details,
|
|
19
23
|
hasBackdrop = true,
|
|
24
|
+
onEdit,
|
|
25
|
+
onDelete,
|
|
20
26
|
}: Props) {
|
|
21
27
|
const navigate = useNavigate()
|
|
28
|
+
const { canEdit } = useViewEditorContext()
|
|
22
29
|
|
|
23
30
|
return (
|
|
24
31
|
<SlidingPanel
|
|
@@ -27,6 +34,7 @@ export default function ProxyConnectorPanel({
|
|
|
27
34
|
panelKey="proxy-connector-panel"
|
|
28
35
|
width={{ base: 'calc(100vw - 24px)', md: '300px' }}
|
|
29
36
|
hasBackdrop={hasBackdrop}
|
|
37
|
+
zIndex={950}
|
|
30
38
|
>
|
|
31
39
|
<PanelHeader title="Relationships" onClose={onClose} />
|
|
32
40
|
|
|
@@ -71,21 +79,54 @@ export default function ProxyConnectorPanel({
|
|
|
71
79
|
>
|
|
72
80
|
<VStack align="stretch" spacing={3}>
|
|
73
81
|
<Box>
|
|
74
|
-
<HStack
|
|
75
|
-
<
|
|
76
|
-
{
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
82
|
+
<HStack justify="space-between" align="start">
|
|
83
|
+
<VStack align="start" spacing={1} flex={1}>
|
|
84
|
+
<HStack spacing={2}>
|
|
85
|
+
<Text color="white" fontSize="sm" fontWeight="semibold" isTruncated>
|
|
86
|
+
{leaf.source.actualElementName}
|
|
87
|
+
</Text>
|
|
88
|
+
<Icon as={ChevronRightIcon} color="whiteAlpha.400" />
|
|
89
|
+
<Text color="white" fontSize="sm" fontWeight="semibold" isTruncated>
|
|
90
|
+
{leaf.target.actualElementName}
|
|
91
|
+
</Text>
|
|
92
|
+
</HStack>
|
|
93
|
+
|
|
94
|
+
{(leaf.connector.label || leaf.connector.relationship) && (
|
|
95
|
+
<Text color="gray.400" fontSize="xs" fontStyle={!leaf.connector.label ? 'italic' : 'normal'}>
|
|
96
|
+
{leaf.connector.label || leaf.connector.relationship}
|
|
97
|
+
</Text>
|
|
98
|
+
)}
|
|
99
|
+
</VStack>
|
|
83
100
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
101
|
+
{canEdit && (
|
|
102
|
+
<HStack spacing={1} mt={-1}>
|
|
103
|
+
<IconButton
|
|
104
|
+
aria-label="Edit connector"
|
|
105
|
+
icon={<EditIcon size={14} />}
|
|
106
|
+
size="xs"
|
|
107
|
+
variant="ghost"
|
|
108
|
+
color="blue.300"
|
|
109
|
+
_hover={{ bg: 'blue.900', color: 'blue.100' }}
|
|
110
|
+
onClick={(e) => {
|
|
111
|
+
e.stopPropagation()
|
|
112
|
+
onEdit?.(leaf.connector)
|
|
113
|
+
}}
|
|
114
|
+
/>
|
|
115
|
+
<IconButton
|
|
116
|
+
aria-label="Delete connector"
|
|
117
|
+
icon={<TrashIcon size={14} />}
|
|
118
|
+
size="xs"
|
|
119
|
+
variant="ghost"
|
|
120
|
+
color="red.400"
|
|
121
|
+
_hover={{ bg: 'red.900', color: 'red.100' }}
|
|
122
|
+
onClick={(e) => {
|
|
123
|
+
e.stopPropagation()
|
|
124
|
+
onDelete?.(leaf.connector.id, leaf.ownerViewId)
|
|
125
|
+
}}
|
|
126
|
+
/>
|
|
127
|
+
</HStack>
|
|
128
|
+
)}
|
|
129
|
+
</HStack>
|
|
89
130
|
</Box>
|
|
90
131
|
|
|
91
132
|
{leaf.connector.description && (
|
|
@@ -128,3 +169,4 @@ export default function ProxyConnectorPanel({
|
|
|
128
169
|
</SlidingPanel>
|
|
129
170
|
)
|
|
130
171
|
}
|
|
172
|
+
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import { memo } from 'react'
|
|
1
|
+
import { memo, useCallback } from 'react'
|
|
2
2
|
import { BaseEdge, EdgeLabelRenderer, Position, useStore, type EdgeProps } from 'reactflow'
|
|
3
3
|
import { measureEdgeLabel, useEdgeLabelLayout } from './ViewEditorEdgeLabelLayout'
|
|
4
|
+
import type { ProxyConnectorDetails } from '../crossBranch/types'
|
|
4
5
|
|
|
5
6
|
const CURVATURE = 0.5
|
|
6
7
|
|
|
@@ -35,6 +36,7 @@ function ViewBezierConnector({
|
|
|
35
36
|
}: EdgeProps) {
|
|
36
37
|
const sourceNode = useStore((s) => s.nodeInternals.get(source))
|
|
37
38
|
const targetNode = useStore((s) => s.nodeInternals.get(target))
|
|
39
|
+
const edge = useStore((s) => s.edges.find((candidate) => candidate.id === id))
|
|
38
40
|
|
|
39
41
|
const finalSourceX = sourceX
|
|
40
42
|
const finalSourceY = sourceY
|
|
@@ -74,6 +76,37 @@ function ViewBezierConnector({
|
|
|
74
76
|
const text = (!selected && fullText.length > 30) ? `${fullText.slice(0, 30)}...` : fullText
|
|
75
77
|
const textWidth = text ? measureEdgeLabel(text, `${fontWeight} ${fontSize}px Inter, system-ui, sans-serif`) : 0
|
|
76
78
|
const padding = Array.isArray(labelBgPadding) ? labelBgPadding : [2, 4]
|
|
79
|
+
const proxyBadgeCount = typeof (edge?.data as { proxyBadgeCount?: number } | undefined)?.proxyBadgeCount === 'number'
|
|
80
|
+
? (edge?.data as { proxyBadgeCount: number }).proxyBadgeCount
|
|
81
|
+
: 0
|
|
82
|
+
const proxyBadgeDetails = ((edge?.data as { proxyBadgeDetails?: ProxyConnectorDetails | null } | undefined)?.proxyBadgeDetails) ?? null
|
|
83
|
+
const proxyBadgeText = proxyBadgeCount > 0 ? `+${proxyBadgeCount}` : ''
|
|
84
|
+
const versionChangeType = (edge?.data as { versionChangeType?: string } | undefined)?.versionChangeType
|
|
85
|
+
const versionBadgeText = versionChangeType === 'added'
|
|
86
|
+
? '+ connector'
|
|
87
|
+
: versionChangeType === 'deleted'
|
|
88
|
+
? '- connector'
|
|
89
|
+
: versionChangeType
|
|
90
|
+
? '~ connector'
|
|
91
|
+
: ''
|
|
92
|
+
const badgeFontSize = 11
|
|
93
|
+
const badgeHorizontalPadding = 7
|
|
94
|
+
const badgeSize = 24
|
|
95
|
+
const labelWidth = textWidth + padding[1] * 2
|
|
96
|
+
const versionBadgeWidth = versionBadgeText
|
|
97
|
+
? measureEdgeLabel(versionBadgeText, `700 ${badgeFontSize}px Inter, system-ui, sans-serif`) + badgeHorizontalPadding * 2
|
|
98
|
+
: 0
|
|
99
|
+
const badgeWidth = proxyBadgeText
|
|
100
|
+
? Math.max(badgeSize, measureEdgeLabel(proxyBadgeText, `600 ${badgeFontSize}px Inter, system-ui, sans-serif`) + badgeHorizontalPadding * 2)
|
|
101
|
+
: 0
|
|
102
|
+
const labelHeight = text ? fontSize + padding[0] * 2 : 0
|
|
103
|
+
const badgeGap = (text && (proxyBadgeText || versionBadgeText)) || (proxyBadgeText && versionBadgeText) ? 8 : 0
|
|
104
|
+
const stackWidth = Math.max(labelWidth, badgeWidth, versionBadgeWidth)
|
|
105
|
+
const stackHeight = labelHeight +
|
|
106
|
+
(text && (proxyBadgeText || versionBadgeText) ? badgeGap : 0) +
|
|
107
|
+
(versionBadgeText ? badgeSize : 0) +
|
|
108
|
+
(versionBadgeText && proxyBadgeText ? badgeGap : 0) +
|
|
109
|
+
(proxyBadgeText ? badgeSize : 0)
|
|
77
110
|
|
|
78
111
|
// Cubic bezier midpoint at t=0.5
|
|
79
112
|
const labelX = 0.125 * finalSourceX + 0.375 * cp1x + 0.375 * cp2x + 0.125 * finalTargetX
|
|
@@ -82,16 +115,23 @@ function ViewBezierConnector({
|
|
|
82
115
|
const labelLayout = useEdgeLabelLayout({
|
|
83
116
|
id,
|
|
84
117
|
preferredX: labelX,
|
|
85
|
-
preferredY: labelY,
|
|
86
|
-
width:
|
|
87
|
-
height: fontSize + padding[0] * 2,
|
|
118
|
+
preferredY: labelY + (stackHeight > 0 ? (stackHeight - labelHeight) / 2 : 0),
|
|
119
|
+
width: stackWidth,
|
|
120
|
+
height: stackHeight || (fontSize + padding[0] * 2),
|
|
88
121
|
dx: finalTargetX - finalSourceX,
|
|
89
122
|
dy: finalTargetY - finalSourceY,
|
|
90
123
|
})
|
|
91
124
|
|
|
92
|
-
const
|
|
93
|
-
const labelPath = text ? ` M ${labelLayout.x - labelWidth / 2},${
|
|
125
|
+
const labelCenterY = labelLayout.y - ((proxyBadgeText || versionBadgeText) ? (stackHeight - labelHeight) / 2 : 0)
|
|
126
|
+
const labelPath = text ? ` M ${labelLayout.x - labelWidth / 2},${labelCenterY} L ${labelLayout.x + labelWidth / 2},${labelCenterY}` : ''
|
|
94
127
|
const combinedInteractionPath = `${interactionPath}${labelPath}`
|
|
128
|
+
const handleBadgeClick = useCallback((event: React.MouseEvent<HTMLButtonElement>) => {
|
|
129
|
+
event.preventDefault()
|
|
130
|
+
event.stopPropagation()
|
|
131
|
+
if (!proxyBadgeDetails) return
|
|
132
|
+
const onOpenProxyBadge = (edge?.data as { onOpenProxyBadge?: (details: ProxyConnectorDetails) => void } | undefined)?.onOpenProxyBadge
|
|
133
|
+
onOpenProxyBadge?.(proxyBadgeDetails)
|
|
134
|
+
}, [edge?.data, proxyBadgeDetails])
|
|
95
135
|
|
|
96
136
|
return (
|
|
97
137
|
<>
|
|
@@ -108,7 +148,7 @@ function ViewBezierConnector({
|
|
|
108
148
|
interactionWidth={20}
|
|
109
149
|
style={{ stroke: 'transparent' }}
|
|
110
150
|
/>
|
|
111
|
-
{text && (
|
|
151
|
+
{(text || proxyBadgeText || versionBadgeText) && (
|
|
112
152
|
<EdgeLabelRenderer>
|
|
113
153
|
<div
|
|
114
154
|
style={{
|
|
@@ -117,22 +157,77 @@ function ViewBezierConnector({
|
|
|
117
157
|
pointerEvents: 'none',
|
|
118
158
|
opacity: Number(labelStyle?.opacity ?? 1),
|
|
119
159
|
zIndex: 2,
|
|
160
|
+
display: 'flex',
|
|
161
|
+
flexDirection: 'column',
|
|
162
|
+
alignItems: 'center',
|
|
163
|
+
gap: badgeGap,
|
|
120
164
|
}}
|
|
121
165
|
>
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
166
|
+
{text && (
|
|
167
|
+
<div
|
|
168
|
+
style={{
|
|
169
|
+
padding: `${padding[0]}px ${padding[1]}px`,
|
|
170
|
+
borderRadius: Array.isArray(labelBgBorderRadius) ? labelBgBorderRadius[0] : Number(labelBgBorderRadius ?? 4),
|
|
171
|
+
background: String(labelBgStyle?.fill ?? 'var(--chakra-colors-gray-900)'),
|
|
172
|
+
color: String(labelStyle?.fill ?? 'var(--accent)'),
|
|
173
|
+
fontSize,
|
|
174
|
+
fontWeight,
|
|
175
|
+
lineHeight: 1,
|
|
176
|
+
whiteSpace: 'nowrap',
|
|
177
|
+
}}
|
|
178
|
+
>
|
|
179
|
+
{text}
|
|
180
|
+
</div>
|
|
181
|
+
)}
|
|
182
|
+
{proxyBadgeText && (
|
|
183
|
+
<button
|
|
184
|
+
type="button"
|
|
185
|
+
onClick={handleBadgeClick}
|
|
186
|
+
style={{
|
|
187
|
+
minWidth: badgeWidth,
|
|
188
|
+
height: badgeSize,
|
|
189
|
+
padding: `0 ${badgeHorizontalPadding}px`,
|
|
190
|
+
borderRadius: 999,
|
|
191
|
+
background: 'var(--bg-element)',
|
|
192
|
+
border: '1px dashed rgba(var(--accent-rgb), 0.8)',
|
|
193
|
+
color: 'white',
|
|
194
|
+
display: 'flex',
|
|
195
|
+
alignItems: 'center',
|
|
196
|
+
justifyContent: 'center',
|
|
197
|
+
fontSize: badgeFontSize,
|
|
198
|
+
fontWeight: 600,
|
|
199
|
+
lineHeight: 1,
|
|
200
|
+
boxShadow: selected ? '0 0 0 1px rgba(255,255,255,0.2)' : 'none',
|
|
201
|
+
cursor: proxyBadgeDetails ? 'pointer' : 'default',
|
|
202
|
+
pointerEvents: 'auto',
|
|
203
|
+
appearance: 'none',
|
|
204
|
+
}}
|
|
205
|
+
>
|
|
206
|
+
{proxyBadgeText}
|
|
207
|
+
</button>
|
|
208
|
+
)}
|
|
209
|
+
{versionBadgeText && (
|
|
210
|
+
<div
|
|
211
|
+
style={{
|
|
212
|
+
minWidth: versionBadgeWidth,
|
|
213
|
+
height: badgeSize,
|
|
214
|
+
padding: `0 ${badgeHorizontalPadding}px`,
|
|
215
|
+
borderRadius: 999,
|
|
216
|
+
background: 'rgba(17, 24, 39, 0.9)',
|
|
217
|
+
border: `1px solid ${versionChangeType === 'added' ? '#68d391' : versionChangeType === 'deleted' ? '#fc8181' : '#f6e05e'}`,
|
|
218
|
+
color: versionChangeType === 'added' ? '#68d391' : versionChangeType === 'deleted' ? '#fc8181' : '#f6e05e',
|
|
219
|
+
display: 'flex',
|
|
220
|
+
alignItems: 'center',
|
|
221
|
+
justifyContent: 'center',
|
|
222
|
+
fontSize: badgeFontSize,
|
|
223
|
+
fontWeight: 700,
|
|
224
|
+
lineHeight: 1,
|
|
225
|
+
boxShadow: '0 6px 18px rgba(0,0,0,0.28)',
|
|
226
|
+
}}
|
|
227
|
+
>
|
|
228
|
+
{versionBadgeText}
|
|
229
|
+
</div>
|
|
230
|
+
)}
|
|
136
231
|
</div>
|
|
137
232
|
</EdgeLabelRenderer>
|
|
138
233
|
)}
|
|
@@ -43,7 +43,7 @@ interface Props {
|
|
|
43
43
|
tagColors: Record<string, Tag>
|
|
44
44
|
selectedElement?: LibraryElement | null
|
|
45
45
|
onUpdateTags?: (elementId: number, tags: string[]) => Promise<void>
|
|
46
|
-
onCreateTag: (tag: string, color?: string) => Promise<void>
|
|
46
|
+
onCreateTag: (tag: string, color?: string, description?: string) => Promise<void>
|
|
47
47
|
layers: ViewLayer[]
|
|
48
48
|
onHoverLayer: (tags: string[] | null, color?: string | null) => void
|
|
49
49
|
onCreateLayer: (name: string, tags: string[], color: string) => Promise<void>
|
|
@@ -20,6 +20,11 @@ interface ViewFloatingMenuProps {
|
|
|
20
20
|
onImport: () => void
|
|
21
21
|
onExport: () => void
|
|
22
22
|
onShare: () => void
|
|
23
|
+
canUndo?: boolean
|
|
24
|
+
canRedo?: boolean
|
|
25
|
+
undoRedoDisabled?: boolean
|
|
26
|
+
onUndo?: () => void
|
|
27
|
+
onRedo?: () => void
|
|
23
28
|
isFreePlan: boolean
|
|
24
29
|
canUpgrade?: boolean
|
|
25
30
|
activeTags?: string[]
|
|
@@ -2,7 +2,7 @@ import React, { memo } from 'react'
|
|
|
2
2
|
import type { ViewFloatingMenuSlots } from '../slots'
|
|
3
3
|
|
|
4
4
|
import {
|
|
5
|
-
HStack, Tooltip, Button, Box, Text, Popover, PopoverTrigger, Portal, PopoverContent, PopoverBody, IconButton, useDisclosure
|
|
5
|
+
HStack, Tooltip, Button, Box, Text, Popover, PopoverTrigger, Portal, PopoverContent, PopoverBody, IconButton, Slider, SliderTrack, SliderFilledTrack, SliderThumb, useDisclosure
|
|
6
6
|
} from '@chakra-ui/react'
|
|
7
7
|
import { DownloadIcon } from '@chakra-ui/icons'
|
|
8
8
|
import {
|
|
@@ -17,6 +17,7 @@ import {
|
|
|
17
17
|
TagsIcon,
|
|
18
18
|
} from './Icons'
|
|
19
19
|
import { KbdHint } from './PanelUI'
|
|
20
|
+
import { RedoSvg, UndoSvg } from './ViewDrawMenu'
|
|
20
21
|
import { useViewEditorContext } from '../pages/ViewEditor/context'
|
|
21
22
|
import type { Tag, ViewLayer } from '../types'
|
|
22
23
|
|
|
@@ -35,6 +36,13 @@ export interface ViewFloatingMenuProps extends ViewFloatingMenuSlots {
|
|
|
35
36
|
onShare?: () => void
|
|
36
37
|
focusMode: boolean
|
|
37
38
|
onFocusModeChange: (enabled: boolean) => void
|
|
39
|
+
densityLevel?: number
|
|
40
|
+
onDensityLevelChange?: (level: number) => void
|
|
41
|
+
canUndo?: boolean
|
|
42
|
+
canRedo?: boolean
|
|
43
|
+
undoRedoDisabled?: boolean
|
|
44
|
+
onUndo?: () => void
|
|
45
|
+
onRedo?: () => void
|
|
38
46
|
|
|
39
47
|
// Tag-related props
|
|
40
48
|
allTags: string[]
|
|
@@ -73,6 +81,13 @@ function ViewFloatingMenu({
|
|
|
73
81
|
onExport,
|
|
74
82
|
focusMode,
|
|
75
83
|
onFocusModeChange,
|
|
84
|
+
densityLevel = 0,
|
|
85
|
+
onDensityLevelChange,
|
|
86
|
+
canUndo = false,
|
|
87
|
+
canRedo = false,
|
|
88
|
+
undoRedoDisabled = false,
|
|
89
|
+
onUndo,
|
|
90
|
+
onRedo,
|
|
76
91
|
allTags,
|
|
77
92
|
layers,
|
|
78
93
|
tagColors,
|
|
@@ -90,6 +105,11 @@ function ViewFloatingMenu({
|
|
|
90
105
|
}: ViewFloatingMenuProps) {
|
|
91
106
|
const { canEdit } = useViewEditorContext()
|
|
92
107
|
const { isOpen: isTagsOpen, onClose: onTagsClose, onToggle: onTagsToggle } = useDisclosure()
|
|
108
|
+
const [draftDensityLevel, setDraftDensityLevel] = React.useState(densityLevel)
|
|
109
|
+
|
|
110
|
+
React.useEffect(() => {
|
|
111
|
+
setDraftDensityLevel(densityLevel)
|
|
112
|
+
}, [densityLevel])
|
|
93
113
|
|
|
94
114
|
return (
|
|
95
115
|
<HStack
|
|
@@ -131,6 +151,46 @@ function ViewFloatingMenu({
|
|
|
131
151
|
</Button>
|
|
132
152
|
</Tooltip>
|
|
133
153
|
|
|
154
|
+
{(canUndo || canRedo) && (
|
|
155
|
+
<>
|
|
156
|
+
<Box w="1px" h="16px" bg="whiteAlpha.100" flexShrink={0} mx={0.5} />
|
|
157
|
+
{canUndo && (
|
|
158
|
+
<Tooltip label="Undo" placement="top" openDelay={200}>
|
|
159
|
+
<IconButton
|
|
160
|
+
aria-label="Undo"
|
|
161
|
+
icon={<UndoSvg />}
|
|
162
|
+
variant="ghost"
|
|
163
|
+
h="28px"
|
|
164
|
+
minW="28px"
|
|
165
|
+
px={0}
|
|
166
|
+
color="gray.300"
|
|
167
|
+
isDisabled={undoRedoDisabled}
|
|
168
|
+
_disabled={{ opacity: 0.35, cursor: 'not-allowed' }}
|
|
169
|
+
_hover={{ bg: 'rgba(var(--accent-rgb), 0.12)', color: 'var(--accent)' }}
|
|
170
|
+
onClick={onUndo}
|
|
171
|
+
/>
|
|
172
|
+
</Tooltip>
|
|
173
|
+
)}
|
|
174
|
+
{canRedo && (
|
|
175
|
+
<Tooltip label="Redo" placement="top" openDelay={200}>
|
|
176
|
+
<IconButton
|
|
177
|
+
aria-label="Redo"
|
|
178
|
+
icon={<RedoSvg />}
|
|
179
|
+
variant="ghost"
|
|
180
|
+
h="28px"
|
|
181
|
+
minW="28px"
|
|
182
|
+
px={0}
|
|
183
|
+
color="gray.300"
|
|
184
|
+
isDisabled={undoRedoDisabled}
|
|
185
|
+
_disabled={{ opacity: 0.35, cursor: 'not-allowed' }}
|
|
186
|
+
_hover={{ bg: 'rgba(var(--accent-rgb), 0.12)', color: 'var(--accent)' }}
|
|
187
|
+
onClick={onRedo}
|
|
188
|
+
/>
|
|
189
|
+
</Tooltip>
|
|
190
|
+
)}
|
|
191
|
+
</>
|
|
192
|
+
)}
|
|
193
|
+
|
|
134
194
|
{!hideFocusView && (
|
|
135
195
|
<>
|
|
136
196
|
<Box w="1px" h="16px" bg="whiteAlpha.100" flexShrink={0} mx={0.5} />
|
|
@@ -265,6 +325,55 @@ function ViewFloatingMenu({
|
|
|
265
325
|
</>
|
|
266
326
|
)}
|
|
267
327
|
|
|
328
|
+
{onDensityLevelChange && (
|
|
329
|
+
<>
|
|
330
|
+
<Box w="1px" h="16px" bg="whiteAlpha.100" flexShrink={0} mx={0.5} />
|
|
331
|
+
<Tooltip label={`Density ${draftDensityLevel}`} placement="top" openDelay={200}>
|
|
332
|
+
<Box
|
|
333
|
+
w="92px"
|
|
334
|
+
h="28px"
|
|
335
|
+
px={2.5}
|
|
336
|
+
display="flex"
|
|
337
|
+
alignItems="center"
|
|
338
|
+
bg="whiteAlpha.50"
|
|
339
|
+
rounded="md"
|
|
340
|
+
>
|
|
341
|
+
<Slider
|
|
342
|
+
aria-label="Density"
|
|
343
|
+
min={-2}
|
|
344
|
+
max={2}
|
|
345
|
+
step={1}
|
|
346
|
+
value={draftDensityLevel}
|
|
347
|
+
onChange={setDraftDensityLevel}
|
|
348
|
+
onChangeEnd={(value) => {
|
|
349
|
+
setDraftDensityLevel(value)
|
|
350
|
+
onDensityLevelChange(value)
|
|
351
|
+
}}
|
|
352
|
+
focusThumbOnChange={false}
|
|
353
|
+
>
|
|
354
|
+
<SliderTrack h="3px" bg="whiteAlpha.200">
|
|
355
|
+
<SliderFilledTrack bg="var(--accent)" />
|
|
356
|
+
</SliderTrack>
|
|
357
|
+
{[-2, -1, 0, 1, 2].map((value) => (
|
|
358
|
+
<Box
|
|
359
|
+
key={value}
|
|
360
|
+
position="absolute"
|
|
361
|
+
left={`${((value + 2) / 4) * 100}%`}
|
|
362
|
+
top="50%"
|
|
363
|
+
transform="translate(-50%, -50%)"
|
|
364
|
+
w="1px"
|
|
365
|
+
h="9px"
|
|
366
|
+
bg={draftDensityLevel >= value ? 'var(--accent)' : 'whiteAlpha.400'}
|
|
367
|
+
pointerEvents="none"
|
|
368
|
+
/>
|
|
369
|
+
))}
|
|
370
|
+
<SliderThumb boxSize="12px" bg="white" border="2px solid" borderColor="var(--accent)" />
|
|
371
|
+
</Slider>
|
|
372
|
+
</Box>
|
|
373
|
+
</Tooltip>
|
|
374
|
+
</>
|
|
375
|
+
)}
|
|
376
|
+
|
|
268
377
|
{/* Draw mode toggle */}
|
|
269
378
|
<Tooltip
|
|
270
379
|
label={drawingMode ? 'Exit drawing mode' : 'Draw on diagram'}
|
|
@@ -43,6 +43,9 @@ export interface ViewGridNodeData {
|
|
|
43
43
|
name: string
|
|
44
44
|
level_label: string | null
|
|
45
45
|
counts?: { nodes: number; edges: number }
|
|
46
|
+
kind?: 'view' | 'cluster'
|
|
47
|
+
collapsedCount?: number
|
|
48
|
+
dimmed?: boolean
|
|
46
49
|
focused: boolean
|
|
47
50
|
canEdit: boolean
|
|
48
51
|
isEditing: boolean
|
|
@@ -72,6 +75,7 @@ export default function ViewGridNode({ data }: { data: ViewGridNodeData }) {
|
|
|
72
75
|
|
|
73
76
|
const { isOpen: isMenuOpen, onOpen: onMenuOpen, onClose: onMenuClose } = useDisclosure()
|
|
74
77
|
const [isTooltipOpen, setIsTooltipOpen] = useState(false)
|
|
78
|
+
const isCluster = data.kind === 'cluster'
|
|
75
79
|
|
|
76
80
|
useEffect(() => {
|
|
77
81
|
if (!isMenuOpen && !isTooltipOpen) return
|
|
@@ -114,6 +118,7 @@ export default function ViewGridNode({ data }: { data: ViewGridNodeData }) {
|
|
|
114
118
|
}, [])
|
|
115
119
|
|
|
116
120
|
useEffect(() => {
|
|
121
|
+
if (isCluster) return
|
|
117
122
|
if (!hasRequested) return
|
|
118
123
|
|
|
119
124
|
let active = true
|
|
@@ -141,13 +146,15 @@ export default function ViewGridNode({ data }: { data: ViewGridNodeData }) {
|
|
|
141
146
|
URL.revokeObjectURL(url)
|
|
142
147
|
}
|
|
143
148
|
}
|
|
144
|
-
}, [hasRequested, data.id])
|
|
149
|
+
}, [hasRequested, data.id, isCluster])
|
|
145
150
|
|
|
146
151
|
const borderColor = data.focused ? accent : 'rgba(255,255,255,0.14)'
|
|
147
152
|
|
|
148
153
|
const boxShadow = data.focused
|
|
149
154
|
? `0 0 24px ${hexToRgba(accent, 0.4)}`
|
|
150
|
-
:
|
|
155
|
+
: isCluster
|
|
156
|
+
? '0 14px 34px rgba(0,0,0,0.42), inset 0 1px 0 rgba(255,255,255,0.05)'
|
|
157
|
+
: '0 8px 24px rgba(0,0,0,0.4), inset 0 1px 0 rgba(255,255,255,0.05)'
|
|
151
158
|
|
|
152
159
|
return (
|
|
153
160
|
// Outer container: sizing + group context, overflow visible for the "New Child" hover button
|
|
@@ -160,7 +167,26 @@ export default function ViewGridNode({ data }: { data: ViewGridNodeData }) {
|
|
|
160
167
|
h="150px"
|
|
161
168
|
position="relative"
|
|
162
169
|
userSelect="none"
|
|
170
|
+
opacity={data.dimmed ? 0.5 : 1}
|
|
163
171
|
transition="opacity 0.3s cubic-bezier(0.16, 1, 0.3, 1)"
|
|
172
|
+
_before={isCluster ? {
|
|
173
|
+
content: '""',
|
|
174
|
+
position: 'absolute',
|
|
175
|
+
inset: '8px -9px -8px 9px',
|
|
176
|
+
borderRadius: '12px',
|
|
177
|
+
border: '1px solid rgba(255,255,255,0.08)',
|
|
178
|
+
bg: 'rgba(var(--bg-element-rgb), 0.55)',
|
|
179
|
+
boxShadow: '0 8px 20px rgba(0,0,0,0.28)',
|
|
180
|
+
} : undefined}
|
|
181
|
+
_after={isCluster ? {
|
|
182
|
+
content: '""',
|
|
183
|
+
position: 'absolute',
|
|
184
|
+
inset: '16px -18px -16px 18px',
|
|
185
|
+
borderRadius: '12px',
|
|
186
|
+
border: '1px solid rgba(255,255,255,0.06)',
|
|
187
|
+
bg: 'rgba(var(--bg-element-rgb), 0.35)',
|
|
188
|
+
boxShadow: '0 8px 20px rgba(0,0,0,0.2)',
|
|
189
|
+
} : undefined}
|
|
164
190
|
>
|
|
165
191
|
<Handle
|
|
166
192
|
type="target"
|
|
@@ -212,9 +238,32 @@ export default function ViewGridNode({ data }: { data: ViewGridNodeData }) {
|
|
|
212
238
|
overflow="hidden"
|
|
213
239
|
borderRadius="8px 8px 0 0"
|
|
214
240
|
flexShrink={0}
|
|
215
|
-
bg=
|
|
241
|
+
bg={isCluster ? 'rgba(var(--bg-element-rgb), 0.88)' : 'var(--bg-card-solid)'}
|
|
216
242
|
>
|
|
217
|
-
{
|
|
243
|
+
{isCluster ? (
|
|
244
|
+
<Flex
|
|
245
|
+
position="absolute"
|
|
246
|
+
inset={0}
|
|
247
|
+
p={3}
|
|
248
|
+
gap={1.5}
|
|
249
|
+
align="flex-start"
|
|
250
|
+
justify="flex-start"
|
|
251
|
+
wrap="wrap"
|
|
252
|
+
bg="radial-gradient(circle at 80% 18%, rgba(var(--accent-rgb), 0.16), transparent 42px), linear-gradient(135deg, rgba(255,255,255,0.04), rgba(255,255,255,0.01))"
|
|
253
|
+
>
|
|
254
|
+
{Array.from({ length: Math.min(18, Math.max(6, data.collapsedCount ?? 6)) }).map((_, i) => (
|
|
255
|
+
<Box
|
|
256
|
+
key={i}
|
|
257
|
+
w={`${18 + (i % 4) * 6}px`}
|
|
258
|
+
h="14px"
|
|
259
|
+
borderRadius="5px"
|
|
260
|
+
bg={i % 5 === 0 ? hexToRgba(accent, 0.2) : 'rgba(255,255,255,0.06)'}
|
|
261
|
+
border="1px solid"
|
|
262
|
+
borderColor={i % 5 === 0 ? hexToRgba(accent, 0.34) : 'rgba(255,255,255,0.07)'}
|
|
263
|
+
/>
|
|
264
|
+
))}
|
|
265
|
+
</Flex>
|
|
266
|
+
) : thumbnailUrl ? (
|
|
218
267
|
<Box
|
|
219
268
|
as="img"
|
|
220
269
|
src={thumbnailUrl}
|
|
@@ -294,7 +343,7 @@ export default function ViewGridNode({ data }: { data: ViewGridNodeData }) {
|
|
|
294
343
|
</Text>
|
|
295
344
|
)}
|
|
296
345
|
|
|
297
|
-
{!data.isEditing && (
|
|
346
|
+
{!data.isEditing && !isCluster && (
|
|
298
347
|
<Flex align="center" gap={1} onClick={(e) => e.stopPropagation()} mt="-2px">
|
|
299
348
|
<Menu
|
|
300
349
|
isLazy
|
|
@@ -404,9 +453,11 @@ export default function ViewGridNode({ data }: { data: ViewGridNodeData }) {
|
|
|
404
453
|
flexShrink={0}
|
|
405
454
|
textShadow="0 1px 2px rgba(0,0,0,0.5)"
|
|
406
455
|
>
|
|
407
|
-
{data.
|
|
408
|
-
? `${data.
|
|
409
|
-
:
|
|
456
|
+
{isCluster && data.collapsedCount
|
|
457
|
+
? `${data.collapsedCount} views`
|
|
458
|
+
: data.counts
|
|
459
|
+
? `${data.counts.nodes}n · ${data.counts.edges}e`
|
|
460
|
+
: '-'}
|
|
410
461
|
</Text>
|
|
411
462
|
</Flex>
|
|
412
463
|
</Flex>
|
|
@@ -28,6 +28,7 @@ interface Props {
|
|
|
28
28
|
view: ViewTreeNode | null
|
|
29
29
|
canEdit?: boolean
|
|
30
30
|
onSave: (updated: ViewTreeNode) => void
|
|
31
|
+
onUnsupportedMutation?: () => void
|
|
31
32
|
hasBackdrop?: boolean
|
|
32
33
|
}
|
|
33
34
|
|
|
@@ -37,7 +38,7 @@ interface Props {
|
|
|
37
38
|
* Location: Right side of the screen on desktop. Overlays screen on mobile.
|
|
38
39
|
* Aliases: View Properties, View Settings.
|
|
39
40
|
*/
|
|
40
|
-
function ViewPanel({ isOpen, onClose, view, canEdit: canEditProp, onSave, hasBackdrop = true }: Props) {
|
|
41
|
+
function ViewPanel({ isOpen, onClose, view, canEdit: canEditProp, onSave, onUnsupportedMutation, hasBackdrop = true }: Props) {
|
|
41
42
|
const ctx = useContext(ViewEditorContext)
|
|
42
43
|
const canEdit = canEditProp ?? ctx?.canEdit ?? true
|
|
43
44
|
const isReadOnly = !canEdit
|
|
@@ -118,7 +119,7 @@ function ViewPanel({ isOpen, onClose, view, canEdit: canEditProp, onSave, hasBac
|
|
|
118
119
|
rows={4}
|
|
119
120
|
/>
|
|
120
121
|
</FormControl>
|
|
121
|
-
<LayoutSection view={view} canEdit={canEdit} />
|
|
122
|
+
<LayoutSection view={view} canEdit={canEdit} onUnsupportedMutation={onUnsupportedMutation} />
|
|
122
123
|
|
|
123
124
|
{view && (
|
|
124
125
|
<Box pt={2} borderTop="1px solid" borderColor="whiteAlpha.50">
|