Skip to content

Commit 83d82a1

Browse files
fix(examples): improve mobile support for map, pdf, and threejs examples (#555)
* Fix touch gesture conflicts in WebGL example apps on mobile * Format with prettier * fix(examples): improve mobile support for map, pdf, and threejs examples - map-server: fullscreen button respects safe area insets, hover/opacity pattern with always-visible on touch devices - pdf-server: fit-to-width scale for narrow viewports, responsive toolbar with compact controls on mobile, safe area in fullscreen toolbar via CSS custom properties - threejs-server: fullscreen button respects safe area and always visible on touch, ResizeObserver for responsive width, host containerDimensions for fullscreen height, no container padding in fullscreen, replace useHostStyles with inline style application to avoid overwriting onhostcontextchanged handler
1 parent 704b6e0 commit 83d82a1

File tree

10 files changed

+305
-88
lines changed

10 files changed

+305
-88
lines changed

examples/map-server/mcp-app.html

Lines changed: 78 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -1,72 +1,90 @@
1-
<!DOCTYPE html>
1+
<!doctype html>
22
<html lang="en">
3-
<head>
4-
<meta charset="UTF-8">
5-
<meta name="viewport" content="width=device-width, initial-scale=1.0">
3+
<head>
4+
<meta charset="UTF-8" />
5+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
66
<title>CesiumJS Globe</title>
77
<!-- CesiumJS is loaded dynamically from CDN in mcp-app.ts because static
88
<script src=""> tags don't work in srcdoc iframes -->
99
<style>
10-
html, body {
11-
width: 100%;
12-
height: 100%;
13-
margin: 0;
14-
padding: 0;
15-
overflow: hidden;
16-
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
17-
background: transparent;
18-
}
19-
#cesiumContainer {
20-
width: 100%;
21-
height: 100%;
22-
}
23-
#fullscreen-btn {
24-
position: absolute;
25-
top: 10px;
26-
right: 10px;
27-
width: 36px;
28-
height: 36px;
29-
background: rgba(0, 0, 0, 0.7);
30-
border: none;
31-
border-radius: 6px;
32-
cursor: pointer;
33-
z-index: 1000;
34-
display: none;
35-
align-items: center;
36-
justify-content: center;
37-
transition: background 0.2s;
38-
}
39-
#fullscreen-btn:hover {
40-
background: rgba(0, 0, 0, 0.85);
41-
}
42-
#fullscreen-btn svg {
43-
width: 20px;
44-
height: 20px;
45-
fill: white;
46-
}
47-
#loading {
48-
position: absolute;
49-
top: 50%;
50-
left: 50%;
51-
transform: translate(-50%, -50%);
52-
background: rgba(0, 0, 0, 0.8);
53-
color: white;
54-
padding: 20px 30px;
55-
border-radius: 8px;
56-
font-size: 16px;
57-
z-index: 1001;
58-
}
10+
html,
11+
body {
12+
width: 100%;
13+
height: 100%;
14+
margin: 0;
15+
padding: 0;
16+
overflow: hidden;
17+
font-family:
18+
-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
19+
background: transparent;
20+
}
21+
#cesiumContainer {
22+
width: 100%;
23+
height: 100%;
24+
touch-action: none;
25+
}
26+
#fullscreen-btn {
27+
position: absolute;
28+
top: 10px;
29+
right: 10px;
30+
width: 36px;
31+
height: 36px;
32+
background: rgba(0, 0, 0, 0.7);
33+
border: none;
34+
border-radius: 6px;
35+
cursor: pointer;
36+
z-index: 1000;
37+
display: none;
38+
align-items: center;
39+
justify-content: center;
40+
transition:
41+
background 0.2s,
42+
opacity 0.2s;
43+
opacity: 0;
44+
}
45+
#fullscreen-btn:hover {
46+
background: rgba(0, 0, 0, 0.85);
47+
opacity: 1;
48+
}
49+
#cesiumContainer:hover ~ #fullscreen-btn {
50+
opacity: 0.7;
51+
}
52+
#fullscreen-btn svg {
53+
width: 20px;
54+
height: 20px;
55+
fill: white;
56+
}
57+
#loading {
58+
position: absolute;
59+
top: 50%;
60+
left: 50%;
61+
transform: translate(-50%, -50%);
62+
background: rgba(0, 0, 0, 0.8);
63+
color: white;
64+
padding: 20px 30px;
65+
border-radius: 8px;
66+
font-size: 16px;
67+
z-index: 1001;
68+
}
5969
</style>
60-
</head>
61-
<body>
70+
</head>
71+
<body>
6272
<div id="cesiumContainer"></div>
6373
<button id="fullscreen-btn" title="Toggle fullscreen">
64-
<!-- Expand icon (shown when inline) -->
65-
<svg id="expand-icon" viewBox="0 0 24 24"><path d="M7 14H5v5h5v-2H7v-3zm-2-4h2V7h3V5H5v5zm12 7h-3v2h5v-5h-2v3zM14 5v2h3v3h2V5h-5z"/></svg>
66-
<!-- Compress icon (shown when fullscreen) -->
67-
<svg id="compress-icon" style="display:none" viewBox="0 0 24 24"><path d="M5 16h3v3h2v-5H5v2zm3-8H5v2h5V5H8v3zm6 11h2v-3h3v-2h-5v5zm2-11V5h-2v5h5V8h-3z"/></svg>
74+
<!-- Expand icon (shown when inline) -->
75+
<svg id="expand-icon" viewBox="0 0 24 24">
76+
<path
77+
d="M7 14H5v5h5v-2H7v-3zm-2-4h2V7h3V5H5v5zm12 7h-3v2h5v-5h-2v3zM14 5v2h3v3h2V5h-5z"
78+
/>
79+
</svg>
80+
<!-- Compress icon (shown when fullscreen) -->
81+
<svg id="compress-icon" style="display: none" viewBox="0 0 24 24">
82+
<path
83+
d="M5 16h3v3h2v-5H5v2zm3-8H5v2h5V5H8v3zm6 11h2v-3h3v-2h-5v5zm2-11V5h-2v5h5V8h-3z"
84+
/>
85+
</svg>
6886
</button>
6987
<div id="loading">Loading globe...</div>
7088
<script type="module" src="/src/mcp-app.ts"></script>
71-
</body>
89+
</body>
7290
</html>

