@sunboard/react 1.0.0 → 1.1.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/README.md CHANGED
@@ -207,7 +207,7 @@ Call `useLauncher()` once for your help menu and pass what it returns down.
207
207
  | `tourRunning` | `true` while a tour runs. |
208
208
  | `opened()` | Call it when your menu opens, so the open counts in Analytics. |
209
209
  | `start(guideKey, replyId?)` | Starts a guide. Pass the id of the answer that offered it, and the conversation gets a line after that answer when the tour ends. |
210
- | `messages` | The conversation with the assistant, oldest first. Each `Message` has an `id` and a `kind`: `question`, `waiting`, `reply`, `error` or `tour`. A `reply` holds an `AskReply` with the answer's text in `answer`, the articles it came from in `sources`, and `guideKey` when it offers a guide to start. |
210
+ | `messages` | The conversation with the assistant, oldest first. Each `Message` has an `id` and a `kind`: `question`, `waiting`, `reply`, `error` or `tour`. A `reply` holds an `AskReply` with the answer's text in `answer` and `guideKey` when it offers a guide to start. |
211
211
  | `waiting` | `true` while a question is out and its answer has not come back. |
212
212
  | `rateLimitedUntil` | While the user has asked too many questions this hour, the time they can ask again, as an ISO string. Otherwise `undefined`. |
213
213
  | `handedOff` | `true` while a tour started from the conversation runs. It turns `false` when the tour ends, the moment to show your panel again. |
package/dist/index.d.mts CHANGED
@@ -332,22 +332,11 @@ type GuideEventReport = {
332
332
  });
333
333
  /**
334
334
  * ANSWERED is an answer written from the docs and checked against them.
335
- * UNSUPPORTED is one the check did not back, so the visitor is pointed at the
336
- * section instead.
337
- * GUIDE offers a tour, with an answer when sections were found too.
335
+ * UNSUPPORTED is one the check did not back twice, so the visitor gets the
336
+ * not-covered reply. NONE found nothing to answer with.
337
+ * GUIDE offers a tour, with an answer when one was written and backed.
338
338
  */
339
339
  type AskOutcome = "GUIDE" | "ANSWERED" | "UNSUPPORTED" | "NONE";
340
- /** One section the answer came from. */
341
- interface AskArticle {
342
- /** The section's id. */
343
- id: string;
344
- /** The article's title. */
345
- title: string;
346
- /** Where the article is read. An article written in Sunboard has none. */
347
- url?: string;
348
- /** The section's heading, when it has one. */
349
- heading?: string;
350
- }
351
340
  /** `id` is the stored question's id. `answer` is the whole text to show. */
