Skip to content
Open
Show file tree
Hide file tree
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
53 changes: 51 additions & 2 deletions app/components/Code/Viewer.vue
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,15 @@ const props = defineProps<{
html: string
lines: number
selectedLines: { start: number; end: number } | null
wordWrap?: boolean
}>()

const emit = defineEmits<{
lineClick: [lineNum: number, event: MouseEvent]
}>()

const codeRef = useTemplateRef('codeRef')
const lineNumbersRef = useTemplateRef('lineNumbersRef')

// Generate line numbers array
const lineNumbers = computed(() => {
Expand All @@ -32,6 +34,30 @@ function onLineClick(lineNum: number, event: MouseEvent) {
emit('lineClick', lineNum, event)
}

// Synchronize line number heights with code line heights (needed for word wrap)
function syncLineHeights() {
if (!props.wordWrap || !codeRef.value || !lineNumbersRef.value) {
// Reset heights if word wrap is disabled
if (lineNumbersRef.value) {
const nums = lineNumbersRef.value.querySelectorAll<HTMLElement>('.line-number')
nums.forEach(num => (num.style.height = ''))
Comment on lines +41 to +43
Copy link
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:

find app/components/Code -name "Viewer.vue" -type f

Repository: npmx-dev/npmx.dev

Length of output: 91


🏁 Script executed:

cat -n app/components/Code/Viewer.vue | sed -n '35,50p'

Repository: npmx-dev/npmx.dev

Length of output: 749


🏁 Script executed:

cat -n app/components/Code/Viewer.vue | head -50

Repository: npmx-dev/npmx.dev

Length of output: 1935


🏁 Script executed:

# Check if there's a Biome configuration file and verify the lint rule
find . -maxdepth 2 -name "biome.json*" -o -name ".biomerc*" 2>/dev/null | head -5

Repository: npmx-dev/npmx.dev

Length of output: 73


🏁 Script executed:

cat biome.json

Repository: npmx-dev/npmx.dev

Length of output: 1835


🏁 Script executed:

# Let's verify the exact behavior of the useIterableCallbackReturn rule by checking if there are similar patterns in the file
rg "forEach" app/components/Code/Viewer.vue -A 1

Repository: npmx-dev/npmx.dev

Length of output: 406


🏁 Script executed:

# Let's check if there are any other similar patterns in the codebase that might show how this is being handled elsewhere
rg "forEach.*=>.*=" --type ts --type tsx --type vue -B 1 -A 1 | head -30

Repository: npmx-dev/npmx.dev

Length of output: 88


🏁 Script executed:

# Check the exact pattern and what Biome would flag
rg "forEach.*=>" --type vue --type ts -B 1 -A 1 | head -40

Repository: npmx-dev/npmx.dev

Length of output: 88


🏁 Script executed:

# Check the exact pattern without type filters
rg "forEach.*=>" -B 1 -A 1 | head -50

Repository: npmx-dev/npmx.dev

Length of output: 2777


🏁 Script executed:

# Let's see the exact context around lines 41-43 to understand the complete picture
cat -n app/components/Code/Viewer.vue | sed -n '38,46p'

Repository: npmx-dev/npmx.dev

Length of output: 456


🏁 Script executed:

# Let's search for how similar patterns are handled elsewhere with block bodies
rg "forEach.*{" --max-count 20 | head -40

Repository: npmx-dev/npmx.dev

Length of output: 144


🏁 Script executed:

# Let's look for how forEach is typically used in Vue components
rg "forEach" app/components/Code/Viewer.vue -B 1 -A 2

Repository: npmx-dev/npmx.dev

Length of output: 750


Use a block body when clearing the line-number heights.

Line 43 returns the assignment value from the forEach callback, which Biome's lint/suspicious/useIterableCallbackReturn rule flags. A block body keeps the behaviour the same and resolves the lint error:

nums.forEach(num => {
  num.style.height = ''
})
🧰 Tools
🪛 Biome (2.4.6)

[error] 43-43: This callback passed to forEach() iterable method should not return a value.

(lint/suspicious/useIterableCallbackReturn)

}
return
}

const lines = codeRef.value.querySelectorAll<HTMLElement>('code > .line')
const nums = lineNumbersRef.value.querySelectorAll<HTMLElement>('.line-number')

lines.forEach((line, index) => {
const num = nums[index]
if (num) {
// Use getBoundingClientRect for more precision if needed, but offsetHeight is usually enough
const height = line.offsetHeight
num.style.height = `${height}px`
}
})
}
Comment on lines +37 to +59
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify that markdown files default to preview mode, that CodeViewer is mounted with v-show,
# and that syncLineHeights currently only reruns on html/wordWrap/resize.
rg -n -C3 "const markdownViewMode|<CodeViewer|v-show=\"!fileContent.markdownHtml \\|\\| markdownViewMode === 'code'\"" "app/pages/package-code/[[org]]/[packageName]/v/[version]/[...filePath].vue"
rg -n -C3 "function syncLineHeights|offsetHeight|watch\\(|props.wordWrap|useEventListener\\(window, 'resize'" "app/components/Code/Viewer.vue"

Repository: npmx-dev/npmx.dev

Length of output: 2129


Re-sync wrapped line heights after the viewer becomes visible.

The parent component mounts CodeViewer with v-show="!fileContent.markdownHtml || markdownViewMode === 'code'" and defaults to preview mode. For markdown files, this means the component is initially hidden (display: none), yet the immediate watcher on [props.selectedLines, props.html] fires during mount and calls syncLineHeights() while the viewer is not rendered. This causes offsetHeight to return 0, setting all line-number heights to 0px. The heights are never corrected when the user switches to code view with word wrap enabled, leaving the line-number column collapsed.

Add visibility detection to skip measurement while hidden, or use a ResizeObserver on the rendered code block to continuously maintain synchronisation.

🧰 Tools
🪛 Biome (2.4.6)

[error] 43-43: This callback passed to forEach() iterable method should not return a value.

(lint/suspicious/useIterableCallbackReturn)


// Apply highlighting to code lines when selection changes
function updateLineHighlighting() {
if (!codeRef.value) return
Expand All @@ -53,11 +79,27 @@ function updateLineHighlighting() {
watch(
() => [props.selectedLines, props.html] as const,
() => {
nextTick(updateLineHighlighting)
nextTick(() => {
updateLineHighlighting()
syncLineHeights()
})
},
{ immediate: true },
)

// Also watch wordWrap specifically
watch(
() => props.wordWrap,
() => {
nextTick(syncLineHeights)
},
)

// Sync on resize
if (import.meta.client) {
useEventListener(window, 'resize', syncLineHeights)
}

// Use Nuxt's `navigateTo` for the rendered import links
function handleImportLinkNavigate() {
if (!codeRef.value) return
Expand Down Expand Up @@ -86,9 +128,10 @@ watch(
</script>

<template>
<div class="code-viewer flex min-h-full max-w-full">
<div class="code-viewer flex min-h-full max-w-full" :class="{ 'is-wrapped': wordWrap }">
<!-- Line numbers column -->
<div
ref="lineNumbersRef"
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"
Expand Down Expand Up @@ -155,6 +198,12 @@ watch(
transition: background-color 0.1s;
}

.is-wrapped .code-content :deep(.line) {
white-space: pre-wrap;
word-break: break-all;
max-height: none;
}

/* Highlighted lines in code content - extend full width with negative margin */
.code-content :deep(.line.highlighted) {
@apply bg-yellow-500/20;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -265,6 +265,8 @@ function scrollToTop() {
// Canonical URL for this code page
const canonicalUrl = computed(() => `https://npmx.dev${getCodeUrl(route.params)}`)

const wordWrap = useLocalStorage('npmx-code-word-wrap', false)

// Toggle markdown view mode
const markdownViewModes = [
{
Expand Down Expand Up @@ -471,6 +473,15 @@ defineOgImageComponent('Default', {
<span class="i-lucide:arrow-up w-3 h-3" />
{{ $t('code.scroll_to_top') }}
</button>
<button
type="button"
class="px-2 py-1 font-mono text-xs text-fg-muted bg-bg-subtle border border-border rounded hover:text-fg hover:border-border-hover transition-colors items-center inline-flex gap-1"
:class="{ 'bg-accent/10 text-accent border-accent/20': wordWrap }"
@click="wordWrap = !wordWrap"
>
<span class="i-lucide:wrap-text w-3 h-3" />
{{ $t('code.word_wrap') }}
</button>
Comment on lines +476 to +484
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Expose the wrap state on the toggle button.

This behaves as a toggle, but without aria-pressed assistive tech cannot tell whether wrapping is on or off. Please bind the pressed state here, for example with :aria-pressed="wordWrap".

<button
v-if="selectedLines"
type="button"
Expand Down Expand Up @@ -515,6 +526,7 @@ defineOgImageComponent('Default', {
:html="fileContent.html"
:lines="fileContent.lines"
:selected-lines="selectedLines"
:word-wrap="wordWrap"
@line-click="handleLineClick"
/>
</template>
Expand Down
3 changes: 2 additions & 1 deletion i18n/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -792,7 +792,8 @@
"code": "code"
},
"file_path": "File path",
"scroll_to_top": "Scroll to top"
"scroll_to_top": "Scroll to top",
"word_wrap": "Word wrap"
},
"badges": {
"provenance": {
Expand Down
3 changes: 3 additions & 0 deletions i18n/schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -2382,6 +2382,9 @@
},
"scroll_to_top": {
"type": "string"
},
"word_wrap": {
"type": "string"
}
},
"additionalProperties": false
Expand Down
Loading