@solhun/feedback-kit-web 0.7.0 → 0.9.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
@@ -30,52 +30,56 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
30
30
  // src/index.ts
31
31
  var index_exports = {};
32
32
  __export(index_exports, {
33
- COMMENT_MAX_CHARS: () => import_feedback_kit_core6.COMMENT_MAX_CHARS,
34
- COMMENT_REQUIRED_MESSAGE: () => import_feedback_kit_core6.COMMENT_REQUIRED_MESSAGE,
35
- COMMENT_TOO_LONG_MESSAGE: () => import_feedback_kit_core6.COMMENT_TOO_LONG_MESSAGE,
33
+ COMMENT_MAX_CHARS: () => import_feedback_kit_core7.COMMENT_MAX_CHARS,
34
+ COMMENT_REQUIRED_MESSAGE: () => import_feedback_kit_core7.COMMENT_REQUIRED_MESSAGE,
35
+ COMMENT_TOO_LONG_MESSAGE: () => import_feedback_kit_core7.COMMENT_TOO_LONG_MESSAGE,
36
36
  ELEMENT_TEXT_MAX_CHARS: () => ELEMENT_TEXT_MAX_CHARS,
37
37
  ElementPickingController: () => ElementPickingController,
38
- FLOATING_BUTTON_ID: () => import_feedback_kit_core6.FLOATING_BUTTON_ID,
38
+ FLOATING_BUTTON_ID: () => import_feedback_kit_core7.FLOATING_BUTTON_ID,
39
39
  FeedbackKit: () => FeedbackKit,
40
- HintProvider: () => import_feedback_kit_core7.HintProvider,
40
+ HintProvider: () => import_feedback_kit_core8.HintProvider,
41
41
  MARKER_KEY_PREFIX: () => MARKER_KEY_PREFIX,
42
42
  MAX_MARKERS_PER_PATH: () => MAX_MARKERS_PER_PATH,
43
- MODAL_ACTION_HINT_TOGGLE: () => import_feedback_kit_core6.MODAL_ACTION_HINT_TOGGLE,
44
- MODAL_ACTION_PICK: () => import_feedback_kit_core6.MODAL_ACTION_PICK,
43
+ MODAL_ACTION_HINT_TOGGLE: () => import_feedback_kit_core7.MODAL_ACTION_HINT_TOGGLE,
44
+ MODAL_ACTION_PICK: () => import_feedback_kit_core7.MODAL_ACTION_PICK,
45
45
  MarkerStore: () => MarkerStore,
46
46
  OWN_UI_ATTR: () => OWN_UI_ATTR,
47
47
  PICKING_MODE_KEY: () => PICKING_MODE_KEY,
48
- ReportModalController: () => import_feedback_kit_core6.ReportModalController,
49
- SCREENSHOT_FAILED_MESSAGE: () => import_feedback_kit_core6.SCREENSHOT_FAILED_MESSAGE,
48
+ ReportModalController: () => import_feedback_kit_core7.ReportModalController,
49
+ SCREENSHOT_FAILED_MESSAGE: () => import_feedback_kit_core7.SCREENSHOT_FAILED_MESSAGE,
50
50
  SELECTOR_MAX_DEPTH: () => SELECTOR_MAX_DEPTH,
51
- SOURCE_ATTR: () => import_feedback_kit_core5.SOURCE_ATTR,
52
- SUBMIT_DONE_MESSAGE: () => import_feedback_kit_core6.SUBMIT_DONE_MESSAGE,
53
- SUBMIT_PENDING_MESSAGE: () => import_feedback_kit_core6.SUBMIT_PENDING_MESSAGE,
54
- WidgetController: () => import_feedback_kit_core6.WidgetController,
51
+ SOURCE_ATTR: () => import_feedback_kit_core6.SOURCE_ATTR,
52
+ SUBMIT_DONE_MESSAGE: () => import_feedback_kit_core7.SUBMIT_DONE_MESSAGE,
53
+ SUBMIT_PENDING_MESSAGE: () => import_feedback_kit_core7.SUBMIT_PENDING_MESSAGE,
54
+ WidgetController: () => import_feedback_kit_core7.WidgetController,
55
55
  captureWebScreenshot: () => captureWebScreenshot,
56
+ createRouteWatcher: () => createRouteWatcher,
56
57
  createWebStorage: () => createWebStorage,
57
58
  createWebWidget: () => createWebWidget,
58
59
  cssSelectorPath: () => cssSelectorPath,
59
- denormalizePin: () => import_feedback_kit_core6.denormalizePin,
60
+ denormalizePin: () => import_feedback_kit_core7.denormalizePin,
60
61
  describeElement: () => describeElement,
61
62
  isOwnUi: () => isOwnUi2,
62
- normalizePin: () => import_feedback_kit_core6.normalizePin,
63
- parseSourceAttr: () => import_feedback_kit_core5.parseSourceAttr,
64
- rankHints: () => import_feedback_kit_core7.rankHints,
63
+ normalizePin: () => import_feedback_kit_core7.normalizePin,
64
+ parseSourceAttr: () => import_feedback_kit_core6.parseSourceAttr,
65
+ rankHints: () => import_feedback_kit_core8.rankHints,
65
66
  reactComponentPath: () => reactComponentPath,
66
67
  reactComponentSummary: () => reactComponentSummary,
67
68
  reencodeWebScreenshot: () => reencodeWebScreenshot,
68
- resolveConfig: () => import_feedback_kit_core4.resolveConfig,
69
- shouldShowWidget: () => import_feedback_kit_core4.shouldShowWidget,
70
- sourceFromElement: () => import_feedback_kit_core5.sourceFromElement,
69
+ resolveConfig: () => import_feedback_kit_core5.resolveConfig,
70
+ shouldShowWidget: () => import_feedback_kit_core5.shouldShowWidget,
71
+ sourceFromElement: () => import_feedback_kit_core6.sourceFromElement,
71
72
  visibleText: () => visibleText,
72
- webContextProviders: () => webContextProviders
73
+ webContextProviders: () => webContextProviders,
74
+ webDiagnosticsOptions: () => webDiagnosticsOptions,
75
+ webRouteStorage: () => webRouteStorage,
76
+ webRouteWatcher: () => webRouteWatcher
73
77
  });
