gloomberb 0.9.3 → 0.9.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (137) hide show
  1. package/README.md +1 -0
  2. package/package.json +1 -1
  3. package/src/app/runtime/desktop-menu.ts +5 -1
  4. package/src/app/runtime/ticker-refresh.ts +4 -1
  5. package/src/app.tsx +10 -1
  6. package/src/brokers/account-cache.ts +1 -1
  7. package/src/brokers/sync-broker-instance.test.ts +107 -0
  8. package/src/brokers/sync-broker-instance.ts +22 -10
  9. package/src/cli/desktop-pane-shot.ts +2 -1
  10. package/src/cli/pane-functions/screenshot.ts +13 -1
  11. package/src/components/chart/bar-chart-renderer.test.ts +108 -3
  12. package/src/components/chart/bar-chart-renderer.ts +226 -27
  13. package/src/components/chart/core/pointer.ts +32 -6
  14. package/src/components/chart/price-axis-labels.tsx +3 -1
  15. package/src/components/chart/static/bar-chart-surface.test.tsx +97 -0
  16. package/src/components/chart/static/bar-chart-surface.tsx +151 -28
  17. package/src/components/chart/static/chart/axis-overlays.tsx +243 -0
  18. package/src/components/chart/static/chart/surface.test.tsx +92 -0
  19. package/src/components/chart/static/chart/surface.tsx +167 -24
  20. package/src/components/chart/stock-chart/index.test.ts +25 -0
  21. package/src/components/command-bar/layout-items/current-layout.ts +5 -1
  22. package/src/components/layout/detached-pane-shell.tsx +6 -3
  23. package/src/components/layout/floating-pane.tsx +3 -2
  24. package/src/components/layout/pane/content.tsx +20 -8
  25. package/src/components/layout/pane/footer/registration.tsx +6 -2
  26. package/src/components/layout/pane/frame.tsx +4 -4
  27. package/src/components/layout/pane/header.tsx +40 -4
  28. package/src/components/layout/pane/index.tsx +3 -1
  29. package/src/components/layout/pane/sizing.test.ts +43 -0
  30. package/src/components/layout/pane/sizing.ts +13 -4
  31. package/src/components/layout/shell/cursor-occlusion.ts +196 -0
  32. package/src/components/layout/shell/drag/runtime.ts +9 -2
  33. package/src/components/layout/shell/index.test.tsx +92 -2
  34. package/src/components/layout/shell/index.tsx +37 -2
  35. package/src/components/layout/shell/layout-state.test.ts +33 -0
  36. package/src/components/layout/shell/layout-state.ts +4 -9
  37. package/src/components/layout/shell/pane/layers.tsx +8 -2
  38. package/src/components/layout/shell/visible-layout.ts +17 -0
  39. package/src/components/layout/status-bar.test.tsx +6 -1
  40. package/src/components/layout/status-bar.tsx +5 -1
  41. package/src/components/onboarding/onboarding-steps.tsx +8 -28
  42. package/src/components/onboarding/onboarding-wizard.test.tsx +13 -12
  43. package/src/components/onboarding/onboarding-wizard.tsx +2 -4
  44. package/src/components/onboarding/portfolio-step/index.tsx +0 -12
  45. package/src/components/onboarding/portfolio-step/types.ts +1 -3
  46. package/src/components/onboarding/wizard-keyboard.ts +2 -5
  47. package/src/components/table-view-shared.tsx +12 -4
  48. package/src/components/ui/data-table/opentui/index.tsx +25 -12
  49. package/src/components/ui/data-table/types.ts +4 -0
  50. package/src/components/ui/fields.tsx +10 -3
  51. package/src/components/ui/page-stack-view.tsx +6 -2
  52. package/src/components/ui/table-layout.test.ts +25 -0
  53. package/src/components/ui/table-layout.ts +22 -6
  54. package/src/components/ui/tabs.tsx +26 -10
  55. package/src/components/ui/ui.test.tsx +87 -0
  56. package/src/core/state/app/layout-reducer.ts +2 -2
  57. package/src/plugins/builtin/analytics/index.test.tsx +6 -2
  58. package/src/plugins/builtin/analytics/pane-model.ts +43 -8
  59. package/src/plugins/builtin/kelly-sizer/constants.ts +3 -0
  60. package/src/plugins/builtin/kelly-sizer/fields.ts +214 -0
  61. package/src/plugins/builtin/kelly-sizer/index.test.tsx +197 -0
  62. package/src/plugins/builtin/kelly-sizer/index.tsx +47 -0
  63. package/src/plugins/builtin/kelly-sizer/model.test.ts +152 -0
  64. package/src/plugins/builtin/kelly-sizer/model.ts +580 -0
  65. package/src/plugins/builtin/kelly-sizer/pane.tsx +558 -0
  66. package/src/plugins/builtin/kelly-sizer/portfolio.ts +71 -0
  67. package/src/plugins/builtin/kelly-sizer/sections.tsx +142 -0
  68. package/src/plugins/builtin/kelly-sizer/state.ts +67 -0
  69. package/src/plugins/builtin/kelly-sizer/test-support.tsx +174 -0
  70. package/src/plugins/builtin/kelly-sizer/types.ts +156 -0
  71. package/src/plugins/builtin/kelly-sizer/view.test.tsx +77 -0
  72. package/src/plugins/builtin/kelly-sizer/view.tsx +375 -0
  73. package/src/plugins/builtin/layout-manager/index.tsx +5 -1
  74. package/src/plugins/builtin/options/view.test.tsx +141 -4
  75. package/src/plugins/builtin/options/view.tsx +76 -15
  76. package/src/plugins/builtin/portfolio-list/account-metrics.test.ts +26 -1
  77. package/src/plugins/builtin/portfolio-list/account-metrics.ts +14 -0
  78. package/src/plugins/builtin/portfolio-list/index.test.ts +23 -0
  79. package/src/plugins/builtin/portfolio-list/pane/index.tsx +2 -0
  80. package/src/plugins/builtin/portfolio-list/summary/index.test.ts +59 -1
  81. package/src/plugins/builtin/portfolio-list/summary/index.tsx +36 -5
  82. package/src/plugins/builtin/ticker-detail/data-panes/fundamental-graph/content.tsx +149 -48
  83. package/src/plugins/builtin/ticker-detail/data-panes/fundamental-graph/model.ts +25 -2
  84. package/src/plugins/builtin/ticker-detail/data-panes/fundamental-graph.tsx +5 -4
  85. package/src/plugins/builtin/ticker-detail/data-panes.test.ts +16 -0
  86. package/src/plugins/builtin/ticker-detail/index.test.tsx +143 -0
  87. package/src/plugins/catalog-ui.ts +2 -0
  88. package/src/plugins/ibkr/broker-adapter.ts +19 -0
  89. package/src/plugins/ibkr/config.test.ts +31 -2
  90. package/src/plugins/ibkr/config.ts +9 -2
  91. package/src/plugins/ibkr/flex/index.test.ts +18 -9
  92. package/src/plugins/ibkr/flex/index.ts +30 -0
  93. package/src/plugins/ibkr/flex/statement-client.ts +68 -14
  94. package/src/plugins/ibkr/gateway/account-loaders.ts +1 -1
  95. package/src/plugins/ibkr/gateway/account-summary.ts +1 -0
  96. package/src/plugins/ibkr/gateway/service/index.test.ts +8 -0
  97. package/src/plugins/pane-manager/gridlock.ts +13 -5
  98. package/src/plugins/pane-manager/layout-state.ts +25 -0
  99. package/src/plugins/pane-manager.test.ts +33 -0
  100. package/src/plugins/pane-manager.ts +6 -1
  101. package/src/plugins/prediction-markets/chart.tsx +10 -4
  102. package/src/plugins/prediction-markets/detail/book.tsx +141 -96
  103. package/src/plugins/prediction-markets/detail/overview.tsx +3 -0
  104. package/src/plugins/prediction-markets/detail/pane.tsx +71 -64
  105. package/src/plugins/prediction-markets/detail/shared.tsx +19 -0
  106. package/src/plugins/prediction-markets/detail/trades.tsx +64 -19
  107. package/src/plugins/prediction-markets/detail.test.tsx +50 -0
  108. package/src/plugins/prediction-markets/pane.test.tsx +43 -0
  109. package/src/plugins/prediction-markets/pane.tsx +50 -22
  110. package/src/plugins/prediction-markets/test-helpers.tsx +41 -23
  111. package/src/renderers/electrobun/view/cli-pane-shot-entry.tsx +13 -2
  112. package/src/renderers/electrobun/view/data-table/index.tsx +19 -4
  113. package/src/renderers/electrobun/view/data-table/row.tsx +30 -6
  114. package/src/renderers/electrobun/view/desktop/controls.tsx +11 -5
  115. package/src/renderers/electrobun/view/host/input.tsx +115 -18
  116. package/src/renderers/electrobun/view/host/tabs.tsx +7 -2
  117. package/src/renderers/electrobun/view/styles.css +11 -0
  118. package/src/renderers/opentui/host.tsx +17 -1
  119. package/src/renderers/opentui/ui-host.tsx +57 -25
  120. package/src/sources/provider-router/batches.ts +16 -2
  121. package/src/sources/provider-router/financial-routes.ts +5 -0
  122. package/src/sources/provider-router/financials.ts +11 -23
  123. package/src/sources/provider-router/index.test.ts +189 -0
  124. package/src/sources/provider-router/primary.ts +3 -2
  125. package/src/sources/sec-edgar.test.ts +83 -0
  126. package/src/sources/sec-edgar.ts +231 -0
  127. package/src/sources/yahoo-finance.test.ts +51 -0
  128. package/src/sources/yahoo-finance.ts +49 -1
  129. package/src/state/app/context/index.test.tsx +5 -8
  130. package/src/types/broker.ts +1 -0
  131. package/src/types/config.ts +10 -0
  132. package/src/types/trading.ts +2 -0
  133. package/src/ui/host.tsx +16 -0
  134. package/src/ui/index.tsx +2 -0
  135. package/src/utils/financial-statements.ts +23 -0
  136. package/src/version.ts +1 -1
  137. package/src/components/onboarding/portfolio-step/manual-name-panel.tsx +0 -62
