issue-sticker 0.1.1 → 0.2.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/CHANGELOG.md +44 -0
- package/README.md +82 -415
- package/dist/NetworkCollector-BlY2qUKF.js +12278 -0
- package/dist/createLucideIcon-B457EluY.js +27415 -0
- package/dist/{index-DN49yZjv.js → index-D6SaRFm4.js} +98 -1
- package/dist/issue-sticker-headless-react.js +764 -0
- package/dist/issue-sticker-headless.js +1914 -0
- package/dist/issue-sticker-react.js +17399 -46756
- package/dist/tooltip-BkQcdrki.js +512 -0
- package/headless-react.d.ts +12 -0
- package/headless.d.ts +172 -0
- package/package.json +26 -26
- package/react.d.ts +29 -2
- package/dist/index.d.ts +0 -37
- package/dist/issue-sticker-runtime.js +0 -37
- package/dist/issue-sticker.js +0 -2105
- package/dist/issue-sticker.umd.js +0 -1
- package/dist/sdkEventListeners-B7idjnFi.js +0 -498
- package/runtime.d.ts +0 -11
|
@@ -0,0 +1,764 @@
|
|
|
1
|
+
import { jsxs, jsx } from "react/jsx-runtime";
|
|
2
|
+
import { memo, useRef, useState, useCallback, useEffect } from "react";
|
|
3
|
+
import { c as createLucideIcon, u as ungzip_1, a as cn, R as Replayer } from "./createLucideIcon-B457EluY.js";
|
|
4
|
+
import { T as TooltipProvider, a as Tooltip, b as TooltipTrigger, c as TooltipContent } from "./tooltip-BkQcdrki.js";
|
|
5
|
+
/**
|
|
6
|
+
* @license lucide-react v0.468.0 - ISC
|
|
7
|
+
*
|
|
8
|
+
* This source code is licensed under the ISC license.
|
|
9
|
+
* See the LICENSE file in the root directory of this source tree.
|
|
10
|
+
*/
|
|
11
|
+
const Maximize2 = createLucideIcon("Maximize2", [
|
|
12
|
+
["polyline", { points: "15 3 21 3 21 9", key: "mznyad" }],
|
|
13
|
+
["polyline", { points: "9 21 3 21 3 15", key: "1avn1i" }],
|
|
14
|
+
["line", { x1: "21", x2: "14", y1: "3", y2: "10", key: "ota7mn" }],
|
|
15
|
+
["line", { x1: "3", x2: "10", y1: "21", y2: "14", key: "1atl0r" }]
|
|
16
|
+
]);
|
|
17
|
+
/**
|
|
18
|
+
* @license lucide-react v0.468.0 - ISC
|
|
19
|
+
*
|
|
20
|
+
* This source code is licensed under the ISC license.
|
|
21
|
+
* See the LICENSE file in the root directory of this source tree.
|
|
22
|
+
*/
|
|
23
|
+
const MousePointer2 = createLucideIcon("MousePointer2", [
|
|
24
|
+
[
|
|
25
|
+
"path",
|
|
26
|
+
{
|
|
27
|
+
d: "M4.037 4.688a.495.495 0 0 1 .651-.651l16 6.5a.5.5 0 0 1-.063.947l-6.124 1.58a2 2 0 0 0-1.438 1.435l-1.579 6.126a.5.5 0 0 1-.947.063z",
|
|
28
|
+
key: "edeuup"
|
|
29
|
+
}
|
|
30
|
+
]
|
|
31
|
+
]);
|
|
32
|
+
/**
|
|
33
|
+
* @license lucide-react v0.468.0 - ISC
|
|
34
|
+
*
|
|
35
|
+
* This source code is licensed under the ISC license.
|
|
36
|
+
* See the LICENSE file in the root directory of this source tree.
|
|
37
|
+
*/
|
|
38
|
+
const Pause = createLucideIcon("Pause", [
|
|
39
|
+
["rect", { x: "14", y: "4", width: "4", height: "16", rx: "1", key: "zuxfzm" }],
|
|
40
|
+
["rect", { x: "6", y: "4", width: "4", height: "16", rx: "1", key: "1okwgv" }]
|
|
41
|
+
]);
|
|
42
|
+
/**
|
|
43
|
+
* @license lucide-react v0.468.0 - ISC
|
|
44
|
+
*
|
|
45
|
+
* This source code is licensed under the ISC license.
|
|
46
|
+
* See the LICENSE file in the root directory of this source tree.
|
|
47
|
+
*/
|
|
48
|
+
const Play = createLucideIcon("Play", [
|
|
49
|
+
["polygon", { points: "6 3 20 12 6 21 6 3", key: "1oa8hb" }]
|
|
50
|
+
]);
|
|
51
|
+
/**
|
|
52
|
+
* @license lucide-react v0.468.0 - ISC
|
|
53
|
+
*
|
|
54
|
+
* This source code is licensed under the ISC license.
|
|
55
|
+
* See the LICENSE file in the root directory of this source tree.
|
|
56
|
+
*/
|
|
57
|
+
const RotateCcw = createLucideIcon("RotateCcw", [
|
|
58
|
+
["path", { d: "M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8", key: "1357e3" }],
|
|
59
|
+
["path", { d: "M3 3v5h5", key: "1xhq8a" }]
|
|
60
|
+
]);
|
|
61
|
+
const rrwebStyle = '.replayer-wrapper{position:relative}.replayer-mouse{position:absolute;width:20px;height:20px;transition:left .05s linear,top .05s linear;background-size:contain;background-position:center center;background-repeat:no-repeat;background-image:url(data:image/svg+xml;base64,PHN2ZyBoZWlnaHQ9JzMwMHB4JyB3aWR0aD0nMzAwcHgnICBmaWxsPSIjMDAwMDAwIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIGRhdGEtbmFtZT0iTGF5ZXIgMSIgdmlld0JveD0iMCAwIDUwIDUwIiB4PSIwcHgiIHk9IjBweCI+PHRpdGxlPkRlc2lnbl90bnA8L3RpdGxlPjxwYXRoIGQ9Ik00OC43MSw0Mi45MUwzNC4wOCwyOC4yOSw0NC4zMywxOEExLDEsMCwwLDAsNDQsMTYuMzlMMi4zNSwxLjA2QTEsMSwwLDAsMCwxLjA2LDIuMzVMMTYuMzksNDRhMSwxLDAsMCwwLDEuNjUuMzZMMjguMjksMzQuMDgsNDIuOTEsNDguNzFhMSwxLDAsMCwwLDEuNDEsMGw0LjM4LTQuMzhBMSwxLDAsMCwwLDQ4LjcxLDQyLjkxWm0tNS4wOSwzLjY3TDI5LDMyYTEsMSwwLDAsMC0xLjQxLDBsLTkuODUsOS44NUwzLjY5LDMuNjlsMzguMTIsMTRMMzIsMjcuNThBMSwxLDAsMCwwLDMyLDI5TDQ2LjU5LDQzLjYyWiI+PC9wYXRoPjwvc3ZnPg==);border-color:transparent}.replayer-mouse:after{content:"";display:inline-block;width:20px;height:20px;background:#4950f6;border-radius:100%;transform:translate(-50%,-50%);opacity:.3}.replayer-mouse.active:after{animation:click .2s ease-in-out 1}.replayer-mouse.touch-device{background-image:none;width:70px;height:70px;border-width:4px;border-style:solid;border-radius:100%;margin-left:-37px;margin-top:-37px;border-color:#4950f600;transition:left 0s linear,top 0s linear,border-color .2s ease-in-out}.replayer-mouse.touch-device.touch-active{border-color:#4950f6;transition:left .25s linear,top .25s linear,border-color .2s ease-in-out}.replayer-mouse.touch-device:after{opacity:0}.replayer-mouse.touch-device.active:after{animation:touch-click .2s ease-in-out 1}.replayer-mouse-tail{position:absolute;pointer-events:none}@keyframes click{0%{opacity:.3;width:20px;height:20px}50%{opacity:.5;width:10px;height:10px}}@keyframes touch-click{0%{opacity:0;width:20px;height:20px}50%{opacity:.5;width:10px;height:10px}}';
|
|
62
|
+
async function loadReplayEvents(url) {
|
|
63
|
+
const res = await fetch(url);
|
|
64
|
+
if (!res.ok) {
|
|
65
|
+
throw new Error(`리플레이 다운로드 실패: ${res.status}`);
|
|
66
|
+
}
|
|
67
|
+
const buffer = await res.arrayBuffer();
|
|
68
|
+
const json = ungzip_1(new Uint8Array(buffer), { to: "string" });
|
|
69
|
+
const parsed = JSON.parse(json);
|
|
70
|
+
if (!Array.isArray(parsed)) {
|
|
71
|
+
throw new Error("리플레이 데이터 형식이 올바르지 않습니다.");
|
|
72
|
+
}
|
|
73
|
+
return parsed;
|
|
74
|
+
}
|
|
75
|
+
const EVENT_TYPE_INCREMENTAL = 3;
|
|
76
|
+
const EVENT_TYPE_META = 4;
|
|
77
|
+
const SOURCE_MOUSE_INTERACTION = 2;
|
|
78
|
+
const SOURCE_INPUT = 5;
|
|
79
|
+
const MOUSE_CLICK = 2;
|
|
80
|
+
function deriveTimeline(events) {
|
|
81
|
+
var _a;
|
|
82
|
+
const list = events;
|
|
83
|
+
const startTime = list.length ? Number(((_a = list[0]) == null ? void 0 : _a.timestamp) ?? 0) : 0;
|
|
84
|
+
let recordedWidth = 0;
|
|
85
|
+
let recordedHeight = 0;
|
|
86
|
+
let seenFirstMeta = false;
|
|
87
|
+
const out = [];
|
|
88
|
+
for (const ev of list) {
|
|
89
|
+
const ts = Number((ev == null ? void 0 : ev.timestamp) ?? 0);
|
|
90
|
+
const t = ts - startTime;
|
|
91
|
+
const data = (ev == null ? void 0 : ev.data) ?? {};
|
|
92
|
+
if ((ev == null ? void 0 : ev.type) === EVENT_TYPE_META) {
|
|
93
|
+
if (!seenFirstMeta) {
|
|
94
|
+
seenFirstMeta = true;
|
|
95
|
+
recordedWidth = Number(data.width ?? 0);
|
|
96
|
+
recordedHeight = Number(data.height ?? 0);
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
out.push({ t, type: "nav", href: String(data.href ?? "") });
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
if ((ev == null ? void 0 : ev.type) === EVENT_TYPE_INCREMENTAL) {
|
|
103
|
+
const source = Number(data.source);
|
|
104
|
+
if (source === SOURCE_MOUSE_INTERACTION && Number(data.type) === MOUSE_CLICK) {
|
|
105
|
+
out.push({ t, type: "click", nodeId: Number(data.id) });
|
|
106
|
+
} else if (source === SOURCE_INPUT) {
|
|
107
|
+
out.push({ t, type: "input", nodeId: Number(data.id) });
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
return { startTime, recordedWidth, recordedHeight, events: out };
|
|
112
|
+
}
|
|
113
|
+
const WIDGET_HOST_HTML_ID = "issue-sticker-host";
|
|
114
|
+
const EVENT_FULL_SNAPSHOT = 2;
|
|
115
|
+
const EVENT_INCREMENTAL = 3;
|
|
116
|
+
const NODE_ELEMENT = 2;
|
|
117
|
+
const SOURCE_MUTATION = 0;
|
|
118
|
+
const SOURCE_MOUSE_MOVE = 1;
|
|
119
|
+
function asNode(value) {
|
|
120
|
+
return value && typeof value === "object" ? value : null;
|
|
121
|
+
}
|
|
122
|
+
function asRecord(value) {
|
|
123
|
+
return value && typeof value === "object" ? value : {};
|
|
124
|
+
}
|
|
125
|
+
function isWidgetHost(node) {
|
|
126
|
+
var _a;
|
|
127
|
+
return node.type === NODE_ELEMENT && ((_a = node.attributes) == null ? void 0 : _a.id) === WIDGET_HOST_HTML_ID;
|
|
128
|
+
}
|
|
129
|
+
function collectIds(node, into) {
|
|
130
|
+
const n = asNode(node);
|
|
131
|
+
if (!n) return;
|
|
132
|
+
if (typeof n.id === "number") into.add(n.id);
|
|
133
|
+
for (const child of Array.isArray(n.childNodes) ? n.childNodes : []) {
|
|
134
|
+
collectIds(child, into);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
function pruneTree(node, widgetIds) {
|
|
138
|
+
const n = asNode(node);
|
|
139
|
+
if (!n || !Array.isArray(n.childNodes)) return node;
|
|
140
|
+
const kept = [];
|
|
141
|
+
let changed = false;
|
|
142
|
+
for (const child of n.childNodes) {
|
|
143
|
+
const c = asNode(child);
|
|
144
|
+
if (c && isWidgetHost(c)) {
|
|
145
|
+
collectIds(c, widgetIds);
|
|
146
|
+
changed = true;
|
|
147
|
+
continue;
|
|
148
|
+
}
|
|
149
|
+
const prunedChild = pruneTree(child, widgetIds);
|
|
150
|
+
if (prunedChild !== child) changed = true;
|
|
151
|
+
kept.push(prunedChild);
|
|
152
|
+
}
|
|
153
|
+
return changed ? { ...n, childNodes: kept } : node;
|
|
154
|
+
}
|
|
155
|
+
function filterById(list, idOf, widgetIds) {
|
|
156
|
+
if (!Array.isArray(list)) return null;
|
|
157
|
+
const entries = list;
|
|
158
|
+
const kept = entries.filter((entry) => {
|
|
159
|
+
const id = idOf(entry);
|
|
160
|
+
return id === void 0 || !widgetIds.has(id);
|
|
161
|
+
});
|
|
162
|
+
return { list: kept, changed: kept.length !== entries.length };
|
|
163
|
+
}
|
|
164
|
+
function num(value) {
|
|
165
|
+
return typeof value === "number" ? value : void 0;
|
|
166
|
+
}
|
|
167
|
+
function stripAdds(adds, widgetIds) {
|
|
168
|
+
if (!Array.isArray(adds)) return null;
|
|
169
|
+
const kept = [];
|
|
170
|
+
let changed = false;
|
|
171
|
+
for (const raw of adds) {
|
|
172
|
+
const add = asRecord(raw);
|
|
173
|
+
const node = asNode(add.node);
|
|
174
|
+
const parentId = num(add.parentId);
|
|
175
|
+
if (node && isWidgetHost(node)) {
|
|
176
|
+
collectIds(node, widgetIds);
|
|
177
|
+
changed = true;
|
|
178
|
+
continue;
|
|
179
|
+
}
|
|
180
|
+
if (parentId !== void 0 && widgetIds.has(parentId)) {
|
|
181
|
+
if (node) collectIds(node, widgetIds);
|
|
182
|
+
changed = true;
|
|
183
|
+
continue;
|
|
184
|
+
}
|
|
185
|
+
kept.push(raw);
|
|
186
|
+
}
|
|
187
|
+
return { list: kept, changed };
|
|
188
|
+
}
|
|
189
|
+
function stripMutation(data, widgetIds) {
|
|
190
|
+
const next = { ...data };
|
|
191
|
+
let changed = false;
|
|
192
|
+
const adds = stripAdds(data.adds, widgetIds);
|
|
193
|
+
if (adds == null ? void 0 : adds.changed) {
|
|
194
|
+
next.adds = adds.list;
|
|
195
|
+
changed = true;
|
|
196
|
+
}
|
|
197
|
+
for (const key of ["removes", "texts", "attributes"]) {
|
|
198
|
+
const result = filterById(
|
|
199
|
+
data[key],
|
|
200
|
+
(entry) => num(entry.id),
|
|
201
|
+
widgetIds
|
|
202
|
+
);
|
|
203
|
+
if (result == null ? void 0 : result.changed) {
|
|
204
|
+
next[key] = result.list;
|
|
205
|
+
changed = true;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
if (!changed) return data;
|
|
209
|
+
const isEmpty = ["adds", "removes", "texts", "attributes"].every((key) => {
|
|
210
|
+
const list = next[key];
|
|
211
|
+
return !Array.isArray(list) || list.length === 0;
|
|
212
|
+
});
|
|
213
|
+
return isEmpty ? null : next;
|
|
214
|
+
}
|
|
215
|
+
function stripWidgetEvents(events) {
|
|
216
|
+
const widgetIds = /* @__PURE__ */ new Set();
|
|
217
|
+
const out = [];
|
|
218
|
+
let removedEvents = 0;
|
|
219
|
+
for (const event of events) {
|
|
220
|
+
const ev = event;
|
|
221
|
+
if ((ev == null ? void 0 : ev.type) === EVENT_FULL_SNAPSHOT) {
|
|
222
|
+
const data2 = asRecord(ev.data);
|
|
223
|
+
const pruned = pruneTree(data2.node, widgetIds);
|
|
224
|
+
out.push(pruned === data2.node ? event : { ...ev, data: { ...data2, node: pruned } });
|
|
225
|
+
continue;
|
|
226
|
+
}
|
|
227
|
+
if ((ev == null ? void 0 : ev.type) !== EVENT_INCREMENTAL) {
|
|
228
|
+
out.push(event);
|
|
229
|
+
continue;
|
|
230
|
+
}
|
|
231
|
+
const data = asRecord(ev.data);
|
|
232
|
+
const source = num(data.source);
|
|
233
|
+
if (source === SOURCE_MUTATION) {
|
|
234
|
+
const next = stripMutation(data, widgetIds);
|
|
235
|
+
if (next === null) {
|
|
236
|
+
removedEvents += 1;
|
|
237
|
+
continue;
|
|
238
|
+
}
|
|
239
|
+
out.push(next === data ? event : { ...ev, data: next });
|
|
240
|
+
continue;
|
|
241
|
+
}
|
|
242
|
+
if (source === SOURCE_MOUSE_MOVE) {
|
|
243
|
+
const positions = filterById(
|
|
244
|
+
data.positions,
|
|
245
|
+
(entry) => num(entry.id),
|
|
246
|
+
widgetIds
|
|
247
|
+
);
|
|
248
|
+
if (!(positions == null ? void 0 : positions.changed)) {
|
|
249
|
+
out.push(event);
|
|
250
|
+
continue;
|
|
251
|
+
}
|
|
252
|
+
if (positions.list.length === 0) {
|
|
253
|
+
removedEvents += 1;
|
|
254
|
+
continue;
|
|
255
|
+
}
|
|
256
|
+
out.push({ ...ev, data: { ...data, positions: positions.list } });
|
|
257
|
+
continue;
|
|
258
|
+
}
|
|
259
|
+
const id = num(data.id);
|
|
260
|
+
if (id !== void 0 && widgetIds.has(id)) {
|
|
261
|
+
removedEvents += 1;
|
|
262
|
+
continue;
|
|
263
|
+
}
|
|
264
|
+
out.push(event);
|
|
265
|
+
}
|
|
266
|
+
return { events: out, stripped: widgetIds.size > 0, removedEvents };
|
|
267
|
+
}
|
|
268
|
+
const TYPE_LABEL = {
|
|
269
|
+
click: "클릭",
|
|
270
|
+
input: "입력",
|
|
271
|
+
nav: "이동"
|
|
272
|
+
};
|
|
273
|
+
function describeNode(el, type) {
|
|
274
|
+
if (!el) return TYPE_LABEL[type];
|
|
275
|
+
const ariaLabel = (el.getAttribute("aria-label") ?? el.getAttribute("title") ?? "").trim();
|
|
276
|
+
if (ariaLabel) return ariaLabel.slice(0, 40);
|
|
277
|
+
const tag = el.tagName.toLowerCase();
|
|
278
|
+
if (tag === "input" || tag === "textarea" || tag === "select") {
|
|
279
|
+
const placeholder = tag !== "select" ? el.placeholder ?? "" : "";
|
|
280
|
+
const name = el.name ?? "";
|
|
281
|
+
const inputType = tag === "input" ? el.type ?? "" : "";
|
|
282
|
+
const label = placeholder || name || (inputType ? `${tag}[${inputType}]` : tag);
|
|
283
|
+
return label.trim().slice(0, 40);
|
|
284
|
+
}
|
|
285
|
+
let directText = "";
|
|
286
|
+
for (const child of Array.from(el.childNodes)) {
|
|
287
|
+
if (child.nodeType === Node.TEXT_NODE) {
|
|
288
|
+
directText += child.textContent ?? "";
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
directText = directText.trim().replace(/\s+/g, " ");
|
|
292
|
+
if (directText) return directText.slice(0, 40);
|
|
293
|
+
if (tag === "button" || tag === "a") {
|
|
294
|
+
const text = (el.textContent ?? "").trim().replace(/\s+/g, " ");
|
|
295
|
+
if (text) return text.slice(0, 40);
|
|
296
|
+
}
|
|
297
|
+
const firstClass = el.classList.item(0);
|
|
298
|
+
return (firstClass ? `${tag}.${firstClass}` : `<${tag}>`).slice(0, 40);
|
|
299
|
+
}
|
|
300
|
+
const MOUSE_TAIL_CONFIG = {
|
|
301
|
+
lineCap: "round",
|
|
302
|
+
lineWidth: 3,
|
|
303
|
+
strokeStyle: "rgb(239, 68, 68)"
|
|
304
|
+
};
|
|
305
|
+
const INITIAL_SHOW_TRAIL = true;
|
|
306
|
+
const SKIP_MS = 5e3;
|
|
307
|
+
const TYPE_DOT = {
|
|
308
|
+
click: "bg-brand",
|
|
309
|
+
input: "bg-success",
|
|
310
|
+
nav: "bg-text-tertiary"
|
|
311
|
+
};
|
|
312
|
+
function formatTime(ms) {
|
|
313
|
+
const total = Math.max(0, Math.floor(ms / 1e3));
|
|
314
|
+
const m = Math.floor(total / 60);
|
|
315
|
+
const s = total % 60;
|
|
316
|
+
return `${m}:${s.toString().padStart(2, "0")}`;
|
|
317
|
+
}
|
|
318
|
+
function describeEvent(mirror, ev) {
|
|
319
|
+
if (ev.type === "nav") return ev.href || TYPE_LABEL.nav;
|
|
320
|
+
const node = ev.nodeId != null && mirror ? mirror.getNode(ev.nodeId) : null;
|
|
321
|
+
return describeNode(node, ev.type);
|
|
322
|
+
}
|
|
323
|
+
function resolveLabel(mirror, row, cache) {
|
|
324
|
+
if (row.type === "nav") return row.href || TYPE_LABEL.nav;
|
|
325
|
+
if (row.nodeId == null) return TYPE_LABEL[row.type];
|
|
326
|
+
const cached = cache.get(row.nodeId);
|
|
327
|
+
if (cached !== void 0) return cached;
|
|
328
|
+
const label = describeEvent(mirror, row);
|
|
329
|
+
if (label !== TYPE_LABEL[row.type]) cache.set(row.nodeId, label);
|
|
330
|
+
return label;
|
|
331
|
+
}
|
|
332
|
+
const EventLog = memo(function EventLog2({
|
|
333
|
+
rows,
|
|
334
|
+
currentIndex,
|
|
335
|
+
filter,
|
|
336
|
+
onFilterChange,
|
|
337
|
+
onSeek,
|
|
338
|
+
total,
|
|
339
|
+
mirror
|
|
340
|
+
}) {
|
|
341
|
+
const labelCacheRef = useRef(/* @__PURE__ */ new Map());
|
|
342
|
+
const prevRowsRef = useRef(rows);
|
|
343
|
+
if (prevRowsRef.current !== rows) {
|
|
344
|
+
prevRowsRef.current = rows;
|
|
345
|
+
labelCacheRef.current = /* @__PURE__ */ new Map();
|
|
346
|
+
}
|
|
347
|
+
const labelCache = labelCacheRef.current;
|
|
348
|
+
const filtered = filter === "all" ? rows : rows.filter((r) => r.type === filter);
|
|
349
|
+
return /* @__PURE__ */ jsxs("div", { className: "flex h-full min-h-0 min-w-0 flex-col rounded-md border border-line-subtle bg-surface-1", children: [
|
|
350
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between border-b border-line-subtle px-3 py-2.5", children: [
|
|
351
|
+
/* @__PURE__ */ jsx("span", { className: "text-sm font-semibold text-text-primary", children: "이벤트 로그" }),
|
|
352
|
+
/* @__PURE__ */ jsxs("span", { className: "text-xs text-text-tertiary", children: [
|
|
353
|
+
"전체 ",
|
|
354
|
+
total
|
|
355
|
+
] })
|
|
356
|
+
] }),
|
|
357
|
+
/* @__PURE__ */ jsx("div", { className: "flex gap-1.5 border-b border-line-subtle px-3 py-2", children: ["all", "click", "input", "nav"].map((f) => /* @__PURE__ */ jsx(
|
|
358
|
+
"button",
|
|
359
|
+
{
|
|
360
|
+
type: "button",
|
|
361
|
+
onClick: () => onFilterChange(f),
|
|
362
|
+
className: cn(
|
|
363
|
+
"rounded-full border px-2 py-0.5 text-xs",
|
|
364
|
+
filter === f ? "border-text-primary bg-text-primary text-text-inverse" : "border-line-subtle text-text-secondary"
|
|
365
|
+
),
|
|
366
|
+
children: f === "all" ? "전체" : TYPE_LABEL[f]
|
|
367
|
+
},
|
|
368
|
+
f
|
|
369
|
+
)) }),
|
|
370
|
+
/* @__PURE__ */ jsx("div", { className: "min-h-0 flex-1 overflow-auto", children: filtered.map((row) => {
|
|
371
|
+
const isCurrent = row.index === currentIndex;
|
|
372
|
+
return /* @__PURE__ */ jsxs(
|
|
373
|
+
"button",
|
|
374
|
+
{
|
|
375
|
+
type: "button",
|
|
376
|
+
onClick: () => onSeek(row.t),
|
|
377
|
+
className: cn(
|
|
378
|
+
"flex w-full items-center gap-2 border-b border-line-subtle px-3 py-2 text-left text-xs hover:bg-surface-2",
|
|
379
|
+
isCurrent && "bg-success-subtle"
|
|
380
|
+
),
|
|
381
|
+
children: [
|
|
382
|
+
/* @__PURE__ */ jsx("span", { className: "w-9 shrink-0 tabular-nums text-text-tertiary", children: formatTime(row.t) }),
|
|
383
|
+
/* @__PURE__ */ jsx("span", { className: cn("h-2 w-2 shrink-0 rounded-sm", TYPE_DOT[row.type]) }),
|
|
384
|
+
/* @__PURE__ */ jsx("span", { className: "min-w-0 flex-1 truncate text-text-secondary", children: resolveLabel(mirror, row, labelCache) }),
|
|
385
|
+
/* @__PURE__ */ jsx("span", { className: "shrink-0 text-text-tertiary", children: TYPE_LABEL[row.type] })
|
|
386
|
+
]
|
|
387
|
+
},
|
|
388
|
+
`${row.t}-${row.index}`
|
|
389
|
+
);
|
|
390
|
+
}) })
|
|
391
|
+
] });
|
|
392
|
+
});
|
|
393
|
+
function SessionReplayPlayer({ url }) {
|
|
394
|
+
const rootRef = useRef(null);
|
|
395
|
+
const hostRef = useRef(null);
|
|
396
|
+
const stageRef = useRef(null);
|
|
397
|
+
const replayerRef = useRef(null);
|
|
398
|
+
const startedRef = useRef(false);
|
|
399
|
+
const rafRef = useRef(null);
|
|
400
|
+
const durationRef = useRef(0);
|
|
401
|
+
const isPlayingRef = useRef(false);
|
|
402
|
+
const showTrailRef = useRef(INITIAL_SHOW_TRAIL);
|
|
403
|
+
const mirrorRef = useRef(null);
|
|
404
|
+
const [state, setState] = useState("loading");
|
|
405
|
+
const [isPlaying, setIsPlaying] = useState(false);
|
|
406
|
+
const [showTrail, setShowTrail] = useState(INITIAL_SHOW_TRAIL);
|
|
407
|
+
const [durationMs, setDurationMs] = useState(0);
|
|
408
|
+
const [currentMs, setCurrentMs] = useState(0);
|
|
409
|
+
const [timeline, setTimeline] = useState([]);
|
|
410
|
+
const [logRows, setLogRows] = useState([]);
|
|
411
|
+
const [filter, setFilter] = useState("all");
|
|
412
|
+
const [isFullscreen, setIsFullscreen] = useState(false);
|
|
413
|
+
const [widgetStripped, setWidgetStripped] = useState(false);
|
|
414
|
+
const applyDuration = (ms) => {
|
|
415
|
+
durationRef.current = ms;
|
|
416
|
+
setDurationMs(ms);
|
|
417
|
+
};
|
|
418
|
+
const applyIsPlaying = (v) => {
|
|
419
|
+
isPlayingRef.current = v;
|
|
420
|
+
setIsPlaying(v);
|
|
421
|
+
};
|
|
422
|
+
const stopRaf = useCallback(() => {
|
|
423
|
+
if (rafRef.current != null) cancelAnimationFrame(rafRef.current);
|
|
424
|
+
rafRef.current = null;
|
|
425
|
+
}, []);
|
|
426
|
+
const startRaf = useCallback(() => {
|
|
427
|
+
stopRaf();
|
|
428
|
+
const tick = () => {
|
|
429
|
+
const r = replayerRef.current;
|
|
430
|
+
if (r) setCurrentMs(Math.min(r.getCurrentTime(), durationRef.current));
|
|
431
|
+
rafRef.current = requestAnimationFrame(tick);
|
|
432
|
+
};
|
|
433
|
+
rafRef.current = requestAnimationFrame(tick);
|
|
434
|
+
}, [stopRaf]);
|
|
435
|
+
useEffect(() => {
|
|
436
|
+
let cancelled = false;
|
|
437
|
+
setState("loading");
|
|
438
|
+
applyIsPlaying(false);
|
|
439
|
+
setShowTrail(INITIAL_SHOW_TRAIL);
|
|
440
|
+
showTrailRef.current = INITIAL_SHOW_TRAIL;
|
|
441
|
+
mirrorRef.current = null;
|
|
442
|
+
setCurrentMs(0);
|
|
443
|
+
startedRef.current = false;
|
|
444
|
+
(async () => {
|
|
445
|
+
var _a;
|
|
446
|
+
try {
|
|
447
|
+
const loaded = await loadReplayEvents(url);
|
|
448
|
+
if (cancelled) return;
|
|
449
|
+
const { events, stripped } = stripWidgetEvents(loaded);
|
|
450
|
+
if (!hostRef.current || events.length < 2) {
|
|
451
|
+
setState("empty");
|
|
452
|
+
return;
|
|
453
|
+
}
|
|
454
|
+
setWidgetStripped(stripped);
|
|
455
|
+
const derived = deriveTimeline(events);
|
|
456
|
+
const host = hostRef.current;
|
|
457
|
+
let shadow = host.shadowRoot;
|
|
458
|
+
if (!shadow) {
|
|
459
|
+
shadow = host.attachShadow({ mode: "open" });
|
|
460
|
+
const styleEl = document.createElement("style");
|
|
461
|
+
styleEl.textContent = typeof rrwebStyle === "string" ? rrwebStyle : "";
|
|
462
|
+
shadow.appendChild(styleEl);
|
|
463
|
+
}
|
|
464
|
+
(_a = stageRef.current) == null ? void 0 : _a.remove();
|
|
465
|
+
const stage = document.createElement("div");
|
|
466
|
+
if (derived.recordedWidth > 0) {
|
|
467
|
+
const recW = derived.recordedWidth;
|
|
468
|
+
const recH = derived.recordedHeight || derived.recordedWidth;
|
|
469
|
+
stage.style.setProperty("--recorded-w", `${recW}px`);
|
|
470
|
+
stage.style.setProperty("--recorded-h", `${recH}px`);
|
|
471
|
+
stage.style.position = "absolute";
|
|
472
|
+
stage.style.left = "50%";
|
|
473
|
+
stage.style.top = "50%";
|
|
474
|
+
stage.style.transformOrigin = "center";
|
|
475
|
+
stage.style.transform = "translate(-50%, -50%) scale(min(tan(atan2(100cqw, var(--recorded-w))), tan(atan2(100cqh, var(--recorded-h)))))";
|
|
476
|
+
host.style.aspectRatio = `${recW} / ${recH}`;
|
|
477
|
+
host.style.containerType = "size";
|
|
478
|
+
}
|
|
479
|
+
shadow.appendChild(stage);
|
|
480
|
+
stageRef.current = stage;
|
|
481
|
+
const replayer = new Replayer(
|
|
482
|
+
events,
|
|
483
|
+
{
|
|
484
|
+
root: stage,
|
|
485
|
+
speed: 1,
|
|
486
|
+
showWarning: false,
|
|
487
|
+
mouseTail: INITIAL_SHOW_TRAIL ? MOUSE_TAIL_CONFIG : false
|
|
488
|
+
}
|
|
489
|
+
);
|
|
490
|
+
replayer.on("finish", () => {
|
|
491
|
+
if (cancelled) return;
|
|
492
|
+
applyIsPlaying(false);
|
|
493
|
+
startedRef.current = false;
|
|
494
|
+
stopRaf();
|
|
495
|
+
});
|
|
496
|
+
replayerRef.current = replayer;
|
|
497
|
+
mirrorRef.current = replayer.getMirror();
|
|
498
|
+
setTimeline(derived.events);
|
|
499
|
+
const totalTime = replayer.getMetaData().totalTime;
|
|
500
|
+
applyDuration(totalTime);
|
|
501
|
+
const rows = derived.events.map((ev, i) => ({
|
|
502
|
+
t: ev.t,
|
|
503
|
+
type: ev.type,
|
|
504
|
+
nodeId: ev.nodeId,
|
|
505
|
+
href: ev.href,
|
|
506
|
+
index: i
|
|
507
|
+
}));
|
|
508
|
+
setLogRows(rows);
|
|
509
|
+
setState("ready");
|
|
510
|
+
} catch {
|
|
511
|
+
if (!cancelled) setState("error");
|
|
512
|
+
}
|
|
513
|
+
})();
|
|
514
|
+
return () => {
|
|
515
|
+
var _a, _b;
|
|
516
|
+
cancelled = true;
|
|
517
|
+
stopRaf();
|
|
518
|
+
try {
|
|
519
|
+
(_b = (_a = replayerRef.current) == null ? void 0 : _a.destroy) == null ? void 0 : _b.call(_a);
|
|
520
|
+
} catch {
|
|
521
|
+
}
|
|
522
|
+
replayerRef.current = null;
|
|
523
|
+
};
|
|
524
|
+
}, [url]);
|
|
525
|
+
useEffect(() => {
|
|
526
|
+
const onChange = () => setIsFullscreen(document.fullscreenElement === rootRef.current);
|
|
527
|
+
document.addEventListener("fullscreenchange", onChange);
|
|
528
|
+
return () => document.removeEventListener("fullscreenchange", onChange);
|
|
529
|
+
}, []);
|
|
530
|
+
const seek = useCallback((ms) => {
|
|
531
|
+
const r = replayerRef.current;
|
|
532
|
+
if (!r) return;
|
|
533
|
+
const clamped = Math.max(0, Math.min(ms, durationRef.current));
|
|
534
|
+
if (isPlayingRef.current) r.play(clamped);
|
|
535
|
+
else r.pause(clamped);
|
|
536
|
+
startedRef.current = true;
|
|
537
|
+
setCurrentMs(clamped);
|
|
538
|
+
}, []);
|
|
539
|
+
const togglePlay = useCallback(() => {
|
|
540
|
+
const r = replayerRef.current;
|
|
541
|
+
if (!r) return;
|
|
542
|
+
if (isPlayingRef.current) {
|
|
543
|
+
r.pause();
|
|
544
|
+
applyIsPlaying(false);
|
|
545
|
+
stopRaf();
|
|
546
|
+
} else {
|
|
547
|
+
r.play(startedRef.current ? r.getCurrentTime() : 0);
|
|
548
|
+
startedRef.current = true;
|
|
549
|
+
applyIsPlaying(true);
|
|
550
|
+
startRaf();
|
|
551
|
+
}
|
|
552
|
+
}, [stopRaf, startRaf]);
|
|
553
|
+
const restart = useCallback(() => {
|
|
554
|
+
const r = replayerRef.current;
|
|
555
|
+
if (!r) return;
|
|
556
|
+
r.play(0);
|
|
557
|
+
startedRef.current = true;
|
|
558
|
+
applyIsPlaying(true);
|
|
559
|
+
startRaf();
|
|
560
|
+
}, [startRaf]);
|
|
561
|
+
const setSpeed = useCallback((speed) => {
|
|
562
|
+
var _a;
|
|
563
|
+
return (_a = replayerRef.current) == null ? void 0 : _a.setConfig({ speed });
|
|
564
|
+
}, []);
|
|
565
|
+
const toggleTrail = useCallback(() => {
|
|
566
|
+
var _a;
|
|
567
|
+
const next = !showTrailRef.current;
|
|
568
|
+
showTrailRef.current = next;
|
|
569
|
+
setShowTrail(next);
|
|
570
|
+
(_a = replayerRef.current) == null ? void 0 : _a.setConfig({ mouseTail: next ? MOUSE_TAIL_CONFIG : false });
|
|
571
|
+
}, []);
|
|
572
|
+
const toggleFullscreen = useCallback(() => {
|
|
573
|
+
var _a, _b;
|
|
574
|
+
if (document.fullscreenElement === rootRef.current) document.exitFullscreen();
|
|
575
|
+
else (_b = (_a = rootRef.current) == null ? void 0 : _a.requestFullscreen) == null ? void 0 : _b.call(_a);
|
|
576
|
+
}, []);
|
|
577
|
+
const onKeyDown = useCallback(
|
|
578
|
+
(e) => {
|
|
579
|
+
const target = e.target;
|
|
580
|
+
if (target.closest('button, input, select, textarea, a, [role="switch"]')) return;
|
|
581
|
+
if (e.code === "Space") {
|
|
582
|
+
e.preventDefault();
|
|
583
|
+
togglePlay();
|
|
584
|
+
} else if (e.code === "ArrowLeft") {
|
|
585
|
+
e.preventDefault();
|
|
586
|
+
seek(currentMs - SKIP_MS);
|
|
587
|
+
} else if (e.code === "ArrowRight") {
|
|
588
|
+
e.preventDefault();
|
|
589
|
+
seek(currentMs + SKIP_MS);
|
|
590
|
+
} else if (e.code === "KeyF") {
|
|
591
|
+
e.preventDefault();
|
|
592
|
+
toggleFullscreen();
|
|
593
|
+
}
|
|
594
|
+
},
|
|
595
|
+
[togglePlay, seek, currentMs, toggleFullscreen]
|
|
596
|
+
);
|
|
597
|
+
const currentIndex = logRows.reduce((acc, row) => row.t <= currentMs ? row.index : acc, -1);
|
|
598
|
+
if (state === "error") {
|
|
599
|
+
return /* @__PURE__ */ jsx("p", { className: "text-sm text-text-secondary", children: "리플레이를 불러오지 못했어요." });
|
|
600
|
+
}
|
|
601
|
+
if (state === "empty") {
|
|
602
|
+
return /* @__PURE__ */ jsx("p", { className: "text-sm text-text-secondary", children: "재생할 리플레이 데이터가 없어요." });
|
|
603
|
+
}
|
|
604
|
+
return /* @__PURE__ */ jsx(TooltipProvider, { children: /* @__PURE__ */ jsxs(
|
|
605
|
+
"div",
|
|
606
|
+
{
|
|
607
|
+
ref: rootRef,
|
|
608
|
+
tabIndex: 0,
|
|
609
|
+
onKeyDown,
|
|
610
|
+
"data-testid": "session-replay-player",
|
|
611
|
+
className: cn(
|
|
612
|
+
"grid min-w-0 gap-3.5 outline-none md:grid-cols-[minmax(0,1fr)_minmax(260px,340px)]",
|
|
613
|
+
isFullscreen && "h-full w-full content-center overflow-hidden bg-surface-1 p-3"
|
|
614
|
+
),
|
|
615
|
+
children: [
|
|
616
|
+
/* @__PURE__ */ jsxs("div", { className: "flex min-w-0 flex-col", children: [
|
|
617
|
+
state === "loading" ? /* @__PURE__ */ jsx("p", { className: "text-sm text-text-secondary", children: "리플레이 불러오는 중…" }) : null,
|
|
618
|
+
/* @__PURE__ */ jsx(
|
|
619
|
+
"div",
|
|
620
|
+
{
|
|
621
|
+
ref: hostRef,
|
|
622
|
+
"data-testid": "session-replay-host",
|
|
623
|
+
className: "overflow-hidden rounded-t-md border border-line-subtle bg-surface-2",
|
|
624
|
+
style: { position: "relative", maxHeight: "80vh" }
|
|
625
|
+
}
|
|
626
|
+
),
|
|
627
|
+
state === "ready" ? /* @__PURE__ */ jsxs("div", { className: "rounded-b-md border border-t-0 border-line-subtle bg-surface-1 px-3 pb-3 pt-2.5", children: [
|
|
628
|
+
/* @__PURE__ */ jsxs("div", { className: "relative mb-2 h-6", children: [
|
|
629
|
+
/* @__PURE__ */ jsx(
|
|
630
|
+
"input",
|
|
631
|
+
{
|
|
632
|
+
type: "range",
|
|
633
|
+
"aria-label": "재생 위치",
|
|
634
|
+
min: 0,
|
|
635
|
+
max: durationMs,
|
|
636
|
+
value: currentMs,
|
|
637
|
+
onChange: (e) => seek(Number(e.target.value)),
|
|
638
|
+
className: "absolute inset-x-0 top-2 w-full accent-brand"
|
|
639
|
+
}
|
|
640
|
+
),
|
|
641
|
+
durationMs > 0 ? timeline.map((ev, i) => /* @__PURE__ */ jsx(
|
|
642
|
+
"button",
|
|
643
|
+
{
|
|
644
|
+
type: "button",
|
|
645
|
+
"aria-label": `${TYPE_LABEL[ev.type]} 지점으로 이동`,
|
|
646
|
+
onClick: () => seek(ev.t),
|
|
647
|
+
style: { left: `${ev.t / durationMs * 100}%` },
|
|
648
|
+
className: cn(
|
|
649
|
+
"absolute top-0 h-3 w-0.5 -translate-x-1/2 rounded-sm",
|
|
650
|
+
TYPE_DOT[ev.type]
|
|
651
|
+
)
|
|
652
|
+
},
|
|
653
|
+
i
|
|
654
|
+
)) : null
|
|
655
|
+
] }),
|
|
656
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2.5", children: [
|
|
657
|
+
/* @__PURE__ */ jsx(
|
|
658
|
+
ControlButton,
|
|
659
|
+
{
|
|
660
|
+
tip: "재생 / 일시정지 · Space",
|
|
661
|
+
onClick: togglePlay,
|
|
662
|
+
primary: true,
|
|
663
|
+
label: isPlaying ? "일시정지" : "재생",
|
|
664
|
+
children: isPlaying ? /* @__PURE__ */ jsx(Pause, { className: "h-3.5 w-3.5" }) : /* @__PURE__ */ jsx(Play, { className: "h-3.5 w-3.5" })
|
|
665
|
+
}
|
|
666
|
+
),
|
|
667
|
+
/* @__PURE__ */ jsx(ControlButton, { tip: "처음부터", onClick: restart, label: "처음부터", children: /* @__PURE__ */ jsx(RotateCcw, { className: "h-3.5 w-3.5" }) }),
|
|
668
|
+
/* @__PURE__ */ jsxs("span", { className: "text-xs tabular-nums text-text-secondary", children: [
|
|
669
|
+
formatTime(currentMs),
|
|
670
|
+
" / ",
|
|
671
|
+
formatTime(durationMs)
|
|
672
|
+
] }),
|
|
673
|
+
/* @__PURE__ */ jsx("span", { className: "flex-1" }),
|
|
674
|
+
/* @__PURE__ */ jsx("div", { className: "flex overflow-hidden rounded-md border border-line-subtle", children: [1, 2, 4].map((s) => /* @__PURE__ */ jsxs(
|
|
675
|
+
"button",
|
|
676
|
+
{
|
|
677
|
+
type: "button",
|
|
678
|
+
onClick: () => setSpeed(s),
|
|
679
|
+
className: "px-2 py-1 text-xs text-text-secondary hover:bg-surface-2",
|
|
680
|
+
children: [
|
|
681
|
+
s,
|
|
682
|
+
"x"
|
|
683
|
+
]
|
|
684
|
+
},
|
|
685
|
+
s
|
|
686
|
+
)) }),
|
|
687
|
+
/* @__PURE__ */ jsx(
|
|
688
|
+
ControlButton,
|
|
689
|
+
{
|
|
690
|
+
tip: "마우스 궤적",
|
|
691
|
+
onClick: toggleTrail,
|
|
692
|
+
active: showTrail,
|
|
693
|
+
label: "마우스 궤적",
|
|
694
|
+
pressed: showTrail,
|
|
695
|
+
children: /* @__PURE__ */ jsx(MousePointer2, { className: "h-3.5 w-3.5" })
|
|
696
|
+
}
|
|
697
|
+
),
|
|
698
|
+
/* @__PURE__ */ jsx(ControlButton, { tip: "전체화면 · F", onClick: toggleFullscreen, label: "전체화면", children: /* @__PURE__ */ jsx(Maximize2, { className: "h-3.5 w-3.5" }) })
|
|
699
|
+
] }),
|
|
700
|
+
widgetStripped ? /* @__PURE__ */ jsx("p", { className: "mt-2 text-xs text-text-tertiary", children: "이슈스티커 위젯(버튼·이슈 생성 폼)은 재생에서 제외했습니다." }) : null
|
|
701
|
+
] }) : null
|
|
702
|
+
] }),
|
|
703
|
+
state === "ready" ? /* @__PURE__ */ jsx("div", { className: "relative min-h-0 min-w-0", children: /* @__PURE__ */ jsx("div", { className: "absolute inset-0", children: /* @__PURE__ */ jsx(
|
|
704
|
+
EventLog,
|
|
705
|
+
{
|
|
706
|
+
rows: logRows,
|
|
707
|
+
currentIndex,
|
|
708
|
+
filter,
|
|
709
|
+
onFilterChange: setFilter,
|
|
710
|
+
onSeek: seek,
|
|
711
|
+
total: timeline.length,
|
|
712
|
+
mirror: mirrorRef.current
|
|
713
|
+
}
|
|
714
|
+
) }) }) : null
|
|
715
|
+
]
|
|
716
|
+
}
|
|
717
|
+
) });
|
|
718
|
+
}
|
|
719
|
+
function ControlButton({
|
|
720
|
+
tip,
|
|
721
|
+
label,
|
|
722
|
+
onClick,
|
|
723
|
+
children,
|
|
724
|
+
primary,
|
|
725
|
+
active,
|
|
726
|
+
pressed
|
|
727
|
+
}) {
|
|
728
|
+
return /* @__PURE__ */ jsxs(Tooltip, { children: [
|
|
729
|
+
/* @__PURE__ */ jsx(TooltipTrigger, { asChild: true, children: /* @__PURE__ */ jsx(
|
|
730
|
+
"button",
|
|
731
|
+
{
|
|
732
|
+
type: "button",
|
|
733
|
+
"aria-label": label,
|
|
734
|
+
"aria-pressed": pressed,
|
|
735
|
+
onClick,
|
|
736
|
+
className: cn(
|
|
737
|
+
"inline-flex h-[30px] w-[30px] items-center justify-center rounded-md border",
|
|
738
|
+
primary ? "border-brand bg-brand text-brand-fg" : active ? "border-success text-success" : "border-line-subtle text-text-secondary hover:bg-surface-2"
|
|
739
|
+
),
|
|
740
|
+
children
|
|
741
|
+
}
|
|
742
|
+
) }),
|
|
743
|
+
/* @__PURE__ */ jsx(TooltipContent, { children: tip })
|
|
744
|
+
] });
|
|
745
|
+
}
|
|
746
|
+
function resolveSessionReplayUrl(issue) {
|
|
747
|
+
var _a;
|
|
748
|
+
const directUrl = issue == null ? void 0 : issue.session_replay_url;
|
|
749
|
+
if (typeof directUrl === "string" && directUrl.trim()) {
|
|
750
|
+
return directUrl;
|
|
751
|
+
}
|
|
752
|
+
const nestedUrl = (_a = issue == null ? void 0 : issue.session_replay) == null ? void 0 : _a.url;
|
|
753
|
+
return typeof nestedUrl === "string" && nestedUrl.trim() ? nestedUrl : null;
|
|
754
|
+
}
|
|
755
|
+
function SessionReplayViewer({ issue }) {
|
|
756
|
+
const url = resolveSessionReplayUrl(issue);
|
|
757
|
+
if (!url) {
|
|
758
|
+
return /* @__PURE__ */ jsx("p", { className: "text-sm text-text-secondary", children: "첨부된 세션 리플레이가 없습니다." });
|
|
759
|
+
}
|
|
760
|
+
return /* @__PURE__ */ jsx(SessionReplayPlayer, { url });
|
|
761
|
+
}
|
|
762
|
+
export {
|
|
763
|
+
SessionReplayViewer
|
|
764
|
+
};
|