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.
Files changed (201) hide show
  1. package/package.json +1 -1
  2. package/src/{plugins/builtin/cloud → account}/auth-dialog.tsx +7 -7
  3. package/src/api-client/data.ts +5 -1
  4. package/src/api-client/doe.ts +19 -0
  5. package/src/api-client/earnings.ts +4 -2
  6. package/src/api-client/index.ts +1 -0
  7. package/src/app/global-shortcuts.ts +10 -0
  8. package/src/app/keybindings/actions.ts +16 -0
  9. package/src/app/runtime/desktop-deeplink.ts +1 -1
  10. package/src/app/runtime/update.ts +16 -0
  11. package/src/app.tsx +1 -1
  12. package/src/architecture/core-builtin-allowlist.json +4 -9
  13. package/src/architecture/dom-cell-metrics-allowlist.json +100 -26
  14. package/src/cli/commands/market.ts +87 -17
  15. package/src/cli/commands/options-chain.ts +93 -23
  16. package/src/cli/commands/options-leaps.ts +462 -63
  17. package/src/cli/commands/search.ts +10 -7
  18. package/src/cli/commands/ticker.ts +19 -10
  19. package/src/cli/glossary.ts +1 -1
  20. package/src/cli/index.ts +14 -6
  21. package/src/cli/known-gaps.ts +22 -0
  22. package/src/cli/pane-functions/catalog.ts +45 -10
  23. package/src/cli/pane-functions/dom.ts +36 -10
  24. package/src/cli/pane-functions/function-help.ts +4 -2
  25. package/src/cli/pane-functions/headless.ts +12 -1
  26. package/src/cli/pane-functions/index.ts +44 -3
  27. package/src/cli/pane-functions/lock-note.ts +56 -0
  28. package/src/cli/pane-functions/report-notices.ts +15 -0
  29. package/src/cli/symbol-args.ts +19 -0
  30. package/src/components/command-bar/list/view.tsx +2 -1
  31. package/src/components/command-bar/workflow/builtin.ts +2 -2
  32. package/src/components/command-bar/workflow/collection-actions.ts +1 -1
  33. package/src/components/feedback-dialog.tsx +1 -1
  34. package/src/components/index.ts +3 -2
  35. package/src/components/layout/header.tsx +58 -14
  36. package/src/components/layout/shell/chrome.ts +10 -4
  37. package/src/components/layout/shell/index.tsx +1 -1
  38. package/src/components/layout/status-bar-groups.ts +1 -1
  39. package/src/components/layout/status-bar.tsx +2 -2
  40. package/src/components/markdown-text.tsx +14 -2
  41. package/src/components/reader/article-reader.tsx +1 -1
  42. package/src/components/reader/detail-reader.tsx +10 -7
  43. package/src/components/reader/reader-text.tsx +32 -17
  44. package/src/components/ui/column-grid.tsx +206 -0
  45. package/src/components/ui/display.tsx +40 -3
  46. package/src/components/ui/fill.ts +8 -0
  47. package/src/components/ui/index.ts +2 -0
  48. package/src/layout-marketplace/gallery.tsx +2 -2
  49. package/src/layout-marketplace/linked-sync.ts +1 -1
  50. package/src/market-data/market/freshness.ts +3 -0
  51. package/src/market-data/published-apac-sessions.ts +12 -2
  52. package/src/market-data/quotes/screener-live-quotes.ts +31 -7
  53. package/src/plugins/builtin/account-management/form-components.tsx +4 -7
  54. package/src/plugins/builtin/account-management/password-dialog.tsx +3 -2
  55. package/src/plugins/builtin/backtest/pane.tsx +9 -3
  56. package/src/plugins/builtin/buildout/detail/company.tsx +4 -3
  57. package/src/plugins/builtin/buildout/detail/ui.tsx +9 -12
  58. package/src/plugins/builtin/chart-composer/index.tsx +1 -1
  59. package/src/plugins/builtin/chart-composer/presets.ts +33 -1
  60. package/src/plugins/builtin/chat/channel-labels.ts +2 -2
  61. package/src/plugins/builtin/chat/content/channel-navigation.ts +1 -1
  62. package/src/plugins/builtin/chat/content/index.tsx +2 -2
  63. package/src/plugins/builtin/chat/conversations.ts +1 -1
  64. package/src/plugins/builtin/chat/index.tsx +2 -2
  65. package/src/plugins/builtin/chat/share/flow.tsx +1 -1
  66. package/src/plugins/builtin/chat/sidebar-rows.ts +1 -1
  67. package/src/plugins/builtin/chat/sidebar.tsx +2 -2
  68. package/src/plugins/builtin/chat/unread-inbox-pane.tsx +2 -2
  69. package/src/plugins/builtin/cloud/askg/pane.tsx +2 -1
  70. package/src/plugins/builtin/cloud/askg/tool-timeline.tsx +3 -2
  71. package/src/plugins/builtin/cloud/auth-actions.tsx +1 -1
  72. package/src/plugins/builtin/cloud/auth-commands.ts +1 -1
  73. package/src/plugins/builtin/cloud/mcp-connect/dialog.tsx +1 -1
  74. package/src/plugins/builtin/cloud/team/acm-tab.tsx +3 -3
  75. package/src/plugins/builtin/cloud/team/capability.ts +2 -2
  76. package/src/plugins/builtin/cloud/team/command.ts +4 -4
  77. package/src/plugins/builtin/cloud/team/module.tsx +3 -3
  78. package/src/plugins/builtin/cloud/team/pane-model.ts +2 -2
  79. package/src/plugins/builtin/cloud/team/pane-sections.tsx +1 -1
  80. package/src/plugins/builtin/cloud/team/pane-ui.tsx +1 -1
  81. package/src/plugins/builtin/cloud/team/pane.tsx +3 -3
  82. package/src/plugins/builtin/cloud/team/status-widget.tsx +3 -3
  83. package/src/plugins/builtin/cloud/team/team-state-host.ts +1 -1
  84. package/src/plugins/builtin/cloud/team/views.ts +2 -2
  85. package/src/plugins/builtin/cloud/thesis/board-pane.tsx +1 -1
  86. package/src/plugins/builtin/cloud/thesis/detail.tsx +12 -10
  87. package/src/plugins/builtin/cloud/thesis/ticker-tab.tsx +1 -1
  88. package/src/plugins/builtin/cloud/upgrade-command.ts +1 -1
  89. package/src/plugins/builtin/congress-trades/detail.tsx +8 -4
  90. package/src/plugins/builtin/connections/pane.tsx +16 -5
  91. package/src/plugins/builtin/cpi/pane.tsx +5 -4
  92. package/src/plugins/builtin/custom-view/pane.tsx +2 -2
  93. package/src/plugins/builtin/dividend-yield/headless.ts +8 -2
  94. package/src/plugins/builtin/dividend-yield/pane.tsx +4 -2
  95. package/src/plugins/builtin/docs/blocks.tsx +13 -2
  96. package/src/plugins/builtin/docs/pane.tsx +6 -5
  97. package/src/plugins/builtin/doe/index.tsx +11 -4
  98. package/src/plugins/builtin/doe/pane.tsx +5 -4
  99. package/src/plugins/builtin/doe/prices.ts +84 -0
  100. package/src/plugins/builtin/doe/series.ts +4 -1
  101. package/src/plugins/builtin/earnings/board-model.ts +18 -1
  102. package/src/plugins/builtin/earnings/board-table.ts +18 -3
  103. package/src/plugins/builtin/earnings/headless.ts +13 -2
  104. package/src/plugins/builtin/earnings/history-model.ts +1 -1
  105. package/src/plugins/builtin/earnings/history-table.ts +2 -2
  106. package/src/plugins/builtin/earnings-calls/headless.ts +7 -21
  107. package/src/plugins/builtin/earnings-calls/index.tsx +2 -0
  108. package/src/plugins/builtin/earnings-calls/lock-note.ts +17 -0
  109. package/src/plugins/builtin/equity-screener/headless.ts +59 -1
  110. package/src/plugins/builtin/equity-screener/live.ts +82 -31
  111. package/src/plugins/builtin/equity-screener/pane.tsx +60 -23
  112. package/src/plugins/builtin/estimate-revisions/client.ts +1 -1
  113. package/src/plugins/builtin/estimate-revisions/headless.ts +9 -2
  114. package/src/plugins/builtin/estimate-revisions/index.tsx +3 -0
  115. package/src/plugins/builtin/estimate-revisions/lock-note.ts +23 -0
  116. package/src/plugins/builtin/executives/index.tsx +18 -0
  117. package/src/plugins/builtin/exposure/charts.tsx +4 -3
  118. package/src/plugins/builtin/filing-events/index.tsx +8 -10
  119. package/src/plugins/builtin/fixed-income/pane.tsx +2 -1
  120. package/src/plugins/builtin/gpu/equities.tsx +9 -6
  121. package/src/plugins/builtin/help/feedback-tab.tsx +1 -1
  122. package/src/plugins/builtin/iv-history/format.ts +3 -0
  123. package/src/plugins/builtin/iv-history/headless.ts +3 -1
  124. package/src/plugins/builtin/iv-history/index.tsx +3 -1
  125. package/src/plugins/builtin/iv-history/model.ts +3 -1
  126. package/src/plugins/builtin/iv-history/screen-pane.tsx +3 -1
  127. package/src/plugins/builtin/iv-history/universe.ts +1 -1
  128. package/src/plugins/builtin/jobs/headless.ts +6 -20
  129. package/src/plugins/builtin/jobs/index.tsx +2 -0
  130. package/src/plugins/builtin/jobs/lock-note.ts +16 -0
  131. package/src/plugins/builtin/notes/index.tsx +1 -1
  132. package/src/plugins/builtin/notes/owner.tsx +2 -2
  133. package/src/plugins/builtin/options/carry.ts +36 -0
  134. package/src/plugins/builtin/options/index.tsx +2 -1
  135. package/src/plugins/builtin/options/model-spot.ts +94 -0
  136. package/src/plugins/builtin/options/open-interest-as-of.ts +74 -0
  137. package/src/plugins/builtin/options/view.tsx +69 -11
  138. package/src/plugins/builtin/options-calculator/pane.tsx +56 -1
  139. package/src/plugins/builtin/options-calculator/premium-history.ts +34 -0
  140. package/src/plugins/builtin/options-scenario/client.ts +33 -9
  141. package/src/plugins/builtin/options-scenario/csp.ts +168 -0
  142. package/src/plugins/builtin/options-scenario/evidence.ts +3 -0
  143. package/src/plugins/builtin/options-scenario/headless.ts +24 -8
  144. package/src/plugins/builtin/options-scenario/index.tsx +3 -1
  145. package/src/plugins/builtin/options-scenario/pane.tsx +32 -6
  146. package/src/plugins/builtin/portfolio-list/mutations.ts +1 -1
  147. package/src/plugins/builtin/portfolio-list/pane/remove-ticker.ts +1 -1
  148. package/src/plugins/builtin/research/corporate-actions-pane.tsx +4 -2
  149. package/src/plugins/builtin/research/event-model.ts +7 -4
  150. package/src/plugins/builtin/risk-factors/index.tsx +18 -0
  151. package/src/plugins/builtin/risk-factors/pane.tsx +2 -1
  152. package/src/plugins/builtin/scanner/headless.ts +9 -2
  153. package/src/plugins/builtin/scanner/index.ts +3 -0
  154. package/src/plugins/builtin/scanner/lock-note.ts +17 -0
  155. package/src/plugins/builtin/series-inventory.ts +2 -1
  156. package/src/plugins/builtin/shared/cloud-denied.ts +39 -8
  157. package/src/plugins/builtin/shared/earnings-report.ts +10 -4
  158. package/src/plugins/builtin/shared/pro-lock-note.ts +58 -0
  159. package/src/plugins/builtin/shared/series-list-detail.tsx +10 -5
  160. package/src/plugins/builtin/social-mentions/client.ts +3 -3
  161. package/src/plugins/builtin/social-mentions/headless.ts +6 -2
  162. package/src/plugins/builtin/social-mentions/index.tsx +2 -1
  163. package/src/plugins/builtin/social-mentions/lock-note.ts +15 -0
  164. package/src/plugins/builtin/supply-chain/flow.tsx +15 -13
  165. package/src/plugins/builtin/supply-chain/graph.tsx +6 -4
  166. package/src/plugins/builtin/supply-chain/pane.tsx +8 -5
  167. package/src/plugins/builtin/ticker-detail/financials/aggregation.ts +14 -1
  168. package/src/plugins/builtin/ticker-detail/financials/ratios.ts +9 -7
  169. package/src/plugins/builtin/ticker-detail/headless.ts +4 -1
  170. package/src/plugins/builtin/ticker-detail/overview/components.tsx +105 -104
  171. package/src/plugins/builtin/ticker-detail/overview/model.ts +9 -2
  172. package/src/plugins/builtin/ticker-detail/overview-tab.tsx +34 -8
  173. package/src/plugins/builtin/vol-surface/pane.tsx +6 -3
  174. package/src/public/__snapshots__/public-api.test.ts.snap +2 -0
  175. package/src/renderers/dom/data-table/index.tsx +48 -15
  176. package/src/renderers/dom/data-table/row.tsx +1 -1
  177. package/src/renderers/dom/desktop/popover.tsx +6 -15
  178. package/src/renderers/dom/host/input.tsx +27 -3
  179. package/src/renderers/dom/host/surface-3d.tsx +6 -4
  180. package/src/renderers/dom/styles.css +26 -6
  181. package/src/renderers/electrobun/view/app-services.ts +2 -22
  182. package/src/renderers/electrobun/view/news-source.ts +42 -0
  183. package/src/series/validate.ts +5 -0
  184. package/src/shares/dialog.tsx +2 -1
  185. package/src/sources/gloomberb-cloud/normalizers.ts +2 -0
  186. package/src/sources/provider-router/financials.ts +5 -0
  187. package/src/{plugins/builtin/cloud/team → team}/collections-sync.ts +6 -6
  188. package/src/{plugins/builtin/cloud/team → team}/collections.ts +3 -3
  189. package/src/{plugins/builtin/cloud/team → team}/model.ts +2 -2
  190. package/src/{plugins/builtin/cloud/team → team}/pane-request.ts +1 -1
  191. package/src/{plugins/builtin/cloud/team → team}/store.ts +3 -3
  192. package/src/types/data-provider.ts +3 -0
  193. package/src/types/financials.ts +28 -0
  194. package/src/types/plugin.ts +17 -4
  195. package/src/utils/decimal-comma.ts +18 -0
  196. package/src/utils/exchanges.ts +7 -0
  197. package/src/utils/financial-statements.ts +25 -0
  198. package/src/utils/forward-dividend.ts +39 -9
  199. package/src/utils/fundamentals.ts +41 -1
  200. package/src/utils/option-expiry.ts +16 -0
  201. 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
