@tra-bilisim/report-issue 0.1.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.
- package/LICENSE +21 -0
- package/README.md +109 -0
- package/dist/AnnotationEditor-ILMYBTOG.js +379 -0
- package/dist/adapters-axios.d.ts +19 -0
- package/dist/adapters-axios.js +52 -0
- package/dist/chunk-5S66KGBW.js +118 -0
- package/dist/chunk-EXDFVVYA.js +73 -0
- package/dist/chunk-JMQUG5Q7.js +99 -0
- package/dist/chunk-KY2IRP36.js +102 -0
- package/dist/chunk-ZYF6UFBB.js +162 -0
- package/dist/consent-DmS4DxOf.d.ts +135 -0
- package/dist/core.d.ts +16 -0
- package/dist/core.js +4 -0
- package/dist/index.css +594 -0
- package/dist/index.d.ts +77 -0
- package/dist/index.js +687 -0
- package/dist/screenshot-BQPXCSLD.js +2 -0
- package/package.json +69 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 TRA Bilişim
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
# @tra-bilisim/report-issue
|
|
2
|
+
|
|
3
|
+
Self-contained **"Report an Issue"** widget for React apps. Zero Tailwind, zero
|
|
4
|
+
design-system coupling — bring your own `submit`, everything else has a default.
|
|
5
|
+
|
|
6
|
+
- 📸 Masked screenshot with an annotation editor (arrows, boxes, text, highlighter)
|
|
7
|
+
- 🎥 Tab-only screen recording (KVKK-safe, current tab only, hard max duration)
|
|
8
|
+
- 🕵️ Console + network log capture (ring buffer)
|
|
9
|
+
- 🔒 KVKK explicit-consent gate before any capture, with versioned consent records
|
|
10
|
+
- 🎨 Standalone Radix + scoped CSS (`.rpi-*`, `--rpi-*`), light/dark aware
|
|
11
|
+
|
|
12
|
+
## Install
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
npm install @tra-bilisim/report-issue
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
Peer deps: `react` and `react-dom` (>=18). `axios` is only needed if you use the
|
|
19
|
+
axios network-logging adapter.
|
|
20
|
+
|
|
21
|
+
## Usage
|
|
22
|
+
|
|
23
|
+
```tsx
|
|
24
|
+
import {
|
|
25
|
+
ReportIssueProvider,
|
|
26
|
+
FloatingReportButton,
|
|
27
|
+
type ReportIssueAdapter,
|
|
28
|
+
} from '@tra-bilisim/report-issue';
|
|
29
|
+
import '@tra-bilisim/report-issue/styles.css';
|
|
30
|
+
|
|
31
|
+
const adapter: ReportIssueAdapter = {
|
|
32
|
+
// The ONLY required field. Persist the report however you like.
|
|
33
|
+
submit: async (payload, formData) => {
|
|
34
|
+
const res = await fetch('/api/report-issue', { method: 'POST', body: formData });
|
|
35
|
+
return { ok: res.ok };
|
|
36
|
+
},
|
|
37
|
+
|
|
38
|
+
// Everything below is optional.
|
|
39
|
+
getCategories: async () => [{ label: 'UI', value: 1 }, { label: 'Data', value: 2 }],
|
|
40
|
+
getMetadata: () => ({ userId: currentUser.id, roles: currentUser.roles }),
|
|
41
|
+
getCurrentUrl: () => window.location.href,
|
|
42
|
+
pageOptions: [{ value: 'https://app/x', label: '/x' }],
|
|
43
|
+
environmentResolver: url => (url.includes('localhost') ? 'Locale' : 'Prod'),
|
|
44
|
+
t: key => i18n.translate(key), // English keys are the default fallback text
|
|
45
|
+
locale: 'tr',
|
|
46
|
+
toast: { success: msg => toast.success(msg), error: msg => toast.error(msg) },
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
function App() {
|
|
50
|
+
return (
|
|
51
|
+
<ReportIssueProvider config={adapter}>
|
|
52
|
+
<YourApp />
|
|
53
|
+
<FloatingReportButton />
|
|
54
|
+
</ReportIssueProvider>
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## Network log capture (optional)
|
|
60
|
+
|
|
61
|
+
Feed your axios instance into the shared log store with one call:
|
|
62
|
+
|
|
63
|
+
```ts
|
|
64
|
+
import { attachAxiosNetworkLogging } from '@tra-bilisim/report-issue/adapters/axios';
|
|
65
|
+
attachAxiosNetworkLogging(myAxiosInstance);
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
Or wire your own transport by calling the primitives from `@tra-bilisim/report-issue/core`:
|
|
69
|
+
`beginNetworkRequest`, `completeNetworkRequest`, `failNetworkRequest`.
|
|
70
|
+
|
|
71
|
+
## Wire contract (`submit`)
|
|
72
|
+
|
|
73
|
+
`submit(payload, formData)` receives both a structured `payload` and a ready-to-POST
|
|
74
|
+
`FormData`. The `FormData` fields are the canonical contract your backend consumes:
|
|
75
|
+
|
|
76
|
+
| Field | Type | Notes |
|
|
77
|
+
|---|---|---|
|
|
78
|
+
| `files` | File[] (repeated) | screenshots / recordings / uploads |
|
|
79
|
+
| `page` | string | absolute URL |
|
|
80
|
+
| `environment` | string | from `environmentResolver` |
|
|
81
|
+
| `categoryId` | string | omitted when no category |
|
|
82
|
+
| `description` | string | omitted when empty |
|
|
83
|
+
| `consoleLogs` | JSON string | `ConsoleEntry[]` |
|
|
84
|
+
| `networkLogs` | JSON string | `NetworkEntry[]` |
|
|
85
|
+
| `recordingConsent` | JSON string | only when a recording is attached |
|
|
86
|
+
| `screenshotConsent` | JSON string | only when a screenshot is attached |
|
|
87
|
+
| `metadata` | JSON string | `ReportMetadata` (user, roles, UA, resolution, ts) |
|
|
88
|
+
|
|
89
|
+
`submit` must resolve to `{ ok: boolean; message?: string | null }`.
|
|
90
|
+
|
|
91
|
+
## Masking controls (data attributes)
|
|
92
|
+
|
|
93
|
+
- `data-report-mask-ignore` — never mask this subtree (also excluded from the video mask)
|
|
94
|
+
- `data-report-mask-text-ignore` — keep text visible (still masks inputs)
|
|
95
|
+
- `data-report-ignore-capture` — exclude from screenshots entirely
|
|
96
|
+
- `data-report-mask-control` / `data-report-mask-value` — force text masking
|
|
97
|
+
|
|
98
|
+
## Entry points
|
|
99
|
+
|
|
100
|
+
- `@tra-bilisim/report-issue` — React provider, button, dialog, hooks, types
|
|
101
|
+
- `@tra-bilisim/report-issue/core` — headless, framework-agnostic engine
|
|
102
|
+
- `@tra-bilisim/report-issue/adapters/axios` — axios network-logging adapter
|
|
103
|
+
- `@tra-bilisim/report-issue/styles.css` — required stylesheet
|
|
104
|
+
|
|
105
|
+
## Build
|
|
106
|
+
|
|
107
|
+
```bash
|
|
108
|
+
npm run build # tsup → dist/ (esm + d.ts + index.css)
|
|
109
|
+
```
|
|
@@ -0,0 +1,379 @@
|
|
|
1
|
+
import { useReportIssueConfig, Button } from './chunk-JMQUG5Q7.js';
|
|
2
|
+
import './chunk-EXDFVVYA.js';
|
|
3
|
+
import { useRef, useState, useEffect, useCallback } from 'react';
|
|
4
|
+
import { Canvas, PencilBrush, FabricImage, Rect, Ellipse, Line, IText, Triangle, Group } from 'fabric';
|
|
5
|
+
import { MousePointer2, Pencil, Minus, MoveRight, Square, Circle, Type, Highlighter, Undo2, Redo2 } from 'lucide-react';
|
|
6
|
+
import { jsx, jsxs } from 'react/jsx-runtime';
|
|
7
|
+
|
|
8
|
+
var COLORS = ["#ef4444", "#f97316", "#eab308", "#22c55e", "#3b82f6", "#000000", "#ffffff"];
|
|
9
|
+
var dataUrlToBlob = (dataUrl) => {
|
|
10
|
+
const [header, data] = dataUrl.split(",");
|
|
11
|
+
const mime = header.match(/data:(.*?);base64/)?.[1] ?? "application/octet-stream";
|
|
12
|
+
const binary = atob(data);
|
|
13
|
+
const bytes = new Uint8Array(binary.length);
|
|
14
|
+
for (let i = 0; i < binary.length; i += 1) {
|
|
15
|
+
bytes[i] = binary.charCodeAt(i);
|
|
16
|
+
}
|
|
17
|
+
return new Blob([bytes], { type: mime });
|
|
18
|
+
};
|
|
19
|
+
var AnnotationEditor = ({ imageDataUrl, onConfirm, onCancel }) => {
|
|
20
|
+
const { t } = useReportIssueConfig();
|
|
21
|
+
const canvasElRef = useRef(null);
|
|
22
|
+
const containerRef = useRef(null);
|
|
23
|
+
const fabricRef = useRef(null);
|
|
24
|
+
const [tool, setTool] = useState("select");
|
|
25
|
+
const [color, setColor] = useState("#ef4444");
|
|
26
|
+
const [strokeWidth, setStrokeWidth] = useState(4);
|
|
27
|
+
const historyRef = useRef([]);
|
|
28
|
+
const historyIndexRef = useRef(-1);
|
|
29
|
+
const isRestoringRef = useRef(false);
|
|
30
|
+
const [canUndo, setCanUndo] = useState(false);
|
|
31
|
+
const [canRedo, setCanRedo] = useState(false);
|
|
32
|
+
const initializedRef = useRef(false);
|
|
33
|
+
const imgRefCache = useRef(null);
|
|
34
|
+
const drawingRef = useRef({ obj: null, startX: 0, startY: 0 });
|
|
35
|
+
const toolRef = useRef(tool);
|
|
36
|
+
const colorRef = useRef(color);
|
|
37
|
+
const strokeRef = useRef(strokeWidth);
|
|
38
|
+
useEffect(() => {
|
|
39
|
+
toolRef.current = tool;
|
|
40
|
+
}, [tool]);
|
|
41
|
+
useEffect(() => {
|
|
42
|
+
colorRef.current = color;
|
|
43
|
+
}, [color]);
|
|
44
|
+
useEffect(() => {
|
|
45
|
+
strokeRef.current = strokeWidth;
|
|
46
|
+
}, [strokeWidth]);
|
|
47
|
+
const saveHistory = useCallback(() => {
|
|
48
|
+
const canvas = fabricRef.current;
|
|
49
|
+
if (!canvas || isRestoringRef.current) return;
|
|
50
|
+
const json = JSON.stringify(canvas.toJSON());
|
|
51
|
+
historyRef.current = historyRef.current.slice(0, historyIndexRef.current + 1);
|
|
52
|
+
historyRef.current.push(json);
|
|
53
|
+
historyIndexRef.current = historyRef.current.length - 1;
|
|
54
|
+
setCanUndo(historyIndexRef.current > 0);
|
|
55
|
+
setCanRedo(false);
|
|
56
|
+
}, []);
|
|
57
|
+
const restore = useCallback((index) => {
|
|
58
|
+
const canvas = fabricRef.current;
|
|
59
|
+
if (!canvas || index < 0 || index >= historyRef.current.length) return;
|
|
60
|
+
isRestoringRef.current = true;
|
|
61
|
+
canvas.loadFromJSON(historyRef.current[index]).then(() => {
|
|
62
|
+
canvas.renderAll();
|
|
63
|
+
isRestoringRef.current = false;
|
|
64
|
+
historyIndexRef.current = index;
|
|
65
|
+
setCanUndo(index > 0);
|
|
66
|
+
setCanRedo(index < historyRef.current.length - 1);
|
|
67
|
+
});
|
|
68
|
+
}, []);
|
|
69
|
+
const undo = useCallback(() => restore(historyIndexRef.current - 1), [restore]);
|
|
70
|
+
const redo = useCallback(() => restore(historyIndexRef.current + 1), [restore]);
|
|
71
|
+
const applySize = useCallback(() => {
|
|
72
|
+
const canvas = fabricRef.current;
|
|
73
|
+
const img = imgRefCache.current;
|
|
74
|
+
const container = containerRef.current;
|
|
75
|
+
if (!canvas || !img || !container) return;
|
|
76
|
+
const { width: containerW, height: containerH } = container.getBoundingClientRect();
|
|
77
|
+
if (!containerW || !containerH) return;
|
|
78
|
+
const el = img.getElement();
|
|
79
|
+
const imgW = el?.naturalWidth || img.width || containerW;
|
|
80
|
+
const imgH = el?.naturalHeight || img.height || containerH;
|
|
81
|
+
const scale = Math.min(containerW / imgW, containerH / imgH);
|
|
82
|
+
const w = Math.round(imgW * scale);
|
|
83
|
+
const h = Math.round(imgH * scale);
|
|
84
|
+
canvas.setViewportTransform([1, 0, 0, 1, 0, 0]);
|
|
85
|
+
canvas.setDimensions({ width: w, height: h });
|
|
86
|
+
img.set({
|
|
87
|
+
scaleX: scale,
|
|
88
|
+
scaleY: scale,
|
|
89
|
+
left: 0,
|
|
90
|
+
top: 0,
|
|
91
|
+
originX: "left",
|
|
92
|
+
originY: "top"
|
|
93
|
+
});
|
|
94
|
+
img.setCoords();
|
|
95
|
+
canvas.backgroundImage = img;
|
|
96
|
+
canvas.renderAll();
|
|
97
|
+
}, []);
|
|
98
|
+
useEffect(() => {
|
|
99
|
+
const canvasEl = canvasElRef.current;
|
|
100
|
+
const container = containerRef.current;
|
|
101
|
+
if (!canvasEl || !container) return void 0;
|
|
102
|
+
initializedRef.current = false;
|
|
103
|
+
imgRefCache.current = null;
|
|
104
|
+
const canvas = new Canvas(canvasEl, { selection: true });
|
|
105
|
+
fabricRef.current = canvas;
|
|
106
|
+
let resizeTimeout;
|
|
107
|
+
const initCanvas = () => {
|
|
108
|
+
if (initializedRef.current) return;
|
|
109
|
+
FabricImage.fromURL(imageDataUrl).then((img) => {
|
|
110
|
+
if (!fabricRef.current) return;
|
|
111
|
+
imgRefCache.current = img;
|
|
112
|
+
img.selectable = false;
|
|
113
|
+
img.evented = false;
|
|
114
|
+
applySize();
|
|
115
|
+
historyRef.current = [JSON.stringify(fabricRef.current.toJSON())];
|
|
116
|
+
historyIndexRef.current = 0;
|
|
117
|
+
initializedRef.current = true;
|
|
118
|
+
});
|
|
119
|
+
};
|
|
120
|
+
const observer = new ResizeObserver((entries) => {
|
|
121
|
+
const entry = entries[0];
|
|
122
|
+
if (!entry) return;
|
|
123
|
+
const { width, height } = entry.contentRect;
|
|
124
|
+
if (!width || !height) return;
|
|
125
|
+
clearTimeout(resizeTimeout);
|
|
126
|
+
resizeTimeout = setTimeout(() => {
|
|
127
|
+
if (!initializedRef.current) initCanvas();
|
|
128
|
+
else applySize();
|
|
129
|
+
}, 300);
|
|
130
|
+
});
|
|
131
|
+
observer.observe(container);
|
|
132
|
+
canvas.on("object:added", saveHistory);
|
|
133
|
+
canvas.on("object:modified", saveHistory);
|
|
134
|
+
return () => {
|
|
135
|
+
clearTimeout(resizeTimeout);
|
|
136
|
+
observer.disconnect();
|
|
137
|
+
canvas.dispose();
|
|
138
|
+
fabricRef.current = null;
|
|
139
|
+
imgRefCache.current = null;
|
|
140
|
+
initializedRef.current = false;
|
|
141
|
+
};
|
|
142
|
+
}, [imageDataUrl, saveHistory, applySize]);
|
|
143
|
+
useEffect(() => {
|
|
144
|
+
const canvas = fabricRef.current;
|
|
145
|
+
if (!canvas) return;
|
|
146
|
+
if (tool === "pencil") {
|
|
147
|
+
const brush = new PencilBrush(canvas);
|
|
148
|
+
brush.color = color;
|
|
149
|
+
brush.width = strokeWidth;
|
|
150
|
+
canvas.freeDrawingBrush = brush;
|
|
151
|
+
canvas.isDrawingMode = true;
|
|
152
|
+
canvas.selection = false;
|
|
153
|
+
} else {
|
|
154
|
+
canvas.isDrawingMode = false;
|
|
155
|
+
canvas.selection = tool === "select";
|
|
156
|
+
}
|
|
157
|
+
canvas.forEachObject((obj) => {
|
|
158
|
+
obj.selectable = tool === "select";
|
|
159
|
+
obj.evented = tool === "select";
|
|
160
|
+
});
|
|
161
|
+
canvas.renderAll();
|
|
162
|
+
}, [tool, color, strokeWidth]);
|
|
163
|
+
useEffect(() => {
|
|
164
|
+
const canvas = fabricRef.current;
|
|
165
|
+
if (!canvas) return void 0;
|
|
166
|
+
const handleMouseDown = (opt) => {
|
|
167
|
+
const activeTool = toolRef.current;
|
|
168
|
+
if (activeTool === "select" || activeTool === "pencil") return;
|
|
169
|
+
const p = canvas.getScenePoint(opt.e);
|
|
170
|
+
drawingRef.current.startX = p.x;
|
|
171
|
+
drawingRef.current.startY = p.y;
|
|
172
|
+
const c = colorRef.current;
|
|
173
|
+
const sw = strokeRef.current;
|
|
174
|
+
let obj = null;
|
|
175
|
+
if (activeTool === "rect") {
|
|
176
|
+
obj = new Rect({ left: p.x, top: p.y, width: 1, height: 1, originX: "left", originY: "top", fill: "transparent", stroke: c, strokeWidth: sw, strokeUniform: true });
|
|
177
|
+
} else if (activeTool === "ellipse") {
|
|
178
|
+
obj = new Ellipse({ left: p.x, top: p.y, rx: 0, ry: 0, originX: "center", originY: "center", fill: "transparent", stroke: c, strokeWidth: sw, strokeUniform: true });
|
|
179
|
+
} else if (activeTool === "highlighter") {
|
|
180
|
+
obj = new Rect({ left: p.x, top: p.y, width: 1, height: 1, originX: "left", originY: "top", fill: c, opacity: 0.35, stroke: "transparent", strokeUniform: true });
|
|
181
|
+
} else if (activeTool === "line" || activeTool === "arrow") {
|
|
182
|
+
obj = new Line([p.x, p.y, p.x, p.y], { stroke: c, strokeWidth: sw });
|
|
183
|
+
} else if (activeTool === "text") {
|
|
184
|
+
const text = new IText("", {
|
|
185
|
+
left: p.x,
|
|
186
|
+
top: p.y,
|
|
187
|
+
fill: c,
|
|
188
|
+
fontSize: Math.max(16, sw * 5),
|
|
189
|
+
hiddenTextareaContainer: containerRef.current
|
|
190
|
+
});
|
|
191
|
+
canvas.add(text);
|
|
192
|
+
canvas.setActiveObject(text);
|
|
193
|
+
canvas.renderAll();
|
|
194
|
+
requestAnimationFrame(() => {
|
|
195
|
+
text.enterEditing();
|
|
196
|
+
text.selectAll();
|
|
197
|
+
text.hiddenTextarea?.focus();
|
|
198
|
+
canvas.renderAll();
|
|
199
|
+
});
|
|
200
|
+
setTool("select");
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
if (obj) {
|
|
204
|
+
obj.selectable = false;
|
|
205
|
+
obj.evented = false;
|
|
206
|
+
canvas.add(obj);
|
|
207
|
+
drawingRef.current.obj = obj;
|
|
208
|
+
}
|
|
209
|
+
};
|
|
210
|
+
const handleMouseMove = (opt) => {
|
|
211
|
+
const { obj, startX, startY } = drawingRef.current;
|
|
212
|
+
if (!obj) return;
|
|
213
|
+
const p = canvas.getScenePoint(opt.e);
|
|
214
|
+
const activeTool = toolRef.current;
|
|
215
|
+
if (activeTool === "rect" || activeTool === "highlighter") {
|
|
216
|
+
obj.set({
|
|
217
|
+
left: Math.min(startX, p.x),
|
|
218
|
+
top: Math.min(startY, p.y),
|
|
219
|
+
scaleX: Math.max(Math.abs(p.x - startX), 1),
|
|
220
|
+
scaleY: Math.max(Math.abs(p.y - startY), 1)
|
|
221
|
+
});
|
|
222
|
+
obj.setCoords();
|
|
223
|
+
} else if (activeTool === "ellipse") {
|
|
224
|
+
obj.set({
|
|
225
|
+
left: (startX + p.x) / 2,
|
|
226
|
+
top: (startY + p.y) / 2,
|
|
227
|
+
rx: Math.abs(p.x - startX) / 2,
|
|
228
|
+
ry: Math.abs(p.y - startY) / 2
|
|
229
|
+
});
|
|
230
|
+
obj.setCoords();
|
|
231
|
+
} else if (activeTool === "line" || activeTool === "arrow") {
|
|
232
|
+
obj.set({ x2: p.x, y2: p.y });
|
|
233
|
+
}
|
|
234
|
+
canvas.renderAll();
|
|
235
|
+
};
|
|
236
|
+
const handleMouseUp = () => {
|
|
237
|
+
const { obj } = drawingRef.current;
|
|
238
|
+
const activeTool = toolRef.current;
|
|
239
|
+
if (!obj) return;
|
|
240
|
+
if (activeTool === "arrow") {
|
|
241
|
+
const line = obj;
|
|
242
|
+
const angle = Math.atan2((line.y2 ?? 0) - (line.y1 ?? 0), (line.x2 ?? 0) - (line.x1 ?? 0)) * (180 / Math.PI);
|
|
243
|
+
const head = new Triangle({
|
|
244
|
+
left: line.x2,
|
|
245
|
+
top: line.y2,
|
|
246
|
+
originX: "center",
|
|
247
|
+
originY: "center",
|
|
248
|
+
angle: angle + 90,
|
|
249
|
+
width: strokeRef.current * 4,
|
|
250
|
+
height: strokeRef.current * 4,
|
|
251
|
+
fill: colorRef.current
|
|
252
|
+
});
|
|
253
|
+
canvas.remove(obj);
|
|
254
|
+
const group = new Group([line, head]);
|
|
255
|
+
group.selectable = false;
|
|
256
|
+
group.evented = false;
|
|
257
|
+
canvas.add(group);
|
|
258
|
+
}
|
|
259
|
+
drawingRef.current.obj = null;
|
|
260
|
+
if (activeTool !== "arrow") {
|
|
261
|
+
const isLine = activeTool === "line";
|
|
262
|
+
if (!isLine) {
|
|
263
|
+
const w = obj.getScaledWidth();
|
|
264
|
+
const h = obj.getScaledHeight();
|
|
265
|
+
if (w < 2 && h < 2) {
|
|
266
|
+
canvas.remove(obj);
|
|
267
|
+
return;
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
saveHistory();
|
|
272
|
+
};
|
|
273
|
+
canvas.on("mouse:down", handleMouseDown);
|
|
274
|
+
canvas.on("mouse:move", handleMouseMove);
|
|
275
|
+
canvas.on("mouse:up", handleMouseUp);
|
|
276
|
+
return () => {
|
|
277
|
+
canvas.off("mouse:down", handleMouseDown);
|
|
278
|
+
canvas.off("mouse:move", handleMouseMove);
|
|
279
|
+
canvas.off("mouse:up", handleMouseUp);
|
|
280
|
+
};
|
|
281
|
+
}, [saveHistory]);
|
|
282
|
+
useEffect(() => {
|
|
283
|
+
const handler = (e) => {
|
|
284
|
+
if ((e.ctrlKey || e.metaKey) && e.key.toLowerCase() === "z") {
|
|
285
|
+
e.preventDefault();
|
|
286
|
+
if (e.shiftKey) redo();
|
|
287
|
+
else undo();
|
|
288
|
+
} else if (e.key === "Delete" || e.key === "Backspace") {
|
|
289
|
+
const canvas = fabricRef.current;
|
|
290
|
+
const active = canvas?.getActiveObject();
|
|
291
|
+
if (canvas && active && !active.isEditing) {
|
|
292
|
+
canvas.remove(active);
|
|
293
|
+
canvas.discardActiveObject();
|
|
294
|
+
canvas.renderAll();
|
|
295
|
+
saveHistory();
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
};
|
|
299
|
+
window.addEventListener("keydown", handler);
|
|
300
|
+
return () => window.removeEventListener("keydown", handler);
|
|
301
|
+
}, [undo, redo, saveHistory]);
|
|
302
|
+
const handleConfirm = useCallback(() => {
|
|
303
|
+
const canvas = fabricRef.current;
|
|
304
|
+
if (!canvas) return;
|
|
305
|
+
canvas.discardActiveObject();
|
|
306
|
+
canvas.renderAll();
|
|
307
|
+
const dataUrl = canvas.toDataURL({ format: "png", multiplier: 1 });
|
|
308
|
+
const blob = dataUrlToBlob(dataUrl);
|
|
309
|
+
const file = new File([blob], `screenshot-${Date.now()}.png`, { type: "image/png" });
|
|
310
|
+
onConfirm(file);
|
|
311
|
+
}, [onConfirm]);
|
|
312
|
+
const tools = [
|
|
313
|
+
{ id: "select", icon: /* @__PURE__ */ jsx(MousePointer2, { size: 16 }), label: t("Select") },
|
|
314
|
+
{ id: "pencil", icon: /* @__PURE__ */ jsx(Pencil, { size: 16 }), label: t("Draw") },
|
|
315
|
+
{ id: "line", icon: /* @__PURE__ */ jsx(Minus, { size: 16 }), label: t("Line") },
|
|
316
|
+
{ id: "arrow", icon: /* @__PURE__ */ jsx(MoveRight, { size: 16 }), label: t("Arrow") },
|
|
317
|
+
{ id: "rect", icon: /* @__PURE__ */ jsx(Square, { size: 16 }), label: t("Rectangle") },
|
|
318
|
+
{ id: "ellipse", icon: /* @__PURE__ */ jsx(Circle, { size: 16 }), label: t("Circle") },
|
|
319
|
+
{ id: "text", icon: /* @__PURE__ */ jsx(Type, { size: 16 }), label: t("Text") },
|
|
320
|
+
{ id: "highlighter", icon: /* @__PURE__ */ jsx(Highlighter, { size: 16 }), label: t("Highlight") }
|
|
321
|
+
];
|
|
322
|
+
return /* @__PURE__ */ jsxs("div", { className: "rpi-editor", children: [
|
|
323
|
+
/* @__PURE__ */ jsxs("div", { className: "rpi-editor__stage", children: [
|
|
324
|
+
/* @__PURE__ */ jsx("div", { className: "rpi-editor__tools", children: tools.map((toolItem) => /* @__PURE__ */ jsx(
|
|
325
|
+
"button",
|
|
326
|
+
{
|
|
327
|
+
type: "button",
|
|
328
|
+
title: toolItem.label,
|
|
329
|
+
onClick: () => setTool(toolItem.id),
|
|
330
|
+
"data-active": tool === toolItem.id,
|
|
331
|
+
className: "rpi-editor__tool",
|
|
332
|
+
children: toolItem.icon
|
|
333
|
+
},
|
|
334
|
+
toolItem.id
|
|
335
|
+
)) }),
|
|
336
|
+
/* @__PURE__ */ jsx("div", { ref: containerRef, className: "rpi-editor__canvas-wrap", children: /* @__PURE__ */ jsx("canvas", { ref: canvasElRef }) })
|
|
337
|
+
] }),
|
|
338
|
+
/* @__PURE__ */ jsxs("div", { className: "rpi-editor__footer", children: [
|
|
339
|
+
/* @__PURE__ */ jsxs("div", { className: "rpi-editor__controls", children: [
|
|
340
|
+
/* @__PURE__ */ jsx("div", { className: "rpi-editor__swatches", children: COLORS.map((c) => /* @__PURE__ */ jsx(
|
|
341
|
+
"button",
|
|
342
|
+
{
|
|
343
|
+
type: "button",
|
|
344
|
+
onClick: () => setColor(c),
|
|
345
|
+
"data-active": color === c,
|
|
346
|
+
style: { backgroundColor: c },
|
|
347
|
+
className: "rpi-editor__swatch"
|
|
348
|
+
},
|
|
349
|
+
c
|
|
350
|
+
)) }),
|
|
351
|
+
/* @__PURE__ */ jsxs("div", { className: "rpi-editor__controls", children: [
|
|
352
|
+
/* @__PURE__ */ jsx("span", { className: "rpi-muted", style: { fontSize: 12 }, children: t("Thickness") }),
|
|
353
|
+
/* @__PURE__ */ jsx(
|
|
354
|
+
"input",
|
|
355
|
+
{
|
|
356
|
+
type: "range",
|
|
357
|
+
min: 1,
|
|
358
|
+
max: 20,
|
|
359
|
+
value: strokeWidth,
|
|
360
|
+
onChange: (e) => setStrokeWidth(Number(e.target.value)),
|
|
361
|
+
style: { width: 96 }
|
|
362
|
+
}
|
|
363
|
+
)
|
|
364
|
+
] }),
|
|
365
|
+
/* @__PURE__ */ jsxs("div", { className: "rpi-editor__controls", style: { gap: 4 }, children: [
|
|
366
|
+
/* @__PURE__ */ jsx(Button, { variant: "outline", size: "icon", disabled: !canUndo, onClick: undo, title: t("Undo"), children: /* @__PURE__ */ jsx(Undo2, { size: 16 }) }),
|
|
367
|
+
/* @__PURE__ */ jsx(Button, { variant: "outline", size: "icon", disabled: !canRedo, onClick: redo, title: t("Redo"), children: /* @__PURE__ */ jsx(Redo2, { size: 16 }) })
|
|
368
|
+
] })
|
|
369
|
+
] }),
|
|
370
|
+
/* @__PURE__ */ jsxs("div", { className: "rpi-editor__controls", style: { gap: 8 }, children: [
|
|
371
|
+
/* @__PURE__ */ jsx(Button, { variant: "outline", onClick: onCancel, children: t("Cancel") }),
|
|
372
|
+
/* @__PURE__ */ jsx(Button, { onClick: handleConfirm, children: t("Confirm") })
|
|
373
|
+
] })
|
|
374
|
+
] })
|
|
375
|
+
] });
|
|
376
|
+
};
|
|
377
|
+
var AnnotationEditor_default = AnnotationEditor;
|
|
378
|
+
|
|
379
|
+
export { AnnotationEditor_default as default };
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { AxiosInstance } from 'axios';
|
|
2
|
+
|
|
3
|
+
interface AttachAxiosOptions {
|
|
4
|
+
/**
|
|
5
|
+
* Extracts a human-readable message from a response body for the log entry.
|
|
6
|
+
* Defaults to common `{ message } | { Message }` shapes.
|
|
7
|
+
*/
|
|
8
|
+
getMessage?: (data: unknown) => string | undefined;
|
|
9
|
+
/** Treat this response as an error for logging. Default: status < 200 || >= 300. */
|
|
10
|
+
isErrorResponse?: (status: number, data: unknown) => boolean;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Wires an axios instance to feed the report-issue network-log store. Returns a
|
|
14
|
+
* detach function that ejects the interceptors. If the host already sets an
|
|
15
|
+
* `X-Request-Tracking-Id` header, it is reused; otherwise one is generated.
|
|
16
|
+
*/
|
|
17
|
+
declare function attachAxiosNetworkLogging(instance: AxiosInstance, options?: AttachAxiosOptions): () => void;
|
|
18
|
+
|
|
19
|
+
export { type AttachAxiosOptions, attachAxiosNetworkLogging };
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { beginNetworkRequest, completeNetworkRequest, failNetworkRequest } from './chunk-EXDFVVYA.js';
|
|
2
|
+
|
|
3
|
+
// src/adapters/axios.ts
|
|
4
|
+
var HEADER = "X-Request-Tracking-Id";
|
|
5
|
+
function generateId() {
|
|
6
|
+
return `rpi_${Date.now()}_${Math.random().toString(36).slice(2, 9)}`;
|
|
7
|
+
}
|
|
8
|
+
function readId(headers) {
|
|
9
|
+
const h = headers;
|
|
10
|
+
const raw = h?.[HEADER];
|
|
11
|
+
return typeof raw === "string" ? raw : "";
|
|
12
|
+
}
|
|
13
|
+
var defaultGetMessage = (data) => {
|
|
14
|
+
if (!data || typeof data !== "object") return typeof data === "string" ? data : void 0;
|
|
15
|
+
const d = data;
|
|
16
|
+
return d.message ?? d.Message ?? d.errorMessage ?? d.ErrorMessage;
|
|
17
|
+
};
|
|
18
|
+
function attachAxiosNetworkLogging(instance, options = {}) {
|
|
19
|
+
const getMessage = options.getMessage ?? defaultGetMessage;
|
|
20
|
+
const isErrorResponse = options.isErrorResponse ?? ((status) => status < 200 || status >= 300);
|
|
21
|
+
const reqId = instance.interceptors.request.use((config) => {
|
|
22
|
+
let id = readId(config.headers);
|
|
23
|
+
if (!id) {
|
|
24
|
+
id = generateId();
|
|
25
|
+
config.headers?.set?.(HEADER, id);
|
|
26
|
+
}
|
|
27
|
+
beginNetworkRequest(id, config.method?.toUpperCase(), config.url);
|
|
28
|
+
return config;
|
|
29
|
+
});
|
|
30
|
+
const resId = instance.interceptors.response.use(
|
|
31
|
+
(response) => {
|
|
32
|
+
const id = readId(response.config.headers);
|
|
33
|
+
const data = response.data;
|
|
34
|
+
const message = getMessage(data);
|
|
35
|
+
const errored = isErrorResponse(response.status, data);
|
|
36
|
+
completeNetworkRequest(id, response.status, errored, message, errored ? message : void 0);
|
|
37
|
+
return response;
|
|
38
|
+
},
|
|
39
|
+
(error) => {
|
|
40
|
+
const id = readId(error?.config?.headers);
|
|
41
|
+
const message = getMessage(error?.response?.data) ?? error?.message;
|
|
42
|
+
failNetworkRequest(id, error?.response?.status, message);
|
|
43
|
+
return Promise.reject(error);
|
|
44
|
+
}
|
|
45
|
+
);
|
|
46
|
+
return () => {
|
|
47
|
+
instance.interceptors.request.eject(reqId);
|
|
48
|
+
instance.interceptors.response.eject(resId);
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export { attachAxiosNetworkLogging };
|