[READ-ONLY] a fast, modern browser for the npm registry
at main 182 lines 5.0 kB view raw
1<script setup lang="ts"> 2const props = defineProps<{ 3 html: string 4 lines: number 5 selectedLines: { start: number; end: number } | null 6}>() 7 8const emit = defineEmits<{ 9 lineClick: [lineNum: number, event: MouseEvent] 10}>() 11 12const codeRef = useTemplateRef('codeRef') 13 14// Generate line numbers array 15const lineNumbers = computed(() => { 16 return Array.from({ length: props.lines }, (_, i) => i + 1) 17}) 18 19// Used for CSS calculation of line number column width 20const lineDigits = computed(() => { 21 return String(props.lines).length 22}) 23 24// Check if a line is selected 25function isLineSelected(lineNum: number): boolean { 26 if (!props.selectedLines) return false 27 return lineNum >= props.selectedLines.start && lineNum <= props.selectedLines.end 28} 29 30// Handle line number click 31function onLineClick(lineNum: number, event: MouseEvent) { 32 emit('lineClick', lineNum, event) 33} 34 35// Apply highlighting to code lines when selection changes 36function updateLineHighlighting() { 37 if (!codeRef.value) return 38 39 // Lines are inside pre > code > .line 40 const lines = codeRef.value.querySelectorAll('code > .line') 41 lines.forEach((line, index) => { 42 const lineNum = index + 1 43 if (isLineSelected(lineNum)) { 44 line.classList.add('highlighted') 45 } else { 46 line.classList.remove('highlighted') 47 } 48 }) 49} 50 51// Watch for changes to selection and HTML content 52// Use deep watch and nextTick to ensure DOM is updated 53watch( 54 () => [props.selectedLines, props.html] as const, 55 () => { 56 nextTick(updateLineHighlighting) 57 }, 58 { immediate: true }, 59) 60 61// Use Nuxt's `navigateTo` for the rendered import links 62function handleImportLinkNavigate() { 63 if (!codeRef.value) return 64 65 const anchors = codeRef.value.querySelectorAll<HTMLAnchorElement>('a.import-link') 66 anchors.forEach(anchor => { 67 // NOTE: We do not need to remove previous listeners because we re-create the entire HTML content on each html update 68 anchor.addEventListener('click', event => { 69 if (event.metaKey || event.altKey || event.ctrlKey || event.shiftKey) return 70 const href = anchor.getAttribute('href') 71 if (href) { 72 event.preventDefault() 73 navigateTo(href) 74 } 75 }) 76 }) 77} 78 79watch( 80 () => props.html, 81 () => { 82 nextTick(handleImportLinkNavigate) 83 }, 84 { immediate: true }, 85) 86</script> 87 88<template> 89 <div class="code-viewer flex min-h-full max-w-full"> 90 <!-- Line numbers column --> 91 <div 92 class="line-numbers shrink-0 bg-bg-subtle border-ie border-solid border-border text-end select-none relative" 93 :style="{ '--line-digits': lineDigits }" 94 aria-hidden="true" 95 > 96 <!-- 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) --> 97 <a 98 v-for="lineNum in lineNumbers" 99 :id="`L${lineNum}`" 100 :key="lineNum" 101 :href="`#L${lineNum}`" 102 tabindex="-1" 103 class="line-number block px-3 py-0 font-mono text-sm leading-6 cursor-pointer transition-colors no-underline" 104 :class="[ 105 isLineSelected(lineNum) 106 ? 'bg-yellow-500/20 text-fg' 107 : 'text-fg-subtle hover:text-fg-muted', 108 ]" 109 @click.prevent="onLineClick(lineNum, $event)" 110 > 111 {{ lineNum }} 112 </a> 113 </div> 114 115 <!-- Code content --> 116 <div class="code-content flex-1 overflow-x-auto min-w-0"> 117 <!-- eslint-disable vue/no-v-html -- HTML is generated server-side by Shiki --> 118 <div ref="codeRef" class="code-lines w-fit" v-html="html" /> 119 <!-- eslint-enable vue/no-v-html --> 120 </div> 121 </div> 122</template> 123 124<style scoped> 125.code-viewer { 126 font-size: 14px; 127} 128 129.line-numbers { 130 /* 1ch per digit + 1.5rem (px-3 * 2) padding */ 131 min-width: calc(var(--line-digits) * 1ch + 1.5rem); 132} 133 134.code-content :deep(pre) { 135 margin: 0; 136 padding: 0; 137 background: transparent !important; 138 overflow: visible; 139} 140 141.code-content :deep(code) { 142 display: block; 143 padding: 0 1rem; 144 background: transparent !important; 145} 146 147.code-content :deep(.line) { 148 display: block; 149 /* Ensure consistent height matching line numbers */ 150 line-height: 24px; 151 min-height: 24px; 152 max-height: 24px; 153 white-space: pre; 154 overflow: hidden; 155 transition: background-color 0.1s; 156} 157 158/* Highlighted lines in code content - extend full width with negative margin */ 159.code-content :deep(.line.highlighted) { 160 background: rgb(234 179 8 / 0.2); /* yellow-500/20 */ 161 margin: 0 -1rem; 162 padding: 0 1rem; 163} 164 165/* Clickable import links */ 166.code-content :deep(.import-link) { 167 color: inherit; 168 text-decoration: underline; 169 text-decoration-style: dotted; 170 text-decoration-color: rgba(158, 203, 255, 0.5); /* syntax.str with transparency */ 171 text-underline-offset: 2px; 172 transition: 173 text-decoration-color 0.15s, 174 text-decoration-style 0.15s; 175 cursor: pointer; 176} 177 178.code-content :deep(.import-link:hover) { 179 text-decoration-style: solid; 180 text-decoration-color: #9ecbff; /* syntax.str - light blue */ 181} 182</style>