dsh-coding-sidebar 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (277) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +524 -0
  3. package/cordis.patch.yml +49 -0
  4. package/lib/client-editor.js +48229 -0
  5. package/lib/client-mermaid.js +200745 -0
  6. package/lib/client-registry.js +21867 -0
  7. package/lib/client-terminal.js +9361 -0
  8. package/lib/client.js +21867 -0
  9. package/lib/index.js +4556 -0
  10. package/lib/invariant.js +22 -0
  11. package/lib/types/agent-opens.d.ts +65 -0
  12. package/lib/types/agent-pty.d.ts +226 -0
  13. package/lib/types/browser-probe.d.ts +15 -0
  14. package/lib/types/bundle-route.d.ts +12 -0
  15. package/lib/types/client/BrowserView.d.ts +39 -0
  16. package/lib/types/client/DiffTab.d.ts +6 -0
  17. package/lib/types/client/DiffView.d.ts +51 -0
  18. package/lib/types/client/EditorHost.d.ts +13 -0
  19. package/lib/types/client/FileTree.d.ts +38 -0
  20. package/lib/types/client/FreeWindow.d.ts +29 -0
  21. package/lib/types/client/GitView.d.ts +10 -0
  22. package/lib/types/client/MarkdownHtml.d.ts +32 -0
  23. package/lib/types/client/OrphanedTab.d.ts +11 -0
  24. package/lib/types/client/PdfView.d.ts +6 -0
  25. package/lib/types/client/RenderBoundary.d.ts +32 -0
  26. package/lib/types/client/SandboxStatusBar.d.ts +10 -0
  27. package/lib/types/client/SideCardSection.d.ts +74 -0
  28. package/lib/types/client/SideChatView.d.ts +16 -0
  29. package/lib/types/client/Sidebar.d.ts +6 -0
  30. package/lib/types/client/SubagentView.d.ts +16 -0
  31. package/lib/types/client/TabBar.d.ts +54 -0
  32. package/lib/types/client/TerminalView.d.ts +24 -0
  33. package/lib/types/client/TextEditor.d.ts +10 -0
  34. package/lib/types/client/TreePanel.d.ts +24 -0
  35. package/lib/types/client/UploadOverlay.d.ts +20 -0
  36. package/lib/types/client/add-plugin-modal.d.ts +15 -0
  37. package/lib/types/client/api.d.ts +275 -0
  38. package/lib/types/client/binary-download.d.ts +12 -0
  39. package/lib/types/client/breakpoints.d.ts +17 -0
  40. package/lib/types/client/browser.d.ts +60 -0
  41. package/lib/types/client/builtins/index.d.ts +18 -0
  42. package/lib/types/client/builtins/tabs.d.ts +11 -0
  43. package/lib/types/client/builtins/viewers.d.ts +3 -0
  44. package/lib/types/client/chunk-loader.d.ts +131 -0
  45. package/lib/types/client/chunks/editor.d.ts +9 -0
  46. package/lib/types/client/chunks/mermaid.d.ts +10 -0
  47. package/lib/types/client/chunks/terminal.d.ts +9 -0
  48. package/lib/types/client/cm-themes.d.ts +24 -0
  49. package/lib/types/client/conversation-draft.d.ts +14 -0
  50. package/lib/types/client/desktop-env.d.ts +35 -0
  51. package/lib/types/client/editor-load.d.ts +66 -0
  52. package/lib/types/client/frame-batcher.d.ts +24 -0
  53. package/lib/types/client/icons.d.ts +59 -0
  54. package/lib/types/client/image-types.d.ts +3 -0
  55. package/lib/types/client/ime-guard.d.ts +36 -0
  56. package/lib/types/client/index.d.ts +25 -0
  57. package/lib/types/client/intercept.d.ts +46 -0
  58. package/lib/types/client/lang.d.ts +14 -0
  59. package/lib/types/client/layout-push.d.ts +15 -0
  60. package/lib/types/client/lazy-chunk.d.ts +25 -0
  61. package/lib/types/client/link-intercept.d.ts +42 -0
  62. package/lib/types/client/locales-ar.d.ts +18 -0
  63. package/lib/types/client/locales-de.d.ts +4 -0
  64. package/lib/types/client/locales-fr.d.ts +10 -0
  65. package/lib/types/client/locales-hi.d.ts +17 -0
  66. package/lib/types/client/locales-id.d.ts +15 -0
  67. package/lib/types/client/locales-it.d.ts +8 -0
  68. package/lib/types/client/locales-ja.d.ts +17 -0
  69. package/lib/types/client/locales-ko.d.ts +9 -0
  70. package/lib/types/client/locales-nl.d.ts +16 -0
  71. package/lib/types/client/locales-pl.d.ts +19 -0
  72. package/lib/types/client/locales-pt.d.ts +1 -0
  73. package/lib/types/client/locales-ru.d.ts +13 -0
  74. package/lib/types/client/locales-sv.d.ts +1 -0
  75. package/lib/types/client/locales-th.d.ts +17 -0
  76. package/lib/types/client/locales-tr.d.ts +17 -0
  77. package/lib/types/client/locales-vi.d.ts +17 -0
  78. package/lib/types/client/locales-zh-HK.d.ts +32 -0
  79. package/lib/types/client/locales-zh-MO.d.ts +32 -0
  80. package/lib/types/client/locales-zh-TW.d.ts +32 -0
  81. package/lib/types/client/locales.d.ts +452 -0
  82. package/lib/types/client/markdown-html.d.ts +96 -0
  83. package/lib/types/client/markdown-images.d.ts +45 -0
  84. package/lib/types/client/markdown-labels.d.ts +38 -0
  85. package/lib/types/client/md-toc.d.ts +4 -0
  86. package/lib/types/client/mermaid-blocks.d.ts +47 -0
  87. package/lib/types/client/mermaid-sanitize.d.ts +2 -0
  88. package/lib/types/client/mermaid.d.ts +12 -0
  89. package/lib/types/client/open-when-sized.d.ts +20 -0
  90. package/lib/types/client/open-with-settings.d.ts +4 -0
  91. package/lib/types/client/open-with.d.ts +87 -0
  92. package/lib/types/client/openpath-intercept.d.ts +85 -0
  93. package/lib/types/client/paths.d.ts +43 -0
  94. package/lib/types/client/pdf-types.d.ts +2 -0
  95. package/lib/types/client/pinned.d.ts +86 -0
  96. package/lib/types/client/plugin-settings.d.ts +8 -0
  97. package/lib/types/client/plugins-shared.d.ts +27 -0
  98. package/lib/types/client/plugins-tabs.d.ts +3 -0
  99. package/lib/types/client/plugins-viewers.d.ts +3 -0
  100. package/lib/types/client/prefs.d.ts +41 -0
  101. package/lib/types/client/produced-files.d.ts +29 -0
  102. package/lib/types/client/selection-payload.d.ts +27 -0
  103. package/lib/types/client/service.d.ts +439 -0
  104. package/lib/types/client/settings-nav-icon.d.ts +19 -0
  105. package/lib/types/client/shell-presets.d.ts +48 -0
  106. package/lib/types/client/sidechat-transcript.d.ts +103 -0
  107. package/lib/types/client/split-pane.d.ts +36 -0
  108. package/lib/types/client/state.d.ts +432 -0
  109. package/lib/types/client/subagent-detect.d.ts +54 -0
  110. package/lib/types/client/subagent-jobs.d.ts +63 -0
  111. package/lib/types/client/tab-content-memo.d.ts +40 -0
  112. package/lib/types/client/terminal-font.d.ts +78 -0
  113. package/lib/types/client/terminal-links.d.ts +119 -0
  114. package/lib/types/client/theme.d.ts +45 -0
  115. package/lib/types/client/titlebar-strip.d.ts +23 -0
  116. package/lib/types/client/upload.d.ts +69 -0
  117. package/lib/types/client/wco.d.ts +47 -0
  118. package/lib/types/config.d.ts +64 -0
  119. package/lib/types/context-types.d.ts +605 -0
  120. package/lib/types/fs-operations.d.ts +28 -0
  121. package/lib/types/fs-search.d.ts +24 -0
  122. package/lib/types/fs-tree.d.ts +51 -0
  123. package/lib/types/git.d.ts +124 -0
  124. package/lib/types/html-route.d.ts +59 -0
  125. package/lib/types/index.d.ts +47 -0
  126. package/lib/types/invariant.d.ts +15 -0
  127. package/lib/types/jobs-routes.d.ts +45 -0
  128. package/lib/types/open-external.d.ts +24 -0
  129. package/lib/types/path-security.d.ts +20 -0
  130. package/lib/types/prefs-shared.d.ts +236 -0
  131. package/lib/types/pty-deps.d.ts +78 -0
  132. package/lib/types/pty-manager.d.ts +168 -0
  133. package/lib/types/sidechat-core.d.ts +172 -0
  134. package/lib/types/sidechat-routes.d.ts +30 -0
  135. package/lib/types/subagent-activity.d.ts +44 -0
  136. package/lib/types/subagent-live-route.d.ts +44 -0
  137. package/lib/types/tools.d.ts +34 -0
  138. package/lib/types/trust-fence.d.ts +24 -0
  139. package/lib/types/wire.d.ts +38 -0
  140. package/package.json +172 -0
  141. package/scripts/install.ps1 +306 -0
  142. package/scripts/install.sh +287 -0
  143. package/src/agent-opens.ts +265 -0
  144. package/src/agent-pty.ts +527 -0
  145. package/src/browser-probe.ts +26 -0
  146. package/src/bundle-route.ts +129 -0
  147. package/src/client/BrowserView.tsx +289 -0
  148. package/src/client/DiffTab.tsx +114 -0
  149. package/src/client/DiffView.tsx +305 -0
  150. package/src/client/EditorHost.tsx +558 -0
  151. package/src/client/FileTree.tsx +669 -0
  152. package/src/client/FreeWindow.tsx +337 -0
  153. package/src/client/GitView.tsx +717 -0
  154. package/src/client/MarkdownHtml.tsx +296 -0
  155. package/src/client/OrphanedTab.tsx +27 -0
  156. package/src/client/PdfView.tsx +110 -0
  157. package/src/client/RenderBoundary.tsx +49 -0
  158. package/src/client/SandboxStatusBar.tsx +60 -0
  159. package/src/client/SideCardSection.module.css +960 -0
  160. package/src/client/SideCardSection.tsx +1163 -0
  161. package/src/client/SideChatView.module.css +437 -0
  162. package/src/client/SideChatView.tsx +645 -0
  163. package/src/client/Sidebar.tsx +1370 -0
  164. package/src/client/SubagentView.module.css +547 -0
  165. package/src/client/SubagentView.tsx +883 -0
  166. package/src/client/TabBar.tsx +365 -0
  167. package/src/client/TerminalView.tsx +424 -0
  168. package/src/client/TextEditor.tsx +468 -0
  169. package/src/client/TreePanel.tsx +275 -0
  170. package/src/client/UploadOverlay.tsx +62 -0
  171. package/src/client/add-plugin-modal.tsx +199 -0
  172. package/src/client/api.ts +357 -0
  173. package/src/client/binary-download.tsx +23 -0
  174. package/src/client/breakpoints.ts +61 -0
  175. package/src/client/browser.ts +167 -0
  176. package/src/client/builtins/index.ts +37 -0
  177. package/src/client/builtins/tabs.tsx +340 -0
  178. package/src/client/builtins/viewers.tsx +125 -0
  179. package/src/client/chunk-loader.ts +358 -0
  180. package/src/client/chunks/editor.tsx +9 -0
  181. package/src/client/chunks/mermaid.tsx +10 -0
  182. package/src/client/chunks/terminal.tsx +9 -0
  183. package/src/client/cm-themes.ts +129 -0
  184. package/src/client/conversation-draft.ts +29 -0
  185. package/src/client/css-modules.d.ts +5 -0
  186. package/src/client/desktop-env.ts +72 -0
  187. package/src/client/editor-load.ts +92 -0
  188. package/src/client/frame-batcher.ts +56 -0
  189. package/src/client/icons.tsx +152 -0
  190. package/src/client/image-types.ts +8 -0
  191. package/src/client/ime-guard.ts +47 -0
  192. package/src/client/index.tsx +427 -0
  193. package/src/client/intercept.tsx +188 -0
  194. package/src/client/lang.ts +225 -0
  195. package/src/client/layout-push.ts +24 -0
  196. package/src/client/layout.css +90 -0
  197. package/src/client/lazy-chunk.tsx +89 -0
  198. package/src/client/link-intercept.ts +73 -0
  199. package/src/client/locales-ar.ts +368 -0
  200. package/src/client/locales-de.ts +353 -0
  201. package/src/client/locales-fr.ts +360 -0
  202. package/src/client/locales-hi.ts +367 -0
  203. package/src/client/locales-id.ts +365 -0
  204. package/src/client/locales-it.ts +358 -0
  205. package/src/client/locales-ja.ts +367 -0
  206. package/src/client/locales-ko.ts +359 -0
  207. package/src/client/locales-nl.ts +365 -0
  208. package/src/client/locales-pl.ts +369 -0
  209. package/src/client/locales-pt.ts +350 -0
  210. package/src/client/locales-ru.ts +365 -0
  211. package/src/client/locales-sv.ts +350 -0
  212. package/src/client/locales-th.ts +367 -0
  213. package/src/client/locales-tr.ts +367 -0
  214. package/src/client/locales-vi.ts +367 -0
  215. package/src/client/locales-zh-HK.ts +382 -0
  216. package/src/client/locales-zh-MO.ts +382 -0
  217. package/src/client/locales-zh-TW.ts +382 -0
  218. package/src/client/locales.ts +894 -0
  219. package/src/client/markdown-html.ts +331 -0
  220. package/src/client/markdown-images.ts +137 -0
  221. package/src/client/markdown-labels.tsx +52 -0
  222. package/src/client/md-toc.tsx +127 -0
  223. package/src/client/mermaid-blocks.ts +110 -0
  224. package/src/client/mermaid-sanitize.ts +93 -0
  225. package/src/client/mermaid.tsx +406 -0
  226. package/src/client/open-when-sized.ts +43 -0
  227. package/src/client/open-with-settings.tsx +135 -0
  228. package/src/client/open-with.ts +221 -0
  229. package/src/client/openpath-intercept.ts +143 -0
  230. package/src/client/paths.ts +63 -0
  231. package/src/client/pdf-types.ts +4 -0
  232. package/src/client/pinned.ts +183 -0
  233. package/src/client/plugin-settings.ts +43 -0
  234. package/src/client/plugins-shared.ts +29 -0
  235. package/src/client/plugins-tabs.ts +96 -0
  236. package/src/client/plugins-viewers.ts +30 -0
  237. package/src/client/prefs.ts +210 -0
  238. package/src/client/produced-files.ts +120 -0
  239. package/src/client/selection-payload.ts +86 -0
  240. package/src/client/service.ts +859 -0
  241. package/src/client/settings-nav-icon.ts +45 -0
  242. package/src/client/shell-presets.ts +83 -0
  243. package/src/client/sidebar.module.css +2788 -0
  244. package/src/client/sidechat-transcript.ts +321 -0
  245. package/src/client/split-pane.tsx +331 -0
  246. package/src/client/state.ts +1497 -0
  247. package/src/client/subagent-detect.ts +137 -0
  248. package/src/client/subagent-jobs.ts +161 -0
  249. package/src/client/tab-content-memo.ts +55 -0
  250. package/src/client/terminal-font.ts +224 -0
  251. package/src/client/terminal-links.ts +194 -0
  252. package/src/client/theme.ts +110 -0
  253. package/src/client/titlebar-strip.ts +37 -0
  254. package/src/client/upload.ts +187 -0
  255. package/src/client/wco.ts +120 -0
  256. package/src/config.ts +154 -0
  257. package/src/context-types.ts +611 -0
  258. package/src/fs-operations.ts +95 -0
  259. package/src/fs-search.ts +113 -0
  260. package/src/fs-tree.ts +158 -0
  261. package/src/git.ts +446 -0
  262. package/src/html-route.ts +106 -0
  263. package/src/index.ts +1291 -0
  264. package/src/invariant.ts +32 -0
  265. package/src/jobs-routes.ts +257 -0
  266. package/src/open-external.ts +90 -0
  267. package/src/path-security.ts +75 -0
  268. package/src/prefs-shared.ts +280 -0
  269. package/src/pty-deps.ts +240 -0
  270. package/src/pty-manager.ts +376 -0
  271. package/src/sidechat-core.ts +468 -0
  272. package/src/sidechat-routes.ts +344 -0
  273. package/src/subagent-activity.ts +91 -0
  274. package/src/subagent-live-route.ts +93 -0
  275. package/src/tools.ts +482 -0
  276. package/src/trust-fence.ts +84 -0
  277. package/src/wire.ts +100 -0
