@replohq/sandbox-runtime 0.3.0 → 1.0.1

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.
Files changed (37) hide show
  1. package/_vendor/schemas/generated/selectModeIframeRpc.d.ts +8 -0
  2. package/_vendor/schemas/selectModeIframeRpc.d.ts +21 -1
  3. package/_vendor/schemas/selectModeIframeRpc.mjs +11 -3
  4. package/buildMetadata.d.ts +9 -0
  5. package/buildMetadata.js +7 -0
  6. package/buildMetadata.js.map +7 -0
  7. package/package.json +17 -5
  8. package/select-mode/EditorSelection.js +8 -2
  9. package/select-mode/EditorSelection.js.map +2 -2
  10. package/select-mode/fiber.d.ts +12 -17
  11. package/select-mode/fiber.js +228 -64
  12. package/select-mode/fiber.js.map +2 -2
  13. package/select-mode/hooks/useEditModeStateMachine.js +2 -0
  14. package/select-mode/hooks/useEditModeStateMachine.js.map +2 -2
  15. package/select-mode/hooks/useOptimisticOverrides.js +2 -2
  16. package/select-mode/hooks/useOptimisticOverrides.js.map +2 -2
  17. package/select-mode/hooks/useParentMessaging.js +21 -1
  18. package/select-mode/hooks/useParentMessaging.js.map +2 -2
  19. package/select-mode/hooks/useSelectModeController.js +2 -0
  20. package/select-mode/hooks/useSelectModeController.js.map +2 -2
  21. package/select-mode/hooks/useSelectionHover.d.ts +4 -5
  22. package/select-mode/hooks/useSelectionHover.js +21 -3
  23. package/select-mode/hooks/useSelectionHover.js.map +2 -2
  24. package/select-mode/hooks/useSelectionInteractions.js +12 -11
  25. package/select-mode/hooks/useSelectionInteractions.js.map +2 -2
  26. package/select-mode/hooks/useSelectionScrollSync.d.ts +3 -4
  27. package/select-mode/hooks/useSelectionScrollSync.js +4 -9
  28. package/select-mode/hooks/useSelectionScrollSync.js.map +2 -2
  29. package/select-mode/hooks/useSiblingResolution.d.ts +2 -2
  30. package/select-mode/hooks/useSiblingResolution.js +10 -47
  31. package/select-mode/hooks/useSiblingResolution.js.map +2 -2
  32. package/select-mode/iframeRpcChannel.d.ts +7 -1
  33. package/select-mode/selectModeSession.d.ts +1 -0
  34. package/select-mode/selectModeSession.js.map +2 -2
  35. package/select-mode/selectionFrame.d.ts +13 -17
  36. package/select-mode/selectionFrame.js +18 -5
  37. package/select-mode/selectionFrame.js.map +2 -2
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../select-mode/fiber.ts"],
4
- "sourcesContent": ["// =============================================================================\n// React Fiber / Source Location Utilities\n// =============================================================================\n\nimport { z } from \"zod\";\n\nexport interface ReactFiber {\n _debugSource?: {\n fileName: string;\n lineNumber: number;\n columnNumber?: number;\n };\n _debugStack?: unknown;\n _debugOwner?: ReactFiber;\n type?:\n | { name?: string; displayName?: string }\n | string\n | ((...args: unknown[]) => unknown)\n | null;\n return?: ReactFiber | null;\n memoizedProps?: Record<string, unknown>;\n pendingProps?: Record<string, unknown>;\n}\n\nconst MAX_FIBER_CHAIN_DEPTH = 80;\nconst MAX_SERIALIZED_PROP_COUNT = 24;\nconst MAX_SERIALIZED_STRING_LENGTH = 500;\n\n// Frames produced by React's own JSX runtime \u2014 these come first in\n// `_debugStack.stack` because the `Error` is constructed inside `jsxDEV`. We\n// want the frame *above* them, which is the actual JSX call site.\n//\n// Turbopack rewrites `node_modules/next/dist/compiled/react-server-dom-*` and\n// friends into mangled chunk paths like\n// `_next/static/chunks/<hash>_next_dist_compiled_react-server-dom-turbopack_<hash>._.js`,\n// so we match both the human-readable module paths and the chunk-name forms.\n// Without this, `_debugStack`'s top frame inside React's RSC runtime gets\n// surfaced as the \"host\" source location for every text node on the page \u2014\n// which then symbolicates back to bundled React, not user code.\nconst REACT_INTERNAL_FRAME_PATTERNS = [\n /react.*jsx-dev-runtime/,\n /react.*jsx-runtime/,\n /react-dom\\//,\n /react\\/cjs\\/react\\./,\n /react-server-dom/,\n /react_server_dom/,\n /next[/_.]dist[/_.]compiled/,\n /\\/node_modules\\/(next|react|react-dom)\\//,\n];\n\nfunction isReactInternalFrame(fileName: string): boolean {\n return REACT_INTERNAL_FRAME_PATTERNS.some((pattern) => {\n return pattern.test(fileName);\n });\n}\n\n// React constructs the `_debugStack` Error inside its JSX runtime (`jsxDEV`,\n// `jsx`, `jsxs`), so those frames sit *above* the real JSX call site we want.\n// The file-based check above skips them when their path is recognizable, but\n// Turbopack mangles the client JSX-runtime chunk to an opaque name like\n// `node_modules__pnpm_<hash>._.js` (no \"react\"/\"jsx-dev-runtime\" substring),\n// so the top `jsxDEV` frame slips through and gets surfaced as the element's\n// source \u2014 which then symbolicates back to bundled React, not user code.\n// Matching by function name is stable across Turbopack's chunk hashing.\n// NOTE (Noah, 2026-07-27): Only bites client components; server-component\n// frames carry a recognizable `react-server-dom-*` path.\nconst REACT_INTERNAL_METHOD_PATTERNS = [\n /(?:^|\\.)jsxDEV(?:Impl)?$/,\n /(?:^|\\.)jsxs?$/,\n /(?:^|\\.)react_stack_bottom_frame$/,\n /(?:^|\\.)fakeJSXCallSite$/,\n];\n\nfunction isReactInternalMethod(methodName: string | null): boolean {\n if (!methodName) {\n return false;\n }\n return REACT_INTERNAL_METHOD_PATTERNS.some((pattern) => {\n return pattern.test(methodName);\n });\n}\n\nfunction parseDebugStackSource(\n debugStack: unknown,\n): ReactFiber[\"_debugSource\"] | null {\n let stackText: string | null = null;\n\n if (typeof debugStack === \"string\") {\n stackText = debugStack;\n } else if (\n typeof debugStack === \"object\" &&\n debugStack &&\n \"stack\" in debugStack\n ) {\n // Legacy cast - may or may not be necessary\n // eslint-disable-next-line replo/no-as-cast\n const stack = (debugStack as { stack?: unknown }).stack;\n if (typeof stack === \"string\") {\n stackText = stack;\n }\n }\n\n if (!stackText) {\n return null;\n }\n\n for (const rawLine of stackText.split(\"\\n\")) {\n const line = rawLine.trim();\n if (!line) {\n continue;\n }\n\n // Prefer the framed form `at <method> (<file>:<line>:<col>)` so we can\n // read the function name and skip React's JSX-runtime frames even when\n // Turbopack has mangled their chunk file name beyond recognition.\n const framedMatch = line.match(/^at\\s+(.+?)\\s+\\((.*):(\\d+):(\\d+)\\)$/);\n const stackFrameMatch =\n framedMatch ??\n line.match(/^at\\s+(?:.*\\()?(.*):(\\d+):(\\d+)\\)?$/) ??\n line.match(/^(.*):(\\d+):(\\d+)$/);\n\n if (!stackFrameMatch) {\n continue;\n }\n\n const methodName = framedMatch ? framedMatch[1]!.trim() : null;\n const fileOffset = framedMatch ? 2 : 1;\n const fileName = stackFrameMatch[fileOffset]?.trim();\n const lineNumber = Number.parseInt(\n stackFrameMatch[fileOffset + 1] ?? \"\",\n 10,\n );\n const columnNumber = Number.parseInt(\n stackFrameMatch[fileOffset + 2] ?? \"\",\n 10,\n );\n\n if (\n !fileName ||\n !Number.isFinite(lineNumber) ||\n !Number.isFinite(columnNumber)\n ) {\n continue;\n }\n\n if (isReactInternalFrame(fileName) || isReactInternalMethod(methodName)) {\n continue;\n }\n\n return {\n fileName,\n lineNumber,\n columnNumber,\n };\n }\n\n return null;\n}\n\nexport function getFiberFromElement(element: HTMLElement): ReactFiber | null {\n if (!element || typeof element !== \"object\") {\n return null;\n }\n\n const symbolKeys = Object.getOwnPropertySymbols(element);\n const fiberSymbolKey = symbolKeys.find((symbolKey) => {\n return symbolKey.toString().includes(\"reactFiber\");\n });\n if (fiberSymbolKey) {\n return (\n // Legacy cast - may or may not be necessary\n // eslint-disable-next-line replo/no-as-cast\n (element as unknown as Record<symbol, ReactFiber>)[fiberSymbolKey] ?? null\n );\n }\n\n const keys = Object.keys(element);\n const fiberKey = keys.find((key) => key.startsWith(\"__reactFiber$\"));\n if (fiberKey) {\n // Legacy cast - may or may not be necessary\n // eslint-disable-next-line replo/no-as-cast\n return (element as unknown as Record<string, ReactFiber>)[fiberKey] ?? null;\n }\n const instanceKey = keys.find((key) => {\n return key.startsWith(\"__reactInternalInstance$\");\n });\n if (instanceKey) {\n return (\n // Legacy cast - may or may not be necessary\n // eslint-disable-next-line replo/no-as-cast\n (element as unknown as Record<string, ReactFiber>)[instanceKey] ?? null\n );\n }\n\n const possibleFiberKey = keys.find((key) => {\n if (!key.startsWith(\"__react\")) {\n return false;\n }\n // Legacy cast - may or may not be necessary\n // eslint-disable-next-line replo/no-as-cast\n const value = (element as unknown as Record<string, unknown>)[key];\n return value && typeof value === \"object\" && \"_debugSource\" in value;\n });\n\n if (possibleFiberKey) {\n return (\n // Legacy cast - may or may not be necessary\n // eslint-disable-next-line replo/no-as-cast\n (element as unknown as Record<string, ReactFiber>)[possibleFiberKey] ??\n null\n );\n }\n\n return null;\n}\n\nexport function getComponentName(\n fiber: ReactFiber | null | undefined,\n): string | undefined {\n if (!fiber?.type) {\n return undefined;\n }\n\n if (typeof fiber.type === \"string\") {\n return undefined;\n }\n\n if (typeof fiber.type === \"function\") {\n return fiber.type.name || undefined;\n }\n\n if (typeof fiber.type === \"object\") {\n return fiber.type.displayName ?? fiber.type.name ?? undefined;\n }\n\n return undefined;\n}\n\nexport function findDebugSource(\n fiber: ReactFiber,\n maxDepth = 50,\n): { source: ReactFiber[\"_debugSource\"]; componentName?: string } | null {\n let current: ReactFiber | null | undefined = fiber;\n let depth = 0;\n\n while (current && depth < maxDepth) {\n if (current._debugSource) {\n return {\n source: current._debugSource,\n componentName: getComponentName(current),\n };\n }\n\n if (current._debugOwner?._debugSource) {\n return {\n source: current._debugOwner._debugSource,\n componentName: getComponentName(current._debugOwner),\n };\n }\n\n current = current.return;\n depth++;\n }\n\n return null;\n}\n\n// React 19 removed `fiber._debugSource`. The only per-element source pointer\n// left is `fiber._debugStack`, an `Error` constructed inside `jsxDEV` at the\n// JSX call site. We pull the topmost non-React frame out of that stack; it\n// references a compiled SSR/client chunk (e.g. `.next-dev/.../ssr/*.js`).\n// `symbolicateSelectedElement` later resolves those compiled frames to\n// original `.tsx` locations via Next.js's `/__nextjs_original-stack-frames`\n// dev-server endpoint.\nexport function findDebugSourceReact19(\n fiber: ReactFiber,\n): { source: ReactFiber[\"_debugSource\"]; componentName?: string } | null {\n let current: ReactFiber | null | undefined = fiber;\n let depth = 0;\n const maxDepth = 50;\n\n while (current && depth < maxDepth) {\n const debugStackSource =\n parseDebugStackSource(current._debugStack) ??\n parseDebugStackSource(current._debugOwner?._debugStack);\n if (debugStackSource) {\n return {\n source: debugStackSource,\n componentName: getComponentName(current),\n };\n }\n\n current = current.return;\n depth++;\n }\n\n return null;\n}\n\nexport function getSourceLocation(element: HTMLElement) {\n let currentElement: HTMLElement | null = element;\n\n while (currentElement) {\n const fiber = getFiberFromElement(currentElement);\n if (fiber) {\n const debugInfo = findDebugSource(fiber) ?? findDebugSourceReact19(fiber);\n if (debugInfo?.source) {\n return {\n filePath: debugInfo.source.fileName,\n lineNumber: debugInfo.source.lineNumber,\n columnNumber: debugInfo.source.columnNumber,\n componentName: debugInfo.componentName ?? undefined,\n };\n }\n }\n\n currentElement = currentElement.parentElement;\n }\n\n return undefined;\n}\n\nfunction getDebugSource(\n fiber: ReactFiber,\n): ReactFiber[\"_debugSource\"] | undefined {\n const debugInfo = findDebugSource(fiber) ?? findDebugSourceReact19(fiber);\n return debugInfo?.source;\n}\n\nfunction getTagName(fiber: ReactFiber): string | undefined {\n return z.string().safeParse(fiber.type).data?.toLowerCase();\n}\n\nexport function getHostSourceIdentity(element: HTMLElement) {\n const fiber = getFiberFromElement(element);\n if (!fiber) {\n return null;\n }\n const source = getDebugSource(fiber);\n if (!source) {\n return null;\n }\n return {\n filePath: source.fileName,\n lineNumber: source.lineNumber,\n columnNumber: source.columnNumber,\n tagName: element.tagName.toLowerCase(),\n };\n}\n\nfunction getSerializablePropValue(value: unknown): unknown {\n if (\n value === null ||\n typeof value === \"number\" ||\n typeof value === \"boolean\"\n ) {\n return value;\n }\n\n if (typeof value === \"string\") {\n return value.length > MAX_SERIALIZED_STRING_LENGTH\n ? `${value.slice(0, MAX_SERIALIZED_STRING_LENGTH)}...`\n : value;\n }\n\n if (Array.isArray(value)) {\n return value.slice(0, 20).map(getSerializablePropValue);\n }\n\n if (typeof value === \"object\") {\n return \"[object]\";\n }\n\n return undefined;\n}\n\nfunction getSerializableProps(\n props: Record<string, unknown> | undefined,\n): Record<string, unknown> | undefined {\n if (!props) {\n return undefined;\n }\n\n const serializedProps: Record<string, unknown> = {};\n for (const [key, value] of Object.entries(props).slice(\n 0,\n MAX_SERIALIZED_PROP_COUNT,\n )) {\n if (\n key === \"children\" ||\n key === \"ref\" ||\n key.startsWith(\"on\") ||\n typeof value === \"function\" ||\n typeof value === \"symbol\"\n ) {\n continue;\n }\n\n const serializableValue = getSerializablePropValue(value);\n if (serializableValue !== undefined) {\n serializedProps[key] = serializableValue;\n }\n }\n\n return Object.keys(serializedProps).length > 0 ? serializedProps : undefined;\n}\n\nexport function getFiberChain(element: HTMLElement) {\n const rootFiber = getFiberFromElement(element);\n const fiberChain = [];\n const seenKeys = new Set<string>();\n let currentFiber: ReactFiber | null | undefined = rootFiber;\n let depth = 0;\n\n while (currentFiber && depth < MAX_FIBER_CHAIN_DEPTH) {\n const source = getDebugSource(currentFiber);\n if (source) {\n const tagName = getTagName(currentFiber);\n const componentName = getComponentName(currentFiber);\n const key = [\n source.fileName,\n source.lineNumber,\n source.columnNumber ?? \"\",\n tagName ?? \"\",\n componentName ?? \"\",\n ].join(\":\");\n\n if (!seenKeys.has(key)) {\n seenKeys.add(key);\n fiberChain.push({\n sourceLocation: {\n filePath: source.fileName,\n lineNumber: source.lineNumber,\n columnNumber: source.columnNumber,\n componentName,\n },\n tagName,\n componentName,\n props: getSerializableProps(\n currentFiber.memoizedProps ?? currentFiber.pendingProps,\n ),\n });\n }\n }\n\n currentFiber = currentFiber.return;\n depth += 1;\n }\n\n return fiberChain;\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["// =============================================================================\n// React Fiber / Source Location Utilities\n// =============================================================================\n\nimport { z } from \"zod\";\n\nexport interface DebugSource {\n fileName: string;\n lineNumber: number;\n columnNumber?: number;\n}\n\nexport interface ReactFiber {\n _debugStack?: unknown;\n _debugOwner?: ReactFiber;\n type?:\n | { name?: string; displayName?: string }\n | string\n | ((...args: unknown[]) => unknown)\n | null;\n return?: ReactFiber | null;\n child?: ReactFiber | null;\n sibling?: ReactFiber | null;\n alternate?: ReactFiber | null;\n stateNode?: unknown;\n memoizedProps?: Record<string, unknown>;\n pendingProps?: Record<string, unknown>;\n}\n\nconst MAX_FIBER_CHAIN_DEPTH = 80;\nconst MAX_SERIALIZED_PROP_COUNT = 24;\nconst MAX_SERIALIZED_STRING_LENGTH = 500;\nconst MAX_SIBLING_SCAN = 5000;\nconst MAX_SUBTREE_SCAN = 5000;\nconst MAX_ROOT_CLIMB_DEPTH = 1000;\nconst MAX_CURRENT_TREE_SCAN = 50_000;\n\n// Frames produced by React's own JSX runtime \u2014 these come first in\n// `_debugStack.stack` because the `Error` is constructed inside `jsxDEV`. We\n// want the frame *above* them, which is the actual JSX call site.\n//\n// Turbopack rewrites `node_modules/next/dist/compiled/react-server-dom-*` and\n// friends into mangled chunk paths like\n// `_next/static/chunks/<hash>_next_dist_compiled_react-server-dom-turbopack_<hash>._.js`,\n// so we match both the human-readable module paths and the chunk-name forms.\n// Without this, `_debugStack`'s top frame inside React's RSC runtime gets\n// surfaced as the \"host\" source location for every text node on the page \u2014\n// which then symbolicates back to bundled React, not user code.\nconst REACT_INTERNAL_FRAME_PATTERNS = [\n /react.*jsx-dev-runtime/,\n /react.*jsx-runtime/,\n /react-dom\\//,\n /react\\/cjs\\/react\\./,\n /react-server-dom/,\n /react_server_dom/,\n /next[/_.]dist[/_.]compiled/,\n /\\/node_modules\\/(next|react|react-dom)\\//,\n];\n\nfunction isReactInternalFrame(fileName: string): boolean {\n return REACT_INTERNAL_FRAME_PATTERNS.some((pattern) => {\n return pattern.test(fileName);\n });\n}\n\n// React constructs the `_debugStack` Error inside its JSX runtime (`jsxDEV`,\n// `jsx`, `jsxs`), so those frames sit *above* the real JSX call site we want.\n// The file-based check above skips them when their path is recognizable, but\n// Turbopack mangles the client JSX-runtime chunk to an opaque name like\n// `node_modules__pnpm_<hash>._.js` (no \"react\"/\"jsx-dev-runtime\" substring),\n// so the top `jsxDEV` frame slips through and gets surfaced as the element's\n// source \u2014 which then symbolicates back to bundled React, not user code.\n// Matching by function name is stable across Turbopack's chunk hashing.\n// NOTE (Noah, 2026-07-27): Only bites client components; server-component\n// frames carry a recognizable `react-server-dom-*` path.\nconst REACT_INTERNAL_METHOD_PATTERNS = [\n /(?:^|\\.)jsxDEV(?:Impl)?$/,\n /(?:^|\\.)jsxs?$/,\n /(?:^|\\.)fakeJSXCallSite$/,\n];\n\n// React invokes user components inside a sentinel-named function\n// (`react_stack_bottom_frame`, hyphenated in some builds) precisely so tools\n// can tell user frames from React internals: every frame at or below the\n// sentinel is React/scheduler/bootstrap code. Cutting there is robust even\n// when Turbopack mangles those frames' chunk paths beyond what\n// REACT_INTERNAL_FRAME_PATTERNS recognizes. (Sentinel treatment adapted from\n// bippy, MIT, https://github.com/aidenybai/bippy.)\nconst REACT_STACK_BOTTOM_FRAME_PATTERN = /react[-_]stack[-_]bottom[-_]frame/;\n\nfunction isReactInternalMethod(methodName: string | null): boolean {\n if (!methodName) {\n return false;\n }\n return REACT_INTERNAL_METHOD_PATTERNS.some((pattern) => {\n return pattern.test(methodName);\n });\n}\n\nfunction parseDebugStackSource(debugStack: unknown): DebugSource | null {\n let stackText: string | null = null;\n\n if (typeof debugStack === \"string\") {\n stackText = debugStack;\n } else if (\n typeof debugStack === \"object\" &&\n debugStack &&\n \"stack\" in debugStack\n ) {\n // Legacy cast - may or may not be necessary\n // eslint-disable-next-line replo/no-as-cast\n const stack = (debugStack as { stack?: unknown }).stack;\n if (typeof stack === \"string\") {\n stackText = stack;\n }\n }\n\n if (!stackText) {\n return null;\n }\n\n for (const rawLine of stackText.split(\"\\n\")) {\n const line = rawLine.trim();\n if (!line) {\n continue;\n }\n\n if (REACT_STACK_BOTTOM_FRAME_PATTERN.test(line)) {\n // Everything from the sentinel down is React internals \u2014 a frame down\n // there can only be a false positive.\n return null;\n }\n\n // Prefer the framed form `at <method> (<file>:<line>:<col>)` (V8) or\n // `<method>@<file>:<line>:<col>` (JavaScriptCore/SpiderMonkey \u2014 Safari\n // and Firefox editors drive the same in-iframe code) so we can read the\n // function name and skip React's JSX-runtime frames even when Turbopack\n // has mangled their chunk file name beyond recognition.\n const framedMatch =\n line.match(/^at\\s+(.+?)\\s+\\((.*):(\\d+):(\\d+)\\)$/) ??\n // Method names never contain `/`, which keeps `@`-versioned path\n // segments (`.pnpm/react@19.2.4/\u2026`) from being read as a method.\n line.match(/^([^@/]*)@(.+):(\\d+):(\\d+)$/);\n const stackFrameMatch =\n framedMatch ??\n line.match(/^at\\s+(?:.*\\()?(.*):(\\d+):(\\d+)\\)?$/) ??\n line.match(/^(.*):(\\d+):(\\d+)$/);\n\n if (!stackFrameMatch) {\n continue;\n }\n\n const methodName = framedMatch ? framedMatch[1]!.trim() : null;\n const fileOffset = framedMatch ? 2 : 1;\n const fileName = stackFrameMatch[fileOffset]?.trim();\n const lineNumber = Number.parseInt(\n stackFrameMatch[fileOffset + 1] ?? \"\",\n 10,\n );\n const columnNumber = Number.parseInt(\n stackFrameMatch[fileOffset + 2] ?? \"\",\n 10,\n );\n\n if (\n !fileName ||\n !Number.isFinite(lineNumber) ||\n !Number.isFinite(columnNumber)\n ) {\n continue;\n }\n\n if (isReactInternalFrame(fileName) || isReactInternalMethod(methodName)) {\n continue;\n }\n\n return {\n fileName,\n lineNumber,\n columnNumber,\n };\n }\n\n return null;\n}\n\nexport function getFiberFromElement(element: HTMLElement): ReactFiber | null {\n if (!element || typeof element !== \"object\") {\n return null;\n }\n\n const symbolKeys = Object.getOwnPropertySymbols(element);\n const fiberSymbolKey = symbolKeys.find((symbolKey) => {\n return symbolKey.toString().includes(\"reactFiber\");\n });\n if (fiberSymbolKey) {\n return (\n // Legacy cast - may or may not be necessary\n // eslint-disable-next-line replo/no-as-cast\n (element as unknown as Record<symbol, ReactFiber>)[fiberSymbolKey] ?? null\n );\n }\n\n const keys = Object.keys(element);\n const fiberKey = keys.find((key) => key.startsWith(\"__reactFiber$\"));\n if (fiberKey) {\n // Legacy cast - may or may not be necessary\n // eslint-disable-next-line replo/no-as-cast\n return (element as unknown as Record<string, ReactFiber>)[fiberKey] ?? null;\n }\n return null;\n}\n\nexport function getComponentName(\n fiber: ReactFiber | null | undefined,\n): string | undefined {\n if (!fiber?.type) {\n return undefined;\n }\n\n if (typeof fiber.type === \"string\") {\n return undefined;\n }\n\n if (typeof fiber.type === \"function\") {\n return fiber.type.name || undefined;\n }\n\n if (typeof fiber.type === \"object\") {\n return fiber.type.displayName ?? fiber.type.name ?? undefined;\n }\n\n return undefined;\n}\n\n// The per-element source pointer is `fiber._debugStack`, an `Error`\n// constructed inside `jsxDEV` at the JSX call site (dev builds only). We pull\n// the topmost non-React frame out of that stack; it references a compiled\n// SSR/client chunk (e.g. `.next-dev/.../ssr/*.js`).\n// `symbolicateSelectedElement` later resolves those compiled frames to\n// original `.tsx` locations via Next.js's `/__nextjs_original-stack-frames`\n// dev-server endpoint.\nexport function findDebugSource(\n fiber: ReactFiber,\n): { source: DebugSource; componentName?: string } | null {\n let current: ReactFiber | null | undefined = fiber;\n let depth = 0;\n const maxDepth = 50;\n\n while (current && depth < maxDepth) {\n const debugStackSource =\n parseDebugStackSource(current._debugStack) ??\n parseDebugStackSource(current._debugOwner?._debugStack);\n if (debugStackSource) {\n return {\n source: debugStackSource,\n componentName: getComponentName(current),\n };\n }\n\n current = current.return;\n depth++;\n }\n\n return null;\n}\n\nexport function getSourceLocation(element: HTMLElement) {\n let currentElement: HTMLElement | null = element;\n\n while (currentElement) {\n const fiber = getFiberFromElement(currentElement);\n if (fiber) {\n const debugInfo = findDebugSource(fiber);\n if (debugInfo?.source) {\n return {\n filePath: debugInfo.source.fileName,\n lineNumber: debugInfo.source.lineNumber,\n columnNumber: debugInfo.source.columnNumber,\n componentName: debugInfo.componentName ?? undefined,\n };\n }\n }\n\n currentElement = currentElement.parentElement;\n }\n\n return undefined;\n}\n\nfunction getDebugSource(fiber: ReactFiber): DebugSource | undefined {\n return findDebugSource(fiber)?.source;\n}\n\nfunction getTagName(fiber: ReactFiber): string | undefined {\n return z.string().safeParse(fiber.type).data?.toLowerCase();\n}\n\n// Resolving a fiber's source requires parsing `_debugStack` in React 19, which\n// is expensive. Cache per fiber object. React reuses fiber objects across\n// updates (current/alternate double-buffering), but a fiber's JSX source\n// location is fixed for its lifetime, so the cached key stays valid. Entries\n// are dropped only when the fiber is GC'd (unmount / remount / navigation).\n// Known caveat: a fast-refresh edit that shifts line numbers can leave\n// long-lived fibers keyed against the old revision while newly mounted\n// siblings key against the new one, temporarily splitting a repetition\n// group. Dev-only, and it self-heals on remount.\nconst fiberSourceKeyCache = new WeakMap<ReactFiber, string | null>();\n\nfunction getFiberSourceKey(fiber: ReactFiber): string | null {\n const cached = fiberSourceKeyCache.get(fiber);\n if (cached !== undefined) {\n return cached;\n }\n const source = getDebugSource(fiber);\n const key = source\n ? `${source.fileName}:${source.lineNumber}:${source.columnNumber ?? \"\"}`\n : null;\n fiberSourceKeyCache.set(fiber, key);\n return key;\n}\n\n// A repetition group is >=2 sibling fibers under one parent that share the\n// clicked fiber's element `type` and JSX source location \u2014 i.e. the instances\n// produced by a single `.map()`/loop. Returns null when the fiber is not part\n// of such a group (source parsing is bounded to the direct siblings scanned).\nfunction findRepetitionGroup(fiber: ReactFiber): ReactFiber[] | null {\n const parent = fiber.return;\n if (!parent) {\n return null;\n }\n const type = fiber.type;\n const candidates: ReactFiber[] = [];\n let child: ReactFiber | null | undefined = parent.child;\n let scanned = 0;\n while (child && scanned < MAX_SIBLING_SCAN) {\n if (child.type === type) {\n candidates.push(child);\n }\n child = child.sibling;\n scanned += 1;\n }\n\n if (candidates.length < 2) {\n return null;\n }\n\n const sourceKey = getFiberSourceKey(fiber);\n if (!sourceKey) {\n return null;\n }\n const members = candidates.filter((candidate) => {\n return getFiberSourceKey(candidate) === sourceKey;\n });\n return members.length >= 2 ? members : null;\n}\n\nfunction descendChildPath(\n fiber: ReactFiber,\n path: number[],\n): ReactFiber | null {\n let current: ReactFiber | null | undefined = fiber;\n for (const childIndex of path) {\n if (!current) {\n return null;\n }\n let child: ReactFiber | null | undefined = current.child;\n let index = 0;\n while (child && index < childIndex) {\n child = child.sibling;\n index += 1;\n }\n if (!child) {\n return null;\n }\n current = child;\n }\n return current ?? null;\n}\n\nfunction getHostNode(fiber: ReactFiber | null): HTMLElement | null {\n return fiber?.stateNode instanceof HTMLElement ? fiber.stateNode : null;\n}\n\nfunction isMatchingHost(\n fiber: ReactFiber | null,\n type: ReactFiber[\"type\"],\n sourceKey: string | null,\n): boolean {\n if (!fiber || !(fiber.stateNode instanceof HTMLElement)) {\n return false;\n }\n return fiber.type === type && getFiberSourceKey(fiber) === sourceKey;\n}\n\n// Fallback for when the child-index path doesn't land on a matching host (e.g.\n// a repeated unit whose subtree structure differs slightly): DFS the unit's\n// subtree for the first host fiber matching the clicked node's identity.\nfunction findHostInSubtree(\n root: ReactFiber,\n type: ReactFiber[\"type\"],\n sourceKey: string | null,\n): HTMLElement | null {\n const stack: ReactFiber[] = [];\n let child: ReactFiber | null | undefined = root.child;\n while (child) {\n stack.push(child);\n child = child.sibling;\n }\n let scanned = 0;\n while (stack.length > 0 && scanned < MAX_SUBTREE_SCAN) {\n const current = stack.pop();\n scanned += 1;\n if (!current) {\n continue;\n }\n if (isMatchingHost(current, type, sourceKey)) {\n return getHostNode(current);\n }\n let next: ReactFiber | null | undefined = current.child;\n while (next) {\n stack.push(next);\n next = next.sibling;\n }\n }\n return null;\n}\n\n// Fallback current-generation heuristic: a fiber still linked into its\n// parent's child list is *probably* the live generation. Only locally\n// consistent \u2014 a stale parent's child list can still link the stale fiber \u2014\n// so this runs only when the authoritative root check below is unavailable.\nfunction resolveCurrentFiberByParentLinkage(fiber: ReactFiber): ReactFiber {\n const alternate = fiber.alternate;\n if (!alternate) {\n return fiber;\n }\n let child: ReactFiber | null | undefined = fiber.return?.child;\n let scanned = 0;\n while (child && scanned < MAX_SIBLING_SCAN) {\n if (child === fiber) {\n return fiber;\n }\n child = child.sibling;\n scanned += 1;\n }\n return alternate;\n}\n\nfunction getFiberRootCurrent(stateNode: unknown): ReactFiber | null {\n if (\n !stateNode ||\n typeof stateNode !== \"object\" ||\n !(\"current\" in stateNode)\n ) {\n return null;\n }\n const current: unknown = Reflect.get(stateNode, \"current\");\n if (current && typeof current === \"object\" && \"child\" in current) {\n // Legacy cast - structural check above is our fiber discriminator\n // eslint-disable-next-line replo/no-as-cast\n return current as ReactFiber;\n }\n return null;\n}\n\n// React sets a DOM node's fiber pointer once, at node creation, and never\n// re-points it. Under double-buffering the current tree alternates between\n// the two fiber generations on every commit, so after a structure-changing\n// re-render the cached fiber can be the non-current generation \u2014 its\n// parent/sibling linkage then describes a stale tree. Which generation is\n// current can only be answered authoritatively from the root: the HostRoot\n// fiber's `stateNode` is the FiberRoot, whose `current` pointer always\n// references the committed tree (both HostRoot generations share the same\n// FiberRoot object, so it doesn't matter which generation we climbed).\n// Whichever of {fiber, alternate} is reachable from `FiberRoot.current` is\n// the live one. Approach adapted from bippy's `getLatestFiber` (MIT,\n// https://github.com/aidenybai/bippy), with our bounded iterative traversal.\nfunction resolveCurrentFiber(fiber: ReactFiber): ReactFiber {\n const alternate = fiber.alternate;\n if (!alternate) {\n return fiber;\n }\n\n let rootFiber: ReactFiber = fiber;\n let climbed = 0;\n while (rootFiber.return && climbed < MAX_ROOT_CLIMB_DEPTH) {\n rootFiber = rootFiber.return;\n climbed += 1;\n }\n const currentRoot = rootFiber.return\n ? null\n : getFiberRootCurrent(rootFiber.stateNode);\n if (!currentRoot) {\n return resolveCurrentFiberByParentLinkage(fiber);\n }\n\n const stack: ReactFiber[] = [currentRoot];\n let scanned = 0;\n while (stack.length > 0 && scanned < MAX_CURRENT_TREE_SCAN) {\n const node = stack.pop();\n scanned += 1;\n if (!node) {\n continue;\n }\n if (node === fiber || node === alternate) {\n return node;\n }\n if (node.child) {\n stack.push(node.child);\n }\n if (node.sibling) {\n stack.push(node.sibling);\n }\n }\n if (scanned >= MAX_CURRENT_TREE_SCAN) {\n return resolveCurrentFiberByParentLinkage(fiber);\n }\n // Neither generation is reachable from the committed tree: the fiber is\n // detached (unmounted mid-click). Degrade to the cached fiber; downstream\n // returns just the clicked element.\n return fiber;\n}\n\n// Given a clicked element, return every host DOM node in its repetition group,\n// including the element itself. Traverses the fiber tree instead of walking the\n// whole document: climb to the lowest ancestor that forms a repetition group,\n// record the child-index path down to the clicked node, then descend that same\n// path inside each group member. Returns just `[element]` when the element is\n// not repeated.\nexport function getRepetitionHostNodes(element: HTMLElement): HTMLElement[] {\n const cachedFiber = getFiberFromElement(element);\n if (!cachedFiber) {\n return [element];\n }\n const selectedFiber = resolveCurrentFiber(cachedFiber);\n\n const climb: ReactFiber[] = [];\n let cursor: ReactFiber | null | undefined = selectedFiber;\n let depth = 0;\n let unit: ReactFiber | null = null;\n let group: ReactFiber[] | null = null;\n while (cursor && depth < MAX_FIBER_CHAIN_DEPTH) {\n climb.push(cursor);\n const candidate = findRepetitionGroup(cursor);\n if (candidate) {\n unit = cursor;\n group = candidate;\n break;\n }\n cursor = cursor.return;\n depth += 1;\n }\n\n if (!unit || !group) {\n return [element];\n }\n\n const downward = climb.slice(0, climb.indexOf(unit) + 1).reverse();\n // null when a climb step is not linked into its parent's child list (stale\n // generation slipped through) \u2014 every member then resolves via subtree\n // search instead of descending a path recorded against the wrong tree.\n let pathFromUnit: number[] | null = [];\n for (let i = 0; i < downward.length - 1 && pathFromUnit; i += 1) {\n const parentFiber = downward[i];\n const targetFiber = downward[i + 1];\n if (!parentFiber || !targetFiber) {\n break;\n }\n let child: ReactFiber | null | undefined = parentFiber.child;\n let index = 0;\n while (child && child !== targetFiber && child !== targetFiber.alternate) {\n child = child.sibling;\n index += 1;\n }\n if (child) {\n pathFromUnit.push(index);\n } else {\n pathFromUnit = null;\n }\n }\n\n const selectedType = selectedFiber.type;\n const selectedSourceKey = getFiberSourceKey(selectedFiber);\n const nodes: HTMLElement[] = [];\n const seen = new Set<HTMLElement>();\n for (const member of group) {\n const landed = pathFromUnit ? descendChildPath(member, pathFromUnit) : null;\n const node = isMatchingHost(landed, selectedType, selectedSourceKey)\n ? getHostNode(landed)\n : findHostInSubtree(member, selectedType, selectedSourceKey);\n if (node && !seen.has(node)) {\n seen.add(node);\n nodes.push(node);\n }\n }\n\n if (!seen.has(element)) {\n nodes.push(element);\n }\n\n return nodes;\n}\n\nfunction getSerializablePropValue(value: unknown): unknown {\n if (\n value === null ||\n typeof value === \"number\" ||\n typeof value === \"boolean\"\n ) {\n return value;\n }\n\n if (typeof value === \"string\") {\n return value.length > MAX_SERIALIZED_STRING_LENGTH\n ? `${value.slice(0, MAX_SERIALIZED_STRING_LENGTH)}...`\n : value;\n }\n\n if (Array.isArray(value)) {\n return value.slice(0, 20).map(getSerializablePropValue);\n }\n\n if (typeof value === \"object\") {\n return \"[object]\";\n }\n\n return undefined;\n}\n\nfunction getSerializableProps(\n props: Record<string, unknown> | undefined,\n): Record<string, unknown> | undefined {\n if (!props) {\n return undefined;\n }\n\n const serializedProps: Record<string, unknown> = {};\n for (const [key, value] of Object.entries(props).slice(\n 0,\n MAX_SERIALIZED_PROP_COUNT,\n )) {\n if (\n key === \"children\" ||\n key === \"ref\" ||\n key.startsWith(\"on\") ||\n typeof value === \"function\" ||\n typeof value === \"symbol\"\n ) {\n continue;\n }\n\n const serializableValue = getSerializablePropValue(value);\n if (serializableValue !== undefined) {\n serializedProps[key] = serializableValue;\n }\n }\n\n return Object.keys(serializedProps).length > 0 ? serializedProps : undefined;\n}\n\nexport function getFiberChain(element: HTMLElement) {\n const rootFiber = getFiberFromElement(element);\n const fiberChain = [];\n const seenKeys = new Set<string>();\n let currentFiber: ReactFiber | null | undefined = rootFiber;\n let depth = 0;\n\n while (currentFiber && depth < MAX_FIBER_CHAIN_DEPTH) {\n const source = getDebugSource(currentFiber);\n if (source) {\n const tagName = getTagName(currentFiber);\n const componentName = getComponentName(currentFiber);\n const key = [\n source.fileName,\n source.lineNumber,\n source.columnNumber ?? \"\",\n tagName ?? \"\",\n componentName ?? \"\",\n ].join(\":\");\n\n if (!seenKeys.has(key)) {\n seenKeys.add(key);\n fiberChain.push({\n sourceLocation: {\n filePath: source.fileName,\n lineNumber: source.lineNumber,\n columnNumber: source.columnNumber,\n componentName,\n },\n tagName,\n componentName,\n props: getSerializableProps(\n currentFiber.memoizedProps ?? currentFiber.pendingProps,\n ),\n });\n }\n }\n\n currentFiber = currentFiber.return;\n depth += 1;\n }\n\n return fiberChain;\n}\n"],
5
+ "mappings": "AAIA,SAAS,SAAS;AAyBlB,MAAM,wBAAwB;AAC9B,MAAM,4BAA4B;AAClC,MAAM,+BAA+B;AACrC,MAAM,mBAAmB;AACzB,MAAM,mBAAmB;AACzB,MAAM,uBAAuB;AAC7B,MAAM,wBAAwB;AAa9B,MAAM,gCAAgC;AAAA,EACpC;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF;AAEA,SAAS,qBAAqB,UAA2B;AACvD,SAAO,8BAA8B,KAAK,CAAC,YAAY;AACrD,WAAO,QAAQ,KAAK,QAAQ;AAAA,EAC9B,CAAC;AACH;AAYA,MAAM,iCAAiC;AAAA,EACrC;AAAA,EACA;AAAA,EACA;AACF;AASA,MAAM,mCAAmC;AAEzC,SAAS,sBAAsB,YAAoC;AACjE,MAAI,CAAC,YAAY;AACf,WAAO;AAAA,EACT;AACA,SAAO,+BAA+B,KAAK,CAAC,YAAY;AACtD,WAAO,QAAQ,KAAK,UAAU;AAAA,EAChC,CAAC;AACH;AAEA,SAAS,sBAAsB,YAAyC;AACtE,MAAI,YAA2B;AAE/B,MAAI,OAAO,eAAe,UAAU;AAClC,gBAAY;AAAA,EACd,WACE,OAAO,eAAe,YACtB,cACA,WAAW,YACX;AAGA,UAAM,QAAS,WAAmC;AAClD,QAAI,OAAO,UAAU,UAAU;AAC7B,kBAAY;AAAA,IACd;AAAA,EACF;AAEA,MAAI,CAAC,WAAW;AACd,WAAO;AAAA,EACT;AAEA,aAAW,WAAW,UAAU,MAAM,IAAI,GAAG;AAC3C,UAAM,OAAO,QAAQ,KAAK;AAC1B,QAAI,CAAC,MAAM;AACT;AAAA,IACF;AAEA,QAAI,iCAAiC,KAAK,IAAI,GAAG;AAG/C,aAAO;AAAA,IACT;AAOA,UAAM,cACJ,KAAK,MAAM,qCAAqC;AAAA;AAAA,IAGhD,KAAK,MAAM,6BAA6B;AAC1C,UAAM,kBACJ,eACA,KAAK,MAAM,qCAAqC,KAChD,KAAK,MAAM,oBAAoB;AAEjC,QAAI,CAAC,iBAAiB;AACpB;AAAA,IACF;AAEA,UAAM,aAAa,cAAc,YAAY,CAAC,EAAG,KAAK,IAAI;AAC1D,UAAM,aAAa,cAAc,IAAI;AACrC,UAAM,WAAW,gBAAgB,UAAU,GAAG,KAAK;AACnD,UAAM,aAAa,OAAO;AAAA,MACxB,gBAAgB,aAAa,CAAC,KAAK;AAAA,MACnC;AAAA,IACF;AACA,UAAM,eAAe,OAAO;AAAA,MAC1B,gBAAgB,aAAa,CAAC,KAAK;AAAA,MACnC;AAAA,IACF;AAEA,QACE,CAAC,YACD,CAAC,OAAO,SAAS,UAAU,KAC3B,CAAC,OAAO,SAAS,YAAY,GAC7B;AACA;AAAA,IACF;AAEA,QAAI,qBAAqB,QAAQ,KAAK,sBAAsB,UAAU,GAAG;AACvE;AAAA,IACF;AAEA,WAAO;AAAA,MACL;AAAA,MACA;AAAA,MACA;AAAA,IACF;AAAA,EACF;AAEA,SAAO;AACT;AAEO,SAAS,oBAAoB,SAAyC;AAC3E,MAAI,CAAC,WAAW,OAAO,YAAY,UAAU;AAC3C,WAAO;AAAA,EACT;AAEA,QAAM,aAAa,OAAO,sBAAsB,OAAO;AACvD,QAAM,iBAAiB,WAAW,KAAK,CAAC,cAAc;AACpD,WAAO,UAAU,SAAS,EAAE,SAAS,YAAY;AAAA,EACnD,CAAC;AACD,MAAI,gBAAgB;AAClB;AAAA;AAAA;AAAA,MAGG,QAAkD,cAAc,KAAK;AAAA;AAAA,EAE1E;AAEA,QAAM,OAAO,OAAO,KAAK,OAAO;AAChC,QAAM,WAAW,KAAK,KAAK,CAAC,QAAQ,IAAI,WAAW,eAAe,CAAC;AACnE,MAAI,UAAU;AAGZ,WAAQ,QAAkD,QAAQ,KAAK;AAAA,EACzE;AACA,SAAO;AACT;AAEO,SAAS,iBACd,OACoB;AACpB,MAAI,CAAC,OAAO,MAAM;AAChB,WAAO;AAAA,EACT;AAEA,MAAI,OAAO,MAAM,SAAS,UAAU;AAClC,WAAO;AAAA,EACT;AAEA,MAAI,OAAO,MAAM,SAAS,YAAY;AACpC,WAAO,MAAM,KAAK,QAAQ;AAAA,EAC5B;AAEA,MAAI,OAAO,MAAM,SAAS,UAAU;AAClC,WAAO,MAAM,KAAK,eAAe,MAAM,KAAK,QAAQ;AAAA,EACtD;AAEA,SAAO;AACT;AASO,SAAS,gBACd,OACwD;AACxD,MAAI,UAAyC;AAC7C,MAAI,QAAQ;AACZ,QAAM,WAAW;AAEjB,SAAO,WAAW,QAAQ,UAAU;AAClC,UAAM,mBACJ,sBAAsB,QAAQ,WAAW,KACzC,sBAAsB,QAAQ,aAAa,WAAW;AACxD,QAAI,kBAAkB;AACpB,aAAO;AAAA,QACL,QAAQ;AAAA,QACR,eAAe,iBAAiB,OAAO;AAAA,MACzC;AAAA,IACF;AAEA,cAAU,QAAQ;AAClB;AAAA,EACF;AAEA,SAAO;AACT;AAEO,SAAS,kBAAkB,SAAsB;AACtD,MAAI,iBAAqC;AAEzC,SAAO,gBAAgB;AACrB,UAAM,QAAQ,oBAAoB,cAAc;AAChD,QAAI,OAAO;AACT,YAAM,YAAY,gBAAgB,KAAK;AACvC,UAAI,WAAW,QAAQ;AACrB,eAAO;AAAA,UACL,UAAU,UAAU,OAAO;AAAA,UAC3B,YAAY,UAAU,OAAO;AAAA,UAC7B,cAAc,UAAU,OAAO;AAAA,UAC/B,eAAe,UAAU,iBAAiB;AAAA,QAC5C;AAAA,MACF;AAAA,IACF;AAEA,qBAAiB,eAAe;AAAA,EAClC;AAEA,SAAO;AACT;AAEA,SAAS,eAAe,OAA4C;AAClE,SAAO,gBAAgB,KAAK,GAAG;AACjC;AAEA,SAAS,WAAW,OAAuC;AACzD,SAAO,EAAE,OAAO,EAAE,UAAU,MAAM,IAAI,EAAE,MAAM,YAAY;AAC5D;AAWA,MAAM,sBAAsB,oBAAI,QAAmC;AAEnE,SAAS,kBAAkB,OAAkC;AAC3D,QAAM,SAAS,oBAAoB,IAAI,KAAK;AAC5C,MAAI,WAAW,QAAW;AACxB,WAAO;AAAA,EACT;AACA,QAAM,SAAS,eAAe,KAAK;AACnC,QAAM,MAAM,SACR,GAAG,OAAO,QAAQ,IAAI,OAAO,UAAU,IAAI,OAAO,gBAAgB,EAAE,KACpE;AACJ,sBAAoB,IAAI,OAAO,GAAG;AAClC,SAAO;AACT;AAMA,SAAS,oBAAoB,OAAwC;AACnE,QAAM,SAAS,MAAM;AACrB,MAAI,CAAC,QAAQ;AACX,WAAO;AAAA,EACT;AACA,QAAM,OAAO,MAAM;AACnB,QAAM,aAA2B,CAAC;AAClC,MAAI,QAAuC,OAAO;AAClD,MAAI,UAAU;AACd,SAAO,SAAS,UAAU,kBAAkB;AAC1C,QAAI,MAAM,SAAS,MAAM;AACvB,iBAAW,KAAK,KAAK;AAAA,IACvB;AACA,YAAQ,MAAM;AACd,eAAW;AAAA,EACb;AAEA,MAAI,WAAW,SAAS,GAAG;AACzB,WAAO;AAAA,EACT;AAEA,QAAM,YAAY,kBAAkB,KAAK;AACzC,MAAI,CAAC,WAAW;AACd,WAAO;AAAA,EACT;AACA,QAAM,UAAU,WAAW,OAAO,CAAC,cAAc;AAC/C,WAAO,kBAAkB,SAAS,MAAM;AAAA,EAC1C,CAAC;AACD,SAAO,QAAQ,UAAU,IAAI,UAAU;AACzC;AAEA,SAAS,iBACP,OACA,MACmB;AACnB,MAAI,UAAyC;AAC7C,aAAW,cAAc,MAAM;AAC7B,QAAI,CAAC,SAAS;AACZ,aAAO;AAAA,IACT;AACA,QAAI,QAAuC,QAAQ;AACnD,QAAI,QAAQ;AACZ,WAAO,SAAS,QAAQ,YAAY;AAClC,cAAQ,MAAM;AACd,eAAS;AAAA,IACX;AACA,QAAI,CAAC,OAAO;AACV,aAAO;AAAA,IACT;AACA,cAAU;AAAA,EACZ;AACA,SAAO,WAAW;AACpB;AAEA,SAAS,YAAY,OAA8C;AACjE,SAAO,OAAO,qBAAqB,cAAc,MAAM,YAAY;AACrE;AAEA,SAAS,eACP,OACA,MACA,WACS;AACT,MAAI,CAAC,SAAS,EAAE,MAAM,qBAAqB,cAAc;AACvD,WAAO;AAAA,EACT;AACA,SAAO,MAAM,SAAS,QAAQ,kBAAkB,KAAK,MAAM;AAC7D;AAKA,SAAS,kBACP,MACA,MACA,WACoB;AACpB,QAAM,QAAsB,CAAC;AAC7B,MAAI,QAAuC,KAAK;AAChD,SAAO,OAAO;AACZ,UAAM,KAAK,KAAK;AAChB,YAAQ,MAAM;AAAA,EAChB;AACA,MAAI,UAAU;AACd,SAAO,MAAM,SAAS,KAAK,UAAU,kBAAkB;AACrD,UAAM,UAAU,MAAM,IAAI;AAC1B,eAAW;AACX,QAAI,CAAC,SAAS;AACZ;AAAA,IACF;AACA,QAAI,eAAe,SAAS,MAAM,SAAS,GAAG;AAC5C,aAAO,YAAY,OAAO;AAAA,IAC5B;AACA,QAAI,OAAsC,QAAQ;AAClD,WAAO,MAAM;AACX,YAAM,KAAK,IAAI;AACf,aAAO,KAAK;AAAA,IACd;AAAA,EACF;AACA,SAAO;AACT;AAMA,SAAS,mCAAmC,OAA+B;AACzE,QAAM,YAAY,MAAM;AACxB,MAAI,CAAC,WAAW;AACd,WAAO;AAAA,EACT;AACA,MAAI,QAAuC,MAAM,QAAQ;AACzD,MAAI,UAAU;AACd,SAAO,SAAS,UAAU,kBAAkB;AAC1C,QAAI,UAAU,OAAO;AACnB,aAAO;AAAA,IACT;AACA,YAAQ,MAAM;AACd,eAAW;AAAA,EACb;AACA,SAAO;AACT;AAEA,SAAS,oBAAoB,WAAuC;AAClE,MACE,CAAC,aACD,OAAO,cAAc,YACrB,EAAE,aAAa,YACf;AACA,WAAO;AAAA,EACT;AACA,QAAM,UAAmB,QAAQ,IAAI,WAAW,SAAS;AACzD,MAAI,WAAW,OAAO,YAAY,YAAY,WAAW,SAAS;AAGhE,WAAO;AAAA,EACT;AACA,SAAO;AACT;AAcA,SAAS,oBAAoB,OAA+B;AAC1D,QAAM,YAAY,MAAM;AACxB,MAAI,CAAC,WAAW;AACd,WAAO;AAAA,EACT;AAEA,MAAI,YAAwB;AAC5B,MAAI,UAAU;AACd,SAAO,UAAU,UAAU,UAAU,sBAAsB;AACzD,gBAAY,UAAU;AACtB,eAAW;AAAA,EACb;AACA,QAAM,cAAc,UAAU,SAC1B,OACA,oBAAoB,UAAU,SAAS;AAC3C,MAAI,CAAC,aAAa;AAChB,WAAO,mCAAmC,KAAK;AAAA,EACjD;AAEA,QAAM,QAAsB,CAAC,WAAW;AACxC,MAAI,UAAU;AACd,SAAO,MAAM,SAAS,KAAK,UAAU,uBAAuB;AAC1D,UAAM,OAAO,MAAM,IAAI;AACvB,eAAW;AACX,QAAI,CAAC,MAAM;AACT;AAAA,IACF;AACA,QAAI,SAAS,SAAS,SAAS,WAAW;AACxC,aAAO;AAAA,IACT;AACA,QAAI,KAAK,OAAO;AACd,YAAM,KAAK,KAAK,KAAK;AAAA,IACvB;AACA,QAAI,KAAK,SAAS;AAChB,YAAM,KAAK,KAAK,OAAO;AAAA,IACzB;AAAA,EACF;AACA,MAAI,WAAW,uBAAuB;AACpC,WAAO,mCAAmC,KAAK;AAAA,EACjD;AAIA,SAAO;AACT;AAQO,SAAS,uBAAuB,SAAqC;AAC1E,QAAM,cAAc,oBAAoB,OAAO;AAC/C,MAAI,CAAC,aAAa;AAChB,WAAO,CAAC,OAAO;AAAA,EACjB;AACA,QAAM,gBAAgB,oBAAoB,WAAW;AAErD,QAAM,QAAsB,CAAC;AAC7B,MAAI,SAAwC;AAC5C,MAAI,QAAQ;AACZ,MAAI,OAA0B;AAC9B,MAAI,QAA6B;AACjC,SAAO,UAAU,QAAQ,uBAAuB;AAC9C,UAAM,KAAK,MAAM;AACjB,UAAM,YAAY,oBAAoB,MAAM;AAC5C,QAAI,WAAW;AACb,aAAO;AACP,cAAQ;AACR;AAAA,IACF;AACA,aAAS,OAAO;AAChB,aAAS;AAAA,EACX;AAEA,MAAI,CAAC,QAAQ,CAAC,OAAO;AACnB,WAAO,CAAC,OAAO;AAAA,EACjB;AAEA,QAAM,WAAW,MAAM,MAAM,GAAG,MAAM,QAAQ,IAAI,IAAI,CAAC,EAAE,QAAQ;AAIjE,MAAI,eAAgC,CAAC;AACrC,WAAS,IAAI,GAAG,IAAI,SAAS,SAAS,KAAK,cAAc,KAAK,GAAG;AAC/D,UAAM,cAAc,SAAS,CAAC;AAC9B,UAAM,cAAc,SAAS,IAAI,CAAC;AAClC,QAAI,CAAC,eAAe,CAAC,aAAa;AAChC;AAAA,IACF;AACA,QAAI,QAAuC,YAAY;AACvD,QAAI,QAAQ;AACZ,WAAO,SAAS,UAAU,eAAe,UAAU,YAAY,WAAW;AACxE,cAAQ,MAAM;AACd,eAAS;AAAA,IACX;AACA,QAAI,OAAO;AACT,mBAAa,KAAK,KAAK;AAAA,IACzB,OAAO;AACL,qBAAe;AAAA,IACjB;AAAA,EACF;AAEA,QAAM,eAAe,cAAc;AACnC,QAAM,oBAAoB,kBAAkB,aAAa;AACzD,QAAM,QAAuB,CAAC;AAC9B,QAAM,OAAO,oBAAI,IAAiB;AAClC,aAAW,UAAU,OAAO;AAC1B,UAAM,SAAS,eAAe,iBAAiB,QAAQ,YAAY,IAAI;AACvE,UAAM,OAAO,eAAe,QAAQ,cAAc,iBAAiB,IAC/D,YAAY,MAAM,IAClB,kBAAkB,QAAQ,cAAc,iBAAiB;AAC7D,QAAI,QAAQ,CAAC,KAAK,IAAI,IAAI,GAAG;AAC3B,WAAK,IAAI,IAAI;AACb,YAAM,KAAK,IAAI;AAAA,IACjB;AAAA,EACF;AAEA,MAAI,CAAC,KAAK,IAAI,OAAO,GAAG;AACtB,UAAM,KAAK,OAAO;AAAA,EACpB;AAEA,SAAO;AACT;AAEA,SAAS,yBAAyB,OAAyB;AACzD,MACE,UAAU,QACV,OAAO,UAAU,YACjB,OAAO,UAAU,WACjB;AACA,WAAO;AAAA,EACT;AAEA,MAAI,OAAO,UAAU,UAAU;AAC7B,WAAO,MAAM,SAAS,+BAClB,GAAG,MAAM,MAAM,GAAG,4BAA4B,CAAC,QAC/C;AAAA,EACN;AAEA,MAAI,MAAM,QAAQ,KAAK,GAAG;AACxB,WAAO,MAAM,MAAM,GAAG,EAAE,EAAE,IAAI,wBAAwB;AAAA,EACxD;AAEA,MAAI,OAAO,UAAU,UAAU;AAC7B,WAAO;AAAA,EACT;AAEA,SAAO;AACT;AAEA,SAAS,qBACP,OACqC;AACrC,MAAI,CAAC,OAAO;AACV,WAAO;AAAA,EACT;AAEA,QAAM,kBAA2C,CAAC;AAClD,aAAW,CAAC,KAAK,KAAK,KAAK,OAAO,QAAQ,KAAK,EAAE;AAAA,IAC/C;AAAA,IACA;AAAA,EACF,GAAG;AACD,QACE,QAAQ,cACR,QAAQ,SACR,IAAI,WAAW,IAAI,KACnB,OAAO,UAAU,cACjB,OAAO,UAAU,UACjB;AACA;AAAA,IACF;AAEA,UAAM,oBAAoB,yBAAyB,KAAK;AACxD,QAAI,sBAAsB,QAAW;AACnC,sBAAgB,GAAG,IAAI;AAAA,IACzB;AAAA,EACF;AAEA,SAAO,OAAO,KAAK,eAAe,EAAE,SAAS,IAAI,kBAAkB;AACrE;AAEO,SAAS,cAAc,SAAsB;AAClD,QAAM,YAAY,oBAAoB,OAAO;AAC7C,QAAM,aAAa,CAAC;AACpB,QAAM,WAAW,oBAAI,IAAY;AACjC,MAAI,eAA8C;AAClD,MAAI,QAAQ;AAEZ,SAAO,gBAAgB,QAAQ,uBAAuB;AACpD,UAAM,SAAS,eAAe,YAAY;AAC1C,QAAI,QAAQ;AACV,YAAM,UAAU,WAAW,YAAY;AACvC,YAAM,gBAAgB,iBAAiB,YAAY;AACnD,YAAM,MAAM;AAAA,QACV,OAAO;AAAA,QACP,OAAO;AAAA,QACP,OAAO,gBAAgB;AAAA,QACvB,WAAW;AAAA,QACX,iBAAiB;AAAA,MACnB,EAAE,KAAK,GAAG;AAEV,UAAI,CAAC,SAAS,IAAI,GAAG,GAAG;AACtB,iBAAS,IAAI,GAAG;AAChB,mBAAW,KAAK;AAAA,UACd,gBAAgB;AAAA,YACd,UAAU,OAAO;AAAA,YACjB,YAAY,OAAO;AAAA,YACnB,cAAc,OAAO;AAAA,YACrB;AAAA,UACF;AAAA,UACA;AAAA,UACA;AAAA,UACA,OAAO;AAAA,YACL,aAAa,iBAAiB,aAAa;AAAA,UAC7C;AAAA,QACF,CAAC;AAAA,MACH;AAAA,IACF;AAEA,mBAAe,aAAa;AAC5B,aAAS;AAAA,EACX;AAEA,SAAO;AACT;",
6
6
  "names": []
7
7
  }
