@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 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,