gloomberb 0.6.1 → 0.7.1

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 (208) hide show
  1. package/package.json +1 -1
  2. package/patches/@opentui%2Fcore@0.1.90.patch +48 -24
  3. package/src/app.tsx +158 -57
  4. package/src/capabilities/factories.ts +3 -0
  5. package/src/cli/registry.test.ts +0 -1
  6. package/src/components/chart/chart-controller.ts +0 -1
  7. package/src/components/chart/chart-data.ts +1 -31
  8. package/src/components/chart/chart-renderer.ts +94 -12
  9. package/src/components/chart/chart-resolution.test.ts +0 -36
  10. package/src/components/chart/chart-resolution.ts +0 -27
  11. package/src/components/chart/chart.test.ts +60 -45
  12. package/src/components/chart/comparison-chart-data.test.ts +1 -5
  13. package/src/components/chart/comparison-chart-data.ts +1 -4
  14. package/src/components/chart/comparison-chart-renderer.test.ts +26 -0
  15. package/src/components/chart/comparison-chart-renderer.ts +13 -6
  16. package/src/components/chart/comparison-stock-chart.tsx +63 -39
  17. package/src/components/chart/price-axis-labels.tsx +114 -0
  18. package/src/components/chart/static-chart-surface.tsx +30 -4
  19. package/src/components/chart/stock-chart.test.ts +0 -16
  20. package/src/components/chart/stock-chart.tsx +69 -41
  21. package/src/components/chart/time-axis-label.tsx +120 -0
  22. package/src/components/command-bar/__snapshots__/command-bar.test.tsx.snap +0 -28
  23. package/src/components/command-bar/command-bar.test.tsx +182 -36
  24. package/src/components/command-bar/command-bar.tsx +943 -1016
  25. package/src/components/command-bar/command-registry.ts +0 -10
  26. package/src/components/command-bar/confirm-body.tsx +51 -0
  27. package/src/components/command-bar/helpers.ts +2 -5
  28. package/src/components/command-bar/list-model.ts +76 -0
  29. package/src/components/command-bar/theme-picker.tsx +254 -0
  30. package/src/components/command-bar/view-model.test.ts +4 -4
  31. package/src/components/command-bar/view-model.ts +1 -20
  32. package/src/components/command-bar/workflow-ops.ts +4 -0
  33. package/src/components/command-bar/workflow-types.ts +1 -2
  34. package/src/components/data-table-stack-view.test.tsx +39 -4
  35. package/src/components/data-table-stack-view.tsx +11 -104
  36. package/src/components/data-table-view.tsx +43 -79
  37. package/src/components/feed-data-table-stack-view.tsx +10 -17
  38. package/src/components/feed-stack-controller.ts +39 -0
  39. package/src/components/index.ts +5 -1
  40. package/src/components/layout/detached-pane-shell.tsx +29 -10
  41. package/src/components/layout/floating-pane.tsx +35 -4
  42. package/src/components/layout/header.tsx +12 -7
  43. package/src/components/layout/pane-content.test.tsx +64 -0
  44. package/src/components/layout/pane-content.tsx +2 -0
  45. package/src/components/layout/pane-cycle-order.test.ts +30 -0
  46. package/src/components/layout/pane-cycle-order.ts +32 -0
  47. package/src/components/layout/pane-footer.tsx +11 -4
  48. package/src/components/layout/pane-header.tsx +5 -5
  49. package/src/components/layout/pane-sizing.ts +9 -2
  50. package/src/components/layout/pane-title.ts +4 -1
  51. package/src/components/layout/pane.tsx +11 -5
  52. package/src/components/layout/shell.test.tsx +264 -22
  53. package/src/components/layout/shell.tsx +46 -8
  54. package/src/components/layout/status-bar.tsx +2 -0
  55. package/src/components/onboarding/onboarding-wizard.tsx +1 -2
  56. package/src/components/pane-settings-dialog.tsx +12 -4
  57. package/src/components/pane-template-wizard.test.tsx +3 -4
  58. package/src/components/speedometer-gauge.test.tsx +45 -0
  59. package/src/components/speedometer-gauge.tsx +562 -0
  60. package/src/components/table-view-shared.tsx +218 -0
  61. package/src/components/ticker-list-table-view.tsx +57 -73
  62. package/src/components/ticker-list-table.test.tsx +82 -1
  63. package/src/components/ticker-list-table.tsx +22 -9
  64. package/src/components/ui/confirm-dialog.tsx +66 -0
  65. package/src/components/ui/data-table.tsx +30 -19
  66. package/src/components/ui/frame.tsx +2 -0
  67. package/src/components/ui/index.ts +0 -1
  68. package/src/components/ui/page-stack-view.tsx +39 -10
  69. package/src/components/ui/table-layout.test.ts +42 -0
  70. package/src/components/ui/table-layout.ts +49 -4
  71. package/src/components/ui/tabs.tsx +34 -1
  72. package/src/components/ui/ui.test.tsx +168 -21
  73. package/src/core/state/app-state.ts +10 -17
  74. package/src/market-data/coordinator.test.ts +76 -1
  75. package/src/market-data/coordinator.ts +218 -49
  76. package/src/news/aggregator.test.ts +49 -1
  77. package/src/news/aggregator.ts +48 -3
  78. package/src/news/hooks.ts +6 -2
  79. package/src/plugins/builtin/ai/ask-ai-detail-tab.tsx +46 -16
  80. package/src/plugins/builtin/ai/providers.ts +0 -17
  81. package/src/plugins/builtin/ai/screener-pane.test.tsx +3 -4
  82. package/src/plugins/builtin/ai/screener-pane.tsx +3 -5
  83. package/src/plugins/builtin/analytics/index.tsx +1 -18
  84. package/src/plugins/builtin/ask-ai.test.tsx +89 -2
  85. package/src/plugins/builtin/broker-manager/index.test.tsx +2 -2
  86. package/src/plugins/builtin/broker-manager/index.tsx +19 -36
  87. package/src/plugins/builtin/chat-controller.test.ts +188 -1
  88. package/src/plugins/builtin/chat-controller.ts +537 -220
  89. package/src/plugins/builtin/chat.test.tsx +431 -13
  90. package/src/plugins/builtin/chat.tsx +1134 -227
  91. package/src/plugins/builtin/cloud-tweets.tsx +1 -1
  92. package/src/plugins/builtin/comparison-chart.test.tsx +53 -2
  93. package/src/plugins/builtin/comparison-chart.tsx +11 -5
  94. package/src/plugins/builtin/correlation/index.tsx +1 -6
  95. package/src/plugins/builtin/debug.tsx +1 -3
  96. package/src/plugins/builtin/econ/index.tsx +1 -2
  97. package/src/plugins/builtin/fear-greed/fear-greed-data.test.ts +72 -0
  98. package/src/plugins/builtin/fear-greed/fear-greed-data.ts +366 -0
  99. package/src/plugins/builtin/fear-greed/index.tsx +587 -0
  100. package/src/plugins/builtin/fx-matrix/index.tsx +2 -2
  101. package/src/plugins/builtin/help.test.tsx +2 -2
  102. package/src/plugins/builtin/help.tsx +2 -2
  103. package/src/plugins/builtin/holders/index.test.ts +13 -1
  104. package/src/plugins/builtin/holders/index.tsx +64 -29
  105. package/src/plugins/builtin/insider/index.tsx +22 -89
  106. package/src/plugins/builtin/market-movers/index.tsx +13 -19
  107. package/src/plugins/builtin/market-movers/screener.test.ts +39 -1
  108. package/src/plugins/builtin/market-movers/screener.ts +70 -17
  109. package/src/plugins/builtin/news-wire/breaking-pane.tsx +3 -2
  110. package/src/plugins/builtin/news-wire/index.tsx +41 -2
  111. package/src/plugins/builtin/news-wire/industry-pane.tsx +4 -18
  112. package/src/plugins/builtin/news-wire/news-detail-view.tsx +46 -4
  113. package/src/plugins/builtin/news-wire/news-preset-pane.tsx +3 -2
  114. package/src/plugins/builtin/news-wire/news-table.tsx +11 -11
  115. package/src/plugins/builtin/news.tsx +4 -16
  116. package/src/plugins/builtin/notes.tsx +19 -55
  117. package/src/plugins/builtin/options.test.tsx +58 -2
  118. package/src/plugins/builtin/options.tsx +110 -65
  119. package/src/plugins/builtin/plugin-groups.ts +2 -0
  120. package/src/plugins/builtin/portfolio-list/cli/commands.ts +1 -1
  121. package/src/plugins/builtin/portfolio-list/pane.tsx +16 -39
  122. package/src/plugins/builtin/research/index.test.ts +94 -0
  123. package/src/plugins/builtin/research/index.tsx +164 -33
  124. package/src/plugins/builtin/sec-filing-display.tsx +84 -0
  125. package/src/plugins/builtin/sec.tsx +13 -66
  126. package/src/plugins/builtin/sectors/index.tsx +1 -1
  127. package/src/plugins/builtin/ticker-detail/chart-tab.tsx +0 -1
  128. package/src/plugins/builtin/ticker-detail/index.tsx +0 -1
  129. package/src/plugins/builtin/ticker-detail/overview-tab.tsx +0 -1
  130. package/src/plugins/builtin/ticker-detail/pane.tsx +28 -22
  131. package/src/plugins/builtin/ticker-detail-chart-tab-switch.test.tsx +3 -4
  132. package/src/plugins/builtin/ticker-detail.test.tsx +38 -0
  133. package/src/plugins/builtin/world-indices/index.tsx +1 -1
  134. package/src/plugins/builtin/yield-curve/index.tsx +1 -1
  135. package/src/plugins/ibkr/broker-adapter.ts +1 -3
  136. package/src/plugins/ibkr/gateway-service-native.ts +1 -5
  137. package/src/plugins/ibkr/gateway-service.ts +1 -1
  138. package/src/plugins/ibkr/index.test.tsx +0 -2
  139. package/src/plugins/ibkr/trade-tab.test.tsx +3 -4
  140. package/src/plugins/pane-manager.ts +0 -13
  141. package/src/plugins/plugin-runtime.test.tsx +40 -6
  142. package/src/plugins/plugin-runtime.tsx +20 -10
  143. package/src/plugins/prediction-markets/cli.ts +0 -1
  144. package/src/plugins/prediction-markets/controller-data.ts +0 -2
  145. package/src/plugins/prediction-markets/controller.ts +0 -15
  146. package/src/plugins/prediction-markets/detail/pane.tsx +1 -0
  147. package/src/plugins/prediction-markets/pane.test.tsx +3 -11
  148. package/src/plugins/prediction-markets/test-helpers.tsx +1 -1
  149. package/src/plugins/registry.ts +13 -42
  150. package/src/plugins/ticker-navigation.test.ts +66 -0
  151. package/src/plugins/ticker-navigation.ts +44 -0
  152. package/src/react/input.ts +2 -1
  153. package/src/renderers/electrobun/bun/index.ts +32 -11
  154. package/src/renderers/electrobun/bun/window-focus.ts +37 -0
  155. package/src/renderers/electrobun/shared/protocol.ts +7 -1
  156. package/src/renderers/electrobun/view/app-services.ts +88 -31
  157. package/src/renderers/electrobun/view/backend-rpc.ts +15 -0
  158. package/src/renderers/electrobun/view/data-table.tsx +138 -101
  159. package/src/renderers/electrobun/view/desktop-controls.tsx +42 -20
  160. package/src/renderers/electrobun/view/desktop-window-bridge.ts +12 -1
  161. package/src/renderers/electrobun/view/dialog-host.tsx +13 -1
  162. package/src/renderers/electrobun/view/input-host.tsx +74 -10
  163. package/src/renderers/electrobun/view/key-event.ts +1 -1
  164. package/src/renderers/electrobun/view/main.tsx +18 -1
  165. package/src/renderers/electrobun/view/styles.css +329 -0
  166. package/src/renderers/electrobun/view/toast-host.tsx +29 -3
  167. package/src/renderers/electrobun/view/ui-host.tsx +42 -0
  168. package/src/renderers/opentui/chart-surface.tsx +179 -0
  169. package/src/renderers/opentui/input-host.tsx +1 -1
  170. package/src/renderers/opentui/start.tsx +48 -30
  171. package/src/renderers/opentui/test-utils.tsx +17 -0
  172. package/src/renderers/opentui/ui-host.tsx +5 -2
  173. package/src/sources/gloomberb-cloud.test.ts +86 -1
  174. package/src/sources/gloomberb-cloud.ts +117 -21
  175. package/src/sources/provider-router.test.ts +122 -0
  176. package/src/sources/provider-router.ts +198 -15
  177. package/src/sources/yahoo-finance.test.ts +44 -0
  178. package/src/sources/yahoo-finance.ts +6 -0
  179. package/src/state/app-bootstrap.ts +20 -7
  180. package/src/state/app-context-selector.test.tsx +189 -4
  181. package/src/state/app-context.test.ts +65 -0
  182. package/src/state/app-context.tsx +48 -16
  183. package/src/state/use-inline-tickers.test.tsx +73 -0
  184. package/src/state/use-inline-tickers.ts +19 -10
  185. package/src/test-support/plugin-runtime.ts +3 -2
  186. package/src/theme/colors.test.ts +75 -0
  187. package/src/theme/colors.ts +76 -6
  188. package/src/theme/theme-context.tsx +26 -0
  189. package/src/theme/themes.test.ts +23 -1
  190. package/src/theme/themes.ts +115 -0
  191. package/src/types/broker.ts +1 -1
  192. package/src/types/capability-route-source.ts +1 -0
  193. package/src/types/data-provider.ts +15 -2
  194. package/src/types/desktop-window.ts +6 -0
  195. package/src/types/financials.ts +2 -0
  196. package/src/types/plugin.ts +1 -3
  197. package/src/ui/host.tsx +7 -0
  198. package/src/utils/api-client.test.ts +32 -0
  199. package/src/utils/api-client.ts +119 -18
  200. package/src/utils/back-navigation.ts +24 -1
  201. package/src/utils/double-escape-close.test.ts +33 -0
  202. package/src/utils/double-escape-close.ts +40 -0
  203. package/src/utils/throttled-fetch.test.ts +22 -0
  204. package/src/utils/throttled-fetch.ts +3 -1
  205. package/src/utils/ticker-search.ts +53 -9
  206. package/src/version.ts +1 -1
  207. package/src/plugins/builtin/news-wire/feed-pane.tsx +0 -21
  208. package/src/plugins/builtin/news-wire/top-pane.tsx +0 -21
