gloomberb 0.16.0 → 0.16.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/api-client/data.ts +18 -3
- package/src/api-client/index.ts +3 -0
- package/src/api-client/members.ts +26 -0
- package/src/api-client/perps.ts +4 -0
- package/src/api-client/types.ts +7 -1
- package/src/app/global-shortcuts.ts +14 -6
- package/src/app/keybindings/actions.ts +1 -1
- package/src/app/runtime/update.ts +38 -2
- package/src/app.tsx +10 -6
- package/src/cli/pane-functions/function-help.ts +9 -2
- package/src/components/command-bar/commands/collection.ts +5 -1
- package/src/components/command-bar/routes/root/command-items.ts +9 -2
- package/src/components/command-bar/routes/root/plugin-install.ts +2 -2
- package/src/components/command-bar/routes/root/provider-search.ts +19 -5
- package/src/components/command-bar/routes/root/runtime.ts +14 -5
- package/src/components/command-bar/routes/ticker-search/results.ts +31 -5
- package/src/components/command-bar/routes/ticker-search/route.ts +2 -2
- package/src/components/command-bar/routing/actions.ts +2 -0
- package/src/components/command-bar/selection-runtime.ts +38 -0
- package/src/components/command-bar/surface/index.tsx +7 -2
- package/src/components/index.ts +2 -2
- package/src/components/layout/floating-pane.tsx +6 -0
- package/src/components/layout/header.tsx +24 -7
- package/src/components/layout/missing-pane.tsx +2 -2
- package/src/components/layout/pane/header.tsx +63 -30
- package/src/components/layout/pane/index.tsx +6 -0
- package/src/components/layout/pane/title.ts +31 -7
- package/src/components/layout/shell/drag/index.ts +6 -5
- package/src/components/layout/shell/drag/runtime.ts +3 -0
- package/src/components/layout/shell/index.tsx +6 -0
- package/src/components/layout/shell/pane/layers.tsx +15 -7
- package/src/components/layout/shell/terminal-pointer-runtime.ts +24 -3
- package/src/components/layout/shell/ticker-link-menu.ts +23 -9
- package/src/components/metric-treemap/heat-surface.tsx +718 -0
- package/src/components/metric-treemap/heat-tiles.ts +210 -0
- package/src/components/metric-treemap/index.tsx +2 -0
- package/src/components/metric-treemap/layout.ts +6 -0
- package/src/components/metric-treemap/scene.ts +432 -0
- package/src/components/pane-settings-dialog/field-dialogs.tsx +1 -0
- package/src/components/toggle-list.tsx +38 -12
- package/src/components/ui/country-flag-art.tsx +449 -0
- package/src/components/ui/country-flag.tsx +41 -0
- package/src/components/ui/data-table/types.ts +13 -0
- package/src/components/ui/icon.tsx +2 -0
- package/src/components/ui/index.ts +4 -0
- package/src/components/ui/multi-select/dialog.tsx +108 -22
- package/src/components/ui/range-track.tsx +46 -0
- package/src/components/ui/tabs.tsx +4 -0
- package/src/core/state/app/types.ts +10 -1
- package/src/i18n/es.ts +5 -3
- package/src/i18n/ja.ts +5 -3
- package/src/i18n/ko.ts +5 -3
- package/src/i18n/zh-cn.ts +5 -3
- package/src/i18n/zh-tw.ts +5 -3
- package/src/layout/ticker-navigation.ts +28 -2
- package/src/market-data/quotes/contributions.ts +6 -0
- package/src/market-data/quotes/freshness.ts +36 -0
- package/src/market-data/quotes/resolution.ts +22 -8
- package/src/market-data/quotes/time.ts +16 -0
- package/src/plugins/builtin/analytics/risk-client.ts +185 -59
- package/src/plugins/builtin/analytics/risk-headless.ts +55 -49
- package/src/plugins/builtin/analytics/risk-math.ts +33 -13
- package/src/plugins/builtin/analytics/risk-model.ts +250 -60
- package/src/plugins/builtin/analytics/risk-pane.tsx +61 -30
- package/src/plugins/builtin/analytics/risk-test-data.ts +182 -0
- package/src/plugins/builtin/buildout/cells.tsx +15 -12
- package/src/plugins/builtin/buildout/detail/company.tsx +3 -3
- package/src/plugins/builtin/buildout/detail/index.tsx +0 -1
- package/src/plugins/builtin/buildout/detail/intel.tsx +6 -6
- package/src/plugins/builtin/buildout/detail/site.tsx +4 -7
- package/src/plugins/builtin/buildout/detail/ui.tsx +0 -19
- package/src/plugins/builtin/catalysts/index.tsx +2 -2
- package/src/plugins/builtin/catalysts/pane.tsx +3 -1
- package/src/plugins/builtin/cds/index.tsx +1 -0
- package/src/plugins/builtin/debt-maturities/wall-bar.tsx +14 -41
- package/src/plugins/builtin/dividend-yield/client.ts +4 -2
- package/src/plugins/builtin/earnings/client.ts +2 -2
- package/src/plugins/builtin/earnings-calls/index.tsx +1 -0
- package/src/plugins/builtin/fx-matrix/direction.ts +81 -0
- package/src/plugins/builtin/fx-matrix/index.tsx +71 -33
- package/src/plugins/builtin/fx-matrix/live-legs.ts +17 -0
- package/src/plugins/builtin/fx-matrix/pairs.ts +68 -14
- package/src/plugins/builtin/fx-matrix/settings.ts +62 -0
- package/src/plugins/builtin/jobs/index.tsx +1 -0
- package/src/plugins/builtin/jobs/share-bars.tsx +5 -28
- package/src/plugins/builtin/market-heatmap/data.ts +28 -2
- package/src/plugins/builtin/market-heatmap/index.tsx +250 -79
- package/src/plugins/builtin/market-heatmap/live.ts +88 -0
- package/src/plugins/builtin/market-heatmap/portfolio.ts +27 -7
- package/src/plugins/builtin/market-movers/client.ts +3 -2
- package/src/plugins/builtin/market-movers/headless.ts +8 -1
- package/src/plugins/builtin/market-movers/screener.ts +18 -5
- package/src/plugins/builtin/members/client.ts +36 -0
- package/src/plugins/builtin/members/headless.ts +33 -0
- package/src/plugins/builtin/members/index.tsx +20 -0
- package/src/plugins/builtin/members/model.ts +60 -0
- package/src/plugins/builtin/members/pane.tsx +168 -0
- package/src/plugins/builtin/mna/index.tsx +2 -1
- package/src/plugins/builtin/mna/pane.tsx +5 -2
- package/src/plugins/builtin/options/index.tsx +2 -1
- package/src/plugins/builtin/pe-band/client.ts +29 -2
- package/src/plugins/builtin/pe-band/headless.ts +2 -2
- package/src/plugins/builtin/pe-band/model.ts +11 -7
- package/src/plugins/builtin/pe-band/pane.tsx +2 -1
- package/src/plugins/builtin/pe-band/report-dates.ts +78 -0
- package/src/plugins/builtin/perps/client.ts +17 -3
- package/src/plugins/builtin/perps/equity-identity.ts +16 -0
- package/src/plugins/builtin/perps/equity-row.tsx +6 -4
- package/src/plugins/builtin/plugin-marketplace/activation.ts +19 -0
- package/src/plugins/builtin/plugin-marketplace/pane.tsx +2 -2
- package/src/plugins/builtin/plugin-marketplace/store.ts +2 -0
- package/src/plugins/builtin/portfolio-list/quick-add/index.tsx +43 -2
- package/src/plugins/builtin/portfolio-list/quick-add/resolution.ts +48 -5
- package/src/plugins/builtin/scanner/hilo-bars.tsx +12 -70
- package/src/plugins/builtin/scanner/hilo-model.ts +0 -18
- package/src/plugins/builtin/sectors/move-bar.tsx +10 -32
- package/src/plugins/builtin/social-mentions/index.tsx +1 -1
- package/src/plugins/builtin/thirteenf/index.tsx +2 -1
- package/src/plugins/builtin/ticker-detail/overview/components.tsx +5 -60
- package/src/plugins/builtin/ticker-detail/overview/pane.tsx +1 -1
- package/src/plugins/builtin/ticker-detail/pane.tsx +1 -1
- package/src/plugins/builtin/ticker-detail/quote-footer.ts +7 -1
- package/src/plugins/builtin/ticker-detail/quote-monitor/card.tsx +20 -4
- package/src/plugins/builtin/ticker-detail/quote-monitor/index.tsx +2 -2
- package/src/plugins/builtin/vol-surface/index.tsx +1 -1
- package/src/plugins/catalog-browser.ts +2 -0
- package/src/plugins/catalog-manifest.ts +1 -0
- package/src/plugins/catalog-ui.ts +3 -1
- package/src/plugins/catalog.ts +1 -1
- package/src/plugins/host-link.ts +33 -1
- package/src/plugins/installer.ts +48 -12
- package/src/plugins/loader.ts +18 -3
- package/src/plugins/manager-node.ts +15 -5
- package/src/plugins/registry/index.ts +1 -1
- package/src/plugins/registry/pane-settings.ts +25 -7
- package/src/public/__snapshots__/public-api.test.ts.snap +2 -0
- package/src/renderers/dom/data-table/row.tsx +50 -16
- package/src/renderers/dom/desktop/icons.tsx +1 -0
- package/src/renderers/dom/host/box.tsx +69 -26
- package/src/renderers/dom/host/tabs.tsx +37 -1
- package/src/renderers/dom/styles.css +40 -5
- package/src/renderers/electrobun/bun/desktop/backend-requests.ts +11 -6
- package/src/renderers/electrobun/bun/desktop/backend.ts +1 -0
- package/src/renderers/electrobun/bun/desktop/update.ts +181 -87
- package/src/renderers/electrobun/bun/index.ts +73 -6
- package/src/renderers/electrobun/bun/window/remembered-frame.ts +122 -0
- package/src/renderers/electrobun/shared/protocol.ts +3 -0
- package/src/renderers/electrobun/view/cli-pane-shot-entry.tsx +2 -1
- package/src/renderers/electrobun/view/main.tsx +7 -1
- package/src/renderers/electrobun/view/renderer-window-error.ts +18 -0
- package/src/renderers/electrobun/view/update-host.ts +45 -21
- package/src/sources/gloomberb-cloud/normalizers.ts +9 -2
- package/src/sources/provider-router/financials.ts +3 -1
- package/src/telemetry/benign-window-error.ts +7 -0
- package/src/telemetry/crash-reports-dom.ts +2 -0
- package/src/test-support/daily-nav.ts +25 -0
- package/src/theme/heat-colors.ts +174 -1
- package/src/tickers/instrument-kind.ts +34 -7
- package/src/tickers/search/asset-classes.ts +24 -9
- package/src/tickers/search/index.ts +44 -11
- package/src/tickers/search/ranking.ts +23 -3
- package/src/types/financials.ts +2 -0
- package/src/types/plugin.ts +18 -5
- package/src/ui/context-menu.tsx +1 -0
- package/src/ui/host.tsx +2 -0
- package/src/updater/index.ts +29 -2
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Box, Text, useUiCapabilities } from "../../../ui";
|
|
1
|
+
import { Box, Text, useUiCapabilities, type InputRenderable } from "../../../ui";
|
|
2
2
|
import { TextAttributes } from "../../../ui";
|
|
3
3
|
import { useShortcut, useViewport } from "../../../react/input";
|
|
4
4
|
import { type AlertContext, useDialog, useDialogKeyboard } from "../../../ui/dialog";
|
|
@@ -18,6 +18,7 @@ import { isDetailBackNavigationKey } from "../../../utils/back-navigation";
|
|
|
18
18
|
import { isPlainKeyboardEvent } from "../../../utils/keyboard";
|
|
19
19
|
import { ToggleList } from "../../toggle-list";
|
|
20
20
|
import { Button } from "../button";
|
|
21
|
+
import { TextField } from "../fields";
|
|
21
22
|
import { listCursorMove } from "../list-view";
|
|
22
23
|
import { DialogFrame } from "../frame";
|
|
23
24
|
import { Popover } from "../popover";
|
|
@@ -64,6 +65,13 @@ export interface MultiSelectDialogContentProps extends AlertContext {
|
|
|
64
65
|
emptyLabel?: string;
|
|
65
66
|
idPrefix?: string;
|
|
66
67
|
rowAction?: MultiSelectRowAction;
|
|
68
|
+
/**
|
|
69
|
+
* For a long list: a filter box over the rows (typing narrows them by label
|
|
70
|
+
* and description), each description beside its label and, when ordered,
|
|
71
|
+
* each selection's position at the row's end. The keyboard stays with the
|
|
72
|
+
* box: arrows move, Space picks, [ and ] reorder, Enter is done.
|
|
73
|
+
*/
|
|
74
|
+
searchable?: boolean;
|
|
67
75
|
}
|
|
68
76
|
|
|
69
77
|
export interface MultiSelectDialogButtonProps {
|
|
@@ -82,6 +90,8 @@ export interface MultiSelectDialogButtonProps {
|
|
|
82
90
|
onOpenChange?: (open: boolean) => void;
|
|
83
91
|
/** Opens the dialog on desktop too, since the popover menu has no row actions. */
|
|
84
92
|
rowAction?: MultiSelectRowAction;
|
|
93
|
+
/** A filter box over the rows; opens the dialog on desktop too, since the popover menu has none. */
|
|
94
|
+
searchable?: boolean;
|
|
85
95
|
}
|
|
86
96
|
|
|
87
97
|
export interface MultiSelectPopoverAnchorPoint {
|
|
@@ -118,6 +128,26 @@ function stopMouseEvent(event?: DialogTriggerEvent) {
|
|
|
118
128
|
const DESKTOP_ROW_HEIGHT = 1.35;
|
|
119
129
|
/** Terminal rows the dialog spends around its list: frame, title, buttons. */
|
|
120
130
|
const TERMINAL_DIALOG_CHROME_ROWS = 10;
|
|
131
|
+
/** Cells a searchable list shows on the desktop and web; the rest scrolls. */
|
|
132
|
+
const DESKTOP_SEARCHABLE_LIST_CELLS = 16;
|
|
133
|
+
|
|
134
|
+
/** A key that types a character: the filter box takes it. */
|
|
135
|
+
function isTextKey(event: { sequence?: string; ctrl?: boolean; meta?: boolean; alt?: boolean }): boolean {
|
|
136
|
+
return event.sequence?.length === 1 && event.sequence >= " " && event.sequence !== "\u007f"
|
|
137
|
+
&& !event.ctrl && !event.meta && !event.alt;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function matchesFilter(option: MultiSelectOption, needle: string): boolean {
|
|
141
|
+
return `${option.label} ${option.description ?? ""}`.toLowerCase().includes(needle);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/** The picks in their order, then every other option in the order given. */
|
|
145
|
+
function selectedFirstOptions(options: readonly MultiSelectOption[], selectedValues: readonly string[]): MultiSelectOption[] {
|
|
146
|
+
const byValue = new Map(options.map((option) => [option.value, option]));
|
|
147
|
+
const picked = selectedValues.flatMap((value) => byValue.get(value) ?? []);
|
|
148
|
+
const chosen = new Set(picked.map((option) => option.value));
|
|
149
|
+
return [...picked, ...options.filter((option) => !chosen.has(option.value))];
|
|
150
|
+
}
|
|
121
151
|
|
|
122
152
|
type FocusedNode = { blur?(): void; closest?(selector: string): unknown };
|
|
123
153
|
|
|
@@ -219,19 +249,32 @@ export function MultiSelectDialogContent({
|
|
|
219
249
|
ordered = false,
|
|
220
250
|
idPrefix,
|
|
221
251
|
rowAction,
|
|
252
|
+
searchable = false,
|
|
222
253
|
}: MultiSelectDialogContentProps) {
|
|
223
254
|
const isDesktopWeb = useUiCapabilities().nativePaneChrome === true;
|
|
224
255
|
const optionByValue = useMemo(() => new Map(options.map((option) => [option.value, option])), [options]);
|
|
225
256
|
const [selectedValues, setSelectedValues] = useState(() => normalizeDialogSelectedValues(options, selectedValuesProp, ordered));
|
|
226
257
|
const [displayValues, setDisplayValues] = useState(() => getMultiSelectDisplayValues(options, selectedValuesProp, ordered));
|
|
227
258
|
const knownSelectedValues = selectedValues.filter((value) => optionByValue.has(value));
|
|
259
|
+
// A long ordered list reads as the list being built on top, the rest below;
|
|
260
|
+
// a short one keeps every option in its row and swaps the picks among them.
|
|
261
|
+
const selectedFirst = searchable && ordered;
|
|
228
262
|
const displayOptions = useMemo(
|
|
229
|
-
() =>
|
|
230
|
-
|
|
263
|
+
() => (selectedFirst
|
|
264
|
+
? selectedFirstOptions(options, selectedValues)
|
|
265
|
+
: orderMultiSelectOptionsForDisplay(options, displayValues)),
|
|
266
|
+
[displayValues, options, selectedFirst, selectedValues],
|
|
267
|
+
);
|
|
268
|
+
const [filter, setFilter] = useState("");
|
|
269
|
+
const filterRef = useRef<InputRenderable | null>(null);
|
|
270
|
+
const needle = searchable ? filter.trim().toLowerCase() : "";
|
|
271
|
+
const visibleOptions = useMemo(
|
|
272
|
+
() => (needle ? displayOptions.filter((option) => matchesFilter(option, needle)) : displayOptions),
|
|
273
|
+
[displayOptions, needle],
|
|
231
274
|
);
|
|
232
275
|
const [selectedOptionId, setSelectedOptionId] = useState(options[0]?.value ?? "");
|
|
233
|
-
const selectedIndex = Math.max(0,
|
|
234
|
-
const selectedOption =
|
|
276
|
+
const selectedIndex = Math.max(0, visibleOptions.findIndex((option) => option.value === selectedOptionId));
|
|
277
|
+
const selectedOption = visibleOptions[selectedIndex];
|
|
235
278
|
const selectedOptionValue = selectedOption?.value ?? "";
|
|
236
279
|
const selectedValueOrder = knownSelectedValues.indexOf(selectedOptionValue);
|
|
237
280
|
const canMoveUp = ordered && selectedValueOrder > 0;
|
|
@@ -258,7 +301,14 @@ export function MultiSelectDialogContent({
|
|
|
258
301
|
setSelectedOptionId(displayOptions[0]?.value ?? "");
|
|
259
302
|
}, [displayOptions, selectedOptionId]);
|
|
260
303
|
|
|
261
|
-
|
|
304
|
+
// A touch screen would raise its keyboard over the list the moment it opens.
|
|
305
|
+
useEffect(() => {
|
|
306
|
+
if (!searchable || (globalThis as { matchMedia?: (query: string) => { matches: boolean } }).matchMedia?.("(pointer: coarse)").matches) return;
|
|
307
|
+
const timer = setTimeout(() => filterRef.current?.focus?.(), 0);
|
|
308
|
+
return () => clearTimeout(timer);
|
|
309
|
+
}, [searchable]);
|
|
310
|
+
|
|
311
|
+
const toggleItems = visibleOptions.map((option) => {
|
|
262
312
|
const order = knownSelectedValues.indexOf(option.value);
|
|
263
313
|
const orderDescription = ordered && order >= 0
|
|
264
314
|
? `Order ${order + 1} of ${knownSelectedValues.length}.`
|
|
@@ -269,15 +319,19 @@ export function MultiSelectDialogContent({
|
|
|
269
319
|
label: labelOverrides[option.value] ?? option.label,
|
|
270
320
|
disabled: option.disabled,
|
|
271
321
|
enabled: selectedValues.includes(option.value),
|
|
272
|
-
description:
|
|
322
|
+
description: searchable
|
|
323
|
+
? option.description
|
|
324
|
+
: [option.description, orderDescription].filter((entry): entry is string => !!entry).join(" "),
|
|
325
|
+
detail: searchable && ordered && order >= 0 ? String(order + 1) : undefined,
|
|
273
326
|
};
|
|
274
327
|
});
|
|
275
328
|
const viewport = useViewport();
|
|
276
329
|
// A short terminal would clip rows under the dialog's edge: the list scrolls
|
|
277
|
-
// in whatever height is left.
|
|
330
|
+
// in whatever height is left. The height follows the whole list, not the rows
|
|
331
|
+
// the filter leaves, so typing does not make the dialog jump.
|
|
278
332
|
const listHeight = isDesktopWeb
|
|
279
|
-
? Math.min(12, Math.max(5, displayOptions.length * DESKTOP_ROW_HEIGHT))
|
|
280
|
-
: Math.max(3, Math.min(12, Math.max(6,
|
|
333
|
+
? Math.min(searchable ? DESKTOP_SEARCHABLE_LIST_CELLS : 12, Math.max(5, displayOptions.length * DESKTOP_ROW_HEIGHT))
|
|
334
|
+
: Math.max(3, Math.min(12, Math.max(6, displayOptions.length), viewport.height - TERMINAL_DIALOG_CHROME_ROWS - (searchable ? 2 : 0)));
|
|
281
335
|
const pageSize = Math.floor(listHeight / (isDesktopWeb ? DESKTOP_ROW_HEIGHT : 1)) - 1;
|
|
282
336
|
|
|
283
337
|
const applySelectedValues = async (nextValues: string[], nextDisplayValues = displayValues) => {
|
|
@@ -296,16 +350,23 @@ export function MultiSelectDialogContent({
|
|
|
296
350
|
|
|
297
351
|
const toggleOption = async (option: MultiSelectOption | undefined) => {
|
|
298
352
|
if (!option || option.disabled) return;
|
|
299
|
-
|
|
353
|
+
const nextValues = ordered
|
|
300
354
|
? toggleOrderedMultiSelectValue(options, selectedValues, option.value)
|
|
301
|
-
: toggleMultiSelectValue(options, selectedValues, option.value)
|
|
355
|
+
: toggleMultiSelectValue(options, selectedValues, option.value);
|
|
356
|
+
const row = visibleOptions.findIndex((entry) => entry.value === option.value);
|
|
357
|
+
await applySelectedValues(nextValues);
|
|
358
|
+
if (!selectedFirst) return;
|
|
359
|
+
// The row moved between the picks and the rest. The cursor stays on its
|
|
360
|
+
// line, so Space picks or drops a row and moves on to the next.
|
|
361
|
+
const next = selectedFirstOptions(options, nextValues).filter((entry) => !needle || matchesFilter(entry, needle));
|
|
362
|
+
setSelectedOptionId(next[Math.min(Math.max(row, 0), next.length - 1)]?.value ?? option.value);
|
|
302
363
|
};
|
|
303
364
|
|
|
304
365
|
const moveOption = async (direction: "up" | "down") => {
|
|
305
366
|
if (!ordered || !selectedOption) return;
|
|
306
367
|
await applySelectedValues(
|
|
307
368
|
moveMultiSelectValue(options, selectedValues, selectedOption.value, direction),
|
|
308
|
-
moveMultiSelectDisplayValue(displayValues, knownSelectedValues, selectedOption.value, direction),
|
|
369
|
+
selectedFirst ? displayValues : moveMultiSelectDisplayValue(displayValues, knownSelectedValues, selectedOption.value, direction),
|
|
309
370
|
);
|
|
310
371
|
};
|
|
311
372
|
|
|
@@ -318,11 +379,24 @@ export function MultiSelectDialogContent({
|
|
|
318
379
|
};
|
|
319
380
|
|
|
320
381
|
useDialogKeyboard((event) => {
|
|
382
|
+
if (searchable) {
|
|
383
|
+
// The box owns every key that types or edits text; the list keeps the rest.
|
|
384
|
+
const typing = event.targetEditable === true;
|
|
385
|
+
const editsText = isTextKey(event) ? event.sequence !== " " && event.name !== "[" && event.name !== "]"
|
|
386
|
+
: typing && ["backspace", "delete", "left", "right", "home", "end"].includes(event.name ?? "");
|
|
387
|
+
const backsUp = !typing && filter !== "" && (event.name === "backspace" || event.name === "delete");
|
|
388
|
+
if (editsText || backsUp) {
|
|
389
|
+
// A click on a row or button took the focus; the first key typed brings it back.
|
|
390
|
+
if (!typing) filterRef.current?.focus?.();
|
|
391
|
+
return;
|
|
392
|
+
}
|
|
393
|
+
if (isSpaceKey(event)) event.preventDefault();
|
|
394
|
+
}
|
|
321
395
|
event.stopPropagation();
|
|
322
396
|
const move = listCursorMove(event, pageSize);
|
|
323
397
|
if (move) {
|
|
324
|
-
if (isDesktopWeb) releaseFocusOutsideList();
|
|
325
|
-
setSelectedOptionId(
|
|
398
|
+
if (isDesktopWeb && !searchable) releaseFocusOutsideList();
|
|
399
|
+
setSelectedOptionId(visibleOptions[move(visibleOptions, selectedIndex)]?.value ?? selectedOptionId);
|
|
326
400
|
} else if (isSpaceKey(event)) {
|
|
327
401
|
void toggleOption(selectedOption).catch(() => {});
|
|
328
402
|
} else if (event.name === "[" && ordered) {
|
|
@@ -334,15 +408,24 @@ export function MultiSelectDialogContent({
|
|
|
334
408
|
} else if (event.name === "enter" || event.name === "return" || event.name === "escape" || isDetailBackNavigationKey(event)) {
|
|
335
409
|
dismiss();
|
|
336
410
|
}
|
|
337
|
-
}, dialogId);
|
|
411
|
+
}, searchable ? { scope: dialogId, allowEditable: true } : dialogId);
|
|
338
412
|
|
|
339
413
|
return (
|
|
340
414
|
<DialogFrame title={title} showTitleDivider={!isDesktopWeb}>
|
|
341
415
|
<Box
|
|
342
416
|
flexDirection="column"
|
|
343
417
|
gap={1}
|
|
344
|
-
style={isDesktopWeb ? { minWidth:
|
|
418
|
+
style={isDesktopWeb ? { minWidth: "min(520px, calc(100vw - 96px))" } : undefined}
|
|
345
419
|
>
|
|
420
|
+
{searchable && (
|
|
421
|
+
<TextField
|
|
422
|
+
inputRef={filterRef}
|
|
423
|
+
value={filter}
|
|
424
|
+
placeholder="Filter"
|
|
425
|
+
focused
|
|
426
|
+
onChange={setFilter}
|
|
427
|
+
/>
|
|
428
|
+
)}
|
|
346
429
|
<ToggleList
|
|
347
430
|
items={toggleItems}
|
|
348
431
|
selectedIdx={selectedIndex}
|
|
@@ -350,11 +433,12 @@ export function MultiSelectDialogContent({
|
|
|
350
433
|
height={listHeight}
|
|
351
434
|
scrollable
|
|
352
435
|
showSelectedDescription={false}
|
|
436
|
+
showDescriptions={searchable}
|
|
353
437
|
rowIdPrefix={idPrefix ? `${idPrefix}:option` : undefined}
|
|
354
438
|
rowGap={isDesktopWeb ? 0 : undefined}
|
|
355
439
|
rowHeight={isDesktopWeb ? DESKTOP_ROW_HEIGHT : undefined}
|
|
356
440
|
surface={isDesktopWeb ? "plain" : undefined}
|
|
357
|
-
onSelect={(index) => setSelectedOptionId(
|
|
441
|
+
onSelect={(index) => setSelectedOptionId(visibleOptions[index]?.value ?? selectedOptionId)}
|
|
358
442
|
onToggle={(id) => {
|
|
359
443
|
setSelectedOptionId(id);
|
|
360
444
|
void toggleOption(optionByValue.get(id)).catch(() => {});
|
|
@@ -364,7 +448,7 @@ export function MultiSelectDialogContent({
|
|
|
364
448
|
flexDirection="row"
|
|
365
449
|
gap={1}
|
|
366
450
|
justifyContent={isDesktopWeb ? "flex-end" : undefined}
|
|
367
|
-
style={isDesktopWeb ? { paddingTop: 6 } : undefined}
|
|
451
|
+
style={isDesktopWeb ? { paddingTop: 6, flexWrap: "wrap" } : undefined}
|
|
368
452
|
>
|
|
369
453
|
{ordered && (
|
|
370
454
|
<>
|
|
@@ -403,6 +487,7 @@ function MultiSelectDialogButtonInner({
|
|
|
403
487
|
shortcutActive = false,
|
|
404
488
|
onOpenChange,
|
|
405
489
|
rowAction,
|
|
490
|
+
searchable = false,
|
|
406
491
|
}: MultiSelectDialogButtonProps, ref: ForwardedRef<MultiSelectDialogButtonHandle>) {
|
|
407
492
|
const isDesktopWeb = useUiCapabilities().nativePaneChrome === true;
|
|
408
493
|
const dialog = useDialog();
|
|
@@ -415,7 +500,7 @@ function MultiSelectDialogButtonInner({
|
|
|
415
500
|
const openDialog = useCallback((event?: DialogTriggerEvent, anchorPoint?: MultiSelectPopoverAnchorPoint) => {
|
|
416
501
|
stopMouseEvent(event);
|
|
417
502
|
if (disabled) return;
|
|
418
|
-
if (isDesktopWeb && !ordered && !rowAction) {
|
|
503
|
+
if (isDesktopWeb && !ordered && !rowAction && !searchable) {
|
|
419
504
|
setPopoverAnchorPoint(anchorPoint ?? null);
|
|
420
505
|
setPopoverOpen(true);
|
|
421
506
|
onOpenChange?.(true);
|
|
@@ -435,10 +520,11 @@ function MultiSelectDialogButtonInner({
|
|
|
435
520
|
emptyLabel={emptyLabel}
|
|
436
521
|
idPrefix={idPrefix}
|
|
437
522
|
rowAction={rowAction}
|
|
523
|
+
searchable={searchable}
|
|
438
524
|
/>
|
|
439
525
|
),
|
|
440
526
|
}).catch(() => {}).finally(() => onOpenChange?.(false));
|
|
441
|
-
}, [dialog, disabled, emptyLabel, idPrefix, isDesktopWeb, label, onChange, onOpenChange, options, ordered, rowAction, selectedValues, title]);
|
|
527
|
+
}, [dialog, disabled, emptyLabel, idPrefix, isDesktopWeb, label, onChange, onOpenChange, options, ordered, rowAction, searchable, selectedValues, title]);
|
|
442
528
|
|
|
443
529
|
const closePopover = useCallback(() => {
|
|
444
530
|
setPopoverOpen(false);
|
|
@@ -521,7 +607,7 @@ function MultiSelectDialogButtonInner({
|
|
|
521
607
|
);
|
|
522
608
|
}
|
|
523
609
|
|
|
524
|
-
if (isDesktopWeb && !ordered && !rowAction) {
|
|
610
|
+
if (isDesktopWeb && !ordered && !rowAction && !searchable) {
|
|
525
611
|
return (
|
|
526
612
|
<Popover
|
|
527
613
|
open={popoverOpen}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { useThemeColors } from "../../theme/theme-context";
|
|
2
|
+
import { Box, Text, useUiCapabilities } from "../../ui";
|
|
3
|
+
|
|
4
|
+
export interface RangeTrackProps {
|
|
5
|
+
/** Where the value sits between the low and the high, 0 to 1. */
|
|
6
|
+
position: number;
|
|
7
|
+
/** Cells the track spans. */
|
|
8
|
+
width: number;
|
|
9
|
+
markerColor: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Where a value sits between a low and a high (a day range, a 52-week range):
|
|
14
|
+
* a rule with a dot on it. The caller draws the endpoints. The terminal draws
|
|
15
|
+
* a box-drawing rule; the desktop draws a real line and dot, never glyphs.
|
|
16
|
+
*/
|
|
17
|
+
export function RangeTrack({ position, width, markerColor }: RangeTrackProps) {
|
|
18
|
+
const colors = useThemeColors();
|
|
19
|
+
const isDesktopWeb = useUiCapabilities().nativePaneChrome === true;
|
|
20
|
+
const at = position > 0 ? Math.min(1, position) : 0;
|
|
21
|
+
if (width <= 0) return null;
|
|
22
|
+
if (isDesktopWeb) {
|
|
23
|
+
return (
|
|
24
|
+
<Box width={width} height={1} flexShrink={0} style={{ position: "relative", justifyContent: "center" }} data-gloom-role="range-track">
|
|
25
|
+
<Box style={{ position: "absolute", left: 0, right: 0, height: "2px", borderRadius: "1px", backgroundColor: colors.border }} />
|
|
26
|
+
<Box style={{
|
|
27
|
+
position: "absolute",
|
|
28
|
+
left: `${at * 100}%`,
|
|
29
|
+
width: "8px",
|
|
30
|
+
height: "8px",
|
|
31
|
+
marginLeft: "-4px",
|
|
32
|
+
borderRadius: "50%",
|
|
33
|
+
backgroundColor: markerColor,
|
|
34
|
+
}} />
|
|
35
|
+
</Box>
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
const marker = Math.min(width - 1, Math.round(at * (width - 1)));
|
|
39
|
+
return (
|
|
40
|
+
<Box width={width} height={1} flexShrink={0} flexDirection="row">
|
|
41
|
+
<Text fg={colors.border}>{"─".repeat(marker)}</Text>
|
|
42
|
+
<Text fg={markerColor}>●</Text>
|
|
43
|
+
<Text fg={colors.border}>{"─".repeat(width - marker - 1)}</Text>
|
|
44
|
+
</Box>
|
|
45
|
+
);
|
|
46
|
+
}
|
|
@@ -43,6 +43,8 @@ export interface TabsProps {
|
|
|
43
43
|
addLabel?: string;
|
|
44
44
|
onAdd?: () => void;
|
|
45
45
|
onReorder?: (fromValue: string, toValue: string) => void;
|
|
46
|
+
/** A press that moves, on a tab that is not reordered. Desktop title-bar tabs use it to move the window. */
|
|
47
|
+
onDrag?: () => void;
|
|
46
48
|
focused?: boolean;
|
|
47
49
|
keyboardNavigation?: boolean;
|
|
48
50
|
/**
|
|
@@ -67,6 +69,7 @@ export function Tabs({
|
|
|
67
69
|
addLabel = "+",
|
|
68
70
|
onAdd,
|
|
69
71
|
onReorder,
|
|
72
|
+
onDrag,
|
|
70
73
|
focused = false,
|
|
71
74
|
keyboardNavigation = true,
|
|
72
75
|
paneMenu = true,
|
|
@@ -229,6 +232,7 @@ export function Tabs({
|
|
|
229
232
|
addLabel={addLabel}
|
|
230
233
|
onAdd={onAdd}
|
|
231
234
|
onReorder={onReorder}
|
|
235
|
+
onDrag={onDrag}
|
|
232
236
|
focused={focused}
|
|
233
237
|
palette={palette}
|
|
234
238
|
/>
|
|
@@ -45,10 +45,18 @@ interface CommandBarRunQueryLaunchRequest extends CommandBarLaunchRequestBase {
|
|
|
45
45
|
query: string;
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
+
/** The add row opened the bar so the chosen venue joins this collection. */
|
|
49
|
+
interface CommandBarAddListingLaunchRequest extends CommandBarLaunchRequestBase {
|
|
50
|
+
kind: "add-listing";
|
|
51
|
+
collectionId: string;
|
|
52
|
+
collectionKind: "watchlist" | "portfolio";
|
|
53
|
+
}
|
|
54
|
+
|
|
48
55
|
/** Forms open in their own modal (`openFormModal`), so the bar launches only its own screens. */
|
|
49
56
|
type CommandBarLaunch =
|
|
50
57
|
| { kind: "ticker-search"; query?: string }
|
|
51
|
-
| { kind: "run-query"; query: string }
|
|
58
|
+
| { kind: "run-query"; query: string }
|
|
59
|
+
| { kind: "add-listing"; collectionId: string; collectionKind: "watchlist" | "portfolio" };
|
|
52
60
|
|
|
53
61
|
export interface AppState {
|
|
54
62
|
config: AppConfig;
|
|
@@ -74,6 +82,7 @@ export interface AppState {
|
|
|
74
82
|
commandBarLaunchRequest:
|
|
75
83
|
| CommandBarTickerSearchLaunchRequest
|
|
76
84
|
| CommandBarRunQueryLaunchRequest
|
|
85
|
+
| CommandBarAddListingLaunchRequest
|
|
77
86
|
| null;
|
|
78
87
|
themePreview: string | null;
|
|
79
88
|
refreshing: Set<string>;
|
package/src/i18n/es.ts
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
* fixed-width terminal layouts stay readable.
|
|
6
6
|
*/
|
|
7
7
|
export const es: Record<string, string> = {
|
|
8
|
+
"NAV · as of {date}": "NAV · al {date}",
|
|
8
9
|
// ── Common UI ────────────────────────────────────────────────
|
|
9
10
|
"Back": "Atrás",
|
|
10
11
|
"Done": "Listo",
|
|
@@ -377,7 +378,6 @@ export const es: Record<string, string> = {
|
|
|
377
378
|
"Price trigger alerts with notifications": "Alertas de precio con notificaciones",
|
|
378
379
|
"Create a price alert from a symbol, condition, and target price": "Crear una alerta de precio con símbolo, condición y precio objetivo",
|
|
379
380
|
"Public market heatmap for the largest US stocks and ETFs.": "Mapa de calor de las mayores acciones y ETF de EE. UU.",
|
|
380
|
-
"Largest US stocks and ETFs, sized by market cap or assets and colored by daily move.": "Mayores acciones y ETF de EE. UU., dimensionados por capitalización o activos y coloreados por variación diaria.",
|
|
381
381
|
"Top gainers, losers, most active, and trending tickers": "Mayores alzas, bajas, más activos y tickers en tendencia",
|
|
382
382
|
"Top gainers, losers, most active, and trending tickers.": "Mayores alzas, bajas, más activos y tickers en tendencia.",
|
|
383
383
|
"S&P 500 sector and industry performance via ETF proxies": "Rendimiento sectorial e industrial del S&P 500 vía ETF",
|
|
@@ -524,7 +524,7 @@ export const es: Record<string, string> = {
|
|
|
524
524
|
"Quit the terminal app.": "Salir de la aplicación de terminal.",
|
|
525
525
|
"Copy the active terminal selection.": "Copiar la selección activa del terminal.",
|
|
526
526
|
"Paste clipboard text into the active input.": "Pegar el portapapeles en la entrada activa.",
|
|
527
|
-
"
|
|
527
|
+
"Restart into a downloaded app update, or retry one that failed.": "Reinicia con una actualización descargada o reintenta una fallida.",
|
|
528
528
|
"Close the focused pane, docked or floating.": "Cerrar el panel activo, anclado o flotante.",
|
|
529
529
|
"Close all floating panes.": "Cerrar todos los paneles flotantes.",
|
|
530
530
|
"Edit settings for the focused pane.": "Editar los ajustes del panel activo.",
|
|
@@ -943,7 +943,9 @@ export const es: Record<string, string> = {
|
|
|
943
943
|
|
|
944
944
|
// ── Main dashboard ──────────────────────────────────────────
|
|
945
945
|
"Installing update...": "Instalando actualización...",
|
|
946
|
-
"Update
|
|
946
|
+
"Update ready, restart to apply": "Actualización lista; reinicia para aplicar",
|
|
947
|
+
"Update ready": "Actualización lista",
|
|
948
|
+
"Restart": "Reiniciar",
|
|
947
949
|
"Checking for updates...": "Buscando actualizaciones...",
|
|
948
950
|
"Downloading v{version}: {percent}%": "Descargando v{version}: {percent}%",
|
|
949
951
|
"Update failed: {error}": "Error al actualizar: {error}",
|
package/src/i18n/ja.ts
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
* fixed-width terminal layouts stay readable.
|
|
6
6
|
*/
|
|
7
7
|
export const ja: Record<string, string> = {
|
|
8
|
+
"NAV · as of {date}": "NAV · {date}時点",
|
|
8
9
|
// ── Common UI ────────────────────────────────────────────────
|
|
9
10
|
"Back": "戻る",
|
|
10
11
|
"Done": "完了",
|
|
@@ -378,7 +379,6 @@ export const ja: Record<string, string> = {
|
|
|
378
379
|
"Price trigger alerts with notifications": "通知付きの価格トリガーアラート",
|
|
379
380
|
"Create a price alert from a symbol, condition, and target price": "銘柄コード、条件、目標価格から価格アラートを作成",
|
|
380
381
|
"Public market heatmap for the largest US stocks and ETFs.": "米国大型株と ETF の市場ヒートマップ。",
|
|
381
|
-
"Largest US stocks and ETFs, sized by market cap or assets and colored by daily move.": "米国大型株と ETF を時価総額・資産規模で表示し、日次騰落率で色分けします。",
|
|
382
382
|
"Top gainers, losers, most active, and trending tickers": "値上がり、値下がり、出来高、注目銘柄",
|
|
383
383
|
"Top gainers, losers, most active, and trending tickers.": "値上がり、値下がり、出来高、注目銘柄。",
|
|
384
384
|
"S&P 500 sector and industry performance via ETF proxies": "ETF を使った S&P 500 のセクター・業種別騰落率",
|
|
@@ -525,7 +525,7 @@ export const ja: Record<string, string> = {
|
|
|
525
525
|
"Quit the terminal app.": "ターミナル アプリを終了します。",
|
|
526
526
|
"Copy the active terminal selection.": "アクティブな端末の選択をコピーします。",
|
|
527
527
|
"Paste clipboard text into the active input.": "クリップボードのテキストをアクティブな入力に貼り付けます。",
|
|
528
|
-
"
|
|
528
|
+
"Restart into a downloaded app update, or retry one that failed.": "ダウンロード済みのアップデートで再起動するか、失敗したアップデートを再試行します。",
|
|
529
529
|
"Close the focused pane, docked or floating.": "フォーカスされたペインをドッキングまたはフローティング状態で閉じます。",
|
|
530
530
|
"Close all floating panes.": "すべてのフローティング ペインを閉じます。",
|
|
531
531
|
"Edit settings for the focused pane.": "フォーカスされたペインの設定を編集します。",
|
|
@@ -957,7 +957,9 @@ export const ja: Record<string, string> = {
|
|
|
957
957
|
|
|
958
958
|
// ── Main dashboard ──────────────────────────────────────────
|
|
959
959
|
"Installing update...": "アップデートをインストール中...",
|
|
960
|
-
"Update
|
|
960
|
+
"Update ready, restart to apply": "アップデートの準備ができました。適用するには再起動してください",
|
|
961
|
+
"Update ready": "アップデートの準備完了",
|
|
962
|
+
"Restart": "再起動",
|
|
961
963
|
"Checking for updates...": "アップデートを確認中...",
|
|
962
964
|
"Downloading v{version}: {percent}%": "v{version} をダウンロード中: {percent}%",
|
|
963
965
|
"Update failed: {error}": "更新に失敗しました: {error}",
|
package/src/i18n/ko.ts
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
* Keep finance terminology precise and translations concise for dense layouts.
|
|
4
4
|
*/
|
|
5
5
|
export const ko: Record<string, string> = {
|
|
6
|
+
"NAV · as of {date}": "NAV · {date} 기준",
|
|
6
7
|
// ── Common UI ────────────────────────────────────────────────
|
|
7
8
|
"Back": "뒤로",
|
|
8
9
|
"Done": "완료",
|
|
@@ -376,7 +377,6 @@ export const ko: Record<string, string> = {
|
|
|
376
377
|
"Price trigger alerts with notifications": "알림이 있는 목표가 알림",
|
|
377
378
|
"Create a price alert from a symbol, condition, and target price": "종목코드, 조건 및 목표가로 가격 알림 생성",
|
|
378
379
|
"Public market heatmap for the largest US stocks and ETFs.": "미국 대형주 및 ETF 시장 히트맵.",
|
|
379
|
-
"Largest US stocks and ETFs, sized by market cap or assets and colored by daily move.": "미국 대형주와 ETF를 시가총액·자산 규모 및 일일 변동률로 표시.",
|
|
380
380
|
"Top gainers, losers, most active, and trending tickers": "상승률·하락률·거래량 상위 및 인기 종목",
|
|
381
381
|
"Top gainers, losers, most active, and trending tickers.": "상승률·하락률·거래량 상위 및 인기 종목.",
|
|
382
382
|
"S&P 500 sector and industry performance via ETF proxies": "ETF 기준 S&P 500 섹터 및 산업 성과",
|
|
@@ -523,7 +523,7 @@ export const ko: Record<string, string> = {
|
|
|
523
523
|
"Quit the terminal app.": "터미널 앱을 종료합니다.",
|
|
524
524
|
"Copy the active terminal selection.": "활성 터미널 선택을 복사합니다.",
|
|
525
525
|
"Paste clipboard text into the active input.": "클립보드 텍스트를 활성 입력에 붙여넣습니다.",
|
|
526
|
-
"
|
|
526
|
+
"Restart into a downloaded app update, or retry one that failed.": "다운로드한 업데이트로 다시 시작하거나, 실패한 업데이트를 다시 시도합니다.",
|
|
527
527
|
"Close the focused pane, docked or floating.": "도킹 또는 플로팅된 포커스 패널을 닫습니다.",
|
|
528
528
|
"Close all floating panes.": "모든 플로팅 패널을 닫습니다.",
|
|
529
529
|
"Edit settings for the focused pane.": "포커스 패널 설정을 편집합니다.",
|
|
@@ -955,7 +955,9 @@ export const ko: Record<string, string> = {
|
|
|
955
955
|
|
|
956
956
|
// ── Main dashboard ──────────────────────────────────────────
|
|
957
957
|
"Installing update...": "업데이트 설치 중...",
|
|
958
|
-
"Update
|
|
958
|
+
"Update ready, restart to apply": "업데이트 준비 완료. 적용하려면 다시 시작하세요",
|
|
959
|
+
"Update ready": "업데이트 준비 완료",
|
|
960
|
+
"Restart": "다시 시작",
|
|
959
961
|
"Checking for updates...": "업데이트 확인 중...",
|
|
960
962
|
"Downloading v{version}: {percent}%": "v{version} 다운로드 중: {percent}%",
|
|
961
963
|
"Update failed: {error}": "업데이트 실패: {error}",
|
package/src/i18n/zh-cn.ts
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
* fixed-width terminal layouts stay readable.
|
|
6
6
|
*/
|
|
7
7
|
export const zhCN: Record<string, string> = {
|
|
8
|
+
"NAV · as of {date}": "NAV · 截至{date}",
|
|
8
9
|
// ── Common UI ────────────────────────────────────────────────
|
|
9
10
|
"Back": "返回",
|
|
10
11
|
"Done": "完成",
|
|
@@ -378,7 +379,6 @@ export const zhCN: Record<string, string> = {
|
|
|
378
379
|
"Price trigger alerts with notifications": "带通知的价格触发提醒",
|
|
379
380
|
"Create a price alert from a symbol, condition, and target price": "用代码、条件和目标价创建价格提醒",
|
|
380
381
|
"Public market heatmap for the largest US stocks and ETFs.": "美股大盘股与 ETF 的市场热力图。",
|
|
381
|
-
"Largest US stocks and ETFs, sized by market cap or assets and colored by daily move.": "按市值/规模定大小、按日涨跌着色的美股与 ETF。",
|
|
382
382
|
"Top gainers, losers, most active, and trending tickers": "涨幅榜、跌幅榜、成交活跃与热门股票",
|
|
383
383
|
"Top gainers, losers, most active, and trending tickers.": "涨幅榜、跌幅榜、成交活跃与热门股票。",
|
|
384
384
|
"S&P 500 sector and industry performance via ETF proxies": "以 ETF 代理呈现的标普 500 板块与行业表现",
|
|
@@ -525,7 +525,7 @@ export const zhCN: Record<string, string> = {
|
|
|
525
525
|
"Quit the terminal app.": "退出终端应用。",
|
|
526
526
|
"Copy the active terminal selection.": "复制当前终端选中内容。",
|
|
527
527
|
"Paste clipboard text into the active input.": "把剪贴板文本粘贴到当前输入框。",
|
|
528
|
-
"
|
|
528
|
+
"Restart into a downloaded app update, or retry one that failed.": "使用已下载的更新重启,或重试失败的更新。",
|
|
529
529
|
"Close the focused pane, docked or floating.": "关闭聚焦面板(停靠或浮动)。",
|
|
530
530
|
"Close all floating panes.": "关闭所有浮动面板。",
|
|
531
531
|
"Edit settings for the focused pane.": "编辑聚焦面板的设置。",
|
|
@@ -957,7 +957,9 @@ export const zhCN: Record<string, string> = {
|
|
|
957
957
|
|
|
958
958
|
// ── Main dashboard ──────────────────────────────────────────
|
|
959
959
|
"Installing update...": "正在安装更新...",
|
|
960
|
-
"Update
|
|
960
|
+
"Update ready, restart to apply": "更新已就绪,重启后生效",
|
|
961
|
+
"Update ready": "更新已就绪",
|
|
962
|
+
"Restart": "重启",
|
|
961
963
|
"Checking for updates...": "正在检查更新...",
|
|
962
964
|
"Downloading v{version}: {percent}%": "正在下载 v{version}:{percent}%",
|
|
963
965
|
"Update failed: {error}": "更新失败:{error}",
|
package/src/i18n/zh-tw.ts
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
* fixed-width terminal layouts stay readable.
|
|
6
6
|
*/
|
|
7
7
|
export const zhTW: Record<string, string> = {
|
|
8
|
+
"NAV · as of {date}": "NAV · 截至{date}",
|
|
8
9
|
// ── Common UI ────────────────────────────────────────────────
|
|
9
10
|
"Back": "返回",
|
|
10
11
|
"Done": "完成",
|
|
@@ -378,7 +379,6 @@ export const zhTW: Record<string, string> = {
|
|
|
378
379
|
"Price trigger alerts with notifications": "透過通知的價格觸發提醒",
|
|
379
380
|
"Create a price alert from a symbol, condition, and target price": "用代號、條件和目標價建立價格提醒",
|
|
380
381
|
"Public market heatmap for the largest US stocks and ETFs.": "美股大盤股與 ETF 的市場熱力圖。",
|
|
381
|
-
"Largest US stocks and ETFs, sized by market cap or assets and colored by daily move.": "按市值/規模定大小、按日漲跌著色的美股與 ETF。",
|
|
382
382
|
"Top gainers, losers, most active, and trending tickers": "漲幅榜、跌幅榜、成交活躍與熱門股票",
|
|
383
383
|
"Top gainers, losers, most active, and trending tickers.": "漲幅榜、跌幅榜、成交活躍與熱門股票。",
|
|
384
384
|
"S&P 500 sector and industry performance via ETF proxies": "以 ETF 代表 S&P 500 類股與產業表現",
|
|
@@ -525,7 +525,7 @@ export const zhTW: Record<string, string> = {
|
|
|
525
525
|
"Quit the terminal app.": "結束終端機應用程式。",
|
|
526
526
|
"Copy the active terminal selection.": "複製目前終端機選取內容。",
|
|
527
527
|
"Paste clipboard text into the active input.": "將剪貼簿文字貼到目前的輸入欄位。",
|
|
528
|
-
"
|
|
528
|
+
"Restart into a downloaded app update, or retry one that failed.": "使用已下載的更新重新啟動,或重試失敗的更新。",
|
|
529
529
|
"Close the focused pane, docked or floating.": "關閉聚焦面板(停靠或浮動)。",
|
|
530
530
|
"Close all floating panes.": "關閉所有浮動面板。",
|
|
531
531
|
"Edit settings for the focused pane.": "編輯聚焦面板的設定。",
|
|
@@ -957,7 +957,9 @@ export const zhTW: Record<string, string> = {
|
|
|
957
957
|
|
|
958
958
|
// ── Main dashboard ──────────────────────────────────────────
|
|
959
959
|
"Installing update...": "正在安裝更新...",
|
|
960
|
-
"Update
|
|
960
|
+
"Update ready, restart to apply": "更新已就緒,重新啟動後生效",
|
|
961
|
+
"Update ready": "更新已就緒",
|
|
962
|
+
"Restart": "重新啟動",
|
|
961
963
|
"Checking for updates...": "正在檢查更新...",
|
|
962
964
|
"Downloading v{version}: {percent}%": "正在下載 v{version}:{percent}%",
|
|
963
965
|
"Update failed: {error}": "更新失敗:{error}",
|
|
@@ -22,8 +22,8 @@ export function listVisibleTickerSourcePanes(
|
|
|
22
22
|
}
|
|
23
23
|
|
|
24
24
|
/**
|
|
25
|
-
* Whether the pane menu may link this pane to
|
|
26
|
-
* never follows: its own cursor symbol would win over the binding.
|
|
25
|
+
* Whether the pane menu may link this pane to another pane's ticker (`PaneDef.tickerFollower`).
|
|
26
|
+
* A source never follows: its own cursor symbol would win over the binding.
|
|
27
27
|
*/
|
|
28
28
|
export function canFollowTickerSource(
|
|
29
29
|
instance: PaneInstanceConfig,
|
|
@@ -34,6 +34,32 @@ export function canFollowTickerSource(
|
|
|
34
34
|
return typeof def.tickerFollower === "function" ? def.tickerFollower(instance) : def.tickerFollower === true;
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
+
/**
|
|
38
|
+
* A single-ticker pane other panes can follow. Lists and scanners stay sources; Ticker Research
|
|
39
|
+
* follows the same rule as any pane flagged `tickerFollower`.
|
|
40
|
+
*/
|
|
41
|
+
export function isTickerLinkPeer(
|
|
42
|
+
instance: PaneInstanceConfig,
|
|
43
|
+
panes: ReadonlyMap<string, PaneDef>,
|
|
44
|
+
): boolean {
|
|
45
|
+
return instance.paneId === TICKER_RESEARCH_PANE_ID || canFollowTickerSource(instance, panes);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** True when `fromInstanceId`'s follow chain reaches `targetInstanceId`. */
|
|
49
|
+
export function followBindingReaches(
|
|
50
|
+
layout: LayoutConfig,
|
|
51
|
+
fromInstanceId: string,
|
|
52
|
+
targetInstanceId: string,
|
|
53
|
+
seen = new Set<string>(),
|
|
54
|
+
): boolean {
|
|
55
|
+
if (seen.has(fromInstanceId)) return false;
|
|
56
|
+
seen.add(fromInstanceId);
|
|
57
|
+
const instance = findPaneInstance(layout, fromInstanceId);
|
|
58
|
+
if (instance?.binding?.kind !== "follow") return false;
|
|
59
|
+
const next = instance.binding.sourceInstanceId;
|
|
60
|
+
return next === targetInstanceId || followBindingReaches(layout, next, targetInstanceId, seen);
|
|
61
|
+
}
|
|
62
|
+
|
|
37
63
|
/** The visible Ticker Research pane that follows `sourceInstanceId`, if any. */
|
|
38
64
|
export function findTickerResearchFollower(
|
|
39
65
|
layout: LayoutConfig,
|
|
@@ -152,6 +152,12 @@ export function mergeQuoteContribution(
|
|
|
152
152
|
}
|
|
153
153
|
if (!current) return next;
|
|
154
154
|
|
|
155
|
+
// NAV responses are complete dated observations. Never carry an earlier
|
|
156
|
+
// date or intraday session fields into one, or its marker into a later tick.
|
|
157
|
+
if (current.priceObservation != null || next.priceObservation != null) {
|
|
158
|
+
return { ...next, name: next.name ?? current.name };
|
|
159
|
+
}
|
|
160
|
+
|
|
155
161
|
const priorBasis = resolvePriceBasis(current.priceBasis, current.instrumentType);
|
|
156
162
|
const samePriceBasis = priorBasis !== null && priorBasis === resolvePriceBasis(next.priceBasis, next.instrumentType || current.instrumentType);
|
|
157
163
|
const merged: QuoteContribution = {
|