@sensiblestats/widget-react 0.98.0 → 0.99.0

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/CHANGELOG.md CHANGED
@@ -1,5 +1,16 @@
1
1
  # @sensiblestats/widget-react
2
2
 
3
+ ## 0.99.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 5d0450f: Add `config.page`. The greeting request carries the page the widget sits on, and a page change while mounted re-greets until the visitor has typed or tapped. Greeting tiles show `meta` in the hero's top row and draw `imageCodes` as text marks when there are no images.
8
+
9
+ ### Patch Changes
10
+
11
+ - Updated dependencies [5d0450f]
12
+ - @sensiblestats/widget-sdk@0.66.0
13
+
3
14
  ## 0.98.0
4
15
 
5
16
  ### Minor Changes
package/README.md CHANGED
@@ -38,6 +38,17 @@ Common `config` fields:
38
38
  and personalization to your signed-in user.
39
39
  - White-label branding — `logoUrl`, `headerTitle`, `accent`, `cta`, `surface`,
40
40
  `ink`. Absent → the default SensibleStats look.
41
+ - `page` — the page the widget sits on, `{ sport, kind?, matchId?, competitionId? }`, so the
42
+ first greeting can lead with it. Football ids are bare (`'4820113'`); NBA ids are
43
+ `'nba:game:{id}'` or digits. `sport` is `'football'` or `'nba'`; the server answers any other
44
+ value with `unsupported_sport` and the visitor sees today's greeting, so check your spelling
45
+ rather than expecting a client-side error. The page rides on greeting requests only, never on a
46
+ typed or tapped turn. Omit it and the request is unchanged.
47
+
48
+ If `page` changes while the widget is mounted, it re-greets with the new page (after 300 ms,
49
+ so quick changes collapse), but only while the visitor has not typed or tapped anything. Once
50
+ the visitor has sent a message, a page change does nothing. The re-greet reads and writes no
51
+ stored conversation.
41
52
 
42
53
  ## Hooks: the slip and the match page
43
54
 
package/dist/index.cjs CHANGED
@@ -1137,7 +1137,7 @@ function isEnglishLocale(locale) {
1137
1137
  }
1138
1138
 
1139
1139
  // src/version.generated.ts
1140
- var WIDGET_REACT_VERSION = "0.98.0";
1140
+ var WIDGET_REACT_VERSION = "0.99.0";
1141
1141
 
1142
1142
  // src/config.ts
1143
1143
  function normalizeUi(cfg) {
@@ -2772,7 +2772,9 @@ function seedIdCounter(messages) {
2772
2772
  }
2773
2773
  return max + 1;
2774
2774
  }
