gloomberb 0.11.0 → 0.11.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 (392) hide show
  1. package/README.md +38 -15
  2. package/README.zh-CN.md +12 -12
  3. package/package.json +10 -3
  4. package/src/api-client/auth.ts +14 -10
  5. package/src/api-client/data.ts +54 -0
  6. package/src/api-client/index.test.ts +124 -0
  7. package/src/api-client/index.ts +139 -4
  8. package/src/api-client/paths.ts +89 -0
  9. package/src/api-client/request.ts +15 -2
  10. package/src/api-client/socket-health.test.ts +1 -0
  11. package/src/api-client/socket.ts +2 -1
  12. package/src/api-client/types.ts +163 -0
  13. package/src/app/pane-runtime/index.tsx +4 -1
  14. package/src/app/pane-runtime/plugin-bindings.test.ts +117 -0
  15. package/src/app/pane-runtime/plugin-bindings.ts +25 -4
  16. package/src/app/pane-runtime/ticker-inspector-runtime.ts +25 -14
  17. package/src/app/runtime/desktop-deeplink.test.ts +16 -0
  18. package/src/app/runtime/desktop-deeplink.ts +66 -2
  19. package/src/app/runtime/desktop-menu.ts +3 -0
  20. package/src/app/runtime/plugin-bindings.ts +30 -28
  21. package/src/app/runtime/update.ts +8 -6
  22. package/src/app.tsx +21 -7
  23. package/src/architecture/import-boundaries.test.ts +5 -1
  24. package/src/capabilities/factories.ts +4 -0
  25. package/src/cli/commands/automation.test.ts +9 -0
  26. package/src/cli/commands/automation.ts +22 -73
  27. package/src/cli/index.ts +10 -6
  28. package/src/cli/registry.ts +1 -1
  29. package/src/components/chart/composite/composite-chart.test.tsx +45 -2
  30. package/src/components/chart/composite/composite-chart.tsx +16 -12
  31. package/src/components/chart/composite/format.test.ts +34 -0
  32. package/src/components/chart/composite/format.ts +33 -10
  33. package/src/components/chart/composite/scene.ts +0 -21
  34. package/src/components/chart/composite/tools.ts +49 -0
  35. package/src/components/chart/core/axis-format.ts +13 -3
  36. package/src/components/chart/core/index.test.ts +7 -0
  37. package/src/components/chart/core/renderer.ts +3 -4
  38. package/src/components/command-bar/commands/direct/index.ts +4 -0
  39. package/src/components/command-bar/commands/registry.ts +8 -1
  40. package/src/components/command-bar/layout-items/current-layout.ts +1 -1
  41. package/src/components/command-bar/pane-templates/workflow.ts +4 -0
  42. package/src/components/command-bar/routes/root/shortcut-feedback.ts +3 -0
  43. package/src/components/command-bar/routes/root/shortcuts.test.ts +19 -0
  44. package/src/components/command-bar/routes/root/shortcuts.ts +3 -1
  45. package/src/components/command-bar/surface/index.test.tsx +6 -6
  46. package/src/components/command-bar/surface/index.tsx +4 -1
  47. package/src/components/command-bar/surface/pane.test.tsx +66 -2
  48. package/src/components/command-bar/view-model.test.ts +2 -1
  49. package/src/components/command-bar/workflow/ops.test.ts +54 -0
  50. package/src/components/command-bar/workflow/ops.ts +1 -1
  51. package/src/components/company-logo.test.ts +19 -0
  52. package/src/components/company-logo.tsx +60 -0
  53. package/src/components/feed-data-table/stack-view.tsx +7 -1
  54. package/src/components/index.ts +1 -0
  55. package/src/components/layout/detached-pane-shell.tsx +85 -15
  56. package/src/components/layout/header.tsx +5 -5
  57. package/src/components/layout/pane/title.ts +31 -3
  58. package/src/components/layout/shell/active-drag.ts +4 -11
  59. package/src/components/layout/shell/drag/index.ts +4 -20
  60. package/src/components/layout/shell/drag/runtime.ts +3 -4
  61. package/src/components/layout/shell/index.test.tsx +17 -3
  62. package/src/components/layout/shell/index.tsx +89 -31
  63. package/src/components/layout/shell/menu.test.ts +37 -1
  64. package/src/components/layout/shell/menu.ts +14 -0
  65. package/src/components/layout/shell/native/pointer-runtime.ts +22 -8
  66. package/src/components/layout/shell/pane/actions.ts +0 -5
  67. package/src/components/layout/shell/pane/management-shortcuts.ts +9 -4
  68. package/src/components/layout/shell/shortcuts.ts +6 -3
  69. package/src/components/layout/shell/ticker-link-menu.test.ts +82 -0
  70. package/src/components/layout/shell/ticker-link-menu.ts +78 -0
  71. package/src/components/layout/status-bar.test.tsx +62 -61
  72. package/src/components/layout/status-bar.tsx +53 -75
  73. package/src/components/onboarding/account-step/form-panel.tsx +4 -2
  74. package/src/components/onboarding/account-step/index.tsx +1 -1
  75. package/src/components/onboarding/account-step/signed-in-panel.tsx +1 -1
  76. package/src/components/onboarding/onboarding-wizard.test.tsx +44 -1
  77. package/src/components/onboarding/onboarding-wizard.tsx +13 -5
  78. package/src/components/onboarding/wizard-account.ts +4 -23
  79. package/src/components/table-view-shared.test.ts +11 -0
  80. package/src/components/table-view-shared.tsx +26 -0
  81. package/src/components/ui/data-table/index.tsx +19 -1
  82. package/src/components/ui/fields.tsx +5 -1
  83. package/src/core/state/app/layout-reducer.ts +36 -0
  84. package/src/core/state/app/layout.ts +10 -5
  85. package/src/core/state/app/state.ts +0 -13
  86. package/src/core/state/app/types.ts +6 -4
  87. package/src/data/config/chart-settings.ts +1 -1
  88. package/src/data/config/layout.ts +2 -0
  89. package/src/data/config/store/index.test.ts +27 -6
  90. package/src/data/config/store/normalize.ts +3 -5
  91. package/src/data/plugin-state-store.ts +11 -0
  92. package/src/data/sqlite/cache.test.ts +10 -0
  93. package/src/i18n/es.ts +13 -5
  94. package/src/i18n/ja.ts +13 -5
  95. package/src/i18n/ko.ts +13 -5
  96. package/src/i18n/zh-cn.ts +13 -5
  97. package/src/i18n/zh-tw.ts +13 -5
  98. package/src/layout-marketplace/api.test.ts +54 -0
  99. package/src/layout-marketplace/api.ts +65 -0
  100. package/src/layout-marketplace/gallery-desktop.test.tsx +262 -0
  101. package/src/layout-marketplace/gallery-desktop.tsx +375 -0
  102. package/src/layout-marketplace/gallery-terminal.tsx +376 -0
  103. package/src/layout-marketplace/gallery.test.tsx +84 -0
  104. package/src/layout-marketplace/gallery.tsx +323 -0
  105. package/src/layout-marketplace/geometry.test.ts +47 -0
  106. package/src/layout-marketplace/geometry.ts +80 -0
  107. package/src/layout-marketplace/mini-workspace.test.tsx +122 -0
  108. package/src/layout-marketplace/mini-workspace.tsx +428 -0
  109. package/src/layout-marketplace/model.test.ts +118 -0
  110. package/src/layout-marketplace/model.ts +171 -0
  111. package/src/layout-marketplace/name-dialog.tsx +60 -0
  112. package/src/layout-marketplace/pane-imagery.ts +85 -0
  113. package/src/layout-marketplace/pane.tsx +20 -0
  114. package/src/layout-marketplace/payload.test.ts +246 -0
  115. package/src/layout-marketplace/payload.ts +624 -0
  116. package/src/layout-marketplace/use-marketplace.ts +63 -0
  117. package/src/market-data/coordinator/index.test.ts +2 -2
  118. package/src/market-data/coordinator/index.ts +1 -1
  119. package/src/market-data/hooks.tsx +0 -41
  120. package/src/market-data/market/format.test.ts +7 -0
  121. package/src/market-data/market/freshness.ts +5 -2
  122. package/src/market-data/quotes/freshness.test.ts +16 -0
  123. package/src/news/aggregator.test.ts +92 -3
  124. package/src/news/aggregator.ts +98 -18
  125. package/src/news/hooks.test.tsx +2 -0
  126. package/src/news/hooks.ts +27 -1
  127. package/src/news/news-model.ts +5 -2
  128. package/src/news/types.ts +7 -0
  129. package/src/plugins/broker-sync/broker-sync.test.ts +58 -0
  130. package/src/plugins/broker-sync/normalize.ts +42 -5
  131. package/src/plugins/builtin/account-management/pane.tsx +5 -5
  132. package/src/plugins/builtin/ai/index.tsx +18 -2
  133. package/src/plugins/builtin/ai/screener/pane.test.tsx +47 -4
  134. package/src/plugins/builtin/ai/screener/pane.tsx +35 -12
  135. package/src/plugins/builtin/alerts/alert-engine.test.ts +53 -1
  136. package/src/plugins/builtin/alerts/alert-engine.ts +34 -1
  137. package/src/plugins/builtin/alerts/index.test.tsx +49 -6
  138. package/src/plugins/builtin/alerts/index.tsx +11 -6
  139. package/src/plugins/builtin/alerts/pane.tsx +51 -9
  140. package/src/plugins/builtin/alerts/quotes.ts +11 -3
  141. package/src/plugins/builtin/alerts/types.ts +1 -0
  142. package/src/plugins/builtin/analytics/index.tsx +11 -1
  143. package/src/plugins/builtin/analytics/pane-model.ts +6 -4
  144. package/src/plugins/builtin/broker-manager/index.tsx +3 -0
  145. package/src/plugins/builtin/buildout/model/index.ts +1 -1
  146. package/src/plugins/builtin/buildout/pane/index.tsx +17 -18
  147. package/src/plugins/builtin/cds/client.test.ts +83 -0
  148. package/src/plugins/builtin/cds/client.ts +67 -0
  149. package/src/plugins/builtin/cds/index.tsx +41 -0
  150. package/src/plugins/builtin/cds/model.test.ts +232 -0
  151. package/src/plugins/builtin/cds/model.ts +401 -0
  152. package/src/plugins/builtin/cds/pane.test.tsx +132 -0
  153. package/src/plugins/builtin/cds/pane.tsx +358 -0
  154. package/src/plugins/builtin/chart-composer/chart-spec.ts +31 -0
  155. package/src/plugins/builtin/chart-composer/index.tsx +88 -2
  156. package/src/plugins/builtin/chart-composer/pane-share.test.ts +71 -0
  157. package/src/plugins/builtin/chart-composer/pane.tsx +103 -15
  158. package/src/plugins/builtin/chat/content/index.tsx +4 -1
  159. package/src/plugins/builtin/chat/controller/channels.ts +7 -1
  160. package/src/plugins/builtin/chat/controller/index.ts +14 -9
  161. package/src/plugins/builtin/chat/controller/notifications.ts +13 -3
  162. package/src/plugins/builtin/chat/controller/persistence.ts +0 -3
  163. package/src/plugins/builtin/chat/controller/realtime.ts +3 -3
  164. package/src/plugins/builtin/chat/controller/send.ts +1 -3
  165. package/src/plugins/builtin/chat/controller/session-runtime.ts +18 -11
  166. package/src/plugins/builtin/chat/controller/state.ts +1 -3
  167. package/src/plugins/builtin/chat/controller/storage.ts +0 -2
  168. package/src/plugins/builtin/chat/controller/utils.ts +4 -6
  169. package/src/plugins/builtin/chat/controller/view.ts +2 -2
  170. package/src/plugins/builtin/chat/controller.test.ts +142 -14
  171. package/src/plugins/builtin/chat/message/desktop.test.tsx +88 -0
  172. package/src/plugins/builtin/chat/message/desktop.tsx +3 -1
  173. package/src/plugins/builtin/chat/message/profile-popover.test.tsx +1 -29
  174. package/src/plugins/builtin/chat/profile-popover.ts +53 -22
  175. package/src/plugins/builtin/chat/sidebar.test.tsx +81 -0
  176. package/src/plugins/builtin/chat/sidebar.tsx +63 -0
  177. package/src/plugins/builtin/chat/test-harness.tsx +29 -1
  178. package/src/plugins/builtin/cloud/auth-actions.tsx +14 -7
  179. package/src/plugins/builtin/cloud/auth-commands.ts +21 -51
  180. package/src/plugins/builtin/cloud/auth-dialog.tsx +296 -0
  181. package/src/{components/onboarding/account-step/model.test.ts → plugins/builtin/cloud/auth-model.test.ts} +1 -1
  182. package/src/{components/onboarding/account-step/model.ts → plugins/builtin/cloud/auth-model.ts} +37 -3
  183. package/src/plugins/builtin/cloud/browser.tsx +9 -0
  184. package/src/plugins/builtin/cloud/buildout-module.tsx +23 -0
  185. package/src/plugins/builtin/cloud/device-signin.ts +2 -8
  186. package/src/plugins/builtin/cloud/index.tsx +2 -0
  187. package/src/plugins/builtin/cloud/plugin.tsx +14 -24
  188. package/src/plugins/builtin/cloud/upgrade-command.ts +13 -6
  189. package/src/plugins/builtin/cloud-tweets/pane-share.test.ts +56 -0
  190. package/src/plugins/builtin/cloud-tweets/pane.tsx +20 -5
  191. package/src/plugins/builtin/cloud-tweets/registration.ts +50 -6
  192. package/src/plugins/builtin/composite-plugins.ts +3 -1
  193. package/src/plugins/builtin/congress-trades/detail.tsx +43 -1
  194. package/src/plugins/builtin/congress-trades/model.test.ts +85 -0
  195. package/src/plugins/builtin/congress-trades/model.ts +69 -1
  196. package/src/plugins/builtin/congress-trades/pane.tsx +51 -2
  197. package/src/plugins/builtin/connections/index.ts +3 -0
  198. package/src/plugins/builtin/dividend-yield/client.ts +18 -0
  199. package/src/plugins/builtin/dividend-yield/pane.tsx +7 -6
  200. package/src/plugins/builtin/earnings/index.tsx +5 -3
  201. package/src/plugins/builtin/earnings/model.test.ts +19 -1
  202. package/src/plugins/builtin/earnings/model.ts +17 -15
  203. package/src/plugins/builtin/econ/calendar-model.ts +0 -5
  204. package/src/plugins/builtin/econ/index.test.tsx +6 -1
  205. package/src/plugins/builtin/fear-greed/charts.tsx +12 -8
  206. package/src/plugins/builtin/help/index.tsx +2 -2
  207. package/src/plugins/builtin/holders/thirteenf-match.ts +1 -2
  208. package/src/plugins/builtin/insider/index.tsx +27 -11
  209. package/src/plugins/builtin/kelly-sizer/index.tsx +4 -1
  210. package/src/plugins/builtin/layout-manager/index.tsx +17 -4
  211. package/src/plugins/builtin/news/index.tsx +11 -6
  212. package/src/plugins/builtin/news/wire/breaking/notifications.test.ts +2 -0
  213. package/src/plugins/builtin/news/wire/breaking/pane.test.tsx +2 -0
  214. package/src/plugins/builtin/news/wire/breaking/pane.tsx +4 -1
  215. package/src/plugins/builtin/news/wire/feed-config.test.ts +15 -1
  216. package/src/plugins/builtin/news/wire/feed-config.ts +25 -5
  217. package/src/plugins/builtin/news/wire/index.tsx +3 -3
  218. package/src/plugins/builtin/news/wire/industry-pane.tsx +7 -2
  219. package/src/plugins/builtin/news/wire/news/footer.ts +34 -1
  220. package/src/plugins/builtin/news/wire/news/preset-pane.tsx +4 -1
  221. package/src/plugins/builtin/news/wire/news/table.tsx +8 -2
  222. package/src/plugins/builtin/news/wire/rss/source.test.ts +12 -0
  223. package/src/plugins/builtin/news/wire/rss/source.ts +1 -1
  224. package/src/plugins/builtin/notes/files.ts +80 -0
  225. package/src/plugins/builtin/notes/index.tsx +3 -0
  226. package/src/plugins/builtin/notes/sync.test.ts +49 -0
  227. package/src/plugins/builtin/notes/sync.ts +98 -0
  228. package/src/plugins/builtin/options/index.tsx +1 -0
  229. package/src/plugins/builtin/options/view.test.tsx +25 -3
  230. package/src/plugins/builtin/options/view.tsx +1 -0
  231. package/src/plugins/builtin/portfolio-list/account-metrics.test.ts +22 -0
  232. package/src/plugins/builtin/portfolio-list/account-metrics.ts +12 -7
  233. package/src/plugins/builtin/portfolio-list/cli/render.ts +5 -3
  234. package/src/plugins/builtin/portfolio-list/column-values.ts +15 -4
  235. package/src/plugins/builtin/portfolio-list/header.tsx +13 -3
  236. package/src/plugins/builtin/portfolio-list/index.tsx +8 -0
  237. package/src/plugins/builtin/portfolio-list/metrics.test.ts +31 -1
  238. package/src/plugins/builtin/portfolio-list/pane/index.test.tsx +1 -0
  239. package/src/plugins/builtin/portfolio-list/pane/index.tsx +10 -7
  240. package/src/plugins/builtin/portfolio-list/pane/streaming.ts +6 -1
  241. package/src/plugins/builtin/portfolio-list/position-metrics.ts +25 -6
  242. package/src/plugins/builtin/portfolio-list/summary/index.tsx +35 -21
  243. package/src/plugins/builtin/portfolio-list/summary/totals.ts +20 -6
  244. package/src/plugins/builtin/research/corporate-actions-pane.tsx +5 -12
  245. package/src/plugins/builtin/research/equity-diagnostic-pane.test.tsx +329 -0
  246. package/src/plugins/builtin/research/equity-diagnostic-pane.tsx +582 -0
  247. package/src/plugins/builtin/research/index.test.ts +14 -0
  248. package/src/plugins/builtin/research/index.tsx +29 -0
  249. package/src/plugins/builtin/sec/index.tsx +24 -9
  250. package/src/plugins/builtin/sectors/index.tsx +14 -0
  251. package/src/plugins/builtin/shared/cloud-upgrade.ts +18 -11
  252. package/src/plugins/builtin/shared/public-pane.test.ts +12 -0
  253. package/src/plugins/builtin/shared/public-pane.ts +9 -0
  254. package/src/plugins/builtin/shared/public-share.ts +23 -0
  255. package/src/plugins/builtin/shared/ticker-source.ts +32 -0
  256. package/src/plugins/builtin/shared/ticker-surface.test.ts +27 -0
  257. package/src/plugins/builtin/shared/ticker-surface.ts +17 -0
  258. package/src/plugins/builtin/substack/api/cache.ts +7 -3
  259. package/src/plugins/builtin/substack/api/loaders.ts +36 -12
  260. package/src/plugins/builtin/substack/api/types.ts +2 -0
  261. package/src/plugins/builtin/substack/pane-state.ts +1 -1
  262. package/src/plugins/builtin/substack/pane.tsx +55 -12
  263. package/src/plugins/builtin/thirteenf/api.test.ts +1 -1
  264. package/src/plugins/builtin/thirteenf/api.ts +40 -18
  265. package/src/plugins/builtin/thirteenf/data.ts +15 -7
  266. package/src/plugins/builtin/thirteenf/pane.tsx +45 -10
  267. package/src/plugins/builtin/ticker-detail/index.tsx +10 -0
  268. package/src/plugins/builtin/ticker-detail/overview-tab.tsx +12 -4
  269. package/src/plugins/builtin/ticker-detail/quote-monitor/card.tsx +8 -2
  270. package/src/plugins/builtin/ticker-detail/settings.ts +12 -0
  271. package/src/plugins/builtin/treasury-auctions/index.tsx +2 -0
  272. package/src/plugins/builtin/world-indices/index.tsx +2 -0
  273. package/src/plugins/catalog-browser.test.ts +78 -0
  274. package/src/plugins/catalog-browser.ts +136 -0
  275. package/src/plugins/gridlock-notification.ts +2 -1
  276. package/src/plugins/ibkr/flex/index.test.ts +19 -0
  277. package/src/plugins/ibkr/flex/index.ts +1 -1
  278. package/src/plugins/pane-manager/gridlock.ts +87 -3
  279. package/src/plugins/pane-manager.test.ts +30 -0
  280. package/src/plugins/pane-manager.ts +6 -1
  281. package/src/plugins/prediction-markets/chart.tsx +2 -2
  282. package/src/plugins/prediction-markets/controller/catalog.ts +77 -2
  283. package/src/plugins/prediction-markets/controller/data.ts +7 -1
  284. package/src/plugins/prediction-markets/controller/index.ts +9 -2
  285. package/src/plugins/prediction-markets/controller/keyboard.ts +10 -14
  286. package/src/plugins/prediction-markets/detail/overview.tsx +21 -0
  287. package/src/plugins/prediction-markets/detail/pane.tsx +43 -34
  288. package/src/plugins/prediction-markets/detail/trades.tsx +34 -9
  289. package/src/plugins/prediction-markets/detail.test.tsx +29 -2
  290. package/src/plugins/prediction-markets/index.tsx +8 -4
  291. package/src/plugins/prediction-markets/navigation.ts +0 -1
  292. package/src/plugins/prediction-markets/pane.test.tsx +77 -0
  293. package/src/plugins/prediction-markets/pane.tsx +12 -0
  294. package/src/plugins/prediction-markets/plugin.test.tsx +36 -0
  295. package/src/plugins/prediction-markets/services/kalshi/adapter.ts +49 -9
  296. package/src/plugins/prediction-markets/services/polymarket/adapter.ts +31 -0
  297. package/src/plugins/prediction-markets/test-helpers.tsx +9 -0
  298. package/src/plugins/registry/index.ts +2 -0
  299. package/src/plugins/ticker-navigation.test.ts +24 -0
  300. package/src/plugins/ticker-navigation.ts +40 -0
  301. package/src/remote/schema.ts +1 -1
  302. package/src/renderers/browser/app-services.ts +61 -0
  303. package/src/renderers/browser/cloud-transport.test.ts +68 -0
  304. package/src/renderers/browser/cloud-transport.ts +48 -0
  305. package/src/renderers/browser/config-host.ts +58 -0
  306. package/src/renderers/browser/deeplink-bridge.test.ts +41 -0
  307. package/src/renderers/browser/deeplink-bridge.ts +22 -0
  308. package/src/renderers/browser/error-boundary.tsx +23 -0
  309. package/src/renderers/browser/main.tsx +64 -0
  310. package/src/renderers/browser/persistence.ts +74 -0
  311. package/src/renderers/browser/storage.test.ts +66 -0
  312. package/src/renderers/browser/storage.ts +50 -0
  313. package/src/renderers/browser/ticker-repository.ts +59 -0
  314. package/src/renderers/browser/ui-host.tsx +40 -0
  315. package/src/renderers/cloudflare/worker.test.ts +136 -0
  316. package/src/renderers/cloudflare/worker.ts +116 -0
  317. package/src/renderers/electrobun/bun/application-menu/index.ts +3 -2
  318. package/src/renderers/electrobun/bun/desktop/initialization.ts +1 -0
  319. package/src/renderers/electrobun/bun/desktop/plugin-state.ts +7 -1
  320. package/src/renderers/electrobun/view/app-services.ts +14 -3
  321. package/src/renderers/electrobun/view/build-assets.ts +12 -10
  322. package/src/renderers/electrobun/view/cli-pane-shot-entry.tsx +1 -1
  323. package/src/renderers/electrobun/view/data-table/index.tsx +10 -1
  324. package/src/renderers/electrobun/view/desktop/controls.tsx +2 -0
  325. package/src/renderers/electrobun/view/dom-ui-host.tsx +143 -0
  326. package/src/renderers/electrobun/view/host/box.test.tsx +33 -0
  327. package/src/renderers/electrobun/view/host/box.tsx +11 -6
  328. package/src/renderers/electrobun/view/host/chart-surface.tsx +7 -1
  329. package/src/renderers/electrobun/view/host/input.tsx +11 -7
  330. package/src/renderers/electrobun/view/host/native.ts +0 -5
  331. package/src/renderers/electrobun/view/main.tsx +2 -0
  332. package/src/renderers/electrobun/view/remote/persistence.ts +5 -0
  333. package/src/renderers/electrobun/view/styles.css +107 -7
  334. package/src/renderers/electrobun/view/toast-host.test.tsx +78 -0
  335. package/src/renderers/electrobun/view/toast-host.tsx +69 -43
  336. package/src/renderers/electrobun/view/ui-host.tsx +6 -137
  337. package/src/renderers/opentui/start.tsx +2 -0
  338. package/src/renderers/share/layout-view.test.tsx +58 -0
  339. package/src/renderers/share/layout-view.tsx +133 -0
  340. package/src/renderers/share/main.tsx +102 -0
  341. package/src/renderers/share/styles.css +54 -0
  342. package/src/renderers/share/view.test.tsx +100 -0
  343. package/src/renderers/share/view.tsx +150 -0
  344. package/src/shares/api.test.ts +105 -0
  345. package/src/shares/api.ts +127 -0
  346. package/src/shares/chart-snapshot.test.ts +34 -0
  347. package/src/shares/chart-snapshot.ts +28 -0
  348. package/src/shares/location.test.ts +19 -0
  349. package/src/shares/location.ts +11 -0
  350. package/src/shares/pane.test.ts +110 -0
  351. package/src/shares/pane.ts +54 -0
  352. package/src/shares/payload.ts +177 -0
  353. package/src/sources/gloomberb-cloud/index.test.ts +61 -1
  354. package/src/sources/gloomberb-cloud/index.ts +86 -14
  355. package/src/sources/gloomberb-cloud/normalizers.test.ts +34 -0
  356. package/src/sources/gloomberb-cloud/normalizers.ts +5 -5
  357. package/src/sources/provider-router/history.test.ts +2 -2
  358. package/src/sources/provider-router/news.test.ts +34 -0
  359. package/src/sources/provider-router/news.ts +15 -1
  360. package/src/sources/sec-edgar.test.ts +59 -0
  361. package/src/sources/sec-edgar.ts +63 -23
  362. package/src/sources/yahoo-finance/mappers.ts +3 -0
  363. package/src/sources/yahoo-finance/snapshots.ts +10 -2
  364. package/src/sources/yahoo-finance/symbols.test.ts +10 -0
  365. package/src/sources/yahoo-finance/symbols.ts +13 -8
  366. package/src/sources/yahoo-finance.test.ts +1 -1
  367. package/src/sources/yahoo-finance.ts +6 -2
  368. package/src/state/app/bootstrap.ts +6 -7
  369. package/src/state/app/context/index.test.ts +41 -1
  370. package/src/state/app/context/index.test.tsx +34 -16
  371. package/src/state/selectors-ui.ts +0 -8
  372. package/src/sync/baseline.ts +32 -0
  373. package/src/sync/controller.test.ts +124 -1
  374. package/src/sync/controller.ts +54 -12
  375. package/src/sync/core-contributors.test.ts +44 -0
  376. package/src/sync/core-contributors.ts +59 -10
  377. package/src/sync/react.ts +22 -2
  378. package/src/sync/types.ts +12 -0
  379. package/src/theme/font-scale.ts +0 -11
  380. package/src/tickers/search/index.test.ts +27 -0
  381. package/src/tickers/search/upsert.ts +10 -0
  382. package/src/types/capability-route-source.ts +2 -1
  383. package/src/types/config.ts +35 -5
  384. package/src/types/desktop-menu.ts +1 -0
  385. package/src/types/plugin.ts +36 -0
  386. package/src/ui/context-menu.tsx +1 -1
  387. package/src/ui/host.tsx +4 -0
  388. package/src/ui/toast.tsx +2 -0
  389. package/src/utils/back-navigation.ts +0 -4
  390. package/src/utils/price-history.ts +11 -10
  391. package/src/utils/sec.test.ts +6 -0
  392. package/src/utils/sec.ts +1 -1
