d8c33b4cd6
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
126 lines
3.3 KiB
Vue
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>
|