diff --git a/.agents/skills/impeccable/scripts/live-wrap.mjs b/.agents/skills/impeccable/scripts/live-wrap.mjs index 965f6a160..7c585f32d 100644 --- a/.agents/skills/impeccable/scripts/live-wrap.mjs +++ b/.agents/skills/impeccable/scripts/live-wrap.mjs @@ -115,10 +115,12 @@ The agent should insert variant HTML at insertLine.`); const content = fs.readFileSync(targetFile, 'utf-8'); const lines = content.split('\n'); - // Find the element, trying each query in priority order + // Find the element, trying each query in priority order. + // Pass tag hint so findElement can reject matches inside wrong element types + // and walk backward to the real opener on multi-line JSX tags. let match = null; for (const q of queries) { - match = findElement(lines, q); + match = findElement(lines, q, tag); if (match) break; } if (!match) { @@ -128,16 +130,22 @@ The agent should insert variant HTML at insertLine.`); const { startLine, endLine } = match; const commentSyntax = detectCommentSyntax(targetFile); + const isJsx = commentSyntax.open === '{/*'; const indent = lines[startLine].match(/^(\s*)/)[1]; // Extract the original element const originalLines = lines.slice(startLine, endLine + 1); const originalIndented = originalLines.map(l => indent + ' ' + l.trimStart()).join('\n'); + // Wrapper attributes differ by syntax. HTML allows plain string attrs; + // JSX requires object-literal style and parses string attrs as HTML (which + // either type-errors or renders a literal CSS string). + const styleContents = isJsx ? 'style={{ display: "contents" }}' : 'style="display: contents"'; + // Build the wrapper const wrapperLines = [ indent + commentSyntax.open + ' impeccable-variants-start ' + id + ' ' + commentSyntax.close, - indent + '
', + indent + '
', indent + ' ' + commentSyntax.open + ' Original ' + commentSyntax.close, indent + '
', originalIndented, @@ -189,23 +197,28 @@ function buildSearchQueries(elementId, classes, tag, query) { queries.push('id="' + elementId + '"'); } - // 2. Full class attribute match (for elements with distinctive multi-class combos) + // 2. Full class attribute match (for elements with distinctive multi-class combos). + // Emit both class="..." (HTML) and className="..." (React/JSX) so whichever + // convention the file uses will match. if (classes) { const classList = classes.split(',').map(c => c.trim()).filter(Boolean); if (classList.length > 1) { - // Try the most distinctive class first (longest, most specific) + const joined = classList.join(' '); const sorted = [...classList].sort((a, b) => b.length - a.length); - queries.push('class="' + classList.join(' ') + '"'); // exact full match - queries.push(sorted[0]); // most distinctive single class + queries.push('class="' + joined + '"'); + queries.push('className="' + joined + '"'); + queries.push(sorted[0]); // most distinctive single class, fallback } else if (classList.length === 1) { queries.push(classList[0]); } } - // 3. Tag + class combo (e.g.,
) + // 3. Tag + class combo (e.g.,
). + // Same dual-emit for JSX compatibility. if (tag && classes) { const firstClass = classes.split(',')[0].trim(); queries.push('<' + tag + ' class="' + firstClass); + queries.push('<' + tag + ' className="' + firstClass); } // 4. Raw fallback query @@ -281,30 +294,66 @@ function searchDir(dir, query, seen, depth, genOpts) { } /** - * Find the element's start and end line in the file. - * The query is a class name, ID, or text snippet. - * We find the line containing the query, then find the matching closing tag. + * Regex that matches a tag opener on a line. Allows the tag name to be + * followed by whitespace, `>`, `/`, or end-of-line so that multi-line JSX + * openers (e.g. ``) are recognised. */ -function findElement(lines, query) { - // Find the line containing the query - let startLine = -1; +const OPENER_RE = /<([A-Za-z][A-Za-z0-9]*)(?=[\s/>]|$)/; + +/** + * Find the element's start and end line in the file. + * + * `query` is a class name, attribute fragment (`class="..."`, `className="..."`, + * `id="..."`), or a raw text snippet. Because a query can appear on a + * continuation line of a multi-line tag (e.g. the `className="..."` row of a + * `` JSX tag), we walk backward from the match + * line to find the actual tag opener. When `tag` is provided, opener candidates + * must match that tag name. + */ +function findElement(lines, query, tag = null) { + // Iterate all matches — the first substring hit isn't always the right one. for (let i = 0; i < lines.length; i++) { - if (lines[i].includes(query)) { - // Make sure this looks like a tag opening, not a comment or string - const line = lines[i].trim(); - if (line.startsWith('