gloomberb 0.10.1 → 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 (351) 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/remote/asset-data-client.ts +6 -44
  274. package/src/renderers/electrobun/view/remote/persistence.ts +6 -2
  275. package/src/renderers/electrobun/view/remote/quote-subscription-registry.test.ts +86 -0
  276. package/src/renderers/electrobun/view/remote/quote-subscription-registry.ts +72 -0
  277. package/src/renderers/electrobun/view/remote/ticker-repository.ts +2 -2
  278. package/src/renderers/electrobun/view/toast-host.tsx +0 -1
  279. package/src/renderers/electrobun/view/ui-host.tsx +1 -2
  280. package/src/renderers/electrobun/view/update-host.ts +1 -1
  281. package/src/renderers/opentui/chart-surface.tsx +2 -1
  282. package/src/renderers/opentui/dialog-host.tsx +20 -4
  283. package/src/renderers/opentui/image/loader.ts +12 -2
  284. package/src/renderers/opentui/input-host.tsx +7 -49
  285. package/src/renderers/opentui/start.tsx +2 -0
  286. package/src/sources/gloomberb-cloud/history-quality.ts +44 -0
  287. package/src/sources/gloomberb-cloud/index.test.ts +173 -0
  288. package/src/sources/gloomberb-cloud/index.ts +42 -20
  289. package/src/sources/gloomberb-cloud/normalizers.ts +9 -3
  290. package/src/sources/provider-router/cache.ts +1 -1
  291. package/src/sources/provider-router/document-routes.ts +10 -5
  292. package/src/sources/provider-router/financials.test.ts +14 -0
  293. package/src/sources/provider-router/financials.ts +6 -1
  294. package/src/sources/provider-router/history.test.ts +163 -1
  295. package/src/sources/provider-router/history.ts +278 -347
  296. package/src/sources/provider-router/index.ts +2 -2
  297. package/src/sources/provider-router/routing.test.ts +39 -0
  298. package/src/sources/provider-router/routing.ts +24 -11
  299. package/src/sources/provider-router/supplemental.ts +37 -17
  300. package/src/sources/sec-edgar/content.ts +2 -12
  301. package/src/sources/sec-edgar.ts +2 -13
  302. package/src/sources/yahoo-finance/history.ts +2 -2
  303. package/src/sources/yahoo-finance/options.ts +5 -0
  304. package/src/sources/yahoo-finance.ts +2 -2
  305. package/src/state/app/bootstrap.ts +2 -2
  306. package/src/state/app/context/index.tsx +2 -2
  307. package/src/state/hooks/quote-streaming.ts +66 -1
  308. package/src/state/hooks/session-snapshot.ts +2 -2
  309. package/src/state/selectors.ts +0 -9
  310. package/src/sync/controller.test.ts +98 -0
  311. package/src/sync/controller.ts +29 -5
  312. package/src/sync/react.ts +2 -2
  313. package/src/sync/types.ts +2 -2
  314. package/src/theme/colors.test.ts +10 -0
  315. package/src/theme/colors.ts +75 -78
  316. package/src/theme/theme-context.test.tsx +67 -0
  317. package/src/theme/theme-context.tsx +9 -5
  318. package/src/tickers/open-target.ts +2 -2
  319. package/src/tickers/search/upsert.ts +2 -2
  320. package/src/time-series/alignment.ts +0 -7
  321. package/src/time-series/chart-data.ts +145 -0
  322. package/src/time-series/date-window.ts +44 -0
  323. package/src/time-series/history-window.ts +21 -0
  324. package/src/time-series/hooks.test.ts +59 -0
  325. package/src/time-series/hooks.ts +33 -12
  326. package/src/time-series/market-resolution.ts +2 -2
  327. package/src/time-series/market.ts +1 -1
  328. package/src/time-series/range.ts +9 -0
  329. package/src/{components/chart/core → time-series}/resolution.ts +3 -102
  330. package/src/time-series/resolve.test.ts +203 -1
  331. package/src/time-series/resolve.ts +96 -18
  332. package/src/time-series/spec-studies.test.ts +15 -0
  333. package/src/time-series/spec.ts +2 -2
  334. package/src/time-series/studies.ts +5 -2
  335. package/src/time-series/types.ts +3 -1
  336. package/src/time-series/use-chart-resolution.test.tsx +358 -0
  337. package/src/time-series/use-chart-resolution.ts +46 -4
  338. package/src/types/broker.ts +2 -2
  339. package/src/types/data-provider.ts +3 -3
  340. package/src/types/financials.ts +12 -0
  341. package/src/types/plugin.ts +2 -2
  342. package/src/ui/context-menu.tsx +3 -2
  343. package/src/ui/dialog.tsx +2 -2
  344. package/src/utils/html-entities.ts +3 -5
  345. package/src/utils/main-thread-monitor.ts +1 -1
  346. package/src/utils/price-history.ts +5 -5
  347. package/src/version.ts +3 -1
  348. package/src/components/chart/core/viewport.test.ts +0 -8
  349. package/src/components/chart/core/viewport.ts +0 -24
  350. package/src/time-series/index.ts +0 -14
  351. /package/src/{components/chart/core → time-series}/resolution.test.ts +0 -0
@@ -1,185 +1,18 @@
1
- import { type ReactNode, useEffect, useMemo, useRef, useState } from "react";
2
- import { Box, Text, type InputRenderable, useNativeRenderer, useUiHost } from "../../../ui";
3
- import { SegmentedControl } from "../../../components";
4
- import {
5
- Button,
6
- Checkbox,
7
- DialogFrame,
8
- ListView,
9
- TextField,
10
- type ListViewItem,
11
- } from "../../../components/ui";
12
- import { NativeSelect } from "../../../components/ui/native-select";
13
- import { type PromptContext, useDialogKeyboard } from "../../../ui/dialog";
1
+ import { Box, Text, useNativeRenderer, useUiHost } from "../../../ui";
2
+ import { DialogFrame, ListView, TextField } from "../../../components/ui";
3
+ import type { PromptContext } from "../../../ui/dialog";
14
4
  import { colors } from "../../../theme/colors";
