Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 15 additions & 6 deletions web-app/css/styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
92 changes: 67 additions & 25 deletions web-app/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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 ────────────────── */
Expand Down Expand Up @@ -859,6 +900,7 @@
</button>
</div>

</div>
</nav>
<!-- CENTER SEARCH -->
<div class="search-box-container">
Expand Down
115 changes: 110 additions & 5 deletions web-app/js/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 =
Expand All @@ -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);
Expand Down Expand Up @@ -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;
}
Expand Down
4 changes: 2 additions & 2 deletions web-app/js/projects.js
Original file line number Diff line number Diff line change
@@ -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) {
Expand Down Expand Up @@ -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);
Expand Down
2 changes: 1 addition & 1 deletion web-app/js/projects/collatz.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
1 change: 0 additions & 1 deletion web-app/js/projects/color-palette.js
Original file line number Diff line number Diff line change
Expand Up @@ -713,7 +713,6 @@ function initColorPalette() {

output.style.display = 'block';
controls.style.display = 'none';
output.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
});

copyBtn.addEventListener('click', () => {
Expand Down
7 changes: 2 additions & 5 deletions web-app/js/projects/typing-speed-tester.js
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -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();
Expand All @@ -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 } = {}) {
Expand Down
4 changes: 2 additions & 2 deletions web-app/js/projects/word-scramble.js
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -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', () => {
Expand Down
Loading