@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,103 @@
1
+ import type { CSSProperties } from 'react'
2
+ import { Position } from 'reactflow'
3
+
4
+ export const HANDLE_SLOT_COUNT = 5
5
+ export const HANDLE_SLOT_GAP = 12
6
+ export const HANDLE_SLOT_CENTER_INDEX = Math.floor(HANDLE_SLOT_COUNT / 2)
7
+
8
+ export type LogicalHandleSide = 'top' | 'bottom' | 'left' | 'right'
9
+
10
+ export const DEFAULT_SOURCE_HANDLE_SIDE: LogicalHandleSide = 'right'
11
+ export const DEFAULT_TARGET_HANDLE_SIDE: LogicalHandleSide = 'left'
12
+
13
+ function clampSlot(slot: number) {
14
+ return Math.max(0, Math.min(HANDLE_SLOT_COUNT - 1, slot))
15
+ }
16
+
17
+ export function getLogicalHandleId(
18
+ handleId: string | null | undefined,
19
+ fallback: LogicalHandleSide | null = null,
20
+ ): LogicalHandleSide | null {
21
+ if (!handleId) return fallback
22
+ const side = handleId.split('-', 1)[0]
23
+ if (side === 'top' || side === 'bottom' || side === 'left' || side === 'right') return side
24
+ return fallback
25
+ }
26
+
27
+ export function getVisualHandleSlot(groupIndex: number, groupCount: number) {
28
+ if (groupIndex < 0) return HANDLE_SLOT_CENTER_INDEX
29
+ if (groupCount <= 1) return HANDLE_SLOT_CENTER_INDEX
30
+ return clampSlot(Math.round((groupIndex * (HANDLE_SLOT_COUNT - 1)) / (groupCount - 1)))
31
+ }
32
+
33
+ export function getVisualHandleSlotFromId(handleId: string | null | undefined) {
34
+ if (!handleId) return null
35
+ const parts = handleId.split('-')
36
+ if (parts.length < 2) return null
37
+ const slot = Number(parts[1])
38
+ return Number.isInteger(slot) ? clampSlot(slot) : null
39
+ }
40
+
41
+ export function getVisualHandleId(side: LogicalHandleSide, slot: number) {
42
+ return `${side}-${clampSlot(slot)}`
43
+ }
44
+
45
+ export function getVisualHandleIdForGroup(side: LogicalHandleSide, groupIndex: number, groupCount: number) {
46
+ return getVisualHandleId(side, getVisualHandleSlot(groupIndex, groupCount))
47
+ }
48
+
49
+ export function ensureVisualHandleId(
50
+ handleId: string | null | undefined,
51
+ fallback: LogicalHandleSide,
52
+ ) {
53
+ const side = getLogicalHandleId(handleId, fallback)
54
+ if (!side) return null
55
+ const slot = getVisualHandleSlotFromId(handleId) ?? HANDLE_SLOT_CENTER_INDEX
56
+ return getVisualHandleId(side, slot)
57
+ }
58
+
59
+ export function getHandleSlotOffset(slot: number) {
60
+ return (clampSlot(slot) - HANDLE_SLOT_CENTER_INDEX) * HANDLE_SLOT_GAP
61
+ }
62
+
63
+ export function getHandleSlotOffsetFromId(handleId: string | null | undefined) {
64
+ const slot = getVisualHandleSlotFromId(handleId)
65
+ if (slot === null) return 0
66
+ return getHandleSlotOffset(slot)
67
+ }
68
+
69
+ export function getVisualHandleStyle(position: Position, slot: number): CSSProperties {
70
+ const offset = getHandleSlotOffset(slot)
71
+
72
+ switch (position) {
73
+ case Position.Top:
74
+ case Position.Bottom:
75
+ return { left: `calc(50% + ${offset}px)` }
76
+ case Position.Left:
77
+ case Position.Right:
78
+ return { top: `calc(50% + ${offset}px)` }
79
+ }
80
+ }
81
+
82
+ export function getHandleFlowPosition(
83
+ nodeX: number,
84
+ nodeY: number,
85
+ width: number,
86
+ height: number,
87
+ handleId: string | null | undefined,
88
+ fallback: LogicalHandleSide,
89
+ ) {
90
+ const side = getLogicalHandleId(handleId, fallback) ?? fallback
91
+ const offset = getHandleSlotOffsetFromId(handleId)
92
+
93
+ switch (side) {
94
+ case 'top':
95
+ return { x: nodeX + width / 2 + offset, y: nodeY, side }
96
+ case 'bottom':
97
+ return { x: nodeX + width / 2 + offset, y: nodeY + height, side }
98
+ case 'left':
99
+ return { x: nodeX, y: nodeY + height / 2 + offset, side }
100
+ case 'right':
101
+ return { x: nodeX + width, y: nodeY + height / 2 + offset, side }
102
+ }
103
+ }
@@ -0,0 +1,121 @@
1
+ const GITHUB_API_BASE = 'https://api.github.com'
2
+ const MIN_REQUEST_GAP_MS = 300
3
+
4
+ type RepoVisibility = 'public' | 'private' | 'unknown'
5
+
6
+ let queue: Promise<void> = Promise.resolve()
7
+ let lastRequestAt = 0
8
+ let rateLimitedUntil = 0
9
+
10
+ const visibilityInFlight = new Map<string, Promise<RepoVisibility>>()
11
+
12
+ function wait(ms: number): Promise<void> {
13
+ if (ms <= 0) return Promise.resolve()
14
+ return new Promise((resolve) => {
15
+ window.setTimeout(resolve, ms)
16
+ })
17
+ }
18
+
19
+ function toNumber(value: string | null): number | null {
20
+ if (!value) return null
21
+ const parsed = Number(value)
22
+ return Number.isFinite(parsed) ? parsed : null
23
+ }
24
+
25
+ function getAuthHeader(): string | null {
26
+ const clientId = import.meta.env.VITE_GH_CLIENT_ID?.trim()
27
+ const clientSecret = import.meta.env.VITE_GH_CLIENT_SECRET?.trim()
28
+ if (!clientId || !clientSecret) return null
29
+ return `Basic ${window.btoa(`${clientId}:${clientSecret}`)}`
30
+ }
31
+
32
+ function updateRateLimitState(response: Response) {
33
+ const now = Date.now()
34
+ const retryAfterSeconds = toNumber(response.headers.get('retry-after'))
35
+ const remaining = toNumber(response.headers.get('x-ratelimit-remaining'))
36
+ const resetUnixSeconds = toNumber(response.headers.get('x-ratelimit-reset'))
37
+
38
+ if (retryAfterSeconds !== null && retryAfterSeconds > 0) {
39
+ rateLimitedUntil = Math.max(rateLimitedUntil, now + retryAfterSeconds * 1000)
40
+ }
41
+
42
+ if (remaining === 0 && resetUnixSeconds !== null) {
43
+ // Add a small safety buffer after reset time to avoid immediate re-limit.
44
+ rateLimitedUntil = Math.max(rateLimitedUntil, resetUnixSeconds * 1000 + 500)
45
+ }
46
+
47
+ if ((response.status === 403 || response.status === 429) && retryAfterSeconds === null && remaining === 0) {
48
+ rateLimitedUntil = Math.max(rateLimitedUntil, now + 60_000)
49
+ }
50
+ }
51
+
52
+ function enqueueRequest<T>(request: () => Promise<T>): Promise<T> {
53
+ const run = async () => {
54
+ const now = Date.now()
55
+ if (rateLimitedUntil > now) {
56
+ await wait(rateLimitedUntil - now)
57
+ }
58
+
59
+ const elapsedSinceLast = Date.now() - lastRequestAt
60
+ if (elapsedSinceLast < MIN_REQUEST_GAP_MS) {
61
+ await wait(MIN_REQUEST_GAP_MS - elapsedSinceLast)
62
+ }
63
+
64
+ lastRequestAt = Date.now()
65
+ return request()
66
+ }
67
+
68
+ const chained = queue.then(run, run)
69
+ queue = chained.then(() => undefined, () => undefined)
70
+ return chained
71
+ }
72
+
73
+ export async function githubRequest(path: string): Promise<Response> {
74
+ return enqueueRequest(async () => {
75
+ const headers = new Headers({
76
+ Accept: 'application/vnd.github+json',
77
+ })
78
+ const authHeader = getAuthHeader()
79
+ if (authHeader) headers.set('Authorization', authHeader)
80
+
81
+ let response = await fetch(`${GITHUB_API_BASE}${path}`, {
82
+ method: 'GET',
83
+ headers,
84
+ })
85
+
86
+ // If the request failed with 401 but we provided credentials, retry without credentials.
87
+ // This handles cases where client ID/secret are misconfigured but the resource is public.
88
+ if (response.status === 401 && authHeader) {
89
+ headers.delete('Authorization')
90
+ response = await fetch(`${GITHUB_API_BASE}${path}`, {
91
+ method: 'GET',
92
+ headers,
93
+ })
94
+ }
95
+
96
+ updateRateLimitState(response)
97
+ return response
98
+ })
99
+ }
100
+
101
+ export function getGithubRepoVisibility(repoSlug: string): Promise<RepoVisibility> {
102
+ const inFlight = visibilityInFlight.get(repoSlug)
103
+ if (inFlight) return inFlight
104
+
105
+ const promise = (async () => {
106
+ const response = await githubRequest(`/repos/${repoSlug}`)
107
+ if (response.status === 200) return 'public'
108
+ if (response.status === 404 || response.status === 401) return 'private'
109
+ if (response.status === 403 || response.status === 429) {
110
+ const remaining = toNumber(response.headers.get('x-ratelimit-remaining'))
111
+ if (remaining === 0 || Date.now() < rateLimitedUntil) return 'unknown'
112
+ return 'private'
113
+ }
114
+ return 'unknown'
115
+ })().finally(() => {
116
+ visibilityInFlight.delete(repoSlug)
117
+ })
118
+
119
+ visibilityInFlight.set(repoSlug, promise)
120
+ return promise
121
+ }
@@ -0,0 +1,108 @@
1
+ interface CacheEntry<T> {
2
+ data: T
3
+ timestamp: number
4
+ }
5
+
6
+ const CACHE_TTL = 10 * 60 * 1000 // 10 minutes
7
+ const STORAGE_KEY = 'diag_github_cache'
8
+
9
+ interface CacheData {
10
+ branches: Record<string, CacheEntry<string[]>>
11
+ trees: Record<string, CacheEntry<string[]>>
12
+ contents: Record<string, CacheEntry<string>>
13
+ repoVisibility: Record<string, CacheEntry<boolean>>
14
+ }
15
+
16
+ class GithubCache {
17
+ private cache: CacheData
18
+
19
+ constructor() {
20
+ this.cache = this.load()
21
+ this.cleanup()
22
+ }
23
+
24
+ private load(): CacheData {
25
+ try {
26
+ const stored = localStorage.getItem(STORAGE_KEY)
27
+ if (stored) return JSON.parse(stored)
28
+ } catch { /* empty */ }
29
+ return { branches: {}, trees: {}, contents: {}, repoVisibility: {} }
30
+ }
31
+
32
+ private save() {
33
+ try {
34
+ // Basic size limit check - clear if too big (simple for now)
35
+ const str = JSON.stringify(this.cache)
36
+ if (str.length > 4 * 1024 * 1024) { // 4MB
37
+ this.cache = { branches: {}, trees: {}, contents: {}, repoVisibility: {} }
38
+ }
39
+ localStorage.setItem(STORAGE_KEY, JSON.stringify(this.cache))
40
+ } catch { /* empty */ }
41
+ }
42
+
43
+ private cleanup() {
44
+ const now = Date.now()
45
+ let changed = false
46
+ const clean = <T>(map: Record<string, CacheEntry<T>>) => {
47
+ for (const [k, v] of Object.entries(map)) {
48
+ if (now - v.timestamp > CACHE_TTL) { delete map[k]; changed = true }
49
+ }
50
+ }
51
+ clean(this.cache.branches)
52
+ clean(this.cache.trees)
53
+ clean(this.cache.contents)
54
+ if (!this.cache.repoVisibility) this.cache.repoVisibility = {}
55
+ clean(this.cache.repoVisibility)
56
+ if (changed) this.save()
57
+ }
58
+
59
+ getBranches(repo: string): string[] | null {
60
+ const entry = this.cache.branches[repo]
61
+ if (entry && Date.now() - entry.timestamp < CACHE_TTL) return entry.data
62
+ return null
63
+ }
64
+
65
+ setBranches(repo: string, data: string[]) {
66
+ this.cache.branches[repo] = { data, timestamp: Date.now() }
67
+ this.save()
68
+ }
69
+
70
+ getTree(repo: string, branch: string): string[] | null {
71
+ const key = `${repo}/${branch}`
72
+ const entry = this.cache.trees[key]
73
+ if (entry && Date.now() - entry.timestamp < CACHE_TTL) return entry.data
74
+ return null
75
+ }
76
+
77
+ setTree(repo: string, branch: string, data: string[]) {
78
+ const key = `${repo}/${branch}`
79
+ this.cache.trees[key] = { data, timestamp: Date.now() }
80
+ this.save()
81
+ }
82
+
83
+ getContent(rawUrl: string): string | null {
84
+ const entry = this.cache.contents[rawUrl]
85
+ if (entry && Date.now() - entry.timestamp < CACHE_TTL) return entry.data
86
+ return null
87
+ }
88
+
89
+ setContent(rawUrl: string, data: string) {
90
+ this.cache.contents[rawUrl] = { data, timestamp: Date.now() }
91
+ this.save()
92
+ }
93
+
94
+ getRepoPublic(slug: string): boolean | null {
95
+ if (!this.cache.repoVisibility) return null
96
+ const entry = this.cache.repoVisibility[slug]
97
+ if (entry && Date.now() - entry.timestamp < CACHE_TTL) return entry.data
98
+ return null
99
+ }
100
+
101
+ setRepoPublic(slug: string, isPublic: boolean) {
102
+ if (!this.cache.repoVisibility) this.cache.repoVisibility = {}
103
+ this.cache.repoVisibility[slug] = { data: isPublic, timestamp: Date.now() }
104
+ this.save()
105
+ }
106
+ }
107
+
108
+ export const githubCache = new GithubCache()
@@ -0,0 +1,9 @@
1
+ export function parseNumericId(value: string | null | undefined): number | null {
2
+ if (value == null) return null
3
+ const n = Number(value)
4
+ return Number.isInteger(n) && n > 0 ? n : null
5
+ }
6
+
7
+ export function idToString(value: number | null | undefined): string {
8
+ return value == null ? '' : String(value)
9
+ }
@@ -0,0 +1,143 @@
1
+ import type { TechnologyCatalogItem } from '../types'
2
+ import { isNativeApp } from '../config/runtime'
3
+
4
+ interface SearchableCatalogItem {
5
+ item: TechnologyCatalogItem
6
+ haystack: string
7
+ }
8
+
9
+ interface TechnologyCatalogIndex {
10
+ items: TechnologyCatalogItem[]
11
+ searchable: SearchableCatalogItem[]
12
+ bySlug: Map<string, TechnologyCatalogItem>
13
+ }
14
+
15
+ let indexPromise: Promise<TechnologyCatalogIndex> | null = null
16
+
17
+ export function resolveWithBase(urlOrPath: string): string {
18
+ if (!urlOrPath) return urlOrPath
19
+ if (urlOrPath.startsWith('http://') || urlOrPath.startsWith('https://') || urlOrPath.startsWith('data:')) {
20
+ return urlOrPath
21
+ }
22
+
23
+ // When running inside the native mobile app (Capacitor), or inside an embedded webview
24
+ // that serves content from localhost, avoid prefixing the app BASE_URL. Mobile builds and
25
+ // the local webview often serve/resolve static assets from the web root (\"/\"), so
26
+ // returning the path without adding import.meta.env.BASE_URL avoids requests to
27
+ // unexpected URLs like \"https://localhost/app/icons/...\" which can 404.
28
+ const runningOnLocalhost = typeof window !== 'undefined' && (() => {
29
+ // file: is unequivocally a native / packaged environment (Capacitor)
30
+ if (window.location.protocol === 'file:') return true
31
+
32
+ // Only treat plain localhost/127.0.0.1 as the native webview when it's
33
+ // served in a way that matches Capacitor's webview (typically https
34
+ // without an explicit dev port). We must NOT treat the dev server
35
+ // (e.g. http://localhost:5173) as native.
36
+ const hostIsLocal = window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1'
37
+
38
+ if (!hostIsLocal) return false
39
+
40
+ // If served over HTTPS on localhost (Capacitor uses https://localhost) assume native/webview.
41
+ if (window.location.protocol === 'https:') return true
42
+
43
+ // If there's no explicit port (or it's the default HTTPS port), treat as native/webview.
44
+ // Dev servers usually expose a non-default port (like 5173) - those should NOT be treated as native.
45
+ const port = (window.location.port || '').trim()
46
+ if (!port || port === '443') return true
47
+
48
+ return false
49
+ })()
50
+ if (isNativeApp || runningOnLocalhost) {
51
+ // Ensure the path is absolute so it resolves correctly from the app's web root.
52
+ return urlOrPath.startsWith('/') ? urlOrPath : `/${urlOrPath}`
53
+ }
54
+
55
+ const base = import.meta.env.BASE_URL || '/'
56
+ const normalizedBase = base.endsWith('/') ? base : `${base}/`
57
+ if (urlOrPath.startsWith(normalizedBase) || urlOrPath === normalizedBase.slice(0, -1)) {
58
+ return urlOrPath
59
+ }
60
+ const normalizedPath = urlOrPath.startsWith('/') ? urlOrPath.slice(1) : urlOrPath
61
+ return `${normalizedBase}${normalizedPath}`
62
+ }
63
+
64
+ function normalizeText(value: string): string {
65
+ return value.toLowerCase().trim()
66
+ }
67
+
68
+ function createHaystack(item: TechnologyCatalogItem): string {
69
+ return normalizeText([
70
+ item.name,
71
+ item.nameShort,
72
+ item.provider,
73
+ item.defaultSlug,
74
+ ].filter(Boolean).join(' '))
75
+ }
76
+
77
+ async function loadCatalogItems(): Promise<TechnologyCatalogItem[]> {
78
+ const response = await fetch(resolveWithBase('icons.json'), { cache: 'force-cache' })
79
+ if (!response.ok) {
80
+ throw new Error('Failed to load technology catalog')
81
+ }
82
+ const data = await response.json()
83
+ if (!Array.isArray(data)) return []
84
+
85
+ return data as TechnologyCatalogItem[]
86
+ }
87
+
88
+ export async function getTechnologyCatalogIndex(): Promise<TechnologyCatalogIndex> {
89
+ if (!indexPromise) {
90
+ indexPromise = loadCatalogItems().then((items) => {
91
+ const bySlug = new Map<string, TechnologyCatalogItem>()
92
+ const searchable: SearchableCatalogItem[] = []
93
+
94
+ for (const item of items) {
95
+ bySlug.set(item.defaultSlug, item)
96
+ searchable.push({ item, haystack: createHaystack(item) })
97
+ }
98
+
99
+ return { items, searchable, bySlug }
100
+ }).catch((error) => {
101
+ indexPromise = null
102
+ throw error
103
+ })
104
+ }
105
+
106
+ return indexPromise
107
+ }
108
+
109
+ export async function searchTechnologyCatalog(query: string, maxResults = 12): Promise<TechnologyCatalogItem[]> {
110
+ const normalizedQuery = query.trim()
111
+ if (!normalizedQuery) return []
112
+
113
+ const index = await getTechnologyCatalogIndex()
114
+
115
+ try {
116
+ const regex = new RegExp(normalizedQuery, 'i')
117
+ const matches: TechnologyCatalogItem[] = []
118
+ for (const entry of index.searchable) {
119
+ if (regex.test(entry.haystack)) {
120
+ matches.push(entry.item)
121
+ if (matches.length >= maxResults) break
122
+ }
123
+ }
124
+ return matches
125
+ } catch {
126
+ const needle = normalizeText(normalizedQuery)
127
+ const matches: TechnologyCatalogItem[] = []
128
+ for (const entry of index.searchable) {
129
+ if (entry.haystack.includes(needle)) {
130
+ matches.push(entry.item)
131
+ if (matches.length >= maxResults) break
132
+ }
133
+ }
134
+ return matches
135
+ }
136
+ }
137
+
138
+ export async function getTechnologyCatalogItemBySlug(slug: string): Promise<TechnologyCatalogItem | null> {
139
+ const cleanSlug = slug.trim()
140
+ if (!cleanSlug) return null
141
+ const index = await getTechnologyCatalogIndex()
142
+ return index.bySlug.get(cleanSlug) ?? null
143
+ }
@@ -0,0 +1,100 @@
1
+ import { createStandaloneToast, UseToastOptions, ToastId } from '@chakra-ui/react'
2
+ import theme from '../theme'
3
+
4
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
5
+ const { toast: chakraToast, ToastContainer }: any = createStandaloneToast({
6
+ theme,
7
+ defaultOptions: {
8
+ status: 'error',
9
+ duration: 5000,
10
+ isClosable: true,
11
+ position: 'bottom-right',
12
+ },
13
+ })
14
+
15
+ let errorToastId: ToastId | null = null
16
+ let errorCount = 0
17
+ const errorSummaries = new Set<string>()
18
+
19
+ /**
20
+ * Extracts a short error code or summary from a ConnectRPC error message.
21
+ * ConnectRPC messages often look like "[unavailable] upstream request timeout"
22
+ * or REST responses like "HTTP 500 Internal Server Error"
23
+ */
24
+ function getErrorSummary(options: UseToastOptions): string {
25
+ const text = (options.description || options.title || 'Unknown error').toString()
26
+
27
+ // ConnectRPC: matches "[code] message"
28
+ const connectMatch = text.match(/^\[(\w+)\]/)
29
+ if (connectMatch) return connectMatch[1]
30
+
31
+ // HTTP status: "HTTP 500"
32
+ const httpMatch = text.match(/HTTP \d+/)
33
+ if (httpMatch) return httpMatch[0]
34
+
35
+ // Generic fallback: first part of message (before colon or newline)
36
+ return text.split(':')[0].split('\n')[0].substring(0, 32).trim()
37
+ }
38
+
39
+ /**
40
+ * Global toast utility that intercepts 'error' status toasts to combine multiple
41
+ * backend errors into a single notification with a counter.
42
+ */
43
+ interface CustomToast {
44
+ (options: UseToastOptions): ToastId | undefined;
45
+ close: typeof chakraToast.close;
46
+ closeAll: typeof chakraToast.closeAll;
47
+ update: typeof chakraToast.update;
48
+ isActive: typeof chakraToast.isActive;
49
+ }
50
+
51
+ const toast: CustomToast = (options: UseToastOptions) => {
52
+ const status = options.status || 'error'
53
+
54
+ if (status === 'error') {
55
+ const summary = getErrorSummary(options)
56
+
57
+ // Check if an error toast is already active
58
+ if (errorToastId && chakraToast.isActive(errorToastId)) {
59
+ errorCount++
60
+ errorSummaries.add(summary)
61
+
62
+ chakraToast.update(errorToastId, {
63
+ ...options,
64
+ title: `${errorCount} Requests Failed`,
65
+ description: `Errors: ${Array.from(errorSummaries).join(', ')}`,
66
+ duration: 5000, // Refresh the timer
67
+ })
68
+ return errorToastId;
69
+ } else {
70
+ // First error or previous toast was closed
71
+ errorCount = 1
72
+ errorSummaries.clear()
73
+ errorSummaries.add(summary)
74
+
75
+ const originalOnCloseComplete = options.onCloseComplete
76
+
77
+ errorToastId = chakraToast({
78
+ ...options,
79
+ title: options.title || 'Request Failed',
80
+ onCloseComplete: () => {
81
+ if (originalOnCloseComplete) originalOnCloseComplete()
82
+ errorToastId = null
83
+ errorCount = 0
84
+ errorSummaries.clear()
85
+ }
86
+ })
87
+ return errorToastId;
88
+ }
89
+ }
90
+
91
+ return chakraToast(options)
92
+ }
93
+
94
+ // Proxy standard chakra toast methods for complete compatibility
95
+ toast.close = chakraToast.close
96
+ toast.closeAll = chakraToast.closeAll
97
+ toast.update = chakraToast.update
98
+ toast.isActive = chakraToast.isActive
99
+
100
+ export { toast, ToastContainer }