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