@tldiagram/core-ui 1.87.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 (272) hide show
  1. package/dist/App.d.ts +1 -0
  2. package/dist/api/client.d.ts +143 -0
  3. package/dist/api/transport-vscode.d.ts +8 -0
  4. package/dist/api/transport.d.ts +1 -0
  5. package/dist/components/CodePreviewPanel-vscode.d.ts +7 -0
  6. package/dist/components/CodePreviewPanel.d.ts +9 -0
  7. package/dist/components/ConfirmDialog.d.ts +12 -0
  8. package/dist/components/ConnectorPanel.d.ts +21 -0
  9. package/dist/components/ContextBoundaryElement.d.ts +11 -0
  10. package/dist/components/ContextNeighborElement.d.ts +29 -0
  11. package/dist/components/ContextStraightConnector.d.ts +4 -0
  12. package/dist/components/CrossBranchControls.d.ts +9 -0
  13. package/dist/components/DependenciesOnboarding.d.ts +5 -0
  14. package/dist/components/DrawingCanvas.d.ts +39 -0
  15. package/dist/components/ElementLibrary-vscode.d.ts +7 -0
  16. package/dist/components/ElementLibrary.d.ts +22 -0
  17. package/dist/components/ElementNode.d.ts +36 -0
  18. package/dist/components/ElementPanel.d.ts +25 -0
  19. package/dist/components/ExploreOnboarding.d.ts +5 -0
  20. package/dist/components/ExplorePageOnboarding.d.ts +5 -0
  21. package/dist/components/ExportModal.d.ts +16 -0
  22. package/dist/components/FloatingEdge.d.ts +9 -0
  23. package/dist/components/GitSourceLinker.d.ts +8 -0
  24. package/dist/components/HeaderContext.d.ts +16 -0
  25. package/dist/components/Icons.d.ts +95 -0
  26. package/dist/components/ImportModal.d.ts +10 -0
  27. package/dist/components/InlineElementAdder.d.ts +17 -0
  28. package/dist/components/LayoutSection.d.ts +7 -0
  29. package/dist/components/LocalSourceLinker.d.ts +8 -0
  30. package/dist/components/MiniZoomOnboarding.d.ts +5 -0
  31. package/dist/components/NavBreadcrumb.d.ts +6 -0
  32. package/dist/components/NodeBody.d.ts +12 -0
  33. package/dist/components/NodeContainer.d.ts +8 -0
  34. package/dist/components/NodeHoverCard.d.ts +10 -0
  35. package/dist/components/PanelHeader.d.ts +8 -0
  36. package/dist/components/PanelUI.d.ts +3 -0
  37. package/dist/components/ProxyConnectorEdge.d.ts +4 -0
  38. package/dist/components/ProxyConnectorPanel.d.ts +9 -0
  39. package/dist/components/SafeBackground.d.ts +13 -0
  40. package/dist/components/ScrollIndicatorWrapper.d.ts +8 -0
  41. package/dist/components/SetChildModal.d.ts +10 -0
  42. package/dist/components/SetParentModal.d.ts +10 -0
  43. package/dist/components/SlidingPanel.d.ts +16 -0
  44. package/dist/components/TagUpsert.d.ts +8 -0
  45. package/dist/components/TopMenuBar.d.ts +8 -0
  46. package/dist/components/ViewBezierConnector.d.ts +4 -0
  47. package/dist/components/ViewDrawMenu.d.ts +22 -0
  48. package/dist/components/ViewEditorEdgeLabelLayout.d.ts +16 -0
  49. package/dist/components/ViewEditorOnboarding.d.ts +5 -0
  50. package/dist/components/ViewExplorer/TagManager/ColorPicker.d.ts +7 -0
  51. package/dist/components/ViewExplorer/TagManager/GroupNamingPopover.d.ts +10 -0
  52. package/dist/components/ViewExplorer/TagManager/LayerItem.d.ts +27 -0
  53. package/dist/components/ViewExplorer/TagManager/TagItem.d.ts +25 -0
  54. package/dist/components/ViewExplorer/TagManager/index.d.ts +21 -0
  55. package/dist/components/ViewExplorer/ViewNavigator.d.ts +11 -0
  56. package/dist/components/ViewExplorer/ViewSearch.d.ts +8 -0
  57. package/dist/components/ViewExplorer/ViewTree.d.ts +18 -0
  58. package/dist/components/ViewExplorer/index.d.ts +31 -0
  59. package/dist/components/ViewExplorer/types.d.ts +11 -0
  60. package/dist/components/ViewExplorer/utils.d.ts +6 -0
  61. package/dist/components/ViewExplorer-vscode.d.ts +6 -0
  62. package/dist/components/ViewFloatingMenu-vscode.d.ts +27 -0
  63. package/dist/components/ViewFloatingMenu.d.ts +39 -0
  64. package/dist/components/ViewGridNode.d.ts +29 -0
  65. package/dist/components/ViewHeaderButton.d.ts +11 -0
  66. package/dist/components/ViewPanel.d.ts +18 -0
  67. package/dist/components/ViewsGridOnboarding.d.ts +5 -0
  68. package/dist/components/ZUI/ZUICanvas.d.ts +18 -0
  69. package/dist/components/ZUI/index.d.ts +2 -0
  70. package/dist/components/ZUI/layout.d.ts +18 -0
  71. package/dist/components/ZUI/proxy.d.ts +25 -0
  72. package/dist/components/ZUI/renderer.d.ts +30 -0
  73. package/dist/components/ZUI/types.d.ts +140 -0
  74. package/dist/components/ZUI/useZUIInteraction.d.ts +21 -0
  75. package/dist/config/runtime-vscode.d.ts +22 -0
  76. package/dist/config/runtime.d.ts +5 -0
  77. package/dist/constants/colors.d.ts +27 -0
  78. package/dist/constants/diagramColors.d.ts +1 -0
  79. package/dist/context/ThemeContext.d.ts +27 -0
  80. package/dist/crossBranch/graph.d.ts +13 -0
  81. package/dist/crossBranch/resolve.d.ts +22 -0
  82. package/dist/crossBranch/settings.d.ts +6 -0
  83. package/dist/crossBranch/store.d.ts +11 -0
  84. package/dist/crossBranch/types.d.ts +96 -0
  85. package/dist/demo/DemoPage.d.ts +9 -0
  86. package/dist/demo/seed.d.ts +9 -0
  87. package/dist/demo/store.d.ts +137 -0
  88. package/dist/demo/viewEditor.d.ts +26 -0
  89. package/dist/favicon.svg +35 -0
  90. package/dist/hooks/useSafeFitView.d.ts +16 -0
  91. package/dist/index.css +1 -0
  92. package/dist/index.d.ts +115 -0
  93. package/dist/index.js +19966 -0
  94. package/dist/lib/vscodeBridge-vscode.d.ts +13 -0
  95. package/dist/lib/vscodeBridge.d.ts +5 -0
  96. package/dist/logo-120.png +0 -0
  97. package/dist/logo-bw.png +0 -0
  98. package/dist/logo-bw.svg +15 -0
  99. package/dist/logo-text.svg +51 -0
  100. package/dist/logo.svg +35 -0
  101. package/dist/pages/AppearanceSettings.d.ts +3 -0
  102. package/dist/pages/Dependencies.d.ts +1 -0
  103. package/dist/pages/InfiniteZoom.d.ts +7 -0
  104. package/dist/pages/Settings.d.ts +7 -0
  105. package/dist/pages/ViewEditor/components/EditorMenus.d.ts +24 -0
  106. package/dist/pages/ViewEditor/components/EditorOverlays.d.ts +30 -0
  107. package/dist/pages/ViewEditor/components/EmptyCanvasState.d.ts +7 -0
  108. package/dist/pages/ViewEditor/context.d.ts +13 -0
  109. package/dist/pages/ViewEditor/hooks/useCanvasInteractions.d.ts +201 -0
  110. package/dist/pages/ViewEditor/hooks/useDrawingEngine.d.ts +40 -0
  111. package/dist/pages/ViewEditor/hooks/useViewContextNeighbours.d.ts +20 -0
  112. package/dist/pages/ViewEditor/hooks/useViewData.d.ts +74 -0
  113. package/dist/pages/ViewEditor/index.d.ts +8 -0
  114. package/dist/pages/ViewEditor/utils.d.ts +14 -0
  115. package/dist/pages/Views.d.ts +6 -0
  116. package/dist/pages/ViewsGrid.d.ts +6 -0
  117. package/dist/pkg/importer/mermaid.d.ts +7 -0
  118. package/dist/pkg/importer/mermaid.test.d.ts +1 -0
  119. package/dist/platform/PlatformContext.d.ts +6 -0
  120. package/dist/platform/context.d.ts +3 -0
  121. package/dist/platform/local.d.ts +2 -0
  122. package/dist/platform/types.d.ts +17 -0
  123. package/dist/slots.d.ts +67 -0
  124. package/dist/theme.d.ts +2 -0
  125. package/dist/types/index.d.ts +193 -0
  126. package/dist/types/vscode-messages.d.ts +60 -0
  127. package/dist/utils/edgeDistribution.d.ts +34 -0
  128. package/dist/utils/githubApi.d.ts +4 -0
  129. package/dist/utils/githubCache.d.ts +17 -0
  130. package/dist/utils/ids.d.ts +2 -0
  131. package/dist/utils/technologyCatalog.d.ts +15 -0
  132. package/dist/utils/toast.d.ts +15 -0
  133. package/dist/utils/treesitter.d.ts +13 -0
  134. package/dist/utils/url.d.ts +12 -0
  135. package/package.json +159 -0
  136. package/src/App.tsx +141 -0
  137. package/src/api/client.ts +618 -0
  138. package/src/api/transport-vscode.ts +28 -0
  139. package/src/api/transport.ts +7 -0
  140. package/src/assets/logo-mark.svg +31 -0
  141. package/src/assets/logo-wordmark.svg +22 -0
  142. package/src/assets/logo.svg +35 -0
  143. package/src/components/CodePreviewPanel-vscode.tsx +85 -0
  144. package/src/components/CodePreviewPanel.tsx +384 -0
  145. package/src/components/ConfirmDialog.tsx +66 -0
  146. package/src/components/ConnectorPanel.tsx +403 -0
  147. package/src/components/ContextBoundaryElement.tsx +35 -0
  148. package/src/components/ContextNeighborElement.tsx +282 -0
  149. package/src/components/ContextStraightConnector.tsx +144 -0
  150. package/src/components/CrossBranchControls.tsx +105 -0
  151. package/src/components/DependenciesOnboarding.tsx +427 -0
  152. package/src/components/DrawingCanvas.tsx +391 -0
  153. package/src/components/ElementLibrary-vscode.tsx +9 -0
  154. package/src/components/ElementLibrary.tsx +512 -0
  155. package/src/components/ElementNode.tsx +1033 -0
  156. package/src/components/ElementPanel.tsx +928 -0
  157. package/src/components/ExploreOnboarding.tsx +347 -0
  158. package/src/components/ExplorePageOnboarding.tsx +383 -0
  159. package/src/components/ExportModal.tsx +132 -0
  160. package/src/components/FloatingEdge.tsx +115 -0
  161. package/src/components/GitSourceLinker.tsx +1053 -0
  162. package/src/components/HeaderContext.tsx +30 -0
  163. package/src/components/Icons.tsx +245 -0
  164. package/src/components/ImportModal.tsx +219 -0
  165. package/src/components/InlineElementAdder.tsx +216 -0
  166. package/src/components/LayoutSection.tsx +624 -0
  167. package/src/components/LocalSourceLinker.tsx +330 -0
  168. package/src/components/MiniZoomOnboarding.tsx +78 -0
  169. package/src/components/NavBreadcrumb.tsx +24 -0
  170. package/src/components/NodeBody.tsx +89 -0
  171. package/src/components/NodeContainer.tsx +58 -0
  172. package/src/components/NodeHoverCard.tsx +135 -0
  173. package/src/components/PanelHeader.tsx +36 -0
  174. package/src/components/PanelUI.tsx +24 -0
  175. package/src/components/ProxyConnectorEdge.tsx +169 -0
  176. package/src/components/ProxyConnectorPanel.tsx +130 -0
  177. package/src/components/SafeBackground.tsx +19 -0
  178. package/src/components/ScrollIndicatorWrapper.tsx +117 -0
  179. package/src/components/SetChildModal.tsx +191 -0
  180. package/src/components/SetParentModal.tsx +187 -0
  181. package/src/components/SlidingPanel.tsx +114 -0
  182. package/src/components/TagUpsert.tsx +142 -0
  183. package/src/components/TopMenuBar.tsx +380 -0
  184. package/src/components/ViewBezierConnector.tsx +143 -0
  185. package/src/components/ViewDrawMenu.tsx +270 -0
  186. package/src/components/ViewEditorEdgeLabelLayout.ts +189 -0
  187. package/src/components/ViewEditorOnboarding.tsx +445 -0
  188. package/src/components/ViewExplorer/TagManager/ColorPicker.tsx +49 -0
  189. package/src/components/ViewExplorer/TagManager/GroupNamingPopover.tsx +96 -0
  190. package/src/components/ViewExplorer/TagManager/LayerItem.tsx +228 -0
  191. package/src/components/ViewExplorer/TagManager/TagItem.tsx +242 -0
  192. package/src/components/ViewExplorer/TagManager/index.tsx +418 -0
  193. package/src/components/ViewExplorer/ViewNavigator.tsx +121 -0
  194. package/src/components/ViewExplorer/ViewSearch.tsx +33 -0
  195. package/src/components/ViewExplorer/ViewTree.tsx +98 -0
  196. package/src/components/ViewExplorer/index.tsx +384 -0
  197. package/src/components/ViewExplorer/types.ts +13 -0
  198. package/src/components/ViewExplorer/utils.ts +56 -0
  199. package/src/components/ViewExplorer-vscode.tsx +8 -0
  200. package/src/components/ViewFloatingMenu-vscode.tsx +248 -0
  201. package/src/components/ViewFloatingMenu.tsx +379 -0
  202. package/src/components/ViewGridNode.tsx +451 -0
  203. package/src/components/ViewHeaderButton.tsx +60 -0
  204. package/src/components/ViewPanel.tsx +162 -0
  205. package/src/components/ViewsGridOnboarding.tsx +400 -0
  206. package/src/components/ZUI/ZUICanvas.tsx +853 -0
  207. package/src/components/ZUI/index.ts +3 -0
  208. package/src/components/ZUI/layout.ts +323 -0
  209. package/src/components/ZUI/proxy.ts +278 -0
  210. package/src/components/ZUI/renderer.ts +1189 -0
  211. package/src/components/ZUI/types.ts +150 -0
  212. package/src/components/ZUI/useZUIInteraction.ts +720 -0
  213. package/src/config/runtime-vscode.ts +46 -0
  214. package/src/config/runtime.ts +30 -0
  215. package/src/constants/colors.ts +80 -0
  216. package/src/constants/diagramColors.ts +9 -0
  217. package/src/context/ThemeContext.tsx +158 -0
  218. package/src/crossBranch/graph.ts +207 -0
  219. package/src/crossBranch/resolve.ts +643 -0
  220. package/src/crossBranch/settings.ts +59 -0
  221. package/src/crossBranch/store.ts +71 -0
  222. package/src/crossBranch/types.ts +102 -0
  223. package/src/demo/DemoPage.tsx +184 -0
  224. package/src/demo/seed.ts +67 -0
  225. package/src/demo/store.ts +536 -0
  226. package/src/demo/viewEditor.ts +110 -0
  227. package/src/hooks/useSafeFitView.ts +60 -0
  228. package/src/index.css +309 -0
  229. package/src/index.ts +184 -0
  230. package/src/kafka-ss.png +0 -0
  231. package/src/lib/vscodeBridge-vscode.ts +27 -0
  232. package/src/lib/vscodeBridge.ts +7 -0
  233. package/src/main.tsx +46 -0
  234. package/src/pages/AppearanceSettings.tsx +135 -0
  235. package/src/pages/Dependencies.tsx +926 -0
  236. package/src/pages/InfiniteZoom.tsx +404 -0
  237. package/src/pages/Settings.tsx +91 -0
  238. package/src/pages/ViewEditor/EDGE_DISTRIBUTION.md +64 -0
  239. package/src/pages/ViewEditor/components/EditorMenus.tsx +112 -0
  240. package/src/pages/ViewEditor/components/EditorOverlays.tsx +172 -0
  241. package/src/pages/ViewEditor/components/EmptyCanvasState.tsx +42 -0
  242. package/src/pages/ViewEditor/context.tsx +21 -0
  243. package/src/pages/ViewEditor/hooks/useCanvasInteractions.ts +1349 -0
  244. package/src/pages/ViewEditor/hooks/useDrawingEngine.ts +127 -0
  245. package/src/pages/ViewEditor/hooks/useViewContextNeighbours.ts +501 -0
  246. package/src/pages/ViewEditor/hooks/useViewData.ts +491 -0
  247. package/src/pages/ViewEditor/index.tsx +1366 -0
  248. package/src/pages/ViewEditor/utils.ts +88 -0
  249. package/src/pages/Views.tsx +171 -0
  250. package/src/pages/ViewsGrid.tsx +1310 -0
  251. package/src/pkg/importer/mermaid.test.ts +141 -0
  252. package/src/pkg/importer/mermaid.ts +76 -0
  253. package/src/platform/PlatformContext.tsx +17 -0
  254. package/src/platform/context.ts +9 -0
  255. package/src/platform/local.tsx +15 -0
  256. package/src/platform/types.ts +19 -0
  257. package/src/slots.ts +92 -0
  258. package/src/styles/editor-panels.css +66 -0
  259. package/src/styles/theme.css +56 -0
  260. package/src/theme.ts +336 -0
  261. package/src/types/index.ts +234 -0
  262. package/src/types/offline-ambient.d.ts +14 -0
  263. package/src/types/vscode-messages.ts +32 -0
  264. package/src/utils/edgeDistribution.ts +103 -0
  265. package/src/utils/githubApi.ts +121 -0
  266. package/src/utils/githubCache.ts +108 -0
  267. package/src/utils/ids.ts +9 -0
  268. package/src/utils/technologyCatalog.ts +143 -0
  269. package/src/utils/toast.ts +100 -0
  270. package/src/utils/treesitter.ts +147 -0
  271. package/src/utils/url.ts +72 -0
  272. package/src/vite-env.d.ts +1 -0
