dark mode pulled

This commit is contained in:
Michael Freno
2025-12-21 19:35:40 -05:00
parent c6ff41b0cf
commit 3832269a96
6 changed files with 168 additions and 74 deletions

View File

@@ -14,6 +14,7 @@ import { TerminalSplash } from "./components/TerminalSplash";
import { MetaProvider } from "@solidjs/meta"; import { MetaProvider } from "@solidjs/meta";
import ErrorBoundaryFallback from "./components/ErrorBoundaryFallback"; import ErrorBoundaryFallback from "./components/ErrorBoundaryFallback";
import { BarsProvider, useBars } from "./context/bars"; import { BarsProvider, useBars } from "./context/bars";
import { DarkModeProvider } from "./context/darkMode";
import { createWindowWidth, isMobile } from "~/lib/resize-utils"; import { createWindowWidth, isMobile } from "~/lib/resize-utils";
function AppLayout(props: { children: any }) { function AppLayout(props: { children: any }) {
@@ -29,31 +30,36 @@ function AppLayout(props: { children: any }) {
barsInitialized barsInitialized
} = useBars(); } = useBars();
const windowWidth = createWindowWidth();
let lastScrollY = 0; let lastScrollY = 0;
const SCROLL_THRESHOLD = 100; const SCROLL_THRESHOLD = 100;
createEffect(() => { // Use onMount to avoid hydration issues - window operations are client-only
const handleResize = () => { onMount(() => {
const currentIsMobile = isMobile(windowWidth()); const windowWidth = createWindowWidth();
// Show bars when switching to desktop createEffect(() => {
if (!currentIsMobile) { const handleResize = () => {
setLeftBarVisible(true); const currentIsMobile = isMobile(windowWidth());
setRightBarVisible(true);
}
// On mobile, leftBarSize() is always 0 (overlay mode) // Show bars when switching to desktop
const newWidth = window.innerWidth - leftBarSize() - rightBarSize(); if (!currentIsMobile) {
setCenterWidth(newWidth); setLeftBarVisible(true);
}; setRightBarVisible(true);
}
// Call immediately and whenever dependencies change // On mobile, leftBarSize() is always 0 (overlay mode)
handleResize(); const newWidth = window.innerWidth - leftBarSize() - rightBarSize();
setCenterWidth(newWidth);
};
// Call immediately and whenever dependencies change
handleResize();
});
}); });
// Recalculate when bar sizes change (visibility or actual resize) // Recalculate when bar sizes change (visibility or actual resize)
createEffect(() => { createEffect(() => {
if (typeof window === "undefined") return;
// On mobile, leftBarSize() is always 0 (overlay mode) // On mobile, leftBarSize() is always 0 (overlay mode)
const newWidth = window.innerWidth - leftBarSize() - rightBarSize(); const newWidth = window.innerWidth - leftBarSize() - rightBarSize();
setCenterWidth(newWidth); setCenterWidth(newWidth);
@@ -61,6 +67,8 @@ function AppLayout(props: { children: any }) {
// Auto-hide on scroll (mobile only) // Auto-hide on scroll (mobile only)
onMount(() => { onMount(() => {
const windowWidth = createWindowWidth();
const handleScroll = () => { const handleScroll = () => {
const currentScrollY = window.scrollY; const currentScrollY = window.scrollY;
const currentIsMobile = isMobile(windowWidth()); const currentIsMobile = isMobile(windowWidth());
@@ -84,6 +92,8 @@ function AppLayout(props: { children: any }) {
// ESC key to close sidebars on mobile // ESC key to close sidebars on mobile
onMount(() => { onMount(() => {
const windowWidth = createWindowWidth();
const handleKeyDown = (e: KeyboardEvent) => { const handleKeyDown = (e: KeyboardEvent) => {
const currentIsMobile = isMobile(windowWidth()); const currentIsMobile = isMobile(windowWidth());
@@ -106,6 +116,7 @@ function AppLayout(props: { children: any }) {
// Global swipe gestures to reveal/hide bars // Global swipe gestures to reveal/hide bars
onMount(() => { onMount(() => {
const windowWidth = createWindowWidth();
let touchStartX = 0; let touchStartX = 0;
let touchStartY = 0; let touchStartY = 0;
const SWIPE_THRESHOLD = 100; const SWIPE_THRESHOLD = 100;
@@ -158,7 +169,8 @@ function AppLayout(props: { children: any }) {
}); });
const handleCenterTapRelease = (e: MouseEvent | TouchEvent) => { const handleCenterTapRelease = (e: MouseEvent | TouchEvent) => {
const currentIsMobile = isMobile(windowWidth()); if (typeof window === "undefined") return;
const currentIsMobile = window.innerWidth < 768;
// Only hide left bar on mobile when it's visible // Only hide left bar on mobile when it's visible
if (currentIsMobile && leftBarVisible()) { if (currentIsMobile && leftBarVisible()) {
@@ -204,11 +216,13 @@ export default function App() {
<ErrorBoundaryFallback error={error} reset={reset} /> <ErrorBoundaryFallback error={error} reset={reset} />
)} )}
> >
<BarsProvider> <DarkModeProvider>
<Router root={(props) => <AppLayout>{props.children}</AppLayout>}> <BarsProvider>
<FileRoutes /> <Router root={(props) => <AppLayout>{props.children}</AppLayout>}>
</Router> <FileRoutes />
</BarsProvider> </Router>
</BarsProvider>
</DarkModeProvider>
</ErrorBoundary> </ErrorBoundary>
</MetaProvider> </MetaProvider>
); );

View File

@@ -1,39 +1,11 @@
import { createSignal, onMount, Show } from "solid-js"; import { Show } from "solid-js";
import MoonIcon from "./icons/MoonIcon"; import MoonIcon from "./icons/MoonIcon";
import SunIcon from "./icons/SunIcon"; import SunIcon from "./icons/SunIcon";
import { Typewriter } from "./Typewriter"; import { Typewriter } from "./Typewriter";
import { useDarkMode } from "~/context/darkMode";
export function DarkModeToggle() { export function DarkModeToggle() {
const [isDark, setIsDark] = createSignal(false); const { isDark, toggleDarkMode } = useDarkMode();
onMount(() => {
const prefersDark = window.matchMedia(
"(prefers-color-scheme: dark)"
).matches;
if (prefersDark) {
setIsDark(true);
document.documentElement.classList.add("dark");
document.documentElement.classList.remove("light");
} else {
setIsDark(false);
document.documentElement.classList.add("light");
document.documentElement.classList.remove("dark");
}
});
const toggleDarkMode = () => {
const newDarkMode = !isDark();
setIsDark(newDarkMode);
if (newDarkMode) {
document.documentElement.classList.add("dark");
document.documentElement.classList.remove("light");
} else {
document.documentElement.classList.add("light");
document.documentElement.classList.remove("dark");
}
};
return ( return (
<button <button

View File

@@ -1,6 +1,12 @@
import { Accessor, createContext, useContext, createMemo } from "solid-js"; import {
Accessor,
createContext,
useContext,
createMemo,
onMount
} from "solid-js";
import { createSignal } from "solid-js"; import { createSignal } from "solid-js";
import { createWindowWidth, isMobile } from "~/lib/resize-utils"; import { isMobile, MOBILE_BREAKPOINT } from "~/lib/resize-utils";
const BarsContext = createContext<{ const BarsContext = createContext<{
leftBarSize: Accessor<number>; leftBarSize: Accessor<number>;
@@ -39,15 +45,41 @@ export function BarsProvider(props: { children: any }) {
const [_rightBarNaturalSize, _setRightBarNaturalSize] = createSignal(0); const [_rightBarNaturalSize, _setRightBarNaturalSize] = createSignal(0);
const [syncedBarSize, setSyncedBarSize] = createSignal(0); const [syncedBarSize, setSyncedBarSize] = createSignal(0);
const [centerWidth, setCenterWidth] = createSignal(0); const [centerWidth, setCenterWidth] = createSignal(0);
const windowWidth = createWindowWidth(); const [windowWidth, setWindowWidth] = createSignal(
const initialIsMobile = isMobile(windowWidth()); typeof window !== "undefined" ? window.innerWidth : 1024
const [leftBarVisible, setLeftBarVisible] = createSignal(!initialIsMobile); );
const [leftBarVisible, setLeftBarVisible] = createSignal(true);
const [rightBarVisible, setRightBarVisible] = createSignal(true); const [rightBarVisible, setRightBarVisible] = createSignal(true);
const [barsInitialized, setBarsInitialized] = createSignal(false); const [barsInitialized, setBarsInitialized] = createSignal(false);
let leftBarSized = false; let leftBarSized = false;
let rightBarSized = false; let rightBarSized = false;
// Setup window width tracking and initial mobile detection on client only
onMount(() => {
// Immediately sync to actual window width
setWindowWidth(window.innerWidth);
const initialIsMobile = isMobile(window.innerWidth);
setLeftBarVisible(!initialIsMobile);
// Initialize immediately on mobile if left bar starts hidden
if (initialIsMobile && !leftBarVisible()) {
leftBarSized = true;
checkAndSync();
}
// Setup resize listener
const handleResize = () => {
setWindowWidth(window.innerWidth);
};
window.addEventListener("resize", handleResize);
return () => {
window.removeEventListener("resize", handleResize);
};
});
const wrappedSetLeftBarSize = (size: number) => { const wrappedSetLeftBarSize = (size: number) => {
if (!barsInitialized()) { if (!barsInitialized()) {
// Before initialization, capture natural size // Before initialization, capture natural size
@@ -62,11 +94,16 @@ export function BarsProvider(props: { children: any }) {
} }
}; };
// Initialize immediately on mobile if left bar starts hidden const checkAndSync = () => {
if (initialIsMobile && !leftBarVisible()) { const currentIsMobile = isMobile(windowWidth());
// Skip waiting for left bar size on mobile when it starts hidden const bothBarsReady = leftBarSized && (currentIsMobile || rightBarSized);
leftBarSized = true;
} if (bothBarsReady) {
const maxWidth = Math.max(_leftBarNaturalSize(), _rightBarNaturalSize());
setSyncedBarSize(maxWidth);
setBarsInitialized(true);
}
};
const wrappedSetRightBarSize = (size: number) => { const wrappedSetRightBarSize = (size: number) => {
if (!barsInitialized()) { if (!barsInitialized()) {
@@ -82,17 +119,6 @@ export function BarsProvider(props: { children: any }) {
} }
}; };
const checkAndSync = () => {
const currentIsMobile = isMobile(windowWidth());
const bothBarsReady = leftBarSized && (currentIsMobile || rightBarSized);
if (bothBarsReady) {
const maxWidth = Math.max(_leftBarNaturalSize(), _rightBarNaturalSize());
setSyncedBarSize(maxWidth);
setBarsInitialized(true);
}
};
const leftBarSize = createMemo(() => { const leftBarSize = createMemo(() => {
// Return 0 if hidden (natural size is 0), otherwise return synced size when initialized // Return 0 if hidden (natural size is 0), otherwise return synced size when initialized
const naturalSize = _leftBarNaturalSize(); const naturalSize = _leftBarNaturalSize();

79
src/context/darkMode.tsx Normal file
View File

@@ -0,0 +1,79 @@
import {
createContext,
useContext,
createEffect,
onMount,
onCleanup,
Accessor,
ParentComponent
} from "solid-js";
import { createSignal } from "solid-js";
interface DarkModeContextType {
isDark: Accessor<boolean>;
toggleDarkMode: () => void;
setDarkMode: (dark: boolean) => void;
}
const DarkModeContext = createContext<DarkModeContextType>({
isDark: () => false,
toggleDarkMode: () => {},
setDarkMode: () => {}
});
export function useDarkMode() {
const context = useContext(DarkModeContext);
return context;
}
export const DarkModeProvider: ParentComponent = (props) => {
const [isDark, setIsDark] = createSignal(false);
onMount(() => {
// Check system preference
const mediaQuery = window.matchMedia("(prefers-color-scheme: dark)");
setIsDark(mediaQuery.matches);
// Listen for system theme changes
const handleChange = (e: MediaQueryListEvent) => {
setIsDark(e.matches);
};
mediaQuery.addEventListener("change", handleChange);
onCleanup(() => {
mediaQuery.removeEventListener("change", handleChange);
});
});
// Reactively update DOM when isDark changes
createEffect(() => {
if (isDark()) {
document.documentElement.classList.add("dark");
document.documentElement.classList.remove("light");
} else {
document.documentElement.classList.add("light");
document.documentElement.classList.remove("dark");
}
});
const toggleDarkMode = () => {
setIsDark(!isDark());
};
const setDarkMode = (dark: boolean) => {
setIsDark(dark);
};
return (
<DarkModeContext.Provider
value={{
isDark,
toggleDarkMode,
setDarkMode
}}
>
{props.children}
</DarkModeContext.Provider>
);
};

View File

@@ -12,6 +12,9 @@ export function createWindowWidth(debounceMs?: number): Accessor<number> {
const [width, setWidth] = createSignal(initialWidth); const [width, setWidth] = createSignal(initialWidth);
onMount(() => { onMount(() => {
// Sync to actual client width immediately on mount to avoid hydration mismatch
setWidth(window.innerWidth);
let timeoutId: ReturnType<typeof setTimeout> | undefined; let timeoutId: ReturnType<typeof setTimeout> | undefined;
const handleResize = () => { const handleResize = () => {

View File

@@ -148,7 +148,7 @@ export default function Home() {
</div> </div>
</div> </div>
</div> </div>
<Typewriter speed={160} class="max-w-3/4 pt-8 md:max-w-1/2"> <Typewriter speed={120} class="max-w-3/4 pt-8 md:max-w-1/2">
And if you love the color schemes of this site And if you love the color schemes of this site
<div class="mx-auto w-fit"> <div class="mx-auto w-fit">
<DarkModeToggle /> <DarkModeToggle />