# @leumas/site-map

The live 'you are here' map of a product's navigation. Walks any resolved nav manifest into a graph of domains → pages → tabs → sub-tabs, locates the current pathname in it, emits it as mermaid, and...


The live **"you are here"** map of a product's navigation — a GTA-style radar you can navigate by
clicking, plus the expanded panel behind it.

It walks any **resolved nav manifest** into a graph of `root → pillar → domain → page → tab →
sub-tab`, locates the current pathname in it, and draws it two ways. It also **emits that graph as
mermaid**, and `ops/tools/domain-diagrams/index.mjs` (`pnpm diagrams`) uses the same emitter — so the
39 reference pages in `ops/reference/diagrams/` and the map in the HUD are one drawing routine, not
two that agree until the next tab is renamed.

## Using it

```jsx
/* SUBPATHS, never the barrel, on a boot path — see "Things that will bite". */
import { SiteMapProvider } from '@leumas/site-map/provider';
import { siteMapScreen } from '@leumas/site-map/screen';
import SiteMapRadarSlot from '@leumas/site-map/radar-slot';
import '@leumas/site-map/site-map.css';

/* MODULE SCOPE. LeviathanWidget keys its open sheet on this array; a fresh identity per render
   remounts the map under whoever is reading it. */
const ASSISTANT_SCREENS = [siteMapScreen()];

<SiteMapProvider nav={resolved} pillars={PILLARS} rootLabel="Leumas Studio" rootHref="/admin">
  <AppLayout assistantScreens={ASSISTANT_SCREENS}>
    <SiteMapRadarSlot />
    …
  </AppLayout>
</SiteMapProvider>
```

