@replohq/sandbox-runtime 1.6.2 → 1.6.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/buildMetadata.js CHANGED
@@ -1,7 +1,7 @@
1
1
  export const BUILD_METADATA = {
2
2
  "packageName": "@replohq/sandbox-runtime",
3
- "version": "1.6.2",
3
+ "version": "1.6.4",
4
4
  "branch": "HEAD",
5
- "commit": "625dfbba5e75a1f3eeebb0cefa1bfeb1354c0de2",
6
- "builtAt": "2026-09-11T22:39:30.736Z"
5
+ "commit": "085a9d43063283dfea1f13552ee57d6b6f6b3f29",
6
+ "builtAt": "2026-09-11T23:34:52.476Z"
7
7
  };
package/package.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@replohq/sandbox-runtime",
3
- "version": "1.6.2",
3
+ "version": "1.6.4",
4
4
  "reploBuild": {
5
5
  "packageName": "@replohq/sandbox-runtime",
6
- "version": "1.6.2",
6
+ "version": "1.6.4",
7
7
  "branch": "HEAD",
8
- "commit": "625dfbba5e75a1f3eeebb0cefa1bfeb1354c0de2",
9
- "builtAt": "2026-09-11T22:39:30.736Z"
8
+ "commit": "085a9d43063283dfea1f13552ee57d6b6f6b3f29",
9
+ "builtAt": "2026-09-11T23:34:52.476Z"
10
10
  },
11
11
  "description": "Replo sandbox runtime — editor integration for agent-built Next.js sites.",
12
12
  "license": "SEE LICENSE IN LICENSE",
