@guidelytech/guide-runtime 0.10.0 → 0.12.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.
@@ -20,20 +20,22 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
20
20
  // src/messenger.ts
21
21
  var messenger_exports = {};
22
22
  __export(messenger_exports, {
23
+ parseLauncherSpot: () => parseLauncherSpot,
23
24
  personalGreeting: () => personalGreeting,
24
25
  relativeTime: () => relativeTime,
25
26
  renderMessenger: () => renderMessenger
26
27
  });
27
28
  module.exports = __toCommonJS(messenger_exports);
28
- var import_runtime5 = require("@guidelytech/shared/runtime");
29
+ var import_runtime6 = require("@guidelytech/shared/runtime");
29
30
 
30
31
  // src/help-view.ts
31
- var import_runtime3 = require("@guidelytech/shared/runtime");
32
+ var import_runtime4 = require("@guidelytech/shared/runtime");
32
33
 
33
34
  // src/pdf-viewer.ts
34
- var import_runtime = require("@guidelytech/shared/runtime");
35
+ var import_runtime2 = require("@guidelytech/shared/runtime");
35
36
 
36
37
  // src/host.ts
38
+ var import_runtime = require("@guidelytech/shared/runtime");
37
39
  function prefersReducedMotion() {
38
40
  try {
39
41
  return typeof window !== "undefined" && typeof window.matchMedia === "function" && window.matchMedia("(prefers-reduced-motion: reduce)").matches;
@@ -44,9 +46,9 @@ function prefersReducedMotion() {
44
46
 
45
47
  // src/pdf-viewer.ts
46
48
  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}`);
49
+ for (const block of Array.from(root.querySelectorAll(`div.${import_runtime2.PDF_BLOCK_CLASS}`))) {
50
+ const link2 = block.querySelector(`a.${import_runtime2.FILE_LINK_CLASS}`);
51
+ const strip = block.querySelector(`.${import_runtime2.PDF_PAGES_CLASS}`);
50
52
  if (!link2 || !strip || block.hasAttribute("data-g-ready")) continue;
51
53
  for (const img of Array.from(strip.querySelectorAll("img:not([src])"))) img.remove();
52
54
  const pages = Array.from(strip.querySelectorAll("img"));
@@ -172,7 +174,7 @@ function openReader(doc, block, link2, pages, start) {
172
174
  }
173
175
 
174
176
  // src/sanitize.ts
175
- var import_runtime2 = require("@guidelytech/shared/runtime");
177
+ var import_runtime3 = require("@guidelytech/shared/runtime");
176
178
  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
179
  var DROP_TAGS = /* @__PURE__ */ new Set(["script", "style", "object", "embed", "form", "input", "textarea", "select", "link", "meta", "base", "template", "svg", "math", "frame", "frameset", "noscript"]);
178
180
  var ALLOWED_ATTRS = /* @__PURE__ */ new Set(["href", "src", "alt", "title", "target", "rel", "class", "width", "height", "data-g-action", "data-g-value"]);
@@ -213,18 +215,18 @@ function rebuildMedia(el2) {
213
215
  const src = el2.getAttribute("src");
214
216
  let out;
215
217
  if (tag === "iframe") {
216
- if (!(0, import_runtime2.isAllowedEmbedSrc)(src)) return null;
218
+ if (!(0, import_runtime3.isAllowedEmbedSrc)(src)) return null;
217
219
  out = doc.createElement("iframe");
218
220
  out.setAttribute("src", src);
219
221
  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);
222
+ for (const [k, v] of Object.entries(import_runtime3.EMBED_IFRAME_ATTRS)) out.setAttribute(k, v);
221
223
  } else {
222
- if (!(0, import_runtime2.isSafeMediaUrl)(src)) return null;
224
+ if (!(0, import_runtime3.isSafeMediaUrl)(src)) return null;
223
225
  out = doc.createElement("video");
224
226
  out.setAttribute("src", src);
225
227
  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);
228
+ if ((0, import_runtime3.isSafeMediaUrl)(poster)) out.setAttribute("poster", poster);
229
+ for (const [k, v] of Object.entries(import_runtime3.VIDEO_ATTRS)) out.setAttribute(k, v);
228
230
  }
229
231
  return out;
230
232
  }
@@ -288,8 +290,8 @@ function resultRow(doc, icon2, title, summary, onOpen, showMe) {
288
290
  return row;
289
291
  }
290
292
  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
+ for (const a of Array.from(root.querySelectorAll(`a[href^="${import_runtime4.SHOW_ME_HREF_PREFIX}"]`))) {
294
+ const key = (0, import_runtime4.showMeKeyFromHref)(a.getAttribute("href"));
293
295
  const holder = a.closest("p.gd-show-me") ?? a;
294
296
  if (!key || !canStart(key)) {
295
297
  if (holder !== a) holder.remove();
@@ -404,10 +406,11 @@ function createHelpView(options) {
404
406
  if (active) return renderArticle(active);
405
407
  const data = options.data();
406
408
  const member = options.audienceIds?.() ?? [];
407
- const sections = (0, import_runtime3.resourceCenterSections)(data.collections, data.articles, member);
409
+ searchWrap.hidden = !!data.modules && !data.modules.includes("search");
410
+ const sections = (0, import_runtime4.resourceCenterSections)(data.collections, data.articles, member);
408
411
  const q = query.trim().toLowerCase();
409
412
  if (q) {
410
- const walkthroughs = (data.recommended ?? []).filter((g) => (0, import_runtime3.matchesHelpQuery)(g.name, q) && startableGuide(options.guides?.() ?? data.recommended, g.id));
413
+ const walkthroughs = (data.recommended ?? []).filter((g) => (0, import_runtime4.matchesHelpQuery)(g.name, q) && startableGuide(options.guides?.() ?? data.recommended, g.id));
411
414
  if (walkthroughs.length) {
412
415
  section(`Walkthroughs (${walkthroughs.length})`);
413
416
  for (const g of walkthroughs) body.appendChild(resultRow(doc, "\u25B6", g.name, "Show me, step by step", () => options.onStartGuide?.(g.id)));
@@ -416,10 +419,10 @@ function createHelpView(options) {
416
419
  body.append(...options.extra?.(q) ?? []);
417
420
  if (q) {
418
421
  const visible = sections.flatMap((s) => s.articles);
419
- const local = visible.filter((a) => (0, import_runtime3.matchesHelpQuery)(`${a.title} ${a.summary}`, q));
422
+ const local = visible.filter((a) => (0, import_runtime4.matchesHelpQuery)(`${a.title} ${a.summary}`, q));
420
423
  const seen = new Set(local.map((a) => a.id));
421
424
  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))];
425
+ const merged = [...local, ...serverResults.filter((a) => !seen.has(a.id) && !hidden.has(a.id) && (0, import_runtime4.isArticleListed)(data.collections, a, member))];
423
426
  section(`Results (${merged.length})`);
424
427
  if (!merged.length) body.appendChild(el(doc, "div", "g-rc-empty", "No articles match your search."));
425
428
  merged.forEach(articleButton);
@@ -434,55 +437,124 @@ function createHelpView(options) {
434
437
  body.appendChild(b);
435
438
  });
436
439
  }
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
- });
440
+ const renderers = {
441
+ search: () => {
442
+ },
443
+ announcements: () => {
444
+ if (!data.announcements?.length) return;
445
+ section(import_runtime4.RC_MODULE_LABELS.announcements);
446
+ const seen = data.announcementsSeenAt ? Date.parse(data.announcementsSeenAt) : NaN;
447
+ data.announcements.slice(0, 20).forEach((n) => body.appendChild(announcementItem(n, Number.isNaN(seen) || Date.parse(n.publishedAt) > seen)));
448
+ },
449
+ guides: () => {
450
+ if (!data.recommended?.length) return;
451
+ section("Recommended");
452
+ data.recommended.forEach((g) => {
453
+ const b = listItem(doc, "\u25B6", g.name, "Start guide");
454
+ b.addEventListener("click", () => options.onStartGuide?.(g.id));
455
+ body.appendChild(b);
456
+ });
457
+ },
458
+ checklists: () => {
459
+ if (!data.checklists?.length) return;
460
+ section("Checklists");
461
+ data.checklists.forEach((g) => {
462
+ const b = listItem(doc, "\u2611", g.name, "Open checklist");
463
+ b.addEventListener("click", () => options.onOpenChecklist?.(g.id));
464
+ body.appendChild(b);
465
+ });
466
+ },
467
+ articles: () => {
468
+ for (const s of sections) {
469
+ section(s.collection ? s.path.join(" \u203A ") : "Articles", s.depth);
470
+ s.articles.forEach(articleButton);
471
+ }
472
+ },
473
+ faqs: () => {
474
+ if (!data.faqs?.length) return;
475
+ section("FAQ");
476
+ data.faqs.forEach((f) => {
477
+ const b = listItem(doc, "?", f.question);
478
+ b.setAttribute("aria-expanded", "false");
479
+ const a = el(doc, "div", "g-faq-a");
480
+ a.id = `g-faq-${f.id}`;
481
+ b.setAttribute("aria-controls", a.id);
482
+ setSanitizedHtml(a, f.answer);
483
+ a.hidden = true;
484
+ b.addEventListener("click", () => {
485
+ a.hidden = !a.hidden;
486
+ b.setAttribute("aria-expanded", String(!a.hidden));
487
+ });
488
+ body.append(b, a);
489
+ });
490
+ },
491
+ links: () => {
492
+ if (!data.links?.length) return;
493
+ section("Links");
494
+ data.links.forEach((l) => {
495
+ const a = el(doc, "a", "g-rc-item");
496
+ a.href = l.url;
497
+ a.target = "_blank";
498
+ a.rel = "noopener noreferrer";
499
+ const i = el(doc, "span", "g-rc-icon", "\u2197");
500
+ i.setAttribute("aria-hidden", "true");
501
+ a.append(i, el(doc, "span", "g-rc-title", l.label));
502
+ a.setAttribute("aria-label", `${l.label} (opens in a new tab)`);
503
+ body.appendChild(a);
504
+ });
505
+ }
506
+ };
507
+ for (const id of data.modules ?? import_runtime4.RC_MODULE_IDS) renderers[id]?.();
508
+ if (!body.childNodes.length) body.appendChild(el(doc, "div", "g-rc-empty", "Nothing here yet."));
509
+ }
510
+ function announcementItem(n, unread) {
511
+ const item = el(doc, "article", `g-rc-news${unread ? " g-rc-news-unread" : ""}`);
512
+ const head = el(doc, "div", "g-rc-news-head");
513
+ const title = el(doc, "h3", "g-rc-news-title", n.title);
514
+ head.appendChild(title);
515
+ if (unread) {
516
+ const dot2 = el(doc, "span", "g-rc-news-new", "New");
517
+ head.appendChild(dot2);
444
518
  }
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
- });
519
+ const date = el(doc, "time", "g-rc-news-date");
520
+ date.dateTime = n.publishedAt;
521
+ try {
522
+ date.textContent = new Date(n.publishedAt).toLocaleDateString(void 0, { year: "numeric", month: "short", day: "numeric" });
523
+ } catch {
524
+ date.textContent = n.publishedAt.slice(0, 10);
452
525
  }
453
- for (const s of sections) {
454
- section(s.collection ? s.path.join(" \u203A ") : "Articles", s.depth);
455
- s.articles.forEach(articleButton);
456
- }
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
- });
526
+ item.append(head, date);
527
+ if (n.body) {
528
+ const b = el(doc, "div", "g-body g-rc-news-body");
529
+ setSanitizedHtml(b, n.body);
530
+ item.appendChild(b);
471
531
  }
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
- });
532
+ const actions = el(doc, "div", "g-rc-news-actions");
533
+ const guide = n.guideId ? startableGuide(options.guides?.(), n.guideId) : null;
534
+ if (guide) {
535
+ const b = el(doc, "button", "g-btn g-btn-primary", n.linkLabel || "Show me");
536
+ b.type = "button";
537
+ b.addEventListener("click", () => options.onShowMe ? options.onShowMe(guide.key) : options.onStartGuide?.(guide.id));
538
+ actions.appendChild(b);
484
539
  }
485
- if (!body.childNodes.length) body.appendChild(el(doc, "div", "g-rc-empty", "Nothing here yet."));
540
+ if (n.articleId) {
541
+ const listed = options.data().articles.find((a) => a.id === n.articleId);
542
+ if (listed) {
543
+ const b = el(doc, "button", "g-btn g-btn-secondary", guide ? "Read more" : n.linkLabel || "Read more");
544
+ b.type = "button";
545
+ b.addEventListener("click", () => openArticle(listed));
546
+ actions.appendChild(b);
547
+ }
548
+ }
549
+ if (n.url && /^https?:\/\//i.test(n.url)) {
550
+ const a = el(doc, "a", "g-btn g-btn-link", !guide && !n.articleId && n.linkLabel ? n.linkLabel : "Learn more");
551
+ a.href = n.url;
552
+ a.target = "_blank";
553
+ a.rel = "noopener noreferrer";
554
+ actions.appendChild(a);
555
+ }
556
+ if (actions.childNodes.length) item.appendChild(actions);
557
+ return item;
486
558
  }
487
559
  let debounce = null;
488
560
  const onInput = () => {
@@ -636,7 +708,7 @@ function renderHowTo(doc, howTo, onShowMe) {
636
708
  }
637
709
 
638
710
  // src/complaint-view.ts
639
- var import_runtime4 = require("@guidelytech/shared/runtime");
711
+ var import_runtime5 = require("@guidelytech/shared/runtime");
640
712
  var emptyComplaintDraft = (description = "") => ({ description, answers: {}, email: "", attachment: null, error: null, sending: false, checked: false });
641
713
  var COMPLAINT_STEPS = [
642
714
  ["open", "Received"],
@@ -655,7 +727,7 @@ function complaintAnswers(category, draft) {
655
727
  const out = {};
656
728
  for (const f of category.fields) {
657
729
  const v = (draft.answers[f.id] ?? "").trim();
658
- if (v) out[f.id] = v.slice(0, import_runtime4.MAX_COMPLAINT_ANSWER_LENGTH);
730
+ if (v) out[f.id] = v.slice(0, import_runtime5.MAX_COMPLAINT_ANSWER_LENGTH);
659
731
  }
660
732
  return out;
661
733
  }
@@ -740,7 +812,7 @@ function complaintForm(doc, o) {
740
812
  } else {
741
813
  const input = el(doc, "input", "g-input");
742
814
  input.type = "text";
743
- input.maxLength = import_runtime4.MAX_COMPLAINT_ANSWER_LENGTH;
815
+ input.maxLength = import_runtime5.MAX_COMPLAINT_ANSWER_LENGTH;
744
816
  input.value = draft.answers[f.id] ?? "";
745
817
  input.addEventListener("input", () => draft.answers[f.id] = input.value);
746
818
  control = input;
@@ -750,7 +822,7 @@ function complaintForm(doc, o) {
750
822
  }
751
823
  const what = el(doc, "textarea", "g-input g-m-textarea");
752
824
  what.rows = 5;
753
- what.maxLength = import_runtime4.MAX_COMPLAINT_LENGTH;
825
+ what.maxLength = import_runtime5.MAX_COMPLAINT_LENGTH;
754
826
  what.required = true;
755
827
  what.value = draft.description;
756
828
  what.placeholder = "What went wrong, and when?";
@@ -891,19 +963,988 @@ async function readScreenshot(doc, file) {
891
963
  ctx.drawImage(img, 0, 0, canvas.width, canvas.height);
892
964
  for (const q of [0.85, 0.7, 0.5]) {
893
965
  dataUrl = canvas.toDataURL("image/jpeg", q);
894
- if ((dataUrl.length - dataUrl.indexOf(",") - 1) * 0.75 <= import_runtime4.MAX_COMPLAINT_ATTACHMENT_BYTES) break;
966
+ if ((dataUrl.length - dataUrl.indexOf(",") - 1) * 0.75 <= import_runtime5.MAX_COMPLAINT_ATTACHMENT_BYTES) break;
895
967
  }
896
968
  scaled = true;
897
969
  } catch {
898
- dataUrl = file.size <= import_runtime4.MAX_COMPLAINT_ATTACHMENT_BYTES ? await asDataUrl(file) : null;
970
+ dataUrl = file.size <= import_runtime5.MAX_COMPLAINT_ATTACHMENT_BYTES ? await asDataUrl(file) : null;
899
971
  }
900
972
  if (!dataUrl) throw new Error("That image is too large. Screenshots can be up to 2 MB.");
901
973
  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.");
974
+ 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
975
  const name = (file.name || "screenshot").slice(0, 180);
904
976
  return { fileName: scaled ? `${name.replace(/\.[a-z0-9]+$/i, "")}.jpg` : name, data, previewUrl: dataUrl };
905
977
  }
906
978
 
979
+ // src/ideas-view.ts
980
+ var import_shared = require("@guidelytech/shared");
981
+
982
+ // src/render-safe.ts
983
+ var CITATION = /\s*\[\[article:[^\]]*\]\]/g;
984
+ var INLINE = /\*\*([^*\n]+)\*\*|`([^`\n]+)`|(!?)\[([^\]\n]{1,200})\]\((https?:\/\/[^\s)]+)\)|(https?:\/\/[^\s<>()"']+[^\s<>().,;:!?'"])/g;
985
+ function stripCitationMarkers(text) {
986
+ return text.replace(CITATION, "");
987
+ }
988
+ function safeHttpUrl(raw) {
989
+ try {
990
+ const u = new URL(raw);
991
+ return u.protocol === "http:" || u.protocol === "https:" ? u.href : null;
992
+ } catch {
993
+ return null;
994
+ }
995
+ }
996
+ function link(doc, label, href) {
997
+ const url = safeHttpUrl(href);
998
+ if (!url) return doc.createTextNode(label);
999
+ const a = doc.createElement("a");
1000
+ a.href = url;
1001
+ a.target = "_blank";
1002
+ a.rel = "noopener noreferrer nofollow";
1003
+ a.textContent = label;
1004
+ return a;
1005
+ }
1006
+ function appendInline(doc, parent, text) {
1007
+ let last = 0;
1008
+ for (const m of text.matchAll(INLINE)) {
1009
+ const start = m.index ?? 0;
1010
+ if (start > last) parent.appendChild(doc.createTextNode(text.slice(last, start)));
1011
+ if (m[1] !== void 0) {
1012
+ const b = doc.createElement("strong");
1013
+ b.textContent = m[1];
1014
+ parent.appendChild(b);
1015
+ } else if (m[2] !== void 0) {
1016
+ const c = doc.createElement("code");
1017
+ c.textContent = m[2];
1018
+ parent.appendChild(c);
1019
+ } else if (m[4] !== void 0 && m[5] !== void 0) {
1020
+ parent.appendChild(m[3] ? doc.createTextNode(m[4]) : link(doc, m[4], m[5]));
1021
+ } else if (m[6] !== void 0) {
1022
+ parent.appendChild(link(doc, m[6], m[6]));
1023
+ }
1024
+ last = start + m[0].length;
1025
+ }
1026
+ if (last < text.length) parent.appendChild(doc.createTextNode(text.slice(last)));
1027
+ }
1028
+ var BULLET = /^\s*[-*•]\s+/;
1029
+ var NUMBERED = /^\s*\d{1,3}[.)]\s+/;
1030
+ function renderSafeText(doc, text) {
1031
+ const frag = doc.createDocumentFragment();
1032
+ const blocks = stripCitationMarkers(text).replace(/\r\n?/g, "\n").trim().split(/\n{2,}/);
1033
+ for (const block of blocks) {
1034
+ if (!block.trim()) continue;
1035
+ const lines = block.split("\n");
1036
+ if (lines.every((l) => BULLET.test(l)) || lines.every((l) => NUMBERED.test(l))) {
1037
+ const ordered = NUMBERED.test(lines[0]);
1038
+ const list = doc.createElement(ordered ? "ol" : "ul");
1039
+ for (const line of lines) {
1040
+ const li = doc.createElement("li");
1041
+ appendInline(doc, li, line.replace(ordered ? NUMBERED : BULLET, ""));
1042
+ list.appendChild(li);
1043
+ }
1044
+ frag.appendChild(list);
1045
+ continue;
1046
+ }
1047
+ const p = doc.createElement("p");
1048
+ lines.forEach((line, i) => {
1049
+ if (i) p.appendChild(doc.createElement("br"));
1050
+ appendInline(doc, p, line);
1051
+ });
1052
+ frag.appendChild(p);
1053
+ }
1054
+ return frag;
1055
+ }
1056
+
1057
+ // src/ideas-view.ts
1058
+ var SVG_NS = "http://www.w3.org/2000/svg";
1059
+ 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"];
1060
+ var UP_ICON = ["m6 15 6-6 6 6"];
1061
+ function svgIcon(doc, paths, cls = "g-m-ico") {
1062
+ const svg = doc.createElementNS(SVG_NS, "svg");
1063
+ svg.setAttribute("viewBox", "0 0 24 24");
1064
+ svg.setAttribute("aria-hidden", "true");
1065
+ svg.setAttribute("class", cls);
1066
+ for (const d of paths) {
1067
+ const p = doc.createElementNS(SVG_NS, "path");
1068
+ p.setAttribute("d", d);
1069
+ svg.appendChild(p);
1070
+ }
1071
+ return svg;
1072
+ }
1073
+ var plural = (n, one, many = `${one}s`) => `${n} ${n === 1 ? one : many}`;
1074
+ function shortDate(iso) {
1075
+ const d = new Date(iso);
1076
+ if (Number.isNaN(d.getTime())) return "";
1077
+ try {
1078
+ return d.toLocaleDateString(void 0, { month: "short", day: "numeric" });
1079
+ } catch {
1080
+ return d.toISOString().slice(0, 10);
1081
+ }
1082
+ }
1083
+ var errorText = (e, fallback) => (e instanceof Error && e.message ? e.message : fallback).replace(/^[\w.]+: /, "");
1084
+ function ideaStatusPill(doc, status) {
1085
+ return el(doc, "span", `g-m-pill g-m-idea-pill g-m-idea-${status}`, import_shared.FEEDBACK_STATUS_LABEL[status] ?? status);
1086
+ }
1087
+ function voteControl(doc, idea, o) {
1088
+ const count = el(doc, "span", "g-m-vote-count", String(idea.votes));
1089
+ if (!o.canVote) {
1090
+ const box = el(doc, "span", "g-m-vote g-m-vote-static");
1091
+ box.setAttribute("aria-label", plural(idea.votes, "vote"));
1092
+ box.append(svgIcon(doc, UP_ICON, "g-m-ico g-m-vote-ico"), count);
1093
+ return box;
1094
+ }
1095
+ const b = el(doc, "button", `g-m-vote${idea.myVote ? " g-m-voted" : ""}`);
1096
+ b.type = "button";
1097
+ b.setAttribute("aria-pressed", String(!!idea.myVote));
1098
+ b.setAttribute("aria-label", `${idea.myVote ? "Remove your vote for" : "Vote for"} ${idea.title} (${plural(idea.votes, "vote")})`);
1099
+ b.disabled = !!o.busy;
1100
+ b.append(svgIcon(doc, UP_ICON, "g-m-ico g-m-vote-ico"), count);
1101
+ b.addEventListener("click", (e) => {
1102
+ e.stopPropagation();
1103
+ o.onToggle?.();
1104
+ });
1105
+ return b;
1106
+ }
1107
+ function ideaRow(doc, idea, o) {
1108
+ const row = el(doc, "div", "g-m-idea");
1109
+ row.setAttribute("role", "listitem");
1110
+ row.appendChild(voteControl(doc, idea, { canVote: o.canVote, busy: o.busy, onToggle: o.onVote }));
1111
+ const open = el(doc, "button", "g-m-idea-open");
1112
+ open.type = "button";
1113
+ open.appendChild(el(doc, "span", "g-m-idea-title", idea.title));
1114
+ const meta = el(doc, "span", "g-m-idea-meta");
1115
+ meta.appendChild(ideaStatusPill(doc, idea.status));
1116
+ if (idea.comments) meta.appendChild(el(doc, "span", "g-m-time", plural(idea.comments, "comment")));
1117
+ if (idea.productArea) meta.appendChild(el(doc, "span", "g-m-time", idea.productArea));
1118
+ if (idea.mine) meta.appendChild(el(doc, "span", "g-m-time", "Yours"));
1119
+ open.appendChild(meta);
1120
+ open.addEventListener("click", o.onOpen);
1121
+ row.appendChild(open);
1122
+ return row;
1123
+ }
1124
+ var FILTERS = [
1125
+ ["open", "Open"],
1126
+ ["all", "All"],
1127
+ ["mine", "Mine"],
1128
+ ["following", "Following"]
1129
+ ];
1130
+ var SORTS = [
1131
+ ["top", "Top"],
1132
+ ["new", "New"],
1133
+ ["trending", "Trending"]
1134
+ ];
1135
+ function filterChips(doc, current, onPick) {
1136
+ const wrap = el(doc, "div", "g-m-filters");
1137
+ wrap.setAttribute("role", "group");
1138
+ wrap.setAttribute("aria-label", "Show");
1139
+ for (const [f, label] of FILTERS) {
1140
+ const b = el(doc, "button", "g-m-filter", label);
1141
+ b.type = "button";
1142
+ b.dataset.filter = f;
1143
+ b.setAttribute("aria-pressed", String(f === current));
1144
+ b.addEventListener("click", () => {
1145
+ wrap.querySelectorAll("button").forEach((x) => x.setAttribute("aria-pressed", String(x === b)));
1146
+ onPick(f);
1147
+ });
1148
+ wrap.appendChild(b);
1149
+ }
1150
+ return wrap;
1151
+ }
1152
+ function sortSelect(doc, current, onChange) {
1153
+ const select = el(doc, "select", "g-input g-m-sort");
1154
+ select.setAttribute("aria-label", "Sort ideas");
1155
+ for (const [s, label] of SORTS) {
1156
+ const opt = el(doc, "option", "", label);
1157
+ opt.value = s;
1158
+ select.appendChild(opt);
1159
+ }
1160
+ select.value = current;
1161
+ select.addEventListener("change", () => onChange(select.value));
1162
+ return select;
1163
+ }
1164
+ function priorityChoice(doc, current, onPick, label = "How important is this to you?") {
1165
+ const wrap = el(doc, "div", "g-m-field");
1166
+ const id = `g-m-prio${++fieldSeq2}`;
1167
+ const l = el(doc, "span", "g-m-label", label);
1168
+ l.id = id;
1169
+ const group = el(doc, "div", "g-m-seg");
1170
+ group.setAttribute("role", "radiogroup");
1171
+ group.setAttribute("aria-labelledby", id);
1172
+ for (const p of import_shared.FEEDBACK_PRIORITIES) {
1173
+ const b = el(doc, "button", "g-m-seg-btn", import_shared.FEEDBACK_PRIORITY_LABEL[p]);
1174
+ b.type = "button";
1175
+ b.dataset.priority = p;
1176
+ b.setAttribute("role", "radio");
1177
+ b.setAttribute("aria-checked", String(p === current));
1178
+ b.addEventListener("click", () => {
1179
+ group.querySelectorAll("button").forEach((x) => x.setAttribute("aria-checked", String(x === b)));
1180
+ onPick(p);
1181
+ });
1182
+ group.appendChild(b);
1183
+ }
1184
+ wrap.append(l, group);
1185
+ return wrap;
1186
+ }
1187
+ var fieldSeq2 = 0;
1188
+ function field2(doc, label, control, opts = {}) {
1189
+ const id = `g-m-i${++fieldSeq2}`;
1190
+ control.id = id;
1191
+ const wrap = el(doc, "div", "g-m-field");
1192
+ const l = el(doc, "label", "g-m-label", label);
1193
+ l.htmlFor = id;
1194
+ if (opts.optional) l.appendChild(el(doc, "span", "g-m-optional", " (optional)"));
1195
+ if (opts.invalid) control.setAttribute("aria-invalid", "true");
1196
+ wrap.append(l, control);
1197
+ return wrap;
1198
+ }
1199
+ function errorLine(doc, text) {
1200
+ const p = el(doc, "p", "g-m-error", text);
1201
+ p.setAttribute("role", "alert");
1202
+ return p;
1203
+ }
1204
+ function button(doc, cls, text, onClick) {
1205
+ const b = el(doc, "button", cls, text);
1206
+ b.type = "button";
1207
+ b.addEventListener("click", onClick);
1208
+ return b;
1209
+ }
1210
+ function writeNotice(doc, reason, onPortal) {
1211
+ const box = el(doc, "div", "g-m-idea-notice");
1212
+ box.setAttribute("role", "note");
1213
+ box.appendChild(el(doc, "span", "", reason));
1214
+ if (onPortal) box.appendChild(button(doc, "g-m-text-btn", "Open the feedback portal", onPortal));
1215
+ return box;
1216
+ }
1217
+ function comment(doc, c) {
1218
+ const li = el(doc, "li", `g-m-comment${c.authorType === "team" ? " g-m-comment-team" : ""}`);
1219
+ const who = el(doc, "div", "g-m-row g-m-center g-m-comment-meta");
1220
+ who.appendChild(el(doc, "span", "g-m-comment-author", c.authorName || (c.authorType === "team" ? "Team" : "Someone")));
1221
+ if (c.authorType === "team") who.appendChild(el(doc, "span", "g-m-ai", "Team"));
1222
+ who.appendChild(el(doc, "span", "g-m-time", shortDate(c.createdAt)));
1223
+ const body = el(doc, "div", "g-m-comment-body");
1224
+ body.appendChild(renderSafeText(doc, c.body));
1225
+ li.append(who, body);
1226
+ return li;
1227
+ }
1228
+ function ideaDetail(doc, o) {
1229
+ const { idea } = o;
1230
+ const wrap = el(doc, "div", "g-m-form g-m-idea-detail");
1231
+ const meta = el(doc, "div", "g-m-row g-m-center g-m-idea-meta");
1232
+ meta.append(el(doc, "span", "g-m-time", (0, import_shared.feedbackReference)(idea.number)), ideaStatusPill(doc, idea.status));
1233
+ if (idea.productArea) meta.appendChild(el(doc, "span", "g-m-time", idea.productArea));
1234
+ wrap.append(meta, el(doc, "h3", "g-m-form-title", idea.title));
1235
+ if (idea.description.trim()) {
1236
+ const d = el(doc, "div", "g-m-idea-desc");
1237
+ d.appendChild(renderSafeText(doc, idea.description));
1238
+ wrap.appendChild(d);
1239
+ }
1240
+ if (idea.statusNote.trim()) {
1241
+ const note = el(doc, "div", "g-m-idea-note");
1242
+ note.appendChild(el(doc, "span", "g-m-card-label", `${import_shared.FEEDBACK_STATUS_LABEL[idea.status] ?? idea.status} \xB7 from the team`));
1243
+ note.appendChild(renderSafeText(doc, idea.statusNote));
1244
+ wrap.appendChild(note);
1245
+ }
1246
+ const panel = el(doc, "div", "g-m-card g-m-static g-m-idea-vote");
1247
+ const top = el(doc, "div", "g-m-row g-m-center");
1248
+ top.appendChild(voteControl(doc, idea, { canVote: false }));
1249
+ const said = el(doc, "span", "g-m-grow");
1250
+ said.appendChild(el(doc, "span", "g-m-card-title", plural(idea.votes, "vote")));
1251
+ if (idea.myVote) said.appendChild(el(doc, "span", "g-m-card-sub", `You voted \xB7 ${import_shared.FEEDBACK_PRIORITY_LABEL[idea.myVote.priority]}`));
1252
+ top.appendChild(said);
1253
+ if (!o.blocked) {
1254
+ const follow = button(doc, `g-m-follow${idea.following ? " g-m-following" : ""}`, idea.following ? "Following" : "Follow", () => o.onFollow(!idea.following));
1255
+ follow.setAttribute("aria-pressed", String(idea.following));
1256
+ follow.disabled = o.busy;
1257
+ follow.title = idea.following ? "Stop getting updates about this idea" : "Get updates here when it changes";
1258
+ top.appendChild(follow);
1259
+ }
1260
+ panel.appendChild(top);
1261
+ if (o.error) panel.appendChild(errorLine(doc, o.error));
1262
+ if (o.blocked) panel.appendChild(writeNotice(doc, o.blocked, o.onPortal));
1263
+ else if (idea.myVote) {
1264
+ panel.appendChild(priorityChoice(doc, idea.myVote.priority, (p) => p !== idea.myVote?.priority && o.onVote(p, ""), "How much do you need it?"));
1265
+ const remove = button(doc, "g-m-text-btn g-m-unvote", "Remove my vote", o.onUnvote);
1266
+ remove.disabled = o.busy;
1267
+ panel.appendChild(remove);
1268
+ } else {
1269
+ const vote = o.vote;
1270
+ panel.appendChild(priorityChoice(doc, vote.priority, (p) => vote.priority = p));
1271
+ const note = el(doc, "textarea", "g-input g-m-idea-why");
1272
+ note.rows = 2;
1273
+ note.maxLength = import_shared.MAX_FEEDBACK_NOTE;
1274
+ note.value = vote.note;
1275
+ note.placeholder = "What would it let you do?";
1276
+ note.addEventListener("input", () => vote.note = note.value);
1277
+ panel.appendChild(field2(doc, "Why it matters", note, { optional: true }));
1278
+ 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()));
1279
+ go.disabled = o.busy;
1280
+ panel.appendChild(go);
1281
+ }
1282
+ wrap.appendChild(panel);
1283
+ const comments = el(doc, "div", "g-m-comments");
1284
+ comments.appendChild(el(doc, "h4", "g-m-comments-title", idea.thread.length ? `Comments (${idea.thread.length})` : "Comments"));
1285
+ if (idea.thread.length) {
1286
+ const list = el(doc, "ul", "g-m-comment-list");
1287
+ idea.thread.forEach((c) => list.appendChild(comment(doc, c)));
1288
+ comments.appendChild(list);
1289
+ } else comments.appendChild(el(doc, "p", "g-m-hint", "No comments yet."));
1290
+ if (!o.blocked) {
1291
+ const form = el(doc, "form", "g-m-comment-form");
1292
+ form.noValidate = true;
1293
+ const box = el(doc, "textarea", "g-input");
1294
+ box.rows = 2;
1295
+ box.maxLength = import_shared.MAX_FEEDBACK_COMMENT;
1296
+ box.placeholder = "Add a comment\u2026";
1297
+ box.setAttribute("aria-label", "Comment");
1298
+ box.value = o.commentDraft;
1299
+ const post = el(doc, "button", "g-btn g-btn-secondary", "Post");
1300
+ post.type = "submit";
1301
+ post.disabled = o.busy || !o.commentDraft.trim();
1302
+ box.addEventListener("input", () => {
1303
+ o.onCommentInput(box.value);
1304
+ post.disabled = o.busy || !box.value.trim();
1305
+ });
1306
+ form.addEventListener("submit", (e) => {
1307
+ e.preventDefault();
1308
+ if (box.value.trim() && !o.busy) o.onComment();
1309
+ });
1310
+ form.append(box, post);
1311
+ comments.appendChild(form);
1312
+ }
1313
+ wrap.appendChild(comments);
1314
+ return wrap;
1315
+ }
1316
+ var emptyIdeaDraft = (over = {}) => ({ title: "", description: "", productArea: "", priority: "nice_to_have", conversationId: null, similar: null, similarFor: "", error: null, sending: false, checked: false, ...over });
1317
+ function ideaProblem(draft) {
1318
+ return draft.title.trim().length < 3 ? "Give your idea a short title." : null;
1319
+ }
1320
+ function ideaForm(doc, o) {
1321
+ const { draft } = o;
1322
+ const form = el(doc, "form", "g-m-form g-m-idea-form");
1323
+ form.noValidate = true;
1324
+ const title = el(doc, "input", "g-input");
1325
+ title.type = "text";
1326
+ title.maxLength = import_shared.MAX_FEEDBACK_TITLE;
1327
+ title.required = true;
1328
+ title.value = draft.title;
1329
+ title.placeholder = "Your idea in a few words";
1330
+ title.addEventListener("input", () => {
1331
+ draft.title = title.value;
1332
+ o.onTitleInput();
1333
+ });
1334
+ form.appendChild(field2(doc, "Idea", title, { invalid: draft.checked && !!ideaProblem(draft) }));
1335
+ const similar = el(doc, "div", "g-m-similar");
1336
+ similar.setAttribute("aria-live", "polite");
1337
+ form.appendChild(similar);
1338
+ const desc = el(doc, "textarea", "g-input g-m-textarea");
1339
+ desc.rows = 4;
1340
+ desc.maxLength = import_shared.MAX_FEEDBACK_DESCRIPTION;
1341
+ desc.value = draft.description;
1342
+ desc.placeholder = "What are you trying to do, and what gets in the way?";
1343
+ desc.addEventListener("input", () => draft.description = desc.value);
1344
+ form.appendChild(field2(doc, "Details", desc, { optional: true }));
1345
+ if (o.productAreas.length) {
1346
+ const select = el(doc, "select", "g-input");
1347
+ const opt = (label, value) => {
1348
+ const x = el(doc, "option", "", label);
1349
+ x.value = value;
1350
+ return x;
1351
+ };
1352
+ select.appendChild(opt("Choose one (optional)", ""));
1353
+ o.productAreas.forEach((a) => select.appendChild(opt(a, a)));
1354
+ select.value = o.productAreas.includes(draft.productArea) ? draft.productArea : "";
1355
+ select.addEventListener("change", () => draft.productArea = select.value);
1356
+ form.appendChild(field2(doc, "Product area", select, { optional: true }));
1357
+ }
1358
+ form.appendChild(priorityChoice(doc, draft.priority, (p) => draft.priority = p));
1359
+ const foot = el(doc, "div", "g-m-form-foot");
1360
+ if (draft.error) foot.appendChild(errorLine(doc, draft.error));
1361
+ const submit = el(doc, "button", "g-btn g-btn-primary g-m-wide", draft.sending ? "Sending\u2026" : "Submit idea");
1362
+ submit.type = "submit";
1363
+ submit.disabled = draft.sending;
1364
+ foot.appendChild(submit);
1365
+ form.appendChild(foot);
1366
+ form.addEventListener("submit", (e) => {
1367
+ e.preventDefault();
1368
+ if (!draft.sending) o.onSubmit();
1369
+ });
1370
+ return { form, similar };
1371
+ }
1372
+ function similarIdeas(doc, ideas, row) {
1373
+ if (!ideas.length) return null;
1374
+ const box = el(doc, "div", "g-m-similar-box");
1375
+ box.appendChild(el(doc, "div", "g-m-card-label", "Similar ideas \u2014 vote instead?"));
1376
+ const list = el(doc, "div", "g-m-idea-list");
1377
+ list.setAttribute("role", "list");
1378
+ ideas.slice(0, 5).forEach((i) => list.appendChild(row(i)));
1379
+ box.appendChild(list);
1380
+ return box;
1381
+ }
1382
+ function ideaSent(doc, o) {
1383
+ const wrap = el(doc, "div", "g-m-form g-m-sent");
1384
+ const top = el(doc, "div", "g-m-sent-top");
1385
+ const tick = el(doc, "span", "g-m-sent-tick", "\u2713");
1386
+ tick.setAttribute("aria-hidden", "true");
1387
+ const p = el(doc, "p", "g-m-card-sub");
1388
+ 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.");
1389
+ top.append(tick, el(doc, "h3", "g-m-form-title", "Thanks for your idea"), p);
1390
+ 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));
1391
+ return wrap;
1392
+ }
1393
+ function updatesList(doc, updates, onOpen) {
1394
+ const wrap = el(doc, "div", "g-m-idea-updates");
1395
+ if (!updates.length) {
1396
+ wrap.appendChild(el(doc, "div", "g-m-empty", "No updates yet. Follow or vote for an idea to hear when it changes."));
1397
+ return wrap;
1398
+ }
1399
+ for (const u of updates) {
1400
+ const card = el(doc, "button", "g-m-card g-m-update");
1401
+ card.type = "button";
1402
+ card.disabled = !u.requestId;
1403
+ const row = el(doc, "span", "g-m-row");
1404
+ if (!u.read) row.appendChild(el(doc, "span", "g-m-dot"));
1405
+ const text = el(doc, "span", "g-m-grow");
1406
+ text.appendChild(el(doc, "span", "g-m-card-title", u.title));
1407
+ if (u.body) text.appendChild(el(doc, "span", "g-m-card-sub g-m-clamp", u.body));
1408
+ row.append(text, el(doc, "span", "g-m-time", shortDate(u.createdAt)));
1409
+ card.appendChild(row);
1410
+ card.setAttribute("aria-label", `${u.title}${u.read ? "" : ", new"}`);
1411
+ card.addEventListener("click", () => onOpen(u));
1412
+ wrap.appendChild(card);
1413
+ }
1414
+ return wrap;
1415
+ }
1416
+ var LIST_TTL_MS = 15e3;
1417
+ var asDetail = (d) => ({ ...d, title: d.title ?? "", description: d.description ?? "", statusNote: d.statusNote ?? "", votes: d.votes ?? 0, thread: Array.isArray(d.thread) ? d.thread : [] });
1418
+ function createIdeas(o) {
1419
+ const { doc, api } = o;
1420
+ let query = { sort: "top", filter: "open", page: 1 };
1421
+ let list = null;
1422
+ let hasMore = false;
1423
+ let listAt = 0;
1424
+ let listLoading = false;
1425
+ let listError = null;
1426
+ let listSeq = 0;
1427
+ let listActionError = null;
1428
+ const details = /* @__PURE__ */ new Map();
1429
+ const detailErrors = /* @__PURE__ */ new Map();
1430
+ const actionErrors = /* @__PURE__ */ new Map();
1431
+ const voteDrafts = /* @__PURE__ */ new Map();
1432
+ const commentDrafts = /* @__PURE__ */ new Map();
1433
+ const busy = /* @__PURE__ */ new Set();
1434
+ const offers = /* @__PURE__ */ new Map();
1435
+ let updates = null;
1436
+ let updatesAt = 0;
1437
+ let updatesLoading = false;
1438
+ let updatesSeq = 0;
1439
+ let draft = emptyIdeaDraft();
1440
+ let similarTimer = null;
1441
+ let similarSeq = 0;
1442
+ let searchTimer = null;
1443
+ let who = o.identified();
1444
+ let mounted = null;
1445
+ const mount = (root, paint) => {
1446
+ mounted = { root, paint };
1447
+ paint();
1448
+ };
1449
+ const repaint = () => {
1450
+ if (mounted?.root.isConnected) mounted.paint();
1451
+ };
1452
+ const showing = (root) => mounted?.root === root && root.isConnected;
1453
+ const cfg = () => o.config() ?? { title: "Ideas", anonymous: false, readOnly: true, productAreas: [], portal: false };
1454
+ function blocked() {
1455
+ const c = cfg();
1456
+ 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.";
1457
+ if (!c.anonymous && !o.identified()) return "Log in to vote, comment or share an idea.";
1458
+ return null;
1459
+ }
1460
+ const canPortal = () => cfg().portal && !!api.ideaPortalLink;
1461
+ function openPortal() {
1462
+ if (!api.ideaPortalLink) return;
1463
+ const win = doc.defaultView;
1464
+ let tab = null;
1465
+ try {
1466
+ tab = win?.open("about:blank", "_blank") ?? null;
1467
+ if (tab) tab.opener = null;
1468
+ } catch {
1469
+ tab = null;
1470
+ }
1471
+ api.ideaPortalLink().then((raw) => {
1472
+ const url = safeHttpUrl(raw);
1473
+ if (!url) throw new Error("The portal link isn't available.");
1474
+ if (tab) tab.location.href = url;
1475
+ else win?.open(url, "_blank", "noopener");
1476
+ }).catch((e) => {
1477
+ tab?.close();
1478
+ listActionError = errorText(e, "The feedback portal couldn't be opened.");
1479
+ repaint();
1480
+ });
1481
+ }
1482
+ const portal = () => canPortal() ? openPortal : void 0;
1483
+ function patch(idea) {
1484
+ if (list) list = list.map((x) => x.id === idea.id ? { ...x, ...idea } : x);
1485
+ const d = details.get(idea.id);
1486
+ if (d) details.set(idea.id, { ...d, ...idea, thread: "thread" in idea ? idea.thread : d.thread });
1487
+ if (draft.similar) draft.similar = draft.similar.map((x) => x.id === idea.id ? { ...x, ...idea } : x);
1488
+ }
1489
+ const find = (id) => details.get(id) ?? list?.find((x) => x.id === id) ?? draft.similar?.find((x) => x.id === id);
1490
+ function toggleVote(idea) {
1491
+ if (busy.has(idea.id) || blocked() || !api.voteIdea || idea.myVote && !api.unvoteIdea) return;
1492
+ busy.add(idea.id);
1493
+ listActionError = null;
1494
+ const voted = !!idea.myVote;
1495
+ patch({ ...idea, votes: Math.max(0, idea.votes + (voted ? -1 : 1)), myVote: voted ? null : { priority: "nice_to_have" } });
1496
+ repaint();
1497
+ (voted ? api.unvoteIdea(idea.id) : api.voteIdea(idea.id, { priority: "nice_to_have" })).then(patch).catch((e) => {
1498
+ patch(idea);
1499
+ listActionError = errorText(e, "Your vote couldn't be saved.");
1500
+ }).finally(() => {
1501
+ busy.delete(idea.id);
1502
+ repaint();
1503
+ });
1504
+ }
1505
+ function loadList(append = false) {
1506
+ if (!api.listIdeas) return;
1507
+ const seq = ++listSeq;
1508
+ listLoading = true;
1509
+ listError = null;
1510
+ const q = { ...query };
1511
+ api.listIdeas(q).then((res) => {
1512
+ if (seq !== listSeq) return;
1513
+ const got = Array.isArray(res?.requests) ? res.requests : [];
1514
+ list = append && list ? [...list, ...got.filter((r) => !list.some((x) => x.id === r.id))] : got;
1515
+ hasMore = !!res?.hasMore;
1516
+ listAt = Date.now();
1517
+ }).catch((e) => {
1518
+ if (seq === listSeq) listError = errorText(e, "Ideas couldn't be loaded.");
1519
+ }).finally(() => {
1520
+ if (seq !== listSeq) return;
1521
+ listLoading = false;
1522
+ repaint();
1523
+ });
1524
+ }
1525
+ const requery = (change) => {
1526
+ query = { ...query, ...change, page: 1 };
1527
+ loadList();
1528
+ repaint();
1529
+ };
1530
+ function listScreen() {
1531
+ const root = el(doc, "div", "g-m-scroll g-m-ideas");
1532
+ const top = el(doc, "div", "g-m-ideas-top");
1533
+ const why = blocked();
1534
+ if (why) top.appendChild(writeNotice(doc, why, portal()));
1535
+ else top.appendChild(button(doc, "g-btn g-btn-primary g-m-wide", "Suggest an idea", o.openForm));
1536
+ const updatesLink = el(doc, "div");
1537
+ top.appendChild(updatesLink);
1538
+ const search = el(doc, "input", "g-input g-m-idea-search");
1539
+ search.type = "search";
1540
+ search.placeholder = "Search ideas";
1541
+ search.setAttribute("aria-label", "Search ideas");
1542
+ search.maxLength = 200;
1543
+ search.value = query.q ?? "";
1544
+ search.addEventListener("input", () => {
1545
+ if (searchTimer) clearTimeout(searchTimer);
1546
+ searchTimer = setTimeout(() => requery({ q: search.value.trim() || void 0 }), 300);
1547
+ });
1548
+ const tools = el(doc, "div", "g-m-row g-m-center g-m-idea-tools");
1549
+ tools.append(filterChips(doc, query.filter, (filter) => requery({ filter })), sortSelect(doc, query.sort, (sort) => requery({ sort })));
1550
+ top.append(search, tools);
1551
+ const results = el(doc, "div", "g-m-idea-results");
1552
+ root.append(top, results);
1553
+ mount(root, () => {
1554
+ updatesLink.textContent = "";
1555
+ const n = updates?.unread ?? 0;
1556
+ 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));
1557
+ results.textContent = "";
1558
+ if (listActionError) results.appendChild(errorLine(doc, listActionError));
1559
+ if (!list) {
1560
+ if (listError) {
1561
+ results.append(errorLine(doc, listError), button(doc, "g-m-text-btn", "Try again", () => loadList()));
1562
+ } else results.appendChild(el(doc, "div", "g-m-empty", "Loading ideas\u2026"));
1563
+ return;
1564
+ }
1565
+ if (!list.length) {
1566
+ 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.";
1567
+ results.appendChild(el(doc, "div", "g-m-empty", empty));
1568
+ } else {
1569
+ const rows = el(doc, "div", "g-m-idea-list");
1570
+ rows.setAttribute("role", "list");
1571
+ rows.setAttribute("aria-busy", String(listLoading));
1572
+ const canVote = !blocked();
1573
+ list.forEach((idea) => rows.appendChild(ideaRow(doc, idea, { canVote, busy: busy.has(idea.id), onVote: () => toggleVote(idea), onOpen: () => o.openIdea(idea.id) })));
1574
+ results.appendChild(rows);
1575
+ }
1576
+ if (listError) results.appendChild(errorLine(doc, listError));
1577
+ if (hasMore) {
1578
+ const more = button(doc, "g-btn g-btn-secondary g-m-wide", listLoading ? "Loading\u2026" : "Show more", () => {
1579
+ query = { ...query, page: query.page + 1 };
1580
+ loadList(true);
1581
+ repaint();
1582
+ });
1583
+ more.disabled = listLoading;
1584
+ results.appendChild(more);
1585
+ }
1586
+ if (!why && canPortal()) results.appendChild(button(doc, "g-m-text-btn g-m-portal-link", "Open the feedback portal", openPortal));
1587
+ });
1588
+ if (!list || Date.now() - listAt > LIST_TTL_MS || query.page > 1) {
1589
+ query = { ...query, page: 1 };
1590
+ loadList();
1591
+ }
1592
+ return root;
1593
+ }
1594
+ function act(id, call, fallback, done) {
1595
+ if (busy.has(id)) return;
1596
+ const p = call();
1597
+ if (!p) return;
1598
+ busy.add(id);
1599
+ actionErrors.delete(id);
1600
+ repaint();
1601
+ p.then((res) => {
1602
+ patch(res);
1603
+ done?.();
1604
+ }).catch((e) => actionErrors.set(id, errorText(e, fallback))).finally(() => {
1605
+ busy.delete(id);
1606
+ repaint();
1607
+ });
1608
+ }
1609
+ function detailScreen(id) {
1610
+ const root = el(doc, "div", "g-m-scroll");
1611
+ mount(root, () => {
1612
+ root.textContent = "";
1613
+ const idea = details.get(id);
1614
+ if (!idea) {
1615
+ const err = detailErrors.get(id);
1616
+ root.appendChild(err ? errorLine(doc, err) : el(doc, "div", "g-m-empty", "Loading\u2026"));
1617
+ return;
1618
+ }
1619
+ if (!voteDrafts.has(id)) voteDrafts.set(id, { priority: "nice_to_have", note: "" });
1620
+ root.appendChild(
1621
+ ideaDetail(doc, {
1622
+ idea,
1623
+ blocked: blocked(),
1624
+ onPortal: portal(),
1625
+ busy: busy.has(id),
1626
+ error: actionErrors.get(id) ?? null,
1627
+ vote: voteDrafts.get(id),
1628
+ commentDraft: commentDrafts.get(id) ?? "",
1629
+ onVote: (priority, note) => act(id, () => api.voteIdea?.(id, { priority, ...note ? { note } : {} }), "Your vote couldn't be saved.", () => voteDrafts.delete(id)),
1630
+ onUnvote: () => act(id, () => api.unvoteIdea?.(id), "Your vote couldn't be removed."),
1631
+ onFollow: (follow) => act(id, () => api.followIdea?.(id, follow), "That didn't work. Please try again."),
1632
+ onCommentInput: (text) => commentDrafts.set(id, text),
1633
+ onComment: () => act(id, () => api.commentIdea?.(id, (commentDrafts.get(id) ?? "").trim()), "Your comment couldn't be posted.", () => commentDrafts.delete(id))
1634
+ })
1635
+ );
1636
+ });
1637
+ if (api.getIdea) {
1638
+ detailErrors.delete(id);
1639
+ api.getIdea(id).then((d) => {
1640
+ details.set(id, asDetail(d));
1641
+ patch(d);
1642
+ }).catch((e) => {
1643
+ if (!details.has(id)) detailErrors.set(id, errorText(e, "This idea couldn't be loaded."));
1644
+ }).finally(() => {
1645
+ if (showing(root)) mounted.paint();
1646
+ });
1647
+ }
1648
+ return root;
1649
+ }
1650
+ function lookUpSimilar(paint) {
1651
+ const title = draft.title.trim();
1652
+ if (title.length < 3 || !api.similarIdeas) {
1653
+ draft.similar = null;
1654
+ draft.similarFor = "";
1655
+ return paint();
1656
+ }
1657
+ if (title === draft.similarFor) return;
1658
+ const seq = ++similarSeq;
1659
+ api.similarIdeas(title).then((res) => {
1660
+ if (seq !== similarSeq) return;
1661
+ draft.similar = res;
1662
+ draft.similarFor = title;
1663
+ paint();
1664
+ }).catch(() => {
1665
+ });
1666
+ }
1667
+ function formScreen() {
1668
+ const root = el(doc, "div", "g-m-scroll");
1669
+ let similarBox = null;
1670
+ const paintSimilar = () => {
1671
+ if (!similarBox) return;
1672
+ similarBox.textContent = "";
1673
+ const canVote = !blocked();
1674
+ const box = similarIdeas(doc, draft.similar ?? [], (idea) => ideaRow(doc, idea, { canVote, busy: busy.has(idea.id), onVote: () => toggleVote(idea), onOpen: () => o.openIdea(idea.id) }));
1675
+ if (box) similarBox.appendChild(box);
1676
+ if (listActionError) similarBox.appendChild(errorLine(doc, listActionError));
1677
+ };
1678
+ const paintAll = () => {
1679
+ root.textContent = "";
1680
+ const why = blocked();
1681
+ if (why) {
1682
+ root.appendChild(writeNotice(doc, why, portal()));
1683
+ similarBox = null;
1684
+ return;
1685
+ }
1686
+ const { form, similar } = ideaForm(doc, {
1687
+ draft,
1688
+ productAreas: cfg().productAreas ?? [],
1689
+ onTitleInput: () => {
1690
+ if (similarTimer) clearTimeout(similarTimer);
1691
+ similarTimer = setTimeout(() => {
1692
+ if (showing(root)) lookUpSimilar(paintSimilar);
1693
+ }, 300);
1694
+ },
1695
+ onSubmit: () => submit(root, paintAll)
1696
+ });
1697
+ root.appendChild(form);
1698
+ similarBox = similar;
1699
+ paintSimilar();
1700
+ };
1701
+ mounted = { root, paint: paintSimilar };
1702
+ paintAll();
1703
+ if (draft.title.trim() && draft.title.trim() !== draft.similarFor) lookUpSimilar(paintSimilar);
1704
+ return root;
1705
+ }
1706
+ function submit(root, paintAll) {
1707
+ const d = draft;
1708
+ d.checked = true;
1709
+ d.error = ideaProblem(d);
1710
+ if (d.error || !api.submitIdea) return paintAll();
1711
+ d.sending = true;
1712
+ paintAll();
1713
+ api.submitIdea({ title: d.title.trim(), description: d.description.trim(), productArea: d.productArea || null, priority: d.priority, conversationId: d.conversationId }).then((idea) => {
1714
+ details.set(idea.id, asDetail(idea));
1715
+ listAt = 0;
1716
+ if (draft === d) draft = emptyIdeaDraft();
1717
+ if (showing(root)) o.openSent(idea);
1718
+ }).catch((e) => {
1719
+ d.sending = false;
1720
+ d.error = errorText(e, "Your idea couldn't be sent.");
1721
+ if (showing(root)) paintAll();
1722
+ });
1723
+ }
1724
+ function updatesScreen() {
1725
+ const root = el(doc, "div", "g-m-scroll g-m-idea-updates-screen");
1726
+ mount(root, () => {
1727
+ root.textContent = "";
1728
+ if (!updates) {
1729
+ root.appendChild(el(doc, "div", "g-m-empty", updatesLoading || api.ideaUpdates ? "Loading\u2026" : "No updates yet."));
1730
+ return;
1731
+ }
1732
+ root.appendChild(updatesList(doc, updates.updates, (u) => u.requestId && o.openIdea(u.requestId)));
1733
+ });
1734
+ if (!updates) loadUpdates();
1735
+ else markRead();
1736
+ return root;
1737
+ }
1738
+ function markRead() {
1739
+ const ids = updates?.updates.filter((u) => !u.read).map((u) => u.id) ?? [];
1740
+ if (!ids.length || !api.markIdeaUpdatesRead) return;
1741
+ const seq = updatesSeq;
1742
+ api.markIdeaUpdatesRead(ids).then((res) => {
1743
+ if (seq !== updatesSeq || !updates) return;
1744
+ updates = { updates: updates.updates, unread: res.unread };
1745
+ o.onUnreadChange();
1746
+ }).catch(() => {
1747
+ });
1748
+ }
1749
+ function loadUpdates(maxAgeMs = 0) {
1750
+ if (!api.ideaUpdates || updatesLoading || Date.now() - updatesAt < maxAgeMs) return;
1751
+ updatesLoading = true;
1752
+ const seq = ++updatesSeq;
1753
+ api.ideaUpdates().then((res) => {
1754
+ if (seq !== updatesSeq) return;
1755
+ const before = updates?.unread ?? 0;
1756
+ updates = { updates: Array.isArray(res?.updates) ? res.updates : [], unread: Number(res?.unread) || 0 };
1757
+ updatesAt = Date.now();
1758
+ if (updates.unread !== before) o.onUnreadChange();
1759
+ if (mounted?.root.isConnected) {
1760
+ mounted.paint();
1761
+ if (mounted.root.classList.contains("g-m-idea-updates-screen")) markRead();
1762
+ }
1763
+ }).catch(() => {
1764
+ }).finally(() => {
1765
+ if (seq === updatesSeq) updatesLoading = false;
1766
+ });
1767
+ }
1768
+ function offerChip(offer, conversationId, lead) {
1769
+ const canWrite = !blocked();
1770
+ const b = el(doc, "button", "g-m-chip g-m-guide g-m-offer g-m-idea-offer");
1771
+ b.type = "button";
1772
+ const label = el(doc, "span");
1773
+ b.append(lead(), label);
1774
+ const id = offer.requestId;
1775
+ if (!id) {
1776
+ if (!canWrite || !api.submitIdea) return null;
1777
+ label.textContent = "Suggest this idea";
1778
+ b.addEventListener("click", () => {
1779
+ startDraft({ title: offer.title, description: offer.description, conversationId });
1780
+ o.openForm();
1781
+ });
1782
+ return b;
1783
+ }
1784
+ const paint = () => {
1785
+ const state = offers.get(id) ?? (find(id)?.myVote ? "voted" : void 0);
1786
+ b.disabled = state === "voting";
1787
+ 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`;
1788
+ return state;
1789
+ };
1790
+ paint();
1791
+ b.addEventListener("click", () => {
1792
+ const state = paint();
1793
+ if (!canWrite || state === "voted" || state === "failed" || !api.voteIdea) return o.openIdea(id);
1794
+ if (state === "voting") return;
1795
+ offers.set(id, "voting");
1796
+ paint();
1797
+ api.voteIdea(id, { priority: "nice_to_have", conversationId }).then((idea) => {
1798
+ patch(idea);
1799
+ offers.set(id, "voted");
1800
+ }).catch(() => offers.set(id, "failed")).finally(paint);
1801
+ });
1802
+ return b;
1803
+ }
1804
+ function startDraft(prefill = {}) {
1805
+ 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 });
1806
+ }
1807
+ return {
1808
+ blocked,
1809
+ unread: () => updates?.unread ?? 0,
1810
+ listScreen,
1811
+ detailScreen,
1812
+ formScreen,
1813
+ sentScreen: (idea) => {
1814
+ mounted = null;
1815
+ const root = el(doc, "div", "g-m-scroll");
1816
+ root.appendChild(ideaSent(doc, { idea, onView: () => o.openIdea(idea.id), onBack: o.openList }));
1817
+ return root;
1818
+ },
1819
+ updatesScreen,
1820
+ startDraft,
1821
+ offerChip,
1822
+ loadUpdates,
1823
+ sync(open) {
1824
+ const now = o.identified();
1825
+ if (now !== who) {
1826
+ who = now;
1827
+ list = null;
1828
+ listAt = 0;
1829
+ details.clear();
1830
+ offers.clear();
1831
+ updates = null;
1832
+ updatesAt = 0;
1833
+ updatesSeq++;
1834
+ updatesLoading = false;
1835
+ o.onUnreadChange();
1836
+ }
1837
+ if (open) loadUpdates(3e4);
1838
+ },
1839
+ destroy() {
1840
+ if (similarTimer) clearTimeout(similarTimer);
1841
+ if (searchTimer) clearTimeout(searchTimer);
1842
+ mounted = null;
1843
+ }
1844
+ };
1845
+ }
1846
+
1847
+ // src/launcher-drag.ts
1848
+ var DRAG_THRESHOLD = 6;
1849
+ var MARGIN = 16;
1850
+ function parseLauncherSpot(raw) {
1851
+ let v = raw;
1852
+ if (typeof v === "string") {
1853
+ try {
1854
+ v = JSON.parse(v);
1855
+ } catch {
1856
+ return null;
1857
+ }
1858
+ }
1859
+ if (!v || typeof v !== "object") return null;
1860
+ const { side, y } = v;
1861
+ if (side !== "left" && side !== "right" || typeof y !== "number" || !Number.isFinite(y)) return null;
1862
+ return { side, y: Math.min(1, Math.max(0, y)) };
1863
+ }
1864
+ function maxLauncherBottom(viewportHeight, size) {
1865
+ return Math.max(0, viewportHeight - size - MARGIN * 2);
1866
+ }
1867
+ function snapLauncher(drop, viewport, size) {
1868
+ const side = drop.left + size / 2 < viewport.width / 2 ? "left" : "right";
1869
+ const max = maxLauncherBottom(viewport.height, size);
1870
+ const bottom = Math.min(max, Math.max(0, viewport.height - MARGIN - size - drop.top));
1871
+ return { side, y: max ? bottom / max : 0 };
1872
+ }
1873
+ function enableLauncherDrag({ launcher, onMove }) {
1874
+ const win = launcher.ownerDocument.defaultView ?? window;
1875
+ let start = null;
1876
+ let dragging = false;
1877
+ let swallowClickUntil = 0;
1878
+ const onDown = (e) => {
1879
+ if (e.button !== 0) return;
1880
+ const r = launcher.getBoundingClientRect();
1881
+ start = { id: e.pointerId, x: e.clientX, y: e.clientY, left: r.left, top: r.top };
1882
+ dragging = false;
1883
+ try {
1884
+ launcher.setPointerCapture(e.pointerId);
1885
+ } catch {
1886
+ }
1887
+ };
1888
+ const onMovePointer = (e) => {
1889
+ if (!start || e.pointerId !== start.id) return;
1890
+ const dx = e.clientX - start.x;
1891
+ const dy = e.clientY - start.y;
1892
+ if (!dragging) {
1893
+ if (Math.hypot(dx, dy) < DRAG_THRESHOLD) return;
1894
+ dragging = true;
1895
+ launcher.classList.add("g-m-dragging");
1896
+ }
1897
+ e.preventDefault();
1898
+ const size = launcher.offsetWidth || 56;
1899
+ const left = Math.min(win.innerWidth - size - MARGIN, Math.max(MARGIN, start.left + dx));
1900
+ const top = Math.min(win.innerHeight - size - MARGIN, Math.max(MARGIN, start.top + dy));
1901
+ Object.assign(launcher.style, { left: `${left}px`, top: `${top}px`, right: "auto", bottom: "auto", margin: "0" });
1902
+ };
1903
+ const onUp = (e) => {
1904
+ if (!start || e.pointerId !== start.id) return;
1905
+ start = null;
1906
+ if (!dragging) return;
1907
+ dragging = false;
1908
+ swallowClickUntil = Date.now() + 400;
1909
+ const before = launcher.getBoundingClientRect();
1910
+ launcher.classList.remove("g-m-dragging");
1911
+ Object.assign(launcher.style, { left: "", top: "", right: "", margin: "" });
1912
+ onMove(snapLauncher({ left: before.left, top: before.top }, { width: win.innerWidth, height: win.innerHeight }, before.width || 56));
1913
+ const after = launcher.getBoundingClientRect();
1914
+ const dx = before.left - after.left;
1915
+ const dy = before.top - after.top;
1916
+ if (!dx && !dy) return;
1917
+ launcher.style.transition = "none";
1918
+ launcher.style.transform = `translate(${dx}px, ${dy}px)`;
1919
+ void launcher.offsetWidth;
1920
+ launcher.style.transition = "";
1921
+ launcher.classList.add("g-m-snapping");
1922
+ launcher.style.transform = "";
1923
+ const done = () => launcher.classList.remove("g-m-snapping");
1924
+ launcher.addEventListener("transitionend", done, { once: true });
1925
+ win.setTimeout(done, 400);
1926
+ };
1927
+ const onClick = (e) => {
1928
+ if (Date.now() < swallowClickUntil) {
1929
+ swallowClickUntil = 0;
1930
+ e.preventDefault();
1931
+ e.stopImmediatePropagation();
1932
+ }
1933
+ };
1934
+ launcher.addEventListener("pointerdown", onDown);
1935
+ launcher.addEventListener("pointermove", onMovePointer);
1936
+ launcher.addEventListener("pointerup", onUp);
1937
+ launcher.addEventListener("pointercancel", onUp);
1938
+ launcher.addEventListener("click", onClick, true);
1939
+ return () => {
1940
+ launcher.removeEventListener("pointerdown", onDown);
1941
+ launcher.removeEventListener("pointermove", onMovePointer);
1942
+ launcher.removeEventListener("pointerup", onUp);
1943
+ launcher.removeEventListener("pointercancel", onUp);
1944
+ launcher.removeEventListener("click", onClick, true);
1945
+ };
1946
+ }
1947
+
907
1948
  // src/messenger-styles.ts
908
1949
  var MESSENGER_STYLES = `
909
1950
  :host([data-g-messenger="bottom-right"]) .g-launcher.g-pos-bottom-right{right:72px}
@@ -912,9 +1953,13 @@ var MESSENGER_STYLES = `
912
1953
  .g-m-launcher:hover{transform:scale(1.05)}
913
1954
  .g-m-launcher:focus-visible{outline:2px solid var(--g-primary);outline-offset:3px}
914
1955
  .g-m-launcher svg{width:26px;height:26px;fill:currentColor}
1956
+ .g-m-launcher{touch-action:none;-webkit-user-select:none;user-select:none}
1957
+ .g-m-launcher.g-m-dragging{transition:none;cursor:grabbing;transform:scale(1.08)}
1958
+ .g-m-launcher.g-m-snapping{transition:transform .3s cubic-bezier(.2,.8,.2,1)}
915
1959
  .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)}
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)}
1960
+ .g-messenger{bottom:var(--g-m-bottom,76px);height:min(640px,calc(100vh - var(--g-m-reserve,110px)));transition:width .2s ease,height .2s ease,max-height .2s ease}
1961
+ .g-messenger.g-m-expanded{width:min(720px,calc(100vw - 32px));height:calc(100vh - var(--g-m-reserve,110px));max-height:calc(100vh - var(--g-m-reserve,110px))}
1962
+ .g-messenger.g-m-below{bottom:auto;top:var(--g-m-top)}
918
1963
  .g-m-expanded .g-rc-article,.g-m-expanded .g-m-scroll{padding:24px clamp(20px,5vw,40px)}
919
1964
  .g-m-expanded .g-rc-article>.g-title{font-size:24px;line-height:1.25;margin-bottom:12px}
920
1965
  .g-m-expanded .g-rc-article .g-body{font-size:15px}
@@ -966,7 +2011,7 @@ var MESSENGER_STYLES = `
966
2011
  .g-m-typing span{width:6px;height:6px;border-radius:999px;background:var(--g-muted);animation:g-m-bounce 1s infinite}
967
2012
  .g-m-typing span:nth-child(2){animation-delay:.15s}.g-m-typing span:nth-child(3){animation-delay:.3s}
968
2013
  @keyframes g-m-bounce{0%,60%,100%{opacity:.3;transform:translateY(0)}30%{opacity:1;transform:translateY(-3px)}}
969
- @media (prefers-reduced-motion: reduce){.g-m-typing span{animation:none}.g-m-launcher{transition:none}}
2014
+ @media (prefers-reduced-motion: reduce){.g-m-typing span{animation:none}.g-m-launcher,.g-m-launcher.g-m-snapping{transition:none}}
970
2015
  .g-m-chips{display:flex;flex-direction:column;gap:6px;margin-top:2px}
971
2016
  .g-m-chip{display:flex;align-items:center;gap:8px;text-align:left;border:1px solid var(--g-border);background:var(--g-bg);color:var(--g-text);border-radius:10px;padding:8px 10px;font:inherit;font-size:13px;cursor:pointer}
972
2017
  .g-m-chip:hover{background:var(--g-subtle)}
@@ -1107,90 +2152,81 @@ var MESSENGER_STYLES = `
1107
2152
  .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
2153
  .g-m-step-done{color:var(--g-text);font-weight:600}
1109
2154
  .g-m-step-done .g-m-step-dot{background:var(--g-primary);border-color:var(--g-primary)}
2155
+ .g-m-ideas-top{display:flex;flex-direction:column;gap:10px;margin-bottom:12px}
2156
+ .g-m-ideas-top:empty{display:none}
2157
+ .g-m-idea-tools{justify-content:space-between;gap:8px;flex-wrap:wrap}
2158
+ .g-m-filters{display:flex;gap:6px;flex-wrap:wrap}
2159
+ .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}
2160
+ .g-m-filter:hover{background:var(--g-subtle)}
2161
+ .g-m-filter[aria-pressed="true"]{background:var(--g-primary);border-color:var(--g-primary);color:var(--g-primary-text)}
2162
+ .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}
2163
+ .g-m-sort{width:auto;min-width:0;min-height:30px;padding:4px 8px;font-size:12px}
2164
+ .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}
2165
+ .g-m-idea{display:flex;align-items:flex-start;gap:12px;padding:12px 14px;border-top:1px solid var(--g-border)}
2166
+ .g-m-idea:first-child{border-top:0}
2167
+ .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}
2168
+ button.g-m-vote:hover{border-color:var(--g-primary);color:var(--g-primary)}
2169
+ .g-m-vote:disabled{opacity:.6;cursor:progress}
2170
+ .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)}
2171
+ .g-m-vote-static{cursor:default;background:var(--g-subtle)}
2172
+ .g-m-vote-ico{width:16px;height:16px;stroke-width:2.4}
2173
+ .g-m-vote-count{font-size:13px;font-weight:700;line-height:1.2}
2174
+ .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}
2175
+ .g-m-idea-open:hover .g-m-idea-title{color:var(--g-primary)}
2176
+ .g-m-idea-title{font-weight:600;overflow-wrap:anywhere}
2177
+ .g-m-idea-meta{display:flex;align-items:center;gap:8px;flex-wrap:wrap}
2178
+ .g-m-idea-pill.g-m-idea-new{background:var(--g-subtle);color:var(--g-muted)}
2179
+ .g-m-idea-pill.g-m-idea-under_review{background:#e0e7ff;color:#3730a3}
2180
+ .g-m-idea-pill.g-m-idea-planned{background:#dbeafe;color:#1e40af}
2181
+ .g-m-idea-pill.g-m-idea-in_progress{background:#fef3c7;color:#92400e}
2182
+ .g-m-idea-pill.g-m-idea-released{background:#dcfce7;color:#166534}
2183
+ .g-m-idea-pill.g-m-idea-declined{background:#fee2e2;color:#991b1b}
2184
+ .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)}
2185
+ .g-m-idea-notice .g-m-text-btn{padding:4px 0;min-height:0}
2186
+ .g-m-updates-link,.g-m-portal-link{align-self:flex-start;padding-left:0}
2187
+ .g-m-portal-link{display:block;margin:0 auto}
2188
+ .g-m-idea-detail{gap:12px}
2189
+ .g-m-idea-desc,.g-m-idea-note{font-size:14px;overflow-wrap:anywhere}
2190
+ .g-m-idea-desc p,.g-m-idea-note p,.g-m-comment-body p{margin:0 0 8px}
2191
+ .g-m-idea-desc p:last-child,.g-m-idea-note p:last-child,.g-m-comment-body p:last-child{margin:0}
2192
+ .g-m-idea-note{border-left:3px solid var(--g-primary);background:var(--g-subtle);border-radius:6px;padding:8px 10px}
2193
+ .g-m-idea-vote{display:flex;flex-direction:column;gap:12px;margin:0}
2194
+ .g-m-idea-vote .g-m-vote{min-height:44px}
2195
+ .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}
2196
+ .g-m-follow:hover{background:var(--g-subtle)}
2197
+ .g-m-follow.g-m-following{border-color:var(--g-primary);color:var(--g-primary)}
2198
+ .g-m-follow:disabled{opacity:.6}
2199
+ .g-m-seg{display:flex;border:1px solid var(--g-border);border-radius:10px;overflow:hidden}
2200
+ .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}
2201
+ .g-m-seg-btn:first-child{border-left:0}
2202
+ .g-m-seg-btn:hover{background:var(--g-subtle)}
2203
+ .g-m-seg-btn[aria-checked="true"]{background:var(--g-primary);color:var(--g-primary-text)}
2204
+ .g-m-unvote{align-self:flex-start;padding-left:0;color:var(--g-muted)}
2205
+ .g-m-comments{display:flex;flex-direction:column;gap:8px}
2206
+ .g-m-comments-title{margin:4px 0 0;font-size:14px}
2207
+ .g-m-comment-list{list-style:none;margin:0;padding:0;display:flex;flex-direction:column;gap:10px}
2208
+ .g-m-comment{border:1px solid var(--g-border);border-radius:10px;padding:8px 10px;font-size:13px}
2209
+ .g-m-comment-team{background:var(--g-subtle);border-color:color-mix(in srgb,var(--g-primary) 35%,var(--g-border))}
2210
+ .g-m-comment-meta{gap:6px;margin-bottom:4px}
2211
+ .g-m-comment-author{font-weight:600}
2212
+ .g-m-comment-body{overflow-wrap:anywhere}
2213
+ .g-m-comment-form{display:flex;flex-direction:column;align-items:flex-end;gap:6px}
2214
+ .g-m-comment-form textarea{resize:vertical;min-height:56px}
2215
+ .g-m-similar:empty{display:none}
2216
+ .g-m-similar-box{display:flex;flex-direction:column;gap:6px}
2217
+ .g-m-similar-box .g-m-idea-list{margin:0}
2218
+ .g-m-update{display:block}
2219
+ .g-m-update:disabled{cursor:default}
2220
+ .g-m-update:disabled:hover{background:var(--g-bg)}
1110
2221
  `;
1111
2222
 
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
2223
  // src/messenger.ts
1188
- var SVG_NS = "http://www.w3.org/2000/svg";
2224
+ var SVG_NS2 = "http://www.w3.org/2000/svg";
1189
2225
  function chatIcon(doc) {
1190
- const svg = doc.createElementNS(SVG_NS, "svg");
2226
+ const svg = doc.createElementNS(SVG_NS2, "svg");
1191
2227
  svg.setAttribute("viewBox", "0 0 24 24");
1192
2228
  svg.setAttribute("aria-hidden", "true");
1193
- const path = doc.createElementNS(SVG_NS, "path");
2229
+ const path = doc.createElementNS(SVG_NS2, "path");
1194
2230
  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
2231
  svg.appendChild(path);
1196
2232
  return svg;
@@ -1215,15 +2251,16 @@ var ICONS = {
1215
2251
  circle: ["M12 3a9 9 0 1 0 0 18 9 9 0 0 0 0-18z"],
1216
2252
  expand: ["M15 3h6v6", "M21 3l-7 7", "M9 21H3v-6", "M3 21l7-7"],
1217
2253
  flag: ["M5 21V4", "M5 4h12l-2.5 4L17 12H5"],
1218
- collapse: ["M4 14h6v6", "M3 21l7-7", "M20 10h-6V4", "M21 3l-7 7"]
2254
+ collapse: ["M4 14h6v6", "M3 21l7-7", "M20 10h-6V4", "M21 3l-7 7"],
2255
+ idea: IDEA_ICON
1219
2256
  };
1220
2257
  function icon(doc, name, cls = "g-m-ico") {
1221
- const svg = doc.createElementNS(SVG_NS, "svg");
2258
+ const svg = doc.createElementNS(SVG_NS2, "svg");
1222
2259
  svg.setAttribute("viewBox", "0 0 24 24");
1223
2260
  svg.setAttribute("aria-hidden", "true");
1224
2261
  svg.setAttribute("class", cls);
1225
2262
  for (const d of ICONS[name] ?? []) {
1226
- const path = doc.createElementNS(SVG_NS, "path");
2263
+ const path = doc.createElementNS(SVG_NS2, "path");
1227
2264
  path.setAttribute("d", d);
1228
2265
  svg.appendChild(path);
1229
2266
  }
@@ -1250,7 +2287,7 @@ function shortDateTime(iso) {
1250
2287
  return d.toISOString().slice(0, 16).replace("T", " ");
1251
2288
  }
1252
2289
  }
1253
- function shortDate(iso) {
2290
+ function shortDate2(iso) {
1254
2291
  const d = new Date(iso);
1255
2292
  if (Number.isNaN(d.getTime())) return "";
1256
2293
  try {
@@ -1323,7 +2360,6 @@ function renderMessenger(options) {
1323
2360
  const pos = `g-pos-${config.launcher.position}`;
1324
2361
  const rootNode = scope.getRootNode();
1325
2362
  const shadowHost = "host" in rootNode ? rootNode.host : null;
1326
- shadowHost?.setAttribute("data-g-messenger", config.launcher.position);
1327
2363
  const root = el(doc, "div", "g-messenger-root");
1328
2364
  const launcher = el(doc, "button", `g-m-launcher ${pos}`);
1329
2365
  launcher.type = "button";
@@ -1335,6 +2371,47 @@ function renderMessenger(options) {
1335
2371
  launcher.appendChild(badge);
1336
2372
  if (options.hideLauncher) launcher.hidden = true;
1337
2373
  const panel = el(doc, "div", `g-panel g-messenger ${pos}`);
2374
+ let spot = options.launcherSpot ?? null;
2375
+ const win = doc.defaultView ?? window;
2376
+ function placeLauncher() {
2377
+ const side = spot?.side ?? (config.launcher.position === "bottom-left" ? "left" : "right");
2378
+ for (const node of [launcher, panel]) {
2379
+ node.classList.toggle("g-pos-bottom-left", side === "left");
2380
+ node.classList.toggle("g-pos-bottom-right", side === "right");
2381
+ }
2382
+ const size = launcher.offsetWidth || 56;
2383
+ const bottom = spot && !options.hideLauncher ? Math.round(spot.y * maxLauncherBottom(win.innerHeight, size)) : 0;
2384
+ launcher.style.bottom = bottom ? `${bottom}px` : "";
2385
+ if (bottom < size + 16) shadowHost?.setAttribute("data-g-messenger", `bottom-${side}`);
2386
+ else shadowHost?.removeAttribute("data-g-messenger");
2387
+ const launcherTop = win.innerHeight - 16 - size - bottom;
2388
+ const below = bottom > 0 && launcherTop < 480 && bottom > launcherTop;
2389
+ panel.classList.toggle("g-m-below", below);
2390
+ if (!bottom) {
2391
+ panel.style.removeProperty("--g-m-bottom");
2392
+ panel.style.removeProperty("--g-m-top");
2393
+ panel.style.removeProperty("--g-m-reserve");
2394
+ } else if (below) {
2395
+ const top = launcherTop + size - 12;
2396
+ panel.style.setProperty("--g-m-top", `${top}px`);
2397
+ panel.style.setProperty("--g-m-reserve", `${top + 40}px`);
2398
+ } else {
2399
+ panel.style.setProperty("--g-m-bottom", `${bottom + 76}px`);
2400
+ panel.style.setProperty("--g-m-reserve", `${bottom + 110}px`);
2401
+ }
2402
+ }
2403
+ const stopDrag = enableLauncherDrag({
2404
+ launcher,
2405
+ onMove(next) {
2406
+ spot = next;
2407
+ placeLauncher();
2408
+ options.onLauncherMove?.(next);
2409
+ }
2410
+ });
2411
+ const onResize = () => {
2412
+ if (spot) placeLauncher();
2413
+ };
2414
+ win.addEventListener("resize", onResize);
1338
2415
  panel.setAttribute("role", "dialog");
1339
2416
  panel.setAttribute("aria-label", config.name);
1340
2417
  panel.hidden = true;
@@ -1345,17 +2422,25 @@ function renderMessenger(options) {
1345
2422
  panel.append(head, main, tabs);
1346
2423
  root.append(launcher, panel);
1347
2424
  scope.appendChild(root);
2425
+ placeLauncher();
2426
+ function withRcText(items, rc, apply) {
2427
+ if (!rc?.length) return items;
2428
+ const byId = new Map(rc.map((x) => [x.id, x]));
2429
+ return items.map((i) => byId.has(i.id) ? apply(i, byId.get(i.id)) : i);
2430
+ }
2431
+ 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
2432
  const rcExtras = (ids = []) => ids.map((id) => options.guides?.find((g) => g.id === id || g.key === id)).filter((g) => !!g);
1349
2433
  const help = createHelpView({
1350
2434
  doc,
1351
2435
  data: () => ({
1352
- collections: articles?.collections ?? options.resourceCenter?.collections ?? [],
1353
- articles: articles?.articles ?? options.resourceCenter?.articles ?? [],
2436
+ collections: withRcText(articles?.collections ?? options.resourceCenter?.collections ?? [], options.resourceCenter?.collections, (c, t) => ({ ...c, name: t.name, description: t.description })),
2437
+ articles: withRcText(articles?.articles ?? options.resourceCenter?.articles ?? [], options.resourceCenter?.articles, (a, t) => ({ ...a, title: t.title, summary: t.summary })),
1354
2438
  suggested: helpSuggestions(),
1355
2439
  recommended: rcExtras(options.resourceCenter?.recommendedGuideIds),
1356
2440
  checklists: rcExtras(options.resourceCenter?.checklistGuideIds),
1357
2441
  faqs: options.resourceCenter?.faqs,
1358
- links: options.resourceCenter?.links
2442
+ links: options.resourceCenter?.links,
2443
+ modules: helpModules()
1359
2444
  }),
1360
2445
  search: (q) => api.searchArticles(q),
1361
2446
  audienceIds: options.audienceIds,
@@ -1374,6 +2459,24 @@ function renderMessenger(options) {
1374
2459
  onShowMe: (key) => startWalkthrough(key),
1375
2460
  onArticleChange: () => syncSize()
1376
2461
  });
2462
+ const ideasOn = () => !!config.ideas && !!api.listIdeas;
2463
+ const ideasTab = { kind: "tab", tab: "ideas" };
2464
+ const ideas = createIdeas({
2465
+ doc,
2466
+ api,
2467
+ config: () => config.ideas,
2468
+ identified: () => visitor().identified !== false,
2469
+ openIdea: (id) => go({ kind: "idea", id, from: view.kind === "idea" ? view.from : view }),
2470
+ openForm: () => go({ kind: "idea-new", from: view }),
2471
+ openSent: (idea) => go({ kind: "idea-sent", idea }),
2472
+ openList: () => go(ideasTab),
2473
+ openUpdates: () => go({ kind: "idea-updates", from: view }),
2474
+ onUnreadChange: () => {
2475
+ if (!open || destroyed) return;
2476
+ if (view.kind === "tab" && view.tab === "home") render();
2477
+ else renderTabs();
2478
+ }
2479
+ });
1377
2480
  let docCollapsed = false;
1378
2481
  let sizeButton = null;
1379
2482
  const readingDoc = () => open && (view.kind === "howto" || view.kind === "tab" && view.tab === "help" && help.isArticleOpen());
@@ -1454,7 +2557,7 @@ function renderMessenger(options) {
1454
2557
  options.onStartGuide?.(guideKey);
1455
2558
  };
1456
2559
  function howToItems(q) {
1457
- const list = q ? (howTos?.howTos ?? []).filter((h) => (0, import_runtime5.matchesHelpQuery)(`${h.title} ${h.description}`, q)).slice(0, 10) : [];
2560
+ const list = q ? (howTos?.howTos ?? []).filter((h) => (0, import_runtime6.matchesHelpQuery)(`${h.title} ${h.description}`, q)).slice(0, 10) : [];
1458
2561
  if (!list.length) return [];
1459
2562
  return [
1460
2563
  el(doc, "div", "g-rc-section", `How-tos (${list.length})`),
@@ -1468,7 +2571,7 @@ function renderMessenger(options) {
1468
2571
  const refreshConversations = () => api.listConversations().then((list) => {
1469
2572
  conversations = [...list].sort((a, b) => b.lastMessageAt.localeCompare(a.lastMessageAt));
1470
2573
  updateBadge();
1471
- if (open && view.kind === "tab" && view.tab !== "help") render();
2574
+ if (open && view.kind === "tab" && view.tab !== "help" && view.tab !== "ideas") render();
1472
2575
  }).catch(() => {
1473
2576
  });
1474
2577
  function renderHead(title, back) {
@@ -1502,6 +2605,7 @@ function renderMessenger(options) {
1502
2605
  ["messages", "messages", "Messages"],
1503
2606
  ["help", "help", "Help"]
1504
2607
  ];
2608
+ if (ideasOn()) items.push(["ideas", "idea", config.ideas.title || "Ideas"]);
1505
2609
  const latest = news();
1506
2610
  if (latest.length) items.push(["news", "news", "News"]);
1507
2611
  for (const [tab, ico, label] of items) {
@@ -1512,7 +2616,7 @@ function renderMessenger(options) {
1512
2616
  const i = el(doc, "span", "g-m-tab-icon");
1513
2617
  i.appendChild(icon(doc, ico));
1514
2618
  b.append(i, el(doc, "span", "", label));
1515
- if (tab === "news" && latest.some((n) => n.unread)) {
2619
+ if (tab === "news" && latest.some((n) => n.unread) || tab === "ideas" && ideas.unread() > 0) {
1516
2620
  const dot2 = el(doc, "span", "g-m-tab-dot");
1517
2621
  dot2.setAttribute("aria-label", "New");
1518
2622
  i.appendChild(dot2);
@@ -1718,11 +2822,16 @@ function renderMessenger(options) {
1718
2822
  const body = el(doc, "span", "g-m-news-body");
1719
2823
  const meta = el(doc, "span", "g-m-row g-m-center g-m-news-meta");
1720
2824
  if (n.unread) meta.appendChild(el(doc, "span", "g-m-tag", "New"));
1721
- meta.appendChild(el(doc, "span", "g-m-time", shortDate(n.publishedAt)));
2825
+ meta.appendChild(el(doc, "span", "g-m-time", shortDate2(n.publishedAt)));
1722
2826
  body.append(meta, el(doc, "span", "g-m-card-title", n.title));
1723
2827
  if (n.body) body.appendChild(el(doc, "span", "g-m-card-sub g-m-clamp", n.body));
1724
2828
  card.appendChild(body);
1725
- card.addEventListener("click", () => startGuide(n.key || n.id));
2829
+ card.addEventListener("click", () => {
2830
+ if (n.key) startGuide(n.key);
2831
+ else if (n.articleId) openArticle(n.articleId);
2832
+ else if (n.url && /^https?:\/\//i.test(n.url)) window.open(n.url, "_blank", "noopener,noreferrer");
2833
+ else startGuide(n.id);
2834
+ });
1726
2835
  return card;
1727
2836
  }
1728
2837
  function renderHome() {
@@ -1748,11 +2857,13 @@ function renderMessenger(options) {
1748
2857
  if (filed) inner.appendChild(conversationCard(filed, "Your complaint"));
1749
2858
  const recent = conversations.find((c) => c.id !== filed?.id);
1750
2859
  if (recent) inner.appendChild(conversationCard(recent, recent.status === "closed" && !recent.unread ? "Recent conversation" : "Continue the conversation"));
2860
+ const ideaUpdates = ideasOn() ? ideas.unread() : 0;
2861
+ 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
2862
  if (config.status) inner.appendChild(statusCard(config.status));
1752
2863
  if (cards.progress) progressCards(v).forEach((c) => inner.appendChild(c));
1753
2864
  const suggestions = pageSuggestions(v);
1754
2865
  const questions = cards.suggestions ? (page?.questions ?? []).slice(0, 2) : [];
1755
- const label = v.path ? (0, import_runtime5.pageLabel)(v.path) : null;
2866
+ const label = v.path ? (0, import_runtime6.pageLabel)(v.path) : null;
1756
2867
  const fallback = !suggestions.length && !questions.length ? rcExtras(options.resourceCenter?.recommendedGuideIds).slice(0, 3) : [];
1757
2868
  if (suggestions.length || questions.length || fallback.length) {
1758
2869
  const box = el(doc, "div", "g-m-card g-m-static g-m-suggest");
@@ -1767,6 +2878,10 @@ function renderMessenger(options) {
1767
2878
  }
1768
2879
  const latest = news()[0];
1769
2880
  if (latest) inner.appendChild(newsCard(latest));
2881
+ if (ideasOn()) {
2882
+ const canWrite = !ideas.blocked();
2883
+ 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) }));
2884
+ }
1770
2885
  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
2886
  scroll.append(hero, inner);
1772
2887
  main.appendChild(scroll);
@@ -1867,6 +2982,25 @@ function renderMessenger(options) {
1867
2982
  scroll.appendChild(complaintSent(doc, { complaint: v.complaint, answers: v.answers, onOpenConversation: () => go({ kind: "thread", conversationId: v.conversationId }), onHome: () => go({ kind: "tab", tab: "home" }) }));
1868
2983
  main.appendChild(scroll);
1869
2984
  }
2985
+ function renderIdeasView(v) {
2986
+ const title = config.ideas?.title || "Ideas";
2987
+ if (v.kind === "idea") {
2988
+ renderHead(title, () => go(v.from));
2989
+ main.appendChild(ideas.detailScreen(v.id));
2990
+ } else if (v.kind === "idea-new") {
2991
+ renderHead("Suggest an idea", () => go(v.from));
2992
+ main.appendChild(ideas.formScreen());
2993
+ } else if (v.kind === "idea-sent") {
2994
+ renderHead(title, () => go(ideasTab));
2995
+ main.appendChild(ideas.sentScreen(v.idea));
2996
+ } else if (v.kind === "idea-updates") {
2997
+ renderHead("Updates on your ideas", () => go(v.from));
2998
+ main.appendChild(ideas.updatesScreen());
2999
+ } else {
3000
+ renderHead(title);
3001
+ main.appendChild(ideas.listScreen());
3002
+ }
3003
+ }
1870
3004
  function renderHowToView(id, from) {
1871
3005
  const cached = howToCache.get(id);
1872
3006
  renderHead(cached?.title ?? "How-to", () => go(from));
@@ -1902,7 +3036,7 @@ function renderMessenger(options) {
1902
3036
  const v = visitor();
1903
3037
  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
3038
  if (!items.length) return void 0;
1905
- const label = v.path ? (0, import_runtime5.pageLabel)(v.path) : null;
3039
+ const label = v.path ? (0, import_runtime6.pageLabel)(v.path) : null;
1906
3040
  return { title: label ? `For the ${label} page` : "Suggested for you", items };
1907
3041
  }
1908
3042
  function renderHelpTab() {
@@ -1929,7 +3063,7 @@ function renderMessenger(options) {
1929
3063
  pageKey = null;
1930
3064
  return;
1931
3065
  }
1932
- const key = (0, import_runtime5.foldPathIds)(path).toLowerCase();
3066
+ const key = (0, import_runtime6.foldPathIds)(path).toLowerCase();
1933
3067
  if (!force && key === pageKey && (pageLoading || Date.now() - pageAt < PAGE_TTL_MS)) return;
1934
3068
  if (key !== pageKey) page = null;
1935
3069
  pageKey = key;
@@ -1980,7 +3114,8 @@ function renderMessenger(options) {
1980
3114
  bubble.dataset.messageId = m.id;
1981
3115
  wrap.appendChild(bubble);
1982
3116
  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) {
3117
+ const ideaChip = m.ideaOffer && ideasOn() ? ideas.offerChip(m.ideaOffer, current?.id ?? null, () => icon(doc, "idea", "g-m-ico g-m-chip-ico")) : null;
3118
+ if (m.citations.length || m.suggestedGuides.length || m.howTos?.length || offer || ideaChip) {
1984
3119
  const chips = el(doc, "div", "g-m-chips");
1985
3120
  if (offer) {
1986
3121
  const b = el(doc, "button", "g-m-chip g-m-guide g-m-offer");
@@ -1993,6 +3128,7 @@ function renderMessenger(options) {
1993
3128
  });
1994
3129
  chips.appendChild(b);
1995
3130
  }
3131
+ if (ideaChip) chips.appendChild(ideaChip);
1996
3132
  for (const g of m.suggestedGuides) {
1997
3133
  const b = el(doc, "button", "g-m-chip g-m-guide", `\u25B6 Show me: ${g.name}`);
1998
3134
  b.type = "button";
@@ -2025,7 +3161,7 @@ function renderMessenger(options) {
2025
3161
  if (current?.complaint) log.appendChild(complaintBanner(current.complaint));
2026
3162
  if (!messages.length) {
2027
3163
  const v = visitor();
2028
- const label = v.path ? (0, import_runtime5.pageLabel)(v.path) : null;
3164
+ const label = v.path ? (0, import_runtime6.pageLabel)(v.path) : null;
2029
3165
  if (label && config.aiAvailable && !current) log.appendChild(el(doc, "div", "g-m-context", `Asking from the ${label} page`));
2030
3166
  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
3167
  log.appendChild(messageNode(intro));
@@ -2145,7 +3281,13 @@ function renderMessenger(options) {
2145
3281
  else if (view.kind === "complaint-category") renderComplaintCategories(view);
2146
3282
  else if (view.kind === "complaint-form") renderComplaintForm(view);
2147
3283
  else if (view.kind === "complaint-sent") renderComplaintSent(view);
2148
- else if (view.tab === "home") renderHome();
3284
+ else if (view.kind !== "tab" || view.tab === "ideas") {
3285
+ if (ideasOn()) renderIdeasView(view);
3286
+ else {
3287
+ view = { kind: "tab", tab: "home" };
3288
+ renderHome();
3289
+ }
3290
+ } else if (view.tab === "home") renderHome();
2149
3291
  else if (view.tab === "messages") renderMessagesTab();
2150
3292
  else if (view.tab === "news" && news().length) renderNewsTab();
2151
3293
  else if (view.tab === "news") {
@@ -2296,6 +3438,7 @@ function renderMessenger(options) {
2296
3438
  render();
2297
3439
  void refreshConversations();
2298
3440
  loadHowTos();
3441
+ if (ideasOn()) ideas.loadUpdates();
2299
3442
  if (view.kind === "thread") scheduleThreadPoll();
2300
3443
  options.onOpen?.();
2301
3444
  } else {
@@ -2344,22 +3487,32 @@ function renderMessenger(options) {
2344
3487
  setOpen(true);
2345
3488
  startComplaint(categoryId ?? null);
2346
3489
  },
3490
+ openIdeas(requestId) {
3491
+ if (!ideasOn()) return;
3492
+ setOpen(true);
3493
+ go(requestId ? { kind: "idea", id: requestId, from: ideasTab } : ideasTab);
3494
+ },
2347
3495
  refresh() {
2348
3496
  if (destroyed) return;
3497
+ if (ideasOn()) ideas.sync(open);
2349
3498
  if (!open) {
2350
3499
  pageKey = null;
2351
3500
  return;
2352
3501
  }
2353
3502
  loadPage();
2354
- if (view.kind === "tab") render();
2355
- else if (!messages.length) renderLog();
3503
+ if (view.kind === "tab" && view.tab !== "ideas") render();
3504
+ else if (view.kind === "tab") renderTabs();
3505
+ else if (view.kind === "thread" && !messages.length) renderLog();
2356
3506
  },
2357
3507
  destroy() {
2358
3508
  destroyed = true;
2359
3509
  if (threadTimer) clearTimeout(threadTimer);
2360
3510
  if (backgroundTimer) clearInterval(backgroundTimer);
2361
3511
  panel.removeEventListener("keydown", onKey);
3512
+ stopDrag();
3513
+ win.removeEventListener("resize", onResize);
2362
3514
  help.destroy();
3515
+ ideas.destroy();
2363
3516
  shadowHost?.removeAttribute("data-g-messenger");
2364
3517
  root.remove();
2365
3518
  }
@@ -2367,6 +3520,7 @@ function renderMessenger(options) {
2367
3520
  }
2368
3521
  // Annotate the CommonJS export names for ESM import in node:
2369
3522
  0 && (module.exports = {
3523
+ parseLauncherSpot,
2370
3524
  personalGreeting,
2371
3525
  relativeTime,
2372
3526
  renderMessenger