@tldiagram/core-ui 2.0.4 → 2.0.6

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.
@@ -173,6 +173,7 @@ export declare function useCanvasInteractions({ viewId, canEdit, drawingMode: _d
173
173
  clientX: number;
174
174
  clientY: number;
175
175
  }) => void;
176
+ stableOnReconnectPick: (targetElementId: number) => Promise<boolean>;
176
177
  showAddingElementAt: (clientX: number, clientY: number, expandResults?: boolean, mode?: "add" | "connect", forceConnect?: boolean) => void;
177
178
  onNodesChange: (changes: NodeChange[]) => void;
178
179
  onEdgesChange: (changes: EdgeChange[]) => void;
@@ -0,0 +1,4 @@
1
+ /**
2
+ * Truncates a string to a specified length and appends an ellipsis.
3
+ */
4
+ export declare const truncate: (str: string, limit?: number) => string;
@@ -0,0 +1 @@
1
+ export {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tldiagram/core-ui",
3
- "version": "2.0.4",
3
+ "version": "2.0.6",
4
4
  "type": "module",
5
5
  "repository": {
6
6
  "type": "git",
@@ -6,7 +6,6 @@ import {
6
6
  Box,
7
7
  Button,
8
8
  CloseButton,
9
- Divider,
10
9
  FormControl,
11
10
  FormLabel,
12
11
  HStack,
@@ -1034,49 +1033,87 @@ function ElementPanel({ isOpen, onClose, element, onSave, autoSave = false, onDe
1034
1033
 
1035
1034
  {elementPanelAfterContentSlot}
1036
1035
 
1037
- {element && (onPromoteVisibility || onDemoteVisibility || onResetVisibility) && (
1038
- <Box borderTop="1px solid" borderColor="whiteAlpha.100" pt={2}>
1039
- <HStack justify="space-between" mb={2}>
1040
- <FormLabel fontSize="xs" fontWeight="bold" color="gray.400" mb={0}>DENSITY</FormLabel>
1041
- {visibilityOverrideDelta !== 0 && (
1042
- <Badge colorScheme={visibilityOverrideDelta > 0 ? 'teal' : 'orange'} variant="subtle">
1043
- {visibilityOverrideDelta > 0 ? `+${visibilityOverrideDelta}` : visibilityOverrideDelta}
1044
- </Badge>
1045
- )}
1046
- </HStack>
1047
- <HStack spacing={2}>
1048
- <Button variant="subtle" size="sm" color="teal.200" _hover={{ bg: 'teal.900', color: 'teal.100' }} onClick={() => onPromoteVisibility?.(element.id)} flex={1} isDisabled={isReadOnly}>
1049
- Promote
1050
- </Button>
1051
- <Button variant="subtle" size="sm" color="orange.200" _hover={{ bg: 'orange.900', color: 'orange.100' }} onClick={() => onDemoteVisibility?.(element.id)} flex={1} isDisabled={isReadOnly}>
1052
- Demote
1036
+ {(element && (onPromoteVisibility || onDemoteVisibility || onResetVisibility)) || (isEdit && canEdit && onMerge) ? (
1037
+ <Box borderTop="1px solid" borderColor="whiteAlpha.100" pt={4} pb={1}>
1038
+ {element && (onPromoteVisibility || onDemoteVisibility || onResetVisibility) && (
1039
+ <>
1040
+ <HStack justify="space-between" mb={2}>
1041
+ <FormLabel fontSize="xs" fontWeight="semibold" letterSpacing="wider" color="gray.500" mb={0} textTransform="uppercase">Density</FormLabel>
1042
+ {visibilityOverrideDelta !== 0 && (
1043
+ <Badge colorScheme={visibilityOverrideDelta > 0 ? 'teal' : 'orange'} variant="subtle" fontSize="xs">
1044
+ {visibilityOverrideDelta > 0 ? `+${visibilityOverrideDelta}` : visibilityOverrideDelta}
1045
+ </Badge>
1046
+ )}
1047
+ </HStack>
1048
+ <HStack spacing={2} mb={isEdit && canEdit && onMerge ? 2 : 0}>
1049
+ <Button
1050
+ variant="outline"
1051
+ size="sm"
1052
+ borderColor="teal.700"
1053
+ color="teal.300"
1054
+ _hover={{ bg: 'teal.900', borderColor: 'teal.500', color: 'teal.100' }}
1055
+ onClick={() => onPromoteVisibility?.(element.id)}
1056
+ flex={1}
1057
+ isDisabled={isReadOnly}
1058
+ >
1059
+ Promote
1060
+ </Button>
1061
+ <Button
1062
+ variant="outline"
1063
+ size="sm"
1064
+ borderColor="orange.700"
1065
+ color="orange.300"
1066
+ _hover={{ bg: 'orange.900', borderColor: 'orange.500', color: 'orange.100' }}
1067
+ onClick={() => onDemoteVisibility?.(element.id)}
1068
+ flex={1}
1069
+ isDisabled={isReadOnly}
1070
+ >
1071
+ Demote
1072
+ </Button>
1073
+ {visibilityOverrideDelta !== 0 && (
1074
+ <Button variant="ghost" size="sm" color="gray.400" _hover={{ bg: 'whiteAlpha.100', color: 'white' }} onClick={() => onResetVisibility?.(element.id)} isDisabled={isReadOnly}>
1075
+ Reset
1076
+ </Button>
1077
+ )}
1078
+ </HStack>
1079
+ </>
1080
+ )}
1081
+ {isEdit && canEdit && onMerge && (
1082
+ <Button
1083
+ variant="outline"
1084
+ size="sm"
1085
+ borderColor="teal.700"
1086
+ color="teal.300"
1087
+ _hover={{ bg: 'teal.900', borderColor: 'teal.500', color: 'teal.100' }}
1088
+ onClick={() => onMerge(element.id)}
1089
+ w="full"
1090
+ >
1091
+ Merge
1053
1092
  </Button>
1054
- {visibilityOverrideDelta !== 0 && (
1055
- <Button variant="ghost" size="sm" onClick={() => onResetVisibility?.(element.id)} isDisabled={isReadOnly}>
1056
- Reset
1057
- </Button>
1058
- )}
1059
- </HStack>
1060
- </Box>
1061
- )}
1062
-
1063
-
1064
-
1065
- {isEdit && canEdit && onMerge && (
1066
- <Box borderTop="1px solid" borderColor="whiteAlpha.100" pt={2}>
1067
- <Button variant="subtle" size="sm" color="teal.200" _hover={{ bg: 'teal.900', color: 'teal.100' }}
1068
- onClick={() => onMerge(element.id)} w="full">
1069
- Merge
1070
- </Button>
1093
+ )}
1071
1094
  </Box>
1072
- )}
1095
+ ) : null}
1073
1096
 
1074
1097
  {isEdit && canEdit && (
1075
- <HStack borderTop="1px solid" borderColor="whiteAlpha.100" pt={2} spacing={2}>
1076
- <Button variant="subtle" size="sm" color="white" _hover={{ bg: 'whiteAlpha.100' }} onClick={handleDelete} flex={1}>
1098
+ <HStack borderTop="1px solid" borderColor="whiteAlpha.100" pt={4} pb={1} spacing={2}>
1099
+ <Button
1100
+ variant="ghost"
1101
+ size="sm"
1102
+ color="gray.400"
1103
+ _hover={{ bg: 'whiteAlpha.100', color: 'white' }}
1104
+ onClick={handleDelete}
1105
+ flex={1}
1106
+ >
1077
1107
  Remove
1078
1108
  </Button>
1079
- <Button variant="subtle" size="sm" color="red.300" _hover={{ bg: 'red.900', color: 'red.100' }} onClick={confirmPermanentDelete.onOpen} flex={1}>
1109
+ <Button
1110
+ variant="ghost"
1111
+ size="sm"
1112
+ color="red.400"
1113
+ _hover={{ bg: 'red.900', color: 'red.200' }}
1114
+ onClick={confirmPermanentDelete.onOpen}
1115
+ flex={1}
1116
+ >
1080
1117
  Delete Element
1081
1118
  </Button>
1082
1119
  </HStack>
@@ -1084,24 +1121,19 @@ function ElementPanel({ isOpen, onClose, element, onSave, autoSave = false, onDe
1084
1121
  </VStack>
1085
1122
  </ScrollIndicatorWrapper>
1086
1123
 
1087
- <Divider borderColor="whiteAlpha.100" />
1088
-
1089
1124
  {/* Footer */}
1090
- <HStack px={4} py={3} justify="space-between" flexShrink={0}>
1091
-
1092
- {!autoSaveEdit && (
1093
- <HStack ml="auto">
1094
- <Button variant="ghost" size="sm" onClick={handleClose}>
1095
- Cancel
1125
+ {!autoSaveEdit && (
1126
+ <HStack px={4} py={3} justify="flex-end" flexShrink={0}>
1127
+ <Button variant="ghost" size="sm" onClick={handleClose}>
1128
+ Cancel
1129
+ </Button>
1130
+ {canEdit && (
1131
+ <Button size="sm" px={5} colorScheme="blue" onClick={handleSave} isLoading={loading}>
1132
+ Save
1096
1133
  </Button>
1097
- {canEdit && (
1098
- <Button size="sm" px={5} colorScheme="blue" onClick={handleSave} isLoading={loading}>
1099
- Save
1100
- </Button>
1101
- )}
1102
- </HStack>
1103
- )}
1104
- </HStack>
1134
+ )}
1135
+ </HStack>
1136
+ )}
1105
1137
  </SlidingPanel>
1106
1138
 
1107
1139
  <ConfirmDialog
@@ -6,6 +6,7 @@ import PanelHeader from './PanelHeader'
6
6
  import { ChevronRightIcon, NavigationIcon, TrashIcon, EditIcon } from './Icons'
7
7
  import { useViewEditorContext } from '../pages/ViewEditor/context'
8
8
  import type { Connector } from '../types'
9
+ import { truncate } from '../utils/string'
9
10
 
10
11
  interface Props {
11
12
  isOpen: boolean
@@ -83,11 +84,11 @@ export default function ProxyConnectorPanel({
83
84
  <VStack align="start" spacing={1} flex={1}>
84
85
  <HStack spacing={2}>
85
86
  <Text color="white" fontSize="sm" fontWeight="semibold" isTruncated>
86
- {leaf.source.actualElementName}
87
+ {truncate(leaf.source.actualElementName)}
87
88
  </Text>
88
89
  <Icon as={ChevronRightIcon} color="whiteAlpha.400" />
89
90
  <Text color="white" fontSize="sm" fontWeight="semibold" isTruncated>
90
- {leaf.target.actualElementName}
91
+ {truncate(leaf.target.actualElementName)}
91
92
  </Text>
92
93
  </HStack>
93
94
 
@@ -99,8 +99,6 @@ export default function SlidingPanel({
99
99
  backdropFilter="blur(24px)"
100
100
  border="1px solid"
101
101
  borderColor="whiteAlpha.100"
102
- borderTop="2px solid"
103
- borderTopColor="var(--accent)"
104
102
  rounded="xl"
105
103
  shadow="panel"
106
104
  >
@@ -1,46 +1,32 @@
1
- import { Box, FormLabel, HStack, Select, Text, Tooltip, VStack, Wrap, WrapItem } from '@chakra-ui/react'
1
+ import {
2
+ Box,
3
+ Button,
4
+ FormLabel,
5
+ HStack,
6
+ Menu,
7
+ MenuButton,
8
+ MenuItem,
9
+ MenuList,
10
+ Text,
11
+ Tooltip,
12
+ VStack,
13
+ Wrap,
14
+ WrapItem,
15
+ } from '@chakra-ui/react'
2
16
  import { ACCENT_OPTIONS, BACKGROUND_OPTIONS, ELEMENT_OPTIONS } from '../constants/colors'
3
17
  import { useTheme } from '../context/ThemeContext'
4
18
  import { useSourceEditor } from '../utils/sourceEditor'
5
- import type { SourceEditor } from '../api/client'
19
+ import { ChevronDownIcon } from '../components/Icons'
6
20
 
7
21
  export default function AppearanceSettings({ compact = false }: { compact?: boolean }) {
8
22
  const { accent, setAccent, background, setBackground, elementColor, setElementColor } = useTheme()
9
23
  const { editor, setEditor } = useSourceEditor()
10
- const swatchSize = compact ? '28px' : '32px'
24
+ const swatchSize = compact ? '21px' : '32px'
11
25
  const sectionGap = compact ? 5 : 8
12
26
 
13
27
  return (
14
28
  <VStack align="start" spacing={sectionGap} maxW={compact ? '320px' : '480px'} w="full">
15
- <Box w="full">
16
- <HStack justify="space-between" align="end" w="full" mb={compact ? 0 : 1}>
17
- <Box>
18
- <Text fontFamily="heading" fontSize={compact ? 'md' : 'lg'} fontWeight="bold" color="gray.100" mb={1}>
19
- Theme
20
- </Text>
21
- </Box>
22
- </HStack>
23
- </Box>
24
29
 
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
30
 
45
31
  <Box w="full">
46
32
  <FormLabel mb={3} fontSize={compact ? 'xs' : 'sm'} textTransform="uppercase" letterSpacing="0.12em" color="gray.400">
@@ -153,6 +139,36 @@ export default function AppearanceSettings({ compact = false }: { compact?: bool
153
139
  })}
154
140
  </Wrap>
155
141
  </Box>
142
+
143
+ <Box w="full">
144
+ <FormLabel mb={3} fontSize={compact ? 'xs' : 'sm'} textTransform="uppercase" letterSpacing="0.12em" color="gray.400">
145
+ Editor
146
+ </FormLabel>
147
+ <Menu>
148
+ <MenuButton
149
+ as={Button}
150
+ size="sm"
151
+ variant="clay"
152
+ rightIcon={<ChevronDownIcon size={12} strokeWidth={4} />}
153
+ minW="140px"
154
+ textAlign="left"
155
+ bg="whiteAlpha.100"
156
+ color="gray.100"
157
+ _hover={{ bg: 'whiteAlpha.200' }}
158
+ _active={{ bg: 'whiteAlpha.300' }}
159
+ >
160
+ {editor === 'zed' ? 'Zed' : 'VS Code'}
161
+ </MenuButton>
162
+ <MenuList>
163
+ <MenuItem onClick={() => setEditor('zed')} fontWeight={editor === 'zed' ? 'bold' : 'normal'}>
164
+ Zed
165
+ </MenuItem>
166
+ <MenuItem onClick={() => setEditor('vscode')} fontWeight={editor === 'vscode' ? 'bold' : 'normal'}>
167
+ VS Code
168
+ </MenuItem>
169
+ </MenuList>
170
+ </Menu>
171
+ </Box>
156
172
  </VStack>
157
173
  )
158
174
  }
@@ -1008,6 +1008,43 @@ export function useCanvasInteractions({
1008
1008
  document.addEventListener('pointercancel', up)
1009
1009
  }, [canEdit, clearHandleReconnectListeners, performReconnect, rfNodesRef, _rfEdgesRef, syncHandleReconnectDrag])
1010
1010
 
1011
+ const stableOnReconnectPick = useCallback(async (targetElementId: number) => {
1012
+ const picking = reconnectPickingRef.current
1013
+ if (!canEdit || !picking) return false
1014
+
1015
+ const oldConnector = _rfEdgesRef.current.find((candidate) => candidate.id === String(picking.edgeId))
1016
+ const pickedNode = rfNodesRef.current.find((node) => node.id === String(targetElementId))
1017
+ if (!oldConnector || !pickedNode) return false
1018
+
1019
+ const fixedNodeId = picking.endpoint === 'source' ? oldConnector.target : oldConnector.source
1020
+ if (fixedNodeId === pickedNode.id) return false
1021
+ const fixedNode = rfNodesRef.current.find((node) => node.id === fixedNodeId)
1022
+ if (!fixedNode) return false
1023
+
1024
+ const closest = picking.endpoint === 'source'
1025
+ ? findClosestHandles(pickedNode, fixedNode)
1026
+ : findClosestHandles(fixedNode, pickedNode)
1027
+ const newConnection: Connection = picking.endpoint === 'source'
1028
+ ? {
1029
+ source: pickedNode.id,
1030
+ sourceHandle: ensureVisualHandleId(closest.sourceHandle, DEFAULT_SOURCE_HANDLE_SIDE) ?? closest.sourceHandle,
1031
+ target: fixedNode.id,
1032
+ targetHandle: ensureVisualHandleId(closest.targetHandle, DEFAULT_TARGET_HANDLE_SIDE) ?? closest.targetHandle,
1033
+ }
1034
+ : {
1035
+ source: fixedNode.id,
1036
+ sourceHandle: ensureVisualHandleId(closest.sourceHandle, DEFAULT_SOURCE_HANDLE_SIDE) ?? closest.sourceHandle,
1037
+ target: pickedNode.id,
1038
+ targetHandle: ensureVisualHandleId(closest.targetHandle, DEFAULT_TARGET_HANDLE_SIDE) ?? closest.targetHandle,
1039
+ }
1040
+
1041
+ reconnectPickingRef.current = null
1042
+ setReconnectPicking(null)
1043
+ setConnectorLongPressMenu(null)
1044
+ await performReconnect(oldConnector, newConnection)
1045
+ return true
1046
+ }, [canEdit, _rfEdgesRef, performReconnect, rfNodesRef])
1047
+
1011
1048
  // ── Click-connect ghost cursor tracking ────────────────────────────────────
1012
1049
  useEffect(() => {
1013
1050
  if (!clickConnectMode) {
@@ -1528,6 +1565,7 @@ export function useCanvasInteractions({
1528
1565
  stableOnConnectTo,
1529
1566
  stableOnInteractionStart,
1530
1567
  stableOnStartHandleReconnect,
1568
+ stableOnReconnectPick,
1531
1569
  showAddingElementAt,
1532
1570
  // RF event handlers
1533
1571
  onNodesChange,
@@ -227,7 +227,13 @@ export function useViewData({
227
227
  queryFn: () => api.workspace.views.treeAround(viewId, { ancestorLevels: 2, descendantLevels: 2 }),
228
228
  staleTime: 0,
229
229
  }).catch(() => null)
230
- if (tree) useStore.getState().setTreeData(tree)
230
+ if (tree) {
231
+ const links = buildViewContentLinks(tree, viewId, viewElementsRef.current)
232
+ useStore.getState().setTreeData(tree)
233
+ useStore.getState().setLinksMap(links.linksMap)
234
+ useStore.getState().setParentLinksMap(links.parentLinksMap)
235
+ useStore.getState().setIncomingLinks(links.incomingLinks)
236
+ }
231
237
  }, [queryClient, viewId])
232
238
 
233
239
  // ── Fetch view content ──────────────────────────────────────────────────
@@ -281,8 +287,12 @@ export function useViewData({
281
287
  if (fresh) {
282
288
  setViewElements(fresh.placements)
283
289
  setConnectors(fresh.connectors)
290
+ const links = buildViewContentLinks(treeDataRef.current, viewId, fresh.placements)
291
+ setLinksMap(links.linksMap)
292
+ setParentLinksMap(links.parentLinksMap)
293
+ useStore.getState().setIncomingLinks(links.incomingLinks)
284
294
  }
285
- }, [queryClient, setConnectors, setViewElements, viewId])
295
+ }, [queryClient, setConnectors, setLinksMap, setParentLinksMap, setViewElements, viewId])
286
296
 
287
297
  // ── Element mutation helpers ───────────────────────────────────────────────
288
298
  const handleElementDeleted = useCallback((deletedId: number) => {
@@ -485,6 +485,7 @@ function ViewEditorInner({
485
485
  const stableOnConnectToRef = useRef<(targetElementId: number) => Promise<void>>(async () => { })
486
486
  const stableOnInteractionStartRef = useRef<(elementId: number, options?: { sourceHandle?: string; clientX?: number; clientY?: number }) => void>(() => { })
487
487
  const stableOnStartHandleReconnectRef = useRef<(args: { edgeId: string; endpoint: 'source' | 'target'; handleId: string; clientX: number; clientY: number }) => void>(() => { })
488
+ const stableOnReconnectPickRef = useRef<(targetElementId: number) => Promise<boolean>>(async () => false)
488
489
 
489
490
  // ── Drawing engine ────────────────────────────────────────────────────────
490
491
  const drawing = useDrawingEngine(viewId)
@@ -518,18 +519,21 @@ function ViewEditorInner({
518
519
  stableOnZoomOut: useCallback(async (id: number) => { await stableOnZoomOutRef.current(id) }, []),
519
520
  stableOnNavigateToView: useCallback((id: number) => { stableOnNavigateToViewRef.current(id) }, []),
520
521
  stableOnSelect: useCallback((obj: PlacedElement) => {
521
- setSelectedEdge(null)
522
- setSelectedProxyConnectorDetails(null)
523
- closeProxyConnectorPanelRef.current()
524
- closeConnectorPanelRef.current()
525
- setSelectedElement({
526
- id: obj.element_id, name: obj.name, description: obj.description, kind: obj.kind,
527
- technology: obj.technology, url: obj.url, logo_url: obj.logo_url,
528
- technology_connectors: obj.technology_connectors, tags: obj.tags, repo: obj.repo,
529
- branch: obj.branch, file_path: obj.file_path, language: obj.language,
530
- created_at: '', updated_at: '', has_view: false, view_label: null,
522
+ void stableOnReconnectPickRef.current(obj.element_id).then((handled) => {
523
+ if (handled) return
524
+ setSelectedEdge(null)
525
+ setSelectedProxyConnectorDetails(null)
526
+ closeProxyConnectorPanelRef.current()
527
+ closeConnectorPanelRef.current()
528
+ setSelectedElement({
529
+ id: obj.element_id, name: obj.name, description: obj.description, kind: obj.kind,
530
+ technology: obj.technology, url: obj.url, logo_url: obj.logo_url,
531
+ technology_connectors: obj.technology_connectors, tags: obj.tags, repo: obj.repo,
532
+ branch: obj.branch, file_path: obj.file_path, language: obj.language,
533
+ created_at: '', updated_at: '', has_view: false, view_label: null,
534
+ })
535
+ openElementPanelRef.current()
531
536
  })
532
- openElementPanelRef.current()
533
537
  }, []),
534
538
  stableOnOpenCodePreview: useCallback((elementId: number) => {
535
539
  const obj = previewViewElementsRef.current.find((o) => o.element_id === elementId)
@@ -1052,7 +1056,8 @@ function ViewEditorInner({
1052
1056
  stableOnConnectToRef.current = canvas.stableOnConnectTo
1053
1057
  stableOnInteractionStartRef.current = canvas.stableOnInteractionStart
1054
1058
  stableOnStartHandleReconnectRef.current = canvas.stableOnStartHandleReconnect
1055
- }, [canvas.stableOnZoomIn, canvas.stableOnZoomOut, canvas.stableOnNavigateToView, canvas.stableOnRemoveElement, canvas.stableOnConnectTo, canvas.stableOnInteractionStart, canvas.stableOnStartHandleReconnect])
1059
+ stableOnReconnectPickRef.current = canvas.stableOnReconnectPick
1060
+ }, [canvas.stableOnZoomIn, canvas.stableOnZoomOut, canvas.stableOnNavigateToView, canvas.stableOnRemoveElement, canvas.stableOnConnectTo, canvas.stableOnInteractionStart, canvas.stableOnStartHandleReconnect, canvas.stableOnReconnectPick])
1056
1061
  const viewName = view?.name ?? null
1057
1062
 
1058
1063
  const [expandedAncestorGroups, setExpandedAncestorGroups] = useState<Set<string>>(new Set())
@@ -1780,7 +1785,7 @@ function ViewEditorInner({
1780
1785
  menu={connectorLongPressMenu}
1781
1786
  onEdit={(edgeId) => { const connector = connectors.find((e) => e.id === edgeId); if (connector) { setSelectedEdge(connector); connectorPanel.onOpen() }; setConnectorLongPressMenu(null) }}
1782
1787
  onMoveSource={(edgeId) => { const picking = { edgeId, endpoint: 'source' as const }; reconnectPickingRef.current = picking; setReconnectPicking(picking); setConnectorLongPressMenu(null) }}
1783
- onMoveTarget={(edgeId) => { const picking = { edgeId, endpoint: 'target' as const }; reconnectPickingRef.current = picking; setConnectorLongPressMenu(null) }}
1788
+ onMoveTarget={(edgeId) => { const picking = { edgeId, endpoint: 'target' as const }; reconnectPickingRef.current = picking; setReconnectPicking(picking); setConnectorLongPressMenu(null) }}
1784
1789
  onDelete={async (edgeId) => {
1785
1790
  setConnectorLongPressMenu(null)
1786
1791
  if (!viewId) return
@@ -832,7 +832,7 @@ function ViewGridInner({ onShare, treeData, loading, focusedId, onFocusChange, s
832
832
  onStartRename: () => startEdit(n.id, n.name),
833
833
  onDetails: () => handleDetailsOpen(n.id),
834
834
  onDelete: () => { setDeleteTargetId(n.id); onDeleteOpen() },
835
- onShare: onShare ? () => onShare(n.id) : () => {},
835
+ onShare: onShare ? () => onShare(n.id) : () => { },
836
836
  onEditNameChange: setEditName,
837
837
  onEditCommit: commitEdit,
838
838
  onEditCancel: cancelEdit,
@@ -1088,7 +1088,7 @@ function ViewGridInner({ onShare, treeData, loading, focusedId, onFocusChange, s
1088
1088
  <Text color="gray.600" fontSize="sm" mb={1}>No views yet.</Text>
1089
1089
  {canEdit && (
1090
1090
  <>
1091
- <Text color="gray.700" fontSize="xs" mb={4}>Click "New Diagram" to get started.</Text>
1091
+ <Text color="gray.700" fontSize="xs" mb={4}>Click "+ New" to get started.</Text>
1092
1092
 
1093
1093
  </>
1094
1094
  )}
@@ -0,0 +1,19 @@
1
+ import { describe, expect, it } from 'vitest'
2
+ import { truncate } from './string'
3
+
4
+ describe('truncate', () => {
5
+ it('does not truncate strings shorter than or equal to the limit', () => {
6
+ expect(truncate('hello', 10)).toBe('hello')
7
+ expect(truncate('1234567890', 10)).toBe('1234567890')
8
+ })
9
+
10
+ it('truncates strings longer than the limit and adds ellipsis', () => {
11
+ expect(truncate('hello world', 5)).toBe('hello...')
12
+ expect(truncate('12345678901', 10)).toBe('1234567890...')
13
+ })
14
+
15
+ it('uses default limit of 15', () => {
16
+ expect(truncate('123456789012345')).toBe('123456789012345')
17
+ expect(truncate('1234567890123456')).toBe('123456789012345...')
18
+ })
19
+ })
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Truncates a string to a specified length and appends an ellipsis.
3
+ */
4
+ export const truncate = (str: string, limit: number = 15): string => {
5
+ if (str.length <= limit) return str
6
+ return str.slice(0, limit) + '...'
7
+ }