@@ -8,6 +8,7 @@ import { buildChartKey } from "../../market-data/selectors";
8
8
  import { useAppSelector, usePaneSettingValue } from "../../state/app-context";
9
9
  import { usePaneFooter, type PaneHint } from "../layout/pane-footer";
10
10
  import { blendHex, colors, getComparisonSeriesColor, priceColor } from "../../theme/colors";
11
+ import { useThemeColors } from "../../theme/theme-context";
11
12
  import type { BrokerContractRef } from "../../types/instrument";
12
13
  import type { PricePoint } from "../../types/financials";
13
14
  import { formatPercentRaw } from "../../utils/format";
@@ -54,12 +55,10 @@ import {
54
55
  normalizeChartResolutionSupport,
55
56
  sortChartResolutions,
56
57
  type ChartResolutionSupport,
57
- type ManualChartResolution,
58
58
  } from "./chart-resolution";
59
59
  import { RIGHT_EDGE_ANCHOR_RATIO, resolveAnchoredChartZoom } from "./chart-viewport";
60
60
  import {
61
61
  buildComparisonChartScene,
62
- formatComparisonAxisValue,
63
62
  formatComparisonCursorAxisValue,
64
63
  renderComparisonChart,
65
64
  } from "./comparison-chart-renderer";
@@ -97,7 +96,9 @@ import {
97
96
  resolveNativeSurfaceVisibleRect,
98
97
  type NativeSurfaceRenderableNode,
99
98
  } from "./native/surface-visibility";
