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

const emit = defineEmits<{
Expand Down Expand Up @@ -113,9 +114,17 @@ watch(
</div>

<!-- Code content -->
<div class="code-content flex-1 overflow-x-auto min-w-0">
<div
class="code-content flex-1 min-w-0"
:class="wordWrap ? 'overflow-x-hidden' : 'overflow-x-auto'"
>
<!-- eslint-disable vue/no-v-html -- HTML is generated server-side by Shiki -->
<div ref="codeRef" class="code-lines min-w-full w-fit" v-html="html" />
<div
ref="codeRef"
class="code-lines min-w-full w-fit"
:class="{ 'word-wrap': wordWrap }"
v-html="html"
/>
<!-- eslint-enable vue/no-v-html -->
</div>
</div>
Expand Down Expand Up @@ -155,6 +164,21 @@ watch(
transition: background-color 0.1s;
}

.code-content.word-wrap-active :deep(.line),
.code-content:has(.word-wrap) :deep(.line) {
white-space: pre-wrap;
overflow-wrap: break-word;
max-height: none;
overflow: visible;
}

.code-lines.word-wrap :deep(.line) {
white-space: pre-wrap;
overflow-wrap: break-word;
max-height: none;
overflow: visible;
}

/* 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 @@ -267,6 +267,8 @@ const markdownViewModes = [

const markdownViewMode = shallowRef<(typeof markdownViewModes)[number]['key']>('preview')

const wordWrap = shallowRef(false)

const bytesFormatter = useBytesFormatter()

// Keep latestVersion for comparison (to show "(latest)" badge)
Expand Down Expand Up @@ -416,6 +418,21 @@ defineOgImageComponent('Default', {
</nav>
</div>
<div class="flex items-center gap-2" v-if="isViewingFile && !isBinaryFile && fileContent">
<button
type="button"
class="px-2 py-1 font-mono text-xs border rounded transition-colors inline-flex items-center gap-1"
:class="
wordWrap
? 'bg-accent/10 text-accent border-accent/30'
: 'text-fg-muted bg-bg-subtle border-border hover:text-fg hover:border-border-hover'
"
:aria-pressed="wordWrap"
:title="$t('code.toggle_word_wrap')"
@click="wordWrap = !wordWrap"
>
<span class="i-lucide:wrap-text w-3 h-3" aria-hidden="true" />
{{ $t('code.toggle_word_wrap') }}
</button>
<button
v-if="selectedLines"
type="button"
Expand Down Expand Up @@ -462,6 +479,7 @@ defineOgImageComponent('Default', {
:html="fileContent.html"
:lines="fileContent.lines"
:selected-lines="selectedLines"
:word-wrap="wordWrap"
@line-click="handleLineClick"
/>
<div class="sticky bottom-0 bg-bg border-t border-border px-4 py-1">
Expand Down
27 changes: 22 additions & 5 deletions i18n/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,10 @@
"social": "social",
"chat": "chat",
"builders_chat": "builders",
"keyboard_shortcuts": "keyboard shortcuts"
"keyboard_shortcuts": "keyboard shortcuts",
"product": "Product",
"legal": "Legal",
"community": "Community"
},
"shortcuts": {
"section": {
Expand Down Expand Up @@ -207,6 +210,8 @@
"members": "members"
},
"scroll_to_top": "Scroll to top",
"previous": "Previous",
"next": "Next",
"cancel": "Cancel",
"save": "Save",
"edit": "Edit",
Expand Down Expand Up @@ -318,7 +323,8 @@
"docs": "docs",
"fund": "fund",
"compare": "compare",
"compare_this_package": "compare this package"
"compare_this_package": "compare this package",
"dependents": "dependents"
},
"likes": {
"like": "Like this package",
Expand Down Expand Up @@ -352,6 +358,13 @@
"title": "Run",
"locally": "Run locally"
},
"dependents": {
"title": "Dependents",
"subtitle": "Packages that depend on {name}",
"count": "{count} dependent | {count} dependents",
"none": "No packages found that depend on {name}",
"error": "Failed to load dependents"
},
"readme": {
"title": "Readme",
"no_readme": "No README available.",
Expand Down Expand Up @@ -454,7 +467,8 @@
"outdated_minor": "{count} minor version behind (latest: {latest}) | {count} minor versions behind (latest: {latest})",
"outdated_patch": "Patch update available (latest: {latest})",
"has_replacement": "This dependency has suggested replacements",
"vulnerabilities_count": "{count} vulnerability | {count} vulnerabilities"
"vulnerabilities_count": "{count} vulnerability | {count} vulnerabilities",
"none": "No dependencies"
},
"peer_dependencies": {
"title": "Peer Dependency ({count}) | Peer Dependencies ({count})",
Expand Down Expand Up @@ -564,7 +578,9 @@
},
"license": {
"view_spdx": "View license text on SPDX",
"none": "None"
"none": "None",
"changed_badge": "changed",
"changed": "License changed from {latest} in the latest version"
},
"vulnerabilities": {
"tree_found": "{vulns} vulnerability in {packages}/{total} packages | {vulns} vulnerabilities in {packages}/{total} packages",
Expand Down Expand Up @@ -840,7 +856,8 @@
},
"file_path": "File path",
"binary_file": "Binary file",
"binary_rendering_warning": "File type \"{contentType}\" is not supported for preview."
"binary_rendering_warning": "File type \"{contentType}\" is not supported for preview.",
"toggle_word_wrap": "Word wrap"
},
"badges": {
"provenance": {
Expand Down
50 changes: 50 additions & 0 deletions test/nuxt/components/CodeViewer.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { describe, expect, it } from 'vitest'
import { mountSuspended } from '@nuxt/test-utils/runtime'
import CodeViewer from '~/components/Code/Viewer.vue'

const SAMPLE_HTML = '<code><span class="line">const x = 1</span></code>'

describe('CodeViewer', () => {
it('renders when given html and line count', async () => {
const wrapper = await mountSuspended(CodeViewer, {
props: {
html: SAMPLE_HTML,
lines: 1,
selectedLines: null,
wordWrap: false,
},
})

expect(wrapper.find('pre').exists() || wrapper.html().includes('line')).toBe(true)
})

it('applies word-wrap class to pre element when wordWrap prop is true', async () => {
const wrapper = await mountSuspended(CodeViewer, {
props: {
html: SAMPLE_HTML,
lines: 1,
selectedLines: null,
wordWrap: true,
},
})

const html = wrapper.html()
// When wordWrap is true, the code-lines div should have the 'word-wrap' class
expect(html).toContain('word-wrap')
})

it('does not apply word-wrap class when wordWrap prop is false', async () => {
const wrapper = await mountSuspended(CodeViewer, {
props: {
html: SAMPLE_HTML,
lines: 1,
selectedLines: null,
wordWrap: false,
},
})

const codeLines = wrapper.find('.code-lines')
// Without word wrap, the code-lines div should NOT have the 'word-wrap' class
expect(codeLines.classes()).not.toContain('word-wrap')
})
})
Loading