@solhun/feedback-kit-web 0.7.0 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +462 -53
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +99 -3
- package/dist/index.d.ts +99 -3
- package/dist/index.js +426 -21
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
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
|
-
|
|
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
|
-
|
|
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(
|
|
663
|
+
this.onPick(describeElement(target), point);
|
|
502
664
|
return;
|
|
503
665
|
}
|
|
504
|
-
const info = describeElement(
|
|
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.
|
|
881
|
+
if (this.unwatchRoutes !== null || !this.watchRoutes) return;
|
|
720
882
|
this.lastPathname = this.getPathname();
|
|
721
|
-
this.
|
|
883
|
+
this.unwatchRoutes = this.watchRoutes(() => {
|
|
722
884
|
const pathname = this.getPathname();
|
|
723
885
|
if (pathname !== this.lastPathname) this.syncPath();
|
|
724
|
-
}
|
|
886
|
+
});
|
|
725
887
|
}
|
|
726
888
|
stopPathWatch() {
|
|
727
|
-
if (this.
|
|
728
|
-
|
|
729
|
-
this.
|
|
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.
|
|
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:
|
|
1291
|
-
transform
|
|
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
|
|
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
|