@solhun/feedback-kit-web 0.3.0 → 0.5.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 +400 -94
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +58 -3
- package/dist/index.d.ts +58 -3
- package/dist/index.js +405 -97
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
package/dist/index.cjs
CHANGED
|
@@ -377,6 +377,10 @@ var ElementPickingController = class {
|
|
|
377
377
|
this.markers = [];
|
|
378
378
|
this.pathWatch = null;
|
|
379
379
|
this.saving = false;
|
|
380
|
+
/** 늦게 끝난 캡처가 다음 주석의 그림을 덮지 못하게 하는 세대 번호. */
|
|
381
|
+
this.shotGeneration = 0;
|
|
382
|
+
/** 지금 도는 자동 캡처. Enter 가 캡처보다 빨랐을 때 기다릴 대상. */
|
|
383
|
+
this.capturing = null;
|
|
380
384
|
this.onClick = (event) => this.handleClick(event);
|
|
381
385
|
this.onMouseOver = (event) => this.handleMouseOver(event);
|
|
382
386
|
this.queue = opts.queue;
|
|
@@ -386,6 +390,9 @@ var ElementPickingController = class {
|
|
|
386
390
|
this.getPathname = opts.getPathname ?? defaultPathname;
|
|
387
391
|
this.doc = opts.doc === void 0 ? defaultDocument() : opts.doc;
|
|
388
392
|
this.getViewport = opts.getViewport ?? defaultViewport;
|
|
393
|
+
this.capture = opts.capture ?? null;
|
|
394
|
+
this.reencode = opts.reencode ?? null;
|
|
395
|
+
this.screenshotLimitBytes = opts.screenshotLimitBytes;
|
|
389
396
|
this.lastPathname = this.getPathname();
|
|
390
397
|
this.markers = this.store.list(this.lastPathname);
|
|
391
398
|
this.unsubscribeQueue = this.queue.subscribe?.(() => {
|
|
@@ -504,24 +511,84 @@ var ElementPickingController = class {
|
|
|
504
511
|
element: describeElement(element),
|
|
505
512
|
point,
|
|
506
513
|
comment: "",
|
|
507
|
-
canSave: false
|
|
514
|
+
canSave: false,
|
|
515
|
+
screenshot: null,
|
|
516
|
+
screenshotStatus: this.capture ? "capturing" : "none",
|
|
517
|
+
saving: false
|
|
508
518
|
};
|
|
509
519
|
this.emit();
|
|
520
|
+
if (this.capture) this.capturing = this.runCapture();
|
|
510
521
|
}
|
|
511
522
|
setAnnotationComment(value) {
|
|
512
523
|
if (!this.popup) return;
|
|
513
524
|
this.popup = {
|
|
514
525
|
...this.popup,
|
|
515
526
|
comment: value,
|
|
516
|
-
canSave:
|
|
527
|
+
canSave: this.isSavable(value)
|
|
517
528
|
};
|
|
518
529
|
this.emit();
|
|
519
530
|
}
|
|
531
|
+
/**
|
|
532
|
+
* 사용자가 붙여넣기(Cmd+V)나 드래그로 넣은 그림. 자동 캡처 결과를 덮는다.
|
|
533
|
+
*
|
|
534
|
+
* 자동 캡처보다 이게 우선인 이유: 사용자가 굳이 그림을 붙였다면 그건 "지금 화면"이
|
|
535
|
+
* 아니라 **보여주고 싶은 다른 것**이다(잘라낸 부분, 다른 탭, 기대하는 디자인).
|
|
536
|
+
*/
|
|
537
|
+
async attachAnnotationImage(shot) {
|
|
538
|
+
if (!this.popup || this.popup.saving) return;
|
|
539
|
+
const generation = ++this.shotGeneration;
|
|
540
|
+
const outcome = await (0, import_feedback_kit_core.captureWithinLimit)({
|
|
541
|
+
capture: () => shot,
|
|
542
|
+
reencode: this.reencode,
|
|
543
|
+
limitBytes: this.screenshotLimitBytes
|
|
544
|
+
});
|
|
545
|
+
if (generation !== this.shotGeneration || !this.popup) return;
|
|
546
|
+
this.popup = {
|
|
547
|
+
...this.popup,
|
|
548
|
+
screenshot: outcome.failed ? null : outcome.screenshot,
|
|
549
|
+
screenshotStatus: outcome.failed || !outcome.screenshot ? "failed" : "ready"
|
|
550
|
+
};
|
|
551
|
+
this.emit();
|
|
552
|
+
}
|
|
553
|
+
/** 그림만 뗀다. 코멘트는 그대로 — 그림이 없어도 주석은 성립한다. */
|
|
554
|
+
removeAnnotationImage() {
|
|
555
|
+
if (!this.popup || this.popup.saving) return;
|
|
556
|
+
this.shotGeneration += 1;
|
|
557
|
+
this.popup = { ...this.popup, screenshot: null, screenshotStatus: "none" };
|
|
558
|
+
this.emit();
|
|
559
|
+
}
|
|
520
560
|
/** 취소 — 마커도 제보도 남기지 않는다. 모드는 켜진 채로 둔다. */
|
|
521
561
|
cancelAnnotation() {
|
|
562
|
+
this.shotGeneration += 1;
|
|
522
563
|
this.popup = null;
|
|
523
564
|
this.emit();
|
|
524
565
|
}
|
|
566
|
+
isSavable(comment) {
|
|
567
|
+
return comment.trim().length > 0 && comment.length <= import_feedback_kit_core.COMMENT_MAX_CHARS;
|
|
568
|
+
}
|
|
569
|
+
/** 도는 캡처가 있으면 끝나기를 기다린다. 캡처가 던져도 저장을 막지 않는다. */
|
|
570
|
+
async settleCapture() {
|
|
571
|
+
try {
|
|
572
|
+
await this.capturing;
|
|
573
|
+
} catch {
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
async runCapture() {
|
|
577
|
+
if (!this.capture) return;
|
|
578
|
+
const generation = ++this.shotGeneration;
|
|
579
|
+
const outcome = await (0, import_feedback_kit_core.captureWithinLimit)({
|
|
580
|
+
capture: this.capture,
|
|
581
|
+
reencode: this.reencode,
|
|
582
|
+
limitBytes: this.screenshotLimitBytes
|
|
583
|
+
});
|
|
584
|
+
if (generation !== this.shotGeneration || !this.popup) return;
|
|
585
|
+
this.popup = {
|
|
586
|
+
...this.popup,
|
|
587
|
+
screenshot: outcome.failed ? null : outcome.screenshot,
|
|
588
|
+
screenshotStatus: outcome.failed || !outcome.screenshot ? "failed" : "ready"
|
|
589
|
+
};
|
|
590
|
+
this.emit();
|
|
591
|
+
}
|
|
525
592
|
/**
|
|
526
593
|
* 제출된 제보를 마커로 남긴다. 모달 경로에서 쓴다 — 마커 생성이 팝업 저장 안에만
|
|
527
594
|
* 있으면, 리포트 모달로 보낸 지목 제보는 화면에 흔적이 남지 않는다("보냈는지 알 수
|
|
@@ -552,14 +619,22 @@ var ElementPickingController = class {
|
|
|
552
619
|
const popup = this.popup;
|
|
553
620
|
if (!popup || !popup.canSave || this.saving) return null;
|
|
554
621
|
this.saving = true;
|
|
622
|
+
if (popup.screenshotStatus === "capturing") await this.settleCapture();
|
|
623
|
+
const settled = this.popup;
|
|
624
|
+
if (!settled) {
|
|
625
|
+
this.saving = false;
|
|
626
|
+
return null;
|
|
627
|
+
}
|
|
628
|
+
this.popup = { ...settled, saving: true };
|
|
629
|
+
this.emit();
|
|
555
630
|
const pathname = this.getPathname();
|
|
556
631
|
const parts = {
|
|
557
632
|
kind: "annotation",
|
|
558
|
-
comment:
|
|
633
|
+
comment: settled.comment,
|
|
559
634
|
priority: "unset",
|
|
560
|
-
screenshot:
|
|
561
|
-
pin:
|
|
562
|
-
element:
|
|
635
|
+
screenshot: settled.screenshot,
|
|
636
|
+
pin: settled.point,
|
|
637
|
+
element: settled.element
|
|
563
638
|
};
|
|
564
639
|
let report;
|
|
565
640
|
try {
|
|
@@ -572,10 +647,10 @@ var ElementPickingController = class {
|
|
|
572
647
|
}
|
|
573
648
|
const marker = {
|
|
574
649
|
id: report.clientSubmissionId,
|
|
575
|
-
x:
|
|
576
|
-
y:
|
|
577
|
-
selector:
|
|
578
|
-
comment:
|
|
650
|
+
x: settled.point.x,
|
|
651
|
+
y: settled.point.y,
|
|
652
|
+
selector: settled.element?.selector ?? null,
|
|
653
|
+
comment: settled.comment,
|
|
579
654
|
status: "sending",
|
|
580
655
|
at: report.createdAt
|
|
581
656
|
};
|
|
@@ -723,6 +798,8 @@ var import_feedback_kit_core2 = require("@solhun/feedback-kit-core");
|
|
|
723
798
|
function createWebWidget(opts) {
|
|
724
799
|
const store = opts.store ?? new MarkerStore();
|
|
725
800
|
let widgetRef = null;
|
|
801
|
+
const capture = opts.capture === void 0 ? captureWebScreenshot : opts.capture;
|
|
802
|
+
const reencode = opts.reencode === void 0 ? reencodeWebScreenshot : opts.reencode;
|
|
726
803
|
const picking = new ElementPickingController({
|
|
727
804
|
queue: opts.queue,
|
|
728
805
|
createReport: opts.createReport,
|
|
@@ -730,18 +807,20 @@ function createWebWidget(opts) {
|
|
|
730
807
|
getPathname: opts.getPathname,
|
|
731
808
|
doc: opts.doc,
|
|
732
809
|
getViewport: opts.getViewport,
|
|
733
|
-
//
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
810
|
+
// 퀵 주석도 같은 캡처를 쓴다. 캡처가 위젯 자신의 UI 를 빼주므로 팝업이 찍히지 않는다.
|
|
811
|
+
capture,
|
|
812
|
+
reencode,
|
|
813
|
+
screenshotLimitBytes: opts.screenshotLimitBytes,
|
|
814
|
+
// 기본은 클릭 자리의 퀵 입력창이라 onPick 을 주지 않는다(주면 팝업 대신 그걸 부른다).
|
|
815
|
+
// 모드는 어느 쪽이든 켜진 채로 둬서 한 번 켜고 여러 요소를 연달아 지목할 수 있다.
|
|
816
|
+
onPick: opts.pickTarget === "modal" ? (element, point) => {
|
|
817
|
+
void widgetRef?.openReport({ element, pin: point });
|
|
818
|
+
} : void 0
|
|
740
819
|
});
|
|
741
820
|
const widget = new import_feedback_kit_core2.WidgetController({
|
|
742
821
|
...opts,
|
|
743
|
-
capture
|
|
744
|
-
reencode
|
|
822
|
+
capture,
|
|
823
|
+
reencode,
|
|
745
824
|
platform: "web",
|
|
746
825
|
// 새로고침·페이지 이동 직후에도 켜져 있던 모드를 그대로 이어받는다.
|
|
747
826
|
initialPicking: store.isPickingActive(),
|
|
@@ -775,7 +854,7 @@ var import_feedback_kit_core3 = require("@solhun/feedback-kit-core");
|
|
|
775
854
|
var import_react = require("react");
|
|
776
855
|
|
|
777
856
|
// src/version.ts
|
|
778
|
-
var VERSION = true ? "0.
|
|
857
|
+
var VERSION = true ? "0.5.0" : "dev";
|
|
779
858
|
|
|
780
859
|
// src/feedback-kit.tsx
|
|
781
860
|
var import_jsx_runtime = require("react/jsx-runtime");
|
|
@@ -804,28 +883,62 @@ var INITIAL_PICKING_STATE = {
|
|
|
804
883
|
function screenshotSource(screenshot) {
|
|
805
884
|
return `data:${screenshot.contentType};base64,${screenshot.base64}`;
|
|
806
885
|
}
|
|
807
|
-
function
|
|
808
|
-
if (file.type !== "image/png" && file.type !== "image/jpeg") {
|
|
809
|
-
return Promise.resolve(null);
|
|
810
|
-
}
|
|
811
|
-
const contentType = file.type;
|
|
886
|
+
function readDataUrl(blob) {
|
|
812
887
|
return new Promise((resolve) => {
|
|
813
888
|
const reader = new FileReader();
|
|
814
889
|
reader.onerror = () => resolve(null);
|
|
815
|
-
reader.onload = () =>
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
890
|
+
reader.onload = () => resolve(typeof reader.result === "string" ? reader.result : null);
|
|
891
|
+
reader.readAsDataURL(blob);
|
|
892
|
+
});
|
|
893
|
+
}
|
|
894
|
+
function base64Of(dataUrl) {
|
|
895
|
+
const comma = dataUrl.indexOf(",");
|
|
896
|
+
return comma >= 0 ? dataUrl.slice(comma + 1) : null;
|
|
897
|
+
}
|
|
898
|
+
async function transcodeToJpeg(dataUrl) {
|
|
899
|
+
const image = await new Promise((resolve) => {
|
|
900
|
+
const img = new Image();
|
|
901
|
+
img.onload = () => resolve(img);
|
|
902
|
+
img.onerror = () => resolve(null);
|
|
903
|
+
img.src = dataUrl;
|
|
828
904
|
});
|
|
905
|
+
if (!image) return null;
|
|
906
|
+
const canvas = document.createElement("canvas");
|
|
907
|
+
canvas.width = image.naturalWidth || image.width;
|
|
908
|
+
canvas.height = image.naturalHeight || image.height;
|
|
909
|
+
if (canvas.width < 1 || canvas.height < 1) return null;
|
|
910
|
+
const context = canvas.getContext("2d");
|
|
911
|
+
if (!context) return null;
|
|
912
|
+
context.drawImage(image, 0, 0);
|
|
913
|
+
try {
|
|
914
|
+
const base64 = base64Of(canvas.toDataURL("image/jpeg", 0.9));
|
|
915
|
+
return base64 ? { base64, contentType: "image/jpeg" } : null;
|
|
916
|
+
} catch {
|
|
917
|
+
return null;
|
|
918
|
+
}
|
|
919
|
+
}
|
|
920
|
+
async function readImageFile(file) {
|
|
921
|
+
if (!file.type.startsWith("image/")) return null;
|
|
922
|
+
const dataUrl = await readDataUrl(file);
|
|
923
|
+
if (!dataUrl) return null;
|
|
924
|
+
if (file.type === "image/png" || file.type === "image/jpeg") {
|
|
925
|
+
const base64 = base64Of(dataUrl);
|
|
926
|
+
return base64 ? { base64, contentType: file.type } : null;
|
|
927
|
+
}
|
|
928
|
+
return transcodeToJpeg(dataUrl);
|
|
929
|
+
}
|
|
930
|
+
function firstImageOf(transfer) {
|
|
931
|
+
if (!transfer) return null;
|
|
932
|
+
for (const item of Array.from(transfer.items ?? [])) {
|
|
933
|
+
if (item.kind === "file" && item.type.startsWith("image/")) {
|
|
934
|
+
const file = item.getAsFile();
|
|
935
|
+
if (file) return file;
|
|
936
|
+
}
|
|
937
|
+
}
|
|
938
|
+
for (const file of Array.from(transfer.files ?? [])) {
|
|
939
|
+
if (file.type.startsWith("image/")) return file;
|
|
940
|
+
}
|
|
941
|
+
return null;
|
|
829
942
|
}
|
|
830
943
|
function nextPaint() {
|
|
831
944
|
return new Promise((resolve) => {
|
|
@@ -878,6 +991,17 @@ var primaryButton = {
|
|
|
878
991
|
background: TOKENS.accent,
|
|
879
992
|
color: TOKENS.surface
|
|
880
993
|
};
|
|
994
|
+
var visuallyHidden = {
|
|
995
|
+
position: "absolute",
|
|
996
|
+
width: 1,
|
|
997
|
+
height: 1,
|
|
998
|
+
margin: -1,
|
|
999
|
+
padding: 0,
|
|
1000
|
+
overflow: "hidden",
|
|
1001
|
+
clip: "rect(0 0 0 0)",
|
|
1002
|
+
whiteSpace: "nowrap",
|
|
1003
|
+
border: 0
|
|
1004
|
+
};
|
|
881
1005
|
var inputStyle = {
|
|
882
1006
|
width: "100%",
|
|
883
1007
|
boxSizing: "border-box",
|
|
@@ -888,6 +1012,96 @@ var inputStyle = {
|
|
|
888
1012
|
padding: "10px 11px",
|
|
889
1013
|
font: "inherit"
|
|
890
1014
|
};
|
|
1015
|
+
function PickToggle({
|
|
1016
|
+
active,
|
|
1017
|
+
disabled,
|
|
1018
|
+
variant,
|
|
1019
|
+
focusId,
|
|
1020
|
+
onToggle
|
|
1021
|
+
}) {
|
|
1022
|
+
const floating = variant === "floating";
|
|
1023
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
|
|
1024
|
+
"button",
|
|
1025
|
+
{
|
|
1026
|
+
type: "button",
|
|
1027
|
+
role: "switch",
|
|
1028
|
+
"aria-checked": active,
|
|
1029
|
+
"aria-label": "\uC694\uC18C \uC9C0\uBAA9 \uBAA8\uB4DC",
|
|
1030
|
+
"data-fk-pick-toggle": active ? "on" : "off",
|
|
1031
|
+
"data-fk-focus-id": focusId,
|
|
1032
|
+
disabled,
|
|
1033
|
+
onClick: onToggle,
|
|
1034
|
+
style: {
|
|
1035
|
+
...baseButton,
|
|
1036
|
+
display: "flex",
|
|
1037
|
+
alignItems: "center",
|
|
1038
|
+
gap: 10,
|
|
1039
|
+
opacity: disabled ? 0.5 : 1,
|
|
1040
|
+
cursor: disabled ? "not-allowed" : "pointer",
|
|
1041
|
+
// 항상 값을 준다. 조건부로 빼면 리렌더 때 shorthand(border)와 충돌한다고 React 가 경고한다.
|
|
1042
|
+
borderColor: active ? TOKENS.accent : TOKENS.line,
|
|
1043
|
+
...floating ? {
|
|
1044
|
+
position: "fixed",
|
|
1045
|
+
top: 16,
|
|
1046
|
+
left: "50%",
|
|
1047
|
+
transform: "translateX(-50%)",
|
|
1048
|
+
// 오버레이 자체는 클릭을 통과시킨다(pointerEvents:none) — 이 토글만 되살린다.
|
|
1049
|
+
pointerEvents: "auto",
|
|
1050
|
+
background: TOKENS.surface,
|
|
1051
|
+
boxShadow: TOKENS.shadow
|
|
1052
|
+
} : {
|
|
1053
|
+
width: "100%",
|
|
1054
|
+
justifyContent: "space-between",
|
|
1055
|
+
marginBottom: 16,
|
|
1056
|
+
background: active ? "rgba(49, 93, 115, 0.06)" : TOKENS.surface
|
|
1057
|
+
}
|
|
1058
|
+
},
|
|
1059
|
+
children: [
|
|
1060
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", { style: { display: "inline-flex", alignItems: "center", gap: 7 }, children: [
|
|
1061
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { "aria-hidden": "true", style: { color: active ? TOKENS.accent : TOKENS.muted }, children: "\u25CE" }),
|
|
1062
|
+
"\uC694\uC18C \uC9C0\uBAA9 \uBAA8\uB4DC"
|
|
1063
|
+
] }),
|
|
1064
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", { "aria-hidden": "true", style: { display: "inline-flex", alignItems: "center", gap: 8 }, children: [
|
|
1065
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
1066
|
+
"span",
|
|
1067
|
+
{
|
|
1068
|
+
style: { fontSize: 12, fontWeight: 600, color: active ? TOKENS.accent : TOKENS.muted },
|
|
1069
|
+
children: active ? "\uCF1C\uC9D0" : "\uAEBC\uC9D0"
|
|
1070
|
+
}
|
|
1071
|
+
),
|
|
1072
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
1073
|
+
"span",
|
|
1074
|
+
{
|
|
1075
|
+
style: {
|
|
1076
|
+
position: "relative",
|
|
1077
|
+
display: "inline-block",
|
|
1078
|
+
width: 38,
|
|
1079
|
+
height: 22,
|
|
1080
|
+
borderRadius: 11,
|
|
1081
|
+
background: active ? TOKENS.accent : TOKENS.line
|
|
1082
|
+
},
|
|
1083
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
1084
|
+
"span",
|
|
1085
|
+
{
|
|
1086
|
+
style: {
|
|
1087
|
+
position: "absolute",
|
|
1088
|
+
top: 3,
|
|
1089
|
+
left: active ? 19 : 3,
|
|
1090
|
+
width: 16,
|
|
1091
|
+
height: 16,
|
|
1092
|
+
borderRadius: "50%",
|
|
1093
|
+
background: TOKENS.surface,
|
|
1094
|
+
boxShadow: "0 1px 3px rgba(23, 32, 42, 0.35)"
|
|
1095
|
+
}
|
|
1096
|
+
}
|
|
1097
|
+
)
|
|
1098
|
+
}
|
|
1099
|
+
)
|
|
1100
|
+
] })
|
|
1101
|
+
]
|
|
1102
|
+
}
|
|
1103
|
+
);
|
|
1104
|
+
}
|
|
891
1105
|
function MarkerStatus({ status }) {
|
|
892
1106
|
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 };
|
|
893
1107
|
return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", { style: { display: "inline-flex", gap: 4, alignItems: "center", color: presentation.color }, children: [
|
|
@@ -976,26 +1190,32 @@ function FeedbackKit(props) {
|
|
|
976
1190
|
(0, import_react.useEffect)(() => {
|
|
977
1191
|
if (!kit || !widgetState) return;
|
|
978
1192
|
const { screen: screen2, modal: modal2 } = widgetState;
|
|
979
|
-
|
|
1193
|
+
const quickOpen = screen2 === "picking" && pickingState.popup !== null;
|
|
1194
|
+
if (screen2 !== "modal" && screen2 !== "pin" && !quickOpen) return;
|
|
980
1195
|
const widget = kit.widget;
|
|
981
1196
|
function onKeyDownCapture(event) {
|
|
982
1197
|
if (event.key !== "Escape") return;
|
|
983
1198
|
if (event.isComposing) return;
|
|
984
1199
|
event.stopPropagation();
|
|
985
1200
|
event.preventDefault();
|
|
986
|
-
if (
|
|
1201
|
+
if (quickOpen) kit.picking.cancelAnnotation();
|
|
1202
|
+
else if (screen2 === "pin") widget.cancelPin();
|
|
987
1203
|
else if (modal2.closeConfirmVisible) widget.cancelCloseReport();
|
|
988
1204
|
else widget.closeReport();
|
|
989
1205
|
}
|
|
990
1206
|
document.addEventListener("keydown", onKeyDownCapture, true);
|
|
991
1207
|
return () => document.removeEventListener("keydown", onKeyDownCapture, true);
|
|
992
|
-
}, [kit, widgetState]);
|
|
1208
|
+
}, [kit, widgetState, pickingState.popup]);
|
|
993
1209
|
if (!kit || !widgetState) {
|
|
994
1210
|
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { ...OWN_UI_PROPS, "data-feedback-kit-loading": "true" });
|
|
995
1211
|
}
|
|
996
1212
|
const activeKit = kit;
|
|
997
|
-
const { modal, screen } = widgetState;
|
|
1213
|
+
const { modal, screen, pickingActive } = widgetState;
|
|
998
1214
|
const draftLocked = modal.submitStatus === "sending" || modal.submitStatus === "pending";
|
|
1215
|
+
function togglePicking() {
|
|
1216
|
+
if (pickingActive) activeKit.widget.stopPicking();
|
|
1217
|
+
else activeKit.widget.startPicking();
|
|
1218
|
+
}
|
|
999
1219
|
async function withOwnUiHidden(action) {
|
|
1000
1220
|
const root = rootRef.current;
|
|
1001
1221
|
const previousVisibility = root?.style.visibility ?? "";
|
|
@@ -1018,11 +1238,19 @@ function FeedbackKit(props) {
|
|
|
1018
1238
|
const input = event.currentTarget;
|
|
1019
1239
|
const file = input.files?.[0];
|
|
1020
1240
|
if (file) {
|
|
1021
|
-
const screenshot = await
|
|
1241
|
+
const screenshot = await readImageFile(file);
|
|
1022
1242
|
if (screenshot) await activeKit.widget.modal.attachFile(screenshot);
|
|
1023
1243
|
}
|
|
1024
1244
|
input.value = "";
|
|
1025
1245
|
}
|
|
1246
|
+
async function attachToPopup(transfer) {
|
|
1247
|
+
const file = firstImageOf(transfer);
|
|
1248
|
+
if (!file) return false;
|
|
1249
|
+
const screenshot = await readImageFile(file);
|
|
1250
|
+
if (!screenshot) return false;
|
|
1251
|
+
await activeKit.picking.attachAnnotationImage(screenshot);
|
|
1252
|
+
return true;
|
|
1253
|
+
}
|
|
1026
1254
|
function closeReport() {
|
|
1027
1255
|
activeKit.widget.closeReport();
|
|
1028
1256
|
}
|
|
@@ -1059,8 +1287,7 @@ function FeedbackKit(props) {
|
|
|
1059
1287
|
ref: floatingButtonRef,
|
|
1060
1288
|
id: import_feedback_kit_core3.FLOATING_BUTTON_ID,
|
|
1061
1289
|
type: "button",
|
|
1062
|
-
|
|
1063
|
-
"aria-label": screen === "picking" ? "\uC694\uC18C \uC9C0\uBAA9 \uC911" : "\uD53C\uB4DC\uBC31 \uBCF4\uB0B4\uAE30",
|
|
1290
|
+
"aria-label": "\uD53C\uB4DC\uBC31 \uBCF4\uB0B4\uAE30",
|
|
1064
1291
|
onClick: () => void openReport(),
|
|
1065
1292
|
style: {
|
|
1066
1293
|
...primaryButton,
|
|
@@ -1071,11 +1298,9 @@ function FeedbackKit(props) {
|
|
|
1071
1298
|
minWidth: 112,
|
|
1072
1299
|
minHeight: 46,
|
|
1073
1300
|
borderRadius: 24,
|
|
1074
|
-
boxShadow: TOKENS.shadow
|
|
1075
|
-
opacity: screen === "picking" ? 0.76 : 1,
|
|
1076
|
-
cursor: screen === "picking" ? "default" : "pointer"
|
|
1301
|
+
boxShadow: TOKENS.shadow
|
|
1077
1302
|
},
|
|
1078
|
-
children:
|
|
1303
|
+
children: "\uD53C\uB4DC\uBC31"
|
|
1079
1304
|
}
|
|
1080
1305
|
) : null,
|
|
1081
1306
|
announcement && !modal.open ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
@@ -1159,7 +1384,17 @@ function FeedbackKit(props) {
|
|
|
1159
1384
|
]
|
|
1160
1385
|
}
|
|
1161
1386
|
),
|
|
1162
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { style: { margin: "0 0
|
|
1387
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { style: { margin: "0 0 14px", color: TOKENS.muted }, children: "\uD604\uC7AC \uD654\uBA74\uC758 \uBB38\uC81C\uB098 \uC758\uACAC\uC744 \uB0A8\uACA8\uC8FC\uC138\uC694." }),
|
|
1388
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
1389
|
+
PickToggle,
|
|
1390
|
+
{
|
|
1391
|
+
variant: "inline",
|
|
1392
|
+
active: pickingActive,
|
|
1393
|
+
disabled: draftLocked,
|
|
1394
|
+
focusId: import_feedback_kit_core3.MODAL_ACTION_PICK,
|
|
1395
|
+
onToggle: togglePicking
|
|
1396
|
+
}
|
|
1397
|
+
),
|
|
1163
1398
|
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("section", { "aria-labelledby": "feedback-kit-screenshot-label", style: { marginBottom: 16 }, children: [
|
|
1164
1399
|
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("strong", { id: "feedback-kit-screenshot-label", children: "\uC2A4\uD06C\uB9B0\uC0F7" }),
|
|
1165
1400
|
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
@@ -1273,19 +1508,6 @@ function FeedbackKit(props) {
|
|
|
1273
1508
|
]
|
|
1274
1509
|
}
|
|
1275
1510
|
),
|
|
1276
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
1277
|
-
"button",
|
|
1278
|
-
{
|
|
1279
|
-
type: "button",
|
|
1280
|
-
role: "switch",
|
|
1281
|
-
"aria-checked": "false",
|
|
1282
|
-
disabled: draftLocked,
|
|
1283
|
-
"data-fk-focus-id": import_feedback_kit_core3.MODAL_ACTION_PICK,
|
|
1284
|
-
onClick: () => kit.widget.startPicking(),
|
|
1285
|
-
style: { ...baseButton, width: "100%", marginTop: 16 },
|
|
1286
|
-
children: "\uC694\uC18C \uC9C0\uBAA9"
|
|
1287
|
-
}
|
|
1288
|
-
),
|
|
1289
1511
|
modal.submitStatus !== "idle" || modal.pending > 0 ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
|
|
1290
1512
|
"div",
|
|
1291
1513
|
{
|
|
@@ -1408,22 +1630,7 @@ function FeedbackKit(props) {
|
|
|
1408
1630
|
}
|
|
1409
1631
|
}
|
|
1410
1632
|
) : null,
|
|
1411
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
1412
|
-
"button",
|
|
1413
|
-
{
|
|
1414
|
-
type: "button",
|
|
1415
|
-
onClick: () => kit.widget.stopPicking(),
|
|
1416
|
-
style: {
|
|
1417
|
-
...primaryButton,
|
|
1418
|
-
position: "fixed",
|
|
1419
|
-
top: 18,
|
|
1420
|
-
right: 18,
|
|
1421
|
-
pointerEvents: "auto",
|
|
1422
|
-
boxShadow: TOKENS.shadow
|
|
1423
|
-
},
|
|
1424
|
-
children: "\uC9C0\uBAA9 \uC885\uB8CC"
|
|
1425
|
-
}
|
|
1426
|
-
),
|
|
1633
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(PickToggle, { variant: "floating", active: true, onToggle: togglePicking }),
|
|
1427
1634
|
pickingState.markers.map((marker, index) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
1428
1635
|
"div",
|
|
1429
1636
|
{
|
|
@@ -1460,48 +1667,147 @@ function FeedbackKit(props) {
|
|
|
1460
1667
|
event.preventDefault();
|
|
1461
1668
|
void kit.picking.saveAnnotation();
|
|
1462
1669
|
},
|
|
1670
|
+
onPaste: (event) => {
|
|
1671
|
+
if (!firstImageOf(event.clipboardData)) return;
|
|
1672
|
+
event.preventDefault();
|
|
1673
|
+
void attachToPopup(event.clipboardData);
|
|
1674
|
+
},
|
|
1675
|
+
onDragOver: (event) => {
|
|
1676
|
+
if (firstImageOf(event.dataTransfer)) event.preventDefault();
|
|
1677
|
+
},
|
|
1678
|
+
onDrop: (event) => {
|
|
1679
|
+
if (!firstImageOf(event.dataTransfer)) return;
|
|
1680
|
+
event.preventDefault();
|
|
1681
|
+
void attachToPopup(event.dataTransfer);
|
|
1682
|
+
},
|
|
1463
1683
|
style: {
|
|
1464
1684
|
position: "fixed",
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1685
|
+
// 클릭한 지점 바로 옆에 띄운다. 화면 밖으로 나가지 않게 양쪽을 물린다.
|
|
1686
|
+
left: `clamp(8px, calc(${popup.point.x * 100}vw + 14px), calc(100vw - 340px))`,
|
|
1687
|
+
top: `clamp(8px, calc(${popup.point.y * 100}vh + 14px), calc(100vh - 200px))`,
|
|
1688
|
+
width: "min(332px, calc(100vw - 16px))",
|
|
1468
1689
|
boxSizing: "border-box",
|
|
1469
1690
|
border: `1px solid ${TOKENS.line}`,
|
|
1470
1691
|
borderRadius: 12,
|
|
1471
1692
|
background: TOKENS.surface,
|
|
1472
|
-
padding:
|
|
1693
|
+
padding: 12,
|
|
1473
1694
|
boxShadow: TOKENS.shadow,
|
|
1474
1695
|
pointerEvents: "auto"
|
|
1475
1696
|
},
|
|
1476
1697
|
children: [
|
|
1477
|
-
/* @__PURE__ */ (0, import_jsx_runtime.
|
|
1478
|
-
|
|
1698
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
|
|
1699
|
+
"div",
|
|
1700
|
+
{
|
|
1701
|
+
style: {
|
|
1702
|
+
display: "flex",
|
|
1703
|
+
alignItems: "center",
|
|
1704
|
+
gap: 6,
|
|
1705
|
+
marginBottom: 7,
|
|
1706
|
+
color: TOKENS.muted,
|
|
1707
|
+
fontSize: 12
|
|
1708
|
+
},
|
|
1709
|
+
children: [
|
|
1710
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { "aria-hidden": "true", children: "\u25CE" }),
|
|
1711
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
1712
|
+
"span",
|
|
1713
|
+
{
|
|
1714
|
+
style: {
|
|
1715
|
+
overflow: "hidden",
|
|
1716
|
+
textOverflow: "ellipsis",
|
|
1717
|
+
whiteSpace: "nowrap"
|
|
1718
|
+
},
|
|
1719
|
+
children: popup.element?.text?.trim() || popup.element?.selector || "\uC774 \uC9C0\uC810"
|
|
1720
|
+
}
|
|
1721
|
+
)
|
|
1722
|
+
]
|
|
1723
|
+
}
|
|
1724
|
+
),
|
|
1725
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("label", { htmlFor: "feedback-kit-annotation", style: visuallyHidden, children: "\uC774 \uC694\uC18C\uC5D0 \uB0A8\uAE38 \uC758\uACAC" }),
|
|
1479
1726
|
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
1480
1727
|
"textarea",
|
|
1481
1728
|
{
|
|
1482
1729
|
id: "feedback-kit-annotation",
|
|
1483
1730
|
autoFocus: true,
|
|
1484
1731
|
value: popup.comment,
|
|
1732
|
+
disabled: popup.saving,
|
|
1485
1733
|
onChange: (event) => kit.picking.setAnnotationComment(event.currentTarget.value),
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1734
|
+
onKeyDown: (event) => {
|
|
1735
|
+
if (event.key !== "Enter" || event.shiftKey) return;
|
|
1736
|
+
const native = event.nativeEvent;
|
|
1737
|
+
if (native.isComposing) return;
|
|
1738
|
+
event.preventDefault();
|
|
1739
|
+
void kit.picking.saveAnnotation();
|
|
1740
|
+
},
|
|
1741
|
+
rows: 2,
|
|
1742
|
+
placeholder: "\uBB34\uC5C7\uC774 \uBB38\uC81C\uC778\uAC00\uC694? (Enter \uB85C \uBCF4\uB0B4\uAE30)",
|
|
1743
|
+
style: { ...inputStyle, resize: "vertical", minHeight: 58 }
|
|
1744
|
+
}
|
|
1745
|
+
),
|
|
1746
|
+
popup.comment.length > import_feedback_kit_core3.COMMENT_MAX_CHARS ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { role: "alert", style: { marginTop: 4, color: TOKENS.danger }, children: "4,000\uC790 \uC774\uD558\uB85C \uC785\uB825\uD574\uC8FC\uC138\uC694" }) : null,
|
|
1747
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
1748
|
+
"div",
|
|
1749
|
+
{
|
|
1750
|
+
style: {
|
|
1751
|
+
display: "flex",
|
|
1752
|
+
alignItems: "center",
|
|
1753
|
+
gap: 8,
|
|
1754
|
+
marginTop: 8,
|
|
1755
|
+
color: TOKENS.muted,
|
|
1756
|
+
fontSize: 12
|
|
1757
|
+
},
|
|
1758
|
+
children: popup.screenshot ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
|
|
1759
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
1760
|
+
"img",
|
|
1761
|
+
{
|
|
1762
|
+
src: screenshotSource(popup.screenshot),
|
|
1763
|
+
alt: "\uC774 \uC8FC\uC11D\uC5D0 \uCCA8\uBD80\uB41C \uD654\uBA74",
|
|
1764
|
+
style: {
|
|
1765
|
+
width: 40,
|
|
1766
|
+
height: 26,
|
|
1767
|
+
objectFit: "cover",
|
|
1768
|
+
borderRadius: 4,
|
|
1769
|
+
border: `1px solid ${TOKENS.line}`
|
|
1770
|
+
}
|
|
1771
|
+
}
|
|
1772
|
+
),
|
|
1773
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { style: { flex: 1 }, children: "\uD654\uBA74 \uCCA8\uBD80\uB428 \xB7 \uBD99\uC5EC\uB123\uAE30\uB85C \uAD50\uCCB4" }),
|
|
1774
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
1775
|
+
"button",
|
|
1776
|
+
{
|
|
1777
|
+
type: "button",
|
|
1778
|
+
disabled: popup.saving,
|
|
1779
|
+
onClick: () => kit.picking.removeAnnotationImage(),
|
|
1780
|
+
style: { ...baseButton, minHeight: 26, padding: "2px 8px", fontSize: 12 },
|
|
1781
|
+
children: "\uC81C\uAC70"
|
|
1782
|
+
}
|
|
1783
|
+
)
|
|
1784
|
+
] }) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { role: "status", style: { flex: 1 }, children: popup.screenshotStatus === "capturing" ? "\uD654\uBA74 \uCEA1\uCC98 \uC911\u2026" : popup.screenshotStatus === "failed" ? "\uCEA1\uCC98 \uC2E4\uD328 \u2014 \uC774\uBBF8\uC9C0\uB97C \uBD99\uC5EC\uB123\uC5B4 \uC8FC\uC138\uC694" : "\uC774\uBBF8\uC9C0\uB97C \uBD99\uC5EC\uB123\uAC70\uB098 \uB04C\uC5B4\uB2E4 \uB193\uC744 \uC218 \uC788\uC5B4\uC694" })
|
|
1489
1785
|
}
|
|
1490
1786
|
),
|
|
1491
|
-
popup.comment.length > 4e3 ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { role: "alert", style: { marginTop: 4, color: TOKENS.danger }, children: "4,000\uC790 \uC774\uD558\uB85C \uC785\uB825\uD574\uC8FC\uC138\uC694" }) : null,
|
|
1492
1787
|
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: { display: "flex", justifyContent: "flex-end", gap: 8, marginTop: 10 }, children: [
|
|
1493
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
1788
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
1789
|
+
"button",
|
|
1790
|
+
{
|
|
1791
|
+
type: "button",
|
|
1792
|
+
disabled: popup.saving,
|
|
1793
|
+
onClick: () => kit.picking.cancelAnnotation(),
|
|
1794
|
+
style: { ...baseButton, minHeight: 32, padding: "5px 10px" },
|
|
1795
|
+
children: "\uCDE8\uC18C"
|
|
1796
|
+
}
|
|
1797
|
+
),
|
|
1494
1798
|
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
1495
1799
|
"button",
|
|
1496
1800
|
{
|
|
1497
1801
|
type: "submit",
|
|
1498
|
-
disabled: !popup.canSave,
|
|
1802
|
+
disabled: !popup.canSave || popup.saving,
|
|
1499
1803
|
style: {
|
|
1500
1804
|
...primaryButton,
|
|
1501
|
-
|
|
1502
|
-
|
|
1805
|
+
minHeight: 32,
|
|
1806
|
+
padding: "5px 12px",
|
|
1807
|
+
opacity: popup.canSave && !popup.saving ? 1 : 0.5,
|
|
1808
|
+
cursor: popup.canSave && !popup.saving ? "pointer" : "not-allowed"
|
|
1503
1809
|
},
|
|
1504
|
-
children: "\
|
|
1810
|
+
children: popup.saving ? "\uBCF4\uB0B4\uB294 \uC911\u2026" : "\uBCF4\uB0B4\uAE30"
|
|
1505
1811
|
}
|
|
1506
1812
|
)
|
|
1507
1813
|
] })
|