Closed bug report

Per-slot custom viewer override for component pages (swap native 3D/symbol/footprint viewers in place) + cross-viewer highlight bus

Ray · 3mo ago ·closed by Colby Knox

Per-slot custom viewer override for component pages (+ a cross-viewer highlight bus)

The goal (John, via Ray): the three native viewer slots on a component page — the "3D Model" panel and the "Schematic Symbol" / "PCB Footprint" panels — are the only viewers we want, and they have to do everything (cross-highlight, hover tooltips, layer toggles, 3D lifecycle animation). We do NOT want a second viewer widget in the readme, and we do NOT want adom-lbr's default 3-column app. We want to replace each native viewer, in its existing slot/position, with a custom embeddable viewer (from adom-lbr, chipsmith, or an AI-authored export). Today that's impossible — the viewer routes are hardwired and nothing in page.json or the repo overrides them.

Ask 1 — per-slot override (the unblock)

Let page.json point each slot at a self-contained embeddable HTML in the repo, e.g.:

"component": { "viewers": {
  "model_3d":  "viewers/3d.html",
  "symbol":    "viewers/symbol.html",
  "footprint": "viewers/footprint.html"
}}

If a slot has an override, the platform embeds THAT file in the existing slot iframe (same position, same panel chrome, same "Open full view" affordance) instead of its default renderer. Absent → current default. Same sandbox as the readme (it already got connect-src 'self', frame-src https:, readme-height auto-grow, readme-lightbox), so these exports can lazy-load their own repo CAD (.glb/.kicad_sym/.kicad_mod), toggle layers, animate, etc. This is the readme-sandbox contract you already shipped, just addressable to the viewer slots.

Ask 2 — cross-viewer highlight bus

Cross-highlight across slots (hover a symbol pin -> light the footprint pad AND the 3D lead) needs the sibling iframes to talk. Have the parent relay a tiny message between a page's viewer slots: a slot posts { type: "lbr-hover", pin: "1" } (or null to clear) and the parent rebroadcasts to the other slots on that page. Data-only (a pin id), so no security surface. That single primitive makes the whole symbol<->footprint<->3D cross-highlight work with each viewer authored independently.

Ready content

The viewers are the easy part — I've already built self-contained symbol + footprint viewers (inline SVG from the real .kicad_sym/.kicad_mod: pin names, pin-1, cross-highlight, hover tooltips with datasheet function, Copper/Silk/Fab/Courtyard layer toggles). The moment Ask 1 lands I can drop them into viewers/{symbol,footprint}.html and they light up in the native slots. 3D (with the lifecycle animation + lasered MPN John wants) is the bigger build and wants the same override.

Relates to #48 (cross-highlight) — that becomes trivial once these two primitives exist.

5 Replies

Ray · 3mo ago

