@four-leaf-studios/rl-overlay 1.0.5 → 1.1.1

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.
Files changed (53) hide show
  1. package/.github/copilot-instructions.md +70 -0
  2. package/dist/index.cjs.js +298 -10599
  3. package/dist/index.cjs.js.map +1 -1
  4. package/dist/index.esm.js +328 -10612
  5. package/dist/index.esm.js.map +1 -1
  6. package/dist/types/Overlay.d.ts +5 -6
  7. package/dist/types/OverlaySlot.d.ts +9 -0
  8. package/dist/types/index.d.ts +2 -0
  9. package/dist/types/registry.d.ts +2 -0
  10. package/package.json +12 -5
  11. package/src/Overlay.tsx +47 -41
  12. package/src/OverlaySlot.tsx +35 -0
  13. package/src/Player.tsx +1 -0
  14. package/src/PlayerBoost.tsx +4 -1
  15. package/src/Scoreboard.tsx +1 -0
  16. package/src/ScoreboardGameBox.tsx +2 -4
  17. package/src/ScoreboardSeriesBox.tsx +2 -3
  18. package/src/ScoreboardTeam.tsx +1 -0
  19. package/src/StatItem.tsx +4 -1
  20. package/src/TargetBoost.tsx +1 -0
  21. package/src/TargetPlayer.tsx +1 -0
  22. package/src/TargetPlayerLocation.tsx +1 -0
  23. package/src/TargetPlayerStats.tsx +1 -1
  24. package/src/Team.tsx +1 -0
  25. package/src/Timer.tsx +7 -4
  26. package/src/css/Florida_Tech.css +696 -0
  27. package/src/css/Horizon_Hues.css +618 -0
  28. package/src/css/Neo_Glass.css +674 -0
  29. package/src/css/Obsidian_Glide.css +646 -0
  30. package/src/css/RLCS_2024.css +566 -0
  31. package/src/css/RLCS_2025.css +524 -0
  32. package/src/css/Shaded_blocks.css +594 -0
  33. package/src/index.ts +2 -0
  34. package/src/registry.ts +19 -0
  35. package/src/types.d.ts +20 -0
  36. package/test-overlay/package-lock.json +2697 -0
  37. package/test-overlay/package.json +3 -0
  38. package/test-overlay/public/mock-css.css +259 -386
  39. package/test-overlay/src/App.jsx +78 -28
  40. package/tests/BroadcastContext.test.tsx +41 -0
  41. package/tests/Overlay.test.tsx +106 -0
  42. package/tests/OverlaySlot.test.tsx +79 -0
  43. package/tests/PlayerBoost.test.tsx +47 -0
  44. package/tests/Replay.test.tsx +29 -0
  45. package/tests/ScoreboardGameBox.test.tsx +35 -0
  46. package/tests/ScoreboardSeriesBox.test.tsx +48 -0
  47. package/tests/StatItem.test.tsx +33 -0
  48. package/tests/__mocks__/@four-leaf-studios/rl-socket-hook.ts +10 -0
  49. package/tests/fixtures.ts +96 -0
  50. package/tests/registry.test.ts +27 -0
  51. package/tests/setup.ts +1 -0
  52. package/tsconfig.json +16 -20
  53. package/vitest.config.ts +9 -0
@@ -1,15 +1,14 @@
1
- import React, { ReactNode } from "react";
2
- import type { Broadcast } from "./types";
3
- import { CSSJSON } from "./hooks/useOverlayStyles";
1
+ import React from "react";
2
+ import type { Broadcast, OverlayObject } from "./types";
4
3
  import "./css/reset.css";
5
4
  export type OverlayProps = {
6
5
  broadcast: Broadcast;
7
- styles?: string | CSSJSON;
8
- children?: ReactNode;
6
+ overlay: OverlayObject;
9
7
  preview?: boolean;
8
+ renderSlot?: (comp: OverlayObject["components"][number], Comp: any) => React.ReactNode;
10
9
  };
