@tldiagram/core-ui 1.95.0 → 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.
Files changed (102) hide show
  1. package/dist/api/client.d.ts +184 -3
  2. package/dist/components/ConnectorPanel.d.ts +5 -1
  3. package/dist/components/CrossBranchControls.d.ts +4 -3
  4. package/dist/components/ElementNode.d.ts +5 -0
  5. package/dist/components/ElementPanel.d.ts +6 -1
  6. package/dist/components/LayoutSection.d.ts +2 -1
  7. package/dist/components/MergeDialog.d.ts +16 -0
  8. package/dist/components/MiniZoomOnboarding.d.ts +2 -1
  9. package/dist/components/NodeContainer.d.ts +2 -0
  10. package/dist/components/ProxyConnectorPanel.d.ts +4 -1
  11. package/dist/components/ViewExplorer/index.d.ts +1 -1
  12. package/dist/components/ViewFloatingMenu-vscode.d.ts +5 -0
  13. package/dist/components/ViewFloatingMenu.d.ts +8 -1
  14. package/dist/components/ViewGridNode.d.ts +3 -0
  15. package/dist/components/ViewPanel.d.ts +2 -1
  16. package/dist/components/WorkspacePanel.d.ts +2 -0
  17. package/dist/components/ZUI/ZUICanvas.d.ts +5 -0
  18. package/dist/components/ZUI/focus.d.ts +32 -0
  19. package/dist/components/ZUI/focus.test.d.ts +1 -0
  20. package/dist/components/ZUI/layout.d.ts +2 -2
  21. package/dist/components/ZUI/proxy.d.ts +20 -4
  22. package/dist/components/ZUI/renderer.d.ts +35 -1
  23. package/dist/components/ZUI/types.d.ts +6 -0
  24. package/dist/components/ZUI/useZUIInteraction.d.ts +1 -0
  25. package/dist/context/WorkspaceVersionContext.d.ts +49 -0
  26. package/dist/crossBranch/resolve.d.ts +39 -2
  27. package/dist/crossBranch/resolve.test.d.ts +1 -0
  28. package/dist/crossBranch/settings.d.ts +6 -1
  29. package/dist/crossBranch/types.d.ts +8 -0
  30. package/dist/hooks/useElementSearch.d.ts +8 -0
  31. package/dist/index.d.ts +1 -0
  32. package/dist/index.js +14597 -12083
  33. package/dist/pages/InfiniteZoom.d.ts +1 -0
  34. package/dist/pages/ViewEditor/hooks/useCanvasInteractions.d.ts +6 -1
  35. package/dist/pages/ViewEditor/hooks/useViewContextNeighbours.d.ts +2 -0
  36. package/dist/pages/ViewEditor/hooks/useViewData.d.ts +4 -2
  37. package/dist/pages/ViewEditor/hooks/useViewEditHistory.d.ts +13 -0
  38. package/dist/pages/viewsJumpSearch.d.ts +22 -0
  39. package/dist/pages/viewsJumpSearch.test.d.ts +1 -0
  40. package/dist/store/useStore.d.ts +3 -0
  41. package/dist/types/index.d.ts +9 -0
  42. package/dist/utils/elementIcon.d.ts +2 -0
  43. package/dist/utils/elementIcon.test.d.ts +1 -0
  44. package/dist/utils/sourceEditor.d.ts +7 -0
  45. package/dist/utils/watchDiffSummary.d.ts +34 -0
  46. package/package.json +2 -2
  47. package/src/App.tsx +12 -8
  48. package/src/api/client.ts +488 -26
  49. package/src/components/CodePreviewPanel.tsx +90 -16
  50. package/src/components/ConnectorPanel.tsx +34 -3
  51. package/src/components/ContextNeighborElement.tsx +2 -5
  52. package/src/components/CrossBranchControls.tsx +46 -17
  53. package/src/components/ElementNode.tsx +98 -47
  54. package/src/components/ElementPanel.tsx +62 -25
  55. package/src/components/InlineElementAdder.tsx +8 -3
  56. package/src/components/LayoutSection.tsx +4 -1
  57. package/src/components/MergeDialog.tsx +269 -0
  58. package/src/components/MiniZoomOnboarding.tsx +29 -22
  59. package/src/components/NodeContainer.tsx +55 -17
  60. package/src/components/ProxyConnectorPanel.tsx +58 -16
  61. package/src/components/ViewBezierConnector.tsx +116 -21
  62. package/src/components/ViewExplorer/index.tsx +1 -1
  63. package/src/components/ViewFloatingMenu-vscode.tsx +5 -0
  64. package/src/components/ViewFloatingMenu.tsx +110 -1
  65. package/src/components/ViewGridNode.tsx +59 -8
  66. package/src/components/ViewPanel.tsx +3 -2
  67. package/src/components/WorkspacePanel.tsx +938 -0
  68. package/src/components/ZUI/ZUICanvas.tsx +226 -127
  69. package/src/components/ZUI/focus.test.ts +534 -0
  70. package/src/components/ZUI/focus.ts +293 -0
  71. package/src/components/ZUI/layout.ts +7 -11
  72. package/src/components/ZUI/proxy.ts +470 -114
  73. package/src/components/ZUI/renderer.ts +510 -134
  74. package/src/components/ZUI/types.ts +6 -0
  75. package/src/components/ZUI/useZUIInteraction.ts +66 -29
  76. package/src/context/WorkspaceVersionContext.tsx +126 -0
  77. package/src/crossBranch/resolve.test.ts +342 -0
  78. package/src/crossBranch/resolve.ts +368 -68
  79. package/src/crossBranch/settings.ts +49 -3
  80. package/src/crossBranch/types.ts +9 -0
  81. package/src/hooks/useElementSearch.ts +45 -0
  82. package/src/index.css +11 -0
  83. package/src/index.ts +7 -0
  84. package/src/pages/AppearanceSettings.tsx +24 -1
  85. package/src/pages/Dependencies.tsx +231 -65
  86. package/src/pages/InfiniteZoom.tsx +76 -27
  87. package/src/pages/Settings.tsx +1 -1
  88. package/src/pages/ViewEditor/hooks/useCanvasInteractions.ts +103 -24
  89. package/src/pages/ViewEditor/hooks/useViewContextNeighbours.ts +102 -6
  90. package/src/pages/ViewEditor/hooks/useViewData.ts +42 -26
  91. package/src/pages/ViewEditor/hooks/useViewEditHistory.ts +62 -0
  92. package/src/pages/ViewEditor/index.tsx +549 -59
  93. package/src/pages/Views.tsx +112 -41
  94. package/src/pages/ViewsGrid.tsx +332 -113
  95. package/src/pages/viewsJumpSearch.test.ts +193 -0
  96. package/src/pages/viewsJumpSearch.ts +111 -0
  97. package/src/store/useStore.ts +58 -0
  98. package/src/types/index.ts +10 -0
  99. package/src/utils/elementIcon.test.ts +28 -0
  100. package/src/utils/elementIcon.ts +20 -0
  101. package/src/utils/sourceEditor.ts +46 -0
  102. package/src/utils/watchDiffSummary.ts +159 -0
