Skip to content

Commit dec2ace

Browse files
jeremymanningclaude
andcommitted
Mobile cleanup: header layout, drawer pull, touch, tutorial, landscape
- Header: right-justify main icons (trophy, suggest, share, tutorial, info); utility buttons (reset, download, upload) scroll right to reveal - Welcome screen: upload left-justified, share+info right-justified - Drawer pull: 48px height, 56x6 bar, distinct background/border for visibility - Drawer pull uses custom event (drawer-pull-toggle) instead of hidden button click - Closed quiz panel height 48px with safe-area-inset-bottom for Android - Touch: tooltip pinning (tap to preview, tap again to open); dismiss on outside tap - Landscape: tutorial modal insets, skip hover-video step on mobile - Minimap: suppress viewport updates during drag - Quiz: 60px padding-bottom so skip button is reachable - Tests updated for new drawer height and open/close model Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 7f76239 commit dec2ace

9 files changed

Lines changed: 212 additions & 122 deletions

File tree

index.html

Lines changed: 49 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,17 @@
111111
z-index: 100;
112112
}
113113
.header-left { display: flex; align-items: center; gap: 0.75rem; }
114-
.header-right { display: flex; align-items: center; gap: 0.5rem; }
114+
.header-right {
115+
display: flex; align-items: center; gap: 0.5rem;
116+
justify-content: flex-end;
117+
overflow-x: auto; overflow-y: hidden;
118+
scrollbar-width: none; /* Firefox */
119+
-webkit-overflow-scrolling: touch;
120+
flex-shrink: 1; min-width: 0;
121+
}
122+
.header-right::-webkit-scrollbar { display: none; }
123+
.header-right .btn-icon,
124+
.header-right .control-btn { flex-shrink: 0; }
115125
.logo {
116126
font-family: var(--font-heading);
117127
font-weight: 700;
@@ -316,7 +326,8 @@
316326
box-shadow: 0 0 8px var(--color-glow-primary);
317327
}
318328
.custom-select-arrow {
319-
font-size: 0.65rem;
329+
display: flex; align-items: center;
330+
font-size: 0.75rem;
320331
color: var(--color-text-muted);
321332
transition: transform 0.2s;
322333
}
@@ -587,6 +598,11 @@
587598
#app[data-screen="welcome"] #minimap-container { display: none; }
588599
#app[data-screen="welcome"] .domain-selector .custom-select { display: none; }
589600
#app[data-screen="welcome"] .control-btn:not([aria-label="Import saved progress"]) { display: none; }
601+
/* Welcome screen: upload (left) — share + info (right) */
602+
#app[data-screen="welcome"] .header-right [aria-label="Import saved progress"] {
603+
order: -1;
604+
margin-right: auto;
605+
}
590606

