oomi-ai 0.2.40 → 0.2.41

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.
@@ -39,7 +39,9 @@ const LEGACY_WEBSPATIAL_TEMPLATE_FILE_RULES = [
39
39
  relativePath: path.join('src', 'spatial.ts'),
40
40
  shouldReplace: (content) =>
41
41
  !content.includes('WEBSPATIAL_FORK_REPOSITORY') ||
42
- !content.includes('configurePersonaScene'),
42
+ !content.includes('configurePersonaScene') ||
43
+ content.includes('WEBSPATIAL_FORK_COMMIT = "b2746721e4fe6b4f86dac0ea55938074eea00cda"') ||
44
+ content.includes('WEBSPATIAL_FORK_COMMIT = "8904ac8fec48fe49ee14d1739237bd1afb2894fe"'),
43
45
  },
44
46
  {
45
47
  relativePath: path.join('src', 'main.tsx'),
@@ -57,21 +59,44 @@ const LEGACY_WEBSPATIAL_TEMPLATE_FILE_RULES = [
57
59
  {
58
60
  relativePath: path.join('src', 'pages', 'HomePage.tsx'),
59
61
  shouldReplace: (content) =>
60
- content.includes('Open Spatial Scene') &&
61
- content.includes('Open Scene Route'),
62
+ (content.includes('Open Spatial Scene') &&
63
+ content.includes('Open Scene Route')) ||
64
+ (content.includes('Launch Spatial Surface') &&
65
+ content.includes('Open Browser Preview') &&
66
+ content.includes('Focused surface') &&
67
+ !content.includes('sceneMode')) ||
68
+ content.includes('enable-xr-monitor={sceneMode || undefined}') ||
69
+ content.includes('enable-xr={sceneMode || undefined}') ||
70
+ (
71
+ content.includes('persona-panel persona-runtime" enable-xr') ||
72
+ content.includes('persona-button" onClick={openPersonaScene} enable-xr') ||
73
+ content.includes('persona-link" to="/scene" target="_blank" enable-xr') ||
74
+ content.includes('persona-card" enable-xr style={xrStyle(')
75
+ ),
62
76
  },
63
77
  {
64
78
  relativePath: path.join('src', 'pages', 'ScenePage.tsx'),
65
79
  shouldReplace: (content) =>
66
- !content.includes('enable-xr-monitor') &&
67
- content.includes(
68
- 'This route is intentionally separate so WebSpatial scene launching has a dedicated',
80
+ (
81
+ !content.includes('enable-xr-monitor') &&
82
+ content.includes(
83
+ 'This route is intentionally separate so WebSpatial scene launching has a dedicated',
84
+ )
85
+ ) ||
86
+ (
87
+ content.includes('Awaiting AndroidXR interaction') &&
88
+ content.includes('Interaction Console') &&
89
+ content.includes('Fork-backed proof points')
69
90
  ),
70
91
  },
71
92
  {
72
93
  relativePath: path.join('src', 'App.css'),
73
94
  shouldReplace: (content) =>
74
- content.includes('.scene-panel') && !content.includes('.scene-interaction-grid'),
95
+ (content.includes('.scene-panel') && !content.includes('.scene-interaction-grid')) ||
96
+ content.includes('html.is-spatial .persona-runtime {') ||
97
+ content.includes('html.is-spatial .persona-scene-root {') ||
98
+ content.includes('html.is-spatial .persona-button,') ||
99
+ content.includes('html.is-spatial .persona-card,'),
75
100
  },
76
101
  {
77
102
  relativePath: 'vite.config.ts',
@@ -2,7 +2,7 @@
2
2
  "id": "oomi-ai",
3
3
  "name": "Oomi Channel Plugin",
4
4
  "description": "Managed Oomi channel integration for OpenClaw.",
5
- "version": "0.2.40",
5
+ "version": "0.2.41",
6
6
  "author": "Oomi",
7
7
  "license": "MIT",
8
8
  "openclawVersion": ">=0.5.0",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "oomi-ai",
3
- "version": "0.2.40",
3
+ "version": "0.2.41",
4
4
  "description": "Oomi OpenClaw channel plugin and bridge tooling",
5
5
  "bin": {
6
6
  "oomi": "bin/oomi-ai.js"
@@ -105,6 +105,10 @@ code {
105
105
  gap: 24px;
106
106
  }
107
107
 
108
+ .persona-scene-root {
109
+ position: relative;
110
+ }
111
+
108
112
  .persona-header,
109
113
  .persona-grid,
110
114
  .scene-interaction-grid,
@@ -594,19 +598,11 @@ html.is-spatial body {
594
598
  background: transparent;
595
599
  }
596
600
 
597
- html.is-spatial .persona-button,
598
- html.is-spatial .persona-link,
599
601
  html.is-spatial .scene-chip,
600
602
  html.is-spatial .scene-material-chip {
601
603
  --xr-background-material: regular;
602
604
  }
603
605
 
604
- html.is-spatial .persona-runtime {
605
- transform: translateZ(20px) rotateX(10deg);
606
- transform-origin: top right;
607
- }
608
-
609
- html.is-spatial .persona-card,
610
606
  html.is-spatial .scene-ops-card,
611
607
  html.is-spatial .scene-interaction-card,
612
608
  html.is-spatial .scene-proof-card,
@@ -47,9 +47,9 @@ export function HomePage({ sceneMode = false }: HomePageProps) {
47
47
  }, []);
48
48
 
49
49
  return (
50
- <main className="persona-shell" enable-xr-monitor={sceneMode || undefined}>
50
+ <main className={`persona-shell${sceneMode ? " persona-scene-root" : ""}`}>
51
51
  <section className="persona-header">
52
- <div className="persona-panel persona-hero" enable-xr style={xrStyle(22, "translucent")}>
52
+ <div className="persona-panel persona-hero">
53
53
  <div>
54
54
  <p className="persona-eyebrow">{sceneMode ? "Spatial Persona Surface" : "WebSpatial Persona"}</p>
55
55
  <h1 className="persona-title">{personaConfig.name}</h1>
@@ -59,15 +59,15 @@ export function HomePage({ sceneMode = false }: HomePageProps) {
59
59
  <div className="persona-actions">
60
60
  {!sceneMode ? (
61
61
  <>
62
- <button className="persona-button" onClick={openPersonaScene} enable-xr style={xrStyle(52, "regular")}>
62
+ <button className="persona-button" onClick={openPersonaScene}>
63
63
  Launch Spatial Surface
64
64
  </button>
65
- <Link className="persona-link" to="/scene" target="_blank" enable-xr style={xrStyle(92, "thin")}>
65
+ <Link className="persona-link" to="/scene" target="_blank">
66
66
  Open Spatial Preview
67
67
  </Link>
68
68
  </>
69
69
  ) : (
70
- <Link className="persona-button" to="/" enable-xr style={xrStyle(52, "regular")}>
70
+ <Link className="persona-button" to="/">
71
71
  Return To Browser View
72
72
  </Link>
73
73
  )}
@@ -89,7 +89,7 @@ export function HomePage({ sceneMode = false }: HomePageProps) {
89
89
  </div>
90
90
  </div>
91
91
 
92
- <aside className="persona-panel persona-runtime" enable-xr style={xrStyle(72, "thin")}>
92
+ <aside className="persona-panel persona-runtime">
93
93
  <p className="persona-eyebrow">Runtime</p>
94
94
  <div className="runtime-list">
95
95
  <div className="runtime-row">
@@ -117,7 +117,7 @@ export function HomePage({ sceneMode = false }: HomePageProps) {
117
117
  </section>
118
118
 
119
119
  <section className="persona-grid">
120
- <article className="persona-panel persona-card" enable-xr style={xrStyle(34, "translucent")}>
120
+ <article className="persona-panel persona-card">
121
121
  <h2>What Good XR Feels Like</h2>
122
122
  <p>
123
123
  Treat the generated persona site like a spatial work surface. The XR route should keep
@@ -131,7 +131,7 @@ export function HomePage({ sceneMode = false }: HomePageProps) {
131
131
  </p>
132
132
  </article>
133
133
 
134
- <article className="persona-panel persona-card" enable-xr style={xrStyle(74, "thin")}>
134
+ <article className="persona-panel persona-card">
135
135
  <h2>Editing Notes</h2>
136
136
  <ul className="persona-note-list">
137
137
  {personaNotes.map(note => (
@@ -140,7 +140,7 @@ export function HomePage({ sceneMode = false }: HomePageProps) {
140
140
  </ul>
141
141
  </article>
142
142
 
143
- <article className="persona-panel persona-card persona-scene-card" enable-xr style={xrStyle(118, "regular")}>
143
+ <article className="persona-panel persona-card persona-scene-card">
144
144
  <h2>Scene Contract</h2>
145
145
  <p>
146
146
  The page should import WebSpatial helpers, self-configure the scene, and keep using
@@ -3,7 +3,7 @@ import { initScene, version as webSpatialSdkVersion } from "@webspatial/react-sd
3
3
 
4
4
  export const PERSONA_SCENE_NAME = "personaScene";
5
5
  export const WEBSPATIAL_FORK_REPOSITORY = "https://github.com/zill4/webspatial-sdk";
6
- export const WEBSPATIAL_FORK_COMMIT = "b2746721e4fe6b4f86dac0ea55938074eea00cda";
6
+ export const WEBSPATIAL_FORK_COMMIT = "ac4bd47eb14a894ffef34a4044ddd0bbd47f3e72";
7
7
 
8
8
  type SpatialWindow = Window & {
9
9
  webspatialBridge?: unknown;
@@ -1,6 +1,6 @@
1
1
  This persona template vendors the AndroidXR-capable WebSpatial runtime packages from:
2
2
 
3
3
  - Repository: https://github.com/zill4/webspatial-sdk
4
- - Commit: 8904ac8fec48fe49ee14d1739237bd1afb2894fe
4
+ - Commit: ac4bd47eb14a894ffef34a4044ddd0bbd47f3e72
5
5
 
6
6
  Oomi uses these vendored packages for persona runtimes so OpenClaw installs the AndroidXR-enabled fork instead of the stock npm release.
@@ -1014,17 +1014,36 @@ function hasTransparentBackground(element) {
1014
1014
  }
1015
1015
  function injectCaptureBackground(element, backgroundColor = DEFAULT_CAPTURE_BACKGROUND) {
1016
1016
  const restoreFunctions = [];
1017
- const originalBg = element.style.backgroundColor;
1018
1017
  const wasTransparent = hasTransparentBackground(element);
1019
- element.style.backgroundColor = backgroundColor;
1020
- restoreFunctions.push(() => {
1021
- element.style.backgroundColor = originalBg;
1022
- });
1018
+ if (wasTransparent) {
1019
+ const originalBg = element.style.backgroundColor;
1020
+ element.style.backgroundColor = backgroundColor;
1021
+ restoreFunctions.push(() => {
1022
+ element.style.backgroundColor = originalBg;
1023
+ });
1024
+ }
1025
+ const shouldInjectDescendantBackground = (candidate) => {
1026
+ if (!hasTransparentBackground(candidate)) {
1027
+ return false;
1028
+ }
1029
+ const style = window.getComputedStyle(candidate);
1030
+ if (style.display === "inline" || style.display === "contents") {
1031
+ return false;
1032
+ }
1033
+ const rect = candidate.getBoundingClientRect();
1034
+ const hasMeaningfulBox = rect.width >= 32 && rect.height >= 32;
1035
+ if (!hasMeaningfulBox) {
1036
+ return false;
1037
+ }
1038
+ const hasNestedLayout = candidate.children.length > 0;
1039
+ const hasVisualContainerTraits = style.borderRadius !== "0px" || style.boxShadow !== "none" || style.backdropFilter !== "none" || style.overflow !== "visible" || style.borderStyle !== "none";
1040
+ return hasNestedLayout || hasVisualContainerTraits;
1041
+ };
1023
1042
  const allDescendants = element.querySelectorAll("*");
1024
1043
  let injectedCount = 0;
1025
1044
  allDescendants.forEach((el) => {
1026
1045
  const htmlEl = el;
1027
- if (hasTransparentBackground(htmlEl)) {
1046
+ if (shouldInjectDescendantBackground(htmlEl)) {
1028
1047
  const childOriginalBg = htmlEl.style.backgroundColor;
1029
1048
  htmlEl.style.backgroundColor = backgroundColor;
1030
1049
  injectedCount++;
@@ -1133,121 +1152,51 @@ async function captureWithSnapdom(snapdom, element, scale) {
1133
1152
  return null;
1134
1153
  }
1135
1154
  }
1136
- function findOffsetElements(element) {
1137
- const offsetElements = [];
1138
- const style = window.getComputedStyle(element);
1139
- if (style.position === "relative") {
1140
- const top = parseFloat(style.top) || 0;
1141
- const left = parseFloat(style.left) || 0;
1142
- if (top !== 0 || left !== 0) {
1143
- offsetElements.push({
1144
- element,
1145
- originalTop: element.style.top,
1146
- originalLeft: element.style.left,
1147
- topValue: top,
1148
- leftValue: left
1149
- });
1150
- }
1151
- }
1152
- let parent = element.parentElement;
1153
- let depth = 0;
1154
- const maxDepth = 5;
1155
- while (parent && depth < maxDepth) {
1156
- const parentStyle = window.getComputedStyle(parent);
1157
- if (parentStyle.position === "relative") {
1158
- const parentTop = parseFloat(parentStyle.top) || 0;
1159
- const parentLeft = parseFloat(parentStyle.left) || 0;
1160
- if (parentTop !== 0 || parentLeft !== 0) {
1161
- console.log(
1162
- `[WebSpatial] Found offset element: ${parent.tagName}.${parent.className?.split(" ")[0] || ""} top=${parentTop}px, left=${parentLeft}px`
1163
- );
1164
- offsetElements.push({
1165
- element: parent,
1166
- originalTop: parent.style.top,
1167
- originalLeft: parent.style.left,
1168
- topValue: parentTop,
1169
- leftValue: parentLeft
1170
- });
1155
+ function createVisibleCaptureClone(element) {
1156
+ const rect = element.getBoundingClientRect();
1157
+ const sandbox = document.createElement("div");
1158
+ sandbox.setAttribute("aria-hidden", "true");
1159
+ sandbox.style.position = "fixed";
1160
+ sandbox.style.left = "-10000px";
1161
+ sandbox.style.top = "0px";
1162
+ sandbox.style.pointerEvents = "none";
1163
+ sandbox.style.zIndex = "-1";
1164
+ sandbox.style.contain = "layout style paint";
1165
+ sandbox.style.opacity = "1";
1166
+ const clone = element.cloneNode(true);
1167
+ const makeCloneVisible = (node) => {
1168
+ node.style.visibility = "visible";
1169
+ node.style.opacity = "1";
1170
+ node.style.transition = "none";
1171
+ node.style.animation = "none";
1172
+ node.style.transform = "none";
1173
+ node.style.top = "0px";
1174
+ node.style.left = "0px";
1175
+ Array.from(node.children).forEach((child) => {
1176
+ if (child instanceof HTMLElement) {
1177
+ makeCloneVisible(child);
1171
1178
  }
1172
- }
1173
- parent = parent.parentElement;
1174
- depth++;
1175
- }
1176
- return offsetElements;
1177
- }
1178
- function getContentOffset(element) {
1179
- const offsetElements = findOffsetElements(element);
1180
- return {
1181
- top: offsetElements.reduce((sum, el) => sum + el.topValue, 0),
1182
- left: offsetElements.reduce((sum, el) => sum + el.leftValue, 0)
1183
- };
1184
- }
1185
- function makeElementVisible(element) {
1186
- const restoreFunctions = [];
1187
- const originalVisibility = element.style.visibility;
1188
- const computedVisibility = window.getComputedStyle(element).visibility;
1189
- if (computedVisibility === "hidden") {
1190
- element.style.visibility = "visible";
1191
- console.log(`[WebSpatial] Made element visible for capture (was: ${computedVisibility})`);
1192
- restoreFunctions.push(() => {
1193
- element.style.visibility = originalVisibility;
1194
1179
  });
1195
- }
1196
- const allDescendants = element.querySelectorAll("*");
1197
- allDescendants.forEach((el) => {
1198
- const htmlEl = el;
1199
- const childComputedVisibility = window.getComputedStyle(htmlEl).visibility;
1200
- if (childComputedVisibility === "hidden") {
1201
- const childOriginalVisibility = htmlEl.style.visibility;
1202
- htmlEl.style.visibility = "visible";
1203
- restoreFunctions.push(() => {
1204
- htmlEl.style.visibility = childOriginalVisibility;
1205
- });
1206
- }
1207
- });
1208
- if (restoreFunctions.length > 1) {
1209
- console.log(`[WebSpatial] Made ${restoreFunctions.length} elements visible for capture`);
1210
- }
1211
- return () => {
1212
- restoreFunctions.forEach((restore) => restore());
1180
+ };
1181
+ makeCloneVisible(clone);
1182
+ clone.style.position = "relative";
1183
+ clone.style.margin = "0px";
1184
+ clone.style.width = `${Math.ceil(rect.width)}px`;
1185
+ clone.style.minHeight = `${Math.ceil(rect.height)}px`;
1186
+ sandbox.appendChild(clone);
1187
+ document.body.appendChild(sandbox);
1188
+ return {
1189
+ clone,
1190
+ cleanup: () => sandbox.remove()
1213
1191
  };
1214
1192
  }
1215
1193
  async function captureWithHtml2Canvas(html2canvas, element, scale, backgroundColor) {
1216
1194
  try {
1217
1195
  const rect = element.getBoundingClientRect();
1218
- const contentOffset = getContentOffset(element);
1219
- console.log(`[WebSpatial] html2canvas capturing: rect=(${rect.x.toFixed(0)},${rect.y.toFixed(0)},${rect.width.toFixed(0)},${rect.height.toFixed(0)}), contentOffset=(${contentOffset.top},${contentOffset.left})`);
1220
- const scrollX = window.scrollX || window.pageXOffset || 0;
1196
+ console.log(
1197
+ `[WebSpatial] html2canvas capturing via visible clone: rect=(${rect.x.toFixed(0)},${rect.y.toFixed(0)},${rect.width.toFixed(0)},${rect.height.toFixed(0)})`
1198
+ );
1221
1199
  const scrollY = window.scrollY || window.pageYOffset || 0;
1222
- const absoluteX = rect.x + scrollX;
1223
- const absoluteY = rect.y + scrollY;
1224
- const offsetElements = findOffsetElements(element);
1225
- if (offsetElements.length > 0) {
1226
- console.log(
1227
- `[WebSpatial] Temporarily resetting ${offsetElements.length} offset elements for capture`
1228
- );
1229
- offsetElements.forEach(({ element: el, topValue, leftValue }) => {
1230
- el.style.top = "0px";
1231
- el.style.left = "0px";
1232
- console.log(
1233
- `[WebSpatial] Reset: ${el.tagName}.${el.className?.split(" ")[0] || ""} from top=${topValue}px, left=${leftValue}px to 0`
1234
- );
1235
- });
1236
- void element.offsetHeight;
1237
- await new Promise((resolve) => setTimeout(resolve, 50));
1238
- console.log("[WebSpatial] Reflow complete after offset reset");
1239
- }
1240
- const restorePositions = () => {
1241
- offsetElements.forEach(({ element: el, originalTop, originalLeft }) => {
1242
- el.style.top = originalTop;
1243
- el.style.left = originalLeft;
1244
- });
1245
- };
1246
- if (element.scrollTop !== 0) {
1247
- console.log(`[WebSpatial] Resetting element scroll from ${element.scrollTop} to 0`);
1248
- element.scrollTop = 0;
1249
- }
1250
- const restoreVisibility = makeElementVisible(element);
1251
1200
  const viewportTop = scrollY;
1252
1201
  const viewportBottom = scrollY + window.innerHeight;
1253
1202
  const elementTop = rect.y + scrollY;
@@ -1256,14 +1205,20 @@ async function captureWithHtml2Canvas(html2canvas, element, scale, backgroundCol
1256
1205
  `[WebSpatial] Capture context: viewport=(${viewportTop}-${viewportBottom}), element=(${elementTop}-${elementBottom}), innerHeight=${window.innerHeight}`
1257
1206
  );
1258
1207
  let canvas;
1208
+ const captureClone = createVisibleCaptureClone(element);
1209
+ const restoreBackground = injectCaptureBackground(
1210
+ captureClone.clone,
1211
+ backgroundColor || DEFAULT_CAPTURE_BACKGROUND
1212
+ );
1259
1213
  try {
1260
- const currentRect = element.getBoundingClientRect();
1261
- const captureX = currentRect.x + scrollX;
1262
- const captureY = currentRect.y + scrollY;
1214
+ await new Promise(
1215
+ (resolve) => requestAnimationFrame(() => requestAnimationFrame(resolve))
1216
+ );
1217
+ const cloneRect = captureClone.clone.getBoundingClientRect();
1263
1218
  console.log(
1264
- `[WebSpatial] Capture bounds: original=(${absoluteX},${absoluteY}), afterReset=(${captureX},${captureY}), size=(${currentRect.width}x${currentRect.height})`
1219
+ `[WebSpatial] Visible clone ready: rect=(${cloneRect.x.toFixed(0)},${cloneRect.y.toFixed(0)},${cloneRect.width.toFixed(0)},${cloneRect.height.toFixed(0)})`
1265
1220
  );
1266
- canvas = await html2canvas(element, {
1221
+ canvas = await html2canvas(captureClone.clone, {
1267
1222
  backgroundColor,
1268
1223
  logging: true,
1269
1224
  // Enable logging to debug
@@ -1272,87 +1227,11 @@ async function captureWithHtml2Canvas(html2canvas, element, scale, backgroundCol
1272
1227
  allowTaint: true,
1273
1228
  imageTimeout: 5e3,
1274
1229
  removeContainer: true,
1275
- foreignObjectRendering: false,
1276
- // Don't set scroll offset - let html2canvas use default (current scroll)
1277
- // scrollX and scrollY would affect rendering position but we want document coordinates
1278
- // Crop to element's document position
1279
- // x/y define the top-left corner of the crop region in document coordinates
1280
- x: captureX,
1281
- y: captureY,
1282
- width: currentRect.width,
1283
- height: currentRect.height,
1284
- // Use onclone to MOVE the element to (0,0) in the cloned document
1285
- // This is the key fix for elements far down the page (like footer at Y=1142)
1286
- // By moving to absolute position (0,0), html2canvas will render at the top-left
1287
- onclone: (clonedDoc, clonedElement) => {
1288
- const originalRect = clonedElement.getBoundingClientRect();
1289
- console.log(`[WebSpatial] CLONE: Element position in clone (${originalRect.x.toFixed(0)},${originalRect.y.toFixed(0)})`);
1290
- clonedElement.style.visibility = "visible";
1291
- const hiddenElements = clonedElement.querySelectorAll("*");
1292
- let visibilityFixCount = 0;
1293
- hiddenElements.forEach((el) => {
1294
- const htmlEl = el;
1295
- const style = clonedDoc.defaultView?.getComputedStyle(htmlEl);
1296
- if (style && style.visibility === "hidden") {
1297
- htmlEl.style.visibility = "visible";
1298
- visibilityFixCount++;
1299
- }
1300
- });
1301
- if (visibilityFixCount > 0) {
1302
- console.log(`[WebSpatial] CLONE: Made ${visibilityFixCount + 1} elements visible`);
1303
- }
1304
- let parent = clonedElement.parentElement;
1305
- let depth = 0;
1306
- const maxDepth = 10;
1307
- while (parent && depth < maxDepth) {
1308
- const parentStyle = clonedDoc.defaultView?.getComputedStyle(parent);
1309
- if (parentStyle && parentStyle.position === "relative") {
1310
- const parentTop = parseFloat(parentStyle.top) || 0;
1311
- const parentLeft = parseFloat(parentStyle.left) || 0;
1312
- if (parentTop !== 0 || parentLeft !== 0) {
1313
- console.log(
1314
- `[WebSpatial] CLONE: Reset ${parent.tagName}.${parent.className?.split(" ")[0] || ""} from top=${parentTop}px, left=${parentLeft}px to 0`
1315
- );
1316
- parent.style.top = "0px";
1317
- parent.style.left = "0px";
1318
- }
1319
- }
1320
- parent = parent.parentElement;
1321
- depth++;
1322
- }
1323
- void clonedElement.offsetHeight;
1324
- const clonedRect = clonedElement.getBoundingClientRect();
1325
- const clonedStyle = clonedDoc.defaultView?.getComputedStyle(clonedElement);
1326
- console.log(
1327
- `[WebSpatial] Cloned element (after visibility fix): rect=(${clonedRect.x.toFixed(0)},${clonedRect.y.toFixed(0)},${clonedRect.width.toFixed(0)},${clonedRect.height.toFixed(0)}), visibility=${clonedStyle?.visibility}`
1328
- );
1329
- const numChildren = Math.min(5, clonedElement.children.length);
1330
- for (let i = 0; i < numChildren; i++) {
1331
- const child = clonedElement.children[i];
1332
- const childRect = child.getBoundingClientRect();
1333
- const childStyle = clonedDoc.defaultView?.getComputedStyle(child);
1334
- console.log(
1335
- `[WebSpatial] Child ${i}: ${child.tagName}.${child.className?.split(" ")[0] || ""}, rect=(${childRect.x.toFixed(0)},${childRect.y.toFixed(0)},${childRect.width.toFixed(0)},${childRect.height.toFixed(0)}), vis=${childStyle?.visibility}, display=${childStyle?.display}`
1336
- );
1337
- }
1338
- const productGrid = clonedElement.querySelector(".auto-fill-grid");
1339
- if (productGrid) {
1340
- const gridRect = productGrid.getBoundingClientRect();
1341
- const gridStyle = clonedDoc.defaultView?.getComputedStyle(productGrid);
1342
- console.log(
1343
- `[WebSpatial] Product grid: rect=(${gridRect.x.toFixed(0)},${gridRect.y.toFixed(0)},${gridRect.width.toFixed(0)},${gridRect.height.toFixed(0)}), columns=${gridStyle?.gridTemplateColumns?.substring(0, 100)}`
1344
- );
1345
- const firstCard = productGrid.children[0];
1346
- if (firstCard) {
1347
- const cardRect = firstCard.getBoundingClientRect();
1348
- console.log(`[WebSpatial] First product card: rect=(${cardRect.x.toFixed(0)},${cardRect.y.toFixed(0)},${cardRect.width.toFixed(0)},${cardRect.height.toFixed(0)})`);
1349
- }
1350
- }
1351
- }
1230
+ foreignObjectRendering: false
1352
1231
  });
1353
1232
  } finally {
1354
- restoreVisibility();
1355
- restorePositions();
1233
+ restoreBackground();
1234
+ captureClone.cleanup();
1356
1235
  }
1357
1236
  const ctx = canvas.getContext("2d");
1358
1237
  if (ctx) {
@@ -1415,42 +1294,34 @@ async function captureElementBitmap(element, options) {
1415
1294
  if (options?.waitForImages !== false) {
1416
1295
  await waitForContent(element, 500);
1417
1296
  }
1418
- const restoreBackground = injectCaptureBackground(
1419
- element,
1420
- options?.backgroundColor || DEFAULT_CAPTURE_BACKGROUND
1421
- );
1422
1297
  let result = null;
1423
- try {
1424
- const html2canvas = await loadHtml2Canvas();
1425
- if (html2canvas) {
1426
- console.log("[WebSpatial] Using html2canvas (primary)");
1427
- result = await captureWithHtml2Canvas(
1428
- html2canvas,
1429
- element,
1430
- scale,
1431
- options?.backgroundColor ?? DEFAULT_CAPTURE_BACKGROUND
1432
- );
1433
- if (result) {
1434
- const elapsed = Math.round(performance.now() - startTime);
1435
- console.log(`[WebSpatial] Capture complete (html2canvas, ${elapsed}ms)`);
1436
- return result;
1437
- }
1298
+ const html2canvas = await loadHtml2Canvas();
1299
+ if (html2canvas) {
1300
+ console.log("[WebSpatial] Using html2canvas (primary)");
1301
+ result = await captureWithHtml2Canvas(
1302
+ html2canvas,
1303
+ element,
1304
+ scale,
1305
+ options?.backgroundColor ?? DEFAULT_CAPTURE_BACKGROUND
1306
+ );
1307
+ if (result) {
1308
+ const elapsed = Math.round(performance.now() - startTime);
1309
+ console.log(`[WebSpatial] Capture complete (html2canvas, ${elapsed}ms)`);
1310
+ return result;
1438
1311
  }
1439
- const snapdom = await loadSnapdom();
1440
- if (snapdom) {
1441
- console.log("[WebSpatial] Falling back to snapdom");
1442
- result = await captureWithSnapdom(snapdom, element, scale);
1443
- if (result) {
1444
- const elapsed = Math.round(performance.now() - startTime);
1445
- console.log(`[WebSpatial] Capture complete (snapdom, ${elapsed}ms)`);
1446
- return result;
1447
- }
1312
+ }
1313
+ const snapdom = await loadSnapdom();
1314
+ if (snapdom) {
1315
+ console.log("[WebSpatial] Falling back to snapdom");
1316
+ result = await captureWithSnapdom(snapdom, element, scale);
1317
+ if (result) {
1318
+ const elapsed = Math.round(performance.now() - startTime);
1319
+ console.log(`[WebSpatial] Capture complete (snapdom, ${elapsed}ms)`);
1320
+ return result;
1448
1321
  }
1449
- console.error("[WebSpatial] No capture library available");
1450
- return null;
1451
- } finally {
1452
- restoreBackground();
1453
1322
  }
1323
+ console.error("[WebSpatial] No capture library available");
1324
+ return null;
1454
1325
  }
1455
1326
  function observeContentChanges(element, onContentChange) {
1456
1327
  const observer = new MutationObserver((_mutations) => {