From 101dc50362d3a3ede730e765c032de99912f5014 Mon Sep 17 00:00:00 2001 From: Paul Bakaus Date: Wed, 22 Apr 2026 09:47:08 -0700 Subject: [PATCH] fix(live): resolve canvas background from ancestors when element is transparent Screenshotting a transparent container rendered black because we were no longer passing `backgroundColor` to modern-screenshot at all (to avoid its `background-color !important` override on elements with their own bg, like the teal card). That fix left elements without their own bg rendering on a transparent canvas, which reads as black wherever the PNG is previewed. Now we resolve per-element: if the element has an opaque background-color or a background-image, omit the option (element's own bg renders, no override). If it's transparent, walk up ancestors to the first opaque background (falling back to body/html) and pass that as the canvas fill. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../skills/impeccable/scripts/live-browser.js | 42 ++++++++++++++++--- .../skills/impeccable/scripts/live-browser.js | 42 ++++++++++++++++--- .../skills/impeccable/scripts/live-browser.js | 42 ++++++++++++++++--- .../skills/impeccable/scripts/live-browser.js | 42 ++++++++++++++++--- .../skills/impeccable/scripts/live-browser.js | 42 ++++++++++++++++--- .../skills/impeccable/scripts/live-browser.js | 42 ++++++++++++++++--- .../skills/impeccable/scripts/live-browser.js | 42 ++++++++++++++++--- .pi/skills/impeccable/scripts/live-browser.js | 42 ++++++++++++++++--- .../skills/impeccable/scripts/live-browser.js | 42 ++++++++++++++++--- .../skills/impeccable/scripts/live-browser.js | 42 ++++++++++++++++--- .../skills/impeccable/scripts/live-browser.js | 42 ++++++++++++++++--- public/index.html | 5 +++ public/privacy.html | 3 ++ .../skills/impeccable/scripts/live-browser.js | 42 ++++++++++++++++--- 14 files changed, 452 insertions(+), 60 deletions(-) diff --git a/.agents/skills/impeccable/scripts/live-browser.js b/.agents/skills/impeccable/scripts/live-browser.js index 7915f88f0..cc1b5bd4f 100644 --- a/.agents/skills/impeccable/scripts/live-browser.js +++ b/.agents/skills/impeccable/scripts/live-browser.js @@ -1767,6 +1767,41 @@ return inlineFontUrls(chunks.join('\n')); } + // True if `s` is a computed color string that renders as nothing + // (explicit `transparent`, or `rgba(...)` with alpha 0). + function isTransparentColor(s) { + if (!s) return true; + if (s === 'transparent') return true; + const m = /rgba?\(([^)]+)\)/.exec(s); + if (!m) return false; + const parts = m[1].split(',').map((p) => p.trim()); + if (parts.length === 4) return parseFloat(parts[3]) === 0; + return false; + } + + // modern-screenshot force-sets `background-color: X !important` on the + // cloned root whenever `backgroundColor` is passed, clobbering the + // element's own background. So we only pass it when the element is + // genuinely transparent (no own color, no own image) — in that case + // we resolve up the DOM to the nearest opaque ancestor so the capture + // sits on the page's real background instead of rendering black. + function resolveCanvasBackground(el) { + const own = getComputedStyle(el); + if (!isTransparentColor(own.backgroundColor)) return null; + if (own.backgroundImage && own.backgroundImage !== 'none') return null; + let node = el.parentElement; + while (node) { + const cs = getComputedStyle(node); + if (!isTransparentColor(cs.backgroundColor)) return cs.backgroundColor; + node = node.parentElement; + } + return ( + getComputedStyle(document.body).backgroundColor || + getComputedStyle(document.documentElement).backgroundColor || + '#ffffff' + ); + } + // Capture the element (with current annotations baked in) and return a PNG // Blob. Shared between the Go flow (uploads it to the server) and the // debug toggle (displays it as an overlay for side-by-side comparison). @@ -1787,14 +1822,11 @@ try { const ms = await loadModernScreenshot(); const fontCssText = await collectFontCssText(); - // Deliberately no `backgroundColor` option. modern-screenshot force-sets - // `background-color: X !important` on the root clone's inline style when - // that option is passed, clobbering the element's real background. Leave - // the canvas transparent; the element's own background renders into the - // foreignObject. + const backgroundColor = resolveCanvasBackground(el); return await ms.domToBlob(el, { scale: Math.min(window.devicePixelRatio || 1, 2), font: fontCssText ? { cssText: fontCssText } : undefined, + ...(backgroundColor ? { backgroundColor } : {}), }); } finally { if (annotNode) annotNode.remove(); diff --git a/.claude/skills/impeccable/scripts/live-browser.js b/.claude/skills/impeccable/scripts/live-browser.js index 7915f88f0..cc1b5bd4f 100644 --- a/.claude/skills/impeccable/scripts/live-browser.js +++ b/.claude/skills/impeccable/scripts/live-browser.js @@ -1767,6 +1767,41 @@ return inlineFontUrls(chunks.join('\n')); } + // True if `s` is a computed color string that renders as nothing + // (explicit `transparent`, or `rgba(...)` with alpha 0). + function isTransparentColor(s) { + if (!s) return true; + if (s === 'transparent') return true; + const m = /rgba?\(([^)]+)\)/.exec(s); + if (!m) return false; + const parts = m[1].split(',').map((p) => p.trim()); + if (parts.length === 4) return parseFloat(parts[3]) === 0; + return false; + } + + // modern-screenshot force-sets `background-color: X !important` on the + // cloned root whenever `backgroundColor` is passed, clobbering the + // element's own background. So we only pass it when the element is + // genuinely transparent (no own color, no own image) — in that case + // we resolve up the DOM to the nearest opaque ancestor so the capture + // sits on the page's real background instead of rendering black. + function resolveCanvasBackground(el) { + const own = getComputedStyle(el); + if (!isTransparentColor(own.backgroundColor)) return null; + if (own.backgroundImage && own.backgroundImage !== 'none') return null; + let node = el.parentElement; + while (node) { + const cs = getComputedStyle(node); + if (!isTransparentColor(cs.backgroundColor)) return cs.backgroundColor; + node = node.parentElement; + } + return ( + getComputedStyle(document.body).backgroundColor || + getComputedStyle(document.documentElement).backgroundColor || + '#ffffff' + ); + } + // Capture the element (with current annotations baked in) and return a PNG // Blob. Shared between the Go flow (uploads it to the server) and the // debug toggle (displays it as an overlay for side-by-side comparison). @@ -1787,14 +1822,11 @@ try { const ms = await loadModernScreenshot(); const fontCssText = await collectFontCssText(); - // Deliberately no `backgroundColor` option. modern-screenshot force-sets - // `background-color: X !important` on the root clone's inline style when - // that option is passed, clobbering the element's real background. Leave - // the canvas transparent; the element's own background renders into the - // foreignObject. + const backgroundColor = resolveCanvasBackground(el); return await ms.domToBlob(el, { scale: Math.min(window.devicePixelRatio || 1, 2), font: fontCssText ? { cssText: fontCssText } : undefined, + ...(backgroundColor ? { backgroundColor } : {}), }); } finally { if (annotNode) annotNode.remove(); diff --git a/.cursor/skills/impeccable/scripts/live-browser.js b/.cursor/skills/impeccable/scripts/live-browser.js index 7915f88f0..cc1b5bd4f 100644 --- a/.cursor/skills/impeccable/scripts/live-browser.js +++ b/.cursor/skills/impeccable/scripts/live-browser.js @@ -1767,6 +1767,41 @@ return inlineFontUrls(chunks.join('\n')); } + // True if `s` is a computed color string that renders as nothing + // (explicit `transparent`, or `rgba(...)` with alpha 0). + function isTransparentColor(s) { + if (!s) return true; + if (s === 'transparent') return true; + const m = /rgba?\(([^)]+)\)/.exec(s); + if (!m) return false; + const parts = m[1].split(',').map((p) => p.trim()); + if (parts.length === 4) return parseFloat(parts[3]) === 0; + return false; + } + + // modern-screenshot force-sets `background-color: X !important` on the + // cloned root whenever `backgroundColor` is passed, clobbering the + // element's own background. So we only pass it when the element is + // genuinely transparent (no own color, no own image) — in that case + // we resolve up the DOM to the nearest opaque ancestor so the capture + // sits on the page's real background instead of rendering black. + function resolveCanvasBackground(el) { + const own = getComputedStyle(el); + if (!isTransparentColor(own.backgroundColor)) return null; + if (own.backgroundImage && own.backgroundImage !== 'none') return null; + let node = el.parentElement; + while (node) { + const cs = getComputedStyle(node); + if (!isTransparentColor(cs.backgroundColor)) return cs.backgroundColor; + node = node.parentElement; + } + return ( + getComputedStyle(document.body).backgroundColor || + getComputedStyle(document.documentElement).backgroundColor || + '#ffffff' + ); + } + // Capture the element (with current annotations baked in) and return a PNG // Blob. Shared between the Go flow (uploads it to the server) and the // debug toggle (displays it as an overlay for side-by-side comparison). @@ -1787,14 +1822,11 @@ try { const ms = await loadModernScreenshot(); const fontCssText = await collectFontCssText(); - // Deliberately no `backgroundColor` option. modern-screenshot force-sets - // `background-color: X !important` on the root clone's inline style when - // that option is passed, clobbering the element's real background. Leave - // the canvas transparent; the element's own background renders into the - // foreignObject. + const backgroundColor = resolveCanvasBackground(el); return await ms.domToBlob(el, { scale: Math.min(window.devicePixelRatio || 1, 2), font: fontCssText ? { cssText: fontCssText } : undefined, + ...(backgroundColor ? { backgroundColor } : {}), }); } finally { if (annotNode) annotNode.remove(); diff --git a/.gemini/skills/impeccable/scripts/live-browser.js b/.gemini/skills/impeccable/scripts/live-browser.js index 7915f88f0..cc1b5bd4f 100644 --- a/.gemini/skills/impeccable/scripts/live-browser.js +++ b/.gemini/skills/impeccable/scripts/live-browser.js @@ -1767,6 +1767,41 @@ return inlineFontUrls(chunks.join('\n')); } + // True if `s` is a computed color string that renders as nothing + // (explicit `transparent`, or `rgba(...)` with alpha 0). + function isTransparentColor(s) { + if (!s) return true; + if (s === 'transparent') return true; + const m = /rgba?\(([^)]+)\)/.exec(s); + if (!m) return false; + const parts = m[1].split(',').map((p) => p.trim()); + if (parts.length === 4) return parseFloat(parts[3]) === 0; + return false; + } + + // modern-screenshot force-sets `background-color: X !important` on the + // cloned root whenever `backgroundColor` is passed, clobbering the + // element's own background. So we only pass it when the element is + // genuinely transparent (no own color, no own image) — in that case + // we resolve up the DOM to the nearest opaque ancestor so the capture + // sits on the page's real background instead of rendering black. + function resolveCanvasBackground(el) { + const own = getComputedStyle(el); + if (!isTransparentColor(own.backgroundColor)) return null; + if (own.backgroundImage && own.backgroundImage !== 'none') return null; + let node = el.parentElement; + while (node) { + const cs = getComputedStyle(node); + if (!isTransparentColor(cs.backgroundColor)) return cs.backgroundColor; + node = node.parentElement; + } + return ( + getComputedStyle(document.body).backgroundColor || + getComputedStyle(document.documentElement).backgroundColor || + '#ffffff' + ); + } + // Capture the element (with current annotations baked in) and return a PNG // Blob. Shared between the Go flow (uploads it to the server) and the // debug toggle (displays it as an overlay for side-by-side comparison). @@ -1787,14 +1822,11 @@ try { const ms = await loadModernScreenshot(); const fontCssText = await collectFontCssText(); - // Deliberately no `backgroundColor` option. modern-screenshot force-sets - // `background-color: X !important` on the root clone's inline style when - // that option is passed, clobbering the element's real background. Leave - // the canvas transparent; the element's own background renders into the - // foreignObject. + const backgroundColor = resolveCanvasBackground(el); return await ms.domToBlob(el, { scale: Math.min(window.devicePixelRatio || 1, 2), font: fontCssText ? { cssText: fontCssText } : undefined, + ...(backgroundColor ? { backgroundColor } : {}), }); } finally { if (annotNode) annotNode.remove(); diff --git a/.github/skills/impeccable/scripts/live-browser.js b/.github/skills/impeccable/scripts/live-browser.js index 7915f88f0..cc1b5bd4f 100644 --- a/.github/skills/impeccable/scripts/live-browser.js +++ b/.github/skills/impeccable/scripts/live-browser.js @@ -1767,6 +1767,41 @@ return inlineFontUrls(chunks.join('\n')); } + // True if `s` is a computed color string that renders as nothing + // (explicit `transparent`, or `rgba(...)` with alpha 0). + function isTransparentColor(s) { + if (!s) return true; + if (s === 'transparent') return true; + const m = /rgba?\(([^)]+)\)/.exec(s); + if (!m) return false; + const parts = m[1].split(',').map((p) => p.trim()); + if (parts.length === 4) return parseFloat(parts[3]) === 0; + return false; + } + + // modern-screenshot force-sets `background-color: X !important` on the + // cloned root whenever `backgroundColor` is passed, clobbering the + // element's own background. So we only pass it when the element is + // genuinely transparent (no own color, no own image) — in that case + // we resolve up the DOM to the nearest opaque ancestor so the capture + // sits on the page's real background instead of rendering black. + function resolveCanvasBackground(el) { + const own = getComputedStyle(el); + if (!isTransparentColor(own.backgroundColor)) return null; + if (own.backgroundImage && own.backgroundImage !== 'none') return null; + let node = el.parentElement; + while (node) { + const cs = getComputedStyle(node); + if (!isTransparentColor(cs.backgroundColor)) return cs.backgroundColor; + node = node.parentElement; + } + return ( + getComputedStyle(document.body).backgroundColor || + getComputedStyle(document.documentElement).backgroundColor || + '#ffffff' + ); + } + // Capture the element (with current annotations baked in) and return a PNG // Blob. Shared between the Go flow (uploads it to the server) and the // debug toggle (displays it as an overlay for side-by-side comparison). @@ -1787,14 +1822,11 @@ try { const ms = await loadModernScreenshot(); const fontCssText = await collectFontCssText(); - // Deliberately no `backgroundColor` option. modern-screenshot force-sets - // `background-color: X !important` on the root clone's inline style when - // that option is passed, clobbering the element's real background. Leave - // the canvas transparent; the element's own background renders into the - // foreignObject. + const backgroundColor = resolveCanvasBackground(el); return await ms.domToBlob(el, { scale: Math.min(window.devicePixelRatio || 1, 2), font: fontCssText ? { cssText: fontCssText } : undefined, + ...(backgroundColor ? { backgroundColor } : {}), }); } finally { if (annotNode) annotNode.remove(); diff --git a/.kiro/skills/impeccable/scripts/live-browser.js b/.kiro/skills/impeccable/scripts/live-browser.js index 7915f88f0..cc1b5bd4f 100644 --- a/.kiro/skills/impeccable/scripts/live-browser.js +++ b/.kiro/skills/impeccable/scripts/live-browser.js @@ -1767,6 +1767,41 @@ return inlineFontUrls(chunks.join('\n')); } + // True if `s` is a computed color string that renders as nothing + // (explicit `transparent`, or `rgba(...)` with alpha 0). + function isTransparentColor(s) { + if (!s) return true; + if (s === 'transparent') return true; + const m = /rgba?\(([^)]+)\)/.exec(s); + if (!m) return false; + const parts = m[1].split(',').map((p) => p.trim()); + if (parts.length === 4) return parseFloat(parts[3]) === 0; + return false; + } + + // modern-screenshot force-sets `background-color: X !important` on the + // cloned root whenever `backgroundColor` is passed, clobbering the + // element's own background. So we only pass it when the element is + // genuinely transparent (no own color, no own image) — in that case + // we resolve up the DOM to the nearest opaque ancestor so the capture + // sits on the page's real background instead of rendering black. + function resolveCanvasBackground(el) { + const own = getComputedStyle(el); + if (!isTransparentColor(own.backgroundColor)) return null; + if (own.backgroundImage && own.backgroundImage !== 'none') return null; + let node = el.parentElement; + while (node) { + const cs = getComputedStyle(node); + if (!isTransparentColor(cs.backgroundColor)) return cs.backgroundColor; + node = node.parentElement; + } + return ( + getComputedStyle(document.body).backgroundColor || + getComputedStyle(document.documentElement).backgroundColor || + '#ffffff' + ); + } + // Capture the element (with current annotations baked in) and return a PNG // Blob. Shared between the Go flow (uploads it to the server) and the // debug toggle (displays it as an overlay for side-by-side comparison). @@ -1787,14 +1822,11 @@ try { const ms = await loadModernScreenshot(); const fontCssText = await collectFontCssText(); - // Deliberately no `backgroundColor` option. modern-screenshot force-sets - // `background-color: X !important` on the root clone's inline style when - // that option is passed, clobbering the element's real background. Leave - // the canvas transparent; the element's own background renders into the - // foreignObject. + const backgroundColor = resolveCanvasBackground(el); return await ms.domToBlob(el, { scale: Math.min(window.devicePixelRatio || 1, 2), font: fontCssText ? { cssText: fontCssText } : undefined, + ...(backgroundColor ? { backgroundColor } : {}), }); } finally { if (annotNode) annotNode.remove(); diff --git a/.opencode/skills/impeccable/scripts/live-browser.js b/.opencode/skills/impeccable/scripts/live-browser.js index 7915f88f0..cc1b5bd4f 100644 --- a/.opencode/skills/impeccable/scripts/live-browser.js +++ b/.opencode/skills/impeccable/scripts/live-browser.js @@ -1767,6 +1767,41 @@ return inlineFontUrls(chunks.join('\n')); } + // True if `s` is a computed color string that renders as nothing + // (explicit `transparent`, or `rgba(...)` with alpha 0). + function isTransparentColor(s) { + if (!s) return true; + if (s === 'transparent') return true; + const m = /rgba?\(([^)]+)\)/.exec(s); + if (!m) return false; + const parts = m[1].split(',').map((p) => p.trim()); + if (parts.length === 4) return parseFloat(parts[3]) === 0; + return false; + } + + // modern-screenshot force-sets `background-color: X !important` on the + // cloned root whenever `backgroundColor` is passed, clobbering the + // element's own background. So we only pass it when the element is + // genuinely transparent (no own color, no own image) — in that case + // we resolve up the DOM to the nearest opaque ancestor so the capture + // sits on the page's real background instead of rendering black. + function resolveCanvasBackground(el) { + const own = getComputedStyle(el); + if (!isTransparentColor(own.backgroundColor)) return null; + if (own.backgroundImage && own.backgroundImage !== 'none') return null; + let node = el.parentElement; + while (node) { + const cs = getComputedStyle(node); + if (!isTransparentColor(cs.backgroundColor)) return cs.backgroundColor; + node = node.parentElement; + } + return ( + getComputedStyle(document.body).backgroundColor || + getComputedStyle(document.documentElement).backgroundColor || + '#ffffff' + ); + } + // Capture the element (with current annotations baked in) and return a PNG // Blob. Shared between the Go flow (uploads it to the server) and the // debug toggle (displays it as an overlay for side-by-side comparison). @@ -1787,14 +1822,11 @@ try { const ms = await loadModernScreenshot(); const fontCssText = await collectFontCssText(); - // Deliberately no `backgroundColor` option. modern-screenshot force-sets - // `background-color: X !important` on the root clone's inline style when - // that option is passed, clobbering the element's real background. Leave - // the canvas transparent; the element's own background renders into the - // foreignObject. + const backgroundColor = resolveCanvasBackground(el); return await ms.domToBlob(el, { scale: Math.min(window.devicePixelRatio || 1, 2), font: fontCssText ? { cssText: fontCssText } : undefined, + ...(backgroundColor ? { backgroundColor } : {}), }); } finally { if (annotNode) annotNode.remove(); diff --git a/.pi/skills/impeccable/scripts/live-browser.js b/.pi/skills/impeccable/scripts/live-browser.js index 7915f88f0..cc1b5bd4f 100644 --- a/.pi/skills/impeccable/scripts/live-browser.js +++ b/.pi/skills/impeccable/scripts/live-browser.js @@ -1767,6 +1767,41 @@ return inlineFontUrls(chunks.join('\n')); } + // True if `s` is a computed color string that renders as nothing + // (explicit `transparent`, or `rgba(...)` with alpha 0). + function isTransparentColor(s) { + if (!s) return true; + if (s === 'transparent') return true; + const m = /rgba?\(([^)]+)\)/.exec(s); + if (!m) return false; + const parts = m[1].split(',').map((p) => p.trim()); + if (parts.length === 4) return parseFloat(parts[3]) === 0; + return false; + } + + // modern-screenshot force-sets `background-color: X !important` on the + // cloned root whenever `backgroundColor` is passed, clobbering the + // element's own background. So we only pass it when the element is + // genuinely transparent (no own color, no own image) — in that case + // we resolve up the DOM to the nearest opaque ancestor so the capture + // sits on the page's real background instead of rendering black. + function resolveCanvasBackground(el) { + const own = getComputedStyle(el); + if (!isTransparentColor(own.backgroundColor)) return null; + if (own.backgroundImage && own.backgroundImage !== 'none') return null; + let node = el.parentElement; + while (node) { + const cs = getComputedStyle(node); + if (!isTransparentColor(cs.backgroundColor)) return cs.backgroundColor; + node = node.parentElement; + } + return ( + getComputedStyle(document.body).backgroundColor || + getComputedStyle(document.documentElement).backgroundColor || + '#ffffff' + ); + } + // Capture the element (with current annotations baked in) and return a PNG // Blob. Shared between the Go flow (uploads it to the server) and the // debug toggle (displays it as an overlay for side-by-side comparison). @@ -1787,14 +1822,11 @@ try { const ms = await loadModernScreenshot(); const fontCssText = await collectFontCssText(); - // Deliberately no `backgroundColor` option. modern-screenshot force-sets - // `background-color: X !important` on the root clone's inline style when - // that option is passed, clobbering the element's real background. Leave - // the canvas transparent; the element's own background renders into the - // foreignObject. + const backgroundColor = resolveCanvasBackground(el); return await ms.domToBlob(el, { scale: Math.min(window.devicePixelRatio || 1, 2), font: fontCssText ? { cssText: fontCssText } : undefined, + ...(backgroundColor ? { backgroundColor } : {}), }); } finally { if (annotNode) annotNode.remove(); diff --git a/.rovodev/skills/impeccable/scripts/live-browser.js b/.rovodev/skills/impeccable/scripts/live-browser.js index 7915f88f0..cc1b5bd4f 100644 --- a/.rovodev/skills/impeccable/scripts/live-browser.js +++ b/.rovodev/skills/impeccable/scripts/live-browser.js @@ -1767,6 +1767,41 @@ return inlineFontUrls(chunks.join('\n')); } + // True if `s` is a computed color string that renders as nothing + // (explicit `transparent`, or `rgba(...)` with alpha 0). + function isTransparentColor(s) { + if (!s) return true; + if (s === 'transparent') return true; + const m = /rgba?\(([^)]+)\)/.exec(s); + if (!m) return false; + const parts = m[1].split(',').map((p) => p.trim()); + if (parts.length === 4) return parseFloat(parts[3]) === 0; + return false; + } + + // modern-screenshot force-sets `background-color: X !important` on the + // cloned root whenever `backgroundColor` is passed, clobbering the + // element's own background. So we only pass it when the element is + // genuinely transparent (no own color, no own image) — in that case + // we resolve up the DOM to the nearest opaque ancestor so the capture + // sits on the page's real background instead of rendering black. + function resolveCanvasBackground(el) { + const own = getComputedStyle(el); + if (!isTransparentColor(own.backgroundColor)) return null; + if (own.backgroundImage && own.backgroundImage !== 'none') return null; + let node = el.parentElement; + while (node) { + const cs = getComputedStyle(node); + if (!isTransparentColor(cs.backgroundColor)) return cs.backgroundColor; + node = node.parentElement; + } + return ( + getComputedStyle(document.body).backgroundColor || + getComputedStyle(document.documentElement).backgroundColor || + '#ffffff' + ); + } + // Capture the element (with current annotations baked in) and return a PNG // Blob. Shared between the Go flow (uploads it to the server) and the // debug toggle (displays it as an overlay for side-by-side comparison). @@ -1787,14 +1822,11 @@ try { const ms = await loadModernScreenshot(); const fontCssText = await collectFontCssText(); - // Deliberately no `backgroundColor` option. modern-screenshot force-sets - // `background-color: X !important` on the root clone's inline style when - // that option is passed, clobbering the element's real background. Leave - // the canvas transparent; the element's own background renders into the - // foreignObject. + const backgroundColor = resolveCanvasBackground(el); return await ms.domToBlob(el, { scale: Math.min(window.devicePixelRatio || 1, 2), font: fontCssText ? { cssText: fontCssText } : undefined, + ...(backgroundColor ? { backgroundColor } : {}), }); } finally { if (annotNode) annotNode.remove(); diff --git a/.trae-cn/skills/impeccable/scripts/live-browser.js b/.trae-cn/skills/impeccable/scripts/live-browser.js index 7915f88f0..cc1b5bd4f 100644 --- a/.trae-cn/skills/impeccable/scripts/live-browser.js +++ b/.trae-cn/skills/impeccable/scripts/live-browser.js @@ -1767,6 +1767,41 @@ return inlineFontUrls(chunks.join('\n')); } + // True if `s` is a computed color string that renders as nothing + // (explicit `transparent`, or `rgba(...)` with alpha 0). + function isTransparentColor(s) { + if (!s) return true; + if (s === 'transparent') return true; + const m = /rgba?\(([^)]+)\)/.exec(s); + if (!m) return false; + const parts = m[1].split(',').map((p) => p.trim()); + if (parts.length === 4) return parseFloat(parts[3]) === 0; + return false; + } + + // modern-screenshot force-sets `background-color: X !important` on the + // cloned root whenever `backgroundColor` is passed, clobbering the + // element's own background. So we only pass it when the element is + // genuinely transparent (no own color, no own image) — in that case + // we resolve up the DOM to the nearest opaque ancestor so the capture + // sits on the page's real background instead of rendering black. + function resolveCanvasBackground(el) { + const own = getComputedStyle(el); + if (!isTransparentColor(own.backgroundColor)) return null; + if (own.backgroundImage && own.backgroundImage !== 'none') return null; + let node = el.parentElement; + while (node) { + const cs = getComputedStyle(node); + if (!isTransparentColor(cs.backgroundColor)) return cs.backgroundColor; + node = node.parentElement; + } + return ( + getComputedStyle(document.body).backgroundColor || + getComputedStyle(document.documentElement).backgroundColor || + '#ffffff' + ); + } + // Capture the element (with current annotations baked in) and return a PNG // Blob. Shared between the Go flow (uploads it to the server) and the // debug toggle (displays it as an overlay for side-by-side comparison). @@ -1787,14 +1822,11 @@ try { const ms = await loadModernScreenshot(); const fontCssText = await collectFontCssText(); - // Deliberately no `backgroundColor` option. modern-screenshot force-sets - // `background-color: X !important` on the root clone's inline style when - // that option is passed, clobbering the element's real background. Leave - // the canvas transparent; the element's own background renders into the - // foreignObject. + const backgroundColor = resolveCanvasBackground(el); return await ms.domToBlob(el, { scale: Math.min(window.devicePixelRatio || 1, 2), font: fontCssText ? { cssText: fontCssText } : undefined, + ...(backgroundColor ? { backgroundColor } : {}), }); } finally { if (annotNode) annotNode.remove(); diff --git a/.trae/skills/impeccable/scripts/live-browser.js b/.trae/skills/impeccable/scripts/live-browser.js index 7915f88f0..cc1b5bd4f 100644 --- a/.trae/skills/impeccable/scripts/live-browser.js +++ b/.trae/skills/impeccable/scripts/live-browser.js @@ -1767,6 +1767,41 @@ return inlineFontUrls(chunks.join('\n')); } + // True if `s` is a computed color string that renders as nothing + // (explicit `transparent`, or `rgba(...)` with alpha 0). + function isTransparentColor(s) { + if (!s) return true; + if (s === 'transparent') return true; + const m = /rgba?\(([^)]+)\)/.exec(s); + if (!m) return false; + const parts = m[1].split(',').map((p) => p.trim()); + if (parts.length === 4) return parseFloat(parts[3]) === 0; + return false; + } + + // modern-screenshot force-sets `background-color: X !important` on the + // cloned root whenever `backgroundColor` is passed, clobbering the + // element's own background. So we only pass it when the element is + // genuinely transparent (no own color, no own image) — in that case + // we resolve up the DOM to the nearest opaque ancestor so the capture + // sits on the page's real background instead of rendering black. + function resolveCanvasBackground(el) { + const own = getComputedStyle(el); + if (!isTransparentColor(own.backgroundColor)) return null; + if (own.backgroundImage && own.backgroundImage !== 'none') return null; + let node = el.parentElement; + while (node) { + const cs = getComputedStyle(node); + if (!isTransparentColor(cs.backgroundColor)) return cs.backgroundColor; + node = node.parentElement; + } + return ( + getComputedStyle(document.body).backgroundColor || + getComputedStyle(document.documentElement).backgroundColor || + '#ffffff' + ); + } + // Capture the element (with current annotations baked in) and return a PNG // Blob. Shared between the Go flow (uploads it to the server) and the // debug toggle (displays it as an overlay for side-by-side comparison). @@ -1787,14 +1822,11 @@ try { const ms = await loadModernScreenshot(); const fontCssText = await collectFontCssText(); - // Deliberately no `backgroundColor` option. modern-screenshot force-sets - // `background-color: X !important` on the root clone's inline style when - // that option is passed, clobbering the element's real background. Leave - // the canvas transparent; the element's own background renders into the - // foreignObject. + const backgroundColor = resolveCanvasBackground(el); return await ms.domToBlob(el, { scale: Math.min(window.devicePixelRatio || 1, 2), font: fontCssText ? { cssText: fontCssText } : undefined, + ...(backgroundColor ? { backgroundColor } : {}), }); } finally { if (annotNode) annotNode.remove(); diff --git a/public/index.html b/public/index.html index 905fd69b3..f693bee2d 100644 --- a/public/index.html +++ b/public/index.html @@ -85,6 +85,7 @@
+