11
10
  export declare const Overlay: {
12
- ({ broadcast, styles, preview, children, }: OverlayProps): React.JSX.Element;
11
+ ({ broadcast, overlay, preview, renderSlot, }: OverlayProps): React.JSX.Element;
13
12
  Scoreboard: () => React.JSX.Element | null;
14
13
  Teams: () => React.JSX.Element;
15
14
  TargetPlayer: React.MemoExoticComponent<() => React.JSX.Element | null>;
@@ -0,0 +1,9 @@
1
+ import React, { type ReactNode } from "react";
2
+ import type { OverlayComponentData } from "./types";
3
+ type OverlaySlotProps = {
4
+ component: OverlayComponentData;
5
+ children: ReactNode;
6
+ } & React.HTMLAttributes<HTMLDivElement>;
7
+ export declare const OverlaySlot: ({ component, children, ...rest }: OverlaySlotProps) => React.JSX.Element;
8
+ declare const _default: React.MemoExoticComponent<({ component, children, ...rest }: OverlaySlotProps) => React.JSX.Element>;
9
+ export default _default;
@@ -13,5 +13,7 @@ export * from "./TargetPlayerLocation";
13
13
  export * from "./TargetPlayerStats";
14
14
  export * from "./Team";
15
15
  export * from "./Timer";
16
+ export * from "./OverlaySlot";
16
17
  export * from "@four-leaf-studios/rl-socket-hook";
17
18
  export * from "./hooks/index";
19
+ export * from "./registry";
@@ -0,0 +1,2 @@
1
+ import React from "react";
2
+ export declare const componentRegistry: Record<string, React.ComponentType<any>>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@four-leaf-studios/rl-overlay",
3
- "version": "1.0.5",
3
+ "version": "1.1.1",
4
4
  "main": "dist/index.cjs.js",
5
5
  "module": "dist/index.esm.js",
6
6
  "types": "dist/types/index.d.ts",
@@ -9,6 +9,8 @@
9
9
  },
10
10
  "scripts": {
11
11
  "build": "rollup -c",
12
+ "test": "vitest run",
13
+ "test:watch": "vitest",
12
14
  "prepare": "npm run build"
13
15
  },
14
16
  "repository": {
@@ -23,6 +25,8 @@
23
25
  },
24
26
  "homepage": "https://github.com/Four-Leaf-Studios/rl-overlay#readme",
25
27
  "peerDependencies": {
28
+ "@four-leaf-studios/rl-socket-hook": "^1.2.4",
29
+ "framer-motion": "^12.9.4",
26
30
  "react": "^18 || ^19",
27
31
  "react-dom": "^18 || ^19"
28
32
  },
@@ -33,18 +37,21 @@
33
37
  "@rollup/plugin-replace": "^6.0.2",
34
38
  "@rollup/plugin-typescript": "^12.1.2",
35
39
  "@rollup/plugin-url": "^8.0.2",
40
+ "@testing-library/jest-dom": "^6.9.1",
41
+ "@testing-library/react": "^16.3.2",
36
42
  "@types/postcss-js": "^4.0.4",
37
43
  "@types/react": "^19.1.2",
44
+ "@types/react-dom": "^19.2.3",
45
+ "jsdom": "^29.1.1",
38
46
  "postcss-url": "^10.1.3",
39
47
  "rollup": "^4.40.1",
40
48
  "rollup-plugin-copy": "^3.5.0",
41
49
  "rollup-plugin-peer-deps-external": "^2.2.4",
42
50
  "rollup-plugin-postcss": "^4.0.2",
43
- "typescript": "^5.8.3"
51
+ "typescript": "^5.8.3",
52
+ "vitest": "^4.1.6"
44
53
  },
45
54
  "dependencies": {
46
- "@four-leaf-studios/rl-socket-hook": "^1.2.4",
47
- "@rysh/json-to-css": "^1.0.0",
48
- "framer-motion": "^12.9.4"
55
+ "@rysh/json-to-css": "^1.0.0"
49
56
  }
50
57
  }
package/src/Overlay.tsx CHANGED
@@ -1,10 +1,10 @@
1
- // src/Overlay.tsx
2
1
  "use client";
3
2
 
4
- import React, { ReactNode } from "react";
3
+ import React from "react";
5
4
  import { RLProvider, WebsocketData } from "@four-leaf-studios/rl-socket-hook";
6
5
  import { BroadcastProvider } from "./context/BroadcastContext";
7
- import type { Broadcast } from "./types";
6
+ import type { Broadcast, OverlayObject } from "./types";
7
+ import { useOverlayStyles } from "./hooks/useOverlayStyles";
8
8
 
