@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.js CHANGED
@@ -1,6 +1,6 @@
1
1
  // src/index.ts
2
2
  import { resolveConfig, shouldShowWidget } from "@solhun/feedback-kit-core";
3
- import { parseSourceAttr, SOURCE_ATTR, sourceFromElement } from "@solhun/feedback-kit-core";
3
+ import { parseSourceAttr, SOURCE_ATTR, sourceFromElement as sourceFromElement2 } from "@solhun/feedback-kit-core";
4
4
  import {
5
5
  COMMENT_MAX_CHARS as COMMENT_MAX_CHARS3,
6
6
  COMMENT_REQUIRED_MESSAGE,
@@ -103,6 +103,7 @@ function reactComponentSummary(node) {
103
103
  // src/element-info.ts
104
104
  var ELEMENT_TEXT_MAX_CHARS = 200;
105
105
  var SELECTOR_MAX_DEPTH = 8;
106
+ var PICK_TARGET_SNAP_PX = 24;
106
107
  var KEPT_ATTRS = [
107
108
  "id",
108
109
  "class",
@@ -175,6 +176,55 @@ function visibleText(el) {
175
176
  if (collapsed === "") return null;
176
177
  return collapsed.length > ELEMENT_TEXT_MAX_CHARS ? collapsed.slice(0, ELEMENT_TEXT_MAX_CHARS) : collapsed;
177
178
  }
179
+ function elementRect(element) {
180
+ try {
181
+ const rect = element.getBoundingClientRect();
182
+ const left = rect.left;
183
+ const top = rect.top;
184
+ const width = rect.width;
185
+ const height = rect.height;
186
+ if (![left, top, width, height].every(Number.isFinite) || width <= 0 || height <= 0) {
187
+ return null;
188
+ }
189
+ return {
190
+ element,
191
+ left,
192
+ top,
193
+ right: left + width,
194
+ bottom: top + height,
195
+ width,
196
+ height
197
+ };
198
+ } catch {
199
+ return null;
200
+ }
201
+ }
202
+ function distanceFromPoint(rect, point) {
203
+ const dx = Math.max(rect.left - point.x, 0, point.x - rect.right);
204
+ const dy = Math.max(rect.top - point.y, 0, point.y - rect.bottom);
205
+ return Math.hypot(dx, dy);
206
+ }
207
+ function resolvePickTarget(target, point) {
208
+ if (!target || typeof target.tagName !== "string") return null;
209
+ if (!Number.isFinite(point.x) || !Number.isFinite(point.y)) return target;
210
+ let current = target;
211
+ while (true) {
212
+ const currentRect = elementRect(current);
213
+ const children = Array.from(current.children).map(elementRect).filter((rect) => rect !== null).map((rect) => ({ rect, distance: distanceFromPoint(rect, point) }));
214
+ if (children.length === 0) return current;
215
+ const containing = children.filter(({ distance }) => distance === 0).sort((a, b) => a.rect.width * a.rect.height - b.rect.width * b.rect.height);
216
+ if (containing[0]) {
217
+ current = containing[0].rect.element;
218
+ continue;
219
+ }
220
+ const nearest = children.sort((a, b) => a.distance - b.distance)[0];
221
+ const isMorePrecise = currentRect ? nearest.rect.width * nearest.rect.height < currentRect.width * currentRect.height : true;
222
+ if (nearest.distance <= PICK_TARGET_SNAP_PX && isMorePrecise) {
223
+ return nearest.rect.element;
224
+ }
225
+ return current;
226
+ }
227
+ }
178
228
  function describeElement(el) {
179
229
  if (!el || typeof el.tagName !== "string") return null;
180
230
  const attributes = {};
@@ -299,6 +349,84 @@ import {
299
349
  captureWithinLimit,
300
350
  normalizePin
301
351
  } from "@solhun/feedback-kit-core";
352
+
353
+ // src/route-watcher.ts
354
+ function defaultTarget() {
355
+ const g = globalThis;
356
+ return g.location ? g : null;
357
+ }
358
+ function createRouteWatcher(target = defaultTarget()) {
359
+ const listeners = /* @__PURE__ */ new Set();
360
+ let detach = null;
361
+ const currentPath = () => {
362
+ const p = target?.location?.pathname;
363
+ return typeof p === "string" && p.length > 0 ? p : "/";
364
+ };
365
+ const emit = () => {
366
+ const path = currentPath();
367
+ for (const listener of [...listeners]) {
368
+ try {
369
+ listener(path);
370
+ } catch {
371
+ }
372
+ }
373
+ };
374
+ const attach = () => {
375
+ if (detach !== null || !target) return;
376
+ const onPop = () => emit();
377
+ target.addEventListener?.("popstate", onPop);
378
+ const history = target.history;
379
+ const originalPush = typeof history?.pushState === "function" ? history.pushState : null;
380
+ const originalReplace = typeof history?.replaceState === "function" ? history.replaceState : null;
381
+ if (history && originalPush) {
382
+ history.pushState = function patched(...args) {
383
+ const out = originalPush.apply(this, args);
384
+ emit();
385
+ return out;
386
+ };
387
+ }
388
+ if (history && originalReplace) {
389
+ history.replaceState = function patched(...args) {
390
+ const out = originalReplace.apply(this, args);
391
+ emit();
392
+ return out;
393
+ };
394
+ }
395
+ detach = () => {
396
+ target.removeEventListener?.("popstate", onPop);
397
+ if (history && originalPush) history.pushState = originalPush;
398
+ if (history && originalReplace) history.replaceState = originalReplace;
399
+ };
400
+ };
401
+ return (onPath) => {
402
+ listeners.add(onPath);
403
+ if (listeners.size === 1) attach();
404
+ try {
405
+ onPath(currentPath());
406
+ } catch {
407
+ }
408
+ return () => {
409
+ listeners.delete(onPath);
410
+ if (listeners.size === 0 && detach) {
411
+ detach();
412
+ detach = null;
413
+ }
414
+ };
415
+ };
416
+ }
417
+ var webRouteWatcher = createRouteWatcher();
418
+ function webRouteStorage() {
419
+ try {
420
+ const s = globalThis.sessionStorage;
421
+ if (!s || typeof s.getItem !== "function" || typeof s.setItem !== "function") return null;
422
+ s.getItem("feedback-kit:probe");
423
+ return s;
424
+ } catch {
425
+ return null;
426
+ }
427
+ }
428
+
429
+ // src/picking.ts
302
430
  var OWN_UI_ATTR = "data-feedback-kit";
303
431
  function defaultDocument() {
304
432
  return globalThis.document ?? null;
@@ -327,10 +455,13 @@ var ElementPickingController = class {
327
455
  */
328
456
  this.paused = false;
329
457
  this.attached = false;
458
+ /** 같은 요소 위 mousemove 마다 React 경로·선택자를 다시 만들지 않기 위한 캐시. */
459
+ this.hoveredTarget = null;
330
460
  this.hovered = null;
331
461
  this.popup = null;
332
462
  this.markers = [];
333
- this.pathWatch = null;
463
+ /** 경로 감시 해제. 감시 중이 아니면 null. */
464
+ this.unwatchRoutes = null;
334
465
  this.saving = false;
335
466
  /** 늦게 끝난 캡처가 다음 주석의 그림을 덮지 못하게 하는 세대 번호. */
336
467
  this.shotGeneration = 0;
@@ -343,6 +474,7 @@ var ElementPickingController = class {
343
474
  this.userToggledHints = false;
344
475
  this.onClick = (event) => this.handleClick(event);
345
476
  this.onMouseOver = (event) => this.handleMouseOver(event);
477
+ this.onMouseMove = (event) => this.handleMouseOver(event);
346
478
  this.queue = opts.queue;
347
479
  this.createReport = opts.createReport;
348
480
  this.store = opts.store ?? new MarkerStore();
@@ -354,6 +486,7 @@ var ElementPickingController = class {
354
486
  this.reencode = opts.reencode ?? null;
355
487
  this.screenshotLimitBytes = opts.screenshotLimitBytes;
356
488
  this.rankHintsFor = opts.rankHintsFor ?? null;
489
+ this.watchRoutes = opts.watchRoutes === void 0 ? webRouteWatcher : opts.watchRoutes;
357
490
  this.lastPathname = this.getPathname();
358
491
  this.markers = this.store.list(this.lastPathname);
359
492
  this.unsubscribeQueue = this.queue.subscribe?.(() => {
@@ -402,6 +535,7 @@ var ElementPickingController = class {
402
535
  this.store.setPickingActive(false);
403
536
  this.detach();
404
537
  this.stopPathWatch();
538
+ this.hoveredTarget = null;
405
539
  this.hovered = null;
406
540
  this.popup = null;
407
541
  this.emit();
@@ -417,6 +551,7 @@ var ElementPickingController = class {
417
551
  if (!this.active || this.paused) return;
418
552
  this.paused = true;
419
553
  this.detach();
554
+ this.hoveredTarget = null;
420
555
  this.hovered = null;
421
556
  this.emit();
422
557
  }
@@ -444,6 +579,22 @@ var ElementPickingController = class {
444
579
  this.start();
445
580
  return true;
446
581
  }
582
+ /**
583
+ * 이 화면에 쌓인 마커 표시를 지운다.
584
+ *
585
+ * 지워지는 건 **화면 표시뿐**이다. 제보는 이미 큐를 거쳐 수집처로 갔으므로 없어지지 않는다.
586
+ * 마커는 `localStorage` 에 남아 새로고침에도 살아남는데(그게 원래 목적이다) 정작 치울
587
+ * 수단이 없어서, 한 번 보낸 [완료] 배지가 그 화면을 볼 때마다 계속 따라다녔다.
588
+ *
589
+ * 화면에 보이는 목록의 기준은 `lastPathname` 이다(`reconcileMarkerOutcomes` 와 같다).
590
+ * 이동 직후 아직 `syncPath` 가 안 돈 순간에도 "지금 눈에 보이는 것"이 지워져야 한다.
591
+ */
592
+ clearMarkers() {
593
+ if (this.markers.length === 0) return;
594
+ this.store.clear(this.lastPathname);
595
+ this.markers = [];
596
+ this.emit();
597
+ }
447
598
  /** 경로가 바뀌었을 때 그 경로의 마커로 갈아 끼운다. */
448
599
  syncPath() {
449
600
  if (this.paused) this.resume();
@@ -451,6 +602,7 @@ var ElementPickingController = class {
451
602
  this.markers = this.store.list(this.lastPathname);
452
603
  this.reconcileMarkerOutcomes();
453
604
  this.popup = null;
605
+ this.hoveredTarget = null;
454
606
  this.hovered = null;
455
607
  this.emit();
456
608
  return this.markers;
@@ -466,17 +618,26 @@ var ElementPickingController = class {
466
618
  if (this.attached || !this.doc) return;
467
619
  this.doc.addEventListener("click", this.onClick, true);
468
620
  this.doc.addEventListener("mouseover", this.onMouseOver, true);
621
+ this.doc.addEventListener("mousemove", this.onMouseMove, true);
469
622
  this.attached = true;
470
623
  }
471
624
  detach() {
472
625
  if (!this.attached || !this.doc) return;
473
626
  this.doc.removeEventListener("click", this.onClick, true);
474
627
  this.doc.removeEventListener("mouseover", this.onMouseOver, true);
628
+ this.doc.removeEventListener("mousemove", this.onMouseMove, true);
475
629
  this.attached = false;
476
630
  }
477
631
  handleMouseOver(event) {
478
632
  if (!this.active || this.paused || isOwnUi(event.target)) return;
479
- this.hovered = describeElement(event.target);
633
+ const mouse = event;
634
+ const target = resolvePickTarget(event.target, {
635
+ x: mouse.clientX ?? 0,
636
+ y: mouse.clientY ?? 0
637
+ });
638
+ if (target === this.hoveredTarget) return;
639
+ this.hoveredTarget = target;
640
+ this.hovered = describeElement(target);
480
641
  this.emit();
481
642
  }
482
643
  handleClick(event) {
@@ -497,11 +658,12 @@ var ElementPickingController = class {
497
658
  /** 클릭 지점 기준으로 주석 팝업을 연다. 좌표는 해상도 무관한 상대값으로 접어 둔다. */
498
659
  openAnnotation(element, clientPoint) {
499
660
  const point = normalizePin(clientPoint, this.getViewport());
661
+ const target = resolvePickTarget(element, clientPoint);
500
662
  if (this.onPick) {
501
- this.onPick(describeElement(element), point);
663
+ this.onPick(describeElement(target), point);
502
664
  return;
503
665
  }
504
- const info = describeElement(element);
666
+ const info = describeElement(target);
505
667
  this.userToggledHints = false;
506
668
  const hints = this.rankHintsFor ? this.rankHintsFor(info) : [];
507
669
  this.popup = {
@@ -716,17 +878,17 @@ ${hint.draft}`;
716
878
  return outcome;
717
879
  }
718
880
  startPathWatch() {
719
- if (this.pathWatch !== null || typeof setInterval !== "function") return;
881
+ if (this.unwatchRoutes !== null || !this.watchRoutes) return;
720
882
  this.lastPathname = this.getPathname();
721
- this.pathWatch = setInterval(() => {
883
+ this.unwatchRoutes = this.watchRoutes(() => {
722
884
  const pathname = this.getPathname();
723
885
  if (pathname !== this.lastPathname) this.syncPath();
724
- }, 200);
886
+ });
725
887
  }
726
888
  stopPathWatch() {
727
- if (this.pathWatch === null) return;
728
- clearInterval(this.pathWatch);
729
- this.pathWatch = null;
889
+ if (this.unwatchRoutes === null) return;
890
+ this.unwatchRoutes();
891
+ this.unwatchRoutes = null;
730
892
  }
731
893
  /** 전역 pending 수가 아니라 마커와 같은 clientSubmissionId의 확정 성공만 완료로 바꾼다. */
732
894
  reconcileMarkerOutcomes() {
@@ -1012,7 +1174,53 @@ import {
1012
1174
  } from "react";
1013
1175
 
1014
1176
  // src/version.ts
1015
- var VERSION = true ? "0.7.0" : "dev";
1177
+ var VERSION = true ? "0.9.0" : "dev";
1178
+
1179
+ // src/dev-clipboard.ts
1180
+ import { sourceFromElement } from "@solhun/feedback-kit-core";
1181
+ function sourceLine(item) {
1182
+ const mapping = sourceFromElement(item.element ?? void 0);
1183
+ if (!mapping.sourceFile) return null;
1184
+ return mapping.sourceLine === null ? `\uC18C\uC2A4: ${mapping.sourceFile}` : `\uC18C\uC2A4: ${mapping.sourceFile}:${mapping.sourceLine}`;
1185
+ }
1186
+ function elementLine(element) {
1187
+ if (!element) return null;
1188
+ const selector = element.selector ?? element.tag;
1189
+ const text = element.text?.trim();
1190
+ return text ? `\uC694\uC18C: ${selector} "${text}"` : `\uC694\uC18C: ${selector}`;
1191
+ }
1192
+ function componentLine(element) {
1193
+ const summary = element?.attributes?.["react-components"];
1194
+ return summary ? `\uCEF4\uD3EC\uB10C\uD2B8: ${summary}` : null;
1195
+ }
1196
+ function renderDevClipItem(item) {
1197
+ const lines = [`[\uD53C\uB4DC\uBC31] ${item.comment.trim()}`];
1198
+ const screen = item.screen?.trim();
1199
+ if (screen) lines.push(`\uD654\uBA74: ${screen}`);
1200
+ const element = elementLine(item.element);
1201
+ if (element) lines.push(element);
1202
+ const component = componentLine(item.element);
1203
+ if (component) lines.push(component);
1204
+ const source = sourceLine(item);
1205
+ if (source) lines.push(source);
1206
+ if (item.priority && item.priority !== "unset") lines.push(`\uC6B0\uC120\uC21C\uC704: ${item.priority}`);
1207
+ return lines.join("\n");
1208
+ }
1209
+ function renderDevClipList(items) {
1210
+ if (items.length === 0) return "";
1211
+ if (items.length === 1) return renderDevClipItem(items[0]);
1212
+ return items.map((item, index) => `${index + 1}. ${renderDevClipItem(item).split("\n").join("\n ")}`).join("\n\n");
1213
+ }
1214
+ async function writeToClipboard(text) {
1215
+ const clipboard = globalThis.navigator?.clipboard;
1216
+ if (!clipboard?.writeText) return false;
1217
+ try {
1218
+ await clipboard.writeText(text);
1219
+ return true;
1220
+ } catch {
1221
+ return false;
1222
+ }
1223
+ }
1016
1224
 
1017
1225
  // src/feedback-kit.tsx
1018
1226
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
@@ -1278,8 +1486,13 @@ function PickToggle({
1278
1486
  }
1279
1487
  function PickBar({
1280
1488
  paused,
1489
+ markerCount,
1281
1490
  onToggle,
1282
- onTogglePause
1491
+ onTogglePause,
1492
+ onClearMarkers,
1493
+ onReportWholeScreen,
1494
+ devClipCount,
1495
+ onCopyAll
1283
1496
  }) {
1284
1497
  return /* @__PURE__ */ jsxs(
1285
1498
  "div",
@@ -1287,10 +1500,20 @@ function PickBar({
1287
1500
  style: {
1288
1501
  position: "fixed",
1289
1502
  top: 16,
1290
- left: "50%",
1291
- transform: "translateX(-50%)",
1503
+ // `left:50% + translateX(-50%)` 로 가운데를 잡으면 **쓸 수 있는 폭이 화면의 절반**이
1504
+ // 된다(transform 은 레이아웃 계산에 안 들어간다). 버튼이 셋이 되면서 그 절반에
1505
+ // 안 맞아 좁은 화면에서 버튼이 한 줄에 하나씩 세로로 쌓였다(390px 에서 3줄).
1506
+ // 양끝을 물리고 auto 마진으로 가운데를 잡으면 화면 전체 폭을 쓴다 — 같은 화면이 2줄.
1507
+ left: 0,
1508
+ right: 0,
1509
+ marginInline: "auto",
1510
+ width: "fit-content",
1511
+ maxWidth: "calc(100% - 32px)",
1292
1512
  display: "flex",
1293
1513
  alignItems: "stretch",
1514
+ justifyContent: "center",
1515
+ // 셋이 한 줄에 안 들어가는 폭에서는 줄을 바꾼다 — 안 그러면 화면 밖으로 나간다.
1516
+ flexWrap: "wrap",
1294
1517
  gap: 8,
1295
1518
  // 오버레이 자체는 클릭을 통과시킨다(pointerEvents:none) — 이 막대만 되살린다.
1296
1519
  pointerEvents: "auto",
@@ -1323,7 +1546,72 @@ function PickBar({
1323
1546
  paused ? "\uC9C0\uBAA9 \uC7AC\uAC1C" : "\uC7A0\uC2DC \uBA48\uCDA4"
1324
1547
  ]
1325
1548
  }
1326
- )
1549
+ ),
1550
+ /* @__PURE__ */ jsxs(
1551
+ "button",
1552
+ {
1553
+ type: "button",
1554
+ "data-fk-pick-whole": "",
1555
+ title: "\uAC00\uB9AC\uD0A8 \uC694\uC18C \uC5C6\uC774 \uC774 \uD654\uBA74 \uC804\uCCB4\uC5D0 \uB300\uD574 \uC81C\uBCF4\uD569\uB2C8\uB2E4.",
1556
+ onClick: onReportWholeScreen,
1557
+ style: {
1558
+ ...baseButton,
1559
+ display: "inline-flex",
1560
+ alignItems: "center",
1561
+ gap: 6,
1562
+ whiteSpace: "nowrap"
1563
+ },
1564
+ children: [
1565
+ /* @__PURE__ */ jsx("span", { "aria-hidden": "true", children: "\u25A3" }),
1566
+ "\uD654\uBA74 \uC804\uCCB4\uB85C \uBCF4\uB0B4\uAE30"
1567
+ ]
1568
+ }
1569
+ ),
1570
+ devClipCount > 0 ? /* @__PURE__ */ jsxs(
1571
+ "button",
1572
+ {
1573
+ type: "button",
1574
+ "data-fk-copy-all": devClipCount,
1575
+ title: "\uC774\uBC88 \uC138\uC158\uC5D0 \uCC0D\uC740 \uAC83\uC744 \uBC88\uD638 \uB9E4\uAE34 \uBAA9\uB85D \uD558\uB098\uB85C \uBCF5\uC0AC\uD569\uB2C8\uB2E4.",
1576
+ onClick: onCopyAll,
1577
+ style: {
1578
+ ...baseButton,
1579
+ display: "inline-flex",
1580
+ alignItems: "center",
1581
+ gap: 6,
1582
+ whiteSpace: "nowrap",
1583
+ fontWeight: 600
1584
+ },
1585
+ children: [
1586
+ /* @__PURE__ */ jsx("span", { "aria-hidden": "true", children: "\u29C9" }),
1587
+ "\uC804\uBD80 \uBCF5\uC0AC ",
1588
+ devClipCount
1589
+ ]
1590
+ }
1591
+ ) : null,
1592
+ markerCount > 0 ? /* @__PURE__ */ jsxs(
1593
+ "button",
1594
+ {
1595
+ type: "button",
1596
+ "data-fk-pick-clear": markerCount,
1597
+ title: "\uD654\uBA74\uC758 \uD45C\uC2DC\uB9CC \uC9C0\uC6C1\uB2C8\uB2E4. \uBCF4\uB0B8 \uC81C\uBCF4\uB294 \uADF8\uB300\uB85C \uB0A8\uC2B5\uB2C8\uB2E4.",
1598
+ "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)`,
1599
+ onClick: onClearMarkers,
1600
+ style: {
1601
+ ...baseButton,
1602
+ display: "inline-flex",
1603
+ alignItems: "center",
1604
+ gap: 6,
1605
+ whiteSpace: "nowrap",
1606
+ color: TOKENS.muted
1607
+ },
1608
+ children: [
1609
+ /* @__PURE__ */ jsx("span", { "aria-hidden": "true", children: "\u2715" }),
1610
+ "\uD45C\uC2DC \uC9C0\uC6B0\uAE30 ",
1611
+ markerCount
1612
+ ]
1613
+ }
1614
+ ) : null
1327
1615
  ]
1328
1616
  }
1329
1617
  );
@@ -1429,6 +1717,10 @@ function FeedbackKit(props) {
1429
1717
  const [widgetState, setWidgetState] = useState(null);
1430
1718
  const [pickingState, setPickingState] = useState(INITIAL_PICKING_STATE);
1431
1719
  const [announcement, setAnnouncement] = useState(null);
1720
+ const [devMode] = useState(() => props.devMode === true);
1721
+ const devClipsRef = useRef([]);
1722
+ const [devClipCount, setDevClipCount] = useState(0);
1723
+ const [devFallbackText, setDevFallbackText] = useState(null);
1432
1724
  const {
1433
1725
  queue,
1434
1726
  createReport,
@@ -1553,6 +1845,39 @@ function FeedbackKit(props) {
1553
1845
  if (root) root.style.visibility = previousVisibility;
1554
1846
  }
1555
1847
  }
1848
+ function currentClip() {
1849
+ const state = pickingState.popup;
1850
+ if (!state) return null;
1851
+ return {
1852
+ comment: state.comment,
1853
+ element: state.element ?? null,
1854
+ screen: typeof location === "undefined" ? null : location.pathname
1855
+ };
1856
+ }
1857
+ async function putOnClipboard(text) {
1858
+ const ok = await writeToClipboard(text);
1859
+ setDevFallbackText(ok ? null : text);
1860
+ }
1861
+ async function copyOne() {
1862
+ const clip = currentClip();
1863
+ if (!clip || clip.comment.trim() === "") return;
1864
+ devClipsRef.current = [...devClipsRef.current, clip];
1865
+ setDevClipCount(devClipsRef.current.length);
1866
+ activeKit.picking.cancelAnnotation();
1867
+ await putOnClipboard(renderDevClipItem(clip));
1868
+ }
1869
+ async function copyAll() {
1870
+ if (devClipsRef.current.length === 0) return;
1871
+ await putOnClipboard(renderDevClipList(devClipsRef.current));
1872
+ }
1873
+ async function pressFloatingButton() {
1874
+ if (props.defaultMode !== "report" && !pickingActive) {
1875
+ setAnnouncement(null);
1876
+ activeKit.widget.startPicking();
1877
+ return;
1878
+ }
1879
+ await openReport();
1880
+ }
1556
1881
  async function openReport() {
1557
1882
  setAnnouncement(null);
1558
1883
  await withOwnUiHidden(() => activeKit.widget.openReport());
@@ -1622,7 +1947,7 @@ function FeedbackKit(props) {
1622
1947
  id: FLOATING_BUTTON_ID,
1623
1948
  type: "button",
1624
1949
  "aria-label": "\uD53C\uB4DC\uBC31 \uBCF4\uB0B4\uAE30",
1625
- onClick: () => void openReport(),
1950
+ onClick: () => void pressFloatingButton(),
1626
1951
  style: {
1627
1952
  ...primaryButton,
1628
1953
  position: "fixed",
@@ -1637,6 +1962,66 @@ function FeedbackKit(props) {
1637
1962
  children: "\uD53C\uB4DC\uBC31"
1638
1963
  }
1639
1964
  ) : null,
1965
+ floating && devMode ? (
1966
+ // 켠 줄 모르고 제보하면 "보냈다"고 믿는데 수집처엔 아무것도 안 남는다.
1967
+ // 조용한 유실이라 이 표시가 유일한 방어다. 버튼 **밖**에 두는 이유는 버튼의
1968
+ // 접근성 이름을 오염시키지 않기 위해서다.
1969
+ /* @__PURE__ */ jsx(
1970
+ "span",
1971
+ {
1972
+ "data-fk-dev-badge": "",
1973
+ "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",
1974
+ style: {
1975
+ position: "fixed",
1976
+ right: 24,
1977
+ bottom: 62,
1978
+ zIndex: 2147483601,
1979
+ padding: "2px 8px",
1980
+ borderRadius: 6,
1981
+ background: TOKENS.warning,
1982
+ color: "#1f2933",
1983
+ fontSize: 11,
1984
+ fontWeight: 700,
1985
+ pointerEvents: "none"
1986
+ },
1987
+ children: "\uAC1C\uBC1C\uC6A9 \u2014 \uD074\uB9BD\uBCF4\uB4DC\uB85C \uBCF5\uC0AC"
1988
+ }
1989
+ )
1990
+ ) : null,
1991
+ devFallbackText !== null ? /* @__PURE__ */ jsxs(
1992
+ "div",
1993
+ {
1994
+ role: "dialog",
1995
+ "aria-label": "\uD074\uB9BD\uBCF4\uB4DC\uC5D0 \uBABB \uC368\uC11C \uB300\uC2E0 \uBCF4\uC5EC\uC90D\uB2C8\uB2E4",
1996
+ "data-fk-dev-fallback": "",
1997
+ style: {
1998
+ position: "fixed",
1999
+ right: 24,
2000
+ bottom: 92,
2001
+ zIndex: 2147483602,
2002
+ width: "min(420px, calc(100vw - 48px))",
2003
+ border: `1px solid ${TOKENS.line}`,
2004
+ borderRadius: 12,
2005
+ background: TOKENS.surface,
2006
+ padding: 12,
2007
+ boxShadow: TOKENS.shadow,
2008
+ pointerEvents: "auto"
2009
+ },
2010
+ children: [
2011
+ /* @__PURE__ */ 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." }),
2012
+ /* @__PURE__ */ jsx(
2013
+ "textarea",
2014
+ {
2015
+ readOnly: true,
2016
+ value: devFallbackText,
2017
+ onFocus: (event) => event.currentTarget.select(),
2018
+ style: { ...inputStyle, height: 140, fontFamily: "ui-monospace, monospace", fontSize: 12 }
2019
+ }
2020
+ ),
2021
+ /* @__PURE__ */ jsx("div", { style: { display: "flex", justifyContent: "flex-end", marginTop: 8 }, children: /* @__PURE__ */ jsx("button", { type: "button", onClick: () => setDevFallbackText(null), style: baseButton, children: "\uB2EB\uAE30" }) })
2022
+ ]
2023
+ }
2024
+ ) : null,
1640
2025
  announcement && !modal.open ? /* @__PURE__ */ jsx(
1641
2026
  "div",
1642
2027
  {
@@ -1988,8 +2373,13 @@ function FeedbackKit(props) {
1988
2373
  PickBar,
1989
2374
  {
1990
2375
  paused: pickingState.paused,
2376
+ markerCount: pickingState.markers.length,
1991
2377
  onToggle: togglePicking,
1992
- onTogglePause: () => activeKit.picking.togglePause()
2378
+ onTogglePause: () => activeKit.picking.togglePause(),
2379
+ onClearMarkers: () => activeKit.picking.clearMarkers(),
2380
+ onReportWholeScreen: () => void openReport(),
2381
+ devClipCount: devMode ? devClipCount : 0,
2382
+ onCopyAll: () => void copyAll()
1993
2383
  }
1994
2384
  ),
1995
2385
  pickingState.markers.map((marker, index) => /* @__PURE__ */ jsx(
@@ -2026,6 +2416,10 @@ function FeedbackKit(props) {
2026
2416
  "aria-label": "\uC694\uC18C \uC8FC\uC11D",
2027
2417
  onSubmit: (event) => {
2028
2418
  event.preventDefault();
2419
+ if (devMode) {
2420
+ void copyOne();
2421
+ return;
2422
+ }
2029
2423
  void kit.picking.saveAnnotation();
2030
2424
  },
2031
2425
  onPaste: (event) => {
@@ -2179,7 +2573,7 @@ function FeedbackKit(props) {
2179
2573
  opacity: popup.canSave && !popup.saving ? 1 : 0.5,
2180
2574
  cursor: popup.canSave && !popup.saving ? "pointer" : "not-allowed"
2181
2575
  },
2182
- children: popup.saving ? "\uBCF4\uB0B4\uB294 \uC911\u2026" : "\uBCF4\uB0B4\uAE30"
2576
+ children: popup.saving ? "\uBCF4\uB0B4\uB294 \uC911\u2026" : devMode ? "\uBCF5\uC0AC" : "\uBCF4\uB0B4\uAE30"
2183
2577
  }
2184
2578
  )
2185
2579
  ] })
@@ -2215,6 +2609,13 @@ function webContextProviders() {
2215
2609
  }
2216
2610
  };
2217
2611
  }
2612
+ function webDiagnosticsOptions(opts = {}) {
2613
+ return {
2614
+ watchRoutes: webRouteWatcher,
2615
+ routeStorage: webRouteStorage(),
2616
+ ...opts
2617
+ };
2618
+ }
2218
2619
  export {
2219
2620
  COMMENT_MAX_CHARS3 as COMMENT_MAX_CHARS,
2220
2621
  COMMENT_REQUIRED_MESSAGE,
@@ -2239,6 +2640,7 @@ export {
2239
2640
  SUBMIT_PENDING_MESSAGE,
2240
2641
  WidgetController2 as WidgetController,
2241
2642
  captureWebScreenshot,
2643
+ createRouteWatcher,
2242
2644
  createWebStorage,
2243
2645
  createWebWidget,
2244
2646
  cssSelectorPath,
@@ -2253,8 +2655,11 @@ export {
2253
2655
  reencodeWebScreenshot,
2254
2656
  resolveConfig,
2255
2657
  shouldShowWidget,
2256
- sourceFromElement,
2658
+ sourceFromElement2 as sourceFromElement,
2257
2659
  visibleText,
2258
- webContextProviders
2660
+ webContextProviders,
2661
+ webDiagnosticsOptions,
2662
+ webRouteStorage,
2663
+ webRouteWatcher
2259
2664
  };
2260
2665
  //# sourceMappingURL=index.js.map