Skip to content

Commit 5277742

Browse files
committed
refactor: resolve modal responsive scaling, focus clipping, and collatz axes color
1 parent 06f6bd4 commit 5277742

5 files changed

Lines changed: 113 additions & 12 deletions

File tree

web-app/js/main.js

Lines changed: 106 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -648,6 +648,95 @@ 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 firstChild = modalBody.firstElementChild;
673+
if (!firstChild) return;
674+
675+
firstChild.style.transform = '';
676+
firstChild.style.transformOrigin = '';
677+
678+
var computedStyle = window.getComputedStyle(modalContent);
679+
var paddingTop = parseFloat(computedStyle.paddingTop) || 32;
680+
var paddingBottom = parseFloat(computedStyle.paddingBottom) || 32;
681+
var paddingLeft = parseFloat(computedStyle.paddingLeft) || 32;
682+
var paddingRight = parseFloat(computedStyle.paddingRight) || 32;
683+
684+
var availableHeight = modalContent.clientHeight - paddingTop - paddingBottom;
685+
var availableWidth = modalContent.clientWidth - paddingLeft - paddingRight;
686+
687+
var contentHeight = firstChild.scrollHeight;
688+
var contentWidth = firstChild.scrollWidth;
689+
690+
if (contentHeight <= 0 || contentWidth <= 0) return;
691+
692+
var zoom = 1;
693+
var heightZoom = availableHeight / contentHeight;
694+
var widthZoom = availableWidth / contentWidth;
695+
696+
zoom = Math.min(heightZoom, widthZoom);
697+
if (zoom > 1) {
698+
zoom = 1;
699+
}
700+
701+
// Apply scale transform and origins
702+
firstChild.style.transform = 'scale(' + zoom + ')';
703+
firstChild.style.transformOrigin = 'top center';
704+
705+
// Constrain wrapper block size to prevent scroll triggering
706+
modalBody.style.height = (contentHeight * zoom) + 'px';
707+
modalBody.style.width = '100%';
708+
modalBody.style.display = 'flex';
709+
modalBody.style.flexDirection = 'column';
710+
modalBody.style.alignItems = 'center';
711+
}
712+
713+
function initModalScaling() {
714+
applyModalScaling();
715+
716+
if (modalResizeObserver) {
717+
modalResizeObserver.disconnect();
718+
}
719+
720+
var modalBody = document.getElementById('modalBody');
721+
var firstChild = modalBody ? modalBody.firstElementChild : null;
722+
if (firstChild) {
723+
modalResizeObserver = new ResizeObserver(function () {
724+
requestAnimationFrame(applyModalScaling);
725+
});
726+
modalResizeObserver.observe(firstChild);
727+
}
728+
729+
window.addEventListener('resize', applyModalScaling);
730+
}
731+
732+
function destroyModalScaling() {
733+
if (modalResizeObserver) {
734+
modalResizeObserver.disconnect();
735+
modalResizeObserver = null;
736+
}
737+
window.removeEventListener('resize', applyModalScaling);
738+
}
739+
651740
// ── Focus Trap for Modal ──────────────────────────────────────────
652741
function getFocusableElements(root) {
653742
var selector =
@@ -668,9 +757,9 @@ if (stickyFilterBar && heroSection) {
668757
var first = focusables[0];
669758
var last = focusables[focusables.length - 1];
670759
if (e.shiftKey && document.activeElement === first) {
671-
e.preventDefault(); last.focus();
760+
e.preventDefault(); last.focus({ preventScroll: true });
672761
} else if (!e.shiftKey && document.activeElement === last) {
673-
e.preventDefault(); first.focus();
762+
e.preventDefault(); first.focus({ preventScroll: true });
674763
}
675764
};
676765
document.addEventListener('keydown', handler, true);
@@ -710,25 +799,37 @@ if (stickyFilterBar && heroSection) {
710799
if (typeof initializeProject === 'function') initializeProject(name);
711800
});
712801

802+
// Initialize reactive scale calculations
803+
initModalScaling();
804+
713805
removeTrap = trapFocus(modal);
714806
var focusables = getFocusableElements(modalBody);
715807
var firstFocusable = focusables[0] || modalClose;
716808
if (firstFocusable && typeof firstFocusable.focus === 'function') {
717-
firstFocusable.focus();
809+
firstFocusable.focus({ preventScroll: true });
718810
}
719811
}
720812

721813
function closeProjectSafe() {
722814
if (!modal || !modal.classList.contains('active')) return;
815+
816+
destroyModalScaling();
817+
723818
modal.classList.remove('active');
724819
modal.setAttribute('aria-hidden', 'true');
725820
document.body.style.paddingRight = '';
726821
document.body.style.overflow = '';
727822
setMainInert(false);
728823
if (removeTrap) { removeTrap(); removeTrap = null; }
729-
if (modalBody) modalBody.innerHTML = '';
824+
if (modalBody) {
825+
modalBody.innerHTML = '';
826+
modalBody.style.transform = '';
827+
modalBody.style.transformOrigin = '';
828+
modalBody.style.width = '';
829+
modalBody.style.height = '';
830+
}
730831
if (lastFocusedElement && typeof lastFocusedElement.focus === 'function') {
731-
lastFocusedElement.focus();
832+
lastFocusedElement.focus({ preventScroll: true });
732833
}
733834
lastFocusedElement = null;
734835
}

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/typing-speed-tester.js

Lines changed: 2 additions & 2 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();

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)