diff --git a/.agents/skills/impeccable/scripts/live-browser-dom.js b/.agents/skills/impeccable/scripts/live-browser-dom.js new file mode 100644 index 000000000..ad6a794b3 --- /dev/null +++ b/.agents/skills/impeccable/scripts/live-browser-dom.js @@ -0,0 +1,146 @@ +/** + * Browser-side DOM helpers for Impeccable live mode. + * + * Kept separate from live-browser.js so future browser script parts can share + * chrome mounting, lookup, focus, and picker helpers without depending on the + * full overlay UI bundle. + */ +(function (root) { + 'use strict'; + if (!root) return; + + function createLiveBrowserDomHelpers({ + prefix, + skipTags, + document: doc = root.document, + css = root.CSS, + crypto = root.crypto, + } = {}) { + if (!prefix) throw new Error('prefix required'); + if (!doc) throw new Error('document required'); + const tagsToSkip = skipTags || new Set(); + + function own(el) { + return el && (el.id?.startsWith(prefix) || el.closest?.('[id^="' + prefix + '"]')); + } + + function pickable(el) { + if (!el || el.nodeType !== 1) return false; + if (tagsToSkip.has(String(el.tagName || '').toLowerCase())) return false; + if (own(el)) return false; + const r = el.getBoundingClientRect(); + return r.width >= 20 && r.height >= 20; + } + + function desc(el) { + if (!el) return ''; + let s = el.tagName.toLowerCase(); + if (el.id) s += '#' + el.id; + else if (el.classList.length) s += '.' + [...el.classList].slice(0, 2).join('.'); + return s; + } + + function rectIsUsableAnchor(rect) { + return !!rect && rect.width > 0.5 && rect.height > 0.5; + } + + function makeFrozenAnchor(el) { + if (!el || !el.getBoundingClientRect) return null; + const r = el.getBoundingClientRect(); + if (!rectIsUsableAnchor(r)) return null; + const rect = { + x: r.x, y: r.y, + top: r.top, left: r.left, + right: r.right, bottom: r.bottom, + width: r.width, height: r.height, + }; + return { + __impeccableFrozenAnchor: true, + tagName: el.tagName || 'DIV', + id: el.id || '', + classList: el.classList ? [...el.classList] : [], + hasAttribute: () => false, + getBoundingClientRect: () => rect, + }; + } + + function id8() { + if (crypto?.randomUUID) return crypto.randomUUID().replace(/-/g, '').slice(0, 8); + return (Math.random().toString(16).slice(2) + Date.now().toString(16)).slice(0, 8); + } + + function cssId(id) { + if (css?.escape) return css.escape(id); + return String(id).replace(/([ !"#$%&'()*+,./:;<=>?@[\\\]^`{|}~])/g, '\\$1'); + } + + function liveUiRoot() { + const uiRoot = root.__IMPECCABLE_LIVE_UI_ROOT__; + if (uiRoot && typeof uiRoot.appendChild === 'function') return uiRoot; + return doc.body; + } + + function uiAppend(el) { + liveUiRoot().appendChild(el); + return el; + } + + function uiAppendStyle(styleEl) { + const uiRoot = liveUiRoot(); + if (uiRoot && uiRoot !== doc.body) uiRoot.appendChild(styleEl); + else doc.head.appendChild(styleEl); + return styleEl; + } + + function uiGetById(id) { + const uiRoot = liveUiRoot(); + if (uiRoot?.getElementById) { + const found = uiRoot.getElementById(id); + if (found) return found; + } + if (uiRoot?.querySelector) { + const found = uiRoot.querySelector('#' + cssId(id)); + if (found) return found; + } + return doc.getElementById(id); + } + + function activeElementDeep() { + let active = doc.activeElement; + while (active?.shadowRoot?.activeElement) active = active.shadowRoot.activeElement; + return active; + } + + function defangOutsideHandlers(rootEl, { setPointerEvents = true } = {}) { + if (!rootEl) return; + if (setPointerEvents) { + rootEl.style.setProperty('pointer-events', 'auto', 'important'); + } + const stop = (e) => e.stopPropagation(); + rootEl.addEventListener('pointerdown', stop); + rootEl.addEventListener('mousedown', stop); + rootEl.addEventListener('focusin', stop); + } + + return { + own, + pickable, + desc, + rectIsUsableAnchor, + makeFrozenAnchor, + id8, + cssId, + liveUiRoot, + uiAppend, + uiAppendStyle, + uiGetById, + activeElementDeep, + defangOutsideHandlers, + }; + } + + root.__IMPECCABLE_LIVE_DOM__ = { + version: 1, + createLiveBrowserDomHelpers, + }; +})(typeof window !== 'undefined' ? window : globalThis); diff --git a/.agents/skills/impeccable/scripts/live-browser.js b/.agents/skills/impeccable/scripts/live-browser.js index 9c4b6c21e..9092338eb 100644 --- a/.agents/skills/impeccable/scripts/live-browser.js +++ b/.agents/skills/impeccable/scripts/live-browser.js @@ -195,93 +195,31 @@ // Helpers // - function own(el) { - return el && (el.id?.startsWith(PREFIX) || el.closest?.('[id^="' + PREFIX + '"]')); - } - - function pickable(el) { - if (!el || el.nodeType !== 1) return false; - if (SKIP_TAGS.has(el.tagName.toLowerCase())) return false; - if (own(el)) return false; - const r = el.getBoundingClientRect(); - return r.width >= 20 && r.height >= 20; - } - - function desc(el) { - if (!el) return ''; - let s = el.tagName.toLowerCase(); - if (el.id) s += '#' + el.id; - else if (el.classList.length) s += '.' + [...el.classList].slice(0, 2).join('.'); - return s; - } - - function rectIsUsableAnchor(rect) { - return !!rect && rect.width > 0.5 && rect.height > 0.5; - } - - function makeFrozenAnchor(el) { - if (!el || !el.getBoundingClientRect) return null; - const r = el.getBoundingClientRect(); - if (!rectIsUsableAnchor(r)) return null; - const rect = { - x: r.x, y: r.y, - top: r.top, left: r.left, - right: r.right, bottom: r.bottom, - width: r.width, height: r.height, - }; - return { - __impeccableFrozenAnchor: true, - tagName: el.tagName || 'DIV', - id: el.id || '', - classList: el.classList ? [...el.classList] : [], - hasAttribute: () => false, - getBoundingClientRect: () => rect, - }; - } - - function id8() { return crypto.randomUUID().replace(/-/g, '').slice(0, 8); } - - function cssId(id) { - if (window.CSS?.escape) return CSS.escape(id); - return String(id).replace(/([ !"#$%&'()*+,./:;<=>?@[\\\]^`{|}~])/g, '\\$1'); - } - - function liveUiRoot() { - const root = window.__IMPECCABLE_LIVE_UI_ROOT__; - if (root && typeof root.appendChild === 'function') return root; - return document.body; - } - - function uiAppend(el) { - liveUiRoot().appendChild(el); - return el; - } - - function uiAppendStyle(styleEl) { - const root = liveUiRoot(); - if (root && root !== document.body) root.appendChild(styleEl); - else document.head.appendChild(styleEl); - return styleEl; - } - - function uiGetById(id) { - const root = liveUiRoot(); - if (root?.getElementById) { - const found = root.getElementById(id); - if (found) return found; - } - if (root?.querySelector) { - const found = root.querySelector('#' + cssId(id)); - if (found) return found; - } - return document.getElementById(id); - } - - function activeElementDeep() { - let active = document.activeElement; - while (active?.shadowRoot?.activeElement) active = active.shadowRoot.activeElement; - return active; + const domHelpers = window.__IMPECCABLE_LIVE_DOM__?.createLiveBrowserDomHelpers({ + prefix: PREFIX, + skipTags: SKIP_TAGS, + document, + }); + if (!domHelpers) { + console.error('[impeccable] live-browser-dom.js was not loaded. Live mode cannot start safely.'); + window.__IMPECCABLE_LIVE_INIT__ = false; + return; } + const { + own, + pickable, + desc, + rectIsUsableAnchor, + makeFrozenAnchor, + id8, + cssId, + liveUiRoot, + uiAppend, + uiAppendStyle, + uiGetById, + activeElementDeep, + defangOutsideHandlers, + } = domHelpers; window.__IMPECCABLE_LIVE_CHROME_CORE__ = { version: 1, @@ -314,45 +252,6 @@ }), }; - // Modal-aware chrome: keep our floating UI clickable inside Radix / - // Headless UI / vaul portals. - // - // Two host-page behaviors break us when the picked element lives inside a - // modal dialog: - // - // 1. Modal scroll-lock disables outside pointer events. Radix's - // `DismissableLayer` sets `document.body.style.pointerEvents = 'none'` - // while a modal is open and only restores `auto` on the layer. Our - // chrome inherits `none` from and becomes unclickable. - // 2. The dialog's outside-interaction handler (Radix's - // `usePointerDownOutside`) listens at document level and dismisses - // the dialog whenever a `pointerdown` lands outside the layer node. - // Our chrome is a sibling of , so Radix classifies our clicks - // as outside and tears the dialog down mid-task. - // - // We can't reliably re-parent our chrome into the dialog subtree (z-index - // stacking, scroll containers, theming all become host-page concerns), so - // we defang both behaviors at our root: - // - // - `pointer-events: auto !important` overrides the inherited `none`. - // - Stop `pointerdown` / `mousedown` propagation so the document-level - // dismiss listener never fires for our clicks. - // - Stop `focusin` propagation so any focus shifts inside our chrome - // don't read as "focus moved outside the dialog" to focus traps. - // - // Click events still bubble normally - only the early pointer/focus - // signals that drive outside-interaction detection are silenced. - function defangOutsideHandlers(rootEl, { setPointerEvents = true } = {}) { - if (!rootEl) return; - if (setPointerEvents) { - rootEl.style.setProperty('pointer-events', 'auto', 'important'); - } - const stop = (e) => e.stopPropagation(); - rootEl.addEventListener('pointerdown', stop); - rootEl.addEventListener('mousedown', stop); - rootEl.addEventListener('focusin', stop); - } - // // Highlight overlay // diff --git a/.agents/skills/impeccable/scripts/live/browser-script-parts.mjs b/.agents/skills/impeccable/scripts/live/browser-script-parts.mjs index f99704aff..9229e34f8 100644 --- a/.agents/skills/impeccable/scripts/live/browser-script-parts.mjs +++ b/.agents/skills/impeccable/scripts/live/browser-script-parts.mjs @@ -3,6 +3,7 @@ import path from 'node:path'; export const LIVE_BROWSER_SCRIPT_PARTS = Object.freeze([ Object.freeze({ name: 'session-state', file: 'live-browser-session.js' }), + Object.freeze({ name: 'dom-helpers', file: 'live-browser-dom.js' }), Object.freeze({ name: 'browser-ui', file: 'live-browser.js' }), ]); diff --git a/.claude/skills/impeccable/scripts/live-browser-dom.js b/.claude/skills/impeccable/scripts/live-browser-dom.js new file mode 100644 index 000000000..ad6a794b3 --- /dev/null +++ b/.claude/skills/impeccable/scripts/live-browser-dom.js @@ -0,0 +1,146 @@ +/** + * Browser-side DOM helpers for Impeccable live mode. + * + * Kept separate from live-browser.js so future browser script parts can share + * chrome mounting, lookup, focus, and picker helpers without depending on the + * full overlay UI bundle. + */ +(function (root) { + 'use strict'; + if (!root) return; + + function createLiveBrowserDomHelpers({ + prefix, + skipTags, + document: doc = root.document, + css = root.CSS, + crypto = root.crypto, + } = {}) { + if (!prefix) throw new Error('prefix required'); + if (!doc) throw new Error('document required'); + const tagsToSkip = skipTags || new Set(); + + function own(el) { + return el && (el.id?.startsWith(prefix) || el.closest?.('[id^="' + prefix + '"]')); + } + + function pickable(el) { + if (!el || el.nodeType !== 1) return false; + if (tagsToSkip.has(String(el.tagName || '').toLowerCase())) return false; + if (own(el)) return false; + const r = el.getBoundingClientRect(); + return r.width >= 20 && r.height >= 20; + } + + function desc(el) { + if (!el) return ''; + let s = el.tagName.toLowerCase(); + if (el.id) s += '#' + el.id; + else if (el.classList.length) s += '.' + [...el.classList].slice(0, 2).join('.'); + return s; + } + + function rectIsUsableAnchor(rect) { + return !!rect && rect.width > 0.5 && rect.height > 0.5; + } + + function makeFrozenAnchor(el) { + if (!el || !el.getBoundingClientRect) return null; + const r = el.getBoundingClientRect(); + if (!rectIsUsableAnchor(r)) return null; + const rect = { + x: r.x, y: r.y, + top: r.top, left: r.left, + right: r.right, bottom: r.bottom, + width: r.width, height: r.height, + }; + return { + __impeccableFrozenAnchor: true, + tagName: el.tagName || 'DIV', + id: el.id || '', + classList: el.classList ? [...el.classList] : [], + hasAttribute: () => false, + getBoundingClientRect: () => rect, + }; + } + + function id8() { + if (crypto?.randomUUID) return crypto.randomUUID().replace(/-/g, '').slice(0, 8); + return (Math.random().toString(16).slice(2) + Date.now().toString(16)).slice(0, 8); + } + + function cssId(id) { + if (css?.escape) return css.escape(id); + return String(id).replace(/([ !"#$%&'()*+,./:;<=>?@[\\\]^`{|}~])/g, '\\$1'); + } + + function liveUiRoot() { + const uiRoot = root.__IMPECCABLE_LIVE_UI_ROOT__; + if (uiRoot && typeof uiRoot.appendChild === 'function') return uiRoot; + return doc.body; + } + + function uiAppend(el) { + liveUiRoot().appendChild(el); + return el; + } + + function uiAppendStyle(styleEl) { + const uiRoot = liveUiRoot(); + if (uiRoot && uiRoot !== doc.body) uiRoot.appendChild(styleEl); + else doc.head.appendChild(styleEl); + return styleEl; + } + + function uiGetById(id) { + const uiRoot = liveUiRoot(); + if (uiRoot?.getElementById) { + const found = uiRoot.getElementById(id); + if (found) return found; + } + if (uiRoot?.querySelector) { + const found = uiRoot.querySelector('#' + cssId(id)); + if (found) return found; + } + return doc.getElementById(id); + } + + function activeElementDeep() { + let active = doc.activeElement; + while (active?.shadowRoot?.activeElement) active = active.shadowRoot.activeElement; + return active; + } + + function defangOutsideHandlers(rootEl, { setPointerEvents = true } = {}) { + if (!rootEl) return; + if (setPointerEvents) { + rootEl.style.setProperty('pointer-events', 'auto', 'important'); + } + const stop = (e) => e.stopPropagation(); + rootEl.addEventListener('pointerdown', stop); + rootEl.addEventListener('mousedown', stop); + rootEl.addEventListener('focusin', stop); + } + + return { + own, + pickable, + desc, + rectIsUsableAnchor, + makeFrozenAnchor, + id8, + cssId, + liveUiRoot, + uiAppend, + uiAppendStyle, + uiGetById, + activeElementDeep, + defangOutsideHandlers, + }; + } + + root.__IMPECCABLE_LIVE_DOM__ = { + version: 1, + createLiveBrowserDomHelpers, + }; +})(typeof window !== 'undefined' ? window : globalThis); diff --git a/.claude/skills/impeccable/scripts/live-browser.js b/.claude/skills/impeccable/scripts/live-browser.js index 9c4b6c21e..9092338eb 100644 --- a/.claude/skills/impeccable/scripts/live-browser.js +++ b/.claude/skills/impeccable/scripts/live-browser.js @@ -195,93 +195,31 @@ // Helpers // - function own(el) { - return el && (el.id?.startsWith(PREFIX) || el.closest?.('[id^="' + PREFIX + '"]')); - } - - function pickable(el) { - if (!el || el.nodeType !== 1) return false; - if (SKIP_TAGS.has(el.tagName.toLowerCase())) return false; - if (own(el)) return false; - const r = el.getBoundingClientRect(); - return r.width >= 20 && r.height >= 20; - } - - function desc(el) { - if (!el) return ''; - let s = el.tagName.toLowerCase(); - if (el.id) s += '#' + el.id; - else if (el.classList.length) s += '.' + [...el.classList].slice(0, 2).join('.'); - return s; - } - - function rectIsUsableAnchor(rect) { - return !!rect && rect.width > 0.5 && rect.height > 0.5; - } - - function makeFrozenAnchor(el) { - if (!el || !el.getBoundingClientRect) return null; - const r = el.getBoundingClientRect(); - if (!rectIsUsableAnchor(r)) return null; - const rect = { - x: r.x, y: r.y, - top: r.top, left: r.left, - right: r.right, bottom: r.bottom, - width: r.width, height: r.height, - }; - return { - __impeccableFrozenAnchor: true, - tagName: el.tagName || 'DIV', - id: el.id || '', - classList: el.classList ? [...el.classList] : [], - hasAttribute: () => false, - getBoundingClientRect: () => rect, - }; - } - - function id8() { return crypto.randomUUID().replace(/-/g, '').slice(0, 8); } - - function cssId(id) { - if (window.CSS?.escape) return CSS.escape(id); - return String(id).replace(/([ !"#$%&'()*+,./:;<=>?@[\\\]^`{|}~])/g, '\\$1'); - } - - function liveUiRoot() { - const root = window.__IMPECCABLE_LIVE_UI_ROOT__; - if (root && typeof root.appendChild === 'function') return root; - return document.body; - } - - function uiAppend(el) { - liveUiRoot().appendChild(el); - return el; - } - - function uiAppendStyle(styleEl) { - const root = liveUiRoot(); - if (root && root !== document.body) root.appendChild(styleEl); - else document.head.appendChild(styleEl); - return styleEl; - } - - function uiGetById(id) { - const root = liveUiRoot(); - if (root?.getElementById) { - const found = root.getElementById(id); - if (found) return found; - } - if (root?.querySelector) { - const found = root.querySelector('#' + cssId(id)); - if (found) return found; - } - return document.getElementById(id); - } - - function activeElementDeep() { - let active = document.activeElement; - while (active?.shadowRoot?.activeElement) active = active.shadowRoot.activeElement; - return active; + const domHelpers = window.__IMPECCABLE_LIVE_DOM__?.createLiveBrowserDomHelpers({ + prefix: PREFIX, + skipTags: SKIP_TAGS, + document, + }); + if (!domHelpers) { + console.error('[impeccable] live-browser-dom.js was not loaded. Live mode cannot start safely.'); + window.__IMPECCABLE_LIVE_INIT__ = false; + return; } + const { + own, + pickable, + desc, + rectIsUsableAnchor, + makeFrozenAnchor, + id8, + cssId, + liveUiRoot, + uiAppend, + uiAppendStyle, + uiGetById, + activeElementDeep, + defangOutsideHandlers, + } = domHelpers; window.__IMPECCABLE_LIVE_CHROME_CORE__ = { version: 1, @@ -314,45 +252,6 @@ }), }; - // Modal-aware chrome: keep our floating UI clickable inside Radix / - // Headless UI / vaul portals. - // - // Two host-page behaviors break us when the picked element lives inside a - // modal dialog: - // - // 1. Modal scroll-lock disables outside pointer events. Radix's - // `DismissableLayer` sets `document.body.style.pointerEvents = 'none'` - // while a modal is open and only restores `auto` on the layer. Our - // chrome inherits `none` from and becomes unclickable. - // 2. The dialog's outside-interaction handler (Radix's - // `usePointerDownOutside`) listens at document level and dismisses - // the dialog whenever a `pointerdown` lands outside the layer node. - // Our chrome is a sibling of , so Radix classifies our clicks - // as outside and tears the dialog down mid-task. - // - // We can't reliably re-parent our chrome into the dialog subtree (z-index - // stacking, scroll containers, theming all become host-page concerns), so - // we defang both behaviors at our root: - // - // - `pointer-events: auto !important` overrides the inherited `none`. - // - Stop `pointerdown` / `mousedown` propagation so the document-level - // dismiss listener never fires for our clicks. - // - Stop `focusin` propagation so any focus shifts inside our chrome - // don't read as "focus moved outside the dialog" to focus traps. - // - // Click events still bubble normally - only the early pointer/focus - // signals that drive outside-interaction detection are silenced. - function defangOutsideHandlers(rootEl, { setPointerEvents = true } = {}) { - if (!rootEl) return; - if (setPointerEvents) { - rootEl.style.setProperty('pointer-events', 'auto', 'important'); - } - const stop = (e) => e.stopPropagation(); - rootEl.addEventListener('pointerdown', stop); - rootEl.addEventListener('mousedown', stop); - rootEl.addEventListener('focusin', stop); - } - // // Highlight overlay // diff --git a/.claude/skills/impeccable/scripts/live/browser-script-parts.mjs b/.claude/skills/impeccable/scripts/live/browser-script-parts.mjs index f99704aff..9229e34f8 100644 --- a/.claude/skills/impeccable/scripts/live/browser-script-parts.mjs +++ b/.claude/skills/impeccable/scripts/live/browser-script-parts.mjs @@ -3,6 +3,7 @@ import path from 'node:path'; export const LIVE_BROWSER_SCRIPT_PARTS = Object.freeze([ Object.freeze({ name: 'session-state', file: 'live-browser-session.js' }), + Object.freeze({ name: 'dom-helpers', file: 'live-browser-dom.js' }), Object.freeze({ name: 'browser-ui', file: 'live-browser.js' }), ]); diff --git a/.cursor/skills/impeccable/scripts/live-browser-dom.js b/.cursor/skills/impeccable/scripts/live-browser-dom.js new file mode 100644 index 000000000..ad6a794b3 --- /dev/null +++ b/.cursor/skills/impeccable/scripts/live-browser-dom.js @@ -0,0 +1,146 @@ +/** + * Browser-side DOM helpers for Impeccable live mode. + * + * Kept separate from live-browser.js so future browser script parts can share + * chrome mounting, lookup, focus, and picker helpers without depending on the + * full overlay UI bundle. + */ +(function (root) { + 'use strict'; + if (!root) return; + + function createLiveBrowserDomHelpers({ + prefix, + skipTags, + document: doc = root.document, + css = root.CSS, + crypto = root.crypto, + } = {}) { + if (!prefix) throw new Error('prefix required'); + if (!doc) throw new Error('document required'); + const tagsToSkip = skipTags || new Set(); + + function own(el) { + return el && (el.id?.startsWith(prefix) || el.closest?.('[id^="' + prefix + '"]')); + } + + function pickable(el) { + if (!el || el.nodeType !== 1) return false; + if (tagsToSkip.has(String(el.tagName || '').toLowerCase())) return false; + if (own(el)) return false; + const r = el.getBoundingClientRect(); + return r.width >= 20 && r.height >= 20; + } + + function desc(el) { + if (!el) return ''; + let s = el.tagName.toLowerCase(); + if (el.id) s += '#' + el.id; + else if (el.classList.length) s += '.' + [...el.classList].slice(0, 2).join('.'); + return s; + } + + function rectIsUsableAnchor(rect) { + return !!rect && rect.width > 0.5 && rect.height > 0.5; + } + + function makeFrozenAnchor(el) { + if (!el || !el.getBoundingClientRect) return null; + const r = el.getBoundingClientRect(); + if (!rectIsUsableAnchor(r)) return null; + const rect = { + x: r.x, y: r.y, + top: r.top, left: r.left, + right: r.right, bottom: r.bottom, + width: r.width, height: r.height, + }; + return { + __impeccableFrozenAnchor: true, + tagName: el.tagName || 'DIV', + id: el.id || '', + classList: el.classList ? [...el.classList] : [], + hasAttribute: () => false, + getBoundingClientRect: () => rect, + }; + } + + function id8() { + if (crypto?.randomUUID) return crypto.randomUUID().replace(/-/g, '').slice(0, 8); + return (Math.random().toString(16).slice(2) + Date.now().toString(16)).slice(0, 8); + } + + function cssId(id) { + if (css?.escape) return css.escape(id); + return String(id).replace(/([ !"#$%&'()*+,./:;<=>?@[\\\]^`{|}~])/g, '\\$1'); + } + + function liveUiRoot() { + const uiRoot = root.__IMPECCABLE_LIVE_UI_ROOT__; + if (uiRoot && typeof uiRoot.appendChild === 'function') return uiRoot; + return doc.body; + } + + function uiAppend(el) { + liveUiRoot().appendChild(el); + return el; + } + + function uiAppendStyle(styleEl) { + const uiRoot = liveUiRoot(); + if (uiRoot && uiRoot !== doc.body) uiRoot.appendChild(styleEl); + else doc.head.appendChild(styleEl); + return styleEl; + } + + function uiGetById(id) { + const uiRoot = liveUiRoot(); + if (uiRoot?.getElementById) { + const found = uiRoot.getElementById(id); + if (found) return found; + } + if (uiRoot?.querySelector) { + const found = uiRoot.querySelector('#' + cssId(id)); + if (found) return found; + } + return doc.getElementById(id); + } + + function activeElementDeep() { + let active = doc.activeElement; + while (active?.shadowRoot?.activeElement) active = active.shadowRoot.activeElement; + return active; + } + + function defangOutsideHandlers(rootEl, { setPointerEvents = true } = {}) { + if (!rootEl) return; + if (setPointerEvents) { + rootEl.style.setProperty('pointer-events', 'auto', 'important'); + } + const stop = (e) => e.stopPropagation(); + rootEl.addEventListener('pointerdown', stop); + rootEl.addEventListener('mousedown', stop); + rootEl.addEventListener('focusin', stop); + } + + return { + own, + pickable, + desc, + rectIsUsableAnchor, + makeFrozenAnchor, + id8, + cssId, + liveUiRoot, + uiAppend, + uiAppendStyle, + uiGetById, + activeElementDeep, + defangOutsideHandlers, + }; + } + + root.__IMPECCABLE_LIVE_DOM__ = { + version: 1, + createLiveBrowserDomHelpers, + }; +})(typeof window !== 'undefined' ? window : globalThis); diff --git a/.cursor/skills/impeccable/scripts/live-browser.js b/.cursor/skills/impeccable/scripts/live-browser.js index 9c4b6c21e..9092338eb 100644 --- a/.cursor/skills/impeccable/scripts/live-browser.js +++ b/.cursor/skills/impeccable/scripts/live-browser.js @@ -195,93 +195,31 @@ // Helpers // - function own(el) { - return el && (el.id?.startsWith(PREFIX) || el.closest?.('[id^="' + PREFIX + '"]')); - } - - function pickable(el) { - if (!el || el.nodeType !== 1) return false; - if (SKIP_TAGS.has(el.tagName.toLowerCase())) return false; - if (own(el)) return false; - const r = el.getBoundingClientRect(); - return r.width >= 20 && r.height >= 20; - } - - function desc(el) { - if (!el) return ''; - let s = el.tagName.toLowerCase(); - if (el.id) s += '#' + el.id; - else if (el.classList.length) s += '.' + [...el.classList].slice(0, 2).join('.'); - return s; - } - - function rectIsUsableAnchor(rect) { - return !!rect && rect.width > 0.5 && rect.height > 0.5; - } - - function makeFrozenAnchor(el) { - if (!el || !el.getBoundingClientRect) return null; - const r = el.getBoundingClientRect(); - if (!rectIsUsableAnchor(r)) return null; - const rect = { - x: r.x, y: r.y, - top: r.top, left: r.left, - right: r.right, bottom: r.bottom, - width: r.width, height: r.height, - }; - return { - __impeccableFrozenAnchor: true, - tagName: el.tagName || 'DIV', - id: el.id || '', - classList: el.classList ? [...el.classList] : [], - hasAttribute: () => false, - getBoundingClientRect: () => rect, - }; - } - - function id8() { return crypto.randomUUID().replace(/-/g, '').slice(0, 8); } - - function cssId(id) { - if (window.CSS?.escape) return CSS.escape(id); - return String(id).replace(/([ !"#$%&'()*+,./:;<=>?@[\\\]^`{|}~])/g, '\\$1'); - } - - function liveUiRoot() { - const root = window.__IMPECCABLE_LIVE_UI_ROOT__; - if (root && typeof root.appendChild === 'function') return root; - return document.body; - } - - function uiAppend(el) { - liveUiRoot().appendChild(el); - return el; - } - - function uiAppendStyle(styleEl) { - const root = liveUiRoot(); - if (root && root !== document.body) root.appendChild(styleEl); - else document.head.appendChild(styleEl); - return styleEl; - } - - function uiGetById(id) { - const root = liveUiRoot(); - if (root?.getElementById) { - const found = root.getElementById(id); - if (found) return found; - } - if (root?.querySelector) { - const found = root.querySelector('#' + cssId(id)); - if (found) return found; - } - return document.getElementById(id); - } - - function activeElementDeep() { - let active = document.activeElement; - while (active?.shadowRoot?.activeElement) active = active.shadowRoot.activeElement; - return active; + const domHelpers = window.__IMPECCABLE_LIVE_DOM__?.createLiveBrowserDomHelpers({ + prefix: PREFIX, + skipTags: SKIP_TAGS, + document, + }); + if (!domHelpers) { + console.error('[impeccable] live-browser-dom.js was not loaded. Live mode cannot start safely.'); + window.__IMPECCABLE_LIVE_INIT__ = false; + return; } + const { + own, + pickable, + desc, + rectIsUsableAnchor, + makeFrozenAnchor, + id8, + cssId, + liveUiRoot, + uiAppend, + uiAppendStyle, + uiGetById, + activeElementDeep, + defangOutsideHandlers, + } = domHelpers; window.__IMPECCABLE_LIVE_CHROME_CORE__ = { version: 1, @@ -314,45 +252,6 @@ }), }; - // Modal-aware chrome: keep our floating UI clickable inside Radix / - // Headless UI / vaul portals. - // - // Two host-page behaviors break us when the picked element lives inside a - // modal dialog: - // - // 1. Modal scroll-lock disables outside pointer events. Radix's - // `DismissableLayer` sets `document.body.style.pointerEvents = 'none'` - // while a modal is open and only restores `auto` on the layer. Our - // chrome inherits `none` from and becomes unclickable. - // 2. The dialog's outside-interaction handler (Radix's - // `usePointerDownOutside`) listens at document level and dismisses - // the dialog whenever a `pointerdown` lands outside the layer node. - // Our chrome is a sibling of , so Radix classifies our clicks - // as outside and tears the dialog down mid-task. - // - // We can't reliably re-parent our chrome into the dialog subtree (z-index - // stacking, scroll containers, theming all become host-page concerns), so - // we defang both behaviors at our root: - // - // - `pointer-events: auto !important` overrides the inherited `none`. - // - Stop `pointerdown` / `mousedown` propagation so the document-level - // dismiss listener never fires for our clicks. - // - Stop `focusin` propagation so any focus shifts inside our chrome - // don't read as "focus moved outside the dialog" to focus traps. - // - // Click events still bubble normally - only the early pointer/focus - // signals that drive outside-interaction detection are silenced. - function defangOutsideHandlers(rootEl, { setPointerEvents = true } = {}) { - if (!rootEl) return; - if (setPointerEvents) { - rootEl.style.setProperty('pointer-events', 'auto', 'important'); - } - const stop = (e) => e.stopPropagation(); - rootEl.addEventListener('pointerdown', stop); - rootEl.addEventListener('mousedown', stop); - rootEl.addEventListener('focusin', stop); - } - // // Highlight overlay // diff --git a/.cursor/skills/impeccable/scripts/live/browser-script-parts.mjs b/.cursor/skills/impeccable/scripts/live/browser-script-parts.mjs index f99704aff..9229e34f8 100644 --- a/.cursor/skills/impeccable/scripts/live/browser-script-parts.mjs +++ b/.cursor/skills/impeccable/scripts/live/browser-script-parts.mjs @@ -3,6 +3,7 @@ import path from 'node:path'; export const LIVE_BROWSER_SCRIPT_PARTS = Object.freeze([ Object.freeze({ name: 'session-state', file: 'live-browser-session.js' }), + Object.freeze({ name: 'dom-helpers', file: 'live-browser-dom.js' }), Object.freeze({ name: 'browser-ui', file: 'live-browser.js' }), ]); diff --git a/.gemini/skills/impeccable/scripts/live-browser-dom.js b/.gemini/skills/impeccable/scripts/live-browser-dom.js new file mode 100644 index 000000000..ad6a794b3 --- /dev/null +++ b/.gemini/skills/impeccable/scripts/live-browser-dom.js @@ -0,0 +1,146 @@ +/** + * Browser-side DOM helpers for Impeccable live mode. + * + * Kept separate from live-browser.js so future browser script parts can share + * chrome mounting, lookup, focus, and picker helpers without depending on the + * full overlay UI bundle. + */ +(function (root) { + 'use strict'; + if (!root) return; + + function createLiveBrowserDomHelpers({ + prefix, + skipTags, + document: doc = root.document, + css = root.CSS, + crypto = root.crypto, + } = {}) { + if (!prefix) throw new Error('prefix required'); + if (!doc) throw new Error('document required'); + const tagsToSkip = skipTags || new Set(); + + function own(el) { + return el && (el.id?.startsWith(prefix) || el.closest?.('[id^="' + prefix + '"]')); + } + + function pickable(el) { + if (!el || el.nodeType !== 1) return false; + if (tagsToSkip.has(String(el.tagName || '').toLowerCase())) return false; + if (own(el)) return false; + const r = el.getBoundingClientRect(); + return r.width >= 20 && r.height >= 20; + } + + function desc(el) { + if (!el) return ''; + let s = el.tagName.toLowerCase(); + if (el.id) s += '#' + el.id; + else if (el.classList.length) s += '.' + [...el.classList].slice(0, 2).join('.'); + return s; + } + + function rectIsUsableAnchor(rect) { + return !!rect && rect.width > 0.5 && rect.height > 0.5; + } + + function makeFrozenAnchor(el) { + if (!el || !el.getBoundingClientRect) return null; + const r = el.getBoundingClientRect(); + if (!rectIsUsableAnchor(r)) return null; + const rect = { + x: r.x, y: r.y, + top: r.top, left: r.left, + right: r.right, bottom: r.bottom, + width: r.width, height: r.height, + }; + return { + __impeccableFrozenAnchor: true, + tagName: el.tagName || 'DIV', + id: el.id || '', + classList: el.classList ? [...el.classList] : [], + hasAttribute: () => false, + getBoundingClientRect: () => rect, + }; + } + + function id8() { + if (crypto?.randomUUID) return crypto.randomUUID().replace(/-/g, '').slice(0, 8); + return (Math.random().toString(16).slice(2) + Date.now().toString(16)).slice(0, 8); + } + + function cssId(id) { + if (css?.escape) return css.escape(id); + return String(id).replace(/([ !"#$%&'()*+,./:;<=>?@[\\\]^`{|}~])/g, '\\$1'); + } + + function liveUiRoot() { + const uiRoot = root.__IMPECCABLE_LIVE_UI_ROOT__; + if (uiRoot && typeof uiRoot.appendChild === 'function') return uiRoot; + return doc.body; + } + + function uiAppend(el) { + liveUiRoot().appendChild(el); + return el; + } + + function uiAppendStyle(styleEl) { + const uiRoot = liveUiRoot(); + if (uiRoot && uiRoot !== doc.body) uiRoot.appendChild(styleEl); + else doc.head.appendChild(styleEl); + return styleEl; + } + + function uiGetById(id) { + const uiRoot = liveUiRoot(); + if (uiRoot?.getElementById) { + const found = uiRoot.getElementById(id); + if (found) return found; + } + if (uiRoot?.querySelector) { + const found = uiRoot.querySelector('#' + cssId(id)); + if (found) return found; + } + return doc.getElementById(id); + } + + function activeElementDeep() { + let active = doc.activeElement; + while (active?.shadowRoot?.activeElement) active = active.shadowRoot.activeElement; + return active; + } + + function defangOutsideHandlers(rootEl, { setPointerEvents = true } = {}) { + if (!rootEl) return; + if (setPointerEvents) { + rootEl.style.setProperty('pointer-events', 'auto', 'important'); + } + const stop = (e) => e.stopPropagation(); + rootEl.addEventListener('pointerdown', stop); + rootEl.addEventListener('mousedown', stop); + rootEl.addEventListener('focusin', stop); + } + + return { + own, + pickable, + desc, + rectIsUsableAnchor, + makeFrozenAnchor, + id8, + cssId, + liveUiRoot, + uiAppend, + uiAppendStyle, + uiGetById, + activeElementDeep, + defangOutsideHandlers, + }; + } + + root.__IMPECCABLE_LIVE_DOM__ = { + version: 1, + createLiveBrowserDomHelpers, + }; +})(typeof window !== 'undefined' ? window : globalThis); diff --git a/.gemini/skills/impeccable/scripts/live-browser.js b/.gemini/skills/impeccable/scripts/live-browser.js index 9c4b6c21e..9092338eb 100644 --- a/.gemini/skills/impeccable/scripts/live-browser.js +++ b/.gemini/skills/impeccable/scripts/live-browser.js @@ -195,93 +195,31 @@ // Helpers // - function own(el) { - return el && (el.id?.startsWith(PREFIX) || el.closest?.('[id^="' + PREFIX + '"]')); - } - - function pickable(el) { - if (!el || el.nodeType !== 1) return false; - if (SKIP_TAGS.has(el.tagName.toLowerCase())) return false; - if (own(el)) return false; - const r = el.getBoundingClientRect(); - return r.width >= 20 && r.height >= 20; - } - - function desc(el) { - if (!el) return ''; - let s = el.tagName.toLowerCase(); - if (el.id) s += '#' + el.id; - else if (el.classList.length) s += '.' + [...el.classList].slice(0, 2).join('.'); - return s; - } - - function rectIsUsableAnchor(rect) { - return !!rect && rect.width > 0.5 && rect.height > 0.5; - } - - function makeFrozenAnchor(el) { - if (!el || !el.getBoundingClientRect) return null; - const r = el.getBoundingClientRect(); - if (!rectIsUsableAnchor(r)) return null; - const rect = { - x: r.x, y: r.y, - top: r.top, left: r.left, - right: r.right, bottom: r.bottom, - width: r.width, height: r.height, - }; - return { - __impeccableFrozenAnchor: true, - tagName: el.tagName || 'DIV', - id: el.id || '', - classList: el.classList ? [...el.classList] : [], - hasAttribute: () => false, - getBoundingClientRect: () => rect, - }; - } - - function id8() { return crypto.randomUUID().replace(/-/g, '').slice(0, 8); } - - function cssId(id) { - if (window.CSS?.escape) return CSS.escape(id); - return String(id).replace(/([ !"#$%&'()*+,./:;<=>?@[\\\]^`{|}~])/g, '\\$1'); - } - - function liveUiRoot() { - const root = window.__IMPECCABLE_LIVE_UI_ROOT__; - if (root && typeof root.appendChild === 'function') return root; - return document.body; - } - - function uiAppend(el) { - liveUiRoot().appendChild(el); - return el; - } - - function uiAppendStyle(styleEl) { - const root = liveUiRoot(); - if (root && root !== document.body) root.appendChild(styleEl); - else document.head.appendChild(styleEl); - return styleEl; - } - - function uiGetById(id) { - const root = liveUiRoot(); - if (root?.getElementById) { - const found = root.getElementById(id); - if (found) return found; - } - if (root?.querySelector) { - const found = root.querySelector('#' + cssId(id)); - if (found) return found; - } - return document.getElementById(id); - } - - function activeElementDeep() { - let active = document.activeElement; - while (active?.shadowRoot?.activeElement) active = active.shadowRoot.activeElement; - return active; + const domHelpers = window.__IMPECCABLE_LIVE_DOM__?.createLiveBrowserDomHelpers({ + prefix: PREFIX, + skipTags: SKIP_TAGS, + document, + }); + if (!domHelpers) { + console.error('[impeccable] live-browser-dom.js was not loaded. Live mode cannot start safely.'); + window.__IMPECCABLE_LIVE_INIT__ = false; + return; } + const { + own, + pickable, + desc, + rectIsUsableAnchor, + makeFrozenAnchor, + id8, + cssId, + liveUiRoot, + uiAppend, + uiAppendStyle, + uiGetById, + activeElementDeep, + defangOutsideHandlers, + } = domHelpers; window.__IMPECCABLE_LIVE_CHROME_CORE__ = { version: 1, @@ -314,45 +252,6 @@ }), }; - // Modal-aware chrome: keep our floating UI clickable inside Radix / - // Headless UI / vaul portals. - // - // Two host-page behaviors break us when the picked element lives inside a - // modal dialog: - // - // 1. Modal scroll-lock disables outside pointer events. Radix's - // `DismissableLayer` sets `document.body.style.pointerEvents = 'none'` - // while a modal is open and only restores `auto` on the layer. Our - // chrome inherits `none` from and becomes unclickable. - // 2. The dialog's outside-interaction handler (Radix's - // `usePointerDownOutside`) listens at document level and dismisses - // the dialog whenever a `pointerdown` lands outside the layer node. - // Our chrome is a sibling of , so Radix classifies our clicks - // as outside and tears the dialog down mid-task. - // - // We can't reliably re-parent our chrome into the dialog subtree (z-index - // stacking, scroll containers, theming all become host-page concerns), so - // we defang both behaviors at our root: - // - // - `pointer-events: auto !important` overrides the inherited `none`. - // - Stop `pointerdown` / `mousedown` propagation so the document-level - // dismiss listener never fires for our clicks. - // - Stop `focusin` propagation so any focus shifts inside our chrome - // don't read as "focus moved outside the dialog" to focus traps. - // - // Click events still bubble normally - only the early pointer/focus - // signals that drive outside-interaction detection are silenced. - function defangOutsideHandlers(rootEl, { setPointerEvents = true } = {}) { - if (!rootEl) return; - if (setPointerEvents) { - rootEl.style.setProperty('pointer-events', 'auto', 'important'); - } - const stop = (e) => e.stopPropagation(); - rootEl.addEventListener('pointerdown', stop); - rootEl.addEventListener('mousedown', stop); - rootEl.addEventListener('focusin', stop); - } - // // Highlight overlay // diff --git a/.gemini/skills/impeccable/scripts/live/browser-script-parts.mjs b/.gemini/skills/impeccable/scripts/live/browser-script-parts.mjs index f99704aff..9229e34f8 100644 --- a/.gemini/skills/impeccable/scripts/live/browser-script-parts.mjs +++ b/.gemini/skills/impeccable/scripts/live/browser-script-parts.mjs @@ -3,6 +3,7 @@ import path from 'node:path'; export const LIVE_BROWSER_SCRIPT_PARTS = Object.freeze([ Object.freeze({ name: 'session-state', file: 'live-browser-session.js' }), + Object.freeze({ name: 'dom-helpers', file: 'live-browser-dom.js' }), Object.freeze({ name: 'browser-ui', file: 'live-browser.js' }), ]); diff --git a/.github/skills/impeccable/scripts/live-browser-dom.js b/.github/skills/impeccable/scripts/live-browser-dom.js new file mode 100644 index 000000000..ad6a794b3 --- /dev/null +++ b/.github/skills/impeccable/scripts/live-browser-dom.js @@ -0,0 +1,146 @@ +/** + * Browser-side DOM helpers for Impeccable live mode. + * + * Kept separate from live-browser.js so future browser script parts can share + * chrome mounting, lookup, focus, and picker helpers without depending on the + * full overlay UI bundle. + */ +(function (root) { + 'use strict'; + if (!root) return; + + function createLiveBrowserDomHelpers({ + prefix, + skipTags, + document: doc = root.document, + css = root.CSS, + crypto = root.crypto, + } = {}) { + if (!prefix) throw new Error('prefix required'); + if (!doc) throw new Error('document required'); + const tagsToSkip = skipTags || new Set(); + + function own(el) { + return el && (el.id?.startsWith(prefix) || el.closest?.('[id^="' + prefix + '"]')); + } + + function pickable(el) { + if (!el || el.nodeType !== 1) return false; + if (tagsToSkip.has(String(el.tagName || '').toLowerCase())) return false; + if (own(el)) return false; + const r = el.getBoundingClientRect(); + return r.width >= 20 && r.height >= 20; + } + + function desc(el) { + if (!el) return ''; + let s = el.tagName.toLowerCase(); + if (el.id) s += '#' + el.id; + else if (el.classList.length) s += '.' + [...el.classList].slice(0, 2).join('.'); + return s; + } + + function rectIsUsableAnchor(rect) { + return !!rect && rect.width > 0.5 && rect.height > 0.5; + } + + function makeFrozenAnchor(el) { + if (!el || !el.getBoundingClientRect) return null; + const r = el.getBoundingClientRect(); + if (!rectIsUsableAnchor(r)) return null; + const rect = { + x: r.x, y: r.y, + top: r.top, left: r.left, + right: r.right, bottom: r.bottom, + width: r.width, height: r.height, + }; + return { + __impeccableFrozenAnchor: true, + tagName: el.tagName || 'DIV', + id: el.id || '', + classList: el.classList ? [...el.classList] : [], + hasAttribute: () => false, + getBoundingClientRect: () => rect, + }; + } + + function id8() { + if (crypto?.randomUUID) return crypto.randomUUID().replace(/-/g, '').slice(0, 8); + return (Math.random().toString(16).slice(2) + Date.now().toString(16)).slice(0, 8); + } + + function cssId(id) { + if (css?.escape) return css.escape(id); + return String(id).replace(/([ !"#$%&'()*+,./:;<=>?@[\\\]^`{|}~])/g, '\\$1'); + } + + function liveUiRoot() { + const uiRoot = root.__IMPECCABLE_LIVE_UI_ROOT__; + if (uiRoot && typeof uiRoot.appendChild === 'function') return uiRoot; + return doc.body; + } + + function uiAppend(el) { + liveUiRoot().appendChild(el); + return el; + } + + function uiAppendStyle(styleEl) { + const uiRoot = liveUiRoot(); + if (uiRoot && uiRoot !== doc.body) uiRoot.appendChild(styleEl); + else doc.head.appendChild(styleEl); + return styleEl; + } + + function uiGetById(id) { + const uiRoot = liveUiRoot(); + if (uiRoot?.getElementById) { + const found = uiRoot.getElementById(id); + if (found) return found; + } + if (uiRoot?.querySelector) { + const found = uiRoot.querySelector('#' + cssId(id)); + if (found) return found; + } + return doc.getElementById(id); + } + + function activeElementDeep() { + let active = doc.activeElement; + while (active?.shadowRoot?.activeElement) active = active.shadowRoot.activeElement; + return active; + } + + function defangOutsideHandlers(rootEl, { setPointerEvents = true } = {}) { + if (!rootEl) return; + if (setPointerEvents) { + rootEl.style.setProperty('pointer-events', 'auto', 'important'); + } + const stop = (e) => e.stopPropagation(); + rootEl.addEventListener('pointerdown', stop); + rootEl.addEventListener('mousedown', stop); + rootEl.addEventListener('focusin', stop); + } + + return { + own, + pickable, + desc, + rectIsUsableAnchor, + makeFrozenAnchor, + id8, + cssId, + liveUiRoot, + uiAppend, + uiAppendStyle, + uiGetById, + activeElementDeep, + defangOutsideHandlers, + }; + } + + root.__IMPECCABLE_LIVE_DOM__ = { + version: 1, + createLiveBrowserDomHelpers, + }; +})(typeof window !== 'undefined' ? window : globalThis); diff --git a/.github/skills/impeccable/scripts/live-browser.js b/.github/skills/impeccable/scripts/live-browser.js index 9c4b6c21e..9092338eb 100644 --- a/.github/skills/impeccable/scripts/live-browser.js +++ b/.github/skills/impeccable/scripts/live-browser.js @@ -195,93 +195,31 @@ // Helpers // - function own(el) { - return el && (el.id?.startsWith(PREFIX) || el.closest?.('[id^="' + PREFIX + '"]')); - } - - function pickable(el) { - if (!el || el.nodeType !== 1) return false; - if (SKIP_TAGS.has(el.tagName.toLowerCase())) return false; - if (own(el)) return false; - const r = el.getBoundingClientRect(); - return r.width >= 20 && r.height >= 20; - } - - function desc(el) { - if (!el) return ''; - let s = el.tagName.toLowerCase(); - if (el.id) s += '#' + el.id; - else if (el.classList.length) s += '.' + [...el.classList].slice(0, 2).join('.'); - return s; - } - - function rectIsUsableAnchor(rect) { - return !!rect && rect.width > 0.5 && rect.height > 0.5; - } - - function makeFrozenAnchor(el) { - if (!el || !el.getBoundingClientRect) return null; - const r = el.getBoundingClientRect(); - if (!rectIsUsableAnchor(r)) return null; - const rect = { - x: r.x, y: r.y, - top: r.top, left: r.left, - right: r.right, bottom: r.bottom, - width: r.width, height: r.height, - }; - return { - __impeccableFrozenAnchor: true, - tagName: el.tagName || 'DIV', - id: el.id || '', - classList: el.classList ? [...el.classList] : [], - hasAttribute: () => false, - getBoundingClientRect: () => rect, - }; - } - - function id8() { return crypto.randomUUID().replace(/-/g, '').slice(0, 8); } - - function cssId(id) { - if (window.CSS?.escape) return CSS.escape(id); - return String(id).replace(/([ !"#$%&'()*+,./:;<=>?@[\\\]^`{|}~])/g, '\\$1'); - } - - function liveUiRoot() { - const root = window.__IMPECCABLE_LIVE_UI_ROOT__; - if (root && typeof root.appendChild === 'function') return root; - return document.body; - } - - function uiAppend(el) { - liveUiRoot().appendChild(el); - return el; - } - - function uiAppendStyle(styleEl) { - const root = liveUiRoot(); - if (root && root !== document.body) root.appendChild(styleEl); - else document.head.appendChild(styleEl); - return styleEl; - } - - function uiGetById(id) { - const root = liveUiRoot(); - if (root?.getElementById) { - const found = root.getElementById(id); - if (found) return found; - } - if (root?.querySelector) { - const found = root.querySelector('#' + cssId(id)); - if (found) return found; - } - return document.getElementById(id); - } - - function activeElementDeep() { - let active = document.activeElement; - while (active?.shadowRoot?.activeElement) active = active.shadowRoot.activeElement; - return active; + const domHelpers = window.__IMPECCABLE_LIVE_DOM__?.createLiveBrowserDomHelpers({ + prefix: PREFIX, + skipTags: SKIP_TAGS, + document, + }); + if (!domHelpers) { + console.error('[impeccable] live-browser-dom.js was not loaded. Live mode cannot start safely.'); + window.__IMPECCABLE_LIVE_INIT__ = false; + return; } + const { + own, + pickable, + desc, + rectIsUsableAnchor, + makeFrozenAnchor, + id8, + cssId, + liveUiRoot, + uiAppend, + uiAppendStyle, + uiGetById, + activeElementDeep, + defangOutsideHandlers, + } = domHelpers; window.__IMPECCABLE_LIVE_CHROME_CORE__ = { version: 1, @@ -314,45 +252,6 @@ }), }; - // Modal-aware chrome: keep our floating UI clickable inside Radix / - // Headless UI / vaul portals. - // - // Two host-page behaviors break us when the picked element lives inside a - // modal dialog: - // - // 1. Modal scroll-lock disables outside pointer events. Radix's - // `DismissableLayer` sets `document.body.style.pointerEvents = 'none'` - // while a modal is open and only restores `auto` on the layer. Our - // chrome inherits `none` from and becomes unclickable. - // 2. The dialog's outside-interaction handler (Radix's - // `usePointerDownOutside`) listens at document level and dismisses - // the dialog whenever a `pointerdown` lands outside the layer node. - // Our chrome is a sibling of , so Radix classifies our clicks - // as outside and tears the dialog down mid-task. - // - // We can't reliably re-parent our chrome into the dialog subtree (z-index - // stacking, scroll containers, theming all become host-page concerns), so - // we defang both behaviors at our root: - // - // - `pointer-events: auto !important` overrides the inherited `none`. - // - Stop `pointerdown` / `mousedown` propagation so the document-level - // dismiss listener never fires for our clicks. - // - Stop `focusin` propagation so any focus shifts inside our chrome - // don't read as "focus moved outside the dialog" to focus traps. - // - // Click events still bubble normally - only the early pointer/focus - // signals that drive outside-interaction detection are silenced. - function defangOutsideHandlers(rootEl, { setPointerEvents = true } = {}) { - if (!rootEl) return; - if (setPointerEvents) { - rootEl.style.setProperty('pointer-events', 'auto', 'important'); - } - const stop = (e) => e.stopPropagation(); - rootEl.addEventListener('pointerdown', stop); - rootEl.addEventListener('mousedown', stop); - rootEl.addEventListener('focusin', stop); - } - // // Highlight overlay // diff --git a/.github/skills/impeccable/scripts/live/browser-script-parts.mjs b/.github/skills/impeccable/scripts/live/browser-script-parts.mjs index f99704aff..9229e34f8 100644 --- a/.github/skills/impeccable/scripts/live/browser-script-parts.mjs +++ b/.github/skills/impeccable/scripts/live/browser-script-parts.mjs @@ -3,6 +3,7 @@ import path from 'node:path'; export const LIVE_BROWSER_SCRIPT_PARTS = Object.freeze([ Object.freeze({ name: 'session-state', file: 'live-browser-session.js' }), + Object.freeze({ name: 'dom-helpers', file: 'live-browser-dom.js' }), Object.freeze({ name: 'browser-ui', file: 'live-browser.js' }), ]); diff --git a/.kiro/skills/impeccable/scripts/live-browser-dom.js b/.kiro/skills/impeccable/scripts/live-browser-dom.js new file mode 100644 index 000000000..ad6a794b3 --- /dev/null +++ b/.kiro/skills/impeccable/scripts/live-browser-dom.js @@ -0,0 +1,146 @@ +/** + * Browser-side DOM helpers for Impeccable live mode. + * + * Kept separate from live-browser.js so future browser script parts can share + * chrome mounting, lookup, focus, and picker helpers without depending on the + * full overlay UI bundle. + */ +(function (root) { + 'use strict'; + if (!root) return; + + function createLiveBrowserDomHelpers({ + prefix, + skipTags, + document: doc = root.document, + css = root.CSS, + crypto = root.crypto, + } = {}) { + if (!prefix) throw new Error('prefix required'); + if (!doc) throw new Error('document required'); + const tagsToSkip = skipTags || new Set(); + + function own(el) { + return el && (el.id?.startsWith(prefix) || el.closest?.('[id^="' + prefix + '"]')); + } + + function pickable(el) { + if (!el || el.nodeType !== 1) return false; + if (tagsToSkip.has(String(el.tagName || '').toLowerCase())) return false; + if (own(el)) return false; + const r = el.getBoundingClientRect(); + return r.width >= 20 && r.height >= 20; + } + + function desc(el) { + if (!el) return ''; + let s = el.tagName.toLowerCase(); + if (el.id) s += '#' + el.id; + else if (el.classList.length) s += '.' + [...el.classList].slice(0, 2).join('.'); + return s; + } + + function rectIsUsableAnchor(rect) { + return !!rect && rect.width > 0.5 && rect.height > 0.5; + } + + function makeFrozenAnchor(el) { + if (!el || !el.getBoundingClientRect) return null; + const r = el.getBoundingClientRect(); + if (!rectIsUsableAnchor(r)) return null; + const rect = { + x: r.x, y: r.y, + top: r.top, left: r.left, + right: r.right, bottom: r.bottom, + width: r.width, height: r.height, + }; + return { + __impeccableFrozenAnchor: true, + tagName: el.tagName || 'DIV', + id: el.id || '', + classList: el.classList ? [...el.classList] : [], + hasAttribute: () => false, + getBoundingClientRect: () => rect, + }; + } + + function id8() { + if (crypto?.randomUUID) return crypto.randomUUID().replace(/-/g, '').slice(0, 8); + return (Math.random().toString(16).slice(2) + Date.now().toString(16)).slice(0, 8); + } + + function cssId(id) { + if (css?.escape) return css.escape(id); + return String(id).replace(/([ !"#$%&'()*+,./:;<=>?@[\\\]^`{|}~])/g, '\\$1'); + } + + function liveUiRoot() { + const uiRoot = root.__IMPECCABLE_LIVE_UI_ROOT__; + if (uiRoot && typeof uiRoot.appendChild === 'function') return uiRoot; + return doc.body; + } + + function uiAppend(el) { + liveUiRoot().appendChild(el); + return el; + } + + function uiAppendStyle(styleEl) { + const uiRoot = liveUiRoot(); + if (uiRoot && uiRoot !== doc.body) uiRoot.appendChild(styleEl); + else doc.head.appendChild(styleEl); + return styleEl; + } + + function uiGetById(id) { + const uiRoot = liveUiRoot(); + if (uiRoot?.getElementById) { + const found = uiRoot.getElementById(id); + if (found) return found; + } + if (uiRoot?.querySelector) { + const found = uiRoot.querySelector('#' + cssId(id)); + if (found) return found; + } + return doc.getElementById(id); + } + + function activeElementDeep() { + let active = doc.activeElement; + while (active?.shadowRoot?.activeElement) active = active.shadowRoot.activeElement; + return active; + } + + function defangOutsideHandlers(rootEl, { setPointerEvents = true } = {}) { + if (!rootEl) return; + if (setPointerEvents) { + rootEl.style.setProperty('pointer-events', 'auto', 'important'); + } + const stop = (e) => e.stopPropagation(); + rootEl.addEventListener('pointerdown', stop); + rootEl.addEventListener('mousedown', stop); + rootEl.addEventListener('focusin', stop); + } + + return { + own, + pickable, + desc, + rectIsUsableAnchor, + makeFrozenAnchor, + id8, + cssId, + liveUiRoot, + uiAppend, + uiAppendStyle, + uiGetById, + activeElementDeep, + defangOutsideHandlers, + }; + } + + root.__IMPECCABLE_LIVE_DOM__ = { + version: 1, + createLiveBrowserDomHelpers, + }; +})(typeof window !== 'undefined' ? window : globalThis); diff --git a/.kiro/skills/impeccable/scripts/live-browser.js b/.kiro/skills/impeccable/scripts/live-browser.js index 9c4b6c21e..9092338eb 100644 --- a/.kiro/skills/impeccable/scripts/live-browser.js +++ b/.kiro/skills/impeccable/scripts/live-browser.js @@ -195,93 +195,31 @@ // Helpers // - function own(el) { - return el && (el.id?.startsWith(PREFIX) || el.closest?.('[id^="' + PREFIX + '"]')); - } - - function pickable(el) { - if (!el || el.nodeType !== 1) return false; - if (SKIP_TAGS.has(el.tagName.toLowerCase())) return false; - if (own(el)) return false; - const r = el.getBoundingClientRect(); - return r.width >= 20 && r.height >= 20; - } - - function desc(el) { - if (!el) return ''; - let s = el.tagName.toLowerCase(); - if (el.id) s += '#' + el.id; - else if (el.classList.length) s += '.' + [...el.classList].slice(0, 2).join('.'); - return s; - } - - function rectIsUsableAnchor(rect) { - return !!rect && rect.width > 0.5 && rect.height > 0.5; - } - - function makeFrozenAnchor(el) { - if (!el || !el.getBoundingClientRect) return null; - const r = el.getBoundingClientRect(); - if (!rectIsUsableAnchor(r)) return null; - const rect = { - x: r.x, y: r.y, - top: r.top, left: r.left, - right: r.right, bottom: r.bottom, - width: r.width, height: r.height, - }; - return { - __impeccableFrozenAnchor: true, - tagName: el.tagName || 'DIV', - id: el.id || '', - classList: el.classList ? [...el.classList] : [], - hasAttribute: () => false, - getBoundingClientRect: () => rect, - }; - } - - function id8() { return crypto.randomUUID().replace(/-/g, '').slice(0, 8); } - - function cssId(id) { - if (window.CSS?.escape) return CSS.escape(id); - return String(id).replace(/([ !"#$%&'()*+,./:;<=>?@[\\\]^`{|}~])/g, '\\$1'); - } - - function liveUiRoot() { - const root = window.__IMPECCABLE_LIVE_UI_ROOT__; - if (root && typeof root.appendChild === 'function') return root; - return document.body; - } - - function uiAppend(el) { - liveUiRoot().appendChild(el); - return el; - } - - function uiAppendStyle(styleEl) { - const root = liveUiRoot(); - if (root && root !== document.body) root.appendChild(styleEl); - else document.head.appendChild(styleEl); - return styleEl; - } - - function uiGetById(id) { - const root = liveUiRoot(); - if (root?.getElementById) { - const found = root.getElementById(id); - if (found) return found; - } - if (root?.querySelector) { - const found = root.querySelector('#' + cssId(id)); - if (found) return found; - } - return document.getElementById(id); - } - - function activeElementDeep() { - let active = document.activeElement; - while (active?.shadowRoot?.activeElement) active = active.shadowRoot.activeElement; - return active; + const domHelpers = window.__IMPECCABLE_LIVE_DOM__?.createLiveBrowserDomHelpers({ + prefix: PREFIX, + skipTags: SKIP_TAGS, + document, + }); + if (!domHelpers) { + console.error('[impeccable] live-browser-dom.js was not loaded. Live mode cannot start safely.'); + window.__IMPECCABLE_LIVE_INIT__ = false; + return; } + const { + own, + pickable, + desc, + rectIsUsableAnchor, + makeFrozenAnchor, + id8, + cssId, + liveUiRoot, + uiAppend, + uiAppendStyle, + uiGetById, + activeElementDeep, + defangOutsideHandlers, + } = domHelpers; window.__IMPECCABLE_LIVE_CHROME_CORE__ = { version: 1, @@ -314,45 +252,6 @@ }), }; - // Modal-aware chrome: keep our floating UI clickable inside Radix / - // Headless UI / vaul portals. - // - // Two host-page behaviors break us when the picked element lives inside a - // modal dialog: - // - // 1. Modal scroll-lock disables outside pointer events. Radix's - // `DismissableLayer` sets `document.body.style.pointerEvents = 'none'` - // while a modal is open and only restores `auto` on the layer. Our - // chrome inherits `none` from and becomes unclickable. - // 2. The dialog's outside-interaction handler (Radix's - // `usePointerDownOutside`) listens at document level and dismisses - // the dialog whenever a `pointerdown` lands outside the layer node. - // Our chrome is a sibling of , so Radix classifies our clicks - // as outside and tears the dialog down mid-task. - // - // We can't reliably re-parent our chrome into the dialog subtree (z-index - // stacking, scroll containers, theming all become host-page concerns), so - // we defang both behaviors at our root: - // - // - `pointer-events: auto !important` overrides the inherited `none`. - // - Stop `pointerdown` / `mousedown` propagation so the document-level - // dismiss listener never fires for our clicks. - // - Stop `focusin` propagation so any focus shifts inside our chrome - // don't read as "focus moved outside the dialog" to focus traps. - // - // Click events still bubble normally - only the early pointer/focus - // signals that drive outside-interaction detection are silenced. - function defangOutsideHandlers(rootEl, { setPointerEvents = true } = {}) { - if (!rootEl) return; - if (setPointerEvents) { - rootEl.style.setProperty('pointer-events', 'auto', 'important'); - } - const stop = (e) => e.stopPropagation(); - rootEl.addEventListener('pointerdown', stop); - rootEl.addEventListener('mousedown', stop); - rootEl.addEventListener('focusin', stop); - } - // // Highlight overlay // diff --git a/.kiro/skills/impeccable/scripts/live/browser-script-parts.mjs b/.kiro/skills/impeccable/scripts/live/browser-script-parts.mjs index f99704aff..9229e34f8 100644 --- a/.kiro/skills/impeccable/scripts/live/browser-script-parts.mjs +++ b/.kiro/skills/impeccable/scripts/live/browser-script-parts.mjs @@ -3,6 +3,7 @@ import path from 'node:path'; export const LIVE_BROWSER_SCRIPT_PARTS = Object.freeze([ Object.freeze({ name: 'session-state', file: 'live-browser-session.js' }), + Object.freeze({ name: 'dom-helpers', file: 'live-browser-dom.js' }), Object.freeze({ name: 'browser-ui', file: 'live-browser.js' }), ]); diff --git a/.opencode/skills/impeccable/scripts/live-browser-dom.js b/.opencode/skills/impeccable/scripts/live-browser-dom.js new file mode 100644 index 000000000..ad6a794b3 --- /dev/null +++ b/.opencode/skills/impeccable/scripts/live-browser-dom.js @@ -0,0 +1,146 @@ +/** + * Browser-side DOM helpers for Impeccable live mode. + * + * Kept separate from live-browser.js so future browser script parts can share + * chrome mounting, lookup, focus, and picker helpers without depending on the + * full overlay UI bundle. + */ +(function (root) { + 'use strict'; + if (!root) return; + + function createLiveBrowserDomHelpers({ + prefix, + skipTags, + document: doc = root.document, + css = root.CSS, + crypto = root.crypto, + } = {}) { + if (!prefix) throw new Error('prefix required'); + if (!doc) throw new Error('document required'); + const tagsToSkip = skipTags || new Set(); + + function own(el) { + return el && (el.id?.startsWith(prefix) || el.closest?.('[id^="' + prefix + '"]')); + } + + function pickable(el) { + if (!el || el.nodeType !== 1) return false; + if (tagsToSkip.has(String(el.tagName || '').toLowerCase())) return false; + if (own(el)) return false; + const r = el.getBoundingClientRect(); + return r.width >= 20 && r.height >= 20; + } + + function desc(el) { + if (!el) return ''; + let s = el.tagName.toLowerCase(); + if (el.id) s += '#' + el.id; + else if (el.classList.length) s += '.' + [...el.classList].slice(0, 2).join('.'); + return s; + } + + function rectIsUsableAnchor(rect) { + return !!rect && rect.width > 0.5 && rect.height > 0.5; + } + + function makeFrozenAnchor(el) { + if (!el || !el.getBoundingClientRect) return null; + const r = el.getBoundingClientRect(); + if (!rectIsUsableAnchor(r)) return null; + const rect = { + x: r.x, y: r.y, + top: r.top, left: r.left, + right: r.right, bottom: r.bottom, + width: r.width, height: r.height, + }; + return { + __impeccableFrozenAnchor: true, + tagName: el.tagName || 'DIV', + id: el.id || '', + classList: el.classList ? [...el.classList] : [], + hasAttribute: () => false, + getBoundingClientRect: () => rect, + }; + } + + function id8() { + if (crypto?.randomUUID) return crypto.randomUUID().replace(/-/g, '').slice(0, 8); + return (Math.random().toString(16).slice(2) + Date.now().toString(16)).slice(0, 8); + } + + function cssId(id) { + if (css?.escape) return css.escape(id); + return String(id).replace(/([ !"#$%&'()*+,./:;<=>?@[\\\]^`{|}~])/g, '\\$1'); + } + + function liveUiRoot() { + const uiRoot = root.__IMPECCABLE_LIVE_UI_ROOT__; + if (uiRoot && typeof uiRoot.appendChild === 'function') return uiRoot; + return doc.body; + } + + function uiAppend(el) { + liveUiRoot().appendChild(el); + return el; + } + + function uiAppendStyle(styleEl) { + const uiRoot = liveUiRoot(); + if (uiRoot && uiRoot !== doc.body) uiRoot.appendChild(styleEl); + else doc.head.appendChild(styleEl); + return styleEl; + } + + function uiGetById(id) { + const uiRoot = liveUiRoot(); + if (uiRoot?.getElementById) { + const found = uiRoot.getElementById(id); + if (found) return found; + } + if (uiRoot?.querySelector) { + const found = uiRoot.querySelector('#' + cssId(id)); + if (found) return found; + } + return doc.getElementById(id); + } + + function activeElementDeep() { + let active = doc.activeElement; + while (active?.shadowRoot?.activeElement) active = active.shadowRoot.activeElement; + return active; + } + + function defangOutsideHandlers(rootEl, { setPointerEvents = true } = {}) { + if (!rootEl) return; + if (setPointerEvents) { + rootEl.style.setProperty('pointer-events', 'auto', 'important'); + } + const stop = (e) => e.stopPropagation(); + rootEl.addEventListener('pointerdown', stop); + rootEl.addEventListener('mousedown', stop); + rootEl.addEventListener('focusin', stop); + } + + return { + own, + pickable, + desc, + rectIsUsableAnchor, + makeFrozenAnchor, + id8, + cssId, + liveUiRoot, + uiAppend, + uiAppendStyle, + uiGetById, + activeElementDeep, + defangOutsideHandlers, + }; + } + + root.__IMPECCABLE_LIVE_DOM__ = { + version: 1, + createLiveBrowserDomHelpers, + }; +})(typeof window !== 'undefined' ? window : globalThis); diff --git a/.opencode/skills/impeccable/scripts/live-browser.js b/.opencode/skills/impeccable/scripts/live-browser.js index 9c4b6c21e..9092338eb 100644 --- a/.opencode/skills/impeccable/scripts/live-browser.js +++ b/.opencode/skills/impeccable/scripts/live-browser.js @@ -195,93 +195,31 @@ // Helpers // - function own(el) { - return el && (el.id?.startsWith(PREFIX) || el.closest?.('[id^="' + PREFIX + '"]')); - } - - function pickable(el) { - if (!el || el.nodeType !== 1) return false; - if (SKIP_TAGS.has(el.tagName.toLowerCase())) return false; - if (own(el)) return false; - const r = el.getBoundingClientRect(); - return r.width >= 20 && r.height >= 20; - } - - function desc(el) { - if (!el) return ''; - let s = el.tagName.toLowerCase(); - if (el.id) s += '#' + el.id; - else if (el.classList.length) s += '.' + [...el.classList].slice(0, 2).join('.'); - return s; - } - - function rectIsUsableAnchor(rect) { - return !!rect && rect.width > 0.5 && rect.height > 0.5; - } - - function makeFrozenAnchor(el) { - if (!el || !el.getBoundingClientRect) return null; - const r = el.getBoundingClientRect(); - if (!rectIsUsableAnchor(r)) return null; - const rect = { - x: r.x, y: r.y, - top: r.top, left: r.left, - right: r.right, bottom: r.bottom, - width: r.width, height: r.height, - }; - return { - __impeccableFrozenAnchor: true, - tagName: el.tagName || 'DIV', - id: el.id || '', - classList: el.classList ? [...el.classList] : [], - hasAttribute: () => false, - getBoundingClientRect: () => rect, - }; - } - - function id8() { return crypto.randomUUID().replace(/-/g, '').slice(0, 8); } - - function cssId(id) { - if (window.CSS?.escape) return CSS.escape(id); - return String(id).replace(/([ !"#$%&'()*+,./:;<=>?@[\\\]^`{|}~])/g, '\\$1'); - } - - function liveUiRoot() { - const root = window.__IMPECCABLE_LIVE_UI_ROOT__; - if (root && typeof root.appendChild === 'function') return root; - return document.body; - } - - function uiAppend(el) { - liveUiRoot().appendChild(el); - return el; - } - - function uiAppendStyle(styleEl) { - const root = liveUiRoot(); - if (root && root !== document.body) root.appendChild(styleEl); - else document.head.appendChild(styleEl); - return styleEl; - } - - function uiGetById(id) { - const root = liveUiRoot(); - if (root?.getElementById) { - const found = root.getElementById(id); - if (found) return found; - } - if (root?.querySelector) { - const found = root.querySelector('#' + cssId(id)); - if (found) return found; - } - return document.getElementById(id); - } - - function activeElementDeep() { - let active = document.activeElement; - while (active?.shadowRoot?.activeElement) active = active.shadowRoot.activeElement; - return active; + const domHelpers = window.__IMPECCABLE_LIVE_DOM__?.createLiveBrowserDomHelpers({ + prefix: PREFIX, + skipTags: SKIP_TAGS, + document, + }); + if (!domHelpers) { + console.error('[impeccable] live-browser-dom.js was not loaded. Live mode cannot start safely.'); + window.__IMPECCABLE_LIVE_INIT__ = false; + return; } + const { + own, + pickable, + desc, + rectIsUsableAnchor, + makeFrozenAnchor, + id8, + cssId, + liveUiRoot, + uiAppend, + uiAppendStyle, + uiGetById, + activeElementDeep, + defangOutsideHandlers, + } = domHelpers; window.__IMPECCABLE_LIVE_CHROME_CORE__ = { version: 1, @@ -314,45 +252,6 @@ }), }; - // Modal-aware chrome: keep our floating UI clickable inside Radix / - // Headless UI / vaul portals. - // - // Two host-page behaviors break us when the picked element lives inside a - // modal dialog: - // - // 1. Modal scroll-lock disables outside pointer events. Radix's - // `DismissableLayer` sets `document.body.style.pointerEvents = 'none'` - // while a modal is open and only restores `auto` on the layer. Our - // chrome inherits `none` from and becomes unclickable. - // 2. The dialog's outside-interaction handler (Radix's - // `usePointerDownOutside`) listens at document level and dismisses - // the dialog whenever a `pointerdown` lands outside the layer node. - // Our chrome is a sibling of , so Radix classifies our clicks - // as outside and tears the dialog down mid-task. - // - // We can't reliably re-parent our chrome into the dialog subtree (z-index - // stacking, scroll containers, theming all become host-page concerns), so - // we defang both behaviors at our root: - // - // - `pointer-events: auto !important` overrides the inherited `none`. - // - Stop `pointerdown` / `mousedown` propagation so the document-level - // dismiss listener never fires for our clicks. - // - Stop `focusin` propagation so any focus shifts inside our chrome - // don't read as "focus moved outside the dialog" to focus traps. - // - // Click events still bubble normally - only the early pointer/focus - // signals that drive outside-interaction detection are silenced. - function defangOutsideHandlers(rootEl, { setPointerEvents = true } = {}) { - if (!rootEl) return; - if (setPointerEvents) { - rootEl.style.setProperty('pointer-events', 'auto', 'important'); - } - const stop = (e) => e.stopPropagation(); - rootEl.addEventListener('pointerdown', stop); - rootEl.addEventListener('mousedown', stop); - rootEl.addEventListener('focusin', stop); - } - // // Highlight overlay // diff --git a/.opencode/skills/impeccable/scripts/live/browser-script-parts.mjs b/.opencode/skills/impeccable/scripts/live/browser-script-parts.mjs index f99704aff..9229e34f8 100644 --- a/.opencode/skills/impeccable/scripts/live/browser-script-parts.mjs +++ b/.opencode/skills/impeccable/scripts/live/browser-script-parts.mjs @@ -3,6 +3,7 @@ import path from 'node:path'; export const LIVE_BROWSER_SCRIPT_PARTS = Object.freeze([ Object.freeze({ name: 'session-state', file: 'live-browser-session.js' }), + Object.freeze({ name: 'dom-helpers', file: 'live-browser-dom.js' }), Object.freeze({ name: 'browser-ui', file: 'live-browser.js' }), ]); diff --git a/.pi/skills/impeccable/scripts/live-browser-dom.js b/.pi/skills/impeccable/scripts/live-browser-dom.js new file mode 100644 index 000000000..ad6a794b3 --- /dev/null +++ b/.pi/skills/impeccable/scripts/live-browser-dom.js @@ -0,0 +1,146 @@ +/** + * Browser-side DOM helpers for Impeccable live mode. + * + * Kept separate from live-browser.js so future browser script parts can share + * chrome mounting, lookup, focus, and picker helpers without depending on the + * full overlay UI bundle. + */ +(function (root) { + 'use strict'; + if (!root) return; + + function createLiveBrowserDomHelpers({ + prefix, + skipTags, + document: doc = root.document, + css = root.CSS, + crypto = root.crypto, + } = {}) { + if (!prefix) throw new Error('prefix required'); + if (!doc) throw new Error('document required'); + const tagsToSkip = skipTags || new Set(); + + function own(el) { + return el && (el.id?.startsWith(prefix) || el.closest?.('[id^="' + prefix + '"]')); + } + + function pickable(el) { + if (!el || el.nodeType !== 1) return false; + if (tagsToSkip.has(String(el.tagName || '').toLowerCase())) return false; + if (own(el)) return false; + const r = el.getBoundingClientRect(); + return r.width >= 20 && r.height >= 20; + } + + function desc(el) { + if (!el) return ''; + let s = el.tagName.toLowerCase(); + if (el.id) s += '#' + el.id; + else if (el.classList.length) s += '.' + [...el.classList].slice(0, 2).join('.'); + return s; + } + + function rectIsUsableAnchor(rect) { + return !!rect && rect.width > 0.5 && rect.height > 0.5; + } + + function makeFrozenAnchor(el) { + if (!el || !el.getBoundingClientRect) return null; + const r = el.getBoundingClientRect(); + if (!rectIsUsableAnchor(r)) return null; + const rect = { + x: r.x, y: r.y, + top: r.top, left: r.left, + right: r.right, bottom: r.bottom, + width: r.width, height: r.height, + }; + return { + __impeccableFrozenAnchor: true, + tagName: el.tagName || 'DIV', + id: el.id || '', + classList: el.classList ? [...el.classList] : [], + hasAttribute: () => false, + getBoundingClientRect: () => rect, + }; + } + + function id8() { + if (crypto?.randomUUID) return crypto.randomUUID().replace(/-/g, '').slice(0, 8); + return (Math.random().toString(16).slice(2) + Date.now().toString(16)).slice(0, 8); + } + + function cssId(id) { + if (css?.escape) return css.escape(id); + return String(id).replace(/([ !"#$%&'()*+,./:;<=>?@[\\\]^`{|}~])/g, '\\$1'); + } + + function liveUiRoot() { + const uiRoot = root.__IMPECCABLE_LIVE_UI_ROOT__; + if (uiRoot && typeof uiRoot.appendChild === 'function') return uiRoot; + return doc.body; + } + + function uiAppend(el) { + liveUiRoot().appendChild(el); + return el; + } + + function uiAppendStyle(styleEl) { + const uiRoot = liveUiRoot(); + if (uiRoot && uiRoot !== doc.body) uiRoot.appendChild(styleEl); + else doc.head.appendChild(styleEl); + return styleEl; + } + + function uiGetById(id) { + const uiRoot = liveUiRoot(); + if (uiRoot?.getElementById) { + const found = uiRoot.getElementById(id); + if (found) return found; + } + if (uiRoot?.querySelector) { + const found = uiRoot.querySelector('#' + cssId(id)); + if (found) return found; + } + return doc.getElementById(id); + } + + function activeElementDeep() { + let active = doc.activeElement; + while (active?.shadowRoot?.activeElement) active = active.shadowRoot.activeElement; + return active; + } + + function defangOutsideHandlers(rootEl, { setPointerEvents = true } = {}) { + if (!rootEl) return; + if (setPointerEvents) { + rootEl.style.setProperty('pointer-events', 'auto', 'important'); + } + const stop = (e) => e.stopPropagation(); + rootEl.addEventListener('pointerdown', stop); + rootEl.addEventListener('mousedown', stop); + rootEl.addEventListener('focusin', stop); + } + + return { + own, + pickable, + desc, + rectIsUsableAnchor, + makeFrozenAnchor, + id8, + cssId, + liveUiRoot, + uiAppend, + uiAppendStyle, + uiGetById, + activeElementDeep, + defangOutsideHandlers, + }; + } + + root.__IMPECCABLE_LIVE_DOM__ = { + version: 1, + createLiveBrowserDomHelpers, + }; +})(typeof window !== 'undefined' ? window : globalThis); diff --git a/.pi/skills/impeccable/scripts/live-browser.js b/.pi/skills/impeccable/scripts/live-browser.js index 9c4b6c21e..9092338eb 100644 --- a/.pi/skills/impeccable/scripts/live-browser.js +++ b/.pi/skills/impeccable/scripts/live-browser.js @@ -195,93 +195,31 @@ // Helpers // - function own(el) { - return el && (el.id?.startsWith(PREFIX) || el.closest?.('[id^="' + PREFIX + '"]')); - } - - function pickable(el) { - if (!el || el.nodeType !== 1) return false; - if (SKIP_TAGS.has(el.tagName.toLowerCase())) return false; - if (own(el)) return false; - const r = el.getBoundingClientRect(); - return r.width >= 20 && r.height >= 20; - } - - function desc(el) { - if (!el) return ''; - let s = el.tagName.toLowerCase(); - if (el.id) s += '#' + el.id; - else if (el.classList.length) s += '.' + [...el.classList].slice(0, 2).join('.'); - return s; - } - - function rectIsUsableAnchor(rect) { - return !!rect && rect.width > 0.5 && rect.height > 0.5; - } - - function makeFrozenAnchor(el) { - if (!el || !el.getBoundingClientRect) return null; - const r = el.getBoundingClientRect(); - if (!rectIsUsableAnchor(r)) return null; - const rect = { - x: r.x, y: r.y, - top: r.top, left: r.left, - right: r.right, bottom: r.bottom, - width: r.width, height: r.height, - }; - return { - __impeccableFrozenAnchor: true, - tagName: el.tagName || 'DIV', - id: el.id || '', - classList: el.classList ? [...el.classList] : [], - hasAttribute: () => false, - getBoundingClientRect: () => rect, - }; - } - - function id8() { return crypto.randomUUID().replace(/-/g, '').slice(0, 8); } - - function cssId(id) { - if (window.CSS?.escape) return CSS.escape(id); - return String(id).replace(/([ !"#$%&'()*+,./:;<=>?@[\\\]^`{|}~])/g, '\\$1'); - } - - function liveUiRoot() { - const root = window.__IMPECCABLE_LIVE_UI_ROOT__; - if (root && typeof root.appendChild === 'function') return root; - return document.body; - } - - function uiAppend(el) { - liveUiRoot().appendChild(el); - return el; - } - - function uiAppendStyle(styleEl) { - const root = liveUiRoot(); - if (root && root !== document.body) root.appendChild(styleEl); - else document.head.appendChild(styleEl); - return styleEl; - } - - function uiGetById(id) { - const root = liveUiRoot(); - if (root?.getElementById) { - const found = root.getElementById(id); - if (found) return found; - } - if (root?.querySelector) { - const found = root.querySelector('#' + cssId(id)); - if (found) return found; - } - return document.getElementById(id); - } - - function activeElementDeep() { - let active = document.activeElement; - while (active?.shadowRoot?.activeElement) active = active.shadowRoot.activeElement; - return active; + const domHelpers = window.__IMPECCABLE_LIVE_DOM__?.createLiveBrowserDomHelpers({ + prefix: PREFIX, + skipTags: SKIP_TAGS, + document, + }); + if (!domHelpers) { + console.error('[impeccable] live-browser-dom.js was not loaded. Live mode cannot start safely.'); + window.__IMPECCABLE_LIVE_INIT__ = false; + return; } + const { + own, + pickable, + desc, + rectIsUsableAnchor, + makeFrozenAnchor, + id8, + cssId, + liveUiRoot, + uiAppend, + uiAppendStyle, + uiGetById, + activeElementDeep, + defangOutsideHandlers, + } = domHelpers; window.__IMPECCABLE_LIVE_CHROME_CORE__ = { version: 1, @@ -314,45 +252,6 @@ }), }; - // Modal-aware chrome: keep our floating UI clickable inside Radix / - // Headless UI / vaul portals. - // - // Two host-page behaviors break us when the picked element lives inside a - // modal dialog: - // - // 1. Modal scroll-lock disables outside pointer events. Radix's - // `DismissableLayer` sets `document.body.style.pointerEvents = 'none'` - // while a modal is open and only restores `auto` on the layer. Our - // chrome inherits `none` from and becomes unclickable. - // 2. The dialog's outside-interaction handler (Radix's - // `usePointerDownOutside`) listens at document level and dismisses - // the dialog whenever a `pointerdown` lands outside the layer node. - // Our chrome is a sibling of , so Radix classifies our clicks - // as outside and tears the dialog down mid-task. - // - // We can't reliably re-parent our chrome into the dialog subtree (z-index - // stacking, scroll containers, theming all become host-page concerns), so - // we defang both behaviors at our root: - // - // - `pointer-events: auto !important` overrides the inherited `none`. - // - Stop `pointerdown` / `mousedown` propagation so the document-level - // dismiss listener never fires for our clicks. - // - Stop `focusin` propagation so any focus shifts inside our chrome - // don't read as "focus moved outside the dialog" to focus traps. - // - // Click events still bubble normally - only the early pointer/focus - // signals that drive outside-interaction detection are silenced. - function defangOutsideHandlers(rootEl, { setPointerEvents = true } = {}) { - if (!rootEl) return; - if (setPointerEvents) { - rootEl.style.setProperty('pointer-events', 'auto', 'important'); - } - const stop = (e) => e.stopPropagation(); - rootEl.addEventListener('pointerdown', stop); - rootEl.addEventListener('mousedown', stop); - rootEl.addEventListener('focusin', stop); - } - // // Highlight overlay // diff --git a/.pi/skills/impeccable/scripts/live/browser-script-parts.mjs b/.pi/skills/impeccable/scripts/live/browser-script-parts.mjs index f99704aff..9229e34f8 100644 --- a/.pi/skills/impeccable/scripts/live/browser-script-parts.mjs +++ b/.pi/skills/impeccable/scripts/live/browser-script-parts.mjs @@ -3,6 +3,7 @@ import path from 'node:path'; export const LIVE_BROWSER_SCRIPT_PARTS = Object.freeze([ Object.freeze({ name: 'session-state', file: 'live-browser-session.js' }), + Object.freeze({ name: 'dom-helpers', file: 'live-browser-dom.js' }), Object.freeze({ name: 'browser-ui', file: 'live-browser.js' }), ]); diff --git a/.qoder/skills/impeccable/scripts/live-browser-dom.js b/.qoder/skills/impeccable/scripts/live-browser-dom.js new file mode 100644 index 000000000..ad6a794b3 --- /dev/null +++ b/.qoder/skills/impeccable/scripts/live-browser-dom.js @@ -0,0 +1,146 @@ +/** + * Browser-side DOM helpers for Impeccable live mode. + * + * Kept separate from live-browser.js so future browser script parts can share + * chrome mounting, lookup, focus, and picker helpers without depending on the + * full overlay UI bundle. + */ +(function (root) { + 'use strict'; + if (!root) return; + + function createLiveBrowserDomHelpers({ + prefix, + skipTags, + document: doc = root.document, + css = root.CSS, + crypto = root.crypto, + } = {}) { + if (!prefix) throw new Error('prefix required'); + if (!doc) throw new Error('document required'); + const tagsToSkip = skipTags || new Set(); + + function own(el) { + return el && (el.id?.startsWith(prefix) || el.closest?.('[id^="' + prefix + '"]')); + } + + function pickable(el) { + if (!el || el.nodeType !== 1) return false; + if (tagsToSkip.has(String(el.tagName || '').toLowerCase())) return false; + if (own(el)) return false; + const r = el.getBoundingClientRect(); + return r.width >= 20 && r.height >= 20; + } + + function desc(el) { + if (!el) return ''; + let s = el.tagName.toLowerCase(); + if (el.id) s += '#' + el.id; + else if (el.classList.length) s += '.' + [...el.classList].slice(0, 2).join('.'); + return s; + } + + function rectIsUsableAnchor(rect) { + return !!rect && rect.width > 0.5 && rect.height > 0.5; + } + + function makeFrozenAnchor(el) { + if (!el || !el.getBoundingClientRect) return null; + const r = el.getBoundingClientRect(); + if (!rectIsUsableAnchor(r)) return null; + const rect = { + x: r.x, y: r.y, + top: r.top, left: r.left, + right: r.right, bottom: r.bottom, + width: r.width, height: r.height, + }; + return { + __impeccableFrozenAnchor: true, + tagName: el.tagName || 'DIV', + id: el.id || '', + classList: el.classList ? [...el.classList] : [], + hasAttribute: () => false, + getBoundingClientRect: () => rect, + }; + } + + function id8() { + if (crypto?.randomUUID) return crypto.randomUUID().replace(/-/g, '').slice(0, 8); + return (Math.random().toString(16).slice(2) + Date.now().toString(16)).slice(0, 8); + } + + function cssId(id) { + if (css?.escape) return css.escape(id); + return String(id).replace(/([ !"#$%&'()*+,./:;<=>?@[\\\]^`{|}~])/g, '\\$1'); + } + + function liveUiRoot() { + const uiRoot = root.__IMPECCABLE_LIVE_UI_ROOT__; + if (uiRoot && typeof uiRoot.appendChild === 'function') return uiRoot; + return doc.body; + } + + function uiAppend(el) { + liveUiRoot().appendChild(el); + return el; + } + + function uiAppendStyle(styleEl) { + const uiRoot = liveUiRoot(); + if (uiRoot && uiRoot !== doc.body) uiRoot.appendChild(styleEl); + else doc.head.appendChild(styleEl); + return styleEl; + } + + function uiGetById(id) { + const uiRoot = liveUiRoot(); + if (uiRoot?.getElementById) { + const found = uiRoot.getElementById(id); + if (found) return found; + } + if (uiRoot?.querySelector) { + const found = uiRoot.querySelector('#' + cssId(id)); + if (found) return found; + } + return doc.getElementById(id); + } + + function activeElementDeep() { + let active = doc.activeElement; + while (active?.shadowRoot?.activeElement) active = active.shadowRoot.activeElement; + return active; + } + + function defangOutsideHandlers(rootEl, { setPointerEvents = true } = {}) { + if (!rootEl) return; + if (setPointerEvents) { + rootEl.style.setProperty('pointer-events', 'auto', 'important'); + } + const stop = (e) => e.stopPropagation(); + rootEl.addEventListener('pointerdown', stop); + rootEl.addEventListener('mousedown', stop); + rootEl.addEventListener('focusin', stop); + } + + return { + own, + pickable, + desc, + rectIsUsableAnchor, + makeFrozenAnchor, + id8, + cssId, + liveUiRoot, + uiAppend, + uiAppendStyle, + uiGetById, + activeElementDeep, + defangOutsideHandlers, + }; + } + + root.__IMPECCABLE_LIVE_DOM__ = { + version: 1, + createLiveBrowserDomHelpers, + }; +})(typeof window !== 'undefined' ? window : globalThis); diff --git a/.qoder/skills/impeccable/scripts/live-browser.js b/.qoder/skills/impeccable/scripts/live-browser.js index 9c4b6c21e..9092338eb 100644 --- a/.qoder/skills/impeccable/scripts/live-browser.js +++ b/.qoder/skills/impeccable/scripts/live-browser.js @@ -195,93 +195,31 @@ // Helpers // - function own(el) { - return el && (el.id?.startsWith(PREFIX) || el.closest?.('[id^="' + PREFIX + '"]')); - } - - function pickable(el) { - if (!el || el.nodeType !== 1) return false; - if (SKIP_TAGS.has(el.tagName.toLowerCase())) return false; - if (own(el)) return false; - const r = el.getBoundingClientRect(); - return r.width >= 20 && r.height >= 20; - } - - function desc(el) { - if (!el) return ''; - let s = el.tagName.toLowerCase(); - if (el.id) s += '#' + el.id; - else if (el.classList.length) s += '.' + [...el.classList].slice(0, 2).join('.'); - return s; - } - - function rectIsUsableAnchor(rect) { - return !!rect && rect.width > 0.5 && rect.height > 0.5; - } - - function makeFrozenAnchor(el) { - if (!el || !el.getBoundingClientRect) return null; - const r = el.getBoundingClientRect(); - if (!rectIsUsableAnchor(r)) return null; - const rect = { - x: r.x, y: r.y, - top: r.top, left: r.left, - right: r.right, bottom: r.bottom, - width: r.width, height: r.height, - }; - return { - __impeccableFrozenAnchor: true, - tagName: el.tagName || 'DIV', - id: el.id || '', - classList: el.classList ? [...el.classList] : [], - hasAttribute: () => false, - getBoundingClientRect: () => rect, - }; - } - - function id8() { return crypto.randomUUID().replace(/-/g, '').slice(0, 8); } - - function cssId(id) { - if (window.CSS?.escape) return CSS.escape(id); - return String(id).replace(/([ !"#$%&'()*+,./:;<=>?@[\\\]^`{|}~])/g, '\\$1'); - } - - function liveUiRoot() { - const root = window.__IMPECCABLE_LIVE_UI_ROOT__; - if (root && typeof root.appendChild === 'function') return root; - return document.body; - } - - function uiAppend(el) { - liveUiRoot().appendChild(el); - return el; - } - - function uiAppendStyle(styleEl) { - const root = liveUiRoot(); - if (root && root !== document.body) root.appendChild(styleEl); - else document.head.appendChild(styleEl); - return styleEl; - } - - function uiGetById(id) { - const root = liveUiRoot(); - if (root?.getElementById) { - const found = root.getElementById(id); - if (found) return found; - } - if (root?.querySelector) { - const found = root.querySelector('#' + cssId(id)); - if (found) return found; - } - return document.getElementById(id); - } - - function activeElementDeep() { - let active = document.activeElement; - while (active?.shadowRoot?.activeElement) active = active.shadowRoot.activeElement; - return active; + const domHelpers = window.__IMPECCABLE_LIVE_DOM__?.createLiveBrowserDomHelpers({ + prefix: PREFIX, + skipTags: SKIP_TAGS, + document, + }); + if (!domHelpers) { + console.error('[impeccable] live-browser-dom.js was not loaded. Live mode cannot start safely.'); + window.__IMPECCABLE_LIVE_INIT__ = false; + return; } + const { + own, + pickable, + desc, + rectIsUsableAnchor, + makeFrozenAnchor, + id8, + cssId, + liveUiRoot, + uiAppend, + uiAppendStyle, + uiGetById, + activeElementDeep, + defangOutsideHandlers, + } = domHelpers; window.__IMPECCABLE_LIVE_CHROME_CORE__ = { version: 1, @@ -314,45 +252,6 @@ }), }; - // Modal-aware chrome: keep our floating UI clickable inside Radix / - // Headless UI / vaul portals. - // - // Two host-page behaviors break us when the picked element lives inside a - // modal dialog: - // - // 1. Modal scroll-lock disables outside pointer events. Radix's - // `DismissableLayer` sets `document.body.style.pointerEvents = 'none'` - // while a modal is open and only restores `auto` on the layer. Our - // chrome inherits `none` from and becomes unclickable. - // 2. The dialog's outside-interaction handler (Radix's - // `usePointerDownOutside`) listens at document level and dismisses - // the dialog whenever a `pointerdown` lands outside the layer node. - // Our chrome is a sibling of , so Radix classifies our clicks - // as outside and tears the dialog down mid-task. - // - // We can't reliably re-parent our chrome into the dialog subtree (z-index - // stacking, scroll containers, theming all become host-page concerns), so - // we defang both behaviors at our root: - // - // - `pointer-events: auto !important` overrides the inherited `none`. - // - Stop `pointerdown` / `mousedown` propagation so the document-level - // dismiss listener never fires for our clicks. - // - Stop `focusin` propagation so any focus shifts inside our chrome - // don't read as "focus moved outside the dialog" to focus traps. - // - // Click events still bubble normally - only the early pointer/focus - // signals that drive outside-interaction detection are silenced. - function defangOutsideHandlers(rootEl, { setPointerEvents = true } = {}) { - if (!rootEl) return; - if (setPointerEvents) { - rootEl.style.setProperty('pointer-events', 'auto', 'important'); - } - const stop = (e) => e.stopPropagation(); - rootEl.addEventListener('pointerdown', stop); - rootEl.addEventListener('mousedown', stop); - rootEl.addEventListener('focusin', stop); - } - // // Highlight overlay // diff --git a/.qoder/skills/impeccable/scripts/live/browser-script-parts.mjs b/.qoder/skills/impeccable/scripts/live/browser-script-parts.mjs index f99704aff..9229e34f8 100644 --- a/.qoder/skills/impeccable/scripts/live/browser-script-parts.mjs +++ b/.qoder/skills/impeccable/scripts/live/browser-script-parts.mjs @@ -3,6 +3,7 @@ import path from 'node:path'; export const LIVE_BROWSER_SCRIPT_PARTS = Object.freeze([ Object.freeze({ name: 'session-state', file: 'live-browser-session.js' }), + Object.freeze({ name: 'dom-helpers', file: 'live-browser-dom.js' }), Object.freeze({ name: 'browser-ui', file: 'live-browser.js' }), ]); diff --git a/.rovodev/skills/impeccable/scripts/live-browser-dom.js b/.rovodev/skills/impeccable/scripts/live-browser-dom.js new file mode 100644 index 000000000..ad6a794b3 --- /dev/null +++ b/.rovodev/skills/impeccable/scripts/live-browser-dom.js @@ -0,0 +1,146 @@ +/** + * Browser-side DOM helpers for Impeccable live mode. + * + * Kept separate from live-browser.js so future browser script parts can share + * chrome mounting, lookup, focus, and picker helpers without depending on the + * full overlay UI bundle. + */ +(function (root) { + 'use strict'; + if (!root) return; + + function createLiveBrowserDomHelpers({ + prefix, + skipTags, + document: doc = root.document, + css = root.CSS, + crypto = root.crypto, + } = {}) { + if (!prefix) throw new Error('prefix required'); + if (!doc) throw new Error('document required'); + const tagsToSkip = skipTags || new Set(); + + function own(el) { + return el && (el.id?.startsWith(prefix) || el.closest?.('[id^="' + prefix + '"]')); + } + + function pickable(el) { + if (!el || el.nodeType !== 1) return false; + if (tagsToSkip.has(String(el.tagName || '').toLowerCase())) return false; + if (own(el)) return false; + const r = el.getBoundingClientRect(); + return r.width >= 20 && r.height >= 20; + } + + function desc(el) { + if (!el) return ''; + let s = el.tagName.toLowerCase(); + if (el.id) s += '#' + el.id; + else if (el.classList.length) s += '.' + [...el.classList].slice(0, 2).join('.'); + return s; + } + + function rectIsUsableAnchor(rect) { + return !!rect && rect.width > 0.5 && rect.height > 0.5; + } + + function makeFrozenAnchor(el) { + if (!el || !el.getBoundingClientRect) return null; + const r = el.getBoundingClientRect(); + if (!rectIsUsableAnchor(r)) return null; + const rect = { + x: r.x, y: r.y, + top: r.top, left: r.left, + right: r.right, bottom: r.bottom, + width: r.width, height: r.height, + }; + return { + __impeccableFrozenAnchor: true, + tagName: el.tagName || 'DIV', + id: el.id || '', + classList: el.classList ? [...el.classList] : [], + hasAttribute: () => false, + getBoundingClientRect: () => rect, + }; + } + + function id8() { + if (crypto?.randomUUID) return crypto.randomUUID().replace(/-/g, '').slice(0, 8); + return (Math.random().toString(16).slice(2) + Date.now().toString(16)).slice(0, 8); + } + + function cssId(id) { + if (css?.escape) return css.escape(id); + return String(id).replace(/([ !"#$%&'()*+,./:;<=>?@[\\\]^`{|}~])/g, '\\$1'); + } + + function liveUiRoot() { + const uiRoot = root.__IMPECCABLE_LIVE_UI_ROOT__; + if (uiRoot && typeof uiRoot.appendChild === 'function') return uiRoot; + return doc.body; + } + + function uiAppend(el) { + liveUiRoot().appendChild(el); + return el; + } + + function uiAppendStyle(styleEl) { + const uiRoot = liveUiRoot(); + if (uiRoot && uiRoot !== doc.body) uiRoot.appendChild(styleEl); + else doc.head.appendChild(styleEl); + return styleEl; + } + + function uiGetById(id) { + const uiRoot = liveUiRoot(); + if (uiRoot?.getElementById) { + const found = uiRoot.getElementById(id); + if (found) return found; + } + if (uiRoot?.querySelector) { + const found = uiRoot.querySelector('#' + cssId(id)); + if (found) return found; + } + return doc.getElementById(id); + } + + function activeElementDeep() { + let active = doc.activeElement; + while (active?.shadowRoot?.activeElement) active = active.shadowRoot.activeElement; + return active; + } + + function defangOutsideHandlers(rootEl, { setPointerEvents = true } = {}) { + if (!rootEl) return; + if (setPointerEvents) { + rootEl.style.setProperty('pointer-events', 'auto', 'important'); + } + const stop = (e) => e.stopPropagation(); + rootEl.addEventListener('pointerdown', stop); + rootEl.addEventListener('mousedown', stop); + rootEl.addEventListener('focusin', stop); + } + + return { + own, + pickable, + desc, + rectIsUsableAnchor, + makeFrozenAnchor, + id8, + cssId, + liveUiRoot, + uiAppend, + uiAppendStyle, + uiGetById, + activeElementDeep, + defangOutsideHandlers, + }; + } + + root.__IMPECCABLE_LIVE_DOM__ = { + version: 1, + createLiveBrowserDomHelpers, + }; +})(typeof window !== 'undefined' ? window : globalThis); diff --git a/.rovodev/skills/impeccable/scripts/live-browser.js b/.rovodev/skills/impeccable/scripts/live-browser.js index 9c4b6c21e..9092338eb 100644 --- a/.rovodev/skills/impeccable/scripts/live-browser.js +++ b/.rovodev/skills/impeccable/scripts/live-browser.js @@ -195,93 +195,31 @@ // Helpers // - function own(el) { - return el && (el.id?.startsWith(PREFIX) || el.closest?.('[id^="' + PREFIX + '"]')); - } - - function pickable(el) { - if (!el || el.nodeType !== 1) return false; - if (SKIP_TAGS.has(el.tagName.toLowerCase())) return false; - if (own(el)) return false; - const r = el.getBoundingClientRect(); - return r.width >= 20 && r.height >= 20; - } - - function desc(el) { - if (!el) return ''; - let s = el.tagName.toLowerCase(); - if (el.id) s += '#' + el.id; - else if (el.classList.length) s += '.' + [...el.classList].slice(0, 2).join('.'); - return s; - } - - function rectIsUsableAnchor(rect) { - return !!rect && rect.width > 0.5 && rect.height > 0.5; - } - - function makeFrozenAnchor(el) { - if (!el || !el.getBoundingClientRect) return null; - const r = el.getBoundingClientRect(); - if (!rectIsUsableAnchor(r)) return null; - const rect = { - x: r.x, y: r.y, - top: r.top, left: r.left, - right: r.right, bottom: r.bottom, - width: r.width, height: r.height, - }; - return { - __impeccableFrozenAnchor: true, - tagName: el.tagName || 'DIV', - id: el.id || '', - classList: el.classList ? [...el.classList] : [], - hasAttribute: () => false, - getBoundingClientRect: () => rect, - }; - } - - function id8() { return crypto.randomUUID().replace(/-/g, '').slice(0, 8); } - - function cssId(id) { - if (window.CSS?.escape) return CSS.escape(id); - return String(id).replace(/([ !"#$%&'()*+,./:;<=>?@[\\\]^`{|}~])/g, '\\$1'); - } - - function liveUiRoot() { - const root = window.__IMPECCABLE_LIVE_UI_ROOT__; - if (root && typeof root.appendChild === 'function') return root; - return document.body; - } - - function uiAppend(el) { - liveUiRoot().appendChild(el); - return el; - } - - function uiAppendStyle(styleEl) { - const root = liveUiRoot(); - if (root && root !== document.body) root.appendChild(styleEl); - else document.head.appendChild(styleEl); - return styleEl; - } - - function uiGetById(id) { - const root = liveUiRoot(); - if (root?.getElementById) { - const found = root.getElementById(id); - if (found) return found; - } - if (root?.querySelector) { - const found = root.querySelector('#' + cssId(id)); - if (found) return found; - } - return document.getElementById(id); - } - - function activeElementDeep() { - let active = document.activeElement; - while (active?.shadowRoot?.activeElement) active = active.shadowRoot.activeElement; - return active; + const domHelpers = window.__IMPECCABLE_LIVE_DOM__?.createLiveBrowserDomHelpers({ + prefix: PREFIX, + skipTags: SKIP_TAGS, + document, + }); + if (!domHelpers) { + console.error('[impeccable] live-browser-dom.js was not loaded. Live mode cannot start safely.'); + window.__IMPECCABLE_LIVE_INIT__ = false; + return; } + const { + own, + pickable, + desc, + rectIsUsableAnchor, + makeFrozenAnchor, + id8, + cssId, + liveUiRoot, + uiAppend, + uiAppendStyle, + uiGetById, + activeElementDeep, + defangOutsideHandlers, + } = domHelpers; window.__IMPECCABLE_LIVE_CHROME_CORE__ = { version: 1, @@ -314,45 +252,6 @@ }), }; - // Modal-aware chrome: keep our floating UI clickable inside Radix / - // Headless UI / vaul portals. - // - // Two host-page behaviors break us when the picked element lives inside a - // modal dialog: - // - // 1. Modal scroll-lock disables outside pointer events. Radix's - // `DismissableLayer` sets `document.body.style.pointerEvents = 'none'` - // while a modal is open and only restores `auto` on the layer. Our - // chrome inherits `none` from and becomes unclickable. - // 2. The dialog's outside-interaction handler (Radix's - // `usePointerDownOutside`) listens at document level and dismisses - // the dialog whenever a `pointerdown` lands outside the layer node. - // Our chrome is a sibling of , so Radix classifies our clicks - // as outside and tears the dialog down mid-task. - // - // We can't reliably re-parent our chrome into the dialog subtree (z-index - // stacking, scroll containers, theming all become host-page concerns), so - // we defang both behaviors at our root: - // - // - `pointer-events: auto !important` overrides the inherited `none`. - // - Stop `pointerdown` / `mousedown` propagation so the document-level - // dismiss listener never fires for our clicks. - // - Stop `focusin` propagation so any focus shifts inside our chrome - // don't read as "focus moved outside the dialog" to focus traps. - // - // Click events still bubble normally - only the early pointer/focus - // signals that drive outside-interaction detection are silenced. - function defangOutsideHandlers(rootEl, { setPointerEvents = true } = {}) { - if (!rootEl) return; - if (setPointerEvents) { - rootEl.style.setProperty('pointer-events', 'auto', 'important'); - } - const stop = (e) => e.stopPropagation(); - rootEl.addEventListener('pointerdown', stop); - rootEl.addEventListener('mousedown', stop); - rootEl.addEventListener('focusin', stop); - } - // // Highlight overlay // diff --git a/.rovodev/skills/impeccable/scripts/live/browser-script-parts.mjs b/.rovodev/skills/impeccable/scripts/live/browser-script-parts.mjs index f99704aff..9229e34f8 100644 --- a/.rovodev/skills/impeccable/scripts/live/browser-script-parts.mjs +++ b/.rovodev/skills/impeccable/scripts/live/browser-script-parts.mjs @@ -3,6 +3,7 @@ import path from 'node:path'; export const LIVE_BROWSER_SCRIPT_PARTS = Object.freeze([ Object.freeze({ name: 'session-state', file: 'live-browser-session.js' }), + Object.freeze({ name: 'dom-helpers', file: 'live-browser-dom.js' }), Object.freeze({ name: 'browser-ui', file: 'live-browser.js' }), ]); diff --git a/.trae-cn/skills/impeccable/scripts/live-browser-dom.js b/.trae-cn/skills/impeccable/scripts/live-browser-dom.js new file mode 100644 index 000000000..ad6a794b3 --- /dev/null +++ b/.trae-cn/skills/impeccable/scripts/live-browser-dom.js @@ -0,0 +1,146 @@ +/** + * Browser-side DOM helpers for Impeccable live mode. + * + * Kept separate from live-browser.js so future browser script parts can share + * chrome mounting, lookup, focus, and picker helpers without depending on the + * full overlay UI bundle. + */ +(function (root) { + 'use strict'; + if (!root) return; + + function createLiveBrowserDomHelpers({ + prefix, + skipTags, + document: doc = root.document, + css = root.CSS, + crypto = root.crypto, + } = {}) { + if (!prefix) throw new Error('prefix required'); + if (!doc) throw new Error('document required'); + const tagsToSkip = skipTags || new Set(); + + function own(el) { + return el && (el.id?.startsWith(prefix) || el.closest?.('[id^="' + prefix + '"]')); + } + + function pickable(el) { + if (!el || el.nodeType !== 1) return false; + if (tagsToSkip.has(String(el.tagName || '').toLowerCase())) return false; + if (own(el)) return false; + const r = el.getBoundingClientRect(); + return r.width >= 20 && r.height >= 20; + } + + function desc(el) { + if (!el) return ''; + let s = el.tagName.toLowerCase(); + if (el.id) s += '#' + el.id; + else if (el.classList.length) s += '.' + [...el.classList].slice(0, 2).join('.'); + return s; + } + + function rectIsUsableAnchor(rect) { + return !!rect && rect.width > 0.5 && rect.height > 0.5; + } + + function makeFrozenAnchor(el) { + if (!el || !el.getBoundingClientRect) return null; + const r = el.getBoundingClientRect(); + if (!rectIsUsableAnchor(r)) return null; + const rect = { + x: r.x, y: r.y, + top: r.top, left: r.left, + right: r.right, bottom: r.bottom, + width: r.width, height: r.height, + }; + return { + __impeccableFrozenAnchor: true, + tagName: el.tagName || 'DIV', + id: el.id || '', + classList: el.classList ? [...el.classList] : [], + hasAttribute: () => false, + getBoundingClientRect: () => rect, + }; + } + + function id8() { + if (crypto?.randomUUID) return crypto.randomUUID().replace(/-/g, '').slice(0, 8); + return (Math.random().toString(16).slice(2) + Date.now().toString(16)).slice(0, 8); + } + + function cssId(id) { + if (css?.escape) return css.escape(id); + return String(id).replace(/([ !"#$%&'()*+,./:;<=>?@[\\\]^`{|}~])/g, '\\$1'); + } + + function liveUiRoot() { + const uiRoot = root.__IMPECCABLE_LIVE_UI_ROOT__; + if (uiRoot && typeof uiRoot.appendChild === 'function') return uiRoot; + return doc.body; + } + + function uiAppend(el) { + liveUiRoot().appendChild(el); + return el; + } + + function uiAppendStyle(styleEl) { + const uiRoot = liveUiRoot(); + if (uiRoot && uiRoot !== doc.body) uiRoot.appendChild(styleEl); + else doc.head.appendChild(styleEl); + return styleEl; + } + + function uiGetById(id) { + const uiRoot = liveUiRoot(); + if (uiRoot?.getElementById) { + const found = uiRoot.getElementById(id); + if (found) return found; + } + if (uiRoot?.querySelector) { + const found = uiRoot.querySelector('#' + cssId(id)); + if (found) return found; + } + return doc.getElementById(id); + } + + function activeElementDeep() { + let active = doc.activeElement; + while (active?.shadowRoot?.activeElement) active = active.shadowRoot.activeElement; + return active; + } + + function defangOutsideHandlers(rootEl, { setPointerEvents = true } = {}) { + if (!rootEl) return; + if (setPointerEvents) { + rootEl.style.setProperty('pointer-events', 'auto', 'important'); + } + const stop = (e) => e.stopPropagation(); + rootEl.addEventListener('pointerdown', stop); + rootEl.addEventListener('mousedown', stop); + rootEl.addEventListener('focusin', stop); + } + + return { + own, + pickable, + desc, + rectIsUsableAnchor, + makeFrozenAnchor, + id8, + cssId, + liveUiRoot, + uiAppend, + uiAppendStyle, + uiGetById, + activeElementDeep, + defangOutsideHandlers, + }; + } + + root.__IMPECCABLE_LIVE_DOM__ = { + version: 1, + createLiveBrowserDomHelpers, + }; +})(typeof window !== 'undefined' ? window : globalThis); diff --git a/.trae-cn/skills/impeccable/scripts/live-browser.js b/.trae-cn/skills/impeccable/scripts/live-browser.js index 9c4b6c21e..9092338eb 100644 --- a/.trae-cn/skills/impeccable/scripts/live-browser.js +++ b/.trae-cn/skills/impeccable/scripts/live-browser.js @@ -195,93 +195,31 @@ // Helpers // - function own(el) { - return el && (el.id?.startsWith(PREFIX) || el.closest?.('[id^="' + PREFIX + '"]')); - } - - function pickable(el) { - if (!el || el.nodeType !== 1) return false; - if (SKIP_TAGS.has(el.tagName.toLowerCase())) return false; - if (own(el)) return false; - const r = el.getBoundingClientRect(); - return r.width >= 20 && r.height >= 20; - } - - function desc(el) { - if (!el) return ''; - let s = el.tagName.toLowerCase(); - if (el.id) s += '#' + el.id; - else if (el.classList.length) s += '.' + [...el.classList].slice(0, 2).join('.'); - return s; - } - - function rectIsUsableAnchor(rect) { - return !!rect && rect.width > 0.5 && rect.height > 0.5; - } - - function makeFrozenAnchor(el) { - if (!el || !el.getBoundingClientRect) return null; - const r = el.getBoundingClientRect(); - if (!rectIsUsableAnchor(r)) return null; - const rect = { - x: r.x, y: r.y, - top: r.top, left: r.left, - right: r.right, bottom: r.bottom, - width: r.width, height: r.height, - }; - return { - __impeccableFrozenAnchor: true, - tagName: el.tagName || 'DIV', - id: el.id || '', - classList: el.classList ? [...el.classList] : [], - hasAttribute: () => false, - getBoundingClientRect: () => rect, - }; - } - - function id8() { return crypto.randomUUID().replace(/-/g, '').slice(0, 8); } - - function cssId(id) { - if (window.CSS?.escape) return CSS.escape(id); - return String(id).replace(/([ !"#$%&'()*+,./:;<=>?@[\\\]^`{|}~])/g, '\\$1'); - } - - function liveUiRoot() { - const root = window.__IMPECCABLE_LIVE_UI_ROOT__; - if (root && typeof root.appendChild === 'function') return root; - return document.body; - } - - function uiAppend(el) { - liveUiRoot().appendChild(el); - return el; - } - - function uiAppendStyle(styleEl) { - const root = liveUiRoot(); - if (root && root !== document.body) root.appendChild(styleEl); - else document.head.appendChild(styleEl); - return styleEl; - } - - function uiGetById(id) { - const root = liveUiRoot(); - if (root?.getElementById) { - const found = root.getElementById(id); - if (found) return found; - } - if (root?.querySelector) { - const found = root.querySelector('#' + cssId(id)); - if (found) return found; - } - return document.getElementById(id); - } - - function activeElementDeep() { - let active = document.activeElement; - while (active?.shadowRoot?.activeElement) active = active.shadowRoot.activeElement; - return active; + const domHelpers = window.__IMPECCABLE_LIVE_DOM__?.createLiveBrowserDomHelpers({ + prefix: PREFIX, + skipTags: SKIP_TAGS, + document, + }); + if (!domHelpers) { + console.error('[impeccable] live-browser-dom.js was not loaded. Live mode cannot start safely.'); + window.__IMPECCABLE_LIVE_INIT__ = false; + return; } + const { + own, + pickable, + desc, + rectIsUsableAnchor, + makeFrozenAnchor, + id8, + cssId, + liveUiRoot, + uiAppend, + uiAppendStyle, + uiGetById, + activeElementDeep, + defangOutsideHandlers, + } = domHelpers; window.__IMPECCABLE_LIVE_CHROME_CORE__ = { version: 1, @@ -314,45 +252,6 @@ }), }; - // Modal-aware chrome: keep our floating UI clickable inside Radix / - // Headless UI / vaul portals. - // - // Two host-page behaviors break us when the picked element lives inside a - // modal dialog: - // - // 1. Modal scroll-lock disables outside pointer events. Radix's - // `DismissableLayer` sets `document.body.style.pointerEvents = 'none'` - // while a modal is open and only restores `auto` on the layer. Our - // chrome inherits `none` from and becomes unclickable. - // 2. The dialog's outside-interaction handler (Radix's - // `usePointerDownOutside`) listens at document level and dismisses - // the dialog whenever a `pointerdown` lands outside the layer node. - // Our chrome is a sibling of , so Radix classifies our clicks - // as outside and tears the dialog down mid-task. - // - // We can't reliably re-parent our chrome into the dialog subtree (z-index - // stacking, scroll containers, theming all become host-page concerns), so - // we defang both behaviors at our root: - // - // - `pointer-events: auto !important` overrides the inherited `none`. - // - Stop `pointerdown` / `mousedown` propagation so the document-level - // dismiss listener never fires for our clicks. - // - Stop `focusin` propagation so any focus shifts inside our chrome - // don't read as "focus moved outside the dialog" to focus traps. - // - // Click events still bubble normally - only the early pointer/focus - // signals that drive outside-interaction detection are silenced. - function defangOutsideHandlers(rootEl, { setPointerEvents = true } = {}) { - if (!rootEl) return; - if (setPointerEvents) { - rootEl.style.setProperty('pointer-events', 'auto', 'important'); - } - const stop = (e) => e.stopPropagation(); - rootEl.addEventListener('pointerdown', stop); - rootEl.addEventListener('mousedown', stop); - rootEl.addEventListener('focusin', stop); - } - // // Highlight overlay // diff --git a/.trae-cn/skills/impeccable/scripts/live/browser-script-parts.mjs b/.trae-cn/skills/impeccable/scripts/live/browser-script-parts.mjs index f99704aff..9229e34f8 100644 --- a/.trae-cn/skills/impeccable/scripts/live/browser-script-parts.mjs +++ b/.trae-cn/skills/impeccable/scripts/live/browser-script-parts.mjs @@ -3,6 +3,7 @@ import path from 'node:path'; export const LIVE_BROWSER_SCRIPT_PARTS = Object.freeze([ Object.freeze({ name: 'session-state', file: 'live-browser-session.js' }), + Object.freeze({ name: 'dom-helpers', file: 'live-browser-dom.js' }), Object.freeze({ name: 'browser-ui', file: 'live-browser.js' }), ]); diff --git a/.trae/skills/impeccable/scripts/live-browser-dom.js b/.trae/skills/impeccable/scripts/live-browser-dom.js new file mode 100644 index 000000000..ad6a794b3 --- /dev/null +++ b/.trae/skills/impeccable/scripts/live-browser-dom.js @@ -0,0 +1,146 @@ +/** + * Browser-side DOM helpers for Impeccable live mode. + * + * Kept separate from live-browser.js so future browser script parts can share + * chrome mounting, lookup, focus, and picker helpers without depending on the + * full overlay UI bundle. + */ +(function (root) { + 'use strict'; + if (!root) return; + + function createLiveBrowserDomHelpers({ + prefix, + skipTags, + document: doc = root.document, + css = root.CSS, + crypto = root.crypto, + } = {}) { + if (!prefix) throw new Error('prefix required'); + if (!doc) throw new Error('document required'); + const tagsToSkip = skipTags || new Set(); + + function own(el) { + return el && (el.id?.startsWith(prefix) || el.closest?.('[id^="' + prefix + '"]')); + } + + function pickable(el) { + if (!el || el.nodeType !== 1) return false; + if (tagsToSkip.has(String(el.tagName || '').toLowerCase())) return false; + if (own(el)) return false; + const r = el.getBoundingClientRect(); + return r.width >= 20 && r.height >= 20; + } + + function desc(el) { + if (!el) return ''; + let s = el.tagName.toLowerCase(); + if (el.id) s += '#' + el.id; + else if (el.classList.length) s += '.' + [...el.classList].slice(0, 2).join('.'); + return s; + } + + function rectIsUsableAnchor(rect) { + return !!rect && rect.width > 0.5 && rect.height > 0.5; + } + + function makeFrozenAnchor(el) { + if (!el || !el.getBoundingClientRect) return null; + const r = el.getBoundingClientRect(); + if (!rectIsUsableAnchor(r)) return null; + const rect = { + x: r.x, y: r.y, + top: r.top, left: r.left, + right: r.right, bottom: r.bottom, + width: r.width, height: r.height, + }; + return { + __impeccableFrozenAnchor: true, + tagName: el.tagName || 'DIV', + id: el.id || '', + classList: el.classList ? [...el.classList] : [], + hasAttribute: () => false, + getBoundingClientRect: () => rect, + }; + } + + function id8() { + if (crypto?.randomUUID) return crypto.randomUUID().replace(/-/g, '').slice(0, 8); + return (Math.random().toString(16).slice(2) + Date.now().toString(16)).slice(0, 8); + } + + function cssId(id) { + if (css?.escape) return css.escape(id); + return String(id).replace(/([ !"#$%&'()*+,./:;<=>?@[\\\]^`{|}~])/g, '\\$1'); + } + + function liveUiRoot() { + const uiRoot = root.__IMPECCABLE_LIVE_UI_ROOT__; + if (uiRoot && typeof uiRoot.appendChild === 'function') return uiRoot; + return doc.body; + } + + function uiAppend(el) { + liveUiRoot().appendChild(el); + return el; + } + + function uiAppendStyle(styleEl) { + const uiRoot = liveUiRoot(); + if (uiRoot && uiRoot !== doc.body) uiRoot.appendChild(styleEl); + else doc.head.appendChild(styleEl); + return styleEl; + } + + function uiGetById(id) { + const uiRoot = liveUiRoot(); + if (uiRoot?.getElementById) { + const found = uiRoot.getElementById(id); + if (found) return found; + } + if (uiRoot?.querySelector) { + const found = uiRoot.querySelector('#' + cssId(id)); + if (found) return found; + } + return doc.getElementById(id); + } + + function activeElementDeep() { + let active = doc.activeElement; + while (active?.shadowRoot?.activeElement) active = active.shadowRoot.activeElement; + return active; + } + + function defangOutsideHandlers(rootEl, { setPointerEvents = true } = {}) { + if (!rootEl) return; + if (setPointerEvents) { + rootEl.style.setProperty('pointer-events', 'auto', 'important'); + } + const stop = (e) => e.stopPropagation(); + rootEl.addEventListener('pointerdown', stop); + rootEl.addEventListener('mousedown', stop); + rootEl.addEventListener('focusin', stop); + } + + return { + own, + pickable, + desc, + rectIsUsableAnchor, + makeFrozenAnchor, + id8, + cssId, + liveUiRoot, + uiAppend, + uiAppendStyle, + uiGetById, + activeElementDeep, + defangOutsideHandlers, + }; + } + + root.__IMPECCABLE_LIVE_DOM__ = { + version: 1, + createLiveBrowserDomHelpers, + }; +})(typeof window !== 'undefined' ? window : globalThis); diff --git a/.trae/skills/impeccable/scripts/live-browser.js b/.trae/skills/impeccable/scripts/live-browser.js index 9c4b6c21e..9092338eb 100644 --- a/.trae/skills/impeccable/scripts/live-browser.js +++ b/.trae/skills/impeccable/scripts/live-browser.js @@ -195,93 +195,31 @@ // Helpers // - function own(el) { - return el && (el.id?.startsWith(PREFIX) || el.closest?.('[id^="' + PREFIX + '"]')); - } - - function pickable(el) { - if (!el || el.nodeType !== 1) return false; - if (SKIP_TAGS.has(el.tagName.toLowerCase())) return false; - if (own(el)) return false; - const r = el.getBoundingClientRect(); - return r.width >= 20 && r.height >= 20; - } - - function desc(el) { - if (!el) return ''; - let s = el.tagName.toLowerCase(); - if (el.id) s += '#' + el.id; - else if (el.classList.length) s += '.' + [...el.classList].slice(0, 2).join('.'); - return s; - } - - function rectIsUsableAnchor(rect) { - return !!rect && rect.width > 0.5 && rect.height > 0.5; - } - - function makeFrozenAnchor(el) { - if (!el || !el.getBoundingClientRect) return null; - const r = el.getBoundingClientRect(); - if (!rectIsUsableAnchor(r)) return null; - const rect = { - x: r.x, y: r.y, - top: r.top, left: r.left, - right: r.right, bottom: r.bottom, - width: r.width, height: r.height, - }; - return { - __impeccableFrozenAnchor: true, - tagName: el.tagName || 'DIV', - id: el.id || '', - classList: el.classList ? [...el.classList] : [], - hasAttribute: () => false, - getBoundingClientRect: () => rect, - }; - } - - function id8() { return crypto.randomUUID().replace(/-/g, '').slice(0, 8); } - - function cssId(id) { - if (window.CSS?.escape) return CSS.escape(id); - return String(id).replace(/([ !"#$%&'()*+,./:;<=>?@[\\\]^`{|}~])/g, '\\$1'); - } - - function liveUiRoot() { - const root = window.__IMPECCABLE_LIVE_UI_ROOT__; - if (root && typeof root.appendChild === 'function') return root; - return document.body; - } - - function uiAppend(el) { - liveUiRoot().appendChild(el); - return el; - } - - function uiAppendStyle(styleEl) { - const root = liveUiRoot(); - if (root && root !== document.body) root.appendChild(styleEl); - else document.head.appendChild(styleEl); - return styleEl; - } - - function uiGetById(id) { - const root = liveUiRoot(); - if (root?.getElementById) { - const found = root.getElementById(id); - if (found) return found; - } - if (root?.querySelector) { - const found = root.querySelector('#' + cssId(id)); - if (found) return found; - } - return document.getElementById(id); - } - - function activeElementDeep() { - let active = document.activeElement; - while (active?.shadowRoot?.activeElement) active = active.shadowRoot.activeElement; - return active; + const domHelpers = window.__IMPECCABLE_LIVE_DOM__?.createLiveBrowserDomHelpers({ + prefix: PREFIX, + skipTags: SKIP_TAGS, + document, + }); + if (!domHelpers) { + console.error('[impeccable] live-browser-dom.js was not loaded. Live mode cannot start safely.'); + window.__IMPECCABLE_LIVE_INIT__ = false; + return; } + const { + own, + pickable, + desc, + rectIsUsableAnchor, + makeFrozenAnchor, + id8, + cssId, + liveUiRoot, + uiAppend, + uiAppendStyle, + uiGetById, + activeElementDeep, + defangOutsideHandlers, + } = domHelpers; window.__IMPECCABLE_LIVE_CHROME_CORE__ = { version: 1, @@ -314,45 +252,6 @@ }), }; - // Modal-aware chrome: keep our floating UI clickable inside Radix / - // Headless UI / vaul portals. - // - // Two host-page behaviors break us when the picked element lives inside a - // modal dialog: - // - // 1. Modal scroll-lock disables outside pointer events. Radix's - // `DismissableLayer` sets `document.body.style.pointerEvents = 'none'` - // while a modal is open and only restores `auto` on the layer. Our - // chrome inherits `none` from and becomes unclickable. - // 2. The dialog's outside-interaction handler (Radix's - // `usePointerDownOutside`) listens at document level and dismisses - // the dialog whenever a `pointerdown` lands outside the layer node. - // Our chrome is a sibling of , so Radix classifies our clicks - // as outside and tears the dialog down mid-task. - // - // We can't reliably re-parent our chrome into the dialog subtree (z-index - // stacking, scroll containers, theming all become host-page concerns), so - // we defang both behaviors at our root: - // - // - `pointer-events: auto !important` overrides the inherited `none`. - // - Stop `pointerdown` / `mousedown` propagation so the document-level - // dismiss listener never fires for our clicks. - // - Stop `focusin` propagation so any focus shifts inside our chrome - // don't read as "focus moved outside the dialog" to focus traps. - // - // Click events still bubble normally - only the early pointer/focus - // signals that drive outside-interaction detection are silenced. - function defangOutsideHandlers(rootEl, { setPointerEvents = true } = {}) { - if (!rootEl) return; - if (setPointerEvents) { - rootEl.style.setProperty('pointer-events', 'auto', 'important'); - } - const stop = (e) => e.stopPropagation(); - rootEl.addEventListener('pointerdown', stop); - rootEl.addEventListener('mousedown', stop); - rootEl.addEventListener('focusin', stop); - } - // // Highlight overlay // diff --git a/.trae/skills/impeccable/scripts/live/browser-script-parts.mjs b/.trae/skills/impeccable/scripts/live/browser-script-parts.mjs index f99704aff..9229e34f8 100644 --- a/.trae/skills/impeccable/scripts/live/browser-script-parts.mjs +++ b/.trae/skills/impeccable/scripts/live/browser-script-parts.mjs @@ -3,6 +3,7 @@ import path from 'node:path'; export const LIVE_BROWSER_SCRIPT_PARTS = Object.freeze([ Object.freeze({ name: 'session-state', file: 'live-browser-session.js' }), + Object.freeze({ name: 'dom-helpers', file: 'live-browser-dom.js' }), Object.freeze({ name: 'browser-ui', file: 'live-browser.js' }), ]); diff --git a/plugin/skills/impeccable/scripts/live-browser-dom.js b/plugin/skills/impeccable/scripts/live-browser-dom.js new file mode 100644 index 000000000..ad6a794b3 --- /dev/null +++ b/plugin/skills/impeccable/scripts/live-browser-dom.js @@ -0,0 +1,146 @@ +/** + * Browser-side DOM helpers for Impeccable live mode. + * + * Kept separate from live-browser.js so future browser script parts can share + * chrome mounting, lookup, focus, and picker helpers without depending on the + * full overlay UI bundle. + */ +(function (root) { + 'use strict'; + if (!root) return; + + function createLiveBrowserDomHelpers({ + prefix, + skipTags, + document: doc = root.document, + css = root.CSS, + crypto = root.crypto, + } = {}) { + if (!prefix) throw new Error('prefix required'); + if (!doc) throw new Error('document required'); + const tagsToSkip = skipTags || new Set(); + + function own(el) { + return el && (el.id?.startsWith(prefix) || el.closest?.('[id^="' + prefix + '"]')); + } + + function pickable(el) { + if (!el || el.nodeType !== 1) return false; + if (tagsToSkip.has(String(el.tagName || '').toLowerCase())) return false; + if (own(el)) return false; + const r = el.getBoundingClientRect(); + return r.width >= 20 && r.height >= 20; + } + + function desc(el) { + if (!el) return ''; + let s = el.tagName.toLowerCase(); + if (el.id) s += '#' + el.id; + else if (el.classList.length) s += '.' + [...el.classList].slice(0, 2).join('.'); + return s; + } + + function rectIsUsableAnchor(rect) { + return !!rect && rect.width > 0.5 && rect.height > 0.5; + } + + function makeFrozenAnchor(el) { + if (!el || !el.getBoundingClientRect) return null; + const r = el.getBoundingClientRect(); + if (!rectIsUsableAnchor(r)) return null; + const rect = { + x: r.x, y: r.y, + top: r.top, left: r.left, + right: r.right, bottom: r.bottom, + width: r.width, height: r.height, + }; + return { + __impeccableFrozenAnchor: true, + tagName: el.tagName || 'DIV', + id: el.id || '', + classList: el.classList ? [...el.classList] : [], + hasAttribute: () => false, + getBoundingClientRect: () => rect, + }; + } + + function id8() { + if (crypto?.randomUUID) return crypto.randomUUID().replace(/-/g, '').slice(0, 8); + return (Math.random().toString(16).slice(2) + Date.now().toString(16)).slice(0, 8); + } + + function cssId(id) { + if (css?.escape) return css.escape(id); + return String(id).replace(/([ !"#$%&'()*+,./:;<=>?@[\\\]^`{|}~])/g, '\\$1'); + } + + function liveUiRoot() { + const uiRoot = root.__IMPECCABLE_LIVE_UI_ROOT__; + if (uiRoot && typeof uiRoot.appendChild === 'function') return uiRoot; + return doc.body; + } + + function uiAppend(el) { + liveUiRoot().appendChild(el); + return el; + } + + function uiAppendStyle(styleEl) { + const uiRoot = liveUiRoot(); + if (uiRoot && uiRoot !== doc.body) uiRoot.appendChild(styleEl); + else doc.head.appendChild(styleEl); + return styleEl; + } + + function uiGetById(id) { + const uiRoot = liveUiRoot(); + if (uiRoot?.getElementById) { + const found = uiRoot.getElementById(id); + if (found) return found; + } + if (uiRoot?.querySelector) { + const found = uiRoot.querySelector('#' + cssId(id)); + if (found) return found; + } + return doc.getElementById(id); + } + + function activeElementDeep() { + let active = doc.activeElement; + while (active?.shadowRoot?.activeElement) active = active.shadowRoot.activeElement; + return active; + } + + function defangOutsideHandlers(rootEl, { setPointerEvents = true } = {}) { + if (!rootEl) return; + if (setPointerEvents) { + rootEl.style.setProperty('pointer-events', 'auto', 'important'); + } + const stop = (e) => e.stopPropagation(); + rootEl.addEventListener('pointerdown', stop); + rootEl.addEventListener('mousedown', stop); + rootEl.addEventListener('focusin', stop); + } + + return { + own, + pickable, + desc, + rectIsUsableAnchor, + makeFrozenAnchor, + id8, + cssId, + liveUiRoot, + uiAppend, + uiAppendStyle, + uiGetById, + activeElementDeep, + defangOutsideHandlers, + }; + } + + root.__IMPECCABLE_LIVE_DOM__ = { + version: 1, + createLiveBrowserDomHelpers, + }; +})(typeof window !== 'undefined' ? window : globalThis); diff --git a/plugin/skills/impeccable/scripts/live-browser.js b/plugin/skills/impeccable/scripts/live-browser.js index 9c4b6c21e..9092338eb 100644 --- a/plugin/skills/impeccable/scripts/live-browser.js +++ b/plugin/skills/impeccable/scripts/live-browser.js @@ -195,93 +195,31 @@ // Helpers // - function own(el) { - return el && (el.id?.startsWith(PREFIX) || el.closest?.('[id^="' + PREFIX + '"]')); - } - - function pickable(el) { - if (!el || el.nodeType !== 1) return false; - if (SKIP_TAGS.has(el.tagName.toLowerCase())) return false; - if (own(el)) return false; - const r = el.getBoundingClientRect(); - return r.width >= 20 && r.height >= 20; - } - - function desc(el) { - if (!el) return ''; - let s = el.tagName.toLowerCase(); - if (el.id) s += '#' + el.id; - else if (el.classList.length) s += '.' + [...el.classList].slice(0, 2).join('.'); - return s; - } - - function rectIsUsableAnchor(rect) { - return !!rect && rect.width > 0.5 && rect.height > 0.5; - } - - function makeFrozenAnchor(el) { - if (!el || !el.getBoundingClientRect) return null; - const r = el.getBoundingClientRect(); - if (!rectIsUsableAnchor(r)) return null; - const rect = { - x: r.x, y: r.y, - top: r.top, left: r.left, - right: r.right, bottom: r.bottom, - width: r.width, height: r.height, - }; - return { - __impeccableFrozenAnchor: true, - tagName: el.tagName || 'DIV', - id: el.id || '', - classList: el.classList ? [...el.classList] : [], - hasAttribute: () => false, - getBoundingClientRect: () => rect, - }; - } - - function id8() { return crypto.randomUUID().replace(/-/g, '').slice(0, 8); } - - function cssId(id) { - if (window.CSS?.escape) return CSS.escape(id); - return String(id).replace(/([ !"#$%&'()*+,./:;<=>?@[\\\]^`{|}~])/g, '\\$1'); - } - - function liveUiRoot() { - const root = window.__IMPECCABLE_LIVE_UI_ROOT__; - if (root && typeof root.appendChild === 'function') return root; - return document.body; - } - - function uiAppend(el) { - liveUiRoot().appendChild(el); - return el; - } - - function uiAppendStyle(styleEl) { - const root = liveUiRoot(); - if (root && root !== document.body) root.appendChild(styleEl); - else document.head.appendChild(styleEl); - return styleEl; - } - - function uiGetById(id) { - const root = liveUiRoot(); - if (root?.getElementById) { - const found = root.getElementById(id); - if (found) return found; - } - if (root?.querySelector) { - const found = root.querySelector('#' + cssId(id)); - if (found) return found; - } - return document.getElementById(id); - } - - function activeElementDeep() { - let active = document.activeElement; - while (active?.shadowRoot?.activeElement) active = active.shadowRoot.activeElement; - return active; + const domHelpers = window.__IMPECCABLE_LIVE_DOM__?.createLiveBrowserDomHelpers({ + prefix: PREFIX, + skipTags: SKIP_TAGS, + document, + }); + if (!domHelpers) { + console.error('[impeccable] live-browser-dom.js was not loaded. Live mode cannot start safely.'); + window.__IMPECCABLE_LIVE_INIT__ = false; + return; } + const { + own, + pickable, + desc, + rectIsUsableAnchor, + makeFrozenAnchor, + id8, + cssId, + liveUiRoot, + uiAppend, + uiAppendStyle, + uiGetById, + activeElementDeep, + defangOutsideHandlers, + } = domHelpers; window.__IMPECCABLE_LIVE_CHROME_CORE__ = { version: 1, @@ -314,45 +252,6 @@ }), }; - // Modal-aware chrome: keep our floating UI clickable inside Radix / - // Headless UI / vaul portals. - // - // Two host-page behaviors break us when the picked element lives inside a - // modal dialog: - // - // 1. Modal scroll-lock disables outside pointer events. Radix's - // `DismissableLayer` sets `document.body.style.pointerEvents = 'none'` - // while a modal is open and only restores `auto` on the layer. Our - // chrome inherits `none` from and becomes unclickable. - // 2. The dialog's outside-interaction handler (Radix's - // `usePointerDownOutside`) listens at document level and dismisses - // the dialog whenever a `pointerdown` lands outside the layer node. - // Our chrome is a sibling of , so Radix classifies our clicks - // as outside and tears the dialog down mid-task. - // - // We can't reliably re-parent our chrome into the dialog subtree (z-index - // stacking, scroll containers, theming all become host-page concerns), so - // we defang both behaviors at our root: - // - // - `pointer-events: auto !important` overrides the inherited `none`. - // - Stop `pointerdown` / `mousedown` propagation so the document-level - // dismiss listener never fires for our clicks. - // - Stop `focusin` propagation so any focus shifts inside our chrome - // don't read as "focus moved outside the dialog" to focus traps. - // - // Click events still bubble normally - only the early pointer/focus - // signals that drive outside-interaction detection are silenced. - function defangOutsideHandlers(rootEl, { setPointerEvents = true } = {}) { - if (!rootEl) return; - if (setPointerEvents) { - rootEl.style.setProperty('pointer-events', 'auto', 'important'); - } - const stop = (e) => e.stopPropagation(); - rootEl.addEventListener('pointerdown', stop); - rootEl.addEventListener('mousedown', stop); - rootEl.addEventListener('focusin', stop); - } - // // Highlight overlay // diff --git a/plugin/skills/impeccable/scripts/live/browser-script-parts.mjs b/plugin/skills/impeccable/scripts/live/browser-script-parts.mjs index f99704aff..9229e34f8 100644 --- a/plugin/skills/impeccable/scripts/live/browser-script-parts.mjs +++ b/plugin/skills/impeccable/scripts/live/browser-script-parts.mjs @@ -3,6 +3,7 @@ import path from 'node:path'; export const LIVE_BROWSER_SCRIPT_PARTS = Object.freeze([ Object.freeze({ name: 'session-state', file: 'live-browser-session.js' }), + Object.freeze({ name: 'dom-helpers', file: 'live-browser-dom.js' }), Object.freeze({ name: 'browser-ui', file: 'live-browser.js' }), ]);