@lightworkai.official/debug-capture 0.14.1 → 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
@@ -148,18 +148,76 @@ function createReporterClass(): CustomElementConstructor {
148
148
  this.returnFocus = active instanceof HTMLElement && active !== document.body ? active : null;
149
149
  const t = strings(getConfig().locale);
150
150
  /*
151
- * One door: the press takes the screen and opens the report form,
152
- * always. Asking the team is a choice made IN that form — เป็นคำขอ?
153
- * เลือกเรื่องที่ขอ — never a door before it (docs/WIDGET-ASK-PLAN.md): a
154
- * chooser here cost every press one more click to serve the one in
155
- * twelve that asks, and a press before the offer was read had no way to
156
- * ask at all. The offer is read without waiting — the share needs this
157
- * click — and the form shows the door whenever it lands.
151
+ * The owner's order (docs/WIDGET-ASK-PLAN.md, 0.14.2): where the desk
152
+ * offers something to ask, the press asks first — แจ้งปัญหา or คำขอ —
153
+ * because a request has no use for the screen and must not be asked to
154
+ * share it. Read from what is already known, never awaited: the share
155
+ * has to start inside the press that chose แจ้งปัญหา. An offer not read
156
+ * yet is the report as it always was, and its form shows the two cards
157
+ * when the offer lands.
158
158
  */
159
- if (requestTypesNow() === null) void loadRequestTypes();
159
+ const offered = requestTypesNow();
160
+ if (offered === null) void loadRequestTypes();
161
+ if (reason.type === "manual" && offered?.length) {
162
+ this.renderChooser(t, reason);
163
+ return;
164
+ }
160
165
  await this.report(reason, t);
161
166
  }
162
167
 
168
+ /** แจ้งปัญหา or คำขอ — the two cards, before anything is asked of the browser. */
169
+ private renderChooser(t: Strings, reason: DebugReason): void {
170
+ const panel = this.shell(t);
171
+ panel.classList.add("narrow");
172
+ panel.setAttribute("aria-label", t.chooseTitle);
173
+ const head = el("div", "head");
174
+ const closeBtn = button("✕", "icon");
175
+ closeBtn.setAttribute("aria-label", t.close);
176
+ closeBtn.addEventListener("click", () => this.close());
177
+ head.append(el("h2", "", t.chooseTitle), closeBtn);
178
+
179
+ const problem = kindButton("problem", BUG, t.kindProblem, t.chooseProblemHint);
180
+ problem.dataset.choose = "problem";
181
+ // Our own dialog leaves the screen before the share starts, or it is in the picture.
182
+ problem.addEventListener("click", () => {
183
+ this.root.replaceChildren();
184
+ void this.report(reason, t);
185
+ });
186
+ const ask = kindButton("request", WORKFLOW, t.kindRequest, t.chooseRequestHint);
187
+ ask.dataset.choose = "request";
188
+ // A request: no share asked for. The page's address still travels with it.
189
+ ask.addEventListener("click", () => {
190
+ this.bundle = buildBundle({ reason, note: "", screenshotDataUrl: null, screenshotMethod: "none", erroredQueries: safeQueries() });
191
+ this.renderForm(t, reason, "request");
192
+ });
193
+ const choices = el("div", "kinds choose");
194
+ choices.append(problem, ask);
195
+ choices.addEventListener("keydown", (e) => {
196
+ if (!ARROWS.includes(e.key)) return;
197
+ e.preventDefault();
198
+ (this.root.activeElement === problem ? ask : problem).focus();
199
+ });
200
+ panel.append(head, choices);
201
+ problem.focus();
202
+ }
203
+
204
+ /**
205
+ * คำขอ → แจ้งปัญหา on a request that began with no screen: the share is
206
+ * asked for now, inside this press, with our dialog off the screen. What
207
+ * was typed crosses over; a share refused goes back to the request, as it
208
+ * was.
209
+ */
210
+ private async switchToReport(t: Strings, reason: DebugReason, carried: Draft): Promise<void> {
211
+ this.root.replaceChildren();
212
+ const shot = await captureScreen();
213
+ if (shot.method === "cancelled") {
214
+ this.renderForm(t, reason, "request", { ...carried, asking: true });
215
+ return;
216
+ }
217
+ this.bundle = buildBundle({ reason, note: "", screenshotDataUrl: shot.dataUrl, screenshotMethod: shot.method, erroredQueries: safeQueries() });
218
+ this.renderForm(t, reason, "problem", carried);
219
+ }
220
+
163
221
  /** A report: the screen first, then the form — see open(). Called inside a click, with nothing awaited before the capture. */
