From 0d3864bcebcdba1b1743367d0f84389fd53e37e1 Mon Sep 17 00:00:00 2001
From: srdusr <99972264+srdusr@users.noreply.github.com>
Date: Sat, 13 Sep 2025 21:11:00 +0200
Subject: Rework the end screen figures, mode picker, corners and race view
End screen
- Every figure is now a dim label directly over its value, the two centred on
each other, grouped by what they qualify: RAW and PB under WPM, ERR and
CONSISTENCY under ACC, KEYSTROKES and CHARACTERS either side of TIME. The
row is a grid so TIME sits on the graph's exact centre line rather than
drifting with its neighbours' widths.
- Errors go back onto the wpm line. Their own y axis implied a magnitude a
mistake does not have; what matters is when one happened.
- Graph hover reads "wpm ... raw ... time" - the figures first, the second
they happened in as the qualifier.
Mode picker
- Opens from the Single Player button itself, and choosing a mode starts it:
picking what to type and starting it are one action. The control has now
been a chevron notched into that button, a caption between the two buttons,
a pill above them and a chip row below them; as the button's own menu it
needs no separate real estate at all.
Corners
- Settings and Store move to the bottom-left. The top-left is the wordmark's
alone.
- The global racer count is gone from the home screen - it is not something
you can act on there. The Friends control carries "N online" instead, which
is.
- Presence: users gain a last_seen column, touched at most once a minute per
active user on any authenticated request, and the friends list reports who
has been seen inside a five-minute window.
Multiplayer race view
- The standings move to the middle of the screen, the space the end screen's
graph occupies, and now include your own row rather than opponents only.
Pinned to the top-left corner they put what you are racing against in your
peripheral vision and left out the one bar you most need to see.
Programming mode
- Snippets carry an explanation of what the code does. Shown under the passage
while you type in single player; in multiplayer that space belongs to the
standings, so it waits for the end screen, where it appears either way.
---
web/src/screens/endScreen.js | 24 +++++----
web/src/screens/mainMenu.js | 97 +++++++++++++-----------------------
web/src/screens/multiplayerScreen.js | 24 ++++++---
web/src/screens/typingGame.js | 5 +-
4 files changed, 69 insertions(+), 81 deletions(-)
(limited to 'web/src/screens')
diff --git a/web/src/screens/endScreen.js b/web/src/screens/endScreen.js
index cd2a41d..551a91c 100644
--- a/web/src/screens/endScreen.js
+++ b/web/src/screens/endScreen.js
@@ -8,7 +8,7 @@ import { renderTopRail } from '../topRail.js';
import { submitTestResult } from '../auth.js';
import { detectDeviceType } from '../deviceDetect.js';
-export function renderEndScreen(root, { stats, text, attribution, userInput, charTimings, keypressHistory, modeKey, onPlayAgain, onMainMenu, onShowStats, onShowPlaceholder, onShowAccount, onShowLeaderboard, onShowFriends, onShowMultiplayer, onShowStore }) {
+export function renderEndScreen(root, { stats, text, attribution, explanation, userInput, charTimings, keypressHistory, modeKey, onPlayAgain, onMainMenu, onShowStats, onShowPlaceholder, onShowAccount, onShowLeaderboard, onShowFriends, onShowMultiplayer, onShowStore }) {
// Monkeytype's four-way split. "Extra" is anything typed past the end of
// the passage, "missed" is passage left untyped - neither is visible in
// a plain correct/incorrect pair, and the two mean very different things
@@ -43,33 +43,37 @@ export function renderEndScreen(root, { stats, text, attribution, userInput, cha
}
// Don't announce "new best" on a mode's very first-ever run - there's
// nothing to have beaten yet, so it just silently becomes the baseline.
+ // Sits under WPM alongside RAW, in the same label-over-value shape as
+ // every other secondary figure rather than as a floating badge.
const pbLine = !modeKey || previous == null ? '' : isNewBest
- ? '
@@ -204,11 +197,6 @@ export function renderMainMenu(root, props) {
-
-
-
-
-
${profile.testsCompleted > 0 ? `
avg ${profile.averageWpm}
@@ -224,23 +212,7 @@ export function renderMainMenu(root, props) {
attachTooltips(root);
const cleanupTheme = renderTopRail(root, { onShowAccount, onShowFriends });
- // The Friends badge belongs to the top rail. The player count is this
- // screen's own, since the menu builds its corner clusters inline.
- const onlineEl = root.querySelector('.corner-rail-bottom-left [data-badge="online"]');
- const mpIconBtn = root.querySelector('.corner-rail-bottom-left [data-action="mode-multiplayer"]');
- function paintCounts({ online }) {
- if (onlineEl) {
- onlineEl.hidden = !online;
- onlineEl.textContent = online === 1 ? '1 racing' : `${online} racing`;
- }
- if (mpIconBtn) {
- mpIconBtn.dataset.tooltip = !online
- ? 'Multiplayer'
- : (online === 1 ? 'Multiplayer - 1 player racing now' : `Multiplayer - ${online} players racing now`);
- }
- }
- paintCounts(getCounts());
- const unsubscribeCounts = onCountsChange(paintCounts);
+ // Friends-online lives in the top rail beside the Friends control.
// Clicking a chevron reveals every mode as a direct pick, rather than
// cycling blind through them one click at a time - Custom and the text
@@ -282,32 +254,34 @@ export function renderMainMenu(root, props) {
setupDropdown(
root.querySelector('.sp-group'),
- root.querySelector('.sp-chevron'),
+ root.querySelector('[data-action="pick-mode"]'),
root.querySelector('.sp-popover'),
- null,
+ root.querySelector('.sp-caption'),
);
- root.querySelectorAll('.sp-popover .mode-popover-item[data-mode], .mode-bar .mode-chip[data-mode]').forEach(item => {
- item.addEventListener('click', () => onSelectCategory(item.dataset.mode));
+ root.querySelectorAll('.sp-popover .mode-popover-item[data-mode]').forEach(item => {
+ item.addEventListener('click', () => {
+ const mode = item.dataset.mode;
+ // Custom needs its text before it can start: with none loaded the
+ // picker hands off to the paste/upload panel instead.
+ if (mode === 'custom') {
+ onSelectCategory(mode);
+ return;
+ }
+ onPickAndStart(mode);
+ });
});
root.querySelector('[data-action="mode-lyrics"]').addEventListener('click', onShowLyrics);
- // Two of these now: the big button and the bottom-left icon beside the
- // live player count.
- root.querySelectorAll('[data-action="mode-multiplayer"]').forEach(el => el.addEventListener('click', onShowMultiplayer));
+ root.querySelector('[data-action="mode-multiplayer"]').addEventListener('click', onShowMultiplayer);
const startBtn = root.querySelector('.sp-group .start-main');
const panel = root.querySelector('.custom-panel');
- startBtn.addEventListener('click', () => {
- if (isCustom && !customText) {
- panel.hidden = false;
- return;
- }
- if (isCustom) {
- onStartCustom();
- } else {
- onStartGame();
- }
- });
+ // No click handler of its own: setupDropdown above owns this button and
+ // opens the mode picker with it. Starting happens when a mode is chosen.
+
+ // Landing on Custom with nothing loaded goes straight to the paste/upload
+ // panel - there is nothing to start until text exists.
+ if (isCustom && !customText) panel.hidden = false;
// Matches the "Enter to start" hint in the bottom-right corner. Skipped
// while typing into a text field or with any popover/panel open, so it
@@ -540,7 +514,6 @@ export function renderMainMenu(root, props) {
return () => {
cleanupTheme();
- unsubscribeCounts();
document.removeEventListener('keydown', handleEnterToStart);
outsideClickHandlers.forEach(h => document.removeEventListener('click', h));
};
diff --git a/web/src/screens/multiplayerScreen.js b/web/src/screens/multiplayerScreen.js
index 0de0182..d2d2a2b 100644
--- a/web/src/screens/multiplayerScreen.js
+++ b/web/src/screens/multiplayerScreen.js
@@ -210,17 +210,25 @@ export function renderMultiplayerScreen(root, { onBack, onFinish, onShowStats, o
}
const opponents = document.createElement('div');
- opponents.className = 'mp-opponents';
+ opponents.className = 'mp-racers';
const progressById = {};
+ // Everyone in the room, yourself included. Showing only opponents left
+ // you guessing where you actually stood: your own bar is the one you
+ // are measuring the others against. The server echoes your Progress
+ // back to you along with everyone else's, so your row is fed the same
+ // way theirs are.
function paintOpponents() {
const colors = racerColors(players, myId);
- opponents.innerHTML = players.filter(p => p.id !== myId).map(p => `
-