@four-leaf-studios/rl-overlay 1.0.4 → 1.1.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.
Files changed (51) hide show
  1. package/.github/copilot-instructions.md +70 -0
  2. package/dist/index.cjs.js +271 -10567
  3. package/dist/index.cjs.js.map +1 -1
  4. package/dist/index.esm.js +300 -10579
  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 +43 -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 +15 -3
  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/public/mock-css.css +294 -372
  37. package/test-overlay/src/App.jsx +28 -28
  38. package/tests/BroadcastContext.test.tsx +41 -0
  39. package/tests/Overlay.test.tsx +106 -0
  40. package/tests/OverlaySlot.test.tsx +79 -0
  41. package/tests/PlayerBoost.test.tsx +47 -0
  42. package/tests/Replay.test.tsx +29 -0
  43. package/tests/ScoreboardGameBox.test.tsx +35 -0
  44. package/tests/ScoreboardSeriesBox.test.tsx +48 -0
  45. package/tests/StatItem.test.tsx +33 -0
  46. package/tests/__mocks__/@four-leaf-studios/rl-socket-hook.ts +10 -0
  47. package/tests/fixtures.ts +96 -0
  48. package/tests/registry.test.ts +27 -0
  49. package/tests/setup.ts +1 -0
  50. package/tsconfig.json +16 -20
  51. 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.4",
3
+ "version": "1.1.0",
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,9 @@
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";
8
7
 
9
8
  import Scoreboard from "./Scoreboard";
10
9
  import Teams from "./Teams";
@@ -12,37 +11,38 @@ import TargetPlayer from "./TargetPlayer";
12
11
  import TargetBoost from "./TargetBoost";
13
12
  import Replay from "./Replay";
14
13
 
15
- import { useOverlayStyles, CSSJSON } from "./hooks/useOverlayStyles";
16
14
  import "./css/reset.css";
15
+ import { componentRegistry } from "./registry";
16
+ import OverlaySlot from "./OverlaySlot";
17
17
 
18
18
  export type OverlayProps = {
19
19
  broadcast: Broadcast;
20
- styles?: string | CSSJSON;
21
- children?: ReactNode;
20
+ overlay: OverlayObject;
22
21
  preview?: boolean;
22
+ renderSlot?: (
23
+ comp: OverlayObject["components"][number],
24
+ Comp: any
25
+ ) => React.ReactNode;
23
26
  };
24
27
 
25
28
  export const Overlay = ({
26
29
  broadcast,
27
- styles,
30
+ overlay,
28
31
  preview,
29
- children,
32
+ renderSlot,
30
33
  }: OverlayProps) => {
31
- useOverlayStyles(broadcast, styles);
34
+ const { components } = overlay;
35
+
36
+ // Collect CSS into one block
37
+ const cssString = components.map((c) => c.css).join("\n");
32
38
 
33
39
  return (
34
40
  <BroadcastProvider broadcast={broadcast}>
35
41
  <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
- >
42
+ <div className="overlay-wrapper">
43
+ {/* Inject all component CSS */}
44
+ <style>{cssString}</style>
45
+
46
46
  {/* The overlay itself */}
47
47
  <div
48
48
  className={`overlay ${preview ? "testing" : ""}`}
@@ -56,31 +56,33 @@ export const Overlay = ({
56
56
  display: "block",
57
57
  }}
58
58
  >
59
- {children ?? (
60
- <>
61
- <Scoreboard />
62
- <Teams />
63
- <TargetPlayer />
64
- <TargetBoost />
65
- <Replay />
66
- </>
67
- )}
59
+ {components.map((comp) => {
60
+ const Comp = componentRegistry[comp.code_id];
61
+ if (!Comp) {
62
+ return (
63
+ <div key={comp.id} style={{ color: "red", fontSize: "14px" }}>
64
+ Missing component: {comp.code_id}
65
+ </div>
66
+ );
67
+ }
68
+
69
+ if (renderSlot) {
70
+ // delegate to editor if provided
71
+ return renderSlot(comp, Comp);
72
+ }
73
+
74
+ // default (non-edit mode)
75
+ return (
76
+ <OverlaySlot key={comp.id} component={comp}>
77
+ <Comp {...comp} />
78
+ </OverlaySlot>
79
+ );
80
+ })}
68
81
  </div>
69
82
 
70
83
  {/* Preview data panel on the right */}
71
84
  {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
- >
85
+ <div className="testing-data">
84
86
  <WebsocketData />
85
87
  </div>
86
88
  )}
@@ -90,7 +92,7 @@ export const Overlay = ({
90
92
  );
91
93
  };
92
94
 
93
- // expose slots
95
+ // expose slots (optional if you want named exports)
94
96
  Overlay.Scoreboard = Scoreboard;
95
97
  Overlay.Teams = Teams;
96
98
  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 }}
@@ -1,3 +1,5 @@
1
+ // src/components/TargetPlayer.tsx
2
+
1
3
  import { AnimatePresence, motion } from "framer-motion";
2
4
  import React, { memo } from "react";
3
5
  import { useBroadcast } from "./context/BroadcastContext";
@@ -9,21 +11,31 @@ export const TargetPlayer = () => {
9
11
  const broadcast = useBroadcast();
10
12
  const targetPlayer = useTargetPlayer();
11
13
 
12
- if (!targetPlayer || !broadcast) return null;
14
+ // Safeguard conditions
15
+ if (
16
+ !targetPlayer ||
17
+ !broadcast ||
18
+ !broadcast.teams ||
19
+ !broadcast.teams[targetPlayer.team]
20
+ ) {
21
+ return null;
22
+ }
13
23
 
14
- const teamColor = broadcast.teams[targetPlayer.team].color?.primary_color;
24
+ const teamData = broadcast.teams[targetPlayer.team];
25
+ const teamColor = teamData?.color?.primary_color ?? "#ffffff";
15
26
  const modifier = targetPlayer.team === 0 ? "left" : "right";
16
27
 
17
28
  return (
18
29
  <AnimatePresence mode="wait">
19
30
  <motion.div
31
+ data-component-id="TargetPlayer"
20
32
  key={targetPlayer.id}
21
33
  className={`stat_box ${modifier}_stat_box`}
22
34
  style={{ "--team-color": teamColor } as React.CSSProperties}
23
35
  initial={{ x: modifier === "left" ? -300 : 300, opacity: 0 }}
24
36
  animate={{ x: 0, opacity: 1 }}
25
37
  exit={{ x: modifier === "left" ? -300 : 300, opacity: 0 }}
26
- transition={{ duration: 0.8, ease: [0.77, 0, 0.175, 1] }} // Quartic easing equivalent
38
+ transition={{ duration: 0.8, ease: [0.77, 0, 0.175, 1] }}
27
39
  >
28
40
  <motion.div
29
41
  className={`stat_box_player ${modifier}_stat_box_player`}
@@ -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);