@lightworkai.official/debug-capture 0.17.1 → 0.17.3

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/src/ui/ring.ts CHANGED
@@ -17,48 +17,70 @@ import { strings } from "./strings";
17
17
 
18
18
  const HOST_TAG = "lw-call-ring";
19
19
 
20
+ /*
21
+ * The support host's own ring card, drawn the same (0.17.2, its §20): the call's frame takes this card's place when it is
22
+ * ready, and the owner saw two different cards — ours (เปิดเพื่อรับสาย) for a few seconds, then the call's (รับสาย). Now
23
+ * the same size, place, colours, words and buttons; the swap is not seen. Its values are the support host's default
24
+ * theme (--app-primary, --app-danger, emerald-600, --app-muted, --app-border; rounded-2xl, shadow-2xl, 22rem).
25
+ */
20
26
  const CSS = `
21
27
  :host { all: initial; }
22
28
  .card {
23
29
  position: fixed; right: 16px; bottom: 16px; z-index: 2147483002;
24
- width: min(340px, calc(100vw - 32px)); box-sizing: border-box;
25
- padding: 16px; border-radius: 16px; background: #fff; color: #0f172a;
26
- border: 1px solid #e2e8f0; box-shadow: 0 24px 60px rgba(15, 23, 42, 0.24);
27
- font-family: system-ui, -apple-system, "Segoe UI", Roboto, "Noto Sans Thai", sans-serif;
30
+ width: min(352px, calc(100vw - 32px)); box-sizing: border-box;
31
+ padding: 16px; border-radius: 16px; background: #fff; color: #1a1a1e;
32
+ border: 1px solid #e8e8ec; box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25);
33
+ font-family: "Inter Variable", "Noto Sans Thai Variable", "IBM Plex Sans Thai", -apple-system, "Segoe UI", Roboto, "Noto Sans Thai", sans-serif;
28
34
  animation: in 200ms cubic-bezier(0.16, 1, 0.3, 1);
29
35
  }
30
- @media (max-width: 480px) { .card { right: 8px; left: 8px; bottom: 8px; width: auto; } }
36
+ @media (max-width: 639px) { .card { right: 8px; left: 8px; bottom: 8px; width: auto; } }
31
37
  .head { display: flex; align-items: center; gap: 12px; }
32
38
  .icon {
33
- position: relative; flex: none; width: 44px; height: 44px; border-radius: 50%;
34
- display: grid; place-items: center; background: #ecfdf5; color: #059669;
39
+ position: relative; flex: none; width: 48px; height: 48px; border-radius: 50%;
40
+ display: grid; place-items: center; background: rgba(0, 70, 143, 0.1); color: #00468f;
35
41
  }
36
42
  .icon::after {
37
43
  content: ""; position: absolute; inset: 0; border-radius: 50%;
38
- background: rgba(16, 185, 129, 0.25); animation: ping 1.4s ease-out infinite;
44
+ background: rgba(0, 70, 143, 0.2); animation: ping 1s cubic-bezier(0, 0, 0.2, 1) infinite;
39
45
  }
40
46
  .icon svg { position: relative; width: 20px; height: 20px; }
41
- h2 { margin: 0; font-size: 15px; font-weight: 600; line-height: 1.4; }
42
- .about { margin: 2px 0 0; font-size: 12px; color: #475569; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; max-width: 250px; }
43
- .how { margin: 10px 0 0; font-size: 12px; line-height: 1.6; color: #64748b; }
44
- .how a { color: #0369a1; font-weight: 600; }
45
- .actions { display: flex; gap: 8px; margin-top: 12px; }
47
+ .words { min-width: 0; }
48
+ h2 { margin: 0; font-size: 15px; font-weight: 600; line-height: 1.5; color: #1a1a1e; }
49
+ .about { margin: 0; font-size: 12px; line-height: 1.5; color: #6b7280; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
50
+ .how { margin: 10px 0 0; font-size: 12px; line-height: 1.5; color: #6b7280; }
51
+ .how a { color: #00468f; font-weight: 600; }
52
+ .test {
53
+ margin: 6px 0 0; padding: 0; border: 0; background: none; cursor: pointer;
54
+ display: inline-flex; align-items: center; gap: 4px; font: inherit; font-size: 12px; font-weight: 500; color: #00468f;
55
+ }
56
+ .test:hover { text-decoration: underline; text-underline-offset: 2px; }
57
+ .test svg { width: 14px; height: 14px; }
58
+ .actions { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; margin-top: 12px; }
59
+ .actions.one { grid-template-columns: 1fr; }
46
60
  .actions button {
47
- flex: 1; box-sizing: border-box; display: inline-flex; align-items: center; justify-content: center;
48
- height: 40px; padding: 0 12px; border-radius: 999px; font: inherit; font-size: 14px; font-weight: 600; cursor: pointer;
61
+ box-sizing: border-box; display: inline-flex; align-items: center; justify-content: center; gap: 8px;
62
+ height: 44px; padding: 0 12px; border: none; border-radius: 999px; font: inherit; font-size: 14px; font-weight: 500; cursor: pointer; color: #fff;
49
63
  }
50
- .decline { border: 1px solid #fecdd3; background: #fff1f2; color: #be123c; }
51
- .decline:hover { background: #ffe4e6; }
52
- .open { border: none; background: #059669; color: #fff; }
53
- .open:hover { background: #047857; }
54
- .close { border: 1px solid #e2e8f0; background: #fff; color: #334155; }
55
- .actions button:focus-visible { outline: 2px solid #0ea5e9; outline-offset: 2px; }
64
+ .actions button svg { width: 16px; height: 16px; }
65
+ .actions button:disabled { opacity: 0.6; cursor: default; }
66
+ .decline { background: #f44336; }
67
+ .decline:hover:not(:disabled) { background: #dd3a2f; }
68
+ .open, .answer { background: #059669; }
69
+ .open:hover, .answer:hover:not(:disabled) { background: #04875f; }
70
+ .close { border: 1px solid #e8e8ec !important; background: #fff; color: #1a1a1e !important; }
71
+ .spin { animation: spin 0.9s linear infinite; }
72
+ .actions button:focus-visible, .test:focus-visible { outline: 2px solid #00468f; outline-offset: 2px; }
56
73
  @keyframes in { from { opacity: 0; transform: translateY(8px); } to { opacity: 1; transform: none; } }
57
- @keyframes ping { from { transform: scale(1); opacity: 1; } to { transform: scale(1.6); opacity: 0; } }
58
- @media (prefers-reduced-motion: reduce) { .card { animation: none; } .icon::after { animation: none; opacity: 0; } }
74
+ @keyframes ping { 75%, 100% { transform: scale(2); opacity: 0; } }
75
+ @keyframes spin { to { transform: rotate(360deg); } }
76
+ @media (prefers-reduced-motion: reduce) { .card { animation: none; } .icon::after { animation: none; opacity: 0; } .spin { animation: none; } }
59
77
  `;
