fix resizing bugs (conflicting handlers)

This commit is contained in:
Michael Freno
2025-12-21 19:16:20 -05:00
parent 2a852f43b2
commit c6ff41b0cf
6 changed files with 150 additions and 110 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 { createWindowWidth, isMobile } from "~/lib/resize-utils";
function AppLayout(props: { children: any }) { function AppLayout(props: { children: any }) {
const { const {
@@ -28,15 +29,16 @@ 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(() => { createEffect(() => {
const handleResize = () => { const handleResize = () => {
const isMobile = window.innerWidth < 768; // md breakpoint const currentIsMobile = isMobile(windowWidth());
// Show bars when switching to desktop // Show bars when switching to desktop
if (!isMobile) { if (!currentIsMobile) {
setLeftBarVisible(true); setLeftBarVisible(true);
setRightBarVisible(true); setRightBarVisible(true);
} }
@@ -48,10 +50,6 @@ function AppLayout(props: { children: any }) {
// Call immediately and whenever dependencies change // Call immediately and whenever dependencies change
handleResize(); handleResize();
window.addEventListener("resize", handleResize);
return () => window.removeEventListener("resize", handleResize);
}); });
// Recalculate when bar sizes change (visibility or actual resize) // Recalculate when bar sizes change (visibility or actual resize)
@@ -65,9 +63,9 @@ function AppLayout(props: { children: any }) {
onMount(() => { onMount(() => {
const handleScroll = () => { const handleScroll = () => {
const currentScrollY = window.scrollY; const currentScrollY = window.scrollY;
const isMobile = window.innerWidth < 768; // md breakpoint const currentIsMobile = isMobile(windowWidth());
if (isMobile && currentScrollY > SCROLL_THRESHOLD) { if (currentIsMobile && currentScrollY > SCROLL_THRESHOLD) {
// Scrolling down past threshold - hide left bar on mobile // Scrolling down past threshold - hide left bar on mobile
if (currentScrollY > lastScrollY) { if (currentScrollY > lastScrollY) {
setLeftBarVisible(false); setLeftBarVisible(false);
@@ -87,9 +85,9 @@ function AppLayout(props: { children: any }) {
// ESC key to close sidebars on mobile // ESC key to close sidebars on mobile
onMount(() => { onMount(() => {
const handleKeyDown = (e: KeyboardEvent) => { const handleKeyDown = (e: KeyboardEvent) => {
const isMobile = window.innerWidth < 768; // md breakpoint const currentIsMobile = isMobile(windowWidth());
if (e.key === "Escape" && isMobile) { if (e.key === "Escape" && currentIsMobile) {
if (leftBarVisible()) { if (leftBarVisible()) {
setLeftBarVisible(false); setLeftBarVisible(false);
} }
@@ -122,12 +120,12 @@ function AppLayout(props: { children: any }) {
const touchEndY = e.changedTouches[0].clientY; const touchEndY = e.changedTouches[0].clientY;
const deltaX = touchEndX - touchStartX; const deltaX = touchEndX - touchStartX;
const deltaY = touchEndY - touchStartY; const deltaY = touchEndY - touchStartY;
const isMobile = window.innerWidth < 768; // md breakpoint const currentIsMobile = isMobile(windowWidth());
// Only trigger if horizontal swipe is dominant // Only trigger if horizontal swipe is dominant
if (Math.abs(deltaX) > Math.abs(deltaY)) { if (Math.abs(deltaX) > Math.abs(deltaY)) {
// Mobile: Only left bar // Mobile: Only left bar
if (isMobile) { if (currentIsMobile) {
// Swipe right anywhere - reveal left bar // Swipe right anywhere - reveal left bar
if (deltaX > SWIPE_THRESHOLD) { if (deltaX > SWIPE_THRESHOLD) {
setLeftBarVisible(true); setLeftBarVisible(true);
@@ -160,10 +158,10 @@ function AppLayout(props: { children: any }) {
}); });
const handleCenterTapRelease = (e: MouseEvent | TouchEvent) => { const handleCenterTapRelease = (e: MouseEvent | TouchEvent) => {
const isMobile = window.innerWidth < 768; const currentIsMobile = isMobile(windowWidth());
// Only hide left bar on mobile when it's visible // Only hide left bar on mobile when it's visible
if (isMobile && leftBarVisible()) { if (currentIsMobile && leftBarVisible()) {
const target = e.target as HTMLElement; const target = e.target as HTMLElement;
const isInteractive = target.closest( const isInteractive = target.closest(
"a, button, input, select, textarea, [onclick]" "a, button, input, select, textarea, [onclick]"

View File

@@ -1,5 +1,15 @@
import { createSignal, createEffect, onMount, onCleanup, children as resolveChildren, type ParentComponent, createMemo, For } from "solid-js"; import {
createSignal,
createEffect,
onMount,
onCleanup,
children as resolveChildren,
type ParentComponent,
createMemo,
For
} from "solid-js";
import { animate } from "motion"; import { animate } from "motion";
import { createWindowWidth } from "~/lib/resize-utils";
type ParallaxBackground = { type ParallaxBackground = {
imageSet: { [key: number]: string }; imageSet: { [key: number]: string };
@@ -22,10 +32,16 @@ type ParallaxLayerProps = {
function ParallaxLayer(props: ParallaxLayerProps) { function ParallaxLayer(props: ParallaxLayerProps) {
let containerRef: HTMLDivElement | undefined; let containerRef: HTMLDivElement | undefined;
const layerDepthFactor = createMemo(() => props.layer / (Object.keys(props.caveParallax.imageSet).length - 1)); const layerDepthFactor = createMemo(
const layerVerticalOffset = createMemo(() => props.verticalOffsetPixels * layerDepthFactor()); () => props.layer / (Object.keys(props.caveParallax.imageSet).length - 1)
);
const layerVerticalOffset = createMemo(
() => props.verticalOffsetPixels * layerDepthFactor()
);
const speed = createMemo(() => (120 - props.layer * 10) * 1000); const speed = createMemo(() => (120 - props.layer * 10) * 1000);
const targetX = createMemo(() => props.direction * -props.caveParallax.size.width * props.imagesNeeded); const targetX = createMemo(
() => props.direction * -props.caveParallax.size.width * props.imagesNeeded
);
const containerStyle = createMemo(() => ({ const containerStyle = createMemo(() => ({
width: `${props.caveParallax.size.width * props.imagesNeeded * 3}px`, width: `${props.caveParallax.size.width * props.imagesNeeded * 3}px`,
@@ -33,7 +49,7 @@ function ParallaxLayer(props: ParallaxLayerProps) {
left: `${(props.dimensions.width - props.scaledWidth) / 2}px`, left: `${(props.dimensions.width - props.scaledWidth) / 2}px`,
top: `${(props.dimensions.height - props.scaledHeight) / 2 + layerVerticalOffset()}px`, top: `${(props.dimensions.height - props.scaledHeight) / 2 + layerVerticalOffset()}px`,
"transform-origin": "center center", "transform-origin": "center center",
"will-change": "transform", "will-change": "transform"
})); }));
// Set up animation when component mounts or when direction/speed changes // Set up animation when component mounts or when direction/speed changes
@@ -54,7 +70,7 @@ function ParallaxLayer(props: ParallaxLayerProps) {
{ {
duration, duration,
easing: "linear", easing: "linear",
repeat: Infinity, repeat: Infinity
} }
); );
@@ -68,7 +84,7 @@ function ParallaxLayer(props: ParallaxLayerProps) {
style={{ style={{
left: `${groupOffset * props.caveParallax.size.width * props.imagesNeeded}px`, left: `${groupOffset * props.caveParallax.size.width * props.imagesNeeded}px`,
width: `${props.caveParallax.size.width * props.imagesNeeded}px`, width: `${props.caveParallax.size.width * props.imagesNeeded}px`,
height: `${props.caveParallax.size.height}px`, height: `${props.caveParallax.size.height}px`
}} }}
> >
{Array.from({ length: props.imagesNeeded }).map((_, index) => ( {Array.from({ length: props.imagesNeeded }).map((_, index) => (
@@ -77,7 +93,7 @@ function ParallaxLayer(props: ParallaxLayerProps) {
style={{ style={{
width: `${props.caveParallax.size.width}px`, width: `${props.caveParallax.size.width}px`,
height: `${props.caveParallax.size.height}px`, height: `${props.caveParallax.size.height}px`,
left: `${index * props.caveParallax.size.width}px`, left: `${index * props.caveParallax.size.width}px`
}} }}
> >
<img <img
@@ -86,7 +102,12 @@ function ParallaxLayer(props: ParallaxLayerProps) {
width={props.caveParallax.size.width} width={props.caveParallax.size.width}
height={props.caveParallax.size.height} height={props.caveParallax.size.height}
style={{ "object-fit": "cover" }} style={{ "object-fit": "cover" }}
loading={props.layer > Object.keys(props.caveParallax.imageSet).length - 3 ? "eager" : "lazy"} loading={
props.layer >
Object.keys(props.caveParallax.imageSet).length - 3
? "eager"
: "lazy"
}
/> />
</div> </div>
))} ))}
@@ -95,11 +116,7 @@ function ParallaxLayer(props: ParallaxLayerProps) {
}); });
return ( return (
<div <div ref={containerRef} class="absolute" style={containerStyle()}>
ref={containerRef}
class="absolute"
style={containerStyle()}
>
{imageGroups()} {imageGroups()}
</div> </div>
); );
@@ -107,9 +124,17 @@ function ParallaxLayer(props: ParallaxLayerProps) {
const SimpleParallax: ParentComponent = (props) => { const SimpleParallax: ParentComponent = (props) => {
let containerRef: HTMLDivElement | undefined; let containerRef: HTMLDivElement | undefined;
const [dimensions, setDimensions] = createSignal({ width: 0, height: 0 }); const windowWidth = createWindowWidth(100);
const [windowHeight, setWindowHeight] = createSignal(
typeof window !== "undefined" ? window.innerHeight : 800
);
const [direction, setDirection] = createSignal(1); const [direction, setDirection] = createSignal(1);
const dimensions = createMemo(() => ({
width: windowWidth(),
height: windowHeight()
}));
const caveParallax = createMemo<ParallaxBackground>(() => ({ const caveParallax = createMemo<ParallaxBackground>(() => ({
imageSet: { imageSet: {
0: "/Cave/0.png", 0: "/Cave/0.png",
@@ -119,33 +144,27 @@ const SimpleParallax: ParentComponent = (props) => {
4: "/Cave/4.png", 4: "/Cave/4.png",
5: "/Cave/5.png", 5: "/Cave/5.png",
6: "/Cave/6.png", 6: "/Cave/6.png",
7: "/Cave/7.png", 7: "/Cave/7.png"
}, },
size: { width: 384, height: 216 }, size: { width: 384, height: 216 },
verticalOffset: 0.4, verticalOffset: 0.4
})); }));
const layerCount = createMemo(() => Object.keys(caveParallax().imageSet).length - 1); const layerCount = createMemo(
() => Object.keys(caveParallax().imageSet).length - 1
);
const imagesNeeded = 3; const imagesNeeded = 3;
const updateDimensions = () => {
if (containerRef) {
setDimensions({
width: window.innerWidth,
height: window.innerHeight,
});
}
};
onMount(() => { onMount(() => {
let timeoutId: ReturnType<typeof setTimeout>; let timeoutId: ReturnType<typeof setTimeout>;
const handleResize = () => { const handleResize = () => {
clearTimeout(timeoutId); clearTimeout(timeoutId);
timeoutId = setTimeout(updateDimensions, 100); timeoutId = setTimeout(() => {
setWindowHeight(window.innerHeight);
}, 100);
}; };
updateDimensions();
window.addEventListener("resize", handleResize); window.addEventListener("resize", handleResize);
const intervalId = setInterval(() => { const intervalId = setInterval(() => {
@@ -166,7 +185,7 @@ const SimpleParallax: ParentComponent = (props) => {
scale: 0, scale: 0,
scaledWidth: 0, scaledWidth: 0,
scaledHeight: 0, scaledHeight: 0,
verticalOffsetPixels: 0, verticalOffsetPixels: 0
}; };
} }
@@ -179,7 +198,7 @@ const SimpleParallax: ParentComponent = (props) => {
scale, scale,
scaledWidth: cave.size.width * scale, scaledWidth: cave.size.width * scale,
scaledHeight: cave.size.height * scale, scaledHeight: cave.size.height * scale,
verticalOffsetPixels: cave.verticalOffset * dims.height, verticalOffsetPixels: cave.verticalOffset * dims.height
}; };
}); });
@@ -214,13 +233,13 @@ const SimpleParallax: ParentComponent = (props) => {
return ( return (
<div <div
ref={containerRef} ref={containerRef}
class="fixed inset-0 w-screen h-screen overflow-hidden" class="fixed inset-0 h-screen w-screen overflow-hidden"
> >
<div class="absolute inset-0 bg-black"></div> <div class="absolute inset-0 bg-black"></div>
<div <div
class="absolute inset-0" class="absolute inset-0"
style={{ style={{
"margin-top": `${calculations().verticalOffsetPixels}px`, "margin-top": `${calculations().verticalOffsetPixels}px`
}} }}
> >
{parallaxLayers()} {parallaxLayers()}

View File

@@ -12,7 +12,7 @@ import type {
CommentReaction, CommentReaction,
UserPublicData UserPublicData
} from "~/types/comment"; } from "~/types/comment";
import { debounce } from "es-toolkit"; import { createWindowWidth } from "~/lib/resize-utils";
import UserDefaultImage from "~/components/icons/UserDefaultImage"; import UserDefaultImage from "~/components/icons/UserDefaultImage";
import ReplyIcon from "~/components/icons/ReplyIcon"; import ReplyIcon from "~/components/icons/ReplyIcon";
import TrashIcon from "~/components/icons/TrashIcon"; import TrashIcon from "~/components/icons/TrashIcon";
@@ -32,7 +32,7 @@ export default function CommentBlock(props: CommentBlockProps) {
const [replyBoxShowing, setReplyBoxShowing] = createSignal(false); const [replyBoxShowing, setReplyBoxShowing] = createSignal(false);
const [toggleHeight, setToggleHeight] = createSignal(0); const [toggleHeight, setToggleHeight] = createSignal(0);
const [reactions, setReactions] = createSignal<CommentReaction[]>([]); const [reactions, setReactions] = createSignal<CommentReaction[]>([]);
const [windowWidth, setWindowWidth] = createSignal(0); const windowWidth = createWindowWidth(200);
const [deletionLoading, setDeletionLoading] = createSignal(false); const [deletionLoading, setDeletionLoading] = createSignal(false);
const [userData, setUserData] = createSignal<UserPublicData | null>(null); const [userData, setUserData] = createSignal<UserPublicData | null>(null);
@@ -45,19 +45,6 @@ export default function CommentBlock(props: CommentBlockProps) {
setCommentCollapsed(props.level >= 4); setCommentCollapsed(props.level >= 4);
}); });
// Window resize handler
onMount(() => {
const handleResize = debounce(() => {
setWindowWidth(window.innerWidth);
}, 200);
window.addEventListener("resize", handleResize);
onCleanup(() => {
window.removeEventListener("resize", handleResize);
});
});
// Find user data from comment map // Find user data from comment map
createEffect(() => { createEffect(() => {
if (props.userCommentMap) { if (props.userCommentMap) {

View File

@@ -1,12 +1,6 @@
import { import { Accessor, createContext, useContext, createMemo } from "solid-js";
Accessor,
createContext,
useContext,
createMemo,
onMount,
onCleanup
} from "solid-js";
import { createSignal } from "solid-js"; import { createSignal } from "solid-js";
import { createWindowWidth, isMobile } from "~/lib/resize-utils";
const BarsContext = createContext<{ const BarsContext = createContext<{
leftBarSize: Accessor<number>; leftBarSize: Accessor<number>;
@@ -45,30 +39,15 @@ 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 initialWindowWidth = const windowWidth = createWindowWidth();
typeof window !== "undefined" ? window.innerWidth : 1024; const initialIsMobile = isMobile(windowWidth());
const isMobile = initialWindowWidth < 768; const [leftBarVisible, setLeftBarVisible] = createSignal(!initialIsMobile);
const [leftBarVisible, setLeftBarVisible] = createSignal(!isMobile);
const [rightBarVisible, setRightBarVisible] = createSignal(true); const [rightBarVisible, setRightBarVisible] = createSignal(true);
const [barsInitialized, setBarsInitialized] = createSignal(false); const [barsInitialized, setBarsInitialized] = createSignal(false);
const [windowWidth, setWindowWidth] = createSignal(initialWindowWidth);
let leftBarSized = false; let leftBarSized = false;
let rightBarSized = false; let rightBarSized = false;
// Track window width reactively for mobile/desktop detection
onMount(() => {
const handleResize = () => {
setWindowWidth(window.innerWidth);
};
window.addEventListener("resize", handleResize);
onCleanup(() => {
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
@@ -84,14 +63,10 @@ export function BarsProvider(props: { children: any }) {
}; };
// Initialize immediately on mobile if left bar starts hidden // Initialize immediately on mobile if left bar starts hidden
onMount(() => { if (initialIsMobile && !leftBarVisible()) {
const isMobile = typeof window !== "undefined" && window.innerWidth < 768;
if (isMobile && !leftBarVisible()) {
// Skip waiting for left bar size on mobile when it starts hidden // Skip waiting for left bar size on mobile when it starts hidden
leftBarSized = true; leftBarSized = true;
checkAndSync();
} }
});
const wrappedSetRightBarSize = (size: number) => { const wrappedSetRightBarSize = (size: number) => {
if (!barsInitialized()) { if (!barsInitialized()) {
@@ -108,8 +83,8 @@ export function BarsProvider(props: { children: any }) {
}; };
const checkAndSync = () => { const checkAndSync = () => {
const isMobile = typeof window !== "undefined" && window.innerWidth < 768; const currentIsMobile = isMobile(windowWidth());
const bothBarsReady = leftBarSized && (isMobile || rightBarSized); const bothBarsReady = leftBarSized && (currentIsMobile || rightBarSized);
if (bothBarsReady) { if (bothBarsReady) {
const maxWidth = Math.max(_leftBarNaturalSize(), _rightBarNaturalSize()); const maxWidth = Math.max(_leftBarNaturalSize(), _rightBarNaturalSize());
@@ -123,8 +98,8 @@ export function BarsProvider(props: { children: any }) {
const naturalSize = _leftBarNaturalSize(); const naturalSize = _leftBarNaturalSize();
if (naturalSize === 0) return 0; // Hidden if (naturalSize === 0) return 0; // Hidden
// On mobile (<768px), always return 0 for layout (overlay mode) // On mobile (<768px), always return 0 for layout (overlay mode)
const isMobile = windowWidth() < 768; const currentIsMobile = isMobile(windowWidth());
if (isMobile) return 0; if (currentIsMobile) return 0;
return barsInitialized() ? syncedBarSize() : naturalSize; return barsInitialized() ? syncedBarSize() : naturalSize;
}); });

57
src/lib/resize-utils.ts Normal file
View File

@@ -0,0 +1,57 @@
import { createSignal, onMount, onCleanup, Accessor } from "solid-js";
export const MOBILE_BREAKPOINT = 768;
/**
* Creates a reactive window width signal that updates on resize
* @param debounceMs Optional debounce delay in milliseconds
* @returns Accessor for current window width
*/
export function createWindowWidth(debounceMs?: number): Accessor<number> {
const initialWidth = typeof window !== "undefined" ? window.innerWidth : 1024;
const [width, setWidth] = createSignal(initialWidth);
onMount(() => {
let timeoutId: ReturnType<typeof setTimeout> | undefined;
const handleResize = () => {
if (debounceMs) {
clearTimeout(timeoutId);
timeoutId = setTimeout(() => {
setWidth(window.innerWidth);
}, debounceMs);
} else {
setWidth(window.innerWidth);
}
};
window.addEventListener("resize", handleResize);
onCleanup(() => {
clearTimeout(timeoutId);
window.removeEventListener("resize", handleResize);
});
});
return width;
}
/**
* Checks if the current window width is in mobile viewport
* @param width Current window width
* @returns true if mobile viewport
*/
export function isMobile(width: number): boolean {
return width < MOBILE_BREAKPOINT;
}
/**
* Creates a derived signal for mobile state
* @param windowWidth Window width accessor
* @returns Accessor for mobile state
*/
export function createIsMobile(
windowWidth: Accessor<number>
): Accessor<boolean> {
return () => isMobile(windowWidth());
}

View File

@@ -1,4 +1,5 @@
import { Title, Meta } from "@solidjs/meta"; import { Title, Meta } from "@solidjs/meta";
import { DarkModeToggle } from "~/components/DarkModeToggle";
import { Typewriter } from "~/components/Typewriter"; import { Typewriter } from "~/components/Typewriter";
export default function Home() { export default function Home() {
@@ -7,10 +8,10 @@ export default function Home() {
<Title>Home | Michael Freno</Title> <Title>Home | Michael Freno</Title>
<Meta <Meta
name="description" name="description"
content="Michael Freno - Software Engineer based in Brooklyn, NY. Passionate about dev tooling, game development, and open source software." content="Michael Freno - Software Engineer based in Brooklyn, NY"
/> />
<main class="flex h-full flex-col gap-8 text-xl"> <main class="flex h-full flex-col gap-8 px-4 text-xl">
<div class="flex-1"> <div class="flex-1">
<Typewriter speed={30} keepAlive={2000}> <Typewriter speed={30} keepAlive={2000}>
<div class="text-4xl">Hey!</div> <div class="text-4xl">Hey!</div>
@@ -147,9 +148,12 @@ export default function Home() {
</div> </div>
</div> </div>
</div> </div>
<div class="max-w-3/4 pt-8 md:max-w-1/2"> <Typewriter speed={160} class="max-w-3/4 pt-8 md:max-w-1/2">
And if you love the color schemes of this site (which of course you And if you love the color schemes of this site
do), you can see{" "} <div class="mx-auto w-fit">
<DarkModeToggle />
</div>
(which of course you do), you can see{" "}
<a <a
href="https://github.com/mikefreno/dots/blob/master/mac/nvim/lua/colors.lua" href="https://github.com/mikefreno/dots/blob/master/mac/nvim/lua/colors.lua"
class="text-blue hover-underline-animation" class="text-blue hover-underline-animation"
@@ -159,7 +163,7 @@ export default function Home() {
- and also see the rest of my various dot files idk. There's a macos - and also see the rest of my various dot files idk. There's a macos
and arch linux rice in there if you're into that kinda thing and a and arch linux rice in there if you're into that kinda thing and a
home server setup too. Which I will write about soon. home server setup too. Which I will write about soon.
</div> </Typewriter>
</div> </div>
<div class="flex flex-col items-end gap-4 pr-4"> <div class="flex flex-col items-end gap-4 pr-4">