gloomberb 0.16.2 → 0.16.3

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 (124) hide show
  1. package/package.json +1 -1
  2. package/src/brokers/account-cache.ts +45 -0
  3. package/src/cli/command-options.ts +74 -0
  4. package/src/cli/commands/command-utils.ts +29 -0
  5. package/src/cli/commands/market.ts +170 -54
  6. package/src/cli/commands/overview.ts +62 -19
  7. package/src/cli/commands/ticker.ts +138 -43
  8. package/src/cli/desktop-pane-shot.ts +3 -0
  9. package/src/cli/help.ts +26 -6
  10. package/src/cli/history-rows.ts +187 -15
  11. package/src/cli/index.ts +12 -7
  12. package/src/cli/options.ts +5 -1
  13. package/src/cli/pane-functions/capabilities.ts +5 -0
  14. package/src/cli/pane-functions/dom.ts +69 -16
  15. package/src/cli/pane-functions/function-help.ts +4 -2
  16. package/src/cli/pane-functions/headless.ts +163 -31
  17. package/src/cli/pane-functions/index.ts +9 -2
  18. package/src/cli/pane-functions/report.ts +8 -1
  19. package/src/cli/pane-functions/resolver.ts +28 -2
  20. package/src/cli/pane-functions/screenshot.ts +4 -0
  21. package/src/cli/registry.ts +6 -3
  22. package/src/cli/report-tables.ts +460 -0
  23. package/src/cli/result.ts +13 -3
  24. package/src/components/data-table/export.ts +6 -51
  25. package/src/components/data-table/view.tsx +3 -1
  26. package/src/components/layout/shell/active-drag.ts +67 -35
  27. package/src/components/layout/shell/cursor-occlusion.ts +6 -5
  28. package/src/components/layout/shell/drag/index.ts +12 -7
  29. package/src/components/layout/shell/drag/live.ts +122 -0
  30. package/src/components/layout/shell/drag/overlays.tsx +179 -35
  31. package/src/components/layout/shell/drag/runtime.ts +62 -50
  32. package/src/components/layout/shell/drag/slide.ts +10 -0
  33. package/src/components/layout/shell/index.tsx +37 -51
  34. package/src/components/layout/shell/native/pointer-runtime.ts +13 -13
  35. package/src/components/layout/shell/native/surfaces.ts +30 -14
  36. package/src/components/layout/shell/pane/layers.tsx +419 -179
  37. package/src/components/layout/shell/terminal-pointer-runtime.ts +18 -18
  38. package/src/components/layout/shell/window-mode/overlays.tsx +12 -4
  39. package/src/data/json-persistence.ts +17 -3
  40. package/src/data/json-storage.ts +2 -0
  41. package/src/data/memory-resource-store.ts +46 -0
  42. package/src/market-data/market/freshness.ts +45 -0
  43. package/src/market-data/quotes/freshness.ts +5 -0
  44. package/src/plugins/builtin/analytics/pane-model.ts +10 -6
  45. package/src/plugins/builtin/attention/pane.tsx +1 -1
  46. package/src/plugins/builtin/catalysts/detail.tsx +1 -1
  47. package/src/plugins/builtin/company-attention/pane.tsx +1 -1
  48. package/src/plugins/builtin/holders/client.ts +3 -6
  49. package/src/plugins/builtin/holders/format.ts +31 -1
  50. package/src/plugins/builtin/holders/headless.ts +26 -5
  51. package/src/plugins/builtin/holders/pane.tsx +22 -13
  52. package/src/plugins/builtin/holders/report-header.ts +46 -5
  53. package/src/plugins/builtin/kelly-sizer/pane.tsx +12 -6
  54. package/src/plugins/builtin/kelly-sizer/portfolio.ts +25 -0
  55. package/src/plugins/builtin/members/pane.tsx +10 -0
  56. package/src/plugins/builtin/perps/pane.tsx +1 -1
  57. package/src/plugins/builtin/portfolio-list/account-metrics.ts +16 -1
  58. package/src/plugins/builtin/portfolio-list/allocation.ts +3 -0
  59. package/src/plugins/builtin/portfolio-list/cached-account.ts +18 -9
  60. package/src/plugins/builtin/portfolio-list/cli/render.ts +2 -0
  61. package/src/plugins/builtin/portfolio-list/headless.ts +2 -0
  62. package/src/plugins/builtin/portfolio-list/pane/index.tsx +7 -4
  63. package/src/plugins/builtin/portfolio-list/summary/index.tsx +22 -4
  64. package/src/plugins/builtin/portfolio-list/summary/live-accounts.ts +11 -1
  65. package/src/plugins/builtin/research/relative-valuation-model.ts +7 -3
  66. package/src/plugins/builtin/reverse-dcf/model.ts +2 -1
  67. package/src/plugins/builtin/shared/members-menu.tsx +260 -0
  68. package/src/plugins/builtin/short-volume/pane.tsx +1 -1
  69. package/src/plugins/builtin/social-mentions/pane.tsx +1 -1
  70. package/src/plugins/builtin/themes/pane.tsx +3 -0
  71. package/src/plugins/builtin/ticker-detail/financials/aggregation.ts +4 -0
  72. package/src/plugins/builtin/ticker-detail/financials/model.ts +25 -2
  73. package/src/plugins/builtin/ticker-detail/headless.ts +20 -4
  74. package/src/plugins/builtin/ticker-detail/overview/model.ts +9 -4
  75. package/src/plugins/builtin/ticker-detail/overview-tab.tsx +3 -2
  76. package/src/plugins/builtin/world-venue-map/basemap-layer.tsx +2 -0
  77. package/src/plugins/builtin/world-venue-map/basemap.ts +18 -0
  78. package/src/plugins/builtin/world-venue-map/country-labels.ts +78 -0
  79. package/src/plugins/builtin/world-venue-map/geo-svg.tsx +2 -1
  80. package/src/plugins/builtin/world-venue-map/index.ts +5 -5
  81. package/src/plugins/builtin/world-venue-map/layered-pane.tsx +27 -14
  82. package/src/plugins/builtin/world-venue-map/layers.ts +68 -10
  83. package/src/plugins/builtin/world-venue-map/map.tsx +198 -62
  84. package/src/plugins/builtin/world-venue-map/model.ts +16 -0
  85. package/src/plugins/builtin/world-venue-map/natural-earth/index.chunk.ts +3 -0
  86. package/src/plugins/builtin/world-venue-map/natural-earth/labels.chunk.ts +244 -0
  87. package/src/plugins/builtin/world-venue-map/pane.tsx +16 -10
  88. package/src/renderers/browser/app-services.ts +3 -1
  89. package/src/renderers/dom/data-table/dom.ts +31 -0
  90. package/src/renderers/dom/data-table/index.tsx +5 -0
  91. package/src/renderers/dom/host/box.tsx +113 -17
  92. package/src/renderers/dom/host/drag-shield.ts +24 -0
  93. package/src/renderers/dom/host/live-frame.ts +54 -0
  94. package/src/renderers/dom/host/mouse.ts +8 -1
  95. package/src/renderers/dom/host/scroll-box.tsx +6 -5
  96. package/src/renderers/dom/host/tabs.tsx +3 -2
  97. package/src/renderers/dom/styles.css +13 -5
  98. package/src/renderers/electrobun/bun/desktop/backend-requests.ts +7 -0
  99. package/src/renderers/electrobun/bun/desktop/backend.ts +2 -0
  100. package/src/renderers/electrobun/bun/desktop/initialization.ts +13 -0
  101. package/src/renderers/electrobun/shared/protocol.ts +10 -1
  102. package/src/renderers/electrobun/view/cli-pane-shot-entry.tsx +3 -0
  103. package/src/renderers/electrobun/view/host/native.ts +0 -10
  104. package/src/renderers/electrobun/view/host/window-move.ts +28 -0
  105. package/src/renderers/electrobun/view/remote/persistence.ts +15 -2
  106. package/src/renderers/electrobun/view/ui-host.tsx +2 -5
  107. package/src/renderers/opentui/ui-host.tsx +7 -1
  108. package/src/sources/gloomberb-cloud/normalizers.ts +16 -20
  109. package/src/sources/provider-router/batches.ts +3 -3
  110. package/src/sources/provider-router/financial-routes.ts +2 -2
  111. package/src/sources/provider-router/financials.ts +70 -7
  112. package/src/sources/provider-router/primary.ts +2 -2
  113. package/src/state/pane-scroll-registry.tsx +67 -35
  114. package/src/test-support/margin-account.ts +108 -0
  115. package/src/time-series/resolution.ts +15 -0
  116. package/src/types/financials.ts +32 -3
  117. package/src/ui/host.tsx +12 -0
  118. package/src/ui/index.tsx +8 -3
  119. package/src/ui/scroll-box-axis.ts +26 -0
  120. package/src/utils/currency-units.ts +8 -3
  121. package/src/utils/depositary-receipt.ts +54 -10
  122. package/src/utils/display-number.ts +63 -0
  123. package/src/utils/fundamentals.ts +17 -1
  124. package/src/utils/price-history-integrity.ts +19 -6
