Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
138 changes: 78 additions & 60 deletions examples/map-server/mcp-app.html
Original file line number Diff line number Diff line change
@@ -1,72 +1,90 @@
<!DOCTYPE html>
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>CesiumJS Globe</title>
<!-- CesiumJS is loaded dynamically from CDN in mcp-app.ts because static
<script src=""> tags don't work in srcdoc iframes -->
<style>
html, body {
width: 100%;
height: 100%;
margin: 0;
padding: 0;
overflow: hidden;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
background: transparent;
}
#cesiumContainer {
width: 100%;
height: 100%;
}
#fullscreen-btn {
position: absolute;
top: 10px;
right: 10px;
width: 36px;
height: 36px;
background: rgba(0, 0, 0, 0.7);
border: none;
border-radius: 6px;
cursor: pointer;
z-index: 1000;
display: none;
align-items: center;
justify-content: center;
transition: background 0.2s;
}
#fullscreen-btn:hover {
background: rgba(0, 0, 0, 0.85);
}
#fullscreen-btn svg {
width: 20px;
height: 20px;
fill: white;
}
#loading {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background: rgba(0, 0, 0, 0.8);
color: white;
padding: 20px 30px;
border-radius: 8px;
font-size: 16px;
z-index: 1001;
}
html,
body {
width: 100%;
height: 100%;
margin: 0;
padding: 0;
overflow: hidden;
font-family:
-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
background: transparent;
}
#cesiumContainer {
width: 100%;
height: 100%;
touch-action: none;
}
#fullscreen-btn {
position: absolute;
top: 10px;
right: 10px;
width: 36px;
height: 36px;
background: rgba(0, 0, 0, 0.7);
border: none;
border-radius: 6px;
cursor: pointer;
z-index: 1000;
display: none;
align-items: center;
justify-content: center;
transition:
background 0.2s,
opacity 0.2s;
opacity: 0;
}
#fullscreen-btn:hover {
background: rgba(0, 0, 0, 0.85);
opacity: 1;
}
#cesiumContainer:hover ~ #fullscreen-btn {
opacity: 0.7;
}
#fullscreen-btn svg {
width: 20px;
height: 20px;
fill: white;
}
#loading {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background: rgba(0, 0, 0, 0.8);
color: white;
padding: 20px 30px;
border-radius: 8px;
font-size: 16px;
z-index: 1001;
}
</style>
</head>
<body>
</head>
<body>
<div id="cesiumContainer"></div>
<button id="fullscreen-btn" title="Toggle fullscreen">
<!-- Expand icon (shown when inline) -->
<svg id="expand-icon" viewBox="0 0 24 24"><path d="M7 14H5v5h5v-2H7v-3zm-2-4h2V7h3V5H5v5zm12 7h-3v2h5v-5h-2v3zM14 5v2h3v3h2V5h-5z"/></svg>
<!-- Compress icon (shown when fullscreen) -->
<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>
<!-- Expand icon (shown when inline) -->
<svg id="expand-icon" viewBox="0 0 24 24">
<path
d="M7 14H5v5h5v-2H7v-3zm-2-4h2V7h3V5H5v5zm12 7h-3v2h5v-5h-2v3zM14 5v2h3v3h2V5h-5z"
/>
</svg>
<!-- Compress icon (shown when fullscreen) -->
<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>
</button>
<div id="loading">Loading globe...</div>
<script type="module" src="/src/mcp-app.ts"></script>
</body>
</body>
</html>
25 changes: 23 additions & 2 deletions examples/map-server/src/mcp-app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -397,6 +397,17 @@ async function initCesium(): Promise<any> {
// CesiumJS sets image-rendering: pixelated by default which looks bad on scaled displays
// Setting to "auto" allows the browser to apply smooth interpolation
cesiumViewer.canvas.style.imageRendering = "auto";
// Prevent touch events from propagating to the parent scroll view.
// CesiumJS uses pointer events internally, which don't suppress native
// scroll gesture recognition on touch devices. Explicit non-passive touch
// listeners with preventDefault() are needed.
for (const eventName of ["touchstart", "touchmove"] as const) {
cesiumViewer.canvas.addEventListener(
eventName,
(e: TouchEvent) => e.preventDefault(),
{ passive: false },
);
}
// Note: DO NOT set resolutionScale = devicePixelRatio here!
// When useBrowserRecommendedResolution: false, Cesium already uses devicePixelRatio.
// Setting resolutionScale = devicePixelRatio would double the scaling (e.g., 2x2=4x on Retina)
Expand Down Expand Up @@ -623,6 +634,16 @@ function updateFullscreenButton(): void {
// Show button only if fullscreen is available
btn.style.display = canFullscreen ? "flex" : "none";

// Position button respecting safe area insets
const insets = context?.safeAreaInsets;
btn.style.top = `${10 + (insets?.top ?? 0)}px`;
btn.style.right = `${10 + (insets?.right ?? 0)}px`;

// Always show button on touch devices (hover doesn't work on mobile)
if (context?.deviceCapabilities?.touch) {
btn.style.opacity = canFullscreen ? "0.7" : "0";
}

// Toggle icons based on current mode
const isFullscreen = currentDisplayMode === "fullscreen";
expandIcon.style.display = isFullscreen ? "none" : "block";
Expand Down Expand Up @@ -718,8 +739,8 @@ app.onhostcontextchanged = (params) => {
);
}

// Update button if available modes changed
if (params.availableDisplayModes) {
// Update button if available modes or safe area changed
if (params.availableDisplayModes || params.safeAreaInsets) {
updateFullscreenButton();
}
};
Expand Down
48 changes: 43 additions & 5 deletions examples/pdf-server/src/mcp-app.css
Original file line number Diff line number Diff line change
Expand Up @@ -103,13 +103,14 @@ body {
display: flex;
align-items: center;
justify-content: space-between;
padding: 0.5rem 1rem;
padding: 0.5rem 0.5rem;
background: var(--bg000);
border-bottom: 1px solid var(--bg200);
flex-shrink: 0;
gap: 0.5rem;
height: 48px;
gap: 0.25rem 0.5rem;
min-height: 48px;
box-sizing: border-box;
flex-wrap: wrap;
}

.toolbar-left {
Expand Down Expand Up @@ -261,7 +262,6 @@ body {
--min-font-size-inv: calc(1 / var(--min-font-size, 1));
}


.text-layer :is(span, br) {
color: transparent;
position: absolute;
Expand Down Expand Up @@ -306,7 +306,13 @@ body {
overflow: hidden; /* No scrolling on main - only canvas-container scrolls */
border-radius: 0;
border: none;
padding: 0 !important; /* Ignore safe area insets in fullscreen */
padding: 0 !important; /* Background extends edge-to-edge */
}

.main.fullscreen .toolbar {
padding-top: calc(0.5rem + var(--safe-top, 0px));
padding-left: calc(0.5rem + var(--safe-left, 0px));
padding-right: calc(0.5rem + var(--safe-right, 0px));
}

.main.fullscreen .viewer {
Expand Down Expand Up @@ -468,3 +474,35 @@ body {
.loading-indicator.error .loading-indicator-arc {
stroke: #e74c3c;
}

/* Compact toolbar on narrow screens */
@media (max-width: 480px) {
.toolbar-left {
display: none;
}
.toolbar {
justify-content: center;
}
.nav-btn,
.zoom-btn,
.search-btn,
.fullscreen-btn {
width: 28px;
height: 28px;
font-size: 0.85rem;
}
.page-input {
width: 40px;
padding: 0.2rem 0.3rem;
font-size: 0.8rem;
}
.total-pages,
.zoom-level {
font-size: 0.75rem;
min-width: 36px;
}
.toolbar-center,
.toolbar-right {
gap: 0.2rem;
}
}
70 changes: 62 additions & 8 deletions examples/pdf-server/src/mcp-app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,36 @@ const loadingIndicatorArc = loadingIndicatorEl.querySelector(
// Track current display mode
let currentDisplayMode: "inline" | "fullscreen" = "inline";

// Whether the user has manually zoomed (disables auto fit-to-width)
let userHasZoomed = false;

/**
* Compute a scale that fits the PDF page width to the available container width.
* Returns null if the container isn't visible or the page width is unavailable.
*/
async function computeFitToWidthScale(): Promise<number | null> {
if (!pdfDocument) return null;

try {
const page = await pdfDocument.getPage(currentPage);
const naturalViewport = page.getViewport({ scale: 1.0 });
const pageWidth = naturalViewport.width;

const container = canvasContainerEl as HTMLElement;
const containerStyle = getComputedStyle(container);
const paddingLeft = parseFloat(containerStyle.paddingLeft);
const paddingRight = parseFloat(containerStyle.paddingRight);
const availableWidth = container.clientWidth - paddingLeft - paddingRight;

if (availableWidth <= 0 || pageWidth <= 0) return null;
if (availableWidth >= pageWidth) return null; // Already fits

return availableWidth / pageWidth;
} catch {
return null;
}
}

/**
* Request the host to resize the app to fit the current PDF page.
* Only applies in inline mode - fullscreen mode uses scrolling.
Expand Down Expand Up @@ -783,16 +813,19 @@ function nextPage() {
}

function zoomIn() {
userHasZoomed = true;
scale = Math.min(scale + 0.25, 3.0);
renderPage();
}

function zoomOut() {
userHasZoomed = true;
scale = Math.max(scale - 0.25, 0.5);
renderPage();
}

function resetZoom() {
userHasZoomed = false;
scale = 1.0;
renderPage();
}
Expand Down Expand Up @@ -1281,6 +1314,14 @@ app.ontoolresult = async (result: CallToolResult) => {
loadingIndicatorEl.style.display = "none";

showViewer();

// Compute fit-to-width scale for narrow containers (e.g. mobile)
const fitScale = await computeFitToWidthScale();
if (fitScale !== null) {
scale = fitScale;
log.info("Fit-to-width scale:", scale);
}

renderPage();
// Start background preloading of all pages for text extraction
startPreloading();
Expand Down Expand Up @@ -1308,17 +1349,30 @@ function handleHostContextChanged(ctx: McpUiHostContext) {
applyHostStyleVariables(ctx.styles.variables);
}

// Apply safe area insets
// Apply safe area insets — set CSS custom properties for use in both
// inline mode (padding on .main) and fullscreen mode (padding on .toolbar)
if (ctx.safeAreaInsets) {
mainEl.style.paddingTop = `${ctx.safeAreaInsets.top}px`;
mainEl.style.paddingRight = `${ctx.safeAreaInsets.right}px`;
mainEl.style.paddingBottom = `${ctx.safeAreaInsets.bottom}px`;
mainEl.style.paddingLeft = `${ctx.safeAreaInsets.left}px`;
const { top, right, bottom, left } = ctx.safeAreaInsets;
mainEl.style.setProperty("--safe-top", `${top}px`);
mainEl.style.setProperty("--safe-right", `${right}px`);
mainEl.style.setProperty("--safe-bottom", `${bottom}px`);
mainEl.style.setProperty("--safe-left", `${left}px`);
mainEl.style.paddingTop = `${top}px`;
mainEl.style.paddingRight = `${right}px`;
mainEl.style.paddingBottom = `${bottom}px`;
mainEl.style.paddingLeft = `${left}px`;
}

// Log containerDimensions for debugging
if (ctx.containerDimensions) {
log.info("Container dimensions:", ctx.containerDimensions);
// Recompute fit-to-width when container dimensions change
if (ctx.containerDimensions && pdfDocument && !userHasZoomed) {
log.info("Container dimensions changed:", ctx.containerDimensions);
computeFitToWidthScale().then((fitScale) => {
if (fitScale !== null && Math.abs(fitScale - scale) > 0.01) {
scale = fitScale;
log.info("Recomputed fit-to-width scale:", scale);
renderPage();
}
});
}

// Handle display mode changes
Expand Down
Loading
Loading