Skip to content

Commit f784344

Browse files
authored
Merge pull request #1 from TigerAppsOrg/feature/polish
making changes to cursor glide over text
2 parents b146539 + 539ebfa commit f784344

4 files changed

Lines changed: 563 additions & 30 deletions

File tree

client/src/components/Typing.css

Lines changed: 59 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -324,6 +324,29 @@
324324
animation: blink 1s infinite;
325325
}
326326

327+
:root[data-cursor='caret'] .snippet-display .current {
328+
background-color: transparent !important;
329+
color: inherit !important;
330+
font-weight: inherit;
331+
animation: none !important;
332+
text-shadow: none;
333+
transition: color 120ms ease;
334+
}
335+
336+
:root[data-cursor='caret'] .snippet-display .current.error {
337+
color: var(--error-color);
338+
text-shadow: none;
339+
}
340+
341+
:root[data-cursor='caret'][data-caret-blink='blink'] .snippet-display .current::before {
342+
animation: caretBlink 1.2s ease-in-out infinite;
343+
}
344+
345+
:root[data-cursor='caret'][data-caret-blink='solid'] .snippet-display .current::before {
346+
animation: none !important;
347+
opacity: 1;
348+
}
349+
327350
/* Default caret rendering (line cursor) when block cursor is disabled */
328351
.current::before {
329352
visibility: var(--line-cursor);
@@ -369,6 +392,16 @@
369392
display: none !important;
370393
}
371394

395+
:root[data-cursor='caret'] .snippet-display.glide-on .current {
396+
color: inherit !important;
397+
background-color: transparent !important;
398+
animation: none !important;
399+
}
400+
401+
:root[data-cursor='caret'] .snippet-display.glide-on .current::before {
402+
display: none !important;
403+
}
404+
372405
.snippet-display {
373406
position: relative;
374407
}
@@ -382,9 +415,9 @@
382415
pointer-events: none;
383416
z-index: 0; /* behind the text */
384417
transition:
385-
transform var(--cursor-glide-duration, 95ms) cubic-bezier(0.2, 0.8, 0.2, 1),
386-
width var(--cursor-glide-duration, 95ms) cubic-bezier(0.2, 0.8, 0.2, 1),
387-
height var(--cursor-glide-duration, 95ms) cubic-bezier(0.2, 0.8, 0.2, 1);
418+
transform var(--cursor-glide-duration, 90ms) cubic-bezier(0.24, 0.92, 0.35, 1),
419+
width var(--cursor-glide-duration, 90ms) cubic-bezier(0.24, 0.92, 0.35, 1),
420+
height var(--cursor-glide-duration, 90ms) cubic-bezier(0.24, 0.92, 0.35, 1);
388421
opacity: calc(var(--glide-cursor-enabled, 0)); /* 0 or 1 set by Settings */
389422
border-radius: 2px;
390423
backface-visibility: hidden;
@@ -402,6 +435,29 @@
402435
z-index: 2; /* above text so thin caret remains visible */
403436
animation: caretBlink 1.2s ease-in-out infinite;
404437
}
438+
:root[data-cursor='caret'][data-caret-blink='solid'] .cursor-overlay.caret {
439+
animation: none !important;
440+
opacity: 1 !important;
441+
}
442+
.cursor-overlay.caret.typing-active {
443+
animation: none !important;
444+
opacity: 1 !important;
445+
}
446+
447+
.snippet-display.caret-solid .cursor-overlay.caret {
448+
animation: none !important;
449+
opacity: 1 !important;
450+
}
451+
452+
.snippet-display.caret-solid .current,
453+
.snippet-display.caret-solid .current::before {
454+
animation: none !important;
455+
opacity: 1 !important;
456+
}
457+
458+
.snippet-display.caret-solid .current::before {
459+
background: var(--caret-color) !important;
460+
}
405461

