Something went wrong. Try again.
[READ-ONLY]
a fast, modern browser for the npm registry forked from npmx.dev/npmx.dev
Something went wrong. Try again.
5.0 kB · 182 lines
Vue
at main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183<script setup lang="ts">const props = defineProps<{ html: string lines: number selectedLines: { start: number; end: number } | null}>()
const emit = defineEmits<{ lineClick: [lineNum: number, event: MouseEvent]}>()
const codeRef = useTemplateRef('codeRef')
// Generate line numbers arrayconst lineNumbers = computed(() => { return Array.from({ length: props.lines }, (_, i) => i + 1)})
// Used for CSS calculation of line number column widthconst lineDigits = computed(() => { return String(props.lines).length})
// Check if a line is selectedfunction isLineSelected(lineNum: number): boolean { if (!props.selectedLines) return false return lineNum >= props.selectedLines.start && lineNum <= props.selectedLines.end}
// Handle line number clickfunction onLineClick(lineNum: number, event: MouseEvent) { emit('lineClick', lineNum, event)}
// Apply highlighting to code lines when selection changesfunction updateLineHighlighting() { if (!codeRef.value) return
// Lines are inside pre > code > .line const lines = codeRef.value.querySelectorAll('code > .line') lines.forEach((line, index) => { const lineNum = index + 1 if (isLineSelected(lineNum)) { line.classList.add('highlighted') } else { line.classList.remove('highlighted') } })}
// Watch for changes to selection and HTML content// Use deep watch and nextTick to ensure DOM is updatedwatch( () => [props.selectedLines, props.html] as const, () => { nextTick(updateLineHighlighting) }, { immediate: true },)
// Use Nuxt's `navigateTo` for the rendered import linksfunction handleImportLinkNavigate() { if (!codeRef.value) return
const anchors = codeRef.value.querySelectorAll<HTMLAnchorElement>('a.import-link') anchors.forEach(anchor => { // NOTE: We do not need to remove previous listeners because we re-create the entire HTML content on each html update anchor.addEventListener('click', event => { if (event.metaKey || event.altKey || event.ctrlKey || event.shiftKey) return const href = anchor.getAttribute('href') if (href) { event.preventDefault() navigateTo(href) } }) })}
watch( () => props.html, () => { nextTick(handleImportLinkNavigate) }, { immediate: true },)</script>
<template> <div class="code-viewer flex min-h-full max-w-full"> <!-- Line numbers column --> <div class="line-numbers shrink-0 bg-bg-subtle border-ie border-solid border-border text-end select-none relative" :style="{ '--line-digits': lineDigits }" aria-hidden="true" > <!-- This needs to be a native <a> element, because `LinkBase` (or specifically `NuxtLink`) does not seem to work when trying to prevent default behavior (jumping to the anchor) --> <a v-for="lineNum in lineNumbers" :id="`L${lineNum}`" :key="lineNum" :href="`#L${lineNum}`" tabindex="-1" class="line-number block px-3 py-0 font-mono text-sm leading-6 cursor-pointer transition-colors no-underline" :class="[ isLineSelected(lineNum) ? 'bg-yellow-500/20 text-fg' : 'text-fg-subtle hover:text-fg-muted', ]" @click.prevent="onLineClick(lineNum, $event)" > {{ lineNum }} </a> </div>
<!-- Code content --> <div class="code-content flex-1 overflow-x-auto min-w-0"> <!-- eslint-disable vue/no-v-html -- HTML is generated server-side by Shiki --> <div ref="codeRef" class="code-lines w-fit" v-html="html" /> <!-- eslint-enable vue/no-v-html --> </div> </div></template>
<style scoped>.code-viewer { font-size: 14px;}
.line-numbers { /* 1ch per digit + 1.5rem (px-3 * 2) padding */ min-width: calc(var(--line-digits) * 1ch + 1.5rem);}
.code-content :deep(pre) { margin: 0; padding: 0; background: transparent !important; overflow: visible;}
.code-content :deep(code) { display: block; padding: 0 1rem; background: transparent !important;}
.code-content :deep(.line) { display: block; /* Ensure consistent height matching line numbers */ line-height: 24px; min-height: 24px; max-height: 24px; white-space: pre; overflow: hidden; transition: background-color 0.1s;}
/* Highlighted lines in code content - extend full width with negative margin */.code-content :deep(.line.highlighted) { background: rgb(234 179 8 / 0.2); /* yellow-500/20 */ margin: 0 -1rem; padding: 0 1rem;}
/* Clickable import links */.code-content :deep(.import-link) { color: inherit; text-decoration: underline; text-decoration-style: dotted; text-decoration-color: rgba(158, 203, 255, 0.5); /* syntax.str with transparency */ text-underline-offset: 2px; transition: text-decoration-color 0.15s, text-decoration-style 0.15s; cursor: pointer;}
.code-content :deep(.import-link:hover) { text-decoration-style: solid; text-decoration-color: #9ecbff; /* syntax.str - light blue */}</style>