74
78
  module.exports = __toCommonJS(index_exports);
75
- var import_feedback_kit_core4 = require("@solhun/feedback-kit-core");
76
79
  var import_feedback_kit_core5 = require("@solhun/feedback-kit-core");
77
80
  var import_feedback_kit_core6 = require("@solhun/feedback-kit-core");
78
81
  var import_feedback_kit_core7 = require("@solhun/feedback-kit-core");
82
+ var import_feedback_kit_core8 = require("@solhun/feedback-kit-core");
79
83
 
80
84
  // src/react-tree.ts
81
85
  var FRAMEWORK_INTERNALS = /* @__PURE__ */ new Set([
@@ -162,6 +166,7 @@ function reactComponentSummary(node) {
162
166
  // src/element-info.ts
163
167
  var ELEMENT_TEXT_MAX_CHARS = 200;
164
168
  var SELECTOR_MAX_DEPTH = 8;
169
+ var PICK_TARGET_SNAP_PX = 24;
165
170
  var KEPT_ATTRS = [
166
171
  "id",
167
172
  "class",
@@ -234,6 +239,55 @@ function visibleText(el) {
234
239
  if (collapsed === "") return null;
235
240
  return collapsed.length > ELEMENT_TEXT_MAX_CHARS ? collapsed.slice(0, ELEMENT_TEXT_MAX_CHARS) : collapsed;
236
241
  }
242
+ function elementRect(element) {
243
+ try {
244
+ const rect = element.getBoundingClientRect();
245
+ const left = rect.left;
246
+ const top = rect.top;
247
+ const width = rect.width;
248
+ const height = rect.height;
249
+ if (![left, top, width, height].every(Number.isFinite) || width <= 0 || height <= 0) {
250
+ return null;
251
+ }
252
+ return {
253
+ element,
254
+ left,
255
+ top,
256
+ right: left + width,
257
+ bottom: top + height,
258
+ width,
259
+ height
260
+ };
261
+ } catch {
262
+ return null;
263
+ }
264
+ }
265
+ function distanceFromPoint(rect, point) {
266
+ const dx = Math.max(rect.left - point.x, 0, point.x - rect.right);
267
+ const dy = Math.max(rect.top - point.y, 0, point.y - rect.bottom);
268
+ return Math.hypot(dx, dy);
269
+ }
270
+ function resolvePickTarget(target, point) {
271
+ if (!target || typeof target.tagName !== "string") return null;
272
+ if (!Number.isFinite(point.x) || !Number.isFinite(point.y)) return target;
273
+ let current = target;
274
+ while (true) {
275
+ const currentRect = elementRect(current);
276
+ const children = Array.from(current.children).map(elementRect).filter((rect) => rect !== null).map((rect) => ({ rect, distance: distanceFromPoint(rect, point) }));
277
+ if (children.length === 0) return current;
278
+ const containing = children.filter(({ distance }) => distance === 0).sort((a, b) => a.rect.width * a.rect.height - b.rect.width * b.rect.height);
279
+ if (containing[0]) {
280
+ current = containing[0].rect.element;
281
+ continue;
282
+ }
283
+ const nearest = children.sort((a, b) => a.distance - b.distance)[0];
284
+ const isMorePrecise = currentRect ? nearest.rect.width * nearest.rect.height < currentRect.width * currentRect.height : true;
285
+ if (nearest.distance <= PICK_TARGET_SNAP_PX && isMorePrecise) {
286
+ return nearest.rect.element;
287
+ }
288
+ return current;
289
+ }
290
+ }
237
291
  function describeElement(el) {
238
292
  if (!el || typeof el.tagName !== "string") return null;
239
293
  const attributes = {};
@@ -354,6 +408,84 @@ var MarkerStore = class {
354
408
 
355
409
  // src/picking.ts
356
410
  var import_feedback_kit_core = require("@solhun/feedback-kit-core");
411
+
412
+ // src/route-watcher.ts
413
+ function defaultTarget() {
414
+ const g = globalThis;
415
+ return g.location ? g : null;
416
+ }
417
+ function createRouteWatcher(target = defaultTarget()) {
418
+ const listeners = /* @__PURE__ */ new Set();
419
+ let detach = null;
420
+ const currentPath = () => {
421
+ const p = target?.location?.pathname;
422
+ return typeof p === "string" && p.length > 0 ? p : "/";
423
+ };
424
+ const emit = () => {
425
+ const path = currentPath();
426
+ for (const listener of [...listeners]) {
427
+ try {
428
+ listener(path);
429
+ } catch {
430
+ }
431
+ }
432
+ };
433
+ const attach = () => {
434
+ if (detach !== null || !target) return;
435
+ const onPop = () => emit();
436
+ target.addEventListener?.("popstate", onPop);
437
+ const history = target.history;
438
+ const originalPush = typeof history?.pushState === "function" ? history.pushState : null;
439
+ const originalReplace = typeof history?.replaceState === "function" ? history.replaceState : null;
440
+ if (history && originalPush) {
441
+ history.pushState = function patched(...args) {
442
+ const out = originalPush.apply(this, args);
443
+ emit();
444
+ return out;
445
+ };
446
+ }
447
+ if (history && originalReplace) {
448
+ history.replaceState = function patched(...args) {
449
+ const out = originalReplace.apply(this, args);
450
+ emit();
451
+ return out;
452
+ };
453
+ }
454
+ detach = () => {
455
+ target.removeEventListener?.("popstate", onPop);
456
+ if (history && originalPush) history.pushState = originalPush;
457
+ if (history && originalReplace) history.replaceState = originalReplace;
458
+ };
459
+ };
460
+ return (onPath) => {
461
+ listeners.add(onPath);
462
+ if (listeners.size === 1) attach();
463
+ try {
464
+ onPath(currentPath());
465
+ } catch {
466
+ }
467
+ return () => {
468
+ listeners.delete(onPath);
469
+ if (listeners.size === 0 && detach) {
470
+ detach();
471
+ detach = null;
472
+ }
473
+ };
474
+ };
475
+ }
476
+ var webRouteWatcher = createRouteWatcher();
477
+ function webRouteStorage() {
478
+ try {
479
+ const s = globalThis.sessionStorage;
480
+ if (!s || typeof s.getItem !== "function" || typeof s.setItem !== "function") return null;
481
+ s.getItem("feedback-kit:probe");
482
+ return s;
483
+ } catch {
484
+ return null;
485
+ }
486
+ }
487
+
488
+ // src/picking.ts
357
489
  var OWN_UI_ATTR = "data-feedback-kit";
358
490
  function defaultDocument() {
359
491
  return globalThis.document ?? null;
@@ -382,10 +514,13 @@ var ElementPickingController = class {
382
514
  */
383
515
  this.paused = false;
384
516
  this.attached = false;
517
+ /** 같은 요소 위 mousemove 마다 React 경로·선택자를 다시 만들지 않기 위한 캐시. */
518
+ this.hoveredTarget = null;
385
519
  this.hovered = null;
386
520
  this.popup = null;
387
521
  this.markers = [];
388
- this.pathWatch = null;
522
+ /** 경로 감시 해제. 감시 중이 아니면 null. */
523
+ this.unwatchRoutes = null;
389
524
  this.saving = false;
390
525
  /** 늦게 끝난 캡처가 다음 주석의 그림을 덮지 못하게 하는 세대 번호. */
391
526
  this.shotGeneration = 0;
@@ -398,6 +533,7 @@ var ElementPickingController = class {
398
533
  this.userToggledHints = false;
399
534
  this.onClick = (event) => this.handleClick(event);
400
535
  this.onMouseOver = (event) => this.handleMouseOver(event);
536
+ this.onMouseMove = (event) => this.handleMouseOver(event);
401
537
  this.queue = opts.queue;
402
538
  this.createReport = opts.createReport;
403
539
  this.store = opts.store ?? new MarkerStore();
@@ -409,6 +545,7 @@ var ElementPickingController = class {
409
545
  this.reencode = opts.reencode ?? null;
410
546
  this.screenshotLimitBytes = opts.screenshotLimitBytes;
411
547
  this.rankHintsFor = opts.rankHintsFor ?? null;
548
+ this.watchRoutes = opts.watchRoutes === void 0 ? webRouteWatcher : opts.watchRoutes;
412
549
  this.lastPathname = this.getPathname();
413
550
  this.markers = this.store.list(this.lastPathname);
414
551
  this.unsubscribeQueue = this.queue.subscribe?.(() => {
@@ -457,6 +594,7 @@ var ElementPickingController = class {
457
594
  this.store.setPickingActive(false);
458
595
  this.detach();
459
596
  this.stopPathWatch();
597
+ this.hoveredTarget = null;
460
598
  this.hovered = null;
461
599
  this.popup = null;
462
600
  this.emit();
@@ -472,6 +610,7 @@ var ElementPickingController = class {
472
610
  if (!this.active || this.paused) return;
473
611
  this.paused = true;
474
612
  this.detach();
613
+ this.hoveredTarget = null;
475
614
  this.hovered = null;
476
615
  this.emit();
477
616
  }
@@ -499,6 +638,22 @@ var ElementPickingController = class {
499
638
  this.start();
500
639
  return true;
501
640
  }
641
+ /**
642
+ * 이 화면에 쌓인 마커 표시를 지운다.
643
+ *
644
+ * 지워지는 건 **화면 표시뿐**이다. 제보는 이미 큐를 거쳐 수집처로 갔으므로 없어지지 않는다.
645
+ * 마커는 `localStorage` 에 남아 새로고침에도 살아남는데(그게 원래 목적이다) 정작 치울
646
+ * 수단이 없어서, 한 번 보낸 [완료] 배지가 그 화면을 볼 때마다 계속 따라다녔다.
647
+ *
648
+ * 화면에 보이는 목록의 기준은 `lastPathname` 이다(`reconcileMarkerOutcomes` 와 같다).
649
+ * 이동 직후 아직 `syncPath` 가 안 돈 순간에도 "지금 눈에 보이는 것"이 지워져야 한다.
650
+ */
651
+ clearMarkers() {
652
+ if (this.markers.length === 0) return;
653
+ this.store.clear(this.lastPathname);
654
+ this.markers = [];
655
+ this.emit();
656
+ }
502
657
  /** 경로가 바뀌었을 때 그 경로의 마커로 갈아 끼운다. */
503
658
  syncPath() {
504
659
  if (this.paused) this.resume();
@@ -506,6 +661,7 @@ var ElementPickingController = class {
506
661
  this.markers = this.store.list(this.lastPathname);
507
662
  this.reconcileMarkerOutcomes();
508
663
  this.popup = null;
664
+ this.hoveredTarget = null;
509
665
  this.hovered = null;
510
666
  this.emit();
511
667
  return this.markers;
@@ -521,17 +677,26 @@ var ElementPickingController = class {
521
677
  if (this.attached || !this.doc) return;
522
678
  this.doc.addEventListener("click", this.onClick, true);
523
679
  this.doc.addEventListener("mouseover", this.onMouseOver, true);
680
+ this.doc.addEventListener("mousemove", this.onMouseMove, true);
524
681
  this.attached = true;
525
682
  }
526
683
  detach() {
527
684
  if (!this.attached || !this.doc) return;
528
685
  this.doc.removeEventListener("click", this.onClick, true);
529
686
  this.doc.removeEventListener("mouseover", this.onMouseOver, true);
687
+ this.doc.removeEventListener("mousemove", this.onMouseMove, true);
530
688
  this.attached = false;
531
689
  }
532
690
  handleMouseOver(event) {
533
691
  if (!this.active || this.paused || isOwnUi(event.target)) return;
534
- this.hovered = describeElement(event.target);
692
+ const mouse = event;
693
+ const target = resolvePickTarget(event.target, {
694
+ x: mouse.clientX ?? 0,
695
+ y: mouse.clientY ?? 0
696
+ });
697
+ if (target === this.hoveredTarget) return;
698
+ this.hoveredTarget = target;
699
+ this.hovered = describeElement(target);
535
700
  this.emit();
536
701
  }
537
702
  handleClick(event) {
@@ -552,11 +717,12 @@ var ElementPickingController = class {
552
717
  /** 클릭 지점 기준으로 주석 팝업을 연다. 좌표는 해상도 무관한 상대값으로 접어 둔다. */
553
718
  openAnnotation(element, clientPoint) {
554
719
  const point = (0, import_feedback_kit_core.normalizePin)(clientPoint, this.getViewport());
720
+ const target = resolvePickTarget(element, clientPoint);
555
721
  if (this.onPick) {
556
- this.onPick(describeElement(element), point);
722
+ this.onPick(describeElement(target), point);
557
723
  return;
558
724
  }
559
- const info = describeElement(element);
725
+ const info = describeElement(target);
560
726
  this.userToggledHints = false;
561
727
  const hints = this.rankHintsFor ? this.rankHintsFor(info) : [];
562
728
  this.popup = {
@@ -771,17 +937,17 @@ ${hint.draft}`;
771
937
  return outcome;
772
938
  }
773
939
  startPathWatch() {
774
- if (this.pathWatch !== null || typeof setInterval !== "function") return;
940
+ if (this.unwatchRoutes !== null || !this.watchRoutes) return;
775
941
  this.lastPathname = this.getPathname();
776
- this.pathWatch = setInterval(() => {
942
+ this.unwatchRoutes = this.watchRoutes(() => {
777
943
  const pathname = this.getPathname();
778
944
  if (pathname !== this.lastPathname) this.syncPath();
779
- }, 200);
945
+ });
780
946
  }
781
947
  stopPathWatch() {
782
- if (this.pathWatch === null) return;
783
- clearInterval(this.pathWatch);
784
- this.pathWatch = null;
948
+ if (this.unwatchRoutes === null) return;
949
+ this.unwatchRoutes();
950
+ this.unwatchRoutes = null;
785
951
  }
786
952
  /** 전역 pending 수가 아니라 마커와 같은 clientSubmissionId의 확정 성공만 완료로 바꾼다. */
787
953
  reconcileMarkerOutcomes() {
@@ -1044,11 +1210,57 @@ function createWebWidget(opts) {
1044
1210
  }
1045
1211
 
1046
1212
  // src/feedback-kit.tsx
1047
- var import_feedback_kit_core3 = require("@solhun/feedback-kit-core");
1213
+ var import_feedback_kit_core4 = require("@solhun/feedback-kit-core");
1048
1214
  var import_react = require("react");
1049
1215
 
1050
1216
  // src/version.ts
1051
- var VERSION = true ? "0.7.0" : "dev";
1217
+ var VERSION = true ? "0.9.0" : "dev";
1218
+
1219
+ // src/dev-clipboard.ts
1220
+ var import_feedback_kit_core3 = require("@solhun/feedback-kit-core");
1221
+ function sourceLine(item) {
1222
+ const mapping = (0, import_feedback_kit_core3.sourceFromElement)(item.element ?? void 0);
1223
+ if (!mapping.sourceFile) return null;
1224
+ return mapping.sourceLine === null ? `\uC18C\uC2A4: ${mapping.sourceFile}` : `\uC18C\uC2A4: ${mapping.sourceFile}:${mapping.sourceLine}`;
1225
+ }
1226
+ function elementLine(element) {
1227
+ if (!element) return null;
1228
+ const selector = element.selector ?? element.tag;
1229
+ const text = element.text?.trim();
1230
+ return text ? `\uC694\uC18C: ${selector} "${text}"` : `\uC694\uC18C: ${selector}`;
1231
+ }
1232
+ function componentLine(element) {
1233
+ const summary = element?.attributes?.["react-components"];
1234
+ return summary ? `\uCEF4\uD3EC\uB10C\uD2B8: ${summary}` : null;
1235
+ }
1236
+ function renderDevClipItem(item) {
1237
+ const lines = [`[\uD53C\uB4DC\uBC31] ${item.comment.trim()}`];
1238
+ const screen = item.screen?.trim();
1239
+ if (screen) lines.push(`\uD654\uBA74: ${screen}`);
1240
+ const element = elementLine(item.element);
1241
+ if (element) lines.push(element);
1242
+ const component = componentLine(item.element);
1243
+ if (component) lines.push(component);
1244
+ const source = sourceLine(item);
1245
+ if (source) lines.push(source);
1246
+ if (item.priority && item.priority !== "unset") lines.push(`\uC6B0\uC120\uC21C\uC704: ${item.priority}`);
1247
+ return lines.join("\n");
1248
+ }
1249
+ function renderDevClipList(items) {
1250
+ if (items.length === 0) return "";
1251
+ if (items.length === 1) return renderDevClipItem(items[0]);
1252
+ return items.map((item, index) => `${index + 1}. ${renderDevClipItem(item).split("\n").join("\n ")}`).join("\n\n");
1253
+ }
1254
+ async function writeToClipboard(text) {
1255
+ const clipboard = globalThis.navigator?.clipboard;
1256
+ if (!clipboard?.writeText) return false;
1257
+ try {
1258
+ await clipboard.writeText(text);
1259
+ return true;
1260
+ } catch {
1261
+ return false;
1262
+ }
1263
+ }
1052
1264
 
1053
1265
  // src/feedback-kit.tsx
1054
1266
  var import_jsx_runtime = require("react/jsx-runtime");
@@ -1314,8 +1526,13 @@ function PickToggle({
1314
1526
  }
1315
1527
  function PickBar({
1316
1528
  paused,
1529
+ markerCount,
1317
1530
  onToggle,
1318
- onTogglePause
1531
+ onTogglePause,
1532
+ onClearMarkers,
1533
+ onReportWholeScreen,
1534
+ devClipCount,
1535
+ onCopyAll
1319
1536
  }) {
1320
1537
  return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
1321
1538
  "div",
@@ -1323,10 +1540,20 @@ function PickBar({
1323
1540
  style: {
1324
1541
  position: "fixed",
1325
1542
  top: 16,
1326
- left: "50%",
1327
- transform: "translateX(-50%)",
1543
+ // `left:50% + translateX(-50%)` 로 가운데를 잡으면 **쓸 수 있는 폭이 화면의 절반**이
1544
+ // 된다(transform 은 레이아웃 계산에 안 들어간다). 버튼이 셋이 되면서 그 절반에
1545
+ // 안 맞아 좁은 화면에서 버튼이 한 줄에 하나씩 세로로 쌓였다(390px 에서 3줄).
1546
+ // 양끝을 물리고 auto 마진으로 가운데를 잡으면 화면 전체 폭을 쓴다 — 같은 화면이 2줄.
1547
+ left: 0,
1548
+ right: 0,
1549
+ marginInline: "auto",
1550
+ width: "fit-content",
1551
+ maxWidth: "calc(100% - 32px)",
1328
1552
  display: "flex",
1329
1553
  alignItems: "stretch",
1554
+ justifyContent: "center",
1555
+ // 셋이 한 줄에 안 들어가는 폭에서는 줄을 바꾼다 — 안 그러면 화면 밖으로 나간다.
1556
+ flexWrap: "wrap",
1330
1557
  gap: 8,
1331
1558
  // 오버레이 자체는 클릭을 통과시킨다(pointerEvents:none) — 이 막대만 되살린다.
1332
1559
  pointerEvents: "auto",
@@ -1359,7 +1586,72 @@ function PickBar({
1359
1586
  paused ? "\uC9C0\uBAA9 \uC7AC\uAC1C" : "\uC7A0\uC2DC \uBA48\uCDA4"
1360
1587
  ]
1361
1588
  }
1362
- )
1589
+ ),
1590
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
1591
+ "button",
1592
+ {
1593
+ type: "button",
1594
+ "data-fk-pick-whole": "",
1595
+ title: "\uAC00\uB9AC\uD0A8 \uC694\uC18C \uC5C6\uC774 \uC774 \uD654\uBA74 \uC804\uCCB4\uC5D0 \uB300\uD574 \uC81C\uBCF4\uD569\uB2C8\uB2E4.",
1596
+ onClick: onReportWholeScreen,
1597
+ style: {
1598
+ ...baseButton,
1599
+ display: "inline-flex",
1600
+ alignItems: "center",
1601
+ gap: 6,
1602
+ whiteSpace: "nowrap"
1603
+ },
1604
+ children: [
1605
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { "aria-hidden": "true", children: "\u25A3" }),
1606
+ "\uD654\uBA74 \uC804\uCCB4\uB85C \uBCF4\uB0B4\uAE30"
1607
+ ]
1608
+ }
1609
+ ),
1610
+ devClipCount > 0 ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
1611
+ "button",
1612
+ {
1613
+ type: "button",
1614
+ "data-fk-copy-all": devClipCount,
1615
+ title: "\uC774\uBC88 \uC138\uC158\uC5D0 \uCC0D\uC740 \uAC83\uC744 \uBC88\uD638 \uB9E4\uAE34 \uBAA9\uB85D \uD558\uB098\uB85C \uBCF5\uC0AC\uD569\uB2C8\uB2E4.",
1616
+ onClick: onCopyAll,
1617
+ style: {
1618
+ ...baseButton,
1619
+ display: "inline-flex",
1620
+ alignItems: "center",
1621
+ gap: 6,
1622
+ whiteSpace: "nowrap",
1623
+ fontWeight: 600
1624
+ },
1625
+ children: [
1626
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { "aria-hidden": "true", children: "\u29C9" }),
1627
+ "\uC804\uBD80 \uBCF5\uC0AC ",
1628
+ devClipCount
1629
+ ]
1630
+ }
1631
+ ) : null,
1632
+ markerCount > 0 ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
1633
+ "button",
1634
+ {
1635
+ type: "button",
1636
+ "data-fk-pick-clear": markerCount,
1637
+ title: "\uD654\uBA74\uC758 \uD45C\uC2DC\uB9CC \uC9C0\uC6C1\uB2C8\uB2E4. \uBCF4\uB0B8 \uC81C\uBCF4\uB294 \uADF8\uB300\uB85C \uB0A8\uC2B5\uB2C8\uB2E4.",
1638
+ "aria-label": `\uC774 \uD654\uBA74\uC758 \uC8FC\uC11D \uD45C\uC2DC ${markerCount}\uAC1C \uC9C0\uC6B0\uAE30 (\uBCF4\uB0B8 \uC81C\uBCF4\uB294 \uADF8\uB300\uB85C \uB0A8\uC2B5\uB2C8\uB2E4)`,
1639
+ onClick: onClearMarkers,
1640
+ style: {
1641
+ ...baseButton,
1642
+ display: "inline-flex",
1643
+ alignItems: "center",
1644
+ gap: 6,
1645
+ whiteSpace: "nowrap",
1646
+ color: TOKENS.muted
1647
+ },
1648
+ children: [
1649
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { "aria-hidden": "true", children: "\u2715" }),
1650
+ "\uD45C\uC2DC \uC9C0\uC6B0\uAE30 ",
1651
+ markerCount
1652
+ ]
1653
+ }
1654
+ ) : null
1363
1655
  ]
1364
1656
  }
1365
1657
  );
@@ -1391,7 +1683,7 @@ function HintChips({
1391
1683
  "button",
1392
1684
  {
1393
1685
  type: "button",
1394
- "data-fk-focus-id": import_feedback_kit_core3.MODAL_ACTION_HINT_TOGGLE,
1686
+ "data-fk-focus-id": import_feedback_kit_core4.MODAL_ACTION_HINT_TOGGLE,
1395
1687
  "aria-expanded": hintsExpanded,
1396
1688
  disabled,
1397
1689
  onClick: onToggle,
@@ -1465,6 +1757,10 @@ function FeedbackKit(props) {
1465
1757
  const [widgetState, setWidgetState] = (0, import_react.useState)(null);
1466
1758
  const [pickingState, setPickingState] = (0, import_react.useState)(INITIAL_PICKING_STATE);
1467
1759
  const [announcement, setAnnouncement] = (0, import_react.useState)(null);
1760
+ const [devMode] = (0, import_react.useState)(() => props.devMode === true);
1761
+ const devClipsRef = (0, import_react.useRef)([]);
1762
+ const [devClipCount, setDevClipCount] = (0, import_react.useState)(0);
1763
+ const [devFallbackText, setDevFallbackText] = (0, import_react.useState)(null);
1468
1764
  const {
1469
1765
  queue,
1470
1766
  createReport,
@@ -1525,7 +1821,7 @@ function FeedbackKit(props) {
1525
1821
  }
1526
1822
  if (modal2.closeConfirmVisible && !previousConfirmVisibleRef.current) confirmRef.current?.focus();
1527
1823
  const wasModal = previousScreenRef.current === "modal";
1528
- if (wasModal && screen2 === "button" && modal2.restoreFocusTo === import_feedback_kit_core3.FLOATING_BUTTON_ID) {
1824
+ if (wasModal && screen2 === "button" && modal2.restoreFocusTo === import_feedback_kit_core4.FLOATING_BUTTON_ID) {
1529
1825
  void nextPaint().then(() => floatingButtonRef.current?.focus());
1530
1826
  }
1531
1827
  previousScreenRef.current = screen2;
@@ -1589,6 +1885,39 @@ function FeedbackKit(props) {
1589
1885
  if (root) root.style.visibility = previousVisibility;
1590
1886
  }
1591
1887
  }
1888
+ function currentClip() {
1889
+ const state = pickingState.popup;
1890
+ if (!state) return null;
1891
+ return {
1892
+ comment: state.comment,
1893
+ element: state.element ?? null,
1894
+ screen: typeof location === "undefined" ? null : location.pathname
1895
+ };
1896
+ }
1897
+ async function putOnClipboard(text) {
1898
+ const ok = await writeToClipboard(text);
1899
+ setDevFallbackText(ok ? null : text);
1900
+ }
1901
+ async function copyOne() {
1902
+ const clip = currentClip();
1903
+ if (!clip || clip.comment.trim() === "") return;
1904
+ devClipsRef.current = [...devClipsRef.current, clip];
1905
+ setDevClipCount(devClipsRef.current.length);
1906
+ activeKit.picking.cancelAnnotation();
1907
+ await putOnClipboard(renderDevClipItem(clip));
1908
+ }
1909
+ async function copyAll() {
1910
+ if (devClipsRef.current.length === 0) return;
1911
+ await putOnClipboard(renderDevClipList(devClipsRef.current));
1912
+ }
1913
+ async function pressFloatingButton() {
1914
+ if (props.defaultMode !== "report" && !pickingActive) {
1915
+ setAnnouncement(null);
1916
+ activeKit.widget.startPicking();
1917
+ return;
1918
+ }
1919
+ await openReport();
1920
+ }
1592
1921
  async function openReport() {
1593
1922
  setAnnouncement(null);
1594
1923
  await withOwnUiHidden(() => activeKit.widget.openReport());
@@ -1655,10 +1984,10 @@ function FeedbackKit(props) {
1655
1984
  "button",
1656
1985
  {
1657
1986
  ref: floatingButtonRef,
1658
- id: import_feedback_kit_core3.FLOATING_BUTTON_ID,
1987
+ id: import_feedback_kit_core4.FLOATING_BUTTON_ID,
1659
1988
  type: "button",
1660
1989
  "aria-label": "\uD53C\uB4DC\uBC31 \uBCF4\uB0B4\uAE30",
1661
- onClick: () => void openReport(),
1990
+ onClick: () => void pressFloatingButton(),
1662
1991
  style: {
1663
1992
  ...primaryButton,
1664
1993
  position: "fixed",
@@ -1673,6 +2002,66 @@ function FeedbackKit(props) {
1673
2002
  children: "\uD53C\uB4DC\uBC31"
1674
2003
  }
1675
2004
  ) : null,
2005
+ floating && devMode ? (
2006
+ // 켠 줄 모르고 제보하면 "보냈다"고 믿는데 수집처엔 아무것도 안 남는다.
2007
+ // 조용한 유실이라 이 표시가 유일한 방어다. 버튼 **밖**에 두는 이유는 버튼의
2008
+ // 접근성 이름을 오염시키지 않기 위해서다.
2009
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
2010
+ "span",
2011
+ {
2012
+ "data-fk-dev-badge": "",
2013
+ "aria-label": "\uAC1C\uBC1C\uC6A9 \uBAA8\uB4DC\uAC00 \uCF1C\uC838 \uC788\uC2B5\uB2C8\uB2E4 \u2014 \uC81C\uBCF4\uAC00 \uC218\uC9D1\uCC98\uB85C \uAC00\uC9C0 \uC54A\uACE0 \uD074\uB9BD\uBCF4\uB4DC\uB85C \uBCF5\uC0AC\uB429\uB2C8\uB2E4",
2014
+ style: {
2015
+ position: "fixed",
2016
+ right: 24,
2017
+ bottom: 62,
2018
+ zIndex: 2147483601,
2019
+ padding: "2px 8px",
2020
+ borderRadius: 6,
2021
+ background: TOKENS.warning,
2022
+ color: "#1f2933",
2023
+ fontSize: 11,
2024
+ fontWeight: 700,
2025
+ pointerEvents: "none"
2026
+ },
2027
+ children: "\uAC1C\uBC1C\uC6A9 \u2014 \uD074\uB9BD\uBCF4\uB4DC\uB85C \uBCF5\uC0AC"
2028
+ }
2029
+ )
2030
+ ) : null,
2031
+ devFallbackText !== null ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
2032
+ "div",
2033
+ {
2034
+ role: "dialog",
2035
+ "aria-label": "\uD074\uB9BD\uBCF4\uB4DC\uC5D0 \uBABB \uC368\uC11C \uB300\uC2E0 \uBCF4\uC5EC\uC90D\uB2C8\uB2E4",
2036
+ "data-fk-dev-fallback": "",
2037
+ style: {
2038
+ position: "fixed",
2039
+ right: 24,
2040
+ bottom: 92,
2041
+ zIndex: 2147483602,
2042
+ width: "min(420px, calc(100vw - 48px))",
2043
+ border: `1px solid ${TOKENS.line}`,
2044
+ borderRadius: 12,
2045
+ background: TOKENS.surface,
2046
+ padding: 12,
2047
+ boxShadow: TOKENS.shadow,
2048
+ pointerEvents: "auto"
2049
+ },
2050
+ children: [
2051
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: { marginBottom: 6, color: TOKENS.muted, fontSize: 12 }, children: "\uD074\uB9BD\uBCF4\uB4DC\uC5D0 \uC4F0\uC9C0 \uBABB\uD588\uC2B5\uB2C8\uB2E4. \uC544\uB798\uC5D0\uC11C \uC9C1\uC811 \uBCF5\uC0AC\uD558\uC138\uC694." }),
2052
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
2053
+ "textarea",
2054
+ {
2055
+ readOnly: true,
2056
+ value: devFallbackText,
2057
+ onFocus: (event) => event.currentTarget.select(),
2058
+ style: { ...inputStyle, height: 140, fontFamily: "ui-monospace, monospace", fontSize: 12 }
2059
+ }
2060
+ ),
2061
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: { display: "flex", justifyContent: "flex-end", marginTop: 8 }, children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { type: "button", onClick: () => setDevFallbackText(null), style: baseButton, children: "\uB2EB\uAE30" }) })
2062
+ ]
2063
+ }
2064
+ ) : null,
1676
2065
  announcement && !modal.open ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1677
2066
  "div",
1678
2067
  {
@@ -1770,7 +2159,7 @@ function FeedbackKit(props) {
1770
2159
  active: pickingActive,
1771
2160
  paused: pickingState.paused,
1772
2161
  disabled: draftLocked,
1773
- focusId: import_feedback_kit_core3.MODAL_ACTION_PICK,
2162
+ focusId: import_feedback_kit_core4.MODAL_ACTION_PICK,
1774
2163
  onToggle: togglePicking
1775
2164
  }
1776
2165
  ),
@@ -1804,7 +2193,7 @@ function FeedbackKit(props) {
1804
2193
  "button",
1805
2194
  {
1806
2195
  type: "button",
1807
- "data-fk-focus-id": import_feedback_kit_core3.MODAL_ACTION_REMOVE_SCREENSHOT,
2196
+ "data-fk-focus-id": import_feedback_kit_core4.MODAL_ACTION_REMOVE_SCREENSHOT,
1808
2197
  disabled: draftLocked,
1809
2198
  onClick: () => kit.widget.modal.removeScreenshot(),
1810
2199
  style: baseButton,
@@ -1864,7 +2253,7 @@ function FeedbackKit(props) {
1864
2253
  "textarea",
1865
2254
  {
1866
2255
  id: "feedback-kit-comment",
1867
- "data-fk-focus-id": import_feedback_kit_core3.MODAL_FIELD_COMMENT,
2256
+ "data-fk-focus-id": import_feedback_kit_core4.MODAL_FIELD_COMMENT,
1868
2257
  value: modal.comment,
1869
2258
  disabled: draftLocked,
1870
2259
  "aria-required": "true",
@@ -1885,7 +2274,7 @@ function FeedbackKit(props) {
1885
2274
  "select",
1886
2275
  {
1887
2276
  id: "feedback-kit-priority",
1888
- "data-fk-focus-id": import_feedback_kit_core3.MODAL_FIELD_PRIORITY,
2277
+ "data-fk-focus-id": import_feedback_kit_core4.MODAL_FIELD_PRIORITY,
1889
2278
  value: modal.priority,
1890
2279
  disabled: draftLocked,
1891
2280
  onChange: (event) => kit.widget.modal.setPriority(event.currentTarget.value),
@@ -1917,7 +2306,7 @@ function FeedbackKit(props) {
1917
2306
  "button",
1918
2307
  {
1919
2308
  type: "button",
1920
- "data-fk-focus-id": import_feedback_kit_core3.MODAL_ACTION_RETRY,
2309
+ "data-fk-focus-id": import_feedback_kit_core4.MODAL_ACTION_RETRY,
1921
2310
  onClick: () => void kit.widget.retryReport(),
1922
2311
  style: { ...baseButton, marginLeft: 10 },
1923
2312
  children: "\uB2E4\uC2DC \uBCF4\uB0B4\uAE30"
@@ -1931,7 +2320,7 @@ function FeedbackKit(props) {
1931
2320
  "button",
1932
2321
  {
1933
2322
  type: "button",
1934
- "data-fk-focus-id": import_feedback_kit_core3.MODAL_ACTION_CANCEL,
2323
+ "data-fk-focus-id": import_feedback_kit_core4.MODAL_ACTION_CANCEL,
1935
2324
  onClick: closeReport,
1936
2325
  style: baseButton,
1937
2326
  children: "\uCDE8\uC18C"
@@ -1941,7 +2330,7 @@ function FeedbackKit(props) {
1941
2330
  "button",
1942
2331
  {
1943
2332
  type: "submit",
1944
- "data-fk-focus-id": import_feedback_kit_core3.MODAL_ACTION_SEND,
2333
+ "data-fk-focus-id": import_feedback_kit_core4.MODAL_ACTION_SEND,
1945
2334
  disabled: !modal.canSubmit,
1946
2335
  style: {
1947
2336
  ...primaryButton,
@@ -2024,8 +2413,13 @@ function FeedbackKit(props) {
2024
2413
  PickBar,
2025
2414
  {
2026
2415
  paused: pickingState.paused,
2416
+ markerCount: pickingState.markers.length,
2027
2417
  onToggle: togglePicking,
2028
- onTogglePause: () => activeKit.picking.togglePause()
2418
+ onTogglePause: () => activeKit.picking.togglePause(),
2419
+ onClearMarkers: () => activeKit.picking.clearMarkers(),
2420
+ onReportWholeScreen: () => void openReport(),
2421
+ devClipCount: devMode ? devClipCount : 0,
2422
+ onCopyAll: () => void copyAll()
2029
2423
  }
2030
2424
  ),
2031
2425
  pickingState.markers.map((marker, index) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
@@ -2062,6 +2456,10 @@ function FeedbackKit(props) {
2062
2456
  "aria-label": "\uC694\uC18C \uC8FC\uC11D",
2063
2457
  onSubmit: (event) => {
2064
2458
  event.preventDefault();
2459
+ if (devMode) {
2460
+ void copyOne();
2461
+ return;
2462
+ }
2065
2463
  void kit.picking.saveAnnotation();
2066
2464
  },
2067
2465
  onPaste: (event) => {
@@ -2151,7 +2549,7 @@ function FeedbackKit(props) {
2151
2549
  style: { ...inputStyle, resize: "vertical", minHeight: 58 }
2152
2550
  }
2153
2551
  ),
2154
- popup.comment.length > import_feedback_kit_core3.COMMENT_MAX_CHARS ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { role: "alert", style: { marginTop: 4, color: TOKENS.danger }, children: "4,000\uC790 \uC774\uD558\uB85C \uC785\uB825\uD574\uC8FC\uC138\uC694" }) : null,
2552
+ popup.comment.length > import_feedback_kit_core4.COMMENT_MAX_CHARS ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { role: "alert", style: { marginTop: 4, color: TOKENS.danger }, children: "4,000\uC790 \uC774\uD558\uB85C \uC785\uB825\uD574\uC8FC\uC138\uC694" }) : null,
2155
2553
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
2156
2554
  "div",
2157
2555
  {
@@ -2215,7 +2613,7 @@ function FeedbackKit(props) {
2215
2613
  opacity: popup.canSave && !popup.saving ? 1 : 0.5,
2216
2614
  cursor: popup.canSave && !popup.saving ? "pointer" : "not-allowed"
2217
2615
  },
2218
- children: popup.saving ? "\uBCF4\uB0B4\uB294 \uC911\u2026" : "\uBCF4\uB0B4\uAE30"
2616
+ children: popup.saving ? "\uBCF4\uB0B4\uB294 \uC911\u2026" : devMode ? "\uBCF5\uC0AC" : "\uBCF4\uB0B4\uAE30"
2219
2617
  }
2220
2618
  )
2221
2619
  ] })
@@ -2251,6 +2649,13 @@ function webContextProviders() {
2251
2649
  }
2252
2650
  };
2253
2651
  }
2652
+ function webDiagnosticsOptions(opts = {}) {
2653
+ return {
2654
+ watchRoutes: webRouteWatcher,
2655
+ routeStorage: webRouteStorage(),
2656
+ ...opts
2657
+ };
2658
+ }
2254
2659
  // Annotate the CommonJS export names for ESM import in node:
2255
2660
  0 && (module.exports = {
2256
2661
  COMMENT_MAX_CHARS,
@@ -2276,6 +2681,7 @@ function webContextProviders() {
2276
2681
  SUBMIT_PENDING_MESSAGE,
2277
2682
  WidgetController,
2278
2683
  captureWebScreenshot,
2684
+ createRouteWatcher,
2279
2685
  createWebStorage,
2280
2686
  createWebWidget,
2281
2687
  cssSelectorPath,
@@ -2292,6 +2698,9 @@ function webContextProviders() {
2292
2698
  shouldShowWidget,
2293
2699
  sourceFromElement,
2294
2700
  visibleText,
2295
- webContextProviders
2701
+ webContextProviders,
2702
+ webDiagnosticsOptions,
2703
+ webRouteStorage,
2704
+ webRouteWatcher
2296
2705
  });
2297
2706
  //# sourceMappingURL=index.cjs.map