@oasiz/sdk 1.6.2 → 1.8.0

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/dist/index.cjs CHANGED
@@ -20,2444 +20,125 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
20
20
  // src/index.ts
21
21
  var index_exports = {};
22
22
  __export(index_exports, {
23
- addScore: () => addScore,
24
- enableAppSimulator: () => enableAppSimulator,
25
- enableBackButtonTesting: () => enableBackButtonTesting,
26
- enableLogOverlay: () => enableLogOverlay,
23
+ consume: () => consume,
24
+ emitScoreConfig: () => emitScoreConfig,
27
25
  flushGameState: () => flushGameState,
26
+ getEntitlements: () => getEntitlements,
28
27
  getGameId: () => getGameId,
29
- getGraphicsPerformance: () => getGraphicsPerformance,
28
+ getJemBalance: () => getJemBalance,
30
29
  getPlayerAvatar: () => getPlayerAvatar,
31
- getPlayerCharacter: () => getPlayerCharacter,
32
- getPlayerId: () => getPlayerId,
33
30
  getPlayerName: () => getPlayerName,
31
+ getProducts: () => getProducts,
32
+ getQuantity: () => getQuantity,
34
33
  getRoomCode: () => getRoomCode,
35
- getSafeAreaTop: () => getSafeAreaTop,
36
- getViewportInsets: () => getViewportInsets,
34
+ hasEntitlement: () => hasEntitlement,
37
35
  leaveGame: () => leaveGame,
38
36
  loadGameState: () => loadGameState,
39
37
  oasiz: () => oasiz,
40
38
  onBackButton: () => onBackButton,
39
+ onEntitlementsChanged: () => onEntitlementsChanged,
40
+ onJemBalanceChanged: () => onJemBalanceChanged,
41
41
  onLeaveGame: () => onLeaveGame,
42
42
  onPause: () => onPause,
43
43
  onResume: () => onResume,
44
- openInviteModal: () => openInviteModal,
44
+ purchase: () => purchase,
45
45
  saveGameState: () => saveGameState,
46
- setLeaderboardVisible: () => setLeaderboardVisible,
47
- setScore: () => setScore,
48
46
  share: () => share,
49
47
  shareRoomCode: () => shareRoomCode,
50
48
  submitScore: () => submitScore,
49
+ syncProducts: () => syncProducts,
51
50
  triggerHaptic: () => triggerHaptic
52
51
  });
53
52
  module.exports = __toCommonJS(index_exports);
54
53
 