15
- import type {
16
- ChartSeriesSpec,
17
- ChartSpec,
18
- SeriesAxis,
19
- PanelScale,
20
- SeriesPeriod,
21
- SeriesStyle,
22
- SeriesTimestampMode,
23
- SeriesTransform,
24
- } from "../../../time-series/types";
25
- import { isFundamentalFieldId } from "../../../time-series/field-catalog";
26
- import { validateChartSpec } from "../../../time-series/spec";
27
- import { isPlainKey } from "../../../utils/keyboard";
28
- import { getSharedRegistry } from "../../registry";
5
+ import type { ChartSpec } from "../../../time-series/types";
6
+ import { useSeriesEditorController } from "./editor/controller";
29
7
  import {
30
- canToggleChartSeries,
31
- MAX_CHART_COMPOSER_SERIES,
32
- parseChartSpecOr,
33
- } from "./chart-spec";
8
+ DesktopSeriesEditorActions,
9
+ DesktopSeriesEditorFields,
10
+ } from "./editor/desktop-view";
34
11
  import {
35
- appendChartSeries,
36
- buildEmptyChartPreset,
37
- applySeriesStyle,
38
- applySeriesTimestampMode,
39
- buildSeriesSpec,
40
- chartSeriesLabel,
41
- defaultFinancialTimestampMode,
42
- formatSeriesExpression,
43
- getCompatibleSeriesStyles,
44
- getCompatibleSeriesTransforms,
45
- getSelectedBuiltinStudies,
46
- getSelectedPairStudies,
47
- parseSeriesExpression,
48
- setBuiltinStudies,
49
- setPairStudies,
50
- } from "./presets";
51
- import type { SeriesCatalogInstrument, SeriesCatalogSuggestion } from "./series-catalog";
52
- import { useSeriesCatalogSuggestions } from "./use-series-catalog";
53
-
54
- const AXES: SeriesAxis[] = ["auto", "left", "right"];
55
- const MARKET_PERIODS: SeriesPeriod[] = ["auto", "daily", "weekly", "monthly", "quarterly", "annual"];
56
- const FINANCIAL_PERIODS: SeriesPeriod[] = ["auto", "quarterly", "annual", "ttm"];
57
- const TIMING_OPTIONS: Array<{ value: SeriesTimestampMode; label: string }> = [
58
- { value: "available-at", label: "Available Date" },
59
- { value: "period-end", label: "Period End" },
60
- ];
61
- type SeriesEditorField =
62
- | "style"
63
- | "transform"
64
- | "axis"
65
- | "visibility"
66
- | "panel"
67
- | "scale"
68
- | "period"
69
- | "timing";
70
- type SeriesEditorFocus = "add" | "series" | "source" | SeriesEditorField;
71
-
72
- function titleCase(value: string): string {
73
- return value
74
- .replaceAll("-", " ")
75
- .replace(/\b\w/g, (character) => character.toUpperCase());
76
- }
77
-
78
- function clampIndex(value: number, length: number): number {
79
- if (length <= 0) return -1;
80
- return Math.max(0, Math.min(value, length - 1));
81
- }
82
-
83
- function seriesFieldId(series: ChartSeriesSpec): string {
84
- return series.source.kind === "security" ? series.source.fieldId : series.source.seriesId;
85
- }
86
-
87
- function compatiblePeriods(series: ChartSeriesSpec | null): SeriesPeriod[] {
88
- if (!series || series.source.kind !== "security") return [];
89
- return series.source.fieldId.startsWith("market.") ? MARKET_PERIODS : FINANCIAL_PERIODS;
90
- }
91
-
92
- function supportsTimestampMode(series: ChartSeriesSpec | null): boolean {
93
- return !!series
94
- && series.source.kind === "security"
95
- && isFundamentalFieldId(series.source.fieldId);
96
- }
97
-
98
- function seriesTimestampMode(series: ChartSeriesSpec): SeriesTimestampMode {
99
- return series.source.kind === "security"
100
- ? series.source.timestampMode
101
- ?? defaultFinancialTimestampMode(series.source.fieldId)
102
- ?? "available-at"
103
- : "available-at";
104
- }
105
-
106
- function timingDescription(series: ChartSeriesSpec): string | null {
107
- if (!supportsTimestampMode(series)) return null;
108
- return seriesTimestampMode(series) === "available-at" ? "Available date" : "Period end";
109
- }
110
-
111
- function DesktopEditorField({
112
- label,
113
- children,
114
- width = "calc(50% - 6px)",
115
- }: {
116
- label: string;
117
- children: ReactNode;
118
- width?: string;
119
- }) {
120
- return (
121
- <Box flexDirection="column" width={width} minWidth="220px" style={{ gap: 4 }}>
122
- <Text fg={colors.textDim} style={{ fontWeight: 600 }}>{label}</Text>
123
- {children}
124
- </Box>
125
- );
126
- }
127
-
128
- function TuiEditorField({
129
- label,
130
- focused,
131
- onFocus,
132
- children,
133
- }: {
134
- label: string;
135
- focused: boolean;
136
- onFocus: () => void;
137
- children: ReactNode;
138
- }) {
139
- return (
140
- <Box
141
- flexDirection="column"
142
- width="100%"
143
- overflow="hidden"
144
- onMouseDown={onFocus}
145
- >
146
- <Text fg={focused ? colors.textBright : colors.textDim}>
147
- {focused ? `› ${label}` : label}
148
- </Text>
149
- {children}
150
- </Box>
151
- );
152
- }
153
-
154
- function pruneSpec(spec: ChartSpec): ChartSpec {
155
- const selectedBuiltinStudies = getSelectedBuiltinStudies(spec);
156
- const selectedPairStudies = getSelectedPairStudies(spec);
157
- const rebound = setPairStudies(
158
- setBuiltinStudies(spec, selectedBuiltinStudies),
159
- selectedPairStudies,
160
- );
161
- const seriesIds = new Set(rebound.series.map((series) => series.id));
162
- const studies = rebound.studies.filter((study) => {
163
- const requiredInputs = study.kind === "ratio" || study.kind === "spread" || study.kind === "correlation" ? 2 : 1;
164
- return study.inputSeriesIds.length === requiredInputs
165
- && study.inputSeriesIds.every((id) => seriesIds.has(id));
166
- });
167
- const panels = [...rebound.panels];
168
- if (!panels.some((panel) => panel.id === "main")) panels.unshift({ id: "main" });
169
- return { ...rebound, panels, studies };
170
- }
171
-
172
- function replaceAt<T>(values: readonly T[], index: number, value: T): T[] {
173
- return values.map((entry, entryIndex) => entryIndex === index ? value : entry);
174
- }
175
-
176
- function moveAt<T>(values: readonly T[], index: number, delta: -1 | 1): T[] {
177
- const target = index + delta;
178
- if (index < 0 || target < 0 || target >= values.length) return [...values];
179
- const next = [...values];
180
- [next[index], next[target]] = [next[target]!, next[index]!];
181
- return next;
182
- }
12
+ OpenTuiSeriesEditorActions,
13
+ OpenTuiSeriesEditorFields,
14
+ } from "./editor/opentui-view";
15
+ import type { SeriesEditorFocus } from "./editor/model";
183
16
 
