@guidelytech/guide-runtime 0.10.0 → 0.11.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,15 +25,16 @@ __export(messenger_exports, {
25
25
  renderMessenger: () => renderMessenger
26
26
  });
27
27
  module.exports = __toCommonJS(messenger_exports);
28
- var import_runtime5 = require("@guidelytech/shared/runtime");
28
+ var import_runtime6 = require("@guidelytech/shared/runtime");
29
29
 
30
30
  // src/help-view.ts
31
- var import_runtime3 = require("@guidelytech/shared/runtime");
31
+ var import_runtime4 = require("@guidelytech/shared/runtime");
32
32
 
33
33
  // src/pdf-viewer.ts
34
- var import_runtime = require("@guidelytech/shared/runtime");
34
+ var import_runtime2 = require("@guidelytech/shared/runtime");
35
35
 
36
36
  // src/host.ts
37
+ var import_runtime = require("@guidelytech/shared/runtime");
37
38
  function prefersReducedMotion() {
38
39
  try {
39
40
  return typeof window !== "undefined" && typeof window.matchMedia === "function" && window.matchMedia("(prefers-reduced-motion: reduce)").matches;
@@ -44,9 +45,9 @@ function prefersReducedMotion() {
44
45
 
45
46
  // src/pdf-viewer.ts
46
47
  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}`);
48
+ for (const block of Array.from(root.querySelectorAll(`div.${import_runtime2.PDF_BLOCK_CLASS}`))) {
49
+ const link2 = block.querySelector(`a.${import_runtime2.FILE_LINK_CLASS}`);
50
+ const strip = block.querySelector(`.${import_runtime2.PDF_PAGES_CLASS}`);
50
51
  if (!link2 || !strip || block.hasAttribute("data-g-ready")) continue;
51
52
  for (const img of Array.from(strip.querySelectorAll("img:not([src])"))) img.remove();
52
53
  const pages = Array.from(strip.querySelectorAll("img"));
@@ -172,7 +173,7 @@ function openReader(doc, block, link2, pages, start) {
172
173
  }
173
174
 
174
175
  // src/sanitize.ts
175
- var import_runtime2 = require("@guidelytech/shared/runtime");
176
+ var import_runtime3 = require("@guidelytech/shared/runtime");
176
177
  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"]);
177
178
  var DROP_TAGS = /* @__PURE__ */ new Set(["script", "style", "object", "embed", "form", "input", "textarea", "select", "link", "meta", "base", "template", "svg", "math", "frame", "frameset", "noscript"]);
178
179
  var ALLOWED_ATTRS = /* @__PURE__ */ new Set(["href", "src", "alt", "title", "target", "rel", "class", "width", "height", "data-g-action", "data-g-value"]);
@@ -213,18 +214,18 @@ function rebuildMedia(el2) {
213
214
  const src = el2.getAttribute("src");
214
215
  let out;
215
216
  if (tag === "iframe") {
216
- if (!(0, import_runtime2.isAllowedEmbedSrc)(src)) return null;
217
+ if (!(0, import_runtime3.isAllowedEmbedSrc)(src)) return null;
217
218
  out = doc.createElement("iframe");
218
219
  out.setAttribute("src", src);
219
220
  out.setAttribute("title", el2.getAttribute("title") || "Embedded video");
220
- for (const [k, v] of Object.entries(import_runtime2.EMBED_IFRAME_ATTRS)) out.setAttribute(k, v);
221
+ for (const [k, v] of Object.entries(import_runtime3.EMBED_IFRAME_ATTRS)) out.setAttribute(k, v);
221
222
  } else {
222
- if (!(0, import_runtime2.isSafeMediaUrl)(src)) return null;
223
+ if (!(0, import_runtime3.isSafeMediaUrl)(src)) return null;
223
224
  out = doc.createElement("video");
224
225
  out.setAttribute("src", src);
225
226
  const poster = el2.getAttribute("poster");
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);
227
+ if ((0, import_runtime3.isSafeMediaUrl)(poster)) out.setAttribute("poster", poster);
228
+ for (const [k, v] of Object.entries(import_runtime3.VIDEO_ATTRS)) out.setAttribute(k, v);
228
229
  }
229
230
  return out;
230
231
  }
@@ -288,8 +289,8 @@ function resultRow(doc, icon2, title, summary, onOpen, showMe) {
288
289
  return row;
289
290
  }
290
291
  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"));
292
+ for (const a of Array.from(root.querySelectorAll(`a[href^="${import_runtime4.SHOW_ME_HREF_PREFIX}"]`))) {
293
+ const key = (0, import_runtime4.showMeKeyFromHref)(a.getAttribute("href"));
293
294
  const holder = a.closest("p.gd-show-me") ?? a;
294
295
  if (!key || !canStart(key)) {
295
296
  if (holder !== a) holder.remove();
@@ -404,10 +405,11 @@ function createHelpView(options) {
404
405
  if (active) return renderArticle(active);
405
406
  const data = options.data();
406
407
  const member = options.audienceIds?.() ?? [];
407
- const sections = (0, import_runtime3.resourceCenterSections)(data.collections, data.articles, member);
408
+ searchWrap.hidden = !!data.modules && !data.modules.includes("search");
409
+ const sections = (0, import_runtime4.resourceCenterSections)(data.collections, data.articles, member);
408
410
  const q = query.trim().toLowerCase();
409
411
  if (q) {
410
- const walkthroughs = (data.recommended ?? []).filter((g) => (0, import_runtime3.matchesHelpQuery)(g.name, q) && startableGuide(options.guides?.() ?? data.recommended, g.id));
412
+ const walkthroughs = (data.recommended ?? []).filter((g) => (0, import_runtime4.matchesHelpQuery)(g.name, q) && startableGuide(options.guides?.() ?? data.recommended, g.id));
411
413
  if (walkthroughs.length) {
412
414
  section(`Walkthroughs (${walkthroughs.length})`);
413
415
  for (const g of walkthroughs) body.appendChild(resultRow(doc, "\u25B6", g.name, "Show me, step by step", () => options.onStartGuide?.(g.id)));
@@ -416,10 +418,10 @@ function createHelpView(options) {
416
418
  body.append(...options.extra?.(q) ?? []);
417
419
  if (q) {
418
420
  const visible = sections.flatMap((s) => s.articles);
419
- const local = visible.filter((a) => (0, import_runtime3.matchesHelpQuery)(`${a.title} ${a.summary}`, q));
421
+ const local = visible.filter((a) => (0, import_runtime4.matchesHelpQuery)(`${a.title} ${a.summary}`, q));
420
422
  const seen = new Set(local.map((a) => a.id));
421
423
  const hidden = new Set(data.articles.map((a) => a.id).filter((id) => !visible.some((v) => v.id === id)));
422
- const merged = [...local, ...serverResults.filter((a) => !seen.has(a.id) && !hidden.has(a.id) && (0, import_runtime3.isArticleListed)(data.collections, a, member))];
424
+ const merged = [...local, ...serverResults.filter((a) => !seen.has(a.id) && !hidden.has(a.id) && (0, import_runtime4.isArticleListed)(data.collections, a, member))];
423
425
  section(`Results (${merged.length})`);
424
426
  if (!merged.length) body.appendChild(el(doc, "div", "g-rc-empty", "No articles match your search."));
425
427
  merged.forEach(articleButton);
@@ -434,55 +436,124 @@ function createHelpView(options) {
434
436
  body.appendChild(b);
435
437
  });
436
438
  }
437
- if (data.recommended?.length) {
438
- section("Recommended");
439
- data.recommended.forEach((g) => {
440
- const b = listItem(doc, "\u25B6", g.name, "Start guide");
441
- b.addEventListener("click", () => options.onStartGuide?.(g.id));
442
- body.appendChild(b);
443
- });
439
+ const renderers = {
440
+ search: () => {
441
+ },
442
+ announcements: () => {
443
+ if (!data.announcements?.length) return;
444
+ section(import_runtime4.RC_MODULE_LABELS.announcements);
445
+ const seen = data.announcementsSeenAt ? Date.parse(data.announcementsSeenAt) : NaN;
446
+ data.announcements.slice(0, 20).forEach((n) => body.appendChild(announcementItem(n, Number.isNaN(seen) || Date.parse(n.publishedAt) > seen)));
447
+ },
448
+ guides: () => {
449
+ if (!data.recommended?.length) return;
450
+ section("Recommended");
451
+ data.recommended.forEach((g) => {
452
+ const b = listItem(doc, "\u25B6", g.name, "Start guide");
453
+ b.addEventListener("click", () => options.onStartGuide?.(g.id));
454
+ body.appendChild(b);
455
+ });
456
+ },
457
+ checklists: () => {
458
+ if (!data.checklists?.length) return;
459
+ section("Checklists");
460
+ data.checklists.forEach((g) => {
461
+ const b = listItem(doc, "\u2611", g.name, "Open checklist");
462
+ b.addEventListener("click", () => options.onOpenChecklist?.(g.id));
463
+ body.appendChild(b);
464
+ });
465
+ },
466
+ articles: () => {
467
+ for (const s of sections) {
468
+ section(s.collection ? s.path.join(" \u203A ") : "Articles", s.depth);
469
+ s.articles.forEach(articleButton);
470
+ }
471
+ },
472
+ faqs: () => {
473
+ if (!data.faqs?.length) return;
474
+ section("FAQ");
475
+ data.faqs.forEach((f) => {
476
+ const b = listItem(doc, "?", f.question);
477
+ b.setAttribute("aria-expanded", "false");
478
+ const a = el(doc, "div", "g-faq-a");
479
+ a.id = `g-faq-${f.id}`;
480
+ b.setAttribute("aria-controls", a.id);
481
+ setSanitizedHtml(a, f.answer);
482
+ a.hidden = true;
483
+ b.addEventListener("click", () => {
484
+ a.hidden = !a.hidden;
485
+ b.setAttribute("aria-expanded", String(!a.hidden));
486
+ });
487
+ body.append(b, a);
488
+ });
489
+ },
490
+ links: () => {
491
+ if (!data.links?.length) return;
492
+ section("Links");
493
+ data.links.forEach((l) => {
494
+ const a = el(doc, "a", "g-rc-item");
495
+ a.href = l.url;
496
+ a.target = "_blank";
497
+ a.rel = "noopener noreferrer";
498
+ const i = el(doc, "span", "g-rc-icon", "\u2197");
499
+ i.setAttribute("aria-hidden", "true");
500
+ a.append(i, el(doc, "span", "g-rc-title", l.label));
501
+ a.setAttribute("aria-label", `${l.label} (opens in a new tab)`);
502
+ body.appendChild(a);
503
+ });
504
+ }
505
+ };
506
+ for (const id of data.modules ?? import_runtime4.RC_MODULE_IDS) renderers[id]?.();
507
+ if (!body.childNodes.length) body.appendChild(el(doc, "div", "g-rc-empty", "Nothing here yet."));
508
+ }
509
+ function announcementItem(n, unread) {
510
+ const item = el(doc, "article", `g-rc-news${unread ? " g-rc-news-unread" : ""}`);
511
+ const head = el(doc, "div", "g-rc-news-head");
512
+ const title = el(doc, "h3", "g-rc-news-title", n.title);
513
+ head.appendChild(title);
514
+ if (unread) {
515
+ const dot2 = el(doc, "span", "g-rc-news-new", "New");
516
+ head.appendChild(dot2);
444
517
  }
445
- if (data.checklists?.length) {
446
- section("Checklists");
447
- data.checklists.forEach((g) => {
448
- const b = listItem(doc, "\u2611", g.name, "Open checklist");
449
- b.addEventListener("click", () => options.onOpenChecklist?.(g.id));
450
- body.appendChild(b);
451
- });
518
+ const date = el(doc, "time", "g-rc-news-date");
519
+ date.dateTime = n.publishedAt;
520
+ try {
521
+ date.textContent = new Date(n.publishedAt).toLocaleDateString(void 0, { year: "numeric", month: "short", day: "numeric" });
522
+ } catch {
523
+ date.textContent = n.publishedAt.slice(0, 10);
452
524
  }
453
- for (const s of sections) {
454
- section(s.collection ? s.path.join(" \u203A ") : "Articles", s.depth);
455
- s.articles.forEach(articleButton);
525
+ item.append(head, date);
526
+ if (n.body) {
527
+ const b = el(doc, "div", "g-body g-rc-news-body");
528
+ setSanitizedHtml(b, n.body);
529
+ item.appendChild(b);
456
530
  }
457
- if (data.faqs?.length) {
458
- section("FAQ");
459
- data.faqs.forEach((f) => {
460
- const b = listItem(doc, "?", f.question);
461
- b.setAttribute("aria-expanded", "false");
462
- const a = el(doc, "div", "g-faq-a");
463
- setSanitizedHtml(a, f.answer);
464
- a.hidden = true;
465
- b.addEventListener("click", () => {
466
- a.hidden = !a.hidden;
467
- b.setAttribute("aria-expanded", String(!a.hidden));
468
- });
469
- body.append(b, a);
470
- });
531
+ const actions = el(doc, "div", "g-rc-news-actions");
532
+ const guide = n.guideId ? startableGuide(options.guides?.(), n.guideId) : null;
533
+ if (guide) {
534
+ const b = el(doc, "button", "g-btn g-btn-primary", n.linkLabel || "Show me");
535
+ b.type = "button";
536
+ b.addEventListener("click", () => options.onShowMe ? options.onShowMe(guide.key) : options.onStartGuide?.(guide.id));
537
+ actions.appendChild(b);
471
538
  }
472
- if (data.links?.length) {
473
- section("Links");
474
- data.links.forEach((l) => {
475
- const a = el(doc, "a", "g-rc-item");
476
- a.href = l.url;
477
- a.target = "_blank";
478
- a.rel = "noopener noreferrer";
479
- const i = el(doc, "span", "g-rc-icon", "\u2197");
480
- i.setAttribute("aria-hidden", "true");
481
- a.append(i, el(doc, "span", "g-rc-title", l.label));
482
- body.appendChild(a);
483
- });
539
+ if (n.articleId) {
540
+ const listed = options.data().articles.find((a) => a.id === n.articleId);
541
+ if (listed) {
542
+ const b = el(doc, "button", "g-btn g-btn-secondary", guide ? "Read more" : n.linkLabel || "Read more");
543
+ b.type = "button";
544
+ b.addEventListener("click", () => openArticle(listed));
545
+ actions.appendChild(b);
546
+ }
484
547
  }
485
- if (!body.childNodes.length) body.appendChild(el(doc, "div", "g-rc-empty", "Nothing here yet."));
548
+ if (n.url && /^https?:\/\//i.test(n.url)) {
549
+ const a = el(doc, "a", "g-btn g-btn-link", !guide && !n.articleId && n.linkLabel ? n.linkLabel : "Learn more");
550
+ a.href = n.url;
551
+ a.target = "_blank";
552
+ a.rel = "noopener noreferrer";
553
+ actions.appendChild(a);
554
+ }
555
+ if (actions.childNodes.length) item.appendChild(actions);
556
+ return item;
486
557
  }
487
558
  let debounce = null;
488
559
  const onInput = () => {
@@ -636,7 +707,7 @@ function renderHowTo(doc, howTo, onShowMe) {
636
707
  }
637
708
 
638
709
  // src/complaint-view.ts
639
- var import_runtime4 = require("@guidelytech/shared/runtime");
710
+ var import_runtime5 = require("@guidelytech/shared/runtime");
640
711
  var emptyComplaintDraft = (description = "") => ({ description, answers: {}, email: "", attachment: null, error: null, sending: false, checked: false });
641
712
  var COMPLAINT_STEPS = [
642
713
  ["open", "Received"],
@@ -655,7 +726,7 @@ function complaintAnswers(category, draft) {
655
726
  const out = {};
656
727
  for (const f of category.fields) {
657
728
  const v = (draft.answers[f.id] ?? "").trim();
658
- if (v) out[f.id] = v.slice(0, import_runtime4.MAX_COMPLAINT_ANSWER_LENGTH);
729
+ if (v) out[f.id] = v.slice(0, import_runtime5.MAX_COMPLAINT_ANSWER_LENGTH);
659
730
  }
660
731
  return out;
661
732
  }
@@ -740,7 +811,7 @@ function complaintForm(doc, o) {
740
811
  } else {
741
812
  const input = el(doc, "input", "g-input");
742
813
  input.type = "text";
743
- input.maxLength = import_runtime4.MAX_COMPLAINT_ANSWER_LENGTH;
814
+ input.maxLength = import_runtime5.MAX_COMPLAINT_ANSWER_LENGTH;
744
815
  input.value = draft.answers[f.id] ?? "";
745
816
  input.addEventListener("input", () => draft.answers[f.id] = input.value);
746
817
  control = input;
@@ -750,7 +821,7 @@ function complaintForm(doc, o) {
750
821
  }
751
822
  const what = el(doc, "textarea", "g-input g-m-textarea");
752
823
  what.rows = 5;
753
- what.maxLength = import_runtime4.MAX_COMPLAINT_LENGTH;
824
+ what.maxLength = import_runtime5.MAX_COMPLAINT_LENGTH;
754
825
  what.required = true;
755
826
  what.value = draft.description;
756
827
  what.placeholder = "What went wrong, and when?";
@@ -891,19 +962,887 @@ async function readScreenshot(doc, file) {
891
962
  ctx.drawImage(img, 0, 0, canvas.width, canvas.height);
892
963
  for (const q of [0.85, 0.7, 0.5]) {
893
964
  dataUrl = canvas.toDataURL("image/jpeg", q);
894
- if ((dataUrl.length - dataUrl.indexOf(",") - 1) * 0.75 <= import_runtime4.MAX_COMPLAINT_ATTACHMENT_BYTES) break;
965
+ if ((dataUrl.length - dataUrl.indexOf(",") - 1) * 0.75 <= import_runtime5.MAX_COMPLAINT_ATTACHMENT_BYTES) break;
895
966
  }
896
967
  scaled = true;
897
968
  } catch {
898
- dataUrl = file.size <= import_runtime4.MAX_COMPLAINT_ATTACHMENT_BYTES ? await asDataUrl(file) : null;
969
+ dataUrl = file.size <= import_runtime5.MAX_COMPLAINT_ATTACHMENT_BYTES ? await asDataUrl(file) : null;
899
970
  }
900
971
  if (!dataUrl) throw new Error("That image is too large. Screenshots can be up to 2 MB.");
901
972
  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.");
973
+ if (data.length * 0.75 > import_runtime5.MAX_COMPLAINT_ATTACHMENT_BYTES) throw new Error("That image is too large. Screenshots can be up to 2 MB.");
903
974
  const name = (file.name || "screenshot").slice(0, 180);
904
975
  return { fileName: scaled ? `${name.replace(/\.[a-z0-9]+$/i, "")}.jpg` : name, data, previewUrl: dataUrl };
905
976
  }
906
977
 
978
+ // src/ideas-view.ts
979
+ var import_shared = require("@guidelytech/shared");
980
+
981
+ // src/render-safe.ts
982
+ var CITATION = /\s*\[\[article:[^\]]*\]\]/g;
983
+ var INLINE = /\*\*([^*\n]+)\*\*|`([^`\n]+)`|(!?)\[([^\]\n]{1,200})\]\((https?:\/\/[^\s)]+)\)|(https?:\/\/[^\s<>()"']+[^\s<>().,;:!?'"])/g;
984
+ function stripCitationMarkers(text) {
985
+ return text.replace(CITATION, "");
986
+ }
987
+ function safeHttpUrl(raw) {
988
+ try {
989
+ const u = new URL(raw);
990
+ return u.protocol === "http:" || u.protocol === "https:" ? u.href : null;
991
+ } catch {
992
+ return null;
993
+ }
994
+ }
995
+ function link(doc, label, href) {
996
+ const url = safeHttpUrl(href);
997
+ if (!url) return doc.createTextNode(label);
998
+ const a = doc.createElement("a");
999
+ a.href = url;
1000
+ a.target = "_blank";
1001
+ a.rel = "noopener noreferrer nofollow";
1002
+ a.textContent = label;
1003
+ return a;
1004
+ }
1005
+ function appendInline(doc, parent, text) {
1006
+ let last = 0;
1007
+ for (const m of text.matchAll(INLINE)) {
1008
+ const start = m.index ?? 0;
1009
+ if (start > last) parent.appendChild(doc.createTextNode(text.slice(last, start)));
1010
+ if (m[1] !== void 0) {
1011
+ const b = doc.createElement("strong");
1012
+ b.textContent = m[1];
1013
+ parent.appendChild(b);
1014
+ } else if (m[2] !== void 0) {
1015
+ const c = doc.createElement("code");
1016
+ c.textContent = m[2];
1017
+ parent.appendChild(c);
1018
+ } else if (m[4] !== void 0 && m[5] !== void 0) {
1019
+ parent.appendChild(m[3] ? doc.createTextNode(m[4]) : link(doc, m[4], m[5]));
1020
+ } else if (m[6] !== void 0) {
1021
+ parent.appendChild(link(doc, m[6], m[6]));
1022
+ }
1023
+ last = start + m[0].length;
1024
+ }
1025
+ if (last < text.length) parent.appendChild(doc.createTextNode(text.slice(last)));
1026
+ }
1027
+ var BULLET = /^\s*[-*•]\s+/;
1028
+ var NUMBERED = /^\s*\d{1,3}[.)]\s+/;
1029
+ function renderSafeText(doc, text) {
1030
+ const frag = doc.createDocumentFragment();
1031
+ const blocks = stripCitationMarkers(text).replace(/\r\n?/g, "\n").trim().split(/\n{2,}/);
1032
+ for (const block of blocks) {
1033
+ if (!block.trim()) continue;
1034
+ const lines = block.split("\n");
1035
+ if (lines.every((l) => BULLET.test(l)) || lines.every((l) => NUMBERED.test(l))) {
1036
+ const ordered = NUMBERED.test(lines[0]);
1037
+ const list = doc.createElement(ordered ? "ol" : "ul");
1038
+ for (const line of lines) {
1039
+ const li = doc.createElement("li");
1040
+ appendInline(doc, li, line.replace(ordered ? NUMBERED : BULLET, ""));
1041
+ list.appendChild(li);
1042
+ }
1043
+ frag.appendChild(list);
1044
+ continue;
1045
+ }
1046
+ const p = doc.createElement("p");
1047
+ lines.forEach((line, i) => {
1048
+ if (i) p.appendChild(doc.createElement("br"));
1049
+ appendInline(doc, p, line);
1050
+ });
1051
+ frag.appendChild(p);
1052
+ }
1053
+ return frag;
1054
+ }
1055
+
1056
+ // src/ideas-view.ts
1057
+ var SVG_NS = "http://www.w3.org/2000/svg";
1058
+ var IDEA_ICON = ["M9 18h6", "M10 21h4", "M12 3a6 6 0 0 0-3.6 10.8c.7.5 1.1 1.3 1.1 2.1V16h5v-.1c0-.8.4-1.6 1.1-2.1A6 6 0 0 0 12 3z"];
1059
+ var UP_ICON = ["m6 15 6-6 6 6"];
1060
+ function svgIcon(doc, paths, cls = "g-m-ico") {
1061
+ const svg = doc.createElementNS(SVG_NS, "svg");
1062
+ svg.setAttribute("viewBox", "0 0 24 24");
1063
+ svg.setAttribute("aria-hidden", "true");
1064
+ svg.setAttribute("class", cls);
1065
+ for (const d of paths) {
1066
+ const p = doc.createElementNS(SVG_NS, "path");
1067
+ p.setAttribute("d", d);
1068
+ svg.appendChild(p);
1069
+ }
1070
+ return svg;
1071
+ }
1072
+ var plural = (n, one, many = `${one}s`) => `${n} ${n === 1 ? one : many}`;
1073
+ function shortDate(iso) {
1074
+ const d = new Date(iso);
1075
+ if (Number.isNaN(d.getTime())) return "";
1076
+ try {
1077
+ return d.toLocaleDateString(void 0, { month: "short", day: "numeric" });
1078
+ } catch {
1079
+ return d.toISOString().slice(0, 10);
1080
+ }
1081
+ }
1082
+ var errorText = (e, fallback) => (e instanceof Error && e.message ? e.message : fallback).replace(/^[\w.]+: /, "");
1083
+ function ideaStatusPill(doc, status) {
1084
+ return el(doc, "span", `g-m-pill g-m-idea-pill g-m-idea-${status}`, import_shared.FEEDBACK_STATUS_LABEL[status] ?? status);
1085
+ }
1086
+ function voteControl(doc, idea, o) {
1087
+ const count = el(doc, "span", "g-m-vote-count", String(idea.votes));
1088
+ if (!o.canVote) {
1089
+ const box = el(doc, "span", "g-m-vote g-m-vote-static");
1090
+ box.setAttribute("aria-label", plural(idea.votes, "vote"));
1091
+ box.append(svgIcon(doc, UP_ICON, "g-m-ico g-m-vote-ico"), count);
1092
+ return box;
1093
+ }
1094
+ const b = el(doc, "button", `g-m-vote${idea.myVote ? " g-m-voted" : ""}`);
1095
+ b.type = "button";
1096
+ b.setAttribute("aria-pressed", String(!!idea.myVote));
1097
+ b.setAttribute("aria-label", `${idea.myVote ? "Remove your vote for" : "Vote for"} ${idea.title} (${plural(idea.votes, "vote")})`);
1098
+ b.disabled = !!o.busy;
1099
+ b.append(svgIcon(doc, UP_ICON, "g-m-ico g-m-vote-ico"), count);
1100
+ b.addEventListener("click", (e) => {
1101
+ e.stopPropagation();
1102
+ o.onToggle?.();
1103
+ });
1104
+ return b;
1105
+ }
1106
+ function ideaRow(doc, idea, o) {
1107
+ const row = el(doc, "div", "g-m-idea");
1108
+ row.setAttribute("role", "listitem");
1109
+ row.appendChild(voteControl(doc, idea, { canVote: o.canVote, busy: o.busy, onToggle: o.onVote }));
1110
+ const open = el(doc, "button", "g-m-idea-open");
1111
+ open.type = "button";
1112
+ open.appendChild(el(doc, "span", "g-m-idea-title", idea.title));
1113
+ const meta = el(doc, "span", "g-m-idea-meta");
1114
+ meta.appendChild(ideaStatusPill(doc, idea.status));
1115
+ if (idea.comments) meta.appendChild(el(doc, "span", "g-m-time", plural(idea.comments, "comment")));
1116
+ if (idea.productArea) meta.appendChild(el(doc, "span", "g-m-time", idea.productArea));
1117
+ if (idea.mine) meta.appendChild(el(doc, "span", "g-m-time", "Yours"));
1118
+ open.appendChild(meta);
1119
+ open.addEventListener("click", o.onOpen);
1120
+ row.appendChild(open);
1121
+ return row;
1122
+ }
1123
+ var FILTERS = [
1124
+ ["open", "Open"],
1125
+ ["all", "All"],
1126
+ ["mine", "Mine"],
1127
+ ["following", "Following"]
1128
+ ];
1129
+ var SORTS = [
1130
+ ["top", "Top"],
1131
+ ["new", "New"],
1132
+ ["trending", "Trending"]
1133
+ ];
1134
+ function filterChips(doc, current, onPick) {
1135
+ const wrap = el(doc, "div", "g-m-filters");
1136
+ wrap.setAttribute("role", "group");
1137
+ wrap.setAttribute("aria-label", "Show");
1138
+ for (const [f, label] of FILTERS) {
1139
+ const b = el(doc, "button", "g-m-filter", label);
1140
+ b.type = "button";
1141
+ b.dataset.filter = f;
1142
+ b.setAttribute("aria-pressed", String(f === current));
1143
+ b.addEventListener("click", () => {
1144
+ wrap.querySelectorAll("button").forEach((x) => x.setAttribute("aria-pressed", String(x === b)));
1145
+ onPick(f);
1146
+ });
1147
+ wrap.appendChild(b);
1148
+ }
1149
+ return wrap;
1150
+ }
1151
+ function sortSelect(doc, current, onChange) {
1152
+ const select = el(doc, "select", "g-input g-m-sort");
1153
+ select.setAttribute("aria-label", "Sort ideas");
1154
+ for (const [s, label] of SORTS) {
1155
+ const opt = el(doc, "option", "", label);
1156
+ opt.value = s;
1157
+ select.appendChild(opt);
1158
+ }
1159
+ select.value = current;
1160
+ select.addEventListener("change", () => onChange(select.value));
1161
+ return select;
1162
+ }
1163
+ function priorityChoice(doc, current, onPick, label = "How important is this to you?") {
1164
+ const wrap = el(doc, "div", "g-m-field");
1165
+ const id = `g-m-prio${++fieldSeq2}`;
1166
+ const l = el(doc, "span", "g-m-label", label);
1167
+ l.id = id;
1168
+ const group = el(doc, "div", "g-m-seg");
1169
+ group.setAttribute("role", "radiogroup");
1170
+ group.setAttribute("aria-labelledby", id);
1171
+ for (const p of import_shared.FEEDBACK_PRIORITIES) {
1172
+ const b = el(doc, "button", "g-m-seg-btn", import_shared.FEEDBACK_PRIORITY_LABEL[p]);
1173
+ b.type = "button";
1174
+ b.dataset.priority = p;
1175
+ b.setAttribute("role", "radio");
1176
+ b.setAttribute("aria-checked", String(p === current));
1177
+ b.addEventListener("click", () => {
1178
+ group.querySelectorAll("button").forEach((x) => x.setAttribute("aria-checked", String(x === b)));
1179
+ onPick(p);
1180
+ });
1181
+ group.appendChild(b);
1182
+ }
1183
+ wrap.append(l, group);
1184
+ return wrap;
1185
+ }
1186
+ var fieldSeq2 = 0;
1187
+ function field2(doc, label, control, opts = {}) {
1188
+ const id = `g-m-i${++fieldSeq2}`;
1189
+ control.id = id;
1190
+ const wrap = el(doc, "div", "g-m-field");
1191
+ const l = el(doc, "label", "g-m-label", label);
1192
+ l.htmlFor = id;
1193
+ if (opts.optional) l.appendChild(el(doc, "span", "g-m-optional", " (optional)"));
1194
+ if (opts.invalid) control.setAttribute("aria-invalid", "true");
1195
+ wrap.append(l, control);
1196
+ return wrap;
1197
+ }
1198
+ function errorLine(doc, text) {
1199
+ const p = el(doc, "p", "g-m-error", text);
1200
+ p.setAttribute("role", "alert");
1201
+ return p;
1202
+ }
1203
+ function button(doc, cls, text, onClick) {
1204
+ const b = el(doc, "button", cls, text);
1205
+ b.type = "button";
1206
+ b.addEventListener("click", onClick);
1207
+ return b;
1208
+ }
1209
+ function writeNotice(doc, reason, onPortal) {
1210
+ const box = el(doc, "div", "g-m-idea-notice");
1211
+ box.setAttribute("role", "note");
1212
+ box.appendChild(el(doc, "span", "", reason));
1213
+ if (onPortal) box.appendChild(button(doc, "g-m-text-btn", "Open the feedback portal", onPortal));
1214
+ return box;
1215
+ }
1216
+ function comment(doc, c) {
1217
+ const li = el(doc, "li", `g-m-comment${c.authorType === "team" ? " g-m-comment-team" : ""}`);
1218
+ const who = el(doc, "div", "g-m-row g-m-center g-m-comment-meta");
1219
+ who.appendChild(el(doc, "span", "g-m-comment-author", c.authorName || (c.authorType === "team" ? "Team" : "Someone")));
1220
+ if (c.authorType === "team") who.appendChild(el(doc, "span", "g-m-ai", "Team"));
1221
+ who.appendChild(el(doc, "span", "g-m-time", shortDate(c.createdAt)));
1222
+ const body = el(doc, "div", "g-m-comment-body");
1223
+ body.appendChild(renderSafeText(doc, c.body));
1224
+ li.append(who, body);
1225
+ return li;
1226
+ }
1227
+ function ideaDetail(doc, o) {
1228
+ const { idea } = o;
1229
+ const wrap = el(doc, "div", "g-m-form g-m-idea-detail");
1230
+ const meta = el(doc, "div", "g-m-row g-m-center g-m-idea-meta");
1231
+ meta.append(el(doc, "span", "g-m-time", (0, import_shared.feedbackReference)(idea.number)), ideaStatusPill(doc, idea.status));
1232
+ if (idea.productArea) meta.appendChild(el(doc, "span", "g-m-time", idea.productArea));
1233
+ wrap.append(meta, el(doc, "h3", "g-m-form-title", idea.title));
1234
+ if (idea.description.trim()) {
1235
+ const d = el(doc, "div", "g-m-idea-desc");
1236
+ d.appendChild(renderSafeText(doc, idea.description));
1237
+ wrap.appendChild(d);
1238
+ }
1239
+ if (idea.statusNote.trim()) {
1240
+ const note = el(doc, "div", "g-m-idea-note");
1241
+ note.appendChild(el(doc, "span", "g-m-card-label", `${import_shared.FEEDBACK_STATUS_LABEL[idea.status] ?? idea.status} \xB7 from the team`));
1242
+ note.appendChild(renderSafeText(doc, idea.statusNote));
1243
+ wrap.appendChild(note);
1244
+ }
1245
+ const panel = el(doc, "div", "g-m-card g-m-static g-m-idea-vote");
1246
+ const top = el(doc, "div", "g-m-row g-m-center");
1247
+ top.appendChild(voteControl(doc, idea, { canVote: false }));
1248
+ const said = el(doc, "span", "g-m-grow");
1249
+ said.appendChild(el(doc, "span", "g-m-card-title", plural(idea.votes, "vote")));
1250
+ if (idea.myVote) said.appendChild(el(doc, "span", "g-m-card-sub", `You voted \xB7 ${import_shared.FEEDBACK_PRIORITY_LABEL[idea.myVote.priority]}`));
1251
+ top.appendChild(said);
1252
+ if (!o.blocked) {
1253
+ const follow = button(doc, `g-m-follow${idea.following ? " g-m-following" : ""}`, idea.following ? "Following" : "Follow", () => o.onFollow(!idea.following));
1254
+ follow.setAttribute("aria-pressed", String(idea.following));
1255
+ follow.disabled = o.busy;
1256
+ follow.title = idea.following ? "Stop getting updates about this idea" : "Get updates here when it changes";
1257
+ top.appendChild(follow);
1258
+ }
1259
+ panel.appendChild(top);
1260
+ if (o.error) panel.appendChild(errorLine(doc, o.error));
1261
+ if (o.blocked) panel.appendChild(writeNotice(doc, o.blocked, o.onPortal));
1262
+ else if (idea.myVote) {
1263
+ panel.appendChild(priorityChoice(doc, idea.myVote.priority, (p) => p !== idea.myVote?.priority && o.onVote(p, ""), "How much do you need it?"));
1264
+ const remove = button(doc, "g-m-text-btn g-m-unvote", "Remove my vote", o.onUnvote);
1265
+ remove.disabled = o.busy;
1266
+ panel.appendChild(remove);
1267
+ } else {
1268
+ const vote = o.vote;
1269
+ panel.appendChild(priorityChoice(doc, vote.priority, (p) => vote.priority = p));
1270
+ const note = el(doc, "textarea", "g-input g-m-idea-why");
1271
+ note.rows = 2;
1272
+ note.maxLength = import_shared.MAX_FEEDBACK_NOTE;
1273
+ note.value = vote.note;
1274
+ note.placeholder = "What would it let you do?";
1275
+ note.addEventListener("input", () => vote.note = note.value);
1276
+ panel.appendChild(field2(doc, "Why it matters", note, { optional: true }));
1277
+ const go = button(doc, "g-btn g-btn-primary g-m-wide g-m-vote-submit", o.busy ? "Voting\u2026" : "Vote for this idea", () => o.onVote(vote.priority, vote.note.trim()));
1278
+ go.disabled = o.busy;
1279
+ panel.appendChild(go);
1280
+ }
1281
+ wrap.appendChild(panel);
1282
+ const comments = el(doc, "div", "g-m-comments");
1283
+ comments.appendChild(el(doc, "h4", "g-m-comments-title", idea.thread.length ? `Comments (${idea.thread.length})` : "Comments"));
1284
+ if (idea.thread.length) {
1285
+ const list = el(doc, "ul", "g-m-comment-list");
1286
+ idea.thread.forEach((c) => list.appendChild(comment(doc, c)));
1287
+ comments.appendChild(list);
1288
+ } else comments.appendChild(el(doc, "p", "g-m-hint", "No comments yet."));
1289
+ if (!o.blocked) {
1290
+ const form = el(doc, "form", "g-m-comment-form");
1291
+ form.noValidate = true;
1292
+ const box = el(doc, "textarea", "g-input");
1293
+ box.rows = 2;
1294
+ box.maxLength = import_shared.MAX_FEEDBACK_COMMENT;
1295
+ box.placeholder = "Add a comment\u2026";
1296
+ box.setAttribute("aria-label", "Comment");
1297
+ box.value = o.commentDraft;
1298
+ const post = el(doc, "button", "g-btn g-btn-secondary", "Post");
1299
+ post.type = "submit";
1300
+ post.disabled = o.busy || !o.commentDraft.trim();
1301
+ box.addEventListener("input", () => {
1302
+ o.onCommentInput(box.value);
1303
+ post.disabled = o.busy || !box.value.trim();
1304
+ });
1305
+ form.addEventListener("submit", (e) => {
1306
+ e.preventDefault();
1307
+ if (box.value.trim() && !o.busy) o.onComment();
1308
+ });
1309
+ form.append(box, post);
1310
+ comments.appendChild(form);
1311
+ }
1312
+ wrap.appendChild(comments);
1313
+ return wrap;
1314
+ }
1315
+ var emptyIdeaDraft = (over = {}) => ({ title: "", description: "", productArea: "", priority: "nice_to_have", conversationId: null, similar: null, similarFor: "", error: null, sending: false, checked: false, ...over });
1316
+ function ideaProblem(draft) {
1317
+ return draft.title.trim().length < 3 ? "Give your idea a short title." : null;
1318
+ }
1319
+ function ideaForm(doc, o) {
1320
+ const { draft } = o;
1321
+ const form = el(doc, "form", "g-m-form g-m-idea-form");
1322
+ form.noValidate = true;
1323
+ const title = el(doc, "input", "g-input");
1324
+ title.type = "text";
1325
+ title.maxLength = import_shared.MAX_FEEDBACK_TITLE;
1326
+ title.required = true;
1327
+ title.value = draft.title;
1328
+ title.placeholder = "Your idea in a few words";
1329
+ title.addEventListener("input", () => {
1330
+ draft.title = title.value;
1331
+ o.onTitleInput();
1332
+ });
1333
+ form.appendChild(field2(doc, "Idea", title, { invalid: draft.checked && !!ideaProblem(draft) }));
1334
+ const similar = el(doc, "div", "g-m-similar");
1335
+ similar.setAttribute("aria-live", "polite");
1336
+ form.appendChild(similar);
1337
+ const desc = el(doc, "textarea", "g-input g-m-textarea");
1338
+ desc.rows = 4;
1339
+ desc.maxLength = import_shared.MAX_FEEDBACK_DESCRIPTION;
1340
+ desc.value = draft.description;
1341
+ desc.placeholder = "What are you trying to do, and what gets in the way?";
1342
+ desc.addEventListener("input", () => draft.description = desc.value);
1343
+ form.appendChild(field2(doc, "Details", desc, { optional: true }));
1344
+ if (o.productAreas.length) {
1345
+ const select = el(doc, "select", "g-input");
1346
+ const opt = (label, value) => {
1347
+ const x = el(doc, "option", "", label);
1348
+ x.value = value;
1349
+ return x;
1350
+ };
1351
+ select.appendChild(opt("Choose one (optional)", ""));
1352
+ o.productAreas.forEach((a) => select.appendChild(opt(a, a)));
1353
+ select.value = o.productAreas.includes(draft.productArea) ? draft.productArea : "";
1354
+ select.addEventListener("change", () => draft.productArea = select.value);
1355
+ form.appendChild(field2(doc, "Product area", select, { optional: true }));
1356
+ }
1357
+ form.appendChild(priorityChoice(doc, draft.priority, (p) => draft.priority = p));
1358
+ const foot = el(doc, "div", "g-m-form-foot");
1359
+ if (draft.error) foot.appendChild(errorLine(doc, draft.error));
1360
+ const submit = el(doc, "button", "g-btn g-btn-primary g-m-wide", draft.sending ? "Sending\u2026" : "Submit idea");
1361
+ submit.type = "submit";
1362
+ submit.disabled = draft.sending;
1363
+ foot.appendChild(submit);
1364
+ form.appendChild(foot);
1365
+ form.addEventListener("submit", (e) => {
1366
+ e.preventDefault();
1367
+ if (!draft.sending) o.onSubmit();
1368
+ });
1369
+ return { form, similar };
1370
+ }
1371
+ function similarIdeas(doc, ideas, row) {
1372
+ if (!ideas.length) return null;
1373
+ const box = el(doc, "div", "g-m-similar-box");
1374
+ box.appendChild(el(doc, "div", "g-m-card-label", "Similar ideas \u2014 vote instead?"));
1375
+ const list = el(doc, "div", "g-m-idea-list");
1376
+ list.setAttribute("role", "list");
1377
+ ideas.slice(0, 5).forEach((i) => list.appendChild(row(i)));
1378
+ box.appendChild(list);
1379
+ return box;
1380
+ }
1381
+ function ideaSent(doc, o) {
1382
+ const wrap = el(doc, "div", "g-m-form g-m-sent");
1383
+ const top = el(doc, "div", "g-m-sent-top");
1384
+ const tick = el(doc, "span", "g-m-sent-tick", "\u2713");
1385
+ tick.setAttribute("aria-hidden", "true");
1386
+ const p = el(doc, "p", "g-m-card-sub");
1387
+ p.append(el(doc, "strong", "", `${(0, import_shared.feedbackReference)(o.idea.number)} \xB7 ${o.idea.title}`), o.idea.following ? ". You're following it: updates show up here when its status changes." : ". Others can now vote for it.");
1388
+ top.append(tick, el(doc, "h3", "g-m-form-title", "Thanks for your idea"), p);
1389
+ wrap.append(top, button(doc, "g-btn g-btn-primary g-m-wide", "View your idea", o.onView), button(doc, "g-btn g-btn-secondary g-m-wide", "Back to ideas", o.onBack));
1390
+ return wrap;
1391
+ }
1392
+ function updatesList(doc, updates, onOpen) {
1393
+ const wrap = el(doc, "div", "g-m-idea-updates");
1394
+ if (!updates.length) {
1395
+ wrap.appendChild(el(doc, "div", "g-m-empty", "No updates yet. Follow or vote for an idea to hear when it changes."));
1396
+ return wrap;
1397
+ }
1398
+ for (const u of updates) {
1399
+ const card = el(doc, "button", "g-m-card g-m-update");
1400
+ card.type = "button";
1401
+ card.disabled = !u.requestId;
1402
+ const row = el(doc, "span", "g-m-row");
1403
+ if (!u.read) row.appendChild(el(doc, "span", "g-m-dot"));
1404
+ const text = el(doc, "span", "g-m-grow");
1405
+ text.appendChild(el(doc, "span", "g-m-card-title", u.title));
1406
+ if (u.body) text.appendChild(el(doc, "span", "g-m-card-sub g-m-clamp", u.body));
1407
+ row.append(text, el(doc, "span", "g-m-time", shortDate(u.createdAt)));
1408
+ card.appendChild(row);
1409
+ card.setAttribute("aria-label", `${u.title}${u.read ? "" : ", new"}`);
1410
+ card.addEventListener("click", () => onOpen(u));
1411
+ wrap.appendChild(card);
1412
+ }
1413
+ return wrap;
1414
+ }
1415
+ var LIST_TTL_MS = 15e3;
1416
+ var asDetail = (d) => ({ ...d, title: d.title ?? "", description: d.description ?? "", statusNote: d.statusNote ?? "", votes: d.votes ?? 0, thread: Array.isArray(d.thread) ? d.thread : [] });
1417
+ function createIdeas(o) {
1418
+ const { doc, api } = o;
1419
+ let query = { sort: "top", filter: "open", page: 1 };
1420
+ let list = null;
1421
+ let hasMore = false;
1422
+ let listAt = 0;
1423
+ let listLoading = false;
1424
+ let listError = null;
1425
+ let listSeq = 0;
1426
+ let listActionError = null;
1427
+ const details = /* @__PURE__ */ new Map();
1428
+ const detailErrors = /* @__PURE__ */ new Map();
1429
+ const actionErrors = /* @__PURE__ */ new Map();
1430
+ const voteDrafts = /* @__PURE__ */ new Map();
1431
+ const commentDrafts = /* @__PURE__ */ new Map();
1432
+ const busy = /* @__PURE__ */ new Set();
1433
+ const offers = /* @__PURE__ */ new Map();
1434
+ let updates = null;
1435
+ let updatesAt = 0;
1436
+ let updatesLoading = false;
1437
+ let updatesSeq = 0;
1438
+ let draft = emptyIdeaDraft();
1439
+ let similarTimer = null;
1440
+ let similarSeq = 0;
1441
+ let searchTimer = null;
1442
+ let who = o.identified();
1443
+ let mounted = null;
1444
+ const mount = (root, paint) => {
1445
+ mounted = { root, paint };
1446
+ paint();
1447
+ };
1448
+ const repaint = () => {
1449
+ if (mounted?.root.isConnected) mounted.paint();
1450
+ };
1451
+ const showing = (root) => mounted?.root === root && root.isConnected;
1452
+ const cfg = () => o.config() ?? { title: "Ideas", anonymous: false, readOnly: true, productAreas: [], portal: false };
1453
+ function blocked() {
1454
+ const c = cfg();
1455
+ if (c.readOnly || !api.voteIdea) return c.portal ? "Voting and new ideas happen in our feedback portal." : "You can browse ideas here. Voting isn't open in the app.";
1456
+ if (!c.anonymous && !o.identified()) return "Log in to vote, comment or share an idea.";
1457
+ return null;
1458
+ }
1459
+ const canPortal = () => cfg().portal && !!api.ideaPortalLink;
1460
+ function openPortal() {
1461
+ if (!api.ideaPortalLink) return;
1462
+ const win = doc.defaultView;
1463
+ let tab = null;
1464
+ try {
1465
+ tab = win?.open("about:blank", "_blank") ?? null;
1466
+ if (tab) tab.opener = null;
1467
+ } catch {
1468
+ tab = null;
1469
+ }
1470
+ api.ideaPortalLink().then((raw) => {
1471
+ const url = safeHttpUrl(raw);
1472
+ if (!url) throw new Error("The portal link isn't available.");
1473
+ if (tab) tab.location.href = url;
1474
+ else win?.open(url, "_blank", "noopener");
1475
+ }).catch((e) => {
1476
+ tab?.close();
1477
+ listActionError = errorText(e, "The feedback portal couldn't be opened.");
1478
+ repaint();
1479
+ });
1480
+ }
1481
+ const portal = () => canPortal() ? openPortal : void 0;
1482
+ function patch(idea) {
1483
+ if (list) list = list.map((x) => x.id === idea.id ? { ...x, ...idea } : x);
1484
+ const d = details.get(idea.id);
1485
+ if (d) details.set(idea.id, { ...d, ...idea, thread: "thread" in idea ? idea.thread : d.thread });
1486
+ if (draft.similar) draft.similar = draft.similar.map((x) => x.id === idea.id ? { ...x, ...idea } : x);
1487
+ }
1488
+ const find = (id) => details.get(id) ?? list?.find((x) => x.id === id) ?? draft.similar?.find((x) => x.id === id);
1489
+ function toggleVote(idea) {
1490
+ if (busy.has(idea.id) || blocked() || !api.voteIdea || idea.myVote && !api.unvoteIdea) return;
1491
+ busy.add(idea.id);
1492
+ listActionError = null;
1493
+ const voted = !!idea.myVote;
1494
+ patch({ ...idea, votes: Math.max(0, idea.votes + (voted ? -1 : 1)), myVote: voted ? null : { priority: "nice_to_have" } });
1495
+ repaint();
1496
+ (voted ? api.unvoteIdea(idea.id) : api.voteIdea(idea.id, { priority: "nice_to_have" })).then(patch).catch((e) => {
1497
+ patch(idea);
1498
+ listActionError = errorText(e, "Your vote couldn't be saved.");
1499
+ }).finally(() => {
1500
+ busy.delete(idea.id);
1501
+ repaint();
1502
+ });
1503
+ }
1504
+ function loadList(append = false) {
1505
+ if (!api.listIdeas) return;
1506
+ const seq = ++listSeq;
1507
+ listLoading = true;
1508
+ listError = null;
1509
+ const q = { ...query };
1510
+ api.listIdeas(q).then((res) => {
1511
+ if (seq !== listSeq) return;
1512
+ const got = Array.isArray(res?.requests) ? res.requests : [];
1513
+ list = append && list ? [...list, ...got.filter((r) => !list.some((x) => x.id === r.id))] : got;
1514
+ hasMore = !!res?.hasMore;
1515
+ listAt = Date.now();
1516
+ }).catch((e) => {
1517
+ if (seq === listSeq) listError = errorText(e, "Ideas couldn't be loaded.");
1518
+ }).finally(() => {
1519
+ if (seq !== listSeq) return;
1520
+ listLoading = false;
1521
+ repaint();
1522
+ });
1523
+ }
1524
+ const requery = (change) => {
1525
+ query = { ...query, ...change, page: 1 };
1526
+ loadList();
1527
+ repaint();
1528
+ };
1529
+ function listScreen() {
1530
+ const root = el(doc, "div", "g-m-scroll g-m-ideas");
1531
+ const top = el(doc, "div", "g-m-ideas-top");
1532
+ const why = blocked();
1533
+ if (why) top.appendChild(writeNotice(doc, why, portal()));
1534
+ else top.appendChild(button(doc, "g-btn g-btn-primary g-m-wide", "Suggest an idea", o.openForm));
1535
+ const updatesLink = el(doc, "div");
1536
+ top.appendChild(updatesLink);
1537
+ const search = el(doc, "input", "g-input g-m-idea-search");
1538
+ search.type = "search";
1539
+ search.placeholder = "Search ideas";
1540
+ search.setAttribute("aria-label", "Search ideas");
1541
+ search.maxLength = 200;
1542
+ search.value = query.q ?? "";
1543
+ search.addEventListener("input", () => {
1544
+ if (searchTimer) clearTimeout(searchTimer);
1545
+ searchTimer = setTimeout(() => requery({ q: search.value.trim() || void 0 }), 300);
1546
+ });
1547
+ const tools = el(doc, "div", "g-m-row g-m-center g-m-idea-tools");
1548
+ tools.append(filterChips(doc, query.filter, (filter) => requery({ filter })), sortSelect(doc, query.sort, (sort) => requery({ sort })));
1549
+ top.append(search, tools);
1550
+ const results = el(doc, "div", "g-m-idea-results");
1551
+ root.append(top, results);
1552
+ mount(root, () => {
1553
+ updatesLink.textContent = "";
1554
+ const n = updates?.unread ?? 0;
1555
+ if (updates?.updates.length) updatesLink.appendChild(button(doc, "g-m-text-btn g-m-updates-link", n ? `Updates on your ideas (${n} new)` : "Updates on your ideas", o.openUpdates));
1556
+ results.textContent = "";
1557
+ if (listActionError) results.appendChild(errorLine(doc, listActionError));
1558
+ if (!list) {
1559
+ if (listError) {
1560
+ results.append(errorLine(doc, listError), button(doc, "g-m-text-btn", "Try again", () => loadList()));
1561
+ } else results.appendChild(el(doc, "div", "g-m-empty", "Loading ideas\u2026"));
1562
+ return;
1563
+ }
1564
+ if (!list.length) {
1565
+ const empty = query.q ? `No ideas match \u201C${query.q}\u201D.` : query.filter === "mine" ? "You haven't suggested any ideas yet." : query.filter === "following" ? "You aren't following any ideas yet." : "No ideas yet. Be the first to suggest one.";
1566
+ results.appendChild(el(doc, "div", "g-m-empty", empty));
1567
+ } else {
1568
+ const rows = el(doc, "div", "g-m-idea-list");
1569
+ rows.setAttribute("role", "list");
1570
+ rows.setAttribute("aria-busy", String(listLoading));
1571
+ const canVote = !blocked();
1572
+ list.forEach((idea) => rows.appendChild(ideaRow(doc, idea, { canVote, busy: busy.has(idea.id), onVote: () => toggleVote(idea), onOpen: () => o.openIdea(idea.id) })));
1573
+ results.appendChild(rows);
1574
+ }
1575
+ if (listError) results.appendChild(errorLine(doc, listError));
1576
+ if (hasMore) {
1577
+ const more = button(doc, "g-btn g-btn-secondary g-m-wide", listLoading ? "Loading\u2026" : "Show more", () => {
1578
+ query = { ...query, page: query.page + 1 };
1579
+ loadList(true);
1580
+ repaint();
1581
+ });
1582
+ more.disabled = listLoading;
1583
+ results.appendChild(more);
1584
+ }
1585
+ if (!why && canPortal()) results.appendChild(button(doc, "g-m-text-btn g-m-portal-link", "Open the feedback portal", openPortal));
1586
+ });
1587
+ if (!list || Date.now() - listAt > LIST_TTL_MS || query.page > 1) {
1588
+ query = { ...query, page: 1 };
1589
+ loadList();
1590
+ }
1591
+ return root;
1592
+ }
1593
+ function act(id, call, fallback, done) {
1594
+ if (busy.has(id)) return;
1595
+ const p = call();
1596
+ if (!p) return;
1597
+ busy.add(id);
1598
+ actionErrors.delete(id);
1599
+ repaint();
1600
+ p.then((res) => {
1601
+ patch(res);
1602
+ done?.();
1603
+ }).catch((e) => actionErrors.set(id, errorText(e, fallback))).finally(() => {
1604
+ busy.delete(id);
1605
+ repaint();
1606
+ });
1607
+ }
1608
+ function detailScreen(id) {
1609
+ const root = el(doc, "div", "g-m-scroll");
1610
+ mount(root, () => {
1611
+ root.textContent = "";
1612
+ const idea = details.get(id);
1613
+ if (!idea) {
1614
+ const err = detailErrors.get(id);
1615
+ root.appendChild(err ? errorLine(doc, err) : el(doc, "div", "g-m-empty", "Loading\u2026"));
1616
+ return;
1617
+ }
1618
+ if (!voteDrafts.has(id)) voteDrafts.set(id, { priority: "nice_to_have", note: "" });
1619
+ root.appendChild(
1620
+ ideaDetail(doc, {
1621
+ idea,
1622
+ blocked: blocked(),
1623
+ onPortal: portal(),
1624
+ busy: busy.has(id),
1625
+ error: actionErrors.get(id) ?? null,
1626
+ vote: voteDrafts.get(id),
1627
+ commentDraft: commentDrafts.get(id) ?? "",
1628
+ onVote: (priority, note) => act(id, () => api.voteIdea?.(id, { priority, ...note ? { note } : {} }), "Your vote couldn't be saved.", () => voteDrafts.delete(id)),
1629
+ onUnvote: () => act(id, () => api.unvoteIdea?.(id), "Your vote couldn't be removed."),
1630
+ onFollow: (follow) => act(id, () => api.followIdea?.(id, follow), "That didn't work. Please try again."),
1631
+ onCommentInput: (text) => commentDrafts.set(id, text),
1632
+ onComment: () => act(id, () => api.commentIdea?.(id, (commentDrafts.get(id) ?? "").trim()), "Your comment couldn't be posted.", () => commentDrafts.delete(id))
1633
+ })
1634
+ );
1635
+ });
1636
+ if (api.getIdea) {
1637
+ detailErrors.delete(id);
1638
+ api.getIdea(id).then((d) => {
1639
+ details.set(id, asDetail(d));
1640
+ patch(d);
1641
+ }).catch((e) => {
1642
+ if (!details.has(id)) detailErrors.set(id, errorText(e, "This idea couldn't be loaded."));
1643
+ }).finally(() => {
1644
+ if (showing(root)) mounted.paint();
1645
+ });
1646
+ }
1647
+ return root;
1648
+ }
1649
+ function lookUpSimilar(paint) {
1650
+ const title = draft.title.trim();
1651
+ if (title.length < 3 || !api.similarIdeas) {
1652
+ draft.similar = null;
1653
+ draft.similarFor = "";
1654
+ return paint();
1655
+ }
1656
+ if (title === draft.similarFor) return;
1657
+ const seq = ++similarSeq;
1658
+ api.similarIdeas(title).then((res) => {
1659
+ if (seq !== similarSeq) return;
1660
+ draft.similar = res;
1661
+ draft.similarFor = title;
1662
+ paint();
1663
+ }).catch(() => {
1664
+ });
1665
+ }
1666
+ function formScreen() {
1667
+ const root = el(doc, "div", "g-m-scroll");
1668
+ let similarBox = null;
1669
+ const paintSimilar = () => {
1670
+ if (!similarBox) return;
1671
+ similarBox.textContent = "";
1672
+ const canVote = !blocked();
1673
+ const box = similarIdeas(doc, draft.similar ?? [], (idea) => ideaRow(doc, idea, { canVote, busy: busy.has(idea.id), onVote: () => toggleVote(idea), onOpen: () => o.openIdea(idea.id) }));
1674
+ if (box) similarBox.appendChild(box);
1675
+ if (listActionError) similarBox.appendChild(errorLine(doc, listActionError));
1676
+ };
1677
+ const paintAll = () => {
1678
+ root.textContent = "";
1679
+ const why = blocked();
1680
+ if (why) {
1681
+ root.appendChild(writeNotice(doc, why, portal()));
1682
+ similarBox = null;
1683
+ return;
1684
+ }
1685
+ const { form, similar } = ideaForm(doc, {
1686
+ draft,
1687
+ productAreas: cfg().productAreas ?? [],
1688
+ onTitleInput: () => {
1689
+ if (similarTimer) clearTimeout(similarTimer);
1690
+ similarTimer = setTimeout(() => {
1691
+ if (showing(root)) lookUpSimilar(paintSimilar);
1692
+ }, 300);
1693
+ },
1694
+ onSubmit: () => submit(root, paintAll)
1695
+ });
1696
+ root.appendChild(form);
1697
+ similarBox = similar;
1698
+ paintSimilar();
1699
+ };
1700
+ mounted = { root, paint: paintSimilar };
1701
+ paintAll();
1702
+ if (draft.title.trim() && draft.title.trim() !== draft.similarFor) lookUpSimilar(paintSimilar);
1703
+ return root;
1704
+ }
1705
+ function submit(root, paintAll) {
1706
+ const d = draft;
1707
+ d.checked = true;
1708
+ d.error = ideaProblem(d);
1709
+ if (d.error || !api.submitIdea) return paintAll();
1710
+ d.sending = true;
1711
+ paintAll();
1712
+ api.submitIdea({ title: d.title.trim(), description: d.description.trim(), productArea: d.productArea || null, priority: d.priority, conversationId: d.conversationId }).then((idea) => {
1713
+ details.set(idea.id, asDetail(idea));
1714
+ listAt = 0;
1715
+ if (draft === d) draft = emptyIdeaDraft();
1716
+ if (showing(root)) o.openSent(idea);
1717
+ }).catch((e) => {
1718
+ d.sending = false;
1719
+ d.error = errorText(e, "Your idea couldn't be sent.");
1720
+ if (showing(root)) paintAll();
1721
+ });
1722
+ }
1723
+ function updatesScreen() {
1724
+ const root = el(doc, "div", "g-m-scroll g-m-idea-updates-screen");
1725
+ mount(root, () => {
1726
+ root.textContent = "";
1727
+ if (!updates) {
1728
+ root.appendChild(el(doc, "div", "g-m-empty", updatesLoading || api.ideaUpdates ? "Loading\u2026" : "No updates yet."));
1729
+ return;
1730
+ }
1731
+ root.appendChild(updatesList(doc, updates.updates, (u) => u.requestId && o.openIdea(u.requestId)));
1732
+ });
1733
+ if (!updates) loadUpdates();
1734
+ else markRead();
1735
+ return root;
1736
+ }
1737
+ function markRead() {
1738
+ const ids = updates?.updates.filter((u) => !u.read).map((u) => u.id) ?? [];
1739
+ if (!ids.length || !api.markIdeaUpdatesRead) return;
1740
+ const seq = updatesSeq;
1741
+ api.markIdeaUpdatesRead(ids).then((res) => {
1742
+ if (seq !== updatesSeq || !updates) return;
1743
+ updates = { updates: updates.updates, unread: res.unread };
1744
+ o.onUnreadChange();
1745
+ }).catch(() => {
1746
+ });
1747
+ }
1748
+ function loadUpdates(maxAgeMs = 0) {
1749
+ if (!api.ideaUpdates || updatesLoading || Date.now() - updatesAt < maxAgeMs) return;
1750
+ updatesLoading = true;
1751
+ const seq = ++updatesSeq;
1752
+ api.ideaUpdates().then((res) => {
1753
+ if (seq !== updatesSeq) return;
1754
+ const before = updates?.unread ?? 0;
1755
+ updates = { updates: Array.isArray(res?.updates) ? res.updates : [], unread: Number(res?.unread) || 0 };
1756
+ updatesAt = Date.now();
1757
+ if (updates.unread !== before) o.onUnreadChange();
1758
+ if (mounted?.root.isConnected) {
1759
+ mounted.paint();
1760
+ if (mounted.root.classList.contains("g-m-idea-updates-screen")) markRead();
1761
+ }
1762
+ }).catch(() => {
1763
+ }).finally(() => {
1764
+ if (seq === updatesSeq) updatesLoading = false;
1765
+ });
1766
+ }
1767
+ function offerChip(offer, conversationId, lead) {
1768
+ const canWrite = !blocked();
1769
+ const b = el(doc, "button", "g-m-chip g-m-guide g-m-offer g-m-idea-offer");
1770
+ b.type = "button";
1771
+ const label = el(doc, "span");
1772
+ b.append(lead(), label);
1773
+ const id = offer.requestId;
1774
+ if (!id) {
1775
+ if (!canWrite || !api.submitIdea) return null;
1776
+ label.textContent = "Suggest this idea";
1777
+ b.addEventListener("click", () => {
1778
+ startDraft({ title: offer.title, description: offer.description, conversationId });
1779
+ o.openForm();
1780
+ });
1781
+ return b;
1782
+ }
1783
+ const paint = () => {
1784
+ const state = offers.get(id) ?? (find(id)?.myVote ? "voted" : void 0);
1785
+ b.disabled = state === "voting";
1786
+ label.textContent = !canWrite ? `View \u201C${offer.title}\u201D` : state === "voting" ? "Voting\u2026" : state === "voted" ? "Voted \xB7 View the idea" : state === "failed" ? "Couldn't vote \xB7 View the idea" : `Vote for \u201C${offer.title}\u201D`;
1787
+ return state;
1788
+ };
1789
+ paint();
1790
+ b.addEventListener("click", () => {
1791
+ const state = paint();
1792
+ if (!canWrite || state === "voted" || state === "failed" || !api.voteIdea) return o.openIdea(id);
1793
+ if (state === "voting") return;
1794
+ offers.set(id, "voting");
1795
+ paint();
1796
+ api.voteIdea(id, { priority: "nice_to_have", conversationId }).then((idea) => {
1797
+ patch(idea);
1798
+ offers.set(id, "voted");
1799
+ }).catch(() => offers.set(id, "failed")).finally(paint);
1800
+ });
1801
+ return b;
1802
+ }
1803
+ function startDraft(prefill = {}) {
1804
+ draft = emptyIdeaDraft({ title: (prefill.title ?? "").slice(0, import_shared.MAX_FEEDBACK_TITLE), description: (prefill.description ?? "").slice(0, import_shared.MAX_FEEDBACK_DESCRIPTION), conversationId: prefill.conversationId ?? null });
1805
+ }
1806
+ return {
1807
+ blocked,
1808
+ unread: () => updates?.unread ?? 0,
1809
+ listScreen,
1810
+ detailScreen,
1811
+ formScreen,
1812
+ sentScreen: (idea) => {
1813
+ mounted = null;
1814
+ const root = el(doc, "div", "g-m-scroll");
1815
+ root.appendChild(ideaSent(doc, { idea, onView: () => o.openIdea(idea.id), onBack: o.openList }));
1816
+ return root;
1817
+ },
1818
+ updatesScreen,
1819
+ startDraft,
1820
+ offerChip,
1821
+ loadUpdates,
1822
+ sync(open) {
1823
+ const now = o.identified();
1824
+ if (now !== who) {
1825
+ who = now;
1826
+ list = null;
1827
+ listAt = 0;
1828
+ details.clear();
1829
+ offers.clear();
1830
+ updates = null;
1831
+ updatesAt = 0;
1832
+ updatesSeq++;
1833
+ updatesLoading = false;
1834
+ o.onUnreadChange();
1835
+ }
1836
+ if (open) loadUpdates(3e4);
1837
+ },
1838
+ destroy() {
1839
+ if (similarTimer) clearTimeout(similarTimer);
1840
+ if (searchTimer) clearTimeout(searchTimer);
1841
+ mounted = null;
1842
+ }
1843
+ };
1844
+ }
1845
+
907
1846
  // src/messenger-styles.ts
908
1847
  var MESSENGER_STYLES = `
909
1848
  :host([data-g-messenger="bottom-right"]) .g-launcher.g-pos-bottom-right{right:72px}
@@ -1107,90 +2046,81 @@ var MESSENGER_STYLES = `
1107
2046
  .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
2047
  .g-m-step-done{color:var(--g-text);font-weight:600}
1109
2048
  .g-m-step-done .g-m-step-dot{background:var(--g-primary);border-color:var(--g-primary)}
2049
+ .g-m-ideas-top{display:flex;flex-direction:column;gap:10px;margin-bottom:12px}
2050
+ .g-m-ideas-top:empty{display:none}
2051
+ .g-m-idea-tools{justify-content:space-between;gap:8px;flex-wrap:wrap}
2052
+ .g-m-filters{display:flex;gap:6px;flex-wrap:wrap}
2053
+ .g-m-filter{border:1px solid var(--g-border);background:var(--g-bg);color:var(--g-text);font:inherit;font-size:12px;font-weight:600;border-radius:999px;padding:5px 11px;cursor:pointer;min-height:30px}
2054
+ .g-m-filter:hover{background:var(--g-subtle)}
2055
+ .g-m-filter[aria-pressed="true"]{background:var(--g-primary);border-color:var(--g-primary);color:var(--g-primary-text)}
2056
+ .g-m-filter:focus-visible,.g-m-seg-btn:focus-visible,.g-m-vote:focus-visible,.g-m-idea-open:focus-visible,.g-m-follow:focus-visible{outline:2px solid var(--g-primary);outline-offset:2px}
2057
+ .g-m-sort{width:auto;min-width:0;min-height:30px;padding:4px 8px;font-size:12px}
2058
+ .g-m-idea-list{display:flex;flex-direction:column;border:1px solid var(--g-border);border-radius:calc(var(--g-radius) * .8);background:var(--g-bg);overflow:hidden;margin-bottom:12px}
2059
+ .g-m-idea{display:flex;align-items:flex-start;gap:12px;padding:12px 14px;border-top:1px solid var(--g-border)}
2060
+ .g-m-idea:first-child{border-top:0}
2061
+ .g-m-vote{flex:none;width:44px;min-height:48px;border:1px solid var(--g-border);border-radius:10px;background:var(--g-bg);color:var(--g-text);font:inherit;display:inline-flex;flex-direction:column;align-items:center;justify-content:center;gap:0;padding:4px 0;cursor:pointer}
2062
+ button.g-m-vote:hover{border-color:var(--g-primary);color:var(--g-primary)}
2063
+ .g-m-vote:disabled{opacity:.6;cursor:progress}
2064
+ .g-m-vote.g-m-voted{background:color-mix(in srgb,var(--g-primary) 12%,var(--g-bg));border-color:var(--g-primary);color:var(--g-primary)}
2065
+ .g-m-vote-static{cursor:default;background:var(--g-subtle)}
2066
+ .g-m-vote-ico{width:16px;height:16px;stroke-width:2.4}
2067
+ .g-m-vote-count{font-size:13px;font-weight:700;line-height:1.2}
2068
+ .g-m-idea-open{flex:1;min-width:0;border:0;background:transparent;color:var(--g-text);font:inherit;text-align:left;padding:2px 0;cursor:pointer;display:flex;flex-direction:column;gap:6px;border-radius:4px}
2069
+ .g-m-idea-open:hover .g-m-idea-title{color:var(--g-primary)}
2070
+ .g-m-idea-title{font-weight:600;overflow-wrap:anywhere}
2071
+ .g-m-idea-meta{display:flex;align-items:center;gap:8px;flex-wrap:wrap}
2072
+ .g-m-idea-pill.g-m-idea-new{background:var(--g-subtle);color:var(--g-muted)}
2073
+ .g-m-idea-pill.g-m-idea-under_review{background:#e0e7ff;color:#3730a3}
2074
+ .g-m-idea-pill.g-m-idea-planned{background:#dbeafe;color:#1e40af}
2075
+ .g-m-idea-pill.g-m-idea-in_progress{background:#fef3c7;color:#92400e}
2076
+ .g-m-idea-pill.g-m-idea-released{background:#dcfce7;color:#166534}
2077
+ .g-m-idea-pill.g-m-idea-declined{background:#fee2e2;color:#991b1b}
2078
+ .g-m-idea-notice{display:flex;flex-direction:column;align-items:flex-start;gap:2px;border:1px solid var(--g-border);background:var(--g-subtle);border-radius:10px;padding:10px 12px;font-size:13px;color:var(--g-text)}
2079
+ .g-m-idea-notice .g-m-text-btn{padding:4px 0;min-height:0}
2080
+ .g-m-updates-link,.g-m-portal-link{align-self:flex-start;padding-left:0}
2081
+ .g-m-portal-link{display:block;margin:0 auto}
2082
+ .g-m-idea-detail{gap:12px}
2083
+ .g-m-idea-desc,.g-m-idea-note{font-size:14px;overflow-wrap:anywhere}
2084
+ .g-m-idea-desc p,.g-m-idea-note p,.g-m-comment-body p{margin:0 0 8px}
2085
+ .g-m-idea-desc p:last-child,.g-m-idea-note p:last-child,.g-m-comment-body p:last-child{margin:0}
2086
+ .g-m-idea-note{border-left:3px solid var(--g-primary);background:var(--g-subtle);border-radius:6px;padding:8px 10px}
2087
+ .g-m-idea-vote{display:flex;flex-direction:column;gap:12px;margin:0}
2088
+ .g-m-idea-vote .g-m-vote{min-height:44px}
2089
+ .g-m-follow{flex:none;border:1px solid var(--g-border);background:var(--g-bg);color:var(--g-text);font:inherit;font-size:13px;font-weight:600;border-radius:999px;padding:6px 12px;cursor:pointer;min-height:32px}
2090
+ .g-m-follow:hover{background:var(--g-subtle)}
2091
+ .g-m-follow.g-m-following{border-color:var(--g-primary);color:var(--g-primary)}
2092
+ .g-m-follow:disabled{opacity:.6}
2093
+ .g-m-seg{display:flex;border:1px solid var(--g-border);border-radius:10px;overflow:hidden}
2094
+ .g-m-seg-btn{flex:1;border:0;border-left:1px solid var(--g-border);background:var(--g-bg);color:var(--g-text);font:inherit;font-size:12px;font-weight:600;padding:8px 4px;cursor:pointer;min-height:36px}
2095
+ .g-m-seg-btn:first-child{border-left:0}
2096
+ .g-m-seg-btn:hover{background:var(--g-subtle)}
2097
+ .g-m-seg-btn[aria-checked="true"]{background:var(--g-primary);color:var(--g-primary-text)}
2098
+ .g-m-unvote{align-self:flex-start;padding-left:0;color:var(--g-muted)}
2099
+ .g-m-comments{display:flex;flex-direction:column;gap:8px}
2100
+ .g-m-comments-title{margin:4px 0 0;font-size:14px}
2101
+ .g-m-comment-list{list-style:none;margin:0;padding:0;display:flex;flex-direction:column;gap:10px}
2102
+ .g-m-comment{border:1px solid var(--g-border);border-radius:10px;padding:8px 10px;font-size:13px}
2103
+ .g-m-comment-team{background:var(--g-subtle);border-color:color-mix(in srgb,var(--g-primary) 35%,var(--g-border))}
2104
+ .g-m-comment-meta{gap:6px;margin-bottom:4px}
2105
+ .g-m-comment-author{font-weight:600}
2106
+ .g-m-comment-body{overflow-wrap:anywhere}
2107
+ .g-m-comment-form{display:flex;flex-direction:column;align-items:flex-end;gap:6px}
2108
+ .g-m-comment-form textarea{resize:vertical;min-height:56px}
2109
+ .g-m-similar:empty{display:none}
2110
+ .g-m-similar-box{display:flex;flex-direction:column;gap:6px}
2111
+ .g-m-similar-box .g-m-idea-list{margin:0}
2112
+ .g-m-update{display:block}
2113
+ .g-m-update:disabled{cursor:default}
2114
+ .g-m-update:disabled:hover{background:var(--g-bg)}
1110
2115
  `;
1111
2116
 
1112
- // src/render-safe.ts
1113
- var CITATION = /\s*\[\[article:[^\]]*\]\]/g;
1114
- var INLINE = /\*\*([^*\n]+)\*\*|`([^`\n]+)`|(!?)\[([^\]\n]{1,200})\]\((https?:\/\/[^\s)]+)\)|(https?:\/\/[^\s<>()"']+[^\s<>().,;:!?'"])/g;
1115
- function stripCitationMarkers(text) {
1116
- return text.replace(CITATION, "");
1117
- }
1118
- function safeHttpUrl(raw) {
1119
- try {
1120
- const u = new URL(raw);
1121
- return u.protocol === "http:" || u.protocol === "https:" ? u.href : null;
1122
- } catch {
1123
- return null;
1124
- }
1125
- }
1126
- function link(doc, label, href) {
1127
- const url = safeHttpUrl(href);
1128
- if (!url) return doc.createTextNode(label);
1129
- const a = doc.createElement("a");
1130
- a.href = url;
1131
- a.target = "_blank";
1132
- a.rel = "noopener noreferrer nofollow";
1133
- a.textContent = label;
1134
- return a;
1135
- }
1136
- function appendInline(doc, parent, text) {
1137
- let last = 0;
1138
- for (const m of text.matchAll(INLINE)) {
1139
- const start = m.index ?? 0;
1140
- if (start > last) parent.appendChild(doc.createTextNode(text.slice(last, start)));
1141
- if (m[1] !== void 0) {
1142
- const b = doc.createElement("strong");
1143
- b.textContent = m[1];
1144
- parent.appendChild(b);
1145
- } else if (m[2] !== void 0) {
1146
- const c = doc.createElement("code");
1147
- c.textContent = m[2];
1148
- parent.appendChild(c);
1149
- } else if (m[4] !== void 0 && m[5] !== void 0) {
1150
- parent.appendChild(m[3] ? doc.createTextNode(m[4]) : link(doc, m[4], m[5]));
1151
- } else if (m[6] !== void 0) {
1152
- parent.appendChild(link(doc, m[6], m[6]));
1153
- }
1154
- last = start + m[0].length;
1155
- }
1156
- if (last < text.length) parent.appendChild(doc.createTextNode(text.slice(last)));
1157
- }
1158
- var BULLET = /^\s*[-*•]\s+/;
1159
- var NUMBERED = /^\s*\d{1,3}[.)]\s+/;
1160
- function renderSafeText(doc, text) {
1161
- const frag = doc.createDocumentFragment();
1162
- const blocks = stripCitationMarkers(text).replace(/\r\n?/g, "\n").trim().split(/\n{2,}/);
1163
- for (const block of blocks) {
1164
- if (!block.trim()) continue;
1165
- const lines = block.split("\n");
1166
- if (lines.every((l) => BULLET.test(l)) || lines.every((l) => NUMBERED.test(l))) {
1167
- const ordered = NUMBERED.test(lines[0]);
1168
- const list = doc.createElement(ordered ? "ol" : "ul");
1169
- for (const line of lines) {
1170
- const li = doc.createElement("li");
1171
- appendInline(doc, li, line.replace(ordered ? NUMBERED : BULLET, ""));
1172
- list.appendChild(li);
1173
- }
1174
- frag.appendChild(list);
1175
- continue;
1176
- }
1177
- const p = doc.createElement("p");
1178
- lines.forEach((line, i) => {
1179
- if (i) p.appendChild(doc.createElement("br"));
1180
- appendInline(doc, p, line);
1181
- });
1182
- frag.appendChild(p);
1183
- }
1184
- return frag;
1185
- }
1186
-
1187
2117
  // src/messenger.ts
1188
- var SVG_NS = "http://www.w3.org/2000/svg";
2118
+ var SVG_NS2 = "http://www.w3.org/2000/svg";
1189
2119
  function chatIcon(doc) {
1190
- const svg = doc.createElementNS(SVG_NS, "svg");
2120
+ const svg = doc.createElementNS(SVG_NS2, "svg");
1191
2121
  svg.setAttribute("viewBox", "0 0 24 24");
1192
2122
  svg.setAttribute("aria-hidden", "true");
1193
- const path = doc.createElementNS(SVG_NS, "path");
2123
+ const path = doc.createElementNS(SVG_NS2, "path");
1194
2124
  path.setAttribute("d", "M12 3C6.5 3 2 6.8 2 11.5c0 2.4 1.2 4.6 3.1 6.1L4.5 21l3.9-2c1.1.3 2.3.5 3.6.5 5.5 0 10-3.8 10-8.5S17.5 3 12 3z");
1195
2125
  svg.appendChild(path);
1196
2126
  return svg;
@@ -1215,15 +2145,16 @@ var ICONS = {
1215
2145
  circle: ["M12 3a9 9 0 1 0 0 18 9 9 0 0 0 0-18z"],
1216
2146
  expand: ["M15 3h6v6", "M21 3l-7 7", "M9 21H3v-6", "M3 21l7-7"],
1217
2147
  flag: ["M5 21V4", "M5 4h12l-2.5 4L17 12H5"],
1218
- collapse: ["M4 14h6v6", "M3 21l7-7", "M20 10h-6V4", "M21 3l-7 7"]
2148
+ collapse: ["M4 14h6v6", "M3 21l7-7", "M20 10h-6V4", "M21 3l-7 7"],
2149
+ idea: IDEA_ICON
1219
2150
  };
1220
2151
  function icon(doc, name, cls = "g-m-ico") {
1221
- const svg = doc.createElementNS(SVG_NS, "svg");
2152
+ const svg = doc.createElementNS(SVG_NS2, "svg");
1222
2153
  svg.setAttribute("viewBox", "0 0 24 24");
1223
2154
  svg.setAttribute("aria-hidden", "true");
1224
2155
  svg.setAttribute("class", cls);
1225
2156
  for (const d of ICONS[name] ?? []) {
1226
- const path = doc.createElementNS(SVG_NS, "path");
2157
+ const path = doc.createElementNS(SVG_NS2, "path");
1227
2158
  path.setAttribute("d", d);
1228
2159
  svg.appendChild(path);
1229
2160
  }
@@ -1250,7 +2181,7 @@ function shortDateTime(iso) {
1250
2181
  return d.toISOString().slice(0, 16).replace("T", " ");
1251
2182
  }
1252
2183
  }
1253
- function shortDate(iso) {
2184
+ function shortDate2(iso) {
1254
2185
  const d = new Date(iso);
1255
2186
  if (Number.isNaN(d.getTime())) return "";
1256
2187
  try {
@@ -1345,17 +2276,24 @@ function renderMessenger(options) {
1345
2276
  panel.append(head, main, tabs);
1346
2277
  root.append(launcher, panel);
1347
2278
  scope.appendChild(root);
2279
+ function withRcText(items, rc, apply) {
2280
+ if (!rc?.length) return items;
2281
+ const byId = new Map(rc.map((x) => [x.id, x]));
2282
+ return items.map((i) => byId.has(i.id) ? apply(i, byId.get(i.id)) : i);
2283
+ }
2284
+ const helpModules = () => options.resourceCenter?.modules?.length ? (0, import_runtime6.orderedModules)(options.resourceCenter.modules).filter((m) => m.id !== "announcements" && m.enabled && (!("targeting" in m) || !m.targeting || !options.evaluate || options.evaluate(m.targeting))).map((m) => m.id) : void 0;
1348
2285
  const rcExtras = (ids = []) => ids.map((id) => options.guides?.find((g) => g.id === id || g.key === id)).filter((g) => !!g);
1349
2286
  const help = createHelpView({
1350
2287
  doc,
1351
2288
  data: () => ({
1352
- collections: articles?.collections ?? options.resourceCenter?.collections ?? [],
1353
- articles: articles?.articles ?? options.resourceCenter?.articles ?? [],
2289
+ collections: withRcText(articles?.collections ?? options.resourceCenter?.collections ?? [], options.resourceCenter?.collections, (c, t) => ({ ...c, name: t.name, description: t.description })),
2290
+ articles: withRcText(articles?.articles ?? options.resourceCenter?.articles ?? [], options.resourceCenter?.articles, (a, t) => ({ ...a, title: t.title, summary: t.summary })),
1354
2291
  suggested: helpSuggestions(),
1355
2292
  recommended: rcExtras(options.resourceCenter?.recommendedGuideIds),
1356
2293
  checklists: rcExtras(options.resourceCenter?.checklistGuideIds),
1357
2294
  faqs: options.resourceCenter?.faqs,
1358
- links: options.resourceCenter?.links
2295
+ links: options.resourceCenter?.links,
2296
+ modules: helpModules()
1359
2297
  }),
1360
2298
  search: (q) => api.searchArticles(q),
1361
2299
  audienceIds: options.audienceIds,
@@ -1374,6 +2312,24 @@ function renderMessenger(options) {
1374
2312
  onShowMe: (key) => startWalkthrough(key),
1375
2313
  onArticleChange: () => syncSize()
1376
2314
  });
2315
+ const ideasOn = () => !!config.ideas && !!api.listIdeas;
2316
+ const ideasTab = { kind: "tab", tab: "ideas" };
2317
+ const ideas = createIdeas({
2318
+ doc,
2319
+ api,
2320
+ config: () => config.ideas,
2321
+ identified: () => visitor().identified !== false,
2322
+ openIdea: (id) => go({ kind: "idea", id, from: view.kind === "idea" ? view.from : view }),
2323
+ openForm: () => go({ kind: "idea-new", from: view }),
2324
+ openSent: (idea) => go({ kind: "idea-sent", idea }),
2325
+ openList: () => go(ideasTab),
2326
+ openUpdates: () => go({ kind: "idea-updates", from: view }),
2327
+ onUnreadChange: () => {
2328
+ if (!open || destroyed) return;
2329
+ if (view.kind === "tab" && view.tab === "home") render();
2330
+ else renderTabs();
2331
+ }
2332
+ });
1377
2333
  let docCollapsed = false;
1378
2334
  let sizeButton = null;
1379
2335
  const readingDoc = () => open && (view.kind === "howto" || view.kind === "tab" && view.tab === "help" && help.isArticleOpen());
@@ -1454,7 +2410,7 @@ function renderMessenger(options) {
1454
2410
  options.onStartGuide?.(guideKey);
1455
2411
  };
1456
2412
  function howToItems(q) {
1457
- const list = q ? (howTos?.howTos ?? []).filter((h) => (0, import_runtime5.matchesHelpQuery)(`${h.title} ${h.description}`, q)).slice(0, 10) : [];
2413
+ const list = q ? (howTos?.howTos ?? []).filter((h) => (0, import_runtime6.matchesHelpQuery)(`${h.title} ${h.description}`, q)).slice(0, 10) : [];
1458
2414
  if (!list.length) return [];
1459
2415
  return [
1460
2416
  el(doc, "div", "g-rc-section", `How-tos (${list.length})`),
@@ -1468,7 +2424,7 @@ function renderMessenger(options) {
1468
2424
  const refreshConversations = () => api.listConversations().then((list) => {
1469
2425
  conversations = [...list].sort((a, b) => b.lastMessageAt.localeCompare(a.lastMessageAt));
1470
2426
  updateBadge();
1471
- if (open && view.kind === "tab" && view.tab !== "help") render();
2427
+ if (open && view.kind === "tab" && view.tab !== "help" && view.tab !== "ideas") render();
1472
2428
  }).catch(() => {
1473
2429
  });
1474
2430
  function renderHead(title, back) {
@@ -1502,6 +2458,7 @@ function renderMessenger(options) {
1502
2458
  ["messages", "messages", "Messages"],
1503
2459
  ["help", "help", "Help"]
1504
2460
  ];
2461
+ if (ideasOn()) items.push(["ideas", "idea", config.ideas.title || "Ideas"]);
1505
2462
  const latest = news();
1506
2463
  if (latest.length) items.push(["news", "news", "News"]);
1507
2464
  for (const [tab, ico, label] of items) {
@@ -1512,7 +2469,7 @@ function renderMessenger(options) {
1512
2469
  const i = el(doc, "span", "g-m-tab-icon");
1513
2470
  i.appendChild(icon(doc, ico));
1514
2471
  b.append(i, el(doc, "span", "", label));
1515
- if (tab === "news" && latest.some((n) => n.unread)) {
2472
+ if (tab === "news" && latest.some((n) => n.unread) || tab === "ideas" && ideas.unread() > 0) {
1516
2473
  const dot2 = el(doc, "span", "g-m-tab-dot");
1517
2474
  dot2.setAttribute("aria-label", "New");
1518
2475
  i.appendChild(dot2);
@@ -1718,11 +2675,16 @@ function renderMessenger(options) {
1718
2675
  const body = el(doc, "span", "g-m-news-body");
1719
2676
  const meta = el(doc, "span", "g-m-row g-m-center g-m-news-meta");
1720
2677
  if (n.unread) meta.appendChild(el(doc, "span", "g-m-tag", "New"));
1721
- meta.appendChild(el(doc, "span", "g-m-time", shortDate(n.publishedAt)));
2678
+ meta.appendChild(el(doc, "span", "g-m-time", shortDate2(n.publishedAt)));
1722
2679
  body.append(meta, el(doc, "span", "g-m-card-title", n.title));
1723
2680
  if (n.body) body.appendChild(el(doc, "span", "g-m-card-sub g-m-clamp", n.body));
1724
2681
  card.appendChild(body);
1725
- card.addEventListener("click", () => startGuide(n.key || n.id));
2682
+ card.addEventListener("click", () => {
2683
+ if (n.key) startGuide(n.key);
2684
+ else if (n.articleId) openArticle(n.articleId);
2685
+ else if (n.url && /^https?:\/\//i.test(n.url)) window.open(n.url, "_blank", "noopener,noreferrer");
2686
+ else startGuide(n.id);
2687
+ });
1726
2688
  return card;
1727
2689
  }
1728
2690
  function renderHome() {
@@ -1748,11 +2710,13 @@ function renderMessenger(options) {
1748
2710
  if (filed) inner.appendChild(conversationCard(filed, "Your complaint"));
1749
2711
  const recent = conversations.find((c) => c.id !== filed?.id);
1750
2712
  if (recent) inner.appendChild(conversationCard(recent, recent.status === "closed" && !recent.unread ? "Recent conversation" : "Continue the conversation"));
2713
+ const ideaUpdates = ideasOn() ? ideas.unread() : 0;
2714
+ if (ideaUpdates) inner.appendChild(actionCard({ title: "Updates on your ideas", sub: ideaUpdates === 1 ? "1 new update" : `${ideaUpdates} new updates`, lead: iconTile("idea", true), trail: true, onClick: () => go({ kind: "idea-updates", from: view }) }));
1751
2715
  if (config.status) inner.appendChild(statusCard(config.status));
1752
2716
  if (cards.progress) progressCards(v).forEach((c) => inner.appendChild(c));
1753
2717
  const suggestions = pageSuggestions(v);
1754
2718
  const questions = cards.suggestions ? (page?.questions ?? []).slice(0, 2) : [];
1755
- const label = v.path ? (0, import_runtime5.pageLabel)(v.path) : null;
2719
+ const label = v.path ? (0, import_runtime6.pageLabel)(v.path) : null;
1756
2720
  const fallback = !suggestions.length && !questions.length ? rcExtras(options.resourceCenter?.recommendedGuideIds).slice(0, 3) : [];
1757
2721
  if (suggestions.length || questions.length || fallback.length) {
1758
2722
  const box = el(doc, "div", "g-m-card g-m-static g-m-suggest");
@@ -1767,6 +2731,10 @@ function renderMessenger(options) {
1767
2731
  }
1768
2732
  const latest = news()[0];
1769
2733
  if (latest) inner.appendChild(newsCard(latest));
2734
+ if (ideasOn()) {
2735
+ const canWrite = !ideas.blocked();
2736
+ inner.appendChild(actionCard({ title: canWrite ? "Share an idea" : "Browse ideas", sub: canWrite ? "Suggest a feature or vote for what's next." : "See what people are asking for and what's planned.", lead: iconTile("idea"), onClick: () => go(ideasTab) }));
2737
+ }
1770
2738
  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" }) }));
1771
2739
  scroll.append(hero, inner);
1772
2740
  main.appendChild(scroll);
@@ -1867,6 +2835,25 @@ function renderMessenger(options) {
1867
2835
  scroll.appendChild(complaintSent(doc, { complaint: v.complaint, answers: v.answers, onOpenConversation: () => go({ kind: "thread", conversationId: v.conversationId }), onHome: () => go({ kind: "tab", tab: "home" }) }));
1868
2836
  main.appendChild(scroll);
1869
2837
  }
2838
+ function renderIdeasView(v) {
2839
+ const title = config.ideas?.title || "Ideas";
2840
+ if (v.kind === "idea") {
2841
+ renderHead(title, () => go(v.from));
2842
+ main.appendChild(ideas.detailScreen(v.id));
2843
+ } else if (v.kind === "idea-new") {
2844
+ renderHead("Suggest an idea", () => go(v.from));
2845
+ main.appendChild(ideas.formScreen());
2846
+ } else if (v.kind === "idea-sent") {
2847
+ renderHead(title, () => go(ideasTab));
2848
+ main.appendChild(ideas.sentScreen(v.idea));
2849
+ } else if (v.kind === "idea-updates") {
2850
+ renderHead("Updates on your ideas", () => go(v.from));
2851
+ main.appendChild(ideas.updatesScreen());
2852
+ } else {
2853
+ renderHead(title);
2854
+ main.appendChild(ideas.listScreen());
2855
+ }
2856
+ }
1870
2857
  function renderHowToView(id, from) {
1871
2858
  const cached = howToCache.get(id);
1872
2859
  renderHead(cached?.title ?? "How-to", () => go(from));
@@ -1902,7 +2889,7 @@ function renderMessenger(options) {
1902
2889
  const v = visitor();
1903
2890
  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 }));
1904
2891
  if (!items.length) return void 0;
1905
- const label = v.path ? (0, import_runtime5.pageLabel)(v.path) : null;
2892
+ const label = v.path ? (0, import_runtime6.pageLabel)(v.path) : null;
1906
2893
  return { title: label ? `For the ${label} page` : "Suggested for you", items };
1907
2894
  }
1908
2895
  function renderHelpTab() {
@@ -1929,7 +2916,7 @@ function renderMessenger(options) {
1929
2916
  pageKey = null;
1930
2917
  return;
1931
2918
  }
1932
- const key = (0, import_runtime5.foldPathIds)(path).toLowerCase();
2919
+ const key = (0, import_runtime6.foldPathIds)(path).toLowerCase();
1933
2920
  if (!force && key === pageKey && (pageLoading || Date.now() - pageAt < PAGE_TTL_MS)) return;
1934
2921
  if (key !== pageKey) page = null;
1935
2922
  pageKey = key;
@@ -1980,7 +2967,8 @@ function renderMessenger(options) {
1980
2967
  bubble.dataset.messageId = m.id;
1981
2968
  wrap.appendChild(bubble);
1982
2969
  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) {
2970
+ const ideaChip = m.ideaOffer && ideasOn() ? ideas.offerChip(m.ideaOffer, current?.id ?? null, () => icon(doc, "idea", "g-m-ico g-m-chip-ico")) : null;
2971
+ if (m.citations.length || m.suggestedGuides.length || m.howTos?.length || offer || ideaChip) {
1984
2972
  const chips = el(doc, "div", "g-m-chips");
1985
2973
  if (offer) {
1986
2974
  const b = el(doc, "button", "g-m-chip g-m-guide g-m-offer");
@@ -1993,6 +2981,7 @@ function renderMessenger(options) {
1993
2981
  });
1994
2982
  chips.appendChild(b);
1995
2983
  }
2984
+ if (ideaChip) chips.appendChild(ideaChip);
1996
2985
  for (const g of m.suggestedGuides) {
1997
2986
  const b = el(doc, "button", "g-m-chip g-m-guide", `\u25B6 Show me: ${g.name}`);
1998
2987
  b.type = "button";
@@ -2025,7 +3014,7 @@ function renderMessenger(options) {
2025
3014
  if (current?.complaint) log.appendChild(complaintBanner(current.complaint));
2026
3015
  if (!messages.length) {
2027
3016
  const v = visitor();
2028
- const label = v.path ? (0, import_runtime5.pageLabel)(v.path) : null;
3017
+ const label = v.path ? (0, import_runtime6.pageLabel)(v.path) : null;
2029
3018
  if (label && config.aiAvailable && !current) log.appendChild(el(doc, "div", "g-m-context", `Asking from the ${label} page`));
2030
3019
  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() };
2031
3020
  log.appendChild(messageNode(intro));
@@ -2145,7 +3134,13 @@ function renderMessenger(options) {
2145
3134
  else if (view.kind === "complaint-category") renderComplaintCategories(view);
2146
3135
  else if (view.kind === "complaint-form") renderComplaintForm(view);
2147
3136
  else if (view.kind === "complaint-sent") renderComplaintSent(view);
2148
- else if (view.tab === "home") renderHome();
3137
+ else if (view.kind !== "tab" || view.tab === "ideas") {
3138
+ if (ideasOn()) renderIdeasView(view);
3139
+ else {
3140
+ view = { kind: "tab", tab: "home" };
3141
+ renderHome();
3142
+ }
3143
+ } else if (view.tab === "home") renderHome();
2149
3144
  else if (view.tab === "messages") renderMessagesTab();
2150
3145
  else if (view.tab === "news" && news().length) renderNewsTab();
2151
3146
  else if (view.tab === "news") {
@@ -2296,6 +3291,7 @@ function renderMessenger(options) {
2296
3291
  render();
2297
3292
  void refreshConversations();
2298
3293
  loadHowTos();
3294
+ if (ideasOn()) ideas.loadUpdates();
2299
3295
  if (view.kind === "thread") scheduleThreadPoll();
2300
3296
  options.onOpen?.();
2301
3297
  } else {
@@ -2344,15 +3340,22 @@ function renderMessenger(options) {
2344
3340
  setOpen(true);
2345
3341
  startComplaint(categoryId ?? null);
2346
3342
  },
3343
+ openIdeas(requestId) {
3344
+ if (!ideasOn()) return;
3345
+ setOpen(true);
3346
+ go(requestId ? { kind: "idea", id: requestId, from: ideasTab } : ideasTab);
3347
+ },
2347
3348
  refresh() {
2348
3349
  if (destroyed) return;
3350
+ if (ideasOn()) ideas.sync(open);
2349
3351
  if (!open) {
2350
3352
  pageKey = null;
2351
3353
  return;
2352
3354
  }
2353
3355
  loadPage();
2354
- if (view.kind === "tab") render();
2355
- else if (!messages.length) renderLog();
3356
+ if (view.kind === "tab" && view.tab !== "ideas") render();
3357
+ else if (view.kind === "tab") renderTabs();
3358
+ else if (view.kind === "thread" && !messages.length) renderLog();
2356
3359
  },
2357
3360
  destroy() {
2358
3361
  destroyed = true;
@@ -2360,6 +3363,7 @@ function renderMessenger(options) {
2360
3363
  if (backgroundTimer) clearInterval(backgroundTimer);
2361
3364
  panel.removeEventListener("keydown", onKey);
2362
3365
  help.destroy();
3366
+ ideas.destroy();
2363
3367
  shadowHost?.removeAttribute("data-g-messenger");
2364
3368
  root.remove();
2365
3369
  }