fix resizing bugs (conflicting handlers)
This commit is contained in:
26
src/app.tsx
26
src/app.tsx
@@ -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 { createWindowWidth, isMobile } from "~/lib/resize-utils";
|
||||
|
||||
function AppLayout(props: { children: any }) {
|
||||
const {
|
||||
@@ -28,15 +29,16 @@ function AppLayout(props: { children: any }) {
|
||||
barsInitialized
|
||||
} = useBars();
|
||||
|
||||
const windowWidth = createWindowWidth();
|
||||
let lastScrollY = 0;
|
||||
const SCROLL_THRESHOLD = 100;
|
||||
|
||||
createEffect(() => {
|
||||
const handleResize = () => {
|
||||
const isMobile = window.innerWidth < 768; // md breakpoint
|
||||
const currentIsMobile = isMobile(windowWidth());
|
||||
|
||||
// Show bars when switching to desktop
|
||||
if (!isMobile) {
|
||||
if (!currentIsMobile) {
|
||||
setLeftBarVisible(true);
|
||||
setRightBarVisible(true);
|
||||
}
|
||||
@@ -48,10 +50,6 @@ function AppLayout(props: { children: any }) {
|
||||
|
||||
// Call immediately and whenever dependencies change
|
||||
handleResize();
|
||||
|
||||
window.addEventListener("resize", handleResize);
|
||||
|
||||
return () => window.removeEventListener("resize", handleResize);
|
||||
});
|
||||
|
||||
// Recalculate when bar sizes change (visibility or actual resize)
|
||||
@@ -65,9 +63,9 @@ function AppLayout(props: { children: any }) {
|
||||
onMount(() => {
|
||||
const handleScroll = () => {
|
||||
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
|
||||
if (currentScrollY > lastScrollY) {
|
||||
setLeftBarVisible(false);
|
||||
@@ -87,9 +85,9 @@ function AppLayout(props: { children: any }) {
|
||||
// ESC key to close sidebars on mobile
|
||||
onMount(() => {
|
||||
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()) {
|
||||
setLeftBarVisible(false);
|
||||
}
|
||||
@@ -122,12 +120,12 @@ function AppLayout(props: { children: any }) {
|
||||
const touchEndY = e.changedTouches[0].clientY;
|
||||
const deltaX = touchEndX - touchStartX;
|
||||
const deltaY = touchEndY - touchStartY;
|
||||
const isMobile = window.innerWidth < 768; // md breakpoint
|
||||
const currentIsMobile = isMobile(windowWidth());
|
||||
|
||||
// Only trigger if horizontal swipe is dominant
|
||||
if (Math.abs(deltaX) > Math.abs(deltaY)) {
|
||||
// Mobile: Only left bar
|
||||
if (isMobile) {
|
||||
if (currentIsMobile) {
|
||||
// Swipe right anywhere - reveal left bar
|
||||
if (deltaX > SWIPE_THRESHOLD) {
|
||||
setLeftBarVisible(true);
|
||||
@@ -160,10 +158,10 @@ function AppLayout(props: { children: any }) {
|
||||
});
|
||||
|
||||
const handleCenterTapRelease = (e: MouseEvent | TouchEvent) => {
|
||||
const isMobile = window.innerWidth < 768;
|
||||
const currentIsMobile = isMobile(windowWidth());
|
||||
|
||||
// Only hide left bar on mobile when it's visible
|
||||
if (isMobile && leftBarVisible()) {
|
||||
if (currentIsMobile && leftBarVisible()) {
|
||||
const target = e.target as HTMLElement;
|
||||
const isInteractive = target.closest(
|
||||
"a, button, input, select, textarea, [onclick]"
|
||||
|
||||
@@ -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 { createWindowWidth } from "~/lib/resize-utils";
|
||||
|
||||
type ParallaxBackground = {
|
||||
imageSet: { [key: number]: string };
|
||||
@@ -22,10 +32,16 @@ type ParallaxLayerProps = {
|
||||
function ParallaxLayer(props: ParallaxLayerProps) {
|
||||
let containerRef: HTMLDivElement | undefined;
|
||||
|
||||
const layerDepthFactor = createMemo(() => props.layer / (Object.keys(props.caveParallax.imageSet).length - 1));
|
||||
const layerVerticalOffset = createMemo(() => props.verticalOffsetPixels * layerDepthFactor());
|
||||
const layerDepthFactor = createMemo(
|
||||
() => props.layer / (Object.keys(props.caveParallax.imageSet).length - 1)
|
||||
);
|
||||
const layerVerticalOffset = createMemo(
|
||||
() => props.verticalOffsetPixels * layerDepthFactor()
|
||||
);
|
||||
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(() => ({
|
||||
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`,
|
||||
top: `${(props.dimensions.height - props.scaledHeight) / 2 + layerVerticalOffset()}px`,
|
||||
"transform-origin": "center center",
|
||||
"will-change": "transform",
|
||||
"will-change": "transform"
|
||||
}));
|
||||
|
||||
// Set up animation when component mounts or when direction/speed changes
|
||||
@@ -54,7 +70,7 @@ function ParallaxLayer(props: ParallaxLayerProps) {
|
||||
{
|
||||
duration,
|
||||
easing: "linear",
|
||||
repeat: Infinity,
|
||||
repeat: Infinity
|
||||
}
|
||||
);
|
||||
|
||||
@@ -68,7 +84,7 @@ function ParallaxLayer(props: ParallaxLayerProps) {
|
||||
style={{
|
||||
left: `${groupOffset * 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) => (
|
||||
@@ -77,7 +93,7 @@ function ParallaxLayer(props: ParallaxLayerProps) {
|
||||
style={{
|
||||
width: `${props.caveParallax.size.width}px`,
|
||||
height: `${props.caveParallax.size.height}px`,
|
||||
left: `${index * props.caveParallax.size.width}px`,
|
||||
left: `${index * props.caveParallax.size.width}px`
|
||||
}}
|
||||
>
|
||||
<img
|
||||
@@ -86,7 +102,12 @@ function ParallaxLayer(props: ParallaxLayerProps) {
|
||||
width={props.caveParallax.size.width}
|
||||
height={props.caveParallax.size.height}
|
||||
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>
|
||||
))}
|
||||
@@ -95,11 +116,7 @@ function ParallaxLayer(props: ParallaxLayerProps) {
|
||||
});
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={containerRef}
|
||||
class="absolute"
|
||||
style={containerStyle()}
|
||||
>
|
||||
<div ref={containerRef} class="absolute" style={containerStyle()}>
|
||||
{imageGroups()}
|
||||
</div>
|
||||
);
|
||||
@@ -107,9 +124,17 @@ function ParallaxLayer(props: ParallaxLayerProps) {
|
||||
|
||||
const SimpleParallax: ParentComponent = (props) => {
|
||||
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 dimensions = createMemo(() => ({
|
||||
width: windowWidth(),
|
||||
height: windowHeight()
|
||||
}));
|
||||
|
||||
const caveParallax = createMemo<ParallaxBackground>(() => ({
|
||||
imageSet: {
|
||||
0: "/Cave/0.png",
|
||||
@@ -119,33 +144,27 @@ const SimpleParallax: ParentComponent = (props) => {
|
||||
4: "/Cave/4.png",
|
||||
5: "/Cave/5.png",
|
||||
6: "/Cave/6.png",
|
||||
7: "/Cave/7.png",
|
||||
7: "/Cave/7.png"
|
||||
},
|
||||
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 updateDimensions = () => {
|
||||
if (containerRef) {
|
||||
setDimensions({
|
||||
width: window.innerWidth,
|
||||
height: window.innerHeight,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
onMount(() => {
|
||||
let timeoutId: ReturnType<typeof setTimeout>;
|
||||
|
||||
const handleResize = () => {
|
||||
clearTimeout(timeoutId);
|
||||
timeoutId = setTimeout(updateDimensions, 100);
|
||||
timeoutId = setTimeout(() => {
|
||||
setWindowHeight(window.innerHeight);
|
||||
}, 100);
|
||||
};
|
||||
|
||||
updateDimensions();
|
||||
window.addEventListener("resize", handleResize);
|
||||
|
||||
const intervalId = setInterval(() => {
|
||||
@@ -166,7 +185,7 @@ const SimpleParallax: ParentComponent = (props) => {
|
||||
scale: 0,
|
||||
scaledWidth: 0,
|
||||
scaledHeight: 0,
|
||||
verticalOffsetPixels: 0,
|
||||
verticalOffsetPixels: 0
|
||||
};
|
||||
}
|
||||
|
||||
@@ -179,7 +198,7 @@ const SimpleParallax: ParentComponent = (props) => {
|
||||
scale,
|
||||
scaledWidth: cave.size.width * 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 (
|
||||
<div
|
||||
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"
|
||||
style={{
|
||||
"margin-top": `${calculations().verticalOffsetPixels}px`,
|
||||
"margin-top": `${calculations().verticalOffsetPixels}px`
|
||||
}}
|
||||
>
|
||||
{parallaxLayers()}
|
||||
|
||||
@@ -12,7 +12,7 @@ import type {
|
||||
CommentReaction,
|
||||
UserPublicData
|
||||
} from "~/types/comment";
|
||||
import { debounce } from "es-toolkit";
|
||||
import { createWindowWidth } from "~/lib/resize-utils";
|
||||
import UserDefaultImage from "~/components/icons/UserDefaultImage";
|
||||
import ReplyIcon from "~/components/icons/ReplyIcon";
|
||||
import TrashIcon from "~/components/icons/TrashIcon";
|
||||
@@ -32,7 +32,7 @@ export default function CommentBlock(props: CommentBlockProps) {
|
||||
const [replyBoxShowing, setReplyBoxShowing] = createSignal(false);
|
||||
const [toggleHeight, setToggleHeight] = createSignal(0);
|
||||
const [reactions, setReactions] = createSignal<CommentReaction[]>([]);
|
||||
const [windowWidth, setWindowWidth] = createSignal(0);
|
||||
const windowWidth = createWindowWidth(200);
|
||||
const [deletionLoading, setDeletionLoading] = createSignal(false);
|
||||
const [userData, setUserData] = createSignal<UserPublicData | null>(null);
|
||||
|
||||
@@ -45,19 +45,6 @@ export default function CommentBlock(props: CommentBlockProps) {
|
||||
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
|
||||
createEffect(() => {
|
||||
if (props.userCommentMap) {
|
||||
|
||||
@@ -1,12 +1,6 @@
|
||||
import {
|
||||
Accessor,
|
||||
createContext,
|
||||
useContext,
|
||||
createMemo,
|
||||
onMount,
|
||||
onCleanup
|
||||
} from "solid-js";
|
||||
import { Accessor, createContext, useContext, createMemo } from "solid-js";
|
||||
import { createSignal } from "solid-js";
|
||||
import { createWindowWidth, isMobile } from "~/lib/resize-utils";
|
||||
|
||||
const BarsContext = createContext<{
|
||||
leftBarSize: Accessor<number>;
|
||||
@@ -45,30 +39,15 @@ export function BarsProvider(props: { children: any }) {
|
||||
const [_rightBarNaturalSize, _setRightBarNaturalSize] = createSignal(0);
|
||||
const [syncedBarSize, setSyncedBarSize] = createSignal(0);
|
||||
const [centerWidth, setCenterWidth] = createSignal(0);
|
||||
const initialWindowWidth =
|
||||
typeof window !== "undefined" ? window.innerWidth : 1024;
|
||||
const isMobile = initialWindowWidth < 768;
|
||||
const [leftBarVisible, setLeftBarVisible] = createSignal(!isMobile);
|
||||
const windowWidth = createWindowWidth();
|
||||
const initialIsMobile = isMobile(windowWidth());
|
||||
const [leftBarVisible, setLeftBarVisible] = createSignal(!initialIsMobile);
|
||||
const [rightBarVisible, setRightBarVisible] = createSignal(true);
|
||||
const [barsInitialized, setBarsInitialized] = createSignal(false);
|
||||
const [windowWidth, setWindowWidth] = createSignal(initialWindowWidth);
|
||||
|
||||
let leftBarSized = 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) => {
|
||||
if (!barsInitialized()) {
|
||||
// Before initialization, capture natural size
|
||||
@@ -84,14 +63,10 @@ export function BarsProvider(props: { children: any }) {
|
||||
};
|
||||
|
||||
// Initialize immediately on mobile if left bar starts hidden
|
||||
onMount(() => {
|
||||
const isMobile = typeof window !== "undefined" && window.innerWidth < 768;
|
||||
if (isMobile && !leftBarVisible()) {
|
||||
// Skip waiting for left bar size on mobile when it starts hidden
|
||||
leftBarSized = true;
|
||||
checkAndSync();
|
||||
}
|
||||
});
|
||||
if (initialIsMobile && !leftBarVisible()) {
|
||||
// Skip waiting for left bar size on mobile when it starts hidden
|
||||
leftBarSized = true;
|
||||
}
|
||||
|
||||
const wrappedSetRightBarSize = (size: number) => {
|
||||
if (!barsInitialized()) {
|
||||
@@ -108,8 +83,8 @@ export function BarsProvider(props: { children: any }) {
|
||||
};
|
||||
|
||||
const checkAndSync = () => {
|
||||
const isMobile = typeof window !== "undefined" && window.innerWidth < 768;
|
||||
const bothBarsReady = leftBarSized && (isMobile || rightBarSized);
|
||||
const currentIsMobile = isMobile(windowWidth());
|
||||
const bothBarsReady = leftBarSized && (currentIsMobile || rightBarSized);
|
||||
|
||||
if (bothBarsReady) {
|
||||
const maxWidth = Math.max(_leftBarNaturalSize(), _rightBarNaturalSize());
|
||||
@@ -123,8 +98,8 @@ export function BarsProvider(props: { children: any }) {
|
||||
const naturalSize = _leftBarNaturalSize();
|
||||
if (naturalSize === 0) return 0; // Hidden
|
||||
// On mobile (<768px), always return 0 for layout (overlay mode)
|
||||
const isMobile = windowWidth() < 768;
|
||||
if (isMobile) return 0;
|
||||
const currentIsMobile = isMobile(windowWidth());
|
||||
if (currentIsMobile) return 0;
|
||||
return barsInitialized() ? syncedBarSize() : naturalSize;
|
||||
});
|
||||
|
||||
|
||||
57
src/lib/resize-utils.ts
Normal file
57
src/lib/resize-utils.ts
Normal 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());
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Title, Meta } from "@solidjs/meta";
|
||||
import { DarkModeToggle } from "~/components/DarkModeToggle";
|
||||
import { Typewriter } from "~/components/Typewriter";
|
||||
|
||||
export default function Home() {
|
||||
@@ -7,10 +8,10 @@ export default function Home() {
|
||||
<Title>Home | Michael Freno</Title>
|
||||
<Meta
|
||||
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">
|
||||
<Typewriter speed={30} keepAlive={2000}>
|
||||
<div class="text-4xl">Hey!</div>
|
||||
@@ -147,9 +148,12 @@ export default function Home() {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div 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
|
||||
do), you can see{" "}
|
||||
<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
|
||||
<div class="mx-auto w-fit">
|
||||
<DarkModeToggle />
|
||||
</div>
|
||||
(which of course you do), you can see{" "}
|
||||
<a
|
||||
href="https://github.com/mikefreno/dots/blob/master/mac/nvim/lua/colors.lua"
|
||||
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 arch linux rice in there if you're into that kinda thing and a
|
||||
home server setup too. Which I will write about soon™.
|
||||
</div>
|
||||
</Typewriter>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col items-end gap-4 pr-4">
|
||||
|
||||
Reference in New Issue
Block a user