@tmls-ai/support 0.1.6 → 0.1.8
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 +94 -13
- 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,
|
|
@@ -327,7 +370,14 @@ function SupportWidget(props) {
|
|
|
327
370
|
background: accent,
|
|
328
371
|
color: "#fff",
|
|
329
372
|
fontSize: 22,
|
|
330
|
-
boxShadow: "0 4px 16px rgba(0,0,0,0.35)"
|
|
373
|
+
boxShadow: "0 4px 16px rgba(0,0,0,0.35)",
|
|
374
|
+
// Center the ×/? glyph in the circle. Without flex centering the glyph
|
|
375
|
+
// renders as baseline-aligned inline text and sits visibly off-center.
|
|
376
|
+
display: "flex",
|
|
377
|
+
alignItems: "center",
|
|
378
|
+
justifyContent: "center",
|
|
379
|
+
lineHeight: 1,
|
|
380
|
+
padding: 0
|
|
331
381
|
};
|
|
332
382
|
return /* @__PURE__ */ jsxs("div", { "data-tmls-support-root": "true", style: { position: "fixed", bottom: 20, right: 20, zIndex: 2147483e3, fontFamily: "system-ui, sans-serif" }, children: [
|
|
333
383
|
open && /* @__PURE__ */ jsxs("div", { style: panelStyle, children: [
|
|
@@ -364,16 +414,45 @@ function SupportWidget(props) {
|
|
|
364
414
|
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
415
|
}
|
|
366
416
|
),
|
|
417
|
+
/* @__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: [
|
|
418
|
+
/* @__PURE__ */ jsx(
|
|
419
|
+
"input",
|
|
420
|
+
{
|
|
421
|
+
type: "file",
|
|
422
|
+
accept: "image/*",
|
|
423
|
+
multiple: true,
|
|
424
|
+
style: { display: "none" },
|
|
425
|
+
onChange: (e) => {
|
|
426
|
+
addFiles(e.target.files);
|
|
427
|
+
e.currentTarget.value = "";
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
),
|
|
431
|
+
/* @__PURE__ */ jsx("span", { style: { fontSize: 15, lineHeight: 1 }, children: "+" }),
|
|
432
|
+
files.length ? `Add more images (${files.length})` : "Attach image(s)"
|
|
433
|
+
] }),
|
|
434
|
+
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: [
|
|
435
|
+
/* @__PURE__ */ jsx("img", { src, alt: "", style: { width: 64, height: 64, objectFit: "cover", borderRadius: 8, border: "0.5px solid #3a3a42", display: "block" } }),
|
|
436
|
+
/* @__PURE__ */ jsx(
|
|
437
|
+
"button",
|
|
438
|
+
{
|
|
439
|
+
onClick: () => removeFile(i),
|
|
440
|
+
"aria-label": "Remove image",
|
|
441
|
+
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)" },
|
|
442
|
+
children: "\xD7"
|
|
443
|
+
}
|
|
444
|
+
)
|
|
445
|
+
] }, i)) }),
|
|
367
446
|
/* @__PURE__ */ jsxs("label", { style: { display: "flex", alignItems: "center", gap: 8, fontSize: 13, color: "#a0a0a8", cursor: "pointer" }, children: [
|
|
368
447
|
/* @__PURE__ */ jsx("input", { type: "checkbox", checked: withShot, onChange: (e) => {
|
|
369
448
|
setWithShot(e.target.checked);
|
|
370
449
|
if (e.target.checked) captureShot();
|
|
371
450
|
else setShotPreview(null);
|
|
372
451
|
} }),
|
|
373
|
-
"
|
|
452
|
+
"Also include a screenshot of the app"
|
|
374
453
|
] }),
|
|
375
454
|
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" } }),
|
|
455
|
+
withShot && shotPreview && /* @__PURE__ */ jsx("img", { src: shotPreview, alt: "", style: { width: "100%", borderRadius: 8, border: "0.5px solid #3a3a42" } }),
|
|
377
456
|
/* @__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
457
|
] }),
|
|
379
458
|
view === "sent" && /* @__PURE__ */ jsxs("div", { style: { textAlign: "center", padding: "20px 0" }, children: [
|
|
@@ -438,7 +517,7 @@ function SupportWidget(props) {
|
|
|
438
517
|
] })
|
|
439
518
|
] })
|
|
440
519
|
] }),
|
|
441
|
-
(!isMobile || !open) && /* @__PURE__ */ jsx("button", { onClick: toggle, "aria-label": "Support", style: launcherStyle, children: open && !isMobile ? "\xD7" : "?" })
|
|
520
|
+
showLauncher && (!isMobile || !open) && /* @__PURE__ */ jsx("button", { onClick: toggle, "aria-label": "Support", style: launcherStyle, children: open && !isMobile ? "\xD7" : "?" })
|
|
442
521
|
] });
|
|
443
522
|
}
|
|
444
523
|
var tab = (active, accent) => ({
|
|
@@ -489,5 +568,7 @@ function mountSupportWidget(props) {
|
|
|
489
568
|
}
|
|
490
569
|
export {
|
|
491
570
|
SupportWidget,
|
|
492
|
-
|
|
571
|
+
closeSupport,
|
|
572
|
+
mountSupportWidget,
|
|
573
|
+
openSupport
|
|
493
574
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tmls-ai/support",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.8",
|
|
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",
|