@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.
Files changed (100) 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/NodeContainer.d.ts +2 -0
  9. package/dist/components/ProxyConnectorPanel.d.ts +4 -1
  10. package/dist/components/ViewExplorer/index.d.ts +1 -1
  11. package/dist/components/ViewFloatingMenu-vscode.d.ts +5 -0
  12. package/dist/components/ViewFloatingMenu.d.ts +8 -1
  13. package/dist/components/ViewGridNode.d.ts +3 -0
  14. package/dist/components/ViewPanel.d.ts +2 -1
  15. package/dist/components/WorkspacePanel.d.ts +2 -0
  16. package/dist/components/ZUI/ZUICanvas.d.ts +4 -0
  17. package/dist/components/ZUI/focus.d.ts +32 -0
  18. package/dist/components/ZUI/focus.test.d.ts +1 -0
  19. package/dist/components/ZUI/layout.d.ts +2 -2
  20. package/dist/components/ZUI/proxy.d.ts +20 -4
  21. package/dist/components/ZUI/renderer.d.ts +35 -1
  22. package/dist/components/ZUI/types.d.ts +6 -0
  23. package/dist/components/ZUI/useZUIInteraction.d.ts +1 -0
  24. package/dist/context/WorkspaceVersionContext.d.ts +49 -0
  25. package/dist/crossBranch/resolve.d.ts +39 -2
  26. package/dist/crossBranch/resolve.test.d.ts +1 -0
  27. package/dist/crossBranch/settings.d.ts +6 -1
  28. package/dist/crossBranch/types.d.ts +8 -0
  29. package/dist/hooks/useElementSearch.d.ts +8 -0
  30. package/dist/index.d.ts +1 -0
  31. package/dist/index.js +16529 -14030
  32. package/dist/pages/InfiniteZoom.d.ts +1 -0
  33. package/dist/pages/ViewEditor/hooks/useCanvasInteractions.d.ts +6 -1
  34. package/dist/pages/ViewEditor/hooks/useViewContextNeighbours.d.ts +2 -0
  35. package/dist/pages/ViewEditor/hooks/useViewData.d.ts +4 -2
  36. package/dist/pages/ViewEditor/hooks/useViewEditHistory.d.ts +13 -0
  37. package/dist/pages/viewsJumpSearch.d.ts +22 -0
  38. package/dist/pages/viewsJumpSearch.test.d.ts +1 -0
  39. package/dist/store/useStore.d.ts +3 -0
  40. package/dist/types/index.d.ts +9 -0
  41. package/dist/utils/elementIcon.d.ts +2 -0
  42. package/dist/utils/elementIcon.test.d.ts +1 -0
  43. package/dist/utils/sourceEditor.d.ts +7 -0
  44. package/dist/utils/watchDiffSummary.d.ts +34 -0
  45. package/package.json +2 -2
  46. package/src/App.tsx +12 -8
  47. package/src/api/client.ts +488 -26
  48. package/src/components/CodePreviewPanel.tsx +90 -16
  49. package/src/components/ConnectorPanel.tsx +34 -3
  50. package/src/components/ContextNeighborElement.tsx +2 -5
  51. package/src/components/CrossBranchControls.tsx +46 -17
  52. package/src/components/ElementNode.tsx +98 -47
  53. package/src/components/ElementPanel.tsx +62 -25
  54. package/src/components/InlineElementAdder.tsx +8 -3
  55. package/src/components/LayoutSection.tsx +4 -1
  56. package/src/components/MergeDialog.tsx +269 -0
  57. package/src/components/NodeContainer.tsx +55 -17
  58. package/src/components/ProxyConnectorPanel.tsx +58 -16
  59. package/src/components/ViewBezierConnector.tsx +116 -21
  60. package/src/components/ViewExplorer/index.tsx +1 -1
  61. package/src/components/ViewFloatingMenu-vscode.tsx +5 -0
  62. package/src/components/ViewFloatingMenu.tsx +110 -1
  63. package/src/components/ViewGridNode.tsx +59 -8
  64. package/src/components/ViewPanel.tsx +3 -2
  65. package/src/components/WorkspacePanel.tsx +938 -0
  66. package/src/components/ZUI/ZUICanvas.tsx +216 -122
  67. package/src/components/ZUI/focus.test.ts +534 -0
  68. package/src/components/ZUI/focus.ts +293 -0
  69. package/src/components/ZUI/layout.ts +7 -11
  70. package/src/components/ZUI/proxy.ts +470 -114
  71. package/src/components/ZUI/renderer.ts +510 -134
  72. package/src/components/ZUI/types.ts +6 -0
  73. package/src/components/ZUI/useZUIInteraction.ts +66 -29
  74. package/src/context/WorkspaceVersionContext.tsx +126 -0
  75. package/src/crossBranch/resolve.test.ts +342 -0
  76. package/src/crossBranch/resolve.ts +368 -68
  77. package/src/crossBranch/settings.ts +49 -3
  78. package/src/crossBranch/types.ts +9 -0
  79. package/src/hooks/useElementSearch.ts +45 -0
  80. package/src/index.css +11 -0
  81. package/src/index.ts +7 -0
  82. package/src/pages/AppearanceSettings.tsx +24 -1
  83. package/src/pages/Dependencies.tsx +231 -65
  84. package/src/pages/InfiniteZoom.tsx +41 -19
  85. package/src/pages/Settings.tsx +1 -1
  86. package/src/pages/ViewEditor/hooks/useCanvasInteractions.ts +103 -24
  87. package/src/pages/ViewEditor/hooks/useViewContextNeighbours.ts +102 -6
  88. package/src/pages/ViewEditor/hooks/useViewData.ts +42 -26
  89. package/src/pages/ViewEditor/hooks/useViewEditHistory.ts +62 -0
  90. package/src/pages/ViewEditor/index.tsx +549 -59
  91. package/src/pages/Views.tsx +112 -41
  92. package/src/pages/ViewsGrid.tsx +332 -113
  93. package/src/pages/viewsJumpSearch.test.ts +193 -0
  94. package/src/pages/viewsJumpSearch.ts +111 -0
  95. package/src/store/useStore.ts +58 -0
  96. package/src/types/index.ts +10 -0
  97. package/src/utils/elementIcon.test.ts +28 -0
  98. package/src/utils/elementIcon.ts +20 -0
  99. package/src/utils/sourceEditor.ts +46 -0
  100. package/src/utils/watchDiffSummary.ts +159 -0
