gloomberb 0.9.5 → 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 (92) hide show
  1. package/README.md +49 -8
  2. package/package.json +2 -2
  3. package/src/app/global-shortcuts.test.tsx +20 -1
  4. package/src/app/global-shortcuts.ts +2 -0
  5. package/src/app/pane-runtime/index.tsx +11 -4
  6. package/src/app/pane-runtime/pane-settings-runtime.tsx +1 -1
  7. package/src/app.tsx +41 -17
  8. package/src/cli/commands/remote.ts +206 -0
  9. package/src/cli/index.ts +2 -0
  10. package/src/components/chart/core/keyboard.ts +10 -1
  11. package/src/components/chart/static/bar-chart-surface.test.tsx +4 -0
  12. package/src/components/chart/static/chart/surface.test.tsx +59 -4
  13. package/src/components/chart/static/chart/surface.tsx +4 -0
  14. package/src/components/chart/stock-chart/index.test.ts +7 -0
  15. package/src/components/command-bar/list/view.tsx +47 -0
  16. package/src/components/command-bar/multi-select-picker.tsx +7 -0
  17. package/src/components/command-bar/routing/navigation-state.ts +10 -1
  18. package/src/components/command-bar/surface/index.test.tsx +14 -0
  19. package/src/components/command-bar/theme-picker.tsx +91 -79
  20. package/src/components/command-bar/workflow/field-row.tsx +30 -5
  21. package/src/components/index.ts +7 -0
  22. package/src/components/layout/pane/footer/index.tsx +23 -0
  23. package/src/components/layout/shell/index.test.tsx +2 -0
  24. package/src/components/layout/shell/index.tsx +8 -3
  25. package/src/components/layout/shell/pane/actions.ts +21 -7
  26. package/src/components/metric-treemap/index.tsx +317 -0
  27. package/src/components/metric-treemap/layout.test.ts +77 -0
  28. package/src/components/metric-treemap/layout.ts +476 -0
  29. package/src/components/toggle-list.tsx +10 -0
  30. package/src/components/ui/button.tsx +12 -0
  31. package/src/components/ui/data-table/index.tsx +51 -0
  32. package/src/components/ui/fields.tsx +18 -0
  33. package/src/components/ui/list-view.tsx +83 -3
  34. package/src/components/ui/message-composer.tsx +22 -0
  35. package/src/components/ui/tabs.tsx +45 -0
  36. package/src/core/state/app/layout-reducer.ts +7 -1
  37. package/src/core/state/app/layout.ts +6 -0
  38. package/src/core/state/app/state.ts +23 -1
  39. package/src/core/state/app/types.ts +4 -2
  40. package/src/plugins/builtin/chat/channels.ts +5 -18
  41. package/src/plugins/builtin/chat/chat.test.tsx +1 -7
  42. package/src/plugins/builtin/chat/status-widget.tsx +1 -7
  43. package/src/plugins/builtin/cloud/plugin.tsx +4 -13
  44. package/src/plugins/builtin/holders/treemap.tsx +28 -297
  45. package/src/plugins/builtin/holders/types.ts +0 -34
  46. package/src/plugins/builtin/market-heatmap/data.test.ts +121 -0
  47. package/src/plugins/builtin/market-heatmap/data.ts +296 -0
  48. package/src/plugins/builtin/market-heatmap/index.tsx +320 -0
  49. package/src/plugins/builtin/plugin-groups.ts +2 -0
  50. package/src/plugins/builtin/portfolio-list/grid.tsx +197 -0
  51. package/src/plugins/builtin/portfolio-list/pane/index.test.tsx +41 -0
  52. package/src/plugins/builtin/portfolio-list/pane/index.tsx +78 -22
  53. package/src/plugins/ibkr/gateway/service/index.test.ts +22 -0
  54. package/src/plugins/ibkr/gateway/service/index.ts +6 -9
  55. package/src/plugins/ibkr/gateway/service/native-loader.ts +12 -0
  56. package/src/remote/app-host.tsx +77 -0
  57. package/src/remote/client.ts +34 -0
  58. package/src/remote/command-bar.ts +98 -0
  59. package/src/remote/controller-utils.ts +111 -0
  60. package/src/remote/controller.test.ts +365 -0
  61. package/src/remote/controller.ts +434 -0
  62. package/src/remote/endpoint.ts +41 -0
  63. package/src/remote/json-patch.test.ts +31 -0
  64. package/src/remote/json-patch.ts +89 -0
  65. package/src/remote/layout-helpers.ts +62 -0
  66. package/src/remote/resources.ts +186 -0
  67. package/src/remote/revision.ts +18 -0
  68. package/src/remote/schema.ts +143 -0
  69. package/src/remote/semantic-helpers.ts +161 -0
  70. package/src/remote/semantic-tree.tsx +112 -0
  71. package/src/remote/server.test.ts +50 -0
  72. package/src/remote/server.ts +121 -0
  73. package/src/remote/types.ts +119 -0
  74. package/src/renderers/electrobun/bun/index.ts +55 -1
  75. package/src/renderers/electrobun/shared/protocol.ts +11 -1
  76. package/src/renderers/electrobun/view/backend-rpc.ts +28 -1
  77. package/src/renderers/electrobun/view/build-assets.ts +1 -0
  78. package/src/renderers/electrobun/view/host/box.tsx +5 -0
  79. package/src/renderers/electrobun/view/host/mouse.test.ts +37 -0
  80. package/src/renderers/electrobun/view/host/mouse.ts +2 -0
  81. package/src/renderers/electrobun/view/main.tsx +5 -1
  82. package/src/renderers/electrobun/view/native-stubs/ibkr-native-loader.ts +3 -0
  83. package/src/renderers/opentui/chart-surface.test.tsx +101 -0
  84. package/src/renderers/opentui/chart-surface.tsx +15 -20
  85. package/src/renderers/opentui/start.tsx +32 -0
  86. package/src/sources/yahoo-finance/http.ts +31 -4
  87. package/src/state/app/context/index.test.tsx +47 -0
  88. package/src/state/app/context/index.tsx +48 -18
  89. package/src/ui/host.tsx +1 -0
  90. package/src/ui/index.tsx +183 -7
  91. package/src/version.ts +1 -1
  92. package/src/plugins/builtin/holders/treemap-layout.ts +0 -231