Important CSP finding while building the custom viewers (relevant to Ask 1's sandbox choice):

2D viewers work under the strict sandbox; 3D does not. I built self-contained symbol + footprint viewers (inline SVG) and they run fine embedded from /blob/.../viewers/*.html — that blob CSP is sandbox allow-scripts; default-src 'none'; script-src 'unsafe-inline' https:; connect-src 'self'. Good enough for SVG + postMessage.

But a real 3D viewer can't run there. I built one (the vendored self-contained Babylon Adom3DViewer, inlined to dodge the .js → application/octet-stream+nosniff serving that blocks <script src>). The bundle loads and inits, but our Draco-compressed board GLB (KHR_draco_mesh_compression) never decodes: the blob CSP has no wasm-unsafe-eval and no worker-src, so Babylon can't compile the Draco WASM or spawn its decode worker. Meanwhile the native /viewer/3d/component/<slug> route renders the same GLB because its CSP is script-src 'self' 'unsafe-inline' 'wasm-unsafe-eval' + worker-src 'self' blob:.

Implication for Ask 1: a custom viewer wired into a native slot must be served under viewer-grade CSP (wasm-unsafe-eval + worker-src 'self' blob:), not the strict readme/blob sandbox — otherwise 3D (and any WASM viewer) is dead on arrival. Two concrete requests:

  1. Per-slot custom viewers render under the same CSP the native viewers already use (wasm + workers allowed).
  2. Serve repo .js as application/javascript (right now it's application/octet-stream+nosniff, so a shared viewer bundle can't be a <script src> — I had to inline 13MB).

Net: our symbol/footprint custom viewers are live in the readme interim now; the custom 3D viewer is built and correct but can only run once it's in a viewer-CSP slot (i.e., Ask 1). Happy to hand over the 3D export as the test payload.

Ray · 3mo ago

@colby — one focused ask to unblock the custom viewers: of the two requirements above, the viewer-grade CSP for per-slot viewers is the critical one. Our symbol + footprint custom viewers already run in the readme interim, but the 3D viewer is fully built and just needs wasm-unsafe-eval + worker-src 'self' blob: (the same CSP /viewer/3d/component/<slug> already uses) to decode the Draco GLB. If the per-slot override serves custom viewer HTML under that existing viewer CSP, our 3D (and the lifecycle animation + lasered MPN on top of it) lights up immediately. The .js → application/javascript serving is a nice-to-have; the CSP is the blocker. Can this piece be prioritized? Happy to hand you our viewers/3d.html as the test payload.

Colby Knox · 3mo ago

Both asks are live on prod, plus the CSP unblock from your last reply.

Ask 1, per-slot override: declare it exactly as you sketched:

"component": { "viewers": {
  "model_3d":  "viewers/3d.html",
  "symbol":    "viewers/symbol.html",
  "footprint": "viewers/footprint.html"
} }

The override is resolved INSIDE the existing /viewer/{3d,symbol,footprint} routes, so it lands in the same slot, same panel chrome, same Open-full-view link, with zero markup changes anywhere -- and an explicit declaration beats the shipped SymView/FpView filename convention. Broken declarations (missing file, traversal, non-html, over 4 MiB) silently fall back to the native renderer: a bad path can never blank a page. Read-gating unchanged.

Your CSP finding, resolved: overrides serve under a VIEWER-GRADE sandbox -- the full readme contract plus 'wasm-unsafe-eval' and worker-src blob: data:. (Slight change from your literal ask of worker-src 'self' blob:: no URL is ever same-origin to an opaque-origin document, so 'self' workers can never load there; blob:/data: is the whole set that CAN work, and it is what Babylon's Draco path actually uses. Your built 3D viewer should run as-is.) connect-src stays 'self': the same anonymity invariant as the readme, your fetches see what an anonymous visitor sees.

Ask 2, the highlight bus: post from any slot:

parent.postMessage({ type: "lbr-hover", pin: "1" }, "*");   // pin: null clears

Every sibling /viewer/ iframe on the page receives a re-serialized { type: "lbr-hover", pin } (pin bounded to 32 chars; the parent never forwards raw payloads; native viewers ignore it). That is the whole primitive: each viewer authored independently, cross-highlight for free.

Verified E2E on staging with a probe component: declared override served in-slot with the wasm/worker grants, then removed cleanly. Full contract documented in docs/readme-sandbox.md (Per-slot custom viewers section), 19-assert suite pinning precedence, CSP, traversal rejection, fallback, and gating.

Wire your three built viewers into stm32f103c8t6 whenever ready -- once the cross-highlight is live on that page, I think this and the cross-highlight half of #48 both close.

Ray · 3mo ago

Yes — saw it, thank you (and for the CSP unblock + override + fallback semantics). This is exactly what we need to make adom-lbr the single Library view and kill the duplicated readme widget.

I'm about to wire our export-viewers into the component-page slots (component.viewers → viewers/{3d,symbol,footprint}.html) and validate end-to-end on a live page — 3D under the new slot CSP especially. Give me that pass and I'll close it the moment it's confirmed working on prod. Really appreciate the fast turnaround.

Colby Knox · 2mo ago

Closing with the end-to-end pass you were about to run; validated on staging today so this does not sit open on a formality.

What was verified, live:

  1. A component page declaring component.viewers.model_3d: viewers/3d.html serves the custom HTML in the /viewer/3d slot, on both the owner-qualified route and the legacy bare route (the slot system picked up the owner-qualified routes from the #61 work transparently).
  2. The served CSP is the viewer-grade one you asked for: sandbox allow-scripts, wasm-unsafe-eval, worker-src blob: data:.
  3. The functional proof, not just headers: a probe viewer served through the slot compiled a WebAssembly module and round-tripped a Worker created from a blob URL inside the real browser context. Those are exactly the two capabilities Draco decode needs, so your built 3D viewer (lifecycle animation, lasered MPN and all) has nothing left blocking it.
  4. The fixture page was created via POST /api/pages and wired by repo push, which also exercises the metadata-detection fix from #67/PR #269, so the API-created path your tooling uses is clean end to end.

The highlight bus half of this issue shipped alongside the slots and is pinned by the custom-viewer suite (data-only relay in app.js; the native 3D viewer joins it and lights the matching pin).

One follow-up you will care about, filed today as adom/wiki#70: referencing a SHARED custom viewer (one file on adom-lbr's page, used by every component) instead of copying viewers/3d.html into each repo. Weigh in there on the reference syntax. If your own end-to-end wiring hits anything the probe did not, reopen with the specifics.

Log in to reply.