@sensiblestats/widget-react 0.3.0 → 0.4.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,11 @@
1
1
  # @sensiblestats/widget-react
2
2
 
3
+ ## 0.4.0
4
+
5
+ ### Minor Changes
6
+
7
+ - Render `betting_insight` events as insight cards (market, selection + line, odds with an optional live badge, supporting stats, and the responsible-gambling disclaimer). **Bug fix:** the reducer previously had no case for `betting_insight` and no fallthrough, so any unmodelled event returned `undefined` and corrupted state — the next event then crashed the widget (`Cannot read properties of undefined (reading 'length')`). Unknown event types are now ignored safely and can never break the UI.
8
+
3
9
  ## 0.3.0
4
10
 
5
11
  ### Minor Changes
package/dist/index.cjs CHANGED
@@ -132,16 +132,19 @@ function str(v) {
132
132
  function num(v) {
133
133
  return typeof v === "number" && Number.isFinite(v) ? String(v) : void 0;
134
134
  }
135
- function readStats(card) {
136
- if (!Array.isArray(card.stats)) return [];
135
+ function readStatCells(raw) {
136
+ if (!Array.isArray(raw)) return [];
137
137
  const cells = [];
138
- for (const entry of card.stats) {
138
+ for (const entry of raw) {
139
139
  const label = str(entry.label);
140
140
  const value = num(entry.value) ?? str(entry.value);
141
141
  if (label !== void 0 && value !== void 0) cells.push({ label, value });
142
142
  }
143
143
  return cells;
144
144
  }
145
+ function readStats(card) {
146
+ return readStatCells(card.stats);
147
+ }
145
148
  function toCardVM(card) {
146
149
  const entityType = str(card.entityType)?.toLowerCase();
147
150
  const kind = entityType === "player" ? "player" : entityType === "team" ? "team" : "generic";
@@ -167,6 +170,24 @@ function toChipVM(intent) {
167
170
  function chipFromText(text) {
168
171
  return { text };
169
172
  }
173
+ function toInsightVM(insight) {
174
+ const market = str(insight.marketLabel) ?? str(insight.marketDeveloperName) ?? "";
175
+ const selection = [str(insight.selectionLabel), str(insight.line)].filter(Boolean).join(" ") || void 0;
176
+ const value = insight.odds?.value;
177
+ const odds = typeof value === "number" && Number.isFinite(value) ? value.toFixed(2) : void 0;
178
+ return {
179
+ id: str(insight.insightId) ?? `${str(insight.ruleId) ?? "insight"}:${num(insight.matchId) ?? ""}`,
180
+ market,
181
+ selection,
182
+ odds,
183
+ // The wire payload carries bookmakerId (not a name); render a name only if one is present.
184
+ bookmaker: str(insight.odds?.bookmakerName),
185
+ isLive: insight.odds?.isLive === true,
186
+ text: str(insight.text) ?? "",
187
+ stats: readStatCells(insight.stats),
188
+ disclaimer: str(insight.disclaimer)
189
+ };
190
+ }
170
191
 
171
192
  // src/reducer.ts
172
193
  var POPUP_MAX = 160;
@@ -193,6 +214,8 @@ function applyEvent(state, event) {
193
214
  return { ...state, messages: mapActive(state, (t) => ({ ...t, cards: [...t.cards, toCardVM(event.data.card)] })) };
194
215
  case "action_button":
195
216
  return { ...state, actionsLoading: false, messages: mapActive(state, (t) => ({ ...t, actions: [...t.actions, toActionVM(event.data.action)] })) };
217
+ case "betting_insight":
218
+ return { ...state, messages: mapActive(state, (t) => ({ ...t, insights: [...t.insights, toInsightVM(event.data.insight)] })) };
196
219
  case "intent_chip": {
197
220
  const chip = toChipVM(event.data.intent);
198
221
  return chip ? { ...state, starters: [...state.starters, chip] } : state;
@@ -210,6 +233,7 @@ function applyEvent(state, event) {
210
233
  return { ...state, error: suppress ? state.error : event.data, isStreaming: false, status: null, actionsLoading: false, messages: mapActive(state, (t) => ({ ...t, done: true })) };
211
234
  }
212
235
  }
236
+ return state;
213
237
  }
214
238
  function reducer(state, action) {
215
239
  switch (action.kind) {
@@ -219,11 +243,11 @@ function reducer(state, action) {
219
243
  return { ...state, isOpen: false, isStreaming: false, status: null, actionsLoading: false };
220
244
  case "USER_SENT": {
221
245
  const user = { role: "user", id: action.userId, text: action.displayText ?? action.input.message };
222
- const assistant = { role: "assistant", id: action.assistantId, text: "", cards: [], actions: [], done: false };
246
+ const assistant = { role: "assistant", id: action.assistantId, text: "", cards: [], actions: [], insights: [], done: false };
223
247
  return { ...state, messages: [...state.messages, user, assistant], isStreaming: true, status: null, actionsLoading: false, starters: [], error: null, lastUserInput: action.input, lastDisplayText: action.displayText ?? null };
224
248
  }
225
249
  case "GREETING_SENT": {
226
- const assistant = { role: "assistant", id: action.assistantId, text: "", cards: [], actions: [], done: false, isGreeting: true };
250
+ const assistant = { role: "assistant", id: action.assistantId, text: "", cards: [], actions: [], insights: [], done: false, isGreeting: true };
227
251
  return { ...state, messages: [...state.messages, assistant], isStreaming: true, status: null, error: null };
228
252
  }
229
253
  case "DISMISS_POPUP":
@@ -411,37 +435,63 @@ function EntityCardList({ cards, disabled, showLessLabel = "Show less", showMore
411
435
  ] });
412
436
  }
413
437
 
414
- // src/components/ActionButtons.tsx
438
+ // src/components/BettingInsightList.tsx
415
439
  var import_jsx_runtime7 = require("react/jsx-runtime");
440
+ function BettingInsightList({ insights }) {
441
+ if (!insights || insights.length === 0) return null;
442
+ return /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("div", { className: "ss-w-binsights", children: insights.map((it) => /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("div", { className: "ss-w-binsight", children: [
443
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("div", { className: "ss-w-bi-head", children: [
444
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("span", { className: "ss-w-bi-mkt", children: [
445
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("span", { className: "ss-w-bi-market", children: it.market }),
446
+ it.selection ? /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("span", { className: "ss-w-bi-sel", children: it.selection }) : null
447
+ ] }),
448
+ it.odds ? /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("span", { className: "ss-w-bi-odds", children: [
449
+ it.isLive ? /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("span", { className: "ss-w-bi-live", children: "Live" }) : null,
450
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("b", { children: it.odds }),
451
+ it.bookmaker ? /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("span", { className: "ss-w-bi-book", children: it.bookmaker }) : null
452
+ ] }) : null
453
+ ] }),
454
+ it.text ? /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("p", { className: "ss-w-bi-text", children: it.text }) : null,
455
+ it.stats.length > 0 ? /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("div", { className: "ss-w-bi-stats", children: it.stats.map((s) => /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("span", { className: "ss-w-bi-stat", children: [
456
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("b", { children: s.value }),
457
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("span", { children: s.label })
458
+ ] }, `${s.label}:${s.value}`)) }) : null,
459
+ it.disclaimer ? /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("p", { className: "ss-w-bi-disc", children: it.disclaimer }) : null
460
+ ] }, it.id)) });
461
+ }
462
+
463
+ // src/components/ActionButtons.tsx
464
+ var import_jsx_runtime8 = require("react/jsx-runtime");
416
465
  function ActionButtons({ actions, loading, disabled, onAct }) {
417
466
  if (actions.length === 0 && !loading) return null;
418
- return /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("div", { className: "ss-w-actions", children: [
419
- actions.map((a, i) => /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("button", { type: "button", className: "ss-w-abtn", disabled, onClick: () => onAct(a), children: a.label }, `${a.label}-${i}`)),
420
- actions.length === 0 && loading ? /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(import_jsx_runtime7.Fragment, { children: [
421
- /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("span", { className: "ss-w-abtn-shimmer" }),
422
- /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("span", { className: "ss-w-abtn-shimmer" })
467
+ return /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("div", { className: "ss-w-actions", children: [
468
+ actions.map((a, i) => /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("button", { type: "button", className: "ss-w-abtn", disabled, onClick: () => onAct(a), children: a.label }, `${a.label}-${i}`)),
469
+ actions.length === 0 && loading ? /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)(import_jsx_runtime8.Fragment, { children: [
470
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("span", { className: "ss-w-abtn-shimmer" }),
471
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("span", { className: "ss-w-abtn-shimmer" })
423
472
  ] }) : null
424
473
  ] });
425
474
  }
426
475
 
427
476
  // src/components/ChatMessage.tsx
428
- var import_jsx_runtime8 = require("react/jsx-runtime");
477
+ var import_jsx_runtime9 = require("react/jsx-runtime");
429
478
  function ChatMessage({ message, disabled, onAct, ui }) {
430
479
  if (message.role === "user") {
431
- return /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("div", { className: "ss-w-msg ss-w-user", children: /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("div", { className: "ss-w-bubble", children: message.text }) });
480
+ return /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("div", { className: "ss-w-msg ss-w-user", children: /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("div", { className: "ss-w-bubble", children: message.text }) });
432
481
  }
433
- return /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("div", { className: "ss-w-msg ss-w-bot", children: [
434
- message.text ? /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(Markdown, { text: message.text }) : null,
435
- /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(EntityCardList, { cards: message.cards, disabled, showLessLabel: ui?.showLess, showMoreLabel: ui?.showMore }),
436
- /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(ActionButtons, { actions: message.actions, loading: false, disabled, onAct })
482
+ return /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("div", { className: "ss-w-msg ss-w-bot", children: [
483
+ message.text ? /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(Markdown, { text: message.text }) : null,
484
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(EntityCardList, { cards: message.cards, disabled, showLessLabel: ui?.showLess, showMoreLabel: ui?.showMore }),
485
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(BettingInsightList, { insights: message.insights }),
486
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(ActionButtons, { actions: message.actions, loading: false, disabled, onAct })
437
487
  ] });
438
488
  }
439
489
 
440
490
  // src/components/IntentChips.tsx
441
- var import_jsx_runtime9 = require("react/jsx-runtime");
491
+ var import_jsx_runtime10 = require("react/jsx-runtime");
442
492
  function IntentChips({ chips, onPick }) {
443
493
  if (chips.length === 0) return null;
444
- return /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("div", { className: "ss-w-chips", children: chips.map((c, i) => /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("button", { type: "button", className: "ss-w-chip", onClick: () => onPick(c.text), children: c.text }, `${c.text}-${i}`)) });
494
+ return /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("div", { className: "ss-w-chips", children: chips.map((c, i) => /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("button", { type: "button", className: "ss-w-chip", onClick: () => onPick(c.text), children: c.text }, `${c.text}-${i}`)) });
445
495
  }
446
496
 
447
497
  // src/scroll.ts
@@ -451,7 +501,7 @@ function isPinnedToBottom(el, threshold = PIN_THRESHOLD) {
451
501
  }
452
502
 
453
503
  // src/components/MessageList.tsx
454
- var import_jsx_runtime10 = require("react/jsx-runtime");
504
+ var import_jsx_runtime11 = require("react/jsx-runtime");
455
505
  function MessageList({ controller, ui }) {
456
506
  const { state, send } = controller;
457
507
  const listRef = (0, import_react3.useRef)(null);
@@ -461,7 +511,7 @@ function MessageList({ controller, ui }) {
461
511
  if (pinnedRef.current) endRef.current?.scrollIntoView({ block: "end" });
462
512
  }, [state.messages, state.status]);
463
513
  const empty = state.messages.length === 0;
464
- return /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)(
514
+ return /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)(
465
515
  "div",
466
516
  {
467
517
  className: "ss-w-list",
@@ -471,7 +521,7 @@ function MessageList({ controller, ui }) {
471
521
  if (listRef.current) pinnedRef.current = isPinnedToBottom(listRef.current);
472
522
  },
473
523
  children: [
474
- state.messages.map((m) => /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(
524
+ state.messages.map((m) => /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(
475
525
  ChatMessage,
476
526
  {
477
527
  message: m,
@@ -481,15 +531,15 @@ function MessageList({ controller, ui }) {
481
531
  },
482
532
  m.id
483
533
  )),
484
- state.isStreaming && state.actionsLoading ? /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(ActionButtons, { actions: [], loading: true, onAct: () => {
534
+ state.isStreaming && state.actionsLoading ? /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(ActionButtons, { actions: [], loading: true, onAct: () => {
485
535
  } }) : null,
486
- state.status ? /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("div", { className: "ss-w-status", children: state.status }) : null,
487
- state.error ? /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)("div", { className: "ss-w-error", role: "alert", children: [
488
- /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("span", { children: "message" in state.error ? state.error.message : ui.somethingWentWrong }),
489
- /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("button", { type: "button", className: "ss-w-retry", onClick: () => controller.retry(), children: ui.retry })
536
+ state.status ? /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("div", { className: "ss-w-status", children: state.status }) : null,
537
+ state.error ? /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)("div", { className: "ss-w-error", role: "alert", children: [
538
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("span", { children: "message" in state.error ? state.error.message : ui.somethingWentWrong }),
539
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("button", { type: "button", className: "ss-w-retry", onClick: () => controller.retry(), children: ui.retry })
490
540
  ] }) : null,
491
- empty ? /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(IntentChips, { chips: state.starters, onPick: (t) => send(t) }) : null,
492
- /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("div", { ref: endRef })
541
+ empty ? /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(IntentChips, { chips: state.starters, onPick: (t) => send(t) }) : null,
542
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("div", { ref: endRef })
493
543
  ]
494
544
  }
495
545
  );
@@ -497,7 +547,7 @@ function MessageList({ controller, ui }) {
497
547
 
498
548
  // src/components/ChatInput.tsx
499
549
  var import_react4 = require("react");
500
- var import_jsx_runtime11 = require("react/jsx-runtime");
550
+ var import_jsx_runtime12 = require("react/jsx-runtime");
501
551
  function ChatInput({ placeholder, streaming, onSend, onStop, stopLabel = "Stop response", sendLabel = "Send message" }) {
502
552
  const [value, setValue] = (0, import_react4.useState)("");
503
553
  const submit = () => {
@@ -512,14 +562,14 @@ function ChatInput({ placeholder, streaming, onSend, onStop, stopLabel = "Stop r
512
562
  submit();
513
563
  }
514
564
  };
515
- return /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)("div", { className: "ss-w-input", children: [
516
- /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("textarea", { className: "ss-w-textarea", rows: 1, placeholder, value, disabled: streaming, onChange: (e) => setValue(e.target.value), onKeyDown }),
517
- streaming ? /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("button", { type: "button", className: "ss-w-send ss-w-stop", "aria-label": stopLabel, onClick: onStop, children: /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("span", { className: "ss-w-stop-glyph" }) }) : /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("button", { type: "button", className: "ss-w-send", "aria-label": sendLabel, onClick: submit, children: /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(SendIcon, {}) })
565
+ return /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)("div", { className: "ss-w-input", children: [
566
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("textarea", { className: "ss-w-textarea", rows: 1, placeholder, value, disabled: streaming, onChange: (e) => setValue(e.target.value), onKeyDown }),
567
+ streaming ? /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("button", { type: "button", className: "ss-w-send ss-w-stop", "aria-label": stopLabel, onClick: onStop, children: /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("span", { className: "ss-w-stop-glyph" }) }) : /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("button", { type: "button", className: "ss-w-send", "aria-label": sendLabel, onClick: submit, children: /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(SendIcon, {}) })
518
568
  ] });
519
569
  }
520
570
 
521
571
  // src/components/ChatPanel.tsx
522
- var import_jsx_runtime12 = require("react/jsx-runtime");
572
+ var import_jsx_runtime13 = require("react/jsx-runtime");
523
573
  var FULLSCREEN_QUERY = "(max-width: 639px)";
524
574
  function useIsModal() {
525
575
  const [modal, setModal] = (0, import_react5.useState)(false);
@@ -543,10 +593,10 @@ function ChatPanel({ controller, ui }) {
543
593
  window.addEventListener("keydown", onKey);
544
594
  return () => window.removeEventListener("keydown", onKey);
545
595
  }, [close]);
546
- return /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)("div", { className: "ss-w-panel", role: "dialog", "aria-modal": isModal, "aria-label": ui.chatDialogLabel, children: [
547
- /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(ChatHeader, { onClose: close, closeLabel: ui.closeChat }),
548
- /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(MessageList, { controller, ui }),
549
- /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(ChatInput, { placeholder: ui.placeholder, streaming: state.isStreaming, onSend: (t) => send(t), onStop: stop, stopLabel: ui.stopResponse, sendLabel: ui.sendMessage })
596
+ return /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)("div", { className: "ss-w-panel", role: "dialog", "aria-modal": isModal, "aria-label": ui.chatDialogLabel, children: [
597
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(ChatHeader, { onClose: close, closeLabel: ui.closeChat }),
598
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(MessageList, { controller, ui }),
599
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(ChatInput, { placeholder: ui.placeholder, streaming: state.isStreaming, onSend: (t) => send(t), onStop: stop, stopLabel: ui.stopResponse, sendLabel: ui.sendMessage })
550
600
  ] });
551
601
  }
552
602
 
@@ -645,6 +695,23 @@ var WIDGET_CSS = `.ss-w-root { all: revert; }
645
695
  .ss-w-cards-more:hover:not(:disabled) { text-decoration: underline; }
646
696
  .ss-w-cards-more:disabled { opacity: .5; cursor: default; }
647
697
 
698
+ /* betting insights */
699
+ .ss-w-binsights { display: flex; flex-direction: column; gap: 7px; }
700
+ .ss-w-binsight { background: var(--ss-w-panel); border: 1px solid var(--ss-w-line); border-radius: 12px; border-left: 3px solid var(--ss-w-accent); padding: 9px 11px; display: flex; flex-direction: column; gap: 6px; }
701
+ .ss-w-bi-head { display: flex; align-items: flex-start; gap: 10px; }
702
+ .ss-w-bi-mkt { display: flex; flex-direction: column; min-width: 0; }
703
+ .ss-w-bi-market { font-size: 8.5px; font-weight: 700; letter-spacing: .05em; text-transform: uppercase; color: var(--ss-w-faint); line-height: 1.3; }
704
+ .ss-w-bi-sel { font-weight: 660; }
705
+ .ss-w-bi-odds { margin-left: auto; display: flex; align-items: center; gap: 6px; flex: none; }
706
+ .ss-w-bi-odds b { font-size: 15px; font-variant-numeric: tabular-nums; color: var(--ss-w-accent); }
707
+ .ss-w-bi-book { font-size: 10px; color: var(--ss-w-faint); }
708
+ .ss-w-bi-live { font-size: 8.5px; font-weight: 700; letter-spacing: .04em; text-transform: uppercase; color: #fff; background: #c0392b; border-radius: 4px; padding: 1px 5px; }
709
+ .ss-w-bi-text { font-size: 12px; color: var(--ss-w-ink); }
710
+ .ss-w-bi-stats { display: flex; flex-wrap: wrap; gap: 12px; }
711
+ .ss-w-bi-stat b { display: block; font-variant-numeric: tabular-nums; font-weight: 660; }
712
+ .ss-w-bi-stat span { font-size: 9px; text-transform: uppercase; color: var(--ss-w-faint); }
713
+ .ss-w-bi-disc { font-size: 10px; font-style: italic; color: var(--ss-w-faint); }
714
+
648
715
  /* action buttons */
649
716
  .ss-w-actions { display: flex; flex-direction: column; align-items: flex-start; gap: 6px; }
650
717
  .ss-w-abtn { font-size: 12px; font-weight: 560; color: var(--ss-w-accent); background: var(--ss-w-panel); border: 1px solid var(--ss-w-accent); border-radius: 16px; padding: 6px 12px; cursor: pointer; }
@@ -674,7 +741,7 @@ var WIDGET_CSS = `.ss-w-root { all: revert; }
674
741
  `;
675
742
 
676
743
  // src/StatsWidget.tsx
677
- var import_jsx_runtime13 = require("react/jsx-runtime");
744
+ var import_jsx_runtime14 = require("react/jsx-runtime");
678
745
  function buildClient(props) {
679
746
  try {
680
747
  if ("client" in props && props.client) {
@@ -710,7 +777,7 @@ function StatsWidget(props) {
710
777
  }, [built, ui.injectStyles]);
711
778
  if (!built) return null;
712
779
  const attrs = themeAttrs(ui);
713
- return /* @__PURE__ */ (0, import_jsx_runtime13.jsx)("div", { ref: rootRef, className: "ss-w-root", "data-ss-w-theme": attrs["data-ss-w-theme"], "data-ss-w-pos": attrs["data-ss-w-pos"], style: attrs.style, children: controller.state.isOpen ? /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(ChatPanel, { controller, ui }) : /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(
780
+ return /* @__PURE__ */ (0, import_jsx_runtime14.jsx)("div", { ref: rootRef, className: "ss-w-root", "data-ss-w-theme": attrs["data-ss-w-theme"], "data-ss-w-pos": attrs["data-ss-w-pos"], style: attrs.style, children: controller.state.isOpen ? /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(ChatPanel, { controller, ui }) : /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(
714
781
  ChatFab,
715
782
  {
716
783
  label: ui.launcherLabel,