@@ -0,0 +1,1053 @@
1
+ import { useState, useEffect, useRef, useMemo } from 'react'
2
+ import { createPortal } from 'react-dom'
3
+ import { AnimatePresence, motion } from 'framer-motion'
4
+ import {
5
+ Alert,
6
+ AlertDescription,
7
+ AlertIcon,
8
+ Badge,
9
+ Box,
10
+ Button,
11
+ CloseButton,
12
+ FormControl,
13
+ FormLabel,
14
+ HStack,
15
+ Input,
16
+ InputGroup,
17
+ InputRightElement,
18
+ Progress,
19
+ Spinner,
20
+ Text,
21
+ Tooltip,
22
+ VStack,
23
+ useToast,
24
+ Accordion,
25
+ AccordionItem,
26
+ AccordionButton,
27
+ AccordionPanel,
28
+ AccordionIcon,
29
+ Portal,
30
+ Popover,
31
+ PopoverTrigger,
32
+ PopoverContent,
33
+ PopoverBody,
34
+ } from '@chakra-ui/react'
35
+ import { CheckIcon, ChevronRightIcon, ExternalLinkIcon, SearchIcon } from '@chakra-ui/icons'
36
+ import { getParser, extractSymbols, detectLanguage, type SupportedLanguage, type ParsedSymbol } from '../utils/treesitter'
37
+ import { githubCache } from '../utils/githubCache'
38
+ import { parseRepoSlug } from '../utils/url'
39
+ import { githubRequest } from '../utils/githubApi'
40
+ import type { LibraryElement } from '../types'
41
+
42
+ interface Props {
43
+ element: LibraryElement
44
+ isReadOnly: boolean
45
+ onUpdate: (updates: Partial<LibraryElement>) => void
46
+ }
47
+
48
+ const SUPPORTED_LANGUAGES: SupportedLanguage[] = ['javascript', 'typescript', 'python', 'java', 'cpp', 'go', 'rust']
49
+
50
+ function parseExistingLink(element: LibraryElement) {
51
+ const fp = element.file_path || ''
52
+ const hashIdx = fp.indexOf('#')
53
+ const basePath = hashIdx >= 0 ? fp.slice(0, hashIdx) : fp
54
+ const anchorStr = hashIdx >= 0 ? fp.slice(hashIdx + 1) : ''
55
+ let symbolName = ''
56
+ let pickedLine: number | null = null
57
+ if (anchorStr) {
58
+ try {
59
+ const p = JSON.parse(anchorStr)
60
+ if (p.name) symbolName = p.name
61
+ if (p.startLine) pickedLine = p.startLine
62
+ } catch { /* intentionally empty */ }
63
+ }
64
+ return { basePath, symbolName, pickedLine }
65
+ }
66
+
67
+ const STEP_LABELS = ['Repo', 'Branch', 'File', 'Symbol']
68
+
69
+ function StepIndicator({ step }: { step: number }) {
70
+ return (
71
+ <Box mb={4} position="relative" w="full">
72
+ {/* Static background line spanning all circles */}
73
+ <Box position="absolute" top="9px" left="12.5%" right="12.5%" h="1px" bg="whiteAlpha.100" zIndex={0} />
74
+ {/* Completed portion */}
75
+ <Box
76
+ position="absolute" top="9px" left="12.5%"
77
+ w={step > 1 ? `${((step - 1) / 3) * 75}%` : '0%'}
78
+ h="1px" bg="blue.700" zIndex={0} transition="width 0.3s"
79
+ />
80
+ <HStack spacing={0} w="full" position="relative" zIndex={1}>
81
+ {STEP_LABELS.map((label, i) => {
82
+ const n = i + 1
83
+ const isCompleted = step > n
84
+ const isActive = step === n
85
+ return (
86
+ <VStack key={n} flex={1} spacing={1} align="center">
87
+ <Box
88
+ w="20px" h="20px" rounded="full"
89
+ display="flex" alignItems="center" justifyContent="center"
90
+ fontSize="10px" fontWeight="bold"
91
+ bg={isCompleted ? 'blue.500' : isActive ? 'var(--accent)' : 'whiteAlpha.150'}
92
+ color={isCompleted || isActive ? 'white' : 'gray.500'}
93
+ boxShadow={isActive ? '0 0 0 3px rgba(99,179,237,0.25)' : 'none'}
94
+ transition="all 0.2s"
95
+ >
96
+ {isCompleted ? <CheckIcon w={2.5} h={2.5} /> : n}
97
+ </Box>
98
+ <Text
99
+ fontSize="9px" textAlign="center" textTransform="uppercase"
100
+ color={isCompleted ? 'blue.300' : isActive ? 'white' : 'gray.600'}
101
+ fontWeight={isActive ? '600' : '400'}
102
+ >
103
+ {label}
104
+ </Text>
105
+ </VStack>
106
+ )
107
+ })}
108
+ </HStack>
109
+ </Box>
110
+ )
111
+ }
112
+
113
+ // Scrollable code line renderer - fills its parent container
114
+ interface CodeLinesProps {
115
+ code: string
116
+ highlightStart?: number | null
117
+ highlightEnd?: number | null
118
+ selectedLine?: number | null
119
+ searchQuery?: string
120
+ onLineClick?: (line: number) => void
121
+ }
122
+
123
+ function CodeLines({ code, highlightStart, highlightEnd, selectedLine, searchQuery, onLineClick }: CodeLinesProps) {
124
+ const containerRef = useRef<HTMLDivElement>(null)
125
+ const lines = useMemo(() => code.split('\n'), [code])
126
+
127
+ const matchingLines = useMemo(() => {
128
+ if (!searchQuery?.trim()) return new Set<number>()
129
+ const q = searchQuery.toLowerCase()
130
+ const set = new Set<number>()
131
+ lines.forEach((l, i) => { if (l.toLowerCase().includes(q)) set.add(i + 1) })
132
+ return set
133
+ }, [lines, searchQuery])
134
+
135
+ const firstMatch = useMemo(() => Array.from(matchingLines)[0] ?? null, [matchingLines])
136
+
137
+ useEffect(() => {
138
+ if (!containerRef.current) return
139
+ const target = highlightStart ?? firstMatch
140
+ if (!target) return
141
+ const el = containerRef.current.querySelector(`[data-line="${target}"]`)
142
+ el?.scrollIntoView({ block: 'center', behavior: 'smooth' })
143
+ }, [highlightStart, firstMatch])
144
+
145
+ return (
146
+ <Box
147
+ ref={containerRef}
148
+ flex={1} overflowY="auto"
149
+ fontFamily="'Menlo', 'Monaco', 'Consolas', monospace" fontSize="11px" lineHeight="1.55"
150
+ bg="#111827"
151
+ className="custom-scrollbar"
152
+ >
153
+ {lines.map((line, i) => {
154
+ const lineNum = i + 1
155
+ const inHighlight = highlightStart != null && highlightEnd != null
156
+ ? lineNum >= highlightStart && lineNum <= highlightEnd
157
+ : false
158
+ const isSelected = selectedLine === lineNum
159
+ const isMatch = matchingLines.has(lineNum)
160
+
161
+ return (
162
+ <Box
163
+ key={i}
164
+ data-line={lineNum}
165
+ display="flex"
166
+ alignItems="stretch"
167
+ bg={isSelected ? 'rgba(49,130,206,0.35)' : inHighlight ? 'rgba(49,130,206,0.12)' : isMatch ? 'rgba(236,201,75,0.06)' : 'transparent'}
168
+ borderLeft="2px solid"
169
+ borderColor={isSelected ? 'blue.400' : inHighlight ? 'blue.800' : 'transparent'}
170
+ cursor={onLineClick ? 'pointer' : 'default'}
171
+ _hover={onLineClick ? { bg: isSelected ? 'rgba(49,130,206,0.45)' : 'whiteAlpha.50' } : {}}
172
+ onClick={() => onLineClick?.(lineNum)}
173
+ transition="background 0.1s"
174
+ >
175
+ <Box
176
+ w="38px" flexShrink={0}
177
+ px={1.5} py={0.5}
178
+ color={isSelected ? 'blue.300' : inHighlight ? 'blue.600' : isMatch ? 'yellow.500' : 'gray.700'}
179
+ fontSize="10px" textAlign="right"
180
+ userSelect="none"
181
+ borderRight="1px solid" borderColor="whiteAlpha.50"
182
+ >
183
+ {lineNum}
184
+ </Box>
185
+ <Box
186
+ px={2} py={0.5}
187
+ color={isSelected ? 'white' : inHighlight ? 'blue.100' : isMatch ? 'yellow.200' : 'gray.400'}
188
+ whiteSpace="pre" flex={1} overflow="hidden" textOverflow="ellipsis"
189
+ >
190
+ {line || '\u00a0'}
191
+ </Box>
192
+ </Box>
193
+ )
194
+ })}
195
+ </Box>
196
+ )
197
+ }
198
+
199
+ // Floating preview card - renders via portal, positioned to the left of the right ElementPanel
200
+ interface PreviewCardProps {
201
+ filename: string
202
+ isLoading: boolean
203
+ rawCode: string
204
+ highlightStart?: number | null
205
+ highlightEnd?: number | null
206
+ selectedLine?: number | null
207
+ searchQuery?: string
208
+ onLineClick?: (line: number) => void
209
+ isUnsupported: boolean
210
+ onSearchChange?: (q: string) => void
211
+ }
212
+
213
+ function PreviewCard({
214
+ filename, isLoading, rawCode,
215
+ highlightStart, highlightEnd,
216
+ selectedLine, searchQuery, onLineClick,
217
+ isUnsupported, onSearchChange,
218
+ }: PreviewCardProps) {
219
+ const EASE = [0.25, 0.46, 0.45, 0.94]
220
+
221
+ return createPortal(
222
+ <AnimatePresence>
223
+ <motion.div
224
+ key="git-preview-card"
225
+ initial={{ x: 32, opacity: 0 }}
226
+ animate={{ x: 0, opacity: 1 }}
227
+ exit={{ x: 32, opacity: 0 }}
228
+ transition={{ duration: 0.2, ease: EASE }}
229
+ style={{
230
+ position: 'fixed',
231
+ right: 'calc(300px + 1rem + 12px)',
232
+ top: 0,
233
+ bottom: 0,
234
+ display: 'flex',
235
+ alignItems: 'center',
236
+ zIndex: 999,
237
+ pointerEvents: 'none',
238
+ }}
239
+ >
240
+ <Box
241
+ pointerEvents="auto"
242
+ w="420px"
243
+ h="calc(90vh - 2rem)"
244
+ maxH="calc(90vh - 2rem)"
245
+ display="flex"
246
+ flexDir="column"
247
+ bg="var(--bg-panel)"
248
+ bgImage="var(--grad-panel)"
249
+ backdropFilter="blur(24px)"
250
+ border="1px solid"
251
+ borderColor="whiteAlpha.100"
252
+ borderTop="2px solid"
253
+ borderTopColor="var(--accent)"
254
+ rounded="xl"
255
+ shadow="panel"
256
+ overflow="hidden"
257
+ >
258
+ {/* Header */}
259
+ <Box px={4} pt={4} pb={3} borderBottom="1px solid" borderColor="whiteAlpha.100" flexShrink={0}>
260
+ <Text fontSize="10px" color="gray.600" letterSpacing="widest" fontWeight="600" mb={0.5}>
261
+ Preview
262
+ </Text>
263
+ <Text fontSize="sm" fontWeight="700" color="white" isTruncated letterSpacing="0.01em">
264
+ {filename}
265
+ </Text>
266
+ {highlightStart != null && !isUnsupported && (
267
+ <Text fontSize="10px" color="blue.400" fontFamily="mono" mt={0.5}>
268
+ L{highlightStart}–L{highlightEnd ?? highlightStart}
269
+ </Text>
270
+ )}
271
+ {isUnsupported && (
272
+ <InputGroup size="sm" mt={2.5}>
273
+ <InputRightElement pointerEvents="none">
274
+ <SearchIcon color="gray.600" boxSize={3} />
275
+ </InputRightElement>
276
+ <Input
277
+ value={searchQuery || ''}
278
+ onChange={e => onSearchChange?.(e.target.value)}
279
+ placeholder="Search in file..."
280
+ pr={8}
281
+ bg="whiteAlpha.50"
282
+ border="1px solid"
283
+ borderColor="whiteAlpha.150"
284
+ _focus={{ borderColor: 'blue.500', bg: 'whiteAlpha.100' }}
285
+ />
286
+ </InputGroup>
287
+ )}
288
+ </Box>
289
+
290
+ {/* Body */}
291
+ <Box flex={1} overflow="hidden" display="flex" flexDir="column" position="relative">
292
+ {isLoading ? (
293
+ <VStack flex={1} justify="center" align="center" spacing={3}>
294
+ <Spinner color="blue.400" size="md" />
295
+ <Text fontSize="xs" color="gray.500">
296
+ {isUnsupported ? 'Fetching file...' : 'Fetching & parsing symbols...'}
297
+ </Text>
298
+ </VStack>
299
+ ) : rawCode ? (
300
+ <CodeLines
301
+ code={rawCode}
302
+ highlightStart={highlightStart}
303
+ highlightEnd={highlightEnd}
304
+ selectedLine={selectedLine}
305
+ searchQuery={searchQuery}
306
+ onLineClick={onLineClick}
307
+ />
308
+ ) : (
309
+ <VStack flex={1} justify="center" align="center">
310
+ <Text fontSize="xs" color="gray.600">No preview available</Text>
311
+ </VStack>
312
+ )}
313
+ </Box>
314
+ </Box>
315
+ </motion.div>
316
+ </AnimatePresence>,
317
+ document.body
318
+ )
319
+ }
320
+
321
+ export default function GitSourceLinker({ element, isReadOnly, onUpdate }: Props) {
322
+ const { basePath: initBasePath, symbolName: initSymbolName, pickedLine: initPickedLine } = parseExistingLink(element)
323
+
324
+ const hasExistingLink = !!(element.repo && element.file_path)
325
+ const [mode, setMode] = useState<'summary' | 'edit'>(hasExistingLink ? 'summary' : 'edit')
326
+
327
+ const [step, setStep] = useState<1 | 2 | 3 | 4>(1)
328
+
329
+ // Step 1
330
+ const [repo, setRepo] = useState(element.repo || '')
331
+
332
+ // Step 2
333
+ const [branch, setBranch] = useState(element.branch || '')
334
+ const [branches, setBranches] = useState<string[]>([])
335
+ const [branchSearch, setBranchSearch] = useState('')
336
+ const [branchOpen, setBranchOpen] = useState(false)
337
+ const [branchLoading, setBranchLoading] = useState(false)
338
+ const [branchActiveIndex, setBranchActiveIndex] = useState(0)
339
+ const branchRef = useRef<HTMLDivElement>(null)
340
+ const branchInputRef = useRef<HTMLInputElement>(null)
341
+
342
+ // Step 3
343
+ const [filePath, setFilePath] = useState(initBasePath)
344
+ const [fileSearch, setFileSearch] = useState(initBasePath)
345
+ const [fileTree, setFileTree] = useState<string[]>([])
346
+ const [fileOpen, setFileOpen] = useState(false)
347
+ const [fileTreeLoading, setFileTreeLoading] = useState(false)
348
+ const [fileActiveIndex, setFileActiveIndex] = useState(0)
349
+ const [language, setLanguage] = useState(element.language || '')
350
+ const fileRef = useRef<HTMLDivElement>(null)
351
+ const fileInputRef = useRef<HTMLInputElement>(null)
352
+
353
+ // Step 4: symbols
354
+ const [symbols, setSymbols] = useState<ParsedSymbol[]>([])
355
+ const [selectedSymbol, setSelectedSymbol] = useState<{ name: string; type: string } | null>(
356
+ initSymbolName ? { name: initSymbolName, type: '' } : null
357
+ )
358
+ const [symbolLoading, setSymbolLoading] = useState(false)
359
+
360
+ // Step 4: code preview (shared)
361
+ const [rawCode, setRawCode] = useState('')
362
+ const [rawCodeLoading, setRawCodeLoading] = useState(false)
363
+
364
+ // Step 4b: line picker
365
+ const [pickedLine, setPickedLine] = useState<number | null>(initPickedLine)
366
+ const [lineSearch, setLineSearch] = useState('')
367
+
368
+ const [apiRateLimited, setApiRateLimited] = useState(false)
369
+ const toast = useToast()
370
+
371
+ useEffect(() => {
372
+ const hasLink = !!(element.repo && element.file_path)
373
+
374
+ // Only force a reset if we are not currently editing, or if the link changed from underneath us
375
+ if (mode === 'summary' || (element.repo !== repo && element.file_path !== filePath)) {
376
+ setMode(hasLink ? 'summary' : 'edit')
377
+ if (hasLink) {
378
+ const { basePath, symbolName, pickedLine: pl } = parseExistingLink(element)
379
+ setStep(1)
380
+ setRepo(element.repo || '')
381
+ setBranch(element.branch || '')
382
+ setFilePath(basePath)
383
+ setFileSearch(basePath)
384
+ setLanguage(element.language || '')
385
+ setSelectedSymbol(symbolName ? { name: symbolName, type: '' } : null)
386
+ setPickedLine(pl)
387
+ setBranches([])
388
+ setFileTree([])
389
+ setApiRateLimited(false)
390
+ setSymbols([])
391
+ setRawCode('')
392
+ setLineSearch('')
393
+ }
394
+ }
395
+ }, [element, filePath, mode, repo])
396
+
397
+ useEffect(() => {
398
+ setBranchActiveIndex(0)
399
+ }, [branchSearch, branches])
400
+
401
+ useEffect(() => {
402
+ setFileActiveIndex(0)
403
+ }, [fileSearch, fileTree])
404
+
405
+ useEffect(() => {
406
+ if (step === 2) {
407
+ setTimeout(() => branchInputRef.current?.focus(), 100)
408
+ } else if (step === 3) {
409
+ setTimeout(() => fileInputRef.current?.focus(), 100)
410
+ }
411
+ }, [step])
412
+
413
+ const filteredBranches = branches.filter(b => b.toLowerCase().includes(branchSearch.toLowerCase()))
414
+ const filteredFiles = fileTree.filter(p => p.toLowerCase().includes(fileSearch.toLowerCase())).slice(0, 100)
415
+ const isSupported = SUPPORTED_LANGUAGES.includes(language as SupportedLanguage)
416
+
417
+ // Find full symbol data (with line numbers) for the currently selected symbol
418
+ const selectedSymbolData = selectedSymbol
419
+ ? symbols.find(s => s.name === selectedSymbol.name && s.type === selectedSymbol.type) ?? null
420
+ : null
421
+
422
+ async function fetchBranches(repoSlug: string) {
423
+ const cached = githubCache.getBranches(repoSlug)
424
+ if (cached) {
425
+ setBranches(cached)
426
+ if (!branch) {
427
+ const def = cached.find(n => n === 'main') ?? cached.find(n => n === 'master')
428
+ if (def) setBranch(def)
429
+ }
430
+ setBranchOpen(true)
431
+ return
432
+ }
433
+
434
+ setBranchLoading(true)
435
+ setApiRateLimited(false)
436
+ try {
437
+ const res = await githubRequest(`/repos/${repoSlug}/branches?per_page=100`)
438
+ if (res.status === 403 || res.status === 429) { setApiRateLimited(true); return }
439
+ if (!res.ok) throw new Error(res.statusText)
440
+ const data: { name: string }[] = await res.json()
441
+ const names = data.map(b => b.name)
442
+ githubCache.setBranches(repoSlug, names)
443
+ setBranches(names)
444
+ if (!branch) {
445
+ const def = names.find(n => n === 'main') ?? names.find(n => n === 'master')
446
+ if (def) setBranch(def)
447
+ }
448
+ setBranchOpen(true) // Auto-open dropdown once fetched
449
+ } catch {
450
+ setApiRateLimited(true)
451
+ } finally {
452
+ setBranchLoading(false)
453
+ }
454
+ }
455
+
456
+ const [fileTreeTruncated, setFileTreeTruncated] = useState(false)
457
+
458
+ async function fetchFileTree(repoSlug: string, branchName: string) {
459
+ const cached = githubCache.getTree(repoSlug, branchName)
460
+ if (cached) {
461
+ setFileTree(cached)
462
+ setFileOpen(true)
463
+ return
464
+ }
465
+
466
+ setFileTreeLoading(true)
467
+ setApiRateLimited(false)
468
+ setFileTreeTruncated(false)
469
+ try {
470
+ const res = await githubRequest(`/repos/${repoSlug}/git/trees/${branchName}?recursive=1`)
471
+ if (res.status === 403 || res.status === 429) { setApiRateLimited(true); return }
472
+ if (!res.ok) throw new Error(res.statusText)
473
+ const data: { tree: { path: string; type: string }[]; truncated?: boolean } = await res.json()
474
+ const files = data.tree.filter(i => i.type === 'blob').map(i => i.path)
475
+ githubCache.setTree(repoSlug, branchName, files)
476
+ setFileTree(files)
477
+ if (data.truncated) setFileTreeTruncated(true)
478
+ setFileOpen(true)
479
+ } catch {
480
+ setApiRateLimited(true)
481
+ } finally {
482
+ setFileTreeLoading(false)
483
+ }
484
+ }
485
+
486
+ async function fetchAndParseSymbols(lang: SupportedLanguage) {
487
+ setSymbolLoading(true)
488
+ setSymbols([])
489
+ setRawCode('')
490
+ try {
491
+ const rawUrl = `https://raw.githubusercontent.com/${repo}/refs/heads/${branch}/${filePath}`
492
+ let source = githubCache.getContent(rawUrl)
493
+ if (!source) {
494
+ const res = await fetch(rawUrl)
495
+ if (!res.ok) throw new Error(`Failed to fetch: ${res.statusText}`)
496
+ source = await res.text()
497
+ githubCache.setContent(rawUrl, source)
498
+ }
499
+ setRawCode(source)
500
+ const parser = await getParser(lang)
501
+ const tree = parser.parse(source)
502
+ const extracted = extractSymbols(tree, lang)
503
+ setSymbols(extracted)
504
+ if (extracted.length === 0) {
505
+ toast({ title: 'No symbols found in this file', status: 'info', duration: 3000 })
506
+ }
507
+ } catch (err: unknown) {
508
+ toast({ title: 'Failed to fetch/parse file', description: err instanceof Error ? err.message : String(err), status: 'error', duration: 4000 })
509
+ } finally {
510
+ setSymbolLoading(false)
511
+ }
512
+ }
513
+
514
+ async function fetchRawCode() {
515
+ setRawCodeLoading(true)
516
+ setRawCode('')
517
+ try {
518
+ const rawUrl = `https://raw.githubusercontent.com/${repo}/refs/heads/${branch}/${filePath}`
519
+ const cached = githubCache.getContent(rawUrl)
520
+ if (cached) {
521
+ setRawCode(cached)
522
+ } else {
523
+ const res = await fetch(rawUrl)
524
+ if (!res.ok) throw new Error(`Failed to fetch: ${res.statusText}`)
525
+ const text = await res.text()
526
+ githubCache.setContent(rawUrl, text)
527
+ setRawCode(text)
528
+ }
529
+ } catch (err: unknown) {
530
+ toast({ title: 'Failed to fetch file', description: err instanceof Error ? err.message : String(err), status: 'error', duration: 4000 })
531
+ } finally {
532
+ setRawCodeLoading(false)
533
+ }
534
+ }
535
+
536
+ function advanceTo(nextStep: 1 | 2 | 3 | 4) {
537
+ setStep(nextStep)
538
+ if (nextStep === 2) {
539
+ // Normalize repo slug before fetching branches
540
+ const slug = parseRepoSlug(repo)
541
+ setRepo(slug)
542
+
543
+ // Avoid refetching if we already have branches for this repo
544
+ if (branches.length === 0 || slug !== (element.repo ? parseRepoSlug(element.repo) : '')) {
545
+ fetchBranches(slug)
546
+ } else {
547
+ setBranchOpen(true)
548
+ }
549
+ }
550
+ if (nextStep === 3) {
551
+ // Avoid refetching if we already have the tree for this repo/branch
552
+ const slug = parseRepoSlug(repo)
553
+ if (fileTree.length === 0 || slug !== (element.repo ? parseRepoSlug(element.repo) : '') || branch !== element.branch) {
554
+ fetchFileTree(slug, branch)
555
+ } else {
556
+ setFileOpen(true)
557
+ }
558
+ }
559
+ if (nextStep === 4) {
560
+ const detected = detectLanguage(filePath)
561
+ const lang = detected ?? 'unsupported'
562
+ setLanguage(lang)
563
+ if (SUPPORTED_LANGUAGES.includes(lang as SupportedLanguage)) {
564
+ fetchAndParseSymbols(lang as SupportedLanguage)
565
+ } else {
566
+ fetchRawCode()
567
+ }
568
+ }
569
+ }
570
+
571
+ function handleFileSelect(path: string) {
572
+ setFilePath(path)
573
+ setFileSearch(path)
574
+ setFileOpen(false)
575
+ setSelectedSymbol(null)
576
+ setSymbols([])
577
+ setRawCode('')
578
+ setPickedLine(null)
579
+ }
580
+
581
+ function buildFilePath(): string {
582
+ if (isSupported && selectedSymbol) {
583
+ return `${filePath}#${JSON.stringify({ name: selectedSymbol.name, type: selectedSymbol.type })}`
584
+ }
585
+ if (!isSupported && pickedLine) {
586
+ return `${filePath}#${JSON.stringify({ startLine: pickedLine, endLine: pickedLine })}`
587
+ }
588
+ return filePath
589
+ }
590
+
591
+ function handleApply() {
592
+ onUpdate({
593
+ repo: parseRepoSlug(repo),
594
+ branch,
595
+ file_path: buildFilePath(),
596
+ language: isSupported ? language : undefined,
597
+ })
598
+ setMode('summary')
599
+ }
600
+
601
+ function handleRemoveLink() {
602
+ onUpdate({ repo: null, branch: null, file_path: null, language: null })
603
+ // Explicitly reset local state to ensure immediate UI update
604
+ setRepo('')
605
+ setBranch('')
606
+ setFilePath('')
607
+ setFileSearch('')
608
+ setLanguage('')
609
+ setSymbols([])
610
+ setRawCode('')
611
+ setPickedLine(null)
612
+ setSelectedSymbol(null)
613
+ setMode('edit')
614
+ setStep(1)
615
+ }
616
+
617
+ const repoValid = /^[\w.-]+\/[\w.-]+$/.test(parseRepoSlug(repo))
618
+ const showPreviewCard = mode === 'edit' && step === 4
619
+
620
+ // --- RENDER ---
621
+ return (
622
+ <Box borderTop="1px solid" borderColor="whiteAlpha.100" pt={1} overflow="visible">
623
+ <Accordion allowToggle overflow="visible">
624
+ <AccordionItem border="none" overflow="visible">
625
+ <h2>
626
+ <AccordionButton px={0} py={3} _hover={{ bg: 'transparent' }}>
627
+ <HStack flex="1" textAlign="left" spacing={2}>
628
+ <Text fontSize="sm" fontFamily="var(--chakra-fonts-heading)" >
629
+ Git Source
630
+ </Text>
631
+ {hasExistingLink && mode === 'summary' && (
632
+ <Badge variant="subtle" colorScheme="blue" fontSize="9px" ml={1} px={1.5}>
633
+ Linked
634
+ </Badge>
635
+ )}
636
+ </HStack>
637
+ <AccordionIcon color="gray.500" />
638
+ </AccordionButton>
639
+ </h2>
640
+ <AccordionPanel pb={4} px={0} overflow="visible">
641
+ {mode === 'summary' ? (
642
+ <VStack align="stretch" spacing={2}>
643
+ <VStack align="stretch" spacing={1.5}
644
+ bg="whiteAlpha.50" rounded="lg" px={3} py={2.5}
645
+ border="1px solid" borderColor="whiteAlpha.100">
646
+ <HStack justify="space-between">
647
+ <HStack spacing={2} minW={0}>
648
+ <Text fontSize="xs" color="gray.500" flexShrink={0}>Repo</Text>
649
+ <Text fontSize="xs" color="white" fontFamily="mono" isTruncated>{element.repo ? parseRepoSlug(element.repo) : ''}</Text>
650
+ </HStack>
651
+ {!isReadOnly && (
652
+ <HStack spacing={1}>
653
+ <Button size="xs" variant="ghost" color="gray.500" h="20px" _hover={{ color: 'white', bg: 'whiteAlpha.100' }}
654
+ onClick={(e) => { e.stopPropagation(); setStep(1); setMode('edit') }}>
655
+ Edit
656
+ </Button>
657
+ <Tooltip label="Remove link" placement="top">
658
+ <CloseButton size="xs" color="gray.600" _hover={{ color: 'red.400', bg: 'whiteAlpha.100' }}
659
+ onClick={(e) => { e.stopPropagation(); handleRemoveLink() }} />
660
+ </Tooltip>
661
+ </HStack>
662
+ )}
663
+ </HStack>
664
+ <HStack spacing={2} minW={0}>
665
+ <Text fontSize="xs" color="gray.500" flexShrink={0}>Branch</Text>
666
+ <Badge colorScheme="blue" fontSize="9px">{element.branch || 'main'}</Badge>
667
+ </HStack>
668
+ <HStack spacing={2} minW={0}>
669
+ <Text fontSize="xs" color="gray.500" flexShrink={0}>File</Text>
670
+ <Text fontSize="xs" color="gray.300" fontFamily="mono" isTruncated>{parseExistingLink(element).basePath}</Text>
671
+ </HStack>
672
+ {parseExistingLink(element).symbolName && (
673
+ <HStack spacing={2} minW={0}>
674
+ <Text fontSize="xs" color="gray.500" flexShrink={0}>Symbol</Text>
675
+ <Text fontSize="xs" color="blue.300" fontFamily="mono" fontWeight="600">{parseExistingLink(element).symbolName}</Text>
676
+ </HStack>
677
+ )}
678
+ {parseExistingLink(element).pickedLine && !parseExistingLink(element).symbolName && (
679
+ <HStack spacing={2} minW={0}>
680
+ <Text fontSize="xs" color="gray.500" flexShrink={0}>Line</Text>
681
+ <Text fontSize="xs" color="blue.300" fontFamily="mono">L{parseExistingLink(element).pickedLine}</Text>
682
+ </HStack>
683
+ )}
684
+ {element.repo && (
685
+ <Button
686
+ as="a"
687
+ href={`https://github.com/${parseRepoSlug(element.repo)}/blob/${element.branch || 'main'}/${parseExistingLink(element).basePath}`}
688
+ target="_blank" rel="noopener noreferrer"
689
+ size="xs" variant="ghost" leftIcon={<ExternalLinkIcon />}
690
+ justifyContent="flex-start" px={0} mt={0.5} h="auto" py={1}
691
+ color="blue.400" _hover={{ color: 'blue.200', bg: 'transparent' }}>
692
+ Open in GitHub
693
+ </Button>
694
+ )}
695
+ </VStack>
696
+ </VStack>
697
+ ) : (
698
+ <VStack align="stretch" spacing={3} overflow="visible">
699
+ {showPreviewCard && (
700
+ <PreviewCard
701
+ filename={filePath.split('/').pop() || filePath}
702
+ isLoading={symbolLoading || rawCodeLoading}
703
+ rawCode={rawCode}
704
+ highlightStart={selectedSymbolData?.startLine ?? null}
705
+ highlightEnd={selectedSymbolData?.endLine ?? null}
706
+ selectedLine={pickedLine}
707
+ searchQuery={lineSearch}
708
+ onLineClick={isReadOnly ? undefined : (line) => setPickedLine(line === pickedLine ? null : line)}
709
+ isUnsupported={!isSupported}
710
+ onSearchChange={setLineSearch}
711
+ />
712
+ )}
713
+
714
+ <HStack justify="space-between" mb={1}>
715
+ <Text fontSize="10px" color="gray.500" fontWeight="bold" textTransform="uppercase">
716
+ {hasExistingLink ? 'Re-configure link' : 'New link'}
717
+ </Text>
718
+ {hasExistingLink && (
719
+ <Button size="xs" variant="ghost" color="gray.500" h="20px" _hover={{ color: 'white', bg: 'whiteAlpha.100' }}
720
+ onClick={() => setMode('summary')}>
721
+ Cancel
722
+ </Button>
723
+ )}
724
+ </HStack>
725
+
726
+ <StepIndicator step={step} />
727
+
728
+ {/* STEP 1 */}
729
+ {step === 1 && (
730
+ <VStack align="stretch" spacing={3}>
731
+ <FormControl>
732
+ <FormLabel fontSize="xs" color="gray.400">Repository</FormLabel>
733
+ <Input
734
+ size="sm" value={repo}
735
+ onChange={e => setRepo(e.target.value)}
736
+ onKeyDown={e => { if (e.key === 'Enter' && repoValid) advanceTo(2) }}
737
+ placeholder="owner/repo (e.g. facebook/react)"
738
+ isDisabled={isReadOnly}
739
+ bg="whiteAlpha.50"
740
+ borderColor="whiteAlpha.100"
741
+ _hover={{ borderColor: 'whiteAlpha.300' }}
742
+ _focus={{ borderColor: 'blue.500', bg: 'whiteAlpha.100' }}
743
+ />
744
+ <Text fontSize="10px" color="gray.600" mt={1}>Public GitHub repositories only</Text>
745
+ </FormControl>
746
+ <Button size="sm" rightIcon={<ChevronRightIcon />} isDisabled={!repoValid || isReadOnly}
747
+ onClick={() => advanceTo(2)} alignSelf="flex-end" colorScheme="blue" variant="outline" h="32px">
748
+ Next
749
+ </Button>
750
+ </VStack>
751
+ )}
752
+
753
+ {/* STEP 2 */}
754
+ {step === 2 && (
755
+ <VStack align="stretch" spacing={3}>
756
+ <Text fontSize="xs" color="gray.500" fontFamily="mono">{repo}</Text>
757
+ <FormControl>
758
+ <FormLabel fontSize="xs" color="gray.400">Branch</FormLabel>
759
+ {apiRateLimited ? (
760
+ <>
761
+ <Alert status="warning" rounded="md" mb={2} py={2} px={3}>
762
+ <AlertIcon boxSize={3} mr={2} />
763
+ <AlertDescription fontSize="xs">GitHub API rate limit reached - enter branch manually</AlertDescription>
764
+ </Alert>
765
+ <Input size="sm" value={branch} onChange={e => setBranch(e.target.value)} placeholder="main" />
766
+ </>
767
+ ) : (
768
+ <Popover
769
+ isOpen={branchOpen && filteredBranches.length > 0}
770
+ onClose={() => setBranchOpen(false)}
771
+ placement="bottom-start"
772
+ autoFocus={false}
773
+ matchWidth
774
+ >
775
+ <PopoverTrigger>
776
+ <Box ref={branchRef}>
777
+ <InputGroup size="sm">
778
+ <Input
779
+ ref={branchInputRef}
780
+ value={branchSearch}
781
+ onChange={e => { setBranchSearch(e.target.value); setBranchOpen(true) }}
782
+ onFocus={() => setBranchOpen(true)}
783
+ onClick={() => setBranchOpen(true)}
784
+ onKeyDown={e => {
785
+ if (e.key === 'ArrowDown') {
786
+ e.preventDefault()
787
+ setBranchActiveIndex(prev => Math.min(prev + 1, filteredBranches.length - 1))
788
+ } else if (e.key === 'ArrowUp') {
789
+ e.preventDefault()
790
+ setBranchActiveIndex(prev => Math.max(prev - 1, 0))
791
+ } else if (e.key === 'Enter') {
792
+ e.preventDefault()
793
+ if (filteredBranches.length > 0) {
794
+ const selected = filteredBranches[branchActiveIndex]
795
+ setBranch(selected)
796
+ setBranchSearch('')
797
+ setBranchOpen(false)
798
+ advanceTo(3)
799
+ } else if (branch) {
800
+ advanceTo(3)
801
+ }
802
+ } else if (e.key === 'Escape') {
803
+ setBranchOpen(false)
804
+ }
805
+ }}
806
+ placeholder={branchLoading ? 'Loading branches...' : (branch || 'Search branches...')}
807
+ isDisabled={branchLoading}
808
+ bg="whiteAlpha.50"
809
+ borderColor="whiteAlpha.100"
810
+ />
811
+ {branchLoading && (
812
+ <InputRightElement><Spinner size="xs" color="gray.400" /></InputRightElement>
813
+ )}
814
+ </InputGroup>
815
+ {branchLoading && <Progress isIndeterminate size="xs" colorScheme="blue" mt={1} rounded="full" />}
816
+ </Box>
817
+ </PopoverTrigger>
818
+ <Portal>
819
+ <PopoverContent
820
+ bg="var(--bg-panel)"
821
+ border="1px solid"
822
+ borderColor="whiteAlpha.200"
823
+ rounded="lg"
824
+ shadow="panel-sm"
825
+ maxH="250px"
826
+ overflow="hidden"
827
+ >
828
+ <PopoverBody p={0} overflowY="auto" className="custom-scrollbar">
829
+ {filteredBranches.map((b, idx) => (
830
+ <Box key={b} px={3} py={1.5} cursor="pointer" fontSize="sm" color="gray.200"
831
+ _hover={{ bg: 'whiteAlpha.100' }}
832
+ bg={branch === b ? 'blue.900' : branchActiveIndex === idx ? 'whiteAlpha.200' : undefined}
833
+ onClick={() => { setBranch(b); setBranchSearch(''); setBranchOpen(false); advanceTo(3) }}>
834
+ {b}
835
+ </Box>
836
+ ))}
837
+ </PopoverBody>
838
+ </PopoverContent>
839
+ </Portal>
840
+ </Popover>
841
+ )}
842
+ </FormControl>
843
+ <HStack justify="space-between">
844
+ <Button size="sm" variant="ghost" color="gray.500" onClick={() => setStep(1)} h="32px">← Back</Button>
845
+ <Button size="sm" rightIcon={<ChevronRightIcon />} isDisabled={!branch || isReadOnly}
846
+ onClick={() => advanceTo(3)} colorScheme="blue" variant="outline" h="32px">
847
+ Next
848
+ </Button>
849
+ </HStack>
850
+ </VStack>
851
+ )}
852
+
853
+ {/* STEP 3 */}
854
+ {step === 3 && (
855
+ <VStack align="stretch" spacing={3}>
856
+ <HStack spacing={1.5} flexWrap="wrap">
857
+ <Text fontSize="xs" color="gray.500" fontFamily="mono">{repo}</Text>
858
+ <Text fontSize="xs" color="gray.600">/</Text>
859
+ <Badge colorScheme="blue" fontSize="9px">{branch}</Badge>
860
+ </HStack>
861
+ <FormControl>
862
+ <FormLabel fontSize="xs" color="gray.400">File Path</FormLabel>
863
+ {apiRateLimited ? (
864
+ <>
865
+ <Alert status="warning" rounded="md" mb={2} py={2} px={3}>
866
+ <AlertIcon boxSize={3} mr={2} />
867
+ <AlertDescription fontSize="xs">GitHub API rate limit reached enter file path manually</AlertDescription>
868
+ </Alert>
869
+ <Input size="sm" value={fileSearch}
870
+ onChange={e => { setFileSearch(e.target.value); setFilePath(e.target.value) }}
871
+ onKeyDown={e => { if (e.key === 'Enter' && fileSearch) { handleFileSelect(fileSearch); advanceTo(4) } }}
872
+ placeholder="src/components/Foo.tsx"
873
+ bg="whiteAlpha.50" borderColor="whiteAlpha.100" />
874
+ </>
875
+ ) : (
876
+ <Popover
877
+ isOpen={fileOpen && filteredFiles.length > 0}
878
+ onClose={() => setFileOpen(false)}
879
+ placement="bottom-start"
880
+ autoFocus={false}
881
+ matchWidth
882
+ >
883
+ <PopoverTrigger>
884
+ <Box ref={fileRef}>
885
+ <InputGroup size="sm">
886
+ <Input
887
+ ref={fileInputRef}
888
+ value={fileSearch}
889
+ onChange={e => { setFileSearch(e.target.value); setFileOpen(true) }}
890
+ onFocus={() => { if (fileTree.length > 0) setFileOpen(true) }}
891
+ onClick={() => { if (fileTree.length > 0) setFileOpen(true) }}
892
+ onKeyDown={e => {
893
+ if (e.key === 'ArrowDown') {
894
+ e.preventDefault()
895
+ setFileActiveIndex(prev => Math.min(prev + 1, filteredFiles.length - 1))
896
+ } else if (e.key === 'ArrowUp') {
897
+ e.preventDefault()
898
+ setFileActiveIndex(prev => Math.max(prev - 1, 0))
899
+ } else if (e.key === 'Tab') {
900
+ e.preventDefault()
901
+ if (filteredFiles.length === 0) return
902
+ // Find longest common prefix of all matches
903
+ let common = filteredFiles[0]
904
+ for (const f of filteredFiles.slice(1)) {
905
+ let i = 0
906
+ while (i < common.length && i < f.length && common[i] === f[i]) i++
907
+ common = common.slice(0, i)
908
+ }
909
+ if (common.length > fileSearch.length) {
910
+ // Expand to next directory boundary
911
+ const nextSlash = common.indexOf('/', fileSearch.length)
912
+ setFileSearch(nextSlash >= 0 ? common.slice(0, nextSlash + 1) : common)
913
+ } else if (filteredFiles.length === 1) {
914
+ setFileSearch(filteredFiles[0])
915
+ }
916
+ } else if (e.key === 'Enter') {
917
+ e.preventDefault()
918
+ if (filteredFiles.length > 0) {
919
+ handleFileSelect(filteredFiles[fileActiveIndex])
920
+ advanceTo(4)
921
+ } else if (fileSearch) {
922
+ handleFileSelect(fileSearch)
923
+ advanceTo(4)
924
+ }
925
+ } else if (e.key === 'Escape') {
926
+ setFileOpen(false)
927
+ }
928
+ }}
929
+ placeholder={fileTreeLoading ? 'Fetching file tree...' : 'Type to search files...'}
930
+ bg="whiteAlpha.50"
931
+ borderColor="whiteAlpha.100"
932
+ _hover={{ borderColor: 'whiteAlpha.300' }}
933
+ _focus={{ borderColor: 'blue.500', bg: 'whiteAlpha.100' }}
934
+ />
935
+ {fileTreeLoading && (
936
+ <InputRightElement><Spinner size="xs" color="gray.400" /></InputRightElement>
937
+ )}
938
+ </InputGroup>
939
+ {fileTreeLoading && <Progress isIndeterminate size="xs" colorScheme="blue" mt={1} rounded="full" />}
940
+ {fileTreeTruncated && (
941
+ <Text fontSize="10px" color="orange.400" mt={1}>
942
+ Large repo results may be incomplete. Type the full path if needed.
943
+ </Text>
944
+ )}
945
+ </Box>
946
+ </PopoverTrigger>
947
+ <Portal>
948
+ <PopoverContent
949
+ bg="var(--bg-panel)"
950
+ border="1px solid"
951
+ borderColor="whiteAlpha.200"
952
+ rounded="lg"
953
+ shadow="panel-sm"
954
+ maxH="300px"
955
+ overflow="hidden"
956
+ >
957
+ <PopoverBody p={0} overflowY="auto" className="custom-scrollbar">
958
+ {filteredFiles.map((p, idx) => (
959
+ <Box key={p} px={3} py={1.5} cursor="pointer" fontSize="xs" color="gray.300"
960
+ fontFamily="mono" _hover={{ bg: 'whiteAlpha.100' }}
961
+ bg={filePath === p ? 'blue.900' : fileActiveIndex === idx ? 'whiteAlpha.200' : undefined}
962
+ onClick={() => { handleFileSelect(p); advanceTo(4) }}>
963
+ {p}
964
+ </Box>
965
+ ))}
966
+ </PopoverBody>
967
+ </PopoverContent>
968
+ </Portal>
969
+ </Popover>
970
+ )}
971
+ </FormControl>
972
+ <HStack justify="space-between">
973
+ <Button size="sm" variant="ghost" color="gray.500" onClick={() => setStep(2)} h="32px">← Back</Button>
974
+ <Button size="sm" rightIcon={<ChevronRightIcon />}
975
+ isDisabled={!filePath || isReadOnly}
976
+ onClick={() => advanceTo(4)} colorScheme="blue" variant="outline" h="32px">
977
+ Next
978
+ </Button>
979
+ </HStack>
980
+ </VStack>
981
+ )}
982
+
983
+ {/* STEP 4 */}
984
+ {step === 4 && (
985
+ <VStack align="stretch" spacing={3}>
986
+ <HStack spacing={1.5} flexWrap="wrap">
987
+ <Text fontSize="xs" color="gray.500" fontFamily="mono" isTruncated maxW="200px">{filePath.split('/').pop()}</Text>
988
+ {isSupported ? (
989
+ <Badge colorScheme="green" fontSize="9px">{language}</Badge>
990
+ ) : (
991
+ <Badge colorScheme="orange" fontSize="9px">unsupported</Badge>
992
+ )}
993
+ </HStack>
994
+
995
+ {isSupported ? (
996
+ <Box>
997
+ <FormLabel fontSize="xs" color="gray.400" mb={1.5}>Select Symbol</FormLabel>
998
+ {symbolLoading ? (
999
+ <Text fontSize="xs" color="gray.600" py={1}>Parsing symbols...</Text>
1000
+ ) : symbols.length === 0 ? (
1001
+ <Text fontSize="xs" color="gray.500" py={1}>No symbols found</Text>
1002
+ ) : (
1003
+ <VStack align="stretch" spacing={0} maxH="250px" overflowY="auto"
1004
+ rounded="lg" border="1px solid" borderColor="whiteAlpha.100"
1005
+ className="custom-scrollbar">
1006
+ {symbols.map((s, i) => {
1007
+ const isSelected = selectedSymbol?.name === s.name && selectedSymbol?.type === s.type
1008
+ return (
1009
+ <Box key={i} px={3} py={1.5} cursor="pointer"
1010
+ bg={isSelected ? 'blue.900' : undefined}
1011
+ _hover={{ bg: isSelected ? 'blue.900' : 'whiteAlpha.80' }}
1012
+ onClick={() => setSelectedSymbol(isSelected ? null : { name: s.name, type: s.type })}>
1013
+ <Text fontSize="xs" fontWeight="600" color="white">{s.name}</Text>
1014
+ <Text fontSize="10px" color="gray.500">{s.type.replace(/_/g, ' ')} · L{s.startLine}</Text>
1015
+ </Box>
1016
+ )
1017
+ })}
1018
+ </VStack>
1019
+ )}
1020
+ </Box>
1021
+ ) : (
1022
+ <Box>
1023
+ <Alert status="warning" rounded="md" py={2} px={3} mb={2}>
1024
+ <AlertIcon boxSize={3.5} mr={2} />
1025
+ <AlertDescription fontSize="10px">
1026
+ Select a line in the preview to link it.
1027
+ </AlertDescription>
1028
+ </Alert>
1029
+ {pickedLine && (
1030
+ <HStack spacing={1.5}>
1031
+ <Text fontSize="10px" color="gray.600">Selected:</Text>
1032
+ <Badge colorScheme="blue" fontSize="9px" fontFamily="mono">Line {pickedLine}</Badge>
1033
+ </HStack>
1034
+ )}
1035
+ </Box>
1036
+ )}
1037
+
1038
+ <HStack justify="space-between">
1039
+ <Button size="sm" variant="ghost" color="gray.500" onClick={() => setStep(3)} h="32px">← Back</Button>
1040
+ <Button size="sm" colorScheme="blue" onClick={handleApply} isDisabled={isReadOnly || symbolLoading || rawCodeLoading} h="32px">
1041
+ Apply
1042
+ </Button>
1043
+ </HStack>
1044
+ </VStack>
1045
+ )}
1046
+ </VStack>
1047
+ )}
1048
+ </AccordionPanel>
1049
+ </AccordionItem>
1050
+ </Accordion>
1051
+ </Box>
1052
+ )
1053
+ }