gloomberb 0.6.0 → 0.6.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gloomberb",
3
- "version": "0.6.0",
3
+ "version": "0.6.1",
4
4
  "description": "Bloomberg-style stock portfolio tracker for the terminal",
5
5
  "module": "src/index.tsx",
6
6
  "type": "module",
@@ -53,6 +53,7 @@
53
53
  "@opentui/react": "^0.1.90",
54
54
  "@stoqey/ib": "^1.5.3",
55
55
  "@tanstack/react-virtual": "^3.13.23",
56
+ "jimp": "1.6.0",
56
57
  "opentui-spinner": "^0.0.6",
57
58
  "react": "19.2.5",
58
59
  "rxjs": "^7.8.2"
@@ -82,7 +82,6 @@ import {
82
82
  } from "./chart-types";
83
83
  import {
84
84
  computeBitmapSize,
85
- intersectCellRects,
86
85
  renderNativeComparisonChartBase,
87
86
  renderNativeCrosshairOverlay,
88
87
  type CellRect,
@@ -93,6 +92,11 @@ import { ensureKittySupport, getCachedKittySupport } from "./native/kitty-suppor
93
92
  import { resolveChartRendererState } from "./native/renderer-selection";
94
93
  import { getNativeSurfaceManager } from "./native/surface-manager";
95
94
  import { syncCachedNativeSurface } from "./native/surface-sync";
95
+ import {
96
+ getRenderableCellRect,
97
+ resolveNativeSurfaceVisibleRect,
98
+ type NativeSurfaceRenderableNode,
99
+ } from "./native/surface-visibility";
96
100
  import { formatAxisCell, formatDateShort, resolveChartAxisWidth, type StyledContent } from "./chart-renderer";
97
101
 
98
102
  const MODE_CHIPS: Record<ComparisonChartRenderMode, string> = {
@@ -169,14 +173,6 @@ interface DisplayCursorState {
169
173
  pixelY: number | null;
170
174
  }
171
175
 
172
- interface RenderableNode {
173
- x: number;
174
- y: number;
175
- width: number;
176
- height: number;
177
- parent: RenderableNode | null;
178
- }
179
-
180
176
  const EMPTY_DISPLAY_CURSOR: DisplayCursorState = {
181
177
  cellX: null,
182
178
  cellY: null,
@@ -384,41 +380,6 @@ function resolveCursorMotionKind(
384
380
  : "cell";
385
381
  }
386
382
 
387
- function extractCellRect(renderable: RenderableNode): CellRect {
388
- return {
389
- x: renderable.x,
390
- y: renderable.y,
391
- width: renderable.width,
392
- height: renderable.height,
393
- };
394
- }
395
-
396
- function resolveVisibleRect(
397
- renderable: RenderableNode | null,
398
- terminalWidth: number,
399
- terminalHeight: number,
400
- ): CellRect | null {
401
- if (!renderable) return null;
402
-
403
- let visible: CellRect = {
404
- x: 0,
405
- y: 0,
406
- width: terminalWidth,
407
- height: terminalHeight,
408
- };
409
- let current: RenderableNode | null = renderable;
410
-
411
- while (current) {
412
- const currentRect = extractCellRect(current);
413
- const nextVisible = intersectCellRects(visible, currentRect);
414
- if (!nextVisible) return null;
415
- visible = nextVisible;
416
- current = current.parent;
417
- }
418
-
419
- return visible;
420
- }
421
-
422
383
  function buildBlankPlotLines(width: number, height: number): string[] {
423
384
  return Array.from({ length: height }, () => " ".repeat(width));
424
385
  }
@@ -1201,8 +1162,9 @@ function ComparisonStockChartView({
1201
1162
 
1202
1163
  const syncPlacement = () => {
1203
1164
  if (effectiveRenderer !== "kitty" || !rendererState.nativeReady || !plotRef.current) return;
1204
- const rect = extractCellRect(plotRef.current);
1205
- const visibleRect = resolveVisibleRect(plotRef.current, renderer.terminalWidth, renderer.terminalHeight);
1165
+ const plot = plotRef.current as NativeSurfaceRenderableNode;
1166
+ const rect = getRenderableCellRect(plot);
1167
+ const visibleRect = resolveNativeSurfaceVisibleRect(plot, renderer.terminalWidth, renderer.terminalHeight);
1206
1168
  const previous = lastNativeGeometryRef.current;
1207
1169
  if (previous
1208
1170
  && previous.rect.x === rect.x
@@ -1257,8 +1219,9 @@ function ComparisonStockChartView({
1257
1219
  return;
1258
1220
  }
1259
1221
 
1260
- const plotRect = extractCellRect(plotRef.current);
1261
- const visibleRect = resolveVisibleRect(plotRef.current, renderer.terminalWidth, renderer.terminalHeight);
1222
+ const plot = plotRef.current as NativeSurfaceRenderableNode;
1223
+ const plotRect = getRenderableCellRect(plot);
1224
+ const visibleRect = resolveNativeSurfaceVisibleRect(plot, renderer.terminalWidth, renderer.terminalHeight);
1262
1225
  const bitmapSize = computeBitmapSize(plotRect, renderer.resolution, renderer.terminalWidth, renderer.terminalHeight);
1263
1226
  const bitmapKey = buildComparisonNativeBitmapKey(
1264
1227
  symbols.length,
@@ -1314,8 +1277,9 @@ function ComparisonStockChartView({
1314
1277
  return;
1315
1278
  }
1316
1279
 
1317
- const plotRect = extractCellRect(plotRef.current);
1318
- const visibleRect = resolveVisibleRect(plotRef.current, renderer.terminalWidth, renderer.terminalHeight);
1280
+ const plot = plotRef.current as NativeSurfaceRenderableNode;
1281
+ const plotRect = getRenderableCellRect(plot);
1282
+ const visibleRect = resolveNativeSurfaceVisibleRect(plot, renderer.terminalWidth, renderer.terminalHeight);
1319
1283
  const bitmapSize = computeBitmapSize(plotRect, renderer.resolution, renderer.terminalWidth, renderer.terminalHeight);
1320
1284
  const renderablePixelSize = getRenderablePixelSize(plotRef.current, renderer);
1321
1285
  const overlayPixelX = scaleLocalPixelCoordinate(
@@ -17,6 +17,7 @@ import { chunkBase64Payload, encodeKittyTransmitRgba } from "./kitty-protocol";
17
17
  import { resolveChartRendererState } from "./renderer-selection";
18
18
  import { computeSurfaceVisibleFragments, NativeSurfaceManager } from "./surface-manager";
19
19
  import { syncCachedNativeSurface } from "./surface-sync";
20
+ import { resolveNativeSurfaceVisibleRect, type NativeSurfaceRenderableNode } from "./surface-visibility";
20
21
 
21
22
  describe("resolveChartRendererState", () => {
22
23
  test("resolves auto and forced kitty correctly", () => {
@@ -98,6 +99,28 @@ describe("computeNativePlacement", () => {
98
99
  });
99
100
  });
100
101
 
102
+ describe("resolveNativeSurfaceVisibleRect", () => {
103
+ test("treats hidden ancestors as invisible for native graphics", () => {
104
+ const hiddenParent: NativeSurfaceRenderableNode = {
105
+ x: 0,
106
+ y: 0,
107
+ width: 80,
108
+ height: 20,
109
+ visible: false,
110
+ parent: null,
111
+ };
112
+ const child: NativeSurfaceRenderableNode = {
113
+ x: 2,
114
+ y: 3,
115
+ width: 20,
116
+ height: 8,
117
+ parent: hiddenParent,
118
+ };
119
+
120
+ expect(resolveNativeSurfaceVisibleRect(child, 100, 30)).toBeNull();
121
+ });
122
+ });
123
+
101
124
  describe("renderNativeChartBase", () => {
102
125
  test("leaves untouched bitmap pixels transparent", () => {
103
126
  const palette = resolveChartPalette({
@@ -0,0 +1,48 @@
1
+ import { intersectCellRects, type CellRect } from "./chart-rasterizer";
2
+
3
+ export interface NativeSurfaceRenderableNode {
4
+ x: number;
5
+ y: number;
6
+ width: number;
7
+ height: number;
8
+ visible?: boolean;
9
+ parent: NativeSurfaceRenderableNode | null;
10
+ }
11
+
12
+ export function getRenderableCellRect(
13
+ renderable: Pick<NativeSurfaceRenderableNode, "x" | "y" | "width" | "height">,
14
+ ): CellRect {
15
+ return {
16
+ x: renderable.x,
17
+ y: renderable.y,
18
+ width: renderable.width,
19
+ height: renderable.height,
20
+ };
21
+ }
22
+
23
+ export function resolveNativeSurfaceVisibleRect(
24
+ renderable: NativeSurfaceRenderableNode | null,
25
+ terminalWidth: number,
26
+ terminalHeight: number,
27
+ ): CellRect | null {
28
+ if (!renderable) return null;
29
+
30
+ let visible: CellRect = {
31
+ x: 0,
32
+ y: 0,
33
+ width: terminalWidth,
34
+ height: terminalHeight,
35
+ };
36
+ let current: NativeSurfaceRenderableNode | null = renderable;
37
+
38
+ while (current) {
39
+ if (current.visible === false) return null;
40
+
41
+ const nextVisible = intersectCellRects(visible, getRenderableCellRect(current));
42
+ if (!nextVisible) return null;
43
+ visible = nextVisible;
44
+ current = current.parent;
45
+ }
46
+
47
+ return visible;
48
+ }
@@ -110,7 +110,6 @@ import {
110
110
  } from "./chart-types";
111
111
  import {
112
112
  computeBitmapSize,
113
- intersectCellRects,
114
113
  renderNativeChartBase,
115
114
  renderNativeCrosshairOverlay,
116
115
  type CellRect,
@@ -121,6 +120,11 @@ import { ensureKittySupport, getCachedKittySupport } from "./native/kitty-suppor
121
120
  import { resolveChartRendererState } from "./native/renderer-selection";
122
121
  import { getNativeSurfaceManager } from "./native/surface-manager";
123
122
  import { syncCachedNativeSurface } from "./native/surface-sync";
123
+ import {
124
+ getRenderableCellRect,
125
+ resolveNativeSurfaceVisibleRect,
126
+ type NativeSurfaceRenderableNode,
127
+ } from "./native/surface-visibility";
124
128
  import type { PricePoint, TickerFinancials } from "../../types/financials";
125
129
  import type { TickerRecord } from "../../types/ticker";
126
130
 
@@ -275,14 +279,6 @@ interface DisplayCursorState {
275
279
  pixelY: number | null;
276
280
  }
277
281
 
278
- interface RenderableNode {
279
- x: number;
280
- y: number;
281
- width: number;
282
- height: number;
283
- parent: RenderableNode | null;
284
- }
285
-
286
282
  const EMPTY_DISPLAY_CURSOR: DisplayCursorState = {
287
283
  cellX: null,
288
284
  cellY: null,
@@ -522,41 +518,6 @@ function resolveCursorMotionKind(
522
518
  : "cell";
523
519
  }
524
520
 
525
- function extractCellRect(renderable: RenderableNode): CellRect {
526
- return {
527
- x: renderable.x,
528
- y: renderable.y,
529
- width: renderable.width,
530
- height: renderable.height,
531
- };
532
- }
533
-
534
- function resolveVisibleRect(
535
- renderable: RenderableNode | null,
536
- terminalWidth: number,
537
- terminalHeight: number,
538
- ): CellRect | null {
539
- if (!renderable) return null;
540
-
541
- let visible: CellRect = {
542
- x: 0,
543
- y: 0,
544
- width: terminalWidth,
545
- height: terminalHeight,
546
- };
547
- let current: RenderableNode | null = renderable;
548
-
549
- while (current) {
550
- const currentRect = extractCellRect(current);
551
- const nextVisible = intersectCellRects(visible, currentRect);
552
- if (!nextVisible) return null;
553
- visible = nextVisible;
554
- current = current.parent;
555
- }
556
-
557
- return visible;
558
- }
559
-
560
521
  function buildBlankPlotLines(width: number, height: number): string[] {
561
522
  return Array.from({ length: height }, () => " ".repeat(width));
562
523
  }
@@ -3236,8 +3197,9 @@ export const ResolvedStockChart = memo(function ResolvedStockChart({
3236
3197
 
3237
3198
  const syncPlacement = () => {
3238
3199
  if (effectiveRenderer !== "kitty" || !rendererState.nativeReady || !plotRef.current) return;
3239
- const rect = extractCellRect(plotRef.current);
3240
- const visibleRect = resolveVisibleRect(plotRef.current, renderer.terminalWidth, renderer.terminalHeight);
3200
+ const plot = plotRef.current as NativeSurfaceRenderableNode;
3201
+ const rect = getRenderableCellRect(plot);
3202
+ const visibleRect = resolveNativeSurfaceVisibleRect(plot, renderer.terminalWidth, renderer.terminalHeight);
3241
3203
  const previous = lastNativeGeometryRef.current;
3242
3204
  if (previous
3243
3205
  && previous.rect.x === rect.x
@@ -3292,8 +3254,9 @@ export const ResolvedStockChart = memo(function ResolvedStockChart({
3292
3254
  return;
3293
3255
  }
3294
3256
 
3295
- const plotRect = extractCellRect(plotRef.current);
3296
- const visibleRect = resolveVisibleRect(plotRef.current, renderer.terminalWidth, renderer.terminalHeight);
3257
+ const plot = plotRef.current as NativeSurfaceRenderableNode;
3258
+ const plotRect = getRenderableCellRect(plot);
3259
+ const visibleRect = resolveNativeSurfaceVisibleRect(plot, renderer.terminalWidth, renderer.terminalHeight);
3297
3260
  const bitmapSize = computeBitmapSize(plotRect, renderer.resolution, renderer.terminalWidth, renderer.terminalHeight);
3298
3261
  const bitmapKey = buildNativeBitmapKey(
3299
3262
  projection.points.length,
@@ -3364,8 +3327,9 @@ export const ResolvedStockChart = memo(function ResolvedStockChart({
3364
3327
  return;
3365
3328
  }
3366
3329
 
3367
- const plotRect = extractCellRect(plotRef.current);
3368
- const visibleRect = resolveVisibleRect(plotRef.current, renderer.terminalWidth, renderer.terminalHeight);
3330
+ const plot = plotRef.current as NativeSurfaceRenderableNode;
3331
+ const plotRect = getRenderableCellRect(plot);
3332
+ const visibleRect = resolveNativeSurfaceVisibleRect(plot, renderer.terminalWidth, renderer.terminalHeight);
3369
3333
  const bitmapSize = computeBitmapSize(plotRect, renderer.resolution, renderer.terminalWidth, renderer.terminalHeight);
3370
3334
  const renderablePixelSize = getRenderablePixelSize(plotRef.current, renderer);
3371
3335
  const overlayPixelX = scaleLocalPixelCoordinate(
@@ -10,7 +10,7 @@ export type { DataTableKeyEvent } from "./data-table-view";
10
10
  export { DataTableStackView } from "./data-table-stack-view";
11
11
  export { FeedDataTableStackView } from "./feed-data-table-stack-view";
12
12
  export type { FeedDataTableItem } from "./feed-data-table-stack-view";
13
- export { usePaneFooter } from "./layout/pane-footer";
13
+ export { PaneFooterScope, usePaneFooter } from "./layout/pane-footer";
14
14
  export type { PaneFooterSegment, PaneHint } from "./layout/pane-footer";
15
15
  export { useExternalLinkFooter } from "./use-external-link-footer";
16
16
  export { Button } from "./ui/button";
@@ -1,5 +1,6 @@
1
1
  import { memo, useCallback } from "react";
2
2
  import { PaneInstanceProvider } from "../../state/app-context";
3
+ import { PaneKeyboardScrollController } from "../../state/pane-scroll-registry";
3
4
  import type { PaneDef } from "../../types/plugin";
4
5
 
5
6
  interface PaneContentProps {
@@ -27,6 +28,7 @@ export const PaneContent = memo(function PaneContent({
27
28
 
28
29
  return (
29
30
  <PaneInstanceProvider paneId={paneId}>
31
+ <PaneKeyboardScrollController paneId={paneId} focused={focused} />
30
32
  <Component
31
33
  paneId={paneId}
32
34
  paneType={paneType}
@@ -158,6 +158,21 @@ export function PaneFooterProvider({
158
158
  );
159
159
  }
160
160
 
161
+ export function PaneFooterScope({
162
+ active,
163
+ children,
164
+ }: {
165
+ active: boolean;
166
+ children: ReactNode;
167
+ }) {
168
+ const context = useContext(PaneFooterContext);
169
+ return (
170
+ <PaneFooterContext.Provider value={active ? context : null}>
171
+ {children}
172
+ </PaneFooterContext.Provider>
173
+ );
174
+ }
175
+
161
176
  export function usePaneFooter(
162
177
  registrationId: string,
163
178
  factory: () => PaneFooterRegistration | null | undefined,
@@ -0,0 +1,226 @@
1
+ import { afterEach, describe, expect, test } from "bun:test";
2
+ import { act, useReducer, type ReactNode } from "react";
3
+ import { testRender } from "../../renderers/opentui/test-utils";
4
+ import { useShortcut } from "../../react/input";
5
+ import { AppContext, appReducer, createInitialState } from "../../state/app-context";
6
+ import { Box, ScrollBox, Text, type ScrollBoxRenderable } from "../../ui";
7
+ import { DataTableStackView } from "../data-table-stack-view";
8
+ import { type DataTableColumn } from "../ui";
9
+ import { createDefaultConfig } from "../../types/config";
10
+ import type { PaneProps } from "../../types/plugin";
11
+ import { PaneContent } from "./pane-content";
12
+
13
+ let testSetup: Awaited<ReturnType<typeof testRender>> | undefined;
14
+ let scrollRef: ScrollBoxRenderable | null = null;
15
+ let hiddenScrollRef: ScrollBoxRenderable | null = null;
16
+
17
+ afterEach(() => {
18
+ if (testSetup) {
19
+ act(() => {
20
+ testSetup!.renderer.destroy();
21
+ });
22
+ testSetup = undefined;
23
+ }
24
+ scrollRef = null;
25
+ hiddenScrollRef = null;
26
+ });
27
+
28
+ function emitDownArrow() {
29
+ return act(async () => {
30
+ testSetup!.mockInput.pressArrow("down");
31
+ await testSetup!.renderOnce();
32
+ });
33
+ }
34
+
35
+ async function renderHarness(component: ReactNode) {
36
+ await act(async () => {
37
+ testSetup = await testRender(component, { width: 30, height: 5 });
38
+ });
39
+ await act(async () => {
40
+ await testSetup!.renderOnce();
41
+ await testSetup!.renderOnce();
42
+ await testSetup!.renderOnce();
43
+ });
44
+ }
45
+
46
+ function LongScrollPane({ width, height }: PaneProps) {
47
+ return (
48
+ <Box width={width} height={height}>
49
+ <ScrollBox
50
+ ref={(node) => {
51
+ scrollRef = node;
52
+ }}
53
+ height={height}
54
+ scrollY
55
+ focusable={false}
56
+ >
57
+ <Box flexDirection="column">
58
+ {Array.from({ length: 20 }, (_, index) => (
59
+ <Box key={index} height={1}>
60
+ <Text>{`Row ${index}`}</Text>
61
+ </Box>
62
+ ))}
63
+ </Box>
64
+ </ScrollBox>
65
+ </Box>
66
+ );
67
+ }
68
+
69
+ function ConsumingScrollPane(props: PaneProps) {
70
+ useShortcut((event) => {
71
+ if (event.name !== "down") return;
72
+ event.preventDefault();
73
+ event.stopPropagation();
74
+ });
75
+ return <LongScrollPane {...props} />;
76
+ }
77
+
78
+ function HiddenMountedTabPane({ width, height }: PaneProps) {
79
+ const renderRows = () => Array.from({ length: 20 }, (_, index) => (
80
+ <Box key={index} height={1}>
81
+ <Text>{`Tab row ${index}`}</Text>
82
+ </Box>
83
+ ));
84
+
85
+ return (
86
+ <Box width={width} height={height}>
87
+ <ScrollBox
88
+ ref={(node) => {
89
+ scrollRef = node;
90
+ }}
91
+ height={height}
92
+ scrollY
93
+ focusable={false}
94
+ >
95
+ <Box flexDirection="column">{renderRows()}</Box>
96
+ </ScrollBox>
97
+ <Box visible={false} height={height}>
98
+ <ScrollBox
99
+ ref={(node) => {
100
+ hiddenScrollRef = node;
101
+ }}
102
+ height={height}
103
+ scrollY
104
+ focusable={false}
105
+ >
106
+ <Box flexDirection="column">{renderRows()}</Box>
107
+ </ScrollBox>
108
+ </Box>
109
+ </Box>
110
+ );
111
+ }
112
+
113
+ interface TestRow {
114
+ id: string;
115
+ }
116
+
117
+ const columns: DataTableColumn[] = [{ id: "label", label: "Label", width: 10, align: "left" }];
118
+ const rows: TestRow[] = [{ id: "row-1" }];
119
+
120
+ function StackDetailPane({ focused, width, height }: PaneProps) {
121
+ const detailContent = (
122
+ <ScrollBox
123
+ ref={(node) => {
124
+ scrollRef = node;
125
+ }}
126
+ flexGrow={1}
127
+ scrollY
128
+ focusable={false}
129
+ >
130
+ <Box flexDirection="column">
131
+ {Array.from({ length: 20 }, (_, index) => (
132
+ <Box key={index} height={1}>
133
+ <Text>{`Tweet detail row ${index}`}</Text>
134
+ </Box>
135
+ ))}
136
+ </Box>
137
+ </ScrollBox>
138
+ );
139
+
140
+ return (
141
+ <DataTableStackView<TestRow>
142
+ focused={focused}
143
+ detailOpen
144
+ onBack={() => {}}
145
+ detailContent={detailContent}
146
+ detailTitle="Tweet"
147
+ selectedIndex={0}
148
+ rootWidth={width}
149
+ rootHeight={height}
150
+ columns={columns}
151
+ items={rows}
152
+ sortColumnId={null}
153
+ sortDirection="asc"
154
+ onHeaderClick={() => {}}
155
+ getItemKey={(row) => row.id}
156
+ isSelected={(_row, index) => index === 0}
157
+ onSelect={() => {}}
158
+ renderCell={(row) => ({ text: row.id })}
159
+ emptyStateTitle="No rows"
160
+ />
161
+ );
162
+ }
163
+
164
+ function Harness({
165
+ component = LongScrollPane,
166
+ focused = true,
167
+ }: {
168
+ component?: (props: PaneProps) => ReactNode;
169
+ focused?: boolean;
170
+ }) {
171
+ const initialState = createInitialState(createDefaultConfig("/tmp/gloomberb-pane-keyboard-scroll-test"));
172
+ const [state, dispatch] = useReducer(appReducer, initialState);
173
+
174
+ return (
175
+ <AppContext value={{ state, dispatch }}>
176
+ <PaneContent
177
+ component={component}
178
+ paneId="test-pane:main"
179
+ paneType="test-pane"
180
+ focused={focused}
181
+ width={30}
182
+ height={5}
183
+ />
184
+ </AppContext>
185
+ );
186
+ }
187
+
188
+ describe("pane keyboard scrolling", () => {
189
+ test("scrolls the focused pane's vertical scrollbox with arrow keys", async () => {
190
+ await renderHarness(<Harness />);
191
+
192
+ expect(scrollRef?.scrollTop).toBe(0);
193
+ await emitDownArrow();
194
+
195
+ expect(scrollRef?.scrollTop).toBe(3);
196
+ });
197
+
198
+ test("does not steal arrows from existing pane shortcut handlers", async () => {
199
+ await renderHarness(<Harness component={ConsumingScrollPane} />);
200
+
201
+ expect(scrollRef?.scrollTop).toBe(0);
202
+ await emitDownArrow();
203
+
204
+ expect(scrollRef?.scrollTop).toBe(0);
205
+ });
206
+
207
+ test("ignores mounted scrollboxes inside hidden tabs", async () => {
208
+ await renderHarness(<Harness component={HiddenMountedTabPane} />);
209
+
210
+ expect(scrollRef?.scrollTop).toBe(0);
211
+ expect(hiddenScrollRef?.scrollTop).toBe(0);
212
+ await emitDownArrow();
213
+
214
+ expect(scrollRef?.scrollTop).toBe(3);
215
+ expect(hiddenScrollRef?.scrollTop).toBe(0);
216
+ });
217
+
218
+ test("scrolls stack detail content without requiring a pane-local detail handler", async () => {
219
+ await renderHarness(<Harness component={StackDetailPane} />);
220
+
221
+ expect(scrollRef?.scrollTop).toBe(0);
222
+ await emitDownArrow();
223
+
224
+ expect(scrollRef?.scrollTop).toBe(3);
225
+ });
226
+ });
@@ -1,4 +1,4 @@
1
- import { Box, ImageSurface, Text, useUiHost } from "../../ui";
1
+ import { Box, ImageSurface, Text } from "../../ui";
2
2
  import { colors } from "../../theme/colors";
3
3
  import { ExternalLinkText } from "./external-link";
4
4
  import { normalizedHttpUrl } from "../../utils/url";
@@ -25,51 +25,26 @@ export function RemoteImage({
25
25
  height = 12,
26
26
  label = "image",
27
27
  }: RemoteImageProps) {
28
- const ui = useUiHost();
29
28
  const resolvedWidth = Math.max(12, width);
30
29
  const imageUrl = normalizedHttpUrl(src);
31
30
  if (!imageUrl) return null;
32
31
 
33
- if (ui.kind === "desktop-web") {
34
- return (
35
- <ImageSurface
36
- src={imageUrl}
37
- alt={alt}
38
- width={resolvedWidth}
39
- height={Math.max(4, height)}
40
- border
41
- borderColor={colors.border}
42
- backgroundColor={colors.panel}
43
- objectFit="contain"
44
- >
45
- <Box paddingX={1} paddingY={1} flexDirection="column" gap={1}>
46
- <Text fg={colors.textDim}>{label}</Text>
47
- <ExternalLinkText
48
- url={imageUrl}
49
- label={truncate(imageUrl, Math.max(1, resolvedWidth - 2))}
50
- color={colors.textBright}
51
- />
52
- </Box>
53
- </ImageSurface>
54
- );
55
- }
56
-
57
32
  return (
58
- <Box
33
+ <ImageSurface
34
+ src={imageUrl}
35
+ alt={alt}
59
36
  width={resolvedWidth}
60
- minHeight={2}
61
- border
62
- borderColor={colors.border}
63
- backgroundColor={colors.panel}
64
- paddingX={1}
65
- flexDirection="column"
37
+ height={Math.max(4, height)}
38
+ objectFit="contain"
66
39
  >
67
- <Text fg={colors.textDim}>{label}</Text>
68
- <ExternalLinkText
69
- url={imageUrl}
70
- label={truncate(imageUrl, Math.max(1, resolvedWidth - 2))}
71
- color={colors.textBright}
72
- />
73
- </Box>
40
+ <Box flexDirection="column" gap={1}>
41
+ <Text fg={colors.textDim}>{label}</Text>
42
+ <ExternalLinkText
43
+ url={imageUrl}
44
+ label={truncate(imageUrl, resolvedWidth)}
45
+ color={colors.textBright}
46
+ />
47
+ </Box>
48
+ </ImageSurface>
74
49
  );
75
50
  }