100
- import { formatAxisCell, formatDateShort, resolveChartAxisWidth, type StyledContent } from "./chart-renderer";
99
+ import { formatDateShort, resolveChartAxisWidth, type StyledContent } from "./chart-renderer";
100
+ import { TimeAxisLabel } from "./time-axis-label";
101
+ import { PriceAxisLabels } from "./price-axis-labels";
101
102
 
102
103
  const MODE_CHIPS: Record<ComparisonChartRenderMode, string> = {
103
104
  area: "A",
@@ -112,6 +113,7 @@ interface ComparisonStockChartProps {
112
113
  symbols: string[];
113
114
  axisMode: ChartAxisMode;
114
115
  onOpenSymbol: (symbol: string) => void;
116
+ onEditTickers?: () => void;
115
117
  }
116
118
 
117
119
  interface ComparisonChartSymbolSource {
@@ -514,7 +516,9 @@ function ComparisonStockChartView({
514
516
  preferredRenderer,
515
517
  symbolSources,
516
518
  onOpenSymbol,
519
+ onEditTickers,
517
520
  }: ComparisonStockChartViewProps) {
521
+ useThemeColors();
518
522
  const renderer = useNativeRenderer();
519
523
  const { canvasCharts, cellWidthPx = 8, cellHeightPx = 18, pixelRatio = 1 } = useUiCapabilities();
520
524
  const nativeSurfaceManager = useMemo(() => getNativeSurfaceManager(renderer), [renderer]);
@@ -548,6 +552,7 @@ function ComparisonStockChartView({
548
552
  const lastCanvasBaseBitmapRef = useRef<{ key: string; bitmap: NativeChartBitmap } | null>(null);
549
553
  const displayCursorRef = useRef<DisplayCursorState>(EMPTY_DISPLAY_CURSOR);
550
554
  const targetCursorRef = useRef<DisplayCursorState>(EMPTY_DISPLAY_CURSOR);
555
+ const mouseCrosshairDisabledRef = useRef(false);
551
556
  const cursorMotionKindRef = useRef<ChartCursorMotionKind>("discrete");
552
557
  const animationFrameRef = useRef<number | null>(null);
553
558
  const pendingCanonicalResetRef = useRef(1);
@@ -908,11 +913,10 @@ function ComparisonStockChartView({
908
913
  }
909
914
  return {
910
915
  ...current,
911
- panOffset: clamp(pendingExpansion.targetPanOffset, 0, getMaxComparisonPanOffset(series, current.presetRange, current.zoomLevel, chartWidth)),
916
+ panOffset: clamp(pendingExpansion.targetPanOffset, 0, getMaxComparisonPanOffset(series, current.zoomLevel)),
912
917
  };
913
918
  });
914
919
  }, [
915
- chartWidth,
916
920
  series,
917
921
  seriesDates,
918
922
  viewState.activePreset,
@@ -982,8 +986,8 @@ function ComparisonStockChartView({
982
986
  }), [viewState.panOffset, viewState.renderMode, viewState.zoomLevel]);
983
987
 
984
988
  const visibleWindow = useMemo(() => (
985
- getVisibleComparisonWindow(series, projectionViewState, chartWidth)
986
- ), [chartWidth, projectionViewState, series]);
989
+ getVisibleComparisonWindow(series, projectionViewState)
990
+ ), [projectionViewState, series]);
987
991
 
988
992
  const projection = useMemo(() => (
989
993
  projectComparisonChartData(series, chartWidth, projectionViewState, axisMode)
@@ -1037,6 +1041,14 @@ function ComparisonStockChartView({
1037
1041
  selectedSymbol: viewState.selectedSymbol,
1038
1042
  colors: chartColors,
1039
1043
  }), [chartColors, chartHeight, chartWidth, projection, viewState.selectedSymbol]);
1044
+ const displayScene = useMemo(() => buildComparisonChartScene(projection, {
1045
+ width: chartWidth,
1046
+ height: chartHeight,
1047
+ cursorX: displayCursorX,
1048
+ cursorY: displayCursorY,
1049
+ selectedSymbol: viewState.selectedSymbol,
1050
+ colors: chartColors,
1051
+ }), [chartColors, chartHeight, chartWidth, displayCursorX, displayCursorY, projection, viewState.selectedSymbol]);
1040
1052
 
1041
1053
  const staticResult = useMemo(() => renderComparisonChart(projection, {
1042
1054
  width: chartWidth,
@@ -1088,6 +1100,12 @@ function ComparisonStockChartView({
1088
1100
  };
1089
1101
 
1090
1102
  return [
1103
+ ...(onEditTickers ? [{
1104
+ id: "tickers",
1105
+ key: "t",
1106
+ label: "ickers",
1107
+ onPress: onEditTickers,
1108
+ }] : []),
1091
1109
  {
1092
1110
  id: "mode",
1093
1111
  key: "m",
@@ -1111,26 +1129,13 @@ function ComparisonStockChartView({
1111
1129
  { id: "reset", key: "0", label: "reset", onPress: resetView },
1112
1130
  ...(width >= 72 ? [{ id: "range", key: "1-7", label: "range", onPress: cycleRange }] : []),
1113
1131
  ];
1114
- }, [effectiveResolution, resolutionChips, series, supportMap, viewState.presetRange, visibleDateWindow, width]);
1132
+ }, [effectiveResolution, onEditTickers, resolutionChips, series, supportMap, viewState.presetRange, visibleDateWindow, width]);
1115
1133
 
1116
1134
  usePaneFooter("comparison-chart", () => ({
1117
1135
  order: 10,
1118
1136
  hints: footerHints,
1119
1137
  }), [footerHints]);
1120
1138
 
1121
- const liveScene = useMemo(() => (
1122
- effectiveRenderer === "kitty"
1123
- ? staticScene
1124
- : buildComparisonChartScene(projection, {
1125
- width: chartWidth,
1126
- height: chartHeight,
1127
- cursorX: displayCursorX,
1128
- cursorY: displayCursorY,
1129
- selectedSymbol: viewState.selectedSymbol,
1130
- colors: chartColors,
1131
- })
1132
- ), [chartColors, chartHeight, chartWidth, displayCursorX, displayCursorY, effectiveRenderer, projection, staticScene, viewState.selectedSymbol]);
1133
-
1134
1139
  const nativeCrosshair = useMemo<NativeCrosshairOverlay | null>(() => {
1135
1140
  if (displayCursor.cellX === null || displayCursor.cellY === null) return null;
1136
1141
  return {
@@ -1385,7 +1390,7 @@ function ComparisonStockChartView({
1385
1390
  return;
1386
1391
  case "a": {
1387
1392
  const panStep = getKeyboardPanCellCount(chartWidth);
1388
- if (viewState.panOffset >= getMaxComparisonPanOffset(series, viewState.presetRange, viewState.zoomLevel, chartWidth) && expandBufferRange({
1393
+ if (viewState.panOffset >= getMaxComparisonPanOffset(series, viewState.zoomLevel) && expandBufferRange({
1389
1394
  kind: "pan-left",
1390
1395
  targetPanOffset: viewState.panOffset + panStep,
1391
1396
  })) {
@@ -1393,7 +1398,7 @@ function ComparisonStockChartView({
1393
1398
  }
1394
1399
  setViewState((current) => ({
1395
1400
  ...clearActivePreset(current),
1396
- panOffset: clamp(current.panOffset + panStep, 0, getMaxComparisonPanOffset(series, current.presetRange, current.zoomLevel, chartWidth)),
1401
+ panOffset: clamp(current.panOffset + panStep, 0, getMaxComparisonPanOffset(series, current.zoomLevel)),
1397
1402
  }));
1398
1403
  return;
1399
1404
  }
@@ -1401,7 +1406,7 @@ function ComparisonStockChartView({
1401
1406
  const panStep = getKeyboardPanCellCount(chartWidth);
1402
1407
  setViewState((current) => ({
1403
1408
  ...clearActivePreset(current),
1404
- panOffset: clamp(current.panOffset - panStep, 0, getMaxComparisonPanOffset(series, current.presetRange, current.zoomLevel, chartWidth)),
1409
+ panOffset: clamp(current.panOffset - panStep, 0, getMaxComparisonPanOffset(series, current.zoomLevel)),
1405
1410
  }));
1406
1411
  return;
1407
1412
  }
@@ -1417,7 +1422,11 @@ function ComparisonStockChartView({
1417
1422
  setResolution(nextResolution);
1418
1423
  return;
1419
1424
  }
1425
+ case "t":
1426
+ onEditTickers?.();
1427
+ return;
1420
1428
  case "h":
1429
+ mouseCrosshairDisabledRef.current = false;
1421
1430
  cursorMotionKindRef.current = "discrete";
1422
1431
  setViewState((current) => ({
1423
1432
  ...current,
@@ -1425,6 +1434,7 @@ function ComparisonStockChartView({
1425
1434
  }));
1426
1435
  return;
1427
1436
  case "l":
1437
+ mouseCrosshairDisabledRef.current = false;
1428
1438
  cursorMotionKindRef.current = "discrete";
1429
1439
  setViewState((current) => ({
1430
1440
  ...current,
@@ -1432,6 +1442,7 @@ function ComparisonStockChartView({
1432
1442
  }));
1433
1443
  return;
1434
1444
  case "escape":
1445
+ mouseCrosshairDisabledRef.current = true;
1435
1446
  updateDisplayCursorTarget(EMPTY_DISPLAY_CURSOR, "discrete");
1436
1447
  setViewState((current) => ({ ...current, cursorX: null, cursorY: null }));
1437
1448
  return;
@@ -1488,6 +1499,7 @@ function ComparisonStockChartView({
1488
1499
  });
1489
1500
 
1490
1501
  const handlePlotMove = (event: ChartMouseEvent) => {
1502
+ if (mouseCrosshairDisabledRef.current) return;
1491
1503
  const localPointer = getLocalPlotPointer(event, plotRef.current, renderer);
1492
1504
  if (!localPointer) return;
1493
1505
  const selectionCursor = resolveSelectionCursor(localPointer, projection.dates.length, chartWidth);
@@ -1506,6 +1518,7 @@ function ComparisonStockChartView({
1506
1518
  };
1507
1519
 
1508
1520
  const handlePlotDown = (event: ChartMouseEvent) => {
1521
+ mouseCrosshairDisabledRef.current = false;
1509
1522
  const localPointer = getLocalPlotPointer(event, plotRef.current, renderer);
1510
1523
  if (!localPointer) return;
1511
1524
  const selectionCursor = resolveSelectionCursor(localPointer, projection.dates.length, chartWidth);
@@ -1528,6 +1541,7 @@ function ComparisonStockChartView({
1528
1541
  };
1529
1542
 
1530
1543
  const handlePlotDrag = (event: ChartMouseEvent) => {
1544
+ if (mouseCrosshairDisabledRef.current && !dragRef.current) return;
1531
1545
  const localPointer = getLocalPlotPointer(event, plotRef.current, renderer);
1532
1546
  if (localPointer) {
1533
1547
  const selectionCursor = resolveSelectionCursor(localPointer, projection.dates.length, chartWidth);
@@ -1592,7 +1606,7 @@ function ComparisonStockChartView({
1592
1606
  if (scrollPanCells === 0) return;
1593
1607
 
1594
1608
  const targetPanOffset = viewState.panOffset + scrollPanCells;
1595
- if (scrollPanCells > 0 && viewState.panOffset >= getMaxComparisonPanOffset(series, viewState.presetRange, viewState.zoomLevel, chartWidth) && expandBufferRange({
1609
+ if (scrollPanCells > 0 && viewState.panOffset >= getMaxComparisonPanOffset(series, viewState.zoomLevel) && expandBufferRange({
1596
1610
  kind: "pan-left",
1597
1611
  targetPanOffset,
1598
1612
  })) {
@@ -1603,7 +1617,7 @@ function ComparisonStockChartView({
1603
1617
  panOffset: clamp(
1604
1618
  current.panOffset + scrollPanCells,
1605
1619
  0,
1606
- getMaxComparisonPanOffset(series, current.presetRange, current.zoomLevel, chartWidth),
1620
+ getMaxComparisonPanOffset(series, current.zoomLevel),
1607
1621
  ),
1608
1622
  }));
1609
1623
  };
@@ -1613,7 +1627,8 @@ function ComparisonStockChartView({
1613
1627
  }
1614
1628
 
1615
1629
  const hasChartData = series.some((entry) => entry.points.length > 0);
1616
- const cursorScene = effectiveRenderer === "kitty" ? staticScene : liveScene;
1630
+ const hasDisplayCursor = displayCursorX !== null && displayCursorY !== null;
1631
+ const cursorScene = hasDisplayCursor ? displayScene : staticScene;
1617
1632
  const selectedSeries = hasChartData ? (cursorScene?.selectedSeries ?? staticResult.selectedSeries) : null;
1618
1633
  const selectedPoint = hasChartData ? (cursorScene?.selectedPoint ?? staticResult.selectedPoint) : null;
1619
1634
  const selectedRawValue = selectedPoint?.rawValue ?? selectedSeries?.latestRawValue ?? null;
@@ -1641,6 +1656,8 @@ function ComparisonStockChartView({
1641
1656
  start: visibleWindow.dates[0] ?? null,
1642
1657
  end: visibleWindow.dates[visibleWindow.dates.length - 1] ?? null,
1643
1658
  });
1659
+ const cursorTimeAxisColumn = hasDisplayCursor ? displayScene?.cursorColumn ?? null : null;
1660
+ const cursorTimeAxisDate = hasDisplayCursor ? displayScene?.activeDate ?? null : null;
1644
1661
 
1645
1662
  const canvasBitmapSize = useMemo(() => {
1646
1663
  if (!canvasCharts) return null;
@@ -1813,17 +1830,16 @@ function ComparisonStockChartView({
1813
1830
  );
1814
1831
 
1815
1832
  const axisBox = (
1816
- <Box width={axisSectionWidth} height={chartHeight} flexDirection="column">
1817
- {Array.from({ length: chartHeight }, (_, row) => {
1818
- const isCursorRow = cursorAxisLabel !== null && cursorRow === row;
1819
- const label = isCursorRow ? cursorAxisLabel : (axisLabels.get(row) ?? null);
1820
- return (
1821
- <Text key={row} fg={isCursorRow ? chartColors.crosshairColor : colors.textDim}>
1822
- {formatAxisCell(label, axisWidth).padEnd(axisSectionWidth)}
1823
- </Text>
1824
- );
1825
- })}
1826
- </Box>
1833
+ <PriceAxisLabels
1834
+ axisLabels={axisLabels}
1835
+ axisWidth={axisWidth}
1836
+ axisSectionWidth={axisSectionWidth}
1837
+ height={chartHeight}
1838
+ cursorRow={cursorRow}
1839
+ cursorPixelY={hasDisplayCursor ? displayCursor.pixelY : null}
1840
+ cursorLabel={cursorAxisLabel}
1841
+ cursorColor={chartColors.crosshairColor}
1842
+ />
1827
1843
  );
1828
1844
 
1829
1845
  const legendRowsData = Array.from({ length: Math.ceil(symbols.length / legendColumns) }, (_, rowIndex) => (
@@ -1929,7 +1945,15 @@ function ComparisonStockChartView({
1929
1945
  </Box>
1930
1946
 
1931
1947
  <Box height={timeAxisRows}>
1932
- <Text fg={colors.textDim}>{timeAxisLabel}</Text>
1948
+ <TimeAxisLabel
1949
+ timeLabels={timeAxisLabel}
1950
+ width={chartWidth}
1951
+ cursorColumn={cursorTimeAxisColumn}
1952
+ cursorPixelX={hasDisplayCursor ? displayCursor.pixelX : null}
1953
+ cursorDate={cursorTimeAxisDate}
1954
+ dates={projection.dates}
1955
+ cursorColor={chartColors.crosshairColor}
1956
+ />
1933
1957
  </Box>
1934
1958
 
1935
1959
  {legendRows > 0 && (
@@ -0,0 +1,114 @@
1
+ import { useMemo } from "react";
2
+ import { Box, Text, useUiCapabilities } from "../../ui";
3
+ import { colors } from "../../theme/colors";
4
+ import { formatAxisCell } from "./chart-renderer";
5
+
6
+ interface PriceAxisLabelsProps {
7
+ axisLabels: ReadonlyMap<number, string>;
8
+ axisWidth: number;
9
+ axisSectionWidth: number;
10
+ height: number;
11
+ cursorRow: number | null;
12
+ cursorPixelY?: number | null;
13
+ cursorLabel: string | null;
14
+ cursorColor: string;
15
+ }
16
+
17
+ interface CursorPriceAxisOverlay {
18
+ labelText: string | null;
19
+ topPercent: number | null;
20
+ }
21
+
22
+ function clamp(value: number, min: number, max: number): number {
23
+ return Math.max(min, Math.min(max, value));
24
+ }
25
+
26
+ export function buildCursorPriceAxisOverlay({
27
+ axisWidth,
28
+ axisSectionWidth,
29
+ height,
30
+ cursorPixelY,
31
+ cursorLabel,
32
+ cellHeightPx,
33
+ }: {
34
+ axisWidth: number;
35
+ axisSectionWidth: number;
36
+ height: number;
37
+ cursorPixelY: number | null | undefined;
38
+ cursorLabel: string | null;
39
+ cellHeightPx: number;
40
+ }): CursorPriceAxisOverlay {
41
+ const labelText = cursorLabel === null
42
+ ? null
43
+ : formatAxisCell(cursorLabel, axisWidth).padEnd(axisSectionWidth);
44
+ const pixelHeight = Math.max(height * cellHeightPx, 1);
45
+ let topPercent: number | null = null;
46
+
47
+ if (labelText && cursorPixelY !== null && cursorPixelY !== undefined && Number.isFinite(cursorPixelY) && pixelHeight > 1) {
48
+ const halfLabelHeight = Math.min(cellHeightPx / 2, (pixelHeight - 1) / 2);
49
+ const topPx = clamp(cursorPixelY, halfLabelHeight, Math.max(pixelHeight - halfLabelHeight, halfLabelHeight));
50
+ topPercent = (topPx / Math.max(pixelHeight - 1, 1)) * 100;
51
+ }
52
+
53
+ return { labelText, topPercent };
54
+ }
55
+
56
+ export function PriceAxisLabels({
57
+ axisLabels,
58
+ axisWidth,
59
+ axisSectionWidth,
60
+ height,
61
+ cursorRow,
62
+ cursorPixelY = null,
63
+ cursorLabel,
64
+ cursorColor,
65
+ }: PriceAxisLabelsProps) {
66
+ const { cellHeightPx = 18, fractionalViewport = false } = useUiCapabilities();
67
+ const overlay = useMemo(() => buildCursorPriceAxisOverlay({
68
+ axisWidth,
69
+ axisSectionWidth,
70
+ height,
71
+ cursorPixelY,
72
+ cursorLabel,
73
+ cellHeightPx,
74
+ }), [axisSectionWidth, axisWidth, cellHeightPx, cursorLabel, cursorPixelY, height]);
75
+ const usePixelOverlay = fractionalViewport && overlay.labelText !== null && overlay.topPercent !== null;
76
+
77
+ return (
78
+ <Box
79
+ width={axisSectionWidth}
80
+ height={height}
81
+ flexDirection="column"
82
+ overflow="hidden"
83
+ style={usePixelOverlay ? { position: "relative" } : undefined}
84
+ >
85
+ {Array.from({ length: height }, (_, row) => {
86
+ const isCursorRow = !usePixelOverlay && cursorLabel !== null && cursorRow === row;
87
+ const label = isCursorRow ? cursorLabel : (axisLabels.get(row) ?? null);
88
+ return (
89
+ <Text key={row} fg={isCursorRow ? cursorColor : colors.textDim}>
90
+ {formatAxisCell(label, axisWidth).padEnd(axisSectionWidth)}
91
+ </Text>
92
+ );
93
+ })}
94
+ {usePixelOverlay ? (
95
+ <Text
96
+ width={axisSectionWidth}
97
+ fg={cursorColor}
98
+ bg={colors.bg}
99
+ style={{
100
+ position: "absolute",
101
+ left: 0,
102
+ top: `${overlay.topPercent}%`,
103
+ transform: "translateY(-50%)",
104
+ whiteSpace: "pre",
105
+ pointerEvents: "none",
106
+ zIndex: 1,
107
+ }}
108
+ >
109
+ {overlay.labelText}
110
+ </Text>
111
+ ) : null}
112
+ </Box>
113
+ );
114
+ }
@@ -1,5 +1,6 @@
1
1
  import { useMemo } from "react";
2
- import { Box, ChartSurface, Text, useUiCapabilities } from "../../ui";
2
+ import { Box, ChartSurface, Text, useNativeRenderer, useUiCapabilities } from "../../ui";
3
+ import { useThemeColors } from "../../theme/theme-context";
3
4
  import type { ProjectedChartPoint } from "./chart-data";
4
5
  import {
5
6
  buildChartScene,
@@ -8,7 +9,7 @@ import {
8
9
  renderChart,
9
10
  type RenderChartOptions,
10
11
  } from "./chart-renderer";
11
- import { renderNativeChartBase, type NativeChartBitmap } from "./native/chart-rasterizer";
12
+ import { computeBitmapSize, renderNativeChartBase, type NativeChartBitmap } from "./native/chart-rasterizer";
12
13
 
13
14
  export interface StaticChartSurfaceProps extends Omit<
14
15
  RenderChartOptions,
@@ -45,12 +46,15 @@ export function StaticChartSurface({
45
46
  yAxisColor,
46
47
  formatYAxisValue,
47
48
  }: StaticChartSurfaceProps) {
49
+ useThemeColors();
48
50
  const {
49
51
  canvasCharts,
52
+ nativeCharts,
50
53
  cellWidthPx = 8,
51
54
  cellHeightPx = 18,
52
55
  pixelRatio = 1,
53
56
  } = useUiCapabilities();
57
+ const nativeRenderer = useNativeRenderer();
54
58
  const timeAxisRows = showTimeAxis ? 1 : 0;
55
59
  const labelRows = yAxisLabel ? 1 : 0;
56
60
  const totalWidth = Math.max(1, Math.floor(width));
@@ -101,14 +105,36 @@ export function StaticChartSurface({
101
105
  : 0;
102
106
  const axisGap = axisWidth > 0 ? 1 : 0;
103
107
  const plotWidth = Math.max(1, totalWidth - axisWidth - axisGap);
108
+ const rendererResolution = nativeRenderer.resolution;
109
+ const rendererTerminalWidth = nativeRenderer.terminalWidth;
110
+ const rendererTerminalHeight = nativeRenderer.terminalHeight;
104
111
  const bitmapSize = useMemo(() => {
105
- if (!canvasCharts) return null;
112
+ if (nativeCharts && rendererResolution && rendererTerminalWidth > 0 && rendererTerminalHeight > 0) {
113
+ return computeBitmapSize(
114
+ { x: 0, y: 0, width: plotWidth, height: plotHeight },
115
+ rendererResolution,
116
+ rendererTerminalWidth,
117
+ rendererTerminalHeight,
118
+ );
119
+ }
120
+ if (!canvasCharts && !nativeCharts) return null;
106
121
  const scale = Math.max(1, pixelRatio);
107
122
  return {
108
123
  pixelWidth: Math.max(1, Math.round(plotWidth * cellWidthPx * scale)),
109
124
  pixelHeight: Math.max(1, Math.round(plotHeight * cellHeightPx * scale)),
110
125
  };
111
- }, [canvasCharts, cellHeightPx, cellWidthPx, pixelRatio, plotHeight, plotWidth]);
126
+ }, [
127
+ canvasCharts,
128
+ cellHeightPx,
129
+ cellWidthPx,
130
+ nativeCharts,
131
+ pixelRatio,
132
+ plotHeight,
133
+ plotWidth,
134
+ rendererResolution,
135
+ rendererTerminalHeight,
136
+ rendererTerminalWidth,
137
+ ]);
112
138
  const renderOptions = useMemo<RenderChartOptions>(() => ({
113
139
  width: plotWidth,
114
140
  height: plotHeight,
@@ -9,7 +9,6 @@ import {
9
9
  resolveAdjacentSelectionCursorX,
10
10
  resolveChartKeyboardKey,
11
11
  resolveIndicatorBufferRange,
12
- resolveVisibleChartDateWindow,
13
12
  resolveSelectionDisplayCursorState,
14
13
  } from "./stock-chart";
15
14
  import { getPointTerminalColumn } from "./chart-renderer";
@@ -215,21 +214,6 @@ describe("stock chart auto helpers", () => {
215
214
  expect(nextState.window).toEqual(renderedAutoView.window);
216
215
  });
217
216
 
218
- test("derives the visible label window from the actual visible chart points", () => {
219
- const visibleWindow = resolveVisibleChartDateWindow([
220
- { date: new Date("2026-01-03T00:00:00Z") },
221
- { date: new Date("2026-01-06T00:00:00Z") },
222
- ], {
223
- start: new Date("2026-01-01T00:00:00Z"),
224
- end: new Date("2026-01-08T00:00:00Z"),
225
- });
226
-
227
- expect(visibleWindow).toEqual({
228
- start: new Date("2026-01-03T00:00:00Z"),
229
- end: new Date("2026-01-06T00:00:00Z"),
230
- });
231
- });
232
-
233
217
  test("zooms in by at least one visible point on every auto step when more detail is available", () => {
234
218
  const history = makeDailyHistory(1, 10);
235
219