@loafmarkets/ui 0.1.427 → 0.1.429

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/dist/index.d.mts CHANGED
@@ -324,7 +324,43 @@ type PropertyNewsItem = {
324
324
  * can concern several properties, user call 2026-08-20). Takes precedence
325
325
  * over the singular `asset`. */
326
326
  assets?: NewsBadgeAsset[];
327
- };
327
+ /** Is this old news? Derived server-side on every read (loaf-news-service
328
+ * src/freshness.ts:41). Only `catching_up` and `retrospective` are badged —
329
+ * `fresh` is the default class and badging it is noise. */
330
+ freshness?: NewsFreshness;
331
+ /** When the EVENT happened, not when it was published (schemas/article.ts:91).
332
+ * Absent is common and correct — never stand `publishedAt` in for it. */
333
+ eventTime?: string;
334
+ /** `month`/`quarter`/`year` are PERIODS, not timestamps (freshness.ts:21). */
335
+ eventTimePrecision?: NewsEventTimePrecision;
336
+ /** How many publishers ran the story; ≥1 (article.ts:190). >1 earns the
337
+ * "Reported by N outlets" trust line. */
338
+ reportedByCount?: number;
339
+ /** Storyline id — equals `id` for a standalone story. */
340
+ storyId?: string;
341
+ storyRole?: 'canonical' | 'follow_up';
342
+ publishedAtSource?: 'publisher' | 'ingest_fallback' | 'clamped_future';
343
+ };
344
+ type NewsFreshness = 'breaking' | 'fresh' | 'catching_up' | 'retrospective' | 'evergreen';
345
+ /** One report in a storyline — loaf-news-service GET /v1/stories/:story_id. */
346
+ type NewsStoryEntry = {
347
+ id: string;
348
+ title: string;
349
+ publishedAt: string;
350
+ source?: string;
351
+ url?: string;
352
+ relation?: 'duplicate' | 'follow_up';
353
+ };
354
+ type NewsStoryThread = {
355
+ storyId: string;
356
+ canonical: NewsStoryEntry;
357
+ /** Oldest → newest. */
358
+ timeline: NewsStoryEntry[];
359
+ reportedBy: string[];
360
+ firstReportedAt?: string;
361
+ lastUpdatedAt?: string;
362
+ };
363
+ type NewsEventTimePrecision = 'instant' | 'day' | 'month' | 'quarter' | 'year' | 'unknown';
328
364
  /** The badge's tooltip is name and ticker only (user call 2026-08-24: no price or % change). */
329
365
  type NewsBadgeAsset = {
330
366
  ticker: string;
@@ -373,6 +409,10 @@ interface PropertyNewsUpdatesProps extends React$1.HTMLAttributes<HTMLDivElement
373
409
  price?: number;
374
410
  changePercent?: number;
375
411
  }) => void;
412
+ /** Loads one storyline for the article popup (DI, like SharePositionModal's
413
+ * `fetchCard`): loaf-ui owns no transport, and the consumer already knows its
414
+ * news base URL. Omitted → the popup shows the article alone. */
415
+ fetchStory?: (storyId: string) => Promise<NewsStoryThread | null>;
376
416
  /** Show the square asset thumbnail(s) on property rows: which platform
377
417
  * asset(s) the story is about. ON by default (user call 2026-08-24, which
378
418
  * reversed the 22 Aug "hide for now"); pass `false` to hide them. */
@@ -387,6 +427,16 @@ declare function rowAgeLabel(publishedAt: string | undefined, now: number): {
387
427
  label: string;
388
428
  fresh: boolean;
389
429
  } | null;