2775
- function useChatStream(conversation, starterTexts, greetingMessage, storageKey) {
2775
+ var pageKey = (p) => p ? `${p.sport}|${p.kind ?? "section"}|${p.matchId ?? p.competitionId ?? ""}` : "";
2776
+ var REGREET_DEBOUNCE_MS = 300;
2777
+ function useChatStream(conversation, starterTexts, greetingMessage, storageKey, page) {
2776
2778
  const boot = (0, import_react3.useMemo)(() => {
2777
2779
  const payload = storageKey ? readConversation(storageKey, Date.now()) : null;
2778
2780
  const restored = payload != null && payload !== "expired" && payload !== "corrupt";
@@ -2790,6 +2792,10 @@ function useChatStream(conversation, starterTexts, greetingMessage, storageKey)
2790
2792
  stateRef.current = state;
2791
2793
  const greetedRef = (0, import_react3.useRef)(boot.restored);
2792
2794
  const hydratedRef = (0, import_react3.useRef)(false);
2795
+ const pageRef = (0, import_react3.useRef)(page);
2796
+ pageRef.current = page;
2797
+ const greetedKeyRef = (0, import_react3.useRef)(null);
2798
+ const greetingCancelledRef = (0, import_react3.useRef)(false);
2793
2799
  const restoreSeededRef = (0, import_react3.useRef)(false);
2794
2800
  if (!restoreSeededRef.current) {
2795
2801
  restoreSeededRef.current = true;
@@ -2865,7 +2871,13 @@ function useChatStream(conversation, starterTexts, greetingMessage, storageKey)
2865
2871
  handleRef.current?.cancel();
2866
2872
  clearFallbackTimer();
2867
2873
  dispatch({ kind: "GREETING_SENT", assistantId: nextId() });
2868
- const handle = conversation.send({ message, clientCapabilities: [...CLIENT_CAPABILITIES] });
2874
+ greetedKeyRef.current = pageKey(pageRef.current);
2875
+ greetingCancelledRef.current = false;
2876
+ const handle = conversation.send({
2877
+ message,
2878
+ clientCapabilities: [...CLIENT_CAPABILITIES],
2879
+ ...pageRef.current ? { page: pageRef.current } : {}
2880
+ });
2869
2881
  handleRef.current = handle;
2870
2882
  armFallbackTimer(() => dispatch({ kind: "STOP" }));
2871
2883
  consume(handle, () => dispatch({ kind: "STOP" }));
@@ -2878,6 +2890,24 @@ function useChatStream(conversation, starterTexts, greetingMessage, storageKey)
2878
2890
  greetedRef.current = true;
2879
2891
  fireGreeting();
2880
2892
  }, [fireGreeting]);
2893
+ const key = pageKey(page);
2894
+ (0, import_react3.useEffect)(() => {
2895
+ if (greetedKeyRef.current === null) return;
2896
+ if (key === greetedKeyRef.current && !greetingCancelledRef.current) return;
2897
+ if (stateRef.current.messages.some((m) => m.role === "user")) return;
2898
+ if (key !== greetedKeyRef.current && handleRef.current) {
2899
+ handleRef.current.cancel();
2900
+ greetingCancelledRef.current = true;
2901
+ }
2902
+ const timer = setTimeout(() => {
2903
+ if (stateRef.current.messages.some((m) => m.role === "user")) return;
2904
+ idRef.current = 0;
2905
+ conversation.conversationId = void 0;
2906
+ dispatch({ kind: "RESET" });
2907
+ fireGreeting();
2908
+ }, REGREET_DEBOUNCE_MS);
2909
+ return () => clearTimeout(timer);
2910
+ }, [key, conversation, fireGreeting]);
2881
2911
  (0, import_react3.useEffect)(() => {
2882
2912
  if (!state.limit) return;
2883
2913
  const delay = limitDelayMs(state.limit, Date.now());
@@ -6009,8 +6039,27 @@ function splitOdds(subtitle) {
6009
6039
  const m = ODDS_TAIL.exec(subtitle);
6010
6040
  return m ? { text: subtitle.slice(0, m.index), odds: m[1] } : { text: subtitle };
6011
6041
  }
6042
+ function codesOf(tile) {
6043
+ return (tile.imageCodes ?? []).filter(Boolean);
6044
+ }
6045
+ function Marks({ codes }) {
6046
+ return /* @__PURE__ */ (0, import_jsx_runtime23.jsx)(import_jsx_runtime23.Fragment, { children: codes.map((code, i) => /* @__PURE__ */ (0, import_jsx_runtime23.jsx)("span", { className: "ss-w-dash__mark", children: code }, `${code}-${i}`)) });
6047
+ }
6048
+ var KEEP_MAX = 16;
6049
+ function DotSeparated({ text }) {
6050
+ const parts = text.split(" \xB7 ");
6051
+ return /* @__PURE__ */ (0, import_jsx_runtime23.jsx)(import_jsx_runtime23.Fragment, { children: parts.map((part, i) => {
6052
+ const piece = i < parts.length - 1 ? `${part} \xB7` : part;
6053
+ return /* @__PURE__ */ (0, import_jsx_runtime23.jsxs)("span", { children: [
6054
+ i > 0 ? " " : null,
6055
+ part.length <= KEEP_MAX ? /* @__PURE__ */ (0, import_jsx_runtime23.jsx)("span", { className: "ss-w-dash__keep", children: piece }) : piece
6056
+ ] }, i);
6057
+ }) });
6058
+ }
6059
+ var keepDashWithHome = (title) => title.replace(/ - /g, "\xA0- ");
6012
6060
  function HeroTile({ tile, onTap, oddsLabel }) {
6013
6061
  const crests = (tile.imageUrls ?? []).filter(Boolean).slice(0, 2);
6062
+ const codes = crests.length === 0 ? codesOf(tile).slice(0, 2) : [];
6014
6063
  const sub = tile.subtitle ? splitOdds(tile.subtitle) : null;
6015
6064
  return /* @__PURE__ */ (0, import_jsx_runtime23.jsxs)("button", { type: "button", className: "ss-w-dash-tile ss-w-dash-tile--full ss-w-dash-hero", onClick: onTap, children: [
6016
6065
  /* @__PURE__ */ (0, import_jsx_runtime23.jsxs)("span", { className: "ss-w-dash-hero-top", children: [
@@ -6022,36 +6071,40 @@ function HeroTile({ tile, onTap, oddsLabel }) {
6022
6071
  ] }),
6023
6072
  sub.odds
6024
6073
  ] }) : null,
6074
+ !sub?.odds && tile.meta ? /* @__PURE__ */ (0, import_jsx_runtime23.jsx)("span", { className: "ss-w-dash__meta", children: /* @__PURE__ */ (0, import_jsx_runtime23.jsx)(DotSeparated, { text: tile.meta }) }) : null,
6025
6075
  /* @__PURE__ */ (0, import_jsx_runtime23.jsx)("span", { className: "ss-w-dash-hero-chevron", children: /* @__PURE__ */ (0, import_jsx_runtime23.jsx)(ChevronIcon, {}) })
6026
6076
  ] }),
6027
6077
  /* @__PURE__ */ (0, import_jsx_runtime23.jsxs)("span", { className: "ss-w-dash-hero-main", children: [
6028
- /* @__PURE__ */ (0, import_jsx_runtime23.jsx)("span", { className: "ss-w-dash-hero-visual", "aria-hidden": "true", children: crests.length > 0 ? crests.map((url, i) => /* @__PURE__ */ (0, import_jsx_runtime23.jsx)("span", { className: "ss-w-dash-hero-crest", style: { backgroundImage: `url(${url})` } }, `${url}-${i}`)) : /* @__PURE__ */ (0, import_jsx_runtime23.jsx)("span", { className: "ss-w-dash-hero-icon", children: /* @__PURE__ */ (0, import_jsx_runtime23.jsx)(DashboardIcon, { name: tile.icon }) }) }),
6078
+ /* @__PURE__ */ (0, import_jsx_runtime23.jsx)("span", { className: "ss-w-dash-hero-visual", "aria-hidden": "true", children: crests.length > 0 ? crests.map((url, i) => /* @__PURE__ */ (0, import_jsx_runtime23.jsx)("span", { className: "ss-w-dash-hero-crest", style: { backgroundImage: `url(${url})` } }, `${url}-${i}`)) : codes.length > 0 ? /* @__PURE__ */ (0, import_jsx_runtime23.jsx)(Marks, { codes }) : /* @__PURE__ */ (0, import_jsx_runtime23.jsx)("span", { className: "ss-w-dash-hero-icon", children: /* @__PURE__ */ (0, import_jsx_runtime23.jsx)(DashboardIcon, { name: tile.icon }) }) }),
6029
6079
  /* @__PURE__ */ (0, import_jsx_runtime23.jsxs)("span", { className: "ss-w-dash-hero-body", children: [
6030
- /* @__PURE__ */ (0, import_jsx_runtime23.jsx)("span", { className: "ss-w-dash-hero-title", children: tile.title }),
6031
- sub?.text ? /* @__PURE__ */ (0, import_jsx_runtime23.jsx)("span", { className: "ss-w-dash-hero-subtitle", children: sub.text }) : null
6080
+ /* @__PURE__ */ (0, import_jsx_runtime23.jsx)("span", { className: "ss-w-dash-hero-title", children: keepDashWithHome(tile.title) }),
6081
+ sub?.text ? /* @__PURE__ */ (0, import_jsx_runtime23.jsx)("span", { className: "ss-w-dash-hero-subtitle", children: sub.text.includes(" \xB7 ") ? /* @__PURE__ */ (0, import_jsx_runtime23.jsx)(DotSeparated, { text: sub.text }) : sub.text }) : null
6032
6082
  ] })
6033
6083
  ] })
6034
6084
  ] });
6035
6085
  }
