@ait-co/devtools 0.1.9 → 0.1.11

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 CHANGED
@@ -1,5 +1,7 @@
1
1
  # @ait-co/devtools
2
2
 
3
+ ![@ait-co/devtools — SDK mock + DevTools panel for Apps In Toss mini-apps](./assets/og/image.png)
4
+
3
5
  `@apps-in-toss/web-framework` SDK의 mock 라이브러리입니다. `@apps-in-toss/web-bridge`, `@apps-in-toss/web-analytics` import도 함께 mock됩니다.
4
6
 
5
7
  앱인토스(Apps in Toss) 미니앱을 **일반 브라우저**에서 개발하고 테스트할 수 있게 해줍니다. 토스 앱 없이도 SDK의 모든 기능을 시뮬레이션하여 빠른 개발 사이클을 지원합니다.
@@ -10,6 +12,8 @@
10
12
  - **Floating DevTools Panel** — 브라우저에서 SDK 상태를 실시간으로 제어 (10개 탭, mock state preset library 포함)
11
13
  - **모든 번들러 지원** — [unplugin](https://github.com/unjs/unplugin) 기반 Vite, Webpack, Rspack, esbuild, Rollup 통합
12
14
 
15
+ 라이브 데모: <https://devtools.aitc.dev/> (이 repo의 `e2e/fixture/`를 GitHub Pages에 그대로 배포한 self-contained 데모).
16
+
13
17
  ## Reference consumer
14
18
 
15
19
  [`sdk-example`](https://github.com/apps-in-toss-community/sdk-example)이 devtools의 reference consumer다. 모든 SDK API를 인터랙티브하게 실행해볼 수 있는 카탈로그 앱으로, 웹 데모는 <https://sdk-example.aitc.dev/>에서 바로 확인할 수 있다. 새 mock을 추가하면 sdk-example의 카드에서 그대로 동작하는 게 1차 sanity check. 단, 이 repo의 E2E suite는 sdk-example을 clone하지 않고 **내부 자기완결 fixture(`e2e/fixture/`)** 로 운영한다 — sdk-example이 깨져도 devtools CI는 영향받지 않는다.
@@ -298,6 +302,22 @@ saveUserPreset('My QA scenario', {
298
302
  });
299
303
  ```
300
304
 
305
+ ### Panel mount / dispose
306
+
307
+ `@ait-co/devtools/panel`을 import하면 DOM ready 시 자동으로 마운트됩니다. 마운트는 idempotent — 같은 페이지에 여러 번 import되거나 `mount()`를 다시 불러도 토글 버튼은 하나만 떠 있습니다.
308
+
309
+ HMR이나 SPA 라우팅에서 패널을 명시적으로 떼어내야 하는 경우 `disposePanel()`을 사용하세요:
310
+
311
+ ```ts
312
+ import { disposePanel, mount } from '@ait-co/devtools/panel';
313
+
314
+ disposePanel(); // 토글 / 패널 / inject된 <style> / 모든 listener 제거.
315
+ // 호출 전이거나 두 번 호출해도 안전.
316
+ mount(); // 깨끗한 상태로 다시 마운트. 중복 <style>·listener 없음.
317
+ ```
318
+
319
+ 내부에서 `disposeViewport()`도 함께 호출하므로 viewport 시뮬레이션도 함께 원복됩니다.
320
+
301
321
  ## Device simulation (Viewport 탭)
302
322
 
303
323
  데스크탑 브라우저에서 모바일 미니앱을 개발할 때, 실제 디바이스 해상도/safe area/노치/홈 인디케이터/앱인토스 nav bar를 반영해 레이아웃을 검증할 수 있습니다.
@@ -6,6 +6,15 @@
6
6
  * 외부 의존성 없이 vanilla DOM으로 구현.
7
7
  */
8
8
  declare function mount(): void;
9
+ /**
10
+ * Pairs with `mount()` (and the existing `disposeViewport()`).
11
+ * Idempotent — safe to call before mount or twice in a row.
12
+ *
13
+ * Removes panel DOM (toggle + panel root), the injected `<style>`, all
14
+ * window/aitState listeners, and resets module-level state. After dispose,
15
+ * `mount()` can be called again to re-mount cleanly.
16
+ */
17
+ declare function disposePanel(): void;
9
18
  //#endregion
10
- export { mount };
19
+ export { disposePanel, mount };
11
20
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","names":[],"sources":["../../src/panel/index.ts"],"mappings":";;;;;;;iBAyNS,KAAA,CAAA"}
1
+ {"version":3,"file":"index.d.ts","names":[],"sources":["../../src/panel/index.ts"],"mappings":";;;;;;;iBAoNS,KAAA,CAAA;;;;;;;;;iBAkJA,YAAA,CAAA"}
@@ -2566,6 +2566,23 @@ function renderHomeIndicator() {
2566
2566
  document.body.appendChild(el);
2567
2567
  }
2568
2568
  /**
2569
+ * 모든 viewport DOM mutation을 원복하고 aitState 구독도 해제한다.
2570
+ * 외부 consumer가 패널을 동적으로 제거할 때 호출. 호출 후에는 aitState 변경이
2571
+ * DOM에 반영되지 않으므로 안전하게 panel을 떼어낼 수 있다.
2572
+ */
2573
+ function disposeViewport() {
2574
+ if (typeof document === "undefined") return;
2575
+ if (viewportUnsubscribe) viewportUnsubscribe();
2576
+ const html = document.documentElement;
2577
+ html.classList.remove("ait-viewport-active");
2578
+ html.classList.remove("ait-viewport-framed");
2579
+ removeById(STYLE_ELEMENT_ID);
2580
+ removeNotchElement();
2581
+ removeHomeIndicator();
2582
+ removeNavBarElement();
2583
+ bodyScrollHintEmitted = false;
2584
+ }
2585
+ /**
2569
2586
  * DOM에 뷰포트 제약을 적용한다.
2570
2587
  * - `html.ait-viewport-active` 클래스로 정적 CSS(styles.ts) 활성화
2571
2588
  * - body의 width/height는 preset 값으로, navbar top offset은 safeAreaTop으로 인라인 주입
@@ -3041,6 +3058,11 @@ let isOpen = false;
3041
3058
  let panelEl = null;
3042
3059
  let bodyEl = null;
3043
3060
  let tabsEl = null;
3061
+ let toggleEl = null;
3062
+ let injectedStyle = null;
3063
+ let panelSwitchTabHandler = null;
3064
+ let resizeHandler = null;
3065
+ let aitStateUnsubscribe = null;
3044
3066
  let tabRenderers = null;
3045
3067
  function refreshPanel() {
3046
3068
  if (!bodyEl || !tabsEl) return;
@@ -3056,26 +3078,19 @@ function refreshPanel() {
3056
3078
  el.classList.toggle("active", el.getAttribute("data-tab") === currentTab);
3057
3079
  });
3058
3080
  }
3059
- if (typeof window !== "undefined") window.addEventListener("__ait:panel-switch-tab", (e) => {
3060
- currentTab = e.detail.tab;
3061
- if (panelEl && !panelEl.classList.contains("open")) {
3062
- isOpen = true;
3063
- panelEl.classList.add("open");
3064
- }
3065
- refreshPanel();
3066
- });
3067
3081
  function mount() {
3068
3082
  if (typeof document === "undefined") return;
3069
3083
  if (document.querySelector(".ait-panel-toggle")) return;
3070
3084
  setDeviceRefreshPanel(refreshPanel);
3071
3085
  initViewport();
3072
- const style = document.createElement("style");
3073
- style.textContent = PANEL_STYLES;
3074
- document.head.appendChild(style);
3086
+ injectedStyle = document.createElement("style");
3087
+ injectedStyle.textContent = PANEL_STYLES;
3088
+ document.head.appendChild(injectedStyle);
3075
3089
  const toggle = h("button", {
3076
3090
  className: "ait-panel-toggle",
3077
3091
  title: "AIT DevTools"
3078
3092
  }, "AIT");
3093
+ toggleEl = toggle;
3079
3094
  restoreButtonPosition(toggle);
3080
3095
  panelEl = h("div", { className: "ait-panel" });
3081
3096
  const closeBtn = h("button", {
@@ -3096,7 +3111,7 @@ function mount() {
3096
3111
  mockBadge.textContent = aitState.state.panelEditable ? "EDIT" : "READ-ONLY";
3097
3112
  refreshPanel();
3098
3113
  });
3099
- 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.9`), closeBtn);
3114
+ 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.11`), closeBtn);
3100
3115
  const header = h("div", { className: "ait-panel-header" }, h("span", {}, "AIT DevTools"), headerRight);
3101
3116
  tabsEl = h("div", { className: "ait-panel-tabs" });
3102
3117
  for (const tab of TABS) {
@@ -3124,7 +3139,7 @@ function mount() {
3124
3139
  }
3125
3140
  });
3126
3141
  let resizeRaf = 0;
3127
- window.addEventListener("resize", () => {
3142
+ resizeHandler = () => {
3128
3143
  if (resizeRaf) return;
3129
3144
  resizeRaf = requestAnimationFrame(() => {
3130
3145
  resizeRaf = 0;
@@ -3132,16 +3147,56 @@ function mount() {
3132
3147
  saveButtonPosition(toggle);
3133
3148
  if (isOpen) updatePanelPosition(toggle);
3134
3149
  });
3135
- });
3136
- aitState.subscribe(() => {
3150
+ };
3151
+ window.addEventListener("resize", resizeHandler);
3152
+ aitStateUnsubscribe = aitState.subscribe(() => {
3137
3153
  try {
3138
3154
  if (isOpen && (currentTab === "analytics" || currentTab === "storage" || currentTab === "device" || currentTab === "viewport" || currentTab === "iap" || currentTab === "ads" || currentTab === "presets")) refreshPanel();
3139
3155
  } catch (err) {
3140
3156
  console.error("[@ait-co/devtools] Error in subscribe callback:", err);
3141
3157
  }
3142
3158
  });
3159
+ panelSwitchTabHandler = (e) => {
3160
+ currentTab = e.detail.tab;
3161
+ if (panelEl && !panelEl.classList.contains("open")) {
3162
+ isOpen = true;
3163
+ panelEl.classList.add("open");
3164
+ }
3165
+ refreshPanel();
3166
+ };
3167
+ window.addEventListener("__ait:panel-switch-tab", panelSwitchTabHandler);
3143
3168
  refreshPanel();
3144
3169
  }
3170
+ /**
3171
+ * Pairs with `mount()` (and the existing `disposeViewport()`).
3172
+ * Idempotent — safe to call before mount or twice in a row.
3173
+ *
3174
+ * Removes panel DOM (toggle + panel root), the injected `<style>`, all
3175
+ * window/aitState listeners, and resets module-level state. After dispose,
3176
+ * `mount()` can be called again to re-mount cleanly.
3177
+ */
3178
+ function disposePanel() {
3179
+ if (typeof document === "undefined") return;
3180
+ if (panelSwitchTabHandler && typeof window !== "undefined") window.removeEventListener("__ait:panel-switch-tab", panelSwitchTabHandler);
3181
+ if (resizeHandler && typeof window !== "undefined") window.removeEventListener("resize", resizeHandler);
3182
+ if (aitStateUnsubscribe) aitStateUnsubscribe();
3183
+ toggleEl?.remove();
3184
+ panelEl?.remove();
3185
+ injectedStyle?.remove();
3186
+ disposeViewport();
3187
+ setDeviceRefreshPanel(() => {});
3188
+ panelSwitchTabHandler = null;
3189
+ resizeHandler = null;
3190
+ aitStateUnsubscribe = null;
3191
+ toggleEl = null;
3192
+ panelEl = null;
3193
+ bodyEl = null;
3194
+ tabsEl = null;
3195
+ injectedStyle = null;
3196
+ tabRenderers = null;
3197
+ currentTab = "env";
3198
+ isOpen = false;
3199
+ }
3145
3200
  if (typeof document !== "undefined") {
3146
3201
  const safeMount = () => {
3147
3202
  try {
@@ -3154,6 +3209,6 @@ if (typeof document !== "undefined") {
3154
3209
  else safeMount();
3155
3210
  }
3156
3211
  //#endregion
3157
- export { mount };
3212
+ export { disposePanel, mount };
3158
3213
 
3159
3214
  //# sourceMappingURL=index.js.map