gloomberb 0.6.1 → 0.7.1

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 (208) hide show
  1. package/package.json +1 -1
  2. package/patches/@opentui%2Fcore@0.1.90.patch +48 -24
  3. package/src/app.tsx +158 -57
  4. package/src/capabilities/factories.ts +3 -0
  5. package/src/cli/registry.test.ts +0 -1
  6. package/src/components/chart/chart-controller.ts +0 -1
  7. package/src/components/chart/chart-data.ts +1 -31
  8. package/src/components/chart/chart-renderer.ts +94 -12
  9. package/src/components/chart/chart-resolution.test.ts +0 -36
  10. package/src/components/chart/chart-resolution.ts +0 -27
  11. package/src/components/chart/chart.test.ts +60 -45
  12. package/src/components/chart/comparison-chart-data.test.ts +1 -5
  13. package/src/components/chart/comparison-chart-data.ts +1 -4
  14. package/src/components/chart/comparison-chart-renderer.test.ts +26 -0
  15. package/src/components/chart/comparison-chart-renderer.ts +13 -6
  16. package/src/components/chart/comparison-stock-chart.tsx +63 -39
  17. package/src/components/chart/price-axis-labels.tsx +114 -0
  18. package/src/components/chart/static-chart-surface.tsx +30 -4
  19. package/src/components/chart/stock-chart.test.ts +0 -16
  20. package/src/components/chart/stock-chart.tsx +69 -41
  21. package/src/components/chart/time-axis-label.tsx +120 -0
  22. package/src/components/command-bar/__snapshots__/command-bar.test.tsx.snap +0 -28
  23. package/src/components/command-bar/command-bar.test.tsx +182 -36
  24. package/src/components/command-bar/command-bar.tsx +943 -1016
  25. package/src/components/command-bar/command-registry.ts +0 -10
  26. package/src/components/command-bar/confirm-body.tsx +51 -0
  27. package/src/components/command-bar/helpers.ts +2 -5
  28. package/src/components/command-bar/list-model.ts +76 -0
  29. package/src/components/command-bar/theme-picker.tsx +254 -0
  30. package/src/components/command-bar/view-model.test.ts +4 -4
  31. package/src/components/command-bar/view-model.ts +1 -20
  32. package/src/components/command-bar/workflow-ops.ts +4 -0
  33. package/src/components/command-bar/workflow-types.ts +1 -2
  34. package/src/components/data-table-stack-view.test.tsx +39 -4
  35. package/src/components/data-table-stack-view.tsx +11 -104
  36. package/src/components/data-table-view.tsx +43 -79
  37. package/src/components/feed-data-table-stack-view.tsx +10 -17
  38. package/src/components/feed-stack-controller.ts +39 -0
  39. package/src/components/index.ts +5 -1
  40. package/src/components/layout/detached-pane-shell.tsx +29 -10
  41. package/src/components/layout/floating-pane.tsx +35 -4
  42. package/src/components/layout/header.tsx +12 -7
  43. package/src/components/layout/pane-content.test.tsx +64 -0
  44. package/src/components/layout/pane-content.tsx +2 -0
  45. package/src/components/layout/pane-cycle-order.test.ts +30 -0
  46. package/src/components/layout/pane-cycle-order.ts +32 -0
  47. package/src/components/layout/pane-footer.tsx +11 -4
  48. package/src/components/layout/pane-header.tsx +5 -5
  49. package/src/components/layout/pane-sizing.ts +9 -2
  50. package/src/components/layout/pane-title.ts +4 -1
  51. package/src/components/layout/pane.tsx +11 -5
  52. package/src/components/layout/shell.test.tsx +264 -22
  53. package/src/components/layout/shell.tsx +46 -8
  54. package/src/components/layout/status-bar.tsx +2 -0
  55. package/src/components/onboarding/onboarding-wizard.tsx +1 -2
  56. package/src/components/pane-settings-dialog.tsx +12 -4
  57. package/src/components/pane-template-wizard.test.tsx +3 -4
  58. package/src/components/speedometer-gauge.test.tsx +45 -0
  59. package/src/components/speedometer-gauge.tsx +562 -0
  60. package/src/components/table-view-shared.tsx +218 -0
  61. package/src/components/ticker-list-table-view.tsx +57 -73
  62. package/src/components/ticker-list-table.test.tsx +82 -1
  63. package/src/components/ticker-list-table.tsx +22 -9
  64. package/src/components/ui/confirm-dialog.tsx +66 -0
  65. package/src/components/ui/data-table.tsx +30 -19
  66. package/src/components/ui/frame.tsx +2 -0
  67. package/src/components/ui/index.ts +0 -1
  68. package/src/components/ui/page-stack-view.tsx +39 -10
  69. package/src/components/ui/table-layout.test.ts +42 -0
  70. package/src/components/ui/table-layout.ts +49 -4
  71. package/src/components/ui/tabs.tsx +34 -1
  72. package/src/components/ui/ui.test.tsx +168 -21
  73. package/src/core/state/app-state.ts +10 -17
  74. package/src/market-data/coordinator.test.ts +76 -1
  75. package/src/market-data/coordinator.ts +218 -49
  76. package/src/news/aggregator.test.ts +49 -1
  77. package/src/news/aggregator.ts +48 -3
  78. package/src/news/hooks.ts +6 -2
  79. package/src/plugins/builtin/ai/ask-ai-detail-tab.tsx +46 -16
  80. package/src/plugins/builtin/ai/providers.ts +0 -17
  81. package/src/plugins/builtin/ai/screener-pane.test.tsx +3 -4
  82. package/src/plugins/builtin/ai/screener-pane.tsx +3 -5
  83. package/src/plugins/builtin/analytics/index.tsx +1 -18
  84. package/src/plugins/builtin/ask-ai.test.tsx +89 -2
  85. package/src/plugins/builtin/broker-manager/index.test.tsx +2 -2
  86. package/src/plugins/builtin/broker-manager/index.tsx +19 -36
  87. package/src/plugins/builtin/chat-controller.test.ts +188 -1
  88. package/src/plugins/builtin/chat-controller.ts +537 -220
  89. package/src/plugins/builtin/chat.test.tsx +431 -13
  90. package/src/plugins/builtin/chat.tsx +1134 -227
  91. package/src/plugins/builtin/cloud-tweets.tsx +1 -1
  92. package/src/plugins/builtin/comparison-chart.test.tsx +53 -2
  93. package/src/plugins/builtin/comparison-chart.tsx +11 -5
  94. package/src/plugins/builtin/correlation/index.tsx +1 -6
  95. package/src/plugins/builtin/debug.tsx +1 -3
  96. package/src/plugins/builtin/econ/index.tsx +1 -2
  97. package/src/plugins/builtin/fear-greed/fear-greed-data.test.ts +72 -0
  98. package/src/plugins/builtin/fear-greed/fear-greed-data.ts +366 -0
  99. package/src/plugins/builtin/fear-greed/index.tsx +587 -0
  100. package/src/plugins/builtin/fx-matrix/index.tsx +2 -2
  101. package/src/plugins/builtin/help.test.tsx +2 -2
  102. package/src/plugins/builtin/help.tsx +2 -2
  103. package/src/plugins/builtin/holders/index.test.ts +13 -1
  104. package/src/plugins/builtin/holders/index.tsx +64 -29
  105. package/src/plugins/builtin/insider/index.tsx +22 -89
  106. package/src/plugins/builtin/market-movers/index.tsx +13 -19
  107. package/src/plugins/builtin/market-movers/screener.test.ts +39 -1
  108. package/src/plugins/builtin/market-movers/screener.ts +70 -17
  109. package/src/plugins/builtin/news-wire/breaking-pane.tsx +3 -2
  110. package/src/plugins/builtin/news-wire/index.tsx +41 -2
  111. package/src/plugins/builtin/news-wire/industry-pane.tsx +4 -18
  112. package/src/plugins/builtin/news-wire/news-detail-view.tsx +46 -4
  113. package/src/plugins/builtin/news-wire/news-preset-pane.tsx +3 -2
  114. package/src/plugins/builtin/news-wire/news-table.tsx +11 -11
  115. package/src/plugins/builtin/news.tsx +4 -16
  116. package/src/plugins/builtin/notes.tsx +19 -55
  117. package/src/plugins/builtin/options.test.tsx +58 -2
  118. package/src/plugins/builtin/options.tsx +110 -65
  119. package/src/plugins/builtin/plugin-groups.ts +2 -0
  120. package/src/plugins/builtin/portfolio-list/cli/commands.ts +1 -1
  121. package/src/plugins/builtin/portfolio-list/pane.tsx +16 -39
  122. package/src/plugins/builtin/research/index.test.ts +94 -0
  123. package/src/plugins/builtin/research/index.tsx +164 -33
  124. package/src/plugins/builtin/sec-filing-display.tsx +84 -0
  125. package/src/plugins/builtin/sec.tsx +13 -66
  126. package/src/plugins/builtin/sectors/index.tsx +1 -1
  127. package/src/plugins/builtin/ticker-detail/chart-tab.tsx +0 -1
  128. package/src/plugins/builtin/ticker-detail/index.tsx +0 -1
  129. package/src/plugins/builtin/ticker-detail/overview-tab.tsx +0 -1
  130. package/src/plugins/builtin/ticker-detail/pane.tsx +28 -22
  131. package/src/plugins/builtin/ticker-detail-chart-tab-switch.test.tsx +3 -4
  132. package/src/plugins/builtin/ticker-detail.test.tsx +38 -0
  133. package/src/plugins/builtin/world-indices/index.tsx +1 -1
  134. package/src/plugins/builtin/yield-curve/index.tsx +1 -1
  135. package/src/plugins/ibkr/broker-adapter.ts +1 -3
  136. package/src/plugins/ibkr/gateway-service-native.ts +1 -5
  137. package/src/plugins/ibkr/gateway-service.ts +1 -1
  138. package/src/plugins/ibkr/index.test.tsx +0 -2
  139. package/src/plugins/ibkr/trade-tab.test.tsx +3 -4
  140. package/src/plugins/pane-manager.ts +0 -13
  141. package/src/plugins/plugin-runtime.test.tsx +40 -6
  142. package/src/plugins/plugin-runtime.tsx +20 -10
  143. package/src/plugins/prediction-markets/cli.ts +0 -1
  144. package/src/plugins/prediction-markets/controller-data.ts +0 -2
  145. package/src/plugins/prediction-markets/controller.ts +0 -15
  146. package/src/plugins/prediction-markets/detail/pane.tsx +1 -0
  147. package/src/plugins/prediction-markets/pane.test.tsx +3 -11
  148. package/src/plugins/prediction-markets/test-helpers.tsx +1 -1
  149. package/src/plugins/registry.ts +13 -42
  150. package/src/plugins/ticker-navigation.test.ts +66 -0
  151. package/src/plugins/ticker-navigation.ts +44 -0
  152. package/src/react/input.ts +2 -1
  153. package/src/renderers/electrobun/bun/index.ts +32 -11
  154. package/src/renderers/electrobun/bun/window-focus.ts +37 -0
  155. package/src/renderers/electrobun/shared/protocol.ts +7 -1
  156. package/src/renderers/electrobun/view/app-services.ts +88 -31
  157. package/src/renderers/electrobun/view/backend-rpc.ts +15 -0
  158. package/src/renderers/electrobun/view/data-table.tsx +138 -101
  159. package/src/renderers/electrobun/view/desktop-controls.tsx +42 -20
  160. package/src/renderers/electrobun/view/desktop-window-bridge.ts +12 -1
  161. package/src/renderers/electrobun/view/dialog-host.tsx +13 -1
  162. package/src/renderers/electrobun/view/input-host.tsx +74 -10
  163. package/src/renderers/electrobun/view/key-event.ts +1 -1
  164. package/src/renderers/electrobun/view/main.tsx +18 -1
  165. package/src/renderers/electrobun/view/styles.css +329 -0
  166. package/src/renderers/electrobun/view/toast-host.tsx +29 -3
  167. package/src/renderers/electrobun/view/ui-host.tsx +42 -0
  168. package/src/renderers/opentui/chart-surface.tsx +179 -0
  169. package/src/renderers/opentui/input-host.tsx +1 -1
  170. package/src/renderers/opentui/start.tsx +48 -30
  171. package/src/renderers/opentui/test-utils.tsx +17 -0
  172. package/src/renderers/opentui/ui-host.tsx +5 -2
  173. package/src/sources/gloomberb-cloud.test.ts +86 -1
  174. package/src/sources/gloomberb-cloud.ts +117 -21
  175. package/src/sources/provider-router.test.ts +122 -0
  176. package/src/sources/provider-router.ts +198 -15
  177. package/src/sources/yahoo-finance.test.ts +44 -0
  178. package/src/sources/yahoo-finance.ts +6 -0
  179. package/src/state/app-bootstrap.ts +20 -7
  180. package/src/state/app-context-selector.test.tsx +189 -4
  181. package/src/state/app-context.test.ts +65 -0
  182. package/src/state/app-context.tsx +48 -16
  183. package/src/state/use-inline-tickers.test.tsx +73 -0
  184. package/src/state/use-inline-tickers.ts +19 -10
  185. package/src/test-support/plugin-runtime.ts +3 -2
  186. package/src/theme/colors.test.ts +75 -0
  187. package/src/theme/colors.ts +76 -6
  188. package/src/theme/theme-context.tsx +26 -0
  189. package/src/theme/themes.test.ts +23 -1
  190. package/src/theme/themes.ts +115 -0
  191. package/src/types/broker.ts +1 -1
  192. package/src/types/capability-route-source.ts +1 -0
  193. package/src/types/data-provider.ts +15 -2
  194. package/src/types/desktop-window.ts +6 -0
  195. package/src/types/financials.ts +2 -0
  196. package/src/types/plugin.ts +1 -3
  197. package/src/ui/host.tsx +7 -0
  198. package/src/utils/api-client.test.ts +32 -0
  199. package/src/utils/api-client.ts +119 -18
  200. package/src/utils/back-navigation.ts +24 -1
  201. package/src/utils/double-escape-close.test.ts +33 -0
  202. package/src/utils/double-escape-close.ts +40 -0
  203. package/src/utils/throttled-fetch.test.ts +22 -0
  204. package/src/utils/throttled-fetch.ts +3 -1
  205. package/src/utils/ticker-search.ts +53 -9
  206. package/src/version.ts +1 -1
  207. package/src/plugins/builtin/news-wire/feed-pane.tsx +0 -21
  208. package/src/plugins/builtin/news-wire/top-pane.tsx +0 -21
