Skip to content

fix(pdf-server): translucent highlights, pinch to/from fullscreen, toolbar layout#587

Merged
ochafik merged 9 commits intomainfrom
ochafik/pdf-highlight-pinch-toolbar
Apr 2, 2026
Merged

fix(pdf-server): translucent highlights, pinch to/from fullscreen, toolbar layout#587
ochafik merged 9 commits intomainfrom
ochafik/pdf-highlight-pinch-toolbar

Conversation

@ochafik
Copy link
Copy Markdown
Contributor

@ochafik ochafik commented Apr 2, 2026

Summary

  • Highlights were opaque — inline background: <hex> overrode the CSS rgba(…,0.35). Now run def.color through cssColorToRgb and force 0.35 alpha so text underneath stays readable.
  • Pinch-in inline → fullscreen — wheel ctrlKey && deltaY<0 or two-finger spread >1.15× triggers toggleFullscreen(). A modeTransitionInFlight latch (250 ms) swallows the gesture tail.
  • Pinch-out fullscreen → exit, with rubber-bandpreviewScale may visibly overshoot down to 0.75×fit so the page pulls away as you pinch. On release: started near fit (≤1.05×) and preview <0.9×fit → exit to inline; otherwise committed scale clamps to ≥fit (snap-back).
  • Fullscreen button floors at fit — never letterboxes.
  • Any fullscreen→inline refits to widthhandleHostContextChanged clears userHasZoomed and arms a one-shot forceNextResizeRefit so the ResizeObserver refits on the iframe shrink (its inline branch normally only refits on growth, to avoid a requestFitToContent loop). Covers pinch, Escape, button, host ×.
  • Fullscreen toolbar heightflex-wrap: nowrap + 0.25rem vertical padding → flat 40px. Dropped --safe-top from toolbar padding (host's own header already clears it). Side/bottom safe-area insets stay.
  • Search-bar collisiontop: 39px in fullscreen, right follows --safe-right.
  • Horizontal swipe → page nav at any fit scale — gate is now actual horizontal overflow (scrollWidth > clientWidth) instead of scale ≤ 1.0, so it works in fullscreen where fit is often >100%.
  • Base .canvas-container gets touch-action: pan-x pan-y so the inline pinch is capturable on iOS.

Test Plan

  • npm run --workspace examples/pdf-server build — type-checks clean
  • e2e:
    • pdf-annotations.spec.ts — added toHaveCSS('background-color', /0\.35/) regression check
    • pdf-viewer-zoom.spec.ts — replaced obsolete "pinch ignored inline" with "pinch-in → .main.fullscreen" + "pinch-out → no-op"
  • Manual in basic-host:
    • Inline → trackpad pinch-in → fullscreen
    • Fullscreen at fit → pinch-out → page shrinks past fit, release → exits to inline → fits inline width
    • Fullscreen at fit → small pinch-out → release → springs back to fit
    • Escape / × in fullscreen after zooming → inline view fits width
    • Fullscreen → horizontal swipe → next/prev page; zoom past width-fit → swipe pans
    • Search in fullscreen → dropdown sits below toolbar

ochafik added 2 commits April 2, 2026 00:42
…olbar layout

Highlights rendered opaque: inline `background: <hex>` overrode the CSS
rgba(...,0.35). Now convert def.color via cssColorToRgb and force alpha
0.35. Added a toHaveCSS regression check in pdf-annotations.spec.ts.

Pinch-in while inline (wheel ctrlKey deltaY<0 or two-finger spread
>1.15x) now enters fullscreen. Pinch-out in fullscreen past 0.9x of
fit-scale, when already at/below fit, exits to inline and clears
userHasZoomed so refit sizes the inline view. previewScaleRaw tracks the
unclamped intent so the exit fires even when fit ~= ZOOM_MIN. A
modeTransitionInFlight latch (held 250ms post-toggle) keeps the gesture
tail from re-toggling or immediately zooming the new view.

Fullscreen toolbar: flex-wrap: nowrap and tighter 0.25rem vertical
padding (min-height 40px + safe-top instead of 48px + wrap). Search bar
top/right now follow --safe-top/--safe-right so it sits flush below the
toolbar instead of overlapping it. Base .canvas-container gets
touch-action: pan-x pan-y so the inline pinch is capturable on iOS.
Hosts wrap fullscreen in their own header (title + close), which already
clears the top safe-area. Adding --safe-top to our toolbar padding-top
double-dipped, leaving a visible gap between the host header and our
toolbar. Keep --safe-left/right (host header doesn't cover the sides).
@pkg-pr-new
Copy link
Copy Markdown

pkg-pr-new bot commented Apr 2, 2026

Open in StackBlitz

@modelcontextprotocol/ext-apps

npm i https://pkg.pr.new/@modelcontextprotocol/ext-apps@587

@modelcontextprotocol/server-basic-preact

npm i https://pkg.pr.new/@modelcontextprotocol/server-basic-preact@587

@modelcontextprotocol/server-basic-react

npm i https://pkg.pr.new/@modelcontextprotocol/server-basic-react@587

@modelcontextprotocol/server-basic-solid

npm i https://pkg.pr.new/@modelcontextprotocol/server-basic-solid@587

@modelcontextprotocol/server-basic-svelte

npm i https://pkg.pr.new/@modelcontextprotocol/server-basic-svelte@587

@modelcontextprotocol/server-basic-vanillajs

npm i https://pkg.pr.new/@modelcontextprotocol/server-basic-vanillajs@587

@modelcontextprotocol/server-basic-vue

npm i https://pkg.pr.new/@modelcontextprotocol/server-basic-vue@587

@modelcontextprotocol/server-budget-allocator

npm i https://pkg.pr.new/@modelcontextprotocol/server-budget-allocator@587

@modelcontextprotocol/server-cohort-heatmap

npm i https://pkg.pr.new/@modelcontextprotocol/server-cohort-heatmap@587

@modelcontextprotocol/server-customer-segmentation

npm i https://pkg.pr.new/@modelcontextprotocol/server-customer-segmentation@587

@modelcontextprotocol/server-debug

npm i https://pkg.pr.new/@modelcontextprotocol/server-debug@587

@modelcontextprotocol/server-map

npm i https://pkg.pr.new/@modelcontextprotocol/server-map@587

@modelcontextprotocol/server-pdf

npm i https://pkg.pr.new/@modelcontextprotocol/server-pdf@587

@modelcontextprotocol/server-scenario-modeler

npm i https://pkg.pr.new/@modelcontextprotocol/server-scenario-modeler@587

@modelcontextprotocol/server-shadertoy

npm i https://pkg.pr.new/@modelcontextprotocol/server-shadertoy@587

@modelcontextprotocol/server-sheet-music

npm i https://pkg.pr.new/@modelcontextprotocol/server-sheet-music@587

@modelcontextprotocol/server-system-monitor

npm i https://pkg.pr.new/@modelcontextprotocol/server-system-monitor@587

@modelcontextprotocol/server-threejs

npm i https://pkg.pr.new/@modelcontextprotocol/server-threejs@587

@modelcontextprotocol/server-transcript

npm i https://pkg.pr.new/@modelcontextprotocol/server-transcript@587

@modelcontextprotocol/server-video-resource

npm i https://pkg.pr.new/@modelcontextprotocol/server-video-resource@587

@modelcontextprotocol/server-wiki-explorer

npm i https://pkg.pr.new/@modelcontextprotocol/server-wiki-explorer@587

commit: 0cb9af4

ochafik added 7 commits April 2, 2026 00:49
…idth

Previously gated on scale <= 1.0, which blocked page-nav in fullscreen
where fit-scale is often >100%. Now gate on actual horizontal overflow
(scrollWidth > clientWidth) so swipe works at any fit-to-width scale and
still defers to native panning once you zoom past it.
handleHostContextChanged calls refitScale() before the iframe has actually
shrunk, and the ResizeObserver's inline branch only refits on width
*growth* (to avoid a requestFitToContent shrink-loop). So the
fullscreen->inline shrink never triggered a refit and the page stayed at
the fullscreen scale.

Add a one-shot forceNextResizeRefit flag, set on fullscreen->inline (both
the pinch-out path and handleHostContextChanged), consumed by the
ResizeObserver on the next size change. One-shot keeps the shrink-loop
guard intact for ordinary inline resizes.
In fullscreen, pinch-out and the zoom-out button now floor at the
fit-to-page scale instead of ZOOM_MIN, so the page never shrinks below
fully-visible (no dead margin around it). previewScaleRaw stays unclamped
so a continued pinch-out past fit still triggers exit-to-inline.
The fit-floor pinned previewScale at fit, but the wheel handler multiplied
the *clamped* previewScale, so it could never accumulate below fit*0.9 to
trigger exit. Drive the wheel accumulator off previewScaleRaw instead, and
bound previewScaleRaw to [floor*0.7, ZOOM_MAX] so it can cross the 0.9
exit threshold without drifting unboundedly (which would make direction
reversal feel sticky). Touch path was unaffected (absolute ratio).
userHasZoomed stayed true after Escape/button/host-× exits, so refitScale()
bailed even though forceNextResizeRefit let the ResizeObserver call it.
Clear userHasZoomed in handleHostContextChanged whenever we land inline —
fullscreen zoom level is meaningless there. Dropped the now-dead
requestFitToContent fallback in the same block.
…lamp

The previewScaleRaw side-channel made the gesture feel dead (page pinned
at fit, no feedback) and the wheel accumulator interaction was fragile.

New model: previewScale is the only tracked value. In fullscreen it may
overshoot down to 0.75*fit so the user *sees* the page pull away as they
pinch out. On commit:
  - started near fit (<=1.05*fit) AND preview <0.9*fit -> exit to inline
  - otherwise clamp committed scale to >=fit (overshoot snaps back)

beginPinch seeds fitScaleAtPinchStart synchronously from  when
!userHasZoomed (the common enter-fullscreen-at-fit case) so the first
frame already has the right floor; the async computeFitScale refines it.
'trackpad pinch is ignored outside fullscreen' is no longer the contract.
Replace with two tests: pinch-in inline -> .main.fullscreen appears;
pinch-out inline -> zoom unchanged, no fullscreen. The two
pdf-annotations.spec.ts failures in the previous run were flaky (passed
on retry).
@ochafik ochafik merged commit 721ccd0 into main Apr 2, 2026
20 checks passed
ochafik added a commit that referenced this pull request Apr 2, 2026
Some hosts overlay UI on the iframe (chat composer, etc.) without
reporting it in safeAreaInsets.bottom, so computeFitScale's 'fit' fills
the iframe but the page bottom sits under the overlay. Flooring the
zoom-out button at fit removed the only escape hatch.

Pinch keeps the fit floor + rubber-band (it needs a defined snap point
for the exit-to-inline gesture). The button now goes to ZOOM_MIN as
before #587.
ochafik added a commit that referenced this pull request Apr 2, 2026
…r) (#589)

Some hosts overlay UI on the iframe (chat composer, etc.) without
reporting it in safeAreaInsets.bottom, so computeFitScale's 'fit' fills
the iframe but the page bottom sits under the overlay. Flooring the
zoom-out button at fit removed the only escape hatch.

Pinch keeps the fit floor + rubber-band (it needs a defined snap point
for the exit-to-inline gesture). The button now goes to ZOOM_MIN as
before #587.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant