@guidelytech/guide-runtime 0.5.0 → 0.6.2

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.
@@ -20,19 +20,162 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
20
20
  // src/messenger.ts
21
21
  var messenger_exports = {};
22
22
  __export(messenger_exports, {
23
+ personalGreeting: () => personalGreeting,
23
24
  relativeTime: () => relativeTime,
24
25
  renderMessenger: () => renderMessenger
25
26
  });
26
27
  module.exports = __toCommonJS(messenger_exports);
28
+ var import_runtime4 = require("@guidelytech/shared/runtime");
27
29
 
28
30
  // src/help-view.ts
29
- var import_runtime2 = require("@guidelytech/shared/runtime");
31
+ var import_runtime3 = require("@guidelytech/shared/runtime");
30
32
 
31
- // src/sanitize.ts
33
+ // src/pdf-viewer.ts
32
34
  var import_runtime = require("@guidelytech/shared/runtime");
35
+
36
+ // src/host.ts
37
+ function prefersReducedMotion() {
38
+ try {
39
+ return typeof window !== "undefined" && typeof window.matchMedia === "function" && window.matchMedia("(prefers-reduced-motion: reduce)").matches;
40
+ } catch {
41
+ return false;
42
+ }
43
+ }
44
+
45
+ // src/pdf-viewer.ts
46
+ function enhancePdfViewers(root, doc = root.ownerDocument) {
47
+ for (const block of Array.from(root.querySelectorAll(`div.${import_runtime.PDF_BLOCK_CLASS}`))) {
48
+ const link2 = block.querySelector(`a.${import_runtime.FILE_LINK_CLASS}`);
49
+ const strip = block.querySelector(`.${import_runtime.PDF_PAGES_CLASS}`);
50
+ if (!link2 || !strip || block.hasAttribute("data-g-ready")) continue;
51
+ for (const img of Array.from(strip.querySelectorAll("img:not([src])"))) img.remove();
52
+ const pages = Array.from(strip.querySelectorAll("img"));
53
+ if (!pages.length) {
54
+ strip.remove();
55
+ continue;
56
+ }
57
+ block.setAttribute("data-g-ready", "");
58
+ const name = (link2.textContent || "").trim() || "PDF";
59
+ block.setAttribute("role", "group");
60
+ block.setAttribute("aria-label", `${name}, ${pages.length} ${pages.length === 1 ? "page" : "pages"}`);
61
+ link2.setAttribute("aria-label", `Read ${name} full screen`);
62
+ link2.addEventListener("click", (e) => {
63
+ e.preventDefault();
64
+ openReader(doc, block, link2, pages, 0);
65
+ });
66
+ const viewport = doc.createElement("div");
67
+ viewport.className = "g-pdf-viewport";
68
+ strip.replaceWith(viewport);
69
+ viewport.appendChild(strip);
70
+ strip.tabIndex = 0;
71
+ strip.setAttribute("aria-label", `${name} pages`);
72
+ const counter = doc.createElement("span");
73
+ counter.className = "g-pdf-count";
74
+ counter.setAttribute("aria-live", "polite");
75
+ const prev = navButton(doc, "prev", "Previous page", "\u2039");
76
+ const next = navButton(doc, "next", "Next page", "\u203A");
77
+ viewport.append(prev, next, counter);
78
+ const stride = () => Math.max(1, pages.length > 1 ? pages[1].offsetLeft - pages[0].offsetLeft : strip.clientWidth);
79
+ const current = () => Math.round(strip.scrollLeft / stride());
80
+ const go = (i) => strip.scrollTo({ left: Math.max(0, Math.min(pages.length - 1, i)) * stride(), behavior: prefersReducedMotion() ? "auto" : "smooth" });
81
+ const update = () => {
82
+ const i = Math.min(pages.length - 1, current());
83
+ counter.textContent = `${i + 1} / ${pages.length}`;
84
+ prev.disabled = i === 0;
85
+ next.disabled = i >= pages.length - 1;
86
+ };
87
+ prev.addEventListener("click", () => go(current() - 1));
88
+ next.addEventListener("click", () => go(current() + 1));
89
+ strip.addEventListener("scroll", update, { passive: true });
90
+ strip.addEventListener("keydown", (e) => {
91
+ if (e.key === "ArrowRight" || e.key === "ArrowLeft") {
92
+ e.preventDefault();
93
+ go(current() + (e.key === "ArrowRight" ? 1 : -1));
94
+ } else if (e.key === "Enter") openReader(doc, block, link2, pages, current());
95
+ });
96
+ pages.forEach((img, i) => img.addEventListener("click", () => openReader(doc, block, link2, pages, i)));
97
+ if (pages.length === 1) viewport.classList.add("g-pdf-single");
98
+ update();
99
+ }
100
+ }
101
+ function navButton(doc, cls, label, text) {
102
+ const b = doc.createElement("button");
103
+ b.type = "button";
104
+ b.className = `g-pdf-nav g-pdf-${cls}`;
105
+ b.setAttribute("aria-label", label);
106
+ b.textContent = text;
107
+ return b;
108
+ }
109
+ function openReader(doc, block, link2, pages, start) {
110
+ const host = block.closest(".g-scope") ?? doc.body;
111
+ const returnFocus = block.getRootNode().activeElement;
112
+ const name = (link2.textContent || "").trim() || "PDF";
113
+ const reader = doc.createElement("div");
114
+ reader.className = "g-pdf-reader";
115
+ reader.setAttribute("role", "dialog");
116
+ reader.setAttribute("aria-modal", "true");
117
+ reader.setAttribute("aria-label", name);
118
+ const bar = doc.createElement("div");
119
+ bar.className = "g-pdf-bar";
120
+ const title = doc.createElement("span");
121
+ title.className = "g-pdf-title";
122
+ title.textContent = name;
123
+ const original = doc.createElement("a");
124
+ original.className = "g-btn g-btn-secondary";
125
+ original.href = link2.href;
126
+ original.target = "_blank";
127
+ original.rel = "noopener noreferrer";
128
+ original.textContent = "Open PDF \u2197";
129
+ const close = doc.createElement("button");
130
+ close.type = "button";
131
+ close.className = "g-close g-pdf-close";
132
+ close.setAttribute("aria-label", "Close");
133
+ close.textContent = "\xD7";
134
+ bar.append(title, original, close);
135
+ const scroller = doc.createElement("div");
136
+ scroller.className = "g-pdf-scroll";
137
+ scroller.tabIndex = 0;
138
+ for (const page of pages) {
139
+ const img = doc.createElement("img");
140
+ img.src = page.src;
141
+ img.alt = page.alt;
142
+ img.decoding = "async";
143
+ for (const dim of ["width", "height"]) if (page.hasAttribute(dim)) img.setAttribute(dim, page.getAttribute(dim));
144
+ scroller.appendChild(img);
145
+ }
146
+ reader.append(bar, scroller);
147
+ const onKey = (e) => {
148
+ if (e.key === "Escape") {
149
+ e.stopPropagation();
150
+ done();
151
+ } else if (e.key === "Tab") {
152
+ const items = [original, close, scroller];
153
+ const at = items.indexOf(reader.getRootNode().activeElement);
154
+ e.preventDefault();
155
+ items[(at + (e.shiftKey ? items.length - 1 : 1)) % items.length].focus();
156
+ }
157
+ };
158
+ const done = () => {
159
+ reader.remove();
160
+ returnFocus?.focus?.({ preventScroll: true });
161
+ };
162
+ close.addEventListener("click", done);
163
+ reader.addEventListener("keydown", onKey);
164
+ host.appendChild(reader);
165
+ close.focus();
166
+ const target = scroller.children[start];
167
+ if (target && start > 0) {
168
+ const land = () => scroller.scrollTop = target.offsetTop - 12;
169
+ requestAnimationFrame(land);
170
+ if (!target.hasAttribute("height")) for (const img of Array.from(scroller.children).slice(0, start)) img.addEventListener("load", land, { once: true });
171
+ }
172
+ }
173
+
174
+ // src/sanitize.ts
175
+ var import_runtime2 = require("@guidelytech/shared/runtime");
33
176
  var ALLOWED_TAGS = /* @__PURE__ */ new Set(["p", "br", "strong", "b", "em", "i", "u", "a", "ul", "ol", "li", "img", "h1", "h2", "h3", "h4", "code", "pre", "blockquote", "span", "div", "hr", "button"]);
34
177
  var DROP_TAGS = /* @__PURE__ */ new Set(["script", "style", "object", "embed", "form", "input", "textarea", "select", "link", "meta", "base", "template", "svg", "math", "frame", "frameset", "noscript"]);
