@series-inc/venus-sdk 3.4.3-beta.2 → 3.4.3-beta.4

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.
@@ -1076,758 +1076,8 @@ function cdnPlugin() {
1076
1076
  // raw-loader:/Users/pchan/Development/series/venus/venus-sdk/packages/api/src/vite/sandboxToolbarStyle.css
1077
1077
  var sandboxToolbarStyle_default = {};
1078
1078
 
1079
- // raw-loader:/Users/pchan/Development/series/venus/venus-sdk/packages/api/src/vite/sandboxToolbarScript.js
1080
- var sandboxToolbarScript_default = `/**\r
1081
- * Venus Sandbox Toolbar\r
1082
- *\r
1083
- * Floating toolbar that shows auth status and provides sign-in functionality.\r
1084
- * Auth state is managed by Firebase Auth and persisted in IndexedDB.\r
1085
- */\r
1086
- \r
1087
- const LOCAL_STORAGE_POS_KEY = 'venus-sandbox-pos'\r
1088
- \r
1089
- ;(async () => {\r
1090
- try {\r
1091
- if (!shouldRenderToolbar()) {\r
1092
- return\r
1093
- }\r
1094
- \r
1095
- await ensureDomReady()\r
1096
- \r
1097
- const sandboxConfig = window.__VENUS_SANDBOX__ || {}\r
1098
- const sandboxEnabled = isSandboxEnabled(sandboxConfig)\r
1099
- \r
1100
- if (!sandboxEnabled) {\r
1101
- return\r
1102
- }\r
1103
- \r
1104
- // Create minimal FAB with inline styles\r
1105
- createFloatingToolbar(sandboxConfig)\r
1106
- } catch (error) {\r
1107
- console.error('[Venus Sandbox] Failed to render toolbar:', error)\r
1108
- }\r
1109
- })()\r
1110
- \r
1111
- function shouldRenderToolbar() {\r
1112
- if (typeof window === 'undefined') {\r
1113
- return false\r
1114
- }\r
1115
- const host = location.hostname\r
1116
- return host === 'localhost' || host === '127.0.0.1' || host.endsWith('.local')\r
1117
- }\r
1118
- \r
1119
- function ensureDomReady() {\r
1120
- if (document.readyState === 'loading') {\r
1121
- return new Promise((resolve) => {\r
1122
- document.addEventListener('DOMContentLoaded', resolve, { once: true })\r
1123
- })\r
1124
- }\r
1125
- return Promise.resolve()\r
1126
- }\r
1127
- \r
1128
- function isSandboxEnabled(sandboxConfig) {\r
1129
- return Boolean(sandboxConfig && sandboxConfig.enabled === true)\r
1130
- }\r
1131
- \r
1132
- function createFloatingToolbar(sandboxConfig) {\r
1133
- // Create main FAB element with inline styles\r
1134
- const fab = document.createElement('div')\r
1135
- fab.id = 'venus-sandbox-toolbar'\r
1136
- \r
1137
- // Get environment for theming\r
1138
- const target = String(sandboxConfig?.target || 'local').toLowerCase()\r
1139
- const envGradients = {\r
1140
- local: 'linear-gradient(120deg, #f59e0b, #d97706)',\r
1141
- dev: 'linear-gradient(120deg, #0891b2, #10b981)',\r
1142
- staging: 'linear-gradient(120deg, #8b5cf6, #6366f1)',\r
1143
- }\r
1144
- const envLabels = {\r
1145
- local: 'LOCAL',\r
1146
- dev: 'DEV',\r
1147
- staging: 'STAGING',\r
1148
- }\r
1149
- const initialGradient = envGradients[target] || envGradients.local\r
1150
- const envLabel = envLabels[target] || 'LOCAL'\r
1151
- \r
1152
- // Get saved position or use defaults\r
1153
- const savedPos = localStorage.getItem(LOCAL_STORAGE_POS_KEY)\r
1154
- let posRight = 16\r
1155
- let posY = Math.round((window.innerHeight - 36) / 2)\r
1156
- \r
1157
- if (savedPos) {\r
1158
- try {\r
1159
- const parsed = JSON.parse(savedPos)\r
1160
- posRight = parsed.right\r
1161
- posY = parsed.y\r
1162
- } catch (e) {\r
1163
- // Ignore invalid saved position\r
1164
- }\r
1165
- }\r
1166
- \r
1167
- // Ensure within bounds\r
1168
- posRight = Math.min(Math.max(0, posRight), window.innerWidth - 36)\r
1169
- posY = Math.min(Math.max(0, posY), window.innerHeight - 36)\r
1170
- \r
1171
- // Start with "checking" state using env gradient\r
1172
- fab.style.cssText = \`\r
1173
- position: fixed;\r
1174
- right: \${posRight}px;\r
1175
- top: \${posY}px;\r
1176
- width: auto;\r
1177
- min-width: 160px;\r
1178
- height: 36px;\r
1179
- padding: 0;\r
1180
- margin: 0;\r
1181
- border: none;\r
1182
- border-radius: 999px;\r
1183
- background: \${initialGradient};\r
1184
- color: #ffffff;\r
1185
- font-family: system-ui, -apple-system, sans-serif;\r
1186
- font-size: 12px;\r
1187
- font-weight: 600;\r
1188
- letter-spacing: 0.02em;\r
1189
- cursor: grab;\r
1190
- user-select: none;\r
1191
- z-index: 2147483601;\r
1192
- box-shadow: 0 12px 25px rgba(0, 0, 0, 0.35);\r
1193
- display: flex;\r
1194
- align-items: center;\r
1195
- gap: 0;\r
1196
- pointer-events: auto;\r
1197
- border: 1px solid rgba(255, 255, 255, 0.2);\r
1198
- \`\r
1199
- \r
1200
- // Store env config on fab for later use\r
1201
- fab.dataset.target = target\r
1202
- fab.dataset.envGradient = initialGradient\r
1203
- \r
1204
- // Create env badge\r
1205
- const envBadge = document.createElement('div')\r
1206
- envBadge.style.cssText = \`\r
1207
- padding: 0 10px;\r
1208
- height: 100%;\r
1209
- display: flex;\r
1210
- align-items: center;\r
1211
- font-size: 10px;\r
1212
- font-weight: 700;\r
1213
- letter-spacing: 0.05em;\r
1214
- text-transform: uppercase;\r
1215
- background: rgba(0, 0, 0, 0.2);\r
1216
- border-top-left-radius: 999px;\r
1217
- border-bottom-left-radius: 999px;\r
1218
- \`\r
1219
- envBadge.textContent = envLabel\r
1220
- \r
1221
- // Create label container\r
1222
- const labelDiv = document.createElement('div')\r
1223
- labelDiv.style.cssText = \`\r
1224
- padding: 0 8px 0 10px;\r
1225
- height: 100%;\r
1226
- display: flex;\r
1227
- align-items: center;\r
1228
- white-space: nowrap;\r
1229
- cursor: pointer;\r
1230
- \`\r
1231
- labelDiv.textContent = '\u{1FA90} Checking...'\r
1232
- \r
1233
- // Create handle for dragging\r
1234
- const handleDiv = document.createElement('div')\r
1235
- handleDiv.style.cssText = \`\r
1236
- width: 38px;\r
1237
- height: 100%;\r
1238
- display: flex;\r
1239
- align-items: center;\r
1240
- justify-content: center;\r
1241
- font-size: 18px;\r
1242
- font-weight: 600;\r
1243
- background: rgba(0, 0, 0, 0.25);\r
1244
- border-top-right-radius: 999px;\r
1245
- border-bottom-right-radius: 999px;\r
1246
- cursor: grab;\r
1247
- \`\r
1248
- handleDiv.textContent = '\u271C'\r
1249
- \r
1250
- fab.appendChild(envBadge)\r
1251
- fab.appendChild(labelDiv)\r
1252
- fab.appendChild(handleDiv)\r
1253
- \r
1254
- const toolbarRoot = document.getElementById('venus-toolbar-root')\r
1255
- ;(toolbarRoot || document.body).appendChild(fab)\r
1256
- \r
1257
- // Set up drag handling\r
1258
- setupDragHandling(fab, handleDiv)\r
1259
- \r
1260
- // Set up collapse/expand behavior\r
1261
- setupCollapseExpand(fab, envBadge, labelDiv)\r
1262
- \r
1263
- // Click handler for label to open modal\r
1264
- labelDiv.addEventListener('click', () => {\r
1265
- openAuthModal(sandboxConfig, getCurrentAuthState())\r
1266
- })\r
1267
- \r
1268
- // Listen for auth state changes (set up by MockHost)\r
1269
- window.addEventListener('venus-auth-state-changed', (e) => {\r
1270
- updateToolbarState(fab, labelDiv, e.detail)\r
1271
- })\r
1272
- \r
1273
- // Check initial auth state after a short delay (give Firebase time to restore session)\r
1274
- setTimeout(() => {\r
1275
- checkInitialAuthState(fab, labelDiv, sandboxConfig)\r
1276
- }, 500)\r
1277
- }\r
1278
- \r
1279
- let currentAuthState = { signedIn: false, user: null }\r
1280
- \r
1281
- function getCurrentAuthState() {\r
1282
- return currentAuthState\r
1283
- }\r
1284
- \r
1285
- function checkInitialAuthState(fab, labelDiv, sandboxConfig) {\r
1286
- // Dispatch event to check auth state (MockHost will respond)\r
1287
- window.dispatchEvent(new CustomEvent('venus-check-auth-state'))\r
1288
- \r
1289
- // Also set a timeout to show sign-in prompt if no response\r
1290
- setTimeout(() => {\r
1291
- if (!currentAuthState.signedIn) {\r
1292
- updateToolbarState(fab, labelDiv, { signedIn: false, user: null })\r
1293
- }\r
1294
- }, 2000)\r
1295
- }\r
1296
- \r
1297
- function updateToolbarState(fab, labelDiv, authState) {\r
1298
- currentAuthState = authState\r
1299
- \r
1300
- // Get stored env gradient\r
1301
- const envGradient = fab.dataset.envGradient || 'linear-gradient(120deg, #f59e0b, #d97706)'\r
1302
- \r
1303
- if (authState.signedIn && authState.user) {\r
1304
- // Signed in - use env gradient with full opacity\r
1305
- fab.style.background = envGradient\r
1306
- fab.style.opacity = '1'\r
1307
- const displayName = authState.user.displayName || authState.user.email || 'Signed in'\r
1308
- labelDiv.textContent = \`\u2713 \${truncate(displayName, 14)}\`\r
1309
- } else {\r
1310
- // Not signed in - use env gradient but slightly dimmed, with sign-in prompt\r
1311
- fab.style.background = envGradient\r
1312
- fab.style.opacity = '0.85'\r
1313
- labelDiv.textContent = '\u25CB Sign in'\r
1314
- }\r
1315
- }\r
1316
- \r
1317
- function truncate(str, maxLength) {\r
1318
- if (str.length <= maxLength) return str\r
1319
- return str.substring(0, maxLength - 1) + '\u2026'\r
1320
- }\r
1321
- \r
1322
- function setupCollapseExpand(fab, envBadge, labelDiv) {\r
1323
- let collapseTimeout\r
1324
- let isCollapsed = false\r
1325
- \r
1326
- const collapse = () => {\r
1327
- fab.style.minWidth = '36px'\r
1328
- fab.style.opacity = '0.4'\r
1329
- envBadge.style.display = 'none'\r
1330
- labelDiv.style.display = 'none'\r
1331
- isCollapsed = true\r
1332
- }\r
1333
- \r
1334
- const expand = () => {\r
1335
- clearTimeout(collapseTimeout)\r
1336
- fab.style.minWidth = '160px'\r
1337
- fab.style.opacity = '1'\r
1338
- envBadge.style.display = 'flex'\r
1339
- labelDiv.style.display = 'flex'\r
1340
- isCollapsed = false\r
1341
- }\r
1342
- \r
1343
- const scheduleCollapse = () => {\r
1344
- clearTimeout(collapseTimeout)\r
1345
- collapseTimeout = setTimeout(collapse, 5000)\r
1346
- }\r
1347
- \r
1348
- // Collapse after 5 seconds\r
1349
- scheduleCollapse()\r
1350
- \r
1351
- fab.addEventListener('mouseenter', expand)\r
1352
- fab.addEventListener('mouseleave', () => {\r
1353
- if (!isCollapsed) scheduleCollapse()\r
1354
- })\r
1355
- }\r
1356
- \r
1357
- function setupDragHandling(fab, handleDiv) {\r
1358
- let isDragging = false\r
1359
- let startX, startY, offsetRight, offsetY\r
1360
- \r
1361
- fab.addEventListener('mousedown', (e) => {\r
1362
- isDragging = true\r
1363
- startX = e.clientX\r
1364
- startY = e.clientY\r
1365
- offsetRight = window.innerWidth - fab.offsetLeft - fab.offsetWidth\r
1366
- offsetY = fab.offsetTop\r
1367
- fab.style.cursor = 'grabbing'\r
1368
- e.preventDefault()\r
1369
- })\r
1370
- \r
1371
- document.addEventListener('mousemove', (e) => {\r
1372
- if (!isDragging) return\r
1373
- \r
1374
- let newRight = offsetRight - (e.clientX - startX)\r
1375
- let newTop = offsetY + (e.clientY - startY)\r
1376
- \r
1377
- newRight = Math.min(Math.max(0, newRight), window.innerWidth - 36)\r
1378
- newTop = Math.min(Math.max(0, newTop), window.innerHeight - 36)\r
1379
- \r
1380
- fab.style.right = \`\${newRight}px\`\r
1381
- fab.style.top = \`\${newTop}px\`\r
1382
- })\r
1383
- \r
1384
- document.addEventListener('mouseup', () => {\r
1385
- if (!isDragging) return\r
1386
- isDragging = false\r
1387
- fab.style.cursor = 'grab'\r
1388
- const currentRight = window.innerWidth - fab.offsetLeft - fab.offsetWidth\r
1389
- localStorage.setItem(LOCAL_STORAGE_POS_KEY, JSON.stringify({\r
1390
- right: currentRight,\r
1391
- y: fab.offsetTop,\r
1392
- }))\r
1393
- })\r
1394
- }\r
1395
- \r
1396
- function openAuthModal(sandboxConfig, authState) {\r
1397
- const overlay = document.createElement('div')\r
1398
- overlay.style.cssText = \`\r
1399
- position: fixed;\r
1400
- top: 0;\r
1401
- left: 0;\r
1402
- right: 0;\r
1403
- bottom: 0;\r
1404
- background: rgba(0, 0, 0, 0.6);\r
1405
- display: flex;\r
1406
- align-items: center;\r
1407
- justify-content: center;\r
1408
- z-index: 2147483602;\r
1409
- backdrop-filter: blur(4px);\r
1410
- \`\r
1411
- \r
1412
- const content = document.createElement('div')\r
1413
- content.style.cssText = \`\r
1414
- background: #111827;\r
1415
- color: #F3F4F6;\r
1416
- border: 1px solid rgba(255, 255, 255, 0.1);\r
1417
- border-radius: 16px;\r
1418
- width: 90%;\r
1419
- max-width: 420px;\r
1420
- box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.6);\r
1421
- font-family: system-ui, -apple-system, sans-serif;\r
1422
- overflow: hidden;\r
1423
- \`\r
1424
- \r
1425
- const target = String(sandboxConfig?.target || 'local').toLowerCase()\r
1426
- const targetUpper = target.toUpperCase()\r
1427
- const gameId = sandboxConfig?.gameId || 'Unknown'\r
1428
- const isSignedIn = authState.signedIn\r
1429
- const user = authState.user\r
1430
- const isLocal = target === 'local'\r
1431
- \r
1432
- // Environment-specific theming\r
1433
- const envConfig = {\r
1434
- local: {\r
1435
- gradient: 'linear-gradient(120deg, #f59e0b, #d97706)',\r
1436
- badge: '#f59e0b',\r
1437
- badgeBg: 'rgba(245, 158, 11, 0.15)',\r
1438
- label: 'LOCAL',\r
1439
- subtitle: 'Firebase Emulator',\r
1440
- },\r
1441
- dev: {\r
1442
- gradient: 'linear-gradient(120deg, #0891b2, #10b981)',\r
1443
- badge: '#10b981',\r
1444
- badgeBg: 'rgba(16, 185, 129, 0.15)',\r
1445
- label: 'DEV',\r
1446
- subtitle: 'Development Server',\r
1447
- },\r
1448
- staging: {\r
1449
- gradient: 'linear-gradient(120deg, #8b5cf6, #6366f1)',\r
1450
- badge: '#8b5cf6',\r
1451
- badgeBg: 'rgba(139, 92, 246, 0.15)',\r
1452
- label: 'STAGING',\r
1453
- subtitle: 'Staging Server',\r
1454
- },\r
1455
- }\r
1456
- \r
1457
- const env = envConfig[target] || envConfig.local\r
1458
- const statusIcon = isSignedIn ? '\u2713' : '\u25CB'\r
1459
- const statusText = isSignedIn ? 'Signed in' : 'Not signed in'\r
1460
- const statusColor = isSignedIn ? '#10b981' : '#6b7280'\r
1461
- const userDisplay = user ? (user.displayName || user.email || 'Unknown') : '\u2014'\r
1462
- const userIdShort = user?.uid ? user.uid.slice(0, 8) + '...' : ''\r
1463
- \r
1464
- content.innerHTML = \`\r
1465
- <div style="\r
1466
- background: \${env.gradient};\r
1467
- padding: 20px 24px;\r
1468
- display: flex;\r
1469
- justify-content: space-between;\r
1470
- align-items: center;\r
1471
- ">\r
1472
- <div style="display: flex; align-items: center; gap: 10px;">\r
1473
- <span style="font-size: 24px;">\u{1FA90}</span>\r
1474
- <div>\r
1475
- <div style="font-size: 16px; font-weight: 700; letter-spacing: 0.02em;">Venus Sandbox</div>\r
1476
- <div style="font-size: 11px; opacity: 0.85; margin-top: 2px;">\${env.subtitle}</div>\r
1477
- </div>\r
1478
- </div>\r
1479
- <button id="close-modal" style="\r
1480
- background: rgba(0, 0, 0, 0.2);\r
1481
- border: none;\r
1482
- color: white;\r
1483
- cursor: pointer;\r
1484
- font-size: 18px;\r
1485
- width: 32px;\r
1486
- height: 32px;\r
1487
- border-radius: 8px;\r
1488
- display: flex;\r
1489
- align-items: center;\r
1490
- justify-content: center;\r
1491
- transition: background 0.2s;\r
1492
- ">&times;</button>\r
1493
- </div>\r
1494
- \r
1495
- <div style="padding: 20px 24px; display: flex; flex-direction: column; gap: 16px;">\r
1496
- \r
1497
- <div style="\r
1498
- display: flex;\r
1499
- align-items: center;\r
1500
- gap: 12px;\r
1501
- padding: 14px 16px;\r
1502
- background: \${env.badgeBg};\r
1503
- border: 1px solid \${env.badge}33;\r
1504
- border-radius: 10px;\r
1505
- ">\r
1506
- <div style="\r
1507
- width: 10px;\r
1508
- height: 10px;\r
1509
- border-radius: 50%;\r
1510
- background: \${env.badge};\r
1511
- box-shadow: 0 0 8px \${env.badge};\r
1512
- "></div>\r
1513
- <div style="flex: 1;">\r
1514
- <div style="font-size: 14px; font-weight: 600; color: \${env.badge};">\${env.label}\${isLocal ? ' (Emulator)' : ''}</div>\r
1515
- <div style="font-size: 11px; color: #9CA3AF; margin-top: 2px;">Game: \${gameId}</div>\r
1516
- </div>\r
1517
- </div>\r
1518
- \r
1519
- <div style="\r
1520
- padding: 14px 16px;\r
1521
- background: rgba(255, 255, 255, 0.03);\r
1522
- border: 1px solid rgba(255, 255, 255, 0.06);\r
1523
- border-radius: 10px;\r
1524
- ">\r
1525
- <div style="display: flex; align-items: center; gap: 8px; margin-bottom: \${isSignedIn ? '10px' : '0'};">\r
1526
- <span style="\r
1527
- display: inline-flex;\r
1528
- align-items: center;\r
1529
- justify-content: center;\r
1530
- width: 20px;\r
1531
- height: 20px;\r
1532
- border-radius: 50%;\r
1533
- background: \${isSignedIn ? 'rgba(16, 185, 129, 0.2)' : 'rgba(107, 114, 128, 0.2)'};\r
1534
- color: \${statusColor};\r
1535
- font-size: 12px;\r
1536
- font-weight: 600;\r
1537
- ">\${statusIcon}</span>\r
1538
- <span style="font-size: 13px; color: \${statusColor}; font-weight: 500;">\${statusText}</span>\r
1539
- </div>\r
1540
- \${isSignedIn ? \`\r
1541
- <div style="\r
1542
- display: flex;\r
1543
- align-items: center;\r
1544
- gap: 10px;\r
1545
- padding-top: 10px;\r
1546
- border-top: 1px solid rgba(255, 255, 255, 0.06);\r
1547
- ">\r
1548
- <div style="\r
1549
- width: 36px;\r
1550
- height: 36px;\r
1551
- border-radius: 50%;\r
1552
- background: linear-gradient(135deg, #6366f1, #8b5cf6);\r
1553
- display: flex;\r
1554
- align-items: center;\r
1555
- justify-content: center;\r
1556
- font-size: 14px;\r
1557
- font-weight: 600;\r
1558
- color: white;\r
1559
- ">\${(userDisplay[0] || '?').toUpperCase()}</div>\r
1560
- <div style="flex: 1; min-width: 0;">\r
1561
- <div style="font-size: 13px; font-weight: 500; color: #F3F4F6; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;">\${userDisplay}</div>\r
1562
- <div style="font-size: 11px; color: #6B7280; font-family: monospace;">\${userIdShort}</div>\r
1563
- </div>\r
1564
- </div>\r
1565
- \` : \`\r
1566
- <div style="font-size: 12px; color: #6B7280; margin-top: 8px; line-height: 1.5;">\r
1567
- Sign in to use sandbox features. Your session persists across reloads.\r
1568
- </div>\r
1569
- \`}\r
1570
- </div>\r
1571
- \r
1572
- <div style="\r
1573
- padding: 12px 14px;\r
1574
- background: rgba(99, 102, 241, 0.08);\r
1575
- border: 1px solid rgba(99, 102, 241, 0.15);\r
1576
- border-radius: 8px;\r
1577
- font-size: 12px;\r
1578
- color: #a5b4fc;\r
1579
- line-height: 1.5;\r
1580
- ">\r
1581
- <div style="font-weight: 600; margin-bottom: 4px; color: #c7d2fe;">\u{1F4A1} Change environment:</div>\r
1582
- <code style="\r
1583
- display: block;\r
1584
- background: rgba(0, 0, 0, 0.3);\r
1585
- padding: 8px 10px;\r
1586
- border-radius: 5px;\r
1587
- font-family: 'SF Mono', Monaco, monospace;\r
1588
- font-size: 11px;\r
1589
- color: #e0e7ff;\r
1590
- margin-top: 6px;\r
1591
- ">venus set-env &lt;local|dev|staging&gt;</code>\r
1592
- </div>\r
1593
- \r
1594
- \${isLocal ? \`\r
1595
- <div style="\r
1596
- padding: 10px 14px;\r
1597
- background: rgba(245, 158, 11, 0.08);\r
1598
- border: 1px solid rgba(245, 158, 11, 0.15);\r
1599
- border-radius: 8px;\r
1600
- font-size: 11px;\r
1601
- color: #fcd34d;\r
1602
- line-height: 1.5;\r
1603
- ">\r
1604
- <strong>Emulator mode:</strong> Profiles are auto-created on first sign-in.\r
1605
- <a href="http://localhost:4000" target="_blank" style="color: #fbbf24; text-decoration: underline; margin-left: 4px;">Open Emulator UI \u2192</a>\r
1606
- </div>\r
1607
- \` : ''}\r
1608
- \r
1609
- \${isSignedIn ? \`\r
1610
- <div style="\r
1611
- padding: 12px 14px;\r
1612
- background: rgba(255, 255, 255, 0.03);\r
1613
- border: 1px solid rgba(255, 255, 255, 0.06);\r
1614
- border-radius: 8px;\r
1615
- ">\r
1616
- <div style="font-size: 11px; font-weight: 600; color: #9CA3AF; margin-bottom: 10px; text-transform: uppercase; letter-spacing: 0.05em;">Storage Debug</div>\r
1617
- <div style="display: flex; gap: 8px;">\r
1618
- <button id="copy-app-storage" style="\r
1619
- flex: 1;\r
1620
- background: rgba(99, 102, 241, 0.1);\r
1621
- color: #a5b4fc;\r
1622
- border: 1px solid rgba(99, 102, 241, 0.2);\r
1623
- border-radius: 6px;\r
1624
- padding: 8px 12px;\r
1625
- font-size: 12px;\r
1626
- font-weight: 500;\r
1627
- cursor: pointer;\r
1628
- transition: background 0.2s;\r
1629
- ">\u{1F4CB} App Storage</button>\r
1630
- <button id="copy-global-storage" style="\r
1631
- flex: 1;\r
1632
- background: rgba(99, 102, 241, 0.1);\r
1633
- color: #a5b4fc;\r
1634
- border: 1px solid rgba(99, 102, 241, 0.2);\r
1635
- border-radius: 6px;\r
1636
- padding: 8px 12px;\r
1637
- font-size: 12px;\r
1638
- font-weight: 500;\r
1639
- cursor: pointer;\r
1640
- transition: background 0.2s;\r
1641
- ">\u{1F4CB} Global Storage</button>\r
1642
- </div>\r
1643
- </div>\r
1644
- \` : ''}\r
1645
- \r
1646
- <div style="display: flex; gap: 10px; margin-top: 4px;">\r
1647
- \${!isSignedIn ? \`\r
1648
- <button id="sign-in-google" style="\r
1649
- flex: 1;\r
1650
- background: \${env.gradient};\r
1651
- color: white;\r
1652
- border: none;\r
1653
- border-radius: 8px;\r
1654
- padding: 12px 16px;\r
1655
- font-size: 14px;\r
1656
- font-weight: 600;\r
1657
- cursor: pointer;\r
1658
- transition: opacity 0.2s, transform 0.1s;\r
1659
- box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);\r
1660
- ">Sign In</button>\r
1661
- \` : \`\r
1662
- <button id="sign-out" style="\r
1663
- flex: 1;\r
1664
- background: rgba(239, 68, 68, 0.1);\r
1665
- color: #f87171;\r
1666
- border: 1px solid rgba(239, 68, 68, 0.2);\r
1667
- border-radius: 8px;\r
1668
- padding: 12px 16px;\r
1669
- font-size: 14px;\r
1670
- font-weight: 600;\r
1671
- cursor: pointer;\r
1672
- transition: background 0.2s;\r
1673
- ">Sign Out</button>\r
1674
- \`}\r
1675
- <button id="reload-page" style="\r
1676
- background: rgba(255, 255, 255, 0.05);\r
1677
- color: #9CA3AF;\r
1678
- border: 1px solid rgba(255, 255, 255, 0.1);\r
1679
- border-radius: 8px;\r
1680
- padding: 12px 16px;\r
1681
- font-size: 14px;\r
1682
- font-weight: 600;\r
1683
- cursor: pointer;\r
1684
- transition: background 0.2s;\r
1685
- ">\u21BB Reload</button>\r
1686
- </div>\r
1687
- </div>\r
1688
- \`\r
1689
- \r
1690
- overlay.appendChild(content)\r
1691
- document.body.appendChild(overlay)\r
1692
- \r
1693
- // Event handlers\r
1694
- const closeModal = () => overlay.remove()\r
1695
- \r
1696
- document.getElementById('close-modal').addEventListener('click', closeModal)\r
1697
- overlay.addEventListener('click', (e) => {\r
1698
- if (e.target === overlay) closeModal()\r
1699
- })\r
1700
- \r
1701
- document.getElementById('reload-page')?.addEventListener('click', () => {\r
1702
- window.location.reload()\r
1703
- })\r
1704
- \r
1705
- const signInBtn = document.getElementById('sign-in-google')\r
1706
- if (signInBtn) {\r
1707
- const originalGradient = env.gradient\r
1708
- signInBtn.addEventListener('click', async () => {\r
1709
- try {\r
1710
- signInBtn.textContent = 'Signing in...'\r
1711
- signInBtn.disabled = true\r
1712
- \r
1713
- if (window.__VENUS_SANDBOX_SIGN_IN__) {\r
1714
- await window.__VENUS_SANDBOX_SIGN_IN__()\r
1715
- // Sign-in successful - show success and reload\r
1716
- signInBtn.textContent = '\u2713 Success!'\r
1717
- signInBtn.style.background = 'linear-gradient(120deg, #10b981, #059669)'\r
1718
- setTimeout(() => {\r
1719
- closeModal()\r
1720
- window.location.reload()\r
1721
- }, 500)\r
1722
- } else {\r
1723
- throw new Error('Sign-in function not available. Is the SDK initialized?')\r
1724
- }\r
1725
- } catch (error) {\r
1726
- console.error('[Venus Sandbox] Sign-in failed:', error)\r
1727
- signInBtn.textContent = 'Sign-in failed'\r
1728
- signInBtn.style.background = '#ef4444'\r
1729
- setTimeout(() => {\r
1730
- signInBtn.textContent = 'Sign In'\r
1731
- signInBtn.style.background = originalGradient\r
1732
- signInBtn.disabled = false\r
1733
- }, 2000)\r
1734
- }\r
1735
- })\r
1736
- }\r
1737
- \r
1738
- const signOutBtn = document.getElementById('sign-out')\r
1739
- if (signOutBtn) {\r
1740
- signOutBtn.addEventListener('click', async () => {\r
1741
- try {\r
1742
- signOutBtn.textContent = 'Signing out...'\r
1743
- signOutBtn.disabled = true\r
1744
- \r
1745
- if (window.__VENUS_SANDBOX_SIGN_OUT__) {\r
1746
- await window.__VENUS_SANDBOX_SIGN_OUT__()\r
1747
- signOutBtn.textContent = '\u2713 Signed out'\r
1748
- setTimeout(() => {\r
1749
- closeModal()\r
1750
- window.location.reload()\r
1751
- }, 500)\r
1752
- }\r
1753
- } catch (error) {\r
1754
- console.error('[Venus Sandbox] Sign-out failed:', error)\r
1755
- signOutBtn.textContent = 'Failed'\r
1756
- setTimeout(() => {\r
1757
- signOutBtn.textContent = 'Sign out'\r
1758
- signOutBtn.disabled = false\r
1759
- }, 2000)\r
1760
- }\r
1761
- })\r
1762
- }\r
1763
- \r
1764
- // Storage debug copy handlers\r
1765
- const copyAppStorageBtn = document.getElementById('copy-app-storage')\r
1766
- if (copyAppStorageBtn) {\r
1767
- copyAppStorageBtn.addEventListener('click', async () => {\r
1768
- await copyStorageToClipboard(copyAppStorageBtn, 'appStorage')\r
1769
- })\r
1770
- }\r
1771
- \r
1772
- const copyGlobalStorageBtn = document.getElementById('copy-global-storage')\r
1773
- if (copyGlobalStorageBtn) {\r
1774
- copyGlobalStorageBtn.addEventListener('click', async () => {\r
1775
- await copyStorageToClipboard(copyGlobalStorageBtn, 'globalStorage')\r
1776
- })\r
1777
- }\r
1778
- }\r
1779
- \r
1780
- async function copyStorageToClipboard(btn, storageType) {\r
1781
- const originalText = btn.textContent\r
1782
- try {\r
1783
- btn.textContent = 'Loading...'\r
1784
- btn.disabled = true\r
1785
- \r
1786
- // Call the SDK directly - VenusAPI is available globally\r
1787
- const api = window.VenusAPI\r
1788
- if (!api) {\r
1789
- throw new Error('VenusAPI not available')\r
1790
- }\r
1791
- \r
1792
- const storage = storageType === 'appStorage' ? api.appStorage : api.globalStorage\r
1793
- if (!storage?.getAllData) {\r
1794
- throw new Error(\`\${storageType} not available or getAllData not supported\`)\r
1795
- }\r
1796
- \r
1797
- const data = await storage.getAllData()\r
1798
- const json = JSON.stringify(data, null, 2)\r
1799
- \r
1800
- await navigator.clipboard.writeText(json)\r
1801
- \r
1802
- const count = Object.keys(data).length\r
1803
- btn.textContent = \`\u2713 Copied (\${count} keys)\`\r
1804
- btn.style.background = 'rgba(16, 185, 129, 0.2)'\r
1805
- btn.style.color = '#34d399'\r
1806
- btn.style.borderColor = 'rgba(16, 185, 129, 0.3)'\r
1807
- \r
1808
- setTimeout(() => {\r
1809
- btn.textContent = originalText\r
1810
- btn.style.background = 'rgba(99, 102, 241, 0.1)'\r
1811
- btn.style.color = '#a5b4fc'\r
1812
- btn.style.borderColor = 'rgba(99, 102, 241, 0.2)'\r
1813
- btn.disabled = false\r
1814
- }, 2000)\r
1815
- } catch (error) {\r
1816
- console.error('[Venus Sandbox] Failed to copy storage:', error)\r
1817
- btn.textContent = 'Failed'\r
1818
- btn.style.background = 'rgba(239, 68, 68, 0.2)'\r
1819
- btn.style.color = '#f87171'\r
1820
- \r
1821
- setTimeout(() => {\r
1822
- btn.textContent = originalText\r
1823
- btn.style.background = 'rgba(99, 102, 241, 0.1)'\r
1824
- btn.style.color = '#a5b4fc'\r
1825
- btn.style.borderColor = 'rgba(99, 102, 241, 0.2)'\r
1826
- btn.disabled = false\r
1827
- }, 2000)\r
1828
- }\r
1829
- }\r
1830
- `;
1079
+ // compiled-string-loader:/Users/pchan/Development/series/venus/venus-sdk/packages/api/src/vite/sandboxToolbarScript.ts
1080
+ var sandboxToolbarScript_default = 'const LOCAL_STORAGE_POS_KEY = "venus-sandbox-pos";\nconst LOCAL_STORAGE_PLAYER_KEY = "venus-sandbox-selected-player";\nconst LOCAL_STORAGE_MAX_PLAYER_KEY = "venus-sandbox-max-player";\nlet inMemorySelectedPlayerId = "player1";\nfunction getMaxPlayerNumber() {\n const selected = getSelectedPlayerId();\n const selectedMatch = String(selected).match(/^player(\\d+)$/);\n const selectedN = selectedMatch ? parseInt(selectedMatch[1], 10) : 1;\n try {\n const raw = localStorage.getItem(LOCAL_STORAGE_MAX_PLAYER_KEY);\n const n = parseInt(String(raw || "").trim(), 10);\n const safe = Number.isFinite(n) && n >= 1 ? n : 1;\n return Math.max(safe, Number.isFinite(selectedN) ? selectedN : 1);\n } catch {\n return Math.max(1, Number.isFinite(selectedN) ? selectedN : 1);\n }\n}\nfunction setMaxPlayerNumber(n) {\n const safe = Number.isFinite(n) && n >= 1 ? Math.floor(n) : 1;\n try {\n localStorage.setItem(LOCAL_STORAGE_MAX_PLAYER_KEY, String(safe));\n } catch {\n }\n return safe;\n}\nfunction getSelectedPlayerId() {\n try {\n const raw = localStorage.getItem(LOCAL_STORAGE_PLAYER_KEY);\n const candidate = (raw || "").trim().toLowerCase();\n if (!candidate) return "player1";\n if (!/^player\\d+$/.test(candidate)) return "player1";\n const n = parseInt(candidate.replace("player", ""), 10);\n if (!Number.isFinite(n) || n < 1) return "player1";\n const normalized = `player${n}`;\n inMemorySelectedPlayerId = normalized;\n return normalized;\n } catch {\n return inMemorySelectedPlayerId || "player1";\n }\n}\nfunction setSelectedPlayerId(playerId) {\n const normalized = String(playerId || "").trim().toLowerCase();\n const safe = /^player\\d+$/.test(normalized) ? normalized : "player1";\n inMemorySelectedPlayerId = safe;\n try {\n localStorage.setItem(LOCAL_STORAGE_PLAYER_KEY, safe);\n } catch {\n }\n return safe;\n}\nfunction nextPlayerId(currentPlayerId) {\n const current = String(currentPlayerId || "").trim().toLowerCase();\n const match = current.match(/^player(\\d+)$/);\n const n = match ? parseInt(match[1], 10) : 1;\n const next = Number.isFinite(n) && n >= 1 ? n + 1 : 2;\n return `player${next}`;\n}\nfunction buildPlayerSelectOptions(selectedPlayerId, maxPlayerNumber) {\n const selected = String(selectedPlayerId || "player1").trim().toLowerCase();\n const maxN = Number.isFinite(maxPlayerNumber) && maxPlayerNumber >= 1 ? maxPlayerNumber : 1;\n let html = "";\n for (let i = 1; i <= maxN; i++) {\n const value = `player${i}`;\n const selectedAttr = value === selected ? " selected" : "";\n html += `<option value="${value}"${selectedAttr}>${value}</option>`;\n }\n html += `<option value="__new__">+ New player\\u2026</option>`;\n return html;\n}\n;\n(async () => {\n try {\n if (!shouldRenderToolbar()) {\n return;\n }\n await ensureDomReady();\n const sandboxConfig = window.__VENUS_SANDBOX__ || {};\n const sandboxEnabled = isSandboxEnabled(sandboxConfig);\n if (!sandboxEnabled) {\n return;\n }\n createFloatingToolbar(sandboxConfig);\n } catch (error) {\n console.error("[Venus Sandbox] Failed to render toolbar:", error);\n }\n})();\nfunction shouldRenderToolbar() {\n if (typeof window === "undefined") {\n return false;\n }\n const host = location.hostname;\n return host === "localhost" || host === "127.0.0.1" || host.endsWith(".local");\n}\nfunction ensureDomReady() {\n if (document.readyState === "loading") {\n return new Promise((resolve) => {\n document.addEventListener("DOMContentLoaded", resolve, { once: true });\n });\n }\n return Promise.resolve();\n}\nfunction isSandboxEnabled(sandboxConfig) {\n return Boolean(sandboxConfig && sandboxConfig.enabled === true);\n}\nfunction createFloatingToolbar(sandboxConfig) {\n const fab = document.createElement("div");\n fab.id = "venus-sandbox-toolbar";\n const target = String(sandboxConfig?.target || "local").toLowerCase();\n const envGradients = {\n local: "linear-gradient(120deg, #f59e0b, #d97706)",\n dev: "linear-gradient(120deg, #0891b2, #10b981)",\n staging: "linear-gradient(120deg, #8b5cf6, #6366f1)"\n };\n const envLabels = {\n local: "LOCAL",\n dev: "DEV",\n staging: "STAGING"\n };\n const initialGradient = envGradients[target] || envGradients.local;\n const envLabel = envLabels[target] || "LOCAL";\n const savedPos = localStorage.getItem(LOCAL_STORAGE_POS_KEY);\n let posRight = 16;\n let posY = Math.round((window.innerHeight - 36) / 2);\n if (savedPos) {\n try {\n const parsed = JSON.parse(savedPos);\n posRight = parsed.right;\n posY = parsed.y;\n } catch (e) {\n }\n }\n posRight = Math.min(Math.max(0, posRight), window.innerWidth - 36);\n posY = Math.min(Math.max(0, posY), window.innerHeight - 36);\n fab.style.cssText = `\n position: fixed;\n right: ${posRight}px;\n top: ${posY}px;\n width: auto;\n min-width: 160px;\n height: 36px;\n padding: 0;\n margin: 0;\n border: none;\n border-radius: 999px;\n background: ${initialGradient};\n color: #ffffff;\n font-family: system-ui, -apple-system, sans-serif;\n font-size: 12px;\n font-weight: 600;\n letter-spacing: 0.02em;\n cursor: grab;\n user-select: none;\n z-index: 2147483601;\n box-shadow: 0 12px 25px rgba(0, 0, 0, 0.35);\n display: flex;\n align-items: center;\n gap: 0;\n pointer-events: auto;\n border: 1px solid rgba(255, 255, 255, 0.2);\n `;\n fab.dataset.target = target;\n fab.dataset.envGradient = initialGradient;\n const envBadge = document.createElement("div");\n envBadge.style.cssText = `\n padding: 0 10px;\n height: 100%;\n display: flex;\n align-items: center;\n font-size: 10px;\n font-weight: 700;\n letter-spacing: 0.05em;\n text-transform: uppercase;\n background: rgba(0, 0, 0, 0.2);\n border-top-left-radius: 999px;\n border-bottom-left-radius: 999px;\n `;\n envBadge.textContent = envLabel;\n const labelDiv = document.createElement("div");\n labelDiv.style.cssText = `\n flex: 1;\n display: flex;\n align-items: center;\n justify-content: center;\n padding: 0 12px;\n height: 100%;\n font-size: 12px;\n font-weight: 600;\n background: rgba(0, 0, 0, 0.1);\n cursor: pointer;\n `;\n labelDiv.textContent = "\\u25CB Sign in";\n const handleDiv = document.createElement("div");\n handleDiv.style.cssText = `\n width: 36px;\n height: 100%;\n display: flex;\n align-items: center;\n justify-content: center;\n font-size: 18px;\n font-weight: 600;\n background: rgba(0, 0, 0, 0.25);\n border-top-right-radius: 999px;\n border-bottom-right-radius: 999px;\n cursor: grab;\n `;\n handleDiv.textContent = "\\u271C";\n fab.appendChild(envBadge);\n fab.appendChild(labelDiv);\n fab.appendChild(handleDiv);\n const toolbarRoot = document.getElementById("venus-toolbar-root");\n (toolbarRoot || document.body).appendChild(fab);\n setupDragHandling(fab, handleDiv);\n setupCollapseExpand(fab, envBadge, labelDiv);\n labelDiv.addEventListener("click", () => {\n openAuthModal(sandboxConfig, getCurrentAuthState());\n });\n window.addEventListener("venus-auth-state-changed", (e) => {\n updateToolbarState(fab, labelDiv, e.detail);\n });\n setTimeout(() => {\n checkInitialAuthState(fab, labelDiv, sandboxConfig);\n }, 500);\n}\nlet currentAuthState = { signedIn: false, user: null };\nfunction getCurrentAuthState() {\n return currentAuthState;\n}\nfunction checkInitialAuthState(fab, labelDiv, sandboxConfig) {\n window.dispatchEvent(new CustomEvent("venus-check-auth-state"));\n setTimeout(() => {\n if (!currentAuthState.signedIn) {\n updateToolbarState(fab, labelDiv, { signedIn: false, user: null });\n }\n }, 2e3);\n}\nfunction updateToolbarState(fab, labelDiv, authState) {\n currentAuthState = authState;\n const envGradient = fab.dataset.envGradient || "linear-gradient(120deg, #f59e0b, #d97706)";\n const isLocal = String(fab.dataset.target || "").toLowerCase() === "local";\n if (authState.signedIn && authState.user) {\n fab.style.background = envGradient;\n fab.style.opacity = "1";\n const displayName = authState.user.displayName || authState.user.email || "Signed in";\n labelDiv.textContent = `\\u2713 ${truncate(displayName, 14)}`;\n } else {\n fab.style.background = envGradient;\n fab.style.opacity = "0.85";\n labelDiv.textContent = isLocal ? `\\u25CB ${getSelectedPlayerId()}` : "\\u25CB Sign in";\n }\n}\nfunction truncate(str, maxLength) {\n if (str.length <= maxLength) return str;\n return str.substring(0, maxLength - 1) + "\\u2026";\n}\nfunction setupCollapseExpand(fab, envBadge, labelDiv) {\n let collapseTimeout;\n let isCollapsed = false;\n const collapse = () => {\n fab.style.minWidth = "36px";\n fab.style.opacity = "0.4";\n envBadge.style.display = "none";\n labelDiv.style.display = "none";\n isCollapsed = true;\n };\n const expand = () => {\n clearTimeout(collapseTimeout);\n fab.style.minWidth = "160px";\n fab.style.opacity = "1";\n envBadge.style.display = "flex";\n labelDiv.style.display = "flex";\n isCollapsed = false;\n };\n const scheduleCollapse = () => {\n clearTimeout(collapseTimeout);\n collapseTimeout = setTimeout(collapse, 5e3);\n };\n scheduleCollapse();\n fab.addEventListener("mouseenter", expand);\n fab.addEventListener("mouseleave", () => {\n if (!isCollapsed) scheduleCollapse();\n });\n}\nfunction setupDragHandling(fab, handleDiv) {\n let isDragging = false;\n let startX, startY, offsetRight, offsetY;\n fab.addEventListener("mousedown", (e) => {\n isDragging = true;\n startX = e.clientX;\n startY = e.clientY;\n offsetRight = window.innerWidth - fab.offsetLeft - fab.offsetWidth;\n offsetY = fab.offsetTop;\n fab.style.cursor = "grabbing";\n e.preventDefault();\n });\n document.addEventListener("mousemove", (e) => {\n if (!isDragging) return;\n let newRight = offsetRight - (e.clientX - startX);\n let newTop = offsetY + (e.clientY - startY);\n newRight = Math.min(Math.max(0, newRight), window.innerWidth - 36);\n newTop = Math.min(Math.max(0, newTop), window.innerHeight - 36);\n fab.style.right = `${newRight}px`;\n fab.style.top = `${newTop}px`;\n });\n document.addEventListener("mouseup", () => {\n if (!isDragging) return;\n isDragging = false;\n fab.style.cursor = "grab";\n const currentRight = window.innerWidth - fab.offsetLeft - fab.offsetWidth;\n localStorage.setItem(\n LOCAL_STORAGE_POS_KEY,\n JSON.stringify({\n right: currentRight,\n y: fab.offsetTop\n })\n );\n });\n}\nfunction openAuthModal(sandboxConfig, authState) {\n const overlay = document.createElement("div");\n overlay.style.cssText = `\n position: fixed;\n top: 0;\n left: 0;\n right: 0;\n bottom: 0;\n background: rgba(0, 0, 0, 0.6);\n display: flex;\n align-items: center;\n justify-content: center;\n z-index: 2147483602;\n backdrop-filter: blur(4px);\n `;\n const content = document.createElement("div");\n content.style.cssText = `\n background: #111827;\n color: #F3F4F6;\n border: 1px solid rgba(255, 255, 255, 0.1);\n border-radius: 16px;\n width: 90%;\n max-width: 420px;\n box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.6);\n font-family: system-ui, -apple-system, sans-serif;\n overflow: hidden;\n `;\n const target = String(sandboxConfig?.target || "local").toLowerCase();\n const targetUpper = target.toUpperCase();\n const gameId = sandboxConfig?.gameId || "Unknown";\n const isSignedIn = authState.signedIn;\n const user = authState.user;\n const isLocal = target === "local";\n const selectedPlayerId = isLocal ? getSelectedPlayerId() : null;\n const maxPlayerNumber = isLocal ? getMaxPlayerNumber() : 1;\n if (isLocal) {\n setMaxPlayerNumber(maxPlayerNumber);\n }\n const envConfig = {\n local: {\n gradient: "linear-gradient(120deg, #f59e0b, #d97706)",\n badge: "#f59e0b",\n badgeBg: "rgba(245, 158, 11, 0.15)",\n label: "LOCAL",\n subtitle: "Firebase Emulator"\n },\n dev: {\n gradient: "linear-gradient(120deg, #0891b2, #10b981)",\n badge: "#10b981",\n badgeBg: "rgba(16, 185, 129, 0.15)",\n label: "DEV",\n subtitle: "Development Server"\n },\n staging: {\n gradient: "linear-gradient(120deg, #8b5cf6, #6366f1)",\n badge: "#8b5cf6",\n badgeBg: "rgba(139, 92, 246, 0.15)",\n label: "STAGING",\n subtitle: "Staging Server"\n }\n };\n const env = envConfig[target] || envConfig.local;\n const statusIcon = isSignedIn ? "\\u2713" : "\\u25CB";\n const statusText = isSignedIn ? "Signed in" : "Not signed in";\n const statusColor = isSignedIn ? "#10b981" : "#6b7280";\n const userDisplay = user ? user.displayName || user.email || "Unknown" : "\\u2014";\n const userIdShort = user?.uid ? user.uid.slice(0, 8) + "..." : "";\n content.innerHTML = `\n <div style="\n background: ${env.gradient};\n padding: 20px 24px;\n display: flex;\n justify-content: space-between;\n align-items: center;\n ">\n <div style="display: flex; align-items: center; gap: 10px;">\n <span style="font-size: 24px;">\\u{1FA90}</span>\n <div>\n <div style="font-size: 16px; font-weight: 700;">Venus Sandbox</div>\n <div style="font-size: 12px; opacity: 0.9;">${env.subtitle}</div>\n </div>\n </div>\n <button id="close-modal" style="\n background: rgba(0, 0, 0, 0.15);\n border: none;\n color: white;\n width: 32px;\n height: 32px;\n border-radius: 999px;\n cursor: pointer;\n font-size: 18px;\n ">\\xD7</button>\n </div>\n\n <div style="padding: 24px;">\n <div style="display: flex; gap: 8px; flex-wrap: wrap; margin-bottom: 18px;">\n <span style="\n padding: 4px 10px;\n border-radius: 999px;\n font-size: 11px;\n font-weight: 700;\n background: ${env.badgeBg};\n color: ${env.badge};\n ">${targetUpper}</span>\n <span style="\n padding: 4px 10px;\n border-radius: 999px;\n font-size: 11px;\n font-weight: 700;\n background: rgba(255, 255, 255, 0.08);\n color: rgba(255, 255, 255, 0.85);\n ">gameId: ${gameId}</span>\n </div>\n\n <div style="\n padding: 14px 16px;\n background: rgba(255, 255, 255, 0.03);\n border: 1px solid rgba(255, 255, 255, 0.06);\n border-radius: 10px;\n margin-bottom: 14px;\n ">\n <div style="display:flex; align-items:center; justify-content:space-between; gap: 10px; flex-wrap: wrap;">\n <div style="display:flex; align-items:center; gap: 10px; flex: 1; min-width: 0;">\n <div style="\n width: 30px;\n height: 30px;\n border-radius: 999px;\n display:flex;\n align-items:center;\n justify-content:center;\n background: rgba(255,255,255,0.06);\n border: 1px solid rgba(255,255,255,0.08);\n font-weight: 800;\n color: ${statusColor};\n ">${statusIcon}</div>\n <div style="min-width: 0;">\n ${// Local emulator UX: when signed out, show the player dropdown right where the status goes.\n // When signed in, show the normal status + user display.\n !isSignedIn && isLocal ? `\n <div style="font-size: 11px; font-weight: 700; color: rgba(255,255,255,0.75); letter-spacing: 0.05em; text-transform: uppercase;">Local Player</div>\n <select id="local-player-select" style="\n margin-top: 6px;\n width: 180px;\n max-width: 45vw;\n min-width: 130px;\n background: rgba(0, 0, 0, 0.25);\n color: #F3F4F6;\n border: 1px solid rgba(255, 255, 255, 0.10);\n border-radius: 8px;\n padding: 8px 10px;\n font-size: 13px;\n font-weight: 800;\n outline: none;\n appearance: auto;\n ">\n ${buildPlayerSelectOptions(selectedPlayerId, maxPlayerNumber)}\n </select>\n ` : `\n <div style="font-size: 13px; font-weight: 700;">${statusText}</div>\n <div style="font-size: 11px; color: rgba(255,255,255,0.6); margin-top: 2px;">\n ${isSignedIn ? `${userDisplay} ${userIdShort ? `(${userIdShort})` : ""}` : "Click sign in to authenticate"}\n </div>\n `}\n </div>\n </div>\n <div>\n ${isSignedIn ? `\n <button id="sign-out" style="\n background: rgba(239, 68, 68, 0.12);\n color: #fecaca;\n border: 1px solid rgba(239, 68, 68, 0.25);\n border-radius: 8px;\n padding: 8px 10px;\n font-size: 12px;\n font-weight: 700;\n cursor: pointer;\n ">Sign out</button>\n ` : `\n <button id="sign-in-google" style="\n background: rgba(255, 255, 255, 0.12);\n color: #ffffff;\n border: 1px solid rgba(255, 255, 255, 0.2);\n border-radius: 8px;\n padding: 8px 10px;\n font-size: 12px;\n font-weight: 700;\n cursor: pointer;\n ">Sign in</button>\n `}\n </div>\n </div>\n </div>\n\n <div style="\n padding: 12px 14px;\n background: rgba(255, 255, 255, 0.03);\n border: 1px solid rgba(255, 255, 255, 0.06);\n border-radius: 10px;\n margin-bottom: 14px;\n font-size: 12px;\n color: rgba(255, 255, 255, 0.75);\n line-height: 1.5;\n ">\n <div style="font-weight: 600; margin-bottom: 4px; color: #c7d2fe;">\\u{1F4A1} Change environment:</div>\n <code style="\n display: block;\n background: rgba(0, 0, 0, 0.3);\n padding: 8px 10px;\n border-radius: 5px;\n font-family: \'SF Mono\', Monaco, monospace;\n font-size: 11px;\n color: #e0e7ff;\n margin-top: 6px;\n ">venus set-env &lt;local|dev|staging&gt;</code>\n </div>\n\n ${isLocal ? `\n <div style="\n padding: 10px 14px;\n background: rgba(245, 158, 11, 0.08);\n border: 1px solid rgba(245, 158, 11, 0.15);\n border-radius: 8px;\n font-size: 11px;\n color: #fcd34d;\n line-height: 1.5;\n margin-bottom: 14px;\n ">\n <strong>Emulator mode:</strong> Profiles are auto-created on first sign-in.\n <a href="http://localhost:4000" target="_blank" style="color: #fbbf24; text-decoration: underline; margin-left: 4px;">Open Emulator UI \\u2192</a>\n </div>\n ` : ""}\n\n <!-- Local player selection is shown in the auth card (signed-out local mode) -->\n\n <div style="display:flex; gap: 10px;">\n <button id="reload-page" style="\n flex: 1;\n background: rgba(255, 255, 255, 0.06);\n color: rgba(255, 255, 255, 0.85);\n border: 1px solid rgba(255, 255, 255, 0.10);\n border-radius: 10px;\n padding: 10px 12px;\n font-size: 12px;\n font-weight: 700;\n cursor: pointer;\n ">Reload page</button>\n </div>\n </div>\n `;\n overlay.appendChild(content);\n document.body.appendChild(overlay);\n const $ = (selector) => content.querySelector(selector);\n const closeModal = () => overlay.remove();\n const closeBtn = $("#close-modal");\n if (closeBtn) closeBtn.addEventListener("click", closeModal);\n overlay.addEventListener("click", (e) => {\n if (e.target === overlay) closeModal();\n });\n $("#reload-page")?.addEventListener("click", () => {\n window.location.reload();\n });\n const playerSelect = $("#local-player-select");\n if (playerSelect) {\n playerSelect.addEventListener("change", () => {\n const value = String(playerSelect.value || "").trim().toLowerCase();\n if (value === "__new__") {\n const nextId = nextPlayerId(getSelectedPlayerId());\n const match = String(nextId).match(/^player(\\d+)$/);\n const n = match ? parseInt(match[1], 10) : getMaxPlayerNumber() + 1;\n setMaxPlayerNumber(n);\n const saved = setSelectedPlayerId(`player${n}`);\n playerSelect.innerHTML = buildPlayerSelectOptions(saved, getMaxPlayerNumber());\n playerSelect.value = saved;\n } else {\n setSelectedPlayerId(value);\n }\n const toolbar = document.getElementById("venus-sandbox-toolbar");\n const toolbarLabelDiv = toolbar?.children?.[1];\n const isLocalTarget = (toolbar?.dataset?.target || "").toLowerCase() === "local";\n if (toolbarLabelDiv && isLocalTarget && !currentAuthState.signedIn) {\n toolbarLabelDiv.textContent = `\\u25CB ${getSelectedPlayerId()}`;\n }\n });\n }\n const signInBtn = $("#sign-in-google");\n if (signInBtn) {\n const originalGradient = env.gradient;\n signInBtn.addEventListener("click", async () => {\n try {\n signInBtn.textContent = "Signing in...";\n signInBtn.disabled = true;\n const waitForSignedIn = (timeoutMs) => {\n return new Promise((resolve) => {\n let done = false;\n const cleanup = () => {\n if (done) return;\n done = true;\n window.removeEventListener("venus-auth-state-changed", handler);\n clearTimeout(timer);\n };\n const handler = (e) => {\n if (e?.detail?.signedIn === true) {\n cleanup();\n resolve(true);\n }\n };\n const timer = setTimeout(() => {\n cleanup();\n resolve(false);\n }, timeoutMs);\n if (currentAuthState?.signedIn === true) {\n cleanup();\n resolve(true);\n return;\n }\n window.addEventListener("venus-auth-state-changed", handler);\n });\n };\n const signedInWait = waitForSignedIn(8e3);\n let signInError = null;\n if (isLocal && window.__VENUS_SANDBOX_SIGN_IN_AS__) {\n try {\n const p = window.__VENUS_SANDBOX_SIGN_IN_AS__(getSelectedPlayerId());\n p?.catch?.((err) => {\n signInError = err;\n });\n } catch (err) {\n signInError = err;\n }\n } else if (window.__VENUS_SANDBOX_SIGN_IN__) {\n try {\n const p = window.__VENUS_SANDBOX_SIGN_IN__();\n p?.catch?.((err) => {\n signInError = err;\n });\n } catch (err) {\n signInError = err;\n }\n } else {\n throw new Error("[Venus Sandbox] Sign-in is not available (missing global hook).");\n }\n const signedIn = await signedInWait;\n if (!signedIn) {\n if (signInError) throw signInError;\n throw new Error("[Venus Sandbox] Sign-in timed out. Try reloading and signing in again.");\n }\n signInBtn.textContent = "\\u2713 Success!";\n signInBtn.style.background = "linear-gradient(120deg, #10b981, #059669)";\n setTimeout(() => {\n closeModal();\n window.location.reload();\n }, 700);\n } catch (error) {\n console.error("[Venus Sandbox] Sign-in failed:", error);\n signInBtn.textContent = "Sign in";\n signInBtn.disabled = false;\n signInBtn.style.background = originalGradient;\n alert("Sign-in failed. Check console for details.");\n }\n });\n }\n const signOutBtn = $("#sign-out");\n if (signOutBtn) {\n signOutBtn.addEventListener("click", async () => {\n try {\n signOutBtn.textContent = "Signing out...";\n signOutBtn.disabled = true;\n if (window.__VENUS_SANDBOX_SIGN_OUT__) {\n await window.__VENUS_SANDBOX_SIGN_OUT__();\n }\n closeModal();\n window.location.reload();\n } catch (error) {\n console.error("[Venus Sandbox] Sign-out failed:", error);\n signOutBtn.textContent = "Sign out";\n signOutBtn.disabled = false;\n alert("Sign-out failed. Check console for details.");\n }\n });\n }\n}\n';
1831
1081
 
1832
1082
  // src/vite/sandboxToolbar.ts
1833
1083
  function createSandboxToolbarTags() {
@@ -2023,17 +1273,20 @@ Firebase is only needed for sandbox mode (local development) and is excluded fro
2023
1273
  };
2024
1274
  }
