gloomberb 0.17.3 → 0.17.5
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 +2 -1
- package/patches/electrobun@1.18.1.patch +80 -0
- package/src/api-client/chat.ts +28 -0
- package/src/api-client/data.ts +4 -1
- package/src/api-client/errors.ts +11 -1
- package/src/api-client/index.ts +4 -1
- package/src/api-client/normalizers.ts +21 -0
- package/src/api-client/research-activity.ts +10 -3
- package/src/api-client/socket.ts +27 -0
- package/src/app/pane-runtime/ticker-open-runtime.ts +2 -1
- package/src/app/runtime/startup.ts +4 -0
- package/src/app/runtime/update.ts +50 -3
- package/src/app.tsx +2 -0
- package/src/ask-gloom/pane-context.ts +11 -1
- package/src/cli/commands/overview.ts +2 -2
- package/src/cli/commands/system.ts +19 -1
- package/src/cli/glossary.ts +34 -4
- package/src/cli/index.ts +6 -0
- package/src/cli/pane-functions/function-help.ts +2 -2
- package/src/cli/pane-functions/market-session.ts +18 -3
- package/src/components/chart/composite/format.ts +7 -0
- package/src/components/chart/composite/legend.tsx +8 -11
- package/src/components/command-bar/commands/direct/index.ts +47 -0
- package/src/components/command-bar/commands/registry.ts +8 -0
- package/src/components/command-bar/panel/prompt-binding.ts +8 -0
- package/src/components/command-bar/routes/root/command-items.ts +16 -1
- package/src/components/command-bar/surface/index.tsx +2 -1
- package/src/components/command-bar/workflow/ops.ts +4 -3
- package/src/components/form-modal/confirm.tsx +1 -0
- package/src/components/form-modal/request.ts +2 -0
- package/src/components/index.ts +1 -1
- package/src/components/layout/detached-pane-shell.tsx +6 -1
- package/src/components/layout/pane/sidebar.tsx +24 -11
- package/src/components/layout/shell/index.tsx +5 -1
- package/src/components/layout/shell/menu.ts +8 -0
- package/src/components/layout/shell/pane/capture.ts +29 -12
- package/src/components/metric-treemap/heat-surface.tsx +76 -14
- package/src/components/metric-treemap/heat-tiles.ts +5 -3
- package/src/components/metric-treemap/index.tsx +1 -1
- package/src/components/ui/button.tsx +24 -9
- package/src/components/ui/confirm-dialog.tsx +30 -4
- package/src/components/ui/fields.tsx +5 -0
- package/src/components/ui/index.ts +1 -1
- package/src/components/ui/status.tsx +148 -5
- package/src/data/config/store/normalize.ts +8 -0
- package/src/i18n/es.ts +2 -0
- package/src/i18n/ja.ts +2 -0
- package/src/i18n/ko.ts +2 -0
- package/src/i18n/zh-cn.ts +2 -0
- package/src/i18n/zh-tw.ts +2 -0
- package/src/layout-marketplace/gallery-desktop.tsx +2 -2
- package/src/market-data/coordinator/events.ts +5 -1
- package/src/market-data/coordinator/index.ts +4 -0
- package/src/market-data/coordinator/quotes.ts +4 -0
- package/src/market-data/futures-board.ts +44 -1
- package/src/market-data/hooks.tsx +168 -29
- package/src/plugins/builtin/account-management/calendar-tab.tsx +1 -1
- package/src/plugins/builtin/alerts/cli.ts +72 -11
- package/src/plugins/builtin/awards/headless.ts +5 -1
- package/src/plugins/builtin/broker-manager/detail.tsx +2 -2
- package/src/plugins/builtin/broker-manager/model.ts +2 -2
- package/src/plugins/builtin/catalysts/headless.ts +5 -1
- package/src/plugins/builtin/chart-composer/headless.ts +2 -1
- package/src/plugins/builtin/chart-composer/pane.tsx +20 -2
- package/src/plugins/builtin/chat/content/footer.tsx +5 -2
- package/src/plugins/builtin/chat/controller/connections.ts +18 -0
- package/src/plugins/builtin/chat/controller/fetch.ts +5 -2
- package/src/plugins/builtin/chat/controller/index.ts +69 -21
- package/src/plugins/builtin/chat/controller/messages.ts +14 -2
- package/src/plugins/builtin/chat/controller/notifications.ts +132 -72
- package/src/plugins/builtin/chat/controller/realtime.ts +37 -1
- package/src/plugins/builtin/chat/controller/state.ts +15 -0
- package/src/plugins/builtin/chat/controller/unread-people.ts +240 -0
- package/src/plugins/builtin/chat/controller/view.ts +2 -0
- package/src/plugins/builtin/chat/index.tsx +1 -1
- package/src/plugins/builtin/chat/share/flow.tsx +18 -9
- package/src/plugins/builtin/chat/unread-badge.ts +38 -7
- package/src/plugins/builtin/cloud/askg/pane-attachment.ts +2 -3
- package/src/plugins/builtin/cot/model.ts +20 -0
- package/src/plugins/builtin/custom-view/pane.tsx +2 -2
- package/src/plugins/builtin/docs/pane.tsx +1 -1
- package/src/plugins/builtin/futures/index.tsx +2 -3
- package/src/plugins/builtin/futures/model.ts +4 -2
- package/src/plugins/builtin/futures/series.ts +3 -3
- package/src/plugins/builtin/futures-curve/headless.ts +13 -6
- package/src/plugins/builtin/futures-curve/index.tsx +3 -3
- package/src/plugins/builtin/futures-curve/model.ts +64 -4
- package/src/plugins/builtin/futures-curve/pane.tsx +11 -4
- package/src/plugins/builtin/jobs/pane.tsx +12 -9
- package/src/plugins/builtin/kelly-sizer/headless.ts +1 -1
- package/src/plugins/builtin/kelly-sizer/history.ts +5 -3
- package/src/plugins/builtin/kelly-sizer/pane.tsx +1 -1
- package/src/plugins/builtin/macro-day/release-table.ts +3 -3
- package/src/plugins/builtin/market-heatmap/index.tsx +6 -15
- package/src/plugins/builtin/mna/model.ts +2 -1
- package/src/plugins/builtin/news/headless.ts +4 -2
- package/src/plugins/builtin/options-positioning/model.ts +14 -0
- package/src/plugins/builtin/options-positioning/pane.tsx +17 -10
- package/src/plugins/builtin/perps/client.ts +77 -6
- package/src/plugins/builtin/perps/headless.ts +25 -14
- package/src/plugins/builtin/perps/index.tsx +1 -1
- package/src/plugins/builtin/perps/model.ts +32 -0
- package/src/plugins/builtin/perps/pane.tsx +5 -4
- package/src/plugins/builtin/plugin-marketplace/pane.tsx +1 -1
- package/src/plugins/builtin/rate-path/model.ts +1 -1
- package/src/plugins/builtin/rate-path/pane.tsx +2 -1
- package/src/plugins/builtin/research/equity-diagnostic-pane.tsx +2 -3
- package/src/plugins/builtin/research/event-sources.ts +1 -1
- package/src/plugins/builtin/research/relative-valuation-model.ts +6 -0
- package/src/plugins/builtin/research/relative-valuation-pane.tsx +3 -3
- package/src/plugins/builtin/sectors/client.ts +46 -19
- package/src/plugins/builtin/sectors/sector-model.ts +22 -10
- package/src/plugins/builtin/thirteenf/api.ts +60 -43
- package/src/plugins/builtin/thirteenf/data.ts +76 -35
- package/src/plugins/builtin/thirteenf/headless.ts +2 -0
- package/src/plugins/builtin/thirteenf/pane.tsx +10 -2
- package/src/plugins/builtin/yield-curve/curves.ts +15 -1
- package/src/plugins/builtin/yield-curve/headless.ts +17 -7
- package/src/plugins/builtin/yield-curve/history.ts +2 -0
- package/src/plugins/builtin/yield-curve/index.tsx +23 -18
- package/src/plugins/builtin/yield-curve/series.ts +19 -9
- package/src/plugins/builtin/yield-curve/treasury-data.ts +2 -0
- package/src/plugins/builtin/yield-curve/world.ts +4 -0
- package/src/plugins/registry/host-actions.ts +3 -0
- package/src/renderers/browser/app-services.ts +7 -1
- package/src/renderers/browser/config-host.ts +55 -2
- package/src/renderers/browser/first-screen.ts +193 -0
- package/src/renderers/browser/main.tsx +59 -7
- package/src/renderers/dom/build-assets.ts +17 -4
- package/src/renderers/dom/desktop/controls.tsx +4 -0
- package/src/renderers/electrobun/bun/application-menu/index.ts +1 -0
- package/src/renderers/electrobun/bun/desktop/backend-requests.ts +3 -0
- package/src/renderers/electrobun/bun/desktop/macos-dock-badge.ts +4 -3
- package/src/renderers/electrobun/bun/desktop/macos-dock-icon.ts +2 -3
- package/src/renderers/electrobun/bun/desktop/macos-objc.ts +17 -6
- package/src/renderers/electrobun/bun/desktop/update.ts +115 -12
- package/src/renderers/electrobun/shared/protocol.ts +3 -2
- package/src/renderers/electrobun/view/update-host.ts +2 -2
- package/src/shares/bug-report-dialog.tsx +162 -0
- package/src/shares/bug-report-flow.tsx +84 -0
- package/src/shares/bug-report.ts +42 -0
- package/src/shares/capture-flow.ts +19 -0
- package/src/shares/capture-preview.tsx +129 -0
- package/src/shares/chat.ts +10 -1
- package/src/shares/dialog.tsx +16 -91
- package/src/shares/flow.tsx +12 -16
- package/src/shares/personal-pane.ts +35 -1
- package/src/sources/gloomberb-cloud/normalizers.ts +12 -4
- package/src/telemetry/usage-counts.ts +16 -0
- package/src/tickers/open-target.ts +61 -11
- package/src/time-series/resolve.ts +26 -9
- package/src/time-series/spec.ts +8 -0
- package/src/types/config.ts +14 -0
- package/src/ui/resize-settle.ts +128 -0
- package/src/updater/channel.ts +51 -0
- package/src/updater/github-releases.ts +85 -16
- package/src/updater/index.ts +125 -16
- package/src/utils/calendar-date.ts +56 -0
- package/src/utils/datetime-format.ts +7 -1
- package/src/utils/decimal-comma.ts +31 -0
- package/src/utils/futures-generic.ts +25 -4
- package/src/utils/plugin-proxy-hosts.json +5 -0
- package/src/utils/semver.ts +47 -2
- package/src/utils/utc-time.ts +14 -3
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { memo, useEffect, useMemo, useRef, useState, type CSSProperties } from "react";
|
|
2
2
|
import { Box, Text, TextAttributes, useUiCapabilities, type BoxRenderable } from "../../ui";
|
|
3
3
|
import { useLiveStretch } from "../../ui/live-stretch";
|
|
4
|
+
import { useSettledLayoutSize } from "../../ui/resize-settle";
|
|
4
5
|
import { blendHex } from "../../theme/colors";
|
|
5
6
|
import {
|
|
6
7
|
heatmapColorDistance,
|
|
@@ -91,9 +92,57 @@ export function buildHeatTreemapScene<T>(
|
|
|
91
92
|
return buildMetricTreemapScene(items, canvas.width, canvas.height, canvas.options, previous);
|
|
92
93
|
}
|
|
93
94
|
|
|
95
|
+
export interface HeatTreemapLayout<T> {
|
|
96
|
+
scene: MetricTreemapScene<T>;
|
|
97
|
+
/** The box is being resized (`useSettledLayoutSize`): hand it to the surface, which then holds still. */
|
|
98
|
+
resizing: boolean;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
interface ShownScene<T> {
|
|
102
|
+
key: string;
|
|
103
|
+
canvas: HeatTreemapCanvas;
|
|
104
|
+
items: readonly MetricTreemapItem<T>[];
|
|
105
|
+
scene: MetricTreemapScene<T>;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* The scene a heat map shows in a `width` by `height` box (cells). Each
|
|
110
|
+
* layout is seeded with the scene on screen under the same `key` (a tab), so
|
|
111
|
+
* a refresh nudges tiles instead of reshuffling the board. On the desktop,
|
|
112
|
+
* while the box is being resized the scene on screen stretches with it and a
|
|
113
|
+
* refresh's new sizes wait: the board is laid out once, at the size the box
|
|
114
|
+
* settles on.
|
|
115
|
+
*/
|
|
116
|
+
export function useHeatTreemapScene<T>(
|
|
117
|
+
items: readonly MetricTreemapItem<T>[],
|
|
118
|
+
width: number,
|
|
119
|
+
height: number,
|
|
120
|
+
key = "",
|
|
121
|
+
): HeatTreemapLayout<T> {
|
|
122
|
+
const { nativePaneChrome, cellWidthPx, cellHeightPx } = useUiCapabilities();
|
|
123
|
+
const layout = useSettledLayoutSize(width, height, { enabled: nativePaneChrome === true });
|
|
124
|
+
const canvas = useMemo(
|
|
125
|
+
() => heatTreemapCanvas(layout.width, layout.height, { nativePaneChrome, cellWidthPx, cellHeightPx }),
|
|
126
|
+
[cellHeightPx, cellWidthPx, layout.height, layout.width, nativePaneChrome],
|
|
127
|
+
);
|
|
128
|
+
const shownRef = useRef<ShownScene<T> | null>(null);
|
|
129
|
+
const scene = useMemo(() => {
|
|
130
|
+
const shown = shownRef.current?.key === key ? shownRef.current : null;
|
|
131
|
+
// While the box resizes, the scene on screen holds until it is laid out
|
|
132
|
+
// again, a refresh's new sizes with it: the board relays out once.
|
|
133
|
+
if (shown && shown.canvas === canvas && (layout.resizing || shown.items === items)) return shown.scene;
|
|
134
|
+
const next = buildHeatTreemapScene(items, canvas, shown?.scene);
|
|
135
|
+
shownRef.current = { key, canvas, items, scene: next };
|
|
136
|
+
return next;
|
|
137
|
+
}, [canvas, items, key, layout.resizing]);
|
|
138
|
+
return { scene, resizing: layout.resizing };
|
|
139
|
+
}
|
|
140
|
+
|
|
94
141
|
export interface HeatTreemapSurfaceProps<T> {
|
|
95
|
-
/** Geometry, from `
|
|
142
|
+
/** Geometry, from `useHeatTreemapScene`; rebuilt only when sizes change. */
|
|
96
143
|
scene: MetricTreemapScene<T>;
|
|
144
|
+
/** From `useHeatTreemapScene`: while the box resizes, no tile glides, fades or pulses. */
|
|
145
|
+
resizing: boolean;
|
|
97
146
|
/** Current colours and text, by id; they change with every tick. */
|
|
98
147
|
items: readonly MetricTreemapItem<T>[];
|
|
99
148
|
width: number;
|
|
@@ -223,7 +272,7 @@ const VERTICAL_LABEL_STYLE: CSSProperties = {
|
|
|
223
272
|
|
|
224
273
|
const DomHeatTile = memo(function DomHeatTile({
|
|
225
274
|
id, rect, canvasWidth, canvasHeight, gap, label, value, valueSuffix, background, foreground,
|
|
226
|
-
selected, reducedMotion, glide, pulseCount, events,
|
|
275
|
+
selected, reducedMotion, resizing, glide, pulseCount, events,
|
|
227
276
|
}: {
|
|
228
277
|
id: string;
|
|
229
278
|
rect: TreemapRect;
|
|
@@ -237,6 +286,8 @@ const DomHeatTile = memo(function DomHeatTile({
|
|
|
237
286
|
foreground: string;
|
|
238
287
|
selected: boolean;
|
|
239
288
|
reducedMotion: boolean;
|
|
289
|
+
resizing: boolean;
|
|
290
|
+
/** A relayout this render may slide the tile: false for one that came with a new size. */
|
|
240
291
|
glide: boolean;
|
|
241
292
|
pulseCount: number;
|
|
242
293
|
events: SurfaceEvents;
|
|
@@ -248,16 +299,22 @@ const DomHeatTile = memo(function DomHeatTile({
|
|
|
248
299
|
});
|
|
249
300
|
// Where this tile sat before the last relayout, kept to slide it from there.
|
|
250
301
|
const placedRef = useRef(rect);
|
|
251
|
-
const glideRef = useRef<{ dx: number; dy: number; count: number }>({ dx: 0, dy: 0, count: 0 });
|
|
302
|
+
const glideRef = useRef<{ dx: number; dy: number; count: number; playing: boolean }>({ dx: 0, dy: 0, count: 0, playing: false });
|
|
303
|
+
const allowGlide = glide && !resizing;
|
|
252
304
|
if (placedRef.current !== rect) {
|
|
253
305
|
const dx = placedRef.current.x - rect.x;
|
|
254
306
|
const dy = placedRef.current.y - rect.y;
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
307
|
+
// A slide belongs to the relayout that moved the tile: a later render
|
|
308
|
+
// (a tick, a hover, the end of a resize) never plays it again.
|
|
309
|
+
glideRef.current = allowGlide && (Math.abs(dx) >= 0.5 || Math.abs(dy) >= 0.5)
|
|
310
|
+
? { dx, dy, count: glideRef.current.count + 1, playing: true }
|
|
311
|
+
: { ...glideRef.current, playing: false };
|
|
258
312
|
placedRef.current = rect;
|
|
313
|
+
} else if (resizing && glideRef.current.playing) {
|
|
314
|
+
// A resize stops a slide under way: the tile sits where its layout has it.
|
|
315
|
+
glideRef.current = { ...glideRef.current, playing: false };
|
|
259
316
|
}
|
|
260
|
-
const motion = heatTileMotion({ reducedMotion, glide, glideCount: glideRef.current.count, pulseCount });
|
|
317
|
+
const motion = heatTileMotion({ reducedMotion, resizing, glide: glideRef.current.playing, glideCount: glideRef.current.count, pulseCount });
|
|
261
318
|
const colors = useThemeColors();
|
|
262
319
|
const ring = resolveHeatmapSelectionRing(colors);
|
|
263
320
|
// The colour this tile showed before, kept to fade out over the new one.
|
|
@@ -274,10 +331,13 @@ const DomHeatTile = memo(function DomHeatTile({
|
|
|
274
331
|
else if (visible) fadeRef.current = { from: shownRef.current, key: (fadeRef.current?.key ?? 0) + 1 };
|
|
275
332
|
shownRef.current = background;
|
|
276
333
|
}
|
|
334
|
+
// A resize ends a fade under way, and the pulses due meanwhile are dropped, not saved for later.
|
|
335
|
+
if (resizing) fadeRef.current = null;
|
|
277
336
|
const fade = fadeRef.current;
|
|
278
337
|
// A finished overlay leaves the DOM, so idle tiles carry no extra nodes.
|
|
279
338
|
const [, setOverlayVersion] = useState(0);
|
|
280
339
|
const pulseDoneRef = useRef(0);
|
|
340
|
+
if (resizing) pulseDoneRef.current = Math.max(pulseDoneRef.current, pulseCount);
|
|
281
341
|
const pulsing = motion.pulseAnimation != null && pulseCount > pulseDoneRef.current;
|
|
282
342
|
const style: CSSProperties = {
|
|
283
343
|
position: "absolute",
|
|
@@ -491,7 +551,7 @@ function DomTooltip<T>({ state, itemsById, otherLines }: {
|
|
|
491
551
|
const EMPTY_PULSES: ReadonlyMap<string, number> = new Map();
|
|
492
552
|
|
|
493
553
|
function DomHeatTreemap<T>({
|
|
494
|
-
scene, items, width, height, selectedId, onSelect, onActivate, otherTooltip, otherSummary: summarizeOther,
|
|
554
|
+
scene, resizing, items, width, height, selectedId, onSelect, onActivate, otherTooltip, otherSummary: summarizeOther,
|
|
495
555
|
}: HeatTreemapSurfaceProps<T>) {
|
|
496
556
|
const reducedMotion = usePrefersReducedMotion();
|
|
497
557
|
const colors = useThemeColors();
|
|
@@ -519,13 +579,14 @@ function DomHeatTreemap<T>({
|
|
|
519
579
|
);
|
|
520
580
|
}, [itemsById, reducedMotion, scene]);
|
|
521
581
|
|
|
522
|
-
//
|
|
582
|
+
// The layout a resize settles on comes in at once, as a chart or a table
|
|
583
|
+
// redraws at its new size: tiles slide only when sizes move at rest.
|
|
523
584
|
const sizeKey = `${scene.width}x${scene.height}`;
|
|
524
|
-
const
|
|
525
|
-
const glide =
|
|
585
|
+
const restedRef = useRef({ sizeKey, resizing });
|
|
586
|
+
const glide = !resizing && !restedRef.current.resizing && restedRef.current.sizeKey === sizeKey;
|
|
526
587
|
useEffect(() => {
|
|
527
|
-
|
|
528
|
-
}, [sizeKey]);
|
|
588
|
+
restedRef.current = { sizeKey, resizing };
|
|
589
|
+
}, [resizing, sizeKey]);
|
|
529
590
|
|
|
530
591
|
const gap = scene.width < NARROW_CANVAS_PX ? 0.5 : 1;
|
|
531
592
|
const otherLines = useMemo(
|
|
@@ -603,6 +664,7 @@ function DomHeatTreemap<T>({
|
|
|
603
664
|
foreground={tileColors.foreground}
|
|
604
665
|
selected={tile.item.id === selectedId}
|
|
605
666
|
reducedMotion={reducedMotion}
|
|
667
|
+
resizing={resizing}
|
|
606
668
|
glide={glide}
|
|
607
669
|
pulseCount={pulses.get(tile.item.id) ?? 0}
|
|
608
670
|
events={events}
|
|
@@ -755,7 +817,7 @@ function TerminalHeatTreemap<T>({
|
|
|
755
817
|
* calm tiles (ticker, then the move, as space allows), a continuous scale
|
|
756
818
|
* from the theme's down colour to its up colour, and on the desktop a hover
|
|
757
819
|
* tooltip, colour fades, a short pulse on a tile whose move jumps and a glide
|
|
758
|
-
* when sizes change.
|
|
820
|
+
* when sizes change, none of them while the box is being resized.
|
|
759
821
|
*/
|
|
760
822
|
export function HeatTreemapSurface<T>(props: HeatTreemapSurfaceProps<T>) {
|
|
761
823
|
const { nativePaneChrome } = useUiCapabilities();
|
|
@@ -240,15 +240,17 @@ export interface HeatTileMotion {
|
|
|
240
240
|
* tile from its old place (its new size applies at once), a colour change
|
|
241
241
|
* fades the old colour out over the new one, and a jump in the move flashes
|
|
242
242
|
* once. `glideCount` alternates two keyframe names, which restarts the slide.
|
|
243
|
-
* Reduced motion keeps every change instant
|
|
243
|
+
* Reduced motion keeps every change instant, and so does a resize: while the
|
|
244
|
+
* box is changing size nothing on the board moves on its own.
|
|
244
245
|
*/
|
|
245
|
-
export function heatTileMotion({ reducedMotion, glide, glideCount, pulseCount }: {
|
|
246
|
+
export function heatTileMotion({ reducedMotion, resizing, glide, glideCount, pulseCount }: {
|
|
246
247
|
reducedMotion: boolean;
|
|
248
|
+
resizing: boolean;
|
|
247
249
|
glide: boolean;
|
|
248
250
|
glideCount: number;
|
|
249
251
|
pulseCount: number;
|
|
250
252
|
}): HeatTileMotion {
|
|
251
|
-
if (reducedMotion) return {};
|
|
253
|
+
if (reducedMotion || resizing) return {};
|
|
252
254
|
return {
|
|
253
255
|
...(glide && glideCount > 0
|
|
254
256
|
? { glideAnimation: `gloom-heat-glide-${glideCount % 2 === 0 ? "b" : "a"} ${GLIDE_MS}ms ease` }
|
|
@@ -18,7 +18,7 @@ export {
|
|
|
18
18
|
type MetricTreemapDirection,
|
|
19
19
|
type MetricTreemapItem,
|
|
20
20
|
} from "./layout";
|
|
21
|
-
export { buildHeatTreemapScene, heatTreemapCanvas, HeatTreemapSurface } from "./heat-surface";
|
|
21
|
+
export { buildHeatTreemapScene, heatTreemapCanvas, HeatTreemapSurface, useHeatTreemapScene } from "./heat-surface";
|
|
22
22
|
export { formatHeatTileMove, settleTreemapLayoutItems } from "./heat-tiles";
|
|
23
23
|
|
|
24
24
|
type PreventableMouseEvent = { preventDefault(): void };
|
|
@@ -26,6 +26,14 @@ export interface ButtonProps {
|
|
|
26
26
|
onPress?: () => void;
|
|
27
27
|
variant?: ButtonVariant;
|
|
28
28
|
disabled?: boolean;
|
|
29
|
+
/**
|
|
30
|
+
* The action it started is still running (a retry waiting for its answer).
|
|
31
|
+
* It reads as disabled and ignores presses, but keeps its key, so it does not
|
|
32
|
+
* change size and the key does not fall through to something else.
|
|
33
|
+
*/
|
|
34
|
+
pending?: boolean;
|
|
35
|
+
/** What it says while pending, such as "Retrying...". Without it the label stays. */
|
|
36
|
+
pendingLabel?: string;
|
|
29
37
|
active?: boolean;
|
|
30
38
|
shortcut?: string;
|
|
31
39
|
/**
|
|
@@ -86,9 +94,11 @@ export function Button({
|
|
|
86
94
|
displayLabel,
|
|
87
95
|
children,
|
|
88
96
|
expanded,
|
|
89
|
-
onPress,
|
|
97
|
+
onPress: pressAction,
|
|
90
98
|
variant = "secondary",
|
|
91
99
|
disabled = false,
|
|
100
|
+
pending = false,
|
|
101
|
+
pendingLabel,
|
|
92
102
|
active = false,
|
|
93
103
|
shortcut,
|
|
94
104
|
desktopShortcut,
|
|
@@ -100,17 +110,21 @@ export function Button({
|
|
|
100
110
|
title,
|
|
101
111
|
}: ButtonProps) {
|
|
102
112
|
const colors = useThemeColors();
|
|
103
|
-
const label = t(rawLabel);
|
|
113
|
+
const label = t(pending && pendingLabel ? pendingLabel : rawLabel);
|
|
114
|
+
if (pending && pendingLabel) displayLabel = undefined;
|
|
115
|
+
// A pending press is ignored, but the button stays its scope's primary action.
|
|
116
|
+
const onPress = pressAction && pending ? () => {} : pressAction;
|
|
104
117
|
const scopedShortcut = useScopedButtonAction(label, onPress, disabled);
|
|
105
118
|
shortcut = shortcut ?? scopedShortcut;
|
|
119
|
+
const inert = disabled || pending;
|
|
106
120
|
const remoteScope = useRemoteUiScope();
|
|
107
121
|
useRemoteUiNode({
|
|
108
122
|
role: "button",
|
|
109
123
|
label,
|
|
110
|
-
disabled,
|
|
124
|
+
disabled: inert,
|
|
111
125
|
actions: {
|
|
112
126
|
press: () => {
|
|
113
|
-
if (!
|
|
127
|
+
if (!inert) onPress?.();
|
|
114
128
|
},
|
|
115
129
|
},
|
|
116
130
|
metadata: { variant, active, shortcut, expanded, ...(remoteScope ? { scope: remoteScope } : {}) },
|
|
@@ -124,7 +138,8 @@ export function Button({
|
|
|
124
138
|
expanded={expanded}
|
|
125
139
|
onPress={onPress}
|
|
126
140
|
variant={variant}
|
|
127
|
-
disabled={
|
|
141
|
+
disabled={inert}
|
|
142
|
+
pending={pending}
|
|
128
143
|
active={active}
|
|
129
144
|
shortcut={shortcut ?? desktopShortcut}
|
|
130
145
|
title={title}
|
|
@@ -137,7 +152,7 @@ export function Button({
|
|
|
137
152
|
);
|
|
138
153
|
}
|
|
139
154
|
|
|
140
|
-
const palette = resolveButtonColors(variant, active,
|
|
155
|
+
const palette = resolveButtonColors(variant, active, inert, colors);
|
|
141
156
|
|
|
142
157
|
return (
|
|
143
158
|
<Box
|
|
@@ -145,20 +160,20 @@ export function Button({
|
|
|
145
160
|
height={height ?? 1}
|
|
146
161
|
flexDirection="row"
|
|
147
162
|
backgroundColor={palette.bg}
|
|
148
|
-
cursor={
|
|
163
|
+
cursor={inert ? "default" : "pointer"}
|
|
149
164
|
onMouseDown={(event: { preventDefault?: () => void; stopPropagation?: () => void }) => {
|
|
150
165
|
if (stopPropagation) {
|
|
151
166
|
event.preventDefault?.();
|
|
152
167
|
event.stopPropagation?.();
|
|
153
168
|
}
|
|
154
|
-
if (!
|
|
169
|
+
if (!inert) onPress?.();
|
|
155
170
|
}}
|
|
156
171
|
>
|
|
157
172
|
{children ?? <Text fg={palette.fg} attributes={active ? TextAttributes.BOLD : 0}>
|
|
158
173
|
{compact ? displayLabel ?? label : ` ${displayLabel ?? label} `}
|
|
159
174
|
</Text>}
|
|
160
175
|
{shortcut && (
|
|
161
|
-
<Text fg={
|
|
176
|
+
<Text fg={inert ? colors.textMuted : colors.textDim}>
|
|
162
177
|
{` ${shortcut}`}
|
|
163
178
|
</Text>
|
|
164
179
|
)}
|
|
@@ -36,6 +36,12 @@ export interface ConfirmDialogProps extends PromptContext<boolean> {
|
|
|
36
36
|
* opens on its own while the person may be typing. Esc and `n` always answer.
|
|
37
37
|
*/
|
|
38
38
|
keyGraceMs?: number;
|
|
39
|
+
/**
|
|
40
|
+
* What Enter answers. `cancel` is for an opt-in a stray Enter must not
|
|
41
|
+
* accept: Enter takes the cancel button, which is drawn as the primary one,
|
|
42
|
+
* and only `y` or the confirm button confirms.
|
|
43
|
+
*/
|
|
44
|
+
defaultAction?: "confirm" | "cancel";
|
|
39
45
|
}
|
|
40
46
|
|
|
41
47
|
export function ConfirmDialog({
|
|
@@ -52,7 +58,9 @@ export function ConfirmDialog({
|
|
|
52
58
|
onClose,
|
|
53
59
|
formButtons = false,
|
|
54
60
|
keyGraceMs = 0,
|
|
61
|
+
defaultAction = "confirm",
|
|
55
62
|
}: ConfirmDialogProps) {
|
|
63
|
+
const enterCancels = defaultAction === "cancel";
|
|
56
64
|
const desktop = useUiHost().kind === "desktop-web";
|
|
57
65
|
const openedAt = useRef(Date.now());
|
|
58
66
|
const confirm = useCallback(() => resolve(true), [resolve]);
|
|
@@ -71,7 +79,13 @@ export function ConfirmDialog({
|
|
|
71
79
|
event.preventDefault();
|
|
72
80
|
return;
|
|
73
81
|
}
|
|
74
|
-
|
|
82
|
+
const enter = event.name === "enter" || event.name === "return";
|
|
83
|
+
if (enter && enterCancels) {
|
|
84
|
+
event.preventDefault();
|
|
85
|
+
cancel();
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
if (enter || isPlainKey(event, "y")) {
|
|
75
89
|
event.preventDefault();
|
|
76
90
|
if (Date.now() - openedAt.current < keyGraceMs) return;
|
|
77
91
|
confirm();
|
|
@@ -88,13 +102,25 @@ export function ConfirmDialog({
|
|
|
88
102
|
|
|
89
103
|
// The desktop shows the keys on the buttons instead of a footer line; a
|
|
90
104
|
// confirm with no key line keeps its buttons plain there too.
|
|
105
|
+
// One primary button, the one Enter answers.
|
|
91
106
|
const keys = footer !== "";
|
|
92
|
-
const confirmButton =
|
|
93
|
-
|
|
107
|
+
const confirmButton = (
|
|
108
|
+
<Button
|
|
109
|
+
label={confirmLabel}
|
|
110
|
+
variant={enterCancels && confirmVariant === "primary" ? "secondary" : confirmVariant}
|
|
111
|
+
disabled={busy}
|
|
112
|
+
desktopShortcut={keys ? (enterCancels ? "Y" : "Enter") : undefined}
|
|
113
|
+
onPress={confirm}
|
|
114
|
+
/>
|
|
115
|
+
);
|
|
116
|
+
const cancelButton = <Button label={cancelLabel} variant={enterCancels ? "primary" : "secondary"} disabled={busy} desktopShortcut={keys ? (enterCancels ? "Enter" : "Esc") : undefined} onPress={cancel} />;
|
|
117
|
+
const keyLine = enterCancels
|
|
118
|
+
? tf("y {action} · Enter or Esc {cancel}", { action: t(confirmLabel).toLowerCase(), cancel: t(cancelLabel).toLowerCase() })
|
|
119
|
+
: tf("Enter {action} · Esc cancel", { action: t(confirmLabel).toLowerCase() });
|
|
94
120
|
return (
|
|
95
121
|
<DialogFrame
|
|
96
122
|
title={title}
|
|
97
|
-
footer={footer ??
|
|
123
|
+
footer={footer ?? keyLine}
|
|
98
124
|
desktopFooter={null}
|
|
99
125
|
onClose={onClose}
|
|
100
126
|
>
|
|
@@ -48,6 +48,8 @@ export interface TextFieldProps {
|
|
|
48
48
|
labelWidth?: number;
|
|
49
49
|
value?: string;
|
|
50
50
|
placeholder?: string;
|
|
51
|
+
/** The most characters it takes; typing and pasting stop there. */
|
|
52
|
+
maxLength?: number;
|
|
51
53
|
focused?: boolean;
|
|
52
54
|
width?: number;
|
|
53
55
|
inputRef?: RefObject<InputRenderable | null>;
|
|
@@ -119,6 +121,7 @@ function TextFieldControl({
|
|
|
119
121
|
active,
|
|
120
122
|
value,
|
|
121
123
|
placeholder,
|
|
124
|
+
maxLength,
|
|
122
125
|
focused,
|
|
123
126
|
width,
|
|
124
127
|
inputRef,
|
|
@@ -163,6 +166,7 @@ function TextFieldControl({
|
|
|
163
166
|
label={label}
|
|
164
167
|
value={value}
|
|
165
168
|
placeholder={placeholder}
|
|
169
|
+
maxLength={maxLength}
|
|
166
170
|
focused={focused}
|
|
167
171
|
width={width}
|
|
168
172
|
inputRef={inputRef}
|
|
@@ -226,6 +230,7 @@ function TextFieldControl({
|
|
|
226
230
|
value={value}
|
|
227
231
|
selectable={!isPassword}
|
|
228
232
|
placeholder={isPassword ? "" : placeholder}
|
|
233
|
+
{...(maxLength === undefined ? {} : { maxLength })}
|
|
229
234
|
focused={focused}
|
|
230
235
|
textColor={isPassword ? backgroundColor : textColor}
|
|
231
236
|
placeholderColor={placeholderColor}
|
|
@@ -60,7 +60,7 @@ export type { TabsProps } from "./tabs";
|
|
|
60
60
|
export { SegmentedControl } from "./toggle";
|
|
61
61
|
export type { SegmentedControlProps } from "./toggle";
|
|
62
62
|
export { EmptyState, PaneStatusBody, Notice, loadingText, unavailableText } from "./status";
|
|
63
|
-
export type { EmptyStateProps, PaneStatusBodyProps, NoticeProps } from "./status";
|
|
63
|
+
export type { EmptyStateProps, PaneStatusBodyProps, NoticeProps, StatusRetry } from "./status";
|
|
64
64
|
export { Badge, BulletList, Divider, FigureList, KeyValueRow, Prose, READING_WIDTH, Section, SectionHeading } from "./display";
|
|
65
65
|
export type {
|
|
66
66
|
BadgeProps, BulletListProps, DividerProps, FigureListItem, FigureListProps, KeyValueRowProps, ProseProps, SectionProps,
|
|
@@ -1,21 +1,153 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { useEffect, useReducer, useRef, useState, type ReactNode } from "react";
|
|
2
2
|
import { t, tf } from "../../i18n";
|
|
3
3
|
import { useThemeColors } from "../../theme/theme-context";
|
|
4
4
|
import { Box, Text, useUiCapabilities } from "../../ui";
|
|
5
|
+
import { formatRelativeAge } from "../../utils/datetime-format";
|
|
5
6
|
import { Spinner } from "./loading";
|
|
6
7
|
import { ButtonActionScope } from "./action-scope";
|
|
8
|
+
import { Button } from "./button";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Asking again from an empty or failed state. The state draws the button
|
|
12
|
+
* (first, so it answers Enter), keeps it busy while the check runs, and when
|
|
13
|
+
* the answer comes back the same, says when it checked: otherwise a press that
|
|
14
|
+
* changes nothing on screen looks like a press that did nothing.
|
|
15
|
+
*/
|
|
16
|
+
export interface StatusRetry {
|
|
17
|
+
/** Asks again. A returned promise keeps the button busy until it settles. */
|
|
18
|
+
onRetry: () => unknown;
|
|
19
|
+
/** "Retry", unless the check has a better verb, such as "Look again". */
|
|
20
|
+
label?: string;
|
|
21
|
+
/** The label while it runs: "Retrying..." for the default label. */
|
|
22
|
+
pendingLabel?: string;
|
|
23
|
+
/**
|
|
24
|
+
* The same check running from anywhere (the button, `r`, a timer), such as
|
|
25
|
+
* a resource's `loading`. Needed when the old answer stays on screen while
|
|
26
|
+
* it runs, as an uncovered company does.
|
|
27
|
+
*/
|
|
28
|
+
pending?: boolean;
|
|
29
|
+
/** Said after "Checked just now." when an empty state comes back the same, such as "Still no careers system." */
|
|
30
|
+
unchanged?: string;
|
|
31
|
+
}
|
|
7
32
|
|
|
8
33
|
export interface EmptyStateProps {
|
|
9
34
|
title: string;
|
|
10
35
|
message?: string;
|
|
11
36
|
hint?: string;
|
|
12
37
|
actions?: ReactNode;
|
|
38
|
+
retry?: StatusRetry;
|
|
13
39
|
status?: "empty" | "error";
|
|
14
40
|
}
|
|
15
41
|
|
|
16
|
-
|
|
42
|
+
type ShownStatus = "empty" | "error" | null;
|
|
43
|
+
|
|
44
|
+
interface RetryFeedback {
|
|
45
|
+
pending: boolean;
|
|
46
|
+
press: () => void;
|
|
47
|
+
/** When a check ended on the state it started from; null once anything else is shown. */
|
|
48
|
+
checkedAt: number | null;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function isThenable(value: unknown): value is PromiseLike<unknown> {
|
|
52
|
+
return !!value && typeof (value as PromiseLike<unknown>).then === "function";
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* A check is the button's own promise, `retry.pending`, or the body showing
|
|
57
|
+
* its spinner (`shown === "loading"`) once a retry has been offered. When one
|
|
58
|
+
* ends on the empty or failed state it started from, the answer did not change.
|
|
59
|
+
*/
|
|
60
|
+
function useRetryFeedback(retry: StatusRetry | undefined, shown: ShownStatus | "loading"): RetryFeedback {
|
|
61
|
+
const [, rerender] = useReducer((count: number) => count + 1, 0);
|
|
62
|
+
const [checkedAt, setCheckedAt] = useState<number | null>(null);
|
|
63
|
+
const inFlight = useRef(0);
|
|
64
|
+
const mounted = useRef(true);
|
|
65
|
+
const track = useRef<{ offered: boolean; checking: boolean; from: ShownStatus; last: ShownStatus }>({
|
|
66
|
+
offered: false, checking: false, from: null, last: null,
|
|
67
|
+
});
|
|
68
|
+
if (retry) track.current.offered = true;
|
|
69
|
+
const pending = inFlight.current > 0 || retry?.pending === true;
|
|
70
|
+
const checking = pending || shown === "loading";
|
|
71
|
+
const begin = () => {
|
|
72
|
+
const state = track.current;
|
|
73
|
+
if (state.checking) return;
|
|
74
|
+
state.checking = true;
|
|
75
|
+
state.from = state.last;
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
useEffect(() => {
|
|
79
|
+
mounted.current = true;
|
|
80
|
+
return () => { mounted.current = false; };
|
|
81
|
+
}, []);
|
|
82
|
+
|
|
83
|
+
// After every commit: a check that just ended either confirms the state on
|
|
84
|
+
// screen or clears the line, and anything else shown clears it. A body that
|
|
85
|
+
// never offered a retry skips all of it.
|
|
86
|
+
useEffect(() => {
|
|
87
|
+
const state = track.current;
|
|
88
|
+
if (!state.offered) return;
|
|
89
|
+
if (checking) {
|
|
90
|
+
begin();
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
// `shown` is not "loading" here: that counts as checking.
|
|
94
|
+
if (state.checking) {
|
|
95
|
+
state.checking = false;
|
|
96
|
+
setCheckedAt(shown !== null && shown === state.from ? Date.now() : null);
|
|
97
|
+
} else if (shown !== state.last) {
|
|
98
|
+
setCheckedAt(null);
|
|
99
|
+
}
|
|
100
|
+
state.last = shown;
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
// "just now" turns into "2m ago" while the line stays up.
|
|
104
|
+
const shownCheckedAt = checking ? null : checkedAt;
|
|
105
|
+
useEffect(() => {
|
|
106
|
+
if (shownCheckedAt === null) return;
|
|
107
|
+
const timer = setInterval(rerender, 30_000);
|
|
108
|
+
return () => clearInterval(timer);
|
|
109
|
+
}, [shownCheckedAt]);
|
|
110
|
+
|
|
111
|
+
const press = () => {
|
|
112
|
+
if (!retry || pending) return;
|
|
113
|
+
const result = retry.onRetry();
|
|
114
|
+
if (!isThenable(result)) return;
|
|
115
|
+
begin();
|
|
116
|
+
inFlight.current += 1;
|
|
117
|
+
rerender();
|
|
118
|
+
const done = () => {
|
|
119
|
+
inFlight.current -= 1;
|
|
120
|
+
if (mounted.current) rerender();
|
|
121
|
+
};
|
|
122
|
+
result.then(done, done);
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
return { pending, press, checkedAt: shownCheckedAt };
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function retryNote(retry: StatusRetry, status: "empty" | "error", checkedAt: number): string {
|
|
129
|
+
const age = formatRelativeAge(checkedAt, Math.max(checkedAt, Date.now()));
|
|
130
|
+
if (status === "error") return tf("Tried again {age}.", { age });
|
|
131
|
+
const checked = tf("Checked {age}.", { age });
|
|
132
|
+
return retry.unchanged ? `${checked} ${t(retry.unchanged)}` : checked;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export function EmptyState(props: EmptyStateProps) {
|
|
136
|
+
const feedback = useRetryFeedback(props.retry, props.retry ? props.status ?? "empty" : null);
|
|
137
|
+
return <EmptyStateBody {...props} feedback={feedback} />;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function EmptyStateBody({ title, message, hint, actions, retry, status = "empty", feedback }: EmptyStateProps & { feedback: RetryFeedback }) {
|
|
17
141
|
const colors = useThemeColors();
|
|
18
142
|
const { nativePaneChrome } = useUiCapabilities();
|
|
143
|
+
const retryButton = retry ? (
|
|
144
|
+
<Button
|
|
145
|
+
label={retry.label ?? "Retry"}
|
|
146
|
+
pendingLabel={retry.pendingLabel ?? (retry.label ? undefined : "Retrying...")}
|
|
147
|
+
pending={feedback.pending}
|
|
148
|
+
onPress={feedback.press}
|
|
149
|
+
/>
|
|
150
|
+
) : null;
|
|
19
151
|
// Provider messages must wrap at narrow pane widths rather than lose their tail.
|
|
20
152
|
return (
|
|
21
153
|
<Box flexDirection="column" data-gloom-status={status} data-gloom-ui="empty-state">
|
|
@@ -23,11 +155,14 @@ export function EmptyState({ title, message, hint, actions, status = "empty" }:
|
|
|
23
155
|
{message && <Box><Text fg={colors.textMuted} wrapText>{t(message)}</Text></Box>}
|
|
24
156
|
{hint && <Box><Text fg={colors.textMuted} wrapText>{t(hint)}</Text></Box>}
|
|
25
157
|
{/* Wraps in a narrow pane rather than clipping the second action; a terminal wraps without a blank row. */}
|
|
26
|
-
{actions && (
|
|
158
|
+
{(retryButton || actions) && (
|
|
27
159
|
<Box flexDirection="row" flexWrap="wrap" gap={1} rowGap={nativePaneChrome ? undefined : 0} marginTop={1}>
|
|
28
|
-
<ButtonActionScope>{actions}</ButtonActionScope>
|
|
160
|
+
<ButtonActionScope>{retryButton}{actions}</ButtonActionScope>
|
|
29
161
|
</Box>
|
|
30
162
|
)}
|
|
163
|
+
{retry && feedback.checkedAt !== null && (
|
|
164
|
+
<Box><Text fg={colors.textMuted} wrapText>{retryNote(retry, status, feedback.checkedAt)}</Text></Box>
|
|
165
|
+
)}
|
|
31
166
|
</Box>
|
|
32
167
|
);
|
|
33
168
|
}
|
|
@@ -68,6 +203,8 @@ export interface PaneStatusBodyProps {
|
|
|
68
203
|
emptyTitle?: string;
|
|
69
204
|
emptyMessage?: string;
|
|
70
205
|
actions?: ReactNode;
|
|
206
|
+
/** Asking again from the error or empty state; see StatusRetry. */
|
|
207
|
+
retry?: StatusRetry;
|
|
71
208
|
align?: "start" | "center";
|
|
72
209
|
width?: number;
|
|
73
210
|
height?: number;
|
|
@@ -85,12 +222,16 @@ export function PaneStatusBody({
|
|
|
85
222
|
emptyTitle,
|
|
86
223
|
emptyMessage,
|
|
87
224
|
actions,
|
|
225
|
+
retry,
|
|
88
226
|
align = "start",
|
|
89
227
|
width,
|
|
90
228
|
height,
|
|
91
229
|
children,
|
|
92
230
|
}: PaneStatusBodyProps) {
|
|
93
231
|
const status = error ? "error" : loading ? "loading" : empty ? "empty" : null;
|
|
232
|
+
// Here rather than in the state, so a retry that shows the spinner while it
|
|
233
|
+
// runs still says it tried once the same failure is back.
|
|
234
|
+
const feedback = useRetryFeedback(retry, status);
|
|
94
235
|
if (!status) return <>{children}</>;
|
|
95
236
|
// Keyed so content that starts with a bare Box never reuses this padded node.
|
|
96
237
|
return (
|
|
@@ -107,11 +248,13 @@ export function PaneStatusBody({
|
|
|
107
248
|
data-gloom-ui="pane-status"
|
|
108
249
|
>
|
|
109
250
|
{status === "loading" ? <Spinner label={loadingLabel ?? loadingText(subject)} /> : (
|
|
110
|
-
<
|
|
251
|
+
<EmptyStateBody
|
|
111
252
|
status={status}
|
|
112
253
|
title={status === "error" ? errorTitle ?? (subject ? unavailableText(subject) : error!) : emptyTitle ?? t("Nothing to show yet.")}
|
|
113
254
|
message={status === "error" ? (errorTitle || subject ? error! : undefined) : emptyMessage}
|
|
114
255
|
actions={actions}
|
|
256
|
+
retry={retry}
|
|
257
|
+
feedback={feedback}
|
|
115
258
|
/>
|
|
116
259
|
)}
|
|
117
260
|
</Box>
|
|
@@ -27,6 +27,7 @@ import { isRecord } from "../../../utils/guards";
|
|
|
27
27
|
import { debugLog } from "../../../utils/debug-log";
|
|
28
28
|
import { DEFAULT_THEME, themes } from "../../../theme/themes";
|
|
29
29
|
import { sanitizeStarPrompt } from "../../../app/star-prompt/model";
|
|
30
|
+
import { sanitizeUpdatesConfig } from "../../../updater/channel";
|
|
30
31
|
|
|
31
32
|
const configLog = debugLog.createLogger("config");
|
|
32
33
|
const reportedUnknownThemes = new Set<string>();
|
|
@@ -106,6 +107,7 @@ export function normalizeLoadedConfig(
|
|
|
106
107
|
...withKeybindings(sanitizeKeybindings(candidate.keybindings)),
|
|
107
108
|
...withTelemetry(sanitizeTelemetry(candidate.telemetry)),
|
|
108
109
|
...withStarPrompt(sanitizeStarPrompt(candidate.starPrompt)),
|
|
110
|
+
...withUpdates(sanitizeUpdatesConfig(candidate.updates)),
|
|
109
111
|
};
|
|
110
112
|
|
|
111
113
|
const needsSave =
|
|
@@ -169,6 +171,8 @@ export function normalizeConfigForSave(config: AppConfig): AppConfig {
|
|
|
169
171
|
Object.assign(persisted, withTelemetry(sanitizeTelemetry(config.telemetry)));
|
|
170
172
|
delete persisted.starPrompt;
|
|
171
173
|
Object.assign(persisted, withStarPrompt(sanitizeStarPrompt(config.starPrompt)));
|
|
174
|
+
delete persisted.updates;
|
|
175
|
+
Object.assign(persisted, withUpdates(sanitizeUpdatesConfig(config.updates)));
|
|
172
176
|
|
|
173
177
|
return persisted;
|
|
174
178
|
}
|
|
@@ -185,6 +189,10 @@ function withStarPrompt(starPrompt: AppConfig["starPrompt"]): Pick<AppConfig, "s
|
|
|
185
189
|
return starPrompt ? { starPrompt } : {};
|
|
186
190
|
}
|
|
187
191
|
|
|
192
|
+
function withUpdates(updates: AppConfig["updates"]): Pick<AppConfig, "updates"> {
|
|
193
|
+
return updates ? { updates } : {};
|
|
194
|
+
}
|
|
195
|
+
|
|
188
196
|
/** Only the switches that are set survive; an empty object is the same as none. */
|
|
189
197
|
function sanitizeTelemetry(value: unknown): TelemetryConfig | undefined {
|
|
190
198
|
if (!isRecord(value)) return undefined;
|
package/src/i18n/es.ts
CHANGED
|
@@ -70,6 +70,7 @@ export const es: Record<string, string> = {
|
|
|
70
70
|
"Could not save this value.": "No se pudo guardar este valor.",
|
|
71
71
|
"Could not apply that setting.": "No se pudo aplicar ese ajuste.",
|
|
72
72
|
"Enter {action} · Esc cancel": "Enter {action} · Esc cancelar",
|
|
73
|
+
"y {action} · Enter or Esc {cancel}": "y {action} · Enter o Esc {cancel}",
|
|
73
74
|
"Published {date}": "Publicado {date}",
|
|
74
75
|
"Language": "Idioma",
|
|
75
76
|
"Unsupported language": "Idioma no compatible",
|
|
@@ -151,6 +152,7 @@ export const es: Record<string, string> = {
|
|
|
151
152
|
"Toggle Status Bar": "Alternar barra de estado",
|
|
152
153
|
"Toggle Value Flashing": "Alternar parpadeo de valores",
|
|
153
154
|
"Check for Updates": "Buscar actualizaciones",
|
|
155
|
+
"Release Channel": "Canal de versiones",
|
|
154
156
|
"Crash Reports": "Informes de fallos",
|
|
155
157
|
"Usage Counts": "Recuento de uso",
|
|
156
158
|
"Change Theme": "Cambiar tema",
|