@lightworkai.official/debug-capture 0.14.0 → 0.14.2

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,13 +294,14 @@ 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
- let select: HTMLSelectElement | null = null;
246
305
 
247
306
  const head = el("div", "head");
248
307
  const heading = el("h2", "", t.title);
@@ -251,23 +310,49 @@ function createReporterClass(): CustomElementConstructor {
251
310
  closeBtn.addEventListener("click", () => this.close());
252
311
  head.append(heading, closeBtn);
253
312
 
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);
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.
319
+ */
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
+ });
261
341
 
262
- // What is asked for — the request's own rows.
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.
263
343
  const ask = el("div", "ask");
264
344
  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(" · ");
345
+ const typeList = el("div", "types");
346
+ typeList.setAttribute("role", "radiogroup");
347
+ typeList.setAttribute("aria-label", t.requestTypeLabel);
348
+ ask.append(labelFor(t.requestTypeLabel, "", true), typeList);
349
+ const typeCards: HTMLButtonElement[] = [];
350
+ const markType = () => {
351
+ for (const card of typeCards) {
352
+ const on = card.dataset.requestType === chosen?.id;
353
+ card.setAttribute("aria-checked", String(on));
354
+ card.tabIndex = on ? 0 : -1;
355
+ }
271
356
  };
272
357
 
273
358
  const summary = document.createElement("input");
