Skip to content

Commit 9767e61

Browse files
Merge pull request #15 from getsentry/mobile-responsive-improvements
feat(orbital): Mobile responsiveness, globe sizing, and UX improvements
2 parents aa5a810 + e1a0597 commit 9767e61

File tree

3 files changed

+190
-19
lines changed

3 files changed

+190
-19
lines changed

static/orbital.css

Lines changed: 103 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,7 @@ body::after {
8181

8282
#globe-container canvas {
8383
display: block;
84+
touch-action: none; /* let OrbitControls own all touch gestures */
8485
}
8586

8687
/* ── UI overlay ──────────────────────────────────────────────── */
@@ -213,15 +214,57 @@ header {
213214
}
214215

215216
.feed-title {
217+
/* button reset */
218+
appearance: none;
219+
background: none;
220+
border: none;
221+
border-bottom: 1px solid rgba(117, 83, 255, 0.10);
222+
border-bottom: 1px solid color-mix(in srgb, var(--sentry-violet) 10%, transparent);
223+
width: 100%;
224+
text-align: left;
225+
font-family: inherit;
226+
/* layout & type */
216227
padding: 10px 14px;
217228
font-size: 9px;
218229
font-weight: 600;
219230
text-transform: uppercase;
220231
letter-spacing: 2px;
221232
color: rgba(158, 134, 255, 0.40);
222233
color: color-mix(in srgb, var(--sentry-violet-soft) 40%, transparent);
223-
border-bottom: 1px solid rgba(117, 83, 255, 0.10);
224-
border-bottom: 1px solid color-mix(in srgb, var(--sentry-violet) 10%, transparent);
234+
display: flex;
235+
justify-content: space-between;
236+
align-items: center;
237+
cursor: pointer;
238+
user-select: none;
239+
}
240+
241+
.feed-title:focus-visible {
242+
outline: 2px solid var(--sentry-violet);
243+
outline-offset: -2px;
244+
border-radius: 4px;
245+
}
246+
247+
.feed-chevron {
248+
font-size: 13px;
249+
line-height: 1;
250+
transition: transform 0.2s ease;
251+
display: inline-block;
252+
}
253+
254+
#event-feed {
255+
pointer-events: auto;
256+
}
257+
258+
#event-feed.collapsed .feed-chevron {
259+
transform: rotate(-90deg);
260+
}
261+
262+
#event-feed.collapsed #feed-list {
263+
display: none;
264+
}
265+
266+
#event-feed.collapsed .feed-title {
267+
border-bottom: none;
225268
}
226269

227270
#feed-list {
@@ -265,6 +308,62 @@ header {
265308
to { opacity: 1; transform: translateX(0); }
266309
}
267310

311+
/* ── Interaction hint ────────────────────────────────────────── */
312+
313+
#interaction-hint {
314+
position: fixed;
315+
top: 58%;
316+
left: 50%;
317+
transform: translateX(-50%) translateY(0);
318+
z-index: 10;
319+
pointer-events: none;
320+
white-space: nowrap;
321+
font-size: 10px;
322+
font-weight: 500;
323+
letter-spacing: 1.8px;
324+
text-transform: uppercase;
325+
color: rgba(158, 134, 255, 0.75);
326+
background: rgba(24, 18, 37, 0.55);
327+
border: 1px solid rgba(117, 83, 255, 0.20);
328+
border-radius: 20px;
329+
padding: 7px 18px;
330+
backdrop-filter: blur(10px);
331+
-webkit-backdrop-filter: blur(10px);
332+
opacity: 0;
333+
animation: hintFade 5.5s ease 1s forwards;
334+
}
335+
336+
@keyframes hintFade {
337+
0% { opacity: 0; transform: translateX(-50%) translateY(6px); }
338+
12% { opacity: 1; transform: translateX(-50%) translateY(0); }
339+
72% { opacity: 1; transform: translateX(-50%) translateY(0); }
340+
100% { opacity: 0; transform: translateX(-50%) translateY(-4px);}
341+
}
342+
343+
/* ── Reduced motion ──────────────────────────────────────────── */
344+
345+
@media (prefers-reduced-motion: reduce) {
346+
#interaction-hint {
347+
animation: none;
348+
opacity: 1;
349+
transition: opacity 0.15s ease;
350+
}
351+
352+
@keyframes slideIn {
353+
from { opacity: 0; }
354+
to { opacity: 1; }
355+
}
356+
357+
.live-dot {
358+
animation: none;
359+
opacity: 1;
360+
}
361+
362+
.feed-chevron {
363+
transition: none;
364+
}
365+
}
366+
268367
/* ── Footer ──────────────────────────────────────────────────── */
269368

