@tmls-ai/support 0.1.6 → 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 +86 -12
- 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);
|
|
@@ -180,16 +196,31 @@ function SupportWidget(props) {
|
|
|
180
196
|
setCapturing(false);
|
|
181
197
|
}
|
|
182
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));
|
|
183
208
|
const submit = useCallback(async () => {
|
|
184
209
|
if (!message.trim() || sending) return;
|
|
185
210
|
setSending(true);
|
|
186
211
|
try {
|
|
187
|
-
|
|
212
|
+
const attachmentKeys = [];
|
|
213
|
+
for (const f of files) {
|
|
214
|
+
try {
|
|
215
|
+
attachmentKeys.push(await api.uploadScreenshot(f));
|
|
216
|
+
} catch {
|
|
217
|
+
}
|
|
218
|
+
}
|
|
188
219
|
if (withShot && !captureShot._blob) await captureShot();
|
|
189
220
|
const blob = withShot ? captureShot._blob : void 0;
|
|
190
221
|
if (blob) {
|
|
191
222
|
try {
|
|
192
|
-
attachmentKeys
|
|
223
|
+
attachmentKeys.push(await api.uploadScreenshot(blob));
|
|
193
224
|
} catch {
|
|
194
225
|
}
|
|
195
226
|
}
|
|
@@ -203,6 +234,7 @@ function SupportWidget(props) {
|
|
|
203
234
|
setLastTicket(ticketNumber);
|
|
204
235
|
setMessage("");
|
|
205
236
|
setShotPreview(null);
|
|
237
|
+
setFiles([]);
|
|
206
238
|
setView("sent");
|
|
207
239
|
} catch {
|
|
208
240
|
setLastTicket(null);
|
|
@@ -210,7 +242,7 @@ function SupportWidget(props) {
|
|
|
210
242
|
} finally {
|
|
211
243
|
setSending(false);
|
|
212
244
|
}
|
|
213
|
-
}, [api, type, severity, message, withShot, getContext, appVersion, sending, captureShot]);
|
|
245
|
+
}, [api, type, severity, message, withShot, files, getContext, appVersion, sending, captureShot]);
|
|
214
246
|
const openThreads = useCallback(async () => {
|
|
215
247
|
setThreads(await api.listConversations());
|
|
216
248
|
setView("threads");
|
|
@@ -262,11 +294,22 @@ function SupportWidget(props) {
|
|
|
262
294
|
background: active ? accent : "transparent",
|
|
263
295
|
color: active ? "#fff" : "#c8c8d0"
|
|
264
296
|
});
|
|
297
|
+
const openPanel = () => {
|
|
298
|
+
setOpen(true);
|
|
299
|
+
if (withShot) captureShot();
|
|
300
|
+
};
|
|
301
|
+
const closePanel = () => {
|
|
302
|
+
deeplinkTicket = null;
|
|
303
|
+
setOpen(false);
|
|
304
|
+
};
|
|
265
305
|
const toggle = () => {
|
|
266
|
-
if (open)
|
|
267
|
-
|
|
268
|
-
if (!open && withShot) captureShot();
|
|
306
|
+
if (open) closePanel();
|
|
307
|
+
else openPanel();
|
|
269
308
|
};
|
|
309
|
+
useEffect(() => subscribeSupport((action) => {
|
|
310
|
+
if (action === "open") openPanel();
|
|
311
|
+
else closePanel();
|
|
312
|
+
}), [withShot]);
|
|
270
313
|
const panelStyle = isMobile ? {
|
|
271
314
|
position: "fixed",
|
|
272
315
|
left: 0,
|
|
@@ -364,16 +407,45 @@ function SupportWidget(props) {
|
|
|
364
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" }
|
|
365
408
|
}
|
|
366
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)) }),
|
|
367
439
|
/* @__PURE__ */ jsxs("label", { style: { display: "flex", alignItems: "center", gap: 8, fontSize: 13, color: "#a0a0a8", cursor: "pointer" }, children: [
|
|
368
440
|
/* @__PURE__ */ jsx("input", { type: "checkbox", checked: withShot, onChange: (e) => {
|
|
369
441
|
setWithShot(e.target.checked);
|
|
370
442
|
if (e.target.checked) captureShot();
|
|
371
443
|
else setShotPreview(null);
|
|
372
444
|
} }),
|
|
373
|
-
"
|
|
445
|
+
"Also include a screenshot of the app"
|
|
374
446
|
] }),
|
|
375
447
|
withShot && capturing && !shotPreview && /* @__PURE__ */ jsx("div", { style: { fontSize: 12, color: "#8a8a92", padding: "10px 0", textAlign: "center" }, children: "Capturing screenshot\u2026" }),
|
|
376
|
-
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" } }),
|
|
377
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" })
|
|
378
450
|
] }),
|
|
379
451
|
view === "sent" && /* @__PURE__ */ jsxs("div", { style: { textAlign: "center", padding: "20px 0" }, children: [
|
|
@@ -438,7 +510,7 @@ function SupportWidget(props) {
|
|
|
438
510
|
] })
|
|
439
511
|
] })
|
|
440
512
|
] }),
|
|
441
|
-
(!isMobile || !open) && /* @__PURE__ */ jsx("button", { onClick: toggle, "aria-label": "Support", style: launcherStyle, children: open && !isMobile ? "\xD7" : "?" })
|
|
513
|
+
showLauncher && (!isMobile || !open) && /* @__PURE__ */ jsx("button", { onClick: toggle, "aria-label": "Support", style: launcherStyle, children: open && !isMobile ? "\xD7" : "?" })
|
|
442
514
|
] });
|
|
443
515
|
}
|
|
444
516
|
var tab = (active, accent) => ({
|
|
@@ -489,5 +561,7 @@ function mountSupportWidget(props) {
|
|
|
489
561
|
}
|
|
490
562
|
export {
|
|
491
563
|
SupportWidget,
|
|
492
|
-
|
|
564
|
+
closeSupport,
|
|
565
|
+
mountSupportWidget,
|
|
566
|
+
openSupport
|
|
493
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",
|