gloomberb 0.10.3 → 0.10.4

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 (152) hide show
  1. package/README.md +3 -3
  2. package/README.zh-CN.md +2 -2
  3. package/package.json +2 -1
  4. package/src/api-client/auth.ts +8 -0
  5. package/src/api-client/index.ts +70 -0
  6. package/src/api-client/types.ts +49 -0
  7. package/src/app/runtime/desktop-deeplink.test.ts +8 -0
  8. package/src/app/runtime/desktop-deeplink.ts +14 -1
  9. package/src/components/chart/composite/composite-chart.test.tsx +340 -17
  10. package/src/components/chart/composite/composite-chart.tsx +859 -68
  11. package/src/components/chart/composite/interactions.test.ts +78 -22
  12. package/src/components/chart/composite/interactions.ts +45 -17
  13. package/src/components/chart/composite/rasterizer.ts +8 -2
  14. package/src/components/chart/composite/renderers.test.ts +61 -1
  15. package/src/components/chart/composite/scene.ts +14 -4
  16. package/src/components/chart/composite/tools.test.ts +371 -0
  17. package/src/components/chart/composite/tools.ts +508 -0
  18. package/src/components/chart/composite/types.ts +4 -0
  19. package/src/components/chart/core/pointer.ts +2 -0
  20. package/src/components/chart/native/raster/primitives.ts +18 -0
  21. package/src/components/chart/price-axis-labels.tsx +42 -2
  22. package/src/components/chart/static/chart/axis-overlays.test.tsx +2 -0
  23. package/src/components/chart/static/chart/axis-overlays.tsx +46 -0
  24. package/src/components/command-bar/action-runtime.ts +3 -0
  25. package/src/components/command-bar/assist/inventory.test.ts +93 -0
  26. package/src/components/command-bar/assist/inventory.ts +92 -0
  27. package/src/components/command-bar/assist/model.test.ts +104 -0
  28. package/src/components/command-bar/assist/model.ts +169 -0
  29. package/src/components/command-bar/assist/runtime.ts +185 -0
  30. package/src/components/command-bar/commands/direct/index.ts +1 -1
  31. package/src/components/command-bar/keyboard-shortcuts.ts +5 -0
  32. package/src/components/command-bar/list/model.ts +15 -2
  33. package/src/components/command-bar/list/navigation.ts +6 -0
  34. package/src/components/command-bar/list/view.tsx +16 -3
  35. package/src/components/command-bar/pane-templates/workflow.ts +1 -0
  36. package/src/components/command-bar/panel/body.tsx +2 -0
  37. package/src/components/command-bar/panel/index.tsx +3 -0
  38. package/src/components/command-bar/panel/runtime.ts +6 -0
  39. package/src/components/command-bar/panel/types.ts +1 -0
  40. package/src/components/command-bar/routes/root/results.ts +28 -2
  41. package/src/components/command-bar/routes/root/runtime.ts +66 -36
  42. package/src/components/command-bar/routes/root/shortcuts.ts +8 -4
  43. package/src/components/command-bar/routing/navigation-state.ts +17 -0
  44. package/src/components/command-bar/selection-runtime.ts +26 -0
  45. package/src/components/command-bar/surface/assist.test.tsx +276 -0
  46. package/src/components/command-bar/surface/index.tsx +93 -2
  47. package/src/components/command-bar/surface/portfolio.test.tsx +2 -1
  48. package/src/components/command-bar/view-model.ts +7 -1
  49. package/src/components/command-bar/workflow/broker.test.ts +22 -0
  50. package/src/components/command-bar/workflow/broker.ts +1 -0
  51. package/src/components/layout/detached-pane-shell.tsx +40 -2
  52. package/src/components/layout/floating-pane.tsx +4 -1
  53. package/src/components/layout/pane/header.tsx +64 -6
  54. package/src/components/layout/pane/index.tsx +4 -1
  55. package/src/components/layout/shell/index.tsx +23 -0
  56. package/src/components/layout/shell/pane/layers.tsx +5 -0
  57. package/src/components/onboarding/account-step/chooser-panel.tsx +74 -0
  58. package/src/components/onboarding/account-step/form-panel.tsx +157 -0
  59. package/src/components/onboarding/account-step/index.tsx +58 -0
  60. package/src/components/onboarding/account-step/model.test.ts +67 -0
  61. package/src/components/onboarding/account-step/model.ts +126 -0
  62. package/src/components/onboarding/account-step/signed-in-panel.tsx +31 -0
  63. package/src/components/onboarding/onboarding-steps.tsx +91 -55
  64. package/src/components/onboarding/onboarding-wizard.test.tsx +73 -6
  65. package/src/components/onboarding/onboarding-wizard.tsx +93 -9
  66. package/src/components/onboarding/portfolio-step/broker-fields-panel.tsx +3 -3
  67. package/src/components/onboarding/wizard-account.ts +235 -0
  68. package/src/components/onboarding/wizard-keyboard.ts +67 -1
  69. package/src/components/onboarding/wizard-model.ts +2 -2
  70. package/src/i18n/es.ts +1093 -0
  71. package/src/i18n/index.test.ts +5 -2
  72. package/src/i18n/index.ts +3 -0
  73. package/src/i18n/ja.ts +64 -3
  74. package/src/i18n/ko.ts +64 -3
  75. package/src/i18n/languages.test.ts +4 -1
  76. package/src/i18n/languages.ts +12 -1
  77. package/src/i18n/zh-cn.ts +64 -3
  78. package/src/i18n/zh-tw.ts +64 -3
  79. package/src/market-data/coordinator/quotes.ts +63 -1
  80. package/src/market-data/coordinator/subscriptions.test.ts +33 -0
  81. package/src/market-data/quotes/cadence.ts +2 -0
  82. package/src/plugins/builtin/account-management/footer.ts +9 -3
  83. package/src/plugins/builtin/account-management/model.ts +53 -1
  84. package/src/plugins/builtin/account-management/pane.tsx +107 -24
  85. package/src/plugins/builtin/ai/index.tsx +7 -2
  86. package/src/plugins/builtin/ai/screener/market-runtime.ts +4 -2
  87. package/src/plugins/builtin/ai/screener/pane.tsx +3 -0
  88. package/src/plugins/builtin/chart-composer/editor.tsx +0 -1
  89. package/src/plugins/builtin/chart-composer/index.tsx +10 -2
  90. package/src/plugins/builtin/chart-composer/pane.tsx +111 -44
  91. package/src/plugins/builtin/chart-composer/quick-add.tsx +27 -3
  92. package/src/plugins/builtin/chart-composer/semantic.ts +21 -0
  93. package/src/plugins/builtin/chat/controller/persistence.ts +5 -0
  94. package/src/plugins/builtin/cloud/plugin.tsx +9 -1
  95. package/src/plugins/builtin/cloud/upgrade-command.ts +28 -0
  96. package/src/plugins/builtin/cloud/upgrade-status-widget.tsx +54 -0
  97. package/src/plugins/builtin/help/index.tsx +38 -0
  98. package/src/plugins/builtin/news/wire/news/footer.ts +18 -38
  99. package/src/plugins/builtin/options/footer.test.ts +20 -124
  100. package/src/plugins/builtin/options/footer.ts +32 -90
  101. package/src/plugins/builtin/options/index.tsx +6 -0
  102. package/src/plugins/builtin/options/view.tsx +3 -0
  103. package/src/plugins/builtin/portfolio-list/index.tsx +12 -4
  104. package/src/plugins/builtin/portfolio-list/pane/index.tsx +22 -1
  105. package/src/plugins/builtin/portfolio-list/pane/streaming.ts +17 -13
  106. package/src/plugins/builtin/portfolio-list/use-throttled-ticker-order.test.tsx +76 -0
  107. package/src/plugins/builtin/portfolio-list/use-throttled-ticker-order.ts +78 -0
  108. package/src/plugins/builtin/shared/cloud-upgrade.ts +131 -0
  109. package/src/plugins/builtin/shared/live-streaming.ts +60 -0
  110. package/src/plugins/builtin/shared/plan-access.test.ts +69 -0
  111. package/src/plugins/builtin/shared/plan-access.ts +109 -0
  112. package/src/plugins/builtin/ticker-detail/chart-tab-switch.test.tsx +1 -1
  113. package/src/plugins/builtin/ticker-detail/index.tsx +11 -2
  114. package/src/plugins/builtin/ticker-detail/pane.tsx +21 -3
  115. package/src/plugins/builtin/ticker-detail/quote-monitor/index.tsx +4 -2
  116. package/src/plugins/registry/index.test.ts +51 -0
  117. package/src/plugins/registry/index.ts +12 -0
  118. package/src/plugins/registry/pane-settings.ts +29 -0
  119. package/src/renderers/electrobun/view/host/box.test.tsx +97 -0
  120. package/src/renderers/electrobun/view/host/box.tsx +3 -0
  121. package/src/renderers/electrobun/view/host/chart-surface.tsx +85 -1
  122. package/src/renderers/electrobun/view/host/focus-scope.test.ts +37 -0
  123. package/src/renderers/electrobun/view/host/focus-scope.ts +56 -0
  124. package/src/renderers/electrobun/view/host/input.tsx +16 -1
  125. package/src/renderers/electrobun/view/host/mouse.ts +1 -0
  126. package/src/renderers/electrobun/view/host/style.ts +1 -1
  127. package/src/renderers/electrobun/view/host/tabs.tsx +4 -2
  128. package/src/renderers/electrobun/view/main.tsx +2 -0
  129. package/src/renderers/opentui/chart-surface.tsx +2 -0
  130. package/src/renderers/opentui/host.tsx +3 -0
  131. package/src/sources/gloomberb-cloud/index.test.ts +8 -0
  132. package/src/sources/gloomberb-cloud/index.ts +4 -4
  133. package/src/sources/provider-router/history.test.ts +3 -3
  134. package/src/sources/provider-router/history.ts +15 -4
  135. package/src/sources/yahoo-finance/history.test.ts +47 -0
  136. package/src/sources/yahoo-finance/history.ts +5 -1
  137. package/src/state/hooks/quote-streaming.test.tsx +124 -1
  138. package/src/state/hooks/quote-streaming.ts +90 -7
  139. package/src/theme/colors.ts +20 -1
  140. package/src/theme/themes.test.ts +3 -0
  141. package/src/{sources/gloomberb-cloud → time-series}/history-quality.ts +27 -3
  142. package/src/time-series/resolution.test.ts +51 -0
  143. package/src/time-series/resolution.ts +41 -0
  144. package/src/time-series/resolve.test.ts +207 -1
  145. package/src/time-series/resolve.ts +196 -29
  146. package/src/time-series/spec-studies.test.ts +4 -0
  147. package/src/time-series/spec.ts +15 -1
  148. package/src/time-series/types.ts +7 -0
  149. package/src/time-series/use-chart-resolution.test.tsx +37 -0
  150. package/src/time-series/use-chart-resolution.ts +17 -1
  151. package/src/types/plugin.ts +9 -0
  152. package/src/ui/host.tsx +17 -0
