Skip to content

Commit 275fe59

Browse files
committed
Merge remote-tracking branch 'origin/main' into ochafik/pdf-interact
# Conflicts: # examples/pdf-server/src/mcp-app.css # examples/pdf-server/src/mcp-app.ts
2 parents 775833c + 83d82a1 commit 275fe59

10 files changed

Lines changed: 306 additions & 90 deletions

File tree

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: 42 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -100,13 +100,14 @@ body {
100100
display: flex;
101101
align-items: center;
102102
justify-content: space-between;
103-
padding: 0.5rem 1rem;
103+
padding: 0.5rem 0.5rem;
104104
background: var(--bg000);
105105
border-bottom: 1px solid var(--bg200);
106106
flex-shrink: 0;
107-
gap: 0.5rem;
108-
height: 48px;
107+
gap: 0.25rem 0.5rem;
108+
min-height: 48px;
109109
box-sizing: border-box;
110+
flex-wrap: wrap;
110111
}
111112

112113
.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 {
@@ -1163,3 +1169,34 @@ body {
11631169
}
11641170

11651171

1172+
/* Compact toolbar on narrow screens */
1173+
@media (max-width: 480px) {
1174+
.toolbar-left {
1175+
display: none;
1176+
}
1177+
.toolbar {
1178+
justify-content: center;
1179+
}
1180+
.nav-btn,
1181+
.zoom-btn,
1182+
.search-btn,
1183+
.fullscreen-btn {
1184+
width: 28px;
1185+
height: 28px;
1186+
font-size: 0.85rem;
1187+
}
1188+
.page-input {
1189+
width: 40px;
1190+
padding: 0.2rem 0.3rem;
1191+
font-size: 0.8rem;
1192+
}
1193+
.total-pages,
1194+
.zoom-level {
1195+
font-size: 0.75rem;
1196+
min-width: 36px;
1197+
}
1198+
.toolbar-center,
1199+
.toolbar-right {
1200+
gap: 0.2rem;
1201+
}
1202+
}

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

Lines changed: 64 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -276,6 +276,36 @@ const loadingIndicatorArc = loadingIndicatorEl.querySelector(
276276
// Track current display mode
277277
let currentDisplayMode: "inline" | "fullscreen" = "inline";
278278

279+
// Whether the user has manually zoomed (disables auto fit-to-width)
280+
let userHasZoomed = false;
281+
282+
/**
283+
* Compute a scale that fits the PDF page width to the available container width.
284+
* Returns null if the container isn't visible or the page width is unavailable.
285+
*/
286+
async function computeFitToWidthScale(): Promise<number | null> {
287+
if (!pdfDocument) return null;
288+
289+
try {
290+
const page = await pdfDocument.getPage(currentPage);
291+
const naturalViewport = page.getViewport({ scale: 1.0 });
292+
const pageWidth = naturalViewport.width;
293+
294+
const container = canvasContainerEl as HTMLElement;
295+
const containerStyle = getComputedStyle(container);
296+
const paddingLeft = parseFloat(containerStyle.paddingLeft);
297+
const paddingRight = parseFloat(containerStyle.paddingRight);
298+
const availableWidth = container.clientWidth - paddingLeft - paddingRight;
299+
300+
if (availableWidth <= 0 || pageWidth <= 0) return null;
301+
if (availableWidth >= pageWidth) return null; // Already fits
302+
303+
return availableWidth / pageWidth;
304+
} catch {
305+
return null;
306+
}
307+
}
308+
279309
/**
280310
* Request the host to resize the app to fit the current PDF page.
281311
* Only applies in inline mode - fullscreen mode uses scrolling.
@@ -4033,16 +4063,19 @@ function scrollSelectionIntoView(): void {
40334063
}
40344064

40354065
function zoomIn() {
4066+
userHasZoomed = true;
40364067
scale = Math.min(scale + 0.25, 3.0);
40374068
renderPage().then(scrollSelectionIntoView);
40384069
}
40394070

40404071
function zoomOut() {
4072+
userHasZoomed = true;
40414073
scale = Math.max(scale - 0.25, 0.5);
40424074
renderPage().then(scrollSelectionIntoView);
40434075
}
40444076

40454077
function resetZoom() {
4078+
userHasZoomed = false;
40464079
scale = 1.0;
40474080
renderPage().then(scrollSelectionIntoView);
40484081
}
@@ -4887,6 +4920,14 @@ app.ontoolresult = async (result: CallToolResult) => {
48874920
syncFormValuesToStorage();
48884921

48894922
updateAnnotationsBadge();
4923+
4924+
// Compute fit-to-width scale for narrow containers (e.g. mobile)
4925+
const fitScale = await computeFitToWidthScale();
4926+
if (fitScale !== null) {
4927+
scale = fitScale;
4928+
log.info("Fit-to-width scale:", scale);
4929+
}
4930+
48904931
renderPage();
48914932
// Start background preloading of all pages for text extraction
48924933
startPreloading();
@@ -5196,17 +5237,30 @@ function handleHostContextChanged(ctx: McpUiHostContext) {
51965237
applyHostStyleVariables(ctx.styles.variables);
51975238
}
51985239

5199-
// Apply safe area insets
5240+
// Apply safe area insets — set CSS custom properties for use in both
5241+
// inline mode (padding on .main) and fullscreen mode (padding on .toolbar)
52005242
if (ctx.safeAreaInsets) {
5201-
mainEl.style.paddingTop = `${ctx.safeAreaInsets.top}px`;
5202-
mainEl.style.paddingRight = `${ctx.safeAreaInsets.right}px`;
5203-
mainEl.style.paddingBottom = `${ctx.safeAreaInsets.bottom}px`;
5204-
mainEl.style.paddingLeft = `${ctx.safeAreaInsets.left}px`;
5205-
}
5206-
5207-
// Log containerDimensions for debugging
5208-
if (ctx.containerDimensions) {
5209-
log.info("Container dimensions:", ctx.containerDimensions);
5243+
const { top, right, bottom, left } = ctx.safeAreaInsets;
5244+
mainEl.style.setProperty("--safe-top", `${top}px`);
5245+
mainEl.style.setProperty("--safe-right", `${right}px`);
5246+
mainEl.style.setProperty("--safe-bottom", `${bottom}px`);
5247+
mainEl.style.setProperty("--safe-left", `${left}px`);
5248+
mainEl.style.paddingTop = `${top}px`;
5249+
mainEl.style.paddingRight = `${right}px`;
5250+
mainEl.style.paddingBottom = `${bottom}px`;
5251+
mainEl.style.paddingLeft = `${left}px`;
5252+
}
5253+
5254+
// Recompute fit-to-width when container dimensions change
5255+
if (ctx.containerDimensions && pdfDocument && !userHasZoomed) {
5256+
log.info("Container dimensions changed:", ctx.containerDimensions);
5257+
computeFitToWidthScale().then((fitScale) => {
5258+
if (fitScale !== null && Math.abs(fitScale - scale) > 0.01) {
5259+
scale = fitScale;
5260+
log.info("Recomputed fit-to-width scale:", scale);
5261+
renderPage();
5262+
}
5263+
});
52105264
}
52115265

52125266
// Handle display mode changes

0 commit comments

Comments
 (0)