6086
+ var PRICE_BADGE = /^\d+(?:[.,]\d+)?$/;
6036
6087
  function DashboardTile({ tile, onTap, oddsLabel = "Odds", badgePlacement = "meta" }) {
6037
6088
  if (tile.emphasis === "hero") return /* @__PURE__ */ (0, import_jsx_runtime23.jsx)(HeroTile, { tile, onTap, oddsLabel });
6038
6089
  const spanClass = tile.span === "full" ? "ss-w-dash-tile--full" : "ss-w-dash-tile--half";
6039
6090
  const crests = (tile.imageUrls ?? []).slice(0, MAX_CRESTS);
6040
- const endBadge = badgePlacement === "end" && tile.badgeText ? tile.badgeText : null;
6091
+ const endBadge = badgePlacement === "end" && tile.badgeText && PRICE_BADGE.test(tile.badgeText.trim()) ? tile.badgeText : null;
6041
6092
  const metaBadge = endBadge ? null : tile.badgeText;
6042
- const hasMeta = Boolean(metaBadge) || crests.length > 0;
6093
+ const codes = crests.length === 0 ? codesOf(tile).slice(0, MAX_CRESTS) : [];
6094
+ const hasMeta = Boolean(metaBadge) || crests.length > 0 || codes.length > 0;
6043
6095
  return /* @__PURE__ */ (0, import_jsx_runtime23.jsxs)("button", { type: "button", className: `ss-w-dash-tile ${spanClass}`, onClick: onTap, children: [
6044
6096
  /* @__PURE__ */ (0, import_jsx_runtime23.jsx)("span", { className: "ss-w-dash-tile-icon", children: /* @__PURE__ */ (0, import_jsx_runtime23.jsx)(DashboardIcon, { name: tile.icon }) }),
6045
6097
  /* @__PURE__ */ (0, import_jsx_runtime23.jsxs)("span", { className: "ss-w-dash-tile-body", children: [
6046
6098
  /* @__PURE__ */ (0, import_jsx_runtime23.jsx)("span", { className: "ss-w-dash-tile-title", children: tile.title }),
6047
- tile.subtitle ? /* @__PURE__ */ (0, import_jsx_runtime23.jsx)("span", { className: "ss-w-dash-tile-subtitle", children: tile.subtitle }) : null,
6099
+ tile.subtitle ? /* @__PURE__ */ (0, import_jsx_runtime23.jsx)("span", { className: "ss-w-dash-tile-subtitle", children: tile.subtitle.includes(" \xB7 ") ? /* @__PURE__ */ (0, import_jsx_runtime23.jsx)(DotSeparated, { text: tile.subtitle }) : tile.subtitle }) : null,
6048
6100
  hasMeta ? (
6049
6101
  // Badge + crests sit on their own row, below the title, instead of sharing the
6050
6102
  // icon/chevron row -- at the half-span tile's ~180px width a nowrap badge alone
6051
6103
  // (~70px) left the title column at ~22px and forced character-by-character wrapping.
6052
6104
  /* @__PURE__ */ (0, import_jsx_runtime23.jsxs)("span", { className: "ss-w-dash-tile-meta", children: [
6053
6105
  metaBadge ? /* @__PURE__ */ (0, import_jsx_runtime23.jsx)("span", { className: "ss-w-dash-tile-badge", children: metaBadge }) : null,
6054
- crests.length > 0 ? /* @__PURE__ */ (0, import_jsx_runtime23.jsx)("span", { className: "ss-w-dash-tile-crests", children: crests.map((url, i) => /* @__PURE__ */ (0, import_jsx_runtime23.jsx)("span", { className: "ss-w-dash-tile-crest", style: { backgroundImage: `url(${url})` }, "aria-hidden": "true" }, `${url}-${i}`)) }) : null
6106
+ crests.length > 0 ? /* @__PURE__ */ (0, import_jsx_runtime23.jsx)("span", { className: "ss-w-dash-tile-crests", children: crests.map((url, i) => /* @__PURE__ */ (0, import_jsx_runtime23.jsx)("span", { className: "ss-w-dash-tile-crest", style: { backgroundImage: `url(${url})` }, "aria-hidden": "true" }, `${url}-${i}`)) }) : null,
6107
+ codes.length > 0 ? /* @__PURE__ */ (0, import_jsx_runtime23.jsx)("span", { className: "ss-w-dash-tile-crests", "aria-hidden": "true", children: /* @__PURE__ */ (0, import_jsx_runtime23.jsx)(Marks, { codes }) }) : null
6055
6108
  ] })
6056
6109
  ) : null
6057
6110
  ] }),
@@ -13513,7 +13566,7 @@ var WIDGET_CSS = `.ss-w-root { all: revert; }
13513
13566
  }
13514
13567
 
13515
13568
  /* greeting dashboard */
13516
- .ss-w-dash { display: flex; flex-direction: column; gap: 12px; padding: 4px 0; }
13569
+ .ss-w-dash { display: flex; flex-direction: column; gap: 12px; padding: 4px 0; container-type: inline-size; }
13517
13570
  .ss-w-dash-head { display: flex; flex-direction: column; }
13518
13571
  .ss-w-dash-title { font-size: 18px; font-weight: 700; line-height: 1.25; color: var(--ss-w-ink); }
13519
13572
  .ss-w-dash-subtitle { font-size: 13px; color: var(--ss-w-faint); margin-top: 4px; }
@@ -13534,8 +13587,8 @@ var WIDGET_CSS = `.ss-w-root { all: revert; }
13534
13587
  .ss-w-dash-tile-icon { flex: none; display: grid; place-items: center; width: 28px; height: 28px; font-size: 22px; }
13535
13588
  .ss-w-dash-emoji { line-height: 1; }
13536
13589
  .ss-w-dash-tile-body { min-width: 0; display: flex; flex-direction: column; gap: 2px; flex: 1; }
13537
- .ss-w-dash-tile-title { min-width: 0; font-weight: 600; font-size: 14px; overflow-wrap: anywhere; }
13538
- .ss-w-dash-tile-subtitle { min-width: 0; font-size: 11.5px; color: var(--ss-w-faint); overflow-wrap: anywhere; }
13590
+ .ss-w-dash-tile-title { min-width: 0; font-weight: 600; font-size: 14px; overflow-wrap: break-word; hyphens: manual; }
13591
+ .ss-w-dash-tile-subtitle { min-width: 0; font-size: 11.5px; color: var(--ss-w-faint); overflow-wrap: break-word; hyphens: manual; }
13539
13592
  .ss-w-dash-tile-chevron { flex: none; margin-left: auto; width: 14px; height: 14px; color: var(--ss-w-faint); }
13540
13593
 
13541
13594
  /* Badge + crests share their own row, below the title -- NOT the icon/chevron row. A
@@ -13557,6 +13610,18 @@ var WIDGET_CSS = `.ss-w-root { all: revert; }
13557
13610
  families (.ss-w-binsight, .ss-w-mocard, .ss-w-omcard) so a market tile reads as
13558
13611
  part of that visual language rather than a plain unstyled button. */
13559
13612
  .ss-w-dash--market .ss-w-dash-tile { border-left: 3px solid var(--ss-w-accent); }
13613
+ /* Market's two columns need the text column to fit a word: a half tile spends ~91px on border, padding,
13614
+ icon and chevron, and a Turkish word such as "kar\u015F\u0131la\u015Fma" is ~80px at 14px bold. Measured against the
13615
+ dashboard's own width (a container query, not the viewport, since the panel is min(460px, 100vw - 40px)
13616
+ and a host can embed it narrower still). Below 306px the half tiles are a single full-width column;
13617
+ up to 349px the decorative chevron is dropped, as Discovery already does, to give the words ~24px back.
13618
+ Words never break inside a tile (overflow-wrap: break-word), so the column count is what protects them. */
13619
+ @container (max-width: 305px) {
13620
+ .ss-w-dash--market .ss-w-dash-grid { grid-template-columns: minmax(0, 1fr); }
13621
+ }
13622
+ @container (min-width: 306px) and (max-width: 349px) {
13623
+ .ss-w-dash--market .ss-w-dash-tile--half .ss-w-dash-tile-chevron { display: none; }
13624
+ }
13560
13625
 
13561
13626
  /* Goal: one column of compact cards, 60px tall with the badge as a price column on the right.
13562
13627
  The value ink is accent mixed toward ink (as on the hero price): plain light accent on the
@@ -13577,7 +13642,8 @@ var WIDGET_CSS = `.ss-w-root { all: revert; }
13577
13642
  /* Discovery: tinted bento cards. Stacked (icon over text) so a two- or three-word title gets
13578
13643
  the tile's full width instead of the ~100px left of a horizontal icon+chevron -- that narrow
13579
13644
  column was wrapping "Most interesting angles" onto three lines. The chevron is dropped (the
13580
- whole card is the tap target) and the badge + crest row is forced onto one line. */
13645
+ whole card is the tap target). The badge + crest row wraps (the base .ss-w-dash-tile-meta rule): an
13646
+ "8 matches" pill plus three crests is wider than a half tile at a 320px viewport. */
13581
13647
  .ss-w-dash--discovery .ss-w-dash-tile { flex-direction: column; align-items: stretch; gap: 8px; }
13582
13648
  .ss-w-dash--discovery .ss-w-dash-tile-icon { align-self: flex-start; width: 32px; height: 32px; font-size: 26px; }
13583
13649
  .ss-w-dash--discovery .ss-w-dash-tile-chevron { display: none; }
@@ -13585,7 +13651,6 @@ var WIDGET_CSS = `.ss-w-root { all: revert; }
13585
13651
  (horizontal, centre-aligned) rather than stacking like the narrow half tiles. */
13586
13652
  .ss-w-dash--discovery .ss-w-dash-tile--full { flex-direction: row; align-items: center; }
13587
13653
  .ss-w-dash--discovery .ss-w-dash-tile--full .ss-w-dash-tile-icon { align-self: center; }
13588
- .ss-w-dash--discovery .ss-w-dash-tile-meta { flex-wrap: nowrap; }
13589
13654
  .ss-w-dash--discovery .ss-w-dash-tile-crest { width: 22px; height: 22px; }
13590
13655
  /* Tints mix a hue into the live panel rather than hardcoding a light pastel, so a dark
13591
13656
  operator surface (e.g. --ss-w-panel set from a navy \`surface\` brand color) yields a
@@ -13638,8 +13703,8 @@ var WIDGET_CSS = `.ss-w-root { all: revert; }
13638
13703
  background: color-mix(in srgb, var(--ss-w-accent) 14%, var(--ss-w-panel));
13639
13704
  }
13640
13705
  .ss-w-dash-hero-body { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 3px; }
13641
- .ss-w-dash-hero-title { font-size: 15.5px; font-weight: 700; line-height: 1.25; color: var(--ss-w-ink); overflow-wrap: anywhere; }
13642
- .ss-w-dash-hero-subtitle { font-size: 12px; color: var(--ss-w-faint); overflow-wrap: anywhere; }
13706
+ .ss-w-dash-hero-title { font-size: 15.5px; font-weight: 700; line-height: 1.25; color: var(--ss-w-ink); overflow-wrap: break-word; hyphens: manual; }
13707
+ .ss-w-dash-hero-subtitle { font-size: 12px; color: var(--ss-w-faint); overflow-wrap: break-word; hyphens: manual; }
13643
13708
  .ss-w-dash-hero-price {
13644
13709
  flex: none; margin-left: auto;
13645
13710
  font-family: ui-monospace, monospace; font-variant-numeric: tabular-nums; font-size: 15px; font-weight: 750; line-height: 1;
@@ -13648,6 +13713,34 @@ var WIDGET_CSS = `.ss-w-root { all: revert; }
13648
13713
  border: 1px solid color-mix(in srgb, var(--ss-w-accent) 30%, var(--ss-w-panel));
13649
13714
  }
13650
13715
 
13716
+ /* Page-grounded additions (u591). Both only exist when the server sends \`meta\` / \`imageCodes\`, so an
13717
+ older payload never reaches these rules. */
13718
+ /* Hero meta ("Tip-off 02:30 \xB7 in 6 h", "Ba\u015Flama 02:30 \xB7 40 dk sonra"): takes the price's seat in the top
13719
+ row. It is the one flexible item there -- the badge and chevron keep their width -- and it wraps
13720
+ rather than truncates: at the narrowest panel the Turkish line (27 characters) has ~120px beside
13721
+ the badge, and the minutes-to-tip are the part a cut-off "40..." would lose. The component keeps
13722
+ each short " \xB7 "-separated part whole, so a wrap falls at the dot instead of inside "40 dk sonra". */
13723
+ .ss-w-dash__keep { white-space: nowrap; }
13724
+ .ss-w-dash__meta {
13725
+ flex: 1 1 auto; min-width: 0; text-align: right; overflow-wrap: break-word; hyphens: manual;
13726
+ font-size: 11.5px; font-weight: 600; line-height: 1.2; font-variant-numeric: tabular-nums;
13727
+ color: var(--ss-w-faint);
13728
+ }
13729
+ /* Text marks stand in for crests until the server has images: 22px circle / 7.5px in a tile, 40px /
13730
+ 12px in the hero (u592 D23). Ring and tint follow the crests they replace; unlike crests they do not
13731
+ overlap, because the letters are the content and an overlap would clip the first one. */
13732
+ .ss-w-dash__mark {
13733
+ flex: none; display: grid; place-items: center; box-sizing: border-box;
13734
+ width: 22px; height: 22px; border-radius: 50%; overflow: hidden;
13735
+ font-size: 7.5px; font-weight: 700; line-height: 1; letter-spacing: .02em; white-space: nowrap;
13736
+ color: var(--ss-w-ink);
13737
+ background-color: color-mix(in srgb, var(--ss-w-ink) 6%, var(--ss-w-panel));
13738
+ box-shadow: 0 0 0 2px var(--ss-w-panel), 0 0 0 3px color-mix(in srgb, var(--ss-w-ink) 10%, var(--ss-w-panel));
13739
+ }
13740
+ .ss-w-dash__mark + .ss-w-dash__mark { margin-left: 3px; }
13741
+ .ss-w-dash-hero-visual .ss-w-dash__mark { width: 40px; height: 40px; font-size: 12px; }
13742
+ .ss-w-dash-hero-visual .ss-w-dash__mark + .ss-w-dash__mark { margin-left: 6px; }
13743
+
13651
13744
  /* club fan pre-match dashboard */
13652
13745
  .ss-w-fandash { display: flex; flex-direction: column; gap: 10px; padding: 4px 0; }
13653
13746
  .ss-w-fandash-greeting { font-size: 14px; font-weight: 600; line-height: 1.3; color: var(--ss-w-ink); }
@@ -14919,7 +15012,7 @@ function StatsWidget(props) {
14919
15012
  introDoneRef.current = true;
14920
15013
  }, []);
14921
15014
  const convStorageKey = `ss-w-conv:${cfg?.operatorId ?? ""}:${cfg?.userContext?.operatorUserId ?? "anon"}`;
14922
- const controller = useChatStream(built?.conversation ?? emptyConversation(), ui.starters, built ? ui.greetingMessage : void 0, convStorageKey);
15015
+ const controller = useChatStream(built?.conversation ?? emptyConversation(), ui.starters, built ? ui.greetingMessage : void 0, convStorageKey, cfg?.page);
14923
15016
  const [hasToday, setHasToday] = (0, import_react74.useState)(false);
14924
15017
  const liveCardWatches = (0, import_react74.useMemo)(() => {
14925
15018
  const seen = /* @__PURE__ */ new Set();