Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 44 additions & 1 deletion app/pages/package-docs/[...path].vue
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,40 @@ const stickyStyle = computed(() => {
'--combined-header-height': `${56 + (packageHeaderHeight.value || 44)}px`,
}
})

// Track active TOC item based on URL hash
const tocContainerRef = useTemplateRef('tocContainerRef')

function updateActiveTocLink() {
const container = tocContainerRef.value
if (!container) return

const hash = window.location.hash

container.querySelector('a.toc-active')?.classList.remove('toc-active')

if (hash) {
const link = container.querySelector(`a[href="#${CSS.escape(hash.slice(1))}"]`)
link?.classList.add('toc-active')
}
}

function onTocClick(e: Event) {
const link = (e.target as HTMLElement).closest('a[href^="#"]')
if (link) {
nextTick(updateActiveTocLink)
}
Comment on lines +159 to +163
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Read-only verification: find cast-based EventTarget.closest usage patterns in Vue files.
rg -nP --type=vue -C2 '\(e\.target as HTMLElement\)\.closest\('

Repository: npmx-dev/npmx.dev

Length of output: 88


🏁 Script executed:

# First, locate and read the specific file
cat -n app/pages/package-docs/[...path].vue | head -180 | tail -40

Repository: npmx-dev/npmx.dev

Length of output: 1286


🏁 Script executed:

# Check the actual file structure and read lines around 159-163
wc -l app/pages/package-docs/[...path].vue && echo "---" && sed -n '150,170p' app/pages/package-docs/[...path].vue

Repository: npmx-dev/npmx.dev

Length of output: 587


🏁 Script executed:

# Find similar patterns in .vue files
fd -e vue -x rg -l 'e\.target' {} \; | head -5

Repository: npmx-dev/npmx.dev

Length of output: 230


🏁 Script executed:

# Search for similar EventTarget.closest patterns in .vue files
fd -e vue -x rg -n 'e\.target.*\.closest\|target.*as HTMLElement' {} +

Repository: npmx-dev/npmx.dev

Length of output: 6990


🏁 Script executed:

# Search for similar patterns in all Vue files
rg -n 'target as HTMLElement' app/

Repository: npmx-dev/npmx.dev

Length of output: 491


Guard EventTarget before calling closest().

e.target is not guaranteed to be an HTMLElement; this cast can fail at runtime on non-Element targets. Add a runtime instanceof Element check first.

🛠 Proposed fix
 function onTocClick(e: Event) {
-  const link = (e.target as HTMLElement).closest('a[href^="#"]')
+  const target = e.target
+  if (!(target instanceof Element)) return
+  const link = target.closest('a[href^="#"]')
   if (link) {
     nextTick(updateActiveTocLink)
   }
 }

Per the coding guidelines, ensure strictly type-safe code by checking types at runtime before calling methods that don't exist on the source type.

}

if (import.meta.client) {
watch(
() => docsData.value?.toc,
() => nextTick(updateActiveTocLink),
)

onMounted(updateActiveTocLink)
useEventListener(window, 'hashchange', updateActiveTocLink)
}
</script>

<template>
Expand All @@ -162,7 +196,12 @@ const stickyStyle = computed(() => {
{{ $t('package.docs.contents') }}
</h2>
<!-- eslint-disable vue/no-v-html -->
<div class="toc-content" v-html="docsData.toc" />
<div
ref="tocContainerRef"
class="toc-content"
@click="onTocClick"
v-html="docsData.toc"
/>
</div>
</aside>

Expand Down Expand Up @@ -231,6 +270,10 @@ const stickyStyle = computed(() => {
@apply text-xs text-fg-subtle hover:text-fg block py-0.5 truncate;
}

.toc-content a.toc-active {
@apply text-fg font-medium;
}

/* Main docs content container - no max-width to use full space */
.docs-content {
@apply max-w-none;
Expand Down
Loading