@solhun/feedback-kit-web 0.3.0 → 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 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: value.trim().length > 0 && value.length <= import_feedback_kit_core.COMMENT_MAX_CHARS
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: popup.comment,
633
+ comment: settled.comment,
559
634
  priority: "unset",
560
- screenshot: null,
561
- pin: popup.point,
562
- element: popup.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: popup.point.x,
576
- y: popup.point.y,
577
- selector: popup.element?.selector ?? null,
578
- comment: popup.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
- onPick: (element, point) => {
737
- void widgetRef?.openReport({ element });
738
- widgetRef?.modal.setPin(point);
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: opts.capture === void 0 ? captureWebScreenshot : opts.capture,
744
- reencode: opts.reencode === void 0 ? reencodeWebScreenshot : opts.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.3.0" : "dev";
857
+ var VERSION = true ? "0.4.0" : "dev";
779
858
 
780
859
  // src/feedback-kit.tsx
781
860
  var import_jsx_runtime = require("react/jsx-runtime");
@@ -804,29 +883,63 @@ var INITIAL_PICKING_STATE = {
804
883
  function screenshotSource(screenshot) {
805
884
  return `data:${screenshot.contentType};base64,${screenshot.base64}`;
806
885
  }
807
- function readScreenshotFile(file) {
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
- const result = typeof reader.result === "string" ? reader.result : "";
817
- const comma = result.indexOf(",");
818
- if (comma < 0) {
819
- resolve(null);
820
- return;
821
- }
822
- resolve({
823
- base64: result.slice(comma + 1),
824
- contentType
825
- });
826
- };
827
- reader.readAsDataURL(file);
890
+ reader.onload = () => resolve(typeof reader.result === "string" ? reader.result : null);
891
+ reader.readAsDataURL(blob);
828
892
  });
829
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
+ }
830
943
  function nextPaint() {
831
944
  return new Promise((resolve) => {
832
945
  if (typeof requestAnimationFrame === "function") {
@@ -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",
@@ -976,20 +1100,22 @@ function FeedbackKit(props) {
976
1100
  (0, import_react.useEffect)(() => {
977
1101
  if (!kit || !widgetState) return;
978
1102
  const { screen: screen2, modal: modal2 } = widgetState;
979
- if (screen2 !== "modal" && screen2 !== "pin") return;
1103
+ const quickOpen = screen2 === "picking" && pickingState.popup !== null;
1104
+ if (screen2 !== "modal" && screen2 !== "pin" && !quickOpen) return;
980
1105
  const widget = kit.widget;
981
1106
  function onKeyDownCapture(event) {
982
1107
  if (event.key !== "Escape") return;
983
1108
  if (event.isComposing) return;
984
1109
  event.stopPropagation();
985
1110
  event.preventDefault();
986
- if (screen2 === "pin") widget.cancelPin();
1111
+ if (quickOpen) kit.picking.cancelAnnotation();
1112
+ else if (screen2 === "pin") widget.cancelPin();
987
1113
  else if (modal2.closeConfirmVisible) widget.cancelCloseReport();
988
1114
  else widget.closeReport();
989
1115
  }
990
1116
  document.addEventListener("keydown", onKeyDownCapture, true);
991
1117
  return () => document.removeEventListener("keydown", onKeyDownCapture, true);
992
- }, [kit, widgetState]);
1118
+ }, [kit, widgetState, pickingState.popup]);
993
1119
  if (!kit || !widgetState) {
994
1120
  return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { ...OWN_UI_PROPS, "data-feedback-kit-loading": "true" });
995
1121
  }
@@ -1018,11 +1144,19 @@ function FeedbackKit(props) {
1018
1144
  const input = event.currentTarget;
1019
1145
  const file = input.files?.[0];
1020
1146
  if (file) {
1021
- const screenshot = await readScreenshotFile(file);
1147
+ const screenshot = await readImageFile(file);
1022
1148
  if (screenshot) await activeKit.widget.modal.attachFile(screenshot);
1023
1149
  }
1024
1150
  input.value = "";
1025
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
+ }
1026
1160
  function closeReport() {
1027
1161
  activeKit.widget.closeReport();
1028
1162
  }
@@ -1460,48 +1594,147 @@ function FeedbackKit(props) {
1460
1594
  event.preventDefault();
1461
1595
  void kit.picking.saveAnnotation();
1462
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
+ },
1463
1610
  style: {
1464
1611
  position: "fixed",
1465
- left: `min(${popup.point.x * 100}vw, calc(100vw - 336px))`,
1466
- top: `min(${popup.point.y * 100}vh, calc(100vh - 230px))`,
1467
- width: "min(320px, calc(100vw - 32px))",
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))",
1468
1616
  boxSizing: "border-box",
1469
1617
  border: `1px solid ${TOKENS.line}`,
1470
1618
  borderRadius: 12,
1471
1619
  background: TOKENS.surface,
1472
- padding: 14,
1620
+ padding: 12,
1473
1621
  boxShadow: TOKENS.shadow,
1474
1622
  pointerEvents: "auto"
1475
1623
  },
1476
1624
  children: [
1477
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("label", { htmlFor: "feedback-kit-annotation", style: { display: "block", fontWeight: 700 }, children: "\uC774 \uC694\uC18C\uC5D0 \uC8FC\uC11D \uB0A8\uAE30\uAE30" }),
1478
- popup.element?.text ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { style: { margin: "4px 0 9px", color: TOKENS.muted }, children: popup.element.text }) : null,
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" }),
1479
1653
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1480
1654
  "textarea",
1481
1655
  {
1482
1656
  id: "feedback-kit-annotation",
1483
1657
  autoFocus: true,
1484
1658
  value: popup.comment,
1659
+ disabled: popup.saving,
1485
1660
  onChange: (event) => kit.picking.setAnnotationComment(event.currentTarget.value),
1486
- rows: 4,
1487
- placeholder: "\uC758\uACAC\uC744 \uC785\uB825\uD574\uC8FC\uC138\uC694",
1488
- style: { ...inputStyle, resize: "vertical" }
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" })
1489
1712
  }
1490
1713
  ),
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
1714
  /* @__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)("button", { type: "button", onClick: () => kit.picking.cancelAnnotation(), style: baseButton, children: "\uCDE8\uC18C" }),
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
+ ),
1494
1725
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1495
1726
  "button",
1496
1727
  {
1497
1728
  type: "submit",
1498
- disabled: !popup.canSave,
1729
+ disabled: !popup.canSave || popup.saving,
1499
1730
  style: {
1500
1731
  ...primaryButton,
1501
- opacity: popup.canSave ? 1 : 0.5,
1502
- cursor: popup.canSave ? "pointer" : "not-allowed"
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"
1503
1736
  },
1504
- children: "\uC800\uC7A5"
1737
+ children: popup.saving ? "\uBCF4\uB0B4\uB294 \uC911\u2026" : "\uBCF4\uB0B4\uAE30"
1505
1738
  }
1506
1739
  )
1507
1740
  ] })