From c2d11412ff7d438f87c9bdc5633157cdf5834aeb Mon Sep 17 00:00:00 2001 From: Mohamed Elsherif Date: Fri, 11 Sep 2026 20:43:11 -0700 Subject: [PATCH 1/5] Add game-native about page --- about.css | 86 ++++++++++++++++++++++++++++++ about.html | 91 ++++++++++++++++++++++++++++++++ arcade.css | 3 +- arcade.js | 6 ++- docs/architecture.md | 2 +- service-worker.js | 4 +- tests/about-page.test.js | 44 +++++++++++++++ tests/game-events.test.js | 2 +- tests/online-rooms.test.js | 2 +- tests/shared-game-assets.test.js | 1 + 10 files changed, 234 insertions(+), 7 deletions(-) create mode 100644 about.css create mode 100644 about.html create mode 100644 tests/about-page.test.js diff --git a/about.css b/about.css new file mode 100644 index 0000000..e577ebb --- /dev/null +++ b/about.css @@ -0,0 +1,86 @@ +:root { + color-scheme:light; + --page:#fff4d6; + --ink:#192541; + --muted:#58647a; + --panel:#fff9e8; + --line:#192541; + --accent:#765cff; + --pink:#ff4fa3; + --mint:#6ef2d0; + --yellow:#ffd84d; + --sky:#56a8ff; + --shadow:#192541; +} +:root[data-theme="dark"] { color-scheme:dark;--page:#0a1020;--ink:#fff4d6;--muted:#b7c2dc;--panel:#111a33;--line:#fff4d6;--shadow:#030712; } +* { box-sizing:border-box; } +body { min-height:100vh;margin:0;color:var(--ink);background:linear-gradient(90deg,#ffffff06 1px,transparent 1px),linear-gradient(#ffffff06 1px,transparent 1px),radial-gradient(circle at 8% 0,#765cff38,transparent 34rem),var(--page);background-size:32px 32px,32px 32px,auto,auto;font-family:var(--pixel-body,ui-monospace,"SFMono-Regular",Consolas,monospace);overflow-x:hidden; } +a { color:inherit; } +.about-shell { width:min(1120px,calc(100% - 40px));margin:0 auto;padding:clamp(38px,6vw,78px) 0 72px; } +.about-hero { display:grid;grid-template-columns:minmax(0,1.45fr) minmax(280px,.55fr);gap:clamp(32px,6vw,76px);align-items:center;margin-bottom:52px; } +.eyebrow,.panel-kicker { margin:0 0 18px;color:var(--pink);font:700 12px/1.2 var(--pixel-font,ui-monospace,monospace);letter-spacing:.1em;text-transform:uppercase; } +.eyebrow { display:inline-block;padding:8px 12px;color:#192541;background:var(--yellow);border:2px solid var(--line);box-shadow:4px 4px 0 var(--line); } +h1,h2,.player-card strong,.button,.resource-links span,.social-grid strong { font-family:var(--pixel-font,ui-monospace,monospace); } +h1 { max-width:740px;margin:0;font-size:clamp(40px,7vw,76px);font-weight:700;letter-spacing:-.065em;line-height:1.08;text-wrap:balance;text-shadow:5px 5px 0 var(--accent); } +h1 span { color:var(--pink); } +.lede { max-width:720px;margin:28px 0 0;color:var(--muted);font-size:clamp(17px,2vw,20px);line-height:1.65; } +.hero-actions { display:flex;flex-wrap:wrap;gap:14px;margin-top:30px; } +.button { display:inline-flex;align-items:center;justify-content:center;gap:9px;min-height:48px;padding:10px 16px;border:2px solid var(--line);font-size:11px;font-weight:700;text-decoration:none;box-shadow:4px 4px 0 var(--line);transition:transform 140ms steps(2,end),box-shadow 140ms steps(2,end); } +.button-primary { color:#192541;background:var(--mint); } +.button-secondary { background:var(--panel); } +.button:hover { transform:translate(4px,4px);box-shadow:0 0 0 var(--line); } +.button:focus-visible,.resource-links a:focus-visible,.patent-list a:focus-visible,.social-grid a:focus-visible { outline:3px solid var(--mint);outline-offset:4px; } +.player-card { position:relative;padding:28px;background:var(--panel);border:3px solid var(--line);box-shadow:8px 8px 0 var(--pink),12px 12px 0 var(--line);transform:rotate(1.5deg); } +.player-card::after { position:absolute;top:14px;right:14px;width:42px;height:42px;background:linear-gradient(90deg,var(--yellow) 0 30%,transparent 30% 70%,var(--yellow) 70%),linear-gradient(var(--yellow) 0 30%,transparent 30% 70%,var(--yellow) 70%);content:"";opacity:.8; } +.player-label { margin:0 0 10px;color:var(--pink);font-size:11px;font-weight:800;letter-spacing:.12em;text-transform:uppercase; } +.player-card>strong { display:block;padding-right:46px;font-size:clamp(19px,3vw,27px); } +.player-card dl { display:grid;gap:0;margin:24px 0 0; } +.player-card dl div { display:grid;grid-template-columns:90px 1fr;gap:12px;padding:12px 0;border-top:1px solid color-mix(in srgb,var(--line) 22%,transparent); } +.player-card dt { color:var(--muted);font-size:12px;text-transform:uppercase; } +.player-card dd { margin:0;font-size:14px;font-weight:800; } +.about-grid { display:grid;grid-template-columns:1.25fr .75fr;gap:24px; } +.panel { position:relative;padding:clamp(25px,4vw,40px);background:var(--panel);border:3px solid var(--line);box-shadow:7px 7px 0 var(--accent),11px 11px 0 var(--line); } +.panel::before { position:absolute;top:0;right:0;left:0;height:8px;background:repeating-linear-gradient(90deg,var(--accent) 0 20px,color-mix(in srgb,var(--accent) 42%,transparent) 20px 28px);content:""; } +.panel h2,.social-section h2 { margin:0;font-size:clamp(26px,4vw,38px);letter-spacing:-.045em;line-height:1.2; } +.panel>p:not(.panel-kicker) { margin:20px 0 0;color:var(--muted);font-size:16px;line-height:1.7; } +.career-path { display:grid;grid-template-columns:repeat(5,minmax(0,1fr));gap:8px;margin:30px 0 0;padding:0;list-style:none; } +.career-path li { position:relative;padding-top:19px;text-align:center; } +.career-path li::before { position:absolute;top:3px;right:50%;left:-58%;height:3px;background:var(--accent);content:""; } +.career-path li:first-child::before { left:50%; } +.career-path li::after { position:absolute;top:0;left:calc(50% - 5px);width:10px;height:10px;background:var(--mint);border:2px solid var(--line);content:""; } +.career-path span { font-size:12px;font-weight:800; } +.community-panel { --accent:var(--mint); } +.resource-links { display:grid;gap:12px;margin-top:28px; } +.resource-links a { display:grid;grid-template-columns:1fr auto;gap:5px 12px;padding:15px;color:inherit;background:color-mix(in srgb,var(--accent) 9%,transparent);border:1px solid color-mix(in srgb,var(--line) 28%,transparent);text-decoration:none; } +.resource-links span { font-size:13px; } +.resource-links small { grid-column:1;color:var(--muted);font-size:12px;line-height:1.45; } +.resource-links b { grid-area:1/2/3;font-size:20px;align-self:center; } +.patent-panel { --accent:var(--yellow);display:grid;grid-template-columns:.4fr 1.6fr;gap:28px;margin-top:30px; } +.patent-list { display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:14px; } +.patent-list a { display:flex;flex-direction:column;gap:10px;min-height:170px;padding:18px;background:color-mix(in srgb,var(--yellow) 12%,transparent);border:2px solid var(--line);text-decoration:none; } +.patent-list small { color:var(--muted);font-size:11px;font-weight:800;text-transform:uppercase; } +.patent-list span { flex:1;font-size:15px;font-weight:800;line-height:1.45; } +.patent-list b { color:var(--accent);font-size:12px; } +.patent-list i { font-style:normal; } +.social-section { margin-top:58px; } +.social-grid { display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:14px;margin-top:24px; } +.social-grid a { display:grid;grid-template-columns:38px 1fr;gap:3px 11px;align-items:center;min-height:92px;padding:16px;background:var(--panel);border:2px solid var(--line);box-shadow:4px 4px 0 var(--line);text-decoration:none;transition:transform 140ms steps(2,end),box-shadow 140ms steps(2,end); } +.social-grid a:hover { transform:translate(3px,3px);box-shadow:1px 1px 0 var(--line); } +.social-grid a>span { grid-row:1/3;display:grid;width:38px;height:38px;place-items:center;color:#192541;background:var(--sky);border:2px solid var(--line);font-size:13px;font-weight:950; } +.social-grid a:nth-child(2n)>span { background:var(--mint); } +.social-grid a:nth-child(3n)>span { background:var(--pink); } +.social-grid strong { font-size:12px; } +.social-grid small { color:var(--muted);font-size:11px; } +:root[data-arcade-theme="cabinet"] .about-shell { width:min(1240px,calc(100% - 32px)); } +:root[data-arcade-theme="cabinet"] :is(.player-card,.panel) { box-shadow:5px 5px 0 var(--yellow); } +:root[data-arcade-theme="calm"] { --page:#eef4f1;--ink:#223a33;--muted:#61746d;--panel:#f8fbf9;--line:#527068;--accent:#356f61;--pink:#9b5548;--mint:#789d90;--yellow:#d9b968;--sky:#668e9b;--shadow:#263c34; } +:root[data-arcade-theme="calm"][data-color-mode="dark"] { --page:#111917;--ink:#e5efeb;--muted:#9fb0a9;--panel:#18231f;--line:#91a59d;--shadow:#000; } +:root[data-arcade-theme="calm"] body { background:var(--page);font-family:Georgia,"Times New Roman",serif; } +:root[data-arcade-theme="calm"] .about-shell { width:min(1020px,calc(100% - 48px)); } +:root[data-arcade-theme="calm"] :is(h1,h2,.player-card strong,.button,.resource-links span,.social-grid strong) { font-family:Georgia,"Times New Roman",serif; } +:root[data-arcade-theme="calm"] h1 { text-shadow:none; } +:root[data-arcade-theme="calm"] :is(.player-card,.panel,.social-grid a) { border-width:1px;box-shadow:none;transform:none; } +:root[data-arcade-theme="calm"] .panel::before { height:3px; } +@media (max-width:800px) { .about-hero,.about-grid,.patent-panel { grid-template-columns:1fr; } .player-card { width:min(100%,520px);transform:none; } .social-grid { grid-template-columns:repeat(2,minmax(0,1fr)); } } +@media (max-width:520px) { .about-shell,:root[data-arcade-theme="calm"] .about-shell { width:min(100% - 28px,1120px);padding-top:32px; } .about-hero { margin-bottom:38px; } .hero-actions { flex-direction:column; } .panel { padding:26px 20px;box-shadow:5px 5px 0 var(--accent),8px 8px 0 var(--line); } .career-path { grid-template-columns:1fr;gap:0; } .career-path li { padding:0 0 14px 25px;text-align:left; } .career-path li::before { top:0;bottom:0;right:auto;left:5px;width:3px;height:auto; } .career-path li:first-child::before { left:5px;top:5px; } .career-path li:last-child::before { bottom:calc(100% - 5px); } .career-path li::after { top:3px;left:0; } .patent-list,.social-grid { grid-template-columns:1fr; } .patent-list a { min-height:0; } } +@media (prefers-reduced-motion:reduce) { .button,.social-grid a { transition:none; } } diff --git a/about.html b/about.html new file mode 100644 index 0000000..adb201c --- /dev/null +++ b/about.html @@ -0,0 +1,91 @@ + + + + + + + + About Mohamed Elsherif · JavaScript Playground + + + + + + +
+
+
+

Meet the maker

+

Built by Mohamed Elsherif.

+

I’m a software architect with more than 20 years in software engineering. I build systems, share what I learn, and still make time for a good JavaScript game.

+ +
+ +
+ +
+
+

Career map

+

Building services that scale

+

I work on Apple’s Services Architecture and Efficiency team, helping online services scale reliably and run efficiently. Earlier chapters of my career include Azure identity at Microsoft, integrations at Okta, enterprise mobility at VMware, and Metrics and Reporting infrastructure at Hulu.

+
    +
  1. Hulu
  2. +
  3. VMware
  4. +
  5. Okta
  6. +
  7. Microsoft
  8. +
  9. Apple
  10. +
+
+ + +
+ +
+
+

Granted achievements

+

Patents

+
+ +
+ + +
+ + + + diff --git a/arcade.css b/arcade.css index 25f1219..02dbdbc 100644 --- a/arcade.css +++ b/arcade.css @@ -10,6 +10,7 @@ body:not(.arcade-has-topbar)::before { display:block; height:env(safe-area-inset body.arena-fullscreen .arcade-topbar { display:none; } body.arena-fullscreen .arcade-account { display:none; } .arcade-account a, .arcade-account button { padding: 8px 11px; color: inherit; background: transparent; border: 0; border-radius: 9px; font: inherit; text-decoration: none; cursor: pointer; } +.arcade-account a[aria-current="page"] { text-decoration:underline; text-decoration-thickness:2px; text-underline-offset:5px; } .arcade-account button { color: #10271f; background: #83e6c3; } .arcade-appearance-button::before { margin-right:6px; content:'◐'; } .arcade-audio-button::before { margin-right:6px; content:'♪'; }.arcade-audio-button[data-audio-muted="true"]::before{content:'×';}.arcade-audio-button:disabled{cursor:not-allowed;opacity:.55} @@ -67,7 +68,7 @@ body.arena-fullscreen .arcade-account { display:none; } .result-share-cancel { color: #f7f8fc; background: rgba(255,255,255,.09); } .result-share-confirm { color: #10271f; background: #83e6c3; } @media (max-width: 760px) { .arcade-topbar-inner { align-items:flex-start; flex-direction:column; gap:11px; } .arcade-account { width:100%; justify-content:flex-start; overflow-x:auto; padding:2px 3px 5px; scrollbar-width:none; } .arcade-account::-webkit-scrollbar { display:none; } } -@media (max-width: 520px) { .arcade-topbar { padding-inline:12px; } .arcade-home > span:last-child { font-size:14px; } .arcade-account > a:first-of-type { display:none; } .arcade-account a,.arcade-account button { white-space:nowrap; } } +@media (max-width: 520px) { .arcade-topbar { padding-inline:12px; } .arcade-home > span:last-child { font-size:14px; } .arcade-account > .arcade-scores-link { display:none; } .arcade-account a,.arcade-account button { white-space:nowrap; } } @media (max-width: 520px) { .arcade-install-hint { right:max(12px,env(safe-area-inset-right)); bottom:max(12px,env(safe-area-inset-bottom)); left:auto; } .arcade-install-dialog form { padding:19px; } } @media (max-width: 480px) { .result-share-content { padding: 17px; } .result-share-actions button { flex: 1; } } @media (prefers-reduced-motion: reduce) { .arcade-install-hint { animation:none; } } diff --git a/arcade.js b/arcade.js index 6dc0eed..7c269a7 100644 --- a/arcade.js +++ b/arcade.js @@ -149,7 +149,7 @@ home.setAttribute('aria-label', 'JavaScript Playground home'); home.innerHTML = 'JavaScript Playground'; const account = document.createElement('nav'); - account.className = 'arcade-account'; account.setAttribute('aria-label', 'Arcade account and appearance'); + account.className = 'arcade-account'; account.setAttribute('aria-label', 'Arcade navigation and account'); topbarInner.append(home, account); topbar.append(topbarInner); const appearanceButton = document.createElement('button'); appearanceButton.type = 'button'; appearanceButton.className = 'arcade-appearance-button'; appearanceButton.textContent = 'Appearance'; const audioButton = document.createElement('button'); audioButton.type = 'button'; audioButton.className = 'arcade-audio-button'; audioButton.textContent = 'Sound'; audioButton.setAttribute('aria-haspopup', 'dialog'); @@ -316,7 +316,9 @@ if (game && audio) { updateAudioControls(); account.append(audioButton); } if (saveManager) account.append(saveManager.button); if (game) { const achievements = document.createElement('button'); achievements.type = 'button'; achievements.className = 'achievement-nav'; achievements.textContent = 'Achievements'; achievements.addEventListener('click', () => { achievementDialog.showModal(); loadAchievements().catch(() => {}); }); account.append(achievements); } - const scores = document.createElement('a'); scores.href = `${rootPath}profile.html#leaderboards`; scores.textContent = 'Top scores'; account.append(scores); + const about = document.createElement('a'); about.href = `${rootPath}about.html`; about.className = 'arcade-about-link'; about.textContent = 'About'; + if (location.pathname.endsWith('/about.html')) about.setAttribute('aria-current', 'page'); + const scores = document.createElement('a'); scores.href = `${rootPath}profile.html#leaderboards`; scores.className = 'arcade-scores-link'; scores.textContent = 'Top scores'; account.append(about, scores); if (currentUser) { const profile = document.createElement('a'); profile.href = `${rootPath}profile.html`; profile.textContent = currentUser.gamertag; const logout = document.createElement('button'); logout.type = 'button'; logout.textContent = 'Sign out'; logout.addEventListener('click', async () => { await api('/api/auth/logout', { method: 'POST', body: '{}' }); currentUser = null; render(); }); diff --git a/docs/architecture.md b/docs/architecture.md index d8bb5d8..f9e9ff0 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -21,7 +21,7 @@ flowchart LR ```mermaid flowchart TB subgraph Browser - Pages[Home, profile, game pages] + Pages[Home, profile, about, game pages] Arcade[arcade.js: account, appearance, results, achievements] Events[game-events.js: browser domain events] Shared[room UI, colors, sharing] diff --git a/service-worker.js b/service-worker.js index b308f35..ae1092f 100644 --- a/service-worker.js +++ b/service-worker.js @@ -1,9 +1,11 @@ 'use strict'; -const CACHE_NAME = 'js-playground-v14'; +const CACHE_NAME = 'js-playground-v15'; const APP_SHELL = [ './', './index.html', + './about.html', + './about.css', './arcade.css', './arcade.js', './fonts/Silkscreen-Regular.ttf', diff --git a/tests/about-page.test.js b/tests/about-page.test.js new file mode 100644 index 0000000..94ed3a9 --- /dev/null +++ b/tests/about-page.test.js @@ -0,0 +1,44 @@ +'use strict'; + +const assert = require('node:assert/strict'); +const fs = require('node:fs'); +const test = require('node:test'); + +const page = fs.readFileSync('about.html', 'utf8'); +const styles = fs.readFileSync('about.css', 'utf8'); +const shell = fs.readFileSync('arcade.js', 'utf8'); +const worker = fs.readFileSync('service-worker.js', 'utf8'); + +test('shared arcade navigation exposes the about page', () => { + assert.match(shell, /about\.href = `\$\{rootPath\}about\.html`/); + assert.match(shell, /about\.setAttribute\('aria-current', 'page'\)/); +}); + +test('about page presents the owner, work, community projects, and patents', () => { + assert.match(page, /Mohamed Elsherif/); + assert.match(page, /Apple’s Services Architecture and Efficiency team/); + assert.match(page, /AskDeveloper Podcast/); + assert.match(page, /US11531532B2/); + assert.match(page, /US11956232B2/); +}); + +test('about page links to the public profile and social destinations safely', () => { + for (const destination of ['bashmohandes.com', 'askdeveloper.com', 'linkedin.com', 'github.com', 'youtube.com', 'soundcloud.com', 'tiktok.com', 'x.com', 'facebook.com']) { + assert.match(page, new RegExp(`href="https://(?:www\\.)?${destination.replace('.', '\\.')}`)); + } + const externalLinks = page.match(/ { + assert.match(page, /src="theme-init\.js"[\s\S]*href="arcade\.css"[\s\S]*href="about\.css"/); + assert.match(styles, /data-arcade-theme="cabinet"/); + assert.match(styles, /data-arcade-theme="calm"/); + assert.match(styles, /@media \(max-width:520px\)/); +}); + +test('about page is available from the offline app shell', () => { + assert.match(worker, /'\.\/about\.html'/); + assert.match(worker, /'\.\/about\.css'/); +}); diff --git a/tests/game-events.test.js b/tests/game-events.test.js index 5abc292..6676a94 100644 --- a/tests/game-events.test.js +++ b/tests/game-events.test.js @@ -62,7 +62,7 @@ test('modern controllers publish domain facts without importing audio', () => { test('shared shell notifications and extension boundaries use the event contract', () => { const shell = read('arcade.js'), guide = read('docs/game-events.md'), adr = read('docs/adr/0014-browser-domain-event-bus.md'); - for (const page of ['index.html','profile.html']) assert.match(read(page), /scripts\/game-events\.js[\s\S]*arcade\.js/); + for (const page of ['index.html','profile.html','about.html']) assert.match(read(page), /scripts\/game-events\.js[\s\S]*arcade\.js/); for (const type of ['system:theme-changed','account:user-changed','achievement:unlocked','score:top']) assert.match(shell, new RegExp(type)); assert.match(read('profile.js'), /account:user-changed/); assert.match(guide, /not a command bus, durable event log, WebSocket protocol, or trust boundary/); diff --git a/tests/online-rooms.test.js b/tests/online-rooms.test.js index 4daa980..386497e 100644 --- a/tests/online-rooms.test.js +++ b/tests/online-rooms.test.js @@ -63,7 +63,7 @@ test('online pages cache-bust shared contract changes',()=>{ assert.match(page,new RegExp(`scripts/online-rooms\\.js\\?v=${rooms.CONTRACT_VERSION}`),`${file} must request the current shared contract`); } const worker=fs.readFileSync('service-worker.js','utf8'); - assert.match(worker,/CACHE_NAME = 'js-playground-v14'/); + assert.match(worker,/CACHE_NAME = 'js-playground-v15'/); }); test('Battle Tanks ignores obsolete sockets and owns one reconnect timer',()=>{ const app=fs.readFileSync('battle-tanks/scripts/app.js','utf8'); diff --git a/tests/shared-game-assets.test.js b/tests/shared-game-assets.test.js index a7b8b3d..edd6bdc 100644 --- a/tests/shared-game-assets.test.js +++ b/tests/shared-game-assets.test.js @@ -162,6 +162,7 @@ test('result sharing preserves user activation when the preview is confirmed', ( test('every arcade page opts into shared iPhone safe-area handling', () => { const pages = [ 'index.html', + 'about.html', 'profile.html', 'pong/index.html', 'pong/classic/index.html', From 4bb4028fb0eb0ed2f29ac6271d0e3bf42d75a062 Mon Sep 17 00:00:00 2001 From: Mohamed Elsherif Date: Fri, 11 Sep 2026 20:44:49 -0700 Subject: [PATCH 2/5] Mention About page in release notes --- releases.json | 5 +++-- tests/release-notes.test.js | 6 ++++++ 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/releases.json b/releases.json index a5c807a..183d3aa 100644 --- a/releases.json +++ b/releases.json @@ -4,10 +4,11 @@ { "version": "1.2.0", "title": "JavaScript Playground 1.2.0", - "summary": "Achievements now celebrate key Tetris and Battle Tanks milestones at the moment players earn them.", + "summary": "Achievements now celebrate key Tetris and Battle Tanks milestones as they happen, and a new About page introduces the person behind the arcade.", "highlights": [ "See achievement unlocks during active play when reaching Tetris level 10, clearing four lines at once, or completing eligible Battle Tanks power-up and tactical challenges.", - "Hear the Tetris music accelerate when the tower rises above two-thirds of the playfield, then settle back to its normal tempo when the danger clears." + "Hear the Tetris music accelerate when the tower rises above two-thirds of the playfield, then settle back to its normal tempo when the danger clears.", + "Meet Mohamed Elsherif through a new game-native About page with his career, community work, patents, websites, and social profiles." ], "fixes": [ "Prevents live achievement notifications from appearing again when the final game result is recorded.", diff --git a/tests/release-notes.test.js b/tests/release-notes.test.js index 19ad5fb..9d1b363 100644 --- a/tests/release-notes.test.js +++ b/tests/release-notes.test.js @@ -29,6 +29,12 @@ test('release notes render tailored public and GitHub views', () => { assert.match(markdown, /^## Technical notes$/m); }); +test('current release notes introduce the new About page', () => { + const release = releaseForVersion(manifest, packageVersion); + assert.match(release.summary, /new About page/); + assert.ok(release.highlights.some(item => /Mohamed Elsherif[\s\S]*career[\s\S]*social profiles/.test(item))); +}); + test('build information exposes release notes only for an exact stable version', () => { assert.deepEqual(createBuildInformation({}, manifest), { version: 'dev', channel: 'dev', release: null }); assert.deepEqual(createBuildInformation({ BUILD_VERSION: 'sha-1234567', BUILD_CHANNEL: 'alpha' }, manifest), { version: 'sha-1234567', channel: 'alpha', release: null }); From d87545bb2d5defd76c136bea3f4e71bf8b5284f2 Mon Sep 17 00:00:00 2001 From: Mohamed Elsherif Date: Fri, 11 Sep 2026 20:46:12 -0700 Subject: [PATCH 3/5] Prepare 1.2.5 release notes --- package-lock.json | 4 ++-- package.json | 2 +- releases.json | 20 +++++++++++++++++--- tests/release-notes.test.js | 2 ++ 4 files changed, 22 insertions(+), 6 deletions(-) diff --git a/package-lock.json b/package-lock.json index 30f053d..bb7986c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "javascript-playground", - "version": "1.2.0", + "version": "1.2.5", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "javascript-playground", - "version": "1.2.0", + "version": "1.2.5", "dependencies": { "ws": "8.21.3" }, diff --git a/package.json b/package.json index aaeea2e..45d23d0 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "javascript-playground", - "version": "1.2.0", + "version": "1.2.5", "private": true, "description": "Browser games with authoritative online multiplayer", "main": "server/index.js", diff --git a/releases.json b/releases.json index 183d3aa..e2dea69 100644 --- a/releases.json +++ b/releases.json @@ -1,14 +1,28 @@ { "schemaVersion": 1, "releases": [ + { + "version": "1.2.5", + "title": "JavaScript Playground 1.2.5", + "summary": "A new About page introduces Mohamed Elsherif and connects the arcade with his work across software architecture and the developer community.", + "highlights": [ + "Meet Mohamed Elsherif through a game-native profile of his career, community work, granted patents, websites, and social profiles.", + "Open the About page from the shared arcade navigation on phones, tablets, and desktops." + ], + "fixes": [ + "Keeps the About link available in compact phone navigation while moving the less essential Top scores shortcut out of the narrow layout." + ], + "technical": [ + "Adds the responsive About page to every experience theme and the offline application shell, with regression coverage for content, links, navigation, and safe-area behavior." + ] + }, { "version": "1.2.0", "title": "JavaScript Playground 1.2.0", - "summary": "Achievements now celebrate key Tetris and Battle Tanks milestones as they happen, and a new About page introduces the person behind the arcade.", + "summary": "Achievements now celebrate key Tetris and Battle Tanks milestones at the moment players earn them.", "highlights": [ "See achievement unlocks during active play when reaching Tetris level 10, clearing four lines at once, or completing eligible Battle Tanks power-up and tactical challenges.", - "Hear the Tetris music accelerate when the tower rises above two-thirds of the playfield, then settle back to its normal tempo when the danger clears.", - "Meet Mohamed Elsherif through a new game-native About page with his career, community work, patents, websites, and social profiles." + "Hear the Tetris music accelerate when the tower rises above two-thirds of the playfield, then settle back to its normal tempo when the danger clears." ], "fixes": [ "Prevents live achievement notifications from appearing again when the final game result is recorded.", diff --git a/tests/release-notes.test.js b/tests/release-notes.test.js index 9d1b363..3f919cb 100644 --- a/tests/release-notes.test.js +++ b/tests/release-notes.test.js @@ -31,8 +31,10 @@ test('release notes render tailored public and GitHub views', () => { test('current release notes introduce the new About page', () => { const release = releaseForVersion(manifest, packageVersion); + assert.equal(release.version, '1.2.5'); assert.match(release.summary, /new About page/); assert.ok(release.highlights.some(item => /Mohamed Elsherif[\s\S]*career[\s\S]*social profiles/.test(item))); + assert.doesNotMatch(releaseForVersion(manifest, '1.2.0').summary, /About page/); }); test('build information exposes release notes only for an exact stable version', () => { From d4e098477ce3f1a1202527c1e9d5a56b97ab4a9b Mon Sep 17 00:00:00 2001 From: Mohamed Elsherif Date: Fri, 11 Sep 2026 20:54:10 -0700 Subject: [PATCH 4/5] Address About page accessibility review --- about.css | 17 ++++++++++------- tests/about-page.test.js | 25 +++++++++++++++++++++++++ 2 files changed, 35 insertions(+), 7 deletions(-) diff --git a/about.css b/about.css index e577ebb..921c758 100644 --- a/about.css +++ b/about.css @@ -7,18 +7,21 @@ --line:#192541; --accent:#765cff; --pink:#ff4fa3; + --pink-text:#a31558; --mint:#6ef2d0; --yellow:#ffd84d; --sky:#56a8ff; + --focus:#4b2fc9; + --link-accent:#4b2fc9; --shadow:#192541; } -:root[data-theme="dark"] { color-scheme:dark;--page:#0a1020;--ink:#fff4d6;--muted:#b7c2dc;--panel:#111a33;--line:#fff4d6;--shadow:#030712; } +:root[data-color-mode="dark"] { color-scheme:dark;--page:#0a1020;--ink:#fff4d6;--muted:#b7c2dc;--panel:#111a33;--line:#fff4d6;--pink-text:#ff70b7;--focus:#6ef2d0;--link-accent:#ffd84d;--shadow:#030712; } * { box-sizing:border-box; } body { min-height:100vh;margin:0;color:var(--ink);background:linear-gradient(90deg,#ffffff06 1px,transparent 1px),linear-gradient(#ffffff06 1px,transparent 1px),radial-gradient(circle at 8% 0,#765cff38,transparent 34rem),var(--page);background-size:32px 32px,32px 32px,auto,auto;font-family:var(--pixel-body,ui-monospace,"SFMono-Regular",Consolas,monospace);overflow-x:hidden; } a { color:inherit; } .about-shell { width:min(1120px,calc(100% - 40px));margin:0 auto;padding:clamp(38px,6vw,78px) 0 72px; } .about-hero { display:grid;grid-template-columns:minmax(0,1.45fr) minmax(280px,.55fr);gap:clamp(32px,6vw,76px);align-items:center;margin-bottom:52px; } -.eyebrow,.panel-kicker { margin:0 0 18px;color:var(--pink);font:700 12px/1.2 var(--pixel-font,ui-monospace,monospace);letter-spacing:.1em;text-transform:uppercase; } +.eyebrow,.panel-kicker { margin:0 0 18px;color:var(--pink-text);font:700 12px/1.2 var(--pixel-font,ui-monospace,monospace);letter-spacing:.1em;text-transform:uppercase; } .eyebrow { display:inline-block;padding:8px 12px;color:#192541;background:var(--yellow);border:2px solid var(--line);box-shadow:4px 4px 0 var(--line); } h1,h2,.player-card strong,.button,.resource-links span,.social-grid strong { font-family:var(--pixel-font,ui-monospace,monospace); } h1 { max-width:740px;margin:0;font-size:clamp(40px,7vw,76px);font-weight:700;letter-spacing:-.065em;line-height:1.08;text-wrap:balance;text-shadow:5px 5px 0 var(--accent); } @@ -29,10 +32,10 @@ h1 span { color:var(--pink); } .button-primary { color:#192541;background:var(--mint); } .button-secondary { background:var(--panel); } .button:hover { transform:translate(4px,4px);box-shadow:0 0 0 var(--line); } -.button:focus-visible,.resource-links a:focus-visible,.patent-list a:focus-visible,.social-grid a:focus-visible { outline:3px solid var(--mint);outline-offset:4px; } +.button:focus-visible,.resource-links a:focus-visible,.patent-list a:focus-visible,.social-grid a:focus-visible { outline:3px solid var(--focus);outline-offset:4px; } .player-card { position:relative;padding:28px;background:var(--panel);border:3px solid var(--line);box-shadow:8px 8px 0 var(--pink),12px 12px 0 var(--line);transform:rotate(1.5deg); } .player-card::after { position:absolute;top:14px;right:14px;width:42px;height:42px;background:linear-gradient(90deg,var(--yellow) 0 30%,transparent 30% 70%,var(--yellow) 70%),linear-gradient(var(--yellow) 0 30%,transparent 30% 70%,var(--yellow) 70%);content:"";opacity:.8; } -.player-label { margin:0 0 10px;color:var(--pink);font-size:11px;font-weight:800;letter-spacing:.12em;text-transform:uppercase; } +.player-label { margin:0 0 10px;color:var(--pink-text);font-size:11px;font-weight:800;letter-spacing:.12em;text-transform:uppercase; } .player-card>strong { display:block;padding-right:46px;font-size:clamp(19px,3vw,27px); } .player-card dl { display:grid;gap:0;margin:24px 0 0; } .player-card dl div { display:grid;grid-template-columns:90px 1fr;gap:12px;padding:12px 0;border-top:1px solid color-mix(in srgb,var(--line) 22%,transparent); } @@ -60,7 +63,7 @@ h1 span { color:var(--pink); } .patent-list a { display:flex;flex-direction:column;gap:10px;min-height:170px;padding:18px;background:color-mix(in srgb,var(--yellow) 12%,transparent);border:2px solid var(--line);text-decoration:none; } .patent-list small { color:var(--muted);font-size:11px;font-weight:800;text-transform:uppercase; } .patent-list span { flex:1;font-size:15px;font-weight:800;line-height:1.45; } -.patent-list b { color:var(--accent);font-size:12px; } +.patent-list b { color:var(--link-accent);font-size:12px; } .patent-list i { font-style:normal; } .social-section { margin-top:58px; } .social-grid { display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:14px;margin-top:24px; } @@ -73,8 +76,8 @@ h1 span { color:var(--pink); } .social-grid small { color:var(--muted);font-size:11px; } :root[data-arcade-theme="cabinet"] .about-shell { width:min(1240px,calc(100% - 32px)); } :root[data-arcade-theme="cabinet"] :is(.player-card,.panel) { box-shadow:5px 5px 0 var(--yellow); } -:root[data-arcade-theme="calm"] { --page:#eef4f1;--ink:#223a33;--muted:#61746d;--panel:#f8fbf9;--line:#527068;--accent:#356f61;--pink:#9b5548;--mint:#789d90;--yellow:#d9b968;--sky:#668e9b;--shadow:#263c34; } -:root[data-arcade-theme="calm"][data-color-mode="dark"] { --page:#111917;--ink:#e5efeb;--muted:#9fb0a9;--panel:#18231f;--line:#91a59d;--shadow:#000; } +:root[data-arcade-theme="calm"] { --page:#eef4f1;--ink:#223a33;--muted:#61746d;--panel:#f8fbf9;--line:#527068;--accent:#356f61;--pink:#9b5548;--pink-text:#854238;--mint:#789d90;--yellow:#d9b968;--sky:#668e9b;--focus:#315f54;--link-accent:#315f54;--shadow:#263c34; } +:root[data-arcade-theme="calm"][data-color-mode="dark"] { --page:#111917;--ink:#e5efeb;--muted:#9fb0a9;--panel:#18231f;--line:#91a59d;--pink-text:#e8a092;--focus:#d9b968;--link-accent:#d9b968;--shadow:#000; } :root[data-arcade-theme="calm"] body { background:var(--page);font-family:Georgia,"Times New Roman",serif; } :root[data-arcade-theme="calm"] .about-shell { width:min(1020px,calc(100% - 48px)); } :root[data-arcade-theme="calm"] :is(h1,h2,.player-card strong,.button,.resource-links span,.social-grid strong) { font-family:Georgia,"Times New Roman",serif; } diff --git a/tests/about-page.test.js b/tests/about-page.test.js index 94ed3a9..2ca7448 100644 --- a/tests/about-page.test.js +++ b/tests/about-page.test.js @@ -9,6 +9,15 @@ const styles = fs.readFileSync('about.css', 'utf8'); const shell = fs.readFileSync('arcade.js', 'utf8'); const worker = fs.readFileSync('service-worker.js', 'utf8'); +function contrast(left, right) { + const luminance = color => { + const channels = color.slice(1).match(/../g).map(value => parseInt(value, 16) / 255).map(value => value <= 0.04045 ? value / 12.92 : ((value + 0.055) / 1.055) ** 2.4); + return 0.2126 * channels[0] + 0.7152 * channels[1] + 0.0722 * channels[2]; + }; + const values = [luminance(left), luminance(right)]; + return (Math.max(...values) + 0.05) / (Math.min(...values) + 0.05); +} + test('shared arcade navigation exposes the about page', () => { assert.match(shell, /about\.href = `\$\{rootPath\}about\.html`/); assert.match(shell, /about\.setAttribute\('aria-current', 'page'\)/); @@ -38,6 +47,22 @@ test('about page supports every experience theme and phone layouts', () => { assert.match(styles, /@media \(max-width:520px\)/); }); +test('about page uses the current color-mode contract and contrast-safe text and focus colors', () => { + assert.match(styles, /:root\[data-color-mode="dark"\]/); + assert.doesNotMatch(styles, /data-theme=/); + assert.match(styles, /color:var\(--pink-text\)/); + assert.match(styles, /outline:3px solid var\(--focus\)/); + assert.match(styles, /\.patent-list b \{ color:var\(--link-accent\)/); + for (const [foreground, background, minimum] of [ + ['#a31558', '#fff9e8', 4.5], + ['#4b2fc9', '#fff9e8', 3], + ['#315f54', '#f8fbf9', 4.5], + ['#ff70b7', '#111a33', 4.5], + ['#6ef2d0', '#111a33', 3], + ['#d9b968', '#18231f', 4.5] + ]) assert.ok(contrast(foreground, background) >= minimum, `${foreground} should meet ${minimum}:1 against ${background}`); +}); + test('about page is available from the offline app shell', () => { assert.match(worker, /'\.\/about\.html'/); assert.match(worker, /'\.\/about\.css'/); From aa7f654079fcce2291d4ca8eca505063f9512dc6 Mon Sep 17 00:00:00 2001 From: Mohamed Elsherif Date: Fri, 11 Sep 2026 21:03:18 -0700 Subject: [PATCH 5/5] Improve About hero contrast --- about.css | 9 +++++---- tests/about-page.test.js | 2 ++ 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/about.css b/about.css index 921c758..4d12bf3 100644 --- a/about.css +++ b/about.css @@ -7,6 +7,7 @@ --line:#192541; --accent:#765cff; --pink:#ff4fa3; + --pink-heading:#e13a87; --pink-text:#a31558; --mint:#6ef2d0; --yellow:#ffd84d; @@ -15,7 +16,7 @@ --link-accent:#4b2fc9; --shadow:#192541; } -:root[data-color-mode="dark"] { color-scheme:dark;--page:#0a1020;--ink:#fff4d6;--muted:#b7c2dc;--panel:#111a33;--line:#fff4d6;--pink-text:#ff70b7;--focus:#6ef2d0;--link-accent:#ffd84d;--shadow:#030712; } +:root[data-color-mode="dark"] { color-scheme:dark;--page:#0a1020;--ink:#fff4d6;--muted:#b7c2dc;--panel:#111a33;--line:#fff4d6;--pink-heading:#ff70b7;--pink-text:#ff70b7;--focus:#6ef2d0;--link-accent:#ffd84d;--shadow:#030712; } * { box-sizing:border-box; } body { min-height:100vh;margin:0;color:var(--ink);background:linear-gradient(90deg,#ffffff06 1px,transparent 1px),linear-gradient(#ffffff06 1px,transparent 1px),radial-gradient(circle at 8% 0,#765cff38,transparent 34rem),var(--page);background-size:32px 32px,32px 32px,auto,auto;font-family:var(--pixel-body,ui-monospace,"SFMono-Regular",Consolas,monospace);overflow-x:hidden; } a { color:inherit; } @@ -25,7 +26,7 @@ a { color:inherit; } .eyebrow { display:inline-block;padding:8px 12px;color:#192541;background:var(--yellow);border:2px solid var(--line);box-shadow:4px 4px 0 var(--line); } h1,h2,.player-card strong,.button,.resource-links span,.social-grid strong { font-family:var(--pixel-font,ui-monospace,monospace); } h1 { max-width:740px;margin:0;font-size:clamp(40px,7vw,76px);font-weight:700;letter-spacing:-.065em;line-height:1.08;text-wrap:balance;text-shadow:5px 5px 0 var(--accent); } -h1 span { color:var(--pink); } +h1 span { color:var(--pink-heading); } .lede { max-width:720px;margin:28px 0 0;color:var(--muted);font-size:clamp(17px,2vw,20px);line-height:1.65; } .hero-actions { display:flex;flex-wrap:wrap;gap:14px;margin-top:30px; } .button { display:inline-flex;align-items:center;justify-content:center;gap:9px;min-height:48px;padding:10px 16px;border:2px solid var(--line);font-size:11px;font-weight:700;text-decoration:none;box-shadow:4px 4px 0 var(--line);transition:transform 140ms steps(2,end),box-shadow 140ms steps(2,end); } @@ -76,8 +77,8 @@ h1 span { color:var(--pink); } .social-grid small { color:var(--muted);font-size:11px; } :root[data-arcade-theme="cabinet"] .about-shell { width:min(1240px,calc(100% - 32px)); } :root[data-arcade-theme="cabinet"] :is(.player-card,.panel) { box-shadow:5px 5px 0 var(--yellow); } -:root[data-arcade-theme="calm"] { --page:#eef4f1;--ink:#223a33;--muted:#61746d;--panel:#f8fbf9;--line:#527068;--accent:#356f61;--pink:#9b5548;--pink-text:#854238;--mint:#789d90;--yellow:#d9b968;--sky:#668e9b;--focus:#315f54;--link-accent:#315f54;--shadow:#263c34; } -:root[data-arcade-theme="calm"][data-color-mode="dark"] { --page:#111917;--ink:#e5efeb;--muted:#9fb0a9;--panel:#18231f;--line:#91a59d;--pink-text:#e8a092;--focus:#d9b968;--link-accent:#d9b968;--shadow:#000; } +:root[data-arcade-theme="calm"] { --page:#eef4f1;--ink:#223a33;--muted:#61746d;--panel:#f8fbf9;--line:#527068;--accent:#356f61;--pink:#9b5548;--pink-heading:#9b5548;--pink-text:#854238;--mint:#789d90;--yellow:#d9b968;--sky:#668e9b;--focus:#315f54;--link-accent:#315f54;--shadow:#263c34; } +:root[data-arcade-theme="calm"][data-color-mode="dark"] { --page:#111917;--ink:#e5efeb;--muted:#9fb0a9;--panel:#18231f;--line:#91a59d;--pink-heading:#e8a092;--pink-text:#e8a092;--focus:#d9b968;--link-accent:#d9b968;--shadow:#000; } :root[data-arcade-theme="calm"] body { background:var(--page);font-family:Georgia,"Times New Roman",serif; } :root[data-arcade-theme="calm"] .about-shell { width:min(1020px,calc(100% - 48px)); } :root[data-arcade-theme="calm"] :is(h1,h2,.player-card strong,.button,.resource-links span,.social-grid strong) { font-family:Georgia,"Times New Roman",serif; } diff --git a/tests/about-page.test.js b/tests/about-page.test.js index 2ca7448..3fbf006 100644 --- a/tests/about-page.test.js +++ b/tests/about-page.test.js @@ -50,10 +50,12 @@ test('about page supports every experience theme and phone layouts', () => { test('about page uses the current color-mode contract and contrast-safe text and focus colors', () => { assert.match(styles, /:root\[data-color-mode="dark"\]/); assert.doesNotMatch(styles, /data-theme=/); + assert.match(styles, /h1 span \{ color:var\(--pink-heading\)/); assert.match(styles, /color:var\(--pink-text\)/); assert.match(styles, /outline:3px solid var\(--focus\)/); assert.match(styles, /\.patent-list b \{ color:var\(--link-accent\)/); for (const [foreground, background, minimum] of [ + ['#e13a87', '#fff4d6', 3], ['#a31558', '#fff9e8', 4.5], ['#4b2fc9', '#fff9e8', 3], ['#315f54', '#f8fbf9', 4.5],