@thestatic-tv/dcl-sdk 2.3.0-dev.0 → 2.3.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.js CHANGED
@@ -42,7 +42,9 @@ __export(index_exports, {
42
42
  StaticTVClient: () => StaticTVClient,
43
43
  fetchUserData: () => fetchUserData,
44
44
  getPlayerDisplayName: () => getPlayerDisplayName,
45
- getPlayerWallet: () => getPlayerWallet
45
+ getPlayerWallet: () => getPlayerWallet,
46
+ setupStaticUI: () => setupStaticUI,
47
+ showNotification: () => showNotification
46
48
  });
47
49
  module.exports = __toCommonJS(index_exports);
48
50
 
@@ -173,7 +175,7 @@ function ensureTimerSystem() {
173
175
  try {
174
176
  timer.callback();
175
177
  } catch (e) {
176
- console.error("[StaticTV Timer] Callback error:", e);
178
+ console.error("[TheStatic] Timer error");
177
179
  }
178
180
  }
179
181
  }
@@ -214,7 +216,7 @@ function ensureTimeoutSystem() {
214
216
  try {
215
217
  timeout.callback();
216
218
  } catch (e) {
217
- console.error("[StaticTV Timer] Timeout callback error:", e);
219
+ console.error("[TheStatic] Timeout error");
218
220
  }
219
221
  }
220
222
  }
@@ -245,20 +247,41 @@ function dclClearTimeout(timeoutId) {
245
247
  }
246
248
 
247
249
  // src/modules/session.ts
250
+ function normalizeTier(tier) {
251
+ if (tier === "lite") return "free";
252
+ if (tier === "full") return "standard";
253
+ if (tier === "free" || tier === "standard" || tier === "pro") return tier;
254
+ return "free";
255
+ }
248
256
  var SessionModule = class {
249
257
  constructor(client) {
250
258
  this.sessionId = null;
259
+ this._keyId = null;
251
260
  this.heartbeatTimerId = null;
252
261
  this.isActive = false;
253
- this._sdkType = "lite";
262
+ this._tier = "free";
254
263
  this.client = client;
255
264
  }
256
265
  /**
257
- * Get the SDK type returned by the server
258
- * Determines what features are available (lite = sessions only, full = guide + chat + more)
266
+ * Get the API key ID (used as default sceneId for Pro users)
267
+ */
268
+ get keyId() {
269
+ return this._keyId;
270
+ }
271
+ /**
272
+ * Get the SDK tier returned by the server
273
+ * - free: Session tracking only
274
+ * - standard: Guide, Chat, Heartbeat, Interactions
275
+ * - pro: Everything + Admin Panel
276
+ */
277
+ get tier() {
278
+ return this._tier;
279
+ }
280
+ /**
281
+ * @deprecated Use `tier` instead. Returns mapped value for compatibility.
259
282
  */
260
283
  get sdkType() {
261
- return this._sdkType;
284
+ return this._tier === "free" ? "lite" : "full";
262
285
  }
263
286
  /**
264
287
  * Get the appropriate session endpoint based on key type
@@ -305,13 +328,12 @@ var SessionModule = class {
305
328
  });
306
329
  if (response.success && response.sessionId) {
307
330
  this.sessionId = response.sessionId;
331
+ this._keyId = response.keyId || null;
308
332
  this.isActive = true;
309
- this._sdkType = response.sdkType || "lite";
333
+ this._tier = normalizeTier(response.sdkType);
310
334
  this.startHeartbeat();
311
- this.client.log(`Session started: ${this.sessionId}, sdkType: ${this._sdkType}`);
312
- if (this._sdkType === "full") {
313
- this.client._enableFullFeatures();
314
- }
335
+ this.client.log(`Session started: ${this.sessionId}, tier: ${this._tier}`);
336
+ this.client._enableFeaturesForTier(this._tier, this._keyId);
315
337
  return this.sessionId;
316
338
  }
317
339
  return null;
@@ -599,7 +621,8 @@ var UI_DIMENSIONS = {
599
621
  // Guide UI - positioned to the left of chat (chat is 380px wide at right:20)
600
622
  guide: {
601
623
  width: 900,
602
- height: "55%",
624
+ height: 580,
625
+ // Numeric value for scaling (matches chat/admin)
603
626
  bottom: 55,
604
627
  right: 410,
605
628
  // 20 + 380 (chat width) + 10 (gap)
@@ -618,15 +641,33 @@ var UI_DIMENSIONS = {
618
641
  padding: 15
619
642
  }
620
643
  },
621
- // Chat UI - match m1d-hq-lifted dimensions
644
+ // Chat UI - positioned at right side
622
645
  chat: {
623
646
  width: 380,
624
647
  height: 580,
625
648
  bottom: 55,
626
649
  right: 20,
650
+ headerHeight: 40,
627
651
  messagesPerPage: 5,
628
652
  channelsPerPage: 6
629
653
  },
654
+ // Admin Panel - positioned left of chat
655
+ admin: {
656
+ width: 400,
657
+ height: 580,
658
+ // Match chat height
659
+ maxHeight: 700,
660
+ bottom: 55,
661
+ right: 410,
662
+ // 20 + 380 (chat width) + 10 (gap)
663
+ headerHeight: 48,
664
+ tabHeight: 40,
665
+ footerHeight: 32,
666
+ sectionHeadHeight: 28,
667
+ buttonHeight: 36,
668
+ buttonHeightSmall: 30,
669
+ inputHeight: 36
670
+ },
630
671
  // Shared
631
672
  closeButton: {
632
673
  size: 40,
@@ -634,18 +675,18 @@ var UI_DIMENSIONS = {
634
675
  }
635
676
  };
636
677
  var DEFAULT_CHAT_THEME = {
637
- header: 22,
678
+ header: 16,
638
679
  channelButton: 14,
639
680
  channelDropdown: 14,
640
- systemMessage: 14,
641
- chatUsername: 16,
681
+ systemMessage: 13,
682
+ chatUsername: 14,
642
683
  chatTimestamp: 11,
643
- chatMessage: 16,
644
- input: 16,
684
+ chatMessage: 14,
685
+ input: 14,
645
686
  sendButton: 14,
646
- userInfo: 14,
687
+ userInfo: 13,
647
688
  authStatus: 12,
648
- notification: 18,
689
+ notification: 16,
649
690
  closeButton: 16
650
691
  };
651
692
  function scaleChatTheme(theme, fontScale) {
@@ -665,6 +706,30 @@ function scaleChatTheme(theme, fontScale) {
665
706
  closeButton: Math.round(theme.closeButton * fontScale)
666
707
  };
667
708
  }
709
+ var DEFAULT_ADMIN_THEME = {
710
+ header: 16,
711
+ tabButton: 14,
712
+ sectionHead: 13,
713
+ label: 13,
714
+ labelSmall: 11,
715
+ button: 13,
716
+ buttonSmall: 11,
717
+ input: 13,
718
+ status: 12
719
+ };
720
+ function scaleAdminTheme(theme, fontScale) {
721
+ return {
722
+ header: Math.round(theme.header * fontScale),
723
+ tabButton: Math.round(theme.tabButton * fontScale),
724
+ sectionHead: Math.round(theme.sectionHead * fontScale),
725
+ label: Math.round(theme.label * fontScale),
726
+ labelSmall: Math.round(theme.labelSmall * fontScale),
727
+ button: Math.round(theme.button * fontScale),
728
+ buttonSmall: Math.round(theme.buttonSmall * fontScale),
729
+ input: Math.round(theme.input * fontScale),
730
+ status: Math.round(theme.status * fontScale)
731
+ };
732
+ }
668
733
 
669
734
  // src/ui/components.tsx
670
735
  var import_react_ecs = __toESM(require("@dcl/sdk/react-ecs"));
@@ -720,21 +785,21 @@ var PanelHeader = (props) => {
720
785
  }),
721
786
  import_react_ecs.default.createElement(import_react_ecs.UiEntity, { key: "pos-spacer", uiTransform: { width: 6 } })
722
787
  ] : [],
723
- // Font controls
788
+ // Font controls (with visual disabled state at limits)
724
789
  ...props.showFontControls ? [
725
790
  import_react_ecs.default.createElement(import_react_ecs.UiEntity, {
726
791
  key: "font-down",
727
792
  uiTransform: { width: 22, height: 22, justifyContent: "center", alignItems: "center" },
728
- uiBackground: { color: THEME.colors.buttonBackground },
729
- onMouseDown: () => props.onFontScaleDown?.(),
730
- children: [import_react_ecs.default.createElement(import_react_ecs.UiEntity, { uiText: { value: "\u2212", fontSize: 14, color: THEME.colors.cyan } })]
793
+ uiBackground: { color: props.fontScaleAtMin ? import_math2.Color4.create(0.1, 0.1, 0.1, 0.3) : THEME.colors.buttonBackground },
794
+ onMouseDown: () => !props.fontScaleAtMin && props.onFontScaleDown?.(),
795
+ children: [import_react_ecs.default.createElement(import_react_ecs.UiEntity, { uiText: { value: "\u2212", fontSize: 14, color: props.fontScaleAtMin ? THEME.colors.gray : THEME.colors.cyan } })]
731
796
  }),
732
797
  import_react_ecs.default.createElement(import_react_ecs.UiEntity, {
733
798
  key: "font-up",
734
799
  uiTransform: { width: 22, height: 22, justifyContent: "center", alignItems: "center" },
735
- uiBackground: { color: THEME.colors.buttonBackground },
736
- onMouseDown: () => props.onFontScaleUp?.(),
737
- children: [import_react_ecs.default.createElement(import_react_ecs.UiEntity, { uiText: { value: "+", fontSize: 14, color: THEME.colors.cyan } })]
800
+ uiBackground: { color: props.fontScaleAtMax ? import_math2.Color4.create(0.1, 0.1, 0.1, 0.3) : THEME.colors.buttonBackground },
801
+ onMouseDown: () => !props.fontScaleAtMax && props.onFontScaleUp?.(),
802
+ children: [import_react_ecs.default.createElement(import_react_ecs.UiEntity, { uiText: { value: "+", fontSize: 14, color: props.fontScaleAtMax ? THEME.colors.gray : THEME.colors.cyan } })]
738
803
  }),
739
804
  import_react_ecs.default.createElement(import_react_ecs.UiEntity, { key: "font-spacer", uiTransform: { width: 6 } })
740
805
  ] : [],
@@ -772,7 +837,6 @@ var GuideUIModule = class {
772
837
  this.currentPage = 0;
773
838
  this.itemsPerPage = 6;
774
839
  this.searchQuery = "";
775
- this.uiScale = 1;
776
840
  // Current video tracking (for "PLAYING" indicator)
777
841
  this._currentVideoId = null;
778
842
  // =============================================================================
@@ -780,34 +844,46 @@ var GuideUIModule = class {
780
844
  // =============================================================================
781
845
  /**
782
846
  * Get the guide UI component for rendering
783
- * Returns toggle button when hidden, full panel when visible
847
+ * Always renders toggle button, plus panel when visible
784
848
  */
785
849
  this.getComponent = () => {
786
- if (!this._isVisible) {
787
- return this.renderToggleButton();
788
- }
789
- const windowW = this.s(UI_DIMENSIONS.guide.width);
850
+ const windowW = UI_DIMENSIONS.guide.width;
851
+ const windowH = UI_DIMENSIONS.guide.height;
790
852
  return import_react_ecs2.default.createElement(import_react_ecs2.UiEntity, {
853
+ key: `guide-root-${this.client.uiScale}`,
791
854
  uiTransform: {
792
- width: windowW,
793
- height: UI_DIMENSIONS.guide.height,
794
- positionType: "absolute",
795
- position: { bottom: UI_DIMENSIONS.guide.bottom, right: UI_DIMENSIONS.guide.right },
796
- flexDirection: "row",
797
- border: { top: 2, bottom: 2, left: 2, right: 2 },
798
- borderColor: THEME.colors.panelBorder
855
+ width: "100%",
856
+ height: "100%",
857
+ positionType: "absolute"
799
858
  },
800
- uiBackground: { color: THEME.colors.panel },
801
859
  children: [
802
- this.renderLeftPanel(),
803
- this.renderRightPanel(),
804
- this.renderCloseButton()
860
+ // Always render toggle button
861
+ this.renderToggleButton(),
862
+ // Render panel when visible - Guide positioned relative to Chat's scaled width
863
+ // Guide.right = Chat.right + Chat.scaledWidth + gap (10px)
864
+ this._isVisible ? import_react_ecs2.default.createElement(import_react_ecs2.UiEntity, {
865
+ key: `guide-panel-${this.client.uiScale}`,
866
+ uiTransform: {
867
+ width: windowW,
868
+ height: windowH,
869
+ positionType: "absolute",
870
+ position: { bottom: UI_DIMENSIONS.guide.bottom, right: UI_DIMENSIONS.chat.right + this.s(UI_DIMENSIONS.chat.width) + 10 },
871
+ flexDirection: "row",
872
+ border: { top: 2, bottom: 2, left: 2, right: 2 },
873
+ borderColor: THEME.colors.panelBorder
874
+ },
875
+ uiBackground: { color: THEME.colors.panel },
876
+ children: [
877
+ this.renderLeftPanel(),
878
+ this.renderRightPanel(),
879
+ this.renderCloseButton()
880
+ ]
881
+ }) : null
805
882
  ]
806
883
  });
807
884
  };
808
885
  this.client = client;
809
886
  this.config = config;
810
- this.uiScale = config.uiScale || 1;
811
887
  this._currentVideoId = config.currentVideoId || null;
812
888
  }
813
889
  /**
@@ -821,6 +897,8 @@ var GuideUIModule = class {
821
897
  * Show the guide UI
822
898
  */
