File size: 3,051 Bytes
3bc016b
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
92a9ea2
3bc016b
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
import { useEffect, useRef, useState, type ReactNode } from "react";
import { STAGE_W, STAGE_H } from "../theme";

// DEV-ONLY: after a slide's animations settle, measure the bounding box of
// every descendant against the 1280×720 canvas and flag anything that spills
// past the edges (which the stage's overflow:hidden would silently clip).
// Renders a red badge + outline so overflow is impossible to miss while authoring.
export function OverflowGuard({ id, children }: { id: string; children: ReactNode }) {
  const ref = useRef<HTMLDivElement>(null);
  const [over, setOver] = useState<{ dx: number; dy: number } | null>(null);

  useEffect(() => {
    if (!import.meta.env.DEV) return;
    const el = ref.current;
    if (!el) return;
    setOver(null);
    const check = () => {
      const base = el.getBoundingClientRect();
      if (base.width === 0) return;
      const sx = base.width / STAGE_W;
      const sy = base.height / STAGE_H;
      let maxR = 0;
      let maxB = 0;
      el.querySelectorAll<HTMLElement>("*").forEach((n) => {
        if (n.dataset.ovBadge) return;
        const r = n.getBoundingClientRect();
        maxR = Math.max(maxR, r.right - base.left);
        maxB = Math.max(maxB, r.bottom - base.top);
      });
      const dx = Math.round(maxR / sx - STAGE_W);
      const dy = Math.round(maxB / sy - STAGE_H);
      if (dx > 2 || dy > 2) {
        setOver({ dx: Math.max(0, dx), dy: Math.max(0, dy) });
        // eslint-disable-next-line no-console
        console.warn(
          `[overflow] slide "${id}" spills ${dx > 0 ? `→ ${dx}px` : ""} ${dy > 0 ? `↓ ${dy}px` : ""}`.trim(),
        );
      } else {
        setOver(null);
      }
    };
    // wait for staggered entrance animations to settle before measuring
    const t = window.setTimeout(check, 1700);
    return () => window.clearTimeout(t);
  }, [id]);

  return (
    <div ref={ref} data-slide-root style={{ position: "absolute", inset: 0 }}>
      {children}
      {over && (
        <>
          <div
            data-ov-badge="1"
            style={{
              position: "absolute",
              inset: 0,
              border: "3px solid #ff4d6d",
              boxShadow: "inset 0 0 0 3px rgba(255,77,109,0.25)",
              pointerEvents: "none",
              zIndex: 999,
            }}
          />
          <div
            data-ov-badge="1"
            style={{
              position: "absolute",
              top: 8,
              left: "50%",
              transform: "translateX(-50%)",
              background: "#ff4d6d",
              color: "#fff",
              fontFamily: "ui-monospace, monospace",
              fontSize: 15,
              fontWeight: 800,
              padding: "5px 12px",
              borderRadius: 8,
              letterSpacing: 0.5,
              zIndex: 1000,
              pointerEvents: "none",
            }}
          >
            ⚠ OVERFLOW {over.dx > 0 ? `→${over.dx}px` : ""} {over.dy > 0 ? `↓${over.dy}px` : ""}
          </div>
        </>
      )}
    </div>
  );
}