Skip to content

Commit 6becc4f

Browse files
committed
fix scroll
1 parent 45768ec commit 6becc4f

5 files changed

Lines changed: 94 additions & 5 deletions

File tree

apps/web/src/App.tsx

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ export default function App() {
3939
const [customPrompts, setCustomPrompts] = useState<PromptPackItem[]>([]);
4040
const [isCustomDialogOpen, setCustomDialogOpen] = useState(false);
4141
const [editingPrompt, setEditingPrompt] = useState<PromptPackItem | null>(null);
42+
const [isTyping, setIsTyping] = useState(false);
4243

4344
const targetSnippet = useSessionStore((state) => state.targetSnippet);
4445
const typed = useSessionStore((state) => state.typed);
@@ -55,6 +56,7 @@ export default function App() {
5556
const setSyntaxThemeId = useSyntaxThemeStore((state) => state.setThemeId);
5657

5758
const textareaRef = useRef<HTMLTextAreaElement | null>(null);
59+
const typingTimeoutRef = useRef<NodeJS.Timeout>();
5860

5961
// Load custom prompts on mount
6062
useEffect(() => {
@@ -159,6 +161,14 @@ export default function App() {
159161
if (isExplorerOpen) {
160162
closeExplorer();
161163
}
164+
165+
setIsTyping(true);
166+
if (typingTimeoutRef.current) {
167+
clearTimeout(typingTimeoutRef.current);
168+
}
169+
typingTimeoutRef.current = setTimeout(() => {
170+
setIsTyping(false);
171+
}, 1000);
162172
}, [closeExplorer, isExplorerOpen]);
163173

164174
const { setPendingPrompt } = usePendingPromptSwitch(selectedLanguage, applyPromptSelection);
@@ -219,7 +229,7 @@ export default function App() {
219229
/>
220230

221231
<div className={appStyles.windowBody}>
222-
<div className={appStyles.editorColumn}>
232+
<div className={`${appStyles.editorColumn}${isTyping ? " is-typing" : ""}`}>
223233
<div className={appStyles.editorInner}>
224234
<EditorSurface
225235
textareaRef={textareaRef}

apps/web/src/app/App.styles.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,8 @@ export const appStyles = {
1010
windowFullscreen:
1111
`fixed inset-0 z-30 m-0 flex h-screen w-screen min-h-0 flex-col overflow-hidden rounded-none shadow-none ${uiTokens.glassSurface} text-white`,
1212
windowBody: "relative flex flex-1 flex-col min-h-0 overflow-hidden",
13-
editorColumn: "flex flex-1 min-h-0 overflow-auto",
14-
editorInner: "flex-1 min-h-0 overflow-auto",
13+
editorColumn: "flex flex-1 min-h-0 overflow-auto scrollbar-auto-hide",
14+
editorInner: "flex-1 min-h-0",
1515
explorerPane: (open: boolean) =>
1616
`absolute top-0 right-0 bottom-0 transform transition-transform duration-300 ease-out will-change-transform backdrop-blur-2xl backdrop-saturate-150 ${open ? "translate-x-0 pointer-events-auto" : "translate-x-full pointer-events-none"
1717
}`,

apps/web/src/features/editor/components/EditorSurface.styles.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
export const editorStyles = {
22
container:
33
"relative bg-[length:100px_100px] bg-[radial-gradient(circle,_rgba(255,255,255,0.015)_1px,_transparent_1px)]",
4-
inner: "py-10",
4+
inner: "pt-10 pb-96",
55
textarea:
66
"absolute inset-0 h-full w-full resize-none opacity-0",
77
};

apps/web/src/features/editor/components/TypingFeedback.tsx

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { useMemo } from "react";
1+
import { useEffect, useMemo, useRef } from "react";
22
import { motion } from "framer-motion";
33
import { feedbackClass, type CharacterFeedback } from "../../../shared/feedback/feedback";
44
import { getSyntaxColorClass, getTokenTypeAtIndex, type SyntaxToken } from "../../../shared/syntax/syntax";
@@ -23,6 +23,8 @@ export function TypingFeedback({
2323
fontSize,
2424
syntaxThemeId,
2525
}: TypingFeedbackProps) {
26+
const activeCursorRef = useRef<HTMLSpanElement>(null);
27+
2628
const lineCount = useMemo(() => {
2729
let count = 1;
2830
feedback.forEach((item) => {
@@ -33,6 +35,38 @@ export function TypingFeedback({
3335
return count;
3436
}, [feedback]);
3537

38+
useEffect(() => {
39+
if (activeCursorRef.current) {
40+
const element = activeCursorRef.current;
41+
const container = element.closest('.scrollbar-auto-hide') as HTMLElement;
42+
43+
if (container) {
44+
const rect = element.getBoundingClientRect();
45+
const containerRect = container.getBoundingClientRect();
46+
47+
// Only scroll down if cursor is below viewport, never scroll up
48+
const buffer = 100; // pixels from bottom before triggering scroll
49+
const isBelowView = rect.bottom > containerRect.bottom - buffer;
50+
51+
if (isBelowView) {
52+
// Calculate position to keep cursor visible with buffer from bottom
53+
const relativeTop = rect.top - containerRect.top + container.scrollTop;
54+
const elementHeight = rect.height;
55+
const containerHeight = container.clientHeight;
56+
57+
// Position cursor at 2/3 down the viewport (not centered, keeps more context above)
58+
const targetPosition = containerHeight * 0.66;
59+
const scrollTop = relativeTop - targetPosition + (elementHeight / 2);
60+
61+
// Only scroll if we need to go down
62+
if (scrollTop > container.scrollTop) {
63+
container.scrollTop = scrollTop;
64+
}
65+
}
66+
}
67+
}
68+
}, [typedLength]);
69+
3670
return (
3771
<div className={typingFeedbackStyles.wrapper}>
3872
<div
@@ -60,6 +94,7 @@ export function TypingFeedback({
6094
return (
6195
<span
6296
key={item.index}
97+
ref={isActive ? activeCursorRef : undefined}
6398
className={typingFeedbackStyles.pendingSpan(syntaxClass)}
6499
>
65100
{item.char}
@@ -82,6 +117,7 @@ export function TypingFeedback({
82117
return (
83118
<span
84119
key={item.index}
120+
ref={isActive ? activeCursorRef : undefined}
85121
className={typingFeedbackStyles.typedSpan(syntaxClass, decorationClass)}
86122
title={
87123
item.typed && item.typed !== item.char

apps/web/src/styles.css

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,3 +30,46 @@ body::before {
3030
position: relative;
3131
z-index: 1;
3232
}
33+
34+
/* Auto-hiding scrollbars */
35+
.scrollbar-auto-hide {
36+
scrollbar-width: thin;
37+
scrollbar-color: transparent transparent;
38+
transition: scrollbar-color 0.3s ease;
39+
}
40+
41+
.scrollbar-auto-hide:hover {
42+
scrollbar-color: rgba(255, 255, 255, 0.2) transparent;
43+
}
44+
45+
/* Hide scrollbar when typing, even on hover */
46+
.scrollbar-auto-hide.is-typing {
47+
scrollbar-color: transparent transparent !important;
48+
}
49+
50+
.scrollbar-auto-hide::-webkit-scrollbar {
51+
width: 8px;
52+
height: 8px;
53+
}
54+
55+
.scrollbar-auto-hide::-webkit-scrollbar-track {
56+
background: transparent;
57+
}
58+
59+
.scrollbar-auto-hide::-webkit-scrollbar-thumb {
60+
background-color: transparent;
61+
border-radius: 4px;
62+
transition: background-color 0.3s ease;
63+
}
64+
65+
.scrollbar-auto-hide:hover::-webkit-scrollbar-thumb {
66+
background-color: rgba(255, 255, 255, 0.2);
67+
}
68+
69+
.scrollbar-auto-hide.is-typing::-webkit-scrollbar-thumb {
70+
background-color: transparent !important;
71+
}
72+
73+
.scrollbar-auto-hide::-webkit-scrollbar-thumb:hover {
74+
background-color: rgba(255, 255, 255, 0.3);
75+
}

0 commit comments

Comments
 (0)