@ait-co/devtools 0.1.10 → 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
@@ -302,6 +302,22 @@ saveUserPreset('My QA scenario', {
302
302
  });
303
303
  ```
304
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
+
305
321
  ## Device simulation (Viewport 탭)
306
322
 
307
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.10`), 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