@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.mjs CHANGED
@@ -3019,7 +3019,7 @@ PropertyTour.displayName = "PropertyTour";
3019
3019
  var BREAKING_WINDOW_HOURS = 6;
3020
3020
  var BREAKING_MEMORY_CAP = 50;
3021
3021
  var BREAKING_MEMORY_STORAGE_KEY = "loaf:dismissed-breaking";
3022
- var emptyBreakingMemory = () => ({ dismissed: {}, pinned: {}, watermark: null });
3022
+ var emptyBreakingMemory = () => ({ dismissed: {}, blocked: {} });
3023
3023
  var isTimestampMap = (v) => typeof v === "object" && v !== null && !Array.isArray(v) && Object.values(v).every((t) => typeof t === "number" && Number.isFinite(t));
3024
3024
  var pruneMap = (map, cutoff) => {
3025
3025
  const fresh = Object.entries(map).filter(([, t]) => t >= cutoff);
@@ -3030,44 +3030,30 @@ var prune = (mem, now) => {
3030
3030
  const cutoff = now - BREAKING_WINDOW_HOURS * 36e5;
3031
3031
  return {
3032
3032
  dismissed: pruneMap(mem.dismissed, cutoff),
3033
- pinned: pruneMap(mem.pinned, cutoff),
3034
- // An expired watermark can't block anything (every in-window story is newer).
3035
- watermark: mem.watermark !== null && mem.watermark >= cutoff ? mem.watermark : null
3033
+ blocked: pruneMap(mem.blocked, cutoff)
3036
3034
  };
3037
3035
  };
3038
3036
  function parseBreakingMemory(raw, now) {
3039
3037
  if (!raw) return emptyBreakingMemory();
3040
3038
  try {
3041
3039
  const v = JSON.parse(raw);
3042
- 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))) {
3040
+ if (typeof v !== "object" || v === null || Array.isArray(v) || !isTimestampMap(v.dismissed)) {
3043
3041
  return emptyBreakingMemory();
3044
3042
  }
3045
- return prune({ dismissed: v.dismissed, pinned: v.pinned, watermark: v.watermark ?? null }, now);
3043
+ const blocked = isTimestampMap(v.blocked) ? v.blocked : {};
3044
+ return prune({ dismissed: v.dismissed, blocked }, now);
3046
3045
  } catch {
3047
3046
  return emptyBreakingMemory();
3048
3047
  }
3049
3048
  }
3050
3049
  var serializeBreakingMemory = (mem) => JSON.stringify(mem);
