gloomberb 0.10.0 → 0.10.2

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 (353) hide show
  1. package/package.json +8 -1
  2. package/src/api-client/auth.ts +1 -0
  3. package/src/api-client/index.test.ts +359 -108
  4. package/src/api-client/index.ts +24 -13
  5. package/src/api-client/socket.ts +70 -35
  6. package/src/api-client/types.ts +1 -1
  7. package/src/app/language-observer.test.tsx +82 -0
  8. package/src/app/language-observer.tsx +13 -0
  9. package/src/app/pane-runtime/index.tsx +2 -2
  10. package/src/app/pane-runtime/pane-settings-runtime.tsx +2 -2
  11. package/src/app/pane-runtime/pane-template-runtime.ts +2 -2
  12. package/src/app/pane-runtime/plugin-bindings.ts +2 -2
  13. package/src/app/pane-runtime/ticker-open-runtime.ts +2 -2
  14. package/src/app/runtime/broker-import.ts +2 -2
  15. package/src/app/runtime/plugin-bindings.ts +2 -2
  16. package/src/app/runtime/startup.ts +2 -2
  17. package/src/app.tsx +16 -7
  18. package/src/brokers/account-cache.ts +6 -6
  19. package/src/brokers/broker-ticker-sync.ts +3 -3
  20. package/src/brokers/remote-broker-adapter.ts +2 -2
  21. package/src/brokers/sync-broker-instance.ts +5 -6
  22. package/src/cli/commands/api.ts +3 -6
  23. package/src/cli/commands/automation.ts +7 -15
  24. package/src/cli/commands/market.ts +26 -61
  25. package/src/cli/commands/overview.ts +34 -45
  26. package/src/cli/commands/search.ts +25 -27
  27. package/src/cli/commands/system.ts +27 -57
  28. package/src/cli/commands/ticker.ts +62 -63
  29. package/src/cli/context.test.ts +63 -0
  30. package/src/cli/context.ts +15 -1
  31. package/src/cli/index.ts +1 -1
  32. package/src/cli/pane-functions/data.ts +2 -19
  33. package/src/cli/pane-functions/index.ts +26 -25
  34. package/src/cli/pane-functions/report.ts +1 -1
  35. package/src/cli/pane-functions/screenshot.ts +3 -3
  36. package/src/cli/scoped-context.ts +41 -0
  37. package/src/components/chart/composite/composite-chart.test.tsx +615 -18
  38. package/src/components/chart/composite/composite-chart.tsx +179 -79
  39. package/src/components/chart/composite/format.test.ts +20 -7
  40. package/src/components/chart/composite/index.ts +0 -21
  41. package/src/components/chart/composite/interactions.test.ts +90 -3
  42. package/src/components/chart/composite/interactions.ts +189 -24
  43. package/src/components/chart/composite/price-series.ts +0 -2
  44. package/src/components/chart/composite/rasterizer.ts +10 -9
  45. package/src/components/chart/composite/renderers.test.ts +108 -2
  46. package/src/components/chart/composite/scene.test.ts +36 -0
  47. package/src/components/chart/composite/scene.ts +5 -2
  48. package/src/components/chart/composite/text-renderer.ts +37 -25
  49. package/src/components/chart/composite/time-axis.test.ts +243 -0
  50. package/src/components/chart/composite/time-axis.ts +608 -0
  51. package/src/components/chart/composite/types.ts +12 -1
  52. package/src/components/chart/core/data.ts +2 -159
  53. package/src/components/chart/core/date-window.ts +6 -225
  54. package/src/components/chart/core/palette.ts +1 -12
  55. package/src/components/chart/core/pointer.ts +0 -142
  56. package/src/components/chart/core/types.ts +1 -58
  57. package/src/components/chart/cursor-motion.ts +2 -4
  58. package/src/components/chart/indicators/types.ts +0 -9
  59. package/src/components/chart/market-session.ts +0 -11
  60. package/src/components/chart/native/kitty/protocol.ts +1 -1
  61. package/src/components/chart/native/raster/layout.ts +3 -10
  62. package/src/components/chart/static/chart/axis-overlays.test.tsx +64 -0
  63. package/src/components/chart/static/chart/axis-overlays.tsx +59 -14
  64. package/src/components/chart/static/chart/surface.tsx +1 -2
  65. package/src/components/command-bar/action-runtime.ts +2 -2
  66. package/src/components/command-bar/commands/direct/index.ts +1 -1
  67. package/src/components/command-bar/panel/body.tsx +2 -2
  68. package/src/components/command-bar/panel/index.tsx +11 -9
  69. package/src/components/command-bar/routes/ticker-search/actions.ts +2 -2
  70. package/src/components/command-bar/surface/index.tsx +2 -2
  71. package/src/components/command-bar/workflow/body.tsx +3 -3
  72. package/src/components/command-bar/workflow/collection-actions.ts +2 -2
  73. package/src/components/command-bar/workflow/coordinator.ts +2 -2
  74. package/src/components/command-bar/workflow/field-row.tsx +25 -11
  75. package/src/components/command-bar/workflow/tickers.ts +2 -2
  76. package/src/components/feed-data-table/stack-view.test.tsx +28 -36
  77. package/src/components/feed-data-table/stack-view.tsx +3 -1
  78. package/src/components/layout/detached-pane-shell.tsx +6 -6
  79. package/src/components/layout/header.tsx +13 -9
  80. package/src/components/layout/pane/content.test.tsx +42 -3
  81. package/src/components/layout/pane/content.tsx +3 -1
  82. package/src/components/layout/pane/footer/index.test.tsx +45 -2
  83. package/src/components/layout/pane/footer/registration.tsx +3 -1
  84. package/src/components/layout/shell/index.tsx +1 -2
  85. package/src/components/layout/shell/native/surfaces.ts +1 -1
  86. package/src/components/layout/shell/native/window-state.ts +1 -1
  87. package/src/components/layout/status-bar.tsx +12 -8
  88. package/src/components/layout/window-controls.tsx +8 -6
  89. package/src/components/onboarding/onboarding-wizard.tsx +3 -1
  90. package/src/components/pane-settings-dialog/desktop.tsx +0 -2
  91. package/src/components/pane-settings-dialog.tsx +0 -2
  92. package/src/components/pane-template-wizard.tsx +10 -9
  93. package/src/components/price-performance.tsx +2 -113
  94. package/src/components/price-sparkline/model.ts +15 -17
  95. package/src/components/price-sparkline/view.tsx +8 -7
  96. package/src/components/speedometer-gauge/desktop.tsx +2 -1
  97. package/src/components/speedometer-gauge/index.tsx +0 -2
  98. package/src/components/speedometer-gauge/terminal.tsx +24 -5
  99. package/src/components/ticker/badge/index.tsx +1 -18
  100. package/src/components/ui/data-table/opentui/index.tsx +3 -3
  101. package/src/components/ui/frame.tsx +1 -2
  102. package/src/components/ui/page-stack-view.test.tsx +126 -0
  103. package/src/components/ui/page-stack-view.tsx +24 -12
  104. package/src/components/ui/tabs.tsx +7 -3
  105. package/src/components/ui/ui.test.tsx +23 -2
  106. package/src/components/use-external-link-footer.ts +4 -2
  107. package/src/core/app-service-ports.ts +46 -0
  108. package/src/core/app-services.test.ts +66 -0
  109. package/src/core/app-services.ts +17 -15
  110. package/src/data/config/chart-settings.ts +3 -10
  111. package/src/data/config/store/index.test.ts +99 -2
  112. package/src/data/config/store/migrations.ts +278 -0
  113. package/src/data/config/store/normalize.ts +44 -186
  114. package/src/data/resource-store.ts +73 -12
  115. package/src/data/sqlite/cache.test.ts +104 -15
  116. package/src/i18n/index.test.ts +17 -0
  117. package/src/i18n/index.ts +23 -6
  118. package/src/i18n/ja.ts +13 -0
  119. package/src/i18n/ko.ts +13 -0
  120. package/src/i18n/react.ts +6 -0
  121. package/src/i18n/zh-cn.ts +13 -0
  122. package/src/i18n/zh-tw.ts +13 -0
  123. package/src/market-data/coordinator/auxiliary.ts +7 -3
  124. package/src/market-data/coordinator/chart.ts +1 -1
  125. package/src/market-data/coordinator/index.test.ts +26 -0
  126. package/src/market-data/coordinator/index.ts +11 -5
  127. package/src/market-data/coordinator/quotes.ts +4 -40
  128. package/src/market-data/coordinator/subscriptions.test.ts +89 -1
  129. package/src/market-data/hooks.tsx +47 -1
  130. package/src/market-data/market/status.ts +6 -6
  131. package/src/market-data/performance.ts +1 -1
  132. package/src/market-data/quote-subscription-target.test.ts +37 -0
  133. package/src/market-data/quote-subscription-target.ts +60 -0
  134. package/src/market-data/quotes/freshness.ts +1 -0
  135. package/src/market-data/quotes/resolution.ts +4 -0
  136. package/src/market-data/request-types.ts +3 -3
  137. package/src/news/aggregator.test.ts +97 -1
  138. package/src/news/aggregator.ts +100 -57
  139. package/src/news/hooks.test.tsx +64 -0
  140. package/src/news/hooks.ts +18 -25
  141. package/src/plugins/builtin/account-management/footer.ts +4 -2
  142. package/src/plugins/builtin/account-management/pane.tsx +9 -4
  143. package/src/plugins/builtin/ai/agent-history.ts +11 -7
  144. package/src/plugins/builtin/ai/ask-ai-detail-tab.tsx +0 -5
  145. package/src/plugins/builtin/ai/ask-ai.test.tsx +3 -4
  146. package/src/plugins/builtin/ai/index.test.ts +2 -2
  147. package/src/plugins/builtin/ai/index.tsx +10 -11
  148. package/src/plugins/builtin/ai/pane-settings.ts +1 -1
  149. package/src/plugins/builtin/ai/pi/runtime.ts +6 -5
  150. package/src/plugins/builtin/ai/providers.ts +1 -9
  151. package/src/plugins/builtin/ai/runner.ts +0 -6
  152. package/src/plugins/builtin/ai/screener/footer.ts +4 -1
  153. package/src/plugins/builtin/ai/screener/pane.test.tsx +9 -9
  154. package/src/plugins/builtin/ai/workspace/model.ts +2 -27
  155. package/src/plugins/builtin/ai/workspace/pane.tsx +2 -2
  156. package/src/plugins/builtin/analytics/metrics.ts +1 -7
  157. package/src/plugins/builtin/broker-manager/detail.tsx +3 -2
  158. package/src/plugins/builtin/broker-manager/index.tsx +4 -2
  159. package/src/plugins/builtin/chart-composer/editor/controller.ts +643 -0
  160. package/src/plugins/builtin/chart-composer/editor/desktop-view.tsx +134 -0
  161. package/src/plugins/builtin/chart-composer/editor/model.test.ts +85 -0
  162. package/src/plugins/builtin/chart-composer/editor/model.ts +292 -0
  163. package/src/plugins/builtin/chart-composer/editor/opentui-view.tsx +108 -0
  164. package/src/plugins/builtin/chart-composer/editor.tsx +65 -947
  165. package/src/plugins/builtin/chart-composer/index.tsx +0 -6
  166. package/src/plugins/builtin/chart-composer/pane.tsx +69 -36
  167. package/src/plugins/builtin/chart-composer/presets.ts +1 -19
  168. package/src/plugins/builtin/chart-composer/settings.ts +1 -1
  169. package/src/plugins/builtin/chat/content/composer-runtime.ts +1 -1
  170. package/src/plugins/builtin/chat/content/shortcuts.ts +0 -1
  171. package/src/plugins/builtin/chat/controller/fetch.ts +8 -2
  172. package/src/plugins/builtin/chat/controller/persistence.ts +2 -0
  173. package/src/plugins/builtin/chat/controller/session.test.ts +25 -0
  174. package/src/plugins/builtin/chat/controller/session.ts +2 -0
  175. package/src/plugins/builtin/chat/controller/state.ts +7 -0
  176. package/src/plugins/builtin/chat/controller.test.ts +73 -20
  177. package/src/plugins/builtin/chat/message/desktop.tsx +2 -0
  178. package/src/plugins/builtin/cloud-tweets/footer.ts +9 -28
  179. package/src/plugins/builtin/cloud-tweets/pane.tsx +0 -5
  180. package/src/plugins/builtin/cloud-tweets/table.tsx +12 -17
  181. package/src/plugins/builtin/correlation/compute.ts +4 -10
  182. package/src/plugins/builtin/econ/index.tsx +17 -19
  183. package/src/plugins/builtin/holders/types.ts +0 -1
  184. package/src/plugins/builtin/insider/index.tsx +6 -31
  185. package/src/plugins/builtin/news/index.tsx +5 -7
  186. package/src/plugins/builtin/news/wire/breaking/pane.test.tsx +6 -8
  187. package/src/plugins/builtin/news/wire/news/footer.ts +10 -11
  188. package/src/plugins/builtin/news/wire/read-state.test.ts +7 -0
  189. package/src/plugins/builtin/news/wire/read-state.ts +18 -28
  190. package/src/plugins/builtin/news/wire/rss/parser.ts +1 -11
  191. package/src/plugins/builtin/options/footer.test.ts +115 -0
  192. package/src/plugins/builtin/options/footer.ts +110 -0
  193. package/src/plugins/builtin/options/live-quotes.test.ts +238 -0
  194. package/src/plugins/builtin/options/live-quotes.ts +147 -0
  195. package/src/plugins/builtin/options/view.test.tsx +71 -1
  196. package/src/plugins/builtin/options/view.tsx +39 -24
  197. package/src/plugins/builtin/portfolio-list/cli/portfolio-command.ts +103 -117
  198. package/src/plugins/builtin/portfolio-list/cli/render.ts +11 -5
  199. package/src/plugins/builtin/portfolio-list/cli/watchlist-command.ts +120 -135
  200. package/src/plugins/builtin/portfolio-list/header.tsx +9 -4
  201. package/src/plugins/builtin/portfolio-list/quick-add/index.tsx +6 -1
  202. package/src/plugins/builtin/portfolio-list/quick-add/resolution.ts +4 -2
  203. package/src/plugins/builtin/research/corporate-actions-pane.tsx +24 -52
  204. package/src/plugins/builtin/sec/filing-content.test.ts +41 -0
  205. package/src/plugins/builtin/sec/filing-content.ts +94 -0
  206. package/src/plugins/builtin/sec/index.tsx +21 -104
  207. package/src/plugins/builtin/sectors/sector-model.ts +1 -7
  208. package/src/plugins/builtin/shared/cloud-upgrade.ts +1 -0
  209. package/src/plugins/builtin/shared/pane-footer.ts +82 -0
  210. package/src/plugins/builtin/shared/read-state.ts +64 -3
  211. package/src/plugins/builtin/substack/read-state.ts +16 -41
  212. package/src/plugins/builtin/thirteenf/pane.tsx +24 -73
  213. package/src/plugins/builtin/ticker-detail/quote-monitor/index.tsx +10 -10
  214. package/src/plugins/builtin/yield-curve/index.tsx +11 -9
  215. package/src/plugins/ibkr/dialogs.tsx +3 -3
  216. package/src/plugins/ibkr/gateway/fundamentals.ts +1 -1
  217. package/src/plugins/ibkr/gateway/history-requests.ts +2 -2
  218. package/src/plugins/ibkr/gateway/history.ts +2 -2
  219. package/src/plugins/ibkr/gateway/selection.ts +2 -2
  220. package/src/plugins/ibkr/gateway/service/index.ts +2 -2
  221. package/src/plugins/ibkr/gateway/service/native.ts +2 -2
  222. package/src/plugins/plugin-persistence.ts +3 -4
  223. package/src/plugins/prediction-markets/controller/keyboard.ts +1 -1
  224. package/src/plugins/prediction-markets/controller/live-updates.ts +6 -5
  225. package/src/plugins/prediction-markets/detail/book.tsx +2 -2
  226. package/src/plugins/prediction-markets/detail/outcomes.tsx +3 -1
  227. package/src/plugins/prediction-markets/detail/pane.tsx +3 -1
  228. package/src/plugins/prediction-markets/detail/trades.tsx +3 -3
  229. package/src/plugins/registry/context.ts +3 -4
  230. package/src/plugins/registry/index.ts +5 -6
  231. package/src/react/input.test.ts +126 -3
  232. package/src/react/input.ts +120 -1
  233. package/src/remote/app-host.tsx +2 -13
  234. package/src/renderers/electrobun/bun/desktop/backend-requests.ts +44 -60
  235. package/src/renderers/electrobun/bun/desktop/capability-bridge.ts +15 -13
  236. package/src/renderers/electrobun/bun/desktop/host-requests.ts +26 -22
  237. package/src/renderers/electrobun/bun/desktop/http-fetch.ts +5 -1
  238. package/src/renderers/electrobun/bun/desktop/initialization.ts +11 -6
  239. package/src/renderers/electrobun/bun/desktop/media.ts +5 -3
  240. package/src/renderers/electrobun/bun/desktop/plugin-state.ts +21 -22
  241. package/src/renderers/electrobun/bun/desktop/window-controls.ts +3 -1
  242. package/src/renderers/electrobun/bun/desktop/workspace/requests.ts +18 -18
  243. package/src/renderers/electrobun/bun/index.ts +123 -83
  244. package/src/renderers/electrobun/shared/protocol.ts +146 -7
  245. package/src/renderers/electrobun/view/app-services.ts +5 -8
  246. package/src/renderers/electrobun/view/backend-rpc.ts +18 -8
  247. package/src/renderers/electrobun/view/broker-remote-client.ts +2 -2
  248. package/src/renderers/electrobun/view/cli-pane-shot-entry.tsx +5 -22
  249. package/src/renderers/electrobun/view/config-host.ts +1 -1
  250. package/src/renderers/electrobun/view/context-menu.ts +1 -17
  251. package/src/renderers/electrobun/view/data-table/index.tsx +0 -1
  252. package/src/renderers/electrobun/view/data-table/row.tsx +0 -4
  253. package/src/renderers/electrobun/view/desktop/control-styles.ts +37 -34
  254. package/src/renderers/electrobun/view/desktop/controls.tsx +36 -33
  255. package/src/renderers/electrobun/view/desktop/list-view.tsx +15 -12
  256. package/src/renderers/electrobun/view/desktop/popover.tsx +2 -3
  257. package/src/renderers/electrobun/view/dialog-host.tsx +2 -3
  258. package/src/renderers/electrobun/view/fatal-screen.tsx +0 -1
  259. package/src/renderers/electrobun/view/host/box.tsx +0 -1
  260. package/src/renderers/electrobun/view/host/chart-surface.tsx +0 -1
  261. package/src/renderers/electrobun/view/host/input.tsx +0 -1
  262. package/src/renderers/electrobun/view/host/media-surface.tsx +0 -1
  263. package/src/renderers/electrobun/view/host/native.ts +0 -1
  264. package/src/renderers/electrobun/view/host/scroll-box.tsx +0 -1
  265. package/src/renderers/electrobun/view/host/tabs.tsx +0 -1
  266. package/src/renderers/electrobun/view/host/text.tsx +0 -1
  267. package/src/renderers/electrobun/view/http-fetch.ts +3 -11
  268. package/src/renderers/electrobun/view/input-host.tsx +10 -53
  269. package/src/renderers/electrobun/view/key-event.ts +0 -1
  270. package/src/renderers/electrobun/view/main.tsx +3 -3
  271. package/src/renderers/electrobun/view/native-renderer.ts +0 -1
  272. package/src/renderers/electrobun/view/native-stubs/backend-rpc.ts +13 -1
  273. package/src/renderers/electrobun/view/native-stubs/chart/surface-manager.ts +11 -0
  274. package/src/renderers/electrobun/view/native-stubs/native-stubs.test.ts +47 -0
  275. package/src/renderers/electrobun/view/remote/asset-data-client.ts +6 -44
  276. package/src/renderers/electrobun/view/remote/persistence.ts +6 -2
  277. package/src/renderers/electrobun/view/remote/quote-subscription-registry.test.ts +86 -0
  278. package/src/renderers/electrobun/view/remote/quote-subscription-registry.ts +72 -0
  279. package/src/renderers/electrobun/view/remote/ticker-repository.ts +2 -2
  280. package/src/renderers/electrobun/view/toast-host.tsx +0 -1
  281. package/src/renderers/electrobun/view/ui-host.tsx +1 -2
  282. package/src/renderers/electrobun/view/update-host.ts +1 -1
  283. package/src/renderers/opentui/chart-surface.tsx +2 -1
  284. package/src/renderers/opentui/dialog-host.tsx +20 -4
  285. package/src/renderers/opentui/image/loader.ts +12 -2
  286. package/src/renderers/opentui/input-host.tsx +7 -49
  287. package/src/renderers/opentui/start.tsx +2 -0
  288. package/src/sources/gloomberb-cloud/history-quality.ts +44 -0
  289. package/src/sources/gloomberb-cloud/index.test.ts +173 -0
  290. package/src/sources/gloomberb-cloud/index.ts +42 -20
  291. package/src/sources/gloomberb-cloud/normalizers.ts +9 -3
  292. package/src/sources/provider-router/cache.ts +1 -1
  293. package/src/sources/provider-router/document-routes.ts +10 -5
  294. package/src/sources/provider-router/financials.test.ts +14 -0
  295. package/src/sources/provider-router/financials.ts +6 -1
  296. package/src/sources/provider-router/history.test.ts +163 -1
  297. package/src/sources/provider-router/history.ts +278 -347
  298. package/src/sources/provider-router/index.ts +2 -2
  299. package/src/sources/provider-router/routing.test.ts +39 -0
  300. package/src/sources/provider-router/routing.ts +24 -11
  301. package/src/sources/provider-router/supplemental.ts +37 -17
  302. package/src/sources/sec-edgar/content.ts +2 -12
  303. package/src/sources/sec-edgar.ts +2 -13
  304. package/src/sources/yahoo-finance/history.ts +2 -2
  305. package/src/sources/yahoo-finance/options.ts +5 -0
  306. package/src/sources/yahoo-finance.ts +2 -2
  307. package/src/state/app/bootstrap.ts +2 -2
  308. package/src/state/app/context/index.tsx +2 -2
  309. package/src/state/hooks/quote-streaming.ts +66 -1
  310. package/src/state/hooks/session-snapshot.ts +2 -2
  311. package/src/state/selectors.ts +0 -9
  312. package/src/sync/controller.test.ts +98 -0
  313. package/src/sync/controller.ts +29 -5
  314. package/src/sync/react.ts +2 -2
  315. package/src/sync/types.ts +2 -2
  316. package/src/theme/colors.test.ts +10 -0
  317. package/src/theme/colors.ts +75 -78
  318. package/src/theme/theme-context.test.tsx +67 -0
  319. package/src/theme/theme-context.tsx +9 -5
  320. package/src/tickers/open-target.ts +2 -2
  321. package/src/tickers/search/upsert.ts +2 -2
  322. package/src/time-series/alignment.ts +0 -7
  323. package/src/time-series/chart-data.ts +145 -0
  324. package/src/time-series/date-window.ts +44 -0
  325. package/src/time-series/history-window.ts +21 -0
  326. package/src/time-series/hooks.test.ts +59 -0
  327. package/src/time-series/hooks.ts +33 -12
  328. package/src/time-series/market-resolution.ts +2 -2
  329. package/src/time-series/market.ts +1 -1
  330. package/src/time-series/range.ts +9 -0
  331. package/src/{components/chart/core → time-series}/resolution.ts +3 -102
  332. package/src/time-series/resolve.test.ts +203 -1
  333. package/src/time-series/resolve.ts +96 -18
  334. package/src/time-series/spec-studies.test.ts +15 -0
  335. package/src/time-series/spec.ts +2 -2
  336. package/src/time-series/studies.ts +5 -2
  337. package/src/time-series/types.ts +3 -1
  338. package/src/time-series/use-chart-resolution.test.tsx +358 -0
  339. package/src/time-series/use-chart-resolution.ts +46 -4
  340. package/src/types/broker.ts +2 -2
  341. package/src/types/data-provider.ts +3 -3
  342. package/src/types/financials.ts +12 -0
  343. package/src/types/plugin.ts +2 -2
  344. package/src/ui/context-menu.tsx +3 -2
  345. package/src/ui/dialog.tsx +2 -2
  346. package/src/utils/html-entities.ts +3 -5
  347. package/src/utils/main-thread-monitor.ts +1 -1
  348. package/src/utils/price-history.ts +5 -5
  349. package/src/version.ts +3 -1
  350. package/src/components/chart/core/viewport.test.ts +0 -8
  351. package/src/components/chart/core/viewport.ts +0 -24
  352. package/src/time-series/index.ts +0 -14
  353. /package/src/{components/chart/core → time-series}/resolution.test.ts +0 -0
