// @effect-diagnostics globalDate:off - This isolated Electron preload does not run inside an Effect runtime.import { ipcRenderer } from "electron";import { getElementContext } from "react-grab/primitives";import type { DesktopPreviewAnnotationTheme, PickedElementPayload, PickedElementStackFrame, PreviewAnnotationPayload, PreviewAnnotationPoint, PreviewAnnotationRect, PreviewAnnotationRegionTarget, PreviewAnnotationStrokeTarget, PreviewAnnotationStyleChange, PreviewAnnotationSubmission,} from "@t3tools/contracts"; import { resolveAnnotationSubmission } from "./AnnotationKeyboard.ts";import { previewAnnotationStyles } from "./AnnotationStyles.generated.ts";import { ANNOTATION_CAPTURED_CHANNEL, ANNOTATION_THEME_CHANNEL, CANCEL_PICK_CHANNEL, ELEMENT_PICKED_CHANNEL, HUMAN_INPUT_CHANNEL, START_PICK_CHANNEL,} from "./GuestProtocol.ts";const OVERLAY_ATTRIBUTE = "data-t3code-annotation-ui";const Z_INDEX_OVERLAY = 2147483646;const PRIMARY = "var(--t3-primary)";const PRIMARY_FILL = "color-mix(in srgb, var(--t3-primary) 10%, transparent)";const MAX_MARQUEE_ELEMENTS = 20;const CONTENT_LAYER_Z_INDEX = 1;const CHROME_LAYER_Z_INDEX = 10; type AnnotationTool = "select" | "marquee" | "draw" | "erase"; interface SelectedElement { id: string; element: Element; outline: HTMLDivElement; label: HTMLDivElement; baselineStyles: Map<string, string>;} interface AnnotationSession { teardown: (notifyMain: boolean) => void; applyTheme: (theme: DesktopPreviewAnnotationTheme) => void;} let activeSession: AnnotationSession | null = null;let idSequence = 0;let annotationTheme: DesktopPreviewAnnotationTheme | null = null; const applyAnnotationTheme = ( host: HTMLElement, theme: DesktopPreviewAnnotationTheme | null,): void => { if (!theme) return; host.style.colorScheme = theme.colorScheme; const variables = { "--t3-radius": theme.radius, "--t3-background": theme.background, "--t3-foreground": theme.foreground, "--t3-popover": theme.popover, "--t3-popover-foreground": theme.popoverForeground, "--t3-primary": theme.primary, "--t3-primary-foreground": theme.primaryForeground, "--t3-muted": theme.muted, "--t3-muted-foreground": theme.mutedForeground, "--t3-accent": theme.accent, "--t3-accent-foreground": theme.accentForeground, "--t3-border": theme.border, "--t3-input": theme.input, "--t3-ring": theme.ring, "--t3-font-sans": theme.fontSans, "--t3-font-mono": theme.fontMono, }; for (const [name, value] of Object.entries(variables)) { host.style.setProperty(name, value); }}; const reportHumanPointerInput = (event: PointerEvent): void => { if (!event.isTrusted) return; ipcRenderer.send(HUMAN_INPUT_CHANNEL, { kind: "pointer", x: event.clientX, y: event.clientY, button: event.button, });}; const reportHumanKeyInput = (event: KeyboardEvent): void => { if (!event.isTrusted) return; ipcRenderer.send(HUMAN_INPUT_CHANNEL, { kind: "key", key: event.key, code: event.code, });}; window.addEventListener("pointerdown", reportHumanPointerInput, true);window.addEventListener("keydown", reportHumanKeyInput, true); const nextId = (prefix: string): string => { idSequence += 1; return `${prefix}_${idSequence.toString(36)}`;}; const rectFromDomRect = (rect: DOMRect): PreviewAnnotationRect => ({ x: rect.left, y: rect.top, width: rect.width, height: rect.height,}); const normalizeRect = ( startX: number, startY: number, endX: number, endY: number,): PreviewAnnotationRect => ({ x: Math.min(startX, endX), y: Math.min(startY, endY), width: Math.abs(endX - startX), height: Math.abs(endY - startY),}); const isUsableRect = (rect: PreviewAnnotationRect): boolean => rect.width >= 3 && rect.height >= 3; function unionRects( rects: ReadonlyArray<PreviewAnnotationRect>, padding = 20,): PreviewAnnotationRect | null { if (rects.length === 0) return null; const left = Math.min(...rects.map((rect) => rect.x)); const top = Math.min(...rects.map((rect) => rect.y)); const right = Math.max(...rects.map((rect) => rect.x + rect.width)); const bottom = Math.max(...rects.map((rect) => rect.y + rect.height)); const x = Math.max(0, left - padding); const y = Math.max(0, top - padding); const maxWidth = Math.max(1, window.innerWidth - x); const maxHeight = Math.max(1, window.innerHeight - y); return { x, y, width: Math.min(maxWidth, right - left + padding * 2), height: Math.min(maxHeight, bottom - top + padding * 2), };} function isAnnotationNode(element: Element): boolean { return element instanceof Element && element.closest(`[${OVERLAY_ATTRIBUTE}]`) !== null;} function pickFromPoint(clientX: number, clientY: number): Element | null { for (const candidate of document.elementsFromPoint(clientX, clientY)) { if (!(candidate instanceof Element)) continue; if (isAnnotationNode(candidate)) continue; if (candidate === document.documentElement || candidate === document.body) continue; return candidate; } return null;} function describeRawElement(element: Element): string { const tag = element.tagName.toLowerCase(); const id = element.id ? `#${element.id}` : ""; const classes = element instanceof HTMLElement && typeof element.className === "string" ? element.className .trim() .split(/\s+/) .filter(Boolean) .slice(0, 2) .map((name) => `.${name}`) .join("") : ""; return `${tag}${id}${classes}`;} function createBox(color: string, fill: string): HTMLDivElement { const node = document.createElement("div"); node.setAttribute(OVERLAY_ATTRIBUTE, ""); node.style.cssText = [ "position:fixed", "pointer-events:none", `border:2px solid ${color}`, `background:${fill}`, "border-radius:3px", "box-sizing:border-box", "display:none", `z-index:${CONTENT_LAYER_Z_INDEX}`, ].join(";"); return node;} function positionBox(node: HTMLElement, rect: PreviewAnnotationRect): void { node.style.display = "block"; node.style.transform = `translate(${rect.x}px, ${rect.y}px)`; node.style.width = `${rect.width}px`; node.style.height = `${rect.height}px`;} function createLabel(): HTMLDivElement { const label = document.createElement("div"); label.setAttribute(OVERLAY_ATTRIBUTE, ""); label.className = "fixed z-1 max-w-70 overflow-hidden rounded-md bg-primary px-2 py-1 font-sans text-xs font-semibold text-primary-foreground shadow-md"; label.style.cssText = [ "position:fixed", "pointer-events:none", "white-space:nowrap", "text-overflow:ellipsis", `z-index:${CONTENT_LAYER_Z_INDEX}`, ].join(";"); return label;} function updateSelectedVisual(target: SelectedElement): void { if (!target.element.isConnected) { target.outline.style.display = "none"; target.label.style.display = "none"; return; } const rect = target.element.getBoundingClientRect(); positionBox(target.outline, rectFromDomRect(rect)); target.label.textContent = describeRawElement(target.element); target.label.style.display = "block"; target.label.style.transform = `translate(${Math.max(4, rect.left)}px, ${Math.max(4, rect.top - 22)}px)`;} function toStackFrame(frame: { functionName?: string; fileName?: string; lineNumber?: number; columnNumber?: number;}): PickedElementStackFrame { return { functionName: frame.functionName ?? null, fileName: frame.fileName ?? null, lineNumber: frame.lineNumber ?? null, columnNumber: frame.columnNumber ?? null, };} async function captureElement(element: Element): Promise<PickedElementPayload | null> { try { const context = await getElementContext(element); const stack = (context.stack ?? []).map(toStackFrame); return { pageUrl: location.href, pageTitle: document.title?.trim() || null, tagName: element.tagName.toLowerCase(), selector: context.selector, htmlPreview: context.htmlPreview ?? "", componentName: context.componentName, source: stack[0] ?? null, stack, styles: context.styles ?? "", pickedAt: new Date().toISOString(), }; } catch { return null; }} function createButton(label: string, title: string): HTMLButtonElement { const button = document.createElement("button"); button.type = "button"; button.textContent = label; button.title = title; button.className = "inline-flex h-7 cursor-pointer items-center justify-center rounded-md border border-transparent px-2 font-sans text-xs font-medium text-foreground outline-none hover:bg-accent disabled:pointer-events-none disabled:opacity-60"; return button;} function styleControl(input: HTMLInputElement | HTMLSelectElement): void { input.setAttribute("aria-label", input.getAttribute("aria-label") ?? "Style value"); input.className = "h-7 min-w-0 w-full appearance-none rounded-md border border-input bg-background px-2 font-mono text-xs text-foreground shadow-xs outline-none";} function createUnitControl(input: HTMLInputElement): HTMLElement { const wrapper = document.createElement("div"); wrapper.style.cssText = "position:relative;min-width:0"; const unit = document.createElement("span"); unit.textContent = input.dataset.unit ?? ""; unit.className = "pointer-events-none absolute top-1/2 right-2 -translate-y-1/2 font-mono text-xs text-muted-foreground"; wrapper.append(input, unit); return wrapper;} function createField( labelText: string, input: HTMLInputElement | HTMLSelectElement,): HTMLLabelElement { const label = document.createElement("label"); label.className = "grid min-h-7 grid-cols-[82px_minmax(0,1fr)] items-center gap-2 font-sans text-xs font-medium text-muted-foreground"; const text = document.createElement("span"); text.textContent = labelText; styleControl(input); label.append( text, input instanceof HTMLInputElement && input.dataset.unit ? createUnitControl(input) : input, ); return label;} function createStyleSection(): HTMLElement { const section = document.createElement("section"); section.className = "grid gap-1 border-t border-border py-2"; return section;} function createUnitInput(unit: string, placeholder = "0"): HTMLInputElement { const input = document.createElement("input"); input.type = "number"; input.placeholder = placeholder; input.style.paddingRight = "30px"; input.dataset.unit = unit; return input;} function pathFromPoints(points: ReadonlyArray<PreviewAnnotationPoint>): string { if (points.length === 0) return ""; if (points.length === 1) return `M ${points[0]!.x} ${points[0]!.y} l 0.01 0.01`; let path = `M ${points[0]!.x} ${points[0]!.y}`; for (let index = 1; index < points.length - 1; index += 1) { const current = points[index]!; const next = points[index + 1]!; path += ` Q ${current.x} ${current.y} ${(current.x + next.x) / 2} ${(current.y + next.y) / 2}`; } const last = points[points.length - 1]!; path += ` L ${last.x} ${last.y}`; return path;} function strokeBounds( points: ReadonlyArray<PreviewAnnotationPoint>, width: number,): PreviewAnnotationRect { const xs = points.map((point) => point.x); const ys = points.map((point) => point.y); const padding = width + 3; const left = Math.min(...xs) - padding; const top = Math.min(...ys) - padding; const right = Math.max(...xs) + padding; const bottom = Math.max(...ys) + padding; return { x: left, y: top, width: right - left, height: bottom - top };} function startAnnotation(): void { activeSession?.teardown(false); let finished = false; const host = document.createElement("div"); host.setAttribute(OVERLAY_ATTRIBUTE, ""); host.style.cssText = `position:fixed;inset:0;z-index:${Z_INDEX_OVERLAY};pointer-events:none`; applyAnnotationTheme(host, annotationTheme); const shadowRoot = host.attachShadow({ mode: "closed" }); const themeStyle = document.createElement("style"); themeStyle.textContent = previewAnnotationStyles; shadowRoot.appendChild(themeStyle); const root = document.createElement("div"); root.setAttribute(OVERLAY_ATTRIBUTE, ""); root.className = "fixed inset-0 font-sans text-foreground"; root.style.cssText = "pointer-events:none"; const cursorStyle = document.createElement("style"); cursorStyle.setAttribute(OVERLAY_ATTRIBUTE, ""); cursorStyle.textContent = `html[data-t3code-annotation-tool] body, html[data-t3code-annotation-tool] body * { cursor: crosshair !important; } [${OVERLAY_ATTRIBUTE}], [${OVERLAY_ATTRIBUTE}] * { cursor: default !important; } [${OVERLAY_ATTRIBUTE}] input[type=number]::-webkit-inner-spin-button, [${OVERLAY_ATTRIBUTE}] input[type=number]::-webkit-outer-spin-button { appearance:none; margin:0; }`; document.documentElement.appendChild(cursorStyle); shadowRoot.appendChild(root); const hoverOutline = createBox(PRIMARY, PRIMARY_FILL); const marqueeBox = createBox(PRIMARY, PRIMARY_FILL); root.append(hoverOutline, marqueeBox); const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg"); svg.setAttribute(OVERLAY_ATTRIBUTE, ""); svg.setAttribute("width", "100%"); svg.setAttribute("height", "100%"); svg.setAttribute("viewBox", `0 0 ${window.innerWidth} ${window.innerHeight}`); svg.style.cssText = "position:fixed;inset:0;overflow:visible;pointer-events:none"; svg.style.zIndex = String(CONTENT_LAYER_Z_INDEX); root.appendChild(svg); const toolbar = document.createElement("div"); toolbar.setAttribute(OVERLAY_ATTRIBUTE, ""); toolbar.className = "pointer-events-auto fixed top-2.5 left-1/2 flex -translate-x-1/2 gap-0.5 rounded-lg border border-border bg-popover/95 p-1 text-popover-foreground shadow-lg backdrop-blur-xl"; toolbar.style.zIndex = String(CHROME_LAYER_Z_INDEX); root.appendChild(toolbar); const editor = document.createElement("div"); editor.setAttribute(OVERLAY_ATTRIBUTE, ""); editor.className = "pointer-events-auto fixed hidden max-h-[calc(100vh-16px)] w-[min(360px,calc(100vw-16px))] flex-col overflow-hidden rounded-xl border border-border bg-popover/96 text-popover-foreground shadow-2xl backdrop-blur-xl"; editor.style.zIndex = String(CHROME_LAYER_Z_INDEX); root.appendChild(editor); const composerRow = document.createElement("div"); composerRow.className = "flex items-start gap-2 p-2"; const adjust = createButton("", "Expand annotation editor"); adjust.setAttribute("aria-label", "Expand annotation editor"); adjust.setAttribute("aria-expanded", "false"); adjust.className += " h-8 w-8 shrink-0 bg-muted p-0 text-muted-foreground hover:bg-accent hover:text-accent-foreground"; adjust.innerHTML = '<svg viewBox="0 0 20 20" width="15" height="15" aria-hidden="true"><path d="M4 5h12M4 10h12M4 15h12M7 3v4M13 8v4M9 13v4" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round"/></svg>'; composerRow.appendChild(adjust); const comment = document.createElement("textarea"); comment.placeholder = "Describe the change…"; comment.rows = 1; comment.className = "min-h-8 max-h-24 min-w-0 flex-1 resize-none overflow-y-hidden border-0 border-b border-b-transparent bg-transparent px-0 py-1.5 font-sans text-sm leading-5 text-foreground outline-none ring-0 placeholder:text-muted-foreground focus:border-b-primary focus:outline-none focus:ring-0"; composerRow.appendChild(comment); const dragHandle = document.createElement("button"); dragHandle.type = "button"; dragHandle.textContent = "⠿"; dragHandle.title = "Drag annotation editor"; dragHandle.className = "hidden h-8 w-6 shrink-0 cursor-grab select-none border-0 bg-transparent p-0 font-sans text-lg font-bold leading-5 text-muted-foreground"; composerRow.appendChild(dragHandle); const submit = createButton("Attach", "Attach annotation and screenshot (Enter)"); submit.className += " h-8 shrink-0 border-primary bg-primary px-3 text-primary-foreground shadow-sm hover:bg-primary/90"; composerRow.appendChild(submit); editor.appendChild(composerRow); const stylePanel = document.createElement("div"); stylePanel.className = "hidden max-h-[min(176px,calc(100vh-180px))] overflow-auto border-t border-border bg-muted/40 px-3"; editor.appendChild(stylePanel); const selected = new Map<Element, SelectedElement>(); const regions: PreviewAnnotationRegionTarget[] = []; const strokes: PreviewAnnotationStrokeTarget[] = []; const styleChanges = new Map<string, PreviewAnnotationStyleChange>(); const toolButtons = new Map<AnnotationTool, HTMLButtonElement>(); let tool: AnnotationTool = "select"; let dragStart: PreviewAnnotationPoint | null = null; let activeStroke: { target: PreviewAnnotationStrokeTarget; path: SVGPathElement } | null = null; let pendingCapture = false; let editorExpanded = false; let editorWasShown = false; let editorPosition: { left: number; top: number } | null = null; let editorDrag: { pointerId: number; offsetX: number; offsetY: number } | null = null; let editorLayoutFrame: number | null = null; const resizeComment = (): void => { const maxHeight = 96; comment.style.height = "auto"; const nextHeight = Math.min(comment.scrollHeight, maxHeight); comment.style.height = `${nextHeight}px`; comment.style.overflowY = comment.scrollHeight > maxHeight ? "auto" : "hidden"; queueEditorLayout(); }; comment.addEventListener("input", resizeComment); const updateStatus = (): void => { const hasTargets = selected.size > 0 || regions.length > 0 || strokes.length > 0; editor.style.display = hasTargets ? "flex" : "none"; submit.disabled = !hasTargets; submit.style.opacity = hasTargets ? "1" : "0.45"; adjust.disabled = !hasTargets; stylePanel.style.display = editorExpanded && selected.size > 0 ? "grid" : "none"; queueEditorLayout(); if (hasTargets && !editorWasShown) { editorWasShown = true; window.setTimeout(() => comment.focus({ preventScroll: true }), 0); } }; const refreshToolButtons = (): void => { for (const [candidate, button] of toolButtons) { const active = candidate === tool; button.classList.toggle("bg-primary/10", active); button.classList.toggle("text-primary", active); button.classList.toggle("text-foreground", !active); } if (tool !== "select") hoverOutline.style.display = "none"; if (tool !== "marquee") marqueeBox.style.display = "none"; document.documentElement.setAttribute("data-t3code-annotation-tool", tool); }; const removeSelected = (target: SelectedElement): void => { if (target.element instanceof HTMLElement || target.element instanceof SVGElement) { for (const [property, baseline] of target.baselineStyles) { if (baseline) target.element.style.setProperty(property, baseline); else target.element.style.removeProperty(property); } } selected.delete(target.element);Lines 1–500 of 1270. Load more
// @effect-diagnostics globalDate:off - This isolated Electron preload does not run inside an Effect runtime. import { ipcRenderer } from "electron"; import { getElementContext } from "react-grab/primitives"; import type { DesktopPreviewAnnotationTheme, PickedElementPayload, PickedElementStackFrame, PreviewAnnotationPayload, PreviewAnnotationPoint, PreviewAnnotationRect, PreviewAnnotationRegionTarget, PreviewAnnotationStrokeTarget, PreviewAnnotationStyleChange, PreviewAnnotationSubmission, } from "@t3tools/contracts"; import { resolveAnnotationSubmission } from "./AnnotationKeyboard.ts"; import { previewAnnotationStyles } from "./AnnotationStyles.generated.ts"; import { ANNOTATION_CAPTURED_CHANNEL, ANNOTATION_THEME_CHANNEL, CANCEL_PICK_CHANNEL, ELEMENT_PICKED_CHANNEL, HUMAN_INPUT_CHANNEL, START_PICK_CHANNEL, } from "./GuestProtocol.ts"; const OVERLAY_ATTRIBUTE = "data-t3code-annotation-ui"; const Z_INDEX_OVERLAY = 2147483646; const PRIMARY = "var(--t3-primary)"; const PRIMARY_FILL = "color-mix(in srgb, var(--t3-primary) 10%, transparent)"; const MAX_MARQUEE_ELEMENTS = 20; const CONTENT_LAYER_Z_INDEX = 1; const CHROME_LAYER_Z_INDEX = 10; type AnnotationTool = "select" | "marquee" | "draw" | "erase"; interface SelectedElement { id: string; element: Element; outline: HTMLDivElement; label: HTMLDivElement; baselineStyles: Map<string, string>; } interface AnnotationSession { teardown: (notifyMain: boolean) => void; applyTheme: (theme: DesktopPreviewAnnotationTheme) => void; } let activeSession: AnnotationSession | null = null; let idSequence = 0; let annotationTheme: DesktopPreviewAnnotationTheme | null = null; const applyAnnotationTheme = ( host: HTMLElement, theme: DesktopPreviewAnnotationTheme | null, ): void => { if (!theme) return; host.style.colorScheme = theme.colorScheme; const variables = { "--t3-radius": theme.radius, "--t3-background": theme.background, "--t3-foreground": theme.foreground, "--t3-popover": theme.popover, "--t3-popover-foreground": theme.popoverForeground, "--t3-primary": theme.primary, "--t3-primary-foreground": theme.primaryForeground, "--t3-muted": theme.muted, "--t3-muted-foreground": theme.mutedForeground, "--t3-accent": theme.accent, "--t3-accent-foreground": theme.accentForeground, "--t3-border": theme.border, "--t3-input": theme.input, "--t3-ring": theme.ring, "--t3-font-sans": theme.fontSans, "--t3-font-mono": theme.fontMono, }; for (const [name, value] of Object.entries(variables)) { host.style.setProperty(name, value); } }; const reportHumanPointerInput = (event: PointerEvent): void => { if (!event.isTrusted) return; ipcRenderer.send(HUMAN_INPUT_CHANNEL, { kind: "pointer", x: event.clientX, y: event.clientY, button: event.button, }); }; const reportHumanKeyInput = (event: KeyboardEvent): void => { if (!event.isTrusted) return; ipcRenderer.send(HUMAN_INPUT_CHANNEL, { kind: "key", key: event.key, code: event.code, }); }; window.addEventListener("pointerdown", reportHumanPointerInput, true); window.addEventListener("keydown", reportHumanKeyInput, true); const nextId = (prefix: string): string => { idSequence += 1; return `${prefix}_${idSequence.toString(36)}`; }; const rectFromDomRect = (rect: DOMRect): PreviewAnnotationRect => ({ x: rect.left, y: rect.top, width: rect.width, height: rect.height, }); const normalizeRect = ( startX: number, startY: number, endX: number, endY: number, ): PreviewAnnotationRect => ({ x: Math.min(startX, endX), y: Math.min(startY, endY), width: Math.abs(endX - startX), height: Math.abs(endY - startY), }); const isUsableRect = (rect: PreviewAnnotationRect): boolean => rect.width >= 3 && rect.height >= 3; function unionRects( rects: ReadonlyArray<PreviewAnnotationRect>, padding = 20, ): PreviewAnnotationRect | null { if (rects.length === 0) return null; const left = Math.min(...rects.map((rect) => rect.x)); const top = Math.min(...rects.map((rect) => rect.y)); const right = Math.max(...rects.map((rect) => rect.x + rect.width)); const bottom = Math.max(...rects.map((rect) => rect.y + rect.height)); const x = Math.max(0, left - padding); const y = Math.max(0, top - padding); const maxWidth = Math.max(1, window.innerWidth - x); const maxHeight = Math.max(1, window.innerHeight - y); return { x, y, width: Math.min(maxWidth, right - left + padding * 2), height: Math.min(maxHeight, bottom - top + padding * 2), }; } function isAnnotationNode(element: Element): boolean { return element instanceof Element && element.closest(`[${OVERLAY_ATTRIBUTE}]`) !== null; } function pickFromPoint(clientX: number, clientY: number): Element | null { for (const candidate of document.elementsFromPoint(clientX, clientY)) { if (!(candidate instanceof Element)) continue; if (isAnnotationNode(candidate)) continue; if (candidate === document.documentElement || candidate === document.body) continue; return candidate; } return null; } function describeRawElement(element: Element): string { const tag = element.tagName.toLowerCase(); const id = element.id ? `#${element.id}` : ""; const classes = element instanceof HTMLElement && typeof element.className === "string" ? element.className .trim() .split(/\s+/) .filter(Boolean) .slice(0, 2) .map((name) => `.${name}`) .join("") : ""; return `${tag}${id}${classes}`; } function createBox(color: string, fill: string): HTMLDivElement { const node = document.createElement("div"); node.setAttribute(OVERLAY_ATTRIBUTE, ""); node.style.cssText = [ "position:fixed", "pointer-events:none", `border:2px solid ${color}`, `background:${fill}`, "border-radius:3px", "box-sizing:border-box", "display:none", `z-index:${CONTENT_LAYER_Z_INDEX}`, ].join(";"); return node; } function positionBox(node: HTMLElement, rect: PreviewAnnotationRect): void { node.style.display = "block"; node.style.transform = `translate(${rect.x}px, ${rect.y}px)`; node.style.width = `${rect.width}px`; node.style.height = `${rect.height}px`; } function createLabel(): HTMLDivElement { const label = document.createElement("div"); label.setAttribute(OVERLAY_ATTRIBUTE, ""); label.className = "fixed z-1 max-w-70 overflow-hidden rounded-md bg-primary px-2 py-1 font-sans text-xs font-semibold text-primary-foreground shadow-md"; label.style.cssText = [ "position:fixed", "pointer-events:none", "white-space:nowrap", "text-overflow:ellipsis", `z-index:${CONTENT_LAYER_Z_INDEX}`, ].join(";"); return label; } function updateSelectedVisual(target: SelectedElement): void { if (!target.element.isConnected) { target.outline.style.display = "none"; target.label.style.display = "none"; return; } const rect = target.element.getBoundingClientRect(); positionBox(target.outline, rectFromDomRect(rect)); target.label.textContent = describeRawElement(target.element); target.label.style.display = "block"; target.label.style.transform = `translate(${Math.max(4, rect.left)}px, ${Math.max(4, rect.top - 22)}px)`; } function toStackFrame(frame: { functionName?: string; fileName?: string; lineNumber?: number; columnNumber?: number; }): PickedElementStackFrame { return { functionName: frame.functionName ?? null, fileName: frame.fileName ?? null, lineNumber: frame.lineNumber ?? null, columnNumber: frame.columnNumber ?? null, }; } async function captureElement(element: Element): Promise<PickedElementPayload | null> { try { const context = await getElementContext(element); const stack = (context.stack ?? []).map(toStackFrame); return { pageUrl: location.href, pageTitle: document.title?.trim() || null, tagName: element.tagName.toLowerCase(), selector: context.selector, htmlPreview: context.htmlPreview ?? "", componentName: context.componentName, source: stack[0] ?? null, stack, styles: context.styles ?? "", pickedAt: new Date().toISOString(), }; } catch { return null; } } function createButton(label: string, title: string): HTMLButtonElement { const button = document.createElement("button"); button.type = "button"; button.textContent = label; button.title = title; button.className = "inline-flex h-7 cursor-pointer items-center justify-center rounded-md border border-transparent px-2 font-sans text-xs font-medium text-foreground outline-none hover:bg-accent disabled:pointer-events-none disabled:opacity-60"; return button; } function styleControl(input: HTMLInputElement | HTMLSelectElement): void { input.setAttribute("aria-label", input.getAttribute("aria-label") ?? "Style value"); input.className = "h-7 min-w-0 w-full appearance-none rounded-md border border-input bg-background px-2 font-mono text-xs text-foreground shadow-xs outline-none"; } function createUnitControl(input: HTMLInputElement): HTMLElement { const wrapper = document.createElement("div"); wrapper.style.cssText = "position:relative;min-width:0"; const unit = document.createElement("span"); unit.textContent = input.dataset.unit ?? ""; unit.className = "pointer-events-none absolute top-1/2 right-2 -translate-y-1/2 font-mono text-xs text-muted-foreground"; wrapper.append(input, unit); return wrapper; } function createField( labelText: string, input: HTMLInputElement | HTMLSelectElement, ): HTMLLabelElement { const label = document.createElement("label"); label.className = "grid min-h-7 grid-cols-[82px_minmax(0,1fr)] items-center gap-2 font-sans text-xs font-medium text-muted-foreground"; const text = document.createElement("span"); text.textContent = labelText; styleControl(input); label.append( text, input instanceof HTMLInputElement && input.dataset.unit ? createUnitControl(input) : input, ); return label; } function createStyleSection(): HTMLElement { const section = document.createElement("section"); section.className = "grid gap-1 border-t border-border py-2"; return section; } function createUnitInput(unit: string, placeholder = "0"): HTMLInputElement { const input = document.createElement("input"); input.type = "number"; input.placeholder = placeholder; input.style.paddingRight = "30px"; input.dataset.unit = unit; return input; } function pathFromPoints(points: ReadonlyArray<PreviewAnnotationPoint>): string { if (points.length === 0) return ""; if (points.length === 1) return `M ${points[0]!.x} ${points[0]!.y} l 0.01 0.01`; let path = `M ${points[0]!.x} ${points[0]!.y}`; for (let index = 1; index < points.length - 1; index += 1) { const current = points[index]!; const next = points[index + 1]!; path += ` Q ${current.x} ${current.y} ${(current.x + next.x) / 2} ${(current.y + next.y) / 2}`; } const last = points[points.length - 1]!; path += ` L ${last.x} ${last.y}`; return path; } function strokeBounds( points: ReadonlyArray<PreviewAnnotationPoint>, width: number, ): PreviewAnnotationRect { const xs = points.map((point) => point.x); const ys = points.map((point) => point.y); const padding = width + 3; const left = Math.min(...xs) - padding; const top = Math.min(...ys) - padding; const right = Math.max(...xs) + padding; const bottom = Math.max(...ys) + padding; return { x: left, y: top, width: right - left, height: bottom - top }; } function startAnnotation(): void { activeSession?.teardown(false); let finished = false; const host = document.createElement("div"); host.setAttribute(OVERLAY_ATTRIBUTE, ""); host.style.cssText = `position:fixed;inset:0;z-index:${Z_INDEX_OVERLAY};pointer-events:none`; applyAnnotationTheme(host, annotationTheme); const shadowRoot = host.attachShadow({ mode: "closed" }); const themeStyle = document.createElement("style"); themeStyle.textContent = previewAnnotationStyles; shadowRoot.appendChild(themeStyle); const root = document.createElement("div"); root.setAttribute(OVERLAY_ATTRIBUTE, ""); root.className = "fixed inset-0 font-sans text-foreground"; root.style.cssText = "pointer-events:none"; const cursorStyle = document.createElement("style"); cursorStyle.setAttribute(OVERLAY_ATTRIBUTE, ""); cursorStyle.textContent = `html[data-t3code-annotation-tool] body, html[data-t3code-annotation-tool] body * { cursor: crosshair !important; } [${OVERLAY_ATTRIBUTE}], [${OVERLAY_ATTRIBUTE}] * { cursor: default !important; } [${OVERLAY_ATTRIBUTE}] input[type=number]::-webkit-inner-spin-button, [${OVERLAY_ATTRIBUTE}] input[type=number]::-webkit-outer-spin-button { appearance:none; margin:0; }`; document.documentElement.appendChild(cursorStyle); shadowRoot.appendChild(root); const hoverOutline = createBox(PRIMARY, PRIMARY_FILL); const marqueeBox = createBox(PRIMARY, PRIMARY_FILL); root.append(hoverOutline, marqueeBox); const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg"); svg.setAttribute(OVERLAY_ATTRIBUTE, ""); svg.setAttribute("width", "100%"); svg.setAttribute("height", "100%"); svg.setAttribute("viewBox", `0 0 ${window.innerWidth} ${window.innerHeight}`); svg.style.cssText = "position:fixed;inset:0;overflow:visible;pointer-events:none"; svg.style.zIndex = String(CONTENT_LAYER_Z_INDEX); root.appendChild(svg); const toolbar = document.createElement("div"); toolbar.setAttribute(OVERLAY_ATTRIBUTE, ""); toolbar.className = "pointer-events-auto fixed top-2.5 left-1/2 flex -translate-x-1/2 gap-0.5 rounded-lg border border-border bg-popover/95 p-1 text-popover-foreground shadow-lg backdrop-blur-xl"; toolbar.style.zIndex = String(CHROME_LAYER_Z_INDEX); root.appendChild(toolbar); const editor = document.createElement("div"); editor.setAttribute(OVERLAY_ATTRIBUTE, ""); editor.className = "pointer-events-auto fixed hidden max-h-[calc(100vh-16px)] w-[min(360px,calc(100vw-16px))] flex-col overflow-hidden rounded-xl border border-border bg-popover/96 text-popover-foreground shadow-2xl backdrop-blur-xl"; editor.style.zIndex = String(CHROME_LAYER_Z_INDEX); root.appendChild(editor); const composerRow = document.createElement("div"); composerRow.className = "flex items-start gap-2 p-2"; const adjust = createButton("", "Expand annotation editor"); adjust.setAttribute("aria-label", "Expand annotation editor"); adjust.setAttribute("aria-expanded", "false"); adjust.className += " h-8 w-8 shrink-0 bg-muted p-0 text-muted-foreground hover:bg-accent hover:text-accent-foreground"; adjust.innerHTML = '<svg viewBox="0 0 20 20" width="15" height="15" aria-hidden="true"><path d="M4 5h12M4 10h12M4 15h12M7 3v4M13 8v4M9 13v4" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round"/></svg>'; composerRow.appendChild(adjust); const comment = document.createElement("textarea"); comment.placeholder = "Describe the change…"; comment.rows = 1; comment.className = "min-h-8 max-h-24 min-w-0 flex-1 resize-none overflow-y-hidden border-0 border-b border-b-transparent bg-transparent px-0 py-1.5 font-sans text-sm leading-5 text-foreground outline-none ring-0 placeholder:text-muted-foreground focus:border-b-primary focus:outline-none focus:ring-0"; composerRow.appendChild(comment); const dragHandle = document.createElement("button"); dragHandle.type = "button"; dragHandle.textContent = "⠿"; dragHandle.title = "Drag annotation editor"; dragHandle.className = "hidden h-8 w-6 shrink-0 cursor-grab select-none border-0 bg-transparent p-0 font-sans text-lg font-bold leading-5 text-muted-foreground"; composerRow.appendChild(dragHandle); const submit = createButton("Attach", "Attach annotation and screenshot (Enter)"); submit.className += " h-8 shrink-0 border-primary bg-primary px-3 text-primary-foreground shadow-sm hover:bg-primary/90"; composerRow.appendChild(submit); editor.appendChild(composerRow); const stylePanel = document.createElement("div"); stylePanel.className = "hidden max-h-[min(176px,calc(100vh-180px))] overflow-auto border-t border-border bg-muted/40 px-3"; editor.appendChild(stylePanel); const selected = new Map<Element, SelectedElement>(); const regions: PreviewAnnotationRegionTarget[] = []; const strokes: PreviewAnnotationStrokeTarget[] = []; const styleChanges = new Map<string, PreviewAnnotationStyleChange>(); const toolButtons = new Map<AnnotationTool, HTMLButtonElement>(); let tool: AnnotationTool = "select"; let dragStart: PreviewAnnotationPoint | null = null; let activeStroke: { target: PreviewAnnotationStrokeTarget; path: SVGPathElement } | null = null; let pendingCapture = false; let editorExpanded = false; let editorWasShown = false; let editorPosition: { left: number; top: number } | null = null; let editorDrag: { pointerId: number; offsetX: number; offsetY: number } | null = null; let editorLayoutFrame: number | null = null; const resizeComment = (): void => { const maxHeight = 96; comment.style.height = "auto"; const nextHeight = Math.min(comment.scrollHeight, maxHeight); comment.style.height = `${nextHeight}px`; comment.style.overflowY = comment.scrollHeight > maxHeight ? "auto" : "hidden"; queueEditorLayout(); }; comment.addEventListener("input", resizeComment); const updateStatus = (): void => { const hasTargets = selected.size > 0 || regions.length > 0 || strokes.length > 0; editor.style.display = hasTargets ? "flex" : "none"; submit.disabled = !hasTargets; submit.style.opacity = hasTargets ? "1" : "0.45"; adjust.disabled = !hasTargets; stylePanel.style.display = editorExpanded && selected.size > 0 ? "grid" : "none"; queueEditorLayout(); if (hasTargets && !editorWasShown) { editorWasShown = true; window.setTimeout(() => comment.focus({ preventScroll: true }), 0); } }; const refreshToolButtons = (): void => { for (const [candidate, button] of toolButtons) { const active = candidate === tool; button.classList.toggle("bg-primary/10", active); button.classList.toggle("text-primary", active); button.classList.toggle("text-foreground", !active); } if (tool !== "select") hoverOutline.style.display = "none"; if (tool !== "marquee") marqueeBox.style.display = "none"; document.documentElement.setAttribute("data-t3code-annotation-tool", tool); }; const removeSelected = (target: SelectedElement): void => { if (target.element instanceof HTMLElement || target.element instanceof SVGElement) { for (const [property, baseline] of target.baselineStyles) { if (baseline) target.element.style.setProperty(property, baseline); else target.element.style.removeProperty(property); } } selected.delete(target.element);