270369
#footer {
@@ -317,15 +416,15 @@ header {
317416
/* Move feed to a slim bar along the bottom edge */
318417
#event-feed {
319418
position: fixed;
320-
bottom: 28px;
419+
bottom: 20px;
321420
left: 12px;
322421
right: 12px;
323422
width: auto;
423+
overflow: hidden;
324424
border-radius: 10px;
325425
}
326426

327427
#feed-list {
328-
max-height: 140px;
329428
overflow: hidden;
330429
}
331430

static/orbital.js

Lines changed: 83 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,10 @@ const DOT_DURATION = 14000;
5959
const DISPLAY_RATE = 80;
6060
const FEED_RATE = 320;
6161
const STATS_INTERVAL = 1000;
62-
const MAX_FEED = 14;
62+
const MAX_FEED_DESKTOP = 14;
63+
const MAX_FEED_MOBILE = 4;
64+
// Keep in sync with @media (max-width: 600px) in orbital.css
65+
const MOBILE_BREAKPOINT = 600;
6366
const MARKER_SOFT_LIMIT = 100;
6467
const MARKER_HARD_LIMIT = 400;
6568

@@ -81,9 +84,6 @@ const scene = new THREE.Scene();
8184
const camera = new THREE.PerspectiveCamera(
8285
45, window.innerWidth / window.innerHeight, 0.1, 1000
8386
);
84-
const mobileQuery = window.matchMedia('(max-width: 600px)');
85-
camera.position.z = mobileQuery.matches ? 8.0 : 5.6;
86-
camera.position.y = mobileQuery.matches ? -0.65 : 0;
8787

8888
// ── Stars ────────────────────────────────────────────────────────────────────
8989