@@ -0,0 +1,643 @@
1
+ import { useEffect, useMemo, useRef, useState } from "react";
2
+ import type { InputRenderable } from "../../../../ui";
3
+ import type { ListViewItem } from "../../../../components/ui";
4
+ import { useDialogKeyboard } from "../../../../ui/dialog";
5
+ import type {
6
+ ChartSeriesSpec,
7
+ ChartSpec,
8
+ PanelScale,
9
+ SeriesPeriod,
10
+ SeriesStyle,
11
+ SeriesTimestampMode,
12
+ SeriesTransform,
13
+ } from "../../../../time-series/types";
14
+ import { validateChartSpec } from "../../../../time-series/spec";
15
+ import { isPlainKey } from "../../../../utils/keyboard";
16
+ import { getSharedRegistry } from "../../../registry";
17
+ import {
18
+ canToggleChartSeries,
19
+ MAX_CHART_COMPOSER_SERIES,
20
+ parseChartSpecOr,
21
+ } from "../chart-spec";
22
+ import {
23
+ appendChartSeries,
24
+ applySeriesStyle,
25
+ applySeriesTimestampMode,
26
+ buildEmptyChartPreset,
27
+ buildSeriesSpec,
28
+ chartSeriesLabel,
29
+ formatSeriesExpression,
30
+ getCompatibleSeriesStyles,
31
+ getCompatibleSeriesTransforms,
32
+ getSelectedBuiltinStudies,
33
+ getSelectedPairStudies,
34
+ parseSeriesExpression,
35
+ setBuiltinStudies,
36
+ setPairStudies,
37
+ } from "../presets";
38
+ import type { SeriesCatalogInstrument, SeriesCatalogSuggestion } from "../series-catalog";
39
+ import { useSeriesCatalogSuggestions } from "../use-series-catalog";
40
+ import type {
41
+ SeriesEditorFieldId,
42
+ SeriesEditorFocus,
43
+ } from "./model";
44
+ import {
45
+ buildSeriesEditorActions,
46
+ buildSeriesEditorFields,
47
+ getSeriesEditorFieldIds,
48
+ getSeriesTimestampMode,
49
+ supportsSeriesTimestampMode,
50
+ titleCaseSeriesEditorValue,
51
+ } from "./model";
52
+
53
+ function clampIndex(value: number, length: number): number {
54
+ if (length <= 0) return -1;
55
+ return Math.max(0, Math.min(value, length - 1));
56
+ }
57
+
58
+ function seriesFieldId(series: ChartSeriesSpec): string {
59
+ return series.source.kind === "security" ? series.source.fieldId : series.source.seriesId;
60
+ }
61
+
62
+ function timingDescription(series: ChartSeriesSpec): string | null {
63
+ if (!supportsSeriesTimestampMode(series)) return null;
64
+ return getSeriesTimestampMode(series) === "available-at" ? "Available date" : "Period end";
65
+ }
66
+
67
+ function pruneSpec(spec: ChartSpec): ChartSpec {
68
+ const selectedBuiltinStudies = getSelectedBuiltinStudies(spec);
69
+ const selectedPairStudies = getSelectedPairStudies(spec);
70
+ const rebound = setPairStudies(
71
+ setBuiltinStudies(spec, selectedBuiltinStudies),
72
+ selectedPairStudies,
73
+ );
74
+ const seriesIds = new Set(rebound.series.map((series) => series.id));
75
+ const studies = rebound.studies.filter((study) => {
76
+ const requiredInputs = study.kind === "ratio" || study.kind === "spread" || study.kind === "correlation" ? 2 : 1;
77
+ return study.inputSeriesIds.length === requiredInputs
78
+ && study.inputSeriesIds.every((id) => seriesIds.has(id));
79
+ });
80
+ const panels = [...rebound.panels];
81
+ if (!panels.some((panel) => panel.id === "main")) panels.unshift({ id: "main" });
82
+ return { ...rebound, panels, studies };
83
+ }
84
+
85
+ function replaceAt<T>(values: readonly T[], index: number, value: T): T[] {
86
+ return values.map((entry, entryIndex) => entryIndex === index ? value : entry);
87
+ }
88
+
89
+ function moveAt<T>(values: readonly T[], index: number, delta: -1 | 1): T[] {
90
+ const target = index + delta;
91
+ if (index < 0 || target < 0 || target >= values.length) return [...values];
92
+ const next = [...values];
93
+ [next[index], next[target]] = [next[target]!, next[index]!];
94
+ return next;
95
+ }
96
+
97
+ export function useSeriesEditorController({
98
+ dialogId,
99
+ resolve,
100
+ initialSpec,
101
+ isDesktop,
102
+ }: {
103
+ dialogId?: string;
104
+ resolve: (value: ChartSpec | null) => void;
105
+ initialSpec: ChartSpec;
106
+ isDesktop: boolean;
107
+ }) {
108
+ const [draft, setDraft] = useState(() => parseChartSpecOr(initialSpec, buildEmptyChartPreset()));
109
+ const [selectedIndex, setSelectedIndex] = useState(() => clampIndex(0, initialSpec.series.length));
110
+ const [expression, setExpression] = useState(() => initialSpec.series[0] ? formatSeriesExpression(initialSpec.series[0]) : "");
111
+ const [editingExpression, setEditingExpression] = useState(false);
112
+ const [quickAddActive, setQuickAddActive] = useState(true);
113
+ const [quickAddQuery, setQuickAddQuery] = useState("");
114
+ const [quickAddSelection, setQuickAddSelection] = useState(0);
115
+ const [keyboardFocus, setKeyboardFocus] = useState<SeriesEditorFocus>("add");
116
+ const [error, setError] = useState<string | null>(null);
117
+ const expressionRef = useRef<InputRenderable | null>(null);
118
+ const quickAddRef = useRef<InputRenderable | null>(null);
119
+ const keyboardFocusRef = useRef<SeriesEditorFocus>("add");
120
+ const catalogCommitLockRef = useRef(false);
121
+ const expressionCommitLockRef = useRef(false);
122
+ const selected = selectedIndex >= 0 ? draft.series[selectedIndex] ?? null : null;
123
+ const keyboardFields = useMemo<SeriesEditorFieldId[]>(
124
+ () => getSeriesEditorFieldIds(selected),
125
+ [selected],
126
+ );
127
+ const keyboardTargets = useMemo<SeriesEditorFocus[]>(() => [
128
+ "add",
129
+ ...(selected ? ["series" as const, "source" as const, ...keyboardFields] : []),
130
+ ], [keyboardFields, selected]);
131
+
132
+ const activateQuickAdd = () => {
133
+ setQuickAddActive(true);
134
+ };
135
+
136
+ const deactivateQuickAdd = () => {
137
+ setQuickAddActive(false);
138
+ };
139
+
140
+ const updateKeyboardFocus = (target: SeriesEditorFocus) => {
141
+ keyboardFocusRef.current = target;
142
+ setKeyboardFocus(target);
143
+ };
144
+
145
+ const focusKeyboardTarget = (target: SeriesEditorFocus) => {
146
+ if (!keyboardTargets.includes(target)) return;
147
+ updateKeyboardFocus(target);
148
+ if (target === "add") {
149
+ setEditingExpression(false);
150
+ expressionRef.current?.blur?.();
151
+ activateQuickAdd();
152
+ queueMicrotask(() => quickAddRef.current?.focus?.());
153
+ return;
154
+ }
155
+
156
+ deactivateQuickAdd();
157
+ quickAddRef.current?.blur?.();
158
+ if (target === "source") {
159
+ setEditingExpression(true);
160
+ queueMicrotask(() => expressionRef.current?.focus?.());
161
+ } else {
162
+ setEditingExpression(false);
163
+ expressionRef.current?.blur?.();
164
+ }
165
+ };
166
+
167
+ const moveKeyboardFocus = (direction: -1 | 1) => {
168
+ const currentIndex = Math.max(0, keyboardTargets.indexOf(keyboardFocusRef.current));
169
+ const nextIndex = (currentIndex + direction + keyboardTargets.length) % keyboardTargets.length;
170
+ const next = keyboardTargets[nextIndex];
171
+ if (next) focusKeyboardTarget(next);
172
+ };
173
+
174
+ useEffect(() => {
175
+ const next = selected ? formatSeriesExpression(selected) : "";
176
+ setExpression(next);
177
+ setError(null);
178
+ setEditingExpression(false);
179
+ }, [selected?.id]);
180
+
181
+ const defaultInstrument = useMemo<SeriesCatalogInstrument>(() => {
182
+ const firstSecurity = draft.series.find((series) => series.source.kind === "security");
183
+ const security = selected?.source.kind === "security"
184
+ ? selected.source.instrument
185
+ : firstSecurity?.source.kind === "security"
186
+ ? firstSecurity.source.instrument
187
+ : undefined;
188
+ const symbol = security?.symbol ?? "AAPL";
189
+ const saved = getSharedRegistry()?.getTickerFn(symbol);
190
+ return {
191
+ symbol,
192
+ ...(security?.exchange ? { exchange: security.exchange } : saved?.metadata.exchange ? { exchange: saved.metadata.exchange } : {}),
193
+ ...(saved?.metadata.name ? { name: saved.metadata.name } : {}),
194
+ };
195
+ }, [draft.series, selected]);
196
+ const {
197
+ suggestions: quickAddSuggestions,
198
+ loading: quickAddLoading,
199
+ } = useSeriesCatalogSuggestions({
200
+ query: quickAddQuery,
201
+ defaultInstrument,
202
+ enabled: quickAddActive,
203
+ });
204
+
205
+ useEffect(() => {
206
+ setQuickAddSelection(0);
207
+ }, [quickAddQuery, quickAddSuggestions.length]);
208
+
209
+ useEffect(() => {
210
+ if (!keyboardTargets.includes(keyboardFocus)) updateKeyboardFocus("add");
211
+ }, [keyboardFocus, keyboardTargets]);
212
+
213
+ const updateSelected = (update: (series: ChartSeriesSpec) => ChartSeriesSpec) => {
214
+ if (!selected) return;
215
+ setDraft((current) => ({
216
+ ...current,
217
+ series: replaceAt(current.series, selectedIndex, update(current.series[selectedIndex]!)),
218
+ }));
219
+ };
220
+
221
+ const commitExpression = (): boolean => {
222
+ if (expressionCommitLockRef.current) return true;
223
+ if (!selected) return false;
224
+ const parsed = parseSeriesExpression(expression);
225
+ if (!parsed) {
226
+ setError("Use SYMBOL, SYMBOL:field, SYMBOL:EXCHANGE:field, or FRED:series.");
227
+ return false;
228
+ }
229
+ expressionCommitLockRef.current = true;
230
+ queueMicrotask(() => {
231
+ expressionCommitLockRef.current = false;
232
+ });
233
+
234
+ const candidate = buildSeriesSpec(parsed, selectedIndex);
235
+ const previousFieldId = seriesFieldId(selected);
236
+ const nextFieldId = seriesFieldId(candidate);
237
+ const styles = getCompatibleSeriesStyles(nextFieldId);
238
+ const transforms = getCompatibleSeriesTransforms(nextFieldId);
239
+ const source = candidate.source.kind === "security" && selected.source.kind === "security"
240
+ ? {
241
+ ...candidate.source,
242
+ ...(previousFieldId === nextFieldId
243
+ ? { period: selected.source.period }
244
+ : {}),
245
+ ...(supportsSeriesTimestampMode(candidate) && supportsSeriesTimestampMode(selected)
246
+ ? { timestampMode: getSeriesTimestampMode(selected) }
247
+ : {}),
248
+ instrument: candidate.source.instrument.symbol === selected.source.instrument.symbol
249
+ && (candidate.source.instrument.exchange ?? "") === (selected.source.instrument.exchange ?? "")
250
+ ? selected.source.instrument
251
+ : candidate.source.instrument,
252
+ }
253
+ : candidate.source;
254
+ const style = previousFieldId === nextFieldId || styles.includes(selected.style)
255
+ ? selected.style
256
+ : candidate.style;
257
+ const next = applySeriesStyle({
258
+ ...candidate,
259
+ id: selected.id,
260
+ source,
261
+ ...(selected.label ? { label: selected.label } : {}),
262
+ ...(selected.color ? { color: selected.color } : {}),
263
+ ...(selected.visible !== undefined ? { visible: selected.visible } : {}),
264
+ style,
265
+ transform: previousFieldId === nextFieldId || transforms.includes(selected.transform)
266
+ ? selected.transform
267
+ : candidate.transform,
268
+ axis: selected.axis,
269
+ panelId: selected.panelId,
270
+ }, style);
271
+ setDraft((current) => ({ ...current, series: replaceAt(current.series, selectedIndex, next) }));
272
+ setExpression(formatSeriesExpression(next));
273
+ setEditingExpression(false);
274
+ expressionRef.current?.blur?.();
275
+ setError(null);
276
+ return true;
277
+ };
278
+
279
+ const clearQuickAddInput = () => {
280
+ setQuickAddQuery("");
281
+ quickAddRef.current?.editBuffer.setText?.("");
282
+ quickAddRef.current?.setCursorOffset?.(0);
283
+ };
284
+
285
+ const beginQuickAdd = (reset = false) => {
286
+ if (draft.series.length >= MAX_CHART_COMPOSER_SERIES) {
287
+ setError(`Charts support up to ${MAX_CHART_COMPOSER_SERIES} base series.`);
288
+ return;
289
+ }
290
+ if (reset) clearQuickAddInput();
291
+ updateKeyboardFocus("add");
292
+ setEditingExpression(false);
293
+ activateQuickAdd();
294
+ setError(null);
295
+ quickAddRef.current?.focus?.();
296
+ if (reset) {
297
+ queueMicrotask(() => {
298
+ clearQuickAddInput();
299
+ quickAddRef.current?.focus?.();
300
+ });
301
+ }
302
+ };
303
+
304
+ const addCatalogSuggestion = (suggestion: SeriesCatalogSuggestion | undefined) => {
305
+ if (!suggestion || catalogCommitLockRef.current) return;
306
+ if (draft.series.length >= MAX_CHART_COMPOSER_SERIES) {
307
+ setError(`Charts support up to ${MAX_CHART_COMPOSER_SERIES} base series.`);
308
+ return;
309
+ }
310
+ catalogCommitLockRef.current = true;
311
+ queueMicrotask(() => {
312
+ catalogCommitLockRef.current = false;
313
+ });
314
+ const appended = appendChartSeries(draft, suggestion.expression);
315
+ setDraft(appended.spec);
316
+ setSelectedIndex(appended.spec.series.length - 1);
317
+ updateKeyboardFocus("series");
318
+ setExpression(formatSeriesExpression(appended.series));
319
+ clearQuickAddInput();
320
+ deactivateQuickAdd();
321
+ quickAddRef.current?.blur?.();
322
+ setError(null);
323
+ };
324
+
325
+ const submitQuickAdd = () => {
326
+ addCatalogSuggestion(quickAddSuggestions[clampIndex(quickAddSelection, quickAddSuggestions.length)]);
327
+ };
328
+
329
+ const leaveQuickAdd = () => {
330
+ deactivateQuickAdd();
331
+ quickAddRef.current?.blur?.();
332
+ setError(null);
333
+ };
334
+
335
+ const removeSeries = () => {
336
+ if (!selected || draft.series.length <= 1) return;
337
+ setDraft((current) => pruneSpec({
338
+ ...current,
339
+ series: current.series.filter((_, index) => index !== selectedIndex),
340
+ }));
341
+ setSelectedIndex((current) => clampIndex(current, draft.series.length - 1));
342
+ setError(null);
343
+ };
344
+
345
+ const moveSeries = (delta: -1 | 1) => {
346
+ if (!selected) return;
347
+ const target = selectedIndex + delta;
348
+ if (target < 0 || target >= draft.series.length) return;
349
+ setDraft((current) => ({ ...current, series: moveAt(current.series, selectedIndex, delta) }));
350
+ setSelectedIndex(target);
351
+ };
352
+
353
+ const beginExpressionEdit = () => {
354
+ if (!selected) return;
355
+ updateKeyboardFocus("source");
356
+ deactivateQuickAdd();
357
+ setEditingExpression(true);
358
+ queueMicrotask(() => expressionRef.current?.focus?.());
359
+ };
360
+
361
+ const setSelectedPanel = (panelId: string) => {
362
+ if (!selected || !draft.panels.some((panel) => panel.id === panelId)) return;
363
+ updateSelected((series) => ({ ...series, panelId }));
364
+ };
365
+
366
+ const addPanel = () => {
367
+ if (!selected) return;
368
+ const used = new Set(draft.panels.map((panel) => panel.id));
369
+ let index = 2;
370
+ while (used.has(`panel-${index}`)) index += 1;
371
+ const id = `panel-${index}`;
372
+ setDraft((current) => ({
373
+ ...current,
374
+ panels: [...current.panels, { id, label: `Panel ${index}`, height: 0.35, scale: "linear" }],
375
+ series: replaceAt(current.series, selectedIndex, { ...current.series[selectedIndex]!, panelId: id }),
376
+ }));
377
+ };
378
+
379
+ const cyclePanel = () => {
380
+ if (!selected || draft.panels.length === 0) return;
381
+ const index = draft.panels.findIndex((panel) => panel.id === selected.panelId);
382
+ setSelectedPanel(draft.panels[(index + 1) % draft.panels.length]?.id ?? "main");
383
+ };
384
+
385
+ const setSelectedPanelScale = (scale: PanelScale) => {
386
+ if (!selected) return;
387
+ setDraft((current) => ({
388
+ ...current,
389
+ panels: current.panels.map((panel) => panel.id === selected.panelId ? { ...panel, scale } : panel),
390
+ series: scale === "log"
391
+ ? current.series.map((series) => series.panelId === selected.panelId && series.transform === "log"
392
+ ? { ...series, transform: "raw" }
393
+ : series)
394
+ : current.series,
395
+ }));
396
+ };
397
+
398
+ const setSelectedTransform = (transform: SeriesTransform) => {
399
+ if (!selected) return;
400
+ setDraft((current) => ({
401
+ ...current,
402
+ panels: transform === "log"
403
+ ? current.panels.map((panel) => panel.id === selected.panelId ? { ...panel, scale: "linear" } : panel)
404
+ : current.panels,
405
+ series: replaceAt(current.series, selectedIndex, (() => {
406
+ const currentSeries = current.series[selectedIndex]!;
407
+ const ohlcStyle = currentSeries.style === "candles" || currentSeries.style === "ohlc" || currentSeries.style === "hlc";
408
+ return {
409
+ ...currentSeries,
410
+ style: transform !== "raw" && ohlcStyle
411
+ ? getCompatibleSeriesStyles(seriesFieldId(currentSeries)).find((style) => style === "line" || style === "area") ?? "line"
412
+ : currentSeries.style,
413
+ transform,
414
+ };
415
+ })()),
416
+ }));
417
+ };
418
+
419
+ const setSelectedStyle = (style: SeriesStyle) => {
420
+ updateSelected((series) => applySeriesStyle(series, style));
421
+ };
422
+
423
+ const setSelectedTimestampMode = (timestampMode: SeriesTimestampMode) => {
424
+ updateSelected((series) => applySeriesTimestampMode(series, timestampMode));
425
+ };
426
+
427
+ const setSelectedVisibility = (visible: boolean) => {
428
+ if (!selected) return;
429
+ setDraft((current) => {
430
+ const target = current.series[selectedIndex];
431
+ if (!target || (!visible && !canToggleChartSeries(current, target.id))) return current;
432
+ return {
433
+ ...current,
434
+ series: replaceAt(current.series, selectedIndex, { ...target, visible }),
435
+ };
436
+ });
437
+ };
438
+
439
+ const saveDraft = () => {
440
+ const next = pruneSpec(draft);
441
+ const validation = validateChartSpec(next);
442
+ if (!validation.valid) {
443
+ setError(validation.errors.map((issue) => issue.message).join(" "));
444
+ return;
445
+ }
446
+ resolve(next);
447
+ };
448
+
449
+ const toggleSelectedPanelScale = () => {
450
+ const panel = selected ? draft.panels.find((entry) => entry.id === selected.panelId) : null;
451
+ setSelectedPanelScale(panel?.scale === "log" ? "linear" : "log");
452
+ };
453
+
454
+ useDialogKeyboard((event) => {
455
+ if (isDesktop && (event.targetEditable === true || event.name === "tab")) {
456
+ if (event.name === "escape") {
457
+ event.stopPropagation();
458
+ event.preventDefault();
459
+ resolve(null);
460
+ }
461
+ return;
462
+ }
463
+
464
+ if ((isDesktop && quickAddActive) || (!isDesktop && keyboardFocusRef.current === "add")) {
465
+ const printableSequence = (
466
+ !event.ctrl
467
+ && !event.alt
468
+ && !event.meta
469
+ && !event.super
470
+ && event.sequence
471
+ && [...event.sequence].length === 1
472
+ && event.sequence >= " "
473
+ );
474
+ if (isPlainKey(event, "up")) {
475
+ event.stopPropagation();
476
+ event.preventDefault();
477
+ setQuickAddSelection((current) => clampIndex(current - 1, quickAddSuggestions.length));
478
+ } else if (isPlainKey(event, "down")) {
479
+ event.stopPropagation();
480
+ event.preventDefault();
481
+ setQuickAddSelection((current) => clampIndex(current + 1, quickAddSuggestions.length));
482
+ } else if (event.name === "enter" || event.name === "return") {
483
+ event.stopPropagation();
484
+ event.preventDefault();
485
+ submitQuickAdd();
486
+ } else if (event.name === "escape") {
487
+ event.stopPropagation();
488
+ event.preventDefault();
489
+ resolve(null);
490
+ } else if (event.name === "tab") {
491
+ event.stopPropagation();
492
+ event.preventDefault();
493
+ leaveQuickAdd();
494
+ moveKeyboardFocus(event.shift ? -1 : 1);
495
+ } else if (
496
+ event.targetEditable !== true
497
+ && printableSequence
498
+ ) {
499
+ event.stopPropagation();
500
+ event.preventDefault();
501
+ const nextQuery = `${quickAddRef.current?.editBuffer.getText() ?? quickAddQuery}${event.sequence}`;
502
+ quickAddRef.current?.editBuffer.setText?.(nextQuery);
503
+ quickAddRef.current?.setCursorOffset?.(nextQuery.length);
504
+ setQuickAddQuery(nextQuery);
505
+ activateQuickAdd();
506
+ quickAddRef.current?.focus?.();
507
+ }
508
+ return;
509
+ }
510
+
511
+ if ((isDesktop && editingExpression) || (!isDesktop && keyboardFocusRef.current === "source")) {
512
+ if (event.name === "escape") {
513
+ event.stopPropagation();
514
+ event.preventDefault();
515
+ resolve(null);
516
+ } else if (event.name === "enter" || event.name === "return") {
517
+ event.stopPropagation();
518
+ event.preventDefault();
519
+ if (commitExpression()) moveKeyboardFocus(1);
520
+ } else if (event.name === "tab") {
521
+ event.stopPropagation();
522
+ event.preventDefault();
523
+ if (commitExpression()) moveKeyboardFocus(event.shift ? -1 : 1);
524
+ }
525
+ return;
526
+ }
527
+
528
+ event.stopPropagation();
529
+ event.preventDefault();
530
+ if (event.name === "tab") {
531
+ moveKeyboardFocus(event.shift ? -1 : 1);
532
+ } else if (keyboardFocusRef.current === "series" && isPlainKey(event, "up", "k")) {
533
+ setSelectedIndex((current) => clampIndex(current - 1, draft.series.length));
534
+ } else if (keyboardFocusRef.current === "series" && isPlainKey(event, "down", "j")) {
535
+ setSelectedIndex((current) => clampIndex(current + 1, draft.series.length));
536
+ } else if (event.name === "[") {
537
+ moveSeries(-1);
538
+ } else if (event.name === "]") {
539
+ moveSeries(1);
540
+ } else if (event.name === "a") {
541
+ beginQuickAdd(true);
542
+ } else if (event.name === "d" || event.name === "delete") {
543
+ removeSeries();
544
+ } else if (event.name === "e") {
545
+ beginExpressionEdit();
546
+ } else if (event.name === "p") {
547
+ cyclePanel();
548
+ } else if (event.name === "n") {
549
+ addPanel();
550
+ } else if (event.name === "l") {
551
+ toggleSelectedPanelScale();
552
+ } else if (event.name === "enter" || event.name === "return") {
553
+ saveDraft();
554
+ } else if (event.name === "escape") {
555
+ resolve(null);
556
+ }
557
+ }, { scope: dialogId, allowEditable: true });
558
+
559
+ const items = useMemo<ListViewItem[]>(() => draft.series.map((series) => ({
560
+ id: series.id,
561
+ label: chartSeriesLabel(series),
562
+ description: [
563
+ titleCaseSeriesEditorValue(series.style),
564
+ timingDescription(series),
565
+ titleCaseSeriesEditorValue(series.transform),
566
+ `${titleCaseSeriesEditorValue(series.axis)} axis`,
567
+ series.panelId,
568
+ ].filter(Boolean).join(" · "),
569
+ })), [draft.series]);
570
+ const quickAddItems = useMemo<ListViewItem[]>(() => quickAddSuggestions.map((suggestion) => ({
571
+ id: suggestion.id,
572
+ label: suggestion.label,
573
+ description: suggestion.description,
574
+ detail: suggestion.detail,
575
+ })), [quickAddSuggestions]);
576
+ const fields = buildSeriesEditorFields({
577
+ draft,
578
+ selected,
579
+ handlers: {
580
+ setStyle: setSelectedStyle,
581
+ setTransform: setSelectedTransform,
582
+ setAxis: (axis) => updateSelected((series) => ({ ...series, axis })),
583
+ setVisibility: setSelectedVisibility,
584
+ setPanel: setSelectedPanel,
585
+ addPanel,
586
+ setScale: setSelectedPanelScale,
587
+ setPeriod: (period) => updateSelected((series) => series.source.kind === "security" ? ({
588
+ ...series,
589
+ source: { ...series.source, period },
590
+ }) : series),
591
+ setTimestampMode: setSelectedTimestampMode,
592
+ },
593
+ });
594
+ const actions = buildSeriesEditorActions({
595
+ draft,
596
+ selected,
597
+ selectedIndex,
598
+ handlers: {
599
+ add: () => beginQuickAdd(true),
600
+ remove: removeSeries,
601
+ moveUp: () => moveSeries(-1),
602
+ moveDown: () => moveSeries(1),
603
+ cancel: () => resolve(null),
604
+ save: saveDraft,
605
+ },
606
+ });
607
+
608
+ return {
609
+ actions,
610
+ activateQuickAdd,
611
+ addCatalogSuggestion,
612
+ beginExpressionEdit,
613
+ beginQuickAdd,
614
+ commitExpression,
615
+ deactivateQuickAdd,
616
+ defaultInstrument,
617
+ editingExpression,
618
+ error,
619
+ expression,
620
+ expressionRef,
621
+ fields,
622
+ focusKeyboardTarget,
623
+ items,
624
+ keyboardFocus,
625
+ moveKeyboardFocus,
626
+ quickAddActive,
627
+ quickAddItems,
628
+ quickAddLoading,
629
+ quickAddQuery,
630
+ quickAddRef,
631
+ quickAddSelection,
632
+ quickAddSuggestions,
633
+ selected,
634
+ selectedIndex,
635
+ setEditingExpression,
636
+ setExpression,
637
+ setQuickAddQuery,
638
+ setQuickAddSelection,
639
+ setSelectedIndex,
640
+ submitQuickAdd,
641
+ updateKeyboardFocus,
642
+ };
643
+ }