Skip to content

Commit c7ef7ec

Browse files
committed
docs(examples): demo client-only docks in the vite and next hubs
Register a client-only "Client Notes" dock on the client host context in both minimal hub examples, source the dock list from the merged context.docks.entries, and show update()/dispose() on the returned handle.
1 parent ae7e638 commit c7ef7ec

2 files changed

Lines changed: 91 additions & 4 deletions

File tree

  • examples
    • minimal-next-devframe-hub/src/client/app
    • minimal-vite-devframe-hub/src/client

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

Lines changed: 47 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import type {
66
DevframeDockEntry,
77
DevframeMessageEntry,
88
DevframeTerminalSession,
9+
DevframeViewIframe,
910
} from '@devframes/hub/types'
1011
import { connectDevframe, createDevframeClientHost } from '@devframes/hub/client'
1112
import { useEffect, useMemo, useRef, useState } from 'react'
@@ -34,6 +35,27 @@ function isRenderableDock(d: DevframeDockEntry): boolean {
3435
return isIframeDock(d) || RENDERER_TYPES.has(d.type)
3536
}
3637

38+
// A self-contained document for the client-only dock, rendered from a Blob URL
39+
// so the whole dock is synthesized in the browser with no server route.
40+
function createClientNotesUrl(): string {
41+
const html = `<!doctype html><meta charset="utf-8">
42+
<style>
43+
:root { color-scheme: light dark; }
44+
body { margin: 0; padding: 24px; font: 14px/1.6 system-ui, sans-serif; }
45+
h1 { margin: 0 0 8px; font-size: 16px; }
46+
p { max-width: 54ch; opacity: .85; }
47+
code { padding: 1px 5px; border-radius: 4px; background: rgba(127,127,127,.18); font-size: 12px; }
48+
</style>
49+
<h1>Client-only dock</h1>
50+
<p>This dock was registered in the browser with
51+
<code>host.context.docks.register()</code>. It lives only in this page — it
52+
never enters the <code>devframe:docks</code> shared state, so it is not synced
53+
to the hub server or to any other connected viewer.</p>
54+
<p>Patch it live through the returned handle with <code>update()</code> (its
55+
<code>badge</code> was set that way), or remove it with <code>dispose()</code>.</p>`
56+
return URL.createObjectURL(new Blob([html], { type: 'text/html' }))
57+
}
58+
3759
/** Render a dock icon, falling back to the title's initial when unmapped. */
3860
function DockIcon({ entry }: { entry: DevframeDockEntry }) {
3961
const cls = iconClass(entry.icon)
@@ -80,6 +102,24 @@ export default function Page() {
80102
})
81103
hostRef.current = clientHost
82104

105+
// Register a *client-only* dock — one this page synthesizes itself.
106+
// Unlike the server-authored docks, it's registered on the client host
107+
// context, so it never enters the `devframe:docks` shared state: it
108+
// stays local to this page and is not synced to the hub server or other
109+
// viewers. It merges into `clientHost.context.docks.entries` alongside
110+
// the server docks. `force` lets React StrictMode re-run this effect
111+
// without tripping the duplicate-id guard.
112+
const clientDock = clientHost.context.docks.register<DevframeViewIframe>({
113+
id: 'client-notes',
114+
title: 'Client Notes',
115+
icon: 'ph:note-pencil-duotone',
116+
type: 'iframe',
117+
url: createClientNotesUrl(),
118+
category: 'app',
119+
}, true)
120+
// Patch it in place with the returned handle (the id is immutable).
121+
clientDock.update({ badge: clientHost.context.clientType })
122+
83123
const docksState = await rpc.sharedState.get<DevframeDockEntry[]>(
84124
'devframe:docks',
85125
{ initialValue: [] },
@@ -89,7 +129,10 @@ export default function Page() {
89129
{ initialValue: [] },
90130
)
91131

92-
const renderDocks = () => setDocks([...(docksState.value() ?? [])] as DevframeDockEntry[])
132+
// The merged list from the client host: server docks (projected from
133+
// `devframe:docks` shared state) plus the client-only dock above. We
134+
// still subscribe to the shared state to re-render on server changes.
135+
const renderDocks = () => setDocks([...clientHost.context.docks.entries])
93136
const renderCommands = () => setCommands([...(commandsState.value() ?? [])] as DevframeCommandEntry[])
94137
docksState.on('updated', renderDocks)
95138
commandsState.on('updated', renderCommands)
@@ -122,6 +165,8 @@ export default function Page() {
122165

123166
cleanup = () => {
124167
window.clearInterval(interval)
168+
// Remove the client-only dock, then tear down the host.
169+
clientDock.dispose()
125170
clientHost.dispose()
126171
}
127172
}
@@ -224,6 +269,7 @@ export default function Page() {
224269
>
225270
<DockIcon entry={dock} />
226271
<span className="truncate">{dock.title}</span>
272+
{dock.badge && <span className="ml-auto shrink-0 rounded bg-active px1 py0.5 text-[0.6rem] font-mono color-base">{dock.badge}</span>}
227273
</button>
228274
</li>
229275
))}

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

