gloomberb 0.9.3 → 0.9.5

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 (176) hide show
  1. package/README.md +1 -0
  2. package/package.json +3 -2
  3. package/patches/@opentui%2Fcore@0.3.2.patch +231 -29
  4. package/patches/react-reconciler@0.33.0.patch +10 -0
  5. package/src/app/runtime/desktop-menu.ts +5 -1
  6. package/src/app/runtime/ticker-refresh.ts +4 -1
  7. package/src/app.tsx +10 -1
  8. package/src/brokers/account-cache.ts +1 -1
  9. package/src/brokers/sync-broker-instance.test.ts +107 -0
  10. package/src/brokers/sync-broker-instance.ts +22 -10
  11. package/src/cli/desktop-pane-shot.ts +2 -1
  12. package/src/cli/entry.ts +8 -1
  13. package/src/cli/native-smoke.ts +5 -0
  14. package/src/cli/pane-functions/screenshot.ts +13 -1
  15. package/src/components/chart/bar-chart-renderer.test.ts +108 -3
  16. package/src/components/chart/bar-chart-renderer.ts +226 -27
  17. package/src/components/chart/core/pointer.ts +32 -6
  18. package/src/components/chart/native/bitmap-support.test.ts +27 -0
  19. package/src/components/chart/native/bitmap-support.ts +55 -0
  20. package/src/components/chart/native/kitty/index.test.ts +47 -0
  21. package/src/components/chart/native/surface/visibility.ts +8 -1
  22. package/src/components/chart/price-axis-labels.tsx +3 -1
  23. package/src/components/chart/static/bar-chart-surface.test.tsx +97 -0
  24. package/src/components/chart/static/bar-chart-surface.tsx +151 -28
  25. package/src/components/chart/static/chart/axis-overlays.tsx +243 -0
  26. package/src/components/chart/static/chart/bitmap.ts +16 -9
  27. package/src/components/chart/static/chart/surface.test.tsx +92 -0
  28. package/src/components/chart/static/chart/surface.tsx +167 -24
  29. package/src/components/chart/stock-chart/index.test.ts +25 -0
  30. package/src/components/command-bar/layout-items/current-layout.ts +5 -1
  31. package/src/components/command-bar/list/view.tsx +1 -1
  32. package/src/components/command-bar/theme-picker.tsx +1 -1
  33. package/src/components/data-table/view.tsx +0 -12
  34. package/src/components/layout/detached-pane-shell.tsx +6 -3
  35. package/src/components/layout/floating-pane.tsx +6 -5
  36. package/src/components/layout/pane/content.tsx +20 -8
  37. package/src/components/layout/pane/footer/registration.tsx +6 -2
  38. package/src/components/layout/pane/frame.tsx +4 -4
  39. package/src/components/layout/pane/header.tsx +45 -4
  40. package/src/components/layout/pane/index.tsx +6 -4
  41. package/src/components/layout/pane/sizing.test.ts +43 -0
  42. package/src/components/layout/pane/sizing.ts +13 -4
  43. package/src/components/layout/shell/action-menu-overlay.tsx +1 -1
  44. package/src/components/layout/shell/cursor-occlusion.ts +204 -0
  45. package/src/components/layout/shell/drag/runtime.ts +9 -2
  46. package/src/components/layout/shell/index.test.tsx +92 -2
  47. package/src/components/layout/shell/index.tsx +45 -3
  48. package/src/components/layout/shell/layout-state.test.ts +33 -0
  49. package/src/components/layout/shell/layout-state.ts +4 -9
  50. package/src/components/layout/shell/pane/layers.tsx +10 -4
  51. package/src/components/layout/shell/visible-layout.ts +17 -0
  52. package/src/components/layout/status-bar.test.tsx +6 -1
  53. package/src/components/layout/status-bar.tsx +10 -6
  54. package/src/components/onboarding/onboarding-steps.tsx +8 -28
  55. package/src/components/onboarding/onboarding-wizard.test.tsx +13 -12
  56. package/src/components/onboarding/onboarding-wizard.tsx +2 -4
  57. package/src/components/onboarding/portfolio-step/index.tsx +0 -12
  58. package/src/components/onboarding/portfolio-step/types.ts +1 -3
  59. package/src/components/onboarding/wizard-keyboard.ts +2 -5
  60. package/src/components/pane-settings-dialog/desktop.tsx +1 -1
  61. package/src/components/price-performance.tsx +1 -1
  62. package/src/components/price-sparkline/view.tsx +15 -14
  63. package/src/components/speedometer-gauge/terminal.tsx +18 -15
  64. package/src/components/table-view-shared.tsx +12 -14
  65. package/src/components/ticker/list-table-view.tsx +1 -7
  66. package/src/components/ticker/list-table.test.tsx +0 -4
  67. package/src/components/ui/data-table/opentui/index.tsx +29 -22
  68. package/src/components/ui/data-table/types.ts +10 -4
  69. package/src/components/ui/fields.tsx +10 -3
  70. package/src/components/ui/list-view.tsx +1 -1
  71. package/src/components/ui/page-stack-view.tsx +6 -2
  72. package/src/components/ui/table-layout.test.ts +25 -0
  73. package/src/components/ui/table-layout.ts +22 -6
  74. package/src/components/ui/tabs.tsx +27 -12
  75. package/src/components/ui/ui.test.tsx +87 -9
  76. package/src/core/state/app/layout-reducer.ts +2 -2
  77. package/src/plugins/builtin/account-management/form-components.tsx +2 -2
  78. package/src/plugins/builtin/alerts/pane.tsx +1 -1
  79. package/src/plugins/builtin/analytics/index.test.tsx +6 -2
  80. package/src/plugins/builtin/analytics/pane-model.ts +43 -8
  81. package/src/plugins/builtin/buildout/cells.tsx +1 -1
  82. package/src/plugins/builtin/buildout/pane/index.tsx +1 -1
  83. package/src/plugins/builtin/chat/message/desktop.tsx +1 -1
  84. package/src/plugins/builtin/chat/message/inline-tokens.tsx +1 -1
  85. package/src/plugins/builtin/chat/message/terminal.tsx +2 -2
  86. package/src/plugins/builtin/chat/sidebar.tsx +1 -1
  87. package/src/plugins/builtin/chat/status-widget.tsx +1 -1
  88. package/src/plugins/builtin/cloud/auth-actions.tsx +2 -2
  89. package/src/plugins/builtin/correlation/matrix/symbol-cell.tsx +1 -1
  90. package/src/plugins/builtin/debug/index.tsx +3 -4
  91. package/src/plugins/builtin/help/components.tsx +1 -1
  92. package/src/plugins/builtin/holders/treemap.tsx +1 -1
  93. package/src/plugins/builtin/kelly-sizer/constants.ts +3 -0
  94. package/src/plugins/builtin/kelly-sizer/fields.ts +214 -0
  95. package/src/plugins/builtin/kelly-sizer/index.test.tsx +197 -0
  96. package/src/plugins/builtin/kelly-sizer/index.tsx +47 -0
  97. package/src/plugins/builtin/kelly-sizer/model.test.ts +152 -0
  98. package/src/plugins/builtin/kelly-sizer/model.ts +580 -0
  99. package/src/plugins/builtin/kelly-sizer/pane.tsx +558 -0
  100. package/src/plugins/builtin/kelly-sizer/portfolio.ts +71 -0
  101. package/src/plugins/builtin/kelly-sizer/sections.tsx +142 -0
  102. package/src/plugins/builtin/kelly-sizer/state.ts +67 -0
  103. package/src/plugins/builtin/kelly-sizer/test-support.tsx +174 -0
  104. package/src/plugins/builtin/kelly-sizer/types.ts +156 -0
  105. package/src/plugins/builtin/kelly-sizer/view.test.tsx +77 -0
  106. package/src/plugins/builtin/kelly-sizer/view.tsx +375 -0
  107. package/src/plugins/builtin/layout-manager/index.tsx +5 -1
  108. package/src/plugins/builtin/options/table.ts +5 -5
  109. package/src/plugins/builtin/options/view.test.tsx +141 -4
  110. package/src/plugins/builtin/options/view.tsx +76 -15
  111. package/src/plugins/builtin/portfolio-list/account-metrics.test.ts +26 -1
  112. package/src/plugins/builtin/portfolio-list/account-metrics.ts +14 -0
  113. package/src/plugins/builtin/portfolio-list/index.test.ts +23 -0
  114. package/src/plugins/builtin/portfolio-list/pane/index.tsx +2 -0
  115. package/src/plugins/builtin/portfolio-list/summary/index.test.ts +59 -1
  116. package/src/plugins/builtin/portfolio-list/summary/index.tsx +36 -5
  117. package/src/plugins/builtin/ticker-detail/data-panes/fundamental-graph/content.tsx +149 -48
  118. package/src/plugins/builtin/ticker-detail/data-panes/fundamental-graph/model.ts +25 -2
  119. package/src/plugins/builtin/ticker-detail/data-panes/fundamental-graph.tsx +5 -4
  120. package/src/plugins/builtin/ticker-detail/data-panes.test.ts +16 -0
  121. package/src/plugins/builtin/ticker-detail/index.test.tsx +143 -0
  122. package/src/plugins/catalog-ui.ts +2 -0
  123. package/src/plugins/ibkr/broker-adapter.ts +19 -0
  124. package/src/plugins/ibkr/config.test.ts +31 -2
  125. package/src/plugins/ibkr/config.ts +9 -2
  126. package/src/plugins/ibkr/flex/index.test.ts +18 -9
  127. package/src/plugins/ibkr/flex/index.ts +30 -0
  128. package/src/plugins/ibkr/flex/statement-client.ts +68 -14
  129. package/src/plugins/ibkr/gateway/account-loaders.ts +1 -1
  130. package/src/plugins/ibkr/gateway/account-summary.ts +1 -0
  131. package/src/plugins/ibkr/gateway/service/index.test.ts +8 -0
  132. package/src/plugins/ibkr/trade/ticket-panel.tsx +1 -1
  133. package/src/plugins/pane-manager/gridlock.ts +13 -5
  134. package/src/plugins/pane-manager/layout-state.ts +25 -0
  135. package/src/plugins/pane-manager.test.ts +33 -0
  136. package/src/plugins/pane-manager.ts +6 -1
  137. package/src/plugins/prediction-markets/chart.tsx +10 -4
  138. package/src/plugins/prediction-markets/detail/book.tsx +141 -96
  139. package/src/plugins/prediction-markets/detail/overview.tsx +3 -0
  140. package/src/plugins/prediction-markets/detail/pane.tsx +71 -64
  141. package/src/plugins/prediction-markets/detail/shared.tsx +19 -0
  142. package/src/plugins/prediction-markets/detail/trades.tsx +64 -19
  143. package/src/plugins/prediction-markets/detail.test.tsx +50 -0
  144. package/src/plugins/prediction-markets/pane.test.tsx +43 -0
  145. package/src/plugins/prediction-markets/pane.tsx +50 -22
  146. package/src/plugins/prediction-markets/test-helpers.tsx +41 -23
  147. package/src/renderers/electrobun/view/cli-pane-shot-entry.tsx +13 -2
  148. package/src/renderers/electrobun/view/data-table/index.tsx +19 -12
  149. package/src/renderers/electrobun/view/data-table/row.tsx +32 -18
  150. package/src/renderers/electrobun/view/desktop/controls.tsx +11 -5
  151. package/src/renderers/electrobun/view/host/box.tsx +6 -1
  152. package/src/renderers/electrobun/view/host/input.tsx +115 -18
  153. package/src/renderers/electrobun/view/host/style.ts +1 -0
  154. package/src/renderers/electrobun/view/host/tabs.tsx +7 -2
  155. package/src/renderers/electrobun/view/styles.css +18 -0
  156. package/src/renderers/opentui/host.tsx +17 -1
  157. package/src/renderers/opentui/native-smoke.ts +1 -0
  158. package/src/renderers/opentui/ui-host.tsx +57 -25
  159. package/src/sources/provider-router/batches.ts +16 -2
  160. package/src/sources/provider-router/financial-routes.ts +5 -0
  161. package/src/sources/provider-router/financials.ts +11 -23
  162. package/src/sources/provider-router/index.test.ts +189 -0
  163. package/src/sources/provider-router/primary.ts +3 -2
  164. package/src/sources/sec-edgar.test.ts +83 -0
  165. package/src/sources/sec-edgar.ts +231 -0
  166. package/src/sources/yahoo-finance.test.ts +51 -0
  167. package/src/sources/yahoo-finance.ts +49 -1
  168. package/src/state/app/context/index.test.tsx +5 -8
  169. package/src/types/broker.ts +1 -0
  170. package/src/types/config.ts +10 -0
  171. package/src/types/trading.ts +2 -0
  172. package/src/ui/host.tsx +16 -0
  173. package/src/ui/index.tsx +2 -0
  174. package/src/utils/financial-statements.ts +23 -0
  175. package/src/version.ts +1 -1
  176. package/src/components/onboarding/portfolio-step/manual-name-panel.tsx +0 -62
