Skip to content

Commit a73faf8

Browse files
committed
docs(examples): register a client-only json-render dock in the hub examples
1 parent e00e389 commit a73faf8

5 files changed

Lines changed: 158 additions & 1 deletion

File tree

docs/examples/minimal-next-devframe-hub.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ Package: `minimal-next-devframe-hub` · framework: **React (Next.js)**
1616
- The built-in `hub:commands:execute` RPC dispatches any registered server command, regardless of how the host was constructed.
1717
- The browser-side `connectDevframe({ baseURL: '/__hub/' })` discovers the WS endpoint via the Next route handler at `/__hub/__connection.json`, which starts the singleton host on demand.
1818
- The [JSON-render](/guide/json-render) hub integration with **registry replacement**: the host authors a view and projects it onto a `json-render` dock, and the React client renders it with a small in-example React registry (rather than the Vue `@devframes/json-render-ui`) — the path a non-Vue host uses.
19+
- [Client-only docks](/guide/client-context#client-only-docks) the page registers itself with `context.docks.register()`: an iframe dock rendered from a Blob URL, and a `json-render` dock whose spec is authored in the browser and seeded into a client-local shared state — rendered by the same React registry as the server-authored view, yet never syncing to the hub or other viewers.
1920

2021
## Run it
2122

docs/examples/minimal-vite-devframe-hub.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ Package: `minimal-vite-devframe-hub` · framework: **Vanilla TypeScript (Vite)**
1616
- The built-in `hub:commands:execute` RPC dispatches any registered server command, regardless of how the host was constructed.
1717
- The browser-side `connectDevframe({ baseURL: '/__hub/' })` discovers the WS endpoint via the kit's `__connection.json` middleware.
1818
- The opt-in [JSON-render](/guide/json-render) hub integration end to end: the host authors a view on its hub context and projects it onto a `json-render` dock, and the client host renders it via `@devframes/json-render-ui` (registered through `createDevframeClientHost({ renderers })`).
19+
- [Client-only docks](/guide/client-context#client-only-docks) the page registers itself with `context.docks.register()`: an iframe dock rendered from a Blob URL, and a `json-render` dock whose spec is authored in the browser and seeded into a client-local shared state — rendered by the same renderer as the server-authored view, yet never syncing to the hub or other viewers.
1920

2021
## Run it
2122

docs/guide/client-context.md

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,24 @@ handle.dispose() // remove it
9090

9191
Client-only docks merge into the same `docks.entries` list, group, select, and load their client scripts exactly like server docks — they just never sync to the hub or other viewers. A client dock sharing an id with a server dock overrides it locally. `ctx.docks.update(entry)` replaces a previously registered client dock wholesale. Registering an id that a client dock already owns throws unless you pass `register(entry, true)`.
9292

93+
A client-only dock can render a [JSON-render](./json-render) view the page authors itself. Seed the spec as this client's local value for the dock's `stateKey` — passing only `initialValue` and never mutating it keeps the spec local to this page — then register a `json-render` dock pointing at that key. With a `json-render` renderer registered at boot, it renders through the same path as a server-authored view:
94+
95+
```ts
96+
import { JSON_RENDER_UPSTREAM_VERSION } from '@devframes/json-render'
97+
98+
await ctx.rpc.sharedState.get('client:json-render:metrics', {
99+
initialValue: spec, // a DevframeJsonRenderSpec built in the browser
100+
})
101+
102+
ctx.docks.register({
103+
id: 'client-metrics',
104+
title: 'Client Metrics',
105+
icon: 'ph:gauge-duotone',
106+
type: 'json-render',
107+
view: { stateKey: 'client:json-render:metrics', upstreamVersion: JSON_RENDER_UPSTREAM_VERSION },
108+
})
109+
```
110+
93111
## Dock client scripts
94112

95113
A dock entry declares its client script as a `ClientScriptEntry``{ importFrom, importName? }`, where `importName` defaults to `'default'`. The field depends on the entry kind:

examples/minimal-next-devframe-hub/src/client/app/page.tsx

Lines changed: 71 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,10 @@ import type {
88
DevframeTerminalSession,
99
DevframeViewIframe,
1010
} from '@devframes/hub/types'
11+
import type { DevframeJsonRenderSpec } from '@devframes/json-render'
12+
import type { DevframeJsonRenderDockEntry } from '@devframes/json-render/hub'
1113
import { connectDevframe, createDevframeClientHost } from '@devframes/hub/client'
14+
import { JSON_RENDER_UPSTREAM_VERSION } from '@devframes/json-render'
1215
import { useEffect, useMemo, useRef, useState } from 'react'
1316
import { createReactJsonRenderDockRenderer } from '../json-render/dock-renderer'
1417
import { iconClass } from './icons'
@@ -56,6 +59,52 @@ function createClientNotesUrl(): string {
5659
return URL.createObjectURL(new Blob([html], { type: 'text/html' }))
5760
}
5861

62+
// The shared-state key the client-only json-render dock renders from. It uses a
63+
// `client:` prefix rather than the server's `devframe:json-render:<scope>:<id>`
64+
// namespace to signal it is authored here, not by a node `createJsonRenderView`.
65+
const CLIENT_JSON_RENDER_KEY = 'client:json-render:metrics'
66+
67+
// A json-render spec synthesized entirely in the browser — the client-only
68+
// counterpart to a server-authored view. It reads real values captured from the
69+
// page at registration time and uses the same base-catalog components the hub's
70+
// server view does, rendered here by the mini React registry.
71+
function createClientMetricsSpec(clientType: string): DevframeJsonRenderSpec {
72+
return {
73+
root: 'root',
74+
elements: {
75+
root: { type: 'Stack', props: { gap: 14 }, children: ['head', 'about', 'env'] },
76+
head: { type: 'Stack', props: { direction: 'row', gap: 8, align: 'center' }, children: ['icon', 'title', 'badge'] },
77+
icon: { type: 'Icon', props: { name: 'ph:gauge-duotone', size: 22 }, children: [] },
78+
title: { type: 'Text', props: { text: 'Client Metrics', variant: 'heading' }, children: [] },
79+
badge: { type: 'Badge', props: { text: 'client-only', variant: 'info' }, children: [] },
80+
about: { type: 'Card', props: { title: 'About this dock' }, children: ['aboutText'] },
81+
aboutText: {
82+
type: 'Text',
83+
props: {
84+
text: 'This json-render view was authored in the browser and seeded into a client-local shared state — it never reaches the hub server or other viewers, yet renders through the same dock renderer as a server-authored view.',
85+
variant: 'body',
86+
color: 'muted',
87+
},
88+
children: [],
89+
},
90+
env: { type: 'Card', props: { title: 'Environment' }, children: ['envTable'] },
91+
envTable: {
92+
type: 'KeyValueTable',
93+
props: {
94+
data: {
95+
clientType,
96+
language: navigator.language,
97+
viewport: `${window.innerWidth}×${window.innerHeight}`,
98+
online: navigator.onLine ? 'yes' : 'no',
99+
},
100+
},
101+
children: [],
102+
},
103+
},
104+
state: {},
105+
}
106+
}
107+
59108
/** Render a dock icon, falling back to the title's initial when unmapped. */
60109
function DockIcon({ entry }: { entry: DevframeDockEntry }) {
61110
const cls = iconClass(entry.icon)
@@ -120,6 +169,26 @@ export default function Page() {
120169
// Patch it in place with the returned handle (the id is immutable).
121170
clientDock.update({ badge: clientHost.context.clientType })
122171

172+
// Register a second client-only dock — this one a *json-render* view the
173+
// page authors itself, the richer sibling of the iframe dock above.
174+
// First seed the spec as this client's local value for the dock's
175+
// `stateKey`: because we only pass `initialValue` and never mutate it,
176+
// the spec stays local to this page — it never syncs to the hub server
177+
// or other viewers. The `json-render` dock renderer registered above
178+
// then reads that same state and renders it with the mini React
179+
// registry. `force` lets React StrictMode re-run this effect safely.
180+
await rpc.sharedState.get<DevframeJsonRenderSpec>(CLIENT_JSON_RENDER_KEY, {
181+
initialValue: createClientMetricsSpec(clientHost.context.clientType),
182+
})
183+
const clientJsonRenderDock = clientHost.context.docks.register<DevframeJsonRenderDockEntry>({
184+
id: 'client-metrics',
185+
title: 'Client Metrics',
186+
icon: 'ph:gauge-duotone',
187+
type: 'json-render',
188+
view: { stateKey: CLIENT_JSON_RENDER_KEY, upstreamVersion: JSON_RENDER_UPSTREAM_VERSION },
189+
category: 'app',
190+
}, true)
191+
123192
const docksState = await rpc.sharedState.get<DevframeDockEntry[]>(
124193
'devframe:docks',
125194
{ initialValue: [] },
@@ -165,8 +234,9 @@ export default function Page() {
165234

166235
cleanup = () => {
167236
window.clearInterval(interval)
168-
// Remove the client-only dock, then tear down the host.
237+
// Remove the client-only docks, then tear down the host.
169238
clientDock.dispose()
239+
clientJsonRenderDock.dispose()
170240
clientHost.dispose()
171241
}
172242
}

examples/minimal-vite-devframe-hub/src/client/main.ts

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,10 @@ import type {
55
DevframeTerminalSession,
66
DevframeViewIframe,
77
} from '@devframes/hub/types'
8+
import type { DevframeJsonRenderSpec } from '@devframes/json-render'
9+
import type { DevframeJsonRenderDockEntry } from '@devframes/json-render/hub'
810
import { connectDevframe, createDevframeClientHost } from '@devframes/hub/client'
11+
import { JSON_RENDER_UPSTREAM_VERSION } from '@devframes/json-render'
912
import { createJsonRenderDockRenderer } from '@devframes/json-render-ui'
1013
import { iconClass } from './icons'
1114
import 'virtual:uno.css'
@@ -80,6 +83,52 @@ function createClientNotesUrl(): string {
8083
return URL.createObjectURL(new Blob([html], { type: 'text/html' }))
8184
}
8285

86+
// The shared-state key the client-only json-render dock renders from. It uses a
87+
// `client:` prefix rather than the server's `devframe:json-render:<scope>:<id>`
88+
// namespace to signal it is authored here, not by a node `createJsonRenderView`.
89+
const CLIENT_JSON_RENDER_KEY = 'client:json-render:metrics'
90+
91+
// A json-render spec synthesized entirely in the browser — the client-only
92+
// counterpart to a server-authored view. It reads real values captured from the
93+
// page at registration time and uses the same base-catalog components the hub's
94+
// server view does, rendered by the very same `createJsonRenderDockRenderer`.
95+
function createClientMetricsSpec(clientType: string): DevframeJsonRenderSpec {
96+
return {
97+
root: 'root',
98+
elements: {
99+
root: { type: 'Stack', props: { gap: 14 }, children: ['head', 'about', 'env'] },
100+
head: { type: 'Stack', props: { direction: 'row', gap: 8, align: 'center' }, children: ['icon', 'title', 'badge'] },
101+
icon: { type: 'Icon', props: { name: 'ph:gauge-duotone', size: 22 }, children: [] },
102+
title: { type: 'Text', props: { text: 'Client Metrics', variant: 'heading' }, children: [] },
103+
badge: { type: 'Badge', props: { text: 'client-only', variant: 'info' }, children: [] },
104+
about: { type: 'Card', props: { title: 'About this dock' }, children: ['aboutText'] },
105+
aboutText: {
106+
type: 'Text',
107+
props: {
108+
text: 'This json-render view was authored in the browser and seeded into a client-local shared state — it never reaches the hub server or other viewers, yet renders through the same dock renderer as a server-authored view.',
109+
variant: 'body',
110+
color: 'muted',
111+
},
112+
children: [],
113+
},
114+
env: { type: 'Card', props: { title: 'Environment' }, children: ['envTable'] },
115+
envTable: {
116+
type: 'KeyValueTable',
117+
props: {
118+
data: {
119+
clientType,
120+
language: navigator.language,
121+
viewport: `${window.innerWidth}×${window.innerHeight}`,
122+
online: navigator.onLine ? 'yes' : 'no',
123+
},
124+
},
125+
children: [],
126+
},
127+
},
128+
state: {},
129+
}
130+
}
131+
83132
async function main() {
84133
setStatus('Connecting…')
85134

@@ -115,6 +164,24 @@ async function main() {
115164
// `clientDock.dispose()` to remove it from the merged list again.
116165
clientDock.update({ badge: host.context.clientType })
117166

167+
// Register a second client-only dock — this one a *json-render* view the page
168+
// authors itself, the richer sibling of the iframe dock above. First seed the
169+
// spec as this client's local value for the dock's `stateKey`: because we only
170+
// pass `initialValue` and never mutate it, the spec stays local to this page —
171+
// it never syncs to the hub server or other viewers. The `json-render` dock
172+
// renderer registered above then reads that same state and renders it.
173+
await rpc.sharedState.get<DevframeJsonRenderSpec>(CLIENT_JSON_RENDER_KEY, {
174+
initialValue: createClientMetricsSpec(host.context.clientType),
175+
})
176+
host.context.docks.register<DevframeJsonRenderDockEntry>({
177+
id: 'client-metrics',
178+
title: 'Client Metrics',
179+
icon: 'ph:gauge-duotone',
180+
type: 'json-render',
181+
view: { stateKey: CLIENT_JSON_RENDER_KEY, upstreamVersion: JSON_RENDER_UPSTREAM_VERSION },
182+
category: 'app',
183+
})
184+
118185
// 1. Docks — the merged list from the client host: server docks (projected
119186
// from `devframe:docks` shared state) plus the client-only dock above. We
120187
// still subscribe to the shared state to re-render when server docks change.

0 commit comments

Comments
 (0)