@solhun/feedback-kit-web 0.6.1 → 0.8.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
@@ -53,6 +53,7 @@ __export(index_exports, {
53
53
  SUBMIT_PENDING_MESSAGE: () => import_feedback_kit_core6.SUBMIT_PENDING_MESSAGE,
54
54
  WidgetController: () => import_feedback_kit_core6.WidgetController,
55
55
  captureWebScreenshot: () => captureWebScreenshot,
56
+ createRouteWatcher: () => createRouteWatcher,
56
57
  createWebStorage: () => createWebStorage,
57
58
  createWebWidget: () => createWebWidget,
58
59
  cssSelectorPath: () => cssSelectorPath,
@@ -69,7 +70,10 @@ __export(index_exports, {
69
70
  shouldShowWidget: () => import_feedback_kit_core4.shouldShowWidget,
70
71
  sourceFromElement: () => import_feedback_kit_core5.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
79
  var import_feedback_kit_core4 = require("@solhun/feedback-kit-core");
@@ -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;
@@ -375,23 +507,33 @@ var ElementPickingController = class {
375
507
  constructor(opts) {
376
508
  this.listeners = /* @__PURE__ */ new Set();
377
509
  this.active = false;
510
+ /**
511
+ * 일시중지. **저장하지 않는다** — 멈추는 목적이 대개 "링크를 눌러 다른 화면으로 가는 것"
512
+ * 이라, 그 이동이 끝나면 지목이 돌아와 있는 게 목적에 맞다. 같은 이유로 경로가 바뀌면
513
+ * 스스로 풀린다(`syncPath`) — 그래야 SPA 이동과 새로고침이 같게 동작한다.
514
+ */
515
+ this.paused = false;
378
516
  this.attached = false;
517
+ /** 같은 요소 위 mousemove 마다 React 경로·선택자를 다시 만들지 않기 위한 캐시. */
518
+ this.hoveredTarget = null;
379
519
  this.hovered = null;
380
520
  this.popup = null;
381
521
  this.markers = [];
382
- this.pathWatch = null;
522
+ /** 경로 감시 해제. 감시 중이 아니면 null. */
523
+ this.unwatchRoutes = null;
383
524
  this.saving = false;
384
525
  /** 늦게 끝난 캡처가 다음 주석의 그림을 덮지 못하게 하는 세대 번호. */
385
526
  this.shotGeneration = 0;
386
527
  /** 지금 도는 자동 캡처. Enter 가 캡처보다 빨랐을 때 기다릴 대상. */
387
528
  this.capturing = null;
388
529
  /**
389
- * 사용자가 [제안 ▾] 를 직접 건드렸는가. 한 번이라도 건드리면 이후 타이핑에 의한 자동
530
+ * 사용자가 [이런 건가요? ▾] 를 직접 건드렸는가. 한 번이라도 건드리면 이후 타이핑에 의한 자동
390
531
  * 접힘/펼침이 멈춘다 — 모달의 `userToggledHints` 와 같은 규칙. 팝업을 새로 열 때마다 리셋된다.
391
532
  */
392
533
  this.userToggledHints = false;
393
534
  this.onClick = (event) => this.handleClick(event);
394
535
  this.onMouseOver = (event) => this.handleMouseOver(event);
536
+ this.onMouseMove = (event) => this.handleMouseOver(event);
395
537
  this.queue = opts.queue;
396
538
  this.createReport = opts.createReport;
397
539
  this.store = opts.store ?? new MarkerStore();
@@ -403,6 +545,7 @@ var ElementPickingController = class {
403
545
  this.reencode = opts.reencode ?? null;
404
546
  this.screenshotLimitBytes = opts.screenshotLimitBytes;
405
547
  this.rankHintsFor = opts.rankHintsFor ?? null;
548
+ this.watchRoutes = opts.watchRoutes === void 0 ? webRouteWatcher : opts.watchRoutes;
406
549
  this.lastPathname = this.getPathname();
407
550
  this.markers = this.store.list(this.lastPathname);
408
551
  this.unsubscribeQueue = this.queue.subscribe?.(() => {
@@ -413,6 +556,7 @@ var ElementPickingController = class {
413
556
  getState() {
414
557
  return {
415
558
  active: this.active,
559
+ paused: this.paused,
416
560
  hovered: this.hovered,
417
561
  popup: this.popup,
418
562
  markers: this.markers
@@ -421,6 +565,10 @@ var ElementPickingController = class {
421
565
  get isActive() {
422
566
  return this.active;
423
567
  }
568
+ /** 켜져 있지만 클릭을 안 먹는 상태인가. */
569
+ get isPaused() {
570
+ return this.paused;
571
+ }
424
572
  subscribe(listener) {
425
573
  this.listeners.add(listener);
426
574
  listener(this.getState());
@@ -432,6 +580,7 @@ var ElementPickingController = class {
432
580
  start() {
433
581
  if (this.active) return;
434
582
  this.active = true;
583
+ this.paused = false;
435
584
  this.store.setPickingActive(true);
436
585
  this.attach();
437
586
  this.startPathWatch();
@@ -441,13 +590,41 @@ var ElementPickingController = class {
441
590
  /** [지목 종료]. 저장된 플래그까지 지워서 새로고침해도 다시 켜지지 않게 한다. */
442
591
  stop() {
443
592
  this.active = false;
593
+ this.paused = false;
444
594
  this.store.setPickingActive(false);
445
595
  this.detach();
446
596
  this.stopPathWatch();
597
+ this.hoveredTarget = null;
447
598
  this.hovered = null;
448
599
  this.popup = null;
449
600
  this.emit();
450
601
  }
602
+ /**
603
+ * 잠시 멈춘다 — 페이지 클릭이 원래대로 동작하고, 모드는 켜진 채로 남는다.
604
+ *
605
+ * 팝업이 열려 있으면 **그대로 둔다.** 쓰던 한 줄을 여기서 지우면 「지목 켜면 모달 초안이
606
+ * 날아간다」와 같은 사고를 다른 자리에 만드는 셈이다. 팝업은 위젯 자신의 UI 라
607
+ * 리스너를 떼도 계속 눌린다.
608
+ */
609
+ pause() {
610
+ if (!this.active || this.paused) return;
611
+ this.paused = true;
612
+ this.detach();
613
+ this.hoveredTarget = null;
614
+ this.hovered = null;
615
+ this.emit();
616
+ }
617
+ /** 다시 지목을 받는다. */
618
+ resume() {
619
+ if (!this.active || !this.paused) return;
620
+ this.paused = false;
621
+ this.attach();
622
+ this.emit();
623
+ }
624
+ togglePause() {
625
+ if (this.paused) this.resume();
626
+ else this.pause();
627
+ }
451
628
  /**
452
629
  * 저장돼 있던 모드를 되살린다. 새로고침·페이지 이동 직후에 한 번 부른다.
453
630
  * @returns 되살아났으면 true.
@@ -461,12 +638,30 @@ var ElementPickingController = class {
461
638
  this.start();
462
639
  return true;
463
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
+ }
464
657
  /** 경로가 바뀌었을 때 그 경로의 마커로 갈아 끼운다. */
465
658
  syncPath() {
659
+ if (this.paused) this.resume();
466
660
  this.lastPathname = this.getPathname();
467
661
  this.markers = this.store.list(this.lastPathname);
468
662
  this.reconcileMarkerOutcomes();
469
663
  this.popup = null;
664
+ this.hoveredTarget = null;
470
665
  this.hovered = null;
471
666
  this.emit();
472
667
  return this.markers;
@@ -482,21 +677,30 @@ var ElementPickingController = class {
482
677
  if (this.attached || !this.doc) return;
483
678
  this.doc.addEventListener("click", this.onClick, true);
484
679
  this.doc.addEventListener("mouseover", this.onMouseOver, true);
680
+ this.doc.addEventListener("mousemove", this.onMouseMove, true);
485
681
  this.attached = true;
486
682
  }
487
683
  detach() {
488
684
  if (!this.attached || !this.doc) return;
489
685
  this.doc.removeEventListener("click", this.onClick, true);
490
686
  this.doc.removeEventListener("mouseover", this.onMouseOver, true);
687
+ this.doc.removeEventListener("mousemove", this.onMouseMove, true);
491
688
  this.attached = false;
492
689
  }
493
690
  handleMouseOver(event) {
494
- if (!this.active || isOwnUi(event.target)) return;
495
- this.hovered = describeElement(event.target);
691
+ if (!this.active || this.paused || isOwnUi(event.target)) return;
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);
496
700
  this.emit();
497
701
  }
498
702
  handleClick(event) {
499
- if (!this.active) return;
703
+ if (!this.active || this.paused) return;
500
704
  if (isOwnUi(event.target)) return;
501
705
  event.preventDefault();
502
706
  event.stopPropagation();
@@ -513,11 +717,12 @@ var ElementPickingController = class {
513
717
  /** 클릭 지점 기준으로 주석 팝업을 연다. 좌표는 해상도 무관한 상대값으로 접어 둔다. */
514
718
  openAnnotation(element, clientPoint) {
515
719
  const point = (0, import_feedback_kit_core.normalizePin)(clientPoint, this.getViewport());
720
+ const target = resolvePickTarget(element, clientPoint);
516
721
  if (this.onPick) {
517
- this.onPick(describeElement(element), point);
722
+ this.onPick(describeElement(target), point);
518
723
  return;
519
724
  }
520
- const info = describeElement(element);
725
+ const info = describeElement(target);
521
726
  this.userToggledHints = false;
522
727
  const hints = this.rankHintsFor ? this.rankHintsFor(info) : [];
523
728
  this.popup = {
@@ -577,7 +782,7 @@ ${hint.draft}`;
577
782
  };
578
783
  this.emit();
579
784
  }
580
- /** [제안 ▾] 토글. 방향과 무관하게 이후 자동 접힘/펼침을 멈춘다. */
785
+ /** [이런 건가요? ▾] 토글. 방향과 무관하게 이후 자동 접힘/펼침을 멈춘다. */
581
786
  toggleHints() {
582
787
  if (!this.popup) return;
583
788
  this.userToggledHints = true;
@@ -732,17 +937,17 @@ ${hint.draft}`;
732
937
  return outcome;
733
938
  }
734
939
  startPathWatch() {
735
- if (this.pathWatch !== null || typeof setInterval !== "function") return;
940
+ if (this.unwatchRoutes !== null || !this.watchRoutes) return;
736
941
  this.lastPathname = this.getPathname();
737
- this.pathWatch = setInterval(() => {
942
+ this.unwatchRoutes = this.watchRoutes(() => {
738
943
  const pathname = this.getPathname();
739
944
  if (pathname !== this.lastPathname) this.syncPath();
740
- }, 200);
945
+ });
741
946
  }
742
947
  stopPathWatch() {
743
- if (this.pathWatch === null) return;
744
- clearInterval(this.pathWatch);
745
- this.pathWatch = null;
948
+ if (this.unwatchRoutes === null) return;
949
+ this.unwatchRoutes();
950
+ this.unwatchRoutes = null;
746
951
  }
747
952
  /** 전역 pending 수가 아니라 마커와 같은 clientSubmissionId의 확정 성공만 완료로 바꾼다. */
748
953
  reconcileMarkerOutcomes() {
@@ -1009,7 +1214,7 @@ var import_feedback_kit_core3 = require("@solhun/feedback-kit-core");
1009
1214
  var import_react = require("react");
1010
1215
 
1011
1216
  // src/version.ts
1012
- var VERSION = true ? "0.6.1" : "dev";
1217
+ var VERSION = true ? "0.8.0" : "dev";
1013
1218
 
1014
1219
  // src/feedback-kit.tsx
1015
1220
  var import_jsx_runtime = require("react/jsx-runtime");
@@ -1031,6 +1236,7 @@ var OWN_UI_PROPS = { [OWN_UI_ATTR]: "" };
1031
1236
  var FONT_STACK = "ui-sans-serif, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif";
1032
1237
  var INITIAL_PICKING_STATE = {
1033
1238
  active: false,
1239
+ paused: false,
1034
1240
  hovered: null,
1035
1241
  popup: null,
1036
1242
  markers: []
@@ -1184,6 +1390,7 @@ var inputStyle = {
1184
1390
  };
1185
1391
  function PickToggle({
1186
1392
  active,
1393
+ paused,
1187
1394
  disabled,
1188
1395
  variant,
1189
1396
  focusId,
@@ -1211,14 +1418,9 @@ function PickToggle({
1211
1418
  // 항상 값을 준다. 조건부로 빼면 리렌더 때 shorthand(border)와 충돌한다고 React 가 경고한다.
1212
1419
  borderColor: active ? TOKENS.accent : TOKENS.line,
1213
1420
  ...floating ? {
1214
- position: "fixed",
1215
- top: 16,
1216
- left: "50%",
1217
- transform: "translateX(-50%)",
1218
- // 오버레이 자체는 클릭을 통과시킨다(pointerEvents:none) — 이 토글만 되살린다.
1219
- pointerEvents: "auto",
1220
- background: TOKENS.surface,
1221
- boxShadow: TOKENS.shadow
1421
+ // 위치는 감싸는 `PickBar` 가 잡는다 — 옆에 [잠시 멈춤] 이 함께 서야 하는데
1422
+ // 토글이 스스로 fixed 면 둘을 나란히 둘 수 없다.
1423
+ background: TOKENS.surface
1222
1424
  } : {
1223
1425
  width: "100%",
1224
1426
  justifyContent: "space-between",
@@ -1235,8 +1437,12 @@ function PickToggle({
1235
1437
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1236
1438
  "span",
1237
1439
  {
1238
- style: { fontSize: 12, fontWeight: 600, color: active ? TOKENS.accent : TOKENS.muted },
1239
- children: active ? "\uCF1C\uC9D0" : "\uAEBC\uC9D0"
1440
+ style: {
1441
+ fontSize: 12,
1442
+ fontWeight: 600,
1443
+ color: active && !paused ? TOKENS.accent : TOKENS.muted
1444
+ },
1445
+ children: active ? paused ? "\uC77C\uC2DC\uC911\uC9C0" : "\uCF1C\uC9D0" : "\uAEBC\uC9D0"
1240
1446
  }
1241
1447
  ),
1242
1448
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
@@ -1248,7 +1454,7 @@ function PickToggle({
1248
1454
  width: 38,
1249
1455
  height: 22,
1250
1456
  borderRadius: 11,
1251
- background: active ? TOKENS.accent : TOKENS.line
1457
+ background: active ? paused ? TOKENS.muted : TOKENS.accent : TOKENS.line
1252
1458
  },
1253
1459
  children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1254
1460
  "span",
@@ -1272,6 +1478,93 @@ function PickToggle({
1272
1478
  }
1273
1479
  );
1274
1480
  }
1481
+ function PickBar({
1482
+ paused,
1483
+ markerCount,
1484
+ onToggle,
1485
+ onTogglePause,
1486
+ onClearMarkers
1487
+ }) {
1488
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
1489
+ "div",
1490
+ {
1491
+ style: {
1492
+ position: "fixed",
1493
+ top: 16,
1494
+ // `left:50% + translateX(-50%)` 로 가운데를 잡으면 **쓸 수 있는 폭이 화면의 절반**이
1495
+ // 된다(transform 은 레이아웃 계산에 안 들어간다). 버튼이 셋이 되면서 그 절반에
1496
+ // 안 맞아 좁은 화면에서 버튼이 한 줄에 하나씩 세로로 쌓였다(390px 에서 3줄).
1497
+ // 양끝을 물리고 auto 마진으로 가운데를 잡으면 화면 전체 폭을 쓴다 — 같은 화면이 2줄.
1498
+ left: 0,
1499
+ right: 0,
1500
+ marginInline: "auto",
1501
+ width: "fit-content",
1502
+ maxWidth: "calc(100% - 32px)",
1503
+ display: "flex",
1504
+ alignItems: "stretch",
1505
+ justifyContent: "center",
1506
+ // 셋이 한 줄에 안 들어가는 폭에서는 줄을 바꾼다 — 안 그러면 화면 밖으로 나간다.
1507
+ flexWrap: "wrap",
1508
+ gap: 8,
1509
+ // 오버레이 자체는 클릭을 통과시킨다(pointerEvents:none) — 이 막대만 되살린다.
1510
+ pointerEvents: "auto",
1511
+ padding: 6,
1512
+ borderRadius: 12,
1513
+ background: TOKENS.surface,
1514
+ boxShadow: TOKENS.shadow
1515
+ },
1516
+ children: [
1517
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(PickToggle, { variant: "floating", active: true, paused, onToggle }),
1518
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
1519
+ "button",
1520
+ {
1521
+ type: "button",
1522
+ "aria-pressed": paused,
1523
+ "data-fk-pick-pause": paused ? "paused" : "running",
1524
+ onClick: onTogglePause,
1525
+ style: {
1526
+ ...baseButton,
1527
+ display: "inline-flex",
1528
+ alignItems: "center",
1529
+ gap: 6,
1530
+ whiteSpace: "nowrap",
1531
+ borderColor: paused ? TOKENS.accent : TOKENS.line,
1532
+ color: paused ? TOKENS.accent : TOKENS.ink,
1533
+ fontWeight: 600
1534
+ },
1535
+ children: [
1536
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { "aria-hidden": "true", children: paused ? "\u25B6" : "\u23F8" }),
1537
+ paused ? "\uC9C0\uBAA9 \uC7AC\uAC1C" : "\uC7A0\uC2DC \uBA48\uCDA4"
1538
+ ]
1539
+ }
1540
+ ),
1541
+ markerCount > 0 ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
1542
+ "button",
1543
+ {
1544
+ type: "button",
1545
+ "data-fk-pick-clear": markerCount,
1546
+ title: "\uD654\uBA74\uC758 \uD45C\uC2DC\uB9CC \uC9C0\uC6C1\uB2C8\uB2E4. \uBCF4\uB0B8 \uC81C\uBCF4\uB294 \uADF8\uB300\uB85C \uB0A8\uC2B5\uB2C8\uB2E4.",
1547
+ "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)`,
1548
+ onClick: onClearMarkers,
1549
+ style: {
1550
+ ...baseButton,
1551
+ display: "inline-flex",
1552
+ alignItems: "center",
1553
+ gap: 6,
1554
+ whiteSpace: "nowrap",
1555
+ color: TOKENS.muted
1556
+ },
1557
+ children: [
1558
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { "aria-hidden": "true", children: "\u2715" }),
1559
+ "\uD45C\uC2DC \uC9C0\uC6B0\uAE30 ",
1560
+ markerCount
1561
+ ]
1562
+ }
1563
+ ) : null
1564
+ ]
1565
+ }
1566
+ );
1567
+ }
1275
1568
  function MarkerStatus({ status }) {
1276
1569
  const presentation = status === "sending" ? { icon: "\u21BB", text: "\uC804\uC1A1 \uC911", color: TOKENS.accent } : status === "done" ? { icon: "\u2713", text: "\uC644\uB8CC", color: TOKENS.success } : { icon: "\u25F7", text: "\uB300\uAE30", color: TOKENS.warning };
1277
1570
  return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", { style: { display: "inline-flex", gap: 4, alignItems: "center", color: presentation.color }, children: [
@@ -1295,47 +1588,7 @@ function HintChips({
1295
1588
  onToggle
1296
1589
  }) {
1297
1590
  return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: { marginBottom: 16 }, children: [
1298
- hintsExpanded ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
1299
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { style: { margin: "0 0 7px", color: TOKENS.muted, fontSize: 12 }, children: "\uC774\uB7F0 \uAC74\uAC00\uC694?" }),
1300
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1301
- "div",
1302
- {
1303
- role: "group",
1304
- "aria-label": "\uC790\uC8FC \uB098\uC624\uB294 \uC81C\uBCF4 \uC81C\uC548",
1305
- style: { display: "flex", flexWrap: "wrap", gap: 6, marginBottom: 7 },
1306
- children: hints.map((hint) => {
1307
- const used = usedHintIds.includes(hint.id);
1308
- return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1309
- "button",
1310
- {
1311
- type: "button",
1312
- "data-fk-focus-id": `hint:${hint.id}`,
1313
- disabled: used || disabled,
1314
- "aria-pressed": used,
1315
- onClick: () => onApply(hint.id),
1316
- style: {
1317
- ...baseButton,
1318
- minHeight: 30,
1319
- padding: "5px 10px",
1320
- fontSize: 12,
1321
- fontWeight: 600,
1322
- borderRadius: 999,
1323
- ...used ? {
1324
- background: TOKENS.subtle,
1325
- color: TOKENS.muted,
1326
- borderColor: TOKENS.line,
1327
- cursor: "default"
1328
- } : {}
1329
- },
1330
- children: hint.label
1331
- },
1332
- hint.id
1333
- );
1334
- })
1335
- }
1336
- )
1337
- ] }) : null,
1338
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1591
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
1339
1592
  "button",
1340
1593
  {
1341
1594
  type: "button",
@@ -1343,10 +1596,61 @@ function HintChips({
1343
1596
  "aria-expanded": hintsExpanded,
1344
1597
  disabled,
1345
1598
  onClick: onToggle,
1346
- style: { ...baseButton, minHeight: 28, padding: "4px 10px", fontSize: 12 },
1347
- children: hintsExpanded ? "\uC81C\uC548 \u25B4" : "\uC81C\uC548 \u25BE"
1599
+ style: {
1600
+ display: "block",
1601
+ margin: "0 0 7px",
1602
+ padding: 0,
1603
+ border: "none",
1604
+ background: "none",
1605
+ font: "inherit",
1606
+ fontSize: 12,
1607
+ color: TOKENS.muted,
1608
+ cursor: disabled ? "default" : "pointer",
1609
+ textAlign: "left"
1610
+ },
1611
+ children: [
1612
+ "\uC774\uB7F0 \uAC74\uAC00\uC694? ",
1613
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { "aria-hidden": "true", children: hintsExpanded ? "\u25B4" : "\u25BE" })
1614
+ ]
1348
1615
  }
1349
- )
1616
+ ),
1617
+ hintsExpanded ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_jsx_runtime.Fragment, { children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1618
+ "div",
1619
+ {
1620
+ role: "group",
1621
+ "aria-label": "\uC790\uC8FC \uB098\uC624\uB294 \uC81C\uBCF4 \uC81C\uC548",
1622
+ style: { display: "flex", flexWrap: "wrap", gap: 6, marginBottom: 7 },
1623
+ children: hints.map((hint) => {
1624
+ const used = usedHintIds.includes(hint.id);
1625
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1626
+ "button",
1627
+ {
1628
+ type: "button",
1629
+ "data-fk-focus-id": `hint:${hint.id}`,
1630
+ disabled: used || disabled,
1631
+ "aria-pressed": used,
1632
+ onClick: () => onApply(hint.id),
1633
+ style: {
1634
+ ...baseButton,
1635
+ minHeight: 30,
1636
+ padding: "5px 10px",
1637
+ fontSize: 12,
1638
+ fontWeight: 600,
1639
+ borderRadius: 999,
1640
+ ...used ? {
1641
+ background: TOKENS.subtle,
1642
+ color: TOKENS.muted,
1643
+ borderColor: TOKENS.line,
1644
+ cursor: "default"
1645
+ } : {}
1646
+ },
1647
+ children: hint.label
1648
+ },
1649
+ hint.id
1650
+ );
1651
+ })
1652
+ }
1653
+ ) }) : null
1350
1654
  ] });
1351
1655
  }
1352
1656
  function FeedbackKit(props) {
@@ -1665,6 +1969,7 @@ function FeedbackKit(props) {
1665
1969
  {
1666
1970
  variant: "inline",
1667
1971
  active: pickingActive,
1972
+ paused: pickingState.paused,
1668
1973
  disabled: draftLocked,
1669
1974
  focusId: import_feedback_kit_core3.MODAL_ACTION_PICK,
1670
1975
  onToggle: togglePicking
@@ -1916,7 +2221,16 @@ function FeedbackKit(props) {
1916
2221
  }
1917
2222
  }
1918
2223
  ) : null,
1919
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(PickToggle, { variant: "floating", active: true, onToggle: togglePicking }),
2224
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
2225
+ PickBar,
2226
+ {
2227
+ paused: pickingState.paused,
2228
+ markerCount: pickingState.markers.length,
2229
+ onToggle: togglePicking,
2230
+ onTogglePause: () => activeKit.picking.togglePause(),
2231
+ onClearMarkers: () => activeKit.picking.clearMarkers()
2232
+ }
2233
+ ),
1920
2234
  pickingState.markers.map((marker, index) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1921
2235
  "div",
1922
2236
  {
@@ -2140,6 +2454,13 @@ function webContextProviders() {
2140
2454
  }
2141
2455
  };
2142
2456
  }
2457
+ function webDiagnosticsOptions(opts = {}) {
2458
+ return {
2459
+ watchRoutes: webRouteWatcher,
2460
+ routeStorage: webRouteStorage(),
2461
+ ...opts
2462
+ };
2463
+ }
2143
2464
  // Annotate the CommonJS export names for ESM import in node:
2144
2465
  0 && (module.exports = {
2145
2466
  COMMENT_MAX_CHARS,
@@ -2165,6 +2486,7 @@ function webContextProviders() {
2165
2486
  SUBMIT_PENDING_MESSAGE,
2166
2487
  WidgetController,
2167
2488
  captureWebScreenshot,
2489
+ createRouteWatcher,
2168
2490
  createWebStorage,
2169
2491
  createWebWidget,
2170
2492
  cssSelectorPath,
@@ -2181,6 +2503,9 @@ function webContextProviders() {
2181
2503
  shouldShowWidget,
2182
2504
  sourceFromElement,
2183
2505
  visibleText,
2184
- webContextProviders
2506
+ webContextProviders,
2507
+ webDiagnosticsOptions,
2508
+ webRouteStorage,
2509
+ webRouteWatcher
2185
2510
  });
2186
2511
  //# sourceMappingURL=index.cjs.map