Impeccable

Design fluency for AI harnesses

@@ -121,6 +122,7 @@
+
@@ -756,5 +758,8 @@ + + + diff --git a/public/privacy.html b/public/privacy.html index 3a41097f0..4139e711e 100644 --- a/public/privacy.html +++ b/public/privacy.html @@ -77,5 +77,8 @@

Contact

Questions about this policy? Open an issue on GitHub or reach out to @pbakaus.

+ + + diff --git a/source/skills/impeccable/scripts/live-browser.js b/source/skills/impeccable/scripts/live-browser.js index 7915f88f0..cc1b5bd4f 100644 --- a/source/skills/impeccable/scripts/live-browser.js +++ b/source/skills/impeccable/scripts/live-browser.js @@ -1767,6 +1767,41 @@ return inlineFontUrls(chunks.join('\n')); } + // True if `s` is a computed color string that renders as nothing + // (explicit `transparent`, or `rgba(...)` with alpha 0). + function isTransparentColor(s) { + if (!s) return true; + if (s === 'transparent') return true; + const m = /rgba?\(([^)]+)\)/.exec(s); + if (!m) return false; + const parts = m[1].split(',').map((p) => p.trim()); + if (parts.length === 4) return parseFloat(parts[3]) === 0; + return false; + } + + // modern-screenshot force-sets `background-color: X !important` on the + // cloned root whenever `backgroundColor` is passed, clobbering the + // element's own background. So we only pass it when the element is + // genuinely transparent (no own color, no own image) — in that case + // we resolve up the DOM to the nearest opaque ancestor so the capture + // sits on the page's real background instead of rendering black. + function resolveCanvasBackground(el) { + const own = getComputedStyle(el); + if (!isTransparentColor(own.backgroundColor)) return null; + if (own.backgroundImage && own.backgroundImage !== 'none') return null; + let node = el.parentElement; + while (node) { + const cs = getComputedStyle(node); + if (!isTransparentColor(cs.backgroundColor)) return cs.backgroundColor; + node = node.parentElement; + } + return ( + getComputedStyle(document.body).backgroundColor || + getComputedStyle(document.documentElement).backgroundColor || + '#ffffff' + ); + } + // Capture the element (with current annotations baked in) and return a PNG // Blob. Shared between the Go flow (uploads it to the server) and the // debug toggle (displays it as an overlay for side-by-side comparison). @@ -1787,14 +1822,11 @@ try { const ms = await loadModernScreenshot(); const fontCssText = await collectFontCssText(); - // Deliberately no `backgroundColor` option. modern-screenshot force-sets - // `background-color: X !important` on the root clone's inline style when - // that option is passed, clobbering the element's real background. Leave - // the canvas transparent; the element's own background renders into the - // foreignObject. + const backgroundColor = resolveCanvasBackground(el); return await ms.domToBlob(el, { scale: Math.min(window.devicePixelRatio || 1, 2), font: fontCssText ? { cssText: fontCssText } : undefined, + ...(backgroundColor ? { backgroundColor } : {}), }); } finally { if (annotNode) annotNode.remove();