35
- var ALLOWED_ATTRS = /* @__PURE__ */ new Set(["href", "src", "alt", "title", "target", "rel", "class", "data-g-action", "data-g-value"]);
178
+ var ALLOWED_ATTRS = /* @__PURE__ */ new Set(["href", "src", "alt", "title", "target", "rel", "class", "width", "height", "data-g-action", "data-g-value"]);
36
179
  function isSafeUrl(value, attr, tag) {
37
180
  const v = value.trim().replace(/[\u0000-\u001f\s]/g, "").toLowerCase();
38
181
  if (!v) return false;
@@ -59,6 +202,7 @@ function sanitizeElement(el2) {
59
202
  }
60
203
  if (tag === "button") el2.setAttribute("type", "button");
61
204
  if (tag === "img") {
205
+ for (const dim of ["width", "height"]) if (el2.hasAttribute(dim) && !/^\d{1,5}$/.test(el2.getAttribute(dim))) el2.removeAttribute(dim);
62
206
  el2.setAttribute("loading", "lazy");
63
207
  if (!el2.getAttribute("alt")) el2.setAttribute("alt", "");
64
208
  }
@@ -69,18 +213,18 @@ function rebuildMedia(el2) {
69
213
  const src = el2.getAttribute("src");
70
214
  let out;
71
215
  if (tag === "iframe") {
72
- if (!(0, import_runtime.isAllowedEmbedSrc)(src)) return null;
216
+ if (!(0, import_runtime2.isAllowedEmbedSrc)(src)) return null;
73
217
  out = doc.createElement("iframe");
74
218
  out.setAttribute("src", src);
75
219
  out.setAttribute("title", el2.getAttribute("title") || "Embedded video");
76
- for (const [k, v] of Object.entries(import_runtime.EMBED_IFRAME_ATTRS)) out.setAttribute(k, v);
220
+ for (const [k, v] of Object.entries(import_runtime2.EMBED_IFRAME_ATTRS)) out.setAttribute(k, v);
77
221
  } else {
78
- if (!(0, import_runtime.isSafeMediaUrl)(src)) return null;
222
+ if (!(0, import_runtime2.isSafeMediaUrl)(src)) return null;
79
223
  out = doc.createElement("video");
80
224
  out.setAttribute("src", src);
81
225
  const poster = el2.getAttribute("poster");
82
- if ((0, import_runtime.isSafeMediaUrl)(poster)) out.setAttribute("poster", poster);
83
- for (const [k, v] of Object.entries(import_runtime.VIDEO_ATTRS)) out.setAttribute(k, v);
226
+ if ((0, import_runtime2.isSafeMediaUrl)(poster)) out.setAttribute("poster", poster);
227
+ for (const [k, v] of Object.entries(import_runtime2.VIDEO_ATTRS)) out.setAttribute(k, v);
84
228
  }
85
229
  return out;
86
230
  }
@@ -132,10 +276,10 @@ function el(doc, tag, cls, text) {
132
276
  if (text != null) e.textContent = text;
133
277
  return e;
134
278
  }
135
- function listItem(doc, icon, title, summary) {
279
+ function listItem(doc, icon2, title, summary) {
136
280
  const b = el(doc, "button", "g-rc-item");
137
281
  b.type = "button";
138
- const i = el(doc, "span", "g-rc-icon", icon);
282
+ const i = el(doc, "span", "g-rc-icon", icon2);
139
283
  i.setAttribute("aria-hidden", "true");
140
284
  const text = el(doc, "span");
141
285
  text.appendChild(el(doc, "div", "g-rc-title", title));
@@ -197,8 +341,10 @@ function createHelpView(options) {
197
341
  const title = el(doc, "h3", "g-title", article.title);
198
342
  const content = el(doc, "div", "g-body");
199
343
  if (loading) content.appendChild(el(doc, "div", "g-rc-empty", "Loading\u2026"));
200
- else if (article.body) setSanitizedHtml(content, article.body);
201
- else content.appendChild(el(doc, "div", "g-rc-empty", "This article couldn't be loaded."));
344
+ else if (article.body) {
345
+ setSanitizedHtml(content, article.body);
346
+ enhancePdfViewers(content, doc);
347
+ } else content.appendChild(el(doc, "div", "g-rc-empty", "This article couldn't be loaded."));
202
348
  wrap.append(back, title, content);
203
349
  body.appendChild(wrap);
204
350
  back.focus?.({ preventScroll: true });
@@ -218,19 +364,29 @@ function createHelpView(options) {
218
364
  if (active) return renderArticle(active);
219
365
  const data = options.data();
220
366
  const member = options.audienceIds?.() ?? [];
221
- const sections = (0, import_runtime2.resourceCenterSections)(data.collections, data.articles, member);
367
+ const sections = (0, import_runtime3.resourceCenterSections)(data.collections, data.articles, member);
222
368
  const q = query.trim().toLowerCase();
369
+ body.append(...options.extra?.(q) ?? []);
223
370
  if (q) {
224
371
  const visible = sections.flatMap((s) => s.articles);
225
372
  const local = visible.filter((a) => a.title.toLowerCase().includes(q) || a.summary.toLowerCase().includes(q));
226
373
  const seen = new Set(local.map((a) => a.id));
227
374
  const hidden = new Set(data.articles.map((a) => a.id).filter((id) => !visible.some((v) => v.id === id)));
228
- const merged = [...local, ...serverResults.filter((a) => !seen.has(a.id) && !hidden.has(a.id) && (0, import_runtime2.isArticleListed)(data.collections, a, member))];
375
+ const merged = [...local, ...serverResults.filter((a) => !seen.has(a.id) && !hidden.has(a.id) && (0, import_runtime3.isArticleListed)(data.collections, a, member))];
229
376
  section(`Results (${merged.length})`);
230
377
  if (!merged.length) body.appendChild(el(doc, "div", "g-rc-empty", "No articles match your search."));
231
378
  merged.forEach(articleButton);
232
379
  return;
233
380
  }
381
+ if (data.suggested?.items.length) {
382
+ section(data.suggested.title);
383
+ const glyph = { guide: "\u25B6", howto: "\u2630", article: "\u{1F4C4}" };
384
+ data.suggested.items.forEach((item) => {
385
+ const b = listItem(doc, glyph[item.kind], item.title, item.summary);
386
+ b.addEventListener("click", item.run);
387
+ body.appendChild(b);
388
+ });
389
+ }
234
390
  if (data.recommended?.length) {
235
391
  section("Recommended");
236
392
  data.recommended.forEach((g) => {
@@ -330,6 +486,98 @@ function createHelpView(options) {
330
486
  };
331
487
  }
332
488
 
489
+ // src/howto-view.ts
490
+ var pct = (v) => `${(v * 100).toFixed(3)}%`;
491
+ var place = (node, r) => Object.assign(node.style, {
492
+ left: pct(r.x),
493
+ top: pct(r.y),
494
+ width: pct(r.width),
495
+ height: pct(r.height)
496
+ });
497
+ function paragraphs(doc, text, cls) {
498
+ const parts = text.split(/\n{2,}/).map((p) => p.trim()).filter(Boolean);
499
+ if (!parts.length) return null;
500
+ const wrap = el(doc, "div", cls);
501
+ for (const p of parts) wrap.appendChild(el(doc, "p", "", p));
502
+ return wrap;
503
+ }
504
+ function screenshot(doc, image, alt) {
505
+ if (!/^https?:\/\//i.test(image.url)) return null;
506
+ const frame = el(doc, "div", "g-howto-shot");
507
+ const img = (cls) => {
508
+ const i = el(doc, "img", cls);
509
+ i.src = image.url;
510
+ i.alt = alt;
511
+ i.loading = "lazy";
512
+ i.decoding = "async";
513
+ return i;
514
+ };
515
+ const crop = image.crop;
516
+ if (crop && crop.width > 0 && crop.height > 0) {
517
+ const ghost = img("g-howto-ghost");
518
+ ghost.alt = "";
519
+ ghost.setAttribute("aria-hidden", "true");
520
+ const zoomed = img("g-howto-zoomed");
521
+ Object.assign(zoomed.style, {
522
+ width: pct(1 / crop.width),
523
+ left: pct(-crop.x / crop.width),
524
+ top: pct(-crop.y / crop.height)
525
+ });
526
+ frame.append(ghost, zoomed);
527
+ } else frame.appendChild(img(""));
528
+ if (image.highlight) {
529
+ const mark = el(doc, "div", "g-howto-mark");
530
+ mark.setAttribute("aria-hidden", "true");
531
+ place(mark, image.highlight);
532
+ frame.appendChild(mark);
533
+ }
534
+ return frame;
535
+ }
536
+ function renderHowTo(doc, howTo, onShowMe) {
537
+ const wrap = el(doc, "div", "g-howto");
538
+ wrap.appendChild(el(doc, "h3", "g-title", howTo.title));
539
+ const description = paragraphs(doc, howTo.description, "g-howto-desc");
540
+ if (description) wrap.appendChild(description);
541
+ const guideKey = howTo.guideKey;
542
+ if (guideKey && onShowMe) {
543
+ const show = el(doc, "button", "g-btn g-btn-primary g-howto-show", "\u25B6 Show me in the app");
544
+ show.type = "button";
545
+ show.addEventListener("click", () => onShowMe(guideKey));
546
+ wrap.appendChild(show);
547
+ }
548
+ const list = el(doc, "ol", "g-howto-steps");
549
+ let n = 0;
550
+ for (const s of howTo.steps) {
551
+ const li = el(doc, "li", `g-howto-step g-howto-${s.kind}`);
552
+ if (s.kind === "heading") li.appendChild(el(doc, "h4", "", s.title));
553
+ else if (s.kind === "tip" || s.kind === "alert") {
554
+ li.appendChild(el(doc, "strong", "", s.title || (s.kind === "tip" ? "Tip" : "Important")));
555
+ const body = paragraphs(doc, s.body, "");
556
+ if (body) li.appendChild(body);
557
+ } else if (s.kind === "text") {
558
+ if (s.title) li.appendChild(el(doc, "strong", "", s.title));
559
+ const body = paragraphs(doc, s.body, "g-howto-note");
560
+ if (body) li.appendChild(body);
561
+ } else {
562
+ const row = el(doc, "div", "g-howto-row");
563
+ const num = el(doc, "span", "g-howto-num", String(++n));
564
+ const text = el(doc, "div", "g-howto-text");
565
+ text.appendChild(el(doc, "p", "g-howto-title", s.title));
566
+ const body = paragraphs(doc, s.body, "g-howto-note");
567
+ if (body) text.appendChild(body);
568
+ row.append(num, text);
569
+ li.appendChild(row);
570
+ const shot = s.image ? screenshot(doc, s.image, s.title) : null;
571
+ if (shot) li.appendChild(shot);
572
+ }
573
+ list.appendChild(li);
574
+ }
575
+ if (!list.childNodes.length)
576
+ wrap.appendChild(el(doc, "div", "g-rc-empty", "This how-to has no steps yet."));
577
+ else wrap.appendChild(list);
578
+ return wrap;
579
+ }
580
+
333
581
  // src/messenger-styles.ts
334
582
  var MESSENGER_STYLES = `
335
583
  :host([data-g-messenger="bottom-right"]) .g-launcher.g-pos-bottom-right{right:72px}
@@ -344,17 +592,16 @@ var MESSENGER_STYLES = `
344
592
  .g-m-head .g-close{color:inherit;top:50%;transform:translateY(-50%)}
345
593
  .g-m-head .g-close:hover,.g-m-head .g-close:focus-visible{background:rgba(255,255,255,.18);color:inherit}
346
594
  .g-m-head h2{margin:0;font-size:16px;font-weight:600;flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
347
- .g-m-back{border:0;background:transparent;color:inherit;font:inherit;font-size:18px;cursor:pointer;width:32px;height:32px;border-radius:999px;margin-left:-8px}
595
+ .g-m-back{border:0;background:transparent;color:inherit;font:inherit;cursor:pointer;width:32px;height:32px;border-radius:999px;margin-left:-8px;display:inline-flex;align-items:center;justify-content:center;padding:0}
348
596
  .g-m-back:hover,.g-m-back:focus-visible{background:rgba(255,255,255,.18);outline:none}
349
597
  .g-m-main{flex:1;min-height:0;display:flex;flex-direction:column;overflow:hidden}
350
598
  .g-m-scroll{flex:1;overflow:auto;padding:16px}
351
- .g-m-hero{font-size:22px;font-weight:700;margin:4px 0 4px}
352
- .g-m-greet{color:var(--g-muted);margin:0 0 16px}
353
599
  .g-m-card{display:block;width:100%;text-align:left;border:1px solid var(--g-border);background:var(--g-bg);color:var(--g-text);border-radius:calc(var(--g-radius) * .8);padding:14px 16px;margin:0 0 12px;font:inherit;cursor:pointer;box-shadow:0 1px 2px rgba(0,0,0,.04)}
354
600
  .g-m-card:hover{background:var(--g-subtle)}
355
601
  .g-m-card:focus-visible{outline:2px solid var(--g-primary);outline-offset:2px}
356
- .g-m-card-title{font-weight:600}
357
- .g-m-card-sub{color:var(--g-muted);font-size:13px;margin-top:2px}
602
+ .g-m-card-title{display:block;font-weight:600}
603
+ .g-m-card-sub{display:block;color:var(--g-muted);font-size:13px;margin-top:2px}
604
+ .g-m-card-label{display:block;font-size:12px;color:var(--g-muted);font-weight:500}
358
605
  .g-m-row{display:flex;align-items:flex-start;gap:10px}
359
606
  .g-m-row .g-m-grow{flex:1;min-width:0}
360
607
  .g-m-ellipsis{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
@@ -364,7 +611,9 @@ var MESSENGER_STYLES = `
364
611
  .g-m-tab{flex:1;border:0;background:transparent;color:var(--g-muted);font:inherit;font-size:12px;font-weight:600;padding:10px 4px 12px;cursor:pointer;display:flex;flex-direction:column;align-items:center;gap:2px}
365
612
  .g-m-tab[aria-selected="true"]{color:var(--g-primary)}
366
613
  .g-m-tab:focus-visible{outline:2px solid var(--g-primary);outline-offset:-2px}
367
- .g-m-tab-icon{font-size:18px;line-height:1}
614
+ .g-m-tab-icon{position:relative;display:inline-flex;line-height:1}
615
+ .g-m-tab-icon .g-m-ico{width:22px;height:22px}
616
+ .g-m-tab-dot{position:absolute;top:-2px;right:-4px;width:8px;height:8px;border-radius:999px;background:#e11d48;border:2px solid var(--g-bg);box-sizing:content-box}
368
617
  .g-m-log{flex:1;overflow:auto;padding:16px;display:flex;flex-direction:column;gap:10px}
369
618
  .g-m-msg{max-width:85%;display:flex;flex-direction:column;gap:4px}
370
619
  .g-m-msg.g-from-user{align-self:flex-end;align-items:flex-end}
@@ -393,7 +642,8 @@ var MESSENGER_STYLES = `
393
642
  .g-m-compose{display:flex;align-items:flex-end;gap:8px}
394
643
  .g-m-compose textarea{flex:1;resize:none;min-height:40px;max-height:120px;border:1px solid var(--g-border);border-radius:10px;padding:9px 10px;font:inherit;font-size:14px;color:var(--g-text);background:var(--g-bg)}
395
644
  .g-m-compose textarea:focus-visible{outline:2px solid var(--g-primary);outline-offset:1px}
396
- .g-m-send{width:40px;height:40px;border-radius:999px;border:0;background:var(--g-primary);color:var(--g-primary-text);cursor:pointer;font-size:18px;flex:none}
645
+ .g-m-send{width:40px;height:40px;border-radius:999px;border:0;background:var(--g-primary);color:var(--g-primary-text);cursor:pointer;flex:none;display:inline-flex;align-items:center;justify-content:center;padding:0}
646
+ .g-m-send .g-m-ico{width:18px;height:18px}
397
647
  .g-m-send:disabled{opacity:.45;cursor:not-allowed}
398
648
  .g-m-send:focus-visible{outline:2px solid var(--g-primary);outline-offset:2px}
399
649
  .g-m-human{border:0;background:transparent;color:var(--g-muted);font:inherit;font-size:12px;cursor:pointer;padding:6px 2px 0;text-decoration:underline}
@@ -403,6 +653,79 @@ var MESSENGER_STYLES = `
403
653
  .g-m-rate button:hover,.g-m-rate button:focus-visible{background:var(--g-subtle);outline:none}
404
654
  .g-m-note{color:var(--g-muted);font-size:12px;margin-top:6px}
405
655
  .g-m-empty{color:var(--g-muted);text-align:center;padding:24px 8px}
656
+ .g-howto .g-title{margin:0 0 6px}
657
+ .g-howto-desc,.g-howto-note{color:var(--g-muted);font-size:13px}
658
+ .g-howto p{margin:0 0 6px;white-space:pre-line}
659
+ .g-howto-show{width:100%;margin:6px 0 4px}
660
+ .g-howto-steps{list-style:none;margin:12px 0 0;padding:0;display:flex;flex-direction:column;gap:16px}
661
+ .g-howto-step h4{margin:4px 0 0;font-size:15px}
662
+ .g-howto-tip,.g-howto-alert{border-left:3px solid var(--g-primary);background:var(--g-subtle);border-radius:6px;padding:8px 10px;font-size:13px}
663
+ .g-howto-alert{border-left-color:#d97706}
664
+ .g-howto-row{display:flex;gap:10px;align-items:flex-start}
665
+ .g-howto-num{flex:none;width:24px;height:24px;border-radius:999px;background:var(--g-primary);color:var(--g-primary-text);font-size:12px;font-weight:700;display:inline-flex;align-items:center;justify-content:center}
666
+ .g-howto-text{flex:1;min-width:0;padding-top:2px}
667
+ .g-howto-title{font-weight:600}
668
+ .g-howto-shot{position:relative;overflow:hidden;margin-top:8px;border:1px solid var(--g-border);border-radius:8px;background:var(--g-subtle)}
669
+ .g-howto-shot img{display:block;width:100%;height:auto}
670
+ .g-howto-shot .g-howto-ghost{visibility:hidden}
671
+ .g-howto-shot .g-howto-zoomed{position:absolute;max-width:none}
672
+ .g-howto-mark{position:absolute;pointer-events:none;border:3px solid var(--g-primary);border-radius:6px;box-shadow:0 0 0 3px color-mix(in srgb,var(--g-primary) 25%,transparent)}
673
+ .g-m-ico{width:20px;height:20px;fill:none;stroke:currentColor;stroke-width:1.8;stroke-linecap:round;stroke-linejoin:round;flex:none}
674
+ .g-close .g-m-ico,.g-m-back .g-m-ico{width:18px;height:18px}
675
+ .g-m-row.g-m-center{align-items:center}
676
+ .g-m-row.g-m-baseline{align-items:baseline}
677
+ .g-m-static{cursor:default}
678
+ .g-m-static:hover{background:var(--g-bg)}
679
+ .g-m-wide{width:100%}
680
+ .g-m-home{position:relative;padding:0}
681
+ .g-m-hero-bg{position:absolute;top:0;left:0;right:0;height:300px;pointer-events:none;background:linear-gradient(180deg,var(--g-primary) 0%,var(--g-primary) 52%,color-mix(in srgb,var(--g-primary) 30%,var(--g-bg)) 84%,var(--g-bg) 100%)}
682
+ .g-m-home-inner{position:relative;padding:16px;display:flex;flex-direction:column;gap:12px}
683
+ .g-m-home .g-m-card{margin:0;box-shadow:0 1px 2px rgba(0,0,0,.04),0 4px 14px rgba(17,24,39,.06)}
684
+ .g-m-hero-top{display:flex;align-items:center;justify-content:space-between;gap:12px;min-height:40px;padding:2px 4px 0}
685
+ .g-m-logo{width:36px;height:36px;border-radius:9px;background:var(--g-bg);color:var(--g-primary);display:inline-flex;align-items:center;justify-content:center;font-weight:700;font-size:17px;overflow:hidden;flex:none}
686
+ .g-m-logo img{width:100%;height:100%;object-fit:contain}
687
+ .g-m-team{display:flex}
688
+ .g-m-avatar{width:32px;height:32px;border-radius:999px;border:2px solid var(--g-primary);display:inline-flex;align-items:center;justify-content:center;font-size:12px;font-weight:700;overflow:hidden;margin-left:-8px;background:#fde7c7;color:#7a4a0a}
689
+ .g-m-avatar:first-child{margin-left:0}
690
+ .g-m-avatar-1{background:#d9f0e3;color:#1f5e3b}
691
+ .g-m-avatar-2{background:#e3e1fa;color:#3b3486}
692
+ .g-m-avatar img{width:100%;height:100%;object-fit:cover}
693
+ .g-m-hero-close{width:36px;height:36px;border:0;border-radius:999px;background:rgba(255,255,255,.16);color:var(--g-primary-text);display:inline-flex;align-items:center;justify-content:center;cursor:pointer;padding:0;margin-left:10px}
694
+ .g-m-hero-close .g-m-ico{width:18px;height:18px}
695
+ .g-m-hero-close:hover{background:rgba(255,255,255,.28)}
696
+ .g-m-hero-close:focus-visible{outline:2px solid var(--g-primary-text);outline-offset:2px}
697
+ .g-m-hello{margin:36px 4px 6px;color:var(--g-primary-text);font-size:26px;font-weight:700;line-height:1.2;letter-spacing:-.01em;display:flex;flex-direction:column}
698
+ .g-m-hello-1{opacity:.8}
699
+ .g-m-trail{color:var(--g-primary)}
700
+ .g-m-lead{color:var(--g-muted)}
701
+ .g-m-status-ico{width:26px;height:26px}
702
+ .g-m-status-operational .g-m-status-ico{color:#15803d}
703
+ .g-m-status-degraded .g-m-status-ico{color:#b45309}
704
+ .g-m-status-outage .g-m-status-ico{color:#b91c1c}
705
+ .g-m-status-maintenance .g-m-status-ico{color:#1d4ed8}
706
+ .g-m-bar{display:block;height:6px;border-radius:999px;background:var(--g-border);margin-top:10px;overflow:hidden}
707
+ .g-m-bar-fill{display:block;height:100%;background:var(--g-primary);border-radius:999px}
708
+ .g-m-link{color:var(--g-primary);font-weight:600;font-size:13px;flex:none}
709
+ .g-m-badge-ico{width:32px;height:32px;border-radius:8px;background:var(--g-subtle);color:color-mix(in srgb,var(--g-text) 78%,transparent);display:inline-flex;align-items:center;justify-content:center;flex:none}
710
+ .g-m-badge-ico .g-m-ico{width:16px;height:16px}
711
+ .g-m-badge-ico.g-m-accent{background:color-mix(in srgb,var(--g-primary) 12%,var(--g-bg));color:var(--g-primary)}
712
+ .g-m-suggest{padding-bottom:4px}
713
+ .g-m-items{display:flex;flex-direction:column;margin-top:8px}
714
+ .g-m-item{display:flex;align-items:center;gap:12px;width:100%;text-align:left;border:0;border-top:1px solid var(--g-border);background:transparent;color:var(--g-text);padding:10px 0;font:inherit;cursor:pointer;min-height:44px}
715
+ .g-m-item:hover .g-m-item-title{color:var(--g-primary)}
716
+ .g-m-item:focus-visible{outline:2px solid var(--g-primary);outline-offset:2px;border-radius:4px}
717
+ .g-m-item .g-m-grow{flex:1;min-width:0}
718
+ .g-m-item-title{display:block;font-weight:600;overflow-wrap:anywhere}
719
+ .g-m-news{padding:0;overflow:hidden}
720
+ .g-m-news-img{display:block;width:100%;height:150px;object-fit:cover;background:var(--g-subtle)}
721
+ .g-m-news-body{display:block;padding:14px 16px}
722
+ .g-m-news-meta{gap:8px;margin-bottom:4px}
723
+ .g-m-tag{font-size:11px;font-weight:700;color:var(--g-primary-text);background:var(--g-primary);border-radius:4px;padding:1px 6px;text-transform:uppercase;letter-spacing:.03em}
724
+ .g-m-clamp{display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden}
725
+ .g-m-context{align-self:center;font-size:12px;color:var(--g-muted);background:var(--g-subtle);border-radius:999px;padding:3px 10px}
726
+ .g-m-asks{align-items:flex-start}
727
+ .g-m-chip.g-m-ask{border-color:var(--g-primary);color:var(--g-primary);font-weight:600;border-radius:999px;padding:8px 14px;min-height:36px}
728
+ .g-m-chip.g-m-ask:disabled{opacity:.5;cursor:not-allowed}
406
729
  `;
407
730
 
408
731
  // src/render-safe.ts
@@ -491,6 +814,73 @@ function chatIcon(doc) {
491
814
  svg.appendChild(path);
492
815
  return svg;
493
816
  }
817
+ var ICONS = {
818
+ home: ["M3 10.5 12 3l9 7.5V20a1 1 0 0 1-1 1h-5v-6H9v6H4a1 1 0 0 1-1-1z"],
819
+ messages: ["M4 5h16v11H8l-4 4z"],
820
+ help: ["M12 3a9 9 0 1 0 0 18 9 9 0 0 0 0-18z", "M9.5 9.5a2.5 2.5 0 1 1 3.5 2.3c-.6.3-1 .8-1 1.5V14", "M12 17h.01"],
821
+ news: ["M3 11v2a1 1 0 0 0 1 1h2l5 4V6L6 10H4a1 1 0 0 0-1 1z", "M15 9a4 4 0 0 1 0 6", "M18 6a8 8 0 0 1 0 12"],
822
+ arrow: ["M5 12h14", "M13 6l6 6-6 6"],
823
+ close: ["M6 6l12 12", "M18 6 6 18"],
824
+ back: ["M15 6l-6 6 6 6"],
825
+ chevron: ["m9 6 6 6-6 6"],
826
+ check: ["M12 3a9 9 0 1 0 0 18 9 9 0 0 0 0-18z", "m8 12 3 3 5-6"],
827
+ alert: ["M12 3a9 9 0 1 0 0 18 9 9 0 0 0 0-18z", "M12 8v4", "M12 16h.01"],
828
+ wrench: ["M14.7 6.3a4 4 0 0 0-5.4 5.4L4 17l3 3 5.3-5.3a4 4 0 0 0 5.4-5.4l-2.5 2.5-2.4-.6-.6-2.4z"],
829
+ play: ["M8 5v14l11-7z"],
830
+ steps: ["M9 6h11", "M9 12h11", "M9 18h11", "M4 6h.01", "M4 12h.01", "M4 18h.01"],
831
+ article: ["M6 3h9l4 4v14H6z", "M9 12h6", "M9 16h6"],
832
+ ask: ["M4 5h16v11H8l-4 4z", "M9 9.5h6", "M9 12.5h4"],
833
+ search: ["M11 4a7 7 0 1 0 0 14 7 7 0 0 0 0-14z", "m20 20-3.5-3.5"],
834
+ circle: ["M12 3a9 9 0 1 0 0 18 9 9 0 0 0 0-18z"]
835
+ };
836
+ function icon(doc, name, cls = "g-m-ico") {
837
+ const svg = doc.createElementNS(SVG_NS, "svg");
838
+ svg.setAttribute("viewBox", "0 0 24 24");
839
+ svg.setAttribute("aria-hidden", "true");
840
+ svg.setAttribute("class", cls);
841
+ for (const d of ICONS[name] ?? []) {
842
+ const path = doc.createElementNS(SVG_NS, "path");
843
+ path.setAttribute("d", d);
844
+ svg.appendChild(path);
845
+ }
846
+ return svg;
847
+ }
848
+ var REPLY_TIME_TEXT = {
849
+ minutes: "typically replies in a few minutes",
850
+ hour: "typically replies within an hour",
851
+ hours: "typically replies in a few hours",
852
+ day: "typically replies within a day"
853
+ };
854
+ var STATUS_TEXT = {
855
+ operational: { title: "All systems operational", icon: "check" },
856
+ degraded: { title: "Degraded performance", icon: "alert" },
857
+ outage: { title: "Service outage", icon: "alert" },
858
+ maintenance: { title: "Scheduled maintenance", icon: "wrench" }
859
+ };
860
+ function shortDateTime(iso) {
861
+ const d = new Date(iso);
862
+ if (Number.isNaN(d.getTime())) return "";
863
+ try {
864
+ return d.toLocaleString(void 0, { month: "short", day: "numeric", hour: "2-digit", minute: "2-digit" });
865
+ } catch {
866
+ return d.toISOString().slice(0, 16).replace("T", " ");
867
+ }
868
+ }
869
+ function shortDate(iso) {
870
+ const d = new Date(iso);
871
+ if (Number.isNaN(d.getTime())) return "";
872
+ try {
873
+ return d.toLocaleDateString(void 0, { month: "short", day: "numeric" });
874
+ } catch {
875
+ return d.toISOString().slice(0, 10);
876
+ }
877
+ }
878
+ var initialOf = (name) => (name.trim().match(/\p{L}/u)?.[0] ?? "?").toUpperCase();
879
+ function personalGreeting(greeting, firstName) {
880
+ if (!firstName) return greeting;
881
+ const rest = greeting.replace(/^\s*(hi|hello|hey)( there)?\s*[!,.]?\s*/i, "");
882
+ return rest ? `Hi ${firstName}! ${rest}` : `Hi ${firstName}!`;
883
+ }
494
884
  function relativeTime(iso, now = Date.now()) {
495
885
  const s = Math.max(0, Math.round((now - Date.parse(iso)) / 1e3));
496
886
  if (!Number.isFinite(s) || s < 60) return "now";
@@ -526,6 +916,25 @@ function renderMessenger(options) {
526
916
  let threadTimer = null;
527
917
  let backgroundTimer = null;
528
918
  let threadSeq = 0;
919
+ let howTos = null;
920
+ let howTosSeq = 0;
921
+ const howToCache = /* @__PURE__ */ new Map();
922
+ let page = null;
923
+ let pageKey = null;
924
+ let pageAt = 0;
925
+ let pageSeq = 0;
926
+ let pageLoading = false;
927
+ const PAGE_TTL_MS = 12e4;
928
+ const cards = { suggestions: config.home?.suggestions !== false, progress: config.home?.progress !== false, news: config.home?.news !== false };
929
+ const visitor = () => {
930
+ try {
931
+ const v = options.visitor?.();
932
+ if (v) return v;
933
+ } catch {
934
+ }
935
+ return { firstName: null, path: null, pageGuides: [], resumeGuide: null, checklist: null, news: [] };
936
+ };
937
+ const news = () => cards.news ? visitor().news : [];
529
938
  const pos = `g-pos-${config.launcher.position}`;
530
939
  const rootNode = scope.getRootNode();
531
940
  const shadowHost = "host" in rootNode ? rootNode.host : null;
@@ -557,6 +966,7 @@ function renderMessenger(options) {
557
966
  data: () => ({
558
967
  collections: articles?.collections ?? options.resourceCenter?.collections ?? [],
559
968
  articles: articles?.articles ?? options.resourceCenter?.articles ?? [],
969
+ suggested: helpSuggestions(),
560
970
  recommended: rcExtras(options.resourceCenter?.recommendedGuideIds),
561
971
  checklists: rcExtras(options.resourceCenter?.checklistGuideIds),
562
972
  faqs: options.resourceCenter?.faqs,
@@ -573,7 +983,8 @@ function renderMessenger(options) {
573
983
  setOpen(false);
574
984
  options.onOpenChecklist?.(id);
575
985
  },
576
- onArticleView: options.onArticleView
986
+ onArticleView: options.onArticleView,
987
+ extra: (q) => howToItems(q)
577
988
  });
578
989
  const unreadTotal = () => conversations.reduce((n, c) => n + (c.unread > 0 ? 1 : 0), 0);
579
990
  const updateBadge = () => {
@@ -603,6 +1014,32 @@ function renderMessenger(options) {
603
1014
  }).catch(() => {
604
1015
  }).finally(() => articlesLoading = false);
605
1016
  };
1017
+ const loadHowTos = () => {
1018
+ if (!api.listHowTos) return;
1019
+ const seq = ++howTosSeq;
1020
+ api.listHowTos().then((res) => {
1021
+ if (seq !== howTosSeq || destroyed) return;
1022
+ howTos = res;
1023
+ if (open && view.kind === "tab" && view.tab === "home") render();
1024
+ else if (open && view.kind === "tab" && view.tab === "help") help.render();
1025
+ }).catch(() => {
1026
+ });
1027
+ };
1028
+ const pageHowTos = () => (howTos?.forPage ?? []).map((id) => howTos?.howTos.find((h) => h.id === id)).filter((h) => !!h);
1029
+ const stepsLabel = (h) => `${h.stepCount} ${h.stepCount === 1 ? "step" : "steps"}`;
1030
+ const openHowTo = (id) => go({ kind: "howto", id, from: view });
1031
+ function howToItems(q) {
1032
+ const list = q ? (howTos?.howTos ?? []).filter((h) => h.title.toLowerCase().includes(q) || h.description.toLowerCase().includes(q)).slice(0, 10) : [];
1033
+ if (!list.length) return [];
1034
+ return [
1035
+ el(doc, "div", "g-rc-section", `How-tos (${list.length})`),
1036
+ ...list.map((h) => {
1037
+ const b = listItem(doc, "\u{1F4CB}", h.title, h.description || stepsLabel(h));
1038
+ b.addEventListener("click", () => openHowTo(h.id));
1039
+ return b;
1040
+ })
1041
+ ];
1042
+ }
606
1043
  const refreshConversations = () => api.listConversations().then((list) => {
607
1044
  conversations = [...list].sort((a, b) => b.lastMessageAt.localeCompare(a.lastMessageAt));
608
1045
  updateBadge();
@@ -611,92 +1048,379 @@ function renderMessenger(options) {
611
1048
  });
612
1049
  function renderHead(title, back) {
613
1050
  head.textContent = "";
1051
+ head.hidden = false;
614
1052
  if (back) {
615
- const b = el(doc, "button", "g-m-back", "\u2190");
1053
+ const b = el(doc, "button", "g-m-back");
616
1054
  b.type = "button";
617
1055
  b.setAttribute("aria-label", "Back");
1056
+ b.appendChild(icon(doc, "back"));
618
1057
  b.addEventListener("click", back);
619
1058
  head.appendChild(b);
620
1059
  }
621
1060
  head.appendChild(el(doc, "h2", "", title));
622
- const close = el(doc, "button", "g-close", "\xD7");
1061
+ head.appendChild(closeButton("g-close"));
1062
+ }
1063
+ function closeButton(cls) {
1064
+ const close = el(doc, "button", cls);
623
1065
  close.type = "button";
624
1066
  close.setAttribute("aria-label", "Close");
1067
+ close.appendChild(icon(doc, "close"));
625
1068
  close.addEventListener("click", () => setOpen(false));
626
- head.appendChild(close);
1069
+ return close;
627
1070
  }
628
1071
  function renderTabs() {
629
1072
  tabs.textContent = "";
630
- tabs.hidden = view.kind === "thread";
1073
+ tabs.hidden = view.kind !== "tab";
631
1074
  const items = [
632
- ["home", "\u2302", "Home"],
633
- ["messages", "\u{1F4AC}", "Messages"],
634
- ["help", "?", "Help"]
1075
+ ["home", "home", "Home"],
1076
+ ["messages", "messages", "Messages"],
1077
+ ["help", "help", "Help"]
635
1078
  ];
636
- for (const [tab, icon, label] of items) {
1079
+ const latest = news();
1080
+ if (latest.length) items.push(["news", "news", "News"]);
1081
+ for (const [tab, ico, label] of items) {
637
1082
  const b = el(doc, "button", "g-m-tab");
638
1083
  b.type = "button";
639
1084
  b.setAttribute("role", "tab");
640
1085
  b.setAttribute("aria-selected", String(view.kind === "tab" && view.tab === tab));
641
- const i = el(doc, "span", "g-m-tab-icon", icon);
642
- i.setAttribute("aria-hidden", "true");
1086
+ const i = el(doc, "span", "g-m-tab-icon");
1087
+ i.appendChild(icon(doc, ico));
643
1088
  b.append(i, el(doc, "span", "", label));
1089
+ if (tab === "news" && latest.some((n) => n.unread)) {
1090
+ const dot = el(doc, "span", "g-m-tab-dot");
1091
+ dot.setAttribute("aria-label", "New");
1092
+ i.appendChild(dot);
1093
+ }
644
1094
  b.addEventListener("click", () => go({ kind: "tab", tab }));
645
1095
  tabs.appendChild(b);
646
1096
  }
647
1097
  }
648
- function conversationCard(c) {
1098
+ function conversationCard(c, label) {
649
1099
  const card = el(doc, "button", "g-m-card");
650
1100
  card.type = "button";
651
1101
  const row = el(doc, "div", "g-m-row");
652
1102
  if (c.unread > 0) row.appendChild(el(doc, "span", "g-m-dot"));
653
1103
  const text = el(doc, "div", "g-m-grow");
1104
+ if (label) text.appendChild(el(doc, "div", "g-m-card-label", label));
654
1105
  text.append(el(doc, "div", "g-m-card-title g-m-ellipsis", c.subject || "Conversation"), el(doc, "div", "g-m-card-sub g-m-ellipsis", c.lastMessagePreview));
655
1106
  row.append(text, el(doc, "span", "g-m-time", relativeTime(c.lastMessageAt)));
656
1107
  card.appendChild(row);
657
- card.setAttribute("aria-label", `${c.subject || "Conversation"}${c.unread ? ", unread" : ""}`);
1108
+ card.setAttribute("aria-label", `${label ? `${label}: ` : ""}${c.subject || "Conversation"}${c.unread ? ", unread" : ""}`);
658
1109
  card.addEventListener("click", () => go({ kind: "thread", conversationId: c.id }));
659
1110
  return card;
660
1111
  }
1112
+ function actionCard(opts) {
1113
+ const card = el(doc, "button", `g-m-card g-m-row g-m-center${opts.cls ? ` ${opts.cls}` : ""}`);
1114
+ card.type = "button";
1115
+ if (opts.lead) card.appendChild(opts.lead);
1116
+ const text = el(doc, "span", "g-m-grow");
1117
+ text.appendChild(el(doc, "span", "g-m-card-title", opts.title));
1118
+ if (opts.sub) text.appendChild(el(doc, "span", "g-m-card-sub", opts.sub));
1119
+ card.appendChild(text);
1120
+ if (opts.trail) card.appendChild(icon(doc, "arrow", "g-m-ico g-m-trail"));
1121
+ card.addEventListener("click", opts.onClick);
1122
+ return card;
1123
+ }
1124
+ function progressBar(done, total) {
1125
+ const bar = el(doc, "span", "g-m-bar");
1126
+ bar.setAttribute("role", "progressbar");
1127
+ bar.setAttribute("aria-valuemin", "0");
1128
+ bar.setAttribute("aria-valuemax", String(total));
1129
+ bar.setAttribute("aria-valuenow", String(done));
1130
+ const fill = el(doc, "span", "g-m-bar-fill");
1131
+ fill.style.width = `${total ? Math.round(Math.min(done, total) / total * 100) : 0}%`;
1132
+ bar.appendChild(fill);
1133
+ return bar;
1134
+ }
1135
+ function teamAvatars() {
1136
+ const team = (config.team ?? []).slice(0, 3);
1137
+ if (!team.length) return null;
1138
+ const wrap = el(doc, "div", "g-m-team");
1139
+ wrap.setAttribute("role", "img");
1140
+ wrap.setAttribute("aria-label", `The team: ${team.map((t) => t.name).join(", ")}`);
1141
+ team.forEach((t, i) => {
1142
+ const a = el(doc, "span", `g-m-avatar g-m-avatar-${i % 3}`);
1143
+ if (t.avatarUrl) {
1144
+ const img = el(doc, "img");
1145
+ img.src = t.avatarUrl;
1146
+ img.alt = "";
1147
+ img.referrerPolicy = "no-referrer";
1148
+ img.addEventListener("error", () => {
1149
+ img.remove();
1150
+ a.textContent = initialOf(t.name);
1151
+ });
1152
+ a.appendChild(img);
1153
+ } else a.textContent = initialOf(t.name);
1154
+ wrap.appendChild(a);
1155
+ });
1156
+ return wrap;
1157
+ }
1158
+ function brandMark() {
1159
+ const name = options.brand?.name || config.name;
1160
+ const mark = el(doc, "span", "g-m-logo");
1161
+ const url = options.brand?.logoUrl;
1162
+ if (url && /^(https?:)?\/\//.test(url)) {
1163
+ const img = el(doc, "img");
1164
+ img.src = url;
1165
+ img.alt = name;
1166
+ img.addEventListener("error", () => {
1167
+ img.remove();
1168
+ mark.textContent = initialOf(name);
1169
+ });
1170
+ mark.appendChild(img);
1171
+ } else {
1172
+ mark.textContent = initialOf(name);
1173
+ mark.setAttribute("aria-label", name);
1174
+ mark.setAttribute("role", "img");
1175
+ }
1176
+ return mark;
1177
+ }
1178
+ function messageSub() {
1179
+ const reply = config.replyTime ? REPLY_TIME_TEXT[config.replyTime] : null;
1180
+ if (config.aiAvailable) return reply ? `${config.name} answers right away. The team ${reply}.` : `${config.name} answers right away, and the team can help too.`;
1181
+ return reply ? `The team ${reply}.` : "The team will reply here.";
1182
+ }
1183
+ function statusCard(status) {
1184
+ const meta = STATUS_TEXT[status.level] ?? STATUS_TEXT.operational;
1185
+ const card = el(doc, "div", `g-m-card g-m-static g-m-row g-m-center g-m-status g-m-status-${status.level}`);
1186
+ card.setAttribute("role", "status");
1187
+ card.appendChild(icon(doc, meta.icon, "g-m-ico g-m-status-ico"));
1188
+ const text = el(doc, "span", "g-m-grow");
1189
+ text.appendChild(el(doc, "span", "g-m-card-title", status.message || meta.title));
1190
+ const when = shortDateTime(status.updatedAt);
1191
+ if (when) text.appendChild(el(doc, "span", "g-m-card-sub", `Updated ${when}`));
1192
+ card.appendChild(text);
1193
+ return card;
1194
+ }
1195
+ function progressCards(v) {
1196
+ const out = [];
1197
+ if (v.resumeGuide) {
1198
+ const g = v.resumeGuide;
1199
+ const card = el(doc, "button", "g-m-card");
1200
+ card.type = "button";
1201
+ const row = el(doc, "span", "g-m-row g-m-center");
1202
+ row.appendChild(iconTile("play", true));
1203
+ const text = el(doc, "span", "g-m-grow");
1204
+ text.append(el(doc, "span", "g-m-card-label", "Pick up where you left off"), el(doc, "span", "g-m-card-title g-m-ellipsis", g.name), el(doc, "span", "g-m-card-sub", `Step ${g.stepIndex + 1} of ${g.stepCount}`));
1205
+ row.append(text, el(doc, "span", "g-m-link", "Resume"));
1206
+ card.append(row, progressBar(g.stepIndex, g.stepCount));
1207
+ card.addEventListener("click", () => {
1208
+ setOpen(false);
1209
+ options.onResumeGuide?.(g.id, g.stepId);
1210
+ });
1211
+ out.push(card);
1212
+ }
1213
+ if (v.checklist && v.checklist.total > 0) {
1214
+ const c = v.checklist;
1215
+ const card = el(doc, "button", "g-m-card");
1216
+ card.type = "button";
1217
+ const top = el(doc, "span", "g-m-row g-m-baseline");
1218
+ top.append(el(doc, "span", "g-m-card-title g-m-grow g-m-ellipsis", c.name), el(doc, "span", "g-m-time", `${c.done} of ${c.total} done`));
1219
+ card.append(top, progressBar(c.done, c.total));
1220
+ if (c.nextTask) card.appendChild(el(doc, "span", "g-m-card-sub", `Next: ${c.nextTask}`));
1221
+ card.addEventListener("click", () => {
1222
+ setOpen(false);
1223
+ options.onOpenChecklist?.(c.id);
1224
+ });
1225
+ out.push(card);
1226
+ }
1227
+ return out;
1228
+ }
1229
+ function iconTile(name, accent = false) {
1230
+ const b = el(doc, "span", `g-m-badge-ico${accent ? " g-m-accent" : ""}`);
1231
+ b.appendChild(icon(doc, name));
1232
+ return b;
1233
+ }
1234
+ function pageSuggestions(v, max = 4) {
1235
+ if (!cards.suggestions) return [];
1236
+ const out = [];
1237
+ for (const g of v.pageGuides.slice(0, 2)) {
1238
+ out.push({ icon: "play", title: g.name, sub: g.kind === "checklist" ? "Checklist" : "Interactive guide", accent: true, run: () => g.kind === "checklist" ? openChecklist(g.id) : startGuide(g.key || g.id) });
1239
+ }
1240
+ const inApp = /* @__PURE__ */ new Set();
1241
+ for (const h of pageHowTos()) {
1242
+ if (out.length >= max) break;
1243
+ inApp.add(h.title.toLowerCase());
1244
+ out.push({ icon: "steps", title: h.title, sub: `How-to \xB7 ${stepsLabel(h)}`, run: () => openHowTo(h.id) });
1245
+ }
1246
+ for (const s of page?.suggestions ?? []) {
1247
+ if (out.length >= max) break;
1248
+ if (s.type === "howto" && inApp.has(s.title.toLowerCase())) continue;
1249
+ if (s.type === "howto") out.push({ icon: "steps", title: s.title, sub: `How-to \xB7 ${s.steps} ${s.steps === 1 ? "step" : "steps"}`, run: () => openArticle(s.articleId) });
1250
+ else out.push({ icon: "article", title: s.title, sub: s.summary || "Article", run: () => openArticle(s.articleId) });
1251
+ }
1252
+ return out.slice(0, max);
1253
+ }
1254
+ function startGuide(keyOrId) {
1255
+ setOpen(false);
1256
+ options.onStartGuide?.(keyOrId);
1257
+ }
1258
+ function openChecklist(id) {
1259
+ setOpen(false);
1260
+ options.onOpenChecklist?.(id);
1261
+ }
1262
+ function ask(question) {
1263
+ go({ kind: "thread", conversationId: null });
1264
+ if (!textarea) return;
1265
+ textarea.value = question;
1266
+ submit();
1267
+ }
1268
+ function suggestionButton(s) {
1269
+ const b = el(doc, "button", "g-m-item");
1270
+ b.type = "button";
1271
+ b.appendChild(iconTile(s.icon, s.accent));
1272
+ const text = el(doc, "span", "g-m-grow");
1273
+ text.append(el(doc, "span", "g-m-item-title", s.title), el(doc, "span", "g-m-card-sub g-m-ellipsis", s.sub));
1274
+ b.appendChild(text);
1275
+ b.addEventListener("click", s.run);
1276
+ return b;
1277
+ }
1278
+ function newsCard(n) {
1279
+ const card = el(doc, "button", "g-m-card g-m-news");
1280
+ card.type = "button";
1281
+ if (n.imageUrl && /^https?:\/\//.test(n.imageUrl)) {
1282
+ const img = el(doc, "img", "g-m-news-img");
1283
+ img.src = n.imageUrl;
1284
+ img.alt = "";
1285
+ img.loading = "lazy";
1286
+ img.addEventListener("error", () => img.remove());
1287
+ card.appendChild(img);
1288
+ }
1289
+ const body = el(doc, "span", "g-m-news-body");
1290
+ const meta = el(doc, "span", "g-m-row g-m-center g-m-news-meta");
1291
+ if (n.unread) meta.appendChild(el(doc, "span", "g-m-tag", "New"));
1292
+ meta.appendChild(el(doc, "span", "g-m-time", shortDate(n.publishedAt)));
1293
+ body.append(meta, el(doc, "span", "g-m-card-title", n.title));
1294
+ if (n.body) body.appendChild(el(doc, "span", "g-m-card-sub g-m-clamp", n.body));
1295
+ card.appendChild(body);
1296
+ card.addEventListener("click", () => startGuide(n.key || n.id));
1297
+ return card;
1298
+ }
661
1299
  function renderHome() {
662
- renderHead(config.name);
663
- const scroll = el(doc, "div", "g-m-scroll");
664
- scroll.append(el(doc, "div", "g-m-hero", "Hi there \u{1F44B}"), el(doc, "p", "g-m-greet", config.greeting));
665
- const ask = el(doc, "button", "g-m-card");
666
- ask.type = "button";
667
- ask.append(el(doc, "div", "g-m-card-title", "Ask a question"), el(doc, "div", "g-m-card-sub", config.aiAvailable ? `${config.name} answers right away, and the team can help too` : "The team will reply here"));
668
- ask.addEventListener("click", () => go({ kind: "thread", conversationId: null }));
669
- scroll.appendChild(ask);
1300
+ head.hidden = true;
1301
+ const v = visitor();
1302
+ loadPage();
1303
+ const scroll = el(doc, "div", "g-m-scroll g-m-home");
1304
+ const hero = el(doc, "div", "g-m-hero-bg");
1305
+ hero.setAttribute("aria-hidden", "true");
1306
+ const inner = el(doc, "div", "g-m-home-inner");
1307
+ const top = el(doc, "div", "g-m-hero-top");
1308
+ const right = el(doc, "div", "g-m-row g-m-center");
1309
+ const team = teamAvatars();
1310
+ if (team) right.appendChild(team);
1311
+ right.appendChild(closeButton("g-m-hero-close"));
1312
+ top.append(brandMark(), right);
1313
+ const hello = el(doc, "h2", "g-m-hello");
1314
+ hello.append(el(doc, "span", "g-m-hello-1", v.firstName ? `Hi ${v.firstName}.` : "Hi there."), el(doc, "span", "g-m-hello-2", "How can we help?"));
1315
+ inner.append(top, hello);
1316
+ inner.appendChild(actionCard({ title: "Send us a message", sub: messageSub(), trail: true, onClick: () => go({ kind: "thread", conversationId: null }) }));
670
1317
  const recent = conversations[0];
671
- if (recent) {
672
- scroll.appendChild(el(doc, "div", "g-rc-section", "Recent conversation"));
673
- scroll.appendChild(conversationCard(recent));
1318
+ if (recent) inner.appendChild(conversationCard(recent, recent.status === "closed" && !recent.unread ? "Recent conversation" : "Continue the conversation"));
1319
+ if (config.status) inner.appendChild(statusCard(config.status));
1320
+ if (cards.progress) progressCards(v).forEach((c) => inner.appendChild(c));
1321
+ const suggestions = pageSuggestions(v);
1322
+ const questions = cards.suggestions ? (page?.questions ?? []).slice(0, 2) : [];
1323
+ const label = v.path ? (0, import_runtime4.pageLabel)(v.path) : null;
1324
+ const fallback = !suggestions.length && !questions.length ? rcExtras(options.resourceCenter?.recommendedGuideIds).slice(0, 3) : [];
1325
+ if (suggestions.length || questions.length || fallback.length) {
1326
+ const box = el(doc, "div", "g-m-card g-m-static g-m-suggest");
1327
+ box.appendChild(el(doc, "div", "g-m-card-title", fallback.length ? "Recommended" : "Suggested for you"));
1328
+ if (!fallback.length && label) box.appendChild(el(doc, "div", "g-m-card-sub", `For the ${label} page`));
1329
+ const list = el(doc, "div", "g-m-items");
1330
+ suggestions.forEach((s) => list.appendChild(suggestionButton(s)));
1331
+ questions.forEach((q) => list.appendChild(suggestionButton({ icon: "ask", title: q, sub: config.aiAvailable ? `Ask ${config.name}` : "Ask the team", run: () => ask(q) })));
1332
+ fallback.forEach((g) => list.appendChild(suggestionButton({ icon: "play", title: g.name, sub: "Interactive guide", accent: true, run: () => startGuide(g.key || g.id) })));
1333
+ box.appendChild(list);
1334
+ inner.appendChild(box);
674
1335
  }
675
- const search = el(doc, "button", "g-m-card");
676
- search.type = "button";
677
- search.append(el(doc, "div", "g-m-card-title", "Search for help"), el(doc, "div", "g-m-card-sub", "Browse articles and guides"));
678
- search.addEventListener("click", () => go({ kind: "tab", tab: "help" }));
679
- scroll.appendChild(search);
1336
+ const latest = news()[0];
1337
+ if (latest) inner.appendChild(newsCard(latest));
1338
+ inner.appendChild(actionCard({ title: "Search for help", sub: "Articles, guides and how-tos", lead: icon(doc, "search", "g-m-ico g-m-lead"), onClick: () => go({ kind: "tab", tab: "help" }) }));
1339
+ scroll.append(hero, inner);
1340
+ main.appendChild(scroll);
1341
+ }
1342
+ function renderHowToView(id, from) {
1343
+ const cached = howToCache.get(id);
1344
+ renderHead(cached?.title ?? "How-to", () => go(from));
1345
+ const scroll = el(doc, "div", "g-m-scroll");
1346
+ if (cached) {
1347
+ scroll.appendChild(
1348
+ renderHowTo(doc, cached, (key) => {
1349
+ setOpen(false);
1350
+ options.onStartGuide?.(key);
1351
+ })
1352
+ );
1353
+ } else scroll.appendChild(el(doc, "div", "g-m-empty", cached === null ? "This how-to isn't available any more." : "Loading\u2026"));
680
1354
  main.appendChild(scroll);
1355
+ if (cached !== void 0 || !api.getHowTo) return;
1356
+ const seq = threadSeq;
1357
+ api.getHowTo(id).catch(() => null).then((h) => {
1358
+ howToCache.set(id, h);
1359
+ if (seq === threadSeq && view.kind === "howto" && view.id === id) render();
1360
+ });
681
1361
  }
682
1362
  function renderMessagesTab() {
683
1363
  renderHead("Messages");
684
1364
  const scroll = el(doc, "div", "g-m-scroll");
685
1365
  if (!conversations.length) scroll.appendChild(el(doc, "div", "g-m-empty", "No conversations yet."));
686
1366
  conversations.forEach((c) => scroll.appendChild(conversationCard(c)));
687
- const start = el(doc, "button", "g-btn g-btn-primary", "Send us a message");
1367
+ const start = el(doc, "button", "g-btn g-btn-primary g-m-wide", "Send us a message");
688
1368
  start.type = "button";
689
- start.style.width = "100%";
690
1369
  start.addEventListener("click", () => go({ kind: "thread", conversationId: null }));
691
1370
  scroll.appendChild(start);
692
1371
  main.appendChild(scroll);
693
1372
  }
1373
+ function helpSuggestions() {
1374
+ const v = visitor();
1375
+ const items = pageSuggestions(v, 5).map((s) => ({ kind: s.icon === "play" ? "guide" : s.icon === "steps" ? "howto" : "article", title: s.title, summary: s.sub, run: s.run }));
1376
+ if (!items.length) return void 0;
1377
+ const label = v.path ? (0, import_runtime4.pageLabel)(v.path) : null;
1378
+ return { title: label ? `For the ${label} page` : "Suggested for you", items };
1379
+ }
694
1380
  function renderHelpTab() {
695
1381
  renderHead("Help");
696
1382
  loadArticles();
1383
+ loadPage();
697
1384
  help.render();
698
1385
  main.appendChild(help.el);
699
1386
  }
1387
+ function renderNewsTab() {
1388
+ renderHead("News");
1389
+ const scroll = el(doc, "div", "g-m-scroll");
1390
+ const items = news();
1391
+ if (!items.length) scroll.appendChild(el(doc, "div", "g-m-empty", "No news yet."));
1392
+ items.forEach((n) => scroll.appendChild(newsCard(n)));
1393
+ main.appendChild(scroll);
1394
+ const unread = items.filter((n) => n.unread).map((n) => n.id);
1395
+ if (unread.length) options.onNewsSeen?.(unread);
1396
+ }
1397
+ function loadPage(force = false) {
1398
+ const path = visitor().path;
1399
+ if (!path || !api.pageContent || !cards.suggestions) {
1400
+ page = null;
1401
+ pageKey = null;
1402
+ return;
1403
+ }
1404
+ const key = (0, import_runtime4.foldPathIds)(path).toLowerCase();
1405
+ if (!force && key === pageKey && (pageLoading || Date.now() - pageAt < PAGE_TTL_MS)) return;
1406
+ if (key !== pageKey) page = null;
1407
+ pageKey = key;
1408
+ pageLoading = true;
1409
+ const seq = ++pageSeq;
1410
+ api.pageContent(path).then((res) => {
1411
+ if (seq !== pageSeq) return;
1412
+ page = res;
1413
+ pageAt = Date.now();
1414
+ if (!open) return;
1415
+ if (view.kind === "tab" && view.tab === "home") render();
1416
+ else if (view.kind === "tab" && view.tab === "help") help.render();
1417
+ else if (view.kind === "thread" && !messages.length) renderLog();
1418
+ }).catch(() => {
1419
+ if (seq === pageSeq) pageAt = Date.now();
1420
+ }).finally(() => {
1421
+ if (seq === pageSeq) pageLoading = false;
1422
+ });
1423
+ }
700
1424
  let log = null;
701
1425
  let foot = null;
702
1426
  let textarea = null;
@@ -727,7 +1451,7 @@ function renderMessenger(options) {
727
1451
  if (m.status === "pending") bubble.setAttribute("aria-busy", "true");
728
1452
  bubble.dataset.messageId = m.id;
729
1453
  wrap.appendChild(bubble);
730
- if (m.citations.length || m.suggestedGuides.length) {
1454
+ if (m.citations.length || m.suggestedGuides.length || m.howTos?.length) {
731
1455
  const chips = el(doc, "div", "g-m-chips");
732
1456
  for (const g of m.suggestedGuides) {
733
1457
  const b = el(doc, "button", "g-m-chip g-m-guide", `\u25B6 Show me: ${g.name}`);
@@ -744,6 +1468,12 @@ function renderMessenger(options) {
744
1468
  b.addEventListener("click", () => openArticle(c.articleId));
745
1469
  chips.appendChild(b);
746
1470
  }
1471
+ for (const h of m.howTos ?? []) {
1472
+ const b = el(doc, "button", "g-m-chip", `\u{1F4CB} ${h.title}`);
1473
+ b.type = "button";
1474
+ b.addEventListener("click", () => openHowTo(h.howToId));
1475
+ chips.appendChild(b);
1476
+ }
747
1477
  wrap.appendChild(chips);
748
1478
  }
749
1479
  return wrap;
@@ -753,8 +1483,27 @@ function renderMessenger(options) {
753
1483
  const atBottom = log.scrollHeight - log.scrollTop - log.clientHeight < 40;
754
1484
  log.textContent = "";
755
1485
  if (!messages.length) {
756
- const intro = { id: "intro", author: config.aiAvailable ? "scout" : "agent", authorName: "Support team", body: config.greeting, citations: [], suggestedGuides: [], status: "complete", createdAt: (/* @__PURE__ */ new Date()).toISOString() };
1486
+ const v = visitor();
1487
+ const label = v.path ? (0, import_runtime4.pageLabel)(v.path) : null;
1488
+ if (label && config.aiAvailable && !current) log.appendChild(el(doc, "div", "g-m-context", `Asking from the ${label} page`));
1489
+ const intro = { id: "intro", author: config.aiAvailable ? "scout" : "agent", authorName: "Support team", body: personalGreeting(config.greeting, v.firstName), citations: [], suggestedGuides: [], status: "complete", createdAt: (/* @__PURE__ */ new Date()).toISOString() };
757
1490
  log.appendChild(messageNode(intro));
1491
+ const questions = !current && cards.suggestions ? page?.questions ?? [] : [];
1492
+ if (questions.length && !sending) {
1493
+ const chips = el(doc, "div", "g-m-chips g-m-asks");
1494
+ chips.setAttribute("aria-label", "Common questions on this page");
1495
+ for (const q of questions) {
1496
+ const b = el(doc, "button", "g-m-chip g-m-ask", q);
1497
+ b.type = "button";
1498
+ b.addEventListener("click", () => {
1499
+ if (!textarea || sending) return;
1500
+ textarea.value = q;
1501
+ submit();
1502
+ });
1503
+ chips.appendChild(b);
1504
+ }
1505
+ log.appendChild(chips);
1506
+ }
758
1507
  }
759
1508
  messages.forEach((m) => log.appendChild(messageNode(m)));
760
1509
  if (atBottom || sending) log.scrollTop = log.scrollHeight;
@@ -820,7 +1569,8 @@ function renderMessenger(options) {
820
1569
  textarea.placeholder = current?.status === "open" ? "Reply to the team\u2026" : "Ask a question\u2026";
821
1570
  textarea.setAttribute("aria-label", "Message");
822
1571
  textarea.value = draft;
823
- sendButton = el(doc, "button", "g-m-send", "\u2191");
1572
+ sendButton = el(doc, "button", "g-m-send");
1573
+ sendButton.appendChild(icon(doc, "arrow"));
824
1574
  sendButton.type = "submit";
825
1575
  sendButton.setAttribute("aria-label", "Send");
826
1576
  compose.append(textarea, sendButton);
@@ -850,9 +1600,14 @@ function renderMessenger(options) {
850
1600
  textarea = null;
851
1601
  sendButton = null;
852
1602
  if (view.kind === "thread") renderThread(view.conversationId);
1603
+ else if (view.kind === "howto") renderHowToView(view.id, view.from);
853
1604
  else if (view.tab === "home") renderHome();
854
1605
  else if (view.tab === "messages") renderMessagesTab();
855
- else renderHelpTab();
1606
+ else if (view.tab === "news" && news().length) renderNewsTab();
1607
+ else if (view.tab === "news") {
1608
+ view = { kind: "tab", tab: "home" };
1609
+ renderHome();
1610
+ } else renderHelpTab();
856
1611
  renderTabs();
857
1612
  }
858
1613
  const handlers = (seq) => ({
@@ -949,13 +1704,14 @@ function renderMessenger(options) {
949
1704
  threadSeq++;
950
1705
  if (threadTimer) clearTimeout(threadTimer);
951
1706
  threadTimer = null;
952
- if (view.kind === "tab" && view.tab === "help" && !(next.kind === "tab" && next.tab === "help")) help.reset();
1707
+ if (view.kind === "tab" && view.tab === "help" && !(next.kind === "tab" && next.tab === "help") && next.kind !== "howto") help.reset();
953
1708
  view = next;
954
1709
  if (next.kind === "thread") {
955
1710
  messages = [];
956
1711
  streaming.clear();
957
1712
  sending = false;
958
1713
  current = next.conversationId ? conversations.find((c) => c.id === next.conversationId) ?? null : null;
1714
+ if (!next.conversationId) loadPage();
959
1715
  render();
960
1716
  if (next.conversationId) {
961
1717
  const seq = threadSeq;
@@ -994,6 +1750,7 @@ function renderMessenger(options) {
994
1750
  if (v) {
995
1751
  render();
996
1752
  void refreshConversations();
1753
+ loadHowTos();
997
1754
  if (view.kind === "thread") scheduleThreadPoll();
998
1755
  options.onOpen?.();
999
1756
  } else {
@@ -1036,6 +1793,16 @@ function renderMessenger(options) {
1036
1793
  setOpen(true);
1037
1794
  openArticle(id);
1038
1795
  },
1796
+ refresh() {
1797
+ if (destroyed) return;
1798
+ if (!open) {
1799
+ pageKey = null;
1800
+ return;
1801
+ }
1802
+ loadPage();
1803
+ if (view.kind === "tab") render();
1804
+ else if (!messages.length) renderLog();
1805
+ },
1039
1806
  destroy() {
1040
1807
  destroyed = true;
1041
1808
  if (threadTimer) clearTimeout(threadTimer);
@@ -1049,6 +1816,7 @@ function renderMessenger(options) {
1049
1816
  }
1050
1817
  // Annotate the CommonJS export names for ESM import in node:
1051
1818
  0 && (module.exports = {
1819
+ personalGreeting,
1052
1820
  relativeTime,
1053
1821
  renderMessenger
1054
1822
  });