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.
- package/README.md +102 -52
- package/package.json +1 -1
- package/src/api-client/chat.ts +8 -0
- package/src/api-client/index.test.ts +30 -0
- package/src/api-client/index.ts +4 -0
- package/src/api-client/normalizers.ts +1 -0
- package/src/api-client/types.ts +1 -0
- package/src/capabilities/factories.ts +4 -0
- package/src/components/chart/comparison/data.test.ts +54 -0
- package/src/components/chart/comparison/data.ts +47 -24
- package/src/components/chart/core/projection.ts +51 -27
- package/src/components/data-table/stack-view.test.tsx +14 -11
- package/src/components/data-table/stack-view.tsx +1 -2
- package/src/components/data-table/view.test.tsx +24 -18
- package/src/components/data-table/view.tsx +313 -83
- package/src/components/feed-data-table/stack-view.tsx +6 -8
- package/src/components/layout/pane/footer/index.test.tsx +6 -0
- package/src/components/layout/pane/footer/index.tsx +14 -2
- package/src/components/layout/pane/title.ts +5 -1
- package/src/components/pane-settings-dialog/field-dialogs.tsx +0 -2
- package/src/components/pane-settings-dialog/tui.tsx +1 -1
- package/src/components/ticker/list-table-view.tsx +11 -25
- package/src/components/ui/choice-dialog.tsx +12 -2
- package/src/components/ui/multi-select/dialog.tsx +17 -5
- package/src/components/ui/shortcut-hint.tsx +14 -2
- package/src/components/ui/table-layout.test.ts +2 -2
- package/src/components/ui/table-layout.ts +8 -2
- package/src/components/ui/tabs.tsx +20 -6
- package/src/components/ui/ui.test.tsx +30 -5
- package/src/market-data/coordinator/auxiliary.ts +29 -1
- package/src/market-data/coordinator/index.ts +17 -1
- package/src/market-data/hooks.tsx +17 -1
- package/src/market-data/selectors.ts +4 -0
- package/src/plugins/builtin/account-management/form-components.tsx +0 -4
- package/src/plugins/builtin/account-management/model.test.ts +54 -0
- package/src/plugins/builtin/account-management/model.ts +13 -3
- package/src/plugins/builtin/account-management/pane.tsx +5 -11
- package/src/plugins/builtin/account-management/password-dialog.tsx +1 -1
- package/src/plugins/builtin/alerts/pane.tsx +5 -3
- package/src/plugins/builtin/analytics/index.tsx +0 -3
- package/src/plugins/builtin/analytics/view.tsx +6 -5
- package/src/plugins/builtin/broker-manager/index.tsx +6 -6
- package/src/plugins/builtin/buildout/pane/index.tsx +6 -6
- package/src/plugins/builtin/changelog/index.tsx +40 -27
- package/src/plugins/builtin/changelog/model.test.ts +77 -0
- package/src/plugins/builtin/changelog/model.ts +98 -0
- package/src/plugins/builtin/chat/channel-labels.ts +53 -0
- package/src/plugins/builtin/chat/channels.test.ts +28 -0
- package/src/plugins/builtin/chat/channels.ts +36 -19
- package/src/plugins/builtin/chat/chat.test.tsx +264 -5
- package/src/plugins/builtin/chat/content/channel-navigation.ts +6 -0
- package/src/plugins/builtin/chat/content/composer-runtime.test.tsx +144 -0
- package/src/plugins/builtin/chat/content/composer-runtime.ts +151 -20
- package/src/plugins/builtin/chat/content/composer.tsx +23 -2
- package/src/plugins/builtin/chat/content/index.tsx +61 -0
- package/src/plugins/builtin/chat/content/layout-metrics.ts +3 -1
- package/src/plugins/builtin/chat/content/shortcuts.ts +20 -1
- package/src/plugins/builtin/chat/content/transcript.tsx +8 -0
- package/src/plugins/builtin/chat/content/types.ts +1 -0
- package/src/plugins/builtin/chat/controller/index.ts +41 -0
- package/src/plugins/builtin/chat/controller/messages.ts +18 -0
- package/src/plugins/builtin/chat/controller/notifications.ts +10 -4
- package/src/plugins/builtin/chat/controller/utils.ts +6 -3
- package/src/plugins/builtin/chat/controller/view.ts +1 -1
- package/src/plugins/builtin/chat/controller.test.ts +189 -0
- package/src/plugins/builtin/chat/edit-window.ts +24 -0
- package/src/plugins/builtin/chat/layout.ts +94 -31
- package/src/plugins/builtin/chat/message/action-chip.tsx +1 -0
- package/src/plugins/builtin/chat/message/desktop.tsx +53 -20
- package/src/plugins/builtin/chat/message/inline-tokens.tsx +16 -10
- package/src/plugins/builtin/chat/message/render-state.ts +5 -1
- package/src/plugins/builtin/chat/message/terminal.tsx +49 -20
- package/src/plugins/builtin/chat/message/types.ts +2 -0
- package/src/plugins/builtin/chat/pane.tsx +35 -7
- package/src/plugins/builtin/chat/sidebar.test.tsx +1 -0
- package/src/plugins/builtin/chat/status-widget.tsx +29 -11
- package/src/plugins/builtin/chat/test-harness.tsx +2 -0
- package/src/plugins/builtin/cloud/plugin.tsx +3 -0
- package/src/plugins/builtin/cloud-tweets/table.tsx +7 -11
- package/src/plugins/builtin/congress-trades/pane.tsx +14 -22
- package/src/plugins/builtin/earnings/index.tsx +8 -7
- package/src/plugins/builtin/econ/index.tsx +6 -8
- package/src/plugins/builtin/help/index.tsx +6 -0
- package/src/plugins/builtin/holders/pane.tsx +7 -6
- package/src/plugins/builtin/market-movers/index.tsx +6 -3
- package/src/plugins/builtin/news/wire/news/table.tsx +7 -6
- package/src/plugins/builtin/options/view.tsx +16 -8
- package/src/plugins/builtin/research/analyst-pane.tsx +1 -2
- package/src/plugins/builtin/research/corporate-actions-pane.tsx +358 -7
- package/src/plugins/builtin/research/index.test.ts +30 -1
- package/src/plugins/builtin/research/relative-valuation-pane.tsx +6 -8
- package/src/plugins/builtin/sec/filing-documents.ts +66 -0
- package/src/plugins/builtin/sec/index.tsx +132 -20
- package/src/plugins/builtin/sectors/index.tsx +13 -23
- package/src/plugins/builtin/shared/ticker-request.ts +1 -0
- package/src/plugins/builtin/thirteenf/pane.tsx +26 -25
- package/src/plugins/builtin/ticker-detail/data-panes/fundamental-graph/content.tsx +5 -4
- package/src/plugins/builtin/ticker-detail/data-panes/historical-prices.tsx +5 -4
- package/src/plugins/builtin/ticker-detail/data-panes/provider-search.tsx +6 -5
- package/src/plugins/builtin/ticker-detail/financials/tab.tsx +10 -8
- package/src/plugins/builtin/world-indices/index.tsx +9 -6
- package/src/plugins/prediction-markets/pane.tsx +10 -9
- package/src/plugins/registry/index.ts +3 -0
- package/src/plugins/runtime/context.tsx +1 -0
- package/src/plugins/runtime/index.tsx +4 -0
- package/src/renderers/electrobun/view/cli-pane-shot-entry.tsx +1 -0
- package/src/renderers/electrobun/view/data-table/index.tsx +3 -2
- package/src/renderers/electrobun/view/data-table/row.tsx +2 -2
- package/src/renderers/electrobun/view/host/input.tsx +23 -2
- package/src/renderers/electrobun/view/remote/asset-data-client.ts +1 -0
- package/src/sources/gloomberb-cloud/index.ts +4 -0
- package/src/sources/provider-router/cache.ts +1 -0
- package/src/sources/provider-router/document-routes.ts +45 -0
- package/src/sources/provider-router/index.ts +4 -0
- package/src/sources/sec-edgar/content.ts +9 -3
- package/src/sources/sec-edgar.test.ts +108 -1
- package/src/sources/sec-edgar.ts +105 -1
- package/src/sources/yahoo-finance.ts +4 -0
- package/src/test-support/plugin-runtime.ts +1 -0
- package/src/types/data-provider.ts +11 -0
- package/src/ui/host.tsx +1 -0
- 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
|
-
|
|
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}
|
|
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
|
|
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,
|
|
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
|
-
}, [
|
|
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
|
-
|
|
262
|
-
|
|
263
|
-
|
|
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
|
-
|
|
44
|
+
selectedChoiceId,
|
|
45
|
+
footer,
|
|
38
46
|
bgColor = colors.bg,
|
|
39
47
|
}: ChoiceDialogProps) {
|
|
40
|
-
const [index, setIndex] = useState(() =>
|
|
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={
|
|
322
|
-
onMouseUp={
|
|
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={
|
|
328
|
-
onMouseUp={
|
|
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 ?
|
|
51
|
-
onMouseUp={interactive ?
|
|
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(
|
|
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(
|
|
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(${
|
|
72
|
+
return `minmax(${minWidth}, ${columnFlexWeight(column)}fr)`;
|
|
67
73
|
}
|
|
68
|
-
return `minmax(${
|
|
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
|
|
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 ===
|
|
75
|
-
|
|
76
|
-
}, [activeValue,
|
|
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={
|
|
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={
|
|
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(
|
|
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("
|
|
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
|
>
|