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
This commit is contained in:
parent
704b6e0af9
commit
83d82a1caa
2 changed files with 305 additions and 88 deletions
|
|
@ -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 {
|
||||
|
|
@ -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;
|
||||
|
|
@ -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 {
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
@ -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();
|
||||
}
|
||||
|
|
@ -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();
|
||||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in a new issue