examples/map-server/src/mcp-app.ts

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -397,6 +397,17 @@ async function initCesium(): Promise<any> {
397397
// CesiumJS sets image-rendering: pixelated by default which looks bad on scaled displays
398398
// Setting to "auto" allows the browser to apply smooth interpolation
399399
cesiumViewer.canvas.style.imageRendering = "auto";
400+
// Prevent touch events from propagating to the parent scroll view.
401+
// CesiumJS uses pointer events internally, which don't suppress native
402+
// scroll gesture recognition on touch devices. Explicit non-passive touch
403+
// listeners with preventDefault() are needed.
404+
for (const eventName of ["touchstart", "touchmove"] as const) {
405+
cesiumViewer.canvas.addEventListener(
406+
eventName,
407+
(e: TouchEvent) => e.preventDefault(),
408+
{ passive: false },
409+
);
410+
}
400411
// Note: DO NOT set resolutionScale = devicePixelRatio here!
401412
// When useBrowserRecommendedResolution: false, Cesium already uses devicePixelRatio.
402413
// Setting resolutionScale = devicePixelRatio would double the scaling (e.g., 2x2=4x on Retina)
@@ -623,6 +634,16 @@ function updateFullscreenButton(): void {
623634
// Show button only if fullscreen is available
624635
btn.style.display = canFullscreen ? "flex" : "none";
625636

637+
// Position button respecting safe area insets
638+
const insets = context?.safeAreaInsets;
639+
btn.style.top = `${10 + (insets?.top ?? 0)}px`;
640+
btn.style.right = `${10 + (insets?.right ?? 0)}px`;
641+
642+
// Always show button on touch devices (hover doesn't work on mobile)
643+
if (context?.deviceCapabilities?.touch) {
644+
btn.style.opacity = canFullscreen ? "0.7" : "0";
645+
}
646+
626647
// Toggle icons based on current mode
627648
const isFullscreen = currentDisplayMode === "fullscreen";
628649
expandIcon.style.display = isFullscreen ? "none" : "block";
@@ -718,8 +739,8 @@ app.onhostcontextchanged = (params) => {
718739
);
719740
}
720741

721-
// Update button if available modes changed
722-
if (params.availableDisplayModes) {
742+
// Update button if available modes or safe area changed
743+
if (params.availableDisplayModes || params.safeAreaInsets) {
723744
updateFullscreenButton();
724745
}
725746
};

examples/pdf-server/src/mcp-app.css

Lines changed: 43 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -103,13 +103,14 @@ body {
103103
display: flex;
104104
align-items: center;
105105
justify-content: space-between;
106-
padding: 0.5rem 1rem;
106+
padding: 0.5rem 0.5rem;
107107
background: var(--bg000);
108108
border-bottom: 1px solid var(--bg200);
109109
flex-shrink: 0;
110-
gap: 0.5rem;
111-
height: 48px;
110+
gap: 0.25rem 0.5rem;
111+
min-height: 48px;
112112
box-sizing: border-box;
113+
flex-wrap: wrap;
113114
}
114115

115116
.toolbar-left {
@@ -261,7 +262,6 @@ body {
261262
--min-font-size-inv: calc(1 / var(--min-font-size, 1));
262263
}
263264

264-
265265
.text-layer :is(span, br) {
266266
color: transparent;
267267
position: absolute;
@@ -306,7 +306,13 @@ body {
306306
overflow: hidden; /* No scrolling on main - only canvas-container scrolls */
307307
border-radius: 0;
308308
border: none;
309-
padding: 0 !important; /* Ignore safe area insets in fullscreen */
309+
padding: 0 !important; /* Background extends edge-to-edge */
310+
}
311+
312+
.main.fullscreen .toolbar {
313+
padding-top: calc(0.5rem + var(--safe-top, 0px));
314+
padding-left: calc(0.5rem + var(--safe-left, 0px));
315+
padding-right: calc(0.5rem + var(--safe-right, 0px));
310316
}
311317

312318
.main.fullscreen .viewer {
@@ -468,3 +474,35 @@ body {
468474
.loading-indicator.error .loading-indicator-arc {
469475
stroke: #e74c3c;
470476
}
477+
478+
/* Compact toolbar on narrow screens */
479+
@media (max-width: 480px) {
480+
.toolbar-left {
481+
display: none;
482+
}
483+
.toolbar {
484+
justify-content: center;
485+
}
486+
.nav-btn,
487+
.zoom-btn,
488+
.search-btn,
489+
.fullscreen-btn {
490+
width: 28px;
491+
height: 28px;
492+
font-size: 0.85rem;
493+
}
494+
.page-input {
495+
width: 40px;
496+
padding: 0.2rem 0.3rem;
497+
font-size: 0.8rem;
498+
}
499+
.total-pages,
500+
.zoom-level {
501+
font-size: 0.75rem;
502+
min-width: 36px;
503+
}
504+
.toolbar-center,
505+
.toolbar-right {
506+
gap: 0.2rem;
507+
}
508+
}

examples/pdf-server/src/mcp-app.ts

Lines changed: 62 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,36 @@ const loadingIndicatorArc = loadingIndicatorEl.querySelector(
109109
// Track current display mode
110110
let currentDisplayMode: "inline" | "fullscreen" = "inline";
111111

112+
// Whether the user has manually zoomed (disables auto fit-to-width)
113+
let userHasZoomed = false;
114+
115+
/**
116+
* Compute a scale that fits the PDF page width to the available container width.
117+
* Returns null if the container isn't visible or the page width is unavailable.
118+
*/
119+
async function computeFitToWidthScale(): Promise<number | null> {
120+
if (!pdfDocument) return null;
121+
122+
try {
123+
const page = await pdfDocument.getPage(currentPage);
124+
const naturalViewport = page.getViewport({ scale: 1.0 });
125+
const pageWidth = naturalViewport.width;
126+
127+
const container = canvasContainerEl as HTMLElement;
128+
const containerStyle = getComputedStyle(container);
129+
const paddingLeft = parseFloat(containerStyle.paddingLeft);
130+
const paddingRight = parseFloat(containerStyle.paddingRight);
131+
const availableWidth = container.clientWidth - paddingLeft - paddingRight;
132+
133+
if (availableWidth <= 0 || pageWidth <= 0) return null;
134+
if (availableWidth >= pageWidth) return null; // Already fits
135+
136+
return availableWidth / pageWidth;
137+
} catch {
138+
return null;
139+
}
140+
}
141+
112142
/**
113143
* Request the host to resize the app to fit the current PDF page.
114144
* Only applies in inline mode - fullscreen mode uses scrolling.
@@ -783,16 +813,19 @@ function nextPage() {
783813
}
784814

785815
function zoomIn() {
816+
userHasZoomed = true;
786817
scale = Math.min(scale + 0.25, 3.0);
787818
renderPage();
788819
}
789820

790821
function zoomOut() {
822+
userHasZoomed = true;
791823
scale = Math.max(scale - 0.25, 0.5);
792824
renderPage();
793825
}
794826

795827
function resetZoom() {
828+
userHasZoomed = false;
796829
scale = 1.0;
797830
renderPage();
798831
}
@@ -1281,6 +1314,14 @@ app.ontoolresult = async (result: CallToolResult) => {
12811314
loadingIndicatorEl.style.display = "none";
12821315

12831316
showViewer();
1317+
1318+
// Compute fit-to-width scale for narrow containers (e.g. mobile)
1319+
const fitScale = await computeFitToWidthScale();
1320+
if (fitScale !== null) {
1321+
scale = fitScale;
1322+
log.info("Fit-to-width scale:", scale);
1323+
}
1324+
12841325
renderPage();
12851326
// Start background preloading of all pages for text extraction
12861327
startPreloading();
@@ -1308,17 +1349,30 @@ function handleHostContextChanged(ctx: McpUiHostContext) {
13081349
applyHostStyleVariables(ctx.styles.variables);
13091350
}
13101351

1311-
// Apply safe area insets
1352+
// Apply safe area insets — set CSS custom properties for use in both
1353+
// inline mode (padding on .main) and fullscreen mode (padding on .toolbar)
13121354
if (ctx.safeAreaInsets) {
1313-
mainEl.style.paddingTop = `${ctx.safeAreaInsets.top}px`;
1314-
mainEl.style.paddingRight = `${ctx.safeAreaInsets.right}px`;
1315-
mainEl.style.paddingBottom = `${ctx.safeAreaInsets.bottom}px`;
1316-
mainEl.style.paddingLeft = `${ctx.safeAreaInsets.left}px`;
1355+
const { top, right, bottom, left } = ctx.safeAreaInsets;
1356+
mainEl.style.setProperty("--safe-top", `${top}px`);
1357+
mainEl.style.setProperty("--safe-right", `${right}px`);
1358+
mainEl.style.setProperty("--safe-bottom", `${bottom}px`);
1359+
mainEl.style.setProperty("--safe-left", `${left}px`);
1360+
mainEl.style.paddingTop = `${top}px`;
1361+
mainEl.style.paddingRight = `${right}px`;
1362+
mainEl.style.paddingBottom = `${bottom}px`;
1363+
mainEl.style.paddingLeft = `${left}px`;
13171364
}
13181365

1319-
// Log containerDimensions for debugging
1320-
if (ctx.containerDimensions) {
1321-
log.info("Container dimensions:", ctx.containerDimensions);
1366+
// Recompute fit-to-width when container dimensions change
1367+
if (ctx.containerDimensions && pdfDocument && !userHasZoomed) {
1368+
log.info("Container dimensions changed:", ctx.containerDimensions);
1369+
computeFitToWidthScale().then((fitScale) => {
1370+
if (fitScale !== null && Math.abs(fitScale - scale) > 0.01) {
1371+
scale = fitScale;
1372+
log.info("Recomputed fit-to-width scale:", scale);
1373+
renderPage();
1374+
}
1375+
});
13221376
}
13231377

13241378
// Handle display mode changes

0 commit comments

Comments
 (0)