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.
@@ -1055,17 +1055,36 @@ function hasTransparentBackground(element) {
1055
1055
  }
1056
1056
  function injectCaptureBackground(element, backgroundColor = DEFAULT_CAPTURE_BACKGROUND) {
1057
1057
  const restoreFunctions = [];
1058
- const originalBg = element.style.backgroundColor;
1059
1058
  const wasTransparent = hasTransparentBackground(element);
1060
- element.style.backgroundColor = backgroundColor;
1061
- restoreFunctions.push(() => {
1062
- element.style.backgroundColor = originalBg;
1063
- });
1059
+ if (wasTransparent) {
1060
+ const originalBg = element.style.backgroundColor;
1061
+ element.style.backgroundColor = backgroundColor;
1062
+ restoreFunctions.push(() => {
1063
+ element.style.backgroundColor = originalBg;
1064
+ });
1065
+ }
1066
+ const shouldInjectDescendantBackground = (candidate) => {
1067
+ if (!hasTransparentBackground(candidate)) {
1068
+ return false;
1069
+ }
1070
+ const style = window.getComputedStyle(candidate);
1071
+ if (style.display === "inline" || style.display === "contents") {
1072
+ return false;
1073
+ }
1074
+ const rect = candidate.getBoundingClientRect();
1075
+ const hasMeaningfulBox = rect.width >= 32 && rect.height >= 32;
1076
+ if (!hasMeaningfulBox) {
1077
+ return false;
1078
+ }
1079
+ const hasNestedLayout = candidate.children.length > 0;
1080
+ const hasVisualContainerTraits = style.borderRadius !== "0px" || style.boxShadow !== "none" || style.backdropFilter !== "none" || style.overflow !== "visible" || style.borderStyle !== "none";
1081
+ return hasNestedLayout || hasVisualContainerTraits;
1082
+ };
1064
1083
  const allDescendants = element.querySelectorAll("*");
1065
1084
  let injectedCount = 0;
1066
1085
  allDescendants.forEach((el) => {
1067
1086
  const htmlEl = el;
1068
- if (hasTransparentBackground(htmlEl)) {
1087
+ if (shouldInjectDescendantBackground(htmlEl)) {
1069
1088
  const childOriginalBg = htmlEl.style.backgroundColor;
1070
1089
  htmlEl.style.backgroundColor = backgroundColor;
1071
1090
  injectedCount++;
@@ -1174,121 +1193,51 @@ async function captureWithSnapdom(snapdom, element, scale) {
1174
1193
  return null;
1175
1194
  }
1176
1195
  }
1177
- function findOffsetElements(element) {
1178
- const offsetElements = [];
1179
- const style = window.getComputedStyle(element);
1180
- if (style.position === "relative") {
1181
- const top = parseFloat(style.top) || 0;
1182
- const left = parseFloat(style.left) || 0;
1183
- if (top !== 0 || left !== 0) {
1184
- offsetElements.push({
1185
- element,
1186
- originalTop: element.style.top,
1187
- originalLeft: element.style.left,
1188
- topValue: top,
1189
- leftValue: left
1190
- });
1191
- }
1192
- }
1193
- let parent = element.parentElement;
1194
- let depth = 0;
1195
- const maxDepth = 5;
1196
- while (parent && depth < maxDepth) {
1197
- const parentStyle = window.getComputedStyle(parent);
1198
- if (parentStyle.position === "relative") {
1199
- const parentTop = parseFloat(parentStyle.top) || 0;
1200
- const parentLeft = parseFloat(parentStyle.left) || 0;
1201
- if (parentTop !== 0 || parentLeft !== 0) {
1202
- console.log(
1203
- `[WebSpatial] Found offset element: ${parent.tagName}.${parent.className?.split(" ")[0] || ""} top=${parentTop}px, left=${parentLeft}px`
1204
- );
1205
- offsetElements.push({
1206
- element: parent,
1207
- originalTop: parent.style.top,
1208
- originalLeft: parent.style.left,
1209
- topValue: parentTop,
1210
- leftValue: parentLeft
1211
- });
1196
+ function createVisibleCaptureClone(element) {
1197
+ const rect = element.getBoundingClientRect();
1198
+ const sandbox = document.createElement("div");
1199
+ sandbox.setAttribute("aria-hidden", "true");
1200
+ sandbox.style.position = "fixed";
1201
+ sandbox.style.left = "-10000px";
1202
+ sandbox.style.top = "0px";
1203
+ sandbox.style.pointerEvents = "none";
1204
+ sandbox.style.zIndex = "-1";
1205
+ sandbox.style.contain = "layout style paint";
1206
+ sandbox.style.opacity = "1";
1207
+ const clone = element.cloneNode(true);
1208
+ const makeCloneVisible = (node) => {
1209
+ node.style.visibility = "visible";
1210
+ node.style.opacity = "1";
1211
+ node.style.transition = "none";
1212
+ node.style.animation = "none";
1213
+ node.style.transform = "none";
1214
+ node.style.top = "0px";
1215
+ node.style.left = "0px";
1216
+ Array.from(node.children).forEach((child) => {
1217
+ if (child instanceof HTMLElement) {
1218
+ makeCloneVisible(child);
1212
1219
  }
1213
- }
1214
- parent = parent.parentElement;
1215
- depth++;
1216
- }
1217
- return offsetElements;
1218
- }
1219
- function getContentOffset(element) {
1220
- const offsetElements = findOffsetElements(element);
1221
- return {
1222
- top: offsetElements.reduce((sum, el) => sum + el.topValue, 0),
1223
- left: offsetElements.reduce((sum, el) => sum + el.leftValue, 0)
1224
- };
1225
- }
1226
- function makeElementVisible(element) {
1227
- const restoreFunctions = [];
1228
- const originalVisibility = element.style.visibility;
1229
- const computedVisibility = window.getComputedStyle(element).visibility;
1230
- if (computedVisibility === "hidden") {
1231
- element.style.visibility = "visible";
1232
- console.log(`[WebSpatial] Made element visible for capture (was: ${computedVisibility})`);
1233
- restoreFunctions.push(() => {
1234
- element.style.visibility = originalVisibility;
1235
1220
  });
1236
- }
1237
- const allDescendants = element.querySelectorAll("*");
1238
- allDescendants.forEach((el) => {
1239
- const htmlEl = el;
1240
- const childComputedVisibility = window.getComputedStyle(htmlEl).visibility;
1241
- if (childComputedVisibility === "hidden") {
1242
- const childOriginalVisibility = htmlEl.style.visibility;
1243
- htmlEl.style.visibility = "visible";
1244
- restoreFunctions.push(() => {
1245
- htmlEl.style.visibility = childOriginalVisibility;
1246
- });
1247
- }
1248
- });
1249
- if (restoreFunctions.length > 1) {
1250
- console.log(`[WebSpatial] Made ${restoreFunctions.length} elements visible for capture`);
1251
- }
1252
- return () => {
1253
- restoreFunctions.forEach((restore) => restore());
1221
+ };
1222
+ makeCloneVisible(clone);
1223
+ clone.style.position = "relative";
1224
+ clone.style.margin = "0px";
1225
+ clone.style.width = `${Math.ceil(rect.width)}px`;
1226
+ clone.style.minHeight = `${Math.ceil(rect.height)}px`;
1227
+ sandbox.appendChild(clone);
1228
+ document.body.appendChild(sandbox);
1229
+ return {
1230
+ clone,
1231
+ cleanup: () => sandbox.remove()
1254
1232
  };
1255
1233
  }
1256
1234
  async function captureWithHtml2Canvas(html2canvas, element, scale, backgroundColor) {
1257
1235
  try {
1258
1236
  const rect = element.getBoundingClientRect();
1259
- const contentOffset = getContentOffset(element);
1260
- 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})`);
1261
- const scrollX = window.scrollX || window.pageXOffset || 0;
1237
+ console.log(
1238
+ `[WebSpatial] html2canvas capturing via visible clone: rect=(${rect.x.toFixed(0)},${rect.y.toFixed(0)},${rect.width.toFixed(0)},${rect.height.toFixed(0)})`
1239
+ );
1262
1240
  const scrollY = window.scrollY || window.pageYOffset || 0;
1263
- const absoluteX = rect.x + scrollX;
1264
- const absoluteY = rect.y + scrollY;
1265
- const offsetElements = findOffsetElements(element);
1266
- if (offsetElements.length > 0) {
1267
- console.log(
1268
- `[WebSpatial] Temporarily resetting ${offsetElements.length} offset elements for capture`
1269
- );
1270
- offsetElements.forEach(({ element: el, topValue, leftValue }) => {
1271
- el.style.top = "0px";
1272
- el.style.left = "0px";
1273
- console.log(
1274
- `[WebSpatial] Reset: ${el.tagName}.${el.className?.split(" ")[0] || ""} from top=${topValue}px, left=${leftValue}px to 0`
1275
- );
1276
- });
1277
- void element.offsetHeight;
1278
- await new Promise((resolve) => setTimeout(resolve, 50));
1279
- console.log("[WebSpatial] Reflow complete after offset reset");
1280
- }
1281
- const restorePositions = () => {
1282
- offsetElements.forEach(({ element: el, originalTop, originalLeft }) => {
1283
- el.style.top = originalTop;
1284
- el.style.left = originalLeft;
1285
- });
1286
- };
1287
- if (element.scrollTop !== 0) {
1288
- console.log(`[WebSpatial] Resetting element scroll from ${element.scrollTop} to 0`);
1289
- element.scrollTop = 0;
1290
- }
1291
- const restoreVisibility = makeElementVisible(element);
1292
1241
  const viewportTop = scrollY;
1293
1242
  const viewportBottom = scrollY + window.innerHeight;
1294
1243
  const elementTop = rect.y + scrollY;
@@ -1297,14 +1246,20 @@ async function captureWithHtml2Canvas(html2canvas, element, scale, backgroundCol
1297
1246
  `[WebSpatial] Capture context: viewport=(${viewportTop}-${viewportBottom}), element=(${elementTop}-${elementBottom}), innerHeight=${window.innerHeight}`
1298
1247
  );
1299
1248
  let canvas;
1249
+ const captureClone = createVisibleCaptureClone(element);
1250
+ const restoreBackground = injectCaptureBackground(
1251
+ captureClone.clone,
1252
+ backgroundColor || DEFAULT_CAPTURE_BACKGROUND
1253
+ );
1300
1254
  try {
1301
- const currentRect = element.getBoundingClientRect();
1302
- const captureX = currentRect.x + scrollX;
1303
- const captureY = currentRect.y + scrollY;
1255
+ await new Promise(
1256
+ (resolve) => requestAnimationFrame(() => requestAnimationFrame(resolve))
1257
+ );
1258
+ const cloneRect = captureClone.clone.getBoundingClientRect();
1304
1259
  console.log(
1305
- `[WebSpatial] Capture bounds: original=(${absoluteX},${absoluteY}), afterReset=(${captureX},${captureY}), size=(${currentRect.width}x${currentRect.height})`
1260
+ `[WebSpatial] Visible clone ready: rect=(${cloneRect.x.toFixed(0)},${cloneRect.y.toFixed(0)},${cloneRect.width.toFixed(0)},${cloneRect.height.toFixed(0)})`
1306
1261
  );
1307
- canvas = await html2canvas(element, {
1262
+ canvas = await html2canvas(captureClone.clone, {
1308
1263
  backgroundColor,
1309
1264
  logging: true,
1310
1265
  // Enable logging to debug
@@ -1313,87 +1268,11 @@ async function captureWithHtml2Canvas(html2canvas, element, scale, backgroundCol
1313
1268
  allowTaint: true,
1314
1269
  imageTimeout: 5e3,
1315
1270
  removeContainer: true,
1316
- foreignObjectRendering: false,
1317
- // Don't set scroll offset - let html2canvas use default (current scroll)
1318
- // scrollX and scrollY would affect rendering position but we want document coordinates
1319
- // Crop to element's document position
1320
- // x/y define the top-left corner of the crop region in document coordinates
1321
- x: captureX,
1322
- y: captureY,
1323
- width: currentRect.width,
1324
- height: currentRect.height,
1325
- // Use onclone to MOVE the element to (0,0) in the cloned document
1326
- // This is the key fix for elements far down the page (like footer at Y=1142)
1327
- // By moving to absolute position (0,0), html2canvas will render at the top-left
1328
- onclone: (clonedDoc, clonedElement) => {
1329
- const originalRect = clonedElement.getBoundingClientRect();
1330
- console.log(`[WebSpatial] CLONE: Element position in clone (${originalRect.x.toFixed(0)},${originalRect.y.toFixed(0)})`);
1331
- clonedElement.style.visibility = "visible";
1332
- const hiddenElements = clonedElement.querySelectorAll("*");
1333
- let visibilityFixCount = 0;
1334
- hiddenElements.forEach((el) => {
1335
- const htmlEl = el;
1336
- const style = clonedDoc.defaultView?.getComputedStyle(htmlEl);
1337
- if (style && style.visibility === "hidden") {
1338
- htmlEl.style.visibility = "visible";
1339
- visibilityFixCount++;
1340
- }
1341
- });
1342
- if (visibilityFixCount > 0) {
1343
- console.log(`[WebSpatial] CLONE: Made ${visibilityFixCount + 1} elements visible`);
1344
- }
1345
- let parent = clonedElement.parentElement;
1346
- let depth = 0;
1347
- const maxDepth = 10;
1348
- while (parent && depth < maxDepth) {
1349
- const parentStyle = clonedDoc.defaultView?.getComputedStyle(parent);
1350
- if (parentStyle && parentStyle.position === "relative") {
1351
- const parentTop = parseFloat(parentStyle.top) || 0;
1352
- const parentLeft = parseFloat(parentStyle.left) || 0;
1353
- if (parentTop !== 0 || parentLeft !== 0) {
1354
- console.log(
1355
- `[WebSpatial] CLONE: Reset ${parent.tagName}.${parent.className?.split(" ")[0] || ""} from top=${parentTop}px, left=${parentLeft}px to 0`
1356
- );
1357
- parent.style.top = "0px";
1358
- parent.style.left = "0px";
1359
- }
1360
- }
1361
- parent = parent.parentElement;
1362
- depth++;
1363
- }
1364
- void clonedElement.offsetHeight;
1365
- const clonedRect = clonedElement.getBoundingClientRect();
1366
- const clonedStyle = clonedDoc.defaultView?.getComputedStyle(clonedElement);
1367
- console.log(
1368
- `[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}`
1369
- );
1370
- const numChildren = Math.min(5, clonedElement.children.length);
1371
- for (let i = 0; i < numChildren; i++) {
1372
- const child = clonedElement.children[i];
1373
- const childRect = child.getBoundingClientRect();
1374
- const childStyle = clonedDoc.defaultView?.getComputedStyle(child);
1375
- console.log(
1376
- `[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}`
1377
- );
1378
- }
1379
- const productGrid = clonedElement.querySelector(".auto-fill-grid");
1380
- if (productGrid) {
1381
- const gridRect = productGrid.getBoundingClientRect();
1382
- const gridStyle = clonedDoc.defaultView?.getComputedStyle(productGrid);
1383
- console.log(
1384
- `[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)}`
1385
- );
1386
- const firstCard = productGrid.children[0];
1387
- if (firstCard) {
1388
- const cardRect = firstCard.getBoundingClientRect();
1389
- console.log(`[WebSpatial] First product card: rect=(${cardRect.x.toFixed(0)},${cardRect.y.toFixed(0)},${cardRect.width.toFixed(0)},${cardRect.height.toFixed(0)})`);
1390
- }
1391
- }
1392
- }
1271
+ foreignObjectRendering: false
1393
1272
  });
1394
1273
  } finally {
1395
- restoreVisibility();
1396
- restorePositions();
1274
+ restoreBackground();
1275
+ captureClone.cleanup();
1397
1276
  }
1398
1277
  const ctx = canvas.getContext("2d");
1399
1278
  if (ctx) {
@@ -1456,42 +1335,34 @@ async function captureElementBitmap(element, options) {
1456
1335
  if (options?.waitForImages !== false) {
1457
1336
  await waitForContent(element, 500);
1458
1337
  }
1459
- const restoreBackground = injectCaptureBackground(
1460
- element,
1461
- options?.backgroundColor || DEFAULT_CAPTURE_BACKGROUND
1462
- );
1463
1338
  let result = null;
1464
- try {
1465
- const html2canvas = await loadHtml2Canvas();
1466
- if (html2canvas) {
1467
- console.log("[WebSpatial] Using html2canvas (primary)");
1468
- result = await captureWithHtml2Canvas(
1469
- html2canvas,
1470
- element,
1471
- scale,
1472
- options?.backgroundColor ?? DEFAULT_CAPTURE_BACKGROUND
1473
- );
1474
- if (result) {
1475
- const elapsed = Math.round(performance.now() - startTime);
1476
- console.log(`[WebSpatial] Capture complete (html2canvas, ${elapsed}ms)`);
1477
- return result;
1478
- }
1339
+ const html2canvas = await loadHtml2Canvas();
1340
+ if (html2canvas) {
1341
+ console.log("[WebSpatial] Using html2canvas (primary)");
1342
+ result = await captureWithHtml2Canvas(
1343
+ html2canvas,
1344
+ element,
1345
+ scale,
1346
+ options?.backgroundColor ?? DEFAULT_CAPTURE_BACKGROUND
1347
+ );
1348
+ if (result) {
1349
+ const elapsed = Math.round(performance.now() - startTime);
1350
+ console.log(`[WebSpatial] Capture complete (html2canvas, ${elapsed}ms)`);
1351
+ return result;
1479
1352
  }
1480
- const snapdom = await loadSnapdom();
1481
- if (snapdom) {
1482
- console.log("[WebSpatial] Falling back to snapdom");
1483
- result = await captureWithSnapdom(snapdom, element, scale);
1484
- if (result) {
1485
- const elapsed = Math.round(performance.now() - startTime);
1486
- console.log(`[WebSpatial] Capture complete (snapdom, ${elapsed}ms)`);
1487
- return result;
1488
- }
1353
+ }
1354
+ const snapdom = await loadSnapdom();
1355
+ if (snapdom) {
1356
+ console.log("[WebSpatial] Falling back to snapdom");
1357
+ result = await captureWithSnapdom(snapdom, element, scale);
1358
+ if (result) {
1359
+ const elapsed = Math.round(performance.now() - startTime);
1360
+ console.log(`[WebSpatial] Capture complete (snapdom, ${elapsed}ms)`);
1361
+ return result;
1489
1362
  }
1490
- console.error("[WebSpatial] No capture library available");
1491
- return null;
1492
- } finally {
1493
- restoreBackground();
1494
1363
  }
1364
+ console.error("[WebSpatial] No capture library available");
1365
+ return null;
1495
1366
  }
1496
1367
  function observeContentChanges(element, onContentChange) {
1497
1368
  const observer = new MutationObserver((_mutations) => {