@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.cjs +228 -69
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +33 -2
- package/dist/index.d.ts +33 -2
- package/dist/index.js +228 -69
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
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.
|
|
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
|
-
|
|
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
|
-
|
|
1098
|
+
const type = contentTypeOf(file);
|
|
1099
|
+
if (type === "image/png" || type === "image/jpeg") {
|
|
1044
1100
|
const base64 = base64Of(dataUrl);
|
|
1045
|
-
return base64 ? { base64, contentType:
|
|
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
|
|
1053
|
-
|
|
1054
|
-
|
|
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
|
|
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
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
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: {
|
|
1188
|
-
|
|
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
|
-
|
|
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: {
|
|
1296
|
-
|
|
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(
|
|
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
|
{
|