@lightworkai.official/debug-capture 0.14.2 → 0.14.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/element.ts CHANGED
@@ -311,41 +311,47 @@ function createReporterClass(): CustomElementConstructor {
311
311
  head.append(heading, closeBtn);
312
312
 
313
313
  /*
314
- * แจ้งปัญหา or คำขอ — the same two cards the press chose from, kept under
315
- * the heading so the form can be switched any time with nothing typed
316
- * lost (to แจ้งปัญหา from a request that began with no screen, the
317
- * share is asked for then — switchToReport). Shown only where the desk
318
- * offers something to ask.
314
+ * The kind was chosen at the press (renderChooser); the form names it in
315
+ * its heading and keeps one quiet line to switch — the owner: "no need
316
+ * to be card style since that select it on the first dialog". Switching
317
+ * keeps what was typed; to แจ้งปัญหา from a request that began with no
318
+ * screen, the share is asked for at that press (switchToReport). Shown
319
+ * only where the desk offers something to ask.
319
320
  */
320
- const kinds = el("div", "kinds");
321
- kinds.hidden = true;
322
- kinds.setAttribute("role", "radiogroup");
323
- kinds.setAttribute("aria-label", t.chooseTitle);
324
- const kindCard = (kind: "problem" | "request", icon: IconNode, title: string, hint: string) => {
325
- const card = kindButton(kind, icon, title, hint);
326
- card.setAttribute("role", "radio");
327
- card.addEventListener("click", () => setMode(kind));
328
- return card;
329
- };
330
- // From a request that began with no screen, แจ้งปัญหา will ask for it — said on the card, as the chooser says it.
331
- const problemCard = kindCard("problem", BUG, t.kindProblem, screenTaken ? t.kindProblemHint : t.chooseProblemHint);
332
- const requestCard = kindCard("request", WORKFLOW, t.kindRequest, t.kindRequestHint);
333
- kinds.append(problemCard, requestCard);
334
- // A radio group's own keys: an arrow moves the choice, and the focus with it.
335
- kinds.addEventListener("keydown", (e) => {
336
- if (!ARROWS.includes(e.key)) return;
337
- e.preventDefault();
338
- setMode(mode === "problem" ? "request" : "problem");
339
- (mode === "problem" ? problemCard : requestCard).focus();
340
- });
321
+ const door = el("p", "door");
322
+ door.hidden = true;
323
+ const doorText = el("span");
324
+ const doorBtn = button("", "link");
325
+ doorBtn.dataset.switchKind = "";
326
+ doorBtn.addEventListener("click", () => setMode(mode === "problem" ? "request" : "problem"));
327
+ door.append(doorText, doorBtn);
341
328
 
342
- // 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.
329
+ /*
330
+ * What is asked for: a row each. The one chosen opens up — its words and
331
+ * how long the team means to take, as the portal shows them — and the
332
+ * rest stay one line, so a desk with 10 or 20 types is still a list to
333
+ * read, not a page to scroll. From SEARCH_FROM types there is a search
334
+ * over them, and the list scrolls within its own height.
335
+ */
343
336
  const ask = el("div", "ask");
344
337
  ask.hidden = true;
338
+ const typeSearch = document.createElement("input");
339
+ // A search, not a text field: the title stays the dialog's one text input.
340
+ typeSearch.type = "search";
341
+ typeSearch.className = "type-search";
342
+ typeSearch.placeholder = t.requestTypeSearch;
343
+ typeSearch.setAttribute("aria-label", t.requestTypeSearch);
344
+ typeSearch.dataset.requestTypeSearch = "";
345
+ typeSearch.hidden = true;
345
346
  const typeList = el("div", "types");
346
347
  typeList.setAttribute("role", "radiogroup");
347
348
  typeList.setAttribute("aria-label", t.requestTypeLabel);
348
- ask.append(labelFor(t.requestTypeLabel, "", true), typeList);
349
+ const typeNone = el("p", "status", t.requestTypeNone);
350
+ typeNone.hidden = true;
351
+ // A long list says how many it holds — the rows past its edge are otherwise unseen.
352
+ const typeCount = el("p", "status type-count", "");
353
+ typeCount.hidden = true;
354
+ ask.append(labelFor(t.requestTypeLabel, "", true), typeSearch, typeList, typeNone, typeCount);
349
355
  const typeCards: HTMLButtonElement[] = [];
350
356
  const markType = () => {
351
357
  for (const card of typeCards) {
@@ -353,6 +359,13 @@ function createReporterClass(): CustomElementConstructor {
353
359
  card.setAttribute("aria-checked", String(on));
354
360
  card.tabIndex = on ? 0 : -1;
355
361
  }
362
+ // The chosen one in view inside the list's own scroll, never the dialog's.
363
+ const on = typeCards.find((c) => c.dataset.requestType === chosen?.id);
364
+ if (on && typeList.scrollHeight > typeList.clientHeight) {
365
+ const top = on.offsetTop - typeList.offsetTop;
366
+ if (top < typeList.scrollTop) typeList.scrollTop = top;
367
+ else if (top + on.offsetHeight > typeList.scrollTop + typeList.clientHeight) typeList.scrollTop = top + on.offsetHeight - typeList.clientHeight;
368
+ }
356
369
  };
357
370
 
358
371
  const summary = document.createElement("input");
@@ -408,12 +421,11 @@ function createReporterClass(): CustomElementConstructor {
408
421
 
409
422
  const apply = () => {
410
423
  const asking = mode === "request";
411
- heading.textContent = kinds.hidden ? t.title : t.chooseTitle;
424
+ heading.textContent = asking ? t.requestTitle : t.title;
412
425
  panel.setAttribute("aria-label", asking ? t.requestTitle : t.title);
413
- for (const [card, on] of [[problemCard, !asking], [requestCard, asking]] as const) {
414
- card.setAttribute("aria-checked", String(on));
415
- card.tabIndex = on ? 0 : -1;
416
- }
426
+ doorText.textContent = `${asking ? t.problemInstead : t.askInstead} `;
427
+ // From a request that began with no screen, แจ้งปัญหา will ask for it — said on the line.
428
+ doorBtn.textContent = asking ? (screenTaken ? t.problemInsteadAction : t.problemInsteadShare) : t.askInsteadAction;
417
429
  ask.hidden = !asking;
418
430
  fillLabel(summaryLabel, asking ? t.requestSummaryLabel : t.summaryLabel, asking ? "" : t.summaryRequired, true);
419
431
  summary.placeholder = asking ? t.requestSummaryPlaceholder : t.summaryPlaceholder;
@@ -472,17 +484,49 @@ function createReporterClass(): CustomElementConstructor {
472
484
  typeCards.push(card);
473
485
  }
474
486
  typeList.replaceChildren(...typeCards);
487
+ // ↑ ↓ among the rows the search leaves.
475
488
  typeList.onkeydown = (e) => {
476
489
  if (!ARROWS.includes(e.key) || !chosen) return;
477
490
  e.preventDefault();
478
- const at = types.findIndex((x) => x.id === chosen!.id);
491
+ const shown = types.filter((x, i) => !typeCards[i]!.hidden);
492
+ if (!shown.length) return;
493
+ const at = shown.findIndex((x) => x.id === chosen!.id);
479
494
  const step = e.key === "ArrowUp" || e.key === "ArrowLeft" ? -1 : 1;
480
- chosen = types[(at + step + types.length) % types.length]!;
495
+ chosen = shown[at < 0 ? 0 : (at + step + shown.length) % shown.length]!;
481
496
  markType();
482
497
  typeCards.find((c) => c.dataset.requestType === chosen!.id)?.focus();
483
498
  };
499
+ typeList.classList.toggle("long", types.length >= SEARCH_FROM);
500
+ typeSearch.hidden = types.length < SEARCH_FROM;
501
+ const count = (n: number) => {
502
+ typeCount.hidden = types.length < SEARCH_FROM || n === 0;
503
+ typeCount.textContent = t.requestTypeCount.replace("{n}", String(n));
504
+ };
505
+ count(types.length);
506
+ typeSearch.oninput = () => {
507
+ const q = typeSearch.value.trim().toLowerCase();
508
+ let first: RequestTypeOption | null = null;
509
+ types.forEach((x, i) => {
510
+ const hit = !q || `${x.name} ${x.description}`.toLowerCase().includes(q);
511
+ typeCards[i]!.hidden = !hit;
512
+ if (hit && !first) first = x;
513
+ });
514
+ typeNone.hidden = !!first;
515
+ count(typeCards.filter((c) => !c.hidden).length);
516
+ // The choice follows the search when the one chosen is filtered out.
517
+ if (first && chosen && typeCards[types.indexOf(chosen)]!.hidden) chosen = first;
518
+ markType();
519
+ ready();
520
+ };
521
+ // Enter in the search goes to the chosen row, not the send button.
522
+ typeSearch.onkeydown = (e) => {
523
+ if (e.key === "ArrowDown" || e.key === "Enter") {
524
+ e.preventDefault();
525
+ typeCards.find((c) => c.dataset.requestType === chosen?.id && !c.hidden)?.focus();
526
+ }
527
+ };
484
528
  markType();
485
- kinds.hidden = false;
529
+ door.hidden = false;
486
530
  apply();
487
531
  };
488
532
  /*
@@ -511,7 +555,7 @@ function createReporterClass(): CustomElementConstructor {
511
555
  const offerAndRestore = (types: RequestTypeOption[]) => {
512
556
  offer(types);
513
557
  // The kind is the press's choice; a request's type comes back as it was.
514
- if (mode !== "request" || kinds.hidden || !restored?.typeId) return;
558
+ if (mode !== "request" || door.hidden || !restored?.typeId) return;
515
559
  const was = types.find((x) => x.id === restored.typeId);
516
560
  if (was) {
517
561
  chosen = was;
@@ -522,7 +566,7 @@ function createReporterClass(): CustomElementConstructor {
522
566
  else if (reason.type === "manual") void loadRequestTypes().then(offerAndRestore);
523
567
  this.keep = () => ({ title: summary.value, detail: detail.value, asking: mode === "request", typeId: chosen?.id ?? null });
524
568
 
525
- panel.append(head, kinds, again, ask, summaryLabel, summary, detailLabel, detail, meta, shotChoice, ...shot);
569
+ panel.append(head, door, again, ask, summaryLabel, summary, detailLabel, detail, meta, shotChoice, ...shot);
526
570
  const foot = el("div", "foot");
527
571
  const cancel = button(t.close);
528
572
  cancel.addEventListener("click", () => this.close());
@@ -656,6 +700,8 @@ function hoursWords(t: Strings, h: number): string {
656
700
  }
657
701
 
658
702
  const ARROWS = ["ArrowLeft", "ArrowRight", "ArrowUp", "ArrowDown"];
703
+ /** From this many request types, the list gets a search and scrolls in its own height. */
704
+ const SEARCH_FROM = 7;
659
705
 
660
706
  /*
661
707
  * The desk's own icons (lucide, as its สร้างเวิร์กโฟลว์ and the portal draw
package/src/ui/strings.ts CHANGED
@@ -65,20 +65,29 @@ export interface Strings {
65
65
  handoffBlocked: string;
66
66
  handoffOpen: string;
67
67
  /*
68
- * ขอให้ทีมดำเนินการ — the same dialog turned into a request, where the desk
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.
68
+ * ขอให้ทีมดำเนินการ, where the desk opens request types to its reporters
69
+ * (docs/WIDGET-ASK-PLAN.md): the press asks first with two cards; the form
70
+ * keeps one quiet line to switch.
71
71
  */
72
72
  chooseTitle: string;
73
73
  kindProblem: string;
74
- kindProblemHint: string;
75
74
  kindRequest: string;
76
- kindRequestHint: string;
75
+ askInstead: string;
76
+ askInsteadAction: string;
77
+ problemInstead: string;
78
+ problemInsteadAction: string;
79
+ /** The same, from a request that began with no screen: the share will be asked for. */
80
+ problemInsteadShare: string;
77
81
  /** On the cards before anything is asked of the browser: what each will do. */
78
82
  chooseProblemHint: string;
79
83
  chooseRequestHint: string;
80
84
  requestTitle: string;
81
85
  requestTypeLabel: string;
86
+ /** Over a long list of types: the search, and what it says when nothing matches. */
87
+ requestTypeSearch: string;
88
+ requestTypeNone: string;
89
+ /** Carries `{n}`. */
90
+ requestTypeCount: string;
82
91
  /** Carries `{time}`. */
83
92
  requestTarget: string;
84
93
  requestSummaryLabel: string;
@@ -146,13 +155,19 @@ const th: Strings = {
146
155
  handoffOpen: "เปิดในแท็บใหม่",
147
156
  chooseTitle: "ต้องการให้ทีมช่วยเรื่องอะไร",
148
157
  kindProblem: "แจ้งปัญหา",
149
- kindProblemHint: "มีอะไรเสีย หรือใช้งานไม่ได้",
150
158
  kindRequest: "คำขอ",
151
- kindRequestHint: "ขอให้ทีมทำ เช่น ขอสิทธิ์ ขอแก้ข้อมูล",
159
+ askInstead: "เป็นคำขอ?",
160
+ askInsteadAction: "เลือกเรื่องที่ขอ",
161
+ problemInstead: "เป็นปัญหาที่ต้องแก้?",
162
+ problemInsteadAction: "แจ้งปัญหาแทน",
163
+ problemInsteadShare: "แจ้งปัญหาแทน (แนบภาพหน้าจอ)",
152
164
  chooseProblemHint: "มีอะไรเสีย หรือใช้งานไม่ได้ — แนบภาพหน้าจอ",
153
165
  chooseRequestHint: "ขอให้ทีมทำ เช่น ขอสิทธิ์ ขอแก้ข้อมูล — ไม่ต้องแชร์หน้าจอ",
154
166
  requestTitle: "ขอให้ทีมดำเนินการ",
155
167
  requestTypeLabel: "เรื่องที่ขอ",
168
+ requestTypeSearch: "ค้นหาเรื่องที่ขอ",
169
+ requestTypeNone: "ไม่พบเรื่องที่ขอ — ลองคำอื่น",
170
+ requestTypeCount: "{n} เรื่อง — เลื่อนดูหรือพิมพ์ค้นหา",
156
171
  requestTarget: "ทีมตั้งใจให้เสร็จภายใน {time}",
157
172
  requestSummaryLabel: "หัวข้อ",
158
173
  requestSummaryPlaceholder: "ขออะไร — สั้น ๆ",
@@ -215,13 +230,19 @@ const en: Strings = {
215
230
  handoffOpen: "Open in a new tab",
216
231
  chooseTitle: "How can the team help?",
217
232
  kindProblem: "Report a problem",
218
- kindProblemHint: "Something is broken or not working",
219
233
  kindRequest: "Request",
220
- kindRequestHint: "Ask the team to do something, like access or a data change",
234
+ askInstead: "Asking for something?",
235
+ askInsteadAction: "Choose what you need",
236
+ problemInstead: "Something broken?",
237
+ problemInsteadAction: "Report a problem instead",
238
+ problemInsteadShare: "Report a problem instead (with a screenshot)",
221
239
  chooseProblemHint: "Something is broken or not working — with a screenshot",
222
240
  chooseRequestHint: "Ask the team to do something, like access or a data change — no screen sharing",
223
241
  requestTitle: "Ask the team",
224
242
  requestTypeLabel: "What you need",
243
+ requestTypeSearch: "Search what you need",
244
+ requestTypeNone: "Nothing matches — try another word",
245
+ requestTypeCount: "{n} to choose from — scroll or search",
225
246
  requestTarget: "The team aims to finish within {time}",
226
247
  requestSummaryLabel: "Subject",
227
248
  requestSummaryPlaceholder: "What do you need — briefly",
package/src/ui/styles.ts CHANGED
@@ -34,11 +34,11 @@ h2 { margin: 0; font-size: 20px; font-weight: 700; flex: 1; }
34
34
  label { display: block; font-size: 14px; font-weight: 600; margin-bottom: 6px; }
35
35
  .req { color: #dc2626; }
36
36
  .hint { font-weight: 400; color: #64748b; }
37
- input[type="text"], textarea, select {
37
+ input[type="text"], input[type="search"], textarea, select {
38
38
  width: 100%; padding: 10px 12px; font: inherit; font-size: 14px;
39
39
  border: 1px solid #cbd5e1; border-radius: 10px; background: #fff; color: inherit;
40
40
  }
41
- input[type="text"]:focus, textarea:focus, select:focus {
41
+ input[type="text"]:focus, input[type="search"]:focus, textarea:focus, select:focus {
42
42
  outline: none; border-color: #2563eb; box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.18);
43
43
  }
44
44
  textarea { min-height: 88px; resize: vertical; }
@@ -98,8 +98,10 @@ canvas { width: 100%; height: auto; display: block; border: 1px solid #e2e8f0; b
98
98
  แจ้งปัญหา · คำขอ as two cards under the heading, the way the desk's own
99
99
  สร้างเวิร์กโฟลว์ asks it; what is asked for as cards, the way the portal lists it. */
100
100
  [hidden] { display: none !important; }
101
- .kinds { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; margin-top: -4px; }
102
- /* Before anything is asked of the browser: the same two cards, a little larger, in a smaller dialog. */
101
+ /* The press's choice: two cards in a smaller dialog, before anything is asked of the browser. */
102
+ .kinds { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; }
103
+ /* In the form, the choice is made: one quiet line under the heading to switch. */
104
+ .door { margin: -8px 0 0; font-size: 13px; color: #64748b; }
103
105
  .panel.narrow { width: min(560px, 100%); }
104
106
  .kinds.choose { margin: 0 0 24px; }
105
107
  .kinds.choose button.kind { padding: 16px 18px; gap: 6px; }
@@ -120,7 +122,16 @@ button.kind[aria-checked="true"], button.kind[aria-checked="true"]:hover:not(:di
120
122
  button.kind[aria-checked="true"] .tick { opacity: 1; }
121
123
  button.kind:focus-visible, button.type:focus-visible { outline: none; box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.25); }
122
124
  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); }
123
- .types { display: grid; gap: 8px; }
125
+ .types { display: grid; gap: 6px; }
126
+ /* A long list scrolls in its own height, so the title and ส่งคำขอ stay near. */
127
+ .types.long { max-height: 300px; overflow-y: auto; padding: 2px; margin: -2px; overscroll-behavior: contain; }
128
+ .type-search { margin-bottom: 8px; }
129
+ .ask .type-count { margin: 6px 0 0; font-size: 12px; }
130
+ /* The rest are one line: only the chosen one opens up with its words and the team's time. */
131
+ button.type:not([aria-checked="true"]) { padding: 8px 12px; align-items: center; }
132
+ button.type:not([aria-checked="true"]) .type-hint, button.type:not([aria-checked="true"]) .type-due { display: none; }
133
+ button.type:not([aria-checked="true"]) .type-body b { font-weight: 500; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
134
+ button.type:not([aria-checked="true"]) .icon { margin-top: 0; }
124
135
  button.type {
125
136
  display: flex; align-items: flex-start; gap: 10px; width: 100%;
126
137
  padding: 10px 12px; text-align: left; border-radius: 10px;