Skip to content

Commit b9e46fe

Browse files
Merge pull request steam-bell-92#566 from prasad-0007/refactor/modal-scaling-fixes
refactor: Eliminate Modal Scrollbars, Mitigate Focus-Scroll Clipping, and Fix Canvas Color Rendering (Fixes steam-bell-92#341)
2 parents 06f6bd4 + 3b2874b commit b9e46fe

8 files changed

Lines changed: 199 additions & 47 deletions

File tree

web-app/css/styles.css

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3189,16 +3189,25 @@ body {
31893189
}
31903190

31913191
.hero-features {
3192-
flex-direction: column;
3193-
align-items: flex-start;
3194-
gap: 0.55rem;
3192+
flex-direction: row;
3193+
flex-wrap: nowrap;
3194+
overflow-x: auto;
3195+
-webkit-overflow-scrolling: touch;
3196+
scrollbar-width: none;
3197+
width: 100%;
3198+
justify-content: flex-start;
3199+
gap: 0.6rem;
31953200
margin-bottom: 1.5rem;
31963201
}
31973202

3203+
.hero-features::-webkit-scrollbar {
3204+
display: none;
3205+
}
3206+
31983207
.feature-badge {
3199-
width: auto;
3200-
justify-content: flex-start;
3201-
padding: 0.15rem 0 0.35rem;
3208+
flex-shrink: 0;
3209+
padding: 0.5rem 0.9rem;
3210+
font-size: 0.85rem;
32023211
}
32033212

32043213
.btn-explore {

web-app/index.html

Lines changed: 67 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -609,8 +609,72 @@
609609

610610
@media (max-width: 768px) {
611611
.navbar { padding: 10px 8px; }
612-
.nav-island { height: 64px; padding: 0 16px; border-radius: 16px; }
613-
.mobile-menu-toggle { display: flex; z-index: 1100; }
612+
.nav-island { height: auto; padding: 0.8rem 1.15rem; border-radius: 20px; }
613+
614+
.nav-wrapper {
615+
flex-wrap: wrap;
616+
justify-content: space-between;
617+
border-radius: 28px;
618+
padding: 0.8rem 1.2rem;
619+
}
620+
621+
.navbar-brand {
622+
order: 1;
623+
}
624+
625+
.nav-controls {
626+
order: 2;
627+
position: static;
628+
width: auto;
629+
height: auto;
630+
background: transparent;
631+
border: none;
632+
padding: 0;
633+
flex-direction: row;
634+
justify-content: flex-end;
635+
gap: 10px;
636+
z-index: auto;
637+
transition: none;
638+
}
639+
[data-theme="light"] .nav-controls {
640+
background: transparent;
641+
border: none;
642+
}
643+
644+
.sound-toggle, .theme-toggle {
645+
width: 42px;
646+
height: 42px;
647+
border-radius: 12px;
648+
}
649+
650+
.container1 {
651+
order: 3;
652+
width: 100%;
653+
min-width: 0;
654+
margin-top: 0.5rem;
655+
}
656+
657+
.hero-features {
658+
display: flex;
659+
flex-direction: row;
660+
flex-wrap: nowrap;
661+
overflow-x: auto;
662+
-webkit-overflow-scrolling: touch;
663+
scrollbar-width: none;
664+
width: 100%;
665+
justify-content: flex-start;
666+
gap: 0.6rem;
667+
padding-bottom: 4px;
668+
margin-bottom: 0;
669+
}
670+
.hero-features::-webkit-scrollbar {
671+
display: none;
672+
}
673+
.hero-features .feature-badge {
674+
flex-shrink: 0;
675+
padding: 0.5rem 0.9rem;
676+
font-size: 0.85rem;
677+
}
614678

615679
.search-box {
616680
position: absolute;
@@ -627,29 +691,6 @@
627691
background: #ffffff;
628692
border-color: rgba(0,0,0,0.08);
629693
}
630-
631-
.nav-controls {
632-
position: fixed;
633-
top: 0;
634-
right: -100%;
635-
width: 260px;
636-
height: 100vh;
637-
background: #071227;
638-
border-left: 1px solid rgba(255, 255, 255, 0.08);
639-
flex-direction: column;
640-
justify-content: center;
641-
gap: 20px;
642-
padding: 2rem;
643-
z-index: 1050;
644-
transition: right 0.35s cubic-bezier(0.4, 0, 0.2, 1);
645-
}
646-
[data-theme="light"] .nav-controls {
647-
background: #ffffff;
648-
border-left: 1px solid rgba(0,0,0,0.06);
649-
}
650-
651-
.nav-controls.mobile-active { right: 0; }
652-
.sound-toggle, .theme-toggle { width: 50px; height: 50px; border-radius: 14px; }
653694
}
654695

655696
/* ── Reduced-motion overrides for playground ────────────────── */
@@ -859,6 +900,7 @@
859900
</button>
860901
</div>
861902

903+
</div>
862904
</nav>
863905
<!-- CENTER SEARCH -->
864906
<div class="search-box-container">

web-app/js/main.js

Lines changed: 110 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -648,6 +648,99 @@ if (stickyFilterBar && heroSection) {
648648

649649
renderRecentSearches();
650650

651+
// ── Central Dynamic Auto-Scaling (ResizeObserver) ─────────────────
652+
var modalResizeObserver = null;
653+
654+
function applyModalScaling() {
655+
var modalContent = document.querySelector('.modal-content');
656+
var modalBody = document.getElementById('modalBody');
657+
if (!modalContent || !modalBody) return;
658+
659+
// Reset scroll position to top to avoid viewport clippings during calculations
660+
modalContent.scrollTop = 0;
661+
modalBody.scrollTop = 0;
662+
663+
// Hide scrollbars on the container
664+
modalContent.style.overflow = 'hidden';
665+
666+
// Reset inline styles to capture natural dimensions
667+
modalBody.style.transform = '';
668+
modalBody.style.transformOrigin = '';
669+
modalBody.style.width = '';
670+
modalBody.style.height = '';
671+
672+
var targetEl = Array.from(modalBody.children).find(function (el) {
673+
return el.tagName.toLowerCase() !== 'style';
674+
}) || modalBody.firstElementChild;
675+
if (!targetEl) return;
676+
677+
targetEl.style.transform = '';
678+
targetEl.style.transformOrigin = '';
679+
680+
var computedStyle = window.getComputedStyle(modalContent);
681+
var paddingTop = parseFloat(computedStyle.paddingTop) || 32;
682+
var paddingBottom = parseFloat(computedStyle.paddingBottom) || 32;
683+
var paddingLeft = parseFloat(computedStyle.paddingLeft) || 32;
684+
var paddingRight = parseFloat(computedStyle.paddingRight) || 32;
685+
686+
var availableHeight = modalContent.clientHeight - paddingTop - paddingBottom;
687+
var availableWidth = modalContent.clientWidth - paddingLeft - paddingRight;
688+
689+
var contentHeight = targetEl.scrollHeight;
690+
var contentWidth = targetEl.scrollWidth;
691+
692+
if (contentHeight <= 0 || contentWidth <= 0) return;
693+
694+
var zoom = 1;
695+
var heightZoom = availableHeight / contentHeight;
696+
var widthZoom = availableWidth / contentWidth;
697+
698+
zoom = Math.min(heightZoom, widthZoom);
699+
if (zoom > 1) {
700+
zoom = 1;
701+
}
702+
703+
// Apply scale transform and origins
704+
targetEl.style.transform = 'scale(' + zoom + ')';
705+
targetEl.style.transformOrigin = 'top center';
706+
707+
// Constrain wrapper block size to prevent scroll triggering
708+
modalBody.style.height = (contentHeight * zoom) + 'px';
709+
modalBody.style.width = '100%';
710+
modalBody.style.display = 'flex';
711+
modalBody.style.flexDirection = 'column';
712+
modalBody.style.alignItems = 'center';
713+
}
714+
715+
function initModalScaling() {
716+
applyModalScaling();
717+
718+
if (modalResizeObserver) {
719+
modalResizeObserver.disconnect();
720+
}
721+
722+
var modalBody = document.getElementById('modalBody');
723+
var targetEl = modalBody ? Array.from(modalBody.children).find(function (el) {
724+
return el.tagName.toLowerCase() !== 'style';
725+
}) || modalBody.firstElementChild : null;
726+
if (targetEl) {
727+
modalResizeObserver = new ResizeObserver(function () {
728+
requestAnimationFrame(applyModalScaling);
729+
});
730+
modalResizeObserver.observe(targetEl);
731+
}
732+
733+
window.addEventListener('resize', applyModalScaling);
734+
}
735+
736+
function destroyModalScaling() {
737+
if (modalResizeObserver) {
738+
modalResizeObserver.disconnect();
739+
modalResizeObserver = null;
740+
}
741+
window.removeEventListener('resize', applyModalScaling);
742+
}
743+
651744
// ── Focus Trap for Modal ──────────────────────────────────────────
652745
function getFocusableElements(root) {
653746
var selector =
@@ -668,9 +761,9 @@ if (stickyFilterBar && heroSection) {
668761
var first = focusables[0];
669762
var last = focusables[focusables.length - 1];
670763
if (e.shiftKey && document.activeElement === first) {
671-
e.preventDefault(); last.focus();
764+
e.preventDefault(); last.focus({ preventScroll: true });
672765
} else if (!e.shiftKey && document.activeElement === last) {
673-
e.preventDefault(); first.focus();
766+
e.preventDefault(); first.focus({ preventScroll: true });
674767
}
675768
};
676769
document.addEventListener('keydown', handler, true);
@@ -710,25 +803,37 @@ if (stickyFilterBar && heroSection) {
710803
if (typeof initializeProject === 'function') initializeProject(name);
711804
});
712805

806+
// Initialize reactive scale calculations
807+
initModalScaling();
808+
713809
removeTrap = trapFocus(modal);
714810
var focusables = getFocusableElements(modalBody);
715811
var firstFocusable = focusables[0] || modalClose;
716812
if (firstFocusable && typeof firstFocusable.focus === 'function') {
717-
firstFocusable.focus();
813+
firstFocusable.focus({ preventScroll: true });
718814
}
719815
}
720816

721817
function closeProjectSafe() {
722818
if (!modal || !modal.classList.contains('active')) return;
819+
820+
destroyModalScaling();
821+
723822
modal.classList.remove('active');
724823
modal.setAttribute('aria-hidden', 'true');
725824
document.body.style.paddingRight = '';
726825
document.body.style.overflow = '';
727826
setMainInert(false);
728827
if (removeTrap) { removeTrap(); removeTrap = null; }
729-
if (modalBody) modalBody.innerHTML = '';
828+
if (modalBody) {
829+
modalBody.innerHTML = '';
830+
modalBody.style.transform = '';
831+
modalBody.style.transformOrigin = '';
832+
modalBody.style.width = '';
833+
modalBody.style.height = '';
834+
}
730835
if (lastFocusedElement && typeof lastFocusedElement.focus === 'function') {
731-
lastFocusedElement.focus();
836+
lastFocusedElement.focus({ preventScroll: true });
732837
}
733838
lastFocusedElement = null;
734839
}

web-app/js/projects.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// Project Registry
1+
// Project Registry
22
// Each project's HTML and logic lives in its own file under js/projects/
33

44
function getProjectHTML(projectName) {
@@ -2091,7 +2091,7 @@ function initCollatz() {
20912091
const yScale = graphHeight / maxValue;
20922092

20932093
// Draw axes
2094-
ctx.strokeStyle = 'var(--text-secondary)';
2094+
ctx.strokeStyle = '#64748b';
20952095
ctx.lineWidth = 2;
20962096
ctx.beginPath();
20972097
ctx.moveTo(padding, padding);

web-app/js/projects/collatz.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -230,7 +230,7 @@ function initCollatz() {
230230
const xStep = graphWidth / (sequence.length - 1);
231231
const yScale = graphHeight / maxValue;
232232

233-
ctx.strokeStyle = 'var(--text-secondary)';
233+
ctx.strokeStyle = '#64748b';
234234
ctx.lineWidth = 2;
235235
ctx.beginPath();
236236
ctx.moveTo(padding, padding);

web-app/js/projects/color-palette.js

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -713,7 +713,6 @@ function initColorPalette() {
713713

714714
output.style.display = 'block';
715715
controls.style.display = 'none';
716-
output.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
717716
});
718717

719718
copyBtn.addEventListener('click', () => {

web-app/js/projects/typing-speed-tester.js

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -257,7 +257,7 @@ function getTypingSpeedTesterHTML() {
257257
inputElement.value = "";
258258
inputElement.disabled = false;
259259
inputElement.removeAttribute("aria-disabled");
260-
inputElement.focus();
260+
inputElement.focus({ preventScroll: true });
261261
262262
result.innerHTML = "";
263263
startTime = Date.now();
@@ -733,7 +733,7 @@ resultDetails.innerHTML = `
733733
requestAnimationFrame(() => sentenceElement.classList.remove('sentence-loading'));
734734
inputElement.value = '';
735735
inputElement.disabled = false;
736-
inputElement.focus();
736+
inputElement.focus({ preventScroll: true });
737737
if (startSession && !sessionStarted) {
738738
sessionStarted = true;
739739
startTime = Date.now();
@@ -743,9 +743,6 @@ resultDetails.innerHTML = `
743743
// ensure pending state
744744
const spans = sentenceElement.querySelectorAll('span');
745745
spans.forEach(s => s.className = 'pending');
746-
if (inputElement.scrollIntoView) {
747-
inputElement.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
748-
}
749746
}
750747

751748
function setDifficulty(mode, { resetGame = false } = {}) {

web-app/js/projects/word-scramble.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -422,7 +422,7 @@ function initWordScramble() {
422422
renderWord(shuffleWord(current.word));
423423
updateStats();
424424
setMessage(keepStreak ? 'Unscramble the letters.' : 'Fresh word loaded.');
425-
guessInput.focus();
425+
guessInput.focus({ preventScroll: true });
426426

427427
// Launch countdown engine for the active round
428428
startTimer();
@@ -506,7 +506,7 @@ function checkGuess() {
506506
shuffleBtn.addEventListener('click', () => {
507507
if (!current) return;
508508
renderWord(shuffleWord(current.word));
509-
guessInput.focus();
509+
guessInput.focus({ preventScroll: true });
510510
});
511511

512512
nextBtn.addEventListener('click', () => {

0 commit comments

Comments
 (0)