mobile improvements
This commit is contained in:
@@ -24,20 +24,28 @@ const CountdownCircleTimer: Component<CountdownCircleTimerProps> = (props) => {
|
||||
const strokeDashoffset = () => circumference * (1 - progress());
|
||||
|
||||
onMount(() => {
|
||||
const interval = setInterval(() => {
|
||||
setRemainingTime((prev) => {
|
||||
const newTime = prev - 1;
|
||||
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) {
|
||||
clearInterval(interval);
|
||||
props.onComplete?.();
|
||||
return 0;
|
||||
return;
|
||||
}
|
||||
return newTime;
|
||||
});
|
||||
}, 1000);
|
||||
|
||||
animationFrameId = requestAnimationFrame(animate);
|
||||
};
|
||||
|
||||
animationFrameId = requestAnimationFrame(animate);
|
||||
|
||||
onCleanup(() => {
|
||||
clearInterval(interval);
|
||||
cancelAnimationFrame(animationFrameId);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -74,9 +82,6 @@ const CountdownCircleTimer: Component<CountdownCircleTimerProps> = (props) => {
|
||||
stroke-dasharray={`${circumference}`}
|
||||
stroke-dashoffset={`${strokeDashoffset()}`}
|
||||
stroke-linecap="round"
|
||||
style={{
|
||||
transition: "stroke-dashoffset 0.5s linear"
|
||||
}}
|
||||
/>
|
||||
</svg>
|
||||
{/* Timer text in center */}
|
||||
|
||||
@@ -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,7 +1475,14 @@ export default function TextEditor(props: TextEditorProps) {
|
||||
</div>
|
||||
</Show>
|
||||
|
||||
<div class="border-surface2 mb-2 flex flex-wrap gap-1 border-b pb-2">
|
||||
{/* Main Toolbar - Pinned at top in fullscreen */}
|
||||
<div
|
||||
class="border-surface2 bg-base border-b"
|
||||
classList={{
|
||||
"sticky top-0 z-[105]": isFullscreen()
|
||||
}}
|
||||
>
|
||||
<div class="flex flex-wrap gap-1 pb-2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() =>
|
||||
@@ -1501,7 +1540,9 @@ export default function TextEditor(props: TextEditorProps) {
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => instance().chain().focus().toggleItalic().run()}
|
||||
onClick={() =>
|
||||
instance().chain().focus().toggleItalic().run()
|
||||
}
|
||||
class={`${
|
||||
instance().isActive("italic")
|
||||
? "bg-surface2"
|
||||
@@ -1513,7 +1554,9 @@ export default function TextEditor(props: TextEditorProps) {
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => instance().chain().focus().toggleStrike().run()}
|
||||
onClick={() =>
|
||||
instance().chain().focus().toggleStrike().run()
|
||||
}
|
||||
class={`${
|
||||
instance().isActive("strike")
|
||||
? "bg-surface2"
|
||||
@@ -1756,7 +1799,9 @@ export default function TextEditor(props: TextEditorProps) {
|
||||
onClick={toggleFullscreen}
|
||||
class="hover:bg-surface1 rounded px-2 py-1 text-xs"
|
||||
title={
|
||||
isFullscreen() ? "Exit Fullscreen (ESC)" : "Enter Fullscreen"
|
||||
isFullscreen()
|
||||
? "Exit Fullscreen (ESC)"
|
||||
: "Enter Fullscreen"
|
||||
}
|
||||
>
|
||||
{isFullscreen() ? "⇲ Exit" : "⇱ Fullscreen"}
|
||||
@@ -1820,7 +1865,9 @@ export default function TextEditor(props: TextEditorProps) {
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => instance().chain().focus().addRowAfter().run()}
|
||||
onClick={() =>
|
||||
instance().chain().focus().addRowAfter().run()
|
||||
}
|
||||
class="hover:bg-surface1 rounded px-2 py-1 text-xs"
|
||||
title="Add Row After"
|
||||
>
|
||||
@@ -1864,7 +1911,9 @@ export default function TextEditor(props: TextEditorProps) {
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => instance().chain().focus().mergeCells().run()}
|
||||
onClick={() =>
|
||||
instance().chain().focus().mergeCells().run()
|
||||
}
|
||||
class="hover:bg-surface1 rounded px-2 py-1 text-xs"
|
||||
title="Merge Cells"
|
||||
>
|
||||
@@ -1881,16 +1930,20 @@ export default function TextEditor(props: TextEditorProps) {
|
||||
</button>
|
||||
</Show>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</Show>
|
||||
|
||||
<div
|
||||
ref={editorRef}
|
||||
class="prose prose-sm prose-invert sm:prose-base md:prose-xl lg:prose-xl xl:prose-2xl mx-auto max-w-full overflow-scroll focus:outline-none"
|
||||
class="prose prose-sm prose-invert sm:prose-base md:prose-xl lg:prose-xl xl:prose-2xl mx-auto max-w-full overflow-scroll transition-all duration-300 focus:outline-none"
|
||||
classList={{
|
||||
"h-[80dvh]": !isFullscreen(),
|
||||
"h-[calc(100dvh-8rem)]": isFullscreen()
|
||||
"flex-1 h-full": isFullscreen()
|
||||
}}
|
||||
style={{
|
||||
"padding-bottom": keyboardVisible() ? `${keyboardHeight()}px` : "1rem"
|
||||
}}
|
||||
/>
|
||||
|
||||
|
||||
@@ -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)}
|
||||
|
||||
Reference in New Issue
Block a user