@solhun/feedback-kit-web 0.1.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.js ADDED
@@ -0,0 +1,1374 @@
1
+ // src/index.ts
2
+ import { resolveConfig, shouldShowWidget } from "@solhun/feedback-kit-core";
3
+ import { parseSourceAttr, SOURCE_ATTR, sourceFromElement } from "@solhun/feedback-kit-core";
4
+ import {
5
+ COMMENT_MAX_CHARS as COMMENT_MAX_CHARS2,
6
+ COMMENT_REQUIRED_MESSAGE,
7
+ COMMENT_TOO_LONG_MESSAGE,
8
+ denormalizePin,
9
+ FLOATING_BUTTON_ID as FLOATING_BUTTON_ID2,
10
+ MODAL_ACTION_PICK as MODAL_ACTION_PICK2,
11
+ normalizePin as normalizePin2,
12
+ ReportModalController,
13
+ SCREENSHOT_FAILED_MESSAGE,
14
+ SUBMIT_DONE_MESSAGE,
15
+ SUBMIT_PENDING_MESSAGE,
16
+ WidgetController as WidgetController2
17
+ } from "@solhun/feedback-kit-core";
18
+
19
+ // src/element-info.ts
20
+ var ELEMENT_TEXT_MAX_CHARS = 200;
21
+ var SELECTOR_MAX_DEPTH = 8;
22
+ var KEPT_ATTRS = [
23
+ "id",
24
+ "class",
25
+ "name",
26
+ "type",
27
+ "role",
28
+ "href",
29
+ "aria-label",
30
+ "data-testid",
31
+ "data-fk-source"
32
+ ];
33
+ function cssEscape(value) {
34
+ const fn = globalThis.CSS?.escape;
35
+ if (typeof fn === "function") return fn(value);
36
+ return value.replace(/[^a-zA-Z0-9_-]/g, (ch) => `\\${ch}`);
37
+ }
38
+ function nthOfType(el) {
39
+ const parent = el.parentElement;
40
+ if (!parent) return 1;
41
+ let index = 0;
42
+ for (const child of Array.from(parent.children)) {
43
+ if (child.tagName === el.tagName) {
44
+ index += 1;
45
+ if (child === el) return index;
46
+ }
47
+ }
48
+ return 1;
49
+ }
50
+ function hasTypeSiblings(el) {
51
+ const parent = el.parentElement;
52
+ if (!parent) return false;
53
+ let count = 0;
54
+ for (const child of Array.from(parent.children)) {
55
+ if (child.tagName === el.tagName) {
56
+ count += 1;
57
+ if (count > 1) return true;
58
+ }
59
+ }
60
+ return false;
61
+ }
62
+ function cssSelectorPath(el) {
63
+ if (!el || typeof el.tagName !== "string") return null;
64
+ try {
65
+ const parts = [];
66
+ let node = el;
67
+ let depth = 0;
68
+ while (node && depth < SELECTOR_MAX_DEPTH) {
69
+ const tag = node.tagName.toLowerCase();
70
+ if (tag === "html" || tag === "body") {
71
+ parts.unshift(tag);
72
+ break;
73
+ }
74
+ const id = node.getAttribute("id");
75
+ if (id) {
76
+ parts.unshift(`#${cssEscape(id)}`);
77
+ break;
78
+ }
79
+ parts.unshift(hasTypeSiblings(node) ? `${tag}:nth-of-type(${nthOfType(node)})` : tag);
80
+ node = node.parentElement;
81
+ depth += 1;
82
+ }
83
+ return parts.length > 0 ? parts.join(" > ") : null;
84
+ } catch {
85
+ return null;
86
+ }
87
+ }
88
+ function visibleText(el) {
89
+ const raw = el.innerText ?? el.textContent ?? "";
90
+ const collapsed = raw.replace(/\s+/g, " ").trim();
91
+ if (collapsed === "") return null;
92
+ return collapsed.length > ELEMENT_TEXT_MAX_CHARS ? collapsed.slice(0, ELEMENT_TEXT_MAX_CHARS) : collapsed;
93
+ }
94
+ function describeElement(el) {
95
+ if (!el || typeof el.tagName !== "string") return null;
96
+ const attributes = {};
97
+ for (const name of KEPT_ATTRS) {
98
+ const value = el.getAttribute(name);
99
+ if (value !== null) attributes[name] = value;
100
+ }
101
+ let boundingBox = null;
102
+ try {
103
+ const rect = el.getBoundingClientRect();
104
+ boundingBox = { x: rect.left, y: rect.top, width: rect.width, height: rect.height };
105
+ } catch {
106
+ boundingBox = null;
107
+ }
108
+ return {
109
+ tag: el.tagName.toLowerCase(),
110
+ id: el.getAttribute("id"),
111
+ className: el.getAttribute("class"),
112
+ text: visibleText(el),
113
+ boundingBox,
114
+ attributes,
115
+ selector: cssSelectorPath(el)
116
+ };
117
+ }
118
+
119
+ // src/marker-store.ts
120
+ var PICKING_MODE_KEY = "feedback-kit:picking-mode";
121
+ var MARKER_KEY_PREFIX = "feedback-kit:markers:";
122
+ var MAX_MARKERS_PER_PATH = 50;
123
+ function defaultStorage() {
124
+ try {
125
+ const ls = globalThis.localStorage;
126
+ return ls ?? null;
127
+ } catch {
128
+ return null;
129
+ }
130
+ }
131
+ function markerKey(pathname) {
132
+ return `${MARKER_KEY_PREFIX}${pathname}`;
133
+ }
134
+ var MarkerStore = class {
135
+ constructor(storage) {
136
+ this.storage = storage === void 0 ? defaultStorage() : storage;
137
+ }
138
+ read(key) {
139
+ try {
140
+ return this.storage?.getItem(key) ?? null;
141
+ } catch {
142
+ return null;
143
+ }
144
+ }
145
+ write(key, value) {
146
+ try {
147
+ this.storage?.setItem(key, value);
148
+ } catch {
149
+ }
150
+ }
151
+ drop(key) {
152
+ try {
153
+ this.storage?.removeItem(key);
154
+ } catch {
155
+ }
156
+ }
157
+ // ── 지목 모드 ─────────────────────────────────────────────────────────────
158
+ isPickingActive() {
159
+ return this.read(PICKING_MODE_KEY) === "1";
160
+ }
161
+ setPickingActive(active) {
162
+ if (active) this.write(PICKING_MODE_KEY, "1");
163
+ else this.drop(PICKING_MODE_KEY);
164
+ }
165
+ // ── 마커 ──────────────────────────────────────────────────────────────────
166
+ list(pathname) {
167
+ const raw = this.read(markerKey(pathname));
168
+ if (!raw) return [];
169
+ try {
170
+ const parsed = JSON.parse(raw);
171
+ if (!Array.isArray(parsed)) return [];
172
+ return parsed.filter((item) => {
173
+ if (typeof item !== "object" || item === null) return false;
174
+ const m = item;
175
+ return typeof m.id === "string" && typeof m.x === "number" && typeof m.y === "number";
176
+ });
177
+ } catch {
178
+ return [];
179
+ }
180
+ }
181
+ /** 같은 id 가 있으면 덮어쓰고, 없으면 뒤에 붙인다. */
182
+ upsert(pathname, marker) {
183
+ const current = this.list(pathname);
184
+ const index = current.findIndex((m) => m.id === marker.id);
185
+ if (index >= 0) current[index] = marker;
186
+ else current.push(marker);
187
+ const trimmed = current.length > MAX_MARKERS_PER_PATH ? current.slice(current.length - MAX_MARKERS_PER_PATH) : current;
188
+ this.write(markerKey(pathname), JSON.stringify(trimmed));
189
+ return trimmed;
190
+ }
191
+ /** 전송 상태만 바꾼다. 없는 id 면 아무것도 하지 않는다. */
192
+ updateStatus(pathname, id, status) {
193
+ const current = this.list(pathname);
194
+ const index = current.findIndex((m) => m.id === id);
195
+ if (index < 0) return current;
196
+ current[index] = { ...current[index], status };
197
+ this.write(markerKey(pathname), JSON.stringify(current));
198
+ return current;
199
+ }
200
+ clear(pathname) {
201
+ this.drop(markerKey(pathname));
202
+ }
203
+ };
204
+
205
+ // src/picking.ts
206
+ import {
207
+ COMMENT_MAX_CHARS,
208
+ normalizePin
209
+ } from "@solhun/feedback-kit-core";
210
+ var OWN_UI_ATTR = "data-feedback-kit";
211
+ function defaultDocument() {
212
+ return globalThis.document ?? null;
213
+ }
214
+ function defaultPathname() {
215
+ const loc = globalThis.location;
216
+ return loc?.pathname ?? "/";
217
+ }
218
+ function defaultViewport() {
219
+ const w = globalThis;
220
+ return { width: w.innerWidth ?? 0, height: w.innerHeight ?? 0 };
221
+ }
222
+ function isOwnUi(target) {
223
+ const el = target;
224
+ if (!el || typeof el.closest !== "function") return false;
225
+ return el.closest(`[${OWN_UI_ATTR}]`) !== null;
226
+ }
227
+ var ElementPickingController = class {
228
+ constructor(opts) {
229
+ this.listeners = /* @__PURE__ */ new Set();
230
+ this.active = false;
231
+ this.attached = false;
232
+ this.hovered = null;
233
+ this.popup = null;
234
+ this.markers = [];
235
+ this.pathWatch = null;
236
+ this.saving = false;
237
+ this.onClick = (event) => this.handleClick(event);
238
+ this.onMouseOver = (event) => this.handleMouseOver(event);
239
+ this.queue = opts.queue;
240
+ this.createReport = opts.createReport;
241
+ this.store = opts.store ?? new MarkerStore();
242
+ this.getPathname = opts.getPathname ?? defaultPathname;
243
+ this.doc = opts.doc === void 0 ? defaultDocument() : opts.doc;
244
+ this.getViewport = opts.getViewport ?? defaultViewport;
245
+ this.lastPathname = this.getPathname();
246
+ this.markers = this.store.list(this.lastPathname);
247
+ this.unsubscribeQueue = this.queue.subscribe?.(() => {
248
+ if (this.reconcileMarkerOutcomes()) this.emit();
249
+ }) ?? (() => void 0);
250
+ }
251
+ // ── 조회 ──────────────────────────────────────────────────────────────────
252
+ getState() {
253
+ return {
254
+ active: this.active,
255
+ hovered: this.hovered,
256
+ popup: this.popup,
257
+ markers: this.markers
258
+ };
259
+ }
260
+ get isActive() {
261
+ return this.active;
262
+ }
263
+ subscribe(listener) {
264
+ this.listeners.add(listener);
265
+ listener(this.getState());
266
+ return () => {
267
+ this.listeners.delete(listener);
268
+ };
269
+ }
270
+ // ── 모드 on/off ───────────────────────────────────────────────────────────
271
+ start() {
272
+ if (this.active) return;
273
+ this.active = true;
274
+ this.store.setPickingActive(true);
275
+ this.attach();
276
+ this.startPathWatch();
277
+ this.markers = this.store.list(this.getPathname());
278
+ this.emit();
279
+ }
280
+ /** [지목 종료]. 저장된 플래그까지 지워서 새로고침해도 다시 켜지지 않게 한다. */
281
+ stop() {
282
+ this.active = false;
283
+ this.store.setPickingActive(false);
284
+ this.detach();
285
+ this.stopPathWatch();
286
+ this.hovered = null;
287
+ this.popup = null;
288
+ this.emit();
289
+ }
290
+ /**
291
+ * 저장돼 있던 모드를 되살린다. 새로고침·페이지 이동 직후에 한 번 부른다.
292
+ * @returns 되살아났으면 true.
293
+ */
294
+ restore() {
295
+ if (!this.store.isPickingActive()) {
296
+ this.markers = this.store.list(this.getPathname());
297
+ this.emit();
298
+ return false;
299
+ }
300
+ this.start();
301
+ return true;
302
+ }
303
+ /** 경로가 바뀌었을 때 그 경로의 마커로 갈아 끼운다. */
304
+ syncPath() {
305
+ this.lastPathname = this.getPathname();
306
+ this.markers = this.store.list(this.lastPathname);
307
+ this.reconcileMarkerOutcomes();
308
+ this.popup = null;
309
+ this.hovered = null;
310
+ this.emit();
311
+ return this.markers;
312
+ }
313
+ dispose() {
314
+ this.detach();
315
+ this.stopPathWatch();
316
+ this.unsubscribeQueue();
317
+ this.listeners.clear();
318
+ }
319
+ // ── DOM 이벤트 ────────────────────────────────────────────────────────────
320
+ attach() {
321
+ if (this.attached || !this.doc) return;
322
+ this.doc.addEventListener("click", this.onClick, true);
323
+ this.doc.addEventListener("mouseover", this.onMouseOver, true);
324
+ this.attached = true;
325
+ }
326
+ detach() {
327
+ if (!this.attached || !this.doc) return;
328
+ this.doc.removeEventListener("click", this.onClick, true);
329
+ this.doc.removeEventListener("mouseover", this.onMouseOver, true);
330
+ this.attached = false;
331
+ }
332
+ handleMouseOver(event) {
333
+ if (!this.active || isOwnUi(event.target)) return;
334
+ this.hovered = describeElement(event.target);
335
+ this.emit();
336
+ }
337
+ handleClick(event) {
338
+ if (!this.active) return;
339
+ if (isOwnUi(event.target)) return;
340
+ event.preventDefault();
341
+ event.stopPropagation();
342
+ if (typeof event.stopImmediatePropagation === "function") {
343
+ event.stopImmediatePropagation();
344
+ }
345
+ const mouse = event;
346
+ this.openAnnotation(event.target, {
347
+ x: mouse.clientX ?? 0,
348
+ y: mouse.clientY ?? 0
349
+ });
350
+ }
351
+ // ── 주석 팝업 ─────────────────────────────────────────────────────────────
352
+ /** 클릭 지점 기준으로 주석 팝업을 연다. 좌표는 해상도 무관한 상대값으로 접어 둔다. */
353
+ openAnnotation(element, clientPoint) {
354
+ const point = normalizePin(clientPoint, this.getViewport());
355
+ this.popup = {
356
+ element: describeElement(element),
357
+ point,
358
+ comment: "",
359
+ canSave: false
360
+ };
361
+ this.emit();
362
+ }
363
+ setAnnotationComment(value) {
364
+ if (!this.popup) return;
365
+ this.popup = {
366
+ ...this.popup,
367
+ comment: value,
368
+ canSave: value.trim().length > 0 && value.length <= COMMENT_MAX_CHARS
369
+ };
370
+ this.emit();
371
+ }
372
+ /** 취소 — 마커도 제보도 남기지 않는다. 모드는 켜진 채로 둔다. */
373
+ cancelAnnotation() {
374
+ this.popup = null;
375
+ this.emit();
376
+ }
377
+ /**
378
+ * 주석 저장. 마커를 먼저 "전송 중"으로 찍고 나서 보낸다 — 사용자는 결과를 기다리지 않고
379
+ * 다음 요소로 넘어갈 수 있어야 한다.
380
+ *
381
+ * @returns 검증에 걸려 아무것도 하지 않았으면 `null`.
382
+ */
383
+ async saveAnnotation() {
384
+ const popup = this.popup;
385
+ if (!popup || !popup.canSave || this.saving) return null;
386
+ this.saving = true;
387
+ const pathname = this.getPathname();
388
+ const parts = {
389
+ kind: "annotation",
390
+ comment: popup.comment,
391
+ priority: "unset",
392
+ screenshot: null,
393
+ pin: popup.point,
394
+ element: popup.element
395
+ };
396
+ let report;
397
+ try {
398
+ report = await this.createReport(parts);
399
+ } catch {
400
+ this.popup = null;
401
+ this.emit();
402
+ this.saving = false;
403
+ return null;
404
+ }
405
+ const marker = {
406
+ id: report.clientSubmissionId,
407
+ x: popup.point.x,
408
+ y: popup.point.y,
409
+ selector: popup.element?.selector ?? null,
410
+ comment: popup.comment,
411
+ status: "sending",
412
+ at: report.createdAt
413
+ };
414
+ this.markers = this.store.upsert(pathname, marker);
415
+ this.popup = null;
416
+ this.emit();
417
+ let outcome;
418
+ try {
419
+ outcome = await this.queue.submit(report);
420
+ } catch {
421
+ outcome = { delivered: false, id: null, queued: true };
422
+ }
423
+ const next = this.store.updateStatus(
424
+ pathname,
425
+ marker.id,
426
+ outcome.delivered ? "done" : "pending"
427
+ );
428
+ if (pathname === this.getPathname()) this.markers = next;
429
+ this.emit();
430
+ this.saving = false;
431
+ return outcome;
432
+ }
433
+ startPathWatch() {
434
+ if (this.pathWatch !== null || typeof setInterval !== "function") return;
435
+ this.lastPathname = this.getPathname();
436
+ this.pathWatch = setInterval(() => {
437
+ const pathname = this.getPathname();
438
+ if (pathname !== this.lastPathname) this.syncPath();
439
+ }, 200);
440
+ }
441
+ stopPathWatch() {
442
+ if (this.pathWatch === null) return;
443
+ clearInterval(this.pathWatch);
444
+ this.pathWatch = null;
445
+ }
446
+ /** 전역 pending 수가 아니라 마커와 같은 clientSubmissionId의 확정 성공만 완료로 바꾼다. */
447
+ reconcileMarkerOutcomes() {
448
+ if (!this.queue.getOutcome) return false;
449
+ let changed = false;
450
+ for (const marker of this.markers) {
451
+ const outcome = this.queue.getOutcome(marker.id);
452
+ if (marker.status !== "done" && outcome?.delivered) {
453
+ this.markers = this.store.updateStatus(this.lastPathname, marker.id, "done");
454
+ changed = true;
455
+ }
456
+ }
457
+ return changed;
458
+ }
459
+ emit() {
460
+ const state = this.getState();
461
+ for (const listener of this.listeners) listener(state);
462
+ }
463
+ };
464
+
465
+ // src/screenshot.ts
466
+ import html2canvas from "html2canvas-pro";
467
+ function payload(dataUrl) {
468
+ const comma = dataUrl.indexOf(",");
469
+ return comma >= 0 ? dataUrl.slice(comma + 1) : null;
470
+ }
471
+ function isOwnUi2(node) {
472
+ return node.hasAttribute?.(OWN_UI_ATTR) === true || node.closest?.(`[${OWN_UI_ATTR}]`) !== null;
473
+ }
474
+ var captureWebScreenshot = async () => {
475
+ const doc = globalThis.document;
476
+ const view = doc?.defaultView;
477
+ if (!doc?.documentElement || !view) return null;
478
+ try {
479
+ const canvas = await html2canvas(doc.documentElement, {
480
+ // 현재 보이는 화면만. 문서 전체를 그리면 제보와 무관한 영역까지 커진다.
481
+ width: view.innerWidth,
482
+ height: view.innerHeight,
483
+ x: view.scrollX,
484
+ y: view.scrollY,
485
+ windowWidth: view.innerWidth,
486
+ windowHeight: view.innerHeight,
487
+ // 배율은 2배까지만. 그 위는 용량만 커지고 8 MiB 한도에 먼저 걸린다.
488
+ scale: Math.min(2, Math.max(1, view.devicePixelRatio || 1)),
489
+ // 위젯 자신의 UI 는 결과에서 뺀다.
490
+ ignoreElements: isOwnUi2,
491
+ // 외부 이미지가 CORS 를 안 열어두면 그것 때문에 전체가 실패할 수 있다.
492
+ // 못 가져오는 리소스는 건너뛰고 나머지를 그린다.
493
+ useCORS: true,
494
+ allowTaint: false,
495
+ logging: false,
496
+ backgroundColor: null
497
+ });
498
+ return { base64: payload(canvas.toDataURL("image/jpeg", 0.9)) ?? "", contentType: "image/jpeg" };
499
+ } catch {
500
+ return null;
501
+ }
502
+ };
503
+ var reencodeWebScreenshot = async (shot, quality) => {
504
+ const doc = globalThis.document;
505
+ if (!doc) return null;
506
+ try {
507
+ const image = await new Promise((resolve) => {
508
+ const img = new Image();
509
+ img.onload = () => resolve(img);
510
+ img.onerror = () => resolve(null);
511
+ img.src = `data:${shot.contentType};base64,${shot.base64}`;
512
+ });
513
+ if (!image) return null;
514
+ const canvas = doc.createElement("canvas");
515
+ canvas.width = image.naturalWidth || image.width;
516
+ canvas.height = image.naturalHeight || image.height;
517
+ const context = canvas.getContext("2d");
518
+ if (!context) return null;
519
+ context.drawImage(image, 0, 0);
520
+ const base64 = payload(canvas.toDataURL("image/jpeg", quality));
521
+ return base64 ? { base64, contentType: "image/jpeg" } : null;
522
+ } catch {
523
+ return null;
524
+ }
525
+ };
526
+
527
+ // src/widget.ts
528
+ import { WidgetController } from "@solhun/feedback-kit-core";
529
+ function createWebWidget(opts) {
530
+ const store = opts.store ?? new MarkerStore();
531
+ const picking = new ElementPickingController({
532
+ queue: opts.queue,
533
+ createReport: opts.createReport,
534
+ store,
535
+ getPathname: opts.getPathname,
536
+ doc: opts.doc,
537
+ getViewport: opts.getViewport
538
+ });
539
+ const widget = new WidgetController({
540
+ ...opts,
541
+ capture: opts.capture === void 0 ? captureWebScreenshot : opts.capture,
542
+ reencode: opts.reencode === void 0 ? reencodeWebScreenshot : opts.reencode,
543
+ platform: "web",
544
+ // 새로고침·페이지 이동 직후에도 켜져 있던 모드를 그대로 이어받는다.
545
+ initialPicking: store.isPickingActive(),
546
+ onPickingChange: (active) => {
547
+ if (active) picking.start();
548
+ else picking.stop();
549
+ }
550
+ });
551
+ picking.restore();
552
+ return {
553
+ widget,
554
+ picking,
555
+ store,
556
+ dispose() {
557
+ picking.dispose();
558
+ widget.dispose();
559
+ }
560
+ };
561
+ }
562
+
563
+ // src/feedback-kit.tsx
564
+ import {
565
+ FLOATING_BUTTON_ID,
566
+ MODAL_ACTION_CANCEL,
567
+ MODAL_ACTION_PICK,
568
+ MODAL_ACTION_REMOVE_SCREENSHOT,
569
+ MODAL_ACTION_RETRY,
570
+ MODAL_ACTION_SEND,
571
+ MODAL_FIELD_COMMENT,
572
+ MODAL_FIELD_PRIORITY
573
+ } from "@solhun/feedback-kit-core";
574
+ import {
575
+ useEffect,
576
+ useRef,
577
+ useState
578
+ } from "react";
579
+ import { jsx, jsxs } from "react/jsx-runtime";
580
+ var TOKENS = {
581
+ ink: "#17202a",
582
+ muted: "#667085",
583
+ surface: "#ffffff",
584
+ subtle: "#f4f6f8",
585
+ line: "#d7dce2",
586
+ accent: "#315d73",
587
+ accentHover: "#274b5d",
588
+ danger: "#b42318",
589
+ warning: "#8a5a00",
590
+ success: "#18794e",
591
+ shadow: "0 18px 50px rgba(23, 32, 42, 0.2)",
592
+ radius: "14px"
593
+ };
594
+ var OWN_UI_PROPS = { [OWN_UI_ATTR]: "" };
595
+ var FONT_STACK = "ui-sans-serif, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif";
596
+ var INITIAL_PICKING_STATE = {
597
+ active: false,
598
+ hovered: null,
599
+ popup: null,
600
+ markers: []
601
+ };
602
+ function screenshotSource(screenshot) {
603
+ return `data:${screenshot.contentType};base64,${screenshot.base64}`;
604
+ }
605
+ function readScreenshotFile(file) {
606
+ if (file.type !== "image/png" && file.type !== "image/jpeg") {
607
+ return Promise.resolve(null);
608
+ }
609
+ const contentType = file.type;
610
+ return new Promise((resolve) => {
611
+ const reader = new FileReader();
612
+ reader.onerror = () => resolve(null);
613
+ reader.onload = () => {
614
+ const result = typeof reader.result === "string" ? reader.result : "";
615
+ const comma = result.indexOf(",");
616
+ if (comma < 0) {
617
+ resolve(null);
618
+ return;
619
+ }
620
+ resolve({
621
+ base64: result.slice(comma + 1),
622
+ contentType
623
+ });
624
+ };
625
+ reader.readAsDataURL(file);
626
+ });
627
+ }
628
+ function nextPaint() {
629
+ return new Promise((resolve) => {
630
+ if (typeof requestAnimationFrame === "function") {
631
+ requestAnimationFrame(() => resolve());
632
+ } else {
633
+ setTimeout(resolve, 0);
634
+ }
635
+ });
636
+ }
637
+ function focusableElements(container) {
638
+ return Array.from(
639
+ container.querySelectorAll(
640
+ "button:not([disabled]), textarea:not([disabled]), select:not([disabled]), input:not([disabled]):not([type='hidden']), [tabindex]:not([tabindex='-1'])"
641
+ )
642
+ ).filter((node) => node.getAttribute("aria-hidden") !== "true");
643
+ }
644
+ function trapTab(event, container) {
645
+ if (event.key !== "Tab") return;
646
+ const items = focusableElements(container);
647
+ if (items.length === 0) {
648
+ event.preventDefault();
649
+ return;
650
+ }
651
+ const first = items[0];
652
+ const last = items[items.length - 1];
653
+ const active = document.activeElement;
654
+ if (event.shiftKey && (active === first || !container.contains(active))) {
655
+ event.preventDefault();
656
+ last.focus();
657
+ } else if (!event.shiftKey && (active === last || !container.contains(active))) {
658
+ event.preventDefault();
659
+ first.focus();
660
+ }
661
+ }
662
+ var baseButton = {
663
+ minHeight: 38,
664
+ border: `1px solid ${TOKENS.line}`,
665
+ borderRadius: 9,
666
+ background: TOKENS.surface,
667
+ color: TOKENS.ink,
668
+ padding: "8px 12px",
669
+ font: "inherit",
670
+ fontWeight: 650,
671
+ cursor: "pointer"
672
+ };
673
+ var primaryButton = {
674
+ ...baseButton,
675
+ borderColor: TOKENS.accent,
676
+ background: TOKENS.accent,
677
+ color: TOKENS.surface
678
+ };
679
+ var inputStyle = {
680
+ width: "100%",
681
+ boxSizing: "border-box",
682
+ border: `1px solid ${TOKENS.line}`,
683
+ borderRadius: 9,
684
+ background: TOKENS.surface,
685
+ color: TOKENS.ink,
686
+ padding: "10px 11px",
687
+ font: "inherit"
688
+ };
689
+ function MarkerStatus({ status }) {
690
+ 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 };
691
+ return /* @__PURE__ */ jsxs("span", { style: { display: "inline-flex", gap: 4, alignItems: "center", color: presentation.color }, children: [
692
+ /* @__PURE__ */ jsx(
693
+ "span",
694
+ {
695
+ "aria-hidden": "true",
696
+ style: status === "sending" ? { display: "inline-block", animation: "feedback-kit-spin 900ms linear infinite" } : void 0,
697
+ children: presentation.icon
698
+ }
699
+ ),
700
+ /* @__PURE__ */ jsx("span", { children: presentation.text })
701
+ ] });
702
+ }
703
+ function FeedbackKit(props) {
704
+ const rootRef = useRef(null);
705
+ const floatingButtonRef = useRef(null);
706
+ const modalRef = useRef(null);
707
+ const confirmRef = useRef(null);
708
+ const fileInputRef = useRef(null);
709
+ const previousScreenRef = useRef("button");
710
+ const previousModalOpenRef = useRef(false);
711
+ const previousConfirmVisibleRef = useRef(false);
712
+ const [kit, setKit] = useState(null);
713
+ const [widgetState, setWidgetState] = useState(null);
714
+ const [pickingState, setPickingState] = useState(INITIAL_PICKING_STATE);
715
+ const [announcement, setAnnouncement] = useState(null);
716
+ const {
717
+ queue,
718
+ createReport,
719
+ capture,
720
+ reencode,
721
+ screenshotLimitBytes,
722
+ store,
723
+ getPathname,
724
+ doc,
725
+ getViewport
726
+ } = props;
727
+ useEffect(() => {
728
+ const created = createWebWidget({
729
+ queue,
730
+ createReport,
731
+ capture,
732
+ reencode,
733
+ screenshotLimitBytes,
734
+ store,
735
+ getPathname,
736
+ doc,
737
+ getViewport
738
+ });
739
+ setKit(created);
740
+ const unsubscribeWidget = created.widget.subscribe(setWidgetState);
741
+ const unsubscribePicking = created.picking.subscribe(setPickingState);
742
+ return () => {
743
+ unsubscribeWidget();
744
+ unsubscribePicking();
745
+ created.dispose();
746
+ };
747
+ }, [
748
+ queue,
749
+ createReport,
750
+ capture,
751
+ reencode,
752
+ screenshotLimitBytes,
753
+ store,
754
+ getPathname,
755
+ doc,
756
+ getViewport
757
+ ]);
758
+ useEffect(() => {
759
+ if (!widgetState) return;
760
+ const { modal: modal2, screen: screen2 } = widgetState;
761
+ if (modal2.submitMessage) setAnnouncement(modal2.submitMessage);
762
+ if (modal2.open && !previousModalOpenRef.current && modal2.focused) {
763
+ document.querySelector(`[data-fk-focus-id="${modal2.focused}"]`)?.focus();
764
+ }
765
+ if (modal2.closeConfirmVisible && !previousConfirmVisibleRef.current) confirmRef.current?.focus();
766
+ const wasModal = previousScreenRef.current === "modal";
767
+ if (wasModal && screen2 === "button" && modal2.restoreFocusTo === FLOATING_BUTTON_ID) {
768
+ void nextPaint().then(() => floatingButtonRef.current?.focus());
769
+ }
770
+ previousScreenRef.current = screen2;
771
+ previousModalOpenRef.current = modal2.open;
772
+ previousConfirmVisibleRef.current = modal2.closeConfirmVisible;
773
+ }, [widgetState]);
774
+ if (!kit || !widgetState) {
775
+ return /* @__PURE__ */ jsx("div", { ...OWN_UI_PROPS, "data-feedback-kit-loading": "true" });
776
+ }
777
+ const activeKit = kit;
778
+ const { modal, screen } = widgetState;
779
+ const draftLocked = modal.submitStatus === "sending" || modal.submitStatus === "pending";
780
+ async function withOwnUiHidden(action) {
781
+ const root = rootRef.current;
782
+ const previousVisibility = root?.style.visibility ?? "";
783
+ if (root) root.style.visibility = "hidden";
784
+ try {
785
+ await nextPaint();
786
+ await action();
787
+ } finally {
788
+ if (root) root.style.visibility = previousVisibility;
789
+ }
790
+ }
791
+ async function openReport() {
792
+ setAnnouncement(null);
793
+ await withOwnUiHidden(() => activeKit.widget.openReport());
794
+ }
795
+ async function recapture() {
796
+ await withOwnUiHidden(() => activeKit.widget.modal.recapture());
797
+ }
798
+ async function attachFile(event) {
799
+ const input = event.currentTarget;
800
+ const file = input.files?.[0];
801
+ if (file) {
802
+ const screenshot = await readScreenshotFile(file);
803
+ if (screenshot) await activeKit.widget.modal.attachFile(screenshot);
804
+ }
805
+ input.value = "";
806
+ }
807
+ function closeReport() {
808
+ activeKit.widget.closeReport();
809
+ }
810
+ function handleModalKeyDown(event) {
811
+ if (event.key === "Escape") {
812
+ event.preventDefault();
813
+ if (modal.closeConfirmVisible) activeKit.widget.cancelCloseReport();
814
+ else closeReport();
815
+ return;
816
+ }
817
+ trapTab(event, modal.closeConfirmVisible ? confirmRef.current ?? event.currentTarget : event.currentTarget);
818
+ }
819
+ function handleBackdrop(event) {
820
+ if (event.target === event.currentTarget) closeReport();
821
+ }
822
+ function submitReport(event) {
823
+ event.preventDefault();
824
+ void activeKit.widget.submitReport();
825
+ }
826
+ const floating = screen === "button" || screen === "picking";
827
+ const hoverBox = pickingState.hovered?.boundingBox;
828
+ const popup = pickingState.popup;
829
+ return /* @__PURE__ */ jsxs(
830
+ "div",
831
+ {
832
+ ref: rootRef,
833
+ ...OWN_UI_PROPS,
834
+ style: { fontFamily: FONT_STACK, color: TOKENS.ink, fontSize: 14, lineHeight: 1.45 },
835
+ children: [
836
+ /* @__PURE__ */ jsx("style", { children: `@keyframes feedback-kit-spin{to{transform:rotate(360deg)}}` }),
837
+ floating ? /* @__PURE__ */ jsx(
838
+ "button",
839
+ {
840
+ ref: floatingButtonRef,
841
+ id: FLOATING_BUTTON_ID,
842
+ type: "button",
843
+ disabled: screen === "picking",
844
+ "aria-label": screen === "picking" ? "\uC694\uC18C \uC9C0\uBAA9 \uC911" : "\uD53C\uB4DC\uBC31 \uBCF4\uB0B4\uAE30",
845
+ onClick: () => void openReport(),
846
+ style: {
847
+ ...primaryButton,
848
+ position: "fixed",
849
+ right: 24,
850
+ bottom: 24,
851
+ zIndex: 2147483600,
852
+ minWidth: 112,
853
+ minHeight: 46,
854
+ borderRadius: 24,
855
+ boxShadow: TOKENS.shadow,
856
+ opacity: screen === "picking" ? 0.76 : 1,
857
+ cursor: screen === "picking" ? "default" : "pointer"
858
+ },
859
+ children: screen === "picking" ? "\uC9C0\uBAA9 \uC911" : "\uD53C\uB4DC\uBC31"
860
+ }
861
+ ) : null,
862
+ announcement && !modal.open ? /* @__PURE__ */ jsx(
863
+ "div",
864
+ {
865
+ role: "status",
866
+ "aria-live": "polite",
867
+ style: {
868
+ position: "fixed",
869
+ right: 24,
870
+ bottom: 82,
871
+ zIndex: 2147483601,
872
+ padding: "10px 14px",
873
+ borderRadius: 10,
874
+ background: TOKENS.ink,
875
+ color: TOKENS.surface,
876
+ boxShadow: TOKENS.shadow
877
+ },
878
+ children: announcement
879
+ }
880
+ ) : null,
881
+ screen === "modal" && modal.open ? /* @__PURE__ */ jsx(
882
+ "div",
883
+ {
884
+ onMouseDown: handleBackdrop,
885
+ style: {
886
+ position: "fixed",
887
+ inset: 0,
888
+ zIndex: 2147483602,
889
+ display: "grid",
890
+ placeItems: "center",
891
+ padding: 20,
892
+ background: "rgba(23, 32, 42, 0.48)"
893
+ },
894
+ children: /* @__PURE__ */ jsxs(
895
+ "div",
896
+ {
897
+ ref: modalRef,
898
+ role: "dialog",
899
+ "aria-modal": "true",
900
+ "aria-labelledby": "feedback-kit-dialog-title",
901
+ onKeyDown: handleModalKeyDown,
902
+ style: {
903
+ width: "min(520px, 100%)",
904
+ maxHeight: "min(760px, calc(100vh - 40px))",
905
+ overflowY: "auto",
906
+ boxSizing: "border-box",
907
+ border: `1px solid ${TOKENS.line}`,
908
+ borderRadius: TOKENS.radius,
909
+ background: TOKENS.surface,
910
+ padding: 22,
911
+ boxShadow: TOKENS.shadow
912
+ },
913
+ children: [
914
+ /* @__PURE__ */ jsxs("form", { onSubmit: submitReport, children: [
915
+ /* @__PURE__ */ jsx("h2", { id: "feedback-kit-dialog-title", style: { margin: "0 0 4px", fontSize: 20 }, children: "\uD53C\uB4DC\uBC31 \uBCF4\uB0B4\uAE30" }),
916
+ /* @__PURE__ */ jsx("p", { style: { margin: "0 0 18px", color: TOKENS.muted }, children: "\uD604\uC7AC \uD654\uBA74\uC758 \uBB38\uC81C\uB098 \uC758\uACAC\uC744 \uB0A8\uACA8\uC8FC\uC138\uC694." }),
917
+ /* @__PURE__ */ jsxs("section", { "aria-labelledby": "feedback-kit-screenshot-label", style: { marginBottom: 16 }, children: [
918
+ /* @__PURE__ */ jsx("strong", { id: "feedback-kit-screenshot-label", children: "\uC2A4\uD06C\uB9B0\uC0F7" }),
919
+ /* @__PURE__ */ jsx(
920
+ "div",
921
+ {
922
+ style: {
923
+ display: "grid",
924
+ placeItems: "center",
925
+ minHeight: 150,
926
+ marginTop: 7,
927
+ overflow: "hidden",
928
+ border: `1px solid ${TOKENS.line}`,
929
+ borderRadius: 10,
930
+ background: TOKENS.subtle
931
+ },
932
+ children: modal.screenshotStatus === "capturing" ? /* @__PURE__ */ jsx("span", { role: "status", children: "\uD654\uBA74 \uCEA1\uCC98 \uC911\u2026" }) : modal.screenshot ? /* @__PURE__ */ jsx(
933
+ "img",
934
+ {
935
+ src: screenshotSource(modal.screenshot),
936
+ alt: "\uC790\uB3D9 \uCEA1\uCC98\uB41C \uD604\uC7AC \uD654\uBA74 \uBBF8\uB9AC\uBCF4\uAE30",
937
+ style: { display: "block", maxWidth: "100%", maxHeight: 260, objectFit: "contain" }
938
+ }
939
+ ) : /* @__PURE__ */ jsx("span", { role: "status", style: { padding: 18, color: TOKENS.muted, textAlign: "center" }, children: modal.screenshotMessage ?? "\uC2A4\uD06C\uB9B0\uC0F7 \uC5C6\uC74C" })
940
+ }
941
+ ),
942
+ /* @__PURE__ */ jsxs("div", { style: { display: "flex", flexWrap: "wrap", gap: 8, marginTop: 8 }, children: [
943
+ modal.screenshot ? /* @__PURE__ */ jsx(
944
+ "button",
945
+ {
946
+ type: "button",
947
+ "data-fk-focus-id": MODAL_ACTION_REMOVE_SCREENSHOT,
948
+ disabled: draftLocked,
949
+ onClick: () => kit.widget.modal.removeScreenshot(),
950
+ style: baseButton,
951
+ children: "\uC81C\uAC70"
952
+ }
953
+ ) : null,
954
+ /* @__PURE__ */ jsx(
955
+ "button",
956
+ {
957
+ type: "button",
958
+ disabled: draftLocked || modal.screenshotStatus === "capturing",
959
+ onClick: () => void recapture(),
960
+ style: baseButton,
961
+ children: "\uB2E4\uC2DC \uCC0D\uAE30"
962
+ }
963
+ ),
964
+ /* @__PURE__ */ jsx(
965
+ "button",
966
+ {
967
+ type: "button",
968
+ disabled: draftLocked,
969
+ onClick: () => fileInputRef.current?.click(),
970
+ style: baseButton,
971
+ children: modal.screenshot ? "\uD30C\uC77C\uB85C \uAD50\uCCB4" : "\uD30C\uC77C\uB85C \uCCA8\uBD80"
972
+ }
973
+ ),
974
+ /* @__PURE__ */ jsx(
975
+ "input",
976
+ {
977
+ ref: fileInputRef,
978
+ type: "file",
979
+ accept: "image/png,image/jpeg",
980
+ tabIndex: -1,
981
+ "aria-hidden": "true",
982
+ onChange: (event) => void attachFile(event),
983
+ style: { display: "none" }
984
+ }
985
+ )
986
+ ] })
987
+ ] }),
988
+ /* @__PURE__ */ jsxs("label", { htmlFor: "feedback-kit-comment", style: { display: "block", fontWeight: 700 }, children: [
989
+ "\uCF54\uBA58\uD2B8 ",
990
+ /* @__PURE__ */ jsx("span", { "aria-hidden": "true", children: "*" })
991
+ ] }),
992
+ /* @__PURE__ */ jsx(
993
+ "textarea",
994
+ {
995
+ id: "feedback-kit-comment",
996
+ "data-fk-focus-id": MODAL_FIELD_COMMENT,
997
+ value: modal.comment,
998
+ disabled: draftLocked,
999
+ "aria-required": "true",
1000
+ "aria-invalid": modal.commentError ? "true" : void 0,
1001
+ "aria-describedby": modal.commentError ? "feedback-kit-comment-error" : void 0,
1002
+ onChange: (event) => kit.widget.modal.setComment(event.currentTarget.value),
1003
+ rows: 5,
1004
+ placeholder: "\uBB34\uC5C7\uC774 \uBD88\uD3B8\uD588\uB294\uC9C0 \uC54C\uB824\uC8FC\uC138\uC694",
1005
+ style: { ...inputStyle, marginTop: 7, resize: "vertical" }
1006
+ }
1007
+ ),
1008
+ /* @__PURE__ */ jsx("div", { style: { minHeight: 21, marginTop: 3 }, children: modal.commentError ? /* @__PURE__ */ jsx("span", { id: "feedback-kit-comment-error", role: "alert", style: { color: TOKENS.danger }, children: modal.commentError }) : /* @__PURE__ */ jsxs("span", { style: { color: TOKENS.muted }, children: [
1009
+ modal.comment.length.toLocaleString(),
1010
+ " / 4,000"
1011
+ ] }) }),
1012
+ /* @__PURE__ */ jsx("label", { htmlFor: "feedback-kit-priority", style: { display: "block", marginTop: 10, fontWeight: 700 }, children: "\uC6B0\uC120\uC21C\uC704" }),
1013
+ /* @__PURE__ */ jsxs(
1014
+ "select",
1015
+ {
1016
+ id: "feedback-kit-priority",
1017
+ "data-fk-focus-id": MODAL_FIELD_PRIORITY,
1018
+ value: modal.priority,
1019
+ disabled: draftLocked,
1020
+ onChange: (event) => kit.widget.modal.setPriority(event.currentTarget.value),
1021
+ style: { ...inputStyle, marginTop: 7 },
1022
+ children: [
1023
+ /* @__PURE__ */ jsx("option", { value: "unset", children: "\uC120\uD0DD \uC548 \uD568" }),
1024
+ /* @__PURE__ */ jsx("option", { value: "normal", children: "\uBCF4\uD1B5" }),
1025
+ /* @__PURE__ */ jsx("option", { value: "high", children: "\uB192\uC74C" }),
1026
+ /* @__PURE__ */ jsx("option", { value: "urgent", children: "\uAE34\uAE09" })
1027
+ ]
1028
+ }
1029
+ ),
1030
+ /* @__PURE__ */ jsx(
1031
+ "button",
1032
+ {
1033
+ type: "button",
1034
+ role: "switch",
1035
+ "aria-checked": "false",
1036
+ disabled: draftLocked,
1037
+ "data-fk-focus-id": MODAL_ACTION_PICK,
1038
+ onClick: () => kit.widget.startPicking(),
1039
+ style: { ...baseButton, width: "100%", marginTop: 16 },
1040
+ children: "\uC694\uC18C \uC9C0\uBAA9"
1041
+ }
1042
+ ),
1043
+ modal.submitStatus !== "idle" || modal.pending > 0 ? /* @__PURE__ */ jsxs(
1044
+ "div",
1045
+ {
1046
+ role: "status",
1047
+ "aria-live": "polite",
1048
+ style: {
1049
+ marginTop: 14,
1050
+ padding: 11,
1051
+ borderRadius: 9,
1052
+ background: TOKENS.subtle,
1053
+ color: modal.submitStatus === "pending" ? TOKENS.warning : TOKENS.ink
1054
+ },
1055
+ children: [
1056
+ modal.submitStatus === "sending" ? "\uC804\uC1A1 \uC911\u2026" : modal.submitMessage,
1057
+ modal.pending > 0 ? ` \xB7 \uB300\uAE30 \uD050 ${modal.pending}\uAC74` : "",
1058
+ modal.showRetry ? /* @__PURE__ */ jsx(
1059
+ "button",
1060
+ {
1061
+ type: "button",
1062
+ "data-fk-focus-id": MODAL_ACTION_RETRY,
1063
+ onClick: () => void kit.widget.retryReport(),
1064
+ style: { ...baseButton, marginLeft: 10 },
1065
+ children: "\uB2E4\uC2DC \uBCF4\uB0B4\uAE30"
1066
+ }
1067
+ ) : null
1068
+ ]
1069
+ }
1070
+ ) : null,
1071
+ /* @__PURE__ */ jsxs("div", { style: { display: "flex", justifyContent: "flex-end", gap: 8, marginTop: 18 }, children: [
1072
+ /* @__PURE__ */ jsx(
1073
+ "button",
1074
+ {
1075
+ type: "button",
1076
+ "data-fk-focus-id": MODAL_ACTION_CANCEL,
1077
+ onClick: closeReport,
1078
+ style: baseButton,
1079
+ children: "\uCDE8\uC18C"
1080
+ }
1081
+ ),
1082
+ /* @__PURE__ */ jsx(
1083
+ "button",
1084
+ {
1085
+ type: "submit",
1086
+ "data-fk-focus-id": MODAL_ACTION_SEND,
1087
+ disabled: !modal.canSubmit,
1088
+ style: {
1089
+ ...primaryButton,
1090
+ opacity: modal.canSubmit ? 1 : 0.5,
1091
+ cursor: modal.canSubmit ? "pointer" : "not-allowed"
1092
+ },
1093
+ children: modal.submitStatus === "sending" ? "\uC804\uC1A1 \uC911\u2026" : "\uBCF4\uB0B4\uAE30"
1094
+ }
1095
+ )
1096
+ ] })
1097
+ ] }),
1098
+ modal.closeConfirmVisible ? /* @__PURE__ */ jsx(
1099
+ "div",
1100
+ {
1101
+ style: {
1102
+ position: "fixed",
1103
+ inset: 0,
1104
+ zIndex: 2147483604,
1105
+ display: "grid",
1106
+ placeItems: "center",
1107
+ padding: 20,
1108
+ background: "rgba(23, 32, 42, 0.58)"
1109
+ },
1110
+ children: /* @__PURE__ */ jsxs(
1111
+ "div",
1112
+ {
1113
+ ref: confirmRef,
1114
+ role: "alertdialog",
1115
+ "aria-modal": "true",
1116
+ "aria-labelledby": "feedback-kit-close-title",
1117
+ tabIndex: -1,
1118
+ style: {
1119
+ width: "min(360px, 100%)",
1120
+ boxSizing: "border-box",
1121
+ borderRadius: 12,
1122
+ background: TOKENS.surface,
1123
+ padding: 20,
1124
+ boxShadow: TOKENS.shadow
1125
+ },
1126
+ children: [
1127
+ /* @__PURE__ */ jsx("h3", { id: "feedback-kit-close-title", style: { margin: "0 0 6px", fontSize: 17 }, children: "\uC791\uC131 \uC911\uC778 \uB0B4\uC6A9\uC744 \uB2EB\uC744\uAE4C\uC694?" }),
1128
+ /* @__PURE__ */ jsx("p", { style: { margin: "0 0 16px", color: TOKENS.muted }, children: "\uC544\uC9C1 \uBCF4\uB0B4\uC9C0 \uC54A\uC740 \uCF54\uBA58\uD2B8\uAC00 \uC788\uC2B5\uB2C8\uB2E4." }),
1129
+ /* @__PURE__ */ jsxs("div", { style: { display: "flex", justifyContent: "flex-end", gap: 8 }, children: [
1130
+ /* @__PURE__ */ jsx("button", { type: "button", onClick: () => kit.widget.cancelCloseReport(), style: baseButton, children: "\uACC4\uC18D \uC4F0\uAE30" }),
1131
+ /* @__PURE__ */ jsx("button", { type: "button", onClick: () => kit.widget.confirmCloseReport(), style: primaryButton, children: "\uB2EB\uAE30" })
1132
+ ] })
1133
+ ]
1134
+ }
1135
+ )
1136
+ }
1137
+ ) : null
1138
+ ]
1139
+ }
1140
+ )
1141
+ }
1142
+ ) : null,
1143
+ screen === "picking" && pickingState.active ? /* @__PURE__ */ jsxs(
1144
+ "div",
1145
+ {
1146
+ "aria-label": "\uC694\uC18C \uC9C0\uBAA9 \uC624\uBC84\uB808\uC774",
1147
+ style: { position: "fixed", inset: 0, zIndex: 2147483599, pointerEvents: "none" },
1148
+ children: [
1149
+ hoverBox ? /* @__PURE__ */ jsx(
1150
+ "div",
1151
+ {
1152
+ "aria-hidden": "true",
1153
+ style: {
1154
+ position: "fixed",
1155
+ left: hoverBox.x,
1156
+ top: hoverBox.y,
1157
+ width: hoverBox.width,
1158
+ height: hoverBox.height,
1159
+ boxSizing: "border-box",
1160
+ border: `2px solid ${TOKENS.accent}`,
1161
+ background: "rgba(49, 93, 115, 0.12)"
1162
+ }
1163
+ }
1164
+ ) : null,
1165
+ /* @__PURE__ */ jsx(
1166
+ "button",
1167
+ {
1168
+ type: "button",
1169
+ onClick: () => kit.widget.stopPicking(),
1170
+ style: {
1171
+ ...primaryButton,
1172
+ position: "fixed",
1173
+ top: 18,
1174
+ right: 18,
1175
+ pointerEvents: "auto",
1176
+ boxShadow: TOKENS.shadow
1177
+ },
1178
+ children: "\uC9C0\uBAA9 \uC885\uB8CC"
1179
+ }
1180
+ ),
1181
+ pickingState.markers.map((marker, index) => /* @__PURE__ */ jsx(
1182
+ "div",
1183
+ {
1184
+ role: "status",
1185
+ "aria-label": `\uC8FC\uC11D ${index + 1}: ${marker.comment}`,
1186
+ style: {
1187
+ position: "fixed",
1188
+ left: `${marker.x * 100}%`,
1189
+ top: `${marker.y * 100}%`,
1190
+ transform: "translate(-13px, -13px)",
1191
+ display: "grid",
1192
+ placeItems: "center",
1193
+ minWidth: 26,
1194
+ height: 26,
1195
+ padding: "0 7px",
1196
+ border: `2px solid ${TOKENS.surface}`,
1197
+ borderRadius: 16,
1198
+ background: TOKENS.ink,
1199
+ color: TOKENS.surface,
1200
+ boxShadow: "0 3px 10px rgba(23, 32, 42, 0.3)",
1201
+ pointerEvents: "none",
1202
+ fontSize: 11,
1203
+ fontWeight: 700
1204
+ },
1205
+ children: /* @__PURE__ */ jsx(MarkerStatus, { status: marker.status })
1206
+ },
1207
+ marker.id
1208
+ )),
1209
+ popup ? /* @__PURE__ */ jsxs(
1210
+ "form",
1211
+ {
1212
+ "aria-label": "\uC694\uC18C \uC8FC\uC11D",
1213
+ onSubmit: (event) => {
1214
+ event.preventDefault();
1215
+ void kit.picking.saveAnnotation();
1216
+ },
1217
+ style: {
1218
+ position: "fixed",
1219
+ left: `min(${popup.point.x * 100}vw, calc(100vw - 336px))`,
1220
+ top: `min(${popup.point.y * 100}vh, calc(100vh - 230px))`,
1221
+ width: "min(320px, calc(100vw - 32px))",
1222
+ boxSizing: "border-box",
1223
+ border: `1px solid ${TOKENS.line}`,
1224
+ borderRadius: 12,
1225
+ background: TOKENS.surface,
1226
+ padding: 14,
1227
+ boxShadow: TOKENS.shadow,
1228
+ pointerEvents: "auto"
1229
+ },
1230
+ children: [
1231
+ /* @__PURE__ */ jsx("label", { htmlFor: "feedback-kit-annotation", style: { display: "block", fontWeight: 700 }, children: "\uC774 \uC694\uC18C\uC5D0 \uC8FC\uC11D \uB0A8\uAE30\uAE30" }),
1232
+ popup.element?.text ? /* @__PURE__ */ jsx("p", { style: { margin: "4px 0 9px", color: TOKENS.muted }, children: popup.element.text }) : null,
1233
+ /* @__PURE__ */ jsx(
1234
+ "textarea",
1235
+ {
1236
+ id: "feedback-kit-annotation",
1237
+ autoFocus: true,
1238
+ value: popup.comment,
1239
+ onChange: (event) => kit.picking.setAnnotationComment(event.currentTarget.value),
1240
+ rows: 4,
1241
+ placeholder: "\uC758\uACAC\uC744 \uC785\uB825\uD574\uC8FC\uC138\uC694",
1242
+ style: { ...inputStyle, resize: "vertical" }
1243
+ }
1244
+ ),
1245
+ 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,
1246
+ /* @__PURE__ */ jsxs("div", { style: { display: "flex", justifyContent: "flex-end", gap: 8, marginTop: 10 }, children: [
1247
+ /* @__PURE__ */ jsx("button", { type: "button", onClick: () => kit.picking.cancelAnnotation(), style: baseButton, children: "\uCDE8\uC18C" }),
1248
+ /* @__PURE__ */ jsx(
1249
+ "button",
1250
+ {
1251
+ type: "submit",
1252
+ disabled: !popup.canSave,
1253
+ style: {
1254
+ ...primaryButton,
1255
+ opacity: popup.canSave ? 1 : 0.5,
1256
+ cursor: popup.canSave ? "pointer" : "not-allowed"
1257
+ },
1258
+ children: "\uC800\uC7A5"
1259
+ }
1260
+ )
1261
+ ] })
1262
+ ]
1263
+ }
1264
+ ) : null
1265
+ ]
1266
+ }
1267
+ ) : null
1268
+ ]
1269
+ }
1270
+ );
1271
+ }
1272
+
1273
+ // src/storage.ts
1274
+ function memoryStorage() {
1275
+ const map = /* @__PURE__ */ new Map();
1276
+ return {
1277
+ getItem: (key) => map.get(key) ?? null,
1278
+ setItem: (key, value) => void map.set(key, value),
1279
+ removeItem: (key) => void map.delete(key)
1280
+ };
1281
+ }
1282
+ function pickBacking() {
1283
+ try {
1284
+ const ls = globalThis.localStorage;
1285
+ if (!ls) return memoryStorage();
1286
+ const probe = "__feedback_kit_probe__";
1287
+ ls.setItem(probe, "1");
1288
+ ls.removeItem(probe);
1289
+ return ls;
1290
+ } catch {
1291
+ return memoryStorage();
1292
+ }
1293
+ }
1294
+ function createWebStorage(backing) {
1295
+ const store = backing ?? pickBacking();
1296
+ return {
1297
+ async get(key) {
1298
+ try {
1299
+ return store.getItem(key);
1300
+ } catch {
1301
+ return null;
1302
+ }
1303
+ },
1304
+ async set(key, value) {
1305
+ store.setItem(key, value);
1306
+ },
1307
+ async remove(key) {
1308
+ try {
1309
+ store.removeItem(key);
1310
+ } catch {
1311
+ }
1312
+ }
1313
+ };
1314
+ }
1315
+
1316
+ // src/providers.ts
1317
+ function webContextProviders() {
1318
+ return {
1319
+ getUrl: () => {
1320
+ const loc = globalThis.location;
1321
+ return typeof loc?.href === "string" && loc.href.length > 0 ? loc.href : null;
1322
+ },
1323
+ getWebContext: () => {
1324
+ const g = globalThis;
1325
+ if (!g.location) return null;
1326
+ return {
1327
+ userAgent: g.navigator?.userAgent ?? null,
1328
+ viewport: {
1329
+ width: typeof g.innerWidth === "number" ? g.innerWidth : null,
1330
+ height: typeof g.innerHeight === "number" ? g.innerHeight : null,
1331
+ devicePixelRatio: typeof g.devicePixelRatio === "number" ? g.devicePixelRatio : null
1332
+ }
1333
+ };
1334
+ }
1335
+ };
1336
+ }
1337
+ export {
1338
+ COMMENT_MAX_CHARS2 as COMMENT_MAX_CHARS,
1339
+ COMMENT_REQUIRED_MESSAGE,
1340
+ COMMENT_TOO_LONG_MESSAGE,
1341
+ ELEMENT_TEXT_MAX_CHARS,
1342
+ ElementPickingController,
1343
+ FLOATING_BUTTON_ID2 as FLOATING_BUTTON_ID,
1344
+ FeedbackKit,
1345
+ MARKER_KEY_PREFIX,
1346
+ MAX_MARKERS_PER_PATH,
1347
+ MODAL_ACTION_PICK2 as MODAL_ACTION_PICK,
1348
+ MarkerStore,
1349
+ OWN_UI_ATTR,
1350
+ PICKING_MODE_KEY,
1351
+ ReportModalController,
1352
+ SCREENSHOT_FAILED_MESSAGE,
1353
+ SELECTOR_MAX_DEPTH,
1354
+ SOURCE_ATTR,
1355
+ SUBMIT_DONE_MESSAGE,
1356
+ SUBMIT_PENDING_MESSAGE,
1357
+ WidgetController2 as WidgetController,
1358
+ captureWebScreenshot,
1359
+ createWebStorage,
1360
+ createWebWidget,
1361
+ cssSelectorPath,
1362
+ denormalizePin,
1363
+ describeElement,
1364
+ isOwnUi2 as isOwnUi,
1365
+ normalizePin2 as normalizePin,
1366
+ parseSourceAttr,
1367
+ reencodeWebScreenshot,
1368
+ resolveConfig,
1369
+ shouldShowWidget,
1370
+ sourceFromElement,
1371
+ visibleText,
1372
+ webContextProviders
1373
+ };
1374
+ //# sourceMappingURL=index.js.map