gloomberb 0.9.7 → 0.9.8

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 (126) hide show
  1. package/package.json +1 -1
  2. package/src/api-client/auth.ts +20 -0
  3. package/src/api-client/index.ts +48 -0
  4. package/src/api-client/types.ts +45 -0
  5. package/src/app/runtime/desktop-deeplink.test.ts +87 -0
  6. package/src/app/runtime/desktop-deeplink.ts +496 -0
  7. package/src/app.tsx +34 -0
  8. package/src/components/chart/bar-chart-renderer.test.ts +87 -12
  9. package/src/components/chart/bar-chart-renderer.ts +111 -55
  10. package/src/components/chart/static/bar-chart-surface.tsx +104 -20
  11. package/src/components/chart/stock-chart/footer.test.ts +65 -0
  12. package/src/components/chart/stock-chart/footer.ts +68 -13
  13. package/src/components/chart/stock-chart/resolved.tsx +18 -4
  14. package/src/components/command-bar/pane-templates/items.ts +11 -3
  15. package/src/components/command-bar/routes/root/results.ts +1 -1
  16. package/src/components/command-bar/surface/index.test.tsx +52 -0
  17. package/src/components/command-bar/workflow/ops.ts +81 -20
  18. package/src/components/index.ts +1 -0
  19. package/src/components/toggle-list.tsx +29 -73
  20. package/src/components/ui/button.tsx +4 -1
  21. package/src/components/ui/checkbox.tsx +76 -0
  22. package/src/components/ui/choice-dialog.tsx +15 -3
  23. package/src/components/ui/frame.tsx +1 -1
  24. package/src/components/ui/index.ts +2 -0
  25. package/src/components/ui/message-composer.tsx +4 -0
  26. package/src/components/ui/native-select.tsx +4 -2
  27. package/src/market-data/coordinator/events.test.ts +49 -0
  28. package/src/market-data/coordinator/events.ts +25 -4
  29. package/src/market-data/coordinator/index.test.ts +223 -0
  30. package/src/market-data/coordinator/index.ts +40 -3
  31. package/src/market-data/coordinator/quotes.ts +13 -5
  32. package/src/market-data/hooks.tsx +17 -13
  33. package/src/market-data/market/freshness.ts +58 -1
  34. package/src/market-data/quotes/freshness.test.ts +49 -1
  35. package/src/market-data/quotes/freshness.ts +8 -0
  36. package/src/market-data/quotes/resolution.test.ts +185 -0
  37. package/src/market-data/quotes/resolution.ts +94 -8
  38. package/src/market-data/request-types.test.ts +61 -1
  39. package/src/market-data/request-types.ts +42 -3
  40. package/src/plugins/builtin/account-management/footer.ts +2 -5
  41. package/src/plugins/builtin/account-management/form-components.tsx +260 -24
  42. package/src/plugins/builtin/account-management/keyboard.ts +32 -2
  43. package/src/plugins/builtin/account-management/model.test.ts +99 -4
  44. package/src/plugins/builtin/account-management/model.ts +263 -3
  45. package/src/plugins/builtin/account-management/pane.tsx +784 -155
  46. package/src/plugins/builtin/chat/chat.test.tsx +125 -0
  47. package/src/plugins/builtin/chat/content/channel-navigation.ts +1 -21
  48. package/src/plugins/builtin/chat/content/composer-cursor.ts +29 -0
  49. package/src/plugins/builtin/chat/content/composer-runtime.ts +22 -24
  50. package/src/plugins/builtin/chat/content/composer.tsx +84 -1
  51. package/src/plugins/builtin/chat/content/index.tsx +154 -16
  52. package/src/plugins/builtin/chat/content/layout-metrics.ts +10 -1
  53. package/src/plugins/builtin/chat/content/mentions.test.ts +74 -0
  54. package/src/plugins/builtin/chat/content/mentions.ts +88 -0
  55. package/src/plugins/builtin/chat/content/new-dm-dialog.tsx +264 -0
  56. package/src/plugins/builtin/chat/content/shortcuts.ts +47 -1
  57. package/src/plugins/builtin/chat/content/snapshot.ts +7 -0
  58. package/src/plugins/builtin/chat/content/transcript.tsx +0 -4
  59. package/src/plugins/builtin/chat/message/profile-popover.test.tsx +36 -0
  60. package/src/plugins/builtin/chat/message/profile-popover.tsx +124 -20
  61. package/src/plugins/builtin/chat/sidebar.test.tsx +69 -2
  62. package/src/plugins/builtin/chat/sidebar.tsx +26 -2
  63. package/src/plugins/builtin/chat/test-harness.tsx +21 -11
  64. package/src/plugins/builtin/cloud/plugin.tsx +7 -0
  65. package/src/plugins/builtin/correlation/index.tsx +4 -1
  66. package/src/plugins/builtin/correlation/relationship/controls.tsx +8 -2
  67. package/src/plugins/builtin/correlation/relationship/model.ts +22 -2
  68. package/src/plugins/builtin/correlation/relationship/pane.tsx +9 -3
  69. package/src/plugins/builtin/portfolio-list/index.test.ts +51 -1
  70. package/src/plugins/builtin/portfolio-list/pane/data.test.ts +54 -0
  71. package/src/plugins/builtin/portfolio-list/pane/data.ts +50 -3
  72. package/src/plugins/builtin/portfolio-list/pane/index.test.tsx +314 -16
  73. package/src/plugins/builtin/portfolio-list/pane/index.tsx +11 -4
  74. package/src/plugins/builtin/portfolio-list/pane/streaming.ts +83 -24
  75. package/src/plugins/builtin/ticker-detail/data-panes/fundamental-graph/content.tsx +43 -7
  76. package/src/plugins/builtin/ticker-detail/data-panes/fundamental-graph/model.ts +415 -64
  77. package/src/plugins/builtin/ticker-detail/data-panes/fundamental-graph/settings.ts +23 -2
  78. package/src/plugins/builtin/ticker-detail/data-panes/fundamental-graph/types.ts +8 -1
  79. package/src/plugins/builtin/ticker-detail/data-panes/fundamental-graph.tsx +3 -1
  80. package/src/plugins/builtin/ticker-detail/data-panes.test.ts +95 -1
  81. package/src/plugins/builtin/ticker-detail/index.tsx +2 -0
  82. package/src/plugins/ibkr/gateway/market-data.ts +12 -5
  83. package/src/plugins/ibkr/gateway/service/index.test.ts +31 -0
  84. package/src/plugins/registry/context.ts +15 -0
  85. package/src/plugins/registry/index.ts +31 -0
  86. package/src/renderers/electrobun/bun/index.ts +42 -0
  87. package/src/renderers/electrobun/shared/protocol.ts +5 -0
  88. package/src/renderers/electrobun/view/backend-rpc.ts +29 -0
  89. package/src/renderers/electrobun/view/desktop/controls.tsx +135 -4
  90. package/src/renderers/electrobun/view/desktop/list-view.tsx +38 -25
  91. package/src/renderers/electrobun/view/desktop-deeplink-bridge.ts +10 -0
  92. package/src/renderers/electrobun/view/host/box.tsx +0 -2
  93. package/src/renderers/electrobun/view/host/scroll-box.tsx +1 -2
  94. package/src/renderers/electrobun/view/host/text.tsx +1 -2
  95. package/src/renderers/electrobun/view/main.tsx +3 -0
  96. package/src/renderers/electrobun/view/ui-host.tsx +2 -0
  97. package/src/sources/provider-router/batches.ts +11 -3
  98. package/src/sources/provider-router/cached-financials.test.ts +123 -0
  99. package/src/sources/provider-router/financial-routes.ts +55 -5
  100. package/src/sources/provider-router/financials.test.ts +49 -0
  101. package/src/sources/provider-router/financials.ts +40 -3
  102. package/src/sources/provider-router/index.test.ts +147 -0
  103. package/src/sources/provider-router/primary.ts +17 -3
  104. package/src/sources/provider-router/streaming.ts +6 -1
  105. package/src/sources/sec-edgar.test.ts +42 -0
  106. package/src/sources/sec-edgar.ts +1 -2
  107. package/src/sources/yahoo-finance/mappers.test.ts +29 -0
  108. package/src/sources/yahoo-finance/mappers.ts +2 -1
  109. package/src/sources/yahoo-finance/requests.ts +2 -1
  110. package/src/sources/yahoo-finance/snapshots.ts +13 -2
  111. package/src/sources/yahoo-finance.ts +6 -2
  112. package/src/state/hooks/quote-streaming.test.ts +14 -1
  113. package/src/state/hooks/quote-streaming.ts +5 -4
  114. package/src/sync/controller.ts +296 -0
  115. package/src/sync/core-contributors.test.ts +251 -0
  116. package/src/sync/core-contributors.ts +400 -0
  117. package/src/sync/profile-analytics.ts +39 -0
  118. package/src/sync/react.ts +53 -0
  119. package/src/sync/types.ts +77 -0
  120. package/src/types/desktop-deeplink.ts +7 -0
  121. package/src/types/plugin.ts +3 -0
  122. package/src/ui/host.tsx +14 -1
  123. package/src/ui/index.tsx +1 -0
  124. package/src/utils/exchanges.test.ts +11 -1
  125. package/src/utils/exchanges.ts +5 -1
  126. package/src/version.ts +1 -1
