Three floors, 49 tenants, search that jumps to the shop and zooms in on it. The plan is a plain SVG file — the names, logos and opening hours come from data, and every shop is clickable because it is a shape with an id.
| Binding | each <path id="u-101"> in the SVG is matched to a record by id; nothing about the tenants is stored in the file |
| Labels | ContentPlugin places names inside the shapes and swaps in a logo where it fits, a shortened name where it does not, and the unit number as a last resort |
| Zoom | ZoomPlugin handles wheel, drag and pinch, and focusElement() flies to the shop picked in the list |
| Hover | a render callback builds the directory entry — the logo the plan may have had no room for, the category, the level, the hours. placement: 'cursor', so it travels with the pointer the way a tooltip on a map does |
| Highlight | setHighlight('found', [id]) paints the match — the style lives in config, not in the SVG |
| Floors | setSrc() swaps the plan and keeps the same client, subscriptions and plugins |
The popup is mounted on document.body by the library, which is why its styles live in a plain <style> block rather than a scoped one.
Everything else on this page — the search box, the result list, the card — is ordinary Vue. The library draws the map and tells you what was clicked; the product around it stays yours.
scripts/gen-mall.mjs generates the three floors, so the geometry is reproducible rather than hand-drawn once. Vertical cores — escalators, lifts, stairs — are declared once and shared by every level, which means they line up in plan across floors.
Each file also carries a hidden nav layer: a graph of the gallery with data-links between nodes and data-vertical on the ones that sit at a core. The core ignores that layer (role: 'data'), and a routing plugin can read it through getLayer('nav').
Facility icons are Material Design Icons inlined at generation time.