60
78
 
61
- const PHONE = `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M22 16.92v3a2 2 0 0 1-2.18 2 19.79 19.79 0 0 1-8.63-3.07 19.5 19.5 0 0 1-6-6 19.79 19.79 0 0 1-3.07-8.67A2 2 0 0 1 4.11 2h3a2 2 0 0 1 2 1.72c.13.96.36 1.9.7 2.81a2 2 0 0 1-.45 2.11L8.09 9.91a16 16 0 0 0 6 6l1.27-1.27a2 2 0 0 1 2.11-.45c.91.34 1.85.57 2.81.7A2 2 0 0 1 22 16.92z"/></svg>`;
79
+ const SVG = (d: string) => `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">${d}</svg>`;
80
+ const PHONE = SVG(`<path d="M22 16.92v3a2 2 0 0 1-2.18 2 19.79 19.79 0 0 1-8.63-3.07 19.5 19.5 0 0 1-6-6 19.79 19.79 0 0 1-3.07-8.67A2 2 0 0 1 4.11 2h3a2 2 0 0 1 2 1.72c.13.96.36 1.9.7 2.81a2 2 0 0 1-.45 2.11L8.09 9.91a16 16 0 0 0 6 6l1.27-1.27a2 2 0 0 1 2.11-.45c.91.34 1.85.57 2.81.7A2 2 0 0 1 22 16.92z"/>`);
81
+ const PHONE_OFF = SVG(`<path d="M10.68 13.31a16 16 0 0 0 3.41 2.6l1.27-1.27a2 2 0 0 1 2.11-.45 12.84 12.84 0 0 0 2.81.7 2 2 0 0 1 1.72 2v3a2 2 0 0 1-2.18 2 19.79 19.79 0 0 1-8.63-3.07 19.42 19.42 0 0 1-3.33-2.67m-2.67-3.34a19.79 19.79 0 0 1-3.07-8.63A2 2 0 0 1 4.11 2h3a2 2 0 0 1 2 1.72 12.84 12.84 0 0 0 .7 2.81 2 2 0 0 1-.45 2.11L8.09 9.91"/><line x1="22" x2="2" y1="2" y2="22"/>`);
82
+ const SLIDERS = SVG(`<path d="M20 7h-9"/><path d="M14 17H5"/><circle cx="17" cy="17" r="3"/><circle cx="7" cy="7" r="3"/>`);
83
+ const SPINNER = SVG(`<path d="M21 12a9 9 0 1 1-6.219-8.56"/>`).replace("<svg ", '<svg class="spin" ');
62
84
 
