gloomberb 0.8.1 → 0.8.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 (122) hide show
  1. package/README.md +102 -52
  2. package/package.json +1 -1
  3. package/src/api-client/chat.ts +8 -0
  4. package/src/api-client/index.test.ts +30 -0
  5. package/src/api-client/index.ts +4 -0
  6. package/src/api-client/normalizers.ts +1 -0
  7. package/src/api-client/types.ts +1 -0
  8. package/src/capabilities/factories.ts +4 -0
  9. package/src/components/chart/comparison/data.test.ts +54 -0
  10. package/src/components/chart/comparison/data.ts +47 -24
  11. package/src/components/chart/core/projection.ts +51 -27
  12. package/src/components/data-table/stack-view.test.tsx +14 -11
  13. package/src/components/data-table/stack-view.tsx +1 -2
  14. package/src/components/data-table/view.test.tsx +24 -18
  15. package/src/components/data-table/view.tsx +313 -83
  16. package/src/components/feed-data-table/stack-view.tsx +6 -8
  17. package/src/components/layout/pane/footer/index.test.tsx +6 -0
  18. package/src/components/layout/pane/footer/index.tsx +14 -2
  19. package/src/components/layout/pane/title.ts +5 -1
  20. package/src/components/pane-settings-dialog/field-dialogs.tsx +0 -2
  21. package/src/components/pane-settings-dialog/tui.tsx +1 -1
  22. package/src/components/ticker/list-table-view.tsx +11 -25
  23. package/src/components/ui/choice-dialog.tsx +12 -2
  24. package/src/components/ui/multi-select/dialog.tsx +17 -5
  25. package/src/components/ui/shortcut-hint.tsx +14 -2
  26. package/src/components/ui/table-layout.test.ts +2 -2
  27. package/src/components/ui/table-layout.ts +8 -2
  28. package/src/components/ui/tabs.tsx +20 -6
  29. package/src/components/ui/ui.test.tsx +30 -5
  30. package/src/market-data/coordinator/auxiliary.ts +29 -1
  31. package/src/market-data/coordinator/index.ts +17 -1
  32. package/src/market-data/hooks.tsx +17 -1
  33. package/src/market-data/selectors.ts +4 -0
  34. package/src/plugins/builtin/account-management/form-components.tsx +0 -4
  35. package/src/plugins/builtin/account-management/model.test.ts +54 -0
  36. package/src/plugins/builtin/account-management/model.ts +13 -3
  37. package/src/plugins/builtin/account-management/pane.tsx +5 -11
  38. package/src/plugins/builtin/account-management/password-dialog.tsx +1 -1
  39. package/src/plugins/builtin/alerts/pane.tsx +5 -3
  40. package/src/plugins/builtin/analytics/index.tsx +0 -3
  41. package/src/plugins/builtin/analytics/view.tsx +6 -5
  42. package/src/plugins/builtin/broker-manager/index.tsx +6 -6
  43. package/src/plugins/builtin/buildout/pane/index.tsx +6 -6
  44. package/src/plugins/builtin/changelog/index.tsx +40 -27
  45. package/src/plugins/builtin/changelog/model.test.ts +77 -0
  46. package/src/plugins/builtin/changelog/model.ts +98 -0
  47. package/src/plugins/builtin/chat/channel-labels.ts +53 -0
  48. package/src/plugins/builtin/chat/channels.test.ts +28 -0
  49. package/src/plugins/builtin/chat/channels.ts +36 -19
  50. package/src/plugins/builtin/chat/chat.test.tsx +264 -5
  51. package/src/plugins/builtin/chat/content/channel-navigation.ts +6 -0
  52. package/src/plugins/builtin/chat/content/composer-runtime.test.tsx +144 -0
  53. package/src/plugins/builtin/chat/content/composer-runtime.ts +151 -20
  54. package/src/plugins/builtin/chat/content/composer.tsx +23 -2
  55. package/src/plugins/builtin/chat/content/index.tsx +61 -0
  56. package/src/plugins/builtin/chat/content/layout-metrics.ts +3 -1
  57. package/src/plugins/builtin/chat/content/shortcuts.ts +20 -1
  58. package/src/plugins/builtin/chat/content/transcript.tsx +8 -0
  59. package/src/plugins/builtin/chat/content/types.ts +1 -0
  60. package/src/plugins/builtin/chat/controller/index.ts +41 -0
  61. package/src/plugins/builtin/chat/controller/messages.ts +18 -0
  62. package/src/plugins/builtin/chat/controller/notifications.ts +10 -4
  63. package/src/plugins/builtin/chat/controller/utils.ts +6 -3
  64. package/src/plugins/builtin/chat/controller/view.ts +1 -1
  65. package/src/plugins/builtin/chat/controller.test.ts +189 -0
  66. package/src/plugins/builtin/chat/edit-window.ts +24 -0
  67. package/src/plugins/builtin/chat/layout.ts +94 -31
  68. package/src/plugins/builtin/chat/message/action-chip.tsx +1 -0
  69. package/src/plugins/builtin/chat/message/desktop.tsx +53 -20
  70. package/src/plugins/builtin/chat/message/inline-tokens.tsx +16 -10
  71. package/src/plugins/builtin/chat/message/render-state.ts +5 -1
  72. package/src/plugins/builtin/chat/message/terminal.tsx +49 -20
  73. package/src/plugins/builtin/chat/message/types.ts +2 -0
  74. package/src/plugins/builtin/chat/pane.tsx +35 -7
  75. package/src/plugins/builtin/chat/sidebar.test.tsx +1 -0
  76. package/src/plugins/builtin/chat/status-widget.tsx +29 -11
  77. package/src/plugins/builtin/chat/test-harness.tsx +2 -0
  78. package/src/plugins/builtin/cloud/plugin.tsx +3 -0
  79. package/src/plugins/builtin/cloud-tweets/table.tsx +7 -11
  80. package/src/plugins/builtin/congress-trades/pane.tsx +14 -22
  81. package/src/plugins/builtin/earnings/index.tsx +8 -7
  82. package/src/plugins/builtin/econ/index.tsx +6 -8
  83. package/src/plugins/builtin/help/index.tsx +6 -0
  84. package/src/plugins/builtin/holders/pane.tsx +7 -6
  85. package/src/plugins/builtin/market-movers/index.tsx +6 -3
  86. package/src/plugins/builtin/news/wire/news/table.tsx +7 -6
  87. package/src/plugins/builtin/options/view.tsx +16 -8
  88. package/src/plugins/builtin/research/analyst-pane.tsx +1 -2
  89. package/src/plugins/builtin/research/corporate-actions-pane.tsx +358 -7
  90. package/src/plugins/builtin/research/index.test.ts +30 -1
  91. package/src/plugins/builtin/research/relative-valuation-pane.tsx +6 -8
  92. package/src/plugins/builtin/sec/filing-documents.ts +66 -0
  93. package/src/plugins/builtin/sec/index.tsx +132 -20
  94. package/src/plugins/builtin/sectors/index.tsx +13 -23
  95. package/src/plugins/builtin/shared/ticker-request.ts +1 -0
  96. package/src/plugins/builtin/thirteenf/pane.tsx +26 -25
  97. package/src/plugins/builtin/ticker-detail/data-panes/fundamental-graph/content.tsx +5 -4
  98. package/src/plugins/builtin/ticker-detail/data-panes/historical-prices.tsx +5 -4
  99. package/src/plugins/builtin/ticker-detail/data-panes/provider-search.tsx +6 -5
  100. package/src/plugins/builtin/ticker-detail/financials/tab.tsx +10 -8
  101. package/src/plugins/builtin/world-indices/index.tsx +9 -6
  102. package/src/plugins/prediction-markets/pane.tsx +10 -9
  103. package/src/plugins/registry/index.ts +3 -0
  104. package/src/plugins/runtime/context.tsx +1 -0
  105. package/src/plugins/runtime/index.tsx +4 -0
  106. package/src/renderers/electrobun/view/cli-pane-shot-entry.tsx +1 -0
  107. package/src/renderers/electrobun/view/data-table/index.tsx +3 -2
  108. package/src/renderers/electrobun/view/data-table/row.tsx +2 -2
  109. package/src/renderers/electrobun/view/host/input.tsx +23 -2
  110. package/src/renderers/electrobun/view/remote/asset-data-client.ts +1 -0
  111. package/src/sources/gloomberb-cloud/index.ts +4 -0
  112. package/src/sources/provider-router/cache.ts +1 -0
  113. package/src/sources/provider-router/document-routes.ts +45 -0
  114. package/src/sources/provider-router/index.ts +4 -0
  115. package/src/sources/sec-edgar/content.ts +9 -3
  116. package/src/sources/sec-edgar.test.ts +108 -1
  117. package/src/sources/sec-edgar.ts +105 -1
  118. package/src/sources/yahoo-finance.ts +4 -0
  119. package/src/test-support/plugin-runtime.ts +1 -0
  120. package/src/types/data-provider.ts +11 -0
  121. package/src/ui/host.tsx +1 -0
  122. package/src/version.ts +1 -1
