@ait-co/devtools 0.0.2 → 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/README.md +14 -6
- package/dist/mock/index.d.ts +571 -534
- package/dist/mock/index.d.ts.map +1 -0
- package/dist/mock/index.js +1030 -454
- package/dist/mock/index.js.map +1 -1
- package/dist/panel/index.d.ts +3 -1
- package/dist/panel/index.d.ts.map +1 -0
- package/dist/panel/index.js +1226 -683
- package/dist/panel/index.js.map +1 -1
- package/dist/unplugin/index.cjs +68 -73
- package/dist/unplugin/index.cjs.map +1 -1
- package/dist/unplugin/index.d.cts +10100 -22
- package/dist/unplugin/index.d.cts.map +1 -0
- package/dist/unplugin/index.d.ts +10100 -22
- package/dist/unplugin/index.d.ts.map +1 -0
- package/dist/unplugin/index.js +59 -44
- package/dist/unplugin/index.js.map +1 -1
- package/package.json +23 -15
- package/dist/chunk-6PPZTREF.js +0 -569
- package/dist/chunk-6PPZTREF.js.map +0 -1
package/dist/panel/index.js
CHANGED
|
@@ -1,14 +1,212 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
1
|
+
//#region src/mock/state.ts
|
|
2
|
+
const DEFAULT_STATE = {
|
|
3
|
+
platform: "ios",
|
|
4
|
+
environment: "sandbox",
|
|
5
|
+
appVersion: "5.240.0",
|
|
6
|
+
locale: "ko-KR",
|
|
7
|
+
schemeUri: "/",
|
|
8
|
+
groupId: "mock-group-id",
|
|
9
|
+
deploymentId: "mock-deployment-id",
|
|
10
|
+
deviceId: "",
|
|
11
|
+
brand: {
|
|
12
|
+
displayName: "Mock App",
|
|
13
|
+
icon: "",
|
|
14
|
+
primaryColor: "#3182F6"
|
|
15
|
+
},
|
|
16
|
+
networkStatus: "WIFI",
|
|
17
|
+
permissions: {
|
|
18
|
+
clipboard: "allowed",
|
|
19
|
+
contacts: "allowed",
|
|
20
|
+
photos: "allowed",
|
|
21
|
+
geolocation: "allowed",
|
|
22
|
+
camera: "allowed",
|
|
23
|
+
microphone: "notDetermined"
|
|
24
|
+
},
|
|
25
|
+
location: {
|
|
26
|
+
coords: {
|
|
27
|
+
latitude: 37.5665,
|
|
28
|
+
longitude: 126.978,
|
|
29
|
+
altitude: 0,
|
|
30
|
+
accuracy: 10,
|
|
31
|
+
altitudeAccuracy: 0,
|
|
32
|
+
heading: 0
|
|
33
|
+
},
|
|
34
|
+
timestamp: Date.now(),
|
|
35
|
+
accessLocation: "FINE"
|
|
36
|
+
},
|
|
37
|
+
safeAreaInsets: {
|
|
38
|
+
top: 47,
|
|
39
|
+
bottom: 34,
|
|
40
|
+
left: 0,
|
|
41
|
+
right: 0
|
|
42
|
+
},
|
|
43
|
+
contacts: [{
|
|
44
|
+
name: "홍길동",
|
|
45
|
+
phoneNumber: "010-1234-5678"
|
|
46
|
+
}, {
|
|
47
|
+
name: "김토스",
|
|
48
|
+
phoneNumber: "010-9876-5432"
|
|
49
|
+
}],
|
|
50
|
+
iap: {
|
|
51
|
+
products: [{
|
|
52
|
+
sku: "mock-gem-100",
|
|
53
|
+
type: "CONSUMABLE",
|
|
54
|
+
displayName: "보석 100개",
|
|
55
|
+
displayAmount: "1,000원",
|
|
56
|
+
iconUrl: "",
|
|
57
|
+
description: "게임에서 사용할 수 있는 보석 100개"
|
|
58
|
+
}],
|
|
59
|
+
nextResult: "success",
|
|
60
|
+
pendingOrders: [],
|
|
61
|
+
completedOrders: []
|
|
62
|
+
},
|
|
63
|
+
payment: {
|
|
64
|
+
nextResult: "success",
|
|
65
|
+
failReason: ""
|
|
66
|
+
},
|
|
67
|
+
auth: {
|
|
68
|
+
isLoggedIn: true,
|
|
69
|
+
isTossLoginIntegrated: true,
|
|
70
|
+
userKeyHash: "mock-user-hash-abc123"
|
|
71
|
+
},
|
|
72
|
+
ads: {
|
|
73
|
+
isLoaded: false,
|
|
74
|
+
nextEvent: "loaded"
|
|
75
|
+
},
|
|
76
|
+
game: {
|
|
77
|
+
profile: {
|
|
78
|
+
nickname: "MockPlayer",
|
|
79
|
+
profileImageUri: ""
|
|
80
|
+
},
|
|
81
|
+
leaderboardScores: []
|
|
82
|
+
},
|
|
83
|
+
analyticsLog: [],
|
|
84
|
+
deviceModes: {
|
|
85
|
+
camera: "mock",
|
|
86
|
+
photos: "mock",
|
|
87
|
+
location: "mock",
|
|
88
|
+
network: "mock",
|
|
89
|
+
clipboard: "web"
|
|
90
|
+
},
|
|
91
|
+
mockData: {
|
|
92
|
+
images: [],
|
|
93
|
+
clipboardText: ""
|
|
94
|
+
},
|
|
95
|
+
panelEditable: true
|
|
96
|
+
};
|
|
97
|
+
function generateDeviceId() {
|
|
98
|
+
const stored = localStorage.getItem("__ait_device_id");
|
|
99
|
+
if (stored) return stored;
|
|
100
|
+
const id = crypto.randomUUID();
|
|
101
|
+
localStorage.setItem("__ait_device_id", id);
|
|
102
|
+
return id;
|
|
103
|
+
}
|
|
104
|
+
var AitStateManager = class {
|
|
105
|
+
_state;
|
|
106
|
+
_listeners = /* @__PURE__ */ new Set();
|
|
107
|
+
constructor() {
|
|
108
|
+
this._state = structuredClone(DEFAULT_STATE);
|
|
109
|
+
try {
|
|
110
|
+
this._state.deviceId = generateDeviceId();
|
|
111
|
+
} catch {
|
|
112
|
+
this._state.deviceId = `mock-device-${Math.random().toString(36).slice(2)}`;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
get state() {
|
|
116
|
+
return this._state;
|
|
117
|
+
}
|
|
118
|
+
update(partial) {
|
|
119
|
+
this._state = {
|
|
120
|
+
...this._state,
|
|
121
|
+
...partial
|
|
122
|
+
};
|
|
123
|
+
this._notify();
|
|
124
|
+
}
|
|
125
|
+
/** 중첩 객체 업데이트용 */
|
|
126
|
+
patch(key, partial) {
|
|
127
|
+
const current = this._state[key];
|
|
128
|
+
if (typeof current === "object" && current !== null && !Array.isArray(current)) this._state = {
|
|
129
|
+
...this._state,
|
|
130
|
+
[key]: {
|
|
131
|
+
...current,
|
|
132
|
+
...partial
|
|
133
|
+
}
|
|
134
|
+
};
|
|
135
|
+
else this._state = {
|
|
136
|
+
...this._state,
|
|
137
|
+
[key]: partial
|
|
138
|
+
};
|
|
139
|
+
this._notify();
|
|
140
|
+
}
|
|
141
|
+
subscribe(listener) {
|
|
142
|
+
this._listeners.add(listener);
|
|
143
|
+
return () => this._listeners.delete(listener);
|
|
144
|
+
}
|
|
145
|
+
/** 분석 로그 추가 */
|
|
146
|
+
logAnalytics(entry) {
|
|
147
|
+
this._state = {
|
|
148
|
+
...this._state,
|
|
149
|
+
analyticsLog: [...this._state.analyticsLog, {
|
|
150
|
+
...entry,
|
|
151
|
+
timestamp: Date.now()
|
|
152
|
+
}]
|
|
153
|
+
};
|
|
154
|
+
this._notify();
|
|
155
|
+
}
|
|
156
|
+
/** 이벤트 트리거 (backEvent, homeEvent 등) */
|
|
157
|
+
trigger(event) {
|
|
158
|
+
window.dispatchEvent(new CustomEvent(`__ait:${event}`));
|
|
159
|
+
}
|
|
160
|
+
reset() {
|
|
161
|
+
const deviceId = this._state.deviceId;
|
|
162
|
+
this._state = {
|
|
163
|
+
...structuredClone(DEFAULT_STATE),
|
|
164
|
+
deviceId
|
|
165
|
+
};
|
|
166
|
+
this._notify();
|
|
167
|
+
}
|
|
168
|
+
_notify() {
|
|
169
|
+
for (const listener of this._listeners) listener();
|
|
170
|
+
}
|
|
171
|
+
};
|
|
172
|
+
const aitState = new AitStateManager();
|
|
173
|
+
if (typeof window !== "undefined") window.__ait = aitState;
|
|
174
|
+
//#endregion
|
|
175
|
+
//#region src/panel/helpers.ts
|
|
176
|
+
/**
|
|
177
|
+
* 공통 DOM 헬퍼 함수
|
|
178
|
+
*/
|
|
179
|
+
function h(tag, attrs, ...children) {
|
|
180
|
+
const el = document.createElement(tag);
|
|
181
|
+
if (attrs) for (const [k, v] of Object.entries(attrs)) if (k === "className") el.className = v;
|
|
182
|
+
else el.setAttribute(k, v);
|
|
183
|
+
for (const child of children) el.append(typeof child === "string" ? document.createTextNode(child) : child);
|
|
184
|
+
return el;
|
|
185
|
+
}
|
|
186
|
+
function selectRow(label, options, value, onChange, disabled = false) {
|
|
187
|
+
const select = h("select", { className: "ait-select" });
|
|
188
|
+
if (disabled) select.disabled = true;
|
|
189
|
+
for (const opt of options) {
|
|
190
|
+
const option = h("option", { value: opt }, opt);
|
|
191
|
+
if (opt === value) option.selected = true;
|
|
192
|
+
select.appendChild(option);
|
|
193
|
+
}
|
|
194
|
+
select.addEventListener("change", () => onChange(select.value));
|
|
195
|
+
return h("div", { className: "ait-row" }, h("label", {}, label), select);
|
|
196
|
+
}
|
|
197
|
+
function inputRow(label, value, onChange, disabled = false) {
|
|
198
|
+
const input = h("input", {
|
|
199
|
+
className: "ait-input",
|
|
200
|
+
value
|
|
201
|
+
});
|
|
202
|
+
if (disabled) input.disabled = true;
|
|
203
|
+
input.addEventListener("change", () => onChange(input.value));
|
|
204
|
+
return h("div", { className: "ait-row" }, h("label", {}, label), input);
|
|
205
|
+
}
|
|
206
|
+
function monitoringNotice() {
|
|
207
|
+
return h("div", { className: "ait-monitoring-notice" }, "Read-only — mock responses are controlled at build time.");
|
|
208
|
+
}
|
|
209
|
+
const PANEL_STYLES = `
|
|
12
210
|
.ait-panel-toggle {
|
|
13
211
|
position: fixed;
|
|
14
212
|
z-index: 99999;
|
|
@@ -36,8 +234,8 @@ var PANEL_STYLES = (
|
|
|
36
234
|
.ait-panel {
|
|
37
235
|
position: fixed;
|
|
38
236
|
z-index: 99998;
|
|
39
|
-
width:
|
|
40
|
-
height:
|
|
237
|
+
width: 360px;
|
|
238
|
+
height: 480px;
|
|
41
239
|
background: #1a1a2e;
|
|
42
240
|
border-radius: 12px;
|
|
43
241
|
box-shadow: 0 8px 32px rgba(0,0,0,0.4);
|
|
@@ -357,6 +555,11 @@ var PANEL_STYLES = (
|
|
|
357
555
|
color: #fbbf24;
|
|
358
556
|
}
|
|
359
557
|
|
|
558
|
+
.ait-panel-tab-error {
|
|
559
|
+
padding: 12px;
|
|
560
|
+
color: #e53e3e; /* readable on both light (#fff) and dark (#1a1a2e) panel backgrounds */
|
|
561
|
+
}
|
|
562
|
+
|
|
360
563
|
@media (max-width: 480px) {
|
|
361
564
|
.ait-panel.open {
|
|
362
565
|
position: fixed;
|
|
@@ -376,709 +579,1049 @@ var PANEL_STYLES = (
|
|
|
376
579
|
display: block;
|
|
377
580
|
}
|
|
378
581
|
}
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
582
|
+
`;
|
|
583
|
+
//#endregion
|
|
584
|
+
//#region src/mock/device/_helpers.ts
|
|
585
|
+
/**
|
|
586
|
+
* 디바이스 모듈 내부 공유 헬퍼
|
|
587
|
+
*/
|
|
588
|
+
function generatePlaceholderImage(width, height, text, color) {
|
|
589
|
+
const canvas = document.createElement("canvas");
|
|
590
|
+
canvas.width = width;
|
|
591
|
+
canvas.height = height;
|
|
592
|
+
const ctx = canvas.getContext("2d");
|
|
593
|
+
if (!ctx) {
|
|
594
|
+
const svg = `<svg xmlns="http://www.w3.org/2000/svg" width="${width}" height="${height}"><rect fill="${color}" width="${width}" height="${height}"/><text x="50%" y="50%" fill="white" font-size="16" text-anchor="middle" dominant-baseline="middle">${text}</text></svg>`;
|
|
595
|
+
return `data:image/svg+xml;base64,${btoa(svg)}`;
|
|
596
|
+
}
|
|
597
|
+
ctx.fillStyle = color;
|
|
598
|
+
ctx.fillRect(0, 0, width, height);
|
|
599
|
+
ctx.fillStyle = "white";
|
|
600
|
+
ctx.font = "16px sans-serif";
|
|
601
|
+
ctx.textAlign = "center";
|
|
602
|
+
ctx.textBaseline = "middle";
|
|
603
|
+
ctx.fillText(text, width / 2, height / 2);
|
|
604
|
+
return canvas.toDataURL("image/png");
|
|
605
|
+
}
|
|
606
|
+
const DEFAULT_PLACEHOLDERS = [
|
|
607
|
+
{
|
|
608
|
+
text: "Mock Photo 1",
|
|
609
|
+
color: "#3182F6"
|
|
610
|
+
},
|
|
611
|
+
{
|
|
612
|
+
text: "Mock Photo 2",
|
|
613
|
+
color: "#27ae60"
|
|
614
|
+
},
|
|
615
|
+
{
|
|
616
|
+
text: "Mock Photo 3",
|
|
617
|
+
color: "#e67e22"
|
|
618
|
+
}
|
|
392
619
|
];
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
if (k === "className") el.className = v;
|
|
398
|
-
else el.setAttribute(k, v);
|
|
399
|
-
}
|
|
400
|
-
}
|
|
401
|
-
for (const child of children) {
|
|
402
|
-
el.append(typeof child === "string" ? document.createTextNode(child) : child);
|
|
403
|
-
}
|
|
404
|
-
return el;
|
|
620
|
+
let cachedPlaceholders = null;
|
|
621
|
+
function getDefaultPlaceholderImages() {
|
|
622
|
+
if (!cachedPlaceholders) cachedPlaceholders = DEFAULT_PLACEHOLDERS.map((p) => generatePlaceholderImage(320, 240, p.text, p.color));
|
|
623
|
+
return [...cachedPlaceholders];
|
|
405
624
|
}
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
if (opt === value) option.selected = true;
|
|
412
|
-
select.appendChild(option);
|
|
413
|
-
}
|
|
414
|
-
select.addEventListener("change", () => onChange(select.value));
|
|
415
|
-
return h("div", { className: "ait-row" }, h("label", {}, label), select);
|
|
625
|
+
/** @internal device 모듈 내부 전용 */
|
|
626
|
+
function getMockImages() {
|
|
627
|
+
const images = aitState.state.mockData.images;
|
|
628
|
+
if (images.length > 0) return images;
|
|
629
|
+
return getDefaultPlaceholderImages();
|
|
416
630
|
}
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
631
|
+
const PROMPT_TIMEOUT_MS = 3e4;
|
|
632
|
+
/** @internal device 모듈 내부 전용 */
|
|
633
|
+
function waitForPromptResponse(type) {
|
|
634
|
+
return new Promise((resolve, reject) => {
|
|
635
|
+
const eventName = `__ait:prompt-response:${type}`;
|
|
636
|
+
const cancelName = "__ait:prompt-cancel";
|
|
637
|
+
function cleanup() {
|
|
638
|
+
clearTimeout(timer);
|
|
639
|
+
window.removeEventListener(eventName, handler);
|
|
640
|
+
window.removeEventListener(cancelName, cancelHandler);
|
|
641
|
+
}
|
|
642
|
+
const timer = setTimeout(() => {
|
|
643
|
+
cleanup();
|
|
644
|
+
const hint = !!document.querySelector(".ait-panel") ? "Please provide input via the DevTools panel." : "Is @ait-co/devtools/panel imported?";
|
|
645
|
+
reject(/* @__PURE__ */ new Error(`[@ait-co/devtools] Prompt timeout for "${type}" after ${PROMPT_TIMEOUT_MS / 1e3}s. ${hint}`));
|
|
646
|
+
}, PROMPT_TIMEOUT_MS);
|
|
647
|
+
const handler = (e) => {
|
|
648
|
+
cleanup();
|
|
649
|
+
resolve(e.detail);
|
|
650
|
+
};
|
|
651
|
+
const cancelHandler = () => {
|
|
652
|
+
cleanup();
|
|
653
|
+
reject(/* @__PURE__ */ new Error(`[@ait-co/devtools] Prompt cancelled for "${type}"`));
|
|
654
|
+
};
|
|
655
|
+
window.addEventListener(eventName, handler);
|
|
656
|
+
window.addEventListener(cancelName, cancelHandler);
|
|
657
|
+
window.dispatchEvent(new CustomEvent("__ait:prompt-request", { detail: { type } }));
|
|
658
|
+
});
|
|
422
659
|
}
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
{ className: "ait-section" },
|
|
432
|
-
h("div", { className: "ait-section-title" }, "Platform"),
|
|
433
|
-
selectRow("OS", ["ios", "android"], s.platform, (v) => aitState.update({ platform: v }), disabled),
|
|
434
|
-
inputRow("App Version", s.appVersion, (v) => aitState.update({ appVersion: v }), disabled),
|
|
435
|
-
selectRow("Environment", ["toss", "sandbox"], s.environment, (v) => aitState.update({ environment: v }), disabled),
|
|
436
|
-
inputRow("Locale", s.locale, (v) => aitState.update({ locale: v }), disabled)
|
|
437
|
-
),
|
|
438
|
-
h(
|
|
439
|
-
"div",
|
|
440
|
-
{ className: "ait-section" },
|
|
441
|
-
h("div", { className: "ait-section-title" }, "Network"),
|
|
442
|
-
selectRow("Status", ["WIFI", "4G", "5G", "3G", "2G", "OFFLINE", "WWAN", "UNKNOWN"], s.networkStatus, (v) => aitState.update({ networkStatus: v }), disabled)
|
|
443
|
-
),
|
|
444
|
-
h(
|
|
445
|
-
"div",
|
|
446
|
-
{ className: "ait-section" },
|
|
447
|
-
h("div", { className: "ait-section-title" }, "Safe Area Insets"),
|
|
448
|
-
inputRow("Top", String(s.safeAreaInsets.top), (v) => aitState.patch("safeAreaInsets", { top: Number(v) }), disabled),
|
|
449
|
-
inputRow("Bottom", String(s.safeAreaInsets.bottom), (v) => aitState.patch("safeAreaInsets", { bottom: Number(v) }), disabled)
|
|
450
|
-
)
|
|
451
|
-
);
|
|
452
|
-
return container;
|
|
660
|
+
//#endregion
|
|
661
|
+
//#region src/mock/permissions.ts
|
|
662
|
+
/**
|
|
663
|
+
* 권한 시스템 mock
|
|
664
|
+
* 각 디바이스 API (.getPermission, .openPermissionDialog)에 부착된다.
|
|
665
|
+
*/
|
|
666
|
+
async function getPermission(name) {
|
|
667
|
+
return aitState.state.permissions[name];
|
|
453
668
|
}
|
|
454
|
-
function
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
const names = ["camera", "photos", "geolocation", "clipboard", "contacts", "microphone"];
|
|
459
|
-
const statuses = ["allowed", "denied", "notDetermined"];
|
|
460
|
-
if (disabled) container.appendChild(monitoringNotice());
|
|
461
|
-
container.append(
|
|
462
|
-
h(
|
|
463
|
-
"div",
|
|
464
|
-
{ className: "ait-section" },
|
|
465
|
-
h("div", { className: "ait-section-title" }, "Device Permissions"),
|
|
466
|
-
...names.map(
|
|
467
|
-
(name) => selectRow(name, statuses, s.permissions[name], (v) => {
|
|
468
|
-
aitState.patch("permissions", { [name]: v });
|
|
469
|
-
}, disabled)
|
|
470
|
-
)
|
|
471
|
-
)
|
|
472
|
-
);
|
|
473
|
-
return container;
|
|
669
|
+
async function openPermissionDialog(name) {
|
|
670
|
+
if (aitState.state.permissions[name] === "allowed") return "allowed";
|
|
671
|
+
aitState.patch("permissions", { [name]: "allowed" });
|
|
672
|
+
return "allowed";
|
|
474
673
|
}
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
h(
|
|
482
|
-
"div",
|
|
483
|
-
{ className: "ait-section" },
|
|
484
|
-
h("div", { className: "ait-section-title" }, "Current Location"),
|
|
485
|
-
inputRow("Latitude", String(s.location.coords.latitude), (v) => {
|
|
486
|
-
const coords = { ...s.location.coords, latitude: Number(v) };
|
|
487
|
-
aitState.patch("location", { coords });
|
|
488
|
-
}, disabled),
|
|
489
|
-
inputRow("Longitude", String(s.location.coords.longitude), (v) => {
|
|
490
|
-
const coords = { ...s.location.coords, longitude: Number(v) };
|
|
491
|
-
aitState.patch("location", { coords });
|
|
492
|
-
}, disabled),
|
|
493
|
-
inputRow("Accuracy", String(s.location.coords.accuracy), (v) => {
|
|
494
|
-
const coords = { ...s.location.coords, accuracy: Number(v) };
|
|
495
|
-
aitState.patch("location", { coords });
|
|
496
|
-
}, disabled)
|
|
497
|
-
)
|
|
498
|
-
);
|
|
499
|
-
return container;
|
|
674
|
+
/** 권한이 필요한 함수에 .getPermission(), .openPermissionDialog()를 부착 */
|
|
675
|
+
function withPermission(fn, permissionName) {
|
|
676
|
+
const enhanced = fn;
|
|
677
|
+
enhanced.getPermission = () => getPermission(permissionName);
|
|
678
|
+
enhanced.openPermissionDialog = () => openPermissionDialog(permissionName);
|
|
679
|
+
return enhanced;
|
|
500
680
|
}
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
const container = h("div");
|
|
505
|
-
const results = ["success", "USER_CANCELED", "INVALID_PRODUCT_ID", "PAYMENT_PENDING", "NETWORK_ERROR", "ITEM_ALREADY_OWNED", "INTERNAL_ERROR"];
|
|
506
|
-
if (disabled) container.appendChild(monitoringNotice());
|
|
507
|
-
container.append(
|
|
508
|
-
h(
|
|
509
|
-
"div",
|
|
510
|
-
{ className: "ait-section" },
|
|
511
|
-
h("div", { className: "ait-section-title" }, "IAP Simulator"),
|
|
512
|
-
selectRow("Next Purchase Result", results, s.iap.nextResult, (v) => {
|
|
513
|
-
aitState.patch("iap", { nextResult: v });
|
|
514
|
-
}, disabled)
|
|
515
|
-
),
|
|
516
|
-
h(
|
|
517
|
-
"div",
|
|
518
|
-
{ className: "ait-section" },
|
|
519
|
-
h("div", { className: "ait-section-title" }, "TossPay"),
|
|
520
|
-
selectRow("Next Payment Result", ["success", "fail"], s.payment.nextResult, (v) => {
|
|
521
|
-
aitState.patch("payment", { nextResult: v });
|
|
522
|
-
}, disabled)
|
|
523
|
-
),
|
|
524
|
-
h(
|
|
525
|
-
"div",
|
|
526
|
-
{ className: "ait-section" },
|
|
527
|
-
h("div", { className: "ait-section-title" }, `Completed Orders (${s.iap.completedOrders.length})`),
|
|
528
|
-
...s.iap.completedOrders.slice(-5).map(
|
|
529
|
-
(o) => h(
|
|
530
|
-
"div",
|
|
531
|
-
{ className: "ait-log-entry" },
|
|
532
|
-
h("span", { className: "ait-log-type" }, o.status),
|
|
533
|
-
`${o.sku} (${o.orderId.slice(-8)})`
|
|
534
|
-
)
|
|
535
|
-
)
|
|
536
|
-
)
|
|
537
|
-
);
|
|
538
|
-
return container;
|
|
681
|
+
/** 권한 체크 후 denied면 에러 throw */
|
|
682
|
+
function checkPermission(name, fnName) {
|
|
683
|
+
if (aitState.state.permissions[name] === "denied") throw new Error(`[@ait-co/devtools] ${fnName}: Permission "${name}" is denied. Change it in the DevTools panel.`);
|
|
539
684
|
}
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
"div",
|
|
553
|
-
{ className: "ait-section" },
|
|
554
|
-
h("div", { className: "ait-section-title" }, "Navigation Events"),
|
|
555
|
-
h("div", { className: "ait-row" }, backBtn, homeBtn)
|
|
556
|
-
),
|
|
557
|
-
h(
|
|
558
|
-
"div",
|
|
559
|
-
{ className: "ait-section" },
|
|
560
|
-
h("div", { className: "ait-section-title" }, "Login"),
|
|
561
|
-
selectRow("Logged In", ["true", "false"], String(aitState.state.auth.isLoggedIn), (v) => {
|
|
562
|
-
aitState.patch("auth", { isLoggedIn: v === "true" });
|
|
563
|
-
}, disabled),
|
|
564
|
-
selectRow("Toss Login Integrated", ["true", "false"], String(aitState.state.auth.isTossLoginIntegrated), (v) => {
|
|
565
|
-
aitState.patch("auth", { isTossLoginIntegrated: v === "true" });
|
|
566
|
-
}, disabled)
|
|
567
|
-
)
|
|
568
|
-
);
|
|
569
|
-
return container;
|
|
685
|
+
//#endregion
|
|
686
|
+
//#region src/mock/device/camera.ts
|
|
687
|
+
/**
|
|
688
|
+
* Camera & Album Photos mock
|
|
689
|
+
* mock/web/prompt 모드 지원
|
|
690
|
+
*/
|
|
691
|
+
async function openCameraMock() {
|
|
692
|
+
const images = getMockImages();
|
|
693
|
+
return {
|
|
694
|
+
id: crypto.randomUUID(),
|
|
695
|
+
dataUri: images[0]
|
|
696
|
+
};
|
|
570
697
|
}
|
|
571
|
-
function
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
)
|
|
603
|
-
);
|
|
604
|
-
return container;
|
|
698
|
+
async function openCameraWeb() {
|
|
699
|
+
return new Promise((resolve, reject) => {
|
|
700
|
+
const input = document.createElement("input");
|
|
701
|
+
input.type = "file";
|
|
702
|
+
input.accept = "image/*";
|
|
703
|
+
input.capture = "environment";
|
|
704
|
+
let settled = false;
|
|
705
|
+
input.onchange = () => {
|
|
706
|
+
settled = true;
|
|
707
|
+
const file = input.files?.[0];
|
|
708
|
+
if (!file) {
|
|
709
|
+
reject(/* @__PURE__ */ new Error("No file selected"));
|
|
710
|
+
return;
|
|
711
|
+
}
|
|
712
|
+
const reader = new FileReader();
|
|
713
|
+
reader.onload = () => resolve({
|
|
714
|
+
id: crypto.randomUUID(),
|
|
715
|
+
dataUri: reader.result
|
|
716
|
+
});
|
|
717
|
+
reader.onerror = () => reject(/* @__PURE__ */ new Error("Failed to read file"));
|
|
718
|
+
reader.readAsDataURL(file);
|
|
719
|
+
};
|
|
720
|
+
const onFocus = () => {
|
|
721
|
+
setTimeout(() => {
|
|
722
|
+
if (!settled) reject(/* @__PURE__ */ new Error("File picker cancelled"));
|
|
723
|
+
window.removeEventListener("focus", onFocus);
|
|
724
|
+
}, 300);
|
|
725
|
+
};
|
|
726
|
+
window.addEventListener("focus", onFocus);
|
|
727
|
+
input.click();
|
|
728
|
+
});
|
|
605
729
|
}
|
|
606
|
-
function
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
for (let i = 0; i < localStorage.length; i++) {
|
|
613
|
-
const key = localStorage.key(i);
|
|
614
|
-
if (key?.startsWith(prefix)) {
|
|
615
|
-
entries.push([key.slice(prefix.length), localStorage.getItem(key) ?? ""]);
|
|
616
|
-
}
|
|
617
|
-
}
|
|
618
|
-
const clearBtn = h("button", { className: "ait-btn ait-btn-sm ait-btn-danger" }, "Clear All");
|
|
619
|
-
if (disabled) clearBtn.disabled = true;
|
|
620
|
-
clearBtn.addEventListener("click", () => {
|
|
621
|
-
entries.forEach(([key]) => localStorage.removeItem(prefix + key));
|
|
622
|
-
refreshPanel();
|
|
623
|
-
});
|
|
624
|
-
container.append(
|
|
625
|
-
h(
|
|
626
|
-
"div",
|
|
627
|
-
{ className: "ait-section" },
|
|
628
|
-
h(
|
|
629
|
-
"div",
|
|
630
|
-
{ className: "ait-row" },
|
|
631
|
-
h("div", { className: "ait-section-title" }, `Storage (${entries.length} items)`),
|
|
632
|
-
clearBtn
|
|
633
|
-
),
|
|
634
|
-
entries.length === 0 ? h("div", { style: "color:#555;font-size:12px" }, "No items in storage") : h(
|
|
635
|
-
"div",
|
|
636
|
-
{},
|
|
637
|
-
...entries.map(
|
|
638
|
-
([key, value]) => h(
|
|
639
|
-
"div",
|
|
640
|
-
{ className: "ait-storage-row" },
|
|
641
|
-
h("span", { className: "ait-storage-key" }, key),
|
|
642
|
-
h("span", { className: "ait-storage-value" }, value.length > 100 ? value.slice(0, 100) + "..." : value)
|
|
643
|
-
)
|
|
644
|
-
)
|
|
645
|
-
)
|
|
646
|
-
)
|
|
647
|
-
);
|
|
648
|
-
return container;
|
|
730
|
+
async function openCameraPrompt() {
|
|
731
|
+
const dataUri = await waitForPromptResponse("camera");
|
|
732
|
+
return {
|
|
733
|
+
id: crypto.randomUUID(),
|
|
734
|
+
dataUri
|
|
735
|
+
};
|
|
649
736
|
}
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
737
|
+
const _openCamera = async (_options) => {
|
|
738
|
+
checkPermission("camera", "openCamera");
|
|
739
|
+
const mode = aitState.state.deviceModes.camera;
|
|
740
|
+
if (mode === "web") return openCameraWeb();
|
|
741
|
+
if (mode === "prompt") return openCameraPrompt();
|
|
742
|
+
return openCameraMock();
|
|
743
|
+
};
|
|
744
|
+
withPermission(_openCamera, "camera");
|
|
745
|
+
async function fetchAlbumPhotosMock(maxCount) {
|
|
746
|
+
return getMockImages().slice(0, maxCount).map((dataUri) => ({
|
|
747
|
+
id: crypto.randomUUID(),
|
|
748
|
+
dataUri
|
|
749
|
+
}));
|
|
750
|
+
}
|
|
751
|
+
async function fetchAlbumPhotosWeb(maxCount) {
|
|
752
|
+
return new Promise((resolve, reject) => {
|
|
753
|
+
const input = document.createElement("input");
|
|
754
|
+
input.type = "file";
|
|
755
|
+
input.accept = "image/*";
|
|
756
|
+
input.multiple = true;
|
|
757
|
+
let settled = false;
|
|
758
|
+
input.onchange = async () => {
|
|
759
|
+
settled = true;
|
|
760
|
+
const files = Array.from(input.files ?? []).slice(0, maxCount);
|
|
761
|
+
if (files.length === 0) {
|
|
762
|
+
reject(/* @__PURE__ */ new Error("No files selected"));
|
|
763
|
+
return;
|
|
764
|
+
}
|
|
765
|
+
resolve(await Promise.all(files.map((file) => new Promise((res, rej) => {
|
|
766
|
+
const reader = new FileReader();
|
|
767
|
+
reader.onload = () => res({
|
|
768
|
+
id: crypto.randomUUID(),
|
|
769
|
+
dataUri: reader.result
|
|
770
|
+
});
|
|
771
|
+
reader.onerror = () => rej(/* @__PURE__ */ new Error("Failed to read file"));
|
|
772
|
+
reader.readAsDataURL(file);
|
|
773
|
+
}))));
|
|
774
|
+
};
|
|
775
|
+
const onFocus = () => {
|
|
776
|
+
setTimeout(() => {
|
|
777
|
+
if (!settled) reject(/* @__PURE__ */ new Error("File picker cancelled"));
|
|
778
|
+
window.removeEventListener("focus", onFocus);
|
|
779
|
+
}, 300);
|
|
780
|
+
};
|
|
781
|
+
window.addEventListener("focus", onFocus);
|
|
782
|
+
input.click();
|
|
783
|
+
});
|
|
784
|
+
}
|
|
785
|
+
async function fetchAlbumPhotosPrompt(maxCount) {
|
|
786
|
+
return (await waitForPromptResponse("photos")).slice(0, maxCount).map((dataUri) => ({
|
|
787
|
+
id: crypto.randomUUID(),
|
|
788
|
+
dataUri
|
|
789
|
+
}));
|
|
790
|
+
}
|
|
791
|
+
const _fetchAlbumPhotos = async (options) => {
|
|
792
|
+
checkPermission("photos", "fetchAlbumPhotos");
|
|
793
|
+
const maxCount = options?.maxCount ?? 10;
|
|
794
|
+
const mode = aitState.state.deviceModes.photos;
|
|
795
|
+
if (mode === "web") return fetchAlbumPhotosWeb(maxCount);
|
|
796
|
+
if (mode === "prompt") return fetchAlbumPhotosPrompt(maxCount);
|
|
797
|
+
return fetchAlbumPhotosMock(maxCount);
|
|
798
|
+
};
|
|
799
|
+
withPermission(_fetchAlbumPhotos, "photos");
|
|
800
|
+
//#endregion
|
|
801
|
+
//#region src/mock/device/clipboard.ts
|
|
802
|
+
/**
|
|
803
|
+
* Clipboard mock
|
|
804
|
+
* mock/web 모드 지원
|
|
805
|
+
*/
|
|
806
|
+
const _getClipboardText = async () => {
|
|
807
|
+
checkPermission("clipboard", "getClipboardText");
|
|
808
|
+
if (aitState.state.deviceModes.clipboard === "mock") return aitState.state.mockData.clipboardText;
|
|
809
|
+
try {
|
|
810
|
+
return await navigator.clipboard.readText();
|
|
811
|
+
} catch {
|
|
812
|
+
return "";
|
|
813
|
+
}
|
|
814
|
+
};
|
|
815
|
+
withPermission(_getClipboardText, "clipboard");
|
|
816
|
+
const _setClipboardText = async (text) => {
|
|
817
|
+
checkPermission("clipboard", "setClipboardText");
|
|
818
|
+
if (aitState.state.deviceModes.clipboard === "mock") {
|
|
819
|
+
aitState.patch("mockData", { clipboardText: text });
|
|
820
|
+
return;
|
|
821
|
+
}
|
|
822
|
+
await navigator.clipboard.writeText(text);
|
|
823
|
+
};
|
|
824
|
+
withPermission(_setClipboardText, "clipboard");
|
|
825
|
+
//#endregion
|
|
826
|
+
//#region src/mock/device/contacts.ts
|
|
827
|
+
/**
|
|
828
|
+
* Contacts mock
|
|
829
|
+
*/
|
|
830
|
+
const _fetchContacts = async (options) => {
|
|
831
|
+
checkPermission("contacts", "fetchContacts");
|
|
832
|
+
let contacts = aitState.state.contacts;
|
|
833
|
+
if (options.query?.contains) {
|
|
834
|
+
const q = options.query.contains.toLowerCase();
|
|
835
|
+
contacts = contacts.filter((c) => c.name.toLowerCase().includes(q) || c.phoneNumber.includes(q));
|
|
836
|
+
}
|
|
837
|
+
const sliced = contacts.slice(options.offset, options.offset + options.size);
|
|
838
|
+
const nextOffset = options.offset + options.size;
|
|
839
|
+
return {
|
|
840
|
+
result: sliced,
|
|
841
|
+
nextOffset: nextOffset < contacts.length ? nextOffset : null,
|
|
842
|
+
done: nextOffset >= contacts.length
|
|
843
|
+
};
|
|
844
|
+
};
|
|
845
|
+
withPermission(_fetchContacts, "contacts");
|
|
846
|
+
//#endregion
|
|
847
|
+
//#region src/mock/device/location.ts
|
|
848
|
+
/**
|
|
849
|
+
* Location mock (getCurrentLocation, startUpdateLocation)
|
|
850
|
+
* mock/web/prompt 모드 지원
|
|
851
|
+
*/
|
|
852
|
+
function buildLocation() {
|
|
853
|
+
return {
|
|
854
|
+
coords: { ...aitState.state.location.coords },
|
|
855
|
+
timestamp: Date.now(),
|
|
856
|
+
accessLocation: aitState.state.location.accessLocation
|
|
857
|
+
};
|
|
858
|
+
}
|
|
859
|
+
async function getCurrentLocationMock() {
|
|
860
|
+
return buildLocation();
|
|
861
|
+
}
|
|
862
|
+
async function getCurrentLocationWeb() {
|
|
863
|
+
return new Promise((resolve) => {
|
|
864
|
+
if (!navigator.geolocation) {
|
|
865
|
+
console.warn("[@ait-co/devtools] Geolocation API not available, falling back to mock");
|
|
866
|
+
resolve(buildLocation());
|
|
867
|
+
return;
|
|
868
|
+
}
|
|
869
|
+
navigator.geolocation.getCurrentPosition((pos) => {
|
|
870
|
+
resolve({
|
|
871
|
+
coords: {
|
|
872
|
+
latitude: pos.coords.latitude,
|
|
873
|
+
longitude: pos.coords.longitude,
|
|
874
|
+
altitude: pos.coords.altitude ?? 0,
|
|
875
|
+
accuracy: pos.coords.accuracy,
|
|
876
|
+
altitudeAccuracy: pos.coords.altitudeAccuracy ?? 0,
|
|
877
|
+
heading: pos.coords.heading ?? 0
|
|
878
|
+
},
|
|
879
|
+
timestamp: pos.timestamp,
|
|
880
|
+
accessLocation: "FINE"
|
|
881
|
+
});
|
|
882
|
+
}, () => {
|
|
883
|
+
console.warn("[@ait-co/devtools] Geolocation failed, falling back to mock");
|
|
884
|
+
resolve(buildLocation());
|
|
885
|
+
});
|
|
886
|
+
});
|
|
661
887
|
}
|
|
888
|
+
async function getCurrentLocationPrompt() {
|
|
889
|
+
return waitForPromptResponse("location");
|
|
890
|
+
}
|
|
891
|
+
const _getCurrentLocation = async (_options) => {
|
|
892
|
+
checkPermission("geolocation", "getCurrentLocation");
|
|
893
|
+
const mode = aitState.state.deviceModes.location;
|
|
894
|
+
if (mode === "web") return getCurrentLocationWeb();
|
|
895
|
+
if (mode === "prompt") return getCurrentLocationPrompt();
|
|
896
|
+
return getCurrentLocationMock();
|
|
897
|
+
};
|
|
898
|
+
withPermission(_getCurrentLocation, "geolocation");
|
|
899
|
+
function startUpdateLocationMock(eventParams) {
|
|
900
|
+
const { onEvent, options } = eventParams;
|
|
901
|
+
const interval = Math.max(options.timeInterval, 500);
|
|
902
|
+
const id = setInterval(() => {
|
|
903
|
+
const loc = buildLocation();
|
|
904
|
+
loc.coords.latitude += (Math.random() - .5) * 1e-4;
|
|
905
|
+
loc.coords.longitude += (Math.random() - .5) * 1e-4;
|
|
906
|
+
onEvent(loc);
|
|
907
|
+
}, interval);
|
|
908
|
+
return () => clearInterval(id);
|
|
909
|
+
}
|
|
910
|
+
function startUpdateLocationWeb(eventParams) {
|
|
911
|
+
const { onEvent, onError } = eventParams;
|
|
912
|
+
if (!navigator.geolocation) {
|
|
913
|
+
console.warn("[@ait-co/devtools] Geolocation API not available, falling back to mock");
|
|
914
|
+
return startUpdateLocationMock(eventParams);
|
|
915
|
+
}
|
|
916
|
+
const watchId = navigator.geolocation.watchPosition((pos) => {
|
|
917
|
+
onEvent({
|
|
918
|
+
coords: {
|
|
919
|
+
latitude: pos.coords.latitude,
|
|
920
|
+
longitude: pos.coords.longitude,
|
|
921
|
+
altitude: pos.coords.altitude ?? 0,
|
|
922
|
+
accuracy: pos.coords.accuracy,
|
|
923
|
+
altitudeAccuracy: pos.coords.altitudeAccuracy ?? 0,
|
|
924
|
+
heading: pos.coords.heading ?? 0
|
|
925
|
+
},
|
|
926
|
+
timestamp: pos.timestamp,
|
|
927
|
+
accessLocation: "FINE"
|
|
928
|
+
});
|
|
929
|
+
}, (err) => onError(err));
|
|
930
|
+
return () => navigator.geolocation.clearWatch(watchId);
|
|
931
|
+
}
|
|
932
|
+
function startUpdateLocationPrompt(eventParams) {
|
|
933
|
+
const { onEvent } = eventParams;
|
|
934
|
+
const handler = (e) => {
|
|
935
|
+
onEvent(e.detail);
|
|
936
|
+
};
|
|
937
|
+
window.addEventListener("__ait:prompt-response:location-update", handler);
|
|
938
|
+
window.dispatchEvent(new CustomEvent("__ait:prompt-request", { detail: { type: "location-update" } }));
|
|
939
|
+
return () => window.removeEventListener("__ait:prompt-response:location-update", handler);
|
|
940
|
+
}
|
|
941
|
+
const _startUpdateLocation = (eventParams) => {
|
|
942
|
+
const mode = aitState.state.deviceModes.location;
|
|
943
|
+
if (mode === "web") return startUpdateLocationWeb(eventParams);
|
|
944
|
+
if (mode === "prompt") return startUpdateLocationPrompt(eventParams);
|
|
945
|
+
return startUpdateLocationMock(eventParams);
|
|
946
|
+
};
|
|
947
|
+
withPermission(_startUpdateLocation, "geolocation");
|
|
948
|
+
//#endregion
|
|
949
|
+
//#region src/mock/proxy.ts
|
|
950
|
+
/**
|
|
951
|
+
* 미구현 API용 Proxy 트립와이어.
|
|
952
|
+
*
|
|
953
|
+
* 미구현 프로퍼티에 접근하면 throw한다. 이는 "devtools에서는 멀쩡히 돌지만
|
|
954
|
+
* 실 SDK에선 실제로 동작하는" 시나리오를 차단하기 위한 의도적 선택이다.
|
|
955
|
+
* mock이 미구현인 API는 실 SDK에서는 존재할 수 있고, 사용자가 이를 인지하지
|
|
956
|
+
* 못한 채 개발을 이어가면 배포 시점에 놀라게 된다. 에러 메시지에 이슈 URL을
|
|
957
|
+
* 포함해 사용자가 mock 누락을 제보할 수 있게 한다.
|
|
958
|
+
*/
|
|
959
|
+
const ISSUES_URL = "https://github.com/apps-in-toss-community/devtools/issues";
|
|
960
|
+
function createMockProxy(moduleName, implementations) {
|
|
961
|
+
return new Proxy(implementations, { get(target, prop) {
|
|
962
|
+
if (typeof prop === "symbol") return void 0;
|
|
963
|
+
if (prop in target) return target[prop];
|
|
964
|
+
throw new Error(`[@ait-co/devtools] ${moduleName}.${prop} is not mocked. This API may exist in @apps-in-toss/web-framework, but devtools' mock does not cover it yet. Please file an issue: ${ISSUES_URL}`);
|
|
965
|
+
} });
|
|
966
|
+
}
|
|
967
|
+
createMockProxy("Storage", {
|
|
968
|
+
getItem: async (key) => {
|
|
969
|
+
return localStorage.getItem(`__ait_storage:${key}`);
|
|
970
|
+
},
|
|
971
|
+
setItem: async (key, value) => {
|
|
972
|
+
localStorage.setItem(`__ait_storage:${key}`, value);
|
|
973
|
+
},
|
|
974
|
+
removeItem: async (key) => {
|
|
975
|
+
localStorage.removeItem(`__ait_storage:${key}`);
|
|
976
|
+
},
|
|
977
|
+
clearItems: async () => {
|
|
978
|
+
const keys = Object.keys(localStorage).filter((k) => k.startsWith("__ait_storage:"));
|
|
979
|
+
for (const k of keys) localStorage.removeItem(k);
|
|
980
|
+
}
|
|
981
|
+
});
|
|
982
|
+
//#endregion
|
|
983
|
+
//#region src/panel/tabs/device.ts
|
|
984
|
+
let pendingPrompt = null;
|
|
985
|
+
let refreshPanel$1 = () => {};
|
|
986
|
+
function setDeviceRefreshPanel(fn) {
|
|
987
|
+
refreshPanel$1 = fn;
|
|
988
|
+
}
|
|
989
|
+
if (typeof window !== "undefined") window.addEventListener("__ait:prompt-request", (e) => {
|
|
990
|
+
pendingPrompt = { type: e.detail.type };
|
|
991
|
+
window.dispatchEvent(new CustomEvent("__ait:panel-switch-tab", { detail: { tab: "device" } }));
|
|
992
|
+
});
|
|
662
993
|
function resolvePrompt(type, data) {
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
994
|
+
window.dispatchEvent(new CustomEvent(`__ait:prompt-response:${type}`, { detail: data }));
|
|
995
|
+
pendingPrompt = null;
|
|
996
|
+
refreshPanel$1();
|
|
666
997
|
}
|
|
667
998
|
function renderPromptBanner() {
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
refreshPanel();
|
|
745
|
-
});
|
|
746
|
-
banner.appendChild(cancelBtn);
|
|
747
|
-
return banner;
|
|
999
|
+
if (!pendingPrompt) return null;
|
|
1000
|
+
const banner = h("div", { className: "ait-prompt-banner" });
|
|
1001
|
+
if (pendingPrompt.type === "camera") {
|
|
1002
|
+
banner.append(h("div", { className: "ait-prompt-title" }, "Camera Prompt — Select an image"));
|
|
1003
|
+
const input = h("input", {
|
|
1004
|
+
type: "file",
|
|
1005
|
+
accept: "image/*",
|
|
1006
|
+
style: "font-size:11px;color:#aaa"
|
|
1007
|
+
});
|
|
1008
|
+
input.addEventListener("change", () => {
|
|
1009
|
+
const file = input.files?.[0];
|
|
1010
|
+
if (!file) return;
|
|
1011
|
+
const reader = new FileReader();
|
|
1012
|
+
reader.onload = () => resolvePrompt("camera", reader.result);
|
|
1013
|
+
reader.readAsDataURL(file);
|
|
1014
|
+
});
|
|
1015
|
+
banner.appendChild(input);
|
|
1016
|
+
} else if (pendingPrompt.type === "photos") {
|
|
1017
|
+
banner.append(h("div", { className: "ait-prompt-title" }, "Photos Prompt — Select images"));
|
|
1018
|
+
const input = h("input", {
|
|
1019
|
+
type: "file",
|
|
1020
|
+
accept: "image/*",
|
|
1021
|
+
multiple: "",
|
|
1022
|
+
style: "font-size:11px;color:#aaa"
|
|
1023
|
+
});
|
|
1024
|
+
input.addEventListener("change", () => {
|
|
1025
|
+
const files = Array.from(input.files ?? []);
|
|
1026
|
+
if (files.length === 0) return;
|
|
1027
|
+
Promise.all(files.map((file) => new Promise((res) => {
|
|
1028
|
+
const reader = new FileReader();
|
|
1029
|
+
reader.onload = () => res(reader.result);
|
|
1030
|
+
reader.readAsDataURL(file);
|
|
1031
|
+
}))).then((dataUris) => resolvePrompt("photos", dataUris));
|
|
1032
|
+
});
|
|
1033
|
+
banner.appendChild(input);
|
|
1034
|
+
} else if (pendingPrompt.type === "location" || pendingPrompt.type === "location-update") {
|
|
1035
|
+
banner.append(h("div", { className: "ait-prompt-title" }, pendingPrompt.type === "location" ? "Location Prompt — Enter coordinates" : "Location Update — Send coordinates"));
|
|
1036
|
+
const latInput = h("input", {
|
|
1037
|
+
className: "ait-input",
|
|
1038
|
+
value: String(aitState.state.location.coords.latitude),
|
|
1039
|
+
style: "width:80px"
|
|
1040
|
+
});
|
|
1041
|
+
const lngInput = h("input", {
|
|
1042
|
+
className: "ait-input",
|
|
1043
|
+
value: String(aitState.state.location.coords.longitude),
|
|
1044
|
+
style: "width:80px"
|
|
1045
|
+
});
|
|
1046
|
+
const sendBtn = h("button", { className: "ait-btn ait-btn-sm" }, "Send");
|
|
1047
|
+
sendBtn.addEventListener("click", () => {
|
|
1048
|
+
const loc = {
|
|
1049
|
+
coords: {
|
|
1050
|
+
latitude: Number(latInput.value),
|
|
1051
|
+
longitude: Number(lngInput.value),
|
|
1052
|
+
altitude: 0,
|
|
1053
|
+
accuracy: 10,
|
|
1054
|
+
altitudeAccuracy: 0,
|
|
1055
|
+
heading: 0
|
|
1056
|
+
},
|
|
1057
|
+
timestamp: Date.now(),
|
|
1058
|
+
accessLocation: "FINE"
|
|
1059
|
+
};
|
|
1060
|
+
resolvePrompt(pendingPrompt.type, loc);
|
|
1061
|
+
});
|
|
1062
|
+
banner.append(h("div", { className: "ait-prompt-input-row" }, h("label", {}, "Lat"), latInput, h("label", {}, "Lng"), lngInput, sendBtn));
|
|
1063
|
+
} else banner.append(h("div", { className: "ait-prompt-title" }, `Prompt: ${pendingPrompt.type}`));
|
|
1064
|
+
const cancelBtn = h("button", {
|
|
1065
|
+
className: "ait-btn ait-btn-sm ait-btn-danger",
|
|
1066
|
+
style: "margin-top:8px"
|
|
1067
|
+
}, "Cancel");
|
|
1068
|
+
cancelBtn.addEventListener("click", () => {
|
|
1069
|
+
pendingPrompt = null;
|
|
1070
|
+
window.dispatchEvent(new CustomEvent("__ait:prompt-cancel"));
|
|
1071
|
+
refreshPanel$1();
|
|
1072
|
+
});
|
|
1073
|
+
banner.appendChild(cancelBtn);
|
|
1074
|
+
return banner;
|
|
748
1075
|
}
|
|
749
1076
|
function renderDeviceTab() {
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
1077
|
+
const s = aitState.state;
|
|
1078
|
+
const disabled = !s.panelEditable;
|
|
1079
|
+
const container = h("div");
|
|
1080
|
+
if (disabled) container.appendChild(monitoringNotice());
|
|
1081
|
+
if (s.panelEditable) {
|
|
1082
|
+
const promptBanner = renderPromptBanner();
|
|
1083
|
+
if (promptBanner) container.appendChild(promptBanner);
|
|
1084
|
+
}
|
|
1085
|
+
container.append(h("div", { className: "ait-section" }, h("div", { className: "ait-section-title" }, "Device API Modes"), ...[
|
|
1086
|
+
{
|
|
1087
|
+
label: "Camera",
|
|
1088
|
+
key: "camera",
|
|
1089
|
+
options: [
|
|
1090
|
+
"mock",
|
|
1091
|
+
"web",
|
|
1092
|
+
"prompt"
|
|
1093
|
+
]
|
|
1094
|
+
},
|
|
1095
|
+
{
|
|
1096
|
+
label: "Photos",
|
|
1097
|
+
key: "photos",
|
|
1098
|
+
options: [
|
|
1099
|
+
"mock",
|
|
1100
|
+
"web",
|
|
1101
|
+
"prompt"
|
|
1102
|
+
]
|
|
1103
|
+
},
|
|
1104
|
+
{
|
|
1105
|
+
label: "Location",
|
|
1106
|
+
key: "location",
|
|
1107
|
+
options: [
|
|
1108
|
+
"mock",
|
|
1109
|
+
"web",
|
|
1110
|
+
"prompt"
|
|
1111
|
+
]
|
|
1112
|
+
},
|
|
1113
|
+
{
|
|
1114
|
+
label: "Network",
|
|
1115
|
+
key: "network",
|
|
1116
|
+
options: ["mock", "web"]
|
|
1117
|
+
},
|
|
1118
|
+
{
|
|
1119
|
+
label: "Clipboard",
|
|
1120
|
+
key: "clipboard",
|
|
1121
|
+
options: ["mock", "web"]
|
|
1122
|
+
}
|
|
1123
|
+
].map((entry) => selectRow(entry.label, entry.options, s.deviceModes[entry.key], (v) => {
|
|
1124
|
+
aitState.patch("deviceModes", { [entry.key]: v });
|
|
1125
|
+
}, disabled))));
|
|
1126
|
+
const images = s.mockData.images;
|
|
1127
|
+
const imageGrid = h("div", { className: "ait-image-grid" });
|
|
1128
|
+
images.forEach((dataUri, idx) => {
|
|
1129
|
+
const thumb = h("div", { className: "ait-image-thumb" });
|
|
1130
|
+
const img = h("img", { src: dataUri });
|
|
1131
|
+
const removeBtn = h("button", { className: "ait-image-remove" }, "x");
|
|
1132
|
+
removeBtn.addEventListener("click", () => {
|
|
1133
|
+
const newImages = [...aitState.state.mockData.images];
|
|
1134
|
+
newImages.splice(idx, 1);
|
|
1135
|
+
aitState.patch("mockData", { images: newImages });
|
|
1136
|
+
});
|
|
1137
|
+
if (disabled) removeBtn.disabled = true;
|
|
1138
|
+
thumb.append(img, removeBtn);
|
|
1139
|
+
imageGrid.appendChild(thumb);
|
|
1140
|
+
});
|
|
1141
|
+
const addBtn = h("button", { className: "ait-btn-secondary" }, "+ Add");
|
|
1142
|
+
addBtn.addEventListener("click", () => {
|
|
1143
|
+
const input = document.createElement("input");
|
|
1144
|
+
input.type = "file";
|
|
1145
|
+
input.accept = "image/*";
|
|
1146
|
+
input.multiple = true;
|
|
1147
|
+
input.onchange = () => {
|
|
1148
|
+
const files = Array.from(input.files ?? []);
|
|
1149
|
+
Promise.all(files.map((file) => new Promise((res) => {
|
|
1150
|
+
const reader = new FileReader();
|
|
1151
|
+
reader.onload = () => res(reader.result);
|
|
1152
|
+
reader.readAsDataURL(file);
|
|
1153
|
+
}))).then((dataUris) => {
|
|
1154
|
+
aitState.patch("mockData", { images: [...aitState.state.mockData.images, ...dataUris] });
|
|
1155
|
+
});
|
|
1156
|
+
};
|
|
1157
|
+
input.click();
|
|
1158
|
+
});
|
|
1159
|
+
if (disabled) addBtn.disabled = true;
|
|
1160
|
+
const defaultsBtn = h("button", { className: "ait-btn-secondary" }, "Use defaults");
|
|
1161
|
+
defaultsBtn.addEventListener("click", () => {
|
|
1162
|
+
aitState.patch("mockData", { images: [...getDefaultPlaceholderImages()] });
|
|
1163
|
+
});
|
|
1164
|
+
if (disabled) defaultsBtn.disabled = true;
|
|
1165
|
+
const clearImagesBtn = h("button", { className: "ait-btn-secondary" }, "Clear");
|
|
1166
|
+
clearImagesBtn.addEventListener("click", () => {
|
|
1167
|
+
aitState.patch("mockData", { images: [] });
|
|
1168
|
+
});
|
|
1169
|
+
if (disabled) clearImagesBtn.disabled = true;
|
|
1170
|
+
container.append(h("div", { className: "ait-section" }, h("div", { className: "ait-section-title" }, `Mock Images (${images.length})`), imageGrid, h("div", { className: "ait-btn-row" }, addBtn, defaultsBtn, clearImagesBtn)));
|
|
1171
|
+
return container;
|
|
836
1172
|
}
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
1173
|
+
//#endregion
|
|
1174
|
+
//#region src/panel/tabs/analytics.ts
|
|
1175
|
+
function renderAnalyticsTab() {
|
|
1176
|
+
const disabled = !aitState.state.panelEditable;
|
|
1177
|
+
const container = h("div");
|
|
1178
|
+
if (disabled) container.appendChild(monitoringNotice());
|
|
1179
|
+
const logs = aitState.state.analyticsLog;
|
|
1180
|
+
const clearBtn = h("button", { className: "ait-btn ait-btn-sm ait-btn-danger" }, "Clear");
|
|
1181
|
+
if (disabled) clearBtn.disabled = true;
|
|
1182
|
+
clearBtn.addEventListener("click", () => {
|
|
1183
|
+
aitState.update({ analyticsLog: [] });
|
|
1184
|
+
});
|
|
1185
|
+
container.append(h("div", { className: "ait-section" }, h("div", { className: "ait-row" }, h("div", { className: "ait-section-title" }, `Analytics Log (${logs.length})`), clearBtn), ...logs.slice(-30).reverse().map((entry) => {
|
|
1186
|
+
return h("div", { className: "ait-log-entry" }, h("span", { className: "ait-log-time" }, new Date(entry.timestamp).toLocaleTimeString("ko-KR", { hour12: false })), h("span", { className: "ait-log-type" }, entry.type), JSON.stringify(entry.params));
|
|
1187
|
+
})));
|
|
1188
|
+
return container;
|
|
843
1189
|
}
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
1190
|
+
//#endregion
|
|
1191
|
+
//#region src/panel/tabs/environment.ts
|
|
1192
|
+
function renderEnvironmentTab() {
|
|
1193
|
+
const s = aitState.state;
|
|
1194
|
+
const disabled = !s.panelEditable;
|
|
1195
|
+
const container = h("div");
|
|
1196
|
+
if (disabled) container.appendChild(monitoringNotice());
|
|
1197
|
+
container.append(h("div", { className: "ait-section" }, h("div", { className: "ait-section-title" }, "Platform"), selectRow("OS", ["ios", "android"], s.platform, (v) => aitState.update({ platform: v }), disabled), inputRow("App Version", s.appVersion, (v) => aitState.update({ appVersion: v }), disabled), selectRow("Environment", ["toss", "sandbox"], s.environment, (v) => aitState.update({ environment: v }), disabled), inputRow("Locale", s.locale, (v) => aitState.update({ locale: v }), disabled)), h("div", { className: "ait-section" }, h("div", { className: "ait-section-title" }, "Network"), selectRow("Status", [
|
|
1198
|
+
"WIFI",
|
|
1199
|
+
"4G",
|
|
1200
|
+
"5G",
|
|
1201
|
+
"3G",
|
|
1202
|
+
"2G",
|
|
1203
|
+
"OFFLINE",
|
|
1204
|
+
"WWAN",
|
|
1205
|
+
"UNKNOWN"
|
|
1206
|
+
], s.networkStatus, (v) => aitState.update({ networkStatus: v }), disabled)), h("div", { className: "ait-section" }, h("div", { className: "ait-section-title" }, "Safe Area Insets"), inputRow("Top", String(s.safeAreaInsets.top), (v) => aitState.patch("safeAreaInsets", { top: Number(v) }), disabled), inputRow("Bottom", String(s.safeAreaInsets.bottom), (v) => aitState.patch("safeAreaInsets", { bottom: Number(v) }), disabled)));
|
|
1207
|
+
return container;
|
|
1208
|
+
}
|
|
1209
|
+
//#endregion
|
|
1210
|
+
//#region src/panel/tabs/events.ts
|
|
1211
|
+
function renderEventsTab() {
|
|
1212
|
+
const disabled = !aitState.state.panelEditable;
|
|
1213
|
+
const container = h("div");
|
|
1214
|
+
if (disabled) container.appendChild(monitoringNotice());
|
|
1215
|
+
const backBtn = h("button", { className: "ait-btn" }, "Trigger Back Event");
|
|
1216
|
+
backBtn.addEventListener("click", () => aitState.trigger("backEvent"));
|
|
1217
|
+
if (disabled) backBtn.disabled = true;
|
|
1218
|
+
const homeBtn = h("button", { className: "ait-btn" }, "Trigger Home Event");
|
|
1219
|
+
homeBtn.addEventListener("click", () => aitState.trigger("homeEvent"));
|
|
1220
|
+
if (disabled) homeBtn.disabled = true;
|
|
1221
|
+
container.append(h("div", { className: "ait-section" }, h("div", { className: "ait-section-title" }, "Navigation Events"), h("div", { className: "ait-row" }, backBtn, homeBtn)), h("div", { className: "ait-section" }, h("div", { className: "ait-section-title" }, "Login"), selectRow("Logged In", ["true", "false"], String(aitState.state.auth.isLoggedIn), (v) => {
|
|
1222
|
+
aitState.patch("auth", { isLoggedIn: v === "true" });
|
|
1223
|
+
}, disabled), selectRow("Toss Login Integrated", ["true", "false"], String(aitState.state.auth.isTossLoginIntegrated), (v) => {
|
|
1224
|
+
aitState.patch("auth", { isTossLoginIntegrated: v === "true" });
|
|
1225
|
+
}, disabled)));
|
|
1226
|
+
return container;
|
|
1227
|
+
}
|
|
1228
|
+
//#endregion
|
|
1229
|
+
//#region src/panel/tabs/iap.ts
|
|
1230
|
+
function renderIapTab() {
|
|
1231
|
+
const s = aitState.state;
|
|
1232
|
+
const disabled = !s.panelEditable;
|
|
1233
|
+
const container = h("div");
|
|
1234
|
+
const results = [
|
|
1235
|
+
"success",
|
|
1236
|
+
"USER_CANCELED",
|
|
1237
|
+
"INVALID_PRODUCT_ID",
|
|
1238
|
+
"PAYMENT_PENDING",
|
|
1239
|
+
"NETWORK_ERROR",
|
|
1240
|
+
"ITEM_ALREADY_OWNED",
|
|
1241
|
+
"INTERNAL_ERROR"
|
|
1242
|
+
];
|
|
1243
|
+
if (disabled) container.appendChild(monitoringNotice());
|
|
1244
|
+
container.append(h("div", { className: "ait-section" }, h("div", { className: "ait-section-title" }, "IAP Simulator"), selectRow("Next Purchase Result", results, s.iap.nextResult, (v) => {
|
|
1245
|
+
aitState.patch("iap", { nextResult: v });
|
|
1246
|
+
}, disabled)), h("div", { className: "ait-section" }, h("div", { className: "ait-section-title" }, "TossPay"), selectRow("Next Payment Result", ["success", "fail"], s.payment.nextResult, (v) => {
|
|
1247
|
+
aitState.patch("payment", { nextResult: v });
|
|
1248
|
+
}, disabled)), h("div", { className: "ait-section" }, h("div", { className: "ait-section-title" }, `Completed Orders (${s.iap.completedOrders.length})`), ...s.iap.completedOrders.slice(-5).map((o) => h("div", { className: "ait-log-entry" }, h("span", { className: "ait-log-type" }, o.status), `${o.sku} (${o.orderId.slice(-8)})`))));
|
|
1249
|
+
return container;
|
|
1250
|
+
}
|
|
1251
|
+
//#endregion
|
|
1252
|
+
//#region src/panel/tabs/location.ts
|
|
1253
|
+
function renderLocationTab() {
|
|
1254
|
+
const s = aitState.state;
|
|
1255
|
+
const disabled = !s.panelEditable;
|
|
1256
|
+
const container = h("div");
|
|
1257
|
+
if (disabled) container.appendChild(monitoringNotice());
|
|
1258
|
+
container.append(h("div", { className: "ait-section" }, h("div", { className: "ait-section-title" }, "Current Location"), inputRow("Latitude", String(s.location.coords.latitude), (v) => {
|
|
1259
|
+
const coords = {
|
|
1260
|
+
...s.location.coords,
|
|
1261
|
+
latitude: Number(v)
|
|
1262
|
+
};
|
|
1263
|
+
aitState.patch("location", { coords });
|
|
1264
|
+
}, disabled), inputRow("Longitude", String(s.location.coords.longitude), (v) => {
|
|
1265
|
+
const coords = {
|
|
1266
|
+
...s.location.coords,
|
|
1267
|
+
longitude: Number(v)
|
|
1268
|
+
};
|
|
1269
|
+
aitState.patch("location", { coords });
|
|
1270
|
+
}, disabled), inputRow("Accuracy", String(s.location.coords.accuracy), (v) => {
|
|
1271
|
+
const coords = {
|
|
1272
|
+
...s.location.coords,
|
|
1273
|
+
accuracy: Number(v)
|
|
1274
|
+
};
|
|
1275
|
+
aitState.patch("location", { coords });
|
|
1276
|
+
}, disabled)));
|
|
1277
|
+
return container;
|
|
1278
|
+
}
|
|
1279
|
+
//#endregion
|
|
1280
|
+
//#region src/panel/tabs/permissions.ts
|
|
1281
|
+
function renderPermissionsTab() {
|
|
1282
|
+
const s = aitState.state;
|
|
1283
|
+
const disabled = !s.panelEditable;
|
|
1284
|
+
const container = h("div");
|
|
1285
|
+
const names = [
|
|
1286
|
+
"camera",
|
|
1287
|
+
"photos",
|
|
1288
|
+
"geolocation",
|
|
1289
|
+
"clipboard",
|
|
1290
|
+
"contacts",
|
|
1291
|
+
"microphone"
|
|
1292
|
+
];
|
|
1293
|
+
const statuses = [
|
|
1294
|
+
"allowed",
|
|
1295
|
+
"denied",
|
|
1296
|
+
"notDetermined"
|
|
1297
|
+
];
|
|
1298
|
+
if (disabled) container.appendChild(monitoringNotice());
|
|
1299
|
+
container.append(h("div", { className: "ait-section" }, h("div", { className: "ait-section-title" }, "Device Permissions"), ...names.map((name) => selectRow(name, statuses, s.permissions[name], (v) => {
|
|
1300
|
+
aitState.patch("permissions", { [name]: v });
|
|
1301
|
+
}, disabled))));
|
|
1302
|
+
return container;
|
|
1303
|
+
}
|
|
1304
|
+
//#endregion
|
|
1305
|
+
//#region src/panel/tabs/storage.ts
|
|
1306
|
+
function renderStorageTab(refreshPanel) {
|
|
1307
|
+
const disabled = !aitState.state.panelEditable;
|
|
1308
|
+
const container = h("div");
|
|
1309
|
+
if (disabled) container.appendChild(monitoringNotice());
|
|
1310
|
+
const prefix = "__ait_storage:";
|
|
1311
|
+
const entries = [];
|
|
1312
|
+
for (let i = 0; i < localStorage.length; i++) {
|
|
1313
|
+
const key = localStorage.key(i);
|
|
1314
|
+
if (key?.startsWith(prefix)) entries.push([key.slice(14), localStorage.getItem(key) ?? ""]);
|
|
1315
|
+
}
|
|
1316
|
+
const clearBtn = h("button", { className: "ait-btn ait-btn-sm ait-btn-danger" }, "Clear All");
|
|
1317
|
+
if (disabled) clearBtn.disabled = true;
|
|
1318
|
+
clearBtn.addEventListener("click", () => {
|
|
1319
|
+
for (const [key] of entries) localStorage.removeItem(prefix + key);
|
|
1320
|
+
refreshPanel();
|
|
1321
|
+
});
|
|
1322
|
+
container.append(h("div", { className: "ait-section" }, h("div", { className: "ait-row" }, h("div", { className: "ait-section-title" }, `Storage (${entries.length} items)`), clearBtn), entries.length === 0 ? h("div", { style: "color:#555;font-size:12px" }, "No items in storage") : h("div", {}, ...entries.map(([key, value]) => h("div", { className: "ait-storage-row" }, h("span", { className: "ait-storage-key" }, key), h("span", { className: "ait-storage-value" }, value.length > 100 ? `${value.slice(0, 100)}...` : value))))));
|
|
1323
|
+
return container;
|
|
1324
|
+
}
|
|
1325
|
+
//#endregion
|
|
1326
|
+
//#region src/panel/tabs/index.ts
|
|
1327
|
+
const TABS = [
|
|
1328
|
+
{
|
|
1329
|
+
id: "env",
|
|
1330
|
+
label: "Environment"
|
|
1331
|
+
},
|
|
1332
|
+
{
|
|
1333
|
+
id: "permissions",
|
|
1334
|
+
label: "Permissions"
|
|
1335
|
+
},
|
|
1336
|
+
{
|
|
1337
|
+
id: "location",
|
|
1338
|
+
label: "Location"
|
|
1339
|
+
},
|
|
1340
|
+
{
|
|
1341
|
+
id: "device",
|
|
1342
|
+
label: "Device"
|
|
1343
|
+
},
|
|
1344
|
+
{
|
|
1345
|
+
id: "iap",
|
|
1346
|
+
label: "IAP"
|
|
1347
|
+
},
|
|
1348
|
+
{
|
|
1349
|
+
id: "events",
|
|
1350
|
+
label: "Events"
|
|
1351
|
+
},
|
|
1352
|
+
{
|
|
1353
|
+
id: "analytics",
|
|
1354
|
+
label: "Analytics"
|
|
1355
|
+
},
|
|
1356
|
+
{
|
|
1357
|
+
id: "storage",
|
|
1358
|
+
label: "Storage"
|
|
1359
|
+
}
|
|
1360
|
+
];
|
|
1361
|
+
function createTabRenderers(refreshPanel) {
|
|
1362
|
+
return {
|
|
1363
|
+
env: renderEnvironmentTab,
|
|
1364
|
+
permissions: renderPermissionsTab,
|
|
1365
|
+
location: renderLocationTab,
|
|
1366
|
+
device: renderDeviceTab,
|
|
1367
|
+
iap: renderIapTab,
|
|
1368
|
+
events: renderEventsTab,
|
|
1369
|
+
analytics: renderAnalyticsTab,
|
|
1370
|
+
storage: () => renderStorageTab(refreshPanel)
|
|
1371
|
+
};
|
|
1372
|
+
}
|
|
1373
|
+
//#endregion
|
|
1374
|
+
//#region src/panel/index.ts
|
|
1375
|
+
/**
|
|
1376
|
+
* @ait-co/devtools Floating Panel
|
|
1377
|
+
*
|
|
1378
|
+
* import 하면 자동으로 페이지에 DevTools 패널을 마운트한다.
|
|
1379
|
+
* 외부 의존성 없이 vanilla DOM으로 구현.
|
|
1380
|
+
*/
|
|
854
1381
|
function makeDraggable(el, onClickOnly) {
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
}
|
|
906
|
-
});
|
|
1382
|
+
let isDragging = false;
|
|
1383
|
+
let startX = 0, startY = 0;
|
|
1384
|
+
let startLeft = 0, startTop = 0;
|
|
1385
|
+
let hasMoved = false;
|
|
1386
|
+
el.addEventListener("pointerdown", (e) => {
|
|
1387
|
+
isDragging = true;
|
|
1388
|
+
hasMoved = false;
|
|
1389
|
+
startX = e.clientX;
|
|
1390
|
+
startY = e.clientY;
|
|
1391
|
+
const rect = el.getBoundingClientRect();
|
|
1392
|
+
startLeft = rect.left;
|
|
1393
|
+
startTop = rect.top;
|
|
1394
|
+
el.setPointerCapture(e.pointerId);
|
|
1395
|
+
e.preventDefault();
|
|
1396
|
+
});
|
|
1397
|
+
el.addEventListener("pointermove", (e) => {
|
|
1398
|
+
if (!isDragging) return;
|
|
1399
|
+
const dx = e.clientX - startX;
|
|
1400
|
+
const dy = e.clientY - startY;
|
|
1401
|
+
if (Math.abs(dx) > 3 || Math.abs(dy) > 3) {
|
|
1402
|
+
hasMoved = true;
|
|
1403
|
+
el.classList.add("dragging");
|
|
1404
|
+
}
|
|
1405
|
+
if (!hasMoved) return;
|
|
1406
|
+
el.style.left = `${startLeft + dx}px`;
|
|
1407
|
+
el.style.top = `${startTop + dy}px`;
|
|
1408
|
+
el.style.right = "auto";
|
|
1409
|
+
el.style.bottom = "auto";
|
|
1410
|
+
});
|
|
1411
|
+
el.addEventListener("pointerup", (e) => {
|
|
1412
|
+
if (!isDragging) return;
|
|
1413
|
+
isDragging = false;
|
|
1414
|
+
el.classList.remove("dragging");
|
|
1415
|
+
el.releasePointerCapture(e.pointerId);
|
|
1416
|
+
if (hasMoved) {
|
|
1417
|
+
snapToEdge(el);
|
|
1418
|
+
updatePanelPosition(el);
|
|
1419
|
+
saveButtonPosition(el);
|
|
1420
|
+
} else onClickOnly();
|
|
1421
|
+
});
|
|
1422
|
+
el.addEventListener("pointercancel", (e) => {
|
|
1423
|
+
isDragging = false;
|
|
1424
|
+
el.classList.remove("dragging");
|
|
1425
|
+
el.releasePointerCapture(e.pointerId);
|
|
1426
|
+
if (hasMoved) {
|
|
1427
|
+
snapToEdge(el);
|
|
1428
|
+
updatePanelPosition(el);
|
|
1429
|
+
saveButtonPosition(el);
|
|
1430
|
+
}
|
|
1431
|
+
});
|
|
907
1432
|
}
|
|
908
1433
|
function snapToEdge(el) {
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
1434
|
+
const rect = el.getBoundingClientRect();
|
|
1435
|
+
const vw = window.innerWidth;
|
|
1436
|
+
const vh = window.innerHeight;
|
|
1437
|
+
const cx = rect.left + rect.width / 2;
|
|
1438
|
+
const margin = 16;
|
|
1439
|
+
if (cx < vw / 2) {
|
|
1440
|
+
el.style.left = `${margin}px`;
|
|
1441
|
+
el.style.right = "auto";
|
|
1442
|
+
} else {
|
|
1443
|
+
el.style.left = "auto";
|
|
1444
|
+
el.style.right = `${margin}px`;
|
|
1445
|
+
}
|
|
1446
|
+
const top = Math.max(margin, Math.min(vh - rect.height - margin, rect.top));
|
|
1447
|
+
el.style.top = `${top}px`;
|
|
1448
|
+
el.style.bottom = "auto";
|
|
924
1449
|
}
|
|
925
1450
|
function updatePanelPosition(toggleEl) {
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
}
|
|
1451
|
+
if (!panelEl) return;
|
|
1452
|
+
const vw = window.innerWidth;
|
|
1453
|
+
const vh = window.innerHeight;
|
|
1454
|
+
if (vw <= 480) {
|
|
1455
|
+
panelEl.style.top = "";
|
|
1456
|
+
panelEl.style.left = "";
|
|
1457
|
+
panelEl.style.right = "";
|
|
1458
|
+
panelEl.style.bottom = "";
|
|
1459
|
+
return;
|
|
1460
|
+
}
|
|
1461
|
+
const rect = toggleEl.getBoundingClientRect();
|
|
1462
|
+
const panelHeight = 480;
|
|
1463
|
+
const margin = 16;
|
|
1464
|
+
if (rect.left < vw / 2) {
|
|
1465
|
+
panelEl.style.left = `${margin}px`;
|
|
1466
|
+
panelEl.style.right = "auto";
|
|
1467
|
+
} else {
|
|
1468
|
+
panelEl.style.left = "auto";
|
|
1469
|
+
panelEl.style.right = `${margin}px`;
|
|
1470
|
+
}
|
|
1471
|
+
if (rect.top < vh / 2) {
|
|
1472
|
+
const top = Math.min(rect.bottom + 8, vh - panelHeight - margin);
|
|
1473
|
+
panelEl.style.top = `${Math.max(margin, top)}px`;
|
|
1474
|
+
panelEl.style.bottom = "auto";
|
|
1475
|
+
} else {
|
|
1476
|
+
const bottom = Math.min(vh - rect.top + 8, vh - panelHeight - margin);
|
|
1477
|
+
panelEl.style.top = "auto";
|
|
1478
|
+
panelEl.style.bottom = `${Math.max(margin, bottom)}px`;
|
|
1479
|
+
}
|
|
956
1480
|
}
|
|
957
1481
|
function saveButtonPosition(el) {
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
1482
|
+
localStorage.setItem("__ait_btn_pos", JSON.stringify({
|
|
1483
|
+
left: el.style.left,
|
|
1484
|
+
top: el.style.top,
|
|
1485
|
+
right: el.style.right,
|
|
1486
|
+
bottom: el.style.bottom
|
|
1487
|
+
}));
|
|
964
1488
|
}
|
|
965
1489
|
function restoreButtonPosition(el) {
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
}
|
|
1490
|
+
const saved = localStorage.getItem("__ait_btn_pos");
|
|
1491
|
+
if (saved) try {
|
|
1492
|
+
const pos = JSON.parse(saved);
|
|
1493
|
+
if (typeof pos !== "object" || pos === null) return;
|
|
1494
|
+
const allowedKeys = [
|
|
1495
|
+
"left",
|
|
1496
|
+
"top",
|
|
1497
|
+
"right",
|
|
1498
|
+
"bottom"
|
|
1499
|
+
];
|
|
1500
|
+
const validCssValue = /^(\d+px|auto)$/;
|
|
1501
|
+
for (const key of allowedKeys) if (key in pos && typeof pos[key] === "string" && validCssValue.test(pos[key])) el.style[key] = pos[key];
|
|
1502
|
+
} catch {}
|
|
1503
|
+
else {
|
|
1504
|
+
el.style.bottom = "16px";
|
|
1505
|
+
el.style.right = "16px";
|
|
1506
|
+
}
|
|
984
1507
|
}
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
1508
|
+
let currentTab = "env";
|
|
1509
|
+
let isOpen = false;
|
|
1510
|
+
let panelEl = null;
|
|
1511
|
+
let bodyEl = null;
|
|
1512
|
+
let tabsEl = null;
|
|
1513
|
+
let tabRenderers = null;
|
|
989
1514
|
function refreshPanel() {
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
1515
|
+
if (!bodyEl || !tabsEl) return;
|
|
1516
|
+
if (!tabRenderers) tabRenderers = createTabRenderers(refreshPanel);
|
|
1517
|
+
bodyEl.innerHTML = "";
|
|
1518
|
+
try {
|
|
1519
|
+
bodyEl.appendChild(tabRenderers[currentTab]());
|
|
1520
|
+
} catch (err) {
|
|
1521
|
+
console.error(`[@ait-co/devtools] Error rendering tab "${currentTab}":`, err);
|
|
1522
|
+
bodyEl.appendChild(h("div", { className: "ait-panel-tab-error" }, `Error rendering "${currentTab}" tab.`));
|
|
1523
|
+
}
|
|
1524
|
+
tabsEl.querySelectorAll(".ait-panel-tab").forEach((el) => {
|
|
1525
|
+
el.classList.toggle("active", el.getAttribute("data-tab") === currentTab);
|
|
1526
|
+
});
|
|
996
1527
|
}
|
|
1528
|
+
if (typeof window !== "undefined") window.addEventListener("__ait:panel-switch-tab", (e) => {
|
|
1529
|
+
currentTab = e.detail.tab;
|
|
1530
|
+
if (panelEl && !panelEl.classList.contains("open")) {
|
|
1531
|
+
isOpen = true;
|
|
1532
|
+
panelEl.classList.add("open");
|
|
1533
|
+
}
|
|
1534
|
+
refreshPanel();
|
|
1535
|
+
});
|
|
997
1536
|
function mount() {
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1537
|
+
if (typeof document === "undefined") return;
|
|
1538
|
+
if (document.querySelector(".ait-panel-toggle")) return;
|
|
1539
|
+
setDeviceRefreshPanel(refreshPanel);
|
|
1540
|
+
const style = document.createElement("style");
|
|
1541
|
+
style.textContent = PANEL_STYLES;
|
|
1542
|
+
document.head.appendChild(style);
|
|
1543
|
+
const toggle = h("button", {
|
|
1544
|
+
className: "ait-panel-toggle",
|
|
1545
|
+
title: "AIT DevTools"
|
|
1546
|
+
}, "AIT");
|
|
1547
|
+
restoreButtonPosition(toggle);
|
|
1548
|
+
panelEl = h("div", { className: "ait-panel" });
|
|
1549
|
+
const closeBtn = h("button", {
|
|
1550
|
+
className: "ait-panel-close",
|
|
1551
|
+
title: "Close"
|
|
1552
|
+
}, "×");
|
|
1553
|
+
closeBtn.addEventListener("click", () => {
|
|
1554
|
+
isOpen = false;
|
|
1555
|
+
panelEl.classList.remove("open");
|
|
1556
|
+
});
|
|
1557
|
+
const mockBadge = h("span", {
|
|
1558
|
+
className: `ait-mock-badge ${aitState.state.panelEditable ? "ait-mock-badge-on" : "ait-mock-badge-off"}`,
|
|
1559
|
+
title: "Toggle panel edit mode"
|
|
1560
|
+
}, aitState.state.panelEditable ? "EDIT" : "READ-ONLY");
|
|
1561
|
+
mockBadge.addEventListener("click", () => {
|
|
1562
|
+
aitState.update({ panelEditable: !aitState.state.panelEditable });
|
|
1563
|
+
mockBadge.className = `ait-mock-badge ${aitState.state.panelEditable ? "ait-mock-badge-on" : "ait-mock-badge-off"}`;
|
|
1564
|
+
mockBadge.textContent = aitState.state.panelEditable ? "EDIT" : "READ-ONLY";
|
|
1565
|
+
refreshPanel();
|
|
1566
|
+
});
|
|
1567
|
+
const headerRight = h("span", { style: "display:flex;align-items:center;gap:6px" }, mockBadge, h("span", { style: "font-size:11px;color:#666;font-weight:400" }, `v0.1.0`), closeBtn);
|
|
1568
|
+
const header = h("div", { className: "ait-panel-header" }, h("span", {}, "AIT DevTools"), headerRight);
|
|
1569
|
+
tabsEl = h("div", { className: "ait-panel-tabs" });
|
|
1570
|
+
for (const tab of TABS) {
|
|
1571
|
+
const tabEl = h("button", {
|
|
1572
|
+
className: "ait-panel-tab",
|
|
1573
|
+
"data-tab": tab.id
|
|
1574
|
+
}, tab.label);
|
|
1575
|
+
tabEl.addEventListener("click", () => {
|
|
1576
|
+
currentTab = tab.id;
|
|
1577
|
+
refreshPanel();
|
|
1578
|
+
});
|
|
1579
|
+
tabsEl.appendChild(tabEl);
|
|
1580
|
+
}
|
|
1581
|
+
bodyEl = h("div", { className: "ait-panel-body" });
|
|
1582
|
+
panelEl.append(header, tabsEl, bodyEl);
|
|
1583
|
+
document.body.append(panelEl, toggle);
|
|
1584
|
+
snapToEdge(toggle);
|
|
1585
|
+
saveButtonPosition(toggle);
|
|
1586
|
+
makeDraggable(toggle, () => {
|
|
1587
|
+
isOpen = !isOpen;
|
|
1588
|
+
panelEl.classList.toggle("open", isOpen);
|
|
1589
|
+
if (isOpen) {
|
|
1590
|
+
updatePanelPosition(toggle);
|
|
1591
|
+
refreshPanel();
|
|
1592
|
+
}
|
|
1593
|
+
});
|
|
1594
|
+
let resizeRaf = 0;
|
|
1595
|
+
window.addEventListener("resize", () => {
|
|
1596
|
+
if (resizeRaf) return;
|
|
1597
|
+
resizeRaf = requestAnimationFrame(() => {
|
|
1598
|
+
resizeRaf = 0;
|
|
1599
|
+
snapToEdge(toggle);
|
|
1600
|
+
saveButtonPosition(toggle);
|
|
1601
|
+
if (isOpen) updatePanelPosition(toggle);
|
|
1602
|
+
});
|
|
1603
|
+
});
|
|
1604
|
+
aitState.subscribe(() => {
|
|
1605
|
+
try {
|
|
1606
|
+
if (isOpen && (currentTab === "analytics" || currentTab === "storage" || currentTab === "device")) refreshPanel();
|
|
1607
|
+
} catch (err) {
|
|
1608
|
+
console.error("[@ait-co/devtools] Error in subscribe callback:", err);
|
|
1609
|
+
}
|
|
1610
|
+
});
|
|
1611
|
+
refreshPanel();
|
|
1073
1612
|
}
|
|
1074
1613
|
if (typeof document !== "undefined") {
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1614
|
+
const safeMount = () => {
|
|
1615
|
+
try {
|
|
1616
|
+
mount();
|
|
1617
|
+
} catch (err) {
|
|
1618
|
+
console.error("[@ait-co/devtools] Failed to mount panel:", err);
|
|
1619
|
+
}
|
|
1620
|
+
};
|
|
1621
|
+
if (document.readyState === "loading") document.addEventListener("DOMContentLoaded", safeMount);
|
|
1622
|
+
else safeMount();
|
|
1080
1623
|
}
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1624
|
+
//#endregion
|
|
1625
|
+
export { mount };
|
|
1626
|
+
|
|
1084
1627
|
//# sourceMappingURL=index.js.map
|