@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 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(true);
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
- let attachmentKeys = [];
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 = [await api.uploadScreenshot(blob)];
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) deeplinkTicket = null;
267
- setOpen((o) => !o);
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
- "Attach a screenshot"
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
- mountSupportWidget
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.6",
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",