From 33580f0ae08c694fbda3d0f14fbc4fab7017633d Mon Sep 17 00:00:00 2001 From: w1n5t0n Date: Sun, 22 Mar 2026 22:41:49 +0200 Subject: [PATCH] feat(playground): add help/intro modal shown on first visit - Glass-style modal explains NISPS, how learning works, and what to expect - Documents all controls: touch, keyboard, and gamepad bindings - Shows automatically on first visit (localStorage flag) - Dismissible via close button, "Got it", overlay click, or Escape - Reopenable via ? button at bottom right --- playground/a-immersive.html | 70 +++++++++++ playground/css/a-immersive.css | 210 +++++++++++++++++++++++++++++++++ playground/js/a-app.js | 20 ++++ 3 files changed, 300 insertions(+) diff --git a/playground/a-immersive.html b/playground/a-immersive.html index 7586852..7603324 100644 --- a/playground/a-immersive.html +++ b/playground/a-immersive.html @@ -157,6 +157,76 @@ + + + + + + diff --git a/playground/css/a-immersive.css b/playground/css/a-immersive.css index e3f46d5..ef59f6b 100644 --- a/playground/css/a-immersive.css +++ b/playground/css/a-immersive.css @@ -1093,6 +1093,216 @@ html, body { border-radius: 2px; } +/* ---- Help button ---- */ +.help-btn { + position: fixed; + bottom: 96px; + right: 12px; + z-index: 45; + width: 32px; + height: 32px; + border-radius: 50%; + border: 1px solid var(--glass-border); + background: var(--glass-bg); + backdrop-filter: blur(var(--glass-blur)); + -webkit-backdrop-filter: blur(var(--glass-blur)); + color: var(--text-dim); + font-size: 16px; + font-weight: 700; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + transition: all 0.15s; + padding: 0; + line-height: 1; +} + +.help-btn:hover, .help-btn:active { + border-color: var(--accent); + color: var(--accent); +} + +/* ---- Help modal ---- */ +.help-overlay { + position: fixed; + inset: 0; + z-index: 100; + background: rgba(0, 0, 0, 0.7); + backdrop-filter: blur(6px); + -webkit-backdrop-filter: blur(6px); + display: flex; + align-items: center; + justify-content: center; + padding: 16px; + animation: helpFadeIn 0.2s ease-out; +} + +.help-overlay.hidden { + display: none; +} + +@keyframes helpFadeIn { + from { opacity: 0; } + to { opacity: 1; } +} + +.help-modal { + position: relative; + max-width: 520px; + width: 100%; + max-height: 85vh; + overflow-y: auto; + background: rgba(18, 18, 18, 0.95); + backdrop-filter: blur(20px); + -webkit-backdrop-filter: blur(20px); + border: 1px solid rgba(255, 255, 255, 0.1); + border-radius: var(--radius); + padding: 28px 24px 20px; + color: var(--text); +} + +.help-modal h2 { + font-size: 18px; + font-weight: 700; + margin: 0 0 2px; + color: #fff; +} + +.help-subtitle { + font-size: 11px; + color: var(--text-dim); + margin: 0 0 16px; + letter-spacing: 0.5px; +} + +.help-close { + position: absolute; + top: 12px; + right: 12px; + width: 28px; + height: 28px; + border-radius: 50%; + border: 1px solid var(--glass-border); + background: rgba(255, 255, 255, 0.06); + color: var(--text-dim); + font-size: 18px; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + padding: 0; + line-height: 1; + transition: all 0.15s; +} + +.help-close:hover { + border-color: var(--accent); + color: var(--accent); +} + +.help-section { + margin-bottom: 16px; +} + +.help-section h3 { + font-size: 13px; + font-weight: 700; + color: var(--accent); + margin: 0 0 6px; + text-transform: uppercase; + letter-spacing: 0.5px; +} + +.help-section p { + font-size: 12px; + line-height: 1.5; + margin: 0 0 6px; + color: var(--text); +} + +.help-section ul { + margin: 0; + padding-left: 18px; + font-size: 12px; + line-height: 1.6; + color: var(--text); +} + +.help-section li { + margin-bottom: 2px; +} + +.help-keys { + width: 100%; + border-collapse: collapse; + font-size: 12px; +} + +.help-keys th { + text-align: left; + padding: 8px 0 4px; + color: var(--text-dim); + font-size: 11px; + text-transform: uppercase; + letter-spacing: 0.5px; + border-bottom: 1px solid rgba(255, 255, 255, 0.06); +} + +.help-keys td { + padding: 3px 0; + color: var(--text); +} + +.help-keys td:first-child { + color: #fff; + font-weight: 500; + width: 45%; +} + +.help-keys kbd { + display: inline-block; + padding: 1px 6px; + border-radius: 3px; + border: 1px solid rgba(255, 255, 255, 0.15); + background: rgba(255, 255, 255, 0.06); + font-family: inherit; + font-size: 11px; +} + +.help-got-it { + display: block; + width: 100%; + padding: 10px; + margin-top: 8px; + border-radius: 6px; + border: 1px solid rgba(255, 106, 0, 0.3); + background: rgba(255, 106, 0, 0.1); + color: var(--accent); + font-size: 14px; + font-weight: 600; + cursor: pointer; + transition: all 0.15s; +} + +.help-got-it:active { + background: rgba(255, 106, 0, 0.25); +} + +/* Help modal scrollbar */ +.help-modal::-webkit-scrollbar { + width: 4px; +} + +.help-modal::-webkit-scrollbar-track { + background: transparent; +} + +.help-modal::-webkit-scrollbar-thumb { + background: rgba(255, 255, 255, 0.1); + border-radius: 2px; +} + /* ---- Media queries ---- */ @media (min-width: 768px) { .joystick-container { diff --git a/playground/js/a-app.js b/playground/js/a-app.js index 940be01..5458bf9 100644 --- a/playground/js/a-app.js +++ b/playground/js/a-app.js @@ -620,6 +620,7 @@ function init() { wireKeyboard(); wireQuickPlayControls(); wireGroupDrawer(); + wireHelp(); // Resize window.addEventListener('resize', onResize); @@ -1805,6 +1806,25 @@ function onResize() { } } +// ---- Help modal ---- +function wireHelp() { + const overlay = document.getElementById('help-overlay'); + const btnOpen = document.getElementById('help-btn'); + const btnClose = document.getElementById('help-close'); + const btnGotIt = document.getElementById('help-got-it'); + + function show() { overlay.classList.remove('hidden'); } + function hide() { overlay.classList.add('hidden'); localStorage.setItem('nisps-help-seen', '1'); } + + btnOpen.addEventListener('click', show); + btnClose.addEventListener('click', hide); + btnGotIt.addEventListener('click', hide); + overlay.addEventListener('click', (e) => { if (e.target === overlay) hide(); }); + + // Show on first visit + if (!localStorage.getItem('nisps-help-seen')) show(); +} + // ---- Animation ---- function animate() { if (gamepad) gamepad.poll();