@@ -0,0 +1,337 @@
1
+ /**
2
+ * One free window: a tab dragged out of the workbench floating over the
3
+ * conversation area at viewport coordinates (rendered inside the panel host,
4
+ * so desktop-shell transforms can never hijack its fixed containing block).
5
+ *
6
+ * The header drags the window with the panel-resize pattern — pointer
7
+ * capture + per-frame direct DOM writes + a store commit on release — and
8
+ * doubling as the DOCK-BACK gesture: while the pointer is over a workbench
9
+ * pane ([data-dsh-pane], either panel), that pane highlights live and
10
+ * releasing docks the tab into it (center merge); releasing anywhere else
11
+ * just moves the window. The SE corner resizes, any press raises (the
12
+ * floats array's order is the stacking order), the header right-click menu
13
+ * and the X button dock / close. The tab content reuses the regular tab
14
+ * renderer, so every tab type (terminal, editor, plugin tabs) floats
15
+ * unchanged.
16
+ */
17
+ import { useEffect, useRef, useState, type ReactNode } from 'react'
18
+ import clsx from 'clsx'
19
+ import { IconCloseFill14, Menu } from '@deepseek-ai/dsh-client-ui-primitives'
20
+ import type { FloatWindow, SidebarTab } from './state.ts'
21
+ import { FLOAT_MIN_H, FLOAT_MIN_W } from './state.ts'
22
+ import { t } from './locales.ts'
23
+ import css from './sidebar.module.css'
24
+
25
+ /** A drag's mutable bookkeeping (start geometry + the last applied values). */
26
+ interface DragState {
27
+ /** Which handle owns the drag (the abort path reads it from this ref —
28
+ * the React `dragging` state lags a render behind pointerdown). */
29
+ mode: 'move' | 'resize'
30
+ pointerX: number
31
+ pointerY: number
32
+ startX: number
33
+ startY: number
34
+ startW: number
35
+ startH: number
36
+ /** Last geometry actually written to the DOM (the cancel-path fallback). */
37
+ applied: { x: number; y: number; w: number; h: number }
38
+ /** Settled after the final pointer event (the up position wins over the
39
+ * rAF-pending value — a fast flick coalesces its tail into the up event). */
40
+ committed: boolean
41
+ }
42
+
43
+ /** The pane under a viewport point, if any (rect hit-test; the dragged
44
+ * window itself is not a pane, so it cannot shadow the targets). */
45
+ function paneAt(x: number, y: number): HTMLElement | null {
46
+ for (const pane of document.querySelectorAll<HTMLElement>('[data-dsh-pane]')) {
47
+ const rect = pane.getBoundingClientRect()
48
+ if (rect.width === 0 || rect.height === 0) continue
49
+ if (x >= rect.left && x <= rect.right && y >= rect.top && y <= rect.bottom) return pane
50
+ }
51
+ return null
52
+ }
53
+
54
+ /** Pointer-capture helpers tolerant of environments without the API (jsdom
55
+ * lacks setPointerCapture — component tests dispatch plain MouseEvents, so
56
+ * the optional calls keep them driving the drag; real browsers always have
57
+ * it and a missing pointerId can never occur there). */
58
+ const capturePointer = (element: HTMLElement, pointerId: number): void => {
59
+ element.setPointerCapture?.(pointerId)
60
+ }
61
+
62
+ const releasePointer = (element: HTMLElement, pointerId: number): void => {
63
+ element.releasePointerCapture?.(pointerId)
64
+ }
65
+
66
+ /** Whether the element holds the pointer (assumed true without the API). */
67
+ const holdsPointer = (element: HTMLElement, pointerId: number): boolean => {
68
+ return element.hasPointerCapture?.(pointerId) !== false
69
+ }
70
+
71
+ export function FreeWindow(props: {
72
+ float: FloatWindow
73
+ renderTab: (tab: SidebarTab, active: boolean, paneId: string) => ReactNode
74
+ getTabIcon?: (tab: SidebarTab) => ReactNode
75
+ onRaise: () => void
76
+ onMove: (x: number, y: number) => void
77
+ onResize: (w: number, h: number) => void
78
+ /** Dock the tab back; the pane id, or null for the active pane. */
79
+ onDock: (paneId: string | null) => void
80
+ onClose: () => void
81
+ }) {
82
+ const { float, renderTab, getTabIcon, onRaise, onMove, onResize, onDock, onClose } = props
83
+ const rootRef = useRef<HTMLDivElement>(null)
84
+ const dragRef = useRef<DragState | null>(null)
85
+ // The pane currently highlighted as the dock target (a direct DOM write:
86
+ // per-frame React state would re-render every mounted tab at drag cadence).
87
+ const dockTargetRef = useRef<HTMLElement | null>(null)
88
+ // rAF coalescing: pointermove outruns the display, one write per frame.
89
+ const frameRef = useRef<number | null>(null)
90
+ const pendingRef = useRef<{ x: number; y: number; w: number; h: number } | null>(null)
91
+ const [dragging, setDragging] = useState<'move' | 'resize' | null>(null)
92
+ const [menu, setMenu] = useState<{ x: number; y: number } | null>(null)
93
+
94
+ useEffect(() => () => {
95
+ if (frameRef.current !== null) cancelAnimationFrame(frameRef.current)
96
+ dockTargetRef.current?.removeAttribute('data-dsh-float-dock-over')
97
+ }, [])
98
+
99
+ /** Apply the pending geometry to the DOM (one rAF per frame at most). */
100
+ const scheduleApply = (geo: { x: number; y: number; w: number; h: number }): void => {
101
+ pendingRef.current = geo
102
+ if (frameRef.current !== null) return
103
+ frameRef.current = requestAnimationFrame(() => {
104
+ frameRef.current = null
105
+ const pending = pendingRef.current
106
+ const drag = dragRef.current
107
+ const root = rootRef.current
108
+ if (pending === null || drag === null || root === null) return
109
+ drag.applied = pending
110
+ root.style.left = `${pending.x}px`
111
+ root.style.top = `${pending.y}px`
112
+ root.style.width = `${pending.w}px`
113
+ root.style.height = `${pending.h}px`
114
+ })
115
+ }
116
+
117
+ /** Flush the pending frame synchronously (the release path's last write). */
118
+ const flushNow = (): void => {
119
+ const pending = pendingRef.current
120
+ const drag = dragRef.current
121
+ const root = rootRef.current
122
+ if (pending === null || drag === null || root === null) return
123
+ if (frameRef.current !== null) {
124
+ cancelAnimationFrame(frameRef.current)
125
+ frameRef.current = null
126
+ }
127
+ pendingRef.current = null
128
+ drag.applied = pending
129
+ root.style.left = `${pending.x}px`
130
+ root.style.top = `${pending.y}px`
131
+ root.style.width = `${pending.w}px`
132
+ root.style.height = `${pending.h}px`
133
+ }
134
+
135
+ const clearDockHighlight = (): void => {
136
+ dockTargetRef.current?.removeAttribute('data-dsh-float-dock-over')
137
+ dockTargetRef.current = null
138
+ }
139
+
140
+ /** Release a drag: `pane` (when set) docks instead of moving. */
141
+ const finishDrag = (mode: 'move' | 'resize', geo: { x: number; y: number; w: number; h: number }, pane: HTMLElement | null): void => {
142
+ const drag = dragRef.current
143
+ if (drag === null || drag.committed) return
144
+ drag.committed = true
145
+ dragRef.current = null
146
+ setDragging(null)
147
+ const target = pane ?? dockTargetRef.current
148
+ clearDockHighlight()
149
+ if (mode === 'move' && target !== null) {
150
+ onDock(target.getAttribute('data-dsh-pane'))
151
+ } else if (mode === 'move') {
152
+ onMove(geo.x, geo.y)
153
+ } else {
154
+ onResize(geo.w, geo.h)
155
+ }
156
+ }
157
+
158
+ /** Cancel-path settle (pointercancel / lostpointercapture): the last
159
+ * APPLIED geometry is the user-visible truth — commit it, never roll back
160
+ * (the panel drags' issue-#247 semantics). The mode comes from the ref —
161
+ * the React `dragging` state can still be null when the cancel lands
162
+ * before the pointerdown re-render commits. */
163
+ const abortDrag = (): void => {
164
+ const drag = dragRef.current
165
+ if (drag === null || drag.committed) return
166
+ flushNow()
167
+ finishDrag(drag.mode, drag.applied, null)
168
+ }
169
+
170
+ const clampMove = (x: number, y: number): { x: number; y: number; w: number; h: number } => {
171
+ const vw = window.innerWidth
172
+ const vh = window.innerHeight
173
+ return {
174
+ x: Math.min(Math.max(x, 0), Math.max(0, vw - float.w)),
175
+ y: Math.min(Math.max(y, 0), Math.max(0, vh - float.h)),
176
+ w: float.w,
177
+ h: float.h,
178
+ }
179
+ }
180
+
181
+ const clampResize = (w: number, h: number): { x: number; y: number; w: number; h: number } => {
182
+ const vw = window.innerWidth
183
+ const vh = window.innerHeight
184
+ return {
185
+ x: float.x,
186
+ y: float.y,
187
+ w: Math.round(Math.min(Math.max(w, FLOAT_MIN_W), Math.max(FLOAT_MIN_W, vw - float.x))),
188
+ h: Math.round(Math.min(Math.max(h, FLOAT_MIN_H), Math.max(FLOAT_MIN_H, vh - float.y))),
189
+ }
190
+ }
191
+
192
+ return (
193
+ <div
194
+ ref={rootRef}
195
+ className={clsx(css.floatWindow, dragging !== null && css.floatWindowDragging)}
196
+ data-dsh-float-window
197
+ data-dsh-float-id={float.id}
198
+ style={{ left: float.x, top: float.y, width: float.w, height: float.h }}
199
+ onPointerDown={() => { onRaise() }}
200
+ >
201
+ <div
202
+ className={css.floatHeader}
203
+ onPointerDown={(event) => {
204
+ if (event.button !== 0) return
205
+ // Portaled overlays (the header Menu) live at document.body in the
206
+ // DOM, yet their events still bubble through the React tree into
207
+ // this handler — without the containment guard a menu-row click
208
+ // would start a drag, preventDefault (killing the row's compat
209
+ // mousedown) and capture the pointer (retargeting the release at
210
+ // the header, so the row's click never fires). Buttons inside the
211
+ // header (the close X) need the same exclusion: a capture here
212
+ // swallows their click.
213
+ if (!(event.target instanceof Node) || !event.currentTarget.contains(event.target)) return
214
+ if (event.target instanceof Element && event.target.closest('button') !== null) return
215
+ event.preventDefault()
216
+ capturePointer(event.currentTarget, event.pointerId)
217
+ dragRef.current = {
218
+ mode: 'move',
219
+ pointerX: event.clientX,
220
+ pointerY: event.clientY,
221
+ startX: float.x,
222
+ startY: float.y,
223
+ startW: float.w,
224
+ startH: float.h,
225
+ applied: { x: float.x, y: float.y, w: float.w, h: float.h },
226
+ committed: false,
227
+ }
228
+ setDragging('move')
229
+ }}
230
+ onPointerMove={(event) => {
231
+ const drag = dragRef.current
232
+ if (drag === null || !holdsPointer(event.currentTarget, event.pointerId)) return
233
+ const geo = clampMove(drag.startX + (event.clientX - drag.pointerX), drag.startY + (event.clientY - drag.pointerY))
234
+ scheduleApply(geo)
235
+ const target = paneAt(event.clientX, event.clientY)
236
+ if (target !== dockTargetRef.current) {
237
+ clearDockHighlight()
238
+ if (target !== null) target.setAttribute('data-dsh-float-dock-over', '')
239
+ dockTargetRef.current = target
240
+ }
241
+ }}
242
+ onPointerUp={(event) => {
243
+ if (dragRef.current === null || !holdsPointer(event.currentTarget, event.pointerId)) return
244
+ releasePointer(event.currentTarget, event.pointerId)
245
+ flushNow()
246
+ const geo = clampMove(
247
+ dragRef.current.startX + (event.clientX - dragRef.current.pointerX),
248
+ dragRef.current.startY + (event.clientY - dragRef.current.pointerY),
249
+ )
250
+ finishDrag('move', geo, paneAt(event.clientX, event.clientY))
251
+ }}
252
+ onPointerCancel={() => { abortDrag() }}
253
+ onLostPointerCapture={() => { abortDrag() }}
254
+ onContextMenu={(event) => {
255
+ event.preventDefault()
256
+ setMenu({ x: event.clientX, y: event.clientY })
257
+ }}
258
+ >
259
+ {getTabIcon?.(float.tab) ?? null}
260
+ <span className={css.floatTitle} title={float.tab.title}>{float.tab.title}</span>
261
+ <button
262
+ type="button"
263
+ className={css.floatClose}
264
+ aria-label={t('close')}
265
+ onClick={(event) => { event.stopPropagation(); onClose() }}
266
+ >
267
+ <IconCloseFill14 />
268
+ </button>
269
+ <Menu
270
+ open={menu !== null}
271
+ onClose={() => { setMenu(null) }}
272
+ items={[
273
+ { id: 'dock', label: t('dockToSidebar') },
274
+ { id: 'close', label: t('close') },
275
+ ]}
276
+ onSelect={(id) => {
277
+ setMenu(null)
278
+ if (id === 'dock') onDock(null)
279
+ else if (id === 'close') onClose()
280
+ }}
281
+ portal
282
+ align="start"
283
+ getAnchorRect={() => (menu === null ? null : new DOMRect(menu.x, menu.y, 0, 0))}
284
+ anchor={<span />}
285
+ />
286
+ </div>
287
+ {/* The tab cell reuses .paneTab (the pane's per-tab wrapper): the
288
+ column-flex chain (window → .floatContent → .paneTab) is what
289
+ gives the component a definite width via align-stretch — the exact
290
+ sizing contract a tab gets inside a pane. */}
291
+ <div className={css.floatContent}>
292
+ <div className={css.paneTab}>{renderTab(float.tab, true, float.id)}</div>
293
+ </div>
294
+ <div
295
+ className={css.floatResize}
296
+ onPointerDown={(event) => {
297
+ if (event.button !== 0) return
298
+ if (!(event.target instanceof Node) || !event.currentTarget.contains(event.target)) return
299
+ event.preventDefault()
300
+ capturePointer(event.currentTarget, event.pointerId)
301
+ dragRef.current = {
302
+ mode: 'resize',
303
+ pointerX: event.clientX,
304
+ pointerY: event.clientY,
305
+ startX: float.x,
306
+ startY: float.y,
307
+ startW: float.w,
308
+ startH: float.h,
309
+ applied: { x: float.x, y: float.y, w: float.w, h: float.h },
310
+ committed: false,
311
+ }
312
+ setDragging('resize')
313
+ }}
314
+ onPointerMove={(event) => {
315
+ const drag = dragRef.current
316
+ if (drag === null || !holdsPointer(event.currentTarget, event.pointerId)) return
317
+ scheduleApply(clampResize(
318
+ drag.startW + (event.clientX - drag.pointerX),
319
+ drag.startH + (event.clientY - drag.pointerY),
320
+ ))
321
+ }}
322
+ onPointerUp={(event) => {
323
+ if (dragRef.current === null || !holdsPointer(event.currentTarget, event.pointerId)) return
324
+ releasePointer(event.currentTarget, event.pointerId)
325
+ flushNow()
326
+ const geo = clampResize(
327
+ dragRef.current.startW + (event.clientX - dragRef.current.pointerX),
328
+ dragRef.current.startH + (event.clientY - dragRef.current.pointerY),
329
+ )
330
+ finishDrag('resize', geo, null)
331
+ }}
332
+ onPointerCancel={() => { abortDrag() }}
333
+ onLostPointerCapture={() => { abortDrag() }}
334
+ />
335
+ </div>
336
+ )
337
+ }