gloomberb 0.9.4 → 0.9.6

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 (138) hide show
  1. package/README.md +49 -8
  2. package/package.json +4 -3
  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/global-shortcuts.test.tsx +20 -1
  6. package/src/app/global-shortcuts.ts +2 -0
  7. package/src/app/pane-runtime/index.tsx +11 -4
  8. package/src/app/pane-runtime/pane-settings-runtime.tsx +1 -1
  9. package/src/app.tsx +41 -17
  10. package/src/cli/commands/remote.ts +206 -0
  11. package/src/cli/entry.ts +8 -1
  12. package/src/cli/index.ts +2 -0
  13. package/src/cli/native-smoke.ts +5 -0
  14. package/src/components/chart/core/keyboard.ts +10 -1
  15. package/src/components/chart/native/bitmap-support.test.ts +27 -0
  16. package/src/components/chart/native/bitmap-support.ts +55 -0
  17. package/src/components/chart/native/kitty/index.test.ts +47 -0
  18. package/src/components/chart/native/surface/visibility.ts +8 -1
  19. package/src/components/chart/static/bar-chart-surface.test.tsx +4 -0
  20. package/src/components/chart/static/chart/bitmap.ts +16 -9
  21. package/src/components/chart/static/chart/surface.test.tsx +59 -4
  22. package/src/components/chart/static/chart/surface.tsx +4 -0
  23. package/src/components/chart/stock-chart/index.test.ts +7 -0
  24. package/src/components/command-bar/list/view.tsx +48 -1
  25. package/src/components/command-bar/multi-select-picker.tsx +7 -0
  26. package/src/components/command-bar/routing/navigation-state.ts +10 -1
  27. package/src/components/command-bar/surface/index.test.tsx +14 -0
  28. package/src/components/command-bar/theme-picker.tsx +91 -79
  29. package/src/components/command-bar/workflow/field-row.tsx +30 -5
  30. package/src/components/data-table/view.tsx +0 -12
  31. package/src/components/index.ts +7 -0
  32. package/src/components/layout/floating-pane.tsx +3 -3
  33. package/src/components/layout/pane/footer/index.tsx +23 -0
  34. package/src/components/layout/pane/header.tsx +5 -0
  35. package/src/components/layout/pane/index.tsx +3 -3
  36. package/src/components/layout/shell/action-menu-overlay.tsx +1 -1
  37. package/src/components/layout/shell/cursor-occlusion.ts +9 -1
  38. package/src/components/layout/shell/index.test.tsx +2 -0
  39. package/src/components/layout/shell/index.tsx +16 -4
  40. package/src/components/layout/shell/pane/actions.ts +21 -7
  41. package/src/components/layout/shell/pane/layers.tsx +2 -2
  42. package/src/components/layout/status-bar.tsx +5 -5
  43. package/src/components/metric-treemap/index.tsx +317 -0
  44. package/src/components/metric-treemap/layout.test.ts +77 -0
  45. package/src/components/metric-treemap/layout.ts +476 -0
  46. package/src/components/pane-settings-dialog/desktop.tsx +1 -1
  47. package/src/components/price-performance.tsx +1 -1
  48. package/src/components/price-sparkline/view.tsx +15 -14
  49. package/src/components/speedometer-gauge/terminal.tsx +18 -15
  50. package/src/components/table-view-shared.tsx +0 -10
  51. package/src/components/ticker/list-table-view.tsx +1 -7
  52. package/src/components/ticker/list-table.test.tsx +0 -4
  53. package/src/components/toggle-list.tsx +10 -0
  54. package/src/components/ui/button.tsx +12 -0
  55. package/src/components/ui/data-table/index.tsx +51 -0
  56. package/src/components/ui/data-table/opentui/index.tsx +4 -10
  57. package/src/components/ui/data-table/types.ts +6 -4
  58. package/src/components/ui/fields.tsx +18 -0
  59. package/src/components/ui/list-view.tsx +84 -4
  60. package/src/components/ui/message-composer.tsx +22 -0
  61. package/src/components/ui/tabs.tsx +46 -2
  62. package/src/components/ui/ui.test.tsx +0 -9
  63. package/src/core/state/app/layout-reducer.ts +7 -1
  64. package/src/core/state/app/layout.ts +6 -0
  65. package/src/core/state/app/state.ts +23 -1
  66. package/src/core/state/app/types.ts +4 -2
  67. package/src/plugins/builtin/account-management/form-components.tsx +2 -2
  68. package/src/plugins/builtin/alerts/pane.tsx +1 -1
  69. package/src/plugins/builtin/buildout/cells.tsx +1 -1
  70. package/src/plugins/builtin/buildout/pane/index.tsx +1 -1
  71. package/src/plugins/builtin/chat/channels.ts +5 -18
  72. package/src/plugins/builtin/chat/chat.test.tsx +1 -7
  73. package/src/plugins/builtin/chat/message/desktop.tsx +1 -1
  74. package/src/plugins/builtin/chat/message/inline-tokens.tsx +1 -1
  75. package/src/plugins/builtin/chat/message/terminal.tsx +2 -2
  76. package/src/plugins/builtin/chat/sidebar.tsx +1 -1
  77. package/src/plugins/builtin/chat/status-widget.tsx +2 -8
  78. package/src/plugins/builtin/cloud/auth-actions.tsx +2 -2
  79. package/src/plugins/builtin/cloud/plugin.tsx +4 -13
  80. package/src/plugins/builtin/correlation/matrix/symbol-cell.tsx +1 -1
  81. package/src/plugins/builtin/debug/index.tsx +3 -4
  82. package/src/plugins/builtin/help/components.tsx +1 -1
  83. package/src/plugins/builtin/holders/treemap.tsx +28 -297
  84. package/src/plugins/builtin/holders/types.ts +0 -34
  85. package/src/plugins/builtin/market-heatmap/data.test.ts +121 -0
  86. package/src/plugins/builtin/market-heatmap/data.ts +296 -0
  87. package/src/plugins/builtin/market-heatmap/index.tsx +320 -0
  88. package/src/plugins/builtin/options/table.ts +5 -5
  89. package/src/plugins/builtin/plugin-groups.ts +2 -0
  90. package/src/plugins/builtin/portfolio-list/grid.tsx +197 -0
  91. package/src/plugins/builtin/portfolio-list/pane/index.test.tsx +41 -0
  92. package/src/plugins/builtin/portfolio-list/pane/index.tsx +78 -22
  93. package/src/plugins/ibkr/gateway/service/index.test.ts +22 -0
  94. package/src/plugins/ibkr/gateway/service/index.ts +6 -9
  95. package/src/plugins/ibkr/gateway/service/native-loader.ts +12 -0
  96. package/src/plugins/ibkr/trade/ticket-panel.tsx +1 -1
  97. package/src/remote/app-host.tsx +77 -0
  98. package/src/remote/client.ts +34 -0
  99. package/src/remote/command-bar.ts +98 -0
  100. package/src/remote/controller-utils.ts +111 -0
  101. package/src/remote/controller.test.ts +365 -0
  102. package/src/remote/controller.ts +434 -0
  103. package/src/remote/endpoint.ts +41 -0
  104. package/src/remote/json-patch.test.ts +31 -0
  105. package/src/remote/json-patch.ts +89 -0
  106. package/src/remote/layout-helpers.ts +62 -0
  107. package/src/remote/resources.ts +186 -0
  108. package/src/remote/revision.ts +18 -0
  109. package/src/remote/schema.ts +143 -0
  110. package/src/remote/semantic-helpers.ts +161 -0
  111. package/src/remote/semantic-tree.tsx +112 -0
  112. package/src/remote/server.test.ts +50 -0
  113. package/src/remote/server.ts +121 -0
  114. package/src/remote/types.ts +119 -0
  115. package/src/renderers/electrobun/bun/index.ts +55 -1
  116. package/src/renderers/electrobun/shared/protocol.ts +11 -1
  117. package/src/renderers/electrobun/view/backend-rpc.ts +28 -1
  118. package/src/renderers/electrobun/view/build-assets.ts +1 -0
  119. package/src/renderers/electrobun/view/data-table/index.tsx +0 -8
  120. package/src/renderers/electrobun/view/data-table/row.tsx +2 -12
  121. package/src/renderers/electrobun/view/host/box.tsx +11 -1
  122. package/src/renderers/electrobun/view/host/mouse.test.ts +37 -0
  123. package/src/renderers/electrobun/view/host/mouse.ts +2 -0
  124. package/src/renderers/electrobun/view/host/style.ts +1 -0
  125. package/src/renderers/electrobun/view/main.tsx +5 -1
  126. package/src/renderers/electrobun/view/native-stubs/ibkr-native-loader.ts +3 -0
  127. package/src/renderers/electrobun/view/styles.css +7 -0
  128. package/src/renderers/opentui/chart-surface.test.tsx +101 -0
  129. package/src/renderers/opentui/chart-surface.tsx +15 -20
  130. package/src/renderers/opentui/native-smoke.ts +1 -0
  131. package/src/renderers/opentui/start.tsx +32 -0
  132. package/src/sources/yahoo-finance/http.ts +31 -4
  133. package/src/state/app/context/index.test.tsx +47 -0
  134. package/src/state/app/context/index.tsx +48 -18
  135. package/src/ui/host.tsx +1 -0
  136. package/src/ui/index.tsx +183 -7
  137. package/src/version.ts +1 -1
  138. package/src/plugins/builtin/holders/treemap-layout.ts +0 -231