352
341
  interface AskReply {
353
342
  id: string;
@@ -356,11 +345,13 @@ interface AskReply {
356
345
  /** This question opened its conversation, so the widget greets the visitor. */
357
346
  firstTurn: boolean;
358
347
  outcome: AskOutcome;
359
- /** Plain text with blank lines between paragraphs, `1.` and `-` lists, and `**bold**`. */
348
+ /**
349
+ * Plain text with blank lines between paragraphs, `1.` and `-` lists, and
350
+ * `**bold**`. Empty when a guide is offered with nothing written.
351
+ */
360
352
  answer: string;
361
- /** At most three, in the order the answer used them. */
362
- sources: AskArticle[];
363
353
  guideKey?: string;
354
+ /** Set when the answer is the not-covered sentence and the project has a support link. */
364
355
  supportUrl?: string;
365
356
  }
366
357
  /** What the server is doing while the visitor waits, in this order. */
@@ -402,8 +393,6 @@ interface Labels {
402
393
  send: string;
403
394
  /** The pill that scrolls back down to the newest message. */
404
395
  latest: string;
405
- /** On an older reply whose sources are folded away. */
406
- sourcesCount: (count: number) => string;
407
396
  /** Over the first answer of a conversation. `name` is the connection's `user.firstName`. */
408
397
  greeting: (name?: string) => string;
409
398
  /** Under an answer that offers a guide, above Show me how. */
@@ -428,8 +417,6 @@ interface Labels {
428
417
  /** After six seconds of waiting. */
429
418
  slow: string;
430
419
  };
431
- /** The name of the group of sections an answer came from, for a screen reader. */
432
- sources: string;
433
420
  /** Over the guides listed before the first question, this page's first. */
434
421
  commonQuestions: string;
435
422
  /** The one button under an answer that has a guide behind it. */
@@ -447,12 +434,10 @@ interface Labels {
447
434
  lostBody: string;
448
435
  tryAgain: string;
449
436
  /**
450
- * What the server answers when nothing was written: no guide and no
451
- * section, a section with no backed answer, a guide with no section.
437
+ * What the server answers when there is nothing to show: no guide and no
438
+ * section, or no guide and an answer the check did not back twice.
452
439
  */
453
440
  notCovered: (product: string) => string;
454
- coveredBy: (article: string, heading?: string) => string;
455
- guideFound: string;
456
441
  closeAria: string;
457
442
  poweredBy: string;
458
443
  };
@@ -970,4 +955,4 @@ interface LauncherProps {
970
955
  }
971
956
  export declare function Launcher({ inline }?: LauncherProps): import("react").JSX.Element | null;
972
957
  //#endregion
973
- export type { ActiveTour, Anchor, AnchorElementState, AnchorMatch, AskArticle, AskFailure, AskOutcome, AskQuestion, AskReply, AskResult, AskScript, AskStage, BootstrapAnswer, BootstrapData, BootstrapOff, ComponentSettings, GuideContent, GuideStep, GuideType, GuidesApi, GuidesTransport, HttpTransportConfig, LabelOverrides, Labels, LauncherList, LauncherOptions, LauncherProps, MemoryTransportOptions, Message, Palette, PinPosition, ScriptedReply, StepPlacement, StepPrimitive, SunboardOverlayProps, SunboardProviderProps, Theme, ThemeMode, ThemeModeSetting, TourEnd, TourEnding, UserState, VisitorDetails };
958
+ export type { ActiveTour, Anchor, AnchorElementState, AnchorMatch, AskFailure, AskOutcome, AskQuestion, AskReply, AskResult, AskScript, AskStage, BootstrapAnswer, BootstrapData, BootstrapOff, ComponentSettings, GuideContent, GuideStep, GuideType, GuidesApi, GuidesTransport, HttpTransportConfig, LabelOverrides, Labels, LauncherList, LauncherOptions, LauncherProps, MemoryTransportOptions, Message, Palette, PinPosition, ScriptedReply, StepPlacement, StepPrimitive, SunboardOverlayProps, SunboardProviderProps, Theme, ThemeMode, ThemeModeSetting, TourEnd, TourEnding, UserState, VisitorDetails };
package/dist/index.mjs CHANGED
@@ -3,7 +3,6 @@
3
3
  import { createContext, useCallback, useContext, useEffect, useId, useLayoutEffect, useMemo, useReducer, useRef, useState, useSyncExternalStore } from "react";
4
4
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
5
5
  import { FloatingArrow, FloatingPortal, arrow, autoUpdate, flip, offset, shift, useFloating } from "@floating-ui/react";
6
- import { flushSync } from "react-dom";
7
6
  //#region src/debug.ts
8
7
  const noLog = () => {};
9
8
  function consoleLogger() {
@@ -220,7 +219,6 @@ const enLabels = {
220
219
  cancel: "Cancel",
221
220
  send: "Send",
222
221
  latest: "Latest",
223
- sourcesCount: (count) => `${count} ${count === 1 ? "source" : "sources"}`,
224
222
  greeting: (name) => name ? `Hey ${name},` : "Hey there,",
225
223
  guideClosing: "I put together a guide for you that walks you through it.",
226
224
  tourFinishedAsk: "Tour finished, how was your experience?",
@@ -236,7 +234,6 @@ const enLabels = {
236
234
  checking: "Checking it against the docs",
237
235
  slow: "Taking a little longer than usual"
238
236
  },
239
- sources: "Sources",
240
237
  commonQuestions: "Common questions",
241
238
  showMeHow: "Show me how",
242
239
  newTab: "(opens in a new tab)",
@@ -249,8 +246,6 @@ const enLabels = {
249
246
  lostBody: "Check your connection and try again.",
250
247
  tryAgain: "Try again",
251
248
  notCovered: (product) => `The ${product} help docs don't cover this yet.`,
252
- coveredBy: (article, heading) => heading ? `The section "${heading}" of the article "${article}" covers this.` : `The article "${article}" covers this.`,
253
- guideFound: "There is a guide for this.",
254
249
  closeAria: "Close help",
255
250
  poweredBy: "Powered by"
256
251
  },
@@ -1654,39 +1649,20 @@ function stringOrUndefined(value) {
1654
1649
  function optionalString(value) {
1655
1650
  return value === void 0 || typeof value === "string";
1656
1651
  }
1657
- function parseSource(value) {
1658
- if (!isRecord$1(value)) return void 0;
1659
- const { id, title, url, heading } = value;
1660
- if (typeof id !== "string" || typeof title !== "string") return void 0;
1661
- if (!optionalString(url) || !optionalString(heading)) return void 0;
1662
- return {
1663
- id,
1664
- title,
1665
- ...url === void 0 ? {} : { url },
1666
- ...heading === void 0 ? {} : { heading }
1667
- };
1668
- }
1669
1652
  /** A reply as the server sends it, or as the thread kept it in session storage. */
1670
1653
  function parseReply(value) {
1671
1654
  if (!isRecord$1(value)) return void 0;
1672
- const { id, conversationId, firstTurn, outcome, answer, sources, guideKey, supportUrl } = value;
1655
+ const { id, conversationId, firstTurn, outcome, answer, guideKey, supportUrl } = value;
1673
1656
  if (typeof id !== "string" || typeof conversationId !== "string") return void 0;
1674
1657
  if (typeof firstTurn !== "boolean") return void 0;
1675
1658
  if (!isAskOutcome(outcome) || typeof answer !== "string") return void 0;
1676
- if (!Array.isArray(sources) || !optionalString(guideKey) || !optionalString(supportUrl)) return;
1677
- const parsed = [];
1678
- for (const source of sources) {
1679
- const article = parseSource(source);
1680
- if (!article) return void 0;
1681
- parsed.push(article);
1682
- }
1659
+ if (!optionalString(guideKey) || !optionalString(supportUrl)) return void 0;
1683
1660
  return {
1684
1661
  id,
1685
1662
  conversationId,
1686
1663
  firstTurn,
1687
1664
  outcome,
1688
1665
  answer,
1689
- sources: parsed,
1690
1666
  ...guideKey === void 0 ? {} : { guideKey },
1691
1667
  ...supportUrl === void 0 ? {} : { supportUrl }
1692
1668
  };
@@ -2720,12 +2696,6 @@ function PlayIcon() {
2720
2696
  children: /* @__PURE__ */ jsx("path", { d: "M7 4.5v15l12-7.5z" })
2721
2697
  });
2722
2698
  }
2723
- function DocIcon() {
2724
- return /* @__PURE__ */ jsxs(Glyph, {
2725
- width: 1.8,
2726
- children: [/* @__PURE__ */ jsx("path", { d: "M14 3H7a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V8z" }), /* @__PURE__ */ jsx("path", { d: "M14 3v5h5M9 13h6M9 17h4" })]
2727
- });
2728
- }
2729
2699
  function ExternalIcon() {
2730
2700
  return /* @__PURE__ */ jsx(Glyph, {
2731
2701
  width: 1.8,
@@ -2763,12 +2733,6 @@ function RestartIcon() {
2763
2733
  function SendIcon() {
2764
2734
  return /* @__PURE__ */ jsx(Glyph, { children: /* @__PURE__ */ jsx("path", { d: "M12 19V5M5 12l7-7 7 7" }) });
2765
2735
  }
2766
- function ChevronDownIcon() {
2767
- return /* @__PURE__ */ jsx(Glyph, {
2768
- width: 1.8,
2769
- children: /* @__PURE__ */ jsx("path", { d: "m6 9 6 6 6-6" })
2770
- });
2771
- }
2772
2736
  function CheckIcon() {
2773
2737
  return /* @__PURE__ */ jsx(Glyph, {
2774
2738
  width: 1.8,
@@ -4022,16 +3986,6 @@ function QuestionPanel({ inline, launcher, close, focusBox }) {
4022
3986
  followRef.current = false;
4023
3987
  measure();
4024
3988
  }, [messages]);
4025
- const keepInPlace = (change) => {
4026
- const box = scrollRef.current;
4027
- const before = box ? {
4028
- height: box.scrollHeight,
4029
- top: box.scrollTop
4030
- } : null;
4031
- flushSync(change);
4032
- if (box && before && anchored) box.scrollTop = before.top - (box.scrollHeight - before.height);
4033
- measure();
4034
- };
4035
3989
  useEffect(() => {
4036
3990
  const seen = seenRef.current;
4037
3991
  let finished = null;
@@ -4040,7 +3994,7 @@ function QuestionPanel({ inline, launcher, close, focusBox }) {
4040
3994
  if (message.kind === "waiting") continue;
4041
3995
  seen.add(message.id);
4042
3996
  if (message.kind === "reply") {
4043
- const { greeting, closing } = replyWords(message, listed, api.firstName, L);
3997
+ const { greeting, closing } = replyWords(message, api.firstName, L);
4044
3998
  finished = [
4045
3999
  greeting,
4046
4000
  plainAnswer(message.reply.answer),
@@ -4230,10 +4184,9 @@ function QuestionPanel({ inline, launcher, close, focusBox }) {
4230
4184
  latest: item.question.id === latestQuestion,
4231
4185
  fresh: !seenRef.current.has(item.response.id),
4232
4186
  guide: guideFor(item.response, listed),
4233
- words: replyWords(item.response, listed, api.firstName, L),
4187
+ words: replyWords(item.response, api.firstName, L),
4234
4188
  labels: L,
4235
- onShowMe: inline ? void 0 : start,
4236
- keepInPlace
4189
+ onShowMe: inline ? void 0 : start
4237
4190
  }, item.response.id),
4238
4191
  item.response?.kind === "error" && /* @__PURE__ */ jsx(FailureNote, {
4239
4192
  message: item.response,
@@ -4311,16 +4264,15 @@ function guideFor(message, listed) {
4311
4264
  return key === void 0 ? void 0 : listed.find((guide) => guide.key === key);
4312
4265
  }
4313
4266
  /**
4314
- * The assistant's own lines around a written answer: the greeting on the
4315
- * first reply of a conversation, and the closing under one that offers a
4316
- * guide. A reply with nothing to offer gets neither.
4267
+ * The assistant's own lines around a reply: the greeting on the first reply
4268
+ * of a conversation that offers a guide or an answer, and the closing under
4269
+ * any reply that offers a guide.
4317
4270
  */
4318
- function replyWords(message, listed, name, labels) {
4319
- const { reply } = message;
4320
- if (reply.outcome === "NONE") return {};
4271
+ function replyWords({ reply }, name, labels) {
4272
+ const offers = reply.outcome === "GUIDE" || reply.outcome === "ANSWERED";
4321
4273
  return {
4322
- greeting: reply.firstTurn ? labels.greeting(name) : void 0,
4323
- closing: guideFor(message, listed) ? labels.guideClosing : void 0
4274
+ greeting: reply.firstTurn && offers ? labels.greeting(name) : void 0,
4275
+ closing: reply.guideKey !== void 0 ? labels.guideClosing : void 0
4324
4276
  };
4325
4277
  }
4326
4278
  const SUGGESTION_LIMIT = 4;
@@ -4414,7 +4366,7 @@ function Waiting({ stage, since, labels, productName }) {
4414
4366
  ]
4415
4367
  });
4416
4368
  }
4417
- function ReplyArticle({ message, latest, fresh, guide, words, labels, onShowMe, keepInPlace }) {
4369
+ function ReplyArticle({ message, latest, fresh, guide, words, labels, onShowMe }) {
4418
4370
  const { reply } = message;
4419
4371
  const primary = latest && !message.toured;
4420
4372
  const [arrived] = useState(fresh);
@@ -4428,7 +4380,7 @@ function ReplyArticle({ message, latest, fresh, guide, words, labels, onShowMe,
4428
4380
  className: "sb-reply-greeting",
4429
4381
  children: words.greeting
4430
4382
  }),
4431
- /* @__PURE__ */ jsx(AnswerText, { text: reply.answer }),
4383
+ reply.answer !== "" && /* @__PURE__ */ jsx(AnswerText, { text: reply.answer }),
4432
4384
  words.closing && /* @__PURE__ */ jsx("p", {
4433
4385
  className: "sb-reply-closing",
4434
4386
  children: words.closing
@@ -4444,7 +4396,7 @@ function ReplyArticle({ message, latest, fresh, guide, words, labels, onShowMe,
4444
4396
  children: [/* @__PURE__ */ jsx(PlayIcon, {}), labels.showMeHow]
4445
4397
  })
4446
4398
  }),
4447
- reply.outcome === "NONE" && reply.supportUrl && /* @__PURE__ */ jsxs("div", {
4399
+ reply.supportUrl && /* @__PURE__ */ jsxs("div", {
4448
4400
  className: "sb-support",
4449
4401
  children: [/* @__PURE__ */ jsx("span", { children: labels.supportOffer }), /* @__PURE__ */ jsxs("a", {
4450
4402
  className: "sb-support-link",
@@ -4460,82 +4412,10 @@ function ReplyArticle({ message, latest, fresh, guide, words, labels, onShowMe,
4460
4412
  })
4461
4413
  ]
4462
4414
  })]
4463
- }),
4464
- reply.sources.length > 0 && (latest ? /* @__PURE__ */ jsx(Sources, {
4465
- sources: reply.sources,
4466
- labels
4467
- }) : /* @__PURE__ */ jsx(FoldedSources, {
4468
- sources: reply.sources,
4469
- labels,
4470
- keepInPlace
4471
- }))
4415
+ })
4472
4416
  ]
4473
4417
  });
4474
4418
  }
4475
- function Sources({ sources, labels }) {
4476
- return /* @__PURE__ */ jsx("div", {
4477
- className: "sb-sources",
4478
- role: "group",
4479
- "aria-label": labels.sources,
4480
- children: /* @__PURE__ */ jsx(SourceRows, {
4481
- sources,
4482
- labels
4483
- })
4484
- });
4485
- }
4486
- function FoldedSources({ sources, labels, keepInPlace }) {
4487
- const [open, setOpen] = useState(false);
4488
- return /* @__PURE__ */ jsxs("div", {
4489
- className: "sb-sources",
4490
- role: "group",
4491
- "aria-label": labels.sources,
4492
- children: [/* @__PURE__ */ jsxs("button", {
4493
- type: "button",
4494
- className: "sb-sources-toggle",
4495
- "aria-expanded": open,
4496
- onClick: () => keepInPlace(() => setOpen((now) => !now)),
4497
- children: [
4498
- /* @__PURE__ */ jsx(DocIcon, {}),
4499
- /* @__PURE__ */ jsx("span", { children: labels.sourcesCount(sources.length) }),
4500
- /* @__PURE__ */ jsx(ChevronDownIcon, {})
4501
- ]
4502
- }), open && /* @__PURE__ */ jsx(SourceRows, {
4503
- sources,
4504
- labels
4505
- })]
4506
- });
4507
- }
4508
- function SourceRows({ sources, labels }) {
4509
- return /* @__PURE__ */ jsx(Fragment, { children: sources.map((source) => {
4510
- const name = /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(DocIcon, {}), /* @__PURE__ */ jsxs("span", {
4511
- className: "sb-doc-text",
4512
- children: [source.heading !== void 0 && /* @__PURE__ */ jsx("span", {
4513
- className: "sb-doc-trail",
4514
- children: `${source.title} › `
4515
- }), /* @__PURE__ */ jsx("span", {
4516
- className: "sb-doc-title",
4517
- children: source.heading ?? source.title
4518
- })]
4519
- })] });
4520
- return source.url === void 0 ? /* @__PURE__ */ jsx("div", {
4521
- className: "sb-doc",
4522
- children: name
4523
- }, source.id) : /* @__PURE__ */ jsxs("a", {
4524
- className: "sb-doc",
4525
- href: source.url,
4526
- target: "_blank",
4527
- rel: "noreferrer",
4528
- children: [
4529
- name,
4530
- /* @__PURE__ */ jsx(ExternalIcon, {}),
4531
- /* @__PURE__ */ jsx("span", {
4532
- className: "sb-sr",
4533
- children: labels.newTab
4534
- })
4535
- ]
4536
- }, source.id);
4537
- }) });
4538
- }
4539
4419
  function FailureNote({ message, fresh, labels, onRetry }) {
4540
4420
  const { failure } = message;
4541
4421
  const lost = failure.kind === "lost";
package/dist/styles.css CHANGED
@@ -560,74 +560,6 @@
560
560
  color: var(--sb-accent);
561
561
  }
562
562
 
563
- .sb-root .sb-sources {
564
- display: flex;
565
- flex-direction: column;
566
- border: 1px solid var(--sb-border);
567
- border-radius: var(--sb-r2);
568
- overflow: hidden;
569
- }
570
- .sb-root .sb-doc,
571
- .sb-root .sb-sources-toggle {
572
- width: 100%;
573
- min-height: 44px;
574
- display: flex;
575
- align-items: center;
576
- gap: 10px;
577
- padding: 0 12px;
578
- font-size: calc(var(--sb-size-base) - 1px);
579
- }
580
- .sb-root .sb-doc {
581
- color: var(--sb-text);
582
- text-decoration: none;
583
- }
584
- .sb-root .sb-doc + .sb-doc,
585
- .sb-root .sb-sources-toggle + .sb-doc {
586
- border-top: 1px solid var(--sb-border);
587
- }
588
- .sb-root a.sb-doc:hover,
589
- .sb-root .sb-sources-toggle:hover {
590
- background: var(--sb-surface-hover);
591
- }
592
- .sb-root .sb-doc > svg {
593
- flex-shrink: 0;
594
- width: 15px;
595
- height: 15px;
596
- color: var(--sb-text-muted);
597
- }
598
- .sb-root .sb-doc-text + svg {
599
- width: 14px;
600
- height: 14px;
601
- }
602
- .sb-root .sb-doc-text {
603
- flex: 1;
604
- min-width: 0;
605
- overflow: hidden;
606
- text-overflow: ellipsis;
607
- white-space: nowrap;
608
- }
609
- .sb-root .sb-doc-trail {
610
- color: var(--sb-text-muted);
611
- }
612
- .sb-root .sb-sources-toggle {
613
- border: 0;
614
- background: transparent;
615
- color: var(--sb-text-muted);
616
- text-align: left;
617
- cursor: pointer;
618
- }
619
- .sb-root .sb-sources-toggle span {
620
- flex: 1;
621
- }
622
- .sb-root .sb-sources-toggle svg {
623
- flex-shrink: 0;
624
- width: 15px;
625
- height: 15px;
626
- }
627
- .sb-root .sb-sources-toggle[aria-expanded="true"] svg:last-child {
628
- transform: rotate(180deg);
629
- }
630
-
631
563
  .sb-root .sb-support {
632
564
  display: flex;
633
565
  align-items: center;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sunboard/react",
3
- "version": "1.0.0",
3
+ "version": "1.1.0",
4
4
  "description": "Product tours, tooltips and a Help launcher with an assistant, for your React app. Sunboard creates the guides from your docs and keeps them working as your app changes.",
5
5
  "license": "MIT",
6
6
  "homepage": "https://sunboardhq.com",