@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.mjs CHANGED
@@ -132,7 +132,7 @@ function ensureTimerSystem() {
132
132
  try {
133
133
  timer.callback();
134
134
  } catch (e) {
135
- console.error("[StaticTV Timer] Callback error:", e);
135
+ console.error("[TheStatic] Timer error");
136
136
  }
137
137
  }
138
138
  }
@@ -173,7 +173,7 @@ function ensureTimeoutSystem() {
173
173
  try {
174
174
  timeout.callback();
175
175
  } catch (e) {
176
- console.error("[StaticTV Timer] Timeout callback error:", e);
176
+ console.error("[TheStatic] Timeout error");
177
177
  }
178
178
  }
179
179
  }
@@ -204,20 +204,41 @@ function dclClearTimeout(timeoutId) {
204
204
  }
205
205
 
206
206
  // src/modules/session.ts
207
+ function normalizeTier(tier) {
208
+ if (tier === "lite") return "free";
209
+ if (tier === "full") return "standard";
210
+ if (tier === "free" || tier === "standard" || tier === "pro") return tier;
211
+ return "free";
212
+ }
207
213
  var SessionModule = class {
208
214
  constructor(client) {
209
215
  this.sessionId = null;
216
+ this._keyId = null;
210
217
  this.heartbeatTimerId = null;
211
218
  this.isActive = false;
212
- this._sdkType = "lite";
219
+ this._tier = "free";
213
220
  this.client = client;
214
221
  }
215
222
  /**
216
- * Get the SDK type returned by the server
217
- * Determines what features are available (lite = sessions only, full = guide + chat + more)
223
+ * Get the API key ID (used as default sceneId for Pro users)
224
+ */
225
+ get keyId() {
226
+ return this._keyId;
227
+ }
228
+ /**
229
+ * Get the SDK tier returned by the server
230
+ * - free: Session tracking only
231
+ * - standard: Guide, Chat, Heartbeat, Interactions
232
+ * - pro: Everything + Admin Panel
233
+ */
234
+ get tier() {
235
+ return this._tier;
236
+ }
237
+ /**
238
+ * @deprecated Use `tier` instead. Returns mapped value for compatibility.
218
239
  */
219
240
  get sdkType() {
220
- return this._sdkType;
241
+ return this._tier === "free" ? "lite" : "full";
221
242
  }
222
243
  /**
223
244
  * Get the appropriate session endpoint based on key type
@@ -264,13 +285,12 @@ var SessionModule = class {
264
285
  });
265
286
  if (response.success && response.sessionId) {
266
287
  this.sessionId = response.sessionId;
288
+ this._keyId = response.keyId || null;
267
289
  this.isActive = true;
268
- this._sdkType = response.sdkType || "lite";
290
+ this._tier = normalizeTier(response.sdkType);
269
291
  this.startHeartbeat();
270
- this.client.log(`Session started: ${this.sessionId}, sdkType: ${this._sdkType}`);
271
- if (this._sdkType === "full") {
272
- this.client._enableFullFeatures();
273
- }
292
+ this.client.log(`Session started: ${this.sessionId}, tier: ${this._tier}`);
293
+ this.client._enableFeaturesForTier(this._tier, this._keyId);
274
294
  return this.sessionId;
275
295
  }
276
296
  return null;
@@ -558,7 +578,8 @@ var UI_DIMENSIONS = {
558
578
  // Guide UI - positioned to the left of chat (chat is 380px wide at right:20)
559
579
  guide: {
560
580
  width: 900,
561
- height: "55%",
581
+ height: 580,
582
+ // Numeric value for scaling (matches chat/admin)
562
583
  bottom: 55,
563
584
  right: 410,
564
585
  // 20 + 380 (chat width) + 10 (gap)
@@ -577,15 +598,33 @@ var UI_DIMENSIONS = {
577
598
  padding: 15
578
599
  }
579
600
  },
580
- // Chat UI - match m1d-hq-lifted dimensions
601
+ // Chat UI - positioned at right side
581
602
  chat: {
582
603
  width: 380,
583
604
  height: 580,
584
605
  bottom: 55,
585
606
  right: 20,
607
+ headerHeight: 40,
586
608
  messagesPerPage: 5,
587
609
  channelsPerPage: 6
588
610
  },
611
+ // Admin Panel - positioned left of chat
612
+ admin: {
613
+ width: 400,
614
+ height: 580,
615
+ // Match chat height
616
+ maxHeight: 700,
617
+ bottom: 55,
618
+ right: 410,
619
+ // 20 + 380 (chat width) + 10 (gap)
620
+ headerHeight: 48,
621
+ tabHeight: 40,
622
+ footerHeight: 32,
623
+ sectionHeadHeight: 28,
624
+ buttonHeight: 36,
625
+ buttonHeightSmall: 30,
626
+ inputHeight: 36
627
+ },
589
628
  // Shared
590
629
  closeButton: {
591
630
  size: 40,
@@ -593,18 +632,18 @@ var UI_DIMENSIONS = {
593
632
  }
594
633
  };
595
634
  var DEFAULT_CHAT_THEME = {
596
- header: 22,
635
+ header: 16,
597
636
  channelButton: 14,
598
637
  channelDropdown: 14,
599
- systemMessage: 14,
600
- chatUsername: 16,
638
+ systemMessage: 13,
639
+ chatUsername: 14,
601
640
  chatTimestamp: 11,
602
- chatMessage: 16,
603
- input: 16,
641
+ chatMessage: 14,
642
+ input: 14,
604
643
  sendButton: 14,
605
- userInfo: 14,
644
+ userInfo: 13,
606
645
  authStatus: 12,
607
- notification: 18,
646
+ notification: 16,
608
647
  closeButton: 16
609
648
  };
610
649
  function scaleChatTheme(theme, fontScale) {
@@ -624,6 +663,30 @@ function scaleChatTheme(theme, fontScale) {
624
663
  closeButton: Math.round(theme.closeButton * fontScale)
625
664
  };
626
665
  }
666
+ var DEFAULT_ADMIN_THEME = {
667
+ header: 16,
668
+ tabButton: 14,
669
+ sectionHead: 13,
670
+ label: 13,
671
+ labelSmall: 11,
672
+ button: 13,
673
+ buttonSmall: 11,
674
+ input: 13,
675
+ status: 12
676
+ };
677
+ function scaleAdminTheme(theme, fontScale) {
678
+ return {
679
+ header: Math.round(theme.header * fontScale),
680
+ tabButton: Math.round(theme.tabButton * fontScale),
681
+ sectionHead: Math.round(theme.sectionHead * fontScale),
682
+ label: Math.round(theme.label * fontScale),
683
+ labelSmall: Math.round(theme.labelSmall * fontScale),
684
+ button: Math.round(theme.button * fontScale),
685
+ buttonSmall: Math.round(theme.buttonSmall * fontScale),
686
+ input: Math.round(theme.input * fontScale),
687
+ status: Math.round(theme.status * fontScale)
688
+ };
689
+ }
627
690
 
628
691
  // src/ui/components.tsx
629
692
  import ReactEcs, { UiEntity, Label, Input } from "@dcl/sdk/react-ecs";
@@ -679,21 +742,21 @@ var PanelHeader = (props) => {
679
742
  }),
680
743
  ReactEcs.createElement(UiEntity, { key: "pos-spacer", uiTransform: { width: 6 } })
681
744
  ] : [],
682
- // Font controls
745
+ // Font controls (with visual disabled state at limits)
683
746
  ...props.showFontControls ? [
684
747
  ReactEcs.createElement(UiEntity, {
685
748
  key: "font-down",
686
749
  uiTransform: { width: 22, height: 22, justifyContent: "center", alignItems: "center" },
687
- uiBackground: { color: THEME.colors.buttonBackground },
688
- onMouseDown: () => props.onFontScaleDown?.(),
689
- children: [ReactEcs.createElement(UiEntity, { uiText: { value: "\u2212", fontSize: 14, color: THEME.colors.cyan } })]
750
+ uiBackground: { color: props.fontScaleAtMin ? Color42.create(0.1, 0.1, 0.1, 0.3) : THEME.colors.buttonBackground },
751
+ onMouseDown: () => !props.fontScaleAtMin && props.onFontScaleDown?.(),
752
+ children: [ReactEcs.createElement(UiEntity, { uiText: { value: "\u2212", fontSize: 14, color: props.fontScaleAtMin ? THEME.colors.gray : THEME.colors.cyan } })]
690
753
  }),
691
754
  ReactEcs.createElement(UiEntity, {
692
755
  key: "font-up",
693
756
  uiTransform: { width: 22, height: 22, justifyContent: "center", alignItems: "center" },
694
- uiBackground: { color: THEME.colors.buttonBackground },
695
- onMouseDown: () => props.onFontScaleUp?.(),
696
- children: [ReactEcs.createElement(UiEntity, { uiText: { value: "+", fontSize: 14, color: THEME.colors.cyan } })]
757
+ uiBackground: { color: props.fontScaleAtMax ? Color42.create(0.1, 0.1, 0.1, 0.3) : THEME.colors.buttonBackground },
758
+ onMouseDown: () => !props.fontScaleAtMax && props.onFontScaleUp?.(),
759
+ children: [ReactEcs.createElement(UiEntity, { uiText: { value: "+", fontSize: 14, color: props.fontScaleAtMax ? THEME.colors.gray : THEME.colors.cyan } })]
697
760
  }),
698
761
  ReactEcs.createElement(UiEntity, { key: "font-spacer", uiTransform: { width: 6 } })
699
762
  ] : [],
@@ -731,7 +794,6 @@ var GuideUIModule = class {
731
794
  this.currentPage = 0;
732
795
  this.itemsPerPage = 6;
733
796
  this.searchQuery = "";
734
- this.uiScale = 1;
735
797
  // Current video tracking (for "PLAYING" indicator)
736
798
  this._currentVideoId = null;
737
799
  // =============================================================================
@@ -739,34 +801,46 @@ var GuideUIModule = class {
739
801
  // =============================================================================
740
802
  /**
741
803
  * Get the guide UI component for rendering
742
- * Returns toggle button when hidden, full panel when visible
804
+ * Always renders toggle button, plus panel when visible
743
805
  */
744
806
  this.getComponent = () => {
745
- if (!this._isVisible) {
746
- return this.renderToggleButton();
747
- }
748
- const windowW = this.s(UI_DIMENSIONS.guide.width);
807
+ const windowW = UI_DIMENSIONS.guide.width;
808
+ const windowH = UI_DIMENSIONS.guide.height;
749
809
  return ReactEcs2.createElement(UiEntity2, {
810
+ key: `guide-root-${this.client.uiScale}`,
750
811
  uiTransform: {
751
- width: windowW,
752
- height: UI_DIMENSIONS.guide.height,
753
- positionType: "absolute",
754
- position: { bottom: UI_DIMENSIONS.guide.bottom, right: UI_DIMENSIONS.guide.right },
755
- flexDirection: "row",
756
- border: { top: 2, bottom: 2, left: 2, right: 2 },
757
- borderColor: THEME.colors.panelBorder
812
+ width: "100%",
813
+ height: "100%",
814
+ positionType: "absolute"
758
815
  },
759
- uiBackground: { color: THEME.colors.panel },
760
816
  children: [
761
- this.renderLeftPanel(),
762
- this.renderRightPanel(),
763
- this.renderCloseButton()
817
+ // Always render toggle button
818
+ this.renderToggleButton(),
819
+ // Render panel when visible - Guide positioned relative to Chat's scaled width
820
+ // Guide.right = Chat.right + Chat.scaledWidth + gap (10px)
821
+ this._isVisible ? ReactEcs2.createElement(UiEntity2, {
822
+ key: `guide-panel-${this.client.uiScale}`,
823
+ uiTransform: {
824
+ width: windowW,
825
+ height: windowH,
826
+ positionType: "absolute",
827
+ position: { bottom: UI_DIMENSIONS.guide.bottom, right: UI_DIMENSIONS.chat.right + this.s(UI_DIMENSIONS.chat.width) + 10 },
828
+ flexDirection: "row",
829
+ border: { top: 2, bottom: 2, left: 2, right: 2 },
830
+ borderColor: THEME.colors.panelBorder
831
+ },
832
+ uiBackground: { color: THEME.colors.panel },
833
+ children: [
834
+ this.renderLeftPanel(),
835
+ this.renderRightPanel(),
836
+ this.renderCloseButton()
837
+ ]
838
+ }) : null
764
839
  ]
765
840
  });
766
841
  };
767
842
  this.client = client;
768
843
  this.config = config;
769
- this.uiScale = config.uiScale || 1;
770
844
  this._currentVideoId = config.currentVideoId || null;
771
845
  }
772
846
  /**
@@ -780,6 +854,8 @@ var GuideUIModule = class {
780
854
  * Show the guide UI
781
855
  */
