1270 lines · 48.1 KB · TypeScript
RawDownload
// @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.