@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 +398 -73
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +111 -5
- package/dist/index.d.ts +111 -5
- package/dist/index.js +393 -72
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
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
|
-
|
|
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
|
-
|
|
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(
|
|
663
|
+
this.onPick(describeElement(target), point);
|
|
463
664
|
return;
|
|
464
665
|
}
|
|
465
|
-
const info = describeElement(
|
|
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.
|
|
881
|
+
if (this.unwatchRoutes !== null || !this.watchRoutes) return;
|
|
681
882
|
this.lastPathname = this.getPathname();
|
|
682
|
-
this.
|
|
883
|
+
this.unwatchRoutes = this.watchRoutes(() => {
|
|
683
884
|
const pathname = this.getPathname();
|
|
684
885
|
if (pathname !== this.lastPathname) this.syncPath();
|
|
685
|
-
}
|
|
886
|
+
});
|
|
686
887
|
}
|
|
687
888
|
stopPathWatch() {
|
|
688
|
-
if (this.
|
|
689
|
-
|
|
690
|
-
this.
|
|
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.
|
|
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
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
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: {
|
|
1203
|
-
|
|
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
|
-
|
|
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: {
|
|
1311
|
-
|
|
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(
|
|
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
|