@@ -1,50 +1,23 @@
1
1
  import { useShortcut } from "../react/input";
2
- import { type ScrollBoxRenderable } from "../ui";
3
- import { type ReactNode, type RefObject } from "react";
4
- import { DataTableView } from "./data-table-view";
5
- import { PageStackView, type DataTableColumn, type DataTableProps } from "./ui";
6
-
7
- interface KeyboardEventLike {
8
- name?: string;
9
- preventDefault?: () => void;
10
- stopPropagation?: () => void;
11
- }
2
+ import { type ReactNode } from "react";
3
+ import {
4
+ DataTableView,
5
+ type DataTableKeyEvent,
6
+ type DataTableViewProps,
7
+ } from "./data-table-view";
8
+ import { PageStackView, type DataTableColumn } from "./ui";
12
9
 
13
10
  export interface DataTableStackViewProps<
14
11
  T,
15
12
  C extends DataTableColumn = DataTableColumn,
16
- > extends Omit<
17
- DataTableProps<T, C>,
18
- | "headerScrollRef"
19
- | "scrollRef"
20
- | "syncHeaderScroll"
21
- | "onBodyScrollActivity"
22
- | "hoveredIdx"
23
- | "setHoveredIdx"
24
- > {
13
+ > extends Omit<DataTableViewProps<T, C>, "focused" | "selectedIndex"> {
25
14
  focused: boolean;
26
15
  detailOpen: boolean;
27
16
  onBack: () => void;
28
17
  detailContent: ReactNode;
29
18
  detailTitle?: string;
30
19
  selectedIndex: number;
31
- onSelectIndex?: (index: number, item: T) => void;
32
- onActivateIndex?: (index: number, item: T) => void;
33
- isNavigable?: (item: T, index: number) => boolean;
34
- rootBefore?: ReactNode;
35
- rootAfter?: ReactNode;
36
- rootWidth?: number;
37
- rootHeight?: number;
38
- rootBackgroundColor?: string;
39
- headerScrollRef?: RefObject<ScrollBoxRenderable | null>;
40
- scrollRef?: RefObject<ScrollBoxRenderable | null>;
41
- syncHeaderScroll?: () => void;
42
- onBodyScrollActivity?: () => void;
43
- hoveredIdx?: number | null;
44
- setHoveredIdx?: (index: number | null) => void;
45
- keyboardNavigation?: boolean;
46
- onRootKeyDown?: (event: KeyboardEventLike) => boolean | void;
47
- onDetailKeyDown?: (event: KeyboardEventLike) => boolean | void;
20
+ onDetailKeyDown?: (event: DataTableKeyEvent) => boolean | void;
48
21
  }
49
22
 
50
23
  export function DataTableStackView<
@@ -56,42 +29,9 @@ export function DataTableStackView<
56
29
  onBack,
57
30
  detailContent,
58
31
  detailTitle,
59
- selectedIndex,
60
- onSelectIndex,
61
- onActivateIndex,
62
- isNavigable,
63
- rootBefore,
64
- rootAfter,
65
- rootWidth,
66
- rootHeight,
67
- rootBackgroundColor,
68
- headerScrollRef,
69
- scrollRef,
70
- syncHeaderScroll,
71
- onBodyScrollActivity,
72
- hoveredIdx,
73
- setHoveredIdx,
74
32
  keyboardNavigation = true,
75
- onRootKeyDown,
76
33
  onDetailKeyDown,
77
- columns,
78
- items,
79
- sortColumnId,
80
- sortDirection,
81
- onHeaderClick,
82
- getItemKey,
83
- isSelected,
84
- onSelect,
85
- onActivate,
86
- renderCell,
87
- renderSectionHeader,
88
- getRowBackgroundColor,
89
- emptyContent,
90
- emptyStateTitle,
91
- emptyStateHint,
92
- virtualize = true,
93
- overscan,
94
- showHorizontalScrollbar,
34
+ ...tableProps
95
35
  }: DataTableStackViewProps<T, C>) {
96
36
  useShortcut((event) => {
97
37
  if (!focused || !detailOpen || !keyboardNavigation) return;
@@ -100,42 +40,9 @@ export function DataTableStackView<
100
40
 
101
41
  const rootContent = (
102
42
  <DataTableView<T, C>
43
+ {...tableProps}
103
44
  focused={focused && !detailOpen}
104
- selectedIndex={selectedIndex}
105
- onSelectIndex={onSelectIndex}
106
- onActivateIndex={onActivateIndex}
107
- isNavigable={isNavigable}
108
- rootBefore={rootBefore}
109
- rootAfter={rootAfter}
110
- rootWidth={rootWidth}
111
- rootHeight={rootHeight}
112
- rootBackgroundColor={rootBackgroundColor}
113
- headerScrollRef={headerScrollRef}
114
- scrollRef={scrollRef}
115
- syncHeaderScroll={syncHeaderScroll}
116
- onBodyScrollActivity={onBodyScrollActivity}
117
- hoveredIdx={hoveredIdx}
118
- setHoveredIdx={setHoveredIdx}
119
45
  keyboardNavigation={keyboardNavigation}
120
- onRootKeyDown={onRootKeyDown}
121
- columns={columns}
122
- items={items}
123
- sortColumnId={sortColumnId}
124
- sortDirection={sortDirection}
125
- onHeaderClick={onHeaderClick}
126
- getItemKey={getItemKey}
127
- isSelected={isSelected}
128
- onSelect={onSelect}
129
- onActivate={onActivate}
130
- renderCell={renderCell}
131
- renderSectionHeader={renderSectionHeader}
132
- getRowBackgroundColor={getRowBackgroundColor}
133
- emptyContent={emptyContent}
134
- emptyStateTitle={emptyStateTitle}
135
- emptyStateHint={emptyStateHint}
136
- virtualize={virtualize}
137
- overscan={overscan}
138
- showHorizontalScrollbar={showHorizontalScrollbar}
139
46
  />
140
47
  );
141
48
 
@@ -2,24 +2,26 @@ import {
2
2
  useCallback,
3
3
  useEffect,
4
4
  useMemo,
5
- useRef,
6
5
  useState,
7
6
  type ReactNode,
8
7
  type RefObject,
9
8
  } from "react";
10
- import { Box, type ScrollBoxRenderable } from "../ui";
9
+ import type { ScrollBoxRenderable } from "../ui";
11
10
  import { useShortcut } from "../react/input";
12
11
  import { DataTable, type DataTableColumn, type DataTableProps } from "./ui";
13
-
14
- export interface DataTableKeyEvent {
15
- name?: string;
16
- ctrl?: boolean;
17
- meta?: boolean;
18
- option?: boolean;
19
- shift?: boolean;
20
- preventDefault?: () => void;
21
- stopPropagation?: () => void;
22
- }
12
+ import {
13
+ isNextTableRowKey,
14
+ isPreviousTableRowKey,
15
+ isTableActivationKey,
16
+ stopTableKey,
17
+ TableViewFrame,
18
+ type TableViewKeyEvent,
19
+ useResetTableScroll,
20
+ useTableBodyScrollActivity,
21
+ useTableViewState,
22
+ } from "./table-view-shared";
23
+
24
+ export type DataTableKeyEvent = TableViewKeyEvent;
23
25
 
24
26
  export interface DataTableViewProps<
25
27
  T,
@@ -54,23 +56,6 @@ export interface DataTableViewProps<
54
56
  resetScrollKey?: unknown;
55
57
  }
56
58
 
57
- function isActivationKey(name: string | undefined): boolean {
58
- return name === "enter" || name === "return";
59
- }
60
-
61
- function isNextRowKey(name: string | undefined): boolean {
62
- return name === "j" || name === "down";
63
- }
64
-
65
- function isPreviousRowKey(name: string | undefined): boolean {
66
- return name === "k" || name === "up";
67
- }
68
-
69
- function stopTableKey(event: DataTableKeyEvent) {
70
- event.stopPropagation?.();
71
- event.preventDefault?.();
72
- }
73
-
74
59
  export function DataTableView<
75
60
  T,
76
61
  C extends DataTableColumn = DataTableColumn,
@@ -97,17 +82,19 @@ export function DataTableView<
97
82
  scrollToIndex,
98
83
  ...tableProps
99
84
  }: DataTableViewProps<T, C>) {
100
- const internalHeaderScrollRef = useRef<ScrollBoxRenderable>(null);
101
- const internalScrollRef = useRef<ScrollBoxRenderable>(null);
102
- const [internalHoveredIdx, setInternalHoveredIdx] = useState<number | null>(
103
- null,
104
- );
105
-
106
- const effectiveHeaderScrollRef = headerScrollRef ?? internalHeaderScrollRef;
107
- const effectiveScrollRef = scrollRef ?? internalScrollRef;
108
- const effectiveHoveredIdx =
109
- hoveredIdx !== undefined ? hoveredIdx : internalHoveredIdx;
110
- const effectiveSetHoveredIdx = setHoveredIdx ?? setInternalHoveredIdx;
85
+ const {
86
+ effectiveHeaderScrollRef,
87
+ effectiveScrollRef,
88
+ effectiveHoveredIdx,
89
+ effectiveSetHoveredIdx,
90
+ effectiveSyncHeaderScroll,
91
+ } = useTableViewState({
92
+ headerScrollRef,
93
+ scrollRef,
94
+ syncHeaderScroll,
95
+ hoveredIdx,
96
+ setHoveredIdx,
97
+ });
111
98
  const [fallbackSelectedIndex, setFallbackSelectedIndex] = useState<
112
99
  number | null
113
100
  >(null);
@@ -148,36 +135,16 @@ export function DataTableView<
148
135
  setFallbackSelectedIndex(nextIndex);
149
136
  }, [fallbackSelectedIndex, navigableIndices, usesFallbackSelection]);
