fix(live): variant outline accuracy and shader re-anchor after wrap

Two regressions surfaced in smoke testing, both traceable to state
drift when live-wrap.mjs rewrites the source file and HMR swaps the
DOM.

1. Variant outline on the wrong element. The skill rewrite lost the
   explicit "each variant must be a complete element replacement"
   rule and dropped the "full element replacement" comments from
   variants 2 and 3. Models started producing variants with loose
   sibling children, so live-browser's :first-child selector framed
   only the first sibling. Restored the rule, made all three comments
   consistent, and replaced :first-child with pickVariantContent —
   which skips non-visual tags (style/script/link/meta/template) and
   falls back to the variant div itself when a model still ships
   multiple visual children.

2. Loading shader freezes after wrap. The MutationObserver only woke
   up when new non-original variants arrived, so when the wrapper
   first appeared via HMR with just the original inside, selectedElement
   was left dangling on the now-detached pre-wrap node. Scroll-tracking
   read a zero rect on every frame and collapsed the shader canvas to
   0x0. The observer now re-anchors selectedElement to the original's
   content the moment the wrapper shows up, keeping overlays positioned
   until real variants land.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Paul Bakaus
2026-04-21 15:30:09 -07:00
co-authored by Claude Opus 4.7
parent 62e5b2bb92
commit 9ccb240dc7
24 changed files with 396 additions and 72 deletions
+5 -3
View File
@@ -147,16 +147,18 @@ Write CSS + all variants in ONE edit at the `insertLine` reported by `wrap`. Col
@scope ([data-impeccable-variant="2"]) { ... }
</style>
<div data-impeccable-variant="1">
<!-- variant 1: full element replacement -->
<!-- variant 1: full element replacement (single top-level element) -->
</div>
<div data-impeccable-variant="2" style="display: none">
<!-- variant 2 -->
<!-- variant 2: full element replacement -->
</div>
<div data-impeccable-variant="3" style="display: none">
<!-- variant 3 -->
<!-- variant 3: full element replacement -->
</div>
```
**Each variant div contains exactly one top-level element — the full replacement for the original.** Use the same tag as the original (e.g. `<section>` if the user picked a `<section>`). Loose siblings (heading + paragraph + div as direct children of the variant div) break the outline tracking and the accept flow, which both assume one child.
The first variant has no `display: none` (visible by default). All others do. If variants use only inline styles and no scoped CSS, omit the `<style>` tag entirely. Use `@scope` for CSS isolation (Chrome 118+ / Firefox 128+ / Safari 17.4+).
One edit, all variants — the browser's MutationObserver picks everything up in one pass.
@@ -1248,8 +1248,7 @@
showVariantInDOM(sessionId, 1);
// Update selectedElement to the visible variant's content
const visEl = wrapper.querySelector('[data-impeccable-variant="1"] > :first-child');
selectedElement = visEl || wrapper.parentElement;
selectedElement = pickVariantContent(wrapper, 1) || wrapper.parentElement;
state = 'CYCLING';
updateBarContent('cycling');
@@ -1276,10 +1275,29 @@
if (!currentSessionId) return;
const wrapper = document.querySelector('[data-impeccable-variants="' + currentSessionId + '"]');
if (!wrapper) return;
const visEl = wrapper.querySelector('[data-impeccable-variant="' + visibleVariant + '"] > :first-child');
const visEl = pickVariantContent(wrapper, visibleVariant);
if (visEl) selectedElement = visEl;
}
// Resolve the element that represents the variant's visible content.
// Contract: each variant div should contain exactly one top-level element
// (the full replacement). In practice a model may ship loose siblings or
// lead with <style>/<script>. Be defensive: skip non-visual elements, and
// if the variant has multiple element children, use the variant div itself
// (it wraps all of them and gets correct bounds).
function pickVariantContent(wrapper, index) {
if (!wrapper) return null;
const variantDiv = wrapper.querySelector('[data-impeccable-variant="' + index + '"]');
if (!variantDiv) return null;
const NON_VISUAL = new Set(['STYLE', 'SCRIPT', 'LINK', 'META', 'TEMPLATE']);
const visual = [];
for (const child of variantDiv.children) {
if (!NON_VISUAL.has(child.tagName)) visual.push(child);
}
if (visual.length === 1) return visual[0];
return variantDiv;
}
// ---------------------------------------------------------------------------
// MutationObserver for progressive variant reveal
// ---------------------------------------------------------------------------
@@ -1307,6 +1325,13 @@
const wrapper = document.querySelector('[data-impeccable-variants="' + sessionId + '"]');
if (!wrapper) return;
// Re-anchor selectedElement if it was detached by live-wrap's HMR swap.
// Without this, the shader / highlight / bar track a zero-rect phantom
// and the overlay appears frozen.
if (selectedElement && !document.body.contains(selectedElement)) {
selectedElement = pickVariantContent(wrapper, 'original') || wrapper;
}
const variants = wrapper.querySelectorAll('[data-impeccable-variant]:not([data-impeccable-variant="original"])');
const count = variants.length;