@@ -2,6 +2,24 @@ import { useEffect, useState, useRef } from 'react'
2
2
  import type { SVGProps } from 'react'
3
3
  import { Box, Button, CloseButton, HStack, Icon, Spinner, Text, Tooltip, VStack } from '@chakra-ui/react'
4
4
  import { ExternalLinkIcon } from '@chakra-ui/icons'
5
+ import CodeMirror, { ReactCodeMirrorRef } from '@uiw/react-codemirror'
6
+ import { EditorView } from '@codemirror/view'
7
+ import { oneDark } from '@codemirror/theme-one-dark'
8
+ import { javascript } from '@codemirror/lang-javascript'
9
+ import { python } from '@codemirror/lang-python'
10
+ import { cpp } from '@codemirror/lang-cpp'
11
+ import { java } from '@codemirror/lang-java'
12
+ import { rust } from '@codemirror/lang-rust'
13
+
14
+ import SlidingPanel from './SlidingPanel'
15
+ import { api } from '../api/client'
16
+ import { findSymbolByName, getParser, detectLanguage, type SupportedLanguage } from '../utils/treesitter'
17
+ import { githubCache } from '../utils/githubCache'
18
+ import { getGithubRepoVisibility } from '../utils/githubApi'
19
+ import { parseRepoSlug } from '../utils/url'
20
+ import { useSourceEditor } from '../utils/sourceEditor'
21
+ import { toast } from '../utils/toast'
22
+ import type { PlacedElement } from '../types'
5
23
 
6
24
  const GithubIcon = (props: SVGProps<SVGSVGElement>) => (
7
25
  <svg
@@ -34,18 +52,6 @@ const customCodeTheme = EditorView.theme({
34
52
  caretColor: "var(--chakra-colors-blue-400)",
35
53
  }
36
54
  }, { dark: true })
37
- import CodeMirror, { ReactCodeMirrorRef } from '@uiw/react-codemirror'
38
- import { EditorView } from '@codemirror/view'
39
- import { oneDark } from '@codemirror/theme-one-dark'
40
- import { javascript } from '@codemirror/lang-javascript'
41
- import { python } from '@codemirror/lang-python'
42
- import { cpp } from '@codemirror/lang-cpp'
43
- import { java } from '@codemirror/lang-java'
44
- import { rust } from '@codemirror/lang-rust'
45
-
46
- import SlidingPanel from './SlidingPanel'
47
- import { findSymbolByName, getParser, detectLanguage, type SupportedLanguage } from '../utils/treesitter'
48
- import type { PlacedElement } from '../types'
49
55
 
50
56
  interface Props {
51
57
  isOpen: boolean
@@ -69,9 +75,15 @@ function parseAnchor(anchorStr: string):
69
75
  return { kind: 'none' }
70
76
  }
71
77
 
