gloomberb 0.9.7 → 0.9.9

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 (276) hide show
  1. package/package.json +1 -1
  2. package/src/api-client/auth.ts +20 -0
  3. package/src/api-client/index.test.ts +3 -3
  4. package/src/api-client/index.ts +48 -6
  5. package/src/api-client/request.test.ts +63 -0
  6. package/src/api-client/request.ts +53 -5
  7. package/src/api-client/types.ts +45 -2
  8. package/src/app/pane-runtime/layout-placement.ts +0 -1
  9. package/src/app/runtime/desktop-deeplink.test.ts +87 -0
  10. package/src/app/runtime/desktop-deeplink.ts +496 -0
  11. package/src/app.tsx +34 -0
  12. package/src/brokers/account-cache.ts +2 -10
  13. package/src/brokers/broker-portfolio-sync.ts +0 -1
  14. package/src/brokers/broker-ticker-sync.ts +20 -16
  15. package/src/brokers/remote-broker-adapter.ts +3 -0
  16. package/src/capabilities/registry.test.ts +25 -0
  17. package/src/capabilities/registry.ts +22 -5
  18. package/src/cli/commands/automation.ts +1 -3
  19. package/src/cli/commands/command-utils.ts +0 -7
  20. package/src/cli/commands/overview.ts +2 -2
  21. package/src/cli/commands/plugins.ts +34 -17
  22. package/src/cli/index.ts +2 -2
  23. package/src/cli.test.ts +0 -157
  24. package/src/components/chart/bar-chart-renderer.test.ts +87 -12
  25. package/src/components/chart/bar-chart-renderer.ts +111 -55
  26. package/src/components/chart/comparison/renderer.ts +0 -1
  27. package/src/components/chart/comparison-stock-chart/render-output.ts +3 -8
  28. package/src/components/chart/comparison-stock-chart/view.tsx +0 -2
  29. package/src/components/chart/core/index.test.ts +0 -24
  30. package/src/components/chart/core/renderer.ts +0 -4
  31. package/src/components/chart/core/resolution.test.ts +0 -17
  32. package/src/components/chart/core/resolution.ts +0 -26
  33. package/src/components/chart/core/scroll.test.ts +0 -6
  34. package/src/components/chart/core/scroll.ts +0 -22
  35. package/src/components/chart/core/viewport.test.ts +2 -23
  36. package/src/components/chart/core/viewport.ts +0 -27
  37. package/src/components/chart/market-session.ts +1 -1
  38. package/src/components/chart/native/surface/hooks.ts +10 -3
  39. package/src/components/chart/static/bar-chart-surface.tsx +104 -20
  40. package/src/components/chart/stock-chart/data-runtime.ts +0 -1
  41. package/src/components/chart/stock-chart/footer.test.ts +65 -0
  42. package/src/components/chart/stock-chart/footer.ts +68 -13
  43. package/src/components/chart/stock-chart/resolved.tsx +18 -4
  44. package/src/components/chart/stock-chart/viewport/effects.ts +0 -3
  45. package/src/components/command-bar/helpers.test.ts +1 -80
  46. package/src/components/command-bar/helpers.ts +1 -36
  47. package/src/components/command-bar/pane-settings.ts +1 -1
  48. package/src/components/command-bar/pane-templates/items.ts +11 -3
  49. package/src/components/command-bar/panel/runtime.ts +0 -2
  50. package/src/components/command-bar/routes/root/results.ts +1 -1
  51. package/src/components/command-bar/routes/ticker-search/results.ts +0 -11
  52. package/src/components/command-bar/surface/index.test.tsx +52 -16
  53. package/src/components/command-bar/surface/index.tsx +0 -1
  54. package/src/components/command-bar/workflow/ops.ts +81 -20
  55. package/src/components/feed-data-table/stack-view.test.tsx +36 -130
  56. package/src/components/feed-data-table/stack-view.tsx +0 -1
  57. package/src/components/index.ts +1 -0
  58. package/src/components/layout/shell/index.test.tsx +2 -113
  59. package/src/components/layout/shell/index.tsx +0 -1
  60. package/src/components/layout/shell/menu.ts +0 -1
  61. package/src/components/layout/shell/terminal-pointer-runtime.ts +0 -1
  62. package/src/components/onboarding/onboarding-wizard.tsx +3 -3
  63. package/src/components/onboarding/wizard-keyboard.ts +2 -23
  64. package/src/components/price-selector-dialog.tsx +3 -3
  65. package/src/components/ticker/badge/text.tsx +0 -1
  66. package/src/components/toggle-list.tsx +29 -73
  67. package/src/components/ui/button.tsx +4 -1
  68. package/src/components/ui/checkbox.tsx +76 -0
  69. package/src/components/ui/choice-dialog.tsx +15 -3
  70. package/src/components/ui/frame.tsx +1 -1
  71. package/src/components/ui/index.ts +2 -0
  72. package/src/components/ui/message-composer.tsx +4 -0
  73. package/src/components/ui/native-select.tsx +4 -2
  74. package/src/components/ui/page-stack-view.tsx +1 -1
  75. package/src/components/ui/ui.test.tsx +0 -78
  76. package/src/core/state/app/layout-reducer.ts +0 -1
  77. package/src/data/config/layout.ts +3 -3
  78. package/src/data/resource-store.ts +6 -0
  79. package/src/data/sqlite/cache.test.ts +5 -0
  80. package/src/data/ticker-store.ts +0 -7
  81. package/src/market-data/coordinator/auxiliary.ts +1 -44
  82. package/src/market-data/coordinator/entries.ts +0 -1
  83. package/src/market-data/coordinator/events.test.ts +49 -0
  84. package/src/market-data/coordinator/events.ts +25 -4
  85. package/src/market-data/coordinator/index.test.ts +223 -10
  86. package/src/market-data/coordinator/index.ts +41 -21
  87. package/src/market-data/coordinator/quotes.ts +13 -11
  88. package/src/market-data/coordinator/subscriptions.test.ts +1 -0
  89. package/src/market-data/hooks.tsx +17 -13
  90. package/src/market-data/market/freshness.ts +58 -1
  91. package/src/market-data/market/status.ts +19 -1
  92. package/src/market-data/query-store.ts +0 -4
  93. package/src/market-data/quotes/freshness.test.ts +49 -1
  94. package/src/market-data/quotes/freshness.ts +8 -0
  95. package/src/market-data/quotes/resolution.test.ts +185 -0
  96. package/src/market-data/quotes/resolution.ts +96 -10
  97. package/src/market-data/request-types.test.ts +61 -1
  98. package/src/market-data/request-types.ts +42 -8
  99. package/src/market-data/result-types.ts +0 -11
  100. package/src/market-data/selectors.ts +1 -5
  101. package/src/news/aggregator.ts +5 -1
  102. package/src/pane-settings.test.ts +0 -18
  103. package/src/plugins/builtin/account-management/footer.ts +2 -5
  104. package/src/plugins/builtin/account-management/form-components.tsx +175 -59
  105. package/src/plugins/builtin/account-management/keyboard.ts +32 -2
  106. package/src/plugins/builtin/account-management/model.test.ts +99 -4
  107. package/src/plugins/builtin/account-management/model.ts +259 -23
  108. package/src/plugins/builtin/account-management/pane.tsx +784 -155
  109. package/src/plugins/builtin/broker-manager/index.test.tsx +0 -32
  110. package/src/plugins/builtin/buildout/cells.tsx +1 -1
  111. package/src/plugins/builtin/buildout/detail/satellite-images.tsx +1 -1
  112. package/src/plugins/builtin/buildout/detail/ui.tsx +0 -1
  113. package/src/plugins/builtin/buildout/format.ts +2 -11
  114. package/src/plugins/builtin/chat/chat.test.tsx +127 -1
  115. package/src/plugins/builtin/chat/content/channel-navigation.ts +1 -21
  116. package/src/plugins/builtin/chat/content/composer-cursor.ts +29 -0
  117. package/src/plugins/builtin/chat/content/composer-runtime.ts +22 -24
  118. package/src/plugins/builtin/chat/content/composer.tsx +84 -1
  119. package/src/plugins/builtin/chat/content/index.tsx +153 -16
  120. package/src/plugins/builtin/chat/content/layout-metrics.ts +10 -1
  121. package/src/plugins/builtin/chat/content/mentions.test.ts +74 -0
  122. package/src/plugins/builtin/chat/content/mentions.ts +88 -0
  123. package/src/plugins/builtin/chat/content/new-dm-dialog.tsx +264 -0
  124. package/src/plugins/builtin/chat/content/shortcuts.ts +47 -1
  125. package/src/plugins/builtin/chat/content/snapshot.ts +7 -0
  126. package/src/plugins/builtin/chat/content/transcript.tsx +0 -5
  127. package/src/plugins/builtin/chat/controller/index.ts +0 -10
  128. package/src/plugins/builtin/chat/controller.test.ts +5 -5
  129. package/src/plugins/builtin/chat/message/profile-popover.test.tsx +36 -0
  130. package/src/plugins/builtin/chat/message/profile-popover.tsx +124 -20
  131. package/src/plugins/builtin/chat/sidebar.test.tsx +83 -6
  132. package/src/plugins/builtin/chat/sidebar.tsx +26 -2
  133. package/src/plugins/builtin/chat/test-harness.tsx +22 -12
  134. package/src/plugins/builtin/cloud/plugin.tsx +8 -1
  135. package/src/plugins/builtin/cloud-tweets/pane.tsx +0 -1
  136. package/src/plugins/builtin/congress-trades/model.ts +2 -7
  137. package/src/plugins/builtin/congress-trades/pane.tsx +0 -1
  138. package/src/plugins/builtin/correlation/index.tsx +4 -1
  139. package/src/plugins/builtin/correlation/relationship/controls.tsx +8 -2
  140. package/src/plugins/builtin/correlation/relationship/model.ts +22 -2
  141. package/src/plugins/builtin/correlation/relationship/pane.tsx +9 -3
  142. package/src/plugins/builtin/earnings/index.tsx +0 -9
  143. package/src/plugins/builtin/econ/index.tsx +1 -5
  144. package/src/plugins/builtin/help/index.tsx +2 -2
  145. package/src/plugins/builtin/holders/pane.tsx +0 -5
  146. package/src/plugins/builtin/insider/index.tsx +1 -8
  147. package/src/plugins/builtin/kelly-sizer/index.test.tsx +0 -26
  148. package/src/plugins/builtin/kelly-sizer/index.tsx +0 -2
  149. package/src/plugins/builtin/market-movers/index.tsx +0 -2
  150. package/src/plugins/builtin/news/index.tsx +0 -1
  151. package/src/plugins/builtin/news/wire/news/table.tsx +0 -11
  152. package/src/plugins/builtin/options/view.test.tsx +1 -38
  153. package/src/plugins/builtin/plugin-groups.ts +17 -3
  154. package/src/plugins/builtin/portfolio-list/column-values.ts +15 -8
  155. package/src/plugins/builtin/portfolio-list/index.test.ts +52 -25
  156. package/src/plugins/builtin/portfolio-list/metrics.test.ts +44 -7
  157. package/src/plugins/builtin/portfolio-list/pane/data.test.ts +54 -0
  158. package/src/plugins/builtin/portfolio-list/pane/data.ts +50 -3
  159. package/src/plugins/builtin/portfolio-list/pane/index.test.tsx +314 -16
  160. package/src/plugins/builtin/portfolio-list/pane/index.tsx +11 -4
  161. package/src/plugins/builtin/portfolio-list/pane/streaming.ts +83 -24
  162. package/src/plugins/builtin/sec/filing-documents.ts +0 -11
  163. package/src/plugins/builtin/sec/index.tsx +0 -2
  164. package/src/plugins/builtin/substack/article-detail.tsx +1 -1
  165. package/src/plugins/builtin/thirteenf/api.ts +0 -15
  166. package/src/plugins/builtin/thirteenf/pane.tsx +1 -3
  167. package/src/plugins/builtin/ticker-detail/data-panes/fundamental-graph/content.tsx +42 -6
  168. package/src/plugins/builtin/ticker-detail/data-panes/fundamental-graph/model.ts +415 -64
  169. package/src/plugins/builtin/ticker-detail/data-panes/fundamental-graph/settings.ts +23 -2
  170. package/src/plugins/builtin/ticker-detail/data-panes/fundamental-graph/types.ts +8 -1
  171. package/src/plugins/builtin/ticker-detail/data-panes/fundamental-graph.tsx +3 -1
  172. package/src/plugins/builtin/ticker-detail/data-panes.test.ts +95 -1
  173. package/src/plugins/builtin/ticker-detail/financials/tab.tsx +0 -7
  174. package/src/plugins/builtin/ticker-detail/index.tsx +2 -0
  175. package/src/plugins/builtin/ticker-detail/quote-monitor/index.test.tsx +5 -34
  176. package/src/plugins/builtin/world-indices/index.tsx +0 -4
  177. package/src/plugins/builtin/world-indices/model.ts +0 -7
  178. package/src/plugins/ibkr/account-cache.ts +1 -1
  179. package/src/plugins/ibkr/gateway/market-data.ts +12 -5
  180. package/src/plugins/ibkr/gateway/service/index.test.ts +31 -0
  181. package/src/plugins/ibkr/portfolio-performance.ts +1 -1
  182. package/src/plugins/ibkr/trade/utils.ts +1 -7
  183. package/src/plugins/loader.ts +2 -1
  184. package/src/plugins/prediction-markets/detail.test.tsx +0 -34
  185. package/src/plugins/registry/context.ts +15 -0
  186. package/src/plugins/registry/contributions.ts +22 -11
  187. package/src/plugins/registry/index.test.ts +44 -0
  188. package/src/plugins/registry/index.ts +75 -26
  189. package/src/remote/controller.ts +1 -7
  190. package/src/remote/json-patch.ts +15 -1
  191. package/src/remote/server.ts +16 -6
  192. package/src/renderers/electrobun/bun/application-menu/index.ts +1 -1
  193. package/src/renderers/electrobun/bun/core-capabilities.ts +2 -0
  194. package/src/renderers/electrobun/bun/desktop/http-fetch.test.ts +14 -0
  195. package/src/renderers/electrobun/bun/desktop/http-fetch.ts +8 -0
  196. package/src/renderers/electrobun/bun/desktop/initialization.ts +2 -1
  197. package/src/renderers/electrobun/bun/index.ts +42 -0
  198. package/src/renderers/electrobun/shared/protocol.ts +5 -0
  199. package/src/renderers/electrobun/view/backend-rpc.ts +29 -0
  200. package/src/renderers/electrobun/view/cli-pane-shot-entry.tsx +1 -0
  201. package/src/renderers/electrobun/view/data-table/dom.ts +0 -1
  202. package/src/renderers/electrobun/view/desktop/controls.tsx +135 -4
  203. package/src/renderers/electrobun/view/desktop/list-view.tsx +38 -25
  204. package/src/renderers/electrobun/view/desktop-deeplink-bridge.ts +10 -0
  205. package/src/renderers/electrobun/view/host/box.tsx +0 -2
  206. package/src/renderers/electrobun/view/host/mouse.ts +0 -11
  207. package/src/renderers/electrobun/view/host/scroll-box.tsx +1 -2
  208. package/src/renderers/electrobun/view/host/text.tsx +1 -2
  209. package/src/renderers/electrobun/view/http-fetch.ts +15 -2
  210. package/src/renderers/electrobun/view/main.tsx +3 -0
  211. package/src/renderers/electrobun/view/remote/asset-data-client.ts +9 -18
  212. package/src/renderers/electrobun/view/remote/capability-invoker.test.ts +50 -0
  213. package/src/renderers/electrobun/view/remote/capability-invoker.ts +36 -0
  214. package/src/renderers/electrobun/view/ui-host.tsx +2 -0
  215. package/src/renderers/opentui/host.tsx +11 -3
  216. package/src/renderers/opentui/input-host.tsx +5 -1
  217. package/src/sources/gloomberb-cloud/index.test.ts +0 -72
  218. package/src/sources/gloomberb-cloud/index.ts +1 -30
  219. package/src/sources/gloomberb-cloud/news.ts +0 -12
  220. package/src/sources/provider-router/batches.ts +11 -3
  221. package/src/sources/provider-router/cached-financials.test.ts +123 -0
  222. package/src/sources/provider-router/financial-routes.ts +77 -5
  223. package/src/sources/provider-router/financials.test.ts +49 -0
  224. package/src/sources/provider-router/financials.ts +40 -3
  225. package/src/sources/provider-router/index.test.ts +314 -0
  226. package/src/sources/provider-router/primary.ts +17 -3
  227. package/src/sources/provider-router/search.ts +5 -0
  228. package/src/sources/provider-router/streaming.ts +6 -1
  229. package/src/sources/provider-router/supplemental.ts +4 -3
  230. package/src/sources/sec-edgar.test.ts +42 -0
  231. package/src/sources/sec-edgar.ts +1 -2
  232. package/src/sources/yahoo-finance/history.ts +5 -1
  233. package/src/sources/yahoo-finance/mappers.test.ts +29 -0
  234. package/src/sources/yahoo-finance/mappers.ts +2 -1
  235. package/src/sources/yahoo-finance/options.ts +59 -14
  236. package/src/sources/yahoo-finance/quote-summary.ts +4 -1
  237. package/src/sources/yahoo-finance/requests.ts +2 -1
  238. package/src/sources/yahoo-finance/snapshots.ts +13 -2
  239. package/src/sources/yahoo-finance.test.ts +40 -21
  240. package/src/sources/yahoo-finance.ts +19 -10
  241. package/src/state/app/context/index.test.tsx +0 -81
  242. package/src/state/app/context/selector.test.tsx +6 -2
  243. package/src/state/hooks/quote-streaming.test.ts +14 -1
  244. package/src/state/hooks/quote-streaming.ts +5 -4
  245. package/src/sync/controller.test.ts +31 -0
  246. package/src/sync/controller.ts +300 -0
  247. package/src/sync/core-contributors.test.ts +251 -0
  248. package/src/sync/core-contributors.ts +400 -0
  249. package/src/sync/profile-analytics.ts +39 -0
  250. package/src/sync/react.ts +53 -0
  251. package/src/sync/types.ts +77 -0
  252. package/src/test-support/data-provider.ts +0 -1
  253. package/src/types/desktop-deeplink.ts +7 -0
  254. package/src/types/plugin.ts +3 -0
  255. package/src/ui/host.tsx +15 -1
  256. package/src/ui/index.tsx +2 -0
  257. package/src/updater.test.ts +0 -21
  258. package/src/updater.ts +20 -16
  259. package/src/utils/async-deadline.ts +26 -0
  260. package/src/utils/exchanges.test.ts +11 -1
  261. package/src/utils/exchanges.ts +5 -1
  262. package/src/utils/selection-clipboard.ts +2 -2
  263. package/src/version.ts +1 -1
  264. package/src/components/layout/pane/header.test.tsx +0 -39
  265. package/src/plugins/builtin/chat/channels.test.ts +0 -28
  266. package/src/plugins/builtin/chat/index.tsx +0 -4
  267. package/src/plugins/builtin/cloud-tweets/index.ts +0 -1
  268. package/src/plugins/builtin/earnings/index.test.ts +0 -12
  269. package/src/plugins/builtin/help/index.test.tsx +0 -242
  270. package/src/plugins/builtin/substack/article-detail.test.tsx +0 -10
  271. package/src/renderers/electrobun/bun/application-menu/index.test.ts +0 -21
  272. package/src/renderers/electrobun/bun/window/focus.test.ts +0 -35
  273. package/src/renderers/electrobun/view/host/mouse.test.ts +0 -37
  274. package/src/renderers/electrobun/view/host/text.test.ts +0 -22
  275. package/src/renderers/opentui/dialog-host.test.tsx +0 -44
  276. /package/src/{plugins/ibkr → utils}/hash.ts +0 -0