9
9
  import Scoreboard from "./Scoreboard";
10
10
  import Teams from "./Teams";
@@ -12,37 +12,41 @@ import TargetPlayer from "./TargetPlayer";
12
12
  import TargetBoost from "./TargetBoost";
13
13
  import Replay from "./Replay";
14
14
 
15
- import { useOverlayStyles, CSSJSON } from "./hooks/useOverlayStyles";
16
15
  import "./css/reset.css";
16
+ import { componentRegistry } from "./registry";
17
+ import OverlaySlot from "./OverlaySlot";
17
18
 
18
19
  export type OverlayProps = {
19
20
  broadcast: Broadcast;
20
- styles?: string | CSSJSON;
21
- children?: ReactNode;
21
+ overlay: OverlayObject;
22
22
  preview?: boolean;
23
+ renderSlot?: (
24
+ comp: OverlayObject["components"][number],
25
+ Comp: any,
26
+ ) => React.ReactNode;
23
27
  };
24
28
 
25
29
  export const Overlay = ({
26
30
  broadcast,
27
- styles,
31
+ overlay,
28
32
  preview,
29
- children,
33
+ renderSlot,
30
34
  }: OverlayProps) => {
31
- useOverlayStyles(broadcast, styles);
35
+ const { components } = overlay;
36
+
37
+ // Inject team color CSS variables (--team-left-primary, etc.)
38
+ useOverlayStyles(broadcast);
39
+
40
+ // Collect CSS into one block
41
+ const cssString = components.map((c) => c.css).join("\n");
32
42
 
33
43
  return (
34
44
  <BroadcastProvider broadcast={broadcast}>
35
45
  <RLProvider>
36
- <div
37
- style={{
38
- display: "flex",
39
- flexDirection: "row",
40
- width: "100%",
41
- height: "100%",
42
- maxWidth: "100%",
43
- maxHeight: "100%",
44
- }}
45
- >
46
+ <div className="overlay-wrapper">
47
+ {/* Inject all component CSS */}
48
+ <style>{cssString}</style>
49
+
46
50
  {/* The overlay itself */}
47
51
  <div
48
52
  className={`overlay ${preview ? "testing" : ""}`}
@@ -56,31 +60,33 @@ export const Overlay = ({
56
60
  display: "block",
57
61
  }}
58
62
  >
59
- {children ?? (
60
- <>
61
- <Scoreboard />
62
- <Teams />
63
- <TargetPlayer />
64
- <TargetBoost />
65
- <Replay />
66
- </>
67
- )}
63
+ {components.map((comp) => {
64
+ const Comp = componentRegistry[comp.code_id];
65
+ if (!Comp) {
66
+ return (
67
+ <div key={comp.id} style={{ color: "red", fontSize: "14px" }}>
68
+ Missing component: {comp.code_id}
69
+ </div>
70
+ );
71
+ }
72
+
73
+ if (renderSlot) {
74
+ // delegate to editor if provided
75
+ return renderSlot(comp, Comp);
76
+ }
77
+
78
+ // default (non-edit mode)
79
+ return (
80
+ <OverlaySlot key={comp.id} component={comp}>
81
+ <Comp {...comp} />
82
+ </OverlaySlot>
83
+ );
84
+ })}
68
85
  </div>
69
86
 
70
87
  {/* Preview data panel on the right */}
71
88
  {preview && (
72
- <div
73
- className="testing-data"
74
- style={{
75
- flexGrow: 1,
76
- marginLeft: "1rem",
77
- overflowY: "auto",
78
- backgroundColor: "rgba(0,0,0,0.8)",
79
- color: "#black",
80
- fontFamily: "monospace",
81
- maxHeight: "1080px",
82
- }}
83
- >
89
+ <div className="testing-data">
84
90
  <WebsocketData />
85
91
  </div>
86
92
  )}
@@ -90,7 +96,7 @@ export const Overlay = ({
90
96
  );
91
97
  };
92
98
 
93
- // expose slots
99
+ // expose slots (optional if you want named exports)
94
100
  Overlay.Scoreboard = Scoreboard;
95
101
  Overlay.Teams = Teams;
96
102
  Overlay.TargetPlayer = TargetPlayer;
@@ -0,0 +1,35 @@
1
+ import React, { memo, type ReactNode } from "react";
2
+ import type { OverlayComponentData } from "./types";
3
+
4
+ type OverlaySlotProps = {
5
+ component: OverlayComponentData;
6
+ children: ReactNode;
7
+ } & React.HTMLAttributes<HTMLDivElement>; // ✅ allow div props
8
+
9
+ export const OverlaySlot = ({
10
+ component,
11
+ children,
12
+ ...rest
13
+ }: OverlaySlotProps) => {
14
+ const { id, name, position } = component;
15
+
16
+ return (
17
+ <div
18
+ className="overlay-slot"
19
+ data-component-id={id}
20
+ data-component-name={name}
21
+ style={{
22
+ position: "absolute",
23
+ top: position?.top ?? 0,
24
+ left: position?.left ?? 0,
25
+ width: position?.width ?? "auto",
26
+ height: position?.height ?? "auto",
27
+ }}
28
+ {...rest}
29
+ >
30
+ {children}
31
+ </div>
32
+ );
33
+ };
34
+
35
+ export default memo(OverlaySlot);
package/src/Player.tsx CHANGED
@@ -13,6 +13,7 @@ export const Player = ({ player }: Props) => {
13
13
  return (
14
14
  <motion.div
15
15
  key={player.id}
16
+ data-component-id="Player"
16
17
  className={`player_box ${modifier}_player_box`}
17
18
  initial={{ opacity: 0, y: 20 }}
18
19
  animate={{ opacity: 1, y: 0 }}
@@ -12,7 +12,10 @@ export const PlayerBoost = ({ team, boost }: Props) => {
12
12
  const modifier = team === 0 ? "left" : "right";
13
13
 
14
14
  return (
15
- <div className={`boost_meter ${modifier}_boost_meter`}>
15
+ <div
16
+ data-component-id="PlayerBoost"
17
+ className={`boost_meter ${modifier}_boost_meter`}
18
+ >
16
19
  <motion.div
17
20
  className={`boost_meter_bar ${modifier}_boost_meter_bar`}
18
21
  initial={{ width: "0%" }}
@@ -24,6 +24,7 @@ export const Scoreboard = () => {
24
24
  <AnimatePresence mode="wait">
25
25
  <motion.div
26
26
  key={broadcast.id} // Ensures animation runs when broadcast updates
27
+ data-component-id="Scoreboard"
27
28
  className="scoreboard_box"
28
29
  initial={{ y: -100, opacity: 0 }}
29
30
  animate={{ y: 0, opacity: 1 }}
@@ -1,6 +1,4 @@
1
- import React from "react";
2
-
3
- import { useMemo } from "react";
1
+ import React, { useMemo } from "react";
4
2
  import { Broadcast } from "./types";
5
3
 
6
4
  interface ScoreboardGameBoxProps {
@@ -18,7 +16,7 @@ export const ScoreboardGameBox: React.FC<ScoreboardGameBoxProps> = ({
18
16
  }, [broadcast.teams]);
19
17
 
20
18
  return (
21
- <div className="game_box">
19
+ <div data-component-id="ScoreboardGameBox" className="game_box">
22
20
  Game {gameNumber} - BO {broadcast.series_number}
23
21
  </div>
24
22
  );
@@ -1,6 +1,4 @@
1
- import React from "react";
2
-
3
- import { memo } from "react";
1
+ import React, { memo } from "react";
4
2
  import { Team } from "./types";
5
3
 
6
4
  interface ScoreboardSeriesBoxProps {
@@ -25,6 +23,7 @@ export const ScoreboardSeriesBoxComponent: React.FC<
25
23
 
26
24
  return (
27
25
  <div
26
+ data-component-id="ScoreboardSeriesBox"
28
27
  className={`series_box ${modifier}_series_box`}
29
28
  style={{ "--team-color": teamColor } as React.CSSProperties}
30
29
  >
@@ -18,6 +18,7 @@ export const ScoreboardTeam: React.FC<ScoreboardTeamProps> = ({ team }) => {
18
18
 
19
19
  return (
20
20
  <div
21
+ data-component-id="ScoreboardTeam"
21
22
  className={`scoreboard_team_box ${modifier}_scoreboard_team_box`}
22
23
  style={{ "--team-color": teamColor } as React.CSSProperties}
23
24
  >
package/src/StatItem.tsx CHANGED
@@ -22,7 +22,10 @@ export const StatItem = <K extends PrimitiveKeys<PlayerState>>({
22
22
  label,
23
23
  value,
24
24
  }: StatItemProps<K>) => (
25
- <li className={`stat_box_statistic stat_box_statistic_player_${id}`}>
25
+ <li
26
+ data-component-id="StatItem"
27
+ className={`stat_box_statistic stat_box_statistic_player_${id}`}
28
+ >
26
29
  <span className="stat_box_statistic_name">{label}</span>
27
30
  <span className="stat_box_statistic_value">{value}</span>
28
31
  </li>
@@ -17,6 +17,7 @@ export const TargetBoost = () => {
17
17
  return (
18
18
  <AnimatePresence mode="wait">
19
19
  <motion.div
20
+ data-component-id="TargetBoost"
20
21
  key={targetPlayer.id}
21
22
  className={`target_boost ${modifier}_target_boost`}
22
23
  initial={{ x: 400, opacity: 0 }}
@@ -28,6 +28,7 @@ export const TargetPlayer = () => {
28
28
  return (
29
29
  <AnimatePresence mode="wait">
30
30
  <motion.div
31
+ data-component-id="TargetPlayer"
31
32
  key={targetPlayer.id}
32
33
  className={`stat_box ${modifier}_stat_box`}
33
34
  style={{ "--team-color": teamColor } as React.CSSProperties}
@@ -9,6 +9,7 @@ type Props = {
9
9
  export const TargetPlayerLocation = ({ location }: Props) => {
10
10
  return (
11
11
  <motion.div
12
+ data-component-id="TargetPlayerLocation"
12
13
  className="stat_box_statistic stat_box_statistic_player_location"
13
14
  initial={{ opacity: 0, y: 10 }}
14
15
  animate={{ opacity: 1, y: 0 }}
@@ -9,7 +9,7 @@ type Props = {
9
9
  };
10
10
 
11
11
  export const TargetPlayerStats = ({ targetPlayer }: Props) => (
12
- <ul className="stat_box_statistics">
12
+ <ul data-component-id="TargetPlayerStats" className="stat_box_statistics">
13
13
  <StatItem id="id" label="ID" value={targetPlayer.id} />
14
14
  <StatItem id="team" label="Team" value={targetPlayer.team} />
15
15
  <StatItem id="score" label="Score" value={targetPlayer.score} />
package/src/Team.tsx CHANGED
@@ -25,6 +25,7 @@ export const Team = ({ id }: Props) => {
25
25
 
26
26
  return (
27
27
  <motion.div
28
+ data-component-id="Team"
28
29
  className={`team_box ${modifier}_team_box`}
29
30
  initial={{ x: isLeft ? -300 : 300, opacity: 0 }}
30
31
  animate={{ x: 0, opacity: 1 }}
package/src/Timer.tsx CHANGED
@@ -1,12 +1,11 @@
1
1
  import { useEventSelector } from "@four-leaf-studios/rl-socket-hook";
2
- import React from "react";
3
- import { memo } from "react";
2
+ import React, { memo } from "react";
4
3
 
5
4
  const TimerComponent: React.FC = () => {
6
5
  // Add proper null checks to prevent "Cannot read properties of undefined" error
7
6
  const time_seconds = useEventSelector(
8
7
  "game:update_state",
9
- (state) => state?.game?.time_seconds
8
+ (state) => state?.game?.time_seconds,
10
9
  );
11
10
 
12
11
  // Return null if time_seconds is not available
@@ -16,7 +15,11 @@ const TimerComponent: React.FC = () => {
16
15
  const seconds = time_seconds % 60;
17
16
  const formattedTime = `${minutes}:${seconds < 10 ? "0" : ""}${seconds}`;
18
17
 
19
- return <span className="time_box">{formattedTime}</span>;
18
+ return (
19
+ <span data-component-id="Timer" className="time_box">
20
+ {formattedTime}
21
+ </span>
22
+ );
20
23
  };
21
24
 
22
25
  export const Timer = memo(TimerComponent);