@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
@@ -29,16 +29,25 @@ import { motion, AnimatePresence } from 'framer-motion'
29
29
  import ViewsGrid from './ViewsGrid'
30
30
  import InfiniteZoom, { type InfiniteZoomHandle } from './InfiniteZoom'
31
31
  import { ZoomInIcon } from '../components/Icons'
32
+ import { WATCH_REPRESENTATION_UPDATED_EVENT } from '../components/WorkspacePanel'
32
33
  import { api } from '../api/client'
33
34
  import { toast } from '../utils/toast'
34
- import type { ViewTreeNode } from '../types'
35
+ import type { ExploreData, ViewTreeNode } from '../types'
36
+ import {
37
+ buildJumpSearchResults,
38
+ flattenTree,
39
+ jumpResultActionLabel,
40
+ jumpResultSubtitle,
41
+ type JumpSearchResult,
42
+ type JumpViewMode,
43
+ } from './viewsJumpSearch'
35
44
 
36
45
  interface Props {
37
46
  shareSlot?: React.ReactNode
38
47
  onShareView?: (viewId: number) => void
39
48
  }
40
49
 
41
- type ViewType = 'explore' | 'hierarchy'
50
+ type ViewType = JumpViewMode
42
51
 
43
52
  const MotionBox = motion.create(Box)
44
53
 
@@ -56,24 +65,14 @@ function HierarchyModeIcon({ size = 13 }: { size?: number }) {
56
65
  )
57
66
  }
58
67
 
59
- function flattenTree(roots: ViewTreeNode[]): ViewTreeNode[] {
60
- const result: ViewTreeNode[] = []
61
- const traverse = (node: ViewTreeNode) => {
62
- result.push(node)
63
- node.children.forEach(traverse)
64
- }
65
- roots.forEach(traverse)
66
- return result
67
- }
68
-
69
68
  interface DiagramJumpToolbarProps {
70
69
  view: ViewType
71
70
  searchTerm: string
72
- searchResults: ViewTreeNode[]
71
+ searchResults: JumpSearchResult[]
73
72
  activeSearchIndex: number
74
73
  onSearchChange: (term: string) => void
75
74
  onSearchKeyDown: (e: React.KeyboardEvent) => void
76
- onResultClick: (result: ViewTreeNode) => void
75
+ onResultClick: (result: JumpSearchResult) => void
77
76
  onViewChange: (view: ViewType) => void
78
77
  onCreateOpen: () => void
79
78
  }