591607
.sr-only {
592608
position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
@@ -690,11 +706,9 @@
690706
#app[data-screen="map"] .logo { display: none; }
691707
.logo span { display: none; }
692708
.logo { font-size: 0.95rem; }
693-
.header-left { gap: 0.3rem; flex: 1; min-width: 0; }
694-
.header-right { gap: 0.25rem; flex-shrink: 0; }
695-
/* Trophy and suggest buttons visible on mobile */
696-
.control-btn[aria-label="Reset all progress"] { display: none !important; }
697-
.btn-icon { min-width: 34px; min-height: 34px; font-size: 0.85rem; }
709+
.header-left { gap: 0.3rem; flex: 0 0 auto; }
710+
.header-right { gap: 0.25rem; flex: 1; min-width: 0; }
711+
.btn-icon, .control-btn { min-width: 34px; min-height: 34px; font-size: 0.85rem; }
698712
/* Make dropdown arrow easier to see on mobile */
699713
.custom-select-arrow { font-size: 0.9rem; }
700714
/* Hide custom tooltips on touch devices */
@@ -707,8 +721,9 @@
707721
#quiz-panel {
708722
position: absolute;
709723
top: auto; bottom: 0; left: 0; right: 0;
710-
width: 100% !important; height: 0;
724+
width: 100% !important; height: 48px;
711725
padding: 0;
726+
padding-bottom: env(safe-area-inset-bottom, 0px);
712727
transform: none;
713728
border-radius: 16px 16px 0 0;
714729
box-shadow: 0 -4px 20px rgba(0,0,0,0.3);
@@ -717,7 +732,9 @@
717732
flex-shrink: 0;
718733
display: flex;
719734
flex-direction: column;
735+
background: var(--color-surface);
720736
}
737+
#quiz-panel:not(.open) .quiz-content { display: none; }
721738
#quiz-panel.open {
722739
width: 100% !important; height: 55vh;
723740
padding: 0.75rem 1rem;
@@ -728,7 +745,7 @@
728745
overflow-y: auto;
729746
-webkit-overflow-scrolling: touch;
730747
min-height: 0; /* Allow flex child to shrink below content size */
731-
padding-bottom: 12px;
748+
padding-bottom: 60px;
732749
}
733750
/* Compact quiz text on mobile */
734751
#quiz-panel .quiz-question { font-size: 0.9rem; line-height: 1.35; }
@@ -739,57 +756,22 @@
739756
display: flex;
740757
align-items: center;
741758
justify-content: center;
742-
height: 32px;
759+
height: 48px;
743760
cursor: pointer;
744761
flex-shrink: 0;
745762
touch-action: none;
763+
background: var(--color-surface-raised);
764+
border-bottom: 1px solid var(--color-border);
746765
}
747766
.drawer-pull-bar {
748-
width: 40px;
749-
height: 4px;
750-
border-radius: 2px;
751-
background: var(--color-text-muted);
752-
opacity: 0.5;
753-
}
754-
755-
/* ── Collapsed drawer state ── */
756-
#quiz-panel.open.drawer-collapsed {
757-
height: 32px !important;
758-
padding: 0 !important;
759-
overflow: hidden;
760-
}
761-
#quiz-panel.open.drawer-collapsed .quiz-content {
762-
display: none;
763-
}
764-
/* Move toggle button down when drawer is collapsed */
765-
#quiz-panel.open.drawer-collapsed .quiz-toggle-btn {
766-
bottom: 32px;
767+
width: 56px;
768+
height: 6px;
769+
border-radius: 3px;
770+
background: #999;
767771
}
768772

769-
/* ── Quiz toggle: wide bottom pull tab ── */
770-
.quiz-toggle-btn {
771-
top: auto;
772-
bottom: 0;
773-
right: 50%;
774-
transform: translateX(50%);
775-
width: 80px;
776-
height: 32px;
777-
border-radius: 12px 12px 0 0;
778-
border: 1px solid var(--color-border);
779-
border-bottom: none;
780-
background: var(--color-primary);
781-
color: #fff;
782-
box-shadow: 0 -2px 12px rgba(0,0,0,0.25);
783-
z-index: 22;
784-
}
785-
/* Arrow points UP when closed (pull up to open): chevron-left rotated 90° CW = ^ */
786-
.quiz-toggle-btn i { transform: rotate(90deg); }
787-
/* Override JS icon swap: force chevron-right to also point up when closed */
788-
#quiz-panel:not(.open) .quiz-toggle-btn i.fa-chevron-right { transform: rotate(-90deg); }
789-
/* Arrow points DOWN when open (pull down to close) */
790-
#quiz-panel.open .quiz-toggle-btn { bottom: 55vh; right: 50%; background: var(--color-surface); color: var(--color-text-muted); }
791-
#quiz-panel.open .quiz-toggle-btn i.fa-chevron-right { transform: rotate(90deg); }
792-
#quiz-panel.open .quiz-toggle-btn i.fa-chevron-left { transform: rotate(-90deg); }
773+
/* ── Quiz toggle: hidden on mobile (drawer pull replaces it) ── */
774+
.quiz-toggle-btn { display: none !important; }
793775

794776
/* ── Video panel: bottom sheet on mobile (not hidden) ── */
795777
#video-panel {
@@ -843,6 +825,20 @@
843825
touch-action: manipulation;
844826
}
845827
}
828+
829+
/* ── Landscape phone: short viewport, wide screen ── */
830+
@media (max-height: 500px) and (orientation: landscape) {
831+
:root { --header-height: 44px; }
832+
#minimap-container {
833+
width: 160px; height: 120px;
834+
bottom: 12px; left: 12px;
835+
z-index: 10; /* Above quiz/video panel toggles */
836+
}
837+
/* Quiz panel: right-side drawer in landscape */
838+
#quiz-panel.open { height: auto; max-height: 100%; }
839+
/* Video panel: left-side drawer in landscape */
840+
#video-panel.open { height: auto; max-height: 100%; }
841+
}
846842
</style>
847843
</head>
848844
<body>