430
+ /**
431
+ * The two freshness classes worth saying out loud. `fresh` is the default class, so
432
+ * badging it would mark every row; `breaking` already has the gold pin and `evergreen`
433
+ * (an explainer with no datable event) has nothing to date. Wording is what the class
434
+ * MEANS to a reader, not the wire value — loaf-news-service src/freshness.ts:41-51.
435
+ */
436
+ declare function freshnessNote(freshness?: string): {
437
+ label: string;
438
+ title: string;
439
+ } | null;
390
440
  declare function SentimentBar({ score, color }: {
391
441
  score: number;
392
442
  color: string;
@@ -1552,4 +1602,4 @@ declare function extractSparkline(candles: ReadonlyArray<{
1552
1602
  }>, maxPoints?: number): number[];
1553
1603
  declare const SparklineChart: React__default.NamedExoticComponent<SparklineChartProps>;
1554
1604
 
1555
- export { type ActivityTabId, AssetSelectorBar, type AssetSelectorBarProps, type AssetSelectorMetric, Badge, type BadgeProps, Button, type ButtonProps, Card, CardContent, type CardContentProps, CardDescription, type CardDescriptionProps, CardFooter, type CardFooterProps, CardHeader, type CardHeaderProps, type CardProps, CardTitle, type CardTitleProps, ContactPopup, type ContactPopupProps, type FinancialItem, type FundWalletParams, type FundWalletResult, Header, type HeaderNavItem, type HeaderPortfolioData, type HeaderProps, HlsVideo, type HlsVideoProps, type HlsVideoSource, type HousePositionOrderbook, type HousePositionPendingOrder, HousePositionSlider, HousePositionSliderMobile, type HousePositionSliderMobileOrderPayload, type HousePositionSliderMobileOrderbook, type HousePositionSliderMobileProps, type HousePositionSliderOrderPayload, type HousePositionSliderProps, type InspectionSlot, LoafLiquidityBadge, type LoafLiquidityDotState, LoafLiquidityLogo, type LoafLiquidityLogoProps, LoginPopup, type LoginPopupProps, MobileTradeNav, type MobileTradeNavItem, type MobileTradeNavProps, type NewsBadgeAsset, type OfferStatus, OfferingProgressCard, type OfferingProgressCardProps, OnboardingGuide, type OnboardingGuideProps, Orderbook, type OrderbookLevel, type OrderbookProps, type OrderbookSide, type OrderbookTrade, type OverviewImage, type OverviewResponse, OwnerBooking, type OwnerBookingProps, PaymentPopup, type PaymentPopupProps, PortfolioActivityPanel, type PortfolioActivityPanelProps, type PortfolioPosition, PortfolioSummary, type PortfolioSummaryProps, PriceChart, type PriceChartCandle, type PriceChartProps, type PriceChartRange, type PropertyAddressOption, PropertyBuy, PropertyCompareBar, type PropertyCompareBarProps, PropertyDocuments, type PropertyGalleryHotspot, type PropertyGalleryImage, PropertyHeroHeader, type PropertyHeroHeaderProps, PropertyHistory, type PropertyInfoItem, PropertyInspectionTimes, PropertyMediaRow, type PropertyMediaRowProps, type PropertyNewsItem, type PropertyNewsType, PropertyNewsUpdates, type PropertyNewsUpdatesProps, type PropertyOffer, PropertyOffers, type PropertyOffersProps, PropertyOverview, type PropertyOverviewProps, PropertyPhotoGallery, type PropertyPriceSummary, PropertySubheader, type PropertySubheaderAction, type PropertySubheaderProps, type PropertySubheaderTab, PropertyTour, type PropertyTourProps, PropertyValuation, type PropertyValuationProps, type PropertyValueSummary, type PurchaseFeedItem, type SelectorItem, SentimentBar, SiteFooter, Skeleton, type SkeletonProps, SlideDigit, SparklineChart, type SparklineChartProps, type ToastData, ToastProvider, type ToastVariant, TradeConfirmationModal, type TradeConfirmationModalDetails, type TradeConfirmationModalProps, TradingSlider, type TradingSliderProps, type YourOrder, type YourOrderSide, YourOrders, type YourOrdersProps, __reloadBreakingMemory, __resetDismissedBreaking, badgeVariants, buttonVariants, extractSparkline, formatAge, hasPendingActivity, matchesTab, rowAgeLabel, useAdaptivePolling, useToast };
1605
+ export { type ActivityTabId, AssetSelectorBar, type AssetSelectorBarProps, type AssetSelectorMetric, Badge, type BadgeProps, Button, type ButtonProps, Card, CardContent, type CardContentProps, CardDescription, type CardDescriptionProps, CardFooter, type CardFooterProps, CardHeader, type CardHeaderProps, type CardProps, CardTitle, type CardTitleProps, ContactPopup, type ContactPopupProps, type FinancialItem, type FundWalletParams, type FundWalletResult, Header, type HeaderNavItem, type HeaderPortfolioData, type HeaderProps, HlsVideo, type HlsVideoProps, type HlsVideoSource, type HousePositionOrderbook, type HousePositionPendingOrder, HousePositionSlider, HousePositionSliderMobile, type HousePositionSliderMobileOrderPayload, type HousePositionSliderMobileOrderbook, type HousePositionSliderMobileProps, type HousePositionSliderOrderPayload, type HousePositionSliderProps, type InspectionSlot, LoafLiquidityBadge, type LoafLiquidityDotState, LoafLiquidityLogo, type LoafLiquidityLogoProps, LoginPopup, type LoginPopupProps, MobileTradeNav, type MobileTradeNavItem, type MobileTradeNavProps, type NewsBadgeAsset, type NewsEventTimePrecision, type NewsFreshness, type NewsStoryEntry, type NewsStoryThread, type OfferStatus, OfferingProgressCard, type OfferingProgressCardProps, OnboardingGuide, type OnboardingGuideProps, Orderbook, type OrderbookLevel, type OrderbookProps, type OrderbookSide, type OrderbookTrade, type OverviewImage, type OverviewResponse, OwnerBooking, type OwnerBookingProps, PaymentPopup, type PaymentPopupProps, PortfolioActivityPanel, type PortfolioActivityPanelProps, type PortfolioPosition, PortfolioSummary, type PortfolioSummaryProps, PriceChart, type PriceChartCandle, type PriceChartProps, type PriceChartRange, type PropertyAddressOption, PropertyBuy, PropertyCompareBar, type PropertyCompareBarProps, PropertyDocuments, type PropertyGalleryHotspot, type PropertyGalleryImage, PropertyHeroHeader, type PropertyHeroHeaderProps, PropertyHistory, type PropertyInfoItem, PropertyInspectionTimes, PropertyMediaRow, type PropertyMediaRowProps, type PropertyNewsItem, type PropertyNewsType, PropertyNewsUpdates, type PropertyNewsUpdatesProps, type PropertyOffer, PropertyOffers, type PropertyOffersProps, PropertyOverview, type PropertyOverviewProps, PropertyPhotoGallery, type PropertyPriceSummary, PropertySubheader, type PropertySubheaderAction, type PropertySubheaderProps, type PropertySubheaderTab, PropertyTour, type PropertyTourProps, PropertyValuation, type PropertyValuationProps, type PropertyValueSummary, type PurchaseFeedItem, type SelectorItem, SentimentBar, SiteFooter, Skeleton, type SkeletonProps, SlideDigit, SparklineChart, type SparklineChartProps, type ToastData, ToastProvider, type ToastVariant, TradeConfirmationModal, type TradeConfirmationModalDetails, type TradeConfirmationModalProps, TradingSlider, type TradingSliderProps, type YourOrder, type YourOrderSide, YourOrders, type YourOrdersProps, __reloadBreakingMemory, __resetDismissedBreaking, badgeVariants, buttonVariants, extractSparkline, formatAge, freshnessNote, hasPendingActivity, matchesTab, rowAgeLabel, useAdaptivePolling, useToast };
package/dist/index.d.ts CHANGED
@@ -324,7 +324,43 @@ type PropertyNewsItem = {
324
324
  * can concern several properties, user call 2026-08-20). Takes precedence
325
325
  * over the singular `asset`. */
326
326
  assets?: NewsBadgeAsset[];
327
- };
327
+ /** Is this old news? Derived server-side on every read (loaf-news-service
328
+ * src/freshness.ts:41). Only `catching_up` and `retrospective` are badged —
329
+ * `fresh` is the default class and badging it is noise. */
330
+ freshness?: NewsFreshness;
331
+ /** When the EVENT happened, not when it was published (schemas/article.ts:91).
332
+ * Absent is common and correct — never stand `publishedAt` in for it. */
333
+ eventTime?: string;
334
+ /** `month`/`quarter`/`year` are PERIODS, not timestamps (freshness.ts:21). */
335
+ eventTimePrecision?: NewsEventTimePrecision;
336
+ /** How many publishers ran the story; ≥1 (article.ts:190). >1 earns the
337
+ * "Reported by N outlets" trust line. */
338
+ reportedByCount?: number;
339
+ /** Storyline id — equals `id` for a standalone story. */
340
+ storyId?: string;
341
+ storyRole?: 'canonical' | 'follow_up';
342
+ publishedAtSource?: 'publisher' | 'ingest_fallback' | 'clamped_future';
343
+ };
344
+ type NewsFreshness = 'breaking' | 'fresh' | 'catching_up' | 'retrospective' | 'evergreen';
345
+ /** One report in a storyline — loaf-news-service GET /v1/stories/:story_id. */
346
+ type NewsStoryEntry = {
347
+ id: string;
348
+ title: string;
349
+ publishedAt: string;
350
+ source?: string;
351
+ url?: string;
352
+ relation?: 'duplicate' | 'follow_up';
353
+ };
354
+ type NewsStoryThread = {
355
+ storyId: string;
356
+ canonical: NewsStoryEntry;
357
+ /** Oldest → newest. */
358
+ timeline: NewsStoryEntry[];
359
+ reportedBy: string[];
360
+ firstReportedAt?: string;
361
+ lastUpdatedAt?: string;
362
+ };
363
+ type NewsEventTimePrecision = 'instant' | 'day' | 'month' | 'quarter' | 'year' | 'unknown';
328
364
  /** The badge's tooltip is name and ticker only (user call 2026-08-24: no price or % change). */
