Engineering Journal
Pdf Processor
Pdf Processor

Syncing a Format Toolbar Across Four Vite Entry Points

2026-05-14

TLDR

Sharing interactive format toolbars across separate HTML entry points without duplicating code or bundling complex SPA framework routers requires three lightweight design patterns: module singletons for state sharing across Vite entry points, CSS --zoom custom properties to maintain IntersectionObserver scroll-sync coordinate frames, and CSS :has() selectors for zero-JS page-card layout styling.
Feature MechanismEngineering ImplementationPrimary Advantage
Shared State SyncVite ES module singleton importZero duplication across 4 separate HTML entries
Canvas Zoom ControlCSS zoom: var(--pdf-zoom) propertyPreserves IntersectionObserver frame math
Page Card ContainersCSS .container:has([data-page])Zero-JS responsive layout styling

Technical problem statement

The Ginexys PDF Processor requires identical format toolbar features across four distinct HTML entry points (Uploader, Monaco Editor, Visual Diff, Multi-Doc Compare).

Each entry HTML is compiled as an independent Vite entry point without a shared SPA router. The toolbar must manage text alignment, CSS Grid element distribution, and viewport zoom without introducing divergent state implementations.


Technical architecture & implementation

1. CSS grid child distribution engine

Selected block elements are arranged into a dynamic flex container by finding their nearest common ancestor:
// Distribute 2+ selected block children evenly across row or column axis
export function applyBlockDistribution(direction) {
  const selectedElements = Array.from(document.querySelectorAll('[data-selected="true"]'));
  if (selectedElements.length < 2) return;

const parentContainer = findNearestCommonAncestor(selectedElements); if (!parentContainer) return;

const currentMode = parentContainer.dataset.distributed;

// Toggle off if clicking active direction button again if (currentMode === direction) { parentContainer.style.display = ''; delete parentContainer.dataset.distributed; return; }

parentContainer.style.display = 'flex'; parentContainer.style.flexDirection = direction === 'row' ? 'row' : 'column'; parentContainer.style.justifyContent = direction === 'row' ? 'space-between' : 'space-evenly'; parentContainer.style.gap = direction === 'column' ? 'var(--distribute-gap, 1rem)' : ''; parentContainer.dataset.distributed = direction; }


2. Layout-Aware zoom control vs. transform: scale

Using transform: scale changes visual bounds without updating scroll geometry, breaking IntersectionObserver scroll-sync math. Using CSS zoom updates viewport geometry and scroll offsets consistently:
export function applyViewportZoom(zoomLevel, $pdfPanel) {
  const clampedZoom = Math.max(0.5, Math.min(3.0, zoomLevel));
  $pdfPanel[0].style.setProperty('--pdf-zoom', clampedZoom);
}
/ Maintain true layout geometry in scroll containers /
.pdf-scroll-container {
  zoom: var(--pdf-zoom, 1);
}

3. Zero-JS page card layout via CSS :has()

Apply card backgrounds and column spacing automatically when page elements are injected into scroll containers:
/ Zero-JS layout styling triggered when [data-page] elements enter DOM /
.pdf-scroll-container:has([data-page]) {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 24px;
  padding: 24px;
}

[data-page] { background: var(--surface-bg); border-radius: 4px; box-shadow: 0 2px 12px rgba(0, 0, 0, 0.12); overflow: hidden; }

Rule of thumb: Use CSS zoom over transform: scale for zoom containers tracked by IntersectionObserver, and use Vite ES module singletons to share toolbar state across multi-entry builds.
Read this post in the full Engineering Journal →