Skip to content

feat(ui): added word wrap to code viewer#2028

Open
stephansama wants to merge 1 commit intonpmx-dev:mainfrom
stephansama:feat/add-word-wrap
Open

feat(ui): added word wrap to code viewer#2028
stephansama wants to merge 1 commit intonpmx-dev:mainfrom
stephansama:feat/add-word-wrap

Conversation

@stephansama
Copy link

🔗 Linked issue

resolves #2027

🧭 Context

scrolling horizontally is annoying when trying to just read code in my opinion.

📚 Description

I just added a small button in the toolbar to toggle word wrap and a class to enable and disable word wrap

I did use gemini to help with this PR however i implemented a similar feature on my personal blog on code blocks. just used the ai to quickly get up to speed

@vercel
Copy link

vercel bot commented Mar 10, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
npmx.dev Ready Ready Preview, Comment Mar 10, 2026 6:20pm
2 Skipped Deployments
Project Deployment Actions Updated (UTC)
docs.npmx.dev Ignored Ignored Preview Mar 10, 2026 6:20pm
npmx-lunaria Ignored Ignored Mar 10, 2026 6:20pm

Request Review

@github-actions
Copy link

Lunaria Status Overview

🌕 This pull request will trigger status changes.

Learn more

By default, every PR changing files present in the Lunaria configuration's files property will be considered and trigger status changes accordingly.

You can change this by adding one of the keywords present in the ignoreKeywords property in your Lunaria configuration file in the PR's title (ignoring all files) or by including a tracker directive in the merged commit's description.

Tracked Files

File Note
i18n/locales/en.json Source changed, localizations will be marked as outdated.
Warnings reference
Icon Description
🔄️ The source for this localization has been updated since the creation of this pull request, make sure all changes in the source have been applied.

@stephansama stephansama changed the title feat(word-wrap): added word wrap to code viewer feat(ui): added word wrap to code viewer Mar 10, 2026
@codecov
Copy link

codecov bot commented Mar 10, 2026

Codecov Report

❌ Patch coverage is 54.16667% with 11 lines in your changes missing coverage. Please review.
✅ All tests successful. No failed tests found.

Files with missing lines Patch % Lines
app/components/Code/Viewer.vue 54.16% 10 Missing and 1 partial ⚠️

📢 Thoughts on this report? Let us know!

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Mar 10, 2026

📝 Walkthrough

Walkthrough

This pull request introduces a word wrap feature for the code viewer component. The Code Viewer component receives a new optional wordWrap prop and implements logic to synchronize line number heights with wrapped code lines using a template reference and watchers. CSS updates enable pre-wrap text rendering and break-all word boundaries when wrapping is active. The package code page adds a UI toggle button that manages word wrap state via localStorage and propagates it to the Code Viewer component. Translations for the "word wrap" label are added to both the English locale file and the i18n schema.

Suggested reviewers

  • ghostdevv
🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description check ✅ Passed The PR description clearly relates to the changeset, explaining the addition of a word wrap toggle button and CSS class for the code viewer.
Linked Issues check ✅ Passed The PR successfully implements the word wrap feature requested in issue #2027, including the toggle button, CSS modifications, and line height synchronisation.
Out of Scope Changes check ✅ Passed All changes are directly scoped to implementing word wrap functionality. No extraneous modifications or unrelated code changes are present.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3


ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 29d80634-bca0-4cc4-8959-376aa22e6480

📥 Commits

Reviewing files that changed from the base of the PR and between 3712560 and ca13cca.

📒 Files selected for processing (4)
  • app/components/Code/Viewer.vue
  • app/pages/package-code/[[org]]/[packageName]/v/[version]/[...filePath].vue
  • i18n/locales/en.json
  • i18n/schema.json

Comment on lines +37 to +59
// 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 = ''))
}
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`
}
})
}
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)

Comment on lines +41 to +43
if (lineNumbersRef.value) {
const nums = lineNumbersRef.value.querySelectorAll<HTMLElement>('.line-number')
nums.forEach(num => (num.style.height = ''))
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)

Comment on lines +476 to +484
<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>
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".

@whitep4nth3r
Copy link
Contributor

Hi and thank you for this change!

In my opinion, and for accessibility reasons (for example for people who don't use a mouse) I believe that it's always better to wrap code regardless just using CSS.

For that reason I don't think we need a toggle and the wrapping can just be implemented using CSS.

@whitep4nth3r
Copy link
Contributor

Perhaps we should even have a max line length in the code viewer like in many IDEs.

@stephansama
Copy link
Author

Hi and thank you for this change!

In my opinion, and for accessibility reasons (for example for people who don't use a mouse) I believe that it's always better to wrap code regardless just using CSS.

For that reason I don't think we need a toggle and the wrapping can just be implemented using CSS.

i was trying to get it to work with css only however i was having a little issue. like it would mostly work but not all the way. i can take another crack at it again when i get out of work. 🫡

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

add word wrap to code viewer

2 participants