329
365
  type NewsBadgeAsset = {
330
366
  ticker: string;
@@ -373,6 +409,10 @@ interface PropertyNewsUpdatesProps extends React$1.HTMLAttributes<HTMLDivElement
373
409
  price?: number;
374
410
  changePercent?: number;
375
411
  }) => void;
412
+ /** Loads one storyline for the article popup (DI, like SharePositionModal's
413
+ * `fetchCard`): loaf-ui owns no transport, and the consumer already knows its
414
+ * news base URL. Omitted → the popup shows the article alone. */
415
+ fetchStory?: (storyId: string) => Promise<NewsStoryThread | null>;
376
416
  /** Show the square asset thumbnail(s) on property rows: which platform
377
417
  * asset(s) the story is about. ON by default (user call 2026-08-24, which
378
418
  * reversed the 22 Aug "hide for now"); pass `false` to hide them. */
@@ -387,6 +427,16 @@ declare function rowAgeLabel(publishedAt: string | undefined, now: number): {
387
427
  label: string;
388
428
  fresh: boolean;
389
429
  } | null;
430
+ /**
431
+ * The two freshness classes worth saying out loud. `fresh` is the default class, so
432
+ * badging it would mark every row; `breaking` already has the gold pin and `evergreen`
433
+ * (an explainer with no datable event) has nothing to date. Wording is what the class
434
+ * MEANS to a reader, not the wire value — loaf-news-service src/freshness.ts:41-51.
435
+ */
436
+ declare function freshnessNote(freshness?: string): {
437
+ label: string;
438
+ title: string;
439
+ } | null;
390
440
  declare function SentimentBar({ score, color }: {
391
441
  score: number;
392
442
  color: string;
@@ -1552,4 +1602,4 @@ declare function extractSparkline(candles: ReadonlyArray<{
1552
1602
  }>, maxPoints?: number): number[];
1553
1603
  declare const SparklineChart: React__default.NamedExoticComponent<SparklineChartProps>;
1554
1604
 
1555
- export { type ActivityTabId, AssetSelectorBar, type AssetSelectorBarProps, type AssetSelectorMetric, Badge, type BadgeProps, Button, type ButtonProps, Card, CardContent, type CardContentProps, CardDescription, type CardDescriptionProps, CardFooter, type CardFooterProps, CardHeader, type CardHeaderProps, type CardProps, CardTitle, type CardTitleProps, ContactPopup, type ContactPopupProps, type FinancialItem, type FundWalletParams, type FundWalletResult, Header, type HeaderNavItem, type HeaderPortfolioData, type HeaderProps, HlsVideo, type HlsVideoProps, type HlsVideoSource, type HousePositionOrderbook, type HousePositionPendingOrder, HousePositionSlider, HousePositionSliderMobile, type HousePositionSliderMobileOrderPayload, type HousePositionSliderMobileOrderbook, type HousePositionSliderMobileProps, type HousePositionSliderOrderPayload, type HousePositionSliderProps, type InspectionSlot, LoafLiquidityBadge, type LoafLiquidityDotState, LoafLiquidityLogo, type LoafLiquidityLogoProps, LoginPopup, type LoginPopupProps, MobileTradeNav, type MobileTradeNavItem, type MobileTradeNavProps, type NewsBadgeAsset, type OfferStatus, OfferingProgressCard, type OfferingProgressCardProps, OnboardingGuide, type OnboardingGuideProps, Orderbook, type OrderbookLevel, type OrderbookProps, type OrderbookSide, type OrderbookTrade, type OverviewImage, type OverviewResponse, OwnerBooking, type OwnerBookingProps, PaymentPopup, type PaymentPopupProps, PortfolioActivityPanel, type PortfolioActivityPanelProps, type PortfolioPosition, PortfolioSummary, type PortfolioSummaryProps, PriceChart, type PriceChartCandle, type PriceChartProps, type PriceChartRange, type PropertyAddressOption, PropertyBuy, PropertyCompareBar, type PropertyCompareBarProps, PropertyDocuments, type PropertyGalleryHotspot, type PropertyGalleryImage, PropertyHeroHeader, type PropertyHeroHeaderProps, PropertyHistory, type PropertyInfoItem, PropertyInspectionTimes, PropertyMediaRow, type PropertyMediaRowProps, type PropertyNewsItem, type PropertyNewsType, PropertyNewsUpdates, type PropertyNewsUpdatesProps, type PropertyOffer, PropertyOffers, type PropertyOffersProps, PropertyOverview, type PropertyOverviewProps, PropertyPhotoGallery, type PropertyPriceSummary, PropertySubheader, type PropertySubheaderAction, type PropertySubheaderProps, type PropertySubheaderTab, PropertyTour, type PropertyTourProps, PropertyValuation, type PropertyValuationProps, type PropertyValueSummary, type PurchaseFeedItem, type SelectorItem, SentimentBar, SiteFooter, Skeleton, type SkeletonProps, SlideDigit, SparklineChart, type SparklineChartProps, type ToastData, ToastProvider, type ToastVariant, TradeConfirmationModal, type TradeConfirmationModalDetails, type TradeConfirmationModalProps, TradingSlider, type TradingSliderProps, type YourOrder, type YourOrderSide, YourOrders, type YourOrdersProps, __reloadBreakingMemory, __resetDismissedBreaking, badgeVariants, buttonVariants, extractSparkline, formatAge, hasPendingActivity, matchesTab, rowAgeLabel, useAdaptivePolling, useToast };
1605
+ export { type ActivityTabId, AssetSelectorBar, type AssetSelectorBarProps, type AssetSelectorMetric, Badge, type BadgeProps, Button, type ButtonProps, Card, CardContent, type CardContentProps, CardDescription, type CardDescriptionProps, CardFooter, type CardFooterProps, CardHeader, type CardHeaderProps, type CardProps, CardTitle, type CardTitleProps, ContactPopup, type ContactPopupProps, type FinancialItem, type FundWalletParams, type FundWalletResult, Header, type HeaderNavItem, type HeaderPortfolioData, type HeaderProps, HlsVideo, type HlsVideoProps, type HlsVideoSource, type HousePositionOrderbook, type HousePositionPendingOrder, HousePositionSlider, HousePositionSliderMobile, type HousePositionSliderMobileOrderPayload, type HousePositionSliderMobileOrderbook, type HousePositionSliderMobileProps, type HousePositionSliderOrderPayload, type HousePositionSliderProps, type InspectionSlot, LoafLiquidityBadge, type LoafLiquidityDotState, LoafLiquidityLogo, type LoafLiquidityLogoProps, LoginPopup, type LoginPopupProps, MobileTradeNav, type MobileTradeNavItem, type MobileTradeNavProps, type NewsBadgeAsset, type NewsEventTimePrecision, type NewsFreshness, type NewsStoryEntry, type NewsStoryThread, type OfferStatus, OfferingProgressCard, type OfferingProgressCardProps, OnboardingGuide, type OnboardingGuideProps, Orderbook, type OrderbookLevel, type OrderbookProps, type OrderbookSide, type OrderbookTrade, type OverviewImage, type OverviewResponse, OwnerBooking, type OwnerBookingProps, PaymentPopup, type PaymentPopupProps, PortfolioActivityPanel, type PortfolioActivityPanelProps, type PortfolioPosition, PortfolioSummary, type PortfolioSummaryProps, PriceChart, type PriceChartCandle, type PriceChartProps, type PriceChartRange, type PropertyAddressOption, PropertyBuy, PropertyCompareBar, type PropertyCompareBarProps, PropertyDocuments, type PropertyGalleryHotspot, type PropertyGalleryImage, PropertyHeroHeader, type PropertyHeroHeaderProps, PropertyHistory, type PropertyInfoItem, PropertyInspectionTimes, PropertyMediaRow, type PropertyMediaRowProps, type PropertyNewsItem, type PropertyNewsType, PropertyNewsUpdates, type PropertyNewsUpdatesProps, type PropertyOffer, PropertyOffers, type PropertyOffersProps, PropertyOverview, type PropertyOverviewProps, PropertyPhotoGallery, type PropertyPriceSummary, PropertySubheader, type PropertySubheaderAction, type PropertySubheaderProps, type PropertySubheaderTab, PropertyTour, type PropertyTourProps, PropertyValuation, type PropertyValuationProps, type PropertyValueSummary, type PurchaseFeedItem, type SelectorItem, SentimentBar, SiteFooter, Skeleton, type SkeletonProps, SlideDigit, SparklineChart, type SparklineChartProps, type ToastData, ToastProvider, type ToastVariant, TradeConfirmationModal, type TradeConfirmationModalDetails, type TradeConfirmationModalProps, TradingSlider, type TradingSliderProps, type YourOrder, type YourOrderSide, YourOrders, type YourOrdersProps, __reloadBreakingMemory, __resetDismissedBreaking, badgeVariants, buttonVariants, extractSparkline, formatAge, freshnessNote, hasPendingActivity, matchesTab, rowAgeLabel, useAdaptivePolling, useToast };
package/dist/index.js CHANGED
@@ -3044,7 +3044,7 @@ PropertyTour.displayName = "PropertyTour";
3044
3044
  var BREAKING_WINDOW_HOURS = 6;
3045
3045
  var BREAKING_MEMORY_CAP = 50;
3046
3046
  var BREAKING_MEMORY_STORAGE_KEY = "loaf:dismissed-breaking";
3047
- var emptyBreakingMemory = () => ({ dismissed: {}, pinned: {}, watermark: null });
3047
+ var emptyBreakingMemory = () => ({ dismissed: {}, blocked: {} });
3048
3048
  var isTimestampMap = (v) => typeof v === "object" && v !== null && !Array.isArray(v) && Object.values(v).every((t) => typeof t === "number" && Number.isFinite(t));
3049
3049
  var pruneMap = (map, cutoff) => {
3050
3050
  const fresh = Object.entries(map).filter(([, t]) => t >= cutoff);
@@ -3055,44 +3055,30 @@ var prune = (mem, now) => {
3055
3055
  const cutoff = now - BREAKING_WINDOW_HOURS * 36e5;
3056
3056
  return {
3057
3057
  dismissed: pruneMap(mem.dismissed, cutoff),
3058
- pinned: pruneMap(mem.pinned, cutoff),
3059
- // An expired watermark can't block anything (every in-window story is newer).
3060
- watermark: mem.watermark !== null && mem.watermark >= cutoff ? mem.watermark : null
3058
+ blocked: pruneMap(mem.blocked, cutoff)
3061
3059
  };
3062
3060
  };
3063
3061
  function parseBreakingMemory(raw, now) {
3064
3062
  if (!raw) return emptyBreakingMemory();
3065
3063
  try {
3066
3064
  const v = JSON.parse(raw);
3067
- if (typeof v !== "object" || v === null || Array.isArray(v) || !isTimestampMap(v.dismissed) || !isTimestampMap(v.pinned) || !(v.watermark === null || typeof v.watermark === "number" && Number.isFinite(v.watermark))) {
3065
+ if (typeof v !== "object" || v === null || Array.isArray(v) || !isTimestampMap(v.dismissed)) {
3068
3066
  return emptyBreakingMemory();
3069
3067
  }
3070
- return prune({ dismissed: v.dismissed, pinned: v.pinned, watermark: v.watermark ?? null }, now);
3068
+ const blocked = isTimestampMap(v.blocked) ? v.blocked : {};
3069
+ return prune({ dismissed: v.dismissed, blocked }, now);
3071
3070
  } catch {
3072
3071
  return emptyBreakingMemory();
3073
3072
  }
3074
3073
  }
3075
3074
  var serializeBreakingMemory = (mem) => JSON.stringify(mem);
3076
- function recordBreakingDismissal(mem, key, publishedAtMs, now) {
3077
- return prune(
3078
- {
3079
- dismissed: { ...mem.dismissed, [key]: publishedAtMs },
3080
- pinned: mem.pinned,
3081
- watermark: Math.max(mem.watermark ?? -Infinity, publishedAtMs)
3082
- },
3083
- now
3084
- );
3075
+ function recordBreakingDismissal(mem, key, publishedAtMs, now, waiting = []) {
3076
+ const blocked = { ...mem.blocked };
3077
+ for (const w of waiting) blocked[w.key] = w.publishedAtMs;
3078
+ return prune({ dismissed: { ...mem.dismissed, [key]: publishedAtMs }, blocked }, now);
3085
3079
  }
3086
- function recordBreakingPins(mem, pins, now) {
3087
- if (pins.every((p) => mem.pinned[p.key] !== void 0)) return mem;
3088
- const pinned = { ...mem.pinned };
3089
- for (const p of pins) pinned[p.key] = p.publishedAtMs;
3090
- return prune({ dismissed: mem.dismissed, pinned, watermark: mem.watermark }, now);
3091
- }
3092
- function mayPinBreaking(mem, key, publishedAtMs) {
3093
- if (mem.dismissed[key] !== void 0) return false;
3094
- if (mem.watermark === null) return true;
3095
- return publishedAtMs > mem.watermark || mem.pinned[key] !== void 0;
3080
+ function mayPinBreaking(mem, key) {
3081
+ return mem.dismissed[key] === void 0 && mem.blocked[key] === void 0;
3096
3082
  }
3097
3083
  function loadBreakingMemory(now) {
3098
3084
  try {
@@ -3357,6 +3343,47 @@ var formatTimeAgo = (timestamp) => {
3357
3343
  if (diff < 3600) return `${Math.floor(diff / 60)}m ago`;
3358
3344
  return `${Math.floor(diff / 3600)}h ago`;
3359
3345
  };
3346
+ function freshnessNote(freshness) {
3347
+ if (freshness === "catching_up") return { label: "Catching up", title: "Reported promptly; we picked it up late" };
3348
+ if (freshness === "retrospective") return { label: "Older event", title: "The event is materially older than this report" };
3349
+ return null;
3350
+ }
3351
+ function NewsMetaNotes({ item, fontSize }) {
3352
+ const note = freshnessNote(item.freshness);
3353
+ const outlets = typeof item.reportedByCount === "number" && item.reportedByCount > 1 ? item.reportedByCount : null;
3354
+ if (!note && outlets === null) return null;
3355
+ return /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
3356
+ note && /* @__PURE__ */ jsxRuntime.jsx(
3357
+ "span",
3358
+ {
3359
+ "data-testid": "news-freshness",
3360
+ title: note.title,
3361
+ style: {
3362
+ fontSize,
3363
+ fontWeight: 500,
3364
+ color: "rgba(255,255,255,0.5)",
3365
+ border: "1px solid rgba(255,255,255,0.14)",
3366
+ borderRadius: "3px",
3367
+ padding: "0.1rem 0.35rem",
3368
+ whiteSpace: "nowrap"
3369
+ },
3370
+ children: note.label
3371
+ }
3372
+ ),
3373
+ outlets !== null && /* @__PURE__ */ jsxRuntime.jsxs(
3374
+ "span",
3375
+ {
3376
+ "data-testid": "news-outlets",
3377
+ title: `Reported by ${outlets} publishers`,
3378
+ style: { fontSize, fontWeight: 500, color: "rgba(255,255,255,0.5)", whiteSpace: "nowrap" },
3379
+ children: [
3380
+ outlets,
3381
+ " outlets"
3382
+ ]
3383
+ }
3384
+ )
3385
+ ] });
3386
+ }
3360
3387
  function getSentimentInfo(score) {
3361
3388
  if (score == null) return null;
3362
3389
  if (score > 0.15) return { arrow: "\u25B2", label: "Bullish", color: "#0ecb81" };
@@ -3419,9 +3446,27 @@ function SentimentBar({ score, color }) {
3419
3446
  }
3420
3447
  );
3421
3448
  }
3422
- function NewsArticleModal({ item, onClose }) {
3449
+ function NewsArticleModal({
3450
+ item,
3451
+ onClose,
3452
+ fetchStory
3453
+ }) {
3423
3454
  const sentimentInfo = getSentimentInfo(item.sentimentScore);
3424
3455
  const catStyle = categoryStyles[item.type] ?? categoryStyles.market;
3456
+ const [thread, setThread] = React9__namespace.useState(null);
3457
+ const threadEntries = thread ? thread.timeline.filter((entry) => entry.id !== item.id) : [];
3458
+ const storyId = item.storyId;
3459
+ const threaded = storyId !== void 0 && (typeof item.reportedByCount === "number" && item.reportedByCount > 1 || storyId !== item.id);
3460
+ React9__namespace.useEffect(() => {
3461
+ if (!fetchStory || !threaded || storyId === void 0) return;
3462
+ let dropped = false;
3463
+ void fetchStory(storyId).then((t) => {
3464
+ if (!dropped) setThread(t);
3465
+ });
3466
+ return () => {
3467
+ dropped = true;
3468
+ };
3469
+ }, [fetchStory, threaded, storyId]);
3425
3470
  React9__namespace.useEffect(() => {
3426
3471
  const handler = (e) => {
3427
3472
  if (e.key === "Escape") onClose();
@@ -3547,6 +3592,40 @@ function NewsArticleModal({ item, onClose }) {
3547
3592
  ] }),
3548
3593
  /* @__PURE__ */ jsxRuntime.jsx("div", { style: { height: "1px", background: "rgba(255,255,255,0.08)", marginBottom: "1.25rem" } }),
3549
3594
  item.summary ? /* @__PURE__ */ jsxRuntime.jsx("p", { style: { fontSize: "0.875rem", color: "rgba(255,255,255,0.8)", lineHeight: 1.75, margin: 0 }, children: item.summary }) : /* @__PURE__ */ jsxRuntime.jsx("p", { style: { fontSize: "0.8rem", color: "rgba(255,255,255,0.35)", fontStyle: "italic", margin: 0 }, children: "Summary not available for this article." }),
3595
+ thread && threadEntries.length > 0 && // Lands after the popup is already open, so it is announced rather than silent.
3596
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { "aria-live": "polite", style: { marginTop: "1.25rem", paddingTop: "1rem", borderTop: "1px solid rgba(255,255,255,0.08)" }, children: [
3597
+ /* @__PURE__ */ jsxRuntime.jsxs("p", { style: { fontSize: "0.75rem", fontWeight: 600, color: "rgba(255,255,255,0.75)", margin: "0 0 0.75rem" }, children: [
3598
+ "Reported by ",
3599
+ thread.reportedBy.length > 0 ? thread.reportedBy.length : threadEntries.length + 1,
3600
+ " outlets"
3601
+ ] }),
3602
+ /* @__PURE__ */ jsxRuntime.jsx("ol", { style: { listStyle: "none", margin: 0, padding: 0, display: "flex", flexDirection: "column", gap: "0.6rem" }, children: threadEntries.map((entry) => /* @__PURE__ */ jsxRuntime.jsxs(
3603
+ "li",
3604
+ {
3605
+ "data-testid": "news-story-entry",
3606
+ "data-entry-id": entry.id,
3607
+ "data-relation": entry.relation,
3608
+ style: { borderLeft: "1px solid rgba(255,255,255,0.14)", paddingLeft: "0.7rem" },
3609
+ children: [
3610
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { style: { display: "flex", gap: "0.4rem", alignItems: "baseline", flexWrap: "wrap", marginBottom: "0.15rem" }, children: [
3611
+ /* @__PURE__ */ jsxRuntime.jsx("span", { style: { fontSize: "0.65rem", color: "#848e9c" }, children: formatDateShort(entry.publishedAt) }),
3612
+ entry.source && /* @__PURE__ */ jsxRuntime.jsx("span", { style: { fontSize: "0.65rem", color: "rgba(255,255,255,0.5)" }, children: entry.source })
3613
+ ] }),
3614
+ entry.url && isSafeUrl(entry.url) ? /* @__PURE__ */ jsxRuntime.jsx(
3615
+ "a",
3616
+ {
3617
+ href: entry.url,
3618
+ target: "_blank",
3619
+ rel: "noopener noreferrer",
3620
+ style: { fontSize: "0.8rem", color: "rgba(255,255,255,0.8)", textDecoration: "none", lineHeight: 1.5, overflowWrap: "anywhere" },
3621
+ children: entry.title
3622
+ }
3623
+ ) : /* @__PURE__ */ jsxRuntime.jsx("span", { style: { fontSize: "0.8rem", color: "rgba(255,255,255,0.8)", lineHeight: 1.5, overflowWrap: "anywhere" }, children: entry.title })
3624
+ ]
3625
+ },
3626
+ entry.id
3627
+ )) })
3628
+ ] }),
3550
3629
  item.url && isSafeUrl(item.url) && /* @__PURE__ */ jsxRuntime.jsx("div", { style: { marginTop: "1.25rem", paddingTop: "1rem", borderTop: "1px solid rgba(255,255,255,0.08)" }, children: /* @__PURE__ */ jsxRuntime.jsxs(
3551
3630
  "a",
3552
3631
  {
@@ -3606,6 +3685,7 @@ var PropertyNewsUpdates = React9__namespace.forwardRef(
3606
3685
  pinBreaking = false,
3607
3686
  onAssetOpen,
3608
3687
  showAssetBadges = true,
3688
+ fetchStory,
3609
3689
  ...props
3610
3690
  }, ref) => {
3611
3691
  const isPurchaseVariant = variant === "purchases";
@@ -3677,18 +3757,8 @@ var PropertyNewsUpdates = React9__namespace.forwardRef(
3677
3757
  };
3678
3758
  const pinnedBreaking = React9__namespace.useMemo(() => {
3679
3759
  const mem = getBreakingMemory();
3680
- return breakingEligible.filter((it) => mayPinBreaking(mem, breakingKey(it), breakingPublishedMs(it))).sort((a, b) => breakingImpact(b) - breakingImpact(a)).slice(0, MAX_PINNED_BREAKING);
3760
+ return breakingEligible.filter((it) => mayPinBreaking(mem, breakingKey(it))).sort((a, b) => breakingImpact(b) - breakingImpact(a)).slice(0, MAX_PINNED_BREAKING);
3681
3761
  }, [breakingEligible, dismissedVersion]);
3682
- React9__namespace.useEffect(() => {
3683
- if (pinnedBreaking.length === 0) return;
3684
- const mem = getBreakingMemory();
3685
- const next = recordBreakingPins(
3686
- mem,
3687
- pinnedBreaking.map((it) => ({ key: breakingKey(it), publishedAtMs: breakingPublishedMs(it) })),
3688
- Date.now()
3689
- );
3690
- if (next !== mem) commitBreakingMemory(next);
3691
- }, [pinnedBreaking]);
3692
3762
  const breakingRowKeys = React9__namespace.useMemo(
3693
3763
  () => new Set(breakingEligible.map(breakingKey)),
3694
3764
  [breakingEligible]
@@ -3794,8 +3864,10 @@ var PropertyNewsUpdates = React9__namespace.forwardRef(
3794
3864
  const k = breakingKey(item);
3795
3865
  const dx = breakingSwipe?.key === k ? breakingSwipe.dx : 0;
3796
3866
  const persistDismissal = () => {
3867
+ const holding = new Set(pinnedBreaking);
3868
+ const waiting = breakingEligible.filter((c) => !holding.has(c)).map((c) => ({ key: breakingKey(c), publishedAtMs: breakingPublishedMs(c) }));
3797
3869
  commitBreakingMemory(
3798
- recordBreakingDismissal(getBreakingMemory(), k, breakingPublishedMs(item), Date.now())
3870
+ recordBreakingDismissal(getBreakingMemory(), k, breakingPublishedMs(item), Date.now(), waiting)
3799
3871
  );
3800
3872
  };
3801
3873
  const finishDismiss = () => {
@@ -4150,6 +4222,7 @@ var PropertyNewsUpdates = React9__namespace.forwardRef(
4150
4222
  },
4151
4223
  badgeAsset.ticker
4152
4224
  )),
4225
+ /* @__PURE__ */ jsxRuntime.jsx(NewsMetaNotes, { item, fontSize: isMobile ? "0.52rem" : "0.62rem" }),
4153
4226
  /* @__PURE__ */ jsxRuntime.jsx("span", { style: { padding: isMobile ? "0.1rem 0.35rem" : "0.15rem 0.4rem", borderRadius: "2px", fontSize: isMobile ? "0.52rem" : "0.65rem", fontWeight: 500, backgroundColor: isProperty ? "rgba(14,203,129,0.1)" : "rgba(240,185,11,0.1)", color: isProperty ? "#0ecb81" : "#E6C87E" }, children: isProperty ? "Property Update" : "Market News" }),
4154
4227
  sentimentInfo && /* @__PURE__ */ jsxRuntime.jsx("span", { style: { fontSize: isMobile ? "0.52rem" : "0.65rem", fontWeight: 600, color: sentimentInfo.color }, children: /* @__PURE__ */ jsxRuntime.jsxs("span", { style: { display: "inline-flex", alignItems: "center", gap: "0.3rem" }, children: [
4155
4228
  sentimentInfo.arrow ? `${sentimentInfo.arrow} ` : "",
@@ -4204,7 +4277,10 @@ var PropertyNewsUpdates = React9__namespace.forwardRef(
4204
4277
  children: [
4205
4278
  /* @__PURE__ */ jsxRuntime.jsx("p", { style: { fontSize: "0.9375rem", fontWeight: 500, marginBottom: "0.4rem", color: "#fff", lineHeight: 1.4 }, children: item.title }),
4206
4279
  /* @__PURE__ */ jsxRuntime.jsxs("div", { style: { display: "flex", justifyContent: "space-between", alignItems: "flex-end", gap: "0.5rem" }, children: [
4207
- /* @__PURE__ */ jsxRuntime.jsx("div", { style: { display: "flex", alignItems: "center", gap: "0.5rem", minWidth: 0, flexWrap: "wrap" }, children: /* @__PURE__ */ jsxRuntime.jsx("span", { style: { fontSize: "0.75rem", color: isHighlighted ? "#0ecb81" : "#b5b8c5", whiteSpace: "nowrap" }, children: dateLabel }) }),
4280
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { style: { display: "flex", alignItems: "center", gap: "0.5rem", minWidth: 0, flexWrap: "wrap" }, children: [
4281
+ /* @__PURE__ */ jsxRuntime.jsx("span", { style: { fontSize: "0.75rem", color: isHighlighted ? "#0ecb81" : "#b5b8c5", whiteSpace: "nowrap" }, children: dateLabel }),
4282
+ /* @__PURE__ */ jsxRuntime.jsx(NewsMetaNotes, { item, fontSize: "0.65rem" })
4283
+ ] }),
4208
4284
  /* @__PURE__ */ jsxRuntime.jsxs("div", { style: { display: "flex", flexDirection: "column", alignItems: "flex-end", gap: "0.3rem", flexShrink: 0 }, children: [
4209
4285
  sentimentInfo && /* @__PURE__ */ jsxRuntime.jsx("span", { style: { fontSize: "0.65rem", fontWeight: 700, color: sentimentInfo.color, whiteSpace: "nowrap" }, children: /* @__PURE__ */ jsxRuntime.jsxs("span", { style: { display: "inline-flex", alignItems: "center", gap: "0.3rem" }, children: [
4210
4286
  sentimentInfo.arrow ? `${sentimentInfo.arrow} ` : "",
@@ -4283,7 +4359,7 @@ var PropertyNewsUpdates = React9__namespace.forwardRef(
4283
4359
  }
4284
4360
  )
4285
4361
  ] }),
4286
- selectedItem && !isPurchaseVariant && /* @__PURE__ */ jsxRuntime.jsx(NewsArticleModal, { item: selectedItem, onClose: () => setSelectedItem(null) }),
4362
+ selectedItem && !isPurchaseVariant && /* @__PURE__ */ jsxRuntime.jsx(NewsArticleModal, { item: selectedItem, onClose: () => setSelectedItem(null), fetchStory }),
4287
4363
  badgeTip && ReactDOM__namespace.createPortal(
4288
4364
  /* @__PURE__ */ jsxRuntime.jsx(
4289
4365
  "span",
@@ -22147,6 +22223,7 @@ exports.badgeVariants = badgeVariants;
22147
22223
  exports.buttonVariants = buttonVariants;
22148
22224
  exports.extractSparkline = extractSparkline;
22149
22225
  exports.formatAge = formatAge;
22226
+ exports.freshnessNote = freshnessNote;
22150
22227
  exports.hasPendingActivity = hasPendingActivity;
22151
22228
  exports.matchesTab = matchesTab;
22152
22229
  exports.rowAgeLabel = rowAgeLabel;