@@ -8,6 +8,8 @@ export interface ElementContainerProps extends BoxProps {
8
8
  isSource?: boolean
9
9
  isTarget?: boolean
10
10
  isConnectorHighlighted?: boolean
11
+ hasStack?: boolean
12
+ kind?: string | null
11
13
  }
12
14
 
13
15
  export const ElementContainer = memo(forwardRef<ElementContainerProps, 'div'>(({
@@ -15,19 +17,24 @@ export const ElementContainer = memo(forwardRef<ElementContainerProps, 'div'>(({
15
17
  isSource,
16
18
  isTarget,
17
19
  isConnectorHighlighted,
20
+ hasStack,
21
+ kind: _kind,
18
22
  children,
19
23
  ...props
20
24
  }, ref) => {
21
25
  const { accent } = useAccentColor()
22
26
 
27
+ const brandedBorder = hexToRgba('#a0aec0', 0.5)
28
+
23
29
  const borderColor = isSource
24
30
  ? accent
25
31
  : isTarget
26
32
  ? 'teal.300'
27
33
  : isSelected || isConnectorHighlighted
28
34
  ? accent
29
- : 'gray.600'
35
+ : brandedBorder
30
36
 
37
+ // Shadows matching ZUICanvas / high-fidelity look
31
38
  const selectionShadow = `0 0 0 3px ${hexToRgba(accent, 0.35)}, 0 10px 36px rgba(0,0,0,0.55), 0 3px 10px rgba(0,0,0,0.4)`
32
39
  const sourceShadow = `0 0 0 3px ${hexToRgba(accent, 0.55)}, 0 0 24px ${hexToRgba(accent, 0.25)}`
33
40
  const edgeHighlightShadow = `0 0 0 2px ${hexToRgba(accent, 0.2)}, 0 8px 32px rgba(0,0,0,0.55), 0 2px 8px rgba(0,0,0,0.35)`
@@ -36,23 +43,54 @@ export const ElementContainer = memo(forwardRef<ElementContainerProps, 'div'>(({
36
43
 
37
44
  const boxShadow = isSource ? sourceShadow : isSelected ? selectionShadow : isConnectorHighlighted ? edgeHighlightShadow : restingShadow
38
45
 
46
+ const finalBorderColor = borderColor
47
+
39
48
  return (
40
- <Box
41
- ref={ref}
42
- bg="var(--bg-element)"
43
- borderColor={borderColor}
44
- borderWidth="1px"
45
- rounded="lg"
46
- boxShadow={boxShadow}
47
- transition="all var(--chakra-transitions-duration-fast) var(--chakra-transitions-easing-pop)"
48
- position="relative"
49
- _hover={{
50
- borderColor: isSource ? accent : isTarget ? 'teal.200' : accent,
51
- boxShadow: hoverShadow,
52
- }}
53
- {...props}
54
- >
55
- {children}
49
+ <Box position="relative" zIndex={1}>
50
+ {hasStack && (
51
+ <>
52
+ {/* Stack effect matching ZUI renderer.ts (offset 4px and 8px) */}
53
+ <Box
54
+ position="absolute"
55
+ inset={0}
56
+ transform="translate(8px, 8px)"
57
+ bg="var(--bg-element)"
58
+ borderColor={finalBorderColor}
59
+ borderWidth="1px"
60
+ rounded="lg"
61
+ opacity={0.4}
62
+ zIndex={-2}
63
+ />
64
+ <Box
65
+ position="absolute"
66
+ inset={0}
67
+ transform="translate(4px, 4px)"
68
+ bg="var(--bg-element)"
69
+ borderColor={finalBorderColor}
70
+ borderWidth="1px"
71
+ rounded="lg"
72
+ opacity={0.7}
73
+ zIndex={-1}
74
+ />
75
+ </>
76
+ )}
77
+ <Box
78
+ ref={ref}
79
+ bg="var(--bg-element)"
80
+ borderColor={finalBorderColor}
81
+ borderWidth="1px"
82
+ rounded="lg"
83
+ boxShadow={boxShadow}
84
+ transition="all var(--chakra-transitions-duration-fast) var(--chakra-transitions-easing-pop)"
85
+ position="relative"
86
+ _hover={{
87
+ borderColor: isSource ? accent : isTarget ? 'teal.200' : accent,
88
+ boxShadow: hoverShadow,
89
+ }}
90
+ {...props}
91
+ >
92
+ {children}
93
+ </Box>
56
94
  </Box>
57
95
  )
58
96
  }))
@@ -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 spacing={2} mb={1}>
75
- <Text color="white" fontSize="sm" fontWeight="semibold" isTruncated>
76
- {leaf.source.actualElementName}
77
- </Text>
78
- <Icon as={ChevronRightIcon} color="whiteAlpha.400" />
79
- <Text color="white" fontSize="sm" fontWeight="semibold" isTruncated>
80
- {leaf.target.actualElementName}
81
- </Text>
82
- </HStack>
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
- {(leaf.connector.label || leaf.connector.relationship) && (
85
- <Text color="gray.400" fontSize="xs" fontStyle={!leaf.connector.label ? 'italic' : 'normal'}>
86
- {leaf.connector.label || leaf.connector.relationship}
87
- </Text>
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: textWidth + padding[1] * 2,
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 labelWidth = textWidth + padding[1] * 2
93
- const labelPath = text ? ` M ${labelLayout.x - labelWidth / 2},${labelLayout.y} L ${labelLayout.x + labelWidth / 2},${labelLayout.y}` : ''
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
- <div
123
- style={{
124
- padding: `${padding[0]}px ${padding[1]}px`,
125
- borderRadius: Array.isArray(labelBgBorderRadius) ? labelBgBorderRadius[0] : Number(labelBgBorderRadius ?? 4),
126
- background: String(labelBgStyle?.fill ?? 'var(--chakra-colors-gray-900)'),
127
- color: String(labelStyle?.fill ?? 'var(--accent)'),
128
- fontSize,
129
- fontWeight,
130
- lineHeight: 1,
131
- whiteSpace: 'nowrap',
132
- }}
133
- >
134
- {text}
135
- </div>
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'}