diff --git a/src/default-theme/Layout.module.css b/src/default-theme/Layout.module.css index d0a0483..db67e2b 100644 --- a/src/default-theme/Layout.module.css +++ b/src/default-theme/Layout.module.css @@ -49,6 +49,10 @@ main { grid-template-columns: 1fr; } + aside.sidenav { + display: none; + } + .sidenav { position: fixed; left: 0; diff --git a/src/default-theme/Layout.tsx b/src/default-theme/Layout.tsx index 0753a70..8514d84 100644 --- a/src/default-theme/Layout.tsx +++ b/src/default-theme/Layout.tsx @@ -29,7 +29,6 @@ import { useDefaultThemeState, } from "./context.jsx"; import { defaultThemeComponents } from "./default-components.js"; -import { mobileLayout } from "./globals.js"; import { usePace } from "./pace.js"; import { useRouteConfig } from "./utils.js"; @@ -60,7 +59,8 @@ export default (props: ParentProps) => { function Layout(props: ParentProps) { const { Header, Article, Link, ProjectSelector } = useDefaultThemeComponents(); - const { sidebarOpen, setSidebarOpen, frontmatter } = useDefaultThemeState(); + const { sidebarOpen, setSidebarOpen, frontmatter, mobileLayout } = + useDefaultThemeState(); const config = useRouteConfig(); const sidebar = useSidebar(); diff --git a/src/default-theme/components/Article.module.css b/src/default-theme/components/Article.module.css index f48baad..b597135 100644 --- a/src/default-theme/components/Article.module.css +++ b/src/default-theme/components/Article.module.css @@ -45,6 +45,10 @@ .article { margin: 2rem 1.25rem; } + + .aside { + display: none; + } } .info { diff --git a/src/default-theme/components/Article.tsx b/src/default-theme/components/Article.tsx index 73f5841..cc45acb 100644 --- a/src/default-theme/components/Article.tsx +++ b/src/default-theme/components/Article.tsx @@ -10,7 +10,6 @@ import { useDefaultThemeState, } from "../context.jsx"; import type { RelativePageConfig } from "../frontmatter.js"; -import { mobileLayout } from "../globals.js"; import { useSolidBaseContext, useThemeText } from "../utils.js"; import styles from "./Article.module.css"; @@ -18,7 +17,7 @@ import styles from "./Article.module.css"; export default function Article(props: ParentProps) { const { config } = useSolidBaseContext(); const text = useThemeText(); - const { frontmatter } = useDefaultThemeState(); + const { frontmatter, mobileLayout } = useDefaultThemeState(); const { TableOfContents, Link, LastUpdated, Footer, Hero, Features } = useDefaultThemeComponents(); diff --git a/src/default-theme/context.tsx b/src/default-theme/context.tsx index 24e37a8..a175873 100644 --- a/src/default-theme/context.tsx +++ b/src/default-theme/context.tsx @@ -1,4 +1,5 @@ import { createContextProvider } from "@solid-primitives/context"; +import { createMediaQuery } from "@solid-primitives/media"; import { createSignal } from "solid-js"; import type { ThemeComponents } from "./default-components.js"; @@ -38,12 +39,14 @@ export function useDefaultThemeComponents() { const [DefaultThemeStateProvider, useDefaultThemeStateContext] = createContextProvider(() => { + const mobileLayout = createMediaQuery("(max-width: 1100px)", false); const [sidebarOpen, setSidebarOpen] = createSignal(false); const [tocOpen, setTocOpen] = createSignal(false); const [navOpen, setNavOpen] = createSignal(false); const frontmatter = useDefaultThemeFrontmatter(); return { + mobileLayout, sidebarOpen, setSidebarOpen, tocOpen, diff --git a/src/default-theme/globals.ts b/src/default-theme/globals.ts index e76f737..cd2ae4e 100644 --- a/src/default-theme/globals.ts +++ b/src/default-theme/globals.ts @@ -1,16 +1,3 @@ -import { createMediaQuery } from "@solid-primitives/media"; -import { createEffect, createRoot, createSignal, on, onMount } from "solid-js"; +import { useDefaultThemeState } from "./context.jsx"; -const [_mobileLayout, setMobileLayout] = createSignal(false); - -onMount(() => { - const query = createMediaQuery("(max-width: 1100px)"); - - createRoot(() => { - createEffect(on(query, (q) => setMobileLayout(q), { defer: true })); - }); - - setTimeout(() => setMobileLayout(query())); -}); - -export const mobileLayout = _mobileLayout; +export const mobileLayout = () => useDefaultThemeState().mobileLayout(); diff --git a/tests/default-theme/context.test.ts b/tests/default-theme/context.test.ts new file mode 100644 index 0000000..3c3b79f --- /dev/null +++ b/tests/default-theme/context.test.ts @@ -0,0 +1,66 @@ +import { createComponent, createRoot, getOwner } from "solid-js"; +import { afterEach, describe, expect, it, vi } from "vitest"; + +const createMediaQuery = vi.fn(() => { + expect(getOwner()).not.toBeNull(); + return () => false; +}); + +vi.mock("@solid-primitives/media", () => ({ createMediaQuery })); + +vi.mock("../../src/default-theme/frontmatter.js", () => ({ + useDefaultThemeFrontmatter: () => () => undefined, +})); + +describe("DefaultThemeStateProvider", () => { + afterEach(() => { + createMediaQuery.mockClear(); + }); + + it("owns responsive layout state with a desktop hydration fallback", async () => { + const { DefaultThemeStateProvider, useDefaultThemeState } = await import( + "../../src/default-theme/context.tsx" + ); + + createRoot((dispose) => { + let state: ReturnType | undefined; + + createComponent(DefaultThemeStateProvider, { + get children() { + state = useDefaultThemeState(); + return null; + }, + }); + + expect(createMediaQuery).toHaveBeenCalledWith( + "(max-width: 1100px)", + false, + ); + expect(state?.mobileLayout()).toBe(false); + dispose(); + }); + }); + + it("keeps the mobile layout deep import connected to provider state", async () => { + const [{ DefaultThemeStateProvider }, { mobileLayout }] = await Promise.all( + [ + import("../../src/default-theme/context.tsx"), + import("../../src/default-theme/globals.ts"), + ], + ); + + createRoot((dispose) => { + let isMobile: boolean | undefined; + + createComponent(DefaultThemeStateProvider, { + get children() { + isMobile = mobileLayout(); + return null; + }, + }); + + expect(isMobile).toBe(false); + dispose(); + }); + }); +});