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