@@ -84,6 +84,7 @@ export function WebTextField({
84
84
  inputRef,
85
85
  onChange,
86
86
  onSubmit,
87
+ onBlur,
87
88
  hint,
88
89
  type = "text",
89
90
  variant = "default",
@@ -155,7 +156,12 @@ export function WebTextField({
155
156
  }}
156
157
  onInput={onChange}
157
158
  onChange={onChange}
158
- onSubmit={() => onSubmit?.(value ?? resolvedInputRef.current?.editBuffer.getText() ?? "")}
159
+ onSubmit={(nextValue?: string) => onSubmit?.(
160
+ typeof nextValue === "string" ? nextValue : resolvedInputRef.current?.editBuffer.getText() ?? value ?? "",
161
+ )}
162
+ onBlur={(nextValue?: string) => onBlur?.(
163
+ typeof nextValue === "string" ? nextValue : resolvedInputRef.current?.editBuffer.getText() ?? value ?? "",
164
+ )}
159
165
  />
160
166
  </Box>
161
167
  {hint && (
@@ -347,14 +353,14 @@ export function WebPageStackView({
347
353
 
348
354
  if (!detailOpen) {
349
355
  return (
350
- <Box flexDirection="column" flexGrow={1} overflow="hidden">
356
+ <Box flexDirection="column" flexGrow={1} flexShrink={1} flexBasis={0} minWidth={0} minHeight={0} overflow="hidden">
351
357
  {rootContent}
352
358
  </Box>
353
359
  );
354
360
  }
355
361
 
356
362
  return (
357
- <Box flexDirection="column" flexGrow={1} overflow="hidden">
363
+ <Box flexDirection="column" flexGrow={1} flexShrink={1} flexBasis={0} minWidth={0} minHeight={0} overflow="hidden">
358
364
  <Box
359
365
  flexDirection="row"
360
366
  alignItems="flex-start"
@@ -370,7 +376,7 @@ export function WebPageStackView({
370
376
  flexDirection="row"
371
377
  alignItems="center"
372
378
  backgroundColor={panelFill()}
373
- onMouseDown={(event) => {
379
+ onMouseDown={(event: { preventDefault?: () => void; stopPropagation?: () => void }) => {
374
380
  event.preventDefault?.();
375
381
  event.stopPropagation?.();
376
382
  onBack();
@@ -413,7 +419,7 @@ export function WebPageStackView({
413
419
  </Text>
414
420
  ) : null}
415
421
  </Box>
416
- <Box flexDirection="column" flexGrow={1} overflow="hidden">
422
+ <Box flexDirection="column" flexGrow={1} flexShrink={1} flexBasis={0} minWidth={0} minHeight={0} overflow="hidden">
417
423
  {detailContent}
418
424
  </Box>
419
425
  </Box>
@@ -33,6 +33,9 @@ export const WebBox = forwardRef<HTMLDivElement, Record<string, unknown> & { chi
33
33
  const pendingDragRef = useRef<CellMouseEvent | null>(null);
34
34
  const propsRef = useRef(props);
35
35
  propsRef.current = props;
36
+ const hoverBackgroundColor = typeof props.hoverBackgroundColor === "string"
37
+ ? props.hoverBackgroundColor
38
+ : undefined;
36
39
 
37
40
  useImperativeHandle(ref, () => cellBoundsForElement(() => elementRef.current, () => propsRef.current) as unknown as HTMLDivElement, []);
38
41
 
@@ -140,6 +143,7 @@ export const WebBox = forwardRef<HTMLDivElement, Record<string, unknown> & { chi
140
143
  return (
141
144
  <div
142
145
  {...cleanDomProps(props)}
146
+ data-gloom-hover-bg={hoverBackgroundColor ? "true" : undefined}
143
147
  data-gloom-interactive={hasDirectMouseHandler(props) ? "true" : undefined}
144
148
  ref={elementRef}
145
149
  onMouseDown={handleMouseDown}
@@ -149,9 +153,10 @@ export const WebBox = forwardRef<HTMLDivElement, Record<string, unknown> & { chi
149
153
  onWheel={typeof props.onMouseScroll === "function" ? handleWheel : undefined}
150
154
  style={{
151
155
  ...commonStyle(props),
156
+ "--gloom-box-hover-bg": hoverBackgroundColor,
152
157
  ...(props.style as CSSProperties | undefined),
153
158
  ...(props.visible === false ? { display: "none" } : undefined),
154
- }}
159
+ } as CSSProperties}
155
160
  >
156
161
  {children as ReactNode}
157
162
  </div>
@@ -2,12 +2,14 @@
2
2
  /** @jsxImportSource react */
3
3
  import {
4
4
  forwardRef,
5
+ useCallback,
5
6
  useEffect,
6
7
  useImperativeHandle,
7
8
  useRef,
8
9
  useState,
9
10
  type CSSProperties,
10
11
  type KeyboardEvent,
12
+ type RefObject,
11
13
  } from "react";
12
14
  import type { InputRenderable, TextareaRenderable } from "../../../../ui/host";
13
15
  import { WEB_CELL_HEIGHT, WEB_CELL_WIDTH } from "../input-host";
@@ -79,6 +81,65 @@ function useEditableValue(props: Record<string, unknown>) {
79
81
  return { value, valueRef, setValue };
80
82
  }
81
83
 
84
+ function useLatestRef<T>(value: T): RefObject<T> {
85
+ const ref = useRef(value);
86
+ ref.current = value;
87
+ return ref;
88
+ }
89
+
90
+ function useSyncedEditableElement<T extends HTMLInputElement | HTMLTextAreaElement>({
91
+ elementRef,
92
+ propsRef,
93
+ valueRef,
94
+ handleValueChange,
95
+ active,
96
+ }: {
97
+ elementRef: RefObject<T | null>;
98
+ propsRef: RefObject<Record<string, unknown>>;
99
+ valueRef: RefObject<string>;
100
+ handleValueChange: (nextValue: string) => void;
101
+ active: boolean;
102
+ }): () => string {
103
+ const syncElementValue = useCallback(() => {
104
+ const nextValue = elementRef.current?.value;
105
+ if (typeof nextValue !== "string") return valueRef.current;
106
+ if (nextValue !== valueRef.current) {
107
+ handleValueChange(nextValue);
108
+ }
109
+ return nextValue;
110
+ }, [elementRef, handleValueChange, valueRef]);
111
+
112
+ useEffect(() => {
113
+ if (!active) return;
114
+ let animationFrame: number | null = null;
115
+ const syncDomValue = () => {
116
+ syncElementValue();
117
+ animationFrame = globalThis.requestAnimationFrame?.(syncDomValue) ?? null;
118
+ };
119
+ animationFrame = globalThis.requestAnimationFrame?.(syncDomValue) ?? null;
120
+ return () => {
121
+ if (animationFrame !== null) globalThis.cancelAnimationFrame?.(animationFrame);
122
+ };
123
+ }, [active, syncElementValue]);
124
+
125
+ useEffect(() => {
126
+ if (!active) return;
127
+ const commitBeforeOutsideMouseDown = (event: globalThis.MouseEvent) => {
128
+ const element = elementRef.current;
129
+ const target = event.target;
130
+ if (!element || (target instanceof Node && element.contains(target))) return;
131
+ const nextValue = syncElementValue();
132
+ callTextHandler(propsRef.current.onBlur, nextValue);
133
+ };
134
+ document.addEventListener("mousedown", commitBeforeOutsideMouseDown, true);
135
+ return () => {
136
+ document.removeEventListener("mousedown", commitBeforeOutsideMouseDown, true);
137
+ };
138
+ }, [active, elementRef, propsRef, syncElementValue]);
139
+
140
+ return syncElementValue;
141
+ }
142
+
82
143
  type WebVisualCursor = TextareaRenderable["visualCursor"];
83
144
 
84
145
  function textareaColumnCount(props: Record<string, unknown>, element: HTMLTextAreaElement | null): number {
@@ -149,8 +210,10 @@ function textareaMetrics(
149
210
 
150
211
  export const WebInput = forwardRef<InputRenderable, Record<string, unknown>>(function WebInput(props, ref) {
151
212
  const elementRef = useRef<HTMLInputElement | null>(null);
213
+ const propsRef = useLatestRef(props);
152
214
  const { value, valueRef, setValue } = useEditableValue(props);
153
215
  const [cursorOffset, setCursorOffset] = useState(value.length);
216
+ const [domFocused, setDomFocused] = useState(false);
154
217
  const applyCursorOffset = (offset: number) => {
155
218
  setCursorOffset(offset);
156
219
  queueMicrotask(() => applyDomCursorOffset(elementRef.current, offset));
@@ -165,7 +228,7 @@ export const WebInput = forwardRef<InputRenderable, Record<string, unknown>>(fun
165
228
 
166
229
  useImperativeHandle(ref, () => ({
167
230
  editBuffer: {
168
- getText: () => valueRef.current,
231
+ getText: () => elementRef.current?.value ?? valueRef.current,
169
232
  setText: (nextText: string) => setValue(nextText),
170
233
  },
171
234
  get cursorOffset() {
@@ -175,18 +238,25 @@ export const WebInput = forwardRef<InputRenderable, Record<string, unknown>>(fun
175
238
  focus: () => elementRef.current?.focus(),
176
239
  }), [applyCursorOffset, cursorOffset, setValue, valueRef]);
177
240
 
178
- const handleValueChange = (nextValue: string) => {
241
+ const handleValueChange = useCallback((nextValue: string) => {
179
242
  setValue(nextValue);
180
243
  setCursorOffset(elementRef.current?.selectionStart ?? nextValue.length);
181
- callTextHandler(props.onInput, nextValue);
182
- callTextHandler(props.onChange, nextValue);
183
- callTextHandler(props.onCursorChange, nextValue);
184
- };
244
+ callTextHandler(propsRef.current.onInput, nextValue);
245
+ callTextHandler(propsRef.current.onChange, nextValue);
246
+ callTextHandler(propsRef.current.onCursorChange, nextValue);
247
+ }, [propsRef, setValue]);
248
+ const syncElementValue = useSyncedEditableElement({
249
+ elementRef,
250
+ propsRef,
251
+ valueRef,
252
+ handleValueChange,
253
+ active: props.focused === true || domFocused,
254
+ });
185
255
 
186
256
  const handleKeyDown = (event: KeyboardEvent<HTMLInputElement>) => {
187
- if (event.key === "Enter" && typeof props.onSubmit === "function") {
257
+ if (event.key === "Enter" && typeof propsRef.current.onSubmit === "function") {
188
258
  event.preventDefault();
189
- (props.onSubmit as (value: string) => void)(valueRef.current);
259
+ (propsRef.current.onSubmit as (value: string) => void)(syncElementValue());
190
260
  }
191
261
  };
192
262
 
@@ -200,7 +270,16 @@ export const WebInput = forwardRef<InputRenderable, Record<string, unknown>>(fun
200
270
  autoComplete="off"
201
271
  spellCheck={false}
202
272
  placeholder={getStringProp(props, "placeholder")}
273
+ onInput={(event) => handleValueChange(event.currentTarget.value)}
203
274
  onChange={(event) => handleValueChange(event.currentTarget.value)}
275
+ onFocus={() => {
276
+ setDomFocused(true);
277
+ callTextHandler(propsRef.current.onFocus, elementRef.current?.value ?? valueRef.current);
278
+ }}
279
+ onBlur={() => {
280
+ setDomFocused(false);
281
+ callTextHandler(propsRef.current.onBlur, syncElementValue());
282
+ }}
204
283
  onKeyDown={handleKeyDown}
205
284
  onContextMenu={(event) => {
206
285
  if (!NATIVE_CONTEXT_MENU_SUPPORTED) return;
@@ -211,7 +290,7 @@ export const WebInput = forwardRef<InputRenderable, Record<string, unknown>>(fun
211
290
  }}
212
291
  onSelect={() => {
213
292
  setCursorOffset(elementRef.current?.selectionStart ?? valueRef.current.length);
214
- callTextHandler(props.onCursorChange, valueRef.current);
293
+ callTextHandler(propsRef.current.onCursorChange, valueRef.current);
215
294
  }}
216
295
  style={textInputStyle(props, false)}
217
296
  />
@@ -220,8 +299,10 @@ export const WebInput = forwardRef<InputRenderable, Record<string, unknown>>(fun
220
299
 
221
300
  export const WebTextarea = forwardRef<TextareaRenderable, Record<string, unknown>>(function WebTextarea(props, ref) {
222
301
  const elementRef = useRef<HTMLTextAreaElement | null>(null);
302
+ const propsRef = useLatestRef(props);
223
303
  const { value, valueRef, setValue } = useEditableValue(props);
224
304
  const [cursorOffset, setCursorOffset] = useState(value.length);
305
+ const [domFocused, setDomFocused] = useState(false);
225
306
  const applyCursorOffset = (offset: number) => {
226
307
  setCursorOffset(offset);
227
308
  queueMicrotask(() => applyDomCursorOffset(elementRef.current, offset));
@@ -236,7 +317,7 @@ export const WebTextarea = forwardRef<TextareaRenderable, Record<string, unknown
236
317
 
237
318
  useImperativeHandle(ref, () => ({
238
319
  editBuffer: {
239
- getText: () => valueRef.current,
320
+ getText: () => elementRef.current?.value ?? valueRef.current,
240
321
  setText: (nextText: string) => setValue(nextText),
241
322
  },
242
323
  get cursorOffset() {
@@ -271,18 +352,25 @@ export const WebTextarea = forwardRef<TextareaRenderable, Record<string, unknown
271
352
  clearLineHighlights: () => {},
272
353
  }), [applyCursorOffset, cursorOffset, props, setValue, valueRef]);
273
354
 
274
- const handleValueChange = (nextValue: string) => {
355
+ const handleValueChange = useCallback((nextValue: string) => {
275
356
  setValue(nextValue);
276
357
  setCursorOffset(elementRef.current?.selectionStart ?? nextValue.length);
277
- callTextHandler(props.onInput, nextValue);
278
- callTextHandler(props.onChange, nextValue);
279
- callTextHandler(props.onCursorChange, nextValue);
280
- };
358
+ callTextHandler(propsRef.current.onInput, nextValue);
359
+ callTextHandler(propsRef.current.onChange, nextValue);
360
+ callTextHandler(propsRef.current.onCursorChange, nextValue);
361
+ }, [propsRef, setValue]);
362
+ const syncElementValue = useSyncedEditableElement({
363
+ elementRef,
364
+ propsRef,
365
+ valueRef,
366
+ handleValueChange,
367
+ active: props.focused === true || domFocused,
368
+ });
281
369
 
282
370
  const handleKeyDown = (event: KeyboardEvent<HTMLTextAreaElement>) => {
283
- if (event.key === "Enter" && !event.shiftKey && typeof props.onSubmit === "function") {
371
+ if (event.key === "Enter" && !event.shiftKey && typeof propsRef.current.onSubmit === "function") {
284
372
  event.preventDefault();
285
- (props.onSubmit as (value: string) => void)(valueRef.current);
373
+ (propsRef.current.onSubmit as (value: string) => void)(syncElementValue());
286
374
  }
287
375
  };
288
376
 
@@ -296,7 +384,16 @@ export const WebTextarea = forwardRef<TextareaRenderable, Record<string, unknown
296
384
  autoComplete="off"
297
385
  spellCheck={false}
298
386
  placeholder={getStringProp(props, "placeholder")}
387
+ onInput={(event) => handleValueChange(event.currentTarget.value)}
299
388
  onChange={(event) => handleValueChange(event.currentTarget.value)}
389
+ onFocus={() => {
390
+ setDomFocused(true);
391
+ callTextHandler(propsRef.current.onFocus, elementRef.current?.value ?? valueRef.current);
392
+ }}
393
+ onBlur={() => {
394
+ setDomFocused(false);
395
+ callTextHandler(propsRef.current.onBlur, syncElementValue());
396
+ }}
300
397
  onKeyDown={handleKeyDown}
301
398
  onContextMenu={(event) => {
302
399
  if (!NATIVE_CONTEXT_MENU_SUPPORTED) return;
@@ -307,7 +404,7 @@ export const WebTextarea = forwardRef<TextareaRenderable, Record<string, unknown
307
404
  }}
308
405
  onSelect={() => {
309
406
  setCursorOffset(elementRef.current?.selectionStart ?? valueRef.current.length);
310
- callTextHandler(props.onCursorChange, valueRef.current);
407
+ callTextHandler(propsRef.current.onCursorChange, valueRef.current);
311
408
  }}
312
409
  style={textInputStyle(props, true)}
313
410
  />
@@ -104,6 +104,7 @@ export function cleanDomProps(props: Record<string, unknown>): Record<string, un
104
104
  "cursorColor", "selectionBg", "selectionFg", "showCursor", "keyBindings", "wrapText", "wrapMode",
105
105
  "initialValue", "value", "onInput", "onChange", "onSubmit", "onCursorChange", "onMouse",
106
106
  "scrollX", "scrollY", "focusable", "bitmap", "bitmaps", "crosshair", "text", "font",
107
+ "hoverBackgroundColor",
107
108
  ]) {
108
109
  delete next[key];
109
110
  }
@@ -21,6 +21,7 @@ export function WebTabs({
21
21
  closeMode = "always",
22
22
  addLabel = "+",
23
23
  onAdd,
24
+ focused = false,
24
25
  palette,
25
26
  }: HostTabsProps) {
26
27
  const activeTabRef = useRef<HTMLButtonElement | null>(null);
@@ -112,16 +113,20 @@ export function WebTabs({
112
113
  paddingBlock: 0,
113
114
  paddingBottom: showUnderline ? 2 : 0,
114
115
  margin: 0,
115
- border: 0,
116
+ border: "1px solid transparent",
116
117
  borderRadius: variant === "underline" ? 5 : 6,
117
118
  background: resolveTabBackground(active, hovered),
119
+ boxSizing: "border-box",
118
120
  font: "inherit",
119
121
  fontSize: tabFontSize,
120
122
  fontWeight: active ? 700 : 500,
121
123
  lineHeight: 1,
122
124
  textAlign: "center",
123
125
  whiteSpace: "nowrap",
124
- transition: "background-color 110ms ease, color 110ms ease, box-shadow 110ms ease",
126
+ borderColor: active && focused && variant !== "pill"
127
+ ? `color-mix(in srgb, ${palette.activeUnderline} 35%, transparent)`
128
+ : "transparent",
129
+ transition: "background-color 110ms ease, border-color 110ms ease, color 110ms ease",
125
130
  cursor: disabled ? "default" : "pointer",
126
131
  } satisfies CssVars;
127
132
 
@@ -20,6 +20,13 @@ body {
20
20
  input, textarea, button { font: inherit; }
21
21
  input, textarea { cursor: text; user-select: text; }
22
22
  button, [data-gloom-interactive="true"] { cursor: pointer; }
23
+ [data-gloom-hover-bg="true"]:hover {
24
+ background-color: var(--gloom-box-hover-bg) !important;
25
+ }
26
+ [data-gloom-role="data-table-row"]:not([data-selected="true"]):hover,
27
+ [data-gloom-role="data-table-row"]:not([data-selected="true"]):hover > [data-gloom-role="data-table-cell"] {
28
+ background-color: var(--gloom-hover-bg) !important;
29
+ }
23
30
  [data-gloom-role="pane-window"] {
24
31
  background-clip: padding-box;
25
32
  isolation: isolate;
@@ -170,6 +177,17 @@ body.gloom-dragging {
170
177
  [data-gloom-role="status-bar"] span {
171
178
  line-height: var(--cell-h);
172
179
  }
180
+ [data-gloom-role="pane-body"] {
181
+ overflow: hidden;
182
+ }
183
+ [data-gloom-role="pane-content"] {
184
+ overflow: hidden;
185
+ }
186
+ [data-gloom-role="pane-content"] > * {
187
+ min-width: 0;
188
+ min-height: 0;
189
+ max-height: 100%;
190
+ }
173
191
  [data-gloom-role="pane-footer"] {
174
192
  flex: 0 0 var(--cell-h);
175
193
  height: var(--cell-h);
@@ -138,8 +138,14 @@ export async function createOpenTuiHost(): Promise<OpenTuiHost> {
138
138
  get isDestroyed() {
139
139
  return renderer.isDestroyed;
140
140
  },
141
+ get currentFocusedRenderable() {
142
+ return renderer.currentFocusedRenderable ?? null;
143
+ },
144
+ get currentFocusedEditor() {
145
+ return renderer.currentFocusedEditor ?? null;
146
+ },
141
147
  get keyInput() {
142
- return renderer.keyInput;
148
+ return renderer.keyInput as unknown as NativeRendererHost["keyInput"];
143
149
  },
144
150
  on: (event, handler) => renderer.on(event, handler),
145
151
  off: (event, handler) => renderer.off(event, handler),
@@ -147,7 +153,17 @@ export async function createOpenTuiHost(): Promise<OpenTuiHost> {
147
153
  registerLifecyclePass: (renderable) => renderer.registerLifecyclePass(renderable as any),
148
154
  unregisterLifecyclePass: (renderable) => renderer.unregisterLifecyclePass(renderable as any),
149
155
  getSelection: () => renderer.getSelection(),
156
+ getCursorState: () => renderer.getCursorState(),
157
+ setCursorPosition: (x, y, visible = true) => renderer.setCursorPosition(x, y, visible),
158
+ addPostProcessFn: (processFn) => renderer.addPostProcessFn(processFn as any),
159
+ removePostProcessFn: (processFn) => renderer.removePostProcessFn(processFn as any),
150
160
  copyToClipboardOSC52: (text) => renderer.copyToClipboardOSC52(text),
161
+ captureMouseRenderable(renderable) {
162
+ const capture = (renderer as unknown as { setCapturedRenderable?: (target: unknown) => void }).setCapturedRenderable;
163
+ if (typeof capture === "function") {
164
+ capture.call(renderer, renderable);
165
+ }
166
+ },
151
167
  write(data) {
152
168
  if (renderer.isDestroyed) return false;
153
169
  const writer = (renderer as unknown as { writeOut?: (payload: string | Uint8Array) => void }).writeOut;
@@ -0,0 +1 @@
1
+ import "@opentui/core";
@@ -1,11 +1,25 @@
1
1
  import { RGBA, StyledText as OpenTuiStyledText, SyntaxStyle, TextAttributes as OpenTuiTextAttributes } from "@opentui/core";
2
- import { createElement } from "react";
2
+ import { createElement, forwardRef, type ReactNode } from "react";
3
3
  import "opentui-spinner/react";
4
4
  import { TextAttributes, type UiHost, type TextProps } from "../../ui/host";
5
5
  import { renderAsciiText } from "../../ui/ascii-font";
6
6
  import { OpenTuiImageSurface } from "./image/surface";
7
7
  import { OpenTuiChartSurface } from "./chart-surface";
8
8
 
9
+ interface OpenTuiPrimitiveProps {
10
+ children?: ReactNode;
11
+ [key: string]: unknown;
12
+ }
13
+
14
+ function createOpenTuiPrimitive(tagName: string) {
15
+ return forwardRef<unknown, OpenTuiPrimitiveProps>(function OpenTuiPrimitive(
16
+ { children, ...props },
17
+ ref,
18
+ ) {
19
+ return createElement(tagName as any, { ...props, ref }, children as ReactNode);
20
+ });
21
+ }
22
+
9
23
  function mapTextAttributes(appAttributes: number | undefined, props?: TextProps): number | undefined {
10
24
  const flags = typeof appAttributes === "number" ? appAttributes : 0;
11
25
  let attributes = 0;
@@ -37,41 +51,59 @@ function mapTextContent(content: unknown): unknown {
37
51
  fg: mapColor(chunk.fg),
38
52
  bg: mapColor(chunk.bg),
39
53
  attributes: mapTextAttributes(chunk.attributes as number | undefined),
40
- })));
54
+ })) as any);
41
55
  }
42
56
  return content;
43
57
  }
44
58
 
59
+ const OpenTuiBox = createOpenTuiPrimitive("box");
60
+ const OpenTuiScrollBox = createOpenTuiPrimitive("scrollbox");
61
+ const OpenTuiInput = createOpenTuiPrimitive("input");
62
+ const OpenTuiTextarea = createOpenTuiPrimitive("textarea");
63
+ const OpenTuiSpinnerMark = createOpenTuiPrimitive("spinner");
64
+
65
+ const OpenTuiText = forwardRef<unknown, TextProps>(function OpenTuiText({ children, ...props }, ref) {
66
+ const textProps = stripTextProps(props);
67
+ return createElement("text" as any, {
68
+ ...textProps,
69
+ ref,
70
+ content: mapTextContent(textProps.content),
71
+ attributes: mapTextAttributes(props.attributes as number | undefined, props),
72
+ }, children as ReactNode);
73
+ });
74
+
75
+ const OpenTuiSpan = forwardRef<unknown, TextProps>(function OpenTuiSpan({ children, ...props }, ref) {
76
+ return createElement("span" as any, {
77
+ ...stripTextProps(props),
78
+ ref,
79
+ attributes: mapTextAttributes(props.attributes as number | undefined, props),
80
+ }, children as ReactNode);
81
+ });
82
+
83
+ const OpenTuiStrong = forwardRef<unknown, TextProps>(function OpenTuiStrong({ children, ...props }, ref) {
84
+ return createElement("strong" as any, { ...stripTextProps(props), ref }, children as ReactNode);
85
+ });
86
+
87
+ const OpenTuiUnderline = forwardRef<unknown, TextProps>(function OpenTuiUnderline({ children, ...props }, ref) {
88
+ return createElement("u" as any, { ...stripTextProps(props), ref }, children as ReactNode);
89
+ });
90
+
45
91
  export const openTuiUiHost: UiHost = {
46
92
  kind: "opentui",
47
93
  capabilities: {
48
94
  nativeCharts: true,
49
95
  },
50
- Box: ({ children, ...props }) => <box {...props}>{children}</box>,
51
- Text: ({ children, ...props }) => {
52
- const textProps = stripTextProps(props);
53
- return (
54
- <text
55
- {...textProps}
56
- content={mapTextContent(textProps.content)}
57
- attributes={mapTextAttributes(props.attributes as number | undefined, props)}
58
- >
59
- {children}
60
- </text>
61
- );
62
- },
63
- Span: ({ children, ...props }) => createElement("span" as any, {
64
- ...stripTextProps(props),
65
- attributes: mapTextAttributes(props.attributes as number | undefined, props),
66
- }, children),
67
- Strong: ({ children, ...props }) => createElement("strong" as any, stripTextProps(props), children),
68
- Underline: ({ children, ...props }) => createElement("u" as any, stripTextProps(props), children),
69
- ScrollBox: ({ children, ...props }) => <scrollbox {...props}>{children}</scrollbox>,
70
- Input: ({ children, ...props }) => createElement("input" as any, props, children),
71
- Textarea: ({ children, ...props }) => createElement("textarea" as any, props, children),
96
+ Box: OpenTuiBox as UiHost["Box"],
97
+ Text: OpenTuiText as UiHost["Text"],
98
+ Span: OpenTuiSpan as UiHost["Span"],
99
+ Strong: OpenTuiStrong as UiHost["Strong"],
100
+ Underline: OpenTuiUnderline as UiHost["Underline"],
101
+ ScrollBox: OpenTuiScrollBox as UiHost["ScrollBox"],
102
+ Input: OpenTuiInput as UiHost["Input"],
103
+ Textarea: OpenTuiTextarea as UiHost["Textarea"],
72
104
  ChartSurface: OpenTuiChartSurface,
73
105
  ImageSurface: OpenTuiImageSurface,
74
- SpinnerMark: ({ children, ...props }) => createElement("spinner" as any, props, children),
106
+ SpinnerMark: OpenTuiSpinnerMark as UiHost["SpinnerMark"],
75
107
  AsciiText: ({ text, font = "tiny", color, fg, bg, backgroundColor, selectable = false, ...props }) => {
76
108
  const resolvedColor = color ?? fg;
77
109
  const resolvedBackground = backgroundColor ?? bg;
@@ -11,7 +11,12 @@ import { normalizeTickerFinancialsPriceHistory } from "../../utils/price-history
11
11
  import { isQuoteStaleForCurrentSession } from "../../market-data/quotes/freshness";
12
12
  import { resolveTickerFinancialsQuoteState } from "../../market-data/quotes/resolution";
13
13
  import { selectCachedResource } from "./cache";
14
- import { quoteWithFreshnessExchange, type CachedFinancialsSelection } from "./financials";
14
+ import {
15
+ hasDeepStatementHistory,
16
+ hasDetailedStatementRows,
17
+ quoteWithFreshnessExchange,
18
+ type CachedFinancialsSelection,
19
+ } from "./financials";
15
20
  import type { ProviderRouterCoreDeps } from "./route-types";
16
21
 
17
22
  export interface ProviderRouterBatchDeps extends ProviderRouterCoreDeps {
@@ -31,6 +36,10 @@ export interface ProviderRouterBatchDeps extends ProviderRouterCoreDeps {
31
36
  export class ProviderRouterBatchRoutes {
32
37
  constructor(private readonly deps: ProviderRouterBatchDeps) {}
33
38
 
39
+ private needsSingleFinancialsRoute(value: TickerFinancials): boolean {
40
+ return !(hasDetailedStatementRows(value) && hasDeepStatementHistory(value));
41
+ }
42
+
34
43
  async getQuotesBatch(
35
44
  targets: QuoteSubscriptionTarget[],
36
45
  options: { forceRefresh?: boolean } = {},
@@ -105,6 +114,7 @@ export class ProviderRouterBatchRoutes {
105
114
  ): Promise<TickerFinancialsBatchResult[]> {
106
115
  const forceRefresh = options.forceRefresh === true;
107
116
  const results = new Array<TickerFinancialsBatchResult | null>(targets.length).fill(null);
117
+ const batchFallbacks = new Array<TickerFinancials | null>(targets.length).fill(null);
108
118
  const misses: Array<{ index: number; target: CachedFinancialsTarget }> = [];
109
119
 
110
120
  targets.forEach((target, index) => {
@@ -139,6 +149,8 @@ export class ProviderRouterBatchRoutes {
139
149
  const entityKey = this.deps.getEntityKey(entry.target.symbol, entry.target.instrument ?? undefined);
140
150
  const variantKey = this.deps.getTickerVariantCandidates(entry.target.exchange)[0] ?? "";
141
151
  this.deps.cacheResource("financials", entityKey, variantKey, sourceKey, value, this.deps.resolveProviderPolicy("financials", batchProvider));
152
+ batchFallbacks[entry.index] = value;
153
+ if (this.needsSingleFinancialsRoute(value)) continue;
142
154
  results[entry.index] = { target: entry.target, financials: value };
143
155
  }
144
156
  }
@@ -153,7 +165,9 @@ export class ProviderRouterBatchRoutes {
153
165
  });
154
166
  results[index] = { target, financials };
155
167
  } catch (error) {
156
- results[index] = { target, financials: null, error };
168
+ results[index] = batchFallbacks[index]
169
+ ? { target, financials: batchFallbacks[index] }
170
+ : { target, financials: null, error };
157
171
  }
158
172
  }));
159
173
 
@@ -19,6 +19,7 @@ import { withBrokerTimeout } from "./brokers";
19
19
  import {
20
20
  deriveMarketCapFromShares,
21
21
  hasMeaningfulProfile,
22
+ hasShallowStatementHistory,
22
23
  mergeCachedFinancialRecords,
23
24
  mergeFinancials,
24
25
  quoteWithFreshnessExchange,
@@ -86,6 +87,10 @@ export class ProviderRouterFinancialRoutes {
86
87
  if (isOptionTicker && !cached.value.quote) {
87
88
  return quoteOnlyFinancials(cached.value);
88
89
  }
90
+ if (!cached.stale && hasMeaningfulProfile(cached.value) && hasShallowStatementHistory(cached.value)) {
91
+ const providerResult = await this.deps.primaryRoutes.fetchProviderFinancials(ticker, exchange, context);
92
+ return mergeFinancials(cached.value, providerResult?.value ?? null) ?? cached.value;
93
+ }
89
94
  if (!cached.stale && hasMeaningfulProfile(cached.value)) {
90
95
  return cached.value;
91
96
  }