diff --git a/web-app/css/styles.css b/web-app/css/styles.css index c59dea6..6fa9d19 100644 --- a/web-app/css/styles.css +++ b/web-app/css/styles.css @@ -3189,16 +3189,25 @@ body { } .hero-features { - flex-direction: column; - align-items: flex-start; - gap: 0.55rem; + flex-direction: row; + flex-wrap: nowrap; + overflow-x: auto; + -webkit-overflow-scrolling: touch; + scrollbar-width: none; + width: 100%; + justify-content: flex-start; + gap: 0.6rem; margin-bottom: 1.5rem; } + .hero-features::-webkit-scrollbar { + display: none; + } + .feature-badge { - width: auto; - justify-content: flex-start; - padding: 0.15rem 0 0.35rem; + flex-shrink: 0; + padding: 0.5rem 0.9rem; + font-size: 0.85rem; } .btn-explore { diff --git a/web-app/index.html b/web-app/index.html index e45672f..76bd085 100644 --- a/web-app/index.html +++ b/web-app/index.html @@ -609,8 +609,72 @@ @media (max-width: 768px) { .navbar { padding: 10px 8px; } - .nav-island { height: 64px; padding: 0 16px; border-radius: 16px; } - .mobile-menu-toggle { display: flex; z-index: 1100; } + .nav-island { height: auto; padding: 0.8rem 1.15rem; border-radius: 20px; } + + .nav-wrapper { + flex-wrap: wrap; + justify-content: space-between; + border-radius: 28px; + padding: 0.8rem 1.2rem; + } + + .navbar-brand { + order: 1; + } + + .nav-controls { + order: 2; + position: static; + width: auto; + height: auto; + background: transparent; + border: none; + padding: 0; + flex-direction: row; + justify-content: flex-end; + gap: 10px; + z-index: auto; + transition: none; + } + [data-theme="light"] .nav-controls { + background: transparent; + border: none; + } + + .sound-toggle, .theme-toggle { + width: 42px; + height: 42px; + border-radius: 12px; + } + + .container1 { + order: 3; + width: 100%; + min-width: 0; + margin-top: 0.5rem; + } + + .hero-features { + display: flex; + flex-direction: row; + flex-wrap: nowrap; + overflow-x: auto; + -webkit-overflow-scrolling: touch; + scrollbar-width: none; + width: 100%; + justify-content: flex-start; + gap: 0.6rem; + padding-bottom: 4px; + margin-bottom: 0; + } + .hero-features::-webkit-scrollbar { + display: none; + } + .hero-features .feature-badge { + flex-shrink: 0; + padding: 0.5rem 0.9rem; + font-size: 0.85rem; + } .search-box { position: absolute; @@ -627,29 +691,6 @@ background: #ffffff; border-color: rgba(0,0,0,0.08); } - - .nav-controls { - position: fixed; - top: 0; - right: -100%; - width: 260px; - height: 100vh; - background: #071227; - border-left: 1px solid rgba(255, 255, 255, 0.08); - flex-direction: column; - justify-content: center; - gap: 20px; - padding: 2rem; - z-index: 1050; - transition: right 0.35s cubic-bezier(0.4, 0, 0.2, 1); - } - [data-theme="light"] .nav-controls { - background: #ffffff; - border-left: 1px solid rgba(0,0,0,0.06); - } - - .nav-controls.mobile-active { right: 0; } - .sound-toggle, .theme-toggle { width: 50px; height: 50px; border-radius: 14px; } } /* ── Reduced-motion overrides for playground ────────────────── */ @@ -859,6 +900,7 @@ +
diff --git a/web-app/js/main.js b/web-app/js/main.js index c04163d..a2a62e7 100644 --- a/web-app/js/main.js +++ b/web-app/js/main.js @@ -648,6 +648,99 @@ if (stickyFilterBar && heroSection) { renderRecentSearches(); + // ── Central Dynamic Auto-Scaling (ResizeObserver) ───────────────── + var modalResizeObserver = null; + + function applyModalScaling() { + var modalContent = document.querySelector('.modal-content'); + var modalBody = document.getElementById('modalBody'); + if (!modalContent || !modalBody) return; + + // Reset scroll position to top to avoid viewport clippings during calculations + modalContent.scrollTop = 0; + modalBody.scrollTop = 0; + + // Hide scrollbars on the container + modalContent.style.overflow = 'hidden'; + + // Reset inline styles to capture natural dimensions + modalBody.style.transform = ''; + modalBody.style.transformOrigin = ''; + modalBody.style.width = ''; + modalBody.style.height = ''; + + var targetEl = Array.from(modalBody.children).find(function (el) { + return el.tagName.toLowerCase() !== 'style'; + }) || modalBody.firstElementChild; + if (!targetEl) return; + + targetEl.style.transform = ''; + targetEl.style.transformOrigin = ''; + + var computedStyle = window.getComputedStyle(modalContent); + var paddingTop = parseFloat(computedStyle.paddingTop) || 32; + var paddingBottom = parseFloat(computedStyle.paddingBottom) || 32; + var paddingLeft = parseFloat(computedStyle.paddingLeft) || 32; + var paddingRight = parseFloat(computedStyle.paddingRight) || 32; + + var availableHeight = modalContent.clientHeight - paddingTop - paddingBottom; + var availableWidth = modalContent.clientWidth - paddingLeft - paddingRight; + + var contentHeight = targetEl.scrollHeight; + var contentWidth = targetEl.scrollWidth; + + if (contentHeight <= 0 || contentWidth <= 0) return; + + var zoom = 1; + var heightZoom = availableHeight / contentHeight; + var widthZoom = availableWidth / contentWidth; + + zoom = Math.min(heightZoom, widthZoom); + if (zoom > 1) { + zoom = 1; + } + + // Apply scale transform and origins + targetEl.style.transform = 'scale(' + zoom + ')'; + targetEl.style.transformOrigin = 'top center'; + + // Constrain wrapper block size to prevent scroll triggering + modalBody.style.height = (contentHeight * zoom) + 'px'; + modalBody.style.width = '100%'; + modalBody.style.display = 'flex'; + modalBody.style.flexDirection = 'column'; + modalBody.style.alignItems = 'center'; + } + + function initModalScaling() { + applyModalScaling(); + + if (modalResizeObserver) { + modalResizeObserver.disconnect(); + } + + var modalBody = document.getElementById('modalBody'); + var targetEl = modalBody ? Array.from(modalBody.children).find(function (el) { + return el.tagName.toLowerCase() !== 'style'; + }) || modalBody.firstElementChild : null; + if (targetEl) { + modalResizeObserver = new ResizeObserver(function () { + requestAnimationFrame(applyModalScaling); + }); + modalResizeObserver.observe(targetEl); + } + + window.addEventListener('resize', applyModalScaling); + } + + function destroyModalScaling() { + if (modalResizeObserver) { + modalResizeObserver.disconnect(); + modalResizeObserver = null; + } + window.removeEventListener('resize', applyModalScaling); + } + // ── Focus Trap for Modal ────────────────────────────────────────── function getFocusableElements(root) { var selector = @@ -668,9 +761,9 @@ if (stickyFilterBar && heroSection) { var first = focusables[0]; var last = focusables[focusables.length - 1]; if (e.shiftKey && document.activeElement === first) { - e.preventDefault(); last.focus(); + e.preventDefault(); last.focus({ preventScroll: true }); } else if (!e.shiftKey && document.activeElement === last) { - e.preventDefault(); first.focus(); + e.preventDefault(); first.focus({ preventScroll: true }); } }; document.addEventListener('keydown', handler, true); @@ -710,25 +803,37 @@ if (stickyFilterBar && heroSection) { if (typeof initializeProject === 'function') initializeProject(name); }); + // Initialize reactive scale calculations + initModalScaling(); + removeTrap = trapFocus(modal); var focusables = getFocusableElements(modalBody); var firstFocusable = focusables[0] || modalClose; if (firstFocusable && typeof firstFocusable.focus === 'function') { - firstFocusable.focus(); + firstFocusable.focus({ preventScroll: true }); } } function closeProjectSafe() { if (!modal || !modal.classList.contains('active')) return; + + destroyModalScaling(); + modal.classList.remove('active'); modal.setAttribute('aria-hidden', 'true'); document.body.style.paddingRight = ''; document.body.style.overflow = ''; setMainInert(false); if (removeTrap) { removeTrap(); removeTrap = null; } - if (modalBody) modalBody.innerHTML = ''; + if (modalBody) { + modalBody.innerHTML = ''; + modalBody.style.transform = ''; + modalBody.style.transformOrigin = ''; + modalBody.style.width = ''; + modalBody.style.height = ''; + } if (lastFocusedElement && typeof lastFocusedElement.focus === 'function') { - lastFocusedElement.focus(); + lastFocusedElement.focus({ preventScroll: true }); } lastFocusedElement = null; } diff --git a/web-app/js/projects.js b/web-app/js/projects.js index 269a23f..04c42f7 100644 --- a/web-app/js/projects.js +++ b/web-app/js/projects.js @@ -1,4 +1,4 @@ -// Project Registry +// Project Registry // Each project's HTML and logic lives in its own file under js/projects/ function getProjectHTML(projectName) { @@ -2091,7 +2091,7 @@ function initCollatz() { const yScale = graphHeight / maxValue; // Draw axes - ctx.strokeStyle = 'var(--text-secondary)'; + ctx.strokeStyle = '#64748b'; ctx.lineWidth = 2; ctx.beginPath(); ctx.moveTo(padding, padding); diff --git a/web-app/js/projects/collatz.js b/web-app/js/projects/collatz.js index ca5e197..d712e02 100644 --- a/web-app/js/projects/collatz.js +++ b/web-app/js/projects/collatz.js @@ -230,7 +230,7 @@ function initCollatz() { const xStep = graphWidth / (sequence.length - 1); const yScale = graphHeight / maxValue; - ctx.strokeStyle = 'var(--text-secondary)'; + ctx.strokeStyle = '#64748b'; ctx.lineWidth = 2; ctx.beginPath(); ctx.moveTo(padding, padding); diff --git a/web-app/js/projects/color-palette.js b/web-app/js/projects/color-palette.js index 8dade8a..3e7fce1 100644 --- a/web-app/js/projects/color-palette.js +++ b/web-app/js/projects/color-palette.js @@ -713,7 +713,6 @@ function initColorPalette() { output.style.display = 'block'; controls.style.display = 'none'; - output.scrollIntoView({ behavior: 'smooth', block: 'nearest' }); }); copyBtn.addEventListener('click', () => { diff --git a/web-app/js/projects/typing-speed-tester.js b/web-app/js/projects/typing-speed-tester.js index edeecaf..3138f48 100644 --- a/web-app/js/projects/typing-speed-tester.js +++ b/web-app/js/projects/typing-speed-tester.js @@ -257,7 +257,7 @@ function getTypingSpeedTesterHTML() { inputElement.value = ""; inputElement.disabled = false; inputElement.removeAttribute("aria-disabled"); - inputElement.focus(); + inputElement.focus({ preventScroll: true }); result.innerHTML = ""; startTime = Date.now(); @@ -733,7 +733,7 @@ resultDetails.innerHTML = ` requestAnimationFrame(() => sentenceElement.classList.remove('sentence-loading')); inputElement.value = ''; inputElement.disabled = false; - inputElement.focus(); + inputElement.focus({ preventScroll: true }); if (startSession && !sessionStarted) { sessionStarted = true; startTime = Date.now(); @@ -743,9 +743,6 @@ resultDetails.innerHTML = ` // ensure pending state const spans = sentenceElement.querySelectorAll('span'); spans.forEach(s => s.className = 'pending'); - if (inputElement.scrollIntoView) { - inputElement.scrollIntoView({ behavior: 'smooth', block: 'nearest' }); - } } function setDifficulty(mode, { resetGame = false } = {}) { diff --git a/web-app/js/projects/word-scramble.js b/web-app/js/projects/word-scramble.js index 3396c47..ffe46ed 100644 --- a/web-app/js/projects/word-scramble.js +++ b/web-app/js/projects/word-scramble.js @@ -422,7 +422,7 @@ function initWordScramble() { renderWord(shuffleWord(current.word)); updateStats(); setMessage(keepStreak ? 'Unscramble the letters.' : 'Fresh word loaded.'); - guessInput.focus(); + guessInput.focus({ preventScroll: true }); // Launch countdown engine for the active round startTimer(); @@ -506,7 +506,7 @@ function checkGuess() { shuffleBtn.addEventListener('click', () => { if (!current) return; renderWord(shuffleWord(current.word)); - guessInput.focus(); + guessInput.focus({ preventScroll: true }); }); nextBtn.addEventListener('click', () => {