Skip to content

Commit eaebcd2

Browse files
jeremymanningclaude
andcommitted
Fix colorbar visibility, tutorial z-index, and arrow positioning on mobile
- Colorbar: raise z-index from 15 to 25 (above panels at 20-21), detect bottom drawer vs sidebar by actual layout width instead of viewport size, position above drawer pull when panel is closed on mobile - Tutorial: add body.tutorial-active class to raise panels/pulls (z-10000) above tutorial modal (z-9999), highlights at z-10002, arrows at z-10003 - Arrow: detect hidden targets (zero dimensions) and fall back to parent panel, fixing video panel arrow appearing at top-left on mobile - Arrow refresh: start interval for arrow-only steps (not just highlight steps), so arrows follow elements that move during drawer transitions Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 1ecf6ed commit eaebcd2

3 files changed

Lines changed: 66 additions & 25 deletions

File tree

src/ui/tutorial.css

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,14 +16,25 @@
1616
/* Highlight ring on targeted elements */
1717
.tutorial-highlight {
1818
position: relative;
19-
z-index: 9999;
19+
z-index: 10002 !important;
2020
outline: 2px solid var(--tutorial-accent);
2121
outline-offset: 4px;
2222
border-radius: inherit;
2323
box-shadow: 0 0 0 9999px rgba(0,0,0,0.45), 0 0 8px 2px rgba(0, 105, 62, 0.4);
2424
animation: tutorialPulse 1.5s ease-in-out infinite;
2525
}
2626

