Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 30 additions & 1 deletion apps/desktop/src/preview/RecordingCursor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,11 @@ import {
type RecordingKeyPress,
} from "./RecordingInput.ts";

// Modal dialogs, popovers and fullscreen elements paint above any z-index, so the cursors live in
// the top layer as manual popovers and move back to its front whenever the page adds to it.
const POPOVER_RESET =
"all:initial;margin:0;padding:0;border:0;background:transparent;overflow:visible;inset:auto;animation:none;transition:none;";

/** Draw in guest coordinates; Chromium's native cursor ignores webview CSS placement/scale. Hide it to avoid duplicates. */
export function installRecordingCursor(
document: Document,
Expand All @@ -20,22 +25,43 @@ export function installRecordingCursor(
) {
const style = document.createElement("style");
style.textContent =
"html, html * { cursor: none !important; } @media (prefers-reduced-motion: reduce) { [data-t3code-recording-agent-cursor] { transition: none !important; } }";
"html, html * { cursor: none !important; } [data-t3code-recording-cursor]::backdrop, [data-t3code-recording-agent-cursor]::backdrop { display: none !important; } @media (prefers-reduced-motion: reduce) { [data-t3code-recording-agent-cursor] { transition: none !important; } }";
const cursor = document.createElement("div");
cursor.setAttribute("aria-hidden", "true");
cursor.setAttribute("data-t3code-recording-cursor", "");
cursor.popover = "manual";
cursor.style.cssText =
POPOVER_RESET +
"position:fixed;left:0;top:0;width:16px;height:24px;pointer-events:none;z-index:2147483647;display:none;";
cursor.innerHTML =
'<svg xmlns="http://www.w3.org/2000/svg" width="16" height="24" viewBox="0 0 16 24"><path d="M1 1v18l4-4 4 8 3-1.5-4-8H15Z" fill="black" stroke="white" stroke-width="1.5" stroke-linejoin="round"/></svg>';
const agentCursor = document.createElement("div");
agentCursor.setAttribute("aria-hidden", "true");
agentCursor.setAttribute("data-t3code-recording-agent-cursor", "");
agentCursor.popover = "manual";
agentCursor.style.cssText =
POPOVER_RESET +
"position:fixed;left:0;top:0;width:20px;height:20px;pointer-events:none;z-index:2147483647;display:none;filter:drop-shadow(0 1px 2px #0003);transition:transform 150ms ease-out,opacity 150ms ease-out;";
agentCursor.innerHTML =
'<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="var(--recording-cursor-background,white)" stroke="var(--recording-cursor-primary,#2563eb)" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="transform:translate(-2px,-2px)"><path d="M4.037 4.688a.495.495 0 0 1 .651-.651l16 6.5a.5.5 0 0 1-.063.947l-6.124 1.58a2 2 0 0 0-1.438 1.435l-1.579 6.126a.5.5 0 0 1-.947.063z"/></svg>';
document.documentElement.append(style, cursor, agentCursor);
const cursors = [cursor, agentCursor];
const raise = () => {
for (const element of cursors) {
if (!element.isConnected || typeof element.showPopover !== "function") continue;
if (element.matches(":popover-open")) element.hidePopover();
element.showPopover();
}
};
raise();
const dialogObserver = new MutationObserver(raise);
dialogObserver.observe(document, { subtree: true, attributes: true, attributeFilter: ["open"] });
const toggle = (event: Event) => {
if (cursors.some((element) => element === event.target)) return;
if ("newState" in event && event.newState === "open") raise();
};
document.addEventListener("toggle", toggle, true);
document.addEventListener("fullscreenchange", raise);
let controller: "human" | "agent" | "none" = "none";
let humanPoint: { readonly x: number; readonly y: number } | null = null;
const drawHuman = () => {
Expand Down Expand Up @@ -182,6 +208,9 @@ export function installRecordingCursor(
window.removeEventListener("keyup", keyUp, true);
window.removeEventListener("pointerout", leave, true);
window.removeEventListener("blur", hide);
dialogObserver.disconnect();
document.removeEventListener("toggle", toggle, true);
document.removeEventListener("fullscreenchange", raise);
cancelPendingPointer();
window.clearTimeout(agentTimer);
cursor.remove();
Expand Down
Loading