From cd8ba1c8aad1a6a3adeca0b870622df811449c00 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=90=D0=BD=D1=82=D0=BE=D0=BD=20=D0=A2=D1=80=D0=BE=D1=88?= =?UTF-8?q?=D0=B8=D0=BD?= Date: Tue, 25 Aug 2026 16:56:28 +0300 Subject: [PATCH] bundle compact symbol font subsets, sw install hardening, back-exit history collapse, lang/ticker/theory fixes --- NotoSansMath-subset.woff2 | Bin 0 -> 3408 bytes NotoSansSymbols2-subset.woff2 | Bin 0 -> 4456 bytes app.js | 91 +++++++++++++++++++++++++++++----- index.html | 6 +-- styles.css | 32 ++++++++++-- sw.js | 15 +++++- 6 files changed, 122 insertions(+), 22 deletions(-) create mode 100644 NotoSansMath-subset.woff2 create mode 100644 NotoSansSymbols2-subset.woff2 diff --git a/NotoSansMath-subset.woff2 b/NotoSansMath-subset.woff2 new file mode 100644 index 0000000000000000000000000000000000000000..7cddac98fed6413b7b60f24c219fd78b2aebd3f1 GIT binary patch literal 3408 zcmV-W4X^TdPew8T0RR9101Z$85&!@I02yEa01WT|0RR9100000000000000000000 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 ulr0KAAIJ^*^?#fHpiTmRe { + btn.textContent = t(MODES[i].labelKey); + }); + [...document.getElementById('setup-timing').options].forEach((opt, i) => { + opt.textContent = TIMINGS[i].value === 0 ? t('noLimit') : TIMINGS[i].value + ' ' + t('sec'); + }); + break; + case 'screen-theory-system': + renderTheorySystem(); + break; + case 'screen-theory-content': + document.getElementById('theory-title').textContent = systemName(lastTheoryKey); + break; + case 'screen-game': + if (game.system && game.range) { + document.getElementById('score-system-range').textContent = systemName(game.system) + ': ' + game.range.label; + } + break; + case 'screen-results': { + const completed = game.currentRound >= game.rounds; + const total = completed ? game.rounds : game.currentRound; + const pct = total > 0 ? Math.round((game.correctCount / total) * 100) : 0; + document.getElementById('results-summary').innerHTML = + '
' + game.correctCount + '/' + total + '
' + + '
' + t('pctCorrect', {pct: pct}) + '
'; + const labelEl = document.getElementById('results-mistakes-label'); + if (game.mistakes.length > 0 || labelEl.style.display !== 'none') { + labelEl.textContent = t('mistakes'); + } + const congratsEl = document.getElementById('results-congrats'); + if (congratsEl.textContent) { + congratsEl.textContent = t('congratulations'); + } + break; + } + } +} + const SYSTEM_RANGES = { binary: [ { label: '2\u2070 \u2013 2\u00B3', min: 1, max: 8 }, @@ -188,6 +239,7 @@ const SYSTEM_GROUPS = { }; const theoryCache = {}; +let lastTheoryKey = null; const MODES = [ { labelKey: 'choose', value: 'choice' }, @@ -222,11 +274,19 @@ let game = { }; let popstateHandled = false; +let navDepth = 0; function showScreen(id) { document.querySelectorAll('.screen').forEach(s => s.classList.remove('active')); document.getElementById('screen-' + id).classList.add('active'); - if (id !== 'menu' && !popstateHandled) { + if (id === 'menu') { + if (navDepth > 0) { + const n = navDepth; + navDepth = 0; + history.go(-n); + } + } else if (!popstateHandled) { + navDepth++; history.pushState({ screen: id }, ''); } popstateHandled = false; @@ -354,7 +414,7 @@ const MARK_INCORRECT = '\u2717 '; function startGame() { lastSetupState = { ...setupState }; game.system = setupState.system; - game.range = setupState.rangeData || RANGES[setupState.range]; + game.range = setupState.rangeData; game.mode = MODES[setupState.mode]?.value ?? setupState.mode; game.timing = TIMINGS[setupState.timing]?.value ?? setupState.timing; game.rounds = ROUNDS[setupState.rounds]; @@ -754,12 +814,11 @@ function renderTheorySystem() { } async function showTheoryContent(key) { + lastTheoryKey = key; const html = await loadTheory(key); - if (!html) return; - document.getElementById('theory-title').textContent = systemName(key); - document.getElementById('theory-scroll').innerHTML = html; - + document.getElementById('theory-scroll').innerHTML = + html || '
' + t('theoryUnavailable') + '
'; showScreen('theory-content'); } @@ -790,9 +849,6 @@ function getPreferredTheme() { return window.matchMedia('(prefers-color-scheme: light)').matches ? 'light' : 'dark'; } -const isLightTheme = getPreferredTheme() === 'light'; -const TICKER_COLORS = isLightTheme ? TICKER_COLORS_LIGHT : TICKER_COLORS_DARK; - const TICKER_SYSTEMS = [ { key: 'binary', min: 1, max: 255, suffix: toSubscript(2) }, { key: 'base3', min: 1, max: 81, suffix: toSubscript(3) }, @@ -806,6 +862,9 @@ const TICKER_SYSTEMS = [ ]; function generateTickerText() { + const palette = document.documentElement.dataset.theme === 'light' + ? TICKER_COLORS_LIGHT + : TICKER_COLORS_DARK; const pairs = []; let lastKey = null; let lastColor = null; @@ -818,7 +877,7 @@ function generateTickerText() { let color; do { - color = TICKER_COLORS[Math.floor(Math.random() * TICKER_COLORS.length)]; + color = palette[Math.floor(Math.random() * palette.length)]; } while (color === lastColor); lastColor = color; const num = sys.min + Math.floor(Math.random() * (sys.max - sys.min + 1)); @@ -834,8 +893,10 @@ function generateTickerText() { return pairs.join('\u00A0\u00A0\u00A0\u00A0\u00A0\u00A0'); } -const tickerText = generateTickerText(); -document.getElementById('ticker').innerHTML = tickerText + '\u00A0\u00A0\u00A0\u00A0\u00A0\u00A0' + tickerText; +function refreshTicker() { + const tickerText = generateTickerText(); + document.getElementById('ticker').innerHTML = tickerText + '\u00A0\u00A0\u00A0\u00A0\u00A0\u00A0' + tickerText; +} const langSelect = document.getElementById('lang-select'); langSelect.value = LANG; @@ -843,6 +904,7 @@ langSelect.addEventListener('change', () => { LANG = langSelect.value; localStorage.setItem('numnum-lang', LANG); applyTranslations(); + refreshDynamicText(); }); applyTranslations(); @@ -850,6 +912,7 @@ applyTranslations(); function applyTheme(theme) { document.documentElement.dataset.theme = theme; document.getElementById('theme-toggle').textContent = theme === 'dark' ? '\u2600' : '\u263E'; + refreshTicker(); } applyTheme(getPreferredTheme()); @@ -918,7 +981,8 @@ document.getElementById('btn-options-back').addEventListener('click', () => { }); document.getElementById('btn-play').addEventListener('click', () => { - if (setupState.mode === undefined || setupState.timing === undefined || setupState.rounds === undefined) { + if (!setupState.system || !setupState.rangeData || + setupState.mode === undefined || setupState.timing === undefined || setupState.rounds === undefined) { showSetupMessage(t('pleaseSelect')); return; } @@ -997,6 +1061,7 @@ const BACK_MAP = { }; window.addEventListener('popstate', () => { + navDepth = Math.max(0, navDepth - 1); const active = document.querySelector('.screen.active'); if (!active) return; diff --git a/index.html b/index.html index 777268a..09b3caa 100644 --- a/index.html +++ b/index.html @@ -10,7 +10,7 @@ - +
@@ -193,10 +193,10 @@
- + diff --git a/styles.css b/styles.css index b052d94..fb72c31 100644 --- a/styles.css +++ b/styles.css @@ -73,13 +73,31 @@ html[data-theme="light"] { font-display: swap; } +@font-face { + font-family: 'Noto Sans Symbols 2'; + src: url('NotoSansSymbols2-subset.woff2') format('woff2'); + font-weight: normal; + font-style: normal; + font-display: swap; + unicode-range: U+232B, U+23F1, U+2600, U+2713, U+2717, U+2801, U+2803, U+2809-280B, U+2811, U+2813, U+2819-281B, U+283C, U+2B95; +} + +@font-face { + font-family: 'Noto Sans Symbols 2'; + src: url('NotoSansMath-subset.woff2') format('woff2'); + font-weight: normal; + font-style: normal; + font-display: swap; + unicode-range: U+2295-2296, U+263E; +} + html, body { height: 100%; overflow: hidden; } body { - font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Noto Sans Symbols 2', sans-serif; background: var(--bg); color: var(--text); -webkit-tap-highlight-color: transparent; @@ -189,7 +207,7 @@ body { white-space: nowrap; color: var(--text-dim); font-size: 1.3rem; - font-family: monospace; + font-family: 'Noto Sans Symbols 2', monospace; animation: ticker-scroll 40s linear infinite; } @@ -455,7 +473,7 @@ body { } .number-display.mono { - font-family: monospace; + font-family: 'Noto Sans Symbols 2', monospace; } .ticker .slavonic-font { @@ -795,13 +813,19 @@ body { background: var(--bg-card); border-radius: 8px; padding: 16px; - font-family: monospace; + font-family: 'Noto Sans Symbols 2', monospace; font-size: 1.1rem; margin: 12px 0; text-align: center; letter-spacing: 2px; } +.theory-unavailable { + text-align: center; + opacity: 0.65; + padding: 24px 0; +} + .theory-scroll .slavonic-font { font-family: 'Ponomar', serif; font-size: 1.4rem; diff --git a/sw.js b/sw.js index fd6c2b8..016558a 100644 --- a/sw.js +++ b/sw.js @@ -1,4 +1,4 @@ -const CACHE_NAME = 'numnum-v27'; +const CACHE_NAME = 'numnum-v28'; const ASSETS = [ '/', '/index.html', @@ -9,6 +9,9 @@ const ASSETS = [ '/icon.svg', '/icon-192.png', '/icon-512.png', + '/Ponomar-Regular.woff', + '/NotoSansSymbols2-subset.woff2', + '/NotoSansMath-subset.woff2', '/theory/base3.en.html', '/theory/base3.ru.html', '/theory/binary.en.html', @@ -33,7 +36,15 @@ const ASSETS = [ self.addEventListener('install', (e) => { e.waitUntil( - caches.open(CACHE_NAME).then(cache => cache.addAll(ASSETS)) + caches.open(CACHE_NAME).then(cache => + Promise.allSettled(ASSETS.map(url => cache.add(url))) + ).then(results => { + results.forEach((r, i) => { + if (r.status === 'rejected') { + console.warn('SW precache failed:', ASSETS[i], r.reason); + } + }); + }) ); self.skipWaiting(); });