Files
portal/app/resources/js/components/DevIndexOverlay.vue
T
2026-05-12 12:02:38 +03:00

126 lines
3.3 KiB
Vue

<template>
<Teleport to="body">
<div
v-if="currentId !== null && currentTarget"
class="dx-badge"
:class="{ 'dx-badge--copied': justCopied }"
:style="badgePosition"
@click.stop="copyToClipboard"
>
<span class="dx-badge__num">#{{ currentId }}</span>
<span class="dx-badge__meta">{{ tagLabel }} · "{{ textPreview }}"</span>
</div>
</Teleport>
</template>
<script setup lang="ts">
import { computed, onMounted, onBeforeUnmount, ref } from 'vue';
import { useDevIndices } from '../composables/useDevIndices';
const { currentId, currentTarget, hoverEnabled, setTarget, reset } = useDevIndices();
const cursorX = ref(0);
const cursorY = ref(0);
const justCopied = ref(false);
let mousemoveRAF: number | null = null;
const tagLabel = computed(() => {
const t = currentTarget.value;
if (!t) return '';
return t.tagName.toLowerCase();
});
const textPreview = computed(() => {
const t = currentTarget.value;
if (!t) return '';
const text = (t.textContent ?? '').trim().slice(0, 24);
return text || '—';
});
const badgePosition = computed(() => ({
left: `${cursorX.value + 12}px`,
top: `${cursorY.value + 12}px`,
}));
function onMousemove(e: MouseEvent) {
if (!hoverEnabled.value) return;
cursorX.value = e.clientX;
cursorY.value = e.clientY;
if (mousemoveRAF !== null) return;
mousemoveRAF = requestAnimationFrame(() => {
mousemoveRAF = null;
const el = document.elementFromPoint(e.clientX, e.clientY) as HTMLElement | null;
if (!el) {
setTarget(null);
return;
}
const withDx = el.closest('[data-dx]') as HTMLElement | null;
setTarget(withDx);
});
}
function onKeydown(e: KeyboardEvent) {
if (e.key === 'Escape') {
reset();
}
}
async function copyToClipboard() {
if (currentId.value === null) return;
try {
await navigator.clipboard.writeText(`#${currentId.value}`);
justCopied.value = true;
setTimeout(() => (justCopied.value = false), 400);
} catch {
// clipboard may be unavailable in some contexts; silent fail OK in dev tool
}
}
onMounted(() => {
document.addEventListener('mousemove', onMousemove);
document.addEventListener('keydown', onKeydown);
});
onBeforeUnmount(() => {
document.removeEventListener('mousemove', onMousemove);
document.removeEventListener('keydown', onKeydown);
if (mousemoveRAF !== null) cancelAnimationFrame(mousemoveRAF);
});
</script>
<style scoped>
.dx-badge {
position: fixed;
z-index: 999999;
pointer-events: auto;
cursor: pointer;
display: inline-flex;
align-items: center;
gap: 8px;
padding: 4px 10px;
background: #0f6e56;
color: #fff;
font-family: 'JetBrains Mono', ui-monospace, monospace;
font-size: 11px;
line-height: 1.4;
border-radius: 4px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.24);
user-select: none;
transition: background 120ms ease;
}
.dx-badge--copied {
background: #21a16e;
}
.dx-badge__num {
background: rgba(255, 255, 255, 0.22);
padding: 1px 6px;
border-radius: 3px;
font-weight: 600;
}
.dx-badge__meta {
letter-spacing: 0.02em;
opacity: 0.92;
}
</style>