@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.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import { resolveConfig, shouldShowWidget } from "@solhun/feedback-kit-core";
|
|
3
3
|
import { parseSourceAttr, SOURCE_ATTR, sourceFromElement } from "@solhun/feedback-kit-core";
|
|
4
4
|
import {
|
|
5
|
-
COMMENT_MAX_CHARS as
|
|
5
|
+
COMMENT_MAX_CHARS as COMMENT_MAX_CHARS3,
|
|
6
6
|
COMMENT_REQUIRED_MESSAGE,
|
|
7
7
|
COMMENT_TOO_LONG_MESSAGE,
|
|
8
8
|
denormalizePin,
|
|
@@ -294,6 +294,7 @@ var MarkerStore = class {
|
|
|
294
294
|
// src/picking.ts
|
|
295
295
|
import {
|
|
296
296
|
COMMENT_MAX_CHARS,
|
|
297
|
+
captureWithinLimit,
|
|
297
298
|
normalizePin
|
|
298
299
|
} from "@solhun/feedback-kit-core";
|
|
299
300
|
var OWN_UI_ATTR = "data-feedback-kit";
|
|
@@ -323,6 +324,10 @@ var ElementPickingController = class {
|
|
|
323
324
|
this.markers = [];
|
|
324
325
|
this.pathWatch = null;
|
|
325
326
|
this.saving = false;
|
|
327
|
+
/** 늦게 끝난 캡처가 다음 주석의 그림을 덮지 못하게 하는 세대 번호. */
|
|
328
|
+
this.shotGeneration = 0;
|
|
329
|
+
/** 지금 도는 자동 캡처. Enter 가 캡처보다 빨랐을 때 기다릴 대상. */
|
|
330
|
+
this.capturing = null;
|
|
326
331
|
this.onClick = (event) => this.handleClick(event);
|
|
327
332
|
this.onMouseOver = (event) => this.handleMouseOver(event);
|
|
328
333
|
this.queue = opts.queue;
|
|
@@ -332,6 +337,9 @@ var ElementPickingController = class {
|
|
|
332
337
|
this.getPathname = opts.getPathname ?? defaultPathname;
|
|
333
338
|
this.doc = opts.doc === void 0 ? defaultDocument() : opts.doc;
|
|
334
339
|
this.getViewport = opts.getViewport ?? defaultViewport;
|
|
340
|
+
this.capture = opts.capture ?? null;
|
|
341
|
+
this.reencode = opts.reencode ?? null;
|
|
342
|
+
this.screenshotLimitBytes = opts.screenshotLimitBytes;
|
|
335
343
|
this.lastPathname = this.getPathname();
|
|
336
344
|
this.markers = this.store.list(this.lastPathname);
|
|
337
345
|
this.unsubscribeQueue = this.queue.subscribe?.(() => {
|
|
@@ -450,24 +458,84 @@ var ElementPickingController = class {
|
|
|
450
458
|
element: describeElement(element),
|
|
451
459
|
point,
|
|
452
460
|
comment: "",
|
|
453
|
-
canSave: false
|
|
461
|
+
canSave: false,
|
|
462
|
+
screenshot: null,
|
|
463
|
+
screenshotStatus: this.capture ? "capturing" : "none",
|
|
464
|
+
saving: false
|
|
454
465
|
};
|
|
455
466
|
this.emit();
|
|
467
|
+
if (this.capture) this.capturing = this.runCapture();
|
|
456
468
|
}
|
|
457
469
|
setAnnotationComment(value) {
|
|
458
470
|
if (!this.popup) return;
|
|
459
471
|
this.popup = {
|
|
460
472
|
...this.popup,
|
|
461
473
|
comment: value,
|
|
462
|
-
canSave:
|
|
474
|
+
canSave: this.isSavable(value)
|
|
463
475
|
};
|
|
464
476
|
this.emit();
|
|
465
477
|
}
|
|
478
|
+
/**
|
|
479
|
+
* 사용자가 붙여넣기(Cmd+V)나 드래그로 넣은 그림. 자동 캡처 결과를 덮는다.
|
|
480
|
+
*
|
|
481
|
+
* 자동 캡처보다 이게 우선인 이유: 사용자가 굳이 그림을 붙였다면 그건 "지금 화면"이
|
|
482
|
+
* 아니라 **보여주고 싶은 다른 것**이다(잘라낸 부분, 다른 탭, 기대하는 디자인).
|
|
483
|
+
*/
|
|
484
|
+
async attachAnnotationImage(shot) {
|
|
485
|
+
if (!this.popup || this.popup.saving) return;
|
|
486
|
+
const generation = ++this.shotGeneration;
|
|
487
|
+
const outcome = await captureWithinLimit({
|
|
488
|
+
capture: () => shot,
|
|
489
|
+
reencode: this.reencode,
|
|
490
|
+
limitBytes: this.screenshotLimitBytes
|
|
491
|
+
});
|
|
492
|
+
if (generation !== this.shotGeneration || !this.popup) return;
|
|
493
|
+
this.popup = {
|
|
494
|
+
...this.popup,
|
|
495
|
+
screenshot: outcome.failed ? null : outcome.screenshot,
|
|
496
|
+
screenshotStatus: outcome.failed || !outcome.screenshot ? "failed" : "ready"
|
|
497
|
+
};
|
|
498
|
+
this.emit();
|
|
499
|
+
}
|
|
500
|
+
/** 그림만 뗀다. 코멘트는 그대로 — 그림이 없어도 주석은 성립한다. */
|
|
501
|
+
removeAnnotationImage() {
|
|
502
|
+
if (!this.popup || this.popup.saving) return;
|
|
503
|
+
this.shotGeneration += 1;
|
|
504
|
+
this.popup = { ...this.popup, screenshot: null, screenshotStatus: "none" };
|
|
505
|
+
this.emit();
|
|
506
|
+
}
|
|
466
507
|
/** 취소 — 마커도 제보도 남기지 않는다. 모드는 켜진 채로 둔다. */
|
|
467
508
|
cancelAnnotation() {
|
|
509
|
+
this.shotGeneration += 1;
|
|
468
510
|
this.popup = null;
|
|
469
511
|
this.emit();
|
|
470
512
|
}
|
|
513
|
+
isSavable(comment) {
|
|
514
|
+
return comment.trim().length > 0 && comment.length <= COMMENT_MAX_CHARS;
|
|
515
|
+
}
|
|
516
|
+
/** 도는 캡처가 있으면 끝나기를 기다린다. 캡처가 던져도 저장을 막지 않는다. */
|
|
517
|
+
async settleCapture() {
|
|
518
|
+
try {
|
|
519
|
+
await this.capturing;
|
|
520
|
+
} catch {
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
async runCapture() {
|
|
524
|
+
if (!this.capture) return;
|
|
525
|
+
const generation = ++this.shotGeneration;
|
|
526
|
+
const outcome = await captureWithinLimit({
|
|
527
|
+
capture: this.capture,
|
|
528
|
+
reencode: this.reencode,
|
|
529
|
+
limitBytes: this.screenshotLimitBytes
|
|
530
|
+
});
|
|
531
|
+
if (generation !== this.shotGeneration || !this.popup) return;
|
|
532
|
+
this.popup = {
|
|
533
|
+
...this.popup,
|
|
534
|
+
screenshot: outcome.failed ? null : outcome.screenshot,
|
|
535
|
+
screenshotStatus: outcome.failed || !outcome.screenshot ? "failed" : "ready"
|
|
536
|
+
};
|
|
537
|
+
this.emit();
|
|
538
|
+
}
|
|
471
539
|
/**
|
|
472
540
|
* 제출된 제보를 마커로 남긴다. 모달 경로에서 쓴다 — 마커 생성이 팝업 저장 안에만
|
|
473
541
|
* 있으면, 리포트 모달로 보낸 지목 제보는 화면에 흔적이 남지 않는다("보냈는지 알 수
|
|
@@ -498,14 +566,22 @@ var ElementPickingController = class {
|
|
|
498
566
|
const popup = this.popup;
|
|
499
567
|
if (!popup || !popup.canSave || this.saving) return null;
|
|
500
568
|
this.saving = true;
|
|
569
|
+
if (popup.screenshotStatus === "capturing") await this.settleCapture();
|
|
570
|
+
const settled = this.popup;
|
|
571
|
+
if (!settled) {
|
|
572
|
+
this.saving = false;
|
|
573
|
+
return null;
|
|
574
|
+
}
|
|
575
|
+
this.popup = { ...settled, saving: true };
|
|
576
|
+
this.emit();
|
|
501
577
|
const pathname = this.getPathname();
|
|
502
578
|
const parts = {
|
|
503
579
|
kind: "annotation",
|
|
504
|
-
comment:
|
|
580
|
+
comment: settled.comment,
|
|
505
581
|
priority: "unset",
|
|
506
|
-
screenshot:
|
|
507
|
-
pin:
|
|
508
|
-
element:
|
|
582
|
+
screenshot: settled.screenshot,
|
|
583
|
+
pin: settled.point,
|
|
584
|
+
element: settled.element
|
|
509
585
|
};
|
|
510
586
|
let report;
|
|
511
587
|
try {
|
|
@@ -518,10 +594,10 @@ var ElementPickingController = class {
|
|
|
518
594
|
}
|
|
519
595
|
const marker = {
|
|
520
596
|
id: report.clientSubmissionId,
|
|
521
|
-
x:
|
|
522
|
-
y:
|
|
523
|
-
selector:
|
|
524
|
-
comment:
|
|
597
|
+
x: settled.point.x,
|
|
598
|
+
y: settled.point.y,
|
|
599
|
+
selector: settled.element?.selector ?? null,
|
|
600
|
+
comment: settled.comment,
|
|
525
601
|
status: "sending",
|
|
526
602
|
at: report.createdAt
|
|
527
603
|
};
|
|
@@ -669,6 +745,8 @@ import { WidgetController } from "@solhun/feedback-kit-core";
|
|
|
669
745
|
function createWebWidget(opts) {
|
|
670
746
|
const store = opts.store ?? new MarkerStore();
|
|
671
747
|
let widgetRef = null;
|
|
748
|
+
const capture = opts.capture === void 0 ? captureWebScreenshot : opts.capture;
|
|
749
|
+
const reencode = opts.reencode === void 0 ? reencodeWebScreenshot : opts.reencode;
|
|
672
750
|
const picking = new ElementPickingController({
|
|
673
751
|
queue: opts.queue,
|
|
674
752
|
createReport: opts.createReport,
|
|
@@ -676,18 +754,20 @@ function createWebWidget(opts) {
|
|
|
676
754
|
getPathname: opts.getPathname,
|
|
677
755
|
doc: opts.doc,
|
|
678
756
|
getViewport: opts.getViewport,
|
|
679
|
-
//
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
757
|
+
// 퀵 주석도 같은 캡처를 쓴다. 캡처가 위젯 자신의 UI 를 빼주므로 팝업이 찍히지 않는다.
|
|
758
|
+
capture,
|
|
759
|
+
reencode,
|
|
760
|
+
screenshotLimitBytes: opts.screenshotLimitBytes,
|
|
761
|
+
// 기본은 클릭 자리의 퀵 입력창이라 onPick 을 주지 않는다(주면 팝업 대신 그걸 부른다).
|
|
762
|
+
// 모드는 어느 쪽이든 켜진 채로 둬서 한 번 켜고 여러 요소를 연달아 지목할 수 있다.
|
|
763
|
+
onPick: opts.pickTarget === "modal" ? (element, point) => {
|
|
764
|
+
void widgetRef?.openReport({ element, pin: point });
|
|
765
|
+
} : void 0
|
|
686
766
|
});
|
|
687
767
|
const widget = new WidgetController({
|
|
688
768
|
...opts,
|
|
689
|
-
capture
|
|
690
|
-
reencode
|
|
769
|
+
capture,
|
|
770
|
+
reencode,
|
|
691
771
|
platform: "web",
|
|
692
772
|
// 새로고침·페이지 이동 직후에도 켜져 있던 모드를 그대로 이어받는다.
|
|
693
773
|
initialPicking: store.isPickingActive(),
|
|
@@ -718,6 +798,7 @@ function createWebWidget(opts) {
|
|
|
718
798
|
|
|
719
799
|
// src/feedback-kit.tsx
|
|
720
800
|
import {
|
|
801
|
+
COMMENT_MAX_CHARS as COMMENT_MAX_CHARS2,
|
|
721
802
|
FLOATING_BUTTON_ID,
|
|
722
803
|
MODAL_ACTION_CANCEL,
|
|
723
804
|
MODAL_ACTION_PICK,
|
|
@@ -734,10 +815,10 @@ import {
|
|
|
734
815
|
} from "react";
|
|
735
816
|
|
|
736
817
|
// src/version.ts
|
|
737
|
-
var VERSION = true ? "0.
|
|
818
|
+
var VERSION = true ? "0.5.0" : "dev";
|
|
738
819
|
|
|
739
820
|
// src/feedback-kit.tsx
|
|
740
|
-
import { jsx, jsxs } from "react/jsx-runtime";
|
|
821
|
+
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
741
822
|
var TOKENS = {
|
|
742
823
|
ink: "#17202a",
|
|
743
824
|
muted: "#667085",
|
|
@@ -763,29 +844,63 @@ var INITIAL_PICKING_STATE = {
|
|
|
763
844
|
function screenshotSource(screenshot) {
|
|
764
845
|
return `data:${screenshot.contentType};base64,${screenshot.base64}`;
|
|
765
846
|
}
|
|
766
|
-
function
|
|
767
|
-
if (file.type !== "image/png" && file.type !== "image/jpeg") {
|
|
768
|
-
return Promise.resolve(null);
|
|
769
|
-
}
|
|
770
|
-
const contentType = file.type;
|
|
847
|
+
function readDataUrl(blob) {
|
|
771
848
|
return new Promise((resolve) => {
|
|
772
849
|
const reader = new FileReader();
|
|
773
850
|
reader.onerror = () => resolve(null);
|
|
774
|
-
reader.onload = () =>
|
|
775
|
-
|
|
776
|
-
const comma = result.indexOf(",");
|
|
777
|
-
if (comma < 0) {
|
|
778
|
-
resolve(null);
|
|
779
|
-
return;
|
|
780
|
-
}
|
|
781
|
-
resolve({
|
|
782
|
-
base64: result.slice(comma + 1),
|
|
783
|
-
contentType
|
|
784
|
-
});
|
|
785
|
-
};
|
|
786
|
-
reader.readAsDataURL(file);
|
|
851
|
+
reader.onload = () => resolve(typeof reader.result === "string" ? reader.result : null);
|
|
852
|
+
reader.readAsDataURL(blob);
|
|
787
853
|
});
|
|
788
854
|
}
|
|
855
|
+
function base64Of(dataUrl) {
|
|
856
|
+
const comma = dataUrl.indexOf(",");
|
|
857
|
+
return comma >= 0 ? dataUrl.slice(comma + 1) : null;
|
|
858
|
+
}
|
|
859
|
+
async function transcodeToJpeg(dataUrl) {
|
|
860
|
+
const image = await new Promise((resolve) => {
|
|
861
|
+
const img = new Image();
|
|
862
|
+
img.onload = () => resolve(img);
|
|
863
|
+
img.onerror = () => resolve(null);
|
|
864
|
+
img.src = dataUrl;
|
|
865
|
+
});
|
|
866
|
+
if (!image) return null;
|
|
867
|
+
const canvas = document.createElement("canvas");
|
|
868
|
+
canvas.width = image.naturalWidth || image.width;
|
|
869
|
+
canvas.height = image.naturalHeight || image.height;
|
|
870
|
+
if (canvas.width < 1 || canvas.height < 1) return null;
|
|
871
|
+
const context = canvas.getContext("2d");
|
|
872
|
+
if (!context) return null;
|
|
873
|
+
context.drawImage(image, 0, 0);
|
|
874
|
+
try {
|
|
875
|
+
const base64 = base64Of(canvas.toDataURL("image/jpeg", 0.9));
|
|
876
|
+
return base64 ? { base64, contentType: "image/jpeg" } : null;
|
|
877
|
+
} catch {
|
|
878
|
+
return null;
|
|
879
|
+
}
|
|
880
|
+
}
|
|
881
|
+
async function readImageFile(file) {
|
|
882
|
+
if (!file.type.startsWith("image/")) return null;
|
|
883
|
+
const dataUrl = await readDataUrl(file);
|
|
884
|
+
if (!dataUrl) return null;
|
|
885
|
+
if (file.type === "image/png" || file.type === "image/jpeg") {
|
|
886
|
+
const base64 = base64Of(dataUrl);
|
|
887
|
+
return base64 ? { base64, contentType: file.type } : null;
|
|
888
|
+
}
|
|
889
|
+
return transcodeToJpeg(dataUrl);
|
|
890
|
+
}
|
|
891
|
+
function firstImageOf(transfer) {
|
|
892
|
+
if (!transfer) return null;
|
|
893
|
+
for (const item of Array.from(transfer.items ?? [])) {
|
|
894
|
+
if (item.kind === "file" && item.type.startsWith("image/")) {
|
|
895
|
+
const file = item.getAsFile();
|
|
896
|
+
if (file) return file;
|
|
897
|
+
}
|
|
898
|
+
}
|
|
899
|
+
for (const file of Array.from(transfer.files ?? [])) {
|
|
900
|
+
if (file.type.startsWith("image/")) return file;
|
|
901
|
+
}
|
|
902
|
+
return null;
|
|
903
|
+
}
|
|
789
904
|
function nextPaint() {
|
|
790
905
|
return new Promise((resolve) => {
|
|
791
906
|
if (typeof requestAnimationFrame === "function") {
|
|
@@ -837,6 +952,17 @@ var primaryButton = {
|
|
|
837
952
|
background: TOKENS.accent,
|
|
838
953
|
color: TOKENS.surface
|
|
839
954
|
};
|
|
955
|
+
var visuallyHidden = {
|
|
956
|
+
position: "absolute",
|
|
957
|
+
width: 1,
|
|
958
|
+
height: 1,
|
|
959
|
+
margin: -1,
|
|
960
|
+
padding: 0,
|
|
961
|
+
overflow: "hidden",
|
|
962
|
+
clip: "rect(0 0 0 0)",
|
|
963
|
+
whiteSpace: "nowrap",
|
|
964
|
+
border: 0
|
|
965
|
+
};
|
|
840
966
|
var inputStyle = {
|
|
841
967
|
width: "100%",
|
|
842
968
|
boxSizing: "border-box",
|
|
@@ -847,6 +973,96 @@ var inputStyle = {
|
|
|
847
973
|
padding: "10px 11px",
|
|
848
974
|
font: "inherit"
|
|
849
975
|
};
|
|
976
|
+
function PickToggle({
|
|
977
|
+
active,
|
|
978
|
+
disabled,
|
|
979
|
+
variant,
|
|
980
|
+
focusId,
|
|
981
|
+
onToggle
|
|
982
|
+
}) {
|
|
983
|
+
const floating = variant === "floating";
|
|
984
|
+
return /* @__PURE__ */ jsxs(
|
|
985
|
+
"button",
|
|
986
|
+
{
|
|
987
|
+
type: "button",
|
|
988
|
+
role: "switch",
|
|
989
|
+
"aria-checked": active,
|
|
990
|
+
"aria-label": "\uC694\uC18C \uC9C0\uBAA9 \uBAA8\uB4DC",
|
|
991
|
+
"data-fk-pick-toggle": active ? "on" : "off",
|
|
992
|
+
"data-fk-focus-id": focusId,
|
|
993
|
+
disabled,
|
|
994
|
+
onClick: onToggle,
|
|
995
|
+
style: {
|
|
996
|
+
...baseButton,
|
|
997
|
+
display: "flex",
|
|
998
|
+
alignItems: "center",
|
|
999
|
+
gap: 10,
|
|
1000
|
+
opacity: disabled ? 0.5 : 1,
|
|
1001
|
+
cursor: disabled ? "not-allowed" : "pointer",
|
|
1002
|
+
// 항상 값을 준다. 조건부로 빼면 리렌더 때 shorthand(border)와 충돌한다고 React 가 경고한다.
|
|
1003
|
+
borderColor: active ? TOKENS.accent : TOKENS.line,
|
|
1004
|
+
...floating ? {
|
|
1005
|
+
position: "fixed",
|
|
1006
|
+
top: 16,
|
|
1007
|
+
left: "50%",
|
|
1008
|
+
transform: "translateX(-50%)",
|
|
1009
|
+
// 오버레이 자체는 클릭을 통과시킨다(pointerEvents:none) — 이 토글만 되살린다.
|
|
1010
|
+
pointerEvents: "auto",
|
|
1011
|
+
background: TOKENS.surface,
|
|
1012
|
+
boxShadow: TOKENS.shadow
|
|
1013
|
+
} : {
|
|
1014
|
+
width: "100%",
|
|
1015
|
+
justifyContent: "space-between",
|
|
1016
|
+
marginBottom: 16,
|
|
1017
|
+
background: active ? "rgba(49, 93, 115, 0.06)" : TOKENS.surface
|
|
1018
|
+
}
|
|
1019
|
+
},
|
|
1020
|
+
children: [
|
|
1021
|
+
/* @__PURE__ */ jsxs("span", { style: { display: "inline-flex", alignItems: "center", gap: 7 }, children: [
|
|
1022
|
+
/* @__PURE__ */ jsx("span", { "aria-hidden": "true", style: { color: active ? TOKENS.accent : TOKENS.muted }, children: "\u25CE" }),
|
|
1023
|
+
"\uC694\uC18C \uC9C0\uBAA9 \uBAA8\uB4DC"
|
|
1024
|
+
] }),
|
|
1025
|
+
/* @__PURE__ */ jsxs("span", { "aria-hidden": "true", style: { display: "inline-flex", alignItems: "center", gap: 8 }, children: [
|
|
1026
|
+
/* @__PURE__ */ jsx(
|
|
1027
|
+
"span",
|
|
1028
|
+
{
|
|
1029
|
+
style: { fontSize: 12, fontWeight: 600, color: active ? TOKENS.accent : TOKENS.muted },
|
|
1030
|
+
children: active ? "\uCF1C\uC9D0" : "\uAEBC\uC9D0"
|
|
1031
|
+
}
|
|
1032
|
+
),
|
|
1033
|
+
/* @__PURE__ */ jsx(
|
|
1034
|
+
"span",
|
|
1035
|
+
{
|
|
1036
|
+
style: {
|
|
1037
|
+
position: "relative",
|
|
1038
|
+
display: "inline-block",
|
|
1039
|
+
width: 38,
|
|
1040
|
+
height: 22,
|
|
1041
|
+
borderRadius: 11,
|
|
1042
|
+
background: active ? TOKENS.accent : TOKENS.line
|
|
1043
|
+
},
|
|
1044
|
+
children: /* @__PURE__ */ jsx(
|
|
1045
|
+
"span",
|
|
1046
|
+
{
|
|
1047
|
+
style: {
|
|
1048
|
+
position: "absolute",
|
|
1049
|
+
top: 3,
|
|
1050
|
+
left: active ? 19 : 3,
|
|
1051
|
+
width: 16,
|
|
1052
|
+
height: 16,
|
|
1053
|
+
borderRadius: "50%",
|
|
1054
|
+
background: TOKENS.surface,
|
|
1055
|
+
boxShadow: "0 1px 3px rgba(23, 32, 42, 0.35)"
|
|
1056
|
+
}
|
|
1057
|
+
}
|
|
1058
|
+
)
|
|
1059
|
+
}
|
|
1060
|
+
)
|
|
1061
|
+
] })
|
|
1062
|
+
]
|
|
1063
|
+
}
|
|
1064
|
+
);
|
|
1065
|
+
}
|
|
850
1066
|
function MarkerStatus({ status }) {
|
|
851
1067
|
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 };
|
|
852
1068
|
return /* @__PURE__ */ jsxs("span", { style: { display: "inline-flex", gap: 4, alignItems: "center", color: presentation.color }, children: [
|
|
@@ -935,26 +1151,32 @@ function FeedbackKit(props) {
|
|
|
935
1151
|
useEffect(() => {
|
|
936
1152
|
if (!kit || !widgetState) return;
|
|
937
1153
|
const { screen: screen2, modal: modal2 } = widgetState;
|
|
938
|
-
|
|
1154
|
+
const quickOpen = screen2 === "picking" && pickingState.popup !== null;
|
|
1155
|
+
if (screen2 !== "modal" && screen2 !== "pin" && !quickOpen) return;
|
|
939
1156
|
const widget = kit.widget;
|
|
940
1157
|
function onKeyDownCapture(event) {
|
|
941
1158
|
if (event.key !== "Escape") return;
|
|
942
1159
|
if (event.isComposing) return;
|
|
943
1160
|
event.stopPropagation();
|
|
944
1161
|
event.preventDefault();
|
|
945
|
-
if (
|
|
1162
|
+
if (quickOpen) kit.picking.cancelAnnotation();
|
|
1163
|
+
else if (screen2 === "pin") widget.cancelPin();
|
|
946
1164
|
else if (modal2.closeConfirmVisible) widget.cancelCloseReport();
|
|
947
1165
|
else widget.closeReport();
|
|
948
1166
|
}
|
|
949
1167
|
document.addEventListener("keydown", onKeyDownCapture, true);
|
|
950
1168
|
return () => document.removeEventListener("keydown", onKeyDownCapture, true);
|
|
951
|
-
}, [kit, widgetState]);
|
|
1169
|
+
}, [kit, widgetState, pickingState.popup]);
|
|
952
1170
|
if (!kit || !widgetState) {
|
|
953
1171
|
return /* @__PURE__ */ jsx("div", { ...OWN_UI_PROPS, "data-feedback-kit-loading": "true" });
|
|
954
1172
|
}
|
|
955
1173
|
const activeKit = kit;
|
|
956
|
-
const { modal, screen } = widgetState;
|
|
1174
|
+
const { modal, screen, pickingActive } = widgetState;
|
|
957
1175
|
const draftLocked = modal.submitStatus === "sending" || modal.submitStatus === "pending";
|
|
1176
|
+
function togglePicking() {
|
|
1177
|
+
if (pickingActive) activeKit.widget.stopPicking();
|
|
1178
|
+
else activeKit.widget.startPicking();
|
|
1179
|
+
}
|
|
958
1180
|
async function withOwnUiHidden(action) {
|
|
959
1181
|
const root = rootRef.current;
|
|
960
1182
|
const previousVisibility = root?.style.visibility ?? "";
|
|
@@ -977,11 +1199,19 @@ function FeedbackKit(props) {
|
|
|
977
1199
|
const input = event.currentTarget;
|
|
978
1200
|
const file = input.files?.[0];
|
|
979
1201
|
if (file) {
|
|
980
|
-
const screenshot = await
|
|
1202
|
+
const screenshot = await readImageFile(file);
|
|
981
1203
|
if (screenshot) await activeKit.widget.modal.attachFile(screenshot);
|
|
982
1204
|
}
|
|
983
1205
|
input.value = "";
|
|
984
1206
|
}
|
|
1207
|
+
async function attachToPopup(transfer) {
|
|
1208
|
+
const file = firstImageOf(transfer);
|
|
1209
|
+
if (!file) return false;
|
|
1210
|
+
const screenshot = await readImageFile(file);
|
|
1211
|
+
if (!screenshot) return false;
|
|
1212
|
+
await activeKit.picking.attachAnnotationImage(screenshot);
|
|
1213
|
+
return true;
|
|
1214
|
+
}
|
|
985
1215
|
function closeReport() {
|
|
986
1216
|
activeKit.widget.closeReport();
|
|
987
1217
|
}
|
|
@@ -1018,8 +1248,7 @@ function FeedbackKit(props) {
|
|
|
1018
1248
|
ref: floatingButtonRef,
|
|
1019
1249
|
id: FLOATING_BUTTON_ID,
|
|
1020
1250
|
type: "button",
|
|
1021
|
-
|
|
1022
|
-
"aria-label": screen === "picking" ? "\uC694\uC18C \uC9C0\uBAA9 \uC911" : "\uD53C\uB4DC\uBC31 \uBCF4\uB0B4\uAE30",
|
|
1251
|
+
"aria-label": "\uD53C\uB4DC\uBC31 \uBCF4\uB0B4\uAE30",
|
|
1023
1252
|
onClick: () => void openReport(),
|
|
1024
1253
|
style: {
|
|
1025
1254
|
...primaryButton,
|
|
@@ -1030,11 +1259,9 @@ function FeedbackKit(props) {
|
|
|
1030
1259
|
minWidth: 112,
|
|
1031
1260
|
minHeight: 46,
|
|
1032
1261
|
borderRadius: 24,
|
|
1033
|
-
boxShadow: TOKENS.shadow
|
|
1034
|
-
opacity: screen === "picking" ? 0.76 : 1,
|
|
1035
|
-
cursor: screen === "picking" ? "default" : "pointer"
|
|
1262
|
+
boxShadow: TOKENS.shadow
|
|
1036
1263
|
},
|
|
1037
|
-
children:
|
|
1264
|
+
children: "\uD53C\uB4DC\uBC31"
|
|
1038
1265
|
}
|
|
1039
1266
|
) : null,
|
|
1040
1267
|
announcement && !modal.open ? /* @__PURE__ */ jsx(
|
|
@@ -1118,7 +1345,17 @@ function FeedbackKit(props) {
|
|
|
1118
1345
|
]
|
|
1119
1346
|
}
|
|
1120
1347
|
),
|
|
1121
|
-
/* @__PURE__ */ jsx("p", { style: { margin: "0 0
|
|
1348
|
+
/* @__PURE__ */ 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." }),
|
|
1349
|
+
/* @__PURE__ */ jsx(
|
|
1350
|
+
PickToggle,
|
|
1351
|
+
{
|
|
1352
|
+
variant: "inline",
|
|
1353
|
+
active: pickingActive,
|
|
1354
|
+
disabled: draftLocked,
|
|
1355
|
+
focusId: MODAL_ACTION_PICK,
|
|
1356
|
+
onToggle: togglePicking
|
|
1357
|
+
}
|
|
1358
|
+
),
|
|
1122
1359
|
/* @__PURE__ */ jsxs("section", { "aria-labelledby": "feedback-kit-screenshot-label", style: { marginBottom: 16 }, children: [
|
|
1123
1360
|
/* @__PURE__ */ jsx("strong", { id: "feedback-kit-screenshot-label", children: "\uC2A4\uD06C\uB9B0\uC0F7" }),
|
|
1124
1361
|
/* @__PURE__ */ jsx(
|
|
@@ -1232,19 +1469,6 @@ function FeedbackKit(props) {
|
|
|
1232
1469
|
]
|
|
1233
1470
|
}
|
|
1234
1471
|
),
|
|
1235
|
-
/* @__PURE__ */ jsx(
|
|
1236
|
-
"button",
|
|
1237
|
-
{
|
|
1238
|
-
type: "button",
|
|
1239
|
-
role: "switch",
|
|
1240
|
-
"aria-checked": "false",
|
|
1241
|
-
disabled: draftLocked,
|
|
1242
|
-
"data-fk-focus-id": MODAL_ACTION_PICK,
|
|
1243
|
-
onClick: () => kit.widget.startPicking(),
|
|
1244
|
-
style: { ...baseButton, width: "100%", marginTop: 16 },
|
|
1245
|
-
children: "\uC694\uC18C \uC9C0\uBAA9"
|
|
1246
|
-
}
|
|
1247
|
-
),
|
|
1248
1472
|
modal.submitStatus !== "idle" || modal.pending > 0 ? /* @__PURE__ */ jsxs(
|
|
1249
1473
|
"div",
|
|
1250
1474
|
{
|
|
@@ -1367,22 +1591,7 @@ function FeedbackKit(props) {
|
|
|
1367
1591
|
}
|
|
1368
1592
|
}
|
|
1369
1593
|
) : null,
|
|
1370
|
-
/* @__PURE__ */ jsx(
|
|
1371
|
-
"button",
|
|
1372
|
-
{
|
|
1373
|
-
type: "button",
|
|
1374
|
-
onClick: () => kit.widget.stopPicking(),
|
|
1375
|
-
style: {
|
|
1376
|
-
...primaryButton,
|
|
1377
|
-
position: "fixed",
|
|
1378
|
-
top: 18,
|
|
1379
|
-
right: 18,
|
|
1380
|
-
pointerEvents: "auto",
|
|
1381
|
-
boxShadow: TOKENS.shadow
|
|
1382
|
-
},
|
|
1383
|
-
children: "\uC9C0\uBAA9 \uC885\uB8CC"
|
|
1384
|
-
}
|
|
1385
|
-
),
|
|
1594
|
+
/* @__PURE__ */ jsx(PickToggle, { variant: "floating", active: true, onToggle: togglePicking }),
|
|
1386
1595
|
pickingState.markers.map((marker, index) => /* @__PURE__ */ jsx(
|
|
1387
1596
|
"div",
|
|
1388
1597
|
{
|
|
@@ -1419,48 +1628,147 @@ function FeedbackKit(props) {
|
|
|
1419
1628
|
event.preventDefault();
|
|
1420
1629
|
void kit.picking.saveAnnotation();
|
|
1421
1630
|
},
|
|
1631
|
+
onPaste: (event) => {
|
|
1632
|
+
if (!firstImageOf(event.clipboardData)) return;
|
|
1633
|
+
event.preventDefault();
|
|
1634
|
+
void attachToPopup(event.clipboardData);
|
|
1635
|
+
},
|
|
1636
|
+
onDragOver: (event) => {
|
|
1637
|
+
if (firstImageOf(event.dataTransfer)) event.preventDefault();
|
|
1638
|
+
},
|
|
1639
|
+
onDrop: (event) => {
|
|
1640
|
+
if (!firstImageOf(event.dataTransfer)) return;
|
|
1641
|
+
event.preventDefault();
|
|
1642
|
+
void attachToPopup(event.dataTransfer);
|
|
1643
|
+
},
|
|
1422
1644
|
style: {
|
|
1423
1645
|
position: "fixed",
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1646
|
+
// 클릭한 지점 바로 옆에 띄운다. 화면 밖으로 나가지 않게 양쪽을 물린다.
|
|
1647
|
+
left: `clamp(8px, calc(${popup.point.x * 100}vw + 14px), calc(100vw - 340px))`,
|
|
1648
|
+
top: `clamp(8px, calc(${popup.point.y * 100}vh + 14px), calc(100vh - 200px))`,
|
|
1649
|
+
width: "min(332px, calc(100vw - 16px))",
|
|
1427
1650
|
boxSizing: "border-box",
|
|
1428
1651
|
border: `1px solid ${TOKENS.line}`,
|
|
1429
1652
|
borderRadius: 12,
|
|
1430
1653
|
background: TOKENS.surface,
|
|
1431
|
-
padding:
|
|
1654
|
+
padding: 12,
|
|
1432
1655
|
boxShadow: TOKENS.shadow,
|
|
1433
1656
|
pointerEvents: "auto"
|
|
1434
1657
|
},
|
|
1435
1658
|
children: [
|
|
1436
|
-
/* @__PURE__ */
|
|
1437
|
-
|
|
1659
|
+
/* @__PURE__ */ jsxs(
|
|
1660
|
+
"div",
|
|
1661
|
+
{
|
|
1662
|
+
style: {
|
|
1663
|
+
display: "flex",
|
|
1664
|
+
alignItems: "center",
|
|
1665
|
+
gap: 6,
|
|
1666
|
+
marginBottom: 7,
|
|
1667
|
+
color: TOKENS.muted,
|
|
1668
|
+
fontSize: 12
|
|
1669
|
+
},
|
|
1670
|
+
children: [
|
|
1671
|
+
/* @__PURE__ */ jsx("span", { "aria-hidden": "true", children: "\u25CE" }),
|
|
1672
|
+
/* @__PURE__ */ jsx(
|
|
1673
|
+
"span",
|
|
1674
|
+
{
|
|
1675
|
+
style: {
|
|
1676
|
+
overflow: "hidden",
|
|
1677
|
+
textOverflow: "ellipsis",
|
|
1678
|
+
whiteSpace: "nowrap"
|
|
1679
|
+
},
|
|
1680
|
+
children: popup.element?.text?.trim() || popup.element?.selector || "\uC774 \uC9C0\uC810"
|
|
1681
|
+
}
|
|
1682
|
+
)
|
|
1683
|
+
]
|
|
1684
|
+
}
|
|
1685
|
+
),
|
|
1686
|
+
/* @__PURE__ */ jsx("label", { htmlFor: "feedback-kit-annotation", style: visuallyHidden, children: "\uC774 \uC694\uC18C\uC5D0 \uB0A8\uAE38 \uC758\uACAC" }),
|
|
1438
1687
|
/* @__PURE__ */ jsx(
|
|
1439
1688
|
"textarea",
|
|
1440
1689
|
{
|
|
1441
1690
|
id: "feedback-kit-annotation",
|
|
1442
1691
|
autoFocus: true,
|
|
1443
1692
|
value: popup.comment,
|
|
1693
|
+
disabled: popup.saving,
|
|
1444
1694
|
onChange: (event) => kit.picking.setAnnotationComment(event.currentTarget.value),
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1695
|
+
onKeyDown: (event) => {
|
|
1696
|
+
if (event.key !== "Enter" || event.shiftKey) return;
|
|
1697
|
+
const native = event.nativeEvent;
|
|
1698
|
+
if (native.isComposing) return;
|
|
1699
|
+
event.preventDefault();
|
|
1700
|
+
void kit.picking.saveAnnotation();
|
|
1701
|
+
},
|
|
1702
|
+
rows: 2,
|
|
1703
|
+
placeholder: "\uBB34\uC5C7\uC774 \uBB38\uC81C\uC778\uAC00\uC694? (Enter \uB85C \uBCF4\uB0B4\uAE30)",
|
|
1704
|
+
style: { ...inputStyle, resize: "vertical", minHeight: 58 }
|
|
1705
|
+
}
|
|
1706
|
+
),
|
|
1707
|
+
popup.comment.length > COMMENT_MAX_CHARS2 ? /* @__PURE__ */ jsx("div", { role: "alert", style: { marginTop: 4, color: TOKENS.danger }, children: "4,000\uC790 \uC774\uD558\uB85C \uC785\uB825\uD574\uC8FC\uC138\uC694" }) : null,
|
|
1708
|
+
/* @__PURE__ */ jsx(
|
|
1709
|
+
"div",
|
|
1710
|
+
{
|
|
1711
|
+
style: {
|
|
1712
|
+
display: "flex",
|
|
1713
|
+
alignItems: "center",
|
|
1714
|
+
gap: 8,
|
|
1715
|
+
marginTop: 8,
|
|
1716
|
+
color: TOKENS.muted,
|
|
1717
|
+
fontSize: 12
|
|
1718
|
+
},
|
|
1719
|
+
children: popup.screenshot ? /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
1720
|
+
/* @__PURE__ */ jsx(
|
|
1721
|
+
"img",
|
|
1722
|
+
{
|
|
1723
|
+
src: screenshotSource(popup.screenshot),
|
|
1724
|
+
alt: "\uC774 \uC8FC\uC11D\uC5D0 \uCCA8\uBD80\uB41C \uD654\uBA74",
|
|
1725
|
+
style: {
|
|
1726
|
+
width: 40,
|
|
1727
|
+
height: 26,
|
|
1728
|
+
objectFit: "cover",
|
|
1729
|
+
borderRadius: 4,
|
|
1730
|
+
border: `1px solid ${TOKENS.line}`
|
|
1731
|
+
}
|
|
1732
|
+
}
|
|
1733
|
+
),
|
|
1734
|
+
/* @__PURE__ */ jsx("span", { style: { flex: 1 }, children: "\uD654\uBA74 \uCCA8\uBD80\uB428 \xB7 \uBD99\uC5EC\uB123\uAE30\uB85C \uAD50\uCCB4" }),
|
|
1735
|
+
/* @__PURE__ */ jsx(
|
|
1736
|
+
"button",
|
|
1737
|
+
{
|
|
1738
|
+
type: "button",
|
|
1739
|
+
disabled: popup.saving,
|
|
1740
|
+
onClick: () => kit.picking.removeAnnotationImage(),
|
|
1741
|
+
style: { ...baseButton, minHeight: 26, padding: "2px 8px", fontSize: 12 },
|
|
1742
|
+
children: "\uC81C\uAC70"
|
|
1743
|
+
}
|
|
1744
|
+
)
|
|
1745
|
+
] }) : /* @__PURE__ */ 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" })
|
|
1448
1746
|
}
|
|
1449
1747
|
),
|
|
1450
|
-
popup.comment.length > 4e3 ? /* @__PURE__ */ jsx("div", { role: "alert", style: { marginTop: 4, color: TOKENS.danger }, children: "4,000\uC790 \uC774\uD558\uB85C \uC785\uB825\uD574\uC8FC\uC138\uC694" }) : null,
|
|
1451
1748
|
/* @__PURE__ */ jsxs("div", { style: { display: "flex", justifyContent: "flex-end", gap: 8, marginTop: 10 }, children: [
|
|
1452
|
-
/* @__PURE__ */ jsx(
|
|
1749
|
+
/* @__PURE__ */ jsx(
|
|
1750
|
+
"button",
|
|
1751
|
+
{
|
|
1752
|
+
type: "button",
|
|
1753
|
+
disabled: popup.saving,
|
|
1754
|
+
onClick: () => kit.picking.cancelAnnotation(),
|
|
1755
|
+
style: { ...baseButton, minHeight: 32, padding: "5px 10px" },
|
|
1756
|
+
children: "\uCDE8\uC18C"
|
|
1757
|
+
}
|
|
1758
|
+
),
|
|
1453
1759
|
/* @__PURE__ */ jsx(
|
|
1454
1760
|
"button",
|
|
1455
1761
|
{
|
|
1456
1762
|
type: "submit",
|
|
1457
|
-
disabled: !popup.canSave,
|
|
1763
|
+
disabled: !popup.canSave || popup.saving,
|
|
1458
1764
|
style: {
|
|
1459
1765
|
...primaryButton,
|
|
1460
|
-
|
|
1461
|
-
|
|
1766
|
+
minHeight: 32,
|
|
1767
|
+
padding: "5px 12px",
|
|
1768
|
+
opacity: popup.canSave && !popup.saving ? 1 : 0.5,
|
|
1769
|
+
cursor: popup.canSave && !popup.saving ? "pointer" : "not-allowed"
|
|
1462
1770
|
},
|
|
1463
|
-
children: "\
|
|
1771
|
+
children: popup.saving ? "\uBCF4\uB0B4\uB294 \uC911\u2026" : "\uBCF4\uB0B4\uAE30"
|
|
1464
1772
|
}
|
|
1465
1773
|
)
|
|
1466
1774
|
] })
|
|
@@ -1540,7 +1848,7 @@ function webContextProviders() {
|
|
|
1540
1848
|
};
|
|
1541
1849
|
}
|
|
1542
1850
|
export {
|
|
1543
|
-
|
|
1851
|
+
COMMENT_MAX_CHARS3 as COMMENT_MAX_CHARS,
|
|
1544
1852
|
COMMENT_REQUIRED_MESSAGE,
|
|
1545
1853
|
COMMENT_TOO_LONG_MESSAGE,
|
|
1546
1854
|
ELEMENT_TEXT_MAX_CHARS,
|