@lightworkai.official/debug-capture-react 0.6.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.
Files changed (41) hide show
  1. package/README.md +85 -0
  2. package/dist/DebugCaptureInit.d.ts +25 -0
  3. package/dist/DebugErrorBoundary.d.ts +26 -0
  4. package/dist/ReportProblemButton.d.ts +21 -0
  5. package/dist/components/ImageAnnotatorDialog.d.ts +11 -0
  6. package/dist/components/MyTicketsPanel.d.ts +11 -0
  7. package/dist/components/ReopenPanel.d.ts +7 -0
  8. package/dist/components/RichReplyEditor.d.ts +29 -0
  9. package/dist/components/StatusHero.d.ts +8 -0
  10. package/dist/components/StatusPill.d.ts +14 -0
  11. package/dist/components/TicketBody.d.ts +5 -0
  12. package/dist/components/TicketConversation.d.ts +13 -0
  13. package/dist/components/TicketDetail.d.ts +23 -0
  14. package/dist/components/TicketScreenshots.d.ts +6 -0
  15. package/dist/components/TicketTable.d.ts +18 -0
  16. package/dist/components/TicketTimeline.d.ts +14 -0
  17. package/dist/components/ToolbarButton.d.ts +18 -0
  18. package/dist/components/icons.d.ts +35 -0
  19. package/dist/crash.d.ts +5 -0
  20. package/dist/index.d.ts +23 -0
  21. package/dist/index.mjs +1705 -0
  22. package/package.json +52 -0
  23. package/src/DebugCaptureInit.tsx +56 -0
  24. package/src/DebugErrorBoundary.tsx +50 -0
  25. package/src/ReportProblemButton.tsx +79 -0
  26. package/src/components/ImageAnnotatorDialog.tsx +160 -0
  27. package/src/components/MyTicketsPanel.tsx +321 -0
  28. package/src/components/ReopenPanel.tsx +52 -0
  29. package/src/components/RichReplyEditor.tsx +264 -0
  30. package/src/components/StatusHero.tsx +92 -0
  31. package/src/components/StatusPill.tsx +14 -0
  32. package/src/components/TicketBody.tsx +50 -0
  33. package/src/components/TicketConversation.tsx +177 -0
  34. package/src/components/TicketDetail.tsx +110 -0
  35. package/src/components/TicketScreenshots.tsx +41 -0
  36. package/src/components/TicketTable.tsx +161 -0
  37. package/src/components/TicketTimeline.tsx +81 -0
  38. package/src/components/ToolbarButton.tsx +37 -0
  39. package/src/components/icons.tsx +79 -0
  40. package/src/crash.ts +15 -0
  41. package/src/index.ts +27 -0