@@ -199,10 +199,17 @@ function latLngToVec3(lat, lng, r = GLOBE_RADIUS) {
199199
// Seer is Sentry's AI debugger. It orbits the globe and flies to each new
200200
// error location, beaming down onto it.
201201

202-
const ufoTex = loader.load('/static/seer.png');
202+
let ufoTexLoaded = false;
203+
const ufoTex = loader.load(
204+
'/static/seer.png',
205+
() => { ufoTexLoaded = true; },
206+
undefined,
207+
() => { console.warn('[Sentry Live] Failed to load Seer UFO texture — UFO disabled'); }
208+
);
203209
const ufoMat = new THREE.SpriteMaterial({
204210
map: ufoTex,
205211
transparent: true,
212+
alphaTest: 0.01,
206213
depthWrite: false,
207214
});
208215
const ufo = new THREE.Sprite(ufoMat);
@@ -351,7 +358,59 @@ let staleDrop = false;
351358
let totalSampled = 0;
352359

353360
const elSampled = document.getElementById('total-sampled');
354-
const feedList = document.getElementById('feed-list');
361+
const feedList = document.getElementById('feed-list');
362+
const eventFeed = document.getElementById('event-feed');
363+
364+
// ── Feed toggle ───────────────────────────────────────────────
365+
const feedToggleBtn = eventFeed.querySelector('.feed-title');
366+
367+
// Restore collapsed state from previous session visit
368+
if (sessionStorage.getItem('feedCollapsed') === '1') {
369+
eventFeed.classList.add('collapsed');
370+
feedToggleBtn.setAttribute('aria-expanded', 'false');
371+
}
372+
373+
feedToggleBtn.addEventListener('click', () => {
374+
const nowCollapsed = eventFeed.classList.toggle('collapsed');
375+
feedToggleBtn.setAttribute('aria-expanded', String(!nowCollapsed));
376+
sessionStorage.setItem('feedCollapsed', nowCollapsed ? '1' : '0');
377+
});
378+
379+
// ── Interaction hint ──────────────────────────────────────────
380+
const hintEl = document.getElementById('interaction-hint');
381+
if (hintEl) {
382+
if (sessionStorage.getItem('hintSeen') === '1') {
383+
hintEl.hidden = true;
384+
} else {
385+
// Adapt text for touch devices
386+
if ('ontouchstart' in window || navigator.maxTouchPoints > 0) {
387+
hintEl.textContent = 'Drag to rotate · Pinch to zoom';
388+
}
389+
390+
// For reduced-motion users the CSS removes the animation; handle opacity manually
391+
const reducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
392+
if (reducedMotion) {
393+
// Show static hint; dismiss on first interaction only
394+
hintEl.style.opacity = '1';
395+
}
396+
397+
const dismissHint = () => {
398+
if (reducedMotion) {
399+
hintEl.style.opacity = '0';
400+
}
401+
hintEl.hidden = true;
402+
sessionStorage.setItem('hintSeen', '1');
403+
['pointerdown', 'wheel', 'touchstart'].forEach(t =>
404+
window.removeEventListener(t, dismissHint));
405+
};
406+
407+
['pointerdown', 'wheel', 'touchstart'].forEach(t =>
408+
window.addEventListener(t, dismissHint, { passive: true, once: true }));
409+
410+
// Also dismiss when the CSS animation naturally ends
411+
hintEl.addEventListener('animationend', dismissHint, { once: true });
412+
}
413+
}
355414

356415

357416
function addFeedItem(platform, lat, lng) {
@@ -378,7 +437,8 @@ function addFeedItem(platform, lat, lng) {
378437
li.appendChild(locationSpan);
379438

380439
feedList.insertBefore(li, feedList.firstChild);
381-
while (feedList.children.length > MAX_FEED) feedList.removeChild(feedList.lastChild);
440+
const maxFeed = window.innerWidth <= MOBILE_BREAKPOINT ? MAX_FEED_MOBILE : MAX_FEED_DESKTOP;
441+
while (feedList.children.length > maxFeed) feedList.removeChild(feedList.lastChild);
382442
}
383443

384444
// ── SSE stream ────────────────────────────────────────────────────────────────
@@ -524,31 +584,40 @@ window.addEventListener('pagehide', onPageHidden);
524584

525585
// ── Resize / breakpoint ───────────────────────────────────────────────────────
526586

527-
window.addEventListener('resize', () => {
587+
function onResize() {
528588
camera.aspect = window.innerWidth / window.innerHeight;
529589
camera.updateProjectionMatrix();
530590
renderer.setSize(window.innerWidth, window.innerHeight);
591+
}
592+
593+
window.addEventListener('resize', onResize);
594+
// orientationchange fires before the viewport dimensions settle; a short
595+
// rAF-based delay ensures innerWidth/Height reflect the new orientation.
596+
window.addEventListener('orientationchange', () => {
597+
requestAnimationFrame(() => { requestAnimationFrame(onResize); });
531598
});
532599

533600
// Adjust camera distance and y-offset at the mobile/desktop breakpoint while
534601
// preserving the current orbital angle so autoRotate doesn't snap the globe.
535-
const CAMERA_DESKTOP = { dist: 2.8, y: 0.0 };
536-
const CAMERA_MOBILE = { dist: 3.5, y: -0.15 };
602+
const CAMERA_DESKTOP = { dist: 5.5, y: 0.0, minD: 1.4, maxD: 10 };
603+
const CAMERA_MOBILE = { dist: 9.5, y: -0.8, minD: 4.0, maxD: 20 };
537604

538605
function applyCameraBreakpoint(cfg) {
539606
// Decompose current position into azimuthal angle around Y axis.
540-
const angle = Math.atan2(camera.position.x, camera.position.z);
607+
const angle = Math.atan2(camera.position.x, camera.position.z);
541608
// Horizontal component of the new spherical position.
542-
const hDist = Math.sqrt(Math.max(0, cfg.dist * cfg.dist - cfg.y * cfg.y));
609+
const hDist = Math.sqrt(Math.max(0, cfg.dist * cfg.dist - cfg.y * cfg.y));
543610
camera.position.set(
544611
Math.sin(angle) * hDist,
545612
cfg.y,
546613
Math.cos(angle) * hDist,
547614
);
615+
controls.minDistance = cfg.minD;
616+
controls.maxDistance = cfg.maxD;
548617
controls.update();
549618
}
550619

551-
const mobileQuery = window.matchMedia('(max-width: 768px)');
620+
const mobileQuery = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT}px)`);
552621
mobileQuery.addEventListener('change', e => {
553622
applyCameraBreakpoint(e.matches ? CAMERA_MOBILE : CAMERA_DESKTOP);
554623
});
@@ -564,7 +633,7 @@ function animate() {
564633

565634
// ── Seer UFO state machine ───────────────────────────────────
566635
if (ufoState === 'hidden') {
567-
if (hasErrorLocation && now >= ufoNextAppear) {
636+
if (ufoTexLoaded && hasErrorLocation && now >= ufoNextAppear) {
568637
// Position Seer above the most recent error, slightly offset from globe
569638
const dir = latLngToVec3(lastErrorLat, lastErrorLng).normalize();
570639
ufoHoverPos.copy(dir).multiplyScalar(UFO_ORBIT_RADIUS);

templates/index.html

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
</head>
3636
<body>
3737
<div id="globe-container"></div>
38+
<div id="interaction-hint">Drag to rotate · Scroll to zoom</div>
3839

3940
<div id="ui">
4041
<header>
@@ -51,7 +52,9 @@
5152
</header>
5253

5354
<div id="event-feed">
54-
<div class="feed-title">Live Events</div>
55+
<button class="feed-title" aria-expanded="true" aria-controls="feed-list">
56+
Live Events <span class="feed-chevron" aria-hidden="true"></span>
57+
</button>
5558
<ul id="feed-list"></ul>
5659
</div>
5760
</div>

0 commit comments

Comments
 (0)