@oasiz/sdk 1.5.4 → 1.5.6

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
@@ -196,11 +196,32 @@ private onGameOver(): void {
196
196
 
197
197
  ### Layout
198
198
 
199
- Use the runtime safe-area value instead of hardcoded top offsets. The SDK returns the top inset as **a percentage of the viewport height (0–100)**. If the host exposes CSS pixels via `window.getSafeAreaTop()` or `window.__OASIZ_SAFE_AREA_TOP__`, the SDK converts using `window.innerHeight`. The host may instead set **`window.getSafeAreaTopPercent()`** or **`window.__OASIZ_SAFE_AREA_TOP_PERCENT__`** (0–100) and that value is used directly.
199
+ Use runtime viewport insets instead of direct CSS `env(safe-area-inset-*)` reads or hardcoded offsets. The SDK resolves host-provided Oasiz values first, then browser CSS `env(safe-area-inset-*)`, then legacy `constant(safe-area-inset-*)`, and finally `0`.
200
+
201
+ The top inset preserves the existing Oasiz game-safe top behavior: host chrome, invite UI, and leaderboard clearance can contribute to it. Left, right, and bottom are device safe-area insets today and may include future host UI obstructions.
202
+
203
+ #### `oasiz.getViewportInsets(): ViewportInsets`
204
+
205
+ Returns effective viewport insets in both CSS pixels and normalized percentages:
206
+
207
+ ```ts
208
+ const insets = oasiz.getViewportInsets();
209
+ hud.style.paddingTop = `${insets.pixels.top}px`;
210
+ hud.style.paddingRight = `${insets.pixels.right}px`;
211
+ hud.style.paddingBottom = `${insets.pixels.bottom}px`;
212
+ hud.style.paddingLeft = `${insets.pixels.left}px`;
213
+ ```
214
+
215
+ Percentages use the matching active viewport axis:
216
+
217
+ - `top` / `bottom` are percentages of viewport height
218
+ - `left` / `right` are percentages of viewport width
219
+
220
+ Hosts may expose pixels via `window.getViewportInsets()` or `window.__OASIZ_VIEWPORT_INSETS__`, for example `{ top, right, bottom, left }` or `{ pixels: { top, right, bottom, left } }`. Hosts may expose percentages via `window.getViewportInsetsPercent()`, `window.__OASIZ_VIEWPORT_INSETS_PERCENT__`, or a `percent` object. Per-side globals such as `window.__OASIZ_SAFE_AREA_BOTTOM__` are also supported.
200
221
 
201
222
  #### `oasiz.getSafeAreaTop(): number`
202
223
 
203
- Returns the top inset as a percentage of viewport height (0–100). To get pixels in JavaScript, use `(getSafeAreaTop() / 100) * window.innerHeight`. In CSS, the same value matches **`vh`** units (for example `12.5vh` for 12.5% of the viewport height). Unsupported hosts return `0`.
224
+ Legacy alias for `oasiz.getViewportInsets().percent.top`. Returns the top inset as a percentage of viewport height (0–100). To get pixels in JavaScript, prefer `oasiz.getViewportInsets().pixels.top`. In CSS, the percent value matches **`vh`** units (for example `12.5vh` for 12.5% of the viewport height). Unsupported hosts return `0`.
204
225
 
205
226
  ```ts
206
227
  const safeTopPct = oasiz.getSafeAreaTop();
@@ -211,6 +232,10 @@ document.documentElement.style.setProperty("--safe-top", `${safeTopPct}vh`);
211
232
 
212
233
  Getter alias for `getSafeAreaTop()`.
213
234
 
235
+ #### `oasiz.viewportInsets`
236
+
237
+ Getter alias for `getViewportInsets()`.
238
+
214
239
  Recommended CSS pattern:
215
240
 
216
241
  ```css
@@ -397,6 +422,7 @@ import {
397
422
  openInviteModal,
398
423
  enableLogOverlay,
399
424
  getSafeAreaTop,
425
+ getViewportInsets,
400
426
  setLeaderboardVisible,
401
427
  onPause,
402
428
  onResume,
@@ -501,7 +527,8 @@ public class GameManager : MonoBehaviour
501
527
  | `oasiz.loadGameState()` | `OasizSDK.LoadGameState()` → `Dictionary<string, object>` |
502
528
  | `oasiz.saveGameState(obj)` | `OasizSDK.SaveGameState(Dictionary<string, object>)` |
503
529
  | `oasiz.flushGameState()` | `OasizSDK.FlushGameState()` |
504
- | `oasiz.getSafeAreaTop()` / `safeAreaTop` | `OasizSDK.GetSafeAreaTop()` / `OasizSDK.SafeAreaTop` (`float`, 0–100, % of viewport height) |
530
+ | `oasiz.getViewportInsets()` / `viewportInsets` | `OasizSDK.GetViewportInsets()` (`ViewportInsets`, each side 0–100, % of matching viewport axis) |
531
+ | `oasiz.getSafeAreaTop()` / `safeAreaTop` | `OasizSDK.GetSafeAreaTop()` / `OasizSDK.SafeAreaTop` (`float`, 0–100, % of viewport height; legacy alias for top viewport inset) |
505
532
  | `oasiz.setLeaderboardVisible(v)` | `OasizSDK.SetLeaderboardVisible(bool)` |
506
533
  | `oasiz.onPause` / `onResume` | `OasizSDK.OnPause` / `OnResume` static events |
507
534
  | `oasiz.onBackButton` | `OasizSDK.OnBackButton` or `SubscribeBackButton(Action)` (reference-counts `__oasizSetBackOverride`) |
package/dist/index.cjs CHANGED
@@ -30,6 +30,7 @@ __export(index_exports, {
30
30
  getPlayerName: () => getPlayerName,
31
31
  getRoomCode: () => getRoomCode,
32
32
  getSafeAreaTop: () => getSafeAreaTop,
33
+ getViewportInsets: () => getViewportInsets,
33
34
  leaveGame: () => leaveGame,
34
35
  loadGameState: () => loadGameState,
35
36
  oasiz: () => oasiz,
@@ -1302,6 +1303,21 @@ function onResume(callback) {
1302
1303
  }
1303
1304
 
1304
1305
  // src/layout.ts
1306
+ var INSET_SIDES = ["top", "right", "bottom", "left"];
1307
+ var SIDE_TO_AXIS = {
1308
+ top: "vertical",
1309
+ right: "horizontal",
1310
+ bottom: "vertical",
1311
+ left: "horizontal"
1312
+ };
1313
+ function createInsetEdges(value) {
1314
+ return {
1315
+ top: value,
1316
+ right: value,
1317
+ bottom: value,
1318
+ left: value
1319
+ };
1320
+ }
1305
1321
  function isDevelopment9() {
1306
1322
  const nodeEnv = globalThis.process?.env?.NODE_ENV;
1307
1323
  return nodeEnv !== "production";
@@ -1319,53 +1335,279 @@ function warnMissingBridge6(methodName) {
1319
1335
  );
1320
1336
  }
1321
1337
  }
1322
- function normalizeSafeAreaTopPixels(value) {
1338
+ function isRecord(value) {
1339
+ return typeof value === "object" && value !== null;
1340
+ }
1341
+ function toFiniteNumber(value) {
1323
1342
  if (typeof value !== "number" || !Number.isFinite(value)) {
1324
- return 0;
1343
+ if (typeof value !== "string") {
1344
+ return void 0;
1345
+ }
1346
+ const parsed = Number.parseFloat(value.trim());
1347
+ if (!Number.isFinite(parsed)) {
1348
+ return void 0;
1349
+ }
1350
+ return parsed;
1325
1351
  }
1326
- return Math.max(0, value);
1352
+ return value;
1327
1353
  }
1328
- function normalizeSafeAreaTopPercent(value) {
1329
- if (typeof value !== "number" || !Number.isFinite(value)) {
1354
+ function clampInsetPixels(value) {
1355
+ const numeric = toFiniteNumber(value);
1356
+ if (typeof numeric === "undefined") {
1357
+ return void 0;
1358
+ }
1359
+ return Math.max(0, numeric);
1360
+ }
1361
+ function normalizeInsetPercent(value) {
1362
+ const numeric = toFiniteNumber(value);
1363
+ if (typeof numeric === "undefined") {
1364
+ return void 0;
1365
+ }
1366
+ return Math.min(100, Math.max(0, numeric));
1367
+ }
1368
+ function getViewportSize2(bridge, axis) {
1369
+ const visualViewportSize = axis === "vertical" ? bridge.visualViewport?.height : bridge.visualViewport?.width;
1370
+ if (typeof visualViewportSize === "number" && Number.isFinite(visualViewportSize) && visualViewportSize > 0) {
1371
+ return visualViewportSize;
1372
+ }
1373
+ const innerSize = axis === "vertical" ? bridge.innerHeight : bridge.innerWidth;
1374
+ if (typeof innerSize === "number" && Number.isFinite(innerSize) && innerSize > 0) {
1375
+ return innerSize;
1376
+ }
1377
+ const documentSize = axis === "vertical" ? bridge.document?.documentElement?.clientHeight : bridge.document?.documentElement?.clientWidth;
1378
+ if (typeof documentSize === "number" && Number.isFinite(documentSize) && documentSize > 0) {
1379
+ return documentSize;
1380
+ }
1381
+ const bodySize = axis === "vertical" ? bridge.document?.body?.clientHeight : bridge.document?.body?.clientWidth;
1382
+ if (typeof bodySize === "number" && Number.isFinite(bodySize) && bodySize > 0) {
1383
+ return bodySize;
1384
+ }
1385
+ return 0;
1386
+ }
1387
+ function readCssSafeAreaValue(bridge, cssValue) {
1388
+ const doc = bridge.document;
1389
+ const root = doc?.body ?? doc?.documentElement;
1390
+ if (!doc || !root || typeof doc.createElement !== "function" || typeof root.appendChild !== "function" || typeof bridge.getComputedStyle !== "function") {
1330
1391
  return 0;
1331
1392
  }
1332
- return Math.min(100, Math.max(0, value));
1393
+ const probe = doc.createElement("div");
1394
+ probe.style.position = "fixed";
1395
+ probe.style.top = "0";
1396
+ probe.style.left = "0";
1397
+ probe.style.width = "0";
1398
+ probe.style.height = "0";
1399
+ probe.style.visibility = "hidden";
1400
+ probe.style.pointerEvents = "none";
1401
+ probe.style.paddingTop = cssValue;
1402
+ root.appendChild(probe);
1403
+ try {
1404
+ return clampInsetPixels(bridge.getComputedStyle(probe).paddingTop) ?? 0;
1405
+ } finally {
1406
+ if (typeof probe.remove === "function") {
1407
+ probe.remove();
1408
+ } else {
1409
+ probe.parentNode?.removeChild(probe);
1410
+ }
1411
+ }
1412
+ }
1413
+ function readCssSafeAreaPixels(bridge, side) {
1414
+ const envPixels = readCssSafeAreaValue(
1415
+ bridge,
1416
+ "env(safe-area-inset-" + side + ")"
1417
+ );
1418
+ if (envPixels > 0) {
1419
+ return envPixels;
1420
+ }
1421
+ return readCssSafeAreaValue(
1422
+ bridge,
1423
+ "constant(safe-area-inset-" + side + ")"
1424
+ );
1425
+ }
1426
+ function getDevicePixelRatio(bridge) {
1427
+ const dpr = bridge.devicePixelRatio;
1428
+ if (typeof dpr !== "number" || !Number.isFinite(dpr) || dpr <= 0) {
1429
+ return 1;
1430
+ }
1431
+ return dpr;
1432
+ }
1433
+ function roughlyEqualPixels(a, b) {
1434
+ return Math.abs(a - b) <= 2;
1435
+ }
1436
+ function normalizeInsetPixels(value, bridge, side) {
1437
+ const pixels = clampInsetPixels(value);
1438
+ if (typeof pixels === "undefined") {
1439
+ return void 0;
1440
+ }
1441
+ const cssEnvPixels = readCssSafeAreaPixels(bridge, side);
1442
+ if (pixels <= 0) {
1443
+ return cssEnvPixels;
1444
+ }
1445
+ const dpr = getDevicePixelRatio(bridge);
1446
+ if (cssEnvPixels > 0 && dpr > 1 && roughlyEqualPixels(pixels / dpr, cssEnvPixels)) {
1447
+ return cssEnvPixels;
1448
+ }
1449
+ return pixels;
1333
1450
  }
1334
- function viewportInnerHeight(bridge) {
1335
- const h = bridge.innerHeight;
1336
- if (typeof h !== "number" || !Number.isFinite(h) || h <= 0) {
1451
+ function pixelsToPercentOfViewport(pixels, bridge, side) {
1452
+ const size = getViewportSize2(bridge, SIDE_TO_AXIS[side]);
1453
+ if (size <= 0) {
1337
1454
  return 0;
1338
1455
  }
1339
- return h;
1456
+ return normalizeInsetPercent(pixels / size * 100) ?? 0;
1340
1457
  }
1341
- function pixelsTopToPercentOfViewport(pixels, bridge) {
1342
- const h = viewportInnerHeight(bridge);
1343
- if (h <= 0) {
1458
+ function percentToPixelsOfViewport(percent, bridge, side) {
1459
+ const size = getViewportSize2(bridge, SIDE_TO_AXIS[side]);
1460
+ if (size <= 0) {
1344
1461
  return 0;
1345
1462
  }
1346
- return normalizeSafeAreaTopPercent(pixels / h * 100);
1463
+ return percent / 100 * size;
1347
1464
  }
1348
- function getSafeAreaTop() {
1465
+ function cssSafeAreaPercent(bridge, side) {
1466
+ return pixelsToPercentOfViewport(readCssSafeAreaPixels(bridge, side), bridge, side);
1467
+ }
1468
+ function resolvePercentValue(value, bridge, side) {
1469
+ const percent = normalizeInsetPercent(value);
1470
+ if (typeof percent === "undefined") {
1471
+ return void 0;
1472
+ }
1473
+ return percent > 0 ? percent : cssSafeAreaPercent(bridge, side);
1474
+ }
1475
+ function resolvePixelValue(value, bridge, side) {
1476
+ const numeric = toFiniteNumber(value);
1477
+ if (typeof numeric === "undefined") {
1478
+ return void 0;
1479
+ }
1480
+ return normalizeInsetPixels(numeric, bridge, side);
1481
+ }
1482
+ function sideSuffix(side) {
1483
+ switch (side) {
1484
+ case "top":
1485
+ return "Top";
1486
+ case "right":
1487
+ return "Right";
1488
+ case "bottom":
1489
+ return "Bottom";
1490
+ case "left":
1491
+ return "Left";
1492
+ }
1493
+ }
1494
+ function callBridgeFunction(bridge, name) {
1495
+ const fn = bridge[name];
1496
+ if (typeof fn !== "function") {
1497
+ return void 0;
1498
+ }
1499
+ try {
1500
+ return fn.call(bridge);
1501
+ } catch (error) {
1502
+ console.error("[oasiz/sdk] " + String(name) + " failed:", error);
1503
+ return void 0;
1504
+ }
1505
+ }
1506
+ function readInsetObjectValue(value, side, group) {
1507
+ if (!isRecord(value)) {
1508
+ return void 0;
1509
+ }
1510
+ if (group) {
1511
+ return isRecord(value[group]) ? value[group][side] : void 0;
1512
+ }
1513
+ return value[side];
1514
+ }
1515
+ function firstDefined(...values) {
1516
+ return values.find((value) => typeof value !== "undefined");
1517
+ }
1518
+ function readHostInsetSources(bridge) {
1519
+ return {
1520
+ globalPixels: bridge.__OASIZ_VIEWPORT_INSETS__,
1521
+ globalPercent: bridge.__OASIZ_VIEWPORT_INSETS_PERCENT__,
1522
+ methodPixels: callBridgeFunction(bridge, "getViewportInsets"),
1523
+ methodPercent: callBridgeFunction(bridge, "getViewportInsetsPercent")
1524
+ };
1525
+ }
1526
+ function readIndividualPercentValue(bridge, side) {
1527
+ const suffix = sideSuffix(side);
1528
+ return firstDefined(
1529
+ callBridgeFunction(
1530
+ bridge,
1531
+ "getSafeArea" + suffix + "Percent"
1532
+ ),
1533
+ bridge["__OASIZ_SAFE_AREA_" + side.toUpperCase() + "_PERCENT__"]
1534
+ );
1535
+ }
1536
+ function readIndividualPixelValue(bridge, side) {
1537
+ const suffix = sideSuffix(side);
1538
+ return firstDefined(
1539
+ callBridgeFunction(
1540
+ bridge,
1541
+ "getSafeArea" + suffix
1542
+ ),
1543
+ bridge["__OASIZ_SAFE_AREA_" + side.toUpperCase() + "__"]
1544
+ );
1545
+ }
1546
+ function resolveInsetSide(bridge, sources, side) {
1547
+ const percentCandidate = firstDefined(
1548
+ readInsetObjectValue(sources.methodPercent, side, "percent"),
1549
+ readInsetObjectValue(sources.methodPercent, side),
1550
+ readInsetObjectValue(sources.globalPercent, side, "percent"),
1551
+ readInsetObjectValue(sources.globalPercent, side),
1552
+ readInsetObjectValue(sources.methodPixels, side, "percent"),
1553
+ readInsetObjectValue(sources.globalPixels, side, "percent"),
1554
+ readIndividualPercentValue(bridge, side)
1555
+ );
1556
+ const percent = resolvePercentValue(percentCandidate, bridge, side);
1557
+ if (typeof percent !== "undefined") {
1558
+ return {
1559
+ pixels: percentToPixelsOfViewport(percent, bridge, side),
1560
+ percent
1561
+ };
1562
+ }
1563
+ const pixelCandidate = firstDefined(
1564
+ readInsetObjectValue(sources.methodPixels, side, "pixels"),
1565
+ readInsetObjectValue(sources.methodPixels, side),
1566
+ readInsetObjectValue(sources.globalPixels, side, "pixels"),
1567
+ readInsetObjectValue(sources.globalPixels, side),
1568
+ readIndividualPixelValue(bridge, side)
1569
+ );
1570
+ const pixels = resolvePixelValue(pixelCandidate, bridge, side);
1571
+ if (typeof pixels !== "undefined") {
1572
+ return {
1573
+ pixels,
1574
+ percent: pixelsToPercentOfViewport(pixels, bridge, side)
1575
+ };
1576
+ }
1577
+ const cssPixels = readCssSafeAreaPixels(bridge, side);
1578
+ return {
1579
+ pixels: cssPixels,
1580
+ percent: pixelsToPercentOfViewport(cssPixels, bridge, side)
1581
+ };
1582
+ }
1583
+ function getViewportInsets() {
1349
1584
  const bridge = getBridgeWindow8();
1350
1585
  if (!bridge) {
1351
- return 0;
1352
- }
1353
- if (typeof bridge.getSafeAreaTopPercent === "function") {
1354
- return normalizeSafeAreaTopPercent(bridge.getSafeAreaTopPercent());
1586
+ return {
1587
+ pixels: createInsetEdges(0),
1588
+ percent: createInsetEdges(0)
1589
+ };
1355
1590
  }
1356
- if (typeof bridge.__OASIZ_SAFE_AREA_TOP_PERCENT__ !== "undefined") {
1357
- return normalizeSafeAreaTopPercent(bridge.__OASIZ_SAFE_AREA_TOP_PERCENT__);
1591
+ const sources = readHostInsetSources(bridge);
1592
+ const pixels = createInsetEdges(0);
1593
+ const percent = createInsetEdges(0);
1594
+ for (const side of INSET_SIDES) {
1595
+ const resolved = resolveInsetSide(bridge, sources, side);
1596
+ pixels[side] = resolved.pixels;
1597
+ percent[side] = resolved.percent;
1358
1598
  }
1359
- if (typeof bridge.getSafeAreaTop === "function") {
1360
- const px = normalizeSafeAreaTopPixels(bridge.getSafeAreaTop());
1361
- return pixelsTopToPercentOfViewport(px, bridge);
1599
+ return { pixels, percent };
1600
+ }
1601
+ function getSafeAreaTop() {
1602
+ const bridge = getBridgeWindow8();
1603
+ if (!bridge) {
1604
+ return 0;
1362
1605
  }
1363
- if (typeof bridge.__OASIZ_SAFE_AREA_TOP__ !== "undefined") {
1364
- const px = normalizeSafeAreaTopPixels(bridge.__OASIZ_SAFE_AREA_TOP__);
1365
- return pixelsTopToPercentOfViewport(px, bridge);
1606
+ const top = getViewportInsets().percent.top;
1607
+ if (top <= 0) {
1608
+ warnMissingBridge6("getSafeAreaTop");
1366
1609
  }
1367
- warnMissingBridge6("getSafeAreaTop");
1368
- return 0;
1610
+ return top;
1369
1611
  }
1370
1612
  function setLeaderboardVisible(visible) {
1371
1613
  if (typeof visible !== "boolean") {
@@ -1486,6 +1728,7 @@ var oasiz = {
1486
1728
  onPause,
1487
1729
  onResume,
1488
1730
  getSafeAreaTop,
1731
+ getViewportInsets,
1489
1732
  setLeaderboardVisible,
1490
1733
  onBackButton,
1491
1734
  onLeaveGame,
@@ -1507,6 +1750,9 @@ var oasiz = {
1507
1750
  },
1508
1751
  get safeAreaTop() {
1509
1752
  return getSafeAreaTop();
1753
+ },
1754
+ get viewportInsets() {
1755
+ return getViewportInsets();
1510
1756
  }
1511
1757
  };
1512
1758
  // Annotate the CommonJS export names for ESM import in node:
@@ -1521,6 +1767,7 @@ var oasiz = {
1521
1767
  getPlayerName,
1522
1768
  getRoomCode,
1523
1769
  getSafeAreaTop,
1770
+ getViewportInsets,
1524
1771
  leaveGame,
1525
1772
  loadGameState,
1526
1773
  oasiz,
package/dist/index.d.cts CHANGED
@@ -1,3 +1,28 @@
1
+ type ViewportInsetSide = "top" | "right" | "bottom" | "left";
2
+ interface ViewportInsetEdges {
3
+ top: number;
4
+ right: number;
5
+ bottom: number;
6
+ left: number;
7
+ }
8
+ interface ViewportInsets {
9
+ pixels: ViewportInsetEdges;
10
+ percent: ViewportInsetEdges;
11
+ }
12
+ /**
13
+ * Effective viewport insets that game UI should avoid.
14
+ *
15
+ * `top` preserves the existing Oasiz game-safe top behavior: host chrome and
16
+ * invite/leaderboard clearance can contribute to it. Other sides are device
17
+ * safe-area insets today, and can include future host UI obstructions.
18
+ */
19
+ declare function getViewportInsets(): ViewportInsets;
20
+ /**
21
+ * Legacy alias for the top entry of `getViewportInsets().percent`.
22
+ */
23
+ declare function getSafeAreaTop(): number;
24
+ declare function setLeaderboardVisible(visible: boolean): void;
25
+
1
26
  type HapticType = "light" | "medium" | "heavy" | "success" | "error";
2
27
  type LogOverlayLevel = "debug" | "log" | "info" | "warn" | "error";
3
28
  interface LogOverlayEntry {
@@ -196,15 +221,6 @@ type Unsubscribe = () => void;
196
221
  declare function onPause(callback: () => void): Unsubscribe;
197
222
  declare function onResume(callback: () => void): Unsubscribe;
198
223
 
199
- /**
200
- * Top safe-area inset as a percentage of the viewport height (0–100).
201
- * The host may expose CSS pixels via `getSafeAreaTop` / `__OASIZ_SAFE_AREA_TOP__`
202
- * (converted using `window.innerHeight`), or percentages via
203
- * `getSafeAreaTopPercent` / `__OASIZ_SAFE_AREA_TOP_PERCENT__`.
204
- */
205
- declare function getSafeAreaTop(): number;
206
- declare function setLeaderboardVisible(visible: boolean): void;
207
-
208
224
  declare function onBackButton(callback: () => void): Unsubscribe;
209
225
  declare function onLeaveGame(callback: () => void): Unsubscribe;
210
226
  declare function leaveGame(): void;
@@ -225,6 +241,7 @@ declare const oasiz: {
225
241
  onPause: typeof onPause;
226
242
  onResume: typeof onResume;
227
243
  getSafeAreaTop: typeof getSafeAreaTop;
244
+ getViewportInsets: typeof getViewportInsets;
228
245
  setLeaderboardVisible: typeof setLeaderboardVisible;
229
246
  onBackButton: typeof onBackButton;
230
247
  onLeaveGame: typeof onLeaveGame;
@@ -235,6 +252,7 @@ declare const oasiz: {
235
252
  readonly playerName: string | undefined;
236
253
  readonly playerAvatar: string | undefined;
237
254
  readonly safeAreaTop: number;
255
+ readonly viewportInsets: ViewportInsets;
238
256
  };
239
257
 
240
- export { type FacingFrameMap, type GameState, type HapticType, type LogOverlayEntry, type LogOverlayHandle, type LogOverlayLevel, type LogOverlayOptions, type PlayerCharacter, type ScoreEditResult, type ShareRequest, type ShareRoomCodeOptions, type TextureAtlas, type TextureAtlasAnimation, type TextureAtlasFrame, type Unsubscribe, addScore, enableLogOverlay, flushGameState, getGameId, getPlayerAvatar, getPlayerCharacter, getPlayerId, getPlayerName, getRoomCode, getSafeAreaTop, leaveGame, loadGameState, oasiz, onBackButton, onLeaveGame, onPause, onResume, openInviteModal, saveGameState, setLeaderboardVisible, setScore, share, shareRoomCode, submitScore, triggerHaptic };
258
+ export { type FacingFrameMap, type GameState, type HapticType, type LogOverlayEntry, type LogOverlayHandle, type LogOverlayLevel, type LogOverlayOptions, type PlayerCharacter, type ScoreEditResult, type ShareRequest, type ShareRoomCodeOptions, type TextureAtlas, type TextureAtlasAnimation, type TextureAtlasFrame, type Unsubscribe, type ViewportInsetEdges, type ViewportInsetSide, type ViewportInsets, addScore, enableLogOverlay, flushGameState, getGameId, getPlayerAvatar, getPlayerCharacter, getPlayerId, getPlayerName, getRoomCode, getSafeAreaTop, getViewportInsets, leaveGame, loadGameState, oasiz, onBackButton, onLeaveGame, onPause, onResume, openInviteModal, saveGameState, setLeaderboardVisible, setScore, share, shareRoomCode, submitScore, triggerHaptic };
package/dist/index.d.ts CHANGED
@@ -1,3 +1,28 @@
1
+ type ViewportInsetSide = "top" | "right" | "bottom" | "left";
2
+ interface ViewportInsetEdges {
3
+ top: number;
4
+ right: number;
5
+ bottom: number;
6
+ left: number;
7
+ }
8
+ interface ViewportInsets {
9
+ pixels: ViewportInsetEdges;
10
+ percent: ViewportInsetEdges;
11
+ }
12
+ /**
13
+ * Effective viewport insets that game UI should avoid.
14
+ *
15
+ * `top` preserves the existing Oasiz game-safe top behavior: host chrome and
16
+ * invite/leaderboard clearance can contribute to it. Other sides are device
17
+ * safe-area insets today, and can include future host UI obstructions.
18
+ */
19
+ declare function getViewportInsets(): ViewportInsets;
20
+ /**
21
+ * Legacy alias for the top entry of `getViewportInsets().percent`.
22
+ */
23
+ declare function getSafeAreaTop(): number;
24
+ declare function setLeaderboardVisible(visible: boolean): void;
25
+
1
26
  type HapticType = "light" | "medium" | "heavy" | "success" | "error";
2
27
  type LogOverlayLevel = "debug" | "log" | "info" | "warn" | "error";
3
28
  interface LogOverlayEntry {
@@ -196,15 +221,6 @@ type Unsubscribe = () => void;
196
221
  declare function onPause(callback: () => void): Unsubscribe;
197
222
  declare function onResume(callback: () => void): Unsubscribe;
198
223
 
199
- /**
200
- * Top safe-area inset as a percentage of the viewport height (0–100).
201
- * The host may expose CSS pixels via `getSafeAreaTop` / `__OASIZ_SAFE_AREA_TOP__`
202
- * (converted using `window.innerHeight`), or percentages via
203
- * `getSafeAreaTopPercent` / `__OASIZ_SAFE_AREA_TOP_PERCENT__`.
204
- */
205
- declare function getSafeAreaTop(): number;
206
- declare function setLeaderboardVisible(visible: boolean): void;
207
-
208
224
  declare function onBackButton(callback: () => void): Unsubscribe;
209
225
  declare function onLeaveGame(callback: () => void): Unsubscribe;
210
226
  declare function leaveGame(): void;
@@ -225,6 +241,7 @@ declare const oasiz: {
225
241
  onPause: typeof onPause;
226
242
  onResume: typeof onResume;
227
243
  getSafeAreaTop: typeof getSafeAreaTop;
244
+ getViewportInsets: typeof getViewportInsets;
228
245
  setLeaderboardVisible: typeof setLeaderboardVisible;
229
246
  onBackButton: typeof onBackButton;
230
247
  onLeaveGame: typeof onLeaveGame;
@@ -235,6 +252,7 @@ declare const oasiz: {
235
252
  readonly playerName: string | undefined;
236
253
  readonly playerAvatar: string | undefined;
237
254
  readonly safeAreaTop: number;
255
+ readonly viewportInsets: ViewportInsets;
238
256
  };
239
257
 
240
- export { type FacingFrameMap, type GameState, type HapticType, type LogOverlayEntry, type LogOverlayHandle, type LogOverlayLevel, type LogOverlayOptions, type PlayerCharacter, type ScoreEditResult, type ShareRequest, type ShareRoomCodeOptions, type TextureAtlas, type TextureAtlasAnimation, type TextureAtlasFrame, type Unsubscribe, addScore, enableLogOverlay, flushGameState, getGameId, getPlayerAvatar, getPlayerCharacter, getPlayerId, getPlayerName, getRoomCode, getSafeAreaTop, leaveGame, loadGameState, oasiz, onBackButton, onLeaveGame, onPause, onResume, openInviteModal, saveGameState, setLeaderboardVisible, setScore, share, shareRoomCode, submitScore, triggerHaptic };
258
+ export { type FacingFrameMap, type GameState, type HapticType, type LogOverlayEntry, type LogOverlayHandle, type LogOverlayLevel, type LogOverlayOptions, type PlayerCharacter, type ScoreEditResult, type ShareRequest, type ShareRoomCodeOptions, type TextureAtlas, type TextureAtlasAnimation, type TextureAtlasFrame, type Unsubscribe, type ViewportInsetEdges, type ViewportInsetSide, type ViewportInsets, addScore, enableLogOverlay, flushGameState, getGameId, getPlayerAvatar, getPlayerCharacter, getPlayerId, getPlayerName, getRoomCode, getSafeAreaTop, getViewportInsets, leaveGame, loadGameState, oasiz, onBackButton, onLeaveGame, onPause, onResume, openInviteModal, saveGameState, setLeaderboardVisible, setScore, share, shareRoomCode, submitScore, triggerHaptic };
package/dist/index.js CHANGED
@@ -1252,6 +1252,21 @@ function onResume(callback) {
1252
1252
  }
1253
1253
 
1254
1254
  // src/layout.ts
1255
+ var INSET_SIDES = ["top", "right", "bottom", "left"];
1256
+ var SIDE_TO_AXIS = {
1257
+ top: "vertical",
1258
+ right: "horizontal",
1259
+ bottom: "vertical",
1260
+ left: "horizontal"
1261
+ };
1262
+ function createInsetEdges(value) {
1263
+ return {
1264
+ top: value,
1265
+ right: value,
1266
+ bottom: value,
1267
+ left: value
1268
+ };
1269
+ }
1255
1270
  function isDevelopment9() {
1256
1271
  const nodeEnv = globalThis.process?.env?.NODE_ENV;
1257
1272
  return nodeEnv !== "production";
@@ -1269,53 +1284,279 @@ function warnMissingBridge6(methodName) {
1269
1284
  );
1270
1285
  }
1271
1286
  }
1272
- function normalizeSafeAreaTopPixels(value) {
1287
+ function isRecord(value) {
1288
+ return typeof value === "object" && value !== null;
1289
+ }
1290
+ function toFiniteNumber(value) {
1273
1291
  if (typeof value !== "number" || !Number.isFinite(value)) {
1274
- return 0;
1292
+ if (typeof value !== "string") {
1293
+ return void 0;
1294
+ }
1295
+ const parsed = Number.parseFloat(value.trim());
1296
+ if (!Number.isFinite(parsed)) {
1297
+ return void 0;
1298
+ }
1299
+ return parsed;
1275
1300
  }
1276
- return Math.max(0, value);
1301
+ return value;
1277
1302
  }
1278
- function normalizeSafeAreaTopPercent(value) {
1279
- if (typeof value !== "number" || !Number.isFinite(value)) {
1303
+ function clampInsetPixels(value) {
1304
+ const numeric = toFiniteNumber(value);
1305
+ if (typeof numeric === "undefined") {
1306
+ return void 0;
1307
+ }
1308
+ return Math.max(0, numeric);
1309
+ }
1310
+ function normalizeInsetPercent(value) {
1311
+ const numeric = toFiniteNumber(value);
1312
+ if (typeof numeric === "undefined") {
1313
+ return void 0;
1314
+ }
1315
+ return Math.min(100, Math.max(0, numeric));
1316
+ }
1317
+ function getViewportSize2(bridge, axis) {
1318
+ const visualViewportSize = axis === "vertical" ? bridge.visualViewport?.height : bridge.visualViewport?.width;
1319
+ if (typeof visualViewportSize === "number" && Number.isFinite(visualViewportSize) && visualViewportSize > 0) {
1320
+ return visualViewportSize;
1321
+ }
1322
+ const innerSize = axis === "vertical" ? bridge.innerHeight : bridge.innerWidth;
1323
+ if (typeof innerSize === "number" && Number.isFinite(innerSize) && innerSize > 0) {
1324
+ return innerSize;
1325
+ }
1326
+ const documentSize = axis === "vertical" ? bridge.document?.documentElement?.clientHeight : bridge.document?.documentElement?.clientWidth;
1327
+ if (typeof documentSize === "number" && Number.isFinite(documentSize) && documentSize > 0) {
1328
+ return documentSize;
1329
+ }
1330
+ const bodySize = axis === "vertical" ? bridge.document?.body?.clientHeight : bridge.document?.body?.clientWidth;
1331
+ if (typeof bodySize === "number" && Number.isFinite(bodySize) && bodySize > 0) {
1332
+ return bodySize;
1333
+ }
1334
+ return 0;
1335
+ }
1336
+ function readCssSafeAreaValue(bridge, cssValue) {
1337
+ const doc = bridge.document;
1338
+ const root = doc?.body ?? doc?.documentElement;
1339
+ if (!doc || !root || typeof doc.createElement !== "function" || typeof root.appendChild !== "function" || typeof bridge.getComputedStyle !== "function") {
1280
1340
  return 0;
1281
1341
  }
1282
- return Math.min(100, Math.max(0, value));
1342
+ const probe = doc.createElement("div");
1343
+ probe.style.position = "fixed";
1344
+ probe.style.top = "0";
1345
+ probe.style.left = "0";
1346
+ probe.style.width = "0";
1347
+ probe.style.height = "0";
1348
+ probe.style.visibility = "hidden";
1349
+ probe.style.pointerEvents = "none";
1350
+ probe.style.paddingTop = cssValue;
1351
+ root.appendChild(probe);
1352
+ try {
1353
+ return clampInsetPixels(bridge.getComputedStyle(probe).paddingTop) ?? 0;
1354
+ } finally {
1355
+ if (typeof probe.remove === "function") {
1356
+ probe.remove();
1357
+ } else {
1358
+ probe.parentNode?.removeChild(probe);
1359
+ }
1360
+ }
1361
+ }
1362
+ function readCssSafeAreaPixels(bridge, side) {
1363
+ const envPixels = readCssSafeAreaValue(
1364
+ bridge,
1365
+ "env(safe-area-inset-" + side + ")"
1366
+ );
1367
+ if (envPixels > 0) {
1368
+ return envPixels;
1369
+ }
1370
+ return readCssSafeAreaValue(
1371
+ bridge,
1372
+ "constant(safe-area-inset-" + side + ")"
1373
+ );
1374
+ }
1375
+ function getDevicePixelRatio(bridge) {
1376
+ const dpr = bridge.devicePixelRatio;
1377
+ if (typeof dpr !== "number" || !Number.isFinite(dpr) || dpr <= 0) {
1378
+ return 1;
1379
+ }
1380
+ return dpr;
1381
+ }
1382
+ function roughlyEqualPixels(a, b) {
1383
+ return Math.abs(a - b) <= 2;
1384
+ }
1385
+ function normalizeInsetPixels(value, bridge, side) {
1386
+ const pixels = clampInsetPixels(value);
1387
+ if (typeof pixels === "undefined") {
1388
+ return void 0;
1389
+ }
1390
+ const cssEnvPixels = readCssSafeAreaPixels(bridge, side);
1391
+ if (pixels <= 0) {
1392
+ return cssEnvPixels;
1393
+ }
1394
+ const dpr = getDevicePixelRatio(bridge);
1395
+ if (cssEnvPixels > 0 && dpr > 1 && roughlyEqualPixels(pixels / dpr, cssEnvPixels)) {
1396
+ return cssEnvPixels;
1397
+ }
1398
+ return pixels;
1283
1399
  }
1284
- function viewportInnerHeight(bridge) {
1285
- const h = bridge.innerHeight;
1286
- if (typeof h !== "number" || !Number.isFinite(h) || h <= 0) {
1400
+ function pixelsToPercentOfViewport(pixels, bridge, side) {
1401
+ const size = getViewportSize2(bridge, SIDE_TO_AXIS[side]);
1402
+ if (size <= 0) {
1287
1403
  return 0;
1288
1404
  }
1289
- return h;
1405
+ return normalizeInsetPercent(pixels / size * 100) ?? 0;
1290
1406
  }
1291
- function pixelsTopToPercentOfViewport(pixels, bridge) {
1292
- const h = viewportInnerHeight(bridge);
1293
- if (h <= 0) {
1407
+ function percentToPixelsOfViewport(percent, bridge, side) {
1408
+ const size = getViewportSize2(bridge, SIDE_TO_AXIS[side]);
1409
+ if (size <= 0) {
1294
1410
  return 0;
1295
1411
  }
1296
- return normalizeSafeAreaTopPercent(pixels / h * 100);
1412
+ return percent / 100 * size;
1297
1413
  }
1298
- function getSafeAreaTop() {
1414
+ function cssSafeAreaPercent(bridge, side) {
1415
+ return pixelsToPercentOfViewport(readCssSafeAreaPixels(bridge, side), bridge, side);
1416
+ }
1417
+ function resolvePercentValue(value, bridge, side) {
1418
+ const percent = normalizeInsetPercent(value);
1419
+ if (typeof percent === "undefined") {
1420
+ return void 0;
1421
+ }
1422
+ return percent > 0 ? percent : cssSafeAreaPercent(bridge, side);
1423
+ }
1424
+ function resolvePixelValue(value, bridge, side) {
1425
+ const numeric = toFiniteNumber(value);
1426
+ if (typeof numeric === "undefined") {
1427
+ return void 0;
1428
+ }
1429
+ return normalizeInsetPixels(numeric, bridge, side);
1430
+ }
1431
+ function sideSuffix(side) {
1432
+ switch (side) {
1433
+ case "top":
1434
+ return "Top";
1435
+ case "right":
1436
+ return "Right";
1437
+ case "bottom":
1438
+ return "Bottom";
1439
+ case "left":
1440
+ return "Left";
1441
+ }
1442
+ }
1443
+ function callBridgeFunction(bridge, name) {
1444
+ const fn = bridge[name];
1445
+ if (typeof fn !== "function") {
1446
+ return void 0;
1447
+ }
1448
+ try {
1449
+ return fn.call(bridge);
1450
+ } catch (error) {
1451
+ console.error("[oasiz/sdk] " + String(name) + " failed:", error);
1452
+ return void 0;
1453
+ }
1454
+ }
1455
+ function readInsetObjectValue(value, side, group) {
1456
+ if (!isRecord(value)) {
1457
+ return void 0;
1458
+ }
1459
+ if (group) {
1460
+ return isRecord(value[group]) ? value[group][side] : void 0;
1461
+ }
1462
+ return value[side];
1463
+ }
1464
+ function firstDefined(...values) {
1465
+ return values.find((value) => typeof value !== "undefined");
1466
+ }
1467
+ function readHostInsetSources(bridge) {
1468
+ return {
1469
+ globalPixels: bridge.__OASIZ_VIEWPORT_INSETS__,
1470
+ globalPercent: bridge.__OASIZ_VIEWPORT_INSETS_PERCENT__,
1471
+ methodPixels: callBridgeFunction(bridge, "getViewportInsets"),
1472
+ methodPercent: callBridgeFunction(bridge, "getViewportInsetsPercent")
1473
+ };
1474
+ }
1475
+ function readIndividualPercentValue(bridge, side) {
1476
+ const suffix = sideSuffix(side);
1477
+ return firstDefined(
1478
+ callBridgeFunction(
1479
+ bridge,
1480
+ "getSafeArea" + suffix + "Percent"
1481
+ ),
1482
+ bridge["__OASIZ_SAFE_AREA_" + side.toUpperCase() + "_PERCENT__"]
1483
+ );
1484
+ }
1485
+ function readIndividualPixelValue(bridge, side) {
1486
+ const suffix = sideSuffix(side);
1487
+ return firstDefined(
1488
+ callBridgeFunction(
1489
+ bridge,
1490
+ "getSafeArea" + suffix
1491
+ ),
1492
+ bridge["__OASIZ_SAFE_AREA_" + side.toUpperCase() + "__"]
1493
+ );
1494
+ }
1495
+ function resolveInsetSide(bridge, sources, side) {
1496
+ const percentCandidate = firstDefined(
1497
+ readInsetObjectValue(sources.methodPercent, side, "percent"),
1498
+ readInsetObjectValue(sources.methodPercent, side),
1499
+ readInsetObjectValue(sources.globalPercent, side, "percent"),
1500
+ readInsetObjectValue(sources.globalPercent, side),
1501
+ readInsetObjectValue(sources.methodPixels, side, "percent"),
1502
+ readInsetObjectValue(sources.globalPixels, side, "percent"),
1503
+ readIndividualPercentValue(bridge, side)
1504
+ );
1505
+ const percent = resolvePercentValue(percentCandidate, bridge, side);
1506
+ if (typeof percent !== "undefined") {
1507
+ return {
1508
+ pixels: percentToPixelsOfViewport(percent, bridge, side),
1509
+ percent
1510
+ };
1511
+ }
1512
+ const pixelCandidate = firstDefined(
1513
+ readInsetObjectValue(sources.methodPixels, side, "pixels"),
1514
+ readInsetObjectValue(sources.methodPixels, side),
1515
+ readInsetObjectValue(sources.globalPixels, side, "pixels"),
1516
+ readInsetObjectValue(sources.globalPixels, side),
1517
+ readIndividualPixelValue(bridge, side)
1518
+ );
1519
+ const pixels = resolvePixelValue(pixelCandidate, bridge, side);
1520
+ if (typeof pixels !== "undefined") {
1521
+ return {
1522
+ pixels,
1523
+ percent: pixelsToPercentOfViewport(pixels, bridge, side)
1524
+ };
1525
+ }
1526
+ const cssPixels = readCssSafeAreaPixels(bridge, side);
1527
+ return {
1528
+ pixels: cssPixels,
1529
+ percent: pixelsToPercentOfViewport(cssPixels, bridge, side)
1530
+ };
1531
+ }
1532
+ function getViewportInsets() {
1299
1533
  const bridge = getBridgeWindow8();
1300
1534
  if (!bridge) {
1301
- return 0;
1302
- }
1303
- if (typeof bridge.getSafeAreaTopPercent === "function") {
1304
- return normalizeSafeAreaTopPercent(bridge.getSafeAreaTopPercent());
1535
+ return {
1536
+ pixels: createInsetEdges(0),
1537
+ percent: createInsetEdges(0)
1538
+ };
1305
1539
  }
1306
- if (typeof bridge.__OASIZ_SAFE_AREA_TOP_PERCENT__ !== "undefined") {
1307
- return normalizeSafeAreaTopPercent(bridge.__OASIZ_SAFE_AREA_TOP_PERCENT__);
1540
+ const sources = readHostInsetSources(bridge);
1541
+ const pixels = createInsetEdges(0);
1542
+ const percent = createInsetEdges(0);
1543
+ for (const side of INSET_SIDES) {
1544
+ const resolved = resolveInsetSide(bridge, sources, side);
1545
+ pixels[side] = resolved.pixels;
1546
+ percent[side] = resolved.percent;
1308
1547
  }
1309
- if (typeof bridge.getSafeAreaTop === "function") {
1310
- const px = normalizeSafeAreaTopPixels(bridge.getSafeAreaTop());
1311
- return pixelsTopToPercentOfViewport(px, bridge);
1548
+ return { pixels, percent };
1549
+ }
1550
+ function getSafeAreaTop() {
1551
+ const bridge = getBridgeWindow8();
1552
+ if (!bridge) {
1553
+ return 0;
1312
1554
  }
1313
- if (typeof bridge.__OASIZ_SAFE_AREA_TOP__ !== "undefined") {
1314
- const px = normalizeSafeAreaTopPixels(bridge.__OASIZ_SAFE_AREA_TOP__);
1315
- return pixelsTopToPercentOfViewport(px, bridge);
1555
+ const top = getViewportInsets().percent.top;
1556
+ if (top <= 0) {
1557
+ warnMissingBridge6("getSafeAreaTop");
1316
1558
  }
1317
- warnMissingBridge6("getSafeAreaTop");
1318
- return 0;
1559
+ return top;
1319
1560
  }
1320
1561
  function setLeaderboardVisible(visible) {
1321
1562
  if (typeof visible !== "boolean") {
@@ -1436,6 +1677,7 @@ var oasiz = {
1436
1677
  onPause,
1437
1678
  onResume,
1438
1679
  getSafeAreaTop,
1680
+ getViewportInsets,
1439
1681
  setLeaderboardVisible,
1440
1682
  onBackButton,
1441
1683
  onLeaveGame,
@@ -1457,6 +1699,9 @@ var oasiz = {
1457
1699
  },
1458
1700
  get safeAreaTop() {
1459
1701
  return getSafeAreaTop();
1702
+ },
1703
+ get viewportInsets() {
1704
+ return getViewportInsets();
1460
1705
  }
1461
1706
  };
1462
1707
  export {
@@ -1470,6 +1715,7 @@ export {
1470
1715
  getPlayerName,
1471
1716
  getRoomCode,
1472
1717
  getSafeAreaTop,
1718
+ getViewportInsets,
1473
1719
  leaveGame,
1474
1720
  loadGameState,
1475
1721
  oasiz,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oasiz/sdk",
3
- "version": "1.5.4",
3
+ "version": "1.5.6",
4
4
  "description": "Typed SDK for Oasiz game platform bridge APIs.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",