@ait-co/devtools 0.0.3 → 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.
@@ -109,7 +109,7 @@ var AitStateManager = class {
109
109
  try {
110
110
  this._state.deviceId = generateDeviceId();
111
111
  } catch {
112
- this._state.deviceId = "mock-device-" + Math.random().toString(36).slice(2);
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 "data:image/svg+xml;base64," + btoa(svg);
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 = "__ait:prompt-response:" + type;
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/location.ts
686
+ //#region src/mock/device/camera.ts
792
687
  /**
793
- * Location mock (getCurrentLocation, startUpdateLocation)
688
+ * Camera & Album Photos mock
794
689
  * mock/web/prompt 모드 지원
795
690
  */
796
- function buildLocation() {
691
+ async function openCameraMock() {
692
+ const images = getMockImages();
797
693
  return {
798
- coords: { ...aitState.state.location.coords },
799
- timestamp: Date.now(),
800
- accessLocation: aitState.state.location.accessLocation
694
+ id: crypto.randomUUID(),
695
+ dataUri: images[0]
801
696
  };
802
697
  }
803
- async function getCurrentLocationMock() {
804
- return buildLocation();
805
- }
806
- async function getCurrentLocationWeb() {
807
- return new Promise((resolve) => {
808
- if (!navigator.geolocation) {
809
- console.warn("[@ait-co/devtools] Geolocation API not available, falling back to mock");
810
- resolve(buildLocation());
811
- return;
812
- }
813
- navigator.geolocation.getCurrentPosition((pos) => {
814
- resolve({
815
- coords: {
816
- latitude: pos.coords.latitude,
817
- longitude: pos.coords.longitude,
818
- altitude: pos.coords.altitude ?? 0,
819
- accuracy: pos.coords.accuracy,
820
- altitudeAccuracy: pos.coords.altitudeAccuracy ?? 0,
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
- console.warn("[@ait-co/devtools] Geolocation failed, falling back to mock");
828
- resolve(buildLocation());
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 getCurrentLocationPrompt() {
833
- return waitForPromptResponse("location");
834
- }
835
- const _getCurrentLocation = async (_options) => {
836
- checkPermission("geolocation", "getCurrentLocation");
837
- const mode = aitState.state.deviceModes.location;
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("__ait:prompt-response:" + type, { detail: data }));
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/events.ts
1269
- function renderEventsTab() {
1270
- const disabled = !aitState.state.panelEditable;
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
- const backBtn = h("button", { className: "ait-btn" }, "Trigger Back Event");
1274
- backBtn.addEventListener("click", () => aitState.trigger("backEvent"));
1275
- if (disabled) backBtn.disabled = true;
1276
- const homeBtn = h("button", { className: "ait-btn" }, "Trigger Home Event");
1277
- homeBtn.addEventListener("click", () => aitState.trigger("homeEvent"));
1278
- if (disabled) homeBtn.disabled = true;
1279
- 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) => {
1280
- aitState.patch("auth", { isLoggedIn: v === "true" });
1281
- }, disabled), selectRow("Toss Login Integrated", ["true", "false"], String(aitState.state.auth.isTossLoginIntegrated), (v) => {
1282
- aitState.patch("auth", { isTossLoginIntegrated: v === "true" });
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/analytics.ts
1288
- function renderAnalyticsTab() {
1289
- const disabled = !aitState.state.panelEditable;
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
- const logs = aitState.state.analyticsLog;
1293
- const clearBtn = h("button", { className: "ait-btn ait-btn-sm ait-btn-danger" }, "Clear");
1294
- if (disabled) clearBtn.disabled = true;
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
- entries.forEach(([key]) => localStorage.removeItem(prefix + key));
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) + "..." : value))))));
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 + "px";
1406
- el.style.top = startTop + dy + "px";
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 + "px";
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 + "px";
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 + "px";
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 + "px";
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 + "px";
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) + "px";
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) + "px";
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.0.3`), closeBtn);
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);
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) {