@lightworkai.official/debug-capture 0.14.2 → 0.15.0
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 +33 -3
- package/dist/config.d.ts +11 -0
- package/dist/debug-capture.js +135 -8
- package/dist/index.d.ts +1 -0
- package/dist/index.mjs +67 -17
- package/dist/live.d.ts +19 -0
- package/dist/portal.d.ts +5 -0
- package/dist/ui/ring.d.ts +19 -0
- package/dist/ui/strings.d.ts +21 -2
- package/dist/ui/styles.d.ts +1 -1
- package/package.json +1 -1
- package/src/config.ts +12 -0
- package/src/index.ts +2 -0
- package/src/install.ts +9 -0
- package/src/live.ts +227 -0
- package/src/portal.ts +3 -0
- package/src/ui/element.ts +84 -38
- package/src/ui/ring.ts +248 -0
- package/src/ui/strings.ts +58 -9
- package/src/ui/styles.ts +16 -5
package/src/ui/ring.ts
ADDED
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A call from the support team, rung in the host app (live.ts).
|
|
3
|
+
*
|
|
4
|
+
* The call itself happens on the support host — its media code, its screen
|
|
5
|
+
* sharing, its dock — so this card does one thing: say who is calling and
|
|
6
|
+
* about what, and open the reporter's request there to answer it. A soft ring
|
|
7
|
+
* and the tab's title too, because a call that rings in a background tab and
|
|
8
|
+
* says nothing is a missed call.
|
|
9
|
+
*
|
|
10
|
+
* Its own shadow root and styles, like the dialog, the toast and the hand-off:
|
|
11
|
+
* it lands on pages we do not control. Marked as ours, so a report filed while
|
|
12
|
+
* it is up does not photograph it.
|
|
13
|
+
*/
|
|
14
|
+
import { getConfig } from "../config";
|
|
15
|
+
import { containKeys } from "./keys";
|
|
16
|
+
import { strings } from "./strings";
|
|
17
|
+
|
|
18
|
+
const HOST_TAG = "lw-call-ring";
|
|
19
|
+
|
|
20
|
+
const CSS = `
|
|
21
|
+
:host { all: initial; }
|
|
22
|
+
.card {
|
|
23
|
+
position: fixed; right: 16px; bottom: 16px; z-index: 2147483002;
|
|
24
|
+
width: min(340px, calc(100vw - 32px)); box-sizing: border-box;
|
|
25
|
+
padding: 16px; border-radius: 16px; background: #fff; color: #0f172a;
|
|
26
|
+
border: 1px solid #e2e8f0; box-shadow: 0 24px 60px rgba(15, 23, 42, 0.24);
|
|
27
|
+
font-family: system-ui, -apple-system, "Segoe UI", Roboto, "Noto Sans Thai", sans-serif;
|
|
28
|
+
animation: in 200ms cubic-bezier(0.16, 1, 0.3, 1);
|
|
29
|
+
}
|
|
30
|
+
@media (max-width: 480px) { .card { right: 8px; left: 8px; bottom: 8px; width: auto; } }
|
|
31
|
+
.head { display: flex; align-items: center; gap: 12px; }
|
|
32
|
+
.icon {
|
|
33
|
+
position: relative; flex: none; width: 44px; height: 44px; border-radius: 50%;
|
|
34
|
+
display: grid; place-items: center; background: #ecfdf5; color: #059669;
|
|
35
|
+
}
|
|
36
|
+
.icon::after {
|
|
37
|
+
content: ""; position: absolute; inset: 0; border-radius: 50%;
|
|
38
|
+
background: rgba(16, 185, 129, 0.25); animation: ping 1.4s ease-out infinite;
|
|
39
|
+
}
|
|
40
|
+
.icon svg { position: relative; width: 20px; height: 20px; }
|
|
41
|
+
h2 { margin: 0; font-size: 15px; font-weight: 600; line-height: 1.4; }
|
|
42
|
+
.about { margin: 2px 0 0; font-size: 12px; color: #475569; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; max-width: 250px; }
|
|
43
|
+
.how { margin: 10px 0 0; font-size: 12px; line-height: 1.6; color: #64748b; }
|
|
44
|
+
.how a { color: #0369a1; font-weight: 600; }
|
|
45
|
+
.actions { display: flex; gap: 8px; margin-top: 12px; }
|
|
46
|
+
.actions button {
|
|
47
|
+
flex: 1; box-sizing: border-box; display: inline-flex; align-items: center; justify-content: center;
|
|
48
|
+
height: 40px; padding: 0 12px; border-radius: 999px; font: inherit; font-size: 14px; font-weight: 600; cursor: pointer;
|
|
49
|
+
}
|
|
50
|
+
.decline { border: 1px solid #fecdd3; background: #fff1f2; color: #be123c; }
|
|
51
|
+
.decline:hover { background: #ffe4e6; }
|
|
52
|
+
.open { border: none; background: #059669; color: #fff; }
|
|
53
|
+
.open:hover { background: #047857; }
|
|
54
|
+
.close { border: 1px solid #e2e8f0; background: #fff; color: #334155; }
|
|
55
|
+
.actions button:focus-visible { outline: 2px solid #0ea5e9; outline-offset: 2px; }
|
|
56
|
+
@keyframes in { from { opacity: 0; transform: translateY(8px); } to { opacity: 1; transform: none; } }
|
|
57
|
+
@keyframes ping { from { transform: scale(1); opacity: 1; } to { transform: scale(1.6); opacity: 0; } }
|
|
58
|
+
@media (prefers-reduced-motion: reduce) { .card { animation: none; } .icon::after { animation: none; opacity: 0; } }
|
|
59
|
+
`;
|
|
60
|
+
|
|
61
|
+
const PHONE = `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M22 16.92v3a2 2 0 0 1-2.18 2 19.79 19.79 0 0 1-8.63-3.07 19.5 19.5 0 0 1-6-6 19.79 19.79 0 0 1-3.07-8.67A2 2 0 0 1 4.11 2h3a2 2 0 0 1 2 1.72c.13.96.36 1.9.7 2.81a2 2 0 0 1-.45 2.11L8.09 9.91a16 16 0 0 0 6 6l1.27-1.27a2 2 0 0 1 2.11-.45c.91.34 1.85.57 2.81.7A2 2 0 0 1 22 16.92z"/></svg>`;
|
|
62
|
+
|
|
63
|
+
export interface RingCall {
|
|
64
|
+
id: string;
|
|
65
|
+
ticketKey: string;
|
|
66
|
+
ticketTitle: string;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export interface RingHandlers {
|
|
70
|
+
/** Runs INSIDE the press: a tab can be opened only there, before anything is awaited. */
|
|
71
|
+
open: () => void;
|
|
72
|
+
decline: () => void;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
let host: HTMLElement | null = null;
|
|
76
|
+
let shownId: string | null = null;
|
|
77
|
+
let titleBefore: string | null = null;
|
|
78
|
+
|
|
79
|
+
/** "เรื่อง CALL-12 · title" — the line under the card's title. Pure, for the tests. */
|
|
80
|
+
export function ringAbout(template: string, call: RingCall): string {
|
|
81
|
+
return template.replace("{key}", call.ticketKey).replace("{title}", call.ticketTitle);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const t = () => strings(getConfig().locale);
|
|
85
|
+
|
|
86
|
+
function mount(): ShadowRoot | null {
|
|
87
|
+
if (typeof document === "undefined") return null;
|
|
88
|
+
if (host?.isConnected) return host.shadowRoot;
|
|
89
|
+
host = document.createElement(HOST_TAG);
|
|
90
|
+
host.dataset.debugReporter = "true";
|
|
91
|
+
const root = host.attachShadow({ mode: "open" });
|
|
92
|
+
containKeys(root);
|
|
93
|
+
document.body.append(host);
|
|
94
|
+
return root;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** Show the ring for this call — once; the same call again changes nothing. */
|
|
98
|
+
export function showRing(call: RingCall, handlers: RingHandlers): void {
|
|
99
|
+
if (shownId === call.id && host?.isConnected) return;
|
|
100
|
+
const root = mount();
|
|
101
|
+
if (!root) return;
|
|
102
|
+
shownId = call.id;
|
|
103
|
+
const s = t();
|
|
104
|
+
root.innerHTML = "";
|
|
105
|
+
const style = document.createElement("style");
|
|
106
|
+
style.textContent = CSS;
|
|
107
|
+
const card = document.createElement("div");
|
|
108
|
+
card.className = "card";
|
|
109
|
+
card.setAttribute("role", "alertdialog");
|
|
110
|
+
card.setAttribute("aria-label", s.ringTitle);
|
|
111
|
+
card.dataset.lwCallRing = call.id;
|
|
112
|
+
const head = document.createElement("div");
|
|
113
|
+
head.className = "head";
|
|
114
|
+
const icon = document.createElement("span");
|
|
115
|
+
icon.className = "icon";
|
|
116
|
+
icon.innerHTML = PHONE;
|
|
117
|
+
const words = document.createElement("div");
|
|
118
|
+
const h = document.createElement("h2");
|
|
119
|
+
h.textContent = s.ringTitle;
|
|
120
|
+
const about = document.createElement("p");
|
|
121
|
+
about.className = "about";
|
|
122
|
+
about.textContent = ringAbout(s.ringAbout, call);
|
|
123
|
+
about.title = about.textContent;
|
|
124
|
+
words.append(h, about);
|
|
125
|
+
head.append(icon, words);
|
|
126
|
+
const how = document.createElement("p");
|
|
127
|
+
how.className = "how";
|
|
128
|
+
how.textContent = s.ringHow;
|
|
129
|
+
const actions = document.createElement("div");
|
|
130
|
+
actions.className = "actions";
|
|
131
|
+
const decline = document.createElement("button");
|
|
132
|
+
decline.type = "button";
|
|
133
|
+
decline.className = "decline";
|
|
134
|
+
decline.textContent = s.ringDecline;
|
|
135
|
+
decline.dataset.lwRing = "decline";
|
|
136
|
+
decline.onclick = () => handlers.decline();
|
|
137
|
+
const open = document.createElement("button");
|
|
138
|
+
open.type = "button";
|
|
139
|
+
open.className = "open";
|
|
140
|
+
open.textContent = s.ringOpen;
|
|
141
|
+
open.dataset.lwRing = "open";
|
|
142
|
+
open.onclick = () => handlers.open();
|
|
143
|
+
actions.append(decline, open);
|
|
144
|
+
card.append(head, how, actions);
|
|
145
|
+
root.append(style, card);
|
|
146
|
+
startTone();
|
|
147
|
+
if (titleBefore === null) {
|
|
148
|
+
titleBefore = document.title;
|
|
149
|
+
document.title = `${s.ringTab} — ${titleBefore}`;
|
|
150
|
+
}
|
|
151
|
+
open.focus({ preventScroll: true });
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/** Opened: the card says where to answer, and stops ringing. `blockedUrl` — the browser refused the tab: a link, which is a fresh press. */
|
|
155
|
+
export function ringOpened(blockedUrl?: string): void {
|
|
156
|
+
const root = host?.shadowRoot;
|
|
157
|
+
if (!root) return;
|
|
158
|
+
stopTone();
|
|
159
|
+
restoreTitle();
|
|
160
|
+
const s = t();
|
|
161
|
+
const how = root.querySelector(".how");
|
|
162
|
+
const actions = root.querySelector(".actions");
|
|
163
|
+
if (how) {
|
|
164
|
+
how.textContent = "";
|
|
165
|
+
if (blockedUrl) {
|
|
166
|
+
const a = document.createElement("a");
|
|
167
|
+
a.href = blockedUrl;
|
|
168
|
+
a.target = "_blank";
|
|
169
|
+
a.rel = "noopener";
|
|
170
|
+
a.textContent = s.ringBlocked;
|
|
171
|
+
a.dataset.lwRing = "link";
|
|
172
|
+
a.onclick = () => window.setTimeout(() => ringOpened(), 0);
|
|
173
|
+
how.append(a);
|
|
174
|
+
} else how.textContent = s.ringOpened;
|
|
175
|
+
}
|
|
176
|
+
if (actions) {
|
|
177
|
+
actions.innerHTML = "";
|
|
178
|
+
const close = document.createElement("button");
|
|
179
|
+
close.type = "button";
|
|
180
|
+
close.className = "close";
|
|
181
|
+
close.textContent = s.ringClose;
|
|
182
|
+
close.dataset.lwRing = "close";
|
|
183
|
+
close.onclick = () => hideRing();
|
|
184
|
+
actions.append(close);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/** The call is no longer ringing for this person — answered, declined, missed or ended. */
|
|
189
|
+
export function hideRing(id?: string): void {
|
|
190
|
+
if (id && shownId && id !== shownId) return;
|
|
191
|
+
stopTone();
|
|
192
|
+
restoreTitle();
|
|
193
|
+
host?.remove();
|
|
194
|
+
host = null;
|
|
195
|
+
shownId = null;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
export function ringShownFor(): string | null {
|
|
199
|
+
return host?.isConnected ? shownId : null;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function restoreTitle(): void {
|
|
203
|
+
if (titleBefore === null || typeof document === "undefined") return;
|
|
204
|
+
if (document.title.startsWith(t().ringTab)) document.title = titleBefore;
|
|
205
|
+
titleBefore = null;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/* ── A soft ring: two notes every two seconds, as the support host's own ── */
|
|
209
|
+
|
|
210
|
+
let audio: AudioContext | null = null;
|
|
211
|
+
let toneTimer: ReturnType<typeof setInterval> | undefined;
|
|
212
|
+
|
|
213
|
+
function startTone(): void {
|
|
214
|
+
stopTone();
|
|
215
|
+
const w = window as unknown as { AudioContext?: typeof AudioContext; webkitAudioContext?: typeof AudioContext };
|
|
216
|
+
const Ctx = w.AudioContext ?? w.webkitAudioContext;
|
|
217
|
+
if (!Ctx) return;
|
|
218
|
+
try {
|
|
219
|
+
const ctx = audio ?? new Ctx();
|
|
220
|
+
audio = ctx;
|
|
221
|
+
// A page nobody has pressed anything on may not play sound — then the card and the title carry it.
|
|
222
|
+
void ctx.resume?.().catch(() => undefined);
|
|
223
|
+
} catch {
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
const beep = () => {
|
|
227
|
+
if (!audio || audio.state !== "running") return;
|
|
228
|
+
const now = audio.currentTime;
|
|
229
|
+
for (const [i, f] of [660, 880].entries()) {
|
|
230
|
+
const osc = audio.createOscillator();
|
|
231
|
+
const gain = audio.createGain();
|
|
232
|
+
osc.frequency.value = f;
|
|
233
|
+
gain.gain.setValueAtTime(0.0001, now + i * 0.22);
|
|
234
|
+
gain.gain.exponentialRampToValueAtTime(0.08, now + i * 0.22 + 0.02);
|
|
235
|
+
gain.gain.exponentialRampToValueAtTime(0.0001, now + i * 0.22 + 0.2);
|
|
236
|
+
osc.connect(gain).connect(audio.destination);
|
|
237
|
+
osc.start(now + i * 0.22);
|
|
238
|
+
osc.stop(now + i * 0.22 + 0.21);
|
|
239
|
+
}
|
|
240
|
+
};
|
|
241
|
+
beep();
|
|
242
|
+
toneTimer = setInterval(beep, 2200);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function stopTone(): void {
|
|
246
|
+
clearInterval(toneTimer);
|
|
247
|
+
toneTimer = undefined;
|
|
248
|
+
}
|
package/src/ui/strings.ts
CHANGED
|
@@ -65,20 +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;
|
|
77
81
|
/** On the cards before anything is asked of the browser: what each will do. */
|
|
78
82
|
chooseProblemHint: string;
|
|
79
83
|
chooseRequestHint: string;
|
|
80
84
|
requestTitle: string;
|
|
81
85
|
requestTypeLabel: string;
|
|
86
|
+
/** Over a long list of types: the search, and what it says when nothing matches. */
|
|
87
|
+
requestTypeSearch: string;
|
|
88
|
+
requestTypeNone: string;
|
|
89
|
+
/** Carries `{n}`. */
|
|
90
|
+
requestTypeCount: string;
|
|
82
91
|
/** Carries `{time}`. */
|
|
83
92
|
requestTarget: string;
|
|
84
93
|
requestSummaryLabel: string;
|
|
@@ -99,6 +108,16 @@ export interface Strings {
|
|
|
99
108
|
/** Carry `{n}`. */
|
|
100
109
|
days: string;
|
|
101
110
|
hours: string;
|
|
111
|
+
/** A call from the support team (ui/ring). `ringAbout` carries `{key}` and `{title}`. */
|
|
112
|
+
ringTitle: string;
|
|
113
|
+
ringAbout: string;
|
|
114
|
+
ringHow: string;
|
|
115
|
+
ringOpen: string;
|
|
116
|
+
ringDecline: string;
|
|
117
|
+
ringOpened: string;
|
|
118
|
+
ringBlocked: string;
|
|
119
|
+
ringClose: string;
|
|
120
|
+
ringTab: string;
|
|
102
121
|
}
|
|
103
122
|
|
|
104
123
|
const th: Strings = {
|
|
@@ -146,13 +165,19 @@ const th: Strings = {
|
|
|
146
165
|
handoffOpen: "เปิดในแท็บใหม่",
|
|
147
166
|
chooseTitle: "ต้องการให้ทีมช่วยเรื่องอะไร",
|
|
148
167
|
kindProblem: "แจ้งปัญหา",
|
|
149
|
-
kindProblemHint: "มีอะไรเสีย หรือใช้งานไม่ได้",
|
|
150
168
|
kindRequest: "คำขอ",
|
|
151
|
-
|
|
169
|
+
askInstead: "เป็นคำขอ?",
|
|
170
|
+
askInsteadAction: "เลือกเรื่องที่ขอ",
|
|
171
|
+
problemInstead: "เป็นปัญหาที่ต้องแก้?",
|
|
172
|
+
problemInsteadAction: "แจ้งปัญหาแทน",
|
|
173
|
+
problemInsteadShare: "แจ้งปัญหาแทน (แนบภาพหน้าจอ)",
|
|
152
174
|
chooseProblemHint: "มีอะไรเสีย หรือใช้งานไม่ได้ — แนบภาพหน้าจอ",
|
|
153
175
|
chooseRequestHint: "ขอให้ทีมทำ เช่น ขอสิทธิ์ ขอแก้ข้อมูล — ไม่ต้องแชร์หน้าจอ",
|
|
154
176
|
requestTitle: "ขอให้ทีมดำเนินการ",
|
|
155
177
|
requestTypeLabel: "เรื่องที่ขอ",
|
|
178
|
+
requestTypeSearch: "ค้นหาเรื่องที่ขอ",
|
|
179
|
+
requestTypeNone: "ไม่พบเรื่องที่ขอ — ลองคำอื่น",
|
|
180
|
+
requestTypeCount: "{n} เรื่อง — เลื่อนดูหรือพิมพ์ค้นหา",
|
|
156
181
|
requestTarget: "ทีมตั้งใจให้เสร็จภายใน {time}",
|
|
157
182
|
requestSummaryLabel: "หัวข้อ",
|
|
158
183
|
requestSummaryPlaceholder: "ขออะไร — สั้น ๆ",
|
|
@@ -168,6 +193,15 @@ const th: Strings = {
|
|
|
168
193
|
requestLeavesWhat: "บันทึกการใช้งานของหน้านี้ — ใช้กับการแจ้งปัญหาเท่านั้น",
|
|
169
194
|
days: "{n} วัน",
|
|
170
195
|
hours: "{n} ชั่วโมง",
|
|
196
|
+
ringTitle: "ทีมงานกำลังโทรหาคุณ",
|
|
197
|
+
ringAbout: "เรื่อง {key} · {title}",
|
|
198
|
+
ringHow: "รับสายในหน้าคำร้องของคุณ — เปิดในแท็บใหม่ ไม่ต้องเข้าสู่ระบบอีก",
|
|
199
|
+
ringOpen: "เปิดเพื่อรับสาย",
|
|
200
|
+
ringDecline: "ไม่รับ",
|
|
201
|
+
ringOpened: "เปิดหน้ารับสายแล้ว — กด รับสาย ในแท็บที่เปิดขึ้น",
|
|
202
|
+
ringBlocked: "เบราว์เซอร์บล็อกแท็บใหม่ — กดที่นี่เพื่อเปิดหน้ารับสาย",
|
|
203
|
+
ringClose: "ปิด",
|
|
204
|
+
ringTab: "📞 สายเข้า",
|
|
171
205
|
};
|
|
172
206
|
|
|
173
207
|
const en: Strings = {
|
|
@@ -215,13 +249,19 @@ const en: Strings = {
|
|
|
215
249
|
handoffOpen: "Open in a new tab",
|
|
216
250
|
chooseTitle: "How can the team help?",
|
|
217
251
|
kindProblem: "Report a problem",
|
|
218
|
-
kindProblemHint: "Something is broken or not working",
|
|
219
252
|
kindRequest: "Request",
|
|
220
|
-
|
|
253
|
+
askInstead: "Asking for something?",
|
|
254
|
+
askInsteadAction: "Choose what you need",
|
|
255
|
+
problemInstead: "Something broken?",
|
|
256
|
+
problemInsteadAction: "Report a problem instead",
|
|
257
|
+
problemInsteadShare: "Report a problem instead (with a screenshot)",
|
|
221
258
|
chooseProblemHint: "Something is broken or not working — with a screenshot",
|
|
222
259
|
chooseRequestHint: "Ask the team to do something, like access or a data change — no screen sharing",
|
|
223
260
|
requestTitle: "Ask the team",
|
|
224
261
|
requestTypeLabel: "What you need",
|
|
262
|
+
requestTypeSearch: "Search what you need",
|
|
263
|
+
requestTypeNone: "Nothing matches — try another word",
|
|
264
|
+
requestTypeCount: "{n} to choose from — scroll or search",
|
|
225
265
|
requestTarget: "The team aims to finish within {time}",
|
|
226
266
|
requestSummaryLabel: "Subject",
|
|
227
267
|
requestSummaryPlaceholder: "What do you need — briefly",
|
|
@@ -237,6 +277,15 @@ const en: Strings = {
|
|
|
237
277
|
requestLeavesWhat: "this page's activity log — it is for problem reports only",
|
|
238
278
|
days: "{n} days",
|
|
239
279
|
hours: "{n} hours",
|
|
280
|
+
ringTitle: "The support team is calling you",
|
|
281
|
+
ringAbout: "About {key} · {title}",
|
|
282
|
+
ringHow: "Answer on your request's page — it opens in a new tab, already signed in",
|
|
283
|
+
ringOpen: "Open to answer",
|
|
284
|
+
ringDecline: "Decline",
|
|
285
|
+
ringOpened: "Opened — press Answer in the new tab",
|
|
286
|
+
ringBlocked: "Your browser blocked the new tab — open the call here",
|
|
287
|
+
ringClose: "Close",
|
|
288
|
+
ringTab: "📞 Incoming call",
|
|
240
289
|
};
|
|
241
290
|
|
|
242
291
|
export function strings(locale: "th" | "en" | undefined): Strings {
|
package/src/ui/styles.ts
CHANGED
|
@@ -34,11 +34,11 @@ h2 { margin: 0; font-size: 20px; font-weight: 700; flex: 1; }
|
|
|
34
34
|
label { display: block; font-size: 14px; font-weight: 600; margin-bottom: 6px; }
|
|
35
35
|
.req { color: #dc2626; }
|
|
36
36
|
.hint { font-weight: 400; color: #64748b; }
|
|
37
|
-
input[type="text"], textarea, select {
|
|
37
|
+
input[type="text"], input[type="search"], textarea, select {
|
|
38
38
|
width: 100%; padding: 10px 12px; font: inherit; font-size: 14px;
|
|
39
39
|
border: 1px solid #cbd5e1; border-radius: 10px; background: #fff; color: inherit;
|
|
40
40
|
}
|
|
41
|
-
input[type="text"]:focus, textarea:focus, select:focus {
|
|
41
|
+
input[type="text"]:focus, input[type="search"]:focus, textarea:focus, select:focus {
|
|
42
42
|
outline: none; border-color: #2563eb; box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.18);
|
|
43
43
|
}
|
|
44
44
|
textarea { min-height: 88px; resize: vertical; }
|
|
@@ -98,8 +98,10 @@ canvas { width: 100%; height: auto; display: block; border: 1px solid #e2e8f0; b
|
|
|
98
98
|
แจ้งปัญหา · คำขอ as two cards under the heading, the way the desk's own
|
|
99
99
|
สร้างเวิร์กโฟลว์ asks it; what is asked for as cards, the way the portal lists it. */
|
|
100
100
|
[hidden] { display: none !important; }
|
|
101
|
-
|
|
102
|
-
|
|
101
|
+
/* The press's choice: two cards in a smaller dialog, before anything is asked of the browser. */
|
|
102
|
+
.kinds { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; }
|
|
103
|
+
/* In the form, the choice is made: one quiet line under the heading to switch. */
|
|
104
|
+
.door { margin: -8px 0 0; font-size: 13px; color: #64748b; }
|
|
103
105
|
.panel.narrow { width: min(560px, 100%); }
|
|
104
106
|
.kinds.choose { margin: 0 0 24px; }
|
|
105
107
|
.kinds.choose button.kind { padding: 16px 18px; gap: 6px; }
|
|
@@ -120,7 +122,16 @@ button.kind[aria-checked="true"], button.kind[aria-checked="true"]:hover:not(:di
|
|
|
120
122
|
button.kind[aria-checked="true"] .tick { opacity: 1; }
|
|
121
123
|
button.kind:focus-visible, button.type:focus-visible { outline: none; box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.25); }
|
|
122
124
|
button.kind[aria-checked="true"]:focus-visible { box-shadow: inset 0 0 0 1px #2563eb, 0 0 0 3px rgba(37, 99, 235, 0.25); }
|
|
123
|
-
.types { display: grid; gap:
|
|
125
|
+
.types { display: grid; gap: 6px; }
|
|
126
|
+
/* A long list scrolls in its own height, so the title and ส่งคำขอ stay near. */
|
|
127
|
+
.types.long { max-height: 300px; overflow-y: auto; padding: 2px; margin: -2px; overscroll-behavior: contain; }
|
|
128
|
+
.type-search { margin-bottom: 8px; }
|
|
129
|
+
.ask .type-count { margin: 6px 0 0; font-size: 12px; }
|
|
130
|
+
/* The rest are one line: only the chosen one opens up with its words and the team's time. */
|
|
131
|
+
button.type:not([aria-checked="true"]) { padding: 8px 12px; align-items: center; }
|
|
132
|
+
button.type:not([aria-checked="true"]) .type-hint, button.type:not([aria-checked="true"]) .type-due { display: none; }
|
|
133
|
+
button.type:not([aria-checked="true"]) .type-body b { font-weight: 500; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
134
|
+
button.type:not([aria-checked="true"]) .icon { margin-top: 0; }
|
|
124
135
|
button.type {
|
|
125
136
|
display: flex; align-items: flex-start; gap: 10px; width: 100%;
|
|
126
137
|
padding: 10px 12px; text-align: left; border-radius: 10px;
|