63
85
  export interface RingCall {
64
86
  id: string;
@@ -67,11 +89,18 @@ export interface RingCall {
67
89
  }
68
90
 
69
91
  export interface RingHandlers {
70
- /** Runs INSIDE the press: a tab can be opened only there, before anything is awaited. */
92
+ /** The portal's way: runs INSIDE the press — a tab can be opened only there, before anything is awaited. */
71
93
  open: () => void;
72
94
  decline: () => void;
95
+ /** The call's frame is coming (0.17.2): รับสาย here — the frame answers the moment it is ready. */
96
+ answer?: () => void;
97
+ /** …and ทดสอบไมค์และลำโพง — the frame's voice settings, the moment it is ready. */
98
+ voice?: () => void;
73
99
  }
74
100
 
101
+ /** `frame`: the call's own card while its frame comes; `portal`: answer in a new tab (no frame, or it failed). */
102
+ export type RingMode = "frame" | "portal";
103
+
75
104
  let host: HTMLElement | null = null;
76
105
  let shownId: string | null = null;
77
106
  let titleBefore: string | null = null;
@@ -94,12 +123,23 @@ function mount(): ShadowRoot | null {
94
123
  return root;
95
124
  }
96
125
 
97
- /** Show the ring for this call — once; the same call again changes nothing. */
98
- export function showRing(call: RingCall, handlers: RingHandlers): void {
99
- if (shownId === call.id && host?.isConnected) return;
100
- const root = mount();
101
- if (!root) return;
102
- shownId = call.id;
126
+ function button(className: string, html: string, data: string, onClick: () => void): HTMLButtonElement {
127
+ const b = document.createElement("button");
128
+ b.type = "button";
129
+ b.className = className;
130
+ b.innerHTML = html;
131
+ b.dataset.lwRing = data;
132
+ b.onclick = onClick;
133
+ return b;
134
+ }
135
+ const label = (text: string) => {
136
+ const span = document.createElement("span");
137
+ span.textContent = text;
138
+ return span.outerHTML;
139
+ };
140
+
141
+ /** The card's insides, for a mode — the head stays the same in both. */
142
+ function render(root: ShadowRoot, call: RingCall, handlers: RingHandlers, mode: RingMode, note?: string): void {
103
143
  const s = t();
104
144
  root.innerHTML = "";
105
145
  const style = document.createElement("style");
@@ -109,12 +149,14 @@ export function showRing(call: RingCall, handlers: RingHandlers): void {
109
149
  card.setAttribute("role", "alertdialog");
110
150
  card.setAttribute("aria-label", s.ringTitle);
111
151
  card.dataset.lwCallRing = call.id;
152
+ card.dataset.lwRingMode = mode;
112
153
  const head = document.createElement("div");
113
154
  head.className = "head";
114
155
  const icon = document.createElement("span");
115
156
  icon.className = "icon";
116
157
  icon.innerHTML = PHONE;
117
158
  const words = document.createElement("div");
159
+ words.className = "words";
118
160
  const h = document.createElement("h2");
119
161
  h.textContent = s.ringTitle;
120
162
  const about = document.createElement("p");
@@ -125,30 +167,53 @@ export function showRing(call: RingCall, handlers: RingHandlers): void {
125
167
  head.append(icon, words);
126
168
  const how = document.createElement("p");
127
169
  how.className = "how";
128
- how.textContent = s.ringHow;
170
+ how.textContent = note ?? (mode === "frame" ? s.ringHint : s.ringHow);
129
171
  const actions = document.createElement("div");
130
172
  actions.className = "actions";
131
- const decline = document.createElement("button");
132
- decline.type = "button";
133
- decline.className = "decline";
134
- decline.textContent = s.ringDecline;
135
- decline.dataset.lwRing = "decline";
136
- decline.onclick = () => handlers.decline();
137
- const open = document.createElement("button");
138
- open.type = "button";
139
- open.className = "open";
140
- open.textContent = s.ringOpen;
141
- open.dataset.lwRing = "open";
142
- open.onclick = () => handlers.open();
143
- actions.append(decline, open);
144
- card.append(head, how, actions);
173
+ const decline = button("decline", PHONE_OFF + label(s.ringDecline), "decline", () => handlers.decline());
174
+ card.append(head, how);
175
+ if (mode === "frame" && handlers.answer) {
176
+ if (handlers.voice) card.append(button("test", SLIDERS + label(s.ringTest), "test", () => handlers.voice?.()));
177
+ const answer = button("answer", PHONE + label(s.ringAnswer), "answer", () => handlers.answer?.());
178
+ actions.append(decline, answer);
179
+ } else actions.append(decline, button("open", label(s.ringOpen), "open", () => handlers.open()));
180
+ card.append(actions);
145
181
  root.append(style, card);
182
+ }
183
+
184
+ /** Show the ring for this call — once; the same call again changes nothing. */
185
+ export function showRing(call: RingCall, handlers: RingHandlers, mode: RingMode = "portal"): void {
186
+ if (shownId === call.id && host?.isConnected) return;
187
+ const root = mount();
188
+ if (!root) return;
189
+ shownId = call.id;
190
+ render(root, call, handlers, mode);
146
191
  startTone();
147
192
  if (titleBefore === null) {
148
193
  titleBefore = document.title;
149
- document.title = `${s.ringTab} — ${titleBefore}`;
194
+ document.title = `${t().ringTab} — ${titleBefore}`;
195
+ }
196
+ }
197
+
198
+ /** รับสาย pressed while the call's frame comes: said, and nothing more to press until it is there. */
199
+ export function ringAnswering(id: string): void {
200
+ const root = host?.shadowRoot;
201
+ if (!root || shownId !== id) return;
202
+ const answer = root.querySelector<HTMLButtonElement>(".answer");
203
+ if (answer) {
204
+ answer.innerHTML = SPINNER + label(t().ringAnswering);
205
+ answer.disabled = true;
150
206
  }
151
- open.focus({ preventScroll: true });
207
+ const decline = root.querySelector<HTMLButtonElement>(".decline");
208
+ if (decline) decline.disabled = true;
209
+ }
210
+
211
+ /** The call's frame could not come up: the same card, now the portal's way — a new tab, which needs a fresh press. */
212
+ export function ringFallback(call: RingCall, handlers: RingHandlers): void {
213
+ const root = host?.isConnected ? host.shadowRoot : null;
214
+ if (!root || shownId !== call.id) return showRing(call, handlers, "portal");
215
+ host!.style.display = "";
216
+ render(root, call, handlers, "portal", t().ringFallback);
152
217
  }
153
218
 
154
219
  /** Opened: the card says where to answer, and stops ringing. `blockedUrl` — the browser refused the tab: a link, which is a fresh press. */
@@ -192,8 +257,22 @@ export function ringOpened(blockedUrl?: string): void {
192
257
  */
193
258
  export function quietRing(id: string, quiet = true): void {
194
259
  if (!host?.isConnected || shownId !== id) return;
195
- host.style.display = quiet ? "none" : "";
260
+ const h = host;
261
+ clearTimeout(fading);
262
+ if (!quiet) {
263
+ h.style.display = "";
264
+ h.style.opacity = "";
265
+ return;
266
+ }
267
+ // Under the call's own card (the frame is above ours): a short fade, so a line that wraps differently in its fonts
268
+ // is not seen to jump (0.17.2).
269
+ h.style.transition = "opacity 150ms ease-out";
270
+ h.style.opacity = "0";
271
+ fading = setTimeout(() => {
272
+ if (h.style.opacity === "0") h.style.display = "none";
273
+ }, 160);
196
274
  }
275
+ let fading: ReturnType<typeof setTimeout> | undefined;
197
276
 
198
277
  /** The call is no longer ringing for this person — answered, declined, missed or ended. */
199
278
  export function hideRing(id?: string): void {
package/src/ui/strings.ts CHANGED
@@ -118,6 +118,13 @@ export interface Strings {
118
118
  ringBlocked: string;
119
119
  ringClose: string;
120
120
  ringTab: string;
121
+ /** The call's own card, while its frame is coming (0.17.2): the same words as the support host's ring card. */
122
+ ringHint: string;
123
+ ringTest: string;
124
+ ringAnswer: string;
125
+ ringAnswering: string;
126
+ /** The frame could not come up: answer in a new tab instead. */
127
+ ringFallback: string;
121
128
  }
122
129
 
123
130
  const th: Strings = {
@@ -202,6 +209,11 @@ const th: Strings = {
202
209
  ringBlocked: "เบราว์เซอร์บล็อกแท็บใหม่ — กดที่นี่เพื่อเปิดหน้ารับสาย",
203
210
  ringClose: "ปิด",
204
211
  ringTab: "📞 สายเข้า",
212
+ ringHint: "เมื่อรับสาย เบราว์เซอร์จะขอใช้ไมโครโฟน · ระหว่างคุย ทีมงานอาจขอดูหน้าจอ — คุณเลือกได้เสมอว่าจะให้ดูหรือไม่",
213
+ ringTest: "ทดสอบไมค์และลำโพงก่อนรับสาย",
214
+ ringAnswer: "รับสาย",
215
+ ringAnswering: "กำลังรับสาย…",
216
+ ringFallback: "เปิดสายในหน้านี้ไม่ได้ — รับสายในแท็บใหม่แทน ไม่ต้องเข้าสู่ระบบอีก",
205
217
  };
206
218
 
207
219
  const en: Strings = {
@@ -286,6 +298,11 @@ const en: Strings = {
286
298
  ringBlocked: "Your browser blocked the new tab — open the call here",
287
299
  ringClose: "Close",
288
300
  ringTab: "📞 Incoming call",
301
+ ringHint: "When you answer, your browser asks for the microphone · during the call the team may ask to see your screen — you always choose",
302
+ ringTest: "Test the microphone and speaker first",
303
+ ringAnswer: "Answer",
304
+ ringAnswering: "Answering…",
305
+ ringFallback: "The call can't open on this page — answer in a new tab instead, already signed in",
289
306
  };
290
307
 
291
308
  export function strings(locale: "th" | "en" | undefined): Strings {