2025
1275
  function buildSandboxConfig(projectRoot, options) {
2026
- const gameConfigPath = findGameConfigPath(projectRoot);
2027
- if (!gameConfigPath) {
2028
- throw new Error(
2029
- `Unable to locate ${GAME_CONFIG_FILENAME} starting from ${projectRoot}. Run "venus configure-game" to generate one.`
2030
- );
1276
+ let gameId = options.gameId?.trim();
1277
+ if (!gameId) {
1278
+ const gameConfigPath = findGameConfigPath(projectRoot);
1279
+ if (gameConfigPath) {
1280
+ const gameConfig = readGameConfig(gameConfigPath);
1281
+ gameId = gameConfig.gameId?.trim();
1282
+ }
2031
1283
  }
2032
- const gameConfig = readGameConfig(gameConfigPath);
2033
- const gameId = gameConfig.gameId?.trim();
2034
1284
  if (!gameId) {
2035
1285
  throw new Error(
2036
- `Missing "gameId" inside ${gameConfigPath}. Run "venus configure-game" to regenerate it.`
1286
+ `[Venus Sandbox] Missing gameId. Either:
1287
+ 1. Add gameId to vite.config: venusSandboxPlugin({ gameId: 'your-app-id' })
1288
+ 2. Create game.config.json with { "gameId": "your-app-id" }
1289
+ 3. Run "venus configure-game" if using Venus CLI`
2037
1290
  );
2038
1291
  }
2039
1292
  const cliConfig = readVenusCliConfig();
@@ -2110,10 +1363,10 @@ function resolveProjectRoot(config) {
2110
1363
  return process.cwd();
2111
1364
  }
2112
1365
  function coerceTarget(value) {
2113
- if (value === "dev" || value === "staging") {
1366
+ if (value === "local" || value === "dev" || value === "staging") {
2114
1367
  return value;
2115
1368
  }
2116
- return "local";
1369
+ return "staging";
2117
1370
  }
2118
1371
  function injectVenusShell(html) {
2119
1372
  const match = html.match(/<body([^>]*)>([\s\S]*)<\/body>/i);