@sensiblestats/widget-react 0.13.1 → 0.14.1

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,27 @@
1
1
  # @sensiblestats/widget-react
2
2
 
3
+ ## 0.14.1
4
+
5
+ ### Patch Changes
6
+
7
+ - 5dce658: Greeting dashboard UX refinements from design review: tile icons are now
8
+ emoji (matching the betting mockup) rendered at a larger size instead of the
9
+ small monochrome line glyphs; the greeting bubble text is suppressed when a
10
+ dashboard is present so the question shows once (as the dashboard title)
11
+ rather than twice; and the discovery full-span trends tile aligns its icon
12
+ and title on a single row.
13
+
14
+ ## 0.14.0
15
+
16
+ ### Minor Changes
17
+
18
+ - Render the greeting dashboard carried on `turn_complete`: tile grid, CSS, and tap handling that turns a tile into a follow-up chat message. Older servers that never send `dashboard` render nothing new. `widget-embed` also now generates and persists a first-party anonymous visitor id (localStorage, falls back to in-memory) so greeting dashboards can be tiered for anonymous visitors.
19
+
20
+ ### Patch Changes
21
+
22
+ - Updated dependencies
23
+ - @sensiblestats/widget-sdk@0.12.0
24
+
3
25
  ## 0.13.1
4
26
 
5
27
  ### Patch Changes
package/dist/index.cjs CHANGED
@@ -37,7 +37,7 @@ module.exports = __toCommonJS(index_exports);
37
37
 
38
38
  // src/StatsWidget.tsx
39
39
  var import_react10 = require("react");
40
- var import_widget_sdk4 = require("@sensiblestats/widget-sdk");
40
+ var import_widget_sdk5 = require("@sensiblestats/widget-sdk");
41
41
 
42
42
  // src/i18n.ts