package/package.json ADDED
@@ -0,0 +1,52 @@
1
+ {
2
+ "name": "@lightworkai.official/debug-capture-react",
3
+ "version": "0.6.0",
4
+ "description": "React bindings for @lightworkai.official/debug-capture",
5
+ "type": "module",
6
+ "module": "./dist/index.mjs",
7
+ "types": "./dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "import": "./dist/index.mjs",
12
+ "default": "./dist/index.mjs"
13
+ }
14
+ },
15
+ "files": [
16
+ "dist",
17
+ "src",
18
+ "!src/**/*.test.ts",
19
+ "!src/test"
20
+ ],
21
+ "scripts": {
22
+ "build": "rm -rf dist && bun build src/index.ts --outfile dist/index.mjs --format esm --external react --external react/jsx-runtime --external @tiptap/react --external @tiptap/starter-kit --external @tiptap/extension-image --external @tiptap/extension-placeholder --external @lightworkai.official/debug-capture && tsc -p tsconfig.build.json --emitDeclarationOnly --outDir dist",
23
+ "typecheck": "tsc -p tsconfig.json --noEmit"
24
+ },
25
+ "peerDependencies": {
26
+ "react": ">=17",
27
+ "@lightworkai.official/debug-capture": "^0.6.0",
28
+ "@tiptap/react": "^3",
29
+ "@tiptap/starter-kit": "^3",
30
+ "@tiptap/extension-image": "^3",
31
+ "@tiptap/extension-placeholder": "^3"
32
+ },
33
+ "devDependencies": {
34
+ "@types/react": "^19.0.0",
35
+ "react": "^19.0.0",
36
+ "typescript": "^5.6.0"
37
+ },
38
+ "main": "./dist/index.mjs",
39
+ "keywords": [
40
+ "react",
41
+ "bug-report",
42
+ "debug",
43
+ "screenshot",
44
+ "support",
45
+ "helpdesk"
46
+ ],
47
+ "license": "MIT",
48
+ "author": "Lightwork AI",
49
+ "publishConfig": {
50
+ "access": "public"
51
+ }
52
+ }
@@ -0,0 +1,56 @@
1
+ /**
2
+ * Arms the capture buffers and mounts the reporter. Renders nothing.
3
+ *
4
+ * Put it once in the root layout, above the routes — the buffers need to be
5
+ * recording BEFORE a user hits a problem. A report opened from a page that only
6
+ * just started listening has no trail of how the user got there, which is the
7
+ * half that makes a report reproducible.
8
+ */
9
+ import { useEffect } from "react";
10
+ import {
11
+ configureDebugCapture,
12
+ installDebugCapture,
13
+ isConfigured,
14
+ setErroredQueriesSource,
15
+ type DebugCaptureConfig,
16
+ type ErroredQuery,
17
+ type InstallOptions,
18
+ } from "@lightworkai.official/debug-capture";
19
+
20
+ export interface DebugCaptureInitProps extends InstallOptions {
21
+ /**
22
+ * The config. Pass it here rather than calling `configureDebugCapture`
23
+ * yourself from a parent's effect — React runs CHILD effects before parent
24
+ * ones, so a parent that configures in `useEffect` runs *after* this
25
+ * component has already tried to install, and capture never arms.
26
+ *
27
+ * Calling `configureDebugCapture` at module scope also works, and is what the
28
+ * README shows. This exists so the wrong order is not reachable.
29
+ */
30
+ config?: DebugCaptureConfig;
31
+ /**
32
+ * Failed queries from the app's data layer, if it has one. A callback so this
33
+ * package never depends on react-query or any other client — with TanStack
34
+ * Query that is:
35
+ *
36
+ * errorQueries={() =>
37
+ * queryClient.getQueryCache().findAll()
38
+ * .filter((q) => q.state.status === 'error')
39
+ * .map((q) => ({ key: JSON.stringify(q.queryKey), error: String(q.state.error) }))}
40
+ */
41
+ errorQueries?: () => ErroredQuery[];
42
+ }
43
+
44
+ export function DebugCaptureInit({ config, errorQueries, ...options }: DebugCaptureInitProps): null {
45
+ // Before the effect, not inside it: an effect runs after the first paint, and
46
+ // anything the app fetched or logged in between would be missed.
47
+ if (config && !isConfigured()) configureDebugCapture(config);
48
+
49
+ useEffect(() => {
50
+ if (errorQueries) setErroredQueriesSource(errorQueries);
51
+ installDebugCapture(options);
52
+ // install is idempotent and app-wide; re-running it when a prop changes
53
+ // would cost a render and do nothing, so the deps stay empty on purpose.
54
+ }, []);
55
+ return null;
56
+ }
@@ -0,0 +1,50 @@
1
+ /**
2
+ * Catches a render crash and offers to report it.
3
+ *
4
+ * A React render error does NOT reach window.onerror, so the crash watcher in
5
+ * the core never sees it — this is the only way those get captured at all, and
6
+ * they are the ones users describe as "the screen went white".
7
+ *
8
+ * Class component because that is the only thing React gives us:
9
+ * componentDidCatch has no hook equivalent.
10
+ */
11
+ import { Component, type ErrorInfo, type ReactNode } from "react";
12
+ import { emitCrashFromBoundary } from "./crash";
13
+
14
+ interface Props {
15
+ children: ReactNode;
16
+ /** What to show instead of the crashed subtree. */
17
+ fallback?: (error: Error, report: () => void) => ReactNode;
18
+ }
19
+
20
+ interface State {
21
+ error: Error | null;
22
+ }
23
+
24
+ export class DebugErrorBoundary extends Component<Props, State> {
25
+ override state: State = { error: null };
26
+
27
+ static getDerivedStateFromError(error: Error): State {
28
+ return { error };
29
+ }
30
+
31
+ override componentDidCatch(error: Error, info: ErrorInfo): void {
32
+ emitCrashFromBoundary(error, info.componentStack ?? undefined);
33
+ }
34
+
35
+ override render(): ReactNode {
36
+ const { error } = this.state;
37
+ if (!error) return this.props.children;
38
+ // No default UI beyond the report action: what a crashed screen should look
39
+ // like is the host's decision, and ours would be wrong in their layout.
40
+ return this.props.fallback?.(error, () => reportBoundaryError(error)) ?? null;
41
+ }
42
+ }
43
+
44
+ function reportBoundaryError(error: Error): void {
45
+ // Imported lazily so the boundary module does not pull the reporter into a
46
+ // chunk that every page loads.
47
+ void import("@lightworkai.official/debug-capture").then(({ launch }) =>
48
+ launch({ type: "framework-error", message: error.message, stack: error.stack }),
49
+ );
50
+ }
@@ -0,0 +1,79 @@
1
+ /**
2
+ * The bug icon that opens the reporter.
3
+ *
4
+ * `launch` runs straight out of the click with nothing awaited in front of it.
5
+ * That is not style: screen capture is gated on a user gesture, and a single
6
+ * await between the click and `getDisplayMedia` loses it — the browser then
7
+ * refuses with an error that says nothing about why.
8
+ *
9
+ * It arrives presentable and `className` replaces that entirely. Shipping it
10
+ * unstyled meant every host wrote the same forty characters of CSS to make a
11
+ * bug icon look like a header button — work the library should have done. But
12
+ * the default must be *replaceable*, not merged: a widget with opinions about
13
+ * its border radius that you cannot override is a widget somebody has to fight.
14
+ */
15
+ import type { ButtonHTMLAttributes, CSSProperties, ReactNode } from "react";
16
+ import { launch } from "@lightworkai.official/debug-capture";
17
+
18
+ export interface ReportProblemButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
19
+ /** Replace the default bug glyph — pass the host's own icon component. */
20
+ children?: ReactNode;
21
+ label?: string;
22
+ }
23
+
24
+ /**
25
+ * A square icon button that sits in a header without looking out of place, in
26
+ * inline styles so it needs no stylesheet and no build step. Passing
27
+ * `className` drops all of it — a host with a design system wants its own
28
+ * button, not ours with theirs layered on top.
29
+ */
30
+ const DEFAULT_STYLE: CSSProperties = {
31
+ display: "inline-flex",
32
+ alignItems: "center",
33
+ justifyContent: "center",
34
+ width: 38,
35
+ height: 38,
36
+ padding: 0,
37
+ border: "none",
38
+ borderRadius: 8,
39
+ background: "transparent",
40
+ color: "currentColor",
41
+ cursor: "pointer",
42
+ };
43
+
44
+ const BUG = (
45
+ <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
46
+ <path d="M8 2l1.9 1.9M16 2l-1.9 1.9" />
47
+ <rect x="8" y="6" width="8" height="14" rx="4" />
48
+ <path d="M3 13h5M16 13h5M4 8l3 2M20 8l-3 2M4 18l3-2M20 18l-3-2" />
49
+ </svg>
50
+ );
51
+
52
+ export function ReportProblemButton({
53
+ children,
54
+ label = "รายงานปัญหา",
55
+ className,
56
+ style,
57
+ onClick,
58
+ ...rest
59
+ }: ReportProblemButtonProps) {
60
+ return (
61
+ <button
62
+ type="button"
63
+ aria-label={label}
64
+ title={label}
65
+ className={className}
66
+ // Pulled out of the spread deliberately: `{...rest}` after this would put
67
+ // the caller's undefined `style` back over the default and undo it.
68
+ style={className ? style : { ...DEFAULT_STYLE, ...style }}
69
+ {...rest}
70
+ onClick={(event) => {
71
+ onClick?.(event);
72
+ if (event.defaultPrevented) return;
73
+ launch({ type: "manual" });
74
+ }}
75
+ >
76
+ {children ?? BUG}
77
+ </button>
78
+ );
79
+ }
@@ -0,0 +1,160 @@
1
+ /**
2
+ * Draw on an image before it is sent.
3
+ *
4
+ * The original opens this the moment you pick a picture, and the reason is in
5
+ * its own comment: annotating BEFORE upload keeps the canvas same-origin, so
6
+ * `toDataURL` is not tainted by a cross-origin presigned URL. Pointing at the
7
+ * problem is also most of what a screenshot is for.
8
+ *
9
+ * The drawing itself is `createAnnotator` from the core — one canvas engine,
10
+ * shared by all three framework packages, because there is no declarative way
11
+ * to draw on a canvas. What is here is only the chrome.
12
+ */
13
+ import { useEffect, useRef, useState } from "react";
14
+ import {
15
+ createAnnotator,
16
+ reporterStrings,
17
+ type Annotator,
18
+ type AnnotatorTool,
19
+ type ReporterStrings,
20
+ } from "@lightworkai.official/debug-capture";
21
+ import { ArrowUpRightIcon, HighlighterIcon, SquareIcon, TrashIcon, TypeIcon } from "./icons";
22
+
23
+ const TOOLS: { tool: AnnotatorTool; key: keyof ReporterStrings; Icon: typeof SquareIcon }[] = [
24
+ { tool: "rect", key: "toolRect", Icon: SquareIcon },
25
+ { tool: "arrow", key: "toolArrow", Icon: ArrowUpRightIcon },
26
+ { tool: "highlight", key: "toolHighlight", Icon: HighlighterIcon },
27
+ { tool: "text", key: "toolText", Icon: TypeIcon },
28
+ ];
29
+
30
+ export interface ImageAnnotatorDialogProps {
31
+ /** The picked image, as a same-origin data URI. */
32
+ src: string;
33
+ /** Shown in the description, as the app's dialog does. */
34
+ filename?: string;
35
+ locale: "th" | "en" | undefined;
36
+ /** The annotated image, at full resolution. */
37
+ onConfirm: (dataUri: string) => void;
38
+ onCancel: () => void;
39
+ }
40
+
41
+ export function ImageAnnotatorDialog({ src, filename, locale, onConfirm, onCancel }: ImageAnnotatorDialogProps) {
42
+ const t = reporterStrings(locale);
43
+ const canvas = useRef<HTMLCanvasElement>(null);
44
+ // A ref, not state: the annotator holds a canvas and its own mutable state,
45
+ // and none of that should drive a render.
46
+ const annotator = useRef<Annotator | null>(null);
47
+ const [active, setActive] = useState<AnnotatorTool | null>(null);
48
+ const [loading, setLoading] = useState(true);
49
+
50
+ useEffect(() => {
51
+ const node = canvas.current;
52
+ if (!node) return;
53
+ const image = new Image();
54
+ image.addEventListener("load", () => {
55
+ // A backing store at the image's own size, BEFORE the annotator draws
56
+ // into it. Without this the canvas keeps its default 300x150, the image
57
+ // is squashed into that, and CSS then stretches the result — blurry, and
58
+ // the wrong shape.
59
+ node.width = image.naturalWidth;
60
+ node.height = image.naturalHeight;
61
+ annotator.current = createAnnotator(node, image, () => {
62
+ // The toolbar mirrors the engine, so it has to hear about a tool being
63
+ // CONSUMED as well as a shape being added.
64
+ setActive(null);
65
+ });
66
+ setLoading(false);
67
+ });
68
+ image.src = src;
69
+ return () => {
70
+ annotator.current?.destroy();
71
+ annotator.current = null;
72
+ };
73
+ }, [src]);
74
+
75
+ function pick(tool: AnnotatorTool): void {
76
+ const next = active === tool ? null : tool;
77
+ setActive(next);
78
+ annotator.current?.setTool(next);
79
+ }
80
+
81
+ return (
82
+ <div
83
+ className="lw-annotator"
84
+ role="dialog"
85
+ aria-modal="true"
86
+ onClick={(event) => {
87
+ if (event.target === event.currentTarget) onCancel();
88
+ }}
89
+ >
90
+ {/* Header, toolbar, canvas, help, footer — the same shape and the same
91
+ words as the app's own dialog, so the two are one thing in two places
92
+ rather than two things that resemble each other. */}
93
+ <div className="lw-annotator__panel">
94
+ <div className="lw-annotator__head">
95
+ <div className="lw-grow">
96
+ <h2 className="lw-annotator__title">{t.annotateTitle}</h2>
97
+ <p className="lw-annotator__desc">
98
+ {t.annotateHint}
99
+ {filename ? ` · ${filename}` : ""}
100
+ </p>
101
+ </div>
102
+ <button type="button" className="lw-annotator__close" aria-label={t.cancel} onClick={onCancel}>
103
+
104
+ </button>
105
+ </div>
106
+
107
+ <div className="lw-annotator__tools">
108
+ {TOOLS.map(({ tool, key, Icon }) => (
109
+ <button
110
+ key={tool}
111
+ type="button"
112
+ className="lw-atool"
113
+ aria-pressed={active === tool}
114
+ disabled={loading}
115
+ onClick={() => pick(tool)}
116
+ >
117
+ <Icon /> {t[key] as string}
118
+ </button>
119
+ ))}
120
+ <span className="lw-grow" />
121
+ {/* Right-aligned and on its own, like the app's. Deleting ONE shape is
122
+ the ✕ on the shape, which is what the help line below explains. */}
123
+ <button
124
+ type="button"
125
+ className="lw-atool lw-atool--plain"
126
+ disabled={loading}
127
+ onClick={() => {
128
+ annotator.current?.clear();
129
+ setActive(null);
130
+ }}
131
+ >
132
+ <TrashIcon /> {t.clearAll}
133
+ </button>
134
+ </div>
135
+
136
+ <div className="lw-annotator__canvas">
137
+ <div className="lw-annotator__canvasbox">
138
+ <canvas ref={canvas} />
139
+ </div>
140
+ </div>
141
+
142
+ <p className="lw-annotator__hint">{t.annotateHelp}</p>
143
+
144
+ <div className="lw-annotator__actions">
145
+ <span className="lw-grow" />
146
+ <button type="button" className="lw-btn" onClick={onCancel}>{t.cancel}</button>
147
+ <button
148
+ type="button"
149
+ className="lw-send"
150
+ disabled={loading}
151
+ // Full resolution, never the on-screen scale.
152
+ onClick={() => onConfirm(annotator.current?.toDataUrl() ?? src)}
153
+ >
154
+ {t.annotateApply}
155
+ </button>
156
+ </div>
157
+ </div>
158
+ </div>
159
+ );
160
+ }