Lines changed: 44 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import type {
33
DevframeDockEntry,
44
DevframeMessageEntry,
55
DevframeTerminalSession,
6+
DevframeViewIframe,
67
} from '@devframes/hub/types'
78
import { connectDevframe, createDevframeClientHost } from '@devframes/hub/client'
89
import { createJsonRenderDockRenderer } from '@devframes/json-render-ui'
@@ -58,6 +59,27 @@ function isRenderableDock(d: DevframeDockEntry): boolean {
5859
return isIframeDock(d) || RENDERER_TYPES.has(d.type)
5960
}
6061

62+
// A self-contained document for the client-only dock, rendered from a Blob URL
63+
// so the whole dock is synthesized in the browser with no server route.
64+
function createClientNotesUrl(): string {
65+
const html = `<!doctype html><meta charset="utf-8">
66+
<style>
67+
:root { color-scheme: light dark; }
68+
body { margin: 0; padding: 24px; font: 14px/1.6 system-ui, sans-serif; }
69+
h1 { margin: 0 0 8px; font-size: 16px; }
70+
p { max-width: 54ch; opacity: .85; }
71+
code { padding: 1px 5px; border-radius: 4px; background: rgba(127,127,127,.18); font-size: 12px; }
72+
</style>
73+
<h1>Client-only dock</h1>
74+
<p>This dock was registered in the browser with
75+
<code>host.context.docks.register()</code>. It lives only in this page — it
76+
never enters the <code>devframe:docks</code> shared state, so it is not synced
77+
to the hub server or to any other connected viewer.</p>
78+
<p>Patch it live through the returned handle with <code>update()</code> (its
79+
<code>badge</code> was set that way), or remove it with <code>dispose()</code>.</p>`
80+
return URL.createObjectURL(new Blob([html], { type: 'text/html' }))
81+
}
82+
6183
async function main() {
6284
setStatus('Connecting…')
6385

@@ -76,7 +98,26 @@ async function main() {
7698
renderers: { 'json-render': createJsonRenderDockRenderer() },
7799
})
78100

79-
// 1. Docks — read from `devframe:docks` shared state.
101+
// Register a *client-only* dock — one this page synthesizes itself. Unlike
102+
// the server-authored docks, it's registered on the client host context, so
103+
// it never enters the `devframe:docks` shared state: it stays local to this
104+
// page and is not synced to the hub server or other viewers. It merges into
105+
// `host.context.docks.entries` (read below) alongside the server docks.
106+
const clientDock = host.context.docks.register<DevframeViewIframe>({
107+
id: 'client-notes',
108+
title: 'Client Notes',
109+
icon: 'ph:note-pencil-duotone',
110+
type: 'iframe',
111+
url: createClientNotesUrl(),
112+
category: 'app',
113+
})
114+
// Patch it in place with the returned handle (the id is immutable). Call
115+
// `clientDock.dispose()` to remove it from the merged list again.
116+
clientDock.update({ badge: host.context.clientType })
117+
118+
// 1. Docks — the merged list from the client host: server docks (projected
119+
// from `devframe:docks` shared state) plus the client-only dock above. We
120+
// still subscribe to the shared state to re-render when server docks change.
80121
const docks = await rpc.sharedState.get<DevframeDockEntry[]>(
81122
'devframe:docks',
82123
{ initialValue: [] },
@@ -119,7 +160,7 @@ async function main() {
119160
}
120161

121162
const renderDocks = () => {
122-
const list = (docks.value() ?? []).filter(isRenderableDock)
163+
const list = host.context.docks.entries.filter(isRenderableDock)
123164

124165
if (selectedDockId && !list.some(d => d.id === selectedDockId))
125166
selectedDockId = null
@@ -136,7 +177,7 @@ async function main() {
136177
}
137178

138179
renderList(docksEl, list, d =>
139-
`<li><button type="button" data-dock-id="${d.id}" class="relative inline-flex items-center gap-1.5 max-w-52 px-2 py-1 rounded-md border border-transparent text-sm op-fade select-none cursor-pointer transition hover:op100 hover:bg-active w-full! max-w-none! gap-2.5!${d.id === selectedDockId ? ' op100! bg-active border-base! color-base' : ''}" title="${d.title}">${dockIcon(d)}<span class="truncate">${d.title}</span></button></li>`)
180+
`<li><button type="button" data-dock-id="${d.id}" class="relative inline-flex items-center gap-1.5 max-w-52 px-2 py-1 rounded-md border border-transparent text-sm op-fade select-none cursor-pointer transition hover:op100 hover:bg-active w-full! max-w-none! gap-2.5!${d.id === selectedDockId ? ' op100! bg-active border-base! color-base' : ''}" title="${d.title}">${dockIcon(d)}<span class="truncate">${d.title}</span>${d.badge ? `<span class="ml-auto shrink-0 rounded bg-active px1 py0.5 text-[0.6rem] font-mono color-base">${d.badge}</span>` : ''}</button></li>`)
140181

141182
void applySelection(list)
142183
}

0 commit comments

Comments
 (0)