diff options
| author | srdusr <[email protected]> | 2025-12-18 19:21:00 +0200 |
|---|---|---|
| committer | srdusr <[email protected]> | 2025-12-18 19:21:00 +0200 |
| commit | b371f21989bfca13fe8cc9bd59e95f5614f63b0d (patch) | |
| tree | 4244c0d4a7000aa12cfee4e86f3ffcc6476bc4ec /web/src | |
| parent | 1740327557074df0c8b99635ee949e2540ac94d0 (diff) | |
| download | typerpunk-b371f21989bfca13fe8cc9bd59e95f5614f63b0d.tar.gz typerpunk-b371f21989bfca13fe8cc9bd59e95f5614f63b0d.zip | |
Fix the end screen layout, make the app responsive, document secrets
End screen
- There was an auto-fit routine that forced this screen into one viewport: it
shrank the graph to a 120px floor, trimmed the Play Again margin, then
capped the passage box at 80px with its own scrollbar. On a 650px window
that left the passage 80px tall and clipped, and the graph 120px, which are
the two things the screen exists to show. Removed. The page scrolls
instead, which is the right trade for a screen that is read rather than
acted on under time pressure.
- The screen is a flex column, so its children also shrank by default once
the content was taller than the viewport. The passage, graph, stat row and
standings no longer shrink, and the chart has a floor below which it stops
carrying information.
Bottom chrome
- The keyboard hint and the footer links were both fixed at bottom centre and
overlapped at every window size. The hint now sits above the footer.
- Normal-flow content could end up underneath the fixed footer and the corner
rails. One --bottom-chrome variable reserves that space on every screen.
- On a narrow screen the footer grows to the full width once its links wrap,
so at 375px it ran through both corner rails and covered Play Again, which
made the button unclickable. The chrome stacks there instead: rails on the
bottom line, footer above them, hint above that.
Mode picker
- It ran to the last pixel of the window at every size. It now keeps clear of
the bottom edge, and opens upward when a short window leaves more room
above than below.
Responsive
- Checked at nine viewports from 1920x1080 down to 375x667: no horizontal
overflow and nothing off-screen on the menu, the typing screen or the end
screen.
Configuration
- dotenvy searches upward from the working directory, so crates/server/.env
was only found when starting the server from that directory. The repository
root is tried as well, which is where it is usually started from.
- .env.example and the README explain where secrets belong: the environment,
a gitignored .env for local work, and EnvironmentFile or a platform secret
store in production. Also what to do if one is exposed.
- TYPERPUNK_ADMIN_USERNAME and TYPERPUNK_ENV are documented rather than left
to be discovered in the source.
Diffstat (limited to 'web/src')
| -rw-r--r-- | web/src/screens/endScreen.js | 50 | ||||
| -rw-r--r-- | web/src/screens/mainMenu.js | 22 | ||||
| -rw-r--r-- | web/src/styles.css | 77 |
3 files changed, 101 insertions, 48 deletions
diff --git a/web/src/screens/endScreen.js b/web/src/screens/endScreen.js index b03c306..2787f76 100644 --- a/web/src/screens/endScreen.js +++ b/web/src/screens/endScreen.js @@ -165,46 +165,16 @@ export function renderEndScreen(root, { stats, text, attribution, explanation, s // passage happened to be on screen when it was measured. This claws the // room back in stages instead, each one only kicking in if the last // wasn't enough, so the page itself never needs to scroll to reach - // Play Again: shrink the graph down to a floor, then trim the button's - // top margin, and only as a last resort (a passage long enough that even - // both of those can't absorb it - realistically only reachable by - // pasting the full text-mode word buffer instead of actually typing it - // in the time given) let the passage preview itself scroll internally, - // which keeps every control below it reachable without the page - // scrolling. Runs once after layout settles. - requestAnimationFrame(() => { - const doc = document.documentElement; - // Re-measured after every stage rather than tracked as a running - // subtraction - margin-collapse and sub-pixel rounding meant an - // assumed "reduced height X means Y px less overflow" estimate - // drifted from the DOM's actual scrollHeight, consistently - // undershooting by a fixed amount however much was cut. - const overflowNow = () => doc.scrollHeight - doc.clientHeight; - if (overflowNow() <= 0) return; - - const graphBox = root.querySelector('.graph-container'); - const MIN_GRAPH_HEIGHT = 120; - const graphHeight = graphBox.getBoundingClientRect().height; - if (graphHeight > MIN_GRAPH_HEIGHT) { - const reduceBy = Math.min(overflowNow(), graphHeight - MIN_GRAPH_HEIGHT); - graphBox.style.height = `${graphHeight - reduceBy}px`; - redraw(); - } - if (overflowNow() <= 0) return; - - const buttons = root.querySelector('.end-screen-buttons'); - const buttonsMargin = parseFloat(getComputedStyle(buttons).marginTop); - if (buttonsMargin > 8) { - const reduceBy = Math.min(overflowNow(), buttonsMargin - 8); - buttons.style.marginTop = `${buttonsMargin - reduceBy}px`; - } - if (overflowNow() <= 0) return; - - const textBox = root.querySelector('.end-screen-text'); - const textHeight = textBox.getBoundingClientRect().height; - textBox.style.maxHeight = `${Math.max(80, textHeight - overflowNow() - 4)}px`; - textBox.style.overflowY = 'auto'; - }); + // There was an auto-fit routine here that forced this screen into one + // viewport: it shrank the graph to a 120px floor, then trimmed the Play + // Again margin, then capped the passage box at 80px with its own + // scrollbar. On an ordinary laptop window that left the two things the + // screen exists to show - the passage you just typed and the graph of + // how you typed it - both too small to read, and the passage clipped + // rather than scrolled to. + // + // A results screen is read, not acted on under time pressure, so the + // page scrolls instead. Nothing is hidden and nothing is squeezed. const tooltip = document.createElement('div'); tooltip.className = 'chart-tooltip'; diff --git a/web/src/screens/mainMenu.js b/web/src/screens/mainMenu.js index 1a3ce87..2d27468 100644 --- a/web/src/screens/mainMenu.js +++ b/web/src/screens/mainMenu.js @@ -240,12 +240,26 @@ export function renderMainMenu(root, props) { function position() { const rect = groupEl.getBoundingClientRect(); const gap = 8; - const spaceBelow = window.innerHeight - rect.bottom - gap; + // Clear of the bottom edge: the corner rails and the footer live + // down there, and a panel that runs to the last pixel of the + // window reads as cut off rather than as a menu. + const bottomInset = 88; + const spaceBelow = window.innerHeight - rect.bottom - gap - bottomInset; + const spaceAbove = rect.top - gap - 16; + popoverEl.style.left = `${rect.left}px`; popoverEl.style.width = `${rect.width}px`; - popoverEl.style.top = `${rect.bottom + gap}px`; - popoverEl.style.bottom = 'auto'; - popoverEl.style.maxHeight = `${Math.max(100, spaceBelow)}px`; + // Opens upward when there is more room there, which is what a + // short window leaves. + if (spaceBelow < 220 && spaceAbove > spaceBelow) { + popoverEl.style.top = 'auto'; + popoverEl.style.bottom = `${window.innerHeight - rect.top + gap}px`; + popoverEl.style.maxHeight = `${Math.max(140, spaceAbove)}px`; + } else { + popoverEl.style.top = `${rect.bottom + gap}px`; + popoverEl.style.bottom = 'auto'; + popoverEl.style.maxHeight = `${Math.max(140, spaceBelow)}px`; + } } chevronEl.addEventListener('click', e => { e.stopPropagation(); diff --git a/web/src/styles.css b/web/src/styles.css index c56ac27..3e4aea4 100644 --- a/web/src/styles.css +++ b/web/src/styles.css @@ -12,6 +12,9 @@ --header-offset: 3.5rem; /* App padding variable so we can compensate where needed */ --app-padding: 1rem; + /* Room at the foot of every screen for the fixed footer and the corner + rails, so normal-flow content never ends up underneath them. */ + --bottom-chrome: 5.5rem; --record-color: #ffd166; --warning-color: #ffb454; --player-2: #4ea3ff; @@ -266,6 +269,7 @@ body { margin-top: calc(var(--header-offset) - var(--app-padding)); min-height: calc(100vh - var(--header-offset)); box-sizing: border-box; + padding-bottom: var(--bottom-chrome); display: flex; flex-direction: column; align-items: center; @@ -1571,7 +1575,7 @@ body { away first if a genuinely tall passage would otherwise force a scrollbar, so it never fights that logic, it just guarantees normal breathing room in the common case. */ - padding-bottom: 2rem; + padding-bottom: var(--bottom-chrome); box-sizing: border-box; } @@ -2039,7 +2043,7 @@ body::-webkit-scrollbar, .app::-webkit-scrollbar, #root::-webkit-scrollbar { width: 100% !important; overflow-x: auto !important; margin: 0 auto 1rem auto !important; - height: 160px !important; + height: 220px !important; } .end-screen-buttons { position: static !important; @@ -2067,7 +2071,7 @@ body::-webkit-scrollbar, .app::-webkit-scrollbar, #root::-webkit-scrollbar { padding: 0 0.5rem !important; } .graph-container { - height: 180px !important; + height: 200px !important; padding: 0.5rem !important; } .endscreen-side-stat.wpm, .endscreen-side-stat.acc { @@ -2397,7 +2401,7 @@ body::-webkit-scrollbar, .app::-webkit-scrollbar, #root::-webkit-scrollbar { title and the buttons where it competed with them for attention. */ .menu-key-hint { position: fixed; - bottom: 1.25rem; + bottom: 3.25rem; left: 50%; transform: translateX(-50%); font-size: 0.75rem; @@ -3081,3 +3085,68 @@ body::-webkit-scrollbar, .app::-webkit-scrollbar, #root::-webkit-scrollbar { border-radius: 4px; padding: 0.1rem 0.35rem; } + + +/* The end screen is a flex column, so every child shrinks by default once the + content is taller than the viewport. That squeezed the two things the + screen exists to show: on a 650px window the passage came out 80px tall and + the graph 120px, both unreadable, and the passage was clipped rather than + scrolled to. They keep their size now and the page scrolls instead, which + is the correct trade for a screen you read rather than one you act on. */ +.end-screen-text, +.end-screen-graph-row, +.end-screen-stat-row, +.end-screen-attribution, +.code-explainer, +.mp-standings, +.end-screen-buttons { + flex-shrink: 0; +} + +/* Below this the chart stops carrying information. */ +.graph-container { + min-height: 180px; +} + +@media (max-height: 600px) { + .graph-container { + min-height: 150px; + } +} + + +/* Bottom chrome on a narrow screen. + The footer is centred and grows to the full width once the links wrap, so + at 375px it ran straight through both corner rails and covered Play Again, + which made the button unclickable. Stacked instead: rails on the bottom + line, footer above them, keyboard hint above that, and the reserved space + grows to match. */ +@media (max-width: 700px) { + :root { + --bottom-chrome: 12rem; + } + + .site-footer { + bottom: 4rem; + flex-wrap: wrap; + justify-content: center; + max-width: calc(100vw - 2rem); + row-gap: 0.2rem; + } + + /* Clears the footer even when its links wrap onto a second line, which + they do below about 400px. */ + .menu-key-hint { + bottom: 9.5rem; + } + + /* The narrow-width rule above sets this to 0.5rem, which is what left the + content underneath the chrome in the first place. */ + .end-screen { + padding-bottom: var(--bottom-chrome) !important; + } + + .stats-screen { + padding-bottom: var(--bottom-chrome); + } +} |