@ait-co/devtools 0.1.41 → 0.1.44

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.
@@ -528,7 +528,6 @@ const DEFAULT_STATE = {
528
528
  preset: "none",
529
529
  orientation: "auto",
530
530
  appOrientation: null,
531
- landscapeSide: "left",
532
531
  customWidth: 402,
533
532
  customHeight: 874,
534
533
  frame: false,
@@ -1109,7 +1108,7 @@ function readGlobalString(key) {
1109
1108
  }
1110
1109
  const TELEMETRY_ENDPOINT = readGlobalString("__TELEMETRY_ENDPOINT__") ?? "https://t.aitc.dev";
1111
1110
  function getVersion() {
1112
- return "0.1.41";
1111
+ return "0.1.44";
1113
1112
  }
1114
1113
  let panelVisibleSince = null;
1115
1114
  let accumulatedMs = 0;
@@ -3773,7 +3772,7 @@ function buildDeviceProfile(preset, appVersion, landscape) {
3773
3772
  const tossToken = `AppsInToss TossApp/${appVersion}`;
3774
3773
  const baseUa = platform === "ios" ? "Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148" : "Mozilla/5.0 (Linux; Android 14) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Mobile Safari/537.36";
3775
3774
  const physWidth = Math.round(preset.width * preset.dpr);
3776
- const physHeight = Math.round(preset.height * preset.dpr);
3775
+ const physHeight = Math.round((preset.screenHeight ?? preset.height) * preset.dpr);
3777
3776
  return {
3778
3777
  platform,
3779
3778
  userAgent: `${baseUa} ${tossToken}`,
@@ -3843,8 +3842,10 @@ function syncDeviceEmulation(preset, landscape) {
3843
3842
  *
3844
3843
  * Panel에서 선택한 디바이스 프리셋을 `document.body`에 적용한다. 정적 CSS는
3845
3844
  * `panel/styles.ts`에 정의되어 있고 (Panel mount 시 head에 주입), 여기서는 프리셋별
3846
- * 동적 값(width/height, 콘텐츠 push용 body padding-top)만 별도 `<style>` 엘리먼트로
3847
- * 관리한다.
3845
+ * 동적 값(width/height)만 별도 `<style>` 엘리먼트로 관리한다.
3846
+ *
3847
+ * body `padding-top`은 주입하지 않는다: 실기기에서 토스 native nav bar는 WebView viewport
3848
+ * 밖이라 콘텐츠는 top=0부터 시작한다(devtools#275).
3848
3849
  */
3849
3850
  const VIEWPORT_STORAGE_KEY = "__ait_viewport";
3850
3851
  /** Custom width/height의 안전 상한 (CSS px). 4K + 여유. */
@@ -3920,16 +3921,19 @@ const VIEWPORT_PRESETS = [
3920
3921
  id: "iphone-15-pro",
3921
3922
  label: "iPhone 15 Pro",
3922
3923
  width: 393,
3923
- height: 852,
3924
+ height: 754,
3925
+ screenHeight: 852,
3924
3926
  dpr: 3,
3925
3927
  notch: "dynamic-island",
3926
3928
  notchInset: 59,
3927
3929
  navBarHeight: 54,
3928
3930
  safeAreaBottom: 34,
3931
+ safeAreaBottomLandscape: 20,
3929
3932
  safeAreaProvenance: {
3930
3933
  source: "measured",
3931
3934
  device: "iPhone 15 Pro",
3932
- date: "2026-05-25"
3935
+ date: "2026-05-28",
3936
+ orientations: ["portrait", "landscape"]
3933
3937
  }
3934
3938
  },
3935
3939
  {
@@ -4104,42 +4108,48 @@ function resolveViewportSize(state) {
4104
4108
  }
4105
4109
  /**
4106
4110
  * 프리셋 + orientation + nav bar 상태로부터 SDK `SafeAreaInsets.get()`이 반환할 insets를
4107
- * 계산한다. iPhone 15 Pro on-device relay 실측(devtools#190)에 맞춘 모델:
4111
+ * 계산한다. iPhone 15 Pro on-device relay 실측(devtools#190, #198, #232, #275)에 맞춘 모델:
4108
4112
  *
4109
- * - **Portrait top = 토스 nav bar 높이** (OS 노치가 아니다). 실측에서
4110
- * `env(safe-area-inset-top)` = 0, `SafeAreaInsets.get().top` = 54 였고, 54는 호스트
4111
- * nav bar다. 따라서 nav bar가 있고 `partner` type일 때만 `navBarHeight`를 top에 준다.
4112
- * `game`(투명 오버레이, 콘텐츠 밀어냄) 또는 nav bar 미표시면 top = 0.
4113
- * - **Bottom = `safeAreaBottom`** (home-indicator). 실측 34와 일치.
4114
- * - **Landscape iPhone(notch/Dynamic Island)**: 노치가 한쪽으로 가므로 `landscapeSide`에
4115
- * 따라 left/right 한쪽에만 `notchInset`을 준다. top은 0(landscape nav bar 거동은
4116
- * 미실측 portrait 모델만 확정), home-indicator는 bottom에 유지.
4113
+ * - **Portrait top = 0** (partner/game 모두). 실측(devtools#275)에서 토스 native nav bar는
4114
+ * partner WebView **viewport 밖**에 그려진다. SDK가 반환하는 `top=54`는 호스트 nav bar
4115
+ * 높이에 대한 정보용 값이고, WebView 좌표계에서 콘텐츠는 top=0부터 시작한다. 소비자가
4116
+ * 값을 `padding-top`으로 적용하면 실기기에서 잉여 공간이 생긴다(double-count).
4117
+ * mock은 top=0을 반환해 소비자 코드가 실기기와 같은 결과를 내도록 한다.
4118
+ * `game` type 측정은 아직 미진행이지만 동일하게 top=0을 반환한다(추후 실측으로 갱신).
4119
+ * - **Bottom = `safeAreaBottom`** (portrait home-indicator, 실측 34).
4120
+ * landscape는 `safeAreaBottomLandscape`가 정의돼 있으면 값을 사용한다
4121
+ * (iPhone 15 Pro landscape 실측 20 — portrait 34와 다름).
4122
+ * - **Landscape iPhone(notch/Dynamic Island)**: CSS env()와 SDK SafeAreaInsets 모두
4123
+ * `left = right = notchInset`(양쪽 대칭)을 반환한다. 물리적 노치는 한쪽으로 가지만
4124
+ * OS가 양쪽 모두에 같은 inset을 부여하므로 landscapeSide mental model은 틀렸다
4125
+ * (2026-05-28 iPhone 15 Pro relay 실측 #198/#232: left=right=59). top=0(landscape에서
4126
+ * 토스 앱이 partner nav bar를 숨김, #232 실측 확인).
4117
4127
  * - **Android punch-hole(status bar)**: landscape에서도 top에 status bar(`notchInset`)가
4118
4128
  * 유지된다.
4119
4129
  */
4120
- function computeSafeAreaInsets(preset, landscape, side, navBarVisible, navBarType) {
4130
+ function computeSafeAreaInsets(preset, landscape) {
4121
4131
  if (preset.id === "none" || preset.id === "custom") return {
4122
4132
  top: 0,
4123
4133
  bottom: 0,
4124
4134
  left: 0,
4125
4135
  right: 0
4126
4136
  };
4127
- const navBarTop = navBarVisible && navBarType === "partner" ? preset.navBarHeight : 0;
4128
4137
  if (!landscape) return {
4129
- top: navBarTop,
4138
+ top: 0,
4130
4139
  bottom: preset.safeAreaBottom,
4131
4140
  left: 0,
4132
4141
  right: 0
4133
4142
  };
4143
+ const landscapeBottom = preset.safeAreaBottomLandscape !== void 0 ? preset.safeAreaBottomLandscape : preset.safeAreaBottom;
4134
4144
  if (preset.notch === "notch" || preset.notch === "dynamic-island") return {
4135
4145
  top: 0,
4136
- bottom: preset.safeAreaBottom,
4137
- left: side === "left" ? preset.notchInset : 0,
4138
- right: side === "right" ? preset.notchInset : 0
4146
+ bottom: landscapeBottom,
4147
+ left: preset.notchInset,
4148
+ right: preset.notchInset
4139
4149
  };
4140
4150
  return {
4141
4151
  top: preset.notchInset,
4142
- bottom: preset.safeAreaBottom,
4152
+ bottom: landscapeBottom,
4143
4153
  left: 0,
4144
4154
  right: 0
4145
4155
  };
@@ -4147,7 +4157,7 @@ function computeSafeAreaInsets(preset, landscape, side, navBarVisible, navBarTyp
4147
4157
  /** viewport preset 또는 orientation이 바뀌면 safe-area insets도 자동 갱신한다. */
4148
4158
  function syncSafeAreaFromViewport(state) {
4149
4159
  if (state.preset === "none" || state.preset === "custom") return;
4150
- const next = computeSafeAreaInsets(getPreset(state.preset), effectiveOrientation(state) === "landscape", state.landscapeSide, state.aitNavBar, state.aitNavBarType);
4160
+ const next = computeSafeAreaInsets(getPreset(state.preset), effectiveOrientation(state) === "landscape");
4151
4161
  const current = aitState.state.safeAreaInsets;
4152
4162
  if (current.top === next.top && current.bottom === next.bottom && current.left === next.left && current.right === next.right) return;
4153
4163
  aitState.update({ safeAreaInsets: next });
@@ -4188,12 +4198,10 @@ function removeNavBarElement() {
4188
4198
  * 우측 `⋯` + 구분선 + `×`.
4189
4199
  * - `game`: 투명 배경, 게임 캔버스를 가리지 않도록 우측 `⋯` + 구분선 + `×`만.
4190
4200
  *
4191
- * nav bar는 WebView(body) 좌표계의 최상단(top 0)에 앉는다 실기기에서 OS notch
4192
- * WebView 밖(status bar)이라 `env(safe-area-inset-top)`이 0이고, WebView 콘텐츠 영역은
4193
- * nav bar 바로 아래(= SDK `SafeAreaInsets.get().top` = `navBarHeight`)에서 시작한다.
4194
- * 콘텐츠를 그만큼 밀어내는 `applyViewport`의 body `padding-top`이 담당하므로, nav bar
4195
- * 바닥과 콘텐츠 시작이 정확히 맞물린다. 시각 notch 오버레이는 body 밖 위쪽(status bar
4196
- * 영역)에 따로 그린다(`renderNotchOverlay`) — body 안이 아니다.
4201
+ * 오버레이는 **시각 참고용 frame 장식**이다. 실기기에서 토스 native nav bar WebView
4202
+ * viewport 밖에 그려지므로(devtools#275), mock의 nav bar 오버레이가 콘텐츠 위에 overlap
4203
+ * 되는 것이 실제 동작과 일치한다 — body에 `padding-top`을 주입하지 않는다.
4204
+ * 시각 notch 오버레이는 body 위쪽에 따로 그린다(`renderNotchOverlay`) body 안이 아니다.
4197
4205
  *
4198
4206
  * 뒤로가기 버튼은 `__ait:backEvent`를 트리거하고, X 버튼은 `closeView()`를 호출한다.
4199
4207
  * 실제 SDK 이벤트 플러밍을 한 곳에서 검증할 수 있다.
@@ -4315,14 +4323,12 @@ function applyViewport(state) {
4315
4323
  const preset = state.preset === "custom" ? null : getPreset(state.preset);
4316
4324
  const landscape = effectiveOrientation(state) === "landscape";
4317
4325
  syncDeviceEmulation(preset, landscape);
4318
- const contentTop = preset ? computeSafeAreaInsets(preset, landscape, state.landscapeSide, state.aitNavBar, state.aitNavBarType).top : 0;
4319
4326
  style.textContent = `
4320
4327
  html.ait-viewport-active body {
4321
4328
  width: ${size.width}px;
4322
4329
  max-width: ${size.width}px;
4323
4330
  min-height: ${size.height}px;
4324
4331
  max-height: ${size.height}px;
4325
- padding-top: ${contentTop}px;
4326
4332
  }
4327
4333
  `;
4328
4334
  if (preset && state.frame && !landscape) renderNotchOverlay(preset);
@@ -4341,9 +4347,6 @@ function isViewportOrientation(v) {
4341
4347
  function isAppOrientation(v) {
4342
4348
  return v === null || v === "portrait" || v === "landscape";
4343
4349
  }
4344
- function isLandscapeSide(v) {
4345
- return v === "left" || v === "right";
4346
- }
4347
4350
  /** 1 이상의 정수 + VIEWPORT_CUSTOM_MAX 이하인지 검사. sessionStorage 보호용. */
4348
4351
  function isValidCustomDimension(v) {
4349
4352
  return typeof v === "number" && Number.isInteger(v) && v >= 1 && v <= 4096;
@@ -4371,7 +4374,6 @@ function loadViewportFromStorage() {
4371
4374
  if (isViewportPresetId(obj.preset)) next.preset = obj.preset;
4372
4375
  if (isViewportOrientation(obj.orientation)) next.orientation = obj.orientation;
4373
4376
  if (isAppOrientation(obj.appOrientation)) next.appOrientation = obj.appOrientation;
4374
- if (isLandscapeSide(obj.landscapeSide)) next.landscapeSide = obj.landscapeSide;
4375
4377
  if (isValidCustomDimension(obj.customWidth)) next.customWidth = obj.customWidth;
4376
4378
  if (isValidCustomDimension(obj.customHeight)) next.customHeight = obj.customHeight;
4377
4379
  if (typeof obj.frame === "boolean") next.frame = obj.frame;
@@ -4486,16 +4488,6 @@ function renderViewportTab() {
4486
4488
  orientationSelect.addEventListener("change", () => {
4487
4489
  aitState.patch("viewport", { orientation: orientationSelect.value });
4488
4490
  });
4489
- const landscapeSideSelect = h("select", { className: "ait-select" });
4490
- if (disabled) landscapeSideSelect.disabled = true;
4491
- for (const opt of ["left", "right"]) {
4492
- const option = h("option", { value: opt }, opt);
4493
- if (opt === vp.landscapeSide) option.selected = true;
4494
- landscapeSideSelect.appendChild(option);
4495
- }
4496
- landscapeSideSelect.addEventListener("change", () => {
4497
- aitState.patch("viewport", { landscapeSide: landscapeSideSelect.value });
4498
- });
4499
4491
  const customRow = h("div", { className: "ait-section" });
4500
4492
  if (vp.preset === "custom") {
4501
4493
  const widthInput = h("input", {
@@ -4566,7 +4558,7 @@ function renderViewportTab() {
4566
4558
  const orientDisplay = vp.orientation === "auto" ? t("viewport.orientation.autoSuffix", { orient: effOrient }) : effOrient;
4567
4559
  rows.push(h("div", { className: "ait-status-row" }, h("span", {}, t("viewport.status.cssPhysical")), h("span", { className: "ait-status-value" }, `${size.width}×${size.height}@${dpr}x | ${physW}×${physH} ${orientDisplay}`)));
4568
4560
  if (preset) {
4569
- const insets = computeSafeAreaInsets(preset, landscape, vp.landscapeSide, vp.aitNavBar, vp.aitNavBarType);
4561
+ const insets = computeSafeAreaInsets(preset, landscape);
4570
4562
  const safeAreaValueEl = h("span", { className: "ait-status-value" }, `T${insets.top} R${insets.right} B${insets.bottom} L${insets.left}`);
4571
4563
  const badge = provenanceBadge(preset.safeAreaProvenance);
4572
4564
  if (badge) safeAreaValueEl.appendChild(badge);
@@ -4582,10 +4574,6 @@ function renderViewportTab() {
4582
4574
  for (const row of rows) statusEl.appendChild(row);
4583
4575
  }
4584
4576
  const deviceSection = h("div", { className: "ait-section" }, h("div", { className: "ait-section-title" }, t("viewport.section.device")), h("div", { className: "ait-row" }, h("label", {}, t("viewport.row.preset")), presetSelect), h("div", { className: "ait-row" }, h("label", {}, t("viewport.row.orientation")), orientationSelect));
4585
- if (effectiveOrientation(vp) === "landscape" && vp.preset !== "none" && vp.preset !== "custom") {
4586
- const notch = getPreset(vp.preset).notch;
4587
- if (notch === "notch" || notch === "dynamic-island") deviceSection.appendChild(h("div", { className: "ait-row" }, h("label", {}, t("viewport.row.notchSide")), landscapeSideSelect));
4588
- }
4589
4577
  container.append(deviceSection, customRow, h("div", { className: "ait-section" }, h("div", { className: "ait-section-title" }, t("viewport.section.appearance")), h("div", { className: "ait-row" }, h("label", {}, t("viewport.row.showFrame")), frameCheckbox), h("div", { className: "ait-row" }, h("label", {}, t("viewport.row.showAitNavBar")), navBarCheckbox), h("div", { className: "ait-row" }, h("label", {}, t("viewport.row.navBarType")), navBarTypeSelect)), statusEl);
4590
4578
  return container;
4591
4579
  }
@@ -4873,7 +4861,7 @@ function mount() {
4873
4861
  mockBadge.textContent = aitState.state.panelEditable ? t("panel.editMode.on") : t("panel.editMode.off");
4874
4862
  refreshPanel();
4875
4863
  });
4876
- 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.41`), closeBtn);
4864
+ 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.44`), closeBtn);
4877
4865
  const header = h("div", { className: "ait-panel-header" }, h("span", {}, t("panel.title")), headerRight);
4878
4866
  tabsEl = h("div", { className: "ait-panel-tabs" });
4879
4867
  for (const tab of getTabs()) {