@@ -323,10 +408,12 @@ function createReporterClass(): CustomElementConstructor {
323
408
 
324
409
  const apply = () => {
325
410
  const asking = mode === "request";
326
- heading.textContent = asking ? t.requestTitle : t.title;
411
+ heading.textContent = kinds.hidden ? t.title : t.chooseTitle;
327
412
  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;
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
+ }
330
417
  ask.hidden = !asking;
331
418
  fillLabel(summaryLabel, asking ? t.requestSummaryLabel : t.summaryLabel, asking ? "" : t.summaryRequired, true);
332
419
  summary.placeholder = asking ? t.requestSummaryPlaceholder : t.summaryPlaceholder;
@@ -336,16 +423,24 @@ function createReporterClass(): CustomElementConstructor {
336
423
  logs.v.textContent = asking ? t.requestLeavesWhat : captured;
337
424
  shotChoice.hidden = !asking || !bundle.screenshotDataUrl;
338
425
  if (markWhat) fillLabel(markWhat, asking ? t.requestAnnotateHint : t.annotateHint);
339
- for (const node of shot) node.classList.toggle("dropped", asking && !withShot.checked);
426
+ for (const node of shot) {
427
+ node.classList.toggle("dropped", asking && !withShot.checked);
428
+ // A request that began with no screen has no picture to speak of.
429
+ node.hidden = asking && !bundle.screenshotDataUrl;
430
+ }
340
431
  submitBtn.textContent = asking ? t.requestSubmit : t.submit;
341
432
  ready();
342
433
  };
343
434
  withShot.addEventListener("change", apply);
344
- doorBtn.addEventListener("click", () => {
345
- mode = mode === "problem" ? "request" : "problem";
435
+ const setMode = (next: "problem" | "request"): void => {
436
+ if (next === mode) return;
437
+ if (next === "problem" && !screenTaken) {
438
+ void this.switchToReport(t, reason, { title: summary.value, detail: detail.value, asking: false, typeId: chosen?.id ?? null });
439
+ return;
440
+ }
441
+ mode = next;
346
442
  apply();
347
- (mode === "request" ? (select ?? summary) : summary).focus();
348
- });
443
+ };
349
444
 
350
445
  /*
351
446
  * What the desk offers, as soon as it is known: now, or when the read
@@ -355,32 +450,48 @@ function createReporterClass(): CustomElementConstructor {
355
450
  const offer = (types: RequestTypeOption[]) => {
356
451
  if (this.bundle !== bundle || reason.type !== "manual" || !types.length) return;
357
452
  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);
453
+ typeCards.length = 0;
454
+ for (const option of types) {
455
+ const card = button("", "type");
456
+ card.setAttribute("role", "radio");
457
+ card.dataset.requestType = option.id;
458
+ const body = el("span", "type-body");
459
+ body.append(el("b", "", option.name));
460
+ if (option.description) body.append(el("span", "type-hint", option.description));
461
+ if (option.targetHours) {
462
+ const due = el("span", "type-due");
463
+ due.append(svgIcon(CLOCK), document.createTextNode(t.requestTarget.replace("{time}", hoursWords(t, option.targetHours))));
464
+ body.append(due);
366
465
  }
367
- select.addEventListener("change", () => {
368
- chosen = types.find((x) => x.id === select!.value) ?? chosen;
369
- describe();
466
+ card.append(svgIcon(WORKFLOW), body);
467
+ card.addEventListener("click", () => {
468
+ chosen = option;
469
+ markType();
470
+ ready();
370
471
  });
371
- typeHolder.replaceChildren(select);
372
- } else typeHolder.replaceChildren(el("p", "ask-type", chosen.name));
373
- describe();
374
- door.hidden = false;
472
+ typeCards.push(card);
473
+ }
474
+ typeList.replaceChildren(...typeCards);
475
+ typeList.onkeydown = (e) => {
476
+ if (!ARROWS.includes(e.key) || !chosen) return;
477
+ e.preventDefault();
478
+ const at = types.findIndex((x) => x.id === chosen!.id);
479
+ const step = e.key === "ArrowUp" || e.key === "ArrowLeft" ? -1 : 1;
480
+ chosen = types[(at + step + types.length) % types.length]!;
481
+ markType();
482
+ typeCards.find((c) => c.dataset.requestType === chosen!.id)?.focus();
483
+ };
484
+ markType();
485
+ kinds.hidden = false;
486
+ apply();
375
487
  };
376
488
  /*
377
489
  * What was typed last time and not sent comes back, said so — with a
378
- * way to clear it. A request goes back to being one once its type is
379
- * offered again.
490
+ * way to clear it. Carried across a switch, the words are simply there.
380
491
  */
381
- const restored = draft && (draft.title || draft.detail) ? draft : null;
492
+ const restored = carried ?? (draft && (draft.title || draft.detail) ? draft : null);
382
493
  const again = el("p", "draft");
383
- again.hidden = !restored;
494
+ again.hidden = !restored || !!carried;
384
495
  if (restored) {
385
496
  summary.value = restored.title;
386
497
  detail.value = restored.detail;
@@ -399,21 +510,19 @@ function createReporterClass(): CustomElementConstructor {
399
510
  const known = requestTypesNow();
400
511
  const offerAndRestore = (types: RequestTypeOption[]) => {
401
512
  offer(types);
402
- if (!restored?.asking || door.hidden) return;
513
+ // 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;
403
515
  const was = types.find((x) => x.id === restored.typeId);
404
516
  if (was) {
405
517
  chosen = was;
406
- if (select) select.value = was.id;
407
- describe();
518
+ markType();
408
519
  }
409
- mode = "request";
410
- apply();
411
520
  };
412
521
  if (known) offerAndRestore(known);
413
522
  else if (reason.type === "manual") void loadRequestTypes().then(offerAndRestore);
414
523
  this.keep = () => ({ title: summary.value, detail: detail.value, asking: mode === "request", typeId: chosen?.id ?? null });
415
524
 
416
- panel.append(head, door, again, ask, summaryLabel, summary, detailLabel, detail, meta, shotChoice, ...shot);
525
+ panel.append(head, kinds, again, ask, summaryLabel, summary, detailLabel, detail, meta, shotChoice, ...shot);
417
526
  const foot = el("div", "foot");
418
527
  const cancel = button(t.close);
419
528
  cancel.addEventListener("click", () => this.close());
@@ -546,6 +655,58 @@ function hoursWords(t: Strings, h: number): string {
546
655
  return h >= 24 && h % 24 === 0 ? t.days.replace("{n}", String(h / 24)) : t.hours.replace("{n}", String(h));
547
656
  }
548
657
 
658
+ const ARROWS = ["ArrowLeft", "ArrowRight", "ArrowUp", "ArrowDown"];
659
+
660
+ /*
661
+ * The desk's own icons (lucide, as its สร้างเวิร์กโฟลว์ and the portal draw
662
+ * them), carried as their few nodes: the widget takes no icon library.
663
+ */
664
+ type IconNode = [string, Record<string, string>][];
665
+ const BUG: IconNode = [
666
+ ["path", { d: "M12 20v-9" }],
667
+ ["path", { d: "M14 7a4 4 0 0 1 4 4v3a6 6 0 0 1-12 0v-3a4 4 0 0 1 4-4z" }],
668
+ ["path", { d: "M14.12 3.88 16 2" }],
669
+ ["path", { d: "M21 21a4 4 0 0 0-3.81-4" }],
670
+ ["path", { d: "M21 5a4 4 0 0 1-3.55 3.97" }],
671
+ ["path", { d: "M22 13h-4" }],
672
+ ["path", { d: "M3 21a4 4 0 0 1 3.81-4" }],
673
+ ["path", { d: "M3 5a4 4 0 0 0 3.55 3.97" }],
674
+ ["path", { d: "M6 13H2" }],
675
+ ["path", { d: "m8 2 1.88 1.88" }],
676
+ ["path", { d: "M9 7.13V6a3 3 0 1 1 6 0v1.13" }],
677
+ ];
678
+ const WORKFLOW: IconNode = [
679
+ ["rect", { width: "8", height: "8", x: "3", y: "3", rx: "2" }],
680
+ ["path", { d: "M7 11v4a2 2 0 0 0 2 2h4" }],
681
+ ["rect", { width: "8", height: "8", x: "13", y: "13", rx: "2" }],
682
+ ];
683
+ const CLOCK: IconNode = [
684
+ ["circle", { cx: "12", cy: "12", r: "10" }],
685
+ ["path", { d: "M12 6v6l4 2" }],
686
+ ];
687
+ const CHECK: IconNode = [["path", { d: "M20 6 9 17l-5-5" }]];
688
+ const SVG_NS = "http://www.w3.org/2000/svg";
689
+ /** A card for แจ้งปัญหา or คำขอ — the chooser's and the form's, one drawing. */
690
+ function kindButton(kind: "problem" | "request", icon: IconNode, title: string, hint: string): HTMLButtonElement {
691
+ const card = button("", "kind");
692
+ card.dataset.kind = kind;
693
+ const top = el("span", "kind-top");
694
+ top.append(svgIcon(icon), el("b", "", title));
695
+ card.append(top, el("span", "kind-hint", hint), svgIcon(CHECK, "tick"));
696
+ return card;
697
+ }
698
+
699
+ function svgIcon(nodes: IconNode, className = "icon"): SVGSVGElement {
700
+ const svg = document.createElementNS(SVG_NS, "svg");
701
+ 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);
702
+ for (const [tag, attrs] of nodes) {
703
+ const node = document.createElementNS(SVG_NS, tag);
704
+ for (const [k, v] of Object.entries(attrs)) node.setAttribute(k, v);
705
+ svg.append(node);
706
+ }
707
+ return svg;
708
+ }
709
+
549
710
  function button(text: string, className = ""): HTMLButtonElement {
550
711
  const node = document.createElement("button");
551
712
  node.type = "button";
package/src/ui/strings.ts CHANGED
@@ -66,13 +66,17 @@ 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;
77
+ /** On the cards before anything is asked of the browser: what each will do. */
78
+ chooseProblemHint: string;
79
+ chooseRequestHint: string;
76
80
  requestTitle: string;
77
81
  requestTypeLabel: string;
78
82
  /** Carries `{time}`. */
@@ -140,10 +144,13 @@ const th: Strings = {
140
144
  handoffFailed: "เปิดรายการที่แจ้งไว้ไม่สำเร็จ",
141
145
  handoffBlocked: "เบราว์เซอร์บล็อกการเปิดแท็บใหม่",
142
146
  handoffOpen: "เปิดในแท็บใหม่",
143
- askInstead: "เป็นคำขอ?",
144
- askInsteadAction: "เลือกเรื่องที่ขอ",
145
- problemInstead: "เป็นปัญหาที่ต้องแก้?",
146
- problemInsteadAction: "แจ้งปัญหาแทน",
147
+ chooseTitle: "ต้องการให้ทีมช่วยเรื่องอะไร",
148
+ kindProblem: "แจ้งปัญหา",
149
+ kindProblemHint: "มีอะไรเสีย หรือใช้งานไม่ได้",
150
+ kindRequest: "คำขอ",
151
+ kindRequestHint: "ขอให้ทีมทำ เช่น ขอสิทธิ์ ขอแก้ข้อมูล",
152
+ chooseProblemHint: "มีอะไรเสีย หรือใช้งานไม่ได้ — แนบภาพหน้าจอ",
153
+ chooseRequestHint: "ขอให้ทีมทำ เช่น ขอสิทธิ์ ขอแก้ข้อมูล — ไม่ต้องแชร์หน้าจอ",
147
154
  requestTitle: "ขอให้ทีมดำเนินการ",
148
155
  requestTypeLabel: "เรื่องที่ขอ",
149
156
  requestTarget: "ทีมตั้งใจให้เสร็จภายใน {time}",
@@ -206,10 +213,13 @@ const en: Strings = {
206
213
  handoffFailed: "Could not open your reports",
207
214
  handoffBlocked: "Your browser blocked the new tab",
208
215
  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",
216
+ chooseTitle: "How can the team help?",
217
+ kindProblem: "Report a problem",
218
+ kindProblemHint: "Something is broken or not working",
219
+ kindRequest: "Request",
220
+ kindRequestHint: "Ask the team to do something, like access or a data change",
221
+ chooseProblemHint: "Something is broken or not working — with a screenshot",
222
+ chooseRequestHint: "Ask the team to do something, like access or a data change — no screen sharing",
213
223
  requestTitle: "Ask the team",
214
224
  requestTypeLabel: "What you need",
215
225
  requestTarget: "The team aims to finish within {time}",
package/src/ui/styles.ts CHANGED
@@ -94,11 +94,50 @@ 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
+ /* Before anything is asked of the browser: the same two cards, a little larger, in a smaller dialog. */
103
+ .panel.narrow { width: min(560px, 100%); }
104
+ .kinds.choose { margin: 0 0 24px; }
105
+ .kinds.choose button.kind { padding: 16px 18px; gap: 6px; }
106
+ .kinds.choose .kind-hint { font-size: 13px; }
107
+ button.kind {
108
+ position: relative; display: flex; flex-direction: column; align-items: flex-start; gap: 4px;
109
+ padding: 12px 34px 12px 14px; text-align: left; border-radius: 12px;
110
+ }
111
+ .kind-top { display: flex; align-items: center; gap: 8px; font-size: 15px; }
112
+ .kind-top b { font-weight: 600; }
113
+ .kind-hint { font-size: 13px; font-weight: 400; line-height: 1.5; color: #64748b; }
114
+ button.kind .icon { width: 18px; height: 18px; flex-shrink: 0; color: #64748b; }
115
+ button.kind[data-kind="request"] .icon { color: #1d4ed8; }
116
+ button.kind .tick { position: absolute; top: 12px; right: 12px; width: 16px; height: 16px; color: #2563eb; opacity: 0; }
117
+ button.kind[aria-checked="true"], button.kind[aria-checked="true"]:hover:not(:disabled) {
118
+ border-color: #2563eb; background: #eff6ff; box-shadow: inset 0 0 0 1px #2563eb;
119
+ }
120
+ button.kind[aria-checked="true"] .tick { opacity: 1; }
121
+ button.kind:focus-visible, button.type:focus-visible { outline: none; box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.25); }
122
+ 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; }
124
+ button.type {
125
+ display: flex; align-items: flex-start; gap: 10px; width: 100%;
126
+ padding: 10px 12px; text-align: left; border-radius: 10px;
127
+ }
128
+ button.type .icon { width: 16px; height: 16px; margin-top: 3px; flex-shrink: 0; color: #64748b; }
129
+ button.type[aria-checked="true"], button.type[aria-checked="true"]:hover:not(:disabled) { border-color: #2563eb; background: #eff6ff; }
130
+ button.type[aria-checked="true"] .icon { color: #2563eb; }
131
+ .type-body { display: grid; gap: 2px; min-width: 0; }
132
+ .type-body b { font-size: 14px; font-weight: 600; }
133
+ .type-hint { font-size: 13px; font-weight: 400; color: #64748b; }
134
+ .type-due { display: inline-flex; align-items: center; gap: 4px; font-size: 12px; font-weight: 400; color: #64748b; }
135
+ button.type .type-due .icon { width: 12px; height: 12px; margin: 0; }
136
+ /* A phone: the two kinds still side by side — their names are the choice; the hints stay short. */
137
+ @media (max-width: 480px) {
138
+ .kind-hint { font-size: 12px; }
139
+ button.kind { padding: 10px 30px 10px 12px; }
140
+ }
102
141
  button.link {
103
142
  display: inline; padding: 0; border: 0; border-radius: 4px; background: none;
104
143
  font-size: 13px; font-weight: 600; color: #2563eb;
@@ -107,10 +146,8 @@ button.link {
107
146
  button.link:hover:not(:disabled) { background: none; color: #1d4ed8; }
108
147
  button.link:focus-visible { outline: 2px solid #2563eb; outline-offset: 2px; }
109
148
  .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
149
  .shot-choice { display: flex; align-items: center; gap: 8px; margin: 0; font-weight: 500; }
114
150
  .shot-choice input { width: 16px; height: 16px; margin: 0; accent-color: #2563eb; }
115
151
  .dropped { opacity: 0.35; pointer-events: none; }
152
+ .draft { margin: -8px 0 0; padding: 8px 12px; border-radius: 10px; background: #eff6ff; font-size: 13px; color: #1e3a8a; }
116
153
  `;