@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/capture.cjs +31 -0
- package/dist/capture.cjs.map +1 -1
- package/dist/capture.js +2 -2
- package/dist/chunk-H3CROHPR.js +585 -0
- package/dist/chunk-H3CROHPR.js.map +1 -0
- package/dist/chunk-VJSQM23R.js +359 -0
- package/dist/chunk-VJSQM23R.js.map +1 -0
- package/dist/chunk-VQJ7MZ2K.js +463 -0
- package/dist/chunk-VQJ7MZ2K.js.map +1 -0
- package/dist/index.cjs +282 -74
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +19 -3
- package/dist/index.d.ts +19 -3
- package/dist/index.js +116 -11
- package/dist/index.js.map +1 -1
- package/dist/messenger.cjs +1326 -172
- package/dist/messenger.cjs.map +1 -1
- package/dist/messenger.d.cts +67 -5
- package/dist/messenger.d.ts +67 -5
- package/dist/messenger.js +1182 -100
- package/dist/messenger.js.map +1 -1
- package/package.json +2 -2
package/dist/messenger.cjs
CHANGED
|
@@ -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
|
|
29
|
+
var import_runtime6 = require("@guidelytech/shared/runtime");
|
|
29
30
|
|
|
30
31
|
// src/help-view.ts
|
|
31
|
-
var
|
|
32
|
+
var import_runtime4 = require("@guidelytech/shared/runtime");
|
|
32
33
|
|
|
33
34
|
// src/pdf-viewer.ts
|
|
34
|
-
var
|
|
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.${
|
|
48
|
-
const link2 = block.querySelector(`a.${
|
|
49
|
-
const strip = block.querySelector(`.${
|
|
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
|
|
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,
|
|
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(
|
|
222
|
+
for (const [k, v] of Object.entries(import_runtime3.EMBED_IFRAME_ATTRS)) out.setAttribute(k, v);
|
|
221
223
|
} else {
|
|
222
|
-
if (!(0,
|
|
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,
|
|
227
|
-
for (const [k, v] of Object.entries(
|
|
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^="${
|
|
292
|
-
const key = (0,
|
|
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
|
-
|
|
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,
|
|
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,
|
|
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,
|
|
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
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
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
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
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
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
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
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
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 (
|
|
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
|
|
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,
|
|
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 =
|
|
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 =
|
|
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 <=
|
|
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 <=
|
|
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 >
|
|
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
|
|
2224
|
+
var SVG_NS2 = "http://www.w3.org/2000/svg";
|
|
1189
2225
|
function chatIcon(doc) {
|
|
1190
|
-
const svg = doc.createElementNS(
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
|
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,
|
|
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",
|
|
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", () =>
|
|
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,
|
|
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,
|
|
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,
|
|
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
|
-
|
|
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,
|
|
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 === "
|
|
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 (
|
|
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
|