T45c Site · Home: full screen stops the glide at once, a starting gallery leaves the host's focus alone, a focused frame stays mounted

This commit is contained in:
ZacharyZhang-NY committed 2026-10-01 08:38:05 -04:00
1 parent c3f9cd533a
commit 24687a09fb
7 files changed
+55 -17

No files matched your search

+6 -2
View File
@@ -1,5 +1,6 @@
import { useEffect, useMemo, useRef, useState } from "react";
import { useEffect, useLayoutEffect, useMemo, useRef, useState } from "react";
import { Icon } from "./icons";
import { hold } from "./scroll";
import { mode, useMode } from "./theme";
const SPIN_AFTER = 400;
@@ -45,13 +46,16 @@ export function Live({ page, story, title }: { page: string; story: string; titl
frame.current?.contentWindow?.postMessage({ ely: "theme", theme }, location.origin);
}, [theme]);
useEffect(() => {
// Before paint, so no glide frame lands after the press.
useLayoutEffect(() => {
if (!full) return;
document.documentElement.style.overflow = "hidden";
hold(true);
const escape = (event: KeyboardEvent) => event.key === "Escape" && setFull(false);
addEventListener("keydown", escape);
return () => {
document.documentElement.style.overflow = "";
hold(false);
removeEventListener("keydown", escape);
};
}, [full]);
+3 -5
View File
@@ -1,10 +1,9 @@
import { Link, Outlet, useRouter } from "@tanstack/react-router";
import Lenis from "lenis";
import "lenis/dist/lenis.css";
import { useEffect } from "react";
import { REPO } from "./data";
import { Icon } from "./icons";
import { useStill } from "./iso/Iso";
import { smooth } from "./scroll";
import { setSound, useSound } from "./iso/marimba";
import { setMode, useMode } from "./theme";
@@ -61,13 +60,12 @@ export function Shell() {
const router = useRouter();
useEffect(() => {
if (still) return;
// It halts while the page's overflow is hidden, as full screen sets.
const lenis = new Lenis({ autoRaf: true, autoToggle: true });
const { lenis, end } = smooth();
// A glide in flight must not outlive its page.
const off = router.subscribe("onBeforeNavigate", () => lenis.scrollTo(lenis.animatedScroll, { immediate: true, force: true }));
return () => {
off();
lenis.destroy();
end();
};
}, [still, router]);
return (
+17 -4
View File
@@ -21,14 +21,27 @@ const FEATURES = [
return { ...f, name: story.title };
});
/** Mounts its children only while near the view. */
/** Mounts its children while near the view or holding focus. */
function Near({ className, children }: { className: string; children: ReactNode }) {
const box = useRef<HTMLDivElement>(null);
const [near, setNear] = useState(false);
useEffect(() => {
const seen = new IntersectionObserver(([e]) => setNear(e.isIntersecting), { rootMargin: "100% 0px" });
seen.observe(box.current!);
return () => seen.disconnect();
const el = box.current!;
let seen = false;
const view = new IntersectionObserver(
([e]) => {
seen = e.isIntersecting;
setNear(seen || el.contains(document.activeElement));
},
{ rootMargin: "100% 0px" },
);
const left = (e: FocusEvent) => setNear(seen || el.contains(e.relatedTarget as Node | null));
view.observe(el);
el.addEventListener("focusout", left);
return () => {
view.disconnect();
el.removeEventListener("focusout", left);
};
}, []);
return (
<div ref={box} className={className}>
+18
View File
@@ -0,0 +1,18 @@
import Lenis from "lenis";
import "lenis/dist/lenis.css";
let lenis: Lenis | null = null;
/** Smooth scroll for the page; the returned call ends it. */
export function smooth(): { lenis: Lenis; end: () => void } {
if (lenis) throw new Error("scroll: smooth scroll already runs");
const own = new Lenis({ autoRaf: true });
lenis = own;
return { lenis: own, end: () => (own.destroy(), (lenis = null)) };
}
/** Holds the page still at once, a glide in flight included. */
export function hold(still: boolean): void {
if (still) lenis?.stop();
else lenis?.start();
}