src/app.js

Lines changed: 28 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -176,6 +176,20 @@ async function boot() {
176176
controls.onExport(handleExport);
177177
controls.onImport(handleImport);
178178

179+
// Move action buttons (upload/download/reset) from domain-selector to header-right
180+
// Append AFTER main icons so they're off-screen to the right on mobile (scroll right to reveal)
181+
const headerRight = headerEl.querySelector('.header-right');
182+
const actionBtns = controls.getActionButtons();
183+
if (headerRight && actionBtns.importButton) {
184+
// Append in order: reset, download, upload (revealed by scrolling right)
185+
headerRight.appendChild(actionBtns.resetButton);
186+
headerRight.appendChild(actionBtns.exportButton);
187+
headerRight.appendChild(actionBtns.importButton);
188+
for (const btn of [actionBtns.resetButton, actionBtns.exportButton, actionBtns.importButton]) {
189+
btn.classList.add('btn-icon');
190+
}
191+
}
192+
179193
const quizPanel = document.getElementById('quiz-panel');
180194
quiz.init(quizPanel);
181195
quiz.onAnswer(handleAnswer);
@@ -318,6 +332,12 @@ async function boot() {
318332
quizToggle.addEventListener('click', () => toggleQuizPanel());
319333
}
320334

335+
// Mobile drawer pull toggle (custom event from quiz.js)
336+
const quizPanelEl = document.getElementById('quiz-panel');
337+
if (quizPanelEl) {
338+
quizPanelEl.addEventListener('drawer-pull-toggle', () => toggleQuizPanel());
339+
}
340+
321341
// Wire auto-advance toggle for tutorial (created dynamically by modes.js)
322342
document.addEventListener('click', (e) => {
323343
if (e.target.closest('.auto-advance-track') || e.target.closest('.auto-advance-label')) {
@@ -590,6 +610,12 @@ async function switchDomain(domainId) {
590610
if (videoToggleBtn) videoToggleBtn.removeAttribute('hidden');
591611
controls.showActionButtons();
592612

613+
// Reset header scroll to start (main icons are first, utility icons scroll right)
614+
const headerIconBar = document.querySelector('.header-right');
615+
if (headerIconBar) {
616+
headerIconBar.scrollLeft = 0;
617+
}
618+
593619
const domainName = registry.getDomains().find(d => d.id === domainId)?.name || domainId;
594620
announce(`Navigated to ${domainName}. ${aggregatedQuestions.length} questions available.`);
595621

@@ -996,13 +1022,6 @@ function toggleQuizPanel(show) {
9961022

9971023
if (show === undefined) show = !quizPanel.classList.contains('open');
9981024

999-
// On mobile, if drawer is collapsed and user is closing the panel,
1000-
// expand the drawer instead (so the pull handle stays accessible)
1001-
if (!show && window.innerWidth <= 480 && $quizDrawerCollapsed.get()) {
1002-
$quizDrawerCollapsed.set(false);
1003-
return;
1004-
}
1005-
10061025
if (show) {
10071026
// On mobile, close the video panel to avoid overlapping bottom sheets
10081027
if (window.innerWidth <= 480) {
@@ -1012,6 +1031,8 @@ function toggleQuizPanel(show) {
10121031
}
10131032
}
10141033
quizPanel.classList.add('open');
1034+
// On mobile, ensure drawer-collapsed is cleared when opening
1035+
if (window.innerWidth <= 480) $quizDrawerCollapsed.set(false);
10151036
if (toggleBtn) {
10161037
toggleBtn.querySelector('i').className = 'fa-solid fa-chevron-right';
10171038
toggleBtn.setAttribute('aria-label', 'Close quiz panel');

src/ui/controls.js

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,9 @@ function createDropdown(placeholder, items, onChange) {
4848

4949
const arrow = document.createElement('span');
5050
arrow.className = 'custom-select-arrow';
51-
arrow.textContent = '\u25BE';
51+
const arrowIcon = document.createElement('i');
52+
arrowIcon.className = 'fa-solid fa-chevron-down';
53+
arrow.appendChild(arrowIcon);
5254

5355
trigger.appendChild(valueSpan);
5456
trigger.appendChild(arrow);
@@ -168,7 +170,6 @@ export function init(headerElement) {
168170
box-shadow: 0 0 8px var(--color-glow-primary);
169171
}
170172
@media (max-width: 768px) {
171-
.header-left { flex: 1; }
172173
.domain-selector { flex: 1; }
173174
}
174175
`;
@@ -255,6 +256,10 @@ export function init(headerElement) {
255256

256257
}
257258

259+
export function getActionButtons() {
260+
return { resetButton, exportButton, importButton };
261+
}
262+
258263
export function onDomainSelect(callback) {
259264
onDomainSelectCb = callback;
260265
}

src/ui/quiz.js

Lines changed: 19 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -270,24 +270,34 @@ export function init(container) {
270270
const quizContent = container.querySelector('.quiz-content');
271271
container.insertBefore(drawerPull, quizContent);
272272

273-
// Tap drawer pull to toggle collapsed state
273+
// Tap drawer pull to toggle panel (mobile) or collapsed state (desktop)
274274
drawerPull.addEventListener('click', () => {
275-
$quizDrawerCollapsed.set(!$quizDrawerCollapsed.get());
275+
if (window.innerWidth <= 480) {
276+
// Dispatch custom event that app.js listens for (more reliable than clicking hidden button)
277+
container.dispatchEvent(new CustomEvent('drawer-pull-toggle', { bubbles: true }));
278+
} else {
279+
$quizDrawerCollapsed.set(!$quizDrawerCollapsed.get());
280+
}
276281
});
277282

278-
// Swipe gesture detection on the quiz panel (mobile only)
283+
// Swipe gesture detection on the quiz panel
279284
let touchStartY = 0;
280285
container.addEventListener('touchstart', (e) => {
281286
touchStartY = e.touches[0].clientY;
282287
}, { passive: true });
283288
container.addEventListener('touchend', (e) => {
284289
const deltaY = e.changedTouches[0].clientY - touchStartY;
285-
if (deltaY > 50) {
286-
// Swipe down → collapse
287-
$quizDrawerCollapsed.set(true);
288-
} else if (deltaY < -50 && $quizDrawerCollapsed.get()) {
289-
// Swipe up when collapsed → expand
290-
$quizDrawerCollapsed.set(false);
290+
if (window.innerWidth <= 480) {
291+
// Mobile: swipe down closes, swipe up opens
292+
if (deltaY > 50 && container.classList.contains('open')) {
293+
container.dispatchEvent(new CustomEvent('drawer-pull-toggle', { bubbles: true }));
294+
} else if (deltaY < -50 && !container.classList.contains('open')) {
295+
container.dispatchEvent(new CustomEvent('drawer-pull-toggle', { bubbles: true }));
296+
}
297+
} else {
298+
// Desktop: swipe toggles collapsed state
299+
if (deltaY > 50) $quizDrawerCollapsed.set(true);
300+
else if (deltaY < -50 && $quizDrawerCollapsed.get()) $quizDrawerCollapsed.set(false);
291301
}
292302
}, { passive: true });
293303

src/ui/tutorial.css

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -99,16 +99,19 @@
9999
box-shadow: 0 0 12px rgba(0,0,0,0.2) !important;
100100
}
101101

102-
/* Allow box-shadow to escape scrollable containers during tutorial highlight */
103-
.quiz-content:has(.tutorial-highlight),
104-
.video-panel-list:has(.tutorial-highlight) {
105-
overflow: visible !important;
102+
/* Allow box-shadow to escape scrollable containers during tutorial highlight (desktop only).
103+
On mobile, keep overflow scrollable and scroll the highlight into view instead. */
104+
@media (min-width: 481px) {
105+
.quiz-content:has(.tutorial-highlight),
106+
.video-panel-list:has(.tutorial-highlight) {
107+
overflow: visible !important;
108+
}
106109
}
107110

108-
/* Tighter highlight around question mode buttons */
111+
/* Tighter highlight around question mode buttons — vertically centered */
109112
.modes-wrapper.tutorial-highlight {
110-
padding-bottom: 0.25rem;
111-
margin-bottom: 0.25rem;
113+
padding: 0.25rem 0;
114+
margin: 0.25rem 0;
112115
}
113116

114117
/* Mobile bottom-sheet adjustments */

0 commit comments

Comments
 (0)