406462
/* Non-glide caret (pseudo) also blinks smoothly */
407463
.current::before {

client/src/components/Typing.jsx

Lines changed: 76 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
// [AI DISCLAIMER: AI was used to help debug socket emit for timed tests; lines 394-408]
22

3-
import { useState, useEffect, useRef, useLayoutEffect } from 'react';
3+
import { useState, useEffect, useRef, useLayoutEffect, useCallback } from 'react';
44
import { useRace } from '../context/RaceContext';
55
import { useSocket } from '../context/SocketContext';
66
import playKeySound from './Sound.jsx';
@@ -65,6 +65,57 @@ function Typing({
6565
return (getComputedStyle(document.documentElement).getPropertyValue('--glide-cursor-enabled') || '0').trim() === '1';
6666
});
6767
const initialCursorSetRef = useRef(false);
68+
const initialCursorStyle = typeof document !== 'undefined'
69+
? (document.documentElement.getAttribute('data-cursor') || 'block')
70+
: 'block';
71+
const cursorStyleRef = useRef(initialCursorStyle);
72+
const [cursorStyle, setCursorStyle] = useState(initialCursorStyle);
73+
const typingActiveRef = useRef(false);
74+
const [typingActive, setTypingActive] = useState(false);
75+
76+
const setTypingActiveState = useCallback((active) => {
77+
if (typingActiveRef.current === active) return;
78+
typingActiveRef.current = active;
79+
setTypingActive(active);
80+
if (typeof document !== 'undefined') {
81+
const overlay = cursorRef.current;
82+
if (overlay) {
83+
if (cursorStyleRef.current === 'caret') {
84+
overlay.classList.toggle('typing-active', active);
85+
} else {
86+
overlay.classList.remove('typing-active');
87+
}
88+
}
89+
const mode = cursorStyleRef.current === 'caret' && active ? 'solid' : 'blink';
90+
document.documentElement.setAttribute('data-caret-blink', mode);
91+
}
92+
}, []);
93+
94+
useEffect(() => {
95+
if (typeof document === 'undefined') return undefined;
96+
return () => {
97+
document.documentElement.removeAttribute('data-caret-blink');
98+
};
99+
}, []);
100+
101+
useEffect(() => {
102+
if (typeof document === 'undefined') return undefined;
103+
const root = document.documentElement;
104+
const syncCursorStyle = () => {
105+
const next = root.getAttribute('data-cursor') || 'block';
106+
cursorStyleRef.current = next;
107+
setCursorStyle(next);
108+
if (next !== 'caret') {
109+
setTypingActiveState(false);
110+
} else if (!typingActiveRef.current) {
111+
document.documentElement.setAttribute('data-caret-blink', 'blink');
112+
}
113+
};
114+
syncCursorStyle();
115+
const observer = new MutationObserver(syncCursorStyle);
116+
observer.observe(root, { attributes: true, attributeFilter: ['data-cursor'] });
117+
return () => observer.disconnect();
118+
}, [setTypingActiveState]);
68119

69120
// Use testMode and testDuration for timed tests if provided
70121
useEffect(() => {
@@ -200,6 +251,15 @@ function Typing({
200251
}, [typingState.position]);
201252

202253
// Track snippet changes to reset input
254+
useEffect(() => {
255+
const active = ((raceState.inProgress || raceState.type === 'practice') && input.length > 0 && !typingState.completed);
256+
setTypingActiveState(active);
257+
}, [input.length, raceState.inProgress, raceState.type, typingState.completed, setTypingActiveState]);
258+
259+
useEffect(() => {
260+
return () => setTypingActiveState(false);
261+
}, [setTypingActiveState]);
262+
203263
useEffect(() => {
204264
if (raceState.snippet && raceState.snippet.id !== snippetId) {
205265
setSnippetId(raceState.snippet.id);
@@ -751,17 +811,26 @@ function Typing({
751811
const y = Math.round((rect.top - containerRect.top) + scrollY);
752812

753813
// Determine caret vs block based on Settings-managed CSS var
754-
const useCaret = (document.documentElement.getAttribute('data-cursor') === 'caret');
814+
const useCaret = (cursorStyleRef.current === 'caret');
755815

756816
// Size overlay to target element
757817
const height = rect.height;
758818
const width = useCaret ? 0 : rect.width; // caret drawn via border-left for crispness
759819
overlay.style.height = `${height}px`;
760820
overlay.style.width = `${width}px`;
761-
overlay.className = `cursor-overlay ${useCaret ? 'caret' : 'block'}`;
821+
overlay.className = 'cursor-overlay';
822+
if (useCaret) {
823+
overlay.classList.add('caret');
824+
overlay.classList.remove('block');
825+
overlay.classList.toggle('typing-active', typingActiveRef.current);
826+
} else {
827+
overlay.classList.add('block');
828+
overlay.classList.remove('caret');
829+
overlay.classList.remove('typing-active');
830+
}
762831

763832
// Cursor-specific duration (caret snappier)
764-
overlay.style.setProperty('--cursor-glide-duration', useCaret ? '95ms' : '95ms');
833+
overlay.style.setProperty('--cursor-glide-duration', useCaret ? '85ms' : '110ms');
765834

766835
// First placement should not animate from origin
767836
if (!initialCursorSetRef.current) {
@@ -1019,14 +1088,16 @@ function Typing({
10191088
};
10201089
}, []);
10211090

1091+
const caretSolid = typingActive && cursorStyle === 'caret';
1092+
10221093
return (
10231094
<>
10241095
<div className="stats-container">{getStatsContent()}</div>
10251096

10261097
{/* Only show typing area (snippet + input) if race is NOT completed */}
10271098
{!raceState.completed && (
10281099
<div className="typing-area">
1029-
<div className={`snippet-display ${isShaking ? 'shake-animation' : ''} ${glideEnabled ? 'glide-on' : ''}`}>
1100+
<div className={`snippet-display ${isShaking ? 'shake-animation' : ''} ${glideEnabled ? 'glide-on' : ''} ${caretSolid ? 'caret-solid' : ''}`}>
10301101
{/* Smooth-glide overlay cursor (rendered behind text) */}
10311102
<div ref={cursorRef} className="cursor-overlay block" aria-hidden="true" />
10321103
{/* Error message moved OUTSIDE snippet-display */}

public/css/styles.css

Lines changed: 61 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@
1212
--error-color: #f44336;
1313
--current-color: #3a506b;
1414
--cursor-color: #F58025;
15+
--caret-width: 3px;
16+
--cursor-glide-duration: 95ms;
1517
--font-family: 'Roboto', -apple-system, BlinkMacSystemFont, 'Segoe UI', Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
1618
}
1719

@@ -287,6 +289,64 @@ h1 {
287289
color: white;
288290
}
289291

292+
#text-display,
293+
#practice-text-display,
294+
#snippet-display {
295+
position: relative;
296+
}
297+
298+
.cursor-overlay {
299+
position: absolute;
300+
top: 0;
301+
left: 0;
302+
transform: translate3d(0, 0, 0);
303+
will-change: transform, width, height, opacity;
304+
pointer-events: none;
305+
z-index: 2;
306+
border-radius: 2px;
307+
opacity: 0;
308+
transition:
309+
transform var(--cursor-glide-duration) cubic-bezier(0.2, 0.8, 0.2, 1),
310+
width var(--cursor-glide-duration) cubic-bezier(0.2, 0.8, 0.2, 1),
311+
height var(--cursor-glide-duration) cubic-bezier(0.2, 0.8, 0.2, 1),
312+
opacity 160ms ease;
313+
}
314+
315+
.cursor-overlay.block {
316+
background-color: var(--current-color);
317+
box-shadow: 0 0 0.01px var(--current-color);
318+
z-index: 0;
319+
}
320+
321+
.cursor-overlay.caret {
322+
background: transparent;
323+
border-left: var(--caret-width) solid var(--cursor-color);
324+
box-shadow: 0 0 0.01px var(--cursor-color);
325+
}
326+
327+
.cursor-overlay.hidden {
328+
opacity: 0 !important;
329+
}
330+
331+
:root[data-cursor="caret"] #snippet-display .current,
332+
:root[data-cursor="caret"] .snippet-display .current,
333+
:root[data-cursor="caret"] #text-display .current,
334+
:root[data-cursor="caret"] #practice-text-display .current {
335+
background-color: transparent;
336+
color: var(--cursor-color);
337+
text-shadow: 0 0 0.6px rgba(245, 128, 37, 0.35);
338+
font-weight: 500;
339+
transition: color 120ms ease;
340+
}
341+
342+
:root[data-cursor="caret"] #snippet-display .current.error,
343+
:root[data-cursor="caret"] .snippet-display .current.error,
344+
:root[data-cursor="caret"] #text-display .current.error,
345+
:root[data-cursor="caret"] #practice-text-display .current.error {
346+
color: var(--error-color);
347+
text-shadow: 0 0 0.6px rgba(244, 67, 54, 0.25);
348+
}
349+
290350
@media (max-width: 768px) {
291351
.modes {
292352
flex-direction: column;
@@ -333,4 +393,4 @@ h1 {
333393
.stat-value {
334394
font-size: 1.2rem;
335395
color: var(--princeton-orange);
336-
}
396+
}

0 commit comments

Comments
 (0)