55
- // src/navigation.ts
56
- var BACK_BUTTON_TEST_STATE_KEY = "__oasizBackButtonTest";
57
- var activeBackListeners = 0;
58
- var activeBackButtonTestingHandle;
59
- function isDevelopment() {
60
- const nodeEnv = globalThis.process?.env?.NODE_ENV;
61
- return nodeEnv !== "production";
62
- }
63
- function getBridgeWindow() {
64
- if (typeof window === "undefined") {
65
- return void 0;
66
- }
67
- return window;
68
- }
69
- function warnMissingBridge(methodName) {
70
- if (isDevelopment()) {
71
- console.warn(
72
- "[oasiz/sdk] " + methodName + " bridge is unavailable. This is expected in local development."
73
- );
74
- }
75
- }
76
- function normalizeNavigationError(error) {
77
- if (error instanceof Error) {
78
- return error;
79
- }
80
- return new Error(
81
- typeof error === "string" ? error : "Back button callback failed."
82
- );
83
- }
84
- function isRecord(value) {
85
- return typeof value === "object" && value !== null;
86
- }
87
- function dispatchNavigationEvent(eventName) {
88
- const bridge = getBridgeWindow();
89
- if (!bridge || typeof bridge.dispatchEvent !== "function") {
90
- return;
91
- }
92
- bridge.dispatchEvent(new Event(eventName));
93
- }
94
- function addNavigationListener(eventName, callback) {
95
- if (typeof window === "undefined") {
96
- if (isDevelopment()) {
97
- console.warn(
98
- "[oasiz/sdk] " + eventName + " listener registered without a browser window. This is expected in local development."
99
- );
100
- }
101
- return () => {
102
- };
103
- }
104
- const handler = () => callback();
105
- window.addEventListener(eventName, handler);
106
- return () => window.removeEventListener(eventName, handler);
107
- }
108
- function enableBackButtonTesting(options = {}) {
109
- activeBackButtonTestingHandle?.destroy();
110
- const bridge = getBridgeWindow();
111
- if (!bridge) {
112
- if (isDevelopment()) {
113
- console.warn(
114
- "[oasiz/sdk] enableBackButtonTesting requires a browser window."
115
- );
116
- }
117
- return {
118
- destroy: () => {
119
- },
120
- isBackOverrideActive: () => false,
121
- triggerBack: () => {
122
- },
123
- triggerLeave: () => {
124
- }
125
- };
126
- }
127
- const bridgeWindow = bridge;
128
- const keyboard = options.keyboard ?? true;
129
- const browserHistory = options.browserHistory ?? true;
130
- const log = options.log === true;
131
- const previousSetBackOverride = bridgeWindow.__oasizSetBackOverride;
132
- const previousLeaveGame = bridgeWindow.__oasizLeaveGame;
133
- let destroyed = false;
134
- let backOverrideActive = false;
135
- let historyTrapArmed = false;
136
- function maybeLog(message) {
137
- if (log) {
138
- console.info("[oasiz/sdk] " + message);
139
- }
140
- }
141
- function canUseHistoryTrap() {
142
- return browserHistory && typeof bridgeWindow.history?.pushState === "function" && typeof bridgeWindow.history?.replaceState === "function" && typeof bridgeWindow.location?.href === "string";
143
- }
144
- function ensureHistoryTrap() {
145
- if (!backOverrideActive || historyTrapArmed || !canUseHistoryTrap()) {
146
- return;
147
- }
148
- try {
149
- const currentState = isRecord(bridgeWindow.history.state) ? bridgeWindow.history.state : {};
150
- bridgeWindow.history.replaceState(
151
- { ...currentState, [BACK_BUTTON_TEST_STATE_KEY]: "base" },
152
- "",
153
- bridgeWindow.location.href
154
- );
155
- bridgeWindow.history.pushState(
156
- { [BACK_BUTTON_TEST_STATE_KEY]: "trap" },
157
- "",
158
- bridgeWindow.location.href
159
- );
160
- historyTrapArmed = true;
161
- maybeLog("Local browser Back testing is armed.");
162
- } catch (error) {
163
- historyTrapArmed = false;
164
- if (log) {
165
- console.warn("[oasiz/sdk] Failed to arm browser Back testing:", error);
166
- }
167
- }
168
- }
169
- function triggerBack() {
170
- dispatchNavigationEvent("oasiz:back");
171
- }
172
- function triggerLeave() {
173
- dispatchNavigationEvent("oasiz:leave");
174
- }
175
- function setBackOverride(active) {
176
- backOverrideActive = active;
177
- if (active) {
178
- ensureHistoryTrap();
179
- }
180
- if (typeof previousSetBackOverride === "function") {
181
- previousSetBackOverride(active);
182
- }
183
- maybeLog("Back override " + (active ? "enabled" : "disabled") + ".");
184
- }
185
- function stopBackEvent(event) {
186
- event.preventDefault();
187
- event.stopPropagation();
188
- event.stopImmediatePropagation?.();
189
- }
190
- const handleKeyDown = (event) => {
191
- if (!backOverrideActive || event.key !== "Escape") {
192
- return;
193
- }
194
- stopBackEvent(event);
195
- triggerBack();
196
- };
197
- const handlePopState = (event) => {
198
- if (!backOverrideActive) {
199
- historyTrapArmed = false;
200
- return;
201
- }
202
- stopBackEvent(event);
203
- triggerBack();
204
- historyTrapArmed = false;
205
- ensureHistoryTrap();
206
- };
207
- const testLeaveGame = () => {
208
- triggerLeave();
209
- if (typeof previousLeaveGame === "function") {
210
- previousLeaveGame();
211
- }
212
- };
213
- bridgeWindow.__oasizSetBackOverride = setBackOverride;
214
- bridgeWindow.__oasizLeaveGame = testLeaveGame;
215
- if (keyboard) {
216
- bridgeWindow.addEventListener("keydown", handleKeyDown);
217
- }
218
- if (browserHistory) {
219
- bridgeWindow.addEventListener("popstate", handlePopState);
220
- }
221
- if (activeBackListeners > 0) {
222
- setBackOverride(true);
223
- }
224
- maybeLog("Back button testing bridge installed.");
225
- const handle = {
226
- destroy: () => {
227
- if (destroyed) return;
228
- destroyed = true;
229
- if (keyboard) {
230
- bridgeWindow.removeEventListener("keydown", handleKeyDown);
231
- }
232
- if (browserHistory) {
233
- bridgeWindow.removeEventListener("popstate", handlePopState);
234
- }
235
- if (bridgeWindow.__oasizSetBackOverride === setBackOverride) {
236
- bridgeWindow.__oasizSetBackOverride = previousSetBackOverride;
237
- }
238
- if (bridgeWindow.__oasizLeaveGame === testLeaveGame) {
239
- bridgeWindow.__oasizLeaveGame = previousLeaveGame;
240
- }
241
- if (activeBackButtonTestingHandle === handle) {
242
- activeBackButtonTestingHandle = void 0;
243
- }
244
- maybeLog("Back button testing bridge removed.");
245
- },
246
- isBackOverrideActive: () => backOverrideActive,
247
- triggerBack,
248
- triggerLeave
249
- };
250
- activeBackButtonTestingHandle = handle;
251
- return handle;
252
- }
253
- function onBackButton(callback) {
254
- const off = addNavigationListener("oasiz:back", () => {
255
- try {
256
- callback();
257
- } catch (error) {
258
- leaveGame();
259
- throw normalizeNavigationError(error);
260
- }
261
- });
262
- const bridge = getBridgeWindow();
263
- activeBackListeners += 1;
264
- if (activeBackListeners === 1) {
265
- if (typeof bridge?.__oasizSetBackOverride === "function") {
266
- bridge.__oasizSetBackOverride(true);
267
- } else {
268
- warnMissingBridge("__oasizSetBackOverride");
269
- }
270
- }
271
- return () => {
272
- off();
273
- activeBackListeners = Math.max(0, activeBackListeners - 1);
274
- if (activeBackListeners === 0) {
275
- const currentBridge = getBridgeWindow();
276
- if (typeof currentBridge?.__oasizSetBackOverride === "function") {
277
- currentBridge.__oasizSetBackOverride(false);
278
- } else {
279
- warnMissingBridge("__oasizSetBackOverride");
280
- }
281
- }
282
- };
283
- }
284
- function onLeaveGame(callback) {
285
- return addNavigationListener("oasiz:leave", callback);
286
- }
287
- function leaveGame() {
288
- const bridge = getBridgeWindow();
289
- if (typeof bridge?.__oasizLeaveGame === "function") {
290
- bridge.__oasizLeaveGame();
291
- return;
292
- }
293
- warnMissingBridge("__oasizLeaveGame");
294
- }
295
-
296
- // src/app-simulator.ts
297
- var DEVICE_PRESETS = {
298
- "iphone-11": {
299
- name: "iPhone 11",
300
- width: 414,
301
- height: 896,
302
- safeArea: { top: 48, right: 0, bottom: 34, left: 0 }
303
- },
304
- "iphone-11-pro": {
305
- name: "iPhone 11 Pro",
306
- width: 375,
307
- height: 812,
308
- safeArea: { top: 44, right: 0, bottom: 34, left: 0 }
309
- },
310
- "iphone-11-pro-max": {
311
- name: "iPhone 11 Pro Max",
312
- width: 414,
313
- height: 896,
314
- safeArea: { top: 44, right: 0, bottom: 34, left: 0 }
315
- },
316
- "iphone-12-mini": {
317
- name: "iPhone 12 mini",
318
- width: 375,
319
- height: 812,
320
- safeArea: { top: 50, right: 0, bottom: 34, left: 0 }
321
- },
322
- "iphone-12": {
323
- name: "iPhone 12",
324
- width: 390,
325
- height: 844,
326
- safeArea: { top: 47, right: 0, bottom: 34, left: 0 }
327
- },
328
- "iphone-12-pro": {
329
- name: "iPhone 12 Pro",
330
- width: 390,
331
- height: 844,
332
- safeArea: { top: 47, right: 0, bottom: 34, left: 0 }
333
- },
334
- "iphone-12-pro-max": {
335
- name: "iPhone 12 Pro Max",
336
- width: 428,
337
- height: 926,
338
- safeArea: { top: 47, right: 0, bottom: 34, left: 0 }
339
- },
340
- "iphone-13-mini": {
341
- name: "iPhone 13 mini",
342
- width: 375,
343
- height: 812,
344
- safeArea: { top: 50, right: 0, bottom: 34, left: 0 }
345
- },
346
- "iphone-13": {
347
- name: "iPhone 13",
348
- width: 390,
349
- height: 844,
350
- safeArea: { top: 47, right: 0, bottom: 34, left: 0 }
351
- },
352
- "iphone-13-pro": {
353
- name: "iPhone 13 Pro",
354
- width: 390,
355
- height: 844,
356
- safeArea: { top: 47, right: 0, bottom: 34, left: 0 }
357
- },
358
- "iphone-13-pro-max": {
359
- name: "iPhone 13 Pro Max",
360
- width: 428,
361
- height: 926,
362
- safeArea: { top: 47, right: 0, bottom: 34, left: 0 }
363
- },
364
- "iphone-14": {
365
- name: "iPhone 14",
366
- width: 390,
367
- height: 844,
368
- safeArea: { top: 47, right: 0, bottom: 34, left: 0 }
369
- },
370
- "iphone-14-plus": {
371
- name: "iPhone 14 Plus",
372
- width: 428,
373
- height: 926,
374
- safeArea: { top: 47, right: 0, bottom: 34, left: 0 }
375
- },
376
- "iphone-14-pro": {
377
- name: "iPhone 14 Pro",
378
- width: 393,
379
- height: 852,
380
- safeArea: { top: 59, right: 0, bottom: 34, left: 0 }
381
- },
382
- "iphone-14-pro-max": {
383
- name: "iPhone 14 Pro Max",
384
- width: 430,
385
- height: 932,
386
- safeArea: { top: 59, right: 0, bottom: 34, left: 0 }
387
- },
388
- "iphone-15": {
389
- name: "iPhone 15",
390
- width: 393,
391
- height: 852,
392
- safeArea: { top: 59, right: 0, bottom: 34, left: 0 }
393
- },
394
- "iphone-15-plus": {
395
- name: "iPhone 15 Plus",
396
- width: 430,
397
- height: 932,
398
- safeArea: { top: 59, right: 0, bottom: 34, left: 0 }
399
- },
400
- "iphone-15-pro": {
401
- name: "iPhone 15 Pro",
402
- width: 393,
403
- height: 852,
404
- safeArea: { top: 59, right: 0, bottom: 34, left: 0 }
405
- },
406
- "iphone-15-pro-max": {
407
- name: "iPhone 15 Pro Max",
408
- width: 430,
409
- height: 932,
410
- safeArea: { top: 59, right: 0, bottom: 34, left: 0 }
411
- },
412
- "iphone-16": {
413
- name: "iPhone 16",
414
- width: 393,
415
- height: 852,
416
- safeArea: { top: 59, right: 0, bottom: 34, left: 0 }
417
- },
418
- "iphone-16-plus": {
419
- name: "iPhone 16 Plus",
420
- width: 430,
421
- height: 932,
422
- safeArea: { top: 59, right: 0, bottom: 34, left: 0 }
423
- },
424
- "iphone-16-pro": {
425
- name: "iPhone 16 Pro",
426
- width: 402,
427
- height: 874,
428
- safeArea: { top: 62, right: 0, bottom: 34, left: 0 }
429
- },
430
- "iphone-16-pro-max": {
431
- name: "iPhone 16 Pro Max",
432
- width: 440,
433
- height: 956,
434
- safeArea: { top: 62, right: 0, bottom: 34, left: 0 }
435
- },
436
- "iphone-16e": {
437
- name: "iPhone 16e",
438
- width: 390,
439
- height: 844,
440
- safeArea: { top: 47, right: 0, bottom: 34, left: 0 }
441
- },
442
- "iphone-17": {
443
- name: "iPhone 17",
444
- width: 402,
445
- height: 874,
446
- safeArea: { top: 62, right: 0, bottom: 34, left: 0 }
447
- },
448
- "iphone-17-pro": {
449
- name: "iPhone 17 Pro",
450
- width: 402,
451
- height: 874,
452
- safeArea: { top: 62, right: 0, bottom: 34, left: 0 }
453
- },
454
- "iphone-17-pro-max": {
455
- name: "iPhone 17 Pro Max",
456
- width: 440,
457
- height: 956,
458
- safeArea: { top: 62, right: 0, bottom: 34, left: 0 }
459
- },
460
- "iphone-17e": {
461
- name: "iPhone 17e",
462
- width: 390,
463
- height: 844,
464
- safeArea: { top: 47, right: 0, bottom: 34, left: 0 }
465
- },
466
- "iphone-air": {
467
- name: "iPhone Air",
468
- width: 420,
469
- height: 912,
470
- safeArea: { top: 62, right: 0, bottom: 34, left: 0 }
471
- },
472
- "iphone-se": {
473
- name: "iPhone SE",
474
- width: 375,
475
- height: 667,
476
- safeArea: { top: 20, right: 0, bottom: 0, left: 0 }
477
- },
478
- "pixel-8": {
479
- name: "Pixel 8",
480
- width: 412,
481
- height: 915,
482
- safeArea: { top: 32, right: 0, bottom: 24, left: 0 }
483
- }
484
- };
485
- var DEFAULT_COUNTS = {
486
- comments: 18,
487
- likes: 128
488
- };
489
- var DEFAULT_SCORE = 12400;
490
- var TOP_BAR_OFFSET = 12;
491
- var TOP_CHROME_HEIGHT = 44;
492
- var BODY_BACKGROUND = "#08090d";
493
- var TEXT_PRIMARY = "#F6F9FB";
494
- var TEXT_SECONDARY = "rgba(246,249,251,0.72)";
495
- var TEXT_MUTED = "rgba(246,249,251,0.52)";
496
- var BORDER = "rgba(255,255,255,0.14)";
497
- var ACCENT = "#00A1E4";
498
- var LIKE = "#ef4444";
499
- var TROPHY = "#FBBF24";
500
- var MAX_Z_INDEX = 2147483638;
501
- var EMPTY_INSETS = {
502
- top: 0,
503
- right: 0,
504
- bottom: 0,
505
- left: 0
506
- };
507
- var BRIDGE_KEYS = [
508
- "__OASIZ_SAFE_AREA_BOTTOM__",
509
- "__OASIZ_SAFE_AREA_BOTTOM_PERCENT__",
510
- "__OASIZ_SAFE_AREA_LEFT__",
511
- "__OASIZ_SAFE_AREA_LEFT_PERCENT__",
512
- "__OASIZ_SAFE_AREA_RIGHT__",
513
- "__OASIZ_SAFE_AREA_RIGHT_PERCENT__",
514
- "__OASIZ_SAFE_AREA_TOP__",
515
- "__OASIZ_SAFE_AREA_TOP_PERCENT__",
516
- "__OASIZ_VIEWPORT_INSETS__",
517
- "__OASIZ_VIEWPORT_INSETS_PERCENT__",
518
- "__oasizSetLeaderboardVisible",
519
- "getSafeAreaBottom",
520
- "getSafeAreaBottomPercent",
521
- "getSafeAreaLeft",
522
- "getSafeAreaLeftPercent",
523
- "getSafeAreaRight",
524
- "getSafeAreaRightPercent",
525
- "getSafeAreaTop",
526
- "getSafeAreaTopPercent",
527
- "getViewportInsets",
528
- "getViewportInsetsPercent"
529
- ];
530
- var NOOP_HANDLE = {
531
- closeSheet() {
532
- },
533
- destroy() {
534
- },
535
- getViewportInsets() {
536
- return { pixels: { ...EMPTY_INSETS }, percent: { ...EMPTY_INSETS } };
537
- },
538
- hide() {
539
- },
540
- isVisible() {
541
- return false;
542
- },
543
- openComments() {
544
- },
545
- openLeaderboard() {
546
- },
547
- setCounts() {
548
- },
549
- setLeaderboardVisible() {
550
- },
551
- setLiked() {
552
- },
553
- show() {
554
- },
555
- triggerBack() {
556
- },
557
- triggerLeave() {
558
- }
559
- };
560
- function getBrowserWindow() {
561
- if (typeof window === "undefined") {
562
- return void 0;
563
- }
564
- return window;
565
- }
566
- function getDocument() {
567
- if (typeof document === "undefined") {
568
- return void 0;
569
- }
570
- return document;
571
- }
572
- function warn(message) {
573
- const nodeEnv = globalThis.process?.env?.NODE_ENV;
574
- if (nodeEnv !== "production") {
575
- console.warn("[oasiz/sdk] " + message);
576
- }
577
- }
578
- function normalizeCount(value, fallback) {
579
- if (typeof value !== "number" || !Number.isFinite(value)) {
580
- return fallback;
581
- }
582
- return Math.max(0, Math.floor(value));
583
- }
584
- function formatCompactCount(value) {
585
- if (value >= 1e6) {
586
- return (value / 1e6).toFixed(1).replace(/\.0$/, "") + "M";
587
- }
588
- if (value >= 1e3) {
589
- return (value / 1e3).toFixed(1).replace(/\.0$/, "") + "k";
590
- }
591
- return String(value);
592
- }
593
- function resolveDevice(device, orientation) {
594
- const preset = typeof device === "string" ? DEVICE_PRESETS[device] : device ? {
595
- name: device.name ?? "Custom phone",
596
- width: device.width,
597
- height: device.height,
598
- safeArea: {
599
- ...EMPTY_INSETS,
600
- ...device.safeArea
601
- }
602
- } : DEVICE_PRESETS["iphone-17-pro-max"];
603
- const normalized = {
604
- name: preset.name,
605
- width: Math.max(320, Math.floor(preset.width)),
606
- height: Math.max(480, Math.floor(preset.height)),
607
- safeArea: {
608
- ...EMPTY_INSETS,
609
- ...preset.safeArea
610
- }
611
- };
612
- if (orientation === "landscape") {
613
- return {
614
- ...normalized,
615
- width: Math.max(normalized.width, normalized.height),
616
- height: Math.min(normalized.width, normalized.height),
617
- safeArea: {
618
- top: 0,
619
- right: normalized.safeArea.top,
620
- bottom: 21,
621
- left: normalized.safeArea.top
622
- }
623
- };
624
- }
625
- return normalized;
626
- }
627
- function shouldFrame(frame, browserWindow, device) {
628
- if (frame === true) {
629
- return true;
630
- }
631
- if (frame === false) {
632
- return false;
633
- }
634
- return browserWindow.innerWidth > device.width + 80 || browserWindow.innerHeight > device.height + 80;
635
- }
636
- function computeRect(browserWindow, device, frameEnabled) {
637
- const viewportWidth = Math.max(320, browserWindow.innerWidth || device.width);
638
- const viewportHeight = Math.max(480, browserWindow.innerHeight || device.height);
639
- if (!frameEnabled) {
640
- return {
641
- left: 0,
642
- top: 0,
643
- width: viewportWidth,
644
- height: viewportHeight,
645
- scale: Math.min(viewportWidth / device.width, viewportHeight / device.height)
646
- };
647
- }
648
- const margin = 24;
649
- const scale = Math.min(
650
- (viewportWidth - margin) / device.width,
651
- (viewportHeight - margin) / device.height,
652
- 1
653
- );
654
- const width = Math.round(device.width * scale);
655
- const height = Math.round(device.height * scale);
656
- return {
657
- left: Math.round((viewportWidth - width) / 2),
658
- top: Math.round((viewportHeight - height) / 2),
659
- width,
660
- height,
661
- scale
662
- };
663
- }
664
- function scaledInset(value, scale) {
665
- if (typeof value !== "number" || !Number.isFinite(value)) {
666
- return 0;
667
- }
668
- return Math.max(0, Math.round(value * scale));
669
- }
670
- function getSimulatorInsets(state) {
671
- const safe = state.device.safeArea;
672
- const top = scaledInset(safe.top, state.rect.scale) + Math.round((TOP_BAR_OFFSET + TOP_CHROME_HEIGHT) * state.rect.scale);
673
- const pixels = {
674
- top,
675
- right: scaledInset(safe.right, state.rect.scale),
676
- bottom: scaledInset(safe.bottom, state.rect.scale),
677
- left: scaledInset(safe.left, state.rect.scale)
678
- };
679
- const percent = {
680
- top: state.rect.height > 0 ? pixels.top / state.rect.height * 100 : 0,
681
- right: state.rect.width > 0 ? pixels.right / state.rect.width * 100 : 0,
682
- bottom: state.rect.height > 0 ? pixels.bottom / state.rect.height * 100 : 0,
683
- left: state.rect.width > 0 ? pixels.left / state.rect.width * 100 : 0
684
- };
685
- return { pixels, percent };
686
- }
687
- function snapshotBridge(browserWindow) {
688
- const globals = {};
689
- for (const key of BRIDGE_KEYS) {
690
- globals[key] = browserWindow[key];
691
- }
692
- return { globals };
693
- }
694
- function restoreBridge(browserWindow, snapshot) {
695
- for (const key of BRIDGE_KEYS) {
696
- const value = snapshot.globals[key];
697
- if (typeof value === "undefined") {
698
- delete browserWindow[key];
699
- } else {
700
- browserWindow[key] = value;
701
- }
702
- }
703
- }
704
- function installBridge(state) {
705
- const browserWindow = getBrowserWindow();
706
- if (!browserWindow) return;
707
- const bridge = browserWindow;
708
- function refreshInsets() {
709
- const insets = getSimulatorInsets(state);
710
- bridge.__OASIZ_VIEWPORT_INSETS__ = insets;
711
- bridge.__OASIZ_VIEWPORT_INSETS_PERCENT__ = insets.percent;
712
- bridge.__OASIZ_SAFE_AREA_TOP__ = insets.pixels.top;
713
- bridge.__OASIZ_SAFE_AREA_RIGHT__ = insets.pixels.right;
714
- bridge.__OASIZ_SAFE_AREA_BOTTOM__ = insets.pixels.bottom;
715
- bridge.__OASIZ_SAFE_AREA_LEFT__ = insets.pixels.left;
716
- bridge.__OASIZ_SAFE_AREA_TOP_PERCENT__ = insets.percent.top;
717
- bridge.__OASIZ_SAFE_AREA_RIGHT_PERCENT__ = insets.percent.right;
718
- bridge.__OASIZ_SAFE_AREA_BOTTOM_PERCENT__ = insets.percent.bottom;
719
- bridge.__OASIZ_SAFE_AREA_LEFT_PERCENT__ = insets.percent.left;
720
- return insets;
721
- }
722
- bridge.getViewportInsets = () => refreshInsets();
723
- bridge.getViewportInsetsPercent = () => refreshInsets().percent;
724
- bridge.getSafeAreaTop = () => refreshInsets().pixels.top;
725
- bridge.getSafeAreaRight = () => refreshInsets().pixels.right;
726
- bridge.getSafeAreaBottom = () => refreshInsets().pixels.bottom;
727
- bridge.getSafeAreaLeft = () => refreshInsets().pixels.left;
728
- bridge.getSafeAreaTopPercent = () => refreshInsets().percent.top;
729
- bridge.getSafeAreaRightPercent = () => refreshInsets().percent.right;
730
- bridge.getSafeAreaBottomPercent = () => refreshInsets().percent.bottom;
731
- bridge.getSafeAreaLeftPercent = () => refreshInsets().percent.left;
732
- bridge.__oasizSetLeaderboardVisible = (visible) => {
733
- state.leaderboard.visible = visible;
734
- renderSimulator(state);
735
- };
736
- refreshInsets();
737
- }
738
- function createStyleElement() {
739
- const style = document.createElement("style");
740
- style.setAttribute("data-oasiz-app-simulator", "styles");
741
- style.textContent = [
742
- ".oasiz-app-sim-button{appearance:none;border:0;margin:0;padding:0;font:inherit;color:inherit;background:transparent;cursor:pointer;-webkit-tap-highlight-color:transparent;touch-action:manipulation}",
743
- ".oasiz-app-sim-button:active{transform:scale(0.97)}",
744
- ".oasiz-app-sim-glass{background:rgba(18,20,24,0.42);border:1px solid rgba(255,255,255,0.22);box-shadow:0 12px 30px rgba(0,0,0,0.28),inset 0 1px 0 rgba(255,255,255,0.18);backdrop-filter:blur(18px);-webkit-backdrop-filter:blur(18px)}",
745
- ".oasiz-app-sim-scroll::-webkit-scrollbar{width:0;height:0}"
746
- ].join("\n");
747
- return style;
748
- }
749
- function applyTextStyle(element, size = 12, weight = 600) {
750
- element.style.font = String(weight) + " " + String(size) + "px/1.2 -apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica,Arial,sans-serif";
751
- }
752
- function createDiv(className) {
753
- const element = document.createElement("div");
754
- if (className) {
755
- element.className = className;
756
- }
757
- return element;
758
- }
759
- function createButton(label, title) {
760
- const button = document.createElement("button");
761
- button.type = "button";
762
- button.className = "oasiz-app-sim-button";
763
- button.setAttribute("aria-label", title);
764
- button.title = title;
765
- button.innerHTML = label;
766
- return button;
767
- }
768
- function svgIcon(name, filled = false) {
769
- if (name === "back") {
770
- return '<svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="M19 12H5"/><path d="m12 19-7-7 7-7"/></svg>';
771
- }
772
- if (name === "heart") {
773
- return filled ? '<svg viewBox="0 0 24 24" width="15" height="15" fill="currentColor"><path d="M12 21s-7.4-4.4-9.7-9.1C.7 8.5 2.7 4.8 6.4 4.3c2-.3 3.8.7 5.6 2.8 1.8-2.1 3.6-3.1 5.6-2.8 3.7.5 5.7 4.2 4.1 7.6C19.4 16.6 12 21 12 21Z"/></svg>' : '<svg viewBox="0 0 24 24" width="15" height="15" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><path d="M20.8 4.6c-2-1.8-5.1-1.5-6.9.6L12 7.4l-1.9-2.2C8.3 3.1 5.2 2.8 3.2 4.6.8 6.8.7 10.5 3 12.9L12 21l9-8.1c2.3-2.4 2.2-6.1-.2-8.3Z"/></svg>';
774
- }
775
- if (name === "chat") {
776
- return filled ? '<svg viewBox="0 0 24 24" width="15" height="15" fill="currentColor"><path d="M4 5.5A3.5 3.5 0 0 1 7.5 2h9A3.5 3.5 0 0 1 20 5.5v7A3.5 3.5 0 0 1 16.5 16H9l-4.2 3.1A1.1 1.1 0 0 1 3 18.2V5.5Z"/></svg>' : '<svg viewBox="0 0 24 24" width="15" height="15" fill="none" stroke="currentColor" stroke-width="2.1" stroke-linecap="round" stroke-linejoin="round"><path d="M4 5.5A3.5 3.5 0 0 1 7.5 2h9A3.5 3.5 0 0 1 20 5.5v7A3.5 3.5 0 0 1 16.5 16H9l-4.2 3.1A1.1 1.1 0 0 1 3 18.2V5.5Z"/></svg>';
777
- }
778
- if (name === "trophy") {
779
- return '<svg viewBox="0 0 24 24" width="16" height="16" fill="currentColor"><path d="M7 3h10v3h3a1 1 0 0 1 1 1c0 3.2-1.8 5.5-4.6 6.1A5 5 0 0 1 13 15.9V19h3v2H8v-2h3v-3.1a5 5 0 0 1-3.4-2.8C4.8 12.5 3 10.2 3 7a1 1 0 0 1 1-1h3V3Zm10 5v2.8c1.1-.5 1.8-1.5 2-2.8h-2ZM5 8c.2 1.3.9 2.3 2 2.8V8H5Z"/></svg>';
780
- }
781
- if (name === "share") {
782
- return '<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2.1" stroke-linecap="round" stroke-linejoin="round"><path d="M12 16V4"/><path d="m7 9 5-5 5 5"/><path d="M5 14v5h14v-5"/></svg>';
783
- }
784
- return filled ? '<svg viewBox="0 0 24 24" width="16" height="16" fill="currentColor"><path d="M6 3h12a1 1 0 0 1 1 1v17l-7-4-7 4V4a1 1 0 0 1 1-1Z"/></svg>' : '<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2.1" stroke-linecap="round" stroke-linejoin="round"><path d="M6 3h12a1 1 0 0 1 1 1v17l-7-4-7 4V4a1 1 0 0 1 1-1Z"/></svg>';
785
- }
786
- function setBoxStyle(element, state) {
787
- const { rect } = state;
788
- element.style.left = rect.left + "px";
789
- element.style.top = rect.top + "px";
790
- element.style.width = rect.width + "px";
791
- element.style.height = rect.height + "px";
792
- }
793
- function createCircleButton(icon, title, onClick) {
794
- const button = createButton(icon, title);
795
- button.classList.add("oasiz-app-sim-glass");
796
- button.style.cssText += [
797
- "position:absolute",
798
- "width:44px",
799
- "height:44px",
800
- "border-radius:999px",
801
- "display:flex",
802
- "align-items:center",
803
- "justify-content:center",
804
- "color:" + TEXT_PRIMARY,
805
- "pointer-events:auto"
806
- ].join(";");
807
- button.addEventListener("click", (event) => {
808
- event.preventDefault();
809
- event.stopPropagation();
810
- onClick();
811
- });
812
- return button;
813
- }
814
- function createHubPill(state) {
815
- const button = createButton("", "Open comments");
816
- button.classList.add("oasiz-app-sim-glass");
817
- button.style.cssText += [
818
- "position:absolute",
819
- "right:16px",
820
- "height:44px",
821
- "min-width:94px",
822
- "border-radius:999px",
823
- "display:flex",
824
- "align-items:center",
825
- "justify-content:center",
826
- "gap:10px",
827
- "padding:0 14px",
828
- "color:" + TEXT_PRIMARY,
829
- "pointer-events:auto"
830
- ].join(";");
831
- const likeColor = state.wasLiked ? LIKE : TEXT_PRIMARY;
832
- button.innerHTML = '<span style="display:inline-flex;align-items:center;gap:4px;color:' + likeColor + '">' + svgIcon("heart", state.wasLiked) + "<span>" + formatCompactCount(state.counts.likes) + '</span></span><span style="display:inline-flex;align-items:center;gap:4px;color:' + ACCENT + '">' + svgIcon("chat", true) + "<span>" + formatCompactCount(state.counts.comments) + "</span></span>";
833
- applyTextStyle(button, 12, 700);
834
- button.addEventListener("click", (event) => {
835
- event.preventDefault();
836
- event.stopPropagation();
837
- state.sheet = "comments";
838
- renderSimulator(state);
839
- });
840
- return button;
841
- }
842
- function createLeaderboardPill(state) {
843
- const button = createButton("", "Open leaderboard");
844
- button.classList.add("oasiz-app-sim-glass");
845
- button.style.cssText += [
846
- "position:absolute",
847
- "left:50%",
848
- "height:44px",
849
- "min-width:118px",
850
- "border-radius:999px",
851
- "display:" + (state.leaderboard.visible ? "flex" : "none"),
852
- "align-items:center",
853
- "justify-content:center",
854
- "gap:8px",
855
- "padding:0 14px",
856
- "color:" + TEXT_PRIMARY,
857
- "pointer-events:auto",
858
- "transform:translateX(-50%)"
859
- ].join(";");
860
- button.innerHTML = '<span style="color:' + TROPHY + ';display:inline-flex">' + svgIcon("trophy", true) + '</span><span style="min-width:0">' + formatCompactCount(state.leaderboard.score) + "</span>";
861
- applyTextStyle(button, 14, 800);
862
- button.addEventListener("click", (event) => {
863
- event.preventDefault();
864
- event.stopPropagation();
865
- state.sheet = "leaderboard";
866
- renderSimulator(state);
867
- });
868
- return button;
869
- }
870
- function createToolbarButton(icon, count, color, title, onClick) {
871
- const button = createButton("", title);
872
- button.classList.add("oasiz-app-sim-glass");
873
- button.style.cssText += [
874
- "height:42px",
875
- "min-width:42px",
876
- "border-radius:999px",
877
- "display:flex",
878
- "align-items:center",
879
- "justify-content:center",
880
- "gap:6px",
881
- "padding:0 12px",
882
- "color:" + color,
883
- "pointer-events:auto"
884
- ].join(";");
885
- button.innerHTML = icon + (count === null ? "" : '<span style="color:' + color + ';min-width:0">' + formatCompactCount(count) + "</span>");
886
- applyTextStyle(button, 13, 700);
887
- button.addEventListener("click", (event) => {
888
- event.preventDefault();
889
- event.stopPropagation();
890
- onClick();
891
- });
892
- return button;
893
- }
894
- function createSheet(state) {
895
- if (!state.sheet) {
896
- return null;
897
- }
898
- const overlay = createDiv();
899
- overlay.style.cssText = [
900
- "position:absolute",
901
- "left:0",
902
- "top:0",
903
- "right:0",
904
- "bottom:0",
905
- "display:flex",
906
- "align-items:center",
907
- "justify-content:center",
908
- "padding:" + String(scaledInset(state.device.safeArea.top, state.rect.scale) + 72) + "px 12px " + String(Math.max(18, scaledInset(state.device.safeArea.bottom, state.rect.scale) + 18)) + "px",
909
- "background:rgba(0,0,0,0.34)",
910
- "pointer-events:auto"
911
- ].join(";");
912
- overlay.addEventListener("click", () => {
913
- state.sheet = null;
914
- renderSimulator(state);
915
- });
916
- const sheet = createDiv();
917
- sheet.style.cssText = [
918
- "position:relative",
919
- "width:min(100%, 408px)",
920
- "max-height:100%",
921
- "min-height:min(390px, 64%)",
922
- "display:flex",
923
- "flex-direction:column",
924
- "border-radius:24px",
925
- "border:1px solid " + BORDER,
926
- "background:linear-gradient(180deg, rgba(18,20,24,0.97), rgba(9,11,15,0.98))",
927
- "box-shadow:0 30px 80px rgba(0,0,0,0.52), inset 0 1px 0 rgba(255,255,255,0.10)",
928
- "overflow:hidden",
929
- "pointer-events:auto"
930
- ].join(";");
931
- sheet.addEventListener("click", (event) => {
932
- event.stopPropagation();
933
- });
934
- const handle = createDiv();
935
- handle.style.cssText = [
936
- "width:42px",
937
- "height:5px",
938
- "border-radius:999px",
939
- "background:rgba(255,255,255,0.28)",
940
- "align-self:center",
941
- "margin:10px 0 2px"
942
- ].join(";");
943
- const header = createDiv();
944
- header.style.cssText = [
945
- "display:flex",
946
- "align-items:center",
947
- "justify-content:space-between",
948
- "gap:8px",
949
- "min-height:62px",
950
- "padding:6px 14px 10px",
951
- "border-bottom:1px solid " + BORDER
952
- ].join(";");
953
- const left = createDiv();
954
- left.style.cssText = "display:flex;align-items:center;gap:8px;min-width:112px";
955
- const title = createDiv();
956
- title.textContent = state.sheet === "comments" ? String(state.counts.comments) + " comments" : "Leaderboard";
957
- title.style.cssText = [
958
- "flex:1",
959
- "min-width:0",
960
- "text-align:center",
961
- "color:" + TEXT_PRIMARY
962
- ].join(";");
963
- applyTextStyle(title, 14, 700);
964
- const right = createDiv();
965
- right.style.cssText = "display:flex;align-items:center;justify-content:flex-end;gap:8px;min-width:112px";
966
- const back = createToolbarButton(svgIcon("back"), null, TEXT_PRIMARY, "Close", () => {
967
- state.sheet = null;
968
- renderSimulator(state);
969
- });
970
- back.style.width = "36px";
971
- back.style.height = "36px";
972
- back.style.padding = "0";
973
- left.appendChild(back);
974
- if (state.sheet === "comments") {
975
- const like = createToolbarButton(
976
- svgIcon("heart", state.wasLiked),
977
- state.counts.likes,
978
- state.wasLiked ? LIKE : TEXT_PRIMARY,
979
- "Like game",
980
- () => {
981
- state.wasLiked = !state.wasLiked;
982
- state.counts.likes = Math.max(0, state.counts.likes + (state.wasLiked ? 1 : -1));
983
- renderSimulator(state);
984
- }
985
- );
986
- left.appendChild(like);
987
- } else {
988
- const trophy = createToolbarButton(svgIcon("trophy", true), null, TROPHY, "Leaderboard", () => {
989
- });
990
- trophy.style.width = "36px";
991
- trophy.style.height = "36px";
992
- trophy.style.padding = "0";
993
- left.appendChild(trophy);
994
- }
995
- const share2 = createToolbarButton(svgIcon("share"), null, TEXT_PRIMARY, "Share", () => {
996
- });
997
- const save = createToolbarButton(svgIcon("bookmark"), null, TEXT_PRIMARY, "Save", () => {
998
- });
999
- right.appendChild(share2);
1000
- right.appendChild(save);
1001
- header.appendChild(left);
1002
- header.appendChild(title);
1003
- header.appendChild(right);
1004
- const body = createDiv("oasiz-app-sim-scroll");
1005
- body.style.cssText = [
1006
- "display:flex",
1007
- "flex-direction:column",
1008
- "gap:10px",
1009
- "overflow:auto",
1010
- "padding:14px 16px calc(" + String(Math.max(16, scaledInset(state.device.safeArea.bottom, state.rect.scale))) + "px + 14px)",
1011
- "min-height:0"
1012
- ].join(";");
1013
- if (state.sheet === "leaderboard") {
1014
- appendLeaderboardBody(body);
1015
- } else {
1016
- appendCommentsBody(body, state);
1017
- }
1018
- sheet.appendChild(handle);
1019
- sheet.appendChild(header);
1020
- sheet.appendChild(body);
1021
- overlay.appendChild(sheet);
1022
- return overlay;
1023
- }
1024
- function appendLeaderboardBody(body) {
1025
- const tabs = createDiv();
1026
- tabs.style.cssText = [
1027
- "display:grid",
1028
- "grid-template-columns:repeat(3,1fr)",
1029
- "gap:6px",
1030
- "padding:4px",
1031
- "border-radius:14px",
1032
- "background:rgba(255,255,255,0.07)"
1033
- ].join(";");
1034
- for (const label of ["Weekly", "Global", "Friends"]) {
1035
- const tab = createDiv();
1036
- tab.textContent = label;
1037
- tab.style.cssText = [
1038
- "border-radius:10px",
1039
- "padding:8px 6px",
1040
- "text-align:center",
1041
- "color:" + (label === "Weekly" ? TEXT_PRIMARY : TEXT_MUTED),
1042
- "background:" + (label === "Weekly" ? "rgba(0,161,228,0.28)" : "transparent")
1043
- ].join(";");
1044
- applyTextStyle(tab, 12, 700);
1045
- tabs.appendChild(tab);
1046
- }
1047
- body.appendChild(tabs);
1048
- const entries = [
1049
- ["1", "Nova", "24.8k"],
1050
- ["2", "You", "12.4k"],
1051
- ["3", "Mika", "9.7k"],
1052
- ["4", "Ari", "8.1k"]
1053
- ];
1054
- for (const [rank, name, score] of entries) {
1055
- const row = createDiv();
1056
- row.style.cssText = [
1057
- "display:grid",
1058
- "grid-template-columns:34px 1fr auto",
1059
- "align-items:center",
1060
- "gap:10px",
1061
- "min-height:58px",
1062
- "padding:10px 12px",
1063
- "border-radius:14px",
1064
- "background:" + (name === "You" ? "rgba(0,161,228,0.13)" : "rgba(255,255,255,0.06)"),
1065
- "border:1px solid " + (name === "You" ? "rgba(0,161,228,0.28)" : "rgba(255,255,255,0.08)")
1066
- ].join(";");
1067
- const rankEl = createDiv();
1068
- rankEl.textContent = rank;
1069
- rankEl.style.cssText = "color:" + (rank === "1" ? TROPHY : TEXT_MUTED) + ";text-align:center";
1070
- applyTextStyle(rankEl, 14, 800);
1071
- const nameEl = createDiv();
1072
- nameEl.textContent = name;
1073
- nameEl.style.cssText = "color:" + TEXT_PRIMARY + ";min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap";
1074
- applyTextStyle(nameEl, 14, 700);
1075
- const scoreEl = createDiv();
1076
- scoreEl.textContent = score;
1077
- scoreEl.style.cssText = "color:" + TEXT_SECONDARY;
1078
- applyTextStyle(scoreEl, 13, 700);
1079
- row.appendChild(rankEl);
1080
- row.appendChild(nameEl);
1081
- row.appendChild(scoreEl);
1082
- body.appendChild(row);
1083
- }
1084
- }
1085
- function appendCommentsBody(body, state) {
1086
- const toolbar = createDiv();
1087
- toolbar.style.cssText = [
1088
- "display:flex",
1089
- "align-items:center",
1090
- "gap:8px",
1091
- "height:48px"
1092
- ].join(";");
1093
- toolbar.appendChild(
1094
- createToolbarButton(
1095
- svgIcon("heart", state.wasLiked),
1096
- state.counts.likes,
1097
- state.wasLiked ? LIKE : TEXT_PRIMARY,
1098
- "Like game",
1099
- () => {
1100
- state.wasLiked = !state.wasLiked;
1101
- state.counts.likes = Math.max(0, state.counts.likes + (state.wasLiked ? 1 : -1));
1102
- renderSimulator(state);
1103
- }
1104
- )
1105
- );
1106
- toolbar.appendChild(
1107
- createToolbarButton(svgIcon("chat", true), state.counts.comments, ACCENT, "Comments", () => {
1108
- })
1109
- );
1110
- toolbar.appendChild(createToolbarButton(svgIcon("share"), null, TEXT_PRIMARY, "Share", () => {
1111
- }));
1112
- toolbar.appendChild(createToolbarButton(svgIcon("bookmark"), null, TEXT_PRIMARY, "Save", () => {
1113
- }));
1114
- body.appendChild(toolbar);
1115
- const comments = [
1116
- ["You", "Can my score panel clear the top buttons?"],
1117
- ["Nova", "This is a good spot to check pause and menu spacing."],
1118
- ["Mika", "The app chrome stays above the game just like mobile."]
1119
- ];
1120
- for (const [author, text] of comments) {
1121
- const row = createDiv();
1122
- row.style.cssText = [
1123
- "display:grid",
1124
- "grid-template-columns:34px 1fr",
1125
- "gap:10px",
1126
- "padding:10px 0"
1127
- ].join(";");
1128
- const avatar = createDiv();
1129
- avatar.textContent = author.charAt(0);
1130
- avatar.style.cssText = [
1131
- "width:34px",
1132
- "height:34px",
1133
- "border-radius:999px",
1134
- "display:flex",
1135
- "align-items:center",
1136
- "justify-content:center",
1137
- "background:rgba(0,161,228,0.28)",
1138
- "color:" + TEXT_PRIMARY
1139
- ].join(";");
1140
- applyTextStyle(avatar, 13, 800);
1141
- const message = createDiv();
1142
- message.innerHTML = '<div style="color:' + TEXT_PRIMARY + ';font-weight:700;margin-bottom:3px">' + author + '</div><div style="color:' + TEXT_SECONDARY + '">' + text + "</div>";
1143
- applyTextStyle(message, 13, 500);
1144
- row.appendChild(avatar);
1145
- row.appendChild(message);
1146
- body.appendChild(row);
1147
- }
1148
- const composer = createDiv();
1149
- composer.textContent = "Add comment...";
1150
- composer.style.cssText = [
1151
- "height:44px",
1152
- "border-radius:18px",
1153
- "display:flex",
1154
- "align-items:center",
1155
- "padding:0 14px",
1156
- "background:rgba(255,255,255,0.08)",
1157
- "border:1px solid rgba(255,255,255,0.10)",
1158
- "color:" + TEXT_MUTED
1159
- ].join(";");
1160
- applyTextStyle(composer, 13, 600);
1161
- body.appendChild(composer);
1162
- }
1163
- function renderSimulator(state) {
1164
- if (state.wasDestroyed) {
1165
- return;
1166
- }
1167
- const { root, stage, gameViewport } = state.elements;
1168
- root.style.display = state.visible ? "block" : "none";
1169
- setBoxStyle(stage, state);
1170
- if (gameViewport) {
1171
- setBoxStyle(gameViewport, state);
1172
- }
1173
- stage.replaceChildren();
1174
- const top = scaledInset(state.device.safeArea.top, state.rect.scale) + Math.round(TOP_BAR_OFFSET * state.rect.scale);
1175
- const back = createCircleButton(svgIcon("back"), "Back", () => {
1176
- if (state.backHandle.isBackOverrideActive()) {
1177
- state.backHandle.triggerBack();
1178
- } else {
1179
- state.backHandle.triggerLeave();
1180
- }
1181
- });
1182
- back.style.left = Math.round(16 * state.rect.scale) + "px";
1183
- back.style.top = top + "px";
1184
- const leaderboard = createLeaderboardPill(state);
1185
- leaderboard.style.top = top + "px";
1186
- const hub = createHubPill(state);
1187
- hub.style.top = top + "px";
1188
- hub.style.right = Math.round(16 * state.rect.scale) + "px";
1189
- stage.appendChild(back);
1190
- stage.appendChild(leaderboard);
1191
- stage.appendChild(hub);
1192
- const sheet = createSheet(state);
1193
- if (sheet) {
1194
- stage.appendChild(sheet);
1195
- }
1196
- if (state.options.log) {
1197
- console.info("[oasiz/sdk] App simulator rendered.", {
1198
- device: state.device.name,
1199
- frame: state.frameEnabled,
1200
- insets: getSimulatorInsets(state)
1201
- });
1202
- }
1203
- }
1204
- function installFrame(doc, state) {
1205
- if (!state.frameEnabled || !doc.body) {
1206
- return;
1207
- }
1208
- const body = doc.body;
1209
- const html = doc.documentElement;
1210
- const viewport = state.elements.gameViewport;
1211
- if (!viewport) {
1212
- return;
1213
- }
1214
- html.style.cssText = [
1215
- state.previousDocumentElementStyle ?? "",
1216
- "width:100%",
1217
- "height:100%",
1218
- "background:" + BODY_BACKGROUND,
1219
- "overflow:hidden"
1220
- ].join(";");
1221
- body.style.cssText = [
1222
- state.previousBodyStyle ?? "",
1223
- "width:100%",
1224
- "height:100%",
1225
- "margin:0",
1226
- "background:" + BODY_BACKGROUND,
1227
- "overflow:hidden"
1228
- ].join(";");
1229
- const candidates = Array.from(body.children).filter(
1230
- (node) => shouldMoveIntoGameViewport(node, state.elements)
1231
- );
1232
- for (const node of candidates) {
1233
- state.movedNodes.push(node);
1234
- viewport.appendChild(node);
1235
- }
1236
- body.appendChild(viewport);
1237
- }
1238
- function shouldMoveIntoGameViewport(node, elements) {
1239
- if (node === elements.root || node === elements.style || node === elements.gameViewport) {
1240
- return false;
1241
- }
1242
- const tagName = node.tagName.toUpperCase();
1243
- if (tagName === "SCRIPT" || tagName === "STYLE" || tagName === "LINK" || tagName === "META" || tagName === "NOSCRIPT") {
1244
- return false;
1245
- }
1246
- return node.getAttribute("data-oasiz-app-simulator") !== "true";
1247
- }
1248
- function restoreFrame(doc, state) {
1249
- const body = doc.body;
1250
- if (!body) {
1251
- return;
1252
- }
1253
- for (const node of state.movedNodes) {
1254
- body.appendChild(node);
1255
- }
1256
- state.movedNodes = [];
1257
- state.elements.gameViewport?.remove();
1258
- if (state.previousBodyStyle === null) {
1259
- body.removeAttribute("style");
1260
- } else {
1261
- body.setAttribute("style", state.previousBodyStyle);
1262
- }
1263
- if (state.previousDocumentElementStyle === null) {
1264
- doc.documentElement.removeAttribute("style");
1265
- } else {
1266
- doc.documentElement.setAttribute("style", state.previousDocumentElementStyle);
1267
- }
1268
- }
1269
- function createElements(frameEnabled) {
1270
- const root = document.createElement("div");
1271
- root.setAttribute("data-oasiz-app-simulator", "true");
1272
- root.style.cssText = [
1273
- "position:fixed",
1274
- "inset:0",
1275
- "z-index:" + String(MAX_Z_INDEX),
1276
- "pointer-events:none",
1277
- "display:block",
1278
- "font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica,Arial,sans-serif"
1279
- ].join(";");
1280
- const stage = document.createElement("div");
1281
- stage.setAttribute("data-oasiz-app-simulator", "stage");
1282
- stage.style.cssText = [
1283
- "position:absolute",
1284
- "overflow:hidden",
1285
- "pointer-events:none"
1286
- ].join(";");
1287
- root.appendChild(stage);
1288
- const gameViewport = frameEnabled ? document.createElement("div") : null;
1289
- if (gameViewport) {
1290
- gameViewport.setAttribute("data-oasiz-app-simulator", "game-viewport");
1291
- gameViewport.style.cssText = [
1292
- "position:fixed",
1293
- "z-index:0",
1294
- "overflow:hidden",
1295
- "background:#000",
1296
- "border-radius:28px",
1297
- "outline:1px solid rgba(255,255,255,0.28)",
1298
- "box-shadow:0 28px 90px rgba(0,0,0,0.48),0 0 0 8px rgba(255,255,255,0.06),0 0 0 10px rgba(0,0,0,0.58)"
1299
- ].join(";");
1300
- }
1301
- return {
1302
- gameViewport,
1303
- root,
1304
- stage,
1305
- style: createStyleElement()
1306
- };
1307
- }
1308
- function installResizeHandler(browserWindow, state) {
1309
- const resize = () => {
1310
- state.rect = computeRect(browserWindow, state.device, state.frameEnabled);
1311
- installBridge(state);
1312
- renderSimulator(state);
1313
- };
1314
- browserWindow.addEventListener("resize", resize);
1315
- browserWindow.addEventListener("orientationchange", resize);
1316
- return () => {
1317
- browserWindow.removeEventListener("resize", resize);
1318
- browserWindow.removeEventListener("orientationchange", resize);
1319
- };
1320
- }
1321
- function enableAppSimulator(options = {}) {
1322
- if (options.enabled === false) {
1323
- return NOOP_HANDLE;
1324
- }
1325
- const browserWindow = getBrowserWindow();
1326
- const doc = getDocument();
1327
- if (!browserWindow || !doc?.body || !doc.documentElement) {
1328
- warn("enableAppSimulator requires a browser document.");
1329
- return NOOP_HANDLE;
1330
- }
1331
- browserWindow.__oasizAppSimulatorHandle__?.destroy();
1332
- const orientation = options.orientation ?? "portrait";
1333
- const device = resolveDevice(options.device, orientation);
1334
- const frameEnabled = shouldFrame(options.frame ?? true, browserWindow, device);
1335
- const elements = createElements(frameEnabled);
1336
- const rect = computeRect(browserWindow, device, frameEnabled);
1337
- const backHandle = enableBackButtonTesting({
1338
- browserHistory: options.browserHistoryBack ?? true,
1339
- keyboard: options.keyboardBack ?? true,
1340
- log: options.log === true
1341
- });
1342
- const state = {
1343
- backHandle,
1344
- bridgeSnapshot: snapshotBridge(browserWindow),
1345
- cleanupResize: () => {
1346
- },
1347
- counts: {
1348
- comments: normalizeCount(options.comments, DEFAULT_COUNTS.comments),
1349
- likes: normalizeCount(options.likes, DEFAULT_COUNTS.likes)
1350
- },
1351
- device,
1352
- elements,
1353
- frameEnabled,
1354
- leaderboard: {
1355
- score: normalizeCount(options.score, DEFAULT_SCORE),
1356
- visible: options.leaderboardVisible ?? true
1357
- },
1358
- movedNodes: [],
1359
- options: {
1360
- browserHistoryBack: options.browserHistoryBack ?? true,
1361
- keyboardBack: options.keyboardBack ?? true,
1362
- log: options.log === true,
1363
- orientation,
1364
- title: options.title ?? "Oasiz App Preview"
1365
- },
1366
- previousBodyStyle: doc.body.getAttribute("style"),
1367
- previousDocumentElementStyle: doc.documentElement.getAttribute("style"),
1368
- rect,
1369
- sheet: null,
1370
- visible: true,
1371
- wasDestroyed: false,
1372
- wasLiked: false
1373
- };
1374
- if (doc.head) {
1375
- doc.head.appendChild(elements.style);
1376
- } else {
1377
- doc.body.appendChild(elements.style);
1378
- }
1379
- installFrame(doc, state);
1380
- doc.body.appendChild(elements.root);
1381
- installBridge(state);
1382
- state.cleanupResize = installResizeHandler(browserWindow, state);
1383
- const handle = {
1384
- closeSheet: () => {
1385
- state.sheet = null;
1386
- renderSimulator(state);
1387
- },
1388
- destroy: () => {
1389
- if (state.wasDestroyed) return;
1390
- state.wasDestroyed = true;
1391
- state.cleanupResize();
1392
- state.backHandle.destroy();
1393
- restoreBridge(browserWindow, state.bridgeSnapshot);
1394
- delete browserWindow.__oasizAppSimulatorHandle__;
1395
- elements.root.remove();
1396
- elements.style.remove();
1397
- restoreFrame(doc, state);
1398
- },
1399
- getViewportInsets: () => getSimulatorInsets(state),
1400
- hide: () => {
1401
- state.visible = false;
1402
- renderSimulator(state);
1403
- },
1404
- isVisible: () => state.visible,
1405
- openComments: () => {
1406
- state.sheet = "comments";
1407
- renderSimulator(state);
1408
- },
1409
- openLeaderboard: () => {
1410
- state.sheet = "leaderboard";
1411
- renderSimulator(state);
1412
- },
1413
- setCounts: (counts) => {
1414
- state.counts.comments = normalizeCount(counts.comments, state.counts.comments);
1415
- state.counts.likes = normalizeCount(counts.likes, state.counts.likes);
1416
- renderSimulator(state);
1417
- },
1418
- setLeaderboardVisible: (visible) => {
1419
- state.leaderboard.visible = visible;
1420
- renderSimulator(state);
1421
- },
1422
- setLiked: (liked) => {
1423
- if (state.wasLiked === liked) {
1424
- return;
1425
- }
1426
- state.wasLiked = liked;
1427
- state.counts.likes = Math.max(0, state.counts.likes + (liked ? 1 : -1));
1428
- renderSimulator(state);
1429
- },
1430
- show: () => {
1431
- state.visible = true;
1432
- renderSimulator(state);
1433
- },
1434
- triggerBack: () => state.backHandle.triggerBack(),
1435
- triggerLeave: () => state.backHandle.triggerLeave()
1436
- };
1437
- browserWindow.__oasizAppSimulatorHandle__ = handle;
1438
- renderSimulator(state);
1439
- return handle;
1440
- }
1441
-
1442
- // src/character.ts
1443
- function isDevelopment2() {
1444
- const nodeEnv = globalThis.process?.env?.NODE_ENV;
1445
- return nodeEnv !== "production";
1446
- }
1447
- function getBridgeWindow2() {
1448
- if (typeof window === "undefined") {
1449
- return void 0;
1450
- }
1451
- return window;
1452
- }
1453
- function warnMissingBridge2(methodName) {
1454
- if (isDevelopment2()) {
1455
- console.warn(
1456
- "[oasiz/sdk] " + methodName + " bridge is unavailable. This is expected in local development."
1457
- );
1458
- }
1459
- }
1460
- async function getPlayerCharacter() {
1461
- const bridge = getBridgeWindow2();
1462
- if (typeof bridge?.__oasizGetPlayerCharacter !== "function") {
1463
- warnMissingBridge2("getPlayerCharacter");
1464
- return null;
1465
- }
1466
- try {
1467
- const result = await bridge.__oasizGetPlayerCharacter();
1468
- return result ?? null;
1469
- } catch (error) {
1470
- if (isDevelopment2()) {
1471
- console.error("[oasiz/sdk] getPlayerCharacter failed:", error);
1472
- }
1473
- return null;
1474
- }
1475
- }
1476
-
1477
- // src/haptics.ts
1478
- function isDevelopment3() {
1479
- const nodeEnv = globalThis.process?.env?.NODE_ENV;
1480
- return nodeEnv !== "production";
1481
- }
1482
- function getBridgeWindow3() {
1483
- if (typeof window === "undefined") {
1484
- return void 0;
1485
- }
1486
- return window;
1487
- }
1488
- function triggerHaptic(type) {
1489
- const bridge = getBridgeWindow3();
1490
- if (typeof bridge?.triggerHaptic === "function") {
1491
- bridge.triggerHaptic(type);
1492
- return;
1493
- }
1494
- if (isDevelopment3()) {
1495
- console.warn(
1496
- "[oasiz/sdk] triggerHaptic bridge is unavailable. This is expected in local development."
1497
- );
1498
- }
1499
- }
1500
-
1501
- // src/log-overlay.ts
1502
- var CONSOLE_METHODS = [
1503
- "debug",
1504
- "log",
1505
- "info",
1506
- "warn",
1507
- "error"
1508
- ];
1509
- var DEFAULT_MAX_ENTRIES = 200;
1510
- var DEFAULT_TITLE = "SDK Logs";
1511
- var OVERLAY_MARGIN = 12;
1512
- var DEFAULT_COLLAPSED_WIDTH = 156;
1513
- var DEFAULT_COLLAPSED_HEIGHT = 52;
1514
- var DEFAULT_EXPANDED_WIDTH = 565;
1515
- var DEFAULT_EXPANDED_HEIGHT = 372;
1516
- var DRAG_THRESHOLD_PX = 6;
1517
- var MIN_EXPANDED_WIDTH = 160;
1518
- var MIN_EXPANDED_HEIGHT = 110;
1519
- var RESIZE_HOTSPOT_PX = 28;
1520
- var TOP_DRAG_ZONE_PX = 44;
1521
- var NOOP_HANDLE2 = {
1522
- clear() {
1523
- },
1524
- destroy() {
1525
- },
1526
- hide() {
1527
- },
1528
- isVisible() {
1529
- return false;
1530
- },
1531
- show() {
1532
- }
1533
- };
1534
- function getBrowserWindow2() {
54
+ // src/haptics.ts
55
+ function isDevelopment() {
56
+ const nodeEnv = globalThis.process?.env?.NODE_ENV;
57
+ return nodeEnv !== "production";
58
+ }
59
+ function getBridgeWindow() {
1535
60
  if (typeof window === "undefined") {
1536
61
  return void 0;
1537
62
  }
1538
63
  return window;
1539
64
  }
1540
- function getDocument2() {
1541
- if (typeof document === "undefined") {
1542
- return void 0;
1543
- }
1544
- return document;
1545
- }
1546
- function clampMaxEntries(value) {
1547
- if (!Number.isFinite(value)) {
1548
- return DEFAULT_MAX_ENTRIES;
1549
- }
1550
- return Math.max(10, Math.floor(value));
1551
- }
1552
- function createConsoleSnapshot() {
1553
- const fallback = console.log.bind(console);
1554
- return {
1555
- debug: typeof console.debug === "function" ? console.debug.bind(console) : fallback,
1556
- log: fallback,
1557
- info: typeof console.info === "function" ? console.info.bind(console) : fallback,
1558
- warn: typeof console.warn === "function" ? console.warn.bind(console) : fallback,
1559
- error: typeof console.error === "function" ? console.error.bind(console) : fallback
1560
- };
1561
- }
1562
- function formatTimestamp(timestamp) {
1563
- const date = new Date(timestamp);
1564
- const hours = String(date.getHours()).padStart(2, "0");
1565
- const minutes = String(date.getMinutes()).padStart(2, "0");
1566
- const seconds = String(date.getSeconds()).padStart(2, "0");
1567
- const milliseconds = String(date.getMilliseconds()).padStart(3, "0");
1568
- return "[" + hours + ":" + minutes + ":" + seconds + "." + milliseconds + "]";
1569
- }
1570
- function safeStringify(value) {
1571
- const seen = /* @__PURE__ */ new WeakSet();
1572
- try {
1573
- return JSON.stringify(
1574
- value,
1575
- (_key, candidate) => {
1576
- if (typeof candidate === "bigint") {
1577
- return candidate.toString() + "n";
1578
- }
1579
- if (typeof candidate === "object" && candidate !== null) {
1580
- if (seen.has(candidate)) {
1581
- return "[Circular]";
1582
- }
1583
- seen.add(candidate);
1584
- }
1585
- return candidate;
1586
- },
1587
- 2
1588
- ) ?? String(value);
1589
- } catch {
1590
- return String(value);
1591
- }
1592
- }
1593
- function formatArg(value) {
1594
- if (typeof value === "string") {
1595
- return value;
1596
- }
1597
- if (value instanceof Error) {
1598
- if (value.stack) {
1599
- return value.stack;
1600
- }
1601
- return value.name + ": " + value.message;
1602
- }
1603
- if (typeof value === "undefined") {
1604
- return "undefined";
1605
- }
1606
- if (typeof value === "function") {
1607
- return "[Function " + (value.name || "anonymous") + "]";
1608
- }
1609
- return safeStringify(value);
1610
- }
1611
- function formatEntryMessage(args) {
1612
- const message = args.map(formatArg).join(" ");
1613
- if (message.length <= 4e3) {
1614
- return message;
1615
- }
1616
- return message.slice(0, 3997) + "...";
1617
- }
1618
- function createEntry(level, args, id) {
1619
- return {
1620
- id,
1621
- level,
1622
- message: formatEntryMessage(args),
1623
- timestamp: Date.now()
1624
- };
1625
- }
1626
- function createButton2(label) {
1627
- const button = document.createElement("button");
1628
- button.type = "button";
1629
- button.textContent = label;
1630
- button.style.cssText = [
1631
- "appearance:none",
1632
- "border:1px solid rgba(255,255,255,0.18)",
1633
- "background:rgba(255,255,255,0.06)",
1634
- "color:#f8fafc",
1635
- "border-radius:999px",
1636
- "padding:6px 10px",
1637
- "font:600 12px/1.1 ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace",
1638
- "cursor:pointer"
1639
- ].join(";");
1640
- return button;
1641
- }
1642
- function getLevelAccent(level) {
1643
- if (level === "error") {
1644
- return {
1645
- lineBackground: "rgba(255, 109, 122, 0.08)"
1646
- };
1647
- }
1648
- if (level === "warn") {
1649
- return {
1650
- lineBackground: "rgba(255, 196, 94, 0.07)"
1651
- };
1652
- }
1653
- if (level === "info") {
1654
- return {
1655
- lineBackground: "rgba(82, 187, 255, 0.07)"
1656
- };
1657
- }
1658
- if (level === "debug") {
1659
- return {
1660
- lineBackground: "rgba(166, 137, 255, 0.07)"
1661
- };
1662
- }
1663
- return {
1664
- lineBackground: "rgba(117, 235, 191, 0.06)"
1665
- };
1666
- }
1667
- function getViewportSize() {
1668
- const browserWindow = getBrowserWindow2();
1669
- return {
1670
- width: Math.max(320, browserWindow?.innerWidth ?? 1280),
1671
- height: Math.max(240, browserWindow?.innerHeight ?? 720)
1672
- };
1673
- }
1674
- function clampPanelSize(size) {
1675
- const viewport = getViewportSize();
1676
- const maxWidth = Math.max(MIN_EXPANDED_WIDTH, viewport.width - OVERLAY_MARGIN * 2);
1677
- const maxHeight = Math.max(
1678
- MIN_EXPANDED_HEIGHT,
1679
- viewport.height - OVERLAY_MARGIN * 2
1680
- );
1681
- return {
1682
- width: Math.min(maxWidth, Math.max(MIN_EXPANDED_WIDTH, size.width)),
1683
- height: Math.min(maxHeight, Math.max(MIN_EXPANDED_HEIGHT, size.height))
1684
- };
1685
- }
1686
- function getOverlaySize(state) {
1687
- if (state.expanded && state.panelSize) {
1688
- return state.panelSize;
1689
- }
1690
- const rect = state.ui?.root && typeof state.ui.root.getBoundingClientRect === "function" ? state.ui.root.getBoundingClientRect() : null;
1691
- if (rect && Number.isFinite(rect.width) && Number.isFinite(rect.height)) {
1692
- return {
1693
- width: Math.max(1, rect.width),
1694
- height: Math.max(1, rect.height)
1695
- };
1696
- }
1697
- return state.expanded ? clampPanelSize({
1698
- width: DEFAULT_EXPANDED_WIDTH,
1699
- height: DEFAULT_EXPANDED_HEIGHT
1700
- }) : { width: DEFAULT_COLLAPSED_WIDTH, height: DEFAULT_COLLAPSED_HEIGHT };
1701
- }
1702
- function applyPanelSize(state) {
1703
- if (!state.ui) {
1704
- return;
1705
- }
1706
- if (!state.expanded) {
1707
- state.ui.root.style.width = "auto";
1708
- state.ui.panel.style.width = "min(565px, calc(100vw - 24px))";
1709
- state.ui.panel.style.height = "auto";
1710
- state.ui.entries.style.maxHeight = "min(36vh, 280px)";
1711
- return;
1712
- }
1713
- if (!state.panelSize) {
1714
- state.ui.root.style.width = "min(565px, calc(100vw - 24px))";
1715
- state.ui.panel.style.width = "min(565px, calc(100vw - 24px))";
1716
- state.ui.panel.style.height = "auto";
1717
- state.ui.entries.style.maxHeight = "min(36vh, 280px)";
1718
- return;
1719
- }
1720
- const nextSize = clampPanelSize(
1721
- state.panelSize
1722
- );
1723
- state.panelSize = nextSize;
1724
- state.ui.root.style.width = nextSize.width + "px";
1725
- state.ui.panel.style.width = "100%";
1726
- state.ui.panel.style.height = nextSize.height + "px";
1727
- state.ui.entries.style.maxHeight = Math.max(72, nextSize.height - 88) + "px";
1728
- }
1729
- function clampPosition(point, state) {
1730
- const viewport = getViewportSize();
1731
- const size = getOverlaySize(state);
1732
- const maxX = Math.max(OVERLAY_MARGIN, viewport.width - size.width - OVERLAY_MARGIN);
1733
- const maxY = Math.max(
1734
- OVERLAY_MARGIN,
1735
- viewport.height - size.height - OVERLAY_MARGIN
1736
- );
1737
- return {
1738
- x: Math.min(maxX, Math.max(OVERLAY_MARGIN, point.x)),
1739
- y: Math.min(maxY, Math.max(OVERLAY_MARGIN, point.y))
1740
- };
1741
- }
1742
- function applyOverlayPosition(state) {
1743
- if (!state.ui) {
1744
- return;
1745
- }
1746
- if (!state.position) {
1747
- const viewport = getViewportSize();
1748
- const size = getOverlaySize(state);
1749
- state.position = clampPosition(
1750
- {
1751
- x: viewport.width - size.width - OVERLAY_MARGIN,
1752
- y: viewport.height - size.height - OVERLAY_MARGIN
1753
- },
1754
- state
1755
- );
1756
- } else {
1757
- state.position = clampPosition(state.position, state);
1758
- }
1759
- state.ui.root.style.left = state.position.x + "px";
1760
- state.ui.root.style.top = state.position.y + "px";
1761
- }
1762
- function getPointFromMouseEvent(event) {
1763
- return { x: event.clientX, y: event.clientY };
1764
- }
1765
- function getPointFromTouchEvent(event) {
1766
- const touch = event.touches[0] ?? event.changedTouches[0];
1767
- if (!touch) {
1768
- return null;
1769
- }
1770
- return { x: touch.clientX, y: touch.clientY };
1771
- }
1772
- function stopDragging(state) {
1773
- if (state.isDragging || state.isResizing) {
1774
- state.suppressToggleClickUntil = Date.now() + 180;
1775
- }
1776
- state.isDragging = false;
1777
- state.dragStartPoint = null;
1778
- state.lastDragPoint = null;
1779
- state.removeDragListeners?.();
1780
- state.removeDragListeners = null;
1781
- if (state.ui) {
1782
- state.ui.panel.style.cursor = "default";
1783
- state.ui.dragZone.style.cursor = "grab";
1784
- state.ui.toggleButton.style.cursor = "grab";
1785
- }
1786
- }
1787
- function stopResizing(state) {
1788
- if (state.isResizing) {
1789
- state.suppressToggleClickUntil = Date.now() + 180;
1790
- }
1791
- state.isResizing = false;
1792
- state.resizeStartPoint = null;
1793
- state.resizeStartSize = null;
1794
- state.removeResizeListeners?.();
1795
- state.removeResizeListeners = null;
1796
- }
1797
- function beginDragTracking(state, startPoint) {
1798
- const doc = getDocument2();
1799
- if (!doc) {
65
+ function triggerHaptic(type) {
66
+ const bridge = getBridgeWindow();
67
+ if (typeof bridge?.triggerHaptic === "function") {
68
+ bridge.triggerHaptic(type);
1800
69
  return;
1801
70
  }
1802
- stopResizing(state);
1803
- stopDragging(state);
1804
- state.dragMoved = false;
1805
- state.dragStartPoint = startPoint;
1806
- state.lastDragPoint = startPoint;
1807
- const handlePointerMove = (nextPoint) => {
1808
- if (!state.dragStartPoint || !state.lastDragPoint || !nextPoint) {
1809
- return;
1810
- }
1811
- if (!state.isDragging) {
1812
- const deltaFromStartX = nextPoint.x - state.dragStartPoint.x;
1813
- const deltaFromStartY = nextPoint.y - state.dragStartPoint.y;
1814
- const distance = Math.sqrt(
1815
- deltaFromStartX * deltaFromStartX + deltaFromStartY * deltaFromStartY
1816
- );
1817
- if (distance < DRAG_THRESHOLD_PX) {
1818
- return;
1819
- }
1820
- state.isDragging = true;
1821
- state.dragMoved = true;
1822
- if (state.ui) {
1823
- state.ui.panel.style.cursor = "grabbing";
1824
- state.ui.dragZone.style.cursor = "grabbing";
1825
- state.ui.toggleButton.style.cursor = "grabbing";
1826
- }
1827
- }
1828
- const currentPosition = state.position ?? { x: OVERLAY_MARGIN, y: OVERLAY_MARGIN };
1829
- state.position = clampPosition(
1830
- {
1831
- x: currentPosition.x + (nextPoint.x - state.lastDragPoint.x),
1832
- y: currentPosition.y + (nextPoint.y - state.lastDragPoint.y)
1833
- },
1834
- state
71
+ if (isDevelopment()) {
72
+ console.warn(
73
+ "[oasiz/sdk] triggerHaptic bridge is unavailable. This is expected in local development."
1835
74
  );
1836
- state.lastDragPoint = nextPoint;
1837
- applyOverlayPosition(state);
1838
- };
1839
- const handleMouseMove = (event) => {
1840
- handlePointerMove(getPointFromMouseEvent(event));
1841
- };
1842
- const handleTouchMove = (event) => {
1843
- handlePointerMove(getPointFromTouchEvent(event));
1844
- };
1845
- const handleMouseUp = () => {
1846
- stopDragging(state);
1847
- };
1848
- const handleTouchEnd = () => {
1849
- stopDragging(state);
1850
- };
1851
- doc.addEventListener("mousemove", handleMouseMove);
1852
- doc.addEventListener("mouseup", handleMouseUp);
1853
- doc.addEventListener("touchmove", handleTouchMove, { passive: true });
1854
- doc.addEventListener("touchend", handleTouchEnd);
1855
- doc.addEventListener("touchcancel", handleTouchEnd);
1856
- state.removeDragListeners = () => {
1857
- doc.removeEventListener("mousemove", handleMouseMove);
1858
- doc.removeEventListener("mouseup", handleMouseUp);
1859
- doc.removeEventListener("touchmove", handleTouchMove);
1860
- doc.removeEventListener("touchend", handleTouchEnd);
1861
- doc.removeEventListener("touchcancel", handleTouchEnd);
1862
- };
1863
- }
1864
- function startResizing(state, startPoint) {
1865
- const doc = getDocument2();
1866
- if (!doc) {
1867
- return;
1868
- }
1869
- stopDragging(state);
1870
- stopResizing(state);
1871
- state.isResizing = true;
1872
- state.resizeStartPoint = startPoint;
1873
- state.resizeStartSize = state.panelSize ?? clampPanelSize({ width: DEFAULT_EXPANDED_WIDTH, height: DEFAULT_EXPANDED_HEIGHT });
1874
- const handleResizeMove = (nextPoint) => {
1875
- if (!state.isResizing || !state.resizeStartPoint || !state.resizeStartSize || !nextPoint) {
1876
- return;
1877
- }
1878
- state.panelSize = clampPanelSize({
1879
- width: state.resizeStartSize.width + (nextPoint.x - state.resizeStartPoint.x),
1880
- height: state.resizeStartSize.height + (nextPoint.y - state.resizeStartPoint.y)
1881
- });
1882
- applyPanelSize(state);
1883
- applyOverlayPosition(state);
1884
- };
1885
- const handleMouseMove = (event) => {
1886
- handleResizeMove(getPointFromMouseEvent(event));
1887
- };
1888
- const handleTouchMove = (event) => {
1889
- handleResizeMove(getPointFromTouchEvent(event));
1890
- };
1891
- const handleFinish = () => {
1892
- stopResizing(state);
1893
- };
1894
- doc.addEventListener("mousemove", handleMouseMove);
1895
- doc.addEventListener("mouseup", handleFinish);
1896
- doc.addEventListener("touchmove", handleTouchMove, { passive: true });
1897
- doc.addEventListener("touchend", handleFinish);
1898
- doc.addEventListener("touchcancel", handleFinish);
1899
- state.removeResizeListeners = () => {
1900
- doc.removeEventListener("mousemove", handleMouseMove);
1901
- doc.removeEventListener("mouseup", handleFinish);
1902
- doc.removeEventListener("touchmove", handleTouchMove);
1903
- doc.removeEventListener("touchend", handleFinish);
1904
- doc.removeEventListener("touchcancel", handleFinish);
1905
- };
1906
- }
1907
- function isInBottomRightResizeZone(element, point) {
1908
- const rect = element.getBoundingClientRect();
1909
- return point.x >= rect.right - RESIZE_HOTSPOT_PX && point.x <= rect.right && point.y >= rect.bottom - RESIZE_HOTSPOT_PX && point.y <= rect.bottom;
1910
- }
1911
- function canStartDragFromTarget(target) {
1912
- if (!(target instanceof Element)) {
1913
- return true;
1914
- }
1915
- if (target.closest("button") || target.closest("a") || target.closest("input") || target.closest("textarea") || target.closest("select")) {
1916
- return false;
1917
- }
1918
- return true;
1919
- }
1920
- function isInTopDragZone(element, point, zoneHeight) {
1921
- const rect = element.getBoundingClientRect();
1922
- return point.x >= rect.left && point.x <= rect.right && point.y >= rect.top && point.y <= rect.top + zoneHeight;
1923
- }
1924
- function attachDragStartListeners(element, state, options = {}) {
1925
- element.addEventListener("mousedown", (event) => {
1926
- if (!options.allowInteractiveTarget && !canStartDragFromTarget(event.target)) {
1927
- return;
1928
- }
1929
- const point = getPointFromMouseEvent(event);
1930
- if (typeof options.topZoneHeight === "number" && !isInTopDragZone(element, point, options.topZoneHeight)) {
1931
- return;
1932
- }
1933
- beginDragTracking(state, point);
1934
- });
1935
- element.addEventListener("touchstart", (event) => {
1936
- if (!options.allowInteractiveTarget && !canStartDragFromTarget(event.target)) {
1937
- return;
1938
- }
1939
- const point = getPointFromTouchEvent(event);
1940
- if (!point) {
1941
- return;
1942
- }
1943
- if (typeof options.topZoneHeight === "number" && !isInTopDragZone(element, point, options.topZoneHeight)) {
1944
- return;
1945
- }
1946
- beginDragTracking(state, point);
1947
- });
1948
- }
1949
- function attachCollapsedToggleListeners(element, state) {
1950
- const startToggleInteraction = (startPoint) => {
1951
- const doc = getDocument2();
1952
- if (!doc) {
1953
- return;
1954
- }
1955
- beginDragTracking(state, startPoint);
1956
- const finishInteraction = () => {
1957
- releaseListeners();
1958
- if (state.dragMoved || Date.now() < state.suppressToggleClickUntil) {
1959
- return;
1960
- }
1961
- state.expanded = true;
1962
- state.unreadCount = 0;
1963
- renderOverlay(state);
1964
- };
1965
- const releaseListeners = () => {
1966
- doc.removeEventListener("mouseup", finishInteraction);
1967
- doc.removeEventListener("touchend", finishInteraction);
1968
- doc.removeEventListener("touchcancel", finishInteraction);
1969
- };
1970
- doc.addEventListener("mouseup", finishInteraction);
1971
- doc.addEventListener("touchend", finishInteraction);
1972
- doc.addEventListener("touchcancel", finishInteraction);
1973
- };
1974
- element.addEventListener("mousedown", (event) => {
1975
- event.preventDefault();
1976
- startToggleInteraction(getPointFromMouseEvent(event));
1977
- });
1978
- element.addEventListener("touchstart", (event) => {
1979
- const point = getPointFromTouchEvent(event);
1980
- if (!point) {
1981
- return;
1982
- }
1983
- event.preventDefault();
1984
- startToggleInteraction(point);
1985
- });
1986
- }
1987
- function attachPanelResizeListeners(element, state) {
1988
- element.addEventListener("mousedown", (event) => {
1989
- if (!state.expanded || !canStartDragFromTarget(event.target)) {
1990
- return;
1991
- }
1992
- const point = getPointFromMouseEvent(event);
1993
- if (!isInBottomRightResizeZone(element, point)) {
1994
- return;
1995
- }
1996
- event.preventDefault();
1997
- event.stopPropagation();
1998
- startResizing(state, point);
1999
- });
2000
- element.addEventListener("touchstart", (event) => {
2001
- if (!state.expanded || !canStartDragFromTarget(event.target)) {
2002
- return;
2003
- }
2004
- const point = getPointFromTouchEvent(event);
2005
- if (!point || !isInBottomRightResizeZone(element, point)) {
2006
- return;
2007
- }
2008
- event.preventDefault();
2009
- event.stopPropagation();
2010
- startResizing(state, point);
2011
- });
2012
- }
2013
- function createOverlayUi(state) {
2014
- const root = document.createElement("div");
2015
- root.style.cssText = [
2016
- "position:fixed",
2017
- "left:12px",
2018
- "top:12px",
2019
- "z-index:2147483647",
2020
- "display:flex",
2021
- "flex-direction:column",
2022
- "align-items:stretch",
2023
- "gap:8px",
2024
- "width:min(565px, calc(100vw - 24px))",
2025
- "pointer-events:none"
2026
- ].join(";");
2027
- const toggleButton = createButton2("Logs");
2028
- toggleButton.style.pointerEvents = "auto";
2029
- toggleButton.style.alignSelf = "flex-end";
2030
- toggleButton.style.display = "inline-flex";
2031
- toggleButton.style.alignItems = "center";
2032
- toggleButton.style.justifyContent = "center";
2033
- toggleButton.style.minHeight = "40px";
2034
- toggleButton.style.minWidth = "76px";
2035
- toggleButton.style.padding = "8px 14px";
2036
- toggleButton.style.textAlign = "center";
2037
- toggleButton.style.border = "1px solid rgba(122, 212, 255, 0.22)";
2038
- toggleButton.style.background = "linear-gradient(180deg, rgba(13,31,54,0.98), rgba(8,19,37,0.98))";
2039
- toggleButton.style.boxShadow = "0 18px 40px rgba(4,12,24,0.34)";
2040
- toggleButton.style.cursor = "grab";
2041
- toggleButton.style.touchAction = "none";
2042
- const panel = document.createElement("div");
2043
- panel.style.cssText = [
2044
- "position:relative",
2045
- "display:flex",
2046
- "flex-direction:column",
2047
- "width:min(565px, calc(100vw - 24px))",
2048
- "max-height:min(48vh, 372px)",
2049
- "border-radius:18px",
2050
- "border:1px solid rgba(116,167,255,0.16)",
2051
- "background:linear-gradient(180deg, rgba(9,19,37,0.98), rgba(5,12,24,0.98))",
2052
- "box-shadow:0 28px 64px rgba(2,8,18,0.46)",
2053
- "backdrop-filter:blur(16px)",
2054
- "overflow:hidden",
2055
- "cursor:default",
2056
- "pointer-events:auto"
2057
- ].join(";");
2058
- const dragZone = document.createElement("div");
2059
- dragZone.style.cssText = [
2060
- "position:absolute",
2061
- "top:0",
2062
- "left:0",
2063
- "right:0",
2064
- "height:" + String(TOP_DRAG_ZONE_PX) + "px",
2065
- "z-index:1",
2066
- "cursor:grab",
2067
- "background:transparent",
2068
- "pointer-events:auto"
2069
- ].join(";");
2070
- const controls = document.createElement("div");
2071
- controls.style.cssText = [
2072
- "position:absolute",
2073
- "top:22px",
2074
- "right:22px",
2075
- "z-index:2",
2076
- "display:flex",
2077
- "align-items:center",
2078
- "gap:8px",
2079
- "pointer-events:auto"
2080
- ].join(";");
2081
- const clearButton = createButton2("Clear");
2082
- clearButton.style.background = "rgba(255,255,255,0.1)";
2083
- clearButton.style.border = "1px solid rgba(255,255,255,0.16)";
2084
- clearButton.style.color = "#eef6ff";
2085
- clearButton.style.minHeight = "30px";
2086
- clearButton.style.padding = "4px 9px";
2087
- clearButton.style.fontSize = "11px";
2088
- clearButton.style.backdropFilter = "blur(8px)";
2089
- const collapseButton = createButton2("Hide");
2090
- collapseButton.style.background = "rgba(113, 171, 255, 0.12)";
2091
- collapseButton.style.border = "1px solid rgba(113, 171, 255, 0.2)";
2092
- collapseButton.style.color = "#d9ebff";
2093
- collapseButton.style.minHeight = "30px";
2094
- collapseButton.style.padding = "4px 9px";
2095
- collapseButton.style.fontSize = "11px";
2096
- collapseButton.style.backdropFilter = "blur(8px)";
2097
- const body = document.createElement("div");
2098
- body.style.cssText = [
2099
- "position:relative",
2100
- "display:flex",
2101
- "flex-direction:column",
2102
- "padding:12px",
2103
- "background:linear-gradient(180deg, rgba(4,10,20,0.88), rgba(3,8,18,0.98))",
2104
- "flex:1 1 auto",
2105
- "min-height:0"
2106
- ].join(";");
2107
- const entries = document.createElement("div");
2108
- entries.style.cssText = [
2109
- "display:flex",
2110
- "flex-direction:column",
2111
- "gap:0",
2112
- "overflow:auto",
2113
- "flex:1 1 auto",
2114
- "min-height:96px",
2115
- "max-height:min(36vh, 280px)",
2116
- "padding:0",
2117
- "border:1px solid rgba(115,153,212,0.14)",
2118
- "border-radius:12px",
2119
- "background:rgba(4,10,20,0.82)"
2120
- ].join(";");
2121
- const emptyState = document.createElement("div");
2122
- emptyState.style.cssText = [
2123
- "display:flex",
2124
- "align-items:center",
2125
- "justify-content:center",
2126
- "flex:1 1 auto",
2127
- "min-height:96px",
2128
- "color:rgba(204,222,250,0.6)",
2129
- "font:500 12px/1.5 ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace",
2130
- "text-align:center",
2131
- "padding:18px"
2132
- ].join(";");
2133
- emptyState.textContent = "Console output will appear here.";
2134
- collapseButton.addEventListener("click", (event) => {
2135
- event.stopPropagation();
2136
- state.expanded = false;
2137
- renderOverlay(state);
2138
- });
2139
- clearButton.addEventListener("click", (event) => {
2140
- event.stopPropagation();
2141
- state.entries = [];
2142
- state.unreadCount = 0;
2143
- renderOverlay(state);
2144
- });
2145
- controls.appendChild(clearButton);
2146
- controls.appendChild(collapseButton);
2147
- entries.appendChild(emptyState);
2148
- body.appendChild(entries);
2149
- body.appendChild(controls);
2150
- panel.appendChild(dragZone);
2151
- panel.appendChild(body);
2152
- attachDragStartListeners(dragZone, state);
2153
- attachPanelResizeListeners(panel, state);
2154
- attachCollapsedToggleListeners(toggleButton, state);
2155
- root.appendChild(panel);
2156
- root.appendChild(toggleButton);
2157
- return {
2158
- body,
2159
- clearButton,
2160
- collapseButton,
2161
- controls,
2162
- dragZone,
2163
- emptyState,
2164
- entries,
2165
- panel,
2166
- root,
2167
- toggleButton
2168
- };
2169
- }
2170
- function renderOverlay(state) {
2171
- if (!state.ui) {
2172
- return;
2173
- }
2174
- state.ui.panel.style.display = state.expanded ? "flex" : "none";
2175
- state.ui.toggleButton.style.display = state.expanded ? "none" : "inline-flex";
2176
- state.ui.toggleButton.textContent = "Logs";
2177
- if (state.entries.length === 0) {
2178
- state.ui.entries.style.display = "flex";
2179
- state.ui.emptyState.style.display = "flex";
2180
- state.ui.entries.replaceChildren(state.ui.emptyState);
2181
- applyPanelSize(state);
2182
- applyOverlayPosition(state);
2183
- return;
2184
- }
2185
- state.ui.entries.style.display = "flex";
2186
- state.ui.emptyState.style.display = "none";
2187
- const nextChildren = state.entries.map((entry) => {
2188
- const accent = getLevelAccent(entry.level);
2189
- const row = document.createElement("div");
2190
- row.style.cssText = [
2191
- "display:flex",
2192
- "align-items:flex-start",
2193
- "gap:0",
2194
- "padding:4px 12px",
2195
- "background:" + accent.lineBackground
2196
- ].join(";");
2197
- const line = document.createElement("div");
2198
- line.textContent = formatTimestamp(entry.timestamp) + " " + entry.level.toUpperCase() + " " + entry.message;
2199
- line.style.cssText = [
2200
- "color:#ecf4ff",
2201
- "font:500 12px/1.5 ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace",
2202
- "white-space:pre-wrap",
2203
- "word-break:break-word",
2204
- "flex:1 1 auto"
2205
- ].join(";");
2206
- row.appendChild(line);
2207
- return row;
2208
- });
2209
- state.ui.entries.replaceChildren(...nextChildren);
2210
- state.ui.entries.scrollTop = state.ui.entries.scrollHeight;
2211
- applyPanelSize(state);
2212
- applyOverlayPosition(state);
2213
- }
2214
- function mountOverlay(state) {
2215
- const doc = getDocument2();
2216
- if (!doc?.body || state.ui) {
2217
- return;
2218
- }
2219
- state.ui = createOverlayUi(state);
2220
- doc.body.appendChild(state.ui.root);
2221
- applyPanelSize(state);
2222
- applyOverlayPosition(state);
2223
- renderOverlay(state);
2224
- }
2225
- function enqueueEntry(state, level, args) {
2226
- state.entries.push(createEntry(level, args, state.nextEntryId));
2227
- state.nextEntryId += 1;
2228
- if (state.entries.length > state.maxEntries) {
2229
- state.entries.splice(0, state.entries.length - state.maxEntries);
2230
- }
2231
- if (!state.expanded) {
2232
- state.unreadCount += 1;
2233
- }
2234
- renderOverlay(state);
2235
- }
2236
- function restoreConsole(snapshot) {
2237
- for (const method of CONSOLE_METHODS) {
2238
- console[method] = snapshot[method];
2239
- }
2240
- }
2241
- function patchConsole(state) {
2242
- for (const method of CONSOLE_METHODS) {
2243
- const original = state.originalConsole[method];
2244
- console[method] = (...args) => {
2245
- enqueueEntry(state, method, args);
2246
- original(...args);
2247
- };
2248
- }
2249
- }
2250
- function cleanupOverlay(browserWindow, state) {
2251
- restoreConsole(state.originalConsole);
2252
- stopResizing(state);
2253
- stopDragging(state);
2254
- if (state.domReadyHandler) {
2255
- getDocument2()?.removeEventListener("DOMContentLoaded", state.domReadyHandler);
2256
- state.domReadyHandler = void 0;
2257
- }
2258
- if (state.resizeHandler && typeof browserWindow.removeEventListener === "function") {
2259
- browserWindow.removeEventListener("resize", state.resizeHandler);
2260
- state.resizeHandler = void 0;
2261
75
  }
2262
- state.ui?.root.remove();
2263
- state.ui = null;
2264
- delete browserWindow.__oasizLogOverlayController__;
2265
- delete browserWindow.__oasizLogOverlayState__;
2266
76
  }
2267
- function createController(browserWindow, state) {
2268
- return {
2269
- retain() {
2270
- state.refCount += 1;
2271
- this.ensureMounted();
2272
- },
2273
- ensureMounted() {
2274
- const doc = getDocument2();
2275
- if (!doc) {
2276
- return;
2277
- }
2278
- if (doc.body) {
2279
- mountOverlay(state);
2280
- applyOverlayPosition(state);
2281
- return;
2282
- }
2283
- if (!state.domReadyHandler) {
2284
- state.domReadyHandler = () => {
2285
- mountOverlay(state);
2286
- state.domReadyHandler = void 0;
2287
- };
2288
- doc.addEventListener("DOMContentLoaded", state.domReadyHandler, {
2289
- once: true
2290
- });
2291
- }
2292
- },
2293
- clear() {
2294
- state.entries = [];
2295
- state.unreadCount = 0;
2296
- renderOverlay(state);
2297
- },
2298
- show() {
2299
- state.expanded = true;
2300
- state.unreadCount = 0;
2301
- renderOverlay(state);
2302
- },
2303
- hide() {
2304
- state.expanded = false;
2305
- renderOverlay(state);
2306
- },
2307
- isVisible() {
2308
- return state.expanded;
2309
- },
2310
- destroy() {
2311
- state.refCount = Math.max(0, state.refCount - 1);
2312
- if (state.refCount === 0) {
2313
- cleanupOverlay(browserWindow, state);
2314
- }
2315
- }
2316
- };
2317
- }
2318
- function enableLogOverlay(options = {}) {
2319
- if (options.enabled === false) {
2320
- return NOOP_HANDLE2;
2321
- }
2322
- const browserWindow = getBrowserWindow2();
2323
- const doc = getDocument2();
2324
- if (!browserWindow || !doc) {
2325
- return NOOP_HANDLE2;
2326
- }
2327
- const existingController = browserWindow.__oasizLogOverlayController__;
2328
- const existingState = browserWindow.__oasizLogOverlayState__;
2329
- if (existingController && existingState) {
2330
- existingController.retain();
2331
- if (typeof options.maxEntries === "number") {
2332
- existingState.maxEntries = clampMaxEntries(options.maxEntries);
2333
- if (existingState.entries.length > existingState.maxEntries) {
2334
- existingState.entries.splice(
2335
- 0,
2336
- existingState.entries.length - existingState.maxEntries
2337
- );
2338
- }
2339
- }
2340
- if (typeof options.collapsed === "boolean") {
2341
- existingState.expanded = !options.collapsed;
2342
- applyPanelSize(existingState);
2343
- if (existingState.expanded) {
2344
- existingState.unreadCount = 0;
2345
- }
2346
- }
2347
- if (typeof options.title === "string" && options.title.trim().length > 0) {
2348
- existingState.title = options.title.trim();
2349
- }
2350
- existingController.ensureMounted();
2351
- renderOverlay(existingState);
2352
- return existingController;
77
+
78
+ // src/multiplayer.ts
79
+ var NULLISH_ROOM_CODES = /* @__PURE__ */ new Set(["NULL", "NIL", "UNDEFINED"]);
80
+ function readInjectedRoomCode(value) {
81
+ if (typeof value !== "string") {
82
+ return void 0;
2353
83
  }
2354
- const state = {
2355
- dragMoved: false,
2356
- dragStartPoint: null,
2357
- entries: [],
2358
- expanded: options.collapsed !== true,
2359
- isDragging: false,
2360
- isResizing: false,
2361
- lastDragPoint: null,
2362
- maxEntries: clampMaxEntries(options.maxEntries),
2363
- nextEntryId: 1,
2364
- originalConsole: createConsoleSnapshot(),
2365
- panelSize: null,
2366
- position: null,
2367
- refCount: 1,
2368
- removeDragListeners: null,
2369
- removeResizeListeners: null,
2370
- resizeStartPoint: null,
2371
- resizeStartSize: null,
2372
- suppressToggleClickUntil: 0,
2373
- title: typeof options.title === "string" && options.title.trim().length > 0 ? options.title.trim() : DEFAULT_TITLE,
2374
- resizeHandler: void 0,
2375
- ui: null,
2376
- unreadCount: 0
2377
- };
2378
- const controller = createController(browserWindow, state);
2379
- browserWindow.__oasizLogOverlayState__ = state;
2380
- browserWindow.__oasizLogOverlayController__ = controller;
2381
- patchConsole(state);
2382
- if (typeof browserWindow.addEventListener === "function") {
2383
- state.resizeHandler = () => {
2384
- applyOverlayPosition(state);
2385
- };
2386
- browserWindow.addEventListener("resize", state.resizeHandler);
84
+ const normalized = value.toUpperCase().replace(/[^A-Z0-9]/g, "");
85
+ if (normalized.length < 4 || NULLISH_ROOM_CODES.has(normalized)) {
86
+ return void 0;
2387
87
  }
2388
- controller.ensureMounted();
2389
- return controller;
88
+ return normalized;
2390
89
  }
2391
-
2392
- // src/multiplayer.ts
2393
- function isDevelopment4() {
90
+ function isDevelopment2() {
2394
91
  const nodeEnv = globalThis.process?.env?.NODE_ENV;
2395
92
  return nodeEnv !== "production";
2396
93
  }
2397
- function getBridgeWindow4() {
94
+ function getBridgeWindow2() {
2398
95
  if (typeof window === "undefined") {
2399
96
  return void 0;
2400
97
  }
2401
98
  return window;
2402
99
  }
2403
- function shareRoomCode(roomCode, options) {
2404
- const bridge = getBridgeWindow4();
100
+ function shareRoomCode(roomCode) {
101
+ const bridge = getBridgeWindow2();
2405
102
  if (typeof bridge?.shareRoomCode === "function") {
2406
- bridge.shareRoomCode(roomCode, options);
103
+ bridge.shareRoomCode(roomCode);
2407
104
  return;
2408
105
  }
2409
- if (isDevelopment4()) {
106
+ if (isDevelopment2()) {
2410
107
  console.warn(
2411
108
  "[oasiz/sdk] shareRoomCode bridge is unavailable. This is expected in local development."
2412
109
  );
2413
110
  }
2414
111
  }
2415
- function openInviteModal() {
2416
- const bridge = getBridgeWindow4();
2417
- if (typeof bridge?.openInviteModal === "function") {
2418
- bridge.openInviteModal();
2419
- return;
2420
- }
2421
- if (isDevelopment4()) {
2422
- console.warn(
2423
- "[oasiz/sdk] openInviteModal bridge is unavailable. This is expected in local development."
2424
- );
2425
- }
2426
- }
2427
112
  function getGameId() {
2428
- const bridge = getBridgeWindow4();
113
+ const bridge = getBridgeWindow2();
2429
114
  return bridge?.__GAME_ID__;
2430
115
  }
2431
116
  function getRoomCode() {
2432
- const bridge = getBridgeWindow4();
2433
- return bridge?.__ROOM_CODE__;
2434
- }
2435
- function getPlayerId() {
2436
- const bridge = getBridgeWindow4();
2437
- return bridge?.__PLAYER_ID__;
117
+ const bridge = getBridgeWindow2();
118
+ return readInjectedRoomCode(bridge?.__ROOM_CODE__);
2438
119
  }
2439
120
  function getPlayerName() {
2440
- const bridge = getBridgeWindow4();
121
+ const bridge = getBridgeWindow2();
2441
122
  return bridge?.__PLAYER_NAME__;
2442
123
  }
2443
124
  function getPlayerAvatar() {
2444
- const bridge = getBridgeWindow4();
125
+ const bridge = getBridgeWindow2();
2445
126
  return bridge?.__PLAYER_AVATAR__;
2446
127
  }
2447
128
 
2448
129
  // src/score.ts
2449
- function isDevelopment5() {
130
+ function isDevelopment3() {
2450
131
  const nodeEnv = globalThis.process?.env?.NODE_ENV;
2451
132
  return nodeEnv !== "production";
2452
133
  }
2453
- function warnMissingBridge3(methodName) {
2454
- if (isDevelopment5()) {
134
+ function warnMissingBridge(methodName) {
135
+ if (isDevelopment3()) {
2455
136
  console.warn(
2456
137
  "[oasiz/sdk] " + methodName + " bridge is unavailable. This is expected in local development."
2457
138
  );
2458
139
  }
2459
140
  }
2460
- function getBridgeWindow5() {
141
+ function getBridgeWindow3() {
2461
142
  if (typeof window === "undefined") {
2462
143
  return void 0;
2463
144
  }
@@ -2465,92 +146,41 @@ function getBridgeWindow5() {
2465
146
  }
2466
147
  function submitScore(score) {
2467
148
  if (!Number.isFinite(score)) {
2468
- if (isDevelopment5()) {
149
+ if (isDevelopment3()) {
2469
150
  console.warn("[oasiz/sdk] submitScore expected a finite number:", score);
2470
151
  }
2471
152
  return;
2472
153
  }
2473
- const bridge = getBridgeWindow5();
154
+ const bridge = getBridgeWindow3();
2474
155
  const normalizedScore = Math.max(0, Math.floor(score));
2475
156
  if (typeof bridge?.submitScore === "function") {
2476
157
  bridge.submitScore(normalizedScore);
2477
158
  return;
2478
159
  }
2479
- warnMissingBridge3("submitScore");
2480
- }
2481
-
2482
- // src/score-edit.ts
2483
- function isDevelopment6() {
2484
- const nodeEnv = globalThis.process?.env?.NODE_ENV;
2485
- return nodeEnv !== "production";
2486
- }
2487
- function getBridgeWindow6() {
2488
- if (typeof window === "undefined") {
2489
- return void 0;
2490
- }
2491
- return window;
2492
- }
2493
- function warnMissingBridge4(methodName) {
2494
- if (isDevelopment6()) {
2495
- console.warn(
2496
- "[oasiz/sdk] " + methodName + " bridge is unavailable. This is expected in local development."
2497
- );
2498
- }
2499
- }
2500
- async function editScore(payload, methodName) {
2501
- const bridge = getBridgeWindow6();
2502
- if (typeof bridge?.__oasizEditScore !== "function") {
2503
- warnMissingBridge4(methodName);
2504
- return null;
2505
- }
2506
- try {
2507
- const result = await bridge.__oasizEditScore(payload);
2508
- return result ?? null;
2509
- } catch (error) {
2510
- if (isDevelopment6()) {
2511
- console.error("[oasiz/sdk] " + methodName + " failed:", error);
2512
- }
2513
- return null;
2514
- }
2515
- }
2516
- async function addScore(delta) {
2517
- if (!Number.isInteger(delta)) {
2518
- if (isDevelopment6()) {
2519
- console.warn("[oasiz/sdk] addScore expected an integer:", delta);
2520
- }
2521
- return null;
2522
- }
2523
- if (delta === 0) {
2524
- return null;
2525
- }
2526
- return editScore({ delta }, "addScore");
160
+ warnMissingBridge("submitScore");
2527
161
  }
2528
- async function setScore(score) {
2529
- if (!Number.isInteger(score) || score < 0) {
2530
- if (isDevelopment6()) {
2531
- console.warn(
2532
- "[oasiz/sdk] setScore expected a non-negative integer:",
2533
- score
2534
- );
2535
- }
2536
- return null;
162
+ function emitScoreConfig(config) {
163
+ const bridge = getBridgeWindow3();
164
+ if (typeof bridge?.emitScoreConfig === "function") {
165
+ bridge.emitScoreConfig(config);
166
+ return;
2537
167
  }
2538
- return editScore({ score }, "setScore");
168
+ warnMissingBridge("emitScoreConfig");
2539
169
  }
2540
170
 
2541
171
  // src/share.ts
2542
- function isDevelopment7() {
172
+ function isDevelopment4() {
2543
173
  const nodeEnv = globalThis.process?.env?.NODE_ENV;
2544
174
  return nodeEnv !== "production";
2545
175
  }
2546
- function getBridgeWindow7() {
176
+ function getBridgeWindow4() {
2547
177
  if (typeof window === "undefined") {
2548
178
  return void 0;
2549
179
  }
2550
180
  return window;
2551
181
  }
2552
- function warnMissingBridge5(methodName) {
2553
- if (isDevelopment7()) {
182
+ function warnMissingBridge2(methodName) {
183
+ if (isDevelopment4()) {
2554
184
  console.warn(
2555
185
  "[oasiz/sdk] " + methodName + " share bridge is unavailable. This is expected in local development."
2556
186
  );
@@ -2593,20 +223,20 @@ function validateRequest(options) {
2593
223
  }
2594
224
  async function share(options) {
2595
225
  const request = validateRequest(options);
2596
- const bridge = getBridgeWindow7();
226
+ const bridge = getBridgeWindow4();
2597
227
  if (typeof bridge?.__oasizShareRequest !== "function") {
2598
- warnMissingBridge5("__oasizShareRequest");
228
+ warnMissingBridge2("__oasizShareRequest");
2599
229
  throw new Error("Share bridge unavailable");
2600
230
  }
2601
231
  await bridge.__oasizShareRequest(request);
2602
232
  }
2603
233
 
2604
234
  // src/state.ts
2605
- function isDevelopment8() {
235
+ function isDevelopment5() {
2606
236
  const nodeEnv = globalThis.process?.env?.NODE_ENV;
2607
237
  return nodeEnv !== "production";
2608
238
  }
2609
- function getBridgeWindow8() {
239
+ function getBridgeWindow5() {
2610
240
  if (typeof window === "undefined") {
2611
241
  return void 0;
2612
242
  }
@@ -2619,22 +249,22 @@ function isPlainObject(value) {
2619
249
  const proto = Object.getPrototypeOf(value);
2620
250
  return proto === Object.prototype || proto === null;
2621
251
  }
2622
- function warnMissingBridge6(methodName) {
2623
- if (isDevelopment8()) {
252
+ function warnMissingBridge3(methodName) {
253
+ if (isDevelopment5()) {
2624
254
  console.warn(
2625
255
  "[oasiz/sdk] " + methodName + " bridge is unavailable. This is expected in local development."
2626
256
  );
2627
257
  }
2628
258
  }
2629
259
  function loadGameState() {
2630
- const bridge = getBridgeWindow8();
260
+ const bridge = getBridgeWindow5();
2631
261
  if (typeof bridge?.loadGameState !== "function") {
2632
- warnMissingBridge6("loadGameState");
262
+ warnMissingBridge3("loadGameState");
2633
263
  return {};
2634
264
  }
2635
265
  const state = bridge.loadGameState();
2636
266
  if (!isPlainObject(state)) {
2637
- if (isDevelopment8()) {
267
+ if (isDevelopment5()) {
2638
268
  console.warn(
2639
269
  "[oasiz/sdk] loadGameState returned invalid data. Falling back to empty object."
2640
270
  );
@@ -2645,35 +275,35 @@ function loadGameState() {
2645
275
  }
2646
276
  function saveGameState(state) {
2647
277
  if (!isPlainObject(state)) {
2648
- if (isDevelopment8()) {
278
+ if (isDevelopment5()) {
2649
279
  console.warn("[oasiz/sdk] saveGameState expected a plain object:", state);
2650
280
  }
2651
281
  return;
2652
282
  }
2653
- const bridge = getBridgeWindow8();
283
+ const bridge = getBridgeWindow5();
2654
284
  if (typeof bridge?.saveGameState === "function") {
2655
285
  bridge.saveGameState(state);
2656
286
  return;
2657
287
  }
2658
- warnMissingBridge6("saveGameState");
288
+ warnMissingBridge3("saveGameState");
2659
289
  }
2660
290
  function flushGameState() {
2661
- const bridge = getBridgeWindow8();
291
+ const bridge = getBridgeWindow5();
2662
292
  if (typeof bridge?.flushGameState === "function") {
2663
293
  bridge.flushGameState();
2664
294
  return;
2665
295
  }
2666
- warnMissingBridge6("flushGameState");
296
+ warnMissingBridge3("flushGameState");
2667
297
  }
2668
298
 
2669
299
  // src/lifecycle.ts
2670
- function isDevelopment9() {
300
+ function isDevelopment6() {
2671
301
  const nodeEnv = globalThis.process?.env?.NODE_ENV;
2672
302
  return nodeEnv !== "production";
2673
303
  }
2674
304
  function addLifecycleListener(eventName, callback) {
2675
305
  if (typeof window === "undefined") {
2676
- if (isDevelopment9()) {
306
+ if (isDevelopment6()) {
2677
307
  console.warn(
2678
308
  "[oasiz/sdk] " + eventName + " listener registered without a browser window. This is expected in local development."
2679
309
  );
@@ -2692,733 +322,253 @@ function onResume(callback) {
2692
322
  return addLifecycleListener("oasiz:resume", callback);
2693
323
  }
2694
324
 
2695
- // src/layout.ts
2696
- var INSET_SIDES = ["top", "right", "bottom", "left"];
2697
- var SIDE_TO_AXIS = {
2698
- top: "vertical",
2699
- right: "horizontal",
2700
- bottom: "vertical",
2701
- left: "horizontal"
2702
- };
2703
- function createInsetEdges(value) {
2704
- return {
2705
- top: value,
2706
- right: value,
2707
- bottom: value,
2708
- left: value
2709
- };
2710
- }
2711
- function isDevelopment10() {
325
+ // src/navigation.ts
326
+ var activeBackListeners = 0;
327
+ function isDevelopment7() {
2712
328
  const nodeEnv = globalThis.process?.env?.NODE_ENV;
2713
329
  return nodeEnv !== "production";
2714
330
  }
2715
- function getBridgeWindow9() {
331
+ function getBridgeWindow6() {
2716
332
  if (typeof window === "undefined") {
2717
333
  return void 0;
2718
334
  }
2719
335
  return window;
2720
336
  }
2721
- function warnMissingBridge7(methodName) {
2722
- if (isDevelopment10()) {
337
+ function warnMissingBridge4(methodName) {
338
+ if (isDevelopment7()) {
2723
339
  console.warn(
2724
340
  "[oasiz/sdk] " + methodName + " bridge is unavailable. This is expected in local development."
2725
341
  );
2726
342
  }
2727
343
  }
2728
- function isRecord2(value) {
2729
- return typeof value === "object" && value !== null;
2730
- }
2731
- function toFiniteNumber(value) {
2732
- if (typeof value !== "number" || !Number.isFinite(value)) {
2733
- if (typeof value !== "string") {
2734
- return void 0;
2735
- }
2736
- const parsed = Number.parseFloat(value.trim());
2737
- if (!Number.isFinite(parsed)) {
2738
- return void 0;
2739
- }
2740
- return parsed;
2741
- }
2742
- return value;
2743
- }
2744
- function clampInsetPixels(value) {
2745
- const numeric = toFiniteNumber(value);
2746
- if (typeof numeric === "undefined") {
2747
- return void 0;
2748
- }
2749
- return Math.max(0, numeric);
2750
- }
2751
- function normalizeInsetPercent(value) {
2752
- const numeric = toFiniteNumber(value);
2753
- if (typeof numeric === "undefined") {
2754
- return void 0;
344
+ function normalizeNavigationError(error) {
345
+ if (error instanceof Error) {
346
+ return error;
2755
347
  }
2756
- return Math.min(100, Math.max(0, numeric));
348
+ return new Error(
349
+ typeof error === "string" ? error : "Back button callback failed."
350
+ );
2757
351
  }
2758
- function getViewportSize2(bridge, axis) {
2759
- const visualViewportSize = axis === "vertical" ? bridge.visualViewport?.height : bridge.visualViewport?.width;
2760
- if (typeof visualViewportSize === "number" && Number.isFinite(visualViewportSize) && visualViewportSize > 0) {
2761
- return visualViewportSize;
2762
- }
2763
- const innerSize = axis === "vertical" ? bridge.innerHeight : bridge.innerWidth;
2764
- if (typeof innerSize === "number" && Number.isFinite(innerSize) && innerSize > 0) {
2765
- return innerSize;
2766
- }
2767
- const documentSize = axis === "vertical" ? bridge.document?.documentElement?.clientHeight : bridge.document?.documentElement?.clientWidth;
2768
- if (typeof documentSize === "number" && Number.isFinite(documentSize) && documentSize > 0) {
2769
- return documentSize;
2770
- }
2771
- const bodySize = axis === "vertical" ? bridge.document?.body?.clientHeight : bridge.document?.body?.clientWidth;
2772
- if (typeof bodySize === "number" && Number.isFinite(bodySize) && bodySize > 0) {
2773
- return bodySize;
352
+ function addNavigationListener(eventName, callback) {
353
+ if (typeof window === "undefined") {
354
+ if (isDevelopment7()) {
355
+ console.warn(
356
+ "[oasiz/sdk] " + eventName + " listener registered without a browser window. This is expected in local development."
357
+ );
358
+ }
359
+ return () => {
360
+ };
2774
361
  }
2775
- return 0;
362
+ const handler = () => callback();
363
+ window.addEventListener(eventName, handler);
364
+ return () => window.removeEventListener(eventName, handler);
2776
365
  }
2777
- function readCssSafeAreaValue(bridge, cssValue) {
2778
- const doc = bridge.document;
2779
- const root = doc?.body ?? doc?.documentElement;
2780
- if (!doc || !root || typeof doc.createElement !== "function" || typeof root.appendChild !== "function" || typeof bridge.getComputedStyle !== "function") {
2781
- return 0;
2782
- }
2783
- const probe = doc.createElement("div");
2784
- probe.style.position = "fixed";
2785
- probe.style.top = "0";
2786
- probe.style.left = "0";
2787
- probe.style.width = "0";
2788
- probe.style.height = "0";
2789
- probe.style.visibility = "hidden";
2790
- probe.style.pointerEvents = "none";
2791
- probe.style.paddingTop = cssValue;
2792
- root.appendChild(probe);
2793
- try {
2794
- return clampInsetPixels(bridge.getComputedStyle(probe).paddingTop) ?? 0;
2795
- } finally {
2796
- if (typeof probe.remove === "function") {
2797
- probe.remove();
366
+ function onBackButton(callback) {
367
+ const off = addNavigationListener("oasiz:back", () => {
368
+ try {
369
+ callback();
370
+ } catch (error) {
371
+ leaveGame();
372
+ throw normalizeNavigationError(error);
373
+ }
374
+ });
375
+ const bridge = getBridgeWindow6();
376
+ activeBackListeners += 1;
377
+ if (activeBackListeners === 1) {
378
+ if (typeof bridge?.__oasizSetBackOverride === "function") {
379
+ bridge.__oasizSetBackOverride(true);
2798
380
  } else {
2799
- probe.parentNode?.removeChild(probe);
381
+ warnMissingBridge4("__oasizSetBackOverride");
2800
382
  }
2801
383
  }
2802
- }
2803
- function readCssSafeAreaPixels(bridge, side) {
2804
- const envPixels = readCssSafeAreaValue(
2805
- bridge,
2806
- "env(safe-area-inset-" + side + ")"
2807
- );
2808
- if (envPixels > 0) {
2809
- return envPixels;
2810
- }
2811
- return readCssSafeAreaValue(
2812
- bridge,
2813
- "constant(safe-area-inset-" + side + ")"
2814
- );
2815
- }
2816
- function getDevicePixelRatio(bridge) {
2817
- const dpr = bridge.devicePixelRatio;
2818
- if (typeof dpr !== "number" || !Number.isFinite(dpr) || dpr <= 0) {
2819
- return 1;
2820
- }
2821
- return dpr;
2822
- }
2823
- function roughlyEqualPixels(a, b) {
2824
- return Math.abs(a - b) <= 2;
2825
- }
2826
- function normalizeInsetPixels(value, bridge, side) {
2827
- const pixels = clampInsetPixels(value);
2828
- if (typeof pixels === "undefined") {
2829
- return void 0;
2830
- }
2831
- const cssEnvPixels = readCssSafeAreaPixels(bridge, side);
2832
- if (pixels <= 0) {
2833
- return cssEnvPixels;
2834
- }
2835
- const dpr = getDevicePixelRatio(bridge);
2836
- if (cssEnvPixels > 0 && dpr > 1 && roughlyEqualPixels(pixels / dpr, cssEnvPixels)) {
2837
- return cssEnvPixels;
2838
- }
2839
- return pixels;
2840
- }
2841
- function pixelsToPercentOfViewport(pixels, bridge, side) {
2842
- const size = getViewportSize2(bridge, SIDE_TO_AXIS[side]);
2843
- if (size <= 0) {
2844
- return 0;
2845
- }
2846
- return normalizeInsetPercent(pixels / size * 100) ?? 0;
2847
- }
2848
- function percentToPixelsOfViewport(percent, bridge, side) {
2849
- const size = getViewportSize2(bridge, SIDE_TO_AXIS[side]);
2850
- if (size <= 0) {
2851
- return 0;
2852
- }
2853
- return percent / 100 * size;
2854
- }
2855
- function cssSafeAreaPercent(bridge, side) {
2856
- return pixelsToPercentOfViewport(readCssSafeAreaPixels(bridge, side), bridge, side);
2857
- }
2858
- function resolvePercentValue(value, bridge, side) {
2859
- const percent = normalizeInsetPercent(value);
2860
- if (typeof percent === "undefined") {
2861
- return void 0;
2862
- }
2863
- return percent > 0 ? percent : cssSafeAreaPercent(bridge, side);
2864
- }
2865
- function resolvePixelValue(value, bridge, side) {
2866
- const numeric = toFiniteNumber(value);
2867
- if (typeof numeric === "undefined") {
2868
- return void 0;
2869
- }
2870
- return normalizeInsetPixels(numeric, bridge, side);
2871
- }
2872
- function sideSuffix(side) {
2873
- switch (side) {
2874
- case "top":
2875
- return "Top";
2876
- case "right":
2877
- return "Right";
2878
- case "bottom":
2879
- return "Bottom";
2880
- case "left":
2881
- return "Left";
2882
- }
2883
- }
2884
- function callBridgeFunction(bridge, name) {
2885
- const fn = bridge[name];
2886
- if (typeof fn !== "function") {
2887
- return void 0;
2888
- }
2889
- try {
2890
- return fn.call(bridge);
2891
- } catch (error) {
2892
- console.error("[oasiz/sdk] " + String(name) + " failed:", error);
2893
- return void 0;
2894
- }
2895
- }
2896
- function readInsetObjectValue(value, side, group) {
2897
- if (!isRecord2(value)) {
2898
- return void 0;
2899
- }
2900
- if (group) {
2901
- return isRecord2(value[group]) ? value[group][side] : void 0;
2902
- }
2903
- return value[side];
2904
- }
2905
- function firstDefined(...values) {
2906
- return values.find((value) => typeof value !== "undefined");
2907
- }
2908
- function readHostInsetSources(bridge) {
2909
- return {
2910
- globalPixels: bridge.__OASIZ_VIEWPORT_INSETS__,
2911
- globalPercent: bridge.__OASIZ_VIEWPORT_INSETS_PERCENT__,
2912
- methodPixels: callBridgeFunction(bridge, "getViewportInsets"),
2913
- methodPercent: callBridgeFunction(bridge, "getViewportInsetsPercent")
2914
- };
2915
- }
2916
- function readIndividualPercentValue(bridge, side) {
2917
- const suffix = sideSuffix(side);
2918
- return firstDefined(
2919
- callBridgeFunction(
2920
- bridge,
2921
- "getSafeArea" + suffix + "Percent"
2922
- ),
2923
- bridge["__OASIZ_SAFE_AREA_" + side.toUpperCase() + "_PERCENT__"]
2924
- );
2925
- }
2926
- function readIndividualPixelValue(bridge, side) {
2927
- const suffix = sideSuffix(side);
2928
- return firstDefined(
2929
- callBridgeFunction(
2930
- bridge,
2931
- "getSafeArea" + suffix
2932
- ),
2933
- bridge["__OASIZ_SAFE_AREA_" + side.toUpperCase() + "__"]
2934
- );
2935
- }
2936
- function resolveInsetSide(bridge, sources, side) {
2937
- const percentCandidate = firstDefined(
2938
- readInsetObjectValue(sources.methodPercent, side, "percent"),
2939
- readInsetObjectValue(sources.methodPercent, side),
2940
- readInsetObjectValue(sources.globalPercent, side, "percent"),
2941
- readInsetObjectValue(sources.globalPercent, side),
2942
- readInsetObjectValue(sources.methodPixels, side, "percent"),
2943
- readInsetObjectValue(sources.globalPixels, side, "percent"),
2944
- readIndividualPercentValue(bridge, side)
2945
- );
2946
- const percent = resolvePercentValue(percentCandidate, bridge, side);
2947
- if (typeof percent !== "undefined") {
2948
- return {
2949
- pixels: percentToPixelsOfViewport(percent, bridge, side),
2950
- percent
2951
- };
2952
- }
2953
- const pixelCandidate = firstDefined(
2954
- readInsetObjectValue(sources.methodPixels, side, "pixels"),
2955
- readInsetObjectValue(sources.methodPixels, side),
2956
- readInsetObjectValue(sources.globalPixels, side, "pixels"),
2957
- readInsetObjectValue(sources.globalPixels, side),
2958
- readIndividualPixelValue(bridge, side)
2959
- );
2960
- const pixels = resolvePixelValue(pixelCandidate, bridge, side);
2961
- if (typeof pixels !== "undefined") {
2962
- return {
2963
- pixels,
2964
- percent: pixelsToPercentOfViewport(pixels, bridge, side)
2965
- };
2966
- }
2967
- const cssPixels = readCssSafeAreaPixels(bridge, side);
2968
- return {
2969
- pixels: cssPixels,
2970
- percent: pixelsToPercentOfViewport(cssPixels, bridge, side)
384
+ return () => {
385
+ off();
386
+ activeBackListeners = Math.max(0, activeBackListeners - 1);
387
+ if (activeBackListeners === 0) {
388
+ const currentBridge = getBridgeWindow6();
389
+ if (typeof currentBridge?.__oasizSetBackOverride === "function") {
390
+ currentBridge.__oasizSetBackOverride(false);
391
+ } else {
392
+ warnMissingBridge4("__oasizSetBackOverride");
393
+ }
394
+ }
2971
395
  };
2972
396
  }
2973
- function getViewportInsets() {
2974
- const bridge = getBridgeWindow9();
2975
- if (!bridge) {
2976
- return {
2977
- pixels: createInsetEdges(0),
2978
- percent: createInsetEdges(0)
2979
- };
2980
- }
2981
- const sources = readHostInsetSources(bridge);
2982
- const pixels = createInsetEdges(0);
2983
- const percent = createInsetEdges(0);
2984
- for (const side of INSET_SIDES) {
2985
- const resolved = resolveInsetSide(bridge, sources, side);
2986
- pixels[side] = resolved.pixels;
2987
- percent[side] = resolved.percent;
2988
- }
2989
- return { pixels, percent };
2990
- }
2991
- function getSafeAreaTop() {
2992
- const bridge = getBridgeWindow9();
2993
- if (!bridge) {
2994
- return 0;
2995
- }
2996
- const top = getViewportInsets().percent.top;
2997
- if (top <= 0) {
2998
- warnMissingBridge7("getSafeAreaTop");
2999
- }
3000
- return top;
397
+ function onLeaveGame(callback) {
398
+ return addNavigationListener("oasiz:leave", callback);
3001
399
  }
3002
- function setLeaderboardVisible(visible) {
3003
- if (typeof visible !== "boolean") {
3004
- if (isDevelopment10()) {
3005
- console.warn(
3006
- "[oasiz/sdk] setLeaderboardVisible expected a boolean:",
3007
- visible
3008
- );
3009
- }
3010
- return;
3011
- }
3012
- const bridge = getBridgeWindow9();
3013
- if (typeof bridge?.__oasizSetLeaderboardVisible === "function") {
3014
- bridge.__oasizSetLeaderboardVisible(visible);
400
+ function leaveGame() {
401
+ const bridge = getBridgeWindow6();
402
+ if (typeof bridge?.__oasizLeaveGame === "function") {
403
+ bridge.__oasizLeaveGame();
3015
404
  return;
3016
405
  }
3017
- warnMissingBridge7("__oasizSetLeaderboardVisible");
406
+ warnMissingBridge4("__oasizLeaveGame");
3018
407
  }
3019
408
 
3020
- // src/performance.ts
3021
- var DEFAULT_GRAPHICS_PERFORMANCE = {
3022
- fps: 45,
3023
- tier: "medium"
3024
- };
3025
- var TIER_DEFAULT_FPS = {
3026
- minimal: 24,
3027
- low: 30,
3028
- medium: 45,
3029
- high: 60
3030
- };
3031
- var cachedEstimatedGraphicsPerformance;
3032
- function getBridgeWindow10() {
409
+ // src/store.ts
410
+ function isDevelopment8() {
411
+ const nodeEnv = globalThis.process?.env?.NODE_ENV;
412
+ return nodeEnv !== "production";
413
+ }
414
+ function getBridgeWindow7() {
3033
415
  if (typeof window === "undefined") {
3034
416
  return void 0;
3035
417
  }
3036
418
  return window;
3037
419
  }
3038
- function isRecord3(value) {
3039
- return typeof value === "object" && value !== null;
3040
- }
3041
- function toFiniteNumber2(value) {
3042
- if (typeof value === "number" && Number.isFinite(value)) {
3043
- return value;
3044
- }
3045
- if (typeof value === "string") {
3046
- const parsed = Number.parseFloat(value.trim());
3047
- if (Number.isFinite(parsed)) {
3048
- return parsed;
3049
- }
420
+ function warnMissingBridge5(methodName) {
421
+ if (isDevelopment8()) {
422
+ console.warn(
423
+ "[oasiz/sdk] " + methodName + " store bridge is unavailable. This is expected in local development."
424
+ );
3050
425
  }
3051
- return void 0;
3052
- }
3053
- function clampScore(value) {
3054
- return Math.min(100, Math.max(0, Math.round(value)));
3055
- }
3056
- function clampFps(value) {
3057
- return Math.min(240, Math.max(1, Math.round(value)));
3058
426
  }
3059
- function tierFromScore(score) {
3060
- if (score < 25) return "minimal";
3061
- if (score < 40) return "low";
3062
- if (score < 70) return "medium";
3063
- return "high";
3064
- }
3065
- function tierFromFps(fps) {
3066
- if (fps < 30) return "minimal";
3067
- if (fps < 45) return "low";
3068
- if (fps < 58) return "medium";
3069
- return "high";
3070
- }
3071
- function fpsFromScore(score) {
3072
- return TIER_DEFAULT_FPS[tierFromScore(score)];
3073
- }
3074
- function normalizeTier(value) {
3075
- if (typeof value !== "string") {
3076
- return void 0;
427
+ async function requestStoreBridge(action, payload) {
428
+ const bridge = getBridgeWindow7();
429
+ if (typeof bridge?.__oasizStoreBridgeRequest !== "function") {
430
+ warnMissingBridge5(action);
431
+ throw new Error("Store bridge unavailable");
3077
432
  }
3078
- const normalized = value.trim().toLowerCase();
3079
- if (normalized === "minimal" || normalized === "safe") return "minimal";
3080
- if (normalized === "high") return "high";
3081
- if (normalized === "medium") return "medium";
3082
- if (normalized === "low") return "low";
3083
- return void 0;
433
+ return await bridge.__oasizStoreBridgeRequest({
434
+ action,
435
+ payload
436
+ });
3084
437
  }
3085
- function firstDefined2(...values) {
3086
- return values.find((value) => typeof value !== "undefined");
438
+ async function getFreshState() {
439
+ return await requestStoreBridge("getStoreState");
3087
440
  }
3088
- function normalizeMetric(value) {
3089
- if (typeof value === "undefined" || value === null) {
3090
- return void 0;
3091
- }
3092
- if (typeof value === "number" || typeof value === "string") {
3093
- const numeric = toFiniteNumber2(value);
3094
- if (typeof numeric !== "undefined") {
3095
- const fps2 = clampFps(numeric);
3096
- return { fps: fps2, tier: tierFromFps(fps2) };
3097
- }
3098
- const tier2 = normalizeTier(value);
3099
- if (tier2) {
3100
- return { fps: TIER_DEFAULT_FPS[tier2], tier: tier2 };
3101
- }
3102
- return void 0;
3103
- }
3104
- if (!isRecord3(value)) {
3105
- return void 0;
3106
- }
3107
- const fpsCandidate = firstDefined2(
3108
- value.fps,
3109
- value.targetFps,
3110
- value.frameRate,
3111
- value.framesPerSecond
3112
- );
3113
- const legacyScoreCandidate = firstDefined2(
3114
- value.score,
3115
- value.metric,
3116
- value.value,
3117
- value.performance,
3118
- value.performanceScore,
3119
- value.graphicsScore
3120
- );
3121
- const tierCandidate = firstDefined2(
3122
- value.tier,
3123
- value.graphicsTier,
3124
- value.performanceTier,
3125
- value.qualityTier,
3126
- value.recommendedTier
3127
- );
3128
- const tier = normalizeTier(tierCandidate);
3129
- const fpsNumeric = toFiniteNumber2(fpsCandidate);
3130
- const scoreNumeric = toFiniteNumber2(legacyScoreCandidate);
3131
- if (typeof fpsNumeric === "undefined" && typeof scoreNumeric === "undefined" && !tier) {
3132
- return void 0;
441
+ function subscribeToStoreEvent(eventName, callback) {
442
+ if (typeof window === "undefined") {
443
+ return () => {
444
+ };
3133
445
  }
3134
- const fps = clampFps(
3135
- typeof fpsNumeric !== "undefined" ? fpsNumeric : typeof scoreNumeric !== "undefined" ? fpsFromScore(clampScore(scoreNumeric)) : TIER_DEFAULT_FPS[tier]
3136
- );
3137
- return {
3138
- fps,
3139
- tier: tier ?? tierFromFps(fps)
446
+ const handler = (event) => {
447
+ const customEvent = event;
448
+ callback(customEvent.detail);
449
+ };
450
+ window.addEventListener(eventName, handler);
451
+ return () => {
452
+ window.removeEventListener(eventName, handler);
3140
453
  };
3141
454
  }
3142
- function callBridgeMetric(bridge, name) {
3143
- const fn = bridge[name];
3144
- if (typeof fn !== "function") {
3145
- return void 0;
3146
- }
3147
- try {
3148
- return fn.call(bridge);
3149
- } catch (error) {
3150
- console.error("[oasiz/sdk] " + String(name) + " failed:", error);
3151
- return void 0;
3152
- }
3153
- }
3154
- function readHostGraphicsPerformance(bridge) {
3155
- const candidates = [
3156
- callBridgeMetric(bridge, "getGraphicsPerformance"),
3157
- callBridgeMetric(bridge, "getGraphicsPerformanceMetric"),
3158
- callBridgeMetric(bridge, "getPerformanceMetric"),
3159
- bridge.__OASIZ_GRAPHICS_PERFORMANCE__,
3160
- bridge.__OASIZ_PERFORMANCE_METRIC__
3161
- ];
3162
- for (const candidate of candidates) {
3163
- const metric = normalizeMetric(candidate);
3164
- if (metric) {
3165
- return metric;
3166
- }
3167
- }
3168
- return void 0;
3169
- }
3170
- function getDevicePixelRatio2(bridge) {
3171
- const dpr = bridge.devicePixelRatio;
3172
- return typeof dpr === "number" && Number.isFinite(dpr) && dpr > 0 ? dpr : 1;
3173
- }
3174
- function getNavigatorValue(bridge) {
3175
- return bridge.navigator;
3176
- }
3177
- function parseIosMajorVersion(userAgent) {
3178
- const match = /\bOS (\d+)_/i.exec(userAgent);
3179
- if (!match) {
3180
- return void 0;
3181
- }
3182
- const parsed = Number.parseInt(match[1] ?? "", 10);
3183
- return Number.isFinite(parsed) ? parsed : void 0;
3184
- }
3185
- function isAppleMobileDevice(bridge) {
3186
- const userAgent = bridge.navigator?.userAgent ?? "";
3187
- return /iP(hone|ad|od)/i.test(userAgent) || /Macintosh/i.test(userAgent) && (bridge.navigator?.maxTouchPoints ?? 0) > 1;
3188
- }
3189
- function getLongestScreenEdge(bridge) {
3190
- const screenWidth = bridge.screen?.width;
3191
- const screenHeight = bridge.screen?.height;
3192
- const innerWidth = bridge.innerWidth;
3193
- const innerHeight = bridge.innerHeight;
3194
- return Math.max(
3195
- typeof screenWidth === "number" ? screenWidth : 0,
3196
- typeof screenHeight === "number" ? screenHeight : 0,
3197
- typeof innerWidth === "number" ? innerWidth : 0,
3198
- typeof innerHeight === "number" ? innerHeight : 0
3199
- );
455
+ async function syncProducts(products, expectedVersion) {
456
+ return await requestStoreBridge("syncProducts", {
457
+ expectedVersion: expectedVersion ?? null,
458
+ products
459
+ });
3200
460
  }
3201
- function isCoarsePointer(bridge) {
3202
- try {
3203
- return bridge.matchMedia?.("(pointer: coarse)").matches === true;
3204
- } catch {
3205
- return false;
3206
- }
461
+ async function getProducts() {
462
+ const state = await getFreshState();
463
+ return state.products;
3207
464
  }
3208
- function getWebGlInfo(bridge) {
3209
- const canvas = bridge.document?.createElement?.("canvas");
3210
- if (!canvas || typeof canvas.getContext !== "function") {
3211
- return { webglVersion: 0 };
3212
- }
3213
- const gl2 = canvas.getContext("webgl2");
3214
- const gl = gl2 ?? canvas.getContext("webgl") ?? canvas.getContext("experimental-webgl");
3215
- if (!gl) {
3216
- return { webglVersion: 0 };
3217
- }
3218
- const context = gl;
3219
- let maxTextureSize;
3220
- let renderer;
3221
- try {
3222
- const value = context.getParameter(context.MAX_TEXTURE_SIZE);
3223
- if (typeof value === "number" && Number.isFinite(value)) {
3224
- maxTextureSize = value;
3225
- }
3226
- } catch {
3227
- }
3228
- try {
3229
- const debugInfo = context.getExtension("WEBGL_debug_renderer_info");
3230
- const rendererParam = debugInfo?.UNMASKED_RENDERER_WEBGL ?? context.RENDERER;
3231
- const value = context.getParameter(rendererParam);
3232
- if (typeof value === "string") {
3233
- renderer = value;
3234
- }
3235
- } catch {
3236
- }
3237
- return {
3238
- maxTextureSize,
3239
- renderer,
3240
- webglVersion: gl2 ? 2 : 1
3241
- };
465
+ async function getEntitlements() {
466
+ const state = await getFreshState();
467
+ return state.entitlements;
3242
468
  }
3243
- function applyMemoryScore(score, memory) {
3244
- if (typeof memory !== "number" || !Number.isFinite(memory) || memory <= 0) {
3245
- return score;
3246
- }
3247
- if (memory <= 1) return score - 18;
3248
- if (memory <= 2) return score - 10;
3249
- if (memory >= 8) return score + 16;
3250
- if (memory >= 6) return score + 10;
3251
- return score;
469
+ async function hasEntitlement(productId) {
470
+ const state = await getFreshState();
471
+ return state.entitlements.find((item) => item.productId === productId)?.owned === true;
3252
472
  }
3253
- function applyCoreScore(score, cores) {
3254
- if (typeof cores !== "number" || !Number.isFinite(cores) || cores <= 0) {
3255
- return score;
3256
- }
3257
- if (cores <= 2) return score - 12;
3258
- if (cores <= 4) return score;
3259
- if (cores >= 8) return score + 12;
3260
- return score + 8;
473
+ async function getQuantity(productId) {
474
+ const state = await getFreshState();
475
+ return state.entitlements.find((item) => item.productId === productId)?.quantity ?? 0;
3261
476
  }
3262
- function applyWebGlScore(score, info) {
3263
- let next = score;
3264
- if (info.webglVersion === 2) {
3265
- next += 16;
3266
- } else if (info.webglVersion === 1) {
3267
- next += 6;
3268
- } else {
3269
- next -= 25;
3270
- }
3271
- if (typeof info.maxTextureSize === "number") {
3272
- if (info.maxTextureSize >= 8192) next += 10;
3273
- else if (info.maxTextureSize >= 4096) next += 3;
3274
- else next -= 8;
3275
- }
3276
- const renderer = info.renderer?.toLowerCase() ?? "";
3277
- if (/swiftshader|llvmpipe|software/.test(renderer)) {
3278
- next = Math.min(next, 35);
3279
- } else if (/\bm[1-9]\b|apple gpu|a1[6-9]|rtx|radeon|adreno 7|adreno 8/.test(renderer)) {
3280
- next += 8;
3281
- }
3282
- return next;
477
+ async function purchase(productId, quantity = 1) {
478
+ return await requestStoreBridge("purchaseProduct", {
479
+ productId,
480
+ quantity
481
+ });
3283
482
  }
3284
- function applyMobileCostScore(score, bridge) {
3285
- if (!isCoarsePointer(bridge)) {
3286
- return score;
3287
- }
3288
- const dpr = getDevicePixelRatio2(bridge);
3289
- const width = bridge.screen?.width ?? bridge.innerWidth ?? 0;
3290
- const height = bridge.screen?.height ?? bridge.innerHeight ?? 0;
3291
- const physicalPixels = width * height * dpr * dpr;
3292
- if (physicalPixels > 4e6) {
3293
- return score - 5;
3294
- }
3295
- return score;
483
+ async function consume(productId, quantity = 1) {
484
+ return await requestStoreBridge("consumeProduct", {
485
+ productId,
486
+ quantity
487
+ });
3296
488
  }
3297
- function applyAppleMobileRules(score, bridge) {
3298
- if (!isAppleMobileDevice(bridge)) {
3299
- return score;
3300
- }
3301
- const userAgent = bridge.navigator?.userAgent ?? "";
3302
- const iosMajorVersion = parseIosMajorVersion(userAgent);
3303
- const highDensityDisplay = getDevicePixelRatio2(bridge) >= 3;
3304
- const longestScreenEdge = getLongestScreenEdge(bridge);
3305
- if (typeof iosMajorVersion === "number" && iosMajorVersion < 15) {
3306
- return Math.min(score, 35);
3307
- }
3308
- if (highDensityDisplay && longestScreenEdge >= 430 && (typeof iosMajorVersion === "undefined" || iosMajorVersion >= 17)) {
3309
- return Math.max(score, 78);
3310
- }
3311
- if (highDensityDisplay && longestScreenEdge >= 414 && (typeof iosMajorVersion === "undefined" || iosMajorVersion >= 16)) {
3312
- return Math.max(score, 62);
3313
- }
3314
- return Math.min(score, 48);
489
+ async function getJemBalance() {
490
+ const state = await getFreshState();
491
+ return state.jemBalance;
3315
492
  }
3316
- function estimateGraphicsPerformance(bridge) {
3317
- const navigatorValue = getNavigatorValue(bridge);
3318
- let score = 45;
3319
- score = applyMemoryScore(score, navigatorValue?.deviceMemory);
3320
- score = applyCoreScore(score, navigatorValue?.hardwareConcurrency);
3321
- score = applyWebGlScore(score, getWebGlInfo(bridge));
3322
- score = applyMobileCostScore(score, bridge);
3323
- score = applyAppleMobileRules(score, bridge);
3324
- const normalizedScore = clampScore(score);
3325
- return {
3326
- fps: fpsFromScore(normalizedScore),
3327
- tier: tierFromScore(normalizedScore)
3328
- };
493
+ function onEntitlementsChanged(callback) {
494
+ return subscribeToStoreEvent("oasiz:entitlements-changed", (state) => {
495
+ callback(state.entitlements);
496
+ });
3329
497
  }
3330
- function getGraphicsPerformance() {
3331
- const bridge = getBridgeWindow10();
3332
- if (!bridge) {
3333
- return { ...DEFAULT_GRAPHICS_PERFORMANCE };
3334
- }
3335
- const hostMetric = readHostGraphicsPerformance(bridge);
3336
- if (hostMetric) {
3337
- return hostMetric;
3338
- }
3339
- cachedEstimatedGraphicsPerformance ??= estimateGraphicsPerformance(bridge);
3340
- return { ...cachedEstimatedGraphicsPerformance };
498
+ function onJemBalanceChanged(callback) {
499
+ return subscribeToStoreEvent("oasiz:jem-balance-changed", (state) => {
500
+ callback(state.jemBalance);
501
+ });
3341
502
  }
3342
503
 
3343
504
  // src/index.ts
3344
505
  var oasiz = {
3345
506
  submitScore,
3346
- enableAppSimulator,
3347
- addScore,
3348
- setScore,
3349
- getPlayerCharacter,
507
+ emitScoreConfig,
3350
508
  share,
3351
509
  triggerHaptic,
3352
- enableLogOverlay,
3353
510
  loadGameState,
3354
511
  saveGameState,
3355
512
  flushGameState,
3356
513
  shareRoomCode,
3357
- openInviteModal,
3358
514
  onPause,
3359
515
  onResume,
3360
- getSafeAreaTop,
3361
- getViewportInsets,
3362
- setLeaderboardVisible,
3363
- getGraphicsPerformance,
3364
- enableBackButtonTesting,
3365
516
  onBackButton,
3366
517
  onLeaveGame,
3367
518
  leaveGame,
519
+ store: {
520
+ syncProducts,
521
+ getProducts,
522
+ getEntitlements,
523
+ hasEntitlement,
524
+ getQuantity,
525
+ purchase,
526
+ consume,
527
+ getJemBalance,
528
+ onEntitlementsChanged,
529
+ onJemBalanceChanged
530
+ },
3368
531
  get gameId() {
3369
532
  return getGameId();
3370
533
  },
3371
534
  get roomCode() {
3372
535
  return getRoomCode();
3373
536
  },
3374
- get playerId() {
3375
- return getPlayerId();
3376
- },
3377
537
  get playerName() {
3378
538
  return getPlayerName();
3379
539
  },
3380
540
  get playerAvatar() {
3381
541
  return getPlayerAvatar();
3382
- },
3383
- get safeAreaTop() {
3384
- return getSafeAreaTop();
3385
- },
3386
- get viewportInsets() {
3387
- return getViewportInsets();
3388
- },
3389
- get graphicsPerformance() {
3390
- return getGraphicsPerformance();
3391
542
  }
3392
543
  };
3393
544
  // Annotate the CommonJS export names for ESM import in node:
3394
545
  0 && (module.exports = {
3395
- addScore,
3396
- enableAppSimulator,
3397
- enableBackButtonTesting,
3398
- enableLogOverlay,
546
+ consume,
547
+ emitScoreConfig,
3399
548
  flushGameState,
549
+ getEntitlements,
3400
550
  getGameId,
3401
- getGraphicsPerformance,
551
+ getJemBalance,
3402
552
  getPlayerAvatar,
3403
- getPlayerCharacter,
3404
- getPlayerId,
3405
553
  getPlayerName,
554
+ getProducts,
555
+ getQuantity,
3406
556
  getRoomCode,
3407
- getSafeAreaTop,
3408
- getViewportInsets,
557
+ hasEntitlement,
3409
558
  leaveGame,
3410
559
  loadGameState,
3411
560
  oasiz,
3412
561
  onBackButton,
562
+ onEntitlementsChanged,
563
+ onJemBalanceChanged,
3413
564
  onLeaveGame,
3414
565
  onPause,
3415
566
  onResume,
3416
- openInviteModal,
567
+ purchase,
3417
568
  saveGameState,
3418
- setLeaderboardVisible,
3419
- setScore,
3420
569
  share,
3421
570
  shareRoomCode,
3422
571
  submitScore,
572
+ syncProducts,
3423
573
  triggerHaptic
3424
574
  });