@tmls-ai/support 0.1.5 → 0.1.7
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/dist/index.d.ts +10 -1
- package/dist/index.js +176 -48
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -38,10 +38,19 @@ interface SupportWidgetProps {
|
|
|
38
38
|
getContext?: () => Record<string, unknown>;
|
|
39
39
|
/** App version string for the base context. */
|
|
40
40
|
appVersion?: string;
|
|
41
|
+
/** Show the built-in floating launcher (FAB on desktop, peek-tab on mobile).
|
|
42
|
+
* Default true. Set false to drive the panel entirely from your own trigger
|
|
43
|
+
* (a menu entry, a settings row) via the exported openSupport(). */
|
|
44
|
+
showLauncher?: boolean;
|
|
41
45
|
}
|
|
42
46
|
|
|
43
47
|
declare function SupportWidget(props: SupportWidgetProps): react.JSX.Element;
|
|
44
48
|
|
|
49
|
+
/** Open the support panel. No-op if no <SupportWidget> is mounted. */
|
|
50
|
+
declare function openSupport(): void;
|
|
51
|
+
/** Close the support panel. */
|
|
52
|
+
declare function closeSupport(): void;
|
|
53
|
+
|
|
45
54
|
/**
|
|
46
55
|
* Vanilla mount for any app (React or not). Renders the widget into a Shadow
|
|
47
56
|
* DOM root so the host's CSS and the widget's CSS can never collide. React apps
|
|
@@ -49,4 +58,4 @@ declare function SupportWidget(props: SupportWidgetProps): react.JSX.Element;
|
|
|
49
58
|
*/
|
|
50
59
|
declare function mountSupportWidget(props: SupportWidgetProps): () => void;
|
|
51
60
|
|
|
52
|
-
export { type Conversation, type Message, type ReportType, type Severity, SupportWidget, type SupportWidgetProps, mountSupportWidget };
|
|
61
|
+
export { type Conversation, type Message, type ReportType, type Severity, SupportWidget, type SupportWidgetProps, closeSupport, mountSupportWidget, openSupport };
|
package/dist/index.js
CHANGED
|
@@ -94,6 +94,21 @@ var SupportApi = class {
|
|
|
94
94
|
}
|
|
95
95
|
};
|
|
96
96
|
|
|
97
|
+
// src/controller.ts
|
|
98
|
+
var listeners = /* @__PURE__ */ new Set();
|
|
99
|
+
function openSupport() {
|
|
100
|
+
listeners.forEach((l) => l("open"));
|
|
101
|
+
}
|
|
102
|
+
function closeSupport() {
|
|
103
|
+
listeners.forEach((l) => l("close"));
|
|
104
|
+
}
|
|
105
|
+
function subscribeSupport(listener) {
|
|
106
|
+
listeners.add(listener);
|
|
107
|
+
return () => {
|
|
108
|
+
listeners.delete(listener);
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
|
|
97
112
|
// src/SupportWidget.tsx
|
|
98
113
|
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
99
114
|
var TYPES = [
|
|
@@ -117,15 +132,16 @@ var deeplinkTicket = (() => {
|
|
|
117
132
|
return tn;
|
|
118
133
|
})();
|
|
119
134
|
function SupportWidget(props) {
|
|
120
|
-
const { productId, apiUrl, getToken, accent = "#0a84ff", getContext, appVersion } = props;
|
|
135
|
+
const { productId, apiUrl, getToken, accent = "#0a84ff", getContext, appVersion, showLauncher = true } = props;
|
|
121
136
|
const api = useMemo(() => new SupportApi(apiUrl, getToken), [apiUrl, getToken]);
|
|
122
137
|
const [open, setOpen] = useState(false);
|
|
123
138
|
const [view, setView] = useState("new");
|
|
124
139
|
const [type, setType] = useState("bug");
|
|
125
140
|
const [severity, setSeverity] = useState("annoying");
|
|
126
141
|
const [message, setMessage] = useState("");
|
|
127
|
-
const [withShot, setWithShot] = useState(
|
|
142
|
+
const [withShot, setWithShot] = useState(false);
|
|
128
143
|
const [shotPreview, setShotPreview] = useState(null);
|
|
144
|
+
const [files, setFiles] = useState([]);
|
|
129
145
|
const [capturing, setCapturing] = useState(false);
|
|
130
146
|
const [sending, setSending] = useState(false);
|
|
131
147
|
const [lastTicket, setLastTicket] = useState(null);
|
|
@@ -135,6 +151,17 @@ function SupportWidget(props) {
|
|
|
135
151
|
const [messages, setMessages] = useState([]);
|
|
136
152
|
const [shotUrls, setShotUrls] = useState({});
|
|
137
153
|
const [reply, setReply] = useState("");
|
|
154
|
+
const [isMobile, setIsMobile] = useState(
|
|
155
|
+
() => typeof window !== "undefined" && window.matchMedia("(max-width: 767px)").matches
|
|
156
|
+
);
|
|
157
|
+
useEffect(() => {
|
|
158
|
+
if (typeof window === "undefined") return;
|
|
159
|
+
const mq = window.matchMedia("(max-width: 767px)");
|
|
160
|
+
const onChange = () => setIsMobile(mq.matches);
|
|
161
|
+
onChange();
|
|
162
|
+
mq.addEventListener("change", onChange);
|
|
163
|
+
return () => mq.removeEventListener("change", onChange);
|
|
164
|
+
}, []);
|
|
138
165
|
useEffect(() => {
|
|
139
166
|
installErrorCapture();
|
|
140
167
|
}, []);
|
|
@@ -169,16 +196,31 @@ function SupportWidget(props) {
|
|
|
169
196
|
setCapturing(false);
|
|
170
197
|
}
|
|
171
198
|
}, []);
|
|
199
|
+
const filePreviews = useMemo(() => files.map((f) => URL.createObjectURL(f)), [files]);
|
|
200
|
+
useEffect(() => () => {
|
|
201
|
+
filePreviews.forEach(URL.revokeObjectURL);
|
|
202
|
+
}, [filePreviews]);
|
|
203
|
+
const addFiles = (picked) => {
|
|
204
|
+
const imgs = Array.from(picked || []).filter((f) => f.type.startsWith("image/"));
|
|
205
|
+
if (imgs.length) setFiles((prev) => [...prev, ...imgs]);
|
|
206
|
+
};
|
|
207
|
+
const removeFile = (idx) => setFiles((prev) => prev.filter((_, i) => i !== idx));
|
|
172
208
|
const submit = useCallback(async () => {
|
|
173
209
|
if (!message.trim() || sending) return;
|
|
174
210
|
setSending(true);
|
|
175
211
|
try {
|
|
176
|
-
|
|
212
|
+
const attachmentKeys = [];
|
|
213
|
+
for (const f of files) {
|
|
214
|
+
try {
|
|
215
|
+
attachmentKeys.push(await api.uploadScreenshot(f));
|
|
216
|
+
} catch {
|
|
217
|
+
}
|
|
218
|
+
}
|
|
177
219
|
if (withShot && !captureShot._blob) await captureShot();
|
|
178
220
|
const blob = withShot ? captureShot._blob : void 0;
|
|
179
221
|
if (blob) {
|
|
180
222
|
try {
|
|
181
|
-
attachmentKeys
|
|
223
|
+
attachmentKeys.push(await api.uploadScreenshot(blob));
|
|
182
224
|
} catch {
|
|
183
225
|
}
|
|
184
226
|
}
|
|
@@ -192,6 +234,7 @@ function SupportWidget(props) {
|
|
|
192
234
|
setLastTicket(ticketNumber);
|
|
193
235
|
setMessage("");
|
|
194
236
|
setShotPreview(null);
|
|
237
|
+
setFiles([]);
|
|
195
238
|
setView("sent");
|
|
196
239
|
} catch {
|
|
197
240
|
setLastTicket(null);
|
|
@@ -199,7 +242,7 @@ function SupportWidget(props) {
|
|
|
199
242
|
} finally {
|
|
200
243
|
setSending(false);
|
|
201
244
|
}
|
|
202
|
-
}, [api, type, severity, message, withShot, getContext, appVersion, sending, captureShot]);
|
|
245
|
+
}, [api, type, severity, message, withShot, files, getContext, appVersion, sending, captureShot]);
|
|
203
246
|
const openThreads = useCallback(async () => {
|
|
204
247
|
setThreads(await api.listConversations());
|
|
205
248
|
setView("threads");
|
|
@@ -251,27 +294,103 @@ function SupportWidget(props) {
|
|
|
251
294
|
background: active ? accent : "transparent",
|
|
252
295
|
color: active ? "#fff" : "#c8c8d0"
|
|
253
296
|
});
|
|
297
|
+
const openPanel = () => {
|
|
298
|
+
setOpen(true);
|
|
299
|
+
if (withShot) captureShot();
|
|
300
|
+
};
|
|
301
|
+
const closePanel = () => {
|
|
302
|
+
deeplinkTicket = null;
|
|
303
|
+
setOpen(false);
|
|
304
|
+
};
|
|
305
|
+
const toggle = () => {
|
|
306
|
+
if (open) closePanel();
|
|
307
|
+
else openPanel();
|
|
308
|
+
};
|
|
309
|
+
useEffect(() => subscribeSupport((action) => {
|
|
310
|
+
if (action === "open") openPanel();
|
|
311
|
+
else closePanel();
|
|
312
|
+
}), [withShot]);
|
|
313
|
+
const panelStyle = isMobile ? {
|
|
314
|
+
position: "fixed",
|
|
315
|
+
left: 0,
|
|
316
|
+
right: 0,
|
|
317
|
+
bottom: 0,
|
|
318
|
+
width: "auto",
|
|
319
|
+
maxHeight: "85vh",
|
|
320
|
+
background: "#141418",
|
|
321
|
+
color: "#e8e8ea",
|
|
322
|
+
borderTop: "0.5px solid rgba(255,255,255,0.12)",
|
|
323
|
+
borderRadius: "16px 16px 0 0",
|
|
324
|
+
boxShadow: "0 -16px 48px rgba(0,0,0,0.5)",
|
|
325
|
+
overflow: "hidden",
|
|
326
|
+
display: "flex",
|
|
327
|
+
flexDirection: "column",
|
|
328
|
+
zIndex: 2147483e3,
|
|
329
|
+
paddingBottom: "env(safe-area-inset-bottom, 0px)"
|
|
330
|
+
} : {
|
|
331
|
+
position: "absolute",
|
|
332
|
+
bottom: 60,
|
|
333
|
+
right: 0,
|
|
334
|
+
width: 360,
|
|
335
|
+
maxHeight: 560,
|
|
336
|
+
background: "#141418",
|
|
337
|
+
color: "#e8e8ea",
|
|
338
|
+
border: "0.5px solid rgba(255,255,255,0.12)",
|
|
339
|
+
borderRadius: 16,
|
|
340
|
+
boxShadow: "0 16px 48px rgba(0,0,0,0.5)",
|
|
341
|
+
overflow: "hidden",
|
|
342
|
+
display: "flex",
|
|
343
|
+
flexDirection: "column"
|
|
344
|
+
};
|
|
345
|
+
const launcherStyle = isMobile ? {
|
|
346
|
+
position: "fixed",
|
|
347
|
+
right: 0,
|
|
348
|
+
top: "50%",
|
|
349
|
+
transform: "translateY(-50%)",
|
|
350
|
+
width: 30,
|
|
351
|
+
height: 56,
|
|
352
|
+
borderRadius: "14px 0 0 14px",
|
|
353
|
+
border: "none",
|
|
354
|
+
cursor: "pointer",
|
|
355
|
+
background: accent,
|
|
356
|
+
color: "#fff",
|
|
357
|
+
fontSize: 20,
|
|
358
|
+
boxShadow: "-3px 0 14px rgba(0,0,0,0.4)",
|
|
359
|
+
display: "flex",
|
|
360
|
+
alignItems: "center",
|
|
361
|
+
justifyContent: "center",
|
|
362
|
+
paddingRight: 4,
|
|
363
|
+
zIndex: 2147483e3
|
|
364
|
+
} : {
|
|
365
|
+
width: 48,
|
|
366
|
+
height: 48,
|
|
367
|
+
borderRadius: 999,
|
|
368
|
+
border: "none",
|
|
369
|
+
cursor: "pointer",
|
|
370
|
+
background: accent,
|
|
371
|
+
color: "#fff",
|
|
372
|
+
fontSize: 22,
|
|
373
|
+
boxShadow: "0 4px 16px rgba(0,0,0,0.35)"
|
|
374
|
+
};
|
|
254
375
|
return /* @__PURE__ */ jsxs("div", { "data-tmls-support-root": "true", style: { position: "fixed", bottom: 20, right: 20, zIndex: 2147483e3, fontFamily: "system-ui, sans-serif" }, children: [
|
|
255
|
-
open && /* @__PURE__ */ jsxs("div", { style:
|
|
256
|
-
position: "absolute",
|
|
257
|
-
bottom: 60,
|
|
258
|
-
right: 0,
|
|
259
|
-
width: 360,
|
|
260
|
-
maxHeight: 560,
|
|
261
|
-
background: "#141418",
|
|
262
|
-
color: "#e8e8ea",
|
|
263
|
-
border: "0.5px solid rgba(255,255,255,0.12)",
|
|
264
|
-
borderRadius: 16,
|
|
265
|
-
boxShadow: "0 16px 48px rgba(0,0,0,0.5)",
|
|
266
|
-
overflow: "hidden",
|
|
267
|
-
display: "flex",
|
|
268
|
-
flexDirection: "column"
|
|
269
|
-
}, children: [
|
|
376
|
+
open && /* @__PURE__ */ jsxs("div", { style: panelStyle, children: [
|
|
270
377
|
/* @__PURE__ */ jsxs("div", { style: { display: "flex", justifyContent: "space-between", alignItems: "center", padding: "12px 14px", borderBottom: "0.5px solid rgba(255,255,255,0.08)" }, children: [
|
|
271
378
|
/* @__PURE__ */ jsx("strong", { style: { fontSize: 14 }, children: "Support" }),
|
|
272
|
-
/* @__PURE__ */ jsxs("div", { style: { display: "flex", gap: 8, fontSize: 12 }, children: [
|
|
379
|
+
/* @__PURE__ */ jsxs("div", { style: { display: "flex", gap: 8, fontSize: 12, alignItems: "center" }, children: [
|
|
273
380
|
/* @__PURE__ */ jsx("button", { onClick: () => setView("new"), style: tab(view === "new" || view === "sent", accent), children: "New" }),
|
|
274
|
-
/* @__PURE__ */ jsx("button", { onClick: openThreads, style: tab(view === "threads" || view === "thread", accent), children: "My tickets" })
|
|
381
|
+
/* @__PURE__ */ jsx("button", { onClick: openThreads, style: tab(view === "threads" || view === "thread", accent), children: "My tickets" }),
|
|
382
|
+
isMobile && /* @__PURE__ */ jsx(
|
|
383
|
+
"button",
|
|
384
|
+
{
|
|
385
|
+
onClick: () => {
|
|
386
|
+
deeplinkTicket = null;
|
|
387
|
+
setOpen(false);
|
|
388
|
+
},
|
|
389
|
+
"aria-label": "Close",
|
|
390
|
+
style: { background: "transparent", border: "none", cursor: "pointer", color: "#8a8a92", fontSize: 20, lineHeight: 1, padding: "0 2px" },
|
|
391
|
+
children: "\xD7"
|
|
392
|
+
}
|
|
393
|
+
)
|
|
275
394
|
] })
|
|
276
395
|
] }),
|
|
277
396
|
/* @__PURE__ */ jsxs("div", { style: { padding: 14, overflowY: "auto" }, children: [
|
|
@@ -288,16 +407,45 @@ function SupportWidget(props) {
|
|
|
288
407
|
style: { width: "100%", background: "rgba(255,255,255,0.05)", color: "#fff", border: "0.5px solid #3a3a42", borderRadius: 10, padding: 10, fontSize: 14, resize: "vertical" }
|
|
289
408
|
}
|
|
290
409
|
),
|
|
410
|
+
/* @__PURE__ */ jsxs("label", { style: { display: "inline-flex", alignItems: "center", gap: 8, alignSelf: "flex-start", padding: "8px 12px", fontSize: 13, color: "#c8c8d0", cursor: "pointer", border: "0.5px dashed #4a4a52", borderRadius: 10, background: "rgba(255,255,255,0.03)" }, children: [
|
|
411
|
+
/* @__PURE__ */ jsx(
|
|
412
|
+
"input",
|
|
413
|
+
{
|
|
414
|
+
type: "file",
|
|
415
|
+
accept: "image/*",
|
|
416
|
+
multiple: true,
|
|
417
|
+
style: { display: "none" },
|
|
418
|
+
onChange: (e) => {
|
|
419
|
+
addFiles(e.target.files);
|
|
420
|
+
e.currentTarget.value = "";
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
),
|
|
424
|
+
/* @__PURE__ */ jsx("span", { style: { fontSize: 15, lineHeight: 1 }, children: "+" }),
|
|
425
|
+
files.length ? `Add more images (${files.length})` : "Attach image(s)"
|
|
426
|
+
] }),
|
|
427
|
+
filePreviews.length > 0 && /* @__PURE__ */ jsx("div", { style: { display: "flex", flexWrap: "wrap", gap: 8 }, children: filePreviews.map((src, i) => /* @__PURE__ */ jsxs("div", { style: { position: "relative" }, children: [
|
|
428
|
+
/* @__PURE__ */ jsx("img", { src, alt: "", style: { width: 64, height: 64, objectFit: "cover", borderRadius: 8, border: "0.5px solid #3a3a42", display: "block" } }),
|
|
429
|
+
/* @__PURE__ */ jsx(
|
|
430
|
+
"button",
|
|
431
|
+
{
|
|
432
|
+
onClick: () => removeFile(i),
|
|
433
|
+
"aria-label": "Remove image",
|
|
434
|
+
style: { position: "absolute", top: -6, right: -6, width: 20, height: 20, borderRadius: 999, border: "none", cursor: "pointer", background: "#1a1a20", color: "#fff", fontSize: 13, lineHeight: "20px", padding: 0, boxShadow: "0 1px 4px rgba(0,0,0,0.5)" },
|
|
435
|
+
children: "\xD7"
|
|
436
|
+
}
|
|
437
|
+
)
|
|
438
|
+
] }, i)) }),
|
|
291
439
|
/* @__PURE__ */ jsxs("label", { style: { display: "flex", alignItems: "center", gap: 8, fontSize: 13, color: "#a0a0a8", cursor: "pointer" }, children: [
|
|
292
440
|
/* @__PURE__ */ jsx("input", { type: "checkbox", checked: withShot, onChange: (e) => {
|
|
293
441
|
setWithShot(e.target.checked);
|
|
294
442
|
if (e.target.checked) captureShot();
|
|
295
443
|
else setShotPreview(null);
|
|
296
444
|
} }),
|
|
297
|
-
"
|
|
445
|
+
"Also include a screenshot of the app"
|
|
298
446
|
] }),
|
|
299
447
|
withShot && capturing && !shotPreview && /* @__PURE__ */ jsx("div", { style: { fontSize: 12, color: "#8a8a92", padding: "10px 0", textAlign: "center" }, children: "Capturing screenshot\u2026" }),
|
|
300
|
-
shotPreview && /* @__PURE__ */ jsx("img", { src: shotPreview, alt: "", style: { width: "100%", borderRadius: 8, border: "0.5px solid #3a3a42" } }),
|
|
448
|
+
withShot && shotPreview && /* @__PURE__ */ jsx("img", { src: shotPreview, alt: "", style: { width: "100%", borderRadius: 8, border: "0.5px solid #3a3a42" } }),
|
|
301
449
|
/* @__PURE__ */ jsx("button", { onClick: submit, disabled: !message.trim() || sending, style: { ...primary(accent), opacity: !message.trim() || sending ? 0.5 : 1 }, children: sending ? "Sending\u2026" : "Send report" })
|
|
302
450
|
] }),
|
|
303
451
|
view === "sent" && /* @__PURE__ */ jsxs("div", { style: { textAlign: "center", padding: "20px 0" }, children: [
|
|
@@ -362,29 +510,7 @@ function SupportWidget(props) {
|
|
|
362
510
|
] })
|
|
363
511
|
] })
|
|
364
512
|
] }),
|
|
365
|
-
/* @__PURE__ */ jsx(
|
|
366
|
-
"button",
|
|
367
|
-
{
|
|
368
|
-
onClick: () => {
|
|
369
|
-
if (open) deeplinkTicket = null;
|
|
370
|
-
setOpen((o) => !o);
|
|
371
|
-
if (!open && withShot) captureShot();
|
|
372
|
-
},
|
|
373
|
-
"aria-label": "Support",
|
|
374
|
-
style: {
|
|
375
|
-
width: 48,
|
|
376
|
-
height: 48,
|
|
377
|
-
borderRadius: 999,
|
|
378
|
-
border: "none",
|
|
379
|
-
cursor: "pointer",
|
|
380
|
-
background: accent,
|
|
381
|
-
color: "#fff",
|
|
382
|
-
fontSize: 22,
|
|
383
|
-
boxShadow: "0 4px 16px rgba(0,0,0,0.35)"
|
|
384
|
-
},
|
|
385
|
-
children: open ? "\xD7" : "?"
|
|
386
|
-
}
|
|
387
|
-
)
|
|
513
|
+
showLauncher && (!isMobile || !open) && /* @__PURE__ */ jsx("button", { onClick: toggle, "aria-label": "Support", style: launcherStyle, children: open && !isMobile ? "\xD7" : "?" })
|
|
388
514
|
] });
|
|
389
515
|
}
|
|
390
516
|
var tab = (active, accent) => ({
|
|
@@ -435,5 +561,7 @@ function mountSupportWidget(props) {
|
|
|
435
561
|
}
|
|
436
562
|
export {
|
|
437
563
|
SupportWidget,
|
|
438
|
-
|
|
564
|
+
closeSupport,
|
|
565
|
+
mountSupportWidget,
|
|
566
|
+
openSupport
|
|
439
567
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tmls-ai/support",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.7",
|
|
4
4
|
"description": "Embeddable Timeless support widget — bottom-right report overlay (type / severity / screenshot) + ticket thread. Auto-captures context, talks to tmls-support-api.",
|
|
5
5
|
"license": "UNLICENSED",
|
|
6
6
|
"type": "module",
|