@@ -0,0 +1,317 @@
1
+ import { useMemo, useState, type CSSProperties } from "react";
2
+ import { Box, Text, TextAttributes, useUiCapabilities } from "../../ui";
3
+ import { blendHex, colors, priceColor } from "../../theme/colors";
4
+ import { blendForContrast, higherContrast } from "../../theme/color-utils";
5
+ import { padTo } from "../../utils/format";
6
+ import {
7
+ buildMetricTreemap,
8
+ buildMetricTreemapRects,
9
+ type FloatMetricTreemapTile,
10
+ type MetricTreemapItem,
11
+ type MetricTreemapTile,
12
+ } from "./layout";
13
+
14
+ export {
15
+ buildMetricTreemapNavigationTiles,
16
+ findMetricTreemapNeighbor,
17
+ type MetricTreemapDirection,
18
+ type MetricTreemapItem,
19
+ } from "./layout";
20
+
21
+ type PreventableMouseEvent = { preventDefault(): void };
22
+
23
+ export interface MetricTreemapSurfaceProps<T> {
24
+ items: MetricTreemapItem<T>[];
25
+ width: number;
26
+ height: number;
27
+ selectedId: string | null;
28
+ onSelect: (item: MetricTreemapItem<T>) => void;
29
+ onActivate?: (item: MetricTreemapItem<T>) => void;
30
+ emptyStateTitle?: string;
31
+ }
32
+
33
+ function metricTileColor(item: MetricTreemapItem): string {
34
+ if (item.colorValue == null) return colors.neutral;
35
+ return priceColor(item.colorValue);
36
+ }
37
+
38
+ function desktopMetricTileColor(item: MetricTreemapItem): string {
39
+ if (item.colorValue == null || item.colorValue === 0) {
40
+ return colors.neutral;
41
+ }
42
+ const intensity = Math.max(0.38, Math.min(0.88, Math.log1p(Math.abs(item.colorValue)) / Math.log1p(30)));
43
+ return blendHex(colors.panel, item.colorValue > 0 ? colors.positive : colors.negative, intensity);
44
+ }
45
+
46
+ const TILE_TEXT_MIN_CONTRAST = 4.5;
47
+
48
+ function tileTextColor(backgroundColor: string): string {
49
+ const preferred = higherContrast(
50
+ higherContrast(colors.text, colors.textBright, backgroundColor),
51
+ colors.selectedText,
52
+ backgroundColor,
53
+ );
54
+ const fallback = higherContrast("#000000", "#ffffff", backgroundColor);
55
+ return blendForContrast(preferred, backgroundColor, fallback, TILE_TEXT_MIN_CONTRAST);
56
+ }
57
+
58
+ function visibleLines(item: MetricTreemapItem): string[] {
59
+ return [
60
+ item.label,
61
+ item.primaryText,
62
+ item.secondaryText,
63
+ item.tertiaryText,
64
+ ].filter((line): line is string => !!line);
65
+ }
66
+
67
+ function Tile<T>({ tile, selected, onSelect, onActivate }: {
68
+ tile: MetricTreemapTile<T>;
69
+ selected: boolean;
70
+ onSelect: () => void;
71
+ onActivate?: () => void;
72
+ }) {
73
+ const renderWidth = Math.max(1, tile.width - (tile.width > 2 ? 1 : 0));
74
+ const renderHeight = Math.max(1, tile.height - (tile.height > 2 ? 1 : 0));
75
+ const innerWidth = Math.max(1, renderWidth - 1);
76
+ const backgroundColor = selected ? colors.selected : metricTileColor(tile.item);
77
+ const textColor = tileTextColor(backgroundColor);
78
+ const attributes = selected ? TextAttributes.BOLD : TextAttributes.NONE;
79
+ const lines = visibleLines(tile.item);
80
+
81
+ return (
82
+ <Box
83
+ position="absolute"
84
+ left={tile.x}
85
+ top={tile.y}
86
+ width={renderWidth}
87
+ height={renderHeight}
88
+ backgroundColor={backgroundColor}
89
+ onMouseDown={(event: PreventableMouseEvent) => {
90
+ event.preventDefault();
91
+ onSelect();
92
+ }}
93
+ onMouseOver={onSelect}
94
+ onMouseMove={onSelect}
95
+ onDoubleClick={onActivate}
96
+ >
97
+ {lines.slice(0, renderHeight).map((line, index) => (
98
+ <Text key={`${tile.item.id}:${index}`} fg={textColor} attributes={attributes}>
99
+ {padTo(line, innerWidth)}
100
+ </Text>
101
+ ))}
102
+ </Box>
103
+ );
104
+ }
105
+
106
+ function pct(value: number, total: number): string {
107
+ return `${total > 0 ? value / total * 100 : 0}%`;
108
+ }
109
+
110
+ function DesktopTile<T>({ tile, chartWidth, chartHeight, selected, hovered, onSelect, onActivate, onHover }: {
111
+ tile: FloatMetricTreemapTile<T>;
112
+ chartWidth: number;
113
+ chartHeight: number;
114
+ selected: boolean;
115
+ hovered: boolean;
116
+ onSelect: () => void;
117
+ onActivate?: () => void;
118
+ onHover: (hovered: boolean) => void;
119
+ }) {
120
+ const lines = visibleLines(tile.item);
121
+ const canShowDetails = tile.width >= 7 && tile.height >= 3;
122
+ const canShowExtra = tile.height >= 3.7;
123
+ const canShowLabel = tile.width >= 4 && tile.height >= 1.4;
124
+ const isTiny = tile.width < 5 || tile.height < 2;
125
+ const backgroundColor = desktopMetricTileColor(tile.item);
126
+ const textColor = tileTextColor(backgroundColor);
127
+ const style: CSSProperties = {
128
+ position: "absolute",
129
+ left: `calc(${pct(tile.x, chartWidth)} + 1px)`,
130
+ top: `calc(${pct(tile.y, chartHeight)} + 1px)`,
131
+ width: `max(1px, calc(${pct(tile.width, chartWidth)} - 2px))`,
132
+ height: `max(1px, calc(${pct(tile.height, chartHeight)} - 2px))`,
133
+ minWidth: 1,
134
+ minHeight: 1,
135
+ padding: isTiny ? "3px 4px" : "7px 8px",
136
+ overflow: "hidden",
137
+ borderRadius: 5,
138
+ border: selected
139
+ ? `1px solid ${colors.textBright}`
140
+ : hovered
141
+ ? `1px solid ${blendHex(colors.textBright, backgroundColor, 0.55)}`
142
+ : `1px solid ${blendHex(backgroundColor, colors.textBright, 0.11)}`,
143
+ boxShadow: selected
144
+ ? `inset 0 0 0 1px ${blendHex(backgroundColor, colors.textBright, 0.55)}`
145
+ : hovered
146
+ ? `inset 0 0 0 1px ${blendHex(backgroundColor, colors.textBright, 0.18)}`
147
+ : "none",
148
+ backgroundColor,
149
+ color: textColor,
150
+ display: "flex",
151
+ flexDirection: "column",
152
+ alignItems: "stretch",
153
+ justifyContent: "flex-start",
154
+ gap: 2,
155
+ cursor: "pointer",
156
+ transition: "border-color 120ms ease, box-shadow 120ms ease, filter 120ms ease",
157
+ filter: hovered ? "brightness(1.08)" : undefined,
158
+ };
159
+ const textStyle: CSSProperties = {
160
+ display: "block",
161
+ minWidth: 0,
162
+ width: "100%",
163
+ overflow: "hidden",
164
+ textOverflow: "ellipsis",
165
+ whiteSpace: "nowrap",
166
+ lineHeight: 1.1,
167
+ letterSpacing: 0,
168
+ textShadow: `0 1px 1px ${blendHex(backgroundColor, colors.bg, 0.35)}`,
169
+ };
170
+ const startHover = () => {
171
+ onHover(true);
172
+ onSelect();
173
+ };
174
+
175
+ return (
176
+ <Box
177
+ data-gloom-role="metric-treemap-desktop-tile"
178
+ style={style}
179
+ onMouseDown={(event: PreventableMouseEvent) => {
180
+ event.preventDefault();
181
+ onSelect();
182
+ }}
183
+ onDoubleClick={onActivate}
184
+ onMouseOver={startHover}
185
+ onMouseMove={startHover}
186
+ onMouseOut={() => onHover(false)}
187
+ >
188
+ {canShowLabel && (
189
+ <Text
190
+ fg={textColor}
191
+ attributes={selected ? TextAttributes.BOLD : TextAttributes.NONE}
192
+ style={{
193
+ ...textStyle,
194
+ fontSize: isTiny ? 11 : 13,
195
+ fontWeight: 700,
196
+ }}
197
+ >
198
+ {lines[0]}
199
+ </Text>
200
+ )}
201
+ {canShowDetails && lines.slice(1, canShowExtra ? 4 : 3).map((line, index) => (
202
+ <Text key={`${tile.item.id}:detail:${index}`} fg={textColor} style={{ ...textStyle, fontSize: 12, fontWeight: 600 }}>
203
+ {line}
204
+ </Text>
205
+ ))}
206
+ </Box>
207
+ );
208
+ }
209
+
210
+ function DesktopMetricTreemapSurface<T>({ items, width, height, selectedId, onSelect, onActivate, cellAspect, emptyStateTitle }: MetricTreemapSurfaceProps<T> & {
211
+ cellAspect: number;
212
+ }) {
213
+ const [hoveredId, setHoveredId] = useState<string | null>(null);
214
+ const chartWidth = Math.max(1, width - 2);
215
+ const tiles = useMemo(() => buildMetricTreemapRects(items, chartWidth, height, cellAspect), [cellAspect, chartWidth, height, items]);
216
+
217
+ if (tiles.length === 0) {
218
+ return (
219
+ <Box width={width} height={height} paddingX={1} paddingY={1}>
220
+ <Text fg={colors.textDim}>{emptyStateTitle ?? "No chart data"}</Text>
221
+ </Box>
222
+ );
223
+ }
224
+
225
+ return (
226
+ <Box
227
+ width={width}
228
+ height={height}
229
+ paddingX={1}
230
+ style={{
231
+ backgroundColor: colors.bg,
232
+ overflow: "hidden",
233
+ }}
234
+ >
235
+ <Box
236
+ data-gloom-role="metric-treemap"
237
+ width={chartWidth}
238
+ height={height}
239
+ style={{
240
+ position: "relative",
241
+ width: "100%",
242
+ height: "100%",
243
+ backgroundColor: colors.bg,
244
+ overflow: "hidden",
245
+ }}
246
+ >
247
+ {tiles.map((tile) => (
248
+ <DesktopTile
249
+ key={tile.item.id}
250
+ tile={tile}
251
+ chartWidth={chartWidth}
252
+ chartHeight={height}
253
+ selected={tile.item.id === selectedId}
254
+ hovered={tile.item.id === hoveredId}
255
+ onSelect={() => onSelect(tile.item)}
256
+ onActivate={onActivate ? () => onActivate(tile.item) : undefined}
257
+ onHover={(isHovered) => setHoveredId((current) => (isHovered ? tile.item.id : current === tile.item.id ? null : current))}
258
+ />
259
+ ))}
260
+ </Box>
261
+ </Box>
262
+ );
263
+ }
264
+
265
+ export function MetricTreemapSurface<T>({
266
+ items,
267
+ width,
268
+ height,
269
+ selectedId,
270
+ onSelect,
271
+ onActivate,
272
+ emptyStateTitle,
273
+ }: MetricTreemapSurfaceProps<T>) {
274
+ const { cellWidthPx = 8, cellHeightPx = 18, nativePaneChrome } = useUiCapabilities();
275
+ const chartWidth = Math.max(1, width - 2);
276
+ const cellAspect = Math.max(0.5, Math.min(4, cellHeightPx / Math.max(1, cellWidthPx)));
277
+ const tiles = useMemo(() => buildMetricTreemap(items, chartWidth, height, cellAspect), [cellAspect, chartWidth, height, items]);
278
+
279
+ if (nativePaneChrome) {
280
+ return (
281
+ <DesktopMetricTreemapSurface
282
+ items={items}
283
+ width={width}
284
+ height={height}
285
+ selectedId={selectedId}
286
+ onSelect={onSelect}
287
+ onActivate={onActivate}
288
+ cellAspect={cellAspect}
289
+ emptyStateTitle={emptyStateTitle}
290
+ />
291
+ );
292
+ }
293
+
294
+ if (tiles.length === 0) {
295
+ return (
296
+ <Box width={width} height={height} paddingX={1} paddingY={1}>
297
+ <Text fg={colors.textDim}>{emptyStateTitle ?? "No chart data"}</Text>
298
+ </Box>
299
+ );
300
+ }
301
+
302
+ return (
303
+ <Box width={width} height={height} paddingX={1} backgroundColor={colors.bg}>
304
+ <Box position="relative" width={chartWidth} height={height} backgroundColor={colors.bg}>
305
+ {tiles.map((tile) => (
306
+ <Tile
307
+ key={tile.item.id}
308
+ tile={tile}
309
+ selected={tile.item.id === selectedId}
310
+ onSelect={() => onSelect(tile.item)}
311
+ onActivate={onActivate ? () => onActivate(tile.item) : undefined}
312
+ />
313
+ ))}
314
+ </Box>
315
+ </Box>
316
+ );
317
+ }
@@ -0,0 +1,77 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import {
3
+ buildMetricTreemap,
4
+ buildMetricTreemapRects,
5
+ findMetricTreemapNeighbor,
6
+ type MetricTreemapItem,
7
+ type MetricTreemapTile,
8
+ } from "./layout";
9
+
10
+ const ITEMS: Array<MetricTreemapItem<string>> = [
11
+ { id: "a", label: "A", weight: 3, data: "A" },
12
+ { id: "b", label: "B", weight: 1, data: "B" },
13
+ { id: "zero", label: "Zero", weight: 0, data: "Zero" },
14
+ ];
15
+
16
+ describe("metric treemap layout", () => {
17
+ test("lays out positive weighted items inside integer bounds", () => {
18
+ const tiles = buildMetricTreemap(ITEMS, 12, 6);
19
+
20
+ expect(tiles.map((tile) => tile.item.id)).toEqual(["a", "b"]);
21
+ for (const tile of tiles) {
22
+ expect(tile.x).toBeGreaterThanOrEqual(0);
23
+ expect(tile.y).toBeGreaterThanOrEqual(0);
24
+ expect(tile.width).toBeGreaterThan(0);
25
+ expect(tile.height).toBeGreaterThan(0);
26
+ expect(tile.x + tile.width).toBeLessThanOrEqual(12);
27
+ expect(tile.y + tile.height).toBeLessThanOrEqual(6);
28
+ }
29
+ });
30
+
31
+ test("produces proportional floating rects for native rendering", () => {
32
+ const tiles = buildMetricTreemapRects(ITEMS, 100, 40);
33
+ const first = tiles[0];
34
+ const second = tiles[1];
35
+
36
+ expect(first?.item.id).toBe("a");
37
+ expect(second?.item.id).toBe("b");
38
+ expect((first?.width ?? 0) * (first?.height ?? 0)).toBeGreaterThan((second?.width ?? 0) * (second?.height ?? 0));
39
+ });
40
+
41
+ test("drops low-ranked float tiles before they render as slivers", () => {
42
+ const denseItems = Array.from({ length: 96 }, (_, index): MetricTreemapItem<number> => ({
43
+ id: `item-${index}`,
44
+ label: `${index}`,
45
+ weight: 100 - index,
46
+ data: index,
47
+ }));
48
+
49
+ const tiles = buildMetricTreemapRects(denseItems, 52, 32, 18 / 8);
50
+
51
+ expect(tiles.length).toBeLessThan(denseItems.length);
52
+ expect(tiles.every((tile) => tile.width >= 3 && tile.height >= 1.8)).toBe(true);
53
+ });
54
+
55
+ test("finds neighbors by rendered geometry instead of source order", () => {
56
+ const tiles: Array<MetricTreemapTile<string>> = [
57
+ { item: { id: "a", label: "A", weight: 1, data: "a" }, x: 0, y: 0, width: 5, height: 5 },
58
+ { item: { id: "below", label: "Below", weight: 1, data: "below" }, x: 0, y: 5, width: 5, height: 5 },
59
+ { item: { id: "right", label: "Right", weight: 1, data: "right" }, x: 5, y: 0, width: 5, height: 5 },
60
+ ];
61
+
62
+ expect(findMetricTreemapNeighbor(tiles, "a", "right")?.item.id).toBe("right");
63
+ expect(findMetricTreemapNeighbor(tiles, "a", "down")?.item.id).toBe("below");
64
+ expect(findMetricTreemapNeighbor(tiles, "right", "left")?.item.id).toBe("a");
65
+ });
66
+
67
+ test("does not choose an overlapping lower tile for right navigation", () => {
68
+ const tiles: Array<MetricTreemapTile<string>> = [
69
+ { item: { id: "current", label: "Current", weight: 1, data: "current" }, x: 33, y: 0, width: 26, height: 10 },
70
+ { item: { id: "lower", label: "Lower", weight: 1, data: "lower" }, x: 49, y: 10, width: 12, height: 6 },
71
+ { item: { id: "right", label: "Right", weight: 1, data: "right" }, x: 59, y: 0, width: 24, height: 10 },
72
+ ];
73
+
74
+ expect(findMetricTreemapNeighbor(tiles, "current", "right")?.item.id).toBe("right");
75
+ expect(findMetricTreemapNeighbor(tiles, "current", "down")?.item.id).toBe("lower");
76
+ });
77
+ });