@@ -11,7 +11,11 @@ export function getPaneDisplayTitle(
11
11
  const channelId = typeof instance.settings?.channelId === "string" && instance.settings.channelId.trim()
12
12
  ? instance.settings.channelId.trim()
13
13
  : "everyone";
14
- return instance.title ?? `#${channelId}`;
14
+ const title = typeof instance.title === "string" ? instance.title.trim() : "";
15
+ const displayTitle = title && !title.startsWith("dm:") && !title.startsWith("group:") ? title : undefined;
16
+ if (channelId.startsWith("dm:")) return displayTitle ?? "DM";
17
+ if (channelId.startsWith("group:")) return displayTitle ?? "Group";
18
+ return displayTitle ?? `#${channelId}`;
15
19
  }
16
20
 
17
21
  if (instance.paneId === TICKER_RESEARCH_PANE_ID) {
@@ -61,7 +61,6 @@ export function TuiSelectFieldDialog(props: SelectFieldDialogProps) {
61
61
  return (
62
62
  <DialogFrame
63
63
  title={field.label}
64
- footer="Use ↑↓ to choose · enter to apply · esc cancel"
65
64
  >
66
65
  <ListView
67
66
  items={field.options.map((option) => ({
@@ -158,7 +157,6 @@ function TuiTextFieldDialog(props: TextFieldDialogProps) {
158
157
  return (
159
158
  <DialogFrame
160
159
  title={field.label}
161
- footer="Enter to apply · esc cancel"
162
160
  >
163
161
  <Box flexDirection="column" gap={1}>
164
162
  <TextField
@@ -29,7 +29,7 @@ export function TuiPaneSettingsDialogBody({
29
29
  onActivate: (field: PaneSettingField | undefined) => void;
30
30
  }) {
31
31
  return (
32
- <DialogFrame title={title} footer="Use ↑↓ to choose · enter to edit · esc cancel">
32
+ <DialogFrame title={title}>
33
33
  <ListView
34
34
  items={fields.map((field) => ({
35
35
  id: field.key,
@@ -3,7 +3,6 @@ import {
3
3
  useEffect,
4
4
  useMemo,
5
5
  useRef,
6
- useState,
7
6
  type ReactNode,
8
7
  type RefObject,
9
8
  } from "react";
@@ -61,8 +60,6 @@ export interface TickerListTableViewProps {
61
60
  setHoveredIdx?: (index: number | null) => void;
62
61
  keyboardNavigation?: boolean;
63
62
  onRootKeyDown?: (event: DataTableKeyEvent) => boolean | void;
64
- onSelectIndex?: (index: number, ticker: TickerRecord) => void;
65
- onActivateIndex?: (index: number, ticker: TickerRecord) => void;
66
63
  onVisibleRangeChange?: (range: TickerListVisibleRange) => void;
67
64
  visibleRangeBuffer?: number;
68
65
  resetScrollKey?: unknown;
@@ -120,8 +117,6 @@ export function TickerListTableView({
120
117
  setHoveredIdx,
121
118
  keyboardNavigation = true,
122
119
  onRootKeyDown,
123
- onSelectIndex,
124
- onActivateIndex,
125
120
  onVisibleRangeChange,
126
121
  visibleRangeBuffer = 0,
127
122
  resetScrollKey,
@@ -153,8 +148,6 @@ export function TickerListTableView({
153
148
  () => tickers.findIndex((ticker) => ticker.metadata.ticker === cursorSymbol),
154
149
  [cursorSymbol, tickers],
155
150
  );
156
- const [scrollToIndex, setScrollToIndex] = useState<number | null>(null);
157
- const [scrollToIndexVersion, setScrollToIndexVersion] = useState(0);
158
151
  const lastScrollCursorSymbolRef = useRef<string | null | undefined>(undefined);
159
152
 
160
153
  const emitVisibleRange = useCallback(() => {
@@ -177,16 +170,12 @@ export function TickerListTableView({
177
170
  useEffect(() => {
178
171
  const shouldScrollToCursor = lastScrollCursorSymbolRef.current !== cursorSymbol;
179
172
  lastScrollCursorSymbolRef.current = cursorSymbol;
180
- if (shouldScrollToCursor && selectedIndex >= 0) {
181
- setScrollToIndex(selectedIndex);
182
- setScrollToIndexVersion((current) => current + 1);
183
- }
184
- queueMicrotask(emitVisibleRange);
173
+ if (shouldScrollToCursor && selectedIndex >= 0) queueMicrotask(emitVisibleRange);
185
174
  }, [cursorSymbol, emitVisibleRange, selectedIndex]);
186
175
 
187
176
  useEffect(() => {
188
177
  queueMicrotask(emitVisibleRange);
189
- }, [emitVisibleRange, scrollToIndexVersion]);
178
+ }, [emitVisibleRange]);
190
179
 
191
180
  const renderCell = useCallback((
192
181
  ticker: TickerRecord,
@@ -236,18 +225,16 @@ export function TickerListTableView({
236
225
  }, [financialsMap, renderer, showContextMenu]);
237
226
 
238
227
  const handleRowMouseDown = useCallback((ticker: TickerRecord, _index: number, event: TableMouseEvent) => {
239
- setCursorSymbol(ticker.metadata.ticker);
240
228
  if (event.button !== 2) return false;
241
229
  if (nativeContextMenu !== true) {
242
230
  showTickerContextMenu(ticker, event);
243
231
  }
244
232
  return true;
245
- }, [nativeContextMenu, setCursorSymbol, showTickerContextMenu]);
233
+ }, [nativeContextMenu, showTickerContextMenu]);
246
234
 
247
235
  const handleRowContextMenu = useCallback((ticker: TickerRecord, _index: number, event: TableMouseEvent) => {
248
- setCursorSymbol(ticker.metadata.ticker);
249
236
  showTickerContextMenu(ticker, event);
250
- }, [setCursorSymbol, showTickerContextMenu]);
237
+ }, [showTickerContextMenu]);
251
238
 
252
239
  return (
253
240
  <DataTableView<TickerRecord, ColumnConfig>
@@ -258,9 +245,13 @@ export function TickerListTableView({
258
245
  sortDirection={sortDirection}
259
246
  onHeaderClick={onHeaderClick ?? (() => {})}
260
247
  getItemKey={(ticker) => ticker.metadata.ticker}
261
- isSelected={(ticker) => ticker.metadata.ticker === cursorSymbol}
262
- onSelect={(ticker) => {
263
- setCursorSymbol(ticker.metadata.ticker);
248
+ selection={{
249
+ kind: "id",
250
+ selectedId: cursorSymbol,
251
+ getId: (ticker) => ticker.metadata.ticker,
252
+ onChange: (symbol) => {
253
+ setCursorSymbol(symbol);
254
+ },
264
255
  }}
265
256
  onActivate={(ticker) => {
266
257
  onRowActivate?.(ticker);
@@ -273,9 +264,6 @@ export function TickerListTableView({
273
264
  emptyStateHint={emptyHint}
274
265
  virtualize={virtualize}
275
266
  overscan={overscan}
276
- selectedIndex={selectedIndex}
277
- onSelectIndex={onSelectIndex}
278
- onActivateIndex={onActivateIndex}
279
267
  rootBefore={rootBefore}
280
268
  rootAfter={rootAfter}
281
269
  rootWidth={rootWidth}
@@ -290,8 +278,6 @@ export function TickerListTableView({
290
278
  keyboardNavigation={keyboardNavigation}
291
279
  onRootKeyDown={onRootKeyDown}
292
280
  resetScrollKey={resetScrollKey}
293
- scrollToIndex={scrollToIndex}
294
- scrollToIndexVersion={scrollToIndexVersion}
295
281
  />
296
282
  );
297
283
  }
@@ -17,6 +17,7 @@ export interface ChoiceDialogChoice {
17
17
  export interface ChoiceDialogProps extends PromptContext<string> {
18
18
  title: string;
19
19
  choices: ChoiceDialogChoice[];
20
+ selectedChoiceId?: string;
20
21
  footer?: string;
21
22
  bgColor?: string;
22
23
  }
@@ -30,14 +31,23 @@ function getChoiceDescription(choice: ChoiceDialogChoice | undefined): string {
30
31
  return choice?.description ?? "";
31
32
  }
32
33
 
34
+ function getInitialChoiceIndex(choices: ChoiceDialogChoice[], selectedChoiceId: string | undefined): number {
35
+ if (!selectedChoiceId) return 0;
36
+ const selectedIndex = choices.findIndex((choice) => choice.id === selectedChoiceId);
37
+ return selectedIndex >= 0 ? selectedIndex : 0;
38
+ }
39
+
33
40
  export function ChoiceDialog({
34
41
  resolve,
35
42
  title,
36
43
  choices,
37
- footer = "↑↓ choose · Enter/click select · Esc cancel",
44
+ selectedChoiceId,
45
+ footer,
38
46
  bgColor = colors.bg,
39
47
  }: ChoiceDialogProps) {
40
- const [index, setIndex] = useState(() => clampChoiceIndex(0, choices.length));
48
+ const [index, setIndex] = useState(() =>
49
+ clampChoiceIndex(getInitialChoiceIndex(choices, selectedChoiceId), choices.length)
50
+ );
41
51
  const selectedIndex = clampChoiceIndex(index, choices.length);
42
52
  const selectedChoice = selectedIndex >= 0 ? choices[selectedIndex] : undefined;
43
53
  const items = useMemo<ListViewItem[]>(() => choices.map((choice) => ({
@@ -2,7 +2,7 @@ import { Box, Text, useUiHost } from "../../../ui";
2
2
  import { TextAttributes } from "../../../ui";
3
3
  import { useShortcut } from "../../../react/input";
4
4
  import { type AlertContext, useDialog, useDialogKeyboard } from "../../../ui/dialog";
5
- import { type ReactNode, useEffect, useMemo, useState } from "react";
5
+ import { type ReactNode, useEffect, useMemo, useRef, useState } from "react";
6
6
  import { colors } from "../../../theme/colors";
7
7
  import { isDetailBackNavigationKey } from "../../../utils/back-navigation";
8
8
  import { isPlainKey } from "../../../utils/keyboard";
@@ -253,6 +253,7 @@ export function MultiSelectDialogButton({
253
253
  }: MultiSelectDialogButtonProps) {
254
254
  const isDesktopWeb = useUiHost().kind === "desktop-web";
255
255
  const dialog = useDialog();
256
+ const triggerMouseDownRef = useRef(false);
256
257
  const summary = summarizeMultiSelectValues({ options, selectedValues, emptyLabel });
257
258
  const buttonLabel = `${label}: ${summary}`;
258
259
  const buttonText = ` ${buttonLabel} `;
@@ -311,6 +312,17 @@ export function MultiSelectDialogButton({
311
312
  );
312
313
  }
313
314
 
315
+ const startTriggerPress = (event?: DialogTriggerEvent) => {
316
+ triggerMouseDownRef.current = true;
317
+ stopMouseEvent(event);
318
+ };
319
+ const finishTriggerPress = (event?: DialogTriggerEvent) => {
320
+ const startedOnTrigger = triggerMouseDownRef.current;
321
+ triggerMouseDownRef.current = false;
322
+ if (startedOnTrigger) openDialog(event);
323
+ else stopMouseEvent(event);
324
+ };
325
+
314
326
  return (
315
327
  <Box
316
328
  id={idPrefix ? `${idPrefix}:button` : undefined}
@@ -318,14 +330,14 @@ export function MultiSelectDialogButton({
318
330
  width={buttonText.length}
319
331
  flexDirection="row"
320
332
  backgroundColor={disabled ? colors.panel : colors.selected}
321
- onMouseDown={stopMouseEvent}
322
- onMouseUp={openDialog}
333
+ onMouseDown={startTriggerPress}
334
+ onMouseUp={finishTriggerPress}
323
335
  >
324
336
  <Text
325
337
  fg={disabled ? colors.textMuted : colors.selectedText}
326
338
  attributes={TextAttributes.BOLD}
327
- onMouseDown={stopMouseEvent}
328
- onMouseUp={openDialog}
339
+ onMouseDown={startTriggerPress}
340
+ onMouseUp={finishTriggerPress}
329
341
  >
330
342
  {buttonText}
331
343
  </Text>
@@ -1,5 +1,6 @@
1
1
  import { StyledText, Text, TextAttributes } from "../../ui";
2
2
  import { colors } from "../../theme/colors";
3
+ import { useRef } from "react";
3
4
 
4
5
  type ShortcutHintMouseEvent = {
5
6
  stopPropagation?: () => void;
@@ -36,6 +37,17 @@ export function ShortcutHint({
36
37
  const keyColor = disabled ? colors.textMuted : colors.textBright;
37
38
  const labelColor = disabled ? colors.textMuted : colors.textDim;
38
39
  const keyText = `[${hotkey}]`;
40
+ const triggerMouseDownRef = useRef(false);
41
+ const startPress = (event?: ShortcutHintMouseEvent) => {
42
+ triggerMouseDownRef.current = true;
43
+ stopMouseEvent(event);
44
+ };
45
+ const finishPress = (event?: ShortcutHintMouseEvent) => {
46
+ const startedOnTrigger = triggerMouseDownRef.current;
47
+ triggerMouseDownRef.current = false;
48
+ if (startedOnTrigger) onPress?.(event);
49
+ else stopMouseEvent(event);
50
+ };
39
51
 
40
52
  return (
41
53
  <Text
@@ -47,8 +59,8 @@ export function ShortcutHint({
47
59
  ])}
48
60
  fg={disabled ? colors.textMuted : colors.textDim}
49
61
  attributes={interactive ? TextAttributes.BOLD : 0}
50
- onMouseDown={interactive ? stopMouseEvent : undefined}
51
- onMouseUp={interactive ? onPress : undefined}
62
+ onMouseDown={interactive ? startPress : undefined}
63
+ onMouseUp={interactive ? finishPress : undefined}
52
64
  {...(dataGloomRole ? { "data-gloom-role": dataGloomRole } : {})}
53
65
  />
54
66
  );
@@ -27,7 +27,7 @@ describe("table layout", () => {
27
27
  ]);
28
28
 
29
29
  expect(template).toBe(
30
- "minmax(4ch, 4ch) minmax(14ch, 40fr) minmax(8ch, 24ch) minmax(5ch, 5ch)",
30
+ "minmax(calc(4 * var(--cell-w)), calc(4 * var(--cell-w))) minmax(calc(14 * var(--cell-w)), 40fr) minmax(calc(8 * var(--cell-w)), calc(24 * var(--cell-w))) minmax(calc(5 * var(--cell-w)), calc(5 * var(--cell-w)))",
31
31
  );
32
32
  });
33
33
 
@@ -37,6 +37,6 @@ describe("table layout", () => {
37
37
  { width: 8, align: "right" },
38
38
  ]);
39
39
 
40
- expect(template).toBe("minmax(8ch, 12fr) minmax(8ch, 8fr)");
40
+ expect(template).toBe("minmax(calc(8 * var(--cell-w)), 12fr) minmax(calc(8 * var(--cell-w)), 8fr)");
41
41
  });
42
42
  });
@@ -8,6 +8,7 @@ export interface TableWidthColumn {
8
8
  }
9
9
 
10
10
  const TRAILING_COLUMN_GUTTER_WIDTH = 1;
11
+ const WEB_CELL_UNIT = "var(--cell-w)";
11
12
 
12
13
  export function getTableWidth(columns: readonly TableWidthColumn[]): number {
13
14
  return columns.reduce((sum, column) => sum + column.width + 1, 2);
@@ -57,15 +58,20 @@ function columnFlexWeight(column: TableWidthColumn): number {
57
58
  return flexGrow > 0 ? baseWeight * Math.max(1, flexGrow) : baseWeight;
58
59
  }
59
60
 
61
+ function cellWidthCss(width: number): string {
62
+ return `calc(${width} * ${WEB_CELL_UNIT})`;
63
+ }
64
+
60
65
  export function buildTableGridTemplateColumns(columns: readonly TableWidthColumn[]): string {
61
66
  const hasFlexColumn = columns.some((column) => (column.flexGrow ?? 0) > 0);
62
67
  return columns
63
68
  .map((column) => {
64
69
  const width = normalizedColumnWidth(column);
70
+ const minWidth = cellWidthCss(columnMinCh(column));
65
71
  if (!hasFlexColumn || (column.flexGrow ?? 0) > 0) {
66
- return `minmax(${columnMinCh(column)}ch, ${columnFlexWeight(column)}fr)`;
72
+ return `minmax(${minWidth}, ${columnFlexWeight(column)}fr)`;
67
73
  }
68
- return `minmax(${columnMinCh(column)}ch, ${width}ch)`;
74
+ return `minmax(${minWidth}, ${cellWidthCss(width)})`;
69
75
  })
70
76
  .join(" ");
71
77
  }
@@ -48,6 +48,12 @@ export function Tabs({
48
48
  }: TabsProps) {
49
49
  const ui = useUiHost();
50
50
  const NativeTabs = ui.Tabs;
51
+ const navigationValueRef = useRef<string | null>(activeValue);
52
+ const renderedActiveValueRef = useRef<string | null>(activeValue);
53
+ if (renderedActiveValueRef.current !== activeValue) {
54
+ renderedActiveValueRef.current = activeValue;
55
+ navigationValueRef.current = activeValue;
56
+ }
51
57
  const palette = {
52
58
  activeFg: variant === "bare" ? colors.textBright : colors.text,
53
59
  inactiveFg: colors.textDim,
@@ -62,18 +68,26 @@ export function Tabs({
62
68
  closeFg: colors.textMuted,
63
69
  addFg: colors.textMuted,
64
70
  };
71
+ const handleSelect = useCallback((value: string) => {
72
+ navigationValueRef.current = value;
73
+ onSelect(value);
74
+ }, [onSelect]);
75
+
65
76
  const selectAdjacentTab = useCallback((direction: -1 | 1) => {
66
77
  const enabledTabs = tabs.filter((tab) => !tab.disabled);
67
78
  if (enabledTabs.length === 0) return;
68
79
 
69
- const activeIndex = enabledTabs.findIndex((tab) => tab.value === activeValue);
80
+ const navigationValue = enabledTabs.some((tab) => tab.value === navigationValueRef.current)
81
+ ? navigationValueRef.current
82
+ : activeValue;
83
+ const activeIndex = enabledTabs.findIndex((tab) => tab.value === navigationValue);
70
84
  const nextIndex = activeIndex >= 0
71
85
  ? Math.max(0, Math.min(activeIndex + direction, enabledTabs.length - 1))
72
86
  : direction > 0 ? 0 : enabledTabs.length - 1;
73
87
  const nextTab = enabledTabs[nextIndex];
74
- if (!nextTab || nextTab.value === activeValue) return;
75
- onSelect(nextTab.value);
76
- }, [activeValue, onSelect, tabs]);
88
+ if (!nextTab || nextTab.value === navigationValue) return;
89
+ handleSelect(nextTab.value);
90
+ }, [activeValue, handleSelect, tabs]);
77
91
 
78
92
  useShortcut((event) => {
79
93
  if (event.ctrl || event.meta || event.alt || event.targetEditable) return;
@@ -96,7 +110,7 @@ export function Tabs({
96
110
  <NativeTabs
97
111
  tabs={tabs}
98
112
  activeValue={activeValue}
99
- onSelect={onSelect}
113
+ onSelect={handleSelect}
100
114
  compact={compact}
101
115
  variant={variant}
102
116
  closeMode={closeMode}
@@ -111,7 +125,7 @@ export function Tabs({
111
125
  <OpenTuiTabs
112
126
  tabs={tabs}
113
127
  activeValue={activeValue}
114
- onSelect={onSelect}
128
+ onSelect={handleSelect}
115
129
  compact={compact}
116
130
  variant={variant}
117
131
  closeMode={closeMode}
@@ -151,6 +151,7 @@ function DataTableHorizontalScrollHarness({
151
151
  <DataTableView
152
152
  rootWidth={containerWidth}
153
153
  rootHeight={containerHeight}
154
+ selection={{ kind: "none" }}
154
155
  columns={columns}
155
156
  items={items}
156
157
  sortColumnId={null}
@@ -159,8 +160,6 @@ function DataTableHorizontalScrollHarness({
159
160
  headerScrollRef={headerScrollRef}
160
161
  scrollRef={scrollRef}
161
162
  getItemKey={(row) => row.id}
162
- isSelected={() => false}
163
- onSelect={() => {}}
164
163
  renderCell={(row) => ({ text: row.name })}
165
164
  emptyStateTitle="No rows."
166
165
  showHorizontalScrollbar={showHorizontalScrollbar}
@@ -228,7 +227,7 @@ function MultiSelectDialogButtonHarness() {
228
227
  );
229
228
  }
230
229
 
231
- function ChoiceDialogHarness() {
230
+ function ChoiceDialogHarness({ selectedChoiceId }: { selectedChoiceId?: string } = {}) {
232
231
  return (
233
232
  <ChoiceDialog
234
233
  title="Choose Account"
@@ -241,6 +240,7 @@ function ChoiceDialogHarness() {
241
240
  { id: "beta", label: "Beta", description: "Beta account" },
242
241
  { id: "gamma", label: "Gamma", description: "Gamma account" },
243
242
  ]}
243
+ selectedChoiceId={selectedChoiceId}
244
244
  />
245
245
  );
246
246
  }
@@ -318,6 +318,7 @@ describe("shared UI kit", () => {
318
318
 
319
319
  test("moves focused tabs with arrow keys and leaves Tab for pane focus", async () => {
320
320
  let lastSelected = "overview";
321
+ const selectedValues: string[] = [];
321
322
 
322
323
  function KeyboardTabsHarness() {
323
324
  const [activeValue, setActiveValue] = useState("overview");
@@ -331,6 +332,7 @@ describe("shared UI kit", () => {
331
332
  activeValue={activeValue}
332
333
  onSelect={(value) => {
333
334
  lastSelected = value;
335
+ selectedValues.push(value);
334
336
  setActiveValue(value);
335
337
  }}
336
338
  focused
@@ -343,17 +345,19 @@ describe("shared UI kit", () => {
343
345
  await act(async () => {
344
346
  await testSetup!.renderOnce();
345
347
  testSetup!.mockInput.pressArrow("right");
348
+ testSetup!.mockInput.pressArrow("right");
346
349
  await testSetup!.renderOnce();
347
350
  });
348
351
 
349
- expect(lastSelected).toBe("news");
352
+ expect(selectedValues).toEqual(["news", "chart"]);
353
+ expect(lastSelected).toBe("chart");
350
354
 
351
355
  await act(async () => {
352
356
  testSetup!.mockInput.pressTab();
353
357
  await testSetup!.renderOnce();
354
358
  });
355
359
 
356
- expect(lastSelected).toBe("news");
360
+ expect(lastSelected).toBe("chart");
357
361
  });
358
362
 
359
363
  test("renders tab actions for editable tab sets", async () => {
@@ -407,6 +411,15 @@ describe("shared UI kit", () => {
407
411
  expect(button).toBeDefined();
408
412
  expect(button!.width).toBe(" IND: SMA ".length);
409
413
 
414
+ await act(async () => {
415
+ await testSetup!.mockMouse.release(button!.x + 1, button!.y);
416
+ await Promise.resolve();
417
+ await testSetup!.renderOnce();
418
+ });
419
+
420
+ frame = testSetup.captureCharFrame();
421
+ expect(frame).not.toContain("Chart Indicators");
422
+
410
423
  await act(async () => {
411
424
  await testSetup!.mockMouse.click(button!.x + 1, button!.y);
412
425
  await Promise.resolve();
@@ -470,6 +483,18 @@ describe("shared UI kit", () => {
470
483
  expect(resolvedChoice).toBe("gamma");
471
484
  });
472
485
 
486
+ test("preselects the current choice in choice dialogs", async () => {
487
+ testSetup = await testRender(<ChoiceDialogHarness selectedChoiceId="beta" />, { width: 44, height: 10 });
488
+
489
+ await act(async () => {
490
+ await testSetup!.renderOnce();
491
+ });
492
+
493
+ const frame = testSetup.captureCharFrame();
494
+ expect(frame).toContain("Beta account");
495
+ expect(frame).not.toContain("Alpha account");
496
+ });
497
+
473
498
  test("cancels choice dialogs with escape", async () => {
474
499
  testSetup = await testRender(<ChoiceDialogHarness />, { width: 44, height: 10 });
475
500
 
@@ -1,4 +1,4 @@
1
- import type { DataProvider, SecFilingItem } from "../../types/data-provider";
1
+ import type { DataProvider, SecFilingDocument, SecFilingItem } from "../../types/data-provider";
2
2
  import type { NewsArticle } from "../../news/types";
3
3
  import type { OptionsChain } from "../../types/financials";
4
4
  import type { NewsRequest, OptionsRequest, SecFilingsRequest } from "../request-types";
@@ -26,6 +26,7 @@ import {
26
26
  buildNewsKey,
27
27
  buildOptionsKey,
28
28
  buildSecContentKey,
29
+ buildSecDocumentsKey,
29
30
  buildSecFilingsKey,
30
31
  toMarketDataContext,
31
32
  } from "../selectors";
@@ -170,6 +171,33 @@ export function loadSecFilingContentEntry(options: {
170
171
  });
171
172
  }
172
173
 
174
+ export function loadSecFilingDocumentsEntry(options: {
175
+ dataProvider: DataProvider;
176
+ filing: SecFilingItem;
177
+ store: QueryStore<SecFilingDocument[]>;
178
+ runSingleFlight: RunSingleFlight;
179
+ }): Promise<QueryEntry<SecFilingDocument[]>> {
180
+ const { dataProvider, filing, store, runSingleFlight } = options;
181
+ const key = buildSecDocumentsKey(filing.accessionNumber);
182
+ return loadAuxiliaryEntry({
183
+ dataProvider,
184
+ key,
185
+ store,
186
+ ttlMs: SEC_CONTENT_CACHE_TTL_MS,
187
+ runSingleFlight,
188
+ load: async (startedAt) => {
189
+ if (!dataProvider.getSecFilingDocuments) {
190
+ const attempt = createAttempt(dataProvider.id, startedAt, "unsupported", "UNSUPPORTED_RANGE", "SEC filing documents are not available");
191
+ return store.update(key, (current) => errorEntry(current, attempt));
192
+ }
193
+ const data = await dataProvider.getSecFilingDocuments(filing);
194
+ const status = data.length > 0 ? "success" : "empty";
195
+ const attempts = [createAttempt(dataProvider.id, startedAt, status, data.length > 0 ? undefined : "NO_DATA")];
196
+ return store.update(key, (current) => readyEntry(current, data.length > 0 ? data : null, dataProvider.id, attempts, { keepLastGoodOnEmpty: true }));
197
+ },
198
+ });
199
+ }
200
+
173
201
  export function loadArticleSummaryEntry(options: {
174
202
  dataProvider: DataProvider;
175
203
  url: string;
@@ -1,4 +1,4 @@
1
- import type { DataProvider, SecFilingItem } from "../../types/data-provider";
1
+ import type { DataProvider, SecFilingDocument, SecFilingItem } from "../../types/data-provider";
2
2
  import type { NewsArticle } from "../../news/types";
3
3
  import type { OptionsChain, PricePoint, Quote, TickerFinancials } from "../../types/financials";
4
4
  import type { ChartRequest, InstrumentRef, NewsRequest, OptionsRequest, SecFilingsRequest } from "../request-types";
@@ -13,6 +13,7 @@ import {
13
13
  buildOptionsKey,
14
14
  buildQuoteKey,
15
15
  buildSecContentKey,
16
+ buildSecDocumentsKey,
16
17
  buildSecFilingsKey,
17
18
  buildSnapshotKey,
18
19
  resolveEntryData,
@@ -41,6 +42,7 @@ import {
41
42
  loadNewsEntry,
42
43
  loadOptionsEntry,
43
44
  loadSecFilingContentEntry,
45
+ loadSecFilingDocumentsEntry,
44
46
  loadSecFilingsEntry,
45
47
  } from "./auxiliary";
46
48
  import {
@@ -72,6 +74,7 @@ export class MarketDataCoordinator {
72
74
  private readonly newsStore = new QueryStore<NewsArticle[]>((key) => this.events.bump(key));
73
75
  private readonly optionsStore = new QueryStore<OptionsChain>((key) => this.events.bump(key));
74
76
  private readonly secFilingsStore = new QueryStore<SecFilingItem[]>((key) => this.events.bump(key));
77
+ private readonly secDocumentsStore = new QueryStore<SecFilingDocument[]>((key) => this.events.bump(key));
75
78
  private readonly secContentStore = new QueryStore<string | null>((key) => this.events.bump(key));
76
79
  private readonly articleSummaryStore = new QueryStore<string | null>((key) => this.events.bump(key));
77
80
  private readonly fxStore = new QueryStore<number>((key) => this.events.bump(key));
@@ -135,6 +138,10 @@ export class MarketDataCoordinator {
135
138
  return this.secContentStore.get(buildSecContentKey(accessionNumber));
136
139
  }
137
140
 
141
+ getSecDocumentsEntry(accessionNumber: string): QueryEntry<SecFilingDocument[]> {
142
+ return this.secDocumentsStore.get(buildSecDocumentsKey(accessionNumber));
143
+ }
144
+
138
145
  getArticleSummaryEntry(url: string): QueryEntry<string | null> {
139
146
  return this.articleSummaryStore.get(buildArticleSummaryKey(url));
140
147
  }
@@ -336,6 +343,15 @@ export class MarketDataCoordinator {
336
343
  });
337
344
  }
338
345
 
346
+ async loadSecFilingDocuments(filing: SecFilingItem): Promise<QueryEntry<SecFilingDocument[]>> {
347
+ return loadSecFilingDocumentsEntry({
348
+ dataProvider: this.dataProvider,
349
+ filing,
350
+ store: this.secDocumentsStore,
351
+ runSingleFlight: (key, task) => this.runSingleFlight(key, task),
352
+ });
353
+ }
354
+
339
355
  async loadArticleSummary(url: string): Promise<QueryEntry<string | null>> {
340
356
  return loadArticleSummaryEntry({
341
357
  dataProvider: this.dataProvider,
@@ -1,5 +1,5 @@
1
1
  import { useCallback, useEffect, useMemo, useRef, useSyncExternalStore } from "react";
2
- import type { SecFilingItem } from "../types/data-provider";
2
+ import type { SecFilingDocument, SecFilingItem } from "../types/data-provider";
3
3
  import type { OptionsChain, PricePoint, Quote, TickerFinancials } from "../types/financials";
4
4
  import type { TickerRecord } from "../types/ticker";
5
5
  import type { ChartRequest, InstrumentRef, OptionsRequest, SecFilingsRequest } from "./request-types";
@@ -17,6 +17,7 @@ import {
17
17
  buildOptionsKey,
18
18
  buildQuoteKey,
19
19
  buildSecContentKey,
20
+ buildSecDocumentsKey,
20
21
  buildSecFilingsKey,
21
22
  buildSnapshotKey,
22
23
  } from "./selectors";
@@ -315,6 +316,21 @@ export function useSecFilingContent(filing: SecFilingItem | null | undefined): Q
315
316
  return entry;
316
317
  }
317
318
 
319
+ export function useSecFilingDocuments(filing: SecFilingItem | null | undefined): QueryEntry<SecFilingDocument[]> | null {
320
+ const key = filing ? buildSecDocumentsKey(filing.accessionNumber) : null;
321
+ useCoordinatorKeysVersion(key ? [key] : []);
322
+ const coordinator = getSharedMarketDataCoordinator();
323
+ const entry = coordinator && filing ? coordinator.getSecDocumentsEntry(filing.accessionNumber) : null;
324
+
325
+ useEffect(() => {
326
+ const coordinator = getSharedMarketDataCoordinator();
327
+ if (!coordinator || !filing) return;
328
+ void coordinator.loadSecFilingDocuments(filing).catch(() => {});
329
+ }, [key]);
330
+
331
+ return entry;
332
+ }
333
+
318
334
  export function useArticleSummary(url: string | null | undefined): QueryEntry<string | null> | null {
319
335
  const key = url ? buildArticleSummaryKey(url) : null;
320
336
  useCoordinatorKeysVersion(key ? [key] : []);
@@ -77,6 +77,10 @@ export function buildSecContentKey(accessionNumber: string): string {
77
77
  return `sec-content:${accessionNumber}`;
78
78
  }
79
79
 
80
+ export function buildSecDocumentsKey(accessionNumber: string): string {
81
+ return `sec-documents:${accessionNumber}`;
82
+ }
83
+
80
84
  export function buildArticleSummaryKey(url: string): string {
81
85
  return `article-summary:${url.trim()}`;
82
86
  }
@@ -90,10 +90,6 @@ export function PickerRow({
90
90
  event?.stopPropagation?.();
91
91
  event?.preventDefault?.();
92
92
  onFocus();
93
- }}
94
- onMouseUp={(event?: { stopPropagation?: () => void; preventDefault?: () => void }) => {
95
- event?.stopPropagation?.();
96
- event?.preventDefault?.();
97
93
  onOpen();
98
94
  }}
99
95
  >