@sensiblestats/widget-react 0.1.0 → 0.2.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 ADDED
@@ -0,0 +1,24 @@
1
+ # @sensiblestats/widget-react
2
+
3
+ ## 0.2.0
4
+
5
+ ### Minor Changes
6
+
7
+ - Add an auto-greeting: set `greetingMessage` (e.g. `"hello"`) and the widget silently sends it on load, surfacing the response — with suggested actions — as a dismissable launcher popup that clears when the panel opens. Also fixes two bugs: the launcher button is now hidden while the panel is open, and the mobile panel no longer pushes the text input off-screen when the keyboard/URL-bar shrinks the viewport (fills via `inset` instead of a fixed `100vh`).
8
+
9
+ ## 0.1.1
10
+
11
+ ### Patch Changes
12
+
13
+ - Republish with the `@sensiblestats/widget-sdk` dependency resolved to a real `^0.2.0` range. The `0.1.0` tarball shipped an unusable `workspace:^` range (npm fails with `Unsupported URL Type "workspace:"`); the release workflow now publishes via yarn, which rewrites workspace ranges to concrete versions.
14
+
15
+ ## 0.1.0
16
+
17
+ ### Minor Changes
18
+
19
+ - 5857cb0: First published release of `StatsWidget`, the SensibleStats embeddable chat widget for React. Streams operator chat over the widget gateway and renders rich entity cards (image, subtitle, season stats), suggested action buttons, intent chips, and markdown answers with horizontally-scrollable tables. Theme-aware (light/dark/auto), style-isolated via injected CSS, and built on `@sensiblestats/widget-sdk`.
20
+
21
+ ### Patch Changes
22
+
23
+ - Updated dependencies [5857cb0]
24
+ - @sensiblestats/widget-sdk@0.2.0
package/dist/index.cjs CHANGED
@@ -49,6 +49,7 @@ function normalizeUi(cfg) {
49
49
  radius: cfg.radius ?? 16,
50
50
  launcherLabel: cfg.launcherLabel ?? "Chat with SensibleStats",
51
51
  greeting: cfg.greeting,
52
+ greetingMessage: cfg.greetingMessage,
52
53
  placeholder: cfg.placeholder ?? "Ask about a team, player or match\u2026",
53
54
  starters: cfg.starters ?? [],
54
55
  injectStyles: cfg.injectStyles ?? true
@@ -126,8 +127,13 @@ function chipFromText(text) {
126
127
  }
127
128
 
128
129
  // src/reducer.ts
130
+ var POPUP_MAX = 160;
129
131
  function initialState(starters) {
130
- return { isOpen: false, messages: [], isStreaming: false, status: null, actionsLoading: false, starters, error: null, lastUserInput: null, lastDisplayText: null };
132
+ return { isOpen: false, messages: [], isStreaming: false, status: null, actionsLoading: false, starters, error: null, lastUserInput: null, lastDisplayText: null, popupText: null };
133
+ }
134
+ function activeTurn(state) {
135
+ const last = state.messages[state.messages.length - 1];
136
+ return last && last.role === "assistant" && !last.done ? last : null;
131
137
  }
132
138
  function mapActive(state, fn) {
133
139
  const last = state.messages[state.messages.length - 1];
@@ -151,16 +157,22 @@ function applyEvent(state, event) {
151
157
  }
152
158
  case "actions_loading":
153
159
  return { ...state, actionsLoading: true, status: event.data.message };
154
- case "turn_complete":
155
- return { ...state, isStreaming: false, status: null, actionsLoading: false, messages: mapActive(state, (t) => ({ ...t, done: true })) };
156
- case "error":
157
- return { ...state, error: event.data, isStreaming: false, status: null, actionsLoading: false, messages: mapActive(state, (t) => ({ ...t, done: true })) };
160
+ case "turn_complete": {
161
+ const active = activeTurn(state);
162
+ const text = active?.text.trim() ?? "";
163
+ const popupText = active?.isGreeting && !state.isOpen && text ? text.slice(0, POPUP_MAX) : state.popupText;
164
+ return { ...state, isStreaming: false, status: null, actionsLoading: false, popupText, messages: mapActive(state, (t) => ({ ...t, done: true })) };
165
+ }
166
+ case "error": {
167
+ const suppress = activeTurn(state)?.isGreeting === true;
168
+ return { ...state, error: suppress ? state.error : event.data, isStreaming: false, status: null, actionsLoading: false, messages: mapActive(state, (t) => ({ ...t, done: true })) };
169
+ }
158
170
  }
159
171
  }
