gloomberb 0.9.0 → 0.9.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/package.json +6 -2
- package/patches/electrobun@1.18.1.patch +85 -0
- package/src/app/global-shortcuts.test.tsx +12 -0
- package/src/app/global-shortcuts.ts +1 -1
- package/src/assets/gloomberb-logo-windows.ico +0 -0
- package/src/assets/gloomberb-logo-windows.png +0 -0
- package/src/assets/gloomberb-logo.ico +0 -0
- package/src/components/chart/comparison-stock-chart/helpers.ts +0 -32
- package/src/components/chart/comparison-stock-chart/layout-metrics.ts +6 -6
- package/src/components/chart/comparison-stock-chart/legend.tsx +41 -44
- package/src/components/chart/comparison-stock-chart/view.tsx +46 -6
- package/src/components/layout/detached-pane-shell.tsx +11 -4
- package/src/components/layout/header.tsx +25 -10
- package/src/components/layout/titlebar-overlay.ts +16 -0
- package/src/components/layout/window-controls.tsx +99 -0
- package/src/components/price-performance.tsx +175 -0
- package/src/market-data/performance.test.ts +54 -0
- package/src/market-data/performance.ts +109 -0
- package/src/plugins/builtin/comparison-chart/index.test.tsx +88 -9
- package/src/plugins/builtin/congress-trades/detail.tsx +204 -68
- package/src/plugins/builtin/congress-trades/footer.ts +14 -8
- package/src/plugins/builtin/congress-trades/keyboard.ts +4 -23
- package/src/plugins/builtin/congress-trades/model.ts +29 -0
- package/src/plugins/builtin/congress-trades/pane.tsx +8 -13
- package/src/plugins/builtin/congress-trades/table.tsx +10 -1
- package/src/plugins/builtin/ticker-detail/overview/model.ts +0 -14
- package/src/plugins/builtin/ticker-detail/overview-tab.tsx +25 -0
- package/src/renderers/electrobun/bun/application-menu/index.test.ts +12 -0
- package/src/renderers/electrobun/bun/application-menu/index.ts +4 -0
- package/src/renderers/electrobun/bun/desktop/detached-windows.ts +51 -3
- package/src/renderers/electrobun/bun/desktop/host-requests.ts +31 -0
- package/src/renderers/electrobun/bun/desktop/initialization.ts +1 -0
- package/src/renderers/electrobun/bun/desktop/window-controls.test.ts +53 -0
- package/src/renderers/electrobun/bun/desktop/window-controls.ts +73 -0
- package/src/renderers/electrobun/bun/desktop/window-style.ts +31 -0
- package/src/renderers/electrobun/bun/desktop/windows-custom-chrome.ts +58 -0
- package/src/renderers/electrobun/bun/desktop/windows-icons.ts +70 -0
- package/src/renderers/electrobun/bun/desktop/windows-native.ts +75 -0
- package/src/renderers/electrobun/bun/index.ts +36 -7
- package/src/renderers/electrobun/bun/window/frame.test.ts +7 -0
- package/src/renderers/electrobun/bun/window/frame.ts +5 -0
- package/src/renderers/electrobun/shared/protocol.ts +1 -0
- package/src/renderers/electrobun/view/build-assets.ts +2 -1
- package/src/renderers/electrobun/view/host/text.tsx +29 -9
- package/src/renderers/electrobun/view/main.tsx +2 -1
- package/src/renderers/electrobun/view/styles.css +29 -8
- package/src/renderers/electrobun/view/ui-host.tsx +113 -74
- package/src/ui/host.tsx +2 -0
- package/src/updater.test.ts +47 -2
- package/src/updater.ts +67 -10
- package/src/version.ts +1 -1
|
@@ -1,25 +1,35 @@
|
|
|
1
|
-
import
|
|
2
|
-
import { Box, ScrollBox, Text, TextAttributes,
|
|
1
|
+
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
2
|
+
import { Box, ScrollBox, Text, TextAttributes, useRendererHost } from "../../../ui";
|
|
3
|
+
import {
|
|
4
|
+
DataTableView,
|
|
5
|
+
usePaneFooter,
|
|
6
|
+
type DataTableKeyEvent,
|
|
7
|
+
} from "../../../components";
|
|
8
|
+
import { useInlineTickerOpener } from "../../../state/hooks/inline-tickers";
|
|
3
9
|
import { colors } from "../../../theme/colors";
|
|
4
10
|
import { padTo } from "../../../utils/format";
|
|
11
|
+
import { isPlainKey } from "../../../utils/keyboard";
|
|
5
12
|
import type {
|
|
13
|
+
CloudCongressHousePayload,
|
|
6
14
|
CloudCongressMemberPayload,
|
|
7
15
|
CloudCongressTradePayload,
|
|
8
16
|
} from "../../../api-client";
|
|
17
|
+
import { apiClient } from "../../../api-client";
|
|
9
18
|
import {
|
|
19
|
+
CONGRESS_MEMBER_FILING_LIMIT,
|
|
20
|
+
CONGRESS_MEMBER_TRADE_LIMIT,
|
|
21
|
+
CONGRESS_TRADES_PANE_ID,
|
|
22
|
+
buildMemberTradeColumns,
|
|
10
23
|
formatAmountRange,
|
|
11
24
|
formatLag,
|
|
12
|
-
|
|
25
|
+
nextSort,
|
|
26
|
+
sortedTrades,
|
|
13
27
|
truncate,
|
|
28
|
+
type LoadStatus,
|
|
29
|
+
type TradeColumn,
|
|
30
|
+
type TradeColumnId,
|
|
14
31
|
} from "./model";
|
|
15
|
-
|
|
16
|
-
export function sideColor(side: CloudCongressTradePayload["side"], selected: boolean): string {
|
|
17
|
-
if (selected) return colors.selectedText;
|
|
18
|
-
if (side === "BUY") return colors.positive;
|
|
19
|
-
if (side === "SELL") return colors.negative;
|
|
20
|
-
if (side === "EXCHANGE") return colors.warning;
|
|
21
|
-
return colors.textDim;
|
|
22
|
-
}
|
|
32
|
+
import { renderCongressTradeCell } from "./table";
|
|
23
33
|
|
|
24
34
|
function DetailLine({
|
|
25
35
|
label,
|
|
@@ -86,69 +96,195 @@ export function TradeDetail({
|
|
|
86
96
|
);
|
|
87
97
|
}
|
|
88
98
|
|
|
89
|
-
export function
|
|
99
|
+
export function MemberTradesDetail({
|
|
90
100
|
member,
|
|
91
|
-
|
|
101
|
+
initialTrades,
|
|
92
102
|
width,
|
|
93
|
-
|
|
103
|
+
focused,
|
|
104
|
+
filingLimit,
|
|
94
105
|
}: {
|
|
95
106
|
member: CloudCongressMemberPayload;
|
|
96
|
-
|
|
107
|
+
initialTrades: CloudCongressTradePayload[];
|
|
97
108
|
width: number;
|
|
98
|
-
|
|
109
|
+
focused: boolean;
|
|
110
|
+
filingLimit: number;
|
|
99
111
|
}) {
|
|
100
|
-
const
|
|
101
|
-
const
|
|
102
|
-
const
|
|
103
|
-
const
|
|
104
|
-
const
|
|
105
|
-
const
|
|
106
|
-
const
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
112
|
+
const rendererHost = useRendererHost();
|
|
113
|
+
const openTicker = useInlineTickerOpener();
|
|
114
|
+
const [trades, setTrades] = useState<CloudCongressTradePayload[]>(initialTrades);
|
|
115
|
+
const [detailPayload, setDetailPayload] = useState<CloudCongressHousePayload | null>(null);
|
|
116
|
+
const [status, setStatus] = useState<LoadStatus>("loading");
|
|
117
|
+
const [error, setError] = useState<string | null>(null);
|
|
118
|
+
const [selectedTradeId, setSelectedTradeId] = useState<string | null>(initialTrades[0]?.id ?? null);
|
|
119
|
+
const [sortPreference, setSortPreference] = useState<{ columnId: TradeColumnId; direction: "asc" | "desc" }>({
|
|
120
|
+
columnId: "filed",
|
|
121
|
+
direction: "desc",
|
|
122
|
+
});
|
|
123
|
+
const fetchGenRef = useRef(0);
|
|
124
|
+
|
|
125
|
+
const load = useCallback((refresh = false) => {
|
|
126
|
+
fetchGenRef.current += 1;
|
|
127
|
+
const gen = fetchGenRef.current;
|
|
128
|
+
setStatus("loading");
|
|
129
|
+
setError(null);
|
|
130
|
+
apiClient.getCloudCongressHouse({
|
|
131
|
+
member: member.memberName,
|
|
132
|
+
limit: CONGRESS_MEMBER_TRADE_LIMIT,
|
|
133
|
+
filingLimit: Math.max(CONGRESS_MEMBER_FILING_LIMIT, filingLimit),
|
|
134
|
+
refresh,
|
|
135
|
+
})
|
|
136
|
+
.then((payload) => {
|
|
137
|
+
if (fetchGenRef.current !== gen) return;
|
|
138
|
+
const exactMemberTrades = payload.trades.filter((trade) => (
|
|
139
|
+
trade.memberName === member.memberName
|
|
140
|
+
&& trade.stateDistrict === member.stateDistrict
|
|
141
|
+
));
|
|
142
|
+
setDetailPayload(payload);
|
|
143
|
+
setTrades(exactMemberTrades.length > 0 ? exactMemberTrades : payload.trades);
|
|
144
|
+
setStatus("loaded");
|
|
145
|
+
})
|
|
146
|
+
.catch((loadError) => {
|
|
147
|
+
if (fetchGenRef.current !== gen) return;
|
|
148
|
+
setError(loadError instanceof Error ? loadError.message : String(loadError));
|
|
149
|
+
setStatus("error");
|
|
150
|
+
});
|
|
151
|
+
}, [filingLimit, member.memberName, member.stateDistrict]);
|
|
152
|
+
|
|
153
|
+
useEffect(() => {
|
|
154
|
+
fetchGenRef.current += 1;
|
|
155
|
+
setTrades(initialTrades);
|
|
156
|
+
setDetailPayload(null);
|
|
157
|
+
setStatus("loading");
|
|
158
|
+
setError(null);
|
|
159
|
+
setSelectedTradeId(initialTrades[0]?.id ?? null);
|
|
160
|
+
}, [member.id]);
|
|
161
|
+
|
|
162
|
+
useEffect(() => {
|
|
163
|
+
load(false);
|
|
164
|
+
}, [load]);
|
|
165
|
+
|
|
166
|
+
const sortedRows = useMemo(() => sortedTrades(trades, sortPreference), [sortPreference, trades]);
|
|
167
|
+
const columns = useMemo(() => buildMemberTradeColumns(width), [width]);
|
|
168
|
+
const selectedTrade = useMemo(() => (
|
|
169
|
+
sortedRows.find((trade) => trade.id === selectedTradeId) ?? sortedRows[0] ?? null
|
|
170
|
+
), [selectedTradeId, sortedRows]);
|
|
171
|
+
const summaryMember = useMemo(() => (
|
|
172
|
+
detailPayload?.members.find((entry) => entry.id === member.id)
|
|
173
|
+
?? detailPayload?.members.find((entry) => (
|
|
174
|
+
entry.memberName === member.memberName
|
|
175
|
+
&& entry.stateDistrict === member.stateDistrict
|
|
176
|
+
))
|
|
177
|
+
?? member
|
|
178
|
+
), [detailPayload?.members, member]);
|
|
179
|
+
const maybeTruncated = status === "loaded" && trades.length >= CONGRESS_MEMBER_TRADE_LIMIT;
|
|
180
|
+
|
|
181
|
+
useEffect(() => {
|
|
182
|
+
if (selectedTradeId && sortedRows.some((trade) => trade.id === selectedTradeId)) return;
|
|
183
|
+
setSelectedTradeId(sortedRows[0]?.id ?? null);
|
|
184
|
+
}, [selectedTradeId, sortedRows]);
|
|
185
|
+
|
|
186
|
+
const refresh = useCallback(() => {
|
|
187
|
+
load(true);
|
|
188
|
+
}, [load]);
|
|
189
|
+
|
|
190
|
+
const openSelectedTicker = useCallback(() => {
|
|
191
|
+
if (selectedTrade?.ticker) openTicker(selectedTrade.ticker);
|
|
192
|
+
}, [openTicker, selectedTrade?.ticker]);
|
|
193
|
+
|
|
194
|
+
const openSelectedSource = useCallback(() => {
|
|
195
|
+
if (selectedTrade?.sourceUrl) void rendererHost.openExternal(selectedTrade.sourceUrl);
|
|
196
|
+
}, [rendererHost, selectedTrade?.sourceUrl]);
|
|
197
|
+
|
|
198
|
+
const handleKeyDown = useCallback((event: DataTableKeyEvent) => {
|
|
199
|
+
if (isPlainKey(event, "r")) {
|
|
200
|
+
event.preventDefault?.();
|
|
201
|
+
event.stopPropagation?.();
|
|
202
|
+
refresh();
|
|
203
|
+
return true;
|
|
204
|
+
}
|
|
205
|
+
if (isPlainKey(event, "t") && selectedTrade?.ticker) {
|
|
206
|
+
event.preventDefault?.();
|
|
207
|
+
event.stopPropagation?.();
|
|
208
|
+
openSelectedTicker();
|
|
209
|
+
return true;
|
|
210
|
+
}
|
|
211
|
+
if (isPlainKey(event, "o") && selectedTrade?.sourceUrl) {
|
|
212
|
+
event.preventDefault?.();
|
|
213
|
+
event.stopPropagation?.();
|
|
214
|
+
openSelectedSource();
|
|
215
|
+
return true;
|
|
216
|
+
}
|
|
217
|
+
return false;
|
|
218
|
+
}, [openSelectedSource, openSelectedTicker, refresh, selectedTrade?.sourceUrl, selectedTrade?.ticker]);
|
|
219
|
+
|
|
220
|
+
usePaneFooter(`${CONGRESS_TRADES_PANE_ID}:member-detail`, () => ({
|
|
221
|
+
info: [
|
|
222
|
+
...(status === "loading" ? [{ id: "member-loading", parts: [{ text: "loading member trades", tone: "muted" as const }] }] : []),
|
|
223
|
+
...(error ? [{ id: "member-error", parts: [{ text: error, tone: "warning" as const }] }] : []),
|
|
224
|
+
...(maybeTruncated ? [{ id: "member-truncated", parts: [{ text: `limited to ${CONGRESS_MEMBER_TRADE_LIMIT} trades`, tone: "warning" as const }] }] : []),
|
|
225
|
+
],
|
|
226
|
+
hints: [
|
|
227
|
+
{ id: "member-refresh", key: "r", label: "efresh", onPress: refresh },
|
|
228
|
+
{ id: "member-ticker", key: "t", label: "icker", onPress: openSelectedTicker, disabled: !selectedTrade?.ticker },
|
|
229
|
+
{ id: "member-open", key: "o", label: "pen", onPress: openSelectedSource, disabled: !selectedTrade?.sourceUrl },
|
|
230
|
+
],
|
|
231
|
+
}), [
|
|
232
|
+
error,
|
|
233
|
+
maybeTruncated,
|
|
234
|
+
openSelectedSource,
|
|
235
|
+
openSelectedTicker,
|
|
236
|
+
refresh,
|
|
237
|
+
selectedTrade?.sourceUrl,
|
|
238
|
+
selectedTrade?.ticker,
|
|
239
|
+
status,
|
|
240
|
+
]);
|
|
241
|
+
|
|
242
|
+
const summary = (
|
|
243
|
+
<Box flexDirection="column" paddingX={1} paddingTop={1} paddingBottom={1}>
|
|
244
|
+
<Box height={1} flexDirection="row">
|
|
245
|
+
<Text fg={colors.textDim}>
|
|
246
|
+
{`${summaryMember.stateDistrict || "--"} ${summaryMember.tradeCount} trades ${summaryMember.buyCount} buys ${summaryMember.sellCount} sells ${formatAmountRange(summaryMember.estimatedLow, summaryMember.estimatedHigh)}`}
|
|
247
|
+
</Text>
|
|
151
248
|
</Box>
|
|
152
|
-
</
|
|
249
|
+
</Box>
|
|
250
|
+
);
|
|
251
|
+
const emptyTitle = status === "loading"
|
|
252
|
+
? "Loading member trades..."
|
|
253
|
+
: error ?? "No trades for this member.";
|
|
254
|
+
|
|
255
|
+
return (
|
|
256
|
+
<Box flexDirection="column" flexGrow={1} overflow="hidden">
|
|
257
|
+
<DataTableView<CloudCongressTradePayload, TradeColumn>
|
|
258
|
+
focused={focused}
|
|
259
|
+
selection={{
|
|
260
|
+
kind: "id",
|
|
261
|
+
selectedId: selectedTradeId,
|
|
262
|
+
getId: (trade) => trade.id,
|
|
263
|
+
onChange: (id) => setSelectedTradeId(id),
|
|
264
|
+
}}
|
|
265
|
+
onActivate={(trade) => {
|
|
266
|
+
if (trade.ticker) openTicker(trade.ticker);
|
|
267
|
+
}}
|
|
268
|
+
onRootKeyDown={handleKeyDown}
|
|
269
|
+
rootWidth={width}
|
|
270
|
+
rootBefore={summary}
|
|
271
|
+
resetScrollKey={member.id}
|
|
272
|
+
columns={columns}
|
|
273
|
+
items={sortedRows}
|
|
274
|
+
sortColumnId={sortPreference.columnId}
|
|
275
|
+
sortDirection={sortPreference.direction}
|
|
276
|
+
onHeaderClick={(columnId) => {
|
|
277
|
+
setSortPreference((current) => nextSort(
|
|
278
|
+
current,
|
|
279
|
+
columnId as TradeColumnId,
|
|
280
|
+
columnId === "ticker" || columnId === "asset" || columnId === "side" || columnId === "owner" ? "asc" : "desc",
|
|
281
|
+
));
|
|
282
|
+
}}
|
|
283
|
+
getItemKey={(trade) => trade.id}
|
|
284
|
+
renderCell={renderCongressTradeCell}
|
|
285
|
+
emptyStateTitle={emptyTitle}
|
|
286
|
+
showHorizontalScrollbar={false}
|
|
287
|
+
/>
|
|
288
|
+
</Box>
|
|
153
289
|
);
|
|
154
290
|
}
|
|
@@ -7,11 +7,13 @@ import type {
|
|
|
7
7
|
import {
|
|
8
8
|
CONGRESS_TRADES_PANE_ID,
|
|
9
9
|
type CongressTab,
|
|
10
|
+
type DetailMode,
|
|
10
11
|
type LoadStatus,
|
|
11
12
|
} from "./model";
|
|
12
13
|
|
|
13
14
|
export function useCongressTradesFooter({
|
|
14
15
|
activeTab,
|
|
16
|
+
detailMode,
|
|
15
17
|
detailTrade,
|
|
16
18
|
error,
|
|
17
19
|
load,
|
|
@@ -23,6 +25,7 @@ export function useCongressTradesFooter({
|
|
|
23
25
|
status,
|
|
24
26
|
}: {
|
|
25
27
|
activeTab: CongressTab;
|
|
28
|
+
detailMode: DetailMode;
|
|
26
29
|
detailTrade: CloudCongressTradePayload | null;
|
|
27
30
|
error: string | null;
|
|
28
31
|
load: (refresh?: boolean) => void;
|
|
@@ -44,16 +47,19 @@ export function useCongressTradesFooter({
|
|
|
44
47
|
...(status === "loading" ? [{ id: "loading", parts: [{ text: "loading", tone: "muted" as const }] }] : []),
|
|
45
48
|
...(error ? [{ id: "error", parts: [{ text: error, tone: "warning" as const }] }] : []),
|
|
46
49
|
],
|
|
47
|
-
hints:
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
50
|
+
hints: detailMode?.kind === "member"
|
|
51
|
+
? []
|
|
52
|
+
: [
|
|
53
|
+
{ id: "refresh", key: "r", label: "efresh", onPress: () => load(true) },
|
|
54
|
+
...(activeTab === "trades" ? [
|
|
55
|
+
{ id: "member", key: "m", label: "ember", onPress: openSelectedTradeMember, disabled: !selectedTrade },
|
|
56
|
+
{ id: "ticker", key: "t", label: "icker", onPress: openSelectedTicker, disabled: !(detailTrade?.ticker ?? selectedTrade?.ticker) },
|
|
57
|
+
{ id: "open", key: "o", label: "pen", onPress: openSelectedTradeSource, disabled: !(detailTrade ?? selectedTrade)?.sourceUrl },
|
|
58
|
+
] : []),
|
|
59
|
+
],
|
|
55
60
|
}), [
|
|
56
61
|
activeTab,
|
|
62
|
+
detailMode,
|
|
57
63
|
detailTrade,
|
|
58
64
|
error,
|
|
59
65
|
load,
|
|
@@ -1,14 +1,12 @@
|
|
|
1
|
-
import { useCallback
|
|
1
|
+
import { useCallback } from "react";
|
|
2
2
|
import { type DataTableKeyEvent } from "../../../components";
|
|
3
3
|
import { useShortcut } from "../../../react/input";
|
|
4
|
-
import { type ScrollBoxRenderable } from "../../../ui";
|
|
5
4
|
import { isPlainKey } from "../../../utils/keyboard";
|
|
6
5
|
import type { CongressTab, DetailMode } from "./model";
|
|
7
6
|
|
|
8
7
|
export function useCongressTradesKeyboard({
|
|
9
8
|
activeTab,
|
|
10
9
|
detailMode,
|
|
11
|
-
detailScrollRef,
|
|
12
10
|
focused,
|
|
13
11
|
load,
|
|
14
12
|
openSelectedTicker,
|
|
@@ -18,7 +16,6 @@ export function useCongressTradesKeyboard({
|
|
|
18
16
|
}: {
|
|
19
17
|
activeTab: CongressTab;
|
|
20
18
|
detailMode: DetailMode;
|
|
21
|
-
detailScrollRef: RefObject<ScrollBoxRenderable | null>;
|
|
22
19
|
focused: boolean;
|
|
23
20
|
load: (refresh?: boolean) => void;
|
|
24
21
|
openSelectedTicker: () => void;
|
|
@@ -26,25 +23,9 @@ export function useCongressTradesKeyboard({
|
|
|
26
23
|
openSelectedTradeSource: () => void;
|
|
27
24
|
selectTab: (tab: string) => void;
|
|
28
25
|
}) {
|
|
29
|
-
const scrollDetailBy = useCallback((delta: number) => {
|
|
30
|
-
const scrollBox = detailScrollRef.current;
|
|
31
|
-
if (!scrollBox?.viewport) return;
|
|
32
|
-
const maxScrollTop = Math.max(0, scrollBox.scrollHeight - scrollBox.viewport.height);
|
|
33
|
-
scrollBox.scrollTop = Math.max(0, Math.min(maxScrollTop, scrollBox.scrollTop + delta));
|
|
34
|
-
}, [detailScrollRef]);
|
|
35
|
-
|
|
36
26
|
const handleDetailKeyDown = useCallback((event: DataTableKeyEvent) => {
|
|
37
|
-
if (
|
|
38
|
-
|
|
39
|
-
event.stopPropagation?.();
|
|
40
|
-
scrollDetailBy(1);
|
|
41
|
-
return true;
|
|
42
|
-
}
|
|
43
|
-
if (isPlainKey(event, "k", "up")) {
|
|
44
|
-
event.preventDefault?.();
|
|
45
|
-
event.stopPropagation?.();
|
|
46
|
-
scrollDetailBy(-1);
|
|
47
|
-
return true;
|
|
27
|
+
if (detailMode?.kind === "member") {
|
|
28
|
+
return false;
|
|
48
29
|
}
|
|
49
30
|
if (isPlainKey(event, "o")) {
|
|
50
31
|
event.preventDefault?.();
|
|
@@ -65,7 +46,7 @@ export function useCongressTradesKeyboard({
|
|
|
65
46
|
return true;
|
|
66
47
|
}
|
|
67
48
|
return false;
|
|
68
|
-
}, [openSelectedTicker, openSelectedTradeMember, openSelectedTradeSource
|
|
49
|
+
}, [detailMode?.kind, openSelectedTicker, openSelectedTradeMember, openSelectedTradeSource]);
|
|
69
50
|
|
|
70
51
|
const handleRootKeyDown = useCallback((event: DataTableKeyEvent) => {
|
|
71
52
|
if (isPlainKey(event, "r")) {
|
|
@@ -8,6 +8,8 @@ import type {
|
|
|
8
8
|
export const CONGRESS_TRADES_PANE_ID = "congress-trades";
|
|
9
9
|
export const CONGRESS_TRADE_LIMIT = 200;
|
|
10
10
|
export const CONGRESS_FILING_LIMIT = 20;
|
|
11
|
+
export const CONGRESS_MEMBER_TRADE_LIMIT = 2000;
|
|
12
|
+
export const CONGRESS_MEMBER_FILING_LIMIT = 500;
|
|
11
13
|
|
|
12
14
|
export type CongressTab = "trades" | "members";
|
|
13
15
|
export type LoadStatus = "idle" | "loading" | "loaded" | "error";
|
|
@@ -25,6 +27,7 @@ export type TradeColumnId =
|
|
|
25
27
|
| "side"
|
|
26
28
|
| "ticker"
|
|
27
29
|
| "amount"
|
|
30
|
+
| "asset"
|
|
28
31
|
| "owner";
|
|
29
32
|
export type TradeColumn = DataTableColumn & { id: TradeColumnId };
|
|
30
33
|
export type MemberColumnId =
|
|
@@ -104,6 +107,8 @@ function compareTrade(
|
|
|
104
107
|
return compareText(left.ticker ?? "", right.ticker ?? "");
|
|
105
108
|
case "amount":
|
|
106
109
|
return (left.amountHigh ?? left.amountLow ?? 0) - (right.amountHigh ?? right.amountLow ?? 0);
|
|
110
|
+
case "asset":
|
|
111
|
+
return compareText(left.assetName, right.assetName);
|
|
107
112
|
case "owner":
|
|
108
113
|
return compareText(left.owner, right.owner);
|
|
109
114
|
}
|
|
@@ -172,6 +177,30 @@ export function buildTradeColumns(width: number): TradeColumn[] {
|
|
|
172
177
|
];
|
|
173
178
|
}
|
|
174
179
|
|
|
180
|
+
export function buildMemberTradeColumns(width: number): TradeColumn[] {
|
|
181
|
+
const filedWidth = 7;
|
|
182
|
+
const txWidth = 7;
|
|
183
|
+
const sideWidth = 8;
|
|
184
|
+
const tickerWidth = 12;
|
|
185
|
+
const amountWidth = 14;
|
|
186
|
+
const ownerWidth = 8;
|
|
187
|
+
const lagWidth = 5;
|
|
188
|
+
const assetWidth = Math.max(
|
|
189
|
+
18,
|
|
190
|
+
width - filedWidth - txWidth - sideWidth - tickerWidth - amountWidth - ownerWidth - lagWidth - 9,
|
|
191
|
+
);
|
|
192
|
+
return [
|
|
193
|
+
{ id: "filed", label: "FILED", width: filedWidth, align: "left" },
|
|
194
|
+
{ id: "tx", label: "TX", width: txWidth, align: "left" },
|
|
195
|
+
{ id: "side", label: "SIDE", width: sideWidth, align: "left" },
|
|
196
|
+
{ id: "ticker", label: "TICKER", width: tickerWidth, align: "left" },
|
|
197
|
+
{ id: "amount", label: "AMOUNT", width: amountWidth, align: "right" },
|
|
198
|
+
{ id: "asset", label: "ASSET", width: assetWidth, align: "left" },
|
|
199
|
+
{ id: "owner", label: "OWNER", width: ownerWidth, align: "left" },
|
|
200
|
+
{ id: "lag", label: "LAG", width: lagWidth, align: "right" },
|
|
201
|
+
];
|
|
202
|
+
}
|
|
203
|
+
|
|
175
204
|
export function buildMemberColumns(width: number): MemberColumn[] {
|
|
176
205
|
const districtWidth = 6;
|
|
177
206
|
const tradesWidth = 7;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
2
|
-
import { Box, useRendererHost
|
|
2
|
+
import { Box, useRendererHost } from "../../../ui";
|
|
3
3
|
import {
|
|
4
4
|
DataTableStackView,
|
|
5
5
|
EmptyState,
|
|
@@ -17,6 +17,7 @@ import {
|
|
|
17
17
|
import type { PaneProps } from "../../../types/plugin";
|
|
18
18
|
import {
|
|
19
19
|
CONGRESS_FILING_LIMIT,
|
|
20
|
+
CONGRESS_MEMBER_FILING_LIMIT,
|
|
20
21
|
CONGRESS_TRADE_LIMIT,
|
|
21
22
|
CONGRESS_TRADES_PANE_ID,
|
|
22
23
|
buildMemberColumns,
|
|
@@ -34,7 +35,7 @@ import {
|
|
|
34
35
|
type TradeColumn,
|
|
35
36
|
type TradeColumnId,
|
|
36
37
|
} from "./model";
|
|
37
|
-
import {
|
|
38
|
+
import { MemberTradesDetail, TradeDetail } from "./detail";
|
|
38
39
|
import { useCongressTradesFooter } from "./footer";
|
|
39
40
|
import { useCongressTradesKeyboard } from "./keyboard";
|
|
40
41
|
import {
|
|
@@ -61,7 +62,6 @@ export function CongressTradesPane({ focused, width, height }: PaneProps) {
|
|
|
61
62
|
columnId: "trades",
|
|
62
63
|
direction: "desc",
|
|
63
64
|
});
|
|
64
|
-
const detailScrollRef = useRef<ScrollBoxRenderable | null>(null);
|
|
65
65
|
const fetchGenRef = useRef(0);
|
|
66
66
|
|
|
67
67
|
const load = useCallback((refresh = false) => {
|
|
@@ -142,12 +142,6 @@ export function CongressTradesPane({ focused, width, height }: PaneProps) {
|
|
|
142
142
|
if (detailMode?.kind === "member" && !detailMember) setDetailMode(null);
|
|
143
143
|
}, [detailMember, detailMode, detailTrade]);
|
|
144
144
|
|
|
145
|
-
useEffect(() => {
|
|
146
|
-
if (!detailMode) return;
|
|
147
|
-
const scrollBox = detailScrollRef.current;
|
|
148
|
-
if (scrollBox) scrollBox.scrollTop = 0;
|
|
149
|
-
}, [detailMode]);
|
|
150
|
-
|
|
151
145
|
const selectTab = useCallback((tab: string) => {
|
|
152
146
|
setActiveTab(tab === "members" ? "members" : "trades");
|
|
153
147
|
setDetailMode(null);
|
|
@@ -176,7 +170,6 @@ export function CongressTradesPane({ focused, width, height }: PaneProps) {
|
|
|
176
170
|
const { handleDetailKeyDown, handleRootKeyDown } = useCongressTradesKeyboard({
|
|
177
171
|
activeTab,
|
|
178
172
|
detailMode,
|
|
179
|
-
detailScrollRef,
|
|
180
173
|
focused,
|
|
181
174
|
load,
|
|
182
175
|
openSelectedTicker,
|
|
@@ -187,6 +180,7 @@ export function CongressTradesPane({ focused, width, height }: PaneProps) {
|
|
|
187
180
|
|
|
188
181
|
useCongressTradesFooter({
|
|
189
182
|
activeTab,
|
|
183
|
+
detailMode,
|
|
190
184
|
detailTrade,
|
|
191
185
|
error,
|
|
192
186
|
load,
|
|
@@ -201,11 +195,12 @@ export function CongressTradesPane({ focused, width, height }: PaneProps) {
|
|
|
201
195
|
const detailContent = detailTrade ? (
|
|
202
196
|
<TradeDetail trade={detailTrade} width={width} />
|
|
203
197
|
) : detailMember ? (
|
|
204
|
-
<
|
|
198
|
+
<MemberTradesDetail
|
|
199
|
+
focused={focused}
|
|
205
200
|
member={detailMember}
|
|
206
|
-
|
|
201
|
+
initialTrades={detailMemberTrades}
|
|
207
202
|
width={width}
|
|
208
|
-
|
|
203
|
+
filingLimit={payload?.filingCount ?? CONGRESS_MEMBER_FILING_LIMIT}
|
|
209
204
|
/>
|
|
210
205
|
) : null;
|
|
211
206
|
const detailTitle = detailTrade
|
|
@@ -12,7 +12,14 @@ import {
|
|
|
12
12
|
type MemberColumn,
|
|
13
13
|
type TradeColumn,
|
|
14
14
|
} from "./model";
|
|
15
|
-
|
|
15
|
+
|
|
16
|
+
export function sideColor(side: CloudCongressTradePayload["side"], selected: boolean): string {
|
|
17
|
+
if (selected) return colors.selectedText;
|
|
18
|
+
if (side === "BUY") return colors.positive;
|
|
19
|
+
if (side === "SELL") return colors.negative;
|
|
20
|
+
if (side === "EXCHANGE") return colors.warning;
|
|
21
|
+
return colors.textDim;
|
|
22
|
+
}
|
|
16
23
|
|
|
17
24
|
export function renderCongressTradeCell(
|
|
18
25
|
trade: CloudCongressTradePayload,
|
|
@@ -51,6 +58,8 @@ export function renderCongressTradeCell(
|
|
|
51
58
|
text: formatAmountRange(trade.amountLow, trade.amountHigh, trade.amount),
|
|
52
59
|
color: selectedColor ?? colors.textBright,
|
|
53
60
|
};
|
|
61
|
+
case "asset":
|
|
62
|
+
return { text: trade.assetName, color: selectedColor ?? colors.text };
|
|
54
63
|
case "owner":
|
|
55
64
|
return { text: trade.owner, color: selectedColor ?? colors.textDim };
|
|
56
65
|
}
|
|
@@ -90,20 +90,6 @@ export function buildOverviewStats({
|
|
|
90
90
|
valueColor: priceColor(fundamentals.revenueGrowth),
|
|
91
91
|
});
|
|
92
92
|
}
|
|
93
|
-
if (fundamentals?.return1Y != null) {
|
|
94
|
-
stats.push({
|
|
95
|
-
label: "1Y Return",
|
|
96
|
-
value: formatPercent(fundamentals.return1Y),
|
|
97
|
-
valueColor: priceColor(fundamentals.return1Y),
|
|
98
|
-
});
|
|
99
|
-
}
|
|
100
|
-
if (fundamentals?.return3Y != null) {
|
|
101
|
-
stats.push({
|
|
102
|
-
label: "3Y Return",
|
|
103
|
-
value: formatPercent(fundamentals.return3Y),
|
|
104
|
-
valueColor: priceColor(fundamentals.return3Y),
|
|
105
|
-
});
|
|
106
|
-
}
|
|
107
93
|
if (fundamentals?.enterpriseValue) {
|
|
108
94
|
stats.push({ label: "EV", value: formatCompact(fundamentals.enterpriseValue) });
|
|
109
95
|
}
|
|
@@ -15,6 +15,11 @@ import {
|
|
|
15
15
|
import { selectEffectiveExchangeRates } from "../../../utils/exchange-rate-map";
|
|
16
16
|
import { EmptyState } from "../../../components";
|
|
17
17
|
import { ResolvedStockChart } from "../../../components/chart/stock-chart";
|
|
18
|
+
import { PriceReturnStrip } from "../../../components/price-performance";
|
|
19
|
+
import {
|
|
20
|
+
appendQuoteToPriceReturnHistory,
|
|
21
|
+
buildPriceReturnFields,
|
|
22
|
+
} from "../../../market-data/performance";
|
|
18
23
|
import type { TickerFinancials } from "../../../types/financials";
|
|
19
24
|
import type { TickerRecord } from "../../../types/ticker";
|
|
20
25
|
import {
|
|
@@ -96,6 +101,19 @@ export function OverviewTab({
|
|
|
96
101
|
baseCurrency,
|
|
97
102
|
toBase,
|
|
98
103
|
});
|
|
104
|
+
const performanceFields = buildPriceReturnFields(
|
|
105
|
+
appendQuoteToPriceReturnHistory(financials?.priceHistory ?? [], quote),
|
|
106
|
+
).map((field) => {
|
|
107
|
+
if (field.value != null) return field;
|
|
108
|
+
if (field.id === "1Y" && fundamentals?.return1Y != null) {
|
|
109
|
+
return { ...field, value: fundamentals.return1Y };
|
|
110
|
+
}
|
|
111
|
+
if (field.id === "3Y" && fundamentals?.return3Y != null) {
|
|
112
|
+
return { ...field, value: fundamentals.return3Y };
|
|
113
|
+
}
|
|
114
|
+
return field;
|
|
115
|
+
});
|
|
116
|
+
const hasPerformance = performanceFields.some((field) => field.value != null);
|
|
99
117
|
const positionRows = buildPositionRows({
|
|
100
118
|
ticker,
|
|
101
119
|
quote,
|
|
@@ -210,6 +228,13 @@ export function OverviewTab({
|
|
|
210
228
|
/>
|
|
211
229
|
)}
|
|
212
230
|
|
|
231
|
+
{hasPerformance && (
|
|
232
|
+
<Box flexDirection="column">
|
|
233
|
+
<SectionHeader title="Price Return" />
|
|
234
|
+
<PriceReturnStrip fields={performanceFields} width={contentWidth} />
|
|
235
|
+
</Box>
|
|
236
|
+
)}
|
|
237
|
+
|
|
213
238
|
{stats.length > 0 && (
|
|
214
239
|
<Box flexDirection="column">
|
|
215
240
|
<SectionHeader title="Fundamentals" />
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import { buildApplicationMenu, buildDesktopApplicationMenu } from "./index";
|
|
3
|
+
|
|
4
|
+
describe("desktop application menu", () => {
|
|
5
|
+
test("hides the application menu on Windows to preserve vertical space", () => {
|
|
6
|
+
expect(buildDesktopApplicationMenu("win32")).toEqual([]);
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
test("keeps the native application menu on macOS", () => {
|
|
10
|
+
expect(buildDesktopApplicationMenu("darwin")).toEqual(buildApplicationMenu());
|
|
11
|
+
});
|
|
12
|
+
});
|
|
@@ -121,3 +121,7 @@ export function buildApplicationMenu(): ApplicationMenuItemConfig[] {
|
|
|
121
121
|
},
|
|
122
122
|
];
|
|
123
123
|
}
|
|
124
|
+
|
|
125
|
+
export function buildDesktopApplicationMenu(platform = process.platform): ApplicationMenuItemConfig[] {
|
|
126
|
+
return platform === "win32" ? [] : buildApplicationMenu();
|
|
127
|
+
}
|