@@ -1,5 +1,5 @@
1
- import { useCallback, useMemo, useRef } from "react";
2
- import { Box } from "../../../../ui";
1
+ import { memo, useCallback, useMemo, useRef } from "react";
2
+ import { Box, type BoxRenderable, type LiveBoxFrame } from "../../../../ui";
3
3
  import { resolveOccludedPaneIds } from "../pane-occlusion";
4
4
  import type {
5
5
  DockDividerLayout,
@@ -10,6 +10,7 @@ import type {
10
10
  } from "../../../../layout/pane-manager";
11
11
  import { colors } from "../../../../theme/colors";
12
12
  import { constrainFloatingRectToBounds } from "../drag";
13
+ import { useLiveBoxFrame, useLiveDrag, type LiveDragGeometry, type LiveDragStore } from "../drag/live";
13
14
  import { pathKey } from "../../window-edit/mode";
14
15
  import { FloatingPaneWrapper } from "../../floating-pane";
15
16
  import { PaneContent } from "../../pane/content";
@@ -17,7 +18,6 @@ import { PaneWrapper } from "../../pane";
17
18
  import type { PaneHeaderQuickSetting } from "../../pane/header";
18
19
  import { hasPaneFooterContent, PaneFooterKeys, PaneFooterProvider } from "../../pane/footer";
19
20
  import { resolvePaneBodyFrame, shouldReservePaneFooter } from "../../pane/sizing";
20
- import type { DividerPreviewState } from "../native/window-state";
21
21
 
22
22
  type ShellMouseHandler = (event: any) => void;
23
23
 
@@ -28,10 +28,8 @@ interface VisibleFloatingPane {
28
28
 
29
29
  interface ShellPaneLayersProps {
30
30
  contentHeight: number;
31
- dividerPreview: DividerPreviewState | null;
32
31
  dockDividerLayouts: DockDividerLayout[];
33
32
  dockLeafLayouts: DockLeafLayout[];
34
- dragFloatingRect: { paneId: string; rect: FloatingRect } | null;
35
33
  focusedPaneId: string | null;
36
34
  getPaneTitle: (pane: ResolvedPane) => string;
37
35
  getPaneQuickSettings: (paneId: string) => PaneHeaderQuickSetting[];
@@ -43,6 +41,7 @@ interface ShellPaneLayersProps {
43
41
  handleNativePaneMouseDown: (paneId: string, event: any) => void;
44
42
  handlePaneAction: (paneId: string, rect: LayoutBounds, event: any) => void;
45
43
  hoveredPaneId: string | null;
44
+ live: LiveDragStore;
46
45
  menuPaneId: string | null;
47
46
  nativeContextMenu?: boolean;
48
47
  nativePaneChrome: boolean;
@@ -61,14 +60,334 @@ interface ShellPaneLayersProps {
61
60
  windowModePaneId: string | null;
62
61
  }
63
62
 
63
+ /**
64
+ * What a pane's chrome calls back into the shell. One stable object that
65
+ * always reaches the shell's current handlers, so a pane layer re-renders only
66
+ * when something it draws changed, not whenever the shell does.
67
+ */
68
+ interface PaneLayerActions {
69
+ paneMouseDown: (paneId: string, event: any) => void;
70
+ hover: (paneId: string) => void;
71
+ dockedDragStart: (paneId: string, rect: LayoutBounds, event: any) => void;
72
+ floatingDragStart: (paneId: string, rect: FloatingRect, event: any) => void;
73
+ floatResizeStart: (paneId: string, rect: FloatingRect, event: any) => void;
74
+ drag: ShellMouseHandler;
75
+ contextMenu: (paneId: string, rect: LayoutBounds, event: any) => void;
76
+ menu: (paneId: string, rect: LayoutBounds, event: any) => void;
77
+ closeMouseDown: (paneId: string, event: any) => void;
78
+ /** Also the failure card's Close: keep it stable, the pane body is memoized on it. */
79
+ close: (paneId: string) => void;
80
+ restoreFullscreen: ShellMouseHandler;
81
+ dividerDragStart: (divider: DockDividerLayout, event: any) => void;
82
+ }
83
+
84
+ interface PaneLayerProps {
85
+ pane: ResolvedPane;
86
+ rect: LayoutBounds;
87
+ title: string;
88
+ focused: boolean;
89
+ windowModeSelected: boolean;
90
+ showActions: boolean;
91
+ /** Fully covered (terminal only): skip drawing, keep the tree mounted. */
92
+ hidden: boolean;
93
+ inView: boolean;
94
+ fullscreen: boolean;
95
+ /** Whether the header can start a move: not in fullscreen. */
96
+ movable: boolean;
97
+ nativePaneChrome: boolean;
98
+ nativeContextMenu: boolean;
99
+ getPaneQuickSettings: (paneId: string) => PaneHeaderQuickSetting[];
100
+ actions: PaneLayerActions;
101
+ }
102
+
64
103
  const EMPTY_OCCLUSION: ReadonlySet<string> = new Set();
104
+ const selectFloatingDragActive = (geometry: LiveDragGeometry) => geometry.floating !== null;
105
+ const selectNothing = () => null;
106
+ const selectNoDrag = () => false;
107
+
108
+ function usePaneQuickSettings(getPaneQuickSettings: PaneLayerProps["getPaneQuickSettings"], paneId: string) {
109
+ return useMemo(() => getPaneQuickSettings(paneId), [getPaneQuickSettings, paneId]);
110
+ }
111
+
112
+ const DockedPaneLayer = memo(function DockedPaneLayer({
113
+ pane,
114
+ rect,
115
+ title,
116
+ focused,
117
+ windowModeSelected,
118
+ showActions,
119
+ hidden,
120
+ inView,
121
+ fullscreen,
122
+ movable,
123
+ nativePaneChrome,
124
+ nativeContextMenu,
125
+ getPaneQuickSettings,
126
+ actions,
127
+ }: PaneLayerProps) {
128
+ const paneId = pane.instance.instanceId;
129
+ const quickSettings = usePaneQuickSettings(getPaneQuickSettings, paneId);
130
+ const nativeMove = nativePaneChrome && movable;
131
+ return (
132
+ <Box
133
+ position="absolute"
134
+ left={rect.x}
135
+ top={rect.y}
136
+ width={rect.width}
137
+ height={rect.height}
138
+ visible={!hidden}
139
+ >
140
+ <PaneFooterProvider>
141
+ {(footer) => {
142
+ const showFooter = hasPaneFooterContent(footer);
143
+ const reserveFooter = shouldReservePaneFooter(nativePaneChrome, showFooter);
144
+ const bodyFrame = resolvePaneBodyFrame({
145
+ width: rect.width,
146
+ height: rect.height,
147
+ nativePaneChrome,
148
+ footerVisible: reserveFooter || showFooter,
149
+ reserveFooter,
150
+ });
151
+ return (
152
+ <PaneWrapper
153
+ paneId={paneId}
154
+ title={title}
155
+ focused={focused}
156
+ width={rect.width}
157
+ height={rect.height}
158
+ locked={pane.instance.locked === true}
159
+ showActions={showActions}
160
+ quickSettings={quickSettings}
161
+ topRule={rect.y > 0.01}
162
+ windowModeSelected={windowModeSelected}
163
+ footer={footer}
164
+ onMouseDownCapture={nativePaneChrome ? (event) => actions.paneMouseDown(paneId, event) : undefined}
165
+ onHeaderMouseMove={() => actions.hover(paneId)}
166
+ onHeaderMouseDown={nativeMove ? (event) => actions.dockedDragStart(paneId, rect, event) : undefined}
167
+ onHeaderMouseDrag={nativeMove ? actions.drag : undefined}
168
+ onHeaderMouseDragEnd={nativeMove ? actions.drag : undefined}
169
+ onHeaderContextMenu={nativePaneChrome && nativeContextMenu ? (event) => actions.contextMenu(paneId, rect, event) : undefined}
170
+ onActionMouseDown={(event) => actions.menu(paneId, rect, event)}
171
+ fullscreen={fullscreen}
172
+ onRestoreMouseDown={fullscreen ? actions.restoreFullscreen : undefined}
173
+ >
174
+ <PaneFooterKeys paneId={paneId} footer={footer} focused={focused} />
175
+ <PaneContent
176
+ component={pane.def.component}
177
+ paneId={paneId}
178
+ paneType={pane.instance.paneId}
179
+ title={title}
180
+ focused={focused}
181
+ width={bodyFrame.width ?? 1}
182
+ height={bodyFrame.height ?? 1}
183
+ inView={inView}
184
+ closePane={actions.close}
185
+ />
186
+ </PaneWrapper>
187
+ );
188
+ }}
189
+ </PaneFooterProvider>
190
+ </Box>
191
+ );
192
+ });
193
+
194
+ const FloatingPaneFrame = memo(function FloatingPaneFrame({
195
+ pane,
196
+ rect,
197
+ title,
198
+ focused,
199
+ windowModeSelected,
200
+ showActions,
201
+ hidden,
202
+ inView,
203
+ fullscreen,
204
+ movable,
205
+ nativePaneChrome,
206
+ nativeContextMenu,
207
+ getPaneQuickSettings,
208
+ actions,
209
+ zIndex,
210
+ }: PaneLayerProps & { rect: FloatingRect; zIndex: number }) {
211
+ const paneId = pane.instance.instanceId;
212
+ const quickSettings = usePaneQuickSettings(getPaneQuickSettings, paneId);
213
+ const nativeMove = nativePaneChrome && movable;
214
+ return (
215
+ <PaneFooterProvider>
216
+ {(footer) => {
217
+ const showFooter = hasPaneFooterContent(footer);
218
+ const reserveFooter = shouldReservePaneFooter(nativePaneChrome, showFooter);
219
+ const bodyFrame = resolvePaneBodyFrame({
220
+ width: rect.width,
221
+ height: rect.height,
222
+ nativePaneChrome,
223
+ footerVisible: reserveFooter || showFooter,
224
+ reserveFooter,
225
+ });
226
+ return (
227
+ <FloatingPaneWrapper
228
+ paneId={paneId}
229
+ title={title}
230
+ x={rect.x}
231
+ y={rect.y}
232
+ width={rect.width}
233
+ height={rect.height}
234
+ zIndex={zIndex}
235
+ hidden={hidden}
236
+ focused={focused}
237
+ windowModeSelected={windowModeSelected}
238
+ locked={pane.instance.locked === true}
239
+ showActions={showActions}
240
+ quickSettings={quickSettings}
241
+ footer={footer}
242
+ onMouseDownCapture={nativePaneChrome ? (event) => actions.paneMouseDown(paneId, event) : undefined}
243
+ onHeaderMouseMove={() => actions.hover(paneId)}
244
+ onHeaderMouseDown={nativeMove ? (event) => actions.floatingDragStart(paneId, rect, event) : undefined}
245
+ onHeaderMouseDrag={nativeMove ? actions.drag : undefined}
246
+ onHeaderMouseDragEnd={nativeMove ? actions.drag : undefined}
247
+ onHeaderContextMenu={nativePaneChrome && nativeContextMenu ? (event) => actions.contextMenu(paneId, rect, event) : undefined}
248
+ onActionMouseDown={(event) => actions.menu(paneId, rect, event)}
249
+ onCloseMouseDown={fullscreen ? undefined : (event) => actions.closeMouseDown(paneId, event)}
250
+ onRestoreMouseDown={fullscreen ? actions.restoreFullscreen : undefined}
251
+ fullscreen={fullscreen}
252
+ onResizeMouseDown={nativeMove ? (event) => actions.floatResizeStart(paneId, rect, event) : undefined}
253
+ onResizeMouseDrag={nativeMove ? actions.drag : undefined}
254
+ onResizeMouseDragEnd={nativeMove ? actions.drag : undefined}
255
+ >
256
+ <PaneFooterKeys paneId={paneId} footer={footer} focused={focused} />
257
+ <PaneContent
258
+ component={pane.def.component}
259
+ paneId={paneId}
260
+ paneType={pane.instance.paneId}
261
+ title={title}
262
+ focused={focused}
263
+ width={bodyFrame.width ?? 1}
264
+ height={bodyFrame.height ?? 1}
265
+ inView={inView}
266
+ onClose={actions.close}
267
+ closePane={actions.close}
268
+ />
269
+ </FloatingPaneWrapper>
270
+ );
271
+ }}
272
+ </PaneFooterProvider>
273
+ );
274
+ });
275
+
276
+ /**
277
+ * A floating pane, drawn at its layout rect or where a drag has it right now.
278
+ * On the desktop a move never renders: the pane's layer is promoted at the
279
+ * press and each frame of the move only writes its transform (see
280
+ * `useLiveBoxFrame`), so nothing inside it re-renders, lays out or repaints
281
+ * until the release commits the new position. A resize, and every terminal
282
+ * frame, redraws it at the drag's rect.
283
+ */
284
+ const FloatingPaneLayer = memo(function FloatingPaneLayer({
285
+ live,
286
+ rect,
287
+ width,
288
+ contentHeight,
289
+ zIndex,
290
+ ...frame
291
+ }: PaneLayerProps & { rect: FloatingRect; zIndex: number; live: LiveDragStore; width: number; contentHeight: number }) {
292
+ const paneId = frame.pane.instance.instanceId;
293
+ const slide = frame.nativePaneChrome;
294
+ const selectRedraw = useCallback((geometry: LiveDragGeometry) => {
295
+ if (geometry.floating?.paneId !== paneId) return null;
296
+ // A desktop move follows on the pane's own layer instead.
297
+ if (slide && geometry.paneDrag?.paneId === paneId) return null;
298
+ return geometry.floating.rect;
299
+ }, [paneId, slide]);
300
+ const dragRect = useLiveDrag(live, selectRedraw);
301
+ const layerRef = useRef<BoxRenderable | null>(null);
302
+ const frameOf = useMemo(() => (slide ? (geometry: LiveDragGeometry): LiveBoxFrame | null => {
303
+ if (geometry.paneDrag?.paneId !== paneId) return null;
304
+ const moving = geometry.floating?.paneId === paneId ? geometry.floating.rect : null;
305
+ // Promoted from the press, so the first move already only slides it.
306
+ if (!moving) return { dx: 0, dy: 0 };
307
+ const at = constrainFloatingRectToBounds(moving, width, contentHeight);
308
+ return { dx: at.x - rect.x, dy: at.y - rect.y };
309
+ } : null), [contentHeight, paneId, rect.x, rect.y, slide, width]);
310
+ useLiveBoxFrame(live, layerRef, frameOf);
311
+ const preview = dragRect ? constrainFloatingRectToBounds(dragRect, width, contentHeight) : rect;
312
+ const content = <FloatingPaneFrame {...frame} rect={preview} zIndex={zIndex} />;
313
+ if (!slide) return content;
314
+ return (
315
+ <Box
316
+ ref={layerRef}
317
+ position="absolute"
318
+ left={0}
319
+ top={0}
320
+ width={0}
321
+ height={0}
322
+ zIndex={zIndex}
323
+ >
324
+ {content}
325
+ </Box>
326
+ );
327
+ });
328
+
329
+ /**
330
+ * A tiled split's divider, which follows the pointer while it is dragged: on
331
+ * the desktop by restyling itself each move, in the terminal by redrawing.
332
+ */
333
+ const DockDividerLayer = memo(function DockDividerLayer({
334
+ divider,
335
+ resizing,
336
+ live,
337
+ nativePaneChrome,
338
+ actions,
339
+ }: {
340
+ divider: DockDividerLayout;
341
+ /** Window mode is resizing this split from the keyboard. */
342
+ resizing: boolean;
343
+ live: LiveDragStore;
344
+ nativePaneChrome: boolean;
345
+ actions: PaneLayerActions;
346
+ }) {
347
+ const dividerPathKey = pathKey(divider.path);
348
+ const selectDragged = useCallback((geometry: LiveDragGeometry) => (
349
+ geometry.divider?.pathKey === dividerPathKey
350
+ ), [dividerPathKey]);
351
+ const selectRect = useCallback((geometry: LiveDragGeometry) => (
352
+ geometry.divider?.pathKey === dividerPathKey ? geometry.divider.rect : null
353
+ ), [dividerPathKey]);
354
+ const dragged = useLiveDrag(live, selectDragged);
355
+ const previewRect = useLiveDrag(live, nativePaneChrome ? selectNothing : selectRect);
356
+ const boxRef = useRef<BoxRenderable | null>(null);
357
+ const frameOf = useMemo(() => (nativePaneChrome ? (geometry: LiveDragGeometry): LiveBoxFrame | null => {
358
+ const at = geometry.divider?.pathKey === dividerPathKey ? geometry.divider.rect : null;
359
+ return at ? { dx: at.x - divider.rect.x, dy: at.y - divider.rect.y } : null;
360
+ } : null), [divider.rect.x, divider.rect.y, dividerPathKey, nativePaneChrome]);
361
+ useLiveBoxFrame(live, boxRef, frameOf);
362
+ const active = dragged || resizing;
363
+ const rect = previewRect ?? divider.rect;
364
+ return (
365
+ <Box
366
+ ref={boxRef}
367
+ position="absolute"
368
+ left={rect.x}
369
+ top={rect.y}
370
+ width={rect.width}
371
+ height={rect.height}
372
+ zIndex={active ? 2 : 1}
373
+ backgroundColor={active ? colors.borderFocused : colors.border}
374
+ {...(nativePaneChrome ? {
375
+ "data-gloom-role": "dock-divider",
376
+ "data-axis": divider.axis,
377
+ "data-active": active ? "true" : "false",
378
+ style: { "--divider-color": active ? colors.borderFocused : colors.border } as any,
379
+ } : {})}
380
+ onMouseDown={nativePaneChrome ? (event: any) => actions.dividerDragStart(divider, event) : undefined}
381
+ onMouseDrag={nativePaneChrome ? actions.drag : undefined}
382
+ onMouseDragEnd={nativePaneChrome ? actions.drag : undefined}
383
+ />
384
+ );
385
+ });
65
386
 
66
387
  export function ShellPaneLayers({
67
388
  contentHeight,
68
- dividerPreview,
69
389
  dockDividerLayouts,
70
390
  dockLeafLayouts,
71
- dragFloatingRect,
72
391
  focusedPaneId,
73
392
  getPaneTitle,
74
393
  getPaneQuickSettings,
@@ -80,8 +399,9 @@ export function ShellPaneLayers({
80
399
  handleNativePaneMouseDown,
81
400
  handlePaneAction,
82
401
  hoveredPaneId,
402
+ live,
83
403
  menuPaneId,
84
- nativeContextMenu,
404
+ nativeContextMenu = false,
85
405
  nativePaneChrome,
86
406
  overlayOpen,
87
407
  paneMap,
@@ -116,12 +436,75 @@ export function ShellPaneLayers({
116
436
  // Skipping the draw is a terminal concern: desktop pane chrome is DOM, where
117
437
  // the compositor already skips covered windows, and a drag keeps everything
118
438
  // drawn so the preview never reveals a blank spot.
119
- const occludedPaneIds = nativePaneChrome || dragFloatingRect ? EMPTY_OCCLUSION : coveredPaneIds;
120
- // The failure card's Close. Stable, so a focus or layout change does not
121
- // re-render every memoized pane body.
122
- const closePaneRef = useRef(handleFloatingClose);
123
- closePaneRef.current = handleFloatingClose;
124
- const closePane = useCallback((paneId: string) => closePaneRef.current(paneId), []);
439
+ const floatingDragActive = useLiveDrag(live, nativePaneChrome ? selectNoDrag : selectFloatingDragActive);
440
+ const occludedPaneIds = nativePaneChrome || floatingDragActive ? EMPTY_OCCLUSION : coveredPaneIds;
441
+ const fullscreenRect = useMemo(() => ({ x: 0, y: 0, width, height: contentHeight }), [contentHeight, width]);
442
+
443
+ const handlersRef = useRef({
444
+ handleFloatingClose,
445
+ handleFloatingCloseMouseDown,
446
+ handleRestoreFullscreen,
447
+ handleNativeDrag,
448
+ handleNativePaneContextMenu,
449
+ handleNativePaneMouseDown,
450
+ handlePaneAction,
451
+ setHoveredPaneIfChanged,
452
+ startNativeDividerDrag,
453
+ startNativeDockedDrag,
454
+ startNativeFloatingDrag,
455
+ startNativeFloatResize,
456
+ });
457
+ handlersRef.current = {
458
+ handleFloatingClose,
459
+ handleFloatingCloseMouseDown,
460
+ handleRestoreFullscreen,
461
+ handleNativeDrag,
462
+ handleNativePaneContextMenu,
463
+ handleNativePaneMouseDown,
464
+ handlePaneAction,
465
+ setHoveredPaneIfChanged,
466
+ startNativeDividerDrag,
467
+ startNativeDockedDrag,
468
+ startNativeFloatingDrag,
469
+ startNativeFloatResize,
470
+ };
471
+ // Only called from events, never while rendering, so reading the latest handlers is safe.
472
+ const actions = useMemo<PaneLayerActions>(() => ({
473
+ paneMouseDown: (paneId, event) => handlersRef.current.handleNativePaneMouseDown(paneId, event),
474
+ hover: (paneId) => handlersRef.current.setHoveredPaneIfChanged(paneId),
475
+ dockedDragStart: (paneId, rect, event) => handlersRef.current.startNativeDockedDrag(paneId, rect, event),
476
+ floatingDragStart: (paneId, rect, event) => handlersRef.current.startNativeFloatingDrag(paneId, rect, event),
477
+ floatResizeStart: (paneId, rect, event) => handlersRef.current.startNativeFloatResize(paneId, rect, event),
478
+ drag: (event) => handlersRef.current.handleNativeDrag(event),
479
+ contextMenu: (paneId, rect, event) => handlersRef.current.handleNativePaneContextMenu(paneId, rect, event),
480
+ menu: (paneId, rect, event) => handlersRef.current.handlePaneAction(paneId, rect, event),
481
+ closeMouseDown: (paneId, event) => handlersRef.current.handleFloatingCloseMouseDown(paneId, event),
482
+ close: (paneId) => handlersRef.current.handleFloatingClose(paneId),
483
+ restoreFullscreen: (event) => handlersRef.current.handleRestoreFullscreen(event),
484
+ dividerDragStart: (divider, event) => handlersRef.current.startNativeDividerDrag(divider, event),
485
+ }), []);
486
+
487
+ const layerProps = (pane: ResolvedPane, rect: LayoutBounds): PaneLayerProps => {
488
+ const paneId = pane.instance.instanceId;
489
+ const focused = focusedPaneId === paneId && (!overlayOpen || menuPaneId === paneId);
490
+ const fullscreen = transientFocusActive && paneId === transientFocusPaneId;
491
+ return {
492
+ pane,
493
+ rect,
494
+ title: getPaneTitle(pane),
495
+ focused,
496
+ windowModeSelected: windowModePaneId === paneId,
497
+ showActions: focused || hoveredPaneId === paneId || menuPaneId === paneId,
498
+ hidden: occludedPaneIds.has(paneId),
499
+ inView: !coveredPaneIds.has(paneId),
500
+ fullscreen,
501
+ movable: !transientFocusActive,
502
+ nativePaneChrome,
503
+ nativeContextMenu,
504
+ getPaneQuickSettings,
505
+ actions,
506
+ };
507
+ };
125
508
 
126
509
  return (
127
510
  <>
@@ -129,182 +512,39 @@ export function ShellPaneLayers({
129
512
  if (transientFocusActive && leaf.instanceId !== transientFocusPaneId) return null;
130
513
  const pane = paneMap.get(leaf.instanceId);
131
514
  if (!pane) return null;
132
- const rect = transientFocusActive
133
- ? { x: 0, y: 0, width, height: contentHeight }
134
- : leaf.rect;
135
- const isFullscreenBase = transientFocusActive && leaf.instanceId === transientFocusPaneId;
136
- const focused = focusedPaneId === leaf.instanceId && (!overlayOpen || menuPaneId === leaf.instanceId);
137
- const windowModeSelected = windowModePaneId === leaf.instanceId;
138
- const showActions = focused || hoveredPaneId === leaf.instanceId || menuPaneId === leaf.instanceId;
139
- const title = getPaneTitle(pane);
140
515
  return (
141
- <Box
516
+ <DockedPaneLayer
142
517
  key={`dock:${leaf.instanceId}`}
143
- position="absolute"
144
- left={rect.x}
145
- top={rect.y}
146
- width={rect.width}
147
- height={rect.height}
148
- visible={!occludedPaneIds.has(leaf.instanceId)}
149
- >
150
- <PaneFooterProvider>
151
- {(footer) => {
152
- const showFooter = hasPaneFooterContent(footer);
153
- const reserveFooter = shouldReservePaneFooter(nativePaneChrome, showFooter);
154
- const renderFooter = reserveFooter || showFooter;
155
- const bodyFrame = resolvePaneBodyFrame({
156
- width: rect.width,
157
- height: rect.height,
158
- nativePaneChrome,
159
- footerVisible: renderFooter,
160
- reserveFooter,
161
- });
162
- return (
163
- <PaneWrapper
164
- paneId={leaf.instanceId}
165
- title={title}
166
- focused={focused}
167
- width={rect.width}
168
- height={rect.height}
169
- locked={pane.instance.locked === true}
170
- showActions={showActions}
171
- quickSettings={getPaneQuickSettings(leaf.instanceId)}
172
- topRule={rect.y > 0.01}
173
- windowModeSelected={windowModeSelected}
174
- footer={footer}
175
- onMouseDownCapture={nativePaneChrome ? (event) => handleNativePaneMouseDown(leaf.instanceId, event) : undefined}
176
- onHeaderMouseMove={() => setHoveredPaneIfChanged(leaf.instanceId)}
177
- onHeaderMouseDown={nativePaneChrome && !transientFocusActive ? (event) => startNativeDockedDrag(leaf.instanceId, rect, event) : undefined}
178
- onHeaderMouseDrag={nativePaneChrome && !transientFocusActive ? handleNativeDrag : undefined}
179
- onHeaderMouseDragEnd={nativePaneChrome && !transientFocusActive ? handleNativeDrag : undefined}
180
- onHeaderContextMenu={nativePaneChrome && nativeContextMenu === true ? (event) => handleNativePaneContextMenu(leaf.instanceId, rect, event) : undefined}
181
- onActionMouseDown={(event) => handlePaneAction(leaf.instanceId, rect, event)}
182
- fullscreen={isFullscreenBase}
183
- onRestoreMouseDown={isFullscreenBase ? handleRestoreFullscreen : undefined}
184
- >
185
- <PaneFooterKeys paneId={leaf.instanceId} footer={footer} focused={focused} />
186
- <PaneContent
187
- component={pane.def.component}
188
- paneId={pane.instance.instanceId}
189
- paneType={pane.instance.paneId}
190
- title={title}
191
- focused={focused}
192
- width={bodyFrame.width ?? 1}
193
- height={bodyFrame.height ?? 1}
194
- inView={!coveredPaneIds.has(leaf.instanceId)}
195
- closePane={closePane}
196
- />
197
- </PaneWrapper>
198
- );
199
- }}
200
- </PaneFooterProvider>
201
- </Box>
518
+ {...layerProps(pane, transientFocusActive ? fullscreenRect : leaf.rect)}
519
+ />
202
520
  );
203
521
  })}
204
522
 
205
523
  {visibleFloatingPanes.map(({ pane, rect }) => {
206
524
  if (transientFocusActive && pane.instance.instanceId !== transientFocusPaneId) return null;
207
- const isFullscreenBase = transientFocusActive && pane.instance.instanceId === transientFocusPaneId;
208
- const preview = transientFocusActive
209
- ? { x: 0, y: 0, width, height: contentHeight }
210
- : dragFloatingRect?.paneId === pane.instance.instanceId
211
- ? constrainFloatingRectToBounds(dragFloatingRect.rect, width, contentHeight)
212
- : rect;
213
- const focused = focusedPaneId === pane.instance.instanceId && (!overlayOpen || menuPaneId === pane.instance.instanceId);
214
- const windowModeSelected = windowModePaneId === pane.instance.instanceId;
215
- const showActions = focused || hoveredPaneId === pane.instance.instanceId || menuPaneId === pane.instance.instanceId;
216
- const title = getPaneTitle(pane);
217
- return (
218
- <PaneFooterProvider key={`float:${pane.instance.instanceId}`}>
219
- {(footer) => {
220
- const showFooter = hasPaneFooterContent(footer);
221
- const reserveFooter = shouldReservePaneFooter(nativePaneChrome, showFooter);
222
- const renderFooter = reserveFooter || showFooter;
223
- const bodyFrame = resolvePaneBodyFrame({
224
- width: preview.width,
225
- height: preview.height,
226
- nativePaneChrome,
227
- footerVisible: renderFooter,
228
- reserveFooter,
229
- });
230
- return (
231
- <FloatingPaneWrapper
232
- paneId={pane.instance.instanceId}
233
- title={title}
234
- x={preview.x}
235
- y={preview.y}
236
- width={preview.width}
237
- height={preview.height}
238
- zIndex={pane.floating?.zIndex ?? 50}
239
- hidden={occludedPaneIds.has(pane.instance.instanceId)}
240
- focused={focused}
241
- windowModeSelected={windowModeSelected}
242
- locked={pane.instance.locked === true}
243
- showActions={showActions}
244
- quickSettings={getPaneQuickSettings(pane.instance.instanceId)}
245
- footer={footer}
246
- onMouseDownCapture={nativePaneChrome ? (event) => handleNativePaneMouseDown(pane.instance.instanceId, event) : undefined}
247
- onHeaderMouseMove={() => setHoveredPaneIfChanged(pane.instance.instanceId)}
248
- onHeaderMouseDown={nativePaneChrome && !isFullscreenBase ? (event) => startNativeFloatingDrag(pane.instance.instanceId, preview, event) : undefined}
249
- onHeaderMouseDrag={nativePaneChrome && !isFullscreenBase ? handleNativeDrag : undefined}
250
- onHeaderMouseDragEnd={nativePaneChrome && !isFullscreenBase ? handleNativeDrag : undefined}
251
- onHeaderContextMenu={nativePaneChrome && nativeContextMenu === true ? (event) => handleNativePaneContextMenu(pane.instance.instanceId, preview, event) : undefined}
252
- onActionMouseDown={(event) => handlePaneAction(pane.instance.instanceId, preview, event)}
253
- onCloseMouseDown={isFullscreenBase ? undefined : (event) => handleFloatingCloseMouseDown(pane.instance.instanceId, event)}
254
- onRestoreMouseDown={isFullscreenBase ? handleRestoreFullscreen : undefined}
255
- fullscreen={isFullscreenBase}
256
- onResizeMouseDown={nativePaneChrome && !isFullscreenBase ? (event) => startNativeFloatResize(pane.instance.instanceId, preview, event) : undefined}
257
- onResizeMouseDrag={nativePaneChrome && !isFullscreenBase ? handleNativeDrag : undefined}
258
- onResizeMouseDragEnd={nativePaneChrome && !isFullscreenBase ? handleNativeDrag : undefined}
259
- >
260
- <PaneFooterKeys paneId={pane.instance.instanceId} footer={footer} focused={focused} />
261
- <PaneContent
262
- component={pane.def.component}
263
- paneId={pane.instance.instanceId}
264
- paneType={pane.instance.paneId}
265
- title={title}
266
- focused={focused}
267
- width={bodyFrame.width ?? 1}
268
- height={bodyFrame.height ?? 1}
269
- inView={!coveredPaneIds.has(pane.instance.instanceId)}
270
- onClose={handleFloatingClose}
271
- closePane={closePane}
272
- />
273
- </FloatingPaneWrapper>
274
- );
275
- }}
276
- </PaneFooterProvider>
277
- );
278
- })}
279
-
280
- {dockDividerLayouts.map((divider) => {
281
- if (transientFocusActive) return null;
282
- const dividerPathKey = pathKey(divider.path);
283
- const previewActive = dividerPreview?.pathKey === dividerPathKey;
284
- const active = previewActive || windowModeDockResizePathKey === dividerPathKey;
285
- const rect = previewActive ? dividerPreview.rect : divider.rect;
286
525
  return (
287
- <Box
288
- key={`divider:${divider.path.join(".")}`}
289
- position="absolute"
290
- left={rect.x}
291
- top={rect.y}
292
- width={rect.width}
293
- height={rect.height}
294
- zIndex={active ? 2 : 1}
295
- backgroundColor={active ? colors.borderFocused : colors.border}
296
- {...(nativePaneChrome ? {
297
- "data-gloom-role": "dock-divider",
298
- "data-axis": divider.axis,
299
- "data-active": active ? "true" : "false",
300
- style: { "--divider-color": active ? colors.borderFocused : colors.border } as any,
301
- } : {})}
302
- onMouseDown={nativePaneChrome ? (event: any) => startNativeDividerDrag(divider, event) : undefined}
303
- onMouseDrag={nativePaneChrome ? handleNativeDrag : undefined}
304
- onMouseDragEnd={nativePaneChrome ? handleNativeDrag : undefined}
526
+ <FloatingPaneLayer
527
+ key={`float:${pane.instance.instanceId}`}
528
+ {...layerProps(pane, transientFocusActive ? fullscreenRect : rect)}
529
+ rect={transientFocusActive ? fullscreenRect : rect}
530
+ zIndex={pane.floating?.zIndex ?? 50}
531
+ live={live}
532
+ width={width}
533
+ contentHeight={contentHeight}
305
534
  />
306
535
  );
307
536
  })}
537
+
538
+ {!transientFocusActive && dockDividerLayouts.map((divider) => (
539
+ <DockDividerLayer
540
+ key={`divider:${divider.path.join(".")}`}
541
+ divider={divider}
542
+ resizing={windowModeDockResizePathKey === pathKey(divider.path)}
543
+ live={live}
544
+ nativePaneChrome={nativePaneChrome}
545
+ actions={actions}
546
+ />
547
+ ))}
308
548
  </>
309
549
  );
310
550
  }