From 2d7b11e3650f53b5a950bca0c0c96ab3b4646acb Mon Sep 17 00:00:00 2001 From: hamki Date: Sun, 28 Dec 2025 15:17:23 +0800 Subject: [PATCH] feat: add TOC sidebar for README and Markdown files --- templates/repo/view_file.tmpl | 98 ++++++++++++++++---------------- web_src/css/repo/home.css | 57 ++++++++++--------- web_src/js/features/file-view.ts | 86 ++++++++++++++++++++++++---- 3 files changed, 153 insertions(+), 88 deletions(-) diff --git a/templates/repo/view_file.tmpl b/templates/repo/view_file.tmpl index f15b932b2b..f638e73f15 100644 --- a/templates/repo/view_file.tmpl +++ b/templates/repo/view_file.tmpl @@ -98,59 +98,57 @@ {{if not .IsMarkup}} {{template "repo/unicode_escape_prompt" dict "EscapeStatus" .EscapeStatus}} {{end}} -
-
- {{if .IsFileTooLarge}} - {{template "shared/filetoolarge" dict "RawFileLink" .RawFileLink}} - {{else if not .FileSize}} - {{template "shared/fileisempty"}} - {{else if .IsMarkup}} - {{.FileContent}} - {{else if .IsPlainText}} -
{{if .FileContent}}{{.FileContent}}{{end}}
- {{else if .FileContent}} - - - {{range $idx, $code := .FileContent}} - {{$line := Eval $idx "+" 1}} - - - {{if $.EscapeStatus.Escaped}} - - {{end}} - - - {{end}} - -
{{if (index $.LineEscapeStatus $idx).Escaped}}{{end}}{{$code}}
- {{else}} -
- {{if .IsImageFile}} - {{$.RawFileLink}} - {{else if .IsVideoFile}} - - {{else if .IsAudioFile}} - - {{else}} -
- +
+ {{if .IsFileTooLarge}} + {{template "shared/filetoolarge" dict "RawFileLink" .RawFileLink}} + {{else if not .FileSize}} + {{template "shared/fileisempty"}} + {{else if .IsMarkup}} + {{.FileContent}} + {{else if .IsPlainText}} +
{{if .FileContent}}{{.FileContent}}{{end}}
+ {{else if .FileContent}} + + + {{range $idx, $code := .FileContent}} + {{$line := Eval $idx "+" 1}} + + + {{if $.EscapeStatus.Escaped}} + + {{end}} + + + {{end}} + +
{{if (index $.LineEscapeStatus $idx).Escaped}}{{end}}{{$code}}
+ {{else}} +
+ {{if .IsImageFile}} + {{$.RawFileLink}} + {{else if .IsVideoFile}} + + {{else if .IsAudioFile}} + + {{else}} + - {{end}} -
- {{if or .ReadmeTocHTML .FileTocHTML}} -
- {{or .ReadmeTocHTML .FileTocHTML}} -
+
+ {{end}} +
{{end}}
+ {{if or .ReadmeTocHTML .FileTocHTML}} +
+ {{or .ReadmeTocHTML .FileTocHTML}} +
+ {{end}}
{{if $.Permission.CanRead ctx.Consts.RepoUnitTypeIssues}} diff --git a/web_src/css/repo/home.css b/web_src/css/repo/home.css index f05408f00e..110f8f7389 100644 --- a/web_src/css/repo/home.css +++ b/web_src/css/repo/home.css @@ -76,6 +76,12 @@ .repo-view-content { flex: 1; min-width: 0; + transition: margin-right 0.2s ease; +} + +/* When TOC is visible, reserve space on the right (only for file view, not home page) */ +.repo-view-content.toc-visible { + margin-right: 270px; } .language-stats { @@ -106,37 +112,31 @@ text-decoration: none; } -/* File view with TOC sidebar */ -.file-view-container { - position: relative; -} - -.file-view-container .file-view { - width: 100%; -} - -/* TOC as a floating sidebar panel */ +/* TOC as a fixed sidebar panel */ .file-view-toc { position: fixed; - top: 60px; - right: 0; - width: 280px; - max-height: calc(100vh - 80px); + top: 120px; /* Will be adjusted by JS to align with file content */ + right: 0.5rem; + width: 260px; + max-height: calc(100vh - 140px); overflow-y: auto; - padding: 1rem; - background: var(--color-body); + padding: 0.75rem; + background: var(--color-box-body); border: 1px solid var(--color-secondary); - border-right: none; - border-radius: var(--border-radius) 0 0 var(--border-radius); - box-shadow: -4px 0 12px rgba(0, 0, 0, 0.15); - z-index: 100; - transform: translateX(0); - transition: transform 0.25s ease-in-out; + border-radius: var(--border-radius); + z-index: 50; + opacity: 0; /* Hidden until JS positions it */ + transition: opacity 0.15s ease; +} + +/* Show TOC after JS has positioned it */ +.file-view-toc.toc-positioned { + opacity: 1; } /* Hidden state - using custom class to avoid Tailwind conflicts */ .file-view-toc.toc-panel-hidden { - transform: translateX(100%); + display: none; } .file-view-toc details { @@ -180,17 +180,20 @@ /* TOC toggle button active state - when TOC is visible */ #toggle-toc-btn.active { color: var(--color-primary); - background: var(--color-hover); } /* Hide TOC sidebar on small screens */ -@media (max-width: 768px) { +@media (max-width: 1400px) { .file-view-toc { - width: 100%; - max-width: 300px; + display: none !important; } #toggle-toc-btn { display: none; } + + /* Don't reserve space for TOC on small screens */ + .repo-view-content.toc-visible { + margin-right: 0; + } } diff --git a/web_src/js/features/file-view.ts b/web_src/js/features/file-view.ts index 74e3d042b2..08f5ce35c9 100644 --- a/web_src/js/features/file-view.ts +++ b/web_src/js/features/file-view.ts @@ -59,35 +59,99 @@ async function renderRawFileToContainer(container: HTMLElement, rawFileLink: str } } +function updateTocPosition(elFileView: HTMLElement, tocSidebar: HTMLElement): void { + const fileHeader = elFileView.querySelector('.file-header'); + if (!fileHeader) return; + + const headerRect = fileHeader.getBoundingClientRect(); + // Align TOC top with the file header top, with a minimum of 12px from viewport top + const topPos = Math.max(12, headerRect.top); + tocSidebar.style.top = `${topPos}px`; + + // Position TOC right next to the content (works for both file view and home page) + const segment = elFileView.querySelector('.ui.segment'); + if (segment) { + const segmentRect = segment.getBoundingClientRect(); + const leftPos = segmentRect.right + 8; // 8px gap from content + tocSidebar.style.left = `${leftPos}px`; + tocSidebar.style.right = 'auto'; + } + + // Mark as positioned to show the TOC (prevents flicker) + tocSidebar.classList.add('toc-positioned'); +} + function initTocToggle(elFileView: HTMLElement): void { const toggleBtn = elFileView.querySelector('#toggle-toc-btn'); - const tocSidebar = elFileView.querySelector('.file-view-toc'); + const tocSidebar = elFileView.querySelector('.file-view-toc'); if (!toggleBtn || !tocSidebar) return; + // Check if we're in file view (not home page) - only file view needs margin adjustment + const repoViewContent = elFileView.closest('.repo-view-content'); + const isFileView = Boolean(repoViewContent); + + // Helper to update position + const updatePosition = () => { + if (!tocSidebar.classList.contains('toc-panel-hidden')) { + updateTocPosition(elFileView, tocSidebar); + } + }; + + // Helper to show TOC with proper positioning + const showToc = () => { + toggleBtn.classList.add('active'); + + // Wait for margin to take effect before showing and positioning TOC + const showAfterLayout = () => { + tocSidebar.classList.remove('toc-panel-hidden'); + requestAnimationFrame(() => { + updateTocPosition(elFileView, tocSidebar); + }); + }; + + // For file view, first add margin, wait for layout, then show TOC + if (isFileView) { + repoViewContent.classList.add('toc-visible'); + // Wait for CSS transition to complete (200ms) before calculating position + setTimeout(showAfterLayout, 220); + } else { + // For home page (README), no margin needed, show with small delay + setTimeout(showAfterLayout, 10); + } + }; + + // Helper to hide TOC + const hideToc = () => { + tocSidebar.classList.add('toc-panel-hidden'); + tocSidebar.classList.remove('toc-positioned'); + toggleBtn.classList.remove('active'); + if (isFileView) { + repoViewContent.classList.remove('toc-visible'); + } + }; + // Restore saved state from localStorage (default to hidden) const savedState = localStorage.getItem('file-view-toc-visible'); const isVisible = savedState === 'true'; // default to hidden // Apply initial state if (isVisible) { - tocSidebar.classList.remove('toc-panel-hidden'); - toggleBtn.classList.add('active'); + showToc(); } else { - tocSidebar.classList.add('toc-panel-hidden'); - toggleBtn.classList.remove('active'); + hideToc(); } + // Update TOC position on resize/scroll to keep aligned with file content + window.addEventListener('resize', updatePosition); + window.addEventListener('scroll', updatePosition); + toggleBtn.addEventListener('click', () => { const isCurrentlyVisible = !tocSidebar.classList.contains('toc-panel-hidden'); if (isCurrentlyVisible) { - // Hide TOC - tocSidebar.classList.add('toc-panel-hidden'); - toggleBtn.classList.remove('active'); + hideToc(); localStorage.setItem('file-view-toc-visible', 'false'); } else { - // Show TOC - tocSidebar.classList.remove('toc-panel-hidden'); - toggleBtn.classList.add('active'); + showToc(); localStorage.setItem('file-view-toc-visible', 'true'); } });