Skip to content
Closed
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
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
3 changes: 2 additions & 1 deletion i18n/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -840,7 +840,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
3 changes: 3 additions & 0 deletions i18n/schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -2526,6 +2526,9 @@
},
"binary_rendering_warning": {
"type": "string"
},
"toggle_word_wrap": {
"type": "string"
}
},
"additionalProperties": false
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