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
@@ -6,7 +6,6 @@ import {
6
6
  type MutableRefObject,
7
7
  type SetStateAction,
8
8
  } from "react";
9
- import { useRafCallback } from "../../../../react/use-raf-callback";
10
9
  import {
11
10
  type DockDividerLayout,
12
11
  type DockGeometryOptions,
@@ -24,6 +23,7 @@ import {
24
23
  } from "./index";
25
24
  import type { ActionMenuState } from "../action-menu-overlay";
26
25
  import type { DividerPreviewState } from "../native/window-state";
26
+ import { createLiveDragStore, IDLE_DRAG, type LiveDragStore } from "./live";
27
27
  import type { WindowEditState } from "../../window-edit/mode";
28
28
  import { useShellActiveDrag } from "../active-drag";
29
29
  import { useShellNativePointerRuntime } from "../native/pointer-runtime";
@@ -58,6 +58,9 @@ export interface ShellMouseEvent {
58
58
  button?: number;
59
59
  preciseX?: number;
60
60
  preciseY?: number;
61
+ /** Desktop: where the browser predicts the pointer a moment later, for drawing only. */
62
+ predictedX?: number;
63
+ predictedY?: number;
61
64
  /** Whether a later handler has claimed this pointer interaction. */
62
65
  isDefaultPrevented?: () => boolean;
63
66
  stopPropagation: () => void;
@@ -71,88 +74,97 @@ export interface VisibleFloatingPane {
71
74
 
72
75
  export interface ShellDragRuntimeState {
73
76
  cancelActiveDrag: () => void;
74
- dividerPreview: DividerPreviewState | null;
75
- dividerPreviewRef: MutableRefObject<DividerPreviewState | null>;
76
- dockPreview: DragPreview | null;
77
- dockPreviewRef: MutableRefObject<DragPreview | null>;
78
- dragCursor: { x: number; y: number } | null;
79
- dragFloatingRect: { paneId: string; rect: FloatingRect } | null;
80
77
  dragRef: MutableRefObject<DragMode | null>;
81
78
  hasActiveDrag: () => boolean;
82
- setDragCursor: Dispatch<SetStateAction<{ x: number; y: number } | null>>;
79
+ /** Positions that follow the pointer, outside React state. */
80
+ live: LiveDragStore;
81
+ /** The tiled pane whose drop grid is under the pointer. */
82
+ setHoverTarget: (targetId: string | null) => void;
83
+ startDrag: (drag: DragMode) => void;
83
84
  updateDividerPreview: (next: DividerPreviewState | null) => void;
84
85
  updateDockPreview: (next: DragPreview | null) => void;
85
86
  updateDragFloatingRect: (next: { paneId: string; rect: FloatingRect } | null) => void;
86
87
  }
87
88
 
89
+ function sameRect(a: LayoutBounds, b: LayoutBounds): boolean {
90
+ return a.x === b.x && a.y === b.y && a.width === b.width && a.height === b.height;
91
+ }
92
+
93
+ function sameDragPreview(a: DragPreview | null, b: DragPreview | null): boolean {
94
+ if (a === b) return true;
95
+ if (!a || !b || a.kind !== b.kind || !sameRect(a.rect, b.rect)) return false;
96
+ if (a.kind === "snap" && b.kind === "snap") return a.position === b.position;
97
+ if (a.kind === "dock" && b.kind === "dock") {
98
+ const [x, y] = [a.target, b.target];
99
+ if (x.kind === "frame" && y.kind === "frame") return x.edge === y.edge;
100
+ if (x.kind === "leaf" && y.kind === "leaf") return x.targetId === y.targetId && x.position === y.position;
101
+ }
102
+ return false;
103
+ }
104
+
88
105
  export function useShellDragRuntimeState({
89
106
  contentHeight,
90
- throttleFloatingPreview = false,
107
+ nativePaneChrome,
91
108
  width,
92
109
  }: {
93
110
  contentHeight: number;
94
- throttleFloatingPreview?: boolean;
111
+ nativePaneChrome: boolean;
95
112
  width: number;
96
113
  }): ShellDragRuntimeState {
97
114
  const dragRef = useRef<DragMode | null>(null);
98
- const [dragFloatingRect, setDragFloatingRect] = useState<{ paneId: string; rect: FloatingRect } | null>(null);
99
- const pendingDragFloatingRectRef = useRef<{ paneId: string; rect: FloatingRect } | null>(null);
100
- const [dragCursor, setDragCursor] = useState<{ x: number; y: number } | null>(null);
101
- const [dividerPreview, setDividerPreview] = useState<DividerPreviewState | null>(null);
102
- const [dockPreview, setDockPreview] = useState<DragPreview | null>(null);
103
- const dividerPreviewRef = useRef<DividerPreviewState | null>(null);
104
- const dockPreviewRef = useRef<DragPreview | null>(null);
105
- const flushDragFloatingRect = useRafCallback(() => {
106
- setDragFloatingRect(pendingDragFloatingRectRef.current);
107
- });
115
+ const [live] = useState(createLiveDragStore);
108
116
 
109
117
  const updateDragFloatingRect = useCallback((next: { paneId: string; rect: FloatingRect } | null) => {
110
- const constrainedNext = next
118
+ const current = live.get();
119
+ const floating = next
111
120
  ? { paneId: next.paneId, rect: constrainFloatingRectToBounds(next.rect, width, contentHeight) }
112
121
  : null;
113
- pendingDragFloatingRectRef.current = constrainedNext;
114
- if (!next) {
115
- setDragFloatingRect(null);
116
- return;
117
- }
118
- if (!throttleFloatingPreview) {
119
- setDragFloatingRect(constrainedNext);
120
- return;
121
- }
122
- flushDragFloatingRect();
123
- }, [contentHeight, flushDragFloatingRect, throttleFloatingPreview, width]);
122
+ // A desktop pane on the move follows by restyling itself (`move`); its
123
+ // start and end, a resize and every terminal frame redraw through React.
124
+ const follows = nativePaneChrome
125
+ && floating !== null
126
+ && current.floating?.paneId === floating.paneId
127
+ && current.paneDrag?.paneId === floating.paneId;
128
+ if (follows) live.move({ floating });
129
+ else live.set({ floating });
130
+ }, [contentHeight, live, nativePaneChrome, width]);
131
+
132
+ const setHoverTarget = useCallback((targetId: string | null) => {
133
+ live.set({ hoverTargetId: targetId });
134
+ }, [live]);
124
135
 
125
136
  const updateDividerPreview = useCallback((next: DividerPreviewState | null) => {
126
- dividerPreviewRef.current = next;
127
- setDividerPreview(next);
128
- }, []);
137
+ const current = live.get().divider;
138
+ // The desktop divider follows by restyling itself; React hears only that it started or ended.
139
+ if (nativePaneChrome && next && current?.pathKey === next.pathKey) live.move({ divider: next });
140
+ else live.set({ divider: next });
141
+ }, [live, nativePaneChrome]);
129
142
 
