@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/README.md +23 -14
- package/dist/debug-capture.js +23 -7
- package/dist/index.mjs +33 -17
- package/dist/ui/strings.d.ts +14 -2
- package/dist/ui/styles.d.ts +1 -1
- package/package.json +1 -1
- package/src/ui/element.ts +180 -61
- package/src/ui/strings.ts +37 -9
- package/src/ui/styles.ts +20 -4
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,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" =
|
|
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
|
-
*
|
|
255
|
-
*
|
|
256
|
-
*
|
|
257
|
-
*
|
|
258
|
-
*
|
|
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
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
424
|
+
heading.textContent = asking ? t.requestTitle : t.title;
|
|
355
425
|
panel.setAttribute("aria-label", asking ? t.requestTitle : t.title);
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
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)
|
|
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
|
-
|
|
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
|
|
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 =
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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,
|
|
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
|
-
*
|
|
69
|
-
*
|
|
70
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
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;
|