cb05657f30
Phase 1B audit found 48 files failing `prettier --check`. Auto-apply
via `npx prettier --write resources/js/**/*.{ts,vue,css}` produced
style-only changes:
- consistent quote style
- trailing comma normalization
- spaces around : in v-card style="position: relative" attrs
- explicit ; insertion
No semantic changes. No code-behavior changes. Production-code only;
test files batched separately into `test(frontend):` commit.
Verification:
- npx vitest run → 79/79 files, 614/614 + 3 skipped (no regression).
- npx vue-tsc --noEmit → 0 errors.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
222 lines
5.7 KiB
Vue
222 lines
5.7 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>
|
|
<Teleport to="body">
|
|
<div v-if="overlayMode" class="dx-mini-layer">
|
|
<div v-for="el in overlayElements" :key="el.id" class="dx-mini" :style="miniStyleFor(el.rect)">
|
|
#{{ el.id }}
|
|
</div>
|
|
</div>
|
|
</Teleport>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import { computed, onMounted, onBeforeUnmount, ref, watch } from 'vue';
|
|
import { useDevIndices } from '../composables/useDevIndices';
|
|
|
|
const {
|
|
currentId,
|
|
currentTarget,
|
|
hoverEnabled,
|
|
overlayMode,
|
|
setTarget,
|
|
reset,
|
|
pauseHover,
|
|
walkToParent,
|
|
walkToChild,
|
|
toggleOverlay,
|
|
} = 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`,
|
|
}));
|
|
|
|
interface OverlayItem {
|
|
id: number;
|
|
rect: DOMRect;
|
|
}
|
|
|
|
const overlayElements = ref<OverlayItem[]>([]);
|
|
|
|
function refreshOverlayElements() {
|
|
const nodes = Array.from(document.querySelectorAll<HTMLElement>('[data-dx]'));
|
|
overlayElements.value = nodes
|
|
.map((el) => {
|
|
const idAttr = el.getAttribute('data-dx');
|
|
const id = Number(idAttr);
|
|
if (!Number.isFinite(id)) return null;
|
|
return { id, rect: el.getBoundingClientRect() };
|
|
})
|
|
.filter((x): x is OverlayItem => x !== null);
|
|
}
|
|
|
|
function miniStyleFor(rect: DOMRect) {
|
|
return {
|
|
left: `${rect.left}px`,
|
|
top: `${rect.top}px`,
|
|
};
|
|
}
|
|
|
|
watch(overlayMode, (on) => {
|
|
if (on) {
|
|
refreshOverlayElements();
|
|
window.addEventListener('resize', refreshOverlayElements);
|
|
window.addEventListener('scroll', refreshOverlayElements, true);
|
|
} else {
|
|
overlayElements.value = [];
|
|
window.removeEventListener('resize', refreshOverlayElements);
|
|
window.removeEventListener('scroll', refreshOverlayElements, true);
|
|
}
|
|
});
|
|
|
|
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.altKey && e.shiftKey && (e.key === 'I' || e.key === 'i')) {
|
|
e.preventDefault();
|
|
toggleOverlay();
|
|
return;
|
|
}
|
|
if (e.altKey && e.key === 'ArrowUp') {
|
|
e.preventDefault();
|
|
walkToParent();
|
|
pauseHover(800);
|
|
return;
|
|
}
|
|
if (e.altKey && e.key === 'ArrowDown') {
|
|
e.preventDefault();
|
|
walkToChild();
|
|
pauseHover(800);
|
|
return;
|
|
}
|
|
if (e.key === 'Escape') {
|
|
reset();
|
|
pauseHover(2000);
|
|
}
|
|
}
|
|
|
|
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);
|
|
if (overlayMode.value) {
|
|
window.removeEventListener('resize', refreshOverlayElements);
|
|
window.removeEventListener('scroll', refreshOverlayElements, true);
|
|
}
|
|
});
|
|
</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;
|
|
}
|
|
.dx-mini-layer {
|
|
position: fixed;
|
|
inset: 0;
|
|
pointer-events: none;
|
|
z-index: 999998;
|
|
}
|
|
.dx-mini {
|
|
position: fixed;
|
|
background: #0f6e56;
|
|
color: #fff;
|
|
font-family: 'JetBrains Mono', ui-monospace, monospace;
|
|
font-size: 9px;
|
|
line-height: 1;
|
|
padding: 1px 3px;
|
|
border-radius: 2px;
|
|
pointer-events: none;
|
|
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.4);
|
|
}
|
|
</style>
|