diff --git a/package/example/src/app/SideNav.tsx b/package/example/src/app/SideNav.tsx index db3d87ab..c5398165 100644 --- a/package/example/src/app/SideNav.tsx +++ b/package/example/src/app/SideNav.tsx @@ -391,6 +391,7 @@ export function SideNav() { { id: 'smart-identification', text: 'Smart Identification' }, { id: 'computed-styles', text: 'Computed Styles' }, { id: 'react-detection', text: 'React Detection' }, + { id: 'deep-select', text: 'Deep Select' }, { id: 'keyboard-shortcuts', text: 'Keyboard Shortcuts' }, { id: 'agent-sync', text: 'Agent Sync' }, { id: 'settings', text: 'Settings' }, diff --git a/package/example/src/app/components/DeepSelectDemo.css b/package/example/src/app/components/DeepSelectDemo.css new file mode 100644 index 00000000..6f8719ca --- /dev/null +++ b/package/example/src/app/components/DeepSelectDemo.css @@ -0,0 +1,221 @@ +/* ───────────────────────────────────────────────────────── + * Deep Select Demo + * Reuses shared demo-window / demo-browser-bar / demo-content + * from FeaturesDemo.css + * ───────────────────────────────────────────────────────── */ + +/* Page layout — sidebar + main */ +.dsd-page-layout { + display: flex; + gap: 12px; + height: 100%; +} + +.dsd-sidebar { + display: flex; + flex-direction: column; + gap: 6px; + padding-top: 2px; +} + +.dsd-sidebar-item { + width: 28px; + height: 28px; + border-radius: 6px; + background: rgba(0,0,0,0.06); +} + +.dsd-sidebar-item.active { + background: rgba(0,0,0,0.12); +} + +.dsd-main { + flex: 1; + min-width: 0; +} + +.dsd-faux-title { + width: 90px; + height: 8px; + background: rgba(0,0,0,0.1); + border-radius: 4px; + margin-bottom: 10px; +} + +/* Dashboard card */ +.dsd-card { + position: relative; + background: white; + border-radius: 10px; + padding: 12px 14px; + box-shadow: 0 1px 3px rgba(0,0,0,0.06); +} + +.dsd-card-header { + display: flex; + align-items: center; + gap: 6px; + margin-bottom: 6px; +} + +.dsd-card-icon { + width: 24px; + height: 24px; + background: #3b82f6; + border-radius: 6px; + opacity: 0.15; +} + +.dsd-card-label { + font-size: 11px; + font-weight: 500; + color: rgba(0,0,0,0.4); + font-family: system-ui, sans-serif; +} + +.dsd-card-value { + font-size: 20px; + font-weight: 700; + color: rgba(0,0,0,0.8); + font-family: system-ui, sans-serif; + margin-bottom: 8px; + letter-spacing: -0.5px; +} + +/* Mini bar chart — stronger opacity so bars read as data */ +.dsd-chart { + display: flex; + align-items: flex-end; + gap: 4px; + height: 28px; + margin-bottom: 10px; +} + +.dsd-chart-bar { + flex: 1; + background: #3b82f6; + border-radius: 3px 3px 0 0; + opacity: 0.22; +} + +.dsd-chart-bar:nth-child(n+5) { + opacity: 0.35; +} + +/* Export button — the target element */ +.dsd-export-btn { + display: inline-block; + padding: 6px 14px; + background: rgba(0,0,0,0.8); + color: white; + font-size: 10px; + font-weight: 600; + font-family: system-ui, sans-serif; + border-radius: 6px; +} + +/* Secondary metric cards below the main card */ +.dsd-mini-cards { + display: flex; + gap: 6px; + margin-top: 6px; +} + +.dsd-mini-card { + flex: 1; + background: white; + border-radius: 8px; + padding: 8px 10px; + box-shadow: 0 1px 3px rgba(0,0,0,0.06); +} + +.dsd-mini-label { + font-size: 9px; + font-weight: 500; + color: rgba(0,0,0,0.35); + font-family: system-ui, sans-serif; + margin-bottom: 2px; +} + +.dsd-mini-value { + font-size: 14px; + font-weight: 700; + color: rgba(0,0,0,0.7); + font-family: system-ui, sans-serif; + letter-spacing: -0.3px; +} + +/* Invisible overlay — the problem this feature solves */ +.dsd-overlay { + position: absolute; + inset: 0; + border-radius: 10px; + z-index: 2; + background: rgba(239, 68, 68, 0); + transition: background 0.3s ease; +} + +.dsd-overlay.flash { + background: rgba(239, 68, 68, 0.05); +} + +/* Hover highlight */ +.ds-highlight { + position: absolute; + border-radius: 6px; + pointer-events: none; + z-index: 30; + opacity: 0; + transition: opacity 0.15s ease, border-color 0.15s ease; +} + +.ds-highlight.visible { + opacity: 1; +} + +.ds-highlight.normal { + border: 2px solid #3b82f6; +} + +.ds-highlight.pierce { + border: 2px dashed #3b82f6; +} + +/* Hover tooltip — shows element name */ +.ds-tooltip { + position: absolute; + padding: 3px 8px; + border-radius: 6px; + font-size: 10px; + font-weight: 500; + font-family: 'SF Mono', SFMono-Regular, ui-monospace, monospace; + white-space: nowrap; + pointer-events: none; + z-index: 35; + opacity: 0; + transform: translateY(2px); + transition: opacity 0.15s ease, transform 0.15s ease; +} + +.ds-tooltip.visible { + opacity: 1; + transform: translateY(0); +} + +.ds-tooltip.wrong { + background: rgba(0,0,0,0.75); + color: rgba(255,255,255,0.5); +} + +.ds-tooltip.correct { + background: rgba(0,0,0,0.85); + color: rgba(255,255,255,0.95); +} + +/* Pierce label on tooltip — readable, not decorative */ +.ds-pierce-label { + font-size: 9px; + color: rgba(255,255,255,0.5); + letter-spacing: 0.03em; + margin-bottom: 1px; +} diff --git a/package/example/src/app/components/DeepSelectDemo.tsx b/package/example/src/app/components/DeepSelectDemo.tsx new file mode 100644 index 00000000..a876b97e --- /dev/null +++ b/package/example/src/app/components/DeepSelectDemo.tsx @@ -0,0 +1,407 @@ +"use client"; + +import { useState, useEffect, useRef } from "react"; +import "./FeaturesDemo.css"; +import "./DeepSelectDemo.css"; + +function delay(ms: number) { + return new Promise((r) => setTimeout(r, ms)); +} + +/* ───────────────────────────────────────────────────────── + * ANIMATION STORYBOARD + * + * 0ms reset — cursor top-right, caption: idle + * 600ms cursor moves toward "Export" button + * 1000ms NORMAL: solid highlight on entire card + overlay flash + * → tooltip: div.AnimatePresence (wrong/dimmed) + * → caption: "Normal hover selects the invisible wrapper." + * 2600ms highlight + tooltip fade out + * 3000ms caption: "Hold ⌘ to pierce through overlay layers." + * 5000ms PIERCE: dashed highlight on just the button + * → tooltip: button "Export" (correct/bright) + * → caption: "Deep select finds the actual element underneath." + * 6400ms popup appears, types "Add loading state" + * 8200ms popup closes, marker placed + * 10200ms marker fades, loop + * ───────────────────────────────────────────────────────── */ + +const LOOP_INTERVAL = 11800; +const CHART_HEIGHTS = [45, 60, 35, 75, 55, 90, 70, 85]; + +type CaptionKey = "idle" | "cmd" | "correct"; + +const CAPTIONS: Record = { + idle: "Normal hover selects the invisible animation wrapper.", + cmd: "Hold \u2318 to pierce through overlay layers.", + correct: "Deep select finds the actual element underneath.", +}; + +export function DeepSelectDemo() { + const [cursorPos, setCursorPos] = useState({ x: 280, y: 40 }); + const [activeCaption, setActiveCaption] = useState("idle"); + + const [highlight, setHighlight] = useState<{ + visible: boolean; + mode: "normal" | "pierce"; + rect: { x: number; y: number; w: number; h: number }; + }>({ visible: false, mode: "normal", rect: { x: 0, y: 0, w: 0, h: 0 } }); + + const [tooltip, setTooltip] = useState<{ + visible: boolean; + text: string; + type: "wrong" | "correct"; + x: number; + y: number; + }>({ visible: false, text: "", type: "wrong", x: 0, y: 0 }); + + const [showPopup, setShowPopup] = useState(false); + const [typedText, setTypedText] = useState(""); + const [showMarker, setShowMarker] = useState(false); + const [overlayFlash, setOverlayFlash] = useState(false); + + const cardRef = useRef(null); + const btnRef = useRef(null); + const contentRef = useRef(null); + + const cardPosRef = useRef({ x: 0, y: 0, w: 0, h: 0 }); + const btnPosRef = useRef({ x: 0, y: 0, w: 0, h: 0 }); + + const measure = () => { + if (!cardRef.current || !btnRef.current || !contentRef.current) return; + const cRect = contentRef.current.getBoundingClientRect(); + const cardRect = cardRef.current.getBoundingClientRect(); + const bRect = btnRef.current.getBoundingClientRect(); + cardPosRef.current = { + x: cardRect.left - cRect.left, + y: cardRect.top - cRect.top, + w: cardRect.width, + h: cardRect.height, + }; + btnPosRef.current = { + x: bRect.left - cRect.left, + y: bRect.top - cRect.top, + w: bRect.width, + h: bRect.height, + }; + }; + + useEffect(() => { + const timer = setTimeout(measure, 100); + window.addEventListener("resize", measure); + return () => { + clearTimeout(timer); + window.removeEventListener("resize", measure); + }; + }, []); + + useEffect(() => { + let cancelled = false; + const feedbackText = "Add loading state"; + + const run = async () => { + // Reset + setCursorPos({ x: 280, y: 40 }); + setHighlight({ visible: false, mode: "normal", rect: { x: 0, y: 0, w: 0, h: 0 } }); + setTooltip({ visible: false, text: "", type: "wrong", x: 0, y: 0 }); + setShowPopup(false); + setTypedText(""); + setShowMarker(false); + setOverlayFlash(false); + setActiveCaption("idle"); + + await delay(600); + if (cancelled) return; + + // Re-measure before using positions (layout may have shifted) + measure(); + const card = cardPosRef.current; + const btn = btnPosRef.current; + setCursorPos({ x: btn.x + btn.w / 2, y: btn.y + btn.h / 2 }); + await delay(400); + if (cancelled) return; + + // Normal hover — highlights the whole card (the overlay intercepts) + setOverlayFlash(true); + setHighlight({ + visible: true, + mode: "normal", + rect: { x: card.x - 3, y: card.y - 3, w: card.w + 6, h: card.h + 6 }, + }); + setTooltip({ + visible: true, + text: "div.AnimatePresence", + type: "wrong", + x: card.x + card.w / 2, + y: card.y - 10, + }); + await delay(1600); + if (cancelled) return; + + // Fade highlight + tooltip + overlay flash + setHighlight((h) => ({ ...h, visible: false })); + setTooltip((t) => ({ ...t, visible: false })); + setOverlayFlash(false); + await delay(400); + if (cancelled) return; + + // ⌘ beat — caption explains the feature + setActiveCaption("cmd"); + await delay(2000); + if (cancelled) return; + + // Pierce hover — highlights just the button + setActiveCaption("correct"); + setHighlight({ + visible: true, + mode: "pierce", + rect: { x: btn.x - 3, y: btn.y - 3, w: btn.w + 6, h: btn.h + 6 }, + }); + setTooltip({ + visible: true, + text: 'button "Export"', + type: "correct", + x: btn.x + btn.w / 2, + y: btn.y - 10, + }); + await delay(1400); + if (cancelled) return; + + // Click — show popup + setShowPopup(true); + await delay(300); + if (cancelled) return; + + // Type feedback + for (let i = 0; i <= feedbackText.length; i++) { + if (cancelled) return; + setTypedText(feedbackText.slice(0, i)); + await delay(30); + } + await delay(400); + if (cancelled) return; + + // Close popup, show marker + setShowPopup(false); + setHighlight((h) => ({ ...h, visible: false })); + setTooltip((t) => ({ ...t, visible: false })); + await delay(200); + if (cancelled) return; + setShowMarker(true); + + await delay(2200); + if (cancelled) return; + + // Clean up for next loop + setShowMarker(false); + await delay(300); + }; + + run(); + let interval = setInterval(run, LOOP_INTERVAL); + + const handleVisibility = () => { + if (document.visibilityState === "visible") { + cancelled = true; + clearInterval(interval); + setTimeout(() => { + cancelled = false; + run(); + interval = setInterval(run, LOOP_INTERVAL); + }, 100); + } + }; + document.addEventListener("visibilitychange", handleVisibility); + + return () => { + cancelled = true; + clearInterval(interval); + document.removeEventListener("visibilitychange", handleVisibility); + }; + }, []); + + return ( +
+
+
+
+
+
+
localhost:3000/dashboard
+
+ +
+
+ {/* Sidebar nav */} +
+
+
+
+
+
+ + {/* Main content area */} +
+
+ +
+ {/* Invisible overlay — the problem */} +
+
+
+
Monthly Revenue
+
+
$12.4k
+
+ {CHART_HEIGHTS.map((h, i) => ( +
+ ))} +
+
Export
+
+ + {/* Secondary metric cards */} +
+
+
Users
+
1,847
+
+
+
Conversion
+
3.2%
+
+
+
+
+ + {/* Highlight */} +
+ + {/* Tooltip */} +
+ {tooltip.type === "correct" &&
{"\u21E3"} deep select
} + {tooltip.text} +
+ + {/* Popup */} +
+
button "Export"
+
+ {typedText}| +
+
+
Cancel
+
Add
+
+
+ + {/* Marker */} +
+ 1 +
+ + {/* Cursor — offset by half SVG size so crosshair center lands on target */} +
+ + + + +
+ + {/* Toolbar */} +
+
+ + + + + +
+ +
+
+
+
+ + {/* Caption — updates with animation state, matching SmartIdentificationDemo pattern */} +

+ {CAPTIONS[activeCaption]} +

+
+ ); +} + +/* ───────────────────────────────────────────────────────── + * Shared toolbar icon — same as FeaturesDemo + * ───────────────────────────────────────────────────────── */ + +function ToolbarIcon({ icon, disabled }: { icon: string; disabled?: boolean }) { + const disabledStyle = disabled ? { opacity: 0.35 } : undefined; + + const icons: Record = { + pause: ( + + + + + ), + eye: ( + + + + + ), + copy: ( + + + + + ), + trash: ( + + + + + + + + ), + settings: ( + + + + + ), + close: ( + + + + + ), + }; + + return ( +
+
+ {icons[icon]} +
+
+ ); +} diff --git a/package/example/src/app/features/page.tsx b/package/example/src/app/features/page.tsx index 0996afff..bcbc6c0d 100644 --- a/package/example/src/app/features/page.tsx +++ b/package/example/src/app/features/page.tsx @@ -2,6 +2,7 @@ import { Footer } from "../Footer"; import { FeaturesDemo, SettingsDemo, SmartIdentificationDemo, MarkerKeyDemo, ComputedStylesDemo, ReactDetectionDemo, AgentChatDemo } from "../components/FeaturesDemo"; +import { DeepSelectDemo } from "../components/DeepSelectDemo"; export default function FeaturesPage() { return ( @@ -82,6 +83,15 @@ export default function FeaturesPage() { +
+

Deep select

+

+ Hold while hovering to pierce through invisible wrappers and select the actual element underneath. + Useful when animation libraries, video frameworks, or overlay patterns render empty divs on top of your content. +

+ +
+

Keyboard shortcuts

@@ -107,8 +117,12 @@ export default function FeaturesPage() { - - + + + + + +
Copy feedback
XClear all annotationsXClear all annotations
/ Ctrl + hoverDeep select (pierce overlays)
diff --git a/package/src/components/page-toolbar-css/index.tsx b/package/src/components/page-toolbar-css/index.tsx index 29fab589..b459d7a5 100644 --- a/package/src/components/page-toolbar-css/index.tsx +++ b/package/src/components/page-toolbar-css/index.tsx @@ -136,6 +136,7 @@ type HoverInfo = { elementPath: string; rect: DOMRect | null; reactComponents?: string | null; + isPiercing?: boolean; }; type OutputDetailLevel = "compact" | "standard" | "detailed" | "forensic"; @@ -214,22 +215,113 @@ const COLOR_OPTIONS = [ // ============================================================================= /** - * Recursively pierces shadow DOMs to find the deepest element at a point. - * document.elementFromPoint() stops at shadow hosts, so we need to - * recursively check inside open shadow roots to find the actual target. + * Pierce through shadow DOMs to find the actual element. + */ +function pierceShadowDOM( + element: HTMLElement, + x: number, + y: number, +): HTMLElement { + let el = element; + while (el?.shadowRoot) { + const deeper = el.shadowRoot.elementFromPoint(x, y) as HTMLElement | null; + if (!deeper || deeper === el) break; + el = deeper; + } + return el; +} + +/** + * Finds the deepest element at a point, piercing shadow DOMs. */ function deepElementFromPoint(x: number, y: number): HTMLElement | null { - let element = document.elementFromPoint(x, y) as HTMLElement | null; + const element = document.elementFromPoint(x, y) as HTMLElement | null; if (!element) return null; + return pierceShadowDOM(element, x, y); +} - // Keep drilling down through shadow roots - while (element?.shadowRoot) { - const deeper = element.shadowRoot.elementFromPoint(x, y) as HTMLElement | null; - if (!deeper || deeper === element) break; - element = deeper; +// ============================================================================= +// Pierce mode (Cmd+hover) — scans through container wrappers and invisible +// elements to find the actual content underneath. Useful for annotation in +// animation-heavy frameworks (Remotion, Framer Motion, etc.) where empty +// overlay divs intercept pointer events. +// ============================================================================= + +const GENERIC_CONTAINER_TAGS = new Set([ + "DIV", "SPAN", "SECTION", "ARTICLE", "MAIN", "ASIDE", "HEADER", "FOOTER", "NAV", +]); + +function isEffectivelyInvisible(el: HTMLElement): boolean { + if (typeof el.checkVisibility === "function") { + return !el.checkVisibility({ checkOpacity: true, checkVisibilityCSS: true }); + } + let current: HTMLElement | null = el; + while (current && current !== document.body) { + if (window.getComputedStyle(current).opacity === "0") return true; + current = current.parentElement; + } + return false; +} + +function hasDirectContent(el: HTMLElement): boolean { + if (!GENERIC_CONTAINER_TAGS.has(el.tagName)) return true; + for (const child of el.childNodes) { + if (child.nodeType === Node.TEXT_NODE && child.textContent?.trim()) return true; + } + return false; +} + +/** + * Pierce mode: scans all elements at a point to find the deepest one with + * visible, meaningful content. Two-pass approach: + * 1. Find elements with direct text content (skips empty wrappers) + * 2. If no text found, return the smallest visible element (catches + * visual-only elements like timeline bars, chart segments, swatches) + * Activated by holding Cmd (Mac) / Ctrl (Windows) while hovering. + */ +function pierceElementFromPoint(x: number, y: number): HTMLElement | null { + const topElement = document.elementFromPoint(x, y) as HTMLElement | null; + if (!topElement) return null; + + const pierced = pierceShadowDOM(topElement, x, y); + if (hasDirectContent(pierced) && !isEffectivelyInvisible(pierced)) + return pierced; + + const allElements = document.elementsFromPoint(x, y) as HTMLElement[]; + + // Pass 1: find first element with direct text content + for (const candidate of allElements) { + if (candidate === topElement) continue; + if (candidate === document.documentElement || candidate === document.body) + continue; + const deep = pierceShadowDOM(candidate, x, y); + if (hasDirectContent(deep) && !isEffectivelyInvisible(deep)) + return deep; } - return element; + // Pass 2: no text content found — return the smallest visible element. + // Catches visual-only elements (timeline bars, color swatches, progress + // indicators, chart segments) that communicate through color/size, not text. + const topRect = pierced.getBoundingClientRect(); + const topArea = topRect.width * topRect.height; + let smallest: HTMLElement | null = null; + let smallestArea = topArea; + + for (const candidate of allElements) { + if (candidate === topElement) continue; + if (candidate === document.documentElement || candidate === document.body) + continue; + if (isEffectivelyInvisible(candidate)) continue; + const deep = pierceShadowDOM(candidate, x, y); + const rect = deep.getBoundingClientRect(); + const area = rect.width * rect.height; + if (area > 0 && area < smallestArea) { + smallest = deep; + smallestArea = area; + } + } + + return smallest || pierced; } function isElementFixed(element: HTMLElement): boolean { @@ -1630,7 +1722,11 @@ export function PageFeedbackToolbarCSS({ return; } - const elementUnder = deepElementFromPoint(e.clientX, e.clientY); + // Cmd (Mac) / Ctrl (Win) = pierce mode: scan through overlays + const piercing = e.metaKey || e.ctrlKey; + const elementUnder = piercing + ? pierceElementFromPoint(e.clientX, e.clientY) + : deepElementFromPoint(e.clientX, e.clientY); if ( !elementUnder || closestCrossingShadow(elementUnder, "[data-feedback-toolbar]") @@ -1649,6 +1745,7 @@ export function PageFeedbackToolbarCSS({ elementPath: path, rect, reactComponents, + isPiercing: piercing, }); setHoverPosition({ x: e.clientX, y: e.clientY }); }; @@ -1675,11 +1772,12 @@ export function PageFeedbackToolbarCSS({ if (closestCrossingShadow(target, "[data-annotation-marker]")) return; // Handle cmd+shift+click for multi-element selection + // Cmd is held so pierce mode is active — use pierceElementFromPoint if (e.metaKey && e.shiftKey && !pendingAnnotation && !editingAnnotation) { e.preventDefault(); e.stopPropagation(); - const elementUnder = deepElementFromPoint(e.clientX, e.clientY); + const elementUnder = pierceElementFromPoint(e.clientX, e.clientY); if (!elementUnder) return; const rect = elementUnder.getBoundingClientRect(); @@ -1746,7 +1844,11 @@ export function PageFeedbackToolbarCSS({ e.preventDefault(); - const elementUnder = deepElementFromPoint(e.clientX, e.clientY); + // Cmd (Mac) / Ctrl (Win) without Shift = pierce mode click + const piercing = (e.metaKey || e.ctrlKey) && !e.shiftKey; + const elementUnder = piercing + ? pierceElementFromPoint(e.clientX, e.clientY) + : deepElementFromPoint(e.clientX, e.clientY); if (!elementUnder) return; const { name, path, reactComponents } = identifyElementWithReact( @@ -4058,6 +4160,7 @@ export function PageFeedbackToolbarCSS({ height: hoverInfo.rect.height, borderColor: `${settings.annotationColor}80`, backgroundColor: `${settings.annotationColor}0A`, + ...(hoverInfo.isPiercing ? { borderStyle: "dashed" } : {}), }} /> )} @@ -4189,11 +4292,16 @@ export function PageFeedbackToolbarCSS({ Math.min(hoverPosition.x, window.innerWidth - 100), ), top: Math.max( - hoverPosition.y - (hoverInfo.reactComponents ? 48 : 32), + hoverPosition.y - (hoverInfo.isPiercing ? 62 : hoverInfo.reactComponents ? 48 : 32), 8, ), }} > + {hoverInfo.isPiercing && ( +
+ {"⇣ deep select"} +
+ )} {hoverInfo.reactComponents && (
{hoverInfo.reactComponents} diff --git a/package/src/components/page-toolbar-css/styles.module.scss b/package/src/components/page-toolbar-css/styles.module.scss index fe6217c3..8f3057a0 100644 --- a/package/src/components/page-toolbar-css/styles.module.scss +++ b/package/src/components/page-toolbar-css/styles.module.scss @@ -885,6 +885,13 @@ $green: #34c759; } } +.hoverPierceIndicator { + font-size: 0.5625rem; + color: rgba(255, 255, 255, 0.45); + margin-bottom: 0.1rem; + letter-spacing: 0.03em; +} + .hoverReactPath { font-size: 0.625rem; color: rgba(255, 255, 255, 0.6); diff --git a/package/src/utils/element-identification.ts b/package/src/utils/element-identification.ts index f86eb97e..e505ce8b 100644 --- a/package/src/utils/element-identification.ts +++ b/package/src/utils/element-identification.ts @@ -97,6 +97,21 @@ export function getElementPath(target: HTMLElement, maxDepth = 4): string { return parts.join(" > "); } +/** + * Gets concatenated direct text node content from an element. + * Only includes immediate text nodes, not text from child elements. + */ +function getDirectTextContent(el: HTMLElement): string { + let text = ""; + for (const child of el.childNodes) { + if (child.nodeType === Node.TEXT_NODE) { + const t = child.textContent?.trim(); + if (t) text += (text ? " " : "") + t; + } + } + return text; +} + /** * Identifies an element and returns a human-readable name + path */ @@ -200,6 +215,12 @@ export function identifyElement(target: HTMLElement): { name: string; path: stri if (ariaLabel) return { name: `${tag} [${ariaLabel}]`, path }; if (role) return { name: `${role}`, path }; + // Prefer direct text content over class names — "$54" is more useful than "styles productPrice" + const directText = getDirectTextContent(target); + if (directText && directText.length < 50) { + return { name: `"${directText}"`, path }; + } + if (typeof className === "string" && className) { const words = className .split(/[\s_-]+/) diff --git a/package/src/utils/react-detection.ts b/package/src/utils/react-detection.ts index 8068621d..984f84f2 100644 --- a/package/src/utils/react-detection.ts +++ b/package/src/utils/react-detection.ts @@ -87,6 +87,9 @@ export const DEFAULT_SKIP_EXACT = new Set([ "ErrorBoundaryHandler", "HotReload", "Hot", + // Video framework internals + "SeriesSequence", + "AbsoluteFill", ]); /** @@ -114,6 +117,11 @@ export const DEFAULT_SKIP_PATTERNS: RegExp[] = [ /^With[A-Z]/, // withRouter, WithAuth (HOCs) /Wrapper$/, // Generic wrappers /^Root$/, // Generic Root component + // React internal wrappers + /RefForwarding/, // React.forwardRef wrapper names + // Video framework internals (Remotion, etc.) + /^Remotion/, // Remotion-prefixed internals + /^TransitionSeries/, // TransitionSeries wrappers ]; /**