gloomberb 0.9.5 → 0.9.7
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/README.md +49 -8
- package/package.json +2 -2
- package/src/app/global-shortcuts.test.tsx +20 -1
- package/src/app/global-shortcuts.ts +2 -0
- package/src/app/pane-runtime/index.tsx +11 -4
- package/src/app/pane-runtime/pane-settings-runtime.tsx +1 -1
- package/src/app.tsx +41 -17
- package/src/cli/commands/remote.ts +206 -0
- package/src/cli/index.ts +2 -0
- package/src/components/chart/core/keyboard.ts +10 -1
- package/src/components/chart/static/bar-chart-surface.test.tsx +4 -0
- package/src/components/chart/static/chart/surface.test.tsx +59 -4
- package/src/components/chart/static/chart/surface.tsx +4 -0
- package/src/components/chart/stock-chart/index.test.ts +7 -0
- package/src/components/command-bar/list/view.tsx +47 -0
- package/src/components/command-bar/multi-select-picker.tsx +7 -0
- package/src/components/command-bar/routing/navigation-state.ts +10 -1
- package/src/components/command-bar/surface/index.test.tsx +14 -0
- package/src/components/command-bar/theme-picker.tsx +91 -79
- package/src/components/command-bar/workflow/field-row.tsx +30 -5
- package/src/components/index.ts +7 -0
- package/src/components/layout/pane/footer/index.tsx +23 -0
- package/src/components/layout/shell/index.test.tsx +2 -0
- package/src/components/layout/shell/index.tsx +8 -3
- package/src/components/layout/shell/pane/actions.ts +21 -7
- package/src/components/metric-treemap/index.tsx +317 -0
- package/src/components/metric-treemap/layout.test.ts +77 -0
- package/src/components/metric-treemap/layout.ts +476 -0
- package/src/components/toggle-list.tsx +10 -0
- package/src/components/ui/button.tsx +12 -0
- package/src/components/ui/data-table/index.tsx +51 -0
- package/src/components/ui/fields.tsx +18 -0
- package/src/components/ui/list-view.tsx +83 -3
- package/src/components/ui/message-composer.tsx +22 -0
- package/src/components/ui/tabs.tsx +45 -0
- package/src/core/state/app/layout-reducer.ts +7 -1
- package/src/core/state/app/layout.ts +6 -0
- package/src/core/state/app/state.ts +23 -1
- package/src/core/state/app/types.ts +4 -2
- package/src/pane-settings.test.ts +1 -0
- package/src/plugins/builtin/chat/channels.ts +5 -18
- package/src/plugins/builtin/chat/chat.test.tsx +1 -7
- package/src/plugins/builtin/chat/status-widget.tsx +1 -7
- package/src/plugins/builtin/cloud/plugin.tsx +4 -13
- package/src/plugins/builtin/holders/treemap.tsx +28 -297
- package/src/plugins/builtin/holders/types.ts +0 -34
- package/src/plugins/builtin/market-heatmap/data.test.ts +121 -0
- package/src/plugins/builtin/market-heatmap/data.ts +296 -0
- package/src/plugins/builtin/market-heatmap/index.tsx +320 -0
- package/src/plugins/builtin/plugin-groups.ts +2 -0
- package/src/plugins/builtin/portfolio-list/grid.tsx +197 -0
- package/src/plugins/builtin/portfolio-list/index.test.ts +52 -1
- package/src/plugins/builtin/portfolio-list/index.tsx +1 -0
- package/src/plugins/builtin/portfolio-list/pane/data.ts +15 -1
- package/src/plugins/builtin/portfolio-list/pane/index.test.tsx +149 -0
- package/src/plugins/builtin/portfolio-list/pane/index.tsx +67 -26
- package/src/plugins/builtin/portfolio-list/pane/streaming.ts +54 -4
- package/src/plugins/builtin/portfolio-list/settings.ts +36 -1
- package/src/plugins/ibkr/gateway/service/index.test.ts +22 -0
- package/src/plugins/ibkr/gateway/service/index.ts +6 -9
- package/src/plugins/ibkr/gateway/service/native-loader.ts +12 -0
- package/src/remote/app-host.tsx +77 -0
- package/src/remote/client.ts +34 -0
- package/src/remote/command-bar.ts +98 -0
- package/src/remote/controller-utils.ts +111 -0
- package/src/remote/controller.test.ts +365 -0
- package/src/remote/controller.ts +434 -0
- package/src/remote/endpoint.ts +41 -0
- package/src/remote/json-patch.test.ts +31 -0
- package/src/remote/json-patch.ts +89 -0
- package/src/remote/layout-helpers.ts +62 -0
- package/src/remote/resources.ts +186 -0
- package/src/remote/revision.ts +18 -0
- package/src/remote/schema.ts +143 -0
- package/src/remote/semantic-helpers.ts +161 -0
- package/src/remote/semantic-tree.tsx +112 -0
- package/src/remote/server.test.ts +50 -0
- package/src/remote/server.ts +121 -0
- package/src/remote/types.ts +119 -0
- package/src/renderers/electrobun/bun/index.ts +55 -1
- package/src/renderers/electrobun/shared/protocol.ts +11 -1
- package/src/renderers/electrobun/view/backend-rpc.ts +28 -1
- package/src/renderers/electrobun/view/build-assets.ts +1 -0
- package/src/renderers/electrobun/view/host/box.tsx +5 -0
- package/src/renderers/electrobun/view/host/mouse.test.ts +37 -0
- package/src/renderers/electrobun/view/host/mouse.ts +2 -0
- package/src/renderers/electrobun/view/main.tsx +5 -1
- package/src/renderers/electrobun/view/native-stubs/ibkr-native-loader.ts +3 -0
- package/src/renderers/opentui/chart-surface.test.tsx +101 -0
- package/src/renderers/opentui/chart-surface.tsx +15 -20
- package/src/renderers/opentui/start.tsx +32 -0
- package/src/sources/yahoo-finance/http.ts +31 -4
- package/src/state/app/context/index.test.tsx +47 -0
- package/src/state/app/context/index.tsx +48 -18
- package/src/types/config.ts +1 -0
- package/src/ui/host.tsx +1 -0
- package/src/ui/index.tsx +183 -7
- package/src/version.ts +1 -1
- package/src/plugins/builtin/holders/treemap-layout.ts +0 -231
|
@@ -0,0 +1,476 @@
|
|
|
1
|
+
export interface MetricTreemapItem<T = unknown> {
|
|
2
|
+
id: string;
|
|
3
|
+
label: string;
|
|
4
|
+
weight: number | null | undefined;
|
|
5
|
+
colorValue?: number | null;
|
|
6
|
+
primaryText?: string | null;
|
|
7
|
+
secondaryText?: string | null;
|
|
8
|
+
tertiaryText?: string | null;
|
|
9
|
+
data: T;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface MetricTreemapTile<T = unknown> {
|
|
13
|
+
item: MetricTreemapItem<T>;
|
|
14
|
+
x: number;
|
|
15
|
+
y: number;
|
|
16
|
+
width: number;
|
|
17
|
+
height: number;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface FloatMetricTreemapTile<T = unknown> {
|
|
21
|
+
item: MetricTreemapItem<T>;
|
|
22
|
+
x: number;
|
|
23
|
+
y: number;
|
|
24
|
+
width: number;
|
|
25
|
+
height: number;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export type MetricTreemapDirection = "left" | "right" | "up" | "down";
|
|
29
|
+
export type MetricTreemapLayoutMode = "integer" | "float";
|
|
30
|
+
|
|
31
|
+
export interface MetricTreemapLayoutOptions {
|
|
32
|
+
maxItems?: number;
|
|
33
|
+
minTileWidth?: number;
|
|
34
|
+
minTileHeight?: number;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
interface WeightedTreemapItem<T> {
|
|
38
|
+
item: MetricTreemapItem<T>;
|
|
39
|
+
weight: number;
|
|
40
|
+
area: number;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
interface TreemapGroup<T> {
|
|
44
|
+
items: WeightedTreemapItem<T>[];
|
|
45
|
+
weight: number;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
interface FloatRect {
|
|
49
|
+
x: number;
|
|
50
|
+
y: number;
|
|
51
|
+
width: number;
|
|
52
|
+
height: number;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const MAX_TREEMAP_ITEMS = 160;
|
|
56
|
+
const DEFAULT_MIN_TILE_WIDTH = 3;
|
|
57
|
+
const DEFAULT_MIN_TILE_HEIGHT = 1.8;
|
|
58
|
+
|
|
59
|
+
function itemWeight(item: MetricTreemapItem): number {
|
|
60
|
+
return Math.max(item.weight ?? 0, 0);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function worstAspectRatio<T>(items: WeightedTreemapItem<T>[], sideLength: number): number {
|
|
64
|
+
if (items.length === 0 || sideLength <= 0) return Number.POSITIVE_INFINITY;
|
|
65
|
+
const areaSum = items.reduce((sum, item) => sum + item.area, 0);
|
|
66
|
+
const minArea = Math.min(...items.map((item) => item.area));
|
|
67
|
+
const maxArea = Math.max(...items.map((item) => item.area));
|
|
68
|
+
if (areaSum <= 0 || minArea <= 0) return Number.POSITIVE_INFINITY;
|
|
69
|
+
|
|
70
|
+
const sideSquared = sideLength * sideLength;
|
|
71
|
+
return Math.max(
|
|
72
|
+
(sideSquared * maxArea) / (areaSum * areaSum),
|
|
73
|
+
(areaSum * areaSum) / (sideSquared * minArea),
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function layoutFloatGroup<T>(items: WeightedTreemapItem<T>[], rect: FloatRect): FloatRect {
|
|
78
|
+
const areaSum = items.reduce((sum, item) => sum + item.area, 0);
|
|
79
|
+
if (areaSum <= 0 || rect.width <= 0 || rect.height <= 0) return rect;
|
|
80
|
+
|
|
81
|
+
if (rect.width >= rect.height) {
|
|
82
|
+
const columnWidth = Math.min(rect.width, areaSum / rect.height);
|
|
83
|
+
return {
|
|
84
|
+
x: rect.x + columnWidth,
|
|
85
|
+
y: rect.y,
|
|
86
|
+
width: Math.max(0, rect.width - columnWidth),
|
|
87
|
+
height: rect.height,
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const rowHeight = Math.min(rect.height, areaSum / rect.width);
|
|
92
|
+
return {
|
|
93
|
+
x: rect.x,
|
|
94
|
+
y: rect.y + rowHeight,
|
|
95
|
+
width: rect.width,
|
|
96
|
+
height: Math.max(0, rect.height - rowHeight),
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function buildSquarifiedGroups<T>(items: WeightedTreemapItem<T>[], width: number, height: number): TreemapGroup<T>[] {
|
|
101
|
+
const groups: TreemapGroup<T>[] = [];
|
|
102
|
+
let rect: FloatRect = { x: 0, y: 0, width, height };
|
|
103
|
+
let index = 0;
|
|
104
|
+
|
|
105
|
+
while (index < items.length && rect.width > 0 && rect.height > 0) {
|
|
106
|
+
const group: WeightedTreemapItem<T>[] = [];
|
|
107
|
+
let currentWorst = Number.POSITIVE_INFINITY;
|
|
108
|
+
const sideLength = Math.min(rect.width, rect.height);
|
|
109
|
+
|
|
110
|
+
while (index < items.length) {
|
|
111
|
+
const candidate = items[index]!;
|
|
112
|
+
const nextGroup = [...group, candidate];
|
|
113
|
+
const nextWorst = worstAspectRatio(nextGroup, sideLength);
|
|
114
|
+
if (group.length > 0 && nextWorst > currentWorst) break;
|
|
115
|
+
group.push(candidate);
|
|
116
|
+
currentWorst = nextWorst;
|
|
117
|
+
index += 1;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
groups.push({
|
|
121
|
+
items: group,
|
|
122
|
+
weight: group.reduce((sum, item) => sum + item.weight, 0),
|
|
123
|
+
});
|
|
124
|
+
rect = layoutFloatGroup(group, rect);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return groups;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function normalizeWeightedItems<T>(
|
|
131
|
+
items: MetricTreemapItem<T>[],
|
|
132
|
+
width: number,
|
|
133
|
+
height: number,
|
|
134
|
+
cellAspect: number,
|
|
135
|
+
maxItems: number,
|
|
136
|
+
): WeightedTreemapItem<T>[] {
|
|
137
|
+
const weightedItems = items
|
|
138
|
+
.map((item) => ({ item, weight: itemWeight(item) }))
|
|
139
|
+
.filter((item) => item.weight > 0)
|
|
140
|
+
.slice(0, maxItems);
|
|
141
|
+
const totalWeight = weightedItems.reduce((sum, item) => sum + item.weight, 0);
|
|
142
|
+
if (totalWeight <= 0) return [];
|
|
143
|
+
|
|
144
|
+
const normalizedHeight = height * cellAspect;
|
|
145
|
+
const totalArea = width * normalizedHeight;
|
|
146
|
+
return weightedItems.map((item) => ({
|
|
147
|
+
...item,
|
|
148
|
+
area: item.weight / totalWeight * totalArea,
|
|
149
|
+
}));
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function maxItemsForLayout(width: number, height: number, options?: MetricTreemapLayoutOptions): number {
|
|
153
|
+
const requested = options?.maxItems ?? width * height;
|
|
154
|
+
return Math.max(1, Math.min(MAX_TREEMAP_ITEMS, Math.floor(requested)));
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function isTooSmall(
|
|
158
|
+
tile: { width: number; height: number },
|
|
159
|
+
options: MetricTreemapLayoutOptions | undefined,
|
|
160
|
+
defaultMinTileWidth: number,
|
|
161
|
+
defaultMinTileHeight: number,
|
|
162
|
+
): boolean {
|
|
163
|
+
const minTileWidth = options?.minTileWidth ?? defaultMinTileWidth;
|
|
164
|
+
const minTileHeight = options?.minTileHeight ?? defaultMinTileHeight;
|
|
165
|
+
return tile.width < minTileWidth || tile.height < minTileHeight;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function allocateLengths(totalLength: number, weights: number[]): number[] {
|
|
169
|
+
if (weights.length === 0 || totalLength <= 0) return [];
|
|
170
|
+
const totalWeight = weights.reduce((sum, weight) => sum + weight, 0);
|
|
171
|
+
let remainingLength = totalLength;
|
|
172
|
+
let remainingWeight = totalWeight;
|
|
173
|
+
|
|
174
|
+
return weights.map((weight, index) => {
|
|
175
|
+
const remainingItems = weights.length - index;
|
|
176
|
+
if (remainingItems === 1) return remainingLength;
|
|
177
|
+
const ideal = remainingWeight > 0 ? Math.round(remainingLength * weight / remainingWeight) : 1;
|
|
178
|
+
const length = Math.max(1, Math.min(remainingLength - (remainingItems - 1), ideal));
|
|
179
|
+
remainingLength -= length;
|
|
180
|
+
remainingWeight -= weight;
|
|
181
|
+
return length;
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function buildMetricTreemapRectsForLimit<T>(
|
|
186
|
+
items: MetricTreemapItem<T>[],
|
|
187
|
+
width: number,
|
|
188
|
+
height: number,
|
|
189
|
+
cellAspect = 1,
|
|
190
|
+
maxItems = maxItemsForLayout(width, height),
|
|
191
|
+
): FloatMetricTreemapTile<T>[] {
|
|
192
|
+
if (width <= 0 || height <= 0) return [];
|
|
193
|
+
const normalizedCellAspect = Math.max(0.25, cellAspect);
|
|
194
|
+
const weightedItems = normalizeWeightedItems(items, width, height, normalizedCellAspect, maxItems);
|
|
195
|
+
if (weightedItems.length === 0) return [];
|
|
196
|
+
|
|
197
|
+
const normalizedHeight = height * normalizedCellAspect;
|
|
198
|
+
const groups = buildSquarifiedGroups(weightedItems, width, normalizedHeight);
|
|
199
|
+
const tiles: FloatMetricTreemapTile<T>[] = [];
|
|
200
|
+
let rect: FloatRect = { x: 0, y: 0, width, height: normalizedHeight };
|
|
201
|
+
|
|
202
|
+
for (const group of groups) {
|
|
203
|
+
const areaSum = group.items.reduce((sum, item) => sum + item.area, 0);
|
|
204
|
+
if (areaSum <= 0 || rect.width <= 0 || rect.height <= 0) break;
|
|
205
|
+
|
|
206
|
+
if (rect.width >= rect.height) {
|
|
207
|
+
const columnWidth = Math.min(rect.width, areaSum / rect.height);
|
|
208
|
+
let itemY = rect.y;
|
|
209
|
+
for (const item of group.items) {
|
|
210
|
+
const itemHeight = Math.min(rect.y + rect.height - itemY, item.area / columnWidth);
|
|
211
|
+
tiles.push({
|
|
212
|
+
item: item.item,
|
|
213
|
+
x: rect.x,
|
|
214
|
+
y: itemY / normalizedCellAspect,
|
|
215
|
+
width: columnWidth,
|
|
216
|
+
height: itemHeight / normalizedCellAspect,
|
|
217
|
+
});
|
|
218
|
+
itemY += itemHeight;
|
|
219
|
+
}
|
|
220
|
+
rect = {
|
|
221
|
+
x: rect.x + columnWidth,
|
|
222
|
+
y: rect.y,
|
|
223
|
+
width: Math.max(0, rect.width - columnWidth),
|
|
224
|
+
height: rect.height,
|
|
225
|
+
};
|
|
226
|
+
} else {
|
|
227
|
+
const rowHeight = Math.min(rect.height, areaSum / rect.width);
|
|
228
|
+
let itemX = rect.x;
|
|
229
|
+
for (const item of group.items) {
|
|
230
|
+
const itemWidth = Math.min(rect.x + rect.width - itemX, item.area / rowHeight);
|
|
231
|
+
tiles.push({
|
|
232
|
+
item: item.item,
|
|
233
|
+
x: itemX,
|
|
234
|
+
y: rect.y / normalizedCellAspect,
|
|
235
|
+
width: itemWidth,
|
|
236
|
+
height: rowHeight / normalizedCellAspect,
|
|
237
|
+
});
|
|
238
|
+
itemX += itemWidth;
|
|
239
|
+
}
|
|
240
|
+
rect = {
|
|
241
|
+
x: rect.x,
|
|
242
|
+
y: rect.y + rowHeight,
|
|
243
|
+
width: rect.width,
|
|
244
|
+
height: Math.max(0, rect.height - rowHeight),
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
return tiles.filter((tile) => tile.width > 0 && tile.height > 0);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
export function buildMetricTreemapRects<T>(
|
|
253
|
+
items: MetricTreemapItem<T>[],
|
|
254
|
+
width: number,
|
|
255
|
+
height: number,
|
|
256
|
+
cellAspect = 1,
|
|
257
|
+
options?: MetricTreemapLayoutOptions,
|
|
258
|
+
): FloatMetricTreemapTile<T>[] {
|
|
259
|
+
let limit = maxItemsForLayout(width, height, options);
|
|
260
|
+
let lastTiles: FloatMetricTreemapTile<T>[] = [];
|
|
261
|
+
|
|
262
|
+
while (limit > 0) {
|
|
263
|
+
const tiles = buildMetricTreemapRectsForLimit(items, width, height, cellAspect, limit);
|
|
264
|
+
lastTiles = tiles;
|
|
265
|
+
const firstSmallIndex = tiles.findIndex((tile) => isTooSmall(tile, options, DEFAULT_MIN_TILE_WIDTH, DEFAULT_MIN_TILE_HEIGHT));
|
|
266
|
+
if (firstSmallIndex < 0) return tiles;
|
|
267
|
+
limit = Math.min(limit - 1, firstSmallIndex);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
return lastTiles.filter((tile) => !isTooSmall(tile, options, DEFAULT_MIN_TILE_WIDTH, DEFAULT_MIN_TILE_HEIGHT));
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
function buildMetricTreemapForLimit<T>(
|
|
274
|
+
items: MetricTreemapItem<T>[],
|
|
275
|
+
width: number,
|
|
276
|
+
height: number,
|
|
277
|
+
cellAspect = 1,
|
|
278
|
+
maxItems = maxItemsForLayout(width, height),
|
|
279
|
+
): MetricTreemapTile<T>[] {
|
|
280
|
+
if (width <= 0 || height <= 0) return [];
|
|
281
|
+
const normalizedCellAspect = Math.max(0.25, cellAspect);
|
|
282
|
+
const weightedItems = normalizeWeightedItems(items, width, height, normalizedCellAspect, maxItems);
|
|
283
|
+
if (weightedItems.length === 0) return [];
|
|
284
|
+
|
|
285
|
+
const normalizedHeight = height * normalizedCellAspect;
|
|
286
|
+
const groups = buildSquarifiedGroups(weightedItems, width, normalizedHeight);
|
|
287
|
+
const tiles: MetricTreemapTile<T>[] = [];
|
|
288
|
+
|
|
289
|
+
let x = 0;
|
|
290
|
+
let y = 0;
|
|
291
|
+
let remainingWidth = width;
|
|
292
|
+
let remainingHeight = height;
|
|
293
|
+
let remainingWeight = groups.reduce((sum, group) => sum + group.weight, 0);
|
|
294
|
+
|
|
295
|
+
groups.forEach((group, groupIndex) => {
|
|
296
|
+
if (remainingWidth <= 0 || remainingHeight <= 0) return;
|
|
297
|
+
const isLastGroup = groupIndex === groups.length - 1;
|
|
298
|
+
const remainingNormalizedHeight = remainingHeight * normalizedCellAspect;
|
|
299
|
+
|
|
300
|
+
if (remainingWidth >= remainingNormalizedHeight) {
|
|
301
|
+
const columnWidth = isLastGroup
|
|
302
|
+
? remainingWidth
|
|
303
|
+
: Math.max(1, Math.min(remainingWidth - 1, Math.round(remainingWidth * group.weight / remainingWeight)));
|
|
304
|
+
const heights = allocateLengths(remainingHeight, group.items.map((item) => item.weight));
|
|
305
|
+
let tileY = y;
|
|
306
|
+
group.items.forEach((item, itemIndex) => {
|
|
307
|
+
const tileHeight = heights[itemIndex] ?? 0;
|
|
308
|
+
if (tileHeight > 0) {
|
|
309
|
+
tiles.push({ item: item.item, x, y: tileY, width: columnWidth, height: tileHeight });
|
|
310
|
+
}
|
|
311
|
+
tileY += tileHeight;
|
|
312
|
+
});
|
|
313
|
+
x += columnWidth;
|
|
314
|
+
remainingWidth -= columnWidth;
|
|
315
|
+
} else {
|
|
316
|
+
const rowHeight = isLastGroup
|
|
317
|
+
? remainingHeight
|
|
318
|
+
: Math.max(1, Math.min(remainingHeight - 1, Math.round(remainingHeight * group.weight / remainingWeight)));
|
|
319
|
+
const widths = allocateLengths(remainingWidth, group.items.map((item) => item.weight));
|
|
320
|
+
let tileX = x;
|
|
321
|
+
group.items.forEach((item, itemIndex) => {
|
|
322
|
+
const tileWidth = widths[itemIndex] ?? 0;
|
|
323
|
+
if (tileWidth > 0) {
|
|
324
|
+
tiles.push({ item: item.item, x: tileX, y, width: tileWidth, height: rowHeight });
|
|
325
|
+
}
|
|
326
|
+
tileX += tileWidth;
|
|
327
|
+
});
|
|
328
|
+
y += rowHeight;
|
|
329
|
+
remainingHeight -= rowHeight;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
remainingWeight -= group.weight;
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
return tiles.filter((tile) => tile.width > 0 && tile.height > 0);
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
export function buildMetricTreemap<T>(
|
|
339
|
+
items: MetricTreemapItem<T>[],
|
|
340
|
+
width: number,
|
|
341
|
+
height: number,
|
|
342
|
+
cellAspect = 1,
|
|
343
|
+
options?: MetricTreemapLayoutOptions,
|
|
344
|
+
): MetricTreemapTile<T>[] {
|
|
345
|
+
let limit = maxItemsForLayout(width, height, options);
|
|
346
|
+
let lastTiles: MetricTreemapTile<T>[] = [];
|
|
347
|
+
|
|
348
|
+
while (limit > 0) {
|
|
349
|
+
const tiles = buildMetricTreemapForLimit(items, width, height, cellAspect, limit);
|
|
350
|
+
lastTiles = tiles;
|
|
351
|
+
const firstSmallIndex = tiles.findIndex((tile) => isTooSmall(tile, options, 2, 1));
|
|
352
|
+
if (firstSmallIndex < 0) return tiles;
|
|
353
|
+
limit = Math.min(limit - 1, firstSmallIndex);
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
return lastTiles.filter((tile) => !isTooSmall(tile, options, 2, 1));
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
function center(rect: { x: number; y: number; width: number; height: number }) {
|
|
360
|
+
return {
|
|
361
|
+
x: rect.x + rect.width / 2,
|
|
362
|
+
y: rect.y + rect.height / 2,
|
|
363
|
+
};
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
function rangeGap(startA: number, endA: number, startB: number, endB: number): number {
|
|
367
|
+
if (endA < startB) return startB - endA;
|
|
368
|
+
if (endB < startA) return startA - endB;
|
|
369
|
+
return 0;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
function primaryGap(
|
|
373
|
+
current: { x: number; y: number; width: number; height: number },
|
|
374
|
+
candidate: { x: number; y: number; width: number; height: number },
|
|
375
|
+
direction: MetricTreemapDirection,
|
|
376
|
+
): number {
|
|
377
|
+
switch (direction) {
|
|
378
|
+
case "left":
|
|
379
|
+
return Math.max(0, current.x - (candidate.x + candidate.width));
|
|
380
|
+
case "right":
|
|
381
|
+
return Math.max(0, candidate.x - (current.x + current.width));
|
|
382
|
+
case "up":
|
|
383
|
+
return Math.max(0, current.y - (candidate.y + candidate.height));
|
|
384
|
+
case "down":
|
|
385
|
+
return Math.max(0, candidate.y - (current.y + current.height));
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
function perpendicularGap(
|
|
390
|
+
current: { x: number; y: number; width: number; height: number },
|
|
391
|
+
candidate: { x: number; y: number; width: number; height: number },
|
|
392
|
+
direction: MetricTreemapDirection,
|
|
393
|
+
): number {
|
|
394
|
+
if (direction === "left" || direction === "right") {
|
|
395
|
+
return rangeGap(current.y, current.y + current.height, candidate.y, candidate.y + candidate.height);
|
|
396
|
+
}
|
|
397
|
+
return rangeGap(current.x, current.x + current.width, candidate.x, candidate.x + candidate.width);
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
function isEdgeCandidate(
|
|
401
|
+
current: { x: number; y: number; width: number; height: number },
|
|
402
|
+
candidate: { x: number; y: number; width: number; height: number },
|
|
403
|
+
direction: MetricTreemapDirection,
|
|
404
|
+
): boolean {
|
|
405
|
+
const epsilon = 0.0001;
|
|
406
|
+
switch (direction) {
|
|
407
|
+
case "left":
|
|
408
|
+
return candidate.x + candidate.width <= current.x + epsilon;
|
|
409
|
+
case "right":
|
|
410
|
+
return candidate.x >= current.x + current.width - epsilon;
|
|
411
|
+
case "up":
|
|
412
|
+
return candidate.y + candidate.height <= current.y + epsilon;
|
|
413
|
+
case "down":
|
|
414
|
+
return candidate.y >= current.y + current.height - epsilon;
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
function isCenterCandidate(
|
|
419
|
+
currentCenter: { x: number; y: number },
|
|
420
|
+
candidateCenter: { x: number; y: number },
|
|
421
|
+
direction: MetricTreemapDirection,
|
|
422
|
+
): boolean {
|
|
423
|
+
switch (direction) {
|
|
424
|
+
case "left":
|
|
425
|
+
return candidateCenter.x < currentCenter.x;
|
|
426
|
+
case "right":
|
|
427
|
+
return candidateCenter.x > currentCenter.x;
|
|
428
|
+
case "up":
|
|
429
|
+
return candidateCenter.y < currentCenter.y;
|
|
430
|
+
case "down":
|
|
431
|
+
return candidateCenter.y > currentCenter.y;
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
export function findMetricTreemapNeighbor<T>(
|
|
436
|
+
tiles: Array<MetricTreemapTile<T> | FloatMetricTreemapTile<T>>,
|
|
437
|
+
selectedId: string | null,
|
|
438
|
+
direction: MetricTreemapDirection,
|
|
439
|
+
): MetricTreemapTile<T> | FloatMetricTreemapTile<T> | null {
|
|
440
|
+
if (tiles.length === 0) return null;
|
|
441
|
+
const current = tiles.find((tile) => tile.item.id === selectedId) ?? tiles[0]!;
|
|
442
|
+
const currentCenter = center(current);
|
|
443
|
+
let best: { tile: MetricTreemapTile<T> | FloatMetricTreemapTile<T>; score: number } | null = null;
|
|
444
|
+
const hasEdgeCandidate = tiles.some((tile) => tile.item.id !== current.item.id && isEdgeCandidate(current, tile, direction));
|
|
445
|
+
|
|
446
|
+
for (const tile of tiles) {
|
|
447
|
+
if (tile.item.id === current.item.id) continue;
|
|
448
|
+
const candidateCenter = center(tile);
|
|
449
|
+
const isCandidate = hasEdgeCandidate
|
|
450
|
+
? isEdgeCandidate(current, tile, direction)
|
|
451
|
+
: isCenterCandidate(currentCenter, candidateCenter, direction);
|
|
452
|
+
if (!isCandidate) continue;
|
|
453
|
+
|
|
454
|
+
const centerDistance = direction === "left" || direction === "right"
|
|
455
|
+
? Math.abs(candidateCenter.x - currentCenter.x)
|
|
456
|
+
: Math.abs(candidateCenter.y - currentCenter.y);
|
|
457
|
+
const score = primaryGap(current, tile, direction) * 1000
|
|
458
|
+
+ perpendicularGap(current, tile, direction) * 100
|
|
459
|
+
+ centerDistance;
|
|
460
|
+
if (!best || score < best.score) best = { tile, score };
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
return best?.tile ?? null;
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
export function buildMetricTreemapNavigationTiles<T>(
|
|
467
|
+
items: MetricTreemapItem<T>[],
|
|
468
|
+
width: number,
|
|
469
|
+
height: number,
|
|
470
|
+
cellAspect: number,
|
|
471
|
+
mode: MetricTreemapLayoutMode,
|
|
472
|
+
): Array<MetricTreemapTile<T> | FloatMetricTreemapTile<T>> {
|
|
473
|
+
return mode === "float"
|
|
474
|
+
? buildMetricTreemapRects(items, width, height, cellAspect)
|
|
475
|
+
: buildMetricTreemap(items, width, height, cellAspect);
|
|
476
|
+
}
|
|
@@ -26,6 +26,9 @@ export interface ToggleListProps {
|
|
|
26
26
|
rowGap?: number;
|
|
27
27
|
rowHeight?: number;
|
|
28
28
|
surface?: "framed" | "plain";
|
|
29
|
+
remoteLabel?: string;
|
|
30
|
+
remoteScope?: string;
|
|
31
|
+
remoteMetadata?: Record<string, unknown>;
|
|
29
32
|
}
|
|
30
33
|
|
|
31
34
|
function DesktopToggleRow({
|
|
@@ -124,6 +127,9 @@ export function ToggleList({
|
|
|
124
127
|
rowGap,
|
|
125
128
|
rowHeight,
|
|
126
129
|
surface,
|
|
130
|
+
remoteLabel,
|
|
131
|
+
remoteScope,
|
|
132
|
+
remoteMetadata,
|
|
127
133
|
}: ToggleListProps) {
|
|
128
134
|
const isDesktopWeb = useUiHost().kind === "desktop-web";
|
|
129
135
|
const listItems: ListViewItem[] = items.map((item) => ({
|
|
@@ -145,6 +151,10 @@ export function ToggleList({
|
|
|
145
151
|
rowGap={rowGap ?? (isDesktopWeb ? 0 : undefined)}
|
|
146
152
|
rowHeight={rowHeight}
|
|
147
153
|
surface={surface}
|
|
154
|
+
remoteLabel={remoteLabel}
|
|
155
|
+
remoteScope={remoteScope}
|
|
156
|
+
remoteMetadata={remoteMetadata}
|
|
157
|
+
remoteItemKind="toggle"
|
|
148
158
|
onSelect={onSelect}
|
|
149
159
|
onActivate={(item) => {
|
|
150
160
|
onToggle?.(item.id);
|
|
@@ -2,6 +2,7 @@ import { Box, Text, useUiHost } from "../../ui";
|
|
|
2
2
|
import { TextAttributes } from "../../ui";
|
|
3
3
|
import { type ComponentType } from "react";
|
|
4
4
|
import { colors } from "../../theme/colors";
|
|
5
|
+
import { useRemoteUiNode } from "../../remote/semantic-tree";
|
|
5
6
|
|
|
6
7
|
export type ButtonVariant = "primary" | "secondary" | "ghost" | "danger";
|
|
7
8
|
|
|
@@ -45,6 +46,17 @@ export function Button({
|
|
|
45
46
|
shortcut,
|
|
46
47
|
width,
|
|
47
48
|
}: ButtonProps) {
|
|
49
|
+
useRemoteUiNode({
|
|
50
|
+
role: "button",
|
|
51
|
+
label,
|
|
52
|
+
disabled,
|
|
53
|
+
actions: {
|
|
54
|
+
press: () => {
|
|
55
|
+
if (!disabled) onPress?.();
|
|
56
|
+
},
|
|
57
|
+
},
|
|
58
|
+
metadata: { variant, active, shortcut },
|
|
59
|
+
});
|
|
48
60
|
const HostButton = useUiHost().Button as ComponentType<ButtonProps> | undefined;
|
|
49
61
|
if (HostButton) {
|
|
50
62
|
return (
|
|
@@ -5,6 +5,8 @@ import type {
|
|
|
5
5
|
DataTableColumn,
|
|
6
6
|
DataTableProps,
|
|
7
7
|
} from "./types";
|
|
8
|
+
import { useRemoteUiNode } from "../../../remote/semantic-tree";
|
|
9
|
+
import { resolveRemoteItemIndex } from "../../../remote/semantic-helpers";
|
|
8
10
|
|
|
9
11
|
export type {
|
|
10
12
|
DataTableCell,
|
|
@@ -16,6 +18,46 @@ export type {
|
|
|
16
18
|
export function DataTable<T, C extends DataTableColumn = DataTableColumn>(
|
|
17
19
|
props: DataTableProps<T, C>,
|
|
18
20
|
) {
|
|
21
|
+
useRemoteUiNode({
|
|
22
|
+
role: "table",
|
|
23
|
+
label: "Data table",
|
|
24
|
+
actions: {
|
|
25
|
+
selectRow: (input) => {
|
|
26
|
+
const index = resolveTableIndex(input, props);
|
|
27
|
+
const item = index >= 0 ? props.items[index] : undefined;
|
|
28
|
+
if (item) props.onSelect(item, index);
|
|
29
|
+
},
|
|
30
|
+
activateRow: (input) => {
|
|
31
|
+
const index = resolveTableIndex(input, props);
|
|
32
|
+
const item = index >= 0 ? props.items[index] : undefined;
|
|
33
|
+
if (item) {
|
|
34
|
+
props.onSelect(item, index);
|
|
35
|
+
props.onActivate?.(item, index);
|
|
36
|
+
}
|
|
37
|
+
},
|
|
38
|
+
sort: (input) => {
|
|
39
|
+
const columnId = typeof input === "string"
|
|
40
|
+
? input
|
|
41
|
+
: input && typeof input === "object" && typeof (input as { columnId?: unknown }).columnId === "string"
|
|
42
|
+
? (input as { columnId: string }).columnId
|
|
43
|
+
: null;
|
|
44
|
+
if (columnId && props.columns.some((column) => column.id === columnId)) {
|
|
45
|
+
props.onHeaderClick(columnId);
|
|
46
|
+
}
|
|
47
|
+
},
|
|
48
|
+
},
|
|
49
|
+
metadata: {
|
|
50
|
+
sortColumnId: props.sortColumnId,
|
|
51
|
+
sortDirection: props.sortDirection,
|
|
52
|
+
columns: props.columns.map((column) => ({ id: column.id, label: column.label })),
|
|
53
|
+
rows: props.items.slice(0, 200).map((item, index) => ({
|
|
54
|
+
index,
|
|
55
|
+
key: props.getItemKey(item, index),
|
|
56
|
+
selected: props.isSelected(item, index),
|
|
57
|
+
})),
|
|
58
|
+
rowCount: props.items.length,
|
|
59
|
+
},
|
|
60
|
+
});
|
|
19
61
|
const HostDataTable = useUiHost().DataTable as
|
|
20
62
|
| ComponentType<DataTableProps<T, C>>
|
|
21
63
|
| undefined;
|
|
@@ -24,3 +66,12 @@ export function DataTable<T, C extends DataTableColumn = DataTableColumn>(
|
|
|
24
66
|
}
|
|
25
67
|
return <OpenTuiDataTable {...props} />;
|
|
26
68
|
}
|
|
69
|
+
|
|
70
|
+
function resolveTableIndex<T, C extends DataTableColumn>(
|
|
71
|
+
input: unknown,
|
|
72
|
+
props: DataTableProps<T, C>,
|
|
73
|
+
): number {
|
|
74
|
+
return resolveRemoteItemIndex(input, props.items, {
|
|
75
|
+
key: (item, index) => props.getItemKey(item, index),
|
|
76
|
+
});
|
|
77
|
+
}
|
|
@@ -2,6 +2,8 @@ import { Box, Input, Span, Text, useUiHost } from "../../ui";
|
|
|
2
2
|
import { useEffect, useRef, useState, type ComponentType, type RefObject } from "react";
|
|
3
3
|
import { type InputRenderable } from "../../ui";
|
|
4
4
|
import { colors } from "../../theme/colors";
|
|
5
|
+
import { useRemoteUiNode } from "../../remote/semantic-tree";
|
|
6
|
+
import { remoteStringValue } from "../../remote/semantic-helpers";
|
|
5
7
|
|
|
6
8
|
export interface TextFieldProps {
|
|
7
9
|
label?: string;
|
|
@@ -46,6 +48,22 @@ export function TextField({
|
|
|
46
48
|
placeholderColor = colors.textDim,
|
|
47
49
|
onMouseDown,
|
|
48
50
|
}: TextFieldProps) {
|
|
51
|
+
useRemoteUiNode({
|
|
52
|
+
role: "text-field",
|
|
53
|
+
label: label ?? placeholder,
|
|
54
|
+
actions: {
|
|
55
|
+
setValue: (input) => {
|
|
56
|
+
const nextValue = remoteStringValue(input);
|
|
57
|
+
onChange?.(nextValue);
|
|
58
|
+
},
|
|
59
|
+
submit: (input) => {
|
|
60
|
+
const nextValue = remoteStringValue(input, value ?? "");
|
|
61
|
+
onSubmit?.(nextValue);
|
|
62
|
+
},
|
|
63
|
+
blur: () => onBlur?.(value ?? ""),
|
|
64
|
+
},
|
|
65
|
+
metadata: { value, placeholder, focused, type, variant },
|
|
66
|
+
});
|
|
49
67
|
const HostTextField = useUiHost().TextField as ComponentType<TextFieldProps> | undefined;
|
|
50
68
|
if (HostTextField) {
|
|
51
69
|
return (
|