@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
@@ -1,9 +1,12 @@
1
- import { Box, FormLabel, HStack, Text, Tooltip, VStack, Wrap, WrapItem } from '@chakra-ui/react'
1
+ import { Box, FormLabel, HStack, Select, Text, Tooltip, VStack, Wrap, WrapItem } from '@chakra-ui/react'
2
2
  import { ACCENT_OPTIONS, BACKGROUND_OPTIONS, ELEMENT_OPTIONS } from '../constants/colors'
3
3
  import { useTheme } from '../context/ThemeContext'
4
+ import { useSourceEditor } from '../utils/sourceEditor'
5
+ import type { SourceEditor } from '../api/client'
4
6
 
5
7
  export default function AppearanceSettings({ compact = false }: { compact?: boolean }) {
6
8
  const { accent, setAccent, background, setBackground, elementColor, setElementColor } = useTheme()
9
+ const { editor, setEditor } = useSourceEditor()
7
10
  const swatchSize = compact ? '28px' : '32px'
8
11
  const sectionGap = compact ? 5 : 8
9
12
 
@@ -19,6 +22,26 @@ export default function AppearanceSettings({ compact = false }: { compact?: bool
19
22
  </HStack>
20
23
  </Box>
21
24
 
25
+ <Box w="full">
26
+ <FormLabel mb={3} fontSize={compact ? 'xs' : 'sm'} textTransform="uppercase" letterSpacing="0.12em" color="gray.400">
27
+ Source Editor
28
+ </FormLabel>
29
+ <Select
30
+ size="sm"
31
+ value={editor}
32
+ onChange={(event) => setEditor(event.target.value as SourceEditor)}
33
+ bg="whiteAlpha.50"
34
+ borderColor="whiteAlpha.200"
35
+ color="gray.100"
36
+ maxW="220px"
37
+ _hover={{ borderColor: 'whiteAlpha.400' }}
38
+ _focus={{ borderColor: 'blue.400', boxShadow: '0 0 0 1px var(--chakra-colors-blue-400)' }}
39
+ >
40
+ <option value="zed">Zed</option>
41
+ <option value="vscode">VS Code</option>
42
+ </Select>
43
+ </Box>
44
+
22
45
  <Box w="full">
23
46
  <FormLabel mb={3} fontSize={compact ? 'xs' : 'sm'} textTransform="uppercase" letterSpacing="0.12em" color="gray.400">
24
47
  Accent
@@ -1,4 +1,5 @@
1
1
  import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'
2
+ import { useSearchParams } from 'react-router-dom'
2
3
  import { motion } from 'framer-motion'
3
4
  import {
4
5
  Box,
@@ -13,6 +14,7 @@ import {
13
14
  MenuItem,
14
15
  MenuList,
15
16
  Spinner,
17
+ Badge,
16
18
  Tag,
17
19
  Text,
18
20
  VStack,
@@ -27,6 +29,7 @@ import { ElementBody } from '../components/NodeBody'
27
29
  import DependenciesOnboarding from '../components/DependenciesOnboarding'
28
30
  import { useTheme } from '../context/ThemeContext'
29
31
  import { hexToRgba } from '../constants/colors'
32
+ import { useWorkspaceVersionPreview, type VersionChangeType } from '../context/WorkspaceVersionContext'
30
33
 
31
34
  // ── Data types ─────────────────────────────────────────────────────────────
32
35
  interface ElementWithNeighbours extends DependencyElement {
@@ -39,6 +42,8 @@ interface NeighbourNode {
39
42
  position: 'left' | 'right' | 'top' | 'bottom'
40
43
  }
41
44
 
45
+ const PAGE_SIZE = 50
46
+
42
47
  // ── Helpers ────────────────────────────────────────────────────────────────
43
48
  function computeNeighbourCounts(elements: DependencyElement[], connectors: DependencyConnector[]): ElementWithNeighbours[] {
44
49
  const counts = new Map<string, Set<string>>()
@@ -175,11 +180,13 @@ function NeighbourCard({
175
180
  onClick,
176
181
  setRef,
177
182
  compactLevel = 0,
183
+ versionChangeType,
178
184
  }: {
179
185
  node: NeighbourNode
180
186
  onClick: () => void
181
187
  setRef?: (el: HTMLDivElement | null) => void
182
188
  compactLevel?: number
189
+ versionChangeType?: VersionChangeType
183
190
  }) {
184
191
  const cardPadding = compactLevel >= 3 ? 1 : compactLevel >= 2 ? 1.5 : compactLevel >= 1 ? 2 : 3
185
192
  const showTech = compactLevel < 2
@@ -195,6 +202,13 @@ function NeighbourCard({
195
202
  compactLevel >= 2 ? (nameLen > 20 ? '2xs' : 'xs') :
196
203
  compactLevel >= 1 ? (nameLen > 22 ? 'xs' : 'sm') :
197
204
  (nameLen > 24 ? 'xs' : 'sm')
205
+ const versionColor = versionChangeType === 'added'
206
+ ? 'green.300'
207
+ : versionChangeType === 'deleted'
208
+ ? 'red.300'
209
+ : versionChangeType
210
+ ? 'yellow.300'
211
+ : undefined
198
212
 
199
213
  return (
200
214
  <motion.div
@@ -212,6 +226,9 @@ function NeighbourCard({
212
226
  p={0}
213
227
  cursor="pointer"
214
228
  borderColor="whiteAlpha.200"
229
+ outline={versionColor ? '2px solid' : undefined}
230
+ outlineColor={versionColor}
231
+ outlineOffset={versionColor ? '2px' : undefined}
215
232
  _hover={{ borderColor: 'var(--accent)', boxShadow: '0 0 0 1px rgba(var(--accent-rgb), 0.25)' }}
216
233
  >
217
234
  <ElementBody
@@ -254,14 +271,25 @@ const TYPE_HEX: Record<string, string> = {
254
271
  export default function Dependencies() {
255
272
  const setHeader = useSetHeader()
256
273
  const { accent, elementColor } = useTheme()
274
+ const [searchParams, setSearchParams] = useSearchParams()
275
+ const { preview: versionPreview, followTarget: versionFollowTarget } = useWorkspaceVersionPreview()
276
+ const versionPulseChangeForElement = useCallback((elementId: number): VersionChangeType | undefined => {
277
+ if (versionFollowTarget?.resourceType !== 'element' || versionFollowTarget.resourceId !== elementId) return undefined
278
+ return versionFollowTarget.changeType ?? versionPreview?.elementChanges.get(elementId)
279
+ }, [versionFollowTarget, versionPreview])
257
280
 
258
281
  const [elements, setElements] = useState<DependencyElement[]>([])
259
282
  const [allEdges, setAllEdges] = useState<DependencyConnector[]>([])
260
283
  const [loading, setLoading] = useState(true)
284
+ const [pageLoading, setPageLoading] = useState(false)
261
285
  const [search, setSearch] = useState('')
262
286
  const [typeFilter, setTypeFilter] = useState('')
263
287
  const [selectedId, setSelectedId] = useState<string | null>(null)
264
288
  const [topRatio, setTopRatio] = useState(0.45)
289
+ const [page, setPage] = useState(0)
290
+ const [hasNextPage, setHasNextPage] = useState(false)
291
+ const [totalCount, setTotalCount] = useState(0)
292
+ const [neighbourElements, setNeighbourElements] = useState<Record<string, DependencyElement>>({})
265
293
 
266
294
  // Graph layout measurement
267
295
  const graphRef = useRef<HTMLDivElement>(null)
@@ -287,57 +315,124 @@ export default function Dependencies() {
287
315
 
288
316
  useEffect(() => { applyPan(0, 0) }, [selectedId, applyPan])
289
317
 
318
+ useEffect(() => {
319
+ const requestedId = searchParams.get('element')
320
+ if (requestedId) setSelectedId(requestedId)
321
+ }, [searchParams])
322
+
323
+ const selectElement = useCallback((id: string | null) => {
324
+ setSelectedId(id)
325
+ const next = new URLSearchParams(searchParams)
326
+ if (id) next.set('element', id)
327
+ else next.delete('element')
328
+ setSearchParams(next, { replace: true })
329
+ }, [searchParams, setSearchParams])
330
+
290
331
  // Header
291
332
  useEffect(() => {
292
333
  setHeader({
293
334
  hideMobileBar: true,
294
- node: (
295
- <HStack
296
- bg="whiteAlpha.50"
297
- border="1px solid"
298
- borderColor="whiteAlpha.100"
299
- px={3}
300
- py={1}
301
- borderRadius="md"
302
- spacing={3}
303
- >
304
- <Text fontSize="xs" color="whiteAlpha.800" fontWeight="medium" display={{ base: 'none', compact: 'inline' }}>
305
- {elements.length} <Text as="span" color="whiteAlpha.400" fontWeight="normal">elements</Text>
306
- </Text>
307
- <Box w="1px" h="10px" bg="whiteAlpha.200" display={{ base: 'none', compact: 'block' }} />
308
- <Text fontSize="xs" color="whiteAlpha.800" fontWeight="medium" display={{ base: 'none', compact: 'inline' }}>
309
- {allEdges.length} <Text as="span" color="whiteAlpha.400" fontWeight="normal">connectors</Text>
310
- </Text>
311
- <Text fontSize="xs" color="whiteAlpha.800" fontWeight="medium" display={{ base: 'none', sm: 'inline', compact: 'none' }}>
312
- {elements.length}<Text as="span" color="whiteAlpha.400">E</Text>
313
- <Text as="span" color="whiteAlpha.200" mx={1}>/</Text>
314
- {allEdges.length}<Text as="span" color="whiteAlpha.400">C</Text>
315
- </Text>
316
- </HStack>
317
- ),
335
+ node: null,
318
336
  })
319
337
  return () => setHeader(null)
320
- }, [elements.length, allEdges.length, setHeader])
338
+ }, [setHeader])
339
+
340
+ useEffect(() => {
341
+ setPage(0)
342
+ }, [search])
321
343
 
322
344
  // Data fetch
323
345
  useEffect(() => {
324
- api.dependencies
325
- .list()
326
- .then((resp) => {
327
- const objs = resp.elements || []
328
- const edgs = resp.connectors || []
329
- setElements(objs)
330
- setAllEdges(edgs)
331
-
332
- if (objs.length > 0) {
333
- const withCounts = computeNeighbourCounts(objs, edgs)
334
- const sorted = [...withCounts].sort((a, b) => b.neighbourCount - a.neighbourCount)
335
- setSelectedId(sorted[0].id)
336
- }
346
+ let cancelled = false
347
+ const timer = window.setTimeout(() => {
348
+ setPageLoading(true)
349
+ api.dependencies
350
+ .list({ limit: PAGE_SIZE, offset: page * PAGE_SIZE, search })
351
+ .then((resp) => {
352
+ if (cancelled) return
353
+ const objs = resp.elements || []
354
+ const edgs = resp.connectors || []
355
+ const total = resp.totalCount
356
+ setElements(objs)
357
+ setAllEdges(edgs)
358
+ setTotalCount(total ?? page * PAGE_SIZE + objs.length)
359
+ setHasNextPage(total === undefined ? objs.length === PAGE_SIZE : page * PAGE_SIZE + objs.length < total)
360
+
361
+ setSelectedId((current) => {
362
+ if (objs.length === 0) return null
363
+ if (current && objs.some((obj) => obj.id === current)) return current
364
+ const withCounts = computeNeighbourCounts(objs, edgs)
365
+ const sorted = [...withCounts].sort((a, b) => b.neighbourCount - a.neighbourCount)
366
+ return sorted[0]?.id ?? null
367
+ })
368
+ })
369
+ .catch(() => { /* intentionally empty */ })
370
+ .finally(() => {
371
+ if (!cancelled) {
372
+ setLoading(false)
373
+ setPageLoading(false)
374
+ }
375
+ })
376
+ }, 180)
377
+ return () => {
378
+ cancelled = true
379
+ window.clearTimeout(timer)
380
+ }
381
+ }, [page, search])
382
+
383
+ const elementUniverse = useMemo(() => {
384
+ const byID = new Map<string, DependencyElement>()
385
+ elements.forEach((element) => byID.set(element.id, element))
386
+ Object.values(neighbourElements).forEach((element) => byID.set(element.id, element))
387
+ return Array.from(byID.values())
388
+ }, [elements, neighbourElements])
389
+
390
+ useEffect(() => {
391
+ if (selectedId === null) return
392
+ const known = new Set(elementUniverse.map((element) => element.id))
393
+ const missing = new Set<string>()
394
+ allEdges.forEach((connector) => {
395
+ if (connector.source_element_id === selectedId && !known.has(connector.target_element_id)) {
396
+ missing.add(connector.target_element_id)
397
+ }
398
+ if (connector.target_element_id === selectedId && !known.has(connector.source_element_id)) {
399
+ missing.add(connector.source_element_id)
400
+ }
401
+ })
402
+ if (missing.size === 0) return
403
+ let cancelled = false
404
+ Promise.all(
405
+ Array.from(missing).slice(0, 120).map((id) =>
406
+ api.elements.get(Number(id)).then((element) => ({
407
+ id: String(element.id),
408
+ name: element.name,
409
+ type: element.kind,
410
+ description: element.description,
411
+ technology: element.technology,
412
+ url: element.url,
413
+ logo_url: element.logo_url,
414
+ technology_connectors: element.technology_connectors,
415
+ tags: element.tags,
416
+ repo: element.repo,
417
+ branch: element.branch,
418
+ language: element.language,
419
+ file_path: element.file_path,
420
+ created_at: element.created_at,
421
+ updated_at: element.updated_at,
422
+ } satisfies DependencyElement)).catch(() => null),
423
+ ),
424
+ ).then((items) => {
425
+ if (cancelled) return
426
+ setNeighbourElements((prev) => {
427
+ const next = { ...prev }
428
+ items.forEach((item) => {
429
+ if (item) next[item.id] = item
430
+ })
431
+ return next
337
432
  })
338
- .catch(() => { /* intentionally empty */ })
339
- .finally(() => setLoading(false))
340
- }, [])
433
+ })
434
+ return () => { cancelled = true }
435
+ }, [allEdges, elementUniverse, selectedId])
341
436
 
342
437
  // Derived data
343
438
  const elementsWithCounts = useMemo(
@@ -364,12 +459,12 @@ export default function Dependencies() {
364
459
 
365
460
  const selectedElement = useMemo(() => {
366
461
  if (selectedId === null) return null
367
- return elements.find((o) => o.id === selectedId) || null
368
- }, [elements, selectedId])
462
+ return elementUniverse.find((o) => o.id === selectedId) || null
463
+ }, [elementUniverse, selectedId])
369
464
  const neighbourGraph = useMemo(() => {
370
465
  if (selectedId === null) return []
371
- return getNeighbourGraph(selectedId, elements, allEdges)
372
- }, [selectedId, elements, allEdges])
466
+ return getNeighbourGraph(selectedId, elementUniverse, allEdges)
467
+ }, [selectedId, elementUniverse, allEdges])
373
468
 
374
469
  // Divider drag
375
470
  const startDrag = useCallback(() => {
@@ -497,7 +592,7 @@ export default function Dependencies() {
497
592
 
498
593
  if (loading) {
499
594
  return (
500
- <Flex h="100vh" align="center" justify="center">
595
+ <Flex h="100%" align="center" justify="center">
501
596
  <Spinner size="xl" color="blue.500" thickness="3px" />
502
597
  </Flex>
503
598
  )
@@ -528,9 +623,11 @@ export default function Dependencies() {
528
623
  const colSpacing = maxCompactLevel >= 3 ? 2 : maxCompactLevel >= 2 ? 3 : maxCompactLevel >= 1 ? 5 : 8
529
624
  const nodeSpacing = maxCompactLevel >= 2 ? 1 : maxCompactLevel >= 1 ? 2 : 3
530
625
  const selectedCardShadow = `0 0 0 3px ${hexToRgba(accent, 0.38)}, 0 18px 48px ${hexToRgba(accent, 0.12)}, 0 10px 36px rgba(0,0,0,0.55), 0 3px 10px rgba(0,0,0,0.4)`
626
+ const rangeStart = elements.length > 0 ? page * PAGE_SIZE + 1 : 0
627
+ const rangeEnd = page * PAGE_SIZE + elements.length
531
628
 
532
629
  return (
533
- <Box h="100vh" display="flex" flexDir="column" bg="var(--bg-canvas)">
630
+ <Box h="100%" display="flex" flexDir="column" bg="var(--bg-canvas)">
534
631
  <Box ref={containerRef} flex={1} display="flex" flexDir="column" overflow="hidden">
535
632
 
536
633
  {/* ── Top: Listing ──────────────────────────────────────────────────── */}
@@ -594,9 +691,45 @@ export default function Dependencies() {
594
691
  </MenuList>
595
692
  </Menu>
596
693
  <Box flex={1} />
597
- <Text fontSize="xs" color="gray.600">
598
- {filteredElements.length} element{filteredElements.length !== 1 ? 's' : ''}
694
+
695
+ <HStack spacing={4} mr={4} display={{ base: 'none', md: 'flex' }}>
696
+ <HStack spacing={1.5}>
697
+ <Text fontSize="xs" color="whiteAlpha.900" fontWeight="bold">{totalCount}</Text>
698
+ <Text fontSize="xs" color="whiteAlpha.400">elements</Text>
699
+ </HStack>
700
+ <HStack spacing={1.5}>
701
+ <Text fontSize="xs" color="whiteAlpha.900" fontWeight="bold">{allEdges.length}</Text>
702
+ <Text fontSize="xs" color="whiteAlpha.400">connectors</Text>
703
+ </HStack>
704
+ </HStack>
705
+
706
+ <Box w="1px" h="12px" bg="whiteAlpha.200" mr={2} display={{ base: 'none', md: 'block' }} />
707
+
708
+ <Text fontSize="xs" color="gray.600" fontWeight="medium">
709
+ {rangeStart}-{rangeEnd} <Text as="span" color="gray.700" display={{ base: 'none', sm: 'inline' }}>of {totalCount}</Text>
599
710
  </Text>
711
+ {pageLoading && <Spinner size="xs" color="gray.500" />}
712
+ <HStack spacing={1} data-pan-block="true">
713
+ <Button
714
+ variant="elevated"
715
+ size="xs"
716
+ isDisabled={page === 0 || pageLoading}
717
+ onClick={() => setPage((current) => Math.max(0, current - 1))}
718
+ >
719
+ Previous
720
+ </Button>
721
+ <Text fontSize="xs" color="gray.500" minW="48px" textAlign="center">
722
+ Page {page + 1}
723
+ </Text>
724
+ <Button
725
+ variant="elevated"
726
+ size="xs"
727
+ isDisabled={!hasNextPage || pageLoading}
728
+ onClick={() => setPage((current) => current + 1)}
729
+ >
730
+ Next
731
+ </Button>
732
+ </HStack>
600
733
  </Flex>
601
734
 
602
735
  {/* Column headers */}
@@ -640,6 +773,15 @@ export default function Dependencies() {
640
773
  const color = TYPE_COLORS[typeKey] ?? 'gray'
641
774
  const accentHex = TYPE_HEX[typeKey] ?? '#718096'
642
775
  const isSelected = selectedId === obj.id
776
+ const versionChangeType = versionPulseChangeForElement(Number(obj.id))
777
+ const versionLineDelta = versionPreview?.elementLineDeltas.get(Number(obj.id))
778
+ const versionColor = versionChangeType === 'added'
779
+ ? 'green.300'
780
+ : versionChangeType === 'deleted'
781
+ ? 'red.300'
782
+ : versionChangeType
783
+ ? 'yellow.300'
784
+ : undefined
643
785
 
644
786
  return (
645
787
  <Flex
@@ -653,9 +795,12 @@ export default function Dependencies() {
653
795
  bg={isSelected ? 'rgba(66,153,225,0.07)' : 'transparent'}
654
796
  _hover={{ bg: isSelected ? 'rgba(66,153,225,0.1)' : 'whiteAlpha.50' }}
655
797
  transition="background 0.1s"
656
- onClick={() => setSelectedId(isSelected ? null : obj.id)}
798
+ onClick={() => selectElement(isSelected ? null : obj.id)}
657
799
  position="relative"
658
800
  role="row"
801
+ outline={versionColor ? '1px solid' : undefined}
802
+ outlineColor={versionColor}
803
+ outlineOffset="-1px"
659
804
  >
660
805
  {/* Left type-color accent */}
661
806
  <Box
@@ -669,14 +814,28 @@ export default function Dependencies() {
669
814
 
670
815
  {/* Name */}
671
816
  <Box flex={1} minW={0} mr={4}>
672
- <Text
673
- fontSize="sm"
674
- fontWeight={isSelected ? 'semibold' : 'medium'}
675
- color={isSelected ? 'white' : 'gray.100'}
676
- noOfLines={1}
677
- >
678
- {obj.name}
679
- </Text>
817
+ <HStack spacing={2} minW={0}>
818
+ <Text
819
+ fontSize="sm"
820
+ fontWeight={isSelected ? 'semibold' : 'medium'}
821
+ color={isSelected ? 'white' : 'gray.100'}
822
+ noOfLines={1}
823
+ minW={0}
824
+ >
825
+ {obj.name}
826
+ </Text>
827
+ {versionChangeType && (
828
+ <Badge colorScheme={versionChangeType === 'added' ? 'green' : versionChangeType === 'deleted' ? 'red' : 'yellow'} fontSize="8px" flexShrink={0}>
829
+ {versionChangeType === 'added' ? '+' : versionChangeType === 'deleted' ? '-' : '~'}
830
+ </Badge>
831
+ )}
832
+ {versionLineDelta && (
833
+ <HStack spacing={1} flexShrink={0}>
834
+ {versionLineDelta.added > 0 && <Text fontSize="10px" color="green.300" fontWeight="800">+{versionLineDelta.added}</Text>}
835
+ {versionLineDelta.removed > 0 && <Text fontSize="10px" color="red.300" fontWeight="800">-{versionLineDelta.removed}</Text>}
836
+ </HStack>
837
+ )}
838
+ </HStack>
680
839
  </Box>
681
840
 
682
841
  {/* Type badge */}
@@ -800,7 +959,8 @@ export default function Dependencies() {
800
959
  key={n.element.id}
801
960
  node={n}
802
961
  compactLevel={maxCompactLevel}
803
- onClick={() => setSelectedId(n.element.id)}
962
+ versionChangeType={versionPulseChangeForElement(Number(n.element.id))}
963
+ onClick={() => selectElement(n.element.id)}
804
964
  />
805
965
  ))}
806
966
  </HStack>
@@ -823,7 +983,8 @@ export default function Dependencies() {
823
983
  key={n.element.id}
824
984
  node={n}
825
985
  compactLevel={leftCompactLevel}
826
- onClick={() => setSelectedId(n.element.id)}
986
+ versionChangeType={versionPulseChangeForElement(Number(n.element.id))}
987
+ onClick={() => selectElement(n.element.id)}
827
988
  />
828
989
  ))}
829
990
  </VStack>
@@ -844,6 +1005,9 @@ export default function Dependencies() {
844
1005
  bg={elementColor}
845
1006
  borderColor={accent}
846
1007
  borderWidth="2px"
1008
+ outline={selectedId && versionPulseChangeForElement(Number(selectedId)) ? '3px solid' : undefined}
1009
+ outlineColor={selectedId && versionPulseChangeForElement(Number(selectedId)) === 'added' ? 'green.300' : selectedId && versionPulseChangeForElement(Number(selectedId)) === 'deleted' ? 'red.300' : selectedId && versionPulseChangeForElement(Number(selectedId)) ? 'yellow.300' : undefined}
1010
+ outlineOffset="3px"
847
1011
  boxShadow={selectedCardShadow}
848
1012
  >
849
1013
  <ElementBody
@@ -873,10 +1037,11 @@ export default function Dependencies() {
873
1037
  {column.map((n) => (
874
1038
  <NeighbourCard
875
1039
  key={n.element.id}
876
- node={n}
877
- compactLevel={rightCompactLevel}
878
- onClick={() => setSelectedId(n.element.id)}
879
- />
1040
+ node={n}
1041
+ compactLevel={rightCompactLevel}
1042
+ versionChangeType={versionPulseChangeForElement(Number(n.element.id))}
1043
+ onClick={() => selectElement(n.element.id)}
1044
+ />
880
1045
  ))}
881
1046
  </VStack>
882
1047
  ))}
@@ -897,7 +1062,8 @@ export default function Dependencies() {
897
1062
  key={n.element.id}
898
1063
  node={n}
899
1064
  compactLevel={maxCompactLevel}
900
- onClick={() => setSelectedId(n.element.id)}
1065
+ versionChangeType={versionPulseChangeForElement(Number(n.element.id))}
1066
+ onClick={() => selectElement(n.element.id)}
901
1067
  />
902
1068
  ))}
903
1069
  </HStack>