@guidelytech/guide-runtime 0.8.0 → 0.10.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.
@@ -25,7 +25,7 @@ __export(messenger_exports, {
25
25
  renderMessenger: () => renderMessenger
26
26
  });
27
27
  module.exports = __toCommonJS(messenger_exports);
28
- var import_runtime4 = require("@guidelytech/shared/runtime");
28
+ var import_runtime5 = require("@guidelytech/shared/runtime");
29
29
 
30
30
  // src/help-view.ts
31
31
  var import_runtime3 = require("@guidelytech/shared/runtime");
@@ -270,6 +270,38 @@ function setSanitizedHtml(el2, html) {
270
270
  }
271
271
 
272
272
  // src/help-view.ts
273
+ var SHOW_ME_KINDS = /* @__PURE__ */ new Set(["flow", "modal", "tooltip", "announcement"]);
274
+ function startableGuide(guides, keyOrId) {
275
+ if (!keyOrId || !guides) return null;
276
+ return guides.find((g) => (g.key === keyOrId || g.id === keyOrId) && SHOW_ME_KINDS.has(g.kind)) ?? null;
277
+ }
278
+ function resultRow(doc, icon2, title, summary, onOpen, showMe) {
279
+ const item = listItem(doc, icon2, title, summary);
280
+ item.addEventListener("click", onOpen);
281
+ if (!showMe) return item;
282
+ const row = el(doc, "div", "g-rc-row");
283
+ const b = el(doc, "button", "g-btn g-btn-primary g-rc-showme", "\u25B6 Show me");
284
+ b.type = "button";
285
+ b.setAttribute("aria-label", `Show me: ${title}`);
286
+ b.addEventListener("click", showMe);
287
+ row.append(item, b);
288
+ return row;
289
+ }
290
+ function enhanceShowMe(root, doc, canStart, start) {
291
+ for (const a of Array.from(root.querySelectorAll(`a[href^="${import_runtime3.SHOW_ME_HREF_PREFIX}"]`))) {
292
+ const key = (0, import_runtime3.showMeKeyFromHref)(a.getAttribute("href"));
293
+ const holder = a.closest("p.gd-show-me") ?? a;
294
+ if (!key || !canStart(key)) {
295
+ if (holder !== a) holder.remove();
296
+ else a.replaceWith(doc.createTextNode(a.textContent ?? ""));
297
+ continue;
298
+ }
299
+ const b = el(doc, "button", "g-btn g-btn-primary g-help-showme", `\u25B6 ${(a.textContent ?? "").trim() || "Show me"}`);
300
+ b.type = "button";
301
+ b.addEventListener("click", () => start(key));
302
+ a.replaceWith(b);
303
+ }
304
+ }
273
305
  function el(doc, tag, cls, text) {
274
306
  const e = doc.createElement(tag);
275
307
  if (cls) e.className = cls;
@@ -294,6 +326,11 @@ function createHelpView(options) {
294
326
  let searchSeq = 0;
295
327
  let openSeq = 0;
296
328
  let active = null;
329
+ const setActive = (next) => {
330
+ const was = !!active;
331
+ active = next;
332
+ if (was !== !!next) options.onArticleChange?.(!!next);
333
+ };
297
334
  const root = el(doc, "div", "g-help");
298
335
  const searchWrap = el(doc, "div", "g-rc-search");
299
336
  const input = el(doc, "input", "g-input");
@@ -304,6 +341,8 @@ function createHelpView(options) {
304
341
  const body = el(doc, "div", "g-panel-body");
305
342
  root.append(searchWrap, body);
306
343
  const hasBody = (a) => typeof a.body === "string" && !a.bodyOmitted;
344
+ const canShowMe = (key) => !!options.onShowMe && !!startableGuide(options.guides?.(), key);
345
+ const showMe = (key) => options.onShowMe?.(startableGuide(options.guides?.(), key)?.key ?? key);
307
346
  const openArticle = (article) => {
308
347
  if (article.externalUrl) {
309
348
  window.open(article.externalUrl, "_blank", "noopener,noreferrer");
@@ -312,20 +351,20 @@ function createHelpView(options) {
312
351
  }
313
352
  const seq = ++openSeq;
314
353
  if (hasBody(article) || !options.loadArticle) {
315
- active = { article, loading: false };
354
+ setActive({ article, loading: false });
316
355
  if (hasBody(article)) options.onArticleView?.(article);
317
356
  return render();
318
357
  }
319
- active = { article, loading: true };
358
+ setActive({ article, loading: true });
320
359
  render();
321
360
  options.loadArticle(article.id).then((full) => {
322
361
  if (seq !== openSeq || !active) return;
323
- active = { article: full ?? { ...article, body: "" }, loading: false };
362
+ setActive({ article: full ?? { ...article, body: "" }, loading: false });
324
363
  if (full) options.onArticleView?.(full);
325
364
  render();
326
365
  }).catch(() => {
327
366
  if (seq !== openSeq || !active) return;
328
- active = { article: { ...article, body: "" }, loading: false };
367
+ setActive({ article: { ...article, body: "" }, loading: false });
329
368
  render();
330
369
  });
331
370
  };
@@ -335,7 +374,7 @@ function createHelpView(options) {
335
374
  back.type = "button";
336
375
  back.addEventListener("click", () => {
337
376
  openSeq++;
338
- active = null;
377
+ setActive(null);
339
378
  render();
340
379
  });
341
380
  const title = el(doc, "h3", "g-title", article.title);
@@ -344,6 +383,7 @@ function createHelpView(options) {
344
383
  else if (article.body) {
345
384
  setSanitizedHtml(content, article.body);
346
385
  enhancePdfViewers(content, doc);
386
+ enhanceShowMe(content, doc, canShowMe, showMe);
347
387
  } else content.appendChild(el(doc, "div", "g-rc-empty", "This article couldn't be loaded."));
348
388
  wrap.append(back, title, content);
349
389
  body.appendChild(wrap);
@@ -355,9 +395,9 @@ function createHelpView(options) {
355
395
  return s;
356
396
  };
357
397
  const articleButton = (a) => {
358
- const b = listItem(doc, "\u{1F4C4}", a.title, a.summary);
359
- b.addEventListener("click", () => openArticle(a));
360
- body.appendChild(b);
398
+ const key = a.walkthroughs?.find(canShowMe);
399
+ const summary = a.hasVideo ? a.summary ? `Video \xB7 ${a.summary}` : "Video" : a.summary;
400
+ body.appendChild(resultRow(doc, a.hasVideo ? "\u25B6" : "\u{1F4C4}", a.title, summary, () => openArticle(a), key ? () => showMe(key) : void 0));
361
401
  };
362
402
  function render() {
363
403
  body.textContent = "";
@@ -366,10 +406,17 @@ function createHelpView(options) {
366
406
  const member = options.audienceIds?.() ?? [];
367
407
  const sections = (0, import_runtime3.resourceCenterSections)(data.collections, data.articles, member);
368
408
  const q = query.trim().toLowerCase();
409
+ if (q) {
410
+ const walkthroughs = (data.recommended ?? []).filter((g) => (0, import_runtime3.matchesHelpQuery)(g.name, q) && startableGuide(options.guides?.() ?? data.recommended, g.id));
411
+ if (walkthroughs.length) {
412
+ section(`Walkthroughs (${walkthroughs.length})`);
413
+ for (const g of walkthroughs) body.appendChild(resultRow(doc, "\u25B6", g.name, "Show me, step by step", () => options.onStartGuide?.(g.id)));
414
+ }
415
+ }
369
416
  body.append(...options.extra?.(q) ?? []);
370
417
  if (q) {
371
418
  const visible = sections.flatMap((s) => s.articles);
372
- const local = visible.filter((a) => a.title.toLowerCase().includes(q) || a.summary.toLowerCase().includes(q));
419
+ const local = visible.filter((a) => (0, import_runtime3.matchesHelpQuery)(`${a.title} ${a.summary}`, q));
373
420
  const seen = new Set(local.map((a) => a.id));
374
421
  const hidden = new Set(data.articles.map((a) => a.id).filter((id) => !visible.some((v) => v.id === id)));
375
422
  const merged = [...local, ...serverResults.filter((a) => !seen.has(a.id) && !hidden.has(a.id) && (0, import_runtime3.isArticleListed)(data.collections, a, member))];
@@ -441,7 +488,7 @@ function createHelpView(options) {
441
488
  const onInput = () => {
442
489
  query = input.value;
443
490
  openSeq++;
444
- active = null;
491
+ setActive(null);
445
492
  render();
446
493
  if (debounce) clearTimeout(debounce);
447
494
  if (!options.search || !query.trim()) {
@@ -464,7 +511,7 @@ function createHelpView(options) {
464
511
  render,
465
512
  reset() {
466
513
  openSeq++;
467
- active = null;
514
+ setActive(null);
468
515
  query = "";
469
516
  input.value = "";
470
517
  serverResults = [];
@@ -473,6 +520,7 @@ function createHelpView(options) {
473
520
  const listed = options.data().articles.find((a) => a.id === id) ?? { id, slug: id, title: "Article", summary: "", updatedAt: "", bodyOmitted: true };
474
521
  openArticle(listed);
475
522
  },
523
+ isArticleOpen: () => !!active,
476
524
  focusSearch: () => input.focus?.({ preventScroll: true }),
477
525
  setQuery(q) {
478
526
  input.value = q;
@@ -545,6 +593,15 @@ function renderHowTo(doc, howTo, onShowMe) {
545
593
  show.addEventListener("click", () => onShowMe(guideKey));
546
594
  wrap.appendChild(show);
547
595
  }
596
+ if (howTo.video && /^https?:\/\//i.test(howTo.video.url)) {
597
+ const video = el(doc, "video", "g-howto-video");
598
+ video.src = howTo.video.url;
599
+ video.controls = true;
600
+ video.preload = "metadata";
601
+ video.playsInline = true;
602
+ video.setAttribute("aria-label", `Video: ${howTo.title}`);
603
+ wrap.appendChild(video);
604
+ }
548
605
  const list = el(doc, "ol", "g-howto-steps");
549
606
  let n = 0;
550
607
  for (const s of howTo.steps) {
@@ -578,6 +635,275 @@ function renderHowTo(doc, howTo, onShowMe) {
578
635
  return wrap;
579
636
  }
580
637
 
638
+ // src/complaint-view.ts
639
+ var import_runtime4 = require("@guidelytech/shared/runtime");
640
+ var emptyComplaintDraft = (description = "") => ({ description, answers: {}, email: "", attachment: null, error: null, sending: false, checked: false });
641
+ var COMPLAINT_STEPS = [
642
+ ["open", "Received"],
643
+ ["in_review", "In review"],
644
+ ["resolved", "Resolved"]
645
+ ];
646
+ var complaintStatusLabel = (s) => COMPLAINT_STEPS.find(([k]) => k === s)?.[1] ?? s;
647
+ var EMAIL = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
648
+ function complaintProblem(category, draft, needsEmail) {
649
+ for (const f of category.fields) if (f.required && !(draft.answers[f.id] ?? "").trim()) return `${f.label} is required.`;
650
+ if (!draft.description.trim()) return "Tell us what happened.";
651
+ if (needsEmail && !EMAIL.test(draft.email.trim())) return "Enter your email address so the team can reply.";
652
+ return null;
653
+ }
654
+ function complaintAnswers(category, draft) {
655
+ const out = {};
656
+ for (const f of category.fields) {
657
+ const v = (draft.answers[f.id] ?? "").trim();
658
+ if (v) out[f.id] = v.slice(0, import_runtime4.MAX_COMPLAINT_ANSWER_LENGTH);
659
+ }
660
+ return out;
661
+ }
662
+ function statusPill(doc, status) {
663
+ return el(doc, "span", `g-m-pill g-m-pill-${status}`, complaintStatusLabel(status));
664
+ }
665
+ function dot(doc, color) {
666
+ const d = el(doc, "span", "g-m-cat-dot");
667
+ if (/^#[0-9a-fA-F]{6}$/.test(color)) d.style.background = color;
668
+ d.setAttribute("aria-hidden", "true");
669
+ return d;
670
+ }
671
+ function categoryList(doc, categories, onPick) {
672
+ const wrap = el(doc, "div", "g-m-form");
673
+ const intro = el(doc, "div");
674
+ intro.append(el(doc, "h3", "g-m-form-title", "What's it about?"), el(doc, "p", "g-m-card-sub", "Picking the right one gets it to the right team faster."));
675
+ wrap.appendChild(intro);
676
+ const list = el(doc, "div", "g-m-cats");
677
+ list.setAttribute("role", "list");
678
+ for (const c of categories) {
679
+ const b = el(doc, "button", "g-m-card g-m-row g-m-center g-m-cat");
680
+ b.type = "button";
681
+ b.setAttribute("role", "listitem");
682
+ b.appendChild(dot(doc, c.color));
683
+ const text = el(doc, "span", "g-m-grow");
684
+ text.appendChild(el(doc, "span", "g-m-card-title", c.name));
685
+ if (c.description) text.appendChild(el(doc, "span", "g-m-card-sub", c.description));
686
+ b.appendChild(text);
687
+ b.addEventListener("click", () => onPick(c));
688
+ list.appendChild(b);
689
+ }
690
+ wrap.appendChild(list);
691
+ return wrap;
692
+ }
693
+ var fieldSeq = 0;
694
+ function field(doc, label, control, opts = {}) {
695
+ const id = `g-m-f${++fieldSeq}`;
696
+ control.id = id;
697
+ const wrap = el(doc, "div", "g-m-field");
698
+ const l = el(doc, "label", "g-m-label", label);
699
+ l.htmlFor = id;
700
+ if (opts.optional) l.appendChild(el(doc, "span", "g-m-optional", " (optional)"));
701
+ wrap.append(l, control);
702
+ if (opts.hint) {
703
+ const hint = el(doc, "span", "g-m-hint", opts.hint);
704
+ hint.id = `${id}-hint`;
705
+ control.setAttribute("aria-describedby", hint.id);
706
+ wrap.appendChild(hint);
707
+ }
708
+ if (opts.invalid) control.setAttribute("aria-invalid", "true");
709
+ return wrap;
710
+ }
711
+ function complaintForm(doc, o) {
712
+ const { category, draft } = o;
713
+ const form = el(doc, "form", "g-m-form");
714
+ form.noValidate = true;
715
+ const picked = el(doc, "div", "g-m-picked g-m-row g-m-center");
716
+ picked.append(dot(doc, category.color), el(doc, "span", "g-m-grow g-m-card-title", category.name));
717
+ if (o.canChangeCategory) {
718
+ const change = el(doc, "button", "g-m-text-btn", "Change");
719
+ change.type = "button";
720
+ change.setAttribute("aria-label", `Change category (${category.name})`);
721
+ change.addEventListener("click", o.onChangeCategory);
722
+ picked.appendChild(change);
723
+ }
724
+ form.appendChild(picked);
725
+ for (const f of category.fields) {
726
+ const missing = draft.checked && f.required && !(draft.answers[f.id] ?? "").trim();
727
+ let control;
728
+ if (f.type === "choice") {
729
+ const select = el(doc, "select", "g-input");
730
+ const option = (label, value) => {
731
+ const o2 = el(doc, "option", "", label);
732
+ o2.value = value;
733
+ return o2;
734
+ };
735
+ select.appendChild(option(f.required ? "Choose one" : "Choose one (optional)", ""));
736
+ for (const opt of f.options ?? []) select.appendChild(option(opt, opt));
737
+ select.value = draft.answers[f.id] ?? "";
738
+ select.addEventListener("change", () => draft.answers[f.id] = select.value);
739
+ control = select;
740
+ } else {
741
+ const input = el(doc, "input", "g-input");
742
+ input.type = "text";
743
+ input.maxLength = import_runtime4.MAX_COMPLAINT_ANSWER_LENGTH;
744
+ input.value = draft.answers[f.id] ?? "";
745
+ input.addEventListener("input", () => draft.answers[f.id] = input.value);
746
+ control = input;
747
+ }
748
+ if (f.required) control.required = true;
749
+ form.appendChild(field(doc, f.label, control, { optional: !f.required, invalid: missing }));
750
+ }
751
+ const what = el(doc, "textarea", "g-input g-m-textarea");
752
+ what.rows = 5;
753
+ what.maxLength = import_runtime4.MAX_COMPLAINT_LENGTH;
754
+ what.required = true;
755
+ what.value = draft.description;
756
+ what.placeholder = "What went wrong, and when?";
757
+ what.addEventListener("input", () => draft.description = what.value);
758
+ form.appendChild(field(doc, "What happened?", what, { invalid: draft.checked && !draft.description.trim() }));
759
+ if (o.needsEmail) {
760
+ const email = el(doc, "input", "g-input");
761
+ email.type = "email";
762
+ email.autocomplete = "email";
763
+ email.required = true;
764
+ email.value = draft.email;
765
+ email.addEventListener("input", () => draft.email = email.value);
766
+ form.appendChild(field(doc, "Your email", email, { hint: "So the team can reply to you.", invalid: draft.checked && !EMAIL.test(draft.email.trim()) }));
767
+ }
768
+ const files = el(doc, "div", "g-m-attach");
769
+ if (draft.attachment) {
770
+ const a = draft.attachment;
771
+ const row = el(doc, "div", "g-m-row g-m-center g-m-attached");
772
+ if (a.previewUrl) {
773
+ const img = el(doc, "img", "g-m-thumb");
774
+ img.src = a.previewUrl;
775
+ img.alt = "";
776
+ img.addEventListener("error", () => img.remove());
777
+ row.appendChild(img);
778
+ }
779
+ row.appendChild(el(doc, "span", "g-m-grow g-m-ellipsis", a.fileName));
780
+ const remove = el(doc, "button", "g-m-text-btn", "Remove");
781
+ remove.type = "button";
782
+ remove.setAttribute("aria-label", `Remove ${a.fileName}`);
783
+ remove.addEventListener("click", o.onRemoveFile);
784
+ row.appendChild(remove);
785
+ files.appendChild(row);
786
+ } else {
787
+ const input = el(doc, "input", "g-m-file");
788
+ input.type = "file";
789
+ input.accept = "image/png,image/jpeg,image/webp,image/gif";
790
+ input.tabIndex = -1;
791
+ input.setAttribute("aria-hidden", "true");
792
+ input.addEventListener("change", () => {
793
+ const file = input.files?.[0];
794
+ if (file) o.onPickFile(file);
795
+ });
796
+ const pick = el(doc, "button", "g-m-attach-btn", "Attach a screenshot");
797
+ pick.type = "button";
798
+ pick.addEventListener("click", () => input.click());
799
+ files.append(input, pick);
800
+ }
801
+ form.appendChild(files);
802
+ const foot = el(doc, "div", "g-m-form-foot");
803
+ foot.appendChild(el(doc, "p", "g-m-hint", "We'll include the page you're on so the team can look into it."));
804
+ if (draft.error) {
805
+ const err = el(doc, "p", "g-m-error", draft.error);
806
+ err.setAttribute("role", "alert");
807
+ foot.appendChild(err);
808
+ }
809
+ const submit = el(doc, "button", "g-btn g-btn-primary g-m-wide", draft.sending ? "Sending\u2026" : "Submit complaint");
810
+ submit.type = "submit";
811
+ submit.disabled = draft.sending;
812
+ foot.appendChild(submit);
813
+ form.appendChild(foot);
814
+ form.addEventListener("submit", (e) => {
815
+ e.preventDefault();
816
+ if (!draft.sending) o.onSubmit();
817
+ });
818
+ return form;
819
+ }
820
+ function statusTracker(doc, status) {
821
+ const list = el(doc, "ol", "g-m-steps");
822
+ list.setAttribute("aria-label", "Progress");
823
+ const at = COMPLAINT_STEPS.findIndex(([k]) => k === status);
824
+ COMPLAINT_STEPS.forEach(([, label], i) => {
825
+ const li = el(doc, "li", `g-m-step${i <= at ? " g-m-step-done" : ""}`);
826
+ if (i === at) li.setAttribute("aria-current", "step");
827
+ li.append(el(doc, "span", "g-m-step-dot"), el(doc, "span", "g-m-step-label", label));
828
+ list.appendChild(li);
829
+ });
830
+ return list;
831
+ }
832
+ function complaintSent(doc, o) {
833
+ const wrap = el(doc, "div", "g-m-form g-m-sent");
834
+ const top = el(doc, "div", "g-m-sent-top");
835
+ const tick = el(doc, "span", "g-m-sent-tick");
836
+ tick.setAttribute("aria-hidden", "true");
837
+ tick.textContent = "\u2713";
838
+ const p = el(doc, "p", "g-m-card-sub");
839
+ p.append("Your reference is ", el(doc, "strong", "", o.complaint.reference), ". The team will reply in this conversation, so check back here.");
840
+ top.append(tick, el(doc, "h3", "g-m-form-title", "We've got your complaint"), p);
841
+ wrap.appendChild(top);
842
+ const details = el(doc, "dl", "g-m-card g-m-static g-m-details");
843
+ const row = (k, v) => {
844
+ const r = el(doc, "div", "g-m-detail");
845
+ r.append(el(doc, "dt", "", k), el(doc, "dd", "", v));
846
+ details.appendChild(r);
847
+ };
848
+ row("Category", o.complaint.categoryName);
849
+ o.answers.slice(0, 3).forEach(([k, v]) => row(k, v));
850
+ wrap.appendChild(details);
851
+ wrap.appendChild(statusTracker(doc, o.complaint.status));
852
+ const open = el(doc, "button", "g-btn g-btn-primary g-m-wide", "Add more detail");
853
+ open.type = "button";
854
+ open.addEventListener("click", o.onOpenConversation);
855
+ const home = el(doc, "button", "g-btn g-btn-secondary g-m-wide", "Back to home");
856
+ home.type = "button";
857
+ home.addEventListener("click", o.onHome);
858
+ wrap.append(open, home);
859
+ return wrap;
860
+ }
861
+ var MAX_SIDE = 1600;
862
+ async function readScreenshot(doc, file) {
863
+ if (!/^image\/(png|jpeg|webp|gif)$/.test(file.type)) throw new Error("Attach a PNG, JPEG, WebP or GIF image.");
864
+ if (file.size > 25 * 1024 * 1024) throw new Error("That image is too large.");
865
+ const win = doc.defaultView;
866
+ const asDataUrl = (blob) => new Promise((resolve, reject) => {
867
+ const r = new FileReader();
868
+ r.onload = () => resolve(String(r.result));
869
+ r.onerror = () => reject(new Error("The image couldn't be read."));
870
+ r.readAsDataURL(blob);
871
+ });
872
+ let dataUrl = null;
873
+ let scaled = false;
874
+ try {
875
+ const url = win?.URL.createObjectURL(file);
876
+ if (!url) throw new Error("no URL");
877
+ const img = await new Promise((resolve, reject) => {
878
+ const i = new Image();
879
+ i.onload = () => resolve(i);
880
+ i.onerror = () => reject(new Error("decode"));
881
+ i.src = url;
882
+ }).finally(() => win?.URL.revokeObjectURL(url));
883
+ const scale = Math.min(1, MAX_SIDE / Math.max(img.naturalWidth || 1, img.naturalHeight || 1));
884
+ const canvas = doc.createElement("canvas");
885
+ canvas.width = Math.max(1, Math.round(img.naturalWidth * scale));
886
+ canvas.height = Math.max(1, Math.round(img.naturalHeight * scale));
887
+ const ctx = canvas.getContext("2d");
888
+ if (!ctx) throw new Error("no canvas");
889
+ ctx.fillStyle = "#fff";
890
+ ctx.fillRect(0, 0, canvas.width, canvas.height);
891
+ ctx.drawImage(img, 0, 0, canvas.width, canvas.height);
892
+ for (const q of [0.85, 0.7, 0.5]) {
893
+ dataUrl = canvas.toDataURL("image/jpeg", q);
894
+ if ((dataUrl.length - dataUrl.indexOf(",") - 1) * 0.75 <= import_runtime4.MAX_COMPLAINT_ATTACHMENT_BYTES) break;
895
+ }
896
+ scaled = true;
897
+ } catch {
898
+ dataUrl = file.size <= import_runtime4.MAX_COMPLAINT_ATTACHMENT_BYTES ? await asDataUrl(file) : null;
899
+ }
900
+ if (!dataUrl) throw new Error("That image is too large. Screenshots can be up to 2 MB.");
901
+ const data = dataUrl.slice(dataUrl.indexOf(",") + 1);
902
+ if (data.length * 0.75 > import_runtime4.MAX_COMPLAINT_ATTACHMENT_BYTES) throw new Error("That image is too large. Screenshots can be up to 2 MB.");
903
+ const name = (file.name || "screenshot").slice(0, 180);
904
+ return { fileName: scaled ? `${name.replace(/\.[a-z0-9]+$/i, "")}.jpg` : name, data, previewUrl: dataUrl };
905
+ }
906
+
581
907
  // src/messenger-styles.ts
582
908
  var MESSENGER_STYLES = `
583
909
  :host([data-g-messenger="bottom-right"]) .g-launcher.g-pos-bottom-right{right:72px}
@@ -587,7 +913,15 @@ var MESSENGER_STYLES = `
587
913
  .g-m-launcher:focus-visible{outline:2px solid var(--g-primary);outline-offset:3px}
588
914
  .g-m-launcher svg{width:26px;height:26px;fill:currentColor}
589
915
  .g-m-badge{position:absolute;top:-2px;right:-2px;min-width:20px;height:20px;padding:0 5px;border-radius:999px;background:#e11d48;color:#fff;font-size:11px;font-weight:700;display:inline-flex;align-items:center;justify-content:center;border:2px solid var(--g-bg)}
590
- .g-messenger{bottom:76px;height:min(640px,calc(100vh - 110px))}
916
+ .g-messenger{bottom:76px;height:min(640px,calc(100vh - 110px));transition:width .2s ease,height .2s ease,max-height .2s ease}
917
+ .g-messenger.g-m-expanded{width:min(720px,calc(100vw - 32px));height:calc(100vh - 110px);max-height:calc(100vh - 110px)}
918
+ .g-m-expanded .g-rc-article,.g-m-expanded .g-m-scroll{padding:24px clamp(20px,5vw,40px)}
919
+ .g-m-expanded .g-rc-article>.g-title{font-size:24px;line-height:1.25;margin-bottom:12px}
920
+ .g-m-expanded .g-rc-article .g-body{font-size:15px}
921
+ .g-m-head.g-m-sizable{padding-right:92px}
922
+ .g-m-size{position:absolute;right:48px;top:50%;transform:translateY(-50%);width:40px;height:40px;border:0;border-radius:999px;background:transparent;color:inherit;cursor:pointer;display:inline-flex;align-items:center;justify-content:center;padding:0}
923
+ .g-m-size .g-m-ico{width:18px;height:18px}
924
+ .g-m-size:hover,.g-m-size:focus-visible{background:rgba(255,255,255,.18);outline:none}
591
925
  .g-m-head{padding:16px 48px 14px 20px;background:var(--g-primary);color:var(--g-primary-text);position:relative;display:flex;align-items:center;gap:8px;min-height:56px}
592
926
  .g-m-head .g-close{color:inherit;top:50%;transform:translateY(-50%)}
593
927
  .g-m-head .g-close:hover,.g-m-head .g-close:focus-visible{background:rgba(255,255,255,.18);color:inherit}
@@ -657,6 +991,7 @@ var MESSENGER_STYLES = `
657
991
  .g-howto-desc,.g-howto-note{color:var(--g-muted);font-size:13px}
658
992
  .g-howto p{margin:0 0 6px;white-space:pre-line}
659
993
  .g-howto-show{width:100%;margin:6px 0 4px}
994
+ .g-howto-video{display:block;width:100%;max-height:260px;margin:8px 0 4px;border-radius:8px;background:#000}
660
995
  .g-howto-steps{list-style:none;margin:12px 0 0;padding:0;display:flex;flex-direction:column;gap:16px}
661
996
  .g-howto-step h4{margin:4px 0 0;font-size:15px}
662
997
  .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}
@@ -726,6 +1061,52 @@ var MESSENGER_STYLES = `
726
1061
  .g-m-asks{align-items:flex-start}
727
1062
  .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
1063
  .g-m-chip.g-m-ask:disabled{opacity:.5;cursor:not-allowed}
1064
+ .g-m-side{display:flex;flex-direction:column;align-items:flex-end;gap:4px;flex:none}
1065
+ .g-m-pill{display:inline-block;font-size:11px;font-weight:700;line-height:1.5;padding:1px 8px;border-radius:999px;white-space:nowrap;background:#fef3c7;color:#92400e}
1066
+ .g-m-pill-in_review{background:#e0e7ff;color:#3730a3}
1067
+ .g-m-pill-resolved{background:#dcfce7;color:#166534}
1068
+ .g-m-complain .g-m-badge-ico{background:#fef3e2;color:#b45309}
1069
+ .g-m-complaint{align-self:stretch;gap:10px;border:1px solid var(--g-border);background:var(--g-subtle);border-radius:10px;padding:10px 12px}
1070
+ .g-m-chip-ico{width:16px;height:16px}
1071
+ .g-m-offer{gap:6px}
1072
+ .g-m-form{display:flex;flex-direction:column;gap:14px;margin:0}
1073
+ .g-m-form-title{margin:0;font-size:17px;font-weight:600}
1074
+ .g-m-cats{display:flex;flex-direction:column;gap:10px}
1075
+ .g-m-cats .g-m-card{margin:0;gap:12px;min-height:44px}
1076
+ .g-m-cat-dot{width:10px;height:10px;border-radius:999px;background:var(--g-muted);flex:none}
1077
+ .g-m-picked{gap:8px;border:1px solid var(--g-border);background:var(--g-subtle);border-radius:10px;padding:8px 12px}
1078
+ .g-m-text-btn{border:0;background:transparent;color:var(--g-primary);font:inherit;font-size:13px;font-weight:600;cursor:pointer;padding:6px 4px;min-height:32px}
1079
+ .g-m-text-btn:hover{text-decoration:underline}
1080
+ .g-m-text-btn:focus-visible{outline:2px solid var(--g-primary);outline-offset:2px;border-radius:4px}
1081
+ .g-m-field{display:flex;flex-direction:column;gap:4px}
1082
+ .g-m-label{font-weight:600;font-size:13px}
1083
+ .g-m-optional{font-weight:400;color:var(--g-muted)}
1084
+ .g-m-hint{color:var(--g-muted);font-size:12px;margin:0}
1085
+ .g-m-error{color:#b91c1c;font-size:13px;margin:0}
1086
+ .g-scope[data-theme="dark"] .g-m-error{color:#fca5a5}
1087
+ .g-m-field .g-input[aria-invalid="true"]{border-color:#dc2626}
1088
+ .g-m-textarea{min-height:110px}
1089
+ .g-m-file{position:absolute;width:1px;height:1px;opacity:0;pointer-events:none}
1090
+ .g-m-attach-btn{align-self:flex-start;border:1px dashed var(--g-border);background:var(--g-bg);color:var(--g-text);font:inherit;font-size:13px;border-radius:8px;padding:8px 12px;cursor:pointer;min-height:36px}
1091
+ .g-m-attach-btn:hover{background:var(--g-subtle)}
1092
+ .g-m-attach-btn:focus-visible{outline:2px solid var(--g-primary);outline-offset:2px}
1093
+ .g-m-attached{gap:10px;border:1px solid var(--g-border);border-radius:8px;padding:6px 8px;font-size:13px}
1094
+ .g-m-thumb{width:40px;height:40px;object-fit:cover;border-radius:6px;background:var(--g-subtle);flex:none}
1095
+ .g-m-form-foot{display:flex;flex-direction:column;gap:10px;margin-top:4px}
1096
+ .g-m-sent{gap:12px}
1097
+ .g-m-sent-top{display:flex;flex-direction:column;align-items:center;text-align:center;gap:6px;padding-top:8px}
1098
+ .g-m-sent-top p{max-width:300px;margin:0}
1099
+ .g-m-sent-tick{width:52px;height:52px;border-radius:999px;background:#dcfce7;color:#15803d;display:inline-flex;align-items:center;justify-content:center;font-size:26px;font-weight:700}
1100
+ .g-m-details{margin:0;display:flex;flex-direction:column;gap:8px}
1101
+ .g-m-detail{display:flex;justify-content:space-between;gap:12px;font-size:13px}
1102
+ .g-m-detail dt{color:var(--g-muted)}
1103
+ .g-m-detail dd{margin:0;font-weight:600;text-align:right;overflow-wrap:anywhere}
1104
+ .g-m-steps{list-style:none;margin:4px 0;padding:0;display:flex;justify-content:space-between;position:relative}
1105
+ .g-m-steps::before{content:"";position:absolute;top:6px;left:12%;right:12%;height:2px;background:var(--g-border)}
1106
+ .g-m-step{position:relative;display:flex;flex-direction:column;align-items:center;gap:4px;flex:1;font-size:12px;color:var(--g-muted)}
1107
+ .g-m-step-dot{width:14px;height:14px;border-radius:999px;border:2px solid var(--g-border);background:var(--g-bg);box-sizing:border-box}
1108
+ .g-m-step-done{color:var(--g-text);font-weight:600}
1109
+ .g-m-step-done .g-m-step-dot{background:var(--g-primary);border-color:var(--g-primary)}
729
1110
  `;
730
1111
 
731
1112
  // src/render-safe.ts
@@ -831,7 +1212,10 @@ var ICONS = {
831
1212
  article: ["M6 3h9l4 4v14H6z", "M9 12h6", "M9 16h6"],
832
1213
  ask: ["M4 5h16v11H8l-4 4z", "M9 9.5h6", "M9 12.5h4"],
833
1214
  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"]
1215
+ circle: ["M12 3a9 9 0 1 0 0 18 9 9 0 0 0 0-18z"],
1216
+ expand: ["M15 3h6v6", "M21 3l-7 7", "M9 21H3v-6", "M3 21l7-7"],
1217
+ flag: ["M5 21V4", "M5 4h12l-2.5 4L17 12H5"],
1218
+ collapse: ["M4 14h6v6", "M3 21l7-7", "M20 10h-6V4", "M21 3l-7 7"]
835
1219
  };
836
1220
  function icon(doc, name, cls = "g-m-ico") {
837
1221
  const svg = doc.createElementNS(SVG_NS, "svg");
@@ -910,6 +1294,7 @@ function renderMessenger(options) {
910
1294
  let current = null;
911
1295
  const streaming = /* @__PURE__ */ new Map();
912
1296
  let sending = false;
1297
+ let complaintDraft = emptyComplaintDraft();
913
1298
  let draft = "";
914
1299
  let articles = null;
915
1300
  let articlesLoading = false;
@@ -984,8 +1369,38 @@ function renderMessenger(options) {
984
1369
  options.onOpenChecklist?.(id);
985
1370
  },
986
1371
  onArticleView: options.onArticleView,
987
- extra: (q) => howToItems(q)
1372
+ extra: (q) => howToItems(q),
1373
+ guides: () => options.guides ?? [],
1374
+ onShowMe: (key) => startWalkthrough(key),
1375
+ onArticleChange: () => syncSize()
988
1376
  });
1377
+ let docCollapsed = false;
1378
+ let sizeButton = null;
1379
+ const readingDoc = () => open && (view.kind === "howto" || view.kind === "tab" && view.tab === "help" && help.isArticleOpen());
1380
+ function syncSize() {
1381
+ const reading = readingDoc();
1382
+ if (!reading) docCollapsed = false;
1383
+ panel.classList.toggle("g-m-expanded", reading && !docCollapsed);
1384
+ head.classList.toggle("g-m-sizable", reading);
1385
+ if (!reading) {
1386
+ sizeButton?.remove();
1387
+ sizeButton = null;
1388
+ return;
1389
+ }
1390
+ if (!sizeButton) {
1391
+ sizeButton = el(doc, "button", "g-m-size");
1392
+ sizeButton.type = "button";
1393
+ sizeButton.addEventListener("click", () => {
1394
+ docCollapsed = !docCollapsed;
1395
+ syncSize();
1396
+ });
1397
+ head.appendChild(sizeButton);
1398
+ }
1399
+ sizeButton.textContent = "";
1400
+ sizeButton.appendChild(icon(doc, docCollapsed ? "expand" : "collapse"));
1401
+ sizeButton.setAttribute("aria-label", docCollapsed ? "Expand" : "Collapse");
1402
+ sizeButton.title = docCollapsed ? "Expand" : "Collapse";
1403
+ }
989
1404
  const unreadTotal = () => conversations.reduce((n, c) => n + (c.unread > 0 ? 1 : 0), 0);
990
1405
  const updateBadge = () => {
991
1406
  const n = unreadTotal();
@@ -993,7 +1408,13 @@ function renderMessenger(options) {
993
1408
  badge.textContent = n > 9 ? "9+" : String(n);
994
1409
  launcher.setAttribute("aria-label", n ? `Open ${config.name}, ${n} unread` : `Open ${config.name}`);
995
1410
  };
996
- const upsertConversation = (c) => {
1411
+ const withComplaint = (c) => {
1412
+ if (c.complaint) return c;
1413
+ const known = conversations.find((x) => x.id === c.id)?.complaint ?? (current?.id === c.id ? current.complaint : void 0);
1414
+ return known ? { ...c, complaint: known } : c;
1415
+ };
1416
+ const upsertConversation = (incoming) => {
1417
+ const c = withComplaint(incoming);
997
1418
  conversations = [c, ...conversations.filter((x) => x.id !== c.id)].sort((a, b) => b.lastMessageAt.localeCompare(a.lastMessageAt));
998
1419
  if (current?.id === c.id) current = c;
999
1420
  updateBadge();
@@ -1028,15 +1449,19 @@ function renderMessenger(options) {
1028
1449
  const pageHowTos = () => (howTos?.forPage ?? []).map((id) => howTos?.howTos.find((h) => h.id === id)).filter((h) => !!h);
1029
1450
  const stepsLabel = (h) => `${h.stepCount} ${h.stepCount === 1 ? "step" : "steps"}`;
1030
1451
  const openHowTo = (id) => go({ kind: "howto", id, from: view });
1452
+ const startWalkthrough = (guideKey) => {
1453
+ setOpen(false);
1454
+ options.onStartGuide?.(guideKey);
1455
+ };
1031
1456
  function howToItems(q) {
1032
- const list = q ? (howTos?.howTos ?? []).filter((h) => h.title.toLowerCase().includes(q) || h.description.toLowerCase().includes(q)).slice(0, 10) : [];
1457
+ const list = q ? (howTos?.howTos ?? []).filter((h) => (0, import_runtime5.matchesHelpQuery)(`${h.title} ${h.description}`, q)).slice(0, 10) : [];
1033
1458
  if (!list.length) return [];
1034
1459
  return [
1035
1460
  el(doc, "div", "g-rc-section", `How-tos (${list.length})`),
1036
1461
  ...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;
1462
+ const key = h.guideKey && startableGuide(options.guides, h.guideKey) ? h.guideKey : null;
1463
+ const sub = [h.hasVideo ? "Video" : "", stepsLabel(h)].filter(Boolean).join(" \xB7 ");
1464
+ return resultRow(doc, "\u{1F4CB}", h.title, h.description ? `${sub} \xB7 ${h.description}` : sub, () => openHowTo(h.id), key ? () => startWalkthrough(key) : void 0);
1040
1465
  })
1041
1466
  ];
1042
1467
  }
@@ -1048,6 +1473,7 @@ function renderMessenger(options) {
1048
1473
  });
1049
1474
  function renderHead(title, back) {
1050
1475
  head.textContent = "";
1476
+ sizeButton = null;
1051
1477
  head.hidden = false;
1052
1478
  if (back) {
1053
1479
  const b = el(doc, "button", "g-m-back");
@@ -1087,9 +1513,9 @@ function renderMessenger(options) {
1087
1513
  i.appendChild(icon(doc, ico));
1088
1514
  b.append(i, el(doc, "span", "", label));
1089
1515
  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);
1516
+ const dot2 = el(doc, "span", "g-m-tab-dot");
1517
+ dot2.setAttribute("aria-label", "New");
1518
+ i.appendChild(dot2);
1093
1519
  }
1094
1520
  b.addEventListener("click", () => go({ kind: "tab", tab }));
1095
1521
  tabs.appendChild(b);
@@ -1103,7 +1529,10 @@ function renderMessenger(options) {
1103
1529
  const text = el(doc, "div", "g-m-grow");
1104
1530
  if (label) text.appendChild(el(doc, "div", "g-m-card-label", label));
1105
1531
  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));
1106
- row.append(text, el(doc, "span", "g-m-time", relativeTime(c.lastMessageAt)));
1532
+ const side = el(doc, "span", "g-m-side");
1533
+ side.appendChild(el(doc, "span", "g-m-time", relativeTime(c.lastMessageAt)));
1534
+ if (c.complaint) side.appendChild(statusPill(doc, c.complaint.status));
1535
+ row.append(text, side);
1107
1536
  card.appendChild(row);
1108
1537
  card.setAttribute("aria-label", `${label ? `${label}: ` : ""}${c.subject || "Conversation"}${c.unread ? ", unread" : ""}`);
1109
1538
  card.addEventListener("click", () => go({ kind: "thread", conversationId: c.id }));
@@ -1314,13 +1743,16 @@ function renderMessenger(options) {
1314
1743
  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
1744
  inner.append(top, hello);
1316
1745
  inner.appendChild(actionCard({ title: "Send us a message", sub: messageSub(), trail: true, onClick: () => go({ kind: "thread", conversationId: null }) }));
1317
- const recent = conversations[0];
1746
+ if (canComplain()) inner.appendChild(actionCard({ title: "File a complaint", sub: "Tell us what went wrong. You'll get a reference.", lead: iconTile("flag"), onClick: () => startComplaint(null), cls: "g-m-complain" }));
1747
+ const filed = latestComplaint();
1748
+ if (filed) inner.appendChild(conversationCard(filed, "Your complaint"));
1749
+ const recent = conversations.find((c) => c.id !== filed?.id);
1318
1750
  if (recent) inner.appendChild(conversationCard(recent, recent.status === "closed" && !recent.unread ? "Recent conversation" : "Continue the conversation"));
1319
1751
  if (config.status) inner.appendChild(statusCard(config.status));
1320
1752
  if (cards.progress) progressCards(v).forEach((c) => inner.appendChild(c));
1321
1753
  const suggestions = pageSuggestions(v);
1322
1754
  const questions = cards.suggestions ? (page?.questions ?? []).slice(0, 2) : [];
1323
- const label = v.path ? (0, import_runtime4.pageLabel)(v.path) : null;
1755
+ const label = v.path ? (0, import_runtime5.pageLabel)(v.path) : null;
1324
1756
  const fallback = !suggestions.length && !questions.length ? rcExtras(options.resourceCenter?.recommendedGuideIds).slice(0, 3) : [];
1325
1757
  if (suggestions.length || questions.length || fallback.length) {
1326
1758
  const box = el(doc, "div", "g-m-card g-m-static g-m-suggest");
@@ -1339,6 +1771,102 @@ function renderMessenger(options) {
1339
1771
  scroll.append(hero, inner);
1340
1772
  main.appendChild(scroll);
1341
1773
  }
1774
+ const complaintCategories = () => config.complaints?.categories ?? [];
1775
+ const needsEmail = () => visitor().identified === false;
1776
+ const canComplain = () => !!api.fileComplaint && complaintCategories().length > 0 && (!needsEmail() || !!config.complaints?.anonymous);
1777
+ function latestComplaint() {
1778
+ let best = null;
1779
+ for (const c of conversations) if (c.complaint && (!best || c.complaint.createdAt > best.complaint.createdAt)) best = c;
1780
+ return best;
1781
+ }
1782
+ function userMessagesText() {
1783
+ return messages.filter((m) => m.author === "user" && m.body.trim()).slice(-3).map((m) => m.body.trim()).join("\n\n").slice(0, 4e3);
1784
+ }
1785
+ function startComplaint(categoryId) {
1786
+ const cats = complaintCategories();
1787
+ const picked = categoryId ? cats.find((c) => c.id === categoryId) : cats.length === 1 ? cats[0] : void 0;
1788
+ const from = view.kind === "tab" || view.kind === "thread" ? view : { kind: "tab", tab: "home" };
1789
+ complaintDraft = emptyComplaintDraft(complaintDraft.description);
1790
+ if (picked) go({ kind: "complaint-form", categoryId: picked.id, conversationId: null, from });
1791
+ else go({ kind: "complaint-category", conversationId: null, from });
1792
+ }
1793
+ function complaintBanner(c) {
1794
+ const b = el(doc, "div", "g-m-complaint g-m-row g-m-center");
1795
+ b.setAttribute("role", "status");
1796
+ b.appendChild(icon(doc, "flag", "g-m-ico g-m-lead"));
1797
+ const text = el(doc, "span", "g-m-grow");
1798
+ text.append(el(doc, "span", "g-m-card-title", c.reference), el(doc, "span", "g-m-card-sub", c.categoryName));
1799
+ b.append(text, statusPill(doc, c.status));
1800
+ return b;
1801
+ }
1802
+ function renderComplaintCategories(v) {
1803
+ renderHead("File a complaint", () => go(v.from));
1804
+ const scroll = el(doc, "div", "g-m-scroll");
1805
+ scroll.appendChild(categoryList(doc, complaintCategories(), (c) => go({ kind: "complaint-form", categoryId: c.id, conversationId: v.conversationId, from: v.from })));
1806
+ main.appendChild(scroll);
1807
+ }
1808
+ function renderComplaintForm(v) {
1809
+ const category = complaintCategories().find((c) => c.id === v.categoryId);
1810
+ if (!category) return go({ kind: "complaint-category", conversationId: v.conversationId, from: v.from });
1811
+ const back = () => complaintCategories().length > 1 && !v.conversationId ? go({ kind: "complaint-category", conversationId: v.conversationId, from: v.from }) : go(v.from);
1812
+ renderHead("File a complaint", back);
1813
+ const scroll = el(doc, "div", "g-m-scroll");
1814
+ const draft2 = complaintDraft;
1815
+ scroll.appendChild(
1816
+ complaintForm(doc, {
1817
+ category,
1818
+ draft: draft2,
1819
+ needsEmail: needsEmail(),
1820
+ canChangeCategory: complaintCategories().length > 1,
1821
+ onChangeCategory: () => go({ kind: "complaint-category", conversationId: v.conversationId, from: v.from }),
1822
+ onPickFile: (file) => {
1823
+ draft2.error = null;
1824
+ readScreenshot(doc, file).then((a) => draft2.attachment = a).catch((e) => draft2.error = e instanceof Error ? e.message : "That image couldn't be attached.").finally(() => {
1825
+ if (view === v) render();
1826
+ });
1827
+ },
1828
+ onRemoveFile: () => {
1829
+ draft2.attachment = null;
1830
+ render();
1831
+ },
1832
+ onSubmit: () => submitComplaint(v, category)
1833
+ })
1834
+ );
1835
+ main.appendChild(scroll);
1836
+ }
1837
+ function submitComplaint(v, category) {
1838
+ const draft2 = complaintDraft;
1839
+ const problem = complaintProblem(category, draft2, needsEmail());
1840
+ draft2.checked = true;
1841
+ draft2.error = problem;
1842
+ if (problem || !api.fileComplaint) return render();
1843
+ draft2.sending = true;
1844
+ render();
1845
+ const answers = complaintAnswers(category, draft2);
1846
+ api.fileComplaint({
1847
+ categoryId: category.id,
1848
+ description: draft2.description.trim(),
1849
+ answers,
1850
+ email: needsEmail() ? draft2.email.trim() : null,
1851
+ conversationId: v.conversationId,
1852
+ attachment: draft2.attachment ? { fileName: draft2.attachment.fileName, data: draft2.attachment.data } : null
1853
+ }).then((res) => {
1854
+ upsertConversation({ ...res.conversation, unread: 0 });
1855
+ complaintDraft = emptyComplaintDraft();
1856
+ const labels = category.fields.filter((f) => answers[f.id]).map((f) => [f.label, answers[f.id]]);
1857
+ if (view === v) go({ kind: "complaint-sent", complaint: res.complaint, conversationId: res.conversation.id, answers: labels });
1858
+ }).catch((e) => {
1859
+ draft2.sending = false;
1860
+ draft2.error = (e instanceof Error && e.message ? e.message : "Your complaint couldn't be sent.").replace(/^[\w.]+: /, "");
1861
+ if (view === v) render();
1862
+ });
1863
+ }
1864
+ function renderComplaintSent(v) {
1865
+ renderHead(v.complaint.reference, () => go({ kind: "tab", tab: "home" }));
1866
+ const scroll = el(doc, "div", "g-m-scroll");
1867
+ scroll.appendChild(complaintSent(doc, { complaint: v.complaint, answers: v.answers, onOpenConversation: () => go({ kind: "thread", conversationId: v.conversationId }), onHome: () => go({ kind: "tab", tab: "home" }) }));
1868
+ main.appendChild(scroll);
1869
+ }
1342
1870
  function renderHowToView(id, from) {
1343
1871
  const cached = howToCache.get(id);
1344
1872
  renderHead(cached?.title ?? "How-to", () => go(from));
@@ -1374,7 +1902,7 @@ function renderMessenger(options) {
1374
1902
  const v = visitor();
1375
1903
  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
1904
  if (!items.length) return void 0;
1377
- const label = v.path ? (0, import_runtime4.pageLabel)(v.path) : null;
1905
+ const label = v.path ? (0, import_runtime5.pageLabel)(v.path) : null;
1378
1906
  return { title: label ? `For the ${label} page` : "Suggested for you", items };
1379
1907
  }
1380
1908
  function renderHelpTab() {
@@ -1401,7 +1929,7 @@ function renderMessenger(options) {
1401
1929
  pageKey = null;
1402
1930
  return;
1403
1931
  }
1404
- const key = (0, import_runtime4.foldPathIds)(path).toLowerCase();
1932
+ const key = (0, import_runtime5.foldPathIds)(path).toLowerCase();
1405
1933
  if (!force && key === pageKey && (pageLoading || Date.now() - pageAt < PAGE_TTL_MS)) return;
1406
1934
  if (key !== pageKey) page = null;
1407
1935
  pageKey = key;
@@ -1451,8 +1979,20 @@ function renderMessenger(options) {
1451
1979
  if (m.status === "pending") bubble.setAttribute("aria-busy", "true");
1452
1980
  bubble.dataset.messageId = m.id;
1453
1981
  wrap.appendChild(bubble);
1454
- if (m.citations.length || m.suggestedGuides.length || m.howTos?.length) {
1982
+ const offer = m.complaintOffer && canComplain() ? complaintCategories().find((c) => c.id === m.complaintOffer.categoryId) : void 0;
1983
+ if (m.citations.length || m.suggestedGuides.length || m.howTos?.length || offer) {
1455
1984
  const chips = el(doc, "div", "g-m-chips");
1985
+ if (offer) {
1986
+ const b = el(doc, "button", "g-m-chip g-m-guide g-m-offer");
1987
+ b.type = "button";
1988
+ b.append(icon(doc, "flag", "g-m-ico g-m-chip-ico"), el(doc, "span", "", `File a complaint \xB7 ${offer.name}`));
1989
+ b.addEventListener("click", () => {
1990
+ const conversationId = current?.id ?? null;
1991
+ complaintDraft = emptyComplaintDraft(userMessagesText());
1992
+ go({ kind: "complaint-form", categoryId: offer.id, conversationId, from: view });
1993
+ });
1994
+ chips.appendChild(b);
1995
+ }
1456
1996
  for (const g of m.suggestedGuides) {
1457
1997
  const b = el(doc, "button", "g-m-chip g-m-guide", `\u25B6 Show me: ${g.name}`);
1458
1998
  b.type = "button";
@@ -1482,9 +2022,10 @@ function renderMessenger(options) {
1482
2022
  if (!log) return;
1483
2023
  const atBottom = log.scrollHeight - log.scrollTop - log.clientHeight < 40;
1484
2024
  log.textContent = "";
2025
+ if (current?.complaint) log.appendChild(complaintBanner(current.complaint));
1485
2026
  if (!messages.length) {
1486
2027
  const v = visitor();
1487
- const label = v.path ? (0, import_runtime4.pageLabel)(v.path) : null;
2028
+ const label = v.path ? (0, import_runtime5.pageLabel)(v.path) : null;
1488
2029
  if (label && config.aiAvailable && !current) log.appendChild(el(doc, "div", "g-m-context", `Asking from the ${label} page`));
1489
2030
  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() };
1490
2031
  log.appendChild(messageNode(intro));
@@ -1601,6 +2142,9 @@ function renderMessenger(options) {
1601
2142
  sendButton = null;
1602
2143
  if (view.kind === "thread") renderThread(view.conversationId);
1603
2144
  else if (view.kind === "howto") renderHowToView(view.id, view.from);
2145
+ else if (view.kind === "complaint-category") renderComplaintCategories(view);
2146
+ else if (view.kind === "complaint-form") renderComplaintForm(view);
2147
+ else if (view.kind === "complaint-sent") renderComplaintSent(view);
1604
2148
  else if (view.tab === "home") renderHome();
1605
2149
  else if (view.tab === "messages") renderMessagesTab();
1606
2150
  else if (view.tab === "news" && news().length) renderNewsTab();
@@ -1609,6 +2153,7 @@ function renderMessenger(options) {
1609
2153
  renderHome();
1610
2154
  } else renderHelpTab();
1611
2155
  renderTabs();
2156
+ syncSize();
1612
2157
  }
1613
2158
  const handlers = (seq) => ({
1614
2159
  onConversation(c) {
@@ -1637,8 +2182,8 @@ function renderMessenger(options) {
1637
2182
  upsertMessage(m);
1638
2183
  options.onMessage?.(m, c);
1639
2184
  }
1640
- upsertConversation({ ...c, unread: 0 });
1641
- current = { ...c, unread: 0 };
2185
+ current = withComplaint({ ...c, unread: 0 });
2186
+ upsertConversation(current);
1642
2187
  },
1643
2188
  onError() {
1644
2189
  }
@@ -1691,8 +2236,8 @@ function renderMessenger(options) {
1691
2236
  return !prev || prev.status !== m.status || prev.body !== m.body;
1692
2237
  });
1693
2238
  res.messages.forEach(upsertMessage);
1694
- upsertConversation({ ...res.conversation, unread: 0 });
1695
- current = { ...res.conversation, unread: 0 };
2239
+ current = withComplaint({ ...res.conversation, unread: 0 });
2240
+ upsertConversation(current);
1696
2241
  if (changed || messages.length !== before) {
1697
2242
  renderLog();
1698
2243
  res.messages.filter((m) => m.author !== "user").forEach((m) => options.onMessage?.(m, res.conversation));
@@ -1718,7 +2263,7 @@ function renderMessenger(options) {
1718
2263
  api.loadMessages(next.conversationId).then((res) => {
1719
2264
  if (seq !== threadSeq) return;
1720
2265
  res.messages.forEach(upsertMessage);
1721
- current = { ...res.conversation, unread: 0 };
2266
+ current = withComplaint({ ...res.conversation, unread: 0 });
1722
2267
  upsertConversation(current);
1723
2268
  renderHead(current.subject || "Conversation", () => go({ kind: "tab", tab: "messages" }));
1724
2269
  renderLog();
@@ -1756,6 +2301,7 @@ function renderMessenger(options) {
1756
2301
  } else {
1757
2302
  if (threadTimer) clearTimeout(threadTimer);
1758
2303
  threadTimer = null;
2304
+ syncSize();
1759
2305
  options.onClose?.();
1760
2306
  }
1761
2307
  };
@@ -1793,6 +2339,11 @@ function renderMessenger(options) {
1793
2339
  setOpen(true);
1794
2340
  openArticle(id);
1795
2341
  },
2342
+ openComplaint(categoryId) {
2343
+ if (!canComplain()) return;
2344
+ setOpen(true);
2345
+ startComplaint(categoryId ?? null);
2346
+ },
1796
2347
  refresh() {
1797
2348
  if (destroyed) return;
1798
2349
  if (!open) {