3051
- function recordBreakingDismissal(mem, key, publishedAtMs, now) {
3052
- return prune(
3053
- {
3054
- dismissed: { ...mem.dismissed, [key]: publishedAtMs },
3055
- pinned: mem.pinned,
3056
- watermark: Math.max(mem.watermark ?? -Infinity, publishedAtMs)
3057
- },
3058
- now
3059
- );
3050
+ function recordBreakingDismissal(mem, key, publishedAtMs, now, waiting = []) {
3051
+ const blocked = { ...mem.blocked };
3052
+ for (const w of waiting) blocked[w.key] = w.publishedAtMs;
3053
+ return prune({ dismissed: { ...mem.dismissed, [key]: publishedAtMs }, blocked }, now);
3060
3054
  }
3061
- function recordBreakingPins(mem, pins, now) {
3062
- if (pins.every((p) => mem.pinned[p.key] !== void 0)) return mem;
3063
- const pinned = { ...mem.pinned };
3064
- for (const p of pins) pinned[p.key] = p.publishedAtMs;
3065
- return prune({ dismissed: mem.dismissed, pinned, watermark: mem.watermark }, now);
3066
- }
3067
- function mayPinBreaking(mem, key, publishedAtMs) {
3068
- if (mem.dismissed[key] !== void 0) return false;
3069
- if (mem.watermark === null) return true;
3070
- return publishedAtMs > mem.watermark || mem.pinned[key] !== void 0;
3055
+ function mayPinBreaking(mem, key) {
3056
+ return mem.dismissed[key] === void 0 && mem.blocked[key] === void 0;
3071
3057
  }
3072
3058
  function loadBreakingMemory(now) {
3073
3059
  try {
@@ -3332,6 +3318,47 @@ var formatTimeAgo = (timestamp) => {
3332
3318
  if (diff < 3600) return `${Math.floor(diff / 60)}m ago`;
3333
3319
  return `${Math.floor(diff / 3600)}h ago`;
3334
3320
  };
3321
+ function freshnessNote(freshness) {
3322
+ if (freshness === "catching_up") return { label: "Catching up", title: "Reported promptly; we picked it up late" };
3323
+ if (freshness === "retrospective") return { label: "Older event", title: "The event is materially older than this report" };
3324
+ return null;
3325
+ }
3326
+ function NewsMetaNotes({ item, fontSize }) {
3327
+ const note = freshnessNote(item.freshness);
3328
+ const outlets = typeof item.reportedByCount === "number" && item.reportedByCount > 1 ? item.reportedByCount : null;
3329
+ if (!note && outlets === null) return null;
3330
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
3331
+ note && /* @__PURE__ */ jsx(
3332
+ "span",
3333
+ {
3334
+ "data-testid": "news-freshness",
3335
+ title: note.title,
3336
+ style: {
3337
+ fontSize,
3338
+ fontWeight: 500,
3339
+ color: "rgba(255,255,255,0.5)",
3340
+ border: "1px solid rgba(255,255,255,0.14)",
3341
+ borderRadius: "3px",
3342
+ padding: "0.1rem 0.35rem",
3343
+ whiteSpace: "nowrap"
3344
+ },
3345
+ children: note.label
3346
+ }
3347
+ ),
3348
+ outlets !== null && /* @__PURE__ */ jsxs(
3349
+ "span",
3350
+ {
3351
+ "data-testid": "news-outlets",
3352
+ title: `Reported by ${outlets} publishers`,
3353
+ style: { fontSize, fontWeight: 500, color: "rgba(255,255,255,0.5)", whiteSpace: "nowrap" },
3354
+ children: [
3355
+ outlets,
3356
+ " outlets"
3357
+ ]
3358
+ }
3359
+ )
3360
+ ] });
3361
+ }
3335
3362
  function getSentimentInfo(score) {
3336
3363
  if (score == null) return null;
3337
3364
  if (score > 0.15) return { arrow: "\u25B2", label: "Bullish", color: "#0ecb81" };
@@ -3394,9 +3421,27 @@ function SentimentBar({ score, color }) {
3394
3421
  }
3395
3422
  );
3396
3423
  }