782
856
  show() {
857
+ if (this._isVisible) return;
858
+ this.client.closeOtherPanels("guide");
783
859
  this._isVisible = true;
784
860
  this.fetchGuideData().catch(() => {
785
861
  });
@@ -915,7 +991,7 @@ var GuideUIModule = class {
915
991
  // --- UTILITIES ---
916
992
  // =============================================================================
917
993
  s(value) {
918
- return Math.round(value * this.uiScale);
994
+ return Math.round(value * this.client.uiScale);
919
995
  }
920
996
  handleVideoSelect(video) {
921
997
  if (this.config.onVideoSelect) {
@@ -930,7 +1006,7 @@ var GuideUIModule = class {
930
1006
  renderLeftPanel() {
931
1007
  const sidebarW = this.s(UI_DIMENSIONS.guide.sidebar.width);
932
1008
  const liveCount = this.liveVideos.length;
933
- const signalCount = this.videos.filter((v) => !v.isLive).length;
1009
+ const signalCount = this.videos.filter((v) => !v.isLive && !v.isCreator).length;
934
1010
  const nodesPlaylist = this.featuredPlaylists.find((p) => p.name === "NODES");
935
1011
  const nodesCount = nodesPlaylist ? nodesPlaylist.videos.filter((v) => v.isCreator).length : 0;
936
1012
  const displayedPlaylists = this.featuredPlaylists.filter((p) => p.name !== "NODES").slice(0, 6);
@@ -951,15 +1027,26 @@ var GuideUIModule = class {
951
1027
  ReactEcs2.createElement(UiEntity2, {
952
1028
  uiTransform: { width: "100%", flexDirection: "column", padding: 10 },
953
1029
  children: [
954
- // Title
1030
+ // Title row
955
1031
  ReactEcs2.createElement(UiEntity2, {
956
- uiText: {
957
- value: "THE STATIC TV",
958
- fontSize: this.s(UI_DIMENSIONS.guide.sidebar.headerSize),
959
- color: THEME.colors.cyan,
960
- textAlign: "middle-center"
1032
+ uiTransform: {
1033
+ width: "100%",
1034
+ height: this.s(40),
1035
+ marginBottom: 10,
1036
+ flexDirection: "row",
1037
+ alignItems: "center"
961
1038
  },
962
- uiTransform: { height: this.s(40), marginBottom: 10 }
1039
+ children: [
1040
+ // Title
1041
+ ReactEcs2.createElement(UiEntity2, {
1042
+ uiText: {
1043
+ value: "THE STATIC TV",
1044
+ fontSize: this.s(UI_DIMENSIONS.guide.sidebar.headerSize),
1045
+ color: THEME.colors.cyan,
1046
+ textAlign: "middle-left"
1047
+ }
1048
+ })
1049
+ ]
963
1050
  }),
964
1051
  // Random Signal button
965
1052
  ReactEcs2.createElement(UiEntity2, {
@@ -1016,7 +1103,7 @@ var GuideUIModule = class {
1016
1103
  }),
1017
1104
  // Menu buttons
1018
1105
  this.renderMenuButton("LIVE NOW", liveCount, THEME.colors.red, liveCount > 0 ? "\u25CF" : void 0, "ACTIVE STREAMS"),
1019
- this.renderMenuButton("SIGNALS", signalCount, THEME.colors.white, void 0, "ALL VIDEOS"),
1106
+ this.renderMenuButton("SIGNALS", signalCount, THEME.colors.white, void 0, "VODS & RECORDINGS"),
1020
1107
  this.renderMenuButton("NODES", nodesCount, THEME.colors.magenta, void 0, "CHANNELS & CREATORS"),
1021
1108
  // Divider
1022
1109
  ReactEcs2.createElement(UiEntity2, {
@@ -1098,7 +1185,7 @@ var GuideUIModule = class {
1098
1185
  let videosToShow = [];
1099
1186
  const nodePl = this.featuredPlaylists.find((p) => p.name === "NODES");
1100
1187
  if (this.activeTab === "SIGNALS") {
1101
- videosToShow = this.videos.filter((v) => !v.isLive);
1188
+ videosToShow = this.videos.filter((v) => !v.isLive && !v.isCreator);
1102
1189
  } else if (this.activeTab === "NODES") {
1103
1190
  videosToShow = nodePl ? nodePl.videos.filter((v) => v.isCreator) : [];
1104
1191
  } else if (this.activeTab === "LIVE NOW") {
@@ -1309,23 +1396,25 @@ var GuideUIModule = class {
1309
1396
  });
1310
1397
  }
1311
1398
  renderToggleButton() {
1399
+ const buttonText = this._isVisible ? "CLOSE" : "GUIDE";
1400
+ const buttonColor = this._isVisible ? Color43.create(0.2, 0.2, 0.28, 0.9) : Color43.create(0, 0.5, 0.5, 0.9);
1401
+ const buttonPos = 20 + this.s(100) + 10;
1312
1402
  return ReactEcs2.createElement(UiEntity2, {
1313
1403
  uiTransform: {
1314
1404
  positionType: "absolute",
1315
- position: { right: 130, bottom: 10 },
1316
- // To the left of CHAT button
1405
+ position: { right: buttonPos, bottom: 10 },
1317
1406
  width: this.s(100),
1318
1407
  height: this.s(45),
1319
1408
  justifyContent: "center",
1320
1409
  alignItems: "center"
1321
1410
  },
1322
- uiBackground: { color: Color43.create(0, 0.5, 0.5, 0.9) },
1323
- onMouseDown: () => this.show(),
1411
+ uiBackground: { color: buttonColor },
1412
+ onMouseDown: () => this._isVisible ? this.hide() : this.show(),
1324
1413
  children: [
1325
1414
  ReactEcs2.createElement(UiEntity2, {
1326
1415
  uiText: {
1327
- value: "GUIDE",
1328
- fontSize: this.s(16),
1416
+ value: buttonText,
1417
+ fontSize: this.s(14),
1329
1418
  color: THEME.colors.white,
1330
1419
  textAlign: "middle-center"
1331
1420
  }
@@ -1367,7 +1456,6 @@ var ChatUIModule = class {
1367
1456
  this.chatScrollOffset = 0;
1368
1457
  // UI preferences
1369
1458
  this.position = "right";
1370
- this.fontScale = 1;
1371
1459
  // Timers
1372
1460
  this.chatTimerId = null;
1373
1461
  this.playerInfoTimerId = null;
@@ -1377,43 +1465,52 @@ var ChatUIModule = class {
1377
1465
  // =============================================================================
1378
1466
  /**
1379
1467
  * Get the chat UI component for rendering
1380
- * Returns toggle button when hidden, full panel when visible
1468
+ * Always renders toggle button, plus panel when visible
1381
1469
  */
1382
1470
  this.getComponent = () => {
1383
- if (!this._isVisible) {
1384
- return this.renderToggleButton();
1385
- }
1386
- const scaledTheme = scaleChatTheme(DEFAULT_CHAT_THEME, this.fontScale);
1471
+ const scaledTheme = scaleChatTheme(DEFAULT_CHAT_THEME, this.client.uiScale);
1387
1472
  const positionStyle = this.getPositionStyle();
1388
1473
  return ReactEcs3.createElement(UiEntity3, {
1389
1474
  uiTransform: {
1390
- width: UI_DIMENSIONS.chat.width,
1391
- height: UI_DIMENSIONS.chat.height,
1392
- positionType: "absolute",
1393
- position: positionStyle,
1394
- // Must be nested object, not spread!
1395
- flexDirection: "column",
1396
- border: { top: 2, bottom: 2, left: 2, right: 2 },
1397
- borderColor: THEME.colors.panelBorder
1475
+ width: "100%",
1476
+ height: "100%",
1477
+ positionType: "absolute"
1398
1478
  },
1399
- uiBackground: { color: THEME.colors.panel },
1400
1479
  children: [
1401
- this.renderHeader(),
1402
- this.renderChannelButton(scaledTheme),
1403
- this.renderMessagesArea(scaledTheme),
1404
- ReactEcs3.createElement(UiEntity3, {
1405
- uiTransform: { width: "100%", height: 1, flexShrink: 0 },
1406
- uiBackground: { color: THEME.colors.panelBorder }
1407
- }),
1408
- this.renderUserInfoBar(scaledTheme),
1409
- this.renderInputArea(scaledTheme),
1410
- this.renderChannelDropdown(scaledTheme)
1480
+ // Always render toggle button
1481
+ this.renderToggleButton(),
1482
+ // Render panel when visible - scaled sizes, fixed position
1483
+ // Key includes uiScale to force re-render when scale changes from other panels
1484
+ this._isVisible ? ReactEcs3.createElement(UiEntity3, {
1485
+ key: `chat-panel-${this.client.uiScale}`,
1486
+ uiTransform: {
1487
+ width: this.s(UI_DIMENSIONS.chat.width),
1488
+ height: this.s(UI_DIMENSIONS.chat.height),
1489
+ positionType: "absolute",
1490
+ position: { bottom: UI_DIMENSIONS.chat.bottom, right: UI_DIMENSIONS.chat.right },
1491
+ flexDirection: "column",
1492
+ border: { top: 2, bottom: 2, left: 2, right: 2 },
1493
+ borderColor: THEME.colors.panelBorder
1494
+ },
1495
+ uiBackground: { color: THEME.colors.panel },
1496
+ children: [
1497
+ this.renderHeader(),
1498
+ this.renderChannelButton(scaledTheme),
1499
+ this.renderMessagesArea(scaledTheme),
1500
+ ReactEcs3.createElement(UiEntity3, {
1501
+ uiTransform: { width: "100%", height: 1, flexShrink: 0 },
1502
+ uiBackground: { color: THEME.colors.panelBorder }
1503
+ }),
1504
+ this.renderUserInfoBar(scaledTheme),
1505
+ this.renderInputArea(scaledTheme),
1506
+ this.renderChannelDropdown(scaledTheme)
1507
+ ]
1508
+ }) : null
1411
1509
  ]
1412
1510
  });
1413
1511
  };
1414
1512
  this.client = client;
1415
1513
  this.config = config;
1416
- this.fontScale = config.fontScale || 1;
1417
1514
  }
1418
1515
  /**
1419
1516
  * Initialize the chat system
@@ -1432,6 +1529,7 @@ var ChatUIModule = class {
1432
1529
  * Show the chat UI
1433
1530
  */
1434
1531
  show() {
1532
+ if (this._isVisible) return;
1435
1533
  this._isVisible = true;
1436
1534
  this._unreadCount = 0;
1437
1535
  this.chatScrollOffset = 0;
@@ -1693,6 +1791,10 @@ var ChatUIModule = class {
1693
1791
  const d = new Date(input);
1694
1792
  if (!isNaN(d.getTime())) return d.getTime();
1695
1793
  }
1794
+ if (input instanceof Date) return input.getTime();
1795
+ if (typeof input === "object" && "seconds" in input) {
1796
+ return input.seconds * 1e3;
1797
+ }
1696
1798
  return 0;
1697
1799
  }
1698
1800
  formatTime(isoString) {
@@ -1705,6 +1807,10 @@ var ChatUIModule = class {
1705
1807
  return "";
1706
1808
  }
1707
1809
  }
1810
+ /** Scale a dimension by shared uiScale */
1811
+ s(value) {
1812
+ return Math.round(value * this.client.uiScale);
1813
+ }
1708
1814
  getPositionStyle() {
1709
1815
  return { bottom: 55, right: 20 };
1710
1816
  }
@@ -1712,15 +1818,9 @@ var ChatUIModule = class {
1712
1818
  return PanelHeader({
1713
1819
  title: "LIVE CHAT",
1714
1820
  fontSize: 14,
1715
- fontScale: this.fontScale,
1821
+ fontScale: 1,
1716
1822
  showPositionControls: false,
1717
- showFontControls: true,
1718
- onFontScaleUp: () => {
1719
- this.fontScale = Math.min(1.4, this.fontScale + 0.1);
1720
- },
1721
- onFontScaleDown: () => {
1722
- this.fontScale = Math.max(0.7, this.fontScale - 0.1);
1723
- },
1823
+ showFontControls: false,
1724
1824
  onClose: () => this.hide()
1725
1825
  });
1726
1826
  }
@@ -2095,23 +2195,24 @@ var ChatUIModule = class {
2095
2195
  }
2096
2196
  renderToggleButton() {
2097
2197
  const unreadBadge = this._unreadCount > 0 ? ` (${this._unreadCount})` : "";
2198
+ const buttonText = this._isVisible ? "CLOSE" : `CHAT${unreadBadge}`;
2199
+ const buttonColor = this._isVisible ? Color44.create(0.2, 0.2, 0.28, 0.9) : Color44.create(0.6, 0, 0.5, 0.9);
2098
2200
  return ReactEcs3.createElement(UiEntity3, {
2099
2201
  uiTransform: {
2100
2202
  positionType: "absolute",
2101
2203
  position: { right: 20, bottom: 10 },
2102
- // Far right corner
2103
- width: 100,
2104
- height: 45,
2204
+ width: this.s(100),
2205
+ height: this.s(45),
2105
2206
  justifyContent: "center",
2106
2207
  alignItems: "center"
2107
2208
  },
2108
- uiBackground: { color: Color44.create(0.6, 0, 0.5, 0.9) },
2109
- onMouseDown: () => this.show(),
2209
+ uiBackground: { color: buttonColor },
2210
+ onMouseDown: () => this._isVisible ? this.hide() : this.show(),
2110
2211
  children: [
2111
2212
  ReactEcs3.createElement(UiEntity3, {
2112
2213
  uiText: {
2113
- value: `CHAT${unreadBadge}`,
2114
- fontSize: 16,
2214
+ value: buttonText,
2215
+ fontSize: this.s(14),
2115
2216
  color: THEME.colors.white,
2116
2217
  textAlign: "middle-center"
2117
2218
  }
@@ -2145,6 +2246,7 @@ var C = {
2145
2246
  textDim: Color45.create(0.6, 0.6, 0.7, 1)
2146
2247
  };
2147
2248
  var AdminPanelUIModule = class {
2249
+ // UI scaling - uses shared client.uiScale
2148
2250
  constructor(client, config) {
2149
2251
  // State
2150
2252
  this.isAdmin = false;
@@ -2156,6 +2258,8 @@ var AdminPanelUIModule = class {
2156
2258
  this.customVideoUrl = "";
2157
2259
  this.streamData = null;
2158
2260
  this.streamFetched = false;
2261
+ this.videoState = null;
2262
+ this.videoStateFetched = false;
2159
2263
  this.channelCreating = false;
2160
2264
  this.channelCreateError = "";
2161
2265
  this.channelDeleting = false;
@@ -2179,18 +2283,18 @@ var AdminPanelUIModule = class {
2179
2283
  this.SectionHead = ({ label, color }) => /* @__PURE__ */ ReactEcs4.createElement(
2180
2284
  UiEntity4,
2181
2285
  {
2182
- uiTransform: { width: "100%", height: 24, margin: { bottom: 6 }, padding: { left: 8 }, alignItems: "center" },
2286
+ uiTransform: { width: "100%", height: this.s(UI_DIMENSIONS.admin.sectionHeadHeight), margin: { bottom: 8 }, padding: { left: 10 }, alignItems: "center" },
2183
2287
  uiBackground: { color: Color45.create(color.r * 0.3, color.g * 0.3, color.b * 0.3, 0.5) }
2184
2288
  },
2185
- /* @__PURE__ */ ReactEcs4.createElement(Label4, { value: label, fontSize: 12, color })
2289
+ /* @__PURE__ */ ReactEcs4.createElement(Label4, { value: label, fontSize: this.theme.sectionHead, color })
2186
2290
  );
2187
2291
  this.TabBtn = ({ label, tab }) => /* @__PURE__ */ ReactEcs4.createElement(
2188
2292
  Button3,
2189
2293
  {
2190
- uiTransform: { flexGrow: 1, height: 36, justifyContent: "center", alignItems: "center" },
2294
+ uiTransform: { flexGrow: 1, height: this.s(UI_DIMENSIONS.admin.tabHeight), justifyContent: "center", alignItems: "center" },
2191
2295
  uiBackground: { color: this.activeTab === tab ? C.tabActive : C.tabInactive },
2192
2296
  value: label,
2193
- fontSize: 12,
2297
+ fontSize: this.theme.tabButton,
2194
2298
  color: this.activeTab === tab ? Color45.Black() : C.text,
2195
2299
  textAlign: "middle-center",
2196
2300
  onMouseDown: () => this.setActiveTab(tab)
@@ -2200,134 +2304,135 @@ var AdminPanelUIModule = class {
2200
2304
  if (!this.streamFetched) {
2201
2305
  this.fetchStreamData();
2202
2306
  }
2203
- return /* @__PURE__ */ ReactEcs4.createElement(UiEntity4, { uiTransform: { flexDirection: "column", width: "100%", padding: 8 } }, !this.streamData?.hasChannel && /* @__PURE__ */ ReactEcs4.createElement(UiEntity4, { uiTransform: { flexDirection: "column", margin: { bottom: 12 } } }, /* @__PURE__ */ ReactEcs4.createElement(this.SectionHead, { label: "LIVE STREAM", color: C.btn }), /* @__PURE__ */ ReactEcs4.createElement(Label4, { value: "No streaming channel linked", fontSize: 10, color: C.textDim, uiTransform: { margin: { bottom: 6 } } }), this.isOwner && /* @__PURE__ */ ReactEcs4.createElement(UiEntity4, { uiTransform: { flexDirection: "column" } }, this.streamData?.trialAvailable && /* @__PURE__ */ ReactEcs4.createElement(UiEntity4, { uiTransform: { flexDirection: "column", margin: { bottom: 8 } } }, /* @__PURE__ */ ReactEcs4.createElement(
2307
+ const t = this.theme;
2308
+ return /* @__PURE__ */ ReactEcs4.createElement(UiEntity4, { uiTransform: { flexDirection: "column", width: "100%", padding: 10 } }, !this.streamData?.hasChannel && /* @__PURE__ */ ReactEcs4.createElement(UiEntity4, { uiTransform: { flexDirection: "column", margin: { bottom: 14 } } }, /* @__PURE__ */ ReactEcs4.createElement(this.SectionHead, { label: "LIVE STREAM", color: C.btn }), /* @__PURE__ */ ReactEcs4.createElement(Label4, { value: "No streaming channel linked", fontSize: t.labelSmall, color: C.textDim, uiTransform: { margin: { bottom: 8 } } }), this.isOwner && /* @__PURE__ */ ReactEcs4.createElement(UiEntity4, { uiTransform: { flexDirection: "column" } }, this.streamData?.trialAvailable && /* @__PURE__ */ ReactEcs4.createElement(UiEntity4, { uiTransform: { flexDirection: "column", margin: { bottom: 10 } } }, /* @__PURE__ */ ReactEcs4.createElement(
2204
2309
  Button3,
2205
2310
  {
2206
- uiTransform: { width: 180, height: 40, margin: { bottom: 4 } },
2311
+ uiTransform: { width: this.s(200), height: this.s(UI_DIMENSIONS.admin.buttonHeight), margin: { bottom: 6 } },
2207
2312
  uiBackground: { color: this.trialClaiming ? C.btn : C.green },
2208
- value: this.trialClaiming ? "Claiming..." : "\u{1F381} Start Free 4-Hour Trial",
2209
- fontSize: 11,
2313
+ value: this.trialClaiming ? "Claiming..." : "Start Free 4-Hour Trial",
2314
+ fontSize: t.button,
2210
2315
  color: C.text,
2211
2316
  onMouseDown: () => this.claimTrial()
2212
2317
  }
2213
- ), this.trialClaimError && /* @__PURE__ */ ReactEcs4.createElement(Label4, { value: this.trialClaimError, fontSize: 9, color: C.red }), /* @__PURE__ */ ReactEcs4.createElement(Label4, { value: "One-time trial \u2022 4 hours of streaming", fontSize: 8, color: C.textDim, uiTransform: { margin: { top: 2 } } })), !this.streamData?.trialAvailable && /* @__PURE__ */ ReactEcs4.createElement(UiEntity4, { uiTransform: { flexDirection: "column" } }, /* @__PURE__ */ ReactEcs4.createElement(
2318
+ ), this.trialClaimError && /* @__PURE__ */ ReactEcs4.createElement(Label4, { value: this.trialClaimError, fontSize: t.status, color: C.red }), /* @__PURE__ */ ReactEcs4.createElement(Label4, { value: "One-time trial \u2022 4 hours of streaming", fontSize: t.labelSmall, color: C.textDim, uiTransform: { margin: { top: 4 } } })), !this.streamData?.trialAvailable && /* @__PURE__ */ ReactEcs4.createElement(UiEntity4, { uiTransform: { flexDirection: "column" } }, /* @__PURE__ */ ReactEcs4.createElement(
2214
2319
  Button3,
2215
2320
  {
2216
- uiTransform: { width: 150, height: 36, margin: { bottom: 4 } },
2321
+ uiTransform: { width: this.s(170), height: this.s(UI_DIMENSIONS.admin.buttonHeight), margin: { bottom: 6 } },
2217
2322
  uiBackground: { color: this.channelCreating ? C.btn : C.cyan },
2218
2323
  value: this.channelCreating ? "Creating..." : "+ Create Channel",
2219
- fontSize: 11,
2324
+ fontSize: t.button,
2220
2325
  color: C.text,
2221
2326
  onMouseDown: () => this.createChannel()
2222
2327
  }
2223
- ), this.channelCreateError && /* @__PURE__ */ ReactEcs4.createElement(Label4, { value: this.channelCreateError, fontSize: 9, color: C.red }), /* @__PURE__ */ ReactEcs4.createElement(Label4, { 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__ */ ReactEcs4.createElement(UiEntity4, { uiTransform: { flexDirection: "column", margin: { bottom: 12 } } }, /* @__PURE__ */ ReactEcs4.createElement(this.SectionHead, { label: this.streamData.isLive ? "\u{1F534} LIVE STREAM" : "STREAM SETTINGS", color: this.streamData.isLive ? C.red : C.yellow }), /* @__PURE__ */ ReactEcs4.createElement(UiEntity4, { uiTransform: { flexDirection: "row", alignItems: "center", margin: { bottom: 6 } } }, /* @__PURE__ */ ReactEcs4.createElement(
2328
+ ), this.channelCreateError && /* @__PURE__ */ ReactEcs4.createElement(Label4, { value: this.channelCreateError, fontSize: t.status, color: C.red }), /* @__PURE__ */ ReactEcs4.createElement(Label4, { 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__ */ ReactEcs4.createElement(UiEntity4, { uiTransform: { flexDirection: "column", margin: { bottom: 14 } } }, /* @__PURE__ */ ReactEcs4.createElement(this.SectionHead, { label: this.streamData.isLive ? "LIVE STREAM" : "STREAM SETTINGS", color: this.streamData.isLive ? C.red : C.yellow }), /* @__PURE__ */ ReactEcs4.createElement(UiEntity4, { uiTransform: { flexDirection: "row", alignItems: "center", margin: { bottom: 8 } } }, /* @__PURE__ */ ReactEcs4.createElement(
2224
2329
  Label4,
2225
2330
  {
2226
2331
  value: this.streamData.isLive ? `LIVE \u2022 ${this.streamData.currentViewers || 0} viewers` : "OFFLINE",
2227
- fontSize: 11,
2332
+ fontSize: t.label,
2228
2333
  color: this.streamData.isLive ? C.red : C.textDim
2229
2334
  }
2230
- ), /* @__PURE__ */ ReactEcs4.createElement(Label4, { value: ` \u2022 ${this.streamData.tier?.toUpperCase() || "RELAY"}`, fontSize: 10, color: C.cyan }), /* @__PURE__ */ ReactEcs4.createElement(Label4, { value: ` \u2022 ${this.streamData.sparksBalance || 0} Sparks`, fontSize: 10, color: C.textDim })), /* @__PURE__ */ ReactEcs4.createElement(
2335
+ ), /* @__PURE__ */ ReactEcs4.createElement(Label4, { value: ` \u2022 ${this.streamData.tier?.toUpperCase() || "RELAY"}`, fontSize: t.labelSmall, color: C.cyan }), /* @__PURE__ */ ReactEcs4.createElement(Label4, { value: ` \u2022 ${this.streamData.sparksBalance || 0} Sparks`, fontSize: t.labelSmall, color: C.textDim })), /* @__PURE__ */ ReactEcs4.createElement(
2231
2336
  Label4,
2232
2337
  {
2233
2338
  value: `Channel: ${this.streamData.channelName || this.streamData.channelId}`,
2234
- fontSize: 9,
2339
+ fontSize: t.labelSmall,
2235
2340
  color: C.textDim,
2236
- uiTransform: { margin: { bottom: 8 } }
2341
+ uiTransform: { margin: { bottom: 10 } }
2237
2342
  }
2238
- ), /* @__PURE__ */ ReactEcs4.createElement(UiEntity4, { uiTransform: { flexDirection: "column", margin: { bottom: 4 } } }, /* @__PURE__ */ ReactEcs4.createElement(Label4, { value: "RTMP Server:", fontSize: 9, color: C.textDim }), /* @__PURE__ */ ReactEcs4.createElement(
2343
+ ), /* @__PURE__ */ ReactEcs4.createElement(UiEntity4, { uiTransform: { flexDirection: "column", margin: { bottom: 6 } } }, /* @__PURE__ */ ReactEcs4.createElement(Label4, { value: "RTMP Server:", fontSize: t.labelSmall, color: C.textDim }), /* @__PURE__ */ ReactEcs4.createElement(
2239
2344
  Label4,
2240
2345
  {
2241
2346
  value: this.streamData.rtmpUrl || "Loading...",
2242
- fontSize: 10,
2347
+ fontSize: t.label,
2243
2348
  color: this.streamData.rtmpUrl ? C.text : C.textDim,
2244
- uiTransform: { margin: { left: 4 } }
2349
+ uiTransform: { margin: { left: 6 } }
2245
2350
  }
2246
- )), /* @__PURE__ */ ReactEcs4.createElement(UiEntity4, { uiTransform: { flexDirection: "column", margin: { bottom: 4 } } }, /* @__PURE__ */ ReactEcs4.createElement(Label4, { value: "Stream Key:", fontSize: 9, color: C.textDim }), /* @__PURE__ */ ReactEcs4.createElement(
2351
+ )), /* @__PURE__ */ ReactEcs4.createElement(UiEntity4, { uiTransform: { flexDirection: "column", margin: { bottom: 6 } } }, /* @__PURE__ */ ReactEcs4.createElement(Label4, { value: "Stream Key:", fontSize: t.labelSmall, color: C.textDim }), /* @__PURE__ */ ReactEcs4.createElement(
2247
2352
  Label4,
2248
2353
  {
2249
2354
  value: this.streamData.streamKey || "Loading...",
2250
- fontSize: 10,
2355
+ fontSize: t.label,
2251
2356
  color: this.streamData.streamKey ? C.cyan : C.textDim,
2252
- uiTransform: { margin: { left: 4 } }
2357
+ uiTransform: { margin: { left: 6 } }
2253
2358
  }
2254
- )), /* @__PURE__ */ ReactEcs4.createElement(UiEntity4, { uiTransform: { flexDirection: "column", margin: { bottom: 8 } } }, /* @__PURE__ */ ReactEcs4.createElement(Label4, { value: "HLS Playback:", fontSize: 9, color: C.textDim }), /* @__PURE__ */ ReactEcs4.createElement(
2359
+ )), /* @__PURE__ */ ReactEcs4.createElement(UiEntity4, { uiTransform: { flexDirection: "column", margin: { bottom: 10 } } }, /* @__PURE__ */ ReactEcs4.createElement(Label4, { value: "HLS Playback:", fontSize: t.labelSmall, color: C.textDim }), /* @__PURE__ */ ReactEcs4.createElement(
2255
2360
  Label4,
2256
2361
  {
2257
2362
  value: this.streamData.hlsUrl || "Not available",
2258
- fontSize: 10,
2363
+ fontSize: t.label,
2259
2364
  color: this.streamData.hlsUrl ? C.green : C.textDim,
2260
- uiTransform: { margin: { left: 4 } }
2365
+ uiTransform: { margin: { left: 6 } }
2261
2366
  }
2262
- )), /* @__PURE__ */ ReactEcs4.createElement(UiEntity4, { uiTransform: { flexDirection: "row", flexWrap: "wrap", margin: { bottom: 4 } } }, !this.streamData.isLive && /* @__PURE__ */ ReactEcs4.createElement(
2367
+ )), /* @__PURE__ */ ReactEcs4.createElement(UiEntity4, { uiTransform: { flexDirection: "row", flexWrap: "wrap", margin: { bottom: 6 } } }, !this.streamData.isLive && /* @__PURE__ */ ReactEcs4.createElement(
2263
2368
  Button3,
2264
2369
  {
2265
- uiTransform: { width: 100, height: 32, margin: 3 },
2370
+ uiTransform: { width: this.s(110), height: this.s(UI_DIMENSIONS.admin.buttonHeightSmall), margin: 4 },
2266
2371
  uiBackground: { color: this.streamControlling ? C.btn : C.green },
2267
2372
  value: this.streamControlling ? "Starting..." : "Start Stream",
2268
- fontSize: 10,
2373
+ fontSize: t.buttonSmall,
2269
2374
  color: C.text,
2270
2375
  onMouseDown: () => this.startStream()
2271
2376
  }
2272
2377
  ), this.streamData.isLive && /* @__PURE__ */ ReactEcs4.createElement(
2273
2378
  Button3,
2274
2379
  {
2275
- uiTransform: { width: 100, height: 32, margin: 3 },
2380
+ uiTransform: { width: this.s(110), height: this.s(UI_DIMENSIONS.admin.buttonHeightSmall), margin: 4 },
2276
2381
  uiBackground: { color: this.streamControlling ? C.btn : C.red },
2277
2382
  value: this.streamControlling ? "Stopping..." : "Stop Stream",
2278
- fontSize: 10,
2383
+ fontSize: t.buttonSmall,
2279
2384
  color: C.text,
2280
2385
  onMouseDown: () => this.stopStream()
2281
2386
  }
2282
2387
  ), this.streamData.isLive && this.streamData.hlsUrl && /* @__PURE__ */ ReactEcs4.createElement(
2283
2388
  Button3,
2284
2389
  {
2285
- uiTransform: { width: 90, height: 32, margin: 3 },
2390
+ uiTransform: { width: this.s(100), height: this.s(UI_DIMENSIONS.admin.buttonHeightSmall), margin: 4 },
2286
2391
  uiBackground: { color: C.cyan },
2287
2392
  value: "Play on Screen",
2288
- fontSize: 9,
2393
+ fontSize: t.buttonSmall,
2289
2394
  color: C.text,
2290
2395
  onMouseDown: () => this.config.onVideoPlay?.(this.streamData.hlsUrl)
2291
2396
  }
2292
- )), /* @__PURE__ */ ReactEcs4.createElement(UiEntity4, { uiTransform: { flexDirection: "row", flexWrap: "wrap", margin: { bottom: 4 } } }, /* @__PURE__ */ ReactEcs4.createElement(
2397
+ )), /* @__PURE__ */ ReactEcs4.createElement(UiEntity4, { uiTransform: { flexDirection: "row", flexWrap: "wrap", margin: { bottom: 6 } } }, /* @__PURE__ */ ReactEcs4.createElement(
2293
2398
  Button3,
2294
2399
  {
2295
- uiTransform: { width: 70, height: 28, margin: 3 },
2400
+ uiTransform: { width: this.s(80), height: this.s(UI_DIMENSIONS.admin.buttonHeightSmall), margin: 4 },
2296
2401
  uiBackground: { color: C.btn },
2297
2402
  value: "Refresh",
2298
- fontSize: 9,
2403
+ fontSize: t.buttonSmall,
2299
2404
  color: C.text,
2300
2405
  onMouseDown: () => this.refreshStreamStatus()
2301
2406
  }
2302
2407
  ), /* @__PURE__ */ ReactEcs4.createElement(
2303
2408
  Button3,
2304
2409
  {
2305
- uiTransform: { width: 85, height: 28, margin: 3 },
2410
+ uiTransform: { width: this.s(95), height: this.s(UI_DIMENSIONS.admin.buttonHeightSmall), margin: 4 },
2306
2411
  uiBackground: { color: this.keyRotating ? C.btn : C.yellow },
2307
2412
  value: this.keyRotating ? "..." : "Rotate Key",
2308
- fontSize: 9,
2413
+ fontSize: t.buttonSmall,
2309
2414
  color: C.text,
2310
2415
  onMouseDown: () => this.rotateStreamKey()
2311
2416
  }
2312
2417
  ), !this.streamData.isLive && /* @__PURE__ */ ReactEcs4.createElement(
2313
2418
  Button3,
2314
2419
  {
2315
- uiTransform: { width: 60, height: 28, margin: 3 },
2420
+ uiTransform: { width: this.s(70), height: this.s(UI_DIMENSIONS.admin.buttonHeightSmall), margin: 4 },
2316
2421
  uiBackground: { color: this.channelDeleting ? C.btn : C.red },
2317
2422
  value: this.channelDeleting ? "..." : "Delete",
2318
- fontSize: 9,
2423
+ fontSize: t.buttonSmall,
2319
2424
  color: C.text,
2320
2425
  onMouseDown: () => this.deleteChannel()
2321
2426
  }
2322
- )), this.streamControlStatus === "started" && /* @__PURE__ */ ReactEcs4.createElement(Label4, { value: "Stream started - begin broadcasting in OBS", fontSize: 9, color: C.green }), this.streamControlStatus === "stopped" && /* @__PURE__ */ ReactEcs4.createElement(Label4, { value: "Stream stopped", fontSize: 9, color: C.textDim }), this.keyRotateStatus === "success" && /* @__PURE__ */ ReactEcs4.createElement(Label4, { value: "Key rotated! Update OBS", fontSize: 9, color: C.green }), this.keyRotateStatus === "error" && /* @__PURE__ */ ReactEcs4.createElement(Label4, { value: "Failed to rotate key", fontSize: 9, color: C.red }), this.channelDeleteError && /* @__PURE__ */ ReactEcs4.createElement(Label4, { value: this.channelDeleteError, fontSize: 9, color: C.red }), this.streamControlStatus === "error" && /* @__PURE__ */ ReactEcs4.createElement(Label4, { value: "Stream control failed", fontSize: 9, color: C.red })), /* @__PURE__ */ ReactEcs4.createElement(this.SectionHead, { label: "PLAY NOW", color: C.orange }), /* @__PURE__ */ ReactEcs4.createElement(UiEntity4, { uiTransform: { flexDirection: "row", alignItems: "center", margin: { bottom: 12 } } }, /* @__PURE__ */ ReactEcs4.createElement(
2427
+ )), this.streamControlStatus === "started" && /* @__PURE__ */ ReactEcs4.createElement(Label4, { value: "Stream started - begin broadcasting in OBS", fontSize: t.status, color: C.green }), this.streamControlStatus === "stopped" && /* @__PURE__ */ ReactEcs4.createElement(Label4, { value: "Stream stopped", fontSize: t.status, color: C.textDim }), this.keyRotateStatus === "success" && /* @__PURE__ */ ReactEcs4.createElement(Label4, { value: "Key rotated! Update OBS", fontSize: t.status, color: C.green }), this.keyRotateStatus === "error" && /* @__PURE__ */ ReactEcs4.createElement(Label4, { value: "Failed to rotate key", fontSize: t.status, color: C.red }), this.channelDeleteError && /* @__PURE__ */ ReactEcs4.createElement(Label4, { value: this.channelDeleteError, fontSize: t.status, color: C.red }), this.streamControlStatus === "error" && /* @__PURE__ */ ReactEcs4.createElement(Label4, { value: "Stream control failed", fontSize: t.status, color: C.red })), /* @__PURE__ */ ReactEcs4.createElement(this.SectionHead, { label: "PLAY NOW", color: C.orange }), /* @__PURE__ */ ReactEcs4.createElement(UiEntity4, { uiTransform: { flexDirection: "row", alignItems: "center", margin: { bottom: 14 } } }, /* @__PURE__ */ ReactEcs4.createElement(
2323
2428
  Input4,
2324
2429
  {
2325
- uiTransform: { width: 220, height: 32 },
2430
+ uiTransform: { width: this.s(230), height: this.s(UI_DIMENSIONS.admin.inputHeight) },
2326
2431
  uiBackground: { color: Color45.create(0.15, 0.15, 0.2, 1) },
2327
2432
  placeholder: "Video URL...",
2328
2433
  placeholderColor: C.textDim,
2329
2434
  color: C.text,
2330
- fontSize: 11,
2435
+ fontSize: t.input,
2331
2436
  value: this.customVideoUrl,
2332
2437
  onChange: (val) => {
2333
2438
  this.customVideoUrl = val;
@@ -2336,10 +2441,10 @@ var AdminPanelUIModule = class {
2336
2441
  ), /* @__PURE__ */ ReactEcs4.createElement(
2337
2442
  Button3,
2338
2443
  {
2339
- uiTransform: { width: 70, height: 32, margin: { left: 6 } },
2444
+ uiTransform: { width: this.s(75), height: this.s(UI_DIMENSIONS.admin.inputHeight), margin: { left: 8 } },
2340
2445
  uiBackground: { color: C.green },
2341
2446
  value: "Play",
2342
- fontSize: 11,
2447
+ fontSize: t.button,
2343
2448
  color: C.text,
2344
2449
  onMouseDown: () => {
2345
2450
  if (this.customVideoUrl) this.config.onVideoPlay?.(this.customVideoUrl);
@@ -2348,211 +2453,218 @@ var AdminPanelUIModule = class {
2348
2453
  ), /* @__PURE__ */ ReactEcs4.createElement(
2349
2454
  Button3,
2350
2455
  {
2351
- uiTransform: { width: 60, height: 32, margin: { left: 4 } },
2456
+ uiTransform: { width: this.s(65), height: this.s(UI_DIMENSIONS.admin.inputHeight), margin: { left: 6 } },
2352
2457
  uiBackground: { color: C.btn },
2353
2458
  value: "Clear",
2354
- fontSize: 11,
2459
+ fontSize: t.button,
2355
2460
  color: C.text,
2356
2461
  onMouseDown: () => {
2357
2462
  this.customVideoUrl = "";
2358
2463
  }
2359
2464
  }
2360
- )), /* @__PURE__ */ ReactEcs4.createElement(this.SectionHead, { label: "PLAYBACK", color: C.green }), /* @__PURE__ */ ReactEcs4.createElement(UiEntity4, { uiTransform: { flexDirection: "row", flexWrap: "wrap", margin: { bottom: 12 } } }, /* @__PURE__ */ ReactEcs4.createElement(
2465
+ )), /* @__PURE__ */ ReactEcs4.createElement(this.SectionHead, { label: "PLAYBACK", color: C.green }), /* @__PURE__ */ ReactEcs4.createElement(UiEntity4, { uiTransform: { flexDirection: "row", flexWrap: "wrap", margin: { bottom: 14 } } }, /* @__PURE__ */ ReactEcs4.createElement(
2361
2466
  Button3,
2362
2467
  {
2363
- uiTransform: { width: 70, height: 32, margin: 3 },
2468
+ uiTransform: { width: this.s(75), height: this.s(UI_DIMENSIONS.admin.buttonHeightSmall), margin: 4 },
2364
2469
  uiBackground: { color: C.green },
2365
2470
  value: "Play",
2366
- fontSize: 11,
2471
+ fontSize: t.button,
2367
2472
  color: C.text,
2368
2473
  onMouseDown: () => this.config.onCommand?.("videoPlay", { playing: true })
2369
2474
  }
2370
2475
  ), /* @__PURE__ */ ReactEcs4.createElement(
2371
2476
  Button3,
2372
2477
  {
2373
- uiTransform: { width: 70, height: 32, margin: 3 },
2478
+ uiTransform: { width: this.s(75), height: this.s(UI_DIMENSIONS.admin.buttonHeightSmall), margin: 4 },
2374
2479
  uiBackground: { color: C.red },
2375
2480
  value: "Stop",
2376
- fontSize: 11,
2481
+ fontSize: t.button,
2377
2482
  color: C.text,
2378
2483
  onMouseDown: () => this.config.onVideoStop?.()
2379
2484
  }
2380
2485
  ), /* @__PURE__ */ ReactEcs4.createElement(
2381
2486
  Button3,
2382
2487
  {
2383
- uiTransform: { width: 90, height: 32, margin: 3 },
2488
+ uiTransform: { width: this.s(100), height: this.s(UI_DIMENSIONS.admin.buttonHeightSmall), margin: 4 },
2384
2489
  uiBackground: { color: C.btn },
2385
2490
  value: "Reset Default",
2386
- fontSize: 10,
2491
+ fontSize: t.buttonSmall,
2387
2492
  color: C.text,
2388
2493
  onMouseDown: () => this.config.onCommand?.("videoClear", {})
2389
2494
  }
2390
- )), /* @__PURE__ */ ReactEcs4.createElement(this.SectionHead, { label: "VIDEO SLOTS", color: C.cyan }), /* @__PURE__ */ ReactEcs4.createElement(UiEntity4, { uiTransform: { flexDirection: "row", flexWrap: "wrap", margin: { bottom: 8 } } }, /* @__PURE__ */ ReactEcs4.createElement(Button3, { 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__ */ ReactEcs4.createElement(Button3, { 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__ */ ReactEcs4.createElement(Button3, { 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__ */ ReactEcs4.createElement(Button3, { 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__ */ ReactEcs4.createElement(Button3, { 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__ */ ReactEcs4.createElement(
2495
+ )), /* @__PURE__ */ ReactEcs4.createElement(this.SectionHead, { label: "VIDEO SLOTS", color: C.cyan }), /* @__PURE__ */ ReactEcs4.createElement(UiEntity4, { uiTransform: { flexDirection: "row", flexWrap: "wrap", margin: { bottom: 10 } } }, /* @__PURE__ */ ReactEcs4.createElement(Button3, { 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__ */ ReactEcs4.createElement(Button3, { 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__ */ ReactEcs4.createElement(Button3, { 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__ */ ReactEcs4.createElement(Button3, { 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__ */ ReactEcs4.createElement(Button3, { 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__ */ ReactEcs4.createElement(
2391
2496
  Button3,
2392
2497
  {
2393
- uiTransform: { height: 20 },
2498
+ uiTransform: { height: this.s(24) },
2394
2499
  uiBackground: { color: Color45.create(0, 0, 0, 0) },
2395
2500
  value: "Edit slots at thestatic.tv \u2192",
2396
- fontSize: 10,
2501
+ fontSize: t.labelSmall,
2397
2502
  color: C.cyan,
2398
2503
  onMouseDown: () => openExternalUrl3({ url: this.config.footerLink || `https://thestatic.tv/scene/${this.config.sceneId}` })
2399
2504
  }
2400
2505
  ));
2401
2506
  };
2402
- this.ModTab = () => /* @__PURE__ */ ReactEcs4.createElement(UiEntity4, { uiTransform: { flexDirection: "column", width: "100%", padding: 8 } }, this.modStatus === "loading" && /* @__PURE__ */ ReactEcs4.createElement(Label4, { value: "Loading...", fontSize: 11, color: C.yellow }), this.modStatus === "saved" && /* @__PURE__ */ ReactEcs4.createElement(Label4, { value: "Saved!", fontSize: 11, color: C.green }), this.modStatus === "error" && /* @__PURE__ */ ReactEcs4.createElement(Label4, { value: "Error - check input", fontSize: 11, color: C.red }), /* @__PURE__ */ ReactEcs4.createElement(this.SectionHead, { label: "BROADCAST", color: C.orange }), /* @__PURE__ */ ReactEcs4.createElement(UiEntity4, { uiTransform: { flexDirection: "row", alignItems: "center", margin: { bottom: 12 } } }, /* @__PURE__ */ ReactEcs4.createElement(
2403
- Input4,
2404
- {
2405
- uiTransform: { width: 200, height: 32 },
2406
- uiBackground: { color: Color45.create(0.15, 0.15, 0.2, 1) },
2407
- placeholder: "Message to all players...",
2408
- placeholderColor: C.textDim,
2409
- color: C.text,
2410
- fontSize: 11,
2411
- value: this.broadcastText,
2412
- onChange: (val) => {
2413
- this.broadcastText = val;
2507
+ this.ModTab = () => {
2508
+ const t = this.theme;
2509
+ return /* @__PURE__ */ ReactEcs4.createElement(UiEntity4, { uiTransform: { flexDirection: "column", width: "100%", padding: 10 } }, this.modStatus === "loading" && /* @__PURE__ */ ReactEcs4.createElement(Label4, { value: "Loading...", fontSize: t.status, color: C.yellow }), this.modStatus === "saved" && /* @__PURE__ */ ReactEcs4.createElement(Label4, { value: "Saved!", fontSize: t.status, color: C.green }), this.modStatus === "error" && /* @__PURE__ */ ReactEcs4.createElement(Label4, { value: "Error - check input", fontSize: t.status, color: C.red }), /* @__PURE__ */ ReactEcs4.createElement(this.SectionHead, { label: "BROADCAST", color: C.orange }), /* @__PURE__ */ ReactEcs4.createElement(UiEntity4, { uiTransform: { flexDirection: "row", alignItems: "center", margin: { bottom: 14 } } }, /* @__PURE__ */ ReactEcs4.createElement(
2510
+ Input4,
2511
+ {
2512
+ uiTransform: { width: this.s(230), height: this.s(UI_DIMENSIONS.admin.inputHeight) },
2513
+ uiBackground: { color: Color45.create(0.15, 0.15, 0.2, 1) },
2514
+ placeholder: "Message to all players...",
2515
+ placeholderColor: C.textDim,
2516
+ color: C.text,
2517
+ fontSize: t.input,
2518
+ value: this.broadcastText,
2519
+ onChange: (val) => {
2520
+ this.broadcastText = val;
2521
+ }
2414
2522
  }
2415
- }
2416
- ), /* @__PURE__ */ ReactEcs4.createElement(
2417
- Button3,
2418
- {
2419
- uiTransform: { width: 55, height: 32, margin: { left: 6 } },
2420
- uiBackground: { color: C.orange },
2421
- value: "Send",
2422
- fontSize: 11,
2423
- color: C.text,
2424
- onMouseDown: () => this.sendBroadcast()
2425
- }
2426
- )), /* @__PURE__ */ ReactEcs4.createElement(this.SectionHead, { label: "CHAOS MODE", color: C.red }), /* @__PURE__ */ ReactEcs4.createElement(UiEntity4, { uiTransform: { flexDirection: "row", flexWrap: "wrap", margin: { bottom: 12 } } }, /* @__PURE__ */ ReactEcs4.createElement(
2427
- Button3,
2428
- {
2429
- uiTransform: { width: 120, height: 32, margin: 3 },
2430
- uiBackground: { color: C.red },
2431
- value: "KICK ALL",
2432
- fontSize: 11,
2433
- color: C.text,
2434
- onMouseDown: () => this.config.onCommand?.("kickAll", {})
2435
- }
2436
- )), /* @__PURE__ */ ReactEcs4.createElement(this.SectionHead, { label: "SCENE ADMINS", color: C.purple }), /* @__PURE__ */ ReactEcs4.createElement(UiEntity4, { uiTransform: { flexDirection: "column", margin: { bottom: 4 } } }, this.sceneAdmins.length === 0 && this.modStatus !== "loading" && /* @__PURE__ */ ReactEcs4.createElement(Label4, { value: "No scene admins", fontSize: 10, color: C.textDim }), this.sceneAdmins.map((wallet, i) => /* @__PURE__ */ ReactEcs4.createElement(
2437
- UiEntity4,
2438
- {
2439
- key: `admin-${i}`,
2440
- uiTransform: { flexDirection: "row", alignItems: "center", margin: { bottom: 3 }, width: "100%" }
2441
- },
2442
- /* @__PURE__ */ ReactEcs4.createElement(
2443
- Label4,
2523
+ ), /* @__PURE__ */ ReactEcs4.createElement(
2524
+ Button3,
2444
2525
  {
2445
- value: `${wallet.slice(0, 6)}...${wallet.slice(-4)}`,
2446
- fontSize: 10,
2526
+ uiTransform: { width: this.s(65), height: this.s(UI_DIMENSIONS.admin.inputHeight), margin: { left: 8 } },
2527
+ uiBackground: { color: C.orange },
2528
+ value: "Send",
2529
+ fontSize: t.button,
2447
2530
  color: C.text,
2448
- uiTransform: { width: 110 }
2531
+ onMouseDown: () => this.sendBroadcast()
2449
2532
  }
2450
- ),
2451
- /* @__PURE__ */ ReactEcs4.createElement(
2533
+ )), /* @__PURE__ */ ReactEcs4.createElement(this.SectionHead, { label: "CHAOS MODE", color: C.red }), /* @__PURE__ */ ReactEcs4.createElement(UiEntity4, { uiTransform: { flexDirection: "row", flexWrap: "wrap", margin: { bottom: 14 } } }, /* @__PURE__ */ ReactEcs4.createElement(
2452
2534
  Button3,
2453
2535
  {
2454
- uiTransform: { width: 55, height: 24, margin: { left: 6 } },
2455
- uiBackground: { color: C.btn },
2456
- value: "Remove",
2457
- fontSize: 9,
2536
+ uiTransform: { width: this.s(130), height: this.s(UI_DIMENSIONS.admin.buttonHeight), margin: 4 },
2537
+ uiBackground: { color: C.red },
2538
+ value: "KICK ALL",
2539
+ fontSize: t.button,
2458
2540
  color: C.text,
2459
- onMouseDown: () => this.removeSceneAdmin(wallet)
2541
+ onMouseDown: () => {
2542
+ this.log("KICK ALL clicked");
2543
+ this.config.onCommand?.("kickAll", {});
2544
+ }
2460
2545
  }
2461
- )
2462
- ))), /* @__PURE__ */ ReactEcs4.createElement(UiEntity4, { uiTransform: { flexDirection: "row", alignItems: "center", margin: { bottom: 12 } } }, /* @__PURE__ */ ReactEcs4.createElement(
2463
- Input4,
2464
- {
2465
- uiTransform: { width: 200, height: 28 },
2466
- uiBackground: { color: Color45.create(0.15, 0.15, 0.2, 1) },
2467
- placeholder: "0x... add admin",
2468
- placeholderColor: C.textDim,
2469
- color: C.text,
2470
- fontSize: 10,
2471
- value: this.newAdminWallet,
2472
- onChange: (val) => {
2473
- this.newAdminWallet = val;
2546
+ )), /* @__PURE__ */ ReactEcs4.createElement(this.SectionHead, { label: "SCENE ADMINS", color: C.purple }), /* @__PURE__ */ ReactEcs4.createElement(UiEntity4, { uiTransform: { flexDirection: "column", margin: { bottom: 6 } } }, this.sceneAdmins.length === 0 && this.modStatus !== "loading" && /* @__PURE__ */ ReactEcs4.createElement(Label4, { value: "No scene admins", fontSize: t.labelSmall, color: C.textDim }), this.sceneAdmins.map((wallet, i) => /* @__PURE__ */ ReactEcs4.createElement(
2547
+ UiEntity4,
2548
+ {
2549
+ key: `admin-${i}`,
2550
+ uiTransform: { flexDirection: "row", alignItems: "center", margin: { bottom: 5 }, width: "100%" }
2551
+ },
2552
+ /* @__PURE__ */ ReactEcs4.createElement(
2553
+ Label4,
2554
+ {
2555
+ value: `${wallet.slice(0, 6)}...${wallet.slice(-4)}`,
2556
+ fontSize: t.label,
2557
+ color: C.text,
2558
+ uiTransform: { width: this.s(130) }
2559
+ }
2560
+ ),
2561
+ /* @__PURE__ */ ReactEcs4.createElement(
2562
+ Button3,
2563
+ {
2564
+ uiTransform: { width: this.s(70), height: this.s(28), margin: { left: 8 } },
2565
+ uiBackground: { color: C.btn },
2566
+ value: "Remove",
2567
+ fontSize: t.buttonSmall,
2568
+ color: C.text,
2569
+ onMouseDown: () => this.removeSceneAdmin(wallet)
2570
+ }
2571
+ )
2572
+ ))), /* @__PURE__ */ ReactEcs4.createElement(UiEntity4, { uiTransform: { flexDirection: "row", alignItems: "center", margin: { bottom: 14 } } }, /* @__PURE__ */ ReactEcs4.createElement(
2573
+ Input4,
2574
+ {
2575
+ uiTransform: { width: this.s(230), height: this.s(UI_DIMENSIONS.admin.inputHeight) },
2576
+ uiBackground: { color: Color45.create(0.15, 0.15, 0.2, 1) },
2577
+ placeholder: "0x... add admin",
2578
+ placeholderColor: C.textDim,
2579
+ color: C.text,
2580
+ fontSize: t.input,
2581
+ value: this.newAdminWallet,
2582
+ onChange: (val) => {
2583
+ this.newAdminWallet = val;
2584
+ }
2474
2585
  }
2475
- }
2476
- ), /* @__PURE__ */ ReactEcs4.createElement(
2477
- Button3,
2478
- {
2479
- uiTransform: { width: 50, height: 28, margin: { left: 6 } },
2480
- uiBackground: { color: C.purple },
2481
- value: "Add",
2482
- fontSize: 10,
2483
- color: C.text,
2484
- onMouseDown: () => {
2485
- if (this.newAdminWallet) this.addSceneAdmin(this.newAdminWallet);
2586
+ ), /* @__PURE__ */ ReactEcs4.createElement(
2587
+ Button3,
2588
+ {
2589
+ uiTransform: { width: this.s(60), height: this.s(UI_DIMENSIONS.admin.inputHeight), margin: { left: 8 } },
2590
+ uiBackground: { color: C.purple },
2591
+ value: "Add",
2592
+ fontSize: t.button,
2593
+ color: C.text,
2594
+ onMouseDown: () => {
2595
+ if (this.newAdminWallet) this.addSceneAdmin(this.newAdminWallet);
2596
+ }
2486
2597
  }
2487
- }
2488
- )), /* @__PURE__ */ ReactEcs4.createElement(this.SectionHead, { label: "BANNED WALLETS", color: C.red }), /* @__PURE__ */ ReactEcs4.createElement(UiEntity4, { uiTransform: { flexDirection: "column", margin: { bottom: 4 } } }, this.bannedWallets.length === 0 && this.modStatus !== "loading" && /* @__PURE__ */ ReactEcs4.createElement(Label4, { value: "No banned wallets", fontSize: 10, color: C.textDim }), this.bannedWallets.map((wallet, i) => /* @__PURE__ */ ReactEcs4.createElement(
2489
- UiEntity4,
2490
- {
2491
- key: `ban-${i}`,
2492
- uiTransform: { flexDirection: "row", alignItems: "center", margin: { bottom: 3 }, width: "100%" }
2493
- },
2494
- /* @__PURE__ */ ReactEcs4.createElement(
2495
- Label4,
2598
+ )), /* @__PURE__ */ ReactEcs4.createElement(this.SectionHead, { label: "BANNED WALLETS", color: C.red }), /* @__PURE__ */ ReactEcs4.createElement(UiEntity4, { uiTransform: { flexDirection: "column", margin: { bottom: 6 } } }, this.bannedWallets.length === 0 && this.modStatus !== "loading" && /* @__PURE__ */ ReactEcs4.createElement(Label4, { value: "No banned wallets", fontSize: t.labelSmall, color: C.textDim }), this.bannedWallets.map((wallet, i) => /* @__PURE__ */ ReactEcs4.createElement(
2599
+ UiEntity4,
2496
2600
  {
2497
- value: `${wallet.slice(0, 6)}...${wallet.slice(-4)}`,
2498
- fontSize: 10,
2499
- color: C.red,
2500
- uiTransform: { width: 110 }
2601
+ key: `ban-${i}`,
2602
+ uiTransform: { flexDirection: "row", alignItems: "center", margin: { bottom: 5 }, width: "100%" }
2603
+ },
2604
+ /* @__PURE__ */ ReactEcs4.createElement(
2605
+ Label4,
2606
+ {
2607
+ value: `${wallet.slice(0, 6)}...${wallet.slice(-4)}`,
2608
+ fontSize: t.label,
2609
+ color: C.red,
2610
+ uiTransform: { width: this.s(130) }
2611
+ }
2612
+ ),
2613
+ /* @__PURE__ */ ReactEcs4.createElement(
2614
+ Button3,
2615
+ {
2616
+ uiTransform: { width: this.s(70), height: this.s(28), margin: { left: 8 } },
2617
+ uiBackground: { color: C.green },
2618
+ value: "Unban",
2619
+ fontSize: t.buttonSmall,
2620
+ color: C.text,
2621
+ onMouseDown: () => this.unbanWallet(wallet)
2622
+ }
2623
+ )
2624
+ ))), /* @__PURE__ */ ReactEcs4.createElement(UiEntity4, { uiTransform: { flexDirection: "row", alignItems: "center", margin: { bottom: 10 } } }, /* @__PURE__ */ ReactEcs4.createElement(
2625
+ Input4,
2626
+ {
2627
+ uiTransform: { width: this.s(230), height: this.s(UI_DIMENSIONS.admin.inputHeight) },
2628
+ uiBackground: { color: Color45.create(0.15, 0.15, 0.2, 1) },
2629
+ placeholder: "0x... ban wallet",
2630
+ placeholderColor: C.textDim,
2631
+ color: C.text,
2632
+ fontSize: t.input,
2633
+ value: this.newBanWallet,
2634
+ onChange: (val) => {
2635
+ this.newBanWallet = val;
2636
+ }
2501
2637
  }
2502
- ),
2503
- /* @__PURE__ */ ReactEcs4.createElement(
2638
+ ), /* @__PURE__ */ ReactEcs4.createElement(
2504
2639
  Button3,
2505
2640
  {
2506
- uiTransform: { width: 55, height: 24, margin: { left: 6 } },
2507
- uiBackground: { color: C.green },
2508
- value: "Unban",
2509
- fontSize: 9,
2641
+ uiTransform: { width: this.s(60), height: this.s(UI_DIMENSIONS.admin.inputHeight), margin: { left: 8 } },
2642
+ uiBackground: { color: C.red },
2643
+ value: "Ban",
2644
+ fontSize: t.button,
2510
2645
  color: C.text,
2511
- onMouseDown: () => this.unbanWallet(wallet)
2512
- }
2513
- )
2514
- ))), /* @__PURE__ */ ReactEcs4.createElement(UiEntity4, { uiTransform: { flexDirection: "row", alignItems: "center", margin: { bottom: 8 } } }, /* @__PURE__ */ ReactEcs4.createElement(
2515
- Input4,
2516
- {
2517
- uiTransform: { width: 200, height: 28 },
2518
- uiBackground: { color: Color45.create(0.15, 0.15, 0.2, 1) },
2519
- placeholder: "0x... ban wallet",
2520
- placeholderColor: C.textDim,
2521
- color: C.text,
2522
- fontSize: 10,
2523
- value: this.newBanWallet,
2524
- onChange: (val) => {
2525
- this.newBanWallet = val;
2646
+ onMouseDown: () => {
2647
+ if (this.newBanWallet) this.banWallet(this.newBanWallet);
2648
+ }
2526
2649
  }
2527
- }
2528
- ), /* @__PURE__ */ ReactEcs4.createElement(
2529
- Button3,
2530
- {
2531
- uiTransform: { width: 50, height: 28, margin: { left: 6 } },
2532
- uiBackground: { color: C.red },
2533
- value: "Ban",
2534
- fontSize: 10,
2535
- color: C.text,
2536
- onMouseDown: () => {
2537
- if (this.newBanWallet) this.banWallet(this.newBanWallet);
2650
+ )), /* @__PURE__ */ ReactEcs4.createElement(
2651
+ Button3,
2652
+ {
2653
+ uiTransform: { height: this.s(24) },
2654
+ uiBackground: { color: Color45.create(0, 0, 0, 0) },
2655
+ value: "Manage at thestatic.tv \u2192",
2656
+ fontSize: t.labelSmall,
2657
+ color: C.cyan,
2658
+ onMouseDown: () => openExternalUrl3({ url: this.config.footerLink || `https://thestatic.tv/scene/${this.config.sceneId}` })
2538
2659
  }
2539
- }
2540
- )), /* @__PURE__ */ ReactEcs4.createElement(
2541
- Button3,
2542
- {
2543
- uiTransform: { height: 20 },
2544
- uiBackground: { color: Color45.create(0, 0, 0, 0) },
2545
- value: "Manage at thestatic.tv \u2192",
2546
- fontSize: 9,
2547
- color: C.cyan,
2548
- onMouseDown: () => openExternalUrl3({ url: this.config.footerLink || `https://thestatic.tv/scene/${this.config.sceneId}` })
2549
- }
2550
- ));
2660
+ ));
2661
+ };
2551
2662
  /**
2552
2663
  * Get the React-ECS component for the admin panel
2553
2664
  */
2554
2665
  this.getComponent = () => {
2555
2666
  if (!this.isAdmin) return null;
2667
+ const t = this.theme;
2556
2668
  const tabs = [];
2557
2669
  if (this.config.sceneTabs && this.config.sceneTabs.length > 0) {
2558
2670
  this.config.sceneTabs.forEach((tab) => tabs.push({ label: tab.label, id: tab.id }));
@@ -2563,7 +2675,7 @@ var AdminPanelUIModule = class {
2563
2675
  if (this.config.showModTab !== false && this.isOwner) {
2564
2676
  tabs.push({ label: "MOD", id: "mod" });
2565
2677
  }
2566
- if (!tabs.find((t) => t.id === this.activeTab) && tabs.length > 0) {
2678
+ if (!tabs.find((tab) => tab.id === this.activeTab) && tabs.length > 0) {
2567
2679
  this.activeTab = tabs[0].id;
2568
2680
  }
2569
2681
  return /* @__PURE__ */ ReactEcs4.createElement(
@@ -2579,17 +2691,17 @@ var AdminPanelUIModule = class {
2579
2691
  UiEntity4,
2580
2692
  {
2581
2693
  uiTransform: {
2582
- position: { right: 16, bottom: 16 },
2694
+ position: { right: 20 + this.s(100) + 10 + this.s(100) + 10, bottom: 10 },
2583
2695
  positionType: "absolute"
2584
2696
  }
2585
2697
  },
2586
2698
  /* @__PURE__ */ ReactEcs4.createElement(
2587
2699
  Button3,
2588
2700
  {
2589
- uiTransform: { width: 90, height: 36 },
2701
+ uiTransform: { width: this.s(100), height: this.s(45) },
2590
2702
  uiBackground: { color: this.panelOpen ? C.btn : C.header },
2591
2703
  value: this.panelOpen ? "CLOSE" : "ADMIN",
2592
- fontSize: 13,
2704
+ fontSize: this.s(14),
2593
2705
  color: C.text,
2594
2706
  onMouseDown: () => this.toggle()
2595
2707
  }
@@ -2598,10 +2710,12 @@ var AdminPanelUIModule = class {
2598
2710
  this.panelOpen && /* @__PURE__ */ ReactEcs4.createElement(
2599
2711
  UiEntity4,
2600
2712
  {
2713
+ key: `admin-panel-${this.client.uiScale}`,
2601
2714
  uiTransform: {
2602
- width: 380,
2603
- maxHeight: 800,
2604
- position: { right: 16, bottom: 60 },
2715
+ width: this.s(UI_DIMENSIONS.admin.width),
2716
+ height: this.s(UI_DIMENSIONS.admin.height),
2717
+ maxHeight: this.s(UI_DIMENSIONS.admin.maxHeight),
2718
+ position: { right: UI_DIMENSIONS.chat.right + this.s(UI_DIMENSIONS.chat.width) + 10, bottom: UI_DIMENSIONS.admin.bottom },
2605
2719
  positionType: "absolute",
2606
2720
  flexDirection: "column"
2607
2721
  },
@@ -2612,16 +2726,28 @@ var AdminPanelUIModule = class {
2612
2726
  {
2613
2727
  uiTransform: {
2614
2728
  width: "100%",
2615
- height: 44,
2616
- justifyContent: "center",
2729
+ height: this.s(UI_DIMENSIONS.admin.headerHeight),
2730
+ justifyContent: "space-between",
2617
2731
  alignItems: "center",
2618
- flexDirection: "row"
2732
+ flexDirection: "row",
2733
+ padding: { left: 12, right: 8 }
2619
2734
  },
2620
2735
  uiBackground: { color: C.header }
2621
2736
  },
2622
- /* @__PURE__ */ ReactEcs4.createElement(Label4, { value: this.config.title || "ADMIN PANEL", fontSize: 15, color: C.text })
2737
+ /* @__PURE__ */ ReactEcs4.createElement(Label4, { value: this.config.title || "ADMIN PANEL", fontSize: t.header, color: C.text }),
2738
+ /* @__PURE__ */ ReactEcs4.createElement(
2739
+ Button3,
2740
+ {
2741
+ uiTransform: { width: this.s(28), height: this.s(28) },
2742
+ uiBackground: { color: Color45.create(0, 0, 0, 0.3) },
2743
+ value: "X",
2744
+ fontSize: t.header,
2745
+ color: THEME.colors.red,
2746
+ onMouseDown: () => this.hide()
2747
+ }
2748
+ )
2623
2749
  ),
2624
- /* @__PURE__ */ ReactEcs4.createElement(UiEntity4, { uiTransform: { width: "100%", height: 36, flexDirection: "row" } }, tabs.map((tab) => /* @__PURE__ */ ReactEcs4.createElement(this.TabBtn, { label: tab.label, tab: tab.id }))),
2750
+ /* @__PURE__ */ ReactEcs4.createElement(UiEntity4, { uiTransform: { width: "100%", height: this.s(UI_DIMENSIONS.admin.tabHeight), flexDirection: "row" } }, tabs.map((tab) => /* @__PURE__ */ ReactEcs4.createElement(this.TabBtn, { label: tab.label, tab: tab.id }))),
2625
2751
  /* @__PURE__ */ ReactEcs4.createElement(
2626
2752
  UiEntity4,
2627
2753
  {
@@ -2641,7 +2767,7 @@ var AdminPanelUIModule = class {
2641
2767
  {
2642
2768
  uiTransform: {
2643
2769
  width: "100%",
2644
- height: 28,
2770
+ height: this.s(UI_DIMENSIONS.admin.footerHeight),
2645
2771
  justifyContent: "center",
2646
2772
  alignItems: "center"
2647
2773
  },
@@ -2650,10 +2776,10 @@ var AdminPanelUIModule = class {
2650
2776
  /* @__PURE__ */ ReactEcs4.createElement(
2651
2777
  Button3,
2652
2778
  {
2653
- uiTransform: { height: 20 },
2779
+ uiTransform: { height: this.s(24) },
2654
2780
  uiBackground: { color: Color45.create(0, 0, 0, 0) },
2655
2781
  value: `thestatic.tv/scene/${this.config.sceneId} \u2192`,
2656
- fontSize: 10,
2782
+ fontSize: t.labelSmall,
2657
2783
  color: C.cyan,
2658
2784
  onMouseDown: () => openExternalUrl3({ url: this.config.footerLink || `https://thestatic.tv/scene/${this.config.sceneId}` })
2659
2785
  }
@@ -2663,14 +2789,19 @@ var AdminPanelUIModule = class {
2663
2789
  );
2664
2790
  };
2665
2791
  this.client = client;
2792
+ if (!config.sceneId) {
2793
+ throw new Error("[AdminPanel] sceneId is required");
2794
+ }
2666
2795
  this.config = {
2667
2796
  showVideoTab: true,
2668
2797
  showModTab: true,
2669
2798
  title: "ADMIN PANEL",
2670
2799
  debug: false,
2671
- ...config
2800
+ ...config,
2801
+ sceneId: config.sceneId
2802
+ // Ensure sceneId is set
2672
2803
  };
2673
- this.baseUrl = client.baseUrl || "https://thestatic.tv/api/v1/dcl";
2804
+ this.baseUrl = client.getBaseUrl();
2674
2805
  if (config.headerColor) {
2675
2806
  C.header = Color45.create(
2676
2807
  config.headerColor.r,
@@ -2685,12 +2816,21 @@ var AdminPanelUIModule = class {
2685
2816
  console.log(`[AdminPanel] ${msg}`, ...args);
2686
2817
  }
2687
2818
  }
2819
+ /** Scale a dimension by shared uiScale */
2820
+ s(value) {
2821
+ return Math.round(value * this.client.uiScale);
2822
+ }
2823
+ /** Get scaled theme */
2824
+ get theme() {
2825
+ return scaleAdminTheme(DEFAULT_ADMIN_THEME, this.client.uiScale);
2826
+ }
2688
2827
  /**
2689
- * Initialize the admin panel - checks admin status
2828
+ * Initialize the admin panel - checks admin status and fetches video state
2690
2829
  */
2691
2830
  async init() {
2692
- await new Promise((resolve) => setTimeout(resolve, 3e3));
2693
2831
  await this.checkAdminStatus();
2832
+ await this.fetchVideoState();
2833
+ this.autoPlayDefault();
2694
2834
  this.log("Initialized");
2695
2835
  }
2696
2836
  /**
@@ -2703,17 +2843,23 @@ var AdminPanelUIModule = class {
2703
2843
  return;
2704
2844
  }
2705
2845
  this.playerWallet = player.userId;
2846
+ if (this.config.forceAdmin) {
2847
+ this.isAdmin = true;
2848
+ this.isOwner = true;
2849
+ this.log("Admin status FORCED (forceAdmin: true)");
2850
+ return;
2851
+ }
2706
2852
  try {
2707
2853
  const res = await fetch(
2708
2854
  `${this.baseUrl}/scene/${this.config.sceneId}/admin-check?wallet=${player.userId}`
2709
2855
  );
2710
2856
  if (res.ok) {
2711
2857
  const data = await res.json();
2712
- this.isAdmin = data.hasAccess;
2713
- this.isOwner = data.isOwner || data.isAdmin || data.isSceneAdmin;
2858
+ this.isAdmin = data.showButton ?? data.hasAccess;
2859
+ this.isOwner = data.isOwner || data.isSceneAdmin;
2714
2860
  if (data.isBanned) {
2715
2861
  this.config.onBroadcast?.("You have been banned from this scene.");
2716
- setTimeout(() => this.banKickPlayer(), 2e3);
2862
+ this.banKickPlayer();
2717
2863
  this.log("Player is banned - kicking");
2718
2864
  }
2719
2865
  this.log("Admin status:", this.isAdmin, "Owner:", this.isOwner);
@@ -2726,13 +2872,31 @@ var AdminPanelUIModule = class {
2726
2872
  * Toggle the admin panel open/closed
2727
2873
  */
2728
2874
  toggle() {
2729
- this.panelOpen = !this.panelOpen;
2730
- if (!this.panelOpen) {
2731
- this.stopStreamPolling();
2732
- } else if (this.activeTab === "video") {
2875
+ if (this.panelOpen) {
2876
+ this.hide();
2877
+ } else {
2878
+ this.show();
2879
+ }
2880
+ }
2881
+ /**
2882
+ * Show the admin panel
2883
+ */
2884
+ show() {
2885
+ if (this.panelOpen) return;
2886
+ this.client.closeOtherPanels("admin");
2887
+ this.panelOpen = true;
2888
+ if (this.activeTab === "video") {
2733
2889
  this.startStreamPolling();
2734
2890
  }
2735
2891
  }
2892
+ /**
2893
+ * Hide the admin panel
2894
+ */
2895
+ hide() {
2896
+ if (!this.panelOpen) return;
2897
+ this.panelOpen = false;
2898
+ this.stopStreamPolling();
2899
+ }
2736
2900
  /**
2737
2901
  * Check if the panel is currently open
2738
2902
  */
@@ -2746,8 +2910,8 @@ var AdminPanelUIModule = class {
2746
2910
  return this.isAdmin;
2747
2911
  }
2748
2912
  /**
2749
- * Register a custom scene tab (Full/Custom tier)
2750
- */
2913
+ * Register a custom scene tab (Pro tier)
2914
+ */
2751
2915
  registerSceneTab(tab) {
2752
2916
  if (!this.config.sceneTabs) {
2753
2917
  this.config.sceneTabs = [];
@@ -2758,7 +2922,7 @@ var AdminPanelUIModule = class {
2758
2922
  // --- Stream Polling ---
2759
2923
  startStreamPolling() {
2760
2924
  if (this.pollIntervalId !== null) return;
2761
- this.pollIntervalId = setInterval(() => {
2925
+ this.pollIntervalId = dclSetInterval(() => {
2762
2926
  if (this.activeTab === "video" && this.panelOpen && this.streamData?.hasChannel) {
2763
2927
  this.refreshStreamStatus();
2764
2928
  }
@@ -2767,7 +2931,7 @@ var AdminPanelUIModule = class {
2767
2931
  }
2768
2932
  stopStreamPolling() {
2769
2933
  if (this.pollIntervalId !== null) {
2770
- clearInterval(this.pollIntervalId);
2934
+ dclClearInterval(this.pollIntervalId);
2771
2935
  this.pollIntervalId = null;
2772
2936
  this.log("Stream polling stopped");
2773
2937
  }
@@ -2800,6 +2964,51 @@ var AdminPanelUIModule = class {
2800
2964
  } catch (err) {
2801
2965
  }
2802
2966
  }
2967
+ // --- Video State (Slots) ---
2968
+ async fetchVideoState() {
2969
+ if (this.videoStateFetched) return;
2970
+ try {
2971
+ const res = await fetch(
2972
+ `${this.baseUrl}/scene/${this.config.sceneId}/video-state`
2973
+ );
2974
+ if (res.ok) {
2975
+ const data = await res.json();
2976
+ this.videoState = {
2977
+ defaultSlot: data.defaultSlot || null,
2978
+ videoSlots: data.videoSlots || {}
2979
+ };
2980
+ this.videoStateFetched = true;
2981
+ this.log("Video state fetched:", this.videoState.defaultSlot || "no default");
2982
+ }
2983
+ } catch (err) {
2984
+ this.log("Video state fetch error:", err);
2985
+ }
2986
+ }
2987
+ /**
2988
+ * Play a video slot by ID - looks up URL and calls onVideoPlay
2989
+ */
2990
+ playSlot(slotId) {
2991
+ const slot = this.videoState?.videoSlots?.[slotId];
2992
+ if (slot?.url) {
2993
+ this.log("Playing slot:", slotId, slot.url);
2994
+ this.config.onVideoPlay?.(slot.url);
2995
+ } else {
2996
+ this.log("Slot has no URL:", slotId);
2997
+ this.config.onVideoSlotPlay?.(slotId);
2998
+ }
2999
+ }
3000
+ /**
3001
+ * Auto-play the default slot if configured
3002
+ */
3003
+ autoPlayDefault() {
3004
+ if (!this.videoState?.defaultSlot) return;
3005
+ const defaultSlot = this.videoState.defaultSlot;
3006
+ const slot = this.videoState.videoSlots?.[defaultSlot];
3007
+ if (slot?.url) {
3008
+ this.log("Auto-playing default slot:", defaultSlot);
3009
+ this.config.onVideoPlay?.(slot.url);
3010
+ }
3011
+ }
2803
3012
  async createChannel() {
2804
3013
  if (!this.playerWallet || this.channelCreating) return;
2805
3014
  this.channelCreating = true;
@@ -2916,9 +3125,6 @@ var AdminPanelUIModule = class {
2916
3125
  this.config.onBroadcast?.("Stream started!");
2917
3126
  this.streamFetched = false;
2918
3127
  await this.fetchStreamData();
2919
- setTimeout(() => {
2920
- this.streamControlStatus = "";
2921
- }, 3e3);
2922
3128
  } else {
2923
3129
  this.streamControlStatus = "error";
2924
3130
  this.log("Start stream failed:", data.error);
@@ -2951,9 +3157,6 @@ var AdminPanelUIModule = class {
2951
3157
  this.config.onBroadcast?.("Stream stopped");
2952
3158
  this.streamFetched = false;
2953
3159
  await this.fetchStreamData();
2954
- setTimeout(() => {
2955
- this.streamControlStatus = "";
2956
- }, 3e3);
2957
3160
  } else {
2958
3161
  this.streamControlStatus = "error";
2959
3162
  this.log("Stop stream failed:", data.error);
@@ -2983,9 +3186,6 @@ var AdminPanelUIModule = class {
2983
3186
  this.keyRotateStatus = "success";
2984
3187
  this.log("Key rotated:", data.message);
2985
3188
  this.config.onBroadcast?.("Stream key rotated - update OBS settings");
2986
- setTimeout(() => {
2987
- this.keyRotateStatus = "";
2988
- }, 3e3);
2989
3189
  } else {
2990
3190
  this.keyRotateStatus = "error";
2991
3191
  this.log("Key rotation failed:", data.error);
@@ -3045,9 +3245,6 @@ var AdminPanelUIModule = class {
3045
3245
  this.sceneAdmins = [...this.sceneAdmins, normalized];
3046
3246
  this.newAdminWallet = "";
3047
3247
  this.modStatus = "saved";
3048
- setTimeout(() => {
3049
- this.modStatus = "";
3050
- }, 2e3);
3051
3248
  this.log("Added scene admin:", normalized);
3052
3249
  } else {
3053
3250
  this.modStatus = "error";
@@ -3075,9 +3272,6 @@ var AdminPanelUIModule = class {
3075
3272
  if (res.ok) {
3076
3273
  this.sceneAdmins = this.sceneAdmins.filter((w) => w !== wallet);
3077
3274
  this.modStatus = "saved";
3078
- setTimeout(() => {
3079
- this.modStatus = "";
3080
- }, 2e3);
3081
3275
  this.log("Removed scene admin:", wallet);
3082
3276
  } else {
3083
3277
  this.modStatus = "error";
@@ -3112,9 +3306,6 @@ var AdminPanelUIModule = class {
3112
3306
  this.bannedWallets = [...this.bannedWallets, normalized];
3113
3307
  this.newBanWallet = "";
3114
3308
  this.modStatus = "saved";
3115
- setTimeout(() => {
3116
- this.modStatus = "";
3117
- }, 2e3);
3118
3309
  this.log("Banned wallet:", normalized);
3119
3310
  this.config.onCommand?.("kickBanned", { wallet: normalized });
3120
3311
  } else {
@@ -3143,9 +3334,6 @@ var AdminPanelUIModule = class {
3143
3334
  if (res.ok) {
3144
3335
  this.bannedWallets = this.bannedWallets.filter((w) => w !== wallet);
3145
3336
  this.modStatus = "saved";
3146
- setTimeout(() => {
3147
- this.modStatus = "";
3148
- }, 2e3);
3149
3337
  this.log("Unbanned wallet:", wallet);
3150
3338
  } else {
3151
3339
  this.modStatus = "error";
@@ -3179,7 +3367,89 @@ var AdminPanelUIModule = class {
3179
3367
  }
3180
3368
  };
3181
3369
 
3370
+ // src/ui/ui-renderer.tsx
3371
+ import ReactEcs5, { ReactEcsRenderer, UiEntity as UiEntity5, Label as Label5 } from "@dcl/sdk/react-ecs";
3372
+ import { Color4 as Color46 } from "@dcl/sdk/math";
3373
+ import { engine as engine2 } from "@dcl/sdk/ecs";
3374
+ var notificationText = "";
3375
+ var notificationVisible = false;
3376
+ var notificationEndTime = 0;
3377
+ var notificationInitialized = false;
3378
+ function showNotification(message, durationMs = 5e3) {
3379
+ notificationText = message;
3380
+ notificationVisible = true;
3381
+ notificationEndTime = Date.now() + durationMs;
3382
+ }
3383
+ function initNotificationSystem() {
3384
+ if (notificationInitialized) return;
3385
+ notificationInitialized = true;
3386
+ engine2.addSystem(() => {
3387
+ if (notificationVisible && Date.now() > notificationEndTime) {
3388
+ notificationVisible = false;
3389
+ }
3390
+ });
3391
+ }
3392
+ function NotificationBanner() {
3393
+ if (!notificationVisible) return null;
3394
+ return /* @__PURE__ */ ReactEcs5.createElement(
3395
+ UiEntity5,
3396
+ {
3397
+ uiTransform: {
3398
+ positionType: "absolute",
3399
+ position: { top: 80 },
3400
+ width: 500,
3401
+ height: 60,
3402
+ padding: 16,
3403
+ alignSelf: "center",
3404
+ justifyContent: "center",
3405
+ alignItems: "center"
3406
+ },
3407
+ uiBackground: { color: Color46.create(0.1, 0.1, 0.15, 0.95) }
3408
+ },
3409
+ /* @__PURE__ */ ReactEcs5.createElement(
3410
+ Label5,
3411
+ {
3412
+ value: notificationText,
3413
+ fontSize: 18,
3414
+ color: Color46.create(0, 1, 1, 1),
3415
+ textAlign: "middle-center"
3416
+ }
3417
+ )
3418
+ );
3419
+ }
3420
+ function createStaticUI(client) {
3421
+ initNotificationSystem();
3422
+ return function StaticUI() {
3423
+ const currentScale = client.uiScale;
3424
+ const guideComponent = client.guideUI?.getComponent() ?? null;
3425
+ const chatComponent = client.chatUI?.getComponent() ?? null;
3426
+ const adminComponent = client.adminPanel?.getComponent() ?? null;
3427
+ return /* @__PURE__ */ ReactEcs5.createElement(
3428
+ UiEntity5,
3429
+ {
3430
+ key: `static-ui-root-${currentScale}`,
3431
+ uiTransform: {
3432
+ width: "100%",
3433
+ height: "100%",
3434
+ positionType: "absolute",
3435
+ flexDirection: "column",
3436
+ alignItems: "center"
3437
+ }
3438
+ },
3439
+ /* @__PURE__ */ ReactEcs5.createElement(NotificationBanner, null),
3440
+ guideComponent,
3441
+ chatComponent,
3442
+ adminComponent
3443
+ );
3444
+ };
3445
+ }
3446
+ function setupStaticUI(client) {
3447
+ const StaticUI = createStaticUI(client);
3448
+ ReactEcsRenderer.setUiRenderer(StaticUI);
3449
+ }
3450
+
3182
3451
  // src/StaticTVClient.ts
3452
+ import { VideoPlayer, Material } from "@dcl/sdk/ecs";
3183
3453
  var DEFAULT_BASE_URL = "https://thestatic.tv/api/v1/dcl";
3184
3454
  var KEY_TYPE_CHANNEL = "channel";
3185
3455
  var KEY_TYPE_SCENE = "scene";
@@ -3189,38 +3459,59 @@ var StaticTVClient = class {
3189
3459
  *
3190
3460
  * @param config Configuration options
3191
3461
  *
3192
- * @example
3462
+ * @example Free tier (session tracking only):
3193
3463
  * ```typescript
3194
- * let staticTV: StaticTVClient
3464
+ * const staticTV = new StaticTVClient({
3465
+ * apiKey: 'dcls_your_key_here'
3466
+ * })
3467
+ * ```
3468
+ *
3469
+ * @example Standard/Pro with video screen (SDK handles playback):
3470
+ * ```typescript
3471
+ * // Create your video screen
3472
+ * const screen = engine.addEntity()
3473
+ * Transform.create(screen, { position: Vector3.create(8, 3, 14), scale: Vector3.create(8, 4.5, 0.1) })
3474
+ * MeshRenderer.setPlane(screen)
3475
+ *
3476
+ * // SDK handles everything - Guide selection + Pro admin controls just work
3477
+ * const staticTV = new StaticTVClient({
3478
+ * apiKey: 'dcls_your_key_here',
3479
+ * videoScreen: screen // That's it! SDK manages video playback
3480
+ * })
3195
3481
  *
3196
3482
  * export function main() {
3197
- * // All keys use dcls_ prefix - features determined by subscription
3198
- * staticTV = new StaticTVClient({
3199
- * apiKey: 'dcls_your_key_here'
3200
- * })
3201
- * // Session tracking starts automatically!
3483
+ * staticTV.setupUI()
3202
3484
  * }
3203
3485
  * ```
3204
3486
  */
3205
3487
  constructor(config) {
3206
3488
  this._keyType = null;
3489
+ this._keyId = null;
3207
3490
  this._disabled = false;
3208
- this._fullFeaturesEnabled = false;
3491
+ this._tier = "free";
3492
+ this._standardFeaturesEnabled = false;
3209
3493
  this._proFeaturesEnabled = false;
3210
- /** Guide module - fetch channel lineup (full SDK only) */
3494
+ this._pendingProConfig = null;
3495
+ /** Guide module - fetch channel lineup (standard/pro tier) */
3211
3496
  this.guide = null;
3212
- /** Session module - track visitor sessions (all keys, null when disabled) */
3497
+ /** Session module - track visitor sessions (all tiers, null when disabled) */
3213
3498
  this.session = null;
3214
- /** Heartbeat module - track video watching (full SDK only) */
3499
+ /** Heartbeat module - track video watching (standard/pro tier) */
3215
3500
  this.heartbeat = null;
3216
- /** Interactions module - like/follow channels (full SDK only) */
3501
+ /** Interactions module - like/follow channels (standard/pro tier) */
3217
3502
  this.interactions = null;
3218
- /** Guide UI module - channel browser UI (full SDK only) */
3503
+ /** Guide UI module - channel browser UI (standard/pro tier) */
3219
3504
  this.guideUI = null;
3220
- /** Chat UI module - real-time chat UI (full SDK only) */
3505
+ /** Chat UI module - real-time chat UI (standard/pro tier) */
3221
3506
  this.chatUI = null;
3222
- /** Admin Panel module - Video/Mod tabs (pro SDK only) */
3507
+ /** Admin Panel module - Video/Mod tabs (pro tier only) */
3223
3508
  this.adminPanel = null;
3509
+ /** UI scale - fixed at 1.0. DCL's UI system auto-scales based on viewport. */
3510
+ this.uiScale = 1;
3511
+ // =============================================================================
3512
+ // --- VIDEO PLAYBACK (Internal handler for videoScreen) ---
3513
+ // =============================================================================
3514
+ this._currentVideoUrl = "";
3224
3515
  this.config = {
3225
3516
  autoStartSession: true,
3226
3517
  sessionHeartbeatInterval: 3e4,
@@ -3230,7 +3521,6 @@ var StaticTVClient = class {
3230
3521
  };
3231
3522
  this.baseUrl = config.baseUrl || DEFAULT_BASE_URL;
3232
3523
  if (!config.apiKey) {
3233
- console.log("[StaticTV] No apiKey provided - tracking disabled. Scene will load normally.");
3234
3524
  this._disabled = true;
3235
3525
  this._keyType = null;
3236
3526
  this.session = null;
@@ -3246,14 +3536,15 @@ var StaticTVClient = class {
3246
3536
  } else if (config.apiKey.startsWith("dcls_")) {
3247
3537
  this._keyType = KEY_TYPE_SCENE;
3248
3538
  } else {
3249
- console.log("[StaticTV] Invalid apiKey format (must start with dclk_ or dcls_) - tracking disabled. Scene will load normally.");
3539
+ console.warn("[TheStatic] Invalid API key format - get your key at thestatic.tv/dashboard");
3250
3540
  this._disabled = true;
3251
3541
  this._keyType = null;
3252
3542
  return;
3253
3543
  }
3254
3544
  this.session = new SessionModule(this);
3255
3545
  if (this._keyType === KEY_TYPE_CHANNEL) {
3256
- this._initFullModules();
3546
+ this._tier = "standard";
3547
+ this._initStandardModules();
3257
3548
  }
3258
3549
  if (this.config.autoStartSession) {
3259
3550
  fetchUserData().then(() => {
@@ -3269,6 +3560,10 @@ var StaticTVClient = class {
3269
3560
  }
3270
3561
  this.log(`StaticTVClient initialized (${this._keyType} mode)`);
3271
3562
  }
3563
+ /** Get the API base URL (for internal module use) */
3564
+ getBaseUrl() {
3565
+ return this.baseUrl;
3566
+ }
3272
3567
  /**
3273
3568
  * Get the key type (channel, scene, or null if disabled)
3274
3569
  */
@@ -3282,11 +3577,23 @@ var StaticTVClient = class {
3282
3577
  return this._disabled;
3283
3578
  }
3284
3579
  /**
3285
- * Check if this is a lite client (no full features)
3286
- * Returns true until session confirms sdkType is 'full'
3580
+ * Get the current SDK tier (free, standard, or pro)
3581
+ */
3582
+ get tier() {
3583
+ return this._tier;
3584
+ }
3585
+ /**
3586
+ * Check if this is a free tier client (session tracking only)
3587
+ * Returns true until session confirms a higher tier
3588
+ */
3589
+ get isFree() {
3590
+ return this._tier === "free";
3591
+ }
3592
+ /**
3593
+ * @deprecated Use `isFree` instead. Kept for backward compatibility.
3287
3594
  */
3288
3595
  get isLite() {
3289
- return !this._fullFeaturesEnabled;
3596
+ return this.isFree;
3290
3597
  }
3291
3598
  /**
3292
3599
  * Make an authenticated API request
@@ -3304,14 +3611,29 @@ var StaticTVClient = class {
3304
3611
  });
3305
3612
  }
3306
3613
  /**
3307
- * Log a message if debug is enabled
3614
+ * Log a debug message (only when debug: true)
3308
3615
  * @internal
3309
3616
  */
3310
3617
  log(message, ...args) {
3311
3618
  if (this.config.debug) {
3312
- console.log(`[StaticTV] ${message}`, ...args);
3619
+ console.log(`[TheStatic] ${message}`, ...args);
3313
3620
  }
3314
3621
  }
3622
+ /**
3623
+ * Log a warning (always shown)
3624
+ * @internal
3625
+ */
3626
+ warn(message) {
3627
+ console.warn(`[TheStatic] ${message}`);
3628
+ }
3629
+ /**
3630
+ * Log an error (always shown, user-friendly format)
3631
+ * @internal
3632
+ */
3633
+ error(message, err) {
3634
+ const errorDetail = err instanceof Error ? err.message : String(err || "");
3635
+ console.error(`[TheStatic] ${message}${errorDetail ? `: ${errorDetail}` : ""}`);
3636
+ }
3315
3637
  /**
3316
3638
  * Get the current configuration
3317
3639
  * @internal
@@ -3320,35 +3642,179 @@ var StaticTVClient = class {
3320
3642
  return this.config;
3321
3643
  }
3322
3644
  /**
3323
- * Initialize full feature modules (guide, heartbeat, interactions, UI)
3645
+ * Play a video on the configured videoScreen entity.
3646
+ * Called by Guide UI and Admin Panel.
3647
+ *
3648
+ * @param url Video URL to play
3649
+ */
3650
+ playVideo(url) {
3651
+ const screen = this.config.videoScreen;
3652
+ if (screen !== void 0) {
3653
+ this.log(`Playing video: ${url}`);
3654
+ this._currentVideoUrl = url;
3655
+ if (VideoPlayer.has(screen)) {
3656
+ VideoPlayer.deleteFrom(screen);
3657
+ }
3658
+ VideoPlayer.create(screen, {
3659
+ src: url,
3660
+ playing: true,
3661
+ volume: 1
3662
+ });
3663
+ Material.setBasicMaterial(screen, {
3664
+ texture: Material.Texture.Video({ videoPlayerEntity: screen })
3665
+ });
3666
+ if (this.guideUI) {
3667
+ const videos = this.guideUI.getVideos();
3668
+ const video = videos.find((v) => v.src === url);
3669
+ if (video) {
3670
+ this.guideUI.currentVideoId = video.id;
3671
+ }
3672
+ }
3673
+ }
3674
+ if (this.config.onVideoPlay) {
3675
+ this.config.onVideoPlay(url);
3676
+ }
3677
+ }
3678
+ /**
3679
+ * Stop video playback on the configured videoScreen entity.
3680
+ * Called by Admin Panel.
3681
+ */
3682
+ stopVideo() {
3683
+ const screen = this.config.videoScreen;
3684
+ if (screen !== void 0 && VideoPlayer.has(screen)) {
3685
+ this.log("Stopping video");
3686
+ VideoPlayer.getMutable(screen).playing = false;
3687
+ this._currentVideoUrl = "";
3688
+ if (this.guideUI) {
3689
+ this.guideUI.currentVideoId = null;
3690
+ }
3691
+ }
3692
+ if (this.config.onVideoStop) {
3693
+ this.config.onVideoStop();
3694
+ }
3695
+ }
3696
+ /**
3697
+ * Get the currently playing video URL
3698
+ */
3699
+ get currentVideoUrl() {
3700
+ return this._currentVideoUrl;
3701
+ }
3702
+ /**
3703
+ * Internal handler for Guide video selection
3324
3704
  * @internal
3325
3705
  */
3326
- _initFullModules() {
3327
- if (this._fullFeaturesEnabled) return;
3706
+ _handleGuideVideoSelect(video) {
3707
+ if (video.src) {
3708
+ this.playVideo(video.src);
3709
+ }
3710
+ }
3711
+ /**
3712
+ * Internal handler for broadcast messages
3713
+ * @internal
3714
+ */
3715
+ _handleBroadcast(text) {
3716
+ if (this.config.onBroadcast) {
3717
+ this.config.onBroadcast(text);
3718
+ } else {
3719
+ this.showNotification(text);
3720
+ }
3721
+ }
3722
+ /**
3723
+ * Initialize standard feature modules (guide, heartbeat, interactions, UI)
3724
+ * @internal
3725
+ */
3726
+ _initStandardModules() {
3727
+ if (this._standardFeaturesEnabled) return;
3328
3728
  this.guide = new GuideModule(this);
3329
3729
  this.heartbeat = new HeartbeatModule(this);
3330
3730
  this.interactions = new InteractionsModule(this);
3331
- this.guideUI = new GuideUIModule(this, this.config.guideUI);
3731
+ const guideConfig = {
3732
+ ...this.config.guideUI,
3733
+ onVideoSelect: (video) => {
3734
+ if (this.config.videoScreen !== void 0 || this.config.onVideoPlay) {
3735
+ this._handleGuideVideoSelect(video);
3736
+ }
3737
+ if (this.config.guideUI?.onVideoSelect) {
3738
+ this.config.guideUI.onVideoSelect(video);
3739
+ }
3740
+ }
3741
+ };
3742
+ this.guideUI = new GuideUIModule(this, guideConfig);
3332
3743
  this.chatUI = new ChatUIModule(this, this.config.chatUI);
3333
- this._fullFeaturesEnabled = true;
3744
+ this._standardFeaturesEnabled = true;
3334
3745
  this.chatUI.init().catch((err) => {
3335
3746
  this.log(`Chat init failed: ${err}`);
3336
3747
  });
3337
- this.log("Full features enabled (guide, chat, heartbeat, interactions)");
3748
+ this.log("Standard features enabled (guide, chat, heartbeat, interactions)");
3749
+ }
3750
+ /**
3751
+ * Initialize pro feature modules (admin panel)
3752
+ * @internal
3753
+ */
3754
+ _initProModules() {
3755
+ if (this._proFeaturesEnabled) return;
3756
+ const sceneId = this._pendingProConfig?.sceneId || this.config.sceneId || this._keyId || void 0;
3757
+ if (!sceneId) {
3758
+ this.log("Pro features: No sceneId and no keyId available - admin panel disabled");
3759
+ return;
3760
+ }
3761
+ const adminConfig = {
3762
+ sceneId,
3763
+ // Use internal handlers that manage videoScreen + call user callbacks
3764
+ onVideoPlay: (url) => this.playVideo(url),
3765
+ onVideoStop: () => this.stopVideo(),
3766
+ onBroadcast: (text) => this._handleBroadcast(text),
3767
+ onCommand: this.config.onCommand || this._pendingProConfig?.onCommand,
3768
+ // Merge other settings from enableProFeatures if provided
3769
+ ...this._pendingProConfig
3770
+ };
3771
+ this.adminPanel = new AdminPanelUIModule(this, adminConfig);
3772
+ this._proFeaturesEnabled = true;
3773
+ this.adminPanel.init().catch((err) => {
3774
+ this.log(`Admin panel init failed: ${err}`);
3775
+ });
3776
+ this.log(`Pro features enabled (admin panel) - sceneId: ${sceneId}`);
3777
+ }
3778
+ /**
3779
+ * Called by SessionModule when server returns the tier
3780
+ * Enables modules based on tier level
3781
+ * @internal
3782
+ */
3783
+ _enableFeaturesForTier(tier, keyId) {
3784
+ this._tier = tier;
3785
+ if (keyId) {
3786
+ this._keyId = keyId;
3787
+ }
3788
+ if (tier === "standard" || tier === "pro") {
3789
+ this._initStandardModules();
3790
+ }
3791
+ if (tier === "pro") {
3792
+ const hasVideoConfig = this.config.videoScreen !== void 0 || this.config.onVideoPlay !== void 0 || this.config.sceneId !== void 0 || this._pendingProConfig !== null;
3793
+ if (hasVideoConfig) {
3794
+ this._initProModules();
3795
+ } else {
3796
+ this.log("Pro tier detected but no video config - call enableProFeatures() or set videoScreen to enable admin panel");
3797
+ }
3798
+ }
3338
3799
  }
3339
3800
  /**
3340
- * Called by SessionModule when server confirms sdkType is 'full'
3341
- * Enables guide, chat, heartbeat, and interactions modules
3801
+ * @deprecated Use `_enableFeaturesForTier` instead
3342
3802
  * @internal
3343
3803
  */
3344
3804
  _enableFullFeatures() {
3345
- this._initFullModules();
3805
+ this._enableFeaturesForTier("standard");
3346
3806
  }
3347
3807
  /**
3348
- * Check if full features are enabled (server confirmed sdkType: 'full')
3808
+ * Check if standard features are enabled (guide, chat, etc.)
3809
+ */
3810
+ get hasStandardFeatures() {
3811
+ return this._standardFeaturesEnabled;
3812
+ }
3813
+ /**
3814
+ * @deprecated Use `hasStandardFeatures` instead
3349
3815
  */
3350
3816
  get hasFullFeatures() {
3351
- return this._fullFeaturesEnabled;
3817
+ return this._standardFeaturesEnabled;
3352
3818
  }
3353
3819
  /**
3354
3820
  * Check if pro features are enabled (admin panel)
@@ -3357,64 +3823,121 @@ var StaticTVClient = class {
3357
3823
  return this._proFeaturesEnabled;
3358
3824
  }
3359
3825
  /**
3360
- * Get the SDK type (lite or full) - only available after session starts
3826
+ * @deprecated Use `tier` instead
3361
3827
  */
3362
3828
  get sdkType() {
3363
- return this.session?.sdkType || "lite";
3829
+ return this._tier === "free" ? "lite" : "full";
3364
3830
  }
3365
3831
  /**
3366
- * Enable Pro features (Admin Panel with Video + Mod tabs)
3367
- * Call this after creating the client to add admin panel functionality.
3832
+ * Configure Pro features (Admin Panel with Video + Mod tabs)
3833
+ *
3834
+ * **OPTIONAL**: If you set `videoScreen` in the constructor, you don't need
3835
+ * to call this method - the Admin Panel will auto-enable with sensible defaults.
3836
+ *
3837
+ * Use this method only if you need:
3838
+ * - Custom scene tab UI
3839
+ * - Custom panel title/styling
3840
+ * - Advanced command handlers
3368
3841
  *
3369
3842
  * @param config Admin panel configuration
3370
3843
  *
3371
- * @example
3844
+ * @example Using videoScreen (recommended - no enableProFeatures needed):
3372
3845
  * ```typescript
3373
- * const staticTV = new StaticTVClient({ apiKey: 'dcls_...' })
3846
+ * const staticTV = new StaticTVClient({
3847
+ * apiKey: 'dcls_...',
3848
+ * videoScreen: myScreen // Admin Panel auto-enabled for Pro tier!
3849
+ * })
3850
+ * ```
3374
3851
  *
3375
- * // Enable admin panel
3852
+ * @example Advanced: Custom scene tabs
3853
+ * ```typescript
3376
3854
  * staticTV.enableProFeatures({
3377
- * sceneId: 'my-scene',
3378
3855
  * title: 'MY SCENE ADMIN',
3379
- * onVideoPlay: (url) => videoPlayer.play(url),
3380
- * onVideoStop: () => videoPlayer.stop(),
3381
- * onBroadcast: (text) => showNotification(text)
3856
+ * sceneTabs: [{ label: 'LIGHTS', id: 'lights', render: () => <LightsTab /> }]
3382
3857
  * })
3383
3858
  * ```
3384
3859
  */
3385
- enableProFeatures(config) {
3860
+ enableProFeatures(config = {}) {
3386
3861
  if (this._proFeaturesEnabled) {
3387
3862
  this.log("Pro features already enabled");
3388
3863
  return;
3389
3864
  }
3390
- this.adminPanel = new AdminPanelUIModule(this, config);
3391
- this._proFeaturesEnabled = true;
3392
- this.adminPanel.init().catch((err) => {
3393
- this.log(`Admin panel init failed: ${err}`);
3394
- });
3395
- this.log("Pro features enabled (admin panel)");
3865
+ this._pendingProConfig = config;
3866
+ if (this._tier === "pro") {
3867
+ this._initProModules();
3868
+ } else {
3869
+ this.log("Pro features configured - will enable when Pro tier is confirmed");
3870
+ }
3871
+ }
3872
+ /**
3873
+ * Register a custom scene tab for the admin panel (Pro tier)
3874
+ * Must call enableProFeatures() first.
3875
+ *
3876
+ * @param tab The tab definition with label, id, and render function
3877
+ *
3878
+ * @example
3879
+ * ```typescript
3880
+ * staticTV.registerSceneTab({
3881
+ * label: 'LIGHTS',
3882
+ * id: 'lights',
3883
+ * render: () => <MyLightsControls />
3884
+ * })
3885
+ * ```
3886
+ */
3887
+ registerSceneTab(tab) {
3888
+ if (!this.adminPanel) {
3889
+ this.log("Cannot register scene tab - call enableProFeatures() first");
3890
+ return;
3891
+ }
3892
+ this.adminPanel.registerSceneTab(tab);
3396
3893
  }
3397
3894
  /**
3398
- * Register a custom scene tab for the admin panel (Full/Custom tier)
3399
- * Must call enableProFeatures() first.
3895
+ * Close Admin/Guide panels (they share the same screen space)
3896
+ * Chat is independent and stays open.
3897
+ * @param except The panel that should stay open: 'admin' | 'guide'
3898
+ */
3899
+ closeOtherPanels(except) {
3900
+ if (except !== "guide" && this.guideUI?.isVisible) {
3901
+ this.guideUI.hide();
3902
+ }
3903
+ if (except !== "admin" && this.adminPanel?.isOpen) {
3904
+ this.adminPanel.hide();
3905
+ }
3906
+ }
3907
+ /**
3908
+ * Set up the UI renderer for all SDK panels
3909
+ * Call this in your scene's main() function to render Guide, Chat, Admin panels.
3910
+ * No need to create your own ui.tsx - the SDK handles everything.
3911
+ *
3912
+ * @example
3913
+ * ```typescript
3914
+ * const staticTV = new StaticTVClient({ apiKey: 'dcls_...' })
3915
+ *
3916
+ * export function main() {
3917
+ * staticTV.setupUI()
3918
+ * // That's it! All panels will render automatically
3919
+ * }
3920
+ * ```
3921
+ */
3922
+ setupUI() {
3923
+ setupStaticUI(this);
3924
+ this.log("UI renderer initialized");
3925
+ }
3926
+ /**
3927
+ * Show a notification message on screen
3928
+ * Works with both SDK-rendered UI and custom UI setups.
3400
3929
  *
3401
- * @param tab The tab definition with label, id, and render function
3930
+ * @param message The message to display
3931
+ * @param durationMs How long to show (default 5000ms)
3402
3932
  *
3403
3933
  * @example
3404
3934
  * ```typescript
3405
- * staticTV.registerSceneTab({
3406
- * label: 'LIGHTS',
3407
- * id: 'lights',
3408
- * render: () => <MyLightsControls />
3409
- * })
3935
+ * staticTV.showNotification('Stream started!')
3936
+ * staticTV.showNotification('Custom message', 10000) // 10 seconds
3410
3937
  * ```
3411
3938
  */
3412
- registerSceneTab(tab) {
3413
- if (!this.adminPanel) {
3414
- this.log("Cannot register scene tab - call enableProFeatures() first");
3415
- return;
3416
- }
3417
- this.adminPanel.registerSceneTab(tab);
3939
+ showNotification(message, durationMs = 5e3) {
3940
+ showNotification(message, durationMs);
3418
3941
  }
3419
3942
  /**
3420
3943
  * Cleanup when done (call before scene unload)
@@ -3449,5 +3972,7 @@ export {
3449
3972
  StaticTVClient,
3450
3973
  fetchUserData,
3451
3974
  getPlayerDisplayName,
3452
- getPlayerWallet
3975
+ getPlayerWallet,
3976
+ setupStaticUI,
3977
+ showNotification
3453
3978
  };