@@ -5,6 +5,7 @@ import {
5
5
  } from "../iframeRpcChannel";
6
6
  import { setSelectionFrameEditing } from "../selectionFrame";
7
7
  import { symbolicateSelectedElement } from "../symbolicate";
8
+ import { getRepetitionSiblingElements } from "./useSiblingResolution";
8
9
  const EMPTY_ACTIVE_MARKS = {
9
10
  bold: false,
10
11
  italic: false,
@@ -180,6 +181,7 @@ function useEditModeStateMachine({
180
181
  interactionSessionRef.current = {
181
182
  type: "element",
182
183
  element,
184
+ siblingElements: getRepetitionSiblingElements({ element }),
183
185
  clickPosition: null,
184
186
  data: descriptor
185
187
  };
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../select-mode/hooks/useEditModeStateMachine.ts"],
4
- "sourcesContent": ["import type { MutableRefObject } from \"react\";\nimport type {\n DisplayMode,\n SelectableData,\n SelectModeFormatCommand,\n} from \"schemas/generated/selectMode\";\nimport type { SelectModeInteractionSession } from \"../selectModeSession\";\nimport type { InlineTiptapEditor } from \"../tiptapInlineEditor\";\n\nimport { useCallback, useEffect, useRef } from \"react\";\n\nimport {\n getWebsiteBuilderRpc,\n requestInlineTextEdit,\n} from \"../iframeRpcChannel\";\nimport { setSelectionFrameEditing } from \"../selectionFrame\";\nimport { symbolicateSelectedElement } from \"../symbolicate\";\n\ntype ActiveMarks = {\n bold: boolean;\n italic: boolean;\n underline: boolean;\n strike: boolean;\n};\n\nconst EMPTY_ACTIVE_MARKS: ActiveMarks = {\n bold: false,\n italic: false,\n underline: false,\n strike: false,\n};\n\n// Notify the parent of the inline editor's state so it can show the inline\n// formatting controls (and live mark state) only while editing.\nfunction postEditorState({\n activeMarks,\n isEditing,\n}: {\n activeMarks: ActiveMarks;\n isEditing: boolean;\n}) {\n getWebsiteBuilderRpc()?.editorStateUpdate({ activeMarks, isEditing });\n}\n\nexport function isTextEditableElement(\n element: Element,\n): element is HTMLElement {\n const tagName = element.tagName.toLowerCase();\n return (\n element instanceof HTMLElement &&\n Boolean(element.textContent?.trim()) &&\n ![\n \"input\",\n \"textarea\",\n \"select\",\n \"option\",\n \"img\",\n \"video\",\n \"canvas\",\n ].includes(tagName)\n );\n}\n\n// Window after entering an edit during which a focus loss to the parent frame\n// is treated as the select-mode panel's composer auto-focus stealing focus\n// (and is reclaimed) rather than a deliberate move into the editor UI.\nconst FOCUS_STEAL_GUARD_MS = 1500;\n\nexport function useEditModeStateMachine({\n displayMode,\n interactionSessionRef,\n}: {\n displayMode: DisplayMode;\n interactionSessionRef: MutableRefObject<SelectModeInteractionSession>;\n}) {\n const editorRef = useRef<InlineTiptapEditor | null>(null);\n const editingElementRef = useRef<HTMLElement | null>(null);\n const editIdRef = useRef<string | null>(null);\n const pendingEditRef = useRef<{\n editId: string;\n element: HTMLElement;\n timeout: number;\n } | null>(null);\n const originalInnerHTMLRef = useRef<string | null>(null);\n // The editor's HTML right after it parsed the original content. tiptap\n // re-serializes the markup, so this \u2014 not the raw `originalInnerHTML` \u2014 is the\n // apples-to-apples baseline for detecting whether a commit actually changed\n // anything.\n const initialHTMLRef = useRef<string | null>(null);\n const isCommittingRef = useRef(false);\n const beginEditingAtRef = useRef(0);\n\n const destroyEditor = useCallback(() => {\n editorRef.current?.destroy();\n editorRef.current = null;\n editingElementRef.current = null;\n editIdRef.current = null;\n if (pendingEditRef.current) {\n clearTimeout(pendingEditRef.current.timeout);\n pendingEditRef.current = null;\n }\n originalInnerHTMLRef.current = null;\n initialHTMLRef.current = null;\n isCommittingRef.current = false;\n setSelectionFrameEditing(false);\n postEditorState({ activeMarks: EMPTY_ACTIVE_MARKS, isEditing: false });\n }, []);\n\n const commit = useCallback(() => {\n const editor = editorRef.current;\n const editingElement = editingElementRef.current;\n const originalInnerHTML = originalInnerHTMLRef.current;\n const editId = editIdRef.current;\n if (\n !editor ||\n !editId ||\n originalInnerHTML === null ||\n isCommittingRef.current\n ) {\n return;\n }\n\n isCommittingRef.current = true;\n const committedHTML = editor.getHTML();\n\n // A blur or Shift-Enter with no actual change must not spawn an edit\n // session. When the content is untouched, restore the original markup and\n // tear down without notifying the parent.\n if (committedHTML === initialHTMLRef.current) {\n destroyEditor();\n if (editingElement) {\n editingElement.innerHTML = originalInnerHTML;\n }\n return;\n }\n\n const committedJSON = editor.getJSON();\n getWebsiteBuilderRpc()?.textCommitted({\n editId,\n html: committedHTML,\n json: committedJSON,\n originalInnerHTML,\n });\n // NOTE (Noah Gilmore, 2026-07-16): A directly mounted ProseMirror view\n // clears its root during destroy, so final HTML must be restored afterward.\n destroyEditor();\n if (editingElement) {\n editingElement.innerHTML = committedHTML;\n }\n }, [destroyEditor]);\n\n const revert = useCallback(() => {\n const editingElement = editingElementRef.current;\n const originalInnerHTML = originalInnerHTMLRef.current;\n destroyEditor();\n if (editingElement && originalInnerHTML !== null) {\n editingElement.innerHTML = originalInnerHTML;\n }\n }, [destroyEditor]);\n\n const handleBlur = useCallback(() => {\n if (isCommittingRef.current) {\n return;\n }\n // Focus stayed inside the iframe \u2014 the user clicked elsewhere on the page,\n // so this is a real \"click away\": commit.\n if (typeof document !== \"undefined\" && document.hasFocus()) {\n commit();\n return;\n }\n // Focus left the iframe to the parent editor. When it happens right as the\n // edit opens, the select-mode panel's composer auto-focus stole it \u2014 pull\n // it back so a double-click keeps the caret in the text. A deliberate move\n // into the editor UI later is left alone, and never commits/destroys.\n if (Date.now() - beginEditingAtRef.current < FOCUS_STEAL_GUARD_MS) {\n editorRef.current?.focus();\n }\n }, [commit]);\n\n const formatCommand = useCallback((command: SelectModeFormatCommand) => {\n editorRef.current?.runFormatCommand(command);\n }, []);\n\n const clearPendingEdit = useCallback((editId: string) => {\n if (pendingEditRef.current?.editId !== editId) {\n return;\n }\n clearTimeout(pendingEditRef.current.timeout);\n pendingEditRef.current = null;\n }, []);\n\n const resolveEditRequest = useCallback(\n ({ editId, allowed }: { editId: string; allowed: boolean }) => {\n const pendingEdit = pendingEditRef.current;\n if (!pendingEdit || pendingEdit.editId !== editId) {\n return;\n }\n clearTimeout(pendingEdit.timeout);\n pendingEditRef.current = null;\n if (!allowed || !pendingEdit.element.isConnected) {\n return;\n }\n\n const element = pendingEdit.element;\n const originalInnerHTML = element.innerHTML;\n editingElementRef.current = element;\n editIdRef.current = editId;\n originalInnerHTMLRef.current = originalInnerHTML;\n\n void import(\"../tiptapInlineEditor\")\n .then(({ createInlineTiptapEditor }) => {\n if (\n editingElementRef.current !== element ||\n editIdRef.current !== editId\n ) {\n return;\n }\n editorRef.current = createInlineTiptapEditor({\n element,\n content: originalInnerHTML,\n onCommit: commit,\n onEscape: revert,\n onBlur: handleBlur,\n onSelectionUpdate: (activeMarks) => {\n postEditorState({ activeMarks, isEditing: true });\n },\n });\n initialHTMLRef.current = editorRef.current.getHTML();\n beginEditingAtRef.current = Date.now();\n editorRef.current.focusAndSelectAll();\n setSelectionFrameEditing(true);\n postEditorState({ activeMarks: EMPTY_ACTIVE_MARKS, isEditing: true });\n })\n .catch(() => {\n if (\n editingElementRef.current === element &&\n editIdRef.current === editId\n ) {\n revert();\n }\n });\n },\n [commit, handleBlur, revert],\n );\n\n const beginEditing = useCallback(\n ({\n element,\n descriptorPromise,\n }: {\n element: Element;\n descriptorPromise: Promise<SelectableData>;\n }) => {\n // Already editing this element \u2014 re-entering would re-read the mid-edit\n // DOM as the \"original\" and rebuild the editor over itself, wiping the\n // content. The pointer-up guard prevents the usual trigger; this is the\n // backstop for any other path.\n if (editingElementRef.current === element) {\n return;\n }\n if (!isTextEditableElement(element)) {\n return;\n }\n // Same element already awaiting symbolicate / parent allow \u2014 ignore.\n // A different element supersedes the in-flight attempt below.\n if (pendingEditRef.current?.element === element) {\n return;\n }\n\n void descriptorPromise.then((descriptor) => {\n if (descriptor.kind !== \"react-element\" || !element.isConnected) {\n return;\n }\n interactionSessionRef.current = {\n type: \"element\",\n element,\n clickPosition: null,\n data: descriptor,\n };\n const editId = crypto.randomUUID();\n if (pendingEditRef.current) {\n clearTimeout(pendingEditRef.current.timeout);\n }\n // Symbolicate has no AbortSignal; a wedged sandbox can hang forever.\n // Self-clear so a hung attempt can't permanently block inline edit.\n const timeout = window.setTimeout(() => {\n clearPendingEdit(editId);\n }, 5000);\n pendingEditRef.current = { editId, element, timeout };\n void symbolicateSelectedElement(descriptor).then(\n (symbolicated) => {\n if (pendingEditRef.current?.editId !== editId) {\n return;\n }\n void requestInlineTextEdit({\n editId,\n element: symbolicated,\n }).then(\n ({ allowed }) => {\n resolveEditRequest({ editId, allowed });\n },\n () => {\n resolveEditRequest({ editId, allowed: false });\n },\n );\n },\n () => {\n clearPendingEdit(editId);\n },\n );\n });\n },\n [clearPendingEdit, interactionSessionRef, resolveEditRequest],\n );\n\n // eslint-disable-next-line replo/no-use-effect -- Tears down the inline editor when leaving selection mode.\n useEffect(() => {\n if (displayMode !== \"selection\") {\n revert();\n }\n }, [displayMode, revert]);\n\n return {\n beginEditing,\n editingElementRef,\n formatCommand,\n revert,\n };\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import type { MutableRefObject } from \"react\";\nimport type {\n DisplayMode,\n SelectableData,\n SelectModeFormatCommand,\n} from \"schemas/generated/selectMode\";\nimport type { SelectModeInteractionSession } from \"../selectModeSession\";\nimport type { InlineTiptapEditor } from \"../tiptapInlineEditor\";\n\nimport { useCallback, useEffect, useRef } from \"react\";\n\nimport {\n getWebsiteBuilderRpc,\n requestInlineTextEdit,\n} from \"../iframeRpcChannel\";\nimport { setSelectionFrameEditing } from \"../selectionFrame\";\nimport { symbolicateSelectedElement } from \"../symbolicate\";\nimport { getRepetitionSiblingElements } from \"./useSiblingResolution\";\n\ntype ActiveMarks = {\n bold: boolean;\n italic: boolean;\n underline: boolean;\n strike: boolean;\n};\n\nconst EMPTY_ACTIVE_MARKS: ActiveMarks = {\n bold: false,\n italic: false,\n underline: false,\n strike: false,\n};\n\n// Notify the parent of the inline editor's state so it can show the inline\n// formatting controls (and live mark state) only while editing.\nfunction postEditorState({\n activeMarks,\n isEditing,\n}: {\n activeMarks: ActiveMarks;\n isEditing: boolean;\n}) {\n getWebsiteBuilderRpc()?.editorStateUpdate({ activeMarks, isEditing });\n}\n\nexport function isTextEditableElement(\n element: Element,\n): element is HTMLElement {\n const tagName = element.tagName.toLowerCase();\n return (\n element instanceof HTMLElement &&\n Boolean(element.textContent?.trim()) &&\n ![\n \"input\",\n \"textarea\",\n \"select\",\n \"option\",\n \"img\",\n \"video\",\n \"canvas\",\n ].includes(tagName)\n );\n}\n\n// Window after entering an edit during which a focus loss to the parent frame\n// is treated as the select-mode panel's composer auto-focus stealing focus\n// (and is reclaimed) rather than a deliberate move into the editor UI.\nconst FOCUS_STEAL_GUARD_MS = 1500;\n\nexport function useEditModeStateMachine({\n displayMode,\n interactionSessionRef,\n}: {\n displayMode: DisplayMode;\n interactionSessionRef: MutableRefObject<SelectModeInteractionSession>;\n}) {\n const editorRef = useRef<InlineTiptapEditor | null>(null);\n const editingElementRef = useRef<HTMLElement | null>(null);\n const editIdRef = useRef<string | null>(null);\n const pendingEditRef = useRef<{\n editId: string;\n element: HTMLElement;\n timeout: number;\n } | null>(null);\n const originalInnerHTMLRef = useRef<string | null>(null);\n // The editor's HTML right after it parsed the original content. tiptap\n // re-serializes the markup, so this \u2014 not the raw `originalInnerHTML` \u2014 is the\n // apples-to-apples baseline for detecting whether a commit actually changed\n // anything.\n const initialHTMLRef = useRef<string | null>(null);\n const isCommittingRef = useRef(false);\n const beginEditingAtRef = useRef(0);\n\n const destroyEditor = useCallback(() => {\n editorRef.current?.destroy();\n editorRef.current = null;\n editingElementRef.current = null;\n editIdRef.current = null;\n if (pendingEditRef.current) {\n clearTimeout(pendingEditRef.current.timeout);\n pendingEditRef.current = null;\n }\n originalInnerHTMLRef.current = null;\n initialHTMLRef.current = null;\n isCommittingRef.current = false;\n setSelectionFrameEditing(false);\n postEditorState({ activeMarks: EMPTY_ACTIVE_MARKS, isEditing: false });\n }, []);\n\n const commit = useCallback(() => {\n const editor = editorRef.current;\n const editingElement = editingElementRef.current;\n const originalInnerHTML = originalInnerHTMLRef.current;\n const editId = editIdRef.current;\n if (\n !editor ||\n !editId ||\n originalInnerHTML === null ||\n isCommittingRef.current\n ) {\n return;\n }\n\n isCommittingRef.current = true;\n const committedHTML = editor.getHTML();\n\n // A blur or Shift-Enter with no actual change must not spawn an edit\n // session. When the content is untouched, restore the original markup and\n // tear down without notifying the parent.\n if (committedHTML === initialHTMLRef.current) {\n destroyEditor();\n if (editingElement) {\n editingElement.innerHTML = originalInnerHTML;\n }\n return;\n }\n\n const committedJSON = editor.getJSON();\n getWebsiteBuilderRpc()?.textCommitted({\n editId,\n html: committedHTML,\n json: committedJSON,\n originalInnerHTML,\n });\n // NOTE (Noah Gilmore, 2026-07-16): A directly mounted ProseMirror view\n // clears its root during destroy, so final HTML must be restored afterward.\n destroyEditor();\n if (editingElement) {\n editingElement.innerHTML = committedHTML;\n }\n }, [destroyEditor]);\n\n const revert = useCallback(() => {\n const editingElement = editingElementRef.current;\n const originalInnerHTML = originalInnerHTMLRef.current;\n destroyEditor();\n if (editingElement && originalInnerHTML !== null) {\n editingElement.innerHTML = originalInnerHTML;\n }\n }, [destroyEditor]);\n\n const handleBlur = useCallback(() => {\n if (isCommittingRef.current) {\n return;\n }\n // Focus stayed inside the iframe \u2014 the user clicked elsewhere on the page,\n // so this is a real \"click away\": commit.\n if (typeof document !== \"undefined\" && document.hasFocus()) {\n commit();\n return;\n }\n // Focus left the iframe to the parent editor. When it happens right as the\n // edit opens, the select-mode panel's composer auto-focus stole it \u2014 pull\n // it back so a double-click keeps the caret in the text. A deliberate move\n // into the editor UI later is left alone, and never commits/destroys.\n if (Date.now() - beginEditingAtRef.current < FOCUS_STEAL_GUARD_MS) {\n editorRef.current?.focus();\n }\n }, [commit]);\n\n const formatCommand = useCallback((command: SelectModeFormatCommand) => {\n editorRef.current?.runFormatCommand(command);\n }, []);\n\n const clearPendingEdit = useCallback((editId: string) => {\n if (pendingEditRef.current?.editId !== editId) {\n return;\n }\n clearTimeout(pendingEditRef.current.timeout);\n pendingEditRef.current = null;\n }, []);\n\n const resolveEditRequest = useCallback(\n ({ editId, allowed }: { editId: string; allowed: boolean }) => {\n const pendingEdit = pendingEditRef.current;\n if (!pendingEdit || pendingEdit.editId !== editId) {\n return;\n }\n clearTimeout(pendingEdit.timeout);\n pendingEditRef.current = null;\n if (!allowed || !pendingEdit.element.isConnected) {\n return;\n }\n\n const element = pendingEdit.element;\n const originalInnerHTML = element.innerHTML;\n editingElementRef.current = element;\n editIdRef.current = editId;\n originalInnerHTMLRef.current = originalInnerHTML;\n\n void import(\"../tiptapInlineEditor\")\n .then(({ createInlineTiptapEditor }) => {\n if (\n editingElementRef.current !== element ||\n editIdRef.current !== editId\n ) {\n return;\n }\n editorRef.current = createInlineTiptapEditor({\n element,\n content: originalInnerHTML,\n onCommit: commit,\n onEscape: revert,\n onBlur: handleBlur,\n onSelectionUpdate: (activeMarks) => {\n postEditorState({ activeMarks, isEditing: true });\n },\n });\n initialHTMLRef.current = editorRef.current.getHTML();\n beginEditingAtRef.current = Date.now();\n editorRef.current.focusAndSelectAll();\n setSelectionFrameEditing(true);\n postEditorState({ activeMarks: EMPTY_ACTIVE_MARKS, isEditing: true });\n })\n .catch(() => {\n if (\n editingElementRef.current === element &&\n editIdRef.current === editId\n ) {\n revert();\n }\n });\n },\n [commit, handleBlur, revert],\n );\n\n const beginEditing = useCallback(\n ({\n element,\n descriptorPromise,\n }: {\n element: Element;\n descriptorPromise: Promise<SelectableData>;\n }) => {\n // Already editing this element \u2014 re-entering would re-read the mid-edit\n // DOM as the \"original\" and rebuild the editor over itself, wiping the\n // content. The pointer-up guard prevents the usual trigger; this is the\n // backstop for any other path.\n if (editingElementRef.current === element) {\n return;\n }\n if (!isTextEditableElement(element)) {\n return;\n }\n // Same element already awaiting symbolicate / parent allow \u2014 ignore.\n // A different element supersedes the in-flight attempt below.\n if (pendingEditRef.current?.element === element) {\n return;\n }\n\n void descriptorPromise.then((descriptor) => {\n if (descriptor.kind !== \"react-element\" || !element.isConnected) {\n return;\n }\n interactionSessionRef.current = {\n type: \"element\",\n element,\n siblingElements: getRepetitionSiblingElements({ element }),\n clickPosition: null,\n data: descriptor,\n };\n const editId = crypto.randomUUID();\n if (pendingEditRef.current) {\n clearTimeout(pendingEditRef.current.timeout);\n }\n // Symbolicate has no AbortSignal; a wedged sandbox can hang forever.\n // Self-clear so a hung attempt can't permanently block inline edit.\n const timeout = window.setTimeout(() => {\n clearPendingEdit(editId);\n }, 5000);\n pendingEditRef.current = { editId, element, timeout };\n void symbolicateSelectedElement(descriptor).then(\n (symbolicated) => {\n if (pendingEditRef.current?.editId !== editId) {\n return;\n }\n void requestInlineTextEdit({\n editId,\n element: symbolicated,\n }).then(\n ({ allowed }) => {\n resolveEditRequest({ editId, allowed });\n },\n () => {\n resolveEditRequest({ editId, allowed: false });\n },\n );\n },\n () => {\n clearPendingEdit(editId);\n },\n );\n });\n },\n [clearPendingEdit, interactionSessionRef, resolveEditRequest],\n );\n\n // eslint-disable-next-line replo/no-use-effect -- Tears down the inline editor when leaving selection mode.\n useEffect(() => {\n if (displayMode !== \"selection\") {\n revert();\n }\n }, [displayMode, revert]);\n\n return {\n beginEditing,\n editingElementRef,\n formatCommand,\n revert,\n };\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -1,6 +1,6 @@
1
1
  import { useCallback } from "react";
2
2
  import { buildSelector } from "../descriptor/elementDescriptor";
3
- import { getSourceLocationSiblingSelectors } from "./useSiblingResolution";
3
+ import { getRepetitionSiblingSelectors } from "./useSiblingResolution";
4
4
  const OVERRIDE_REGISTRATION_TIMEOUT_MS = 15e3;
5
5
  const SESSION_OUTCOME_RETENTION_MS = 6e4;
6
6
  const OVERRIDE_ATTRIBUTE = "data-replo-select-overrides";
@@ -36,7 +36,7 @@ function getElementsForTargetMode({
36
36
  }
37
37
  return [
38
38
  element,
39
- ...getSourceLocationSiblingSelectors({ element }).map((selector) => document.querySelector(selector)).filter((candidate) => {
39
+ ...getRepetitionSiblingSelectors({ element }).map((selector) => document.querySelector(selector)).filter((candidate) => {
40
40
  return candidate instanceof HTMLElement;
41
41
  })
42
42
  ];
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../select-mode/hooks/useOptimisticOverrides.ts"],
4
- "sourcesContent": ["import type { SelectModeOptimisticUpdatePayload } from \"schemas/generated/selectMode\";\n\nimport { useCallback } from \"react\";\n\nimport { buildSelector } from \"../descriptor/elementDescriptor\";\nimport { getSourceLocationSiblingSelectors } from \"./useSiblingResolution\";\n\ntype OverrideTarget = {\n element: HTMLElement;\n originalState: OriginalPayloadState;\n selector: string;\n};\n\ntype OriginalPayloadState =\n | {\n kind: \"attributes\";\n values: { name: string; value: string | null }[];\n }\n | { kind: \"content\"; innerHTML: string }\n | { kind: \"css\" };\n\ntype IframeOverride = {\n editId: string;\n payload: SelectModeOptimisticUpdatePayload;\n registrationTimeout: number;\n sessionId: string | null;\n state: \"live\" | \"fired\";\n targets: OverrideTarget[];\n token: string;\n};\n\nconst OVERRIDE_REGISTRATION_TIMEOUT_MS = 15_000;\nconst SESSION_OUTCOME_RETENTION_MS = 60_000;\nconst OVERRIDE_ATTRIBUTE = \"data-replo-select-overrides\";\nconst OVERRIDE_REGISTRY_KEY = \"reploSelectOverrideRegistryV2\";\nconst SESSION_OUTCOME_REGISTRY_KEY = \"reploSelectOverrideSessionOutcomesV2\";\n\ntype SessionOutcome = \"persisted\" | \"failed\";\n\nfunction getOverrideList(): IframeOverride[] {\n const existing: unknown = Reflect.get(window, OVERRIDE_REGISTRY_KEY);\n if (Array.isArray(existing)) {\n return existing;\n }\n const overrides: IframeOverride[] = [];\n Reflect.set(window, OVERRIDE_REGISTRY_KEY, overrides);\n return overrides;\n}\n\nfunction setOverrideList(overrides: IframeOverride[]) {\n Reflect.set(window, OVERRIDE_REGISTRY_KEY, overrides);\n}\n\nfunction getSessionOutcomes(): Map<string, SessionOutcome> {\n const existing: unknown = Reflect.get(window, SESSION_OUTCOME_REGISTRY_KEY);\n if (existing instanceof Map) {\n return existing;\n }\n const outcomes = new Map<string, SessionOutcome>();\n Reflect.set(window, SESSION_OUTCOME_REGISTRY_KEY, outcomes);\n return outcomes;\n}\n\nfunction getElementsForTargetMode({\n element,\n targetMode,\n}: {\n element: HTMLElement;\n targetMode: \"primary\" | \"group\";\n}) {\n if (targetMode === \"primary\") {\n return [element];\n }\n\n return [\n element,\n ...getSourceLocationSiblingSelectors({ element })\n .map((selector) => document.querySelector(selector))\n .filter((candidate): candidate is HTMLElement => {\n return candidate instanceof HTMLElement;\n }),\n ];\n}\n\nfunction isImageSourcePayload({\n element,\n payload,\n}: {\n element: HTMLElement;\n payload: SelectModeOptimisticUpdatePayload;\n}) {\n return (\n payload.kind === \"attribute\" &&\n payload.name.toLowerCase() === \"src\" &&\n element instanceof HTMLImageElement\n );\n}\n\nfunction getOriginalState({\n element,\n payload,\n}: {\n element: HTMLElement;\n payload: SelectModeOptimisticUpdatePayload;\n}): OriginalPayloadState {\n if (payload.kind === \"attribute\") {\n const names = isImageSourcePayload({ element, payload })\n ? [payload.name, \"srcset\"]\n : [payload.name];\n return {\n kind: \"attributes\",\n values: names.map((name) => ({\n name,\n value: element.getAttribute(name),\n })),\n };\n }\n if (payload.kind === \"content\") {\n return { kind: \"content\", innerHTML: element.innerHTML };\n }\n return { kind: \"css\" };\n}\n\nfunction getStyleElement(token: string) {\n return document.querySelector<HTMLStyleElement>(\n `style[data-replo-select-override=\"${token}\"]`,\n );\n}\n\nfunction addToken(element: HTMLElement, token: string) {\n const tokens = new Set(\n (element.getAttribute(OVERRIDE_ATTRIBUTE) ?? \"\")\n .split(/\\s+/)\n .filter(Boolean),\n );\n tokens.add(token);\n element.setAttribute(OVERRIDE_ATTRIBUTE, [...tokens].join(\" \"));\n}\n\nfunction removeToken(element: HTMLElement, token: string) {\n const tokens = (element.getAttribute(OVERRIDE_ATTRIBUTE) ?? \"\")\n .split(/\\s+/)\n .filter((candidate) => candidate && candidate !== token);\n if (tokens.length === 0) {\n element.removeAttribute(OVERRIDE_ATTRIBUTE);\n return;\n }\n element.setAttribute(OVERRIDE_ATTRIBUTE, tokens.join(\" \"));\n}\n\nfunction applyPayload({\n element,\n payload,\n token,\n}: {\n element: HTMLElement;\n payload: SelectModeOptimisticUpdatePayload;\n token: string;\n}) {\n if (payload.kind === \"css\") {\n addToken(element, token);\n return;\n }\n if (payload.kind === \"attribute\") {\n if (isImageSourcePayload({ element, payload })) {\n element.removeAttribute(\"srcset\");\n }\n element.setAttribute(payload.name, payload.value);\n return;\n }\n element.innerHTML = payload.innerHTML;\n}\n\nfunction restoreOriginalState({\n element,\n originalState,\n}: {\n element: HTMLElement;\n originalState: OriginalPayloadState;\n}) {\n if (originalState.kind === \"attributes\") {\n for (const { name, value } of originalState.values) {\n if (value === null) {\n element.removeAttribute(name);\n } else {\n element.setAttribute(name, value);\n }\n }\n return;\n }\n if (originalState.kind === \"content\") {\n element.innerHTML = originalState.innerHTML;\n }\n}\n\nfunction installCssRule(override: IframeOverride) {\n if (override.payload.kind !== \"css\") {\n return;\n }\n const style = document.createElement(\"style\");\n style.dataset.reploSelectOverride = override.token;\n style.textContent = `[${OVERRIDE_ATTRIBUTE}~=\"${override.token}\"] { ${override.payload.property}: ${override.payload.value ?? \"revert\"} !important; }`;\n document.head.append(style);\n}\n\nfunction resolveTarget(target: OverrideTarget) {\n if (target.element.isConnected) {\n return target.element;\n }\n const replacement = document.querySelector(target.selector);\n return replacement instanceof HTMLElement ? replacement : null;\n}\n\nfunction reapplyOverride({\n override,\n skipElement,\n}: {\n override: IframeOverride;\n skipElement: HTMLElement | null;\n}) {\n for (const target of override.targets) {\n const element = resolveTarget(target);\n if (!element || (skipElement && element.contains(skipElement))) {\n continue;\n }\n if (element !== target.element) {\n target.element = element;\n target.originalState = getOriginalState({\n element,\n payload: override.payload,\n });\n }\n applyPayload({ element, payload: override.payload, token: override.token });\n }\n}\n\nfunction removeOverride({\n editId,\n outcome,\n}: {\n editId: string;\n outcome: SessionOutcome;\n}) {\n const override = getOverrideList().find(\n (candidate) => candidate.editId === editId,\n );\n if (!override) {\n return;\n }\n\n clearTimeout(override.registrationTimeout);\n getStyleElement(override.token)?.remove();\n for (const target of override.targets) {\n const element = resolveTarget(target);\n if (!element) {\n continue;\n }\n removeToken(element, override.token);\n if (outcome === \"failed\") {\n restoreOriginalState({\n element,\n originalState: target.originalState,\n });\n }\n }\n setOverrideList(\n getOverrideList().filter((candidate) => candidate.editId !== editId),\n );\n\n if (outcome === \"failed\") {\n for (const remainingOverride of getOverrideList()) {\n reapplyOverride({ override: remainingOverride, skipElement: null });\n }\n }\n}\n\nexport function useOptimisticOverrides() {\n const applyOptimisticUpdate = useCallback(\n ({\n editId,\n payload,\n targetSelector,\n targetMode,\n }: {\n editId: string;\n payload: SelectModeOptimisticUpdatePayload;\n targetSelector: string;\n targetMode: \"primary\" | \"group\";\n }) => {\n const targetElement = document.querySelector(targetSelector);\n if (!(targetElement instanceof HTMLElement)) {\n return;\n }\n\n removeOverride({ editId, outcome: \"failed\" });\n const elements = getElementsForTargetMode({\n element: targetElement,\n targetMode,\n });\n const token = `override-${editId}`;\n const override: IframeOverride = {\n editId,\n payload,\n registrationTimeout: window.setTimeout(() => {\n removeOverride({ editId, outcome: \"failed\" });\n }, OVERRIDE_REGISTRATION_TIMEOUT_MS),\n sessionId: null,\n state: \"live\",\n targets: elements.map((element) => ({\n element,\n originalState: getOriginalState({ element, payload }),\n selector: buildSelector(element),\n })),\n token,\n };\n getOverrideList().push(override);\n installCssRule(override);\n reapplyOverride({ override, skipElement: null });\n },\n [],\n );\n\n const reapplyOverrides = useCallback(\n ({ skipElement }: { skipElement: HTMLElement | null }) => {\n for (const override of getOverrideList()) {\n reapplyOverride({ override, skipElement });\n }\n },\n [],\n );\n\n const markCommitted = useCallback((editId: string, sessionId: string) => {\n const override = getOverrideList().find((candidate) => {\n return candidate.editId === editId;\n });\n if (!override) {\n return;\n }\n clearTimeout(override.registrationTimeout);\n override.sessionId = sessionId;\n override.state = \"fired\";\n const outcome = getSessionOutcomes().get(sessionId);\n if (outcome) {\n removeOverride({ editId, outcome });\n }\n }, []);\n\n const resolveSessionOverrides = useCallback(\n (sessionId: string, outcome: SessionOutcome) => {\n getSessionOutcomes().set(sessionId, outcome);\n window.setTimeout(() => {\n getSessionOutcomes().delete(sessionId);\n }, SESSION_OUTCOME_RETENTION_MS);\n const editIds = getOverrideList()\n .filter((override) => override.sessionId === sessionId)\n .map((override) => override.editId);\n for (const editId of editIds) {\n removeOverride({ editId, outcome });\n }\n },\n [],\n );\n\n const revertOverride = useCallback((editId: string) => {\n removeOverride({ editId, outcome: \"failed\" });\n }, []);\n\n return {\n applyOptimisticUpdate,\n markCommitted,\n reapplyOverrides,\n resolveSessionOverrides,\n revertOverride,\n };\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import type { SelectModeOptimisticUpdatePayload } from \"schemas/generated/selectMode\";\n\nimport { useCallback } from \"react\";\n\nimport { buildSelector } from \"../descriptor/elementDescriptor\";\nimport { getRepetitionSiblingSelectors } from \"./useSiblingResolution\";\n\ntype OverrideTarget = {\n element: HTMLElement;\n originalState: OriginalPayloadState;\n selector: string;\n};\n\ntype OriginalPayloadState =\n | {\n kind: \"attributes\";\n values: { name: string; value: string | null }[];\n }\n | { kind: \"content\"; innerHTML: string }\n | { kind: \"css\" };\n\ntype IframeOverride = {\n editId: string;\n payload: SelectModeOptimisticUpdatePayload;\n registrationTimeout: number;\n sessionId: string | null;\n state: \"live\" | \"fired\";\n targets: OverrideTarget[];\n token: string;\n};\n\nconst OVERRIDE_REGISTRATION_TIMEOUT_MS = 15_000;\nconst SESSION_OUTCOME_RETENTION_MS = 60_000;\nconst OVERRIDE_ATTRIBUTE = \"data-replo-select-overrides\";\nconst OVERRIDE_REGISTRY_KEY = \"reploSelectOverrideRegistryV2\";\nconst SESSION_OUTCOME_REGISTRY_KEY = \"reploSelectOverrideSessionOutcomesV2\";\n\ntype SessionOutcome = \"persisted\" | \"failed\";\n\nfunction getOverrideList(): IframeOverride[] {\n const existing: unknown = Reflect.get(window, OVERRIDE_REGISTRY_KEY);\n if (Array.isArray(existing)) {\n return existing;\n }\n const overrides: IframeOverride[] = [];\n Reflect.set(window, OVERRIDE_REGISTRY_KEY, overrides);\n return overrides;\n}\n\nfunction setOverrideList(overrides: IframeOverride[]) {\n Reflect.set(window, OVERRIDE_REGISTRY_KEY, overrides);\n}\n\nfunction getSessionOutcomes(): Map<string, SessionOutcome> {\n const existing: unknown = Reflect.get(window, SESSION_OUTCOME_REGISTRY_KEY);\n if (existing instanceof Map) {\n return existing;\n }\n const outcomes = new Map<string, SessionOutcome>();\n Reflect.set(window, SESSION_OUTCOME_REGISTRY_KEY, outcomes);\n return outcomes;\n}\n\nfunction getElementsForTargetMode({\n element,\n targetMode,\n}: {\n element: HTMLElement;\n targetMode: \"primary\" | \"group\";\n}) {\n if (targetMode === \"primary\") {\n return [element];\n }\n\n return [\n element,\n ...getRepetitionSiblingSelectors({ element })\n .map((selector) => document.querySelector(selector))\n .filter((candidate): candidate is HTMLElement => {\n return candidate instanceof HTMLElement;\n }),\n ];\n}\n\nfunction isImageSourcePayload({\n element,\n payload,\n}: {\n element: HTMLElement;\n payload: SelectModeOptimisticUpdatePayload;\n}) {\n return (\n payload.kind === \"attribute\" &&\n payload.name.toLowerCase() === \"src\" &&\n element instanceof HTMLImageElement\n );\n}\n\nfunction getOriginalState({\n element,\n payload,\n}: {\n element: HTMLElement;\n payload: SelectModeOptimisticUpdatePayload;\n}): OriginalPayloadState {\n if (payload.kind === \"attribute\") {\n const names = isImageSourcePayload({ element, payload })\n ? [payload.name, \"srcset\"]\n : [payload.name];\n return {\n kind: \"attributes\",\n values: names.map((name) => ({\n name,\n value: element.getAttribute(name),\n })),\n };\n }\n if (payload.kind === \"content\") {\n return { kind: \"content\", innerHTML: element.innerHTML };\n }\n return { kind: \"css\" };\n}\n\nfunction getStyleElement(token: string) {\n return document.querySelector<HTMLStyleElement>(\n `style[data-replo-select-override=\"${token}\"]`,\n );\n}\n\nfunction addToken(element: HTMLElement, token: string) {\n const tokens = new Set(\n (element.getAttribute(OVERRIDE_ATTRIBUTE) ?? \"\")\n .split(/\\s+/)\n .filter(Boolean),\n );\n tokens.add(token);\n element.setAttribute(OVERRIDE_ATTRIBUTE, [...tokens].join(\" \"));\n}\n\nfunction removeToken(element: HTMLElement, token: string) {\n const tokens = (element.getAttribute(OVERRIDE_ATTRIBUTE) ?? \"\")\n .split(/\\s+/)\n .filter((candidate) => candidate && candidate !== token);\n if (tokens.length === 0) {\n element.removeAttribute(OVERRIDE_ATTRIBUTE);\n return;\n }\n element.setAttribute(OVERRIDE_ATTRIBUTE, tokens.join(\" \"));\n}\n\nfunction applyPayload({\n element,\n payload,\n token,\n}: {\n element: HTMLElement;\n payload: SelectModeOptimisticUpdatePayload;\n token: string;\n}) {\n if (payload.kind === \"css\") {\n addToken(element, token);\n return;\n }\n if (payload.kind === \"attribute\") {\n if (isImageSourcePayload({ element, payload })) {\n element.removeAttribute(\"srcset\");\n }\n element.setAttribute(payload.name, payload.value);\n return;\n }\n element.innerHTML = payload.innerHTML;\n}\n\nfunction restoreOriginalState({\n element,\n originalState,\n}: {\n element: HTMLElement;\n originalState: OriginalPayloadState;\n}) {\n if (originalState.kind === \"attributes\") {\n for (const { name, value } of originalState.values) {\n if (value === null) {\n element.removeAttribute(name);\n } else {\n element.setAttribute(name, value);\n }\n }\n return;\n }\n if (originalState.kind === \"content\") {\n element.innerHTML = originalState.innerHTML;\n }\n}\n\nfunction installCssRule(override: IframeOverride) {\n if (override.payload.kind !== \"css\") {\n return;\n }\n const style = document.createElement(\"style\");\n style.dataset.reploSelectOverride = override.token;\n style.textContent = `[${OVERRIDE_ATTRIBUTE}~=\"${override.token}\"] { ${override.payload.property}: ${override.payload.value ?? \"revert\"} !important; }`;\n document.head.append(style);\n}\n\nfunction resolveTarget(target: OverrideTarget) {\n if (target.element.isConnected) {\n return target.element;\n }\n const replacement = document.querySelector(target.selector);\n return replacement instanceof HTMLElement ? replacement : null;\n}\n\nfunction reapplyOverride({\n override,\n skipElement,\n}: {\n override: IframeOverride;\n skipElement: HTMLElement | null;\n}) {\n for (const target of override.targets) {\n const element = resolveTarget(target);\n if (!element || (skipElement && element.contains(skipElement))) {\n continue;\n }\n if (element !== target.element) {\n target.element = element;\n target.originalState = getOriginalState({\n element,\n payload: override.payload,\n });\n }\n applyPayload({ element, payload: override.payload, token: override.token });\n }\n}\n\nfunction removeOverride({\n editId,\n outcome,\n}: {\n editId: string;\n outcome: SessionOutcome;\n}) {\n const override = getOverrideList().find(\n (candidate) => candidate.editId === editId,\n );\n if (!override) {\n return;\n }\n\n clearTimeout(override.registrationTimeout);\n getStyleElement(override.token)?.remove();\n for (const target of override.targets) {\n const element = resolveTarget(target);\n if (!element) {\n continue;\n }\n removeToken(element, override.token);\n if (outcome === \"failed\") {\n restoreOriginalState({\n element,\n originalState: target.originalState,\n });\n }\n }\n setOverrideList(\n getOverrideList().filter((candidate) => candidate.editId !== editId),\n );\n\n if (outcome === \"failed\") {\n for (const remainingOverride of getOverrideList()) {\n reapplyOverride({ override: remainingOverride, skipElement: null });\n }\n }\n}\n\nexport function useOptimisticOverrides() {\n const applyOptimisticUpdate = useCallback(\n ({\n editId,\n payload,\n targetSelector,\n targetMode,\n }: {\n editId: string;\n payload: SelectModeOptimisticUpdatePayload;\n targetSelector: string;\n targetMode: \"primary\" | \"group\";\n }) => {\n const targetElement = document.querySelector(targetSelector);\n if (!(targetElement instanceof HTMLElement)) {\n return;\n }\n\n removeOverride({ editId, outcome: \"failed\" });\n const elements = getElementsForTargetMode({\n element: targetElement,\n targetMode,\n });\n const token = `override-${editId}`;\n const override: IframeOverride = {\n editId,\n payload,\n registrationTimeout: window.setTimeout(() => {\n removeOverride({ editId, outcome: \"failed\" });\n }, OVERRIDE_REGISTRATION_TIMEOUT_MS),\n sessionId: null,\n state: \"live\",\n targets: elements.map((element) => ({\n element,\n originalState: getOriginalState({ element, payload }),\n selector: buildSelector(element),\n })),\n token,\n };\n getOverrideList().push(override);\n installCssRule(override);\n reapplyOverride({ override, skipElement: null });\n },\n [],\n );\n\n const reapplyOverrides = useCallback(\n ({ skipElement }: { skipElement: HTMLElement | null }) => {\n for (const override of getOverrideList()) {\n reapplyOverride({ override, skipElement });\n }\n },\n [],\n );\n\n const markCommitted = useCallback((editId: string, sessionId: string) => {\n const override = getOverrideList().find((candidate) => {\n return candidate.editId === editId;\n });\n if (!override) {\n return;\n }\n clearTimeout(override.registrationTimeout);\n override.sessionId = sessionId;\n override.state = \"fired\";\n const outcome = getSessionOutcomes().get(sessionId);\n if (outcome) {\n removeOverride({ editId, outcome });\n }\n }, []);\n\n const resolveSessionOverrides = useCallback(\n (sessionId: string, outcome: SessionOutcome) => {\n getSessionOutcomes().set(sessionId, outcome);\n window.setTimeout(() => {\n getSessionOutcomes().delete(sessionId);\n }, SESSION_OUTCOME_RETENTION_MS);\n const editIds = getOverrideList()\n .filter((override) => override.sessionId === sessionId)\n .map((override) => override.editId);\n for (const editId of editIds) {\n removeOverride({ editId, outcome });\n }\n },\n [],\n );\n\n const revertOverride = useCallback((editId: string) => {\n removeOverride({ editId, outcome: \"failed\" });\n }, []);\n\n return {\n applyOptimisticUpdate,\n markCommitted,\n reapplyOverrides,\n resolveSessionOverrides,\n revertOverride,\n };\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -1,10 +1,28 @@
1
1
  import { useEffect, useMemo } from "react";
2
2
  import { useIframeRpcChannel } from "../../_vendor/replo-utils/react/use-iframe-rpc-channel.mjs";
3
+ import { iframeRuntimeBuildMetadataSchema } from "../../_vendor/schemas/selectModeIframeRpc.mjs";
4
+ import { z } from "zod";
5
+ import { BUILD_METADATA } from "../../buildMetadata";
3
6
  import {
4
7
  getSelectModeIframeRpcChannel,
5
8
  getWebsiteBuilderRpc,
6
9
  setSelectModeParentHandlers
7
10
  } from "../iframeRpcChannel";
11
+ const SDK_BUILD_METADATA_SPECIFIER = "@replohq/sdk/lib/buildMetadata";
12
+ const sdkBuildMetadataModuleSchema = z.object({
13
+ BUILD_METADATA: iframeRuntimeBuildMetadataSchema
14
+ });
15
+ async function announceSdkBuildMetadata() {
16
+ try {
17
+ const sdkBuildMetadata = sdkBuildMetadataModuleSchema.safeParse(
18
+ await import(SDK_BUILD_METADATA_SPECIFIER)
19
+ );
20
+ if (sdkBuildMetadata.success) {
21
+ getWebsiteBuilderRpc()?.iframeReady(sdkBuildMetadata.data.BUILD_METADATA);
22
+ }
23
+ } catch {
24
+ }
25
+ }
8
26
  function useParentMessaging({
9
27
  applyOptimisticUpdate,
10
28
  markCommitted,
@@ -52,7 +70,9 @@ function useParentMessaging({
52
70
  onFormatCommand?.(command);
53
71
  }
54
72
  });
55
- getWebsiteBuilderRpc()?.iframeReady(void 0);
73
+ const rpc = getWebsiteBuilderRpc();
74
+ rpc?.iframeReady(BUILD_METADATA);
75
+ void announceSdkBuildMetadata();
56
76
  return () => {
57
77
  setSelectModeParentHandlers(null);
58
78
  };
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../select-mode/hooks/useParentMessaging.ts"],
4
- "sourcesContent": ["import type {\n DisplayMode,\n SelectModeFormatCommand,\n SelectModeOptimisticUpdatePayload,\n SelectModeSelectionType,\n} from \"schemas/generated/selectMode\";\n\nimport { useEffect, useMemo } from \"react\";\n\nimport { useIframeRpcChannel } from \"replo-utils/react/use-iframe-rpc-channel\";\n\nimport {\n getSelectModeIframeRpcChannel,\n getWebsiteBuilderRpc,\n setSelectModeParentHandlers,\n} from \"../iframeRpcChannel\";\n\n/**\n * Inbound side of the iframe<->parent selectMode bridge.\n *\n * Listens for Website Builder \u2192 canopy methods and applies them locally:\n * - `setDisplayMode`: switch the iframe between `browse` and `selection`.\n * - `setSelectionType`: declarative push of the parent's current\n * selection type. The interesting transition is `* \u2192 \"none\"`, which\n * tears down any in-iframe selection frame / in-progress drag \u2014 agent-ui\n * drives it from `agentUiState.selectionType` so panel dismiss / submit\n * flow through the same path as exiting selection mode entirely. The\n * regression test in `EditorSelection.spec.tsx` covers the case where\n * losing this used to lock the iframe out of further selections.\n *\n * Also fires `iframeReady` on mount so the parent knows it's safe to push\n * the initial `setDisplayMode` \u2014 without this handshake the parent would\n * race the iframe's mount and the first message could be lost.\n *\n * @author Kevin O'Connell 2026-04-17\n */\nexport function useParentMessaging({\n applyOptimisticUpdate,\n markCommitted,\n resolveSessionOverrides,\n onEnterEditMode,\n onExitEditMode,\n onFormatCommand,\n revertOverride,\n setDisplayMode,\n setSelectionType,\n}: {\n applyOptimisticUpdate?: (input: {\n editId: string;\n payload: SelectModeOptimisticUpdatePayload;\n targetSelector: string;\n targetMode: \"primary\" | \"group\";\n }) => void;\n markCommitted?: (editId: string, sessionId: string) => void;\n resolveSessionOverrides?: (\n sessionId: string,\n outcome: \"persisted\" | \"failed\",\n ) => void;\n onEnterEditMode?: () => void;\n onExitEditMode?: () => void;\n onFormatCommand?: (command: SelectModeFormatCommand) => void;\n revertOverride?: (editId: string) => void;\n setDisplayMode: (displayMode: DisplayMode) => void;\n setSelectionType: (selectionType: SelectModeSelectionType) => void;\n}) {\n useIframeRpcChannel(useMemo(() => getSelectModeIframeRpcChannel(), []));\n\n // eslint-disable-next-line replo/no-use-effect -- Legacy effect - may or may not be necessary\n useEffect(() => {\n setSelectModeParentHandlers({\n setDisplayMode: ({ displayMode }) => {\n setDisplayMode(displayMode);\n },\n setSelectionType: ({ selectionType }) => {\n setSelectionType(selectionType);\n },\n applyOptimisticUpdate: (message) => {\n applyOptimisticUpdate?.({\n editId: message.editId,\n payload: message.payload,\n targetSelector: message.targetSelector,\n targetMode: message.targetMode,\n });\n },\n markCommitted: ({ editId, sessionId }) => {\n markCommitted?.(editId, sessionId);\n },\n resolveSessionOverrides: ({ sessionId, outcome }) => {\n resolveSessionOverrides?.(sessionId, outcome);\n },\n revertOverride: ({ editId }) => {\n revertOverride?.(editId);\n },\n enterEditMode: () => {\n onEnterEditMode?.();\n },\n exitEditMode: () => {\n onExitEditMode?.();\n },\n formatCommand: ({ command }) => {\n onFormatCommand?.(command);\n },\n });\n getWebsiteBuilderRpc()?.iframeReady(undefined);\n\n return () => {\n setSelectModeParentHandlers(null);\n };\n }, [\n applyOptimisticUpdate,\n markCommitted,\n resolveSessionOverrides,\n onEnterEditMode,\n onExitEditMode,\n onFormatCommand,\n revertOverride,\n setDisplayMode,\n setSelectionType,\n ]);\n}\n"],
5
- "mappings": "AAOA,SAAS,WAAW,eAAe;AAEnC,SAAS,2BAA2B;AAEpC;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,OACK;AAqBA,SAAS,mBAAmB;AAAA,EACjC;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,GAkBG;AACD,sBAAoB,QAAQ,MAAM,8BAA8B,GAAG,CAAC,CAAC,CAAC;AAGtE,YAAU,MAAM;AACd,gCAA4B;AAAA,MAC1B,gBAAgB,CAAC,EAAE,YAAY,MAAM;AACnC,uBAAe,WAAW;AAAA,MAC5B;AAAA,MACA,kBAAkB,CAAC,EAAE,cAAc,MAAM;AACvC,yBAAiB,aAAa;AAAA,MAChC;AAAA,MACA,uBAAuB,CAAC,YAAY;AAClC,gCAAwB;AAAA,UACtB,QAAQ,QAAQ;AAAA,UAChB,SAAS,QAAQ;AAAA,UACjB,gBAAgB,QAAQ;AAAA,UACxB,YAAY,QAAQ;AAAA,QACtB,CAAC;AAAA,MACH;AAAA,MACA,eAAe,CAAC,EAAE,QAAQ,UAAU,MAAM;AACxC,wBAAgB,QAAQ,SAAS;AAAA,MACnC;AAAA,MACA,yBAAyB,CAAC,EAAE,WAAW,QAAQ,MAAM;AACnD,kCAA0B,WAAW,OAAO;AAAA,MAC9C;AAAA,MACA,gBAAgB,CAAC,EAAE,OAAO,MAAM;AAC9B,yBAAiB,MAAM;AAAA,MACzB;AAAA,MACA,eAAe,MAAM;AACnB,0BAAkB;AAAA,MACpB;AAAA,MACA,cAAc,MAAM;AAClB,yBAAiB;AAAA,MACnB;AAAA,MACA,eAAe,CAAC,EAAE,QAAQ,MAAM;AAC9B,0BAAkB,OAAO;AAAA,MAC3B;AAAA,IACF,CAAC;AACD,yBAAqB,GAAG,YAAY,MAAS;AAE7C,WAAO,MAAM;AACX,kCAA4B,IAAI;AAAA,IAClC;AAAA,EACF,GAAG;AAAA,IACD;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EACF,CAAC;AACH;",
4
+ "sourcesContent": ["import type {\n DisplayMode,\n SelectModeFormatCommand,\n SelectModeOptimisticUpdatePayload,\n SelectModeSelectionType,\n} from \"schemas/generated/selectMode\";\n\nimport { useEffect, useMemo } from \"react\";\n\nimport { useIframeRpcChannel } from \"replo-utils/react/use-iframe-rpc-channel\";\nimport { iframeRuntimeBuildMetadataSchema } from \"schemas/selectModeIframeRpc\";\nimport { z } from \"zod\";\n\nimport { BUILD_METADATA } from \"../../buildMetadata\";\nimport {\n getSelectModeIframeRpcChannel,\n getWebsiteBuilderRpc,\n setSelectModeParentHandlers,\n} from \"../iframeRpcChannel\";\n\nconst SDK_BUILD_METADATA_SPECIFIER = \"@replohq/sdk/lib/buildMetadata\";\n\nconst sdkBuildMetadataModuleSchema = z.object({\n BUILD_METADATA: iframeRuntimeBuildMetadataSchema,\n});\n\nasync function announceSdkBuildMetadata() {\n try {\n const sdkBuildMetadata = sdkBuildMetadataModuleSchema.safeParse(\n await import(SDK_BUILD_METADATA_SPECIFIER),\n );\n if (sdkBuildMetadata.success) {\n getWebsiteBuilderRpc()?.iframeReady(sdkBuildMetadata.data.BUILD_METADATA);\n }\n } catch {\n // Sites that haven't installed @replohq/sdk still handshake with sandbox-runtime alone.\n }\n}\n\n/**\n * Inbound side of the iframe<->parent selectMode bridge.\n *\n * Listens for Website Builder \u2192 canopy methods and applies them locally:\n * - `setDisplayMode`: switch the iframe between `browse` and `selection`.\n * - `setSelectionType`: declarative push of the parent's current\n * selection type. The interesting transition is `* \u2192 \"none\"`, which\n * tears down any in-iframe selection frame / in-progress drag \u2014 agent-ui\n * drives it from `agentUiState.selectionType` so panel dismiss / submit\n * flow through the same path as exiting selection mode entirely. The\n * regression test in `EditorSelection.spec.tsx` covers the case where\n * losing this used to lock the iframe out of further selections.\n *\n * Also fires `iframeReady` on mount so the parent knows it's safe to push\n * the initial `setDisplayMode` \u2014 without this handshake the parent would\n * race the iframe's mount and the first message could be lost.\n *\n * @author Kevin O'Connell 2026-04-17\n */\nexport function useParentMessaging({\n applyOptimisticUpdate,\n markCommitted,\n resolveSessionOverrides,\n onEnterEditMode,\n onExitEditMode,\n onFormatCommand,\n revertOverride,\n setDisplayMode,\n setSelectionType,\n}: {\n applyOptimisticUpdate?: (input: {\n editId: string;\n payload: SelectModeOptimisticUpdatePayload;\n targetSelector: string;\n targetMode: \"primary\" | \"group\";\n }) => void;\n markCommitted?: (editId: string, sessionId: string) => void;\n resolveSessionOverrides?: (\n sessionId: string,\n outcome: \"persisted\" | \"failed\",\n ) => void;\n onEnterEditMode?: () => void;\n onExitEditMode?: () => void;\n onFormatCommand?: (command: SelectModeFormatCommand) => void;\n revertOverride?: (editId: string) => void;\n setDisplayMode: (displayMode: DisplayMode) => void;\n setSelectionType: (selectionType: SelectModeSelectionType) => void;\n}) {\n useIframeRpcChannel(useMemo(() => getSelectModeIframeRpcChannel(), []));\n\n // eslint-disable-next-line replo/no-use-effect -- Legacy effect - may or may not be necessary\n useEffect(() => {\n setSelectModeParentHandlers({\n setDisplayMode: ({ displayMode }) => {\n setDisplayMode(displayMode);\n },\n setSelectionType: ({ selectionType }) => {\n setSelectionType(selectionType);\n },\n applyOptimisticUpdate: (message) => {\n applyOptimisticUpdate?.({\n editId: message.editId,\n payload: message.payload,\n targetSelector: message.targetSelector,\n targetMode: message.targetMode,\n });\n },\n markCommitted: ({ editId, sessionId }) => {\n markCommitted?.(editId, sessionId);\n },\n resolveSessionOverrides: ({ sessionId, outcome }) => {\n resolveSessionOverrides?.(sessionId, outcome);\n },\n revertOverride: ({ editId }) => {\n revertOverride?.(editId);\n },\n enterEditMode: () => {\n onEnterEditMode?.();\n },\n exitEditMode: () => {\n onExitEditMode?.();\n },\n formatCommand: ({ command }) => {\n onFormatCommand?.(command);\n },\n });\n const rpc = getWebsiteBuilderRpc();\n rpc?.iframeReady(BUILD_METADATA);\n void announceSdkBuildMetadata();\n\n return () => {\n setSelectModeParentHandlers(null);\n };\n }, [\n applyOptimisticUpdate,\n markCommitted,\n resolveSessionOverrides,\n onEnterEditMode,\n onExitEditMode,\n onFormatCommand,\n revertOverride,\n setDisplayMode,\n setSelectionType,\n ]);\n}\n"],
5
+ "mappings": "AAOA,SAAS,WAAW,eAAe;AAEnC,SAAS,2BAA2B;AACpC,SAAS,wCAAwC;AACjD,SAAS,SAAS;AAElB,SAAS,sBAAsB;AAC/B;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,OACK;AAEP,MAAM,+BAA+B;AAErC,MAAM,+BAA+B,EAAE,OAAO;AAAA,EAC5C,gBAAgB;AAClB,CAAC;AAED,eAAe,2BAA2B;AACxC,MAAI;AACF,UAAM,mBAAmB,6BAA6B;AAAA,MACpD,MAAM,OAAO;AAAA,IACf;AACA,QAAI,iBAAiB,SAAS;AAC5B,2BAAqB,GAAG,YAAY,iBAAiB,KAAK,cAAc;AAAA,IAC1E;AAAA,EACF,QAAQ;AAAA,EAER;AACF;AAqBO,SAAS,mBAAmB;AAAA,EACjC;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,GAkBG;AACD,sBAAoB,QAAQ,MAAM,8BAA8B,GAAG,CAAC,CAAC,CAAC;AAGtE,YAAU,MAAM;AACd,gCAA4B;AAAA,MAC1B,gBAAgB,CAAC,EAAE,YAAY,MAAM;AACnC,uBAAe,WAAW;AAAA,MAC5B;AAAA,MACA,kBAAkB,CAAC,EAAE,cAAc,MAAM;AACvC,yBAAiB,aAAa;AAAA,MAChC;AAAA,MACA,uBAAuB,CAAC,YAAY;AAClC,gCAAwB;AAAA,UACtB,QAAQ,QAAQ;AAAA,UAChB,SAAS,QAAQ;AAAA,UACjB,gBAAgB,QAAQ;AAAA,UACxB,YAAY,QAAQ;AAAA,QACtB,CAAC;AAAA,MACH;AAAA,MACA,eAAe,CAAC,EAAE,QAAQ,UAAU,MAAM;AACxC,wBAAgB,QAAQ,SAAS;AAAA,MACnC;AAAA,MACA,yBAAyB,CAAC,EAAE,WAAW,QAAQ,MAAM;AACnD,kCAA0B,WAAW,OAAO;AAAA,MAC9C;AAAA,MACA,gBAAgB,CAAC,EAAE,OAAO,MAAM;AAC9B,yBAAiB,MAAM;AAAA,MACzB;AAAA,MACA,eAAe,MAAM;AACnB,0BAAkB;AAAA,MACpB;AAAA,MACA,cAAc,MAAM;AAClB,yBAAiB;AAAA,MACnB;AAAA,MACA,eAAe,CAAC,EAAE,QAAQ,MAAM;AAC9B,0BAAkB,OAAO;AAAA,MAC3B;AAAA,IACF,CAAC;AACD,UAAM,MAAM,qBAAqB;AACjC,SAAK,YAAY,cAAc;AAC/B,SAAK,yBAAyB;AAE9B,WAAO,MAAM;AACX,kCAA4B,IAAI;AAAA,IAClC;AAAA,EACF,GAAG;AAAA,IACD;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EACF,CAAC;AACH;",
6
6
  "names": []
7
7
  }
@@ -3,6 +3,7 @@ import { displayModeStore, useDisplayMode } from "../displayModeStore";
3
3
  import { getWebsiteBuilderRpc } from "../iframeRpcChannel";
4
4
  import { hideSelectionFrame } from "../selectionFrame";
5
5
  import { IDLE_SESSION } from "../selectModeSession";
6
+ import { resetHoverRectDedup } from "./useSelectionHover";
6
7
  function resetInteractionSession({
7
8
  interactionSessionRef
8
9
  }) {
@@ -14,6 +15,7 @@ function resetInteractionSession({
14
15
  });
15
16
  }
16
17
  hideSelectionFrame();
18
+ resetHoverRectDedup();
17
19
  interactionSessionRef.current = IDLE_SESSION;
18
20
  }
19
21
  function useSelectModeController() {
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../select-mode/hooks/useSelectModeController.ts"],
4
- "sourcesContent": ["import type {\n DisplayMode,\n SelectModeSelectionType,\n} from \"schemas/generated/selectMode\";\nimport type { SelectModeInteractionSession } from \"../selectModeSession\";\n\nimport { useCallback, useRef } from \"react\";\n\nimport { displayModeStore, useDisplayMode } from \"../displayModeStore\";\nimport { getWebsiteBuilderRpc } from \"../iframeRpcChannel\";\nimport { hideSelectionFrame } from \"../selectionFrame\";\nimport { IDLE_SESSION } from \"../selectModeSession\";\n\n/**\n * Tear down any in-progress drag/selection. Re-used by both the\n * display-mode flip-out path (defensive: belt-and-braces with the\n * parent's `setSelectionType(\"none\")` push) and by the explicit\n * `setSelectionType(\"none\")` path below.\n */\nfunction resetInteractionSession({\n interactionSessionRef,\n}: {\n interactionSessionRef: React.MutableRefObject<SelectModeInteractionSession>;\n}) {\n const currentSession = interactionSessionRef.current;\n if (currentSession.type === \"drag\" && currentSession.hasExceededThreshold) {\n getWebsiteBuilderRpc()?.dragRectUpdated({\n dragRect: null,\n selectionRects: [],\n });\n }\n hideSelectionFrame();\n interactionSessionRef.current = IDLE_SESSION;\n}\n\nexport function useSelectModeController() {\n // Display mode lives in a module singleton so the sibling `LinkInterceptor`\n // can read it (and stand down during selection) without a shared provider.\n const displayMode = useDisplayMode();\n const interactionSessionRef =\n useRef<SelectModeInteractionSession>(IDLE_SESSION);\n\n // The parent is authoritative for \"is there a current selection?\". Canopy\n // originates selections via pointer events, posts them up, agent-ui\n // mirrors `selection.type` into `agentUiState`, and the result is pushed\n // back down here. When the parent says `none` (panel dismissed / submit /\n // exit selection mode) we tear down the in-iframe frame; the matching\n // commit-side paths are in `useSelectionInteractions`.\n const setSelectionType = useCallback(\n (selectionType: SelectModeSelectionType) => {\n if (selectionType === \"none\") {\n resetInteractionSession({ interactionSessionRef });\n }\n },\n [],\n );\n\n const setDisplayMode = useCallback((nextDisplayMode: DisplayMode) => {\n if (\n displayModeStore.getSnapshot() === \"selection\" &&\n nextDisplayMode !== \"selection\"\n ) {\n resetInteractionSession({ interactionSessionRef });\n }\n\n displayModeStore.setDisplayMode(nextDisplayMode);\n }, []);\n\n return {\n displayMode,\n interactionSessionRef,\n setDisplayMode,\n setSelectionType,\n };\n}\n"],
5
- "mappings": "AAMA,SAAS,aAAa,cAAc;AAEpC,SAAS,kBAAkB,sBAAsB;AACjD,SAAS,4BAA4B;AACrC,SAAS,0BAA0B;AACnC,SAAS,oBAAoB;AAQ7B,SAAS,wBAAwB;AAAA,EAC/B;AACF,GAEG;AACD,QAAM,iBAAiB,sBAAsB;AAC7C,MAAI,eAAe,SAAS,UAAU,eAAe,sBAAsB;AACzE,yBAAqB,GAAG,gBAAgB;AAAA,MACtC,UAAU;AAAA,MACV,gBAAgB,CAAC;AAAA,IACnB,CAAC;AAAA,EACH;AACA,qBAAmB;AACnB,wBAAsB,UAAU;AAClC;AAEO,SAAS,0BAA0B;AAGxC,QAAM,cAAc,eAAe;AACnC,QAAM,wBACJ,OAAqC,YAAY;AAQnD,QAAM,mBAAmB;AAAA,IACvB,CAAC,kBAA2C;AAC1C,UAAI,kBAAkB,QAAQ;AAC5B,gCAAwB,EAAE,sBAAsB,CAAC;AAAA,MACnD;AAAA,IACF;AAAA,IACA,CAAC;AAAA,EACH;AAEA,QAAM,iBAAiB,YAAY,CAAC,oBAAiC;AACnE,QACE,iBAAiB,YAAY,MAAM,eACnC,oBAAoB,aACpB;AACA,8BAAwB,EAAE,sBAAsB,CAAC;AAAA,IACnD;AAEA,qBAAiB,eAAe,eAAe;AAAA,EACjD,GAAG,CAAC,CAAC;AAEL,SAAO;AAAA,IACL;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EACF;AACF;",
4
+ "sourcesContent": ["import type {\n DisplayMode,\n SelectModeSelectionType,\n} from \"schemas/generated/selectMode\";\nimport type { SelectModeInteractionSession } from \"../selectModeSession\";\n\nimport { useCallback, useRef } from \"react\";\n\nimport { displayModeStore, useDisplayMode } from \"../displayModeStore\";\nimport { getWebsiteBuilderRpc } from \"../iframeRpcChannel\";\nimport { hideSelectionFrame } from \"../selectionFrame\";\nimport { IDLE_SESSION } from \"../selectModeSession\";\nimport { resetHoverRectDedup } from \"./useSelectionHover\";\n\n/**\n * Tear down any in-progress drag/selection. Re-used by both the\n * display-mode flip-out path (defensive: belt-and-braces with the\n * parent's `setSelectionType(\"none\")` push) and by the explicit\n * `setSelectionType(\"none\")` path below.\n */\nfunction resetInteractionSession({\n interactionSessionRef,\n}: {\n interactionSessionRef: React.MutableRefObject<SelectModeInteractionSession>;\n}) {\n const currentSession = interactionSessionRef.current;\n if (currentSession.type === \"drag\" && currentSession.hasExceededThreshold) {\n getWebsiteBuilderRpc()?.dragRectUpdated({\n dragRect: null,\n selectionRects: [],\n });\n }\n hideSelectionFrame();\n // The parent wiped its hover overlay as part of this clear; the hover\n // hook's rect dedup must forget its last post or an identical rect would\n // never re-render the overlay.\n resetHoverRectDedup();\n interactionSessionRef.current = IDLE_SESSION;\n}\n\nexport function useSelectModeController() {\n // Display mode lives in a module singleton so the sibling `LinkInterceptor`\n // can read it (and stand down during selection) without a shared provider.\n const displayMode = useDisplayMode();\n const interactionSessionRef =\n useRef<SelectModeInteractionSession>(IDLE_SESSION);\n\n // The parent is authoritative for \"is there a current selection?\". Canopy\n // originates selections via pointer events, posts them up, agent-ui\n // mirrors `selection.type` into `agentUiState`, and the result is pushed\n // back down here. When the parent says `none` (panel dismissed / submit /\n // exit selection mode) we tear down the in-iframe frame; the matching\n // commit-side paths are in `useSelectionInteractions`.\n const setSelectionType = useCallback(\n (selectionType: SelectModeSelectionType) => {\n if (selectionType === \"none\") {\n resetInteractionSession({ interactionSessionRef });\n }\n },\n [],\n );\n\n const setDisplayMode = useCallback((nextDisplayMode: DisplayMode) => {\n if (\n displayModeStore.getSnapshot() === \"selection\" &&\n nextDisplayMode !== \"selection\"\n ) {\n resetInteractionSession({ interactionSessionRef });\n }\n\n displayModeStore.setDisplayMode(nextDisplayMode);\n }, []);\n\n return {\n displayMode,\n interactionSessionRef,\n setDisplayMode,\n setSelectionType,\n };\n}\n"],
5
+ "mappings": "AAMA,SAAS,aAAa,cAAc;AAEpC,SAAS,kBAAkB,sBAAsB;AACjD,SAAS,4BAA4B;AACrC,SAAS,0BAA0B;AACnC,SAAS,oBAAoB;AAC7B,SAAS,2BAA2B;AAQpC,SAAS,wBAAwB;AAAA,EAC/B;AACF,GAEG;AACD,QAAM,iBAAiB,sBAAsB;AAC7C,MAAI,eAAe,SAAS,UAAU,eAAe,sBAAsB;AACzE,yBAAqB,GAAG,gBAAgB;AAAA,MACtC,UAAU;AAAA,MACV,gBAAgB,CAAC;AAAA,IACnB,CAAC;AAAA,EACH;AACA,qBAAmB;AAInB,sBAAoB;AACpB,wBAAsB,UAAU;AAClC;AAEO,SAAS,0BAA0B;AAGxC,QAAM,cAAc,eAAe;AACnC,QAAM,wBACJ,OAAqC,YAAY;AAQnD,QAAM,mBAAmB;AAAA,IACvB,CAAC,kBAA2C;AAC1C,UAAI,kBAAkB,QAAQ;AAC5B,gCAAwB,EAAE,sBAAsB,CAAC;AAAA,MACnD;AAAA,IACF;AAAA,IACA,CAAC;AAAA,EACH;AAEA,QAAM,iBAAiB,YAAY,CAAC,oBAAiC;AACnE,QACE,iBAAiB,YAAY,MAAM,eACnC,oBAAoB,aACpB;AACA,8BAAwB,EAAE,sBAAsB,CAAC;AAAA,IACnD;AAEA,qBAAiB,eAAe,eAAe;AAAA,EACjD,GAAG,CAAC,CAAC;AAEL,SAAO;AAAA,IACL;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EACF;AACF;",
6
6
  "names": []
7
7
  }
@@ -1,6 +1,7 @@
1
1
  import type { MutableRefObject } from "react";
2
2
  import type { DisplayMode } from "../../_vendor/schemas/generated/selectMode";
3
3
  import type { SelectModeInteractionSession } from "../selectModeSession";
4
+ export declare function resetHoverRectDedup(): void;
4
5
  /**
5
6
  * Tracks whichever element is under the user's pointer while in `selection`
6
7
  * mode and reports its bounding rect to the parent so the parent can render
@@ -9,12 +10,10 @@ import type { SelectModeInteractionSession } from "../selectModeSession";
9
10
  * Two behaviors worth knowing about:
10
11
  * - During an active drag we suppress hover updates; the drag overlay is
11
12
  * the only highlight we want visible.
12
- * - On scroll/wheel/resize we re-run the hover lookup from the last known
13
- * pointer position (we use rAF to coalesce). Without this, scrolling
14
- * over an already-hovered element wouldn't update the highlight to the
15
- * new element actually under the cursor.
13
+ * - Unchanged rectangles do not cross the iframe bridge. Viewport updates
14
+ * are coalesced because scroll and wheel can fire in the same frame.
16
15
  *
17
- * @author Kevin O'Connell 2026-04-17
16
+ * @author Noah Gilmore 2026-07-26
18
17
  */
19
18
  export declare function useSelectionHover({ displayMode, interactionSessionRef, }: {
20
19
  displayMode: DisplayMode;
@@ -2,6 +2,10 @@ import { useEffect } from "react";
2
2
  import { getNearestSelectableElement } from "../descriptor/elementDescriptor";
3
3
  import { getWebsiteBuilderRpc } from "../iframeRpcChannel";
4
4
  import { getElementBoundingRect } from "../interactions/selectionGeometry";
5
+ let activeHoverDedupReset = null;
6
+ function resetHoverRectDedup() {
7
+ activeHoverDedupReset?.();
8
+ }
5
9
  function useSelectionHover({
6
10
  displayMode,
7
11
  interactionSessionRef
@@ -12,10 +16,18 @@ function useSelectionHover({
12
16
  }
13
17
  let hoverSyncAnimationFrameId = null;
14
18
  let lastPointerClientPosition = null;
19
+ let lastPostedRect;
20
+ activeHoverDedupReset = () => {
21
+ lastPostedRect = void 0;
22
+ };
15
23
  const postHoveredElement = (element) => {
16
- getWebsiteBuilderRpc()?.hoverElement({
17
- rect: element ? getElementBoundingRect(element) : null
18
- });
24
+ const rect = element ? getElementBoundingRect(element) : null;
25
+ const isUnchanged = lastPostedRect !== void 0 && (rect === lastPostedRect || rect !== null && lastPostedRect !== null && rect.x === lastPostedRect.x && rect.y === lastPostedRect.y && rect.width === lastPostedRect.width && rect.height === lastPostedRect.height);
26
+ if (isUnchanged) {
27
+ return;
28
+ }
29
+ lastPostedRect = rect;
30
+ getWebsiteBuilderRpc()?.hoverElement({ rect });
19
31
  };
20
32
  const syncCurrentHover = (pointerClientPosition = lastPointerClientPosition) => {
21
33
  let hoveredElement = null;
@@ -74,6 +86,10 @@ function useSelectionHover({
74
86
  return;
75
87
  }
76
88
  lastPointerClientPosition = null;
89
+ if (hoverSyncAnimationFrameId !== null) {
90
+ cancelAnimationFrame(hoverSyncAnimationFrameId);
91
+ hoverSyncAnimationFrameId = null;
92
+ }
77
93
  postHoveredElement(null);
78
94
  };
79
95
  const handleViewportChange = () => {
@@ -90,6 +106,7 @@ function useSelectionHover({
90
106
  });
91
107
  window.addEventListener("resize", handleViewportChange);
92
108
  return () => {
109
+ activeHoverDedupReset = null;
93
110
  if (hoverSyncAnimationFrameId !== null) {
94
111
  cancelAnimationFrame(hoverSyncAnimationFrameId);
95
112
  }
@@ -105,6 +122,7 @@ function useSelectionHover({
105
122
  }, [displayMode, interactionSessionRef]);
106
123
  }
107
124
  export {
125
+ resetHoverRectDedup,
108
126
  useSelectionHover
109
127
  };
110
128
  //# sourceMappingURL=useSelectionHover.js.map