@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/README.md +20 -12
- package/dist/debug-capture.js +47 -10
- package/dist/index.mjs +58 -21
- package/dist/ui/strings.d.ts +8 -4
- package/dist/ui/styles.d.ts +1 -1
- package/package.json +1 -1
- package/src/ui/element.ts +220 -59
- package/src/ui/strings.ts +24 -14
- package/src/ui/styles.ts +44 -7
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
|
-
*
|
|
152
|
-
*
|
|
153
|
-
*
|
|
154
|
-
*
|
|
155
|
-
*
|
|
156
|
-
*
|
|
157
|
-
*
|
|
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
|
-
|
|
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" =
|
|
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
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
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
|
|
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
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
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 =
|
|
411
|
+
heading.textContent = kinds.hidden ? t.title : t.chooseTitle;
|
|
327
412
|
panel.setAttribute("aria-label", asking ? t.requestTitle : t.title);
|
|
328
|
-
|
|
329
|
-
|
|
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)
|
|
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
|
-
|
|
345
|
-
|
|
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
|
-
|
|
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
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
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
|
-
|
|
368
|
-
|
|
369
|
-
|
|
466
|
+
card.append(svgIcon(WORKFLOW), body);
|
|
467
|
+
card.addEventListener("click", () => {
|
|
468
|
+
chosen = option;
|
|
469
|
+
markType();
|
|
470
|
+
ready();
|
|
370
471
|
});
|
|
371
|
-
|
|
372
|
-
}
|
|
373
|
-
|
|
374
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
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):
|
|
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;
|
|
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
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
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
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
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
|
-
|
|
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
|
+
/* 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
|
`;
|