72
- import { githubCache } from '../utils/githubCache'
73
- import { getGithubRepoVisibility } from '../utils/githubApi'
74
- import { parseRepoSlug } from '../utils/url'
78
+ function inferLineFromDescription(description: string | null | undefined, basePath: string): number | null {
79
+ if (!description || !basePath) return null
80
+ const match = description.match(/:(\d+)(?::\d+)?$/)
81
+ if (!match) return null
82
+ const pathPart = description.slice(0, match.index)
83
+ if (pathPart && pathPart !== basePath) return null
84
+ const line = Number(match[1])
85
+ return Number.isFinite(line) && line > 0 ? line : null
86
+ }
75
87
 
76
88
  export default function CodePreviewPanel({ isOpen, onClose, element, hasBackdrop = true }: Props) {
77
89
  const [code, setCode] = useState('')
@@ -80,6 +92,8 @@ export default function CodePreviewPanel({ isOpen, onClose, element, hasBackdrop
80
92
  const [resolvedStartLine, setResolvedStartLine] = useState<number | null>(null)
81
93
  const [resolvedEndLine, setResolvedEndLine] = useState<number | null>(null)
82
94
  const [isPrivateRepo, setIsPrivateRepo] = useState(false)
95
+ const [openingEditor, setOpeningEditor] = useState(false)
96
+ const { editor: sourceEditor } = useSourceEditor()
83
97
 
84
98
  const editorRef = useRef<ReactCodeMirrorRef>(null)
85
99
 
@@ -88,6 +102,10 @@ export default function CodePreviewPanel({ isOpen, onClose, element, hasBackdrop
88
102
  const basePath = hashIdx >= 0 ? filePath.slice(0, hashIdx) : filePath
89
103
  const symbolInfoStr = hashIdx >= 0 ? filePath.slice(hashIdx + 1) : ''
90
104
  const repoSlug = element?.repo ? parseRepoSlug(element.repo) : ''
105
+ const anchor = parseAnchor(symbolInfoStr)
106
+ const anchorStartLine = anchor.kind === 'lines' ? anchor.startLine : null
107
+ const fallbackStartLine = inferLineFromDescription(element?.description, basePath)
108
+ const editorStartLine = resolvedStartLine ?? anchorStartLine ?? fallbackStartLine
91
109
 
92
110
  useEffect(() => {
93
111
  if (!isOpen || !element || !repoSlug || !basePath) return
@@ -205,9 +223,31 @@ export default function CodePreviewPanel({ isOpen, onClose, element, hasBackdrop
205
223
 
206
224
  const githubUrl = element?.repo && basePath
207
225
  ? `https://github.com/${repoSlug}/blob/${element.branch || 'main'}/${basePath}`
208
- + (resolvedStartLine ? `#L${resolvedStartLine}-L${resolvedEndLine ?? resolvedStartLine}` : '')
226
+ + (editorStartLine ? `#L${editorStartLine}-L${resolvedEndLine ?? editorStartLine}` : '')
209
227
  : null
210
228
 
229
+ const handleOpenInEditor = async () => {
230
+ if (!basePath) return
231
+ setOpeningEditor(true)
232
+ try {
233
+ await api.editor.open({
234
+ editor: sourceEditor,
235
+ repo: element?.repo ?? '',
236
+ file_path: basePath,
237
+ line: editorStartLine,
238
+ })
239
+ } catch (err) {
240
+ toast({
241
+ title: 'Failed to open editor',
242
+ description: err instanceof Error ? err.message : String(err),
243
+ status: 'error',
244
+ duration: 4000,
245
+ })
246
+ } finally {
247
+ setOpeningEditor(false)
248
+ }
249
+ }
250
+
211
251
  const getLanguageExtension = () => {
212
252
  const extensions = [customCodeTheme]
213
253
  const effectiveLanguage = element?.language || detectLanguage(basePath)
@@ -333,6 +373,40 @@ export default function CodePreviewPanel({ isOpen, onClose, element, hasBackdrop
333
373
  </Button>
334
374
  </Tooltip>
335
375
  )}
376
+ {basePath && (
377
+ <Tooltip label={`Open in ${sourceEditor === 'zed' ? 'Zed' : 'VS Code'}`} placement="bottom">
378
+ <Button
379
+ aria-label={`Open in ${sourceEditor === 'zed' ? 'Zed' : 'VS Code'}`}
380
+ leftIcon={<ExternalLinkIcon w="12px" h="12px" />}
381
+ size="xs"
382
+ variant="outline"
383
+ color="whiteAlpha.700"
384
+ borderColor="whiteAlpha.200"
385
+ h="24px"
386
+ px={2.5}
387
+ fontSize="11px"
388
+ fontWeight="600"
389
+ bg="whiteAlpha.50"
390
+ isLoading={openingEditor}
391
+ onClick={handleOpenInEditor}
392
+ _hover={{
393
+ color: 'white',
394
+ bg: 'whiteAlpha.100',
395
+ borderColor: 'whiteAlpha.400',
396
+ textDecoration: 'none',
397
+ transform: 'translateY(-0.5px)',
398
+ boxShadow: '0 2px 4px rgba(0,0,0,0.2)'
399
+ }}
400
+ _active={{
401
+ bg: 'whiteAlpha.200',
402
+ transform: 'translateY(0)',
403
+ }}
404
+ transition="all 0.1s"
405
+ >
406
+ Open in Editor
407
+ </Button>
408
+ </Tooltip>
409
+ )}
336
410
  <CloseButton size="sm" color="whiteAlpha.500"
337
411
  _hover={{ color: 'white', bg: 'whiteAlpha.100' }}
338
412
  onClick={onClose} />
@@ -1,6 +1,7 @@
1
1
  import { memo, useEffect, useRef, useState, useCallback } from 'react'
2
2
  import type { ConnectorPanelSlots } from '../slots'
3
3
  import {
4
+ Badge,
4
5
  Box,
5
6
  Button,
6
7
  Divider,
@@ -68,6 +69,10 @@ export interface ConnectorPanelProps extends ConnectorPanelSlots {
68
69
  onSave: (connector: Connector) => void
69
70
  autoSave?: boolean
70
71
  onDelete: (edgeId: number) => void
72
+ visibilityOverrideDelta?: number
73
+ onPromoteVisibility?: (id: number) => Promise<void> | void
74
+ onDemoteVisibility?: (id: number) => Promise<void> | void
75
+ onResetVisibility?: (id: number) => Promise<void> | void
71
76
  hasBackdrop?: boolean
72
77
  }
73
78
 
@@ -77,7 +82,7 @@ export interface ConnectorPanelProps extends ConnectorPanelSlots {
77
82
  * Location: Right side of the screen on desktop. Overlays screen on mobile.
78
83
  * Aliases: Connector Properties, Connector Details.
79
84
  */
80
- function ConnectorPanel({ isOpen, onClose, connector, orgId, onSave, autoSave = false, onDelete, hasBackdrop = true, connectorPanelAfterContentSlot }: ConnectorPanelProps) {
85
+ function ConnectorPanel({ isOpen, onClose, connector, orgId, onSave, autoSave = false, onDelete, visibilityOverrideDelta = 0, onPromoteVisibility, onDemoteVisibility, onResetVisibility, hasBackdrop = true, connectorPanelAfterContentSlot }: ConnectorPanelProps) {
81
86
  const { canEdit, viewId } = useViewEditorContext()
82
87
  const isReadOnly = !canEdit
83
88
  const autoSaveEdit = autoSave && !!connector && !isReadOnly
@@ -172,9 +177,9 @@ function ConnectorPanel({ isOpen, onClose, connector, orgId, onSave, autoSave =
172
177
  })
173
178
  }
174
179
 
175
- const handleClose = useCallback(() => {
180
+ const handleClose = useCallback(async () => {
176
181
  if (autoSaveEdit) {
177
- void saveIfDirtyRef.current?.()
182
+ await saveIfDirtyRef.current?.()
178
183
  }
179
184
  onClose()
180
185
  }, [autoSaveEdit, onClose])
@@ -350,6 +355,32 @@ function ConnectorPanel({ isOpen, onClose, connector, orgId, onSave, autoSave =
350
355
  />
351
356
  </FormControl>
352
357
 
358
+ {connector && (onPromoteVisibility || onDemoteVisibility || onResetVisibility) && (
359
+ <Box borderTop="1px solid" borderColor="whiteAlpha.100" pt={2}>
360
+ <HStack justify="space-between" mb={2}>
361
+ <FormLabel fontSize="xs" fontWeight="bold" color="gray.400" mb={0}>DENSITY</FormLabel>
362
+ {visibilityOverrideDelta !== 0 && (
363
+ <Badge colorScheme={visibilityOverrideDelta > 0 ? 'teal' : 'orange'} variant="subtle">
364
+ {visibilityOverrideDelta > 0 ? `+${visibilityOverrideDelta}` : visibilityOverrideDelta}
365
+ </Badge>
366
+ )}
367
+ </HStack>
368
+ <HStack spacing={2}>
369
+ <Button variant="subtle" size="sm" color="teal.200" _hover={{ bg: 'teal.900', color: 'teal.100' }} onClick={() => onPromoteVisibility?.(connector.id)} flex={1} isDisabled={isReadOnly}>
370
+ Promote
371
+ </Button>
372
+ <Button variant="subtle" size="sm" color="orange.200" _hover={{ bg: 'orange.900', color: 'orange.100' }} onClick={() => onDemoteVisibility?.(connector.id)} flex={1} isDisabled={isReadOnly}>
373
+ Demote
374
+ </Button>
375
+ {visibilityOverrideDelta !== 0 && (
376
+ <Button variant="ghost" size="sm" onClick={() => onResetVisibility?.(connector.id)} isDisabled={isReadOnly}>
377
+ Reset
378
+ </Button>
379
+ )}
380
+ </HStack>
381
+ </Box>
382
+ )}
383
+
353
384
  {connectorPanelAfterContentSlot}
354
385
 
355
386
  </VStack>
@@ -20,7 +20,7 @@ import {
20
20
  import { ChevronDownIcon, ChevronLeftIcon, ChevronRightIcon, ChevronUpIcon, LinkIcon } from '@chakra-ui/icons'
21
21
  import type { PlacedElement } from '../types'
22
22
  import { TYPE_COLORS } from '../types'
23
- import { resolveIconPath } from '../utils/url'
23
+ import { resolveElementIconUrl } from '../utils/elementIcon'
24
24
  import { ElementBody } from './NodeBody'
25
25
  import { ElementContainer } from './NodeContainer'
26
26
 
@@ -61,10 +61,7 @@ function ContextNeighborNode({ data }: Props) {
61
61
  const color = TYPE_COLORS[data.kind ?? ''] ?? 'gray'
62
62
 
63
63
  const logoUrl = useMemo(() => {
64
- if (data.logo_url) return resolveIconPath(data.logo_url)
65
- const selected = data.technology_connectors?.find((link) => link.type === 'catalog' && !!(link.is_primary_icon ?? (link as any).isPrimaryIcon) && !!link.slug)
66
- if (!selected?.slug) return undefined
67
- return resolveIconPath(`/icons/${selected.slug}.png`)
64
+ return resolveElementIconUrl(data.logo_url, data.technology_connectors) ?? undefined
68
65
  }, [data.logo_url, data.technology_connectors])
69
66
 
70
67
  const primaryOwnerViewId = data.ownerViewIds[0] ?? data.commonAncestorViewId ?? null
@@ -16,26 +16,29 @@ import {
16
16
  Text,
17
17
  VStack,
18
18
  } from '@chakra-ui/react'
19
- import { CROSS_BRANCH_DEPTH_ALL, CROSS_BRANCH_DEPTH_MAX, CROSS_BRANCH_DEPTH_MIN } from '../crossBranch/types'
20
- import type { CrossBranchContextSettings } from '../crossBranch/types'
19
+ import {
20
+ CROSS_BRANCH_CONNECTOR_BUDGET_MAX,
21
+ CROSS_BRANCH_CONNECTOR_BUDGET_MIN,
22
+ } from '../crossBranch/types'
23
+ import type { CrossBranchConnectorPriority, CrossBranchContextSettings } from '../crossBranch/types'
21
24
 
22
25
  interface Props {
23
26
  settings: CrossBranchContextSettings
24
27
  onEnabledChange: (enabled: boolean) => void
25
- onDepthChange: (depth: number) => void
28
+ onBudgetChange: (budget: number) => void
29
+ onPriorityChange: (priority: CrossBranchConnectorPriority) => void
26
30
  label?: string
27
31
  }
28
32
 
29
- function depthLabel(depth: number) {
30
- return depth >= CROSS_BRANCH_DEPTH_ALL ? 'All' : String(depth)
31
- }
32
-
33
33
  export default function CrossBranchControls({
34
34
  settings,
35
35
  onEnabledChange,
36
- onDepthChange,
36
+ onBudgetChange,
37
+ onPriorityChange,
37
38
  label = 'Cross-Branch',
38
39
  }: Props) {
40
+ const connectorBudget = settings.connectorBudget
41
+
39
42
  return (
40
43
  <Popover placement="top-start" isLazy>
41
44
  <PopoverTrigger>
@@ -50,7 +53,7 @@ export default function CrossBranchControls({
50
53
  <HStack spacing={1.5}>
51
54
  <Box w="7px" h="7px" rounded="full" bg={settings.enabled ? 'var(--accent)' : 'gray.500'} />
52
55
  <Text fontSize="11px" fontWeight="normal">{label}</Text>
53
- <Text fontSize="10px" color="gray.400">{settings.enabled ? depthLabel(settings.depth) : 'Off'}</Text>
56
+ <Text fontSize="10px" color="gray.400">{settings.enabled ? connectorBudget : 'Off'}</Text>
54
57
  </HStack>
55
58
  </Button>
56
59
  </PopoverTrigger>
@@ -71,20 +74,46 @@ export default function CrossBranchControls({
71
74
  <Text fontSize="xs" fontWeight="600" color="white">Show cross-branch context</Text>
72
75
  <Switch isChecked={settings.enabled} onChange={(event) => onEnabledChange(event.target.checked)} colorScheme="blue" />
73
76
  </HStack>
77
+ <Box opacity={settings.enabled ? 1 : 0.4}>
78
+ <Text fontSize="10px" fontWeight="700" color="gray.400" letterSpacing="0.08em" textTransform="uppercase" mb={2}>
79
+ Priority
80
+ </Text>
81
+ <HStack spacing={1} bg="whiteAlpha.100" borderRadius="md" p={1}>
82
+ {(['external', 'internal'] as const).map((priority) => {
83
+ const active = settings.connectorPriority === priority
84
+ return (
85
+ <Button
86
+ key={priority}
87
+ size="xs"
88
+ h="24px"
89
+ flex={1}
90
+ isDisabled={!settings.enabled}
91
+ variant="ghost"
92
+ bg={active ? 'rgba(var(--accent-rgb), 0.18)' : 'transparent'}
93
+ color={active ? 'var(--accent)' : 'gray.300'}
94
+ _hover={{ bg: active ? 'rgba(var(--accent-rgb), 0.22)' : 'whiteAlpha.100' }}
95
+ onClick={() => onPriorityChange(priority)}
96
+ >
97
+ {priority === 'external' ? 'External' : 'Internal'}
98
+ </Button>
99
+ )
100
+ })}
101
+ </HStack>
102
+ </Box>
74
103
  <Box opacity={settings.enabled ? 1 : 0.4}>
75
104
  <HStack justify="space-between" mb={2}>
76
105
  <Text fontSize="10px" fontWeight="700" color="gray.400" letterSpacing="0.08em" textTransform="uppercase">
77
- Descendant Depth
106
+ Connector Budget
78
107
  </Text>
79
- <Text fontSize="xs" color="gray.300">{depthLabel(settings.depth)}</Text>
108
+ <Text fontSize="xs" color="gray.300">{connectorBudget}</Text>
80
109
  </HStack>
81
110
  <Slider
82
111
  isDisabled={!settings.enabled}
83
- min={CROSS_BRANCH_DEPTH_MIN}
84
- max={CROSS_BRANCH_DEPTH_MAX}
112
+ min={CROSS_BRANCH_CONNECTOR_BUDGET_MIN}
113
+ max={CROSS_BRANCH_CONNECTOR_BUDGET_MAX}
85
114
  step={1}
86
- value={settings.depth}
87
- onChange={onDepthChange}
115
+ value={connectorBudget}
116
+ onChange={onBudgetChange}
88
117
  >
89
118
  <SliderTrack bg="whiteAlpha.200">
90
119
  <SliderFilledTrack bg="var(--accent)" />
@@ -92,8 +121,8 @@ export default function CrossBranchControls({
92
121
  <SliderThumb boxSize={4} />
93
122
  </Slider>
94
123
  <HStack justify="space-between" mt={1}>
95
- <Text fontSize="10px" color="gray.500">Near</Text>
96
- <Text fontSize="10px" color="gray.500">All</Text>
124
+ <Text fontSize="10px" color="gray.500">{CROSS_BRANCH_CONNECTOR_BUDGET_MIN}</Text>
125
+ <Text fontSize="10px" color="gray.500">{CROSS_BRANCH_CONNECTOR_BUDGET_MAX}</Text>
97
126
  </HStack>
98
127
  </Box>
99
128
  </VStack>
@@ -7,7 +7,7 @@ import { useAccentColor } from '../context/ThemeContext'
7
7
  import type { PlacedElement, ViewConnector, Tag } from '../types'
8
8
  import { ElementContainer } from './NodeContainer'
9
9
  import { ElementBody } from './NodeBody'
10
- import { resolveIconPath } from '../utils/url'
10
+ import { resolveElementIconUrl } from '../utils/elementIcon'
11
11
  import { ZoomInIcon, ZoomOutIcon, TrashIcon as TrashSvg, EditIcon as EditSvg } from './Icons'
12
12
  import { vscodeBridge } from '../lib/vscodeBridge'
13
13
  import type { ExtensionToWebviewMessage } from '../types/vscode-messages'
@@ -155,6 +155,8 @@ interface NodeData extends PlacedElement {
155
155
  selectedHandleIds?: readonly string[]
156
156
  reconnectCandidates?: readonly { handleId: string; edgeId: string; endpoint: 'source' | 'target'; selected: boolean }[]
157
157
  isConnectorHighlighted?: boolean
158
+ versionChangeType?: 'added' | 'updated' | 'deleted' | 'initialized'
159
+ versionLineDelta?: { added: number; removed: number }
158
160
  }
159
161
 
160
162
  interface Props {
@@ -316,13 +318,7 @@ function ElementNode({ data, selected }: Props) {
316
318
  return next
317
319
  }, [data.reconnectCandidates])
318
320
 
319
- const derivedPrimaryIconPath = (() => {
320
- const selected = data.technology_connectors?.find((link) => link.type === 'catalog' && !!(link.is_primary_icon ?? (link as any).isPrimaryIcon) && !!link.slug)
321
- if (!selected?.slug) return undefined
322
- return resolveIconPath(`/icons/${selected.slug}.png`)
323
- })()
324
-
325
- const nodeLogoUrl = data.logo_url ? resolveIconPath(data.logo_url) : derivedPrimaryIconPath
321
+ const nodeLogoUrl = resolveElementIconUrl(data.logo_url, data.technology_connectors) ?? undefined
326
322
 
327
323
  const technologyLinkCount = (data.technology_connectors || []).filter((l) => !!l.label).length
328
324
  const technologyParts = (data.technology || '')
@@ -456,6 +452,13 @@ function ElementNode({ data, selected }: Props) {
456
452
  const isTarget = !!data.interactionSourceId && !isSource
457
453
 
458
454
  const bodyCursor = isSource ? 'crosshair' : isTarget ? 'cell' : 'pointer'
455
+ const versionColor = data.versionChangeType === 'added'
456
+ ? 'green.300'
457
+ : data.versionChangeType === 'deleted'
458
+ ? 'red.300'
459
+ : data.versionChangeType
460
+ ? 'yellow.300'
461
+ : undefined
459
462
 
460
463
  return (
461
464
  <ElementContainer
@@ -463,12 +466,14 @@ function ElementNode({ data, selected }: Props) {
463
466
  isSource={isSource}
464
467
  isTarget={isTarget}
465
468
  isConnectorHighlighted={!!data.isConnectorHighlighted}
469
+ hasStack={hasChild}
470
+ kind={data.kind}
466
471
  minW="180px"
467
472
  maxW="230px"
468
473
  cursor={bodyCursor}
469
- outline={isDraggedOver ? '2px solid' : undefined}
470
- outlineColor={isDraggedOver ? 'var(--accent)' : undefined}
471
- outlineOffset={isDraggedOver ? '2px' : undefined}
474
+ outline={isDraggedOver || versionColor ? '2px solid' : undefined}
475
+ outlineColor={isDraggedOver ? 'var(--accent)' : versionColor}
476
+ outlineOffset={isDraggedOver || versionColor ? '2px' : undefined}
472
477
  borderTopWidth={data.layerHighlightColor ? '2px' : undefined}
473
478
  borderTopColor={data.layerHighlightColor ?? undefined}
474
479
  onClick={handleBodyClick}
@@ -483,7 +488,7 @@ function ElementNode({ data, selected }: Props) {
483
488
  style={{
484
489
  userSelect: 'none',
485
490
  WebkitUserSelect: 'none',
486
- transition: 'outline 0.15s, outline-color 0.15s',
491
+ transition: 'outline 0.15s, outline-color 0.15s, opacity 0.15s',
487
492
  } as React.CSSProperties}
488
493
  >
489
494
  {HANDLE_CONFIGS.flatMap(({ side, position }) =>
@@ -702,62 +707,108 @@ function ElementNode({ data, selected }: Props) {
702
707
  )}
703
708
 
704
709
  {/* Code Preview Icon/Link in Bottom Right Corner */}
705
- {((data.repo || data.url) && !window.__TLD_VSCODE__) && (
706
- <Box
710
+ {!window.__TLD_VSCODE__ && ((data.repo || data.url) || data.versionLineDelta) && (
711
+ <HStack
707
712
  position="absolute"
708
713
  bottom="8px"
709
714
  right="8px"
710
715
  zIndex={10}
716
+ spacing={1}
717
+ align="center"
711
718
  >
712
- <Tooltip
713
- label={
714
- data.repo
715
- ? `View source: ${data.file_path?.includes('#') ? (() => { try { return JSON.parse(data.file_path.split('#')[1]).name } catch { return 'Link' } })() : 'Link'}${data.url ? ' / URL' : ''}`
716
- : 'Open Link'
717
- }
718
- placement="top"
719
- isDisabled={data.isCanvasMoving}
720
- >
721
- <Box
722
- as="button"
723
- display="flex"
724
- alignItems="center"
725
- justifyContent="center"
726
- w="18px"
719
+ {data.versionLineDelta && (
720
+ <HStack
721
+ spacing={1}
727
722
  h="18px"
723
+ px={1.5}
728
724
  rounded="md"
729
- color="whiteAlpha.900"
730
- _hover={{ color: 'blue.300', bg: 'whiteAlpha.200', transform: 'scale(1.1)' }}
731
- transition="all 0.15s"
732
- onClick={(e: React.MouseEvent) => {
733
- e.stopPropagation()
734
- if (data.repo) {
735
- data.onOpenCodePreview?.(data.element_id)
736
- } else if (data.url) {
737
- window.open(data.url, '_blank', 'noopener,noreferrer')
738
- }
739
- }}
740
- onPointerDown={(e: React.PointerEvent) => e.stopPropagation()}
725
+ bg="rgba(var(--bg-main-rgb), 0.86)"
726
+ border="1px solid"
727
+ borderColor="whiteAlpha.300"
728
+ boxShadow="0 4px 12px rgba(0,0,0,0.28)"
729
+ pointerEvents="none"
741
730
  >
742
- <LinkIcon w={2.5} h={2.5} />
743
- </Box>
744
- </Tooltip>
745
- </Box>
731
+ {data.versionLineDelta.added > 0 && (
732
+ <Text fontSize="9px" fontWeight="800" lineHeight="1" color="green.300">+{data.versionLineDelta.added}</Text>
733
+ )}
734
+ {data.versionLineDelta.removed > 0 && (
735
+ <Text fontSize="9px" fontWeight="800" lineHeight="1" color="red.300">-{data.versionLineDelta.removed}</Text>
736
+ )}
737
+ </HStack>
738
+ )}
739
+ {(data.repo || data.url) && !window.__TLD_VSCODE__ && (
740
+ <Tooltip
741
+ label={
742
+ data.repo
743
+ ? `View source: ${data.file_path?.includes('#') ? (() => { try { return JSON.parse(data.file_path.split('#')[1]).name } catch { return 'Link' } })() : 'Link'}${data.url ? ' / URL' : ''}`
744
+ : 'Open Link'
745
+ }
746
+ placement="top"
747
+ isDisabled={data.isCanvasMoving}
748
+ >
749
+ <Box
750
+ as="button"
751
+ display="flex"
752
+ alignItems="center"
753
+ justifyContent="center"
754
+ w="18px"
755
+ h="18px"
756
+ rounded="md"
757
+ color="whiteAlpha.900"
758
+ _hover={{ color: 'blue.300', bg: 'whiteAlpha.200', transform: 'scale(1.1)' }}
759
+ transition="all 0.15s"
760
+ onClick={(e: React.MouseEvent) => {
761
+ e.stopPropagation()
762
+ if (data.repo) {
763
+ data.onOpenCodePreview?.(data.element_id)
764
+ } else if (data.url) {
765
+ window.open(data.url, '_blank', 'noopener,noreferrer')
766
+ }
767
+ }}
768
+ onPointerDown={(e: React.PointerEvent) => e.stopPropagation()}
769
+ >
770
+ <LinkIcon w={2.5} h={2.5} />
771
+ </Box>
772
+ </Tooltip>
773
+ )}
774
+ </HStack>
746
775
  )}
747
776
 
748
777
  {/* VSCode specific file link with hover preview */}
749
778
  {window.__TLD_VSCODE__ && data.file_path && (
750
- <Box
779
+ <HStack
751
780
  position="absolute"
752
781
  bottom="8px"
753
782
  right="8px"
754
783
  zIndex={10}
784
+ spacing={1}
785
+ align="center"
755
786
  >
787
+ {data.versionLineDelta && (
788
+ <HStack
789
+ spacing={1}
790
+ h="18px"
791
+ px={1.5}
792
+ rounded="md"
793
+ bg="rgba(var(--bg-main-rgb), 0.86)"
794
+ border="1px solid"
795
+ borderColor="whiteAlpha.300"
796
+ boxShadow="0 4px 12px rgba(0,0,0,0.28)"
797
+ pointerEvents="none"
798
+ >
799
+ {data.versionLineDelta.added > 0 && (
800
+ <Text fontSize="9px" fontWeight="800" lineHeight="1" color="green.300">+{data.versionLineDelta.added}</Text>
801
+ )}
802
+ {data.versionLineDelta.removed > 0 && (
803
+ <Text fontSize="9px" fontWeight="800" lineHeight="1" color="red.300">-{data.versionLineDelta.removed}</Text>
804
+ )}
805
+ </HStack>
806
+ )}
756
807
  <VscodeCodePreview
757
808
  filePath={data.file_path}
758
809
  isCanvasMoving={data.isCanvasMoving}
759
810
  />
760
- </Box>
811
+ </HStack>
761
812
  )}
762
813
 
763
814
  {selected && !isSource && (