@@ -1,8 +1,9 @@
1
- import { useState, type ReactNode } from "react";
2
- import { TextField } from "../../../components";
3
- import { Box, Text, TextAttributes } from "../../../ui";
4
- import { colors, hoverBg } from "../../../theme/colors";
5
- import type { AccountFieldKey } from "./model";
1
+ import { type ReactNode } from "react";
2
+ import { Button, Checkbox, TextField, type ChoiceDialogChoice } from "../../../components";
3
+ import { Box, Text, TextAttributes, useUiHost } from "../../../ui";
4
+ import { colors } from "../../../theme/colors";
5
+ import { NativeSelect, type NativeSelectElement } from "../../../components/ui/native-select";
6
+ import type { AccountFieldKey, ProfileAnalyticsPreview } from "./model";
6
7
  import { truncate } from "./model";
7
8
 
8
9
  export function AccountTextField({
@@ -31,14 +32,30 @@ export function AccountTextField({
31
32
  onSubmit?: () => void;
32
33
  }) {
33
34
  const active = activeField === fieldKey;
35
+ const labelWidth = accountFieldLabelWidth(width);
36
+ const inputWidth = Math.max(8, width - labelWidth - 1);
37
+ const labelText = `${active ? "> " : " "}${label}`;
34
38
  return (
35
- <Box onMouseDown={() => onFocus(fieldKey)}>
39
+ <Box
40
+ height={1}
41
+ width={width}
42
+ flexDirection="row"
43
+ alignItems="center"
44
+ gap={1}
45
+ onMouseDown={() => onFocus(fieldKey)}
46
+ >
47
+ <Text
48
+ width={labelWidth}
49
+ fg={active ? colors.textBright : colors.textDim}
50
+ attributes={active ? TextAttributes.BOLD : 0}
51
+ >
52
+ {truncate(labelText, labelWidth)}
53
+ </Text>
36
54
  <TextField
37
- label={`${active ? "> " : " "}${label}`}
38
55
  value={value}
39
56
  placeholder={placeholder}
40
57
  focused={focused && active}
41
- width={width}
58
+ width={inputWidth}
42
59
  type={type}
43
60
  onChange={onChange}
44
61
  onSubmit={onSubmit}
@@ -48,6 +65,11 @@ export function AccountTextField({
48
65
  );
49
66
  }
50
67
 
68
+ export function accountFieldLabelWidth(width: number) {
69
+ const preferred = width >= 28 ? 16 : Math.max(10, Math.floor(width * 0.42));
70
+ return Math.max(8, Math.min(preferred, width - 9));
71
+ }
72
+
51
73
  export function FieldRow({
52
74
  twoColumns,
53
75
  children,
@@ -62,95 +84,189 @@ export function FieldRow({
62
84
  );
63
85
  }
64
86
 
65
- export function PickerRow({
87
+ export function CheckboxRow({
66
88
  label,
67
- value,
68
- detail,
89
+ checked,
69
90
  active,
91
+ description,
70
92
  width,
71
93
  onFocus,
72
- onOpen,
94
+ onChange,
73
95
  }: {
74
96
  label: string;
75
- value: string;
76
- detail?: string;
97
+ checked: boolean;
77
98
  active: boolean;
99
+ description?: string;
78
100
  width: number;
79
101
  onFocus: () => void;
80
- onOpen: () => void;
102
+ onChange: (checked: boolean) => void;
81
103
  }) {
82
- const buttonWidth = Math.max(12, Math.min(32, width - 18));
83
- const detailWidth = Math.max(0, width - buttonWidth - 4);
84
-
104
+ const labelText = `${active ? "> " : " "}${label}`;
105
+ const labelWidth = Math.min(
106
+ width - 9,
107
+ Math.max(accountFieldLabelWidth(width), Math.min(labelText.length, 34)),
108
+ );
109
+ const controlWidth = Math.max(8, width - labelWidth - 1);
85
110
  return (
86
111
  <Box
87
112
  flexDirection="column"
113
+ width={width}
88
114
  onMouseOver={onFocus}
89
- onMouseDown={(event?: { stopPropagation?: () => void; preventDefault?: () => void }) => {
90
- event?.stopPropagation?.();
115
+ onMouseDown={(event?: { preventDefault?: () => void; stopPropagation?: () => void }) => {
91
116
  event?.preventDefault?.();
117
+ event?.stopPropagation?.();
92
118
  onFocus();
93
- onOpen();
119
+ onChange(!checked);
94
120
  }}
95
121
  >
96
- <Text fg={active ? colors.textBright : colors.textDim} attributes={active ? TextAttributes.BOLD : 0}>
97
- {active ? `> ${label}` : ` ${label}`}
98
- </Text>
99
- <Box height={1} flexDirection="row" gap={1}>
100
- <Box width={buttonWidth} backgroundColor={active ? colors.selected : colors.panel}>
101
- <Text fg={active ? colors.selectedText : colors.text}>
102
- {` ${truncate(value, Math.max(1, buttonWidth - 2))} `}
122
+ <Box height={1} flexDirection="row" alignItems="center" gap={1}>
123
+ <Text
124
+ width={labelWidth}
125
+ fg={active ? colors.textBright : colors.textDim}
126
+ attributes={active ? TextAttributes.BOLD : 0}
127
+ >
128
+ {truncate(labelText, labelWidth)}
129
+ </Text>
130
+ <Checkbox
131
+ label={label}
132
+ displayLabel={checked ? "On" : "Off"}
133
+ checked={checked}
134
+ active={false}
135
+ width={controlWidth}
136
+ variant="desktop"
137
+ onChange={(nextChecked) => {
138
+ onFocus();
139
+ onChange(nextChecked);
140
+ }}
141
+ />
142
+ </Box>
143
+ {description ? (
144
+ <Box height={1} flexDirection="row" gap={1}>
145
+ <Box width={labelWidth} />
146
+ <Text fg={colors.textMuted} wrapText width={controlWidth}>
147
+ {description}
103
148
  </Text>
104
149
  </Box>
105
- {detail ? (
106
- <Text fg={colors.textMuted}>{truncate(detail, detailWidth)}</Text>
107
- ) : null}
108
- </Box>
150
+ ) : null}
109
151
  </Box>
110
152
  );
111
153
  }
112
154
 
113
- export function CheckboxRow({
155
+ function metricColor(tone: ProfileAnalyticsPreview["metrics"][number]["tone"]): string {
156
+ if (tone === "positive") return colors.positive;
157
+ if (tone === "negative") return colors.negative;
158
+ if (tone === "muted") return colors.textMuted;
159
+ return colors.text;
160
+ }
161
+
162
+ export function PublicAnalyticsGroup({
163
+ preview,
164
+ choices,
165
+ value,
114
166
  label,
115
- checked,
167
+ detail,
116
168
  active,
117
- description,
118
169
  width,
170
+ disclaimer,
171
+ selectRef,
119
172
  onFocus,
120
- onChange,
173
+ onSelect,
174
+ onOpen,
121
175
  }: {
176
+ preview: ProfileAnalyticsPreview;
177
+ choices: ChoiceDialogChoice[];
178
+ value: string;
122
179
  label: string;
123
- checked: boolean;
180
+ detail?: string;
124
181
  active: boolean;
125
- description?: string;
126
182
  width: number;
183
+ disclaimer?: string | null;
184
+ selectRef?: (element: NativeSelectElement | null) => void;
127
185
  onFocus: () => void;
128
- onChange: (checked: boolean) => void;
186
+ onSelect: (value: string) => void;
187
+ onOpen: () => void;
129
188
  }) {
130
- const [hovered, setHovered] = useState(false);
131
- const marker = checked ? "x" : " ";
132
- const fg = active ? colors.textBright : colors.text;
133
-
189
+ const isDesktop = useUiHost().kind === "desktop-web";
190
+ const contentWidth = Math.max(1, width - 2);
191
+ const labelText = active ? "> Public Stats:" : " Public Stats:";
192
+ const labelWidth = Math.min(accountFieldLabelWidth(width), Math.max(1, contentWidth));
193
+ const nativeButtonWidth = Math.max(14, Math.min(24, Math.floor(contentWidth * 0.3)));
194
+ const terminalMaxButtonWidth = Math.max(8, Math.min(24, contentWidth - labelWidth - 1));
195
+ const buttonLabel = truncate(label, Math.max(1, terminalMaxButtonWidth - 4));
196
+ const buttonWidth = buttonLabel.length + 4;
197
+ const layoutButtonWidth = isDesktop ? nativeButtonWidth : buttonWidth;
198
+ const nativeSelectWidth = nativeButtonWidth * 8;
199
+ const normalizedDetail = (detail ?? "").replace(/\.+$/, "");
200
+ const displayPreview = (
201
+ preview.metrics.length === 0
202
+ && normalizedDetail
203
+ && preview.subtitle.replace(/\.+$/, "") === normalizedDetail
204
+ ) ? { ...preview, subtitle: "" } : preview;
205
+ const metrics = displayPreview.metrics.slice(0, 2);
206
+ const metricAreaWidth = Math.max(0, contentWidth - labelWidth - layoutButtonWidth - 2);
207
+ const metricWidth = metrics.length > 0 ? Math.max(8, Math.floor((metricAreaWidth - (metrics.length - 1)) / metrics.length)) : 0;
208
+ const detailWidth = Math.max(0, metricAreaWidth);
134
209
  return (
135
210
  <Box
136
211
  flexDirection="column"
137
- backgroundColor={hovered ? hoverBg() : undefined}
138
- onMouseOver={() => {
139
- setHovered(true);
140
- onFocus();
141
- }}
142
- onMouseOut={() => setHovered(false)}
143
- onMouseDown={() => {
144
- onFocus();
145
- onChange(!checked);
146
- }}
212
+ width={width}
213
+ onMouseOver={onFocus}
147
214
  >
148
- <Text fg={fg} attributes={active ? TextAttributes.BOLD : 0}>
149
- {`${active ? "> " : " "}[${marker}] ${label}`}
150
- </Text>
151
- {description ? (
152
- <Text fg={colors.textMuted} wrapText width={Math.max(24, width - 2)}>
153
- {description}
215
+ <Box height={isDesktop ? "24px" : 1} flexDirection="row" gap={1} alignItems="center">
216
+ <Text fg={active ? colors.textBright : colors.textDim} attributes={active ? TextAttributes.BOLD : 0}>
217
+ {truncate(labelText, labelWidth)}
218
+ </Text>
219
+ {isDesktop ? (
220
+ <NativeSelect
221
+ value={value}
222
+ options={choices.map((choice) => ({
223
+ value: choice.id,
224
+ label: choice.label,
225
+ disabled: choice.disabled,
226
+ }))}
227
+ width={nativeSelectWidth}
228
+ height={22}
229
+ selectRef={selectRef}
230
+ onFocus={onFocus}
231
+ onChange={onSelect}
232
+ />
233
+ ) : (
234
+ <Button
235
+ label={`${buttonLabel} v`}
236
+ variant="secondary"
237
+ width={buttonWidth}
238
+ active={active}
239
+ onPress={() => {
240
+ onFocus();
241
+ onOpen();
242
+ }}
243
+ />
244
+ )}
245
+ {metrics.length > 0 ? metrics.map((metric) => {
246
+ const labelTextWidth = Math.max(1, Math.min(metric.label.length, metricWidth - 2));
247
+ const valueWidth = Math.max(1, metricWidth - labelTextWidth - 1);
248
+ return (
249
+ <Box key={metric.id} width={metricWidth} height={1} flexDirection="row" gap={1}>
250
+ <Text fg={colors.textDim}>{truncate(metric.label, labelTextWidth)}</Text>
251
+ <Text fg={metricColor(metric.tone)} attributes={TextAttributes.BOLD}>
252
+ {truncate(metric.value, valueWidth)}
253
+ </Text>
254
+ </Box>
255
+ );
256
+ }) : detail ? (
257
+ <Text fg={colors.textMuted}>
258
+ {truncate(detail, detailWidth)}
259
+ </Text>
260
+ ) : null}
261
+ </Box>
262
+ {metrics.length === 0 && displayPreview.subtitle ? (
263
+ <Text fg={colors.textMuted} wrapText width={contentWidth}>
264
+ {displayPreview.subtitle}
265
+ </Text>
266
+ ) : null}
267
+ {disclaimer ? (
268
+ <Text fg={colors.textMuted} wrapText width={contentWidth}>
269
+ {disclaimer}
154
270
  </Text>
155
271
  ) : null}
156
272
  </Box>
@@ -10,8 +10,11 @@ export function useAccountManagementKeyboard({
10
10
  focused,
11
11
  openPasswordDialog,
12
12
  openPortfolioDialog,
13
+ openUpgrade,
13
14
  saveProfile,
14
15
  setDraftValue,
16
+ deleteAccount,
17
+ turnOffEmailAlerts,
15
18
  }: {
16
19
  activeField: AccountFieldKey;
17
20
  cycleField: (delta: number) => void;
@@ -20,8 +23,11 @@ export function useAccountManagementKeyboard({
20
23
  focused: boolean;
21
24
  openPasswordDialog: () => void;
22
25
  openPortfolioDialog: () => Promise<void>;
26
+ openUpgrade: () => void;
23
27
  saveProfile: () => Promise<void>;
24
28
  setDraftValue: <K extends keyof AccountDraft>(key: K, value: AccountDraft[K]) => void;
29
+ deleteAccount: () => Promise<void>;
30
+ turnOffEmailAlerts: () => Promise<void>;
25
31
  }) {
26
32
  useShortcut((event) => {
27
33
  if (!focused) return;
@@ -57,6 +63,18 @@ export function useAccountManagementKeyboard({
57
63
  setDraftValue("acceptUnknownDms", !draftRef.current.acceptUnknownDms);
58
64
  return;
59
65
  }
66
+ if (!event.targetEditable && activeField === "weeklyRoundupEnabled" && isPlainKey(event, "space", "enter", "return")) {
67
+ event.preventDefault?.();
68
+ event.stopPropagation?.();
69
+ setDraftValue("weeklyRoundupEnabled", !draftRef.current.weeklyRoundupEnabled);
70
+ return;
71
+ }
72
+ if (!event.targetEditable && activeField === "positionAlertsEnabled" && isPlainKey(event, "space", "enter", "return")) {
73
+ event.preventDefault?.();
74
+ event.stopPropagation?.();
75
+ setDraftValue("positionAlertsEnabled", !draftRef.current.positionAlertsEnabled);
76
+ return;
77
+ }
60
78
  if (!event.targetEditable && activeField === "sharedPortfolioId" && isPlainKey(event, "left", "h", "[")) {
61
79
  event.preventDefault?.();
62
80
  event.stopPropagation?.();
@@ -81,10 +99,22 @@ export function useAccountManagementKeyboard({
81
99
  openPasswordDialog();
82
100
  return;
83
101
  }
84
- if (!event.targetEditable && event.name === "p") {
102
+ if (!event.targetEditable && activeField === "upgradeAction" && isPlainKey(event, "space", "enter", "return")) {
85
103
  event.preventDefault?.();
86
104
  event.stopPropagation?.();
87
- openPasswordDialog();
105
+ openUpgrade();
106
+ return;
107
+ }
108
+ if (!event.targetEditable && activeField === "deleteAccountAction" && isPlainKey(event, "space", "enter", "return")) {
109
+ event.preventDefault?.();
110
+ event.stopPropagation?.();
111
+ void deleteAccount();
112
+ return;
113
+ }
114
+ if (!event.targetEditable && activeField === "emailAlertsOffAction" && isPlainKey(event, "space", "enter", "return")) {
115
+ event.preventDefault?.();
116
+ event.stopPropagation?.();
117
+ void turnOffEmailAlerts();
88
118
  }
89
119
  }, { allowEditable: true });
90
120
  }
@@ -1,8 +1,16 @@
1
1
  import { describe, expect, test } from "bun:test";
2
2
  import type { Portfolio, TickerRecord } from "../../../types/ticker";
3
- import { buildPortfolioChoices, countPortfolioHoldings, NO_PORTFOLIO_VALUE } from "./model";
3
+ import {
4
+ buildPublishedProfileAnalyticsPreview,
5
+ buildPortfolioChoices,
6
+ buildProfileAnalyticsPreview,
7
+ computeCumulativeReturn,
8
+ countPortfolioHoldings,
9
+ getPortfolioPositionTickers,
10
+ NO_PORTFOLIO_VALUE,
11
+ } from "./model";
4
12
 
5
- function makeTicker(symbol: string, portfolios: string[]): TickerRecord {
13
+ function makeTicker(symbol: string, portfolios: string[], positionedPortfolios: string[] = []): TickerRecord {
6
14
  return {
7
15
  metadata: {
8
16
  ticker: symbol,
@@ -11,7 +19,13 @@ function makeTicker(symbol: string, portfolios: string[]): TickerRecord {
11
19
  name: symbol,
12
20
  portfolios,
13
21
  watchlists: [],
14
- positions: [],
22
+ positions: positionedPortfolios.map((portfolio) => ({
23
+ portfolio,
24
+ shares: 10,
25
+ avgCost: 90,
26
+ currency: "USD",
27
+ broker: "manual",
28
+ })),
15
29
  custom: {},
16
30
  tags: [],
17
31
  },
@@ -47,8 +61,89 @@ describe("account management model", () => {
47
61
  id: "main",
48
62
  label: "Main Portfolio",
49
63
  detail: "2 tickers",
50
- description: "Shares this portfolio's YTD % and SPY Beta on your public profile.",
64
+ description: "Shares this portfolio's 1Y return and SPY Beta on your public profile.",
51
65
  },
52
66
  ]);
53
67
  });
68
+
69
+ test("filters shared analytics tickers by actual portfolio positions", () => {
70
+ const tickers = new Map([
71
+ ["AAPL", makeTicker("AAPL", ["main"], ["main"])],
72
+ ["MSFT", makeTicker("MSFT", ["main"], [])],
73
+ ["NVDA", makeTicker("NVDA", ["other"], ["other"])],
74
+ ]);
75
+
76
+ expect(getPortfolioPositionTickers(tickers, "main").map((ticker) => ticker.metadata.ticker)).toEqual(["AAPL"]);
77
+ });
78
+
79
+ test("compounds the selected one-year return series", () => {
80
+ expect(computeCumulativeReturn([
81
+ { dateKey: "2025-06-01", value: 0.1 },
82
+ { dateKey: "2025-06-02", value: 0.05 },
83
+ { dateKey: "2025-06-03", value: -0.02 },
84
+ ])).toBeCloseTo(0.1319, 5);
85
+ });
86
+
87
+ test("builds a real public analytics preview snapshot", () => {
88
+ const portfolio: Portfolio = { id: "main", name: "Main Portfolio", currency: "USD" };
89
+ const ticker = makeTicker("AAPL", ["main"], ["main"]);
90
+ const preview = buildProfileAnalyticsPreview({
91
+ beta: 1.1,
92
+ portfolio,
93
+ portfolioTickers: [ticker],
94
+ selectedPortfolioId: "main",
95
+ oneYearReturn: 0.1,
96
+ });
97
+
98
+ expect(preview.status).toBe("ready");
99
+ expect(preview.subtitle).toBe("");
100
+ expect(preview.metrics).toHaveLength(2);
101
+ expect(preview.metrics[0]).toMatchObject({ label: "1Y", value: "+10.00%", tone: "positive" });
102
+ expect(preview.metrics[1]).toMatchObject({ label: "SPY Beta", value: "1.10" });
103
+ expect(preview.publicAnalytics).toEqual({
104
+ oneYearReturn: 0.1,
105
+ spyBeta: 1.1,
106
+ });
107
+ });
108
+
109
+ test("builds the visible profile analytics card from published account data", () => {
110
+ const portfolio: Portfolio = { id: "main", name: "Main Portfolio", currency: "USD" };
111
+ const preview = buildPublishedProfileAnalyticsPreview({
112
+ analytics: { oneYearReturn: 0.15, spyBeta: 1.25 },
113
+ draftProfilePublic: true,
114
+ portfolio,
115
+ profileLoaded: true,
116
+ savedProfilePublic: true,
117
+ savedSharedPortfolioId: "main",
118
+ selectedPortfolioId: "main",
119
+ syncing: false,
120
+ });
121
+
122
+ expect(preview.status).toBe("ready");
123
+ expect(preview.title).toBe("Main Portfolio");
124
+ expect(preview.metrics).toEqual([
125
+ { id: "one-year", label: "1Y", value: "+15.00%", tone: "positive" },
126
+ { id: "beta", label: "SPY Beta", value: "1.25" },
127
+ ]);
128
+ expect(preview.publicAnalytics).toEqual({ oneYearReturn: 0.15, spyBeta: 1.25 });
129
+ });
130
+
131
+ test("does not show stale published analytics when the selected profile portfolio is unsaved", () => {
132
+ const portfolio: Portfolio = { id: "new", name: "New Portfolio", currency: "USD" };
133
+ const preview = buildPublishedProfileAnalyticsPreview({
134
+ analytics: { oneYearReturn: 0.15, spyBeta: 1.25 },
135
+ draftProfilePublic: true,
136
+ portfolio,
137
+ profileLoaded: true,
138
+ savedProfilePublic: true,
139
+ savedSharedPortfolioId: "old",
140
+ selectedPortfolioId: "new",
141
+ syncing: false,
142
+ });
143
+
144
+ expect(preview.status).toBe("pending");
145
+ expect(preview.metrics).toEqual([]);
146
+ expect(preview.subtitle).toBe("Save profile to update published metrics.");
147
+ expect(preview.publicAnalytics).toBeNull();
148
+ });
54
149
  });