@ait-co/devtools 0.0.3 → 0.1.1
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/mock/index.d.ts +464 -464
- package/dist/mock/index.d.ts.map +1 -1
- package/dist/mock/index.js +452 -451
- package/dist/mock/index.js.map +1 -1
- package/dist/panel/index.d.ts.map +1 -1
- package/dist/panel/index.js +325 -324
- package/dist/panel/index.js.map +1 -1
- package/dist/unplugin/index.cjs +18 -1
- package/dist/unplugin/index.cjs.map +1 -1
- package/dist/unplugin/index.d.cts +9945 -1211
- package/dist/unplugin/index.d.cts.map +1 -1
- package/dist/unplugin/index.d.ts +9945 -1211
- package/dist/unplugin/index.d.ts.map +1 -1
- package/dist/unplugin/index.js +18 -1
- package/dist/unplugin/index.js.map +1 -1
- package/package.json +8 -2
package/dist/panel/index.js
CHANGED
|
@@ -109,7 +109,7 @@ var AitStateManager = class {
|
|
|
109
109
|
try {
|
|
110
110
|
this._state.deviceId = generateDeviceId();
|
|
111
111
|
} catch {
|
|
112
|
-
this._state.deviceId =
|
|
112
|
+
this._state.deviceId = `mock-device-${Math.random().toString(36).slice(2)}`;
|
|
113
113
|
}
|
|
114
114
|
}
|
|
115
115
|
get state() {
|
|
@@ -171,6 +171,41 @@ var AitStateManager = class {
|
|
|
171
171
|
};
|
|
172
172
|
const aitState = new AitStateManager();
|
|
173
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
|
+
}
|
|
174
209
|
const PANEL_STYLES = `
|
|
175
210
|
.ait-panel-toggle {
|
|
176
211
|
position: fixed;
|
|
@@ -546,113 +581,6 @@ const PANEL_STYLES = `
|
|
|
546
581
|
}
|
|
547
582
|
`;
|
|
548
583
|
//#endregion
|
|
549
|
-
//#region src/panel/helpers.ts
|
|
550
|
-
/**
|
|
551
|
-
* 공통 DOM 헬퍼 함수
|
|
552
|
-
*/
|
|
553
|
-
function h(tag, attrs, ...children) {
|
|
554
|
-
const el = document.createElement(tag);
|
|
555
|
-
if (attrs) for (const [k, v] of Object.entries(attrs)) if (k === "className") el.className = v;
|
|
556
|
-
else el.setAttribute(k, v);
|
|
557
|
-
for (const child of children) el.append(typeof child === "string" ? document.createTextNode(child) : child);
|
|
558
|
-
return el;
|
|
559
|
-
}
|
|
560
|
-
function selectRow(label, options, value, onChange, disabled = false) {
|
|
561
|
-
const select = h("select", { className: "ait-select" });
|
|
562
|
-
if (disabled) select.disabled = true;
|
|
563
|
-
for (const opt of options) {
|
|
564
|
-
const option = h("option", { value: opt }, opt);
|
|
565
|
-
if (opt === value) option.selected = true;
|
|
566
|
-
select.appendChild(option);
|
|
567
|
-
}
|
|
568
|
-
select.addEventListener("change", () => onChange(select.value));
|
|
569
|
-
return h("div", { className: "ait-row" }, h("label", {}, label), select);
|
|
570
|
-
}
|
|
571
|
-
function inputRow(label, value, onChange, disabled = false) {
|
|
572
|
-
const input = h("input", {
|
|
573
|
-
className: "ait-input",
|
|
574
|
-
value
|
|
575
|
-
});
|
|
576
|
-
if (disabled) input.disabled = true;
|
|
577
|
-
input.addEventListener("change", () => onChange(input.value));
|
|
578
|
-
return h("div", { className: "ait-row" }, h("label", {}, label), input);
|
|
579
|
-
}
|
|
580
|
-
function monitoringNotice() {
|
|
581
|
-
return h("div", { className: "ait-monitoring-notice" }, "Read-only — mock responses are controlled at build time.");
|
|
582
|
-
}
|
|
583
|
-
//#endregion
|
|
584
|
-
//#region src/panel/tabs/environment.ts
|
|
585
|
-
function renderEnvironmentTab() {
|
|
586
|
-
const s = aitState.state;
|
|
587
|
-
const disabled = !s.panelEditable;
|
|
588
|
-
const container = h("div");
|
|
589
|
-
if (disabled) container.appendChild(monitoringNotice());
|
|
590
|
-
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", [
|
|
591
|
-
"WIFI",
|
|
592
|
-
"4G",
|
|
593
|
-
"5G",
|
|
594
|
-
"3G",
|
|
595
|
-
"2G",
|
|
596
|
-
"OFFLINE",
|
|
597
|
-
"WWAN",
|
|
598
|
-
"UNKNOWN"
|
|
599
|
-
], 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)));
|
|
600
|
-
return container;
|
|
601
|
-
}
|
|
602
|
-
//#endregion
|
|
603
|
-
//#region src/panel/tabs/permissions.ts
|
|
604
|
-
function renderPermissionsTab() {
|
|
605
|
-
const s = aitState.state;
|
|
606
|
-
const disabled = !s.panelEditable;
|
|
607
|
-
const container = h("div");
|
|
608
|
-
const names = [
|
|
609
|
-
"camera",
|
|
610
|
-
"photos",
|
|
611
|
-
"geolocation",
|
|
612
|
-
"clipboard",
|
|
613
|
-
"contacts",
|
|
614
|
-
"microphone"
|
|
615
|
-
];
|
|
616
|
-
const statuses = [
|
|
617
|
-
"allowed",
|
|
618
|
-
"denied",
|
|
619
|
-
"notDetermined"
|
|
620
|
-
];
|
|
621
|
-
if (disabled) container.appendChild(monitoringNotice());
|
|
622
|
-
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) => {
|
|
623
|
-
aitState.patch("permissions", { [name]: v });
|
|
624
|
-
}, disabled))));
|
|
625
|
-
return container;
|
|
626
|
-
}
|
|
627
|
-
//#endregion
|
|
628
|
-
//#region src/panel/tabs/location.ts
|
|
629
|
-
function renderLocationTab() {
|
|
630
|
-
const s = aitState.state;
|
|
631
|
-
const disabled = !s.panelEditable;
|
|
632
|
-
const container = h("div");
|
|
633
|
-
if (disabled) container.appendChild(monitoringNotice());
|
|
634
|
-
container.append(h("div", { className: "ait-section" }, h("div", { className: "ait-section-title" }, "Current Location"), inputRow("Latitude", String(s.location.coords.latitude), (v) => {
|
|
635
|
-
const coords = {
|
|
636
|
-
...s.location.coords,
|
|
637
|
-
latitude: Number(v)
|
|
638
|
-
};
|
|
639
|
-
aitState.patch("location", { coords });
|
|
640
|
-
}, disabled), inputRow("Longitude", String(s.location.coords.longitude), (v) => {
|
|
641
|
-
const coords = {
|
|
642
|
-
...s.location.coords,
|
|
643
|
-
longitude: Number(v)
|
|
644
|
-
};
|
|
645
|
-
aitState.patch("location", { coords });
|
|
646
|
-
}, disabled), inputRow("Accuracy", String(s.location.coords.accuracy), (v) => {
|
|
647
|
-
const coords = {
|
|
648
|
-
...s.location.coords,
|
|
649
|
-
accuracy: Number(v)
|
|
650
|
-
};
|
|
651
|
-
aitState.patch("location", { coords });
|
|
652
|
-
}, disabled)));
|
|
653
|
-
return container;
|
|
654
|
-
}
|
|
655
|
-
//#endregion
|
|
656
584
|
//#region src/mock/device/_helpers.ts
|
|
657
585
|
/**
|
|
658
586
|
* 디바이스 모듈 내부 공유 헬퍼
|
|
@@ -664,7 +592,7 @@ function generatePlaceholderImage(width, height, text, color) {
|
|
|
664
592
|
const ctx = canvas.getContext("2d");
|
|
665
593
|
if (!ctx) {
|
|
666
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>`;
|
|
667
|
-
return
|
|
595
|
+
return `data:image/svg+xml;base64,${btoa(svg)}`;
|
|
668
596
|
}
|
|
669
597
|
ctx.fillStyle = color;
|
|
670
598
|
ctx.fillRect(0, 0, width, height);
|
|
@@ -704,7 +632,7 @@ const PROMPT_TIMEOUT_MS = 3e4;
|
|
|
704
632
|
/** @internal device 모듈 내부 전용 */
|
|
705
633
|
function waitForPromptResponse(type) {
|
|
706
634
|
return new Promise((resolve, reject) => {
|
|
707
|
-
const eventName =
|
|
635
|
+
const eventName = `__ait:prompt-response:${type}`;
|
|
708
636
|
const cancelName = "__ait:prompt-cancel";
|
|
709
637
|
function cleanup() {
|
|
710
638
|
clearTimeout(timer);
|
|
@@ -730,39 +658,6 @@ function waitForPromptResponse(type) {
|
|
|
730
658
|
});
|
|
731
659
|
}
|
|
732
660
|
//#endregion
|
|
733
|
-
//#region src/mock/proxy.ts
|
|
734
|
-
/**
|
|
735
|
-
* 미구현 API용 Proxy 트립와이어.
|
|
736
|
-
*
|
|
737
|
-
* 미구현 프로퍼티에 접근하면 throw한다. 이는 "devtools에서는 멀쩡히 돌지만
|
|
738
|
-
* 실 SDK에선 실제로 동작하는" 시나리오를 차단하기 위한 의도적 선택이다.
|
|
739
|
-
* mock이 미구현인 API는 실 SDK에서는 존재할 수 있고, 사용자가 이를 인지하지
|
|
740
|
-
* 못한 채 개발을 이어가면 배포 시점에 놀라게 된다. 에러 메시지에 이슈 URL을
|
|
741
|
-
* 포함해 사용자가 mock 누락을 제보할 수 있게 한다.
|
|
742
|
-
*/
|
|
743
|
-
const ISSUES_URL = "https://github.com/apps-in-toss-community/devtools/issues";
|
|
744
|
-
function createMockProxy(moduleName, implementations) {
|
|
745
|
-
return new Proxy(implementations, { get(target, prop) {
|
|
746
|
-
if (typeof prop === "symbol") return void 0;
|
|
747
|
-
if (prop in target) return target[prop];
|
|
748
|
-
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}`);
|
|
749
|
-
} });
|
|
750
|
-
}
|
|
751
|
-
createMockProxy("Storage", {
|
|
752
|
-
getItem: async (key) => {
|
|
753
|
-
return localStorage.getItem(`__ait_storage:${key}`);
|
|
754
|
-
},
|
|
755
|
-
setItem: async (key, value) => {
|
|
756
|
-
localStorage.setItem(`__ait_storage:${key}`, value);
|
|
757
|
-
},
|
|
758
|
-
removeItem: async (key) => {
|
|
759
|
-
localStorage.removeItem(`__ait_storage:${key}`);
|
|
760
|
-
},
|
|
761
|
-
clearItems: async () => {
|
|
762
|
-
Object.keys(localStorage).filter((k) => k.startsWith("__ait_storage:")).forEach((k) => localStorage.removeItem(k));
|
|
763
|
-
}
|
|
764
|
-
});
|
|
765
|
-
//#endregion
|
|
766
661
|
//#region src/mock/permissions.ts
|
|
767
662
|
/**
|
|
768
663
|
* 권한 시스템 mock
|
|
@@ -788,158 +683,56 @@ function checkPermission(name, fnName) {
|
|
|
788
683
|
if (aitState.state.permissions[name] === "denied") throw new Error(`[@ait-co/devtools] ${fnName}: Permission "${name}" is denied. Change it in the DevTools panel.`);
|
|
789
684
|
}
|
|
790
685
|
//#endregion
|
|
791
|
-
//#region src/mock/device/
|
|
686
|
+
//#region src/mock/device/camera.ts
|
|
792
687
|
/**
|
|
793
|
-
*
|
|
688
|
+
* Camera & Album Photos mock
|
|
794
689
|
* mock/web/prompt 모드 지원
|
|
795
690
|
*/
|
|
796
|
-
function
|
|
691
|
+
async function openCameraMock() {
|
|
692
|
+
const images = getMockImages();
|
|
797
693
|
return {
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
accessLocation: aitState.state.location.accessLocation
|
|
694
|
+
id: crypto.randomUUID(),
|
|
695
|
+
dataUri: images[0]
|
|
801
696
|
};
|
|
802
697
|
}
|
|
803
|
-
async function
|
|
804
|
-
return
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
heading: pos.coords.heading ?? 0
|
|
822
|
-
},
|
|
823
|
-
timestamp: pos.timestamp,
|
|
824
|
-
accessLocation: "FINE"
|
|
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
|
|
825
716
|
});
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
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();
|
|
830
728
|
});
|
|
831
729
|
}
|
|
832
|
-
async function
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
if (mode === "web") return getCurrentLocationWeb();
|
|
839
|
-
if (mode === "prompt") return getCurrentLocationPrompt();
|
|
840
|
-
return getCurrentLocationMock();
|
|
841
|
-
};
|
|
842
|
-
withPermission(_getCurrentLocation, "geolocation");
|
|
843
|
-
function startUpdateLocationMock(eventParams) {
|
|
844
|
-
const { onEvent, options } = eventParams;
|
|
845
|
-
const interval = Math.max(options.timeInterval, 500);
|
|
846
|
-
const id = setInterval(() => {
|
|
847
|
-
const loc = buildLocation();
|
|
848
|
-
loc.coords.latitude += (Math.random() - .5) * 1e-4;
|
|
849
|
-
loc.coords.longitude += (Math.random() - .5) * 1e-4;
|
|
850
|
-
onEvent(loc);
|
|
851
|
-
}, interval);
|
|
852
|
-
return () => clearInterval(id);
|
|
853
|
-
}
|
|
854
|
-
function startUpdateLocationWeb(eventParams) {
|
|
855
|
-
const { onEvent, onError } = eventParams;
|
|
856
|
-
if (!navigator.geolocation) {
|
|
857
|
-
console.warn("[@ait-co/devtools] Geolocation API not available, falling back to mock");
|
|
858
|
-
return startUpdateLocationMock(eventParams);
|
|
859
|
-
}
|
|
860
|
-
const watchId = navigator.geolocation.watchPosition((pos) => {
|
|
861
|
-
onEvent({
|
|
862
|
-
coords: {
|
|
863
|
-
latitude: pos.coords.latitude,
|
|
864
|
-
longitude: pos.coords.longitude,
|
|
865
|
-
altitude: pos.coords.altitude ?? 0,
|
|
866
|
-
accuracy: pos.coords.accuracy,
|
|
867
|
-
altitudeAccuracy: pos.coords.altitudeAccuracy ?? 0,
|
|
868
|
-
heading: pos.coords.heading ?? 0
|
|
869
|
-
},
|
|
870
|
-
timestamp: pos.timestamp,
|
|
871
|
-
accessLocation: "FINE"
|
|
872
|
-
});
|
|
873
|
-
}, (err) => onError(err));
|
|
874
|
-
return () => navigator.geolocation.clearWatch(watchId);
|
|
875
|
-
}
|
|
876
|
-
function startUpdateLocationPrompt(eventParams) {
|
|
877
|
-
const { onEvent } = eventParams;
|
|
878
|
-
const handler = (e) => {
|
|
879
|
-
onEvent(e.detail);
|
|
880
|
-
};
|
|
881
|
-
window.addEventListener("__ait:prompt-response:location-update", handler);
|
|
882
|
-
window.dispatchEvent(new CustomEvent("__ait:prompt-request", { detail: { type: "location-update" } }));
|
|
883
|
-
return () => window.removeEventListener("__ait:prompt-response:location-update", handler);
|
|
884
|
-
}
|
|
885
|
-
const _startUpdateLocation = (eventParams) => {
|
|
886
|
-
const mode = aitState.state.deviceModes.location;
|
|
887
|
-
if (mode === "web") return startUpdateLocationWeb(eventParams);
|
|
888
|
-
if (mode === "prompt") return startUpdateLocationPrompt(eventParams);
|
|
889
|
-
return startUpdateLocationMock(eventParams);
|
|
890
|
-
};
|
|
891
|
-
withPermission(_startUpdateLocation, "geolocation");
|
|
892
|
-
//#endregion
|
|
893
|
-
//#region src/mock/device/camera.ts
|
|
894
|
-
/**
|
|
895
|
-
* Camera & Album Photos mock
|
|
896
|
-
* mock/web/prompt 모드 지원
|
|
897
|
-
*/
|
|
898
|
-
async function openCameraMock() {
|
|
899
|
-
const images = getMockImages();
|
|
900
|
-
return {
|
|
901
|
-
id: crypto.randomUUID(),
|
|
902
|
-
dataUri: images[0]
|
|
903
|
-
};
|
|
904
|
-
}
|
|
905
|
-
async function openCameraWeb() {
|
|
906
|
-
return new Promise((resolve, reject) => {
|
|
907
|
-
const input = document.createElement("input");
|
|
908
|
-
input.type = "file";
|
|
909
|
-
input.accept = "image/*";
|
|
910
|
-
input.capture = "environment";
|
|
911
|
-
let settled = false;
|
|
912
|
-
input.onchange = () => {
|
|
913
|
-
settled = true;
|
|
914
|
-
const file = input.files?.[0];
|
|
915
|
-
if (!file) {
|
|
916
|
-
reject(/* @__PURE__ */ new Error("No file selected"));
|
|
917
|
-
return;
|
|
918
|
-
}
|
|
919
|
-
const reader = new FileReader();
|
|
920
|
-
reader.onload = () => resolve({
|
|
921
|
-
id: crypto.randomUUID(),
|
|
922
|
-
dataUri: reader.result
|
|
923
|
-
});
|
|
924
|
-
reader.onerror = () => reject(/* @__PURE__ */ new Error("Failed to read file"));
|
|
925
|
-
reader.readAsDataURL(file);
|
|
926
|
-
};
|
|
927
|
-
const onFocus = () => {
|
|
928
|
-
setTimeout(() => {
|
|
929
|
-
if (!settled) reject(/* @__PURE__ */ new Error("File picker cancelled"));
|
|
930
|
-
window.removeEventListener("focus", onFocus);
|
|
931
|
-
}, 300);
|
|
932
|
-
};
|
|
933
|
-
window.addEventListener("focus", onFocus);
|
|
934
|
-
input.click();
|
|
935
|
-
});
|
|
936
|
-
}
|
|
937
|
-
async function openCameraPrompt() {
|
|
938
|
-
const dataUri = await waitForPromptResponse("camera");
|
|
939
|
-
return {
|
|
940
|
-
id: crypto.randomUUID(),
|
|
941
|
-
dataUri
|
|
942
|
-
};
|
|
730
|
+
async function openCameraPrompt() {
|
|
731
|
+
const dataUri = await waitForPromptResponse("camera");
|
|
732
|
+
return {
|
|
733
|
+
id: crypto.randomUUID(),
|
|
734
|
+
dataUri
|
|
735
|
+
};
|
|
943
736
|
}
|
|
944
737
|
const _openCamera = async (_options) => {
|
|
945
738
|
checkPermission("camera", "openCamera");
|
|
@@ -1051,6 +844,142 @@ const _fetchContacts = async (options) => {
|
|
|
1051
844
|
};
|
|
1052
845
|
withPermission(_fetchContacts, "contacts");
|
|
1053
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
|
+
});
|
|
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
|
|
1054
983
|
//#region src/panel/tabs/device.ts
|
|
1055
984
|
let pendingPrompt = null;
|
|
1056
985
|
let refreshPanel$1 = () => {};
|
|
@@ -1062,7 +991,7 @@ if (typeof window !== "undefined") window.addEventListener("__ait:prompt-request
|
|
|
1062
991
|
window.dispatchEvent(new CustomEvent("__ait:panel-switch-tab", { detail: { tab: "device" } }));
|
|
1063
992
|
});
|
|
1064
993
|
function resolvePrompt(type, data) {
|
|
1065
|
-
window.dispatchEvent(new CustomEvent(
|
|
994
|
+
window.dispatchEvent(new CustomEvent(`__ait:prompt-response:${type}`, { detail: data }));
|
|
1066
995
|
pendingPrompt = null;
|
|
1067
996
|
refreshPanel$1();
|
|
1068
997
|
}
|
|
@@ -1242,6 +1171,61 @@ function renderDeviceTab() {
|
|
|
1242
1171
|
return container;
|
|
1243
1172
|
}
|
|
1244
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;
|
|
1189
|
+
}
|
|
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
|
|
1245
1229
|
//#region src/panel/tabs/iap.ts
|
|
1246
1230
|
function renderIapTab() {
|
|
1247
1231
|
const s = aitState.state;
|
|
@@ -1265,39 +1249,56 @@ function renderIapTab() {
|
|
|
1265
1249
|
return container;
|
|
1266
1250
|
}
|
|
1267
1251
|
//#endregion
|
|
1268
|
-
//#region src/panel/tabs/
|
|
1269
|
-
function
|
|
1270
|
-
const
|
|
1252
|
+
//#region src/panel/tabs/location.ts
|
|
1253
|
+
function renderLocationTab() {
|
|
1254
|
+
const s = aitState.state;
|
|
1255
|
+
const disabled = !s.panelEditable;
|
|
1271
1256
|
const container = h("div");
|
|
1272
1257
|
if (disabled) container.appendChild(monitoringNotice());
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
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 });
|
|
1283
1276
|
}, disabled)));
|
|
1284
1277
|
return container;
|
|
1285
1278
|
}
|
|
1286
1279
|
//#endregion
|
|
1287
|
-
//#region src/panel/tabs/
|
|
1288
|
-
function
|
|
1289
|
-
const
|
|
1280
|
+
//#region src/panel/tabs/permissions.ts
|
|
1281
|
+
function renderPermissionsTab() {
|
|
1282
|
+
const s = aitState.state;
|
|
1283
|
+
const disabled = !s.panelEditable;
|
|
1290
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
|
+
];
|
|
1291
1298
|
if (disabled) container.appendChild(monitoringNotice());
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
clearBtn.addEventListener("click", () => {
|
|
1296
|
-
aitState.update({ analyticsLog: [] });
|
|
1297
|
-
});
|
|
1298
|
-
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) => {
|
|
1299
|
-
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));
|
|
1300
|
-
})));
|
|
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))));
|
|
1301
1302
|
return container;
|
|
1302
1303
|
}
|
|
1303
1304
|
//#endregion
|
|
@@ -1315,10 +1316,10 @@ function renderStorageTab(refreshPanel) {
|
|
|
1315
1316
|
const clearBtn = h("button", { className: "ait-btn ait-btn-sm ait-btn-danger" }, "Clear All");
|
|
1316
1317
|
if (disabled) clearBtn.disabled = true;
|
|
1317
1318
|
clearBtn.addEventListener("click", () => {
|
|
1318
|
-
|
|
1319
|
+
for (const [key] of entries) localStorage.removeItem(prefix + key);
|
|
1319
1320
|
refreshPanel();
|
|
1320
1321
|
});
|
|
1321
|
-
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)
|
|
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))))));
|
|
1322
1323
|
return container;
|
|
1323
1324
|
}
|
|
1324
1325
|
//#endregion
|
|
@@ -1402,8 +1403,8 @@ function makeDraggable(el, onClickOnly) {
|
|
|
1402
1403
|
el.classList.add("dragging");
|
|
1403
1404
|
}
|
|
1404
1405
|
if (!hasMoved) return;
|
|
1405
|
-
el.style.left = startLeft + dx
|
|
1406
|
-
el.style.top = startTop + dy
|
|
1406
|
+
el.style.left = `${startLeft + dx}px`;
|
|
1407
|
+
el.style.top = `${startTop + dy}px`;
|
|
1407
1408
|
el.style.right = "auto";
|
|
1408
1409
|
el.style.bottom = "auto";
|
|
1409
1410
|
});
|
|
@@ -1436,14 +1437,14 @@ function snapToEdge(el) {
|
|
|
1436
1437
|
const cx = rect.left + rect.width / 2;
|
|
1437
1438
|
const margin = 16;
|
|
1438
1439
|
if (cx < vw / 2) {
|
|
1439
|
-
el.style.left = margin
|
|
1440
|
+
el.style.left = `${margin}px`;
|
|
1440
1441
|
el.style.right = "auto";
|
|
1441
1442
|
} else {
|
|
1442
1443
|
el.style.left = "auto";
|
|
1443
|
-
el.style.right = margin
|
|
1444
|
+
el.style.right = `${margin}px`;
|
|
1444
1445
|
}
|
|
1445
1446
|
const top = Math.max(margin, Math.min(vh - rect.height - margin, rect.top));
|
|
1446
|
-
el.style.top = top
|
|
1447
|
+
el.style.top = `${top}px`;
|
|
1447
1448
|
el.style.bottom = "auto";
|
|
1448
1449
|
}
|
|
1449
1450
|
function updatePanelPosition(toggleEl) {
|
|
@@ -1461,20 +1462,20 @@ function updatePanelPosition(toggleEl) {
|
|
|
1461
1462
|
const panelHeight = 480;
|
|
1462
1463
|
const margin = 16;
|
|
1463
1464
|
if (rect.left < vw / 2) {
|
|
1464
|
-
panelEl.style.left = margin
|
|
1465
|
+
panelEl.style.left = `${margin}px`;
|
|
1465
1466
|
panelEl.style.right = "auto";
|
|
1466
1467
|
} else {
|
|
1467
1468
|
panelEl.style.left = "auto";
|
|
1468
|
-
panelEl.style.right = margin
|
|
1469
|
+
panelEl.style.right = `${margin}px`;
|
|
1469
1470
|
}
|
|
1470
1471
|
if (rect.top < vh / 2) {
|
|
1471
1472
|
const top = Math.min(rect.bottom + 8, vh - panelHeight - margin);
|
|
1472
|
-
panelEl.style.top = Math.max(margin, top)
|
|
1473
|
+
panelEl.style.top = `${Math.max(margin, top)}px`;
|
|
1473
1474
|
panelEl.style.bottom = "auto";
|
|
1474
1475
|
} else {
|
|
1475
1476
|
const bottom = Math.min(vh - rect.top + 8, vh - panelHeight - margin);
|
|
1476
1477
|
panelEl.style.top = "auto";
|
|
1477
|
-
panelEl.style.bottom = Math.max(margin, bottom)
|
|
1478
|
+
panelEl.style.bottom = `${Math.max(margin, bottom)}px`;
|
|
1478
1479
|
}
|
|
1479
1480
|
}
|
|
1480
1481
|
function saveButtonPosition(el) {
|
|
@@ -1563,7 +1564,7 @@ function mount() {
|
|
|
1563
1564
|
mockBadge.textContent = aitState.state.panelEditable ? "EDIT" : "READ-ONLY";
|
|
1564
1565
|
refreshPanel();
|
|
1565
1566
|
});
|
|
1566
|
-
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.
|
|
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.1`), closeBtn);
|
|
1567
1568
|
const header = h("div", { className: "ait-panel-header" }, h("span", {}, "AIT DevTools"), headerRight);
|
|
1568
1569
|
tabsEl = h("div", { className: "ait-panel-tabs" });
|
|
1569
1570
|
for (const tab of TABS) {
|