823
899
  show() {
900
+ if (this._isVisible) return;
901
+ this.client.closeOtherPanels("guide");
824
902
  this._isVisible = true;
825
903
  this.fetchGuideData().catch(() => {
826
904
  });
@@ -956,7 +1034,7 @@ var GuideUIModule = class {
956
1034
  // --- UTILITIES ---
957
1035
  // =============================================================================
958
1036
  s(value) {
959
- return Math.round(value * this.uiScale);
1037
+ return Math.round(value * this.client.uiScale);
960
1038
  }
961
1039
  handleVideoSelect(video) {
962
1040
  if (this.config.onVideoSelect) {
@@ -971,7 +1049,7 @@ var GuideUIModule = class {
971
1049
  renderLeftPanel() {
972
1050
  const sidebarW = this.s(UI_DIMENSIONS.guide.sidebar.width);
973
1051
  const liveCount = this.liveVideos.length;
974
- const signalCount = this.videos.filter((v) => !v.isLive).length;
1052
+ const signalCount = this.videos.filter((v) => !v.isLive && !v.isCreator).length;
975
1053
  const nodesPlaylist = this.featuredPlaylists.find((p) => p.name === "NODES");
976
1054
  const nodesCount = nodesPlaylist ? nodesPlaylist.videos.filter((v) => v.isCreator).length : 0;
977
1055
  const displayedPlaylists = this.featuredPlaylists.filter((p) => p.name !== "NODES").slice(0, 6);
@@ -992,15 +1070,26 @@ var GuideUIModule = class {
992
1070
  import_react_ecs2.default.createElement(import_react_ecs2.UiEntity, {
993
1071
  uiTransform: { width: "100%", flexDirection: "column", padding: 10 },
994
1072
  children: [
995
- // Title
1073
+ // Title row
996
1074
  import_react_ecs2.default.createElement(import_react_ecs2.UiEntity, {
997
- uiText: {
998
- value: "THE STATIC TV",
999
- fontSize: this.s(UI_DIMENSIONS.guide.sidebar.headerSize),
1000
- color: THEME.colors.cyan,
1001
- textAlign: "middle-center"
1075
+ uiTransform: {
1076
+ width: "100%",
1077
+ height: this.s(40),
1078
+ marginBottom: 10,
1079
+ flexDirection: "row",
1080
+ alignItems: "center"
1002
1081
  },
1003
- uiTransform: { height: this.s(40), marginBottom: 10 }
1082
+ children: [
1083
+ // Title
1084
+ import_react_ecs2.default.createElement(import_react_ecs2.UiEntity, {
1085
+ uiText: {
1086
+ value: "THE STATIC TV",
1087
+ fontSize: this.s(UI_DIMENSIONS.guide.sidebar.headerSize),
1088
+ color: THEME.colors.cyan,
1089
+ textAlign: "middle-left"
1090
+ }
1091
+ })
1092
+ ]
1004
1093
  }),
1005
1094
  // Random Signal button
1006
1095
  import_react_ecs2.default.createElement(import_react_ecs2.UiEntity, {
@@ -1057,7 +1146,7 @@ var GuideUIModule = class {
1057
1146
  }),
1058
1147
  // Menu buttons
1059
1148
  this.renderMenuButton("LIVE NOW", liveCount, THEME.colors.red, liveCount > 0 ? "\u25CF" : void 0, "ACTIVE STREAMS"),
1060
- this.renderMenuButton("SIGNALS", signalCount, THEME.colors.white, void 0, "ALL VIDEOS"),
1149
+ this.renderMenuButton("SIGNALS", signalCount, THEME.colors.white, void 0, "VODS & RECORDINGS"),
1061
1150
  this.renderMenuButton("NODES", nodesCount, THEME.colors.magenta, void 0, "CHANNELS & CREATORS"),
1062
1151
  // Divider
1063
1152
  import_react_ecs2.default.createElement(import_react_ecs2.UiEntity, {
@@ -1139,7 +1228,7 @@ var GuideUIModule = class {
1139
1228
  let videosToShow = [];
1140
1229
  const nodePl = this.featuredPlaylists.find((p) => p.name === "NODES");
1141
1230
  if (this.activeTab === "SIGNALS") {
1142
- videosToShow = this.videos.filter((v) => !v.isLive);
1231
+ videosToShow = this.videos.filter((v) => !v.isLive && !v.isCreator);
1143
1232
  } else if (this.activeTab === "NODES") {
1144
1233
  videosToShow = nodePl ? nodePl.videos.filter((v) => v.isCreator) : [];
1145
1234
  } else if (this.activeTab === "LIVE NOW") {
@@ -1350,23 +1439,25 @@ var GuideUIModule = class {
1350
1439
  });
1351
1440
  }
1352
1441
  renderToggleButton() {
1442
+ const buttonText = this._isVisible ? "CLOSE" : "GUIDE";
1443
+ const buttonColor = this._isVisible ? import_math3.Color4.create(0.2, 0.2, 0.28, 0.9) : import_math3.Color4.create(0, 0.5, 0.5, 0.9);
1444
+ const buttonPos = 20 + this.s(100) + 10;
1353
1445
  return import_react_ecs2.default.createElement(import_react_ecs2.UiEntity, {
1354
1446
  uiTransform: {
1355
1447
  positionType: "absolute",
1356
- position: { right: 130, bottom: 10 },
1357
- // To the left of CHAT button
1448
+ position: { right: buttonPos, bottom: 10 },
1358
1449
  width: this.s(100),
1359
1450
  height: this.s(45),
1360
1451
  justifyContent: "center",
1361
1452
  alignItems: "center"
1362
1453
  },
1363
- uiBackground: { color: import_math3.Color4.create(0, 0.5, 0.5, 0.9) },
1364
- onMouseDown: () => this.show(),
1454
+ uiBackground: { color: buttonColor },
1455
+ onMouseDown: () => this._isVisible ? this.hide() : this.show(),
1365
1456
  children: [
1366
1457
  import_react_ecs2.default.createElement(import_react_ecs2.UiEntity, {
1367
1458
  uiText: {
1368
- value: "GUIDE",
1369
- fontSize: this.s(16),
1459
+ value: buttonText,
1460
+ fontSize: this.s(14),
1370
1461
  color: THEME.colors.white,
1371
1462
  textAlign: "middle-center"
1372
1463
  }
@@ -1408,7 +1499,6 @@ var ChatUIModule = class {
1408
1499
  this.chatScrollOffset = 0;
1409
1500
  // UI preferences
1410
1501
  this.position = "right";
1411
- this.fontScale = 1;
1412
1502
  // Timers
1413
1503
  this.chatTimerId = null;
1414
1504
  this.playerInfoTimerId = null;
@@ -1418,43 +1508,52 @@ var ChatUIModule = class {
1418
1508
  // =============================================================================
1419
1509
  /**
1420
1510
  * Get the chat UI component for rendering
1421
- * Returns toggle button when hidden, full panel when visible
1511
+ * Always renders toggle button, plus panel when visible
1422
1512
  */
1423
1513
  this.getComponent = () => {
1424
- if (!this._isVisible) {
1425
- return this.renderToggleButton();
1426
- }
1427
- const scaledTheme = scaleChatTheme(DEFAULT_CHAT_THEME, this.fontScale);
1514
+ const scaledTheme = scaleChatTheme(DEFAULT_CHAT_THEME, this.client.uiScale);
1428
1515
  const positionStyle = this.getPositionStyle();
1429
1516
  return import_react_ecs3.default.createElement(import_react_ecs3.UiEntity, {
1430
1517
  uiTransform: {
1431
- width: UI_DIMENSIONS.chat.width,
1432
- height: UI_DIMENSIONS.chat.height,
1433
- positionType: "absolute",
1434
- position: positionStyle,
1435
- // Must be nested object, not spread!
1436
- flexDirection: "column",
1437
- border: { top: 2, bottom: 2, left: 2, right: 2 },
1438
- borderColor: THEME.colors.panelBorder
1518
+ width: "100%",
1519
+ height: "100%",
1520
+ positionType: "absolute"
1439
1521
  },
1440
- uiBackground: { color: THEME.colors.panel },
1441
1522
  children: [
1442
- this.renderHeader(),
1443
- this.renderChannelButton(scaledTheme),
1444
- this.renderMessagesArea(scaledTheme),
1445
- import_react_ecs3.default.createElement(import_react_ecs3.UiEntity, {
1446
- uiTransform: { width: "100%", height: 1, flexShrink: 0 },
1447
- uiBackground: { color: THEME.colors.panelBorder }
1448
- }),
1449
- this.renderUserInfoBar(scaledTheme),
1450
- this.renderInputArea(scaledTheme),
1451
- this.renderChannelDropdown(scaledTheme)
1523
+ // Always render toggle button
1524
+ this.renderToggleButton(),
1525
+ // Render panel when visible - scaled sizes, fixed position
1526
+ // Key includes uiScale to force re-render when scale changes from other panels
1527
+ this._isVisible ? import_react_ecs3.default.createElement(import_react_ecs3.UiEntity, {
1528
+ key: `chat-panel-${this.client.uiScale}`,
1529
+ uiTransform: {
1530
+ width: this.s(UI_DIMENSIONS.chat.width),
1531
+ height: this.s(UI_DIMENSIONS.chat.height),
1532
+ positionType: "absolute",
1533
+ position: { bottom: UI_DIMENSIONS.chat.bottom, right: UI_DIMENSIONS.chat.right },
1534
+ flexDirection: "column",
1535
+ border: { top: 2, bottom: 2, left: 2, right: 2 },
1536
+ borderColor: THEME.colors.panelBorder
1537
+ },
1538
+ uiBackground: { color: THEME.colors.panel },
1539
+ children: [
1540
+ this.renderHeader(),
1541
+ this.renderChannelButton(scaledTheme),
1542
+ this.renderMessagesArea(scaledTheme),
1543
+ import_react_ecs3.default.createElement(import_react_ecs3.UiEntity, {
1544
+ uiTransform: { width: "100%", height: 1, flexShrink: 0 },
1545
+ uiBackground: { color: THEME.colors.panelBorder }
1546
+ }),
1547
+ this.renderUserInfoBar(scaledTheme),
1548
+ this.renderInputArea(scaledTheme),
1549
+ this.renderChannelDropdown(scaledTheme)
1550
+ ]
1551
+ }) : null
1452
1552
  ]
1453
1553
  });
1454
1554
  };
1455
1555
  this.client = client;
1456
1556
  this.config = config;
1457
- this.fontScale = config.fontScale || 1;
1458
1557
  }
1459
1558
  /**
1460
1559
  * Initialize the chat system
@@ -1473,6 +1572,7 @@ var ChatUIModule = class {
1473
1572
  * Show the chat UI
1474
1573
  */
1475
1574
  show() {
1575
+ if (this._isVisible) return;
1476
1576
  this._isVisible = true;
1477
1577
  this._unreadCount = 0;
1478
1578
  this.chatScrollOffset = 0;
@@ -1734,6 +1834,10 @@ var ChatUIModule = class {
1734
1834
  const d = new Date(input);
1735
1835
  if (!isNaN(d.getTime())) return d.getTime();
1736
1836
  }
1837
+ if (input instanceof Date) return input.getTime();
1838
+ if (typeof input === "object" && "seconds" in input) {
1839
+ return input.seconds * 1e3;
1840
+ }
1737
1841
  return 0;
1738
1842
  }
1739
1843
  formatTime(isoString) {
@@ -1746,6 +1850,10 @@ var ChatUIModule = class {
1746
1850
  return "";
1747
1851
  }
1748
1852
  }
1853
+ /** Scale a dimension by shared uiScale */
1854
+ s(value) {
1855
+ return Math.round(value * this.client.uiScale);
1856
+ }
1749
1857
  getPositionStyle() {
1750
1858
  return { bottom: 55, right: 20 };
1751
1859
  }
@@ -1753,15 +1861,9 @@ var ChatUIModule = class {
1753
1861
  return PanelHeader({
1754
1862
  title: "LIVE CHAT",
1755
1863
  fontSize: 14,
1756
- fontScale: this.fontScale,
1864
+ fontScale: 1,
1757
1865
  showPositionControls: false,
1758
- showFontControls: true,
1759
- onFontScaleUp: () => {
1760
- this.fontScale = Math.min(1.4, this.fontScale + 0.1);
1761
- },
1762
- onFontScaleDown: () => {
1763
- this.fontScale = Math.max(0.7, this.fontScale - 0.1);
1764
- },
1866
+ showFontControls: false,
1765
1867
  onClose: () => this.hide()
1766
1868
  });
1767
1869
  }
@@ -2136,23 +2238,24 @@ var ChatUIModule = class {
2136
2238
  }
2137
2239
  renderToggleButton() {
2138
2240
  const unreadBadge = this._unreadCount > 0 ? ` (${this._unreadCount})` : "";
2241
+ const buttonText = this._isVisible ? "CLOSE" : `CHAT${unreadBadge}`;
2242
+ const buttonColor = this._isVisible ? import_math4.Color4.create(0.2, 0.2, 0.28, 0.9) : import_math4.Color4.create(0.6, 0, 0.5, 0.9);
2139
2243
  return import_react_ecs3.default.createElement(import_react_ecs3.UiEntity, {
2140
2244
  uiTransform: {
2141
2245
  positionType: "absolute",
2142
2246
  position: { right: 20, bottom: 10 },
2143
- // Far right corner
2144
- width: 100,
2145
- height: 45,
2247
+ width: this.s(100),
2248
+ height: this.s(45),
2146
2249
  justifyContent: "center",
2147
2250
  alignItems: "center"
2148
2251
  },
2149
- uiBackground: { color: import_math4.Color4.create(0.6, 0, 0.5, 0.9) },
2150
- onMouseDown: () => this.show(),
2252
+ uiBackground: { color: buttonColor },
2253
+ onMouseDown: () => this._isVisible ? this.hide() : this.show(),
2151
2254
  children: [
2152
2255
  import_react_ecs3.default.createElement(import_react_ecs3.UiEntity, {
2153
2256
  uiText: {
2154
- value: `CHAT${unreadBadge}`,
2155
- fontSize: 16,
2257
+ value: buttonText,
2258
+ fontSize: this.s(14),
2156
2259
  color: THEME.colors.white,
2157
2260
  textAlign: "middle-center"
2158
2261
  }
@@ -2186,6 +2289,7 @@ var C = {
2186
2289
  textDim: import_math5.Color4.create(0.6, 0.6, 0.7, 1)
2187
2290
  };
2188
2291
  var AdminPanelUIModule = class {
2292
+ // UI scaling - uses shared client.uiScale
2189
2293
  constructor(client, config) {
2190
2294
  // State
2191
2295
  this.isAdmin = false;
@@ -2197,6 +2301,8 @@ var AdminPanelUIModule = class {
2197
2301
  this.customVideoUrl = "";
2198
2302
  this.streamData = null;
2199
2303
  this.streamFetched = false;
2304
+ this.videoState = null;
2305
+ this.videoStateFetched = false;
2200
2306
  this.channelCreating = false;
2201
2307
  this.channelCreateError = "";
2202
2308
  this.channelDeleting = false;
@@ -2220,18 +2326,18 @@ var AdminPanelUIModule = class {
2220
2326
  this.SectionHead = ({ label, color }) => /* @__PURE__ */ import_react_ecs4.default.createElement(
2221
2327
  import_react_ecs4.UiEntity,
2222
2328
  {
2223
- uiTransform: { width: "100%", height: 24, margin: { bottom: 6 }, padding: { left: 8 }, alignItems: "center" },
2329
+ uiTransform: { width: "100%", height: this.s(UI_DIMENSIONS.admin.sectionHeadHeight), margin: { bottom: 8 }, padding: { left: 10 }, alignItems: "center" },
2224
2330
  uiBackground: { color: import_math5.Color4.create(color.r * 0.3, color.g * 0.3, color.b * 0.3, 0.5) }
2225
2331
  },
2226
- /* @__PURE__ */ import_react_ecs4.default.createElement(import_react_ecs4.Label, { value: label, fontSize: 12, color })
2332
+ /* @__PURE__ */ import_react_ecs4.default.createElement(import_react_ecs4.Label, { value: label, fontSize: this.theme.sectionHead, color })
2227
2333
  );
2228
2334
  this.TabBtn = ({ label, tab }) => /* @__PURE__ */ import_react_ecs4.default.createElement(
2229
2335
  import_react_ecs4.Button,
2230
2336
  {
2231
- uiTransform: { flexGrow: 1, height: 36, justifyContent: "center", alignItems: "center" },
2337
+ uiTransform: { flexGrow: 1, height: this.s(UI_DIMENSIONS.admin.tabHeight), justifyContent: "center", alignItems: "center" },
2232
2338
  uiBackground: { color: this.activeTab === tab ? C.tabActive : C.tabInactive },
2233
2339
  value: label,
2234
- fontSize: 12,
2340
+ fontSize: this.theme.tabButton,
2235
2341
  color: this.activeTab === tab ? import_math5.Color4.Black() : C.text,
2236
2342
  textAlign: "middle-center",
2237
2343
  onMouseDown: () => this.setActiveTab(tab)
@@ -2241,134 +2347,135 @@ var AdminPanelUIModule = class {
2241
2347
  if (!this.streamFetched) {
2242
2348
  this.fetchStreamData();
2243
2349
  }
2244
- return /* @__PURE__ */ import_react_ecs4.default.createElement(import_react_ecs4.UiEntity, { uiTransform: { flexDirection: "column", width: "100%", padding: 8 } }, !this.streamData?.hasChannel && /* @__PURE__ */ import_react_ecs4.default.createElement(import_react_ecs4.UiEntity, { uiTransform: { flexDirection: "column", margin: { bottom: 12 } } }, /* @__PURE__ */ import_react_ecs4.default.createElement(this.SectionHead, { label: "LIVE STREAM", color: C.btn }), /* @__PURE__ */ import_react_ecs4.default.createElement(import_react_ecs4.Label, { value: "No streaming channel linked", fontSize: 10, color: C.textDim, uiTransform: { margin: { bottom: 6 } } }), this.isOwner && /* @__PURE__ */ import_react_ecs4.default.createElement(import_react_ecs4.UiEntity, { uiTransform: { flexDirection: "column" } }, this.streamData?.trialAvailable && /* @__PURE__ */ import_react_ecs4.default.createElement(import_react_ecs4.UiEntity, { uiTransform: { flexDirection: "column", margin: { bottom: 8 } } }, /* @__PURE__ */ import_react_ecs4.default.createElement(
2350
+ const t = this.theme;
2351
+ return /* @__PURE__ */ import_react_ecs4.default.createElement(import_react_ecs4.UiEntity, { uiTransform: { flexDirection: "column", width: "100%", padding: 10 } }, !this.streamData?.hasChannel && /* @__PURE__ */ import_react_ecs4.default.createElement(import_react_ecs4.UiEntity, { uiTransform: { flexDirection: "column", margin: { bottom: 14 } } }, /* @__PURE__ */ import_react_ecs4.default.createElement(this.SectionHead, { label: "LIVE STREAM", color: C.btn }), /* @__PURE__ */ import_react_ecs4.default.createElement(import_react_ecs4.Label, { value: "No streaming channel linked", fontSize: t.labelSmall, color: C.textDim, uiTransform: { margin: { bottom: 8 } } }), this.isOwner && /* @__PURE__ */ import_react_ecs4.default.createElement(import_react_ecs4.UiEntity, { uiTransform: { flexDirection: "column" } }, this.streamData?.trialAvailable && /* @__PURE__ */ import_react_ecs4.default.createElement(import_react_ecs4.UiEntity, { uiTransform: { flexDirection: "column", margin: { bottom: 10 } } }, /* @__PURE__ */ import_react_ecs4.default.createElement(
2245
2352
  import_react_ecs4.Button,
2246
2353
  {
2247
- uiTransform: { width: 180, height: 40, margin: { bottom: 4 } },
2354
+ uiTransform: { width: this.s(200), height: this.s(UI_DIMENSIONS.admin.buttonHeight), margin: { bottom: 6 } },
2248
2355
  uiBackground: { color: this.trialClaiming ? C.btn : C.green },
2249
- value: this.trialClaiming ? "Claiming..." : "\u{1F381} Start Free 4-Hour Trial",
2250
- fontSize: 11,
2356
+ value: this.trialClaiming ? "Claiming..." : "Start Free 4-Hour Trial",
2357
+ fontSize: t.button,
2251
2358
  color: C.text,
2252
2359
  onMouseDown: () => this.claimTrial()
2253
2360
  }
2254
- ), this.trialClaimError && /* @__PURE__ */ import_react_ecs4.default.createElement(import_react_ecs4.Label, { value: this.trialClaimError, fontSize: 9, color: C.red }), /* @__PURE__ */ import_react_ecs4.default.createElement(import_react_ecs4.Label, { value: "One-time trial \u2022 4 hours of streaming", fontSize: 8, color: C.textDim, uiTransform: { margin: { top: 2 } } })), !this.streamData?.trialAvailable && /* @__PURE__ */ import_react_ecs4.default.createElement(import_react_ecs4.UiEntity, { uiTransform: { flexDirection: "column" } }, /* @__PURE__ */ import_react_ecs4.default.createElement(
2361
+ ), this.trialClaimError && /* @__PURE__ */ import_react_ecs4.default.createElement(import_react_ecs4.Label, { value: this.trialClaimError, fontSize: t.status, color: C.red }), /* @__PURE__ */ import_react_ecs4.default.createElement(import_react_ecs4.Label, { value: "One-time trial \u2022 4 hours of streaming", fontSize: t.labelSmall, color: C.textDim, uiTransform: { margin: { top: 4 } } })), !this.streamData?.trialAvailable && /* @__PURE__ */ import_react_ecs4.default.createElement(import_react_ecs4.UiEntity, { uiTransform: { flexDirection: "column" } }, /* @__PURE__ */ import_react_ecs4.default.createElement(
2255
2362
  import_react_ecs4.Button,
2256
2363
  {
2257
- uiTransform: { width: 150, height: 36, margin: { bottom: 4 } },
2364
+ uiTransform: { width: this.s(170), height: this.s(UI_DIMENSIONS.admin.buttonHeight), margin: { bottom: 6 } },
2258
2365
  uiBackground: { color: this.channelCreating ? C.btn : C.cyan },
2259
2366
  value: this.channelCreating ? "Creating..." : "+ Create Channel",
2260
- fontSize: 11,
2367
+ fontSize: t.button,
2261
2368
  color: C.text,
2262
2369
  onMouseDown: () => this.createChannel()
2263
2370
  }
2264
- ), this.channelCreateError && /* @__PURE__ */ import_react_ecs4.default.createElement(import_react_ecs4.Label, { value: this.channelCreateError, fontSize: 9, color: C.red }), /* @__PURE__ */ import_react_ecs4.default.createElement(import_react_ecs4.Label, { value: "Relay tier \u2022 $25/mo \u2022 8 hours streaming", fontSize: 8, color: C.textDim, uiTransform: { margin: { top: 2 } } })))), this.streamData?.hasChannel && this.isOwner && /* @__PURE__ */ import_react_ecs4.default.createElement(import_react_ecs4.UiEntity, { uiTransform: { flexDirection: "column", margin: { bottom: 12 } } }, /* @__PURE__ */ import_react_ecs4.default.createElement(this.SectionHead, { label: this.streamData.isLive ? "\u{1F534} LIVE STREAM" : "STREAM SETTINGS", color: this.streamData.isLive ? C.red : C.yellow }), /* @__PURE__ */ import_react_ecs4.default.createElement(import_react_ecs4.UiEntity, { uiTransform: { flexDirection: "row", alignItems: "center", margin: { bottom: 6 } } }, /* @__PURE__ */ import_react_ecs4.default.createElement(
2371
+ ), this.channelCreateError && /* @__PURE__ */ import_react_ecs4.default.createElement(import_react_ecs4.Label, { value: this.channelCreateError, fontSize: t.status, color: C.red }), /* @__PURE__ */ import_react_ecs4.default.createElement(import_react_ecs4.Label, { value: "Relay tier \u2022 $25/mo \u2022 8 hours streaming", fontSize: t.labelSmall, color: C.textDim, uiTransform: { margin: { top: 4 } } })))), this.streamData?.hasChannel && this.isOwner && /* @__PURE__ */ import_react_ecs4.default.createElement(import_react_ecs4.UiEntity, { uiTransform: { flexDirection: "column", margin: { bottom: 14 } } }, /* @__PURE__ */ import_react_ecs4.default.createElement(this.SectionHead, { label: this.streamData.isLive ? "LIVE STREAM" : "STREAM SETTINGS", color: this.streamData.isLive ? C.red : C.yellow }), /* @__PURE__ */ import_react_ecs4.default.createElement(import_react_ecs4.UiEntity, { uiTransform: { flexDirection: "row", alignItems: "center", margin: { bottom: 8 } } }, /* @__PURE__ */ import_react_ecs4.default.createElement(
2265
2372
  import_react_ecs4.Label,
2266
2373
  {
2267
2374
  value: this.streamData.isLive ? `LIVE \u2022 ${this.streamData.currentViewers || 0} viewers` : "OFFLINE",
2268
- fontSize: 11,
2375
+ fontSize: t.label,
2269
2376
  color: this.streamData.isLive ? C.red : C.textDim
2270
2377
  }
2271
- ), /* @__PURE__ */ import_react_ecs4.default.createElement(import_react_ecs4.Label, { value: ` \u2022 ${this.streamData.tier?.toUpperCase() || "RELAY"}`, fontSize: 10, color: C.cyan }), /* @__PURE__ */ import_react_ecs4.default.createElement(import_react_ecs4.Label, { value: ` \u2022 ${this.streamData.sparksBalance || 0} Sparks`, fontSize: 10, color: C.textDim })), /* @__PURE__ */ import_react_ecs4.default.createElement(
2378
+ ), /* @__PURE__ */ import_react_ecs4.default.createElement(import_react_ecs4.Label, { value: ` \u2022 ${this.streamData.tier?.toUpperCase() || "RELAY"}`, fontSize: t.labelSmall, color: C.cyan }), /* @__PURE__ */ import_react_ecs4.default.createElement(import_react_ecs4.Label, { value: ` \u2022 ${this.streamData.sparksBalance || 0} Sparks`, fontSize: t.labelSmall, color: C.textDim })), /* @__PURE__ */ import_react_ecs4.default.createElement(
2272
2379
  import_react_ecs4.Label,
2273
2380
  {
2274
2381
  value: `Channel: ${this.streamData.channelName || this.streamData.channelId}`,
2275
- fontSize: 9,
2382
+ fontSize: t.labelSmall,
2276
2383
  color: C.textDim,
2277
- uiTransform: { margin: { bottom: 8 } }
2384
+ uiTransform: { margin: { bottom: 10 } }
2278
2385
  }
2279
- ), /* @__PURE__ */ import_react_ecs4.default.createElement(import_react_ecs4.UiEntity, { uiTransform: { flexDirection: "column", margin: { bottom: 4 } } }, /* @__PURE__ */ import_react_ecs4.default.createElement(import_react_ecs4.Label, { value: "RTMP Server:", fontSize: 9, color: C.textDim }), /* @__PURE__ */ import_react_ecs4.default.createElement(
2386
+ ), /* @__PURE__ */ import_react_ecs4.default.createElement(import_react_ecs4.UiEntity, { uiTransform: { flexDirection: "column", margin: { bottom: 6 } } }, /* @__PURE__ */ import_react_ecs4.default.createElement(import_react_ecs4.Label, { value: "RTMP Server:", fontSize: t.labelSmall, color: C.textDim }), /* @__PURE__ */ import_react_ecs4.default.createElement(
2280
2387
  import_react_ecs4.Label,
2281
2388
  {
2282
2389
  value: this.streamData.rtmpUrl || "Loading...",
2283
- fontSize: 10,
2390
+ fontSize: t.label,
2284
2391
  color: this.streamData.rtmpUrl ? C.text : C.textDim,
2285
- uiTransform: { margin: { left: 4 } }
2392
+ uiTransform: { margin: { left: 6 } }
2286
2393
  }
2287
- )), /* @__PURE__ */ import_react_ecs4.default.createElement(import_react_ecs4.UiEntity, { uiTransform: { flexDirection: "column", margin: { bottom: 4 } } }, /* @__PURE__ */ import_react_ecs4.default.createElement(import_react_ecs4.Label, { value: "Stream Key:", fontSize: 9, color: C.textDim }), /* @__PURE__ */ import_react_ecs4.default.createElement(
2394
+ )), /* @__PURE__ */ import_react_ecs4.default.createElement(import_react_ecs4.UiEntity, { uiTransform: { flexDirection: "column", margin: { bottom: 6 } } }, /* @__PURE__ */ import_react_ecs4.default.createElement(import_react_ecs4.Label, { value: "Stream Key:", fontSize: t.labelSmall, color: C.textDim }), /* @__PURE__ */ import_react_ecs4.default.createElement(
2288
2395
  import_react_ecs4.Label,
2289
2396
  {
2290
2397
  value: this.streamData.streamKey || "Loading...",
2291
- fontSize: 10,
2398
+ fontSize: t.label,
2292
2399
  color: this.streamData.streamKey ? C.cyan : C.textDim,
2293
- uiTransform: { margin: { left: 4 } }
2400
+ uiTransform: { margin: { left: 6 } }
2294
2401
  }
2295
- )), /* @__PURE__ */ import_react_ecs4.default.createElement(import_react_ecs4.UiEntity, { uiTransform: { flexDirection: "column", margin: { bottom: 8 } } }, /* @__PURE__ */ import_react_ecs4.default.createElement(import_react_ecs4.Label, { value: "HLS Playback:", fontSize: 9, color: C.textDim }), /* @__PURE__ */ import_react_ecs4.default.createElement(
2402
+ )), /* @__PURE__ */ import_react_ecs4.default.createElement(import_react_ecs4.UiEntity, { uiTransform: { flexDirection: "column", margin: { bottom: 10 } } }, /* @__PURE__ */ import_react_ecs4.default.createElement(import_react_ecs4.Label, { value: "HLS Playback:", fontSize: t.labelSmall, color: C.textDim }), /* @__PURE__ */ import_react_ecs4.default.createElement(
2296
2403
  import_react_ecs4.Label,
2297
2404
  {
2298
2405
  value: this.streamData.hlsUrl || "Not available",
2299
- fontSize: 10,
2406
+ fontSize: t.label,
2300
2407
  color: this.streamData.hlsUrl ? C.green : C.textDim,
2301
- uiTransform: { margin: { left: 4 } }
2408
+ uiTransform: { margin: { left: 6 } }
2302
2409
  }
2303
- )), /* @__PURE__ */ import_react_ecs4.default.createElement(import_react_ecs4.UiEntity, { uiTransform: { flexDirection: "row", flexWrap: "wrap", margin: { bottom: 4 } } }, !this.streamData.isLive && /* @__PURE__ */ import_react_ecs4.default.createElement(
2410
+ )), /* @__PURE__ */ import_react_ecs4.default.createElement(import_react_ecs4.UiEntity, { uiTransform: { flexDirection: "row", flexWrap: "wrap", margin: { bottom: 6 } } }, !this.streamData.isLive && /* @__PURE__ */ import_react_ecs4.default.createElement(
2304
2411
  import_react_ecs4.Button,
2305
2412
  {
2306
- uiTransform: { width: 100, height: 32, margin: 3 },
2413
+ uiTransform: { width: this.s(110), height: this.s(UI_DIMENSIONS.admin.buttonHeightSmall), margin: 4 },
2307
2414
  uiBackground: { color: this.streamControlling ? C.btn : C.green },
2308
2415
  value: this.streamControlling ? "Starting..." : "Start Stream",
2309
- fontSize: 10,
2416
+ fontSize: t.buttonSmall,
2310
2417
  color: C.text,
2311
2418
  onMouseDown: () => this.startStream()
2312
2419
  }
2313
2420
  ), this.streamData.isLive && /* @__PURE__ */ import_react_ecs4.default.createElement(
2314
2421
  import_react_ecs4.Button,
2315
2422
  {
2316
- uiTransform: { width: 100, height: 32, margin: 3 },
2423
+ uiTransform: { width: this.s(110), height: this.s(UI_DIMENSIONS.admin.buttonHeightSmall), margin: 4 },
2317
2424
  uiBackground: { color: this.streamControlling ? C.btn : C.red },
2318
2425
  value: this.streamControlling ? "Stopping..." : "Stop Stream",
2319
- fontSize: 10,
2426
+ fontSize: t.buttonSmall,
2320
2427
  color: C.text,
2321
2428
  onMouseDown: () => this.stopStream()
2322
2429
  }
2323
2430
  ), this.streamData.isLive && this.streamData.hlsUrl && /* @__PURE__ */ import_react_ecs4.default.createElement(
2324
2431
  import_react_ecs4.Button,
2325
2432
  {
2326
- uiTransform: { width: 90, height: 32, margin: 3 },
2433
+ uiTransform: { width: this.s(100), height: this.s(UI_DIMENSIONS.admin.buttonHeightSmall), margin: 4 },
2327
2434
  uiBackground: { color: C.cyan },
2328
2435
  value: "Play on Screen",
2329
- fontSize: 9,
2436
+ fontSize: t.buttonSmall,
2330
2437
  color: C.text,
2331
2438
  onMouseDown: () => this.config.onVideoPlay?.(this.streamData.hlsUrl)
2332
2439
  }
2333
- )), /* @__PURE__ */ import_react_ecs4.default.createElement(import_react_ecs4.UiEntity, { uiTransform: { flexDirection: "row", flexWrap: "wrap", margin: { bottom: 4 } } }, /* @__PURE__ */ import_react_ecs4.default.createElement(
2440
+ )), /* @__PURE__ */ import_react_ecs4.default.createElement(import_react_ecs4.UiEntity, { uiTransform: { flexDirection: "row", flexWrap: "wrap", margin: { bottom: 6 } } }, /* @__PURE__ */ import_react_ecs4.default.createElement(
2334
2441
  import_react_ecs4.Button,
2335
2442
  {
2336
- uiTransform: { width: 70, height: 28, margin: 3 },
2443
+ uiTransform: { width: this.s(80), height: this.s(UI_DIMENSIONS.admin.buttonHeightSmall), margin: 4 },
2337
2444
  uiBackground: { color: C.btn },
2338
2445
  value: "Refresh",
2339
- fontSize: 9,
2446
+ fontSize: t.buttonSmall,
2340
2447
  color: C.text,
2341
2448
  onMouseDown: () => this.refreshStreamStatus()
2342
2449
  }
2343
2450
  ), /* @__PURE__ */ import_react_ecs4.default.createElement(
2344
2451
  import_react_ecs4.Button,
2345
2452
  {
2346
- uiTransform: { width: 85, height: 28, margin: 3 },
2453
+ uiTransform: { width: this.s(95), height: this.s(UI_DIMENSIONS.admin.buttonHeightSmall), margin: 4 },
2347
2454
  uiBackground: { color: this.keyRotating ? C.btn : C.yellow },
2348
2455
  value: this.keyRotating ? "..." : "Rotate Key",
2349
- fontSize: 9,
2456
+ fontSize: t.buttonSmall,
2350
2457
  color: C.text,
2351
2458
  onMouseDown: () => this.rotateStreamKey()
2352
2459
  }
2353
2460
  ), !this.streamData.isLive && /* @__PURE__ */ import_react_ecs4.default.createElement(
2354
2461
  import_react_ecs4.Button,
2355
2462
  {
2356
- uiTransform: { width: 60, height: 28, margin: 3 },
2463
+ uiTransform: { width: this.s(70), height: this.s(UI_DIMENSIONS.admin.buttonHeightSmall), margin: 4 },
2357
2464
  uiBackground: { color: this.channelDeleting ? C.btn : C.red },
2358
2465
  value: this.channelDeleting ? "..." : "Delete",
2359
- fontSize: 9,
2466
+ fontSize: t.buttonSmall,
2360
2467
  color: C.text,
2361
2468
  onMouseDown: () => this.deleteChannel()
2362
2469
  }
2363
- )), this.streamControlStatus === "started" && /* @__PURE__ */ import_react_ecs4.default.createElement(import_react_ecs4.Label, { value: "Stream started - begin broadcasting in OBS", fontSize: 9, color: C.green }), this.streamControlStatus === "stopped" && /* @__PURE__ */ import_react_ecs4.default.createElement(import_react_ecs4.Label, { value: "Stream stopped", fontSize: 9, color: C.textDim }), this.keyRotateStatus === "success" && /* @__PURE__ */ import_react_ecs4.default.createElement(import_react_ecs4.Label, { value: "Key rotated! Update OBS", fontSize: 9, color: C.green }), this.keyRotateStatus === "error" && /* @__PURE__ */ import_react_ecs4.default.createElement(import_react_ecs4.Label, { value: "Failed to rotate key", fontSize: 9, color: C.red }), this.channelDeleteError && /* @__PURE__ */ import_react_ecs4.default.createElement(import_react_ecs4.Label, { value: this.channelDeleteError, fontSize: 9, color: C.red }), this.streamControlStatus === "error" && /* @__PURE__ */ import_react_ecs4.default.createElement(import_react_ecs4.Label, { value: "Stream control failed", fontSize: 9, color: C.red })), /* @__PURE__ */ import_react_ecs4.default.createElement(this.SectionHead, { label: "PLAY NOW", color: C.orange }), /* @__PURE__ */ import_react_ecs4.default.createElement(import_react_ecs4.UiEntity, { uiTransform: { flexDirection: "row", alignItems: "center", margin: { bottom: 12 } } }, /* @__PURE__ */ import_react_ecs4.default.createElement(
2470
+ )), this.streamControlStatus === "started" && /* @__PURE__ */ import_react_ecs4.default.createElement(import_react_ecs4.Label, { value: "Stream started - begin broadcasting in OBS", fontSize: t.status, color: C.green }), this.streamControlStatus === "stopped" && /* @__PURE__ */ import_react_ecs4.default.createElement(import_react_ecs4.Label, { value: "Stream stopped", fontSize: t.status, color: C.textDim }), this.keyRotateStatus === "success" && /* @__PURE__ */ import_react_ecs4.default.createElement(import_react_ecs4.Label, { value: "Key rotated! Update OBS", fontSize: t.status, color: C.green }), this.keyRotateStatus === "error" && /* @__PURE__ */ import_react_ecs4.default.createElement(import_react_ecs4.Label, { value: "Failed to rotate key", fontSize: t.status, color: C.red }), this.channelDeleteError && /* @__PURE__ */ import_react_ecs4.default.createElement(import_react_ecs4.Label, { value: this.channelDeleteError, fontSize: t.status, color: C.red }), this.streamControlStatus === "error" && /* @__PURE__ */ import_react_ecs4.default.createElement(import_react_ecs4.Label, { value: "Stream control failed", fontSize: t.status, color: C.red })), /* @__PURE__ */ import_react_ecs4.default.createElement(this.SectionHead, { label: "PLAY NOW", color: C.orange }), /* @__PURE__ */ import_react_ecs4.default.createElement(import_react_ecs4.UiEntity, { uiTransform: { flexDirection: "row", alignItems: "center", margin: { bottom: 14 } } }, /* @__PURE__ */ import_react_ecs4.default.createElement(
2364
2471
  import_react_ecs4.Input,
2365
2472
  {
2366
- uiTransform: { width: 220, height: 32 },
2473
+ uiTransform: { width: this.s(230), height: this.s(UI_DIMENSIONS.admin.inputHeight) },
2367
2474
  uiBackground: { color: import_math5.Color4.create(0.15, 0.15, 0.2, 1) },
2368
2475
  placeholder: "Video URL...",
2369
2476
  placeholderColor: C.textDim,
2370
2477
  color: C.text,
2371
- fontSize: 11,
2478
+ fontSize: t.input,
2372
2479
  value: this.customVideoUrl,
2373
2480
  onChange: (val) => {
2374
2481
  this.customVideoUrl = val;
@@ -2377,10 +2484,10 @@ var AdminPanelUIModule = class {
2377
2484
  ), /* @__PURE__ */ import_react_ecs4.default.createElement(
2378
2485
  import_react_ecs4.Button,
2379
2486
  {
2380
- uiTransform: { width: 70, height: 32, margin: { left: 6 } },
2487
+ uiTransform: { width: this.s(75), height: this.s(UI_DIMENSIONS.admin.inputHeight), margin: { left: 8 } },
2381
2488
  uiBackground: { color: C.green },
2382
2489
  value: "Play",
2383
- fontSize: 11,
2490
+ fontSize: t.button,
2384
2491
  color: C.text,
2385
2492
  onMouseDown: () => {
2386
2493
  if (this.customVideoUrl) this.config.onVideoPlay?.(this.customVideoUrl);
@@ -2389,211 +2496,218 @@ var AdminPanelUIModule = class {
2389
2496
  ), /* @__PURE__ */ import_react_ecs4.default.createElement(
2390
2497
  import_react_ecs4.Button,
2391
2498
  {
2392
- uiTransform: { width: 60, height: 32, margin: { left: 4 } },
2499
+ uiTransform: { width: this.s(65), height: this.s(UI_DIMENSIONS.admin.inputHeight), margin: { left: 6 } },
2393
2500
  uiBackground: { color: C.btn },
2394
2501
  value: "Clear",
2395
- fontSize: 11,
2502
+ fontSize: t.button,
2396
2503
  color: C.text,
2397
2504
  onMouseDown: () => {
2398
2505
  this.customVideoUrl = "";
2399
2506
  }
2400
2507
  }
2401
- )), /* @__PURE__ */ import_react_ecs4.default.createElement(this.SectionHead, { label: "PLAYBACK", color: C.green }), /* @__PURE__ */ import_react_ecs4.default.createElement(import_react_ecs4.UiEntity, { uiTransform: { flexDirection: "row", flexWrap: "wrap", margin: { bottom: 12 } } }, /* @__PURE__ */ import_react_ecs4.default.createElement(
2508
+ )), /* @__PURE__ */ import_react_ecs4.default.createElement(this.SectionHead, { label: "PLAYBACK", color: C.green }), /* @__PURE__ */ import_react_ecs4.default.createElement(import_react_ecs4.UiEntity, { uiTransform: { flexDirection: "row", flexWrap: "wrap", margin: { bottom: 14 } } }, /* @__PURE__ */ import_react_ecs4.default.createElement(
2402
2509
  import_react_ecs4.Button,
2403
2510
  {
2404
- uiTransform: { width: 70, height: 32, margin: 3 },
2511
+ uiTransform: { width: this.s(75), height: this.s(UI_DIMENSIONS.admin.buttonHeightSmall), margin: 4 },
2405
2512
  uiBackground: { color: C.green },
2406
2513
  value: "Play",
2407
- fontSize: 11,
2514
+ fontSize: t.button,
2408
2515
  color: C.text,
2409
2516
  onMouseDown: () => this.config.onCommand?.("videoPlay", { playing: true })
2410
2517
  }
2411
2518
  ), /* @__PURE__ */ import_react_ecs4.default.createElement(
2412
2519
  import_react_ecs4.Button,
2413
2520
  {
2414
- uiTransform: { width: 70, height: 32, margin: 3 },
2521
+ uiTransform: { width: this.s(75), height: this.s(UI_DIMENSIONS.admin.buttonHeightSmall), margin: 4 },
2415
2522
  uiBackground: { color: C.red },
2416
2523
  value: "Stop",
2417
- fontSize: 11,
2524
+ fontSize: t.button,
2418
2525
  color: C.text,
2419
2526
  onMouseDown: () => this.config.onVideoStop?.()
2420
2527
  }
2421
2528
  ), /* @__PURE__ */ import_react_ecs4.default.createElement(
2422
2529
  import_react_ecs4.Button,
2423
2530
  {
2424
- uiTransform: { width: 90, height: 32, margin: 3 },
2531
+ uiTransform: { width: this.s(100), height: this.s(UI_DIMENSIONS.admin.buttonHeightSmall), margin: 4 },
2425
2532
  uiBackground: { color: C.btn },
2426
2533
  value: "Reset Default",
2427
- fontSize: 10,
2534
+ fontSize: t.buttonSmall,
2428
2535
  color: C.text,
2429
2536
  onMouseDown: () => this.config.onCommand?.("videoClear", {})
2430
2537
  }
2431
- )), /* @__PURE__ */ import_react_ecs4.default.createElement(this.SectionHead, { label: "VIDEO SLOTS", color: C.cyan }), /* @__PURE__ */ import_react_ecs4.default.createElement(import_react_ecs4.UiEntity, { uiTransform: { flexDirection: "row", flexWrap: "wrap", margin: { bottom: 8 } } }, /* @__PURE__ */ import_react_ecs4.default.createElement(import_react_ecs4.Button, { uiTransform: { width: 65, height: 32, margin: 3 }, uiBackground: { color: C.cyan }, value: "Play 1", fontSize: 11, color: C.text, onMouseDown: () => this.config.onVideoSlotPlay?.("slot1") }), /* @__PURE__ */ import_react_ecs4.default.createElement(import_react_ecs4.Button, { uiTransform: { width: 65, height: 32, margin: 3 }, uiBackground: { color: C.cyan }, value: "Play 2", fontSize: 11, color: C.text, onMouseDown: () => this.config.onVideoSlotPlay?.("slot2") }), /* @__PURE__ */ import_react_ecs4.default.createElement(import_react_ecs4.Button, { uiTransform: { width: 65, height: 32, margin: 3 }, uiBackground: { color: C.cyan }, value: "Play 3", fontSize: 11, color: C.text, onMouseDown: () => this.config.onVideoSlotPlay?.("slot3") }), /* @__PURE__ */ import_react_ecs4.default.createElement(import_react_ecs4.Button, { uiTransform: { width: 65, height: 32, margin: 3 }, uiBackground: { color: C.cyan }, value: "Play 4", fontSize: 11, color: C.text, onMouseDown: () => this.config.onVideoSlotPlay?.("slot4") }), /* @__PURE__ */ import_react_ecs4.default.createElement(import_react_ecs4.Button, { uiTransform: { width: 65, height: 32, margin: 3 }, uiBackground: { color: C.cyan }, value: "Play 5", fontSize: 11, color: C.text, onMouseDown: () => this.config.onVideoSlotPlay?.("slot5") })), /* @__PURE__ */ import_react_ecs4.default.createElement(
2538
+ )), /* @__PURE__ */ import_react_ecs4.default.createElement(this.SectionHead, { label: "VIDEO SLOTS", color: C.cyan }), /* @__PURE__ */ import_react_ecs4.default.createElement(import_react_ecs4.UiEntity, { uiTransform: { flexDirection: "row", flexWrap: "wrap", margin: { bottom: 10 } } }, /* @__PURE__ */ import_react_ecs4.default.createElement(import_react_ecs4.Button, { uiTransform: { width: this.s(70), height: this.s(UI_DIMENSIONS.admin.buttonHeightSmall), margin: 4 }, uiBackground: { color: C.cyan }, value: "Play 1", fontSize: t.button, color: C.text, onMouseDown: () => this.playSlot("slot1") }), /* @__PURE__ */ import_react_ecs4.default.createElement(import_react_ecs4.Button, { uiTransform: { width: this.s(70), height: this.s(UI_DIMENSIONS.admin.buttonHeightSmall), margin: 4 }, uiBackground: { color: C.cyan }, value: "Play 2", fontSize: t.button, color: C.text, onMouseDown: () => this.playSlot("slot2") }), /* @__PURE__ */ import_react_ecs4.default.createElement(import_react_ecs4.Button, { uiTransform: { width: this.s(70), height: this.s(UI_DIMENSIONS.admin.buttonHeightSmall), margin: 4 }, uiBackground: { color: C.cyan }, value: "Play 3", fontSize: t.button, color: C.text, onMouseDown: () => this.playSlot("slot3") }), /* @__PURE__ */ import_react_ecs4.default.createElement(import_react_ecs4.Button, { uiTransform: { width: this.s(70), height: this.s(UI_DIMENSIONS.admin.buttonHeightSmall), margin: 4 }, uiBackground: { color: C.cyan }, value: "Play 4", fontSize: t.button, color: C.text, onMouseDown: () => this.playSlot("slot4") }), /* @__PURE__ */ import_react_ecs4.default.createElement(import_react_ecs4.Button, { uiTransform: { width: this.s(70), height: this.s(UI_DIMENSIONS.admin.buttonHeightSmall), margin: 4 }, uiBackground: { color: C.cyan }, value: "Play 5", fontSize: t.button, color: C.text, onMouseDown: () => this.playSlot("slot5") })), /* @__PURE__ */ import_react_ecs4.default.createElement(
2432
2539
  import_react_ecs4.Button,
2433
2540
  {
2434
- uiTransform: { height: 20 },
2541
+ uiTransform: { height: this.s(24) },
2435
2542
  uiBackground: { color: import_math5.Color4.create(0, 0, 0, 0) },
2436
2543
  value: "Edit slots at thestatic.tv \u2192",
2437
- fontSize: 10,
2544
+ fontSize: t.labelSmall,
2438
2545
  color: C.cyan,
2439
2546
  onMouseDown: () => (0, import_RestrictedActions3.openExternalUrl)({ url: this.config.footerLink || `https://thestatic.tv/scene/${this.config.sceneId}` })
2440
2547
  }
2441
2548
  ));
2442
2549
  };
2443
- this.ModTab = () => /* @__PURE__ */ import_react_ecs4.default.createElement(import_react_ecs4.UiEntity, { uiTransform: { flexDirection: "column", width: "100%", padding: 8 } }, this.modStatus === "loading" && /* @__PURE__ */ import_react_ecs4.default.createElement(import_react_ecs4.Label, { value: "Loading...", fontSize: 11, color: C.yellow }), this.modStatus === "saved" && /* @__PURE__ */ import_react_ecs4.default.createElement(import_react_ecs4.Label, { value: "Saved!", fontSize: 11, color: C.green }), this.modStatus === "error" && /* @__PURE__ */ import_react_ecs4.default.createElement(import_react_ecs4.Label, { value: "Error - check input", fontSize: 11, color: C.red }), /* @__PURE__ */ import_react_ecs4.default.createElement(this.SectionHead, { label: "BROADCAST", color: C.orange }), /* @__PURE__ */ import_react_ecs4.default.createElement(import_react_ecs4.UiEntity, { uiTransform: { flexDirection: "row", alignItems: "center", margin: { bottom: 12 } } }, /* @__PURE__ */ import_react_ecs4.default.createElement(
2444
- import_react_ecs4.Input,
2445
- {
2446
- uiTransform: { width: 200, height: 32 },
2447
- uiBackground: { color: import_math5.Color4.create(0.15, 0.15, 0.2, 1) },
2448
- placeholder: "Message to all players...",
2449
- placeholderColor: C.textDim,
2450
- color: C.text,
2451
- fontSize: 11,
2452
- value: this.broadcastText,
2453
- onChange: (val) => {
2454
- this.broadcastText = val;
2550
+ this.ModTab = () => {
2551
+ const t = this.theme;
2552
+ return /* @__PURE__ */ import_react_ecs4.default.createElement(import_react_ecs4.UiEntity, { uiTransform: { flexDirection: "column", width: "100%", padding: 10 } }, this.modStatus === "loading" && /* @__PURE__ */ import_react_ecs4.default.createElement(import_react_ecs4.Label, { value: "Loading...", fontSize: t.status, color: C.yellow }), this.modStatus === "saved" && /* @__PURE__ */ import_react_ecs4.default.createElement(import_react_ecs4.Label, { value: "Saved!", fontSize: t.status, color: C.green }), this.modStatus === "error" && /* @__PURE__ */ import_react_ecs4.default.createElement(import_react_ecs4.Label, { value: "Error - check input", fontSize: t.status, color: C.red }), /* @__PURE__ */ import_react_ecs4.default.createElement(this.SectionHead, { label: "BROADCAST", color: C.orange }), /* @__PURE__ */ import_react_ecs4.default.createElement(import_react_ecs4.UiEntity, { uiTransform: { flexDirection: "row", alignItems: "center", margin: { bottom: 14 } } }, /* @__PURE__ */ import_react_ecs4.default.createElement(
2553
+ import_react_ecs4.Input,
2554
+ {
2555
+ uiTransform: { width: this.s(230), height: this.s(UI_DIMENSIONS.admin.inputHeight) },
2556
+ uiBackground: { color: import_math5.Color4.create(0.15, 0.15, 0.2, 1) },
2557
+ placeholder: "Message to all players...",
2558
+ placeholderColor: C.textDim,
2559
+ color: C.text,
2560
+ fontSize: t.input,
2561
+ value: this.broadcastText,
2562
+ onChange: (val) => {
2563
+ this.broadcastText = val;
2564
+ }
2455
2565
  }
2456
- }
2457
- ), /* @__PURE__ */ import_react_ecs4.default.createElement(
2458
- import_react_ecs4.Button,
2459
- {
2460
- uiTransform: { width: 55, height: 32, margin: { left: 6 } },
2461
- uiBackground: { color: C.orange },
2462
- value: "Send",
2463
- fontSize: 11,
2464
- color: C.text,
2465
- onMouseDown: () => this.sendBroadcast()
2466
- }
2467
- )), /* @__PURE__ */ import_react_ecs4.default.createElement(this.SectionHead, { label: "CHAOS MODE", color: C.red }), /* @__PURE__ */ import_react_ecs4.default.createElement(import_react_ecs4.UiEntity, { uiTransform: { flexDirection: "row", flexWrap: "wrap", margin: { bottom: 12 } } }, /* @__PURE__ */ import_react_ecs4.default.createElement(
2468
- import_react_ecs4.Button,
2469
- {
2470
- uiTransform: { width: 120, height: 32, margin: 3 },
2471
- uiBackground: { color: C.red },
2472
- value: "KICK ALL",
2473
- fontSize: 11,
2474
- color: C.text,
2475
- onMouseDown: () => this.config.onCommand?.("kickAll", {})
2476
- }
2477
- )), /* @__PURE__ */ import_react_ecs4.default.createElement(this.SectionHead, { label: "SCENE ADMINS", color: C.purple }), /* @__PURE__ */ import_react_ecs4.default.createElement(import_react_ecs4.UiEntity, { uiTransform: { flexDirection: "column", margin: { bottom: 4 } } }, this.sceneAdmins.length === 0 && this.modStatus !== "loading" && /* @__PURE__ */ import_react_ecs4.default.createElement(import_react_ecs4.Label, { value: "No scene admins", fontSize: 10, color: C.textDim }), this.sceneAdmins.map((wallet, i) => /* @__PURE__ */ import_react_ecs4.default.createElement(
2478
- import_react_ecs4.UiEntity,
2479
- {
2480
- key: `admin-${i}`,
2481
- uiTransform: { flexDirection: "row", alignItems: "center", margin: { bottom: 3 }, width: "100%" }
2482
- },
2483
- /* @__PURE__ */ import_react_ecs4.default.createElement(
2484
- import_react_ecs4.Label,
2566
+ ), /* @__PURE__ */ import_react_ecs4.default.createElement(
2567
+ import_react_ecs4.Button,
2485
2568
  {
2486
- value: `${wallet.slice(0, 6)}...${wallet.slice(-4)}`,
2487
- fontSize: 10,
2569
+ uiTransform: { width: this.s(65), height: this.s(UI_DIMENSIONS.admin.inputHeight), margin: { left: 8 } },
2570
+ uiBackground: { color: C.orange },
2571
+ value: "Send",
2572
+ fontSize: t.button,
2488
2573
  color: C.text,
2489
- uiTransform: { width: 110 }
2574
+ onMouseDown: () => this.sendBroadcast()
2490
2575
  }
2491
- ),
2492
- /* @__PURE__ */ import_react_ecs4.default.createElement(
2576
+ )), /* @__PURE__ */ import_react_ecs4.default.createElement(this.SectionHead, { label: "CHAOS MODE", color: C.red }), /* @__PURE__ */ import_react_ecs4.default.createElement(import_react_ecs4.UiEntity, { uiTransform: { flexDirection: "row", flexWrap: "wrap", margin: { bottom: 14 } } }, /* @__PURE__ */ import_react_ecs4.default.createElement(
2493
2577
  import_react_ecs4.Button,
2494
2578
  {
2495
- uiTransform: { width: 55, height: 24, margin: { left: 6 } },
2496
- uiBackground: { color: C.btn },
2497
- value: "Remove",
2498
- fontSize: 9,
2579
+ uiTransform: { width: this.s(130), height: this.s(UI_DIMENSIONS.admin.buttonHeight), margin: 4 },
2580
+ uiBackground: { color: C.red },
2581
+ value: "KICK ALL",
2582
+ fontSize: t.button,
2499
2583
  color: C.text,
2500
- onMouseDown: () => this.removeSceneAdmin(wallet)
2584
+ onMouseDown: () => {
2585
+ this.log("KICK ALL clicked");
2586
+ this.config.onCommand?.("kickAll", {});
2587
+ }
2501
2588
  }
2502
- )
2503
- ))), /* @__PURE__ */ import_react_ecs4.default.createElement(import_react_ecs4.UiEntity, { uiTransform: { flexDirection: "row", alignItems: "center", margin: { bottom: 12 } } }, /* @__PURE__ */ import_react_ecs4.default.createElement(
2504
- import_react_ecs4.Input,
2505
- {
2506
- uiTransform: { width: 200, height: 28 },
2507
- uiBackground: { color: import_math5.Color4.create(0.15, 0.15, 0.2, 1) },
2508
- placeholder: "0x... add admin",
2509
- placeholderColor: C.textDim,
2510
- color: C.text,
2511
- fontSize: 10,
2512
- value: this.newAdminWallet,
2513
- onChange: (val) => {
2514
- this.newAdminWallet = val;
2589
+ )), /* @__PURE__ */ import_react_ecs4.default.createElement(this.SectionHead, { label: "SCENE ADMINS", color: C.purple }), /* @__PURE__ */ import_react_ecs4.default.createElement(import_react_ecs4.UiEntity, { uiTransform: { flexDirection: "column", margin: { bottom: 6 } } }, this.sceneAdmins.length === 0 && this.modStatus !== "loading" && /* @__PURE__ */ import_react_ecs4.default.createElement(import_react_ecs4.Label, { value: "No scene admins", fontSize: t.labelSmall, color: C.textDim }), this.sceneAdmins.map((wallet, i) => /* @__PURE__ */ import_react_ecs4.default.createElement(
2590
+ import_react_ecs4.UiEntity,
2591
+ {
2592
+ key: `admin-${i}`,
2593
+ uiTransform: { flexDirection: "row", alignItems: "center", margin: { bottom: 5 }, width: "100%" }
2594
+ },
2595
+ /* @__PURE__ */ import_react_ecs4.default.createElement(
2596
+ import_react_ecs4.Label,
2597
+ {
2598
+ value: `${wallet.slice(0, 6)}...${wallet.slice(-4)}`,
2599
+ fontSize: t.label,
2600
+ color: C.text,
2601
+ uiTransform: { width: this.s(130) }
2602
+ }
2603
+ ),
2604
+ /* @__PURE__ */ import_react_ecs4.default.createElement(
2605
+ import_react_ecs4.Button,
2606
+ {
2607
+ uiTransform: { width: this.s(70), height: this.s(28), margin: { left: 8 } },
2608
+ uiBackground: { color: C.btn },
2609
+ value: "Remove",
2610
+ fontSize: t.buttonSmall,
2611
+ color: C.text,
2612
+ onMouseDown: () => this.removeSceneAdmin(wallet)
2613
+ }
2614
+ )
2615
+ ))), /* @__PURE__ */ import_react_ecs4.default.createElement(import_react_ecs4.UiEntity, { uiTransform: { flexDirection: "row", alignItems: "center", margin: { bottom: 14 } } }, /* @__PURE__ */ import_react_ecs4.default.createElement(
2616
+ import_react_ecs4.Input,
2617
+ {
2618
+ uiTransform: { width: this.s(230), height: this.s(UI_DIMENSIONS.admin.inputHeight) },
2619
+ uiBackground: { color: import_math5.Color4.create(0.15, 0.15, 0.2, 1) },
2620
+ placeholder: "0x... add admin",
2621
+ placeholderColor: C.textDim,
2622
+ color: C.text,
2623
+ fontSize: t.input,
2624
+ value: this.newAdminWallet,
2625
+ onChange: (val) => {
2626
+ this.newAdminWallet = val;
2627
+ }
2515
2628
  }
2516
- }
2517
- ), /* @__PURE__ */ import_react_ecs4.default.createElement(
2518
- import_react_ecs4.Button,
2519
- {
2520
- uiTransform: { width: 50, height: 28, margin: { left: 6 } },
2521
- uiBackground: { color: C.purple },
2522
- value: "Add",
2523
- fontSize: 10,
2524
- color: C.text,
2525
- onMouseDown: () => {
2526
- if (this.newAdminWallet) this.addSceneAdmin(this.newAdminWallet);
2629
+ ), /* @__PURE__ */ import_react_ecs4.default.createElement(
2630
+ import_react_ecs4.Button,
2631
+ {
2632
+ uiTransform: { width: this.s(60), height: this.s(UI_DIMENSIONS.admin.inputHeight), margin: { left: 8 } },
2633
+ uiBackground: { color: C.purple },
2634
+ value: "Add",
2635
+ fontSize: t.button,
2636
+ color: C.text,
2637
+ onMouseDown: () => {
2638
+ if (this.newAdminWallet) this.addSceneAdmin(this.newAdminWallet);
2639
+ }
2527
2640
  }
2528
- }
2529
- )), /* @__PURE__ */ import_react_ecs4.default.createElement(this.SectionHead, { label: "BANNED WALLETS", color: C.red }), /* @__PURE__ */ import_react_ecs4.default.createElement(import_react_ecs4.UiEntity, { uiTransform: { flexDirection: "column", margin: { bottom: 4 } } }, this.bannedWallets.length === 0 && this.modStatus !== "loading" && /* @__PURE__ */ import_react_ecs4.default.createElement(import_react_ecs4.Label, { value: "No banned wallets", fontSize: 10, color: C.textDim }), this.bannedWallets.map((wallet, i) => /* @__PURE__ */ import_react_ecs4.default.createElement(
2530
- import_react_ecs4.UiEntity,
2531
- {
2532
- key: `ban-${i}`,
2533
- uiTransform: { flexDirection: "row", alignItems: "center", margin: { bottom: 3 }, width: "100%" }
2534
- },
2535
- /* @__PURE__ */ import_react_ecs4.default.createElement(
2536
- import_react_ecs4.Label,
2641
+ )), /* @__PURE__ */ import_react_ecs4.default.createElement(this.SectionHead, { label: "BANNED WALLETS", color: C.red }), /* @__PURE__ */ import_react_ecs4.default.createElement(import_react_ecs4.UiEntity, { uiTransform: { flexDirection: "column", margin: { bottom: 6 } } }, this.bannedWallets.length === 0 && this.modStatus !== "loading" && /* @__PURE__ */ import_react_ecs4.default.createElement(import_react_ecs4.Label, { value: "No banned wallets", fontSize: t.labelSmall, color: C.textDim }), this.bannedWallets.map((wallet, i) => /* @__PURE__ */ import_react_ecs4.default.createElement(
2642
+ import_react_ecs4.UiEntity,
2537
2643
  {
2538
- value: `${wallet.slice(0, 6)}...${wallet.slice(-4)}`,
2539
- fontSize: 10,
2540
- color: C.red,
2541
- uiTransform: { width: 110 }
2644
+ key: `ban-${i}`,
2645
+ uiTransform: { flexDirection: "row", alignItems: "center", margin: { bottom: 5 }, width: "100%" }
2646
+ },
2647
+ /* @__PURE__ */ import_react_ecs4.default.createElement(
2648
+ import_react_ecs4.Label,
2649
+ {
2650
+ value: `${wallet.slice(0, 6)}...${wallet.slice(-4)}`,
2651
+ fontSize: t.label,
2652
+ color: C.red,
2653
+ uiTransform: { width: this.s(130) }
2654
+ }
2655
+ ),
2656
+ /* @__PURE__ */ import_react_ecs4.default.createElement(
2657
+ import_react_ecs4.Button,
2658
+ {
2659
+ uiTransform: { width: this.s(70), height: this.s(28), margin: { left: 8 } },
2660
+ uiBackground: { color: C.green },
2661
+ value: "Unban",
2662
+ fontSize: t.buttonSmall,
2663
+ color: C.text,
2664
+ onMouseDown: () => this.unbanWallet(wallet)
2665
+ }
2666
+ )
2667
+ ))), /* @__PURE__ */ import_react_ecs4.default.createElement(import_react_ecs4.UiEntity, { uiTransform: { flexDirection: "row", alignItems: "center", margin: { bottom: 10 } } }, /* @__PURE__ */ import_react_ecs4.default.createElement(
2668
+ import_react_ecs4.Input,
2669
+ {
2670
+ uiTransform: { width: this.s(230), height: this.s(UI_DIMENSIONS.admin.inputHeight) },
2671
+ uiBackground: { color: import_math5.Color4.create(0.15, 0.15, 0.2, 1) },
2672
+ placeholder: "0x... ban wallet",
2673
+ placeholderColor: C.textDim,
2674
+ color: C.text,
2675
+ fontSize: t.input,
2676
+ value: this.newBanWallet,
2677
+ onChange: (val) => {
2678
+ this.newBanWallet = val;
2679
+ }
2542
2680
  }
2543
- ),
2544
- /* @__PURE__ */ import_react_ecs4.default.createElement(
2681
+ ), /* @__PURE__ */ import_react_ecs4.default.createElement(
2545
2682
  import_react_ecs4.Button,
2546
2683
  {
2547
- uiTransform: { width: 55, height: 24, margin: { left: 6 } },
2548
- uiBackground: { color: C.green },
2549
- value: "Unban",
2550
- fontSize: 9,
2684
+ uiTransform: { width: this.s(60), height: this.s(UI_DIMENSIONS.admin.inputHeight), margin: { left: 8 } },
2685
+ uiBackground: { color: C.red },
2686
+ value: "Ban",
2687
+ fontSize: t.button,
2551
2688
  color: C.text,
2552
- onMouseDown: () => this.unbanWallet(wallet)
2553
- }
2554
- )
2555
- ))), /* @__PURE__ */ import_react_ecs4.default.createElement(import_react_ecs4.UiEntity, { uiTransform: { flexDirection: "row", alignItems: "center", margin: { bottom: 8 } } }, /* @__PURE__ */ import_react_ecs4.default.createElement(
2556
- import_react_ecs4.Input,
2557
- {
2558
- uiTransform: { width: 200, height: 28 },
2559
- uiBackground: { color: import_math5.Color4.create(0.15, 0.15, 0.2, 1) },
2560
- placeholder: "0x... ban wallet",
2561
- placeholderColor: C.textDim,
2562
- color: C.text,
2563
- fontSize: 10,
2564
- value: this.newBanWallet,
2565
- onChange: (val) => {
2566
- this.newBanWallet = val;
2689
+ onMouseDown: () => {
2690
+ if (this.newBanWallet) this.banWallet(this.newBanWallet);
2691
+ }
2567
2692
  }
2568
- }
2569
- ), /* @__PURE__ */ import_react_ecs4.default.createElement(
2570
- import_react_ecs4.Button,
2571
- {
2572
- uiTransform: { width: 50, height: 28, margin: { left: 6 } },
2573
- uiBackground: { color: C.red },
2574
- value: "Ban",
2575
- fontSize: 10,
2576
- color: C.text,
2577
- onMouseDown: () => {
2578
- if (this.newBanWallet) this.banWallet(this.newBanWallet);
2693
+ )), /* @__PURE__ */ import_react_ecs4.default.createElement(
2694
+ import_react_ecs4.Button,
2695
+ {
2696
+ uiTransform: { height: this.s(24) },
2697
+ uiBackground: { color: import_math5.Color4.create(0, 0, 0, 0) },
2698
+ value: "Manage at thestatic.tv \u2192",
2699
+ fontSize: t.labelSmall,
2700
+ color: C.cyan,
2701
+ onMouseDown: () => (0, import_RestrictedActions3.openExternalUrl)({ url: this.config.footerLink || `https://thestatic.tv/scene/${this.config.sceneId}` })
2579
2702
  }
2580
- }
2581
- )), /* @__PURE__ */ import_react_ecs4.default.createElement(
2582
- import_react_ecs4.Button,
2583
- {
2584
- uiTransform: { height: 20 },
2585
- uiBackground: { color: import_math5.Color4.create(0, 0, 0, 0) },
2586
- value: "Manage at thestatic.tv \u2192",
2587
- fontSize: 9,
2588
- color: C.cyan,
2589
- onMouseDown: () => (0, import_RestrictedActions3.openExternalUrl)({ url: this.config.footerLink || `https://thestatic.tv/scene/${this.config.sceneId}` })
2590
- }
2591
- ));
2703
+ ));
2704
+ };
2592
2705
  /**
2593
2706
  * Get the React-ECS component for the admin panel
2594
2707
  */
2595
2708
  this.getComponent = () => {
2596
2709
  if (!this.isAdmin) return null;
2710
+ const t = this.theme;
2597
2711
  const tabs = [];
2598
2712
  if (this.config.sceneTabs && this.config.sceneTabs.length > 0) {
2599
2713
  this.config.sceneTabs.forEach((tab) => tabs.push({ label: tab.label, id: tab.id }));
@@ -2604,7 +2718,7 @@ var AdminPanelUIModule = class {
2604
2718
  if (this.config.showModTab !== false && this.isOwner) {
2605
2719
  tabs.push({ label: "MOD", id: "mod" });
2606
2720
  }
2607
- if (!tabs.find((t) => t.id === this.activeTab) && tabs.length > 0) {
2721
+ if (!tabs.find((tab) => tab.id === this.activeTab) && tabs.length > 0) {
2608
2722
  this.activeTab = tabs[0].id;
2609
2723
  }
2610
2724
  return /* @__PURE__ */ import_react_ecs4.default.createElement(
@@ -2620,17 +2734,17 @@ var AdminPanelUIModule = class {
2620
2734
  import_react_ecs4.UiEntity,
2621
2735
  {
2622
2736
  uiTransform: {
2623
- position: { right: 16, bottom: 16 },
2737
+ position: { right: 20 + this.s(100) + 10 + this.s(100) + 10, bottom: 10 },
2624
2738
  positionType: "absolute"
2625
2739
  }
2626
2740
  },
2627
2741
  /* @__PURE__ */ import_react_ecs4.default.createElement(
2628
2742
  import_react_ecs4.Button,
2629
2743
  {
2630
- uiTransform: { width: 90, height: 36 },
2744
+ uiTransform: { width: this.s(100), height: this.s(45) },
2631
2745
  uiBackground: { color: this.panelOpen ? C.btn : C.header },
2632
2746
  value: this.panelOpen ? "CLOSE" : "ADMIN",
2633
- fontSize: 13,
2747
+ fontSize: this.s(14),
2634
2748
  color: C.text,
2635
2749
  onMouseDown: () => this.toggle()
2636
2750
  }
@@ -2639,10 +2753,12 @@ var AdminPanelUIModule = class {
2639
2753
  this.panelOpen && /* @__PURE__ */ import_react_ecs4.default.createElement(
2640
2754
  import_react_ecs4.UiEntity,
2641
2755
  {
2756
+ key: `admin-panel-${this.client.uiScale}`,
2642
2757
  uiTransform: {
2643
- width: 380,
2644
- maxHeight: 800,
2645
- position: { right: 16, bottom: 60 },
2758
+ width: this.s(UI_DIMENSIONS.admin.width),
2759
+ height: this.s(UI_DIMENSIONS.admin.height),
2760
+ maxHeight: this.s(UI_DIMENSIONS.admin.maxHeight),
2761
+ position: { right: UI_DIMENSIONS.chat.right + this.s(UI_DIMENSIONS.chat.width) + 10, bottom: UI_DIMENSIONS.admin.bottom },
2646
2762
  positionType: "absolute",
2647
2763
  flexDirection: "column"
2648
2764
  },
@@ -2653,16 +2769,28 @@ var AdminPanelUIModule = class {
2653
2769
  {
2654
2770
  uiTransform: {
2655
2771
  width: "100%",
2656
- height: 44,
2657
- justifyContent: "center",
2772
+ height: this.s(UI_DIMENSIONS.admin.headerHeight),
2773
+ justifyContent: "space-between",
2658
2774
  alignItems: "center",
2659
- flexDirection: "row"
2775
+ flexDirection: "row",
2776
+ padding: { left: 12, right: 8 }
2660
2777
  },
2661
2778
  uiBackground: { color: C.header }
2662
2779
  },
2663
- /* @__PURE__ */ import_react_ecs4.default.createElement(import_react_ecs4.Label, { value: this.config.title || "ADMIN PANEL", fontSize: 15, color: C.text })
2780
+ /* @__PURE__ */ import_react_ecs4.default.createElement(import_react_ecs4.Label, { value: this.config.title || "ADMIN PANEL", fontSize: t.header, color: C.text }),
2781
+ /* @__PURE__ */ import_react_ecs4.default.createElement(
2782
+ import_react_ecs4.Button,
2783
+ {
2784
+ uiTransform: { width: this.s(28), height: this.s(28) },
2785
+ uiBackground: { color: import_math5.Color4.create(0, 0, 0, 0.3) },
2786
+ value: "X",
2787
+ fontSize: t.header,
2788
+ color: THEME.colors.red,
2789
+ onMouseDown: () => this.hide()
2790
+ }
2791
+ )
2664
2792
  ),
2665
- /* @__PURE__ */ import_react_ecs4.default.createElement(import_react_ecs4.UiEntity, { uiTransform: { width: "100%", height: 36, flexDirection: "row" } }, tabs.map((tab) => /* @__PURE__ */ import_react_ecs4.default.createElement(this.TabBtn, { label: tab.label, tab: tab.id }))),
2793
+ /* @__PURE__ */ import_react_ecs4.default.createElement(import_react_ecs4.UiEntity, { uiTransform: { width: "100%", height: this.s(UI_DIMENSIONS.admin.tabHeight), flexDirection: "row" } }, tabs.map((tab) => /* @__PURE__ */ import_react_ecs4.default.createElement(this.TabBtn, { label: tab.label, tab: tab.id }))),
2666
2794
  /* @__PURE__ */ import_react_ecs4.default.createElement(
2667
2795
  import_react_ecs4.UiEntity,
2668
2796
  {
@@ -2682,7 +2810,7 @@ var AdminPanelUIModule = class {
2682
2810
  {
2683
2811
  uiTransform: {
2684
2812
  width: "100%",
2685
- height: 28,
2813
+ height: this.s(UI_DIMENSIONS.admin.footerHeight),
2686
2814
  justifyContent: "center",
2687
2815
  alignItems: "center"
2688
2816
  },
@@ -2691,10 +2819,10 @@ var AdminPanelUIModule = class {
2691
2819
  /* @__PURE__ */ import_react_ecs4.default.createElement(
2692
2820
  import_react_ecs4.Button,
2693
2821
  {
2694
- uiTransform: { height: 20 },
2822
+ uiTransform: { height: this.s(24) },
2695
2823
  uiBackground: { color: import_math5.Color4.create(0, 0, 0, 0) },
2696
2824
  value: `thestatic.tv/scene/${this.config.sceneId} \u2192`,
2697
- fontSize: 10,
2825
+ fontSize: t.labelSmall,
2698
2826
  color: C.cyan,
2699
2827
  onMouseDown: () => (0, import_RestrictedActions3.openExternalUrl)({ url: this.config.footerLink || `https://thestatic.tv/scene/${this.config.sceneId}` })
2700
2828
  }
@@ -2704,14 +2832,19 @@ var AdminPanelUIModule = class {
2704
2832
  );
2705
2833
  };
2706
2834
  this.client = client;
2835
+ if (!config.sceneId) {
2836
+ throw new Error("[AdminPanel] sceneId is required");
2837
+ }
2707
2838
  this.config = {
2708
2839
  showVideoTab: true,
2709
2840
  showModTab: true,
2710
2841
  title: "ADMIN PANEL",
2711
2842
  debug: false,
2712
- ...config
2843
+ ...config,
2844
+ sceneId: config.sceneId
2845
+ // Ensure sceneId is set
2713
2846
  };
2714
- this.baseUrl = client.baseUrl || "https://thestatic.tv/api/v1/dcl";
2847
+ this.baseUrl = client.getBaseUrl();
2715
2848
  if (config.headerColor) {
2716
2849
  C.header = import_math5.Color4.create(
2717
2850
  config.headerColor.r,
@@ -2726,12 +2859,21 @@ var AdminPanelUIModule = class {
2726
2859
  console.log(`[AdminPanel] ${msg}`, ...args);
2727
2860
  }
2728
2861
  }
2862
+ /** Scale a dimension by shared uiScale */
2863
+ s(value) {
2864
+ return Math.round(value * this.client.uiScale);
2865
+ }
2866
+ /** Get scaled theme */
2867
+ get theme() {
2868
+ return scaleAdminTheme(DEFAULT_ADMIN_THEME, this.client.uiScale);
2869
+ }
2729
2870
  /**
2730
- * Initialize the admin panel - checks admin status
2871
+ * Initialize the admin panel - checks admin status and fetches video state
2731
2872
  */
2732
2873
  async init() {
2733
- await new Promise((resolve) => setTimeout(resolve, 3e3));
2734
2874
  await this.checkAdminStatus();
2875
+ await this.fetchVideoState();
2876
+ this.autoPlayDefault();
2735
2877
  this.log("Initialized");
2736
2878
  }
2737
2879
  /**
@@ -2744,17 +2886,23 @@ var AdminPanelUIModule = class {
2744
2886
  return;
2745
2887
  }
2746
2888
  this.playerWallet = player.userId;
2889
+ if (this.config.forceAdmin) {
2890
+ this.isAdmin = true;
2891
+ this.isOwner = true;
2892
+ this.log("Admin status FORCED (forceAdmin: true)");
2893
+ return;
2894
+ }
2747
2895
  try {
2748
2896
  const res = await fetch(
2749
2897
  `${this.baseUrl}/scene/${this.config.sceneId}/admin-check?wallet=${player.userId}`
2750
2898
  );
2751
2899
  if (res.ok) {
2752
2900
  const data = await res.json();
2753
- this.isAdmin = data.hasAccess;
2754
- this.isOwner = data.isOwner || data.isAdmin || data.isSceneAdmin;
2901
+ this.isAdmin = data.showButton ?? data.hasAccess;
2902
+ this.isOwner = data.isOwner || data.isSceneAdmin;
2755
2903
  if (data.isBanned) {
2756
2904
  this.config.onBroadcast?.("You have been banned from this scene.");
2757
- setTimeout(() => this.banKickPlayer(), 2e3);
2905
+ this.banKickPlayer();
2758
2906
  this.log("Player is banned - kicking");
2759
2907
  }
2760
2908
  this.log("Admin status:", this.isAdmin, "Owner:", this.isOwner);
@@ -2767,13 +2915,31 @@ var AdminPanelUIModule = class {
2767
2915
  * Toggle the admin panel open/closed
2768
2916
  */
2769
2917
  toggle() {
2770
- this.panelOpen = !this.panelOpen;
2771
- if (!this.panelOpen) {
2772
- this.stopStreamPolling();
2773
- } else if (this.activeTab === "video") {
2918
+ if (this.panelOpen) {
2919
+ this.hide();
2920
+ } else {
2921
+ this.show();
2922
+ }
2923
+ }
2924
+ /**
2925
+ * Show the admin panel
2926
+ */
2927
+ show() {
2928
+ if (this.panelOpen) return;
2929
+ this.client.closeOtherPanels("admin");
2930
+ this.panelOpen = true;
2931
+ if (this.activeTab === "video") {
2774
2932
  this.startStreamPolling();
2775
2933
  }
2776
2934
  }
2935
+ /**
2936
+ * Hide the admin panel
2937
+ */
2938
+ hide() {
2939
+ if (!this.panelOpen) return;
2940
+ this.panelOpen = false;
2941
+ this.stopStreamPolling();
2942
+ }
2777
2943
  /**
2778
2944
  * Check if the panel is currently open
2779
2945
  */
@@ -2787,8 +2953,8 @@ var AdminPanelUIModule = class {
2787
2953
  return this.isAdmin;
2788
2954
  }
2789
2955
  /**
2790
- * Register a custom scene tab (Full/Custom tier)
2791
- */
2956
+ * Register a custom scene tab (Pro tier)
2957
+ */
2792
2958
  registerSceneTab(tab) {
2793
2959
  if (!this.config.sceneTabs) {
2794
2960
  this.config.sceneTabs = [];
@@ -2799,7 +2965,7 @@ var AdminPanelUIModule = class {
2799
2965
  // --- Stream Polling ---
2800
2966
  startStreamPolling() {
2801
2967
  if (this.pollIntervalId !== null) return;
2802
- this.pollIntervalId = setInterval(() => {
2968
+ this.pollIntervalId = dclSetInterval(() => {
2803
2969
  if (this.activeTab === "video" && this.panelOpen && this.streamData?.hasChannel) {
2804
2970
  this.refreshStreamStatus();
2805
2971
  }
@@ -2808,7 +2974,7 @@ var AdminPanelUIModule = class {
2808
2974
  }
2809
2975
  stopStreamPolling() {
2810
2976
  if (this.pollIntervalId !== null) {
2811
- clearInterval(this.pollIntervalId);
2977
+ dclClearInterval(this.pollIntervalId);
2812
2978
  this.pollIntervalId = null;
2813
2979
  this.log("Stream polling stopped");
2814
2980
  }
@@ -2841,6 +3007,51 @@ var AdminPanelUIModule = class {
2841
3007
  } catch (err) {
2842
3008
  }
2843
3009
  }
3010
+ // --- Video State (Slots) ---
3011
+ async fetchVideoState() {
3012
+ if (this.videoStateFetched) return;
3013
+ try {
3014
+ const res = await fetch(
3015
+ `${this.baseUrl}/scene/${this.config.sceneId}/video-state`
3016
+ );
3017
+ if (res.ok) {
3018
+ const data = await res.json();
3019
+ this.videoState = {
3020
+ defaultSlot: data.defaultSlot || null,
3021
+ videoSlots: data.videoSlots || {}
3022
+ };
3023
+ this.videoStateFetched = true;
3024
+ this.log("Video state fetched:", this.videoState.defaultSlot || "no default");
3025
+ }
3026
+ } catch (err) {
3027
+ this.log("Video state fetch error:", err);
3028
+ }
3029
+ }
3030
+ /**
3031
+ * Play a video slot by ID - looks up URL and calls onVideoPlay
3032
+ */
3033
+ playSlot(slotId) {
3034
+ const slot = this.videoState?.videoSlots?.[slotId];
3035
+ if (slot?.url) {
3036
+ this.log("Playing slot:", slotId, slot.url);
3037
+ this.config.onVideoPlay?.(slot.url);
3038
+ } else {
3039
+ this.log("Slot has no URL:", slotId);
3040
+ this.config.onVideoSlotPlay?.(slotId);
3041
+ }
3042
+ }
3043
+ /**
3044
+ * Auto-play the default slot if configured
3045
+ */
3046
+ autoPlayDefault() {
3047
+ if (!this.videoState?.defaultSlot) return;
3048
+ const defaultSlot = this.videoState.defaultSlot;
3049
+ const slot = this.videoState.videoSlots?.[defaultSlot];
3050
+ if (slot?.url) {
3051
+ this.log("Auto-playing default slot:", defaultSlot);
3052
+ this.config.onVideoPlay?.(slot.url);
3053
+ }
3054
+ }
2844
3055
  async createChannel() {
2845
3056
  if (!this.playerWallet || this.channelCreating) return;
2846
3057
  this.channelCreating = true;
@@ -2957,9 +3168,6 @@ var AdminPanelUIModule = class {
2957
3168
  this.config.onBroadcast?.("Stream started!");
2958
3169
  this.streamFetched = false;
2959
3170
  await this.fetchStreamData();
2960
- setTimeout(() => {
2961
- this.streamControlStatus = "";
2962
- }, 3e3);
2963
3171
  } else {
2964
3172
  this.streamControlStatus = "error";
2965
3173
  this.log("Start stream failed:", data.error);
@@ -2992,9 +3200,6 @@ var AdminPanelUIModule = class {
2992
3200
  this.config.onBroadcast?.("Stream stopped");
2993
3201
  this.streamFetched = false;
2994
3202
  await this.fetchStreamData();
2995
- setTimeout(() => {
2996
- this.streamControlStatus = "";
2997
- }, 3e3);
2998
3203
  } else {
2999
3204
  this.streamControlStatus = "error";
3000
3205
  this.log("Stop stream failed:", data.error);
@@ -3024,9 +3229,6 @@ var AdminPanelUIModule = class {
3024
3229
  this.keyRotateStatus = "success";
3025
3230
  this.log("Key rotated:", data.message);
3026
3231
  this.config.onBroadcast?.("Stream key rotated - update OBS settings");
3027
- setTimeout(() => {
3028
- this.keyRotateStatus = "";
3029
- }, 3e3);
3030
3232
  } else {
3031
3233
  this.keyRotateStatus = "error";
3032
3234
  this.log("Key rotation failed:", data.error);
@@ -3086,9 +3288,6 @@ var AdminPanelUIModule = class {
3086
3288
  this.sceneAdmins = [...this.sceneAdmins, normalized];
3087
3289
  this.newAdminWallet = "";
3088
3290
  this.modStatus = "saved";
3089
- setTimeout(() => {
3090
- this.modStatus = "";
3091
- }, 2e3);
3092
3291
  this.log("Added scene admin:", normalized);
3093
3292
  } else {
3094
3293
  this.modStatus = "error";
@@ -3116,9 +3315,6 @@ var AdminPanelUIModule = class {
3116
3315
  if (res.ok) {
3117
3316
  this.sceneAdmins = this.sceneAdmins.filter((w) => w !== wallet);
3118
3317
  this.modStatus = "saved";
3119
- setTimeout(() => {
3120
- this.modStatus = "";
3121
- }, 2e3);
3122
3318
  this.log("Removed scene admin:", wallet);
3123
3319
  } else {
3124
3320
  this.modStatus = "error";
@@ -3153,9 +3349,6 @@ var AdminPanelUIModule = class {
3153
3349
  this.bannedWallets = [...this.bannedWallets, normalized];
3154
3350
  this.newBanWallet = "";
3155
3351
  this.modStatus = "saved";
3156
- setTimeout(() => {
3157
- this.modStatus = "";
3158
- }, 2e3);
3159
3352
  this.log("Banned wallet:", normalized);
3160
3353
  this.config.onCommand?.("kickBanned", { wallet: normalized });
3161
3354
  } else {
@@ -3184,9 +3377,6 @@ var AdminPanelUIModule = class {
3184
3377
  if (res.ok) {
3185
3378
  this.bannedWallets = this.bannedWallets.filter((w) => w !== wallet);
3186
3379
  this.modStatus = "saved";
3187
- setTimeout(() => {
3188
- this.modStatus = "";
3189
- }, 2e3);
3190
3380
  this.log("Unbanned wallet:", wallet);
3191
3381
  } else {
3192
3382
  this.modStatus = "error";
@@ -3220,7 +3410,89 @@ var AdminPanelUIModule = class {
3220
3410
  }
3221
3411
  };
3222
3412
 
3413
+ // src/ui/ui-renderer.tsx
3414
+ var import_react_ecs5 = __toESM(require("@dcl/sdk/react-ecs"));
3415
+ var import_math6 = require("@dcl/sdk/math");
3416
+ var import_ecs2 = require("@dcl/sdk/ecs");
3417
+ var notificationText = "";
3418
+ var notificationVisible = false;
3419
+ var notificationEndTime = 0;
3420
+ var notificationInitialized = false;
3421
+ function showNotification(message, durationMs = 5e3) {
3422
+ notificationText = message;
3423
+ notificationVisible = true;
3424
+ notificationEndTime = Date.now() + durationMs;
3425
+ }
3426
+ function initNotificationSystem() {
3427
+ if (notificationInitialized) return;
3428
+ notificationInitialized = true;
3429
+ import_ecs2.engine.addSystem(() => {
3430
+ if (notificationVisible && Date.now() > notificationEndTime) {
3431
+ notificationVisible = false;
3432
+ }
3433
+ });
3434
+ }
3435
+ function NotificationBanner() {
3436
+ if (!notificationVisible) return null;
3437
+ return /* @__PURE__ */ import_react_ecs5.default.createElement(
3438
+ import_react_ecs5.UiEntity,
3439
+ {
3440
+ uiTransform: {
3441
+ positionType: "absolute",
3442
+ position: { top: 80 },
3443
+ width: 500,
3444
+ height: 60,
3445
+ padding: 16,
3446
+ alignSelf: "center",
3447
+ justifyContent: "center",
3448
+ alignItems: "center"
3449
+ },
3450
+ uiBackground: { color: import_math6.Color4.create(0.1, 0.1, 0.15, 0.95) }
3451
+ },
3452
+ /* @__PURE__ */ import_react_ecs5.default.createElement(
3453
+ import_react_ecs5.Label,
3454
+ {
3455
+ value: notificationText,
3456
+ fontSize: 18,
3457
+ color: import_math6.Color4.create(0, 1, 1, 1),
3458
+ textAlign: "middle-center"
3459
+ }
3460
+ )
3461
+ );
3462
+ }
3463
+ function createStaticUI(client) {
3464
+ initNotificationSystem();
3465
+ return function StaticUI() {
3466
+ const currentScale = client.uiScale;
3467
+ const guideComponent = client.guideUI?.getComponent() ?? null;
3468
+ const chatComponent = client.chatUI?.getComponent() ?? null;
3469
+ const adminComponent = client.adminPanel?.getComponent() ?? null;
3470
+ return /* @__PURE__ */ import_react_ecs5.default.createElement(
3471
+ import_react_ecs5.UiEntity,
3472
+ {
3473
+ key: `static-ui-root-${currentScale}`,
3474
+ uiTransform: {
3475
+ width: "100%",
3476
+ height: "100%",
3477
+ positionType: "absolute",
3478
+ flexDirection: "column",
3479
+ alignItems: "center"
3480
+ }
3481
+ },
3482
+ /* @__PURE__ */ import_react_ecs5.default.createElement(NotificationBanner, null),
3483
+ guideComponent,
3484
+ chatComponent,
3485
+ adminComponent
3486
+ );
3487
+ };
3488
+ }
3489
+ function setupStaticUI(client) {
3490
+ const StaticUI = createStaticUI(client);
3491
+ import_react_ecs5.ReactEcsRenderer.setUiRenderer(StaticUI);
3492
+ }
3493
+
3223
3494
  // src/StaticTVClient.ts
3495
+ var import_ecs3 = require("@dcl/sdk/ecs");
3224
3496
  var DEFAULT_BASE_URL = "https://thestatic.tv/api/v1/dcl";
3225
3497
  var KEY_TYPE_CHANNEL = "channel";
3226
3498
  var KEY_TYPE_SCENE = "scene";
@@ -3230,38 +3502,59 @@ var StaticTVClient = class {
3230
3502
  *
3231
3503
  * @param config Configuration options
3232
3504
  *
3233
- * @example
3505
+ * @example Free tier (session tracking only):
3234
3506
  * ```typescript
3235
- * let staticTV: StaticTVClient
3507
+ * const staticTV = new StaticTVClient({
3508
+ * apiKey: 'dcls_your_key_here'
3509
+ * })
3510
+ * ```
3511
+ *
3512
+ * @example Standard/Pro with video screen (SDK handles playback):
3513
+ * ```typescript
3514
+ * // Create your video screen
3515
+ * const screen = engine.addEntity()
3516
+ * Transform.create(screen, { position: Vector3.create(8, 3, 14), scale: Vector3.create(8, 4.5, 0.1) })
3517
+ * MeshRenderer.setPlane(screen)
3518
+ *
3519
+ * // SDK handles everything - Guide selection + Pro admin controls just work
3520
+ * const staticTV = new StaticTVClient({
3521
+ * apiKey: 'dcls_your_key_here',
3522
+ * videoScreen: screen // That's it! SDK manages video playback
3523
+ * })
3236
3524
  *
3237
3525
  * export function main() {
3238
- * // All keys use dcls_ prefix - features determined by subscription
3239
- * staticTV = new StaticTVClient({
3240
- * apiKey: 'dcls_your_key_here'
3241
- * })
3242
- * // Session tracking starts automatically!
3526
+ * staticTV.setupUI()
3243
3527
  * }
3244
3528
  * ```
3245
3529
  */
3246
3530
  constructor(config) {
3247
3531
  this._keyType = null;
3532
+ this._keyId = null;
3248
3533
  this._disabled = false;
3249
- this._fullFeaturesEnabled = false;
3534
+ this._tier = "free";
3535
+ this._standardFeaturesEnabled = false;
3250
3536
  this._proFeaturesEnabled = false;
3251
- /** Guide module - fetch channel lineup (full SDK only) */
3537
+ this._pendingProConfig = null;
3538
+ /** Guide module - fetch channel lineup (standard/pro tier) */
3252
3539
  this.guide = null;
3253
- /** Session module - track visitor sessions (all keys, null when disabled) */
3540
+ /** Session module - track visitor sessions (all tiers, null when disabled) */
3254
3541
  this.session = null;
3255
- /** Heartbeat module - track video watching (full SDK only) */
3542
+ /** Heartbeat module - track video watching (standard/pro tier) */
3256
3543
  this.heartbeat = null;
3257
- /** Interactions module - like/follow channels (full SDK only) */
3544
+ /** Interactions module - like/follow channels (standard/pro tier) */
3258
3545
  this.interactions = null;
3259
- /** Guide UI module - channel browser UI (full SDK only) */
3546
+ /** Guide UI module - channel browser UI (standard/pro tier) */
3260
3547
  this.guideUI = null;
3261
- /** Chat UI module - real-time chat UI (full SDK only) */
3548
+ /** Chat UI module - real-time chat UI (standard/pro tier) */
3262
3549
  this.chatUI = null;
3263
- /** Admin Panel module - Video/Mod tabs (pro SDK only) */
3550
+ /** Admin Panel module - Video/Mod tabs (pro tier only) */
3264
3551
  this.adminPanel = null;
3552
+ /** UI scale - fixed at 1.0. DCL's UI system auto-scales based on viewport. */
3553
+ this.uiScale = 1;
3554
+ // =============================================================================
3555
+ // --- VIDEO PLAYBACK (Internal handler for videoScreen) ---
3556
+ // =============================================================================
3557
+ this._currentVideoUrl = "";
3265
3558
  this.config = {
3266
3559
  autoStartSession: true,
3267
3560
  sessionHeartbeatInterval: 3e4,
@@ -3271,7 +3564,6 @@ var StaticTVClient = class {
3271
3564
  };
3272
3565
  this.baseUrl = config.baseUrl || DEFAULT_BASE_URL;
3273
3566
  if (!config.apiKey) {
3274
- console.log("[StaticTV] No apiKey provided - tracking disabled. Scene will load normally.");
3275
3567
  this._disabled = true;
3276
3568
  this._keyType = null;
3277
3569
  this.session = null;
@@ -3287,14 +3579,15 @@ var StaticTVClient = class {
3287
3579
  } else if (config.apiKey.startsWith("dcls_")) {
3288
3580
  this._keyType = KEY_TYPE_SCENE;
3289
3581
  } else {
3290
- console.log("[StaticTV] Invalid apiKey format (must start with dclk_ or dcls_) - tracking disabled. Scene will load normally.");
3582
+ console.warn("[TheStatic] Invalid API key format - get your key at thestatic.tv/dashboard");
3291
3583
  this._disabled = true;
3292
3584
  this._keyType = null;
3293
3585
  return;
3294
3586
  }
3295
3587
  this.session = new SessionModule(this);
3296
3588
  if (this._keyType === KEY_TYPE_CHANNEL) {
3297
- this._initFullModules();
3589
+ this._tier = "standard";
3590
+ this._initStandardModules();
3298
3591
  }
3299
3592
  if (this.config.autoStartSession) {
3300
3593
  fetchUserData().then(() => {
@@ -3310,6 +3603,10 @@ var StaticTVClient = class {
3310
3603
  }
3311
3604
  this.log(`StaticTVClient initialized (${this._keyType} mode)`);
3312
3605
  }
3606
+ /** Get the API base URL (for internal module use) */
3607
+ getBaseUrl() {
3608
+ return this.baseUrl;
3609
+ }
3313
3610
  /**
3314
3611
  * Get the key type (channel, scene, or null if disabled)
3315
3612
  */
@@ -3323,11 +3620,23 @@ var StaticTVClient = class {
3323
3620
  return this._disabled;
3324
3621
  }
3325
3622
  /**
3326
- * Check if this is a lite client (no full features)
3327
- * Returns true until session confirms sdkType is 'full'
3623
+ * Get the current SDK tier (free, standard, or pro)
3624
+ */
3625
+ get tier() {
3626
+ return this._tier;
3627
+ }
3628
+ /**
3629
+ * Check if this is a free tier client (session tracking only)
3630
+ * Returns true until session confirms a higher tier
3631
+ */
3632
+ get isFree() {
3633
+ return this._tier === "free";
3634
+ }
3635
+ /**
3636
+ * @deprecated Use `isFree` instead. Kept for backward compatibility.
3328
3637
  */
3329
3638
  get isLite() {
3330
- return !this._fullFeaturesEnabled;
3639
+ return this.isFree;
3331
3640
  }
3332
3641
  /**
3333
3642
  * Make an authenticated API request
@@ -3345,14 +3654,29 @@ var StaticTVClient = class {
3345
3654
  });
3346
3655
  }
3347
3656
  /**
3348
- * Log a message if debug is enabled
3657
+ * Log a debug message (only when debug: true)
3349
3658
  * @internal
3350
3659
  */
3351
3660
  log(message, ...args) {
3352
3661
  if (this.config.debug) {
3353
- console.log(`[StaticTV] ${message}`, ...args);
3662
+ console.log(`[TheStatic] ${message}`, ...args);
3354
3663
  }
3355
3664
  }
3665
+ /**
3666
+ * Log a warning (always shown)
3667
+ * @internal
3668
+ */
3669
+ warn(message) {
3670
+ console.warn(`[TheStatic] ${message}`);
3671
+ }
3672
+ /**
3673
+ * Log an error (always shown, user-friendly format)
3674
+ * @internal
3675
+ */
3676
+ error(message, err) {
3677
+ const errorDetail = err instanceof Error ? err.message : String(err || "");
3678
+ console.error(`[TheStatic] ${message}${errorDetail ? `: ${errorDetail}` : ""}`);
3679
+ }
3356
3680
  /**
3357
3681
  * Get the current configuration
3358
3682
  * @internal
@@ -3361,35 +3685,179 @@ var StaticTVClient = class {
3361
3685
  return this.config;
3362
3686
  }
3363
3687
  /**
3364
- * Initialize full feature modules (guide, heartbeat, interactions, UI)
3688
+ * Play a video on the configured videoScreen entity.
3689
+ * Called by Guide UI and Admin Panel.
3690
+ *
3691
+ * @param url Video URL to play
3692
+ */
3693
+ playVideo(url) {
3694
+ const screen = this.config.videoScreen;
3695
+ if (screen !== void 0) {
3696
+ this.log(`Playing video: ${url}`);
3697
+ this._currentVideoUrl = url;
3698
+ if (import_ecs3.VideoPlayer.has(screen)) {
3699
+ import_ecs3.VideoPlayer.deleteFrom(screen);
3700
+ }
3701
+ import_ecs3.VideoPlayer.create(screen, {
3702
+ src: url,
3703
+ playing: true,
3704
+ volume: 1
3705
+ });
3706
+ import_ecs3.Material.setBasicMaterial(screen, {
3707
+ texture: import_ecs3.Material.Texture.Video({ videoPlayerEntity: screen })
3708
+ });
3709
+ if (this.guideUI) {
3710
+ const videos = this.guideUI.getVideos();
3711
+ const video = videos.find((v) => v.src === url);
3712
+ if (video) {
3713
+ this.guideUI.currentVideoId = video.id;
3714
+ }
3715
+ }
3716
+ }
3717
+ if (this.config.onVideoPlay) {
3718
+ this.config.onVideoPlay(url);
3719
+ }
3720
+ }
3721
+ /**
3722
+ * Stop video playback on the configured videoScreen entity.
3723
+ * Called by Admin Panel.
3724
+ */
3725
+ stopVideo() {
3726
+ const screen = this.config.videoScreen;
3727
+ if (screen !== void 0 && import_ecs3.VideoPlayer.has(screen)) {
3728
+ this.log("Stopping video");
3729
+ import_ecs3.VideoPlayer.getMutable(screen).playing = false;
3730
+ this._currentVideoUrl = "";
3731
+ if (this.guideUI) {
3732
+ this.guideUI.currentVideoId = null;
3733
+ }
3734
+ }
3735
+ if (this.config.onVideoStop) {
3736
+ this.config.onVideoStop();
3737
+ }
3738
+ }
3739
+ /**
3740
+ * Get the currently playing video URL
3741
+ */
3742
+ get currentVideoUrl() {
3743
+ return this._currentVideoUrl;
3744
+ }
3745
+ /**
3746
+ * Internal handler for Guide video selection
3365
3747
  * @internal
3366
3748
  */
3367
- _initFullModules() {
3368
- if (this._fullFeaturesEnabled) return;
3749
+ _handleGuideVideoSelect(video) {
3750
+ if (video.src) {
3751
+ this.playVideo(video.src);
3752
+ }
3753
+ }
3754
+ /**
3755
+ * Internal handler for broadcast messages
3756
+ * @internal
3757
+ */
3758
+ _handleBroadcast(text) {
3759
+ if (this.config.onBroadcast) {
3760
+ this.config.onBroadcast(text);
3761
+ } else {
3762
+ this.showNotification(text);
3763
+ }
3764
+ }
3765
+ /**
3766
+ * Initialize standard feature modules (guide, heartbeat, interactions, UI)
3767
+ * @internal
3768
+ */
3769
+ _initStandardModules() {
3770
+ if (this._standardFeaturesEnabled) return;
3369
3771
  this.guide = new GuideModule(this);
3370
3772
  this.heartbeat = new HeartbeatModule(this);
3371
3773
  this.interactions = new InteractionsModule(this);
3372
- this.guideUI = new GuideUIModule(this, this.config.guideUI);
3774
+ const guideConfig = {
3775
+ ...this.config.guideUI,
3776
+ onVideoSelect: (video) => {
3777
+ if (this.config.videoScreen !== void 0 || this.config.onVideoPlay) {
3778
+ this._handleGuideVideoSelect(video);
3779
+ }
3780
+ if (this.config.guideUI?.onVideoSelect) {
3781
+ this.config.guideUI.onVideoSelect(video);
3782
+ }
3783
+ }
3784
+ };
3785
+ this.guideUI = new GuideUIModule(this, guideConfig);
3373
3786
  this.chatUI = new ChatUIModule(this, this.config.chatUI);
3374
- this._fullFeaturesEnabled = true;
3787
+ this._standardFeaturesEnabled = true;
3375
3788
  this.chatUI.init().catch((err) => {
3376
3789
  this.log(`Chat init failed: ${err}`);
3377
3790
  });
3378
- this.log("Full features enabled (guide, chat, heartbeat, interactions)");
3791
+ this.log("Standard features enabled (guide, chat, heartbeat, interactions)");
3792
+ }
3793
+ /**
3794
+ * Initialize pro feature modules (admin panel)
3795
+ * @internal
3796
+ */
3797
+ _initProModules() {
3798
+ if (this._proFeaturesEnabled) return;
3799
+ const sceneId = this._pendingProConfig?.sceneId || this.config.sceneId || this._keyId || void 0;
3800
+ if (!sceneId) {
3801
+ this.log("Pro features: No sceneId and no keyId available - admin panel disabled");
3802
+ return;
3803
+ }
3804
+ const adminConfig = {
3805
+ sceneId,
3806
+ // Use internal handlers that manage videoScreen + call user callbacks
3807
+ onVideoPlay: (url) => this.playVideo(url),
3808
+ onVideoStop: () => this.stopVideo(),
3809
+ onBroadcast: (text) => this._handleBroadcast(text),
3810
+ onCommand: this.config.onCommand || this._pendingProConfig?.onCommand,
3811
+ // Merge other settings from enableProFeatures if provided
3812
+ ...this._pendingProConfig
3813
+ };
3814
+ this.adminPanel = new AdminPanelUIModule(this, adminConfig);
3815
+ this._proFeaturesEnabled = true;
3816
+ this.adminPanel.init().catch((err) => {
3817
+ this.log(`Admin panel init failed: ${err}`);
3818
+ });
3819
+ this.log(`Pro features enabled (admin panel) - sceneId: ${sceneId}`);
3820
+ }
3821
+ /**
3822
+ * Called by SessionModule when server returns the tier
3823
+ * Enables modules based on tier level
3824
+ * @internal
3825
+ */
3826
+ _enableFeaturesForTier(tier, keyId) {
3827
+ this._tier = tier;
3828
+ if (keyId) {
3829
+ this._keyId = keyId;
3830
+ }
3831
+ if (tier === "standard" || tier === "pro") {
3832
+ this._initStandardModules();
3833
+ }
3834
+ if (tier === "pro") {
3835
+ const hasVideoConfig = this.config.videoScreen !== void 0 || this.config.onVideoPlay !== void 0 || this.config.sceneId !== void 0 || this._pendingProConfig !== null;
3836
+ if (hasVideoConfig) {
3837
+ this._initProModules();
3838
+ } else {
3839
+ this.log("Pro tier detected but no video config - call enableProFeatures() or set videoScreen to enable admin panel");
3840
+ }
3841
+ }
3379
3842
  }
3380
3843
  /**
3381
- * Called by SessionModule when server confirms sdkType is 'full'
3382
- * Enables guide, chat, heartbeat, and interactions modules
3844
+ * @deprecated Use `_enableFeaturesForTier` instead
3383
3845
  * @internal
3384
3846
  */
3385
3847
  _enableFullFeatures() {
3386
- this._initFullModules();
3848
+ this._enableFeaturesForTier("standard");
3387
3849
  }
3388
3850
  /**
3389
- * Check if full features are enabled (server confirmed sdkType: 'full')
3851
+ * Check if standard features are enabled (guide, chat, etc.)
3852
+ */
3853
+ get hasStandardFeatures() {
3854
+ return this._standardFeaturesEnabled;
3855
+ }
3856
+ /**
3857
+ * @deprecated Use `hasStandardFeatures` instead
3390
3858
  */
3391
3859
  get hasFullFeatures() {
3392
- return this._fullFeaturesEnabled;
3860
+ return this._standardFeaturesEnabled;
3393
3861
  }
3394
3862
  /**
3395
3863
  * Check if pro features are enabled (admin panel)
@@ -3398,64 +3866,121 @@ var StaticTVClient = class {
3398
3866
  return this._proFeaturesEnabled;
3399
3867
  }
3400
3868
  /**
3401
- * Get the SDK type (lite or full) - only available after session starts
3869
+ * @deprecated Use `tier` instead
3402
3870
  */
3403
3871
  get sdkType() {
3404
- return this.session?.sdkType || "lite";
3872
+ return this._tier === "free" ? "lite" : "full";
3405
3873
  }
3406
3874
  /**
3407
- * Enable Pro features (Admin Panel with Video + Mod tabs)
3408
- * Call this after creating the client to add admin panel functionality.
3875
+ * Configure Pro features (Admin Panel with Video + Mod tabs)
3876
+ *
3877
+ * **OPTIONAL**: If you set `videoScreen` in the constructor, you don't need
3878
+ * to call this method - the Admin Panel will auto-enable with sensible defaults.
3879
+ *
3880
+ * Use this method only if you need:
3881
+ * - Custom scene tab UI
3882
+ * - Custom panel title/styling
3883
+ * - Advanced command handlers
3409
3884
  *
3410
3885
  * @param config Admin panel configuration
3411
3886
  *
3412
- * @example
3887
+ * @example Using videoScreen (recommended - no enableProFeatures needed):
3413
3888
  * ```typescript
3414
- * const staticTV = new StaticTVClient({ apiKey: 'dcls_...' })
3889
+ * const staticTV = new StaticTVClient({
3890
+ * apiKey: 'dcls_...',
3891
+ * videoScreen: myScreen // Admin Panel auto-enabled for Pro tier!
3892
+ * })
3893
+ * ```
3415
3894
  *
3416
- * // Enable admin panel
3895
+ * @example Advanced: Custom scene tabs
3896
+ * ```typescript
3417
3897
  * staticTV.enableProFeatures({
3418
- * sceneId: 'my-scene',
3419
3898
  * title: 'MY SCENE ADMIN',
3420
- * onVideoPlay: (url) => videoPlayer.play(url),
3421
- * onVideoStop: () => videoPlayer.stop(),
3422
- * onBroadcast: (text) => showNotification(text)
3899
+ * sceneTabs: [{ label: 'LIGHTS', id: 'lights', render: () => <LightsTab /> }]
3423
3900
  * })
3424
3901
  * ```
3425
3902
  */
3426
- enableProFeatures(config) {
3903
+ enableProFeatures(config = {}) {
3427
3904
  if (this._proFeaturesEnabled) {
3428
3905
  this.log("Pro features already enabled");
3429
3906
  return;
3430
3907
  }
3431
- this.adminPanel = new AdminPanelUIModule(this, config);
3432
- this._proFeaturesEnabled = true;
3433
- this.adminPanel.init().catch((err) => {
3434
- this.log(`Admin panel init failed: ${err}`);
3435
- });
3436
- this.log("Pro features enabled (admin panel)");
3908
+ this._pendingProConfig = config;
3909
+ if (this._tier === "pro") {
3910
+ this._initProModules();
3911
+ } else {
3912
+ this.log("Pro features configured - will enable when Pro tier is confirmed");
3913
+ }
3914
+ }
3915
+ /**
3916
+ * Register a custom scene tab for the admin panel (Pro tier)
3917
+ * Must call enableProFeatures() first.
3918
+ *
3919
+ * @param tab The tab definition with label, id, and render function
3920
+ *
3921
+ * @example
3922
+ * ```typescript
3923
+ * staticTV.registerSceneTab({
3924
+ * label: 'LIGHTS',
3925
+ * id: 'lights',
3926
+ * render: () => <MyLightsControls />
3927
+ * })
3928
+ * ```
3929
+ */
3930
+ registerSceneTab(tab) {
3931
+ if (!this.adminPanel) {
3932
+ this.log("Cannot register scene tab - call enableProFeatures() first");
3933
+ return;
3934
+ }
3935
+ this.adminPanel.registerSceneTab(tab);
3437
3936
  }
3438
3937
  /**
3439
- * Register a custom scene tab for the admin panel (Full/Custom tier)
3440
- * Must call enableProFeatures() first.
3938
+ * Close Admin/Guide panels (they share the same screen space)
3939
+ * Chat is independent and stays open.
3940
+ * @param except The panel that should stay open: 'admin' | 'guide'
3941
+ */
3942
+ closeOtherPanels(except) {
3943
+ if (except !== "guide" && this.guideUI?.isVisible) {
3944
+ this.guideUI.hide();
3945
+ }
3946
+ if (except !== "admin" && this.adminPanel?.isOpen) {
3947
+ this.adminPanel.hide();
3948
+ }
3949
+ }
3950
+ /**
3951
+ * Set up the UI renderer for all SDK panels
3952
+ * Call this in your scene's main() function to render Guide, Chat, Admin panels.
3953
+ * No need to create your own ui.tsx - the SDK handles everything.
3954
+ *
3955
+ * @example
3956
+ * ```typescript
3957
+ * const staticTV = new StaticTVClient({ apiKey: 'dcls_...' })
3958
+ *
3959
+ * export function main() {
3960
+ * staticTV.setupUI()
3961
+ * // That's it! All panels will render automatically
3962
+ * }
3963
+ * ```
3964
+ */
3965
+ setupUI() {
3966
+ setupStaticUI(this);
3967
+ this.log("UI renderer initialized");
3968
+ }
3969
+ /**
3970
+ * Show a notification message on screen
3971
+ * Works with both SDK-rendered UI and custom UI setups.
3441
3972
  *
3442
- * @param tab The tab definition with label, id, and render function
3973
+ * @param message The message to display
3974
+ * @param durationMs How long to show (default 5000ms)
3443
3975
  *
3444
3976
  * @example
3445
3977
  * ```typescript
3446
- * staticTV.registerSceneTab({
3447
- * label: 'LIGHTS',
3448
- * id: 'lights',
3449
- * render: () => <MyLightsControls />
3450
- * })
3978
+ * staticTV.showNotification('Stream started!')
3979
+ * staticTV.showNotification('Custom message', 10000) // 10 seconds
3451
3980
  * ```
3452
3981
  */
3453
- registerSceneTab(tab) {
3454
- if (!this.adminPanel) {
3455
- this.log("Cannot register scene tab - call enableProFeatures() first");
3456
- return;
3457
- }
3458
- this.adminPanel.registerSceneTab(tab);
3982
+ showNotification(message, durationMs = 5e3) {
3983
+ showNotification(message, durationMs);
3459
3984
  }
3460
3985
  /**
3461
3986
  * Cleanup when done (call before scene unload)
@@ -3491,5 +4016,7 @@ var StaticTVClient = class {
3491
4016
  StaticTVClient,
3492
4017
  fetchUserData,
3493
4018
  getPlayerDisplayName,
3494
- getPlayerWallet
4019
+ getPlayerWallet,
4020
+ setupStaticUI,
4021
+ showNotification
3495
4022
  });