150
137
 
151
- const defaultSyncHeaderScroll = useCallback(() => {
152
- const body = effectiveScrollRef.current;
153
- const header = effectiveHeaderScrollRef.current;
154
- if (!body || !header) return;
155
- if (header.scrollLeft !== body.scrollLeft) {
156
- header.scrollLeft = body.scrollLeft;
157
- }
158
- }, [effectiveHeaderScrollRef, effectiveScrollRef]);
159
- const effectiveSyncHeaderScroll = syncHeaderScroll ?? defaultSyncHeaderScroll;
160
-
161
- const handleBodyScrollActivity = useCallback(() => {
162
- if (onBodyScrollActivity) {
163
- onBodyScrollActivity();
164
- return;
165
- }
166
- queueMicrotask(effectiveSyncHeaderScroll);
167
- }, [effectiveSyncHeaderScroll, onBodyScrollActivity]);
138
+ const handleBodyScrollActivity = useTableBodyScrollActivity({
139
+ onBodyScrollActivity,
140
+ syncHeaderScroll: effectiveSyncHeaderScroll,
141
+ });
168
142
 
169
- useEffect(() => {
170
- if (resetScrollKey === undefined) return;
171
- const body = effectiveScrollRef.current;
172
- if (body) {
173
- body.scrollTop = 0;
174
- body.scrollLeft = 0;
175
- }
176
- const header = effectiveHeaderScrollRef.current;
177
- if (header) {
178
- header.scrollLeft = 0;
179
- }
180
- }, [effectiveHeaderScrollRef, effectiveScrollRef, resetScrollKey]);
143
+ useResetTableScroll({
144
+ headerScrollRef: effectiveHeaderScrollRef,
145
+ scrollRef: effectiveScrollRef,
146
+ resetScrollKey,
147
+ });
181
148
 
