dark mode pulled
This commit is contained in:
18
src/app.tsx
18
src/app.tsx
@@ -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,10 +30,13 @@ 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;
|
||||||
|
|
||||||
|
// Use onMount to avoid hydration issues - window operations are client-only
|
||||||
|
onMount(() => {
|
||||||
|
const windowWidth = createWindowWidth();
|
||||||
|
|
||||||
createEffect(() => {
|
createEffect(() => {
|
||||||
const handleResize = () => {
|
const handleResize = () => {
|
||||||
const currentIsMobile = isMobile(windowWidth());
|
const currentIsMobile = isMobile(windowWidth());
|
||||||
@@ -51,9 +55,11 @@ function AppLayout(props: { children: any }) {
|
|||||||
// Call immediately and whenever dependencies change
|
// Call immediately and whenever dependencies change
|
||||||
handleResize();
|
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} />
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
|
<DarkModeProvider>
|
||||||
<BarsProvider>
|
<BarsProvider>
|
||||||
<Router root={(props) => <AppLayout>{props.children}</AppLayout>}>
|
<Router root={(props) => <AppLayout>{props.children}</AppLayout>}>
|
||||||
<FileRoutes />
|
<FileRoutes />
|
||||||
</Router>
|
</Router>
|
||||||
</BarsProvider>
|
</BarsProvider>
|
||||||
|
</DarkModeProvider>
|
||||||
</ErrorBoundary>
|
</ErrorBoundary>
|
||||||
</MetaProvider>
|
</MetaProvider>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
79
src/context/darkMode.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -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 = () => {
|
||||||
|
|||||||
@@ -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 />
|
||||||
|
|||||||
Reference in New Issue
Block a user