From b640099fc58575b8a6529c94d3b2a5028d929b84 Mon Sep 17 00:00:00 2001 From: Michael Freno Date: Mon, 22 Dec 2025 11:01:01 -0500 Subject: [PATCH] mobile improvements --- src/components/CountdownCircleTimer.tsx | 35 +- src/components/blog/TextEditor.tsx | 897 +++++++++++++----------- src/routes/login/password-reset.tsx | 2 +- 3 files changed, 496 insertions(+), 438 deletions(-) diff --git a/src/components/CountdownCircleTimer.tsx b/src/components/CountdownCircleTimer.tsx index c22b69d..2b4e47b 100644 --- a/src/components/CountdownCircleTimer.tsx +++ b/src/components/CountdownCircleTimer.tsx @@ -24,20 +24,28 @@ const CountdownCircleTimer: Component = (props) => { const strokeDashoffset = () => circumference * (1 - progress()); onMount(() => { - const interval = setInterval(() => { - setRemainingTime((prev) => { - const newTime = prev - 1; - if (newTime <= 0) { - clearInterval(interval); - props.onComplete?.(); - return 0; - } - return newTime; - }); - }, 1000); + const startTime = Date.now(); + const initialTime = remainingTime(); + let animationFrameId: number; + + const animate = () => { + const elapsed = (Date.now() - startTime) / 1000; + const newTime = Math.max(0, initialTime - elapsed); + + setRemainingTime(newTime); + + if (newTime <= 0) { + props.onComplete?.(); + return; + } + + animationFrameId = requestAnimationFrame(animate); + }; + + animationFrameId = requestAnimationFrame(animate); onCleanup(() => { - clearInterval(interval); + cancelAnimationFrame(animationFrameId); }); }); @@ -74,9 +82,6 @@ const CountdownCircleTimer: Component = (props) => { stroke-dasharray={`${circumference}`} stroke-dashoffset={`${strokeDashoffset()}`} stroke-linecap="round" - style={{ - transition: "stroke-dashoffset 0.5s linear" - }} /> {/* Timer text in center */} diff --git a/src/components/blog/TextEditor.tsx b/src/components/blog/TextEditor.tsx index ff2c8c5..523b3a6 100644 --- a/src/components/blog/TextEditor.tsx +++ b/src/components/blog/TextEditor.tsx @@ -381,6 +381,8 @@ export default function TextEditor(props: TextEditorProps) { const [showKeyboardHelp, setShowKeyboardHelp] = createSignal(false); const [isFullscreen, setIsFullscreen] = createSignal(false); + const [keyboardVisible, setKeyboardVisible] = createSignal(false); + const [keyboardHeight, setKeyboardHeight] = createSignal(0); const editor = createTiptapEditor(() => ({ element: editorRef, @@ -1040,6 +1042,36 @@ export default function TextEditor(props: TextEditorProps) { } }); + // Detect mobile keyboard visibility + createEffect(() => { + if (typeof window === "undefined" || !window.visualViewport) return; + + const viewport = window.visualViewport; + const initialHeight = viewport.height; + + const handleResize = () => { + const currentHeight = viewport.height; + const heightDiff = initialHeight - currentHeight; + + // If viewport height decreased by more than 150px, keyboard is likely open + if (heightDiff > 150) { + setKeyboardVisible(true); + setKeyboardHeight(heightDiff); + } else { + setKeyboardVisible(false); + setKeyboardHeight(0); + } + }; + + viewport.addEventListener("resize", handleResize); + viewport.addEventListener("scroll", handleResize); + + return () => { + viewport.removeEventListener("resize", handleResize); + viewport.removeEventListener("scroll", handleResize); + }; + }); + // Table Grid Selector Component const TableGridSelector = () => { const [hoverCell, setHoverCell] = createSignal({ row: 0, col: 0 }); @@ -1096,7 +1128,7 @@ export default function TextEditor(props: TextEditorProps) { ref={containerRef} class="border-surface2 text-text w-full max-w-full overflow-hidden rounded-md border px-4 py-2" classList={{ - "fixed inset-0 z-[100] m-0 h-screen max-h-screen rounded-none": + "fixed inset-0 z-[100] m-0 h-screen max-h-screen rounded-none flex flex-col": isFullscreen(), "bg-base": isFullscreen() }} @@ -1443,443 +1475,461 @@ export default function TextEditor(props: TextEditorProps) { -
- - - -
- - - - - -
- - - - - -
- - {/* Text Alignment */} - - - - -
- - - - - - -
- -
- - - - {/* Table controls - shown when cursor is in a table */} - -
- + {/* Main Toolbar - Pinned at top in fullscreen */} +
+
- - - - - -
- - - - - - - -
- - - - - - - +
+ + + + + +
+ + + + + +
+ + {/* Text Alignment */} + + + + +
+ + + + + + +
+ +
+ + + + {/* Table controls - shown when cursor is in a table */} + +
+ + + + + + + +
+ + + + + + + +
+ + + + + + + + +
+
)} @@ -1887,10 +1937,13 @@ export default function TextEditor(props: TextEditorProps) {
diff --git a/src/routes/login/password-reset.tsx b/src/routes/login/password-reset.tsx index 09cad70..3606b9d 100644 --- a/src/routes/login/password-reset.tsx +++ b/src/routes/login/password-reset.tsx @@ -326,7 +326,7 @@ export default function PasswordResetPage() { duration={5} size={200} strokeWidth={12} - colors="#60a5fa" + colors="var(--color-blue)" onComplete={() => false} > {({ remainingTime }) => renderTime(remainingTime)}