@lightworkai.official/debug-capture 0.14.0 → 0.14.1
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/README.md +6 -4
- package/dist/debug-capture.js +42 -10
- package/dist/index.mjs +53 -21
- package/dist/ui/strings.d.ts +5 -4
- package/dist/ui/styles.d.ts +1 -1
- package/package.json +1 -1
- package/src/ui/element.ts +129 -41
- package/src/ui/strings.ts +17 -14
- package/src/ui/styles.ts +39 -7
package/src/ui/element.ts
CHANGED
|
@@ -242,7 +242,6 @@ function createReporterClass(): CustomElementConstructor {
|
|
|
242
242
|
const panel = this.shell(t);
|
|
243
243
|
let mode: "problem" | "request" = "problem";
|
|
244
244
|
let chosen: RequestTypeOption | null = null;
|
|
245
|
-
let select: HTMLSelectElement | null = null;
|
|
246
245
|
|
|
247
246
|
const head = el("div", "head");
|
|
248
247
|
const heading = el("h2", "", t.title);
|
|
@@ -251,23 +250,52 @@ function createReporterClass(): CustomElementConstructor {
|
|
|
251
250
|
closeBtn.addEventListener("click", () => this.close());
|
|
252
251
|
head.append(heading, closeBtn);
|
|
253
252
|
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
253
|
+
/*
|
|
254
|
+
* แจ้งปัญหา or คำขอ — two cards under the heading, the way the desk's own
|
|
255
|
+
* สร้างเวิร์กโฟลว์ asks it, shown only where the desk offers something to
|
|
256
|
+
* ask. แจ้งปัญหา is chosen as the form opens (the press is a report for
|
|
257
|
+
* most, so no click is added); either card switches the form, any time,
|
|
258
|
+
* and nothing typed is lost by it.
|
|
259
|
+
*/
|
|
260
|
+
const kinds = el("div", "kinds");
|
|
261
|
+
kinds.hidden = true;
|
|
262
|
+
kinds.setAttribute("role", "radiogroup");
|
|
263
|
+
kinds.setAttribute("aria-label", t.chooseTitle);
|
|
264
|
+
const kindCard = (kind: "problem" | "request", icon: IconNode, title: string, hint: string) => {
|
|
265
|
+
const card = button("", "kind");
|
|
266
|
+
card.setAttribute("role", "radio");
|
|
267
|
+
card.dataset.kind = kind;
|
|
268
|
+
const top = el("span", "kind-top");
|
|
269
|
+
top.append(svgIcon(icon), el("b", "", title));
|
|
270
|
+
card.append(top, el("span", "kind-hint", hint), svgIcon(CHECK, "tick"));
|
|
271
|
+
card.addEventListener("click", () => setMode(kind));
|
|
272
|
+
return card;
|
|
273
|
+
};
|
|
274
|
+
const problemCard = kindCard("problem", BUG, t.kindProblem, t.kindProblemHint);
|
|
275
|
+
const requestCard = kindCard("request", WORKFLOW, t.kindRequest, t.kindRequestHint);
|
|
276
|
+
kinds.append(problemCard, requestCard);
|
|
277
|
+
// A radio group's own keys: an arrow moves the choice, and the focus with it.
|
|
278
|
+
kinds.addEventListener("keydown", (e) => {
|
|
279
|
+
if (!ARROWS.includes(e.key)) return;
|
|
280
|
+
e.preventDefault();
|
|
281
|
+
setMode(mode === "problem" ? "request" : "problem");
|
|
282
|
+
(mode === "problem" ? problemCard : requestCard).focus();
|
|
283
|
+
});
|
|
261
284
|
|
|
262
|
-
// What is asked for — the
|
|
285
|
+
// What is asked for: one card each — the type's own words and how long the team means to take, as the portal shows them.
|
|
263
286
|
const ask = el("div", "ask");
|
|
264
287
|
ask.hidden = true;
|
|
265
|
-
const
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
288
|
+
const typeList = el("div", "types");
|
|
289
|
+
typeList.setAttribute("role", "radiogroup");
|
|
290
|
+
typeList.setAttribute("aria-label", t.requestTypeLabel);
|
|
291
|
+
ask.append(labelFor(t.requestTypeLabel, "", true), typeList);
|
|
292
|
+
const typeCards: HTMLButtonElement[] = [];
|
|
293
|
+
const markType = () => {
|
|
294
|
+
for (const card of typeCards) {
|
|
295
|
+
const on = card.dataset.requestType === chosen?.id;
|
|
296
|
+
card.setAttribute("aria-checked", String(on));
|
|
297
|
+
card.tabIndex = on ? 0 : -1;
|
|
298
|
+
}
|
|
271
299
|
};
|
|
272
300
|
|
|
273
301
|
const summary = document.createElement("input");
|
|
@@ -323,10 +351,12 @@ function createReporterClass(): CustomElementConstructor {
|
|
|
323
351
|
|
|
324
352
|
const apply = () => {
|
|
325
353
|
const asking = mode === "request";
|
|
326
|
-
heading.textContent =
|
|
354
|
+
heading.textContent = kinds.hidden ? t.title : t.chooseTitle;
|
|
327
355
|
panel.setAttribute("aria-label", asking ? t.requestTitle : t.title);
|
|
328
|
-
|
|
329
|
-
|
|
356
|
+
for (const [card, on] of [[problemCard, !asking], [requestCard, asking]] as const) {
|
|
357
|
+
card.setAttribute("aria-checked", String(on));
|
|
358
|
+
card.tabIndex = on ? 0 : -1;
|
|
359
|
+
}
|
|
330
360
|
ask.hidden = !asking;
|
|
331
361
|
fillLabel(summaryLabel, asking ? t.requestSummaryLabel : t.summaryLabel, asking ? "" : t.summaryRequired, true);
|
|
332
362
|
summary.placeholder = asking ? t.requestSummaryPlaceholder : t.summaryPlaceholder;
|
|
@@ -341,11 +371,11 @@ function createReporterClass(): CustomElementConstructor {
|
|
|
341
371
|
ready();
|
|
342
372
|
};
|
|
343
373
|
withShot.addEventListener("change", apply);
|
|
344
|
-
|
|
345
|
-
|
|
374
|
+
function setMode(next: "problem" | "request"): void {
|
|
375
|
+
if (next === mode) return;
|
|
376
|
+
mode = next;
|
|
346
377
|
apply();
|
|
347
|
-
|
|
348
|
-
});
|
|
378
|
+
}
|
|
349
379
|
|
|
350
380
|
/*
|
|
351
381
|
* What the desk offers, as soon as it is known: now, or when the read
|
|
@@ -355,23 +385,40 @@ function createReporterClass(): CustomElementConstructor {
|
|
|
355
385
|
const offer = (types: RequestTypeOption[]) => {
|
|
356
386
|
if (this.bundle !== bundle || reason.type !== "manual" || !types.length) return;
|
|
357
387
|
chosen = types[0]!;
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
388
|
+
typeCards.length = 0;
|
|
389
|
+
for (const option of types) {
|
|
390
|
+
const card = button("", "type");
|
|
391
|
+
card.setAttribute("role", "radio");
|
|
392
|
+
card.dataset.requestType = option.id;
|
|
393
|
+
const body = el("span", "type-body");
|
|
394
|
+
body.append(el("b", "", option.name));
|
|
395
|
+
if (option.description) body.append(el("span", "type-hint", option.description));
|
|
396
|
+
if (option.targetHours) {
|
|
397
|
+
const due = el("span", "type-due");
|
|
398
|
+
due.append(svgIcon(CLOCK), document.createTextNode(t.requestTarget.replace("{time}", hoursWords(t, option.targetHours))));
|
|
399
|
+
body.append(due);
|
|
366
400
|
}
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
401
|
+
card.append(svgIcon(WORKFLOW), body);
|
|
402
|
+
card.addEventListener("click", () => {
|
|
403
|
+
chosen = option;
|
|
404
|
+
markType();
|
|
405
|
+
ready();
|
|
370
406
|
});
|
|
371
|
-
|
|
372
|
-
}
|
|
373
|
-
|
|
374
|
-
|
|
407
|
+
typeCards.push(card);
|
|
408
|
+
}
|
|
409
|
+
typeList.replaceChildren(...typeCards);
|
|
410
|
+
typeList.onkeydown = (e) => {
|
|
411
|
+
if (!ARROWS.includes(e.key) || !chosen) return;
|
|
412
|
+
e.preventDefault();
|
|
413
|
+
const at = types.findIndex((x) => x.id === chosen!.id);
|
|
414
|
+
const step = e.key === "ArrowUp" || e.key === "ArrowLeft" ? -1 : 1;
|
|
415
|
+
chosen = types[(at + step + types.length) % types.length]!;
|
|
416
|
+
markType();
|
|
417
|
+
typeCards.find((c) => c.dataset.requestType === chosen!.id)?.focus();
|
|
418
|
+
};
|
|
419
|
+
markType();
|
|
420
|
+
kinds.hidden = false;
|
|
421
|
+
apply();
|
|
375
422
|
};
|
|
376
423
|
/*
|
|
377
424
|
* What was typed last time and not sent comes back, said so — with a
|
|
@@ -399,12 +446,11 @@ function createReporterClass(): CustomElementConstructor {
|
|
|
399
446
|
const known = requestTypesNow();
|
|
400
447
|
const offerAndRestore = (types: RequestTypeOption[]) => {
|
|
401
448
|
offer(types);
|
|
402
|
-
if (!restored?.asking ||
|
|
449
|
+
if (!restored?.asking || kinds.hidden) return;
|
|
403
450
|
const was = types.find((x) => x.id === restored.typeId);
|
|
404
451
|
if (was) {
|
|
405
452
|
chosen = was;
|
|
406
|
-
|
|
407
|
-
describe();
|
|
453
|
+
markType();
|
|
408
454
|
}
|
|
409
455
|
mode = "request";
|
|
410
456
|
apply();
|
|
@@ -413,7 +459,7 @@ function createReporterClass(): CustomElementConstructor {
|
|
|
413
459
|
else if (reason.type === "manual") void loadRequestTypes().then(offerAndRestore);
|
|
414
460
|
this.keep = () => ({ title: summary.value, detail: detail.value, asking: mode === "request", typeId: chosen?.id ?? null });
|
|
415
461
|
|
|
416
|
-
panel.append(head,
|
|
462
|
+
panel.append(head, kinds, again, ask, summaryLabel, summary, detailLabel, detail, meta, shotChoice, ...shot);
|
|
417
463
|
const foot = el("div", "foot");
|
|
418
464
|
const cancel = button(t.close);
|
|
419
465
|
cancel.addEventListener("click", () => this.close());
|
|
@@ -546,6 +592,48 @@ function hoursWords(t: Strings, h: number): string {
|
|
|
546
592
|
return h >= 24 && h % 24 === 0 ? t.days.replace("{n}", String(h / 24)) : t.hours.replace("{n}", String(h));
|
|
547
593
|
}
|
|
548
594
|
|
|
595
|
+
const ARROWS = ["ArrowLeft", "ArrowRight", "ArrowUp", "ArrowDown"];
|
|
596
|
+
|
|
597
|
+
/*
|
|
598
|
+
* The desk's own icons (lucide, as its สร้างเวิร์กโฟลว์ and the portal draw
|
|
599
|
+
* them), carried as their few nodes: the widget takes no icon library.
|
|
600
|
+
*/
|
|
601
|
+
type IconNode = [string, Record<string, string>][];
|
|
602
|
+
const BUG: IconNode = [
|
|
603
|
+
["path", { d: "M12 20v-9" }],
|
|
604
|
+
["path", { d: "M14 7a4 4 0 0 1 4 4v3a6 6 0 0 1-12 0v-3a4 4 0 0 1 4-4z" }],
|
|
605
|
+
["path", { d: "M14.12 3.88 16 2" }],
|
|
606
|
+
["path", { d: "M21 21a4 4 0 0 0-3.81-4" }],
|
|
607
|
+
["path", { d: "M21 5a4 4 0 0 1-3.55 3.97" }],
|
|
608
|
+
["path", { d: "M22 13h-4" }],
|
|
609
|
+
["path", { d: "M3 21a4 4 0 0 1 3.81-4" }],
|
|
610
|
+
["path", { d: "M3 5a4 4 0 0 0 3.55 3.97" }],
|
|
611
|
+
["path", { d: "M6 13H2" }],
|
|
612
|
+
["path", { d: "m8 2 1.88 1.88" }],
|
|
613
|
+
["path", { d: "M9 7.13V6a3 3 0 1 1 6 0v1.13" }],
|
|
614
|
+
];
|
|
615
|
+
const WORKFLOW: IconNode = [
|
|
616
|
+
["rect", { width: "8", height: "8", x: "3", y: "3", rx: "2" }],
|
|
617
|
+
["path", { d: "M7 11v4a2 2 0 0 0 2 2h4" }],
|
|
618
|
+
["rect", { width: "8", height: "8", x: "13", y: "13", rx: "2" }],
|
|
619
|
+
];
|
|
620
|
+
const CLOCK: IconNode = [
|
|
621
|
+
["circle", { cx: "12", cy: "12", r: "10" }],
|
|
622
|
+
["path", { d: "M12 6v6l4 2" }],
|
|
623
|
+
];
|
|
624
|
+
const CHECK: IconNode = [["path", { d: "M20 6 9 17l-5-5" }]];
|
|
625
|
+
const SVG_NS = "http://www.w3.org/2000/svg";
|
|
626
|
+
function svgIcon(nodes: IconNode, className = "icon"): SVGSVGElement {
|
|
627
|
+
const svg = document.createElementNS(SVG_NS, "svg");
|
|
628
|
+
for (const [k, v] of Object.entries({ viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", "stroke-width": "2", "stroke-linecap": "round", "stroke-linejoin": "round", "aria-hidden": "true", class: className })) svg.setAttribute(k, v);
|
|
629
|
+
for (const [tag, attrs] of nodes) {
|
|
630
|
+
const node = document.createElementNS(SVG_NS, tag);
|
|
631
|
+
for (const [k, v] of Object.entries(attrs)) node.setAttribute(k, v);
|
|
632
|
+
svg.append(node);
|
|
633
|
+
}
|
|
634
|
+
return svg;
|
|
635
|
+
}
|
|
636
|
+
|
|
549
637
|
function button(text: string, className = ""): HTMLButtonElement {
|
|
550
638
|
const node = document.createElement("button");
|
|
551
639
|
node.type = "button";
|
package/src/ui/strings.ts
CHANGED
|
@@ -66,13 +66,14 @@ export interface Strings {
|
|
|
66
66
|
handoffOpen: string;
|
|
67
67
|
/*
|
|
68
68
|
* ขอให้ทีมดำเนินการ — the same dialog turned into a request, where the desk
|
|
69
|
-
* opens request types to its reporters (docs/WIDGET-ASK-PLAN.md):
|
|
70
|
-
*
|
|
69
|
+
* opens request types to its reporters (docs/WIDGET-ASK-PLAN.md): two cards
|
|
70
|
+
* under the heading, แจ้งปัญหา chosen as it opens, either one any time.
|
|
71
71
|
*/
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
72
|
+
chooseTitle: string;
|
|
73
|
+
kindProblem: string;
|
|
74
|
+
kindProblemHint: string;
|
|
75
|
+
kindRequest: string;
|
|
76
|
+
kindRequestHint: string;
|
|
76
77
|
requestTitle: string;
|
|
77
78
|
requestTypeLabel: string;
|
|
78
79
|
/** Carries `{time}`. */
|
|
@@ -140,10 +141,11 @@ const th: Strings = {
|
|
|
140
141
|
handoffFailed: "เปิดรายการที่แจ้งไว้ไม่สำเร็จ",
|
|
141
142
|
handoffBlocked: "เบราว์เซอร์บล็อกการเปิดแท็บใหม่",
|
|
142
143
|
handoffOpen: "เปิดในแท็บใหม่",
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
144
|
+
chooseTitle: "ต้องการให้ทีมช่วยเรื่องอะไร",
|
|
145
|
+
kindProblem: "แจ้งปัญหา",
|
|
146
|
+
kindProblemHint: "มีอะไรเสีย หรือใช้งานไม่ได้",
|
|
147
|
+
kindRequest: "คำขอ",
|
|
148
|
+
kindRequestHint: "ขอให้ทีมทำ เช่น ขอสิทธิ์ ขอแก้ข้อมูล",
|
|
147
149
|
requestTitle: "ขอให้ทีมดำเนินการ",
|
|
148
150
|
requestTypeLabel: "เรื่องที่ขอ",
|
|
149
151
|
requestTarget: "ทีมตั้งใจให้เสร็จภายใน {time}",
|
|
@@ -206,10 +208,11 @@ const en: Strings = {
|
|
|
206
208
|
handoffFailed: "Could not open your reports",
|
|
207
209
|
handoffBlocked: "Your browser blocked the new tab",
|
|
208
210
|
handoffOpen: "Open in a new tab",
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
211
|
+
chooseTitle: "How can the team help?",
|
|
212
|
+
kindProblem: "Report a problem",
|
|
213
|
+
kindProblemHint: "Something is broken or not working",
|
|
214
|
+
kindRequest: "Request",
|
|
215
|
+
kindRequestHint: "Ask the team to do something, like access or a data change",
|
|
213
216
|
requestTitle: "Ask the team",
|
|
214
217
|
requestTypeLabel: "What you need",
|
|
215
218
|
requestTarget: "The team aims to finish within {time}",
|
package/src/ui/styles.ts
CHANGED
|
@@ -94,11 +94,45 @@ canvas { width: 100%; height: auto; display: block; border: 1px solid #e2e8f0; b
|
|
|
94
94
|
}
|
|
95
95
|
.foot .grow { flex: 1; }
|
|
96
96
|
|
|
97
|
-
/* ขอให้ทีมดำเนินการ — the same dialog turned into a request (docs/WIDGET-ASK-PLAN.md)
|
|
98
|
-
|
|
99
|
-
|
|
97
|
+
/* ขอให้ทีมดำเนินการ — the same dialog turned into a request (docs/WIDGET-ASK-PLAN.md):
|
|
98
|
+
แจ้งปัญหา · คำขอ as two cards under the heading, the way the desk's own
|
|
99
|
+
สร้างเวิร์กโฟลว์ asks it; what is asked for as cards, the way the portal lists it. */
|
|
100
100
|
[hidden] { display: none !important; }
|
|
101
|
-
.
|
|
101
|
+
.kinds { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; margin-top: -4px; }
|
|
102
|
+
button.kind {
|
|
103
|
+
position: relative; display: flex; flex-direction: column; align-items: flex-start; gap: 4px;
|
|
104
|
+
padding: 12px 34px 12px 14px; text-align: left; border-radius: 12px;
|
|
105
|
+
}
|
|
106
|
+
.kind-top { display: flex; align-items: center; gap: 8px; font-size: 15px; }
|
|
107
|
+
.kind-top b { font-weight: 600; }
|
|
108
|
+
.kind-hint { font-size: 13px; font-weight: 400; line-height: 1.5; color: #64748b; }
|
|
109
|
+
button.kind .icon { width: 18px; height: 18px; flex-shrink: 0; color: #64748b; }
|
|
110
|
+
button.kind[data-kind="request"] .icon { color: #1d4ed8; }
|
|
111
|
+
button.kind .tick { position: absolute; top: 12px; right: 12px; width: 16px; height: 16px; color: #2563eb; opacity: 0; }
|
|
112
|
+
button.kind[aria-checked="true"], button.kind[aria-checked="true"]:hover:not(:disabled) {
|
|
113
|
+
border-color: #2563eb; background: #eff6ff; box-shadow: inset 0 0 0 1px #2563eb;
|
|
114
|
+
}
|
|
115
|
+
button.kind[aria-checked="true"] .tick { opacity: 1; }
|
|
116
|
+
button.kind:focus-visible, button.type:focus-visible { outline: none; box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.25); }
|
|
117
|
+
button.kind[aria-checked="true"]:focus-visible { box-shadow: inset 0 0 0 1px #2563eb, 0 0 0 3px rgba(37, 99, 235, 0.25); }
|
|
118
|
+
.types { display: grid; gap: 8px; }
|
|
119
|
+
button.type {
|
|
120
|
+
display: flex; align-items: flex-start; gap: 10px; width: 100%;
|
|
121
|
+
padding: 10px 12px; text-align: left; border-radius: 10px;
|
|
122
|
+
}
|
|
123
|
+
button.type .icon { width: 16px; height: 16px; margin-top: 3px; flex-shrink: 0; color: #64748b; }
|
|
124
|
+
button.type[aria-checked="true"], button.type[aria-checked="true"]:hover:not(:disabled) { border-color: #2563eb; background: #eff6ff; }
|
|
125
|
+
button.type[aria-checked="true"] .icon { color: #2563eb; }
|
|
126
|
+
.type-body { display: grid; gap: 2px; min-width: 0; }
|
|
127
|
+
.type-body b { font-size: 14px; font-weight: 600; }
|
|
128
|
+
.type-hint { font-size: 13px; font-weight: 400; color: #64748b; }
|
|
129
|
+
.type-due { display: inline-flex; align-items: center; gap: 4px; font-size: 12px; font-weight: 400; color: #64748b; }
|
|
130
|
+
button.type .type-due .icon { width: 12px; height: 12px; margin: 0; }
|
|
131
|
+
/* A phone: the two kinds still side by side — their names are the choice; the hints stay short. */
|
|
132
|
+
@media (max-width: 480px) {
|
|
133
|
+
.kind-hint { font-size: 12px; }
|
|
134
|
+
button.kind { padding: 10px 30px 10px 12px; }
|
|
135
|
+
}
|
|
102
136
|
button.link {
|
|
103
137
|
display: inline; padding: 0; border: 0; border-radius: 4px; background: none;
|
|
104
138
|
font-size: 13px; font-weight: 600; color: #2563eb;
|
|
@@ -107,10 +141,8 @@ button.link {
|
|
|
107
141
|
button.link:hover:not(:disabled) { background: none; color: #1d4ed8; }
|
|
108
142
|
button.link:focus-visible { outline: 2px solid #2563eb; outline-offset: 2px; }
|
|
109
143
|
.ask p { margin: 0; }
|
|
110
|
-
.draft { margin: -8px 0 0; padding: 8px 12px; border-radius: 10px; background: #eff6ff; font-size: 13px; color: #1e3a8a; }
|
|
111
|
-
.ask .status { margin-top: 6px; }
|
|
112
|
-
.ask-type { font-size: 14px; font-weight: 600; }
|
|
113
144
|
.shot-choice { display: flex; align-items: center; gap: 8px; margin: 0; font-weight: 500; }
|
|
114
145
|
.shot-choice input { width: 16px; height: 16px; margin: 0; accent-color: #2563eb; }
|
|
115
146
|
.dropped { opacity: 0.35; pointer-events: none; }
|
|
147
|
+
.draft { margin: -8px 0 0; padding: 8px 12px; border-radius: 10px; background: #eff6ff; font-size: 13px; color: #1e3a8a; }
|
|
116
148
|
`;
|