gloomberb 0.17.0 → 0.17.2
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/{plugins/builtin/cloud → account}/auth-dialog.tsx +7 -7
- package/src/api-client/data.ts +5 -1
- package/src/api-client/doe.ts +19 -0
- package/src/api-client/earnings.ts +4 -2
- package/src/api-client/index.ts +1 -0
- package/src/app/global-shortcuts.ts +10 -0
- package/src/app/keybindings/actions.ts +16 -0
- package/src/app/runtime/desktop-deeplink.ts +1 -1
- package/src/app/runtime/update.ts +16 -0
- package/src/app.tsx +1 -1
- package/src/architecture/core-builtin-allowlist.json +4 -9
- package/src/architecture/dom-cell-metrics-allowlist.json +100 -26
- package/src/cli/commands/market.ts +87 -17
- package/src/cli/commands/options-chain.ts +93 -23
- package/src/cli/commands/options-leaps.ts +462 -63
- package/src/cli/commands/search.ts +10 -7
- package/src/cli/commands/ticker.ts +19 -10
- package/src/cli/glossary.ts +1 -1
- package/src/cli/index.ts +14 -6
- package/src/cli/known-gaps.ts +22 -0
- package/src/cli/pane-functions/catalog.ts +45 -10
- package/src/cli/pane-functions/dom.ts +36 -10
- package/src/cli/pane-functions/function-help.ts +4 -2
- package/src/cli/pane-functions/headless.ts +12 -1
- package/src/cli/pane-functions/index.ts +44 -3
- package/src/cli/pane-functions/lock-note.ts +56 -0
- package/src/cli/pane-functions/report-notices.ts +15 -0
- package/src/cli/symbol-args.ts +19 -0
- package/src/components/command-bar/list/view.tsx +2 -1
- package/src/components/command-bar/workflow/builtin.ts +2 -2
- package/src/components/command-bar/workflow/collection-actions.ts +1 -1
- package/src/components/feedback-dialog.tsx +1 -1
- package/src/components/index.ts +3 -2
- package/src/components/layout/header.tsx +58 -14
- package/src/components/layout/shell/chrome.ts +10 -4
- package/src/components/layout/shell/index.tsx +1 -1
- package/src/components/layout/status-bar-groups.ts +1 -1
- package/src/components/layout/status-bar.tsx +2 -2
- package/src/components/markdown-text.tsx +14 -2
- package/src/components/reader/article-reader.tsx +1 -1
- package/src/components/reader/detail-reader.tsx +10 -7
- package/src/components/reader/reader-text.tsx +32 -17
- package/src/components/ui/column-grid.tsx +206 -0
- package/src/components/ui/display.tsx +40 -3
- package/src/components/ui/fill.ts +8 -0
- package/src/components/ui/index.ts +2 -0
- package/src/layout-marketplace/gallery.tsx +2 -2
- package/src/layout-marketplace/linked-sync.ts +1 -1
- package/src/market-data/market/freshness.ts +3 -0
- package/src/market-data/published-apac-sessions.ts +12 -2
- package/src/market-data/quotes/screener-live-quotes.ts +31 -7
- package/src/plugins/builtin/account-management/form-components.tsx +4 -7
- package/src/plugins/builtin/account-management/password-dialog.tsx +3 -2
- package/src/plugins/builtin/backtest/pane.tsx +9 -3
- package/src/plugins/builtin/buildout/detail/company.tsx +4 -3
- package/src/plugins/builtin/buildout/detail/ui.tsx +9 -12
- package/src/plugins/builtin/chart-composer/index.tsx +1 -1
- package/src/plugins/builtin/chart-composer/presets.ts +33 -1
- package/src/plugins/builtin/chat/channel-labels.ts +2 -2
- package/src/plugins/builtin/chat/content/channel-navigation.ts +1 -1
- package/src/plugins/builtin/chat/content/index.tsx +2 -2
- package/src/plugins/builtin/chat/conversations.ts +1 -1
- package/src/plugins/builtin/chat/index.tsx +2 -2
- package/src/plugins/builtin/chat/share/flow.tsx +1 -1
- package/src/plugins/builtin/chat/sidebar-rows.ts +1 -1
- package/src/plugins/builtin/chat/sidebar.tsx +2 -2
- package/src/plugins/builtin/chat/unread-inbox-pane.tsx +2 -2
- package/src/plugins/builtin/cloud/askg/pane.tsx +2 -1
- package/src/plugins/builtin/cloud/askg/tool-timeline.tsx +3 -2
- package/src/plugins/builtin/cloud/auth-actions.tsx +1 -1
- package/src/plugins/builtin/cloud/auth-commands.ts +1 -1
- package/src/plugins/builtin/cloud/mcp-connect/dialog.tsx +1 -1
- package/src/plugins/builtin/cloud/team/acm-tab.tsx +3 -3
- package/src/plugins/builtin/cloud/team/capability.ts +2 -2
- package/src/plugins/builtin/cloud/team/command.ts +4 -4
- package/src/plugins/builtin/cloud/team/module.tsx +3 -3
- package/src/plugins/builtin/cloud/team/pane-model.ts +2 -2
- package/src/plugins/builtin/cloud/team/pane-sections.tsx +1 -1
- package/src/plugins/builtin/cloud/team/pane-ui.tsx +1 -1
- package/src/plugins/builtin/cloud/team/pane.tsx +3 -3
- package/src/plugins/builtin/cloud/team/status-widget.tsx +3 -3
- package/src/plugins/builtin/cloud/team/team-state-host.ts +1 -1
- package/src/plugins/builtin/cloud/team/views.ts +2 -2
- package/src/plugins/builtin/cloud/thesis/board-pane.tsx +1 -1
- package/src/plugins/builtin/cloud/thesis/detail.tsx +12 -10
- package/src/plugins/builtin/cloud/thesis/ticker-tab.tsx +1 -1
- package/src/plugins/builtin/cloud/upgrade-command.ts +1 -1
- package/src/plugins/builtin/congress-trades/detail.tsx +8 -4
- package/src/plugins/builtin/connections/pane.tsx +16 -5
- package/src/plugins/builtin/cpi/pane.tsx +5 -4
- package/src/plugins/builtin/custom-view/pane.tsx +2 -2
- package/src/plugins/builtin/dividend-yield/headless.ts +8 -2
- package/src/plugins/builtin/dividend-yield/pane.tsx +4 -2
- package/src/plugins/builtin/docs/blocks.tsx +13 -2
- package/src/plugins/builtin/docs/pane.tsx +6 -5
- package/src/plugins/builtin/doe/index.tsx +11 -4
- package/src/plugins/builtin/doe/pane.tsx +5 -4
- package/src/plugins/builtin/doe/prices.ts +84 -0
- package/src/plugins/builtin/doe/series.ts +4 -1
- package/src/plugins/builtin/earnings/board-model.ts +18 -1
- package/src/plugins/builtin/earnings/board-table.ts +18 -3
- package/src/plugins/builtin/earnings/headless.ts +13 -2
- package/src/plugins/builtin/earnings/history-model.ts +1 -1
- package/src/plugins/builtin/earnings/history-table.ts +2 -2
- package/src/plugins/builtin/earnings-calls/headless.ts +7 -21
- package/src/plugins/builtin/earnings-calls/index.tsx +2 -0
- package/src/plugins/builtin/earnings-calls/lock-note.ts +17 -0
- package/src/plugins/builtin/equity-screener/headless.ts +59 -1
- package/src/plugins/builtin/equity-screener/live.ts +82 -31
- package/src/plugins/builtin/equity-screener/pane.tsx +60 -23
- package/src/plugins/builtin/estimate-revisions/client.ts +1 -1
- package/src/plugins/builtin/estimate-revisions/headless.ts +9 -2
- package/src/plugins/builtin/estimate-revisions/index.tsx +3 -0
- package/src/plugins/builtin/estimate-revisions/lock-note.ts +23 -0
- package/src/plugins/builtin/executives/index.tsx +18 -0
- package/src/plugins/builtin/exposure/charts.tsx +4 -3
- package/src/plugins/builtin/filing-events/index.tsx +8 -10
- package/src/plugins/builtin/fixed-income/pane.tsx +2 -1
- package/src/plugins/builtin/gpu/equities.tsx +9 -6
- package/src/plugins/builtin/help/feedback-tab.tsx +1 -1
- package/src/plugins/builtin/iv-history/format.ts +3 -0
- package/src/plugins/builtin/iv-history/headless.ts +3 -1
- package/src/plugins/builtin/iv-history/index.tsx +3 -1
- package/src/plugins/builtin/iv-history/model.ts +3 -1
- package/src/plugins/builtin/iv-history/screen-pane.tsx +3 -1
- package/src/plugins/builtin/iv-history/universe.ts +1 -1
- package/src/plugins/builtin/jobs/headless.ts +6 -20
- package/src/plugins/builtin/jobs/index.tsx +2 -0
- package/src/plugins/builtin/jobs/lock-note.ts +16 -0
- package/src/plugins/builtin/notes/index.tsx +1 -1
- package/src/plugins/builtin/notes/owner.tsx +2 -2
- package/src/plugins/builtin/options/carry.ts +36 -0
- package/src/plugins/builtin/options/index.tsx +2 -1
- package/src/plugins/builtin/options/model-spot.ts +94 -0
- package/src/plugins/builtin/options/open-interest-as-of.ts +74 -0
- package/src/plugins/builtin/options/view.tsx +69 -11
- package/src/plugins/builtin/options-calculator/pane.tsx +56 -1
- package/src/plugins/builtin/options-calculator/premium-history.ts +34 -0
- package/src/plugins/builtin/options-scenario/client.ts +33 -9
- package/src/plugins/builtin/options-scenario/csp.ts +168 -0
- package/src/plugins/builtin/options-scenario/evidence.ts +3 -0
- package/src/plugins/builtin/options-scenario/headless.ts +24 -8
- package/src/plugins/builtin/options-scenario/index.tsx +3 -1
- package/src/plugins/builtin/options-scenario/pane.tsx +32 -6
- package/src/plugins/builtin/portfolio-list/mutations.ts +1 -1
- package/src/plugins/builtin/portfolio-list/pane/remove-ticker.ts +1 -1
- package/src/plugins/builtin/research/corporate-actions-pane.tsx +4 -2
- package/src/plugins/builtin/research/event-model.ts +7 -4
- package/src/plugins/builtin/risk-factors/index.tsx +18 -0
- package/src/plugins/builtin/risk-factors/pane.tsx +2 -1
- package/src/plugins/builtin/scanner/headless.ts +9 -2
- package/src/plugins/builtin/scanner/index.ts +3 -0
- package/src/plugins/builtin/scanner/lock-note.ts +17 -0
- package/src/plugins/builtin/series-inventory.ts +2 -1
- package/src/plugins/builtin/shared/cloud-denied.ts +39 -8
- package/src/plugins/builtin/shared/earnings-report.ts +10 -4
- package/src/plugins/builtin/shared/pro-lock-note.ts +58 -0
- package/src/plugins/builtin/shared/series-list-detail.tsx +10 -5
- package/src/plugins/builtin/social-mentions/client.ts +3 -3
- package/src/plugins/builtin/social-mentions/headless.ts +6 -2
- package/src/plugins/builtin/social-mentions/index.tsx +2 -1
- package/src/plugins/builtin/social-mentions/lock-note.ts +15 -0
- package/src/plugins/builtin/supply-chain/flow.tsx +15 -13
- package/src/plugins/builtin/supply-chain/graph.tsx +6 -4
- package/src/plugins/builtin/supply-chain/pane.tsx +8 -5
- package/src/plugins/builtin/ticker-detail/financials/aggregation.ts +14 -1
- package/src/plugins/builtin/ticker-detail/financials/ratios.ts +9 -7
- package/src/plugins/builtin/ticker-detail/headless.ts +4 -1
- package/src/plugins/builtin/ticker-detail/overview/components.tsx +105 -104
- package/src/plugins/builtin/ticker-detail/overview/model.ts +9 -2
- package/src/plugins/builtin/ticker-detail/overview-tab.tsx +34 -8
- package/src/plugins/builtin/vol-surface/pane.tsx +6 -3
- package/src/public/__snapshots__/public-api.test.ts.snap +2 -0
- package/src/renderers/dom/data-table/index.tsx +48 -15
- package/src/renderers/dom/data-table/row.tsx +1 -1
- package/src/renderers/dom/desktop/popover.tsx +6 -15
- package/src/renderers/dom/host/input.tsx +27 -3
- package/src/renderers/dom/host/surface-3d.tsx +6 -4
- package/src/renderers/dom/styles.css +26 -6
- package/src/renderers/electrobun/view/app-services.ts +2 -22
- package/src/renderers/electrobun/view/news-source.ts +42 -0
- package/src/series/validate.ts +5 -0
- package/src/shares/dialog.tsx +2 -1
- package/src/sources/gloomberb-cloud/normalizers.ts +2 -0
- package/src/sources/provider-router/financials.ts +5 -0
- package/src/{plugins/builtin/cloud/team → team}/collections-sync.ts +6 -6
- package/src/{plugins/builtin/cloud/team → team}/collections.ts +3 -3
- package/src/{plugins/builtin/cloud/team → team}/model.ts +2 -2
- package/src/{plugins/builtin/cloud/team → team}/pane-request.ts +1 -1
- package/src/{plugins/builtin/cloud/team → team}/store.ts +3 -3
- package/src/types/data-provider.ts +3 -0
- package/src/types/financials.ts +28 -0
- package/src/types/plugin.ts +17 -4
- package/src/utils/decimal-comma.ts +18 -0
- package/src/utils/exchanges.ts +7 -0
- package/src/utils/financial-statements.ts +25 -0
- package/src/utils/forward-dividend.ts +39 -9
- package/src/utils/fundamentals.ts +41 -1
- package/src/utils/option-expiry.ts +16 -0
- package/src/utils/sec.ts +2 -1
|
@@ -27,6 +27,7 @@ import type { TickerRecord } from "../../../types/ticker";
|
|
|
27
27
|
import { Box, ScrollBox, Text, useUiCapabilities } from "../../../ui";
|
|
28
28
|
import { publicTickerKey, resolveExchangeTimeZone } from "../../../utils/exchanges";
|
|
29
29
|
import { convertCurrency, displayWidth, formatPercentRaw, truncateToDisplayWidth } from "../../../utils/format";
|
|
30
|
+
import { enterpriseValueReconciliationNote } from "../../../utils/fundamentals";
|
|
30
31
|
import { zonedDateKey } from "../../../utils/zoned-date-time";
|
|
31
32
|
import { sharesOutstandingInReceipts } from "../../../utils/depositary-receipt";
|
|
32
33
|
import {
|
|
@@ -44,6 +45,7 @@ import type { OverviewFunctionLink, StatField } from "./overview/types";
|
|
|
44
45
|
import { describeFundamentalMarketCap } from "../../../utils/market-capitalization";
|
|
45
46
|
import { liveFiftyTwoWeekRange, liveMarketCapitalization } from "../portfolio-list/live-valuation";
|
|
46
47
|
import { instrumentTypeLabel } from "../../../tickers/instrument-kind";
|
|
48
|
+
import { ClippedText } from "../../../components/ui/display";
|
|
47
49
|
|
|
48
50
|
/** Cells between the Day and 52W ranges when they share a row. */
|
|
49
51
|
const RANGE_PAIR_GAP = 4;
|
|
@@ -142,13 +144,16 @@ function ResolvedOverviewTab({ width, height, focused = false, ticker, financial
|
|
|
142
144
|
panelId: "price",
|
|
143
145
|
timeBasis: chartTimeZone ? { kind: "market", timeZone: chartTimeZone } : undefined,
|
|
144
146
|
});
|
|
147
|
+
// A provider EV off market cap + debt - cash, or why a withdrawn one is unavailable.
|
|
148
|
+
const enterpriseValueNote = enterpriseValueReconciliationNote(fundamentals)
|
|
149
|
+
?? (fundamentals?.unavailableFields?.includes("enterpriseValue")
|
|
150
|
+
? t("Enterprise value unavailable: the source observation failed validation.") : undefined);
|
|
145
151
|
usePaneNoticeFooter({
|
|
146
152
|
registrationId: "overview-notices",
|
|
147
153
|
notices: [
|
|
148
154
|
...(!quote && financials ? [t("Current quote unavailable. Other research data is still available.")] : []),
|
|
149
155
|
...(priceSeries.warning ? [priceSeries.warning] : []),
|
|
150
|
-
...(
|
|
151
|
-
? [t("Enterprise value unavailable: the source observation failed validation.")] : []),
|
|
156
|
+
...(enterpriseValueNote ? [enterpriseValueNote] : []),
|
|
152
157
|
...(capitalization?.provenance.kind === "fundamentals" && !capitalization.live
|
|
153
158
|
? [`Market cap: ${describeFundamentalMarketCap(capitalization.provenance)}.`] : []),
|
|
154
159
|
],
|
|
@@ -261,8 +266,22 @@ function ResolvedOverviewTab({ width, height, focused = false, ticker, financial
|
|
|
261
266
|
// On the desktop the header keeps half a row clear of the tab bar's edge.
|
|
262
267
|
style={nativePaneChrome ? { paddingTop: Math.round(cellHeightPx / 2) } : undefined}
|
|
263
268
|
>
|
|
264
|
-
<Box
|
|
265
|
-
|
|
269
|
+
<Box
|
|
270
|
+
flexDirection={quoteBookInline ? "row" : "column"}
|
|
271
|
+
gap={quoteBookInline && !nativePaneChrome ? 2 : 0}
|
|
272
|
+
width={nativePaneChrome ? undefined : contentWidth}
|
|
273
|
+
style={nativePaneChrome && quoteBookInline ? { columnGap: "2ch" } : undefined}
|
|
274
|
+
>
|
|
275
|
+
<Box
|
|
276
|
+
flexDirection="row"
|
|
277
|
+
width={nativePaneChrome ? undefined : quoteSummaryWidth}
|
|
278
|
+
// On the desktop the summary takes what the book beside it leaves, so the book keeps to the pane's right edge.
|
|
279
|
+
flexGrow={nativePaneChrome && quoteBookInline ? 1 : undefined}
|
|
280
|
+
flexShrink={nativePaneChrome && quoteBookInline ? 1 : 0}
|
|
281
|
+
flexBasis={nativePaneChrome && quoteBookInline ? 0 : undefined}
|
|
282
|
+
minWidth={0}
|
|
283
|
+
overflow="hidden"
|
|
284
|
+
>
|
|
266
285
|
<CompanyLogo
|
|
267
286
|
symbol={ticker.metadata.ticker}
|
|
268
287
|
assetCategory={ticker.metadata.assetCategory}
|
|
@@ -275,7 +294,7 @@ function ResolvedOverviewTab({ width, height, focused = false, ticker, financial
|
|
|
275
294
|
{hasIdentityLine && (
|
|
276
295
|
<Box flexDirection="row" minWidth={0} overflow="hidden">
|
|
277
296
|
{companyName && (
|
|
278
|
-
<
|
|
297
|
+
<ClippedText fg={colors.textDim} text={companyName} width={companyNameWidth} />
|
|
279
298
|
)}
|
|
280
299
|
{venueText && <Text flexShrink={0} fg={colors.textDim}>{venueText}</Text>}
|
|
281
300
|
{quote?.marketState && (
|
|
@@ -323,7 +342,13 @@ function ResolvedOverviewTab({ width, height, focused = false, ticker, financial
|
|
|
323
342
|
</Box>
|
|
324
343
|
|
|
325
344
|
{quote && hasBidAsk && (
|
|
326
|
-
<QuoteBook
|
|
345
|
+
<QuoteBook
|
|
346
|
+
quote={quote}
|
|
347
|
+
assetCategory={moneyOptions.assetCategory}
|
|
348
|
+
width={quoteBookWidth}
|
|
349
|
+
// The terminal's 30% of the width between 24 and 32 cells, in characters.
|
|
350
|
+
desktopWidth={quoteBookInline ? "clamp(24ch, 30%, 32ch)" : "min(100%, 32ch)"}
|
|
351
|
+
/>
|
|
327
352
|
)}
|
|
328
353
|
</Box>
|
|
329
354
|
|
|
@@ -430,9 +455,10 @@ function ResolvedOverviewTab({ width, height, focused = false, ticker, financial
|
|
|
430
455
|
|
|
431
456
|
{/* Description — last, collapsed */}
|
|
432
457
|
{description && (
|
|
433
|
-
<Box flexDirection="column" width={contentWidth}>
|
|
458
|
+
<Box flexDirection="column" width={nativePaneChrome ? undefined : contentWidth}>
|
|
434
459
|
<SectionHeading title={t("Description")} />
|
|
435
|
-
|
|
460
|
+
{/* The desktop wraps it at the pane's own width, so it follows a resize to the pixel. */}
|
|
461
|
+
<Text fg={colors.text} width={nativePaneChrome ? undefined : contentWidth} wrapMode="word" wrapText>{description}</Text>
|
|
436
462
|
</Box>
|
|
437
463
|
)}
|
|
438
464
|
</Box>
|
|
@@ -12,7 +12,8 @@ import { usePaneSettingValue, usePluginAppActions, usePluginPaneState } from "..
|
|
|
12
12
|
import { blendHex } from "../../../theme/color-utils";
|
|
13
13
|
import { useThemeColors } from "../../../theme/theme-context";
|
|
14
14
|
import type { PaneProps } from "../../../types/plugin";
|
|
15
|
-
import { Box, type ScrollBoxRenderable } from "../../../ui";
|
|
15
|
+
import { Box, useUiCapabilities, type ScrollBoxRenderable } from "../../../ui";
|
|
16
|
+
import { FILL_REST } from "../../../components/ui/fill";
|
|
16
17
|
import { resolveOptionsTarget } from "../../../utils/options";
|
|
17
18
|
import { nextHeaderSort, type SortDirection } from "../../../utils/sort-values";
|
|
18
19
|
import { buildOptionCalcParams, OPTIONS_CALCULATOR_TEMPLATE_ID } from "../options-calculator/model";
|
|
@@ -58,6 +59,7 @@ export function VolSurfacePane({ focused, width, height }: PaneProps) {
|
|
|
58
59
|
// Only while the table is on screen: loading, a failure or an empty chain
|
|
59
60
|
// leave the strip its keys.
|
|
60
61
|
const [tableOnScreen, setTableOnScreen] = useState(false);
|
|
62
|
+
const { nativePaneChrome } = useUiCapabilities();
|
|
61
63
|
const tabKeys = activeTab !== "table" || !tableOnScreen;
|
|
62
64
|
const { strip: tabStrip, rows: tabRows } = usePaneTabs({ tabs: TABS, activeValue: activeTab, onSelect: setActiveTab,
|
|
63
65
|
focused: focused && activeTab !== "surface", keyboardNavigation: tabKeys, dense: true });
|
|
@@ -427,7 +429,7 @@ export function VolSurfacePane({ focused, width, height }: PaneProps) {
|
|
|
427
429
|
focused={focused} onKey={handleKey} metadata={exportMetadata} /> : table;
|
|
428
430
|
const tableShown = activeTab === "table" && !!symbol && !!snapshot && !(!shownDate && liveEmpty);
|
|
429
431
|
useEffect(() => setTableOnScreen(tableShown), [tableShown]);
|
|
430
|
-
return <Box flexDirection="column" width={width}
|
|
432
|
+
return <Box flexDirection="column" width={width} overflow="hidden" {...(nativePaneChrome ? FILL_REST : { height })}>
|
|
431
433
|
{tabStrip}
|
|
432
434
|
{!symbol ? <EmptyState title="Choose an underlying ticker." /> : <PaneStatusBody loading={active.loading && !snapshot}
|
|
433
435
|
error={!snapshot ? active.error : null} empty={!snapshot && !active.loading} subject="volatility surface">
|
|
@@ -444,7 +446,8 @@ export function VolSurfacePane({ focused, width, height }: PaneProps) {
|
|
|
444
446
|
? `Stored close · spot ${formatPrice(snapshot.spot)} · captured ${formatCaptureTime(snapshot.stored.capturedAt)} New York`
|
|
445
447
|
: [`Spot ${formatPrice(snapshot?.spot)}${quote?.currency ? ` ${quote.currency}` : ""}`, selectedExpiry?.asOf?.slice(0, 10)].filter(Boolean).join(" · ")} />
|
|
446
448
|
<StatGrid items={ivStats} width={width} />
|
|
447
|
-
{
|
|
449
|
+
{/* The terminal sizes the view in the rows under the bar and figures; the desktop fills what is left. */}
|
|
450
|
+
{nativePaneChrome ? <Box flexDirection="column" overflow="hidden" {...FILL_REST}>{content}</Box> : content}
|
|
448
451
|
</PaneStatusBody>}
|
|
449
452
|
</Box>;
|
|
450
453
|
}
|
|
@@ -8,6 +8,8 @@ import {
|
|
|
8
8
|
useRef,
|
|
9
9
|
useState,
|
|
10
10
|
type CSSProperties,
|
|
11
|
+
type ReactNode,
|
|
12
|
+
type RefObject,
|
|
11
13
|
} from "react";
|
|
12
14
|
import { EmptyState } from "../../../components/ui/status";
|
|
13
15
|
import { usePaneInstance } from "../../../state/app/context";
|
|
@@ -326,8 +328,11 @@ export function WebDataTable<T, C extends DataTableColumn = DataTableColumn>({
|
|
|
326
328
|
const frozenCell = freezeFirstColumn && overflowEdges.start
|
|
327
329
|
? body?.querySelector<HTMLElement>('[data-gloom-role="data-table-header-cell"]')
|
|
328
330
|
: null;
|
|
331
|
+
// The gap after it is the next column's margin, as laid out.
|
|
332
|
+
const following = frozenCell?.nextElementSibling;
|
|
333
|
+
const gap = following ? parseFloat(following.ownerDocument.defaultView?.getComputedStyle(following).marginLeft ?? "") || 0 : 0;
|
|
329
334
|
const next = frozenCell && body
|
|
330
|
-
? Math.max(0, frozenCell.getBoundingClientRect().right - body.getBoundingClientRect().left +
|
|
335
|
+
? Math.max(0, frozenCell.getBoundingClientRect().right - body.getBoundingClientRect().left + gap)
|
|
331
336
|
: 0;
|
|
332
337
|
// Set only on a change: a state update from a layout effect costs the render a second commit.
|
|
333
338
|
if (next !== frozenEdgePx) setFrozenEdgePx(next);
|
|
@@ -353,6 +358,8 @@ export function WebDataTable<T, C extends DataTableColumn = DataTableColumn>({
|
|
|
353
358
|
};
|
|
354
359
|
const bodyScrollerStyle: CSSProperties = {
|
|
355
360
|
width: "100%",
|
|
361
|
+
// What is kept to the columns in view (a section header's action) measures the body in `cqw`.
|
|
362
|
+
containerType: "inline-size",
|
|
356
363
|
flex: fitRows ? "0 0 auto" : "1 1 0px",
|
|
357
364
|
minWidth: 0,
|
|
358
365
|
minHeight: 0,
|
|
@@ -425,21 +432,9 @@ export function WebDataTable<T, C extends DataTableColumn = DataTableColumn>({
|
|
|
425
432
|
/>
|
|
426
433
|
{items.length === 0 ? (
|
|
427
434
|
emptyContent ?? (
|
|
428
|
-
<
|
|
429
|
-
data-gloom-role="data-table-empty"
|
|
430
|
-
style={{
|
|
431
|
-
width: viewportWidth > 0 ? viewportWidth * WEB_CELL_WIDTH : "100%",
|
|
432
|
-
maxWidth: "100%",
|
|
433
|
-
boxSizing: "border-box",
|
|
434
|
-
position: "sticky",
|
|
435
|
-
left: 0,
|
|
436
|
-
padding: `${WEB_CELL_HEIGHT}px ${WEB_CELL_WIDTH}px`,
|
|
437
|
-
color: CSS_TEXT_DIM,
|
|
438
|
-
lineHeight: "var(--cell-h)",
|
|
439
|
-
}}
|
|
440
|
-
>
|
|
435
|
+
<EmptyBody body={bodyElementRef}>
|
|
441
436
|
<EmptyState title={emptyStateTitle} hint={emptyStateHint} />
|
|
442
|
-
</
|
|
437
|
+
</EmptyBody>
|
|
443
438
|
)
|
|
444
439
|
) : measurePerf(
|
|
445
440
|
"data-table.desktop.render-virtual-rows",
|
|
@@ -523,3 +518,41 @@ export function WebDataTable<T, C extends DataTableColumn = DataTableColumn>({
|
|
|
523
518
|
</div>
|
|
524
519
|
);
|
|
525
520
|
}
|
|
521
|
+
|
|
522
|
+
/**
|
|
523
|
+
* An empty table's message, as wide as the body shows: it stays in view
|
|
524
|
+
* however wide the columns scroll. Read from the body's laid-out width, and
|
|
525
|
+
* set on the element, so a resize does not render the table.
|
|
526
|
+
*/
|
|
527
|
+
function EmptyBody({ body, children }: { body: RefObject<HTMLDivElement | null>; children: ReactNode }) {
|
|
528
|
+
const ref = useRef<HTMLDivElement | null>(null);
|
|
529
|
+
useLayoutEffect(() => {
|
|
530
|
+
const element = ref.current, scroller = body.current;
|
|
531
|
+
if (!element || !scroller) return;
|
|
532
|
+
const fit = () => {
|
|
533
|
+
element.style.width = scroller.clientWidth > 0 ? `${scroller.clientWidth}px` : "100%";
|
|
534
|
+
};
|
|
535
|
+
fit();
|
|
536
|
+
if (typeof ResizeObserver === "undefined") return;
|
|
537
|
+
const observer = new ResizeObserver(fit);
|
|
538
|
+
observer.observe(scroller);
|
|
539
|
+
return () => observer.disconnect();
|
|
540
|
+
}, [body]);
|
|
541
|
+
return (
|
|
542
|
+
<div
|
|
543
|
+
ref={ref}
|
|
544
|
+
data-gloom-role="data-table-empty"
|
|
545
|
+
style={{
|
|
546
|
+
maxWidth: "100%",
|
|
547
|
+
boxSizing: "border-box",
|
|
548
|
+
position: "sticky",
|
|
549
|
+
left: 0,
|
|
550
|
+
padding: `${WEB_CELL_HEIGHT}px ${WEB_CELL_WIDTH}px`,
|
|
551
|
+
color: CSS_TEXT_DIM,
|
|
552
|
+
lineHeight: "var(--cell-h)",
|
|
553
|
+
}}
|
|
554
|
+
>
|
|
555
|
+
{children}
|
|
556
|
+
</div>
|
|
557
|
+
);
|
|
558
|
+
}
|
|
@@ -300,7 +300,7 @@ function WebDataTableRowInner<
|
|
|
300
300
|
alignItems: "center",
|
|
301
301
|
gap: 4,
|
|
302
302
|
// Kept to the columns in view, so a table wider than its pane does not push the action out of sight.
|
|
303
|
-
...(action
|
|
303
|
+
...(action ? { maxWidth: `calc(100cqw - ${inlinePaddingPx(horizontalPadding) * 2}px)` } : {}),
|
|
304
304
|
}}
|
|
305
305
|
>
|
|
306
306
|
{sectionHeader.expanded !== undefined && (
|
|
@@ -4,7 +4,6 @@ import { useCallback, useEffect, useLayoutEffect, useRef, useState } from "react
|
|
|
4
4
|
import { createPortal } from "react-dom";
|
|
5
5
|
import type { BoxRenderable, HostPopoverProps } from "../../../ui";
|
|
6
6
|
import { blendHex } from "../../../theme/colors";
|
|
7
|
-
import { WEB_CELL_HEIGHT, WEB_CELL_WIDTH } from "../../../theme/font-scale";
|
|
8
7
|
import { useThemeColors } from "../../../theme/theme-context";
|
|
9
8
|
|
|
10
9
|
const VIEWPORT_MARGIN = 10;
|
|
@@ -33,23 +32,15 @@ interface Edges {
|
|
|
33
32
|
}
|
|
34
33
|
|
|
35
34
|
/**
|
|
36
|
-
* Where an anchor element is now, in viewport pixels
|
|
37
|
-
*
|
|
35
|
+
* Where an anchor element is now, in viewport pixels, as the page lays it out
|
|
36
|
+
* (every box and scroller on the desktop reports its rect). Null once it is
|
|
37
|
+
* gone from the page (a removed message), so the popover falls back to its
|
|
38
|
+
* trigger.
|
|
38
39
|
*/
|
|
39
40
|
function anchorEdges(anchor: BoxRenderable): Edges | null {
|
|
40
41
|
const rect = anchor.getBoundingClientRect?.();
|
|
41
|
-
if (rect)
|
|
42
|
-
|
|
43
|
-
return { left: rect.x, top: rect.y, right: rect.x + rect.width, bottom: rect.y + rect.height };
|
|
44
|
-
}
|
|
45
|
-
const bounds = anchor.absoluteBounds;
|
|
46
|
-
if (!bounds) return null;
|
|
47
|
-
return {
|
|
48
|
-
left: bounds.x * WEB_CELL_WIDTH,
|
|
49
|
-
top: bounds.y * WEB_CELL_HEIGHT,
|
|
50
|
-
right: (bounds.x + bounds.width) * WEB_CELL_WIDTH,
|
|
51
|
-
bottom: (bounds.y + bounds.height) * WEB_CELL_HEIGHT,
|
|
52
|
-
};
|
|
42
|
+
if (!rect || (rect.width === 0 && rect.height === 0)) return null;
|
|
43
|
+
return { left: rect.x, top: rect.y, right: rect.x + rect.width, bottom: rect.y + rect.height };
|
|
53
44
|
}
|
|
54
45
|
|
|
55
46
|
/** The area a popover must stay in: the window, and for `pane` the pane that holds its trigger too. */
|
|
@@ -16,7 +16,6 @@ import {
|
|
|
16
16
|
import { editableTextContextMenuItems } from "../../../ui/context-menu";
|
|
17
17
|
import { useRendererHost, useUiCapabilities, type InputRenderable, type TextareaRenderable } from "../../../ui/host";
|
|
18
18
|
import { toKeyEventLike } from "../input-host";
|
|
19
|
-
import { WEB_CELL_WIDTH } from "../../../theme/font-scale";
|
|
20
19
|
import { cellHeight, cellWidth, cleanDomProps, commonStyle } from "./style";
|
|
21
20
|
|
|
22
21
|
function textInputStyle(props: Record<string, unknown>, multiline: boolean): CSSProperties {
|
|
@@ -161,10 +160,35 @@ function useSyncedEditableElement<T extends HTMLInputElement | HTMLTextAreaEleme
|
|
|
161
160
|
|
|
162
161
|
type WebVisualCursor = TextareaRenderable["visualCursor"];
|
|
163
162
|
|
|
163
|
+
/**
|
|
164
|
+
* How many characters a line of the field holds before it wraps: its text
|
|
165
|
+
* box over the advance of one character of its own font, as the browser lays
|
|
166
|
+
* them out. A character is narrower than a terminal cell.
|
|
167
|
+
*/
|
|
164
168
|
function textareaColumnCount(props: Record<string, unknown>, element: HTMLTextAreaElement | null): number {
|
|
165
169
|
if (typeof props.width === "number") return Math.max(1, Math.floor(props.width));
|
|
166
|
-
|
|
167
|
-
|
|
170
|
+
if (!element) return 1;
|
|
171
|
+
const style = element.ownerDocument.defaultView?.getComputedStyle(element);
|
|
172
|
+
const inset = (parseFloat(style?.paddingLeft ?? "") || 0) + (parseFloat(style?.paddingRight ?? "") || 0);
|
|
173
|
+
return Math.max(1, Math.floor((element.clientWidth - inset) / characterAdvance(element, style)));
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const ADVANCES = new Map<string, number>();
|
|
177
|
+
/** One character's advance in the element's font, measured once per font. */
|
|
178
|
+
function characterAdvance(element: HTMLElement, style: CSSStyleDeclaration | undefined): number {
|
|
179
|
+
const font = style?.font ?? "";
|
|
180
|
+
const known = ADVANCES.get(font);
|
|
181
|
+
if (known) return known;
|
|
182
|
+
const context = element.ownerDocument.createElement("canvas").getContext?.("2d");
|
|
183
|
+
let advance = 0;
|
|
184
|
+
if (context && font) {
|
|
185
|
+
context.font = font;
|
|
186
|
+
advance = context.measureText("0000000000").width / 10;
|
|
187
|
+
}
|
|
188
|
+
// No canvas (a test DOM): a monospace character is about 0.6 of its font size.
|
|
189
|
+
if (!(advance > 0)) advance = (parseFloat(style?.fontSize ?? "") || 12) * 0.6;
|
|
190
|
+
ADVANCES.set(font, advance);
|
|
191
|
+
return advance;
|
|
168
192
|
}
|
|
169
193
|
|
|
170
194
|
function wrappedLineCount(line: string, columns: number, wrap: boolean): number {
|
|
@@ -287,10 +287,12 @@ function prefersReducedMotion(): boolean {
|
|
|
287
287
|
*/
|
|
288
288
|
export function WebSurface3D(props: Surface3DHostProps) {
|
|
289
289
|
const boxRef = useRef<BoxRenderable | null>(null);
|
|
290
|
-
//
|
|
291
|
-
//
|
|
292
|
-
//
|
|
293
|
-
|
|
290
|
+
// The box takes the space the pane gives it, like a chart: at rest it fills
|
|
291
|
+
// the fraction of a cell its cells miss or overrun (a pane counts its chrome
|
|
292
|
+
// bars as rows, and they are taller), and while a resize has the pane ahead
|
|
293
|
+
// of this size it follows the frame or takes the size it will have. The
|
|
294
|
+
// drawing buffer follows the box (below), crisp on every move.
|
|
295
|
+
useLiveStretch(boxRef, props.width, props.height);
|
|
294
296
|
const containerRef = useRef<HTMLDivElement | null>(null);
|
|
295
297
|
const canvasRef = useRef<HTMLCanvasElement | null>(null);
|
|
296
298
|
const labelLayerRef = useRef<HTMLDivElement | null>(null);
|
|
@@ -263,10 +263,8 @@ button, [data-gloom-interactive="true"] { cursor: pointer; }
|
|
|
263
263
|
max-width: calc(100vw - 48px);
|
|
264
264
|
}
|
|
265
265
|
[data-gloom-role="status-bar"] {
|
|
266
|
-
flex:
|
|
266
|
+
flex: none;
|
|
267
267
|
height: var(--cell-h);
|
|
268
|
-
min-height: var(--cell-h);
|
|
269
|
-
max-height: var(--cell-h);
|
|
270
268
|
align-items: center;
|
|
271
269
|
border-top: 1px solid color-mix(in srgb, var(--gloom-border) 72%, transparent);
|
|
272
270
|
font-size: 11.5px;
|
|
@@ -280,7 +278,11 @@ button, [data-gloom-interactive="true"] { cursor: pointer; }
|
|
|
280
278
|
[data-gloom-role="pane-content"] {
|
|
281
279
|
overflow: hidden;
|
|
282
280
|
}
|
|
283
|
-
|
|
281
|
+
/* A pane shown as a view inside another pane (Fixed Income's sections) gets
|
|
282
|
+
its space the way a pane gets the pane's: the host's bars are taller than
|
|
283
|
+
the rows the view was given, so its root never runs past the host's box. */
|
|
284
|
+
[data-gloom-role="pane-content"] > *,
|
|
285
|
+
[data-gloom-role="nested-pane-content"] > * {
|
|
284
286
|
min-width: 0;
|
|
285
287
|
min-height: 0;
|
|
286
288
|
max-height: 100%;
|
|
@@ -1119,6 +1121,22 @@ html[data-gloom-screenshot="true"] [data-gloom-role="chart-watermark"] {
|
|
|
1119
1121
|
box-shadow: inset -1px 0 0 var(--sg-hairline);
|
|
1120
1122
|
}
|
|
1121
1123
|
[data-gloom-role="stat-grid-cell"][data-wide="true"] { grid-column: 1 / -1; box-shadow: none; }
|
|
1124
|
+
/* Column grid text whose track can be narrower than it, and the grid itself: clipped
|
|
1125
|
+
without a scroll container where the engine has `overflow: clip`. A text box sets its
|
|
1126
|
+
own overflow inline, hence !important. The margin keeps the antialiased edge of a glyph
|
|
1127
|
+
that ends at the box from being cut. */
|
|
1128
|
+
[data-gloom-clip] {
|
|
1129
|
+
overflow: clip !important;
|
|
1130
|
+
overflow-clip-margin: 2px;
|
|
1131
|
+
}
|
|
1132
|
+
@supports not (overflow: clip) {
|
|
1133
|
+
[data-gloom-clip] {
|
|
1134
|
+
overflow: hidden !important;
|
|
1135
|
+
}
|
|
1136
|
+
}
|
|
1137
|
+
[data-gloom-clip="text"] {
|
|
1138
|
+
text-overflow: ellipsis !important;
|
|
1139
|
+
}
|
|
1122
1140
|
[data-gloom-role="stat-grid-label"] {
|
|
1123
1141
|
flex: 0 0 var(--stat-label-w, 10ch);
|
|
1124
1142
|
width: var(--stat-label-w, 10ch);
|
|
@@ -1262,14 +1280,16 @@ html[data-gloom-screenshot="true"] [data-gloom-role="chart-watermark"] {
|
|
|
1262
1280
|
}
|
|
1263
1281
|
|
|
1264
1282
|
/* Reading columns (src/components/reader) set their type from their own
|
|
1265
|
-
width, not from a count of cells:
|
|
1283
|
+
width, not from a count of cells: body text at 1.1667em of the app's size
|
|
1284
|
+
(14px), or 1.1em (13.2px) under 480px. The rest of the reader's scale is in
|
|
1285
|
+
em of it (reader-text.tsx). */
|
|
1266
1286
|
[data-gloom-ui="article-reader"],
|
|
1267
1287
|
[data-gloom-ui="detail-reader"] {
|
|
1268
1288
|
container-type: inline-size;
|
|
1269
1289
|
}
|
|
1270
1290
|
@container (max-width: 480px) {
|
|
1271
1291
|
[data-gloom-ui="reader-column"] {
|
|
1272
|
-
--reader-em: 1.
|
|
1292
|
+
--reader-em: 1.1;
|
|
1273
1293
|
}
|
|
1274
1294
|
}
|
|
1275
1295
|
/* A detail's facts: label and value pairs, two pairs to a row when the
|
|
@@ -1,15 +1,13 @@
|
|
|
1
1
|
import { createRemoteBrokerAdapter } from "../../../brokers/remote-broker-adapter";
|
|
2
2
|
import type { AppServicesFactoryOptions } from "../../../core/app-service-ports";
|
|
3
3
|
import { createAppRuntime } from "../../../core/app-runtime";
|
|
4
|
-
import { newsProvider } from "../../../capabilities";
|
|
5
4
|
import { createRemoteAssetDataClient } from "./remote/asset-data-client";
|
|
6
5
|
import { RemotePersistence } from "./remote/persistence";
|
|
7
6
|
import { RemoteTickerRepository } from "./remote/ticker-repository";
|
|
8
7
|
import { connectBackendConnectionHealth } from "./remote/connection-health";
|
|
9
8
|
import { backendRequest, getElectrobunBackendInitSnapshot } from "./backend-rpc";
|
|
10
9
|
import { createCapabilityInvoker } from "./remote/capability-invoker";
|
|
11
|
-
import {
|
|
12
|
-
import { cloudNewsParams, mapCloudNewsArticle } from "../../../sources/gloomberb-cloud/news";
|
|
10
|
+
import { createDesktopNewsSource } from "./news-source";
|
|
13
11
|
|
|
14
12
|
export function createElectrobunAppServices({ config, plugins, externalPlugins }: AppServicesFactoryOptions) {
|
|
15
13
|
const dataProvider = createRemoteAssetDataClient();
|
|
@@ -33,25 +31,7 @@ export function createElectrobunAppServices({ config, plugins, externalPlugins }
|
|
|
33
31
|
// shared default drops a hidden app to the configured refresh interval.
|
|
34
32
|
newsOptions: { pollIntervalMs: undefined },
|
|
35
33
|
configure({ newsService }) {
|
|
36
|
-
newsService.register(
|
|
37
|
-
id: dataProvider.id,
|
|
38
|
-
name: dataProvider.name,
|
|
39
|
-
priority: 0,
|
|
40
|
-
provider: {
|
|
41
|
-
fetchNews: (query) => dataProvider.getNews(query),
|
|
42
|
-
fetchNewsPage: async (query) => {
|
|
43
|
-
try {
|
|
44
|
-
const response = await apiClient.getCloudNews(cloudNewsParams(query));
|
|
45
|
-
return {
|
|
46
|
-
articles: response.items.map((item) => mapCloudNewsArticle(item, query.ticker)),
|
|
47
|
-
nextCursor: response.nextCursor ?? null,
|
|
48
|
-
};
|
|
49
|
-
} catch {
|
|
50
|
-
return { articles: await dataProvider.getNews(query), nextCursor: null };
|
|
51
|
-
}
|
|
52
|
-
},
|
|
53
|
-
},
|
|
54
|
-
}));
|
|
34
|
+
newsService.register(createDesktopNewsSource(dataProvider));
|
|
55
35
|
},
|
|
56
36
|
onReady: ({ pluginRegistry }) => connectBackendConnectionHealth(pluginRegistry.connectionHealth),
|
|
57
37
|
});
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { apiClient } from "../../../api-client";
|
|
2
|
+
import { newsProvider, type NewsCapability } from "../../../capabilities";
|
|
3
|
+
import { cloudNewsParams, mapCloudNewsArticle } from "../../../sources/gloomberb-cloud/news";
|
|
4
|
+
import type { NewsArticle, NewsQuery } from "../../../types/news-source";
|
|
5
|
+
|
|
6
|
+
interface BackendNews {
|
|
7
|
+
id: string;
|
|
8
|
+
name: string;
|
|
9
|
+
getNews(query: NewsQuery): Promise<NewsArticle[]>;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
type CloudNewsClient = Pick<typeof apiClient, "getCloudNews" | "getCloudNewsStory">;
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* The view's only news source: the desktop view runs without capability
|
|
16
|
+
* handlers, so the Gloom Cloud news capability lives in the backend and is not
|
|
17
|
+
* registered here. Lists come through the backend; a page or a story by id
|
|
18
|
+
* is read from Gloom Cloud directly, which is what opens a story from a chat
|
|
19
|
+
* link that carries no list copy.
|
|
20
|
+
*/
|
|
21
|
+
export function createDesktopNewsSource(backend: BackendNews, client: CloudNewsClient = apiClient): NewsCapability {
|
|
22
|
+
return newsProvider({
|
|
23
|
+
id: backend.id,
|
|
24
|
+
name: backend.name,
|
|
25
|
+
priority: 0,
|
|
26
|
+
provider: {
|
|
27
|
+
fetchNews: (query) => backend.getNews(query),
|
|
28
|
+
fetchNewsPage: async (query) => {
|
|
29
|
+
try {
|
|
30
|
+
const response = await client.getCloudNews(cloudNewsParams(query));
|
|
31
|
+
return {
|
|
32
|
+
articles: response.items.map((item) => mapCloudNewsArticle(item, query.ticker)),
|
|
33
|
+
nextCursor: response.nextCursor ?? null,
|
|
34
|
+
};
|
|
35
|
+
} catch {
|
|
36
|
+
return { articles: await backend.getNews(query), nextCursor: null };
|
|
37
|
+
}
|
|
38
|
+
},
|
|
39
|
+
fetchNewsStory: async (storyId) => mapCloudNewsArticle(await client.getCloudNewsStory(storyId)),
|
|
40
|
+
},
|
|
41
|
+
});
|
|
42
|
+
}
|
package/src/series/validate.ts
CHANGED
|
@@ -67,11 +67,16 @@ export function splitSeriesId(id: string): { namespace: string; name: string } |
|
|
|
67
67
|
* The words a public label must not contain: the services the registering
|
|
68
68
|
* plugin reaches, taken from its declared hosts (`api.example.com` forbids
|
|
69
69
|
* "example"). A catalog says what a series is, not who serves it.
|
|
70
|
+
*
|
|
71
|
+
* A government host (`api.eia.gov`, `www.ons.gov.uk`) is the agency that
|
|
72
|
+
* publishes the statistic, not a vendor between it and the user, and a
|
|
73
|
+
* series may name its official publisher, so it forbids nothing.
|
|
70
74
|
*/
|
|
71
75
|
export function hostLabelWords(hosts: readonly string[] = []): string[] {
|
|
72
76
|
const words = new Set<string>();
|
|
73
77
|
for (const host of hosts) {
|
|
74
78
|
const labels = host.trim().toLowerCase().split(".").filter(Boolean);
|
|
79
|
+
if (labels.at(-1) === "gov" || (labels.at(-2) === "gov" && labels.at(-1)?.length === 2)) continue;
|
|
75
80
|
const name = labels.length >= 2 ? labels[labels.length - 2] : labels[0];
|
|
76
81
|
if (name && name.length >= 3) words.add(name);
|
|
77
82
|
}
|
package/src/shares/dialog.tsx
CHANGED
|
@@ -17,6 +17,7 @@ import { isPlainKey } from "../utils/keyboard";
|
|
|
17
17
|
import { truncateWithEllipsis } from "../utils/text-wrap";
|
|
18
18
|
|
|
19
19
|
import type { SharePost, ShareTarget } from "./chat";
|
|
20
|
+
import { ClippedText } from "../components/ui/display";
|
|
20
21
|
|
|
21
22
|
/** The widest the dialog gets, in cells; the preview is as wide as the form. */
|
|
22
23
|
export const SHARE_DIALOG_COLUMNS = 72;
|
|
@@ -224,7 +225,7 @@ export function ShareDialog({
|
|
|
224
225
|
<Box flexDirection="column" gap={1} {...(nativePaneChrome ? { style: { width: fitDialogWidth(SHARE_DIALOG_COLUMNS) } } : { width })}>
|
|
225
226
|
<Box flexDirection="column">
|
|
226
227
|
{preview}
|
|
227
|
-
<
|
|
228
|
+
<ClippedText fg={colors.textDim} text={meta} width={width} cut={truncateWithEllipsis} />
|
|
228
229
|
</Box>
|
|
229
230
|
<SelectButton
|
|
230
231
|
label={t("Destination")}
|
|
@@ -311,6 +311,8 @@ export function mapOptionsChain(
|
|
|
311
311
|
delayMinutes: chain.delayMinutes,
|
|
312
312
|
realtimeEligible: chain.realtimeEligible,
|
|
313
313
|
asOf: chain.asOf,
|
|
314
|
+
// Left absent when the server does not send it, so the date is then established against OPX.
|
|
315
|
+
...(chain.openInterestAsOf !== undefined ? { openInterestAsOf: chain.openInterestAsOf } : {}),
|
|
314
316
|
};
|
|
315
317
|
}
|
|
316
318
|
|
|
@@ -483,6 +483,10 @@ function mergeFundamentals(primary: Fundamentals | undefined, fallback: Fundamen
|
|
|
483
483
|
const trailing = primary?.trailingPE != null ? primary : fallback;
|
|
484
484
|
if (trailing?.trailingPEConversion) merged.trailingPEConversion = trailing.trailingPEConversion;
|
|
485
485
|
else delete merged.trailingPEConversion;
|
|
486
|
+
// An enterprise value check describes the EV it came with, or its withdrawal.
|
|
487
|
+
const valued = primary?.enterpriseValue != null || primary?.unavailableFields?.includes("enterpriseValue") ? primary : fallback;
|
|
488
|
+
if (valued?.enterpriseValueCheck) merged.enterpriseValueCheck = valued.enterpriseValueCheck;
|
|
489
|
+
else delete merged.enterpriseValueCheck;
|
|
486
490
|
}
|
|
487
491
|
return redactUnavailableFundamentals(merged);
|
|
488
492
|
}
|
|
@@ -546,6 +550,7 @@ export function mergeRefreshedFinancials(cached: TickerFinancials, fresh: Ticker
|
|
|
546
550
|
for (const field of RETRACTABLE_VALUATION_FIELDS) {
|
|
547
551
|
if (!currencyConflict && typeof statistics[field] === "number" && Number.isFinite(statistics[field])) update[field] = statistics[field];
|
|
548
552
|
}
|
|
553
|
+
if (!currencyConflict && statistics.enterpriseValueCheck) update.enterpriseValueCheck = statistics.enterpriseValueCheck;
|
|
549
554
|
let fundamentals = mergeFundamentals(update, merged.fundamentals);
|
|
550
555
|
// The source serving a multiple or yield without its per-share base has
|
|
551
556
|
// withdrawn that base, so an older one must not keep repricing the figure.
|
|
@@ -4,12 +4,12 @@ import {
|
|
|
4
4
|
type TeamCollection,
|
|
5
5
|
type TeamCollectionEvent,
|
|
6
6
|
type TeamCollectionItem,
|
|
7
|
-
} from "
|
|
8
|
-
import type { AppTickerRepositoryPort } from "
|
|
9
|
-
import type { AppAction, AppState } from "
|
|
10
|
-
import { useAppDispatch, useAppSelector, useAppStateRef } from "
|
|
11
|
-
import type { TickerRecord } from "
|
|
12
|
-
import { debugLog } from "
|
|
7
|
+
} from "../api-client";
|
|
8
|
+
import type { AppTickerRepositoryPort } from "../core/app-service-ports";
|
|
9
|
+
import type { AppAction, AppState } from "../core/state/app/types";
|
|
10
|
+
import { useAppDispatch, useAppSelector, useAppStateRef } from "../state/app/context";
|
|
11
|
+
import type { TickerRecord } from "../types/ticker";
|
|
12
|
+
import { debugLog } from "../utils/debug-log";
|
|
13
13
|
import {
|
|
14
14
|
applyMembership,
|
|
15
15
|
blankTickerRecord,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import type { TeamCollection, TeamCollectionItem } from "
|
|
2
|
-
import type { AppConfig } from "
|
|
3
|
-
import type { Portfolio, TickerRecord, Watchlist } from "
|
|
1
|
+
import type { TeamCollection, TeamCollectionItem } from "../api-client";
|
|
2
|
+
import type { AppConfig } from "../types/config";
|
|
3
|
+
import type { Portfolio, TickerRecord, Watchlist } from "../types/ticker";
|
|
4
4
|
|
|
5
5
|
const TEAM_COLLECTION_PREFIX = "team:";
|
|
6
6
|
|
|
@@ -4,8 +4,8 @@ import type {
|
|
|
4
4
|
TeamRole,
|
|
5
5
|
TeamSummary,
|
|
6
6
|
TeamUser,
|
|
7
|
-
} from "
|
|
8
|
-
import { blendHex, colors } from "
|
|
7
|
+
} from "../api-client";
|
|
8
|
+
import { blendHex, colors } from "../theme/colors";
|
|
9
9
|
|
|
10
10
|
const TEAM_CHANNEL_PREFIX = "team:";
|
|
11
11
|
|
|
@@ -4,9 +4,9 @@ import {
|
|
|
4
4
|
type TeamReceivedInvitation,
|
|
5
5
|
type TeamSummary,
|
|
6
6
|
type TeamUpdatedEvent,
|
|
7
|
-
} from "
|
|
8
|
-
import type { AppNotificationDelivery, AppNotificationRequest, PluginPersistence } from "
|
|
9
|
-
import { markNotificationLogReadByRef } from "
|
|
7
|
+
} from "../api-client";
|
|
8
|
+
import type { AppNotificationDelivery, AppNotificationRequest, PluginPersistence } from "../types/plugin";
|
|
9
|
+
import { markNotificationLogReadByRef } from "../notifications/notification-log";
|
|
10
10
|
import { describeTeamNotification, findTeam, teamIdFromChannelId, teamNotificationRefId } from "./model";
|
|
11
11
|
|
|
12
12
|
export interface TeamStoreSnapshot {
|