3397
- function NewsArticleModal({ item, onClose }) {
3424
+ function NewsArticleModal({
3425
+ item,
3426
+ onClose,
3427
+ fetchStory
3428
+ }) {
3398
3429
  const sentimentInfo = getSentimentInfo(item.sentimentScore);
3399
3430
  const catStyle = categoryStyles[item.type] ?? categoryStyles.market;
3431
+ const [thread, setThread] = React9.useState(null);
3432
+ const threadEntries = thread ? thread.timeline.filter((entry) => entry.id !== item.id) : [];
3433
+ const storyId = item.storyId;
3434
+ const threaded = storyId !== void 0 && (typeof item.reportedByCount === "number" && item.reportedByCount > 1 || storyId !== item.id);
3435
+ React9.useEffect(() => {
3436
+ if (!fetchStory || !threaded || storyId === void 0) return;
3437
+ let dropped = false;
3438
+ void fetchStory(storyId).then((t) => {
3439
+ if (!dropped) setThread(t);
3440
+ });
3441
+ return () => {
3442
+ dropped = true;
3443
+ };
3444
+ }, [fetchStory, threaded, storyId]);
3400
3445
  React9.useEffect(() => {
3401
3446
  const handler = (e) => {
3402
3447
  if (e.key === "Escape") onClose();
@@ -3522,6 +3567,40 @@ function NewsArticleModal({ item, onClose }) {
3522
3567
  ] }),
3523
3568
  /* @__PURE__ */ jsx("div", { style: { height: "1px", background: "rgba(255,255,255,0.08)", marginBottom: "1.25rem" } }),
3524
3569
  item.summary ? /* @__PURE__ */ jsx("p", { style: { fontSize: "0.875rem", color: "rgba(255,255,255,0.8)", lineHeight: 1.75, margin: 0 }, children: item.summary }) : /* @__PURE__ */ jsx("p", { style: { fontSize: "0.8rem", color: "rgba(255,255,255,0.35)", fontStyle: "italic", margin: 0 }, children: "Summary not available for this article." }),
3570
+ thread && threadEntries.length > 0 && // Lands after the popup is already open, so it is announced rather than silent.
3571
+ /* @__PURE__ */ jsxs("div", { "aria-live": "polite", style: { marginTop: "1.25rem", paddingTop: "1rem", borderTop: "1px solid rgba(255,255,255,0.08)" }, children: [
3572
+ /* @__PURE__ */ jsxs("p", { style: { fontSize: "0.75rem", fontWeight: 600, color: "rgba(255,255,255,0.75)", margin: "0 0 0.75rem" }, children: [
3573
+ "Reported by ",
3574
+ thread.reportedBy.length > 0 ? thread.reportedBy.length : threadEntries.length + 1,
3575
+ " outlets"
3576
+ ] }),
3577
+ /* @__PURE__ */ jsx("ol", { style: { listStyle: "none", margin: 0, padding: 0, display: "flex", flexDirection: "column", gap: "0.6rem" }, children: threadEntries.map((entry) => /* @__PURE__ */ jsxs(
3578
+ "li",
3579
+ {
3580
+ "data-testid": "news-story-entry",
3581
+ "data-entry-id": entry.id,
3582
+ "data-relation": entry.relation,
3583
+ style: { borderLeft: "1px solid rgba(255,255,255,0.14)", paddingLeft: "0.7rem" },
3584
+ children: [
3585
+ /* @__PURE__ */ jsxs("div", { style: { display: "flex", gap: "0.4rem", alignItems: "baseline", flexWrap: "wrap", marginBottom: "0.15rem" }, children: [
3586
+ /* @__PURE__ */ jsx("span", { style: { fontSize: "0.65rem", color: "#848e9c" }, children: formatDateShort(entry.publishedAt) }),
3587
+ entry.source && /* @__PURE__ */ jsx("span", { style: { fontSize: "0.65rem", color: "rgba(255,255,255,0.5)" }, children: entry.source })
3588
+ ] }),
3589
+ entry.url && isSafeUrl(entry.url) ? /* @__PURE__ */ jsx(
3590
+ "a",
3591
+ {
3592
+ href: entry.url,
3593
+ target: "_blank",
3594
+ rel: "noopener noreferrer",
3595
+ style: { fontSize: "0.8rem", color: "rgba(255,255,255,0.8)", textDecoration: "none", lineHeight: 1.5, overflowWrap: "anywhere" },
3596
+ children: entry.title
3597
+ }
3598
+ ) : /* @__PURE__ */ jsx("span", { style: { fontSize: "0.8rem", color: "rgba(255,255,255,0.8)", lineHeight: 1.5, overflowWrap: "anywhere" }, children: entry.title })
3599
+ ]
3600
+ },
3601
+ entry.id
3602
+ )) })
3603
+ ] }),
3525
3604
  item.url && isSafeUrl(item.url) && /* @__PURE__ */ jsx("div", { style: { marginTop: "1.25rem", paddingTop: "1rem", borderTop: "1px solid rgba(255,255,255,0.08)" }, children: /* @__PURE__ */ jsxs(
3526
3605
  "a",
3527
3606
  {
@@ -3581,6 +3660,7 @@ var PropertyNewsUpdates = React9.forwardRef(
3581
3660
  pinBreaking = false,
3582
3661
  onAssetOpen,
3583
3662
  showAssetBadges = true,
3663
+ fetchStory,
3584
3664
  ...props
3585
3665
  }, ref) => {
3586
3666
  const isPurchaseVariant = variant === "purchases";
@@ -3652,18 +3732,8 @@ var PropertyNewsUpdates = React9.forwardRef(
3652
3732
  };
3653
3733
  const pinnedBreaking = React9.useMemo(() => {
3654
3734
  const mem = getBreakingMemory();
3655
- return breakingEligible.filter((it) => mayPinBreaking(mem, breakingKey(it), breakingPublishedMs(it))).sort((a, b) => breakingImpact(b) - breakingImpact(a)).slice(0, MAX_PINNED_BREAKING);
3735
+ return breakingEligible.filter((it) => mayPinBreaking(mem, breakingKey(it))).sort((a, b) => breakingImpact(b) - breakingImpact(a)).slice(0, MAX_PINNED_BREAKING);
3656
3736
  }, [breakingEligible, dismissedVersion]);
3657
- React9.useEffect(() => {
3658
- if (pinnedBreaking.length === 0) return;
3659
- const mem = getBreakingMemory();
3660
- const next = recordBreakingPins(
3661
- mem,
3662
- pinnedBreaking.map((it) => ({ key: breakingKey(it), publishedAtMs: breakingPublishedMs(it) })),
3663
- Date.now()
3664
- );
3665
- if (next !== mem) commitBreakingMemory(next);
3666
- }, [pinnedBreaking]);
3667
3737
  const breakingRowKeys = React9.useMemo(
3668
3738
  () => new Set(breakingEligible.map(breakingKey)),
3669
3739
  [breakingEligible]
@@ -3769,8 +3839,10 @@ var PropertyNewsUpdates = React9.forwardRef(
3769
3839
  const k = breakingKey(item);
3770
3840
  const dx = breakingSwipe?.key === k ? breakingSwipe.dx : 0;
3771
3841
  const persistDismissal = () => {
3842
+ const holding = new Set(pinnedBreaking);
3843
+ const waiting = breakingEligible.filter((c) => !holding.has(c)).map((c) => ({ key: breakingKey(c), publishedAtMs: breakingPublishedMs(c) }));
3772
3844
  commitBreakingMemory(
3773
- recordBreakingDismissal(getBreakingMemory(), k, breakingPublishedMs(item), Date.now())
3845
+ recordBreakingDismissal(getBreakingMemory(), k, breakingPublishedMs(item), Date.now(), waiting)
3774
3846
  );
3775
3847
  };
3776
3848
  const finishDismiss = () => {
@@ -4125,6 +4197,7 @@ var PropertyNewsUpdates = React9.forwardRef(
4125
4197
  },
4126
4198
  badgeAsset.ticker
4127
4199
  )),
4200
+ /* @__PURE__ */ jsx(NewsMetaNotes, { item, fontSize: isMobile ? "0.52rem" : "0.62rem" }),
4128
4201
  /* @__PURE__ */ 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" }),
4129
4202
  sentimentInfo && /* @__PURE__ */ jsx("span", { style: { fontSize: isMobile ? "0.52rem" : "0.65rem", fontWeight: 600, color: sentimentInfo.color }, children: /* @__PURE__ */ jsxs("span", { style: { display: "inline-flex", alignItems: "center", gap: "0.3rem" }, children: [
4130
4203
  sentimentInfo.arrow ? `${sentimentInfo.arrow} ` : "",
@@ -4179,7 +4252,10 @@ var PropertyNewsUpdates = React9.forwardRef(
4179
4252
  children: [
4180
4253
  /* @__PURE__ */ jsx("p", { style: { fontSize: "0.9375rem", fontWeight: 500, marginBottom: "0.4rem", color: "#fff", lineHeight: 1.4 }, children: item.title }),
4181
4254
  /* @__PURE__ */ jsxs("div", { style: { display: "flex", justifyContent: "space-between", alignItems: "flex-end", gap: "0.5rem" }, children: [
4182
- /* @__PURE__ */ jsx("div", { style: { display: "flex", alignItems: "center", gap: "0.5rem", minWidth: 0, flexWrap: "wrap" }, children: /* @__PURE__ */ jsx("span", { style: { fontSize: "0.75rem", color: isHighlighted ? "#0ecb81" : "#b5b8c5", whiteSpace: "nowrap" }, children: dateLabel }) }),
4255
+ /* @__PURE__ */ jsxs("div", { style: { display: "flex", alignItems: "center", gap: "0.5rem", minWidth: 0, flexWrap: "wrap" }, children: [
4256
+ /* @__PURE__ */ jsx("span", { style: { fontSize: "0.75rem", color: isHighlighted ? "#0ecb81" : "#b5b8c5", whiteSpace: "nowrap" }, children: dateLabel }),
4257
+ /* @__PURE__ */ jsx(NewsMetaNotes, { item, fontSize: "0.65rem" })
4258
+ ] }),
4183
4259
  /* @__PURE__ */ jsxs("div", { style: { display: "flex", flexDirection: "column", alignItems: "flex-end", gap: "0.3rem", flexShrink: 0 }, children: [
4184
4260
  sentimentInfo && /* @__PURE__ */ jsx("span", { style: { fontSize: "0.65rem", fontWeight: 700, color: sentimentInfo.color, whiteSpace: "nowrap" }, children: /* @__PURE__ */ jsxs("span", { style: { display: "inline-flex", alignItems: "center", gap: "0.3rem" }, children: [
4185
4261
  sentimentInfo.arrow ? `${sentimentInfo.arrow} ` : "",
@@ -4258,7 +4334,7 @@ var PropertyNewsUpdates = React9.forwardRef(
4258
4334
  }
4259
4335
  )
4260
4336
  ] }),
4261
- selectedItem && !isPurchaseVariant && /* @__PURE__ */ jsx(NewsArticleModal, { item: selectedItem, onClose: () => setSelectedItem(null) }),
4337
+ selectedItem && !isPurchaseVariant && /* @__PURE__ */ jsx(NewsArticleModal, { item: selectedItem, onClose: () => setSelectedItem(null), fetchStory }),
4262
4338
  badgeTip && ReactDOM.createPortal(
4263
4339
  /* @__PURE__ */ jsx(
4264
4340
  "span",
@@ -22067,6 +22143,6 @@ var SparklineChart = React9__default.memo(function SparklineChart2({ data, width
22067
22143
  );
22068
22144
  });
22069
22145
 
22070
- export { AssetSelectorBar, Badge, Button, Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle, ContactPopup, Header, HlsVideo, HousePositionSlider, HousePositionSliderMobile, LoafLiquidityBadge, LoafLiquidityLogo, LoginPopup, MobileTradeNav, OfferingProgressCard, OnboardingGuide, Orderbook, owner_booking_default as OwnerBooking, PaymentPopup, PortfolioActivityPanel, PortfolioSummary, PriceChart, PropertyBuy, PropertyCompareBar, PropertyDocuments, PropertyHeroHeader, PropertyHistory, PropertyInspectionTimes, PropertyMediaRow, PropertyNewsUpdates, PropertyOffers, PropertyOverview, PropertyPhotoGallery, PropertySubheader, PropertyTour, PropertyValuation, SentimentBar, SiteFooter, Skeleton, SlideDigit, SparklineChart, ToastProvider, TradeConfirmationModal, TradingSlider, YourOrders, __reloadBreakingMemory, __resetDismissedBreaking, badgeVariants, buttonVariants, extractSparkline, formatAge, hasPendingActivity, matchesTab, rowAgeLabel, useAdaptivePolling, useToast };
22146
+ export { AssetSelectorBar, Badge, Button, Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle, ContactPopup, Header, HlsVideo, HousePositionSlider, HousePositionSliderMobile, LoafLiquidityBadge, LoafLiquidityLogo, LoginPopup, MobileTradeNav, OfferingProgressCard, OnboardingGuide, Orderbook, owner_booking_default as OwnerBooking, PaymentPopup, PortfolioActivityPanel, PortfolioSummary, PriceChart, PropertyBuy, PropertyCompareBar, PropertyDocuments, PropertyHeroHeader, PropertyHistory, PropertyInspectionTimes, PropertyMediaRow, PropertyNewsUpdates, PropertyOffers, PropertyOverview, PropertyPhotoGallery, PropertySubheader, PropertyTour, PropertyValuation, SentimentBar, SiteFooter, Skeleton, SlideDigit, SparklineChart, ToastProvider, TradeConfirmationModal, TradingSlider, YourOrders, __reloadBreakingMemory, __resetDismissedBreaking, badgeVariants, buttonVariants, extractSparkline, formatAge, freshnessNote, hasPendingActivity, matchesTab, rowAgeLabel, useAdaptivePolling, useToast };
22071
22147
  //# sourceMappingURL=index.mjs.map
22072
22148
  //# sourceMappingURL=index.mjs.map