From 5097aba842a48a078292141e6af58f7867ff14b5 Mon Sep 17 00:00:00 2001 From: hypercross Date: Wed, 8 Jul 2026 14:58:45 +0800 Subject: [PATCH] refactor: implement scroll container context and flex layout Switch from fixed positioning to a flexbox-based layout for the main application structure. This introduces a `ScrollContainerCtx` to provide access to the main scrollable element, allowing child components like `FileTree` and `RevealManager` to perform accurate scrolling and positioning relative to the container instead of the window. refactor: set JournalPanel height to full --- src/App.tsx | 114 ++++++++++++++++-------- src/components/FileTree.tsx | 15 +++- src/components/RevealManager.tsx | 14 ++- src/components/Sidebar.tsx | 4 +- src/components/journal/JournalPanel.tsx | 17 ++-- 5 files changed, 115 insertions(+), 49 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index e448854..4fc7c34 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,4 +1,11 @@ -import { Component, createEffect, createMemo, createSignal } from "solid-js"; +import { + Component, + createContext, + useContext, + createEffect, + createMemo, + createSignal, +} from "solid-js"; import { useLocation } from "@solidjs/router"; import { useJournalStream } from "./components/stores/journalStream"; @@ -15,6 +22,23 @@ import { import { generateToc, type FileNode, type TocNode } from "./data-loader"; import { JournalPanel } from "./components/journal"; +// --------------------------------------------------------------------------- +// Scroll container context – lets child components (FileTree, RevealManager) +// find the correct scrollable element instead of relying on window.scroll +// --------------------------------------------------------------------------- + +const ScrollContainerCtx = createContext<() => HTMLElement | undefined>(); + +/** Access the main content scroll container from any descendant. */ +export function useScrollContainer(): () => HTMLElement | undefined { + const ctx = useContext(ScrollContainerCtx); + return ctx ?? (() => undefined); +} + +// --------------------------------------------------------------------------- +// App +// --------------------------------------------------------------------------- + const App: Component = () => { const location = useLocation(); const stream = useJournalStream(); @@ -30,6 +54,8 @@ const App: Component = () => { >({}); const [tocKey, setTocKey] = createSignal(0); + let mainRef!: HTMLElement; + const loadToc = async () => { const toc = await generateToc(); setFileTree(toc.fileTree); @@ -58,23 +84,10 @@ const App: Component = () => { }); return ( -
- {/* 桌面端固定侧边栏 */} - setIsDataSourceOpen(true)} - /> - {/* 移动端抽屉式侧边栏 */} - setIsSidebarOpen(false)} - fileTree={fileTree()} - pathHeadings={pathHeadings()} - onDataSourceOpen={() => setIsDataSourceOpen(true)} - /> -
-
+
+ {/* Header */} +
+
{/* 仅在移动端显示菜单按钮 */}
- {/* fill the rest of the space */} -
-
-
- -
-
+ + {/* 移动端抽屉式侧边栏 (overlay) */} + setIsSidebarOpen(false)} + fileTree={fileTree()} + pathHeadings={pathHeadings()} + onDataSourceOpen={() => setIsDataSourceOpen(true)} + /> + + {/* Body: sidebar + main + journal */} +
+ {/* 桌面端固定侧边栏 (flex child) */} + setIsDataSourceOpen(true)} + /> + + {/* Main content — the scrollable region */} + mainRef}> +
+
+
+ +
+
+
+
+ + {/* Journal panel wrapper — on desktop: flex child with width transition; + on mobile: passes through to JournalPanel's own overlay */} +
+ setIsJournalOpen(false)} + /> +
+ setIsDocOpen(false)} /> setIsDataSourceOpen(false)} onSourceChanged={handleSourceChanged} /> - setIsJournalOpen(false)} - />
); }; diff --git a/src/components/FileTree.tsx b/src/components/FileTree.tsx index d8a083f..df0e96f 100644 --- a/src/components/FileTree.tsx +++ b/src/components/FileTree.tsx @@ -1,6 +1,7 @@ import { Component, createMemo, createSignal, Show } from "solid-js"; import { type FileNode, type TocNode } from "../data-loader"; import { useNavigateWithParams } from "./useNavigateWithParams"; +import { useScrollContainer } from "../App"; /** * 检查当前文件路径是否在文件夹内 @@ -98,6 +99,8 @@ export const HeadingNode: Component<{ setIsExpanded(!isExpanded()); } }; + const scrollContainer = useScrollContainer(); + const handleClick = (e: MouseEvent) => { e.preventDefault(); navigate(href); @@ -105,10 +108,18 @@ export const HeadingNode: Component<{ requestAnimationFrame(() => { const element = document.getElementById(anchor); if (element) { + const scroller = scrollContainer(); const navBarHeight = 80; const elementPosition = element.getBoundingClientRect().top; - const offsetPosition = window.scrollY + elementPosition - navBarHeight; - window.scrollTo({ top: offsetPosition, behavior: "instant" }); + const containerTop = scroller?.getBoundingClientRect().top ?? 0; + const relativePosition = elementPosition - containerTop; + const currentScroll = scroller?.scrollTop ?? window.scrollY; + const offsetPosition = currentScroll + relativePosition - navBarHeight; + if (scroller) { + scroller.scrollTo({ top: offsetPosition, behavior: "instant" }); + } else { + window.scrollTo({ top: offsetPosition, behavior: "instant" }); + } } }); }; diff --git a/src/components/RevealManager.tsx b/src/components/RevealManager.tsx index ad8ecea..e5e8857 100644 --- a/src/components/RevealManager.tsx +++ b/src/components/RevealManager.tsx @@ -18,6 +18,7 @@ import { import { Portal } from "solid-js/web"; import { useLocation } from "@solidjs/router"; import { useArticleDom } from "./Article"; +import { useScrollContainer } from "../App"; import { journalStreamState } from "./stores/journalStream"; import { useJournalCompletions } from "./journal/completions"; import { @@ -75,6 +76,7 @@ function hide() { export const RevealManager: Component = () => { const contentDom = useArticleDom(); + const scrollContainer = useScrollContainer(); const location = useLocation(); const comp = useJournalCompletions(); @@ -179,8 +181,16 @@ export const RevealManager: Component = () => {