<script setup lang="ts">
import { onMounted, onUnmounted, ref } from "vue";
import type { ElementContext } from "@ltt/shared";
import { buildElementContext } from "../../lib/selector";
const emit = defineEmits<{
(e: "pick", element: ElementContext): void;
(e: "cancel"): void;
}>();
const highlight = ref<{ x: number; y: number; w: number; h: number } | null>(null);
const label = ref("");
const layerRef = ref<HTMLDivElement>();
function elementAtPoint(x: number, y: number): Element | null {
const layer = layerRef.value;
if (!layer) return null;
// momentarily drop the layer from hit-testing so elementFromPoint
// resolves to the page element underneath
layer.style.pointerEvents = "none";
const el = document.elementFromPoint(x, y);
layer.style.pointerEvents = "";
return el;
}
function describe(el: Element): string {
const tag = el.tagName.toLowerCase();
const id = el.id ? `#${el.id}` : "";
const classes = Array.from(el.classList).slice(0, 2).map((c) => `.${c}`).join("");
return `${tag}${id}${classes}`.slice(0, 80);
}
function onMouseMove(event: MouseEvent) {
const el = elementAtPoint(event.clientX, event.clientY);
if (!el || el === document.documentElement || el === document.body) {
highlight.value = null;
label.value = "";
return;
}
const rect = el.getBoundingClientRect();
highlight.value = { x: rect.x, y: rect.y, w: rect.width, h: rect.height };
label.value = describe(el);
}
function onClick(event: MouseEvent) {
event.preventDefault();
event.stopPropagation();
const el = elementAtPoint(event.clientX, event.clientY);
if (!el || el === document.documentElement || el === document.body) return;
emit("pick", buildElementContext(el));
}
function onKeyDown(event: KeyboardEvent) {
if (event.key === "Escape") {
event.preventDefault();
event.stopPropagation();
emit("cancel");
}
}
onMounted(() => {
window.addEventListener("mousemove", onMouseMove, true);
window.addEventListener("keydown", onKeyDown, true);
});
onUnmounted(() => {
window.removeEventListener("mousemove", onMouseMove, true);
window.removeEventListener("keydown", onKeyDown, true);
});
</script>
<template>
<div ref="layerRef" class="picker-layer" @click.capture="onClick">
<div
v-if="highlight"
class="picker-highlight"
:style="{
left: `${highlight.x}px`,
top: `${highlight.y}px`,
width: `${highlight.w}px`,
height: `${highlight.h}px`,
}"
/>
<div
v-if="label"
class="picker-tooltip"
:style="highlight ? { left: `${highlight.x}px`, top: `${Math.max(highlight.y - 26, 4)}px` } : {}"
>
{{ label }}
</div>
</div>
</template>
<style scoped>
.picker-layer {
position: absolute;
inset: 0;
cursor: crosshair;
}
.picker-highlight {
position: fixed;
box-sizing: border-box;
border: 2px solid var(--color-accent, #7aa2f7);
background: rgba(122, 162, 247, 0.12);
pointer-events: none;
}
.picker-tooltip {
position: fixed;
transform: translateY(-100%);
padding: 2px 8px;
background: var(--color-surface, #1a1c2b);
border: 1px solid var(--color-accent, #7aa2f7);
color: var(--color-text, #c0caf5);
font-size: 11px;
white-space: nowrap;
pointer-events: none;
}
</style>