oomi-ai 0.2.40 → 0.2.42

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.
@@ -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) => {