27+
/* During tutorial, raise drawers and pulls above the tutorial modal (z-index 9999) */
28+
body.tutorial-active #quiz-panel,
29+
body.tutorial-active #video-panel {
30+
z-index: 10000 !important;
31+
}
32+
body.tutorial-active .quiz-toggle-btn,
33+
body.tutorial-active .video-toggle-btn,
34+
body.tutorial-active .drawer-pull {
35+
z-index: 10001 !important;
36+
}
37+
2738
/* Arrow bounce animations for each direction */
2839
@keyframes tutorialArrowBounce {
2940
0%, 100% { transform: translateX(0); }
@@ -83,8 +94,8 @@
8394
/* Reduced motion */
8495
@media (prefers-reduced-motion: reduce) {
8596
.tutorial-highlight {
86-
animation: none;
87-
box-shadow: 0 0 0 9999px rgba(0,0,0,0.45), 0 0 8px 2px rgba(0, 105, 62, 0.3);
97+
animation: none !important;
98+
box-shadow: 0 0 0 9999px rgba(0,0,0,0.45), 0 0 8px 2px rgba(0, 105, 62, 0.3) !important;
8899
}
89100
#tutorial-modal { transition: none !important; }
90101
#tutorial-overlay { transition: none !important; }

src/ui/tutorial.js

Lines changed: 26 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -425,7 +425,7 @@ function showDismissConfirmation() {
425425
const overlay = document.createElement('div');
426426
overlay.id = 'tutorial-dismiss-confirm';
427427
Object.assign(overlay.style, {
428-
position: 'fixed', inset: '0', zIndex: '10001',
428+
position: 'fixed', inset: '0', zIndex: '10005',
429429
background: 'rgba(0,0,0,0.5)',
430430
display: 'flex', alignItems: 'center', justifyContent: 'center',
431431
});
@@ -479,6 +479,7 @@ export function dismissTutorial() {
479479
saveState();
480480
removeOverlay();
481481
stopHighlightRefresh();
482+
document.body.classList.remove('tutorial-active');
482483
}
483484

484485
export function resetTutorial() {
@@ -549,6 +550,7 @@ function completeTutorial() {
549550
saveState();
550551
removeOverlay();
551552
stopHighlightRefresh();
553+
document.body.classList.remove('tutorial-active');
552554

553555
// Re-select "All (General)" domain and zoom fully out
554556
const allOption = document.querySelector('.custom-select-option[data-value="all"]');
@@ -692,6 +694,7 @@ function showWelcomePrompt() {
692694
// ── Rendering ───────────────────────────────────────────────────────
693695

694696
function renderCurrentStep() {
697+
document.body.classList.add('tutorial-active');
695698
const stepDef = getStepDef(state.step);
696699
if (!stepDef) { completeTutorial(); return; }
697700
// Skip top-level steps marked skipOnMobile on render (e.g. resumed state)
@@ -900,13 +903,16 @@ function executeOnEnter(action) {
900903

901904
function startHighlightRefresh() {
902905
stopHighlightRefresh();
903-
if (!_currentHighlightSelector) return;
906+
907+
// Start refresh if there's a highlight OR an active arrow (arrow-only steps need repositioning too)
908+
const hasArrow = !!document.getElementById('tutorial-arrow');
909+
if (!_currentHighlightSelector && !hasArrow) return;
904910

905911
_highlightInterval = setInterval(() => {
906-
if (!_currentHighlightSelector) { stopHighlightRefresh(); return; }
912+
const arrowEl = document.getElementById('tutorial-arrow');
913+
if (!_currentHighlightSelector && !arrowEl) { stopHighlightRefresh(); return; }
907914

908915
// Reposition arrow (panels may animate after onEnter)
909-
const arrowEl = document.getElementById('tutorial-arrow');
910916
if (arrowEl && arrowEl._targetSelector) {
911917
const target = queryFirst(arrowEl._targetSelector);
912918
if (target) repositionArrow(arrowEl, target, arrowEl._side);
@@ -923,9 +929,7 @@ function stopHighlightRefresh() {
923929
}
924930

925931
function _onResizeHighlight() {
926-
if (!_currentHighlightSelector) return;
927-
928-
// Reposition arrow
932+
// Reposition arrow on resize (works for both highlight and arrow-only steps)
929933
const arrowEl = document.getElementById('tutorial-arrow');
930934
if (arrowEl && arrowEl._targetSelector) {
931935
const target = queryFirst(arrowEl._targetSelector);
@@ -974,7 +978,7 @@ function renderArrow(targetEl, side = 'left') {
974978

975979
Object.assign(arrow.style, {
976980
position: 'fixed',
977-
zIndex: '10000',
981+
zIndex: '10003',
978982
width: '32px',
979983
height: '32px',
980984
pointerEvents: 'none',
@@ -1027,7 +1031,20 @@ function renderArrow(targetEl, side = 'left') {
10271031
}
10281032

10291033
function repositionArrow(arrow, targetEl, side) {
1030-
const rect = targetEl.getBoundingClientRect();
1034+
let rect = targetEl.getBoundingClientRect();
1035+
1036+
// If target is hidden (zero dimensions), try its parent panel as fallback
1037+
if (rect.width === 0 && rect.height === 0) {
1038+
const panel = targetEl.closest('#video-panel, #quiz-panel');
1039+
if (panel) rect = panel.getBoundingClientRect();
1040+
// Still zero? Hide arrow
1041+
if (rect.width === 0 && rect.height === 0) {
1042+
arrow.style.display = 'none';
1043+
return;
1044+
}
1045+
}
1046+
arrow.style.display = '';
1047+
10311048
const size = 32;
10321049
let top, left;
10331050

src/viz/renderer.js

Lines changed: 26 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -138,7 +138,7 @@ export class Renderer {
138138
this._colorbarEl = document.createElement('div');
139139
this._colorbarEl.className = 'map-colorbar';
140140
this._colorbarEl.style.cssText =
141-
'position:absolute;bottom:16px;right:16px;z-index:15;' +
141+
'position:absolute;bottom:16px;right:16px;z-index:25;' +
142142
'width:12px;height:120px;border-radius:6px;cursor:grab;' +
143143
'background:linear-gradient(to bottom, rgb(0,105,62), rgb(245,220,105) 50%, rgb(157,22,46));' +
144144
'box-shadow:0 2px 8px rgba(0,0,0,0.15);border:1px solid rgba(0,0,0,0.1);' +
@@ -808,25 +808,38 @@ export class Renderer {
808808
}
809809
if (this._colorbarUserDragged) return;
810810

811-
if (panelOpen && isMobile) {
812-
// Mobile: panel is a bottom drawer — move colorbar above it
811+
if (panelOpen) {
813812
const panelRect = quizPanel.getBoundingClientRect();
814813
const containerRect = this._container.getBoundingClientRect();
815-
const panelTopRelative = panelRect.top - containerRect.top;
816-
const newBottom = containerRect.height - panelTopRelative + 8;
814+
const isBottomDrawer = panelRect.width > containerRect.width * 0.8;
815+
816+
if (isBottomDrawer) {
817+
// Bottom drawer (mobile portrait): move colorbar above the panel
818+
const panelTopRelative = panelRect.top - containerRect.top;
819+
const newBottom = containerRect.height - panelTopRelative + 8;
820+
this._colorbarEl.style.bottom = newBottom + 'px';
821+
this._colorbarEl.style.right = '16px';
822+
this._colorbarEl.style.left = 'auto';
823+
this._colorbarEl.style.top = 'auto';
824+
} else {
825+
// Right sidebar (desktop or landscape): move colorbar left of the panel
826+
const panelLeftRelative = panelRect.left - containerRect.left;
827+
this._colorbarEl.style.left = (panelLeftRelative - 30) + 'px';
828+
this._colorbarEl.style.bottom = '16px';
829+
this._colorbarEl.style.right = 'auto';
830+
this._colorbarEl.style.top = 'auto';
831+
}
832+
} else if (isMobile) {
833+
// Panel closed on mobile — position above the drawer pull area
834+
const panelRect = quizPanel.getBoundingClientRect();
835+
const containerRect = this._container.getBoundingClientRect();
836+
const newBottom = containerRect.height - (panelRect.top - containerRect.top) + 8;
817837
this._colorbarEl.style.bottom = newBottom + 'px';
818838
this._colorbarEl.style.right = '16px';
819839
this._colorbarEl.style.left = 'auto';
820840
this._colorbarEl.style.top = 'auto';
821-
} else if (panelOpen && !isMobile) {
822-
// Desktop: panel is a right sidebar — move colorbar left of it
823-
const panelWidth = quizPanel.offsetWidth;
824-
this._colorbarEl.style.right = (panelWidth + 24) + 'px';
825-
this._colorbarEl.style.bottom = '16px';
826-
this._colorbarEl.style.left = 'auto';
827-
this._colorbarEl.style.top = 'auto';
828841
} else {
829-
// Panel closed — return to default
842+
// Panel closed on desktop — return to default
830843
this._colorbarEl.style.right = '16px';
831844
this._colorbarEl.style.bottom = '16px';
832845
this._colorbarEl.style.left = 'auto';

0 commit comments

Comments
 (0)