@@ -96,6 +96,47 @@ function ChannelNotificationIcon({
96
96
  );
97
97
  }
98
98
 
99
+ function ProfileIcon({
100
+ color,
101
+ onMouseDown,
102
+ }: {
103
+ color: string;
104
+ onMouseDown?: (event: any) => void;
105
+ }) {
106
+ const { nativePaneChrome } = useUiCapabilities();
107
+
108
+ if (!nativePaneChrome) {
109
+ return (
110
+ <Text fg={color} selectable={false} onMouseDown={onMouseDown}>@</Text>
111
+ );
112
+ }
113
+
114
+ return (
115
+ <Span
116
+ fg={color}
117
+ onMouseDown={onMouseDown}
118
+ style={{
119
+ color,
120
+ display: "inline-flex",
121
+ alignItems: "center",
122
+ justifyContent: "center",
123
+ width: 16,
124
+ height: 16,
125
+ }}
126
+ >
127
+ <svg viewBox="0 0 24 24" width="15" height="15" fill="none" aria-hidden="true">
128
+ <circle cx="12" cy="8.5" r="3.4" stroke="currentColor" strokeWidth="1.8" />
129
+ <path
130
+ d="M5.5 19.5a6.5 6.5 0 0 1 13 0"
131
+ stroke="currentColor"
132
+ strokeWidth="1.8"
133
+ strokeLinecap="round"
134
+ />
135
+ </svg>
136
+ </Span>
137
+ );
138
+ }
139
+
99
140
  export function ChannelSidebar({
100
141
  channels,
101
142
  channelStates,
@@ -109,9 +150,11 @@ export function ChannelSidebar({
109
150
  canManageNotifications,
110
151
  canCreateConversation,
111
152
  directExpanded,
153
+ needsProfileSetup = false,
112
154
  onSelect,
113
155
  onFocusRequest,
114
156
  onCreateConversation,
157
+ onOpenProfile,
115
158
  onToggleNotifications,
116
159
  onToggleDirectExpanded,
117
160
  }: {
@@ -127,9 +170,11 @@ export function ChannelSidebar({
127
170
  canManageNotifications: boolean;
128
171
  canCreateConversation: boolean;
129
172
  directExpanded: boolean;
173
+ needsProfileSetup?: boolean;
130
174
  onSelect?: (channelId: string) => void;
131
175
  onFocusRequest?: () => void;
132
176
  onCreateConversation?: () => void;
177
+ onOpenProfile?: () => void;
133
178
  onToggleNotifications?: (channelId: string, enabled: boolean) => void;
134
179
  onToggleDirectExpanded?: () => void;
135
180
  }) {
@@ -240,6 +285,24 @@ export function ChannelSidebar({
240
285
  );
241
286
  })}
242
287
  <Box flexGrow={1} />
288
+ {needsProfileSetup && (
289
+ <PaneSidebarRow
290
+ active={false}
291
+ ariaLabel={t("Profile")}
292
+ onSelect={onOpenProfile}
293
+ >
294
+ {({ foregroundColor, onMouseDown }) => (
295
+ <>
296
+ <Text fg={foregroundColor} selectable={false} onMouseDown={onMouseDown}> </Text>
297
+ <ProfileIcon color={foregroundColor} onMouseDown={onMouseDown} />
298
+ <Text fg={foregroundColor} selectable={false} onMouseDown={onMouseDown}>
299
+ {` ${truncateChannelLabel(t("Profile"), Math.max(listWidth - 3, 1))}`}
300
+ </Text>
301
+ <Box flexGrow={1} onMouseDown={onMouseDown} />
302
+ </>
303
+ )}
304
+ </PaneSidebarRow>
305
+ )}
243
306
  {loading && focused && (
244
307
  <Box height={1} width={listWidth} flexDirection="row">
245
308
  <Text fg={colors.textDim}>{` ${t("syncing")}`}</Text>
@@ -6,7 +6,7 @@ import { MemoryPluginPersistence } from "../../../test-support/plugin-persistenc
6
6
  import { createTestPluginRuntime } from "../../../test-support/plugin-runtime";
7
7
  import { createDefaultConfig } from "../../../types/config";
8
8
  import { Box } from "../../../ui";
9
- import { apiClient, type ChatChannel, type ChatMessage } from "../../../api-client";
9
+ import { apiClient, type AccountProfile, type ChatChannel, type ChatMessage } from "../../../api-client";
10
10
  import { PluginRenderProvider, type PluginRuntimeAccess } from "../../runtime";
11
11
  import { setSharedMarketDataForTests, setSharedRegistryForTests } from "../../registry";
12
12
  import { ChatContent } from "./content";
@@ -67,6 +67,34 @@ export function installServerChannels(controller: ChatController, channels = TES
67
67
 
68
68
  export { MemoryPluginPersistence as MemoryPersistence } from "../../../test-support/plugin-persistence";
69
69
 
70
+ export function makeAccountProfile(overrides: Partial<AccountProfile> = {}): AccountProfile {
71
+ return {
72
+ id: "u1",
73
+ email: "vince@example.com",
74
+ emailVerified: true,
75
+ plan: "pro",
76
+ username: "vince",
77
+ name: "Vince",
78
+ company: "Gloom",
79
+ title: null,
80
+ bio: "Made Gloomberb",
81
+ profilePublic: true,
82
+ publicEmail: null,
83
+ xAccount: null,
84
+ sharedPortfolioId: "broker:ibkr:coldstart",
85
+ acceptUnknownDms: false,
86
+ chatEmailNotificationsEnabled: true,
87
+ portfolioAnalytics: null,
88
+ syncEnabled: true,
89
+ weeklyRoundupEnabled: true,
90
+ positionAlertsEnabled: true,
91
+ lastSyncAt: null,
92
+ lastRoundupEmailAt: null,
93
+ updatedAt: "2026-07-21T22:03:59.832Z",
94
+ ...overrides,
95
+ };
96
+ }
97
+
70
98
  export function makeMessage(index: number): ChatMessage {
71
99
  return {
72
100
  id: `m${index}`,
@@ -4,15 +4,19 @@ import { Button } from "../../../components";
4
4
  import { usePluginAppActions } from "../../runtime";
5
5
  import { colors, hoverBg } from "../../../theme/colors";
6
6
  import { t } from "../../../i18n";
7
+ import { requestAuthDialog } from "./auth-dialog";
8
+ import type { AccountMode } from "./auth-model";
7
9
 
8
- function openAuthCommand(
10
+ function openAuth(
9
11
  openCommandBar: (query?: string) => void,
10
- query: string,
12
+ mode: AccountMode,
11
13
  event?: { preventDefault?: () => void; stopPropagation?: () => void },
12
14
  ) {
13
15
  event?.preventDefault?.();
14
16
  event?.stopPropagation?.();
15
- openCommandBar(query);
17
+ if (!requestAuthDialog({ mode })) {
18
+ openCommandBar(mode === "login" ? "Log In" : "Sign Up");
19
+ }
16
20
  }
17
21
 
18
22
  export function InlineAuthActions({ showSignup = true }: { showSignup?: boolean }) {
@@ -25,7 +29,7 @@ export function InlineAuthActions({ showSignup = true }: { showSignup?: boolean
25
29
  backgroundColor={hoveredAction === "login" ? hoverBg() : undefined}
26
30
  onMouseOver={() => setHoveredAction((current) => (current === "login" ? current : "login"))}
27
31
  onMouseOut={() => setHoveredAction((current) => (current === "login" ? null : current))}
28
- onMouseDown={(event: any) => openAuthCommand(openCommandBar, "Log In", event)}
32
+ onMouseDown={(event: any) => openAuth(openCommandBar, "login", event)}
29
33
  >
30
34
  <Text fg={hoveredAction === "login" ? colors.text : colors.textDim}>{` ${t("Log In")} `}</Text>
31
35
  </Box>
@@ -36,7 +40,7 @@ export function InlineAuthActions({ showSignup = true }: { showSignup?: boolean
36
40
  backgroundColor={hoveredAction === "signup" ? hoverBg() : undefined}
37
41
  onMouseOver={() => setHoveredAction((current) => (current === "signup" ? current : "signup"))}
38
42
  onMouseOut={() => setHoveredAction((current) => (current === "signup" ? null : current))}
39
- onMouseDown={(event: any) => openAuthCommand(openCommandBar, "Sign Up", event)}
43
+ onMouseDown={(event: any) => openAuth(openCommandBar, "signup", event)}
40
44
  >
41
45
  <Text fg={hoveredAction === "signup" ? colors.text : colors.textDim}>{` ${t("Sign Up")} `}</Text>
42
46
  </Box>
@@ -49,16 +53,19 @@ export function InlineAuthActions({ showSignup = true }: { showSignup?: boolean
49
53
  export function CloudAuthNotice({
50
54
  message,
51
55
  showSignup = true,
56
+ needsVerification = false,
52
57
  }: {
53
58
  message: string;
54
59
  showSignup?: boolean;
60
+ /** Forces the verification branch and uses `message` as its headline. */
61
+ needsVerification?: boolean;
55
62
  }) {
56
63
  const { openCommandBar } = usePluginAppActions();
57
64
 
58
- if (/verification/i.test(message)) {
65
+ if (needsVerification || /verification/i.test(message)) {
59
66
  return (
60
67
  <Box flexDirection="column" padding={1} gap={1}>
61
- <Text fg={colors.positive}>{t("Verify your email to use Cloud tweets.")}</Text>
68
+ <Text fg={colors.positive}>{needsVerification ? message : t("Verify your email to use Cloud tweets.")}</Text>
62
69
  <Button label={t("Resend Verification Email")} variant="secondary" onPress={() => openCommandBar("Resend Verification Email")} />
63
70
  </Box>
64
71
  );
@@ -1,6 +1,7 @@
1
1
  import type { GloomPluginContext } from "../../../types/plugin";
2
2
  import { apiClient } from "../../../api-client";
3
3
  import { chatController } from "../chat/controller";
4
+ import { requestAuthDialog } from "./auth-dialog";
4
5
  import { requestDeviceSignInDialog } from "./device-signin-dialog";
5
6
 
6
7
  export function registerCloudAuthCommands(ctx: GloomPluginContext): void {
@@ -10,7 +11,7 @@ export function registerCloudAuthCommands(ctx: GloomPluginContext): void {
10
11
  description: "Sign in by scanning a QR code with the Gloom app",
11
12
  keywords: ["login", "sign in", "qr", "scan", "device", "mobile", "phone", "app", "code"],
12
13
  category: "config",
13
- hidden: () => !!apiClient.getSessionToken(),
14
+ hidden: () => apiClient.isSignedIn(),
14
15
  execute: () => {
15
16
  const opened = requestDeviceSignInDialog({
16
17
  onSignedIn: () => ctx.showPane("chat"),
@@ -27,24 +28,15 @@ export function registerCloudAuthCommands(ctx: GloomPluginContext): void {
27
28
  description: "Log in to your Gloomberb account",
28
29
  keywords: ["login", "sign in", "auth", "account"],
29
30
  category: "config",
30
- wizardLayout: "form",
31
- hidden: () => !!apiClient.getSessionToken(),
32
- wizard: [
33
- { key: "email", label: "Email", type: "text", placeholder: "email@example.com" },
34
- { key: "password", label: "Password", type: "password", placeholder: "Your password" },
35
- { key: "_validate", label: "Signing in...", type: "info", body: ["Connecting to Gloomberb...", "Logged in successfully!"] },
36
- ],
37
- execute: async (values) => {
38
- if (!values?.email || !values?.password) {
39
- throw new Error("Email and password are required");
40
- }
41
- const user = await apiClient.signIn(values.email, values.password);
42
- if (!user.emailVerified) {
43
- await apiClient.sendVerification().catch(() => {});
31
+ hidden: () => apiClient.isSignedIn(),
32
+ execute: () => {
33
+ const opened = requestAuthDialog({
34
+ mode: "login",
35
+ onSignedIn: () => ctx.showPane("chat"),
36
+ });
37
+ if (!opened) {
38
+ ctx.notify({ body: "Sign-in is not available right now.", type: "error" });
44
39
  }
45
- chatController.clearSession();
46
- await chatController.refreshSession();
47
- ctx.showPane("chat");
48
40
  },
49
41
  });
50
42
 
@@ -54,33 +46,15 @@ export function registerCloudAuthCommands(ctx: GloomPluginContext): void {
54
46
  description: "Create a Gloomberb account",
55
47
  keywords: ["signup", "register", "create account"],
56
48
  category: "config",
57
- wizardLayout: "form",
58
- hidden: () => !!apiClient.getSessionToken(),
59
- wizard: [
60
- { key: "email", label: "Email", type: "text", placeholder: "email@example.com" },
61
- {
62
- key: "username",
63
- label: "Username",
64
- type: "text",
65
- placeholder: "3-30 chars, starts with letter",
66
- body: ["Choose a username (3-30 characters, starts with a letter, alphanumeric and underscore only)"],
67
- },
68
- { key: "password", label: "Password", type: "password", placeholder: "Min 8 characters" },
69
- { key: "confirmPassword", label: "Confirm Password", type: "password", placeholder: "Re-enter password" },
70
- { key: "_validate", label: "Creating account...", type: "info", body: ["Registering with Gloomberb...", "Account created! Welcome to Gloomberb."] },
71
- ],
72
- execute: async (values) => {
73
- if (!values?.email || !values?.username || !values?.password) {
74
- throw new Error("All fields are required");
75
- }
76
- if (values.password !== values.confirmPassword) {
77
- throw new Error("Passwords do not match");
49
+ hidden: () => apiClient.isSignedIn(),
50
+ execute: () => {
51
+ const opened = requestAuthDialog({
52
+ mode: "signup",
53
+ onSignedIn: () => ctx.showPane("chat"),
54
+ });
55
+ if (!opened) {
56
+ ctx.notify({ body: "Sign-up is not available right now.", type: "error" });
78
57
  }
79
- await apiClient.signUp(values.email, values.username, values.username, values.password);
80
- await apiClient.sendVerification();
81
- chatController.clearSession();
82
- await chatController.refreshSession();
83
- ctx.showPane("chat");
84
58
  },
85
59
  });
86
60
 
@@ -92,7 +66,7 @@ export function registerCloudAuthCommands(ctx: GloomPluginContext): void {
92
66
  category: "config",
93
67
  hidden: () => {
94
68
  const user = chatController.getSnapshot().user;
95
- return !apiClient.getSessionToken() || !user || user.emailVerified;
69
+ return !apiClient.isSignedIn() || !user || user.emailVerified;
96
70
  },
97
71
  execute: async () => {
98
72
  await apiClient.sendVerification();
@@ -100,10 +74,6 @@ export function registerCloudAuthCommands(ctx: GloomPluginContext): void {
100
74
  },
101
75
  });
102
76
 
103
- if (apiClient.getSessionToken()) {
104
- void chatController.refreshSession().catch(() => {});
105
- }
106
-
107
77
  ctx.registerCommand({
108
78
  id: "auth-logout",
109
79
  label: "Logout",
@@ -111,7 +81,7 @@ export function registerCloudAuthCommands(ctx: GloomPluginContext): void {
111
81
  keywords: ["logout", "sign out"],
112
82
  category: "config",
113
83
  execute: async () => {
114
- if (!apiClient.getSessionToken()) {
84
+ if (!apiClient.isSignedIn()) {
115
85
  ctx.notify({ body: "Not logged in.", type: "error" });
116
86
  return;
117
87
  }
@@ -128,6 +98,6 @@ export function registerCloudAuthCommands(ctx: GloomPluginContext): void {
128
98
  type: "info",
129
99
  });
130
100
  },
131
- hidden: () => !apiClient.getSessionToken(),
101
+ hidden: () => !apiClient.isSignedIn(),
132
102
  });
133
103
  }
@@ -0,0 +1,296 @@
1
+ /**
2
+ * Shared email/password auth dialog plus the request bridge that lets commands
3
+ * (which run outside the React tree) open it. `AuthDialogHost` is mounted by
4
+ * the shell for the life of the app and owns the actual dialog, mirroring the
5
+ * QR sign-in dialog host.
6
+ */
7
+ import { useCallback, useState } from "react";
8
+ import { apiClient, type AuthUser } from "../../../api-client";
9
+ import { Button, Spinner, TextField } from "../../../components";
10
+ import { DialogFrame } from "../../../components/ui/frame";
11
+ import { t, tf } from "../../../i18n";
12
+ import { useAppLanguage } from "../../../i18n/react";
13
+ import { colors } from "../../../theme/colors";
14
+ import { Box, Text, TextAttributes } from "../../../ui";
15
+ import { useDialog, useDialogKeyboard, type PromptContext } from "../../../ui/dialog";
16
+ import { isPlainKey } from "../../../utils/keyboard";
17
+ import { useEffect, useRef } from "react";
18
+ import {
19
+ advanceAccountField,
20
+ classifyAccountError,
21
+ performEmailAuth,
22
+ validateAccountEmail,
23
+ validateAccountPassword,
24
+ type AccountMode,
25
+ type AccountSubmitError,
26
+ } from "./auth-model";
27
+
28
+ const FIELD_WIDTH = 42;
29
+
30
+ type AuthField = "email" | "password";
31
+
32
+ type ResetState = "idle" | "sending" | "sent";
33
+
34
+ export function AuthDialog({
35
+ initialMode,
36
+ resolve,
37
+ dismiss,
38
+ }: PromptContext<AuthUser | undefined> & { initialMode: AccountMode }) {
39
+ useAppLanguage();
40
+ const [mode, setMode] = useState<AccountMode>(initialMode);
41
+ const [email, setEmail] = useState("");
42
+ const [password, setPassword] = useState("");
43
+ const [activeField, setActiveField] = useState<AuthField>("email");
44
+ const [showPassword, setShowPassword] = useState(false);
45
+ const [submitting, setSubmitting] = useState(false);
46
+ const [validationError, setValidationError] = useState<string | null>(null);
47
+ const [submitError, setSubmitError] = useState<AccountSubmitError | null>(null);
48
+ const [resetState, setResetState] = useState<ResetState>("idle");
49
+ const attemptRef = useRef(0);
50
+
51
+ const clearErrors = useCallback(() => {
52
+ setValidationError(null);
53
+ setSubmitError(null);
54
+ }, []);
55
+
56
+ useEffect(() => () => {
57
+ // Unmounting abandons any in-flight attempt so a late response can't set state.
58
+ attemptRef.current += 1;
59
+ }, []);
60
+
61
+ const switchMode = useCallback((nextMode: AccountMode) => {
62
+ attemptRef.current += 1;
63
+ setMode(nextMode);
64
+ setSubmitting(false);
65
+ setPassword("");
66
+ setResetState("idle");
67
+ clearErrors();
68
+ setActiveField(email.trim() ? "password" : "email");
69
+ }, [clearErrors, email]);
70
+
71
+ const requestReset = useCallback(() => {
72
+ if (submitting || resetState === "sending") return;
73
+ const trimmedEmail = email.trim();
74
+ const emailError = validateAccountEmail(trimmedEmail);
75
+ if (emailError) {
76
+ setActiveField("email");
77
+ setValidationError(emailError);
78
+ return;
79
+ }
80
+ setResetState("sending");
81
+ clearErrors();
82
+ void apiClient.requestPasswordReset(trimmedEmail)
83
+ .then(() => setResetState("sent"))
84
+ .catch(() => {
85
+ setResetState("idle");
86
+ setSubmitError({ message: t("Could not send the reset email."), kind: "retry" });
87
+ });
88
+ }, [clearErrors, email, resetState, submitting]);
89
+
90
+ const submit = useCallback(() => {
91
+ if (submitting) return;
92
+ const trimmedEmail = email.trim();
93
+ const emailError = validateAccountEmail(trimmedEmail);
94
+ if (emailError) {
95
+ setActiveField("email");
96
+ setValidationError(emailError);
97
+ return;
98
+ }
99
+ const passwordError = validateAccountPassword(password, mode);
100
+ if (passwordError) {
101
+ setActiveField("password");
102
+ setValidationError(passwordError);
103
+ return;
104
+ }
105
+
106
+ const attemptId = attemptRef.current + 1;
107
+ attemptRef.current = attemptId;
108
+ setSubmitting(true);
109
+ clearErrors();
110
+ void (async () => {
111
+ try {
112
+ const user = await performEmailAuth(mode, trimmedEmail, password);
113
+ if (attemptRef.current !== attemptId) return;
114
+ resolve(user);
115
+ } catch (error) {
116
+ if (attemptRef.current !== attemptId) return;
117
+ setSubmitting(false);
118
+ setSubmitError(classifyAccountError(error, mode));
119
+ }
120
+ })();
121
+ }, [clearErrors, email, mode, password, resolve, submitting]);
122
+
123
+ const submitField = useCallback(() => {
124
+ const advance = advanceAccountField({
125
+ mode,
126
+ email: email.trim(),
127
+ password,
128
+ fieldIdx: activeField === "email" ? 0 : 1,
129
+ });
130
+ if (advance.action === "invalid") {
131
+ setValidationError(advance.message);
132
+ return;
133
+ }
134
+ if (advance.action === "next-field") {
135
+ setValidationError(null);
136
+ setActiveField("password");
137
+ return;
138
+ }
139
+ submit();
140
+ }, [activeField, email, mode, password, submit]);
141
+
142
+ useDialogKeyboard((event) => {
143
+ if (event.name === "escape") {
144
+ event.stopPropagation?.();
145
+ dismiss();
146
+ return;
147
+ }
148
+ const forward = isPlainKey(event, "tab") || (!event.targetEditable && isPlainKey(event, "down", "j"));
149
+ const backward = !event.targetEditable && isPlainKey(event, "up", "k");
150
+ if (forward || backward) {
151
+ event.preventDefault?.();
152
+ event.stopPropagation?.();
153
+ setActiveField(forward ? "password" : "email");
154
+ }
155
+ }, { allowEditable: true });
156
+
157
+ const switchToLogin = submitError?.kind === "switch-to-login";
158
+ const error = validationError ?? submitError?.message ?? null;
159
+
160
+ return (
161
+ <DialogFrame title={mode === "login" ? t("Log in to Gloom Cloud") : t("Create your free Gloom Cloud account")}>
162
+ <Box flexDirection="column" gap={1}>
163
+ <TextField
164
+ label={t("Email")}
165
+ value={email}
166
+ placeholder="email@example.com"
167
+ focused={activeField === "email" && !submitting}
168
+ width={FIELD_WIDTH}
169
+ type="email"
170
+ autoComplete="email"
171
+ onMouseDown={() => setActiveField("email")}
172
+ onChange={(value) => {
173
+ setEmail(value);
174
+ setResetState("idle");
175
+ clearErrors();
176
+ }}
177
+ onSubmit={submitField}
178
+ />
179
+ <Box flexDirection="column">
180
+ <Box height={1} width={FIELD_WIDTH} flexDirection="row" justifyContent="space-between">
181
+ <Text
182
+ fg={activeField === "password" ? colors.textBright : colors.textDim}
183
+ attributes={activeField === "password" ? TextAttributes.BOLD : 0}
184
+ >
185
+ {t("Password")}
186
+ </Text>
187
+ <Box onMouseDown={() => setShowPassword((current) => !current)}>
188
+ <Text fg={colors.textMuted}>{showPassword ? t("hide") : t("show")}</Text>
189
+ </Box>
190
+ </Box>
191
+ <TextField
192
+ value={password}
193
+ placeholder={mode === "signup" ? t("At least 8 characters") : t("Your password")}
194
+ focused={activeField === "password" && !submitting}
195
+ width={FIELD_WIDTH}
196
+ type={showPassword ? "text" : "password"}
197
+ autoComplete={mode === "signup" ? "new-password" : "current-password"}
198
+ onMouseDown={() => setActiveField("password")}
199
+ onChange={(value) => {
200
+ setPassword(value);
201
+ clearErrors();
202
+ }}
203
+ onSubmit={submitField}
204
+ />
205
+ </Box>
206
+ <Box flexDirection="column" minHeight={2} width={FIELD_WIDTH}>
207
+ {submitting ? (
208
+ <Spinner label={mode === "signup" ? t("Creating your account...") : t("Signing you in...")} />
209
+ ) : resetState === "sending" ? (
210
+ <Spinner label={t("Sending reset link...")} />
211
+ ) : error ? (
212
+ <Text fg={colors.negative} wrapText>{error}</Text>
213
+ ) : resetState === "sent" ? (
214
+ <Text fg={colors.positive} wrapText>
215
+ {tf("Reset link sent to {email}. Check your inbox.", { email: email.trim() })}
216
+ </Text>
217
+ ) : mode === "signup" ? (
218
+ <Text fg={colors.textMuted} wrapText>{t("We'll email you a verification link.")}</Text>
219
+ ) : null}
220
+ </Box>
221
+ <Box flexDirection="row" justifyContent="space-between" width={FIELD_WIDTH}>
222
+ <Button
223
+ label={switchToLogin || mode === "signup" ? t("Log in instead") : t("Sign up instead")}
224
+ variant="ghost"
225
+ disabled={submitting}
226
+ onPress={() => switchMode(mode === "login" ? "signup" : "login")}
227
+ />
228
+ <Button
229
+ label={mode === "login" ? t("Log In") : t("Create Account")}
230
+ variant="primary"
231
+ disabled={submitting}
232
+ onPress={submit}
233
+ />
234
+ </Box>
235
+ {mode === "login" && (
236
+ <Box flexDirection="row" width={FIELD_WIDTH}>
237
+ <Button
238
+ label={t("Forgot password?")}
239
+ variant="ghost"
240
+ disabled={submitting || resetState === "sending"}
241
+ onPress={requestReset}
242
+ />
243
+ </Box>
244
+ )}
245
+ </Box>
246
+ </DialogFrame>
247
+ );
248
+ }
249
+
250
+ export interface AuthDialogRequest {
251
+ mode?: AccountMode;
252
+ onSignedIn?: (user: AuthUser) => void;
253
+ }
254
+
255
+ const authDialogListeners = new Set<(request: AuthDialogRequest) => void>();
256
+
257
+ /** Returns false when no dialog host is mounted, so callers can fall back. */
258
+ export function requestAuthDialog(request: AuthDialogRequest = {}): boolean {
259
+ if (authDialogListeners.size === 0) return false;
260
+ for (const listener of authDialogListeners) listener(request);
261
+ return true;
262
+ }
263
+
264
+ export function AuthDialogHost() {
265
+ const dialog = useDialog();
266
+ const openRef = useRef(false);
267
+
268
+ useEffect(() => {
269
+ const listener = (request: AuthDialogRequest) => {
270
+ if (openRef.current) return;
271
+ openRef.current = true;
272
+ void dialog
273
+ .prompt<AuthUser | undefined>({
274
+ closeOnClickOutside: false,
275
+ content: (context: unknown) => (
276
+ <AuthDialog
277
+ {...(context as PromptContext<AuthUser | undefined>)}
278
+ initialMode={request.mode ?? "login"}
279
+ />
280
+ ),
281
+ })
282
+ .then((user) => {
283
+ if (user) request.onSignedIn?.(user);
284
+ })
285
+ .finally(() => {
286
+ openRef.current = false;
287
+ });
288
+ };
289
+ authDialogListeners.add(listener);
290
+ return () => {
291
+ authDialogListeners.delete(listener);
292
+ };
293
+ }, [dialog]);
294
+
295
+ return null;
296
+ }
@@ -4,7 +4,7 @@ import {
4
4
  classifyAccountError,
5
5
  deriveUsernameFromEmail,
6
6
  isUsernameConflictError,
7
- } from "./model";
7
+ } from "./auth-model";
8
8
 
9
9
  describe("deriveUsernameFromEmail", () => {
10
10
  test("builds a server-legal username from the email local part", () => {