ADR-009: Page-Based Drill-Down Navigation

Status

Accepted

Context

Bausteinsicht renders views as draw.io diagrams and lets architects navigate between abstraction levels (context → container → component). Early specification text (UC-7, AC-7.1) and a Key Design Decision in CLAUDE.md described this as single-page semantic zoom: all views on one draw.io canvas, where zooming in reveals the next level of detail and the parent view stays visible (smaller) above for orientation.

The implementation went a different way: one draw.io page per view, with sync-generated cross-page links on elements that have a deeper view, and a generated "back" button to return to the parent. This ADR records that the page-based approach is the accepted design and explains why, resolving the contradiction the documentation audit (#422) surfaced.

Evaluated Approaches

Approach A: Single-page semantic zoom (the original vision)

All views laid out on one canvas; drilling down is zooming; the parent stays on screen.

  • Keeps the parent visible for orientation

  • draw.io has no native semantic zoom / level-of-detail — this would require simulating it by scaling and positioning nested content, a fragile hack

  • Fights bidirectional sync: a single canvas mixing all levels is hard to diff and reconcile against the model

Each view is its own draw.io page; sync adds page links to drill-down elements and a back-navigation button.

  • draw.io pages and page links are native features

  • Each page maps cleanly to one view, which the sync engine already resolves — diff/reconcile stays simple

  • Leaving the page loses the "parent visible" orientation (mitigated by the generated back button)

Weighted Pugh Matrix

Rating scale: -1 = worse than reference, 0 = same, +1 = better. Reference option: A (single-page zoom).

Criterion Weight A: Single-page zoom (Ref) B: Page-based

Native draw.io support

5

0

+1

Bidirectional-sync robustness

4

0

+1

Implementation effort

4

0

+1

Orientation (parent visible while drilling)

3

0

-1

Per-level export/print

2

0

+1

Weighted Results

Criterion A: Single-page zoom (Ref) B: Page-based

Native draw.io support (×5)

0

+5

Bidirectional-sync robustness (×4)

0

+4

Implementation effort (×4)

0

+4

Orientation (×3)

0

-3

Per-level export/print (×2)

0

+2

Total

0

+12

Decision

Accepted — page-based drill-down (Approach B).

Each view is rendered as its own draw.io page. Sync generates a cross-page link on every element that has a deeper view, and a "back" navigation button on child pages. This works with draw.io’s native page model instead of against it, and keeps the bidirectional sync simple (one page ↔ one view).

Consequences

Positive

  • Uses native draw.io pages and links — no fragile semantic-zoom emulation

  • Clean page ↔ view mapping keeps forward/reverse sync tractable

  • Each level exports and prints as a self-contained page

Negative

  • The parent view is not visible while viewing a child level; orientation relies on the generated back button and the page list

Risk

The lost "parent visible" orientation is a usability trade-off, not a correctness or security concern; it creates no new Chapter 11 risk and is explicitly accepted without a tracked risk.

Implementation

  • Page per view and cross-page links: internal/sync/forward.go (pageID := "view-" + viewID, data:page/id, links)

  • Back-navigation button: internal/sync/forward.go (generated nav-back- cell)

Elements may carry an optional link field in the JSONC model, pointing to an external URL or document anchor (e.g. "link": "architecture.adoc#sec-backend"). This supports clickable diagram elements in SVG-based documentation pipelines.

Priority rule: Drill-down links take priority over user-defined links. When both exist for the same element, the drill-down link (data:page/id,…​) is applied last and overwrites the user-defined value. A sync warning is emitted to alert the user.

Rationale: drill-down navigation is the primary navigation mechanism established by this ADR. Overriding it silently via a user-defined link would break diagram navigation without a visible signal.

Reverse sync: link is never read back from draw.io. It is model-authoritative; draw.io edits to the link attribute are overwritten on the next sync.

Extension: Doc/ADR Drilldown Icons (#543)

The element’s own shape link (Extension #483, above) can only ever point to one place at a time — and the priority rule means a drill-down link silently wins over a user-defined link when both apply to the same element. This extension gives the model↔documentation mapping its own, always-clickable carrier, independent of the shape’s own link attribute:

  • link (existing field): in addition to (optionally) owning the element’s shape link, it now also gets a small dedicated icon of its own, so it stays reachable even when a drill-down link owns the shape.

  • decisions (existing field, element.go/badge.go decision badges): each decision badge is now a clickable icon, wrapped in its own <object link="…​">, resolving to the referenced specification.decisions[].file when present. A decision without a file still renders its badge, just not clickable.

  • docLinks (new field, Element.DocLinks/View.DocLinks, internal/model/types.go): a typed array of {type, href, title} entries — type is a free string (not a closed schema enum) with conventional values arc42, adr, persona, prd, spec; each renders as its own icon with a type-specific glyph. Elements get a row of icons in their top-right corner; a view’s own docLinks render as a row on the page itself (positioned below the metadata/legend boxes, mirroring their placement convention — there is no notion of a "free diagram corner" in this codebase, only the max Y/X of existing content).

Mechanism: each icon is its own top-level <object id="doclink-…​" link="…​"> (never a bare <mxCell>, which cannot carry a link attribute in this codebase’s draw.io dialect), positioned via mxGraph’s parent ID attribute — either the element’s own on-page cell ID (element-level icons) or "1" (page-absolute, for view-level icons). Old icons are removed and recreated on every sync for elements (their position is a pure function of element width and icon count, so this is idempotent); the view-level row’s position is captured once and reused on subsequent syncs, avoiding drift. Implementation: internal/sync/doclinks.go.

Exclusion from reverse sync: icon cells are identified by the doclink- ID prefix and excluded from unmanaged-element detection and connector/relationship extraction in internal/sync/diff.go, the same way nav-back- back-navigation buttons are (#205) — otherwise reverse sync would try to import them as new model elements.

PNG degradation: PNG export discards link attributes entirely (same limitation the element shape’s own link already has, see Extension #483) — the icon shapes and glyphs still render, just without click behavior, giving a visible, non-interactive hint rather than silently vanishing.

Reverse sync: like link, none of link/decisions/docLinks icon state is ever read back from draw.io; the model is authoritative and icons are fully regenerated from it each sync.