184
17
  export interface SeriesEditorDialogProps extends PromptContext<ChartSpec | null> {
185
18
  initialSpec: ChartSpec;
@@ -188,492 +21,47 @@ export interface SeriesEditorDialogProps extends PromptContext<ChartSpec | null>
188
21
  export function SeriesEditorDialog({ dialogId, resolve, initialSpec }: SeriesEditorDialogProps) {
189
22
  const isDesktop = useUiHost().kind === "desktop-web";
190
23
  const nativeRenderer = useNativeRenderer();
191
- const [draft, setDraft] = useState(() => parseChartSpecOr(initialSpec, buildEmptyChartPreset()));
192
- const [selectedIndex, setSelectedIndex] = useState(() => clampIndex(0, initialSpec.series.length));
193
- const [expression, setExpression] = useState(() => initialSpec.series[0] ? formatSeriesExpression(initialSpec.series[0]) : "");
194
- const [editingExpression, setEditingExpression] = useState(false);
195
- const [quickAddActive, setQuickAddActive] = useState(true);
196
- const [quickAddQuery, setQuickAddQuery] = useState("");
197
- const [quickAddSelection, setQuickAddSelection] = useState(0);
198
- const [keyboardFocus, setKeyboardFocus] = useState<SeriesEditorFocus>("add");
199
- const [error, setError] = useState<string | null>(null);
200
- const expressionRef = useRef<InputRenderable | null>(null);
201
- const quickAddRef = useRef<InputRenderable | null>(null);
202
- const keyboardFocusRef = useRef<SeriesEditorFocus>("add");
203
- const catalogCommitLockRef = useRef(false);
204
- const expressionCommitLockRef = useRef(false);
205
- const selected = selectedIndex >= 0 ? draft.series[selectedIndex] ?? null : null;
206
- const keyboardFields = useMemo<SeriesEditorField[]>(() => {
207
- if (!selected) return [];
208
- return [
209
- "style",
210
- "transform",
211
- "axis",
212
- "visibility",
213
- "panel",
214
- "scale",
215
- ...(selected.source.kind === "security" ? ["period" as const] : []),
216
- ...(supportsTimestampMode(selected) ? ["timing" as const] : []),
217
- ];
218
- }, [selected]);
219
- const keyboardTargets = useMemo<SeriesEditorFocus[]>(() => [
220
- "add",
221
- ...(selected ? ["series" as const, "source" as const, ...keyboardFields] : []),
222
- ], [keyboardFields, selected]);
223
-
224
- const activateQuickAdd = () => {
225
- setQuickAddActive(true);
226
- };
227
-
228
- const deactivateQuickAdd = () => {
229
- setQuickAddActive(false);
230
- };
231
-
232
- const updateKeyboardFocus = (target: SeriesEditorFocus) => {
233
- keyboardFocusRef.current = target;
234
- setKeyboardFocus(target);
235
- };
236
-
237
- const focusKeyboardTarget = (target: SeriesEditorFocus) => {
238
- if (!keyboardTargets.includes(target)) return;
239
- updateKeyboardFocus(target);
240
- if (target === "add") {
241
- setEditingExpression(false);
242
- expressionRef.current?.blur?.();
243
- activateQuickAdd();
244
- queueMicrotask(() => quickAddRef.current?.focus?.());
245
- return;
246
- }
247
-
248
- deactivateQuickAdd();
249
- quickAddRef.current?.blur?.();
250
- if (target === "source") {
251
- setEditingExpression(true);
252
- queueMicrotask(() => expressionRef.current?.focus?.());
253
- } else {
254
- setEditingExpression(false);
255
- expressionRef.current?.blur?.();
256
- }
257
- };
258
-
259
- const moveKeyboardFocus = (direction: -1 | 1) => {
260
- const currentIndex = Math.max(0, keyboardTargets.indexOf(keyboardFocusRef.current));
261
- const nextIndex = (currentIndex + direction + keyboardTargets.length) % keyboardTargets.length;
262
- const next = keyboardTargets[nextIndex];
263
- if (next) focusKeyboardTarget(next);
264
- };
265
-
266
- useEffect(() => {
267
- const next = selected ? formatSeriesExpression(selected) : "";
268
- setExpression(next);
269
- setError(null);
270
- setEditingExpression(false);
271
- }, [selected?.id]);
272
-
273
- const defaultInstrument = useMemo<SeriesCatalogInstrument>(() => {
274
- const firstSecurity = draft.series.find((series) => series.source.kind === "security");
275
- const security = selected?.source.kind === "security"
276
- ? selected.source.instrument
277
- : firstSecurity?.source.kind === "security"
278
- ? firstSecurity.source.instrument
279
- : undefined;
280
- const symbol = security?.symbol ?? "AAPL";
281
- const saved = getSharedRegistry()?.getTickerFn(symbol);
282
- return {
283
- symbol,
284
- ...(security?.exchange ? { exchange: security.exchange } : saved?.metadata.exchange ? { exchange: saved.metadata.exchange } : {}),
285
- ...(saved?.metadata.name ? { name: saved.metadata.name } : {}),
286
- };
287
- }, [draft.series, selected]);
24
+ const controller = useSeriesEditorController({
25
+ dialogId,
26
+ resolve,
27
+ initialSpec,
28
+ isDesktop,
29
+ });
288
30
  const {
289
- suggestions: quickAddSuggestions,
290
- loading: quickAddLoading,
291
- } = useSeriesCatalogSuggestions({
292
- query: quickAddQuery,
31
+ actions,
32
+ activateQuickAdd,
33
+ addCatalogSuggestion,
34
+ beginExpressionEdit,
35
+ beginQuickAdd,
36
+ commitExpression,
37
+ deactivateQuickAdd,
293
38
  defaultInstrument,
294
- enabled: quickAddActive,
295
- });
296
-
297
- useEffect(() => {
298
- setQuickAddSelection(0);
299
- }, [quickAddQuery, quickAddSuggestions.length]);
300
-
301
- useEffect(() => {
302
- if (!keyboardTargets.includes(keyboardFocus)) updateKeyboardFocus("add");
303
- }, [keyboardFocus, keyboardTargets]);
304
-
305
- const updateSelected = (update: (series: ChartSeriesSpec) => ChartSeriesSpec) => {
306
- if (!selected) return;
307
- setDraft((current) => ({
308
- ...current,
309
- series: replaceAt(current.series, selectedIndex, update(current.series[selectedIndex]!)),
310
- }));
311
- };
312
-
313
- const commitExpression = (): boolean => {
314
- if (expressionCommitLockRef.current) return true;
315
- if (!selected) return false;
316
- const parsed = parseSeriesExpression(expression);
317
- if (!parsed) {
318
- setError("Use SYMBOL, SYMBOL:field, SYMBOL:EXCHANGE:field, or FRED:series.");
319
- return false;
320
- }
321
- expressionCommitLockRef.current = true;
322
- queueMicrotask(() => {
323
- expressionCommitLockRef.current = false;
324
- });
325
-
326
- const candidate = buildSeriesSpec(parsed, selectedIndex);
327
- const previousFieldId = seriesFieldId(selected);
328
- const nextFieldId = seriesFieldId(candidate);
329
- const styles = getCompatibleSeriesStyles(nextFieldId);
330
- const transforms = getCompatibleSeriesTransforms(nextFieldId);
331
- const source = candidate.source.kind === "security" && selected.source.kind === "security"
332
- ? {
333
- ...candidate.source,
334
- ...(previousFieldId === nextFieldId
335
- ? { period: selected.source.period }
336
- : {}),
337
- ...(supportsTimestampMode(candidate) && supportsTimestampMode(selected)
338
- ? { timestampMode: seriesTimestampMode(selected) }
339
- : {}),
340
- instrument: candidate.source.instrument.symbol === selected.source.instrument.symbol
341
- && (candidate.source.instrument.exchange ?? "") === (selected.source.instrument.exchange ?? "")
342
- ? selected.source.instrument
343
- : candidate.source.instrument,
344
- }
345
- : candidate.source;
346
- const style = previousFieldId === nextFieldId || styles.includes(selected.style)
347
- ? selected.style
348
- : candidate.style;
349
- const next = applySeriesStyle({
350
- ...candidate,
351
- id: selected.id,
352
- source,
353
- ...(selected.label ? { label: selected.label } : {}),
354
- ...(selected.color ? { color: selected.color } : {}),
355
- ...(selected.visible !== undefined ? { visible: selected.visible } : {}),
356
- style,
357
- transform: previousFieldId === nextFieldId || transforms.includes(selected.transform)
358
- ? selected.transform
359
- : candidate.transform,
360
- axis: selected.axis,
361
- panelId: selected.panelId,
362
- }, style);
363
- setDraft((current) => ({ ...current, series: replaceAt(current.series, selectedIndex, next) }));
364
- setExpression(formatSeriesExpression(next));
365
- setEditingExpression(false);
366
- expressionRef.current?.blur?.();
367
- setError(null);
368
- return true;
369
- };
370
-
371
- const clearQuickAddInput = () => {
372
- setQuickAddQuery("");
373
- quickAddRef.current?.editBuffer.setText?.("");
374
- quickAddRef.current?.setCursorOffset?.(0);
375
- };
376
-
377
- const beginQuickAdd = (reset = false) => {
378
- if (draft.series.length >= MAX_CHART_COMPOSER_SERIES) {
379
- setError(`Charts support up to ${MAX_CHART_COMPOSER_SERIES} base series.`);
380
- return;
381
- }
382
- if (reset) clearQuickAddInput();
383
- updateKeyboardFocus("add");
384
- setEditingExpression(false);
385
- activateQuickAdd();
386
- setError(null);
387
- quickAddRef.current?.focus?.();
388
- if (reset) {
389
- queueMicrotask(() => {
390
- clearQuickAddInput();
391
- quickAddRef.current?.focus?.();
392
- });
393
- }
394
- };
395
-
396
- const addCatalogSuggestion = (suggestion: SeriesCatalogSuggestion | undefined) => {
397
- if (!suggestion || catalogCommitLockRef.current) return;
398
- if (draft.series.length >= MAX_CHART_COMPOSER_SERIES) {
399
- setError(`Charts support up to ${MAX_CHART_COMPOSER_SERIES} base series.`);
400
- return;
401
- }
402
- catalogCommitLockRef.current = true;
403
- queueMicrotask(() => {
404
- catalogCommitLockRef.current = false;
405
- });
406
- const appended = appendChartSeries(draft, suggestion.expression);
407
- setDraft(appended.spec);
408
- setSelectedIndex(appended.spec.series.length - 1);
409
- updateKeyboardFocus("series");
410
- setExpression(formatSeriesExpression(appended.series));
411
- clearQuickAddInput();
412
- deactivateQuickAdd();
413
- quickAddRef.current?.blur?.();
414
- setError(null);
415
- };
416
-
417
- const submitQuickAdd = () => {
418
- addCatalogSuggestion(quickAddSuggestions[clampIndex(quickAddSelection, quickAddSuggestions.length)]);
419
- };
420
-
421
- const leaveQuickAdd = () => {
422
- deactivateQuickAdd();
423
- quickAddRef.current?.blur?.();
424
- setError(null);
425
- };
426
-
427
- const removeSeries = () => {
428
- if (!selected || draft.series.length <= 1) return;
429
- setDraft((current) => pruneSpec({
430
- ...current,
431
- series: current.series.filter((_, index) => index !== selectedIndex),
432
- }));
433
- setSelectedIndex((current) => clampIndex(current, draft.series.length - 1));
434
- setError(null);
435
- };
436
-
437
- const moveSeries = (delta: -1 | 1) => {
438
- if (!selected) return;
439
- const target = selectedIndex + delta;
440
- if (target < 0 || target >= draft.series.length) return;
441
- setDraft((current) => ({ ...current, series: moveAt(current.series, selectedIndex, delta) }));
442
- setSelectedIndex(target);
443
- };
444
-
445
- const beginExpressionEdit = () => {
446
- if (!selected) return;
447
- updateKeyboardFocus("source");
448
- deactivateQuickAdd();
449
- setEditingExpression(true);
450
- queueMicrotask(() => expressionRef.current?.focus?.());
451
- };
452
-
453
- const setSelectedPanel = (panelId: string) => {
454
- if (!selected || !draft.panels.some((panel) => panel.id === panelId)) return;
455
- updateSelected((series) => ({ ...series, panelId }));
456
- };
457
-
458
- const addPanel = () => {
459
- if (!selected) return;
460
- const used = new Set(draft.panels.map((panel) => panel.id));
461
- let index = 2;
462
- while (used.has(`panel-${index}`)) index += 1;
463
- const id = `panel-${index}`;
464
- setDraft((current) => ({
465
- ...current,
466
- panels: [...current.panels, { id, label: `Panel ${index}`, height: 0.35, scale: "linear" }],
467
- series: replaceAt(current.series, selectedIndex, { ...current.series[selectedIndex]!, panelId: id }),
468
- }));
469
- };
470
-
471
- const cyclePanel = () => {
472
- if (!selected || draft.panels.length === 0) return;
473
- const index = draft.panels.findIndex((panel) => panel.id === selected.panelId);
474
- setSelectedPanel(draft.panels[(index + 1) % draft.panels.length]?.id ?? "main");
475
- };
476
-
477
- const setSelectedPanelScale = (scale: PanelScale) => {
478
- if (!selected) return;
479
- setDraft((current) => ({
480
- ...current,
481
- panels: current.panels.map((panel) => panel.id === selected.panelId ? { ...panel, scale } : panel),
482
- series: scale === "log"
483
- ? current.series.map((series) => series.panelId === selected.panelId && series.transform === "log"
484
- ? { ...series, transform: "raw" }
485
- : series)
486
- : current.series,
487
- }));
488
- };
489
-
490
- const setSelectedTransform = (transform: SeriesTransform) => {
491
- if (!selected) return;
492
- setDraft((current) => ({
493
- ...current,
494
- panels: transform === "log"
495
- ? current.panels.map((panel) => panel.id === selected.panelId ? { ...panel, scale: "linear" } : panel)
496
- : current.panels,
497
- series: replaceAt(current.series, selectedIndex, (() => {
498
- const currentSeries = current.series[selectedIndex]!;
499
- const ohlcStyle = currentSeries.style === "candles" || currentSeries.style === "ohlc" || currentSeries.style === "hlc";
500
- return {
501
- ...currentSeries,
502
- style: transform !== "raw" && ohlcStyle
503
- ? getCompatibleSeriesStyles(seriesFieldId(currentSeries)).find((style) => style === "line" || style === "area") ?? "line"
504
- : currentSeries.style,
505
- transform,
506
- };
507
- })()),
508
- }));
509
- };
510
-
511
- const setSelectedStyle = (style: SeriesStyle) => {
512
- updateSelected((series) => applySeriesStyle(series, style));
513
- };
514
-
515
- const setSelectedTimestampMode = (timestampMode: SeriesTimestampMode) => {
516
- updateSelected((series) => applySeriesTimestampMode(series, timestampMode));
517
- };
518
-
519
- const setSelectedVisibility = (visible: boolean) => {
520
- if (!selected) return;
521
- setDraft((current) => {
522
- const target = current.series[selectedIndex];
523
- if (!target || (!visible && !canToggleChartSeries(current, target.id))) return current;
524
- return {
525
- ...current,
526
- series: replaceAt(current.series, selectedIndex, { ...target, visible }),
527
- };
528
- });
529
- };
530
-
531
- const saveDraft = () => {
532
- const next = pruneSpec(draft);
533
- const validation = validateChartSpec(next);
534
- if (!validation.valid) {
535
- setError(validation.errors.map((issue) => issue.message).join(" "));
536
- return;
537
- }
538
- resolve(next);
539
- };
540
-
541
- const toggleSelectedPanelScale = () => {
542
- const panel = selected ? draft.panels.find((entry) => entry.id === selected.panelId) : null;
543
- setSelectedPanelScale(panel?.scale === "log" ? "linear" : "log");
544
- };
545
-
546
- useDialogKeyboard((event) => {
547
- if (isDesktop && (event.targetEditable === true || event.name === "tab")) {
548
- if (event.name === "escape") {
549
- event.stopPropagation();
550
- event.preventDefault();
551
- resolve(null);
552
- }
553
- return;
554
- }
555
-
556
- if ((isDesktop && quickAddActive) || (!isDesktop && keyboardFocusRef.current === "add")) {
557
- const printableSequence = (
558
- !event.ctrl
559
- && !event.alt
560
- && !event.meta
561
- && !event.super
562
- && event.sequence
563
- && [...event.sequence].length === 1
564
- && event.sequence >= " "
565
- );
566
- if (isPlainKey(event, "up")) {
567
- event.stopPropagation();
568
- event.preventDefault();
569
- setQuickAddSelection((current) => clampIndex(current - 1, quickAddSuggestions.length));
570
- } else if (isPlainKey(event, "down")) {
571
- event.stopPropagation();
572
- event.preventDefault();
573
- setQuickAddSelection((current) => clampIndex(current + 1, quickAddSuggestions.length));
574
- } else if (event.name === "enter" || event.name === "return") {
575
- event.stopPropagation();
576
- event.preventDefault();
577
- submitQuickAdd();
578
- } else if (event.name === "escape") {
579
- event.stopPropagation();
580
- event.preventDefault();
581
- resolve(null);
582
- } else if (event.name === "tab") {
583
- event.stopPropagation();
584
- event.preventDefault();
585
- leaveQuickAdd();
586
- moveKeyboardFocus(event.shift ? -1 : 1);
587
- } else if (
588
- event.targetEditable !== true
589
- && printableSequence
590
- ) {
591
- event.stopPropagation();
592
- event.preventDefault();
593
- const nextQuery = `${quickAddRef.current?.editBuffer.getText() ?? quickAddQuery}${event.sequence}`;
594
- quickAddRef.current?.editBuffer.setText?.(nextQuery);
595
- quickAddRef.current?.setCursorOffset?.(nextQuery.length);
596
- setQuickAddQuery(nextQuery);
597
- activateQuickAdd();
598
- quickAddRef.current?.focus?.();
599
- }
600
- return;
601
- }
602
-
603
- if ((isDesktop && editingExpression) || (!isDesktop && keyboardFocusRef.current === "source")) {
604
- if (event.name === "escape") {
605
- event.stopPropagation();
606
- event.preventDefault();
607
- resolve(null);
608
- } else if (event.name === "enter" || event.name === "return") {
609
- event.stopPropagation();
610
- event.preventDefault();
611
- if (commitExpression()) moveKeyboardFocus(1);
612
- } else if (event.name === "tab") {
613
- event.stopPropagation();
614
- event.preventDefault();
615
- if (commitExpression()) moveKeyboardFocus(event.shift ? -1 : 1);
616
- }
617
- return;
618
- }
619
-
620
- event.stopPropagation();
621
- event.preventDefault();
622
- if (event.name === "tab") {
623
- moveKeyboardFocus(event.shift ? -1 : 1);
624
- } else if (keyboardFocusRef.current === "series" && isPlainKey(event, "up", "k")) {
625
- setSelectedIndex((current) => clampIndex(current - 1, draft.series.length));
626
- } else if (keyboardFocusRef.current === "series" && isPlainKey(event, "down", "j")) {
627
- setSelectedIndex((current) => clampIndex(current + 1, draft.series.length));
628
- } else if (event.name === "[") {
629
- moveSeries(-1);
630
- } else if (event.name === "]") {
631
- moveSeries(1);
632
- } else if (event.name === "a") {
633
- beginQuickAdd(true);
634
- } else if (event.name === "d" || event.name === "delete") {
635
- removeSeries();
636
- } else if (event.name === "e") {
637
- beginExpressionEdit();
638
- } else if (event.name === "p") {
639
- cyclePanel();
640
- } else if (event.name === "n") {
641
- addPanel();
642
- } else if (event.name === "l") {
643
- toggleSelectedPanelScale();
644
- } else if (event.name === "enter" || event.name === "return") {
645
- saveDraft();
646
- } else if (event.name === "escape") {
647
- resolve(null);
648
- }
649
- }, { scope: dialogId, allowEditable: true });
650
-
651
- const items = useMemo<ListViewItem[]>(() => draft.series.map((series) => ({
652
- id: series.id,
653
- label: chartSeriesLabel(series),
654
- description: [
655
- titleCase(series.style),
656
- timingDescription(series),
657
- titleCase(series.transform),
658
- `${titleCase(series.axis)} axis`,
659
- series.panelId,
660
- ].filter(Boolean).join(" · "),
661
- })), [draft.series]);
662
- const quickAddItems = useMemo<ListViewItem[]>(() => quickAddSuggestions.map((suggestion) => ({
663
- id: suggestion.id,
664
- label: suggestion.label,
665
- description: suggestion.description,
666
- detail: suggestion.detail,
667
- })), [quickAddSuggestions]);
668
- const fieldId = selected ? seriesFieldId(selected) : "";
669
- const styleOptions = selected
670
- ? getCompatibleSeriesStyles(fieldId).map((value) => ({ value, label: titleCase(value) }))
671
- : [];
672
- const transformOptions = selected
673
- ? getCompatibleSeriesTransforms(fieldId).map((value) => ({ value, label: value === "index100" ? "Index 100" : value.toUpperCase() }))
674
- : [];
675
- const selectedPanel = selected ? draft.panels.find((panel) => panel.id === selected.panelId) : null;
676
- const periodOptions = compatiblePeriods(selected);
39
+ editingExpression,
40
+ error,
41
+ expression,
42
+ expressionRef,
43
+ fields,
44
+ focusKeyboardTarget,
45
+ items,
46
+ keyboardFocus,
47
+ moveKeyboardFocus,
48
+ quickAddActive,
49
+ quickAddItems,
50
+ quickAddLoading,
51
+ quickAddQuery,
52
+ quickAddRef,
53
+ quickAddSelection,
54
+ quickAddSuggestions,
55
+ selected,
56
+ selectedIndex,
57
+ setEditingExpression,
58
+ setExpression,
59
+ setQuickAddQuery,
60
+ setQuickAddSelection,
61
+ setSelectedIndex,
62
+ submitQuickAdd,
63
+ updateKeyboardFocus,
64
+ } = controller;
677
65
  const availableTuiWidth = nativeRenderer.terminalWidth > 0
678
66
  ? nativeRenderer.terminalWidth - 8
679
67
  : 76;
@@ -708,9 +96,7 @@ export function SeriesEditorDialog({ dialogId, resolve, initialSpec }: SeriesEdi
708
96
  onMouseDown={() => beginQuickAdd()}
709
97
  onChange={(value) => {
710
98
  setQuickAddQuery(value);
711
- if (value.trim()) {
712
- activateQuickAdd();
713
- }
99
+ if (value.trim()) activateQuickAdd();
714
100
  }}
715
101
  onSubmit={submitQuickAdd}
716
102
  onBlur={deactivateQuickAdd}
@@ -760,9 +146,7 @@ export function SeriesEditorDialog({ dialogId, resolve, initialSpec }: SeriesEdi
760
146
  rowGap={isDesktop ? 0 : undefined}
761
147
  selectOnHover={isDesktop}
762
148
  onSelect={setSelectedIndex}
763
- onActivate={(_, index) => {
764
- setSelectedIndex(index);
765
- }}
149
+ onActivate={(_, index) => setSelectedIndex(index)}
766
150
  />
767
151
  </Box>
768
152
  ) : (
@@ -816,291 +200,25 @@ export function SeriesEditorDialog({ dialogId, resolve, initialSpec }: SeriesEdi
816
200
  />
817
201
 
818
202
  {isDesktop ? (
819
- <Box
820
- flexDirection="row"
821
- flexWrap="wrap"
822
- width="100%"
823
- style={{ columnGap: 12, rowGap: 10 }}
824
- >
825
- <DesktopEditorField label="Style">
826
- <NativeSelect
827
- value={selected.style}
828
- options={styleOptions}
829
- width="100%"
830
- onChange={(value) => setSelectedStyle(value as SeriesStyle)}
831
- />
832
- </DesktopEditorField>
833
- <DesktopEditorField label="Transform">
834
- <NativeSelect
835
- value={selected.transform}
836
- options={transformOptions}
837
- width="100%"
838
- onChange={(value) => setSelectedTransform(value as SeriesTransform)}
839
- />
840
- </DesktopEditorField>
841
- <DesktopEditorField label="Axis">
842
- <NativeSelect
843
- value={selected.axis}
844
- options={AXES.map((value) => ({ value, label: titleCase(value) }))}
845
- width="100%"
846
- onChange={(value) => updateSelected((series) => ({ ...series, axis: value as SeriesAxis }))}
847
- />
848
- </DesktopEditorField>
849
- {selected.source.kind === "security" ? (
850
- <DesktopEditorField label="Period">
851
- <NativeSelect
852
- value={selected.source.period ?? "auto"}
853
- options={periodOptions.map((value) => ({ value, label: value === "ttm" ? "TTM" : titleCase(value) }))}
854
- width="100%"
855
- onChange={(value) => updateSelected((series) => series.source.kind === "security" ? ({
856
- ...series,
857
- source: { ...series.source, period: value as SeriesPeriod },
858
- }) : series)}
859
- />
860
- </DesktopEditorField>
861
- ) : (
862
- <DesktopEditorField label="Visibility">
863
- <Box height="28px" justifyContent="center">
864
- <Checkbox
865
- label="Show series"
866
- checked={selected.visible !== false}
867
- variant="desktop"
868
- onChange={setSelectedVisibility}
869
- />
870
- </Box>
871
- </DesktopEditorField>
872
- )}
873
- {supportsTimestampMode(selected) && (
874
- <DesktopEditorField label="Timing">
875
- <NativeSelect
876
- value={seriesTimestampMode(selected)}
877
- options={TIMING_OPTIONS}
878
- width="100%"
879
- onChange={(value) => setSelectedTimestampMode(value as SeriesTimestampMode)}
880
- />
881
- </DesktopEditorField>
882
- )}
883
- <DesktopEditorField label="Panel">
884
- <Box flexDirection="row" width="100%" style={{ gap: 6 }}>
885
- <Box flexGrow={1} minWidth={0}>
886
- <NativeSelect
887
- value={selected.panelId}
888
- options={draft.panels.map((panel) => ({ value: panel.id, label: panel.label ?? titleCase(panel.id) }))}
889
- width="100%"
890
- onChange={setSelectedPanel}
891
- />
892
- </Box>
893
- <Button label="New Panel" onPress={addPanel} />
894
- </Box>
895
- </DesktopEditorField>
896
- <DesktopEditorField label={`Scale (${selectedPanel?.label ?? selected.panelId})`}>
897
- <NativeSelect
898
- value={selectedPanel?.scale ?? "linear"}
899
- options={[
900
- { value: "linear", label: "Linear" },
901
- { value: "log", label: "Log" },
902
- ]}
903
- width="100%"
904
- onChange={(value) => setSelectedPanelScale(value as PanelScale)}
905
- />
906
- </DesktopEditorField>
907
- {selected.source.kind === "security" && (
908
- <DesktopEditorField label="Visibility" width="100%">
909
- <Box height="28px" justifyContent="center">
910
- <Checkbox
911
- label="Show series"
912
- checked={selected.visible !== false}
913
- variant="desktop"
914
- onChange={setSelectedVisibility}
915
- />
916
- </Box>
917
- </DesktopEditorField>
918
- )}
919
- </Box>
203
+ <DesktopSeriesEditorFields fields={fields} />
920
204
  ) : (
921
- <>
922
- <TuiEditorField
923
- label="Style"
924
- focused={keyboardFocus === "style"}
925
- onFocus={() => focusKeyboardTarget("style")}
926
- >
927
- <SegmentedControl
928
- options={styleOptions}
929
- value={selected.style}
930
- onChange={(value) => setSelectedStyle(value as SeriesStyle)}
931
- focused={keyboardFocus === "style"}
932
- shortcutScope={dialogId}
933
- width="100%"
934
- wrap
935
- />
936
- </TuiEditorField>
937
-
938
- <TuiEditorField
939
- label="Transform"
940
- focused={keyboardFocus === "transform"}
941
- onFocus={() => focusKeyboardTarget("transform")}
942
- >
943
- <SegmentedControl
944
- options={transformOptions}
945
- value={selected.transform}
946
- onChange={(value) => setSelectedTransform(value as SeriesTransform)}
947
- focused={keyboardFocus === "transform"}
948
- shortcutScope={dialogId}
949
- width="100%"
950
- wrap
951
- />
952
- </TuiEditorField>
953
-
954
- <TuiEditorField
955
- label="Axis"
956
- focused={keyboardFocus === "axis"}
957
- onFocus={() => focusKeyboardTarget("axis")}
958
- >
959
- <SegmentedControl
960
- options={AXES.map((value) => ({ value, label: titleCase(value) }))}
961
- value={selected.axis}
962
- onChange={(value) => updateSelected((series) => ({ ...series, axis: value as SeriesAxis }))}
963
- focused={keyboardFocus === "axis"}
964
- shortcutScope={dialogId}
965
- width="100%"
966
- wrap
967
- />
968
- </TuiEditorField>
969
-
970
- <TuiEditorField
971
- label="Visibility"
972
- focused={keyboardFocus === "visibility"}
973
- onFocus={() => focusKeyboardTarget("visibility")}
974
- >
975
- <SegmentedControl
976
- options={[
977
- { value: "shown", label: "Shown" },
978
- {
979
- value: "hidden",
980
- label: "Hidden",
981
- disabled: selected.visible !== false && !canToggleChartSeries(draft, selected.id),
982
- },
983
- ]}
984
- value={selected.visible === false ? "hidden" : "shown"}
985
- onChange={(value) => setSelectedVisibility(value !== "hidden")}
986
- focused={keyboardFocus === "visibility"}
987
- shortcutScope={dialogId}
988
- width="100%"
989
- wrap
990
- />
991
- </TuiEditorField>
992
-
993
- <TuiEditorField
994
- label="Panel"
995
- focused={keyboardFocus === "panel"}
996
- onFocus={() => focusKeyboardTarget("panel")}
997
- >
998
- <Box flexDirection="row" width="100%" minWidth={0} gap={1} overflow="hidden">
999
- <Box flexGrow={1} minWidth={0} overflow="hidden">
1000
- <SegmentedControl
1001
- options={draft.panels.map((panel) => ({ value: panel.id, label: panel.label ?? titleCase(panel.id) }))}
1002
- value={selected.panelId}
1003
- onChange={setSelectedPanel}
1004
- focused={keyboardFocus === "panel"}
1005
- shortcutScope={dialogId}
1006
- width="100%"
1007
- wrap
1008
- />
1009
- </Box>
1010
- <Button label="New Panel" shortcut="N" onPress={addPanel} />
1011
- </Box>
1012
- </TuiEditorField>
1013
-
1014
- <TuiEditorField
1015
- label={`Scale (${selectedPanel?.label ?? selected.panelId})`}
1016
- focused={keyboardFocus === "scale"}
1017
- onFocus={() => focusKeyboardTarget("scale")}
1018
- >
1019
- <SegmentedControl
1020
- options={[
1021
- { value: "linear", label: "Linear" },
1022
- { value: "log", label: "Log" },
1023
- ]}
1024
- value={selectedPanel?.scale ?? "linear"}
1025
- onChange={(value) => setSelectedPanelScale(value as PanelScale)}
1026
- focused={keyboardFocus === "scale"}
1027
- shortcutScope={dialogId}
1028
- width="100%"
1029
- wrap
1030
- />
1031
- </TuiEditorField>
1032
-
1033
- {selected.source.kind === "security" && (
1034
- <TuiEditorField
1035
- label="Period"
1036
- focused={keyboardFocus === "period"}
1037
- onFocus={() => focusKeyboardTarget("period")}
1038
- >
1039
- <SegmentedControl
1040
- options={periodOptions.map((value) => ({ value, label: value === "ttm" ? "TTM" : titleCase(value) }))}
1041
- value={selected.source.period ?? "auto"}
1042
- onChange={(value) => updateSelected((series) => series.source.kind === "security" ? ({
1043
- ...series,
1044
- source: { ...series.source, period: value as SeriesPeriod },
1045
- }) : series)}
1046
- focused={keyboardFocus === "period"}
1047
- shortcutScope={dialogId}
1048
- width="100%"
1049
- wrap
1050
- />
1051
- </TuiEditorField>
1052
- )}
1053
-
1054
- {supportsTimestampMode(selected) && (
1055
- <TuiEditorField
1056
- label="Timing"
1057
- focused={keyboardFocus === "timing"}
1058
- onFocus={() => focusKeyboardTarget("timing")}
1059
- >
1060
- <SegmentedControl
1061
- options={TIMING_OPTIONS}
1062
- value={seriesTimestampMode(selected)}
1063
- onChange={(value) => setSelectedTimestampMode(value as SeriesTimestampMode)}
1064
- focused={keyboardFocus === "timing"}
1065
- shortcutScope={dialogId}
1066
- width="100%"
1067
- wrap
1068
- />
1069
- </TuiEditorField>
1070
- )}
1071
- </>
205
+ <OpenTuiSeriesEditorFields
206
+ fields={fields}
207
+ keyboardFocus={keyboardFocus}
208
+ dialogId={dialogId}
209
+ onFocus={focusKeyboardTarget}
210
+ />
1072
211
  )}
1073
212
  </Box>
1074
213
  )}
1075
214
 
1076
215
  {error && <Text fg={colors.negative} wrapText>{error}</Text>}
1077
216
 
1078
- <Box flexDirection="row" gap={1} width="100%" style={isDesktop ? { gap: 6, paddingTop: 2 } : undefined}>
1079
- {isDesktop ? (
1080
- <>
1081
- <Box flexDirection="row" style={{ gap: 6 }}>
1082
- <Button label="Add Series" onPress={() => beginQuickAdd(true)} disabled={draft.series.length >= MAX_CHART_COMPOSER_SERIES} />
1083
- <Button label="Remove" variant="danger" onPress={removeSeries} disabled={!selected || draft.series.length <= 1} />
1084
- <Button label="Move Up" onPress={() => moveSeries(-1)} disabled={selectedIndex <= 0} />
1085
- <Button label="Move Down" onPress={() => moveSeries(1)} disabled={selectedIndex < 0 || selectedIndex >= draft.series.length - 1} />
1086
- </Box>
1087
- <Box flexGrow={1} />
1088
- <Box flexDirection="row" style={{ gap: 6 }}>
1089
- <Button label="Cancel" variant="ghost" onPress={() => resolve(null)} />
1090
- <Button label="Save" variant="primary" onPress={saveDraft} />
1091
- </Box>
1092
- </>
1093
- ) : (
1094
- <>
1095
- <Button label="Add Series" shortcut="A" onPress={() => beginQuickAdd(true)} disabled={draft.series.length >= MAX_CHART_COMPOSER_SERIES} />
1096
- <Button label="Remove" shortcut="D" variant="danger" onPress={removeSeries} disabled={!selected || draft.series.length <= 1} />
1097
- <Button label="Move Up" onPress={() => moveSeries(-1)} disabled={selectedIndex <= 0} />
1098
- <Button label="Move Down" onPress={() => moveSeries(1)} disabled={selectedIndex < 0 || selectedIndex >= draft.series.length - 1} />
1099
- <Button label="Cancel" variant="ghost" onPress={() => resolve(null)} />
1100
- <Button label="Save" variant="primary" onPress={saveDraft} />
1101
- </>
1102
- )}
1103
- </Box>
217
+ {isDesktop ? (
218
+ <DesktopSeriesEditorActions actions={actions} />
219
+ ) : (
220
+ <OpenTuiSeriesEditorActions actions={actions} />
221
+ )}
1104
222
  </Box>
1105
223
  </DialogFrame>
1106
224
  );