182
149
  const selectIndex = useCallback((index: number) => {
183
150
  if (index < 0 || index >= tableProps.items.length) return;
@@ -226,34 +193,32 @@ export function DataTableView<
226
193
  if (onRootKeyDown?.(event)) return;
227
194
  if (tableProps.items.length === 0) return;
228
195
 
229
- if (isNextRowKey(event.name)) {
196
+ if (isNextTableRowKey(event.name)) {
230
197
  stopTableKey(event);
231
198
  selectByOffset(1);
232
199
  return;
233
200
  }
234
201
 
235
- if (isPreviousRowKey(event.name)) {
202
+ if (isPreviousTableRowKey(event.name)) {
236
203
  stopTableKey(event);
237
204
  selectByOffset(-1);
238
205
  return;
239
206
  }
240
207
 
241
- if (isActivationKey(event.name)) {
208
+ if (isTableActivationKey(event.name)) {
242
209
  stopTableKey(event);
243
210
  activateSelection();
244
211
  }
245
212
  });
246
213
 
247
214
  return (
248
- <Box
249
- flexDirection="column"
250
- flexGrow={1}
215
+ <TableViewFrame
251
216
  width={rootWidth}
252
217
  height={rootHeight}
253
218
  backgroundColor={rootBackgroundColor}
254
- overflow="hidden"
219
+ before={rootBefore}
220
+ after={rootAfter}
255
221
  >
256
- {rootBefore}
257
222
  <DataTable<T, C>
258
223
  {...tableProps}
259
224
  headerScrollRef={effectiveHeaderScrollRef}
@@ -273,7 +238,6 @@ export function DataTableView<
273
238
  )
274
239
  )}
275
240
  />
276
- {rootAfter}
277
- </Box>
241
+ </TableViewFrame>
278
242
  );