@@ -1,7 +1,8 @@
1
1
  import { useMemo, type Dispatch, type MutableRefObject, type SetStateAction } from "react";
2
- import { formatCompact } from "../../../utils/format";
2
+ import { formatCompact, formatPercentRaw } from "../../../utils/format";
3
3
  import { formatMarketPriceWithCurrency } from "../../../market-data/market/format";
4
4
  import { usePaneFooter, type PaneFooterSegment, type PaneHint } from "../../layout/pane/footer";
5
+ import { priceColor } from "../../../theme/colors";
5
6
  import type { PricePoint } from "../../../types/financials";
6
7
  import type { ProjectedChartPoint } from "../core/data";
7
8
  import type { DateWindowRange } from "../core/controller";
@@ -24,8 +25,53 @@ import {
24
25
  type StockChartViewportState,
25
26
  } from "./viewport";
26
27
 
27
- interface StockChartFooterOptions {
28
+ interface StockChartOhlcReadout extends ProjectedChartPoint {
29
+ changePercent: number | null;
30
+ }
31
+
32
+ function summarizeOhlcWindow(points: readonly ProjectedChartPoint[]): StockChartOhlcReadout | null {
33
+ const first = points[0];
34
+ const last = points.at(-1);
35
+ if (!first || !last) return null;
36
+
37
+ let high = first.high;
38
+ let low = first.low;
39
+ let volume = 0;
40
+ for (const point of points) {
41
+ high = Math.max(high, point.high);
42
+ low = Math.min(low, point.low);
43
+ volume += point.volume;
44
+ }
45
+
46
+ return {
47
+ date: last.date,
48
+ open: first.open,
49
+ high,
50
+ low,
51
+ close: last.close,
52
+ volume,
53
+ changePercent: Number.isFinite(first.open) && first.open !== 0
54
+ ? ((last.close - first.open) / first.open) * 100
55
+ : null,
56
+ };
57
+ }
58
+
59
+ export function resolveStockChartFooterOhlcReadout({
60
+ activePoint,
61
+ hasDisplayCursor,
62
+ points,
63
+ }: {
28
64
  activePoint: ProjectedChartPoint | null;
65
+ hasDisplayCursor: boolean;
66
+ points: readonly ProjectedChartPoint[];
67
+ }): StockChartOhlcReadout | null {
68
+ if (hasDisplayCursor) {
69
+ return activePoint ? { ...activePoint, changePercent: null } : null;
70
+ }
71
+ return summarizeOhlcWindow(points);
72
+ }
73
+
74
+ interface StockChartFooterOptions {
29
75
  activePreset: TimeRange | null;
30
76
  baseDateBounds: DateWindowRange | null;
31
77
  boundsHistoryDates: Date[];
@@ -37,6 +83,7 @@ interface StockChartFooterOptions {
37
83
  history: PricePoint[];
38
84
  manualMinimumSpanMs: number | null;
39
85
  navigableDateWindow: DateWindowRange | null;
86
+ ohlcReadout: StockChartOhlcReadout | null;
40
87
  pendingAutoWindowRef: MutableRefObject<DateWindowRange | null>;
41
88
  pendingCanonicalResetRef: MutableRefObject<number>;
42
89
  persistRenderMode: (mode: ChartRenderMode) => void;
@@ -50,7 +97,6 @@ interface StockChartFooterOptions {
50
97
  setRenderedAutoView: Dispatch<SetStateAction<AutoRenderedView | null>>;
51
98
  setResolution: (resolution: ChartResolution) => void;
52
99
  setViewState: Dispatch<SetStateAction<StockChartViewportState>>;
53
- showOhlcSummary: boolean;
54
100
  updateDisplayCursorTarget: (next: DisplayCursorState, motionKind: ChartCursorMotionKind) => void;
55
101
  visibleDateWindow: DateWindowRange | null;
56
102
  visiblePriceRange: number | undefined;
@@ -58,7 +104,6 @@ interface StockChartFooterOptions {
58
104
  }
59
105
 
60
106
  export function useStockChartFooter({
61
- activePoint,
62
107
  activePreset,
63
108
  baseDateBounds,
64
109
  boundsHistoryDates,
@@ -70,6 +115,7 @@ export function useStockChartFooter({
70
115
  history,
71
116
  manualMinimumSpanMs,
72
117
  navigableDateWindow,
118
+ ohlcReadout,
73
119
  pendingAutoWindowRef,
74
120
  pendingCanonicalResetRef,
75
121
  persistRenderMode,
@@ -83,7 +129,6 @@ export function useStockChartFooter({
83
129
  setRenderedAutoView,
84
130
  setResolution,
85
131
  setViewState,
86
- showOhlcSummary,
87
132
  updateDisplayCursorTarget,
88
133
  visibleDateWindow,
89
134
  visiblePriceRange,
@@ -196,7 +241,7 @@ export function useStockChartFooter({
196
241
  ]);
197
242
 
198
243
  const chartFooterInfo = useMemo<PaneFooterSegment[]>(() => {
199
- if (compact || !showOhlcSummary || !activePoint) return [];
244
+ if (compact || !ohlcReadout) return [];
200
245
 
201
246
  const formatPrice = (value: number) => formatMarketPriceWithCurrency(value, chartCurrency, {
202
247
  assetCategory: chartAssetCategory,
@@ -210,16 +255,27 @@ export function useStockChartFooter({
210
255
  ? []
211
256
  : [
212
257
  { text: "O", tone: "label" as const },
213
- { text: formatPrice(activePoint.open), tone: "value" as const },
258
+ { text: formatPrice(ohlcReadout.open), tone: "value" as const },
214
259
  ]),
215
260
  { text: "H", tone: "label" },
216
- { text: formatPrice(activePoint.high), tone: "value" },
261
+ { text: formatPrice(ohlcReadout.high), tone: "value" },
217
262
  { text: "L", tone: "label" },
218
- { text: formatPrice(activePoint.low), tone: "value" },
263
+ { text: formatPrice(ohlcReadout.low), tone: "value" },
219
264
  { text: "C", tone: "label" },
220
- { text: formatPrice(activePoint.close), tone: "value" },
265
+ { text: formatPrice(ohlcReadout.close), tone: "value" },
266
+ ...(ohlcReadout.changePercent === null
267
+ ? []
268
+ : [
269
+ { text: "%", tone: "label" as const },
270
+ {
271
+ text: formatPercentRaw(ohlcReadout.changePercent),
272
+ tone: "value" as const,
273
+ color: priceColor(ohlcReadout.changePercent),
274
+ bold: true,
275
+ },
276
+ ]),
221
277
  { text: "V", tone: "label" },
222
- { text: formatCompact(activePoint.volume), tone: "value" },
278
+ { text: formatCompact(ohlcReadout.volume), tone: "value" },
223
279
  ];
224
280
 
225
281
  return [{
@@ -227,12 +283,11 @@ export function useStockChartFooter({
227
283
  parts,
228
284
  }];
229
285
  }, [
230
- activePoint,
231
286
  chartAssetCategory,
232
287
  chartCurrency,
233
288
  compact,
289
+ ohlcReadout,
234
290
  projectionMode,
235
- showOhlcSummary,
236
291
  visiblePriceRange,
237
292
  ]);
238
293
 
@@ -1,5 +1,5 @@
1
- import { memo, type ComponentProps } from "react";
2
- import { useStockChartFooter } from "./footer";
1
+ import { memo, useMemo, type ComponentProps } from "react";
2
+ import { resolveStockChartFooterOhlcReadout, useStockChartFooter } from "./footer";
3
3
  import { useStockChartKeyboardShortcuts } from "./keyboard";
4
4
  import { useStockChartControlRenderInvalidation } from "./rendering/invalidation";
5
5
  import { useStockChartRenderOutput } from "./rendering/output";
@@ -139,9 +139,23 @@ function useResolvedStockChartControls(
139
139
  } = runtime;
140
140
  const { output, status } = render;
141
141
  const manualMinimumSpanMs = getChartResolutionStepMs(dataRuntime.renderedResolution);
142
+ const ohlcReadout = useMemo(
143
+ () => output.showOhlcSummary
144
+ ? resolveStockChartFooterOhlcReadout({
145
+ activePoint: output.activePoint,
146
+ hasDisplayCursor: output.hasDisplayCursor,
147
+ points: projectionModel.projection.points,
148
+ })
149
+ : null,
150
+ [
151
+ output.activePoint,
152
+ output.hasDisplayCursor,
153
+ output.showOhlcSummary,
154
+ projectionModel.projection.points,
155
+ ],
156
+ );
142
157
 
143
158
  useStockChartFooter({
144
- activePoint: output.activePoint,
145
159
  activePreset: viewportRuntime.activePreset,
146
160
  baseDateBounds: dataRuntime.baseDateBounds,
147
161
  boundsHistoryDates: dataRuntime.boundsHistoryDates,
@@ -153,6 +167,7 @@ function useResolvedStockChartControls(
153
167
  history: dataRuntime.history,
154
168
  manualMinimumSpanMs,
155
169
  navigableDateWindow: dataRuntime.navigableDateWindow,
170
+ ohlcReadout,
156
171
  pendingAutoWindowRef: interactionRuntime.pendingAutoWindowRef,
157
172
  pendingCanonicalResetRef: interactionRuntime.pendingCanonicalResetRef,
158
173
  persistRenderMode: viewportRuntime.persistRenderMode,
@@ -166,7 +181,6 @@ function useResolvedStockChartControls(
166
181
  setRenderedAutoView: dataRuntime.setRenderedAutoView,
167
182
  setResolution: viewportRuntime.setResolution,
168
183
  setViewState: settings.setViewState,
169
- showOhlcSummary: output.showOhlcSummary,
170
184
  updateDisplayCursorTarget: interactionRuntime.updateDisplayCursorTarget,
171
185
  visibleDateWindow: dataRuntime.visibleDateWindow,
172
186
  visiblePriceRange: output.visiblePriceRange,
@@ -139,6 +139,14 @@ export function buildPaneTemplateItem(options: {
139
139
  .join(" ")
140
140
  : null;
141
141
  const arg = options.createOptions?.arg;
142
+ const searchText = [
143
+ options.template.keywords?.join(" ") || "",
144
+ displayLabel,
145
+ options.template.label,
146
+ options.template.paneId,
147
+ shortcutLabel || "",
148
+ pluginName || "",
149
+ ].filter(Boolean).join(" ");
142
150
 
143
151
  const action = () => {
144
152
  if (
@@ -163,7 +171,7 @@ export function buildPaneTemplateItem(options: {
163
171
  kind: "action",
164
172
  right: options.showShortcut ? options.template.shortcut?.prefix : undefined,
165
173
  shortcutQuery: options.template.shortcut?.prefix,
166
- searchText: `${displayLabel} ${options.template.label} ${options.template.paneId} ${options.template.keywords?.join(" ") || ""} ${shortcutLabel || ""} ${pluginName || ""}`,
174
+ searchText,
167
175
  action,
168
176
  };
169
177
  }
@@ -187,7 +195,7 @@ export function buildPaneShortcutItems(options: {
187
195
  }));
188
196
 
189
197
  return options.filterQuery
190
- ? fuzzyFilter(items, options.filterQuery, (item) => `${item.label} ${item.detail} ${item.searchText || ""} ${item.right || ""}`)
198
+ ? fuzzyFilter(items, options.filterQuery, (item) => `${item.label} ${item.searchText || ""} ${item.detail} ${item.right || ""}`)
191
199
  : items;
192
200
  }
193
201
 
@@ -201,6 +209,6 @@ export function buildNonShortcutPaneTemplateItems(options: {
201
209
  .map((template) => options.createItem(template, { category: "Panes" }));
202
210
 
203
211
  return options.filterQuery
204
- ? fuzzyFilter(items, options.filterQuery, (item) => `${item.label} ${item.detail} ${item.searchText || ""} ${item.right || ""}`)
212
+ ? fuzzyFilter(items, options.filterQuery, (item) => `${item.label} ${item.searchText || ""} ${item.detail} ${item.right || ""}`)
205
213
  : items;
206
214
  }
@@ -196,7 +196,7 @@ export function buildRootResultModel(options: RootResultModelOptions): RootResul
196
196
  ...tickerActionItems(),
197
197
  ...pluginCommandItems(),
198
198
  ];
199
- items.push(...fuzzyFilter(allItems, rootQuery, (item) => `${item.label} ${item.detail} ${item.searchText || ""} ${item.right || ""}`));
199
+ items.push(...fuzzyFilter(allItems, rootQuery, (item) => `${item.label} ${item.searchText || ""} ${item.detail} ${item.right || ""}`));
200
200
  }
201
201
 
202
202
  return { items, initialIdx };
@@ -6,6 +6,7 @@ import type { CommandDef, CommandShortcutArgContext, PaneTemplateCreateOptions,
6
6
  import {
7
7
  CommandBarHarness,
8
8
  createCommandBarTestControls,
9
+ emitKeypress,
9
10
  expectSingleBackControl,
10
11
  makeDataProvider,
11
12
  makeTicker,
@@ -26,6 +27,11 @@ type MutableCommandRegistry = {
26
27
  commands: ReadonlyMap<string, CommandDef>;
27
28
  };
28
29
 
30
+ type MutablePaneRegistry = {
31
+ panes: ReadonlyMap<string, unknown>;
32
+ paneTemplates: ReadonlyMap<string, unknown>;
33
+ };
34
+
29
35
  const DEFAULT_ALERT_OPTIONS = [
30
36
  { label: "Above", value: "above" },
31
37
  { label: "Below", value: "below" },
@@ -70,6 +76,10 @@ function registerAlertCommand(
70
76
  });
71
77
  }
72
78
 
79
+ function mutablePaneRegistryMap(map: ReadonlyMap<string, unknown>): Map<string, unknown> {
80
+ return map as Map<string, unknown>;
81
+ }
82
+
73
83
  describe("CommandBar", () => {
74
84
  test("renders the default layout with opencode-style chrome", async () => {
75
85
  testSetup = await testRender(<CommandBarHarness query="" selectedTicker="AAPL" />, {
@@ -129,6 +139,48 @@ describe("CommandBar", () => {
129
139
  expect(testSetup.captureCharFrame()).not.toContain("Commands");
130
140
  });
131
141
 
142
+ test("shows one account management result when searching profile", async () => {
143
+ const created: Array<{ templateId: string; options?: PaneTemplateCreateOptions }> = [];
144
+
145
+ testSetup = await testRender(<CommandBarHarness
146
+ query="profile"
147
+ live
148
+ configurePluginRegistry={(pluginRegistry) => {
149
+ const registry = pluginRegistry as MutablePaneRegistry;
150
+ mutablePaneRegistryMap(registry.panes).set("account-management", {
151
+ id: "account-management",
152
+ name: "Account Management",
153
+ component: () => null,
154
+ defaultPosition: "right",
155
+ defaultMode: "floating",
156
+ });
157
+ mutablePaneRegistryMap(registry.paneTemplates).set("account-management-pane", {
158
+ id: "account-management-pane",
159
+ paneId: "account-management",
160
+ label: "Account Management",
161
+ description: "Edit your Gloom Cloud profile, password, and public portfolio sharing settings",
162
+ keywords: ["account", "profile", "cloud", "acm", "password", "settings"],
163
+ shortcut: { prefix: "ACM" },
164
+ });
165
+ pluginRegistry.createPaneFromTemplateAsyncFn = async (templateId, options) => {
166
+ created.push({ templateId, options });
167
+ };
168
+ }}
169
+ />, {
170
+ width: 80,
171
+ height: 24,
172
+ });
173
+
174
+ await testSetup.renderOnce();
175
+ const frame = await waitForFrameToContain("Account Management");
176
+ expect(frame).not.toMatch(/\n\s*Profile\s*(?:\n|$)/);
177
+ expect(frame.indexOf("Account Management")).toBeLessThan(frame.indexOf("Add Broker Account"));
178
+
179
+ await emitKeypress(testSetup, { name: "return", sequence: "\r" });
180
+
181
+ expect(created).toEqual([{ templateId: "account-management-pane", options: undefined }]);
182
+ });
183
+
132
184
  test("shows theme picker rows and commits a filtered light theme", async () => {
133
185
  testSetup = await testRender(<CommandBarHarness query="TH light" live />, {
134
186
  width: 80,
@@ -6,11 +6,22 @@ import { formatTickerListInput } from "../../../tickers/list";
6
6
  import { updatePaneInstance, setPaneSettings } from "../../../pane-settings";
7
7
  import { TICKER_RESEARCH_PANE_ID } from "../../../types/config";
8
8
  import { cleanPortfolioPaneSettings, resolvePortfolioPaneCollectionId } from "../../../plugins/builtin/portfolio-list/settings";
9
+ import {
10
+ DEFAULT_RELATIONSHIP_SECOND_SYMBOL,
11
+ RELATIONSHIP_GRAPH_PANE_ID,
12
+ buildRelationshipGraphPaneTitle,
13
+ } from "../../../plugins/builtin/correlation/relationship/model";
9
14
  import {
10
15
  buildComparisonChartPaneTitle,
11
16
  COMPARISON_CHART_PANE_ID,
12
17
  MIN_COMPARISON_CHART_SYMBOLS,
13
18
  } from "../../../plugins/builtin/comparison-chart";
19
+ import {
20
+ FUNDAMENTAL_GRAPH_PANE_ID,
21
+ graphKindFromSettings,
22
+ graphShortcutForKind,
23
+ graphTemplateTitle,
24
+ } from "../../../plugins/builtin/ticker-detail/data-panes/fundamental-graph/settings";
14
25
  import { buildQuoteMonitorPaneTitle } from "../../../plugins/builtin/ticker-detail/settings";
15
26
  import { getPaneTemplateDisplayLabel } from "../pane-templates/items";
16
27
  import {
@@ -54,6 +65,30 @@ interface ApplyPaneSettingDeps extends SharedWorkflowDeps {
54
65
  persistLayout: (layout: LayoutConfig, options?: { pushHistory?: boolean }) => void;
55
66
  }
56
67
 
68
+ function updateTickerListPane(
69
+ layout: LayoutConfig,
70
+ targetId: string,
71
+ options: {
72
+ title: string;
73
+ symbols: readonly string[];
74
+ primarySymbol?: string;
75
+ settings?: Record<string, unknown>;
76
+ },
77
+ ): LayoutConfig {
78
+ const symbols = [...options.symbols];
79
+ return updatePaneInstance(layout, targetId, (instance) => ({
80
+ ...instance,
81
+ title: options.title,
82
+ ...(options.primarySymbol ? { binding: { kind: "fixed", symbol: options.primarySymbol } as PaneBinding } : {}),
83
+ settings: {
84
+ ...(instance.settings ?? {}),
85
+ symbols,
86
+ symbolsText: formatTickerListInput(symbols),
87
+ ...(options.settings ?? {}),
88
+ },
89
+ }));
90
+ }
91
+
57
92
  async function resolvePaneTemplateOptions(
58
93
  template: PaneTemplateDef,
59
94
  options: PaneTemplateCreateOptions | undefined,
@@ -180,19 +215,50 @@ export async function applyPaneSettingFieldValue(
180
215
  const rawQuery = typeof value === "string" ? value.trim() : "";
181
216
  const symbols = await resolveTickerListInput(rawQuery, null, deps);
182
217
  const primarySymbol = symbols[0]!;
183
- const symbolsText = formatTickerListInput(symbols);
184
-
185
- const nextLayout = updatePaneInstance(state.config.layout, targetId, (instance) => ({
186
- ...instance,
218
+ const nextLayout = updateTickerListPane(state.config.layout, targetId, {
187
219
  title: buildQuoteMonitorPaneTitle(symbols),
188
- binding: { kind: "fixed", symbol: primarySymbol },
189
- settings: {
190
- ...(instance.settings ?? {}),
191
- symbol: primarySymbol,
192
- symbols,
193
- symbolsText,
194
- },
195
- }));
220
+ symbols,
221
+ primarySymbol,
222
+ settings: { symbol: primarySymbol },
223
+ });
224
+ deps.persistLayout(nextLayout, { pushHistory: shouldPushHistory });
225
+ return;
226
+ }
227
+
228
+ if (descriptor.pane.paneId === FUNDAMENTAL_GRAPH_PANE_ID && field.key === "symbolsText") {
229
+ const rawInput = typeof value === "string" ? value : "";
230
+ const symbols = await resolveTickerListInput(
231
+ rawInput,
232
+ descriptor.context.activeCollectionId,
233
+ deps,
234
+ );
235
+ const primarySymbol = symbols[0]!;
236
+ const chartKind = graphKindFromSettings(descriptor.context.settings, "fundamental");
237
+ const nextLayout = updateTickerListPane(state.config.layout, targetId, {
238
+ title: graphTemplateTitle(graphShortcutForKind(chartKind), symbols),
239
+ symbols,
240
+ primarySymbol,
241
+ });
242
+ deps.persistLayout(nextLayout, { pushHistory: shouldPushHistory });
243
+ return;
244
+ }
245
+
246
+ if (descriptor.pane.paneId === RELATIONSHIP_GRAPH_PANE_ID && field.key === "symbolsText") {
247
+ const rawInput = typeof value === "string" ? value : "";
248
+ const symbols = await resolveTickerListInput(
249
+ rawInput,
250
+ descriptor.context.activeCollectionId,
251
+ deps,
252
+ );
253
+ if (symbols.length > 2) {
254
+ throw new Error("Enter one or two tickers.");
255
+ }
256
+ const pair: [string, string] = [symbols[0]!, symbols[1] ?? DEFAULT_RELATIONSHIP_SECOND_SYMBOL];
257
+ const nextLayout = updateTickerListPane(state.config.layout, targetId, {
258
+ title: buildRelationshipGraphPaneTitle(pair),
259
+ symbols: pair,
260
+ primarySymbol: pair[0],
261
+ });
196
262
  deps.persistLayout(nextLayout, { pushHistory: shouldPushHistory });
197
263
  return;
198
264
  }
@@ -207,15 +273,10 @@ export async function applyPaneSettingFieldValue(
207
273
  if (symbols.length < MIN_COMPARISON_CHART_SYMBOLS) {
208
274
  throw new Error(`Enter at least ${MIN_COMPARISON_CHART_SYMBOLS} tickers.`);
209
275
  }
210
- const nextLayout = updatePaneInstance(state.config.layout, targetId, (instance) => ({
211
- ...instance,
276
+ const nextLayout = updateTickerListPane(state.config.layout, targetId, {
212
277
  title: buildComparisonChartPaneTitle(symbols),
213
- settings: {
214
- ...(instance.settings ?? {}),
215
- symbols,
216
- symbolsText: formatTickerListInput(symbols),
217
- },
218
- }));
278
+ symbols,
279
+ });
219
280
  deps.persistLayout(nextLayout, { pushHistory: shouldPushHistory });
220
281
  return;
221
282
  }
@@ -25,6 +25,7 @@ export { PaneFooterScope, usePaneFooter } from "./layout/pane/footer";
25
25
  export type { PaneFooterSegment, PaneHint } from "./layout/pane/footer";
26
26
  export { useExternalLinkFooter } from "./use-external-link-footer";
27
27
  export { Button } from "./ui/button";
28
+ export { Checkbox } from "./ui/checkbox";
28
29
  export { ConfirmDialog } from "./ui/confirm-dialog";
29
30
  export { ChoiceDialog } from "./ui/choice-dialog";
30
31
  export type { ChoiceDialogChoice } from "./ui/choice-dialog";
@@ -1,6 +1,6 @@
1
- import { Box, Text, useUiHost } from "../ui";
2
- import { TextAttributes } from "../ui";
3
- import { blendHex, colors } from "../theme/colors";
1
+ import { Box, useUiHost } from "../ui";
2
+ import { colors } from "../theme/colors";
3
+ import { Checkbox } from "./ui/checkbox";
4
4
  import { ListView, type ListRowState, type ListViewItem } from "./ui/list-view";
5
5
 
6
6
  interface ToggleListItem {
@@ -36,23 +36,14 @@ function DesktopToggleRow({
36
36
  state,
37
37
  rowIdPrefix,
38
38
  enabled,
39
+ onPress,
39
40
  }: {
40
41
  item: ListViewItem;
41
42
  state: ListRowState;
42
43
  rowIdPrefix?: string;
43
44
  enabled: boolean;
45
+ onPress: () => void;
44
46
  }) {
45
- const checkboxBorder = enabled
46
- ? colors.borderFocused
47
- : state.selected
48
- ? colors.textMuted
49
- : colors.border;
50
- const textColor = state.disabled
51
- ? colors.textMuted
52
- : state.selected
53
- ? colors.text
54
- : colors.textDim;
55
-
56
47
  return (
57
48
  <Box
58
49
  id={rowIdPrefix ? `${rowIdPrefix}:${item.id}` : undefined}
@@ -67,48 +58,15 @@ function DesktopToggleRow({
67
58
  opacity: state.disabled ? 0.55 : 1,
68
59
  }}
69
60
  >
70
- <Box
71
- alignItems="center"
72
- justifyContent="center"
73
- backgroundColor={enabled ? colors.borderFocused : "transparent"}
74
- style={{
75
- width: 16,
76
- height: 16,
77
- minWidth: 16,
78
- alignSelf: "center",
79
- border: `1px solid ${checkboxBorder}`,
80
- borderRadius: 4,
81
- boxShadow: enabled
82
- ? `inset 0 1px 0 ${blendHex(colors.borderFocused, colors.textBright, 0.28)}`
83
- : `inset 0 1px 0 ${blendHex(colors.bg, colors.textBright, 0.05)}`,
84
- }}
85
- >
86
- {enabled && (
87
- <Text
88
- fg={colors.bg}
89
- attributes={TextAttributes.BOLD}
90
- style={{ fontSize: 12, lineHeight: "14px", fontWeight: 800 }}
91
- >
92
- {"✓"}
93
- </Text>
94
- )}
95
- </Box>
96
- <Box minWidth={0} flexShrink={1}>
97
- <Text
98
- fg={textColor}
99
- attributes={state.selected ? TextAttributes.BOLD : 0}
100
- style={{
101
- display: "block",
102
- overflow: "hidden",
103
- textOverflow: "ellipsis",
104
- whiteSpace: "nowrap",
105
- fontWeight: state.selected ? 700 : 500,
106
- lineHeight: "16px",
107
- }}
108
- >
109
- {item.label}
110
- </Text>
111
- </Box>
61
+ <Checkbox
62
+ label={item.label}
63
+ checked={enabled}
64
+ disabled={state.disabled}
65
+ active={state.selected}
66
+ width="100%"
67
+ variant="desktop"
68
+ onChange={onPress}
69
+ />
112
70
  </Box>
113
71
  );
114
72
  }
@@ -161,6 +119,13 @@ export function ToggleList({
161
119
  }}
162
120
  renderRow={(item, state, index) => {
163
121
  const toggleItem = items.find((entry) => entry.id === item.id);
122
+ const activate = (event?: { stopPropagation?: () => void; preventDefault?: () => void }) => {
123
+ event?.stopPropagation?.();
124
+ event?.preventDefault?.();
125
+ if (state.disabled) return;
126
+ onSelect?.(index);
127
+ onToggle?.(item.id);
128
+ };
164
129
  if (isDesktopWeb) {
165
130
  return (
166
131
  <DesktopToggleRow
@@ -168,33 +133,24 @@ export function ToggleList({
168
133
  state={state}
169
134
  rowIdPrefix={rowIdPrefix}
170
135
  enabled={toggleItem?.enabled === true}
136
+ onPress={() => activate()}
171
137
  />
172
138
  );
173
139
  }
174
140
 
175
- const checked = toggleItem?.enabled ? "\u2713" : " ";
176
- const activate = (event: any) => {
177
- event.stopPropagation?.();
178
- if (state.disabled) return;
179
- onSelect?.(index);
180
- onToggle?.(item.id);
181
- };
182
141
  return (
183
142
  <Box
184
143
  id={rowIdPrefix ? `${rowIdPrefix}:${item.id}` : undefined}
185
144
  flexDirection="row"
186
145
  onMouseDown={activate}
187
146
  >
188
- <Text fg={state.selected ? colors.selectedText : colors.textDim}>
189
- {state.selected ? "\u25b8 " : " "}
190
- </Text>
191
- <Text
192
- fg={state.selected ? colors.text : colors.textDim}
193
- attributes={state.selected ? TextAttributes.BOLD : 0}
194
- onMouseDown={activate}
195
- >
196
- {`[${checked}] ${item.label}`}
197
- </Text>
147
+ <Checkbox
148
+ label={item.label}
149
+ checked={toggleItem?.enabled === true}
150
+ disabled={state.disabled}
151
+ active={state.selected}
152
+ onChange={() => activate()}
153
+ />
198
154
  </Box>
199
155
  );
200
156
  }}
@@ -14,6 +14,7 @@ export interface ButtonProps {
14
14
  active?: boolean;
15
15
  shortcut?: string;
16
16
  width?: number;
17
+ height?: number | string;
17
18
  }
18
19
 
19
20
  function resolveButtonColors(variant: ButtonVariant, active: boolean, disabled: boolean) {
@@ -45,6 +46,7 @@ export function Button({
45
46
  active = false,
46
47
  shortcut,
47
48
  width,
49
+ height,
48
50
  }: ButtonProps) {
49
51
  useRemoteUiNode({
50
52
  role: "button",
@@ -68,6 +70,7 @@ export function Button({
68
70
  active={active}
69
71
  shortcut={shortcut}
70
72
  width={width}
73
+ height={height}
71
74
  />
72
75
  );
73
76
  }
@@ -77,7 +80,7 @@ export function Button({
77
80
  return (
78
81
  <Box
79
82
  width={width}
80
- height={1}
83
+ height={height ?? 1}
81
84
  flexDirection="row"
82
85
  backgroundColor={palette.bg}
83
86
  onMouseDown={() => {