@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.
package/dist/messenger.js CHANGED
@@ -3,11 +3,11 @@ import {
3
3
  el,
4
4
  resultRow,
5
5
  startableGuide
6
- } from "./chunk-KJRXCITD.js";
7
- import "./chunk-PG7CMBHD.js";
6
+ } from "./chunk-H3CROHPR.js";
7
+ import "./chunk-VJSQM23R.js";
8
8
 
9
9
  // src/messenger.ts
10
- import { foldPathIds, matchesHelpQuery, pageLabel } from "@guidelytech/shared/runtime";
10
+ import { foldPathIds, matchesHelpQuery, orderedModules, pageLabel } from "@guidelytech/shared/runtime";
11
11
 
12
12
  // src/howto-view.ts
13
13
  var pct = (v) => `${(v * 100).toFixed(3)}%`;
@@ -379,6 +379,975 @@ async function readScreenshot(doc, file) {
379
379
  return { fileName: scaled ? `${name.replace(/\.[a-z0-9]+$/i, "")}.jpg` : name, data, previewUrl: dataUrl };
380
380
  }
381
381
 
382
+ // src/ideas-view.ts
383
+ import { FEEDBACK_PRIORITIES, FEEDBACK_PRIORITY_LABEL, FEEDBACK_STATUS_LABEL, MAX_FEEDBACK_COMMENT, MAX_FEEDBACK_DESCRIPTION, MAX_FEEDBACK_NOTE, MAX_FEEDBACK_TITLE, feedbackReference } from "@guidelytech/shared";
384
+
385
+ // src/render-safe.ts
386
+ var CITATION = /\s*\[\[article:[^\]]*\]\]/g;
387
+ var INLINE = /\*\*([^*\n]+)\*\*|`([^`\n]+)`|(!?)\[([^\]\n]{1,200})\]\((https?:\/\/[^\s)]+)\)|(https?:\/\/[^\s<>()"']+[^\s<>().,;:!?'"])/g;
388
+ function stripCitationMarkers(text) {
389
+ return text.replace(CITATION, "");
390
+ }
391
+ function safeHttpUrl(raw) {
392
+ try {
393
+ const u = new URL(raw);
394
+ return u.protocol === "http:" || u.protocol === "https:" ? u.href : null;
395
+ } catch {
396
+ return null;
397
+ }
398
+ }
399
+ function link(doc, label, href) {
400
+ const url = safeHttpUrl(href);
401
+ if (!url) return doc.createTextNode(label);
402
+ const a = doc.createElement("a");
403
+ a.href = url;
404
+ a.target = "_blank";
405
+ a.rel = "noopener noreferrer nofollow";
406
+ a.textContent = label;
407
+ return a;
408
+ }
409
+ function appendInline(doc, parent, text) {
410
+ let last = 0;
411
+ for (const m of text.matchAll(INLINE)) {
412
+ const start = m.index ?? 0;
413
+ if (start > last) parent.appendChild(doc.createTextNode(text.slice(last, start)));
414
+ if (m[1] !== void 0) {
415
+ const b = doc.createElement("strong");
416
+ b.textContent = m[1];
417
+ parent.appendChild(b);
418
+ } else if (m[2] !== void 0) {
419
+ const c = doc.createElement("code");
420
+ c.textContent = m[2];
421
+ parent.appendChild(c);
422
+ } else if (m[4] !== void 0 && m[5] !== void 0) {
423
+ parent.appendChild(m[3] ? doc.createTextNode(m[4]) : link(doc, m[4], m[5]));
424
+ } else if (m[6] !== void 0) {
425
+ parent.appendChild(link(doc, m[6], m[6]));
426
+ }
427
+ last = start + m[0].length;
428
+ }
429
+ if (last < text.length) parent.appendChild(doc.createTextNode(text.slice(last)));
430
+ }
431
+ var BULLET = /^\s*[-*•]\s+/;
432
+ var NUMBERED = /^\s*\d{1,3}[.)]\s+/;
433
+ function renderSafeText(doc, text) {
434
+ const frag = doc.createDocumentFragment();
435
+ const blocks = stripCitationMarkers(text).replace(/\r\n?/g, "\n").trim().split(/\n{2,}/);
436
+ for (const block of blocks) {
437
+ if (!block.trim()) continue;
438
+ const lines = block.split("\n");
439
+ if (lines.every((l) => BULLET.test(l)) || lines.every((l) => NUMBERED.test(l))) {
440
+ const ordered = NUMBERED.test(lines[0]);
441
+ const list = doc.createElement(ordered ? "ol" : "ul");
442
+ for (const line of lines) {
443
+ const li = doc.createElement("li");
444
+ appendInline(doc, li, line.replace(ordered ? NUMBERED : BULLET, ""));
445
+ list.appendChild(li);
446
+ }
447
+ frag.appendChild(list);
448
+ continue;
449
+ }
450
+ const p = doc.createElement("p");
451
+ lines.forEach((line, i) => {
452
+ if (i) p.appendChild(doc.createElement("br"));
453
+ appendInline(doc, p, line);
454
+ });
455
+ frag.appendChild(p);
456
+ }
457
+ return frag;
458
+ }
459
+
460
+ // src/ideas-view.ts
461
+ var SVG_NS = "http://www.w3.org/2000/svg";
462
+ 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"];
463
+ var UP_ICON = ["m6 15 6-6 6 6"];
464
+ function svgIcon(doc, paths, cls = "g-m-ico") {
465
+ const svg = doc.createElementNS(SVG_NS, "svg");
466
+ svg.setAttribute("viewBox", "0 0 24 24");
467
+ svg.setAttribute("aria-hidden", "true");
468
+ svg.setAttribute("class", cls);
469
+ for (const d of paths) {
470
+ const p = doc.createElementNS(SVG_NS, "path");
471
+ p.setAttribute("d", d);
472
+ svg.appendChild(p);
473
+ }
474
+ return svg;
475
+ }
476
+ var plural = (n, one, many = `${one}s`) => `${n} ${n === 1 ? one : many}`;
477
+ function shortDate(iso) {
478
+ const d = new Date(iso);
479
+ if (Number.isNaN(d.getTime())) return "";
480
+ try {
481
+ return d.toLocaleDateString(void 0, { month: "short", day: "numeric" });
482
+ } catch {
483
+ return d.toISOString().slice(0, 10);
484
+ }
485
+ }
486
+ var errorText = (e, fallback) => (e instanceof Error && e.message ? e.message : fallback).replace(/^[\w.]+: /, "");
487
+ function ideaStatusPill(doc, status) {
488
+ return el(doc, "span", `g-m-pill g-m-idea-pill g-m-idea-${status}`, FEEDBACK_STATUS_LABEL[status] ?? status);
489
+ }
490
+ function voteControl(doc, idea, o) {
491
+ const count = el(doc, "span", "g-m-vote-count", String(idea.votes));
492
+ if (!o.canVote) {
493
+ const box = el(doc, "span", "g-m-vote g-m-vote-static");
494
+ box.setAttribute("aria-label", plural(idea.votes, "vote"));
495
+ box.append(svgIcon(doc, UP_ICON, "g-m-ico g-m-vote-ico"), count);
496
+ return box;
497
+ }
498
+ const b = el(doc, "button", `g-m-vote${idea.myVote ? " g-m-voted" : ""}`);
499
+ b.type = "button";
500
+ b.setAttribute("aria-pressed", String(!!idea.myVote));
501
+ b.setAttribute("aria-label", `${idea.myVote ? "Remove your vote for" : "Vote for"} ${idea.title} (${plural(idea.votes, "vote")})`);
502
+ b.disabled = !!o.busy;
503
+ b.append(svgIcon(doc, UP_ICON, "g-m-ico g-m-vote-ico"), count);
504
+ b.addEventListener("click", (e) => {
505
+ e.stopPropagation();
506
+ o.onToggle?.();
507
+ });
508
+ return b;
509
+ }
510
+ function ideaRow(doc, idea, o) {
511
+ const row = el(doc, "div", "g-m-idea");
512
+ row.setAttribute("role", "listitem");
513
+ row.appendChild(voteControl(doc, idea, { canVote: o.canVote, busy: o.busy, onToggle: o.onVote }));
514
+ const open = el(doc, "button", "g-m-idea-open");
515
+ open.type = "button";
516
+ open.appendChild(el(doc, "span", "g-m-idea-title", idea.title));
517
+ const meta = el(doc, "span", "g-m-idea-meta");
518
+ meta.appendChild(ideaStatusPill(doc, idea.status));
519
+ if (idea.comments) meta.appendChild(el(doc, "span", "g-m-time", plural(idea.comments, "comment")));
520
+ if (idea.productArea) meta.appendChild(el(doc, "span", "g-m-time", idea.productArea));
521
+ if (idea.mine) meta.appendChild(el(doc, "span", "g-m-time", "Yours"));
522
+ open.appendChild(meta);
523
+ open.addEventListener("click", o.onOpen);
524
+ row.appendChild(open);
525
+ return row;
526
+ }
527
+ var FILTERS = [
528
+ ["open", "Open"],
529
+ ["all", "All"],
530
+ ["mine", "Mine"],
531
+ ["following", "Following"]
532
+ ];
533
+ var SORTS = [
534
+ ["top", "Top"],
535
+ ["new", "New"],
536
+ ["trending", "Trending"]
537
+ ];
538
+ function filterChips(doc, current, onPick) {
539
+ const wrap = el(doc, "div", "g-m-filters");
540
+ wrap.setAttribute("role", "group");
541
+ wrap.setAttribute("aria-label", "Show");
542
+ for (const [f, label] of FILTERS) {
543
+ const b = el(doc, "button", "g-m-filter", label);
544
+ b.type = "button";
545
+ b.dataset.filter = f;
546
+ b.setAttribute("aria-pressed", String(f === current));
547
+ b.addEventListener("click", () => {
548
+ wrap.querySelectorAll("button").forEach((x) => x.setAttribute("aria-pressed", String(x === b)));
549
+ onPick(f);
550
+ });
551
+ wrap.appendChild(b);
552
+ }
553
+ return wrap;
554
+ }
555
+ function sortSelect(doc, current, onChange) {
556
+ const select = el(doc, "select", "g-input g-m-sort");
557
+ select.setAttribute("aria-label", "Sort ideas");
558
+ for (const [s, label] of SORTS) {
559
+ const opt = el(doc, "option", "", label);
560
+ opt.value = s;
561
+ select.appendChild(opt);
562
+ }
563
+ select.value = current;
564
+ select.addEventListener("change", () => onChange(select.value));
565
+ return select;
566
+ }
567
+ function priorityChoice(doc, current, onPick, label = "How important is this to you?") {
568
+ const wrap = el(doc, "div", "g-m-field");
569
+ const id = `g-m-prio${++fieldSeq2}`;
570
+ const l = el(doc, "span", "g-m-label", label);
571
+ l.id = id;
572
+ const group = el(doc, "div", "g-m-seg");
573
+ group.setAttribute("role", "radiogroup");
574
+ group.setAttribute("aria-labelledby", id);
575
+ for (const p of FEEDBACK_PRIORITIES) {
576
+ const b = el(doc, "button", "g-m-seg-btn", FEEDBACK_PRIORITY_LABEL[p]);
577
+ b.type = "button";
578
+ b.dataset.priority = p;
579
+ b.setAttribute("role", "radio");
580
+ b.setAttribute("aria-checked", String(p === current));
581
+ b.addEventListener("click", () => {
582
+ group.querySelectorAll("button").forEach((x) => x.setAttribute("aria-checked", String(x === b)));
583
+ onPick(p);
584
+ });
585
+ group.appendChild(b);
586
+ }
587
+ wrap.append(l, group);
588
+ return wrap;
589
+ }
590
+ var fieldSeq2 = 0;
591
+ function field2(doc, label, control, opts = {}) {
592
+ const id = `g-m-i${++fieldSeq2}`;
593
+ control.id = id;
594
+ const wrap = el(doc, "div", "g-m-field");
595
+ const l = el(doc, "label", "g-m-label", label);
596
+ l.htmlFor = id;
597
+ if (opts.optional) l.appendChild(el(doc, "span", "g-m-optional", " (optional)"));
598
+ if (opts.invalid) control.setAttribute("aria-invalid", "true");
599
+ wrap.append(l, control);
600
+ return wrap;
601
+ }
602
+ function errorLine(doc, text) {
603
+ const p = el(doc, "p", "g-m-error", text);
604
+ p.setAttribute("role", "alert");
605
+ return p;
606
+ }
607
+ function button(doc, cls, text, onClick) {
608
+ const b = el(doc, "button", cls, text);
609
+ b.type = "button";
610
+ b.addEventListener("click", onClick);
611
+ return b;
612
+ }
613
+ function writeNotice(doc, reason, onPortal) {
614
+ const box = el(doc, "div", "g-m-idea-notice");
615
+ box.setAttribute("role", "note");
616
+ box.appendChild(el(doc, "span", "", reason));
617
+ if (onPortal) box.appendChild(button(doc, "g-m-text-btn", "Open the feedback portal", onPortal));
618
+ return box;
619
+ }
620
+ function comment(doc, c) {
621
+ const li = el(doc, "li", `g-m-comment${c.authorType === "team" ? " g-m-comment-team" : ""}`);
622
+ const who = el(doc, "div", "g-m-row g-m-center g-m-comment-meta");
623
+ who.appendChild(el(doc, "span", "g-m-comment-author", c.authorName || (c.authorType === "team" ? "Team" : "Someone")));
624
+ if (c.authorType === "team") who.appendChild(el(doc, "span", "g-m-ai", "Team"));
625
+ who.appendChild(el(doc, "span", "g-m-time", shortDate(c.createdAt)));
626
+ const body = el(doc, "div", "g-m-comment-body");
627
+ body.appendChild(renderSafeText(doc, c.body));
628
+ li.append(who, body);
629
+ return li;
630
+ }
631
+ function ideaDetail(doc, o) {
632
+ const { idea } = o;
633
+ const wrap = el(doc, "div", "g-m-form g-m-idea-detail");
634
+ const meta = el(doc, "div", "g-m-row g-m-center g-m-idea-meta");
635
+ meta.append(el(doc, "span", "g-m-time", feedbackReference(idea.number)), ideaStatusPill(doc, idea.status));
636
+ if (idea.productArea) meta.appendChild(el(doc, "span", "g-m-time", idea.productArea));
637
+ wrap.append(meta, el(doc, "h3", "g-m-form-title", idea.title));
638
+ if (idea.description.trim()) {
639
+ const d = el(doc, "div", "g-m-idea-desc");
640
+ d.appendChild(renderSafeText(doc, idea.description));
641
+ wrap.appendChild(d);
642
+ }
643
+ if (idea.statusNote.trim()) {
644
+ const note = el(doc, "div", "g-m-idea-note");
645
+ note.appendChild(el(doc, "span", "g-m-card-label", `${FEEDBACK_STATUS_LABEL[idea.status] ?? idea.status} \xB7 from the team`));
646
+ note.appendChild(renderSafeText(doc, idea.statusNote));
647
+ wrap.appendChild(note);
648
+ }
649
+ const panel = el(doc, "div", "g-m-card g-m-static g-m-idea-vote");
650
+ const top = el(doc, "div", "g-m-row g-m-center");
651
+ top.appendChild(voteControl(doc, idea, { canVote: false }));
652
+ const said = el(doc, "span", "g-m-grow");
653
+ said.appendChild(el(doc, "span", "g-m-card-title", plural(idea.votes, "vote")));
654
+ if (idea.myVote) said.appendChild(el(doc, "span", "g-m-card-sub", `You voted \xB7 ${FEEDBACK_PRIORITY_LABEL[idea.myVote.priority]}`));
655
+ top.appendChild(said);
656
+ if (!o.blocked) {
657
+ const follow = button(doc, `g-m-follow${idea.following ? " g-m-following" : ""}`, idea.following ? "Following" : "Follow", () => o.onFollow(!idea.following));
658
+ follow.setAttribute("aria-pressed", String(idea.following));
659
+ follow.disabled = o.busy;
660
+ follow.title = idea.following ? "Stop getting updates about this idea" : "Get updates here when it changes";
661
+ top.appendChild(follow);
662
+ }
663
+ panel.appendChild(top);
664
+ if (o.error) panel.appendChild(errorLine(doc, o.error));
665
+ if (o.blocked) panel.appendChild(writeNotice(doc, o.blocked, o.onPortal));
666
+ else if (idea.myVote) {
667
+ panel.appendChild(priorityChoice(doc, idea.myVote.priority, (p) => p !== idea.myVote?.priority && o.onVote(p, ""), "How much do you need it?"));
668
+ const remove = button(doc, "g-m-text-btn g-m-unvote", "Remove my vote", o.onUnvote);
669
+ remove.disabled = o.busy;
670
+ panel.appendChild(remove);
671
+ } else {
672
+ const vote = o.vote;
673
+ panel.appendChild(priorityChoice(doc, vote.priority, (p) => vote.priority = p));
674
+ const note = el(doc, "textarea", "g-input g-m-idea-why");
675
+ note.rows = 2;
676
+ note.maxLength = MAX_FEEDBACK_NOTE;
677
+ note.value = vote.note;
678
+ note.placeholder = "What would it let you do?";
679
+ note.addEventListener("input", () => vote.note = note.value);
680
+ panel.appendChild(field2(doc, "Why it matters", note, { optional: true }));
681
+ 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()));
682
+ go.disabled = o.busy;
683
+ panel.appendChild(go);
684
+ }
685
+ wrap.appendChild(panel);
686
+ const comments = el(doc, "div", "g-m-comments");
687
+ comments.appendChild(el(doc, "h4", "g-m-comments-title", idea.thread.length ? `Comments (${idea.thread.length})` : "Comments"));
688
+ if (idea.thread.length) {
689
+ const list = el(doc, "ul", "g-m-comment-list");
690
+ idea.thread.forEach((c) => list.appendChild(comment(doc, c)));
691
+ comments.appendChild(list);
692
+ } else comments.appendChild(el(doc, "p", "g-m-hint", "No comments yet."));
693
+ if (!o.blocked) {
694
+ const form = el(doc, "form", "g-m-comment-form");
695
+ form.noValidate = true;
696
+ const box = el(doc, "textarea", "g-input");
697
+ box.rows = 2;
698
+ box.maxLength = MAX_FEEDBACK_COMMENT;
699
+ box.placeholder = "Add a comment\u2026";
700
+ box.setAttribute("aria-label", "Comment");
701
+ box.value = o.commentDraft;
702
+ const post = el(doc, "button", "g-btn g-btn-secondary", "Post");
703
+ post.type = "submit";
704
+ post.disabled = o.busy || !o.commentDraft.trim();
705
+ box.addEventListener("input", () => {
706
+ o.onCommentInput(box.value);
707
+ post.disabled = o.busy || !box.value.trim();
708
+ });
709
+ form.addEventListener("submit", (e) => {
710
+ e.preventDefault();
711
+ if (box.value.trim() && !o.busy) o.onComment();
712
+ });
713
+ form.append(box, post);
714
+ comments.appendChild(form);
715
+ }
716
+ wrap.appendChild(comments);
717
+ return wrap;
718
+ }
719
+ var emptyIdeaDraft = (over = {}) => ({ title: "", description: "", productArea: "", priority: "nice_to_have", conversationId: null, similar: null, similarFor: "", error: null, sending: false, checked: false, ...over });
720
+ function ideaProblem(draft) {
721
+ return draft.title.trim().length < 3 ? "Give your idea a short title." : null;
722
+ }
723
+ function ideaForm(doc, o) {
724
+ const { draft } = o;
725
+ const form = el(doc, "form", "g-m-form g-m-idea-form");
726
+ form.noValidate = true;
727
+ const title = el(doc, "input", "g-input");
728
+ title.type = "text";
729
+ title.maxLength = MAX_FEEDBACK_TITLE;
730
+ title.required = true;
731
+ title.value = draft.title;
732
+ title.placeholder = "Your idea in a few words";
733
+ title.addEventListener("input", () => {
734
+ draft.title = title.value;
735
+ o.onTitleInput();
736
+ });
737
+ form.appendChild(field2(doc, "Idea", title, { invalid: draft.checked && !!ideaProblem(draft) }));
738
+ const similar = el(doc, "div", "g-m-similar");
739
+ similar.setAttribute("aria-live", "polite");
740
+ form.appendChild(similar);
741
+ const desc = el(doc, "textarea", "g-input g-m-textarea");
742
+ desc.rows = 4;
743
+ desc.maxLength = MAX_FEEDBACK_DESCRIPTION;
744
+ desc.value = draft.description;
745
+ desc.placeholder = "What are you trying to do, and what gets in the way?";
746
+ desc.addEventListener("input", () => draft.description = desc.value);
747
+ form.appendChild(field2(doc, "Details", desc, { optional: true }));
748
+ if (o.productAreas.length) {
749
+ const select = el(doc, "select", "g-input");
750
+ const opt = (label, value) => {
751
+ const x = el(doc, "option", "", label);
752
+ x.value = value;
753
+ return x;
754
+ };
755
+ select.appendChild(opt("Choose one (optional)", ""));
756
+ o.productAreas.forEach((a) => select.appendChild(opt(a, a)));
757
+ select.value = o.productAreas.includes(draft.productArea) ? draft.productArea : "";
758
+ select.addEventListener("change", () => draft.productArea = select.value);
759
+ form.appendChild(field2(doc, "Product area", select, { optional: true }));
760
+ }
761
+ form.appendChild(priorityChoice(doc, draft.priority, (p) => draft.priority = p));
762
+ const foot = el(doc, "div", "g-m-form-foot");
763
+ if (draft.error) foot.appendChild(errorLine(doc, draft.error));
764
+ const submit = el(doc, "button", "g-btn g-btn-primary g-m-wide", draft.sending ? "Sending\u2026" : "Submit idea");
765
+ submit.type = "submit";
766
+ submit.disabled = draft.sending;
767
+ foot.appendChild(submit);
768
+ form.appendChild(foot);
769
+ form.addEventListener("submit", (e) => {
770
+ e.preventDefault();
771
+ if (!draft.sending) o.onSubmit();
772
+ });
773
+ return { form, similar };
774
+ }
775
+ function similarIdeas(doc, ideas, row) {
776
+ if (!ideas.length) return null;
777
+ const box = el(doc, "div", "g-m-similar-box");
778
+ box.appendChild(el(doc, "div", "g-m-card-label", "Similar ideas \u2014 vote instead?"));
779
+ const list = el(doc, "div", "g-m-idea-list");
780
+ list.setAttribute("role", "list");
781
+ ideas.slice(0, 5).forEach((i) => list.appendChild(row(i)));
782
+ box.appendChild(list);
783
+ return box;
784
+ }
785
+ function ideaSent(doc, o) {
786
+ const wrap = el(doc, "div", "g-m-form g-m-sent");
787
+ const top = el(doc, "div", "g-m-sent-top");
788
+ const tick = el(doc, "span", "g-m-sent-tick", "\u2713");
789
+ tick.setAttribute("aria-hidden", "true");
790
+ const p = el(doc, "p", "g-m-card-sub");
791
+ p.append(el(doc, "strong", "", `${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.");
792
+ top.append(tick, el(doc, "h3", "g-m-form-title", "Thanks for your idea"), p);
793
+ 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));
794
+ return wrap;
795
+ }
796
+ function updatesList(doc, updates, onOpen) {
797
+ const wrap = el(doc, "div", "g-m-idea-updates");
798
+ if (!updates.length) {
799
+ wrap.appendChild(el(doc, "div", "g-m-empty", "No updates yet. Follow or vote for an idea to hear when it changes."));
800
+ return wrap;
801
+ }
802
+ for (const u of updates) {
803
+ const card = el(doc, "button", "g-m-card g-m-update");
804
+ card.type = "button";
805
+ card.disabled = !u.requestId;
806
+ const row = el(doc, "span", "g-m-row");
807
+ if (!u.read) row.appendChild(el(doc, "span", "g-m-dot"));
808
+ const text = el(doc, "span", "g-m-grow");
809
+ text.appendChild(el(doc, "span", "g-m-card-title", u.title));
810
+ if (u.body) text.appendChild(el(doc, "span", "g-m-card-sub g-m-clamp", u.body));
811
+ row.append(text, el(doc, "span", "g-m-time", shortDate(u.createdAt)));
812
+ card.appendChild(row);
813
+ card.setAttribute("aria-label", `${u.title}${u.read ? "" : ", new"}`);
814
+ card.addEventListener("click", () => onOpen(u));
815
+ wrap.appendChild(card);
816
+ }
817
+ return wrap;
818
+ }
819
+ var LIST_TTL_MS = 15e3;
820
+ var asDetail = (d) => ({ ...d, title: d.title ?? "", description: d.description ?? "", statusNote: d.statusNote ?? "", votes: d.votes ?? 0, thread: Array.isArray(d.thread) ? d.thread : [] });
821
+ function createIdeas(o) {
822
+ const { doc, api } = o;
823
+ let query = { sort: "top", filter: "open", page: 1 };
824
+ let list = null;
825
+ let hasMore = false;
826
+ let listAt = 0;
827
+ let listLoading = false;
828
+ let listError = null;
829
+ let listSeq = 0;
830
+ let listActionError = null;
831
+ const details = /* @__PURE__ */ new Map();
832
+ const detailErrors = /* @__PURE__ */ new Map();
833
+ const actionErrors = /* @__PURE__ */ new Map();
834
+ const voteDrafts = /* @__PURE__ */ new Map();
835
+ const commentDrafts = /* @__PURE__ */ new Map();
836
+ const busy = /* @__PURE__ */ new Set();
837
+ const offers = /* @__PURE__ */ new Map();
838
+ let updates = null;
839
+ let updatesAt = 0;
840
+ let updatesLoading = false;
841
+ let updatesSeq = 0;
842
+ let draft = emptyIdeaDraft();
843
+ let similarTimer = null;
844
+ let similarSeq = 0;
845
+ let searchTimer = null;
846
+ let who = o.identified();
847
+ let mounted = null;
848
+ const mount = (root, paint) => {
849
+ mounted = { root, paint };
850
+ paint();
851
+ };
852
+ const repaint = () => {
853
+ if (mounted?.root.isConnected) mounted.paint();
854
+ };
855
+ const showing = (root) => mounted?.root === root && root.isConnected;
856
+ const cfg = () => o.config() ?? { title: "Ideas", anonymous: false, readOnly: true, productAreas: [], portal: false };
857
+ function blocked() {
858
+ const c = cfg();
859
+ 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.";
860
+ if (!c.anonymous && !o.identified()) return "Log in to vote, comment or share an idea.";
861
+ return null;
862
+ }
863
+ const canPortal = () => cfg().portal && !!api.ideaPortalLink;
864
+ function openPortal() {
865
+ if (!api.ideaPortalLink) return;
866
+ const win = doc.defaultView;
867
+ let tab = null;
868
+ try {
869
+ tab = win?.open("about:blank", "_blank") ?? null;
870
+ if (tab) tab.opener = null;
871
+ } catch {
872
+ tab = null;
873
+ }
874
+ api.ideaPortalLink().then((raw) => {
875
+ const url = safeHttpUrl(raw);
876
+ if (!url) throw new Error("The portal link isn't available.");
877
+ if (tab) tab.location.href = url;
878
+ else win?.open(url, "_blank", "noopener");
879
+ }).catch((e) => {
880
+ tab?.close();
881
+ listActionError = errorText(e, "The feedback portal couldn't be opened.");
882
+ repaint();
883
+ });
884
+ }
885
+ const portal = () => canPortal() ? openPortal : void 0;
886
+ function patch(idea) {
887
+ if (list) list = list.map((x) => x.id === idea.id ? { ...x, ...idea } : x);
888
+ const d = details.get(idea.id);
889
+ if (d) details.set(idea.id, { ...d, ...idea, thread: "thread" in idea ? idea.thread : d.thread });
890
+ if (draft.similar) draft.similar = draft.similar.map((x) => x.id === idea.id ? { ...x, ...idea } : x);
891
+ }
892
+ const find = (id) => details.get(id) ?? list?.find((x) => x.id === id) ?? draft.similar?.find((x) => x.id === id);
893
+ function toggleVote(idea) {
894
+ if (busy.has(idea.id) || blocked() || !api.voteIdea || idea.myVote && !api.unvoteIdea) return;
895
+ busy.add(idea.id);
896
+ listActionError = null;
897
+ const voted = !!idea.myVote;
898
+ patch({ ...idea, votes: Math.max(0, idea.votes + (voted ? -1 : 1)), myVote: voted ? null : { priority: "nice_to_have" } });
899
+ repaint();
900
+ (voted ? api.unvoteIdea(idea.id) : api.voteIdea(idea.id, { priority: "nice_to_have" })).then(patch).catch((e) => {
901
+ patch(idea);
902
+ listActionError = errorText(e, "Your vote couldn't be saved.");
903
+ }).finally(() => {
904
+ busy.delete(idea.id);
905
+ repaint();
906
+ });
907
+ }
908
+ function loadList(append = false) {
909
+ if (!api.listIdeas) return;
910
+ const seq = ++listSeq;
911
+ listLoading = true;
912
+ listError = null;
913
+ const q = { ...query };
914
+ api.listIdeas(q).then((res) => {
915
+ if (seq !== listSeq) return;
916
+ const got = Array.isArray(res?.requests) ? res.requests : [];
917
+ list = append && list ? [...list, ...got.filter((r) => !list.some((x) => x.id === r.id))] : got;
918
+ hasMore = !!res?.hasMore;
919
+ listAt = Date.now();
920
+ }).catch((e) => {
921
+ if (seq === listSeq) listError = errorText(e, "Ideas couldn't be loaded.");
922
+ }).finally(() => {
923
+ if (seq !== listSeq) return;
924
+ listLoading = false;
925
+ repaint();
926
+ });
927
+ }
928
+ const requery = (change) => {
929
+ query = { ...query, ...change, page: 1 };
930
+ loadList();
931
+ repaint();
932
+ };
933
+ function listScreen() {
934
+ const root = el(doc, "div", "g-m-scroll g-m-ideas");
935
+ const top = el(doc, "div", "g-m-ideas-top");
936
+ const why = blocked();
937
+ if (why) top.appendChild(writeNotice(doc, why, portal()));
938
+ else top.appendChild(button(doc, "g-btn g-btn-primary g-m-wide", "Suggest an idea", o.openForm));
939
+ const updatesLink = el(doc, "div");
940
+ top.appendChild(updatesLink);
941
+ const search = el(doc, "input", "g-input g-m-idea-search");
942
+ search.type = "search";
943
+ search.placeholder = "Search ideas";
944
+ search.setAttribute("aria-label", "Search ideas");
945
+ search.maxLength = 200;
946
+ search.value = query.q ?? "";
947
+ search.addEventListener("input", () => {
948
+ if (searchTimer) clearTimeout(searchTimer);
949
+ searchTimer = setTimeout(() => requery({ q: search.value.trim() || void 0 }), 300);
950
+ });
951
+ const tools = el(doc, "div", "g-m-row g-m-center g-m-idea-tools");
952
+ tools.append(filterChips(doc, query.filter, (filter) => requery({ filter })), sortSelect(doc, query.sort, (sort) => requery({ sort })));
953
+ top.append(search, tools);
954
+ const results = el(doc, "div", "g-m-idea-results");
955
+ root.append(top, results);
956
+ mount(root, () => {
957
+ updatesLink.textContent = "";
958
+ const n = updates?.unread ?? 0;
959
+ 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));
960
+ results.textContent = "";
961
+ if (listActionError) results.appendChild(errorLine(doc, listActionError));
962
+ if (!list) {
963
+ if (listError) {
964
+ results.append(errorLine(doc, listError), button(doc, "g-m-text-btn", "Try again", () => loadList()));
965
+ } else results.appendChild(el(doc, "div", "g-m-empty", "Loading ideas\u2026"));
966
+ return;
967
+ }
968
+ if (!list.length) {
969
+ 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.";
970
+ results.appendChild(el(doc, "div", "g-m-empty", empty));
971
+ } else {
972
+ const rows = el(doc, "div", "g-m-idea-list");
973
+ rows.setAttribute("role", "list");
974
+ rows.setAttribute("aria-busy", String(listLoading));
975
+ const canVote = !blocked();
976
+ list.forEach((idea) => rows.appendChild(ideaRow(doc, idea, { canVote, busy: busy.has(idea.id), onVote: () => toggleVote(idea), onOpen: () => o.openIdea(idea.id) })));
977
+ results.appendChild(rows);
978
+ }
979
+ if (listError) results.appendChild(errorLine(doc, listError));
980
+ if (hasMore) {
981
+ const more = button(doc, "g-btn g-btn-secondary g-m-wide", listLoading ? "Loading\u2026" : "Show more", () => {
982
+ query = { ...query, page: query.page + 1 };
983
+ loadList(true);
984
+ repaint();
985
+ });
986
+ more.disabled = listLoading;
987
+ results.appendChild(more);
988
+ }
989
+ if (!why && canPortal()) results.appendChild(button(doc, "g-m-text-btn g-m-portal-link", "Open the feedback portal", openPortal));
990
+ });
991
+ if (!list || Date.now() - listAt > LIST_TTL_MS || query.page > 1) {
992
+ query = { ...query, page: 1 };
993
+ loadList();
994
+ }
995
+ return root;
996
+ }
997
+ function act(id, call, fallback, done) {
998
+ if (busy.has(id)) return;
999
+ const p = call();
1000
+ if (!p) return;
1001
+ busy.add(id);
1002
+ actionErrors.delete(id);
1003
+ repaint();
1004
+ p.then((res) => {
1005
+ patch(res);
1006
+ done?.();
1007
+ }).catch((e) => actionErrors.set(id, errorText(e, fallback))).finally(() => {
1008
+ busy.delete(id);
1009
+ repaint();
1010
+ });
1011
+ }
1012
+ function detailScreen(id) {
1013
+ const root = el(doc, "div", "g-m-scroll");
1014
+ mount(root, () => {
1015
+ root.textContent = "";
1016
+ const idea = details.get(id);
1017
+ if (!idea) {
1018
+ const err = detailErrors.get(id);
1019
+ root.appendChild(err ? errorLine(doc, err) : el(doc, "div", "g-m-empty", "Loading\u2026"));
1020
+ return;
1021
+ }
1022
+ if (!voteDrafts.has(id)) voteDrafts.set(id, { priority: "nice_to_have", note: "" });
1023
+ root.appendChild(
1024
+ ideaDetail(doc, {
1025
+ idea,
1026
+ blocked: blocked(),
1027
+ onPortal: portal(),
1028
+ busy: busy.has(id),
1029
+ error: actionErrors.get(id) ?? null,
1030
+ vote: voteDrafts.get(id),
1031
+ commentDraft: commentDrafts.get(id) ?? "",
1032
+ onVote: (priority, note) => act(id, () => api.voteIdea?.(id, { priority, ...note ? { note } : {} }), "Your vote couldn't be saved.", () => voteDrafts.delete(id)),
1033
+ onUnvote: () => act(id, () => api.unvoteIdea?.(id), "Your vote couldn't be removed."),
1034
+ onFollow: (follow) => act(id, () => api.followIdea?.(id, follow), "That didn't work. Please try again."),
1035
+ onCommentInput: (text) => commentDrafts.set(id, text),
1036
+ onComment: () => act(id, () => api.commentIdea?.(id, (commentDrafts.get(id) ?? "").trim()), "Your comment couldn't be posted.", () => commentDrafts.delete(id))
1037
+ })
1038
+ );
1039
+ });
1040
+ if (api.getIdea) {
1041
+ detailErrors.delete(id);
1042
+ api.getIdea(id).then((d) => {
1043
+ details.set(id, asDetail(d));
1044
+ patch(d);
1045
+ }).catch((e) => {
1046
+ if (!details.has(id)) detailErrors.set(id, errorText(e, "This idea couldn't be loaded."));
1047
+ }).finally(() => {
1048
+ if (showing(root)) mounted.paint();
1049
+ });
1050
+ }
1051
+ return root;
1052
+ }
1053
+ function lookUpSimilar(paint) {
1054
+ const title = draft.title.trim();
1055
+ if (title.length < 3 || !api.similarIdeas) {
1056
+ draft.similar = null;
1057
+ draft.similarFor = "";
1058
+ return paint();
1059
+ }
1060
+ if (title === draft.similarFor) return;
1061
+ const seq = ++similarSeq;
1062
+ api.similarIdeas(title).then((res) => {
1063
+ if (seq !== similarSeq) return;
1064
+ draft.similar = res;
1065
+ draft.similarFor = title;
1066
+ paint();
1067
+ }).catch(() => {
1068
+ });
1069
+ }
1070
+ function formScreen() {
1071
+ const root = el(doc, "div", "g-m-scroll");
1072
+ let similarBox = null;
1073
+ const paintSimilar = () => {
1074
+ if (!similarBox) return;
1075
+ similarBox.textContent = "";
1076
+ const canVote = !blocked();
1077
+ const box = similarIdeas(doc, draft.similar ?? [], (idea) => ideaRow(doc, idea, { canVote, busy: busy.has(idea.id), onVote: () => toggleVote(idea), onOpen: () => o.openIdea(idea.id) }));
1078
+ if (box) similarBox.appendChild(box);
1079
+ if (listActionError) similarBox.appendChild(errorLine(doc, listActionError));
1080
+ };
1081
+ const paintAll = () => {
1082
+ root.textContent = "";
1083
+ const why = blocked();
1084
+ if (why) {
1085
+ root.appendChild(writeNotice(doc, why, portal()));
1086
+ similarBox = null;
1087
+ return;
1088
+ }
1089
+ const { form, similar } = ideaForm(doc, {
1090
+ draft,
1091
+ productAreas: cfg().productAreas ?? [],
1092
+ onTitleInput: () => {
1093
+ if (similarTimer) clearTimeout(similarTimer);
1094
+ similarTimer = setTimeout(() => {
1095
+ if (showing(root)) lookUpSimilar(paintSimilar);
1096
+ }, 300);
1097
+ },
1098
+ onSubmit: () => submit(root, paintAll)
1099
+ });
1100
+ root.appendChild(form);
1101
+ similarBox = similar;
1102
+ paintSimilar();
1103
+ };
1104
+ mounted = { root, paint: paintSimilar };
1105
+ paintAll();
1106
+ if (draft.title.trim() && draft.title.trim() !== draft.similarFor) lookUpSimilar(paintSimilar);
1107
+ return root;
1108
+ }
1109
+ function submit(root, paintAll) {
1110
+ const d = draft;
1111
+ d.checked = true;
1112
+ d.error = ideaProblem(d);
1113
+ if (d.error || !api.submitIdea) return paintAll();
1114
+ d.sending = true;
1115
+ paintAll();
1116
+ api.submitIdea({ title: d.title.trim(), description: d.description.trim(), productArea: d.productArea || null, priority: d.priority, conversationId: d.conversationId }).then((idea) => {
1117
+ details.set(idea.id, asDetail(idea));
1118
+ listAt = 0;
1119
+ if (draft === d) draft = emptyIdeaDraft();
1120
+ if (showing(root)) o.openSent(idea);
1121
+ }).catch((e) => {
1122
+ d.sending = false;
1123
+ d.error = errorText(e, "Your idea couldn't be sent.");
1124
+ if (showing(root)) paintAll();
1125
+ });
1126
+ }
1127
+ function updatesScreen() {
1128
+ const root = el(doc, "div", "g-m-scroll g-m-idea-updates-screen");
1129
+ mount(root, () => {
1130
+ root.textContent = "";
1131
+ if (!updates) {
1132
+ root.appendChild(el(doc, "div", "g-m-empty", updatesLoading || api.ideaUpdates ? "Loading\u2026" : "No updates yet."));
1133
+ return;
1134
+ }
1135
+ root.appendChild(updatesList(doc, updates.updates, (u) => u.requestId && o.openIdea(u.requestId)));
1136
+ });
1137
+ if (!updates) loadUpdates();
1138
+ else markRead();
1139
+ return root;
1140
+ }
1141
+ function markRead() {
1142
+ const ids = updates?.updates.filter((u) => !u.read).map((u) => u.id) ?? [];
1143
+ if (!ids.length || !api.markIdeaUpdatesRead) return;
1144
+ const seq = updatesSeq;
1145
+ api.markIdeaUpdatesRead(ids).then((res) => {
1146
+ if (seq !== updatesSeq || !updates) return;
1147
+ updates = { updates: updates.updates, unread: res.unread };
1148
+ o.onUnreadChange();
1149
+ }).catch(() => {
1150
+ });
1151
+ }
1152
+ function loadUpdates(maxAgeMs = 0) {
1153
+ if (!api.ideaUpdates || updatesLoading || Date.now() - updatesAt < maxAgeMs) return;
1154
+ updatesLoading = true;
1155
+ const seq = ++updatesSeq;
1156
+ api.ideaUpdates().then((res) => {
1157
+ if (seq !== updatesSeq) return;
1158
+ const before = updates?.unread ?? 0;
1159
+ updates = { updates: Array.isArray(res?.updates) ? res.updates : [], unread: Number(res?.unread) || 0 };
1160
+ updatesAt = Date.now();
1161
+ if (updates.unread !== before) o.onUnreadChange();
1162
+ if (mounted?.root.isConnected) {
1163
+ mounted.paint();
1164
+ if (mounted.root.classList.contains("g-m-idea-updates-screen")) markRead();
1165
+ }
1166
+ }).catch(() => {
1167
+ }).finally(() => {
1168
+ if (seq === updatesSeq) updatesLoading = false;
1169
+ });
1170
+ }
1171
+ function offerChip(offer, conversationId, lead) {
1172
+ const canWrite = !blocked();
1173
+ const b = el(doc, "button", "g-m-chip g-m-guide g-m-offer g-m-idea-offer");
1174
+ b.type = "button";
1175
+ const label = el(doc, "span");
1176
+ b.append(lead(), label);
1177
+ const id = offer.requestId;
1178
+ if (!id) {
1179
+ if (!canWrite || !api.submitIdea) return null;
1180
+ label.textContent = "Suggest this idea";
1181
+ b.addEventListener("click", () => {
1182
+ startDraft({ title: offer.title, description: offer.description, conversationId });
1183
+ o.openForm();
1184
+ });
1185
+ return b;
1186
+ }
1187
+ const paint = () => {
1188
+ const state = offers.get(id) ?? (find(id)?.myVote ? "voted" : void 0);
1189
+ b.disabled = state === "voting";
1190
+ 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`;
1191
+ return state;
1192
+ };
1193
+ paint();
1194
+ b.addEventListener("click", () => {
1195
+ const state = paint();
1196
+ if (!canWrite || state === "voted" || state === "failed" || !api.voteIdea) return o.openIdea(id);
1197
+ if (state === "voting") return;
1198
+ offers.set(id, "voting");
1199
+ paint();
1200
+ api.voteIdea(id, { priority: "nice_to_have", conversationId }).then((idea) => {
1201
+ patch(idea);
1202
+ offers.set(id, "voted");
1203
+ }).catch(() => offers.set(id, "failed")).finally(paint);
1204
+ });
1205
+ return b;
1206
+ }
1207
+ function startDraft(prefill = {}) {
1208
+ draft = emptyIdeaDraft({ title: (prefill.title ?? "").slice(0, MAX_FEEDBACK_TITLE), description: (prefill.description ?? "").slice(0, MAX_FEEDBACK_DESCRIPTION), conversationId: prefill.conversationId ?? null });
1209
+ }
1210
+ return {
1211
+ blocked,
1212
+ unread: () => updates?.unread ?? 0,
1213
+ listScreen,
1214
+ detailScreen,
1215
+ formScreen,
1216
+ sentScreen: (idea) => {
1217
+ mounted = null;
1218
+ const root = el(doc, "div", "g-m-scroll");
1219
+ root.appendChild(ideaSent(doc, { idea, onView: () => o.openIdea(idea.id), onBack: o.openList }));
1220
+ return root;
1221
+ },
1222
+ updatesScreen,
1223
+ startDraft,
1224
+ offerChip,
1225
+ loadUpdates,
1226
+ sync(open) {
1227
+ const now = o.identified();
1228
+ if (now !== who) {
1229
+ who = now;
1230
+ list = null;
1231
+ listAt = 0;
1232
+ details.clear();
1233
+ offers.clear();
1234
+ updates = null;
1235
+ updatesAt = 0;
1236
+ updatesSeq++;
1237
+ updatesLoading = false;
1238
+ o.onUnreadChange();
1239
+ }
1240
+ if (open) loadUpdates(3e4);
1241
+ },
1242
+ destroy() {
1243
+ if (similarTimer) clearTimeout(similarTimer);
1244
+ if (searchTimer) clearTimeout(searchTimer);
1245
+ mounted = null;
1246
+ }
1247
+ };
1248
+ }
1249
+
1250
+ // src/launcher-drag.ts
1251
+ var DRAG_THRESHOLD = 6;
1252
+ var MARGIN = 16;
1253
+ function parseLauncherSpot(raw) {
1254
+ let v = raw;
1255
+ if (typeof v === "string") {
1256
+ try {
1257
+ v = JSON.parse(v);
1258
+ } catch {
1259
+ return null;
1260
+ }
1261
+ }
1262
+ if (!v || typeof v !== "object") return null;
1263
+ const { side, y } = v;
1264
+ if (side !== "left" && side !== "right" || typeof y !== "number" || !Number.isFinite(y)) return null;
1265
+ return { side, y: Math.min(1, Math.max(0, y)) };
1266
+ }
1267
+ function maxLauncherBottom(viewportHeight, size) {
1268
+ return Math.max(0, viewportHeight - size - MARGIN * 2);
1269
+ }
1270
+ function snapLauncher(drop, viewport, size) {
1271
+ const side = drop.left + size / 2 < viewport.width / 2 ? "left" : "right";
1272
+ const max = maxLauncherBottom(viewport.height, size);
1273
+ const bottom = Math.min(max, Math.max(0, viewport.height - MARGIN - size - drop.top));
1274
+ return { side, y: max ? bottom / max : 0 };
1275
+ }
1276
+ function enableLauncherDrag({ launcher, onMove }) {
1277
+ const win = launcher.ownerDocument.defaultView ?? window;
1278
+ let start = null;
1279
+ let dragging = false;
1280
+ let swallowClickUntil = 0;
1281
+ const onDown = (e) => {
1282
+ if (e.button !== 0) return;
1283
+ const r = launcher.getBoundingClientRect();
1284
+ start = { id: e.pointerId, x: e.clientX, y: e.clientY, left: r.left, top: r.top };
1285
+ dragging = false;
1286
+ try {
1287
+ launcher.setPointerCapture(e.pointerId);
1288
+ } catch {
1289
+ }
1290
+ };
1291
+ const onMovePointer = (e) => {
1292
+ if (!start || e.pointerId !== start.id) return;
1293
+ const dx = e.clientX - start.x;
1294
+ const dy = e.clientY - start.y;
1295
+ if (!dragging) {
1296
+ if (Math.hypot(dx, dy) < DRAG_THRESHOLD) return;
1297
+ dragging = true;
1298
+ launcher.classList.add("g-m-dragging");
1299
+ }
1300
+ e.preventDefault();
1301
+ const size = launcher.offsetWidth || 56;
1302
+ const left = Math.min(win.innerWidth - size - MARGIN, Math.max(MARGIN, start.left + dx));
1303
+ const top = Math.min(win.innerHeight - size - MARGIN, Math.max(MARGIN, start.top + dy));
1304
+ Object.assign(launcher.style, { left: `${left}px`, top: `${top}px`, right: "auto", bottom: "auto", margin: "0" });
1305
+ };
1306
+ const onUp = (e) => {
1307
+ if (!start || e.pointerId !== start.id) return;
1308
+ start = null;
1309
+ if (!dragging) return;
1310
+ dragging = false;
1311
+ swallowClickUntil = Date.now() + 400;
1312
+ const before = launcher.getBoundingClientRect();
1313
+ launcher.classList.remove("g-m-dragging");
1314
+ Object.assign(launcher.style, { left: "", top: "", right: "", margin: "" });
1315
+ onMove(snapLauncher({ left: before.left, top: before.top }, { width: win.innerWidth, height: win.innerHeight }, before.width || 56));
1316
+ const after = launcher.getBoundingClientRect();
1317
+ const dx = before.left - after.left;
1318
+ const dy = before.top - after.top;
1319
+ if (!dx && !dy) return;
1320
+ launcher.style.transition = "none";
1321
+ launcher.style.transform = `translate(${dx}px, ${dy}px)`;
1322
+ void launcher.offsetWidth;
1323
+ launcher.style.transition = "";
1324
+ launcher.classList.add("g-m-snapping");
1325
+ launcher.style.transform = "";
1326
+ const done = () => launcher.classList.remove("g-m-snapping");
1327
+ launcher.addEventListener("transitionend", done, { once: true });
1328
+ win.setTimeout(done, 400);
1329
+ };
1330
+ const onClick = (e) => {
1331
+ if (Date.now() < swallowClickUntil) {
1332
+ swallowClickUntil = 0;
1333
+ e.preventDefault();
1334
+ e.stopImmediatePropagation();
1335
+ }
1336
+ };
1337
+ launcher.addEventListener("pointerdown", onDown);
1338
+ launcher.addEventListener("pointermove", onMovePointer);
1339
+ launcher.addEventListener("pointerup", onUp);
1340
+ launcher.addEventListener("pointercancel", onUp);
1341
+ launcher.addEventListener("click", onClick, true);
1342
+ return () => {
1343
+ launcher.removeEventListener("pointerdown", onDown);
1344
+ launcher.removeEventListener("pointermove", onMovePointer);
1345
+ launcher.removeEventListener("pointerup", onUp);
1346
+ launcher.removeEventListener("pointercancel", onUp);
1347
+ launcher.removeEventListener("click", onClick, true);
1348
+ };
1349
+ }
1350
+
382
1351
  // src/messenger-styles.ts
383
1352
  var MESSENGER_STYLES = `
384
1353
  :host([data-g-messenger="bottom-right"]) .g-launcher.g-pos-bottom-right{right:72px}
@@ -387,9 +1356,13 @@ var MESSENGER_STYLES = `
387
1356
  .g-m-launcher:hover{transform:scale(1.05)}
388
1357
  .g-m-launcher:focus-visible{outline:2px solid var(--g-primary);outline-offset:3px}
389
1358
  .g-m-launcher svg{width:26px;height:26px;fill:currentColor}
1359
+ .g-m-launcher{touch-action:none;-webkit-user-select:none;user-select:none}
1360
+ .g-m-launcher.g-m-dragging{transition:none;cursor:grabbing;transform:scale(1.08)}
1361
+ .g-m-launcher.g-m-snapping{transition:transform .3s cubic-bezier(.2,.8,.2,1)}
390
1362
  .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)}
391
- .g-messenger{bottom:76px;height:min(640px,calc(100vh - 110px));transition:width .2s ease,height .2s ease,max-height .2s ease}
392
- .g-messenger.g-m-expanded{width:min(720px,calc(100vw - 32px));height:calc(100vh - 110px);max-height:calc(100vh - 110px)}
1363
+ .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}
1364
+ .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))}
1365
+ .g-messenger.g-m-below{bottom:auto;top:var(--g-m-top)}
393
1366
  .g-m-expanded .g-rc-article,.g-m-expanded .g-m-scroll{padding:24px clamp(20px,5vw,40px)}
394
1367
  .g-m-expanded .g-rc-article>.g-title{font-size:24px;line-height:1.25;margin-bottom:12px}
395
1368
  .g-m-expanded .g-rc-article .g-body{font-size:15px}
@@ -441,7 +1414,7 @@ var MESSENGER_STYLES = `
441
1414
  .g-m-typing span{width:6px;height:6px;border-radius:999px;background:var(--g-muted);animation:g-m-bounce 1s infinite}
442
1415
  .g-m-typing span:nth-child(2){animation-delay:.15s}.g-m-typing span:nth-child(3){animation-delay:.3s}
443
1416
  @keyframes g-m-bounce{0%,60%,100%{opacity:.3;transform:translateY(0)}30%{opacity:1;transform:translateY(-3px)}}
444
- @media (prefers-reduced-motion: reduce){.g-m-typing span{animation:none}.g-m-launcher{transition:none}}
1417
+ @media (prefers-reduced-motion: reduce){.g-m-typing span{animation:none}.g-m-launcher,.g-m-launcher.g-m-snapping{transition:none}}
445
1418
  .g-m-chips{display:flex;flex-direction:column;gap:6px;margin-top:2px}
446
1419
  .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}
447
1420
  .g-m-chip:hover{background:var(--g-subtle)}
@@ -582,90 +1555,81 @@ var MESSENGER_STYLES = `
582
1555
  .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}
583
1556
  .g-m-step-done{color:var(--g-text);font-weight:600}
584
1557
  .g-m-step-done .g-m-step-dot{background:var(--g-primary);border-color:var(--g-primary)}
1558
+ .g-m-ideas-top{display:flex;flex-direction:column;gap:10px;margin-bottom:12px}
1559
+ .g-m-ideas-top:empty{display:none}
1560
+ .g-m-idea-tools{justify-content:space-between;gap:8px;flex-wrap:wrap}
1561
+ .g-m-filters{display:flex;gap:6px;flex-wrap:wrap}
1562
+ .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}
1563
+ .g-m-filter:hover{background:var(--g-subtle)}
1564
+ .g-m-filter[aria-pressed="true"]{background:var(--g-primary);border-color:var(--g-primary);color:var(--g-primary-text)}
1565
+ .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}
1566
+ .g-m-sort{width:auto;min-width:0;min-height:30px;padding:4px 8px;font-size:12px}
1567
+ .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}
1568
+ .g-m-idea{display:flex;align-items:flex-start;gap:12px;padding:12px 14px;border-top:1px solid var(--g-border)}
1569
+ .g-m-idea:first-child{border-top:0}
1570
+ .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}
1571
+ button.g-m-vote:hover{border-color:var(--g-primary);color:var(--g-primary)}
1572
+ .g-m-vote:disabled{opacity:.6;cursor:progress}
1573
+ .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)}
1574
+ .g-m-vote-static{cursor:default;background:var(--g-subtle)}
1575
+ .g-m-vote-ico{width:16px;height:16px;stroke-width:2.4}
1576
+ .g-m-vote-count{font-size:13px;font-weight:700;line-height:1.2}
1577
+ .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}
1578
+ .g-m-idea-open:hover .g-m-idea-title{color:var(--g-primary)}
1579
+ .g-m-idea-title{font-weight:600;overflow-wrap:anywhere}
1580
+ .g-m-idea-meta{display:flex;align-items:center;gap:8px;flex-wrap:wrap}
1581
+ .g-m-idea-pill.g-m-idea-new{background:var(--g-subtle);color:var(--g-muted)}
1582
+ .g-m-idea-pill.g-m-idea-under_review{background:#e0e7ff;color:#3730a3}
1583
+ .g-m-idea-pill.g-m-idea-planned{background:#dbeafe;color:#1e40af}
1584
+ .g-m-idea-pill.g-m-idea-in_progress{background:#fef3c7;color:#92400e}
1585
+ .g-m-idea-pill.g-m-idea-released{background:#dcfce7;color:#166534}
1586
+ .g-m-idea-pill.g-m-idea-declined{background:#fee2e2;color:#991b1b}
1587
+ .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)}
1588
+ .g-m-idea-notice .g-m-text-btn{padding:4px 0;min-height:0}
1589
+ .g-m-updates-link,.g-m-portal-link{align-self:flex-start;padding-left:0}
1590
+ .g-m-portal-link{display:block;margin:0 auto}
1591
+ .g-m-idea-detail{gap:12px}
1592
+ .g-m-idea-desc,.g-m-idea-note{font-size:14px;overflow-wrap:anywhere}
1593
+ .g-m-idea-desc p,.g-m-idea-note p,.g-m-comment-body p{margin:0 0 8px}
1594
+ .g-m-idea-desc p:last-child,.g-m-idea-note p:last-child,.g-m-comment-body p:last-child{margin:0}
1595
+ .g-m-idea-note{border-left:3px solid var(--g-primary);background:var(--g-subtle);border-radius:6px;padding:8px 10px}
1596
+ .g-m-idea-vote{display:flex;flex-direction:column;gap:12px;margin:0}
1597
+ .g-m-idea-vote .g-m-vote{min-height:44px}
1598
+ .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}
1599
+ .g-m-follow:hover{background:var(--g-subtle)}
1600
+ .g-m-follow.g-m-following{border-color:var(--g-primary);color:var(--g-primary)}
1601
+ .g-m-follow:disabled{opacity:.6}
1602
+ .g-m-seg{display:flex;border:1px solid var(--g-border);border-radius:10px;overflow:hidden}
1603
+ .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}
1604
+ .g-m-seg-btn:first-child{border-left:0}
1605
+ .g-m-seg-btn:hover{background:var(--g-subtle)}
1606
+ .g-m-seg-btn[aria-checked="true"]{background:var(--g-primary);color:var(--g-primary-text)}
1607
+ .g-m-unvote{align-self:flex-start;padding-left:0;color:var(--g-muted)}
1608
+ .g-m-comments{display:flex;flex-direction:column;gap:8px}
1609
+ .g-m-comments-title{margin:4px 0 0;font-size:14px}
1610
+ .g-m-comment-list{list-style:none;margin:0;padding:0;display:flex;flex-direction:column;gap:10px}
1611
+ .g-m-comment{border:1px solid var(--g-border);border-radius:10px;padding:8px 10px;font-size:13px}
1612
+ .g-m-comment-team{background:var(--g-subtle);border-color:color-mix(in srgb,var(--g-primary) 35%,var(--g-border))}
1613
+ .g-m-comment-meta{gap:6px;margin-bottom:4px}
1614
+ .g-m-comment-author{font-weight:600}
1615
+ .g-m-comment-body{overflow-wrap:anywhere}
1616
+ .g-m-comment-form{display:flex;flex-direction:column;align-items:flex-end;gap:6px}
1617
+ .g-m-comment-form textarea{resize:vertical;min-height:56px}
1618
+ .g-m-similar:empty{display:none}
1619
+ .g-m-similar-box{display:flex;flex-direction:column;gap:6px}
1620
+ .g-m-similar-box .g-m-idea-list{margin:0}
1621
+ .g-m-update{display:block}
1622
+ .g-m-update:disabled{cursor:default}
1623
+ .g-m-update:disabled:hover{background:var(--g-bg)}
585
1624
  `;
586
1625
 
587
- // src/render-safe.ts
588
- var CITATION = /\s*\[\[article:[^\]]*\]\]/g;
589
- var INLINE = /\*\*([^*\n]+)\*\*|`([^`\n]+)`|(!?)\[([^\]\n]{1,200})\]\((https?:\/\/[^\s)]+)\)|(https?:\/\/[^\s<>()"']+[^\s<>().,;:!?'"])/g;
590
- function stripCitationMarkers(text) {
591
- return text.replace(CITATION, "");
592
- }
593
- function safeHttpUrl(raw) {
594
- try {
595
- const u = new URL(raw);
596
- return u.protocol === "http:" || u.protocol === "https:" ? u.href : null;
597
- } catch {
598
- return null;
599
- }
600
- }
601
- function link(doc, label, href) {
602
- const url = safeHttpUrl(href);
603
- if (!url) return doc.createTextNode(label);
604
- const a = doc.createElement("a");
605
- a.href = url;
606
- a.target = "_blank";
607
- a.rel = "noopener noreferrer nofollow";
608
- a.textContent = label;
609
- return a;
610
- }
611
- function appendInline(doc, parent, text) {
612
- let last = 0;
613
- for (const m of text.matchAll(INLINE)) {
614
- const start = m.index ?? 0;
615
- if (start > last) parent.appendChild(doc.createTextNode(text.slice(last, start)));
616
- if (m[1] !== void 0) {
617
- const b = doc.createElement("strong");
618
- b.textContent = m[1];
619
- parent.appendChild(b);
620
- } else if (m[2] !== void 0) {
621
- const c = doc.createElement("code");
622
- c.textContent = m[2];
623
- parent.appendChild(c);
624
- } else if (m[4] !== void 0 && m[5] !== void 0) {
625
- parent.appendChild(m[3] ? doc.createTextNode(m[4]) : link(doc, m[4], m[5]));
626
- } else if (m[6] !== void 0) {
627
- parent.appendChild(link(doc, m[6], m[6]));
628
- }
629
- last = start + m[0].length;
630
- }
631
- if (last < text.length) parent.appendChild(doc.createTextNode(text.slice(last)));
632
- }
633
- var BULLET = /^\s*[-*•]\s+/;
634
- var NUMBERED = /^\s*\d{1,3}[.)]\s+/;
635
- function renderSafeText(doc, text) {
636
- const frag = doc.createDocumentFragment();
637
- const blocks = stripCitationMarkers(text).replace(/\r\n?/g, "\n").trim().split(/\n{2,}/);
638
- for (const block of blocks) {
639
- if (!block.trim()) continue;
640
- const lines = block.split("\n");
641
- if (lines.every((l) => BULLET.test(l)) || lines.every((l) => NUMBERED.test(l))) {
642
- const ordered = NUMBERED.test(lines[0]);
643
- const list = doc.createElement(ordered ? "ol" : "ul");
644
- for (const line of lines) {
645
- const li = doc.createElement("li");
646
- appendInline(doc, li, line.replace(ordered ? NUMBERED : BULLET, ""));
647
- list.appendChild(li);
648
- }
649
- frag.appendChild(list);
650
- continue;
651
- }
652
- const p = doc.createElement("p");
653
- lines.forEach((line, i) => {
654
- if (i) p.appendChild(doc.createElement("br"));
655
- appendInline(doc, p, line);
656
- });
657
- frag.appendChild(p);
658
- }
659
- return frag;
660
- }
661
-
662
1626
  // src/messenger.ts
663
- var SVG_NS = "http://www.w3.org/2000/svg";
1627
+ var SVG_NS2 = "http://www.w3.org/2000/svg";
664
1628
  function chatIcon(doc) {
665
- const svg = doc.createElementNS(SVG_NS, "svg");
1629
+ const svg = doc.createElementNS(SVG_NS2, "svg");
666
1630
  svg.setAttribute("viewBox", "0 0 24 24");
667
1631
  svg.setAttribute("aria-hidden", "true");
668
- const path = doc.createElementNS(SVG_NS, "path");
1632
+ const path = doc.createElementNS(SVG_NS2, "path");
669
1633
  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");
670
1634
  svg.appendChild(path);
671
1635
  return svg;
@@ -690,15 +1654,16 @@ var ICONS = {
690
1654
  circle: ["M12 3a9 9 0 1 0 0 18 9 9 0 0 0 0-18z"],
691
1655
  expand: ["M15 3h6v6", "M21 3l-7 7", "M9 21H3v-6", "M3 21l7-7"],
692
1656
  flag: ["M5 21V4", "M5 4h12l-2.5 4L17 12H5"],
693
- collapse: ["M4 14h6v6", "M3 21l7-7", "M20 10h-6V4", "M21 3l-7 7"]
1657
+ collapse: ["M4 14h6v6", "M3 21l7-7", "M20 10h-6V4", "M21 3l-7 7"],
1658
+ idea: IDEA_ICON
694
1659
  };
695
1660
  function icon(doc, name, cls = "g-m-ico") {
696
- const svg = doc.createElementNS(SVG_NS, "svg");
1661
+ const svg = doc.createElementNS(SVG_NS2, "svg");
697
1662
  svg.setAttribute("viewBox", "0 0 24 24");
698
1663
  svg.setAttribute("aria-hidden", "true");
699
1664
  svg.setAttribute("class", cls);
700
1665
  for (const d of ICONS[name] ?? []) {
701
- const path = doc.createElementNS(SVG_NS, "path");
1666
+ const path = doc.createElementNS(SVG_NS2, "path");
702
1667
  path.setAttribute("d", d);
703
1668
  svg.appendChild(path);
704
1669
  }
@@ -725,7 +1690,7 @@ function shortDateTime(iso) {
725
1690
  return d.toISOString().slice(0, 16).replace("T", " ");
726
1691
  }
727
1692
  }
728
- function shortDate(iso) {
1693
+ function shortDate2(iso) {
729
1694
  const d = new Date(iso);
730
1695
  if (Number.isNaN(d.getTime())) return "";
731
1696
  try {
@@ -798,7 +1763,6 @@ function renderMessenger(options) {
798
1763
  const pos = `g-pos-${config.launcher.position}`;
799
1764
  const rootNode = scope.getRootNode();
800
1765
  const shadowHost = "host" in rootNode ? rootNode.host : null;
801
- shadowHost?.setAttribute("data-g-messenger", config.launcher.position);
802
1766
  const root = el(doc, "div", "g-messenger-root");
803
1767
  const launcher = el(doc, "button", `g-m-launcher ${pos}`);
804
1768
  launcher.type = "button";
@@ -810,6 +1774,47 @@ function renderMessenger(options) {
810
1774
  launcher.appendChild(badge);
811
1775
  if (options.hideLauncher) launcher.hidden = true;
812
1776
  const panel = el(doc, "div", `g-panel g-messenger ${pos}`);
1777
+ let spot = options.launcherSpot ?? null;
1778
+ const win = doc.defaultView ?? window;
1779
+ function placeLauncher() {
1780
+ const side = spot?.side ?? (config.launcher.position === "bottom-left" ? "left" : "right");
1781
+ for (const node of [launcher, panel]) {
1782
+ node.classList.toggle("g-pos-bottom-left", side === "left");
1783
+ node.classList.toggle("g-pos-bottom-right", side === "right");
1784
+ }
1785
+ const size = launcher.offsetWidth || 56;
1786
+ const bottom = spot && !options.hideLauncher ? Math.round(spot.y * maxLauncherBottom(win.innerHeight, size)) : 0;
1787
+ launcher.style.bottom = bottom ? `${bottom}px` : "";
1788
+ if (bottom < size + 16) shadowHost?.setAttribute("data-g-messenger", `bottom-${side}`);
1789
+ else shadowHost?.removeAttribute("data-g-messenger");
1790
+ const launcherTop = win.innerHeight - 16 - size - bottom;
1791
+ const below = bottom > 0 && launcherTop < 480 && bottom > launcherTop;
1792
+ panel.classList.toggle("g-m-below", below);
1793
+ if (!bottom) {
1794
+ panel.style.removeProperty("--g-m-bottom");
1795
+ panel.style.removeProperty("--g-m-top");
1796
+ panel.style.removeProperty("--g-m-reserve");
1797
+ } else if (below) {
1798
+ const top = launcherTop + size - 12;
1799
+ panel.style.setProperty("--g-m-top", `${top}px`);
1800
+ panel.style.setProperty("--g-m-reserve", `${top + 40}px`);
1801
+ } else {
1802
+ panel.style.setProperty("--g-m-bottom", `${bottom + 76}px`);
1803
+ panel.style.setProperty("--g-m-reserve", `${bottom + 110}px`);
1804
+ }
1805
+ }
1806
+ const stopDrag = enableLauncherDrag({
1807
+ launcher,
1808
+ onMove(next) {
1809
+ spot = next;
1810
+ placeLauncher();
1811
+ options.onLauncherMove?.(next);
1812
+ }
1813
+ });
1814
+ const onResize = () => {
1815
+ if (spot) placeLauncher();
1816
+ };
1817
+ win.addEventListener("resize", onResize);
813
1818
  panel.setAttribute("role", "dialog");
814
1819
  panel.setAttribute("aria-label", config.name);
815
1820
  panel.hidden = true;
@@ -820,17 +1825,25 @@ function renderMessenger(options) {
820
1825
  panel.append(head, main, tabs);
821
1826
  root.append(launcher, panel);
822
1827
  scope.appendChild(root);
1828
+ placeLauncher();
1829
+ function withRcText(items, rc, apply) {
1830
+ if (!rc?.length) return items;
1831
+ const byId = new Map(rc.map((x) => [x.id, x]));
1832
+ return items.map((i) => byId.has(i.id) ? apply(i, byId.get(i.id)) : i);
1833
+ }
1834
+ const helpModules = () => options.resourceCenter?.modules?.length ? 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;
823
1835
  const rcExtras = (ids = []) => ids.map((id) => options.guides?.find((g) => g.id === id || g.key === id)).filter((g) => !!g);
824
1836
  const help = createHelpView({
825
1837
  doc,
826
1838
  data: () => ({
827
- collections: articles?.collections ?? options.resourceCenter?.collections ?? [],
828
- articles: articles?.articles ?? options.resourceCenter?.articles ?? [],
1839
+ collections: withRcText(articles?.collections ?? options.resourceCenter?.collections ?? [], options.resourceCenter?.collections, (c, t) => ({ ...c, name: t.name, description: t.description })),
1840
+ articles: withRcText(articles?.articles ?? options.resourceCenter?.articles ?? [], options.resourceCenter?.articles, (a, t) => ({ ...a, title: t.title, summary: t.summary })),
829
1841
  suggested: helpSuggestions(),
830
1842
  recommended: rcExtras(options.resourceCenter?.recommendedGuideIds),
831
1843
  checklists: rcExtras(options.resourceCenter?.checklistGuideIds),
832
1844
  faqs: options.resourceCenter?.faqs,
833
- links: options.resourceCenter?.links
1845
+ links: options.resourceCenter?.links,
1846
+ modules: helpModules()
834
1847
  }),
835
1848
  search: (q) => api.searchArticles(q),
836
1849
  audienceIds: options.audienceIds,
@@ -849,6 +1862,24 @@ function renderMessenger(options) {
849
1862
  onShowMe: (key) => startWalkthrough(key),
850
1863
  onArticleChange: () => syncSize()
851
1864
  });
1865
+ const ideasOn = () => !!config.ideas && !!api.listIdeas;
1866
+ const ideasTab = { kind: "tab", tab: "ideas" };
1867
+ const ideas = createIdeas({
1868
+ doc,
1869
+ api,
1870
+ config: () => config.ideas,
1871
+ identified: () => visitor().identified !== false,
1872
+ openIdea: (id) => go({ kind: "idea", id, from: view.kind === "idea" ? view.from : view }),
1873
+ openForm: () => go({ kind: "idea-new", from: view }),
1874
+ openSent: (idea) => go({ kind: "idea-sent", idea }),
1875
+ openList: () => go(ideasTab),
1876
+ openUpdates: () => go({ kind: "idea-updates", from: view }),
1877
+ onUnreadChange: () => {
1878
+ if (!open || destroyed) return;
1879
+ if (view.kind === "tab" && view.tab === "home") render();
1880
+ else renderTabs();
1881
+ }
1882
+ });
852
1883
  let docCollapsed = false;
853
1884
  let sizeButton = null;
854
1885
  const readingDoc = () => open && (view.kind === "howto" || view.kind === "tab" && view.tab === "help" && help.isArticleOpen());
@@ -943,7 +1974,7 @@ function renderMessenger(options) {
943
1974
  const refreshConversations = () => api.listConversations().then((list) => {
944
1975
  conversations = [...list].sort((a, b) => b.lastMessageAt.localeCompare(a.lastMessageAt));
945
1976
  updateBadge();
946
- if (open && view.kind === "tab" && view.tab !== "help") render();
1977
+ if (open && view.kind === "tab" && view.tab !== "help" && view.tab !== "ideas") render();
947
1978
  }).catch(() => {
948
1979
  });
949
1980
  function renderHead(title, back) {
@@ -977,6 +2008,7 @@ function renderMessenger(options) {
977
2008
  ["messages", "messages", "Messages"],
978
2009
  ["help", "help", "Help"]
979
2010
  ];
2011
+ if (ideasOn()) items.push(["ideas", "idea", config.ideas.title || "Ideas"]);
980
2012
  const latest = news();
981
2013
  if (latest.length) items.push(["news", "news", "News"]);
982
2014
  for (const [tab, ico, label] of items) {
@@ -987,7 +2019,7 @@ function renderMessenger(options) {
987
2019
  const i = el(doc, "span", "g-m-tab-icon");
988
2020
  i.appendChild(icon(doc, ico));
989
2021
  b.append(i, el(doc, "span", "", label));
990
- if (tab === "news" && latest.some((n) => n.unread)) {
2022
+ if (tab === "news" && latest.some((n) => n.unread) || tab === "ideas" && ideas.unread() > 0) {
991
2023
  const dot2 = el(doc, "span", "g-m-tab-dot");
992
2024
  dot2.setAttribute("aria-label", "New");
993
2025
  i.appendChild(dot2);
@@ -1193,11 +2225,16 @@ function renderMessenger(options) {
1193
2225
  const body = el(doc, "span", "g-m-news-body");
1194
2226
  const meta = el(doc, "span", "g-m-row g-m-center g-m-news-meta");
1195
2227
  if (n.unread) meta.appendChild(el(doc, "span", "g-m-tag", "New"));
1196
- meta.appendChild(el(doc, "span", "g-m-time", shortDate(n.publishedAt)));
2228
+ meta.appendChild(el(doc, "span", "g-m-time", shortDate2(n.publishedAt)));
1197
2229
  body.append(meta, el(doc, "span", "g-m-card-title", n.title));
1198
2230
  if (n.body) body.appendChild(el(doc, "span", "g-m-card-sub g-m-clamp", n.body));
1199
2231
  card.appendChild(body);
1200
- card.addEventListener("click", () => startGuide(n.key || n.id));
2232
+ card.addEventListener("click", () => {
2233
+ if (n.key) startGuide(n.key);
2234
+ else if (n.articleId) openArticle(n.articleId);
2235
+ else if (n.url && /^https?:\/\//i.test(n.url)) window.open(n.url, "_blank", "noopener,noreferrer");
2236
+ else startGuide(n.id);
2237
+ });
1201
2238
  return card;
1202
2239
  }
1203
2240
  function renderHome() {
@@ -1223,6 +2260,8 @@ function renderMessenger(options) {
1223
2260
  if (filed) inner.appendChild(conversationCard(filed, "Your complaint"));
1224
2261
  const recent = conversations.find((c) => c.id !== filed?.id);
1225
2262
  if (recent) inner.appendChild(conversationCard(recent, recent.status === "closed" && !recent.unread ? "Recent conversation" : "Continue the conversation"));
2263
+ const ideaUpdates = ideasOn() ? ideas.unread() : 0;
2264
+ 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 }) }));
1226
2265
  if (config.status) inner.appendChild(statusCard(config.status));
1227
2266
  if (cards.progress) progressCards(v).forEach((c) => inner.appendChild(c));
1228
2267
  const suggestions = pageSuggestions(v);
@@ -1242,6 +2281,10 @@ function renderMessenger(options) {
1242
2281
  }
1243
2282
  const latest = news()[0];
1244
2283
  if (latest) inner.appendChild(newsCard(latest));
2284
+ if (ideasOn()) {
2285
+ const canWrite = !ideas.blocked();
2286
+ 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) }));
2287
+ }
1245
2288
  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" }) }));
1246
2289
  scroll.append(hero, inner);
1247
2290
  main.appendChild(scroll);
@@ -1342,6 +2385,25 @@ function renderMessenger(options) {
1342
2385
  scroll.appendChild(complaintSent(doc, { complaint: v.complaint, answers: v.answers, onOpenConversation: () => go({ kind: "thread", conversationId: v.conversationId }), onHome: () => go({ kind: "tab", tab: "home" }) }));
1343
2386
  main.appendChild(scroll);
1344
2387
  }
2388
+ function renderIdeasView(v) {
2389
+ const title = config.ideas?.title || "Ideas";
2390
+ if (v.kind === "idea") {
2391
+ renderHead(title, () => go(v.from));
2392
+ main.appendChild(ideas.detailScreen(v.id));
2393
+ } else if (v.kind === "idea-new") {
2394
+ renderHead("Suggest an idea", () => go(v.from));
2395
+ main.appendChild(ideas.formScreen());
2396
+ } else if (v.kind === "idea-sent") {
2397
+ renderHead(title, () => go(ideasTab));
2398
+ main.appendChild(ideas.sentScreen(v.idea));
2399
+ } else if (v.kind === "idea-updates") {
2400
+ renderHead("Updates on your ideas", () => go(v.from));
2401
+ main.appendChild(ideas.updatesScreen());
2402
+ } else {
2403
+ renderHead(title);
2404
+ main.appendChild(ideas.listScreen());
2405
+ }
2406
+ }
1345
2407
  function renderHowToView(id, from) {
1346
2408
  const cached = howToCache.get(id);
1347
2409
  renderHead(cached?.title ?? "How-to", () => go(from));
@@ -1455,7 +2517,8 @@ function renderMessenger(options) {
1455
2517
  bubble.dataset.messageId = m.id;
1456
2518
  wrap.appendChild(bubble);
1457
2519
  const offer = m.complaintOffer && canComplain() ? complaintCategories().find((c) => c.id === m.complaintOffer.categoryId) : void 0;
1458
- if (m.citations.length || m.suggestedGuides.length || m.howTos?.length || offer) {
2520
+ const ideaChip = m.ideaOffer && ideasOn() ? ideas.offerChip(m.ideaOffer, current?.id ?? null, () => icon(doc, "idea", "g-m-ico g-m-chip-ico")) : null;
2521
+ if (m.citations.length || m.suggestedGuides.length || m.howTos?.length || offer || ideaChip) {
1459
2522
  const chips = el(doc, "div", "g-m-chips");
1460
2523
  if (offer) {
1461
2524
  const b = el(doc, "button", "g-m-chip g-m-guide g-m-offer");
@@ -1468,6 +2531,7 @@ function renderMessenger(options) {
1468
2531
  });
1469
2532
  chips.appendChild(b);
1470
2533
  }
2534
+ if (ideaChip) chips.appendChild(ideaChip);
1471
2535
  for (const g of m.suggestedGuides) {
1472
2536
  const b = el(doc, "button", "g-m-chip g-m-guide", `\u25B6 Show me: ${g.name}`);
1473
2537
  b.type = "button";
@@ -1620,7 +2684,13 @@ function renderMessenger(options) {
1620
2684
  else if (view.kind === "complaint-category") renderComplaintCategories(view);
1621
2685
  else if (view.kind === "complaint-form") renderComplaintForm(view);
1622
2686
  else if (view.kind === "complaint-sent") renderComplaintSent(view);
1623
- else if (view.tab === "home") renderHome();
2687
+ else if (view.kind !== "tab" || view.tab === "ideas") {
2688
+ if (ideasOn()) renderIdeasView(view);
2689
+ else {
2690
+ view = { kind: "tab", tab: "home" };
2691
+ renderHome();
2692
+ }
2693
+ } else if (view.tab === "home") renderHome();
1624
2694
  else if (view.tab === "messages") renderMessagesTab();
1625
2695
  else if (view.tab === "news" && news().length) renderNewsTab();
1626
2696
  else if (view.tab === "news") {
@@ -1771,6 +2841,7 @@ function renderMessenger(options) {
1771
2841
  render();
1772
2842
  void refreshConversations();
1773
2843
  loadHowTos();
2844
+ if (ideasOn()) ideas.loadUpdates();
1774
2845
  if (view.kind === "thread") scheduleThreadPoll();
1775
2846
  options.onOpen?.();
1776
2847
  } else {
@@ -1819,28 +2890,39 @@ function renderMessenger(options) {
1819
2890
  setOpen(true);
1820
2891
  startComplaint(categoryId ?? null);
1821
2892
  },
2893
+ openIdeas(requestId) {
2894
+ if (!ideasOn()) return;
2895
+ setOpen(true);
2896
+ go(requestId ? { kind: "idea", id: requestId, from: ideasTab } : ideasTab);
2897
+ },
1822
2898
  refresh() {
1823
2899
  if (destroyed) return;
2900
+ if (ideasOn()) ideas.sync(open);
1824
2901
  if (!open) {
1825
2902
  pageKey = null;
1826
2903
  return;
1827
2904
  }
1828
2905
  loadPage();
1829
- if (view.kind === "tab") render();
1830
- else if (!messages.length) renderLog();
2906
+ if (view.kind === "tab" && view.tab !== "ideas") render();
2907
+ else if (view.kind === "tab") renderTabs();
2908
+ else if (view.kind === "thread" && !messages.length) renderLog();
1831
2909
  },
1832
2910
  destroy() {
1833
2911
  destroyed = true;
1834
2912
  if (threadTimer) clearTimeout(threadTimer);
1835
2913
  if (backgroundTimer) clearInterval(backgroundTimer);
1836
2914
  panel.removeEventListener("keydown", onKey);
2915
+ stopDrag();
2916
+ win.removeEventListener("resize", onResize);
1837
2917
  help.destroy();
2918
+ ideas.destroy();
1838
2919
  shadowHost?.removeAttribute("data-g-messenger");
1839
2920
  root.remove();
1840
2921
  }
1841
2922
  };
1842
2923
  }
1843
2924
  export {
2925
+ parseLauncherSpot,
1844
2926
  personalGreeting,
1845
2927
  relativeTime,
1846
2928
  renderMessenger