@solhun/feedback-kit-web 0.1.0 → 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/dist/index.cjs +122 -2
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +26 -7
- package/dist/index.d.ts +26 -7
- package/dist/index.js +120 -2
- package/dist/index.js.map +1 -1
- package/package.json +4 -4
- package/src/element-info.ts +0 -138
- package/src/feedback-kit.test.ts +0 -227
- package/src/feedback-kit.tsx +0 -778
- package/src/index.ts +0 -97
- package/src/marker-store.ts +0 -153
- package/src/picking.test.ts +0 -249
- package/src/picking.ts +0 -370
- package/src/providers.ts +0 -43
- package/src/screen-map.test.ts +0 -455
- package/src/screenshot.test.ts +0 -47
- package/src/screenshot.ts +0 -90
- package/src/storage.test.ts +0 -65
- package/src/storage.ts +0 -69
- package/src/widget.ts +0 -70
package/dist/index.cjs
CHANGED
|
@@ -59,6 +59,8 @@ __export(index_exports, {
|
|
|
59
59
|
isOwnUi: () => isOwnUi2,
|
|
60
60
|
normalizePin: () => import_feedback_kit_core6.normalizePin,
|
|
61
61
|
parseSourceAttr: () => import_feedback_kit_core5.parseSourceAttr,
|
|
62
|
+
reactComponentPath: () => reactComponentPath,
|
|
63
|
+
reactComponentSummary: () => reactComponentSummary,
|
|
62
64
|
reencodeWebScreenshot: () => reencodeWebScreenshot,
|
|
63
65
|
resolveConfig: () => import_feedback_kit_core4.resolveConfig,
|
|
64
66
|
shouldShowWidget: () => import_feedback_kit_core4.shouldShowWidget,
|
|
@@ -71,6 +73,48 @@ var import_feedback_kit_core4 = require("@solhun/feedback-kit-core");
|
|
|
71
73
|
var import_feedback_kit_core5 = require("@solhun/feedback-kit-core");
|
|
72
74
|
var import_feedback_kit_core6 = require("@solhun/feedback-kit-core");
|
|
73
75
|
|
|
76
|
+
// src/react-tree.ts
|
|
77
|
+
function componentName(type) {
|
|
78
|
+
if (typeof type === "function") {
|
|
79
|
+
const fn = type;
|
|
80
|
+
return fn.displayName || fn.name || null;
|
|
81
|
+
}
|
|
82
|
+
if (type !== null && typeof type === "object") {
|
|
83
|
+
const wrapped = type;
|
|
84
|
+
if (wrapped.displayName) return wrapped.displayName;
|
|
85
|
+
if (wrapped.render) return componentName(wrapped.render);
|
|
86
|
+
if (wrapped.type) return componentName(wrapped.type);
|
|
87
|
+
}
|
|
88
|
+
return null;
|
|
89
|
+
}
|
|
90
|
+
function fiberOf(node) {
|
|
91
|
+
for (const key of Object.getOwnPropertyNames(node)) {
|
|
92
|
+
if (key.startsWith("__reactFiber$") || key.startsWith("__reactInternalInstance$")) {
|
|
93
|
+
const value = node[key];
|
|
94
|
+
if (value !== null && typeof value === "object") return value;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
return null;
|
|
98
|
+
}
|
|
99
|
+
function reactComponentPath(node, limit = 20) {
|
|
100
|
+
try {
|
|
101
|
+
let fiber = fiberOf(node);
|
|
102
|
+
const names = [];
|
|
103
|
+
while (fiber && names.length < limit) {
|
|
104
|
+
const name = componentName(fiber.elementType ?? fiber.type);
|
|
105
|
+
if (name && names[0] !== name) names.unshift(name);
|
|
106
|
+
fiber = fiber.return ?? null;
|
|
107
|
+
}
|
|
108
|
+
return names;
|
|
109
|
+
} catch {
|
|
110
|
+
return [];
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
function reactComponentSummary(node) {
|
|
114
|
+
const path = reactComponentPath(node);
|
|
115
|
+
return path.length > 0 ? path.join(" > ") : null;
|
|
116
|
+
}
|
|
117
|
+
|
|
74
118
|
// src/element-info.ts
|
|
75
119
|
var ELEMENT_TEXT_MAX_CHARS = 200;
|
|
76
120
|
var SELECTOR_MAX_DEPTH = 8;
|
|
@@ -160,13 +204,20 @@ function describeElement(el) {
|
|
|
160
204
|
} catch {
|
|
161
205
|
boundingBox = null;
|
|
162
206
|
}
|
|
207
|
+
const react = reactComponentSummary(el);
|
|
163
208
|
return {
|
|
164
209
|
tag: el.tagName.toLowerCase(),
|
|
165
210
|
id: el.getAttribute("id"),
|
|
166
211
|
className: el.getAttribute("class"),
|
|
167
212
|
text: visibleText(el),
|
|
168
213
|
boundingBox,
|
|
169
|
-
attributes
|
|
214
|
+
attributes: {
|
|
215
|
+
...attributes,
|
|
216
|
+
// 지목한 요소가 어느 컴포넌트에 속하는지. `div.flex.items-center` 만 오면
|
|
217
|
+
// 고치는 사람이 코드에서 다시 찾아야 하지만, `LoginForm > SubmitButton` 이
|
|
218
|
+
// 오면 바로 파일이 열린다. React 가 아니면 이 키 자체가 없다.
|
|
219
|
+
...react ? { "react-components": react } : {}
|
|
220
|
+
},
|
|
170
221
|
selector: cssSelectorPath(el)
|
|
171
222
|
};
|
|
172
223
|
}
|
|
@@ -291,6 +342,7 @@ var ElementPickingController = class {
|
|
|
291
342
|
this.queue = opts.queue;
|
|
292
343
|
this.createReport = opts.createReport;
|
|
293
344
|
this.store = opts.store ?? new MarkerStore();
|
|
345
|
+
this.onPick = opts.onPick ?? null;
|
|
294
346
|
this.getPathname = opts.getPathname ?? defaultPathname;
|
|
295
347
|
this.doc = opts.doc === void 0 ? defaultDocument() : opts.doc;
|
|
296
348
|
this.getViewport = opts.getViewport ?? defaultViewport;
|
|
@@ -404,6 +456,10 @@ var ElementPickingController = class {
|
|
|
404
456
|
/** 클릭 지점 기준으로 주석 팝업을 연다. 좌표는 해상도 무관한 상대값으로 접어 둔다. */
|
|
405
457
|
openAnnotation(element, clientPoint) {
|
|
406
458
|
const point = (0, import_feedback_kit_core.normalizePin)(clientPoint, this.getViewport());
|
|
459
|
+
if (this.onPick) {
|
|
460
|
+
this.onPick(describeElement(element), point);
|
|
461
|
+
return;
|
|
462
|
+
}
|
|
407
463
|
this.popup = {
|
|
408
464
|
element: describeElement(element),
|
|
409
465
|
point,
|
|
@@ -426,6 +482,26 @@ var ElementPickingController = class {
|
|
|
426
482
|
this.popup = null;
|
|
427
483
|
this.emit();
|
|
428
484
|
}
|
|
485
|
+
/**
|
|
486
|
+
* 제출된 제보를 마커로 남긴다. 모달 경로에서 쓴다 — 마커 생성이 팝업 저장 안에만
|
|
487
|
+
* 있으면, 리포트 모달로 보낸 지목 제보는 화면에 흔적이 남지 않는다("보냈는지 알 수
|
|
488
|
+
* 없다"가 이 모드가 없애려던 불편이다).
|
|
489
|
+
*/
|
|
490
|
+
noteSubmitted(report, delivered) {
|
|
491
|
+
if (!report.element && !report.pin) return;
|
|
492
|
+
const pathname = this.getPathname();
|
|
493
|
+
const marker = {
|
|
494
|
+
id: report.clientSubmissionId,
|
|
495
|
+
x: report.pin?.x ?? 0,
|
|
496
|
+
y: report.pin?.y ?? 0,
|
|
497
|
+
selector: report.element?.selector ?? null,
|
|
498
|
+
comment: report.comment,
|
|
499
|
+
status: delivered ? "done" : "pending",
|
|
500
|
+
at: report.createdAt
|
|
501
|
+
};
|
|
502
|
+
this.markers = this.store.upsert(pathname, marker);
|
|
503
|
+
this.emit();
|
|
504
|
+
}
|
|
429
505
|
/**
|
|
430
506
|
* 주석 저장. 마커를 먼저 "전송 중"으로 찍고 나서 보낸다 — 사용자는 결과를 기다리지 않고
|
|
431
507
|
* 다음 요소로 넘어갈 수 있어야 한다.
|
|
@@ -523,6 +599,30 @@ function payload(dataUrl) {
|
|
|
523
599
|
function isOwnUi2(node) {
|
|
524
600
|
return node.hasAttribute?.(OWN_UI_ATTR) === true || node.closest?.(`[${OWN_UI_ATTR}]`) !== null;
|
|
525
601
|
}
|
|
602
|
+
function freezeCanvases(source, clone) {
|
|
603
|
+
const originals = source.querySelectorAll("canvas");
|
|
604
|
+
const clones = clone.querySelectorAll("canvas");
|
|
605
|
+
for (let index = 0; index < originals.length && index < clones.length; index++) {
|
|
606
|
+
const from = originals[index];
|
|
607
|
+
const to = clones[index];
|
|
608
|
+
let dataUrl;
|
|
609
|
+
try {
|
|
610
|
+
dataUrl = from.toDataURL("image/png");
|
|
611
|
+
} catch {
|
|
612
|
+
continue;
|
|
613
|
+
}
|
|
614
|
+
if (!dataUrl.startsWith("data:image") || dataUrl.length < 64) continue;
|
|
615
|
+
const box = from.getBoundingClientRect();
|
|
616
|
+
if (box.width < 1 || box.height < 1) continue;
|
|
617
|
+
const image = clone.createElement("img");
|
|
618
|
+
image.src = dataUrl;
|
|
619
|
+
image.setAttribute("style", to.getAttribute("style") ?? "");
|
|
620
|
+
image.style.width = `${box.width}px`;
|
|
621
|
+
image.style.height = `${box.height}px`;
|
|
622
|
+
image.className = to.className;
|
|
623
|
+
to.replaceWith(image);
|
|
624
|
+
}
|
|
625
|
+
}
|
|
526
626
|
var captureWebScreenshot = async () => {
|
|
527
627
|
const doc = globalThis.document;
|
|
528
628
|
const view = doc?.defaultView;
|
|
@@ -540,6 +640,8 @@ var captureWebScreenshot = async () => {
|
|
|
540
640
|
scale: Math.min(2, Math.max(1, view.devicePixelRatio || 1)),
|
|
541
641
|
// 위젯 자신의 UI 는 결과에서 뺀다.
|
|
542
642
|
ignoreElements: isOwnUi2,
|
|
643
|
+
// 캔버스(Rive·차트 등)는 이미지로 바꿔 넘긴다 — freezeCanvases 주석 참고.
|
|
644
|
+
onclone: (cloned) => freezeCanvases(doc, cloned),
|
|
543
645
|
// 외부 이미지가 CORS 를 안 열어두면 그것 때문에 전체가 실패할 수 있다.
|
|
544
646
|
// 못 가져오는 리소스는 건너뛰고 나머지를 그린다.
|
|
545
647
|
useCORS: true,
|
|
@@ -580,13 +682,21 @@ var reencodeWebScreenshot = async (shot, quality) => {
|
|
|
580
682
|
var import_feedback_kit_core2 = require("@solhun/feedback-kit-core");
|
|
581
683
|
function createWebWidget(opts) {
|
|
582
684
|
const store = opts.store ?? new MarkerStore();
|
|
685
|
+
let widgetRef = null;
|
|
583
686
|
const picking = new ElementPickingController({
|
|
584
687
|
queue: opts.queue,
|
|
585
688
|
createReport: opts.createReport,
|
|
586
689
|
store,
|
|
587
690
|
getPathname: opts.getPathname,
|
|
588
691
|
doc: opts.doc,
|
|
589
|
-
getViewport: opts.getViewport
|
|
692
|
+
getViewport: opts.getViewport,
|
|
693
|
+
// 요소를 고르면 인라인 팝업이 아니라 **리포트 모달**을 연다.
|
|
694
|
+
// 코멘트만 받던 팝업과 달리 스크린샷·우선순위까지 같은 화면에서 받는다.
|
|
695
|
+
// 모드는 켜진 채로 둬서 한 번 켜고 여러 요소를 연달아 지목할 수 있다.
|
|
696
|
+
onPick: (element, point) => {
|
|
697
|
+
void widgetRef?.openReport({ element });
|
|
698
|
+
widgetRef?.modal.setPin(point);
|
|
699
|
+
}
|
|
590
700
|
});
|
|
591
701
|
const widget = new import_feedback_kit_core2.WidgetController({
|
|
592
702
|
...opts,
|
|
@@ -600,6 +710,14 @@ function createWebWidget(opts) {
|
|
|
600
710
|
else picking.stop();
|
|
601
711
|
}
|
|
602
712
|
});
|
|
713
|
+
widgetRef = widget;
|
|
714
|
+
const originalSubmit = widget.submitReport.bind(widget);
|
|
715
|
+
widget.submitReport = async () => {
|
|
716
|
+
const outcome = await originalSubmit();
|
|
717
|
+
const report = widget.modal.getLastReport();
|
|
718
|
+
if (report?.element) picking.noteSubmitted(report, outcome?.delivered === true);
|
|
719
|
+
return outcome;
|
|
720
|
+
};
|
|
603
721
|
picking.restore();
|
|
604
722
|
return {
|
|
605
723
|
widget,
|
|
@@ -1404,6 +1522,8 @@ function webContextProviders() {
|
|
|
1404
1522
|
isOwnUi,
|
|
1405
1523
|
normalizePin,
|
|
1406
1524
|
parseSourceAttr,
|
|
1525
|
+
reactComponentPath,
|
|
1526
|
+
reactComponentSummary,
|
|
1407
1527
|
reencodeWebScreenshot,
|
|
1408
1528
|
resolveConfig,
|
|
1409
1529
|
shouldShowWidget,
|