43
43
  var STRINGS = {
@@ -205,6 +205,27 @@ function toUserContext(cfg) {
205
205
  return cfg.userContext && Object.keys(cfg.userContext).length > 0 ? cfg.userContext : void 0;
206
206
  }
207
207
 
208
+ // src/dashboardTap.ts
209
+ var import_widget_sdk = require("@sensiblestats/widget-sdk");
210
+ function isSignalEntityType(value) {
211
+ return Object.prototype.hasOwnProperty.call(import_widget_sdk.ENTITY_TYPE_CODES, value);
212
+ }
213
+ function createDashboardTapSignal(sendSignal) {
214
+ return (tile, index, layout) => {
215
+ if (!isSignalEntityType(tile.signal.entityType)) return;
216
+ try {
217
+ void sendSignal({
218
+ signalType: "MarketView",
219
+ entityType: tile.signal.entityType,
220
+ entityId: tile.signal.entityId,
221
+ dashboardLayout: layout,
222
+ tileIndex: index
223
+ }).catch(() => void 0);
224
+ } catch {
225
+ }
226
+ };
227
+ }
228
+
208
229
  // src/theme.ts
209
230
  function themeAttrs(ui) {
210
231
  const style = {
@@ -218,10 +239,10 @@ function themeAttrs(ui) {
218
239
 
219
240
  // src/useChatStream.ts
220
241
  var import_react = require("react");
221
- var import_widget_sdk2 = require("@sensiblestats/widget-sdk");
242
+ var import_widget_sdk3 = require("@sensiblestats/widget-sdk");
222
243
 
223
244
  // src/reducer.ts
224
- var import_widget_sdk = require("@sensiblestats/widget-sdk");
245
+ var import_widget_sdk2 = require("@sensiblestats/widget-sdk");
225
246
 
226
247
  // src/chart.ts
227
248
  var CHART_VIEW_W = 320;
@@ -507,7 +528,11 @@ function applyEvent(state, event) {
507
528
  actionsLoading: false,
508
529
  popupText,
509
530
  conversationId: event.data.conversationId ?? state.conversationId,
510
- messages: mapActive(state, (t) => ({ ...t, done: true }))
531
+ messages: mapActive(state, (t) => ({
532
+ ...t,
533
+ done: true,
534
+ greetingDashboard: event.data.dashboard ?? void 0
535
+ }))
511
536
  };
512
537
  }
513
538
  case "error": {
@@ -559,7 +584,7 @@ function useChatStream(conversation, starterTexts, greetingMessage) {
559
584
  try {
560
585
  for await (const event of handle) dispatch({ kind: "EVENT", event });
561
586
  } catch (err) {
562
- if (err instanceof import_widget_sdk2.WidgetSdkError) onError(err);
587
+ if (err instanceof import_widget_sdk3.WidgetSdkError) onError(err);
563
588
  else if (err?.name === "AbortError") {
564
589
  } else throw err;
565
590
  } finally {
@@ -630,6 +655,28 @@ function SendIcon() {
630
655
  function CloseIcon() {
631
656
  return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("svg", { viewBox: "0 0 24 24", width: "1em", height: "1em", "aria-hidden": "true", children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("path", { fill: "none", d: "M18 6L6 18M6 6l12 12", stroke: "currentColor", strokeWidth: "2" }) });
632
657
  }
658
+ function ChevronIcon() {
659
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("svg", { viewBox: "0 0 24 24", width: "1em", height: "1em", "aria-hidden": "true", children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("path", { fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", d: "M9 5l7 7-7 7" }) });
660
+ }
661
+ var DASHBOARD_EMOJI = {
662
+ football: "\u26BD",
663
+ "goal-net": "\u{1F945}",
664
+ btts: "\u{1F91D}",
665
+ card: "\u{1F7E8}",
666
+ "corner-flag": "\u{1F6A9}",
667
+ player: "\u{1F464}",
668
+ target: "\u{1F3AF}",
669
+ trophy: "\u{1F3C6}",
670
+ "chart-line": "\u{1F4C8}",
671
+ "chart-bar": "\u{1F4CA}",
672
+ chat: "\u{1F4AC}",
673
+ fire: "\u{1F525}",
674
+ sparkle: "\u2728"
675
+ };
676
+ function DashboardIcon({ name }) {
677
+ const glyph = Object.prototype.hasOwnProperty.call(DASHBOARD_EMOJI, name) ? DASHBOARD_EMOJI[name] : DASHBOARD_EMOJI.target;
678
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "ss-w-dash-emoji", "aria-hidden": "true", children: glyph });
679
+ }
633
680
 
634
681
  // src/components/GreetingBubble.tsx
635
682
  var import_jsx_runtime2 = require("react/jsx-runtime");
@@ -788,7 +835,7 @@ var import_react5 = require("react");
788
835
 
789
836
  // src/components/AddToSlipButton.tsx
790
837
  var import_react4 = require("react");
791
- var import_widget_sdk3 = require("@sensiblestats/widget-sdk");
838
+ var import_widget_sdk4 = require("@sensiblestats/widget-sdk");
792
839
 
793
840
  // src/slip.ts
794
841
  var ADD_TO_SLIP_TIMEOUT_MS = 1e4;
@@ -851,7 +898,7 @@ function SlipButton({ selection, shownOdds, marketName, slip, ui }) {
851
898
  setPending(true);
852
899
  try {
853
900
  const raw = await withTimeout(Promise.resolve(handler(selection)));
854
- if (raw != null && !(0, import_widget_sdk3.isAddToSlipResult)(raw)) throw new Error("malformed add-to-slip result");
901
+ if (raw != null && !(0, import_widget_sdk4.isAddToSlipResult)(raw)) throw new Error("malformed add-to-slip result");
855
902
  const result = raw ?? void 0;
856
903
  setToast(resultToToast(result, shownOdds || selection.oddsDecimal.toFixed(2), ui));
857
904
  setSettled(isTerminal(result));
@@ -1101,27 +1148,75 @@ function ActionButtons({ actions, loading, disabled, onAct }) {
1101
1148
  ] });
1102
1149
  }
1103
1150
 
1104
- // src/components/ChatMessage.tsx
1151
+ // src/components/dashboard/DashboardTile.tsx
1105
1152
  var import_jsx_runtime14 = require("react/jsx-runtime");
1106
- function ChatMessage({ message, disabled, onAct, ui, slip }) {
1153
+ var MAX_CRESTS = 3;
1154
+ function DashboardTile({ tile, onTap }) {
1155
+ const spanClass = tile.span === "full" ? "ss-w-dash-tile--full" : "ss-w-dash-tile--half";
1156
+ const crests = (tile.imageUrls ?? []).slice(0, MAX_CRESTS);
1157
+ const hasMeta = Boolean(tile.badgeText) || crests.length > 0;
1158
+ return /* @__PURE__ */ (0, import_jsx_runtime14.jsxs)("button", { type: "button", className: `ss-w-dash-tile ${spanClass}`, onClick: onTap, children: [
1159
+ /* @__PURE__ */ (0, import_jsx_runtime14.jsx)("span", { className: "ss-w-dash-tile-icon", children: /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(DashboardIcon, { name: tile.icon }) }),
1160
+ /* @__PURE__ */ (0, import_jsx_runtime14.jsxs)("span", { className: "ss-w-dash-tile-body", children: [
1161
+ /* @__PURE__ */ (0, import_jsx_runtime14.jsx)("span", { className: "ss-w-dash-tile-title", children: tile.title }),
1162
+ tile.subtitle ? /* @__PURE__ */ (0, import_jsx_runtime14.jsx)("span", { className: "ss-w-dash-tile-subtitle", children: tile.subtitle }) : null,
1163
+ hasMeta ? (
1164
+ // Badge + crests sit on their own row, below the title, instead of sharing the
1165
+ // icon/chevron row -- at the half-span tile's ~180px width a nowrap badge alone
1166
+ // (~70px) left the title column at ~22px and forced character-by-character wrapping.
1167
+ /* @__PURE__ */ (0, import_jsx_runtime14.jsxs)("span", { className: "ss-w-dash-tile-meta", children: [
1168
+ tile.badgeText ? /* @__PURE__ */ (0, import_jsx_runtime14.jsx)("span", { className: "ss-w-dash-tile-badge", children: tile.badgeText }) : null,
1169
+ crests.length > 0 ? /* @__PURE__ */ (0, import_jsx_runtime14.jsx)("span", { className: "ss-w-dash-tile-crests", children: crests.map((url, i) => /* @__PURE__ */ (0, import_jsx_runtime14.jsx)("span", { className: "ss-w-dash-tile-crest", style: { backgroundImage: `url(${url})` }, "aria-hidden": "true" }, `${url}-${i}`)) }) : null
1170
+ ] })
1171
+ ) : null
1172
+ ] }),
1173
+ /* @__PURE__ */ (0, import_jsx_runtime14.jsx)("span", { className: "ss-w-dash-tile-chevron", children: /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(ChevronIcon, {}) })
1174
+ ] });
1175
+ }
1176
+
1177
+ // src/components/dashboard/GreetingDashboard.tsx
1178
+ var import_jsx_runtime15 = require("react/jsx-runtime");
1179
+ var LAYOUT_CLASSES = {
1180
+ market: "ss-w-dash--market",
1181
+ goal: "ss-w-dash--goal",
1182
+ discovery: "ss-w-dash--discovery"
1183
+ };
1184
+ function layoutClass(layout) {
1185
+ return Object.prototype.hasOwnProperty.call(LAYOUT_CLASSES, layout) ? LAYOUT_CLASSES[layout] : LAYOUT_CLASSES.goal;
1186
+ }
1187
+ function GreetingDashboard({ dashboard, onTap }) {
1188
+ return /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)("div", { className: `ss-w-dash ${layoutClass(dashboard.layout)}`, children: [
1189
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)("div", { className: "ss-w-dash-head", children: [
1190
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("span", { className: "ss-w-dash-title", children: dashboard.title }),
1191
+ dashboard.subtitle ? /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("span", { className: "ss-w-dash-subtitle", children: dashboard.subtitle }) : null
1192
+ ] }),
1193
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("div", { className: "ss-w-dash-grid", children: dashboard.tiles.map((t, i) => /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(DashboardTile, { tile: t, onTap: () => onTap(t, i) }, `${t.title}-${i}`)) }),
1194
+ dashboard.footerTip ? /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("span", { className: "ss-w-dash-foot", children: dashboard.footerTip }) : null
1195
+ ] });
1196
+ }
1197
+
1198
+ // src/components/ChatMessage.tsx
1199
+ var import_jsx_runtime16 = require("react/jsx-runtime");
1200
+ function ChatMessage({ message, disabled, onAct, onTap, ui, slip }) {
1107
1201
  if (message.role === "user") {
1108
- return /* @__PURE__ */ (0, import_jsx_runtime14.jsx)("div", { className: "ss-w-msg ss-w-user", children: /* @__PURE__ */ (0, import_jsx_runtime14.jsx)("div", { className: "ss-w-bubble", children: message.text }) });
1202
+ return /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("div", { className: "ss-w-msg ss-w-user", children: /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("div", { className: "ss-w-bubble", children: message.text }) });
1109
1203
  }
1110
- return /* @__PURE__ */ (0, import_jsx_runtime14.jsxs)("div", { className: "ss-w-msg ss-w-bot", children: [
1111
- message.text ? /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(Markdown, { text: message.text }) : null,
1112
- /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(EntityCardList, { cards: message.cards, disabled, showLessLabel: ui?.showLess, showMoreLabel: ui?.showMore }),
1113
- /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(BettingInsightList, { insights: message.insights, ui, slip }),
1114
- /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(MarketOddsCardList, { cards: message.marketOdds, ui, slip }),
1115
- /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(OddsMovementCardList, { cards: message.oddsMovement, ui }),
1116
- /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(ActionButtons, { actions: message.actions, loading: false, disabled, onAct })
1204
+ return /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)("div", { className: "ss-w-msg ss-w-bot", children: [
1205
+ message.text && !message.greetingDashboard ? /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(Markdown, { text: message.text }) : null,
1206
+ message.greetingDashboard ? /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(GreetingDashboard, { dashboard: message.greetingDashboard, onTap }) : null,
1207
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(EntityCardList, { cards: message.cards, disabled, showLessLabel: ui?.showLess, showMoreLabel: ui?.showMore }),
1208
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(BettingInsightList, { insights: message.insights, ui, slip }),
1209
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(MarketOddsCardList, { cards: message.marketOdds, ui, slip }),
1210
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(OddsMovementCardList, { cards: message.oddsMovement, ui }),
1211
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(ActionButtons, { actions: message.actions, loading: false, disabled, onAct })
1117
1212
  ] });
1118
1213
  }
1119
1214
 
1120
1215
  // src/components/IntentChips.tsx
1121
- var import_jsx_runtime15 = require("react/jsx-runtime");
1216
+ var import_jsx_runtime17 = require("react/jsx-runtime");
1122
1217
  function IntentChips({ chips, onPick }) {
1123
1218
  if (chips.length === 0) return null;
1124
- return /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("div", { className: "ss-w-chips", children: chips.map((c, i) => /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("button", { type: "button", className: "ss-w-chip", onClick: () => onPick(c.text), children: c.text }, `${c.text}-${i}`)) });
1219
+ return /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("div", { className: "ss-w-chips", children: chips.map((c, i) => /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("button", { type: "button", className: "ss-w-chip", onClick: () => onPick(c.text), children: c.text }, `${c.text}-${i}`)) });
1125
1220
  }
1126
1221
 
1127
1222
  // src/scroll.ts
@@ -1131,8 +1226,8 @@ function isPinnedToBottom(el, threshold = PIN_THRESHOLD) {
1131
1226
  }
1132
1227
 
1133
1228
  // src/components/MessageList.tsx
1134
- var import_jsx_runtime16 = require("react/jsx-runtime");
1135
- function MessageList({ controller, ui, slip }) {
1229
+ var import_jsx_runtime18 = require("react/jsx-runtime");
1230
+ function MessageList({ controller, ui, slip, onDashboardTap }) {
1136
1231
  const { state, send } = controller;
1137
1232
  const listRef = (0, import_react7.useRef)(null);
1138
1233
  const endRef = (0, import_react7.useRef)(null);
@@ -1140,8 +1235,9 @@ function MessageList({ controller, ui, slip }) {
1140
1235
  (0, import_react7.useEffect)(() => {
1141
1236
  if (pinnedRef.current) endRef.current?.scrollIntoView({ block: "end" });
1142
1237
  }, [state.messages, state.status]);
1143
- const empty = state.messages.length === 0;
1144
- return /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)(
1238
+ const hasUserMessage = state.messages.some((m) => m.role === "user");
1239
+ const hasDashboard = state.messages.some((m) => m.role === "assistant" && m.greetingDashboard);
1240
+ return /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)(
1145
1241
  "div",
1146
1242
  {
1147
1243
  className: "ss-w-list",
@@ -1151,26 +1247,30 @@ function MessageList({ controller, ui, slip }) {
1151
1247
  if (listRef.current) pinnedRef.current = isPinnedToBottom(listRef.current);
1152
1248
  },
1153
1249
  children: [
1154
- state.messages.map((m) => /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
1250
+ state.messages.map((m) => /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(
1155
1251
  ChatMessage,
1156
1252
  {
1157
1253
  message: m,
1158
1254
  disabled: state.isStreaming,
1159
1255
  onAct: (a) => send(a.message, { actionId: a.actionId, displayText: a.label }),
1256
+ onTap: (tile, index) => {
1257
+ send(tile.actionValue, { displayText: tile.title });
1258
+ if (m.role === "assistant" && m.greetingDashboard) onDashboardTap?.(tile, index, m.greetingDashboard.layout);
1259
+ },
1160
1260
  ui,
1161
1261
  slip
1162
1262
  },
1163
1263
  m.id
1164
1264
  )),
1165
- state.isStreaming && state.actionsLoading ? /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(ActionButtons, { actions: [], loading: true, onAct: () => {
1265
+ state.isStreaming && state.actionsLoading ? /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(ActionButtons, { actions: [], loading: true, onAct: () => {
1166
1266
  } }) : null,
1167
- state.status ? /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("div", { className: "ss-w-status", children: state.status }) : null,
1168
- state.error ? /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)("div", { className: "ss-w-error", role: "alert", children: [
1169
- /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("span", { children: "message" in state.error ? state.error.message : ui.somethingWentWrong }),
1170
- /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("button", { type: "button", className: "ss-w-retry", onClick: () => controller.retry(), children: ui.retry })
1267
+ state.status ? /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("div", { className: "ss-w-status", children: state.status }) : null,
1268
+ state.error ? /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)("div", { className: "ss-w-error", role: "alert", children: [
1269
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("span", { children: "message" in state.error ? state.error.message : ui.somethingWentWrong }),
1270
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("button", { type: "button", className: "ss-w-retry", onClick: () => controller.retry(), children: ui.retry })
1171
1271
  ] }) : null,
1172
- empty ? /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(IntentChips, { chips: state.starters, onPick: (t) => send(t) }) : null,
1173
- /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("div", { ref: endRef })
1272
+ !hasUserMessage && !hasDashboard ? /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(IntentChips, { chips: state.starters, onPick: (t) => send(t) }) : null,
1273
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("div", { ref: endRef })
1174
1274
  ]
1175
1275
  }
1176
1276
  );
@@ -1178,7 +1278,7 @@ function MessageList({ controller, ui, slip }) {
1178
1278
 
1179
1279
  // src/components/ChatInput.tsx
1180
1280
  var import_react8 = require("react");
1181
- var import_jsx_runtime17 = require("react/jsx-runtime");
1281
+ var import_jsx_runtime19 = require("react/jsx-runtime");
1182
1282
  function ChatInput({ placeholder, streaming, onSend, onStop, stopLabel = "Stop response", sendLabel = "Send message" }) {
1183
1283
  const [value, setValue] = (0, import_react8.useState)("");
1184
1284
  const submit = () => {
@@ -1193,14 +1293,14 @@ function ChatInput({ placeholder, streaming, onSend, onStop, stopLabel = "Stop r
1193
1293
  submit();
1194
1294
  }
1195
1295
  };
1196
- return /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)("div", { className: "ss-w-input", children: [
1197
- /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("textarea", { className: "ss-w-textarea", rows: 1, placeholder, value, disabled: streaming, onChange: (e) => setValue(e.target.value), onKeyDown }),
1198
- streaming ? /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("button", { type: "button", className: "ss-w-send ss-w-stop", "aria-label": stopLabel, onClick: onStop, children: /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("span", { className: "ss-w-stop-glyph" }) }) : /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("button", { type: "button", className: "ss-w-send", "aria-label": sendLabel, onClick: submit, children: /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(SendIcon, {}) })
1296
+ return /* @__PURE__ */ (0, import_jsx_runtime19.jsxs)("div", { className: "ss-w-input", children: [
1297
+ /* @__PURE__ */ (0, import_jsx_runtime19.jsx)("textarea", { className: "ss-w-textarea", rows: 1, placeholder, value, disabled: streaming, onChange: (e) => setValue(e.target.value), onKeyDown }),
1298
+ streaming ? /* @__PURE__ */ (0, import_jsx_runtime19.jsx)("button", { type: "button", className: "ss-w-send ss-w-stop", "aria-label": stopLabel, onClick: onStop, children: /* @__PURE__ */ (0, import_jsx_runtime19.jsx)("span", { className: "ss-w-stop-glyph" }) }) : /* @__PURE__ */ (0, import_jsx_runtime19.jsx)("button", { type: "button", className: "ss-w-send", "aria-label": sendLabel, onClick: submit, children: /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(SendIcon, {}) })
1199
1299
  ] });
1200
1300
  }
1201
1301
 
1202
1302
  // src/components/ChatPanel.tsx
1203
- var import_jsx_runtime18 = require("react/jsx-runtime");
1303
+ var import_jsx_runtime20 = require("react/jsx-runtime");
1204
1304
  var FULLSCREEN_QUERY = "(max-width: 639px)";
1205
1305
  function useIsModal() {
1206
1306
  const [modal, setModal] = (0, import_react9.useState)(false);
@@ -1214,7 +1314,7 @@ function useIsModal() {
1214
1314
  }, []);
1215
1315
  return modal;
1216
1316
  }
1217
- function ChatPanel({ controller, ui, slip }) {
1317
+ function ChatPanel({ controller, ui, slip, onDashboardTap }) {
1218
1318
  const { state, close, stop, send } = controller;
1219
1319
  const isModal = useIsModal();
1220
1320
  (0, import_react9.useEffect)(() => {
@@ -1224,10 +1324,10 @@ function ChatPanel({ controller, ui, slip }) {
1224
1324
  window.addEventListener("keydown", onKey);
1225
1325
  return () => window.removeEventListener("keydown", onKey);
1226
1326
  }, [close]);
1227
- return /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)("div", { className: "ss-w-panel", role: "dialog", "aria-modal": isModal, "aria-label": ui.chatDialogLabel, children: [
1228
- /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(ChatHeader, { onClose: close, closeLabel: ui.closeChat }),
1229
- /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(MessageList, { controller, ui, slip }),
1230
- /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(ChatInput, { placeholder: ui.placeholder, streaming: state.isStreaming, onSend: (t) => send(t), onStop: stop, stopLabel: ui.stopResponse, sendLabel: ui.sendMessage })
1327
+ return /* @__PURE__ */ (0, import_jsx_runtime20.jsxs)("div", { className: "ss-w-panel", role: "dialog", "aria-modal": isModal, "aria-label": ui.chatDialogLabel, children: [
1328
+ /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(ChatHeader, { onClose: close, closeLabel: ui.closeChat }),
1329
+ /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(MessageList, { controller, ui, slip, onDashboardTap }),
1330
+ /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(ChatInput, { placeholder: ui.placeholder, streaming: state.isStreaming, onSend: (t) => send(t), onStop: stop, stopLabel: ui.stopResponse, sendLabel: ui.sendMessage })
1231
1331
  ] });
1232
1332
  }
1233
1333
 
@@ -1484,17 +1584,85 @@ var WIDGET_CSS = `.ss-w-root { all: revert; }
1484
1584
  @media (prefers-reduced-motion: reduce) {
1485
1585
  .ss-w-abtn-shimmer { animation: none; }
1486
1586
  }
1587
+
1588
+ /* greeting dashboard */
1589
+ .ss-w-dash { display: flex; flex-direction: column; gap: 12px; padding: 4px 0; }
1590
+ .ss-w-dash-head { display: flex; flex-direction: column; }
1591
+ .ss-w-dash-title { font-size: 18px; font-weight: 700; line-height: 1.25; color: var(--ss-w-ink); }
1592
+ .ss-w-dash-subtitle { font-size: 13px; color: var(--ss-w-faint); margin-top: 4px; }
1593
+ .ss-w-dash-foot { font-size: 11px; color: var(--ss-w-faint); margin-top: 2px; }
1594
+
1595
+ .ss-w-dash-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 8px; }
1596
+ .ss-w-dash-tile--half { grid-column: span 1; }
1597
+ .ss-w-dash-tile--full { grid-column: 1 / -1; }
1598
+
1599
+ .ss-w-dash-tile {
1600
+ display: flex; align-items: center; gap: 10px;
1601
+ padding: 12px; border-radius: 14px; border: 1px solid var(--ss-w-line);
1602
+ background: var(--ss-w-panel); text-align: left; cursor: pointer; width: 100%;
1603
+ color: inherit; font: inherit;
1604
+ min-width: 0; /* the panel is min(400px, 100vw - 40px): without this,
1605
+ a long Turkish title blows the column out */
1606
+ }
1607
+ .ss-w-dash-tile-icon { flex: none; display: grid; place-items: center; width: 28px; height: 28px; font-size: 22px; }
1608
+ .ss-w-dash-emoji { line-height: 1; }
1609
+ .ss-w-dash-tile-body { min-width: 0; display: flex; flex-direction: column; gap: 2px; flex: 1; }
1610
+ .ss-w-dash-tile-title { min-width: 0; font-weight: 600; font-size: 14px; overflow-wrap: anywhere; }
1611
+ .ss-w-dash-tile-subtitle { min-width: 0; font-size: 11.5px; color: var(--ss-w-faint); overflow-wrap: anywhere; }
1612
+ .ss-w-dash-tile-chevron { flex: none; margin-left: auto; width: 14px; height: 14px; color: var(--ss-w-faint); }
1613
+
1614
+ /* Badge + crests share their own row, below the title -- NOT the icon/chevron row. A
1615
+ nowrap badge (~70px) sitting beside icon+chevron in a ~180px half-span tile left the
1616
+ title column at ~22px wide, wrapping every title character-by-character. Here the row
1617
+ gets the body's full width, so a badge this size is comfortably inside it. */
1618
+ .ss-w-dash-tile-meta { display: flex; align-items: center; gap: 6px; flex-wrap: wrap; min-width: 0; }
1619
+ .ss-w-dash-tile-badge { flex: none; font-size: 10.5px; font-weight: 700; color: var(--ss-w-accent); background: color-mix(in srgb, var(--ss-w-accent) 13%, var(--ss-w-panel)); border-radius: 999px; padding: 3px 9px; white-space: nowrap; }
1620
+
1621
+ .ss-w-dash-tile-crests { flex: none; display: flex; align-items: center; }
1622
+ .ss-w-dash-tile-crest {
1623
+ width: 18px; height: 18px; border-radius: 50%;
1624
+ background-size: cover; background-position: center; background-color: var(--ss-w-bubble);
1625
+ margin-left: -6px; border: 2px solid var(--ss-w-panel); /* overlapping stack, capped at 3 in DashboardTile */
1626
+ }
1627
+ .ss-w-dash-tile-crest:first-child { margin-left: 0; }
1628
+
1629
+ /* Market: default 2-column grid, tiles get the same accent rail as the other card
1630
+ families (.ss-w-binsight, .ss-w-mocard, .ss-w-omcard) so a market tile reads as
1631
+ part of that visual language rather than a plain unstyled button. */
1632
+ .ss-w-dash--market .ss-w-dash-tile { border-left: 3px solid var(--ss-w-accent); }
1633
+
1634
+ /* Goal: one column of pill rows. */
1635
+ .ss-w-dash--goal .ss-w-dash-grid { grid-template-columns: 1fr; }
1636
+ .ss-w-dash--goal .ss-w-dash-tile { border-radius: 999px; padding: 10px 14px; }
1637
+
1638
+ /* Discovery: tinted bento cards. Stacked (icon over text) so a two- or three-word title gets
1639
+ the tile's full width instead of the ~100px left of a horizontal icon+chevron -- that narrow
1640
+ column was wrapping "Most interesting angles" onto three lines. The chevron is dropped (the
1641
+ whole card is the tap target) and the badge + crest row is forced onto one line. */
1642
+ .ss-w-dash--discovery .ss-w-dash-tile { flex-direction: column; align-items: stretch; gap: 8px; }
1643
+ .ss-w-dash--discovery .ss-w-dash-tile-icon { align-self: flex-start; width: 32px; height: 32px; font-size: 26px; }
1644
+ .ss-w-dash--discovery .ss-w-dash-tile-chevron { display: none; }
1645
+ /* The full-span trends tile has the whole panel width, so its icon and title sit on one row
1646
+ (horizontal, centre-aligned) rather than stacking like the narrow half tiles. */
1647
+ .ss-w-dash--discovery .ss-w-dash-tile--full { flex-direction: row; align-items: center; }
1648
+ .ss-w-dash--discovery .ss-w-dash-tile--full .ss-w-dash-tile-icon { align-self: center; }
1649
+ .ss-w-dash--discovery .ss-w-dash-tile-meta { flex-wrap: nowrap; }
1650
+ .ss-w-dash--discovery .ss-w-dash-tile-crest { width: 22px; height: 22px; }
1651
+ .ss-w-dash--discovery .ss-w-dash-tile:nth-child(1) { background: #eef8f1; }
1652
+ .ss-w-dash--discovery .ss-w-dash-tile:nth-child(2) { background: #eef3fb; }
1653
+ .ss-w-dash--discovery .ss-w-dash-tile:nth-child(3) { background: #fdf6ec; }
1654
+ .ss-w-dash--discovery .ss-w-dash-tile:nth-child(4) { background: #f5eefb; }
1487
1655
  `;
1488
1656
 
1489
1657
  // src/StatsWidget.tsx
1490
- var import_jsx_runtime19 = require("react/jsx-runtime");
1658
+ var import_jsx_runtime21 = require("react/jsx-runtime");
1491
1659
  function buildClient(props) {
1492
1660
  try {
1493
1661
  if ("client" in props && props.client) {
1494
1662
  return { client: props.client, conversation: props.client.conversation() };
1495
1663
  }
1496
1664
  const cfg = props.config;
1497
- const client = new import_widget_sdk4.StatsWidgetClient(toClientOptions(cfg));
1665
+ const client = new import_widget_sdk5.StatsWidgetClient(toClientOptions(cfg));
1498
1666
  return { client, conversation: client.conversation(toUserContext(cfg)) };
1499
1667
  } catch (err) {
1500
1668
  console.error("[StatsWidget] failed to initialize:", err instanceof Error ? err.message : err);
@@ -1529,6 +1697,10 @@ function StatsWidget(props) {
1529
1697
  }),
1530
1698
  [props.config?.onAddToSlip, controller.state.conversationId, built]
1531
1699
  );
1700
+ const onDashboardTap = (0, import_react10.useMemo)(
1701
+ () => built ? createDashboardTapSignal((input) => built.conversation.sendSignal(input)) : void 0,
1702
+ [built]
1703
+ );
1532
1704
  (0, import_react10.useEffect)(() => {
1533
1705
  if (!built || !ui.injectStyles) return;
1534
1706
  const node = rootRef.current?.getRootNode();
@@ -1536,7 +1708,7 @@ function StatsWidget(props) {
1536
1708
  }, [built, ui.injectStyles]);
1537
1709
  if (!built) return null;
1538
1710
  const attrs = themeAttrs(ui);
1539
- return /* @__PURE__ */ (0, import_jsx_runtime19.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_runtime19.jsx)(ChatPanel, { controller, ui, slip }) : /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(
1711
+ return /* @__PURE__ */ (0, import_jsx_runtime21.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_runtime21.jsx)(ChatPanel, { controller, ui, slip, onDashboardTap }) : /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(
1540
1712
  ChatFab,
1541
1713
  {
1542
1714
  label: ui.launcherLabel,