@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.js CHANGED
@@ -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;
@@ -320,23 +448,33 @@ var ElementPickingController = class {
320
448
  constructor(opts) {
321
449
  this.listeners = /* @__PURE__ */ new Set();
322
450
  this.active = false;
451
+ /**
452
+ * 일시중지. **저장하지 않는다** — 멈추는 목적이 대개 "링크를 눌러 다른 화면으로 가는 것"
453
+ * 이라, 그 이동이 끝나면 지목이 돌아와 있는 게 목적에 맞다. 같은 이유로 경로가 바뀌면
454
+ * 스스로 풀린다(`syncPath`) — 그래야 SPA 이동과 새로고침이 같게 동작한다.
455
+ */
456
+ this.paused = false;
323
457
  this.attached = false;
458
+ /** 같은 요소 위 mousemove 마다 React 경로·선택자를 다시 만들지 않기 위한 캐시. */
459
+ this.hoveredTarget = null;
324
460
  this.hovered = null;
325
461
  this.popup = null;
326
462
  this.markers = [];
327
- this.pathWatch = null;
463
+ /** 경로 감시 해제. 감시 중이 아니면 null. */
464
+ this.unwatchRoutes = null;
328
465
  this.saving = false;
329
466
  /** 늦게 끝난 캡처가 다음 주석의 그림을 덮지 못하게 하는 세대 번호. */
330
467
  this.shotGeneration = 0;
331
468
  /** 지금 도는 자동 캡처. Enter 가 캡처보다 빨랐을 때 기다릴 대상. */
332
469
  this.capturing = null;
333
470
  /**
334
- * 사용자가 [제안 ▾] 를 직접 건드렸는가. 한 번이라도 건드리면 이후 타이핑에 의한 자동
471
+ * 사용자가 [이런 건가요? ▾] 를 직접 건드렸는가. 한 번이라도 건드리면 이후 타이핑에 의한 자동
335
472
  * 접힘/펼침이 멈춘다 — 모달의 `userToggledHints` 와 같은 규칙. 팝업을 새로 열 때마다 리셋된다.
336
473
  */
337
474
  this.userToggledHints = false;
338
475
  this.onClick = (event) => this.handleClick(event);
339
476
  this.onMouseOver = (event) => this.handleMouseOver(event);
477
+ this.onMouseMove = (event) => this.handleMouseOver(event);
340
478
  this.queue = opts.queue;
341
479
  this.createReport = opts.createReport;
342
480
  this.store = opts.store ?? new MarkerStore();
@@ -348,6 +486,7 @@ var ElementPickingController = class {
348
486
  this.reencode = opts.reencode ?? null;
349
487
  this.screenshotLimitBytes = opts.screenshotLimitBytes;
350
488
  this.rankHintsFor = opts.rankHintsFor ?? null;
489
+ this.watchRoutes = opts.watchRoutes === void 0 ? webRouteWatcher : opts.watchRoutes;
351
490
  this.lastPathname = this.getPathname();
352
491
  this.markers = this.store.list(this.lastPathname);
353
492
  this.unsubscribeQueue = this.queue.subscribe?.(() => {
@@ -358,6 +497,7 @@ var ElementPickingController = class {
358
497
  getState() {
359
498
  return {
360
499
  active: this.active,
500
+ paused: this.paused,
361
501
  hovered: this.hovered,
362
502
  popup: this.popup,
363
503
  markers: this.markers
@@ -366,6 +506,10 @@ var ElementPickingController = class {
366
506
  get isActive() {
367
507
  return this.active;
368
508
  }
509
+ /** 켜져 있지만 클릭을 안 먹는 상태인가. */
510
+ get isPaused() {
511
+ return this.paused;
512
+ }
369
513
  subscribe(listener) {
370
514
  this.listeners.add(listener);
371
515
  listener(this.getState());
@@ -377,6 +521,7 @@ var ElementPickingController = class {
377
521
  start() {
378
522
  if (this.active) return;
379
523
  this.active = true;
524
+ this.paused = false;
380
525
  this.store.setPickingActive(true);
381
526
  this.attach();
382
527
  this.startPathWatch();
@@ -386,13 +531,41 @@ var ElementPickingController = class {
386
531
  /** [지목 종료]. 저장된 플래그까지 지워서 새로고침해도 다시 켜지지 않게 한다. */
387
532
  stop() {
388
533
  this.active = false;
534
+ this.paused = false;
389
535
  this.store.setPickingActive(false);
390
536
  this.detach();
391
537
  this.stopPathWatch();
538
+ this.hoveredTarget = null;
392
539
  this.hovered = null;
393
540
  this.popup = null;
394
541
  this.emit();
395
542
  }
543
+ /**
544
+ * 잠시 멈춘다 — 페이지 클릭이 원래대로 동작하고, 모드는 켜진 채로 남는다.
545
+ *
546
+ * 팝업이 열려 있으면 **그대로 둔다.** 쓰던 한 줄을 여기서 지우면 「지목 켜면 모달 초안이
547
+ * 날아간다」와 같은 사고를 다른 자리에 만드는 셈이다. 팝업은 위젯 자신의 UI 라
548
+ * 리스너를 떼도 계속 눌린다.
549
+ */
550
+ pause() {
551
+ if (!this.active || this.paused) return;
552
+ this.paused = true;
553
+ this.detach();
554
+ this.hoveredTarget = null;
555
+ this.hovered = null;
556
+ this.emit();
557
+ }
558
+ /** 다시 지목을 받는다. */
559
+ resume() {
560
+ if (!this.active || !this.paused) return;
561
+ this.paused = false;
562
+ this.attach();
563
+ this.emit();
564
+ }
565
+ togglePause() {
566
+ if (this.paused) this.resume();
567
+ else this.pause();
568
+ }
396
569
  /**
397
570
  * 저장돼 있던 모드를 되살린다. 새로고침·페이지 이동 직후에 한 번 부른다.
398
571
  * @returns 되살아났으면 true.
@@ -406,12 +579,30 @@ var ElementPickingController = class {
406
579
  this.start();
407
580
  return true;
408
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
+ }
409
598
  /** 경로가 바뀌었을 때 그 경로의 마커로 갈아 끼운다. */
410
599
  syncPath() {
600
+ if (this.paused) this.resume();
411
601
  this.lastPathname = this.getPathname();
412
602
  this.markers = this.store.list(this.lastPathname);
413
603
  this.reconcileMarkerOutcomes();
414
604
  this.popup = null;
605
+ this.hoveredTarget = null;
415
606
  this.hovered = null;
416
607
  this.emit();
417
608
  return this.markers;
@@ -427,21 +618,30 @@ var ElementPickingController = class {
427
618
  if (this.attached || !this.doc) return;
428
619
  this.doc.addEventListener("click", this.onClick, true);
429
620
  this.doc.addEventListener("mouseover", this.onMouseOver, true);
621
+ this.doc.addEventListener("mousemove", this.onMouseMove, true);
430
622
  this.attached = true;
431
623
  }
432
624
  detach() {
433
625
  if (!this.attached || !this.doc) return;
434
626
  this.doc.removeEventListener("click", this.onClick, true);
435
627
  this.doc.removeEventListener("mouseover", this.onMouseOver, true);
628
+ this.doc.removeEventListener("mousemove", this.onMouseMove, true);
436
629
  this.attached = false;
437
630
  }
438
631
  handleMouseOver(event) {
439
- if (!this.active || isOwnUi(event.target)) return;
440
- this.hovered = describeElement(event.target);
632
+ if (!this.active || this.paused || isOwnUi(event.target)) return;
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);
441
641
  this.emit();
442
642
  }
443
643
  handleClick(event) {
444
- if (!this.active) return;
644
+ if (!this.active || this.paused) return;
445
645
  if (isOwnUi(event.target)) return;
446
646
  event.preventDefault();
447
647
  event.stopPropagation();
@@ -458,11 +658,12 @@ var ElementPickingController = class {
458
658
  /** 클릭 지점 기준으로 주석 팝업을 연다. 좌표는 해상도 무관한 상대값으로 접어 둔다. */
459
659
  openAnnotation(element, clientPoint) {
460
660
  const point = normalizePin(clientPoint, this.getViewport());
661
+ const target = resolvePickTarget(element, clientPoint);
461
662
  if (this.onPick) {
462
- this.onPick(describeElement(element), point);
663
+ this.onPick(describeElement(target), point);
463
664
  return;
464
665
  }
465
- const info = describeElement(element);
666
+ const info = describeElement(target);
466
667
  this.userToggledHints = false;
467
668
  const hints = this.rankHintsFor ? this.rankHintsFor(info) : [];
468
669
  this.popup = {
@@ -522,7 +723,7 @@ ${hint.draft}`;
522
723
  };
523
724
  this.emit();
524
725
  }
525
- /** [제안 ▾] 토글. 방향과 무관하게 이후 자동 접힘/펼침을 멈춘다. */
726
+ /** [이런 건가요? ▾] 토글. 방향과 무관하게 이후 자동 접힘/펼침을 멈춘다. */
526
727
  toggleHints() {
527
728
  if (!this.popup) return;
528
729
  this.userToggledHints = true;
@@ -677,17 +878,17 @@ ${hint.draft}`;
677
878
  return outcome;
678
879
  }
679
880
  startPathWatch() {
680
- if (this.pathWatch !== null || typeof setInterval !== "function") return;
881
+ if (this.unwatchRoutes !== null || !this.watchRoutes) return;
681
882
  this.lastPathname = this.getPathname();
682
- this.pathWatch = setInterval(() => {
883
+ this.unwatchRoutes = this.watchRoutes(() => {
683
884
  const pathname = this.getPathname();
684
885
  if (pathname !== this.lastPathname) this.syncPath();
685
- }, 200);
886
+ });
686
887
  }
687
888
  stopPathWatch() {
688
- if (this.pathWatch === null) return;
689
- clearInterval(this.pathWatch);
690
- this.pathWatch = null;
889
+ if (this.unwatchRoutes === null) return;
890
+ this.unwatchRoutes();
891
+ this.unwatchRoutes = null;
691
892
  }
692
893
  /** 전역 pending 수가 아니라 마커와 같은 clientSubmissionId의 확정 성공만 완료로 바꾼다. */
693
894
  reconcileMarkerOutcomes() {
@@ -973,7 +1174,7 @@ import {
973
1174
  } from "react";
974
1175
 
975
1176
  // src/version.ts
976
- var VERSION = true ? "0.6.1" : "dev";
1177
+ var VERSION = true ? "0.8.0" : "dev";
977
1178
 
978
1179
  // src/feedback-kit.tsx
979
1180
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
@@ -995,6 +1196,7 @@ var OWN_UI_PROPS = { [OWN_UI_ATTR]: "" };
995
1196
  var FONT_STACK = "ui-sans-serif, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif";
996
1197
  var INITIAL_PICKING_STATE = {
997
1198
  active: false,
1199
+ paused: false,
998
1200
  hovered: null,
999
1201
  popup: null,
1000
1202
  markers: []
@@ -1148,6 +1350,7 @@ var inputStyle = {
1148
1350
  };
1149
1351
  function PickToggle({
1150
1352
  active,
1353
+ paused,
1151
1354
  disabled,
1152
1355
  variant,
1153
1356
  focusId,
@@ -1175,14 +1378,9 @@ function PickToggle({
1175
1378
  // 항상 값을 준다. 조건부로 빼면 리렌더 때 shorthand(border)와 충돌한다고 React 가 경고한다.
1176
1379
  borderColor: active ? TOKENS.accent : TOKENS.line,
1177
1380
  ...floating ? {
1178
- position: "fixed",
1179
- top: 16,
1180
- left: "50%",
1181
- transform: "translateX(-50%)",
1182
- // 오버레이 자체는 클릭을 통과시킨다(pointerEvents:none) — 이 토글만 되살린다.
1183
- pointerEvents: "auto",
1184
- background: TOKENS.surface,
1185
- boxShadow: TOKENS.shadow
1381
+ // 위치는 감싸는 `PickBar` 가 잡는다 — 옆에 [잠시 멈춤] 이 함께 서야 하는데
1382
+ // 토글이 스스로 fixed 면 둘을 나란히 둘 수 없다.
1383
+ background: TOKENS.surface
1186
1384
  } : {
1187
1385
  width: "100%",
1188
1386
  justifyContent: "space-between",
@@ -1199,8 +1397,12 @@ function PickToggle({
1199
1397
  /* @__PURE__ */ jsx(
1200
1398
  "span",
1201
1399
  {
1202
- style: { fontSize: 12, fontWeight: 600, color: active ? TOKENS.accent : TOKENS.muted },
1203
- children: active ? "\uCF1C\uC9D0" : "\uAEBC\uC9D0"
1400
+ style: {
1401
+ fontSize: 12,
1402
+ fontWeight: 600,
1403
+ color: active && !paused ? TOKENS.accent : TOKENS.muted
1404
+ },
1405
+ children: active ? paused ? "\uC77C\uC2DC\uC911\uC9C0" : "\uCF1C\uC9D0" : "\uAEBC\uC9D0"
1204
1406
  }
1205
1407
  ),
1206
1408
  /* @__PURE__ */ jsx(
@@ -1212,7 +1414,7 @@ function PickToggle({
1212
1414
  width: 38,
1213
1415
  height: 22,
1214
1416
  borderRadius: 11,
1215
- background: active ? TOKENS.accent : TOKENS.line
1417
+ background: active ? paused ? TOKENS.muted : TOKENS.accent : TOKENS.line
1216
1418
  },
1217
1419
  children: /* @__PURE__ */ jsx(
1218
1420
  "span",
@@ -1236,6 +1438,93 @@ function PickToggle({
1236
1438
  }
1237
1439
  );
1238
1440
  }
1441
+ function PickBar({
1442
+ paused,
1443
+ markerCount,
1444
+ onToggle,
1445
+ onTogglePause,
1446
+ onClearMarkers
1447
+ }) {
1448
+ return /* @__PURE__ */ jsxs(
1449
+ "div",
1450
+ {
1451
+ style: {
1452
+ position: "fixed",
1453
+ top: 16,
1454
+ // `left:50% + translateX(-50%)` 로 가운데를 잡으면 **쓸 수 있는 폭이 화면의 절반**이
1455
+ // 된다(transform 은 레이아웃 계산에 안 들어간다). 버튼이 셋이 되면서 그 절반에
1456
+ // 안 맞아 좁은 화면에서 버튼이 한 줄에 하나씩 세로로 쌓였다(390px 에서 3줄).
1457
+ // 양끝을 물리고 auto 마진으로 가운데를 잡으면 화면 전체 폭을 쓴다 — 같은 화면이 2줄.
1458
+ left: 0,
1459
+ right: 0,
1460
+ marginInline: "auto",
1461
+ width: "fit-content",
1462
+ maxWidth: "calc(100% - 32px)",
1463
+ display: "flex",
1464
+ alignItems: "stretch",
1465
+ justifyContent: "center",
1466
+ // 셋이 한 줄에 안 들어가는 폭에서는 줄을 바꾼다 — 안 그러면 화면 밖으로 나간다.
1467
+ flexWrap: "wrap",
1468
+ gap: 8,
1469
+ // 오버레이 자체는 클릭을 통과시킨다(pointerEvents:none) — 이 막대만 되살린다.
1470
+ pointerEvents: "auto",
1471
+ padding: 6,
1472
+ borderRadius: 12,
1473
+ background: TOKENS.surface,
1474
+ boxShadow: TOKENS.shadow
1475
+ },
1476
+ children: [
1477
+ /* @__PURE__ */ jsx(PickToggle, { variant: "floating", active: true, paused, onToggle }),
1478
+ /* @__PURE__ */ jsxs(
1479
+ "button",
1480
+ {
1481
+ type: "button",
1482
+ "aria-pressed": paused,
1483
+ "data-fk-pick-pause": paused ? "paused" : "running",
1484
+ onClick: onTogglePause,
1485
+ style: {
1486
+ ...baseButton,
1487
+ display: "inline-flex",
1488
+ alignItems: "center",
1489
+ gap: 6,
1490
+ whiteSpace: "nowrap",
1491
+ borderColor: paused ? TOKENS.accent : TOKENS.line,
1492
+ color: paused ? TOKENS.accent : TOKENS.ink,
1493
+ fontWeight: 600
1494
+ },
1495
+ children: [
1496
+ /* @__PURE__ */ jsx("span", { "aria-hidden": "true", children: paused ? "\u25B6" : "\u23F8" }),
1497
+ paused ? "\uC9C0\uBAA9 \uC7AC\uAC1C" : "\uC7A0\uC2DC \uBA48\uCDA4"
1498
+ ]
1499
+ }
1500
+ ),
1501
+ markerCount > 0 ? /* @__PURE__ */ jsxs(
1502
+ "button",
1503
+ {
1504
+ type: "button",
1505
+ "data-fk-pick-clear": markerCount,
1506
+ title: "\uD654\uBA74\uC758 \uD45C\uC2DC\uB9CC \uC9C0\uC6C1\uB2C8\uB2E4. \uBCF4\uB0B8 \uC81C\uBCF4\uB294 \uADF8\uB300\uB85C \uB0A8\uC2B5\uB2C8\uB2E4.",
1507
+ "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)`,
1508
+ onClick: onClearMarkers,
1509
+ style: {
1510
+ ...baseButton,
1511
+ display: "inline-flex",
1512
+ alignItems: "center",
1513
+ gap: 6,
1514
+ whiteSpace: "nowrap",
1515
+ color: TOKENS.muted
1516
+ },
1517
+ children: [
1518
+ /* @__PURE__ */ jsx("span", { "aria-hidden": "true", children: "\u2715" }),
1519
+ "\uD45C\uC2DC \uC9C0\uC6B0\uAE30 ",
1520
+ markerCount
1521
+ ]
1522
+ }
1523
+ ) : null
1524
+ ]
1525
+ }
1526
+ );
1527
+ }
1239
1528
  function MarkerStatus({ status }) {
1240
1529
  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 };
1241
1530
  return /* @__PURE__ */ jsxs("span", { style: { display: "inline-flex", gap: 4, alignItems: "center", color: presentation.color }, children: [
@@ -1259,47 +1548,7 @@ function HintChips({
1259
1548
  onToggle
1260
1549
  }) {
1261
1550
  return /* @__PURE__ */ jsxs("div", { style: { marginBottom: 16 }, children: [
1262
- hintsExpanded ? /* @__PURE__ */ jsxs(Fragment, { children: [
1263
- /* @__PURE__ */ jsx("p", { style: { margin: "0 0 7px", color: TOKENS.muted, fontSize: 12 }, children: "\uC774\uB7F0 \uAC74\uAC00\uC694?" }),
1264
- /* @__PURE__ */ jsx(
1265
- "div",
1266
- {
1267
- role: "group",
1268
- "aria-label": "\uC790\uC8FC \uB098\uC624\uB294 \uC81C\uBCF4 \uC81C\uC548",
1269
- style: { display: "flex", flexWrap: "wrap", gap: 6, marginBottom: 7 },
1270
- children: hints.map((hint) => {
1271
- const used = usedHintIds.includes(hint.id);
1272
- return /* @__PURE__ */ jsx(
1273
- "button",
1274
- {
1275
- type: "button",
1276
- "data-fk-focus-id": `hint:${hint.id}`,
1277
- disabled: used || disabled,
1278
- "aria-pressed": used,
1279
- onClick: () => onApply(hint.id),
1280
- style: {
1281
- ...baseButton,
1282
- minHeight: 30,
1283
- padding: "5px 10px",
1284
- fontSize: 12,
1285
- fontWeight: 600,
1286
- borderRadius: 999,
1287
- ...used ? {
1288
- background: TOKENS.subtle,
1289
- color: TOKENS.muted,
1290
- borderColor: TOKENS.line,
1291
- cursor: "default"
1292
- } : {}
1293
- },
1294
- children: hint.label
1295
- },
1296
- hint.id
1297
- );
1298
- })
1299
- }
1300
- )
1301
- ] }) : null,
1302
- /* @__PURE__ */ jsx(
1551
+ /* @__PURE__ */ jsxs(
1303
1552
  "button",
1304
1553
  {
1305
1554
  type: "button",
@@ -1307,10 +1556,61 @@ function HintChips({
1307
1556
  "aria-expanded": hintsExpanded,
1308
1557
  disabled,
1309
1558
  onClick: onToggle,
1310
- style: { ...baseButton, minHeight: 28, padding: "4px 10px", fontSize: 12 },
1311
- children: hintsExpanded ? "\uC81C\uC548 \u25B4" : "\uC81C\uC548 \u25BE"
1559
+ style: {
1560
+ display: "block",
1561
+ margin: "0 0 7px",
1562
+ padding: 0,
1563
+ border: "none",
1564
+ background: "none",
1565
+ font: "inherit",
1566
+ fontSize: 12,
1567
+ color: TOKENS.muted,
1568
+ cursor: disabled ? "default" : "pointer",
1569
+ textAlign: "left"
1570
+ },
1571
+ children: [
1572
+ "\uC774\uB7F0 \uAC74\uAC00\uC694? ",
1573
+ /* @__PURE__ */ jsx("span", { "aria-hidden": "true", children: hintsExpanded ? "\u25B4" : "\u25BE" })
1574
+ ]
1312
1575
  }
1313
- )
1576
+ ),
1577
+ hintsExpanded ? /* @__PURE__ */ jsx(Fragment, { children: /* @__PURE__ */ jsx(
1578
+ "div",
1579
+ {
1580
+ role: "group",
1581
+ "aria-label": "\uC790\uC8FC \uB098\uC624\uB294 \uC81C\uBCF4 \uC81C\uC548",
1582
+ style: { display: "flex", flexWrap: "wrap", gap: 6, marginBottom: 7 },
1583
+ children: hints.map((hint) => {
1584
+ const used = usedHintIds.includes(hint.id);
1585
+ return /* @__PURE__ */ jsx(
1586
+ "button",
1587
+ {
1588
+ type: "button",
1589
+ "data-fk-focus-id": `hint:${hint.id}`,
1590
+ disabled: used || disabled,
1591
+ "aria-pressed": used,
1592
+ onClick: () => onApply(hint.id),
1593
+ style: {
1594
+ ...baseButton,
1595
+ minHeight: 30,
1596
+ padding: "5px 10px",
1597
+ fontSize: 12,
1598
+ fontWeight: 600,
1599
+ borderRadius: 999,
1600
+ ...used ? {
1601
+ background: TOKENS.subtle,
1602
+ color: TOKENS.muted,
1603
+ borderColor: TOKENS.line,
1604
+ cursor: "default"
1605
+ } : {}
1606
+ },
1607
+ children: hint.label
1608
+ },
1609
+ hint.id
1610
+ );
1611
+ })
1612
+ }
1613
+ ) }) : null
1314
1614
  ] });
1315
1615
  }
1316
1616
  function FeedbackKit(props) {
@@ -1629,6 +1929,7 @@ function FeedbackKit(props) {
1629
1929
  {
1630
1930
  variant: "inline",
1631
1931
  active: pickingActive,
1932
+ paused: pickingState.paused,
1632
1933
  disabled: draftLocked,
1633
1934
  focusId: MODAL_ACTION_PICK,
1634
1935
  onToggle: togglePicking
@@ -1880,7 +2181,16 @@ function FeedbackKit(props) {
1880
2181
  }
1881
2182
  }
1882
2183
  ) : null,
1883
- /* @__PURE__ */ jsx(PickToggle, { variant: "floating", active: true, onToggle: togglePicking }),
2184
+ /* @__PURE__ */ jsx(
2185
+ PickBar,
2186
+ {
2187
+ paused: pickingState.paused,
2188
+ markerCount: pickingState.markers.length,
2189
+ onToggle: togglePicking,
2190
+ onTogglePause: () => activeKit.picking.togglePause(),
2191
+ onClearMarkers: () => activeKit.picking.clearMarkers()
2192
+ }
2193
+ ),
1884
2194
  pickingState.markers.map((marker, index) => /* @__PURE__ */ jsx(
1885
2195
  "div",
1886
2196
  {
@@ -2104,6 +2414,13 @@ function webContextProviders() {
2104
2414
  }
2105
2415
  };
2106
2416
  }
2417
+ function webDiagnosticsOptions(opts = {}) {
2418
+ return {
2419
+ watchRoutes: webRouteWatcher,
2420
+ routeStorage: webRouteStorage(),
2421
+ ...opts
2422
+ };
2423
+ }
2107
2424
  export {
2108
2425
  COMMENT_MAX_CHARS3 as COMMENT_MAX_CHARS,
2109
2426
  COMMENT_REQUIRED_MESSAGE,
@@ -2128,6 +2445,7 @@ export {
2128
2445
  SUBMIT_PENDING_MESSAGE,
2129
2446
  WidgetController2 as WidgetController,
2130
2447
  captureWebScreenshot,
2448
+ createRouteWatcher,
2131
2449
  createWebStorage,
2132
2450
  createWebWidget,
2133
2451
  cssSelectorPath,
@@ -2144,6 +2462,9 @@ export {
2144
2462
  shouldShowWidget,
2145
2463
  sourceFromElement,
2146
2464
  visibleText,
2147
- webContextProviders
2465
+ webContextProviders,
2466
+ webDiagnosticsOptions,
2467
+ webRouteStorage,
2468
+ webRouteWatcher
2148
2469
  };
2149
2470
  //# sourceMappingURL=index.js.map