@@ -1,19 +1,23 @@
1
1
  import { afterEach, describe, expect, test } from "bun:test";
2
- import { act } from "react";
2
+ import { act, useEffect } from "react";
3
3
  import { testRender } from "../../../../renderers/opentui/test-utils";
4
4
  import { colors } from "../../../../theme/colors";
5
+ import { RemoteUiRegistryProvider, useRemoteUiRegistry, type RemoteUiRegistry } from "../../../../remote/semantic-tree";
5
6
  import { resolveChartPalette } from "../../core/renderer";
6
7
  import { StaticChartSurface } from "./surface";
7
8
  import type { ProjectedChartPoint } from "../../core/data";
8
9
 
9
10
  let testSetup: Awaited<ReturnType<typeof testRender>> | undefined;
11
+ let remoteRegistry: RemoteUiRegistry | null = null;
10
12
 
11
13
  afterEach(async () => {
12
- if (!testSetup) return;
14
+ const setup = testSetup;
15
+ testSetup = undefined;
16
+ remoteRegistry = null;
17
+ if (!setup) return;
13
18
  await act(async () => {
14
- testSetup!.renderer.destroy();
19
+ setup.renderer.destroy();
15
20
  });
16
- testSetup = undefined;
17
21
  });
18
22
 
19
23
  const points: ProjectedChartPoint[] = [
@@ -22,6 +26,14 @@ const points: ProjectedChartPoint[] = [
22
26
  { date: new Date("2026-01-03"), open: 4.9, high: 4.9, low: 4.9, close: 4.9, volume: 0 },
23
27
  ];
24
28
 
29
+ function RemoteRegistryProbe() {
30
+ const registry = useRemoteUiRegistry();
31
+ useEffect(() => {
32
+ remoteRegistry = registry;
33
+ }, [registry]);
34
+ return null;
35
+ }
36
+
25
37
  describe("StaticChartSurface", () => {
26
38
  test("renders unit and custom y-axis labels", async () => {
27
39
  testSetup = await testRender(
@@ -132,6 +144,49 @@ describe("StaticChartSurface", () => {
132
144
 
133
145
  await act(async () => {
134
146
  await testSetup!.mockMouse.moveTo(20, 4);
147
+ });
148
+ await act(async () => {
149
+ await testSetup!.renderOnce();
150
+ await testSetup!.renderOnce();
151
+ });
152
+
153
+ expect(testSetup.captureCharFrame()).not.toBe(initialFrame);
154
+ expect(testSetup.captureCharFrame()).toContain("X49");
155
+ expect(testSetup.captureCharFrame()).toContain("Y3.97");
156
+ });
157
+
158
+ test("exposes semantic remote cursor control", async () => {
159
+ testSetup = await testRender(
160
+ <RemoteUiRegistryProvider>
161
+ <RemoteRegistryProbe />
162
+ <StaticChartSurface
163
+ points={points}
164
+ width={48}
165
+ height={10}
166
+ mode="line"
167
+ colors={resolveChartPalette(colors, "positive")}
168
+ xAxisLabels={["0%", "50%", "100%"]}
169
+ formatXAxisCursorValue={(ratio) => `X${Math.round(ratio * 100)}`}
170
+ formatYAxisValue={(value) => `Y${value.toFixed(2)}`}
171
+ />
172
+ </RemoteUiRegistryProvider>,
173
+ { width: 50, height: 12 },
174
+ );
175
+
176
+ await act(async () => {
177
+ await testSetup!.renderOnce();
178
+ await testSetup!.renderOnce();
179
+ });
180
+ const initialFrame = testSetup.captureCharFrame();
181
+ const chartNode = remoteRegistry?.snapshot().find((node) => (
182
+ node.role === "chart" && node.metadata?.kind === "static-chart"
183
+ ));
184
+ expect(chartNode?.actions).toContain("moveCursor");
185
+
186
+ await act(async () => {
187
+ await remoteRegistry!.invoke(chartNode!.id, "moveCursor", { x: 20, y: 4 });
188
+ });
189
+ await act(async () => {
135
190
  await testSetup!.renderOnce();
136
191
  await testSetup!.renderOnce();
137
192
  });
@@ -253,6 +253,10 @@ export function StaticChartSurface({
253
253
  flexDirection="column"
254
254
  bitmaps={bitmap ? [bitmap] : null}
255
255
  crosshair={canvasCrosshair}
256
+ onMouseMove={handleCursorEvent}
257
+ onMouseDown={handleCursorEvent}
258
+ onMouseOut={clearCursor}
259
+ data-gloom-remote-kind="static-chart"
256
260
  >
257
261
  {textResult.lines.map((line, index) => (
258
262
  <Text key={index} content={line} />
@@ -159,6 +159,13 @@ describe("stock chart keyboard helpers", () => {
159
159
  expect(resolveChartKeyboardKey({ name: "minus" })).toBe("zoom-out");
160
160
  expect(resolveChartKeyboardKey({ name: "A" })).toBe("a");
161
161
  });
162
+
163
+ test("ignores shortcut-modified keys so app shortcuts can handle them", () => {
164
+ expect(resolveChartKeyboardKey({ name: "1", ctrl: true })).toBe("");
165
+ expect(resolveChartKeyboardKey({ name: "2", meta: true })).toBe("");
166
+ expect(resolveChartKeyboardKey({ name: "3", super: true })).toBe("");
167
+ expect(resolveChartKeyboardKey({ name: "4", alt: true })).toBe("");
168
+ });
162
169
  });
163
170
 
164
171
  describe("stock chart viewport helpers", () => {
@@ -10,6 +10,7 @@ import {
10
10
  import { Spinner } from "../../ui";
11
11
  import type { CommandBarListRow, ListScreenState, ResultItem } from "./model";
12
12
  import { getRowPresentation, truncateText } from "../view-model";
13
+ import { useRemoteUiNode } from "../../../remote/semantic-tree";
13
14
 
14
15
  export type CommandBarListScrollEvent = {
15
16
  stopPropagation: () => void;
@@ -70,6 +71,8 @@ export const CommandBarListHeader = memo(function CommandBarListHeader({
70
71
  onChange={onQueryChange}
71
72
  placeholder={kind === "root" ? "Search commands" : title === "Security Description" ? "Search tickers" : "Filter"}
72
73
  focused
74
+ data-gloom-remote-scope="command-bar"
75
+ data-gloom-remote-surface="command-bar"
73
76
  width={nativePaneChrome ? "100%" : queryDisplayWidth}
74
77
  backgroundColor={nativePaneChrome ? "transparent" : paletteBg}
75
78
  focusedBackgroundColor={nativePaneChrome ? "transparent" : paletteBg}
@@ -114,6 +117,9 @@ interface CommandBarListItemRowProps {
114
117
  globalIdx: number;
115
118
  isSelected: boolean;
116
119
  isHovered: boolean;
120
+ listKind: ListScreenState["kind"];
121
+ listTitle: string;
122
+ listQuery: string;
117
123
  contentPadding: number;
118
124
  labelWidth: number;
119
125
  trailingWidth: number;
@@ -135,6 +141,9 @@ const CommandBarListItemRow = memo(function CommandBarListItemRow({
135
141
  globalIdx,
136
142
  isSelected,
137
143
  isHovered,
144
+ listKind,
145
+ listTitle,
146
+ listQuery,
138
147
  contentPadding,
139
148
  labelWidth,
140
149
  trailingWidth,
@@ -153,6 +162,41 @@ const CommandBarListItemRow = memo(function CommandBarListItemRow({
153
162
  const presentation = getRowPresentation(item, isSelected, trailingWidth > 0);
154
163
  const label = truncateText(presentation.label, labelWidth);
155
164
  const trailing = truncateText(presentation.trailing, trailingWidth);
165
+ const activate = () => onRowMouseDown({
166
+ preventDefault() {},
167
+ stopPropagation() {},
168
+ }, item, globalIdx);
169
+ useRemoteUiNode({
170
+ role: "command-bar-result",
171
+ label: item.label,
172
+ disabled: item.disabled === true,
173
+ actions: {
174
+ activate,
175
+ press: activate,
176
+ secondary: item.secondaryAction ? () => item.secondaryAction?.() : undefined,
177
+ },
178
+ metadata: {
179
+ index: globalIdx,
180
+ selected: isSelected,
181
+ hovered: isHovered,
182
+ listKind,
183
+ listTitle,
184
+ listQuery,
185
+ item: {
186
+ id: item.id,
187
+ label: item.label,
188
+ detail: item.detail,
189
+ category: item.category,
190
+ kind: item.kind,
191
+ right: item.right,
192
+ shortcutQuery: item.shortcutQuery,
193
+ searchText: item.searchText,
194
+ checked: item.checked,
195
+ current: item.current,
196
+ disabled: item.disabled === true,
197
+ },
198
+ },
199
+ });
156
200
 
157
201
  return (
158
202
  <Box
@@ -165,7 +209,7 @@ const CommandBarListItemRow = memo(function CommandBarListItemRow({
165
209
  : isHovered
166
210
  ? paletteHoverBg
167
211
  : (nativePaneChrome ? panelBg : paletteBg)}
168
- onMouseMove={() => onHoverIndex(globalIdx)}
212
+ onMouseOver={() => onHoverIndex(globalIdx)}
169
213
  onMouseOut={() => onHoverIndex(null)}
170
214
  {...(!nativePaneChrome ? { onMouseScroll: onListScroll } : {})}
171
215
  onMouseDown={(event: any) => onRowMouseDown(event, item, globalIdx)}
@@ -283,6 +327,9 @@ export const CommandBarListBody = memo(function CommandBarListBody({
283
327
  globalIdx={row.globalIdx}
284
328
  isSelected={isSelected}
285
329
  isHovered={isHovered}
330
+ listKind={visibleListState.kind}
331
+ listTitle={visibleListState.title}
332
+ listQuery={visibleListState.query}
286
333
  contentPadding={contentPadding}
287
334
  labelWidth={labelWidth}
288
335
  trailingWidth={trailingWidth}
@@ -197,6 +197,13 @@ export function CommandBarMultiSelectBody({
197
197
  onSelect={onSelect}
198
198
  onToggle={onToggle}
199
199
  bgColor={nativePaneChrome ? panelBg : paletteBg}
200
+ remoteLabel={route.title}
201
+ remoteScope="command-bar"
202
+ remoteMetadata={{
203
+ surface: "multi-select-picker",
204
+ routeKind: route.kind,
205
+ pickerId: route.pickerId,
206
+ }}
200
207
  />
201
208
  <Box flexDirection="row" gap={1}>
202
209
  <Button label="Done" variant="primary" onPress={onCommit} />
@@ -1,4 +1,4 @@
1
- import { useCallback, useRef, useState, type Dispatch, type RefObject, type SetStateAction } from "react";
1
+ import { useCallback, useEffect, useRef, useState, type Dispatch, type RefObject, type SetStateAction } from "react";
2
2
  import type { AppAction } from "../../../state/app/context";
3
3
  import type { Command } from "../commands/registry";
4
4
  import { resolveCommandBarMode } from "../view-model";
@@ -63,6 +63,15 @@ export function useCommandBarNavigationState({
63
63
  const currentRouteRef = useRef<CommandBarRoute | null>(currentRoute);
64
64
  currentRouteRef.current = currentRoute;
65
65
 
66
+ useEffect(() => {
67
+ if (currentRouteRef.current) return;
68
+ if (initialQuery === rootQueryRef.current) return;
69
+ rootQueryRef.current = initialQuery;
70
+ setRootQueryValue(initialQuery);
71
+ setRootSelectedIdx(0);
72
+ setRootHoveredIdx(null);
73
+ }, [initialQuery]);
74
+
66
75
  const closeAll = useCallback((options?: CloseCommandBarOptions) => {
67
76
  if (options?.revertThemePreview !== false) {
68
77
  restoreThemePreview();
@@ -129,6 +129,20 @@ describe("CommandBar", () => {
129
129
  expect(testSetup.captureCharFrame()).not.toContain("Commands");
130
130
  });
131
131
 
132
+ test("shows theme picker rows and commits a filtered light theme", async () => {
133
+ testSetup = await testRender(<CommandBarHarness query="TH light" live />, {
134
+ width: 80,
135
+ height: 24,
136
+ });
137
+
138
+ await testSetup.renderOnce();
139
+ expect(testSetup.captureCharFrame()).toContain("GitHub Light");
140
+
141
+ await clickFrameText("GitHub Light");
142
+ await waitForFrameToContain("theme:github-light");
143
+ expect(testSetup.captureCharFrame()).not.toContain("GitHub Light");
144
+ });
145
+
132
146
  test("runs plugin command shortcuts from the root query", async () => {
133
147
  const calls: string[] = [];
134
148
 
@@ -4,13 +4,14 @@ import {
4
4
  useCallback,
5
5
  useEffect,
6
6
  useImperativeHandle,
7
- useLayoutEffect,
8
7
  useMemo,
9
8
  useRef,
10
9
  useState,
11
10
  } from "react";
12
- import { Box, ScrollBox, Text } from "../../ui";
13
- import { TextAttributes, type ScrollBoxRenderable } from "../../ui";
11
+ import { Box, Text } from "../../ui";
12
+ import { TextAttributes } from "../../ui";
13
+ import { ListView, type ListViewItem } from "../ui";
14
+ import type { ListRowState } from "../ui/list-view";
14
15
  import { getThemeIds, themes as themeRegistry } from "../../theme/themes";
15
16
  import { truncateText } from "./view-model";
16
17
 
@@ -75,7 +76,6 @@ export const ThemePicker = memo(forwardRef<ThemePickerHandle, ThemePickerProps>(
75
76
  onPreview,
76
77
  onCommit,
77
78
  }: ThemePickerProps, ref) {
78
- const scrollRef = useRef<ScrollBoxRenderable | null>(null);
79
79
  const previewTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
80
80
  const pendingPreviewIdRef = useRef<string | null>(null);
81
81
  const committedThemeIdRef = useRef(committedThemeId);
@@ -92,9 +92,20 @@ export const ThemePicker = memo(forwardRef<ThemePickerHandle, ThemePickerProps>(
92
92
  const [selectedIndex, setSelectedIndex] = useState(() => (
93
93
  Math.max(0, themes.findIndex((theme) => theme.id === committedThemeId))
94
94
  ));
95
- const [hoveredIndex, setHoveredIndex] = useState<number | null>(null);
96
95
  const themesRef = useRef(themes);
97
96
  const selectedIndexRef = useRef(selectedIndex);
97
+ const items = useMemo<ListViewItem[]>(() => themes.map((theme) => {
98
+ const current = theme.id === committedThemeId;
99
+ return {
100
+ id: theme.id,
101
+ label: theme.name,
102
+ detail: current ? "current" : "",
103
+ category: "Themes",
104
+ kind: "theme",
105
+ right: current ? "current" : "",
106
+ current,
107
+ };
108
+ }), [committedThemeId, themes]);
98
109
 
99
110
  themesRef.current = themes;
100
111
  selectedIndexRef.current = selectedIndex;
@@ -131,7 +142,6 @@ export const ThemePicker = memo(forwardRef<ThemePickerHandle, ThemePickerProps>(
131
142
  if (nextIndex === selectedIndexRef.current) return false;
132
143
  selectedIndexRef.current = nextIndex;
133
144
  setSelectedIndex(nextIndex);
134
- setHoveredIndex(null);
135
145
  requestPreview(options[nextIndex]!.id);
136
146
  return true;
137
147
  }, [requestPreview]);
@@ -155,24 +165,10 @@ export const ThemePicker = memo(forwardRef<ThemePickerHandle, ThemePickerProps>(
155
165
  const nextIndex = preferredIndex >= 0 ? preferredIndex : 0;
156
166
  selectedIndexRef.current = nextIndex;
157
167
  setSelectedIndex(nextIndex);
158
- setHoveredIndex(null);
159
168
  }, [committedThemeId, themes]);
160
169
 
161
170
  useEffect(() => cancelPreview, [cancelPreview]);
162
171
 
163
- useLayoutEffect(() => {
164
- const scrollBox = scrollRef.current;
165
- if (!scrollBox) return;
166
- if (scrollBox.verticalScrollBar) scrollBox.verticalScrollBar.visible = false;
167
- if (themes.length === 0) return;
168
- const viewportHeight = Math.max(1, scrollBox.viewport?.height ?? height);
169
- if (selectedIndex < scrollBox.scrollTop) {
170
- scrollBox.scrollTo(selectedIndex);
171
- } else if (selectedIndex >= scrollBox.scrollTop + viewportHeight) {
172
- scrollBox.scrollTo(selectedIndex - viewportHeight + 1);
173
- }
174
- }, [height, selectedIndex, themes.length]);
175
-
176
172
  const handleScroll = useCallback((event: ThemePickerScrollEvent) => {
177
173
  event.stopPropagation();
178
174
  event.preventDefault();
@@ -185,70 +181,86 @@ export const ThemePicker = memo(forwardRef<ThemePickerHandle, ThemePickerProps>(
185
181
  }
186
182
  }, [move]);
187
183
 
188
- if (themes.length === 0) {
184
+ const handleSelect = useCallback((index: number) => {
185
+ const selected = themesRef.current[index];
186
+ if (!selected) return;
187
+ selectedIndexRef.current = index;
188
+ setSelectedIndex(index);
189
+ requestPreview(selected.id);
190
+ }, [requestPreview]);
191
+
192
+ const handleActivate = useCallback((item: ListViewItem, index: number) => {
193
+ const selected = themesRef.current[index] ?? themesRef.current.find((theme) => theme.id === item.id);
194
+ if (!selected) return;
195
+ selectedIndexRef.current = index;
196
+ setSelectedIndex(index);
197
+ cancelPreview();
198
+ onCommitRef.current(selected.id);
199
+ }, [cancelPreview]);
200
+
201
+ const renderRow = useCallback((item: ListViewItem, state: ListRowState) => {
202
+ const label = truncateText(item.label, labelWidth);
203
+ const trailing = item.current ? "current" : "";
189
204
  return (
190
- <Box height={height} paddingX={contentPadding}>
191
- <Text fg={paletteSubtleText}>{truncateText("No themes match", queryDisplayWidth)}</Text>
205
+ <Box
206
+ flexDirection="row"
207
+ height={1}
208
+ paddingX={contentPadding}
209
+ width="100%"
210
+ data-command-bar-row-selected={nativePaneChrome && state.selected ? "true" : undefined}
211
+ style={nativePaneChrome ? { borderRadius: 6 } : undefined}
212
+ >
213
+ <Box width={labelWidth}>
214
+ <Text
215
+ fg={state.selected ? paletteSelectedText : paletteText}
216
+ attributes={item.current ? TextAttributes.BOLD : undefined}
217
+ >
218
+ {label}
219
+ </Text>
220
+ </Box>
221
+ <Box width={trailingWidth}>
222
+ <Text fg={state.selected ? paletteSelectedText : paletteSubtleText}>
223
+ {truncateText(trailing, trailingWidth)}
224
+ </Text>
225
+ </Box>
192
226
  </Box>
193
227
  );
194
- }
228
+ }, [
229
+ contentPadding,
230
+ labelWidth,
231
+ nativePaneChrome,
232
+ paletteSelectedText,
233
+ paletteSubtleText,
234
+ paletteText,
235
+ trailingWidth,
236
+ ]);
195
237
 
196
238
  return (
197
- <ScrollBox
198
- ref={scrollRef}
199
- flexDirection="column"
239
+ <ListView
240
+ items={items}
241
+ selectedIndex={selectedIndex}
200
242
  height={height}
201
- scrollY
202
- focusable={false}
203
- {...(!nativePaneChrome ? { onMouseScroll: handleScroll } : {})}
204
- >
205
- {themes.map((theme, index) => {
206
- const selected = index === selectedIndex;
207
- const hovered = index === hoveredIndex && !selected;
208
- const current = theme.id === committedThemeId;
209
- const label = truncateText(theme.name, labelWidth);
210
- const trailing = current ? "current" : "";
211
- return (
212
- <Box
213
- key={theme.id}
214
- flexDirection="row"
215
- height={1}
216
- paddingX={contentPadding}
217
- backgroundColor={selected
218
- ? paletteSelectedBg
219
- : hovered
220
- ? paletteHoverBg
221
- : (nativePaneChrome ? panelBg : paletteBg)}
222
- onMouseMove={() => setHoveredIndex(index)}
223
- onMouseOut={() => setHoveredIndex(null)}
224
- onMouseDown={(event: any) => {
225
- event.stopPropagation?.();
226
- event.preventDefault?.();
227
- selectedIndexRef.current = index;
228
- setSelectedIndex(index);
229
- cancelPreview();
230
- onCommitRef.current(theme.id);
231
- }}
232
- {...(!nativePaneChrome ? { onMouseScroll: handleScroll } : {})}
233
- data-command-bar-row-selected={nativePaneChrome && selected ? "true" : undefined}
234
- style={nativePaneChrome ? { borderRadius: 6 } : undefined}
235
- >
236
- <Box width={labelWidth}>
237
- <Text
238
- fg={selected ? paletteSelectedText : paletteText}
239
- attributes={current ? TextAttributes.BOLD : undefined}
240
- >
241
- {label}
242
- </Text>
243
- </Box>
244
- <Box width={trailingWidth}>
245
- <Text fg={selected ? paletteSelectedText : paletteSubtleText}>
246
- {truncateText(trailing, trailingWidth)}
247
- </Text>
248
- </Box>
249
- </Box>
250
- );
251
- })}
252
- </ScrollBox>
243
+ scrollable
244
+ rowGap={0}
245
+ rowHeight={1}
246
+ surface="plain"
247
+ bgColor={nativePaneChrome ? panelBg : paletteBg}
248
+ selectedBgColor={paletteSelectedBg}
249
+ hoverBgColor={paletteHoverBg}
250
+ emptyMessage={truncateText("No themes match", queryDisplayWidth)}
251
+ showSelectedDescription={false}
252
+ onSelect={handleSelect}
253
+ onActivate={handleActivate}
254
+ onMouseScroll={!nativePaneChrome ? handleScroll : undefined}
255
+ renderRow={renderRow}
256
+ remoteLabel="Theme picker"
257
+ remoteScope="command-bar"
258
+ remoteItemKind="theme"
259
+ remoteItemCategory="Themes"
260
+ remoteMetadata={{
261
+ surface: "theme-picker",
262
+ filter: normalizedFilter,
263
+ }}
264
+ />
253
265
  );
254
266
  }));
@@ -15,6 +15,7 @@ import {
15
15
  isWorkflowTextField,
16
16
  summarizeWorkflowFieldValue,
17
17
  } from "../helpers";
18
+ import { useRemoteUiNode } from "../../../remote/semantic-tree";
18
19
  import type {
19
20
  CommandBarFieldValue,
20
21
  CommandBarWorkflowField,
@@ -84,6 +85,34 @@ export function CommandBarWorkflowFieldRow({
84
85
  onMoveFieldFocus(1);
85
86
  }
86
87
  };
88
+ const focusOrOpenField = () => {
89
+ onActiveTextareaSync(route);
90
+ onFieldFocus(field.id);
91
+ if (!isWorkflowTextField(field) && !useNativeSelect) {
92
+ onFieldPickerOpen(route, field);
93
+ }
94
+ };
95
+
96
+ useRemoteUiNode({
97
+ role: "command-bar-field",
98
+ label: field.label,
99
+ disabled: route.pending,
100
+ actions: {
101
+ focus: focusOrOpenField,
102
+ open: !isWorkflowTextField(field) && !useNativeSelect ? focusOrOpenField : undefined,
103
+ submit: () => void onSubmit(route),
104
+ },
105
+ metadata: {
106
+ scope: "command-bar",
107
+ surface: "workflow",
108
+ routeTitle: route.title,
109
+ fieldId: field.id,
110
+ fieldType: field.type,
111
+ active,
112
+ value: summarizeWorkflowFieldValue(field, value),
113
+ description: fieldDescription,
114
+ },
115
+ });
87
116
 
88
117
  return (
89
118
  <Box
@@ -93,11 +122,7 @@ export function CommandBarWorkflowFieldRow({
93
122
  backgroundColor={fieldBg}
94
123
  onMouseDown={(event: any) => {
95
124
  event.stopPropagation?.();
96
- onActiveTextareaSync(route);
97
- onFieldFocus(field.id);
98
- if (!isWorkflowTextField(field) && !useNativeSelect) {
99
- onFieldPickerOpen(route, field);
100
- }
125
+ focusOrOpenField();
101
126
  }}
102
127
  style={nativePaneChrome ? {
103
128
  marginBottom: isLastField ? 8 : 10,
@@ -73,8 +73,6 @@ export interface DataTableViewProps<
73
73
  | "scrollRef"
74
74
  | "syncHeaderScroll"
75
75
  | "onBodyScrollActivity"
76
- | "hoveredIdx"
77
- | "setHoveredIdx"
78
76
  | "isSelected"
79
77
  | "onSelect"
80
78
  | "onActivate"
@@ -97,8 +95,6 @@ export interface DataTableViewProps<
97
95
  scrollRef?: RefObject<ScrollBoxRenderable | null>;
98
96
  syncHeaderScroll?: () => void;
99
97
  onBodyScrollActivity?: () => void;
100
- hoveredIdx?: number | null;
101
- setHoveredIdx?: (index: number | null) => void;
102
98
  keyboardNavigation?: boolean;
103
99
  onRootKeyDown?: (
104
100
  event: DataTableKeyEvent,
@@ -125,8 +121,6 @@ export function DataTableView<
125
121
  scrollRef,
126
122
  syncHeaderScroll,
127
123
  onBodyScrollActivity,
128
- hoveredIdx,
129
- setHoveredIdx,
130
124
  keyboardNavigation = true,
131
125
  onRootKeyDown,
132
126
  resetScrollKey,
@@ -137,15 +131,11 @@ export function DataTableView<
137
131
  const {
138
132
  effectiveHeaderScrollRef,
139
133
  effectiveScrollRef,
140
- effectiveHoveredIdx,
141
- effectiveSetHoveredIdx,
142
134
  effectiveSyncHeaderScroll,
143
135
  } = useTableViewState({
144
136
  headerScrollRef,
145
137
  scrollRef,
146
138
  syncHeaderScroll,
147
- hoveredIdx,
148
- setHoveredIdx,
149
139
  });
150
140
  const [cursorIndex, setCursorIndex] = useState<number | null>(null);
151
141
  const pendingCommitRef = useRef(false);
@@ -532,8 +522,6 @@ export function DataTableView<
532
522
  scrollRef={effectiveScrollRef}
533
523
  syncHeaderScroll={effectiveSyncHeaderScroll}
534
524
  onBodyScrollActivity={handleBodyScrollActivity}
535
- hoveredIdx={effectiveHoveredIdx}
536
- setHoveredIdx={effectiveSetHoveredIdx}
537
525
  scrollToIndex={effectiveScrollToIndex}
538
526
  scrollToIndexVersion={scrollToIndexVersion + selectionScrollVersion}
539
527
  isSelected={isItemSelected}
@@ -1,6 +1,13 @@
1
1
 
2
2
  export { PriceSelectorDialog } from "./price-selector-dialog";
3
3
  export { StaticChartSurface } from "./chart/static/chart/surface";
4
+ export {
5
+ buildMetricTreemapNavigationTiles,
6
+ findMetricTreemapNeighbor,
7
+ MetricTreemapSurface,
8
+ type MetricTreemapDirection,
9
+ type MetricTreemapItem,
10
+ } from "./metric-treemap";
4
11
  export { SpeedometerGauge } from "./speedometer-gauge";
5
12
  export type { SpeedometerSegment } from "./speedometer-gauge";
6
13
  export { TickerListTableView } from "./ticker/list-table-view";
@@ -18,7 +18,7 @@ interface FloatingPaneWrapperProps {
18
18
  windowModeSelected?: boolean;
19
19
  showActions?: boolean;
20
20
  onMouseDown?: (event: any) => void;
21
- onMouseMove?: (event: any) => void;
21
+ onHeaderMouseMove?: (event: any) => void;
22
22
  onHeaderMouseDown?: (event: any) => void;
23
23
  onHeaderMouseDrag?: (event: any) => void;
24
24
  onHeaderMouseDragEnd?: (event: any) => void;
@@ -63,7 +63,7 @@ export function FloatingPaneWrapper({
63
63
  windowModeSelected = false,
64
64
  showActions = false,
65
65
  onMouseDown,
66
- onMouseMove,
66
+ onHeaderMouseMove,
67
67
  onHeaderMouseDown,
68
68
  onHeaderMouseDrag,
69
69
  onHeaderMouseDragEnd,
@@ -104,7 +104,6 @@ export function FloatingPaneWrapper({
104
104
  showBorderColor: true,
105
105
  })}
106
106
  onMouseDown={onMouseDown}
107
- onMouseMove={onMouseMove}
108
107
  >
109
108
  <PaneHeader
110
109
  title={title}
@@ -113,6 +112,7 @@ export function FloatingPaneWrapper({
113
112
  windowModeSelected={windowModeSelected}
114
113
  floating
115
114
  showActions={showActions}
115
+ onHeaderMouseMove={onHeaderMouseMove}
116
116
  onHeaderMouseDown={onHeaderMouseDown}
117
117
  onHeaderMouseDrag={onHeaderMouseDrag}
118
118
  onHeaderMouseDragEnd={onHeaderMouseDragEnd}