130
143
  const updateDockPreview = useCallback((next: DragPreview | null) => {
131
- dockPreviewRef.current = next;
132
- setDockPreview(next);
133
- }, []);
144
+ // Recomputed on every move; only a different target is news.
145
+ if (sameDragPreview(live.get().dockPreview, next)) return;
146
+ live.set({ dockPreview: next });
147
+ }, [live]);
148
+
149
+ const startDrag = useCallback((drag: DragMode) => {
150
+ dragRef.current = drag;
151
+ live.set({ paneDrag: drag.type === "pane-drag" ? { paneId: drag.paneId, mode: drag.mode } : null });
152
+ }, [live]);
134
153
 
135
154
  const cancelActiveDrag = useCallback(() => {
136
155
  dragRef.current = null;
137
- updateDragFloatingRect(null);
138
- setDragCursor(null);
139
- updateDividerPreview(null);
140
- updateDockPreview(null);
141
- }, [updateDividerPreview, updateDockPreview, updateDragFloatingRect]);
156
+ live.set(IDLE_DRAG);
157
+ }, [live]);
142
158
 
143
159
  const hasActiveDrag = useCallback(() => dragRef.current != null, []);
144
160
 
145
161
  return {
146
162
  cancelActiveDrag,
147
- dividerPreview,
148
- dividerPreviewRef,
149
- dockPreview,
150
- dockPreviewRef,
151
- dragCursor,
152
- dragFloatingRect,
153
163
  dragRef,
154
164
  hasActiveDrag,
155
- setDragCursor,
165
+ live,
166
+ setHoverTarget,
167
+ startDrag,
156
168
  updateDividerPreview,
157
169
  updateDockPreview,
158
170
  updateDragFloatingRect,
@@ -0,0 +1,10 @@
1
+ import type { CSSProperties } from "react";
2
+ import { WEB_CELL_HEIGHT, WEB_CELL_WIDTH } from "../../../../theme/font-scale";
3
+
4
+ /**
5
+ * Desktop: draws a box laid out at the shell's corner at cell (x, y) with a
6
+ * transform, so placing it somewhere else never lays anything out.
7
+ */
8
+ export function placeStyle(x: number, y: number): CSSProperties {
9
+ return { transform: `translate3d(${x * WEB_CELL_WIDTH}px, ${y * WEB_CELL_HEIGHT}px, 0)` };
10
+ }
@@ -41,11 +41,10 @@ import { tickerLinkMenuItems } from "./ticker-link-menu";
41
41
  import {
42
42
  makeSnapGuides,
43
43
  resolveExternalDockPreview,
44
- resolveHoverOverlay,
45
44
  } from "./drag";
46
45
  import { resolveAppHeaderHeightCells } from "./chrome";
47
46
  import { useShellWindowMode } from "./window-mode";
48
- import { useShellNativeSurfaceWindowState } from "./native/surfaces";
47
+ import { ShellNativeSurfaceSync } from "./native/surfaces";
49
48
  import { ShellWindowModeOverlays } from "./window-mode/overlays";
50
49
  import { ShellPaneLayers } from "./pane/layers";
51
50
  import { paneMenuButtonAnchor, ShellActionMenuOverlay, type ActionMenuState } from "./action-menu-overlay";
@@ -134,10 +133,6 @@ export function Shell({
134
133
  const layout = useAppSelector((state) => state.config.layout);
135
134
  const dialogOpen = useDialogState((dialog) => dialog.isOpen);
136
135
  const [hoveredPaneId, setHoveredPaneId] = useState<string | null>(null);
137
- const setHoveredPaneIfChanged = useCallback((paneId: string | null) => {
138
- if (commandBarOpen) return;
139
- setHoveredPaneId((current) => (current === paneId ? current : paneId));
140
- }, [commandBarOpen]);
141
136
  const [menuState, setMenuState] = useState<ActionMenuState | null>(null);
142
137
  const [transientFocusLayoutState, setTransientFocusLayoutState] = useState<TransientFocusLayoutState | null>(null);
143
138
  const transientFocusLayoutStateRef = useRef<TransientFocusLayoutState | null>(null);
@@ -165,21 +160,20 @@ export function Shell({
165
160
  if (commandBarOpen) setHoveredPaneId(null);
166
161
  }, [commandBarOpen]);
167
162
 
168
- const dragRuntime = useShellDragRuntimeState({
169
- contentHeight,
170
- throttleFloatingPreview: nativePaneChrome,
171
- width,
172
- });
163
+ const dragRuntime = useShellDragRuntimeState({ contentHeight, nativePaneChrome, width });
173
164
  const {
174
165
  cancelActiveDrag,
175
- dividerPreview,
176
- dockPreview,
177
- dragCursor,
178
- dragFloatingRect,
179
- dragRef,
180
166
  hasActiveDrag,
167
+ live,
181
168
  } = dragRuntime;
182
169
 
170
+ // A pane dragged across other headers is not hovering them: re-rendering
171
+ // the layout for their menu buttons would only cost frames.
172
+ const setHoveredPaneIfChanged = useCallback((paneId: string | null) => {
173
+ if (commandBarOpen || hasActiveDrag()) return;
174
+ setHoveredPaneId((current) => (current === paneId ? current : paneId));
175
+ }, [commandBarOpen, hasActiveDrag]);
176
+
183
177
  const { disabledPaneIds, visibleLayout } = useShellVisibleLayout({
184
178
  disabledPlugins: config.disabledPlugins,
185
179
  layout,
@@ -261,9 +255,10 @@ export function Shell({
261
255
  pluginRegistry,
262
256
  width,
263
257
  });
264
- const cursorOcclusionRects = useMemo(() => resolveShellCursorOcclusionRects({
258
+ // Read when the terminal draws a frame, so a pane on the move is covered where it is now.
259
+ const resolveCursorOcclusionRects = useCallback(() => resolveShellCursorOcclusionRects({
265
260
  contentHeight,
266
- dragFloatingRect,
261
+ dragFloatingRect: live.get().floating,
267
262
  nativePaneChrome,
268
263
  overlayOpen,
269
264
  transientFocusActive,
@@ -271,7 +266,7 @@ export function Shell({
271
266
  width,
272
267
  }), [
273
268
  contentHeight,
274
- dragFloatingRect,
269
+ live,
275
270
  nativePaneChrome,
276
271
  overlayOpen,
277
272
  transientFocusActive,
@@ -279,7 +274,7 @@ export function Shell({
279
274
  width,
280
275
  ]);
281
276
  useShellCursorOcclusionGuard({
282
- occlusionRects: cursorOcclusionRects,
277
+ resolveOcclusionRects: resolveCursorOcclusionRects,
283
278
  shellRef,
284
279
  });
285
280
 
@@ -460,29 +455,6 @@ export function Shell({
460
455
  () => resolveExternalDockPreview(desktopDockPreview, bounds),
461
456
  [bounds, desktopDockPreview],
462
457
  );
463
- const activePaneDrag = dragRef.current?.type === "pane-drag" ? dragRef.current : null;
464
- const activeHoverOverlay = activePaneDrag && dragCursor
465
- ? resolveHoverOverlay(dragCursor.x, dragCursor.y, dockLeafLayouts, activePaneDrag.paneId)
466
- : null;
467
- const effectiveDockPreview = dockPreview ?? externalDockPreview;
468
- useShellNativeSurfaceWindowState({
469
- activeHoverOverlay,
470
- activePaneDrag,
471
- appHeaderHeight,
472
- commandBarNativeOccluder,
473
- contentHeight,
474
- dialogOpen,
475
- dividerPreview,
476
- dockDividerLayouts: screenDividerLayouts,
477
- dockedPanes: screenDockedPanes,
478
- dragFloatingRect,
479
- effectiveDockPreview,
480
- menuState,
481
- nativeWindowModePanelRect,
482
- visibleFloatingPanes: screenFloatingPanes,
483
- width,
484
- windowModeDockMovePreview,
485
- });
486
458
 
487
459
  const titleState = useMemo(
488
460
  () => ({ config, paneState }) as Parameters<typeof resolveTickerForPane>[0],
@@ -824,12 +796,27 @@ export function Shell({
824
796
  </Box>
825
797
  </Box>
826
798
 
799
+ <ShellNativeSurfaceSync
800
+ appHeaderHeight={appHeaderHeight}
801
+ commandBarNativeOccluder={commandBarNativeOccluder}
802
+ contentHeight={contentHeight}
803
+ dialogOpen={dialogOpen}
804
+ dockDividerLayouts={screenDividerLayouts}
805
+ dockedPanes={screenDockedPanes}
806
+ dockLeafLayouts={dockLeafLayouts}
807
+ externalDockPreview={externalDockPreview}
808
+ live={live}
809
+ menuState={menuState}
810
+ nativeWindowModePanelRect={nativeWindowModePanelRect}
811
+ visibleFloatingPanes={screenFloatingPanes}
812
+ width={width}
813
+ windowModeDockMovePreview={windowModeDockMovePreview}
814
+ />
815
+
827
816
  <ShellPaneLayers
828
817
  contentHeight={contentHeight}
829
- dividerPreview={dividerPreview}
830
818
  dockDividerLayouts={dockDividerLayouts}
831
819
  dockLeafLayouts={dockLeafLayouts}
832
- dragFloatingRect={dragFloatingRect}
833
820
  focusedPaneId={focusedPaneId}
834
821
  getPaneTitle={getPaneTitle}
835
822
  getPaneQuickSettings={getPaneQuickSettings}
@@ -845,6 +832,7 @@ export function Shell({
845
832
  handleNativePaneMouseDown={handleNativePaneMouseDown}
846
833
  handlePaneAction={handlePaneAction}
847
834
  hoveredPaneId={hoveredPaneId}
835
+ live={live}
848
836
  menuPaneId={menuState?.paneId ?? null}
849
837
  nativeContextMenu={nativeContextMenu}
850
838
  nativePaneChrome={nativePaneChrome}
@@ -868,9 +856,9 @@ export function Shell({
868
856
  contentHeight={contentHeight}
869
857
  dockGeometryOptions={dockGeometryOptions}
870
858
  dockLeafLayouts={screenDockLeafLayouts}
871
- dragFloatingRect={dragFloatingRect}
872
859
  focusedPaneId={focusedPaneId}
873
860
  getPaneTitle={getPaneTitle}
861
+ live={live}
874
862
  menuOpen={!!menuState}
875
863
  nativePaneChrome={nativePaneChrome}
876
864
  nativeWindowModePanelRect={nativeWindowModePanelRect}
@@ -883,11 +871,9 @@ export function Shell({
883
871
  />
884
872
 
885
873
  <ShellDragOverlays
886
- activeHoverOverlay={activeHoverOverlay}
887
- activePaneDrag={activePaneDrag}
888
- dockPreview={dockPreview}
889
- dragFloatingRect={dragFloatingRect}
890
- effectiveDockPreview={effectiveDockPreview}
874
+ dockLeafLayouts={dockLeafLayouts}
875
+ externalDockPreview={externalDockPreview}
876
+ live={live}
891
877
  />
892
878
 
893
879
  <ShellActionMenuOverlay
@@ -42,7 +42,7 @@ export function useShellNativePointerRuntime({
42
42
  commandBarOpen = false,
43
43
  }: UseShellNativePointerRuntimeOptions) {
44
44
  const {
45
- dragRef,
45
+ startDrag,
46
46
  updateDividerPreview,
47
47
  updateDragFloatingRect,
48
48
  } = dragRuntime;
@@ -87,17 +87,17 @@ export function useShellNativePointerRuntime({
87
87
 
88
88
  const pointer = getShellPointer(event);
89
89
  focusNativePane(paneId);
90
- dragRef.current = {
90
+ startDrag({
91
91
  type: "pane-drag",
92
92
  paneId,
93
93
  mode: "floating",
94
94
  startX: pointer.x,
95
95
  startY: pointer.y,
96
96
  origRect: { x: rect.x, y: rect.y, width: rect.width, height: rect.height },
97
- };
97
+ });
98
98
  updateDragFloatingRect({ paneId, rect: { x: rect.x, y: rect.y, width: rect.width, height: rect.height } });
99
99
  event.preventDefault();
100
- }, [commandBarOpen, dragRef, focusNativePane, getShellPointer, nativePaneChrome, transientFocusActive, updateDragFloatingRect, windowMode]);
100
+ }, [commandBarOpen, focusNativePane, getShellPointer, nativePaneChrome, startDrag, transientFocusActive, updateDragFloatingRect, windowMode]);
101
101
 
102
102
  const startNativeDockedDrag = useCallback((paneId: string, rect: LayoutBounds, event: ShellMouseEvent) => {
103
103
  if (!nativePaneChrome) return;
@@ -108,16 +108,16 @@ export function useShellNativePointerRuntime({
108
108
 
109
109
  const pointer = getShellPointer(event);
110
110
  focusNativePane(paneId);
111
- dragRef.current = {
111
+ startDrag({
112
112
  type: "pane-drag",
113
113
  paneId,
114
114
  mode: "docked",
115
115
  startX: pointer.x,
116
116
  startY: pointer.y,
117
117
  origRect: { x: rect.x, y: rect.y, width: rect.width, height: rect.height },
118
- };
118
+ });
119
119
  event.preventDefault();
120
- }, [commandBarOpen, dragRef, focusNativePane, getShellPointer, nativePaneChrome, transientFocusActive, windowMode]);
120
+ }, [commandBarOpen, focusNativePane, getShellPointer, nativePaneChrome, startDrag, transientFocusActive, windowMode]);
121
121
 
122
122
  const startNativeFloatResize = useCallback((paneId: string, rect: FloatingRect, event: ShellMouseEvent) => {
123
123
  if (!nativePaneChrome) return;
@@ -130,17 +130,17 @@ export function useShellNativePointerRuntime({
130
130
  } else {
131
131
  focusNativePane(paneId);
132
132
  }
133
- dragRef.current = {
133
+ startDrag({
134
134
  type: "float-resize",
135
135
  paneId,
136
136
  startX: pointer.x,
137
137
  startY: pointer.y,
138
138
  origRect: { x: rect.x, y: rect.y, width: rect.width, height: rect.height },
139
- };
139
+ });
140
140
  updateDragFloatingRect({ paneId, rect: { x: rect.x, y: rect.y, width: rect.width, height: rect.height } });
141
141
  event.stopPropagation();
142
142
  event.preventDefault();
143
- }, [commandBarOpen, dragRef, focusNativePane, getShellPointer, nativePaneChrome, selectWindowModePane, transientFocusActive, updateDragFloatingRect, windowMode]);
143
+ }, [commandBarOpen, focusNativePane, getShellPointer, nativePaneChrome, selectWindowModePane, startDrag, transientFocusActive, updateDragFloatingRect, windowMode]);
144
144
 
145
145
  const startNativeDividerDrag = useCallback((divider: DockDividerLayout, event: ShellMouseEvent) => {
146
146
  if (!nativePaneChrome) return;
@@ -152,7 +152,7 @@ export function useShellNativePointerRuntime({
152
152
  setMenuState(null);
153
153
  setHoveredMenuItemId(null);
154
154
  }
155
- dragRef.current = {
155
+ startDrag({
156
156
  type: "divider",
157
157
  path: divider.path,
158
158
  axis: divider.axis,
@@ -160,7 +160,7 @@ export function useShellNativePointerRuntime({
160
160
  startY: pointer.y,
161
161
  startRatio: divider.ratio,
162
162
  bounds: divider.bounds,
163
- };
163
+ });
164
164
  updateDividerPreview({
165
165
  pathKey: divider.path.join("."),
166
166
  rect: divider.rect,
@@ -168,7 +168,7 @@ export function useShellNativePointerRuntime({
168
168
  });
169
169
  event.stopPropagation();
170
170
  event.preventDefault();
171
- }, [commandBarOpen, dragRef, getShellPointer, menuState, nativePaneChrome, setHoveredMenuItemId, setMenuState, transientFocusActive, updateDividerPreview]);
171
+ }, [commandBarOpen, getShellPointer, menuState, nativePaneChrome, setHoveredMenuItemId, setMenuState, startDrag, transientFocusActive, updateDividerPreview]);
172
172
 
173
173
  const handlePaneAction = useCallback((paneId: string, rect: LayoutBounds, event: ShellMouseEvent) => {
174
174
  if (commandBarOpen) return;
@@ -1,17 +1,18 @@
1
1
  import { useEffect, useMemo } from "react";
2
2
  import type {
3
3
  DockDividerLayout,
4
+ DockLeafLayout,
4
5
  FloatingRect,
5
6
  LayoutBounds,
6
7
  ResolvedPane,
7
8
  } from "../../../../layout/pane-manager";
8
9
  import { useNativeRenderer } from "../../../../ui";
9
10
  import type { DragPreview } from "../drag";
11
+ import { IDLE_DRAG, useLiveDrag, useLiveHoverOverlay, type LiveDragGeometry, type LiveDragStore } from "../drag/live";
10
12
  import {
11
13
  buildNativeTransientOccluders,
12
14
  buildNativeWindowState,
13
15
  resolveNativeDockDividers,
14
- type DividerPreviewState,
15
16
  } from "./window-state";
16
17
  import type { WindowEditDockMovePreview } from "../../window-edit/presentation";
17
18
 
@@ -24,18 +25,18 @@ interface ShellNativeSurfaceMenuState {
24
25
  maxRows?: number;
25
26
  }
26
27
 
27
- interface UseShellNativeSurfaceWindowStateOptions {
28
- activeHoverOverlay: Parameters<typeof buildNativeTransientOccluders>[0]["activeHoverOverlay"];
29
- activePaneDrag: { paneId: string; mode: "docked" | "floating" } | null;
28
+ interface ShellNativeSurfaceSyncProps {
30
29
  appHeaderHeight: number;
31
30
  commandBarNativeOccluder: LayoutBounds | null;
32
31
  contentHeight: number;
33
32
  dialogOpen: boolean;
34
- dividerPreview: DividerPreviewState | null;
35
33
  dockDividerLayouts: DockDividerLayout[];
36
34
  dockedPanes: ResolvedPane[];
37
- dragFloatingRect: { paneId: string; rect: FloatingRect } | null;
38
- effectiveDockPreview: DragPreview | null;
35
+ /** The tiled panes a moving pane can be dropped on. */
36
+ dockLeafLayouts: DockLeafLayout[];
37
+ /** A pane from another window over this one's edge. */
38
+ externalDockPreview: DragPreview | null;
39
+ live: LiveDragStore;
39
40
  menuState: ShellNativeSurfaceMenuState | null;
40
41
  nativeWindowModePanelRect: LayoutBounds | null;
41
42
  visibleFloatingPanes: Array<{ pane: ResolvedPane; rect: FloatingRect }>;
@@ -43,26 +44,40 @@ interface UseShellNativeSurfaceWindowStateOptions {
43
44
  windowModeDockMovePreview: WindowEditDockMovePreview | null;
44
45
  }
45
46
 
46
- export function useShellNativeSurfaceWindowState({
47
- activeHoverOverlay,
48
- activePaneDrag,
47
+ const NO_LEAVES: DockLeafLayout[] = [];
48
+ const selectAll = (geometry: LiveDragGeometry) => geometry;
49
+ const selectIdle = () => IDLE_DRAG;
50
+
51
+ /**
52
+ * Keeps the native image surfaces clear of the layout. A component of its own,
53
+ * so following a drag redraws nothing but this.
54
+ */
55
+ export function ShellNativeSurfaceSync({
49
56
  appHeaderHeight,
50
57
  commandBarNativeOccluder,
51
58
  contentHeight,
52
59
  dialogOpen,
53
- dividerPreview,
54
60
  dockDividerLayouts,
55
61
  dockedPanes,
56
- dragFloatingRect,
57
- effectiveDockPreview,
62
+ dockLeafLayouts,
63
+ externalDockPreview,
64
+ live,
58
65
  menuState,
59
66
  nativeWindowModePanelRect,
60
67
  visibleFloatingPanes,
61
68
  width,
62
69
  windowModeDockMovePreview,
63
- }: UseShellNativeSurfaceWindowStateOptions) {
70
+ }: ShellNativeSurfaceSyncProps): null {
64
71
  // Only terminal hosts draw kitty images that need to stay clear of the layout.
65
72
  const nativeSurfaceManager = useNativeRenderer().nativeSurfaceManager;
73
+ const {
74
+ paneDrag: activePaneDrag,
75
+ floating: dragFloatingRect,
76
+ divider: dividerPreview,
77
+ dockPreview,
78
+ } = useLiveDrag(live, nativeSurfaceManager ? selectAll : selectIdle);
79
+ const effectiveDockPreview = dockPreview ?? externalDockPreview;
80
+ const activeHoverOverlay = useLiveHoverOverlay(live, nativeSurfaceManager ? dockLeafLayouts : NO_LEAVES);
66
81
  const nativeTransientOccluders = useMemo(() => buildNativeTransientOccluders({
67
82
  activeHoverOverlay,
68
83
  activePaneDrag,
@@ -124,4 +139,5 @@ export function useShellNativeSurfaceWindowState({
124
139
  useEffect(() => {
125
140
  nativeSurfaceManager?.setWindowState(nativeWindowState);
126
141
  }, [nativeSurfaceManager, nativeWindowState]);
142
+ return null;
127
143
  }