`SiteMapRadarSlot` is what makes the radar (and the panel's "Pin as radar") work. Mount it and forget
it: while the radar is closed it reads one value from a store and renders null, and the radar's own
module is never fetched.

The provider must sit **above** whatever draws the map. In Studio that means outside `AppLayout`,
because `AppLayout` is what renders the Leviathan widget that hosts the map screen.

For a product with no assistant — the control plane runs `assistant={false}` on purpose — drop the
screen and keep the slot, which is the whole reason the radar is not Leviathan-only:

```jsx
<SiteMapProvider nav={VENDOR_NAV} rootLabel="Control plane" rootHref="/">
  <AppLayout assistant={false}>
    <SiteMapRadarSlot storageKey="leumas.siteMap.control.v1" />
    …
  </AppLayout>
</SiteMapProvider>
```

`storageKey` is **per product**. Sharing one would mean opening the radar in Studio opened it in the
control plane too.

## The public surface

| Import | What it is |
|---|---|
| `SiteMapProvider` / `useSiteMap()` | builds the graph once, re-locates on navigation. `{ graph, scoped, here, level, zoom }` |
| `SiteMapRadarSlot` | **mount this**, not the radar — a store read while closed, lazy radar when opened |
| `SiteMapRadar` | the compact floating radar itself — corner-docked, three sizes, closable |
| `SiteMapPanel` | the expanded map: `[Radar ｜ Diagram]`, .mmd, Pin |
| `SiteMapStage` | the HUD chrome + pan/zoom surface both renderers sit on |
| `SiteMapView` | just the radar SVG, if you want to place it yourself |
| `usePanZoom()` | drag-to-pan, wheel-to-zoom, fit-to-view — for any canvas-like child |
| `siteMapScreen()` | a `LeviathanWidget` speed-dial screen descriptor |
| `useNavConfirm(onGo)` / `NavConfirm` | the "Go to X?" gate — ONE gate both graphs share |
| `setRadarOpen(key, open?)` | open/close/toggle the radar from anywhere — a menu, a shortcut |
| `@leumas/site-map/graph` | **pure, no React** — `buildSiteGraph`, `buildDomainGraph`, `buildProductGraph`, `locate`, `scopeGraph` |
| `@leumas/site-map/mermaid` | **pure, no React, no Node** — `toMermaid`, `escapeLabel`, `DIAGRAM_PALETTE` |

## Things that will bite

- **`./graph` and `./mermaid` must stay React-free.** `pnpm diagrams` imports them in bare Node with
  no bundler. A React import there does not fail loudly; it breaks the script that keeps the
  checked-in reference diagrams honest.
- **`.lms-hud-tokens` on the host element is load-bearing.** The neon palette (`--hud-cyan`,
  `--hud-violet`, `--hud-line`) is declared on `.lms-hud, .lms-hud-tokens` in `hud-shell.css`. Drop
  the class and the widget renders as a flat borderless box **with no error anywhere**.
- **An href can be claimed by two nodes.** Studio's pillars point straight at their headline domain,
  so seven live paths have two claimants. `locate` breaks the tie by DEPTH, and `seal` must
  **recompute** depth rather than trust the one a sub-graph already carried — trusting it made every
  pillar tie with the domain beneath it and win by array order.
- **`trail` vs `crumbs`.** Studio really does nest a Devices pillar → a Devices domain → a Devices
  page. Light the `trail`; print the `crumbs`, which collapse a run of identical labels.
- **Never import the barrel from a product's `App.jsx`.** No package here declares `sideEffects`, so
  a bundler will not shake it: importing `SiteMapProvider` from `@leumas/site-map` measurably dragged
  the panel, the SVG renderer and `treeLayout2D` onto Studio's boot path — 272.1KB gzip with subpaths
  versus 284.8KB with the barrel. `/provider`, `/screen`, `/radar-slot`, `/store`, `/graph` and
  `/mermaid` are the boot-path-safe entries.
- **The viewBox is in PIXELS.** A 0..1 viewBox was the first attempt: `vector-effect: non-scaling-stroke`
  rescues strokes there, but nothing rescues TEXT — a `font-size: 0.026px` label is clamped up to the
  browser's minimum rendered size while its advances stay tiny, so every label draws as full-size
  letters at hairline spacing.
- **Mermaid is ~600KB and lives behind exactly one `lazy()` edge** (`MermaidView.jsx`). Nothing else
  in the package imports it. `pnpm check:boot` is what proves that claim.
- **Every pick is a QUESTION, not a jump.** `useNavConfirm` sits between the renderers and
  `navigate()`, so a radar dot and a diagram box both raise "Go to X?" with Yes/No (Enter/Escape).
  It is intercepted ONCE in `SiteMapPanel`, which is why neither renderer knows the gate exists and a
  third would inherit it. NOT the kit's `ConfirmDialog` — that is a Modal for irreversible actions,
  and `.st-modal__backdrop` is `position: fixed` and unportalled, which canvas-hud.css says is
  trapped by a `<CanvasHud>`'s `container-type`.
- **Float the gate off `--lms-chud-footer-h`**, the height CanvasHud measures with a ResizeObserver
  (`canvas-hud.css:181` uses the same expression). A guessed offset landed the question on top of the
  zoom control, because the footer's height depends on what a surface puts in it.
- **The Diagram view is ALWAYS the whole domain**, deliberately — the zoom ladder
  (`domain → pillar → site`) drives the RADAR only. The radar is an instrument that has to stay
  legible; the diagram is what you open to see your section whole. Scoping the diagram to the ladder
  meant "zoom out" quietly replaced the picture you asked for.
- **Mermaid's node ids are NOT `flowchart-<id>-<n>`** — they carry the render id too
  (`lms-smap-n_devices_door-2-flowchart-n_devices_door-0`). `idFromMermaidNode` splits on the LAST
  `flowchart-`; anchoring at the start matched nothing and made every diagram node silently
  unclickable.
- **Fit with `offsetWidth`, never `getBoundingClientRect()`.** The rect is the TRANSFORMED box, so
  fitting from it needs the current scale divided back out — and reading that scale inside a
  ResizeObserver races the render that set it. `offsetWidth` is the untransformed layout size, and
  because ResizeObserver does not fire on a transform, `fit()` writing a new scale cannot loop.
- **Container queries against `.lms-chud` must NAME it** (`@container lms-chud (...)`).
  `canvas-hud.css` sets `container-name` and warns why: `@leumas/inputs` puts an ANONYMOUS
  `container-type: inline-size` on its controls, so an unnamed query resolves against whichever
  slider is the nearest ancestor.
- **`treeLayout2D` sorts siblings alphabetically**, so the radar is alphabetical where the sidebar is
  authored. That is a property of the shared layout (it must be stable for data arriving in any
  order), not something this package chose.


---
Source: shared/packages/site-map/README.md
Canonical: https://docs.leumas.tech/p/packages/site-map
