Skip to content

Commit fe192f2

Browse files
author
danil-nizamov
committed
added key tip mode
1 parent f1525e6 commit fe192f2

5 files changed

Lines changed: 227 additions & 17 deletions

File tree

client/config.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,5 +2,6 @@
22
"keyboard": true,
33
"availableKeys": [],
44
"showStats": true,
5-
"realTimeStats": ["time", "chars"]
5+
"realTimeStats": ["time", "chars"],
6+
"keyTips": true
67
}

client/stats.txt

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
Typing Statistics
22
==================
33

4-
Total Errors Made: 48
5-
Errors Left (Unfixed): 48
6-
Total Time: 7.41 seconds
7-
Accuracy: 42.17%
8-
Speed: 121.42 words per minute
4+
Total Errors Made: 3
5+
Errors Left (Unfixed): 3
6+
Total Time: 2.81 seconds
7+
Accuracy: 84.21%
8+
Speed: 85.29 words per minute
99

10-
Generated: 28/11/2025, 17:14:21
10+
Generated: 03/12/2025, 10:19:24

client/text-to-input.txt

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1 @@
1-
testing text.
2-
with newlines!
3-
4-
And special symbols: \ [ `.
5-
6-
Reach the end to finish!
1+
Test text to input!

client/typing-simulator.css

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@
2424
position: relative;
2525
width: 100%;
2626
max-width: 900px;
27-
margin: var(--UI-Spacing-spacing-xl) auto 0;
27+
margin: var(--UI-Spacing-spacing-md) auto 0;
2828
flex: 0 0 auto; /* Don't grow, don't shrink */
2929
min-height: 0; /* Allow flexbox to shrink */
3030
}
@@ -323,9 +323,9 @@
323323
.bespoke .keyboard-container {
324324
display: none;
325325
width: 100%;
326-
padding: var(--UI-Spacing-spacing-lg);
326+
padding: var(--UI-Spacing-spacing-md) var(--UI-Spacing-spacing-lg) var(--UI-Spacing-spacing-sm) var(--UI-Spacing-spacing-lg);
327327
background: transparent;
328-
overflow: hidden; /* Prevent keyboard from causing scroll */
328+
overflow: visible; /* Allow key tip glow to be visible */
329329
}
330330

331331
.bespoke .keyboard-container.visible {
@@ -415,6 +415,24 @@
415415
cursor: not-allowed;
416416
}
417417

418+
.bespoke .keyboard-key.key-tip {
419+
background: color-mix(in srgb, var(--Colors-Primary-Default) 40%, transparent);
420+
border-color: var(--Colors-Primary-Default);
421+
animation: keyTipPulse 1.5s ease-in-out infinite;
422+
box-shadow: 0 0 8px 2px color-mix(in srgb, var(--Colors-Primary-Default) 50%, transparent);
423+
}
424+
425+
@keyframes keyTipPulse {
426+
0%, 100% {
427+
transform: scale(1);
428+
box-shadow: 0 0 8px 2px color-mix(in srgb, var(--Colors-Primary-Default) 50%, transparent);
429+
}
430+
50% {
431+
transform: scale(1.05);
432+
box-shadow: 0 0 12px 4px color-mix(in srgb, var(--Colors-Primary-Default) 70%, transparent);
433+
}
434+
}
435+
418436
/* Responsive Design */
419437
@media (max-width: 1024px) {
420438
.bespoke .keyboard-stats-wrapper {

client/typing-simulator.js

Lines changed: 197 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
let statsStartOverButton = null;
1212
let keyboardContainer = null;
1313
let realtimeStatsContainer = null;
14-
let config = { keyboard: true, availableKeys: [], showStats: false, realTimeStats: [] };
14+
let config = { keyboard: true, availableKeys: [], showStats: false, realTimeStats: [], keyTips: false };
1515

1616
// Normalized set of available keys (for fast lookup)
1717
let availableKeysSet = new Set();
@@ -28,6 +28,9 @@
2828
let keyboardEnabled = false;
2929
let activeKeyElement = null;
3030
let activeKeyTimeout = null;
31+
let keyTipsEnabled = false;
32+
let currentTipKeyElement = null;
33+
let currentTipShiftElement = null; // Track which shift key is highlighted
3134

3235
// Real-time stats update interval
3336
let realtimeStatsInterval = null;
@@ -136,8 +139,153 @@
136139
return keyboardContainer.querySelector(`[data-key="${normalizedChar}"]`);
137140
}
138141

142+
// Map shift symbols to their base keys
143+
const shiftSymbolMap = {
144+
'!': '1', '@': '2', '#': '3', '$': '4', '%': '5',
145+
'^': '6', '&': '7', '*': '8', '(': '9', ')': '0',
146+
'_': '-', '+': '=', '{': '[', '}': ']', '|': '\\',
147+
':': ';', '"': "'", '<': ',', '>': '.', '?': '/', '~': '`'
148+
};
149+
150+
// Get the base key for a character (maps shift symbols to their base keys)
151+
function getBaseKey(char) {
152+
// If it's a shift symbol, return the base key
153+
if (shiftSymbolMap[char]) {
154+
return shiftSymbolMap[char];
155+
}
156+
// For uppercase letters, return lowercase
157+
if (char >= 'A' && char <= 'Z') {
158+
return char.toLowerCase();
159+
}
160+
// Otherwise return as-is
161+
return char;
162+
}
163+
164+
// Check if a character requires Shift to type
165+
function requiresShift(char) {
166+
// Check if it's an uppercase letter
167+
if (char >= 'A' && char <= 'Z') {
168+
return true;
169+
}
170+
171+
// Check if it's a special symbol that requires Shift
172+
if (shiftSymbolMap[char]) {
173+
return true;
174+
}
175+
176+
return false;
177+
}
178+
179+
// Determine if a key is on the left side of the keyboard
180+
// Returns true for left side, false for right side
181+
// Based on standard QWERTY layout:
182+
// Left: `, 1-5, tab, q-t, caps, a-g, shift, z-b
183+
// Right: 6-0, -, =, y-p, [, ], \, h-l, ;, ', n-/, shift
184+
function isKeyOnLeftSide(char) {
185+
const normalizedChar = char.toLowerCase();
186+
187+
// Handle special keys
188+
if (normalizedChar === 'tab' || normalizedChar === 'caps' || normalizedChar === 'shift') {
189+
// These keys span or are on left side
190+
return true;
191+
}
192+
193+
// Number row: left side is `, 1-5
194+
if (normalizedChar === '`' || (normalizedChar >= '1' && normalizedChar <= '5')) {
195+
return true;
196+
}
197+
198+
// Top letter row: left side is q-t
199+
if (normalizedChar >= 'q' && normalizedChar <= 't') {
200+
return true;
201+
}
202+
203+
// Middle letter row: left side is a-g
204+
if (normalizedChar >= 'a' && normalizedChar <= 'g') {
205+
return true;
206+
}
207+
208+
// Bottom letter row: left side is z-b
209+
if (normalizedChar >= 'z' && normalizedChar <= 'b') {
210+
return true;
211+
}
212+
213+
// Everything else is on the right side
214+
return false;
215+
}
216+
217+
// Get shift key element (left or right)
218+
function getShiftKeyElement(isLeft) {
219+
if (!keyboardContainer) return null;
220+
221+
// Find shift key by data attribute
222+
const shiftSide = isLeft ? 'left' : 'right';
223+
return keyboardContainer.querySelector(`[data-key="shift"][data-shift-side="${shiftSide}"]`);
224+
}
225+
226+
// Update the key tip highlight (shows which key to press next)
227+
function updateKeyTip() {
228+
// Only update if keyTips mode is enabled and keyboard is visible
229+
if (!keyTipsEnabled || !keyboardEnabled || !keyboardContainer) {
230+
return;
231+
}
232+
233+
// Clear previous tip
234+
if (currentTipKeyElement) {
235+
currentTipKeyElement.classList.remove('key-tip');
236+
currentTipKeyElement = null;
237+
}
238+
239+
// Clear previous shift tip
240+
if (currentTipShiftElement) {
241+
currentTipShiftElement.classList.remove('key-tip');
242+
currentTipShiftElement = null;
243+
}
244+
245+
// Find the next character to type
246+
const nextCharIndex = typedText.length;
247+
if (nextCharIndex >= originalText.length) {
248+
// All characters typed, no tip needed
249+
return;
250+
}
251+
252+
const nextChar = originalText[nextCharIndex];
253+
if (!nextChar) {
254+
return;
255+
}
256+
257+
// Get the base key for highlighting (maps shift symbols to their base keys)
258+
const baseKey = getBaseKey(nextChar);
259+
260+
// Get the key element for the base key
261+
const keyElement = getKeyElement(baseKey);
262+
if (keyElement) {
263+
currentTipKeyElement = keyElement;
264+
keyElement.classList.add('key-tip');
265+
}
266+
267+
// Check if shift is needed and highlight appropriate shift key
268+
if (requiresShift(nextChar)) {
269+
// Determine which side of keyboard the key is on
270+
const charForSideCheck = baseKey.toLowerCase();
271+
const isLeftSide = isKeyOnLeftSide(charForSideCheck);
272+
273+
// Highlight opposite shift: left side keys use right shift, right side keys use left shift
274+
const shiftElement = getShiftKeyElement(!isLeftSide);
275+
if (shiftElement) {
276+
currentTipShiftElement = shiftElement;
277+
shiftElement.classList.add('key-tip');
278+
}
279+
}
280+
}
281+
139282
// Highlight a key on the keyboard
140283
function highlightKey(char, isError = false) {
284+
// Skip regular highlighting if keyTips mode is enabled
285+
if (keyTipsEnabled) {
286+
return;
287+
}
288+
141289
// Don't highlight unavailable keys
142290
if (!isKeyAvailable(char)) {
143291
return;
@@ -179,6 +327,8 @@
179327
const keyboard = document.createElement('div');
180328
keyboard.className = 'keyboard';
181329

330+
let shiftKeyIndex = 0; // Track which shift key we're rendering (0 = left, 1 = right)
331+
182332
keyboardLayout.forEach(row => {
183333
const rowElement = document.createElement('div');
184334
rowElement.className = 'keyboard-row';
@@ -189,6 +339,13 @@
189339
keyElement.className = 'keyboard-key';
190340
keyElement.setAttribute('data-key', normalizedKey);
191341

342+
// Add data attribute to distinguish left vs right shift
343+
if (key === 'shift') {
344+
const shiftSide = shiftKeyIndex === 0 ? 'left' : 'right';
345+
keyElement.setAttribute('data-shift-side', shiftSide);
346+
shiftKeyIndex++;
347+
}
348+
192349
// Check if this key is available (use isKeyAvailable to ensure space, comma, dot are always available)
193350
const isAvailable = isKeyAvailable(key);
194351
if (!isAvailable) {
@@ -224,10 +381,15 @@
224381
if (!keyboardContainer) return;
225382

226383
keyboardEnabled = config.keyboard === true;
384+
keyTipsEnabled = config.keyTips === true; // Defaults to false if undefined or not set
227385

228386
if (keyboardEnabled) {
229387
renderKeyboard();
230388
keyboardContainer.classList.add('visible');
389+
// Update key tip after keyboard is rendered
390+
if (keyTipsEnabled) {
391+
updateKeyTip();
392+
}
231393
} else {
232394
keyboardContainer.classList.remove('visible');
233395
}
@@ -330,6 +492,9 @@
330492
}
331493

332494
textContainer.innerHTML = html;
495+
496+
// Update key tip if enabled
497+
updateKeyTip();
333498
}
334499

335500
function escapeHtml(text) {
@@ -415,6 +580,7 @@
415580

416581
renderText();
417582
updateRealtimeStats();
583+
// updateKeyTip is called in renderText, so no need to call it here
418584
}
419585

420586
function handleKeyDown(e) {
@@ -529,6 +695,16 @@
529695
activeKeyTimeout = null;
530696
}
531697

698+
// Clear key tip if enabled
699+
if (currentTipKeyElement) {
700+
currentTipKeyElement.classList.remove('key-tip');
701+
currentTipKeyElement = null;
702+
}
703+
if (currentTipShiftElement) {
704+
currentTipShiftElement.classList.remove('key-tip');
705+
currentTipShiftElement = null;
706+
}
707+
532708
// Show typing container and hide completion screen and stats dashboard
533709
const typingTextContainer = document.querySelector('.typing-text-container');
534710
if (typingTextContainer) {
@@ -815,6 +991,16 @@ Generated: ${new Date().toLocaleString()}
815991
keyboardContainer.classList.remove('visible');
816992
}
817993

994+
// Clear key tip when dashboard is shown
995+
if (currentTipKeyElement) {
996+
currentTipKeyElement.classList.remove('key-tip');
997+
currentTipKeyElement = null;
998+
}
999+
if (currentTipShiftElement) {
1000+
currentTipShiftElement.classList.remove('key-tip');
1001+
currentTipShiftElement = null;
1002+
}
1003+
8181004
// Hide real-time stats when dashboard is shown
8191005
if (realtimeStatsContainer) {
8201006
realtimeStatsContainer.style.display = 'none';
@@ -909,6 +1095,16 @@ Generated: ${new Date().toLocaleString()}
9091095
keyboardContainer.classList.remove('visible');
9101096
}
9111097

1098+
// Clear key tip when completion screen is shown
1099+
if (currentTipKeyElement) {
1100+
currentTipKeyElement.classList.remove('key-tip');
1101+
currentTipKeyElement = null;
1102+
}
1103+
if (currentTipShiftElement) {
1104+
currentTipShiftElement.classList.remove('key-tip');
1105+
currentTipShiftElement = null;
1106+
}
1107+
9121108
// Hide real-time stats when completion screen is shown
9131109
if (realtimeStatsContainer) {
9141110
realtimeStatsContainer.style.display = 'none';

0 commit comments

Comments
 (0)