From 336c652bf7dba1ba60716f401234c799525c8e68 Mon Sep 17 00:00:00 2001 From: Timothy Pidashev Date: Mon, 6 Apr 2026 13:57:15 -0700 Subject: [PATCH] mobile optimizations --- src/components/blog/header.tsx | 14 ++---- src/components/blog/tagged-posts.tsx | 14 ++---- src/components/mobile-nav/index.tsx | 2 +- src/components/projects/project-list.tsx | 14 ++---- src/components/stream-content.tsx | 27 ++++++----- src/components/typed-text.tsx | 59 +++++++++++++++++++++--- 6 files changed, 85 insertions(+), 45 deletions(-) diff --git a/src/components/blog/header.tsx b/src/components/blog/header.tsx index 837bb30..d33e975 100644 --- a/src/components/blog/header.tsx +++ b/src/components/blog/header.tsx @@ -1,18 +1,14 @@ import { RssIcon, TagIcon, TrendingUpIcon } from "lucide-react"; import { AnimateIn } from "@/components/animate-in"; -import { TypedText } from "@/components/typed-text"; export const BlogHeader = () => { return (
-
- -
+ +

+ Latest Thoughts & Writings +

+
{ return (
-
- -
+ +

+ #{tag} +

+
[]; @@ -9,14 +8,11 @@ interface ProjectListProps { export function ProjectList({ projects }: ProjectListProps) { return (
-
- -
+ +

+ Here's what I've been building lately +

+
    {projects.map((project, i) => ( diff --git a/src/components/stream-content.tsx b/src/components/stream-content.tsx index 12b001b..0b34ede 100644 --- a/src/components/stream-content.tsx +++ b/src/components/stream-content.tsx @@ -9,29 +9,34 @@ function typeInHeading(el: HTMLElement): Promise { const textLength = text.length; if (textLength === 0) { resolve(); return; } - // Preserve height - const rect = el.getBoundingClientRect(); - el.style.minHeight = `${rect.height}px`; - - // Speed scales with length const speed = Math.max(8, Math.min(25, 600 / textLength)); - - // Store original HTML, clear visible text but keep element structure const originalHTML = el.innerHTML; - el.textContent = ""; + + // Wrap each character in a span with opacity:0 + // The full text stays in the DOM so layout/wrapping is correct from the start + el.innerHTML = ""; + const chars: HTMLSpanElement[] = []; + for (const char of text) { + const span = document.createElement("span"); + span.textContent = char; + span.style.opacity = "0"; + chars.push(span); + el.appendChild(span); + } + el.style.opacity = "1"; el.style.transform = "translate3d(0,0,0)"; let i = 0; const step = () => { - if (i >= text.length) { + if (i >= chars.length) { + // Restore original HTML to clean up spans el.innerHTML = originalHTML; - el.style.minHeight = ""; resolve(); return; } + chars[i].style.opacity = "1"; i++; - el.textContent = text.slice(0, i); setTimeout(step, speed); }; step(); diff --git a/src/components/typed-text.tsx b/src/components/typed-text.tsx index c184a04..f353fe5 100644 --- a/src/components/typed-text.tsx +++ b/src/components/typed-text.tsx @@ -79,17 +79,64 @@ export function TypedText({ speed = 12, cursor = true, }: TypedTextProps) { + const containerRef = useRef(null); + const tagRef = useRef(null); const { ref, visible } = useScrollVisible(); - const { displayed, done } = useTypewriter(text, visible, speed); + const [done, setDone] = useState(false); + const [started, setStarted] = useState(false); + + useEffect(() => { + if (!visible || started) return; + setStarted(true); + + if (prefersReducedMotion()) { + setDone(true); + return; + } + + const el = tagRef.current; + if (!el) return; + + // Wrap each character in an invisible span — layout stays correct + el.textContent = ""; + const chars: HTMLSpanElement[] = []; + for (const char of text) { + const span = document.createElement("span"); + span.textContent = char; + span.style.opacity = "0"; + chars.push(span); + el.appendChild(span); + } + + const textLength = text.length; + const charSpeed = Math.max(8, Math.min(speed, 600 / textLength)); + + let i = 0; + const step = () => { + if (i >= chars.length) { + el.textContent = text; + setDone(true); + return; + } + chars[i].style.opacity = "1"; + i++; + setTimeout(step, charSpeed); + }; + step(); + }, [visible, started, text, speed]); return (
    - - {visible ? displayed : "\u00A0"} - {cursor && visible && !done && ( - | - )} + + {!started ? "\u00A0" : done ? text : null} + {cursor && started && !done && ( + | + )}
    ); }