Skip to content

Commit 990fd74

Browse files
committed
docs: document shared-iframe soft navigation; tidy the plan
Add a 'Shared-iframe soft navigation' section to the Client Context guide (the anchor's subTabs opt-in, the devframe:frame-nav postMessage protocol, the client-only member docks, and the viewer's dom:iframe:mounted seam), a frameId note + demo pointer in the Hub grouping guide, and a mention in the examples list. Refocus plans/shared-iframe-soft-nav.md now that the hub side has shipped: replace the status banner with a concise 'what shipped + where' list and turn the implementation checklist into the remaining downstream work (the Vite DevTools UI contract and the Nuxt shim).
1 parent 37cee32 commit 990fd74

3 files changed

Lines changed: 60 additions & 21 deletions

File tree

docs/guide/client-context.md

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,3 +153,37 @@ The [a11y inspector](/plugins/a11y)'s in-page agent is the canonical client scri
153153
## Iframe panels
154154

155155
Dock iframes are their own documents, so they connect themselves instead of reading the host page's context: the panel SPA calls `connectDevframe()`, which discovers `./__connection.json` relative to its own base — `mountDevframe` serves the hub's connection meta under every dock base for exactly this. The client script (host page) and the iframe panel then share the server through RPC and shared state, or a same-origin `BroadcastChannel` when the loop must survive static builds.
156+
157+
## Shared-iframe soft navigation
158+
159+
A tool with many internal views — Nuxt DevTools' tabs, say — can surface each view as its own hub dock while they all share **one** live iframe, switching between them with client-side (soft) navigation instead of reloading. One iframe dock is the **anchor**: it owns a `frameId` and opts in with `subTabs`.
160+
161+
```ts
162+
await mountDevframe(ctx, nuxtDevtools, {
163+
dock: { frameId: 'nuxt-devtools', subTabs: { protocol: 'postmessage' } },
164+
})
165+
```
166+
167+
When the anchor's iframe mounts, the client host attaches a **frame-nav adapter** that speaks a small, versioned, origin-locked `postMessage` protocol with the embedded app. The app ships a ~40-line shim on the `devframe:frame-nav` channel:
168+
169+
| Message | Direction | Meaning |
170+
|---|---|---|
171+
| `ready` / `manifest` | frame → host | the current tab list (`{ tabs, current }`), on load and whenever it changes |
172+
| `navigate` | host → frame | show this view (`{ tabId, navTarget }`) — the app routes client-side |
173+
| `navigated` | frame → host | the app navigated internally, so the host highlights the matching dock |
174+
175+
The adapter materializes one [client-only dock](#client-only-docks) per reported tab (id `<frameId>:<tabId>`), each sharing the anchor's `frameId` and carrying a `navTarget`. Selecting a member soft-navigates the shared iframe; navigating inside the iframe moves the hub's active dock — the loop runs both ways with an idempotent guard against echoes. The embedded app needs no hub or RPC dependency, only the shim; a plain iframe with no shim simply stays a single dock.
176+
177+
`frameId` is independent of [`groupId`](./hub#grouping-dock-entries): members sharing one iframe may live in a group, several groups, or none.
178+
179+
### The viewer's part
180+
181+
A viewer keeps one iframe alive per `frameId` (shown/hidden across switches, never re-`src`'d) and, when it mounts that element, sets it on the anchor's dock state and announces it:
182+
183+
```ts
184+
const state = ctx.docks.getStateById(anchorId)!
185+
state.domElements.iframe = iframeEl
186+
state.events.emit('dom:iframe:mounted', iframeEl)
187+
```
188+
189+
That announcement is what the adapter attaches to. Both minimal hubs wire this end to end — see the "Tabbed Tool" in [`examples/minimal-vite-devframe-hub`](https://github.com/devframes/devframe/tree/main/examples/minimal-vite-devframe-hub) and [`examples/minimal-next-devframe-hub`](https://github.com/devframes/devframe/tree/main/examples/minimal-next-devframe-hub), including the SPA's `postMessage` shim.

docs/guide/hub.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -199,6 +199,8 @@ ctx.docks.register({
199199

200200
`groupId` lives on every entry kind, so iframes, launchers, custom-render views, and integration-contributed types (e.g. json-render panels) all join groups the same way. The group and its members stay independent top-level entries in `devframe:docks`; a downstream UI derives the visual collapse by matching each member's `groupId` to the group's `id` and renders members in a popover or sub-navigation. `defaultChildId` names the member opened when the group button is activated.
201201

202+
Grouping is about the dock bar; it does not share an iframe. When several iframe docks should render into **one** live iframe and switch views by soft navigation — a multi-tab tool like Nuxt DevTools hosted as first-class docks — give them a shared `frameId` and mark the anchor with `subTabs`. `frameId` is an axis independent of `groupId`, so shared-iframe members may sit in a group, across groups, or ungrouped. See [Shared-iframe soft navigation](./client-context#shared-iframe-soft-navigation).
203+
202204
### The dual role of `category`
203205

204206
`category` decides an entry's outer bucket on the dock bar (ordered by `DEFAULT_CATEGORIES_ORDER`) — but its meaning shifts once an entry joins a group:
@@ -261,6 +263,8 @@ Two minimal, copyable hubs mount every built-in plugin (git, terminals, code-ser
261263
- [`examples/minimal-vite-devframe-hub/`](https://github.com/devframes/devframe/tree/main/examples/minimal-vite-devframe-hub) — a ~120-line Vite plugin host with a vanilla DOM UI.
262264
- [`examples/minimal-next-devframe-hub/`](https://github.com/devframes/devframe/tree/main/examples/minimal-next-devframe-hub) — the same protocol hosted from a Next.js App Router app.
263265

266+
Both also mount a "Tabbed Tool" that demonstrates [shared-iframe soft navigation](./client-context#shared-iframe-soft-navigation) — one SPA whose tabs surface as separate docks sharing a single iframe.
267+
264268
Every framework's hub host follows the same shape: a thin `DevframeHost` adapter over the framework's dev server, with the dock/commands/messages/terminals protocol unchanged above it.
265269

266270
## Diagnostics

plans/shared-iframe-soft-nav.md

Lines changed: 22 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,19 @@
1-
# Design: Shared-iframe docks with soft navigation
1+
# Shared-iframe docks with soft navigation
22

3-
**Status:** Hub side implemented — hand-off spec for the Vite DevTools UI
4-
**Audience:** Vite DevTools (`@vitejs/devtools-kit`) UI implementers + `@devframes/hub` maintainers
53
**Driving use case:** hosting a foreign multi-tab devtool (e.g. **Nuxt DevTools**) as a set of first-class hub docks that all share **one** live iframe and switch between views via **client-side (soft) navigation**, for a unified experience.
64

7-
> **Landed foundations.** [#129](https://github.com/devframes/devframe/pull/129) shipped client-only `docks.register()`/`docks.update()`; [#130](https://github.com/devframes/devframe/pull/130) reinterpreted a grouped entry's `category` as an in-group sub-category (outer bucket = the group's category) and made `framework` sort first. This spec and the hub-side implementation (types + the frame-nav adapter) build directly on both.
5+
**Status — the hub side is shipped and documented.** This doc is the design record, plus the hand-off spec for the parts that live in other repos: the viewer UI contract (§6) and the embedded-app nav shim (§10).
6+
7+
Shipped in `@devframes/hub`:
8+
9+
- **Data model**`frameId` + `navTarget` + `subTabs` on `DevframeViewIframe` ([`packages/hub/src/types/docks.ts`](../packages/hub/src/types/docks.ts)).
10+
- **Client-only docks**`docks.register()` / `update()` on the client context ([#129](https://github.com/devframes/devframe/pull/129)).
11+
- **Frame-nav adapter**`attachFrameNavClient`, auto-attached by `createDevframeClientHost` ([`packages/hub/src/client/frame-nav.ts`](../packages/hub/src/client/frame-nav.ts)).
12+
- **Docs**[Shared-iframe soft navigation](../docs/guide/client-context.md#shared-iframe-soft-navigation).
13+
- **Examples** — the "Tabbed Tool" in both `examples/minimal-{vite,next}-devframe-hub`.
14+
- **Tests**[`packages/hub/src/client/__tests__/frame-nav.test.ts`](../packages/hub/src/client/__tests__/frame-nav.test.ts).
15+
16+
Remaining work lives downstream (§11): the Vite DevTools UI contract and the Nuxt DevTools shim.
817

918
---
1019

@@ -424,24 +433,16 @@ onTabsChanged(() => post({ type: 'manifest', tabs: tabsSnapshot(), current: curr
424433

425434
---
426435

427-
## 11. Implementation checklist
436+
## 11. Remaining work (downstream repos)
428437

429-
**`@devframes/hub` (data model + adapter):**
430-
- [x] Client-only `docks.register()`/`update()` on the client `DocksEntriesContext`**#129**.
431-
- [x] Add `navTarget` + `subTabs` to `DevframeViewIframe`; add `NavTarget`, `FrameSubTabsConfig` ([`packages/hub/src/types/docks.ts`](../packages/hub/src/types/docks.ts)).
432-
- [x] Ship the frame-nav adapter (`attachFrameNavClient`, [`packages/hub/src/client/frame-nav.ts`](../packages/hub/src/client/frame-nav.ts)); auto-attach on `dom:iframe:mounted` for `subTabs` anchors ([`host.ts`](../packages/hub/src/client/host.ts)).
433-
- [x] Tests: handshake, manifest reconcile add/remove, echo-guard, internal-nav highlight, origin-lock, timeout→no-shim, active-removed→null ([`frame-nav.test.ts`](../packages/hub/src/client/__tests__/frame-nav.test.ts)).
434-
- [x] Worked examples: a `subTabs` "Tabbed Tool" anchor + a postMessage shim SPA, with keep-alive iframe pooling and `dom:iframe:mounted`, in both `examples/minimal-vite-devframe-hub` and `examples/minimal-next-devframe-hub`.
435-
- [ ] Follow-up: a Client Context guide section.
438+
The `@devframes/hub` side is complete (see the status list at the top). What's left lives in other repos:
436439

437-
**Vite DevTools (UI contract):**
438-
- [ ] One kept-alive iframe per `frameId`; hide/show, never re-`src`.
439-
- [ ] Populate `domElements.iframe` + emit `dom:iframe:mounted` on the anchor entry.
440-
- [ ] Render member docks (grouped or not) through the normal selection path.
440+
**Vite DevTools — the UI contract (§6):**
441+
- One kept-alive iframe per `frameId`; hide/show, never re-`src`.
442+
- Populate `domElements.iframe` + emit `dom:iframe:mounted` on the anchor entry.
443+
- Render member docks (grouped or not) through the normal selection path.
441444

442-
**Embedded app (Nuxt DevTools):**
443-
- [ ] Ship the `postMessage` nav shim (§10).
444-
445-
---
445+
**Nuxt DevTools — the embedded app:**
446+
- Ship the `postMessage` nav shim (§10).
446447

447-
*This design was produced with the help of an agent, from a structured requirements interview.*
448+
Both minimal hub examples already implement the viewer contract in miniature (vanilla DOM and React), so they double as a reference for the Vite DevTools port.

0 commit comments

Comments
 (0)