@@ -328,7 +327,7 @@ function DiagramJumpToolbar({
328
327
  >
329
328
  {searchResults.map((result, idx) => (
330
329
  <Flex
331
- key={result.id}
330
+ key={result.key}
332
331
  px={4}
333
332
  py={2.5}
334
333
  align="center"
@@ -352,13 +351,13 @@ function DiagramJumpToolbar({
352
351
  {result.name}
353
352
  </Text>
354
353
  <Text color="whiteAlpha.500" fontSize="10px" textTransform="uppercase" letterSpacing="0.05em">
355
- Level {result.level} • {result.level_label || 'Diagram'}
354
+ {jumpResultSubtitle(result)}
356
355
  </Text>
357
356
  </Box>
358
357
  {idx === activeSearchIndex && (
359
358
  <HStack spacing={1} opacity={0.8}>
360
359
  <Text color="var(--accent)" fontSize="9px" fontWeight="800" letterSpacing="0.1em">
361
- {view === 'explore' ? 'ZOOM' : 'OPEN'}
360
+ {jumpResultActionLabel(view)}
362
361
  </Text>
363
362
  <Text color="whiteAlpha.400" fontSize="9px">↵</Text>
364
363
  </HStack>
@@ -384,15 +383,33 @@ export default function ViewsPage({ shareSlot, onShareView }: Props) {
384
383
  const [treeLoading, setTreeLoading] = useState(true)
385
384
  const [focusedHierarchyId, setFocusedHierarchyId] = useState<number | null>(null)
386
385
  const [searchTerm, setSearchTerm] = useState('')
387
- const [searchResults, setSearchResults] = useState<ViewTreeNode[]>([])
386
+ const [searchResults, setSearchResults] = useState<JumpSearchResult[]>([])
388
387
  const [activeSearchIndex, setActiveSearchIndex] = useState(-1)
388
+ const [exploreSearchData, setExploreSearchData] = useState<ExploreData | null>(null)
389
389
  const { isOpen: isCreateOpen, onOpen: onCreateOpen, onClose: onCreateClose } = useDisclosure()
390
390
  const [newName, setNewName] = useState('')
391
391
  const [isCreating, setIsCreating] = useState(false)
392
392
  const exploreRef = useRef<InfiniteZoomHandle>(null)
393
+ const exploreSearchLoadRef = useRef<Promise<ExploreData | null> | null>(null)
393
394
 
394
395
  const flatTree = useMemo(() => flattenTree(treeData), [treeData])
395
396
 
397
+ const ensureExploreSearchData = useCallback(() => {
398
+ if (exploreSearchData || exploreSearchLoadRef.current) return
399
+ exploreSearchLoadRef.current = api.explore.load()
400
+ .then((data) => {
401
+ if (!data.password_required) {
402
+ setExploreSearchData(data)
403
+ return data
404
+ }
405
+ return null
406
+ })
407
+ .catch(() => null)
408
+ .finally(() => {
409
+ exploreSearchLoadRef.current = null
410
+ })
411
+ }, [exploreSearchData])
412
+
396
413
  const handleViewChange = useCallback((newView: ViewType) => {
397
414
  setView(newView)
398
415
  const newParams = new URLSearchParams(searchParams)
@@ -411,8 +428,43 @@ export default function ViewsPage({ shareSlot, onShareView }: Props) {
411
428
  }
412
429
  }, [searchParams])
413
430
 
431
+ useEffect(() => {
432
+ const focusId = Number(searchParams.get('focus') ?? 0)
433
+ const elementId = Number(searchParams.get('element') ?? 0)
434
+ if (view !== 'explore' || !Number.isFinite(focusId) || focusId <= 0) return
435
+ let attempts = 0
436
+ let timer: number | null = null
437
+ const focus = () => {
438
+ attempts += 1
439
+ if (Number.isFinite(elementId) && elementId > 0) {
440
+ if (exploreRef.current?.focusElement(focusId, elementId)) return
441
+ if (attempts < 12) timer = window.setTimeout(focus, 150)
442
+ return
443
+ }
444
+ if (exploreRef.current?.focusDiagram(focusId)) return
445
+ if (attempts < 12) timer = window.setTimeout(focus, 150)
446
+ }
447
+ focus()
448
+ return () => {
449
+ if (timer !== null) window.clearTimeout(timer)
450
+ }
451
+ }, [searchParams, view])
452
+
453
+ useEffect(() => {
454
+ const trimmed = searchTerm.trim()
455
+ if (trimmed.length < 3) return
456
+
457
+ const matches = buildJumpSearchResults(trimmed, flatTree, exploreSearchData)
458
+ setSearchResults(matches)
459
+ setActiveSearchIndex(matches.length > 0 ? 0 : -1)
460
+ if (view === 'hierarchy' && matches[0]) {
461
+ setFocusedHierarchyId(matches[0].viewId)
462
+ }
463
+ }, [exploreSearchData, flatTree, searchTerm, view])
464
+
414
465
  const refreshTree = useCallback(async () => {
415
466
  setTreeLoading(true)
467
+ setExploreSearchData(null)
416
468
  const tree = await api.workspace.views.tree().catch(() => null)
417
469
  if (tree) {
418
470
  setTreeData(tree)
@@ -452,17 +504,38 @@ export default function ViewsPage({ shareSlot, onShareView }: Props) {
452
504
  // eslint-disable-next-line react-hooks/exhaustive-deps
453
505
  }, [])
454
506
 
455
- const commitSearchResult = useCallback((result: ViewTreeNode) => {
507
+ useEffect(() => {
508
+ const refresh = () => {
509
+ void refreshTree()
510
+ }
511
+ window.addEventListener(WATCH_REPRESENTATION_UPDATED_EVENT, refresh)
512
+ return () => window.removeEventListener(WATCH_REPRESENTATION_UPDATED_EVENT, refresh)
513
+ }, [refreshTree])
514
+
515
+ const commitSearchResult = useCallback((result: JumpSearchResult) => {
456
516
  if (view === 'explore') {
457
- exploreRef.current?.focusDiagram(result.id)
517
+ const newParams = new URLSearchParams(searchParams)
518
+ newParams.set('view', 'explore')
519
+ newParams.set('focus', String(result.viewId))
520
+ if (result.type === 'element') {
521
+ newParams.set('element', String(result.elementId))
522
+ exploreRef.current?.focusElement(result.viewId, result.elementId)
523
+ } else {
524
+ newParams.delete('element')
525
+ exploreRef.current?.focusDiagram(result.viewId)
526
+ }
527
+ setSearchParams(newParams, { replace: true })
528
+ } else if (result.type === 'element') {
529
+ setFocusedHierarchyId(result.viewId)
530
+ navigate(`/views/${result.viewId}?element=${result.elementId}`)
458
531
  } else {
459
- setFocusedHierarchyId(result.id)
460
- navigate(`/views/${result.id}`)
532
+ setFocusedHierarchyId(result.viewId)
533
+ navigate(`/views/${result.viewId}`)
461
534
  }
462
535
  setSearchResults([])
463
536
  setActiveSearchIndex(-1)
464
537
  setSearchTerm('')
465
- }, [navigate, view])
538
+ }, [navigate, searchParams, setSearchParams, view])
466
539
 
467
540
  const handleSearchChange = useCallback((term: string) => {
468
541
  setSearchTerm(term)
@@ -471,20 +544,8 @@ export default function ViewsPage({ shareSlot, onShareView }: Props) {
471
544
  setActiveSearchIndex(-1)
472
545
  return
473
546
  }
474
-
475
- const normalized = term.trim().toLowerCase()
476
- const matches = flatTree
477
- .filter((n) => n.name.toLowerCase().includes(normalized))
478
- .slice(0, 5)
479
-
480
- setSearchResults(matches)
481
- if (matches.length > 0) {
482
- setActiveSearchIndex(0)
483
- if (view === 'hierarchy') setFocusedHierarchyId(matches[0].id)
484
- } else {
485
- setActiveSearchIndex(-1)
486
- }
487
- }, [flatTree, view])
547
+ ensureExploreSearchData()
548
+ }, [ensureExploreSearchData])
488
549
 
489
550
  const handleSearchKeyDown = useCallback((e: React.KeyboardEvent) => {
490
551
  if (e.key === 'Escape') {
@@ -498,12 +559,12 @@ export default function ViewsPage({ shareSlot, onShareView }: Props) {
498
559
  e.preventDefault()
499
560
  const nextIndex = (activeSearchIndex + 1) % searchResults.length
500
561
  setActiveSearchIndex(nextIndex)
501
- if (view === 'hierarchy') setFocusedHierarchyId(searchResults[nextIndex].id)
562
+ if (view === 'hierarchy') setFocusedHierarchyId(searchResults[nextIndex].viewId)
502
563
  } else if (e.key === 'ArrowUp') {
503
564
  e.preventDefault()
504
565
  const nextIndex = (activeSearchIndex - 1 + searchResults.length) % searchResults.length
505
566
  setActiveSearchIndex(nextIndex)
506
- if (view === 'hierarchy') setFocusedHierarchyId(searchResults[nextIndex].id)
567
+ if (view === 'hierarchy') setFocusedHierarchyId(searchResults[nextIndex].viewId)
507
568
  } else if (e.key === 'Enter' && activeSearchIndex >= 0) {
508
569
  e.preventDefault()
509
570
  commitSearchResult(searchResults[activeSearchIndex])
@@ -514,7 +575,17 @@ export default function ViewsPage({ shareSlot, onShareView }: Props) {
514
575
  if (!newName.trim()) return
515
576
  setIsCreating(true)
516
577
  try {
517
- const d = await api.workspace.views.create({ name: newName.trim() })
578
+ let d
579
+ if (treeData.length > 0) {
580
+ // Root view already exists. Create a new element in the root view to own this new diagram.
581
+ const name = newName.trim()
582
+ const element = await api.workspace.elements.create({ name })
583
+ const root = treeData[0]
584
+ await api.workspace.views.placements.add(root.id, element.id, 100, 100)
585
+ d = await api.workspace.views.create({ name, parent_view_id: element.id })
586
+ } else {
587
+ d = await api.workspace.views.create({ name: newName.trim() })
588
+ }
518
589
  await refreshTree()
519
590
  navigate(`/views/${d.id}`)
520
591
  onCreateClose()
@@ -524,7 +595,7 @@ export default function ViewsPage({ shareSlot, onShareView }: Props) {
524
595
  } finally {
525
596
  setIsCreating(false)
526
597
  }
527
- }, [navigate, newName, onCreateClose, refreshTree])
598
+ }, [navigate, newName, onCreateClose, refreshTree, treeData])
528
599
 
529
600
  if (initializing) {
530
601
  return (