Skip to content
Closed
Show file tree
Hide file tree
Changes from 6 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
108 changes: 70 additions & 38 deletions app/components/Code/Viewer.vue
Original file line number Diff line number Diff line change
Expand Up @@ -11,16 +11,37 @@ const emit = defineEmits<{

const codeRef = useTemplateRef('codeRef')

// Generate line numbers array
const lineNumbers = computed(() => {
return Array.from({ length: props.lines }, (_, i) => i + 1)
})
// Using this so we can track the height of each line, and therefore compute digit sidebar
const lineMultipliers = ref<number[]>([])

function updateLineMultipliers() {
if (!codeRef.value) return
const lines = Array.from(codeRef.value.querySelectorAll('code > .line'))
lineMultipliers.value = lines
.map(line => Math.round(parseFloat(getComputedStyle(line).height) / 24)) // since each line "row" is 24px high
.filter(m => m > 0)
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

// Used for CSS calculation of line number column width
const lineDigits = computed(() => {
return String(props.lines).length
watch(
() => props.html,
() => nextTick(updateLineMultipliers),
{ immediate: true },
)
useResizeObserver(codeRef, updateLineMultipliers)

// Line numbers ++ blank rows for the wrapped lines
const displayLines = computed(() => {
const result: (number | null)[] = []
for (let i = 0; i < props.lines; i++) {
result.push(i + 1)
const extra = (lineMultipliers.value[i] ?? 1) - 1
for (let j = 0; j < extra; j++) result.push(null)
}
return result
})

const lineDigits = computed(() => String(props.lines).length)

// Check if a line is selected
function isLineSelected(lineNum: number): boolean {
if (!props.selectedLines) return false
Expand Down Expand Up @@ -86,36 +107,37 @@ 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" :style="{ '--line-digits': lineDigits }">
<!-- Line numbers column -->
<div
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"
>
<!-- This needs to be a native <a> element, because `LinkBase` (or specifically `NuxtLink`) does not seem to work when trying to prevent default behavior (jumping to the anchor) -->
<a
v-for="lineNum in lineNumbers"
:id="`L${lineNum}`"
:key="lineNum"
:href="`#L${lineNum}`"
tabindex="-1"
class="line-number block px-3 py-0 font-mono text-sm leading-6 cursor-pointer transition-colors no-underline"
:class="[
isLineSelected(lineNum)
? 'bg-yellow-500/20 text-fg'
: 'text-fg-subtle hover:text-fg-muted',
]"
@click.prevent="onLineClick(lineNum, $event)"
>
{{ lineNum }}
</a>
<template v-for="(lineNum, idx) in displayLines" :key="idx">
<a
v-if="lineNum !== null"
:id="`L${lineNum}`"
:href="`#L${lineNum}`"
tabindex="-1"
class="line-number block px-3 py-0 font-mono text-sm leading-6 cursor-pointer transition-colors no-underline"
:class="[
isLineSelected(lineNum)
? 'bg-yellow-500/20 text-fg'
: 'text-fg-subtle hover:text-fg-muted',
]"
@click.prevent="onLineClick(lineNum, $event)"
>
{{ lineNum }}
</a>
<span v-else class="block px-3 leading-6">&nbsp;</span>
</template>
</div>

<!-- Code content -->
<div class="code-content flex-1 overflow-x-auto min-w-0">
<div class="code-content">
<!-- 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" v-html="html" />
<!-- eslint-enable vue/no-v-html -->
</div>
</div>
Expand All @@ -124,46 +146,56 @@ watch(
<style scoped>
.code-viewer {
font-size: 14px;
/* 1ch per digit + 1.5rem (px-3 * 2) padding */
--line-numbers-width: calc(var(--line-digits) * 1ch + 1.5rem);
}

.line-numbers {
/* 1ch per digit + 1.5rem (px-3 * 2) padding */
min-width: calc(var(--line-digits) * 1ch + 1.5rem);
min-width: var(--line-numbers-width);
}

.code-content :deep(pre) {
.code-content {
flex: 1;
min-width: 0;
max-width: calc(100% - var(--line-numbers-width));
}

.code-content:deep(pre) {
margin: 0;
padding: 0;
background: transparent !important;
overflow: visible;
max-width: 100%;
}

.code-content :deep(code) {
.code-content:deep(code) {
display: block;
padding: 0 1rem;
background: transparent !important;
max-width: 100%;
}

.code-content :deep(.line) {
display: block;
.code-content:deep(.line) {
display: flex;
flex-wrap: wrap;
/* Ensure consistent height matching line numbers */
line-height: 24px;
min-height: 24px;
max-height: 24px;
white-space: pre;
white-space: pre-wrap;
overflow: hidden;
Comment on lines +175 to 182
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 | 🟠 Major

Long unbroken tokens can still overflow in wrap mode.

Line 185 uses white-space: pre-wrap, which does not hard-break long strings without natural breakpoints (e.g. minified blobs, hashes, base64). This can reintroduce horizontal overflow despite the wrapping objective.

🩹 Proposed fix
 .code-content:deep(.line) {
   display: flex;
   flex-wrap: wrap;
   /* Ensure consistent height matching line numbers */
   line-height: calc(v-bind(LINE_HEIGHT_PX) * 1px);
   min-height: calc(v-bind(LINE_HEIGHT_PX) * 1px);
   white-space: pre-wrap;
+  overflow-wrap: anywhere;
+  word-break: break-word;
   overflow: hidden;
   transition: background-color 0.1s;
   max-width: 100%;
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
.code-content:deep(.line) {
display: flex;
flex-wrap: wrap;
/* Ensure consistent height matching line numbers */
line-height: 24px;
min-height: 24px;
max-height: 24px;
white-space: pre;
line-height: calc(v-bind(LINE_HEIGHT_PX) * 1px);
min-height: calc(v-bind(LINE_HEIGHT_PX) * 1px);
white-space: pre-wrap;
overflow: hidden;
.code-content:deep(.line) {
display: flex;
flex-wrap: wrap;
/* Ensure consistent height matching line numbers */
line-height: calc(v-bind(LINE_HEIGHT_PX) * 1px);
min-height: calc(v-bind(LINE_HEIGHT_PX) * 1px);
white-space: pre-wrap;
overflow-wrap: anywhere;
word-break: break-word;
overflow: hidden;

transition: background-color 0.1s;
max-width: 100%;
}

/* Highlighted lines in code content - extend full width with negative margin */
.code-content :deep(.line.highlighted) {
.code-content:deep(.line.highlighted) {
@apply bg-yellow-500/20;
margin: 0 -1rem;
padding: 0 1rem;
}

/* Clickable import links */
.code-content :deep(.import-link) {
.code-content:deep(.import-link) {
color: inherit;
text-decoration: underline;
text-decoration-style: dotted;
Expand All @@ -175,7 +207,7 @@ watch(
cursor: pointer;
}

.code-content :deep(.import-link:hover) {
.code-content:deep(.import-link:hover) {
text-decoration-style: solid;
text-decoration-color: #9ecbff; /* syntax.str - light blue */
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -347,10 +347,10 @@ defineOgImageComponent('Default', {
</div>

<!-- Main content: file tree + file viewer -->
<div v-else-if="fileTree" class="flex flex-1" dir="ltr">
<div v-else-if="fileTree" class="main-content flex flex-1" dir="ltr">
<!-- File tree sidebar - sticky with internal scroll -->
<aside
class="w-64 lg:w-72 border-ie border-border shrink-0 hidden md:block bg-bg-subtle sticky top-25 self-start h-[calc(100vh-7rem)] overflow-y-auto"
class="file-tree border-ie border-border shrink-0 hidden md:block bg-bg-subtle sticky top-25 self-start h-[calc(100vh-7rem)] overflow-y-auto"
>
<CodeFileTree
:tree="fileTree.tree"
Expand All @@ -361,7 +361,7 @@ defineOgImageComponent('Default', {
</aside>

<!-- File content / Directory listing - sticky with internal scroll on desktop -->
<div class="flex-1 min-w-0 self-start">
<div class="file-viewer flex-1 min-w-0 self-start">
<div
class="sticky z-10 top-25 bg-bg border-b border-border px-4 py-2 flex items-center justify-between gap-2 text-nowrap overflow-x-auto max-w-full"
>
Expand Down Expand Up @@ -584,3 +584,21 @@ defineOgImageComponent('Default', {
</ClientOnly>
</main>
</template>

<style scoped>
.main-content {
--sidebar-space: calc(var(--spacing) * 64);
}
@screen lg {
.main-content {
--sidebar-space: calc(var(--spacing) * 72);
}
}

.file-tree {
width: var(--sidebar-space);
}
.file-viewer {
width: calc(100% - var(--sidebar-space));
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
</style>
12 changes: 4 additions & 8 deletions app/utils/chart-data-buckets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,17 +34,13 @@ export function buildWeeklyEvolution(
if (sorted.length === 0) return []

const rangeStartDate = parseIsoDate(rangeStartIso)
const rangeEndDate = parseIsoDate(rangeEndIso)

// Align from last day with actual data (npm has 1-2 day delay, today is incomplete)
const lastNonZero = sorted.findLast(d => d.value > 0)
const pickerEnd = parseIsoDate(rangeEndIso)
const effectiveEnd = lastNonZero ? parseIsoDate(lastNonZero.day) : pickerEnd
const rangeEndDate = effectiveEnd.getTime() < pickerEnd.getTime() ? effectiveEnd : pickerEnd

// Group into 7-day buckets from END backwards
const buckets = new Map<number, number>()

for (const item of sorted) {
const offset = Math.floor((rangeEndDate.getTime() - parseIsoDate(item.day).getTime()) / DAY_MS)
const itemDate = parseIsoDate(item.day)
const offset = Math.floor((rangeEndDate.getTime() - itemDate.getTime()) / DAY_MS)
Comment on lines +37 to +43
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.

Are these changes intended in this PR?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

They are not, a mistake on my part for merging latest instead of rebasing :)

I will have to remove them. Don't think this PR will end up being merged to be honest, given the above conversation

if (offset < 0) continue
const idx = Math.floor(offset / 7)
buckets.set(idx, (buckets.get(idx) ?? 0) + item.value)
Expand Down
12 changes: 8 additions & 4 deletions test/unit/app/utils/chart-data-buckets.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ describe('buildWeeklyEvolution', () => {
expect(result[1]!.weekEnd).toBe('2025-03-10')
})

it('aligns from last non-zero data day, ignoring trailing zeros', () => {
it('always aligns from rangeEnd, even with trailing zeros', () => {
const daily = [
{ day: '2025-03-01', value: 10 },
{ day: '2025-03-02', value: 10 },
Expand All @@ -99,10 +99,14 @@ describe('buildWeeklyEvolution', () => {

const result = buildWeeklyEvolution(daily, '2025-03-01', '2025-03-09')

expect(result).toHaveLength(1)
expect(result[0]!.value).toBe(70)
// Bucket 0: 03-03..03-09 = 50, Bucket 1: 03-01..03-02 (partial, scaled)
expect(result).toHaveLength(2)
Comment thread
nosthrillz marked this conversation as resolved.
expect(result[0]!.weekStart).toBe('2025-03-01')
expect(result[0]!.weekEnd).toBe('2025-03-07')
expect(result[0]!.weekEnd).toBe('2025-03-02')
expect(result[0]!.value).toBe(Math.round((20 * 7) / 2))
expect(result[1]!.weekStart).toBe('2025-03-03')
expect(result[1]!.weekEnd).toBe('2025-03-09')
expect(result[1]!.value).toBe(50)
})

it('returns empty array for empty input', () => {
Expand Down
Loading