From 48315a45dd0d7b1e41507fa856784e5a65c7fad5 Mon Sep 17 00:00:00 2001 From: Richie McIlroy <33632126+richiemcilroy@users.noreply.github.com> Date: Thu, 29 Oct 2026 15:06:46 +0000 Subject: [PATCH 01/28] fix(chrome-extension): restore the in-page recorder panel and camera preview controls --- apps/chrome-extension/src/content/overlay.css | 31 -- apps/chrome-extension/src/content/overlay.tsx | 278 +++++++++++++++++- 2 files changed, 263 insertions(+), 46 deletions(-) diff --git a/apps/chrome-extension/src/content/overlay.css b/apps/chrome-extension/src/content/overlay.css index 2202cb47c88..fd13f7c1136 100644 --- a/apps/chrome-extension/src/content/overlay.css +++ b/apps/chrome-extension/src/content/overlay.css @@ -266,37 +266,6 @@ border-radius: 0 0 6px; } -.cap-extension-camera-dismiss-btn { - position: absolute; - top: 10px; - right: 10px; - z-index: 12; - display: flex; - align-items: center; - justify-content: center; - width: 26px; - height: 26px; - border: none; - border-radius: 50%; - background: rgba(18, 18, 23, 0.65); - color: #ffffff; - cursor: pointer; - opacity: 0; - pointer-events: none; - transition: all 140ms ease; - backdrop-filter: blur(8px); -} - -.cap-extension-camera-window:hover .cap-extension-camera-dismiss-btn { - opacity: 1; - pointer-events: auto; -} - -.cap-extension-camera-dismiss-btn:hover { - background: rgba(239, 68, 68, 0.9); - transform: scale(1.1); -} - .cap-extension-camera-iframe { position: absolute; inset: 0; diff --git a/apps/chrome-extension/src/content/overlay.tsx b/apps/chrome-extension/src/content/overlay.tsx index f7f2946cef4..7725c58c850 100644 --- a/apps/chrome-extension/src/content/overlay.tsx +++ b/apps/chrome-extension/src/content/overlay.tsx @@ -1,4 +1,12 @@ -import { X } from "lucide-react"; +import { + Circle, + FlipHorizontal, + Maximize2, + PictureInPicture, + RectangleHorizontal, + Square, + X, +} from "lucide-react"; import { type PointerEvent as ReactPointerEvent, useCallback, @@ -48,6 +56,10 @@ import { replayStartupMessages, setStartupMessages } from "./startup-messages"; const ROOT_ID = "cap-extension-recorder-overlay"; const WINDOW_PADDING = 20; const BAR_HEIGHT = 52; +// The iframe tokens are readable from the host page DOM (they sit in the +// iframe src), so token checks alone cannot authenticate window messages. +// Frames we embed always run on the extension origin; require it too. +const EXTENSION_ORIGIN = new URL(chrome.runtime.getURL("")).origin; const createSecureToken = () => { if (typeof crypto.randomUUID === "function") return crypto.randomUUID(); const bytes = new Uint8Array(16); @@ -61,6 +73,12 @@ const PREVIEW_TOKEN = createSecureToken(); const PREVIEW_URL = chrome.runtime.getURL("camera-preview.html"); const PREVIEW_SRC = `${PREVIEW_URL}#${encodeURIComponent(PREVIEW_TOKEN)}`; const PREVIEW_ERROR_DELAY_MS = 1200; +const PANEL_TOKEN = createSecureToken(); +const PANEL_URL = chrome.runtime.getURL("popup.html"); +const PANEL_SRC = `${PANEL_URL}#${encodeURIComponent(PANEL_TOKEN)}`; +const PANEL_WIDTH = 300; +const PANEL_DEFAULT_HEIGHT = 460; +const PANEL_MARGIN = 16; const CAMERA_MIN_SIZE = 120; const CAMERA_MAX_SIZE = 420; const CAMERA_RESIZE_CORNERS = ["nw", "ne", "sw", "se"] as const; @@ -78,7 +96,7 @@ let overlayTokensRegistration: Promise | null = null; const ensureOverlayTokensRegistered = () => { overlayTokensRegistration ??= Promise.all( - [PREVIEW_TOKEN].map((token) => + [PREVIEW_TOKEN, PANEL_TOKEN].map((token) => sendServiceWorkerMessage({ target: "service-worker", type: "register-overlay-token", @@ -314,6 +332,127 @@ const connectCameraPreview = async ( }; }; +type PanelFrameMessage = + | { + source: "cap-extension-panel"; + token: string; + type: "size"; + height: number; + } + | { + source: "cap-extension-panel"; + token: string; + type: "dismiss"; + }; + +const isPanelFrameMessage = (value: unknown): value is PanelFrameMessage => { + if (!value || typeof value !== "object") return false; + const candidate = value as Partial; + if ( + candidate.source !== "cap-extension-panel" || + candidate.token !== PANEL_TOKEN + ) { + return false; + } + if (candidate.type === "dismiss") return true; + return ( + candidate.type === "size" && + typeof candidate.height === "number" && + Number.isFinite(candidate.height) + ); +}; + +function RecorderPanelOverlay({ + open, + onClose, +}: { + open: boolean; + onClose: () => void; +}) { + const [pageVisible, setPageVisible] = useState( + () => document.visibilityState === "visible", + ); + const [contentHeight, setContentHeight] = useState(PANEL_DEFAULT_HEIGHT); + const [viewportHeight, setViewportHeight] = useState( + () => window.innerHeight, + ); + const [tokenReady, setTokenReady] = useState(false); + + useEffect(() => { + if (!open || tokenReady) return; + let disposed = false; + void ensureOverlayTokensRegistered().then((registered) => { + if (!disposed && registered) setTokenReady(true); + }); + return () => { + disposed = true; + }; + }, [open, tokenReady]); + + useEffect(() => { + const handleVisibility = () => + setPageVisible(document.visibilityState === "visible"); + document.addEventListener("visibilitychange", handleVisibility); + return () => + document.removeEventListener("visibilitychange", handleVisibility); + }, []); + + useEffect(() => { + const handleFrameMessage = (event: MessageEvent) => { + if (event.origin !== EXTENSION_ORIGIN) return; + if (!isPanelFrameMessage(event.data)) return; + if (event.data.type === "size") { + setContentHeight(Math.max(320, Math.ceil(event.data.height))); + return; + } + onClose(); + }; + + window.addEventListener("message", handleFrameMessage); + return () => window.removeEventListener("message", handleFrameMessage); + }, [onClose]); + + useEffect(() => { + if (!open) return; + const handleResize = () => setViewportHeight(window.innerHeight); + handleResize(); + window.addEventListener("resize", handleResize); + return () => window.removeEventListener("resize", handleResize); + }, [open]); + + // Only the tab on screen renders the iframe; hidden tabs keep just the + // shared flag so the panel reappears instantly when they come forward. + // The iframe also waits for its token registration so the panel page can + // verify it was embedded by this extension. + if (!open || !pageVisible || !tokenReady) return null; + + const height = Math.min(contentHeight, viewportHeight - PANEL_MARGIN * 2); + + return ( + <> +