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