|
51 | 51 | let viewportHeight = $state(600) |
52 | 52 | let contentRef: HTMLDivElement | undefined = $state() |
53 | 53 | let containerRef: HTMLDivElement | undefined = $state() |
| 54 | + let linesContainerRef: HTMLDivElement | undefined = $state() |
| 55 | +
|
| 56 | + // High watermark of rendered line container width, for horizontal scroll |
| 57 | + let contentWidth = $state(0) |
54 | 58 |
|
55 | 59 | // Derived: which lines are visible |
56 | 60 | const visibleFrom = $derived(Math.max(0, Math.floor(scrollTop / LINE_HEIGHT) - BUFFER_LINES)) |
|
126 | 130 | } |
127 | 131 | }) |
128 | 132 |
|
| 133 | + // Track horizontal content width so .scroll-spacer can create a scrollbar |
| 134 | + $effect(() => { |
| 135 | + void visibleLines |
| 136 | + const rafId = requestAnimationFrame(() => { |
| 137 | + if (linesContainerRef) { |
| 138 | + const w = linesContainerRef.scrollWidth |
| 139 | + if (w > contentWidth) { |
| 140 | + contentWidth = w |
| 141 | + } |
| 142 | + } |
| 143 | + }) |
| 144 | + return () => { |
| 145 | + cancelAnimationFrame(rafId) |
| 146 | + } |
| 147 | + }) |
| 148 | +
|
129 | 149 | function scheduleFetch(from: number, to: number) { |
130 | 150 | // Clear any pending fetch |
131 | 151 | if (fetchDebounceTimer) { |
|
677 | 697 | bind:this={contentRef} |
678 | 698 | onscroll={handleScroll} |
679 | 699 | > |
680 | | - <div class="scroll-spacer" style="height: {estimatedTotalLines() * LINE_HEIGHT}px"> |
681 | | - <div class="lines-container" style="transform: translateY({visibleFrom * LINE_HEIGHT}px)"> |
| 700 | + <div |
| 701 | + class="scroll-spacer" |
| 702 | + style="height: {estimatedTotalLines() * LINE_HEIGHT}px; min-width: {contentWidth}px" |
| 703 | + > |
| 704 | + <div |
| 705 | + class="lines-container" |
| 706 | + bind:this={linesContainerRef} |
| 707 | + style="transform: translateY({visibleFrom * LINE_HEIGHT}px)" |
| 708 | + > |
682 | 709 | {#each visibleLines as { lineNumber, text } (lineNumber)} |
683 | 710 | <div class="line" data-line={lineNumber}> |
684 | 711 | <span class="line-number" style="width: {gutterWidth}ch" aria-hidden="true" |
|
812 | 839 | .lines-container { |
813 | 840 | position: absolute; |
814 | 841 | left: 0; |
815 | | - right: 0; |
| 842 | + width: max-content; |
| 843 | + min-width: 100%; |
816 | 844 | } |
817 | 845 |
|
818 | 846 | .line { |
|
839 | 867 |
|
840 | 868 | .line-text { |
841 | 869 | white-space: pre; |
842 | | - word-break: break-all; |
843 | | - flex: 1; |
844 | | - min-width: 0; |
845 | | - overflow: hidden; |
846 | 870 | } |
847 | 871 |
|
848 | 872 | mark { |
|
0 commit comments