|
|
@ -65,76 +65,55 @@ function getHierarchy(startEl: Element | null): Element[] { |
|
|
return list; // [0: innermost leaf, ..., N-1: outermost container]
|
|
|
return list; // [0: innermost leaf, ..., N-1: outermost container]
|
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
export type HierarchyMode = "OUTER" | "PARENT" | "INNER" | "LEVEL"; |
|
|
|
|
|
|
|
|
|
|
|
interface HierarchyTarget { |
|
|
|
|
|
element: Element; |
|
|
|
|
|
index: number; |
|
|
|
|
|
total: number; |
|
|
|
|
|
mode: HierarchyMode; |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
function selectTargetFromHierarchy( |
|
|
|
|
|
|
|
|
function selectTargetElement( |
|
|
hierarchy: Element[], |
|
|
hierarchy: Element[], |
|
|
baseMode: "OUTER" | "PARENT" | "INNER", |
|
|
|
|
|
customIndex: number | null |
|
|
|
|
|
): HierarchyTarget | null { |
|
|
|
|
|
|
|
|
isInnerMode: boolean |
|
|
|
|
|
): Element | null { |
|
|
if (hierarchy.length === 0) return null; |
|
|
if (hierarchy.length === 0) return null; |
|
|
const total = hierarchy.length; |
|
|
|
|
|
|
|
|
|
|
|
if (customIndex !== null && customIndex >= 0 && customIndex < total) { |
|
|
|
|
|
let mode: HierarchyMode = "LEVEL"; |
|
|
|
|
|
if (customIndex === 0) mode = "INNER"; |
|
|
|
|
|
else if (customIndex === total - 1) mode = "OUTER"; |
|
|
|
|
|
else if (customIndex === 1) mode = "PARENT"; |
|
|
|
|
|
|
|
|
|
|
|
return { |
|
|
|
|
|
element: hierarchy[customIndex], |
|
|
|
|
|
index: customIndex, |
|
|
|
|
|
total, |
|
|
|
|
|
mode, |
|
|
|
|
|
}; |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
if (hierarchy.length === 1) return hierarchy[0]; |
|
|
|
|
|
|
|
|
if (baseMode === "INNER" || total === 1) { |
|
|
|
|
|
return { |
|
|
|
|
|
element: hierarchy[0], |
|
|
|
|
|
index: 0, |
|
|
|
|
|
total, |
|
|
|
|
|
mode: "INNER", |
|
|
|
|
|
}; |
|
|
|
|
|
|
|
|
if (isInnerMode) { |
|
|
|
|
|
// Inner Mode (Alt + Ctrl): absolute innermost leaf element (<path>, <span>, <p>, etc.)
|
|
|
|
|
|
return hierarchy[0]; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
if (baseMode === "PARENT") { |
|
|
|
|
|
const parentIdx = Math.min(1, total - 1); |
|
|
|
|
|
return { |
|
|
|
|
|
element: hierarchy[parentIdx], |
|
|
|
|
|
index: parentIdx, |
|
|
|
|
|
total, |
|
|
|
|
|
mode: "PARENT", |
|
|
|
|
|
}; |
|
|
|
|
|
|
|
|
// Outer Mode (Alt): Intermediate component wrapper / button (pushed one level inside)
|
|
|
|
|
|
const leafTag = hierarchy[0].tagName.toLowerCase(); |
|
|
|
|
|
const isSvgLeaf = [ |
|
|
|
|
|
"path", |
|
|
|
|
|
"circle", |
|
|
|
|
|
"g", |
|
|
|
|
|
"rect", |
|
|
|
|
|
"line", |
|
|
|
|
|
"polyline", |
|
|
|
|
|
"polygon", |
|
|
|
|
|
"ellipse", |
|
|
|
|
|
"defs", |
|
|
|
|
|
].includes(leafTag); |
|
|
|
|
|
|
|
|
|
|
|
if (isSvgLeaf) { |
|
|
|
|
|
// If leaf is inside an SVG, select the immediate element enclosing <svg> (e.g. <button>, <a>, <div>)
|
|
|
|
|
|
const outsideSvgIndex = hierarchy.findIndex( |
|
|
|
|
|
(el, idx) => idx > 0 && el.tagName.toLowerCase() !== "svg" |
|
|
|
|
|
); |
|
|
|
|
|
if (outsideSvgIndex !== -1) { |
|
|
|
|
|
return hierarchy[outsideSvgIndex]; |
|
|
|
|
|
} |
|
|
|
|
|
return hierarchy[1] || hierarchy[0]; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
// Outer Mode: find outermost component container, filtering out root page/layout if deeper elements exist
|
|
|
|
|
|
const nonRootIndices: number[] = []; |
|
|
|
|
|
hierarchy.forEach((el, idx) => { |
|
|
|
|
|
|
|
|
// For standard elements (e.g. <span> inside <button>, or <p> inside <div>):
|
|
|
|
|
|
// Return the intermediate parent/wrapper directly above the leaf
|
|
|
|
|
|
const nonRoot = hierarchy.filter((el) => { |
|
|
const loc = el.getAttribute("data-locator") || ""; |
|
|
const loc = el.getAttribute("data-locator") || ""; |
|
|
if (!loc.includes("app/layout.") && !loc.includes("app/page.")) { |
|
|
|
|
|
nonRootIndices.push(idx); |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
return !loc.includes("app/layout.") && !loc.includes("app/page."); |
|
|
}); |
|
|
}); |
|
|
|
|
|
|
|
|
const outerIdx = |
|
|
|
|
|
nonRootIndices.length > 0 |
|
|
|
|
|
? nonRootIndices[nonRootIndices.length - 1] |
|
|
|
|
|
: total - 1; |
|
|
|
|
|
|
|
|
if (nonRoot.length > 1) { |
|
|
|
|
|
return nonRoot[1]; |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
return { |
|
|
|
|
|
element: hierarchy[outerIdx], |
|
|
|
|
|
index: outerIdx, |
|
|
|
|
|
total, |
|
|
|
|
|
mode: "OUTER", |
|
|
|
|
|
}; |
|
|
|
|
|
|
|
|
return hierarchy[1] || hierarchy[0]; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
interface HoveredTargetState { |
|
|
interface HoveredTargetState { |
|
|
@ -142,10 +121,8 @@ interface HoveredTargetState { |
|
|
locator: string; |
|
|
locator: string; |
|
|
borderRadius: string; |
|
|
borderRadius: string; |
|
|
tagName: string; |
|
|
tagName: string; |
|
|
mode: HierarchyMode; |
|
|
|
|
|
levelIndex: number; |
|
|
|
|
|
totalLevels: number; |
|
|
|
|
|
hierarchyTags: string[]; |
|
|
|
|
|
|
|
|
isInner: boolean; |
|
|
|
|
|
hasMultipleLevels: boolean; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
export function DevClickToComponent() { |
|
|
export function DevClickToComponent() { |
|
|
@ -155,20 +132,11 @@ export function DevClickToComponent() { |
|
|
const [isDragging, setIsDragging] = useState(false); |
|
|
const [isDragging, setIsDragging] = useState(false); |
|
|
const [hoveredTarget, setHoveredTarget] = useState<HoveredTargetState | null>(null); |
|
|
const [hoveredTarget, setHoveredTarget] = useState<HoveredTargetState | null>(null); |
|
|
const [isOpening, setIsOpening] = useState(false); |
|
|
const [isOpening, setIsOpening] = useState(false); |
|
|
const [customDepthIndex, setCustomDepthIndex] = useState<number | null>(null); |
|
|
|
|
|
|
|
|
|
|
|
const currentElementRef = useRef<Element | null>(null); |
|
|
const currentElementRef = useRef<Element | null>(null); |
|
|
const currentHierarchyRef = useRef<Element[]>([]); |
|
|
|
|
|
const currentDepthIndexRef = useRef<number | null>(null); |
|
|
|
|
|
const lastPointerPos = useRef<{ x: number; y: number } | null>(null); |
|
|
const lastPointerPos = useRef<{ x: number; y: number } | null>(null); |
|
|
const lastHoveredLeafRef = useRef<Element | null>(null); |
|
|
|
|
|
const dragStart = useRef({ x: 0, y: 0, buttonX: 0, buttonY: 0, hasMoved: false }); |
|
|
const dragStart = useRef({ x: 0, y: 0, buttonX: 0, buttonY: 0, hasMoved: false }); |
|
|
|
|
|
|
|
|
// Keep ref synced
|
|
|
|
|
|
useEffect(() => { |
|
|
|
|
|
currentDepthIndexRef.current = customDepthIndex; |
|
|
|
|
|
}, [customDepthIndex]); |
|
|
|
|
|
|
|
|
|
|
|
// Detect mobile / small screen size
|
|
|
// Detect mobile / small screen size
|
|
|
useEffect(() => { |
|
|
useEffect(() => { |
|
|
const checkSize = () => { |
|
|
const checkSize = () => { |
|
|
@ -228,47 +196,27 @@ export function DevClickToComponent() { |
|
|
} |
|
|
} |
|
|
}, []); |
|
|
}, []); |
|
|
|
|
|
|
|
|
// Desktop: Alt (Outer) vs Alt+Shift (Parent) vs Alt+Ctrl (Inner) vs Scroll Wheel / Arrow depth stepping
|
|
|
|
|
|
|
|
|
// Desktop: Alt (Outer / Wrapper) vs Alt + Ctrl (Inner Leaf)
|
|
|
useEffect(() => { |
|
|
useEffect(() => { |
|
|
if (process.env.NODE_ENV !== "development") { |
|
|
if (process.env.NODE_ENV !== "development") { |
|
|
return; |
|
|
return; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
const updateHoverTarget = ( |
|
|
|
|
|
el: Element | null, |
|
|
|
|
|
baseMode: "OUTER" | "PARENT" | "INNER", |
|
|
|
|
|
overrideDepthIndex?: number | null |
|
|
|
|
|
) => { |
|
|
|
|
|
|
|
|
const updateHoverTarget = (el: Element | null, isInnerMode: boolean) => { |
|
|
const hierarchy = getHierarchy(el); |
|
|
const hierarchy = getHierarchy(el); |
|
|
currentHierarchyRef.current = hierarchy; |
|
|
|
|
|
|
|
|
|
|
|
const leafEl = hierarchy[0] || null; |
|
|
|
|
|
let depthIdx = overrideDepthIndex !== undefined ? overrideDepthIndex : currentDepthIndexRef.current; |
|
|
|
|
|
|
|
|
const targetEl = selectTargetElement(hierarchy, isInnerMode); |
|
|
|
|
|
|
|
|
// If moved to a completely different leaf element, reset custom depth index
|
|
|
|
|
|
if (leafEl !== lastHoveredLeafRef.current) { |
|
|
|
|
|
lastHoveredLeafRef.current = leafEl; |
|
|
|
|
|
if (overrideDepthIndex === undefined) { |
|
|
|
|
|
depthIdx = null; |
|
|
|
|
|
setCustomDepthIndex(null); |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
const target = selectTargetFromHierarchy(hierarchy, baseMode, depthIdx); |
|
|
|
|
|
|
|
|
|
|
|
if (target) { |
|
|
|
|
|
currentElementRef.current = target.element; |
|
|
|
|
|
const rect = target.element.getBoundingClientRect(); |
|
|
|
|
|
const computed = window.getComputedStyle(target.element); |
|
|
|
|
|
|
|
|
if (targetEl) { |
|
|
|
|
|
currentElementRef.current = targetEl; |
|
|
|
|
|
const rect = targetEl.getBoundingClientRect(); |
|
|
|
|
|
const computed = window.getComputedStyle(targetEl); |
|
|
setHoveredTarget({ |
|
|
setHoveredTarget({ |
|
|
rect, |
|
|
rect, |
|
|
locator: target.element.getAttribute("data-locator") || "", |
|
|
|
|
|
|
|
|
locator: targetEl.getAttribute("data-locator") || "", |
|
|
borderRadius: computed.borderRadius, |
|
|
borderRadius: computed.borderRadius, |
|
|
tagName: target.element.tagName.toLowerCase(), |
|
|
|
|
|
mode: target.mode, |
|
|
|
|
|
levelIndex: target.index, |
|
|
|
|
|
totalLevels: target.total, |
|
|
|
|
|
hierarchyTags: hierarchy.map((h) => h.tagName.toLowerCase()), |
|
|
|
|
|
|
|
|
tagName: targetEl.tagName.toLowerCase(), |
|
|
|
|
|
isInner: isInnerMode || hierarchy.length === 1, |
|
|
|
|
|
hasMultipleLevels: hierarchy.length > 1, |
|
|
}); |
|
|
}); |
|
|
} else { |
|
|
} else { |
|
|
currentElementRef.current = null; |
|
|
currentElementRef.current = null; |
|
|
@ -276,121 +224,53 @@ export function DevClickToComponent() { |
|
|
} |
|
|
} |
|
|
}; |
|
|
}; |
|
|
|
|
|
|
|
|
const determineBaseMode = (e: { ctrlKey?: boolean; metaKey?: boolean; shiftKey?: boolean }): "OUTER" | "PARENT" | "INNER" => { |
|
|
|
|
|
if (e.ctrlKey || e.metaKey) return "INNER"; |
|
|
|
|
|
if (e.shiftKey) return "PARENT"; |
|
|
|
|
|
return "OUTER"; |
|
|
|
|
|
}; |
|
|
|
|
|
|
|
|
|
|
|
const handlePointerMove = (e: PointerEvent) => { |
|
|
const handlePointerMove = (e: PointerEvent) => { |
|
|
lastPointerPos.current = { x: e.clientX, y: e.clientY }; |
|
|
lastPointerPos.current = { x: e.clientX, y: e.clientY }; |
|
|
|
|
|
|
|
|
if (e.altKey) { |
|
|
if (e.altKey) { |
|
|
const target = e.target instanceof Element ? e.target : null; |
|
|
const target = e.target instanceof Element ? e.target : null; |
|
|
const mode = determineBaseMode(e); |
|
|
|
|
|
updateHoverTarget(target, mode); |
|
|
|
|
|
|
|
|
const isInnerMode = Boolean(e.ctrlKey || e.metaKey); |
|
|
|
|
|
updateHoverTarget(target, isInnerMode); |
|
|
} else if (currentElementRef.current) { |
|
|
} else if (currentElementRef.current) { |
|
|
currentElementRef.current = null; |
|
|
currentElementRef.current = null; |
|
|
setHoveredTarget(null); |
|
|
setHoveredTarget(null); |
|
|
setCustomDepthIndex(null); |
|
|
|
|
|
} |
|
|
} |
|
|
}; |
|
|
}; |
|
|
|
|
|
|
|
|
const handleKeyDown = (e: KeyboardEvent) => { |
|
|
const handleKeyDown = (e: KeyboardEvent) => { |
|
|
const isAlt = e.altKey || e.key === "Alt"; |
|
|
const isAlt = e.altKey || e.key === "Alt"; |
|
|
if (!isAlt || !lastPointerPos.current) return; |
|
|
|
|
|
|
|
|
|
|
|
const hierarchy = currentHierarchyRef.current; |
|
|
|
|
|
const total = hierarchy.length; |
|
|
|
|
|
|
|
|
|
|
|
// Level Stepping: ArrowDown or '[' steps deeper (towards inner leaf 0)
|
|
|
|
|
|
if (e.key === "ArrowDown" || e.key === "[") { |
|
|
|
|
|
e.preventDefault(); |
|
|
|
|
|
if (total > 0) { |
|
|
|
|
|
const current = currentDepthIndexRef.current ?? (e.shiftKey ? 1 : total - 1); |
|
|
|
|
|
const next = Math.max(0, current - 1); |
|
|
|
|
|
setCustomDepthIndex(next); |
|
|
|
|
|
const el = document.elementFromPoint(lastPointerPos.current.x, lastPointerPos.current.y); |
|
|
|
|
|
updateHoverTarget(el, "LEVEL" as any, next); |
|
|
|
|
|
} |
|
|
|
|
|
return; |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
// Level Stepping: ArrowUp or ']' steps outwards (towards outer container N-1)
|
|
|
|
|
|
if (e.key === "ArrowUp" || e.key === "]") { |
|
|
|
|
|
e.preventDefault(); |
|
|
|
|
|
if (total > 0) { |
|
|
|
|
|
const current = currentDepthIndexRef.current ?? (e.shiftKey ? 1 : 0); |
|
|
|
|
|
const next = Math.min(total - 1, current + 1); |
|
|
|
|
|
setCustomDepthIndex(next); |
|
|
|
|
|
const el = document.elementFromPoint(lastPointerPos.current.x, lastPointerPos.current.y); |
|
|
|
|
|
updateHoverTarget(el, "LEVEL" as any, next); |
|
|
|
|
|
} |
|
|
|
|
|
return; |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
const mode = determineBaseMode({ |
|
|
|
|
|
ctrlKey: e.ctrlKey || e.key === "Control", |
|
|
|
|
|
metaKey: e.metaKey || e.key === "Meta", |
|
|
|
|
|
shiftKey: e.shiftKey || e.key === "Shift", |
|
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
|
|
const el = document.elementFromPoint( |
|
|
|
|
|
lastPointerPos.current.x, |
|
|
|
|
|
lastPointerPos.current.y |
|
|
|
|
|
|
|
|
const isInnerMode = Boolean( |
|
|
|
|
|
e.ctrlKey || e.metaKey || e.key === "Control" || e.key === "Meta" |
|
|
); |
|
|
); |
|
|
updateHoverTarget(el, mode, null); |
|
|
|
|
|
}; |
|
|
|
|
|
|
|
|
|
|
|
const handleKeyUp = (e: KeyboardEvent) => { |
|
|
|
|
|
if (e.key === "Alt" || !e.altKey) { |
|
|
|
|
|
currentElementRef.current = null; |
|
|
|
|
|
setHoveredTarget(null); |
|
|
|
|
|
setCustomDepthIndex(null); |
|
|
|
|
|
} else if (e.altKey && lastPointerPos.current) { |
|
|
|
|
|
// Modifier key released while keeping Alt pressed -> update target accordingly
|
|
|
|
|
|
const mode = determineBaseMode(e); |
|
|
|
|
|
|
|
|
if (isAlt && lastPointerPos.current) { |
|
|
const el = document.elementFromPoint( |
|
|
const el = document.elementFromPoint( |
|
|
lastPointerPos.current.x, |
|
|
lastPointerPos.current.x, |
|
|
lastPointerPos.current.y |
|
|
lastPointerPos.current.y |
|
|
); |
|
|
); |
|
|
updateHoverTarget(el, mode, null); |
|
|
|
|
|
|
|
|
updateHoverTarget(el, isInnerMode); |
|
|
} |
|
|
} |
|
|
}; |
|
|
}; |
|
|
|
|
|
|
|
|
const handleWheel = (e: WheelEvent) => { |
|
|
|
|
|
if (!e.altKey || !currentHierarchyRef.current.length) return; |
|
|
|
|
|
|
|
|
|
|
|
e.preventDefault(); |
|
|
|
|
|
e.stopPropagation(); |
|
|
|
|
|
|
|
|
|
|
|
const total = currentHierarchyRef.current.length; |
|
|
|
|
|
if (total <= 1) return; |
|
|
|
|
|
|
|
|
|
|
|
// Scroll Down (deltaY > 0) -> Step Deeper / Inner (towards index 0)
|
|
|
|
|
|
// Scroll Up (deltaY < 0) -> Step Outer (towards index total - 1)
|
|
|
|
|
|
const current = |
|
|
|
|
|
currentDepthIndexRef.current ?? |
|
|
|
|
|
(hoveredTarget?.levelIndex ?? (e.ctrlKey || e.metaKey ? 0 : total - 1)); |
|
|
|
|
|
|
|
|
|
|
|
const delta = e.deltaY > 0 ? -1 : 1; |
|
|
|
|
|
const next = Math.max(0, Math.min(total - 1, current + delta)); |
|
|
|
|
|
|
|
|
|
|
|
setCustomDepthIndex(next); |
|
|
|
|
|
|
|
|
|
|
|
if (lastPointerPos.current) { |
|
|
|
|
|
const el = document.elementFromPoint( |
|
|
|
|
|
lastPointerPos.current.x, |
|
|
|
|
|
lastPointerPos.current.y |
|
|
|
|
|
); |
|
|
|
|
|
updateHoverTarget(el, "LEVEL" as any, next); |
|
|
|
|
|
|
|
|
const handleKeyUp = (e: KeyboardEvent) => { |
|
|
|
|
|
if (e.key === "Alt" || !e.altKey) { |
|
|
|
|
|
currentElementRef.current = null; |
|
|
|
|
|
setHoveredTarget(null); |
|
|
|
|
|
} else if (e.altKey && (e.key === "Control" || e.key === "Meta")) { |
|
|
|
|
|
// Released Ctrl while keeping Alt pressed -> switch back to Outer Mode
|
|
|
|
|
|
if (lastPointerPos.current) { |
|
|
|
|
|
const el = document.elementFromPoint( |
|
|
|
|
|
lastPointerPos.current.x, |
|
|
|
|
|
lastPointerPos.current.y |
|
|
|
|
|
); |
|
|
|
|
|
updateHoverTarget(el, false); |
|
|
|
|
|
} |
|
|
} |
|
|
} |
|
|
}; |
|
|
}; |
|
|
|
|
|
|
|
|
const handleWindowBlur = () => { |
|
|
const handleWindowBlur = () => { |
|
|
currentElementRef.current = null; |
|
|
currentElementRef.current = null; |
|
|
setHoveredTarget(null); |
|
|
setHoveredTarget(null); |
|
|
setCustomDepthIndex(null); |
|
|
|
|
|
}; |
|
|
}; |
|
|
|
|
|
|
|
|
const handleScrollOrResize = () => { |
|
|
const handleScrollOrResize = () => { |
|
|
@ -415,15 +295,10 @@ export function DevClickToComponent() { |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
const target = event.target instanceof Element ? event.target : null; |
|
|
const target = event.target instanceof Element ? event.target : null; |
|
|
|
|
|
const isInnerMode = Boolean(event.ctrlKey || event.metaKey); |
|
|
const hierarchy = getHierarchy(target); |
|
|
const hierarchy = getHierarchy(target); |
|
|
const mode = determineBaseMode(event); |
|
|
|
|
|
const selected = selectTargetFromHierarchy( |
|
|
|
|
|
hierarchy, |
|
|
|
|
|
mode, |
|
|
|
|
|
currentDepthIndexRef.current |
|
|
|
|
|
); |
|
|
|
|
|
|
|
|
|
|
|
const locator = selected?.element?.getAttribute("data-locator"); |
|
|
|
|
|
|
|
|
const targetEl = selectTargetElement(hierarchy, isInnerMode); |
|
|
|
|
|
const locator = targetEl?.getAttribute("data-locator"); |
|
|
|
|
|
|
|
|
if (!locator) { |
|
|
if (!locator) { |
|
|
return; |
|
|
return; |
|
|
@ -438,7 +313,6 @@ export function DevClickToComponent() { |
|
|
window.addEventListener("pointermove", handlePointerMove, true); |
|
|
window.addEventListener("pointermove", handlePointerMove, true); |
|
|
window.addEventListener("keydown", handleKeyDown, true); |
|
|
window.addEventListener("keydown", handleKeyDown, true); |
|
|
window.addEventListener("keyup", handleKeyUp, true); |
|
|
window.addEventListener("keyup", handleKeyUp, true); |
|
|
window.addEventListener("wheel", handleWheel, { capture: true, passive: false }); |
|
|
|
|
|
window.addEventListener("blur", handleWindowBlur); |
|
|
window.addEventListener("blur", handleWindowBlur); |
|
|
document.addEventListener("mouseleave", handleWindowBlur); |
|
|
document.addEventListener("mouseleave", handleWindowBlur); |
|
|
window.addEventListener("scroll", handleScrollOrResize, true); |
|
|
window.addEventListener("scroll", handleScrollOrResize, true); |
|
|
@ -449,14 +323,13 @@ export function DevClickToComponent() { |
|
|
window.removeEventListener("pointermove", handlePointerMove, true); |
|
|
window.removeEventListener("pointermove", handlePointerMove, true); |
|
|
window.removeEventListener("keydown", handleKeyDown, true); |
|
|
window.removeEventListener("keydown", handleKeyDown, true); |
|
|
window.removeEventListener("keyup", handleKeyUp, true); |
|
|
window.removeEventListener("keyup", handleKeyUp, true); |
|
|
window.removeEventListener("wheel", handleWheel, true); |
|
|
|
|
|
window.removeEventListener("blur", handleWindowBlur); |
|
|
window.removeEventListener("blur", handleWindowBlur); |
|
|
document.removeEventListener("mouseleave", handleWindowBlur); |
|
|
document.removeEventListener("mouseleave", handleWindowBlur); |
|
|
window.removeEventListener("scroll", handleScrollOrResize, true); |
|
|
window.removeEventListener("scroll", handleScrollOrResize, true); |
|
|
window.removeEventListener("resize", handleScrollOrResize, true); |
|
|
window.removeEventListener("resize", handleScrollOrResize, true); |
|
|
document.removeEventListener("click", handleDesktopClick, true); |
|
|
document.removeEventListener("click", handleDesktopClick, true); |
|
|
}; |
|
|
}; |
|
|
}, [openInIde, hoveredTarget?.levelIndex]); |
|
|
|
|
|
|
|
|
}, [openInIde]); |
|
|
|
|
|
|
|
|
// Mobile: Inspect Mode click interception
|
|
|
// Mobile: Inspect Mode click interception
|
|
|
useEffect(() => { |
|
|
useEffect(() => { |
|
|
@ -475,18 +348,17 @@ export function DevClickToComponent() { |
|
|
event.stopPropagation(); |
|
|
event.stopPropagation(); |
|
|
|
|
|
|
|
|
const hierarchy = getHierarchy(target); |
|
|
const hierarchy = getHierarchy(target); |
|
|
// On mobile tap, choose innermost leaf by default or first available element
|
|
|
|
|
|
const selectedEl = hierarchy[0] || target.closest<HTMLElement>("[data-locator]"); |
|
|
|
|
|
const locator = selectedEl?.getAttribute("data-locator"); |
|
|
|
|
|
|
|
|
const element = hierarchy[0] || target.closest<HTMLElement>("[data-locator]"); |
|
|
|
|
|
const locator = element?.getAttribute("data-locator"); |
|
|
|
|
|
|
|
|
if (locator && selectedEl) { |
|
|
|
|
|
console.log(`[DevClickToComponent] Mobile inspect matched element. Opening in IDE:`, locator); |
|
|
|
|
|
|
|
|
if (locator && element) { |
|
|
|
|
|
console.log(`[DevClickToComponent] Inspect Mode matched element. Opening in IDE:`, locator); |
|
|
|
|
|
|
|
|
if (selectedEl instanceof HTMLElement) { |
|
|
|
|
|
const originalOutline = selectedEl.style.outline; |
|
|
|
|
|
selectedEl.style.outline = "3px solid #38bdf8"; |
|
|
|
|
|
|
|
|
if (element instanceof HTMLElement) { |
|
|
|
|
|
const originalOutline = element.style.outline; |
|
|
|
|
|
element.style.outline = "3px solid #2dd9a4"; |
|
|
setTimeout(() => { |
|
|
setTimeout(() => { |
|
|
selectedEl.style.outline = originalOutline; |
|
|
|
|
|
|
|
|
element.style.outline = originalOutline; |
|
|
}, 400); |
|
|
}, 400); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
@ -545,29 +417,14 @@ export function DevClickToComponent() { |
|
|
} |
|
|
} |
|
|
}; |
|
|
}; |
|
|
|
|
|
|
|
|
// Theme colors for different modes
|
|
|
|
|
|
// INNER: Cyan (#38bdf8), PARENT / LEVEL: Amber (#fbbf24) / Purple (#a78bfa), OUTER: Emerald (#2dd9a4)
|
|
|
|
|
|
let accentColor = "#2dd9a4"; |
|
|
|
|
|
let accentBg = "rgba(45, 217, 164, 0.08)"; |
|
|
|
|
|
let accentShadow = "0 0 14px rgba(45, 217, 164, 0.45), inset 0 0 6px rgba(45, 217, 164, 0.15)"; |
|
|
|
|
|
let modeBadgeLabel = "OUTER"; |
|
|
|
|
|
|
|
|
|
|
|
if (hoveredTarget?.mode === "INNER") { |
|
|
|
|
|
accentColor = "#38bdf8"; |
|
|
|
|
|
accentBg = "rgba(56, 189, 248, 0.08)"; |
|
|
|
|
|
accentShadow = "0 0 14px rgba(56, 189, 248, 0.5), inset 0 0 6px rgba(56, 189, 248, 0.2)"; |
|
|
|
|
|
modeBadgeLabel = "INNER LEAF"; |
|
|
|
|
|
} else if (hoveredTarget?.mode === "PARENT") { |
|
|
|
|
|
accentColor = "#a78bfa"; |
|
|
|
|
|
accentBg = "rgba(167, 139, 250, 0.08)"; |
|
|
|
|
|
accentShadow = "0 0 14px rgba(167, 139, 250, 0.45), inset 0 0 6px rgba(167, 139, 250, 0.15)"; |
|
|
|
|
|
modeBadgeLabel = "PARENT"; |
|
|
|
|
|
} else if (hoveredTarget?.mode === "LEVEL") { |
|
|
|
|
|
accentColor = "#fbbf24"; |
|
|
|
|
|
accentBg = "rgba(251, 191, 36, 0.08)"; |
|
|
|
|
|
accentShadow = "0 0 14px rgba(251, 191, 36, 0.45), inset 0 0 6px rgba(251, 191, 36, 0.15)"; |
|
|
|
|
|
modeBadgeLabel = `DEPTH ${hoveredTarget.levelIndex + 1}/${hoveredTarget.totalLevels}`; |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
// Color scheme: Emerald (#2dd9a4) for Outer, Cyan (#38bdf8) for Inner
|
|
|
|
|
|
const accentColor = hoveredTarget?.isInner ? "#38bdf8" : "#2dd9a4"; |
|
|
|
|
|
const accentBg = hoveredTarget?.isInner |
|
|
|
|
|
? "rgba(56, 189, 248, 0.08)" |
|
|
|
|
|
: "rgba(45, 217, 164, 0.08)"; |
|
|
|
|
|
const accentShadow = hoveredTarget?.isInner |
|
|
|
|
|
? "0 0 14px rgba(56, 189, 248, 0.5), inset 0 0 6px rgba(56, 189, 248, 0.2)" |
|
|
|
|
|
: "0 0 14px rgba(45, 217, 164, 0.45), inset 0 0 6px rgba(45, 217, 164, 0.15)"; |
|
|
|
|
|
|
|
|
if (process.env.NODE_ENV !== "development") { |
|
|
if (process.env.NODE_ENV !== "development") { |
|
|
return null; |
|
|
return null; |
|
|
@ -575,7 +432,7 @@ export function DevClickToComponent() { |
|
|
|
|
|
|
|
|
return ( |
|
|
return ( |
|
|
<> |
|
|
<> |
|
|
{/* Component Bounding Box Highlight Overlay */} |
|
|
|
|
|
|
|
|
{/* Alt (Outer) / Alt + Ctrl (Inner) Highlight Overlay Box */} |
|
|
{hoveredTarget && ( |
|
|
{hoveredTarget && ( |
|
|
<div |
|
|
<div |
|
|
style={{ |
|
|
style={{ |
|
|
@ -593,107 +450,70 @@ export function DevClickToComponent() { |
|
|
pointerEvents: "none", |
|
|
pointerEvents: "none", |
|
|
zIndex: 999999, |
|
|
zIndex: 999999, |
|
|
transition: |
|
|
transition: |
|
|
"top 0.05s ease-out, left 0.05s ease-out, width 0.05s ease-out, height 0.05s ease-out, border 0.12s, background-color 0.12s, box-shadow 0.12s", |
|
|
|
|
|
|
|
|
"top 0.06s ease-out, left 0.06s ease-out, width 0.06s ease-out, height 0.06s ease-out, border 0.15s, background-color 0.15s, box-shadow 0.15s", |
|
|
}} |
|
|
}} |
|
|
> |
|
|
> |
|
|
{/* Smart Component Info Tag */} |
|
|
|
|
|
|
|
|
{/* Component Info Tag */} |
|
|
<div |
|
|
<div |
|
|
style={{ |
|
|
style={{ |
|
|
position: "absolute", |
|
|
position: "absolute", |
|
|
top: hoveredTarget.rect.top < 34 ? "6px" : "-32px", |
|
|
|
|
|
|
|
|
top: hoveredTarget.rect.top < 30 ? "6px" : "-28px", |
|
|
left: Math.max(0, -hoveredTarget.rect.left), |
|
|
left: Math.max(0, -hoveredTarget.rect.left), |
|
|
display: "inline-flex", |
|
|
display: "inline-flex", |
|
|
flexDirection: "column", |
|
|
|
|
|
gap: "3px", |
|
|
|
|
|
|
|
|
alignItems: "center", |
|
|
|
|
|
gap: "6px", |
|
|
backgroundColor: "#020d09", |
|
|
backgroundColor: "#020d09", |
|
|
color: accentColor, |
|
|
color: accentColor, |
|
|
fontSize: "11px", |
|
|
fontSize: "11px", |
|
|
fontWeight: 600, |
|
|
fontWeight: 600, |
|
|
fontFamily: |
|
|
fontFamily: |
|
|
"ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace", |
|
|
"ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace", |
|
|
padding: "4px 8px", |
|
|
|
|
|
borderRadius: "6px", |
|
|
|
|
|
|
|
|
padding: "3px 8px", |
|
|
|
|
|
borderRadius: "5px", |
|
|
whiteSpace: "nowrap", |
|
|
whiteSpace: "nowrap", |
|
|
boxShadow: "0 6px 18px rgba(0,0,0,0.75)", |
|
|
|
|
|
border: `1px solid ${accentColor}88`, |
|
|
|
|
|
|
|
|
boxShadow: "0 4px 14px rgba(0,0,0,0.6)", |
|
|
|
|
|
border: `1px solid ${accentColor}66`, |
|
|
pointerEvents: "none", |
|
|
pointerEvents: "none", |
|
|
}} |
|
|
}} |
|
|
> |
|
|
> |
|
|
{/* Top row: Mode + Tag Name + File Location */} |
|
|
|
|
|
<div style={{ display: "flex", alignItems: "center", gap: "6px" }}> |
|
|
|
|
|
<span |
|
|
|
|
|
style={{ |
|
|
|
|
|
backgroundColor: `${accentColor}33`, |
|
|
|
|
|
color: accentColor, |
|
|
|
|
|
padding: "1px 6px", |
|
|
|
|
|
borderRadius: "3px", |
|
|
|
|
|
fontSize: "10px", |
|
|
|
|
|
fontWeight: 700, |
|
|
|
|
|
letterSpacing: "0.04em", |
|
|
|
|
|
}} |
|
|
|
|
|
> |
|
|
|
|
|
{modeBadgeLabel} |
|
|
|
|
|
</span> |
|
|
|
|
|
|
|
|
|
|
|
<span style={{ color: "#ffffff", opacity: 0.85, fontWeight: 700 }}> |
|
|
|
|
|
<{hoveredTarget.tagName}> |
|
|
|
|
|
</span> |
|
|
|
|
|
<span style={{ color: accentColor }}> |
|
|
|
|
|
{formatLocatorLabel(hoveredTarget.locator)} |
|
|
|
|
|
</span> |
|
|
|
|
|
|
|
|
|
|
|
{isOpening && ( |
|
|
|
|
|
<span style={{ color: "#ffffff", fontWeight: 700 }}> |
|
|
|
|
|
⚡ Opening IDE... |
|
|
|
|
|
</span> |
|
|
|
|
|
)} |
|
|
|
|
|
</div> |
|
|
|
|
|
|
|
|
|
|
|
{/* Bottom row: Breadcrumb Hierarchy Trail & Interactive Shortcut Hints */} |
|
|
|
|
|
{!isOpening && hoveredTarget.totalLevels > 1 && ( |
|
|
|
|
|
<div |
|
|
|
|
|
style={{ |
|
|
|
|
|
display: "flex", |
|
|
|
|
|
alignItems: "center", |
|
|
|
|
|
gap: "4px", |
|
|
|
|
|
fontSize: "9.5px", |
|
|
|
|
|
color: "#94a3b8", |
|
|
|
|
|
paddingTop: "2px", |
|
|
|
|
|
borderTop: "1px solid rgba(255,255,255,0.08)", |
|
|
|
|
|
}} |
|
|
|
|
|
> |
|
|
|
|
|
{/* Visual breadcrumbs */} |
|
|
|
|
|
<span style={{ opacity: 0.7 }}>Hierarchy:</span> |
|
|
|
|
|
{hoveredTarget.hierarchyTags.map((tag, idx) => ( |
|
|
|
|
|
<span |
|
|
|
|
|
key={idx} |
|
|
|
|
|
style={{ |
|
|
|
|
|
color: |
|
|
|
|
|
idx === hoveredTarget.levelIndex |
|
|
|
|
|
? accentColor |
|
|
|
|
|
: "rgba(255,255,255,0.45)", |
|
|
|
|
|
fontWeight: idx === hoveredTarget.levelIndex ? 700 : 400, |
|
|
|
|
|
textDecoration: |
|
|
|
|
|
idx === hoveredTarget.levelIndex ? "underline" : "none", |
|
|
|
|
|
}} |
|
|
|
|
|
> |
|
|
|
|
|
{tag} |
|
|
|
|
|
{idx < hoveredTarget.hierarchyTags.length - 1 && " › "} |
|
|
|
|
|
</span> |
|
|
|
|
|
))} |
|
|
|
|
|
|
|
|
|
|
|
{/* Shortcut Guide */} |
|
|
|
|
|
<span |
|
|
|
|
|
style={{ |
|
|
|
|
|
marginLeft: "8px", |
|
|
|
|
|
color: "rgba(255,255,255,0.5)", |
|
|
|
|
|
fontSize: "9px", |
|
|
|
|
|
}} |
|
|
|
|
|
> |
|
|
|
|
|
(Scroll / ↑↓ / [ ]: Level | Ctrl: Inner | Shift: Parent) |
|
|
|
|
|
</span> |
|
|
|
|
|
</div> |
|
|
|
|
|
)} |
|
|
|
|
|
|
|
|
{/* Outer / Inner Mode Pill */} |
|
|
|
|
|
<span |
|
|
|
|
|
style={{ |
|
|
|
|
|
backgroundColor: hoveredTarget.isInner |
|
|
|
|
|
? "rgba(56, 189, 248, 0.25)" |
|
|
|
|
|
: "rgba(45, 217, 164, 0.2)", |
|
|
|
|
|
color: accentColor, |
|
|
|
|
|
padding: "1px 5px", |
|
|
|
|
|
borderRadius: "3px", |
|
|
|
|
|
fontSize: "10px", |
|
|
|
|
|
fontWeight: 700, |
|
|
|
|
|
letterSpacing: "0.03em", |
|
|
|
|
|
}} |
|
|
|
|
|
> |
|
|
|
|
|
{hoveredTarget.isInner ? "INNER" : "OUTER"} |
|
|
|
|
|
</span> |
|
|
|
|
|
|
|
|
|
|
|
<span style={{ color: "#ffffff", opacity: 0.65 }}> |
|
|
|
|
|
<{hoveredTarget.tagName}> |
|
|
|
|
|
</span> |
|
|
|
|
|
<span>{formatLocatorLabel(hoveredTarget.locator)}</span> |
|
|
|
|
|
|
|
|
|
|
|
<span |
|
|
|
|
|
style={{ |
|
|
|
|
|
color: "#ffffff", |
|
|
|
|
|
opacity: 0.5, |
|
|
|
|
|
fontSize: "10px", |
|
|
|
|
|
fontWeight: 400, |
|
|
|
|
|
}} |
|
|
|
|
|
> |
|
|
|
|
|
{isOpening |
|
|
|
|
|
? "⚡ Opening IDE..." |
|
|
|
|
|
: hoveredTarget.hasMultipleLevels |
|
|
|
|
|
? hoveredTarget.isInner |
|
|
|
|
|
? "⌥+Click | Release Ctrl for Outer" |
|
|
|
|
|
: "⌥+Click | Hold Ctrl for Inner" |
|
|
|
|
|
: "⌥+Click to open"} |
|
|
|
|
|
</span> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
)} |
|
|
)} |
|
|
@ -724,13 +544,13 @@ export function DevClickToComponent() { |
|
|
width: "50px", |
|
|
width: "50px", |
|
|
height: "50px", |
|
|
height: "50px", |
|
|
borderRadius: "25px", |
|
|
borderRadius: "25px", |
|
|
backgroundColor: isInspecting ? "#38bdf8" : "#020d09", |
|
|
|
|
|
|
|
|
backgroundColor: isInspecting ? "#2dd9a4" : "#020d09", |
|
|
color: isInspecting ? "#020d09" : "#ffffff", |
|
|
color: isInspecting ? "#020d09" : "#ffffff", |
|
|
border: isInspecting |
|
|
border: isInspecting |
|
|
? "2px solid #38bdf8" |
|
|
|
|
|
|
|
|
? "2px solid #2dd9a4" |
|
|
: "1px solid rgba(255,255,255,0.15)", |
|
|
: "1px solid rgba(255,255,255,0.15)", |
|
|
boxShadow: isInspecting |
|
|
boxShadow: isInspecting |
|
|
? "0 0 16px rgba(56, 189, 248, 0.7)" |
|
|
|
|
|
|
|
|
? "0 0 16px rgba(45, 217, 164, 0.7)" |
|
|
: "0 4px 14px rgba(0,0,0,0.4)", |
|
|
: "0 4px 14px rgba(0,0,0,0.4)", |
|
|
display: "flex", |
|
|
display: "flex", |
|
|
alignItems: "center", |
|
|
alignItems: "center", |
|
|
|