@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/LICENSE +21 -0
- package/dist/index.cjs +1414 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +229 -0
- package/dist/index.d.ts +229 -0
- package/dist/index.js +1374 -0
- package/dist/index.js.map +1 -0
- package/package.json +45 -0
- package/src/element-info.ts +138 -0
- package/src/feedback-kit.test.ts +227 -0
- package/src/feedback-kit.tsx +778 -0
- package/src/index.ts +97 -0
- package/src/marker-store.ts +153 -0
- package/src/picking.test.ts +249 -0
- package/src/picking.ts +370 -0
- package/src/providers.ts +43 -0
- package/src/screen-map.test.ts +455 -0
- package/src/screenshot.test.ts +47 -0
- package/src/screenshot.ts +90 -0
- package/src/storage.test.ts +65 -0
- package/src/storage.ts +69 -0
- package/src/widget.ts +70 -0
package/src/storage.ts
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
// 웹 기본 저장소 — `localStorage` 를 FeedbackStorage 계약에 맞춘다.
|
|
2
|
+
//
|
|
3
|
+
// 왜 패키지가 제공하나: 이게 없으면 붙이는 앱마다 같은 20줄을 다시 쓴다.
|
|
4
|
+
// 코어는 저장소 구현을 모르고(플랫폼 무관), 웹 패키지가 기본값을 준다.
|
|
5
|
+
//
|
|
6
|
+
// localStorage 는 동기지만 계약은 비동기라 Promise 로 감싼다. 사파리 프라이빗
|
|
7
|
+
// 모드처럼 접근 자체가 던지는 환경이 있어서, 실패하면 **메모리로 조용히 물러난다**
|
|
8
|
+
// — 제보를 잃는 것보다 세션 동안만 유지되는 편이 낫다.
|
|
9
|
+
|
|
10
|
+
import type { FeedbackStorage } from "@solhun/feedback-kit-core";
|
|
11
|
+
|
|
12
|
+
interface SyncStorage {
|
|
13
|
+
getItem(key: string): string | null;
|
|
14
|
+
setItem(key: string, value: string): void;
|
|
15
|
+
removeItem(key: string): void;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function memoryStorage(): SyncStorage {
|
|
19
|
+
const map = new Map<string, string>();
|
|
20
|
+
return {
|
|
21
|
+
getItem: (key) => map.get(key) ?? null,
|
|
22
|
+
setItem: (key, value) => void map.set(key, value),
|
|
23
|
+
removeItem: (key) => void map.delete(key),
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** 접근 가능한 localStorage 를 고른다. 못 쓰면 메모리. */
|
|
28
|
+
function pickBacking(): SyncStorage {
|
|
29
|
+
try {
|
|
30
|
+
const ls = (globalThis as { localStorage?: SyncStorage }).localStorage;
|
|
31
|
+
if (!ls) return memoryStorage();
|
|
32
|
+
// 존재만으로는 부족하다 — 프라이빗 모드는 쓰기에서 던진다.
|
|
33
|
+
const probe = "__feedback_kit_probe__";
|
|
34
|
+
ls.setItem(probe, "1");
|
|
35
|
+
ls.removeItem(probe);
|
|
36
|
+
return ls;
|
|
37
|
+
} catch {
|
|
38
|
+
return memoryStorage();
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* 웹 기본 FeedbackStorage.
|
|
44
|
+
*
|
|
45
|
+
* 쓰기가 쿼터를 넘기면 그대로 던진다 — 큐가 그 신호를 받아 스크린샷을 떼어내고
|
|
46
|
+
* 본문만 남기는 축소 경로를 타기 때문에, 여기서 삼키면 안 된다.
|
|
47
|
+
*/
|
|
48
|
+
export function createWebStorage(backing?: SyncStorage): FeedbackStorage {
|
|
49
|
+
const store = backing ?? pickBacking();
|
|
50
|
+
return {
|
|
51
|
+
async get(key) {
|
|
52
|
+
try {
|
|
53
|
+
return store.getItem(key);
|
|
54
|
+
} catch {
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
},
|
|
58
|
+
async set(key, value) {
|
|
59
|
+
store.setItem(key, value);
|
|
60
|
+
},
|
|
61
|
+
async remove(key) {
|
|
62
|
+
try {
|
|
63
|
+
store.removeItem(key);
|
|
64
|
+
} catch {
|
|
65
|
+
/* 지우기 실패는 다음 쓰기가 덮으므로 무시한다. */
|
|
66
|
+
}
|
|
67
|
+
},
|
|
68
|
+
};
|
|
69
|
+
}
|
package/src/widget.ts
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
// 웹 위젯 조립.
|
|
2
|
+
//
|
|
3
|
+
// 코어의 화면 지도(`WidgetController`)와 웹 전용 지목 모드(`ElementPickingController`)를
|
|
4
|
+
// 한 덩어리로 묶는다. 둘을 따로 두면 "모달에서 [요소 지목]을 눌렀다"와 "지목 오버레이가 떴다"가
|
|
5
|
+
// 서로 모르는 상태가 되므로, 연결 지점을 여기 한 곳으로 모은다.
|
|
6
|
+
//
|
|
7
|
+
// 지목 모드 on/off 의 유일한 입구는 `widget.startPicking()` / `widget.stopPicking()` 이다.
|
|
8
|
+
// 지목 컨트롤러를 직접 켜면 화면 지도가 그 사실을 모른다.
|
|
9
|
+
|
|
10
|
+
import { WidgetController, type WidgetControllerOpts } from "@solhun/feedback-kit-core";
|
|
11
|
+
import { MarkerStore } from "./marker-store.js";
|
|
12
|
+
import { ElementPickingController } from "./picking.js";
|
|
13
|
+
import { captureWebScreenshot, reencodeWebScreenshot } from "./screenshot.js";
|
|
14
|
+
|
|
15
|
+
export interface WebWidgetOpts
|
|
16
|
+
extends Omit<WidgetControllerOpts, "platform" | "initialPicking" | "onPickingChange"> {
|
|
17
|
+
/** 지목 모드 플래그와 마커를 담아 둘 저장소. 기본은 `localStorage`. */
|
|
18
|
+
store?: MarkerStore;
|
|
19
|
+
/** 현재 경로. 마커를 경로별로 나눠 담는 기준. */
|
|
20
|
+
getPathname?: () => string;
|
|
21
|
+
/** 지목 이벤트를 들을 문서. 기본은 전역 `document`. */
|
|
22
|
+
doc?: Document | null;
|
|
23
|
+
getViewport?: () => { width: number; height: number };
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface WebWidget {
|
|
27
|
+
widget: WidgetController;
|
|
28
|
+
picking: ElementPickingController;
|
|
29
|
+
store: MarkerStore;
|
|
30
|
+
dispose(): void;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function createWebWidget(opts: WebWidgetOpts): WebWidget {
|
|
34
|
+
const store = opts.store ?? new MarkerStore();
|
|
35
|
+
|
|
36
|
+
const picking = new ElementPickingController({
|
|
37
|
+
queue: opts.queue,
|
|
38
|
+
createReport: opts.createReport,
|
|
39
|
+
store,
|
|
40
|
+
getPathname: opts.getPathname,
|
|
41
|
+
doc: opts.doc,
|
|
42
|
+
getViewport: opts.getViewport,
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
const widget = new WidgetController({
|
|
46
|
+
...opts,
|
|
47
|
+
capture: opts.capture === undefined ? captureWebScreenshot : opts.capture,
|
|
48
|
+
reencode: opts.reencode === undefined ? reencodeWebScreenshot : opts.reencode,
|
|
49
|
+
platform: "web",
|
|
50
|
+
// 새로고침·페이지 이동 직후에도 켜져 있던 모드를 그대로 이어받는다.
|
|
51
|
+
initialPicking: store.isPickingActive(),
|
|
52
|
+
onPickingChange: (active) => {
|
|
53
|
+
if (active) picking.start();
|
|
54
|
+
else picking.stop();
|
|
55
|
+
},
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
// 저장돼 있던 모드면 여기서 DOM 리스너까지 다시 붙인다. 꺼져 있으면 마커만 읽고 만다.
|
|
59
|
+
picking.restore();
|
|
60
|
+
|
|
61
|
+
return {
|
|
62
|
+
widget,
|
|
63
|
+
picking,
|
|
64
|
+
store,
|
|
65
|
+
dispose(): void {
|
|
66
|
+
picking.dispose();
|
|
67
|
+
widget.dispose();
|
|
68
|
+
},
|
|
69
|
+
};
|
|
70
|
+
}
|