@solhun/feedback-kit-web 0.6.0 → 0.7.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.d.cts CHANGED
@@ -109,6 +109,18 @@ interface AnnotationPopupState {
109
109
  }
110
110
  interface PickingState {
111
111
  active: boolean;
112
+ /**
113
+ * 켜져 있지만 **클릭 가로채기만 잠시 끈 상태**.
114
+ *
115
+ * 왜 필요한가: 지목 모드는 페이지의 모든 클릭을 먹는다(그래야 지목하려고 누른 링크가
116
+ * 실제로 이동해 버리지 않는다). 그 대가로 **제보할 화면을 만들 수가 없다** — 드롭다운을
117
+ * 열고 그 안의 항목을 지목하고 싶어도 여는 클릭부터 막힌다. 그때 모드를 끄면 다시
118
+ * 켜야 하고, 사용자에겐 그게 "모드가 유지되지 않는다"로 읽힌다.
119
+ *
120
+ * 그래서 끄는 것과 멈추는 것을 나눈다. 멈춘 동안 페이지는 원래대로 동작하고, 모드는
121
+ * 켜진 채로 남는다.
122
+ */
123
+ paused: boolean;
112
124
  /** 지금 마우스가 올라가 있는 요소. 하이라이트를 그리는 근거. */
113
125
  hovered: ElementInfo | null;
114
126
  popup: AnnotationPopupState | null;
@@ -167,6 +179,12 @@ declare class ElementPickingController {
167
179
  private readonly rankHintsFor;
168
180
  private readonly listeners;
169
181
  private active;
182
+ /**
183
+ * 일시중지. **저장하지 않는다** — 멈추는 목적이 대개 "링크를 눌러 다른 화면으로 가는 것"
184
+ * 이라, 그 이동이 끝나면 지목이 돌아와 있는 게 목적에 맞다. 같은 이유로 경로가 바뀌면
185
+ * 스스로 풀린다(`syncPath`) — 그래야 SPA 이동과 새로고침이 같게 동작한다.
186
+ */
187
+ private paused;
170
188
  private attached;
171
189
  private hovered;
172
190
  private popup;
@@ -179,7 +197,7 @@ declare class ElementPickingController {
179
197
  /** 지금 도는 자동 캡처. Enter 가 캡처보다 빨랐을 때 기다릴 대상. */
180
198
  private capturing;
181
199
  /**
182
- * 사용자가 [제안 ▾] 를 직접 건드렸는가. 한 번이라도 건드리면 이후 타이핑에 의한 자동
200
+ * 사용자가 [이런 건가요? ▾] 를 직접 건드렸는가. 한 번이라도 건드리면 이후 타이핑에 의한 자동
183
201
  * 접힘/펼침이 멈춘다 — 모달의 `userToggledHints` 와 같은 규칙. 팝업을 새로 열 때마다 리셋된다.
184
202
  */
185
203
  private userToggledHints;
@@ -188,10 +206,23 @@ declare class ElementPickingController {
188
206
  constructor(opts: ElementPickingOpts);
189
207
  getState(): PickingState;
190
208
  get isActive(): boolean;
209
+ /** 켜져 있지만 클릭을 안 먹는 상태인가. */
210
+ get isPaused(): boolean;
191
211
  subscribe(listener: PickingListener): () => void;
192
212
  start(): void;
193
213
  /** [지목 종료]. 저장된 플래그까지 지워서 새로고침해도 다시 켜지지 않게 한다. */
194
214
  stop(): void;
215
+ /**
216
+ * 잠시 멈춘다 — 페이지 클릭이 원래대로 동작하고, 모드는 켜진 채로 남는다.
217
+ *
218
+ * 팝업이 열려 있으면 **그대로 둔다.** 쓰던 한 줄을 여기서 지우면 「지목 켜면 모달 초안이
219
+ * 날아간다」와 같은 사고를 다른 자리에 만드는 셈이다. 팝업은 위젯 자신의 UI 라
220
+ * 리스너를 떼도 계속 눌린다.
221
+ */
222
+ pause(): void;
223
+ /** 다시 지목을 받는다. */
224
+ resume(): void;
225
+ togglePause(): void;
195
226
  /**
196
227
  * 저장돼 있던 모드를 되살린다. 새로고침·페이지 이동 직후에 한 번 부른다.
197
228
  * @returns 되살아났으면 true.
@@ -220,7 +251,7 @@ declare class ElementPickingController {
220
251
  * 아니면 줄바꿈 뒤 이어붙인다 — 모달의 `applyHint` 와 동일한 규칙이다.
221
252
  */
222
253
  applyHint(hintId: string): void;
223
- /** [제안 ▾] 토글. 방향과 무관하게 이후 자동 접힘/펼침을 멈춘다. */
254
+ /** [이런 건가요? ▾] 토글. 방향과 무관하게 이후 자동 접힘/펼침을 멈춘다. */
224
255
  toggleHints(): void;
225
256
  /**
226
257
  * 사용자가 붙여넣기(Cmd+V)나 드래그로 넣은 그림. 자동 캡처 결과를 덮는다.
package/dist/index.d.ts CHANGED
@@ -109,6 +109,18 @@ interface AnnotationPopupState {
109
109
  }
110
110
  interface PickingState {
111
111
  active: boolean;
112
+ /**
113
+ * 켜져 있지만 **클릭 가로채기만 잠시 끈 상태**.
114
+ *
115
+ * 왜 필요한가: 지목 모드는 페이지의 모든 클릭을 먹는다(그래야 지목하려고 누른 링크가
116
+ * 실제로 이동해 버리지 않는다). 그 대가로 **제보할 화면을 만들 수가 없다** — 드롭다운을
117
+ * 열고 그 안의 항목을 지목하고 싶어도 여는 클릭부터 막힌다. 그때 모드를 끄면 다시
118
+ * 켜야 하고, 사용자에겐 그게 "모드가 유지되지 않는다"로 읽힌다.
119
+ *
120
+ * 그래서 끄는 것과 멈추는 것을 나눈다. 멈춘 동안 페이지는 원래대로 동작하고, 모드는
121
+ * 켜진 채로 남는다.
122
+ */
123
+ paused: boolean;
112
124
  /** 지금 마우스가 올라가 있는 요소. 하이라이트를 그리는 근거. */
113
125
  hovered: ElementInfo | null;
114
126
  popup: AnnotationPopupState | null;
@@ -167,6 +179,12 @@ declare class ElementPickingController {
167
179
  private readonly rankHintsFor;
168
180
  private readonly listeners;
169
181
  private active;
182
+ /**
183
+ * 일시중지. **저장하지 않는다** — 멈추는 목적이 대개 "링크를 눌러 다른 화면으로 가는 것"
184
+ * 이라, 그 이동이 끝나면 지목이 돌아와 있는 게 목적에 맞다. 같은 이유로 경로가 바뀌면
185
+ * 스스로 풀린다(`syncPath`) — 그래야 SPA 이동과 새로고침이 같게 동작한다.
186
+ */
187
+ private paused;
170
188
  private attached;
171
189
  private hovered;
172
190
  private popup;
@@ -179,7 +197,7 @@ declare class ElementPickingController {
179
197
  /** 지금 도는 자동 캡처. Enter 가 캡처보다 빨랐을 때 기다릴 대상. */
180
198
  private capturing;
181
199
  /**
182
- * 사용자가 [제안 ▾] 를 직접 건드렸는가. 한 번이라도 건드리면 이후 타이핑에 의한 자동
200
+ * 사용자가 [이런 건가요? ▾] 를 직접 건드렸는가. 한 번이라도 건드리면 이후 타이핑에 의한 자동
183
201
  * 접힘/펼침이 멈춘다 — 모달의 `userToggledHints` 와 같은 규칙. 팝업을 새로 열 때마다 리셋된다.
184
202
  */
185
203
  private userToggledHints;
@@ -188,10 +206,23 @@ declare class ElementPickingController {
188
206
  constructor(opts: ElementPickingOpts);
189
207
  getState(): PickingState;
190
208
  get isActive(): boolean;
209
+ /** 켜져 있지만 클릭을 안 먹는 상태인가. */
210
+ get isPaused(): boolean;
191
211
  subscribe(listener: PickingListener): () => void;
192
212
  start(): void;
193
213
  /** [지목 종료]. 저장된 플래그까지 지워서 새로고침해도 다시 켜지지 않게 한다. */
194
214
  stop(): void;
215
+ /**
216
+ * 잠시 멈춘다 — 페이지 클릭이 원래대로 동작하고, 모드는 켜진 채로 남는다.
217
+ *
218
+ * 팝업이 열려 있으면 **그대로 둔다.** 쓰던 한 줄을 여기서 지우면 「지목 켜면 모달 초안이
219
+ * 날아간다」와 같은 사고를 다른 자리에 만드는 셈이다. 팝업은 위젯 자신의 UI 라
220
+ * 리스너를 떼도 계속 눌린다.
221
+ */
222
+ pause(): void;
223
+ /** 다시 지목을 받는다. */
224
+ resume(): void;
225
+ togglePause(): void;
195
226
  /**
196
227
  * 저장돼 있던 모드를 되살린다. 새로고침·페이지 이동 직후에 한 번 부른다.
197
228
  * @returns 되살아났으면 true.
@@ -220,7 +251,7 @@ declare class ElementPickingController {
220
251
  * 아니면 줄바꿈 뒤 이어붙인다 — 모달의 `applyHint` 와 동일한 규칙이다.
221
252
  */
222
253
  applyHint(hintId: string): void;
223
- /** [제안 ▾] 토글. 방향과 무관하게 이후 자동 접힘/펼침을 멈춘다. */
254
+ /** [이런 건가요? ▾] 토글. 방향과 무관하게 이후 자동 접힘/펼침을 멈춘다. */
224
255
  toggleHints(): void;
225
256
  /**
226
257
  * 사용자가 붙여넣기(Cmd+V)나 드래그로 넣은 그림. 자동 캡처 결과를 덮는다.
package/dist/index.js CHANGED
@@ -320,6 +320,12 @@ var ElementPickingController = class {
320
320
  constructor(opts) {
321
321
  this.listeners = /* @__PURE__ */ new Set();
322
322
  this.active = false;
323
+ /**
324
+ * 일시중지. **저장하지 않는다** — 멈추는 목적이 대개 "링크를 눌러 다른 화면으로 가는 것"
325
+ * 이라, 그 이동이 끝나면 지목이 돌아와 있는 게 목적에 맞다. 같은 이유로 경로가 바뀌면
326
+ * 스스로 풀린다(`syncPath`) — 그래야 SPA 이동과 새로고침이 같게 동작한다.
327
+ */
328
+ this.paused = false;
323
329
  this.attached = false;
324
330
  this.hovered = null;
325
331
  this.popup = null;
@@ -331,7 +337,7 @@ var ElementPickingController = class {
331
337
  /** 지금 도는 자동 캡처. Enter 가 캡처보다 빨랐을 때 기다릴 대상. */
332
338
  this.capturing = null;
333
339
  /**
334
- * 사용자가 [제안 ▾] 를 직접 건드렸는가. 한 번이라도 건드리면 이후 타이핑에 의한 자동
340
+ * 사용자가 [이런 건가요? ▾] 를 직접 건드렸는가. 한 번이라도 건드리면 이후 타이핑에 의한 자동
335
341
  * 접힘/펼침이 멈춘다 — 모달의 `userToggledHints` 와 같은 규칙. 팝업을 새로 열 때마다 리셋된다.
336
342
  */
337
343
  this.userToggledHints = false;
@@ -358,6 +364,7 @@ var ElementPickingController = class {
358
364
  getState() {
359
365
  return {
360
366
  active: this.active,
367
+ paused: this.paused,
361
368
  hovered: this.hovered,
362
369
  popup: this.popup,
363
370
  markers: this.markers
@@ -366,6 +373,10 @@ var ElementPickingController = class {
366
373
  get isActive() {
367
374
  return this.active;
368
375
  }
376
+ /** 켜져 있지만 클릭을 안 먹는 상태인가. */
377
+ get isPaused() {
378
+ return this.paused;
379
+ }
369
380
  subscribe(listener) {
370
381
  this.listeners.add(listener);
371
382
  listener(this.getState());
@@ -377,6 +388,7 @@ var ElementPickingController = class {
377
388
  start() {
378
389
  if (this.active) return;
379
390
  this.active = true;
391
+ this.paused = false;
380
392
  this.store.setPickingActive(true);
381
393
  this.attach();
382
394
  this.startPathWatch();
@@ -386,6 +398,7 @@ var ElementPickingController = class {
386
398
  /** [지목 종료]. 저장된 플래그까지 지워서 새로고침해도 다시 켜지지 않게 한다. */
387
399
  stop() {
388
400
  this.active = false;
401
+ this.paused = false;
389
402
  this.store.setPickingActive(false);
390
403
  this.detach();
391
404
  this.stopPathWatch();
@@ -393,6 +406,31 @@ var ElementPickingController = class {
393
406
  this.popup = null;
394
407
  this.emit();
395
408
  }
409
+ /**
410
+ * 잠시 멈춘다 — 페이지 클릭이 원래대로 동작하고, 모드는 켜진 채로 남는다.
411
+ *
412
+ * 팝업이 열려 있으면 **그대로 둔다.** 쓰던 한 줄을 여기서 지우면 「지목 켜면 모달 초안이
413
+ * 날아간다」와 같은 사고를 다른 자리에 만드는 셈이다. 팝업은 위젯 자신의 UI 라
414
+ * 리스너를 떼도 계속 눌린다.
415
+ */
416
+ pause() {
417
+ if (!this.active || this.paused) return;
418
+ this.paused = true;
419
+ this.detach();
420
+ this.hovered = null;
421
+ this.emit();
422
+ }
423
+ /** 다시 지목을 받는다. */
424
+ resume() {
425
+ if (!this.active || !this.paused) return;
426
+ this.paused = false;
427
+ this.attach();
428
+ this.emit();
429
+ }
430
+ togglePause() {
431
+ if (this.paused) this.resume();
432
+ else this.pause();
433
+ }
396
434
  /**
397
435
  * 저장돼 있던 모드를 되살린다. 새로고침·페이지 이동 직후에 한 번 부른다.
398
436
  * @returns 되살아났으면 true.
@@ -408,6 +446,7 @@ var ElementPickingController = class {
408
446
  }
409
447
  /** 경로가 바뀌었을 때 그 경로의 마커로 갈아 끼운다. */
410
448
  syncPath() {
449
+ if (this.paused) this.resume();
411
450
  this.lastPathname = this.getPathname();
412
451
  this.markers = this.store.list(this.lastPathname);
413
452
  this.reconcileMarkerOutcomes();
@@ -436,12 +475,12 @@ var ElementPickingController = class {
436
475
  this.attached = false;
437
476
  }
438
477
  handleMouseOver(event) {
439
- if (!this.active || isOwnUi(event.target)) return;
478
+ if (!this.active || this.paused || isOwnUi(event.target)) return;
440
479
  this.hovered = describeElement(event.target);
441
480
  this.emit();
442
481
  }
443
482
  handleClick(event) {
444
- if (!this.active) return;
483
+ if (!this.active || this.paused) return;
445
484
  if (isOwnUi(event.target)) return;
446
485
  event.preventDefault();
447
486
  event.stopPropagation();
@@ -522,7 +561,7 @@ ${hint.draft}`;
522
561
  };
523
562
  this.emit();
524
563
  }
525
- /** [제안 ▾] 토글. 방향과 무관하게 이후 자동 접힘/펼침을 멈춘다. */
564
+ /** [이런 건가요? ▾] 토글. 방향과 무관하게 이후 자동 접힘/펼침을 멈춘다. */
526
565
  toggleHints() {
527
566
  if (!this.popup) return;
528
567
  this.userToggledHints = true;
@@ -973,7 +1012,7 @@ import {
973
1012
  } from "react";
974
1013
 
975
1014
  // src/version.ts
976
- var VERSION = true ? "0.6.0" : "dev";
1015
+ var VERSION = true ? "0.7.0" : "dev";
977
1016
 
978
1017
  // src/feedback-kit.tsx
979
1018
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
@@ -995,6 +1034,7 @@ var OWN_UI_PROPS = { [OWN_UI_ATTR]: "" };
995
1034
  var FONT_STACK = "ui-sans-serif, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif";
996
1035
  var INITIAL_PICKING_STATE = {
997
1036
  active: false,
1037
+ paused: false,
998
1038
  hovered: null,
999
1039
  popup: null,
1000
1040
  markers: []
@@ -1036,26 +1076,41 @@ async function transcodeToJpeg(dataUrl) {
1036
1076
  return null;
1037
1077
  }
1038
1078
  }
1079
+ function looksLikeImageName(name) {
1080
+ return /\.(png|jpe?g|gif|webp|bmp|avif|heic|heif)$/i.test(name);
1081
+ }
1082
+ function isImageFile(file) {
1083
+ if (file.type.startsWith("image/")) return true;
1084
+ return (file.type === "" || file.type === "application/octet-stream") && looksLikeImageName(file.name);
1085
+ }
1086
+ function contentTypeOf(file) {
1087
+ if (file.type !== "") return file.type;
1088
+ const name = file instanceof File ? file.name : "";
1089
+ if (/\.png$/i.test(name)) return "image/png";
1090
+ if (/\.jpe?g$/i.test(name)) return "image/jpeg";
1091
+ return "";
1092
+ }
1039
1093
  async function readImageFile(file) {
1040
- if (!file.type.startsWith("image/")) return null;
1094
+ const named = file instanceof File ? file : null;
1095
+ if (!file.type.startsWith("image/") && !(named !== null && isImageFile(named))) return null;
1041
1096
  const dataUrl = await readDataUrl(file);
1042
1097
  if (!dataUrl) return null;
1043
- if (file.type === "image/png" || file.type === "image/jpeg") {
1098
+ const type = contentTypeOf(file);
1099
+ if (type === "image/png" || type === "image/jpeg") {
1044
1100
  const base64 = base64Of(dataUrl);
1045
- return base64 ? { base64, contentType: file.type } : null;
1101
+ return base64 ? { base64, contentType: type } : null;
1046
1102
  }
1047
1103
  return transcodeToJpeg(dataUrl);
1048
1104
  }
1049
1105
  function firstImageOf(transfer) {
1050
1106
  if (!transfer) return null;
1051
1107
  for (const item of Array.from(transfer.items ?? [])) {
1052
- if (item.kind === "file" && item.type.startsWith("image/")) {
1053
- const file = item.getAsFile();
1054
- if (file) return file;
1055
- }
1108
+ if (item.kind !== "file") continue;
1109
+ const file = item.getAsFile();
1110
+ if (file && isImageFile(file)) return file;
1056
1111
  }
1057
1112
  for (const file of Array.from(transfer.files ?? [])) {
1058
- if (file.type.startsWith("image/")) return file;
1113
+ if (isImageFile(file)) return file;
1059
1114
  }
1060
1115
  return null;
1061
1116
  }
@@ -1133,6 +1188,7 @@ var inputStyle = {
1133
1188
  };
1134
1189
  function PickToggle({
1135
1190
  active,
1191
+ paused,
1136
1192
  disabled,
1137
1193
  variant,
1138
1194
  focusId,
@@ -1160,14 +1216,9 @@ function PickToggle({
1160
1216
  // 항상 값을 준다. 조건부로 빼면 리렌더 때 shorthand(border)와 충돌한다고 React 가 경고한다.
1161
1217
  borderColor: active ? TOKENS.accent : TOKENS.line,
1162
1218
  ...floating ? {
1163
- position: "fixed",
1164
- top: 16,
1165
- left: "50%",
1166
- transform: "translateX(-50%)",
1167
- // 오버레이 자체는 클릭을 통과시킨다(pointerEvents:none) — 이 토글만 되살린다.
1168
- pointerEvents: "auto",
1169
- background: TOKENS.surface,
1170
- boxShadow: TOKENS.shadow
1219
+ // 위치는 감싸는 `PickBar` 가 잡는다 — 옆에 [잠시 멈춤] 이 함께 서야 하는데
1220
+ // 토글이 스스로 fixed 면 둘을 나란히 둘 수 없다.
1221
+ background: TOKENS.surface
1171
1222
  } : {
1172
1223
  width: "100%",
1173
1224
  justifyContent: "space-between",
@@ -1184,8 +1235,12 @@ function PickToggle({
1184
1235
  /* @__PURE__ */ jsx(
1185
1236
  "span",
1186
1237
  {
1187
- style: { fontSize: 12, fontWeight: 600, color: active ? TOKENS.accent : TOKENS.muted },
1188
- children: active ? "\uCF1C\uC9D0" : "\uAEBC\uC9D0"
1238
+ style: {
1239
+ fontSize: 12,
1240
+ fontWeight: 600,
1241
+ color: active && !paused ? TOKENS.accent : TOKENS.muted
1242
+ },
1243
+ children: active ? paused ? "\uC77C\uC2DC\uC911\uC9C0" : "\uCF1C\uC9D0" : "\uAEBC\uC9D0"
1189
1244
  }
1190
1245
  ),
1191
1246
  /* @__PURE__ */ jsx(
@@ -1197,7 +1252,7 @@ function PickToggle({
1197
1252
  width: 38,
1198
1253
  height: 22,
1199
1254
  borderRadius: 11,
1200
- background: active ? TOKENS.accent : TOKENS.line
1255
+ background: active ? paused ? TOKENS.muted : TOKENS.accent : TOKENS.line
1201
1256
  },
1202
1257
  children: /* @__PURE__ */ jsx(
1203
1258
  "span",
@@ -1221,6 +1276,58 @@ function PickToggle({
1221
1276
  }
1222
1277
  );
1223
1278
  }
1279
+ function PickBar({
1280
+ paused,
1281
+ onToggle,
1282
+ onTogglePause
1283
+ }) {
1284
+ return /* @__PURE__ */ jsxs(
1285
+ "div",
1286
+ {
1287
+ style: {
1288
+ position: "fixed",
1289
+ top: 16,
1290
+ left: "50%",
1291
+ transform: "translateX(-50%)",
1292
+ display: "flex",
1293
+ alignItems: "stretch",
1294
+ gap: 8,
1295
+ // 오버레이 자체는 클릭을 통과시킨다(pointerEvents:none) — 이 막대만 되살린다.
1296
+ pointerEvents: "auto",
1297
+ padding: 6,
1298
+ borderRadius: 12,
1299
+ background: TOKENS.surface,
1300
+ boxShadow: TOKENS.shadow
1301
+ },
1302
+ children: [
1303
+ /* @__PURE__ */ jsx(PickToggle, { variant: "floating", active: true, paused, onToggle }),
1304
+ /* @__PURE__ */ jsxs(
1305
+ "button",
1306
+ {
1307
+ type: "button",
1308
+ "aria-pressed": paused,
1309
+ "data-fk-pick-pause": paused ? "paused" : "running",
1310
+ onClick: onTogglePause,
1311
+ style: {
1312
+ ...baseButton,
1313
+ display: "inline-flex",
1314
+ alignItems: "center",
1315
+ gap: 6,
1316
+ whiteSpace: "nowrap",
1317
+ borderColor: paused ? TOKENS.accent : TOKENS.line,
1318
+ color: paused ? TOKENS.accent : TOKENS.ink,
1319
+ fontWeight: 600
1320
+ },
1321
+ children: [
1322
+ /* @__PURE__ */ jsx("span", { "aria-hidden": "true", children: paused ? "\u25B6" : "\u23F8" }),
1323
+ paused ? "\uC9C0\uBAA9 \uC7AC\uAC1C" : "\uC7A0\uC2DC \uBA48\uCDA4"
1324
+ ]
1325
+ }
1326
+ )
1327
+ ]
1328
+ }
1329
+ );
1330
+ }
1224
1331
  function MarkerStatus({ status }) {
1225
1332
  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 };
1226
1333
  return /* @__PURE__ */ jsxs("span", { style: { display: "inline-flex", gap: 4, alignItems: "center", color: presentation.color }, children: [
@@ -1244,47 +1351,7 @@ function HintChips({
1244
1351
  onToggle
1245
1352
  }) {
1246
1353
  return /* @__PURE__ */ jsxs("div", { style: { marginBottom: 16 }, children: [
1247
- hintsExpanded ? /* @__PURE__ */ jsxs(Fragment, { children: [
1248
- /* @__PURE__ */ jsx("p", { style: { margin: "0 0 7px", color: TOKENS.muted, fontSize: 12 }, children: "\uC774\uB7F0 \uAC74\uAC00\uC694?" }),
1249
- /* @__PURE__ */ jsx(
1250
- "div",
1251
- {
1252
- role: "group",
1253
- "aria-label": "\uC790\uC8FC \uB098\uC624\uB294 \uC81C\uBCF4 \uC81C\uC548",
1254
- style: { display: "flex", flexWrap: "wrap", gap: 6, marginBottom: 7 },
1255
- children: hints.map((hint) => {
1256
- const used = usedHintIds.includes(hint.id);
1257
- return /* @__PURE__ */ jsx(
1258
- "button",
1259
- {
1260
- type: "button",
1261
- "data-fk-focus-id": `hint:${hint.id}`,
1262
- disabled: used || disabled,
1263
- "aria-pressed": used,
1264
- onClick: () => onApply(hint.id),
1265
- style: {
1266
- ...baseButton,
1267
- minHeight: 30,
1268
- padding: "5px 10px",
1269
- fontSize: 12,
1270
- fontWeight: 600,
1271
- borderRadius: 999,
1272
- ...used ? {
1273
- background: TOKENS.subtle,
1274
- color: TOKENS.muted,
1275
- borderColor: TOKENS.line,
1276
- cursor: "default"
1277
- } : {}
1278
- },
1279
- children: hint.label
1280
- },
1281
- hint.id
1282
- );
1283
- })
1284
- }
1285
- )
1286
- ] }) : null,
1287
- /* @__PURE__ */ jsx(
1354
+ /* @__PURE__ */ jsxs(
1288
1355
  "button",
1289
1356
  {
1290
1357
  type: "button",
@@ -1292,10 +1359,61 @@ function HintChips({
1292
1359
  "aria-expanded": hintsExpanded,
1293
1360
  disabled,
1294
1361
  onClick: onToggle,
1295
- style: { ...baseButton, minHeight: 28, padding: "4px 10px", fontSize: 12 },
1296
- children: hintsExpanded ? "\uC81C\uC548 \u25B4" : "\uC81C\uC548 \u25BE"
1362
+ style: {
1363
+ display: "block",
1364
+ margin: "0 0 7px",
1365
+ padding: 0,
1366
+ border: "none",
1367
+ background: "none",
1368
+ font: "inherit",
1369
+ fontSize: 12,
1370
+ color: TOKENS.muted,
1371
+ cursor: disabled ? "default" : "pointer",
1372
+ textAlign: "left"
1373
+ },
1374
+ children: [
1375
+ "\uC774\uB7F0 \uAC74\uAC00\uC694? ",
1376
+ /* @__PURE__ */ jsx("span", { "aria-hidden": "true", children: hintsExpanded ? "\u25B4" : "\u25BE" })
1377
+ ]
1297
1378
  }
1298
- )
1379
+ ),
1380
+ hintsExpanded ? /* @__PURE__ */ jsx(Fragment, { children: /* @__PURE__ */ jsx(
1381
+ "div",
1382
+ {
1383
+ role: "group",
1384
+ "aria-label": "\uC790\uC8FC \uB098\uC624\uB294 \uC81C\uBCF4 \uC81C\uC548",
1385
+ style: { display: "flex", flexWrap: "wrap", gap: 6, marginBottom: 7 },
1386
+ children: hints.map((hint) => {
1387
+ const used = usedHintIds.includes(hint.id);
1388
+ return /* @__PURE__ */ jsx(
1389
+ "button",
1390
+ {
1391
+ type: "button",
1392
+ "data-fk-focus-id": `hint:${hint.id}`,
1393
+ disabled: used || disabled,
1394
+ "aria-pressed": used,
1395
+ onClick: () => onApply(hint.id),
1396
+ style: {
1397
+ ...baseButton,
1398
+ minHeight: 30,
1399
+ padding: "5px 10px",
1400
+ fontSize: 12,
1401
+ fontWeight: 600,
1402
+ borderRadius: 999,
1403
+ ...used ? {
1404
+ background: TOKENS.subtle,
1405
+ color: TOKENS.muted,
1406
+ borderColor: TOKENS.line,
1407
+ cursor: "default"
1408
+ } : {}
1409
+ },
1410
+ children: hint.label
1411
+ },
1412
+ hint.id
1413
+ );
1414
+ })
1415
+ }
1416
+ ) }) : null
1299
1417
  ] });
1300
1418
  }
1301
1419
  function FeedbackKit(props) {
@@ -1397,6 +1515,23 @@ function FeedbackKit(props) {
1397
1515
  document.addEventListener("keydown", onKeyDownCapture, true);
1398
1516
  return () => document.removeEventListener("keydown", onKeyDownCapture, true);
1399
1517
  }, [kit, widgetState, pickingState.popup]);
1518
+ useEffect(() => {
1519
+ if (!kit || !widgetState?.modal.open) return;
1520
+ if (pickingState.popup) return;
1521
+ const locked = widgetState.modal.submitStatus === "sending" || widgetState.modal.submitStatus === "pending";
1522
+ if (locked) return;
1523
+ const controller = kit;
1524
+ async function onPaste(event) {
1525
+ const file = firstImageOf(event.clipboardData);
1526
+ if (!file) return;
1527
+ event.preventDefault();
1528
+ const shot = await readImageFile(file);
1529
+ if (shot) await controller.widget.modal.attachFile(shot);
1530
+ }
1531
+ const handler = (event) => void onPaste(event);
1532
+ document.addEventListener("paste", handler);
1533
+ return () => document.removeEventListener("paste", handler);
1534
+ }, [kit, widgetState, pickingState.popup]);
1400
1535
  if (!kit || !widgetState) {
1401
1536
  return /* @__PURE__ */ jsx("div", { ...OWN_UI_PROPS, "data-feedback-kit-loading": "true" });
1402
1537
  }
@@ -1434,6 +1569,14 @@ function FeedbackKit(props) {
1434
1569
  }
1435
1570
  input.value = "";
1436
1571
  }
1572
+ async function attachToModal(transfer) {
1573
+ const file = firstImageOf(transfer);
1574
+ if (!file) return false;
1575
+ const screenshot = await readImageFile(file);
1576
+ if (!screenshot) return false;
1577
+ await activeKit.widget.modal.attachFile(screenshot);
1578
+ return true;
1579
+ }
1437
1580
  async function attachToPopup(transfer) {
1438
1581
  const file = firstImageOf(transfer);
1439
1582
  if (!file) return false;
@@ -1534,6 +1677,14 @@ function FeedbackKit(props) {
1534
1677
  "aria-modal": "true",
1535
1678
  "aria-labelledby": "feedback-kit-dialog-title",
1536
1679
  onKeyDown: handleModalKeyDown,
1680
+ onDragOver: (event) => {
1681
+ if (!draftLocked && firstImageOf(event.dataTransfer)) event.preventDefault();
1682
+ },
1683
+ onDrop: (event) => {
1684
+ if (draftLocked || !firstImageOf(event.dataTransfer)) return;
1685
+ event.preventDefault();
1686
+ void attachToModal(event.dataTransfer);
1687
+ },
1537
1688
  style: {
1538
1689
  width: "min(520px, 100%)",
1539
1690
  maxHeight: "min(760px, calc(100vh - 40px))",
@@ -1581,6 +1732,7 @@ function FeedbackKit(props) {
1581
1732
  {
1582
1733
  variant: "inline",
1583
1734
  active: pickingActive,
1735
+ paused: pickingState.paused,
1584
1736
  disabled: draftLocked,
1585
1737
  focusId: MODAL_ACTION_PICK,
1586
1738
  onToggle: togglePicking
@@ -1832,7 +1984,14 @@ function FeedbackKit(props) {
1832
1984
  }
1833
1985
  }
1834
1986
  ) : null,
1835
- /* @__PURE__ */ jsx(PickToggle, { variant: "floating", active: true, onToggle: togglePicking }),
1987
+ /* @__PURE__ */ jsx(
1988
+ PickBar,
1989
+ {
1990
+ paused: pickingState.paused,
1991
+ onToggle: togglePicking,
1992
+ onTogglePause: () => activeKit.picking.togglePause()
1993
+ }
1994
+ ),
1836
1995
  pickingState.markers.map((marker, index) => /* @__PURE__ */ jsx(
1837
1996
  "div",
1838
1997
  {