@solhun/feedback-kit-web 0.2.3 → 0.4.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 +322 -57
- 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 +327 -60
- 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)
|
|
528
|
+
};
|
|
529
|
+
this.emit();
|
|
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"
|
|
517
550
|
};
|
|
518
551
|
this.emit();
|
|
519
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(),
|
|
@@ -773,6 +852,11 @@ function createWebWidget(opts) {
|
|
|
773
852
|
// src/feedback-kit.tsx
|
|
774
853
|
var import_feedback_kit_core3 = require("@solhun/feedback-kit-core");
|
|
775
854
|
var import_react = require("react");
|
|
855
|
+
|
|
856
|
+
// src/version.ts
|
|
857
|
+
var VERSION = true ? "0.4.0" : "dev";
|
|
858
|
+
|
|
859
|
+
// src/feedback-kit.tsx
|
|
776
860
|
var import_jsx_runtime = require("react/jsx-runtime");
|
|
777
861
|
var TOKENS = {
|
|
778
862
|
ink: "#17202a",
|
|
@@ -799,29 +883,63 @@ var INITIAL_PICKING_STATE = {
|
|
|
799
883
|
function screenshotSource(screenshot) {
|
|
800
884
|
return `data:${screenshot.contentType};base64,${screenshot.base64}`;
|
|
801
885
|
}
|
|
802
|
-
function
|
|
803
|
-
if (file.type !== "image/png" && file.type !== "image/jpeg") {
|
|
804
|
-
return Promise.resolve(null);
|
|
805
|
-
}
|
|
806
|
-
const contentType = file.type;
|
|
886
|
+
function readDataUrl(blob) {
|
|
807
887
|
return new Promise((resolve) => {
|
|
808
888
|
const reader = new FileReader();
|
|
809
889
|
reader.onerror = () => resolve(null);
|
|
810
|
-
reader.onload = () =>
|
|
811
|
-
|
|
812
|
-
const comma = result.indexOf(",");
|
|
813
|
-
if (comma < 0) {
|
|
814
|
-
resolve(null);
|
|
815
|
-
return;
|
|
816
|
-
}
|
|
817
|
-
resolve({
|
|
818
|
-
base64: result.slice(comma + 1),
|
|
819
|
-
contentType
|
|
820
|
-
});
|
|
821
|
-
};
|
|
822
|
-
reader.readAsDataURL(file);
|
|
890
|
+
reader.onload = () => resolve(typeof reader.result === "string" ? reader.result : null);
|
|
891
|
+
reader.readAsDataURL(blob);
|
|
823
892
|
});
|
|
824
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;
|
|
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;
|
|
942
|
+
}
|
|
825
943
|
function nextPaint() {
|
|
826
944
|
return new Promise((resolve) => {
|
|
827
945
|
if (typeof requestAnimationFrame === "function") {
|
|
@@ -873,6 +991,17 @@ var primaryButton = {
|
|
|
873
991
|
background: TOKENS.accent,
|
|
874
992
|
color: TOKENS.surface
|
|
875
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
|
+
};
|
|
876
1005
|
var inputStyle = {
|
|
877
1006
|
width: "100%",
|
|
878
1007
|
boxSizing: "border-box",
|
|
@@ -971,20 +1100,22 @@ function FeedbackKit(props) {
|
|
|
971
1100
|
(0, import_react.useEffect)(() => {
|
|
972
1101
|
if (!kit || !widgetState) return;
|
|
973
1102
|
const { screen: screen2, modal: modal2 } = widgetState;
|
|
974
|
-
|
|
1103
|
+
const quickOpen = screen2 === "picking" && pickingState.popup !== null;
|
|
1104
|
+
if (screen2 !== "modal" && screen2 !== "pin" && !quickOpen) return;
|
|
975
1105
|
const widget = kit.widget;
|
|
976
1106
|
function onKeyDownCapture(event) {
|
|
977
1107
|
if (event.key !== "Escape") return;
|
|
978
1108
|
if (event.isComposing) return;
|
|
979
1109
|
event.stopPropagation();
|
|
980
1110
|
event.preventDefault();
|
|
981
|
-
if (
|
|
1111
|
+
if (quickOpen) kit.picking.cancelAnnotation();
|
|
1112
|
+
else if (screen2 === "pin") widget.cancelPin();
|
|
982
1113
|
else if (modal2.closeConfirmVisible) widget.cancelCloseReport();
|
|
983
1114
|
else widget.closeReport();
|
|
984
1115
|
}
|
|
985
1116
|
document.addEventListener("keydown", onKeyDownCapture, true);
|
|
986
1117
|
return () => document.removeEventListener("keydown", onKeyDownCapture, true);
|
|
987
|
-
}, [kit, widgetState]);
|
|
1118
|
+
}, [kit, widgetState, pickingState.popup]);
|
|
988
1119
|
if (!kit || !widgetState) {
|
|
989
1120
|
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { ...OWN_UI_PROPS, "data-feedback-kit-loading": "true" });
|
|
990
1121
|
}
|
|
@@ -1013,11 +1144,19 @@ function FeedbackKit(props) {
|
|
|
1013
1144
|
const input = event.currentTarget;
|
|
1014
1145
|
const file = input.files?.[0];
|
|
1015
1146
|
if (file) {
|
|
1016
|
-
const screenshot = await
|
|
1147
|
+
const screenshot = await readImageFile(file);
|
|
1017
1148
|
if (screenshot) await activeKit.widget.modal.attachFile(screenshot);
|
|
1018
1149
|
}
|
|
1019
1150
|
input.value = "";
|
|
1020
1151
|
}
|
|
1152
|
+
async function attachToPopup(transfer) {
|
|
1153
|
+
const file = firstImageOf(transfer);
|
|
1154
|
+
if (!file) return false;
|
|
1155
|
+
const screenshot = await readImageFile(file);
|
|
1156
|
+
if (!screenshot) return false;
|
|
1157
|
+
await activeKit.picking.attachAnnotationImage(screenshot);
|
|
1158
|
+
return true;
|
|
1159
|
+
}
|
|
1021
1160
|
function closeReport() {
|
|
1022
1161
|
activeKit.widget.closeReport();
|
|
1023
1162
|
}
|
|
@@ -1126,7 +1265,34 @@ function FeedbackKit(props) {
|
|
|
1126
1265
|
},
|
|
1127
1266
|
children: [
|
|
1128
1267
|
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("form", { onSubmit: submitReport, children: [
|
|
1129
|
-
/* @__PURE__ */ (0, import_jsx_runtime.
|
|
1268
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
|
|
1269
|
+
"h2",
|
|
1270
|
+
{
|
|
1271
|
+
id: "feedback-kit-dialog-title",
|
|
1272
|
+
style: {
|
|
1273
|
+
margin: "0 0 4px",
|
|
1274
|
+
fontSize: 20,
|
|
1275
|
+
display: "flex",
|
|
1276
|
+
alignItems: "baseline",
|
|
1277
|
+
gap: 8
|
|
1278
|
+
},
|
|
1279
|
+
children: [
|
|
1280
|
+
"\uD53C\uB4DC\uBC31 \uBCF4\uB0B4\uAE30",
|
|
1281
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
|
|
1282
|
+
"span",
|
|
1283
|
+
{
|
|
1284
|
+
"data-feedback-kit-version": VERSION,
|
|
1285
|
+
title: "feedback-kit \uBC84\uC804",
|
|
1286
|
+
style: { fontSize: 11, fontWeight: 400, color: TOKENS.muted },
|
|
1287
|
+
children: [
|
|
1288
|
+
"v",
|
|
1289
|
+
VERSION
|
|
1290
|
+
]
|
|
1291
|
+
}
|
|
1292
|
+
)
|
|
1293
|
+
]
|
|
1294
|
+
}
|
|
1295
|
+
),
|
|
1130
1296
|
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { style: { margin: "0 0 18px", color: TOKENS.muted }, children: "\uD604\uC7AC \uD654\uBA74\uC758 \uBB38\uC81C\uB098 \uC758\uACAC\uC744 \uB0A8\uACA8\uC8FC\uC138\uC694." }),
|
|
1131
1297
|
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("section", { "aria-labelledby": "feedback-kit-screenshot-label", style: { marginBottom: 16 }, children: [
|
|
1132
1298
|
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("strong", { id: "feedback-kit-screenshot-label", children: "\uC2A4\uD06C\uB9B0\uC0F7" }),
|
|
@@ -1428,48 +1594,147 @@ function FeedbackKit(props) {
|
|
|
1428
1594
|
event.preventDefault();
|
|
1429
1595
|
void kit.picking.saveAnnotation();
|
|
1430
1596
|
},
|
|
1597
|
+
onPaste: (event) => {
|
|
1598
|
+
if (!firstImageOf(event.clipboardData)) return;
|
|
1599
|
+
event.preventDefault();
|
|
1600
|
+
void attachToPopup(event.clipboardData);
|
|
1601
|
+
},
|
|
1602
|
+
onDragOver: (event) => {
|
|
1603
|
+
if (firstImageOf(event.dataTransfer)) event.preventDefault();
|
|
1604
|
+
},
|
|
1605
|
+
onDrop: (event) => {
|
|
1606
|
+
if (!firstImageOf(event.dataTransfer)) return;
|
|
1607
|
+
event.preventDefault();
|
|
1608
|
+
void attachToPopup(event.dataTransfer);
|
|
1609
|
+
},
|
|
1431
1610
|
style: {
|
|
1432
1611
|
position: "fixed",
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1612
|
+
// 클릭한 지점 바로 옆에 띄운다. 화면 밖으로 나가지 않게 양쪽을 물린다.
|
|
1613
|
+
left: `clamp(8px, calc(${popup.point.x * 100}vw + 14px), calc(100vw - 340px))`,
|
|
1614
|
+
top: `clamp(8px, calc(${popup.point.y * 100}vh + 14px), calc(100vh - 200px))`,
|
|
1615
|
+
width: "min(332px, calc(100vw - 16px))",
|
|
1436
1616
|
boxSizing: "border-box",
|
|
1437
1617
|
border: `1px solid ${TOKENS.line}`,
|
|
1438
1618
|
borderRadius: 12,
|
|
1439
1619
|
background: TOKENS.surface,
|
|
1440
|
-
padding:
|
|
1620
|
+
padding: 12,
|
|
1441
1621
|
boxShadow: TOKENS.shadow,
|
|
1442
1622
|
pointerEvents: "auto"
|
|
1443
1623
|
},
|
|
1444
1624
|
children: [
|
|
1445
|
-
/* @__PURE__ */ (0, import_jsx_runtime.
|
|
1446
|
-
|
|
1625
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
|
|
1626
|
+
"div",
|
|
1627
|
+
{
|
|
1628
|
+
style: {
|
|
1629
|
+
display: "flex",
|
|
1630
|
+
alignItems: "center",
|
|
1631
|
+
gap: 6,
|
|
1632
|
+
marginBottom: 7,
|
|
1633
|
+
color: TOKENS.muted,
|
|
1634
|
+
fontSize: 12
|
|
1635
|
+
},
|
|
1636
|
+
children: [
|
|
1637
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { "aria-hidden": "true", children: "\u25CE" }),
|
|
1638
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
1639
|
+
"span",
|
|
1640
|
+
{
|
|
1641
|
+
style: {
|
|
1642
|
+
overflow: "hidden",
|
|
1643
|
+
textOverflow: "ellipsis",
|
|
1644
|
+
whiteSpace: "nowrap"
|
|
1645
|
+
},
|
|
1646
|
+
children: popup.element?.text?.trim() || popup.element?.selector || "\uC774 \uC9C0\uC810"
|
|
1647
|
+
}
|
|
1648
|
+
)
|
|
1649
|
+
]
|
|
1650
|
+
}
|
|
1651
|
+
),
|
|
1652
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("label", { htmlFor: "feedback-kit-annotation", style: visuallyHidden, children: "\uC774 \uC694\uC18C\uC5D0 \uB0A8\uAE38 \uC758\uACAC" }),
|
|
1447
1653
|
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
1448
1654
|
"textarea",
|
|
1449
1655
|
{
|
|
1450
1656
|
id: "feedback-kit-annotation",
|
|
1451
1657
|
autoFocus: true,
|
|
1452
1658
|
value: popup.comment,
|
|
1659
|
+
disabled: popup.saving,
|
|
1453
1660
|
onChange: (event) => kit.picking.setAnnotationComment(event.currentTarget.value),
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1661
|
+
onKeyDown: (event) => {
|
|
1662
|
+
if (event.key !== "Enter" || event.shiftKey) return;
|
|
1663
|
+
const native = event.nativeEvent;
|
|
1664
|
+
if (native.isComposing) return;
|
|
1665
|
+
event.preventDefault();
|
|
1666
|
+
void kit.picking.saveAnnotation();
|
|
1667
|
+
},
|
|
1668
|
+
rows: 2,
|
|
1669
|
+
placeholder: "\uBB34\uC5C7\uC774 \uBB38\uC81C\uC778\uAC00\uC694? (Enter \uB85C \uBCF4\uB0B4\uAE30)",
|
|
1670
|
+
style: { ...inputStyle, resize: "vertical", minHeight: 58 }
|
|
1671
|
+
}
|
|
1672
|
+
),
|
|
1673
|
+
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,
|
|
1674
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
1675
|
+
"div",
|
|
1676
|
+
{
|
|
1677
|
+
style: {
|
|
1678
|
+
display: "flex",
|
|
1679
|
+
alignItems: "center",
|
|
1680
|
+
gap: 8,
|
|
1681
|
+
marginTop: 8,
|
|
1682
|
+
color: TOKENS.muted,
|
|
1683
|
+
fontSize: 12
|
|
1684
|
+
},
|
|
1685
|
+
children: popup.screenshot ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
|
|
1686
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
1687
|
+
"img",
|
|
1688
|
+
{
|
|
1689
|
+
src: screenshotSource(popup.screenshot),
|
|
1690
|
+
alt: "\uC774 \uC8FC\uC11D\uC5D0 \uCCA8\uBD80\uB41C \uD654\uBA74",
|
|
1691
|
+
style: {
|
|
1692
|
+
width: 40,
|
|
1693
|
+
height: 26,
|
|
1694
|
+
objectFit: "cover",
|
|
1695
|
+
borderRadius: 4,
|
|
1696
|
+
border: `1px solid ${TOKENS.line}`
|
|
1697
|
+
}
|
|
1698
|
+
}
|
|
1699
|
+
),
|
|
1700
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { style: { flex: 1 }, children: "\uD654\uBA74 \uCCA8\uBD80\uB428 \xB7 \uBD99\uC5EC\uB123\uAE30\uB85C \uAD50\uCCB4" }),
|
|
1701
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
1702
|
+
"button",
|
|
1703
|
+
{
|
|
1704
|
+
type: "button",
|
|
1705
|
+
disabled: popup.saving,
|
|
1706
|
+
onClick: () => kit.picking.removeAnnotationImage(),
|
|
1707
|
+
style: { ...baseButton, minHeight: 26, padding: "2px 8px", fontSize: 12 },
|
|
1708
|
+
children: "\uC81C\uAC70"
|
|
1709
|
+
}
|
|
1710
|
+
)
|
|
1711
|
+
] }) : /* @__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" })
|
|
1457
1712
|
}
|
|
1458
1713
|
),
|
|
1459
|
-
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,
|
|
1460
1714
|
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: { display: "flex", justifyContent: "flex-end", gap: 8, marginTop: 10 }, children: [
|
|
1461
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
1715
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
1716
|
+
"button",
|
|
1717
|
+
{
|
|
1718
|
+
type: "button",
|
|
1719
|
+
disabled: popup.saving,
|
|
1720
|
+
onClick: () => kit.picking.cancelAnnotation(),
|
|
1721
|
+
style: { ...baseButton, minHeight: 32, padding: "5px 10px" },
|
|
1722
|
+
children: "\uCDE8\uC18C"
|
|
1723
|
+
}
|
|
1724
|
+
),
|
|
1462
1725
|
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
1463
1726
|
"button",
|
|
1464
1727
|
{
|
|
1465
1728
|
type: "submit",
|
|
1466
|
-
disabled: !popup.canSave,
|
|
1729
|
+
disabled: !popup.canSave || popup.saving,
|
|
1467
1730
|
style: {
|
|
1468
1731
|
...primaryButton,
|
|
1469
|
-
|
|
1470
|
-
|
|
1732
|
+
minHeight: 32,
|
|
1733
|
+
padding: "5px 12px",
|
|
1734
|
+
opacity: popup.canSave && !popup.saving ? 1 : 0.5,
|
|
1735
|
+
cursor: popup.canSave && !popup.saving ? "pointer" : "not-allowed"
|
|
1471
1736
|
},
|
|
1472
|
-
children: "\
|
|
1737
|
+
children: popup.saving ? "\uBCF4\uB0B4\uB294 \uC911\u2026" : "\uBCF4\uB0B4\uAE30"
|
|
1473
1738
|
}
|
|
1474
1739
|
)
|
|
1475
1740
|
] })
|