Syncing a Format Toolbar Across Four Vite Entry Points
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 Mechanism | Engineering Implementation | Primary Advantage |
|---|---|---|
| Shared State Sync | Vite ES module singleton import | Zero duplication across 4 separate HTML entries |
| Canvas Zoom Control | CSS zoom: var(--pdf-zoom) property | Preserves IntersectionObserver frame math |
| Page Card Containers | CSS .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 CSSzoomovertransform: scalefor zoom containers tracked byIntersectionObserver, and use Vite ES module singletons to share toolbar state across multi-entry builds.