160
172
  function reducer(state, action) {
161
173
  switch (action.kind) {
162
174
  case "OPEN":
163
- return { ...state, isOpen: true };
175
+ return { ...state, isOpen: true, popupText: null };
164
176
  case "CLOSE":
165
177
  return { ...state, isOpen: false, isStreaming: false, status: null, actionsLoading: false };
166
178
  case "USER_SENT": {
@@ -168,6 +180,12 @@ function reducer(state, action) {
168
180
  const assistant = { role: "assistant", id: action.assistantId, text: "", cards: [], actions: [], done: false };
169
181
  return { ...state, messages: [...state.messages, user, assistant], isStreaming: true, status: null, actionsLoading: false, starters: [], error: null, lastUserInput: action.input, lastDisplayText: action.displayText ?? null };
170
182
  }
183
+ case "GREETING_SENT": {
184
+ const assistant = { role: "assistant", id: action.assistantId, text: "", cards: [], actions: [], done: false, isGreeting: true };
185
+ return { ...state, messages: [...state.messages, assistant], isStreaming: true, status: null, error: null };
186
+ }
187
+ case "DISMISS_POPUP":
188
+ return { ...state, popupText: null };
171
189
  case "EVENT":
172
190
  return applyEvent(state, action.event);
173
191
  case "STREAM_ERROR":
@@ -178,31 +196,36 @@ function reducer(state, action) {
178
196
  }
179
197
 
180
198
  // src/useChatStream.ts
181
- function useChatStream(conversation, starterTexts) {
199
+ function useChatStream(conversation, starterTexts, greetingMessage) {
182
200
  const seed = (0, import_react.useMemo)(() => initialState(starterTexts.map(chipFromText)), []);
183
201
  const [state, dispatch] = (0, import_react.useReducer)(reducer, seed);
184
202
  const idRef = (0, import_react.useRef)(0);
185
203
  const handleRef = (0, import_react.useRef)(null);
186
204
  const stateRef = (0, import_react.useRef)(state);
187
205
  stateRef.current = state;
206
+ const greetedRef = (0, import_react.useRef)(false);
188
207
  const nextId = () => `m${idRef.current++}`;
189
- const run = (0, import_react.useCallback)((input, displayText) => {
190
- handleRef.current?.cancel();
191
- dispatch({ kind: "USER_SENT", input, userId: nextId(), assistantId: nextId(), displayText });
192
- const handle = conversation.send(input);
193
- handleRef.current = handle;
208
+ const consume = (0, import_react.useCallback)((handle, onError) => {
209
+ ;
194
210
  (async () => {
195
211
  try {
196
212
  for await (const event of handle) dispatch({ kind: "EVENT", event });
197
213
  } catch (err) {
198
- if (err instanceof import_widget_sdk2.WidgetSdkError) dispatch({ kind: "STREAM_ERROR", error: err });
214
+ if (err instanceof import_widget_sdk2.WidgetSdkError) onError(err);
199
215
  else if (err?.name === "AbortError") {
200
216
  } else throw err;
201
217
  } finally {
202
218
  if (handleRef.current === handle) handleRef.current = null;
203
219
  }
204
220
  })();
205
- }, [conversation]);
221
+ }, []);
222
+ const run = (0, import_react.useCallback)((input, displayText) => {
223
+ handleRef.current?.cancel();
224
+ dispatch({ kind: "USER_SENT", input, userId: nextId(), assistantId: nextId(), displayText });
225
+ const handle = conversation.send(input);
226
+ handleRef.current = handle;
227
+ consume(handle, (err) => dispatch({ kind: "STREAM_ERROR", error: err }));
228
+ }, [conversation, consume]);
206
229
  const send = (0, import_react.useCallback)((text, opts) => {
207
230
  const trimmed = text.trim();
208
231
  if (!trimmed) return;
@@ -212,6 +235,20 @@ function useChatStream(conversation, starterTexts) {
212
235
  const last = stateRef.current.lastUserInput;
213
236
  if (last) run(last, stateRef.current.lastDisplayText ?? void 0);
214
237
  }, [run]);
238
+ (0, import_react.useEffect)(() => {
239
+ const message = greetingMessage?.trim();
240
+ if (!message || greetedRef.current) return;
241
+ greetedRef.current = true;
242
+ try {
243
+ handleRef.current?.cancel();
244
+ dispatch({ kind: "GREETING_SENT", assistantId: nextId() });
245
+ const handle = conversation.send({ message });
246
+ handleRef.current = handle;
247
+ consume(handle, () => dispatch({ kind: "STOP" }));
248
+ } catch {
249
+ dispatch({ kind: "STOP" });
250
+ }
251
+ }, [greetingMessage, conversation, consume]);
215
252
  const open = (0, import_react.useCallback)(() => dispatch({ kind: "OPEN" }), []);
216
253
  const close = (0, import_react.useCallback)(() => {
217
254
  handleRef.current?.cancel();
@@ -227,7 +264,8 @@ function useChatStream(conversation, starterTexts) {
227
264
  if (stateRef.current.isOpen) close();
228
265
  else open();
229
266
  }, [close, open]);
230
- return { state, open, close, stop, toggle, send, retry };
267
+ const dismissPopup = (0, import_react.useCallback)(() => dispatch({ kind: "DISMISS_POPUP" }), []);
268
+ return { state, open, close, stop, toggle, send, retry, dismissPopup };
231
269
  }
232
270
 
233
271
  // src/components/Icons.tsx
@@ -253,16 +291,20 @@ function ChevronIcon() {
253
291
 
254
292
  // src/components/GreetingBubble.tsx
255
293
  var import_jsx_runtime2 = require("react/jsx-runtime");
256
- function GreetingBubble({ text }) {
257
- return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: "ss-w-greeting", role: "note", children: text });
294
+ function GreetingBubble({ text, onOpen, onDismiss }) {
295
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "ss-w-greeting", role: "note", children: [
296
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("button", { type: "button", className: "ss-w-greeting-body", onClick: onOpen, children: text }),
297
+ onDismiss ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("button", { type: "button", className: "ss-w-greeting-x", "aria-label": "Dismiss", onClick: onDismiss, children: "\xD7" }) : null
298
+ ] });
258
299
  }
259
300
 
260
301
  // src/components/ChatFab.tsx
261
302
  var import_jsx_runtime3 = require("react/jsx-runtime");
262
- function ChatFab({ label, open, greeting, onToggle }) {
303
+ function ChatFab({ label, greeting, popupText, onOpen, onDismissPopup }) {
304
+ const bubble = popupText ?? greeting;
263
305
  return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "ss-w-fab-wrap", children: [
264
- !open && greeting ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(GreetingBubble, { text: greeting }) : null,
265
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("button", { type: "button", className: "ss-w-fab", "aria-label": label, "aria-expanded": open, onClick: onToggle, children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(SsMark, { className: "ss-w-fab-mark" }) })
306
+ bubble ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(GreetingBubble, { text: bubble, onOpen, onDismiss: popupText ? onDismissPopup : void 0 }) : null,
307
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("button", { type: "button", className: "ss-w-fab", "aria-label": label, onClick: onOpen, children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(SsMark, { className: "ss-w-fab-mark" }) })
266
308
  ] });
267
309
  }
268
310
 
@@ -456,7 +498,123 @@ function ChatPanel({ controller, ui }) {
456
498
  }
457
499
 
458
500
  // src/styles/css.generated.ts
459
- var WIDGET_CSS = '.ss-w-root { all: revert; }\n.ss-w-root, .ss-w-root * { box-sizing: border-box; margin: 0; padding: 0; }\n.ss-w-root {\n --ss-w-accent: #17936a;\n --ss-w-ink: #14181d; --ss-w-faint: #6b7580; --ss-w-line: #e4e8ec;\n --ss-w-panel: #ffffff; --ss-w-bubble: #f3f5f7; --ss-w-nav: #0e1622;\n --ss-w-radius: 16px; --ss-w-offset-x: 20px; --ss-w-offset-y: 20px;\n font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;\n font-size: 13px; line-height: 1.5; color: var(--ss-w-ink);\n}\n@media (prefers-color-scheme: dark) {\n .ss-w-root[data-ss-w-theme="auto"] {\n --ss-w-accent: #37c891; --ss-w-ink: #e8edf2; --ss-w-faint: #8b95a1;\n --ss-w-line: #2a3441; --ss-w-panel: #141b24; --ss-w-bubble: #1e2732; --ss-w-nav: #0a0f16;\n }\n}\n.ss-w-root[data-ss-w-theme="dark"] {\n --ss-w-accent: #37c891; --ss-w-ink: #e8edf2; --ss-w-faint: #8b95a1;\n --ss-w-line: #2a3441; --ss-w-panel: #141b24; --ss-w-bubble: #1e2732; --ss-w-nav: #0a0f16;\n}\n.ss-w-root[data-ss-w-theme="light"] {\n --ss-w-accent: #17936a; --ss-w-ink: #14181d; --ss-w-faint: #6b7580;\n --ss-w-line: #e4e8ec; --ss-w-panel: #ffffff; --ss-w-bubble: #f3f5f7; --ss-w-nav: #0e1622;\n}\n\n/* launcher */\n.ss-w-fab-wrap { position: fixed; bottom: var(--ss-w-offset-y); z-index: 2147483000; display: flex; flex-direction: column; align-items: flex-end; gap: 10px; }\n.ss-w-root[data-ss-w-pos="right"] .ss-w-fab-wrap { right: var(--ss-w-offset-x); }\n.ss-w-root[data-ss-w-pos="left"] .ss-w-fab-wrap { left: var(--ss-w-offset-x); align-items: flex-start; }\n.ss-w-fab { width: 56px; height: 56px; border: none; border-radius: 50%; background: var(--ss-w-nav); color: #fff; cursor: pointer; display: grid; place-items: center; box-shadow: 0 6px 20px rgba(0,0,0,.28); }\n.ss-w-fab-mark { font-size: 30px; }\n.ss-w-greeting { max-width: 220px; background: var(--ss-w-panel); color: var(--ss-w-ink); border: 1px solid var(--ss-w-line); border-radius: 12px; padding: 9px 12px; box-shadow: 0 4px 14px rgba(0,0,0,.14); }\n\n/* panel */\n.ss-w-panel { position: fixed; bottom: calc(var(--ss-w-offset-y) + 72px); z-index: 2147483000; display: flex; flex-direction: column; width: min(400px, calc(100vw - 40px)); height: min(620px, calc(100vh - 120px)); background: var(--ss-w-panel); border: 1px solid var(--ss-w-line); border-radius: var(--ss-w-radius); overflow: hidden; box-shadow: 0 12px 40px rgba(0,0,0,.24); }\n.ss-w-root[data-ss-w-pos="right"] .ss-w-panel { right: var(--ss-w-offset-x); }\n.ss-w-root[data-ss-w-pos="left"] .ss-w-panel { left: var(--ss-w-offset-x); }\n@media (max-width: 639px) {\n .ss-w-panel { inset: 0; width: 100vw; height: 100vh; border: none; border-radius: 0; }\n}\n.ss-w-header { display: flex; align-items: center; gap: 8px; padding: 12px 14px; border-bottom: 1px solid var(--ss-w-line); }\n.ss-w-header-mark { font-size: 18px; }\n.ss-w-header-title { font-weight: 660; }\n.ss-w-close { margin-left: auto; background: none; border: none; color: var(--ss-w-faint); cursor: pointer; font-size: 16px; display: grid; place-items: center; }\n\n/* messages */\n.ss-w-list { flex: 1; overflow-y: auto; padding: 14px; display: flex; flex-direction: column; gap: 12px; }\n.ss-w-msg { display: flex; }\n.ss-w-user { justify-content: flex-end; }\n.ss-w-user .ss-w-bubble { background: color-mix(in srgb, var(--ss-w-accent) 14%, var(--ss-w-panel)); border: 1px solid color-mix(in srgb, var(--ss-w-accent) 30%, var(--ss-w-line)); border-radius: 14px; padding: 8px 12px; max-width: 80%; }\n.ss-w-bot { flex-direction: column; gap: 8px; }\n.ss-w-status { font-size: 11px; color: var(--ss-w-faint); font-style: italic; }\n.ss-w-error { display: flex; align-items: center; gap: 8px; font-size: 12px; color: #c0392b; }\n.ss-w-retry { background: none; border: 1px solid currentColor; border-radius: 10px; padding: 2px 8px; cursor: pointer; color: inherit; }\n\n/* markdown */\n.ss-w-md { display: flex; flex-direction: column; gap: 8px; }\n.ss-w-md :where(p, ul, ol, table, pre, blockquote, h1, h2, h3, h4) { margin: 0; }\n.ss-w-md > *:first-child { margin-top: 0; }\n.ss-w-md > *:last-child { margin-bottom: 0; }\n.ss-w-md-ul { list-style: disc outside; padding-left: 18px; }\n.ss-w-md-ol { list-style: decimal outside; padding-left: 18px; }\n.ss-w-md-ul li::marker, .ss-w-md-ol li::marker { color: var(--ss-w-accent); }\n.ss-w-md-link { color: var(--ss-w-accent); text-decoration: underline; }\n.ss-w-table-wrap { overflow-x: auto; max-width: 100%; -webkit-overflow-scrolling: touch; }\n.ss-w-md-table { border-collapse: collapse; width: max-content; min-width: 100%; font-variant-numeric: tabular-nums; }\n.ss-w-md-table td, .ss-w-md-table th { border: 1px solid var(--ss-w-line); padding: 4px 8px; text-align: left; white-space: nowrap; }\n.ss-w-md-table th { background: color-mix(in srgb, var(--ss-w-accent) 12%, var(--ss-w-panel)); font-weight: 700; }\n.ss-w-md-table tbody tr:nth-child(even) { background: var(--ss-w-bubble); }\n.ss-w-md-code { background: var(--ss-w-bubble); border-radius: 5px; padding: 1px 5px; font-family: ui-monospace, monospace; }\n\n/* entity cards */\n.ss-w-cards { display: flex; flex-direction: column; gap: 7px; }\n.ss-w-ecard { display: flex; align-items: center; gap: 11px; width: 100%; background: var(--ss-w-panel); border: 1px solid var(--ss-w-line); border-radius: 12px; padding: 8px 11px; text-align: left; cursor: pointer; }\n.ss-w-thumb { width: 40px; height: 40px; flex: none; border-radius: 50%; display: grid; place-items: center; font-size: 13px; font-weight: 700; color: #fff; background-color: var(--ss-w-nav); background-size: cover; background-position: center; }\n.ss-w-thumb.ss-w-team { border-radius: 10px; }\n.ss-w-ecard[data-kind="player"] .ss-w-thumb { background-color: var(--ss-w-accent); }\n.ss-w-meta { display: flex; flex-direction: column; min-width: 0; }\n.ss-w-kind { font-size: 8.5px; font-weight: 700; letter-spacing: .05em; text-transform: uppercase; color: var(--ss-w-faint); line-height: 1.3; }\n.ss-w-nm { font-weight: 660; }\n.ss-w-sub { font-size: 11px; color: var(--ss-w-faint); }\n.ss-w-stat { margin-left: auto; text-align: center; }\n.ss-w-stat b { display: block; font-variant-numeric: tabular-nums; }\n.ss-w-stat span { font-size: 9px; text-transform: uppercase; color: var(--ss-w-faint); }\n.ss-w-chev { color: var(--ss-w-faint); font-size: 16px; display: grid; place-items: center; }\n.ss-w-cards-more { align-self: flex-start; font-size: 11.5px; font-weight: 560; color: var(--ss-w-accent); background: none; border: none; cursor: pointer; padding: 3px 2px; }\n.ss-w-cards-more:hover:not(:disabled) { text-decoration: underline; }\n.ss-w-ecard:disabled, .ss-w-cards-more:disabled { opacity: .5; cursor: default; }\n\n/* action buttons */\n.ss-w-actions { display: flex; flex-direction: column; align-items: flex-start; gap: 6px; }\n.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; }\n.ss-w-abtn:disabled { opacity: .5; cursor: default; }\n.ss-w-abtn-shimmer { width: 140px; height: 30px; border-radius: 16px; background: linear-gradient(90deg, var(--ss-w-bubble), var(--ss-w-line), var(--ss-w-bubble)); background-size: 200% 100%; animation: ss-w-shimmer 1.2s infinite; }\n@keyframes ss-w-shimmer { from { background-position: 200% 0; } to { background-position: -200% 0; } }\n\n/* intent chips */\n.ss-w-chips { display: flex; flex-wrap: wrap; gap: 6px; }\n.ss-w-chip { font-size: 11.5px; font-weight: 550; color: var(--ss-w-ink); background: var(--ss-w-bubble); border: 1px solid var(--ss-w-line); border-radius: 14px; padding: 5px 11px; cursor: pointer; }\n\n/* input */\n.ss-w-input { display: flex; align-items: flex-end; gap: 8px; padding: 10px 12px; border-top: 1px solid var(--ss-w-line); }\n.ss-w-textarea { flex: 1; resize: none; border: 1px solid var(--ss-w-line); border-radius: 12px; padding: 8px 10px; font: inherit; color: inherit; background: var(--ss-w-panel); max-height: 96px; }\n.ss-w-textarea:disabled { opacity: .55; cursor: not-allowed; }\n.ss-w-send { width: 36px; height: 36px; flex: none; border: none; border-radius: 50%; background: var(--ss-w-accent); color: #fff; cursor: pointer; display: grid; place-items: center; font-size: 16px; }\n.ss-w-stop-glyph { width: 11px; height: 11px; background: currentColor; border-radius: 2px; }\n\n@media (prefers-reduced-motion: reduce) {\n .ss-w-abtn-shimmer { animation: none; }\n}\n';
501
+ var WIDGET_CSS = `.ss-w-root { all: revert; }
502
+ .ss-w-root, .ss-w-root * { box-sizing: border-box; margin: 0; padding: 0; }
503
+ .ss-w-root {
504
+ --ss-w-accent: #17936a;
505
+ --ss-w-ink: #14181d; --ss-w-faint: #6b7580; --ss-w-line: #e4e8ec;
506
+ --ss-w-panel: #ffffff; --ss-w-bubble: #f3f5f7; --ss-w-nav: #0e1622;
507
+ --ss-w-radius: 16px; --ss-w-offset-x: 20px; --ss-w-offset-y: 20px;
508
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
509
+ font-size: 13px; line-height: 1.5; color: var(--ss-w-ink);
510
+ }
511
+ @media (prefers-color-scheme: dark) {
512
+ .ss-w-root[data-ss-w-theme="auto"] {
513
+ --ss-w-accent: #37c891; --ss-w-ink: #e8edf2; --ss-w-faint: #8b95a1;
514
+ --ss-w-line: #2a3441; --ss-w-panel: #141b24; --ss-w-bubble: #1e2732; --ss-w-nav: #0a0f16;
515
+ }
516
+ }
517
+ .ss-w-root[data-ss-w-theme="dark"] {
518
+ --ss-w-accent: #37c891; --ss-w-ink: #e8edf2; --ss-w-faint: #8b95a1;
519
+ --ss-w-line: #2a3441; --ss-w-panel: #141b24; --ss-w-bubble: #1e2732; --ss-w-nav: #0a0f16;
520
+ }
521
+ .ss-w-root[data-ss-w-theme="light"] {
522
+ --ss-w-accent: #17936a; --ss-w-ink: #14181d; --ss-w-faint: #6b7580;
523
+ --ss-w-line: #e4e8ec; --ss-w-panel: #ffffff; --ss-w-bubble: #f3f5f7; --ss-w-nav: #0e1622;
524
+ }
525
+
526
+ /* launcher */
527
+ .ss-w-fab-wrap { position: fixed; bottom: var(--ss-w-offset-y); z-index: 2147483000; display: flex; flex-direction: column; align-items: flex-end; gap: 10px; }
528
+ .ss-w-root[data-ss-w-pos="right"] .ss-w-fab-wrap { right: var(--ss-w-offset-x); }
529
+ .ss-w-root[data-ss-w-pos="left"] .ss-w-fab-wrap { left: var(--ss-w-offset-x); align-items: flex-start; }
530
+ .ss-w-fab { width: 56px; height: 56px; border: none; border-radius: 50%; background: var(--ss-w-nav); color: #fff; cursor: pointer; display: grid; place-items: center; box-shadow: 0 6px 20px rgba(0,0,0,.28); }
531
+ .ss-w-fab-mark { font-size: 30px; }
532
+ .ss-w-greeting { max-width: 240px; display: flex; align-items: flex-start; background: var(--ss-w-panel); color: var(--ss-w-ink); border: 1px solid var(--ss-w-line); border-radius: 12px; box-shadow: 0 4px 14px rgba(0,0,0,.14); }
533
+ .ss-w-greeting-body { flex: 1; min-width: 0; text-align: left; background: none; border: none; color: inherit; font: inherit; cursor: pointer; padding: 9px 4px 9px 12px; }
534
+ .ss-w-greeting-x { flex: none; background: none; border: none; color: var(--ss-w-faint); cursor: pointer; font-size: 17px; line-height: 1; padding: 7px 9px 0 2px; }
535
+
536
+ /* panel */
537
+ .ss-w-panel { position: fixed; bottom: calc(var(--ss-w-offset-y) + 72px); z-index: 2147483000; display: flex; flex-direction: column; width: min(400px, calc(100vw - 40px)); height: min(620px, calc(100vh - 120px)); background: var(--ss-w-panel); border: 1px solid var(--ss-w-line); border-radius: var(--ss-w-radius); overflow: hidden; box-shadow: 0 12px 40px rgba(0,0,0,.24); }
538
+ .ss-w-root[data-ss-w-pos="right"] .ss-w-panel { right: var(--ss-w-offset-x); }
539
+ .ss-w-root[data-ss-w-pos="left"] .ss-w-panel { left: var(--ss-w-offset-x); }
540
+ @media (max-width: 639px) {
541
+ /* Fill via inset with no explicit height (like the site's working panel) so the
542
+ input stays visible when the mobile keyboard/URL-bar shrinks the viewport. The
543
+ position-specific selectors override the right/left offsets set above. */
544
+ .ss-w-root[data-ss-w-pos="right"] .ss-w-panel,
545
+ .ss-w-root[data-ss-w-pos="left"] .ss-w-panel { inset: 0; width: auto; height: auto; border: none; border-radius: 0; }
546
+ }
547
+ .ss-w-header { display: flex; align-items: center; gap: 8px; padding: 12px 14px; border-bottom: 1px solid var(--ss-w-line); }
548
+ .ss-w-header-mark { font-size: 18px; }
549
+ .ss-w-header-title { font-weight: 660; }
550
+ .ss-w-close { margin-left: auto; background: none; border: none; color: var(--ss-w-faint); cursor: pointer; font-size: 16px; display: grid; place-items: center; }
551
+
552
+ /* messages */
553
+ .ss-w-list { flex: 1; overflow-y: auto; padding: 14px; display: flex; flex-direction: column; gap: 12px; }
554
+ .ss-w-msg { display: flex; }
555
+ .ss-w-user { justify-content: flex-end; }
556
+ .ss-w-user .ss-w-bubble { background: color-mix(in srgb, var(--ss-w-accent) 14%, var(--ss-w-panel)); border: 1px solid color-mix(in srgb, var(--ss-w-accent) 30%, var(--ss-w-line)); border-radius: 14px; padding: 8px 12px; max-width: 80%; }
557
+ .ss-w-bot { flex-direction: column; gap: 8px; }
558
+ .ss-w-status { font-size: 11px; color: var(--ss-w-faint); font-style: italic; }
559
+ .ss-w-error { display: flex; align-items: center; gap: 8px; font-size: 12px; color: #c0392b; }
560
+ .ss-w-retry { background: none; border: 1px solid currentColor; border-radius: 10px; padding: 2px 8px; cursor: pointer; color: inherit; }
561
+
562
+ /* markdown */
563
+ .ss-w-md { display: flex; flex-direction: column; gap: 8px; }
564
+ .ss-w-md :where(p, ul, ol, table, pre, blockquote, h1, h2, h3, h4) { margin: 0; }
565
+ .ss-w-md > *:first-child { margin-top: 0; }
566
+ .ss-w-md > *:last-child { margin-bottom: 0; }
567
+ .ss-w-md-ul { list-style: disc outside; padding-left: 18px; }
568
+ .ss-w-md-ol { list-style: decimal outside; padding-left: 18px; }
569
+ .ss-w-md-ul li::marker, .ss-w-md-ol li::marker { color: var(--ss-w-accent); }
570
+ .ss-w-md-link { color: var(--ss-w-accent); text-decoration: underline; }
571
+ .ss-w-table-wrap { overflow-x: auto; max-width: 100%; -webkit-overflow-scrolling: touch; }
572
+ .ss-w-md-table { border-collapse: collapse; width: max-content; min-width: 100%; font-variant-numeric: tabular-nums; }
573
+ .ss-w-md-table td, .ss-w-md-table th { border: 1px solid var(--ss-w-line); padding: 4px 8px; text-align: left; white-space: nowrap; }
574
+ .ss-w-md-table th { background: color-mix(in srgb, var(--ss-w-accent) 12%, var(--ss-w-panel)); font-weight: 700; }
575
+ .ss-w-md-table tbody tr:nth-child(even) { background: var(--ss-w-bubble); }
576
+ .ss-w-md-code { background: var(--ss-w-bubble); border-radius: 5px; padding: 1px 5px; font-family: ui-monospace, monospace; }
577
+
578
+ /* entity cards */
579
+ .ss-w-cards { display: flex; flex-direction: column; gap: 7px; }
580
+ .ss-w-ecard { display: flex; align-items: center; gap: 11px; width: 100%; background: var(--ss-w-panel); border: 1px solid var(--ss-w-line); border-radius: 12px; padding: 8px 11px; text-align: left; cursor: pointer; }
581
+ .ss-w-thumb { width: 40px; height: 40px; flex: none; border-radius: 50%; display: grid; place-items: center; font-size: 13px; font-weight: 700; color: #fff; background-color: var(--ss-w-nav); background-size: cover; background-position: center; }
582
+ .ss-w-thumb.ss-w-team { border-radius: 10px; }
583
+ .ss-w-ecard[data-kind="player"] .ss-w-thumb { background-color: var(--ss-w-accent); }
584
+ .ss-w-meta { display: flex; flex-direction: column; min-width: 0; }
585
+ .ss-w-kind { font-size: 8.5px; font-weight: 700; letter-spacing: .05em; text-transform: uppercase; color: var(--ss-w-faint); line-height: 1.3; }
586
+ .ss-w-nm { font-weight: 660; }
587
+ .ss-w-sub { font-size: 11px; color: var(--ss-w-faint); }
588
+ .ss-w-stat { margin-left: auto; text-align: center; }
589
+ .ss-w-stat b { display: block; font-variant-numeric: tabular-nums; }
590
+ .ss-w-stat span { font-size: 9px; text-transform: uppercase; color: var(--ss-w-faint); }
591
+ .ss-w-chev { color: var(--ss-w-faint); font-size: 16px; display: grid; place-items: center; }
592
+ .ss-w-cards-more { align-self: flex-start; font-size: 11.5px; font-weight: 560; color: var(--ss-w-accent); background: none; border: none; cursor: pointer; padding: 3px 2px; }
593
+ .ss-w-cards-more:hover:not(:disabled) { text-decoration: underline; }
594
+ .ss-w-ecard:disabled, .ss-w-cards-more:disabled { opacity: .5; cursor: default; }
595
+
596
+ /* action buttons */
597
+ .ss-w-actions { display: flex; flex-direction: column; align-items: flex-start; gap: 6px; }
598
+ .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; }
599
+ .ss-w-abtn:disabled { opacity: .5; cursor: default; }
600
+ .ss-w-abtn-shimmer { width: 140px; height: 30px; border-radius: 16px; background: linear-gradient(90deg, var(--ss-w-bubble), var(--ss-w-line), var(--ss-w-bubble)); background-size: 200% 100%; animation: ss-w-shimmer 1.2s infinite; }
601
+ @keyframes ss-w-shimmer { from { background-position: 200% 0; } to { background-position: -200% 0; } }
602
+
603
+ /* intent chips */
604
+ .ss-w-chips { display: flex; flex-wrap: wrap; gap: 6px; }
605
+ .ss-w-chip { font-size: 11.5px; font-weight: 550; color: var(--ss-w-ink); background: var(--ss-w-bubble); border: 1px solid var(--ss-w-line); border-radius: 14px; padding: 5px 11px; cursor: pointer; }
606
+
607
+ /* input */
608
+ .ss-w-input { display: flex; align-items: flex-end; gap: 8px; padding: 10px 12px; border-top: 1px solid var(--ss-w-line); }
609
+ .ss-w-textarea { flex: 1; resize: none; border: 1px solid var(--ss-w-line); border-radius: 12px; padding: 8px 10px; font: inherit; color: inherit; background: var(--ss-w-panel); max-height: 96px; }
610
+ .ss-w-textarea:disabled { opacity: .55; cursor: not-allowed; }
611
+ .ss-w-send { width: 36px; height: 36px; flex: none; border: none; border-radius: 50%; background: var(--ss-w-accent); color: #fff; cursor: pointer; display: grid; place-items: center; font-size: 16px; }
612
+ .ss-w-stop-glyph { width: 11px; height: 11px; background: currentColor; border-radius: 2px; }
613
+
614
+ @media (prefers-reduced-motion: reduce) {
615
+ .ss-w-abtn-shimmer { animation: none; }
616
+ }
617
+ `;
460
618
 
461
619
  // src/StatsWidget.tsx
462
620
  var import_jsx_runtime13 = require("react/jsx-runtime");
@@ -487,7 +645,7 @@ function StatsWidget(props) {
487
645
  const built = (0, import_react6.useMemo)(() => buildClient(props), [injectedClient, cfg?.operatorId, cfg?.publicKey, cfg?.baseUrl]);
488
646
  const ui = (0, import_react6.useMemo)(() => normalizeUi(props.config ?? { operatorId: "", publicKey: "" }), [props.config]);
489
647
  const rootRef = (0, import_react6.useRef)(null);
490
- const controller = useChatStream(built?.conversation ?? emptyConversation(), ui.starters);
648
+ const controller = useChatStream(built?.conversation ?? emptyConversation(), ui.starters, built ? ui.greetingMessage : void 0);
491
649
  (0, import_react6.useEffect)(() => {
492
650
  if (!built || !ui.injectStyles) return;
493
651
  const node = rootRef.current?.getRootNode();
@@ -495,10 +653,16 @@ function StatsWidget(props) {
495
653
  }, [built, ui.injectStyles]);
496
654
  if (!built) return null;
497
655
  const attrs = themeAttrs(ui);
498
- return /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)("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: [
499
- controller.state.isOpen ? /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(ChatPanel, { controller, ui }) : null,
500
- /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(ChatFab, { label: ui.launcherLabel, open: controller.state.isOpen, greeting: ui.greeting, onToggle: controller.toggle })
501
- ] });
656
+ 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)(
657
+ ChatFab,
658
+ {
659
+ label: ui.launcherLabel,
660
+ greeting: ui.greeting,
661
+ popupText: controller.state.popupText,
662
+ onOpen: controller.open,
663
+ onDismissPopup: controller.dismissPopup
664
+ }
665
+ ) });
502
666
  }
503
667
  function emptyConversation() {
504
668
  return { send: () => {