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 ErrorBoundaryFallback from "./components/ErrorBoundaryFallback";
import { BarsProvider, useBars } from "./context/bars";
import { DarkModeProvider } from "./context/darkMode";
import { createWindowWidth, isMobile } from "~/lib/resize-utils";
function AppLayout(props: { children: any }) {
@@ -29,31 +30,36 @@ function AppLayout(props: { children: any }) {
barsInitialized
} = useBars();
const windowWidth = createWindowWidth();
let lastScrollY = 0;
const SCROLL_THRESHOLD = 100;
createEffect(() => {
const handleResize = () => {
const currentIsMobile = isMobile(windowWidth());
// Use onMount to avoid hydration issues - window operations are client-only
onMount(() => {
const windowWidth = createWindowWidth();
// Show bars when switching to desktop
if (!currentIsMobile) {
setLeftBarVisible(true);
setRightBarVisible(true);
}
createEffect(() => {
const handleResize = () => {
const currentIsMobile = isMobile(windowWidth());
// On mobile, leftBarSize() is always 0 (overlay mode)
const newWidth = window.innerWidth - leftBarSize() - rightBarSize();
setCenterWidth(newWidth);
};
// Show bars when switching to desktop
if (!currentIsMobile) {
setLeftBarVisible(true);
setRightBarVisible(true);
}
// Call immediately and whenever dependencies change
handleResize();
// On mobile, leftBarSize() is always 0 (overlay mode)
const newWidth = window.innerWidth - leftBarSize() - rightBarSize();
setCenterWidth(newWidth);
};
// Call immediately and whenever dependencies change
handleResize();
});
});
// Recalculate when bar sizes change (visibility or actual resize)
createEffect(() => {
if (typeof window === "undefined") return;
// On mobile, leftBarSize() is always 0 (overlay mode)
const newWidth = window.innerWidth - leftBarSize() - rightBarSize();
setCenterWidth(newWidth);
@@ -61,6 +67,8 @@ function AppLayout(props: { children: any }) {
// Auto-hide on scroll (mobile only)
onMount(() => {
const windowWidth = createWindowWidth();
const handleScroll = () => {
const currentScrollY = window.scrollY;
const currentIsMobile = isMobile(windowWidth());
@@ -84,6 +92,8 @@ function AppLayout(props: { children: any }) {
// ESC key to close sidebars on mobile
onMount(() => {
const windowWidth = createWindowWidth();
const handleKeyDown = (e: KeyboardEvent) => {
const currentIsMobile = isMobile(windowWidth());
@@ -106,6 +116,7 @@ function AppLayout(props: { children: any }) {
// Global swipe gestures to reveal/hide bars
onMount(() => {
const windowWidth = createWindowWidth();
let touchStartX = 0;
let touchStartY = 0;
const SWIPE_THRESHOLD = 100;
@@ -158,7 +169,8 @@ function AppLayout(props: { children: any }) {
});
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
if (currentIsMobile && leftBarVisible()) {
@@ -204,11 +216,13 @@ export default function App() {
<ErrorBoundaryFallback error={error} reset={reset} />
)}
>
<BarsProvider>
<Router root={(props) => <AppLayout>{props.children}</AppLayout>}>
<FileRoutes />
</Router>
</BarsProvider>
<DarkModeProvider>
<BarsProvider>
<Router root={(props) => <AppLayout>{props.children}</AppLayout>}>
<FileRoutes />
</Router>
</BarsProvider>
</DarkModeProvider>
</ErrorBoundary>
</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 SunIcon from "./icons/SunIcon";
import { Typewriter } from "./Typewriter";
import { useDarkMode } from "~/context/darkMode";
export function DarkModeToggle() {
const [isDark, setIsDark] = createSignal(false);
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");
}
};
const { isDark, toggleDarkMode } = useDarkMode();
return (
<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 { createWindowWidth, isMobile } from "~/lib/resize-utils";
import { isMobile, MOBILE_BREAKPOINT } from "~/lib/resize-utils";
const BarsContext = createContext<{
leftBarSize: Accessor<number>;
@@ -39,15 +45,41 @@ export function BarsProvider(props: { children: any }) {
const [_rightBarNaturalSize, _setRightBarNaturalSize] = createSignal(0);
const [syncedBarSize, setSyncedBarSize] = createSignal(0);
const [centerWidth, setCenterWidth] = createSignal(0);
const windowWidth = createWindowWidth();
const initialIsMobile = isMobile(windowWidth());
const [leftBarVisible, setLeftBarVisible] = createSignal(!initialIsMobile);
const [windowWidth, setWindowWidth] = createSignal(
typeof window !== "undefined" ? window.innerWidth : 1024
);
const [leftBarVisible, setLeftBarVisible] = createSignal(true);
const [rightBarVisible, setRightBarVisible] = createSignal(true);
const [barsInitialized, setBarsInitialized] = createSignal(false);
let leftBarSized = 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) => {
if (!barsInitialized()) {
// Before initialization, capture natural size
@@ -62,11 +94,16 @@ export function BarsProvider(props: { children: any }) {
}
};
// Initialize immediately on mobile if left bar starts hidden
if (initialIsMobile && !leftBarVisible()) {
// Skip waiting for left bar size on mobile when it starts hidden
leftBarSized = true;
}
const checkAndSync = () => {
const currentIsMobile = isMobile(windowWidth());
const bothBarsReady = leftBarSized && (currentIsMobile || rightBarSized);
if (bothBarsReady) {
const maxWidth = Math.max(_leftBarNaturalSize(), _rightBarNaturalSize());
setSyncedBarSize(maxWidth);
setBarsInitialized(true);
}
};
const wrappedSetRightBarSize = (size: number) => {
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(() => {
// Return 0 if hidden (natural size is 0), otherwise return synced size when initialized
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);
onMount(() => {
// Sync to actual client width immediately on mount to avoid hydration mismatch
setWidth(window.innerWidth);
let timeoutId: ReturnType<typeof setTimeout> | undefined;
const handleResize = () => {

View File

@@ -148,7 +148,7 @@ export default function Home() {
</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
<div class="mx-auto w-fit">
<DarkModeToggle />