164
222
  private async report(reason: DebugReason, t: Strings): Promise<void> {
165
223
  /*
@@ -186,7 +244,7 @@ function createReporterClass(): CustomElementConstructor {
186
244
  screenshotMethod: shot.method,
187
245
  erroredQueries: safeQueries(),
188
246
  });
189
- this.renderForm(t, reason);
247
+ this.renderForm(t, reason, "problem");
190
248
  }
191
249
 
192
250
  /** Closed unsent, what was typed is kept for the next press; sent, it is gone. */
@@ -236,11 +294,13 @@ function createReporterClass(): CustomElementConstructor {
236
294
  * leaves the page's logs behind — they are a problem's evidence, not an
237
295
  * ask's — and its picture can be taken off.
238
296
  */
239
- private renderForm(t: Strings, reason: DebugReason): void {
297
+ private renderForm(t: Strings, reason: DebugReason, start: "problem" | "request", carried?: Draft): void {
240
298
  const bundle = this.bundle;
241
299
  if (!bundle) return;
242
300
  const panel = this.shell(t);
243
- let mode: "problem" | "request" = "problem";
301
+ let mode: "problem" | "request" = start;
302
+ // Was the screen asked for? A request chosen first was not — แจ้งปัญหา asks for it then.
303
+ const screenTaken = start === "problem" || !!bundle.screenshotDataUrl;
244
304
  let chosen: RequestTypeOption | null = null;
245
305
 
246
306
  const head = el("div", "head");
@@ -251,44 +311,47 @@ function createReporterClass(): CustomElementConstructor {
251
311
  head.append(heading, closeBtn);
252
312
 
253
313
  /*
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.
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.
259
320
  */
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
- });
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);
284
328
 
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.
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
+ */
286
336
  const ask = el("div", "ask");
287
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;
288
346
  const typeList = el("div", "types");
289
347
  typeList.setAttribute("role", "radiogroup");
290
348
  typeList.setAttribute("aria-label", t.requestTypeLabel);
291
- 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);
292
355
  const typeCards: HTMLButtonElement[] = [];
293
356
  const markType = () => {
294
357
  for (const card of typeCards) {
@@ -296,6 +359,13 @@ function createReporterClass(): CustomElementConstructor {
296
359
  card.setAttribute("aria-checked", String(on));
297
360
  card.tabIndex = on ? 0 : -1;
298
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
+ }
299
369
  };
300
370
 
301
371
  const summary = document.createElement("input");
@@ -351,12 +421,11 @@ function createReporterClass(): CustomElementConstructor {
351
421
 
352
422
  const apply = () => {
353
423
  const asking = mode === "request";
354
- heading.textContent = kinds.hidden ? t.title : t.chooseTitle;
424
+ heading.textContent = asking ? t.requestTitle : t.title;
355
425
  panel.setAttribute("aria-label", asking ? t.requestTitle : t.title);
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
- }
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;
360
429
  ask.hidden = !asking;
361
430
  fillLabel(summaryLabel, asking ? t.requestSummaryLabel : t.summaryLabel, asking ? "" : t.summaryRequired, true);
362
431
  summary.placeholder = asking ? t.requestSummaryPlaceholder : t.summaryPlaceholder;
@@ -366,16 +435,24 @@ function createReporterClass(): CustomElementConstructor {
366
435
  logs.v.textContent = asking ? t.requestLeavesWhat : captured;
367
436
  shotChoice.hidden = !asking || !bundle.screenshotDataUrl;
368
437
  if (markWhat) fillLabel(markWhat, asking ? t.requestAnnotateHint : t.annotateHint);
369
- for (const node of shot) node.classList.toggle("dropped", asking && !withShot.checked);
438
+ for (const node of shot) {
439
+ node.classList.toggle("dropped", asking && !withShot.checked);
440
+ // A request that began with no screen has no picture to speak of.
441
+ node.hidden = asking && !bundle.screenshotDataUrl;
442
+ }
370
443
  submitBtn.textContent = asking ? t.requestSubmit : t.submit;
371
444
  ready();
372
445
  };
373
446
  withShot.addEventListener("change", apply);
374
- function setMode(next: "problem" | "request"): void {
447
+ const setMode = (next: "problem" | "request"): void => {
375
448
  if (next === mode) return;
449
+ if (next === "problem" && !screenTaken) {
450
+ void this.switchToReport(t, reason, { title: summary.value, detail: detail.value, asking: false, typeId: chosen?.id ?? null });
451
+ return;
452
+ }
376
453
  mode = next;
377
454
  apply();
378
- }
455
+ };
379
456
 
380
457
  /*
381
458
  * What the desk offers, as soon as it is known: now, or when the read
@@ -407,27 +484,58 @@ function createReporterClass(): CustomElementConstructor {
407
484
  typeCards.push(card);
408
485
  }
409
486
  typeList.replaceChildren(...typeCards);
487
+ // ↑ ↓ among the rows the search leaves.
410
488
  typeList.onkeydown = (e) => {
411
489
  if (!ARROWS.includes(e.key) || !chosen) return;
412
490
  e.preventDefault();
413
- 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);
414
494
  const step = e.key === "ArrowUp" || e.key === "ArrowLeft" ? -1 : 1;
415
- chosen = types[(at + step + types.length) % types.length]!;
495
+ chosen = shown[at < 0 ? 0 : (at + step + shown.length) % shown.length]!;
416
496
  markType();
417
497
  typeCards.find((c) => c.dataset.requestType === chosen!.id)?.focus();
418
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
+ };
419
528
  markType();
420
- kinds.hidden = false;
529
+ door.hidden = false;
421
530
  apply();
422
531
  };
423
532
  /*
424
533
  * What was typed last time and not sent comes back, said so — with a
425
- * way to clear it. A request goes back to being one once its type is
426
- * offered again.
534
+ * way to clear it. Carried across a switch, the words are simply there.
427
535
  */
428
- const restored = draft && (draft.title || draft.detail) ? draft : null;
536
+ const restored = carried ?? (draft && (draft.title || draft.detail) ? draft : null);
429
537
  const again = el("p", "draft");
430
- again.hidden = !restored;
538
+ again.hidden = !restored || !!carried;
431
539
  if (restored) {
432
540
  summary.value = restored.title;
433
541
  detail.value = restored.detail;
@@ -446,20 +554,19 @@ function createReporterClass(): CustomElementConstructor {
446
554
  const known = requestTypesNow();
447
555
  const offerAndRestore = (types: RequestTypeOption[]) => {
448
556
  offer(types);
449
- if (!restored?.asking || kinds.hidden) return;
557
+ // The kind is the press's choice; a request's type comes back as it was.
558
+ if (mode !== "request" || door.hidden || !restored?.typeId) return;
450
559
  const was = types.find((x) => x.id === restored.typeId);
451
560
  if (was) {
452
561
  chosen = was;
453
562
  markType();
454
563
  }
455
- mode = "request";
456
- apply();
457
564
  };
458
565
  if (known) offerAndRestore(known);
459
566
  else if (reason.type === "manual") void loadRequestTypes().then(offerAndRestore);
460
567
  this.keep = () => ({ title: summary.value, detail: detail.value, asking: mode === "request", typeId: chosen?.id ?? null });
461
568
 
462
- 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);
463
570
  const foot = el("div", "foot");
464
571
  const cancel = button(t.close);
465
572
  cancel.addEventListener("click", () => this.close());
@@ -593,6 +700,8 @@ function hoursWords(t: Strings, h: number): string {
593
700
  }
594
701
 
595
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;
596
705
 
597
706
  /*
598
707
  * The desk's own icons (lucide, as its สร้างเวิร์กโฟลว์ and the portal draw
@@ -623,6 +732,16 @@ const CLOCK: IconNode = [
623
732
  ];
624
733
  const CHECK: IconNode = [["path", { d: "M20 6 9 17l-5-5" }]];
625
734
  const SVG_NS = "http://www.w3.org/2000/svg";
735
+ /** A card for แจ้งปัญหา or คำขอ — the chooser's and the form's, one drawing. */
736
+ function kindButton(kind: "problem" | "request", icon: IconNode, title: string, hint: string): HTMLButtonElement {
737
+ const card = button("", "kind");
738
+ card.dataset.kind = kind;
739
+ const top = el("span", "kind-top");
740
+ top.append(svgIcon(icon), el("b", "", title));
741
+ card.append(top, el("span", "kind-hint", hint), svgIcon(CHECK, "tick"));
742
+ return card;
743
+ }
744
+
626
745
  function svgIcon(nodes: IconNode, className = "icon"): SVGSVGElement {
627
746
  const svg = document.createElementNS(SVG_NS, "svg");
628
747
  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);
package/src/ui/strings.ts CHANGED
@@ -65,17 +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;
81
+ /** On the cards before anything is asked of the browser: what each will do. */
82
+ chooseProblemHint: string;
83
+ chooseRequestHint: string;
77
84
  requestTitle: string;
78
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;
79
91
  /** Carries `{time}`. */
80
92
  requestTarget: string;
81
93
  requestSummaryLabel: string;
@@ -143,11 +155,19 @@ const th: Strings = {
143
155
  handoffOpen: "เปิดในแท็บใหม่",
144
156
  chooseTitle: "ต้องการให้ทีมช่วยเรื่องอะไร",
145
157
  kindProblem: "แจ้งปัญหา",
146
- kindProblemHint: "มีอะไรเสีย หรือใช้งานไม่ได้",
147
158
  kindRequest: "คำขอ",
148
- kindRequestHint: "ขอให้ทีมทำ เช่น ขอสิทธิ์ ขอแก้ข้อมูล",
159
+ askInstead: "เป็นคำขอ?",
160
+ askInsteadAction: "เลือกเรื่องที่ขอ",
161
+ problemInstead: "เป็นปัญหาที่ต้องแก้?",
162
+ problemInsteadAction: "แจ้งปัญหาแทน",
163
+ problemInsteadShare: "แจ้งปัญหาแทน (แนบภาพหน้าจอ)",
164
+ chooseProblemHint: "มีอะไรเสีย หรือใช้งานไม่ได้ — แนบภาพหน้าจอ",
165
+ chooseRequestHint: "ขอให้ทีมทำ เช่น ขอสิทธิ์ ขอแก้ข้อมูล — ไม่ต้องแชร์หน้าจอ",
149
166
  requestTitle: "ขอให้ทีมดำเนินการ",
150
167
  requestTypeLabel: "เรื่องที่ขอ",
168
+ requestTypeSearch: "ค้นหาเรื่องที่ขอ",
169
+ requestTypeNone: "ไม่พบเรื่องที่ขอ — ลองคำอื่น",
170
+ requestTypeCount: "{n} เรื่อง — เลื่อนดูหรือพิมพ์ค้นหา",
151
171
  requestTarget: "ทีมตั้งใจให้เสร็จภายใน {time}",
152
172
  requestSummaryLabel: "หัวข้อ",
153
173
  requestSummaryPlaceholder: "ขออะไร — สั้น ๆ",
@@ -210,11 +230,19 @@ const en: Strings = {
210
230
  handoffOpen: "Open in a new tab",
211
231
  chooseTitle: "How can the team help?",
212
232
  kindProblem: "Report a problem",
213
- kindProblemHint: "Something is broken or not working",
214
233
  kindRequest: "Request",
215
- 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)",
239
+ chooseProblemHint: "Something is broken or not working — with a screenshot",
240
+ chooseRequestHint: "Ask the team to do something, like access or a data change — no screen sharing",
216
241
  requestTitle: "Ask the team",
217
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",
218
246
  requestTarget: "The team aims to finish within {time}",
219
247
  requestSummaryLabel: "Subject",
220
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,7 +98,14 @@ 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; }
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; }
105
+ .panel.narrow { width: min(560px, 100%); }
106
+ .kinds.choose { margin: 0 0 24px; }
107
+ .kinds.choose button.kind { padding: 16px 18px; gap: 6px; }
108
+ .kinds.choose .kind-hint { font-size: 13px; }
102
109
  button.kind {
103
110
  position: relative; display: flex; flex-direction: column; align-items: flex-start; gap: 4px;
104
111
  padding: 12px 34px 12px 14px; text-align: left; border-radius: 12px;
@@ -115,7 +122,16 @@ button.kind[aria-checked="true"], button.kind[aria-checked="true"]:hover:not(:di
115
122
  button.kind[aria-checked="true"] .tick { opacity: 1; }
116
123
  button.kind:focus-visible, button.type:focus-visible { outline: none; box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.25); }
117
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); }
118
- .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; }
119
135
  button.type {
120
136
  display: flex; align-items: flex-start; gap: 10px; width: 100%;
121
137
  padding: 10px 12px; text-align: left; border-radius: 10px;