@ait-co/devtools 0.1.31 → 0.1.32
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.en.md +11 -7
- package/README.md +24 -9
- package/dist/mcp/cli.js +2 -2
- package/dist/mcp/server.js +1 -1
- package/dist/mock/index.d.ts +5 -2
- package/dist/mock/index.d.ts.map +1 -1
- package/dist/mock/index.js +5 -3
- package/dist/mock/index.js.map +1 -1
- package/dist/panel/index.d.ts.map +1 -1
- package/dist/panel/index.js +248 -53
- package/dist/panel/index.js.map +1 -1
- package/package.json +1 -1
package/dist/panel/index.js
CHANGED
|
@@ -35,6 +35,12 @@ const en = {
|
|
|
35
35
|
"env.section.safeArea": "Safe Area Insets",
|
|
36
36
|
"env.row.safeArea.top": "Top",
|
|
37
37
|
"env.row.safeArea.bottom": "Bottom",
|
|
38
|
+
"env.section.navigation": "Navigation",
|
|
39
|
+
"env.row.iosSwipeGesture": "iOS swipe-back",
|
|
40
|
+
"env.value.iosSwipeGesture.unset": "not called",
|
|
41
|
+
"env.value.iosSwipeGesture.enabled": "enabled",
|
|
42
|
+
"env.value.iosSwipeGesture.disabled": "disabled",
|
|
43
|
+
"env.hint.iosSwipeGesture": "Last value passed to setIosSwipeGestureEnabled. Switching Environment to toss lets a toss-gated guard toggle this.",
|
|
38
44
|
"env.telemetry.section": "Telemetry",
|
|
39
45
|
"env.telemetry.t0Row": "Anonymous usage signal (Tier 0)",
|
|
40
46
|
"env.telemetry.t0On": "On",
|
|
@@ -99,7 +105,7 @@ const en = {
|
|
|
99
105
|
"viewport.status.cssPhysical": "CSS / physical",
|
|
100
106
|
"viewport.status.safeArea": "Safe area",
|
|
101
107
|
"viewport.status.aitNavBar": "AIT nav bar",
|
|
102
|
-
"viewport.status.aitNavBarValue": "{height}px
|
|
108
|
+
"viewport.status.aitNavBarValue": "{height}px → SafeArea top · {type}",
|
|
103
109
|
"viewport.orientation.autoSuffix": "{orient} (auto)",
|
|
104
110
|
"iap.section.simulator": "IAP Simulator",
|
|
105
111
|
"iap.row.nextResult": "Next Purchase Result",
|
|
@@ -186,6 +192,12 @@ const ko = {
|
|
|
186
192
|
"env.section.safeArea": "Safe Area Insets",
|
|
187
193
|
"env.row.safeArea.top": "Top",
|
|
188
194
|
"env.row.safeArea.bottom": "Bottom",
|
|
195
|
+
"env.section.navigation": "Navigation",
|
|
196
|
+
"env.row.iosSwipeGesture": "iOS swipe-back",
|
|
197
|
+
"env.value.iosSwipeGesture.unset": "미호출",
|
|
198
|
+
"env.value.iosSwipeGesture.enabled": "enabled",
|
|
199
|
+
"env.value.iosSwipeGesture.disabled": "disabled",
|
|
200
|
+
"env.hint.iosSwipeGesture": "setIosSwipeGestureEnabled의 마지막 호출값. Environment를 toss로 바꾸면 toss-gated 가드가 이 값을 토글합니다.",
|
|
189
201
|
"env.telemetry.section": "Telemetry",
|
|
190
202
|
"env.telemetry.t0Row": "익명 사용 신호 (Tier 0)",
|
|
191
203
|
"env.telemetry.t0On": "On",
|
|
@@ -250,7 +262,7 @@ const ko = {
|
|
|
250
262
|
"viewport.status.cssPhysical": "CSS / physical",
|
|
251
263
|
"viewport.status.safeArea": "Safe area",
|
|
252
264
|
"viewport.status.aitNavBar": "AIT nav bar",
|
|
253
|
-
"viewport.status.aitNavBarValue": "{height}px
|
|
265
|
+
"viewport.status.aitNavBarValue": "{height}px → SafeArea top · {type}",
|
|
254
266
|
"viewport.orientation.autoSuffix": "{orient} (auto)",
|
|
255
267
|
"iap.section.simulator": "IAP Simulator",
|
|
256
268
|
"iap.row.nextResult": "Next Purchase Result",
|
|
@@ -395,6 +407,7 @@ const DEFAULT_STATE = {
|
|
|
395
407
|
primaryColor: "#3182F6"
|
|
396
408
|
},
|
|
397
409
|
networkStatus: "WIFI",
|
|
410
|
+
navigation: { iosSwipeGestureEnabled: null },
|
|
398
411
|
permissions: {
|
|
399
412
|
clipboard: "allowed",
|
|
400
413
|
contacts: "allowed",
|
|
@@ -416,7 +429,7 @@ const DEFAULT_STATE = {
|
|
|
416
429
|
accessLocation: "FINE"
|
|
417
430
|
},
|
|
418
431
|
safeAreaInsets: {
|
|
419
|
-
top:
|
|
432
|
+
top: 54,
|
|
420
433
|
bottom: 34,
|
|
421
434
|
left: 0,
|
|
422
435
|
right: 0
|
|
@@ -1050,7 +1063,7 @@ function readGlobalString(key) {
|
|
|
1050
1063
|
}
|
|
1051
1064
|
const TELEMETRY_ENDPOINT = readGlobalString("__TELEMETRY_ENDPOINT__") ?? "https://t.aitc.dev";
|
|
1052
1065
|
function getVersion() {
|
|
1053
|
-
return "0.1.
|
|
1066
|
+
return "0.1.32";
|
|
1054
1067
|
}
|
|
1055
1068
|
let panelVisibleSince = null;
|
|
1056
1069
|
let accumulatedMs = 0;
|
|
@@ -1587,28 +1600,39 @@ const PANEL_STYLES = `
|
|
|
1587
1600
|
}
|
|
1588
1601
|
html.ait-viewport-framed body {
|
|
1589
1602
|
border-radius: 36px;
|
|
1603
|
+
/* Reserve the status bar strip above the WebView so the notch sits outside
|
|
1604
|
+
the body (OS notch is outside the WebView; env top=0). */
|
|
1605
|
+
margin-top: 74px;
|
|
1590
1606
|
box-shadow:
|
|
1591
1607
|
0 0 0 10px #1a1a2e,
|
|
1592
1608
|
0 0 0 12px #3a3a5a,
|
|
1593
1609
|
0 24px 48px rgba(0,0,0,0.5);
|
|
1594
1610
|
}
|
|
1595
1611
|
|
|
1596
|
-
/* Notch / Dynamic Island / punch-hole
|
|
1612
|
+
/* Notch / Dynamic Island / punch-hole — drawn in the status bar strip ABOVE
|
|
1613
|
+
the WebView (negative top puts it in the reserved margin), so it never
|
|
1614
|
+
overlaps the nav bar (which sits at the body's top edge). */
|
|
1597
1615
|
.ait-notch {
|
|
1598
1616
|
position: absolute;
|
|
1599
|
-
top: 0;
|
|
1600
1617
|
left: 50%;
|
|
1601
1618
|
transform: translateX(-50%);
|
|
1602
1619
|
background: #000;
|
|
1603
1620
|
z-index: 10;
|
|
1604
1621
|
pointer-events: none;
|
|
1605
1622
|
}
|
|
1606
|
-
.ait-notch-dynamic-island {
|
|
1623
|
+
.ait-notch-dynamic-island {
|
|
1624
|
+
top: -44px;
|
|
1625
|
+
width: 126px; height: 37px; border-radius: 20px;
|
|
1626
|
+
}
|
|
1607
1627
|
.ait-notch-pill {
|
|
1628
|
+
top: -50px;
|
|
1608
1629
|
width: 160px; height: 30px;
|
|
1609
1630
|
border-bottom-left-radius: 20px; border-bottom-right-radius: 20px;
|
|
1610
1631
|
}
|
|
1611
|
-
.ait-notch-punch-hole {
|
|
1632
|
+
.ait-notch-punch-hole {
|
|
1633
|
+
top: -40px;
|
|
1634
|
+
width: 12px; height: 12px; border-radius: 50%;
|
|
1635
|
+
}
|
|
1612
1636
|
|
|
1613
1637
|
/* Home indicator pill (bottom of body, iPhones with safe-area bottom > 0) */
|
|
1614
1638
|
.ait-home-indicator {
|
|
@@ -1624,12 +1648,16 @@ const PANEL_STYLES = `
|
|
|
1624
1648
|
pointer-events: none;
|
|
1625
1649
|
}
|
|
1626
1650
|
|
|
1627
|
-
/* Apps in Toss host nav bar — sits
|
|
1651
|
+
/* Apps in Toss host nav bar — sits at the top of the WebView (body). The OS
|
|
1652
|
+
notch lives outside the WebView (env top=0), so the nav bar bottom is the
|
|
1653
|
+
content's top edge; applyViewport gives body padding-top = nav bar height
|
|
1654
|
+
so content starts exactly below it. */
|
|
1628
1655
|
.ait-navbar {
|
|
1629
1656
|
position: absolute;
|
|
1657
|
+
top: 0;
|
|
1630
1658
|
left: 0;
|
|
1631
1659
|
right: 0;
|
|
1632
|
-
height:
|
|
1660
|
+
height: 54px; /* AIT_NAV_BAR_HEIGHT_PARTNER (relay 실측) */
|
|
1633
1661
|
background: rgba(255, 255, 255, 0.92);
|
|
1634
1662
|
backdrop-filter: blur(8px);
|
|
1635
1663
|
display: flex;
|
|
@@ -2590,9 +2618,20 @@ function renderEnvironmentTab() {
|
|
|
2590
2618
|
"OFFLINE",
|
|
2591
2619
|
"WWAN",
|
|
2592
2620
|
"UNKNOWN"
|
|
2593
|
-
], s.networkStatus, (v) => aitState.update({ networkStatus: v }), disabled)), h("div", { className: "ait-section" }, h("div", { className: "ait-section-title" }, t("env.section.safeArea")), inputRow(t("env.row.safeArea.top"), String(s.safeAreaInsets.top), (v) => aitState.patch("safeAreaInsets", { top: Number(v) }), disabled), inputRow(t("env.row.safeArea.bottom"), String(s.safeAreaInsets.bottom), (v) => aitState.patch("safeAreaInsets", { bottom: Number(v) }), disabled)), buildLanguageSection(), buildTelemetrySection());
|
|
2621
|
+
], s.networkStatus, (v) => aitState.update({ networkStatus: v }), disabled)), h("div", { className: "ait-section" }, h("div", { className: "ait-section-title" }, t("env.section.safeArea")), inputRow(t("env.row.safeArea.top"), String(s.safeAreaInsets.top), (v) => aitState.patch("safeAreaInsets", { top: Number(v) }), disabled), inputRow(t("env.row.safeArea.bottom"), String(s.safeAreaInsets.bottom), (v) => aitState.patch("safeAreaInsets", { bottom: Number(v) }), disabled)), buildNavigationSection(), buildLanguageSection(), buildTelemetrySection());
|
|
2594
2622
|
return container;
|
|
2595
2623
|
}
|
|
2624
|
+
/**
|
|
2625
|
+
* Navigation 동작 관측 (read-only) — real(토스 WebView)에서 native bridge로 발화하는
|
|
2626
|
+
* no-op API의 마지막 호출값을 보여준다. Environment를 toss로 바꾸면 toss-gated 가드
|
|
2627
|
+
* (예: sdk-example `useDisableIosSwipeGestureInToss`)가 돌면서 이 값이 토글되므로,
|
|
2628
|
+
* "toss 진입 → 가드 실행 → 관측 가능한 state 변화" 루프를 패널에서 한눈에 확인할 수 있다.
|
|
2629
|
+
*/
|
|
2630
|
+
function buildNavigationSection() {
|
|
2631
|
+
const swipe = aitState.state.navigation.iosSwipeGestureEnabled;
|
|
2632
|
+
const valueText = swipe === null ? t("env.value.iosSwipeGesture.unset") : swipe ? t("env.value.iosSwipeGesture.enabled") : t("env.value.iosSwipeGesture.disabled");
|
|
2633
|
+
return h("div", { className: "ait-section" }, h("div", { className: "ait-section-title" }, t("env.section.navigation")), h("div", { className: "ait-row" }, h("label", {}, t("env.row.iosSwipeGesture")), h("span", { style: `font-family:'SF Mono','Menlo',monospace;font-size:12px;color:${swipe === null ? "#888" : "#95e6cb"}` }, valueText)), h("div", { style: "font-size:11px;color:#666;margin-top:4px" }, t("env.hint.iosSwipeGesture")));
|
|
2634
|
+
}
|
|
2596
2635
|
function buildLanguageSection() {
|
|
2597
2636
|
const current = getLocale();
|
|
2598
2637
|
const select = h("select", { className: "ait-select" });
|
|
@@ -3311,13 +3350,114 @@ async function getServerTime() {
|
|
|
3311
3350
|
}
|
|
3312
3351
|
getServerTime.isSupported = () => true;
|
|
3313
3352
|
//#endregion
|
|
3353
|
+
//#region src/panel/device-emulation.ts
|
|
3354
|
+
/**
|
|
3355
|
+
* 기기 preset ↔ 브라우저 특성 정합
|
|
3356
|
+
*
|
|
3357
|
+
* Viewport preset이 active일 때(`none`/`custom` 아님), 그 preset이 주장하는 기기와
|
|
3358
|
+
* `navigator.userAgent`·`navigator.platform`·`window.devicePixelRatio`·`screen.*`를
|
|
3359
|
+
* 일치시킨다. 특정 기기 frame 가상환경을 제공하는 이상 UA/DPR만 호스트 데스크톱 값으로
|
|
3360
|
+
* 남으면 비일관적이기 때문이다 (#190).
|
|
3361
|
+
*
|
|
3362
|
+
* 한계 — page-JS override는 **JS 읽기값만** 바꾼다. 실 CSS media query(`@media`),
|
|
3363
|
+
* 실제 터치 이벤트, 엔진 레벨 레이아웃은 호스트 브라우저 값이 그대로다. 픽셀/입력 단위
|
|
3364
|
+
* 완전 emulation이 필요하면 Chrome DevTools device-mode(또는 CDP)를 쓴다. preset이
|
|
3365
|
+
* `none`/`custom`이면 override를 걸지 않아 일반 dev의 호스트 환경을 건드리지 않는다.
|
|
3366
|
+
*
|
|
3367
|
+
* 구현 — 대상 속성은 setter가 없어도 `configurable: true`이므로 `Object.defineProperty`로
|
|
3368
|
+
* getter를 덮어쓸 수 있다. 원복을 위해 최초 override 직전의 디스크립터를 저장한다.
|
|
3369
|
+
*/
|
|
3370
|
+
/** preset id → 플랫폼. Apple 계열은 ios, 그 외(Galaxy)는 android. */
|
|
3371
|
+
function platformForPreset(presetId) {
|
|
3372
|
+
return presetId.startsWith("iphone") || presetId.startsWith("ipad") ? "ios" : "android";
|
|
3373
|
+
}
|
|
3374
|
+
/**
|
|
3375
|
+
* preset + 토스 앱 버전으로 기기 프로필을 합성한다.
|
|
3376
|
+
*
|
|
3377
|
+
* UA는 표준 모바일 UA 뒤에 `AppsInToss TossApp/<appVersion>` 토큰을 붙인다 — #171
|
|
3378
|
+
* 실측(`AppsInToss TossApp/5.261.0`)에서 확인된 토스 WebView UA 형태.
|
|
3379
|
+
*
|
|
3380
|
+
* @param preset portrait 기준 width/height/dpr를 가진 device preset
|
|
3381
|
+
* @param appVersion `aitState.state.appVersion` (UA suffix의 버전 토큰)
|
|
3382
|
+
* @param landscape true면 screen width/height를 swap
|
|
3383
|
+
*/
|
|
3384
|
+
function buildDeviceProfile(preset, appVersion, landscape) {
|
|
3385
|
+
const platform = platformForPreset(preset.id);
|
|
3386
|
+
const tossToken = `AppsInToss TossApp/${appVersion}`;
|
|
3387
|
+
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";
|
|
3388
|
+
const physWidth = Math.round(preset.width * preset.dpr);
|
|
3389
|
+
const physHeight = Math.round(preset.height * preset.dpr);
|
|
3390
|
+
return {
|
|
3391
|
+
platform,
|
|
3392
|
+
userAgent: `${baseUa} ${tossToken}`,
|
|
3393
|
+
navigatorPlatform: platform === "ios" ? "iPhone" : "Linux armv8l",
|
|
3394
|
+
devicePixelRatio: preset.dpr,
|
|
3395
|
+
screenWidth: landscape ? physHeight : physWidth,
|
|
3396
|
+
screenHeight: landscape ? physWidth : physHeight
|
|
3397
|
+
};
|
|
3398
|
+
}
|
|
3399
|
+
let savedDescriptors = null;
|
|
3400
|
+
function override(target, prop, value, saved) {
|
|
3401
|
+
if (!saved.some((s) => s.target === target && s.prop === prop)) saved.push({
|
|
3402
|
+
target,
|
|
3403
|
+
prop,
|
|
3404
|
+
descriptor: Object.getOwnPropertyDescriptor(target, prop)
|
|
3405
|
+
});
|
|
3406
|
+
try {
|
|
3407
|
+
Object.defineProperty(target, prop, {
|
|
3408
|
+
configurable: true,
|
|
3409
|
+
get: () => value
|
|
3410
|
+
});
|
|
3411
|
+
} catch {}
|
|
3412
|
+
}
|
|
3413
|
+
/**
|
|
3414
|
+
* 기기 프로필을 현재 페이지의 `navigator`/`window`/`screen`에 적용한다.
|
|
3415
|
+
* 호출 시 직전 디스크립터를 저장하므로 `revertDeviceEmulation()`으로 원복 가능.
|
|
3416
|
+
*/
|
|
3417
|
+
function applyDeviceEmulation(profile) {
|
|
3418
|
+
if (typeof navigator === "undefined" || typeof window === "undefined") return;
|
|
3419
|
+
revertDeviceEmulation();
|
|
3420
|
+
const saved = [];
|
|
3421
|
+
override(navigator, "userAgent", profile.userAgent, saved);
|
|
3422
|
+
override(navigator, "platform", profile.navigatorPlatform, saved);
|
|
3423
|
+
override(window, "devicePixelRatio", profile.devicePixelRatio, saved);
|
|
3424
|
+
if (typeof screen !== "undefined") {
|
|
3425
|
+
override(screen, "width", profile.screenWidth, saved);
|
|
3426
|
+
override(screen, "height", profile.screenHeight, saved);
|
|
3427
|
+
}
|
|
3428
|
+
savedDescriptors = saved;
|
|
3429
|
+
}
|
|
3430
|
+
/** `applyDeviceEmulation`이 덮어쓴 속성을 원래 디스크립터로 되돌린다. */
|
|
3431
|
+
function revertDeviceEmulation() {
|
|
3432
|
+
if (!savedDescriptors) return;
|
|
3433
|
+
for (const { target, prop, descriptor } of savedDescriptors) try {
|
|
3434
|
+
if (descriptor) Object.defineProperty(target, prop, descriptor);
|
|
3435
|
+
else delete target[prop];
|
|
3436
|
+
} catch {}
|
|
3437
|
+
savedDescriptors = null;
|
|
3438
|
+
}
|
|
3439
|
+
/**
|
|
3440
|
+
* Viewport state를 받아 preset이 active면 emulation 적용, 아니면 원복.
|
|
3441
|
+
* `applyViewport`가 매 viewport 변경마다 호출한다.
|
|
3442
|
+
*/
|
|
3443
|
+
function syncDeviceEmulation(preset, landscape) {
|
|
3444
|
+
if (!preset || preset.id === "none" || preset.id === "custom") {
|
|
3445
|
+
revertDeviceEmulation();
|
|
3446
|
+
return;
|
|
3447
|
+
}
|
|
3448
|
+
const profile = buildDeviceProfile(preset, aitState.state.appVersion, landscape);
|
|
3449
|
+
applyDeviceEmulation(profile);
|
|
3450
|
+
if (aitState.state.platform !== profile.platform) aitState.update({ platform: profile.platform });
|
|
3451
|
+
}
|
|
3452
|
+
//#endregion
|
|
3314
3453
|
//#region src/panel/viewport.ts
|
|
3315
3454
|
/**
|
|
3316
3455
|
* Viewport 시뮬레이션 유틸
|
|
3317
3456
|
*
|
|
3318
3457
|
* Panel에서 선택한 디바이스 프리셋을 `document.body`에 적용한다. 정적 CSS는
|
|
3319
3458
|
* `panel/styles.ts`에 정의되어 있고 (Panel mount 시 head에 주입), 여기서는 프리셋별
|
|
3320
|
-
* 동적 값(width/height,
|
|
3459
|
+
* 동적 값(width/height, 콘텐츠 push용 body padding-top)만 별도 `<style>` 엘리먼트로
|
|
3460
|
+
* 관리한다.
|
|
3321
3461
|
*/
|
|
3322
3462
|
const VIEWPORT_STORAGE_KEY = "__ait_viewport";
|
|
3323
3463
|
/** Custom width/height의 안전 상한 (CSS px). 4K + 여유. */
|
|
@@ -3329,14 +3469,27 @@ const NONE_PRESET = {
|
|
|
3329
3469
|
height: 0,
|
|
3330
3470
|
dpr: 1,
|
|
3331
3471
|
notch: "none",
|
|
3332
|
-
|
|
3472
|
+
notchInset: 0,
|
|
3473
|
+
navBarHeight: 0,
|
|
3333
3474
|
safeAreaBottom: 0
|
|
3334
3475
|
};
|
|
3335
3476
|
/**
|
|
3336
3477
|
* Device presets (2026). CSS viewport 크기는 실제 기기의 `window.innerWidth/innerHeight`.
|
|
3337
3478
|
* iPhone 17 시리즈는 2025-09 출시. iPhone Air는 2026-04 출시.
|
|
3338
3479
|
* Galaxy S26 시리즈는 2026-03-11 출시 — viewport 값은 phone-simulator.com에서 보고된
|
|
3339
|
-
* 측정치를 사용.
|
|
3480
|
+
* 측정치를 사용.
|
|
3481
|
+
*
|
|
3482
|
+
* safe-area 모델 (devtools#190 relay 실측 반영):
|
|
3483
|
+
* - `notchInset` = OS 노치/status bar inset. 기기별 물리값(landscape 측면 inset + 시각
|
|
3484
|
+
* 노치 오버레이용). iPhone 15 Pro 실측에서 `env(safe-area-inset-top)`은 0이었으므로 이
|
|
3485
|
+
* 값은 portrait SDK top에는 들어가지 않는다.
|
|
3486
|
+
* - `navBarHeight` = 토스 호스트 nav bar 높이. partner type portrait의 SDK `top`(실측 54).
|
|
3487
|
+
* 호스트 chrome이라 기기 무관 — 전 preset이 `AIT_NAV_BAR_HEIGHT_PARTNER` 공유.
|
|
3488
|
+
* - `safeAreaBottom` = home-indicator inset. 기기별(노치 iPhone 34, 홈버튼/Android 0).
|
|
3489
|
+
* iPhone 15 Pro 실측 bottom 34와 일치.
|
|
3490
|
+
*
|
|
3491
|
+
* 단, navBarHeight 54는 iOS partner에서만 실측됐다 — Android nav bar 높이와 game type
|
|
3492
|
+
* 미세 차이는 후속 실측 대상(현재는 같은 값을 잠정 적용).
|
|
3340
3493
|
*
|
|
3341
3494
|
* iPhone 17과 17 Pro는 CSS viewport / DPR / safe area가 동일 — 이는 의도이며 카피-페이스트
|
|
3342
3495
|
* 실수가 아니다. Apple의 17 lineup은 base와 Pro의 web-relevant 스펙이 같다.
|
|
@@ -3350,9 +3503,21 @@ const VIEWPORT_PRESETS = [
|
|
|
3350
3503
|
height: 667,
|
|
3351
3504
|
dpr: 2,
|
|
3352
3505
|
notch: "none",
|
|
3353
|
-
|
|
3506
|
+
notchInset: 20,
|
|
3507
|
+
navBarHeight: 54,
|
|
3354
3508
|
safeAreaBottom: 0
|
|
3355
3509
|
},
|
|
3510
|
+
{
|
|
3511
|
+
id: "iphone-15-pro",
|
|
3512
|
+
label: "iPhone 15 Pro",
|
|
3513
|
+
width: 393,
|
|
3514
|
+
height: 852,
|
|
3515
|
+
dpr: 3,
|
|
3516
|
+
notch: "dynamic-island",
|
|
3517
|
+
notchInset: 59,
|
|
3518
|
+
navBarHeight: 54,
|
|
3519
|
+
safeAreaBottom: 34
|
|
3520
|
+
},
|
|
3356
3521
|
{
|
|
3357
3522
|
id: "iphone-16e",
|
|
3358
3523
|
label: "iPhone 16e",
|
|
@@ -3360,7 +3525,8 @@ const VIEWPORT_PRESETS = [
|
|
|
3360
3525
|
height: 844,
|
|
3361
3526
|
dpr: 3,
|
|
3362
3527
|
notch: "notch",
|
|
3363
|
-
|
|
3528
|
+
notchInset: 47,
|
|
3529
|
+
navBarHeight: 54,
|
|
3364
3530
|
safeAreaBottom: 34
|
|
3365
3531
|
},
|
|
3366
3532
|
{
|
|
@@ -3370,7 +3536,8 @@ const VIEWPORT_PRESETS = [
|
|
|
3370
3536
|
height: 874,
|
|
3371
3537
|
dpr: 3,
|
|
3372
3538
|
notch: "dynamic-island",
|
|
3373
|
-
|
|
3539
|
+
notchInset: 59,
|
|
3540
|
+
navBarHeight: 54,
|
|
3374
3541
|
safeAreaBottom: 34
|
|
3375
3542
|
},
|
|
3376
3543
|
{
|
|
@@ -3380,7 +3547,8 @@ const VIEWPORT_PRESETS = [
|
|
|
3380
3547
|
height: 912,
|
|
3381
3548
|
dpr: 3,
|
|
3382
3549
|
notch: "dynamic-island",
|
|
3383
|
-
|
|
3550
|
+
notchInset: 59,
|
|
3551
|
+
navBarHeight: 54,
|
|
3384
3552
|
safeAreaBottom: 34
|
|
3385
3553
|
},
|
|
3386
3554
|
{
|
|
@@ -3390,7 +3558,8 @@ const VIEWPORT_PRESETS = [
|
|
|
3390
3558
|
height: 874,
|
|
3391
3559
|
dpr: 3,
|
|
3392
3560
|
notch: "dynamic-island",
|
|
3393
|
-
|
|
3561
|
+
notchInset: 59,
|
|
3562
|
+
navBarHeight: 54,
|
|
3394
3563
|
safeAreaBottom: 34
|
|
3395
3564
|
},
|
|
3396
3565
|
{
|
|
@@ -3400,7 +3569,8 @@ const VIEWPORT_PRESETS = [
|
|
|
3400
3569
|
height: 956,
|
|
3401
3570
|
dpr: 3,
|
|
3402
3571
|
notch: "dynamic-island",
|
|
3403
|
-
|
|
3572
|
+
notchInset: 62,
|
|
3573
|
+
navBarHeight: 54,
|
|
3404
3574
|
safeAreaBottom: 34
|
|
3405
3575
|
},
|
|
3406
3576
|
{
|
|
@@ -3410,7 +3580,8 @@ const VIEWPORT_PRESETS = [
|
|
|
3410
3580
|
height: 773,
|
|
3411
3581
|
dpr: 3,
|
|
3412
3582
|
notch: "punch-hole-center",
|
|
3413
|
-
|
|
3583
|
+
notchInset: 32,
|
|
3584
|
+
navBarHeight: 54,
|
|
3414
3585
|
safeAreaBottom: 0
|
|
3415
3586
|
},
|
|
3416
3587
|
{
|
|
@@ -3420,7 +3591,8 @@ const VIEWPORT_PRESETS = [
|
|
|
3420
3591
|
height: 1040,
|
|
3421
3592
|
dpr: 3,
|
|
3422
3593
|
notch: "punch-hole-center",
|
|
3423
|
-
|
|
3594
|
+
notchInset: 32,
|
|
3595
|
+
navBarHeight: 54,
|
|
3424
3596
|
safeAreaBottom: 0
|
|
3425
3597
|
},
|
|
3426
3598
|
{
|
|
@@ -3430,7 +3602,8 @@ const VIEWPORT_PRESETS = [
|
|
|
3430
3602
|
height: 1040,
|
|
3431
3603
|
dpr: 3,
|
|
3432
3604
|
notch: "punch-hole-center",
|
|
3433
|
-
|
|
3605
|
+
notchInset: 40,
|
|
3606
|
+
navBarHeight: 54,
|
|
3434
3607
|
safeAreaBottom: 0
|
|
3435
3608
|
},
|
|
3436
3609
|
{
|
|
@@ -3440,7 +3613,8 @@ const VIEWPORT_PRESETS = [
|
|
|
3440
3613
|
height: 990,
|
|
3441
3614
|
dpr: 3,
|
|
3442
3615
|
notch: "punch-hole-center",
|
|
3443
|
-
|
|
3616
|
+
notchInset: 36,
|
|
3617
|
+
navBarHeight: 54,
|
|
3444
3618
|
safeAreaBottom: 0
|
|
3445
3619
|
},
|
|
3446
3620
|
{
|
|
@@ -3450,7 +3624,8 @@ const VIEWPORT_PRESETS = [
|
|
|
3450
3624
|
height: 870,
|
|
3451
3625
|
dpr: 3,
|
|
3452
3626
|
notch: "punch-hole-center",
|
|
3453
|
-
|
|
3627
|
+
notchInset: 32,
|
|
3628
|
+
navBarHeight: 54,
|
|
3454
3629
|
safeAreaBottom: 0
|
|
3455
3630
|
},
|
|
3456
3631
|
{
|
|
@@ -3460,7 +3635,8 @@ const VIEWPORT_PRESETS = [
|
|
|
3460
3635
|
height: 884,
|
|
3461
3636
|
dpr: 2.625,
|
|
3462
3637
|
notch: "punch-hole-center",
|
|
3463
|
-
|
|
3638
|
+
notchInset: 32,
|
|
3639
|
+
navBarHeight: 54,
|
|
3464
3640
|
safeAreaBottom: 0
|
|
3465
3641
|
},
|
|
3466
3642
|
{
|
|
@@ -3470,7 +3646,8 @@ const VIEWPORT_PRESETS = [
|
|
|
3470
3646
|
height: 0,
|
|
3471
3647
|
dpr: 1,
|
|
3472
3648
|
notch: "none",
|
|
3473
|
-
|
|
3649
|
+
notchInset: 0,
|
|
3650
|
+
navBarHeight: 0,
|
|
3474
3651
|
safeAreaBottom: 0
|
|
3475
3652
|
}
|
|
3476
3653
|
];
|
|
@@ -3511,23 +3688,30 @@ function resolveViewportSize(state) {
|
|
|
3511
3688
|
};
|
|
3512
3689
|
}
|
|
3513
3690
|
/**
|
|
3514
|
-
* 프리셋 +
|
|
3691
|
+
* 프리셋 + orientation + nav bar 상태로부터 SDK `SafeAreaInsets.get()`이 반환할 insets를
|
|
3692
|
+
* 계산한다. iPhone 15 Pro on-device relay 실측(devtools#190)에 맞춘 모델:
|
|
3515
3693
|
*
|
|
3516
|
-
* - Portrait
|
|
3517
|
-
*
|
|
3518
|
-
*
|
|
3519
|
-
*
|
|
3520
|
-
* -
|
|
3694
|
+
* - **Portrait top = 토스 nav bar 높이** (OS 노치가 아니다). 실측에서
|
|
3695
|
+
* `env(safe-area-inset-top)` = 0, `SafeAreaInsets.get().top` = 54 였고, 그 54는 호스트
|
|
3696
|
+
* nav bar다. 따라서 nav bar가 떠 있고 `partner` type일 때만 `navBarHeight`를 top에 준다.
|
|
3697
|
+
* `game`(투명 오버레이, 콘텐츠 안 밀어냄) 또는 nav bar 미표시면 top = 0.
|
|
3698
|
+
* - **Bottom = `safeAreaBottom`** (home-indicator). 실측 34와 일치.
|
|
3699
|
+
* - **Landscape iPhone(notch/Dynamic Island)**: 노치가 한쪽으로 가므로 `landscapeSide`에
|
|
3700
|
+
* 따라 left/right 한쪽에만 `notchInset`을 준다. top은 0(landscape nav bar 거동은
|
|
3701
|
+
* 미실측 — portrait 모델만 확정), home-indicator는 bottom에 유지.
|
|
3702
|
+
* - **Android punch-hole(status bar)**: landscape에서도 top에 status bar(`notchInset`)가
|
|
3703
|
+
* 유지된다.
|
|
3521
3704
|
*/
|
|
3522
|
-
function computeSafeAreaInsets(preset, landscape, side) {
|
|
3705
|
+
function computeSafeAreaInsets(preset, landscape, side, navBarVisible, navBarType) {
|
|
3523
3706
|
if (preset.id === "none" || preset.id === "custom") return {
|
|
3524
3707
|
top: 0,
|
|
3525
3708
|
bottom: 0,
|
|
3526
3709
|
left: 0,
|
|
3527
3710
|
right: 0
|
|
3528
3711
|
};
|
|
3712
|
+
const navBarTop = navBarVisible && navBarType === "partner" ? preset.navBarHeight : 0;
|
|
3529
3713
|
if (!landscape) return {
|
|
3530
|
-
top:
|
|
3714
|
+
top: navBarTop,
|
|
3531
3715
|
bottom: preset.safeAreaBottom,
|
|
3532
3716
|
left: 0,
|
|
3533
3717
|
right: 0
|
|
@@ -3535,11 +3719,11 @@ function computeSafeAreaInsets(preset, landscape, side) {
|
|
|
3535
3719
|
if (preset.notch === "notch" || preset.notch === "dynamic-island") return {
|
|
3536
3720
|
top: 0,
|
|
3537
3721
|
bottom: preset.safeAreaBottom,
|
|
3538
|
-
left: side === "left" ? preset.
|
|
3539
|
-
right: side === "right" ? preset.
|
|
3722
|
+
left: side === "left" ? preset.notchInset : 0,
|
|
3723
|
+
right: side === "right" ? preset.notchInset : 0
|
|
3540
3724
|
};
|
|
3541
3725
|
return {
|
|
3542
|
-
top: preset.
|
|
3726
|
+
top: preset.notchInset,
|
|
3543
3727
|
bottom: preset.safeAreaBottom,
|
|
3544
3728
|
left: 0,
|
|
3545
3729
|
right: 0
|
|
@@ -3548,7 +3732,7 @@ function computeSafeAreaInsets(preset, landscape, side) {
|
|
|
3548
3732
|
/** viewport preset 또는 orientation이 바뀌면 safe-area insets도 자동 갱신한다. */
|
|
3549
3733
|
function syncSafeAreaFromViewport(state) {
|
|
3550
3734
|
if (state.preset === "none" || state.preset === "custom") return;
|
|
3551
|
-
const next = computeSafeAreaInsets(getPreset(state.preset), effectiveOrientation(state) === "landscape", state.landscapeSide);
|
|
3735
|
+
const next = computeSafeAreaInsets(getPreset(state.preset), effectiveOrientation(state) === "landscape", state.landscapeSide, state.aitNavBar, state.aitNavBarType);
|
|
3552
3736
|
const current = aitState.state.safeAreaInsets;
|
|
3553
3737
|
if (current.top === next.top && current.bottom === next.bottom && current.left === next.left && current.right === next.right) return;
|
|
3554
3738
|
aitState.update({ safeAreaInsets: next });
|
|
@@ -3582,27 +3766,30 @@ function removeNavBarElement() {
|
|
|
3582
3766
|
removeById(NAV_BAR_ELEMENT_ID);
|
|
3583
3767
|
}
|
|
3584
3768
|
/**
|
|
3585
|
-
* Apps in Toss host nav bar 렌더. OS status bar 아래에
|
|
3769
|
+
* Apps in Toss host nav bar 렌더. OS status bar(notch) 아래에 쌓인다.
|
|
3586
3770
|
*
|
|
3587
3771
|
* 변형(SDK `webViewProps.type`과 의미 일치):
|
|
3588
3772
|
* - `partner` (기본): 흰 배경, 좌측 뒤로가기(‹), 앱 아이콘 + 이름(`brand.displayName`),
|
|
3589
3773
|
* 우측 `⋯` + 구분선 + `×`.
|
|
3590
3774
|
* - `game`: 투명 배경, 게임 캔버스를 가리지 않도록 우측 `⋯` + 구분선 + `×`만.
|
|
3591
3775
|
*
|
|
3592
|
-
*
|
|
3593
|
-
*
|
|
3776
|
+
* nav bar는 WebView(body) 좌표계의 최상단(top 0)에 앉는다 — 실기기에서 OS notch는
|
|
3777
|
+
* WebView 밖(status bar)이라 `env(safe-area-inset-top)`이 0이고, WebView 콘텐츠 영역은
|
|
3778
|
+
* nav bar 바로 아래(= SDK `SafeAreaInsets.get().top` = `navBarHeight`)에서 시작한다.
|
|
3779
|
+
* 콘텐츠를 그만큼 밀어내는 건 `applyViewport`의 body `padding-top`이 담당하므로, nav bar
|
|
3780
|
+
* 바닥과 콘텐츠 시작이 정확히 맞물린다. 시각 notch 오버레이는 body 밖 위쪽(status bar
|
|
3781
|
+
* 영역)에 따로 그린다(`renderNotchOverlay`) — body 안이 아니다.
|
|
3594
3782
|
*
|
|
3595
3783
|
* 뒤로가기 버튼은 `__ait:backEvent`를 트리거하고, X 버튼은 `closeView()`를 호출한다.
|
|
3596
3784
|
* 실제 SDK 이벤트 플러밍을 한 곳에서 검증할 수 있다.
|
|
3597
3785
|
*/
|
|
3598
|
-
function renderNavBar(
|
|
3786
|
+
function renderNavBar(displayName, type) {
|
|
3599
3787
|
removeNavBarElement();
|
|
3600
3788
|
const el = h("div", {
|
|
3601
3789
|
id: NAV_BAR_ELEMENT_ID,
|
|
3602
3790
|
className: `ait-navbar ait-navbar-${type}`,
|
|
3603
3791
|
"aria-hidden": "true"
|
|
3604
3792
|
});
|
|
3605
|
-
el.style.top = `${preset.safeAreaTop}px`;
|
|
3606
3793
|
const moreBtn = h("button", {
|
|
3607
3794
|
className: "ait-navbar-btn",
|
|
3608
3795
|
type: "button",
|
|
@@ -3680,12 +3867,13 @@ function disposeViewport() {
|
|
|
3680
3867
|
removeNotchElement();
|
|
3681
3868
|
removeHomeIndicator();
|
|
3682
3869
|
removeNavBarElement();
|
|
3870
|
+
revertDeviceEmulation();
|
|
3683
3871
|
bodyScrollHintEmitted = false;
|
|
3684
3872
|
}
|
|
3685
3873
|
/**
|
|
3686
3874
|
* DOM에 뷰포트 제약을 적용한다.
|
|
3687
3875
|
* - `html.ait-viewport-active` 클래스로 정적 CSS(styles.ts) 활성화
|
|
3688
|
-
* - body의 width/height는 preset 값으로, navbar top offset은
|
|
3876
|
+
* - body의 width/height는 preset 값으로, navbar top offset은 notchInset으로 인라인 주입
|
|
3689
3877
|
*/
|
|
3690
3878
|
function applyViewport(state) {
|
|
3691
3879
|
if (typeof document === "undefined") return;
|
|
@@ -3700,6 +3888,7 @@ function applyViewport(state) {
|
|
|
3700
3888
|
removeNotchElement();
|
|
3701
3889
|
removeHomeIndicator();
|
|
3702
3890
|
removeNavBarElement();
|
|
3891
|
+
syncDeviceEmulation(null, false);
|
|
3703
3892
|
return;
|
|
3704
3893
|
}
|
|
3705
3894
|
if (!bodyScrollHintEmitted) {
|
|
@@ -3710,19 +3899,22 @@ function applyViewport(state) {
|
|
|
3710
3899
|
html.classList.toggle("ait-viewport-framed", state.frame);
|
|
3711
3900
|
const preset = state.preset === "custom" ? null : getPreset(state.preset);
|
|
3712
3901
|
const landscape = effectiveOrientation(state) === "landscape";
|
|
3902
|
+
syncDeviceEmulation(preset, landscape);
|
|
3903
|
+
const contentTop = preset ? computeSafeAreaInsets(preset, landscape, state.landscapeSide, state.aitNavBar, state.aitNavBarType).top : 0;
|
|
3713
3904
|
style.textContent = `
|
|
3714
3905
|
html.ait-viewport-active body {
|
|
3715
3906
|
width: ${size.width}px;
|
|
3716
3907
|
max-width: ${size.width}px;
|
|
3717
3908
|
min-height: ${size.height}px;
|
|
3718
3909
|
max-height: ${size.height}px;
|
|
3910
|
+
padding-top: ${contentTop}px;
|
|
3719
3911
|
}
|
|
3720
3912
|
`;
|
|
3721
3913
|
if (preset && state.frame && !landscape) renderNotchOverlay(preset);
|
|
3722
3914
|
else removeNotchElement();
|
|
3723
3915
|
if (preset && state.frame && !landscape && preset.safeAreaBottom > 0) renderHomeIndicator();
|
|
3724
3916
|
else removeHomeIndicator();
|
|
3725
|
-
if (preset && state.aitNavBar && !landscape) renderNavBar(
|
|
3917
|
+
if (preset && state.aitNavBar && !landscape) renderNavBar(aitState.state.brand.displayName, state.aitNavBarType);
|
|
3726
3918
|
else removeNavBarElement();
|
|
3727
3919
|
}
|
|
3728
3920
|
function isViewportPresetId(v) {
|
|
@@ -3941,13 +4133,16 @@ function renderViewportTab() {
|
|
|
3941
4133
|
const orientDisplay = vp.orientation === "auto" ? t("viewport.orientation.autoSuffix", { orient: effOrient }) : effOrient;
|
|
3942
4134
|
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}`)));
|
|
3943
4135
|
if (preset) {
|
|
3944
|
-
const insets = computeSafeAreaInsets(preset, landscape, vp.landscapeSide);
|
|
4136
|
+
const insets = computeSafeAreaInsets(preset, landscape, vp.landscapeSide, vp.aitNavBar, vp.aitNavBarType);
|
|
3945
4137
|
rows.push(h("div", { className: "ait-status-row" }, h("span", {}, t("viewport.status.safeArea")), h("span", { className: "ait-status-value" }, `T${insets.top} R${insets.right} B${insets.bottom} L${insets.left}`)));
|
|
3946
4138
|
}
|
|
3947
|
-
if (vp.aitNavBar && !landscape)
|
|
3948
|
-
|
|
3949
|
-
|
|
3950
|
-
|
|
4139
|
+
if (vp.aitNavBar && !landscape) {
|
|
4140
|
+
const navBarTop = vp.aitNavBarType === "partner" ? preset?.navBarHeight ?? 0 : 0;
|
|
4141
|
+
rows.push(h("div", { className: "ait-status-row" }, h("span", {}, t("viewport.status.aitNavBar")), h("span", { className: "ait-status-value" }, t("viewport.status.aitNavBarValue", {
|
|
4142
|
+
height: navBarTop,
|
|
4143
|
+
type: vp.aitNavBarType
|
|
4144
|
+
}))));
|
|
4145
|
+
}
|
|
3951
4146
|
for (const row of rows) statusEl.appendChild(row);
|
|
3952
4147
|
}
|
|
3953
4148
|
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));
|
|
@@ -4242,7 +4437,7 @@ function mount() {
|
|
|
4242
4437
|
mockBadge.textContent = aitState.state.panelEditable ? t("panel.editMode.on") : t("panel.editMode.off");
|
|
4243
4438
|
refreshPanel();
|
|
4244
4439
|
});
|
|
4245
|
-
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.
|
|
4440
|
+
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.32`), closeBtn);
|
|
4246
4441
|
const header = h("div", { className: "ait-panel-header" }, h("span", {}, t("panel.title")), headerRight);
|
|
4247
4442
|
tabsEl = h("div", { className: "ait-panel-tabs" });
|
|
4248
4443
|
for (const tab of getTabs()) {
|
|
@@ -4284,7 +4479,7 @@ function mount() {
|
|
|
4284
4479
|
window.addEventListener("resize", resizeHandler);
|
|
4285
4480
|
aitStateUnsubscribe = aitState.subscribe(() => {
|
|
4286
4481
|
try {
|
|
4287
|
-
if (isOpen && (currentTab === "analytics" || currentTab === "storage" || currentTab === "device" || currentTab === "viewport" || currentTab === "iap" || currentTab === "ads" || currentTab === "presets")) refreshPanel();
|
|
4482
|
+
if (isOpen && (currentTab === "env" || currentTab === "analytics" || currentTab === "storage" || currentTab === "device" || currentTab === "viewport" || currentTab === "iap" || currentTab === "ads" || currentTab === "presets")) refreshPanel();
|
|
4288
4483
|
} catch (err) {
|
|
4289
4484
|
console.error("[@ait-co/devtools] Error in subscribe callback:", err);
|
|
4290
4485
|
}
|