@lightworkai.official/debug-capture 0.19.4 → 0.19.5

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.
@@ -65,6 +65,14 @@ export declare function bubbleLabel(s: {
65
65
  shown: boolean;
66
66
  isNew: boolean;
67
67
  } | null;
68
+ export interface DeskLook {
69
+ label?: string;
70
+ color?: string;
71
+ /** A path on the support host; null: the chat glyph on the colour. */
72
+ faceUrl?: string | null;
73
+ }
74
+ /** The face's address for a desk's look: its own path on the support host, the shipped one, or none (the glyph). Pure. */
75
+ export declare function lookFaceUrl(host: string, look: DeskLook | null): string | null;
68
76
  /** Open the panel — from the bubble, or from the host app's own button (`openSupportChat`). */
69
77
  export declare function openSupportChat(): void;
70
78
  /** Fold the panel — it stays loaded, so the chat is there as it was the next time. */
@@ -72,4 +80,6 @@ export declare function closeSupportChat(refocus?: boolean): void;
72
80
  /** The bubble, once — hidden until the host's token says somebody is signed in. */
73
81
  export declare function mountSupportChat(): void;
74
82
  /** Take it away — for a host that unmounts, and for the tests. */
83
+ /** Ask again whether there is a bubble, and what waits (0.19.5) — after a sign-in or a sign-out, or `show` changed. */
84
+ export declare function refreshSupportChat(): Promise<void>;
75
85
  export declare function unmountSupportChat(): void;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lightworkai.official/debug-capture",
3
- "version": "0.19.4",
3
+ "version": "0.19.5",
4
4
  "description": "Report-a-problem widget: rolling network/console/action capture, optional rrweb session replay, annotated screenshot, and submission to a Lightwork Support host",
5
5
  "type": "module",
6
6
  "module": "./dist/index.mjs",
package/src/config.ts CHANGED
@@ -232,6 +232,12 @@ export interface ChatOptions {
232
232
  * of "แชทกับทีมงาน", or `false` for the button alone.
233
233
  */
234
234
  label?: string | false;
235
+ /**
236
+ * Whether the bubble is for whoever is signed in now (0.19.5) — asked before every read; false: no bubble. For a
237
+ * host whose own staff should not chat with its support desk, or not on every page. Ask again after a sign-in with
238
+ * `refreshSupportChat()`.
239
+ */
240
+ show?: () => boolean;
235
241
  }
236
242
 
237
243
  /** The bubble's place, from `chat`: null when it is off. */
package/src/index.ts CHANGED
@@ -77,7 +77,7 @@ export type { OpenPortalOptions } from "./portal";
77
77
  * แชทกับทีม (0.19): `chat` in the config mounts the bubble on install; a host with its own button opens and closes
78
78
  * the same panel. The panel is the support host's page, framed — as the tickets are, maintained where they live.
79
79
  */
80
- export { openSupportChat, closeSupportChat, mountSupportChat, unmountSupportChat, supportFrameUrl, badgeText, chatLift, panelHeight } from "./ui/chatBubble";
80
+ export { openSupportChat, closeSupportChat, mountSupportChat, unmountSupportChat, refreshSupportChat, supportFrameUrl, badgeText, chatLift, panelHeight } from "./ui/chatBubble";
81
81
  export type { ChatOptions } from "./config";
82
82
 
83
83
  export type * from "./types";
@@ -33,11 +33,11 @@ const CSS = `
33
33
  .bubble {
34
34
  position: fixed; z-index: 2147483000; width: ${SIZE}px; height: ${SIZE}px; border-radius: 50%;
35
35
  display: grid; place-items: center; border: 0; cursor: pointer; padding: 0;
36
- background: #00468f; color: #fff; box-shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.3), 0 4px 10px -4px rgba(0, 0, 0, 0.2);
36
+ background: var(--lw-chat-color, #00468f); color: #fff; box-shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.3), 0 4px 10px -4px rgba(0, 0, 0, 0.2);
37
37
  transition: transform 150ms ease, background 150ms ease;
38
38
  font-family: var(--lw-host-font, ${OWN_FONTS});
39
39
  }
40
- .bubble:hover { background: #003a78; transform: translateY(-1px); }
40
+ .bubble:hover { filter: brightness(0.92); transform: translateY(-1px); }
41
41
  .bubble { touch-action: none; }
42
42
  .bubble.dragging { cursor: grabbing; transform: scale(1.06); transition: none; box-shadow: 0 18px 35px -8px rgba(0, 0, 0, 0.4); }
43
43
  .bubble:focus-visible { outline: 3px solid rgba(0, 70, 143, 0.35); outline-offset: 3px; }
@@ -65,7 +65,7 @@ const CSS = `
65
65
  .label .text { display: block; max-width: min(240px, calc(100vw - ${SIZE + 64}px)); overflow: hidden; text-overflow: ellipsis; }
66
66
  .at-left .label { right: auto; left: calc(100% + 12px); transform: translate(-4px, -50%); }
67
67
  .at-left .label::after { right: auto; left: -4px; }
68
- .new .label { color: #00468f; }
68
+ .new .label { color: var(--lw-chat-color, #00468f); }
69
69
  .labelled .label { opacity: 1; visibility: visible; pointer-events: auto; transform: translate(0, -50%); }
70
70
  @media (hover: hover) { .bubble:hover .label { opacity: 1; visibility: visible; pointer-events: auto; transform: translate(0, -50%); } }
71
71
  .unlabelled .label, .bubble.dragging .label, .open .label { display: none; }
@@ -207,7 +207,9 @@ let opened = false;
207
207
  function setLabel(): void {
208
208
  if (!label || !bubble) return;
209
209
  const config = getConfig();
210
- const own = config.chat && config.chat !== true ? config.chat.label : undefined;
210
+ // The host's own words in code win; else the desk's (ปุ่มแชท, its look); else the shipped ones.
211
+ const hostOwn = config.chat && config.chat !== true ? config.chat.label : undefined;
212
+ const own = hostOwn !== undefined ? hostOwn : deskLook?.label;
211
213
  const l = bubbleLabel({ own, unread, opened, open: isOpen }, strings(config.locale));
212
214
  bubble.classList.toggle("unlabelled", !l);
213
215
  bubble.classList.toggle("labelled", !!l?.shown);
@@ -216,28 +218,83 @@ function setLabel(): void {
216
218
  label.querySelector(".text")!.textContent = l?.text ?? "";
217
219
  }
218
220
 
221
+ /*
222
+ * The desk's look (0.19.5, the support desk's ปุ่มแชท — docs/CHAT-BUBBLE-PLAN.md): its face (or the chat glyph on its
223
+ * colour), its colour, the words beside the button — set on the desk, read with the first answer here.
224
+ */
225
+ export interface DeskLook {
226
+ label?: string;
227
+ color?: string;
228
+ /** A path on the support host; null: the chat glyph on the colour. */
229
+ faceUrl?: string | null;
230
+ }
231
+ let deskLook: DeskLook | null = null;
232
+ /** The face's address for a desk's look: its own path on the support host, the shipped one, or none (the glyph). Pure. */
233
+ export function lookFaceUrl(host: string, look: DeskLook | null): string | null {
234
+ if (!look || look.faceUrl === undefined) return supportFaceUrl(host);
235
+ if (look.faceUrl === null) return null;
236
+ try {
237
+ return new URL(look.faceUrl.replace(/^\//, ""), base(host)).toString();
238
+ } catch {
239
+ return supportFaceUrl(host);
240
+ }
241
+ }
242
+ function wearLook(look: DeskLook | null): void {
243
+ deskLook = look;
244
+ if (!root || !bubble) return;
245
+ const color = look?.color && /^#[0-9a-f]{6}$/i.test(look.color) ? look.color : null;
246
+ if (color) root.style.setProperty("--lw-chat-color", color);
247
+ else root.style.removeProperty("--lw-chat-color");
248
+ const face = shadow?.querySelector<HTMLImageElement>(".face");
249
+ const url = lookFaceUrl(getConfig().host, look);
250
+ if (face) {
251
+ if (!url) {
252
+ bubble.classList.remove("has-face");
253
+ face.removeAttribute("src");
254
+ } else if (face.getAttribute("src") !== url) {
255
+ bubble.classList.remove("has-face");
256
+ face.src = url;
257
+ }
258
+ }
259
+ setLabel();
260
+ }
261
+
219
262
  let asking: Promise<void> | null = null;
220
263
  /** What waits for the person, read with the host's token — no token: nobody is signed in, and there is no bubble. */
221
264
  async function refreshUnread(): Promise<void> {
222
- asking ??= (async () => {
223
- try {
224
- const token = await hostToken();
225
- if (!token) return void show(false);
226
- const config = getConfig();
227
- const res = await fetch(new URL("ingest/support-unread", base(config.host)).toString(), { headers: { "x-realm-key": config.realmKey, authorization: `Bearer ${token}` } });
228
- // A support host from before the chat, or a token it does not accept: no bubble rather than one that cannot open.
229
- if (res.status === 404 || res.status === 401 || res.status === 403) return void show(false);
230
- if (!res.ok) return;
231
- show(true);
232
- const out = (await res.json()) as { total?: number };
233
- if (!isOpen) setBadge(typeof out.total === "number" ? out.total : 0);
234
- } catch {
235
- /* the next nudge asks again */
236
- } finally {
237
- asking = null;
238
- }
239
- })();
240
- return asking;
265
+ if (asking) return asking;
266
+ /*
267
+ * Kept until it settles, then let go — by THIS read only. (`asking ??= (async () => { … finally { asking = null } })()`
268
+ * lost the let-go when the read ended before its first await — `show` saying no: the finally ran before the
269
+ * assignment, and every read after was that settled one, never asked again.)
270
+ */
271
+ const run = readUnread();
272
+ asking = run;
273
+ try {
274
+ await run;
275
+ } finally {
276
+ if (asking === run) asking = null;
277
+ }
278
+ }
279
+ async function readUnread(): Promise<void> {
280
+ try {
281
+ const config = getConfig();
282
+ // The host's own say (0.19.5): not for whoever is signed in now — no bubble, nothing asked.
283
+ const opts = config.chat && config.chat !== true ? config.chat : null;
284
+ if (opts?.show && !opts.show()) return void show(false);
285
+ const token = await hostToken();
286
+ if (!token) return void show(false);
287
+ const res = await fetch(new URL("ingest/support-unread", base(config.host)).toString(), { headers: { "x-realm-key": config.realmKey, authorization: `Bearer ${token}` } });
288
+ // A support host from before the chat, a desk that turned its bubble off, or a token it does not accept: no bubble.
289
+ if (res.status === 404 || res.status === 401 || res.status === 403) return void show(false);
290
+ if (!res.ok) return;
291
+ const out = (await res.json()) as { total?: number; look?: DeskLook };
292
+ if (out.look && typeof out.look === "object") wearLook(out.look);
293
+ show(true);
294
+ if (!isOpen) setBadge(typeof out.total === "number" ? out.total : 0);
295
+ } catch {
296
+ /* the next nudge asks again */
297
+ }
241
298
  }
242
299
 
243
300
  function show(on: boolean): void {
@@ -491,8 +548,12 @@ export function mountSupportChat(): void {
491
548
  // The desk's face (the owner, 10-31: "all the support icon that user see … use this icon"): the support host's own,
492
549
  // the one its replies wear — the chat glyph until it is there, and if it never comes.
493
550
  const face = shadow.querySelector<HTMLImageElement>(".face")!;
494
- face.addEventListener("load", () => bubble?.classList.add("has-face"), { once: true });
495
- face.src = supportFaceUrl(config.host);
551
+ // Every picture it is given (the desk's look may change it, 0.19.5): worn once loaded, the glyph if it fails.
552
+ face.addEventListener("load", () => {
553
+ if (face.getAttribute("src")) bubble?.classList.add("has-face");
554
+ });
555
+ face.addEventListener("error", () => bubble?.classList.remove("has-face"));
556
+ face.src = lookFaceUrl(config.host, deskLook) ?? supportFaceUrl(config.host);
496
557
  // Where the person left it on this device, else the config's place (0.19.2).
497
558
  loadPlace();
498
559
  layout();
@@ -521,7 +582,13 @@ export function mountSupportChat(): void {
521
582
  }
522
583
 
523
584
  /** Take it away — for a host that unmounts, and for the tests. */
585
+ /** Ask again whether there is a bubble, and what waits (0.19.5) — after a sign-in or a sign-out, or `show` changed. */
586
+ export function refreshSupportChat(): Promise<void> {
587
+ return root ? refreshUnread() : Promise.resolve();
588
+ }
589
+
524
590
  export function unmountSupportChat(): void {
591
+ deskLook = null;
525
592
  stopFrames?.();
526
593
  stopFrames = null;
527
594
  window.removeEventListener("message", onMessage);