279
243
  }
@@ -5,6 +5,12 @@ import { colors } from "../theme/colors";
5
5
  import { formatTimeAgo } from "../utils/format";
6
6
  import { toTimestampMillis } from "../utils/timestamp";
7
7
  import { DataTableStackView } from "./data-table-stack-view";
8
+ import {
9
+ activeStackIndex,
10
+ sortIndexedStackRows,
11
+ type IndexedStackRow,
12
+ type StackSortPreference,
13
+ } from "./feed-stack-controller";
8
14
  import { ExternalLink, type DataTableCell, type DataTableColumn } from "./ui";
9
15
  import { wrapTextLines } from "../utils/text-wrap";
10
16
 
@@ -23,15 +29,9 @@ export interface FeedDataTableItem {
23
29
  type DetailColumnId = "time" | "source" | "title";
24
30
  type DetailColumn = DataTableColumn & { id: DetailColumnId };
25
31
 
26
- interface DetailRow {
27
- item: FeedDataTableItem;
28
- itemIndex: number;
29
- }
32
+ type DetailRow = IndexedStackRow<FeedDataTableItem>;
30
33
 
31
- interface SortPreference {
32
- columnId: DetailColumnId;
33
- direction: "asc" | "desc";
34
- }
34
+ type SortPreference = StackSortPreference<DetailColumnId>;
35
35
 
36
36
  interface FeedDataTableStackViewProps {
37
37
  width: number;
@@ -151,19 +151,12 @@ export function FeedDataTableStackView({
151
151
  [items, sourceLabel, titleLabel, width],
152
152
  );
153
153
  const sortedRows = useMemo(() => {
154
- const direction = sortPreference.direction === "asc" ? 1 : -1;
155
- return items
156
- .map((item, itemIndex) => ({ item, itemIndex }))
157
- .sort((a, b) => {
158
- const primary = compareRows(a, b, sortPreference.columnId) * direction;
159
- return primary !== 0 ? primary : a.itemIndex - b.itemIndex;
160
- });
154
+ return sortIndexedStackRows(items, sortPreference, compareRows);
161
155
  }, [items, sortPreference]);
162
156
  const selectedRowIndex = sortedRows.findIndex(
163
157
  (row) => row.itemIndex === selectedIdx,
164
158
  );
165
- const activeRowIndex =
166
- selectedRowIndex >= 0 ? selectedRowIndex : sortedRows.length > 0 ? 0 : -1;
159
+ const activeRowIndex = activeStackIndex(sortedRows.length, selectedRowIndex);
167
160
  const openItem = useMemo(
168
161
  () =>
169
162
  openItemId
@@ -0,0 +1,39 @@
1
+ export interface StackSortPreference<C extends string> {
2
+ columnId: C;
3
+ direction: "asc" | "desc";
4
+ }
5
+
6
+ export interface IndexedStackRow<T> {
7
+ item: T;
8
+ itemIndex: number;
9
+ }
10
+
11
+ export function sortStackItems<T, C extends string>(
12
+ items: T[],
13
+ preference: StackSortPreference<C>,
14
+ compare: (a: T, b: T, columnId: C) => number,
15
+ tieBreak: (a: T, b: T) => number = () => 0,
16
+ ): T[] {
17
+ const direction = preference.direction === "asc" ? 1 : -1;
18
+ return [...items].sort((a, b) => {
19
+ const primary = compare(a, b, preference.columnId) * direction;
20
+ return primary !== 0 ? primary : tieBreak(a, b);
21
+ });
22
+ }
23
+
24
+ export function sortIndexedStackRows<T, C extends string>(
25
+ items: T[],
26
+ preference: StackSortPreference<C>,
27
+ compare: (a: IndexedStackRow<T>, b: IndexedStackRow<T>, columnId: C) => number,
28
+ ): Array<IndexedStackRow<T>> {
29
+ return sortStackItems(
30
+ items.map((item, itemIndex) => ({ item, itemIndex })),
31
+ preference,
32
+ compare,
33
+ (a, b) => a.itemIndex - b.itemIndex,
34
+ );
35
+ }
36
+
37
+ export function activeStackIndex(length: number, selectedIndex: number): number {
38
+ return selectedIndex >= 0 ? selectedIndex : length > 0 ? 0 : -1;
39
+ }
@@ -1,6 +1,8 @@
1
1
 
2
2
  export { PriceSelectorDialog } from "./price-selector-dialog";
3
3
  export { StaticChartSurface } from "./chart/static-chart-surface";
4
+ export { SpeedometerGauge } from "./speedometer-gauge";
5
+ export type { SpeedometerSegment } from "./speedometer-gauge";
4
6
  export type { QuoteFlashDirection } from "./ticker-list-table";
5
7
  export { TickerListTableView } from "./ticker-list-table-view";
6
8
  export type { TickerListVisibleRange } from "./ticker-list-table-view";
@@ -10,14 +12,16 @@ export type { DataTableKeyEvent } from "./data-table-view";
10
12
  export { DataTableStackView } from "./data-table-stack-view";
11
13
  export { FeedDataTableStackView } from "./feed-data-table-stack-view";
12
14
  export type { FeedDataTableItem } from "./feed-data-table-stack-view";
15
+ export { activeStackIndex, sortStackItems } from "./feed-stack-controller";
16
+ export type { StackSortPreference } from "./feed-stack-controller";
13
17
  export { PaneFooterScope, usePaneFooter } from "./layout/pane-footer";
14
18
  export type { PaneFooterSegment, PaneHint } from "./layout/pane-footer";
15
19
  export { useExternalLinkFooter } from "./use-external-link-footer";
16
20
  export { Button } from "./ui/button";
21
+ export { ConfirmDialog } from "./ui/confirm-dialog";
17
22
  export { ChoiceDialog } from "./ui/choice-dialog";
18
23
  export type { ChoiceDialogChoice, ChoiceDialogProps } from "./ui/choice-dialog";
19
24
  export type { DataTableCell, DataTableColumn } from "./ui/data-table";
20
- export { DialogFrame } from "./ui/frame";
21
25
  export { EmptyState } from "./ui/status";
22
26
  export { getMessageComposerBlockHeight, MessageComposer } from "./ui/message-composer";
23
27
  export { NumberField, TextField } from "./ui/fields";
@@ -1,16 +1,22 @@
1
1
  import { Box, Text, useRendererHost, useUiCapabilities } from "../../ui";
2
- import { useCallback, useEffect, useMemo, useState } from "react";
2
+ import { useCallback, useEffect, useMemo, useRef, useState } from "react";
3
3
  import { useShortcut, useViewport } from "../../react/input";
4
4
  import { useAppDispatch, useAppSelector } from "../../state/app-context";
5
5
  import type { DesktopWindowBridge } from "../../types/desktop-window";
6
6
  import { findPaneInstance } from "../../types/config";
7
7
  import type { PluginRegistry } from "../../plugins/registry";
8
8
  import { colors, floatingPaneBg, floatingPaneTitleBg, paneTitleText } from "../../theme/colors";
9
+ import { useThemeColors } from "../../theme/theme-context";
9
10
  import { hasPaneFooterContent, PaneFooterBar, PaneFooterProvider } from "./pane-footer";
10
11
  import { PaneContent } from "./pane-content";
11
- import { getPaneBodyWidth } from "./pane-sizing";
12
+ import { getPaneBodyWidth, NATIVE_PANE_BODY_LAYOUT_PROPS } from "./pane-sizing";
12
13
  import { getPaneDisplayTitle } from "./pane-title";
13
14
  import { TITLEBAR_OVERLAY_HEIGHT_PX, TITLEBAR_TRAFFIC_LIGHT_WIDTH } from "./titlebar-overlay";
15
+ import {
16
+ createDoubleEscapeCloseState,
17
+ recordDoubleEscapeClose,
18
+ resetDoubleEscapeClose,
19
+ } from "../../utils/double-escape-close";
14
20
 
15
21
  interface DetachedPaneShellProps {
16
22
  pluginRegistry: PluginRegistry;
@@ -23,12 +29,13 @@ function stopMouse(event?: { stopPropagation?: () => void; preventDefault?: () =
23
29
  }
24
30
 
25
31
  export function DetachedPaneShell({ pluginRegistry, desktopWindowBridge }: DetachedPaneShellProps) {
32
+ useThemeColors();
26
33
  const dispatch = useAppDispatch();
27
34
  const rendererHost = useRendererHost();
28
35
  const config = useAppSelector((state) => state.config);
29
36
  const paneState = useAppSelector((state) => state.paneState);
30
- const focusedPaneId = useAppSelector((state) => state.focusedPaneId);
31
37
  const inputCaptured = useAppSelector((state) => state.inputCaptured);
38
+ const doubleEscapeCloseRef = useRef(createDoubleEscapeCloseState());
32
39
  const [windowFocused, setWindowFocused] = useState(() => (
33
40
  typeof document === "undefined" ? true : document.hasFocus()
34
41
  ));
@@ -52,12 +59,6 @@ export function DetachedPaneShell({ pluginRegistry, desktopWindowBridge }: Detac
52
59
  dispatch({ type: "FOCUS_PANE", paneId: desktopWindowBridge.paneId });
53
60
  }, [desktopWindowBridge.paneId, dispatch]);
54
61
 
55
- useEffect(() => {
56
- if (windowFocused && focusedPaneId !== desktopWindowBridge.paneId) {
57
- focusPane();
58
- }
59
- }, [desktopWindowBridge.paneId, focusPane, focusedPaneId, windowFocused]);
60
-
61
62
  useEffect(() => {
62
63
  if (typeof window === "undefined" || typeof document === "undefined") return;
63
64
 
@@ -81,6 +82,21 @@ export function DetachedPaneShell({ pluginRegistry, desktopWindowBridge }: Detac
81
82
  };
82
83
  }, [focusPane]);
83
84
 
85
+ useShortcut((event) => {
86
+ const doubleEscapeState = doubleEscapeCloseRef.current;
87
+ const isEscape = event.name === "escape" || event.name === "esc";
88
+ if (isEscape) {
89
+ if (recordDoubleEscapeClose(doubleEscapeState, desktopWindowBridge.paneId, Date.now())) {
90
+ event.preventDefault();
91
+ event.stopPropagation();
92
+ void desktopWindowBridge.closeDetachedPane?.(desktopWindowBridge.paneId);
93
+ }
94
+ return;
95
+ }
96
+
97
+ resetDoubleEscapeClose(doubleEscapeState);
98
+ }, { phase: "before" });
99
+
84
100
  useShortcut((event) => {
85
101
  if (event.name !== "w" || (!event.ctrl && !event.meta && !event.super)) return;
86
102
  if (inputCaptured && event.ctrl && !event.meta && !event.super) return;
@@ -116,6 +132,9 @@ export function DetachedPaneShell({ pluginRegistry, desktopWindowBridge }: Detac
116
132
  const bodyHeight = Math.max(1, height - headerHeightRows - footerHeightRows);
117
133
  const background = floatingPaneBg(focused);
118
134
  const titleBackground = floatingPaneTitleBg(focused);
135
+ const bodyLayoutProps = nativePaneChrome
136
+ ? NATIVE_PANE_BODY_LAYOUT_PROPS
137
+ : { height: bodyHeight };
119
138
 
120
139
  return (
121
140
  <Box
@@ -166,7 +185,7 @@ export function DetachedPaneShell({ pluginRegistry, desktopWindowBridge }: Detac
166
185
  )}
167
186
  </Box>
168
187
  </Box>
169
- <Box height={bodyHeight} overflow="hidden" backgroundColor={background}>
188
+ <Box {...bodyLayoutProps} overflow="hidden" backgroundColor={background}>
170
189
  <PaneContent
171
190
  component={paneDef.component}
172
191
  paneId={instance.instanceId}
@@ -3,7 +3,7 @@ import type { ReactNode } from "react";
3
3
  import { colors, floatingPaneBg } from "../../theme/colors";
4
4
  import { PaneHeader } from "./pane-header";
5
5
  import { hasPaneFooterContent, PaneFooterBar, type CombinedPaneFooter } from "./pane-footer";
6
- import { getPaneBodyHeight, shouldReservePaneFooter } from "./pane-sizing";
6
+ import { getPaneBodyHeight, NATIVE_PANE_BODY_LAYOUT_PROPS, shouldReservePaneFooter } from "./pane-sizing";
7
7
 
8
8
  interface FloatingPaneWrapperProps {
9
9
  paneId?: string;
@@ -30,6 +30,24 @@ interface FloatingPaneWrapperProps {
30
30
  children: ReactNode;
31
31
  }
32
32
 
33
+ function TerminalFloatingPaneBorder({ width, height }: { width: number; height: number }) {
34
+ const borderWidth = Math.max(0, Math.floor(width));
35
+ const borderHeight = Math.max(0, Math.floor(height));
36
+ const bodyHeight = Math.max(0, borderHeight - 2);
37
+ if (borderWidth < 2 || bodyHeight <= 0) return null;
38
+
39
+ return (
40
+ <>
41
+ <Box position="absolute" top={1} left={0} width={1} height={bodyHeight}>
42
+ <Text fg={colors.border} selectable={false}>{"│".repeat(bodyHeight)}</Text>
43
+ </Box>
44
+ <Box position="absolute" top={1} left={borderWidth - 1} width={1} height={bodyHeight}>
45
+ <Text fg={colors.border} selectable={false}>{"│".repeat(bodyHeight)}</Text>
46
+ </Box>
47
+ </>
48
+ );
49
+ }
50
+
33
51
  /** Pure visual wrapper; Shell owns the interaction state and supplies handlers. */
34
52
  export function FloatingPaneWrapper({
35
53
  paneId,
@@ -60,6 +78,9 @@ export function FloatingPaneWrapper({
60
78
  const showFooter = hasPaneFooterContent(footer);
61
79
  const reserveFooter = shouldReservePaneFooter(nativePaneChrome, showFooter);
62
80
  const bodyHeight = getPaneBodyHeight(height, reserveFooter);
81
+ const bodyLayoutProps = nativePaneChrome
82
+ ? NATIVE_PANE_BODY_LAYOUT_PROPS
83
+ : { height: bodyHeight };
63
84
 
64
85
  return (
65
86
  <Box
@@ -97,11 +118,21 @@ export function FloatingPaneWrapper({
97
118
  />
98
119
 
99
120
  {/* Content */}
100
- <Box height={bodyHeight} overflow="hidden">
121
+ <Box {...bodyLayoutProps} overflow="hidden" backgroundColor={bg}>
101
122
  {children}
102
123
  </Box>
103
124
 
104
- {reserveFooter && <PaneFooterBar footer={footer} focused={focused} width={width} reserveRight={2} />}
125
+ {reserveFooter && (
126
+ <PaneFooterBar
127
+ footer={footer}
128
+ focused={focused}
129
+ width={width}
130
+ reserveRight={2}
131
+ showBorder={!nativePaneChrome && !focused}
132
+ />
133
+ )}
134
+
135
+ {!nativePaneChrome && !focused && <TerminalFloatingPaneBorder width={width} height={height} />}
105
136
 
106
137
  {nativePaneChrome ? (
107
138
  <Box
@@ -117,7 +148,7 @@ export function FloatingPaneWrapper({
117
148
  />
118
149
  ) : (
119
150
  <Box position="absolute" bottom={0} right={0} width={2} height={1}>
120
- <Text fg={focused ? colors.borderFocused : colors.textDim} selectable={false}>{focused ? "─◢" : " ◢"}</Text>
151
+ <Text fg={focused ? colors.borderFocused : colors.border} selectable={false}>{"─◢"}</Text>
121
152
  </Box>
122
153
  )}
123
154
  </Box>
@@ -1,6 +1,7 @@
1
1
  import { Box, SpinnerMark, Text, TextAttributes, useUiCapabilities } from "../../ui";
2
2
  import { useEffect, type ReactNode } from "react";
3
- import { colors, priceColor } from "../../theme/colors";
3
+ import { blendHex, colors, priceColor } from "../../theme/colors";
4
+ import { useThemeColors } from "../../theme/theme-context";
4
5
  import { useAppActive } from "../../state/app-activity";
5
6
  import { useAppDispatch, useAppSelector } from "../../state/app-context";
6
7
  import {
@@ -23,8 +24,8 @@ const UPDATE_NOTICE_DURATION_MS = 5_000;
23
24
 
24
25
  function DesktopHeaderPill({
25
26
  children,
26
- backgroundColor = "rgba(8, 12, 15, 0.34)",
27
- borderColor = "rgba(132, 145, 161, 0.22)",
27
+ backgroundColor = blendHex(colors.header, colors.bg, 0.28),
28
+ borderColor = blendHex(colors.border, colors.headerText, 0.22),
28
29
  }: {
29
30
  children: ReactNode;
30
31
  backgroundColor?: string;
@@ -121,6 +122,7 @@ function UpdateStatus() {
121
122
  }
122
123
 
123
124
  export function Header() {
125
+ useThemeColors();
124
126
  const baseCurrency = useAppSelector(selectBaseCurrency);
125
127
  const appActive = useAppActive();
126
128
  const { titleBarOverlay } = useUiCapabilities();
@@ -173,8 +175,8 @@ export function Header() {
173
175
  Gloomberb
174
176
  </Text>
175
177
  <DesktopHeaderPill
176
- backgroundColor="rgba(84, 201, 159, 0.12)"
177
- borderColor="rgba(84, 201, 159, 0.28)"
178
+ backgroundColor={blendHex(colors.header, colors.headerText, 0.1)}
179
+ borderColor={blendHex(colors.border, colors.headerText, 0.28)}
178
180
  >
179
181
  <Text fg={colors.headerText} style={{ fontSize: 11 }}>v{VERSION}</Text>
180
182
  </DesktopHeaderPill>
@@ -184,8 +186,11 @@ export function Header() {
184
186
  </Box>
185
187
  {mktLabel ? (
186
188
  <Box paddingRight={1}>
187
- <DesktopHeaderPill borderColor="rgba(132, 145, 161, 0.18)">
188
- <Text fg={mktColor}>{mktLabel}</Text>
189
+ <DesktopHeaderPill
190
+ backgroundColor={blendHex(colors.header, mktColor, 0.12)}
191
+ borderColor={blendHex(colors.border, mktColor, 0.3)}
192
+ >
193
+ <Text fg={mktColor} style={{ fontSize: 11, fontWeight: 700 }}>{mktLabel}</Text>
189
194
  </DesktopHeaderPill>
190
195
  </Box>
191
196
  ) : null}