issue-sticker 0.3.1 → 0.5.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 +58 -0
- package/README.md +22 -5
- package/dist/{createLucideIcon-CmSNiW63.js → createLucideIcon-BzRMP02h.js} +102 -99
- package/dist/{externalUserPayload-hJjuUx6q.js → externalUserPayload-zu5IXBjn.js} +10101 -8502
- package/dist/issue-sticker-headless-react.js +129 -72
- package/dist/issue-sticker-headless.js +219 -91
- package/dist/issue-sticker-react.js +4480 -3219
- package/dist/{tooltip-DciWyUWs.js → tooltip-B6UPO-U8.js} +1 -1
- package/headless.d.ts +31 -4
- package/package.json +3 -2
- package/react.d.ts +9 -1
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { jsxs, jsx } from "react/jsx-runtime";
|
|
2
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-
|
|
4
|
-
import { T as TooltipProvider, a as Tooltip, b as TooltipTrigger, c as TooltipContent } from "./tooltip-
|
|
3
|
+
import { c as createLucideIcon, u as ungzip_1, a as cn, R as Replayer } from "./createLucideIcon-BzRMP02h.js";
|
|
4
|
+
import { T as TooltipProvider, a as Tooltip, b as TooltipTrigger, c as TooltipContent } from "./tooltip-B6UPO-U8.js";
|
|
5
5
|
/**
|
|
6
6
|
* @license lucide-react v0.468.0 - ISC
|
|
7
7
|
*
|
|
@@ -72,8 +72,107 @@ async function loadReplayEvents(url) {
|
|
|
72
72
|
}
|
|
73
73
|
return parsed;
|
|
74
74
|
}
|
|
75
|
+
const TYPE_LABEL = {
|
|
76
|
+
click: "클릭",
|
|
77
|
+
input: "입력",
|
|
78
|
+
nav: "이동"
|
|
79
|
+
};
|
|
80
|
+
const NODE_TYPE_ELEMENT = 2;
|
|
81
|
+
const NODE_TYPE_TEXT = 3;
|
|
82
|
+
class NodeIndex {
|
|
83
|
+
constructor() {
|
|
84
|
+
this.nodes = /* @__PURE__ */ new Map();
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* 색인을 비우고 주어진 트리로 다시 채운다.
|
|
88
|
+
*
|
|
89
|
+
* FullSnapshot 마다 호출한다. rrweb 은 문서마다 `record()` 를 새로 돌리며 nodeId 를
|
|
90
|
+
* 1부터 다시 매기므로, 문서가 바뀌면 앞 문서의 색인이 남아 있으면 안 된다 — 남으면
|
|
91
|
+
* 같은 id 의 엉뚱한 요소 이름이 조용히 붙는다.
|
|
92
|
+
*/
|
|
93
|
+
reset(root) {
|
|
94
|
+
this.nodes = /* @__PURE__ */ new Map();
|
|
95
|
+
if (root) this.add(root);
|
|
96
|
+
}
|
|
97
|
+
/** 노드와 그 하위 트리를 색인에 넣는다. `parentId` 를 주면 부모의 자식으로도 잇는다. */
|
|
98
|
+
add(node, parentId) {
|
|
99
|
+
var _a;
|
|
100
|
+
if (!node || typeof node.id !== "number") return;
|
|
101
|
+
this.nodes.set(node.id, { node, childIds: [] });
|
|
102
|
+
if (parentId !== void 0) {
|
|
103
|
+
(_a = this.nodes.get(parentId)) == null ? void 0 : _a.childIds.push(node.id);
|
|
104
|
+
}
|
|
105
|
+
for (const child of node.childNodes ?? []) {
|
|
106
|
+
this.add(child, node.id);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
get(id) {
|
|
110
|
+
var _a;
|
|
111
|
+
return (_a = this.nodes.get(id)) == null ? void 0 : _a.node;
|
|
112
|
+
}
|
|
113
|
+
/** 직접 자식 노드들. 트리 안의 childNodes 와 mutation 으로 붙은 것을 함께 본다. */
|
|
114
|
+
childrenOf(id) {
|
|
115
|
+
const entry = this.nodes.get(id);
|
|
116
|
+
if (!entry) return [];
|
|
117
|
+
const fromTree = entry.node.childNodes ?? [];
|
|
118
|
+
const fromMutation = entry.childIds.map((childId) => {
|
|
119
|
+
var _a;
|
|
120
|
+
return (_a = this.nodes.get(childId)) == null ? void 0 : _a.node;
|
|
121
|
+
}).filter((n) => n !== void 0 && !fromTree.includes(n));
|
|
122
|
+
return [...fromTree, ...fromMutation];
|
|
123
|
+
}
|
|
124
|
+
/** 하위 텍스트를 모두 이어 붙인다. button·a 같은 리프 레이블용. */
|
|
125
|
+
deepText(id, depth = 0) {
|
|
126
|
+
if (depth > 10) return "";
|
|
127
|
+
const node = this.get(id);
|
|
128
|
+
if (!node) return "";
|
|
129
|
+
if (node.type === NODE_TYPE_TEXT) return node.textContent ?? "";
|
|
130
|
+
return this.childrenOf(id).map(
|
|
131
|
+
(child) => child.type === NODE_TYPE_TEXT ? child.textContent ?? "" : typeof child.id === "number" ? this.deepText(child.id, depth + 1) : ""
|
|
132
|
+
).join("");
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* 상호작용 대상을 사람이 읽을 짧은 레이블로 바꾼다. 우선순위는 다음과 같다.
|
|
136
|
+
* 1. aria-label / title
|
|
137
|
+
* 2. form 컨트롤: placeholder → name → type (예: input[email])
|
|
138
|
+
* 3. 직접 자식 Text 노드만 결합 (컨테이너가 자손 텍스트를 통째로 빨아들이는 것 방지)
|
|
139
|
+
* 4. button / a: 하위 텍스트 전체 (리프 레이블이라 짧다)
|
|
140
|
+
* 5. 압축 셀렉터: tag.firstClass 또는 <tag>
|
|
141
|
+
*
|
|
142
|
+
* 40자로 자른다. 색인에 없으면 `TYPE_LABEL[type]` 폴백.
|
|
143
|
+
*/
|
|
144
|
+
describe(id, type) {
|
|
145
|
+
const node = id === void 0 ? void 0 : this.get(id);
|
|
146
|
+
if (!node || node.type !== NODE_TYPE_ELEMENT || id === void 0) return TYPE_LABEL[type];
|
|
147
|
+
const attrs = node.attributes ?? {};
|
|
148
|
+
const attr = (name) => {
|
|
149
|
+
const value = attrs[name];
|
|
150
|
+
return typeof value === "string" ? value : "";
|
|
151
|
+
};
|
|
152
|
+
const ariaLabel = (attr("aria-label") || attr("title")).trim();
|
|
153
|
+
if (ariaLabel) return ariaLabel.slice(0, 40);
|
|
154
|
+
const tag = (node.tagName ?? "").toLowerCase();
|
|
155
|
+
if (tag === "input" || tag === "textarea" || tag === "select") {
|
|
156
|
+
const placeholder = tag === "select" ? "" : attr("placeholder");
|
|
157
|
+
const name = attr("name");
|
|
158
|
+
const inputType = tag === "input" ? attr("type") : "";
|
|
159
|
+
const label = placeholder || name || (inputType ? `${tag}[${inputType}]` : tag);
|
|
160
|
+
return label.trim().slice(0, 40);
|
|
161
|
+
}
|
|
162
|
+
const directText = this.childrenOf(id).filter((child) => child.type === NODE_TYPE_TEXT).map((child) => child.textContent ?? "").join("").trim().replace(/\s+/g, " ");
|
|
163
|
+
if (directText) return directText.slice(0, 40);
|
|
164
|
+
if (tag === "button" || tag === "a") {
|
|
165
|
+
const deep = this.deepText(id).trim().replace(/\s+/g, " ");
|
|
166
|
+
if (deep) return deep.slice(0, 40);
|
|
167
|
+
}
|
|
168
|
+
const firstClass = attr("class").trim().split(/\s+/)[0];
|
|
169
|
+
return (firstClass ? `${tag}.${firstClass}` : `<${tag}>`).slice(0, 40);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
const EVENT_TYPE_FULL_SNAPSHOT = 2;
|
|
75
173
|
const EVENT_TYPE_INCREMENTAL = 3;
|
|
76
174
|
const EVENT_TYPE_META = 4;
|
|
175
|
+
const SOURCE_MUTATION$1 = 0;
|
|
77
176
|
const SOURCE_MOUSE_INTERACTION = 2;
|
|
78
177
|
const SOURCE_INPUT = 5;
|
|
79
178
|
const MOUSE_CLICK = 2;
|
|
@@ -85,6 +184,7 @@ function deriveTimeline(events) {
|
|
|
85
184
|
let recordedHeight = 0;
|
|
86
185
|
let seenFirstMeta = false;
|
|
87
186
|
const out = [];
|
|
187
|
+
const index = new NodeIndex();
|
|
88
188
|
for (const ev of list) {
|
|
89
189
|
const ts = Number((ev == null ? void 0 : ev.timestamp) ?? 0);
|
|
90
190
|
const t = ts - startTime;
|
|
@@ -96,15 +196,26 @@ function deriveTimeline(events) {
|
|
|
96
196
|
recordedHeight = Number(data.height ?? 0);
|
|
97
197
|
continue;
|
|
98
198
|
}
|
|
99
|
-
|
|
199
|
+
const href = String(data.href ?? "");
|
|
200
|
+
out.push({ t, type: "nav", href, label: href || TYPE_LABEL.nav });
|
|
201
|
+
continue;
|
|
202
|
+
}
|
|
203
|
+
if ((ev == null ? void 0 : ev.type) === EVENT_TYPE_FULL_SNAPSHOT) {
|
|
204
|
+
index.reset(data.node);
|
|
100
205
|
continue;
|
|
101
206
|
}
|
|
102
207
|
if ((ev == null ? void 0 : ev.type) === EVENT_TYPE_INCREMENTAL) {
|
|
103
208
|
const source = Number(data.source);
|
|
104
|
-
if (source ===
|
|
105
|
-
|
|
209
|
+
if (source === SOURCE_MUTATION$1) {
|
|
210
|
+
for (const add of data.adds ?? []) {
|
|
211
|
+
index.add(add == null ? void 0 : add.node, add == null ? void 0 : add.parentId);
|
|
212
|
+
}
|
|
213
|
+
} else if (source === SOURCE_MOUSE_INTERACTION && Number(data.type) === MOUSE_CLICK) {
|
|
214
|
+
const nodeId = Number(data.id);
|
|
215
|
+
out.push({ t, type: "click", nodeId, label: index.describe(nodeId, "click") });
|
|
106
216
|
} else if (source === SOURCE_INPUT) {
|
|
107
|
-
|
|
217
|
+
const nodeId = Number(data.id);
|
|
218
|
+
out.push({ t, type: "input", nodeId, label: index.describe(nodeId, "input") });
|
|
108
219
|
}
|
|
109
220
|
}
|
|
110
221
|
}
|
|
@@ -265,38 +376,6 @@ function stripWidgetEvents(events) {
|
|
|
265
376
|
}
|
|
266
377
|
return { events: out, stripped: widgetIds.size > 0, removedEvents };
|
|
267
378
|
}
|
|
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
379
|
const MOUSE_TAIL_CONFIG = {
|
|
301
380
|
lineCap: "round",
|
|
302
381
|
lineWidth: 3,
|
|
@@ -315,36 +394,14 @@ function formatTime(ms) {
|
|
|
315
394
|
const s = total % 60;
|
|
316
395
|
return `${m}:${s.toString().padStart(2, "0")}`;
|
|
317
396
|
}
|
|
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
397
|
const EventLog = memo(function EventLog2({
|
|
333
398
|
rows,
|
|
334
399
|
currentIndex,
|
|
335
400
|
filter,
|
|
336
401
|
onFilterChange,
|
|
337
402
|
onSeek,
|
|
338
|
-
total
|
|
339
|
-
mirror
|
|
403
|
+
total
|
|
340
404
|
}) {
|
|
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
405
|
const filtered = filter === "all" ? rows : rows.filter((r) => r.type === filter);
|
|
349
406
|
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
407
|
/* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between border-b border-line-subtle px-3 py-2.5", children: [
|
|
@@ -381,7 +438,7 @@ const EventLog = memo(function EventLog2({
|
|
|
381
438
|
children: [
|
|
382
439
|
/* @__PURE__ */ jsx("span", { className: "w-9 shrink-0 tabular-nums text-text-tertiary", children: formatTime(row.t) }),
|
|
383
440
|
/* @__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:
|
|
441
|
+
/* @__PURE__ */ jsx("span", { className: "min-w-0 flex-1 truncate text-text-secondary", children: row.label }),
|
|
385
442
|
/* @__PURE__ */ jsx("span", { className: "shrink-0 text-text-tertiary", children: TYPE_LABEL[row.type] })
|
|
386
443
|
]
|
|
387
444
|
},
|
|
@@ -400,7 +457,6 @@ function SessionReplayPlayer({ url }) {
|
|
|
400
457
|
const durationRef = useRef(0);
|
|
401
458
|
const isPlayingRef = useRef(false);
|
|
402
459
|
const showTrailRef = useRef(INITIAL_SHOW_TRAIL);
|
|
403
|
-
const mirrorRef = useRef(null);
|
|
404
460
|
const [state, setState] = useState("loading");
|
|
405
461
|
const [isPlaying, setIsPlaying] = useState(false);
|
|
406
462
|
const [showTrail, setShowTrail] = useState(INITIAL_SHOW_TRAIL);
|
|
@@ -438,7 +494,6 @@ function SessionReplayPlayer({ url }) {
|
|
|
438
494
|
applyIsPlaying(false);
|
|
439
495
|
setShowTrail(INITIAL_SHOW_TRAIL);
|
|
440
496
|
showTrailRef.current = INITIAL_SHOW_TRAIL;
|
|
441
|
-
mirrorRef.current = null;
|
|
442
497
|
setCurrentMs(0);
|
|
443
498
|
startedRef.current = false;
|
|
444
499
|
(async () => {
|
|
@@ -463,6 +518,7 @@ function SessionReplayPlayer({ url }) {
|
|
|
463
518
|
}
|
|
464
519
|
(_a = stageRef.current) == null ? void 0 : _a.remove();
|
|
465
520
|
const stage = document.createElement("div");
|
|
521
|
+
stage.style.pointerEvents = "none";
|
|
466
522
|
if (derived.recordedWidth > 0) {
|
|
467
523
|
const recW = derived.recordedWidth;
|
|
468
524
|
const recH = derived.recordedHeight || derived.recordedWidth;
|
|
@@ -494,15 +550,13 @@ function SessionReplayPlayer({ url }) {
|
|
|
494
550
|
stopRaf();
|
|
495
551
|
});
|
|
496
552
|
replayerRef.current = replayer;
|
|
497
|
-
mirrorRef.current = replayer.getMirror();
|
|
498
553
|
setTimeline(derived.events);
|
|
499
554
|
const totalTime = replayer.getMetaData().totalTime;
|
|
500
555
|
applyDuration(totalTime);
|
|
501
556
|
const rows = derived.events.map((ev, i) => ({
|
|
502
557
|
t: ev.t,
|
|
503
558
|
type: ev.type,
|
|
504
|
-
|
|
505
|
-
href: ev.href,
|
|
559
|
+
label: ev.label,
|
|
506
560
|
index: i
|
|
507
561
|
}));
|
|
508
562
|
setLogRows(rows);
|
|
@@ -596,10 +650,10 @@ function SessionReplayPlayer({ url }) {
|
|
|
596
650
|
);
|
|
597
651
|
const currentIndex = logRows.reduce((acc, row) => row.t <= currentMs ? row.index : acc, -1);
|
|
598
652
|
if (state === "error") {
|
|
599
|
-
return /* @__PURE__ */ jsx("p", { className: "text-sm text-text-secondary", children: "리플레이를 불러오지
|
|
653
|
+
return /* @__PURE__ */ jsx("p", { className: "text-sm text-text-secondary", children: "리플레이를 불러오지 못했습니다." });
|
|
600
654
|
}
|
|
601
655
|
if (state === "empty") {
|
|
602
|
-
return /* @__PURE__ */ jsx("p", { className: "text-sm text-text-secondary", children: "재생할 리플레이 데이터가
|
|
656
|
+
return /* @__PURE__ */ jsx("p", { className: "text-sm text-text-secondary", children: "재생할 리플레이 데이터가 없습니다." });
|
|
603
657
|
}
|
|
604
658
|
return /* @__PURE__ */ jsx(TooltipProvider, { children: /* @__PURE__ */ jsxs(
|
|
605
659
|
"div",
|
|
@@ -620,7 +674,11 @@ function SessionReplayPlayer({ url }) {
|
|
|
620
674
|
{
|
|
621
675
|
ref: hostRef,
|
|
622
676
|
"data-testid": "session-replay-host",
|
|
623
|
-
|
|
677
|
+
onClick: state === "ready" ? togglePlay : void 0,
|
|
678
|
+
className: cn(
|
|
679
|
+
"overflow-hidden rounded-t-md border border-line-subtle bg-surface-2",
|
|
680
|
+
state === "ready" && "cursor-pointer"
|
|
681
|
+
),
|
|
624
682
|
style: { position: "relative", maxHeight: "80vh" }
|
|
625
683
|
}
|
|
626
684
|
),
|
|
@@ -708,8 +766,7 @@ function SessionReplayPlayer({ url }) {
|
|
|
708
766
|
filter,
|
|
709
767
|
onFilterChange: setFilter,
|
|
710
768
|
onSeek: seek,
|
|
711
|
-
total: timeline.length
|
|
712
|
-
mirror: mirrorRef.current
|
|
769
|
+
total: timeline.length
|
|
713
770
|
}
|
|
714
771
|
) }) }) : null
|
|
715
772
|
]
|