@@ -1,7 +1,7 @@
1
- import { Box, Text } from "../../../ui";
2
- import { TextAttributes } from "../../../ui";
1
+ import { useEffect, useMemo, useState } from "react";
2
+ import { DataTableView, type DataTableColumn } from "../../../components";
3
3
  import { colors } from "../../../theme/colors";
4
- import { formatNumber, padTo } from "../../../utils/format";
4
+ import { formatNumber } from "../../../utils/format";
5
5
  import { formatPredictionProbability } from "../metrics";
6
6
  import type {
7
7
  PredictionBookLevel,
@@ -9,111 +9,156 @@ import type {
9
9
  PredictionOrderPreviewIntent,
10
10
  } from "../types";
11
11
 
12
- function BookTable({
13
- title,
12
+ type BookColumnId = "outcome" | "side" | "price" | "size";
13
+ type BookColumn = DataTableColumn & { id: BookColumnId };
14
+
15
+ interface BookRow {
16
+ id: string;
17
+ outcome: "yes" | "no";
18
+ side: "buy" | "sell";
19
+ level: PredictionBookLevel;
20
+ intent: PredictionOrderPreviewIntent;
21
+ }
22
+
23
+ const BOOK_COLUMNS: BookColumn[] = [
24
+ { id: "outcome", label: "OUT", width: 5 },
25
+ { id: "side", label: "SIDE", width: 6 },
26
+ { id: "price", label: "PRICE", width: 8, align: "right" },
27
+ { id: "size", label: "SIZE", width: 10, align: "right" },
28
+ ];
29
+
30
+ function bookRowsForLevels({
14
31
  levels,
15
- onSelect,
32
+ marketKey,
33
+ outcome,
34
+ side,
16
35
  }: {
17
- title: string;
18
36
  levels: PredictionBookLevel[];
19
- onSelect: (level: PredictionBookLevel) => void;
20
- }) {
21
- return (
22
- <Box flexDirection="column" flexGrow={1}>
23
- <Box height={1}>
24
- <Text fg={colors.textBright} attributes={TextAttributes.BOLD}>
25
- {title}
26
- </Text>
27
- </Box>
28
- <Box height={1}>
29
- <Text fg={colors.textDim}>
30
- {padTo("PRICE", 8)} {padTo("SIZE", 10)}
31
- </Text>
32
- </Box>
33
- {levels.slice(0, 10).map((level) => (
34
- <Box
35
- key={`${title}:${level.price}:${level.size}`}
36
- height={1}
37
- onMouseDown={(event) => {
38
- event.preventDefault();
39
- onSelect(level);
40
- }}
41
- >
42
- <Text fg={colors.text}>
43
- {`${padTo(formatPredictionProbability(level.price), 8)} ${padTo(formatNumber(level.size, 0), 10, "right")}`}
44
- </Text>
45
- </Box>
46
- ))}
47
- {levels.length === 0 && <Text fg={colors.textDim}>No levels.</Text>}
48
- </Box>
49
- );
37
+ marketKey: string;
38
+ outcome: "yes" | "no";
39
+ side: "buy" | "sell";
40
+ }): BookRow[] {
41
+ return levels.slice(0, 10).map((level, index) => ({
42
+ id: `${outcome}:${side}:${index}:${level.price}:${level.size}`,
43
+ outcome,
44
+ side,
45
+ level,
46
+ intent: {
47
+ marketKey,
48
+ outcome,
49
+ side,
50
+ price: level.price,
51
+ size: level.size,
52
+ },
53
+ }));
54
+ }
55
+
56
+ function buildBookRows(detail: PredictionMarketDetail): BookRow[] {
57
+ const marketKey = detail.summary.key;
58
+ return [
59
+ ...bookRowsForLevels({
60
+ levels: detail.book.yesBids,
61
+ marketKey,
62
+ outcome: "yes",
63
+ side: "buy",
64
+ }),
65
+ ...bookRowsForLevels({
66
+ levels: detail.book.yesAsks,
67
+ marketKey,
68
+ outcome: "yes",
69
+ side: "sell",
70
+ }),
71
+ ...bookRowsForLevels({
72
+ levels: detail.book.noBids,
73
+ marketKey,
74
+ outcome: "no",
75
+ side: "buy",
76
+ }),
77
+ ...bookRowsForLevels({
78
+ levels: detail.book.noAsks,
79
+ marketKey,
80
+ outcome: "no",
81
+ side: "sell",
82
+ }),
83
+ ];
50
84
  }
51
85
 
52
86
  export function PredictionMarketBookView({
53
87
  detail,
88
+ focused,
54
89
  onPreviewOrder,
90
+ width,
55
91
  }: {
56
92
  detail: PredictionMarketDetail;
93
+ focused: boolean;
57
94
  onPreviewOrder: (intent: PredictionOrderPreviewIntent) => void;
95
+ width: number;
58
96
  }) {
97
+ const rows = useMemo(() => buildBookRows(detail), [detail]);
98
+ const [selectedIndex, setSelectedIndex] = useState<number | null>(() =>
99
+ rows.length > 0 ? 0 : null,
100
+ );
101
+
102
+ useEffect(() => {
103
+ setSelectedIndex((current) => {
104
+ if (rows.length === 0) return null;
105
+ if (current == null || current >= rows.length) return 0;
106
+ return current;
107
+ });
108
+ }, [rows.length]);
109
+
59
110
  return (
60
- <Box flexDirection="column" gap={1}>
61
- <Box flexDirection="row" gap={2}>
62
- <BookTable
63
- title="YES Bids"
64
- levels={detail.book.yesBids}
65
- onSelect={(level) =>
66
- onPreviewOrder({
67
- marketKey: detail.summary.key,
68
- outcome: "yes",
69
- side: "buy",
70
- price: level.price,
71
- size: level.size,
72
- })
73
- }
74
- />
75
- <BookTable
76
- title="YES Asks"
77
- levels={detail.book.yesAsks}
78
- onSelect={(level) =>
79
- onPreviewOrder({
80
- marketKey: detail.summary.key,
81
- outcome: "yes",
82
- side: "sell",
83
- price: level.price,
84
- size: level.size,
85
- })
86
- }
87
- />
88
- </Box>
89
- <Box flexDirection="row" gap={2}>
90
- <BookTable
91
- title="NO Bids"
92
- levels={detail.book.noBids}
93
- onSelect={(level) =>
94
- onPreviewOrder({
95
- marketKey: detail.summary.key,
96
- outcome: "no",
97
- side: "buy",
98
- price: level.price,
99
- size: level.size,
100
- })
101
- }
102
- />
103
- <BookTable
104
- title="NO Asks"
105
- levels={detail.book.noAsks}
106
- onSelect={(level) =>
107
- onPreviewOrder({
108
- marketKey: detail.summary.key,
109
- outcome: "no",
110
- side: "sell",
111
- price: level.price,
112
- size: level.size,
113
- })
114
- }
115
- />
116
- </Box>
117
- </Box>
111
+ <DataTableView<BookRow, BookColumn>
112
+ focused={focused}
113
+ keyboardNavigation={focused}
114
+ rootWidth={width}
115
+ rootBackgroundColor={colors.panel}
116
+ selection={{
117
+ kind: "index",
118
+ selectedIndex,
119
+ onChange: (index) => setSelectedIndex(index),
120
+ }}
121
+ columns={BOOK_COLUMNS}
122
+ items={rows}
123
+ sortColumnId={null}
124
+ sortDirection="asc"
125
+ onHeaderClick={() => {}}
126
+ getItemKey={(row) => row.id}
127
+ onActivate={(row) => onPreviewOrder(row.intent)}
128
+ onRowMouseDown={(row, index, event) => {
129
+ event.preventDefault();
130
+ setSelectedIndex(index);
131
+ onPreviewOrder(row.intent);
132
+ return true;
133
+ }}
134
+ renderCell={(row, column, _index, rowState) => {
135
+ const color = (fallback: string) =>
136
+ rowState.selected ? undefined : fallback;
137
+ switch (column.id) {
138
+ case "outcome":
139
+ return {
140
+ text: row.outcome.toUpperCase(),
141
+ color: color(colors.textBright),
142
+ };
143
+ case "side":
144
+ return {
145
+ text: row.side === "buy" ? "BID" : "ASK",
146
+ color: color(row.side === "buy" ? colors.positive : colors.negative),
147
+ };
148
+ case "price":
149
+ return {
150
+ text: formatPredictionProbability(row.level.price),
151
+ color: color(colors.text),
152
+ };
153
+ case "size":
154
+ return {
155
+ text: formatNumber(row.level.size, 0),
156
+ color: color(colors.textDim),
157
+ };
158
+ }
159
+ }}
160
+ emptyStateTitle="No book levels."
161
+ emptyStateHint="This venue did not return current order book depth."
162
+ />
118
163
  );
119
164
  }
@@ -17,6 +17,7 @@ export function PredictionMarketOverviewView({
17
17
  detailWidth,
18
18
  height,
19
19
  historyRange,
20
+ loading,
20
21
  onHistoryRangeChange,
21
22
  onSelectMarket,
22
23
  selectedRow,
@@ -26,6 +27,7 @@ export function PredictionMarketOverviewView({
26
27
  detailWidth: number;
27
28
  height: number;
28
29
  historyRange: PredictionHistoryRange;
30
+ loading: boolean;
29
31
  onHistoryRangeChange: (range: PredictionHistoryRange) => void;
30
32
  onSelectMarket: (marketKey: string) => void;
31
33
  selectedRow: PredictionListRow | null;
@@ -47,6 +49,7 @@ export function PredictionMarketOverviewView({
47
49
  history={detail?.history ?? []}
48
50
  width={detailWidth}
49
51
  height={Math.max(Math.floor(height * 0.36), 10)}
52
+ loading={loading}
50
53
  range={historyRange}
51
54
  onRangeSelect={onHistoryRangeChange}
52
55
  />
@@ -1,7 +1,7 @@
1
1
  import { Box, ScrollBox, Text } from "../../../ui";
2
2
  import { TextAttributes, type ScrollBoxRenderable } from "../../../ui";
3
3
  import type { RefObject } from "react";
4
- import { Spinner, Tabs } from "../../../components";
4
+ import { Tabs } from "../../../components";
5
5
  import { EmptyState } from "../../../components/ui/status";
6
6
  import { colors } from "../../../theme/colors";
7
7
  import { padTo } from "../../../utils/format";
@@ -37,8 +37,11 @@ interface MetricCell {
37
37
  function MetricLabelRow({ metrics }: { metrics: MetricCell[] }) {
38
38
  return (
39
39
  <Box flexDirection="row" height={1}>
40
- {metrics.map((metric) => (
41
- <Box key={metric.label} width={metric.width + 1}>
40
+ {metrics.map((metric, index) => (
41
+ <Box
42
+ key={metric.label}
43
+ width={metric.width + (index === metrics.length - 1 ? 0 : 1)}
44
+ >
42
45
  <Text fg={colors.textDim}>{padTo(metric.label, metric.width)}</Text>
43
46
  </Box>
44
47
  ))}
@@ -49,8 +52,11 @@ function MetricLabelRow({ metrics }: { metrics: MetricCell[] }) {
49
52
  function MetricValueRow({ metrics }: { metrics: MetricCell[] }) {
50
53
  return (
51
54
  <Box flexDirection="row" height={1}>
52
- {metrics.map((metric) => (
53
- <Box key={metric.label} width={metric.width + 1}>
55
+ {metrics.map((metric, index) => (
56
+ <Box
57
+ key={metric.label}
58
+ width={metric.width + (index === metrics.length - 1 ? 0 : 1)}
59
+ >
54
60
  <Text
55
61
  fg={metric.color ?? colors.textBright}
56
62
  attributes={TextAttributes.BOLD}
@@ -108,8 +114,6 @@ export function PredictionMarketDetailPane({
108
114
  }
109
115
 
110
116
  const summaryMetrics = detail?.summary ?? selectedSummary;
111
- const detailTitle =
112
- selectedRow?.kind === "group" ? selectedRow.title : summaryMetrics.title;
113
117
  const detailSubtitleParts: string[] = [];
114
118
  if (selectedRow?.kind === "group") {
115
119
  if (summaryMetrics.category) detailSubtitleParts.push(summaryMetrics.category);
@@ -126,18 +130,18 @@ export function PredictionMarketDetailPane({
126
130
  );
127
131
  if (endsLabel !== "—") detailSubtitleParts.push(endsLabel);
128
132
  const detailSubtitle = detailSubtitleParts.join(" · ");
129
- const primaryMetrics: MetricCell[] = [
133
+ const metrics: MetricCell[] = [
130
134
  {
131
135
  label: "YES",
132
136
  value: formatPredictionProbability(summaryMetrics.yesPrice),
133
137
  color: getPredictionProbabilityColor(summaryMetrics.yesPrice),
134
- width: 12,
138
+ width: 6,
135
139
  },
136
140
  {
137
141
  label: "NO",
138
142
  value: formatPredictionProbability(summaryMetrics.noPrice),
139
143
  color: getPredictionProbabilityColor(summaryMetrics.noPrice),
140
- width: 12,
144
+ width: 6,
141
145
  },
142
146
  {
143
147
  label: "24H VOL",
@@ -145,7 +149,7 @@ export function PredictionMarketDetailPane({
145
149
  summaryMetrics.volume24h,
146
150
  summaryMetrics.volume24hUnit ?? "usd",
147
151
  ),
148
- width: 16,
152
+ width: 8,
149
153
  },
150
154
  {
151
155
  label: "TOTAL VOL",
@@ -153,28 +157,26 @@ export function PredictionMarketDetailPane({
153
157
  summaryMetrics.totalVolume,
154
158
  summaryMetrics.totalVolumeUnit ?? "usd",
155
159
  ),
156
- width: 16,
160
+ width: 9,
157
161
  },
158
- ];
159
- const secondaryMetrics: MetricCell[] = [
160
162
  {
161
163
  label: "OI",
162
164
  value: formatPredictionMetric(
163
165
  summaryMetrics.openInterest,
164
166
  summaryMetrics.openInterestUnit ?? "usd",
165
167
  ),
166
- width: 16,
168
+ width: 7,
167
169
  },
168
170
  {
169
171
  label: "SPREAD",
170
172
  value: formatPredictionSpread(summaryMetrics.spread),
171
- width: 16,
173
+ width: 7,
172
174
  },
173
175
  {
174
176
  label: "LAST",
175
177
  value: formatPredictionProbability(summaryMetrics.lastTradePrice),
176
178
  color: colors.textBright,
177
- width: 16,
179
+ width: 7,
178
180
  },
179
181
  ];
180
182
  const relatedSiblings =
@@ -186,33 +188,25 @@ export function PredictionMarketDetailPane({
186
188
  0,
187
189
  Math.max(Math.min(Math.floor((detailWidth - 10) / 18), 3), 0),
188
190
  ) ?? []);
189
- const headerHeight = detailSubtitle.length > 0 ? 3 : 2;
191
+ const headerHeight = detailSubtitle.length > 0 ? 2 : 0;
190
192
  const detailLoading = detailLoadCount > 0 && !detail;
191
- const titleColor = focused ? colors.textBright : colors.text;
192
193
  const detailTextWidth = Math.max(detailWidth, 12);
193
194
 
194
195
  return (
195
196
  <>
196
- <Box flexDirection="column" height={headerHeight} paddingBottom={1}>
197
- <Box flexDirection="row" height={1}>
198
- <Text fg={titleColor} attributes={TextAttributes.BOLD}>
199
- {truncatePredictionText(detailTitle, detailTextWidth)}
200
- </Text>
201
- </Box>
202
- {detailSubtitle.length > 0 && (
197
+ {headerHeight > 0 && (
198
+ <Box flexDirection="column" height={headerHeight} paddingBottom={1}>
203
199
  <Box flexDirection="row" height={1}>
204
200
  <Text fg={colors.textDim}>
205
201
  {truncatePredictionText(detailSubtitle, detailTextWidth)}
206
202
  </Text>
207
203
  </Box>
208
- )}
209
- </Box>
204
+ </Box>
205
+ )}
210
206
 
211
- <Box flexDirection="column" height={4} paddingBottom={1}>
212
- <MetricLabelRow metrics={primaryMetrics} />
213
- <MetricValueRow metrics={primaryMetrics} />
214
- <MetricLabelRow metrics={secondaryMetrics} />
215
- <MetricValueRow metrics={secondaryMetrics} />
207
+ <Box flexDirection="column" height={3} paddingBottom={1}>
208
+ <MetricLabelRow metrics={metrics} />
209
+ <MetricValueRow metrics={metrics} />
216
210
  </Box>
217
211
 
218
212
  {relatedSiblings.length > 0 && (
@@ -260,43 +254,56 @@ export function PredictionMarketDetailPane({
260
254
  </Box>
261
255
  )}
262
256
 
263
- <ScrollBox ref={scrollRef} flexGrow={1} scrollY>
264
- {detailLoading && (
265
- <Box height={1} paddingBottom={1}>
266
- <Spinner label="Loading market detail..." />
267
- </Box>
268
- )}
269
- {detailTab === "overview" && (
270
- <PredictionMarketOverviewView
271
- detail={detail}
272
- detailWidth={detailWidth}
273
- height={height}
274
- historyRange={historyRange}
275
- onHistoryRangeChange={onHistoryRangeChange}
276
- onSelectMarket={onSelectMarket}
277
- selectedRow={selectedRow}
278
- summary={summaryMetrics}
279
- />
280
- )}
257
+ {detailTab === "overview" || detailTab === "rules" ? (
258
+ <ScrollBox ref={scrollRef} flexGrow={1} scrollY>
259
+ {detailTab === "overview" && (
260
+ <PredictionMarketOverviewView
261
+ detail={detail}
262
+ detailWidth={detailWidth}
263
+ height={height}
264
+ historyRange={historyRange}
265
+ loading={detailLoading}
266
+ onHistoryRangeChange={onHistoryRangeChange}
267
+ onSelectMarket={onSelectMarket}
268
+ selectedRow={selectedRow}
269
+ summary={summaryMetrics}
270
+ />
271
+ )}
281
272
 
282
- {detailTab === "book" && detail && (
273
+ {detailTab === "rules" && (
274
+ <PredictionMarketRulesView
275
+ detailWidth={detailTextWidth}
276
+ rules={detail?.rules ?? []}
277
+ />
278
+ )}
279
+ </ScrollBox>
280
+ ) : null}
281
+
282
+ {detailTab === "book" && (
283
+ detail ? (
283
284
  <PredictionMarketBookView
284
285
  detail={detail}
286
+ focused={focused}
285
287
  onPreviewOrder={onPreviewOrder}
288
+ width={detailWidth}
286
289
  />
287
- )}
288
-
289
- {detailTab === "trades" && (
290
- <PredictionMarketTradesView trades={detail?.trades ?? []} />
291
- )}
290
+ ) : (
291
+ <Box flexGrow={1} justifyContent="center">
292
+ <EmptyState
293
+ title={detailLoading ? "Loading order book." : "No order book."}
294
+ hint="This venue did not return current order book depth."
295
+ />
296
+ </Box>
297
+ )
298
+ )}
292
299
 
293
- {detailTab === "rules" && (
294
- <PredictionMarketRulesView
295
- detailWidth={detailTextWidth}
296
- rules={detail?.rules ?? []}
297
- />
298
- )}
299
- </ScrollBox>
300
+ {detailTab === "trades" && (
301
+ <PredictionMarketTradesView
302
+ focused={focused}
303
+ trades={detail?.trades ?? []}
304
+ width={detailWidth}
305
+ />
306
+ )}
300
307
  </>
301
308
  );
302
309
  }
@@ -1,6 +1,11 @@
1
1
  import { Box } from "../../../ui";
2
2
  import { colors } from "../../../theme/colors";
3
3
  import { ExternalLinkText } from "../../../components/ui/external-link";
4
+ import type {
5
+ PredictionListRow,
6
+ PredictionMarketDetail,
7
+ PredictionMarketSummary,
8
+ } from "../types";
4
9
 
5
10
  export function truncatePredictionText(
6
11
  value: string,
@@ -11,6 +16,20 @@ export function truncatePredictionText(
11
16
  return `${value.slice(0, maxLength - 3)}...`;
12
17
  }
13
18
 
19
+ export function resolvePredictionDetailTitle({
20
+ detail,
21
+ selectedRow,
22
+ selectedSummary,
23
+ }: {
24
+ detail: PredictionMarketDetail | null;
25
+ selectedRow: PredictionListRow | null;
26
+ selectedSummary: PredictionMarketSummary | null;
27
+ }): string | undefined {
28
+ if (!selectedSummary) return undefined;
29
+ const summary = detail?.summary ?? selectedSummary;
30
+ return selectedRow?.kind === "group" ? selectedRow.title : summary.title;
31
+ }
32
+
14
33
  export function SummaryLink({
15
34
  url,
16
35
  maxLength,
@@ -1,31 +1,76 @@
1
- import { Box, Text } from "../../../ui";
1
+ import { DataTableView, type DataTableColumn } from "../../../components";
2
2
  import { colors } from "../../../theme/colors";
3
- import { formatNumber, padTo } from "../../../utils/format";
3
+ import { formatNumber } from "../../../utils/format";
4
4
  import { formatPredictionProbability } from "../metrics";
5
5
  import type { PredictionTrade } from "../types";
6
6
 
7
+ type TradeColumnId = "time" | "side" | "outcome" | "price" | "size";
8
+ type TradeColumn = DataTableColumn & { id: TradeColumnId };
9
+
10
+ const TRADE_COLUMNS: TradeColumn[] = [
11
+ { id: "time", label: "TIME", width: 16 },
12
+ { id: "side", label: "SIDE", width: 6 },
13
+ { id: "outcome", label: "OUT", width: 4 },
14
+ { id: "price", label: "PRICE", width: 8, align: "right" },
15
+ { id: "size", label: "SIZE", width: 10, align: "right" },
16
+ ];
17
+
7
18
  export function PredictionMarketTradesView({
19
+ focused,
8
20
  trades,
21
+ width,
9
22
  }: {
23
+ focused: boolean;
10
24
  trades: PredictionTrade[];
25
+ width: number;
11
26
  }) {
12
27
  return (
13
- <Box flexDirection="column">
14
- <Box height={1}>
15
- <Text
16
- fg={colors.textDim}
17
- >{`${padTo("TIME", 16)} ${padTo("SIDE", 6)} ${padTo("OUT", 4)} ${padTo("PRICE", 8)} ${padTo("SIZE", 10)}`}</Text>
18
- </Box>
19
- {trades.slice(0, 30).map((trade) => (
20
- <Box key={trade.id} height={1}>
21
- <Text fg={trade.side === "buy" ? colors.positive : colors.negative}>
22
- {`${padTo(new Date(trade.timestamp).toLocaleTimeString("en-US", { hour12: false }), 16)} ${padTo(trade.side.toUpperCase(), 6)} ${padTo(trade.outcome.toUpperCase(), 4)} ${padTo(formatPredictionProbability(trade.price), 8)} ${padTo(formatNumber(trade.size, 0), 10, "right")}`}
23
- </Text>
24
- </Box>
25
- ))}
26
- {trades.length === 0 && (
27
- <Text fg={colors.textDim}>No recent trades.</Text>
28
- )}
29
- </Box>
28
+ <DataTableView<PredictionTrade, TradeColumn>
29
+ focused={focused}
30
+ keyboardNavigation={false}
31
+ rootWidth={width}
32
+ rootBackgroundColor={colors.panel}
33
+ selection={{ kind: "none" }}
34
+ columns={TRADE_COLUMNS}
35
+ items={trades.slice(0, 30)}
36
+ sortColumnId={null}
37
+ sortDirection="asc"
38
+ onHeaderClick={() => {}}
39
+ getItemKey={(trade) => trade.id}
40
+ renderCell={(trade, column) => {
41
+ const tradeColor = trade.side === "buy" ? colors.positive : colors.negative;
42
+ switch (column.id) {
43
+ case "time":
44
+ return {
45
+ text: new Date(trade.timestamp).toLocaleTimeString("en-US", {
46
+ hour12: false,
47
+ }),
48
+ color: colors.textDim,
49
+ };
50
+ case "side":
51
+ return {
52
+ text: trade.side.toUpperCase(),
53
+ color: tradeColor,
54
+ };
55
+ case "outcome":
56
+ return {
57
+ text: trade.outcome.toUpperCase(),
58
+ color: colors.text,
59
+ };
60
+ case "price":
61
+ return {
62
+ text: formatPredictionProbability(trade.price),
63
+ color: tradeColor,
64
+ };
65
+ case "size":
66
+ return {
67
+ text: formatNumber(trade.size, 0),
68
+ color: colors.textDim,
69
+ };
70
+ }
71
+ }}
72
+ emptyStateTitle="No recent trades."
73
+ emptyStateHint="This venue did not return recent prints."
74
+ />
30
75
  );
31
76
  }