@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/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
- // The other door: a quiet line under the heading, only where the desk offers something to ask.
255
- const door = el("p", "door");
256
- door.hidden = true;
257
- const doorText = el("span");
258
- const doorBtn = button("", "link");
259
- doorBtn.dataset.switchKind = "";
260
- door.append(doorText, doorBtn);
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 request's own rows.
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 typeHolder = el("div");
266
- const about = el("p", "status", "");
267
- ask.append(labelFor(t.requestTypeLabel, "", true), typeHolder, about);
268
- const describe = () => {
269
- if (!chosen) return;
270
- about.textContent = [chosen.description, chosen.targetHours ? t.requestTarget.replace("{time}", hoursWords(t, chosen.targetHours)) : ""].filter(Boolean).join(" · ");
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 = asking ? t.requestTitle : t.title;
354
+ heading.textContent = kinds.hidden ? t.title : t.chooseTitle;
327
355
  panel.setAttribute("aria-label", asking ? t.requestTitle : t.title);
328
- doorText.textContent = `${asking ? t.problemInstead : t.askInstead} `;
329
- doorBtn.textContent = asking ? t.problemInsteadAction : t.askInsteadAction;
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
- doorBtn.addEventListener("click", () => {
345
- mode = mode === "problem" ? "request" : "problem";
374
+ function setMode(next: "problem" | "request"): void {
375
+ if (next === mode) return;
376
+ mode = next;
346
377
  apply();
347
- (mode === "request" ? (select ?? summary) : summary).focus();
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
- if (types.length > 1) {
359
- select = document.createElement("select");
360
- select.dataset.requestType = "";
361
- for (const option of types) {
362
- const o = document.createElement("option");
363
- o.value = option.id;
364
- o.textContent = option.name;
365
- select.append(o);
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
- select.addEventListener("change", () => {
368
- chosen = types.find((x) => x.id === select!.value) ?? chosen;
369
- describe();
401
+ card.append(svgIcon(WORKFLOW), body);
402
+ card.addEventListener("click", () => {
403
+ chosen = option;
404
+ markType();
405
+ ready();
370
406
  });
371
- typeHolder.replaceChildren(select);
372
- } else typeHolder.replaceChildren(el("p", "ask-type", chosen.name));
373
- describe();
374
- door.hidden = false;
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 || door.hidden) return;
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
- if (select) select.value = was.id;
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, door, again, ask, summaryLabel, summary, detailLabel, detail, meta, shotChoice, ...shot);
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): the door
70
- * is a line in the report form, and back.
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
- askInstead: string;
73
- askInsteadAction: string;
74
- problemInstead: string;
75
- problemInsteadAction: string;
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
- askInstead: "เป็นคำขอ?",
144
- askInsteadAction: "เลือกเรื่องที่ขอ",
145
- problemInstead: "เป็นปัญหาที่ต้องแก้?",
146
- problemInsteadAction: "แจ้งปัญหาแทน",
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
- askInstead: "Asking for something?",
210
- askInsteadAction: "Choose what you need",
211
- problemInstead: "Something broken?",
212
- problemInsteadAction: "Report a problem instead",
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
- The door is a quiet line under the heading: read by the one who asks,
99
- costing nothing to the many who report. */
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
- .door { margin: -8px 0 0; font-size: 13px; color: #64748b; }
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
  `;