- ...(fundamentals?.unavailableFields?.includes("enterpriseValue")
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 flexDirection={quoteBookInline ? "row" : "column"} gap={quoteBookInline ? 2 : 0} width={contentWidth}>
265
- <Box flexDirection="row" width={quoteSummaryWidth} flexShrink={0} minWidth={0} overflow="hidden">
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
- <Text fg={colors.textDim}>{truncateToDisplayWidth(companyName, companyNameWidth)}</Text>
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 quote={quote} assetCategory={moneyOptions.assetCategory} width={quoteBookWidth} />
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
- <Text fg={colors.text} width={contentWidth} wrapMode="word" wrapText>{description}</Text>
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} height={height} overflow="hidden">
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
- {content}
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
  }
@@ -47,6 +47,8 @@ exports[`the runtime exports of every public subpath are the reviewed ones 1`] =
47
47
  "ChartTableHeader",
48
48
  "Checkbox",
49
49
  "ChoiceDialog",
50
+ "ColumnGrid",
51
+ "ColumnGridPair",
50
52
  "CompositeChart",
51
53
  "ConfirmDialog",
52
54
  "CurveSurface",
@@ -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 + columnGap * WEB_CELL_WIDTH)
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
- <div
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
- </div>
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 && viewportWidth > 0 ? { maxWidth: Math.max(0, viewportWidth * WEB_CELL_WIDTH - inlinePaddingPx(horizontalPadding) * 2) } : {}),
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. Null once it is gone
37
- * from the page (a removed message), so the popover falls back to its trigger.
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
- if (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 };
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
- const width = element?.clientWidth ?? 0;
167
- return Math.max(1, Math.floor(width / WEB_CELL_WIDTH));
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
- // While a resize has the pane ahead of this size, the box takes the size it
291
- // will have and the drawing buffer follows it (below), crisp on every move.
292
- // It keeps its cells at rest: the GPU path is not checked without a GPU.
293
- useLiveStretch(boxRef, props.width, props.height, { fit: false });
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: 0 0 var(--cell-h);
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
- [data-gloom-role="pane-content"] > * {
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: 1.45em, or 1.35em under 480px. */
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.35;
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 { apiClient } from "../../../api-client";
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(newsProvider({
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
+ }
@@ -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
  }
@@ -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
- <Text fg={colors.textDim}>{truncateWithEllipsis(meta, width)}</Text>
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 "../../../../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";
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 "../../../../api-client";
2
- import type { AppConfig } from "../../../../types/config";
3
- import type { Portfolio, TickerRecord, Watchlist } from "../../../../types/ticker";
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 "../../../../api-client";
8
- import { blendHex, colors } from "../../../../theme/colors";
7
+ } from "../api-client";
8
+ import { blendHex, colors } from "../theme/colors";
9
9
 
10
10
  const TEAM_CHANNEL_PREFIX = "team:";
11
11
 
@@ -1,4 +1,4 @@
1
- import { createPaneRequestChannel } from "../../shared/pane-request";
1
+ import { createPaneRequestChannel } from "../plugins/builtin/shared/pane-request";
2
2
 
3
3
  export type TeamPaneSection = "members" | "invites" | "channels" | "settings";
4
4
 
@@ -4,9 +4,9 @@ import {
4
4
  type TeamReceivedInvitation,
5
5
  type TeamSummary,
6
6
  type TeamUpdatedEvent,
7
- } from "../../../../api-client";
8
- import type { AppNotificationDelivery, AppNotificationRequest, PluginPersistence } from "../../../../types/plugin";
9
- import { markNotificationLogReadByRef } from "../../../../notifications/notification-log";
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 {