@@ -7,7 +7,11 @@ function useCrosshairCursor({
7
7
  return;
8
8
  }
9
9
  const styleElement = document.createElement("style");
10
- styleElement.textContent = "* { cursor: crosshair !important; user-select: none !important; -webkit-user-drag: none !important; }";
10
+ styleElement.textContent = `
11
+ * { cursor: crosshair !important; user-select: none !important; -webkit-user-drag: none !important; }
12
+ [data-select-mode-inline-editor][contenteditable="true"],
13
+ [data-select-mode-inline-editor][contenteditable="true"] * { cursor: text !important; }
14
+ `;
11
15
  document.head.append(styleElement);
12
16
  return () => {
13
17
  styleElement.remove();
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../select-mode/hooks/useCrosshairCursor.ts"],
4
- "sourcesContent": ["import type { DisplayMode } from \"schemas/generated/selectMode\";\n\nimport { useEffect } from \"react\";\n\n/**\n * Forces a crosshair cursor + disables text selection / drag while the iframe\n * is in `selection` mode. We inject a `<style>` tag into the iframe document\n * (rather than toggling a class on `<html>`) so that the `!important` rule\n * wins against any per-element cursor styling on the page being edited.\n *\n * @author Kevin O'Connell 2026-04-17\n */\nexport function useCrosshairCursor({\n displayMode,\n}: {\n displayMode: DisplayMode;\n}) {\n // eslint-disable-next-line replo/no-use-effect -- Legacy effect - may or may not be necessary\n useEffect(() => {\n if (displayMode !== \"selection\") {\n return;\n }\n\n const styleElement = document.createElement(\"style\");\n styleElement.textContent =\n \"* { cursor: crosshair !important; user-select: none !important; -webkit-user-drag: none !important; }\";\n document.head.append(styleElement);\n\n return () => {\n styleElement.remove();\n };\n }, [displayMode]);\n}\n"],
5
- "mappings": "AAEA,SAAS,iBAAiB;AAUnB,SAAS,mBAAmB;AAAA,EACjC;AACF,GAEG;AAED,YAAU,MAAM;AACd,QAAI,gBAAgB,aAAa;AAC/B;AAAA,IACF;AAEA,UAAM,eAAe,SAAS,cAAc,OAAO;AACnD,iBAAa,cACX;AACF,aAAS,KAAK,OAAO,YAAY;AAEjC,WAAO,MAAM;AACX,mBAAa,OAAO;AAAA,IACtB;AAAA,EACF,GAAG,CAAC,WAAW,CAAC;AAClB;",
4
+ "sourcesContent": ["import type { DisplayMode } from \"schemas/generated/selectMode\";\n\nimport { useEffect } from \"react\";\n\n/**\n * Forces a crosshair cursor + disables text selection / drag while the iframe\n * is in `selection` mode. We inject a `<style>` tag into the iframe document\n * (rather than toggling a class on `<html>`) so that the `!important` rule\n * wins against any per-element cursor styling on the page being edited.\n *\n * @author Kevin O'Connell 2026-04-17\n */\nexport function useCrosshairCursor({\n displayMode,\n}: {\n displayMode: DisplayMode;\n}) {\n // eslint-disable-next-line replo/no-use-effect -- Legacy effect - may or may not be necessary\n useEffect(() => {\n if (displayMode !== \"selection\") {\n return;\n }\n\n const styleElement = document.createElement(\"style\");\n styleElement.textContent = `\n * { cursor: crosshair !important; user-select: none !important; -webkit-user-drag: none !important; }\n [data-select-mode-inline-editor][contenteditable=\"true\"],\n [data-select-mode-inline-editor][contenteditable=\"true\"] * { cursor: text !important; }\n `;\n document.head.append(styleElement);\n\n return () => {\n styleElement.remove();\n };\n }, [displayMode]);\n}\n"],
5
+ "mappings": "AAEA,SAAS,iBAAiB;AAUnB,SAAS,mBAAmB;AAAA,EACjC;AACF,GAEG;AAED,YAAU,MAAM;AACd,QAAI,gBAAgB,aAAa;AAC/B;AAAA,IACF;AAEA,UAAM,eAAe,SAAS,cAAc,OAAO;AACnD,iBAAa,cAAc;AAAA;AAAA;AAAA;AAAA;AAK3B,aAAS,KAAK,OAAO,YAAY;AAEjC,WAAO,MAAM;AACX,mBAAa,OAAO;AAAA,IACtB;AAAA,EACF,GAAG,CAAC,WAAW,CAAC;AAClB;",
6
6
  "names": []
7
7
  }
@@ -38,6 +38,7 @@ function useSelectionInteractions({
38
38
  return;
39
39
  }
40
40
  let dragAnimationFrameId = null;
41
+ const editingPointerIds = /* @__PURE__ */ new Set();
41
42
  let lastClick = null;
42
43
  let dragCandidates = [];
43
44
  const stopDragAnimation = () => {
@@ -131,7 +132,9 @@ function useSelectionInteractions({
131
132
  dragAnimationFrameId = requestAnimationFrame(syncDragRectToViewport);
132
133
  };
133
134
  const handlePointerDown = (event) => {
135
+ editingPointerIds.delete(event.pointerId);
134
136
  if (editingElementRef?.current && event.target instanceof Node && editingElementRef.current.contains(event.target)) {
137
+ editingPointerIds.add(event.pointerId);
135
138
  return;
136
139
  }
137
140
  event.preventDefault();
@@ -230,6 +233,9 @@ function useSelectionInteractions({
230
233
  });
231
234
  };
232
235
  const handlePointerUp = (event) => {
236
+ if (editingPointerIds.delete(event.pointerId)) {
237
+ return;
238
+ }
233
239
  if (editingElementRef?.current && event.target instanceof Node && editingElementRef.current.contains(event.target)) {
234
240
  return;
235
241
  }
@@ -317,10 +323,12 @@ function useSelectionInteractions({
317
323
  event.preventDefault();
318
324
  event.stopPropagation();
319
325
  };
320
- const handlePointerCancel = () => {
326
+ const handlePointerCancel = (event) => {
327
+ editingPointerIds.delete(event.pointerId);
321
328
  resetDragState();
322
329
  };
323
330
  const handleWindowBlur = () => {
331
+ editingPointerIds.clear();
324
332
  resetDragState();
325
333
  };
326
334
  document.addEventListener("pointerdown", handlePointerDown, true);
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../select-mode/hooks/useSelectionInteractions.ts"],
4
- "sourcesContent": ["import type { MutableRefObject } from \"react\";\nimport type {\n DisplayMode,\n SelectableData,\n SelectModeClickPosition,\n} from \"schemas/generated/selectMode\";\nimport type { DragCandidate } from \"../interactions/selectionGeometry\";\nimport type { SelectModeInteractionSession } from \"../selectModeSession\";\n\nimport { useEffect } from \"react\";\n\nimport {\n buildElementDescriptor,\n buildSelector,\n getNearestSelectableElement,\n} from \"../descriptor/elementDescriptor\";\nimport { getWebsiteBuilderRpc } from \"../iframeRpcChannel\";\nimport {\n collectDragCandidates,\n getCandidatesInSelection,\n getDocumentScrollOffset,\n getDragPositionFromClient,\n getElementAnchor,\n getPageRectFromViewportRect,\n getViewportRectFromPageRect,\n getViewportSelectionRect,\n} from \"../interactions/selectionGeometry\";\nimport {\n showElementSelectionFrames,\n showSelectionFrames,\n} from \"../selectionFrame\";\nimport { IDLE_SESSION } from \"../selectModeSession\";\nimport { symbolicateSelectedElement } from \"../symbolicate\";\nimport { isTextEditableElement } from \"./useEditModeStateMachine\";\nimport { getRepetitionSiblingElements } from \"./useSiblingResolution\";\n\nconst DOUBLE_CLICK_THRESHOLD_MS = 500;\n\n// React fibers carry only Turbopack chunk paths; resolving them to the\n// user's source file is async and only meaningful for \"react-element\"\n// kinds. Plan selectables already point at the right thing.\nasync function maybeSymbolicate(data: SelectableData): Promise<SelectableData> {\n return data.kind === \"react-element\"\n ? symbolicateSelectedElement(data)\n : data;\n}\n\n/**\n * Owns the iframe-side pointer interactions that drive selection:\n *\n * - On `pointerdown` we open a `drag` session (storing both client + page\n * coords plus the current scroll offset). We don't yet know if this will\n * become a click or an actual drag.\n * - On `pointermove` we update the drag rect and post `DRAG_RECT_UPDATED`\n * to the parent so the live drag rectangle can render. We only flip\n * `hasExceededThreshold` once the rect grows past 6\u00D76 px so that a tiny\n * accidental drag still resolves as a click.\n * - On `pointerup` we either:\n * - resolve as an `ELEMENT_SELECTED` (single click / sub-threshold drag)\n * - resolve as an `AREA_SELECTED` (drag past threshold), filtering the\n * candidates snapshotted at drag start against the final rectangle to\n * enumerate which descendants the user enclosed.\n * - During an active drag a `requestAnimationFrame` loop keeps the rect in\n * sync with the page scroll so the rectangle visually \"sticks\" to the\n * page rather than the viewport.\n *\n * We capture-listen and `preventDefault`/`stopPropagation` clicks so the\n * underlying page can't navigate or trigger handlers while the user is\n * working in selection mode.\n *\n * @author Kevin O'Connell 2026-04-17\n */\nexport function useSelectionInteractions({\n displayMode,\n editingElementRef,\n interactionSessionRef,\n onBeginEditing,\n}: {\n displayMode: DisplayMode;\n editingElementRef?: MutableRefObject<HTMLElement | null>;\n interactionSessionRef: MutableRefObject<SelectModeInteractionSession>;\n onBeginEditing: (args: {\n element: Element;\n descriptorPromise: Promise<SelectableData>;\n }) => void;\n}) {\n // eslint-disable-next-line replo/no-use-effect -- Legacy effect - may or may not be necessary\n useEffect(() => {\n if (displayMode !== \"selection\") {\n return;\n }\n\n let dragAnimationFrameId: number | null = null;\n let lastClick: {\n descriptorPromise: Promise<SelectableData>;\n target: Element;\n time: number;\n } | null = null;\n // Snapshot of the page's meaningful elements + their page rects, taken\n // once when a drag begins so per-move hit-testing avoids a full-DOM scan.\n let dragCandidates: DragCandidate[] = [];\n\n const stopDragAnimation = () => {\n if (dragAnimationFrameId === null) {\n return;\n }\n\n cancelAnimationFrame(dragAnimationFrameId);\n dragAnimationFrameId = null;\n };\n\n // `notifyParentDragCleared` posts a null drag rect so the parent tears\n // down the live drag overlay. The area-commit path suppresses it: the\n // parent maps a null drag rect to `selection = none`, which round-trips\n // back as `setSelectionType(\"none\")` and wipes the in-iframe frames we're\n // about to paint. The follow-up `areaSelected` supersedes the drag overlay\n // on its own, so the null post is pure collateral damage there.\n const resetDragState = ({\n notifyParentDragCleared = true,\n }: { notifyParentDragCleared?: boolean } = {}) => {\n const currentSession = interactionSessionRef.current;\n\n stopDragAnimation();\n\n if (currentSession.type !== \"drag\") {\n dragCandidates = [];\n return;\n }\n\n if (notifyParentDragCleared && currentSession.hasExceededThreshold) {\n getWebsiteBuilderRpc()?.dragRectUpdated({\n dragRect: null,\n selectionRects: [],\n });\n }\n\n dragCandidates = [];\n interactionSessionRef.current = IDLE_SESSION;\n };\n\n const postCurrentDragRect = () => {\n const session = interactionSessionRef.current;\n if (session.type !== \"drag\") {\n return;\n }\n\n const rect = getViewportSelectionRect({\n dragStart: session.dragStart,\n dragCurrent: session.dragCurrent,\n scrollOffset: session.scrollOffset,\n });\n\n if (\n rect.width <= 6 &&\n rect.height <= 6 &&\n !session.hasExceededThreshold\n ) {\n return;\n }\n\n if (!session.hasExceededThreshold) {\n interactionSessionRef.current = {\n ...session,\n hasExceededThreshold: true,\n };\n }\n\n // Cheap hit-test against the candidates we snapshotted at drag start,\n // projected back to viewport coords so the parent can paint a live\n // highlight on each element the drag will capture. No DOM scan here.\n const selectionPageRect = getPageRectFromViewportRect({\n rect,\n scrollOffset: session.scrollOffset,\n });\n const selectionRects = getCandidatesInSelection({\n candidates: dragCandidates,\n selectionPageRect,\n }).map((candidate) => ({\n id: candidate.id,\n rect: getViewportRectFromPageRect({\n rect: candidate.pageRect,\n scrollOffset: session.scrollOffset,\n }),\n }));\n\n getWebsiteBuilderRpc()?.dragRectUpdated({\n dragRect: rect,\n selectionRects,\n });\n };\n\n const syncDragRectToViewport = () => {\n const session = interactionSessionRef.current;\n if (session.type !== \"drag\") {\n dragAnimationFrameId = null;\n return;\n }\n\n const nextScrollOffset = getDocumentScrollOffset();\n const nextDragCurrent = getDragPositionFromClient({\n clientX: session.dragCurrent.clientX,\n clientY: session.dragCurrent.clientY,\n scrollOffset: nextScrollOffset,\n });\n\n if (\n nextDragCurrent.pageX !== session.dragCurrent.pageX ||\n nextDragCurrent.pageY !== session.dragCurrent.pageY\n ) {\n interactionSessionRef.current = {\n ...session,\n dragCurrent: nextDragCurrent,\n scrollOffset: nextScrollOffset,\n };\n postCurrentDragRect();\n } else {\n interactionSessionRef.current = {\n ...session,\n scrollOffset: nextScrollOffset,\n };\n }\n\n dragAnimationFrameId = requestAnimationFrame(syncDragRectToViewport);\n };\n\n const handlePointerDown = (event: PointerEvent) => {\n if (\n editingElementRef?.current &&\n event.target instanceof Node &&\n editingElementRef.current.contains(event.target)\n ) {\n return;\n }\n event.preventDefault();\n event.stopPropagation();\n\n const scrollOffset = getDocumentScrollOffset();\n const dragStart = getDragPositionFromClient({\n clientX: event.clientX,\n clientY: event.clientY,\n scrollOffset,\n });\n interactionSessionRef.current = {\n type: \"drag\",\n dragStart,\n dragCurrent: dragStart,\n scrollOffset,\n hasExceededThreshold: false,\n };\n dragCandidates = collectDragCandidates(scrollOffset);\n\n if (dragAnimationFrameId === null) {\n dragAnimationFrameId = requestAnimationFrame(syncDragRectToViewport);\n }\n };\n\n const handlePointerMove = (event: PointerEvent) => {\n const session = interactionSessionRef.current;\n if (session.type !== \"drag\") {\n return;\n }\n\n interactionSessionRef.current = {\n ...session,\n dragCurrent: getDragPositionFromClient({\n clientX: event.clientX,\n clientY: event.clientY,\n scrollOffset: getDocumentScrollOffset(),\n }),\n };\n postCurrentDragRect();\n };\n\n const handleViewportChangeDuringDrag = () => {\n const session = interactionSessionRef.current;\n if (session.type !== \"drag\" || dragAnimationFrameId !== null) {\n return;\n }\n\n dragAnimationFrameId = requestAnimationFrame(syncDragRectToViewport);\n };\n\n const commitElementSelection = ({\n targetElement,\n pointerEvent,\n }: {\n targetElement: Element;\n pointerEvent: PointerEvent;\n }) => {\n const selectionElement = getNearestSelectableElement(targetElement);\n const targetRect = selectionElement.getBoundingClientRect();\n const clickPosition: SelectModeClickPosition = {\n x: pointerEvent.clientX,\n y: pointerEvent.clientY,\n };\n\n // Paint the in-iframe selection frames synchronously for instant\n // feedback \u2014 symbolicate is a network roundtrip and we don't want\n // the visual selection to wait on it.\n const scrollOffset = getDocumentScrollOffset();\n const siblingElements =\n selectionElement instanceof HTMLElement\n ? getRepetitionSiblingElements({ element: selectionElement })\n : [];\n showElementSelectionFrames({\n elements: [selectionElement, ...siblingElements],\n scrollOffset,\n });\n\n const now = pointerEvent.timeStamp;\n const previousClick = lastClick;\n if (\n previousClick &&\n previousClick.target === selectionElement &&\n now - previousClick.time <= DOUBLE_CLICK_THRESHOLD_MS &&\n isTextEditableElement(selectionElement)\n ) {\n lastClick = null;\n onBeginEditing({\n element: selectionElement,\n descriptorPromise: previousClick.descriptorPromise,\n });\n return;\n }\n\n // NOTE (Noah, 2026-07-24): Commit the selection immediately rather than\n // deferring it behind the double-click window. A single click always\n // selects, and a double-click is just \"select + edit\", so there's nothing\n // to suppress \u2014 waiting only added a full DOUBLE_CLICK_THRESHOLD_MS of\n // latency to every click and left `interactionSessionRef` non-element in\n // the gap. Double-click is still detected via the `lastClick` timestamp\n // above; the follow-up click reuses this descriptor to open the editor.\n const data = buildElementDescriptor({\n element: selectionElement,\n clickPosition,\n siblingSelectors: siblingElements.map(buildSelector),\n });\n lastClick = {\n descriptorPromise: Promise.resolve(data),\n target: selectionElement,\n time: now,\n };\n\n if (!selectionElement.isConnected) {\n return;\n }\n interactionSessionRef.current = {\n type: \"element\",\n element: selectionElement,\n siblingElements,\n clickPosition: {\n x: pointerEvent.clientX - targetRect.left,\n y: pointerEvent.clientY - targetRect.top,\n },\n data,\n };\n void maybeSymbolicate(data).then((symbolicated) => {\n getWebsiteBuilderRpc()?.elementSelected({ data: symbolicated });\n });\n };\n\n const handlePointerUp = (event: PointerEvent) => {\n // Clicks inside the element being edited belong to the inline editor, not\n // select mode. Without this guard the double-click detection below re-runs\n // `onBeginEditing` on the same element (e.g. when repositioning the\n // caret), rebuilding the editor over itself and wiping the content.\n // Mirrors the guards in `handlePointerDown` and `handleClick`.\n if (\n editingElementRef?.current &&\n event.target instanceof Node &&\n editingElementRef.current.contains(event.target)\n ) {\n return;\n }\n const snapshot = interactionSessionRef.current;\n const wasDragging =\n snapshot.type === \"drag\" ? snapshot.hasExceededThreshold : false;\n const dragStart = snapshot.type === \"drag\" ? snapshot.dragStart : null;\n const dragCurrent =\n snapshot.type === \"drag\" ? snapshot.dragCurrent : null;\n const scrollOffset =\n snapshot.type === \"drag\"\n ? snapshot.scrollOffset\n : getDocumentScrollOffset();\n // `resetDragState` clears `dragCandidates`, so grab them first \u2014 the\n // final committed set is the same cached candidates the live preview\n // used, so preview and result never disagree.\n const candidates = dragCandidates;\n\n // Suppress the null drag-rect post: if this resolves to an area\n // selection it would otherwise flash the parent to `selection = none`\n // and wipe the frames we paint below. Click/empty-drag paths don't\n // depend on it (a sub-threshold drag never posted a rect to clear).\n resetDragState({ notifyParentDragCleared: false });\n\n if (!dragStart || snapshot.type !== \"drag\") {\n const targetElement = event.target;\n if (!(targetElement instanceof Element)) {\n return;\n }\n\n commitElementSelection({\n targetElement,\n pointerEvent: event,\n });\n return;\n }\n\n const resolvedDragCurrent = dragCurrent ?? dragStart;\n const selectionRect = getViewportSelectionRect({\n dragStart,\n dragCurrent: resolvedDragCurrent,\n scrollOffset,\n });\n\n if (!wasDragging) {\n const targetElement = event.target;\n if (!(targetElement instanceof Element)) {\n return;\n }\n\n commitElementSelection({\n targetElement,\n pointerEvent: event,\n });\n return;\n }\n\n const pageRect = getPageRectFromViewportRect({\n rect: selectionRect,\n scrollOffset,\n });\n\n const selectedCandidates = getCandidatesInSelection({\n candidates,\n selectionPageRect: pageRect,\n });\n const items = selectedCandidates.map((candidate) => {\n return buildElementDescriptor({\n element: candidate.element,\n clickPosition: getElementAnchor({\n element: candidate.element,\n selectionRect,\n }),\n });\n });\n\n interactionSessionRef.current = {\n type: \"area\",\n pageRect,\n items,\n };\n\n // Highlight each captured element so the committed area selection\n // \"sticks\" inside the iframe \u2014 matching how a single click stays\n // outlined \u2014 rather than drawing one faint box over the drag region.\n showSelectionFrames({\n frames: selectedCandidates.map((candidate) => ({\n variant: \"area\" as const,\n pageRect: {\n x: candidate.pageRect.x,\n y: candidate.pageRect.y,\n width: candidate.pageRect.width,\n height: candidate.pageRect.height,\n },\n })),\n });\n\n void Promise.all(items.map(maybeSymbolicate)).then(\n (symbolicatedItems) => {\n getWebsiteBuilderRpc()?.areaSelected({\n rect: selectionRect,\n items: symbolicatedItems,\n });\n },\n );\n };\n\n const handleClick = (event: MouseEvent) => {\n if (\n editingElementRef?.current &&\n event.target instanceof Node &&\n editingElementRef.current.contains(event.target)\n ) {\n return;\n }\n event.preventDefault();\n event.stopPropagation();\n };\n\n const handlePointerCancel = () => {\n resetDragState();\n };\n\n const handleWindowBlur = () => {\n resetDragState();\n };\n\n document.addEventListener(\"pointerdown\", handlePointerDown, true);\n document.addEventListener(\"pointermove\", handlePointerMove, true);\n document.addEventListener(\"pointerup\", handlePointerUp, true);\n document.addEventListener(\"pointercancel\", handlePointerCancel, true);\n document.addEventListener(\"click\", handleClick, true);\n document.addEventListener(\"scroll\", handleViewportChangeDuringDrag, true);\n window.addEventListener(\"blur\", handleWindowBlur);\n window.addEventListener(\"resize\", handleViewportChangeDuringDrag);\n\n return () => {\n document.removeEventListener(\"pointerdown\", handlePointerDown, true);\n document.removeEventListener(\"pointermove\", handlePointerMove, true);\n document.removeEventListener(\"pointerup\", handlePointerUp, true);\n document.removeEventListener(\"pointercancel\", handlePointerCancel, true);\n document.removeEventListener(\"click\", handleClick, true);\n document.removeEventListener(\n \"scroll\",\n handleViewportChangeDuringDrag,\n true,\n );\n window.removeEventListener(\"blur\", handleWindowBlur);\n window.removeEventListener(\"resize\", handleViewportChangeDuringDrag);\n resetDragState();\n };\n }, [displayMode, editingElementRef, interactionSessionRef, onBeginEditing]);\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import type { MutableRefObject } from \"react\";\nimport type {\n DisplayMode,\n SelectableData,\n SelectModeClickPosition,\n} from \"schemas/generated/selectMode\";\nimport type { DragCandidate } from \"../interactions/selectionGeometry\";\nimport type { SelectModeInteractionSession } from \"../selectModeSession\";\n\nimport { useEffect } from \"react\";\n\nimport {\n buildElementDescriptor,\n buildSelector,\n getNearestSelectableElement,\n} from \"../descriptor/elementDescriptor\";\nimport { getWebsiteBuilderRpc } from \"../iframeRpcChannel\";\nimport {\n collectDragCandidates,\n getCandidatesInSelection,\n getDocumentScrollOffset,\n getDragPositionFromClient,\n getElementAnchor,\n getPageRectFromViewportRect,\n getViewportRectFromPageRect,\n getViewportSelectionRect,\n} from \"../interactions/selectionGeometry\";\nimport {\n showElementSelectionFrames,\n showSelectionFrames,\n} from \"../selectionFrame\";\nimport { IDLE_SESSION } from \"../selectModeSession\";\nimport { symbolicateSelectedElement } from \"../symbolicate\";\nimport { isTextEditableElement } from \"./useEditModeStateMachine\";\nimport { getRepetitionSiblingElements } from \"./useSiblingResolution\";\n\nconst DOUBLE_CLICK_THRESHOLD_MS = 500;\n\n// React fibers carry only Turbopack chunk paths; resolving them to the\n// user's source file is async and only meaningful for \"react-element\"\n// kinds. Plan selectables already point at the right thing.\nasync function maybeSymbolicate(data: SelectableData): Promise<SelectableData> {\n return data.kind === \"react-element\"\n ? symbolicateSelectedElement(data)\n : data;\n}\n\n/**\n * Owns the iframe-side pointer interactions that drive selection:\n *\n * - On `pointerdown` we open a `drag` session (storing both client + page\n * coords plus the current scroll offset). We don't yet know if this will\n * become a click or an actual drag.\n * - On `pointermove` we update the drag rect and post `DRAG_RECT_UPDATED`\n * to the parent so the live drag rectangle can render. We only flip\n * `hasExceededThreshold` once the rect grows past 6\u00D76 px so that a tiny\n * accidental drag still resolves as a click.\n * - On `pointerup` we either:\n * - resolve as an `ELEMENT_SELECTED` (single click / sub-threshold drag)\n * - resolve as an `AREA_SELECTED` (drag past threshold), filtering the\n * candidates snapshotted at drag start against the final rectangle to\n * enumerate which descendants the user enclosed.\n * - During an active drag a `requestAnimationFrame` loop keeps the rect in\n * sync with the page scroll so the rectangle visually \"sticks\" to the\n * page rather than the viewport.\n *\n * We capture-listen and `preventDefault`/`stopPropagation` clicks so the\n * underlying page can't navigate or trigger handlers while the user is\n * working in selection mode.\n *\n * @author Kevin O'Connell 2026-04-17\n */\nexport function useSelectionInteractions({\n displayMode,\n editingElementRef,\n interactionSessionRef,\n onBeginEditing,\n}: {\n displayMode: DisplayMode;\n editingElementRef?: MutableRefObject<HTMLElement | null>;\n interactionSessionRef: MutableRefObject<SelectModeInteractionSession>;\n onBeginEditing: (args: {\n element: Element;\n descriptorPromise: Promise<SelectableData>;\n }) => void;\n}) {\n // eslint-disable-next-line replo/no-use-effect -- Legacy effect - may or may not be necessary\n useEffect(() => {\n if (displayMode !== \"selection\") {\n return;\n }\n\n let dragAnimationFrameId: number | null = null;\n const editingPointerIds = new Set<number>();\n let lastClick: {\n descriptorPromise: Promise<SelectableData>;\n target: Element;\n time: number;\n } | null = null;\n // Snapshot of the page's meaningful elements + their page rects, taken\n // once when a drag begins so per-move hit-testing avoids a full-DOM scan.\n let dragCandidates: DragCandidate[] = [];\n\n const stopDragAnimation = () => {\n if (dragAnimationFrameId === null) {\n return;\n }\n\n cancelAnimationFrame(dragAnimationFrameId);\n dragAnimationFrameId = null;\n };\n\n // `notifyParentDragCleared` posts a null drag rect so the parent tears\n // down the live drag overlay. The area-commit path suppresses it: the\n // parent maps a null drag rect to `selection = none`, which round-trips\n // back as `setSelectionType(\"none\")` and wipes the in-iframe frames we're\n // about to paint. The follow-up `areaSelected` supersedes the drag overlay\n // on its own, so the null post is pure collateral damage there.\n const resetDragState = ({\n notifyParentDragCleared = true,\n }: { notifyParentDragCleared?: boolean } = {}) => {\n const currentSession = interactionSessionRef.current;\n\n stopDragAnimation();\n\n if (currentSession.type !== \"drag\") {\n dragCandidates = [];\n return;\n }\n\n if (notifyParentDragCleared && currentSession.hasExceededThreshold) {\n getWebsiteBuilderRpc()?.dragRectUpdated({\n dragRect: null,\n selectionRects: [],\n });\n }\n\n dragCandidates = [];\n interactionSessionRef.current = IDLE_SESSION;\n };\n\n const postCurrentDragRect = () => {\n const session = interactionSessionRef.current;\n if (session.type !== \"drag\") {\n return;\n }\n\n const rect = getViewportSelectionRect({\n dragStart: session.dragStart,\n dragCurrent: session.dragCurrent,\n scrollOffset: session.scrollOffset,\n });\n\n if (\n rect.width <= 6 &&\n rect.height <= 6 &&\n !session.hasExceededThreshold\n ) {\n return;\n }\n\n if (!session.hasExceededThreshold) {\n interactionSessionRef.current = {\n ...session,\n hasExceededThreshold: true,\n };\n }\n\n // Cheap hit-test against the candidates we snapshotted at drag start,\n // projected back to viewport coords so the parent can paint a live\n // highlight on each element the drag will capture. No DOM scan here.\n const selectionPageRect = getPageRectFromViewportRect({\n rect,\n scrollOffset: session.scrollOffset,\n });\n const selectionRects = getCandidatesInSelection({\n candidates: dragCandidates,\n selectionPageRect,\n }).map((candidate) => ({\n id: candidate.id,\n rect: getViewportRectFromPageRect({\n rect: candidate.pageRect,\n scrollOffset: session.scrollOffset,\n }),\n }));\n\n getWebsiteBuilderRpc()?.dragRectUpdated({\n dragRect: rect,\n selectionRects,\n });\n };\n\n const syncDragRectToViewport = () => {\n const session = interactionSessionRef.current;\n if (session.type !== \"drag\") {\n dragAnimationFrameId = null;\n return;\n }\n\n const nextScrollOffset = getDocumentScrollOffset();\n const nextDragCurrent = getDragPositionFromClient({\n clientX: session.dragCurrent.clientX,\n clientY: session.dragCurrent.clientY,\n scrollOffset: nextScrollOffset,\n });\n\n if (\n nextDragCurrent.pageX !== session.dragCurrent.pageX ||\n nextDragCurrent.pageY !== session.dragCurrent.pageY\n ) {\n interactionSessionRef.current = {\n ...session,\n dragCurrent: nextDragCurrent,\n scrollOffset: nextScrollOffset,\n };\n postCurrentDragRect();\n } else {\n interactionSessionRef.current = {\n ...session,\n scrollOffset: nextScrollOffset,\n };\n }\n\n dragAnimationFrameId = requestAnimationFrame(syncDragRectToViewport);\n };\n\n const handlePointerDown = (event: PointerEvent) => {\n editingPointerIds.delete(event.pointerId);\n if (\n editingElementRef?.current &&\n event.target instanceof Node &&\n editingElementRef.current.contains(event.target)\n ) {\n editingPointerIds.add(event.pointerId);\n return;\n }\n event.preventDefault();\n event.stopPropagation();\n\n const scrollOffset = getDocumentScrollOffset();\n const dragStart = getDragPositionFromClient({\n clientX: event.clientX,\n clientY: event.clientY,\n scrollOffset,\n });\n interactionSessionRef.current = {\n type: \"drag\",\n dragStart,\n dragCurrent: dragStart,\n scrollOffset,\n hasExceededThreshold: false,\n };\n dragCandidates = collectDragCandidates(scrollOffset);\n\n if (dragAnimationFrameId === null) {\n dragAnimationFrameId = requestAnimationFrame(syncDragRectToViewport);\n }\n };\n\n const handlePointerMove = (event: PointerEvent) => {\n const session = interactionSessionRef.current;\n if (session.type !== \"drag\") {\n return;\n }\n\n interactionSessionRef.current = {\n ...session,\n dragCurrent: getDragPositionFromClient({\n clientX: event.clientX,\n clientY: event.clientY,\n scrollOffset: getDocumentScrollOffset(),\n }),\n };\n postCurrentDragRect();\n };\n\n const handleViewportChangeDuringDrag = () => {\n const session = interactionSessionRef.current;\n if (session.type !== \"drag\" || dragAnimationFrameId !== null) {\n return;\n }\n\n dragAnimationFrameId = requestAnimationFrame(syncDragRectToViewport);\n };\n\n const commitElementSelection = ({\n targetElement,\n pointerEvent,\n }: {\n targetElement: Element;\n pointerEvent: PointerEvent;\n }) => {\n const selectionElement = getNearestSelectableElement(targetElement);\n const targetRect = selectionElement.getBoundingClientRect();\n const clickPosition: SelectModeClickPosition = {\n x: pointerEvent.clientX,\n y: pointerEvent.clientY,\n };\n\n // Paint the in-iframe selection frames synchronously for instant\n // feedback \u2014 symbolicate is a network roundtrip and we don't want\n // the visual selection to wait on it.\n const scrollOffset = getDocumentScrollOffset();\n const siblingElements =\n selectionElement instanceof HTMLElement\n ? getRepetitionSiblingElements({ element: selectionElement })\n : [];\n showElementSelectionFrames({\n elements: [selectionElement, ...siblingElements],\n scrollOffset,\n });\n\n const now = pointerEvent.timeStamp;\n const previousClick = lastClick;\n if (\n previousClick &&\n previousClick.target === selectionElement &&\n now - previousClick.time <= DOUBLE_CLICK_THRESHOLD_MS &&\n isTextEditableElement(selectionElement)\n ) {\n lastClick = null;\n onBeginEditing({\n element: selectionElement,\n descriptorPromise: previousClick.descriptorPromise,\n });\n return;\n }\n\n // NOTE (Noah, 2026-07-24): Commit the selection immediately rather than\n // deferring it behind the double-click window. A single click always\n // selects, and a double-click is just \"select + edit\", so there's nothing\n // to suppress \u2014 waiting only added a full DOUBLE_CLICK_THRESHOLD_MS of\n // latency to every click and left `interactionSessionRef` non-element in\n // the gap. Double-click is still detected via the `lastClick` timestamp\n // above; the follow-up click reuses this descriptor to open the editor.\n const data = buildElementDescriptor({\n element: selectionElement,\n clickPosition,\n siblingSelectors: siblingElements.map(buildSelector),\n });\n lastClick = {\n descriptorPromise: Promise.resolve(data),\n target: selectionElement,\n time: now,\n };\n\n if (!selectionElement.isConnected) {\n return;\n }\n interactionSessionRef.current = {\n type: \"element\",\n element: selectionElement,\n siblingElements,\n clickPosition: {\n x: pointerEvent.clientX - targetRect.left,\n y: pointerEvent.clientY - targetRect.top,\n },\n data,\n };\n void maybeSymbolicate(data).then((symbolicated) => {\n getWebsiteBuilderRpc()?.elementSelected({ data: symbolicated });\n });\n };\n\n const handlePointerUp = (event: PointerEvent) => {\n if (editingPointerIds.delete(event.pointerId)) {\n return;\n }\n // Clicks inside the element being edited belong to the inline editor, not\n // select mode. Without this guard the double-click detection below re-runs\n // `onBeginEditing` on the same element (e.g. when repositioning the\n // caret), rebuilding the editor over itself and wiping the content.\n // Mirrors the guards in `handlePointerDown` and `handleClick`.\n if (\n editingElementRef?.current &&\n event.target instanceof Node &&\n editingElementRef.current.contains(event.target)\n ) {\n return;\n }\n const snapshot = interactionSessionRef.current;\n const wasDragging =\n snapshot.type === \"drag\" ? snapshot.hasExceededThreshold : false;\n const dragStart = snapshot.type === \"drag\" ? snapshot.dragStart : null;\n const dragCurrent =\n snapshot.type === \"drag\" ? snapshot.dragCurrent : null;\n const scrollOffset =\n snapshot.type === \"drag\"\n ? snapshot.scrollOffset\n : getDocumentScrollOffset();\n // `resetDragState` clears `dragCandidates`, so grab them first \u2014 the\n // final committed set is the same cached candidates the live preview\n // used, so preview and result never disagree.\n const candidates = dragCandidates;\n\n // Suppress the null drag-rect post: if this resolves to an area\n // selection it would otherwise flash the parent to `selection = none`\n // and wipe the frames we paint below. Click/empty-drag paths don't\n // depend on it (a sub-threshold drag never posted a rect to clear).\n resetDragState({ notifyParentDragCleared: false });\n\n if (!dragStart || snapshot.type !== \"drag\") {\n const targetElement = event.target;\n if (!(targetElement instanceof Element)) {\n return;\n }\n\n commitElementSelection({\n targetElement,\n pointerEvent: event,\n });\n return;\n }\n\n const resolvedDragCurrent = dragCurrent ?? dragStart;\n const selectionRect = getViewportSelectionRect({\n dragStart,\n dragCurrent: resolvedDragCurrent,\n scrollOffset,\n });\n\n if (!wasDragging) {\n const targetElement = event.target;\n if (!(targetElement instanceof Element)) {\n return;\n }\n\n commitElementSelection({\n targetElement,\n pointerEvent: event,\n });\n return;\n }\n\n const pageRect = getPageRectFromViewportRect({\n rect: selectionRect,\n scrollOffset,\n });\n\n const selectedCandidates = getCandidatesInSelection({\n candidates,\n selectionPageRect: pageRect,\n });\n const items = selectedCandidates.map((candidate) => {\n return buildElementDescriptor({\n element: candidate.element,\n clickPosition: getElementAnchor({\n element: candidate.element,\n selectionRect,\n }),\n });\n });\n\n interactionSessionRef.current = {\n type: \"area\",\n pageRect,\n items,\n };\n\n // Highlight each captured element so the committed area selection\n // \"sticks\" inside the iframe \u2014 matching how a single click stays\n // outlined \u2014 rather than drawing one faint box over the drag region.\n showSelectionFrames({\n frames: selectedCandidates.map((candidate) => ({\n variant: \"area\" as const,\n pageRect: {\n x: candidate.pageRect.x,\n y: candidate.pageRect.y,\n width: candidate.pageRect.width,\n height: candidate.pageRect.height,\n },\n })),\n });\n\n void Promise.all(items.map(maybeSymbolicate)).then(\n (symbolicatedItems) => {\n getWebsiteBuilderRpc()?.areaSelected({\n rect: selectionRect,\n items: symbolicatedItems,\n });\n },\n );\n };\n\n const handleClick = (event: MouseEvent) => {\n if (\n editingElementRef?.current &&\n event.target instanceof Node &&\n editingElementRef.current.contains(event.target)\n ) {\n return;\n }\n event.preventDefault();\n event.stopPropagation();\n };\n\n const handlePointerCancel = (event: PointerEvent) => {\n editingPointerIds.delete(event.pointerId);\n resetDragState();\n };\n\n const handleWindowBlur = () => {\n editingPointerIds.clear();\n resetDragState();\n };\n\n document.addEventListener(\"pointerdown\", handlePointerDown, true);\n document.addEventListener(\"pointermove\", handlePointerMove, true);\n document.addEventListener(\"pointerup\", handlePointerUp, true);\n document.addEventListener(\"pointercancel\", handlePointerCancel, true);\n document.addEventListener(\"click\", handleClick, true);\n document.addEventListener(\"scroll\", handleViewportChangeDuringDrag, true);\n window.addEventListener(\"blur\", handleWindowBlur);\n window.addEventListener(\"resize\", handleViewportChangeDuringDrag);\n\n return () => {\n document.removeEventListener(\"pointerdown\", handlePointerDown, true);\n document.removeEventListener(\"pointermove\", handlePointerMove, true);\n document.removeEventListener(\"pointerup\", handlePointerUp, true);\n document.removeEventListener(\"pointercancel\", handlePointerCancel, true);\n document.removeEventListener(\"click\", handleClick, true);\n document.removeEventListener(\n \"scroll\",\n handleViewportChangeDuringDrag,\n true,\n );\n window.removeEventListener(\"blur\", handleWindowBlur);\n window.removeEventListener(\"resize\", handleViewportChangeDuringDrag);\n resetDragState();\n };\n }, [displayMode, editingElementRef, interactionSessionRef, onBeginEditing]);\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -60,7 +60,10 @@ function createInlineTiptapEditor({
60
60
  editorProps: {
61
61
  // Drop the browser's default focus ring on the contenteditable — the
62
62
  // select-mode selection frame already indicates the edit target.
63
- attributes: { style: "outline: none;" },
63
+ attributes: {
64
+ "data-select-mode-inline-editor": "",
65
+ style: "outline: none;"
66
+ },
64
67
  handleKeyDown: (_view, event) => {
65
68
  if (event.key === "Enter" && (event.shiftKey || event.metaKey || event.ctrlKey)) {
66
69
  event.preventDefault();
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../select-mode/tiptapInlineEditor.ts"],
4
- "sourcesContent": ["import type { JSONContent } from \"@tiptap/core\";\nimport type { SelectModeFormatCommand } from \"schemas/generated/selectMode\";\n\nimport { Editor, Mark } from \"@tiptap/core\";\nimport Bold from \"@tiptap/extension-bold\";\nimport Document from \"@tiptap/extension-document\";\nimport Italic from \"@tiptap/extension-italic\";\nimport Strike from \"@tiptap/extension-strike\";\nimport Text from \"@tiptap/extension-text\";\nimport Underline from \"@tiptap/extension-underline\";\n\nconst InlineDocument = Document.extend({\n content: \"inline*\",\n});\n\nconst InlineLink = Mark.create({\n name: \"link\",\n inclusive: false,\n addAttributes() {\n return {\n class: { default: null },\n href: { default: null },\n rel: { default: null },\n target: { default: null },\n };\n },\n parseHTML() {\n return [{ tag: \"a[href]\" }];\n },\n renderHTML({ HTMLAttributes }) {\n return [\"a\", HTMLAttributes, 0];\n },\n});\n\nexport type InlineTiptapEditor = {\n focus: () => void;\n focusAndSelectAll: () => void;\n destroy: () => void;\n getHTML: () => string;\n getJSON: () => JSONContent;\n runFormatCommand: (command: SelectModeFormatCommand) => void;\n};\n\nexport function createInlineTiptapEditor({\n element,\n content,\n onCommit,\n onEscape,\n onBlur,\n onSelectionUpdate,\n}: {\n element: HTMLElement;\n content: string;\n onCommit: () => void;\n onEscape: () => void;\n onBlur: () => void;\n onSelectionUpdate: (activeMarks: {\n bold: boolean;\n italic: boolean;\n underline: boolean;\n strike: boolean;\n }) => void;\n}): InlineTiptapEditor {\n const originalAttributes = Array.from(\n element.attributes,\n ({ name, value }) => ({\n name,\n value,\n }),\n );\n // ProseMirror *appends* its contenteditable to the mount node rather than\n // replacing it, so the element's original text node would survive as a\n // sibling and render a duplicate copy. The editor's initial content comes\n // from the `content` option below (not the element's DOM), and the caller\n // keeps the original HTML for revert, so emptying the node here is safe.\n element.replaceChildren();\n const mount = {\n mount: element,\n } as unknown as Element; /* eslint-disable-line replo/no-as-cast -- NOTE (Noah Gilmore, 2026-07-16): ProseMirror accepts this documented direct-mount shape at runtime, but Tiptap v2's Editor option incorrectly narrows it to Element. */\n const editor = new Editor({\n element: mount,\n content,\n extensions: [\n InlineDocument,\n Text,\n Bold,\n Italic,\n Underline,\n Strike,\n InlineLink,\n ],\n editorProps: {\n // Drop the browser's default focus ring on the contenteditable \u2014 the\n // select-mode selection frame already indicates the edit target.\n attributes: { style: \"outline: none;\" },\n handleKeyDown: (_view, event) => {\n // Commit on Shift-Enter or Cmd/Ctrl-Enter; a bare Enter inserts a line\n // break as usual.\n if (\n event.key === \"Enter\" &&\n (event.shiftKey || event.metaKey || event.ctrlKey)\n ) {\n event.preventDefault();\n onCommit();\n return true;\n }\n if (event.key === \"Escape\") {\n event.preventDefault();\n onEscape();\n return true;\n }\n return false;\n },\n },\n onSelectionUpdate: ({ editor: activeEditor }) => {\n onSelectionUpdate({\n bold: activeEditor.isActive(\"bold\"),\n italic: activeEditor.isActive(\"italic\"),\n underline: activeEditor.isActive(\"underline\"),\n strike: activeEditor.isActive(\"strike\"),\n });\n },\n onBlur: () => {\n onBlur();\n },\n });\n\n return {\n focus: () => editor.commands.focus(),\n // Focus the editor with the full text block selected so the user can type\n // to replace it immediately (or arrow/click to drop the selection).\n focusAndSelectAll: () => editor.chain().focus().selectAll().run(),\n destroy: () => {\n editor.destroy();\n for (const attribute of Array.from(element.attributes)) {\n element.removeAttribute(attribute.name);\n }\n for (const { name, value } of originalAttributes) {\n element.setAttribute(name, value);\n }\n },\n getHTML: () => editor.getHTML(),\n getJSON: () => editor.getJSON(),\n runFormatCommand: (command) => {\n const commandRunners: Record<SelectModeFormatCommand, () => void> = {\n toggleBold: () => editor.chain().focus().toggleBold().run(),\n toggleItalic: () => editor.chain().focus().toggleItalic().run(),\n toggleUnderline: () => editor.chain().focus().toggleUnderline().run(),\n toggleStrike: () => editor.chain().focus().toggleStrike().run(),\n };\n commandRunners[command]();\n },\n };\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import type { JSONContent } from \"@tiptap/core\";\nimport type { SelectModeFormatCommand } from \"schemas/generated/selectMode\";\n\nimport { Editor, Mark } from \"@tiptap/core\";\nimport Bold from \"@tiptap/extension-bold\";\nimport Document from \"@tiptap/extension-document\";\nimport Italic from \"@tiptap/extension-italic\";\nimport Strike from \"@tiptap/extension-strike\";\nimport Text from \"@tiptap/extension-text\";\nimport Underline from \"@tiptap/extension-underline\";\n\nconst InlineDocument = Document.extend({\n content: \"inline*\",\n});\n\nconst InlineLink = Mark.create({\n name: \"link\",\n inclusive: false,\n addAttributes() {\n return {\n class: { default: null },\n href: { default: null },\n rel: { default: null },\n target: { default: null },\n };\n },\n parseHTML() {\n return [{ tag: \"a[href]\" }];\n },\n renderHTML({ HTMLAttributes }) {\n return [\"a\", HTMLAttributes, 0];\n },\n});\n\nexport type InlineTiptapEditor = {\n focus: () => void;\n focusAndSelectAll: () => void;\n destroy: () => void;\n getHTML: () => string;\n getJSON: () => JSONContent;\n runFormatCommand: (command: SelectModeFormatCommand) => void;\n};\n\nexport function createInlineTiptapEditor({\n element,\n content,\n onCommit,\n onEscape,\n onBlur,\n onSelectionUpdate,\n}: {\n element: HTMLElement;\n content: string;\n onCommit: () => void;\n onEscape: () => void;\n onBlur: () => void;\n onSelectionUpdate: (activeMarks: {\n bold: boolean;\n italic: boolean;\n underline: boolean;\n strike: boolean;\n }) => void;\n}): InlineTiptapEditor {\n const originalAttributes = Array.from(\n element.attributes,\n ({ name, value }) => ({\n name,\n value,\n }),\n );\n // ProseMirror *appends* its contenteditable to the mount node rather than\n // replacing it, so the element's original text node would survive as a\n // sibling and render a duplicate copy. The editor's initial content comes\n // from the `content` option below (not the element's DOM), and the caller\n // keeps the original HTML for revert, so emptying the node here is safe.\n element.replaceChildren();\n const mount = {\n mount: element,\n } as unknown as Element; /* eslint-disable-line replo/no-as-cast -- NOTE (Noah Gilmore, 2026-07-16): ProseMirror accepts this documented direct-mount shape at runtime, but Tiptap v2's Editor option incorrectly narrows it to Element. */\n const editor = new Editor({\n element: mount,\n content,\n extensions: [\n InlineDocument,\n Text,\n Bold,\n Italic,\n Underline,\n Strike,\n InlineLink,\n ],\n editorProps: {\n // Drop the browser's default focus ring on the contenteditable \u2014 the\n // select-mode selection frame already indicates the edit target.\n attributes: {\n \"data-select-mode-inline-editor\": \"\",\n style: \"outline: none;\",\n },\n handleKeyDown: (_view, event) => {\n // Commit on Shift-Enter or Cmd/Ctrl-Enter; a bare Enter inserts a line\n // break as usual.\n if (\n event.key === \"Enter\" &&\n (event.shiftKey || event.metaKey || event.ctrlKey)\n ) {\n event.preventDefault();\n onCommit();\n return true;\n }\n if (event.key === \"Escape\") {\n event.preventDefault();\n onEscape();\n return true;\n }\n return false;\n },\n },\n onSelectionUpdate: ({ editor: activeEditor }) => {\n onSelectionUpdate({\n bold: activeEditor.isActive(\"bold\"),\n italic: activeEditor.isActive(\"italic\"),\n underline: activeEditor.isActive(\"underline\"),\n strike: activeEditor.isActive(\"strike\"),\n });\n },\n onBlur: () => {\n onBlur();\n },\n });\n\n return {\n focus: () => editor.commands.focus(),\n // Focus the editor with the full text block selected so the user can type\n // to replace it immediately (or arrow/click to drop the selection).\n focusAndSelectAll: () => editor.chain().focus().selectAll().run(),\n destroy: () => {\n editor.destroy();\n for (const attribute of Array.from(element.attributes)) {\n element.removeAttribute(attribute.name);\n }\n for (const { name, value } of originalAttributes) {\n element.setAttribute(name, value);\n }\n },\n getHTML: () => editor.getHTML(),\n getJSON: () => editor.getJSON(),\n runFormatCommand: (command) => {\n const commandRunners: Record<SelectModeFormatCommand, () => void> = {\n toggleBold: () => editor.chain().focus().toggleBold().run(),\n toggleItalic: () => editor.chain().focus().toggleItalic().run(),\n toggleUnderline: () => editor.chain().focus().toggleUnderline().run(),\n toggleStrike: () => editor.chain().focus().toggleStrike().run(),\n };\n commandRunners[command]();\n },\n };\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }