gloomberb 0.4.0 → 0.5.0
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 +2 -2
- package/package.json +1 -1
- package/src/app.tsx +184 -69
- package/src/cli/commands/ticker.ts +45 -15
- package/src/cli/helpers.ts +4 -2
- package/src/cli/registry.test.ts +42 -35
- package/src/cli.test.ts +4 -2
- package/src/components/chart/chart-control-hint.tsx +39 -0
- package/src/components/chart/chart-controller.test.ts +110 -0
- package/src/components/chart/chart-controller.ts +364 -2
- package/src/components/chart/chart-data.ts +177 -29
- package/src/components/chart/chart-indicator-selector.tsx +53 -0
- package/src/components/chart/chart-pane-settings.ts +20 -0
- package/src/components/chart/chart-renderer.ts +220 -30
- package/src/components/chart/chart-resolution.test.ts +67 -0
- package/src/components/chart/chart-resolution.ts +74 -4
- package/src/components/chart/chart-scroll.test.ts +69 -0
- package/src/components/chart/chart-scroll.ts +104 -0
- package/src/components/chart/chart-types.ts +12 -0
- package/src/components/chart/chart-viewport.test.ts +5 -1
- package/src/components/chart/chart-viewport.ts +1 -1
- package/src/components/chart/chart.test.ts +249 -10
- package/src/components/chart/comparison-chart-renderer.test.ts +71 -1
- package/src/components/chart/comparison-chart-renderer.ts +40 -3
- package/src/components/chart/comparison-stock-chart.tsx +218 -123
- package/src/components/chart/indicators/bands.test.ts +50 -0
- package/src/components/chart/indicators/bands.ts +29 -0
- package/src/components/chart/indicators/moving-averages.test.ts +54 -0
- package/src/components/chart/indicators/moving-averages.ts +42 -0
- package/src/components/chart/indicators/options.test.ts +32 -0
- package/src/components/chart/indicators/options.ts +107 -0
- package/src/components/chart/indicators/oscillators.test.ts +79 -0
- package/src/components/chart/indicators/oscillators.ts +92 -0
- package/src/components/chart/indicators/types.ts +36 -0
- package/src/components/chart/native/chart-rasterizer.ts +51 -0
- package/src/components/chart/native/kitty.test.ts +68 -0
- package/src/components/chart/stock-chart-renderer-switch.test.tsx +740 -9
- package/src/components/chart/stock-chart.test.ts +239 -0
- package/src/components/chart/stock-chart.tsx +2257 -372
- package/src/components/command-bar/command-bar.test.tsx +337 -5
- package/src/components/command-bar/command-bar.tsx +296 -120
- package/src/components/command-bar/helpers.ts +1 -1
- package/src/components/command-bar/root-shortcuts.ts +33 -4
- package/src/components/command-bar/view-model.test.ts +3 -3
- package/src/components/command-bar/view-model.ts +4 -4
- package/src/components/command-bar/workflow-ops.ts +10 -0
- package/src/components/layout/floating-pane.tsx +3 -1
- package/src/components/layout/header.tsx +5 -4
- package/src/components/layout/pane-sizing.ts +6 -0
- package/src/components/layout/pane.tsx +5 -1
- package/src/components/layout/shell.test.tsx +263 -32
- package/src/components/layout/shell.tsx +10 -6
- package/src/components/layout/status-bar.test.tsx +16 -7
- package/src/components/layout/status-bar.tsx +4 -1
- package/src/components/markdown-editor.tsx +1 -0
- package/src/components/pane-settings-dialog.tsx +13 -152
- package/src/components/pane-template-wizard.tsx +1 -0
- package/src/components/price-selector-dialog.tsx +12 -7
- package/src/components/ticker-badge-text.test.tsx +50 -0
- package/src/components/ticker-badge-text.tsx +17 -2
- package/src/components/ticker-badge.tsx +2 -2
- package/src/components/ticker-list-table.tsx +62 -2
- package/src/components/toggle-list.tsx +15 -2
- package/src/components/ui/data-table.tsx +82 -5
- package/src/components/ui/external-link.tsx +48 -11
- package/src/components/ui/fields.tsx +2 -1
- package/src/components/ui/index.ts +8 -1
- package/src/components/ui/multi-select-chips.tsx +69 -0
- package/src/components/ui/multi-select-dialog.tsx +267 -0
- package/src/components/ui/multi-select.ts +78 -0
- package/src/components/ui/ui.test.tsx +343 -1
- package/src/data/plugin-state-store.ts +30 -19
- package/src/data/resource-store.ts +133 -103
- package/src/data/session-store.ts +19 -12
- package/src/data/sqlite-cache.test.ts +37 -2
- package/src/data/sqlite-database.ts +8 -2
- package/src/data/sqlite-retry.test.ts +63 -0
- package/src/data/sqlite-retry.ts +66 -0
- package/src/data/ticker-store.ts +29 -18
- package/src/index.tsx +6 -1
- package/src/market-data/coordinator.test.ts +114 -5
- package/src/market-data/coordinator.ts +70 -1
- package/src/market-data/hooks.test.tsx +73 -2
- package/src/market-data/hooks.tsx +16 -4
- package/src/notifications/app-notifier.test.ts +177 -0
- package/src/notifications/app-notifier.ts +241 -0
- package/src/pane-settings.test.ts +2 -0
- package/src/plugins/builtin/ai/index.test.tsx +1 -1
- package/src/plugins/builtin/ai/screener-pane.test.tsx +2 -0
- package/src/plugins/builtin/ai/screener-pane.tsx +1 -0
- package/src/plugins/builtin/ai/ticker-context.ts +3 -2
- package/src/plugins/builtin/alerts/alert-engine.test.ts +90 -0
- package/src/plugins/builtin/alerts/alert-engine.ts +55 -0
- package/src/plugins/builtin/alerts/index.test.tsx +357 -0
- package/src/plugins/builtin/alerts/index.tsx +519 -0
- package/src/plugins/builtin/alerts/types.ts +14 -0
- package/src/plugins/builtin/analytics/index.test.tsx +300 -0
- package/src/plugins/builtin/analytics/index.tsx +775 -0
- package/src/plugins/builtin/analytics/metrics.test.ts +125 -0
- package/src/plugins/builtin/analytics/metrics.ts +155 -0
- package/src/plugins/builtin/ask-ai.test.tsx +2 -0
- package/src/plugins/builtin/chat-controller.test.ts +92 -18
- package/src/plugins/builtin/chat-controller.ts +40 -11
- package/src/plugins/builtin/chat.test.tsx +715 -4
- package/src/plugins/builtin/chat.tsx +420 -119
- package/src/plugins/builtin/comparison-chart.test.tsx +97 -57
- package/src/plugins/builtin/comparison-chart.tsx +1 -2
- package/src/plugins/builtin/correlation/compute.test.ts +147 -0
- package/src/plugins/builtin/correlation/compute.ts +115 -0
- package/src/plugins/builtin/correlation/index.tsx +385 -0
- package/src/plugins/builtin/correlation/plugin.test.ts +44 -0
- package/src/plugins/builtin/correlation/settings.test.ts +34 -0
- package/src/plugins/builtin/correlation/settings.ts +95 -0
- package/src/plugins/builtin/debug.tsx +36 -50
- package/src/plugins/builtin/earnings/earnings-cache.test.ts +181 -0
- package/src/plugins/builtin/earnings/earnings-cache.ts +136 -0
- package/src/plugins/builtin/earnings/earnings-data.test.ts +59 -0
- package/src/plugins/builtin/earnings/earnings-data.ts +50 -0
- package/src/plugins/builtin/earnings/index.tsx +337 -0
- package/src/plugins/builtin/econ/calendar-source.test.ts +219 -0
- package/src/plugins/builtin/econ/calendar-source.ts +121 -0
- package/src/plugins/builtin/econ/fred-client.test.ts +63 -0
- package/src/plugins/builtin/econ/fred-client.ts +79 -0
- package/src/plugins/builtin/econ/fred-series-map.ts +88 -0
- package/src/plugins/builtin/econ/index.tsx +999 -0
- package/src/plugins/builtin/econ/types.ts +13 -0
- package/src/plugins/builtin/fred-settings.ts +10 -0
- package/src/plugins/builtin/fx-matrix/index.tsx +178 -0
- package/src/plugins/builtin/fx-matrix/pairs.ts +30 -0
- package/src/plugins/builtin/help.test.tsx +1 -5
- package/src/plugins/builtin/help.tsx +1 -16
- package/src/plugins/builtin/insider/index.tsx +417 -0
- package/src/plugins/builtin/insider/insider-data.test.ts +55 -0
- package/src/plugins/builtin/insider/insider-data.ts +49 -0
- package/src/plugins/builtin/layout-manager.tsx +33 -16
- package/src/plugins/builtin/market-movers/index.tsx +573 -0
- package/src/plugins/builtin/market-movers/screener.test.ts +143 -0
- package/src/plugins/builtin/market-movers/screener.ts +105 -0
- package/src/plugins/builtin/options.tsx +4 -3
- package/src/plugins/builtin/portfolio-list/cli/commands.ts +11 -6
- package/src/plugins/builtin/portfolio-list/command-bar.ts +6 -3
- package/src/plugins/builtin/portfolio-list/header.tsx +3 -2
- package/src/plugins/builtin/portfolio-list/metrics.ts +22 -11
- package/src/plugins/builtin/portfolio-list/pane.tsx +3 -9
- package/src/plugins/builtin/portfolio-list/settings.ts +9 -0
- package/src/plugins/builtin/portfolio-list/table.tsx +3 -0
- package/src/plugins/builtin/portfolio-list.test.tsx +3 -1
- package/src/plugins/builtin/portfolio-metrics.test.ts +13 -1
- package/src/plugins/builtin/sec.tsx +114 -7
- package/src/plugins/builtin/sectors/index.tsx +359 -0
- package/src/plugins/builtin/sectors/sector-data.ts +18 -0
- package/src/plugins/builtin/ticker-detail/chart-tab.tsx +75 -13
- package/src/plugins/builtin/ticker-detail/overview-tab.tsx +316 -80
- package/src/plugins/builtin/ticker-detail/quote-monitor.tsx +4 -6
- package/src/plugins/builtin/ticker-detail-chart-tab-switch.test.tsx +17 -12
- package/src/plugins/builtin/ticker-detail.test.tsx +3 -3
- package/src/plugins/builtin/world-indices/index.tsx +410 -0
- package/src/plugins/builtin/world-indices/indices.ts +52 -0
- package/src/plugins/builtin/yield-curve/index.tsx +242 -0
- package/src/plugins/builtin/yield-curve/treasury-data.test.ts +212 -0
- package/src/plugins/builtin/yield-curve/treasury-data.ts +107 -0
- package/src/plugins/catalog.ts +22 -0
- package/src/plugins/gridlock-notification.ts +15 -0
- package/src/plugins/ibkr/__snapshots__/trade-tab.test.tsx.snap +5 -5
- package/src/plugins/ibkr/gateway-service.test.ts +22 -0
- package/src/plugins/ibkr/gateway-service.ts +6 -0
- package/src/plugins/ibkr/index.test.tsx +1 -1
- package/src/plugins/ibkr/index.tsx +8 -2
- package/src/plugins/ibkr/trade-tab.tsx +23 -5
- package/src/plugins/ibkr/trade-utils.test.ts +1 -1
- package/src/plugins/ibkr/trade-utils.ts +7 -6
- package/src/plugins/ibkr/trading-pane.tsx +8 -7
- package/src/plugins/plugin-runtime.test.tsx +2 -0
- package/src/plugins/plugin-runtime.tsx +10 -0
- package/src/plugins/prediction-markets/controller-data.ts +129 -14
- package/src/plugins/prediction-markets/controller.ts +1 -1
- package/src/plugins/prediction-markets/detail/shared.tsx +7 -6
- package/src/plugins/prediction-markets/index.tsx +5 -1
- package/src/plugins/prediction-markets/pane.test.tsx +78 -0
- package/src/plugins/prediction-markets/pane.tsx +7 -3
- package/src/plugins/prediction-markets/plugin.test.tsx +48 -1
- package/src/plugins/prediction-markets/services/fetch.ts +17 -7
- package/src/plugins/prediction-markets/services/polymarket-adapter.ts +29 -11
- package/src/plugins/prediction-markets/services/polymarket-ws.ts +20 -0
- package/src/plugins/prediction-markets/test-helpers.tsx +22 -5
- package/src/plugins/registry.ts +45 -13
- package/src/sources/provider-router.test.ts +3 -2
- package/src/sources/sec-edgar.ts +13 -1
- package/src/sources/yahoo-finance.ts +75 -1
- package/src/state/app-context-selector.test.tsx +17 -1
- package/src/state/app-context.test.tsx +24 -0
- package/src/state/app-context.tsx +29 -6
- package/src/theme/colors.ts +5 -0
- package/src/theme/themes.ts +25 -0
- package/src/types/config.test.ts +107 -0
- package/src/types/config.ts +30 -1
- package/src/types/data-provider.ts +13 -0
- package/src/types/plugin.ts +35 -1
- package/src/updater.test.ts +2 -0
- package/src/utils/api-client.ts +12 -0
- package/src/utils/format.test.ts +11 -1
- package/src/utils/format.ts +86 -8
- package/src/utils/inline-content-tokenizer.test.ts +22 -0
- package/src/utils/inline-content-tokenizer.ts +76 -0
- package/src/utils/link-tokenizer.test.ts +27 -0
- package/src/utils/link-tokenizer.ts +93 -0
- package/src/utils/market-format.test.ts +109 -0
- package/src/utils/market-format.ts +299 -0
- package/src/utils/market-status.ts +3 -2
- package/src/utils/selection-clipboard.test.ts +43 -0
- package/src/utils/selection-clipboard.ts +88 -0
- package/src/utils/terminal-input-reset.test.ts +50 -0
- package/src/utils/terminal-input-reset.ts +24 -0
- package/src/utils/throttled-fetch.test.ts +145 -0
- package/src/utils/throttled-fetch.ts +212 -0
- package/src/utils/ticker-tokenizer.test.ts +31 -0
- package/src/utils/ticker-tokenizer.ts +7 -42
- package/src/version.ts +1 -1
|
@@ -3,9 +3,11 @@ import { TextAttributes, type ScrollBoxRenderable } from "@opentui/core";
|
|
|
3
3
|
import { EmptyState } from "./ui";
|
|
4
4
|
import { colors, hoverBg } from "../theme/colors";
|
|
5
5
|
import type { ColumnConfig } from "../types/config";
|
|
6
|
-
import type { TickerFinancials } from "../types/financials";
|
|
6
|
+
import type { TickerFinancials, PricePoint } from "../types/financials";
|
|
7
7
|
import type { TickerRecord } from "../types/ticker";
|
|
8
8
|
import { padTo } from "../utils/format";
|
|
9
|
+
import { renderChart, resolveChartPalette, type StyledContent } from "./chart/chart-renderer";
|
|
10
|
+
import type { ProjectedChartPoint } from "./chart/chart-data";
|
|
9
11
|
|
|
10
12
|
export interface TickerTableCell {
|
|
11
13
|
text: string;
|
|
@@ -64,6 +66,7 @@ interface TickerListTableProps {
|
|
|
64
66
|
onRowActivate?: (ticker: TickerRecord) => void;
|
|
65
67
|
emptyTitle?: string;
|
|
66
68
|
emptyHint?: string;
|
|
69
|
+
showSparklines?: boolean;
|
|
67
70
|
}
|
|
68
71
|
|
|
69
72
|
const TickerListHeader = memo(function TickerListHeader({
|
|
@@ -86,7 +89,13 @@ const TickerListHeader = memo(function TickerListHeader({
|
|
|
86
89
|
scrollX
|
|
87
90
|
focusable={false}
|
|
88
91
|
>
|
|
89
|
-
<box
|
|
92
|
+
<box
|
|
93
|
+
flexDirection="row"
|
|
94
|
+
height={1}
|
|
95
|
+
width="100%"
|
|
96
|
+
paddingX={1}
|
|
97
|
+
backgroundColor={colors.panel}
|
|
98
|
+
>
|
|
90
99
|
{columns.map((column) => {
|
|
91
100
|
const isSorted = sortColumnId === column.id;
|
|
92
101
|
const indicator = isSorted ? (sortDirection === "asc" ? " \u25B2" : " \u25BC") : "";
|
|
@@ -95,6 +104,7 @@ const TickerListHeader = memo(function TickerListHeader({
|
|
|
95
104
|
<box
|
|
96
105
|
key={column.id}
|
|
97
106
|
width={column.width + 1}
|
|
107
|
+
backgroundColor={colors.panel}
|
|
98
108
|
onMouseDown={onHeaderClick ? (event) => {
|
|
99
109
|
event.preventDefault();
|
|
100
110
|
onHeaderClick(column.id);
|
|
@@ -111,6 +121,38 @@ const TickerListHeader = memo(function TickerListHeader({
|
|
|
111
121
|
);
|
|
112
122
|
});
|
|
113
123
|
|
|
124
|
+
function renderSparkline(priceHistory: PricePoint[]): StyledContent | null {
|
|
125
|
+
if (priceHistory.length < 5) return null;
|
|
126
|
+
|
|
127
|
+
const last20 = priceHistory.slice(-20);
|
|
128
|
+
const points: ProjectedChartPoint[] = last20.map((pt) => ({
|
|
129
|
+
date: pt.date,
|
|
130
|
+
open: pt.open ?? pt.close,
|
|
131
|
+
high: pt.high ?? pt.close,
|
|
132
|
+
low: pt.low ?? pt.close,
|
|
133
|
+
close: pt.close,
|
|
134
|
+
volume: pt.volume ?? 0,
|
|
135
|
+
}));
|
|
136
|
+
|
|
137
|
+
const first = last20[0]?.close ?? 0;
|
|
138
|
+
const last = last20[last20.length - 1]?.close ?? 0;
|
|
139
|
+
const trend = last >= first ? "positive" : "negative";
|
|
140
|
+
const palette = resolveChartPalette(colors, trend);
|
|
141
|
+
|
|
142
|
+
const result = renderChart(points, {
|
|
143
|
+
width: 10,
|
|
144
|
+
height: 1,
|
|
145
|
+
showVolume: false,
|
|
146
|
+
volumeHeight: 0,
|
|
147
|
+
cursorX: null,
|
|
148
|
+
cursorY: null,
|
|
149
|
+
mode: "line",
|
|
150
|
+
colors: palette,
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
return result.lines[0] ?? null;
|
|
154
|
+
}
|
|
155
|
+
|
|
114
156
|
const TickerListRow = memo(function TickerListRow({
|
|
115
157
|
columns,
|
|
116
158
|
ticker,
|
|
@@ -123,6 +165,8 @@ const TickerListRow = memo(function TickerListRow({
|
|
|
123
165
|
setCursorSymbol,
|
|
124
166
|
resolveCell,
|
|
125
167
|
onRowActivate,
|
|
168
|
+
showSparklines,
|
|
169
|
+
priceHistory,
|
|
126
170
|
}: {
|
|
127
171
|
columns: ColumnConfig[];
|
|
128
172
|
ticker: TickerRecord;
|
|
@@ -135,10 +179,16 @@ const TickerListRow = memo(function TickerListRow({
|
|
|
135
179
|
setCursorSymbol: (symbol: string) => void;
|
|
136
180
|
resolveCell: ResolveTickerTableCell;
|
|
137
181
|
onRowActivate?: (ticker: TickerRecord) => void;
|
|
182
|
+
showSparklines?: boolean;
|
|
183
|
+
priceHistory?: PricePoint[];
|
|
138
184
|
}) {
|
|
139
185
|
const lastActivatedAtRef = useRef<number | null>(null);
|
|
140
186
|
const rowBg = isSelected ? colors.selected : isHovered ? hoverBg() : colors.bg;
|
|
141
187
|
|
|
188
|
+
const sparkline = showSparklines && priceHistory && priceHistory.length >= 5
|
|
189
|
+
? renderSparkline(priceHistory)
|
|
190
|
+
: null;
|
|
191
|
+
|
|
142
192
|
return (
|
|
143
193
|
<box
|
|
144
194
|
flexDirection="row"
|
|
@@ -175,6 +225,11 @@ const TickerListRow = memo(function TickerListRow({
|
|
|
175
225
|
</box>
|
|
176
226
|
);
|
|
177
227
|
})}
|
|
228
|
+
{sparkline && (
|
|
229
|
+
<box width={12}>
|
|
230
|
+
<text content={sparkline} />
|
|
231
|
+
</box>
|
|
232
|
+
)}
|
|
178
233
|
</box>
|
|
179
234
|
);
|
|
180
235
|
}, (previous, next) => (
|
|
@@ -189,6 +244,8 @@ const TickerListRow = memo(function TickerListRow({
|
|
|
189
244
|
&& previous.setCursorSymbol === next.setCursorSymbol
|
|
190
245
|
&& previous.resolveCell === next.resolveCell
|
|
191
246
|
&& previous.onRowActivate === next.onRowActivate
|
|
247
|
+
&& previous.showSparklines === next.showSparklines
|
|
248
|
+
&& previous.priceHistory === next.priceHistory
|
|
192
249
|
));
|
|
193
250
|
|
|
194
251
|
export function TickerListTable({
|
|
@@ -211,6 +268,7 @@ export function TickerListTable({
|
|
|
211
268
|
onRowActivate,
|
|
212
269
|
emptyTitle = "No tickers.",
|
|
213
270
|
emptyHint = "Press Ctrl+P to add one.",
|
|
271
|
+
showSparklines,
|
|
214
272
|
}: TickerListTableProps) {
|
|
215
273
|
const safeFlashSymbols = flashSymbols ?? new Map<string, QuoteFlashDirection>();
|
|
216
274
|
|
|
@@ -255,6 +313,8 @@ export function TickerListTable({
|
|
|
255
313
|
setCursorSymbol={setCursorSymbol}
|
|
256
314
|
resolveCell={resolveCell}
|
|
257
315
|
onRowActivate={onRowActivate}
|
|
316
|
+
showSparklines={showSparklines}
|
|
317
|
+
priceHistory={financialsMap.get(ticker.metadata.ticker)?.priceHistory}
|
|
258
318
|
/>
|
|
259
319
|
);
|
|
260
320
|
})
|
|
@@ -20,6 +20,7 @@ export interface ToggleListProps {
|
|
|
20
20
|
flexGrow?: number;
|
|
21
21
|
scrollable?: boolean;
|
|
22
22
|
showSelectedDescription?: boolean;
|
|
23
|
+
rowIdPrefix?: string;
|
|
23
24
|
}
|
|
24
25
|
|
|
25
26
|
export function ToggleList({
|
|
@@ -32,6 +33,7 @@ export function ToggleList({
|
|
|
32
33
|
flexGrow,
|
|
33
34
|
scrollable,
|
|
34
35
|
showSelectedDescription = true,
|
|
36
|
+
rowIdPrefix,
|
|
35
37
|
}: ToggleListProps) {
|
|
36
38
|
const listItems: ListViewItem[] = items.map((item) => ({
|
|
37
39
|
id: item.id,
|
|
@@ -52,17 +54,28 @@ export function ToggleList({
|
|
|
52
54
|
onActivate={(item) => {
|
|
53
55
|
onToggle?.(item.id);
|
|
54
56
|
}}
|
|
55
|
-
renderRow={(item, state) => {
|
|
57
|
+
renderRow={(item, state, index) => {
|
|
56
58
|
const toggleItem = items.find((entry) => entry.id === item.id);
|
|
57
59
|
const checked = toggleItem?.enabled ? "\u2713" : " ";
|
|
60
|
+
const activate = (event: any) => {
|
|
61
|
+
event.stopPropagation?.();
|
|
62
|
+
if (state.disabled) return;
|
|
63
|
+
onSelect?.(index);
|
|
64
|
+
onToggle?.(item.id);
|
|
65
|
+
};
|
|
58
66
|
return (
|
|
59
|
-
<box
|
|
67
|
+
<box
|
|
68
|
+
id={rowIdPrefix ? `${rowIdPrefix}:${item.id}` : undefined}
|
|
69
|
+
flexDirection="row"
|
|
70
|
+
onMouseDown={activate}
|
|
71
|
+
>
|
|
60
72
|
<text fg={state.selected ? colors.selectedText : colors.textDim}>
|
|
61
73
|
{state.selected ? "\u25b8 " : " "}
|
|
62
74
|
</text>
|
|
63
75
|
<text
|
|
64
76
|
fg={state.selected ? colors.text : colors.textDim}
|
|
65
77
|
attributes={state.selected ? TextAttributes.BOLD : 0}
|
|
78
|
+
onMouseDown={activate}
|
|
66
79
|
>
|
|
67
80
|
{`[${checked}] ${item.label}`}
|
|
68
81
|
</text>
|
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import { useMemo, useState, type RefObject } from "react";
|
|
1
|
+
import { useCallback, useEffect, useMemo, useState, type RefObject } from "react";
|
|
2
2
|
import { TextAttributes, type ScrollBoxRenderable } from "@opentui/core";
|
|
3
3
|
import { colors, hoverBg } from "../../theme/colors";
|
|
4
4
|
import type { ColumnConfig } from "../../types/config";
|
|
5
|
+
import { useAppDispatch, usePaneInstance } from "../../state/app-context";
|
|
5
6
|
import { padTo } from "../../utils/format";
|
|
6
7
|
import { useDoubleClickActivation } from "../use-double-click-activation";
|
|
7
8
|
import { EmptyState } from "./status";
|
|
@@ -14,9 +15,17 @@ export type DataTableColumn = Pick<
|
|
|
14
15
|
export interface DataTableCell {
|
|
15
16
|
text: string;
|
|
16
17
|
color?: string;
|
|
18
|
+
attributes?: number;
|
|
17
19
|
onMouseDown?: (event: any) => void;
|
|
18
20
|
}
|
|
19
21
|
|
|
22
|
+
export interface DataTableSectionHeader {
|
|
23
|
+
text: string;
|
|
24
|
+
color?: string;
|
|
25
|
+
backgroundColor?: string;
|
|
26
|
+
attributes?: number;
|
|
27
|
+
}
|
|
28
|
+
|
|
20
29
|
export interface DataTableProps<
|
|
21
30
|
T,
|
|
22
31
|
C extends DataTableColumn = DataTableColumn,
|
|
@@ -42,10 +51,15 @@ export interface DataTableProps<
|
|
|
42
51
|
index: number,
|
|
43
52
|
rowState: { selected: boolean; hovered: boolean },
|
|
44
53
|
) => DataTableCell;
|
|
54
|
+
renderSectionHeader?: (
|
|
55
|
+
item: T,
|
|
56
|
+
index: number,
|
|
57
|
+
) => DataTableSectionHeader | null;
|
|
45
58
|
emptyStateTitle: string;
|
|
46
59
|
emptyStateHint?: string;
|
|
47
60
|
virtualize?: boolean;
|
|
48
61
|
overscan?: number;
|
|
62
|
+
showHorizontalScrollbar?: boolean;
|
|
49
63
|
}
|
|
50
64
|
|
|
51
65
|
interface DataTableRowPointerTarget<T> {
|
|
@@ -70,11 +84,15 @@ export function DataTable<T, C extends DataTableColumn = DataTableColumn>({
|
|
|
70
84
|
onSelect,
|
|
71
85
|
onActivate,
|
|
72
86
|
renderCell,
|
|
87
|
+
renderSectionHeader,
|
|
73
88
|
emptyStateTitle,
|
|
74
89
|
emptyStateHint,
|
|
75
90
|
virtualize = false,
|
|
76
91
|
overscan = 3,
|
|
92
|
+
showHorizontalScrollbar = true,
|
|
77
93
|
}: DataTableProps<T, C>) {
|
|
94
|
+
const dispatch = useAppDispatch();
|
|
95
|
+
const paneInstanceId = usePaneInstance()?.instanceId ?? null;
|
|
78
96
|
const [scrollVersion, setScrollVersion] = useState(0);
|
|
79
97
|
const scrollTop = virtualize ? (scrollRef.current?.scrollTop ?? 0) : 0;
|
|
80
98
|
const viewportHeight = virtualize
|
|
@@ -106,11 +124,36 @@ export function DataTable<T, C extends DataTableColumn = DataTableColumn>({
|
|
|
106
124
|
}
|
|
107
125
|
onBodyScrollActivity();
|
|
108
126
|
};
|
|
127
|
+
const focusPane = useCallback(() => {
|
|
128
|
+
if (!paneInstanceId) return;
|
|
129
|
+
dispatch({ type: "FOCUS_PANE", paneId: paneInstanceId });
|
|
130
|
+
}, [dispatch, paneInstanceId]);
|
|
131
|
+
|
|
132
|
+
useEffect(() => {
|
|
133
|
+
if (headerScrollRef.current) {
|
|
134
|
+
headerScrollRef.current.horizontalScrollBar.visible = false;
|
|
135
|
+
if (!showHorizontalScrollbar) {
|
|
136
|
+
headerScrollRef.current.scrollLeft = 0;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
if (scrollRef.current) {
|
|
140
|
+
scrollRef.current.horizontalScrollBar.visible = showHorizontalScrollbar;
|
|
141
|
+
if (!showHorizontalScrollbar) {
|
|
142
|
+
scrollRef.current.scrollLeft = 0;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}, [columns.length, headerScrollRef, items.length, scrollRef, showHorizontalScrollbar]);
|
|
109
146
|
|
|
110
147
|
return (
|
|
111
148
|
<>
|
|
112
|
-
<scrollbox ref={headerScrollRef} height={1} scrollX focusable={false}>
|
|
113
|
-
<box
|
|
149
|
+
<scrollbox ref={headerScrollRef} height={1} scrollX={showHorizontalScrollbar} focusable={false}>
|
|
150
|
+
<box
|
|
151
|
+
flexDirection="row"
|
|
152
|
+
height={1}
|
|
153
|
+
width="100%"
|
|
154
|
+
paddingX={1}
|
|
155
|
+
backgroundColor={colors.panel}
|
|
156
|
+
>
|
|
114
157
|
{columns.map((column) => {
|
|
115
158
|
const isSorted = sortColumnId === column.id;
|
|
116
159
|
const indicator = isSorted
|
|
@@ -127,7 +170,9 @@ export function DataTable<T, C extends DataTableColumn = DataTableColumn>({
|
|
|
127
170
|
<box
|
|
128
171
|
key={column.id}
|
|
129
172
|
width={column.width + 1}
|
|
173
|
+
backgroundColor={colors.panel}
|
|
130
174
|
onMouseDown={(event) => {
|
|
175
|
+
focusPane();
|
|
131
176
|
event.preventDefault();
|
|
132
177
|
onHeaderClick(column.id);
|
|
133
178
|
}}
|
|
@@ -147,10 +192,13 @@ export function DataTable<T, C extends DataTableColumn = DataTableColumn>({
|
|
|
147
192
|
<scrollbox
|
|
148
193
|
ref={scrollRef}
|
|
149
194
|
flexGrow={1}
|
|
150
|
-
scrollX
|
|
195
|
+
scrollX={showHorizontalScrollbar}
|
|
151
196
|
scrollY
|
|
152
197
|
focusable={false}
|
|
153
|
-
onMouseDown={() =>
|
|
198
|
+
onMouseDown={() => {
|
|
199
|
+
focusPane();
|
|
200
|
+
queueMicrotask(syncHeaderScroll);
|
|
201
|
+
}}
|
|
154
202
|
onMouseUp={() => queueMicrotask(handleBodyScrollActivity)}
|
|
155
203
|
onMouseDrag={() => queueMicrotask(handleBodyScrollActivity)}
|
|
156
204
|
onMouseScroll={() => queueMicrotask(handleBodyScrollActivity)}
|
|
@@ -164,6 +212,32 @@ export function DataTable<T, C extends DataTableColumn = DataTableColumn>({
|
|
|
164
212
|
{virtualize && startIndex > 0 && <box height={startIndex} />}
|
|
165
213
|
{visibleItems.map((item, visibleIndex) => {
|
|
166
214
|
const index = startIndex + visibleIndex;
|
|
215
|
+
const sectionHeader = renderSectionHeader?.(item, index) ?? null;
|
|
216
|
+
|
|
217
|
+
if (sectionHeader) {
|
|
218
|
+
return (
|
|
219
|
+
<box
|
|
220
|
+
key={getItemKey(item, index)}
|
|
221
|
+
flexDirection="row"
|
|
222
|
+
height={1}
|
|
223
|
+
width="100%"
|
|
224
|
+
paddingX={1}
|
|
225
|
+
backgroundColor={sectionHeader.backgroundColor ?? colors.bg}
|
|
226
|
+
onMouseDown={(event) => {
|
|
227
|
+
focusPane();
|
|
228
|
+
event.preventDefault();
|
|
229
|
+
}}
|
|
230
|
+
>
|
|
231
|
+
<text
|
|
232
|
+
attributes={sectionHeader.attributes ?? TextAttributes.BOLD}
|
|
233
|
+
fg={sectionHeader.color ?? colors.textBright}
|
|
234
|
+
>
|
|
235
|
+
{sectionHeader.text}
|
|
236
|
+
</text>
|
|
237
|
+
</box>
|
|
238
|
+
);
|
|
239
|
+
}
|
|
240
|
+
|
|
167
241
|
const selected = isSelected(item, index);
|
|
168
242
|
const hovered = hoveredIdx === index && !selected;
|
|
169
243
|
const rowBg = selected
|
|
@@ -181,6 +255,7 @@ export function DataTable<T, C extends DataTableColumn = DataTableColumn>({
|
|
|
181
255
|
backgroundColor={rowBg}
|
|
182
256
|
onMouseMove={() => setHoveredIdx(index)}
|
|
183
257
|
onMouseDown={(event) => {
|
|
258
|
+
focusPane();
|
|
184
259
|
event.preventDefault();
|
|
185
260
|
handleRowMouseDown(getItemKey(item, index), {
|
|
186
261
|
item,
|
|
@@ -198,6 +273,7 @@ export function DataTable<T, C extends DataTableColumn = DataTableColumn>({
|
|
|
198
273
|
key={column.id}
|
|
199
274
|
width={column.width + 1}
|
|
200
275
|
onMouseDown={(event) => {
|
|
276
|
+
focusPane();
|
|
201
277
|
if (cell.onMouseDown) {
|
|
202
278
|
cell.onMouseDown(event);
|
|
203
279
|
return;
|
|
@@ -211,6 +287,7 @@ export function DataTable<T, C extends DataTableColumn = DataTableColumn>({
|
|
|
211
287
|
}}
|
|
212
288
|
>
|
|
213
289
|
<text
|
|
290
|
+
attributes={cell.attributes ?? TextAttributes.NONE}
|
|
214
291
|
fg={
|
|
215
292
|
cell.color ??
|
|
216
293
|
(selected ? colors.selectedText : colors.text)
|
|
@@ -1,22 +1,59 @@
|
|
|
1
1
|
import { TextAttributes } from "@opentui/core";
|
|
2
|
-
import {
|
|
2
|
+
import { spawn } from "child_process";
|
|
3
3
|
import { colors } from "../../theme/colors";
|
|
4
4
|
|
|
5
5
|
export function openUrl(url: string) {
|
|
6
|
-
|
|
7
|
-
|
|
6
|
+
if (!url.trim()) return;
|
|
7
|
+
|
|
8
|
+
const child = process.platform === "darwin"
|
|
9
|
+
? spawn("open", [url], { detached: true, stdio: "ignore" })
|
|
8
10
|
: process.platform === "win32"
|
|
9
|
-
?
|
|
10
|
-
:
|
|
11
|
-
|
|
11
|
+
? spawn("cmd", ["/c", "start", "", url], { detached: true, stdio: "ignore" })
|
|
12
|
+
: spawn("xdg-open", [url], { detached: true, stdio: "ignore" });
|
|
13
|
+
|
|
14
|
+
child.unref();
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function handleOpen(
|
|
18
|
+
url: string,
|
|
19
|
+
onOpen: (url: string) => void,
|
|
20
|
+
event?: { preventDefault?: () => void; stopPropagation?: () => void },
|
|
21
|
+
) {
|
|
22
|
+
event?.preventDefault?.();
|
|
23
|
+
event?.stopPropagation?.();
|
|
24
|
+
onOpen(url);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function ExternalLinkText(
|
|
28
|
+
{ url, label, color = colors.textBright, onOpen = openUrl }: {
|
|
29
|
+
url: string;
|
|
30
|
+
label?: string;
|
|
31
|
+
color?: string;
|
|
32
|
+
onOpen?: (url: string) => void;
|
|
33
|
+
},
|
|
34
|
+
) {
|
|
35
|
+
return (
|
|
36
|
+
<text
|
|
37
|
+
fg={color}
|
|
38
|
+
attributes={TextAttributes.UNDERLINE}
|
|
39
|
+
onMouseDown={(event: any) => handleOpen(url, onOpen, event)}
|
|
40
|
+
>
|
|
41
|
+
{label ?? url}
|
|
42
|
+
</text>
|
|
43
|
+
);
|
|
12
44
|
}
|
|
13
45
|
|
|
14
|
-
export function ExternalLink(
|
|
46
|
+
export function ExternalLink(
|
|
47
|
+
{ url, label, color, onOpen }: {
|
|
48
|
+
url: string;
|
|
49
|
+
label?: string;
|
|
50
|
+
color?: string;
|
|
51
|
+
onOpen?: (url: string) => void;
|
|
52
|
+
},
|
|
53
|
+
) {
|
|
15
54
|
return (
|
|
16
|
-
<box height={1}
|
|
17
|
-
<
|
|
18
|
-
{label ?? url}
|
|
19
|
-
</text>
|
|
55
|
+
<box height={1}>
|
|
56
|
+
<ExternalLinkText url={url} label={label} color={color} onOpen={onOpen} />
|
|
20
57
|
</box>
|
|
21
58
|
);
|
|
22
59
|
}
|
|
@@ -82,6 +82,7 @@ export function TextField({
|
|
|
82
82
|
ref={resolvedInputRef}
|
|
83
83
|
width={width}
|
|
84
84
|
value={value}
|
|
85
|
+
selectable={!isPassword}
|
|
85
86
|
placeholder={isPassword ? "" : placeholder}
|
|
86
87
|
focused={focused}
|
|
87
88
|
textColor={isPassword ? backgroundColor : textColor}
|
|
@@ -115,7 +116,7 @@ export function TextField({
|
|
|
115
116
|
resolvedInputRef.current?.focus?.();
|
|
116
117
|
}}
|
|
117
118
|
>
|
|
118
|
-
<text fg={maskedTextColor}>
|
|
119
|
+
<text fg={maskedTextColor} selectable={false}>
|
|
119
120
|
{maskedBefore}
|
|
120
121
|
{focused && (
|
|
121
122
|
<span bg={maskedTextColor} fg={backgroundColor}>{maskedCursorChar}</span>
|
|
@@ -6,6 +6,7 @@ export type {
|
|
|
6
6
|
DataTableCell,
|
|
7
7
|
DataTableColumn,
|
|
8
8
|
DataTableProps,
|
|
9
|
+
DataTableSectionHeader,
|
|
9
10
|
} from "./data-table";
|
|
10
11
|
|
|
11
12
|
export { Tabs } from "./tabs";
|
|
@@ -14,6 +15,12 @@ export type { TabItem, TabsProps } from "./tabs";
|
|
|
14
15
|
export { Button, IconButton } from "./button";
|
|
15
16
|
export type { ButtonProps, ButtonVariant, IconButtonProps } from "./button";
|
|
16
17
|
|
|
18
|
+
export { MultiSelectChips } from "./multi-select-chips";
|
|
19
|
+
export type { MultiSelectChipsProps, MultiSelectChipOption } from "./multi-select-chips";
|
|
20
|
+
export { MultiSelectDialogButton, MultiSelectDialogContent } from "./multi-select-dialog";
|
|
21
|
+
export type { MultiSelectDialogButtonProps, MultiSelectDialogContentProps } from "./multi-select-dialog";
|
|
22
|
+
export type { MultiSelectOption } from "./multi-select";
|
|
23
|
+
|
|
17
24
|
export { Checkbox, Switch, RadioGroup, SegmentedControl } from "./toggle";
|
|
18
25
|
export type {
|
|
19
26
|
CheckboxProps,
|
|
@@ -42,7 +49,7 @@ export type {
|
|
|
42
49
|
export { Section, FieldRow, DialogFrame } from "./frame";
|
|
43
50
|
export type { SectionProps, FieldRowProps, DialogFrameProps } from "./frame";
|
|
44
51
|
|
|
45
|
-
export { ExternalLink, openUrl } from "./external-link";
|
|
52
|
+
export { ExternalLink, ExternalLinkText, openUrl } from "./external-link";
|
|
46
53
|
export { PageStackView } from "./page-stack-view";
|
|
47
54
|
|
|
48
55
|
export { Spinner, ProgressBar, SkeletonRow, LoadingBlock } from "./loading";
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { TextAttributes } from "@opentui/core";
|
|
2
|
+
import { colors } from "../../theme/colors";
|
|
3
|
+
import { toggleMultiSelectValue, type MultiSelectOption } from "./multi-select";
|
|
4
|
+
|
|
5
|
+
export type MultiSelectChipOption = MultiSelectOption;
|
|
6
|
+
|
|
7
|
+
export interface MultiSelectChipsProps {
|
|
8
|
+
label?: string;
|
|
9
|
+
options: MultiSelectChipOption[];
|
|
10
|
+
selectedValues: string[];
|
|
11
|
+
onChange?: (values: string[]) => void;
|
|
12
|
+
disabled?: boolean;
|
|
13
|
+
emptyLabel?: string;
|
|
14
|
+
idPrefix?: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function MultiSelectChips({
|
|
18
|
+
label,
|
|
19
|
+
options,
|
|
20
|
+
selectedValues,
|
|
21
|
+
onChange,
|
|
22
|
+
disabled = false,
|
|
23
|
+
emptyLabel = "No options",
|
|
24
|
+
idPrefix,
|
|
25
|
+
}: MultiSelectChipsProps) {
|
|
26
|
+
const selectedSet = new Set(selectedValues);
|
|
27
|
+
|
|
28
|
+
return (
|
|
29
|
+
<box flexDirection="row" height={1} gap={1}>
|
|
30
|
+
{label && <text fg={colors.textDim}>{`${label}:`}</text>}
|
|
31
|
+
{options.length === 0 && <text fg={colors.textMuted}>{emptyLabel}</text>}
|
|
32
|
+
{options.map((option) => {
|
|
33
|
+
const selected = selectedSet.has(option.value);
|
|
34
|
+
const optionDisabled = disabled || option.disabled;
|
|
35
|
+
const bg = selected ? colors.selected : colors.panel;
|
|
36
|
+
const fg = optionDisabled
|
|
37
|
+
? colors.textMuted
|
|
38
|
+
: selected
|
|
39
|
+
? colors.selectedText
|
|
40
|
+
: colors.textDim;
|
|
41
|
+
const marker = selected ? "[x]" : "[ ]";
|
|
42
|
+
const text = `${marker} ${option.label}`;
|
|
43
|
+
const toggle = () => {
|
|
44
|
+
if (optionDisabled) return;
|
|
45
|
+
onChange?.(toggleMultiSelectValue(options, selectedValues, option.value));
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
return (
|
|
49
|
+
<box
|
|
50
|
+
key={option.value}
|
|
51
|
+
id={idPrefix ? `${idPrefix}:${option.value}` : undefined}
|
|
52
|
+
height={1}
|
|
53
|
+
width={text.length}
|
|
54
|
+
backgroundColor={bg}
|
|
55
|
+
onMouseDown={toggle}
|
|
56
|
+
>
|
|
57
|
+
<text
|
|
58
|
+
fg={fg}
|
|
59
|
+
attributes={selected ? TextAttributes.BOLD : 0}
|
|
60
|
+
onMouseDown={toggle}
|
|
61
|
+
>
|
|
62
|
+
{text}
|
|
63
|
+
</text>
|
|
64
|
+
</box>
|
|
65
|
+
);
|
|
66
|
+
})}
|
|
67
|
+
</box>
|
|
68
|
+
);
|
|
69
|
+
}
|