@@ -82,6 +82,8 @@ function pointerEvent(
82
82
  localY: number,
83
83
  options: {
84
84
  ctrl?: boolean;
85
+ shift?: boolean;
86
+ alt?: boolean;
85
87
  scroll?: { direction: "up" | "down" | "left" | "right"; delta: number };
86
88
  } = {},
87
89
  ) {
@@ -89,14 +91,18 @@ function pointerEvent(
89
91
  return {
90
92
  x: (node.x as number) + localX,
91
93
  y: (node.y as number) + localY,
92
- modifiers: { shift: false, alt: false, ctrl: options.ctrl === true },
94
+ modifiers: {
95
+ shift: options.shift === true,
96
+ alt: options.alt === true,
97
+ ctrl: options.ctrl === true,
98
+ },
93
99
  scroll: options.scroll,
94
100
  preventDefault() {},
95
101
  stopPropagation() {},
96
102
  };
97
103
  }
98
104
 
99
- function keyEvent(name: string): KeyEventLike {
105
+ function keyEvent(name: string, shift = false): KeyEventLike {
100
106
  let defaultPrevented = false;
101
107
  let propagationStopped = false;
102
108
  return {
@@ -104,7 +110,7 @@ function keyEvent(name: string): KeyEventLike {
104
110
  name,
105
111
  sequence: name,
106
112
  ctrl: false,
107
- shift: false,
113
+ shift,
108
114
  alt: false,
109
115
  meta: false,
110
116
  get defaultPrevented() {
@@ -196,6 +202,26 @@ function twoSessionCandles(): ResolvedSeries {
196
202
  }
197
203
 
198
204
  describe("CompositeChart", () => {
205
+ test("shows the regular-session move beside the latest intraday value", async () => {
206
+ testSetup = await testRender(
207
+ <CompositeChart
208
+ width={60}
209
+ height={12}
210
+ series={[{
211
+ ...series("price", "main", "left", "USD", [100, 110]),
212
+ latestChangePercent: 10.65,
213
+ }]}
214
+ panels={[{ id: "main" }]}
215
+ showLatestChangePercent
216
+ />,
217
+ { width: 62, height: 14 },
218
+ );
219
+
220
+ await act(async () => testSetup!.renderOnce());
221
+
222
+ expect(testSetup.captureCharFrame()).toContain("ACME Price $110 +10.65%");
223
+ });
224
+
199
225
  test("lays out mixed panels with one shared legend and time axis", async () => {
200
226
  testSetup = await testRender(
201
227
  <CompositeChart
@@ -321,7 +347,7 @@ describe("CompositeChart", () => {
321
347
  expect(largestGap).toBeLessThan(10);
322
348
  });
323
349
 
324
- test("shows an explicit legend toggle action on hover", async () => {
350
+ test("toggles a series from its legend entry", async () => {
325
351
  const toggled: string[] = [];
326
352
  testSetup = await testRender(
327
353
  <CompositeChart
@@ -340,17 +366,9 @@ describe("CompositeChart", () => {
340
366
  await act(async () => {
341
367
  await testSetup!.renderOnce();
342
368
  });
343
- const firstLegendLine = testSetup.captureCharFrame().split("\n")[0]!;
344
- const priceColumn = firstLegendLine.indexOf("ACME Price");
369
+ const priceColumn = testSetup.captureCharFrame().split("\n")[0]!.indexOf("ACME Price");
345
370
  expect(priceColumn).toBeGreaterThan(0);
346
371
 
347
- await act(async () => {
348
- await testSetup!.mockMouse.moveTo(2, 5);
349
- await testSetup!.mockMouse.moveTo(priceColumn, 0);
350
- await testSetup!.renderOnce();
351
- });
352
- expect(testSetup.captureCharFrame().split("\n")[0]).toContain("Hide");
353
-
354
372
  await act(async () => {
355
373
  await testSetup!.mockMouse.click(priceColumn, 0);
356
374
  await testSetup!.renderOnce();
@@ -490,7 +508,8 @@ describe("CompositeChart", () => {
490
508
 
491
509
  await act(async () => chartShortcut?.(keyEvent("escape")));
492
510
  await act(async () => testSetup!.renderOnce());
493
- expect(testSetup.captureCharFrame()).toContain("Latest");
511
+ // Escape drops the cursor, so its date leaves the time axis with it.
512
+ expect(testSetup.captureCharFrame()).not.toContain("2025-01-02");
494
513
  });
495
514
 
496
515
  test("does not emit the same snapped cursor timestamp twice", async () => {
@@ -913,6 +932,304 @@ describe("CompositeChart", () => {
913
932
  expect(testSetup.captureCharFrame()).toContain("Jan 3");
914
933
  });
915
934
 
935
+ test("zooms to a shift-drag selection instead of panning", async () => {
936
+ const viewportChanges: Array<{ start: string; end: string } | null> = [];
937
+ testSetup = await testRender(
938
+ <CaptureChartSurfaceProvider>
939
+ <CompositeChart
940
+ width={60}
941
+ height={12}
942
+ interactive
943
+ series={[series("price", "main", "left", "USD", [100, 101, 102, 103, 104, 105, 106, 107, 108])]}
944
+ panels={[{ id: "main" }]}
945
+ onViewportChange={(next) => {
946
+ viewportChanges.push(next
947
+ ? { start: next.start.toISOString(), end: next.end.toISOString() }
948
+ : null);
949
+ }}
950
+ />
951
+ </CaptureChartSurfaceProvider>,
952
+ { width: 62, height: 14 },
953
+ );
954
+ await act(async () => testSetup!.renderOnce());
955
+
956
+ await act(async () => {
957
+ capturedSurfaceProps!.onMouseDown(pointerEvent(10, 3, { shift: true }));
958
+ capturedSurfaceProps!.onMouseDrag(pointerEvent(30, 3, { shift: true }));
959
+ });
960
+ await act(async () => testSetup!.renderOnce());
961
+ // The selection must not pan the way an unmodified drag would, and the
962
+ // range has to read as text for renderers that cannot draw the band.
963
+ expect(viewportChanges).toHaveLength(0);
964
+ expect(testSetup.captureCharFrame()).toContain("→");
965
+
966
+ await act(async () => {
967
+ capturedSurfaceProps!.onMouseUp(pointerEvent(30, 3, { shift: true }));
968
+ });
969
+ await act(async () => testSetup!.renderOnce());
970
+
971
+ const zoomed = viewportChanges.at(-1);
972
+ expect(zoomed).toBeTruthy();
973
+ expect(new Date(zoomed!.start).getTime()).toBeGreaterThan(Date.parse("2025-01-01T00:00:00.000Z"));
974
+ expect(new Date(zoomed!.end).getTime()).toBeLessThan(Date.parse("2025-01-09T00:00:00.000Z"));
975
+ });
976
+
977
+ test("reports an alt-drag measurement without moving the viewport", async () => {
978
+ const viewportChanges: Array<{ start: string; end: string } | null> = [];
979
+ testSetup = await testRender(
980
+ <CaptureChartSurfaceProvider>
981
+ <CompositeChart
982
+ width={60}
983
+ height={12}
984
+ interactive
985
+ series={[series("price", "main", "left", "USD", [100, 101, 102, 103, 104, 105, 106, 107, 108])]}
986
+ panels={[{ id: "main" }]}
987
+ onViewportChange={(next) => {
988
+ viewportChanges.push(next
989
+ ? { start: next.start.toISOString(), end: next.end.toISOString() }
990
+ : null);
991
+ }}
992
+ />
993
+ </CaptureChartSurfaceProvider>,
994
+ { width: 62, height: 14 },
995
+ );
996
+ await act(async () => testSetup!.renderOnce());
997
+
998
+ await act(async () => {
999
+ capturedSurfaceProps!.onMouseDown(pointerEvent(8, 6, { alt: true }));
1000
+ capturedSurfaceProps!.onMouseDrag(pointerEvent(34, 1, { alt: true }));
1001
+ });
1002
+ await act(async () => testSetup!.renderOnce());
1003
+
1004
+ const measuringFrame = testSetup.captureCharFrame();
1005
+ // The readout sits in the middle of the box it measures, not in the legend.
1006
+ expect(measuringFrame).toContain("Δ");
1007
+ expect(measuringFrame).toContain("bars");
1008
+ expect(measuringFrame.split("\n")[0]).not.toContain("Δ");
1009
+ expect(viewportChanges).toHaveLength(0);
1010
+
1011
+ await act(async () => {
1012
+ capturedSurfaceProps!.onMouseUp(pointerEvent(34, 1, { alt: true }));
1013
+ });
1014
+ await act(async () => testSetup!.renderOnce());
1015
+ expect(testSetup.captureCharFrame()).not.toContain("Δ");
1016
+ expect(viewportChanges).toHaveLength(0);
1017
+ });
1018
+
1019
+ test("arms a measurement from the keyboard when a modifier drag cannot reach the app", async () => {
1020
+ testSetup = await testRender(
1021
+ <InputHostProvider host={chartInputHost}>
1022
+ <CaptureChartSurfaceProvider>
1023
+ <CompositeChart
1024
+ width={60}
1025
+ height={12}
1026
+ focused
1027
+ interactive
1028
+ series={[series("price", "main", "left", "USD", [100, 101, 102, 103, 104, 105, 106, 107, 108])]}
1029
+ panels={[{ id: "main" }]}
1030
+ />
1031
+ </CaptureChartSurfaceProvider>
1032
+ </InputHostProvider>,
1033
+ { width: 62, height: 14 },
1034
+ );
1035
+ await act(async () => testSetup!.renderOnce());
1036
+
1037
+ await act(async () => chartShortcut?.(keyEvent("m", true)));
1038
+ await act(async () => testSetup!.renderOnce());
1039
+
1040
+ // No modifier on the drag: the armed tool is what makes it a measurement.
1041
+ await act(async () => {
1042
+ capturedSurfaceProps!.onMouseDown(pointerEvent(8, 6));
1043
+ capturedSurfaceProps!.onMouseDrag(pointerEvent(34, 1));
1044
+ });
1045
+ await act(async () => testSetup!.renderOnce());
1046
+ expect(testSetup.captureCharFrame()).toContain("Δ");
1047
+
1048
+ await act(async () => {
1049
+ capturedSurfaceProps!.onMouseUp(pointerEvent(34, 1));
1050
+ });
1051
+ await act(async () => testSetup!.renderOnce());
1052
+ expect(testSetup.captureCharFrame()).not.toContain("Δ");
1053
+
1054
+ // Sticky: the tool still owns the next drag until it is dismissed.
1055
+ await act(async () => {
1056
+ capturedSurfaceProps!.onMouseDown(pointerEvent(8, 6));
1057
+ capturedSurfaceProps!.onMouseDrag(pointerEvent(30, 2));
1058
+ });
1059
+ await act(async () => testSetup!.renderOnce());
1060
+ expect(testSetup.captureCharFrame()).toContain("Δ");
1061
+ });
1062
+
1063
+ test("arms and disarms a chart tool from the toolbar", async () => {
1064
+ testSetup = await testRender(
1065
+ <CaptureChartSurfaceProvider>
1066
+ <CompositeChart
1067
+ width={60}
1068
+ height={12}
1069
+ interactive
1070
+ series={[series("price", "main", "left", "USD", [100, 101, 102, 103, 104, 105, 106, 107, 108])]}
1071
+ panels={[{ id: "main" }]}
1072
+ />
1073
+ </CaptureChartSurfaceProvider>,
1074
+ { width: 62, height: 14 },
1075
+ );
1076
+ await act(async () => testSetup!.renderOnce());
1077
+
1078
+ // Pressing the ruler icon has to arm the tool for an unmodified drag.
1079
+ await act(async () => {
1080
+ await testSetup!.mockMouse.moveTo(11, 1);
1081
+ await testSetup!.mockMouse.click(11, 1);
1082
+ });
1083
+ await act(async () => testSetup!.renderOnce());
1084
+ await act(async () => {
1085
+ capturedSurfaceProps!.onMouseDown(pointerEvent(8, 6));
1086
+ capturedSurfaceProps!.onMouseDrag(pointerEvent(30, 2));
1087
+ });
1088
+ await act(async () => testSetup!.renderOnce());
1089
+ expect(testSetup.captureCharFrame()).toContain("Δ");
1090
+
1091
+ await act(async () => {
1092
+ capturedSurfaceProps!.onMouseUp(pointerEvent(30, 2));
1093
+ await testSetup!.mockMouse.click(11, 1);
1094
+ });
1095
+ await act(async () => testSetup!.renderOnce());
1096
+ await act(async () => {
1097
+ capturedSurfaceProps!.onMouseDown(pointerEvent(8, 6));
1098
+ capturedSurfaceProps!.onMouseDrag(pointerEvent(30, 2));
1099
+ });
1100
+ await act(async () => testSetup!.renderOnce());
1101
+ expect(testSetup.captureCharFrame()).not.toContain("Δ");
1102
+ });
1103
+
1104
+ test("draws a line, then grabs its end to reshape it", async () => {
1105
+ testSetup = await testRender(
1106
+ <InputHostProvider host={chartInputHost}>
1107
+ <CaptureChartSurfaceProvider>
1108
+ <CompositeChart
1109
+ width={60}
1110
+ height={12}
1111
+ focused
1112
+ interactive
1113
+ series={[series("price", "main", "left", "USD", [100, 101, 102, 103, 104, 105, 106, 107, 108])]}
1114
+ panels={[{ id: "main" }]}
1115
+ />
1116
+ </CaptureChartSurfaceProvider>
1117
+ </InputHostProvider>,
1118
+ { width: 62, height: 14 },
1119
+ );
1120
+ await act(async () => testSetup!.renderOnce());
1121
+ await act(async () => chartShortcut?.(keyEvent("d", true)));
1122
+ await act(async () => testSetup!.renderOnce());
1123
+ const base = capturedSurfaceProps!.bitmaps?.[0] as { pixels: Uint8Array } | undefined;
1124
+
1125
+ await act(async () => {
1126
+ capturedSurfaceProps!.onMouseDown(pointerEvent(8, 6));
1127
+ capturedSurfaceProps!.onMouseDrag(pointerEvent(30, 2));
1128
+ capturedSurfaceProps!.onMouseUp(pointerEvent(30, 2));
1129
+ });
1130
+ await act(async () => testSetup!.renderOnce());
1131
+ const drawn = capturedSurfaceProps!.bitmaps?.[0] as { pixels: Uint8Array } | undefined;
1132
+ expect(Buffer.from(drawn!.pixels).equals(Buffer.from(base!.pixels))).toBe(false);
1133
+
1134
+ // Grabbing the endpoint that was just dropped has to reshape, not redraw.
1135
+ await act(async () => {
1136
+ capturedSurfaceProps!.onMouseDown(pointerEvent(8, 6));
1137
+ capturedSurfaceProps!.onMouseDrag(pointerEvent(14, 8));
1138
+ capturedSurfaceProps!.onMouseUp(pointerEvent(14, 8));
1139
+ });
1140
+ await act(async () => testSetup!.renderOnce());
1141
+ const reshaped = capturedSurfaceProps!.bitmaps?.[0] as { pixels: Uint8Array } | undefined;
1142
+ expect(reshaped).toBeTruthy();
1143
+ expect(Buffer.from(reshaped!.pixels).equals(Buffer.from(drawn!.pixels))).toBe(false);
1144
+ });
1145
+
1146
+ test("disarms a chart tool with escape", async () => {
1147
+ testSetup = await testRender(
1148
+ <InputHostProvider host={chartInputHost}>
1149
+ <CaptureChartSurfaceProvider>
1150
+ <CompositeChart
1151
+ width={60}
1152
+ height={12}
1153
+ focused
1154
+ interactive
1155
+ series={[series("price", "main", "left", "USD", [100, 101, 102, 103, 104, 105, 106, 107, 108])]}
1156
+ panels={[{ id: "main" }]}
1157
+ />
1158
+ </CaptureChartSurfaceProvider>
1159
+ </InputHostProvider>,
1160
+ { width: 62, height: 14 },
1161
+ );
1162
+ await act(async () => testSetup!.renderOnce());
1163
+
1164
+ await act(async () => chartShortcut?.(keyEvent("z", true)));
1165
+ await act(async () => chartShortcut?.(keyEvent("escape")));
1166
+ await act(async () => testSetup!.renderOnce());
1167
+
1168
+ // Disarmed: an unmodified drag pans instead of selecting a range.
1169
+ await act(async () => {
1170
+ capturedSurfaceProps!.onMouseDown(pointerEvent(8, 6));
1171
+ capturedSurfaceProps!.onMouseDrag(pointerEvent(30, 2));
1172
+ });
1173
+ await act(async () => testSetup!.renderOnce());
1174
+ expect(testSetup.captureCharFrame()).not.toContain("→");
1175
+ });
1176
+
1177
+ test("keeps a measurement readable in a narrow pane", async () => {
1178
+ testSetup = await testRender(
1179
+ <CaptureChartSurfaceProvider>
1180
+ <CompositeChart
1181
+ width={28}
1182
+ height={12}
1183
+ interactive
1184
+ series={[series("price", "main", "left", "USD", [100, 101, 102, 103, 104, 105, 106, 107, 108])]}
1185
+ panels={[{ id: "main" }]}
1186
+ />
1187
+ </CaptureChartSurfaceProvider>,
1188
+ { width: 30, height: 14 },
1189
+ );
1190
+ await act(async () => testSetup!.renderOnce());
1191
+
1192
+ await act(async () => {
1193
+ capturedSurfaceProps!.onMouseDown(pointerEvent(2, 6, { alt: true }));
1194
+ capturedSurfaceProps!.onMouseDrag(pointerEvent(14, 1, { alt: true }));
1195
+ });
1196
+ await act(async () => testSetup!.renderOnce());
1197
+
1198
+ // Too narrow for the whole summary, but the change still has to be visible.
1199
+ expect(testSetup.captureCharFrame()).toContain("Δ");
1200
+ });
1201
+
1202
+ test("ends a tool drag whose release landed on another pane", async () => {
1203
+ testSetup = await testRender(
1204
+ <CaptureChartSurfaceProvider>
1205
+ <CompositeChart
1206
+ width={60}
1207
+ height={12}
1208
+ interactive
1209
+ series={[series("price", "main", "left", "USD", [100, 101, 102, 103, 104, 105, 106, 107, 108])]}
1210
+ panels={[{ id: "main" }]}
1211
+ />
1212
+ </CaptureChartSurfaceProvider>,
1213
+ { width: 62, height: 14 },
1214
+ );
1215
+ await act(async () => testSetup!.renderOnce());
1216
+
1217
+ await act(async () => {
1218
+ capturedSurfaceProps!.onMouseDown(pointerEvent(8, 6, { alt: true }));
1219
+ capturedSurfaceProps!.onMouseDrag(pointerEvent(34, 1, { alt: true }));
1220
+ });
1221
+ await act(async () => testSetup!.renderOnce());
1222
+ expect(testSetup.captureCharFrame()).toContain("Δ");
1223
+
1224
+ // No mouse-up arrives: the pointer was released over a floating pane. The
1225
+ // next plain move only happens with the button already up.
1226
+ await act(async () => {
1227
+ capturedSurfaceProps!.onMouseMove(pointerEvent(20, 4));
1228
+ });
1229
+ await act(async () => testSetup!.renderOnce());
1230
+ expect(testSetup.captureCharFrame()).not.toContain("Δ");
1231
+ });
1232
+
916
1233
  test("maps the pointer crosshair level through both axes and labels its date", async () => {
917
1234
  testSetup = await testRender(
918
1235
  <CaptureChartSurfaceProvider>
@@ -1175,8 +1492,14 @@ describe("CompositeChart", () => {
1175
1492
  });
1176
1493
  await act(async () => testSetup!.renderOnce());
1177
1494
 
1495
+ // An observation-free window keeps the chart frame and its axes; only the
1496
+ // plotted points go away.
1497
+ // An observation-free window keeps the chart frame and its axes; only the
1498
+ // plotted points go away.
1178
1499
  const emptyFrame = testSetup.captureCharFrame();
1179
- expect(emptyFrame).toContain("No chart data");
1500
+ expect(emptyFrame).not.toContain("No chart data");
1501
+ expect(emptyFrame).not.toContain("•");
1502
+ expect(emptyFrame).toContain("$108");
1180
1503
  expect(emptyFrame).toContain("Jan ");
1181
1504
  expect(typeof capturedSurfaceProps!.onMouseScroll).toBe("function");
1182
1505
 
@@ -1189,7 +1512,7 @@ describe("CompositeChart", () => {
1189
1512
  await act(async () => testSetup!.renderOnce());
1190
1513
 
1191
1514
  const recoveredFrame = testSetup.captureCharFrame();
1192
- expect(recoveredFrame).not.toContain("No chart data");
1515
+ expect(recoveredFrame).toContain("•");
1193
1516
  expect(recoveredFrame).toContain("2025-01-01");
1194
1517
  expect(recoveredFrame).toContain("Jan 9");
1195
1518
  });
@@ -1220,7 +1543,7 @@ describe("CompositeChart", () => {
1220
1543
  });
1221
1544
 
1222
1545
  const frame = testSetup.captureCharFrame();
1223
- expect(frame).toContain("2025-01-02 12:05 UTC");
1546
+ expect(frame).toContain("12:05 UTC");
1224
1547
  expect(frame).toContain("09:30 UTC");
1225
1548
  expect(frame).toContain("16:00 UTC");
1226
1549
  });