@guidelytech/guide-runtime 0.1.0 → 0.2.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.
@@ -0,0 +1,1023 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/messenger.ts
21
+ var messenger_exports = {};
22
+ __export(messenger_exports, {
23
+ relativeTime: () => relativeTime,
24
+ renderMessenger: () => renderMessenger
25
+ });
26
+ module.exports = __toCommonJS(messenger_exports);
27
+
28
+ // src/sanitize.ts
29
+ 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"]);
30
+ var DROP_TAGS = /* @__PURE__ */ new Set(["script", "style", "iframe", "object", "embed", "form", "input", "textarea", "select", "link", "meta", "base", "template", "svg", "math", "frame", "frameset", "noscript"]);
31
+ var ALLOWED_ATTRS = /* @__PURE__ */ new Set(["href", "src", "alt", "title", "target", "rel", "class", "data-g-action", "data-g-value"]);
32
+ function isSafeUrl(value, attr, tag) {
33
+ const v = value.trim().replace(/[\u0000-\u001f\s]/g, "").toLowerCase();
34
+ if (!v) return false;
35
+ const m = /^([a-z][a-z0-9+.-]*):/.exec(v);
36
+ if (!m) return true;
37
+ const scheme = m[1];
38
+ if (scheme === "data") return tag === "img" && attr === "src" && v.startsWith("data:image/");
39
+ return scheme === "http" || scheme === "https" || scheme === "mailto" || scheme === "tel";
40
+ }
41
+ function sanitizeElement(el2) {
42
+ const tag = el2.tagName.toLowerCase();
43
+ for (const attr of Array.from(el2.attributes)) {
44
+ const name = attr.name.toLowerCase();
45
+ if (name.startsWith("on") || !ALLOWED_ATTRS.has(name)) {
46
+ el2.removeAttribute(attr.name);
47
+ continue;
48
+ }
49
+ if ((name === "href" || name === "src") && !isSafeUrl(attr.value, name, tag)) el2.removeAttribute(attr.name);
50
+ }
51
+ if (tag === "a") {
52
+ const target = el2.getAttribute("target");
53
+ if (target && target !== "_blank" && target !== "_self") el2.removeAttribute("target");
54
+ if (el2.getAttribute("target") === "_blank") el2.setAttribute("rel", "noopener noreferrer");
55
+ }
56
+ if (tag === "button") el2.setAttribute("type", "button");
57
+ if (tag === "img") {
58
+ el2.setAttribute("loading", "lazy");
59
+ if (!el2.getAttribute("alt")) el2.setAttribute("alt", "");
60
+ }
61
+ }
62
+ function walk(node) {
63
+ const children = Array.from(node.childNodes);
64
+ for (const child of children) {
65
+ if (child.nodeType === 8) {
66
+ node.removeChild(child);
67
+ continue;
68
+ }
69
+ if (child.nodeType !== 1) continue;
70
+ const el2 = child;
71
+ const tag = el2.tagName.toLowerCase();
72
+ if (DROP_TAGS.has(tag)) {
73
+ node.removeChild(el2);
74
+ continue;
75
+ }
76
+ if (!ALLOWED_TAGS.has(tag)) {
77
+ while (el2.firstChild) node.insertBefore(el2.firstChild, el2);
78
+ node.removeChild(el2);
79
+ walk(node);
80
+ return;
81
+ }
82
+ sanitizeElement(el2);
83
+ walk(el2);
84
+ }
85
+ }
86
+ function sanitizeToFragment(html, doc = document) {
87
+ const tpl = doc.createElement("template");
88
+ tpl.innerHTML = html || "";
89
+ walk(tpl.content);
90
+ return tpl.content;
91
+ }
92
+ function setSanitizedHtml(el2, html) {
93
+ el2.textContent = "";
94
+ el2.appendChild(sanitizeToFragment(html, el2.ownerDocument));
95
+ }
96
+
97
+ // src/help-view.ts
98
+ function el(doc, tag, cls, text) {
99
+ const e = doc.createElement(tag);
100
+ if (cls) e.className = cls;
101
+ if (text != null) e.textContent = text;
102
+ return e;
103
+ }
104
+ function listItem(doc, icon, title, summary) {
105
+ const b = el(doc, "button", "g-rc-item");
106
+ b.type = "button";
107
+ const i = el(doc, "span", "g-rc-icon", icon);
108
+ i.setAttribute("aria-hidden", "true");
109
+ const text = el(doc, "span");
110
+ text.appendChild(el(doc, "div", "g-rc-title", title));
111
+ if (summary) text.appendChild(el(doc, "div", "g-rc-sum", summary));
112
+ b.append(i, text);
113
+ return b;
114
+ }
115
+ function createHelpView(options) {
116
+ const { doc } = options;
117
+ let query = "";
118
+ let serverResults = [];
119
+ let searchSeq = 0;
120
+ let openSeq = 0;
121
+ let active = null;
122
+ const root = el(doc, "div", "g-help");
123
+ const searchWrap = el(doc, "div", "g-rc-search");
124
+ const input = el(doc, "input", "g-input");
125
+ input.type = "search";
126
+ input.placeholder = "Search help articles\u2026";
127
+ input.setAttribute("aria-label", "Search help articles");
128
+ searchWrap.appendChild(input);
129
+ const body = el(doc, "div", "g-panel-body");
130
+ root.append(searchWrap, body);
131
+ const hasBody = (a) => typeof a.body === "string" && !a.bodyOmitted;
132
+ const openArticle = (article) => {
133
+ if (article.externalUrl) {
134
+ window.open(article.externalUrl, "_blank", "noopener,noreferrer");
135
+ if (hasBody(article)) options.onArticleView?.(article);
136
+ return;
137
+ }
138
+ const seq = ++openSeq;
139
+ if (hasBody(article) || !options.loadArticle) {
140
+ active = { article, loading: false };
141
+ if (hasBody(article)) options.onArticleView?.(article);
142
+ return render();
143
+ }
144
+ active = { article, loading: true };
145
+ render();
146
+ options.loadArticle(article.id).then((full) => {
147
+ if (seq !== openSeq || !active) return;
148
+ active = { article: full ?? { ...article, body: "" }, loading: false };
149
+ if (full) options.onArticleView?.(full);
150
+ render();
151
+ }).catch(() => {
152
+ if (seq !== openSeq || !active) return;
153
+ active = { article: { ...article, body: "" }, loading: false };
154
+ render();
155
+ });
156
+ };
157
+ const renderArticle = ({ article, loading }) => {
158
+ const wrap = el(doc, "div", "g-rc-article");
159
+ const back = el(doc, "button", "g-btn g-btn-link g-rc-back", "\u2190 Back");
160
+ back.type = "button";
161
+ back.addEventListener("click", () => {
162
+ openSeq++;
163
+ active = null;
164
+ render();
165
+ });
166
+ const title = el(doc, "h3", "g-title", article.title);
167
+ const content = el(doc, "div", "g-body");
168
+ if (loading) content.appendChild(el(doc, "div", "g-rc-empty", "Loading\u2026"));
169
+ else if (article.body) setSanitizedHtml(content, article.body);
170
+ else content.appendChild(el(doc, "div", "g-rc-empty", "This article couldn't be loaded."));
171
+ wrap.append(back, title, content);
172
+ body.appendChild(wrap);
173
+ back.focus?.({ preventScroll: true });
174
+ };
175
+ const section = (label) => body.appendChild(el(doc, "div", "g-rc-section", label));
176
+ const articleButton = (a) => {
177
+ const b = listItem(doc, "\u{1F4C4}", a.title, a.summary);
178
+ b.addEventListener("click", () => openArticle(a));
179
+ body.appendChild(b);
180
+ };
181
+ function render() {
182
+ body.textContent = "";
183
+ if (active) return renderArticle(active);
184
+ const data = options.data();
185
+ const q = query.trim().toLowerCase();
186
+ if (q) {
187
+ const local = data.articles.filter((a) => a.title.toLowerCase().includes(q) || a.summary.toLowerCase().includes(q));
188
+ const seen = new Set(local.map((a) => a.id));
189
+ const merged = [...local, ...serverResults.filter((a) => !seen.has(a.id))];
190
+ section(`Results (${merged.length})`);
191
+ if (!merged.length) body.appendChild(el(doc, "div", "g-rc-empty", "No articles match your search."));
192
+ merged.forEach(articleButton);
193
+ return;
194
+ }
195
+ if (data.recommended?.length) {
196
+ section("Recommended");
197
+ data.recommended.forEach((g) => {
198
+ const b = listItem(doc, "\u25B6", g.name, "Start guide");
199
+ b.addEventListener("click", () => options.onStartGuide?.(g.id));
200
+ body.appendChild(b);
201
+ });
202
+ }
203
+ if (data.checklists?.length) {
204
+ section("Checklists");
205
+ data.checklists.forEach((g) => {
206
+ const b = listItem(doc, "\u2611", g.name, "Open checklist");
207
+ b.addEventListener("click", () => options.onOpenChecklist?.(g.id));
208
+ body.appendChild(b);
209
+ });
210
+ }
211
+ const collections = [...data.collections].sort((a, b) => a.order - b.order);
212
+ const uncollected = data.articles.filter((a) => !a.collectionId || !collections.some((c) => c.id === a.collectionId));
213
+ collections.forEach((c) => {
214
+ const articles = data.articles.filter((a) => a.collectionId === c.id);
215
+ if (!articles.length) return;
216
+ section(c.name);
217
+ articles.forEach(articleButton);
218
+ });
219
+ if (uncollected.length) {
220
+ section("Articles");
221
+ uncollected.forEach(articleButton);
222
+ }
223
+ if (data.faqs?.length) {
224
+ section("FAQ");
225
+ data.faqs.forEach((f) => {
226
+ const b = listItem(doc, "?", f.question);
227
+ b.setAttribute("aria-expanded", "false");
228
+ const a = el(doc, "div", "g-faq-a");
229
+ setSanitizedHtml(a, f.answer);
230
+ a.hidden = true;
231
+ b.addEventListener("click", () => {
232
+ a.hidden = !a.hidden;
233
+ b.setAttribute("aria-expanded", String(!a.hidden));
234
+ });
235
+ body.append(b, a);
236
+ });
237
+ }
238
+ if (data.links?.length) {
239
+ section("Links");
240
+ data.links.forEach((l) => {
241
+ const a = el(doc, "a", "g-rc-item");
242
+ a.href = l.url;
243
+ a.target = "_blank";
244
+ a.rel = "noopener noreferrer";
245
+ const i = el(doc, "span", "g-rc-icon", "\u2197");
246
+ i.setAttribute("aria-hidden", "true");
247
+ a.append(i, el(doc, "span", "g-rc-title", l.label));
248
+ body.appendChild(a);
249
+ });
250
+ }
251
+ if (!body.childNodes.length) body.appendChild(el(doc, "div", "g-rc-empty", "Nothing here yet."));
252
+ }
253
+ let debounce = null;
254
+ const onInput = () => {
255
+ query = input.value;
256
+ openSeq++;
257
+ active = null;
258
+ render();
259
+ if (debounce) clearTimeout(debounce);
260
+ if (!options.search || !query.trim()) {
261
+ serverResults = [];
262
+ return;
263
+ }
264
+ const seq = ++searchSeq;
265
+ debounce = setTimeout(() => {
266
+ options.search?.(query.trim()).then((results) => {
267
+ if (seq !== searchSeq) return;
268
+ serverResults = Array.isArray(results) ? results : [];
269
+ render();
270
+ }).catch(() => {
271
+ });
272
+ }, 250);
273
+ };
274
+ input.addEventListener("input", onInput);
275
+ return {
276
+ el: root,
277
+ render,
278
+ reset() {
279
+ openSeq++;
280
+ active = null;
281
+ query = "";
282
+ input.value = "";
283
+ serverResults = [];
284
+ },
285
+ openArticle(id) {
286
+ const listed = options.data().articles.find((a) => a.id === id) ?? { id, slug: id, title: "Article", summary: "", updatedAt: "", bodyOmitted: true };
287
+ openArticle(listed);
288
+ },
289
+ focusSearch: () => input.focus?.({ preventScroll: true }),
290
+ setQuery(q) {
291
+ input.value = q;
292
+ onInput();
293
+ },
294
+ destroy() {
295
+ if (debounce) clearTimeout(debounce);
296
+ input.removeEventListener("input", onInput);
297
+ root.remove();
298
+ }
299
+ };
300
+ }
301
+
302
+ // src/messenger-styles.ts
303
+ var MESSENGER_STYLES = `
304
+ :host([data-g-messenger="bottom-right"]) .g-launcher.g-pos-bottom-right{right:72px}
305
+ :host([data-g-messenger="bottom-left"]) .g-launcher.g-pos-bottom-left{left:72px}
306
+ .g-m-launcher{position:fixed;z-index:calc(var(--g-z) + 1);pointer-events:auto;width:56px;height:56px;margin:16px;border-radius:999px;border:0;background:var(--g-primary);color:var(--g-primary-text);cursor:pointer;box-shadow:var(--g-shadow);display:inline-flex;align-items:center;justify-content:center;padding:0;transition:transform .15s}
307
+ .g-m-launcher:hover{transform:scale(1.05)}
308
+ .g-m-launcher:focus-visible{outline:2px solid var(--g-primary);outline-offset:3px}
309
+ .g-m-launcher svg{width:26px;height:26px;fill:currentColor}
310
+ .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)}
311
+ .g-messenger{bottom:76px;height:min(640px,calc(100vh - 110px))}
312
+ .g-m-head{padding:16px 48px 14px 20px;background:var(--g-primary);color:var(--g-primary-text);position:relative;display:flex;align-items:center;gap:8px;min-height:56px}
313
+ .g-m-head .g-close{color:inherit;top:50%;transform:translateY(-50%)}
314
+ .g-m-head .g-close:hover,.g-m-head .g-close:focus-visible{background:rgba(255,255,255,.18);color:inherit}
315
+ .g-m-head h2{margin:0;font-size:16px;font-weight:600;flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
316
+ .g-m-back{border:0;background:transparent;color:inherit;font:inherit;font-size:18px;cursor:pointer;width:32px;height:32px;border-radius:999px;margin-left:-8px}
317
+ .g-m-back:hover,.g-m-back:focus-visible{background:rgba(255,255,255,.18);outline:none}
318
+ .g-m-main{flex:1;min-height:0;display:flex;flex-direction:column;overflow:hidden}
319
+ .g-m-scroll{flex:1;overflow:auto;padding:16px}
320
+ .g-m-hero{font-size:22px;font-weight:700;margin:4px 0 4px}
321
+ .g-m-greet{color:var(--g-muted);margin:0 0 16px}
322
+ .g-m-card{display:block;width:100%;text-align:left;border:1px solid var(--g-border);background:var(--g-bg);color:var(--g-text);border-radius:calc(var(--g-radius) * .8);padding:14px 16px;margin:0 0 12px;font:inherit;cursor:pointer;box-shadow:0 1px 2px rgba(0,0,0,.04)}
323
+ .g-m-card:hover{background:var(--g-subtle)}
324
+ .g-m-card:focus-visible{outline:2px solid var(--g-primary);outline-offset:2px}
325
+ .g-m-card-title{font-weight:600}
326
+ .g-m-card-sub{color:var(--g-muted);font-size:13px;margin-top:2px}
327
+ .g-m-row{display:flex;align-items:flex-start;gap:10px}
328
+ .g-m-row .g-m-grow{flex:1;min-width:0}
329
+ .g-m-ellipsis{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
330
+ .g-m-time{color:var(--g-muted);font-size:12px;flex:none}
331
+ .g-m-dot{width:8px;height:8px;border-radius:999px;background:var(--g-primary);flex:none;margin-top:6px}
332
+ .g-m-tabs{display:flex;border-top:1px solid var(--g-border);background:var(--g-bg)}
333
+ .g-m-tab{flex:1;border:0;background:transparent;color:var(--g-muted);font:inherit;font-size:12px;font-weight:600;padding:10px 4px 12px;cursor:pointer;display:flex;flex-direction:column;align-items:center;gap:2px}
334
+ .g-m-tab[aria-selected="true"]{color:var(--g-primary)}
335
+ .g-m-tab:focus-visible{outline:2px solid var(--g-primary);outline-offset:-2px}
336
+ .g-m-tab-icon{font-size:18px;line-height:1}
337
+ .g-m-log{flex:1;overflow:auto;padding:16px;display:flex;flex-direction:column;gap:10px}
338
+ .g-m-msg{max-width:85%;display:flex;flex-direction:column;gap:4px}
339
+ .g-m-msg.g-from-user{align-self:flex-end;align-items:flex-end}
340
+ .g-m-msg.g-from-system{align-self:center;max-width:95%;text-align:center;color:var(--g-muted);font-size:12px}
341
+ .g-m-author{font-size:12px;color:var(--g-muted);display:flex;align-items:center;gap:6px}
342
+ .g-m-ai{font-size:10px;font-weight:700;letter-spacing:.04em;padding:1px 5px;border-radius:4px;background:var(--g-subtle);color:var(--g-muted)}
343
+ .g-m-bubble{padding:10px 12px;border-radius:14px;background:var(--g-subtle);color:var(--g-text);overflow-wrap:anywhere}
344
+ .g-from-user .g-m-bubble{background:var(--g-primary);color:var(--g-primary-text)}
345
+ .g-from-user .g-m-bubble a{color:inherit}
346
+ .g-m-bubble p{margin:0 0 8px}.g-m-bubble p:last-child{margin:0}
347
+ .g-m-bubble ul,.g-m-bubble ol{margin:4px 0 8px;padding-left:20px}
348
+ .g-m-bubble code{font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:12px;background:color-mix(in srgb,var(--g-text) 8%,transparent);padding:1px 4px;border-radius:4px}
349
+ .g-m-bubble a{color:var(--g-primary)}
350
+ .g-m-failed .g-m-bubble{opacity:.7;font-style:italic}
351
+ .g-m-typing{display:inline-flex;gap:4px;padding:4px 0}
352
+ .g-m-typing span{width:6px;height:6px;border-radius:999px;background:var(--g-muted);animation:g-m-bounce 1s infinite}
353
+ .g-m-typing span:nth-child(2){animation-delay:.15s}.g-m-typing span:nth-child(3){animation-delay:.3s}
354
+ @keyframes g-m-bounce{0%,60%,100%{opacity:.3;transform:translateY(0)}30%{opacity:1;transform:translateY(-3px)}}
355
+ @media (prefers-reduced-motion: reduce){.g-m-typing span{animation:none}.g-m-launcher{transition:none}}
356
+ .g-m-chips{display:flex;flex-direction:column;gap:6px;margin-top:2px}
357
+ .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}
358
+ .g-m-chip:hover{background:var(--g-subtle)}
359
+ .g-m-chip:focus-visible{outline:2px solid var(--g-primary);outline-offset:2px}
360
+ .g-m-chip.g-m-guide{border-color:var(--g-primary);color:var(--g-primary);font-weight:600}
361
+ .g-m-foot{border-top:1px solid var(--g-border);padding:10px 12px 12px;background:var(--g-bg)}
362
+ .g-m-compose{display:flex;align-items:flex-end;gap:8px}
363
+ .g-m-compose textarea{flex:1;resize:none;min-height:40px;max-height:120px;border:1px solid var(--g-border);border-radius:10px;padding:9px 10px;font:inherit;font-size:14px;color:var(--g-text);background:var(--g-bg)}
364
+ .g-m-compose textarea:focus-visible{outline:2px solid var(--g-primary);outline-offset:1px}
365
+ .g-m-send{width:40px;height:40px;border-radius:999px;border:0;background:var(--g-primary);color:var(--g-primary-text);cursor:pointer;font-size:18px;flex:none}
366
+ .g-m-send:disabled{opacity:.45;cursor:not-allowed}
367
+ .g-m-send:focus-visible{outline:2px solid var(--g-primary);outline-offset:2px}
368
+ .g-m-human{border:0;background:transparent;color:var(--g-muted);font:inherit;font-size:12px;cursor:pointer;padding:6px 2px 0;text-decoration:underline}
369
+ .g-m-human:hover{color:var(--g-text)}
370
+ .g-m-rate{text-align:center;padding:4px 0 2px;font-size:13px;color:var(--g-muted)}
371
+ .g-m-rate button{border:0;background:transparent;font-size:22px;cursor:pointer;padding:4px 8px;border-radius:8px}
372
+ .g-m-rate button:hover,.g-m-rate button:focus-visible{background:var(--g-subtle);outline:none}
373
+ .g-m-note{color:var(--g-muted);font-size:12px;margin-top:6px}
374
+ .g-m-empty{color:var(--g-muted);text-align:center;padding:24px 8px}
375
+ `;
376
+
377
+ // src/render-safe.ts
378
+ var CITATION = /\s*\[\[article:[^\]]*\]\]/g;
379
+ var INLINE = /\*\*([^*\n]+)\*\*|`([^`\n]+)`|(!?)\[([^\]\n]{1,200})\]\((https?:\/\/[^\s)]+)\)|(https?:\/\/[^\s<>()"']+[^\s<>().,;:!?'"])/g;
380
+ function stripCitationMarkers(text) {
381
+ return text.replace(CITATION, "");
382
+ }
383
+ function safeHttpUrl(raw) {
384
+ try {
385
+ const u = new URL(raw);
386
+ return u.protocol === "http:" || u.protocol === "https:" ? u.href : null;
387
+ } catch {
388
+ return null;
389
+ }
390
+ }
391
+ function link(doc, label, href) {
392
+ const url = safeHttpUrl(href);
393
+ if (!url) return doc.createTextNode(label);
394
+ const a = doc.createElement("a");
395
+ a.href = url;
396
+ a.target = "_blank";
397
+ a.rel = "noopener noreferrer nofollow";
398
+ a.textContent = label;
399
+ return a;
400
+ }
401
+ function appendInline(doc, parent, text) {
402
+ let last = 0;
403
+ for (const m of text.matchAll(INLINE)) {
404
+ const start = m.index ?? 0;
405
+ if (start > last) parent.appendChild(doc.createTextNode(text.slice(last, start)));
406
+ if (m[1] !== void 0) {
407
+ const b = doc.createElement("strong");
408
+ b.textContent = m[1];
409
+ parent.appendChild(b);
410
+ } else if (m[2] !== void 0) {
411
+ const c = doc.createElement("code");
412
+ c.textContent = m[2];
413
+ parent.appendChild(c);
414
+ } else if (m[4] !== void 0 && m[5] !== void 0) {
415
+ parent.appendChild(m[3] ? doc.createTextNode(m[4]) : link(doc, m[4], m[5]));
416
+ } else if (m[6] !== void 0) {
417
+ parent.appendChild(link(doc, m[6], m[6]));
418
+ }
419
+ last = start + m[0].length;
420
+ }
421
+ if (last < text.length) parent.appendChild(doc.createTextNode(text.slice(last)));
422
+ }
423
+ var BULLET = /^\s*[-*•]\s+/;
424
+ var NUMBERED = /^\s*\d{1,3}[.)]\s+/;
425
+ function renderSafeText(doc, text) {
426
+ const frag = doc.createDocumentFragment();
427
+ const blocks = stripCitationMarkers(text).replace(/\r\n?/g, "\n").trim().split(/\n{2,}/);
428
+ for (const block of blocks) {
429
+ if (!block.trim()) continue;
430
+ const lines = block.split("\n");
431
+ if (lines.every((l) => BULLET.test(l)) || lines.every((l) => NUMBERED.test(l))) {
432
+ const ordered = NUMBERED.test(lines[0]);
433
+ const list = doc.createElement(ordered ? "ol" : "ul");
434
+ for (const line of lines) {
435
+ const li = doc.createElement("li");
436
+ appendInline(doc, li, line.replace(ordered ? NUMBERED : BULLET, ""));
437
+ list.appendChild(li);
438
+ }
439
+ frag.appendChild(list);
440
+ continue;
441
+ }
442
+ const p = doc.createElement("p");
443
+ lines.forEach((line, i) => {
444
+ if (i) p.appendChild(doc.createElement("br"));
445
+ appendInline(doc, p, line);
446
+ });
447
+ frag.appendChild(p);
448
+ }
449
+ return frag;
450
+ }
451
+
452
+ // src/messenger.ts
453
+ var SVG_NS = "http://www.w3.org/2000/svg";
454
+ function chatIcon(doc) {
455
+ const svg = doc.createElementNS(SVG_NS, "svg");
456
+ svg.setAttribute("viewBox", "0 0 24 24");
457
+ svg.setAttribute("aria-hidden", "true");
458
+ const path = doc.createElementNS(SVG_NS, "path");
459
+ 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");
460
+ svg.appendChild(path);
461
+ return svg;
462
+ }
463
+ function relativeTime(iso, now = Date.now()) {
464
+ const s = Math.max(0, Math.round((now - Date.parse(iso)) / 1e3));
465
+ if (!Number.isFinite(s) || s < 60) return "now";
466
+ if (s < 3600) return `${Math.floor(s / 60)}m`;
467
+ if (s < 86400) return `${Math.floor(s / 3600)}h`;
468
+ return `${Math.floor(s / 86400)}d`;
469
+ }
470
+ function ensureStyles(scope) {
471
+ const root = scope.getRootNode();
472
+ const container = "host" in root ? root : scope.ownerDocument.head;
473
+ if (container.querySelector?.("style[data-g-messenger]")) return;
474
+ const style = scope.ownerDocument.createElement("style");
475
+ style.setAttribute("data-g-messenger", "");
476
+ style.textContent = MESSENGER_STYLES;
477
+ container.appendChild(style);
478
+ }
479
+ function renderMessenger(options) {
480
+ const { scope, config, api } = options;
481
+ const doc = scope.ownerDocument;
482
+ const poll = options.pollMs ?? { thread: 5e3, background: 6e4 };
483
+ ensureStyles(scope);
484
+ let open = false;
485
+ let destroyed = false;
486
+ let view = { kind: "tab", tab: "home" };
487
+ let conversations = [];
488
+ let messages = [];
489
+ let current = null;
490
+ const streaming = /* @__PURE__ */ new Map();
491
+ let sending = false;
492
+ let draft = "";
493
+ let articles = null;
494
+ let articlesLoading = false;
495
+ let threadTimer = null;
496
+ let backgroundTimer = null;
497
+ let threadSeq = 0;
498
+ const pos = `g-pos-${config.launcher.position}`;
499
+ const rootNode = scope.getRootNode();
500
+ const shadowHost = "host" in rootNode ? rootNode.host : null;
501
+ shadowHost?.setAttribute("data-g-messenger", config.launcher.position);
502
+ const root = el(doc, "div", "g-messenger-root");
503
+ const launcher = el(doc, "button", `g-m-launcher ${pos}`);
504
+ launcher.type = "button";
505
+ launcher.setAttribute("aria-label", `Open ${config.name}`);
506
+ launcher.setAttribute("aria-expanded", "false");
507
+ launcher.appendChild(chatIcon(doc));
508
+ const badge = el(doc, "span", "g-m-badge");
509
+ badge.hidden = true;
510
+ launcher.appendChild(badge);
511
+ if (options.hideLauncher) launcher.hidden = true;
512
+ const panel = el(doc, "div", `g-panel g-messenger ${pos}`);
513
+ panel.setAttribute("role", "dialog");
514
+ panel.setAttribute("aria-label", config.name);
515
+ panel.hidden = true;
516
+ const head = el(doc, "div", "g-m-head");
517
+ const main = el(doc, "div", "g-m-main");
518
+ const tabs = el(doc, "div", "g-m-tabs");
519
+ tabs.setAttribute("role", "tablist");
520
+ panel.append(head, main, tabs);
521
+ root.append(launcher, panel);
522
+ scope.appendChild(root);
523
+ const rcExtras = (ids = []) => ids.map((id) => options.guides?.find((g) => g.id === id || g.key === id)).filter((g) => !!g);
524
+ const help = createHelpView({
525
+ doc,
526
+ data: () => ({
527
+ collections: articles?.collections ?? options.resourceCenter?.collections ?? [],
528
+ articles: articles?.articles ?? options.resourceCenter?.articles ?? [],
529
+ recommended: rcExtras(options.resourceCenter?.recommendedGuideIds),
530
+ checklists: rcExtras(options.resourceCenter?.checklistGuideIds),
531
+ faqs: options.resourceCenter?.faqs,
532
+ links: options.resourceCenter?.links
533
+ }),
534
+ search: (q) => api.searchArticles(q),
535
+ loadArticle: (id) => api.getArticle(id),
536
+ onStartGuide: (id) => {
537
+ setOpen(false);
538
+ options.onStartGuide?.(id);
539
+ },
540
+ onOpenChecklist: (id) => {
541
+ setOpen(false);
542
+ options.onOpenChecklist?.(id);
543
+ },
544
+ onArticleView: options.onArticleView
545
+ });
546
+ const unreadTotal = () => conversations.reduce((n, c) => n + (c.unread > 0 ? 1 : 0), 0);
547
+ const updateBadge = () => {
548
+ const n = unreadTotal();
549
+ badge.hidden = n === 0 || open;
550
+ badge.textContent = n > 9 ? "9+" : String(n);
551
+ launcher.setAttribute("aria-label", n ? `Open ${config.name}, ${n} unread` : `Open ${config.name}`);
552
+ };
553
+ const upsertConversation = (c) => {
554
+ conversations = [c, ...conversations.filter((x) => x.id !== c.id)].sort((a, b) => b.lastMessageAt.localeCompare(a.lastMessageAt));
555
+ if (current?.id === c.id) current = c;
556
+ updateBadge();
557
+ };
558
+ const upsertMessage = (m) => {
559
+ const i = messages.findIndex((x) => x.id === m.id);
560
+ if (i >= 0) messages[i] = m;
561
+ else messages.push(m);
562
+ messages.sort((a, b) => a.createdAt.localeCompare(b.createdAt));
563
+ if (m.status !== "pending") streaming.delete(m.id);
564
+ };
565
+ const loadArticles = () => {
566
+ if (articles || articlesLoading) return;
567
+ articlesLoading = true;
568
+ api.listArticles().then((res) => {
569
+ articles = res;
570
+ if (view.kind === "tab" && view.tab === "help") help.render();
571
+ }).catch(() => {
572
+ }).finally(() => articlesLoading = false);
573
+ };
574
+ const refreshConversations = () => api.listConversations().then((list) => {
575
+ conversations = [...list].sort((a, b) => b.lastMessageAt.localeCompare(a.lastMessageAt));
576
+ updateBadge();
577
+ if (open && view.kind === "tab" && view.tab !== "help") render();
578
+ }).catch(() => {
579
+ });
580
+ function renderHead(title, back) {
581
+ head.textContent = "";
582
+ if (back) {
583
+ const b = el(doc, "button", "g-m-back", "\u2190");
584
+ b.type = "button";
585
+ b.setAttribute("aria-label", "Back");
586
+ b.addEventListener("click", back);
587
+ head.appendChild(b);
588
+ }
589
+ head.appendChild(el(doc, "h2", "", title));
590
+ const close = el(doc, "button", "g-close", "\xD7");
591
+ close.type = "button";
592
+ close.setAttribute("aria-label", "Close");
593
+ close.addEventListener("click", () => setOpen(false));
594
+ head.appendChild(close);
595
+ }
596
+ function renderTabs() {
597
+ tabs.textContent = "";
598
+ tabs.hidden = view.kind === "thread";
599
+ const items = [
600
+ ["home", "\u2302", "Home"],
601
+ ["messages", "\u{1F4AC}", "Messages"],
602
+ ["help", "?", "Help"]
603
+ ];
604
+ for (const [tab, icon, label] of items) {
605
+ const b = el(doc, "button", "g-m-tab");
606
+ b.type = "button";
607
+ b.setAttribute("role", "tab");
608
+ b.setAttribute("aria-selected", String(view.kind === "tab" && view.tab === tab));
609
+ const i = el(doc, "span", "g-m-tab-icon", icon);
610
+ i.setAttribute("aria-hidden", "true");
611
+ b.append(i, el(doc, "span", "", label));
612
+ b.addEventListener("click", () => go({ kind: "tab", tab }));
613
+ tabs.appendChild(b);
614
+ }
615
+ }
616
+ function conversationCard(c) {
617
+ const card = el(doc, "button", "g-m-card");
618
+ card.type = "button";
619
+ const row = el(doc, "div", "g-m-row");
620
+ if (c.unread > 0) row.appendChild(el(doc, "span", "g-m-dot"));
621
+ const text = el(doc, "div", "g-m-grow");
622
+ text.append(el(doc, "div", "g-m-card-title g-m-ellipsis", c.subject || "Conversation"), el(doc, "div", "g-m-card-sub g-m-ellipsis", c.lastMessagePreview));
623
+ row.append(text, el(doc, "span", "g-m-time", relativeTime(c.lastMessageAt)));
624
+ card.appendChild(row);
625
+ card.setAttribute("aria-label", `${c.subject || "Conversation"}${c.unread ? ", unread" : ""}`);
626
+ card.addEventListener("click", () => go({ kind: "thread", conversationId: c.id }));
627
+ return card;
628
+ }
629
+ function renderHome() {
630
+ renderHead(config.name);
631
+ const scroll = el(doc, "div", "g-m-scroll");
632
+ scroll.append(el(doc, "div", "g-m-hero", "Hi there \u{1F44B}"), el(doc, "p", "g-m-greet", config.greeting));
633
+ const ask = el(doc, "button", "g-m-card");
634
+ ask.type = "button";
635
+ ask.append(el(doc, "div", "g-m-card-title", "Ask a question"), el(doc, "div", "g-m-card-sub", config.aiAvailable ? `${config.name} answers right away, and the team can help too` : "The team will reply here"));
636
+ ask.addEventListener("click", () => go({ kind: "thread", conversationId: null }));
637
+ scroll.appendChild(ask);
638
+ const recent = conversations[0];
639
+ if (recent) {
640
+ scroll.appendChild(el(doc, "div", "g-rc-section", "Recent conversation"));
641
+ scroll.appendChild(conversationCard(recent));
642
+ }
643
+ const search = el(doc, "button", "g-m-card");
644
+ search.type = "button";
645
+ search.append(el(doc, "div", "g-m-card-title", "Search for help"), el(doc, "div", "g-m-card-sub", "Browse articles and guides"));
646
+ search.addEventListener("click", () => go({ kind: "tab", tab: "help" }));
647
+ scroll.appendChild(search);
648
+ main.appendChild(scroll);
649
+ }
650
+ function renderMessagesTab() {
651
+ renderHead("Messages");
652
+ const scroll = el(doc, "div", "g-m-scroll");
653
+ if (!conversations.length) scroll.appendChild(el(doc, "div", "g-m-empty", "No conversations yet."));
654
+ conversations.forEach((c) => scroll.appendChild(conversationCard(c)));
655
+ const start = el(doc, "button", "g-btn g-btn-primary", "Send us a message");
656
+ start.type = "button";
657
+ start.style.width = "100%";
658
+ start.addEventListener("click", () => go({ kind: "thread", conversationId: null }));
659
+ scroll.appendChild(start);
660
+ main.appendChild(scroll);
661
+ }
662
+ function renderHelpTab() {
663
+ renderHead("Help");
664
+ loadArticles();
665
+ help.render();
666
+ main.appendChild(help.el);
667
+ }
668
+ let log = null;
669
+ let foot = null;
670
+ let textarea = null;
671
+ let sendButton = null;
672
+ function messageNode(m) {
673
+ const wrap = el(doc, "div", `g-m-msg g-from-${m.author}${m.status === "failed" ? " g-m-failed" : ""}`);
674
+ if (m.author === "system") {
675
+ wrap.textContent = m.body;
676
+ return wrap;
677
+ }
678
+ if (m.author !== "user") {
679
+ const author = el(doc, "div", "g-m-author", m.author === "scout" ? config.name : m.authorName || "Support team");
680
+ if (m.author === "scout") author.appendChild(el(doc, "span", "g-m-ai", "AI"));
681
+ wrap.appendChild(author);
682
+ }
683
+ const bubble = el(doc, "div", "g-m-bubble");
684
+ const live = streaming.get(m.id);
685
+ if (m.status === "pending" && !live) {
686
+ const dots = el(doc, "span", "g-m-typing");
687
+ dots.setAttribute("aria-label", `${config.name} is typing`);
688
+ dots.append(el(doc, "span"), el(doc, "span"), el(doc, "span"));
689
+ bubble.appendChild(dots);
690
+ } else if (m.status === "failed") {
691
+ bubble.textContent = "This reply couldn't be completed.";
692
+ } else {
693
+ bubble.appendChild(renderSafeText(doc, live ?? m.body));
694
+ }
695
+ if (m.status === "pending") bubble.setAttribute("aria-busy", "true");
696
+ bubble.dataset.messageId = m.id;
697
+ wrap.appendChild(bubble);
698
+ if (m.citations.length || m.suggestedGuides.length) {
699
+ const chips = el(doc, "div", "g-m-chips");
700
+ for (const g of m.suggestedGuides) {
701
+ const b = el(doc, "button", "g-m-chip g-m-guide", `\u25B6 Show me: ${g.name}`);
702
+ b.type = "button";
703
+ b.addEventListener("click", () => {
704
+ setOpen(false);
705
+ options.onStartGuide?.(g.key);
706
+ });
707
+ chips.appendChild(b);
708
+ }
709
+ for (const c of m.citations) {
710
+ const b = el(doc, "button", "g-m-chip", `\u{1F4C4} ${c.title}`);
711
+ b.type = "button";
712
+ b.addEventListener("click", () => openArticle(c.articleId));
713
+ chips.appendChild(b);
714
+ }
715
+ wrap.appendChild(chips);
716
+ }
717
+ return wrap;
718
+ }
719
+ function renderLog() {
720
+ if (!log) return;
721
+ const atBottom = log.scrollHeight - log.scrollTop - log.clientHeight < 40;
722
+ log.textContent = "";
723
+ if (!messages.length) {
724
+ const intro = { id: "intro", author: config.aiAvailable ? "scout" : "agent", authorName: "Support team", body: config.greeting, citations: [], suggestedGuides: [], status: "complete", createdAt: (/* @__PURE__ */ new Date()).toISOString() };
725
+ log.appendChild(messageNode(intro));
726
+ }
727
+ messages.forEach((m) => log.appendChild(messageNode(m)));
728
+ if (atBottom || sending) log.scrollTop = log.scrollHeight;
729
+ renderFoot();
730
+ }
731
+ function renderFoot() {
732
+ if (!foot || !textarea || !sendButton) return;
733
+ sendButton.disabled = sending || !textarea.value.trim();
734
+ textarea.disabled = sending;
735
+ for (const extra of Array.from(foot.querySelectorAll(".g-m-human,.g-m-rate,.g-m-note"))) extra.remove();
736
+ if (current?.status === "closed" && current.rating == null) {
737
+ const rate = el(doc, "div", "g-m-rate", "How was this conversation? ");
738
+ for (const [score, face, label] of [
739
+ [1, "\u{1F61E}", "Bad"],
740
+ [3, "\u{1F610}", "OK"],
741
+ [5, "\u{1F600}", "Great"]
742
+ ]) {
743
+ const b = el(doc, "button", "", face);
744
+ b.type = "button";
745
+ b.setAttribute("aria-label", label);
746
+ b.addEventListener("click", () => {
747
+ if (!current) return;
748
+ const id = current.id;
749
+ current = { ...current, rating: score };
750
+ upsertConversation(current);
751
+ renderFoot();
752
+ api.rate(id, score).catch(() => {
753
+ });
754
+ });
755
+ rate.appendChild(b);
756
+ }
757
+ foot.prepend(rate);
758
+ } else if (current && current.rating != null && current.status === "closed") {
759
+ foot.prepend(el(doc, "div", "g-m-note", "Thanks for the feedback!"));
760
+ }
761
+ if (config.handoffEnabled && config.aiAvailable && current?.status === "bot" && !sending) {
762
+ const human = el(doc, "button", "g-m-human", "Talk to a person");
763
+ human.type = "button";
764
+ human.addEventListener("click", () => {
765
+ if (!current) return;
766
+ const id = current.id;
767
+ human.disabled = true;
768
+ api.handoff(id).then((res) => {
769
+ if (current?.id !== id) return;
770
+ upsertConversation(res.conversation);
771
+ res.messages.forEach(upsertMessage);
772
+ renderLog();
773
+ }).catch(() => human.disabled = false);
774
+ });
775
+ foot.appendChild(human);
776
+ }
777
+ }
778
+ function renderThread(conversationId) {
779
+ renderHead(current?.subject || (conversationId ? "Conversation" : "New conversation"), () => go({ kind: "tab", tab: "messages" }));
780
+ log = el(doc, "div", "g-m-log");
781
+ log.setAttribute("role", "log");
782
+ log.setAttribute("aria-live", "polite");
783
+ foot = el(doc, "div", "g-m-foot");
784
+ const compose = el(doc, "form", "g-m-compose");
785
+ textarea = el(doc, "textarea");
786
+ textarea.rows = 1;
787
+ textarea.maxLength = 4e3;
788
+ textarea.placeholder = current?.status === "open" ? "Reply to the team\u2026" : "Ask a question\u2026";
789
+ textarea.setAttribute("aria-label", "Message");
790
+ textarea.value = draft;
791
+ sendButton = el(doc, "button", "g-m-send", "\u2191");
792
+ sendButton.type = "submit";
793
+ sendButton.setAttribute("aria-label", "Send");
794
+ compose.append(textarea, sendButton);
795
+ foot.appendChild(compose);
796
+ textarea.addEventListener("input", () => {
797
+ draft = textarea.value;
798
+ if (sendButton) sendButton.disabled = sending || !draft.trim();
799
+ });
800
+ textarea.addEventListener("keydown", (e) => {
801
+ if (e.key === "Enter" && !e.shiftKey && !e.isComposing) {
802
+ e.preventDefault();
803
+ submit();
804
+ }
805
+ });
806
+ compose.addEventListener("submit", (e) => {
807
+ e.preventDefault();
808
+ submit();
809
+ });
810
+ main.append(log, foot);
811
+ renderLog();
812
+ textarea.focus?.({ preventScroll: true });
813
+ }
814
+ function render() {
815
+ if (destroyed) return;
816
+ main.textContent = "";
817
+ log = foot = null;
818
+ textarea = null;
819
+ sendButton = null;
820
+ if (view.kind === "thread") renderThread(view.conversationId);
821
+ else if (view.tab === "home") renderHome();
822
+ else if (view.tab === "messages") renderMessagesTab();
823
+ else renderHelpTab();
824
+ renderTabs();
825
+ }
826
+ const handlers = (seq) => ({
827
+ onConversation(c) {
828
+ if (seq !== threadSeq) return;
829
+ current = c;
830
+ view = { kind: "thread", conversationId: c.id };
831
+ upsertConversation(c);
832
+ },
833
+ onMessage(m) {
834
+ if (seq !== threadSeq) return;
835
+ upsertMessage(m);
836
+ renderLog();
837
+ },
838
+ onDelta(id, text) {
839
+ if (seq !== threadSeq) return;
840
+ streaming.set(id, (streaming.get(id) ?? "") + text);
841
+ const bubble = log?.querySelector(`[data-message-id="${id}"]`);
842
+ if (!bubble) return renderLog();
843
+ bubble.textContent = "";
844
+ bubble.appendChild(renderSafeText(doc, streaming.get(id)));
845
+ if (log) log.scrollTop = log.scrollHeight;
846
+ },
847
+ onDone(m, c) {
848
+ if (seq !== threadSeq) return;
849
+ if (m) {
850
+ upsertMessage(m);
851
+ options.onMessage?.(m, c);
852
+ }
853
+ upsertConversation({ ...c, unread: 0 });
854
+ current = { ...c, unread: 0 };
855
+ },
856
+ onError() {
857
+ }
858
+ });
859
+ function submit() {
860
+ if (!textarea || sending) return;
861
+ const text = textarea.value.trim();
862
+ if (!text) return;
863
+ sending = true;
864
+ draft = "";
865
+ textarea.value = "";
866
+ renderFoot();
867
+ const seq = threadSeq;
868
+ const h = handlers(seq);
869
+ const done = current ? api.send(current.id, text, h) : api.start(text, h);
870
+ done.catch(() => {
871
+ if (seq !== threadSeq) return;
872
+ draft = text;
873
+ if (textarea) textarea.value = text;
874
+ const note = { id: `local_${Date.now()}`, author: "system", body: "Your message couldn't be sent. Please try again.", citations: [], suggestedGuides: [], status: "complete", createdAt: (/* @__PURE__ */ new Date()).toISOString() };
875
+ upsertMessage(note);
876
+ }).finally(() => {
877
+ if (seq !== threadSeq) return;
878
+ sending = false;
879
+ streaming.clear();
880
+ if (view.kind === "thread") {
881
+ if (current) renderHead(current.subject || "Conversation", () => go({ kind: "tab", tab: "messages" }));
882
+ renderLog();
883
+ }
884
+ scheduleThreadPoll();
885
+ });
886
+ }
887
+ function scheduleThreadPoll() {
888
+ if (threadTimer) clearTimeout(threadTimer);
889
+ threadTimer = null;
890
+ if (!open || view.kind !== "thread" || !current || destroyed) return;
891
+ threadTimer = setTimeout(pollThread, poll.thread);
892
+ }
893
+ function pollThread() {
894
+ if (!current || sending) return scheduleThreadPoll();
895
+ const seq = threadSeq;
896
+ const pending = messages.find((m) => m.status === "pending");
897
+ const last = messages.filter((m) => !m.id.startsWith("local_")).at(-1);
898
+ const since = pending ? new Date(Date.parse(pending.createdAt) - 1).toISOString() : last?.createdAt ?? null;
899
+ api.loadMessages(current.id, since).then((res) => {
900
+ if (seq !== threadSeq) return;
901
+ const before = messages.length;
902
+ const changed = res.messages.some((m) => {
903
+ const prev = messages.find((x) => x.id === m.id);
904
+ return !prev || prev.status !== m.status || prev.body !== m.body;
905
+ });
906
+ res.messages.forEach(upsertMessage);
907
+ upsertConversation({ ...res.conversation, unread: 0 });
908
+ current = { ...res.conversation, unread: 0 };
909
+ if (changed || messages.length !== before) {
910
+ renderLog();
911
+ res.messages.filter((m) => m.author !== "user").forEach((m) => options.onMessage?.(m, res.conversation));
912
+ } else renderFoot();
913
+ }).catch(() => {
914
+ }).finally(scheduleThreadPoll);
915
+ }
916
+ function go(next) {
917
+ threadSeq++;
918
+ if (threadTimer) clearTimeout(threadTimer);
919
+ threadTimer = null;
920
+ if (view.kind === "tab" && view.tab === "help" && !(next.kind === "tab" && next.tab === "help")) help.reset();
921
+ view = next;
922
+ if (next.kind === "thread") {
923
+ messages = [];
924
+ streaming.clear();
925
+ sending = false;
926
+ current = next.conversationId ? conversations.find((c) => c.id === next.conversationId) ?? null : null;
927
+ render();
928
+ if (next.conversationId) {
929
+ const seq = threadSeq;
930
+ api.loadMessages(next.conversationId).then((res) => {
931
+ if (seq !== threadSeq) return;
932
+ res.messages.forEach(upsertMessage);
933
+ current = { ...res.conversation, unread: 0 };
934
+ upsertConversation(current);
935
+ renderHead(current.subject || "Conversation", () => go({ kind: "tab", tab: "messages" }));
936
+ renderLog();
937
+ }).catch(() => {
938
+ }).finally(scheduleThreadPoll);
939
+ }
940
+ } else {
941
+ draft = "";
942
+ render();
943
+ }
944
+ }
945
+ function openArticle(id) {
946
+ go({ kind: "tab", tab: "help" });
947
+ help.openArticle(id);
948
+ }
949
+ function startBackgroundPoll() {
950
+ if (backgroundTimer) clearInterval(backgroundTimer);
951
+ backgroundTimer = setInterval(() => {
952
+ if (!open && conversations.length && !destroyed) void refreshConversations();
953
+ }, poll.background);
954
+ }
955
+ const setOpen = (v) => {
956
+ if (open === v || destroyed) return;
957
+ open = v;
958
+ panel.hidden = !v;
959
+ launcher.setAttribute("aria-expanded", String(v));
960
+ launcher.setAttribute("aria-label", v ? `Close ${config.name}` : `Open ${config.name}`);
961
+ updateBadge();
962
+ if (v) {
963
+ render();
964
+ void refreshConversations();
965
+ if (view.kind === "thread") scheduleThreadPoll();
966
+ options.onOpen?.();
967
+ } else {
968
+ if (threadTimer) clearTimeout(threadTimer);
969
+ threadTimer = null;
970
+ options.onClose?.();
971
+ }
972
+ };
973
+ launcher.addEventListener("click", () => setOpen(!open));
974
+ const onKey = (e) => {
975
+ if (e.key === "Escape") {
976
+ e.stopPropagation();
977
+ setOpen(false);
978
+ launcher.focus();
979
+ }
980
+ };
981
+ panel.addEventListener("keydown", onKey);
982
+ void refreshConversations();
983
+ startBackgroundPoll();
984
+ return {
985
+ el: root,
986
+ open(tab) {
987
+ if (tab) view = { kind: "tab", tab };
988
+ if (open && tab) render();
989
+ setOpen(true);
990
+ },
991
+ close: () => setOpen(false),
992
+ toggle: () => setOpen(!open),
993
+ isOpen: () => open,
994
+ showNewMessage(text) {
995
+ setOpen(true);
996
+ go({ kind: "thread", conversationId: null });
997
+ if (text && textarea) {
998
+ textarea.value = text.slice(0, 4e3);
999
+ draft = textarea.value;
1000
+ renderFoot();
1001
+ }
1002
+ },
1003
+ openArticle(id) {
1004
+ setOpen(true);
1005
+ openArticle(id);
1006
+ },
1007
+ destroy() {
1008
+ destroyed = true;
1009
+ if (threadTimer) clearTimeout(threadTimer);
1010
+ if (backgroundTimer) clearInterval(backgroundTimer);
1011
+ panel.removeEventListener("keydown", onKey);
1012
+ help.destroy();
1013
+ shadowHost?.removeAttribute("data-g-messenger");
1014
+ root.remove();
1015
+ }
1016
+ };
1017
+ }
1018
+ // Annotate the CommonJS export names for ESM import in node:
1019
+ 0 && (module.exports = {
1020
+ relativeTime,
1021
+ renderMessenger
1022
+ });
1023
+ //# sourceMappingURL=messenger.cjs.map